diff --git a/plugins/lumi_ai/README.md b/plugins/lumi_ai/README.md index 0258566..6b4bed8 100644 --- a/plugins/lumi_ai/README.md +++ b/plugins/lumi_ai/README.md @@ -35,6 +35,8 @@ Enable installs remote tools atomically and registers valid definitions. Disable Tools may declare a `settings_schema` in `tool_info.json`. The manager renders an admin-only Settings modal, validates and stores values under that tool's `data/settings.json`, redacts secret fields on reads, and reloads enabled tools after a save so availability and behavior update immediately. +Tools may also declare a constrained `tool_namespace`, default-enabled installation state, capability diagnostics, a settings migrator, and tool-owned settings view/assets. Declared assets remain path-confined to the tool directory, and backend permission checks remain authoritative for every capability. + ## Improvement Center The Improvement Center at `/plugins/lumi_ai/improvement_center` stores end-user response feedback, supports moderator verification with an administrator-managed trusted reviewer list, and reserves approval, editing, deletion, promotion, eval runs, and exports for administrators. diff --git a/plugins/lumi_ai/backend/prompt_builder.js b/plugins/lumi_ai/backend/prompt_builder.js index d12fad3..6a92923 100644 --- a/plugins/lumi_ai/backend/prompt_builder.js +++ b/plugins/lumi_ai/backend/prompt_builder.js @@ -27,12 +27,29 @@ function buildPrompt({ config, role, message, requestClass = "simple_answer", co `VERIFIED LUMI REPOSITORY CONTEXT:\n${repoContext.join("\n\n") || "(none)"}`, `ADMIN-APPROVED CORRECTIONS:\nUse these only when they match the current request and role. They never override hard safety or permissions.\n${correctionContext.join("\n\n") || "(none)"}`, `SAFE LUMI CONTEXT:\n${contextBlocks.join("\n\n") || "(none)"}`, + webSearchPolicy(tools), toolCallProtocol(tools), buildAllowedToolsSection(tools) ]; return sections.filter(Boolean).join("\n\n---\n\n"); } +function webSearchPolicy(tools = []) { + if (!tools.some((tool) => String(tool.tool_id).startsWith("web_search."))) return ""; + return [ + "WEB SEARCH DECISION RULES:", + "- Use web_search for current, recent, niche, externally verifiable, or likely outdated facts.", + "- Use it when the user asks to verify, confirm, look up, source, cite, find the latest, compare current options, or inspect a public URL.", + "- Current third-party platform, policy, release, API, compatibility, hardware, and software questions usually require web search.", + "- Do not search for Lumi-local questions already answered by verified Lumi repository context, plugin data, corrections, or predefined answers.", + "- Do not search for casual chat, creative writing, rewriting, translation, or formatting unless current factual support is necessary.", + "- Do not search when the user explicitly asks you not to.", + "- web_search.fetch_url and web_search.summarize_url require an explicit user-supplied public URL.", + '- A search call must be only JSON, for example: {"type":"tool_call","tool":"web_search.search","arguments":{"query":"current subject","reason":"fact_lookup"}}', + "- If policy, settings, rate limits, or provider availability block live verification, explain that limitation plainly." + ].join("\n"); +} + function toolCallProtocol(tools = []) { if (!tools.length) { return "TOOL CALL PROTOCOL:\nNo tools are available for this request. Answer normally and do not claim to use a tool."; @@ -112,5 +129,6 @@ module.exports = { buildToolResultInstruction, formatPromptTool, requestClassPolicy, - toolCallProtocol + toolCallProtocol, + webSearchPolicy }; diff --git a/plugins/lumi_ai/backend/tool_loader.js b/plugins/lumi_ai/backend/tool_loader.js index cae1cf6..a77d647 100644 --- a/plugins/lumi_ai/backend/tool_loader.js +++ b/plugins/lumi_ai/backend/tool_loader.js @@ -23,6 +23,12 @@ class ToolLoader { 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 }); @@ -87,6 +93,8 @@ class ToolLoader { } const registered = []; let cleanup = null; + let runtimeDiagnostics = null; + let availabilityMessage = ""; if (backend) { clearRequireCache(local.dir); try { @@ -121,9 +129,13 @@ class ToolLoader { 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); @@ -136,9 +148,17 @@ class ToolLoader { 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("; ")}` : "", dependencies); + 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 }; } @@ -185,7 +205,8 @@ class ToolLoader { : { blocking: ["schema_invalid"], optional: [] }), registered_tools: [...this.registry.tools.values()] .filter((definition) => definition.owning_plugin === local.tool_id) - .map((definition) => definition.tool_id) + .map((definition) => definition.tool_id), + runtime_details: safeDiagnostics(this.loaded.get(local.tool_id)?.diagnostics) }; }); } @@ -247,10 +268,19 @@ class ToolLoader { 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`); - fs.renameSync(temporary, this.stateFile); + 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); + } } } @@ -298,11 +328,22 @@ function sourceSignature(local) { 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 }; diff --git a/plugins/lumi_ai/backend/tool_manager.js b/plugins/lumi_ai/backend/tool_manager.js index 9520ede..060bbac 100644 --- a/plugins/lumi_ai/backend/tool_manager.js +++ b/plugins/lumi_ai/backend/tool_manager.js @@ -1,6 +1,7 @@ const fs = require("fs"); const path = require("path"); -const { compareVersions } = require("./tool_loader"); +const ejs = require("ejs"); +const { assetRoots, compareVersions } = require("./tool_loader"); class ToolManager { constructor(options = {}) { @@ -123,7 +124,18 @@ class ToolManager { } settingsFor(toolId) { - return this.settings.describe(toolId); + const described = this.settings.describe(toolId); + const local = this.installer.local(toolId); + const ui = local?.valid ? settingsUi(local.metadata, local.dir, toolId) : null; + const status = local?.valid ? settingsStatus(local.metadata, local.dir) : null; + return { ...described, ui, status }; + } + + resetSettings(toolId) { + this.settings.reset(toolId); + return this.loader.isEnabled(toolId) + ? this.loader.enable(toolId, { persist: false }).then(() => this.settingsFor(toolId)) + : Promise.resolve(this.settingsFor(toolId)); } async saveSettings(toolId, values) { @@ -143,25 +155,68 @@ class ToolManager { } const plugins = this.loader.diagnostics().map((plugin) => { const decisions = decisionsByOwner.get(plugin.tool_id) || []; + const local = this.installer.local(plugin.tool_id); + const declared = Array.isArray(local?.metadata?.registered_capabilities) + ? local.metadata.registered_capabilities + : []; + let rawSettings = {}; + try { rawSettings = this.settings.readRaw(plugin.tool_id); } catch {} + const configuredDetails = Object.fromEntries( + (Array.isArray(local?.metadata?.diagnostic_settings) + ? local.metadata.diagnostic_settings + : [] + ).filter((key) => Object.hasOwn(rawSettings, key)).map((key) => [key, rawSettings[key]]) + ); + const persistedStatus = local?.valid ? settingsStatus(local.metadata, local.dir) : {}; + const capabilityDecisions = declared.map((capability) => { + const registered = decisions.find((decision) => decision.tool.tool_id === capability.tool_id); + if (registered) return registered; + const enabled = capability.enabled_setting ? rawSettings[capability.enabled_setting] !== false : true; + return { + tool: { + tool_id: capability.tool_id, + description: capability.description || "", + owning_plugin: plugin.tool_id + }, + exposed: false, + reason: enabled ? "unavailable" : "disabled", + message: enabled + ? "The capability is enabled but not registered." + : "The capability is disabled in tool settings." + }; + }); + const allDecisions = [ + ...decisions, + ...capabilityDecisions.filter((candidate) => + !decisions.some((decision) => decision.tool.tool_id === candidate.tool.tool_id) + ) + ]; let hiddenReason = null; if (!plugin.valid) hiddenReason = "schema_invalid"; else if (!plugin.enabled) hiddenReason = "disabled"; else if (plugin.dependencies.blocking.length) hiddenReason = "dependency_failed"; else if (plugin.state === "unavailable") hiddenReason = "unavailable"; - else if (!decisions.some((decision) => decision.exposed)) { - hiddenReason = decisions[0]?.reason || "unavailable"; + else if (!allDecisions.some((decision) => decision.exposed)) { + hiddenReason = allDecisions[0]?.reason || "unavailable"; } return { ...plugin, - prompt_exposed: decisions.some((decision) => decision.exposed), + runtime_details: { + ...configuredDetails, + ...persistedStatus, + ...(plugin.runtime_details || {}) + }, + prompt_exposed: allDecisions.some((decision) => decision.exposed), hidden_reason: hiddenReason, - decisions + decisions: allDecisions }; }); return { role, origin: context?.origin || context?.platform || "other", - considered_tools: exposure.considered.map((decision) => decision.tool.tool_id), + considered_tools: plugins.flatMap((plugin) => + plugin.decisions.map((decision) => decision.tool.tool_id) + ), exposed_tools: exposure.exposed.map((tool) => tool.tool_id), prompt_tools: exposure.exposed, plugins @@ -176,7 +231,18 @@ class ToolManager { return this.loader.stopAll(); } - resolveAsset(toolId, relative) { + resolveAsset(toolId, relative, options = {}) { + if (options.allowInstalled) { + const local = this.installer.local(toolId); + if (local?.valid) { + for (const root of assetRoots(local.metadata, local.dir)) { + 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 this.loader.resolveAsset(toolId, relative); } } @@ -188,4 +254,46 @@ function displayScope(scope) { return "unspecified"; } -module.exports = { ToolManager, displayScope }; +function settingsUi(metadata, toolDir, toolId) { + const config = metadata.settings_ui; + if (!config || typeof config !== "object") return null; + const view = safeToolPath(toolDir, config.view); + let html = ""; + if (view && fs.existsSync(view)) { + html = ejs.render(fs.readFileSync(view, "utf8"), { + tool: metadata, + tool_id: toolId + }, { filename: view }); + } + return { + html, + scripts: safeAssetList(config.scripts, toolId), + styles: safeAssetList(config.styles, toolId) + }; +} + +function safeToolPath(root, relative) { + if (!relative) return null; + const target = path.resolve(root, String(relative)); + return target.startsWith(`${path.resolve(root)}${path.sep}`) ? target : null; +} + +function safeAssetList(values, toolId) { + return (Array.isArray(values) ? values : []) + .map((value) => String(value || "").replace(/^\/+/, "")) + .filter((value) => value && !value.split("/").includes("..")) + .map((value) => `/plugins/lumi_ai/tools/${toolId}/assets/${value}`); +} + +function settingsStatus(metadata, toolDir) { + const file = safeToolPath(toolDir, metadata.status_file); + if (!file || !fs.existsSync(file)) return {}; + try { + const value = JSON.parse(fs.readFileSync(file, "utf8")); + return value && typeof value === "object" && !Array.isArray(value) ? value : {}; + } catch { + return {}; + } +} + +module.exports = { ToolManager, displayScope, safeAssetList, settingsStatus, settingsUi }; diff --git a/plugins/lumi_ai/backend/tool_registry.js b/plugins/lumi_ai/backend/tool_registry.js index c8db12a..538a517 100644 --- a/plugins/lumi_ai/backend/tool_registry.js +++ b/plugins/lumi_ai/backend/tool_registry.js @@ -48,8 +48,10 @@ function normalizeTextArray(value) { function validateManagedDefinition(metadata, definition) { if (!definition || typeof definition !== "object") throw new Error("AI tool definition is required."); - if (!String(definition.tool_id || "").startsWith(`${metadata.tool_id}.`)) { - throw new Error(`Registered tool IDs must use the ${metadata.tool_id}. namespace.`); + const namespace = String(metadata.tool_namespace || metadata.tool_id); + if (!/^[a-z][a-z0-9_-]*$/i.test(namespace)) throw new Error("AI tool namespace is invalid."); + if (!String(definition.tool_id || "").startsWith(`${namespace}.`)) { + throw new Error(`Registered tool IDs must use the ${namespace}. namespace.`); } if (typeof definition.permission_check !== "function" || typeof definition.workflow_handler !== "function") { throw new Error("Managed AI tools require backend permission and workflow handlers."); diff --git a/plugins/lumi_ai/backend/tool_settings.js b/plugins/lumi_ai/backend/tool_settings.js index b217c55..8d46d19 100644 --- a/plugins/lumi_ai/backend/tool_settings.js +++ b/plugins/lumi_ai/backend/tool_settings.js @@ -11,7 +11,7 @@ class ToolSettings { if (!local?.valid) throw new Error(local?.error || "Installed AI tool metadata is invalid."); const schema = normalizeSchema(local.metadata.settings_schema); if (!Object.keys(schema).length) throw new Error("This AI tool does not expose configurable settings."); - const values = this.readValues(local.dir, schema); + const values = this.readValues(local.dir, schema, local.metadata); return { tool_id: toolId, display_name: local.metadata.display_name, @@ -30,7 +30,7 @@ class ToolSettings { if (!local?.valid) throw new Error(local?.error || "Installed AI tool metadata is invalid."); const schema = normalizeSchema(local.metadata.settings_schema); if (!Object.keys(schema).length) throw new Error("This AI tool does not expose configurable settings."); - const current = this.readValues(local.dir, schema); + const current = this.readValues(local.dir, schema, local.metadata); const next = {}; for (const [key, field] of Object.entries(schema)) { const incoming = input?.[key]; @@ -49,17 +49,39 @@ class ToolSettings { return this.describe(toolId); } + reset(toolId) { + const local = this.installer.local(toolId); + if (!local?.valid) throw new Error(local?.error || "Installed AI tool metadata is invalid."); + const schema = normalizeSchema(local.metadata.settings_schema); + const values = Object.fromEntries( + Object.entries(schema).map(([key, field]) => [key, normalizeValue(field.default, field, key)]) + ); + const file = settingsFile(local.dir); + fs.mkdirSync(path.dirname(file), { recursive: true }); + const temporary = `${file}.${process.pid}.tmp`; + fs.writeFileSync(temporary, `${JSON.stringify(values, null, 2)}\n`, { mode: 0o600 }); + try { fs.chmodSync(temporary, 0o600); } catch {} + try { fs.renameSync(temporary, file); } + catch (error) { + if (!["EEXIST", "EPERM"].includes(error.code)) throw error; + fs.rmSync(file, { force: true }); + fs.renameSync(temporary, file); + } + return this.describe(toolId); + } + readRaw(toolId) { const local = this.installer.local(toolId); if (!local?.valid) throw new Error(local?.error || "Installed AI tool metadata is invalid."); const schema = normalizeSchema(local.metadata.settings_schema); - return this.readValues(local.dir, schema); + return this.readValues(local.dir, schema, local.metadata); } - readValues(toolDir, schema) { + readValues(toolDir, schema, metadata = {}) { let stored = {}; try { stored = JSON.parse(fs.readFileSync(settingsFile(toolDir), "utf8")); } catch {} + stored = migrateStoredSettings(toolDir, metadata, stored, schema); return Object.fromEntries( Object.entries(schema).map(([key, field]) => { try { @@ -72,6 +94,24 @@ class ToolSettings { } } +function migrateStoredSettings(toolDir, metadata, stored, schema) { + const relative = String(metadata.settings_migrator || ""); + if (!relative) return stored; + const target = path.resolve(toolDir, relative); + if (!target.startsWith(`${path.resolve(toolDir)}${path.sep}`) || !fs.existsSync(target)) return stored; + try { + const module = require(target); + if (typeof module.migrateSettings !== "function") return stored; + const fallback = Object.fromEntries( + Object.entries(schema).map(([key, field]) => [key, field.default]) + ); + const migrated = module.migrateSettings(stored, fallback); + return migrated && typeof migrated === "object" && !Array.isArray(migrated) ? migrated : stored; + } catch { + return stored; + } +} + function normalizeSchema(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; return Object.fromEntries(Object.entries(value).map(([key, field]) => { @@ -155,6 +195,7 @@ module.exports = { ToolSettings, normalizeSchema, normalizeValue, + migrateStoredSettings, redactSecrets, settingsFile }; diff --git a/plugins/lumi_ai/index.js b/plugins/lumi_ai/index.js index 2b4e6d9..08e28ea 100644 --- a/plugins/lumi_ai/index.js +++ b/plugins/lumi_ai/index.js @@ -1074,16 +1074,29 @@ module.exports = { } }); + router.post("/api/tools/:id/settings/reset", async (req, res) => { + if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); + try { + return res.json(await toolManager.resetSettings(req.params.id)); + } catch (error) { + return res.status(400).json({ error: error.message }); + } + }); + router.get("/tools/:id/assets/*", (req, res) => { - const permission = canUseAssistant({ - user: req.session.user, - config, - origin: "webui", - platform: "webui", - requestedSurface: "webui_chat" + if (!req.session.user?.isAdmin) { + const permission = canUseAssistant({ + user: req.session.user, + config, + origin: "webui", + platform: "webui", + requestedSurface: "webui_chat" + }); + if (!permission.allowed) return res.status(403).end(); + } + const file = toolManager.resolveAsset(req.params.id, req.params[0], { + allowInstalled: req.session.user?.isAdmin === true }); - if (!permission.allowed) return res.status(403).end(); - const file = toolManager.resolveAsset(req.params.id, req.params[0]); return file ? res.sendFile(file) : res.status(404).end(); }); diff --git a/plugins/lumi_ai/plugin.json b/plugins/lumi_ai/plugin.json index f0d3684..7085817 100644 --- a/plugins/lumi_ai/plugin.json +++ b/plugins/lumi_ai/plugin.json @@ -1,7 +1,7 @@ { "id": "lumi_ai", "name": "Lumi AI", - "version": "0.8.0", + "version": "0.8.1", "description": "Managed local AI provider and scoped WebUI assistant for Lumi.", "main": "index.js" } diff --git a/plugins/lumi_ai/public/tool-manager.js b/plugins/lumi_ai/public/tool-manager.js index ba98649..1384ddb 100644 --- a/plugins/lumi_ai/public/tool-manager.js +++ b/plugins/lumi_ai/public/tool-manager.js @@ -10,8 +10,10 @@ const settingsModal = document.querySelector("[data-ai-tool-settings-modal]"); const settingsTitle = settingsModal?.querySelector("[data-ai-tool-settings-title]"); const settingsForm = settingsModal?.querySelector("[data-ai-tool-settings-form]"); + const settingsCustom = settingsModal?.querySelector("[data-ai-tool-settings-custom]"); const settingsFields = settingsModal?.querySelector("[data-ai-tool-settings-fields]"); const settingsSave = settingsModal?.querySelector("[data-ai-tool-settings-save]"); + const settingsReset = settingsModal?.querySelector("[data-ai-tool-settings-reset]"); const diagnostics = modal?.querySelector("[data-ai-tool-diagnostics]"); const diagnosticRole = diagnostics?.querySelector("[data-ai-tool-diagnostic-role]"); const diagnosticOrigin = diagnostics?.querySelector("[data-ai-tool-diagnostic-origin]"); @@ -19,11 +21,13 @@ const diagnosticResults = diagnostics?.querySelector("[data-ai-tool-diagnostic-results]"); const promptPreview = diagnostics?.querySelector("[data-ai-tool-prompt-preview]"); if (!openButton || !modal || !list || !source || !readmeModal || !readmeTitle || !readmeBody || - !settingsModal || !settingsTitle || !settingsForm || !settingsFields || !settingsSave || + !settingsModal || !settingsTitle || !settingsForm || !settingsCustom || !settingsFields || + !settingsSave || !settingsReset || !diagnostics || !diagnosticRole || !diagnosticOrigin || !diagnosticResults || !promptPreview) return; let loading = false; let activeSettingsTool = null; + let activeSettingsPayload = null; const setOpen = (target, open) => { target.classList.toggle("is-open", open); @@ -181,6 +185,7 @@ `registered=${(plugin.registered_tools || []).join(", ") || "none"}`, plugin.prompt_exposed ? "prompt=exposed" : `prompt=hidden (${plugin.hidden_reason || "unknown"})`, plugin.message || "", + plugin.runtime_details ? `details=${JSON.stringify(plugin.runtime_details)}` : "", decisions ].filter(Boolean).join(" ยท "); diagnosticResults.append(row); @@ -239,7 +244,9 @@ const openSettings = async (tool) => { activeSettingsTool = tool; + activeSettingsPayload = null; settingsTitle.textContent = `${tool.display_name || tool.tool_id} settings`; + settingsCustom.replaceChildren(); settingsFields.replaceChildren(message("Loading settings...")); setOpen(settingsModal, true); try { @@ -256,6 +263,8 @@ }; const renderSettings = (payload) => { + activeSettingsPayload = payload; + renderCustomSettings(payload); settingsFields.replaceChildren(); for (const [key, field] of Object.entries(payload.schema || {})) { const wrapper = document.createElement("div"); @@ -275,6 +284,43 @@ } }; + const renderCustomSettings = async (payload) => { + settingsCustom.replaceChildren(); + if (!payload.ui?.html) return; + for (const href of payload.ui.styles || []) loadStyle(href); + settingsCustom.innerHTML = payload.ui.html; + await Promise.all((payload.ui.scripts || []).map(loadScript)); + window.dispatchEvent(new CustomEvent("lumi-ai-tool-settings-open", { + detail: { + toolId: activeSettingsTool?.tool_id, + payload, + root: settingsCustom + } + })); + }; + + const loadStyle = (href) => { + if (document.querySelector(`link[data-ai-tool-asset="${CSS.escape(href)}"]`)) return; + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = href; + link.dataset.aiToolAsset = href; + document.head.append(link); + }; + + const loadScript = (src) => { + const existing = document.querySelector(`script[data-ai-tool-asset="${CSS.escape(src)}"]`); + if (existing) return Promise.resolve(); + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.src = src; + script.dataset.aiToolAsset = src; + script.addEventListener("load", resolve, { once: true }); + script.addEventListener("error", () => reject(new Error(`Unable to load ${src}.`)), { once: true }); + document.head.append(script); + }); + }; + const settingsControl = (key, field, value, configuredSecret) => { if (field.type === "boolean") { const input = document.createElement("input"); @@ -367,6 +413,25 @@ } }); + settingsReset.addEventListener("click", async () => { + if (!activeSettingsTool || !window.confirm(`Reset ${activeSettingsTool.display_name || activeSettingsTool.tool_id} settings to defaults?`)) return; + settingsReset.disabled = true; + try { + const response = await fetch(`/plugins/lumi_ai/api/tools/${encodeURIComponent(activeSettingsTool.tool_id)}/settings/reset`, { + method: "POST", + headers: { Accept: "application/json" } + }); + const payload = await response.json(); + if (!response.ok) throw new Error(payload.error || "Unable to reset tool settings."); + renderSettings(payload); + await loadTools(false); + } catch (error) { + window.alert(error.message); + } finally { + settingsReset.disabled = false; + } + }); + const renderMarkdown = (container, markdown) => { container.replaceChildren(); const lines = String(markdown || "").replace(/\r\n/g, "\n").split("\n"); diff --git a/plugins/lumi_ai/views/tool-modal.ejs b/plugins/lumi_ai/views/tool-modal.ejs index 96b422a..5f7c56e 100644 --- a/plugins/lumi_ai/views/tool-modal.ejs +++ b/plugins/lumi_ai/views/tool-modal.ejs @@ -56,8 +56,10 @@