Lumi/plugins/lumi_ai/backend/tool_manager.js
2026-06-13 21:32:36 +02:00

152 lines
5.5 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const { compareVersions } = require("./tool_loader");
class ToolManager {
constructor(options = {}) {
this.repoClient = options.repoClient;
this.installer = options.installer;
this.loader = options.loader;
this.settings = options.settings;
}
async list({ force = false } = {}) {
const remoteResult = await this.repoClient.discover({ force });
const localRows = this.installer.scanLocal();
const remoteMap = new Map(remoteResult.tools.map((metadata) => [metadata.tool_id, metadata]));
const localMap = new Map(localRows.map((entry) => [entry.tool_id, entry]));
const ids = [...new Set([...remoteMap.keys(), ...localMap.keys()])].sort();
return {
repository: remoteResult.repository,
branch: remoteResult.branch,
checked_at: remoteResult.checked_at,
cached: remoteResult.cached,
stale: remoteResult.stale,
error: remoteResult.error || null,
tools: ids.map((toolId) => {
const remote = remoteMap.get(toolId) || null;
const local = localMap.get(toolId) || null;
const metadata = local?.metadata || remote;
const installed = Boolean(local);
const remoteMissing = installed && !remote;
const enabled = installed && this.loader.isEnabled(toolId);
const runtime = this.loader.status(toolId);
const updateAvailable = Boolean(
installed && remote && local.valid && !remote.remote_invalid &&
compareVersions(remote.version, local.metadata.version) > 0
);
const dependencies = local?.valid ? this.loader.inspectDependencies(local.metadata, local.dir) : { blocking: [], optional: [] };
return {
...metadata,
installed,
enabled,
local_version: installed ? local.metadata.version : null,
remote_version: remote?.version || null,
update_available: updateAvailable,
update_enabled: installed && Boolean(remote) && !remote?.remote_invalid,
remote_missing: remoteMissing,
local_only: remoteMissing,
local_valid: local?.valid !== false,
local_error: local?.error || null,
remote_invalid: remote?.remote_invalid === true,
remote_error: remote?.remote_error || null,
runtime_state: runtime.state,
runtime_message: runtime.message,
dependency_status: dependencies,
primary_type: metadata?.tool_type || "general",
primary_scope: displayScope(metadata?.scope),
has_settings: installed && Boolean(metadata?.settings_schema && Object.keys(metadata.settings_schema).length)
};
})
};
}
async enable(toolId) {
let installed = this.installer.local(toolId);
if (!installed) {
const remote = await this.repoClient.discover();
if (!remote.tools.some((entry) => entry.tool_id === toolId && !entry.remote_invalid)) {
throw new Error("Remote AI tool plugin is unavailable.");
}
await this.installer.install(toolId);
installed = this.installer.local(toolId);
}
return this.loader.enable(toolId);
}
async disable(toolId) {
return this.loader.disable(toolId);
}
async update(toolId) {
const local = this.installer.local(toolId);
if (!local) throw new Error("Install the AI tool plugin before updating it.");
const remote = await this.repoClient.discover({ force: true });
if (!remote.tools.some((entry) => entry.tool_id === toolId && !entry.remote_invalid)) {
throw new Error("This installed tool is missing from the configured repository.");
}
const wasEnabled = this.loader.isEnabled(toolId);
if (wasEnabled) await this.loader.disable(toolId, { persist: false });
try {
const result = await this.installer.update(toolId);
if (wasEnabled) await this.loader.enable(toolId, { persist: false });
this.loader.setEnabled(toolId, wasEnabled);
return result;
} catch (error) {
if (wasEnabled && this.installer.local(toolId)) {
try { await this.loader.enable(toolId, { persist: false }); } catch {}
this.loader.setEnabled(toolId, true);
}
throw error;
}
}
async delete(toolId) {
await this.loader.disable(toolId);
const deleted = this.installer.delete(toolId);
this.loader.setEnabled(toolId, false);
return { deleted };
}
async readme(toolId) {
const local = this.installer.local(toolId);
if (local) {
const file = path.join(local.dir, "readme.md");
if (!fs.existsSync(file)) throw new Error("Installed tool readme.md is missing.");
return { markdown: fs.readFileSync(file, "utf8"), source: "local" };
}
return { markdown: await this.repoClient.readReadme(toolId), source: "remote" };
}
settingsFor(toolId) {
return this.settings.describe(toolId);
}
async saveSettings(toolId, values) {
const saved = this.settings.save(toolId, values);
if (this.loader.isEnabled(toolId)) await this.loader.enable(toolId, { persist: false });
return saved;
}
async loadEnabled() {
return this.loader.loadEnabled();
}
async stopAll() {
return this.loader.stopAll();
}
resolveAsset(toolId, relative) {
return this.loader.resolveAsset(toolId, relative);
}
}
function displayScope(scope) {
if (typeof scope === "string") return scope;
if (Array.isArray(scope)) return scope.join(", ");
if (scope && typeof scope === "object") return scope.label || scope.required_role || JSON.stringify(scope);
return "unspecified";
}
module.exports = { ToolManager, displayScope };