import * as fs from "node:fs"; import * as path from "node:path"; import { execSync } from "node:child_process"; export interface ToolDefinition { name: string; description: string; parameters: { type: "object"; properties: Record; required: string[]; }; } export interface ToolCall { name: string; arguments: Record; } export interface ToolResult { name: string; content: string; isError?: boolean; } export const TOOL_DEFINITIONS: ToolDefinition[] = [ { name: "readFile", description: "Read the contents of a file at the given path", parameters: { type: "object", properties: { path: { type: "string", description: "Absolute file path to read" }, }, required: ["path"], }, }, { name: "writeFile", description: "Write content to a file, creating it if it doesn't exist", parameters: { type: "object", properties: { path: { type: "string", description: "Absolute file path to write" }, content: { type: "string", description: "Content to write to the file" }, }, required: ["path", "content"], }, }, { name: "editFile", description: "Replace an exact string in a file with a new string", parameters: { type: "object", properties: { path: { type: "string", description: "Absolute file path to edit" }, old: { type: "string", description: "Exact string to find in the file" }, new: { type: "string", description: "String to replace it with" }, }, required: ["path", "old", "new"], }, }, { name: "runBash", description: "Execute a bash command and return stdout/stderr", parameters: { type: "object", properties: { command: { type: "string", description: "Bash command to execute" }, cwd: { type: "string", description: "Working directory for the command" }, timeout: { type: "number", description: "Timeout in milliseconds (default 30000)" }, }, required: ["command"], }, }, { name: "glob", description: "Find files matching a glob pattern recursively", parameters: { type: "object", properties: { pattern: { type: "string", description: "Glob pattern (e.g. **/*.ts)" }, cwd: { type: "string", description: "Directory to search in" }, }, required: ["pattern"], }, }, { name: "grep", description: "Search file contents using a regular expression", parameters: { type: "object", properties: { pattern: { type: "string", description: "Regex pattern to search for" }, include: { type: "string", description: "File pattern to include (e.g. *.ts)" }, cwd: { type: "string", description: "Directory to search in" }, }, required: ["pattern"], }, }, ]; export class ToolRegistry { private projectPath: string; private maxFileSize: number; constructor(projectPath: string, maxFileSize: number = 1024 * 1024) { this.projectPath = projectPath; this.maxFileSize = maxFileSize; } execute(call: ToolCall): ToolResult { try { switch (call.name) { case "readFile": return this.readFile(call.arguments); case "writeFile": return this.writeFile(call.arguments); case "editFile": return this.editFile(call.arguments); case "runBash": return this.runBash(call.arguments); case "glob": return this.glob(call.arguments); case "grep": return this.grep(call.arguments); default: return { name: call.name, content: `Unknown tool: ${call.name}`, isError: true }; } } catch (err) { return { name: call.name, content: `Tool error: ${err instanceof Error ? err.message : String(err)}`, isError: true, }; } } getDefinitions(): ToolDefinition[] { return TOOL_DEFINITIONS; } getOpenAIToolSchema(): Array> { return TOOL_DEFINITIONS.map((def) => ({ type: "function", function: { name: def.name, description: def.description, parameters: def.parameters, }, })); } private readFile(args: Record): ToolResult { const filePath = String(args.path); if (!fs.existsSync(filePath)) { return { name: "readFile", content: `File not found: ${filePath}`, isError: true }; } try { const stat = fs.statSync(filePath); if (stat.size > this.maxFileSize) { return { name: "readFile", content: `File too large: ${filePath} (${stat.size} bytes)`, isError: true }; } const content = fs.readFileSync(filePath, "utf-8"); return { name: "readFile", content }; } catch (err) { return { name: "readFile", content: `Read error: ${err instanceof Error ? err.message : String(err)}`, isError: true }; } } private writeFile(args: Record): ToolResult { const filePath = String(args.path); const content = String(args.content); try { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, content, "utf-8"); return { name: "writeFile", content: `Written: ${filePath}` }; } catch (err) { return { name: "writeFile", content: `Write error: ${err instanceof Error ? err.message : String(err)}`, isError: true }; } } private editFile(args: Record): ToolResult { const filePath = String(args.path); const oldStr = String(args.old); const newStr = String(args.new); if (!fs.existsSync(filePath)) { return { name: "editFile", content: `File not found: ${filePath}`, isError: true }; } try { const content = fs.readFileSync(filePath, "utf-8"); if (!content.includes(oldStr)) { return { name: "editFile", content: `String not found in ${filePath}`, isError: true }; } const updated = content.replace(oldStr, newStr); fs.writeFileSync(filePath, updated, "utf-8"); return { name: "editFile", content: `Edited: ${filePath}` }; } catch (err) { return { name: "editFile", content: `Edit error: ${err instanceof Error ? err.message : String(err)}`, isError: true }; } } private runBash(args: Record): ToolResult { const command = String(args.command); const cwd = args.cwd ? String(args.cwd) : this.projectPath; const timeout = args.timeout ? Number(args.timeout) : 30000; try { const stdout = execSync(command, { cwd, timeout, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 1024 * 1024, }); return { name: "runBash", content: stdout || "(no output)" }; } catch (err: unknown) { const execErr = err as { stderr?: string; stdout?: string; status?: number }; const output = [`Exit code: ${execErr.status || 1}`, `stdout: ${execErr.stdout || ""}`, `stderr: ${execErr.stderr || ""}`].join("\n"); return { name: "runBash", content: output, isError: true }; } } private glob(args: Record): ToolResult { const pattern = String(args.pattern); const cwd = args.cwd ? String(args.cwd) : this.projectPath; const matches = this.globRecursive(cwd, pattern); return { name: "glob", content: JSON.stringify(matches.slice(0, 200)) }; } private grep(args: Record): ToolResult { const pattern = String(args.pattern); const cwd = args.cwd ? String(args.cwd) : this.projectPath; const include = args.include ? String(args.include) : undefined; const matches = this.grepRecursive(cwd, pattern, include); return { name: "grep", content: JSON.stringify(matches.slice(0, 100)) }; } private globRecursive(dir: string, pattern: string): string[] { const results: string[] = []; const regex = this.globToRegex(pattern); try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === ".git") continue; const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { results.push(...this.globRecursive(fullPath, pattern)); } else if (regex.test(entry.name) || regex.test(path.relative(this.projectPath, fullPath))) { results.push(path.relative(this.projectPath, fullPath)); } } } catch {} return results.sort(); } private globToRegex(pattern: string): RegExp { const escaped = pattern .replace(/[.+^${}()|[\]\\]/g, "\\$&") .replace(/\*\*/g, "{{GLOBSTAR}}") .replace(/\*/g, "[^/]*") .replace(/{{GLOBSTAR}}/g, ".*") .replace(/\?/g, "[^/]"); return new RegExp(`^${escaped}$`); } private grepRecursive(dir: string, patternStr: string, include?: string): Array<{ file: string; line: number; content: string }> { const results: Array<{ file: string; line: number; content: string }> = []; const regex = new RegExp(patternStr); const includeRegex = include ? this.globToRegex(include) : null; try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === ".git") continue; const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { results.push(...this.grepRecursive(fullPath, patternStr, include)); } else if (includeRegex ? includeRegex.test(entry.name) : true) { try { const content = fs.readFileSync(fullPath, "utf-8"); const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { if (regex.test(lines[i])) { results.push({ file: path.relative(this.projectPath, fullPath), line: i + 1, content: lines[i].trim(), }); } } } catch {} } } } catch {} return results; } }