@cdoing/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/coordinator.d.ts +114 -0
- package/dist/agents/coordinator.d.ts.map +1 -0
- package/dist/agents/coordinator.js +158 -0
- package/dist/agents/coordinator.js.map +1 -0
- package/dist/context-providers/clipboard.d.ts +13 -0
- package/dist/context-providers/clipboard.d.ts.map +1 -0
- package/dist/context-providers/clipboard.js +53 -0
- package/dist/context-providers/clipboard.js.map +1 -0
- package/dist/context-providers/codebase.d.ts +46 -0
- package/dist/context-providers/codebase.d.ts.map +1 -0
- package/dist/context-providers/codebase.js +273 -0
- package/dist/context-providers/codebase.js.map +1 -0
- package/dist/context-providers/diff.d.ts +18 -0
- package/dist/context-providers/diff.d.ts.map +1 -0
- package/dist/context-providers/diff.js +63 -0
- package/dist/context-providers/diff.js.map +1 -0
- package/dist/context-providers/docs.d.ts +21 -0
- package/dist/context-providers/docs.d.ts.map +1 -0
- package/dist/context-providers/docs.js +180 -0
- package/dist/context-providers/docs.js.map +1 -0
- package/dist/context-providers/file-include.d.ts +13 -0
- package/dist/context-providers/file-include.d.ts.map +1 -0
- package/dist/context-providers/file-include.js +82 -0
- package/dist/context-providers/file-include.js.map +1 -0
- package/dist/context-providers/folder.d.ts +19 -0
- package/dist/context-providers/folder.d.ts.map +1 -0
- package/dist/context-providers/folder.js +130 -0
- package/dist/context-providers/folder.js.map +1 -0
- package/dist/context-providers/git.d.ts +19 -0
- package/dist/context-providers/git.d.ts.map +1 -0
- package/dist/context-providers/git.js +74 -0
- package/dist/context-providers/git.js.map +1 -0
- package/dist/context-providers/index.d.ts +26 -0
- package/dist/context-providers/index.d.ts.map +1 -0
- package/dist/context-providers/index.js +37 -0
- package/dist/context-providers/index.js.map +1 -0
- package/dist/context-providers/open-files.d.ts +25 -0
- package/dist/context-providers/open-files.d.ts.map +1 -0
- package/dist/context-providers/open-files.js +134 -0
- package/dist/context-providers/open-files.js.map +1 -0
- package/dist/context-providers/problems.d.ts +24 -0
- package/dist/context-providers/problems.d.ts.map +1 -0
- package/dist/context-providers/problems.js +97 -0
- package/dist/context-providers/problems.js.map +1 -0
- package/dist/context-providers/registry.d.ts +61 -0
- package/dist/context-providers/registry.d.ts.map +1 -0
- package/dist/context-providers/registry.js +92 -0
- package/dist/context-providers/registry.js.map +1 -0
- package/dist/context-providers/terminal.d.ts +25 -0
- package/dist/context-providers/terminal.d.ts.map +1 -0
- package/dist/context-providers/terminal.js +55 -0
- package/dist/context-providers/terminal.js.map +1 -0
- package/dist/context-providers/tree.d.ts +29 -0
- package/dist/context-providers/tree.d.ts.map +1 -0
- package/dist/context-providers/tree.js +172 -0
- package/dist/context-providers/tree.js.map +1 -0
- package/dist/context-providers/types.d.ts +72 -0
- package/dist/context-providers/types.d.ts.map +1 -0
- package/dist/context-providers/types.js +10 -0
- package/dist/context-providers/types.js.map +1 -0
- package/dist/context-providers/url.d.ts +27 -0
- package/dist/context-providers/url.d.ts.map +1 -0
- package/dist/context-providers/url.js +131 -0
- package/dist/context-providers/url.js.map +1 -0
- package/dist/effort/index.d.ts +78 -0
- package/dist/effort/index.d.ts.map +1 -0
- package/dist/effort/index.js +146 -0
- package/dist/effort/index.js.map +1 -0
- package/dist/hooks/index.d.ts +47 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +151 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +152 -0
- package/dist/index.js.map +1 -0
- package/dist/indexing/chunker.d.ts +25 -0
- package/dist/indexing/chunker.d.ts.map +1 -0
- package/dist/indexing/chunker.js +217 -0
- package/dist/indexing/chunker.js.map +1 -0
- package/dist/indexing/database.d.ts +49 -0
- package/dist/indexing/database.d.ts.map +1 -0
- package/dist/indexing/database.js +287 -0
- package/dist/indexing/database.js.map +1 -0
- package/dist/indexing/index.d.ts +9 -0
- package/dist/indexing/index.d.ts.map +1 -0
- package/dist/indexing/index.js +13 -0
- package/dist/indexing/index.js.map +1 -0
- package/dist/indexing/indexer.d.ts +63 -0
- package/dist/indexing/indexer.d.ts.map +1 -0
- package/dist/indexing/indexer.js +352 -0
- package/dist/indexing/indexer.js.map +1 -0
- package/dist/indexing/recent-edits-cache.d.ts +77 -0
- package/dist/indexing/recent-edits-cache.d.ts.map +1 -0
- package/dist/indexing/recent-edits-cache.js +123 -0
- package/dist/indexing/recent-edits-cache.js.map +1 -0
- package/dist/indexing/types.d.ts +39 -0
- package/dist/indexing/types.d.ts.map +1 -0
- package/dist/indexing/types.js +6 -0
- package/dist/indexing/types.js.map +1 -0
- package/dist/mcp/index.d.ts +33 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +37 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/manager.d.ts +123 -0
- package/dist/mcp/manager.d.ts.map +1 -0
- package/dist/mcp/manager.js +331 -0
- package/dist/mcp/manager.js.map +1 -0
- package/dist/oauth.d.ts +33 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +312 -0
- package/dist/oauth.js.map +1 -0
- package/dist/permissions/index.d.ts +216 -0
- package/dist/permissions/index.d.ts.map +1 -0
- package/dist/permissions/index.js +938 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/plan/index.d.ts +20 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan/index.js +24 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/plan/manager.d.ts +101 -0
- package/dist/plan/manager.d.ts.map +1 -0
- package/dist/plan/manager.js +170 -0
- package/dist/plan/manager.js.map +1 -0
- package/dist/rules/index.d.ts +28 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +31 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/manager.d.ts +77 -0
- package/dist/rules/manager.d.ts.map +1 -0
- package/dist/rules/manager.js +279 -0
- package/dist/rules/manager.js.map +1 -0
- package/dist/rules/types.d.ts +34 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +9 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/sandbox/filesystem.d.ts +20 -0
- package/dist/sandbox/filesystem.d.ts.map +1 -0
- package/dist/sandbox/filesystem.js +141 -0
- package/dist/sandbox/filesystem.js.map +1 -0
- package/dist/sandbox/index.d.ts +4 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +8 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/manager.d.ts +47 -0
- package/dist/sandbox/manager.d.ts.map +1 -0
- package/dist/sandbox/manager.js +220 -0
- package/dist/sandbox/manager.js.map +1 -0
- package/dist/sandbox/network.d.ts +14 -0
- package/dist/sandbox/network.d.ts.map +1 -0
- package/dist/sandbox/network.js +87 -0
- package/dist/sandbox/network.js.map +1 -0
- package/dist/sandbox/types.d.ts +42 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +25 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/tools/ast-edit.d.ts +57 -0
- package/dist/tools/ast-edit.d.ts.map +1 -0
- package/dist/tools/ast-edit.js +443 -0
- package/dist/tools/ast-edit.js.map +1 -0
- package/dist/tools/code-verify.d.ts +8 -0
- package/dist/tools/code-verify.d.ts.map +1 -0
- package/dist/tools/code-verify.js +159 -0
- package/dist/tools/code-verify.js.map +1 -0
- package/dist/tools/codebase-search.d.ts +17 -0
- package/dist/tools/codebase-search.d.ts.map +1 -0
- package/dist/tools/codebase-search.js +104 -0
- package/dist/tools/codebase-search.js.map +1 -0
- package/dist/tools/file-delete.d.ts +26 -0
- package/dist/tools/file-delete.d.ts.map +1 -0
- package/dist/tools/file-delete.js +179 -0
- package/dist/tools/file-delete.js.map +1 -0
- package/dist/tools/file-edit.d.ts +10 -0
- package/dist/tools/file-edit.d.ts.map +1 -0
- package/dist/tools/file-edit.js +138 -0
- package/dist/tools/file-edit.js.map +1 -0
- package/dist/tools/file-read.d.ts +12 -0
- package/dist/tools/file-read.d.ts.map +1 -0
- package/dist/tools/file-read.js +211 -0
- package/dist/tools/file-read.js.map +1 -0
- package/dist/tools/file-run.d.ts +10 -0
- package/dist/tools/file-run.d.ts.map +1 -0
- package/dist/tools/file-run.js +179 -0
- package/dist/tools/file-run.js.map +1 -0
- package/dist/tools/file-write.d.ts +10 -0
- package/dist/tools/file-write.d.ts.map +1 -0
- package/dist/tools/file-write.js +134 -0
- package/dist/tools/file-write.js.map +1 -0
- package/dist/tools/glob-search.d.ts +8 -0
- package/dist/tools/glob-search.d.ts.map +1 -0
- package/dist/tools/glob-search.js +108 -0
- package/dist/tools/glob-search.js.map +1 -0
- package/dist/tools/grep-search.d.ts +8 -0
- package/dist/tools/grep-search.d.ts.map +1 -0
- package/dist/tools/grep-search.js +139 -0
- package/dist/tools/grep-search.js.map +1 -0
- package/dist/tools/list-dir.d.ts +16 -0
- package/dist/tools/list-dir.d.ts.map +1 -0
- package/dist/tools/list-dir.js +183 -0
- package/dist/tools/list-dir.js.map +1 -0
- package/dist/tools/multi-edit.d.ts +16 -0
- package/dist/tools/multi-edit.d.ts.map +1 -0
- package/dist/tools/multi-edit.js +163 -0
- package/dist/tools/multi-edit.js.map +1 -0
- package/dist/tools/notebook-edit.d.ts +31 -0
- package/dist/tools/notebook-edit.d.ts.map +1 -0
- package/dist/tools/notebook-edit.js +321 -0
- package/dist/tools/notebook-edit.js.map +1 -0
- package/dist/tools/registry.d.ts +16 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +41 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/shell-exec.d.ts +12 -0
- package/dist/tools/shell-exec.d.ts.map +1 -0
- package/dist/tools/shell-exec.js +261 -0
- package/dist/tools/shell-exec.js.map +1 -0
- package/dist/tools/sub-agent-manager.d.ts +57 -0
- package/dist/tools/sub-agent-manager.d.ts.map +1 -0
- package/dist/tools/sub-agent-manager.js +153 -0
- package/dist/tools/sub-agent-manager.js.map +1 -0
- package/dist/tools/sub-agent-status.d.ts +12 -0
- package/dist/tools/sub-agent-status.d.ts.map +1 -0
- package/dist/tools/sub-agent-status.js +59 -0
- package/dist/tools/sub-agent-status.js.map +1 -0
- package/dist/tools/sub-agent-terminate.d.ts +12 -0
- package/dist/tools/sub-agent-terminate.d.ts.map +1 -0
- package/dist/tools/sub-agent-terminate.js +55 -0
- package/dist/tools/sub-agent-terminate.js.map +1 -0
- package/dist/tools/sub-agent.d.ts +34 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +140 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/system-info.d.ts +24 -0
- package/dist/tools/system-info.d.ts.map +1 -0
- package/dist/tools/system-info.js +220 -0
- package/dist/tools/system-info.js.map +1 -0
- package/dist/tools/todo.d.ts +16 -0
- package/dist/tools/todo.d.ts.map +1 -0
- package/dist/tools/todo.js +144 -0
- package/dist/tools/todo.js.map +1 -0
- package/dist/tools/types.d.ts +20 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +3 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/view-diff.d.ts +11 -0
- package/dist/tools/view-diff.d.ts.map +1 -0
- package/dist/tools/view-diff.js +88 -0
- package/dist/tools/view-diff.js.map +1 -0
- package/dist/tools/view-repo-map.d.ts +18 -0
- package/dist/tools/view-repo-map.d.ts.map +1 -0
- package/dist/tools/view-repo-map.js +245 -0
- package/dist/tools/view-repo-map.js.map +1 -0
- package/dist/tools/web-fetch.d.ts +13 -0
- package/dist/tools/web-fetch.d.ts.map +1 -0
- package/dist/tools/web-fetch.js +106 -0
- package/dist/tools/web-fetch.js.map +1 -0
- package/dist/tools/web-search.d.ts +10 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +106 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/utils/gitignore.d.ts +10 -0
- package/dist/utils/gitignore.d.ts.map +1 -0
- package/dist/utils/gitignore.js +104 -0
- package/dist/utils/gitignore.js.map +1 -0
- package/dist/utils/lazy-apply.d.ts +45 -0
- package/dist/utils/lazy-apply.d.ts.map +1 -0
- package/dist/utils/lazy-apply.js +164 -0
- package/dist/utils/lazy-apply.js.map +1 -0
- package/dist/utils/memory.d.ts +36 -0
- package/dist/utils/memory.d.ts.map +1 -0
- package/dist/utils/memory.js +136 -0
- package/dist/utils/memory.js.map +1 -0
- package/dist/utils/path-matching.d.ts +24 -0
- package/dist/utils/path-matching.d.ts.map +1 -0
- package/dist/utils/path-matching.js +116 -0
- package/dist/utils/path-matching.js.map +1 -0
- package/dist/utils/path-safety.d.ts +13 -0
- package/dist/utils/path-safety.d.ts.map +1 -0
- package/dist/utils/path-safety.js +54 -0
- package/dist/utils/path-safety.js.map +1 -0
- package/dist/utils/project-config.d.ts +18 -0
- package/dist/utils/project-config.d.ts.map +1 -0
- package/dist/utils/project-config.js +76 -0
- package/dist/utils/project-config.js.map +1 -0
- package/dist/utils/search-match.d.ts +63 -0
- package/dist/utils/search-match.d.ts.map +1 -0
- package/dist/utils/search-match.js +426 -0
- package/dist/utils/search-match.js.map +1 -0
- package/dist/utils/shell-paths.d.ts +17 -0
- package/dist/utils/shell-paths.d.ts.map +1 -0
- package/dist/utils/shell-paths.js +107 -0
- package/dist/utils/shell-paths.js.map +1 -0
- package/dist/utils/streaming-diff.d.ts +45 -0
- package/dist/utils/streaming-diff.d.ts.map +1 -0
- package/dist/utils/streaming-diff.js +230 -0
- package/dist/utils/streaming-diff.js.map +1 -0
- package/dist/utils/todo.d.ts +47 -0
- package/dist/utils/todo.d.ts.map +1 -0
- package/dist/utils/todo.js +102 -0
- package/dist/utils/todo.js.map +1 -0
- package/package.json +23 -0
- package/src/agents/coordinator.ts +240 -0
- package/src/context-providers/clipboard.ts +48 -0
- package/src/context-providers/codebase.ts +274 -0
- package/src/context-providers/diff.ts +66 -0
- package/src/context-providers/docs.ts +160 -0
- package/src/context-providers/file-include.ts +54 -0
- package/src/context-providers/folder.ts +106 -0
- package/src/context-providers/git.ts +72 -0
- package/src/context-providers/index.ts +26 -0
- package/src/context-providers/open-files.ts +113 -0
- package/src/context-providers/problems.ts +100 -0
- package/src/context-providers/registry.ts +99 -0
- package/src/context-providers/terminal.ts +58 -0
- package/src/context-providers/tree.ts +161 -0
- package/src/context-providers/types.ts +84 -0
- package/src/context-providers/url.ts +138 -0
- package/src/effort/index.ts +177 -0
- package/src/hooks/index.ts +148 -0
- package/src/index.ts +114 -0
- package/src/indexing/README.md +267 -0
- package/src/indexing/chunker.ts +206 -0
- package/src/indexing/database.ts +299 -0
- package/src/indexing/index.ts +15 -0
- package/src/indexing/indexer.ts +383 -0
- package/src/indexing/recent-edits-cache.ts +150 -0
- package/src/indexing/types.ts +44 -0
- package/src/mcp/index.ts +33 -0
- package/src/mcp/manager.ts +385 -0
- package/src/oauth.ts +330 -0
- package/src/permissions/index.ts +1011 -0
- package/src/plan/index.ts +20 -0
- package/src/plan/manager.ts +233 -0
- package/src/rules/index.ts +28 -0
- package/src/rules/manager.ts +276 -0
- package/src/rules/types.ts +40 -0
- package/src/sandbox/filesystem.ts +135 -0
- package/src/sandbox/index.ts +9 -0
- package/src/sandbox/manager.ts +213 -0
- package/src/sandbox/network.ts +101 -0
- package/src/sandbox/types.ts +63 -0
- package/src/tools/ast-edit.ts +493 -0
- package/src/tools/code-verify.ts +143 -0
- package/src/tools/codebase-search.ts +117 -0
- package/src/tools/file-delete.ts +155 -0
- package/src/tools/file-edit.ts +115 -0
- package/src/tools/file-read.ts +195 -0
- package/src/tools/file-run.ts +158 -0
- package/src/tools/file-write.ts +104 -0
- package/src/tools/glob-search.ts +80 -0
- package/src/tools/grep-search.ts +120 -0
- package/src/tools/list-dir.ts +172 -0
- package/src/tools/multi-edit.ts +138 -0
- package/src/tools/notebook-edit.ts +342 -0
- package/src/tools/registry.ts +43 -0
- package/src/tools/shell-exec.ts +251 -0
- package/src/tools/sub-agent-manager.ts +183 -0
- package/src/tools/sub-agent-status.ts +67 -0
- package/src/tools/sub-agent-terminate.ts +62 -0
- package/src/tools/sub-agent.ts +162 -0
- package/src/tools/system-info.ts +248 -0
- package/src/tools/todo.ts +149 -0
- package/src/tools/types.ts +21 -0
- package/src/tools/view-diff.ts +99 -0
- package/src/tools/view-repo-map.ts +249 -0
- package/src/tools/web-fetch.ts +118 -0
- package/src/tools/web-search.ts +129 -0
- package/src/utils/gitignore.ts +73 -0
- package/src/utils/lazy-apply.ts +189 -0
- package/src/utils/memory.ts +124 -0
- package/src/utils/path-matching.ts +84 -0
- package/src/utils/path-safety.ts +19 -0
- package/src/utils/project-config.ts +41 -0
- package/src/utils/search-match.ts +495 -0
- package/src/utils/shell-paths.ts +79 -0
- package/src/utils/streaming-diff.ts +260 -0
- package/src/utils/todo.ts +115 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST Edit Tool — Tree-sitter powered structural code editing.
|
|
3
|
+
*
|
|
4
|
+
* Uses tree-sitter to parse source code into an AST, then performs
|
|
5
|
+
* targeted edits on specific AST nodes (functions, classes, methods,
|
|
6
|
+
* imports, etc.) by name rather than by string matching.
|
|
7
|
+
*
|
|
8
|
+
* This is more reliable than string-based editing for:
|
|
9
|
+
* - Renaming functions/methods/classes across a file
|
|
10
|
+
* - Replacing entire function bodies
|
|
11
|
+
* - Adding/removing imports
|
|
12
|
+
* - Inserting methods into classes
|
|
13
|
+
* - Extracting or inlining code blocks
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from "fs";
|
|
17
|
+
import type { BaseTool, ToolDefinition, ToolResult } from "./types";
|
|
18
|
+
import { safePath } from "../utils/path-safety";
|
|
19
|
+
import type { SandboxManager } from "../sandbox";
|
|
20
|
+
|
|
21
|
+
// Tree-sitter types (loaded dynamically to keep it optional)
|
|
22
|
+
let Parser: any = null;
|
|
23
|
+
const languageCache = new Map<string, any>();
|
|
24
|
+
|
|
25
|
+
/** Supported languages and their tree-sitter package names */
|
|
26
|
+
const LANGUAGE_MAP: Record<string, string> = {
|
|
27
|
+
".ts": "tree-sitter-typescript/typescript",
|
|
28
|
+
".tsx": "tree-sitter-typescript/tsx",
|
|
29
|
+
".js": "tree-sitter-javascript",
|
|
30
|
+
".jsx": "tree-sitter-javascript",
|
|
31
|
+
".py": "tree-sitter-python",
|
|
32
|
+
".rs": "tree-sitter-rust",
|
|
33
|
+
".go": "tree-sitter-go",
|
|
34
|
+
".java": "tree-sitter-java",
|
|
35
|
+
".c": "tree-sitter-c",
|
|
36
|
+
".cpp": "tree-sitter-cpp",
|
|
37
|
+
".rb": "tree-sitter-ruby",
|
|
38
|
+
".css": "tree-sitter-css",
|
|
39
|
+
".json": "tree-sitter-json",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** AST node types we can target for editing */
|
|
43
|
+
type ASTNodeType =
|
|
44
|
+
| "function"
|
|
45
|
+
| "class"
|
|
46
|
+
| "method"
|
|
47
|
+
| "import"
|
|
48
|
+
| "variable"
|
|
49
|
+
| "type"
|
|
50
|
+
| "interface"
|
|
51
|
+
| "enum"
|
|
52
|
+
| "block"
|
|
53
|
+
| "expression";
|
|
54
|
+
|
|
55
|
+
interface ASTEditOperation {
|
|
56
|
+
/** Type of AST node to target */
|
|
57
|
+
node_type: ASTNodeType;
|
|
58
|
+
/** Name of the node (function name, class name, etc.) */
|
|
59
|
+
name: string;
|
|
60
|
+
/** Action to perform */
|
|
61
|
+
action: "replace" | "rename" | "delete" | "insert_before" | "insert_after" | "replace_body";
|
|
62
|
+
/** New content (for replace, insert_before, insert_after, replace_body) */
|
|
63
|
+
content?: string;
|
|
64
|
+
/** New name (for rename) */
|
|
65
|
+
new_name?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class ASTEditTool implements BaseTool {
|
|
69
|
+
definition: ToolDefinition = {
|
|
70
|
+
name: "ast_edit",
|
|
71
|
+
description:
|
|
72
|
+
`Perform structural edits on source code using Tree-sitter AST parsing. More reliable than string matching for targeting specific code structures.
|
|
73
|
+
|
|
74
|
+
Supported operations:
|
|
75
|
+
- replace: Replace an entire AST node (function, class, etc.) with new content
|
|
76
|
+
- rename: Rename a function, class, method, variable, or type
|
|
77
|
+
- delete: Remove an AST node entirely
|
|
78
|
+
- insert_before/insert_after: Insert content before or after a named node
|
|
79
|
+
- replace_body: Replace only the body of a function/method/class (keeps signature)
|
|
80
|
+
|
|
81
|
+
Supported languages: TypeScript, JavaScript, Python, Rust, Go, Java, C, C++, Ruby, CSS, JSON.
|
|
82
|
+
|
|
83
|
+
Use this when string-matching edits are fragile or when you need to target code by structure rather than text.`,
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
file_path: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Path to the file to edit",
|
|
90
|
+
},
|
|
91
|
+
operations: {
|
|
92
|
+
type: "array",
|
|
93
|
+
description: "Array of AST edit operations to apply sequentially",
|
|
94
|
+
items: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
node_type: {
|
|
98
|
+
type: "string",
|
|
99
|
+
enum: ["function", "class", "method", "import", "variable", "type", "interface", "enum", "block", "expression"],
|
|
100
|
+
description: "Type of AST node to target",
|
|
101
|
+
},
|
|
102
|
+
name: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "Name of the node (function name, class name, variable name, import module name, etc.)",
|
|
105
|
+
},
|
|
106
|
+
action: {
|
|
107
|
+
type: "string",
|
|
108
|
+
enum: ["replace", "rename", "delete", "insert_before", "insert_after", "replace_body"],
|
|
109
|
+
description: "Action to perform on the node",
|
|
110
|
+
},
|
|
111
|
+
content: {
|
|
112
|
+
type: "string",
|
|
113
|
+
description: "New content (for replace, insert_before, insert_after, replace_body actions)",
|
|
114
|
+
},
|
|
115
|
+
new_name: {
|
|
116
|
+
type: "string",
|
|
117
|
+
description: "New name (for rename action)",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
required: ["node_type", "name", "action"],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
required: ["file_path", "operations"],
|
|
125
|
+
},
|
|
126
|
+
requiresPermission: true,
|
|
127
|
+
permissionMessage: (input) => {
|
|
128
|
+
const ops = input.operations as unknown[];
|
|
129
|
+
return `AST edit: ${input.file_path} (${Array.isArray(ops) ? ops.length : "?"} operation(s))`;
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
private workingDir: string;
|
|
134
|
+
private sandboxManager?: SandboxManager;
|
|
135
|
+
|
|
136
|
+
constructor(workingDir: string, sandboxManager?: SandboxManager) {
|
|
137
|
+
this.workingDir = workingDir;
|
|
138
|
+
this.sandboxManager = sandboxManager;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async execute(input: Record<string, unknown>): Promise<ToolResult> {
|
|
142
|
+
let filePath: string;
|
|
143
|
+
try {
|
|
144
|
+
filePath = safePath(input.file_path as string, this.workingDir);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return { success: false, output: "", error: (err as Error).message };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (this.sandboxManager) {
|
|
150
|
+
const check = this.sandboxManager.checkFileWrite(filePath);
|
|
151
|
+
if (!check.allowed) {
|
|
152
|
+
return { success: false, output: "", error: check.reason || "Sandbox: write access denied" };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!fs.existsSync(filePath)) {
|
|
157
|
+
return { success: false, output: "", error: `File not found: ${filePath}` };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const operations = input.operations as ASTEditOperation[];
|
|
161
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
162
|
+
return { success: false, output: "", error: "operations must be a non-empty array" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Initialize tree-sitter
|
|
166
|
+
const initResult = await this.initParser(filePath);
|
|
167
|
+
if (!initResult.success) {
|
|
168
|
+
return initResult;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let content = fs.readFileSync(filePath, "utf-8");
|
|
172
|
+
const originalContent = content;
|
|
173
|
+
const results: string[] = [];
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < operations.length; i++) {
|
|
176
|
+
const op = operations[i];
|
|
177
|
+
|
|
178
|
+
// Validate operation
|
|
179
|
+
if (op.action === "rename" && !op.new_name) {
|
|
180
|
+
return { success: false, output: "", error: `Operation ${i + 1}: rename requires new_name` };
|
|
181
|
+
}
|
|
182
|
+
if ((op.action === "replace" || op.action === "insert_before" || op.action === "insert_after" || op.action === "replace_body") && op.content === undefined) {
|
|
183
|
+
return { success: false, output: "", error: `Operation ${i + 1}: ${op.action} requires content` };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const result = this.applyOperation(content, filePath, op);
|
|
188
|
+
content = result.content;
|
|
189
|
+
results.push(`${i + 1}. ${op.action} ${op.node_type} "${op.name}" — ${result.message}`);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
return { success: false, output: results.join("\n"), error: `Operation ${i + 1} failed: ${(err as Error).message}` };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Write result
|
|
196
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
197
|
+
|
|
198
|
+
// Generate diff summary
|
|
199
|
+
const oldLines = originalContent.split("\n");
|
|
200
|
+
const newLines = content.split("\n");
|
|
201
|
+
const diffHeader = `--- a/${input.file_path}\n+++ b/${input.file_path}\n@@ -1,${oldLines.length} +1,${newLines.length} @@`;
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
success: true,
|
|
205
|
+
output: `AST-edited ${filePath}: ${operations.length} operation(s)\n\n${results.join("\n")}\n\n${diffHeader}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Initialize tree-sitter parser for the given file's language.
|
|
211
|
+
*/
|
|
212
|
+
private async initParser(filePath: string): Promise<ToolResult> {
|
|
213
|
+
const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
|
|
214
|
+
const langPackage = LANGUAGE_MAP[ext];
|
|
215
|
+
|
|
216
|
+
if (!langPackage) {
|
|
217
|
+
return {
|
|
218
|
+
success: false,
|
|
219
|
+
output: "",
|
|
220
|
+
error: `Unsupported file type: ${ext}. Supported: ${Object.keys(LANGUAGE_MAP).join(", ")}`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
if (!Parser) {
|
|
226
|
+
Parser = require("tree-sitter");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!languageCache.has(langPackage)) {
|
|
230
|
+
const lang = require(langPackage);
|
|
231
|
+
languageCache.set(langPackage, lang);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { success: true, output: "" };
|
|
235
|
+
} catch {
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
output: "",
|
|
239
|
+
error: `Tree-sitter not available. Install with: npm install tree-sitter ${langPackage}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Parse source code and apply a single AST operation.
|
|
246
|
+
*/
|
|
247
|
+
private applyOperation(
|
|
248
|
+
content: string,
|
|
249
|
+
filePath: string,
|
|
250
|
+
op: ASTEditOperation,
|
|
251
|
+
): { content: string; message: string } {
|
|
252
|
+
const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
|
|
253
|
+
const langPackage = LANGUAGE_MAP[ext]!;
|
|
254
|
+
const language = languageCache.get(langPackage);
|
|
255
|
+
|
|
256
|
+
const parser = new Parser();
|
|
257
|
+
parser.setLanguage(language);
|
|
258
|
+
const tree = parser.parse(content);
|
|
259
|
+
|
|
260
|
+
// Find the target node
|
|
261
|
+
const node = this.findNode(tree.rootNode, op.node_type, op.name);
|
|
262
|
+
if (!node) {
|
|
263
|
+
throw new Error(`Could not find ${op.node_type} named "${op.name}" in AST`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const startIdx = node.startIndex;
|
|
267
|
+
const endIdx = node.endIndex;
|
|
268
|
+
|
|
269
|
+
switch (op.action) {
|
|
270
|
+
case "replace":
|
|
271
|
+
return {
|
|
272
|
+
content: content.substring(0, startIdx) + op.content! + content.substring(endIdx),
|
|
273
|
+
message: `replaced (${node.startPosition.row + 1}:${node.startPosition.column}-${node.endPosition.row + 1}:${node.endPosition.column})`,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
case "delete":
|
|
277
|
+
// Delete the node and any trailing newline
|
|
278
|
+
let deleteEnd = endIdx;
|
|
279
|
+
if (content[deleteEnd] === "\n") deleteEnd++;
|
|
280
|
+
return {
|
|
281
|
+
content: content.substring(0, startIdx) + content.substring(deleteEnd),
|
|
282
|
+
message: `deleted (${node.startPosition.row + 1}:${node.startPosition.column}-${node.endPosition.row + 1}:${node.endPosition.column})`,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
case "rename":
|
|
286
|
+
return this.renameNode(content, node, op.name, op.new_name!);
|
|
287
|
+
|
|
288
|
+
case "insert_before":
|
|
289
|
+
return {
|
|
290
|
+
content: content.substring(0, startIdx) + op.content! + "\n" + content.substring(startIdx),
|
|
291
|
+
message: `inserted before (line ${node.startPosition.row + 1})`,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
case "insert_after":
|
|
295
|
+
return {
|
|
296
|
+
content: content.substring(0, endIdx) + "\n" + op.content! + content.substring(endIdx),
|
|
297
|
+
message: `inserted after (line ${node.endPosition.row + 1})`,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
case "replace_body": {
|
|
301
|
+
const body = this.findBody(node);
|
|
302
|
+
if (!body) {
|
|
303
|
+
throw new Error(`No body found for ${op.node_type} "${op.name}"`);
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
content: content.substring(0, body.startIndex) + op.content! + content.substring(body.endIndex),
|
|
307
|
+
message: `body replaced (${body.startPosition.row + 1}:${body.startPosition.column}-${body.endPosition.row + 1}:${body.endPosition.column})`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
default:
|
|
312
|
+
throw new Error(`Unknown action: ${op.action}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Find an AST node by type and name.
|
|
318
|
+
*/
|
|
319
|
+
private findNode(rootNode: any, nodeType: ASTNodeType, name: string): any {
|
|
320
|
+
const matches: any[] = [];
|
|
321
|
+
this.walkTree(rootNode, (node: any) => {
|
|
322
|
+
if (this.matchesNodeType(node, nodeType) && this.getNodeName(node) === name) {
|
|
323
|
+
matches.push(node);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
return matches[0] || null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Walk the AST tree depth-first, calling visitor on each node.
|
|
331
|
+
*/
|
|
332
|
+
private walkTree(node: any, visitor: (node: any) => void): void {
|
|
333
|
+
visitor(node);
|
|
334
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
335
|
+
this.walkTree(node.child(i), visitor);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Check if an AST node matches the requested type category.
|
|
341
|
+
*/
|
|
342
|
+
private matchesNodeType(node: any, nodeType: ASTNodeType): boolean {
|
|
343
|
+
const type = node.type as string;
|
|
344
|
+
|
|
345
|
+
switch (nodeType) {
|
|
346
|
+
case "function":
|
|
347
|
+
return [
|
|
348
|
+
"function_declaration", "function_definition", "function_item",
|
|
349
|
+
"arrow_function", "method_definition", "function_expression",
|
|
350
|
+
"export_statement", // may wrap a function
|
|
351
|
+
].includes(type);
|
|
352
|
+
|
|
353
|
+
case "class":
|
|
354
|
+
return [
|
|
355
|
+
"class_declaration", "class_definition", "class_statement",
|
|
356
|
+
"struct_item", "impl_item",
|
|
357
|
+
].includes(type);
|
|
358
|
+
|
|
359
|
+
case "method":
|
|
360
|
+
return [
|
|
361
|
+
"method_definition", "method_declaration", "function_item",
|
|
362
|
+
"public_method_definition",
|
|
363
|
+
].includes(type);
|
|
364
|
+
|
|
365
|
+
case "import":
|
|
366
|
+
return [
|
|
367
|
+
"import_statement", "import_declaration", "use_declaration",
|
|
368
|
+
"import_from_statement", "include_statement",
|
|
369
|
+
].includes(type);
|
|
370
|
+
|
|
371
|
+
case "variable":
|
|
372
|
+
return [
|
|
373
|
+
"variable_declaration", "lexical_declaration", "const_declaration",
|
|
374
|
+
"let_declaration", "variable_declarator", "assignment_expression",
|
|
375
|
+
"static_item", "const_item",
|
|
376
|
+
].includes(type);
|
|
377
|
+
|
|
378
|
+
case "type":
|
|
379
|
+
return [
|
|
380
|
+
"type_alias_declaration", "type_definition", "typedef_declaration",
|
|
381
|
+
].includes(type);
|
|
382
|
+
|
|
383
|
+
case "interface":
|
|
384
|
+
return [
|
|
385
|
+
"interface_declaration", "interface_definition",
|
|
386
|
+
].includes(type);
|
|
387
|
+
|
|
388
|
+
case "enum":
|
|
389
|
+
return [
|
|
390
|
+
"enum_declaration", "enum_definition", "enum_item",
|
|
391
|
+
].includes(type);
|
|
392
|
+
|
|
393
|
+
case "block":
|
|
394
|
+
return [
|
|
395
|
+
"statement_block", "block", "compound_statement",
|
|
396
|
+
"block_expression",
|
|
397
|
+
].includes(type);
|
|
398
|
+
|
|
399
|
+
case "expression":
|
|
400
|
+
return [
|
|
401
|
+
"expression_statement", "call_expression", "assignment_expression",
|
|
402
|
+
].includes(type);
|
|
403
|
+
|
|
404
|
+
default:
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Extract the name identifier from an AST node.
|
|
411
|
+
*/
|
|
412
|
+
private getNodeName(node: any): string | null {
|
|
413
|
+
// Direct name child
|
|
414
|
+
const nameNode = node.childForFieldName?.("name");
|
|
415
|
+
if (nameNode) return nameNode.text;
|
|
416
|
+
|
|
417
|
+
// Check for declarator (variable declarations)
|
|
418
|
+
const declarator = node.childForFieldName?.("declarator");
|
|
419
|
+
if (declarator) {
|
|
420
|
+
const declName = declarator.childForFieldName?.("name");
|
|
421
|
+
if (declName) return declName.text;
|
|
422
|
+
return declarator.text;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// For export statements, check the declaration inside
|
|
426
|
+
if (node.type === "export_statement") {
|
|
427
|
+
const decl = node.childForFieldName?.("declaration");
|
|
428
|
+
if (decl) return this.getNodeName(decl);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// For imports, try to get the module/source name
|
|
432
|
+
if (node.type?.includes("import")) {
|
|
433
|
+
const source = node.childForFieldName?.("source") || node.childForFieldName?.("module_name");
|
|
434
|
+
if (source) return source.text?.replace(/['"]/g, "");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// For lexical_declaration, check first declarator child
|
|
438
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
439
|
+
const child = node.child(i);
|
|
440
|
+
if (child.type === "variable_declarator") {
|
|
441
|
+
const childName = child.childForFieldName?.("name");
|
|
442
|
+
if (childName) return childName.text;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Find the body/block node within a function, method, or class.
|
|
451
|
+
*/
|
|
452
|
+
private findBody(node: any): any {
|
|
453
|
+
// Common body field names
|
|
454
|
+
const bodyNode = node.childForFieldName?.("body") || node.childForFieldName?.("block");
|
|
455
|
+
if (bodyNode) return bodyNode;
|
|
456
|
+
|
|
457
|
+
// Fallback: find first block-type child
|
|
458
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
459
|
+
const child = node.child(i);
|
|
460
|
+
if (["statement_block", "block", "compound_statement", "class_body"].includes(child.type)) {
|
|
461
|
+
return child;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Rename a node by replacing all occurrences of the old name with the new name
|
|
470
|
+
* within the node's text range.
|
|
471
|
+
*/
|
|
472
|
+
private renameNode(
|
|
473
|
+
content: string,
|
|
474
|
+
node: any,
|
|
475
|
+
oldName: string,
|
|
476
|
+
newName: string,
|
|
477
|
+
): { content: string; message: string } {
|
|
478
|
+
const nodeText = content.substring(node.startIndex, node.endIndex);
|
|
479
|
+
// Replace identifier occurrences (word-boundary aware)
|
|
480
|
+
const pattern = new RegExp(`\\b${escapeRegex(oldName)}\\b`, "g");
|
|
481
|
+
const updatedText = nodeText.replace(pattern, newName);
|
|
482
|
+
const count = (nodeText.match(pattern) || []).length;
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
content: content.substring(0, node.startIndex) + updatedText + content.substring(node.endIndex),
|
|
486
|
+
message: `renamed ${count} occurrence(s) of "${oldName}" → "${newName}"`,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function escapeRegex(str: string): string {
|
|
492
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
493
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { exec } from "child_process";
|
|
4
|
+
import type { BaseTool, ToolDefinition, ToolResult } from "./types";
|
|
5
|
+
import { safePath } from "../utils/path-safety";
|
|
6
|
+
|
|
7
|
+
/** Map file extensions to the command that checks their syntax/types */
|
|
8
|
+
const CHECKERS: Record<string, (filePath: string, cwd: string) => string> = {
|
|
9
|
+
// JavaScript — Node's --check flag validates syntax without running
|
|
10
|
+
".js": (f) => `node --check "${f}"`,
|
|
11
|
+
".mjs": (f) => `node --check "${f}"`,
|
|
12
|
+
".cjs": (f) => `node --check "${f}"`,
|
|
13
|
+
|
|
14
|
+
// TypeScript — tsc with --noEmit to type-check without producing output
|
|
15
|
+
".ts": (f, cwd) => {
|
|
16
|
+
const tsconfig = path.join(cwd, "tsconfig.json");
|
|
17
|
+
if (fs.existsSync(tsconfig)) {
|
|
18
|
+
return `npx tsc --noEmit --pretty "${f}"`;
|
|
19
|
+
}
|
|
20
|
+
return `npx tsc --noEmit --pretty --esModuleInterop --resolveJsonModule "${f}"`;
|
|
21
|
+
},
|
|
22
|
+
".tsx": (f, cwd) => {
|
|
23
|
+
const tsconfig = path.join(cwd, "tsconfig.json");
|
|
24
|
+
if (fs.existsSync(tsconfig)) {
|
|
25
|
+
return `npx tsc --noEmit --pretty --jsx react-jsx "${f}"`;
|
|
26
|
+
}
|
|
27
|
+
return `npx tsc --noEmit --pretty --esModuleInterop --resolveJsonModule --jsx react-jsx "${f}"`;
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// Python — py_compile checks syntax
|
|
31
|
+
".py": (f) => `python3 -m py_compile "${f}"`,
|
|
32
|
+
|
|
33
|
+
// Ruby — -c flag checks syntax
|
|
34
|
+
".rb": (f) => `ruby -c "${f}"`,
|
|
35
|
+
|
|
36
|
+
// Shell — bash -n checks syntax without executing
|
|
37
|
+
".sh": (f) => `bash -n "${f}"`,
|
|
38
|
+
".bash": (f) => `bash -n "${f}"`,
|
|
39
|
+
".zsh": (f) => `zsh -n "${f}"`,
|
|
40
|
+
|
|
41
|
+
// Go — vet checks for common errors
|
|
42
|
+
".go": (f) => `go vet "${f}"`,
|
|
43
|
+
|
|
44
|
+
// PHP — lint mode
|
|
45
|
+
".php": (f) => `php -l "${f}"`,
|
|
46
|
+
|
|
47
|
+
// Perl — check syntax
|
|
48
|
+
".pl": (f) => `perl -c "${f}"`,
|
|
49
|
+
|
|
50
|
+
// Swift — just parse/typecheck
|
|
51
|
+
".swift": (f) => `swiftc -typecheck "${f}"`,
|
|
52
|
+
|
|
53
|
+
// JSON — validate structure
|
|
54
|
+
".json": (f) => `node -e "JSON.parse(require('fs').readFileSync('${f.replace(/'/g, "\\'")}','utf8'));console.log('Valid JSON')"`,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export class CodeVerifyTool implements BaseTool {
|
|
58
|
+
definition: ToolDefinition = {
|
|
59
|
+
name: "code_verify",
|
|
60
|
+
description:
|
|
61
|
+
"Verify that code in a file is syntactically correct and (where possible) type-safe. " +
|
|
62
|
+
"Runs language-specific checks: node --check for JS, tsc --noEmit for TS, py_compile for Python, etc. " +
|
|
63
|
+
"Use after writing or editing code to catch errors before running.",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
file_path: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Path to the file to verify",
|
|
70
|
+
},
|
|
71
|
+
timeout: {
|
|
72
|
+
type: "number",
|
|
73
|
+
description: "Timeout in ms. Default: 30000",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: ["file_path"],
|
|
77
|
+
},
|
|
78
|
+
requiresPermission: false,
|
|
79
|
+
permissionMessage: (input) => `Verify code: ${input.file_path}`,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
private workingDir: string;
|
|
83
|
+
constructor(workingDir: string) {
|
|
84
|
+
this.workingDir = workingDir;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async execute(input: Record<string, unknown>): Promise<ToolResult> {
|
|
88
|
+
let filePath: string;
|
|
89
|
+
try {
|
|
90
|
+
filePath = safePath(input.file_path as string, this.workingDir);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return { success: false, output: "", error: (err as Error).message };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const timeout = (input.timeout as number) || 30000;
|
|
96
|
+
|
|
97
|
+
if (!fs.existsSync(filePath)) {
|
|
98
|
+
return { success: false, output: "", error: `File not found: ${filePath}` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
102
|
+
const checkerFn = CHECKERS[ext];
|
|
103
|
+
if (!checkerFn) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
output: "",
|
|
107
|
+
error: `No verifier for ${ext} files. Supported: ${Object.keys(CHECKERS).join(", ")}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const command = checkerFn(filePath, this.workingDir);
|
|
112
|
+
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
exec(
|
|
115
|
+
command,
|
|
116
|
+
{ cwd: this.workingDir, timeout, maxBuffer: 10 * 1024 * 1024, env: { ...process.env } },
|
|
117
|
+
(error, stdout, stderr) => {
|
|
118
|
+
const outputParts: string[] = [];
|
|
119
|
+
if (stdout) outputParts.push(stdout.trimEnd());
|
|
120
|
+
if (stderr) outputParts.push(stderr.trimEnd());
|
|
121
|
+
const output = outputParts.join("\n") || "(no output)";
|
|
122
|
+
|
|
123
|
+
if (error?.killed) {
|
|
124
|
+
return resolve({ success: false, output, error: `Timed out after ${timeout}ms` });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (error) {
|
|
128
|
+
resolve({
|
|
129
|
+
success: false,
|
|
130
|
+
output,
|
|
131
|
+
error: `Verification failed (exit code ${error.code}). See output for details.`,
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
resolve({
|
|
135
|
+
success: true,
|
|
136
|
+
output: output === "(no output)" ? "Code verification passed — no errors found." : output,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|