const fs = require("fs"); const path = require("path"); const { resolveData } = require("./paths"); const { registerManagedTool } = require("./tool_registry"); class ToolLoader { constructor(options = {}) { this.registry = options.registry; this.installer = options.installer; this.settings = options.settings; this.lumiAiVersion = options.lumiAiVersion || "0.0.0"; this.lumiVersion = options.lumiVersion || "0.0.0"; this.stateFile = options.stateFile || resolveData("tools", "enabled.json"); this.loaded = new Map(); this.statuses = new Map(); } async loadEnabled() { return this.reconcile(); } async reconcile({ reload = false } = {}) { const state = this.readState(); const locals = this.installer.scanLocal(); const localMap = new Map(locals.map((local) => [local.tool_id, local])); for (const local of locals) { if (local.valid && state.enabled[local.tool_id] == null && local.metadata.default_enabled === true) { state.enabled[local.tool_id] = true; this.writeState(state); } } for (const toolId of [...this.loaded.keys()]) { if (!localMap.has(toolId) || state.enabled[toolId] !== true) { await this.disable(toolId, { persist: false }); } } for (const local of locals) { if (!local.valid) { this.registry.unregisterOwner(local.tool_id); this.loaded.delete(local.tool_id); this.setStatus(local.tool_id, "unavailable", local.error, { blocking: ["schema_invalid"], optional: [] }); continue; } if (state.enabled[local.tool_id] !== true) { this.setStatus(local.tool_id, "disabled", ""); continue; } const dependencies = this.inspectDependencies(local.metadata, local.dir); if (dependencies.blocking.length) { await this.disable(local.tool_id, { persist: false }); this.setStatus( local.tool_id, "unavailable", `Unavailable: ${dependencies.blocking.join("; ")}`, dependencies ); continue; } let signature; try { signature = sourceSignature(local); } catch (error) { await this.disable(local.tool_id, { persist: false }); this.setStatus(local.tool_id, "unavailable", error.message, dependencies); continue; } if (!reload && this.loaded.get(local.tool_id)?.source_signature === signature) continue; try { await this.enable(local.tool_id, { persist: false }); } catch (error) { this.setStatus(local.tool_id, "unavailable", error.message); } } return this.diagnostics(); } async enable(toolId, options = {}) { const local = this.installer.local(toolId); if (!local?.valid) throw new Error(local?.error || "Installed AI tool metadata is invalid."); const dependencies = this.inspectDependencies(local.metadata, local.dir); if (dependencies.blocking.length) { await this.disable(toolId, { persist: false }); const message = `Unavailable: ${dependencies.blocking.join("; ")}`; this.setStatus(toolId, "unavailable", message, dependencies); if (options.persist !== false) this.setEnabled(toolId, true); return { loaded: false, unavailable: true, message, dependencies }; } await this.disable(toolId, { persist: false }); let backend; try { backend = backendEntrypoint(local.metadata, local.dir); } catch (error) { this.setStatus(toolId, "unavailable", error.message, dependencies); if (options.persist !== false) this.setEnabled(toolId, true); return { loaded: false, unavailable: true, message: error.message, dependencies }; } const registered = []; let cleanup = null; let runtimeDiagnostics = null; let availabilityMessage = ""; if (backend) { clearRequireCache(local.dir); try { const module = require(backend); const register = module?.register || module?.init; if (typeof register !== "function") throw new Error("Backend entrypoint must export register() or init()."); const context = { metadata: Object.freeze({ ...local.metadata }), registerTool: (definition) => { const unregister = registerManagedTool(this.registry, local.metadata, definition); registered.push({ id: definition.tool_id, unregister }); return unregister; }, paths: Object.freeze({ root: local.dir, data: path.join(local.dir, "data"), config: path.join(local.dir, "config") }), assetUrl: (relative = "") => `/plugins/lumi_ai/tools/${toolId}/assets/${String(relative).replace(/^\/+/, "")}` }; if (typeof module.checkAvailability === "function") { const availability = await module.checkAvailability(context); if (availability?.available === false) { this.loaded.set(toolId, { cleanup: null, registered, metadata: local.metadata, dir: local.dir, source_signature: sourceSignature(local) }); this.setStatus(toolId, "unavailable", String(availability.message || "Tool configuration is incomplete."), dependencies); if (options.persist !== false) this.setEnabled(toolId, true); return { loaded: false, unavailable: true, message: availability.message, dependencies }; } availabilityMessage = String(availability?.message || ""); } const result = await register(context); cleanup = typeof result === "function" ? result : typeof result?.stop === "function" ? () => result.stop() : null; runtimeDiagnostics = typeof module.diagnostics === "function" ? () => module.diagnostics(context) : null; } catch (error) { this.registry.unregisterOwner(toolId); this.setStatus(toolId, "unavailable", error.message, dependencies); if (options.persist !== false) this.setEnabled(toolId, true); return { loaded: false, unavailable: true, message: error.message, dependencies }; } } this.loaded.set(toolId, { cleanup, registered, metadata: local.metadata, dir: local.dir, diagnostics: runtimeDiagnostics, source_signature: sourceSignature(local) }); this.setStatus( toolId, "enabled", dependencies.optional.length ? `Enabled with limitations: ${dependencies.optional.join("; ")}` : availabilityMessage, dependencies ); if (options.persist !== false) this.setEnabled(toolId, true); return { loaded: true, registered: registered.map((entry) => entry.id), dependencies }; } async disable(toolId, options = {}) { const loaded = this.loaded.get(toolId); if (loaded) { try { await loaded.cleanup?.(); } catch {} this.registry.unregisterOwner(toolId); this.loaded.delete(toolId); } else { this.registry.unregisterOwner(toolId); } if (options.persist !== false) this.setEnabled(toolId, false); this.setStatus(toolId, "disabled", ""); return { disabled: true }; } async stopAll() { for (const toolId of [...this.loaded.keys()]) await this.disable(toolId, { persist: false }); } isEnabled(toolId) { return this.readState().enabled[toolId] === true; } status(toolId) { return this.statuses.get(toolId) || { state: this.isEnabled(toolId) ? "pending" : "disabled", message: "" }; } diagnostics() { return this.installer.scanLocal().map((local) => { const enabled = this.isEnabled(local.tool_id); const status = this.status(local.tool_id); return { tool_id: local.tool_id, installed: true, enabled, valid: local.valid, state: status.state, message: status.message || local.error || "", dependencies: status.dependencies || (local.valid ? this.inspectDependencies(local.metadata, local.dir) : { blocking: ["schema_invalid"], optional: [] }), registered_tools: [...this.registry.tools.values()] .filter((definition) => definition.owning_plugin === local.tool_id) .map((definition) => definition.tool_id), runtime_details: safeDiagnostics(this.loaded.get(local.tool_id)?.diagnostics) }; }); } inspectDependencies(metadata, toolDir) { const blocking = []; const optional = []; for (const pluginId of metadata.required_plugins || []) { if (/^lumi_ai_.+/i.test(pluginId)) { blocking.push(`AI tool plugins cannot depend on AI tool plugin ${pluginId}`); } else if (!["core", "lumi_ai"].includes(pluginId) && !fs.existsSync(path.resolve(toolDir, "..", pluginId))) { blocking.push(`required plugin ${pluginId} is missing`); } } for (const platform of metadata.required_platforms || []) { if (this.settings?.getSetting?.(`platform_${platform}_enabled`, false) !== true) { blocking.push(`required platform ${platform} is disabled`); } } for (const dependency of metadata.dependencies || []) { try { require.resolve(dependency, { paths: [toolDir] }); } catch { optional.push(`optional dependency ${dependency} is missing`); } } if (metadata.minimum_lumi_ai_version && compareVersions(this.lumiAiVersion, metadata.minimum_lumi_ai_version) < 0) { blocking.push(`requires Lumi AI ${metadata.minimum_lumi_ai_version}`); } if (metadata.minimum_lumi_version && compareVersions(this.lumiVersion, metadata.minimum_lumi_version) < 0) { blocking.push(`requires Lumi ${metadata.minimum_lumi_version}`); } return { blocking, optional }; } resolveAsset(toolId, relative) { const loaded = this.loaded.get(toolId); if (!loaded || !this.isEnabled(toolId)) return null; const roots = assetRoots(loaded.metadata, loaded.dir); for (const root of roots) { const candidate = path.resolve(root, String(relative || "")); if ((candidate === root || candidate.startsWith(`${root}${path.sep}`)) && fs.existsSync(candidate) && fs.statSync(candidate).isFile()) { return candidate; } } return null; } setStatus(toolId, state, message, dependencies = null) { this.statuses.set(toolId, { state, message: String(message || ""), dependencies }); } readState() { try { const value = JSON.parse(fs.readFileSync(this.stateFile, "utf8")); return { enabled: value.enabled && typeof value.enabled === "object" ? value.enabled : {} }; } catch { return { enabled: {} }; } } setEnabled(toolId, enabled) { const state = this.readState(); state.enabled[toolId] = Boolean(enabled); this.writeState(state); } writeState(state) { fs.mkdirSync(path.dirname(this.stateFile), { recursive: true }); const temporary = `${this.stateFile}.${process.pid}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`); try { fs.renameSync(temporary, this.stateFile); } catch (error) { if (!["EEXIST", "EPERM"].includes(error.code)) throw error; fs.rmSync(this.stateFile, { force: true }); fs.renameSync(temporary, this.stateFile); } } } function backendEntrypoint(metadata, toolDir) { const configured = typeof metadata.entrypoints === "string" ? metadata.entrypoints : metadata.entrypoints?.backend; const relative = configured || (fs.existsSync(path.join(toolDir, "index.js")) ? "index.js" : ""); if (!relative) return null; const target = path.resolve(toolDir, relative); if (!target.startsWith(`${path.resolve(toolDir)}${path.sep}`) || !fs.existsSync(target)) { throw new Error("Backend entrypoint is missing or unsafe."); } return target; } function assetRoots(metadata, toolDir) { const entries = Array.isArray(metadata.frontend_assets) ? metadata.frontend_assets : metadata.frontend_assets ? [metadata.frontend_assets] : ["public"]; return entries.map((entry) => path.resolve(toolDir, entry)) .filter((entry) => entry.startsWith(`${path.resolve(toolDir)}${path.sep}`) && fs.existsSync(entry)); } function clearRequireCache(directory) { const prefix = `${path.resolve(directory)}${path.sep}`; for (const key of Object.keys(require.cache)) if (key.startsWith(prefix)) delete require.cache[key]; } function compareVersions(left, right) { const parse = (value) => String(value || "0.0.0").split(/[+-]/)[0].split(".").map((part) => Number.parseInt(part, 10) || 0); const a = parse(left); const b = parse(right); for (let index = 0; index < Math.max(a.length, b.length, 3); index += 1) { if ((a[index] || 0) !== (b[index] || 0)) return (a[index] || 0) > (b[index] || 0) ? 1 : -1; } return 0; } function sourceSignature(local) { const entry = backendEntrypoint(local.metadata, local.dir); const entryMtime = entry && fs.existsSync(entry) ? fs.statSync(entry).mtimeMs : 0; const metadataFile = path.join(local.dir, "tool_info.json"); const metadataMtime = fs.existsSync(metadataFile) ? fs.statSync(metadataFile).mtimeMs : 0; return `${local.metadata.version}:${metadataMtime}:${entryMtime}`; } function safeDiagnostics(callback) { if (typeof callback !== "function") return null; try { const value = callback(); return value && typeof value === "object" && !Array.isArray(value) ? value : null; } catch (error) { return { error: String(error?.message || "Tool diagnostics failed.") }; } } module.exports = { ToolLoader, assetRoots, backendEntrypoint, clearRequireCache, compareVersions, safeDiagnostics, sourceSignature };