src / toolsProvider.ts
import { tool, Tool, ToolsProviderController } from "@lmstudio/sdk";
import { spawn, exec } from "child_process";
import { existsSync, statSync } from "fs";
import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
import { homedir } from "os";
import { dirname, join, resolve } from "path";
import { promisify } from "util";
import { z } from "zod";
import {
canRead,
checkCommand,
checkRead,
checkWrite,
expandPath,
loadPermissions,
resolveRule,
SYSTEM_COMMANDS,
} from "./permissions";
import { currentTimeString } from "./promptPreprocessor";
import { buildWebTools } from "./webTools";
const execAsync = promisify(exec);
const CONTEXT_LINES = 3; // lines of context shown around each change
function clamp(text: string, max: number): string {
if (text.length <= max) return text;
return text.slice(0, max) + `\n... [truncated — ${text.length - max} more chars]`;
}
/**
* Build a unified-diff-style string between two versions of file content.
* Used by edit_file and edit_file_lines so the model can present the result
* as a ```diff block with green/red syntax highlighting.
*/
function buildDiff(filePath: string, before: string, after: string): string {
const bLines = before.split("\n");
const aLines = after.split("\n");
// Collect changed line ranges (simple LCS-free approach: find first/last differing line)
let firstDiff = 0;
while (firstDiff < bLines.length && firstDiff < aLines.length && bLines[firstDiff] === aLines[firstDiff]) firstDiff++;
let lastB = bLines.length - 1;
let lastA = aLines.length - 1;
while (lastB > firstDiff && lastA > firstDiff && bLines[lastB] === aLines[lastA]) { lastB--; lastA--; }
const ctxStart = Math.max(0, firstDiff - CONTEXT_LINES);
const ctxEndB = Math.min(bLines.length - 1, lastB + CONTEXT_LINES);
const ctxEndA = Math.min(aLines.length - 1, lastA + CONTEXT_LINES);
const removedCount = lastB - firstDiff + 1;
const addedCount = lastA - firstDiff + 1;
const header = `--- ${filePath}\n+++ ${filePath}\n@@ -${ctxStart + 1},${ctxEndB - ctxStart + 1} +${ctxStart + 1},${ctxEndA - ctxStart + 1} @@`;
const lines: string[] = [header];
for (let i = ctxStart; i < firstDiff; i++) lines.push(` ${bLines[i]}`);
for (let i = firstDiff; i <= lastB; i++) lines.push(`-${bLines[i]}`);
for (let i = firstDiff; i <= lastA; i++) lines.push(`+${aLines[i]}`);
for (let i = lastB + 1; i <= ctxEndB; i++) lines.push(` ${bLines[i]}`);
const summary = `−${removedCount} line${removedCount !== 1 ? "s" : ""}, +${addedCount} line${addedCount !== 1 ? "s" : ""} · ${resolve(filePath)}`;
return "```diff\n" + lines.join("\n") + "\n```\n" + summary;
}
// Recursive directory walk; returns paths relative to `dir`.
async function walk(dir: string, depth: number, prefix = ""): Promise<string[]> {
const out: string[] = [];
let entries: ReturnType<typeof statSync>[] = [];
try {
const dirents = await readdir(dir, { withFileTypes: true });
for (const e of dirents) {
const rel = prefix ? join(prefix, e.name) : e.name;
if (e.isDirectory()) {
out.push(rel + "/");
if (depth > 1) out.push(...(await walk(join(dir, e.name), depth - 1, rel)));
} else {
out.push(rel);
}
}
} catch { /* permission error from OS — skip */ }
void entries;
return out;
}
export async function toolsProvider(ctl: ToolsProviderController) {
const tools: Tool[] = [...buildWebTools()];
// ── GET CURRENT TIME ─────────────────────────────────────────────────────
tools.push(tool({
name: "get_current_time",
description: "Return the current local date and time. Use this whenever you need an up-to-date timestamp mid-conversation (the time injected at conversation start may be stale).",
parameters: {},
implementation: async () => currentTimeString(),
}));
// ── OPEN CONTROL PANEL ───────────────────────────────────────────────────
tools.push(tool({
name: "open_control_panel",
description:
"Open the agent-toolkit Python control panel GUI. " +
"Call this when the user is denied access to a path or command and wants to change permissions, " +
"or when they ask to configure the toolkit, change allowed folders, edit the system prompt, " +
"or manage the command executor settings.",
parameters: {},
implementation: async () => {
if (process.platform !== "darwin") {
return "Error: control panel GUI is only supported on macOS.";
}
// Resolve control_panel.py — check the known project location first,
// then fall back to paths relative to the permissions dir.
const candidates = [
join(homedir(), "PycharmProjects", "lmstudio", "agent-toolkit", "control_panel.py"),
join(homedir(), ".lmstudio", "extensions", "plugins", "tap1x", "agent-toolkit", "control_panel.py"),
];
const panelPath = candidates.find(existsSync);
if (!panelPath) {
return `Error: control_panel.py not found. Looked in:\n${candidates.join("\n")}`;
}
// Direct spawn from Node.js has no Window Server session, so Tkinter
// opens and immediately closes. osascript's `do shell script` runs with
// the full user login session (Window Server included) — backgrounded
// with & so it doesn't block and no Terminal window appears.
const escaped = panelPath.replace(/'/g, "'\\''");
await execAsync(
`osascript -e 'do shell script "python3 '"'"'${escaped}'"'"' &> /dev/null &"'`,
{ timeout: 5000 },
);
return "Control panel opened.";
},
}));
// ── GET MODEL INFO ───────────────────────────────────────────────────────
tools.push(tool({
name: "get_model_info",
description: "Return information about the currently loaded LLM: display name, file size, parameter count, context length, and capabilities (vision, tool-use training).",
parameters: {},
implementation: async () => {
try {
const model = await ctl.client.llm.model();
const info = await model.getModelInfo();
if (!info) return "No model is currently loaded.";
const gb = (info.sizeBytes / 1_073_741_824).toFixed(2) + " GB";
const params = info.paramsString ? info.paramsString + " params" : "params unknown";
return [
`Name : ${info.displayName}`,
`Key : ${info.modelKey}`,
`Size : ${gb}`,
`Params : ${params}`,
`Context : ${info.maxContextLength.toLocaleString()} tokens`,
`Vision : ${info.vision ? "yes" : "no"}`,
`Tool-use trained: ${info.trainedForToolUse ? "yes" : "no"}`,
`Path : ${info.path}`,
].join("\n");
} catch (err: any) {
return `Error fetching model info: ${err.message ?? err}`;
}
},
}));
// ── MEDIA CONTROL ────────────────────────────────────────────────────────
tools.push(tool({
name: "media_control",
description:
"Control macOS system media playback and volume via AppleScript. " +
"Works with Spotify, Apple Music, browsers, and any Now Playing source.\n" +
"Actions:\n" +
" play | pause | toggle — start / stop / flip playback\n" +
" next | previous — skip tracks\n" +
" volume <0–100> — set output volume to exact level\n" +
" volume_up | volume_down — nudge volume ±10\n" +
" mute | unmute — mute/unmute output\n" +
" status — return current volume level\n" +
"Does NOT require command executor to be enabled.",
parameters: {
action: z.enum([
"play", "pause", "toggle",
"next", "previous",
"volume", "volume_up", "volume_down",
"mute", "unmute",
"status",
]),
level: z.number().int().min(0).max(100).optional()
.describe("Required when action=volume. Target volume 0–100."),
},
implementation: async ({ action, level }) => {
// ── platform + dependency checks ────────────────────────────────────
if (process.platform !== "darwin") {
return "Error: media_control is only supported on macOS.";
}
// Locate nowplaying-cli — try both Homebrew prefixes (Apple Silicon / Intel).
const npcCandidates = [
"/opt/homebrew/bin/nowplaying-cli", // Apple Silicon default
"/usr/local/bin/nowplaying-cli", // Intel Homebrew
];
const npc = npcCandidates.find((p) => existsSync(p));
if (!npc) {
return (
"Error: nowplaying-cli is not installed.\n" +
"Install it with: brew install nowplaying-cli\n" +
"It is required for play/pause/next/previous/status actions."
);
}
const runNpc = (cmd: string) => execAsync(`${npc} ${cmd}`, { timeout: 5000 });
// System-level osascript for volume/mute (not app-specific, always works).
const runScript = (script: string) =>
execAsync(`osascript << 'HEREDOC'\n${script}\nHEREDOC`, { timeout: 5000 });
try {
switch (action) {
case "play":
await runNpc("play");
return "Playback started.";
case "pause":
await runNpc("pause");
return "Playback paused.";
case "toggle":
await runNpc("togglePlayPause");
return "Playback toggled.";
case "next":
await runNpc("next");
return "Skipped to next track.";
case "previous":
await runNpc("previous");
return "Went to previous track.";
case "volume": {
const vol = level ?? 50;
await runScript(`set volume output volume ${vol}`);
return `Volume set to ${vol}%.`;
}
case "volume_up": {
const { stdout } = await runScript(`output volume of (get volume settings)`);
const current = parseInt(stdout.trim(), 10) || 50;
const next = Math.min(100, current + 10);
await runScript(`set volume output volume ${next}`);
return `Volume raised to ${next}%.`;
}
case "volume_down": {
const { stdout } = await runScript(`output volume of (get volume settings)`);
const current = parseInt(stdout.trim(), 10) || 50;
const next = Math.max(0, current - 10);
await runScript(`set volume output volume ${next}`);
return `Volume lowered to ${next}%.`;
}
case "mute":
await runScript(`set volume with output muted`);
return "Output muted.";
case "unmute":
await runScript(`set volume without output muted`);
return "Output unmuted.";
case "status": {
const [volResult, npResult] = await Promise.all([
runScript(`output volume of (get volume settings)`),
runNpc("get --json title artist album").catch(() => ({ stdout: "{}" })),
]);
const vol = volResult.stdout.trim();
let nowPlaying = "Nothing playing";
try {
const np = JSON.parse(npResult.stdout.trim());
const parts = [np.title, np.artist, np.album].filter(Boolean);
if (parts.length) nowPlaying = parts.join(" — ");
} catch { /* nowplaying-cli returned non-JSON or nothing */ }
return `Volume: ${vol}%\nNow playing: ${nowPlaying}`;
}
}
} catch (err: any) {
return `Error: ${err.message ?? err}`;
}
},
}));
// ── RUN JAVASCRIPT ───────────────────────────────────────────────────────
tools.push(tool({
name: "run_javascript",
description:
"Execute a JavaScript/TypeScript snippet using the bundled Deno runtime. " +
"Ideal for maths, data transformation, date calculations, sorting, formatting, " +
"and any logic too complex to do mentally. " +
"Print results with console.log — stdout/stderr are returned. " +
"No network, no env vars, no subprocesses — pure computation only. " +
"Cannot import external modules. Default timeout 10 s (max 60 s).",
parameters: {
javascript: z.string().min(1),
timeout_seconds: z.number().int().min(1).max(60).optional()
.describe("Execution timeout in seconds (default 10, max 60)."),
},
implementation: async ({ javascript, timeout_seconds }) => {
const denoPath = join(homedir(), ".lmstudio", ".internal", "utils", "deno");
const sandboxDir = join(homedir(), ".lmstudio-toolkit", "sandbox");
await mkdir(sandboxDir, { recursive: true });
const scriptName = `js_${Date.now()}_${Math.random().toString(36).slice(2)}.ts`;
const scriptPath = join(sandboxDir, scriptName);
await writeFile(scriptPath, javascript, "utf-8");
let stdout = "";
let stderr = "";
try {
await new Promise<void>((res, rej) => {
const child = spawn(
denoPath,
[
"run",
"--allow-read=.", // sandbox dir only
"--allow-write=.", // sandbox dir only
"--no-prompt",
"--deny-net",
"--deny-env",
"--deny-sys",
"--deny-run",
"--deny-ffi",
scriptPath,
],
{
cwd: sandboxDir,
timeout: (timeout_seconds ?? 10) * 1000,
stdio: "pipe",
env: { NO_COLOR: "true", HOME: homedir() },
},
);
child.stdout.setEncoding("utf-8");
child.stderr.setEncoding("utf-8");
child.stdout.on("data", (d: string) => { stdout += d; });
child.stderr.on("data", (d: string) => { stderr += d; });
child.on("close", (code: number | null) => {
if (code === 0) res();
else rej(new Error(`Deno exited ${code}. stderr: ${stderr.trim()}`));
});
child.on("error", rej);
});
} finally {
await rm(scriptPath).catch(() => {});
}
const out = stdout.trim();
const err = stderr.trim();
if (!out && !err) return "(no output)";
return [out && `[stdout]\n${out}`, err && `[stderr]\n${err}`].filter(Boolean).join("\n");
},
}));
// ── RUN COMMAND ──────────────────────────────────────────────────────────
tools.push(tool({
name: "run_command",
description:
"Execute a shell command. The permission tier of working_directory controls what is allowed:\n" +
" deny → no commands at all; change working_directory to a readable/writable path\n" +
" read → read-only commands only (ls, cat, grep, find, stat, …)\n" +
" read_write → any command not in the deny list; write-impact commands (rm, mv, cp, …)\n" +
" additionally require each path argument to have write permission.\n" +
"System-destructive commands (sudo*, shutdown*, passwd*, mkfs*, dd, kill, …) are always blocked.",
parameters: {
command: z.string().min(1),
working_directory: z.string().optional()
.describe("Absolute path. Defaults to the first configured working dir."),
},
implementation: async ({ command, working_directory }) => {
const perms = loadPermissions();
const binary = (command.trim().split(/\s+/)[0] ?? "").split(/[\\/]/).pop() ?? "";
const isSystemCmd = perms.command_executor.allow_system_commands && SYSTEM_COMMANDS.has(binary);
const cwd = expandPath(working_directory || (isSystemCmd ? homedir() : ""));
if (!cwd) return "Error: no working directory. Configure one in the control panel.";
const denied = checkCommand(perms, command, cwd);
if (denied) return `Error: ${denied}`;
try {
const { stdout, stderr } = await execAsync(command, {
cwd: resolve(cwd),
timeout: perms.command_executor.timeout_ms,
maxBuffer: 1024 * 1024 * 10,
windowsHide: true,
});
const body = [stdout && `[stdout]\n${stdout}`, stderr && `[stderr]\n${stderr}`]
.filter(Boolean).join("\n") || "(no output)";
return clamp(body, perms.command_executor.max_output_chars);
} catch (err: any) {
const parts = [`Command failed (exit ${err.code ?? "?"})`];
if (err.stdout) parts.push(`[stdout]\n${err.stdout}`);
if (err.stderr) parts.push(`[stderr]\n${err.stderr}`);
if (err.killed) parts.push("(killed: timeout)");
return clamp(parts.join("\n"), perms.command_executor.max_output_chars);
}
},
}));
// ── READ FILE (whole) ────────────────────────────────────────────────────
tools.push(tool({
name: "read_file",
description:
"Read an entire text file. For large files prefer read_file_lines to fetch " +
"only the relevant range. Path must be in a readable location.",
parameters: {
path: z.string().min(1),
max_chars: z.number().int().positive().optional()
.describe("Cap returned text (default 100 000)."),
},
implementation: async ({ path, max_chars }) => {
path = expandPath(path);
const perms = loadPermissions();
const denied = checkRead(perms, path);
if (denied) return `Error: ${denied}`;
if (!existsSync(path)) return "Error: file does not exist.";
if (statSync(path).isDirectory()) return "Error: path is a directory — use list_directory.";
return clamp(await readFile(path, "utf-8"), max_chars ?? 100_000);
},
}));
// ── READ FILE LINES ──────────────────────────────────────────────────────
tools.push(tool({
name: "read_file_lines",
description:
"Read a specific line range from a file (1-indexed, inclusive). " +
"Much more efficient than read_file for large code files when you only need " +
"a section. Returns lines with their line numbers prepended.",
parameters: {
path: z.string().min(1),
start_line: z.number().int().min(1).describe("First line to return (1-indexed)."),
end_line: z.number().int().min(1).optional()
.describe("Last line to return (inclusive). Omit to read to end of file."),
},
implementation: async ({ path, start_line, end_line }) => {
path = expandPath(path);
const perms = loadPermissions();
const denied = checkRead(perms, path);
if (denied) return `Error: ${denied}`;
if (!existsSync(path)) return "Error: file does not exist.";
if (statSync(path).isDirectory()) return "Error: path is a directory.";
const allLines = (await readFile(path, "utf-8")).split("\n");
const total = allLines.length;
const s = Math.max(1, start_line);
const e = end_line ? Math.min(end_line, total) : total;
if (s > total) return `Error: file only has ${total} lines.`;
const slice = allLines.slice(s - 1, e);
const numbered = slice.map((l: string, i: number) => `${s + i}\t${l}`).join("\n");
return `Lines ${s}–${e} of ${total} total:\n${numbered}`;
},
}));
// ── WRITE FILE ───────────────────────────────────────────────────────────
tools.push(tool({
name: "write_file",
description:
"Create a new file or overwrite an existing one with the given content. " +
"Creates parent directories as needed. For editing existing code prefer " +
"edit_file (find/replace) or edit_file_lines (line-range replace). " +
"Path must be in a writable location.",
parameters: {
path: z.string().min(1),
content: z.string(),
},
implementation: async ({ path, content }) => {
path = expandPath(path);
const perms = loadPermissions();
const denied = checkWrite(perms, path);
if (denied) return `Error: ${denied}`;
const dir = dirname(resolve(path));
if (!existsSync(dir)) await mkdir(dir, { recursive: true });
await writeFile(path, content, "utf-8");
return `Wrote ${content.length} chars to ${resolve(path)}`;
},
}));
// ── EDIT FILE (exact find / replace) ────────────────────────────────────
tools.push(tool({
name: "edit_file",
description:
"Replace an exact substring in a file. old_string must appear exactly once " +
"unless replace_all is true. Use this for targeted code edits — never rewrite " +
"the whole file just to change a few lines. Path must be in a writable location. " +
"Returns a diff — always show it to the user verbatim (it is already inside a ```diff block).",
parameters: {
path: z.string().min(1),
old_string: z.string().min(1, "old_string cannot be empty"),
new_string: z.string(),
replace_all: z.boolean().optional(),
},
implementation: async ({ path, old_string, new_string, replace_all }) => {
path = expandPath(path);
const perms = loadPermissions();
const denied = checkWrite(perms, path);
if (denied) return `Error: ${denied}`;
if (!existsSync(path)) return "Error: file does not exist.";
const original = await readFile(path, "utf-8");
const count = original.split(old_string).length - 1;
if (count === 0) return "Error: old_string not found in file.";
if (count > 1 && !replace_all)
return `Error: old_string appears ${count} times. Make it unique or pass replace_all=true.`;
const updated = replace_all
? original.split(old_string).join(new_string)
: original.replace(old_string, new_string);
await writeFile(path, updated, "utf-8");
return buildDiff(path, original, updated);
},
}));
// ── EDIT FILE LINES ──────────────────────────────────────────────────────
tools.push(tool({
name: "edit_file_lines",
description:
"Replace a range of lines in a file (1-indexed, inclusive). " +
"Ideal for code edits when you know the line numbers from read_file_lines. " +
"new_content replaces everything from start_line through end_line; it may " +
"contain a different number of lines than the range it replaces. " +
"Path must be in a writable location. " +
"Returns a diff — always show it to the user verbatim (it is already inside a ```diff block).",
parameters: {
path: z.string().min(1),
start_line: z.number().int().min(1),
end_line: z.number().int().min(1),
new_content: z.string().describe(
"Replacement text for the line range. Do NOT include a trailing newline " +
"unless you want a blank line inserted after the replacement.",
),
},
implementation: async ({ path, start_line, end_line, new_content }) => {
path = expandPath(path);
const perms = loadPermissions();
const denied = checkWrite(perms, path);
if (denied) return `Error: ${denied}`;
if (!existsSync(path)) return "Error: file does not exist.";
const original = await readFile(path, "utf-8");
const allLines = original.split("\n");
const total = allLines.length;
if (start_line > total) return `Error: start_line ${start_line} exceeds file length ${total}.`;
const s = start_line - 1; // 0-indexed
const e = Math.min(end_line, total); // 0-indexed exclusive
const beforeLines = allLines.slice(0, s);
const afterLines = allLines.slice(e);
const replacementLines = new_content === "" ? [] : new_content.split("\n");
const updated = [...beforeLines, ...replacementLines, ...afterLines].join("\n");
await writeFile(path, updated, "utf-8");
return buildDiff(path, original, updated);
},
}));
// ── LIST DIRECTORY ───────────────────────────────────────────────────────
tools.push(tool({
name: "list_directory",
description:
"List the contents of a directory. Denied entries are hidden entirely. " +
"Entries that are denied but contain accessible sub-paths are shown as " +
"'<name>/ [passthrough]'. Path must be readable.",
parameters: {
path: z.string().min(1),
recursive: z.boolean().optional(),
depth: z.number().int().positive().optional().describe("Max recursion depth (default 3)."),
},
implementation: async ({ path, recursive, depth }) => {
path = expandPath(path);
const perms = loadPermissions();
const denied = checkRead(perms, path);
if (denied) return `Error: ${denied}`;
if (!existsSync(path)) return "Error: directory does not exist.";
if (!statSync(path).isDirectory()) return "Error: not a directory.";
if (recursive) {
const all = await walk(path, depth ?? 3);
// Filter out entries the model can't read
const visible = all.filter((rel) => canRead(perms, join(path, rel.replace(/\/$/, ""))));
return visible.length === 0 ? "(empty or all entries denied)" : visible.sort().map((e: string) => `- ${e}`).join("\n");
}
const dirents = await readdir(path, { withFileTypes: true });
const lines: string[] = [];
for (const e of dirents) {
const fullPath = join(path, e.name);
const rule = resolveRule(perms, fullPath);
if (rule === "deny") {
// Show passthrough only if a sub-rule grants access inside
const hasInnerAccess = Object.keys(perms.rules).some((r) => {
const rR = resolve(r);
const rF = resolve(fullPath);
return rR.startsWith(rF + "/") && (perms.rules[r] === "read" || perms.rules[r] === "read_write");
});
if (hasInnerAccess) lines.push(`${e.name}${e.isDirectory() ? "/" : ""} [passthrough — denied but contains accessible paths]`);
// else: completely hidden
} else {
lines.push(e.name + (e.isDirectory() ? "/" : ""));
}
}
return lines.length === 0 ? "(empty or all entries denied)" : lines.sort().map((l: string) => `- ${l}`).join("\n");
},
}));
// ── CREATE DIRECTORY ─────────────────────────────────────────────────────
tools.push(tool({
name: "create_directory",
description: "Create a directory (and parents). Path must be in a writable location.",
parameters: { path: z.string().min(1) },
implementation: async ({ path }) => {
path = expandPath(path);
const perms = loadPermissions();
const denied = checkWrite(perms, path);
if (denied) return `Error: ${denied}`;
await mkdir(path, { recursive: true });
return `Created directory ${resolve(path)}`;
},
}));
// ── DELETE PATH ──────────────────────────────────────────────────────────
tools.push(tool({
name: "delete_path",
description:
"Delete a file or directory. Directories require recursive=true. " +
"Path must be in a writable location.",
parameters: {
path: z.string().min(1),
recursive: z.boolean().optional(),
},
implementation: async ({ path, recursive }) => {
path = expandPath(path);
const perms = loadPermissions();
const denied = checkWrite(perms, path);
if (denied) return `Error: ${denied}`;
if (!existsSync(path)) return "Error: path does not exist.";
if (statSync(path).isDirectory() && !recursive)
return "Error: path is a directory — set recursive=true.";
await rm(path, { recursive: Boolean(recursive), force: false });
return `Deleted ${resolve(path)}`;
},
}));
// ── MOVE / RENAME ────────────────────────────────────────────────────────
tools.push(tool({
name: "move_path",
description:
"Move or rename a file/directory. Both source and destination must be in writable locations.",
parameters: {
source: z.string().min(1),
destination: z.string().min(1),
},
implementation: async ({ source, destination }) => {
source = expandPath(source);
destination = expandPath(destination);
const perms = loadPermissions();
const dSrc = checkWrite(perms, source);
if (dSrc) return `Error (source): ${dSrc}`;
const dDst = checkWrite(perms, destination);
if (dDst) return `Error (destination): ${dDst}`;
if (!existsSync(source)) return "Error: source does not exist.";
const dir = dirname(resolve(destination));
if (!existsSync(dir)) await mkdir(dir, { recursive: true });
await rename(source, destination);
return `Moved ${resolve(source)} → ${resolve(destination)}`;
},
}));
// ── GET PERMISSIONS ──────────────────────────────────────────────────────
tools.push(tool({
name: "get_permissions",
description:
"Report the effective permission policy: global default, per-path rules, " +
"and command executor status. Call this first when unsure of what you can access.",
parameters: {},
implementation: async () => {
const p = loadPermissions();
const ce = p.command_executor;
const ruleLines = Object.entries(p.rules)
.sort(([a], [b]) => a.localeCompare(b))
.map(([path, rule]) => ` ${rule.padEnd(12)} ${path}`);
return [
`Default permission: ${p.default}`,
"",
ruleLines.length ? `Path rules:\n${ruleLines.join("\n")}` : "Path rules: (none)",
"",
`Command executor: ${ce.enabled ? "ENABLED" : "disabled"}`,
` (working dir tier comes from path permissions — see File Manager)`,
` allow list : ${ce.allow_commands.join(", ") || "(any)"}`,
` deny list : ${ce.deny_commands.join(", ")}`,
` timeout : ${ce.timeout_ms}ms`,
].join("\n");
},
}));
return tools;
}