src / toolsProvider.ts
/**
* High-Performance Tools — toolsProvider
*
* 40 tools across 6 categories:
* Filesystem · Execution · Python Dev · C++ Dev · Research · Git
*/
import { text, tool, type Tool, type ToolsProvider } from "@lmstudio/sdk";
import {
readFile, writeFile, appendFile, unlink, rename,
copyFile, mkdir, readdir, stat, rm,
} from "fs/promises";
import { existsSync, statSync } from "fs";
import { join, dirname, basename, extname, relative, resolve } from "path";
import { tmpdir } from "os";
import { webSearch as _webSearch, type TimeRange } from "./search";
import { z } from "zod";
import { pluginConfigSchematics } from "./config";
import {
SandboxError, RunResult, safe, run, runShell, detectCxx, getWorkspace, resolvePython,
} from "./sandbox";
import { memorySave, memoryRecall, memoryList, memoryDelete } from "./agents/memory";
import { spawnAgent, listLoadedModels } from "./agents/subagent";
import { createTool, callTool, listTools, deleteTool } from "./agents/dynamicTools";
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
function json(obj: unknown): string {
return JSON.stringify(obj, null, 2);
}
function disabled(toolName: string): string {
return `Tool '${toolName}' is disabled in plugin settings. Enable it under Settings → Plugins → high-perf-tools.`;
}
function stripHtml(html: string): string {
return html
.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, " ")
.replace(/<[^>]+>/g, " ")
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
.replace(/ /g, " ").replace(/"/g, '"').replace(/'/g, "'")
.replace(/[ \t]+/g, " ")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
async function fileExists(p: string): Promise<boolean> {
try { await stat(p); return true; } catch { return false; }
}
/** Generate a simple unified diff between two strings (no external deps). */
function unifiedDiff(oldText: string, newText: string, label = "file"): string {
const oldLines = oldText.split("\n");
const newLines = newText.split("\n");
const lines: string[] = [`--- a/${label}`, `+++ b/${label}`];
// Basic LCS-free diff: find changed blocks with ±3 context lines
let i = 0, j = 0;
const chunks: string[] = [];
while (i < oldLines.length || j < newLines.length) {
if (oldLines[i] === newLines[j]) { chunks.push(` ${oldLines[i]}`); i++; j++; }
else {
// Find next sync point (up to 8 lines ahead)
let oi = i, ni = j;
let found = false;
for (let d = 1; d <= 8 && !found; d++) {
for (let k = 0; k <= d; k++) {
if (oldLines[i + k] !== undefined && oldLines[i + k] === newLines[j + d - k]) {
for (let x = 0; x < k; x++) chunks.push(`-${oldLines[i + x] ?? ""}`);
for (let x = 0; x < d - k; x++) chunks.push(`+${newLines[j + x] ?? ""}`);
i += k; j += d - k; found = true; break;
}
}
}
if (!found) {
if (i < oldLines.length) chunks.push(`-${oldLines[i++]}`);
if (j < newLines.length) chunks.push(`+${newLines[j++]}`);
}
}
}
// Collapse unchanged runs into @@ hunks with 3 lines of context
const CONTEXT = 3;
let inHunk = false;
const result: string[] = [lines[0], lines[1]];
for (let idx = 0; idx < chunks.length; idx++) {
const changed = chunks[idx][0] === "+" || chunks[idx][0] === "-";
if (changed) {
if (!inHunk) {
const ctx = Math.max(0, idx - CONTEXT);
result.push(`@@ -${ctx + 1} @@`);
for (let c = ctx; c < idx; c++) result.push(chunks[c]);
inHunk = true;
}
result.push(chunks[idx]);
} else if (inHunk) {
result.push(chunks[idx]);
// End hunk after CONTEXT unchanged lines
const remaining = chunks.slice(idx + 1).findIndex((l) => l[0] === "+" || l[0] === "-");
if (remaining === -1 || remaining >= CONTEXT) inHunk = false;
}
}
return result.length > 2 ? result.join("\n") : "(no differences)";
}
/**
* Wrap a tool implementation so any thrown error is returned as a structured
* message the model can read and act on, instead of silently failing.
*/
function safe_impl<T extends Record<string, unknown>>(
name: string,
fn: (params: T) => Promise<string>
): (params: T) => Promise<string> {
return async (params: T) => {
try {
return await fn(params);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
// Return a structured error the model can parse and self-correct from
return JSON.stringify({
tool_error: true,
tool: name,
error: msg,
hint: "Read the error above, fix the parameter causing the issue, and retry the tool call.",
}, null, 2);
}
};
}
// Shorthand coerced primitive schemas — more lenient than strict zBool(false)/z.number()
const zBool = (def: boolean) => z.coerce.boolean().default(def);
const zInt = (def: number) => z.coerce.number().int().default(def);
const zNum = (def: number) => z.coerce.number().default(def);
// ---------------------------------------------------------------------------
// Tools Provider
// ---------------------------------------------------------------------------
export const toolsProvider: ToolsProvider = async (ctl) => {
const cfg = ctl.getPluginConfig(pluginConfigSchematics);
const ws = () => getWorkspace(cfg.get("workspacePath"));
const timeout = () => cfg.get("commandTimeoutSeconds");
// Resolve the Python interpreter once (cached per provider instantiation).
// Re-reads config each call so switching envs in settings takes effect on next chat.
const py = async () => resolvePython(cfg.get("pythonInterpreter"));
// Permission helpers
const canShell = () => cfg.get("allowShellCommands");
const canPython = () => cfg.get("allowPythonExecution");
const canCpp = () => cfg.get("allowCppCompilation");
const canPip = () => cfg.get("allowPipInstall");
const canGitWrite = () => cfg.get("allowGitWrite");
const preferClang = () => cfg.get("preferClang");
const searxng = () => cfg.get("searxngUrl").trim() || undefined;
const searchWindow = (): TimeRange | undefined => {
const v = cfg.get("searchRecencyWindow").trim().toLowerCase();
return (["day", "week", "month", "year"].includes(v) ? v : undefined) as TimeRange | undefined;
};
const webSearch = (query: string, max?: number, timeRange?: TimeRange) =>
_webSearch(query, max, 10_000, searxng(), timeRange ?? searchWindow());
const tools: Tool[] = [
// =========================================================================
// FILESYSTEM
// =========================================================================
tool({
name: "read_file",
description: text`
Read a file from the workspace and return its contents.
For large files, use start_line and end_line to read a range.
Binary files return a hex summary.
`,
parameters: {
path: z.string().describe("Relative path inside the workspace"),
start_line: z.coerce.number().int().min(1).default(1).describe("First line to return (1-indexed)"),
end_line: zInt(0).describe("Last line to return (0 = EOF)"),
},
implementation: safe_impl("read_file", async ({ path, start_line, end_line }) => {
const p = safe(ws(), path);
if (!await fileExists(p)) return `File not found: ${path}`;
const s = statSync(p);
if (s.size > 100 * 1024 * 1024) return `File too large (${(s.size/1e6).toFixed(1)} MB). Use start_line/end_line.`;
try {
const text = await readFile(p, "utf-8");
const lines = text.split("\n");
const total = lines.length;
const from = start_line - 1;
const to = end_line > 0 ? end_line : total;
return `[Lines ${from+1}–${Math.min(to, total)} of ${total} | ${relative(ws(), p)}]\n` + lines.slice(from, to).join("\n");
} catch {
const raw = await readFile(p);
return `[Binary — ${s.size} bytes]\n${raw.slice(0, 512).toString("hex").replace(/(.{2})/g, "$1 ")}`;
}
}),
}),
tool({
name: "write_file",
description: text`
Write content to a file in the workspace, creating parent directories as needed.
Set append=true to append instead of overwrite.
`,
parameters: {
path: z.string().describe("Relative path inside the workspace"),
content: z.string().describe("Text content to write"),
append: zBool(false).describe("Append instead of overwrite"),
},
implementation: safe_impl("write_file", async ({ path, content, append }) => {
const p = safe(ws(), path);
await mkdir(dirname(p), { recursive: true });
if (append) {
await appendFile(p, content, "utf-8");
} else {
await writeFile(p, content, "utf-8");
}
const s = statSync(p);
return `${append ? "Appended" : "Wrote"} ${content.length} chars → ${relative(ws(), p)} (${s.size} bytes on disk)`;
}),
}),
tool({
name: "replace_in_file",
description: text`
Surgically replace text in a file without rewriting the whole thing.
Set regex=true to use a JavaScript regular expression as the search string.
Set count to limit the number of replacements (0 = all).
`,
parameters: {
path: z.string().describe("Relative path inside the workspace"),
search: z.string().describe("Exact string or regex pattern to find"),
replacement: z.string().describe("Replacement text"),
regex: zBool(false).describe("Treat search as a JS regex"),
count: z.coerce.number().int().min(0).default(0).describe("Max replacements (0 = all)"),
},
implementation: safe_impl("replace_in_file", async ({ path, search, replacement, regex, count }) => {
const p = safe(ws(), path);
if (!await fileExists(p)) return `File not found: ${path}`;
let original = await readFile(p, "utf-8");
let n = 0;
let result: string;
if (regex) {
const re = new RegExp(search, "g");
let replaced = 0;
result = original.replace(re, (match, ...args) => {
if (count > 0 && replaced >= count) return match;
replaced++;
n++;
return replacement;
});
} else {
// Literal replacement — count all occurrences manually
const idx = original.indexOf(search);
if (idx === -1) return `No matches for: ${search}`;
let pos = 0;
result = "";
let found = 0;
while (true) {
const i = original.indexOf(search, pos);
if (i === -1 || (count > 0 && found >= count)) {
result += original.slice(pos);
break;
}
result += original.slice(pos, i) + replacement;
pos = i + search.length;
found++;
n++;
}
}
if (n === 0) return `No matches found for: ${search}`;
await writeFile(p, result, "utf-8");
return `Made ${n} replacement(s) in ${relative(ws(), p)}`;
}),
}),
tool({
name: "diff_preview",
description: text`
Preview what a write_file or replace_in_file call would change — shows a unified diff without modifying anything.
Use this before overwriting a file you haven't reviewed, or when making a large replacement.
Modes:
- write: shows diff between current file and new_content
- replace: shows diff of applying search→replacement in the file
`,
parameters: {
path: z.string().describe("Relative path inside the workspace"),
mode: z.enum(["write", "replace"]).describe("'write' to preview full overwrite, 'replace' to preview a substitution"),
new_content: z.string().default("").describe("For mode=write: the content that would be written"),
search: z.string().default("").describe("For mode=replace: text or regex to find"),
replacement: z.string().default("").describe("For mode=replace: replacement text"),
regex: zBool(false).describe("For mode=replace: treat search as a JS regex"),
},
implementation: safe_impl("diff_preview", async ({ path, mode, new_content, search, replacement, regex }) => {
const p = safe(ws(), path);
const original = await fileExists(p) ? await readFile(p, "utf-8") : "";
const label = relative(ws(), p);
let proposed: string;
if (mode === "write") {
proposed = new_content;
} else {
if (!search) return "Error: search is required for mode=replace";
if (!original) return `File not found: ${path}`;
if (regex) {
proposed = original.replace(new RegExp(search, "g"), replacement);
} else {
proposed = original.split(search).join(replacement);
}
}
const diff = unifiedDiff(original, proposed, label);
const added = (diff.match(/^\+(?!\+\+)/mg) ?? []).length;
const removed = (diff.match(/^-(?!--)/mg) ?? []).length;
return [
`--- diff preview: ${label} ---`,
`+${added} lines -${removed} lines`,
"",
diff,
"",
`To apply: call ${mode === "write" ? "write_file" : "replace_in_file"} with the same parameters.`,
].join("\n");
}),
}),
tool({
name: "delete_path",
description: text`
Delete a file or directory from the workspace.
Directories require recursive=true unless empty.
`,
parameters: {
path: z.string().describe("Relative path to delete"),
recursive: zBool(false).describe("Required for non-empty directories"),
},
implementation: safe_impl("delete_path", async ({ path, recursive }) => {
const p = safe(ws(), path);
if (!await fileExists(p)) return `Not found: ${path}`;
const s = statSync(p);
if (s.isDirectory()) {
await rm(p, { recursive, force: false });
return `Deleted directory ${relative(ws(), p)}`;
}
await unlink(p);
return `Deleted file ${relative(ws(), p)}`;
}),
}),
tool({
name: "move_path",
description: text`Move or rename a file/directory within the workspace.`,
parameters: {
src: z.string().describe("Source path (relative)"),
dst: z.string().describe("Destination path (relative)"),
},
implementation: safe_impl("move_path", async ({ src, dst }) => {
const s = safe(ws(), src);
const d = safe(ws(), dst);
if (!await fileExists(s)) return `Not found: ${src}`;
await mkdir(dirname(d), { recursive: true });
await rename(s, d);
return `Moved ${relative(ws(), s)} → ${relative(ws(), d)}`;
}),
}),
tool({
name: "copy_path",
description: text`Copy a file within the workspace.`,
parameters: {
src: z.string().describe("Source file path (relative)"),
dst: z.string().describe("Destination path (relative)"),
},
implementation: safe_impl("copy_path", async ({ src, dst }) => {
const s = safe(ws(), src);
const d = safe(ws(), dst);
if (!await fileExists(s)) return `Not found: ${src}`;
await mkdir(dirname(d), { recursive: true });
await copyFile(s, d);
return `Copied ${relative(ws(), s)} → ${relative(ws(), d)}`;
}),
}),
tool({
name: "list_directory",
description: text`
List files and directories with metadata.
Use pattern like "*.py" to filter by extension.
Use recursive=true to walk subdirectories (capped at max_entries).
`,
parameters: {
path: z.string().default(".").describe("Directory to list (default: workspace root)"),
pattern: z.string().default("").describe("Filter: extension like '.py' or partial name"),
recursive: zBool(false).describe("Walk subdirectories"),
show_hidden: zBool(false).describe("Include dot-files"),
max_entries: z.coerce.number().int().min(1).max(10000).default(2000).describe("Entry limit"),
},
implementation: safe_impl("list_directory", async ({ path, pattern, recursive, show_hidden, max_entries }) => {
const p = safe(ws(), path);
if (!await fileExists(p)) return `Directory not found: ${path}`;
const entries: Array<{ name: string; type: string; size?: number }> = [];
let truncated = false;
async function walk(dir: string) {
if (entries.length >= max_entries) { truncated = true; return; }
const items = await readdir(dir, { withFileTypes: true });
for (const item of items) {
if (!show_hidden && item.name.startsWith(".")) continue;
if (pattern && !item.name.includes(pattern) && !item.name.endsWith(pattern)) continue;
if (entries.length >= max_entries) { truncated = true; return; }
const full = join(dir, item.name);
const rel = relative(ws(), full);
if (item.isDirectory()) {
entries.push({ name: rel, type: "dir" });
if (recursive) await walk(full);
} else {
try {
const st = statSync(full);
entries.push({ name: rel, type: "file", size: st.size });
} catch { /* skip */ }
}
}
}
await walk(p);
return json({ entries, count: entries.length, truncated });
}),
}),
tool({
name: "create_directory",
description: text`Create a directory (and all missing parents) inside the workspace.`,
parameters: {
path: z.string().describe("Relative path to create"),
},
implementation: safe_impl("create_directory", async ({ path }) => {
const p = safe(ws(), path);
await mkdir(p, { recursive: true });
return `Directory ready: ${relative(ws(), p)}`;
}),
}),
tool({
name: "file_info",
description: text`Return metadata about a file: size, line count, permissions, MIME type.`,
parameters: {
path: z.string().describe("Relative path to inspect"),
},
implementation: safe_impl("file_info", async ({ path }) => {
const p = safe(ws(), path);
if (!await fileExists(p)) return `Not found: ${path}`;
const s = statSync(p);
const info: Record<string, unknown> = {
path: relative(ws(), p),
type: s.isDirectory() ? "directory" : "file",
size_bytes: s.size,
modified: s.mtime.toISOString(),
mode: (s.mode & 0o777).toString(8),
};
if (s.isFile()) {
try {
const content = await readFile(p, "utf-8");
info.line_count = content.split("\n").length;
info.encoding = "utf-8";
} catch {
info.encoding = "binary";
}
}
return json(info);
}),
}),
tool({
name: "search_in_files",
description: text`
Search for a string or regex across files in the workspace (like grep).
Returns matching lines with surrounding context.
`,
parameters: {
pattern: z.string().describe("Text or regex to search for"),
directory: z.string().default(".").describe("Directory to search (default: workspace root)"),
file_extension: z.string().default("").describe('Restrict to files with this extension (e.g. ".py")'),
regex: zBool(false).describe("Treat pattern as a JS regex"),
case_sensitive: zBool(true).describe("Case-sensitive matching"),
context_lines: z.coerce.number().int().min(0).max(10).default(2).describe("Lines of context around each match"),
max_matches: z.coerce.number().int().min(1).max(500).default(200).describe("Stop after this many matches"),
},
implementation: safe_impl("search_in_files", async ({
pattern, directory, file_extension, regex, case_sensitive, context_lines, max_matches
}) => {
const base = safe(ws(), directory);
const flags = case_sensitive ? "" : "i";
let re: RegExp;
try {
re = new RegExp(regex ? pattern : pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
} catch (e: any) {
return `Invalid regex: ${e.message}`;
}
const results: string[] = [];
let total = 0;
async function walk(dir: string) {
const items = await readdir(dir, { withFileTypes: true });
for (const item of items) {
if (item.name.startsWith(".")) continue;
const full = join(dir, item.name);
if (item.isDirectory()) {
await walk(full);
continue;
}
if (file_extension && !item.name.endsWith(file_extension)) continue;
try {
const st = statSync(full);
if (st.size > 20 * 1024 * 1024) {
results.push(`[skipped ${relative(ws(), full)}: ${(st.size/1e6).toFixed(0)} MB]`);
continue;
}
const content = await readFile(full, "utf-8");
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
if (re.test(lines[i])) {
if (++total > max_matches) return;
const from = Math.max(0, i - context_lines);
const to = Math.min(lines.length - 1, i + context_lines);
const block = [`${relative(ws(), full)}:${i+1}: ${lines[i]}`];
for (let c = from; c <= to; c++) {
if (c !== i) block.push(` ${c+1}: ${lines[c]}`);
}
results.push(block.join("\n"));
}
}
} catch { /* skip binary/unreadable */ }
}
}
await walk(base);
if (total === 0) return `No matches for ${pattern} in ${directory}`;
const header = total > max_matches
? `Found ${max_matches}+ matches (stopped at limit):\n\n`
: `Found ${total} match(es):\n\n`;
return header + results.join("\n---\n");
}),
}),
// =========================================================================
// EXECUTION
// =========================================================================
tool({
name: "run_command",
description: text`
Run a shell command inside the workspace.
WARNING: This executes arbitrary commands — the cwd sandbox only sets the starting
directory. Enable in Settings → Plugins → high-perf-tools → Allow Shell Commands.
`,
parameters: {
command: z.string().describe("Shell command string"),
cwd: z.string().default(".").describe("Working directory relative to workspace"),
stdin: z.string().default("").describe("Text piped to stdin"),
},
implementation: safe_impl("run_command", async ({ command, cwd, stdin }) => {
if (!canShell()) return disabled("run_command");
const dir = safe(ws(), cwd);
const r = await runShell(command, { cwd: dir, stdin: stdin || undefined, timeout: timeout() });
return json({ returncode: r.code, stdout: r.stdout, stderr: r.stderr, success: r.success });
}),
}),
tool({
name: "run_python_code",
description: text`
Execute a Python code snippet. Creates a temp file and runs it with the system Python.
Enable in Settings → Allow Python Execution.
`,
parameters: {
code: z.string().describe("Python source code"),
stdin: z.string().default("").describe("Text piped to stdin"),
},
implementation: safe_impl("run_python_code", async ({ code, stdin }) => {
if (!canPython()) return disabled("run_python_code");
const tmp = join(tmpdir(), `lms_py_${Date.now()}.py`);
try {
await writeFile(tmp, code, "utf-8");
const r = await run(await py(), [tmp], { stdin: stdin || undefined, timeout: timeout() });
return json({ returncode: r.code, stdout: r.stdout, stderr: r.stderr, success: r.success });
} finally {
unlink(tmp).catch(() => {});
}
}),
}),
tool({
name: "run_python_file",
description: text`
Run a Python script that already exists in the workspace.
Enable in Settings → Allow Python Execution.
`,
parameters: {
path: z.string().describe("Relative path to the .py file"),
args: z.string().default("").describe("Space-separated arguments"),
stdin: z.string().default("").describe("Text piped to stdin"),
},
implementation: safe_impl("run_python_file", async ({ path, args, stdin }) => {
if (!canPython()) return disabled("run_python_file");
const p = safe(ws(), path);
if (!await fileExists(p)) return `File not found: ${path}`;
const argv = args.trim() ? args.split(/\s+/) : [];
const r = await run(await py(), [p, ...argv], {
cwd: ws(), stdin: stdin || undefined, timeout: timeout(),
});
return json({ returncode: r.code, stdout: r.stdout, stderr: r.stderr, success: r.success });
}),
}),
tool({
name: "get_environment",
description: text`
Return key runtime information: Python version, PATH, compiler availability,
and which development tools are installed.
`,
parameters: {},
implementation: safe_impl("get_environment", async () => {
const checks = [
"python3", "pip3", "gcc", "g++", "clang", "clang++",
"cmake", "make", "ninja", "ruff", "black", "mypy",
"pytest", "clang-format", "clang-tidy", "git",
];
const info: Record<string, string> = {};
await Promise.all(checks.map(async (t) => {
const r = await run("which", [t], { timeout: 3 });
info[t] = r.success ? r.stdout.trim() : "not found";
}));
const pyVer = await run(await py(), ["--version"], { timeout: 5 });
return json({
workspace: ws(),
platform: process.platform,
node: process.version,
python: pyVer.stdout.trim() || pyVer.stderr.trim(),
tools: info,
});
}),
}),
// =========================================================================
// PYTHON DEV
// =========================================================================
tool({
name: "analyze_python",
description: text`
Parse a Python file with the AST and return its full structure:
imports, classes (with methods), functions (with signatures and docstrings),
and top-level globals.
`,
parameters: {
path: z.string().describe("Relative path to the .py file"),
},
implementation: safe_impl("analyze_python", async ({ path }) => {
if (!canPython()) return disabled("analyze_python");
const p = safe(ws(), path);
if (!await fileExists(p)) return `File not found: ${path}`;
const script = `
import ast, json, sys
src = open(${JSON.stringify(p)}).read()
try:
tree = ast.parse(src, filename=${JSON.stringify(p)})
except SyntaxError as e:
print(json.dumps({"syntax_error": str(e)})); sys.exit(0)
result = {"imports": [], "classes": [], "functions": [], "globals": []}
def get_args(f):
a = f.args
pos = [f"{x.arg}/" for x in (a.posonlyargs or [])]
reg = [x.arg for x in a.args]
var = [f"*{a.vararg.arg}"] if a.vararg else []
kwo = [x.arg for x in a.kwonlyargs]
kw = [f"**{a.kwarg.arg}"] if a.kwarg else []
return pos + reg + var + kwo + kw
for node in tree.body:
if isinstance(node, (ast.Import, ast.ImportFrom)):
if isinstance(node, ast.Import):
result["imports"] += [a.asname or a.name for a in node.names]
else:
m = node.module or ""
result["imports"] += [f"{m}.{a.asname or a.name}" for a in node.names]
elif isinstance(node, ast.ClassDef):
methods = []
for item in node.body:
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
methods.append({"name": item.name, "async": isinstance(item, ast.AsyncFunctionDef),
"args": get_args(item), "returns": ast.unparse(item.returns) if item.returns else None,
"doc": ast.get_docstring(item), "line": item.lineno})
result["classes"].append({"name": node.name,
"bases": [ast.unparse(b) for b in node.bases],
"methods": methods, "doc": ast.get_docstring(node), "line": node.lineno})
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
result["functions"].append({"name": node.name, "async": isinstance(node, ast.AsyncFunctionDef),
"args": get_args(node), "returns": ast.unparse(node.returns) if node.returns else None,
"doc": ast.get_docstring(node), "line": node.lineno,
"decorators": [ast.unparse(d) for d in node.decorator_list]})
elif isinstance(node, ast.Assign):
for t in node.targets:
if isinstance(t, ast.Name): result["globals"].append(t.id)
print(json.dumps(result))
`;
const r = await run(await py(), ["-c", script], { timeout: 30 });
return r.success ? r.stdout.trim() : `Analysis failed:\n${r.stderr}`;
}),
}),
tool({
name: "format_python",
description: text`
Format a Python file in-place using ruff (preferred) or black.
Set check_only=true to report if formatting is needed without changing the file.
`,
parameters: {
path: z.string().describe("Relative path to the .py file or directory"),
tool: z.enum(["auto", "ruff", "black"]).default("auto").describe("Formatter to use"),
check_only: zBool(false).describe("Report only, do not modify"),
},
implementation: safe_impl("format_python", async ({ path, tool: fmt, check_only }) => {
const p = safe(ws(), path);
if (!await fileExists(p)) return `Not found: ${path}`;
const hasRuff = (await run("which", ["ruff"], { timeout: 3 })).success;
const hasBlack = (await run("which", ["black"], { timeout: 3 })).success;
const useRuff = fmt === "auto" ? hasRuff : fmt === "ruff";
if (useRuff && !hasRuff) return "ruff not found. Install with: pip install ruff";
if (!useRuff && !hasBlack) return "black not found. Install with: pip install black";
let cmd: string; let args: string[];
if (useRuff) {
cmd = "ruff"; args = ["format", ...(check_only ? ["--check"] : []), p];
} else {
cmd = "black"; args = [...(check_only ? ["--check"] : []), p];
}
const r = await run(cmd, args, { timeout: 30 });
return (r.stdout + r.stderr).trim() || `Formatted with ${useRuff ? "ruff" : "black"}`;
}),
}),
tool({
name: "lint_python",
description: text`
Lint a Python file and return diagnostics (ruff or pylint).
Set fix=true to auto-fix safe violations (ruff only).
`,
parameters: {
path: z.string().describe("Relative path to .py file or directory"),
tool: z.enum(["auto", "ruff", "pylint"]).default("auto").describe("Linter to use"),
fix: zBool(false).describe("Auto-fix safe violations (ruff only)"),
},
implementation: safe_impl("lint_python", async ({ path, tool: linter, fix }) => {
const p = safe(ws(), path);
if (!await fileExists(p)) return `Not found: ${path}`;
const hasRuff = (await run("which", ["ruff"], { timeout: 3 })).success;
const useRuff = linter === "auto" ? hasRuff : linter === "ruff";
let cmd: string; let args: string[];
if (useRuff) {
cmd = "ruff"; args = ["check", ...(fix ? ["--fix"] : []), "--output-format", "text", p];
} else {
cmd = "pylint"; args = [p];
}
const r = await run(cmd, args, { timeout: 60 });
return (r.stdout + r.stderr).trim() || "No issues found.";
}),
}),
tool({
name: "run_tests",
description: text`
Run pytest on a path and return the test report.
Use pattern to filter tests by name (passed as -k).
`,
parameters: {
path: z.string().default(".").describe("Path to test file or directory"),
pattern: z.string().default("").describe("pytest -k pattern to filter tests"),
verbose: zBool(true).describe("Show individual test outcomes"),
extra_args: z.string().default("").describe("Additional pytest arguments"),
},
implementation: safe_impl("run_tests", async ({ path, pattern, verbose, extra_args }) => {
if (!canPython()) return disabled("run_tests");
const p = safe(ws(), path);
const python = await py();
// Use the env's own pytest if available, otherwise fall back to python -m pytest
const envPytest = python.replace(/\/python[^/]*$/, "/pytest");
const hasPytest = (await run(envPytest, ["--version"], { timeout: 3 })).success;
const extraFlags = [
...(verbose ? ["-v"] : []),
...(pattern ? ["-k", pattern] : []),
...(extra_args.trim() ? extra_args.split(/\s+/) : []),
p,
];
const cmd = hasPytest ? envPytest : python;
const args = hasPytest ? extraFlags : ["-m", "pytest", ...extraFlags];
const r = await run(cmd, args, { cwd: ws(), timeout: timeout() });
return (r.stdout + r.stderr).trim();
}),
}),
tool({
name: "pip_install",
description: text`
Install Python packages with pip.
Enable in Settings → Allow pip install.
`,
parameters: {
packages: z.string().describe("Space-separated package names (e.g. 'requests numpy')"),
upgrade: zBool(false).describe("Pass --upgrade flag"),
},
implementation: safe_impl("pip_install", async ({ packages, upgrade }) => {
if (!canPip()) return disabled("pip_install");
const pkgs = packages.trim().split(/\s+/).filter(Boolean);
if (!pkgs.length) return "No packages specified.";
const args = ["-m", "pip", "install", ...(upgrade ? ["--upgrade"] : []), ...pkgs];
const r = await run(await py(), args, { timeout: 180 });
return (r.stdout + r.stderr).trim();
}),
}),
tool({
name: "pip_list",
description: text`List installed Python packages. Set outdated=true to show only outdated packages.`,
parameters: {
outdated: zBool(false).describe("Show only outdated packages"),
},
implementation: safe_impl("pip_list", async ({ outdated }) => {
const args = ["-m", "pip", "list", "--format", "json", ...(outdated ? ["--outdated"] : [])];
const r = await run(await py(), args, { timeout: 30 });
return r.success ? r.stdout.trim() : r.stderr;
}),
}),
tool({
name: "type_check_python",
description: text`Run mypy type-checker on a Python file or package.`,
parameters: {
path: z.string().describe("Relative path to check"),
strict: zBool(false).describe("Enable --strict mode"),
},
implementation: safe_impl("type_check_python", async ({ path, strict }) => {
const hasMypy = (await run("which", ["mypy"], { timeout: 3 })).success;
if (!hasMypy) return "mypy not found. Install with: pip install mypy";
const p = safe(ws(), path);
const r = await run("mypy", [...(strict ? ["--strict"] : []), p], { cwd: ws(), timeout: 120 });
return (r.stdout + r.stderr).trim() || "No type errors found.";
}),
}),
// =========================================================================
// C++ DEV
// =========================================================================
tool({
name: "compile_cpp",
description: text`
Compile a C++ source file with the best available compiler (clang++ or g++).
Supports AddressSanitizer, UBSan, and custom compiler flags.
Enable in Settings → Allow C++ Compilation.
`,
parameters: {
path: z.string().describe("Relative path to the .cpp source file"),
output: z.string().default("").describe("Output binary name (default: source stem)"),
standard: z.enum(["c++11", "c++14", "c++17", "c++20", "c++23"]).default("c++17").describe("C++ standard"),
flags: z.string().default("-Wall -Wextra").describe("Extra compiler flags"),
sanitizers: z.string().default("").describe("Comma-separated: address, undefined, thread"),
optimisation: z.enum(["0", "1", "2", "3", "s", "fast"]).default("0").describe("Optimisation level"),
},
implementation: safe_impl("compile_cpp", async ({ path, output, standard, flags, sanitizers, optimisation }) => {
if (!canCpp()) return disabled("compile_cpp");
const cxx = await detectCxx(preferClang());
if (!cxx) return "No C++ compiler found. Install g++ or clang++.";
const src = safe(ws(), path);
if (!await fileExists(src)) return `File not found: ${path}`;
const outName = output || basename(src, extname(src));
const outPath = safe(ws(), outName);
const args = [src, `-std=${standard}`, `-O${optimisation}`, "-o", outPath];
if (flags.trim()) args.push(...flags.split(/\s+/));
const sanMap: Record<string, string> = {
address: "-fsanitize=address",
undefined: "-fsanitize=undefined",
thread: "-fsanitize=thread",
};
for (const s of sanitizers.split(",").map(s => s.trim()).filter(Boolean)) {
if (!sanMap[s]) return `Unknown sanitizer: '${s}'. Choose from: ${Object.keys(sanMap).join(", ")}`;
args.push(sanMap[s]);
}
const r = await run(cxx, args, { cwd: ws(), timeout: 120 });
return json({ compiler: cxx, command: [cxx, ...args].join(" "), output_binary: r.success ? relative(ws(), outPath) : null, ...r });
}),
}),
tool({
name: "run_binary",
description: text`
Execute a compiled binary that lives inside the workspace.
Enable in Settings → Allow C++ Compilation.
`,
parameters: {
path: z.string().describe("Relative path to the binary"),
args: z.string().default("").describe("Space-separated arguments"),
stdin: z.string().default("").describe("Text piped to stdin"),
},
implementation: safe_impl("run_binary", async ({ path, args, stdin }) => {
if (!canCpp()) return disabled("run_binary");
const p = safe(ws(), path);
if (!await fileExists(p)) return `Binary not found: ${path}`;
const argv = args.trim() ? args.split(/\s+/) : [];
const r = await run(p, argv, { cwd: ws(), stdin: stdin || undefined, timeout: timeout() });
return json({ returncode: r.code, stdout: r.stdout, stderr: r.stderr, success: r.success });
}),
}),
tool({
name: "compile_and_run",
description: text`
Compile C++ source code and immediately run it — ideal for quick snippets.
If is_file=false, source_or_path is treated as raw C++ source code.
Enable in Settings → Allow C++ Compilation.
`,
parameters: {
source_or_path: z.string().describe("Raw C++ source code OR relative file path (see is_file)"),
stdin: z.string().default("").describe("Input piped to the program"),
standard: z.enum(["c++11", "c++14", "c++17", "c++20", "c++23"]).default("c++17"),
flags: z.string().default("-Wall -Wextra").describe("Extra compile flags"),
sanitizers: z.string().default("address,undefined").describe("Comma-separated sanitizers"),
is_file: zBool(false).describe("Treat source_or_path as a file path"),
},
implementation: safe_impl("compile_and_run", async ({ source_or_path, stdin, standard, flags, sanitizers, is_file }) => {
if (!canCpp()) return disabled("compile_and_run");
const cxx = await detectCxx(preferClang());
if (!cxx) return "No C++ compiler found.";
let srcPath: string;
let cleanup = false;
if (is_file) {
srcPath = safe(ws(), source_or_path);
if (!await fileExists(srcPath)) return `File not found: ${source_or_path}`;
} else {
srcPath = join(tmpdir(), `lms_cpp_${Date.now()}.cpp`);
await writeFile(srcPath, source_or_path, "utf-8");
cleanup = true;
}
const binPath = srcPath.replace(/\.cpp$/, "").replace(/\.cc$/, "") + "_bin";
try {
// Compile
const compileArgs = [srcPath, `-std=${standard}`, "-O0", "-o", binPath];
if (flags.trim()) compileArgs.push(...flags.split(/\s+/));
const sanMap: Record<string, string> = {
address: "-fsanitize=address",
undefined: "-fsanitize=undefined",
thread: "-fsanitize=thread",
};
for (const s of sanitizers.split(",").map(s => s.trim()).filter(Boolean)) {
if (sanMap[s]) compileArgs.push(sanMap[s]);
}
const comp = await run(cxx, compileArgs, { timeout: 120 });
if (!comp.success) {
return json({ phase: "compile", returncode: comp.code, error: comp.stderr, success: false });
}
// Run
const execResult = await run(binPath, [], {
stdin: stdin || undefined, timeout: timeout(),
});
return json({
compile: { returncode: comp.code, warnings: comp.stderr },
run: { returncode: execResult.code, stdout: execResult.stdout, stderr: execResult.stderr, success: execResult.success },
});
} finally {
if (cleanup) unlink(srcPath).catch(() => {});
unlink(binPath).catch(() => {});
}
}),
}),
tool({
name: "format_cpp",
description: text`
Format a C/C++ source file with clang-format.
style can be: LLVM, Google, Mozilla, WebKit, Microsoft, or "file" (uses .clang-format).
`,
parameters: {
path: z.string().describe("Relative path to the source file"),
style: z.string().default("LLVM").describe("clang-format style preset or 'file'"),
check_only: zBool(false).describe("Report only, do not modify"),
},
implementation: safe_impl("format_cpp", async ({ path, style, check_only }) => {
const hasClangFmt = (await run("which", ["clang-format"], { timeout: 3 })).success;
if (!hasClangFmt) return "clang-format not found. Install LLVM/Clang tools.";
const p = safe(ws(), path);
if (!await fileExists(p)) return `Not found: ${path}`;
const args = [`-style=${style}`, ...(check_only ? ["--dry-run", "--Werror"] : ["-i"]), p];
const r = await run("clang-format", args, { timeout: 30 });
if (check_only) return r.success ? "Already formatted." : `Formatting needed:\n${r.stderr}`;
return r.success ? `Formatted ${relative(ws(), p)}` : `clang-format error:\n${r.stderr}`;
}),
}),
tool({
name: "analyze_cpp",
description: text`
Run clang-tidy static analysis on a C++ source file.
Returns diagnostics with file:line:col locations.
`,
parameters: {
path: z.string().describe("Relative path to the source file"),
checks: z.string().default("clang-analyzer-*,bugprone-*,modernize-*,performance-*").describe("Comma-separated check globs"),
fix: zBool(false).describe("Apply auto-fixes where available"),
},
implementation: safe_impl("analyze_cpp", async ({ path, checks, fix }) => {
const hasTidy = (await run("which", ["clang-tidy"], { timeout: 3 })).success;
if (!hasTidy) return "clang-tidy not found. Install LLVM/Clang tools.";
const p = safe(ws(), path);
if (!await fileExists(p)) return `Not found: ${path}`;
const args = [`--checks=${checks}`, ...(fix ? ["--fix"] : []), p];
const r = await run("clang-tidy", args, { cwd: ws(), timeout: 120 });
return (r.stdout + r.stderr).trim() || "No issues found by clang-tidy.";
}),
}),
tool({
name: "cmake_build",
description: text`
Configure and build a CMake project.
Steps: mkdir build_dir → cmake configure → cmake --build.
Enable in Settings → Allow C++ Compilation.
`,
parameters: {
source_dir: z.string().default(".").describe("Directory containing CMakeLists.txt"),
build_dir: z.string().default("build").describe("Out-of-source build directory"),
cmake_args: z.string().default("").describe("Extra cmake args (e.g. '-DCMAKE_BUILD_TYPE=Release')"),
build_target: z.string().default("").describe("Specific build target (empty = default)"),
clean: zBool(false).describe("Run cmake --build --target clean first"),
},
implementation: safe_impl("cmake_build", async ({ source_dir, build_dir, cmake_args, build_target, clean }) => {
if (!canCpp()) return disabled("cmake_build");
const hasCmake = (await run("which", ["cmake"], { timeout: 3 })).success;
if (!hasCmake) return "cmake not found. Install CMake.";
const src = safe(ws(), source_dir);
const bld = safe(ws(), build_dir);
await mkdir(bld, { recursive: true });
// Configure
const cfgArgs = [src, "-B", bld, ...(cmake_args.trim() ? cmake_args.split(/\s+/) : [])];
const cfg = await run("cmake", cfgArgs, { cwd: ws(), timeout: 120 });
if (!cfg.success) return json({ phase: "configure", returncode: cfg.code, error: cfg.stderr, success: false });
// Clean
if (clean) await run("cmake", ["--build", bld, "--target", "clean"], { timeout: 60 });
// Build
const bldArgs = ["--build", bld, "--parallel", ...(build_target ? ["--target", build_target] : [])];
const bldResult = await run("cmake", bldArgs, { timeout: 300 });
return json({
configure: { returncode: cfg.code, output: cfg.stdout },
build: { returncode: bldResult.code, stdout: bldResult.stdout, stderr: bldResult.stderr, success: bldResult.success },
});
}),
}),
tool({
name: "get_cpp_info",
description: text`Return version strings for available C++ compilers and tools.`,
parameters: {},
implementation: safe_impl("get_cpp_info", async () => {
const tools = ["clang++", "g++", "cmake", "make", "ninja", "clang-format", "clang-tidy", "ar"];
const info: Record<string, string> = {};
await Promise.all(tools.map(async (t) => {
const w = await run("which", [t], { timeout: 3 });
if (!w.success) { info[t] = "not found"; return; }
const v = await run(t, ["--version"], { timeout: 5 });
info[t] = (v.stdout + v.stderr).split("\n")[0].trim();
}));
return json(info);
}),
}),
// =========================================================================
// RESEARCH
// =========================================================================
tool({
name: "fetch_url",
description: text`
Fetch a web page and return its text content (HTML stripped by default).
Only public URLs (http/https) are supported — private/internal IPs are blocked.
`,
parameters: {
url: z.string().describe("Full URL to fetch (must start with http:// or https://)"),
raw_html: zBool(false).describe("Return raw HTML instead of plain text"),
max_chars: zInt(50000).describe("Truncate at this many characters"),
},
implementation: safe_impl("fetch_url", async ({ url, raw_html, max_chars }) => {
if (!url.startsWith("http://") && !url.startsWith("https://")) {
return "Error: URL must start with http:// or https://";
}
// Block private/link-local IPs via a quick DNS+check approach
try {
const { hostname } = new URL(url);
// Rough private IP pattern check (before DNS resolution)
const privatePatterns = [
/^localhost$/i,
/^127\./,
/^10\./,
/^172\.(1[6-9]|2\d|3[01])\./,
/^192\.168\./,
/^169\.254\./,
/^::1$/,
/^fc[0-9a-f]{2}:/i,
/^fe[89ab][0-9a-f]:/i,
];
if (privatePatterns.some(r => r.test(hostname))) {
return `Request blocked: '${hostname}' is a private/internal address`;
}
} catch {
return "Invalid URL";
}
try {
const res = await fetch(url, {
headers: { "User-Agent": "Mozilla/5.0 (LM Studio high-perf-tools/1.0)" },
signal: AbortSignal.timeout(20_000),
});
if (!res.ok) return `HTTP ${res.status} ${res.statusText}`;
let body = await res.text();
if (!raw_html) body = stripHtml(body);
if (body.length > max_chars) body = body.slice(0, max_chars) + `\n\n[... truncated at ${max_chars} chars ...]`;
return body;
} catch (e: any) {
return `Fetch failed: ${e.message}`;
}
}),
}),
tool({
name: "search_web",
description: text`
Search the web with DuckDuckGo and return a list of results with titles, URLs, and snippets.
`,
parameters: {
query: z.string().describe("Search query"),
num_results: z.coerce.number().int().min(1).max(20).default(5).describe("Number of results"),
time_range: z.enum(["day", "week", "month", "year", "any"]).default("any")
.describe("Limit results to a recency window. 'any' = no filter (default). Applied natively on each search engine."),
},
implementation: safe_impl("search_web", async ({ query, num_results, time_range }) => {
const tr = time_range === "any" ? undefined : time_range as TimeRange;
const items = await webSearch(query, num_results, tr);
return json(items);
}),
}),
// =========================================================================
// GIT
// =========================================================================
tool({
name: "git_status",
description: text`Show working-tree status (staged, unstaged, untracked files).`,
parameters: {
path: z.string().default(".").describe("Relative path of git repository"),
},
implementation: safe_impl("git_status", async ({ path }) => {
const p = safe(ws(), path);
const r = await run("git", ["status", "--short", "--branch"], { cwd: p, timeout: 15 });
return r.stdout || r.stderr || "Not a git repository";
}),
}),
tool({
name: "git_diff",
description: text`
Show diff of changes in the repository.
Set staged=true for the index diff. Optionally restrict to a specific file.
`,
parameters: {
path: z.string().default(".").describe("Relative path of git repository"),
staged: zBool(false).describe("Show staged diff instead of working-tree"),
file_path: z.string().default("").describe("Restrict diff to this file"),
},
implementation: safe_impl("git_diff", async ({ path, staged, file_path }) => {
const p = safe(ws(), path);
const args = ["diff", ...(staged ? ["--cached"] : []), ...(file_path.trim() ? ["--", file_path] : [])];
const r = await run("git", args, { cwd: p, timeout: 30 });
if (r.stdout) return r.stdout;
return r.stderr || "(no diff)";
}),
}),
tool({
name: "git_log",
description: text`Show the git commit history.`,
parameters: {
path: z.string().default(".").describe("Relative path of git repository"),
n: z.coerce.number().int().min(1).max(100).default(15).describe("Number of commits"),
oneline: zBool(true).describe("Compact one-line format"),
author: z.string().default("").describe("Filter by author name/email"),
since: z.string().default("").describe("Show commits after this date (e.g. '2 weeks ago')"),
},
implementation: safe_impl("git_log", async ({ path, n, oneline, author, since }) => {
const p = safe(ws(), path);
const args = ["log", `-${n}`,
...(oneline ? ["--oneline"] : []),
...(author ? [`--author=${author}`] : []),
...(since ? [`--since=${since}`] : []),
];
const r = await run("git", args, { cwd: p, timeout: 15 });
return r.stdout || r.stderr;
}),
}),
tool({
name: "git_show",
description: text`Show details and diff of a specific commit.`,
parameters: {
path: z.string().default(".").describe("Relative path of git repository"),
ref: z.string().default("HEAD").describe("Commit hash, tag, or branch"),
},
implementation: safe_impl("git_show", async ({ path, ref }) => {
if (ref.startsWith("-")) return "Error: ref must not start with '-'";
const p = safe(ws(), path);
const r = await run("git", ["show", ref], { cwd: p, timeout: 15 });
return r.stdout || r.stderr;
}),
}),
tool({
name: "git_branches",
description: text`List all local and remote branches.`,
parameters: {
path: z.string().default(".").describe("Relative path of git repository"),
},
implementation: safe_impl("git_branches", async ({ path }) => {
const p = safe(ws(), path);
const r = await run("git", ["branch", "-a"], { cwd: p, timeout: 10 });
return r.stdout || r.stderr;
}),
}),
tool({
name: "git_stage",
description: text`
Stage files for the next commit (git add).
Enable Git Write in Settings.
Accepts explicit file paths OR glob patterns (e.g. "src/**/*.py", "*.ts").
Globs are expanded relative to the repo root before passing to git add.
`,
parameters: {
path: z.string().default(".").describe("Relative path of git repository"),
files: z.string().default(".").describe("Space-separated file paths or glob patterns (e.g. 'src/**/*.py *.md')"),
},
implementation: safe_impl("git_stage", async ({ path, files }) => {
if (!canGitWrite()) return disabled("git_stage");
const p = safe(ws(), path);
const patterns = files.trim().split(/\s+/).filter(Boolean);
// Expand any glob patterns using git ls-files --others --modified for untracked
// Simpler: pass patterns directly to git add — git itself handles globs when quoted
// But shell glob expansion won't happen in execFile; use git add with -- and patterns
const expanded: string[] = [];
for (const pattern of patterns) {
if (pattern.includes("*") || pattern.includes("?") || pattern.includes("{")) {
// Use git ls-files to expand glob
const ls = await run("git", ["ls-files", "--modified", "--others", "--exclude-standard", "-z"], { cwd: p, timeout: 15 });
const tracked = await run("git", ["ls-files", "-z"], { cwd: p, timeout: 15 });
const allFiles = [...new Set([
...ls.stdout.split("\0"),
...tracked.stdout.split("\0"),
])].filter(Boolean);
// Simple glob match using regex conversion
const globToRegex = (g: string) =>
new RegExp("^" + g.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]") + "$");
const re = globToRegex(pattern);
const matches = allFiles.filter((f) => re.test(f));
if (matches.length === 0) {
expanded.push(pattern); // Pass through — git will handle or error
} else {
expanded.push(...matches);
}
} else {
expanded.push(pattern);
}
}
const unique = [...new Set(expanded)];
const r = await run("git", ["add", "--", ...unique], { cwd: p, timeout: 30 });
const stagedCount = unique.filter((f) => !f.includes("*")).length;
return r.stdout || r.stderr || `Staged ${stagedCount} file(s): ${unique.slice(0, 10).join(", ")}${unique.length > 10 ? `… (+${unique.length - 10} more)` : ""}`;
}),
}),
tool({
name: "git_commit",
description: text`
Create a git commit with a message.
Optionally stage specified files first.
Enable Git Write in Settings.
`,
parameters: {
message: z.string().describe("Commit message"),
path: z.string().default(".").describe("Relative path of git repository"),
files: z.string().default("").describe("Files to stage before committing (empty = use already-staged)"),
},
implementation: safe_impl("git_commit", async ({ message, path, files }) => {
if (!canGitWrite()) return disabled("git_commit");
if (!message.trim()) return "Error: commit message cannot be empty";
const p = safe(ws(), path);
if (files.trim()) {
const stage = await run("git", ["add", ...files.split(/\s+/).filter(Boolean)], { cwd: p, timeout: 15 });
if (!stage.success) return `git add failed:\n${stage.stderr}`;
}
const r = await run("git", ["commit", "-m", message], { cwd: p, timeout: 30 });
return r.stdout || r.stderr;
}),
}),
tool({
name: "git_init",
description: text`Initialise a new git repository.`,
parameters: {
path: z.string().default(".").describe("Directory to initialise"),
},
implementation: safe_impl("git_init", async ({ path }) => {
const p = safe(ws(), path);
await mkdir(p, { recursive: true });
const r = await run("git", ["init"], { cwd: p, timeout: 15 });
return r.stdout || r.stderr;
}),
}),
// =========================================================================
// AGENT ZERO — Memory · Sub-agents · Dynamic Tools
// =========================================================================
// ── Memory ────────────────────────────────────────────────────────────────
tool({
name: "memory_save",
description: text`
Save a fact, solution, or piece of knowledge to persistent memory.
Memories survive across conversations.
Use a short descriptive key and optional tags for easy retrieval later.
`,
parameters: {
key: z.string().describe("Short title / identifier for this memory"),
content: z.string().describe("Full content to remember"),
tags: z.string().default("").describe("Comma-separated tags (e.g. 'python,bug-fix')"),
},
implementation: safe_impl("memory_save", async ({ key, content, tags }) => {
const tagList = tags.split(",").map(t => t.trim()).filter(Boolean);
return memorySave(ws(), key, content, tagList);
}),
}),
tool({
name: "memory_recall",
description: text`
Search persistent memory for entries matching a query.
Returns the most relevant memories sorted by keyword relevance.
Use tags to narrow the search.
`,
parameters: {
query: z.string().describe("What to search for (keywords)"),
limit: zInt(5).describe("Maximum results to return"),
tags: z.string().default("").describe("Comma-separated tags to filter by"),
},
implementation: safe_impl("memory_recall", async ({ query, limit, tags }) => {
const tagList = tags.split(",").map(t => t.trim()).filter(Boolean);
return memoryRecall(ws(), query, limit, tagList);
}),
}),
tool({
name: "memory_list",
description: text`List all saved memories (keys, tags, dates). Use tags to filter.`,
parameters: {
tags: z.string().default("").describe("Comma-separated tags to filter by"),
},
implementation: safe_impl("memory_list", async ({ tags }) => {
const tagList = tags.split(",").map(t => t.trim()).filter(Boolean);
return memoryList(ws(), tagList);
}),
}),
tool({
name: "memory_delete",
description: text`Delete a memory entry by its id or exact key.`,
parameters: {
id_or_key: z.string().describe("Memory id or exact key to delete"),
},
implementation: safe_impl("memory_delete", async ({ id_or_key }) => {
return memoryDelete(ws(), id_or_key);
}),
}),
// ── Sub-agents ────────────────────────────────────────────────────────────
tool({
name: "list_models",
description: text`
List all models currently loaded in LM Studio.
Use this before spawn_agent to see what models are available.
On low-RAM devices only one model will typically be shown.
`,
parameters: {},
implementation: safe_impl("list_models", async () => {
const endpoint = cfg.get("lmStudioEndpoint");
const models = await listLoadedModels(endpoint);
if (!models.length) return "No models currently loaded in LM Studio.";
return json({
loaded_models: models,
note: models.length === 1
? "Only one model loaded. spawn_agent will reuse it (no extra RAM required)."
: `${models.length} models loaded. spawn_agent can use a dedicated sub-agent model.`,
});
}),
}),
tool({
name: "spawn_agent",
description: text`
Spawn a sub-agent to handle a specific task autonomously.
RAM behaviour:
• If only one model is loaded (low-RAM device) → the same model is reused
with a different system prompt. No extra RAM consumed.
• If a Sub-Agent Model ID is configured in settings AND that model is loaded
→ it is used as the dedicated sub-agent.
The sub-agent has access to: read_file, write_file, list_directory,
run_python_code, run_command, memory_save, memory_recall.
Set allow_tools=false for a simple one-shot generation (faster, less RAM).
`,
parameters: {
task: z.string().describe("The task to delegate to the sub-agent"),
context: z.string().default("").describe("Optional background context to provide"),
system_role: z.string().default("").describe("Custom system prompt for the sub-agent (blank = sensible default)"),
allow_tools: zBool(true).describe("Whether the sub-agent can use tools (set false for pure generation tasks)"),
},
implementation: safe_impl("spawn_agent", async ({ task, context, system_role, allow_tools }) => {
const toolCtx = {
workspace: ws(),
pythonBin: await py(),
timeout: timeout(),
};
return spawnAgent({
endpoint: cfg.get("lmStudioEndpoint"),
preferredModelId: cfg.get("subAgentModelId"),
maxIterations: cfg.get("subAgentMaxIterations"),
systemPrompt: system_role,
task,
context,
toolCtx,
allowTools: allow_tools,
});
}),
}),
// ── Dynamic tools ─────────────────────────────────────────────────────────
tool({
name: "create_tool",
description: text`
Create a new reusable Python tool and save it to the workspace.
The tool persists across conversations and can be called by name with call_tool.
The python_code body receives an 'args' dict (from JSON stdin) and should
return a string or JSON-serialisable value.
Example python_code:
result = args.get("x", 0) + args.get("y", 0)
return str(result)
`,
parameters: {
name: z.string().describe("Tool name (alphanumeric + underscores)"),
description: z.string().describe("What this tool does"),
args_schema: z.string().default("").describe("Description of expected JSON args (free text)"),
python_code: z.string().describe("Python function body — receives 'args: dict', returns a value"),
},
implementation: safe_impl("create_tool", async ({ name, description, args_schema, python_code }) => {
return createTool(ws(), name, description, args_schema, python_code);
}),
}),
tool({
name: "call_tool",
description: text`
Call a previously created dynamic tool by name.
Pass arguments as a JSON string (e.g. '{"x": 1, "y": 2}').
`,
parameters: {
name: z.string().describe("Tool name (as given to create_tool)"),
args_json: z.string().default("{}").describe("JSON object of arguments"),
},
implementation: safe_impl("call_tool", async ({ name, args_json }) => {
return callTool(ws(), name, args_json, await py(), timeout());
}),
}),
tool({
name: "list_custom_tools",
description: text`List all dynamically created tools with their descriptions and argument schemas.`,
parameters: {},
implementation: safe_impl("list_custom_tools", async () => {
return listTools(ws());
}),
}),
tool({
name: "delete_tool",
description: text`Delete a dynamically created tool by name.`,
parameters: {
name: z.string().describe("Tool name to delete"),
},
implementation: safe_impl("delete_tool", async ({ name }) => {
return deleteTool(ws(), name);
}),
}),
];
return tools;
};