@acmecloud/core 1.0.2
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/agent/index.d.ts +52 -0
- package/dist/agent/index.js +476 -0
- package/dist/config/index.d.ts +83 -0
- package/dist/config/index.js +318 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +30 -0
- package/dist/llm/provider.d.ts +27 -0
- package/dist/llm/provider.js +202 -0
- package/dist/llm/vision.d.ts +7 -0
- package/dist/llm/vision.js +37 -0
- package/dist/mcp/index.d.ts +10 -0
- package/dist/mcp/index.js +84 -0
- package/dist/prompt/anthropic.d.ts +1 -0
- package/dist/prompt/anthropic.js +32 -0
- package/dist/prompt/architect.d.ts +1 -0
- package/dist/prompt/architect.js +17 -0
- package/dist/prompt/autopilot.d.ts +1 -0
- package/dist/prompt/autopilot.js +18 -0
- package/dist/prompt/beast.d.ts +1 -0
- package/dist/prompt/beast.js +83 -0
- package/dist/prompt/gemini.d.ts +1 -0
- package/dist/prompt/gemini.js +45 -0
- package/dist/prompt/index.d.ts +18 -0
- package/dist/prompt/index.js +239 -0
- package/dist/prompt/zen.d.ts +1 -0
- package/dist/prompt/zen.js +13 -0
- package/dist/session/index.d.ts +18 -0
- package/dist/session/index.js +97 -0
- package/dist/skills/index.d.ts +6 -0
- package/dist/skills/index.js +72 -0
- package/dist/tools/batch.d.ts +2 -0
- package/dist/tools/batch.js +65 -0
- package/dist/tools/browser.d.ts +7 -0
- package/dist/tools/browser.js +86 -0
- package/dist/tools/edit.d.ts +11 -0
- package/dist/tools/edit.js +312 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +980 -0
- package/dist/tools/lsp-client.d.ts +11 -0
- package/dist/tools/lsp-client.js +224 -0
- package/package.json +42 -0
- package/src/agent/index.ts +588 -0
- package/src/config/index.ts +383 -0
- package/src/context/index.ts +34 -0
- package/src/llm/provider.ts +237 -0
- package/src/llm/vision.ts +43 -0
- package/src/mcp/index.ts +110 -0
- package/src/prompt/anthropic.ts +32 -0
- package/src/prompt/architect.ts +17 -0
- package/src/prompt/autopilot.ts +18 -0
- package/src/prompt/beast.ts +83 -0
- package/src/prompt/gemini.ts +45 -0
- package/src/prompt/index.ts +267 -0
- package/src/prompt/zen.ts +13 -0
- package/src/session/index.ts +129 -0
- package/src/skills/index.ts +86 -0
- package/src/tools/batch.ts +73 -0
- package/src/tools/browser.ts +95 -0
- package/src/tools/edit.ts +317 -0
- package/src/tools/index.ts +1112 -0
- package/src/tools/lsp-client.ts +303 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
import { tool as createTool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import * as fs from "fs/promises";
|
|
4
|
+
import { existsSync, createReadStream } from "fs";
|
|
5
|
+
import { exec } from "child_process";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { pathToFileURL } from "url";
|
|
9
|
+
import { createInterface } from "readline";
|
|
10
|
+
import TurndownService from "turndown";
|
|
11
|
+
import * as diff from "diff";
|
|
12
|
+
import { replaceCode } from "./edit.js";
|
|
13
|
+
import { getLspClientForFile } from "./lsp-client.js";
|
|
14
|
+
import { executeBatch, BATCH_WHITELIST } from "./batch.js";
|
|
15
|
+
import { executeBrowserAction } from "./browser.js";
|
|
16
|
+
|
|
17
|
+
const execAsync = promisify(exec);
|
|
18
|
+
|
|
19
|
+
// ── Constants (from opencode patterns) ──
|
|
20
|
+
const DEFAULT_READ_LIMIT = 2000;
|
|
21
|
+
const MAX_LINE_LENGTH = 2000;
|
|
22
|
+
const MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)`;
|
|
23
|
+
const COMMAND_TIMEOUT = 120_000; // 2 minutes
|
|
24
|
+
|
|
25
|
+
// Directories to ignore in list_dir (from opencode's ls.ts)
|
|
26
|
+
const IGNORE_DIRS = new Set([
|
|
27
|
+
"node_modules",
|
|
28
|
+
"__pycache__",
|
|
29
|
+
".git",
|
|
30
|
+
"dist",
|
|
31
|
+
"build",
|
|
32
|
+
"target",
|
|
33
|
+
"vendor",
|
|
34
|
+
"bin",
|
|
35
|
+
"obj",
|
|
36
|
+
".idea",
|
|
37
|
+
".vscode",
|
|
38
|
+
".cache",
|
|
39
|
+
"coverage",
|
|
40
|
+
".venv",
|
|
41
|
+
"venv",
|
|
42
|
+
"env",
|
|
43
|
+
"tmp",
|
|
44
|
+
"temp",
|
|
45
|
+
"logs",
|
|
46
|
+
".zig-cache",
|
|
47
|
+
"zig-out",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
// ── Tool Definitions ──
|
|
51
|
+
interface ToolDef {
|
|
52
|
+
description: string;
|
|
53
|
+
parameters: z.ZodType<any>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const toolDefinitions: Record<string, ToolDef> = {
|
|
57
|
+
read_file: {
|
|
58
|
+
description: [
|
|
59
|
+
"Read a file from the local filesystem with line-based access.",
|
|
60
|
+
"",
|
|
61
|
+
"Parameters:",
|
|
62
|
+
"- filePath: Required. Absolute or relative path to the file.",
|
|
63
|
+
"- offset: Line number to start from (1-indexed, default: 1).",
|
|
64
|
+
"- limit: Max lines to read (default: 2000).",
|
|
65
|
+
"- infoOnly: If true, only return file info (line count, size) without content.",
|
|
66
|
+
"",
|
|
67
|
+
"Tips:",
|
|
68
|
+
"- Use infoOnly=true first to check file size and line count before reading.",
|
|
69
|
+
"- For large files, read in chunks using offset and limit.",
|
|
70
|
+
"- Lines are numbered starting from 1.",
|
|
71
|
+
"- Lines longer than 2000 chars are truncated.",
|
|
72
|
+
"- You can read multiple files in parallel using the batch tool.",
|
|
73
|
+
].join("\n"),
|
|
74
|
+
parameters: z.object({
|
|
75
|
+
filePath: z.string().describe("The path to the file to read. Required."),
|
|
76
|
+
offset: z
|
|
77
|
+
.number()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("Line number to start from (1-indexed, default: 1)"),
|
|
80
|
+
limit: z
|
|
81
|
+
.number()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Max number of lines to read (default: 2000)"),
|
|
84
|
+
infoOnly: z
|
|
85
|
+
.boolean()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("If true, only return file info without content"),
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
webfetch: {
|
|
91
|
+
description: [
|
|
92
|
+
"Fetch content from a URL.",
|
|
93
|
+
"Best used for reading online documentation, API references, or web pages.",
|
|
94
|
+
"Automatically converts HTML to clean Markdown by default to save tokens.",
|
|
95
|
+
].join("\n"),
|
|
96
|
+
parameters: z.object({
|
|
97
|
+
url: z
|
|
98
|
+
.string()
|
|
99
|
+
.url()
|
|
100
|
+
.optional()
|
|
101
|
+
.describe(
|
|
102
|
+
"The URL to fetch content from (must start with http:// or https://). Do NOT omit this.",
|
|
103
|
+
),
|
|
104
|
+
format: z
|
|
105
|
+
.enum(["markdown", "text", "html"])
|
|
106
|
+
.optional()
|
|
107
|
+
.describe("Format to return (default: markdown)"),
|
|
108
|
+
}),
|
|
109
|
+
},
|
|
110
|
+
websearch: {
|
|
111
|
+
description: [
|
|
112
|
+
"Perform a web search using the Exa search engine.",
|
|
113
|
+
"Best used for finding general information, recent news, or broad topics.",
|
|
114
|
+
].join("\n"),
|
|
115
|
+
parameters: z.object({
|
|
116
|
+
query: z
|
|
117
|
+
.string()
|
|
118
|
+
.optional()
|
|
119
|
+
.describe("The search query. Do NOT omit this."),
|
|
120
|
+
numResults: z
|
|
121
|
+
.number()
|
|
122
|
+
.optional()
|
|
123
|
+
.describe("Number of results to return (default: 8)"),
|
|
124
|
+
}),
|
|
125
|
+
},
|
|
126
|
+
codesearch: {
|
|
127
|
+
description: [
|
|
128
|
+
"Search for code snippets, API documentation, and technical references.",
|
|
129
|
+
'Uses Exa optimized for coding queries. E.g., "React useState examples", "TypeScript omit utility".',
|
|
130
|
+
].join("\n"),
|
|
131
|
+
parameters: z.object({
|
|
132
|
+
query: z
|
|
133
|
+
.string()
|
|
134
|
+
.optional()
|
|
135
|
+
.describe("The technical search query. Do NOT omit this."),
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
grep_search: {
|
|
139
|
+
description: [
|
|
140
|
+
"Search for a regex pattern within files in a directory using ripgrep.",
|
|
141
|
+
"Returns the file paths and line numbers of the matches.",
|
|
142
|
+
"Requires the pattern to search for.",
|
|
143
|
+
].join("\n"),
|
|
144
|
+
parameters: z.object({
|
|
145
|
+
pattern: z
|
|
146
|
+
.string()
|
|
147
|
+
.optional()
|
|
148
|
+
.describe(
|
|
149
|
+
"The regex pattern to search for in file contents. Do NOT omit this.",
|
|
150
|
+
),
|
|
151
|
+
path: z
|
|
152
|
+
.string()
|
|
153
|
+
.optional()
|
|
154
|
+
.describe(
|
|
155
|
+
"The directory to search in. Defaults to the current working directory.",
|
|
156
|
+
),
|
|
157
|
+
include: z
|
|
158
|
+
.string()
|
|
159
|
+
.optional()
|
|
160
|
+
.describe(
|
|
161
|
+
'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
|
|
162
|
+
),
|
|
163
|
+
}),
|
|
164
|
+
},
|
|
165
|
+
edit_file: {
|
|
166
|
+
description: [
|
|
167
|
+
"Edit a specific chunk of code in an existing file using fuzzy replacement.",
|
|
168
|
+
"You provide the exact old code text block to replace (oldString), and the new code to replace it with (newString).",
|
|
169
|
+
"This tool is vastly safer and more token-efficient than replacing the whole file with write_file.",
|
|
170
|
+
"Automatic Safety: After editing, the tool automatically runs LSP diagnostic checks and reports any new errors or warnings introduced by the change.",
|
|
171
|
+
"Requires an absolute path or a path relative to CWD.",
|
|
172
|
+
].join("\n"),
|
|
173
|
+
parameters: z.object({
|
|
174
|
+
filePath: z
|
|
175
|
+
.string()
|
|
176
|
+
.optional()
|
|
177
|
+
.describe("The path to the file to modify. Do NOT omit this."),
|
|
178
|
+
oldString: z
|
|
179
|
+
.string()
|
|
180
|
+
.optional()
|
|
181
|
+
.describe(
|
|
182
|
+
"The block of text or code to replace. Provide enough surrounding code so the match is unique in the file. Do NOT omit this.",
|
|
183
|
+
),
|
|
184
|
+
newString: z
|
|
185
|
+
.string()
|
|
186
|
+
.optional()
|
|
187
|
+
.describe(
|
|
188
|
+
"The new block of text or code to replace it with. Must be different from oldString. Do NOT omit this.",
|
|
189
|
+
),
|
|
190
|
+
replaceAll: z
|
|
191
|
+
.boolean()
|
|
192
|
+
.optional()
|
|
193
|
+
.describe(
|
|
194
|
+
"If true, replaces all occurrences instead of throwing an error when multiple matches are found (default: false)",
|
|
195
|
+
),
|
|
196
|
+
}),
|
|
197
|
+
},
|
|
198
|
+
lsp: {
|
|
199
|
+
description: [
|
|
200
|
+
"Language Server Protocol integration for Deep Code Understanding.",
|
|
201
|
+
"Supported languages: TypeScript/JavaScript, Python, Go, Rust.",
|
|
202
|
+
"Allows you to find where a variable/function is defined, where it gets used, or what its type signature is.",
|
|
203
|
+
].join("\n"),
|
|
204
|
+
parameters: z.object({
|
|
205
|
+
operation: z
|
|
206
|
+
.enum(["goToDefinition", "findReferences", "hover"])
|
|
207
|
+
.optional()
|
|
208
|
+
.describe("The LSP operation to perform. Do NOT omit this."),
|
|
209
|
+
filePath: z
|
|
210
|
+
.string()
|
|
211
|
+
.optional()
|
|
212
|
+
.describe(
|
|
213
|
+
"The path to the file you want to interrogate. Do NOT omit this.",
|
|
214
|
+
),
|
|
215
|
+
line: z
|
|
216
|
+
.number()
|
|
217
|
+
.int()
|
|
218
|
+
.min(1)
|
|
219
|
+
.optional()
|
|
220
|
+
.describe("The line number in editors (1-based). Do NOT omit this."),
|
|
221
|
+
character: z
|
|
222
|
+
.number()
|
|
223
|
+
.int()
|
|
224
|
+
.min(1)
|
|
225
|
+
.optional()
|
|
226
|
+
.describe(
|
|
227
|
+
"The character column offset in editors (1-based). Do NOT omit this.",
|
|
228
|
+
),
|
|
229
|
+
}),
|
|
230
|
+
},
|
|
231
|
+
write_file: {
|
|
232
|
+
description: [
|
|
233
|
+
"Write content to a file. Creates parent directories if needed.",
|
|
234
|
+
"The filePath parameter must be provided.",
|
|
235
|
+
"Use absolute or relative paths (resolved against CWD).",
|
|
236
|
+
].join("\n"),
|
|
237
|
+
parameters: z.object({
|
|
238
|
+
filePath: z
|
|
239
|
+
.string()
|
|
240
|
+
.min(1)
|
|
241
|
+
.describe("The path to the file to write (required)"),
|
|
242
|
+
content: z
|
|
243
|
+
.string()
|
|
244
|
+
.optional()
|
|
245
|
+
.describe("The content to write into the file. Do NOT omit this."),
|
|
246
|
+
}),
|
|
247
|
+
},
|
|
248
|
+
run_command: {
|
|
249
|
+
description: [
|
|
250
|
+
"Run a shell command on the host machine.",
|
|
251
|
+
"The working directory defaults to the current project directory.",
|
|
252
|
+
`Platform: ${process.platform}. Use platform-appropriate commands (e.g., 'Get-Content' instead of 'cat' on Windows).`,
|
|
253
|
+
"On Windows, commands are executed via PowerShell for maximum compatibility.",
|
|
254
|
+
"Commands that take longer than 2 minutes will be killed.",
|
|
255
|
+
"Dangerous commands (e.g. destructive actions, system changes) will require manual user approval.",
|
|
256
|
+
"Use this for system operations, builds, tests, and git commands.",
|
|
257
|
+
].join("\n"),
|
|
258
|
+
parameters: z.object({
|
|
259
|
+
command: z
|
|
260
|
+
.string()
|
|
261
|
+
.describe("The shell command to execute. Do NOT omit this."),
|
|
262
|
+
description: z
|
|
263
|
+
.string()
|
|
264
|
+
.optional()
|
|
265
|
+
.describe("Brief description of what this command does (5-10 words)"),
|
|
266
|
+
riskLevel: z
|
|
267
|
+
.enum(["low", "medium", "high"])
|
|
268
|
+
.describe(
|
|
269
|
+
"Self-assessed risk level of the command. low: read-only/no-side-effect, medium: build/complex-read, high: destructive/system-change.",
|
|
270
|
+
),
|
|
271
|
+
cwd: z
|
|
272
|
+
.string()
|
|
273
|
+
.optional()
|
|
274
|
+
.describe("Working directory to run the command in"),
|
|
275
|
+
}),
|
|
276
|
+
},
|
|
277
|
+
list_dir: {
|
|
278
|
+
description: [
|
|
279
|
+
"List directory contents as a tree structure.",
|
|
280
|
+
"Automatically ignores common non-essential directories (node_modules, .git, dist, etc.).",
|
|
281
|
+
'Use this tool instead of running "ls" or "dir" commands.',
|
|
282
|
+
"The path defaults to the current working directory if not specified.",
|
|
283
|
+
].join("\n"),
|
|
284
|
+
parameters: z.object({
|
|
285
|
+
path: z
|
|
286
|
+
.string()
|
|
287
|
+
.optional()
|
|
288
|
+
.describe("The path to the directory to list (defaults to CWD)"),
|
|
289
|
+
}),
|
|
290
|
+
},
|
|
291
|
+
switch_mode: {
|
|
292
|
+
description: [
|
|
293
|
+
"Switch the agent mode to plan or code mode.",
|
|
294
|
+
"Use this when transitioning phases as instructed by your system prompt.",
|
|
295
|
+
].join("\n"),
|
|
296
|
+
parameters: z.object({
|
|
297
|
+
mode: z
|
|
298
|
+
.enum(["plan", "code"])
|
|
299
|
+
.optional()
|
|
300
|
+
.describe("The target mode to switch to. Do NOT omit this."),
|
|
301
|
+
contextFile: z
|
|
302
|
+
.string()
|
|
303
|
+
.optional()
|
|
304
|
+
.describe(
|
|
305
|
+
"The task checklist or design document to focus on (usually for code mode).",
|
|
306
|
+
),
|
|
307
|
+
reason: z
|
|
308
|
+
.string()
|
|
309
|
+
.optional()
|
|
310
|
+
.describe(
|
|
311
|
+
"Explanation for why the mode is being switched. Do NOT omit this.",
|
|
312
|
+
),
|
|
313
|
+
}),
|
|
314
|
+
},
|
|
315
|
+
batch: {
|
|
316
|
+
description: [
|
|
317
|
+
"Execute multiple read-only tools in parallel and return their combined output.",
|
|
318
|
+
"Use this to speed up research, dependency analysis, or searching across multiple locations.",
|
|
319
|
+
`Allowed tools: ${Array.from(BATCH_WHITELIST).join(", ")}.`,
|
|
320
|
+
"",
|
|
321
|
+
"IMPORTANT: Each call MUST have 'name' (tool name) and 'arguments' (tool parameters object).",
|
|
322
|
+
"Example format:",
|
|
323
|
+
`{ "calls": [`,
|
|
324
|
+
` { "name": "read_file", "arguments": { "filePath": "/path/to/file.ts" } },`,
|
|
325
|
+
` { "name": "list_dir", "arguments": { "path": "/path/to/dir" } }`,
|
|
326
|
+
`] }`,
|
|
327
|
+
].join("\n"),
|
|
328
|
+
parameters: z.object({
|
|
329
|
+
calls: z
|
|
330
|
+
.array(
|
|
331
|
+
z.object({
|
|
332
|
+
name: z
|
|
333
|
+
.string()
|
|
334
|
+
.describe("The tool name, e.g. 'read_file', 'list_dir'"),
|
|
335
|
+
arguments: z
|
|
336
|
+
.record(z.string(), z.any())
|
|
337
|
+
.describe(
|
|
338
|
+
'The tool arguments as an object, e.g. { "filePath": "/path/to/file" }',
|
|
339
|
+
),
|
|
340
|
+
}),
|
|
341
|
+
)
|
|
342
|
+
.describe("List of tool calls to execute in parallel."),
|
|
343
|
+
}),
|
|
344
|
+
},
|
|
345
|
+
browser_action: {
|
|
346
|
+
description: [
|
|
347
|
+
"Perform actions in a headless browser (headless Chromium).",
|
|
348
|
+
"Actions: navigate, screenshot, click, type, scroll.",
|
|
349
|
+
"Visual Feedback: If a vision model is configured, screenshots will be automatically analyzed and described.",
|
|
350
|
+
"Use this to test web applications, check UI layouts, or read web-only content.",
|
|
351
|
+
].join("\n"),
|
|
352
|
+
parameters: z.object({
|
|
353
|
+
action: z.enum(["navigate", "screenshot", "click", "type", "scroll"]),
|
|
354
|
+
url: z.string().optional().describe("URL to navigate to."),
|
|
355
|
+
selector: z.string().optional().describe("CSS selector for click/type."),
|
|
356
|
+
text: z.string().optional().describe("Text to type."),
|
|
357
|
+
}),
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Identify dangerous or destructive commands that should require user approval.
|
|
363
|
+
*/
|
|
364
|
+
export function isCommandSafe(command: string): boolean {
|
|
365
|
+
const lower = command.toLowerCase().trim();
|
|
366
|
+
|
|
367
|
+
const dangerousPatterns = [
|
|
368
|
+
// Destructive (Linux/Unix)
|
|
369
|
+
/\brm\b.*(-rf|-r\b|-f\b)/i,
|
|
370
|
+
/\bmkfs\b/i,
|
|
371
|
+
/\bdd\b.*\bof=\/dev\/(sd[a-z]|nvme|mmcblk)/i,
|
|
372
|
+
|
|
373
|
+
// Destructive (Windows)
|
|
374
|
+
/\bdel\b.*\s+\/[sq]+\b/i,
|
|
375
|
+
/\brd\b.*\s+\/[sq]+\b/i,
|
|
376
|
+
/\bformat\b\s+[a-z]:/i,
|
|
377
|
+
/\bsc\s+delete\b/i,
|
|
378
|
+
/\bbcdedit\b/i,
|
|
379
|
+
|
|
380
|
+
// System configuration/control
|
|
381
|
+
/\breg\b\s+(delete|add)\b/i,
|
|
382
|
+
/\bshutdown\b/i,
|
|
383
|
+
/\breboot\b/i,
|
|
384
|
+
/\bnet\s+(user|localgroup|share)\b/i,
|
|
385
|
+
/\bnetsh\b/i,
|
|
386
|
+
/\bkill\b/i,
|
|
387
|
+
/\btaskkill\b/i,
|
|
388
|
+
/\bstop-service\b/i,
|
|
389
|
+
/\bremove-item\b.*(-recurse|-force|-confirm)/i,
|
|
390
|
+
/\b(clear-disk|initialize-disk|format-volume|remove-partition)\b/i,
|
|
391
|
+
/\bset-executionpolicy\b.*\b(unrestricted|bypass)\b/i,
|
|
392
|
+
|
|
393
|
+
// Permission/Ownership changes
|
|
394
|
+
/\bchmod\b.*\b777\b/,
|
|
395
|
+
/\bchown\b/i,
|
|
396
|
+
/\bicacls\b/i,
|
|
397
|
+
/\btakeown\b/i,
|
|
398
|
+
|
|
399
|
+
// Shell piping (Remote execution risk)
|
|
400
|
+
/\|\s*(bash|sh|powershell|pwsh|cmd)(\s|$)/i,
|
|
401
|
+
/>\s*(bash|sh|powershell|pwsh|cmd)(\s|$)/i,
|
|
402
|
+
/\bcurl\b.*\s+\|\s*(bash|sh|powershell|pwsh|cmd)\b/i,
|
|
403
|
+
/\bwget\b.*\s+\|\s*(bash|sh|powershell|pwsh|cmd)\b/i,
|
|
404
|
+
/\biex\b/i, // PowerShell Invoke-Expression
|
|
405
|
+
|
|
406
|
+
// Cloud & Operations Tools (Destructive)
|
|
407
|
+
/\bdocker\b.*\s+(rm|stop|rmi|prune|kill)\b/i,
|
|
408
|
+
/\bkubectl\b.*\s+(delete|scale|patch|edit)\b/i,
|
|
409
|
+
/\bterraform\b.*\s+(destroy|apply)\b/i,
|
|
410
|
+
/\bgcloud\b.*\s+(delete|remove|stop)\b/i,
|
|
411
|
+
/\baws\b.*\s+(delete|terminate|stop)\b/i,
|
|
412
|
+
|
|
413
|
+
// Credential/Identity Access
|
|
414
|
+
/\.ssh\//,
|
|
415
|
+
/\.aws\//,
|
|
416
|
+
/\.kube\//,
|
|
417
|
+
/\.env\b/i,
|
|
418
|
+
/\b(npm|git|gh)\b.*\s+(login|auth|config)\b/i,
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
for (const pattern of dangerousPatterns) {
|
|
422
|
+
if (pattern.test(lower)) return false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
export const toolExecutors: Record<string, (args: any) => Promise<string>> = {
|
|
428
|
+
read_file: async (args) => {
|
|
429
|
+
try {
|
|
430
|
+
const filePath = args.filePath || args.path;
|
|
431
|
+
if (!filePath)
|
|
432
|
+
return `Error: The "filePath" argument is required. Please provide the file path.`;
|
|
433
|
+
|
|
434
|
+
const resolved = path.resolve(process.cwd(), filePath);
|
|
435
|
+
|
|
436
|
+
// Check if file exists
|
|
437
|
+
try {
|
|
438
|
+
const stat = await fs.stat(resolved);
|
|
439
|
+
if (stat.isDirectory()) {
|
|
440
|
+
// If it's a directory, delegate to list_dir
|
|
441
|
+
return toolExecutors.list_dir!({ path: filePath });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// infoOnly mode: just return file info
|
|
445
|
+
if (args.infoOnly) {
|
|
446
|
+
const stream = createReadStream(resolved, { encoding: "utf8" });
|
|
447
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
448
|
+
let totalLines = 0;
|
|
449
|
+
try {
|
|
450
|
+
for await (const _ of rl) {
|
|
451
|
+
totalLines++;
|
|
452
|
+
}
|
|
453
|
+
} finally {
|
|
454
|
+
rl.close();
|
|
455
|
+
stream.destroy();
|
|
456
|
+
}
|
|
457
|
+
return `<file-info>
|
|
458
|
+
<path>${resolved}</path>
|
|
459
|
+
<size>${stat.size} bytes</size>
|
|
460
|
+
<lines>${totalLines}</lines>
|
|
461
|
+
</file-info>`;
|
|
462
|
+
}
|
|
463
|
+
} catch {
|
|
464
|
+
// Try to suggest similar files
|
|
465
|
+
const dir = path.dirname(resolved);
|
|
466
|
+
const base = path.basename(resolved).toLowerCase();
|
|
467
|
+
try {
|
|
468
|
+
const entries = await fs.readdir(dir);
|
|
469
|
+
const suggestions = entries
|
|
470
|
+
.filter(
|
|
471
|
+
(e) =>
|
|
472
|
+
e.toLowerCase().includes(base) ||
|
|
473
|
+
base.includes(e.toLowerCase()),
|
|
474
|
+
)
|
|
475
|
+
.slice(0, 3);
|
|
476
|
+
if (suggestions.length > 0) {
|
|
477
|
+
return `Error: File not found: ${resolved}\n\nDid you mean one of these?\n${suggestions.map((s) => path.join(dir, s)).join("\n")}`;
|
|
478
|
+
}
|
|
479
|
+
} catch {
|
|
480
|
+
/* ignore */
|
|
481
|
+
}
|
|
482
|
+
return `Error: File not found: ${resolved}`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Read file with line numbers (opencode pattern)
|
|
486
|
+
const limit = args.limit ?? DEFAULT_READ_LIMIT;
|
|
487
|
+
const offset = args.offset ?? 1;
|
|
488
|
+
const start = offset - 1;
|
|
489
|
+
|
|
490
|
+
const stream = createReadStream(resolved, { encoding: "utf8" });
|
|
491
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
492
|
+
|
|
493
|
+
const lines: string[] = [];
|
|
494
|
+
let totalLines = 0;
|
|
495
|
+
let hasMore = false;
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
for await (const text of rl) {
|
|
499
|
+
totalLines++;
|
|
500
|
+
if (totalLines <= start) continue;
|
|
501
|
+
|
|
502
|
+
if (lines.length >= limit) {
|
|
503
|
+
hasMore = true;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const line =
|
|
508
|
+
text.length > MAX_LINE_LENGTH
|
|
509
|
+
? text.substring(0, MAX_LINE_LENGTH) + MAX_LINE_SUFFIX
|
|
510
|
+
: text;
|
|
511
|
+
lines.push(line);
|
|
512
|
+
}
|
|
513
|
+
} finally {
|
|
514
|
+
rl.close();
|
|
515
|
+
stream.destroy();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Format output with line numbers (opencode-style)
|
|
519
|
+
const numbered = lines.map((line, i) => `${i + offset}: ${line}`);
|
|
520
|
+
const lastLine = offset + lines.length - 1;
|
|
521
|
+
|
|
522
|
+
let output = `<path>${resolved}</path>\n<content>\n`;
|
|
523
|
+
output += numbered.join("\n");
|
|
524
|
+
|
|
525
|
+
if (hasMore) {
|
|
526
|
+
output += `\n\n(Showing lines ${offset}-${lastLine} of ${totalLines}. Use offset=${lastLine + 1} to continue.)`;
|
|
527
|
+
} else {
|
|
528
|
+
output += `\n\n(End of file - total ${totalLines} lines)`;
|
|
529
|
+
}
|
|
530
|
+
output += "\n</content>";
|
|
531
|
+
|
|
532
|
+
return output;
|
|
533
|
+
} catch (err: any) {
|
|
534
|
+
return `Error reading file: ${err.message}`;
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
webfetch: async (args) => {
|
|
539
|
+
try {
|
|
540
|
+
const url = args.url;
|
|
541
|
+
if (!url) return 'Error: The "url" argument is required';
|
|
542
|
+
const format = args.format || "markdown";
|
|
543
|
+
if (!url?.startsWith("http://") && !url?.startsWith("https://")) {
|
|
544
|
+
return "Error: URL must start with http:// or https://";
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const controller = new AbortController();
|
|
548
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
|
|
549
|
+
|
|
550
|
+
const headers = {
|
|
551
|
+
"User-Agent":
|
|
552
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
|
|
553
|
+
Accept:
|
|
554
|
+
format === "markdown"
|
|
555
|
+
? "text/markdown, text/html, */*"
|
|
556
|
+
: "text/html, */*",
|
|
557
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
let response = await fetch(url, { signal: controller.signal, headers });
|
|
561
|
+
|
|
562
|
+
// Handle Cloudflare challenge fingerprint mismatch
|
|
563
|
+
if (
|
|
564
|
+
response.status === 403 &&
|
|
565
|
+
response.headers.get("cf-mitigated") === "challenge"
|
|
566
|
+
) {
|
|
567
|
+
response = await fetch(url, {
|
|
568
|
+
signal: controller.signal,
|
|
569
|
+
headers: { ...headers, "User-Agent": "acmecode" },
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
clearTimeout(timeoutId);
|
|
574
|
+
|
|
575
|
+
if (!response.ok) {
|
|
576
|
+
return `Error: Request failed with status ${response.status} ${response.statusText}`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const contentType = response.headers.get("content-type") || "";
|
|
580
|
+
const mime = contentType.split(";")[0]?.trim().toLowerCase();
|
|
581
|
+
|
|
582
|
+
if (
|
|
583
|
+
mime.startsWith("image/") ||
|
|
584
|
+
mime.startsWith("video/") ||
|
|
585
|
+
mime.startsWith("audio/")
|
|
586
|
+
) {
|
|
587
|
+
return `Error: Cannot fetch binary media files (${mime})`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Ensure response isn't massive (limit 5MB)
|
|
591
|
+
const contentLength = response.headers.get("content-length");
|
|
592
|
+
if (contentLength && parseInt(contentLength) > 5 * 1024 * 1024) {
|
|
593
|
+
return "Error: Response too large (exceeds 5MB)";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const content = await response.text();
|
|
597
|
+
|
|
598
|
+
if (format === "markdown" && contentType.includes("text/html")) {
|
|
599
|
+
const turndownService = new TurndownService({
|
|
600
|
+
headingStyle: "atx",
|
|
601
|
+
codeBlockStyle: "fenced",
|
|
602
|
+
});
|
|
603
|
+
turndownService.remove([
|
|
604
|
+
"script",
|
|
605
|
+
"style",
|
|
606
|
+
"meta",
|
|
607
|
+
"link",
|
|
608
|
+
"noscript",
|
|
609
|
+
"iframe",
|
|
610
|
+
]);
|
|
611
|
+
const markdown = turndownService.turndown(content);
|
|
612
|
+
return `<source>${url}</source>\n<content>\n${markdown}\n</content>`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return `<source>${url}</source>\n<content>\n${content.slice(0, 100000)}\n</content>`; // Cap raw text
|
|
616
|
+
} catch (err: any) {
|
|
617
|
+
if (err.name === "AbortError")
|
|
618
|
+
return "Error: Request timed out after 30 seconds";
|
|
619
|
+
return `Error fetching URL: ${err.message}`;
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
|
|
623
|
+
websearch: async (args) => {
|
|
624
|
+
try {
|
|
625
|
+
if (!args.query) return 'Error: The "query" argument is required';
|
|
626
|
+
const controller = new AbortController();
|
|
627
|
+
const timeoutId = setTimeout(() => controller.abort(), 25000); // 25s timeout
|
|
628
|
+
|
|
629
|
+
const payload = {
|
|
630
|
+
jsonrpc: "2.0",
|
|
631
|
+
id: 1,
|
|
632
|
+
method: "tools/call",
|
|
633
|
+
params: {
|
|
634
|
+
name: "web_search_exa",
|
|
635
|
+
arguments: {
|
|
636
|
+
query: args.query,
|
|
637
|
+
type: "auto",
|
|
638
|
+
numResults: args.numResults || 8,
|
|
639
|
+
livecrawl: "fallback",
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const response = await fetch("https://mcp.exa.ai/mcp", {
|
|
645
|
+
method: "POST",
|
|
646
|
+
headers: {
|
|
647
|
+
accept: "application/json, text/event-stream",
|
|
648
|
+
"content-type": "application/json",
|
|
649
|
+
},
|
|
650
|
+
body: JSON.stringify(payload),
|
|
651
|
+
signal: controller.signal,
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
clearTimeout(timeoutId);
|
|
655
|
+
|
|
656
|
+
if (!response.ok)
|
|
657
|
+
return `Error: Search failed with status ${response.status} ${await response.text()}`;
|
|
658
|
+
|
|
659
|
+
const responseText = await response.text();
|
|
660
|
+
for (const line of responseText.split("\n")) {
|
|
661
|
+
if (line.startsWith("data: ")) {
|
|
662
|
+
const data = JSON.parse(line.substring(6));
|
|
663
|
+
if (data.result?.content?.[0]?.text) {
|
|
664
|
+
return data.result.content[0].text;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return "No search results found.";
|
|
669
|
+
} catch (err: any) {
|
|
670
|
+
if (err.name === "AbortError") return "Error: Search request timed out";
|
|
671
|
+
return `Error performing web search: ${err.message}`;
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
codesearch: async (args) => {
|
|
676
|
+
try {
|
|
677
|
+
if (!args.query) return 'Error: The "query" argument is required';
|
|
678
|
+
const controller = new AbortController();
|
|
679
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
680
|
+
|
|
681
|
+
const payload = {
|
|
682
|
+
jsonrpc: "2.0",
|
|
683
|
+
id: 1,
|
|
684
|
+
method: "tools/call",
|
|
685
|
+
params: {
|
|
686
|
+
name: "get_code_context_exa",
|
|
687
|
+
arguments: {
|
|
688
|
+
query: args.query,
|
|
689
|
+
tokensNum: 5000,
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const response = await fetch("https://mcp.exa.ai/mcp", {
|
|
695
|
+
method: "POST",
|
|
696
|
+
headers: {
|
|
697
|
+
accept: "application/json, text/event-stream",
|
|
698
|
+
"content-type": "application/json",
|
|
699
|
+
},
|
|
700
|
+
body: JSON.stringify(payload),
|
|
701
|
+
signal: controller.signal,
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
clearTimeout(timeoutId);
|
|
705
|
+
|
|
706
|
+
if (!response.ok)
|
|
707
|
+
return `Error: Code search failed with status ${response.status} ${await response.text()}`;
|
|
708
|
+
|
|
709
|
+
const responseText = await response.text();
|
|
710
|
+
for (const line of responseText.split("\n")) {
|
|
711
|
+
if (line.startsWith("data: ")) {
|
|
712
|
+
const data = JSON.parse(line.substring(6));
|
|
713
|
+
if (data.result?.content?.[0]?.text) {
|
|
714
|
+
return data.result.content[0].text;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return "No code snippets or documentation found.";
|
|
719
|
+
} catch (err: any) {
|
|
720
|
+
if (err.name === "AbortError")
|
|
721
|
+
return "Error: Code search request timed out";
|
|
722
|
+
return `Error performing code search: ${err.message}`;
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
|
|
726
|
+
grep_search: async (args) => {
|
|
727
|
+
try {
|
|
728
|
+
if (!args.pattern) return "Error: pattern is required";
|
|
729
|
+
const searchPath = path.resolve(process.cwd(), args.path || ".");
|
|
730
|
+
|
|
731
|
+
// Assuming rg (ripgrep) is available in PATH
|
|
732
|
+
const rgArgs = [
|
|
733
|
+
"-nH",
|
|
734
|
+
"--hidden",
|
|
735
|
+
"--no-messages",
|
|
736
|
+
"--glob",
|
|
737
|
+
"!.git/*",
|
|
738
|
+
"--glob",
|
|
739
|
+
"!node_modules/*",
|
|
740
|
+
"--field-match-separator=|",
|
|
741
|
+
"--regexp",
|
|
742
|
+
args.pattern,
|
|
743
|
+
];
|
|
744
|
+
if (args.include) {
|
|
745
|
+
rgArgs.push("--glob", args.include);
|
|
746
|
+
}
|
|
747
|
+
rgArgs.push(searchPath);
|
|
748
|
+
|
|
749
|
+
const { stdout } = await execAsync(
|
|
750
|
+
`rg ${rgArgs.map((arg) => `"${arg.replace(/"/g, '\\"')}"`).join(" ")}`,
|
|
751
|
+
{
|
|
752
|
+
cwd: searchPath,
|
|
753
|
+
maxBuffer: 1024 * 1024 * 5, // 5MB limit
|
|
754
|
+
},
|
|
755
|
+
).catch((e) => {
|
|
756
|
+
// Exit code 1 means no matches, 2 means error.
|
|
757
|
+
if (e.code === 1) return { stdout: "" };
|
|
758
|
+
throw e;
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
if (!stdout.trim())
|
|
762
|
+
return `No files found matching pattern: ${args.pattern}`;
|
|
763
|
+
|
|
764
|
+
const lines = stdout.trim().split(/\r?\n/);
|
|
765
|
+
const matches: string[] = [];
|
|
766
|
+
|
|
767
|
+
let currentFile = "";
|
|
768
|
+
for (const line of lines) {
|
|
769
|
+
if (!line) continue;
|
|
770
|
+
const parts = line.split("|");
|
|
771
|
+
if (parts.length < 3) continue;
|
|
772
|
+
|
|
773
|
+
const filePath = parts[0];
|
|
774
|
+
const lineNum = parts[1];
|
|
775
|
+
const lineText = parts.slice(2).join("|");
|
|
776
|
+
|
|
777
|
+
if (currentFile !== filePath) {
|
|
778
|
+
if (currentFile !== "") matches.push("");
|
|
779
|
+
currentFile = filePath!;
|
|
780
|
+
matches.push(`${filePath}:`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const truncatedText =
|
|
784
|
+
lineText!.length > 2000
|
|
785
|
+
? lineText!.substring(0, 2000) + "..."
|
|
786
|
+
: lineText;
|
|
787
|
+
matches.push(` Line ${lineNum}: ${truncatedText}`);
|
|
788
|
+
|
|
789
|
+
if (matches.length > 200) {
|
|
790
|
+
matches.push("");
|
|
791
|
+
matches.push(`(Results truncated. Showing first 200 matches.)`);
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return matches.join("\n");
|
|
797
|
+
} catch (err: any) {
|
|
798
|
+
return `Error running grep search: ${err.message}`;
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
|
|
802
|
+
edit_file: async (args) => {
|
|
803
|
+
try {
|
|
804
|
+
const filePath = args.filePath;
|
|
805
|
+
if (!filePath) return 'Error: The "filePath" argument is required';
|
|
806
|
+
if (args.oldString === undefined)
|
|
807
|
+
return 'Error: The "oldString" argument is required';
|
|
808
|
+
if (args.newString === undefined)
|
|
809
|
+
return 'Error: The "newString" argument is required';
|
|
810
|
+
if (args.oldString === args.newString)
|
|
811
|
+
return "Error: No changes to apply (oldString and newString are identical)";
|
|
812
|
+
|
|
813
|
+
const resolved = path.resolve(process.cwd(), filePath);
|
|
814
|
+
|
|
815
|
+
// Check if file exists
|
|
816
|
+
try {
|
|
817
|
+
const stat = await fs.stat(resolved);
|
|
818
|
+
if (stat.isDirectory())
|
|
819
|
+
return `Error: Path is a directory, not a file: ${resolved}`;
|
|
820
|
+
} catch {
|
|
821
|
+
return `Error: File not found: ${resolved}`;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const contentOld = await fs.readFile(resolved, "utf8");
|
|
825
|
+
const contentNew = replaceCode(
|
|
826
|
+
contentOld,
|
|
827
|
+
args.oldString,
|
|
828
|
+
args.newString,
|
|
829
|
+
args.replaceAll,
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
await fs.writeFile(resolved, contentNew, "utf8");
|
|
833
|
+
|
|
834
|
+
const patch = diff.createTwoFilesPatch(
|
|
835
|
+
path.basename(resolved),
|
|
836
|
+
path.basename(resolved),
|
|
837
|
+
contentOld,
|
|
838
|
+
contentNew,
|
|
839
|
+
"original",
|
|
840
|
+
"modified",
|
|
841
|
+
{ context: 5 },
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
// Calculate additions/deletions
|
|
845
|
+
let additions = 0;
|
|
846
|
+
let deletions = 0;
|
|
847
|
+
const lineDiffs = diff.diffLines(contentOld, contentNew);
|
|
848
|
+
for (const change of lineDiffs) {
|
|
849
|
+
if (change.added) additions += change.count || 0;
|
|
850
|
+
if (change.removed) deletions += change.count || 0;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
let output = `Edit applied to ${path.basename(resolved)} (+${additions} -${deletions})\n\n`;
|
|
854
|
+
output += `\`\`\`diff\n${patch}\`\`\``;
|
|
855
|
+
|
|
856
|
+
// LSP Diagnostics Check
|
|
857
|
+
try {
|
|
858
|
+
const workspaceRoot = process.cwd();
|
|
859
|
+
const client = await getLspClientForFile(workspaceRoot, resolved);
|
|
860
|
+
// openFile waits for publishDiagnostics (or times out), so diagnostics are ready after this
|
|
861
|
+
await client.openFile(resolved);
|
|
862
|
+
|
|
863
|
+
const uri = pathToFileURL(resolved).href;
|
|
864
|
+
|
|
865
|
+
const normalizeUri = (u: string) =>
|
|
866
|
+
decodeURIComponent(u)
|
|
867
|
+
.toLowerCase()
|
|
868
|
+
.replace(/^file:\/\/\/([a-z]):/, "file:///$1:");
|
|
869
|
+
const targetNorm = normalizeUri(uri);
|
|
870
|
+
|
|
871
|
+
let diagnostics: any[] = client.diagnostics.get(uri) || [];
|
|
872
|
+
if (diagnostics.length === 0) {
|
|
873
|
+
for (const [key, val] of client.diagnostics.entries()) {
|
|
874
|
+
if (normalizeUri(key) === targetNorm) {
|
|
875
|
+
diagnostics = val;
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const errors = diagnostics.filter((d) => d.severity === 1); // 1 is Error
|
|
882
|
+
|
|
883
|
+
if (errors.length > 0) {
|
|
884
|
+
output += `\n\n⚠️ LSP Errors detected after edit (please fix cases where your edit broke the code):\n`;
|
|
885
|
+
for (const err of errors) {
|
|
886
|
+
const line = err.range.start.line + 1;
|
|
887
|
+
const char = err.range.start.character + 1;
|
|
888
|
+
output += `- [Line ${line}, Col ${char}]: ${err.message}\n`;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
} catch (lspErr) {
|
|
892
|
+
// LSP failure shouldn't fail the edit itself
|
|
893
|
+
console.error(`[EditTool] LSP check failed:`, lspErr);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return output;
|
|
897
|
+
} catch (err: any) {
|
|
898
|
+
return `Error editing file: ${err.message}`;
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
|
|
902
|
+
lsp: async (args) => {
|
|
903
|
+
try {
|
|
904
|
+
if (!args.operation) return 'Error: The "operation" argument is required';
|
|
905
|
+
if (!args.filePath) return 'Error: The "filePath" argument is required';
|
|
906
|
+
if (!args.line) return 'Error: The "line" argument is required';
|
|
907
|
+
if (!args.character) return 'Error: The "character" argument is required';
|
|
908
|
+
|
|
909
|
+
const workspaceRoot = process.cwd(); // Assume agent runs in workspace root
|
|
910
|
+
const resolved = path.resolve(workspaceRoot, args.filePath);
|
|
911
|
+
|
|
912
|
+
// Validate file bounds before attempting
|
|
913
|
+
const fileExists = existsSync(resolved);
|
|
914
|
+
if (!fileExists) return `Error: File not found: ${resolved}`;
|
|
915
|
+
|
|
916
|
+
const client = await getLspClientForFile(workspaceRoot, resolved);
|
|
917
|
+
|
|
918
|
+
// Notify Server we are looking at this file (updates diagnostics)
|
|
919
|
+
await client.openFile(resolved);
|
|
920
|
+
|
|
921
|
+
const position = {
|
|
922
|
+
line: args.line - 1, // LSP is 0-based index
|
|
923
|
+
character: args.character - 1,
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
const uri = pathToFileURL(resolved).href;
|
|
927
|
+
|
|
928
|
+
let result: any;
|
|
929
|
+
switch (args.operation) {
|
|
930
|
+
case "goToDefinition":
|
|
931
|
+
result = await client.connection.sendRequest(
|
|
932
|
+
"textDocument/definition",
|
|
933
|
+
{
|
|
934
|
+
textDocument: { uri },
|
|
935
|
+
position,
|
|
936
|
+
},
|
|
937
|
+
);
|
|
938
|
+
break;
|
|
939
|
+
case "findReferences":
|
|
940
|
+
result = await client.connection.sendRequest(
|
|
941
|
+
"textDocument/references",
|
|
942
|
+
{
|
|
943
|
+
textDocument: { uri },
|
|
944
|
+
position,
|
|
945
|
+
context: { includeDeclaration: true },
|
|
946
|
+
},
|
|
947
|
+
);
|
|
948
|
+
break;
|
|
949
|
+
case "hover":
|
|
950
|
+
result = await client.connection.sendRequest("textDocument/hover", {
|
|
951
|
+
textDocument: { uri },
|
|
952
|
+
position,
|
|
953
|
+
});
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (!result || (Array.isArray(result) && result.length === 0)) {
|
|
958
|
+
return `No results found for ${args.operation} at ${args.filePath}:${args.line}:${args.character}`;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return JSON.stringify(result, null, 2);
|
|
962
|
+
} catch (err: any) {
|
|
963
|
+
return `Error running LSP operation: ${err.message}`;
|
|
964
|
+
}
|
|
965
|
+
},
|
|
966
|
+
|
|
967
|
+
write_file: async (args) => {
|
|
968
|
+
try {
|
|
969
|
+
const filePath = args.filePath || args.path;
|
|
970
|
+
if (!filePath) return `Error: The "filePath" argument is required`;
|
|
971
|
+
if (args.content === undefined)
|
|
972
|
+
return `Error: The "content" argument is required`;
|
|
973
|
+
const resolved = path.resolve(process.cwd(), filePath);
|
|
974
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
975
|
+
const existed = existsSync(resolved);
|
|
976
|
+
await fs.writeFile(resolved, args.content, "utf8");
|
|
977
|
+
const lineCount = (args.content.match(/\n/g) || []).length + 1;
|
|
978
|
+
return `Wrote file successfully: ${resolved} (${lineCount} lines, ${existed ? "updated" : "created"})`;
|
|
979
|
+
} catch (err: any) {
|
|
980
|
+
return `Error writing file: ${err.message}`;
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
|
|
984
|
+
run_command: async (args) => {
|
|
985
|
+
const decodeOutput = (buf: any): string => {
|
|
986
|
+
if (!buf) return "";
|
|
987
|
+
if (typeof buf === "string") return buf;
|
|
988
|
+
if (process.platform === "win32") {
|
|
989
|
+
try {
|
|
990
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(buf);
|
|
991
|
+
} catch {
|
|
992
|
+
return new TextDecoder("gbk").decode(buf);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return new TextDecoder("utf-8").decode(buf);
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
try {
|
|
999
|
+
if (!args.command) return 'Error: The "command" argument is required';
|
|
1000
|
+
const cwd = args.cwd || process.cwd();
|
|
1001
|
+
const shell = process.platform === "win32" ? "powershell.exe" : undefined;
|
|
1002
|
+
const { stdout, stderr } = await execAsync(args.command, {
|
|
1003
|
+
cwd,
|
|
1004
|
+
timeout: COMMAND_TIMEOUT,
|
|
1005
|
+
maxBuffer: 1024 * 1024 * 10, // 10MB
|
|
1006
|
+
windowsHide: true,
|
|
1007
|
+
encoding: "buffer",
|
|
1008
|
+
shell,
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
const outStr = decodeOutput(stdout);
|
|
1012
|
+
const errStr = decodeOutput(stderr);
|
|
1013
|
+
|
|
1014
|
+
let output = `> ${args.command}\n`;
|
|
1015
|
+
if (outStr.trim()) output += outStr.trim();
|
|
1016
|
+
if (errStr.trim())
|
|
1017
|
+
output += (output ? "\n\nStderr:\n" : "Stderr:\n") + errStr.trim();
|
|
1018
|
+
return output || `> ${args.command}\n(Command completed with no output)`;
|
|
1019
|
+
} catch (err: any) {
|
|
1020
|
+
const outStr = decodeOutput(err.stdout);
|
|
1021
|
+
const errStr = decodeOutput(err.stderr) || err.message;
|
|
1022
|
+
if (err.killed)
|
|
1023
|
+
return `> ${args.command}\nCommand timed out after ${COMMAND_TIMEOUT / 1000}s and was killed.\nPartial output:\n${outStr}`;
|
|
1024
|
+
return `> ${args.command}\nCommand failed (exit code ${err.code || "?"}):\n${errStr}`;
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
|
|
1028
|
+
list_dir: async (args) => {
|
|
1029
|
+
try {
|
|
1030
|
+
const targetPath = args.path || ".";
|
|
1031
|
+
const dirPath = path.resolve(process.cwd(), targetPath);
|
|
1032
|
+
|
|
1033
|
+
// Build tree structure (opencode pattern)
|
|
1034
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
1035
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1036
|
+
|
|
1037
|
+
const lines: string[] = [`${dirPath}/`];
|
|
1038
|
+
|
|
1039
|
+
for (const entry of entries) {
|
|
1040
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
1041
|
+
if (
|
|
1042
|
+
entry.name.startsWith(".") &&
|
|
1043
|
+
![
|
|
1044
|
+
".env",
|
|
1045
|
+
".acmecode.md",
|
|
1046
|
+
".acmecode",
|
|
1047
|
+
".github",
|
|
1048
|
+
".vscode",
|
|
1049
|
+
".cursor",
|
|
1050
|
+
].includes(entry.name)
|
|
1051
|
+
)
|
|
1052
|
+
continue;
|
|
1053
|
+
|
|
1054
|
+
if (entry.isDirectory()) {
|
|
1055
|
+
lines.push(` ${entry.name}/`);
|
|
1056
|
+
// Read one level deep for subdirectories
|
|
1057
|
+
try {
|
|
1058
|
+
const subEntries = await fs.readdir(
|
|
1059
|
+
path.join(dirPath, entry.name),
|
|
1060
|
+
{ withFileTypes: true },
|
|
1061
|
+
);
|
|
1062
|
+
subEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1063
|
+
for (const sub of subEntries.slice(0, 20)) {
|
|
1064
|
+
if (IGNORE_DIRS.has(sub.name)) continue;
|
|
1065
|
+
lines.push(` ${sub.name}${sub.isDirectory() ? "/" : ""}`);
|
|
1066
|
+
}
|
|
1067
|
+
if (subEntries.length > 20) {
|
|
1068
|
+
lines.push(` ... and ${subEntries.length - 20} more`);
|
|
1069
|
+
}
|
|
1070
|
+
} catch {
|
|
1071
|
+
/* skip unreadable subdirs */
|
|
1072
|
+
}
|
|
1073
|
+
} else {
|
|
1074
|
+
lines.push(` ${entry.name}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return lines.join("\n");
|
|
1079
|
+
} catch (err: any) {
|
|
1080
|
+
return `Error listing directory: ${err.message}`;
|
|
1081
|
+
}
|
|
1082
|
+
},
|
|
1083
|
+
|
|
1084
|
+
switch_mode: async (args) => {
|
|
1085
|
+
if (!args.mode)
|
|
1086
|
+
return JSON.stringify({ error: 'The "mode" argument is required' });
|
|
1087
|
+
// We return a JSON string containing the mode switch request,
|
|
1088
|
+
// which the agent loop will intercept and translate into a 'mode-changed' event.
|
|
1089
|
+
return JSON.stringify({
|
|
1090
|
+
switched_mode: args.mode,
|
|
1091
|
+
contextFile: args.contextFile,
|
|
1092
|
+
reason: args.reason,
|
|
1093
|
+
});
|
|
1094
|
+
},
|
|
1095
|
+
batch: async (args) => {
|
|
1096
|
+
return executeBatch(args, toolExecutors);
|
|
1097
|
+
},
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
// Create AI SDK tool objects (with execute) for the agent
|
|
1101
|
+
const tool = (options: any): any => createTool(options);
|
|
1102
|
+
|
|
1103
|
+
export const builtInTools: Record<string, any> = Object.fromEntries(
|
|
1104
|
+
Object.entries(toolDefinitions).map(([name, def]) => [
|
|
1105
|
+
name,
|
|
1106
|
+
tool({
|
|
1107
|
+
description: def.description,
|
|
1108
|
+
parameters: def.parameters,
|
|
1109
|
+
execute: toolExecutors[name],
|
|
1110
|
+
}),
|
|
1111
|
+
]),
|
|
1112
|
+
);
|