(() => { const openButton = document.querySelector("[data-ai-tools-open]"); const modal = document.querySelector("[data-ai-tools-modal]"); const list = modal?.querySelector("[data-ai-tools-list]"); const source = modal?.querySelector("[data-ai-tools-source]"); const refresh = modal?.querySelector("[data-ai-tools-refresh]"); const readmeModal = document.querySelector("[data-ai-tool-readme-modal]"); const readmeTitle = readmeModal?.querySelector("[data-ai-tool-readme-title]"); const readmeBody = readmeModal?.querySelector("[data-ai-tool-readme]"); if (!openButton || !modal || !list || !source || !readmeModal || !readmeTitle || !readmeBody) return; let loading = false; const setOpen = (target, open) => { target.classList.toggle("is-open", open); target.setAttribute("aria-hidden", String(!open)); }; const loadTools = async (force = false) => { if (loading) return; loading = true; list.replaceChildren(message("Loading AI tool plugins...")); try { const response = await fetch(`/plugins/lumi_ai/api/tools${force ? "?refresh=1" : ""}`, { cache: "no-store", headers: { Accept: "application/json" } }); const payload = await response.json(); if (!response.ok) throw new Error(payload.error || "Unable to load AI tools."); source.textContent = `${payload.repository} · ${payload.branch} · checked ${payload.checked_at ? new Date(payload.checked_at).toLocaleString() : "never"}${payload.cached ? " · cached" : ""}${payload.stale ? " · stale" : ""}`; source.classList.toggle("error", Boolean(payload.error)); if (payload.error) source.title = payload.error; renderTools(payload.tools || []); } catch (error) { list.replaceChildren(message(error.message, true)); } finally { loading = false; } }; const renderTools = (tools) => { list.replaceChildren(); if (!tools.length) { list.append(message("No local or remote Lumi AI tool plugins were found.")); return; } for (const tool of tools) list.append(renderTool(tool)); }; const renderTool = (tool) => { const row = document.createElement("article"); row.className = "ai-tool-row"; const summary = document.createElement("div"); summary.className = "ai-tool-summary"; const identity = document.createElement("div"); identity.className = "ai-tool-identity"; const name = document.createElement("strong"); name.textContent = tool.display_name || tool.tool_id; const id = document.createElement("span"); id.textContent = tool.tool_id; identity.append(name, id); const versions = document.createElement("div"); versions.className = "ai-tool-versions"; versions.append( badge(tool.installed ? "Installed" : "Remote", tool.installed ? "installed" : ""), badge(tool.enabled ? "Enabled" : "Disabled", tool.enabled ? "installed" : ""), textPair("Local", tool.local_version || "-"), textPair("Remote", tool.remote_version || "-"), badge(tool.remote_missing ? "Remote missing" : tool.update_available ? "Update available" : "Current", tool.update_available ? "warning" : "") ); const scope = document.createElement("div"); scope.className = "ai-tool-scope"; scope.textContent = `${tool.primary_type || "general"} · ${tool.primary_scope || "unspecified"}`; const actions = document.createElement("div"); actions.className = "ai-tool-actions"; const expand = button("Details", "subtle"); const details = renderDetails(tool); expand.addEventListener("click", () => { details.hidden = !details.hidden; expand.textContent = details.hidden ? "Details" : "Hide details"; }); const inspect = button("Inspect", "subtle"); inspect.addEventListener("click", () => inspectReadme(tool)); const enable = button(tool.enabled ? "Disable" : "Enable", tool.enabled ? "subtle" : ""); enable.disabled = tool.installed && !tool.local_valid; enable.addEventListener("click", () => runAction(tool, tool.enabled ? "disable" : "enable", enable)); const update = button("Update", tool.update_available ? "update" : "subtle"); update.disabled = !tool.update_enabled; update.title = !tool.installed ? "Install the tool before updating." : tool.remote_missing ? "This tool is missing remotely." : ""; update.addEventListener("click", () => runAction(tool, "update", update)); actions.append(expand, inspect, enable, update); if (tool.installed) actions.append(deleteForm(tool)); summary.append(identity, versions, scope, actions); row.append(summary, details); return row; }; const renderDetails = (tool) => { const details = document.createElement("div"); details.className = "ai-tool-details"; details.hidden = true; details.append( detail("Description", tool.description || "No description."), detail("Capabilities", joinValue(tool.capabilities)), detail("Limitations", joinValue(tool.limitations)), detail("Scope", joinValue(tool.scope)), detail("Permissions", joinValue(tool.permissions)), detail("Dependencies", joinValue(tool.dependencies)), detail("Required plugins", joinValue(tool.required_plugins)), detail("Required platforms", joinValue(tool.required_platforms)), detail("Dependency availability", dependencyStatus(tool.dependency_status)), detail("Risk / confirmation", `${tool.risk_level || "sensitive"} / ${tool.confirmation_required === false ? "not required" : "required"}`), detail("Runtime", `${tool.runtime_state || "unknown"}${tool.runtime_message ? `: ${tool.runtime_message}` : ""}`), detail("Install status", tool.local_error || tool.remote_error || (tool.remote_missing ? "Installed locally; missing from remote repository." : "Ready")) ); return details; }; const runAction = async (tool, action, control) => { control.disabled = true; const original = control.textContent; control.textContent = action === "enable" ? "Enabling..." : action === "disable" ? "Disabling..." : "Updating..."; try { const response = await fetch(`/plugins/lumi_ai/tools/${encodeURIComponent(tool.tool_id)}/${action}`, { method: "POST", headers: { Accept: "application/json" } }); const payload = await response.json(); if (!response.ok) throw new Error(payload.error || `${action} failed.`); await loadTools(false); } catch (error) { window.alert(error.message); control.disabled = false; control.textContent = original; } }; const deleteForm = (tool) => { const form = document.createElement("form"); form.method = "post"; form.action = `/plugins/lumi_ai/tools/${encodeURIComponent(tool.tool_id)}/delete`; form.dataset.confirmMode = "modal"; form.dataset.confirmTitle = `Delete ${tool.display_name || tool.tool_id}?`; form.dataset.confirmText = "The installed AI tool files will be removed. Shared Lumi AI data and unrelated plugins are not affected."; const remove = button("Delete", "danger"); remove.type = "submit"; form.append(remove); return form; }; const inspectReadme = async (tool) => { readmeTitle.textContent = `${tool.display_name || tool.tool_id} documentation`; readmeBody.replaceChildren(message("Loading readme.md...")); setOpen(readmeModal, true); try { const response = await fetch(`/plugins/lumi_ai/api/tools/${encodeURIComponent(tool.tool_id)}/readme`, { cache: "no-store", headers: { Accept: "application/json" } }); const payload = await response.json(); if (!response.ok) throw new Error(payload.error || "Unable to load readme.md."); renderMarkdown(readmeBody, payload.markdown); } catch (error) { readmeBody.replaceChildren(message(error.message, true)); } }; const renderMarkdown = (container, markdown) => { container.replaceChildren(); const lines = String(markdown || "").replace(/\r\n/g, "\n").split("\n"); let paragraph = []; const flush = () => { if (!paragraph.length) return; const p = document.createElement("p"); appendInline(p, paragraph.join(" ")); container.append(p); paragraph = []; }; for (let index = 0; index < lines.length; index += 1) { const fence = lines[index].match(/^```([a-z0-9_+-]*)/i); if (fence) { flush(); const code = []; for (index += 1; index < lines.length && !/^```/.test(lines[index]); index += 1) code.push(lines[index]); const pre = document.createElement("pre"); const element = document.createElement("code"); element.textContent = code.join("\n"); element.className = `language-${fence[1] || "text"}`; pre.append(element); container.append(pre); continue; } const heading = lines[index].match(/^(#{1,4})\s+(.+)/); if (heading) { flush(); const element = document.createElement(`h${heading[1].length}`); appendInline(element, heading[2]); container.append(element); } else if (/^\s*[-*]\s+/.test(lines[index])) { flush(); const list = container.lastElementChild?.tagName === "UL" ? container.lastElementChild : document.createElement("ul"); const item = document.createElement("li"); appendInline(item, lines[index].replace(/^\s*[-*]\s+/, "")); list.append(item); if (!list.isConnected) container.append(list); } else if (!lines[index].trim()) { flush(); } else { paragraph.push(lines[index]); } } flush(); }; const appendInline = (parent, value) => { const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\[[^\]]+\]\([^)]+\))/g; let offset = 0; for (const match of String(value).matchAll(pattern)) { parent.append(document.createTextNode(value.slice(offset, match.index))); const token = match[0]; if (token.startsWith("`")) { const code = document.createElement("code"); code.textContent = token.slice(1, -1); parent.append(code); } else if (token.startsWith("**")) { const strong = document.createElement("strong"); strong.textContent = token.slice(2, -2); parent.append(strong); } else { const parts = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/); const anchor = safeAnchor(parts?.[2], parts?.[1]); parent.append(anchor || document.createTextNode(parts?.[1] || token)); } offset = match.index + token.length; } parent.append(document.createTextNode(String(value).slice(offset))); }; const safeAnchor = (href, label) => { try { const parsed = new URL(href, window.location.origin); if (!["http:", "https:"].includes(parsed.protocol)) return null; const anchor = document.createElement("a"); anchor.href = parsed.href; anchor.textContent = label; if (parsed.origin !== window.location.origin) { anchor.target = "_blank"; anchor.rel = "noopener noreferrer"; } return anchor; } catch { return null; } }; const detail = (label, value) => { const item = document.createElement("div"); const term = document.createElement("strong"); const body = document.createElement("span"); term.textContent = label; body.textContent = value || "-"; item.append(term, body); return item; }; const dependencyStatus = (value) => { const blocking = Array.isArray(value?.blocking) ? value.blocking : []; const optional = Array.isArray(value?.optional) ? value.optional : []; if (!blocking.length && !optional.length) return "Available"; return [ blocking.length ? `Blocking: ${blocking.join("; ")}` : "", optional.length ? `Optional: ${optional.join("; ")}` : "" ].filter(Boolean).join(" | "); }; const joinValue = (value) => Array.isArray(value) ? value.join(", ") || "-" : value && typeof value === "object" ? JSON.stringify(value) : String(value || "-"); const badge = (text, className = "") => { const element = document.createElement("span"); element.className = `ai-tag ${className}`.trim(); element.textContent = text; return element; }; const textPair = (label, value) => { const element = document.createElement("span"); element.className = "ai-tool-version"; element.textContent = `${label} ${value}`; return element; }; const button = (text, className = "") => { const element = document.createElement("button"); element.type = "button"; element.className = `button ${className}`.trim(); element.textContent = text; return element; }; const message = (text, error = false) => { const element = document.createElement("div"); element.className = `callout${error ? " danger" : ""}`; element.textContent = text; return element; }; openButton.addEventListener("click", () => { setOpen(modal, true); loadTools(false); }); refresh?.addEventListener("click", () => loadTools(true)); modal.querySelectorAll("[data-ai-tools-close]").forEach((control) => control.addEventListener("click", () => setOpen(modal, false))); readmeModal.querySelectorAll("[data-ai-tool-readme-close]").forEach((control) => control.addEventListener("click", () => setOpen(readmeModal, false))); for (const target of [modal, readmeModal]) { target.addEventListener("click", (event) => { if (event.target === target) setOpen(target, false); }); } })();