|
1 | 1 | import * as fs from "node:fs"; |
| 2 | +import * as os from "node:os"; |
2 | 3 | import * as path from "node:path"; |
3 | 4 | import { exec } from "node:child_process"; |
4 | 5 | import { promisify } from "node:util"; |
| 6 | +import { randomUUID } from "node:crypto"; |
5 | 7 | import type { ToolDefinition } from "./providers/types"; |
6 | 8 |
|
7 | 9 | const execAsync = promisify(exec); |
@@ -66,6 +68,59 @@ export const AGENT_TOOLS: ToolDefinition[] = [ |
66 | 68 | required: ["command"], |
67 | 69 | }, |
68 | 70 | }, |
| 71 | + { |
| 72 | + name: "run_code", |
| 73 | + description: |
| 74 | + "Run a Python or JavaScript code snippet in the workspace and return its stdout/stderr/exit code. A convenience over run_command for multi-line code (no shell-quoting to worry about) — it is not a sandbox: the code runs with the same permissions as run_command and is subject to the same safety checks.", |
| 75 | + parameters: { |
| 76 | + type: "object", |
| 77 | + properties: { |
| 78 | + language: { type: "string", enum: ["python", "javascript"], description: "Which interpreter to run the code with." }, |
| 79 | + code: { type: "string", description: "The full source code to execute." }, |
| 80 | + cwd: { type: "string", description: 'Working directory, relative to the workspace root. Defaults to "."' }, |
| 81 | + }, |
| 82 | + required: ["language", "code"], |
| 83 | + }, |
| 84 | + }, |
| 85 | + { |
| 86 | + name: "git_status", |
| 87 | + description: "Show the working tree status (git status) for the workspace.", |
| 88 | + parameters: { type: "object", properties: {}, required: [] }, |
| 89 | + }, |
| 90 | + { |
| 91 | + name: "git_diff", |
| 92 | + description: "Show unstaged (or, if staged=true, staged) changes in the workspace as a unified diff.", |
| 93 | + parameters: { |
| 94 | + type: "object", |
| 95 | + properties: { |
| 96 | + staged: { type: "boolean", description: "Show staged changes (git diff --staged) instead of unstaged." }, |
| 97 | + path: { type: "string", description: "Limit the diff to this file or directory, relative to the workspace root." }, |
| 98 | + }, |
| 99 | + required: [], |
| 100 | + }, |
| 101 | + }, |
| 102 | + { |
| 103 | + name: "git_log", |
| 104 | + description: "Show recent commit history for the workspace.", |
| 105 | + parameters: { |
| 106 | + type: "object", |
| 107 | + properties: { |
| 108 | + count: { type: "number", description: "How many commits to show. Defaults to 10." }, |
| 109 | + }, |
| 110 | + required: [], |
| 111 | + }, |
| 112 | + }, |
| 113 | + { |
| 114 | + name: "git_commit", |
| 115 | + description: "Stage all changes and create a commit in the workspace. Requires explicit approval, like write_file.", |
| 116 | + parameters: { |
| 117 | + type: "object", |
| 118 | + properties: { |
| 119 | + message: { type: "string", description: "The commit message." }, |
| 120 | + }, |
| 121 | + required: ["message"], |
| 122 | + }, |
| 123 | + }, |
69 | 124 | ]; |
70 | 125 |
|
71 | 126 | const MAX_READ_CHARS = 100_000; |
@@ -284,6 +339,54 @@ export async function runCommand(workspaceRoot: string, command: string, relativ |
284 | 339 | } |
285 | 340 | } |
286 | 341 |
|
| 342 | +// run_code is a thin convenience wrapper over run_command for multi-line |
| 343 | +// snippets (avoids shell-quoting hell for real code) — it carries the exact |
| 344 | +// same risk and is checked against the exact same blocklist as run_command, |
| 345 | +// applied to the source text too since dangerous *content* (not just the |
| 346 | +// invocation) could otherwise slip past a check that only looks at the |
| 347 | +// command line. |
| 348 | +export async function runCode( |
| 349 | + workspaceRoot: string, |
| 350 | + language: "python" | "javascript", |
| 351 | + code: string, |
| 352 | + relativeCwd = "." |
| 353 | +): Promise<string> { |
| 354 | + const dangerReason = findDangerousCommandReason(code); |
| 355 | + if (dangerReason) throw new Error(dangerReason); |
| 356 | + |
| 357 | + const ext = language === "python" ? "py" : "js"; |
| 358 | + const tmpFile = path.join(os.tmpdir(), `modelforge-code-${randomUUID()}.${ext}`); |
| 359 | + fs.writeFileSync(tmpFile, code); |
| 360 | + try { |
| 361 | + const interpreter = language === "python" ? "python3" : "node"; |
| 362 | + return await runCommand(workspaceRoot, `${interpreter} "${tmpFile}"`, relativeCwd); |
| 363 | + } finally { |
| 364 | + fs.rmSync(tmpFile, { force: true }); |
| 365 | + } |
| 366 | +} |
| 367 | + |
| 368 | +function gitCommand(workspaceRoot: string, args: string): Promise<string> { |
| 369 | + return runCommand(workspaceRoot, `git ${args}`, "."); |
| 370 | +} |
| 371 | + |
| 372 | +export function gitStatus(workspaceRoot: string): Promise<string> { |
| 373 | + return gitCommand(workspaceRoot, "status"); |
| 374 | +} |
| 375 | + |
| 376 | +export function gitDiff(workspaceRoot: string, staged = false, relativePath?: string): Promise<string> { |
| 377 | + const target = relativePath ? ` -- "${relativePath}"` : ""; |
| 378 | + return gitCommand(workspaceRoot, `diff${staged ? " --staged" : ""}${target}`); |
| 379 | +} |
| 380 | + |
| 381 | +export function gitLog(workspaceRoot: string, count = 10): Promise<string> { |
| 382 | + return gitCommand(workspaceRoot, `log -n ${Math.max(1, Math.min(count, 100))} --oneline`); |
| 383 | +} |
| 384 | + |
| 385 | +export async function gitCommit(workspaceRoot: string, message: string): Promise<string> { |
| 386 | + await gitCommand(workspaceRoot, "add -A"); |
| 387 | + return gitCommand(workspaceRoot, `commit -m ${JSON.stringify(message)}`); |
| 388 | +} |
| 389 | + |
287 | 390 | export interface ProjectScripts { |
288 | 391 | test?: string; |
289 | 392 | lint?: string; |
@@ -321,6 +424,18 @@ export async function executeTool(workspaceRoot: string, name: string, args: Rec |
321 | 424 | return searchFiles(workspaceRoot, String(args.query ?? ""), args.path ? String(args.path) : "."); |
322 | 425 | case "run_command": |
323 | 426 | return runCommand(workspaceRoot, String(args.command ?? ""), args.cwd ? String(args.cwd) : "."); |
| 427 | + case "run_code": { |
| 428 | + const language = args.language === "python" ? "python" : "javascript"; |
| 429 | + return runCode(workspaceRoot, language, String(args.code ?? ""), args.cwd ? String(args.cwd) : "."); |
| 430 | + } |
| 431 | + case "git_status": |
| 432 | + return gitStatus(workspaceRoot); |
| 433 | + case "git_diff": |
| 434 | + return gitDiff(workspaceRoot, args.staged === true, args.path ? String(args.path) : undefined); |
| 435 | + case "git_log": |
| 436 | + return gitLog(workspaceRoot, typeof args.count === "number" ? args.count : 10); |
| 437 | + case "git_commit": |
| 438 | + return gitCommit(workspaceRoot, String(args.message ?? "")); |
324 | 439 | default: |
325 | 440 | throw new Error(`Unknown tool: ${name}`); |
326 | 441 | } |
|
0 commit comments