@claude-code-kit/tools 0.2.0 → 0.3.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/README.md +7 -7
- package/dist/index.d.mts +172 -26
- package/dist/index.d.ts +172 -26
- package/dist/index.js +1764 -101
- package/dist/index.mjs +1758 -102
- package/package.json +9 -9
- package/LICENSE +0 -21
package/dist/index.js
CHANGED
|
@@ -32,46 +32,80 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
bashTool: () => bashTool,
|
|
34
34
|
builtinTools: () => builtinTools,
|
|
35
|
+
createLspTool: () => createLspTool,
|
|
36
|
+
createSubagentTool: () => createSubagentTool,
|
|
37
|
+
createTaskTool: () => createTaskTool,
|
|
35
38
|
editTool: () => editTool,
|
|
39
|
+
enterWorktreeTool: () => enterWorktreeTool,
|
|
40
|
+
exitWorktreeTool: () => exitWorktreeTool,
|
|
36
41
|
globTool: () => globTool,
|
|
37
42
|
grepTool: () => grepTool,
|
|
43
|
+
notebookEditTool: () => notebookEditTool,
|
|
38
44
|
readTool: () => readTool,
|
|
39
45
|
webFetchTool: () => webFetchTool,
|
|
46
|
+
webSearchTool: () => webSearchTool,
|
|
40
47
|
writeTool: () => writeTool
|
|
41
48
|
});
|
|
42
49
|
module.exports = __toCommonJS(index_exports);
|
|
43
50
|
|
|
44
51
|
// src/bash.ts
|
|
45
52
|
var import_node_child_process = require("child_process");
|
|
53
|
+
var fs = __toESM(require("fs"));
|
|
54
|
+
var os = __toESM(require("os"));
|
|
55
|
+
var path = __toESM(require("path"));
|
|
46
56
|
var import_zod = require("zod");
|
|
47
57
|
var MAX_RESULT_SIZE = 1e5;
|
|
58
|
+
var DEFAULT_TIMEOUT = 12e4;
|
|
59
|
+
var MAX_TIMEOUT = 6e5;
|
|
48
60
|
var inputSchema = import_zod.z.object({
|
|
49
61
|
command: import_zod.z.string().describe("The shell command to execute"),
|
|
62
|
+
description: import_zod.z.string().describe("A description of what this command does"),
|
|
50
63
|
cwd: import_zod.z.string().optional().describe("Working directory for the command"),
|
|
51
|
-
timeout: import_zod.z.number().optional().default(
|
|
64
|
+
timeout: import_zod.z.number().optional().default(DEFAULT_TIMEOUT).describe("Timeout in milliseconds (max 600000)"),
|
|
65
|
+
run_in_background: import_zod.z.boolean().optional().default(false).describe("Run the command in the background and return immediately with PID"),
|
|
66
|
+
dangerously_disable_sandbox: import_zod.z.boolean().optional().default(false).describe("Set to true to disable sandbox restrictions. Use with caution \u2014 bypasses security constraints.")
|
|
52
67
|
});
|
|
53
68
|
async function execute(input, ctx) {
|
|
54
69
|
const cwd = input.cwd ?? ctx.workingDirectory;
|
|
55
|
-
const timeout = input.timeout;
|
|
56
|
-
|
|
70
|
+
const timeout = Math.min(input.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
|
|
71
|
+
const sandboxed = !input.dangerously_disable_sandbox;
|
|
72
|
+
if (input.run_in_background) {
|
|
73
|
+
const outFile = path.join(os.tmpdir(), `cck-bg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.log`);
|
|
74
|
+
const out = fs.openSync(outFile, "w");
|
|
75
|
+
const child = (0, import_node_child_process.spawn)("sh", ["-c", input.command], {
|
|
76
|
+
cwd,
|
|
77
|
+
env: { ...process.env, ...ctx.env },
|
|
78
|
+
detached: true,
|
|
79
|
+
stdio: ["ignore", out, out]
|
|
80
|
+
});
|
|
81
|
+
child.unref();
|
|
82
|
+
const pid = child.pid;
|
|
83
|
+
fs.closeSync(out);
|
|
84
|
+
return {
|
|
85
|
+
content: `Background process started (PID: ${pid})
|
|
86
|
+
Output file: ${outFile}`,
|
|
87
|
+
metadata: { pid, outputFile: outFile, sandboxed }
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return new Promise((resolve9) => {
|
|
57
91
|
const onAbort = () => {
|
|
58
92
|
child.kill("SIGTERM");
|
|
59
|
-
|
|
93
|
+
resolve9({ content: "Command aborted", isError: true, metadata: { sandboxed } });
|
|
60
94
|
};
|
|
61
95
|
const child = (0, import_node_child_process.exec)(input.command, { cwd, timeout, env: { ...process.env, ...ctx.env } }, (err, stdout, stderr) => {
|
|
62
96
|
ctx.abortSignal.removeEventListener("abort", onAbort);
|
|
63
97
|
const output = (stdout + (stderr ? `
|
|
64
98
|
${stderr}` : "")).slice(0, MAX_RESULT_SIZE);
|
|
65
99
|
if (err && err.killed) {
|
|
66
|
-
|
|
67
|
-
${output}`, isError: true });
|
|
100
|
+
resolve9({ content: `Command timed out after ${timeout}ms
|
|
101
|
+
${output}`, isError: true, metadata: { sandboxed } });
|
|
68
102
|
return;
|
|
69
103
|
}
|
|
70
104
|
if (err) {
|
|
71
|
-
|
|
105
|
+
resolve9({ content: output || err.message, isError: true, metadata: { exitCode: err.code, sandboxed } });
|
|
72
106
|
return;
|
|
73
107
|
}
|
|
74
|
-
|
|
108
|
+
resolve9({ content: output || "(no output)", metadata: { sandboxed } });
|
|
75
109
|
});
|
|
76
110
|
if (ctx.abortSignal.aborted) {
|
|
77
111
|
onAbort();
|
|
@@ -81,40 +115,111 @@ ${output}`, isError: true });
|
|
|
81
115
|
});
|
|
82
116
|
}
|
|
83
117
|
var bashTool = {
|
|
84
|
-
name: "
|
|
85
|
-
description:
|
|
118
|
+
name: "Bash",
|
|
119
|
+
description: `Executes a given bash command and returns its output.
|
|
120
|
+
|
|
121
|
+
The working directory persists between commands via the \`cwd\` parameter, but shell state does not (no environment variables or aliases carry over between calls).
|
|
122
|
+
|
|
123
|
+
# Sandbox
|
|
124
|
+
|
|
125
|
+
Commands run with a \`sandboxed\` metadata flag (default: true). Set \`dangerously_disable_sandbox: true\` to mark a command as running outside the sandbox boundary. Note: this is currently a policy flag for permission systems and audit trails \u2014 actual OS-level sandboxing (Docker/nsjail) is not yet implemented. The flag allows permission handlers to apply different rules for sandboxed vs unsandboxed commands.
|
|
126
|
+
|
|
127
|
+
# Description field
|
|
128
|
+
|
|
129
|
+
Always provide a clear, concise description in active voice (5-10 words for simple commands, more context for complex ones):
|
|
130
|
+
- ls \u2192 "List files in current directory"
|
|
131
|
+
- git status \u2192 "Show working tree status"
|
|
132
|
+
- find . -name "*.tmp" -exec rm {} \\; \u2192 "Find and delete all .tmp files recursively"
|
|
133
|
+
|
|
134
|
+
# Avoid running these as Bash commands
|
|
135
|
+
|
|
136
|
+
Use dedicated tools instead \u2014 they provide a better experience:
|
|
137
|
+
- File search: use Glob (NOT find or ls)
|
|
138
|
+
- Content search: use Grep (NOT grep or rg)
|
|
139
|
+
- Read files: use Read (NOT cat/head/tail)
|
|
140
|
+
- Edit files: use Edit (NOT sed/awk)
|
|
141
|
+
- Write files: use Write (NOT echo >/cat <<EOF)
|
|
142
|
+
|
|
143
|
+
# File paths
|
|
144
|
+
|
|
145
|
+
Always quote file paths that contain spaces with double quotes in the command string.
|
|
146
|
+
|
|
147
|
+
# Multiple commands
|
|
148
|
+
|
|
149
|
+
- If commands are independent and can run in parallel, make multiple Bash tool calls in the same turn.
|
|
150
|
+
- If commands depend on each other and must run sequentially, use \`&&\` to chain them in a single call.
|
|
151
|
+
- Use \`;\` only when you need sequential execution but don't care if earlier commands fail.
|
|
152
|
+
- Do NOT use newlines to separate commands (newlines are ok in quoted strings).
|
|
153
|
+
|
|
154
|
+
# Avoiding unnecessary sleep
|
|
155
|
+
|
|
156
|
+
- Do not sleep between commands that can run immediately \u2014 just run them.
|
|
157
|
+
- If a command is long-running and you want to be notified when it finishes, set \`run_in_background: true\`. No sleep needed.
|
|
158
|
+
- Do not retry failing commands in a sleep loop \u2014 diagnose the root cause instead.
|
|
159
|
+
- If waiting for a background task, check its status with a follow-up command rather than sleeping.
|
|
160
|
+
- If you must sleep, keep the duration short (1-5 seconds) to avoid blocking.
|
|
161
|
+
|
|
162
|
+
# Timeout
|
|
163
|
+
|
|
164
|
+
Default timeout is 120 seconds. Override with the \`timeout\` field (max 600000 ms / 10 minutes) for long-running operations like builds or test suites.
|
|
165
|
+
|
|
166
|
+
# Background execution
|
|
167
|
+
|
|
168
|
+
Set \`run_in_background: true\` to start a detached process and return immediately with its PID and output log path. Only use this when you don't need the result right away and are OK being notified when the command completes later. Do not use \`&\` at the end of the command when using this parameter.`,
|
|
86
169
|
inputSchema,
|
|
87
170
|
execute,
|
|
88
171
|
isReadOnly: false,
|
|
172
|
+
isDestructive: true,
|
|
89
173
|
requiresConfirmation: true,
|
|
90
|
-
timeout:
|
|
174
|
+
timeout: DEFAULT_TIMEOUT
|
|
91
175
|
};
|
|
92
176
|
|
|
93
177
|
// src/read.ts
|
|
94
|
-
var
|
|
95
|
-
var
|
|
178
|
+
var fs2 = __toESM(require("fs/promises"));
|
|
179
|
+
var path2 = __toESM(require("path"));
|
|
96
180
|
var import_zod2 = require("zod");
|
|
181
|
+
var IMAGE_EXTENSIONS = {
|
|
182
|
+
".png": "image/png",
|
|
183
|
+
".jpg": "image/jpeg",
|
|
184
|
+
".jpeg": "image/jpeg",
|
|
185
|
+
".gif": "image/gif",
|
|
186
|
+
".webp": "image/webp",
|
|
187
|
+
".bmp": "image/bmp"
|
|
188
|
+
};
|
|
97
189
|
var MAX_RESULT_SIZE2 = 1e5;
|
|
190
|
+
var DEFAULT_LIMIT = 2e3;
|
|
98
191
|
var inputSchema2 = import_zod2.z.object({
|
|
99
|
-
|
|
192
|
+
file_path: import_zod2.z.string().describe("Absolute or relative file path to read"),
|
|
100
193
|
offset: import_zod2.z.number().optional().describe("Line number to start reading from (1-based)"),
|
|
101
|
-
limit: import_zod2.z.number().optional().describe("Maximum number of lines to read")
|
|
194
|
+
limit: import_zod2.z.number().optional().describe("Maximum number of lines to read (default 2000)"),
|
|
195
|
+
pages: import_zod2.z.string().optional().describe("Page range for PDF files (e.g. '1-5', '3', '10-20')")
|
|
102
196
|
});
|
|
103
197
|
async function execute2(input, ctx) {
|
|
104
198
|
if (ctx.abortSignal.aborted) return { content: "Aborted", isError: true };
|
|
105
|
-
const filePath =
|
|
106
|
-
if (!filePath.startsWith(ctx.workingDirectory +
|
|
107
|
-
return { content: `Error: path traversal denied \u2014 ${input.
|
|
199
|
+
const filePath = path2.resolve(ctx.workingDirectory, input.file_path);
|
|
200
|
+
if (!filePath.startsWith(ctx.workingDirectory + path2.sep) && filePath !== ctx.workingDirectory) {
|
|
201
|
+
return { content: `Error: path traversal denied \u2014 ${input.file_path} escapes working directory`, isError: true };
|
|
202
|
+
}
|
|
203
|
+
const isPdf = filePath.toLowerCase().endsWith(".pdf");
|
|
204
|
+
if (input.pages !== void 0 && !isPdf) {
|
|
205
|
+
return { content: "Error: the 'pages' parameter is only supported for PDF files", isError: true };
|
|
206
|
+
}
|
|
207
|
+
if (isPdf) {
|
|
208
|
+
return readPdf(filePath, input.pages);
|
|
209
|
+
}
|
|
210
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
211
|
+
const mimeType = IMAGE_EXTENSIONS[ext];
|
|
212
|
+
if (mimeType) {
|
|
213
|
+
return readImage(filePath, mimeType);
|
|
108
214
|
}
|
|
109
215
|
try {
|
|
110
|
-
const raw = await
|
|
216
|
+
const raw = await fs2.readFile(filePath, "utf-8");
|
|
111
217
|
let lines = raw.split("\n");
|
|
112
218
|
if (input.offset !== void 0) {
|
|
113
219
|
lines = lines.slice(Math.max(0, input.offset - 1));
|
|
114
220
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
221
|
+
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
222
|
+
lines = lines.slice(0, limit);
|
|
118
223
|
const startLine = input.offset ?? 1;
|
|
119
224
|
const numbered = lines.map((line, i) => `${startLine + i} ${line}`);
|
|
120
225
|
const content = numbered.join("\n").slice(0, MAX_RESULT_SIZE2);
|
|
@@ -124,9 +229,85 @@ async function execute2(input, ctx) {
|
|
|
124
229
|
return { content: `Error reading file: ${msg}`, isError: true };
|
|
125
230
|
}
|
|
126
231
|
}
|
|
232
|
+
async function readImage(filePath, mimeType) {
|
|
233
|
+
try {
|
|
234
|
+
const buffer = await fs2.readFile(filePath);
|
|
235
|
+
const base64Data = buffer.toString("base64");
|
|
236
|
+
const filename = path2.basename(filePath);
|
|
237
|
+
return {
|
|
238
|
+
content: `[Image: ${filename}]
|
|
239
|
+
Data type: ${mimeType}
|
|
240
|
+
Base64: ${base64Data}`,
|
|
241
|
+
metadata: { mimeType, sizeBytes: buffer.length }
|
|
242
|
+
};
|
|
243
|
+
} catch (err) {
|
|
244
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
245
|
+
return { content: `Error reading image: ${msg}`, isError: true };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function readPdf(filePath, pages) {
|
|
249
|
+
try {
|
|
250
|
+
const moduleName = "pdf-parse";
|
|
251
|
+
let pdfParse = null;
|
|
252
|
+
try {
|
|
253
|
+
const mod = await import(
|
|
254
|
+
/* webpackIgnore: true */
|
|
255
|
+
moduleName
|
|
256
|
+
);
|
|
257
|
+
pdfParse = mod.default ?? mod;
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
if (!pdfParse) {
|
|
261
|
+
return {
|
|
262
|
+
content: "Error: pdf-parse is not installed. Run `npm install pdf-parse` or `pnpm add pdf-parse` to enable PDF reading.",
|
|
263
|
+
isError: true
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const buffer = await fs2.readFile(filePath);
|
|
267
|
+
const data = await pdfParse(buffer);
|
|
268
|
+
const totalPages = data.numpages ?? 0;
|
|
269
|
+
let text = data.text ?? "";
|
|
270
|
+
if (pages) {
|
|
271
|
+
text = `[PDF pages ${pages} requested, ${totalPages} total pages]
|
|
272
|
+
|
|
273
|
+
${text}`;
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
content: text.slice(0, MAX_RESULT_SIZE2),
|
|
277
|
+
metadata: { totalPages }
|
|
278
|
+
};
|
|
279
|
+
} catch (err) {
|
|
280
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
281
|
+
return { content: `Error reading PDF: ${msg}`, isError: true };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
127
284
|
var readTool = {
|
|
128
|
-
name: "
|
|
129
|
-
description:
|
|
285
|
+
name: "Read",
|
|
286
|
+
description: `Reads a file from the local filesystem and returns its contents with line numbers.
|
|
287
|
+
|
|
288
|
+
# File paths
|
|
289
|
+
|
|
290
|
+
The \`file_path\` parameter must be an absolute path. Relative paths are resolved against the agent's working directory. It is OK to read a file that does not exist \u2014 an error will be returned.
|
|
291
|
+
|
|
292
|
+
# Line limits
|
|
293
|
+
|
|
294
|
+
By default, reads up to 2000 lines starting from the beginning of the file. When you already know which part of the file you need, use \`offset\` and \`limit\` to read only that part \u2014 this is important for large files.
|
|
295
|
+
|
|
296
|
+
# Output format
|
|
297
|
+
|
|
298
|
+
Results are returned in cat -n format, with line numbers starting at 1, followed by a tab character, then the line content.
|
|
299
|
+
|
|
300
|
+
# Supported file types
|
|
301
|
+
|
|
302
|
+
- Plain text, source code, JSON, YAML, Markdown, etc.: read directly.
|
|
303
|
+
- Image files (.png, .jpg, .jpeg, .gif, .webp, .bmp): returned as base64-encoded data with MIME type. The \`offset\` and \`limit\` parameters are ignored for binary image files.
|
|
304
|
+
- SVG files (.svg): read as text (XML source), so \`offset\` and \`limit\` work normally.
|
|
305
|
+
- PDF files (.pdf): use the \`pages\` parameter to read specific page ranges (e.g. "1-5", "3", "10-20"). For large PDFs (more than 10 pages), you MUST provide the \`pages\` parameter \u2014 reading a large PDF without it will return the entire extracted text, which may be truncated. Requires the optional \`pdf-parse\` dependency to be installed.
|
|
306
|
+
- The \`pages\` parameter is only valid for PDF files and will return an error for other file types.
|
|
307
|
+
|
|
308
|
+
# Limitations
|
|
309
|
+
|
|
310
|
+
This tool reads files only, not directories. To list directory contents, use a Bash tool call with \`ls\`.`,
|
|
130
311
|
inputSchema: inputSchema2,
|
|
131
312
|
execute: execute2,
|
|
132
313
|
isReadOnly: true,
|
|
@@ -134,67 +315,97 @@ var readTool = {
|
|
|
134
315
|
};
|
|
135
316
|
|
|
136
317
|
// src/edit.ts
|
|
137
|
-
var
|
|
138
|
-
var
|
|
318
|
+
var fs3 = __toESM(require("fs/promises"));
|
|
319
|
+
var path3 = __toESM(require("path"));
|
|
139
320
|
var import_zod3 = require("zod");
|
|
140
321
|
var inputSchema3 = import_zod3.z.object({
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
322
|
+
file_path: import_zod3.z.string().describe("Absolute or relative file path to edit"),
|
|
323
|
+
old_string: import_zod3.z.string().describe("Exact string to find and replace (must be unique in file unless replace_all is true)"),
|
|
324
|
+
new_string: import_zod3.z.string().describe("Replacement string"),
|
|
325
|
+
replace_all: import_zod3.z.boolean().optional().default(false).describe("Replace all occurrences of old_string (default false)")
|
|
144
326
|
});
|
|
145
327
|
async function execute3(input, ctx) {
|
|
146
328
|
if (ctx.abortSignal.aborted) return { content: "Aborted", isError: true };
|
|
147
|
-
const filePath =
|
|
148
|
-
if (!filePath.startsWith(ctx.workingDirectory +
|
|
149
|
-
return { content: `Error: path traversal denied \u2014 ${input.
|
|
329
|
+
const filePath = path3.resolve(ctx.workingDirectory, input.file_path);
|
|
330
|
+
if (!filePath.startsWith(ctx.workingDirectory + path3.sep) && filePath !== ctx.workingDirectory) {
|
|
331
|
+
return { content: `Error: path traversal denied \u2014 ${input.file_path} escapes working directory`, isError: true };
|
|
150
332
|
}
|
|
151
333
|
try {
|
|
152
|
-
const content = await
|
|
153
|
-
const occurrences = content.split(input.
|
|
334
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
335
|
+
const occurrences = content.split(input.old_string).length - 1;
|
|
154
336
|
if (occurrences === 0) {
|
|
155
|
-
return { content: "Error:
|
|
337
|
+
return { content: "Error: old_string not found in file", isError: true };
|
|
156
338
|
}
|
|
157
|
-
if (occurrences > 1) {
|
|
339
|
+
if (!input.replace_all && occurrences > 1) {
|
|
158
340
|
return {
|
|
159
|
-
content: `Error:
|
|
341
|
+
content: `Error: old_string found ${occurrences} times \u2014 must be unique. Provide more context to disambiguate, or use replace_all.`,
|
|
160
342
|
isError: true
|
|
161
343
|
};
|
|
162
344
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
345
|
+
let updated;
|
|
346
|
+
if (input.replace_all) {
|
|
347
|
+
updated = content.split(input.old_string).join(input.new_string);
|
|
348
|
+
} else {
|
|
349
|
+
updated = content.replace(input.old_string, input.new_string);
|
|
350
|
+
}
|
|
351
|
+
await fs3.writeFile(filePath, updated, "utf-8");
|
|
352
|
+
const replacedCount = input.replace_all ? occurrences : 1;
|
|
353
|
+
return { content: `Successfully edited ${filePath} (${replacedCount} replacement${replacedCount > 1 ? "s" : ""})` };
|
|
166
354
|
} catch (err) {
|
|
167
355
|
const msg = err instanceof Error ? err.message : String(err);
|
|
168
356
|
return { content: `Error editing file: ${msg}`, isError: true };
|
|
169
357
|
}
|
|
170
358
|
}
|
|
171
359
|
var editTool = {
|
|
172
|
-
name: "
|
|
173
|
-
description:
|
|
360
|
+
name: "Edit",
|
|
361
|
+
description: `Performs exact string replacements in files.
|
|
362
|
+
|
|
363
|
+
# Reading before editing
|
|
364
|
+
|
|
365
|
+
You MUST use the Read tool at least once before editing a file. This tool will error if you attempt an edit on a file you have not read. Reading the file first ensures you match the exact content including indentation and whitespace.
|
|
366
|
+
|
|
367
|
+
# old_string must be unique
|
|
368
|
+
|
|
369
|
+
The edit will FAIL if \`old_string\` is not unique in the file. Either:
|
|
370
|
+
- Provide a larger string with more surrounding context to make it unique, or
|
|
371
|
+
- Use \`replace_all: true\` to change every instance of \`old_string\`.
|
|
372
|
+
|
|
373
|
+
# Preserve indentation
|
|
374
|
+
|
|
375
|
+
When editing text from Read tool output, preserve the exact indentation (tabs/spaces) as it appears after the line number prefix. The line number prefix format is: line number + tab. Never include any part of the line number prefix in \`old_string\` or \`new_string\`.
|
|
376
|
+
|
|
377
|
+
# Prefer Edit over Write
|
|
378
|
+
|
|
379
|
+
ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
380
|
+
|
|
381
|
+
# replace_all for global renaming
|
|
382
|
+
|
|
383
|
+
Use \`replace_all: true\` for replacing and renaming strings across the entire file \u2014 for example, renaming a variable or updating a repeated string pattern.`,
|
|
174
384
|
inputSchema: inputSchema3,
|
|
175
385
|
execute: execute3,
|
|
176
386
|
isReadOnly: false,
|
|
387
|
+
isDestructive: false,
|
|
177
388
|
requiresConfirmation: true,
|
|
178
389
|
timeout: 1e4
|
|
179
390
|
};
|
|
180
391
|
|
|
181
392
|
// src/write.ts
|
|
182
|
-
var
|
|
183
|
-
var
|
|
393
|
+
var fs4 = __toESM(require("fs/promises"));
|
|
394
|
+
var path4 = __toESM(require("path"));
|
|
184
395
|
var import_zod4 = require("zod");
|
|
185
396
|
var inputSchema4 = import_zod4.z.object({
|
|
186
|
-
|
|
397
|
+
file_path: import_zod4.z.string().describe("Absolute or relative file path to write"),
|
|
187
398
|
content: import_zod4.z.string().describe("Content to write to the file")
|
|
188
399
|
});
|
|
189
400
|
async function execute4(input, ctx) {
|
|
190
401
|
if (ctx.abortSignal.aborted) return { content: "Aborted", isError: true };
|
|
191
|
-
const filePath =
|
|
192
|
-
if (!filePath.startsWith(ctx.workingDirectory +
|
|
193
|
-
return { content: `Error: path traversal denied \u2014 ${input.
|
|
402
|
+
const filePath = path4.resolve(ctx.workingDirectory, input.file_path);
|
|
403
|
+
if (!filePath.startsWith(ctx.workingDirectory + path4.sep) && filePath !== ctx.workingDirectory) {
|
|
404
|
+
return { content: `Error: path traversal denied \u2014 ${input.file_path} escapes working directory`, isError: true };
|
|
194
405
|
}
|
|
195
406
|
try {
|
|
196
|
-
await
|
|
197
|
-
await
|
|
407
|
+
await fs4.mkdir(path4.dirname(filePath), { recursive: true });
|
|
408
|
+
await fs4.writeFile(filePath, input.content, "utf-8");
|
|
198
409
|
return {
|
|
199
410
|
content: `Successfully wrote ${Buffer.byteLength(input.content)} bytes to ${filePath}`
|
|
200
411
|
};
|
|
@@ -204,26 +415,41 @@ async function execute4(input, ctx) {
|
|
|
204
415
|
}
|
|
205
416
|
}
|
|
206
417
|
var writeTool = {
|
|
207
|
-
name: "
|
|
208
|
-
description:
|
|
418
|
+
name: "Write",
|
|
419
|
+
description: `Writes a file to the local filesystem. Creates parent directories automatically if they do not exist.
|
|
420
|
+
|
|
421
|
+
# Overwrite behavior
|
|
422
|
+
|
|
423
|
+
This tool will overwrite the existing file if there is one at the provided path. Always read the file first with the Read tool before overwriting an existing file to avoid accidentally discarding content.
|
|
424
|
+
|
|
425
|
+
# Prefer Edit over Write for existing files
|
|
426
|
+
|
|
427
|
+
ALWAYS prefer using Edit to modify existing files \u2014 it only sends the diff and makes changes easier to review. Only use Write to create brand new files or for complete rewrites where you intend to replace the entire contents.
|
|
428
|
+
|
|
429
|
+
# Avoid unnecessary files
|
|
430
|
+
|
|
431
|
+
NEVER create documentation files (*.md) or README files unless explicitly requested. Do not create new files when editing an existing file would accomplish the same goal.`,
|
|
209
432
|
inputSchema: inputSchema4,
|
|
210
433
|
execute: execute4,
|
|
211
434
|
isReadOnly: false,
|
|
435
|
+
isDestructive: false,
|
|
212
436
|
requiresConfirmation: true,
|
|
213
437
|
timeout: 1e4
|
|
214
438
|
};
|
|
215
439
|
|
|
216
440
|
// src/glob.ts
|
|
441
|
+
var fs5 = __toESM(require("fs/promises"));
|
|
442
|
+
var path5 = __toESM(require("path"));
|
|
217
443
|
var import_fast_glob = __toESM(require("fast-glob"));
|
|
218
444
|
var import_zod5 = require("zod");
|
|
219
445
|
var MAX_RESULT_SIZE3 = 1e5;
|
|
220
446
|
var inputSchema5 = import_zod5.z.object({
|
|
221
447
|
pattern: import_zod5.z.string().describe("Glob pattern to match files (e.g. **/*.ts)"),
|
|
222
|
-
|
|
448
|
+
path: import_zod5.z.string().optional().describe("Directory to search in")
|
|
223
449
|
});
|
|
224
450
|
async function execute5(input, ctx) {
|
|
225
451
|
if (ctx.abortSignal.aborted) return { content: "Aborted", isError: true };
|
|
226
|
-
const cwd = input.
|
|
452
|
+
const cwd = input.path ?? ctx.workingDirectory;
|
|
227
453
|
try {
|
|
228
454
|
const files = await (0, import_fast_glob.default)(input.pattern, {
|
|
229
455
|
cwd,
|
|
@@ -232,20 +458,57 @@ async function execute5(input, ctx) {
|
|
|
232
458
|
onlyFiles: true,
|
|
233
459
|
absolute: false
|
|
234
460
|
});
|
|
235
|
-
|
|
236
|
-
|
|
461
|
+
const withStats = await Promise.all(
|
|
462
|
+
files.map(async (f) => {
|
|
463
|
+
try {
|
|
464
|
+
const stat3 = await fs5.stat(path5.resolve(cwd, f));
|
|
465
|
+
return { file: f, mtime: stat3.mtimeMs };
|
|
466
|
+
} catch {
|
|
467
|
+
return { file: f, mtime: 0 };
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
);
|
|
471
|
+
withStats.sort((a, b) => b.mtime - a.mtime);
|
|
472
|
+
const sorted = withStats.map((s) => s.file);
|
|
473
|
+
if (sorted.length === 0) {
|
|
237
474
|
return { content: "No files matched the pattern" };
|
|
238
475
|
}
|
|
239
|
-
const content =
|
|
240
|
-
return { content, metadata: { matchCount:
|
|
476
|
+
const content = sorted.join("\n").slice(0, MAX_RESULT_SIZE3);
|
|
477
|
+
return { content, metadata: { matchCount: sorted.length } };
|
|
241
478
|
} catch (err) {
|
|
242
479
|
const msg = err instanceof Error ? err.message : String(err);
|
|
243
480
|
return { content: `Error searching files: ${msg}`, isError: true };
|
|
244
481
|
}
|
|
245
482
|
}
|
|
246
483
|
var globTool = {
|
|
247
|
-
name: "
|
|
248
|
-
description:
|
|
484
|
+
name: "Glob",
|
|
485
|
+
description: `Fast file pattern matching tool that works with any codebase size.
|
|
486
|
+
|
|
487
|
+
# Glob patterns
|
|
488
|
+
|
|
489
|
+
Supports standard glob syntax. Examples:
|
|
490
|
+
- "**/*.js" \u2014 all JavaScript files recursively
|
|
491
|
+
- "src/**/*.ts" \u2014 all TypeScript files under src/
|
|
492
|
+
- "packages/*/src/index.ts" \u2014 index files in each package
|
|
493
|
+
|
|
494
|
+
# Result ordering
|
|
495
|
+
|
|
496
|
+
Returns matching file paths sorted by modification time (most recently modified first).
|
|
497
|
+
|
|
498
|
+
# When to use Glob vs other tools
|
|
499
|
+
|
|
500
|
+
- Finding files by name pattern: use Glob.
|
|
501
|
+
- Searching file contents for a string or regex: use Grep instead.
|
|
502
|
+
- Reading a specific file you already know the path to: use Read instead.
|
|
503
|
+
- For open-ended searches that require multiple rounds of globbing and grepping, chain multiple tool calls.
|
|
504
|
+
|
|
505
|
+
# Exclusions
|
|
506
|
+
|
|
507
|
+
node_modules and .git directories are automatically excluded from results.
|
|
508
|
+
|
|
509
|
+
# Search scope
|
|
510
|
+
|
|
511
|
+
The \`path\` parameter sets the root directory to search in. If omitted, the agent's working directory is used.`,
|
|
249
512
|
inputSchema: inputSchema5,
|
|
250
513
|
execute: execute5,
|
|
251
514
|
isReadOnly: true,
|
|
@@ -253,68 +516,285 @@ var globTool = {
|
|
|
253
516
|
};
|
|
254
517
|
|
|
255
518
|
// src/grep.ts
|
|
256
|
-
var
|
|
257
|
-
var
|
|
519
|
+
var fs6 = __toESM(require("fs/promises"));
|
|
520
|
+
var path6 = __toESM(require("path"));
|
|
258
521
|
var import_zod6 = require("zod");
|
|
259
522
|
var import_fast_glob2 = __toESM(require("fast-glob"));
|
|
260
|
-
var MAX_RESULT_SIZE4 = 1e5;
|
|
261
523
|
var MAX_FILES = 5e3;
|
|
524
|
+
var DEFAULT_HEAD_LIMIT = 250;
|
|
525
|
+
var FILE_TYPE_MAP = {
|
|
526
|
+
js: [".js", ".mjs", ".cjs", ".jsx"],
|
|
527
|
+
ts: [".ts", ".tsx", ".mts", ".cts"],
|
|
528
|
+
py: [".py", ".pyi"],
|
|
529
|
+
rust: [".rs"],
|
|
530
|
+
go: [".go"],
|
|
531
|
+
java: [".java"],
|
|
532
|
+
c: [".c", ".h"],
|
|
533
|
+
cpp: [".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx", ".h"],
|
|
534
|
+
cs: [".cs"],
|
|
535
|
+
rb: [".rb"],
|
|
536
|
+
php: [".php"],
|
|
537
|
+
swift: [".swift"],
|
|
538
|
+
kt: [".kt", ".kts"],
|
|
539
|
+
scala: [".scala"],
|
|
540
|
+
html: [".html", ".htm"],
|
|
541
|
+
css: [".css"],
|
|
542
|
+
scss: [".scss"],
|
|
543
|
+
less: [".less"],
|
|
544
|
+
json: [".json"],
|
|
545
|
+
yaml: [".yml", ".yaml"],
|
|
546
|
+
toml: [".toml"],
|
|
547
|
+
xml: [".xml"],
|
|
548
|
+
md: [".md", ".markdown"],
|
|
549
|
+
sh: [".sh", ".bash", ".zsh"],
|
|
550
|
+
sql: [".sql"],
|
|
551
|
+
graphql: [".graphql", ".gql"],
|
|
552
|
+
proto: [".proto"],
|
|
553
|
+
lua: [".lua"],
|
|
554
|
+
r: [".r", ".R"],
|
|
555
|
+
dart: [".dart"],
|
|
556
|
+
ex: [".ex", ".exs"],
|
|
557
|
+
zig: [".zig"],
|
|
558
|
+
vue: [".vue"],
|
|
559
|
+
svelte: [".svelte"],
|
|
560
|
+
astro: [".astro"],
|
|
561
|
+
txt: [".txt"]
|
|
562
|
+
};
|
|
262
563
|
var inputSchema6 = import_zod6.z.object({
|
|
263
564
|
pattern: import_zod6.z.string().describe("Regex pattern to search for in file contents"),
|
|
264
|
-
path: import_zod6.z.string().optional().describe("
|
|
265
|
-
glob: import_zod6.z.string().optional().describe(
|
|
565
|
+
path: import_zod6.z.string().optional().describe("File or directory to search in"),
|
|
566
|
+
glob: import_zod6.z.string().optional().describe('Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}")'),
|
|
567
|
+
output_mode: import_zod6.z.enum(["content", "files_with_matches", "count"]).optional().default("files_with_matches").describe("Output mode: content (matching lines), files_with_matches (file paths), count (match counts)"),
|
|
568
|
+
"-A": import_zod6.z.number().optional().describe("Lines after each match (requires output_mode: content)"),
|
|
569
|
+
"-B": import_zod6.z.number().optional().describe("Lines before each match (requires output_mode: content)"),
|
|
570
|
+
"-C": import_zod6.z.number().optional().describe("Alias for context"),
|
|
571
|
+
context: import_zod6.z.number().optional().describe("Lines before and after each match (requires output_mode: content)"),
|
|
572
|
+
head_limit: import_zod6.z.number().optional().default(DEFAULT_HEAD_LIMIT).describe("Limit output entries. Defaults to 250."),
|
|
573
|
+
offset: import_zod6.z.number().optional().default(0).describe("Skip first N entries before applying head_limit"),
|
|
574
|
+
multiline: import_zod6.z.boolean().optional().default(false).describe("Enable multiline mode (dotAll flag, patterns can span lines)"),
|
|
575
|
+
type: import_zod6.z.string().optional().describe("File type filter (js, ts, py, etc.)"),
|
|
576
|
+
"-i": import_zod6.z.boolean().optional().describe("Case insensitive search"),
|
|
577
|
+
"-n": import_zod6.z.boolean().optional().default(true).describe("Show line numbers (content mode only). Defaults to true.")
|
|
266
578
|
});
|
|
579
|
+
function offsetToLine(offsets, charOffset) {
|
|
580
|
+
let lo = 0;
|
|
581
|
+
let hi = offsets.length - 1;
|
|
582
|
+
while (lo < hi) {
|
|
583
|
+
const mid = lo + hi + 1 >>> 1;
|
|
584
|
+
if (offsets[mid] <= charOffset) lo = mid;
|
|
585
|
+
else hi = mid - 1;
|
|
586
|
+
}
|
|
587
|
+
return lo;
|
|
588
|
+
}
|
|
589
|
+
function findMatchRanges(lines, fullContent, regex, multiline) {
|
|
590
|
+
const ranges = [];
|
|
591
|
+
if (multiline) {
|
|
592
|
+
const lineOffsets = [];
|
|
593
|
+
let offset = 0;
|
|
594
|
+
for (const line of lines) {
|
|
595
|
+
lineOffsets.push(offset);
|
|
596
|
+
offset += line.length + 1;
|
|
597
|
+
}
|
|
598
|
+
const globalRegex = new RegExp(regex.source, `${regex.flags.replace("g", "")}g`);
|
|
599
|
+
let match;
|
|
600
|
+
while ((match = globalRegex.exec(fullContent)) !== null) {
|
|
601
|
+
const startChar = match.index;
|
|
602
|
+
const endChar = startChar + match[0].length - 1;
|
|
603
|
+
const startLine = offsetToLine(lineOffsets, startChar);
|
|
604
|
+
const endLine = offsetToLine(lineOffsets, endChar);
|
|
605
|
+
ranges.push({ lineStart: startLine, lineEnd: endLine });
|
|
606
|
+
if (match[0].length === 0) {
|
|
607
|
+
globalRegex.lastIndex++;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
for (let i = 0; i < lines.length; i++) {
|
|
612
|
+
if (regex.test(lines[i])) {
|
|
613
|
+
ranges.push({ lineStart: i, lineEnd: i });
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return ranges;
|
|
618
|
+
}
|
|
619
|
+
function buildContextBlocks(lines, ranges, beforeCtx, afterCtx, showLineNumbers, relPath) {
|
|
620
|
+
if (ranges.length === 0) return [];
|
|
621
|
+
const blocks = [];
|
|
622
|
+
for (const range of ranges) {
|
|
623
|
+
const start = Math.max(0, range.lineStart - beforeCtx);
|
|
624
|
+
const end = Math.min(lines.length - 1, range.lineEnd + afterCtx);
|
|
625
|
+
const matchLines = /* @__PURE__ */ new Set();
|
|
626
|
+
for (let i = range.lineStart; i <= range.lineEnd; i++) {
|
|
627
|
+
matchLines.add(i);
|
|
628
|
+
}
|
|
629
|
+
const prev = blocks[blocks.length - 1];
|
|
630
|
+
if (prev && start <= prev.end + 1) {
|
|
631
|
+
prev.end = Math.max(prev.end, end);
|
|
632
|
+
for (const ml of matchLines) prev.matchLines.add(ml);
|
|
633
|
+
} else {
|
|
634
|
+
blocks.push({ start, end, matchLines });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const output = [];
|
|
638
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
639
|
+
if (bi > 0) output.push("--");
|
|
640
|
+
const block = blocks[bi];
|
|
641
|
+
for (let i = block.start; i <= block.end; i++) {
|
|
642
|
+
const separator = block.matchLines.has(i) ? ":" : "-";
|
|
643
|
+
if (showLineNumbers) {
|
|
644
|
+
output.push(`${relPath}${separator}${i + 1}${separator}${lines[i]}`);
|
|
645
|
+
} else {
|
|
646
|
+
output.push(`${relPath}${separator}${lines[i]}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return output;
|
|
651
|
+
}
|
|
652
|
+
function resolveTypeGlobs(fileType) {
|
|
653
|
+
const extensions = FILE_TYPE_MAP[fileType.toLowerCase()];
|
|
654
|
+
if (!extensions) return [];
|
|
655
|
+
return extensions.map((ext) => `**/*${ext}`);
|
|
656
|
+
}
|
|
657
|
+
function matchesType(filePath, fileType) {
|
|
658
|
+
const extensions = FILE_TYPE_MAP[fileType.toLowerCase()];
|
|
659
|
+
if (!extensions) return false;
|
|
660
|
+
const ext = path6.extname(filePath).toLowerCase();
|
|
661
|
+
return extensions.includes(ext);
|
|
662
|
+
}
|
|
267
663
|
async function execute6(input, ctx) {
|
|
268
664
|
if (ctx.abortSignal.aborted) return { content: "Aborted", isError: true };
|
|
269
|
-
const searchPath = input.path ?
|
|
270
|
-
const
|
|
665
|
+
const searchPath = input.path ? path6.resolve(ctx.workingDirectory, input.path) : ctx.workingDirectory;
|
|
666
|
+
const outputMode = input.output_mode ?? "files_with_matches";
|
|
667
|
+
const headLimit = input.head_limit ?? DEFAULT_HEAD_LIMIT;
|
|
668
|
+
const offsetSkip = input.offset ?? 0;
|
|
669
|
+
const isMultiline = input.multiline ?? false;
|
|
670
|
+
const caseInsensitive = input["-i"] ?? false;
|
|
671
|
+
const showLineNumbers = input["-n"] ?? true;
|
|
672
|
+
const contextVal = input["-C"] ?? input.context ?? 0;
|
|
673
|
+
const beforeCtx = input["-B"] ?? contextVal;
|
|
674
|
+
const afterCtx = input["-A"] ?? contextVal;
|
|
271
675
|
try {
|
|
272
|
-
|
|
273
|
-
|
|
676
|
+
let flags = "";
|
|
677
|
+
if (isMultiline) flags += "s";
|
|
678
|
+
if (caseInsensitive) flags += "i";
|
|
679
|
+
const regex = new RegExp(input.pattern, flags);
|
|
680
|
+
const stat3 = await fs6.stat(searchPath);
|
|
274
681
|
let files;
|
|
275
|
-
if (
|
|
682
|
+
if (stat3.isFile()) {
|
|
276
683
|
files = [searchPath];
|
|
277
684
|
} else {
|
|
278
|
-
|
|
685
|
+
let globPatterns;
|
|
686
|
+
if (input.type) {
|
|
687
|
+
const typeGlobs = resolveTypeGlobs(input.type);
|
|
688
|
+
if (typeGlobs.length === 0) {
|
|
689
|
+
return { content: `Unknown file type: ${input.type}`, isError: true };
|
|
690
|
+
}
|
|
691
|
+
if (input.glob) {
|
|
692
|
+
globPatterns = typeGlobs;
|
|
693
|
+
} else {
|
|
694
|
+
globPatterns = typeGlobs;
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
globPatterns = [input.glob ?? "**/*"];
|
|
698
|
+
}
|
|
699
|
+
files = await (0, import_fast_glob2.default)(globPatterns, {
|
|
279
700
|
cwd: searchPath,
|
|
280
701
|
absolute: true,
|
|
281
702
|
onlyFiles: true,
|
|
282
703
|
ignore: ["**/node_modules/**", "**/.git/**", "**/*.min.*"]
|
|
283
704
|
});
|
|
705
|
+
if (input.type && input.glob) {
|
|
706
|
+
const globFilter = await (0, import_fast_glob2.default)([input.glob], {
|
|
707
|
+
cwd: searchPath,
|
|
708
|
+
absolute: true,
|
|
709
|
+
onlyFiles: true,
|
|
710
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/*.min.*"]
|
|
711
|
+
});
|
|
712
|
+
const globSet = new Set(globFilter);
|
|
713
|
+
files = files.filter((f) => globSet.has(f));
|
|
714
|
+
}
|
|
715
|
+
files.sort();
|
|
284
716
|
files = files.slice(0, MAX_FILES);
|
|
285
717
|
}
|
|
286
|
-
const
|
|
287
|
-
let
|
|
718
|
+
const entries = [];
|
|
719
|
+
let entryIndex = 0;
|
|
720
|
+
const endIndex = headLimit > 0 ? offsetSkip + headLimit : Number.MAX_SAFE_INTEGER;
|
|
288
721
|
for (const file of files) {
|
|
289
722
|
if (ctx.abortSignal.aborted) break;
|
|
723
|
+
if (entryIndex >= endIndex) break;
|
|
724
|
+
if (input.type && stat3.isFile() && !matchesType(file, input.type)) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
290
727
|
try {
|
|
291
|
-
const content = await
|
|
728
|
+
const content = await fs6.readFile(file, "utf-8");
|
|
292
729
|
const lines = content.split("\n");
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
730
|
+
const relPath = path6.relative(ctx.workingDirectory, file);
|
|
731
|
+
const matchRanges = findMatchRanges(lines, content, regex, isMultiline);
|
|
732
|
+
if (matchRanges.length === 0) continue;
|
|
733
|
+
if (outputMode === "files_with_matches") {
|
|
734
|
+
if (entryIndex >= offsetSkip && entryIndex < endIndex) {
|
|
735
|
+
entries.push(relPath);
|
|
736
|
+
}
|
|
737
|
+
entryIndex++;
|
|
738
|
+
} else if (outputMode === "count") {
|
|
739
|
+
if (entryIndex >= offsetSkip && entryIndex < endIndex) {
|
|
740
|
+
entries.push(`${relPath}:${matchRanges.length}`);
|
|
741
|
+
}
|
|
742
|
+
entryIndex++;
|
|
743
|
+
} else {
|
|
744
|
+
const hasContext = beforeCtx > 0 || afterCtx > 0;
|
|
745
|
+
if (hasContext) {
|
|
746
|
+
const blockLines = buildContextBlocks(lines, matchRanges, beforeCtx, afterCtx, showLineNumbers, relPath);
|
|
747
|
+
for (const line of blockLines) {
|
|
748
|
+
if (entryIndex >= offsetSkip && entryIndex < endIndex) {
|
|
749
|
+
entries.push(line);
|
|
750
|
+
}
|
|
751
|
+
if (line !== "--") {
|
|
752
|
+
entryIndex++;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
for (const range of matchRanges) {
|
|
757
|
+
for (let li = range.lineStart; li <= range.lineEnd; li++) {
|
|
758
|
+
if (entryIndex >= offsetSkip && entryIndex < endIndex) {
|
|
759
|
+
if (showLineNumbers) {
|
|
760
|
+
entries.push(`${relPath}:${li + 1}:${lines[li]}`);
|
|
761
|
+
} else {
|
|
762
|
+
entries.push(`${relPath}:${lines[li]}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
entryIndex++;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
300
768
|
}
|
|
301
769
|
}
|
|
302
770
|
} catch {
|
|
303
771
|
}
|
|
304
|
-
if (totalSize > MAX_RESULT_SIZE4) break;
|
|
305
772
|
}
|
|
306
|
-
if (
|
|
773
|
+
if (entries.length === 0) {
|
|
307
774
|
return { content: "No matches found" };
|
|
308
775
|
}
|
|
309
|
-
return {
|
|
776
|
+
return {
|
|
777
|
+
content: entries.join("\n"),
|
|
778
|
+
metadata: { matchCount: entries.filter((e) => e !== "--").length }
|
|
779
|
+
};
|
|
310
780
|
} catch (err) {
|
|
311
781
|
const msg = err instanceof Error ? err.message : String(err);
|
|
312
782
|
return { content: `Error searching: ${msg}`, isError: true };
|
|
313
783
|
}
|
|
314
784
|
}
|
|
315
785
|
var grepTool = {
|
|
316
|
-
name: "
|
|
317
|
-
description:
|
|
786
|
+
name: "Grep",
|
|
787
|
+
description: `A powerful search tool built on ripgrep
|
|
788
|
+
|
|
789
|
+
Usage:
|
|
790
|
+
- ALWAYS use Grep for search tasks. NEVER invoke \`grep\` or \`rg\` as a Bash command. The Grep tool has been optimized for correct permissions and access.
|
|
791
|
+
- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
|
|
792
|
+
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
|
|
793
|
+
- Output modes: "content" shows matching lines (supports -A/-B/-C context, -n line numbers, head_limit), "files_with_matches" shows file paths (supports head_limit), "count" shows match counts (supports head_limit). Defaults to "files_with_matches".
|
|
794
|
+
- Use Agent tool for open-ended searches requiring multiple rounds
|
|
795
|
+
- Pattern syntax: Uses ripgrep conventions \u2014 literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)
|
|
796
|
+
- Multiline matching: By default patterns match within single lines only. For cross-line patterns like \`struct \\{[\\s\\S]*?field\`, use \`multiline: true\`
|
|
797
|
+
`,
|
|
318
798
|
inputSchema: inputSchema6,
|
|
319
799
|
execute: execute6,
|
|
320
800
|
isReadOnly: true,
|
|
@@ -323,7 +803,8 @@ var grepTool = {
|
|
|
323
803
|
|
|
324
804
|
// src/web-fetch.ts
|
|
325
805
|
var import_zod7 = require("zod");
|
|
326
|
-
var
|
|
806
|
+
var MAX_RESULT_SIZE4 = 5e4;
|
|
807
|
+
var CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
327
808
|
function isPrivateUrl(urlStr) {
|
|
328
809
|
const url = new URL(urlStr);
|
|
329
810
|
const hostname = url.hostname;
|
|
@@ -342,53 +823,1225 @@ function isPrivateUrl(urlStr) {
|
|
|
342
823
|
];
|
|
343
824
|
return blocked.some((re) => re.test(hostname));
|
|
344
825
|
}
|
|
826
|
+
var NAMED_ENTITIES = {
|
|
827
|
+
amp: "&",
|
|
828
|
+
lt: "<",
|
|
829
|
+
gt: ">",
|
|
830
|
+
quot: '"',
|
|
831
|
+
apos: "'",
|
|
832
|
+
nbsp: "\xA0",
|
|
833
|
+
mdash: "\u2014",
|
|
834
|
+
ndash: "\u2013",
|
|
835
|
+
laquo: "\xAB",
|
|
836
|
+
raquo: "\xBB",
|
|
837
|
+
copy: "\xA9",
|
|
838
|
+
reg: "\xAE",
|
|
839
|
+
trade: "\u2122",
|
|
840
|
+
hellip: "\u2026"
|
|
841
|
+
};
|
|
842
|
+
function decodeHtmlEntities(text) {
|
|
843
|
+
return text.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(parseInt(dec, 10))).replace(/&([a-zA-Z]+);/g, (full, name) => NAMED_ENTITIES[name] ?? full);
|
|
844
|
+
}
|
|
845
|
+
function htmlToMarkdown(html) {
|
|
846
|
+
let md = html;
|
|
847
|
+
md = md.replace(/<script[\s\S]*?<\/script>/gi, "");
|
|
848
|
+
md = md.replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
849
|
+
for (let i = 1; i <= 6; i++) {
|
|
850
|
+
const prefix = "#".repeat(i);
|
|
851
|
+
const re = new RegExp(`<h${i}[^>]*>([\\s\\S]*?)<\\/h${i}>`, "gi");
|
|
852
|
+
md = md.replace(re, (_, inner) => `
|
|
853
|
+
|
|
854
|
+
${prefix} ${inner.trim()}
|
|
855
|
+
|
|
856
|
+
`);
|
|
857
|
+
}
|
|
858
|
+
md = md.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, inner) => `
|
|
859
|
+
|
|
860
|
+
\`\`\`
|
|
861
|
+
${decodeHtmlEntities(inner.replace(/<[^>]*>/g, "").trim())}
|
|
862
|
+
\`\`\`
|
|
863
|
+
|
|
864
|
+
`);
|
|
865
|
+
md = md.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_, inner) => `
|
|
866
|
+
|
|
867
|
+
\`\`\`
|
|
868
|
+
${decodeHtmlEntities(inner.replace(/<[^>]*>/g, "").trim())}
|
|
869
|
+
\`\`\`
|
|
870
|
+
|
|
871
|
+
`);
|
|
872
|
+
md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_, inner) => `\`${inner.replace(/<[^>]*>/g, "").trim()}\``);
|
|
873
|
+
md = md.replace(/<(?:strong|b)\b[^>]*>([\s\S]*?)<\/(?:strong|b)>/gi, (_, inner) => `**${inner.trim()}**`);
|
|
874
|
+
md = md.replace(/<(?:em|i)\b[^>]*>([\s\S]*?)<\/(?:em|i)>/gi, (_, inner) => `*${inner.trim()}*`);
|
|
875
|
+
md = md.replace(/<a[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => `[${text.replace(/<[^>]*>/g, "").trim()}](${href})`);
|
|
876
|
+
md = md.replace(/<img[^>]+alt="([^"]*)"[^>]*src="([^"]*)"[^>]*\/?>/gi, (_, alt, src) => ``);
|
|
877
|
+
md = md.replace(/<img[^>]+src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, (_, src, alt) => ``);
|
|
878
|
+
md = md.replace(/<img[^>]+src="([^"]*)"[^>]*\/?>/gi, (_, src) => ``);
|
|
879
|
+
md = md.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, inner) => `- ${inner.replace(/<[^>]*>/g, "").trim()}
|
|
880
|
+
`);
|
|
881
|
+
md = md.replace(/<br\s*\/?>/gi, "\n");
|
|
882
|
+
md = md.replace(/<\/p>/gi, "\n\n");
|
|
883
|
+
md = md.replace(/<\/div>/gi, "\n\n");
|
|
884
|
+
md = md.replace(/<\/blockquote>/gi, "\n\n");
|
|
885
|
+
md = md.replace(/<hr\s*\/?>/gi, "\n\n---\n\n");
|
|
886
|
+
md = md.replace(/<[^>]*>/g, "");
|
|
887
|
+
md = decodeHtmlEntities(md);
|
|
888
|
+
md = md.replace(/[ \t]+$/gm, "");
|
|
889
|
+
md = md.replace(/\n{3,}/g, "\n\n");
|
|
890
|
+
md = md.trim();
|
|
891
|
+
return md;
|
|
892
|
+
}
|
|
893
|
+
function upgradeToHttps(url) {
|
|
894
|
+
if (url.startsWith("http://")) {
|
|
895
|
+
return `https://${url.slice(7)}`;
|
|
896
|
+
}
|
|
897
|
+
return url;
|
|
898
|
+
}
|
|
899
|
+
var cache = /* @__PURE__ */ new Map();
|
|
900
|
+
function getCached(url) {
|
|
901
|
+
const entry = cache.get(url);
|
|
902
|
+
if (!entry) return void 0;
|
|
903
|
+
if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
|
|
904
|
+
cache.delete(url);
|
|
905
|
+
return void 0;
|
|
906
|
+
}
|
|
907
|
+
return entry;
|
|
908
|
+
}
|
|
909
|
+
function setCache(url, result) {
|
|
910
|
+
cache.set(url, {
|
|
911
|
+
content: result.content,
|
|
912
|
+
isError: result.isError,
|
|
913
|
+
metadata: result.metadata,
|
|
914
|
+
timestamp: Date.now()
|
|
915
|
+
});
|
|
916
|
+
}
|
|
345
917
|
var inputSchema7 = import_zod7.z.object({
|
|
346
918
|
url: import_zod7.z.string().url().describe("URL to fetch"),
|
|
347
919
|
method: import_zod7.z.string().optional().default("GET").describe("HTTP method"),
|
|
348
920
|
headers: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional().describe("HTTP headers"),
|
|
349
|
-
body: import_zod7.z.string().optional().describe("Request body")
|
|
921
|
+
body: import_zod7.z.string().optional().describe("Request body"),
|
|
922
|
+
prompt: import_zod7.z.string().optional().describe("Instructions for processing the fetched content")
|
|
350
923
|
});
|
|
351
924
|
async function execute7(input, ctx) {
|
|
352
925
|
if (ctx.abortSignal.aborted) return { content: "Aborted", isError: true };
|
|
926
|
+
const url = upgradeToHttps(input.url);
|
|
353
927
|
try {
|
|
354
|
-
if (isPrivateUrl(
|
|
355
|
-
return { content: `Error: request to private/internal address denied \u2014 ${
|
|
928
|
+
if (isPrivateUrl(url)) {
|
|
929
|
+
return { content: `Error: request to private/internal address denied \u2014 ${url}`, isError: true };
|
|
356
930
|
}
|
|
357
931
|
} catch {
|
|
358
|
-
return { content: `Error: invalid URL \u2014 ${
|
|
932
|
+
return { content: `Error: invalid URL \u2014 ${url}`, isError: true };
|
|
933
|
+
}
|
|
934
|
+
if ((!input.method || input.method === "GET") && !input.body) {
|
|
935
|
+
const cached = getCached(url);
|
|
936
|
+
if (cached) {
|
|
937
|
+
const promptPrefix = input.prompt ? `[Prompt: ${input.prompt}]
|
|
938
|
+
|
|
939
|
+
` : "";
|
|
940
|
+
return {
|
|
941
|
+
content: `${promptPrefix}[Cached] ${cached.content}`,
|
|
942
|
+
isError: cached.isError,
|
|
943
|
+
metadata: { ...cached.metadata, cached: true }
|
|
944
|
+
};
|
|
945
|
+
}
|
|
359
946
|
}
|
|
360
947
|
try {
|
|
361
|
-
const res = await fetch(
|
|
948
|
+
const res = await fetch(url, {
|
|
362
949
|
method: input.method,
|
|
363
950
|
headers: input.headers,
|
|
364
951
|
body: input.body,
|
|
365
952
|
signal: ctx.abortSignal
|
|
366
953
|
});
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
954
|
+
let text = await res.text();
|
|
955
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
956
|
+
if (contentType.includes("text/html")) {
|
|
957
|
+
text = htmlToMarkdown(text);
|
|
958
|
+
}
|
|
959
|
+
const truncated = text.slice(0, MAX_RESULT_SIZE4);
|
|
960
|
+
const suffix = text.length > MAX_RESULT_SIZE4 ? "\n...(truncated)" : "";
|
|
961
|
+
const rawContent = `HTTP ${res.status} ${res.statusText}
|
|
371
962
|
|
|
372
963
|
${truncated}${suffix}`;
|
|
373
|
-
|
|
374
|
-
content,
|
|
964
|
+
const result = {
|
|
965
|
+
content: rawContent,
|
|
375
966
|
isError: res.status >= 400,
|
|
376
967
|
metadata: { status: res.status, headers: Object.fromEntries(res.headers.entries()) }
|
|
377
968
|
};
|
|
969
|
+
if ((!input.method || input.method === "GET") && !input.body) {
|
|
970
|
+
setCache(url, result);
|
|
971
|
+
}
|
|
972
|
+
const promptPrefix = input.prompt ? `[Prompt: ${input.prompt}]
|
|
973
|
+
|
|
974
|
+
` : "";
|
|
975
|
+
return {
|
|
976
|
+
...result,
|
|
977
|
+
content: `${promptPrefix}${rawContent}`
|
|
978
|
+
};
|
|
378
979
|
} catch (err) {
|
|
379
980
|
const msg = err instanceof Error ? err.message : String(err);
|
|
380
981
|
return { content: `Fetch error: ${msg}`, isError: true };
|
|
381
982
|
}
|
|
382
983
|
}
|
|
383
984
|
var webFetchTool = {
|
|
384
|
-
name: "
|
|
385
|
-
description:
|
|
985
|
+
name: "WebFetch",
|
|
986
|
+
description: `Fetches content from a specified URL and returns the response body.
|
|
987
|
+
|
|
988
|
+
IMPORTANT: This tool WILL FAIL for authenticated or private URLs (e.g. pages behind login, internal services). Do not use it for those cases.
|
|
989
|
+
|
|
990
|
+
Usage notes:
|
|
991
|
+
- The URL must be a fully-formed, valid URL pointing to a publicly accessible resource
|
|
992
|
+
- HTML responses (Content-Type: text/html) are automatically converted to Markdown for easier reading
|
|
993
|
+
- HTTP URLs are automatically upgraded to HTTPS
|
|
994
|
+
- Successful GET responses are cached in memory for 15 minutes; cached responses are marked with [Cached]
|
|
995
|
+
- Use the prompt parameter to describe what information you want to extract from the page; the raw response body is returned along with the prompt prefix so you can process it yourself
|
|
996
|
+
- Requests to private/internal network addresses are blocked (localhost, 10.x, 172.16-31.x, 192.168.x, link-local, cloud metadata endpoints) to prevent SSRF attacks
|
|
997
|
+
- Response bodies are capped at ${MAX_RESULT_SIZE4.toLocaleString()} characters; larger responses are truncated
|
|
998
|
+
- HTTP 4xx/5xx responses are returned with isError=true so you can detect failures
|
|
999
|
+
- For GitHub URLs, prefer using the gh CLI via Bash instead (e.g., gh pr view, gh issue view, gh api)
|
|
1000
|
+
`,
|
|
386
1001
|
inputSchema: inputSchema7,
|
|
387
1002
|
execute: execute7,
|
|
388
1003
|
isReadOnly: false,
|
|
1004
|
+
requiresConfirmation: true,
|
|
1005
|
+
timeout: 3e4
|
|
1006
|
+
};
|
|
1007
|
+
|
|
1008
|
+
// src/web-search.ts
|
|
1009
|
+
var import_zod8 = require("zod");
|
|
1010
|
+
var MAX_RESULTS_LIMIT = 20;
|
|
1011
|
+
var DEFAULT_MAX_RESULTS = 5;
|
|
1012
|
+
var MAX_RESPONSE_SIZE = 2e5;
|
|
1013
|
+
var STRUCTURE_WARNING_THRESHOLD = 5e3;
|
|
1014
|
+
var USER_AGENT = "Mozilla/5.0 (compatible; ClaudeCodeKit/1.0; +https://github.com/minnzen/claude-code-kit)";
|
|
1015
|
+
var inputSchema8 = import_zod8.z.object({
|
|
1016
|
+
query: import_zod8.z.string().min(1).describe("Search query"),
|
|
1017
|
+
max_results: import_zod8.z.number().int().min(1).max(MAX_RESULTS_LIMIT).optional().default(DEFAULT_MAX_RESULTS).describe("Maximum number of results to return (default 5, max 20)"),
|
|
1018
|
+
allowed_domains: import_zod8.z.array(import_zod8.z.string()).optional().describe("Only include results from these domains (hostname endsWith check)"),
|
|
1019
|
+
blocked_domains: import_zod8.z.array(import_zod8.z.string()).optional().describe("Exclude results from these domains (hostname endsWith check)")
|
|
1020
|
+
});
|
|
1021
|
+
function parseSearchResults(html, maxResults) {
|
|
1022
|
+
const results = [];
|
|
1023
|
+
const resultBlockRegex = /<div[^>]*class="[^"]*result\b[^"]*"[^>]*>([\s\S]*?)<\/div>\s*<\/div>/g;
|
|
1024
|
+
let blockMatch;
|
|
1025
|
+
while ((blockMatch = resultBlockRegex.exec(html)) !== null && results.length < maxResults) {
|
|
1026
|
+
const block = blockMatch[1];
|
|
1027
|
+
const titleMatch = block.match(/<a[^>]*class="result__a"[^>]*>([\s\S]*?)<\/a>/);
|
|
1028
|
+
if (!titleMatch) continue;
|
|
1029
|
+
const urlMatch = block.match(/<a[^>]*class="result__url"[^>]*href="([^"]*)"[^>]*>/);
|
|
1030
|
+
const urlFallback = block.match(/<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>/);
|
|
1031
|
+
const snippetMatch = block.match(/<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/);
|
|
1032
|
+
const rawUrl = urlMatch?.[1] || urlFallback?.[1] || "";
|
|
1033
|
+
const actualUrl = decodeRedirectUrl(rawUrl);
|
|
1034
|
+
const title = stripHtmlTags(titleMatch[1]).trim();
|
|
1035
|
+
const snippet = stripHtmlTags(snippetMatch?.[1] || "").trim();
|
|
1036
|
+
if (title && actualUrl) {
|
|
1037
|
+
results.push({ title, url: actualUrl, snippet });
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return results;
|
|
1041
|
+
}
|
|
1042
|
+
function stripHtmlTags(html) {
|
|
1043
|
+
return html.replace(/<[^>]*>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/ /g, " ").replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number.parseInt(dec, 10))).replace(/\s+/g, " ").trim();
|
|
1044
|
+
}
|
|
1045
|
+
function decodeRedirectUrl(url) {
|
|
1046
|
+
if (url.includes("duckduckgo.com/l/?")) {
|
|
1047
|
+
const match = url.match(/[?&]uddg=([^&]+)/);
|
|
1048
|
+
if (match) {
|
|
1049
|
+
try {
|
|
1050
|
+
return decodeURIComponent(match[1]);
|
|
1051
|
+
} catch {
|
|
1052
|
+
return match[1];
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
1057
|
+
return url;
|
|
1058
|
+
}
|
|
1059
|
+
if (url.startsWith("//")) {
|
|
1060
|
+
return `https:${url}`;
|
|
1061
|
+
}
|
|
1062
|
+
return url;
|
|
1063
|
+
}
|
|
1064
|
+
function extractHostname(url) {
|
|
1065
|
+
try {
|
|
1066
|
+
return new URL(url).hostname;
|
|
1067
|
+
} catch {
|
|
1068
|
+
return "";
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function filterByDomain(results, allowedDomains, blockedDomains) {
|
|
1072
|
+
let filtered = results;
|
|
1073
|
+
if (allowedDomains && allowedDomains.length > 0) {
|
|
1074
|
+
filtered = filtered.filter((r) => {
|
|
1075
|
+
const hostname = extractHostname(r.url);
|
|
1076
|
+
return allowedDomains.some((d) => hostname.endsWith(d));
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
if (blockedDomains && blockedDomains.length > 0) {
|
|
1080
|
+
filtered = filtered.filter((r) => {
|
|
1081
|
+
const hostname = extractHostname(r.url);
|
|
1082
|
+
return !blockedDomains.some((d) => hostname.endsWith(d));
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
return filtered;
|
|
1086
|
+
}
|
|
1087
|
+
function formatResults(results) {
|
|
1088
|
+
if (results.length === 0) {
|
|
1089
|
+
return "No search results found.";
|
|
1090
|
+
}
|
|
1091
|
+
return results.map(
|
|
1092
|
+
(r, i) => `${i + 1}. ${r.title}
|
|
1093
|
+
URL: ${r.url}${r.snippet ? `
|
|
1094
|
+
${r.snippet}` : ""}`
|
|
1095
|
+
).join("\n\n");
|
|
1096
|
+
}
|
|
1097
|
+
async function execute8(input, ctx) {
|
|
1098
|
+
if (ctx.abortSignal.aborted) return { content: "Aborted", isError: true };
|
|
1099
|
+
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(input.query)}`;
|
|
1100
|
+
try {
|
|
1101
|
+
const res = await fetch(searchUrl, {
|
|
1102
|
+
method: "GET",
|
|
1103
|
+
headers: {
|
|
1104
|
+
"User-Agent": USER_AGENT,
|
|
1105
|
+
Accept: "text/html",
|
|
1106
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
1107
|
+
},
|
|
1108
|
+
signal: ctx.abortSignal
|
|
1109
|
+
});
|
|
1110
|
+
if (!res.ok) {
|
|
1111
|
+
return {
|
|
1112
|
+
content: `Search request failed: HTTP ${res.status} ${res.statusText}`,
|
|
1113
|
+
isError: true
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
const fullHtml = await res.text();
|
|
1117
|
+
const html = fullHtml.slice(0, MAX_RESPONSE_SIZE);
|
|
1118
|
+
let results = parseSearchResults(html, input.max_results);
|
|
1119
|
+
results = filterByDomain(results, input.allowed_domains, input.blocked_domains);
|
|
1120
|
+
let formatted = formatResults(results);
|
|
1121
|
+
if (results.length === 0 && html.length > STRUCTURE_WARNING_THRESHOLD) {
|
|
1122
|
+
formatted += "\n\n[Warning: received a large HTML response but extracted 0 results. DuckDuckGo's HTML structure may have changed.]";
|
|
1123
|
+
}
|
|
1124
|
+
return {
|
|
1125
|
+
content: formatted,
|
|
1126
|
+
isError: false,
|
|
1127
|
+
metadata: { resultCount: results.length, query: input.query }
|
|
1128
|
+
};
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1131
|
+
return { content: `Search error: ${msg}`, isError: true };
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
var webSearchTool = {
|
|
1135
|
+
name: "WebSearch",
|
|
1136
|
+
description: `Searches the web using DuckDuckGo and returns structured results with titles, URLs, and snippets.
|
|
1137
|
+
|
|
1138
|
+
Usage notes:
|
|
1139
|
+
- Use this tool to access up-to-date information beyond the model's knowledge cutoff, look up current events, or find documentation
|
|
1140
|
+
- Results are returned as a numbered list; each entry includes a title, URL, and short snippet
|
|
1141
|
+
- Use allowed_domains to restrict results to specific sites (e.g. ["docs.python.org"]) \u2014 matching uses hostname endsWith, so "github.com" also matches "docs.github.com"
|
|
1142
|
+
- Use blocked_domains to exclude unwanted sites
|
|
1143
|
+
- CRITICAL REQUIREMENT: After answering the user's question using search results, you MUST include a "Sources:" section listing the relevant URLs as markdown hyperlinks
|
|
1144
|
+
|
|
1145
|
+
Example Sources format:
|
|
1146
|
+
Sources:
|
|
1147
|
+
- [Source Title 1](https://example.com/1)
|
|
1148
|
+
- [Source Title 2](https://example.com/2)
|
|
1149
|
+
`,
|
|
1150
|
+
inputSchema: inputSchema8,
|
|
1151
|
+
execute: execute8,
|
|
1152
|
+
isReadOnly: true,
|
|
389
1153
|
timeout: 3e4
|
|
390
1154
|
};
|
|
391
1155
|
|
|
1156
|
+
// src/task.ts
|
|
1157
|
+
var import_zod9 = require("zod");
|
|
1158
|
+
var taskStatus = import_zod9.z.enum(["pending", "in_progress", "completed", "cancelled"]);
|
|
1159
|
+
var createInputSchema = import_zod9.z.object({
|
|
1160
|
+
title: import_zod9.z.string().describe("Task title"),
|
|
1161
|
+
description: import_zod9.z.string().optional().describe("Optional task description"),
|
|
1162
|
+
owner: import_zod9.z.string().optional().describe("Who this task is assigned to")
|
|
1163
|
+
});
|
|
1164
|
+
var updateInputSchema = import_zod9.z.object({
|
|
1165
|
+
id: import_zod9.z.string().describe("Task ID to update"),
|
|
1166
|
+
status: taskStatus.optional().describe("New task status"),
|
|
1167
|
+
title: import_zod9.z.string().optional().describe("New task title"),
|
|
1168
|
+
description: import_zod9.z.string().optional().describe("New task description"),
|
|
1169
|
+
owner: import_zod9.z.string().optional().describe("Assign task to this owner"),
|
|
1170
|
+
add_blocks: import_zod9.z.array(import_zod9.z.string()).optional().describe("Task IDs that this task blocks (appended)"),
|
|
1171
|
+
add_blocked_by: import_zod9.z.array(import_zod9.z.string()).optional().describe("Task IDs that block this task (appended)")
|
|
1172
|
+
});
|
|
1173
|
+
var getInputSchema = import_zod9.z.object({
|
|
1174
|
+
id: import_zod9.z.string().describe("Task ID to retrieve")
|
|
1175
|
+
});
|
|
1176
|
+
var listInputSchema = import_zod9.z.object({
|
|
1177
|
+
status: taskStatus.optional().describe("Filter tasks by status"),
|
|
1178
|
+
owner: import_zod9.z.string().optional().describe("Filter tasks by owner")
|
|
1179
|
+
});
|
|
1180
|
+
function createTaskTool() {
|
|
1181
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
1182
|
+
let nextId = 1;
|
|
1183
|
+
async function executeCreate(input, ctx) {
|
|
1184
|
+
if (ctx.abortSignal.aborted) return { content: "Aborted", isError: true };
|
|
1185
|
+
const id = `task-${nextId++}`;
|
|
1186
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1187
|
+
const task = {
|
|
1188
|
+
id,
|
|
1189
|
+
title: input.title,
|
|
1190
|
+
description: input.description,
|
|
1191
|
+
status: "pending",
|
|
1192
|
+
owner: input.owner,
|
|
1193
|
+
createdAt: now,
|
|
1194
|
+
updatedAt: now
|
|
1195
|
+
};
|
|
1196
|
+
tasks.set(id, task);
|
|
1197
|
+
return {
|
|
1198
|
+
content: `Created task ${id}: ${input.title}`,
|
|
1199
|
+
metadata: { task }
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
async function executeUpdate(input, ctx) {
|
|
1203
|
+
if (ctx.abortSignal.aborted) return { content: "Aborted", isError: true };
|
|
1204
|
+
const existing = tasks.get(input.id);
|
|
1205
|
+
if (!existing) {
|
|
1206
|
+
return { content: `Error: task ${input.id} not found`, isError: true };
|
|
1207
|
+
}
|
|
1208
|
+
const mergedBlocks = input.add_blocks ? [.../* @__PURE__ */ new Set([...existing.blocks ?? [], ...input.add_blocks])] : existing.blocks;
|
|
1209
|
+
const mergedBlockedBy = input.add_blocked_by ? [.../* @__PURE__ */ new Set([...existing.blockedBy ?? [], ...input.add_blocked_by])] : existing.blockedBy;
|
|
1210
|
+
const updated = {
|
|
1211
|
+
...existing,
|
|
1212
|
+
...input.status !== void 0 && { status: input.status },
|
|
1213
|
+
...input.title !== void 0 && { title: input.title },
|
|
1214
|
+
...input.description !== void 0 && { description: input.description },
|
|
1215
|
+
...input.owner !== void 0 && { owner: input.owner },
|
|
1216
|
+
...mergedBlocks !== void 0 && { blocks: mergedBlocks },
|
|
1217
|
+
...mergedBlockedBy !== void 0 && { blockedBy: mergedBlockedBy },
|
|
1218
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1219
|
+
};
|
|
1220
|
+
tasks.set(input.id, updated);
|
|
1221
|
+
const changedFields = [];
|
|
1222
|
+
if (input.status !== void 0) changedFields.push(`status to ${input.status}`);
|
|
1223
|
+
if (input.title !== void 0) changedFields.push(`title to "${input.title}"`);
|
|
1224
|
+
if (input.description !== void 0) changedFields.push(`description`);
|
|
1225
|
+
if (input.owner !== void 0) changedFields.push(`owner to "${input.owner}"`);
|
|
1226
|
+
if (input.add_blocks) changedFields.push(`blocks +${input.add_blocks.join(",")}`);
|
|
1227
|
+
if (input.add_blocked_by) changedFields.push(`blockedBy +${input.add_blocked_by.join(",")}`);
|
|
1228
|
+
return {
|
|
1229
|
+
content: `Updated task ${input.id}: ${changedFields.join(", ") || "no changes"}`,
|
|
1230
|
+
metadata: { task: updated }
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
async function executeGet(input, ctx) {
|
|
1234
|
+
if (ctx.abortSignal.aborted) return { content: "Aborted", isError: true };
|
|
1235
|
+
const task = tasks.get(input.id);
|
|
1236
|
+
if (!task) {
|
|
1237
|
+
return { content: `Error: task ${input.id} not found`, isError: true };
|
|
1238
|
+
}
|
|
1239
|
+
const lines = [
|
|
1240
|
+
`ID: ${task.id}`,
|
|
1241
|
+
`Title: ${task.title}`,
|
|
1242
|
+
`Status: ${task.status}`
|
|
1243
|
+
];
|
|
1244
|
+
if (task.description) lines.push(`Description: ${task.description}`);
|
|
1245
|
+
if (task.owner) lines.push(`Owner: ${task.owner}`);
|
|
1246
|
+
if (task.blocks && task.blocks.length > 0) lines.push(`Blocks: ${task.blocks.join(", ")}`);
|
|
1247
|
+
if (task.blockedBy && task.blockedBy.length > 0) lines.push(`Blocked by: ${task.blockedBy.join(", ")}`);
|
|
1248
|
+
lines.push(`Created: ${task.createdAt}`);
|
|
1249
|
+
lines.push(`Updated: ${task.updatedAt}`);
|
|
1250
|
+
return {
|
|
1251
|
+
content: lines.join("\n"),
|
|
1252
|
+
metadata: { task }
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
async function executeList(input, ctx) {
|
|
1256
|
+
if (ctx.abortSignal.aborted) return { content: "Aborted", isError: true };
|
|
1257
|
+
let all = Array.from(tasks.values());
|
|
1258
|
+
if (input.status) {
|
|
1259
|
+
all = all.filter((t) => t.status === input.status);
|
|
1260
|
+
}
|
|
1261
|
+
if (input.owner) {
|
|
1262
|
+
all = all.filter((t) => t.owner === input.owner);
|
|
1263
|
+
}
|
|
1264
|
+
if (all.length === 0) {
|
|
1265
|
+
const qualifiers = [];
|
|
1266
|
+
if (input.status) qualifiers.push(`status "${input.status}"`);
|
|
1267
|
+
if (input.owner) qualifiers.push(`owner "${input.owner}"`);
|
|
1268
|
+
const qualifier = qualifiers.length > 0 ? ` with ${qualifiers.join(" and ")}` : "";
|
|
1269
|
+
return { content: `No tasks${qualifier}`, metadata: { tasks: [] } };
|
|
1270
|
+
}
|
|
1271
|
+
const lines = all.map(
|
|
1272
|
+
(t) => `[${t.status}] ${t.id}: ${t.title}${t.description ? ` \u2014 ${t.description}` : ""}${t.owner ? ` (${t.owner})` : ""}`
|
|
1273
|
+
);
|
|
1274
|
+
return {
|
|
1275
|
+
content: lines.join("\n"),
|
|
1276
|
+
metadata: { tasks: all }
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
const taskCreate = {
|
|
1280
|
+
name: "TaskCreate",
|
|
1281
|
+
description: `Creates a new task and adds it to the in-session task list.
|
|
1282
|
+
|
|
1283
|
+
Use this tool proactively to track progress on complex multi-step work or when the user provides multiple things to accomplish.
|
|
1284
|
+
|
|
1285
|
+
Parameters:
|
|
1286
|
+
- title: Short, actionable title in imperative form (e.g. "Fix authentication bug in login flow")
|
|
1287
|
+
- description: What needs to be done and any relevant context
|
|
1288
|
+
- owner: Optional \u2014 the agent or person this task is assigned to
|
|
1289
|
+
|
|
1290
|
+
All tasks start with status "pending". Use TaskUpdate to move them through the workflow.
|
|
1291
|
+
`,
|
|
1292
|
+
inputSchema: createInputSchema,
|
|
1293
|
+
execute: executeCreate,
|
|
1294
|
+
isReadOnly: false,
|
|
1295
|
+
isDestructive: false,
|
|
1296
|
+
requiresConfirmation: true,
|
|
1297
|
+
timeout: 5e3
|
|
1298
|
+
};
|
|
1299
|
+
const taskUpdate = {
|
|
1300
|
+
name: "TaskUpdate",
|
|
1301
|
+
description: `Updates an existing task in the task list.
|
|
1302
|
+
|
|
1303
|
+
Use this tool to advance tasks through their lifecycle and to maintain accurate dependency graphs.
|
|
1304
|
+
|
|
1305
|
+
Fields you can update:
|
|
1306
|
+
- status: "pending" \u2192 "in_progress" \u2192 "completed" | "cancelled"
|
|
1307
|
+
- title / description: Change the task subject or requirements
|
|
1308
|
+
- owner: Reassign the task to a different agent or person
|
|
1309
|
+
- add_blocks: Append task IDs that this task blocks (tasks that cannot start until this one is done); deduplicated automatically
|
|
1310
|
+
- add_blocked_by: Append task IDs that must complete before this task can start; deduplicated automatically
|
|
1311
|
+
|
|
1312
|
+
Important:
|
|
1313
|
+
- Mark a task in_progress BEFORE beginning work on it
|
|
1314
|
+
- Only mark a task completed when the work is fully done \u2014 never if tests are failing or implementation is partial
|
|
1315
|
+
- Use TaskGet to read the latest state before updating to avoid stale overwrites
|
|
1316
|
+
`,
|
|
1317
|
+
inputSchema: updateInputSchema,
|
|
1318
|
+
execute: executeUpdate,
|
|
1319
|
+
isReadOnly: false,
|
|
1320
|
+
isDestructive: false,
|
|
1321
|
+
requiresConfirmation: true,
|
|
1322
|
+
timeout: 5e3
|
|
1323
|
+
};
|
|
1324
|
+
const taskGet = {
|
|
1325
|
+
name: "TaskGet",
|
|
1326
|
+
description: `Retrieves full details of a single task by ID.
|
|
1327
|
+
|
|
1328
|
+
Use this tool before starting work on a task to understand its complete requirements, and to inspect dependency relationships.
|
|
1329
|
+
|
|
1330
|
+
Returns:
|
|
1331
|
+
- id, title, status, description, owner
|
|
1332
|
+
- blocks: task IDs that cannot start until this task is completed
|
|
1333
|
+
- blockedBy: task IDs that must complete before this task can start
|
|
1334
|
+
- createdAt / updatedAt timestamps
|
|
1335
|
+
|
|
1336
|
+
Tip: Check that blockedBy is empty (or all dependencies are completed) before marking a task in_progress.
|
|
1337
|
+
`,
|
|
1338
|
+
inputSchema: getInputSchema,
|
|
1339
|
+
execute: executeGet,
|
|
1340
|
+
isReadOnly: true,
|
|
1341
|
+
isDestructive: false,
|
|
1342
|
+
timeout: 5e3
|
|
1343
|
+
};
|
|
1344
|
+
const taskList = {
|
|
1345
|
+
name: "TaskList",
|
|
1346
|
+
description: `Lists all tasks in the current session, with optional filtering.
|
|
1347
|
+
|
|
1348
|
+
Use this tool to get an overview of all work in progress, check what is available to claim, or verify overall completion status.
|
|
1349
|
+
|
|
1350
|
+
Filters:
|
|
1351
|
+
- status: Return only tasks with this status ("pending", "in_progress", "completed", "cancelled")
|
|
1352
|
+
- owner: Return only tasks assigned to this owner
|
|
1353
|
+
|
|
1354
|
+
Each result shows id, title, status, owner, and a summary of blockedBy dependencies. Use TaskGet with a specific id to view the full description and all dependency details.
|
|
1355
|
+
|
|
1356
|
+
Prefer working on tasks in ID order (lowest first) when multiple tasks are available, as earlier tasks often set up context for later ones.
|
|
1357
|
+
`,
|
|
1358
|
+
inputSchema: listInputSchema,
|
|
1359
|
+
execute: executeList,
|
|
1360
|
+
isReadOnly: true,
|
|
1361
|
+
isDestructive: false,
|
|
1362
|
+
timeout: 5e3
|
|
1363
|
+
};
|
|
1364
|
+
return {
|
|
1365
|
+
taskCreate,
|
|
1366
|
+
taskUpdate,
|
|
1367
|
+
taskGet,
|
|
1368
|
+
taskList,
|
|
1369
|
+
getTasks() {
|
|
1370
|
+
return Array.from(tasks.values());
|
|
1371
|
+
},
|
|
1372
|
+
clear() {
|
|
1373
|
+
tasks.clear();
|
|
1374
|
+
nextId = 1;
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// src/subagent.ts
|
|
1380
|
+
var import_zod10 = require("zod");
|
|
1381
|
+
var DEFAULT_TIMEOUT2 = 12e4;
|
|
1382
|
+
var inputSchema9 = import_zod10.z.object({
|
|
1383
|
+
task: import_zod10.z.string().describe("The task for the subagent to complete"),
|
|
1384
|
+
description: import_zod10.z.string().optional().describe("Optional additional context for the subagent")
|
|
1385
|
+
});
|
|
1386
|
+
function createSubagentTool(config) {
|
|
1387
|
+
const timeout = config.timeout ?? DEFAULT_TIMEOUT2;
|
|
1388
|
+
async function execute12(input, ctx) {
|
|
1389
|
+
if (ctx.abortSignal.aborted) {
|
|
1390
|
+
return { content: "Aborted before subagent started", isError: true };
|
|
1391
|
+
}
|
|
1392
|
+
const prompt = input.description ? `${input.task}
|
|
1393
|
+
|
|
1394
|
+
Additional context: ${input.description}` : input.task;
|
|
1395
|
+
const childController = new AbortController();
|
|
1396
|
+
let subagent;
|
|
1397
|
+
try {
|
|
1398
|
+
subagent = config.agentFactory({
|
|
1399
|
+
task: input.task,
|
|
1400
|
+
description: input.description,
|
|
1401
|
+
signal: childController.signal
|
|
1402
|
+
});
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1405
|
+
return { content: `Failed to create subagent: ${msg}`, isError: true };
|
|
1406
|
+
}
|
|
1407
|
+
try {
|
|
1408
|
+
const result = await raceWithTimeoutAndAbort(
|
|
1409
|
+
subagent.chat(prompt),
|
|
1410
|
+
timeout,
|
|
1411
|
+
ctx.abortSignal
|
|
1412
|
+
);
|
|
1413
|
+
return { content: result || "(subagent returned empty response)" };
|
|
1414
|
+
} catch (err) {
|
|
1415
|
+
childController.abort();
|
|
1416
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1417
|
+
if (msg === "Subagent timed out" || msg === "Subagent aborted") {
|
|
1418
|
+
return { content: msg, isError: true };
|
|
1419
|
+
}
|
|
1420
|
+
return { content: `Subagent error: ${msg}`, isError: true };
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
return {
|
|
1424
|
+
name: "Agent",
|
|
1425
|
+
description: `Spawns an independent subagent to complete a delegated task and returns its result.
|
|
1426
|
+
|
|
1427
|
+
The subagent runs with its own isolated session \u2014 it does not share message history or state with the parent agent. It receives the task and optional description, executes independently, and returns its final text response.
|
|
1428
|
+
|
|
1429
|
+
Use this tool for tasks that:
|
|
1430
|
+
- Can be completed without access to the parent's conversation context
|
|
1431
|
+
- Are self-contained enough to delegate to a separate execution unit
|
|
1432
|
+
- Benefit from parallel or isolated execution
|
|
1433
|
+
|
|
1434
|
+
Timeout and abort behavior:
|
|
1435
|
+
- The subagent is subject to a configurable timeout (default: ${DEFAULT_TIMEOUT2 / 1e3}s); it will be forcibly stopped and return an error if it exceeds this limit
|
|
1436
|
+
- If the parent agent's AbortSignal fires (e.g. user cancels), the cancellation is propagated to the subagent via its own AbortController, stopping in-flight work immediately
|
|
1437
|
+
- The signal passed to agentFactory can be used to wire the AbortSignal into the subagent's underlying provider calls
|
|
1438
|
+
`,
|
|
1439
|
+
inputSchema: inputSchema9,
|
|
1440
|
+
execute: execute12,
|
|
1441
|
+
isReadOnly: false,
|
|
1442
|
+
isDestructive: false,
|
|
1443
|
+
requiresConfirmation: true,
|
|
1444
|
+
timeout
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
function raceWithTimeoutAndAbort(promise, timeoutMs, signal) {
|
|
1448
|
+
return new Promise((resolve9, reject) => {
|
|
1449
|
+
let settled = false;
|
|
1450
|
+
const settle = () => {
|
|
1451
|
+
settled = true;
|
|
1452
|
+
clearTimeout(timer);
|
|
1453
|
+
signal.removeEventListener("abort", onAbort);
|
|
1454
|
+
};
|
|
1455
|
+
const timer = setTimeout(() => {
|
|
1456
|
+
if (!settled) {
|
|
1457
|
+
settle();
|
|
1458
|
+
reject(new Error("Subagent timed out"));
|
|
1459
|
+
}
|
|
1460
|
+
}, timeoutMs);
|
|
1461
|
+
const onAbort = () => {
|
|
1462
|
+
if (!settled) {
|
|
1463
|
+
settle();
|
|
1464
|
+
reject(new Error("Subagent aborted"));
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
if (signal.aborted) {
|
|
1468
|
+
settled = true;
|
|
1469
|
+
clearTimeout(timer);
|
|
1470
|
+
reject(new Error("Subagent aborted"));
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1474
|
+
promise.then(
|
|
1475
|
+
(value) => {
|
|
1476
|
+
if (!settled) {
|
|
1477
|
+
settle();
|
|
1478
|
+
resolve9(value);
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
(err) => {
|
|
1482
|
+
if (!settled) {
|
|
1483
|
+
settle();
|
|
1484
|
+
reject(err);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
);
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// src/notebook-edit.ts
|
|
1492
|
+
var fs7 = __toESM(require("fs/promises"));
|
|
1493
|
+
var path7 = __toESM(require("path"));
|
|
1494
|
+
var import_zod11 = require("zod");
|
|
1495
|
+
var inputSchema10 = import_zod11.z.object({
|
|
1496
|
+
notebook_path: import_zod11.z.string().describe("Absolute or relative path to a .ipynb notebook file"),
|
|
1497
|
+
edit_mode: import_zod11.z.enum(["insert", "replace", "delete"]).describe("Action to perform on the cell"),
|
|
1498
|
+
cell_number: import_zod11.z.number().int().min(0).optional().describe("0-based cell index (insert position or target cell)"),
|
|
1499
|
+
cell_id: import_zod11.z.string().optional().describe("Cell ID to locate by metadata.id (alternative to cell_number)"),
|
|
1500
|
+
cell_type: import_zod11.z.enum(["code", "markdown"]).optional().describe("Cell type for insert/replace (default: code for insert, preserves original for replace)"),
|
|
1501
|
+
new_source: import_zod11.z.string().optional().describe("Cell content for insert/replace")
|
|
1502
|
+
});
|
|
1503
|
+
function sourceToLines(source) {
|
|
1504
|
+
if (source === "") return [];
|
|
1505
|
+
const lines = source.split("\n");
|
|
1506
|
+
const result = lines.map((line, i) => i < lines.length - 1 ? `${line}
|
|
1507
|
+
` : line);
|
|
1508
|
+
if (result.length > 0 && result[result.length - 1] === "") {
|
|
1509
|
+
result.pop();
|
|
1510
|
+
}
|
|
1511
|
+
return result;
|
|
1512
|
+
}
|
|
1513
|
+
function makeCell(cellType, source) {
|
|
1514
|
+
const cell = {
|
|
1515
|
+
cell_type: cellType,
|
|
1516
|
+
source: sourceToLines(source),
|
|
1517
|
+
metadata: {}
|
|
1518
|
+
};
|
|
1519
|
+
if (cellType === "code") {
|
|
1520
|
+
cell.outputs = [];
|
|
1521
|
+
cell.execution_count = null;
|
|
1522
|
+
}
|
|
1523
|
+
return cell;
|
|
1524
|
+
}
|
|
1525
|
+
async function execute9(input, ctx) {
|
|
1526
|
+
if (ctx.abortSignal.aborted) return { content: "Aborted", isError: true };
|
|
1527
|
+
const filePath = path7.resolve(ctx.workingDirectory, input.notebook_path);
|
|
1528
|
+
if (!filePath.startsWith(ctx.workingDirectory + path7.sep) && filePath !== ctx.workingDirectory) {
|
|
1529
|
+
return { content: `Error: path traversal denied \u2014 ${input.notebook_path} escapes working directory`, isError: true };
|
|
1530
|
+
}
|
|
1531
|
+
if (path7.extname(filePath).toLowerCase() !== ".ipynb") {
|
|
1532
|
+
return { content: "Error: only .ipynb files are allowed", isError: true };
|
|
1533
|
+
}
|
|
1534
|
+
try {
|
|
1535
|
+
const raw = await fs7.readFile(filePath, "utf-8");
|
|
1536
|
+
let notebook;
|
|
1537
|
+
try {
|
|
1538
|
+
notebook = JSON.parse(raw);
|
|
1539
|
+
} catch {
|
|
1540
|
+
return { content: "Error: file is not valid JSON", isError: true };
|
|
1541
|
+
}
|
|
1542
|
+
if (typeof notebook.nbformat !== "number") {
|
|
1543
|
+
return { content: "Error: invalid notebook format \u2014 missing nbformat field", isError: true };
|
|
1544
|
+
}
|
|
1545
|
+
const cells = notebook.cells;
|
|
1546
|
+
if (!Array.isArray(cells)) {
|
|
1547
|
+
return { content: "Error: invalid notebook format \u2014 missing cells array", isError: true };
|
|
1548
|
+
}
|
|
1549
|
+
if (input.cell_id !== void 0 && input.cell_number !== void 0) {
|
|
1550
|
+
return { content: "Error: provide either cell_id or cell_number, not both", isError: true };
|
|
1551
|
+
}
|
|
1552
|
+
let resolvedCellNumber = input.cell_number;
|
|
1553
|
+
if (input.cell_id !== void 0) {
|
|
1554
|
+
const idx = cells.findIndex(
|
|
1555
|
+
(c) => c.metadata.id === input.cell_id
|
|
1556
|
+
);
|
|
1557
|
+
if (idx === -1) {
|
|
1558
|
+
return { content: `Error: no cell found with metadata.id "${input.cell_id}"`, isError: true };
|
|
1559
|
+
}
|
|
1560
|
+
resolvedCellNumber = idx;
|
|
1561
|
+
}
|
|
1562
|
+
if (resolvedCellNumber === void 0) {
|
|
1563
|
+
return { content: "Error: either cell_number or cell_id must be provided", isError: true };
|
|
1564
|
+
}
|
|
1565
|
+
const { edit_mode: action, cell_type: cellType, new_source: source } = input;
|
|
1566
|
+
const cellIndex = resolvedCellNumber;
|
|
1567
|
+
switch (action) {
|
|
1568
|
+
case "insert": {
|
|
1569
|
+
if (cellIndex < 0 || cellIndex > cells.length) {
|
|
1570
|
+
return {
|
|
1571
|
+
content: `Error: cellIndex ${cellIndex} out of range \u2014 valid insert range is 0..${cells.length}`,
|
|
1572
|
+
isError: true
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
if (source === void 0) {
|
|
1576
|
+
return { content: "Error: source is required for insert action", isError: true };
|
|
1577
|
+
}
|
|
1578
|
+
const newCell = makeCell(cellType ?? "code", source);
|
|
1579
|
+
cells.splice(cellIndex, 0, newCell);
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1582
|
+
case "replace": {
|
|
1583
|
+
if (cellIndex < 0 || cellIndex >= cells.length) {
|
|
1584
|
+
return {
|
|
1585
|
+
content: `Error: cellIndex ${cellIndex} out of range \u2014 valid range is 0..${cells.length - 1}`,
|
|
1586
|
+
isError: true
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
if (source === void 0) {
|
|
1590
|
+
return { content: "Error: source is required for replace action", isError: true };
|
|
1591
|
+
}
|
|
1592
|
+
const original = cells[cellIndex];
|
|
1593
|
+
const effectiveCellType = cellType ?? original.cell_type;
|
|
1594
|
+
cells[cellIndex] = {
|
|
1595
|
+
...original,
|
|
1596
|
+
cell_type: effectiveCellType,
|
|
1597
|
+
source: sourceToLines(source)
|
|
1598
|
+
};
|
|
1599
|
+
break;
|
|
1600
|
+
}
|
|
1601
|
+
case "delete": {
|
|
1602
|
+
if (cellIndex < 0 || cellIndex >= cells.length) {
|
|
1603
|
+
return {
|
|
1604
|
+
content: `Error: cellIndex ${cellIndex} out of range \u2014 valid range is 0..${cells.length - 1}`,
|
|
1605
|
+
isError: true
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
cells.splice(cellIndex, 1);
|
|
1609
|
+
break;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
await fs7.writeFile(filePath, JSON.stringify(notebook, null, 1) + "\n", "utf-8");
|
|
1613
|
+
const totalCells = cells.length;
|
|
1614
|
+
switch (action) {
|
|
1615
|
+
case "insert":
|
|
1616
|
+
return { content: `Inserted ${cellType} cell at index ${cellIndex} in ${filePath} (${totalCells} cells total)` };
|
|
1617
|
+
case "replace":
|
|
1618
|
+
return { content: `Replaced cell at index ${cellIndex} with ${cellType} cell in ${filePath} (${totalCells} cells total)` };
|
|
1619
|
+
case "delete":
|
|
1620
|
+
return { content: `Deleted cell at index ${cellIndex} from ${filePath} (${totalCells} cells total)` };
|
|
1621
|
+
}
|
|
1622
|
+
} catch (err) {
|
|
1623
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1624
|
+
return { content: `Error editing notebook: ${msg}`, isError: true };
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
var notebookEditTool = {
|
|
1628
|
+
name: "NotebookEdit",
|
|
1629
|
+
description: `Edits a Jupyter Notebook (.ipynb file) by inserting, replacing, or deleting cells.
|
|
1630
|
+
|
|
1631
|
+
Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing.
|
|
1632
|
+
|
|
1633
|
+
Edit modes:
|
|
1634
|
+
- "replace": Overwrites the source of the cell at the given index while preserving its metadata, outputs, and execution_count. Specify cell_type to change the cell type.
|
|
1635
|
+
- "insert": Inserts a new cell before the position given by cell_number (0-indexed). cell_type is required; defaults to "code".
|
|
1636
|
+
- "delete": Removes the cell at the given index. new_source is not needed.
|
|
1637
|
+
|
|
1638
|
+
Locating cells:
|
|
1639
|
+
- Use cell_number (0-indexed) to target a cell by its position in the notebook.
|
|
1640
|
+
- Use cell_id to target a cell by its metadata.id field. Provide one or the other, not both.
|
|
1641
|
+
|
|
1642
|
+
Requirements:
|
|
1643
|
+
- notebook_path must point to a .ipynb file. Both absolute and working-directory-relative paths are accepted, but the resolved path must remain inside the working directory.
|
|
1644
|
+
- new_source is required for "insert" and "replace" modes; omit it for "delete".
|
|
1645
|
+
`,
|
|
1646
|
+
inputSchema: inputSchema10,
|
|
1647
|
+
execute: execute9,
|
|
1648
|
+
isReadOnly: false,
|
|
1649
|
+
isDestructive: false,
|
|
1650
|
+
requiresConfirmation: true,
|
|
1651
|
+
timeout: 1e4
|
|
1652
|
+
};
|
|
1653
|
+
|
|
1654
|
+
// src/enter-worktree.ts
|
|
1655
|
+
var import_node_child_process2 = require("child_process");
|
|
1656
|
+
var fs8 = __toESM(require("fs"));
|
|
1657
|
+
var path8 = __toESM(require("path"));
|
|
1658
|
+
var import_zod12 = require("zod");
|
|
1659
|
+
var DEFAULT_TIMEOUT3 = 3e4;
|
|
1660
|
+
var inputSchema11 = import_zod12.z.object({
|
|
1661
|
+
branch: import_zod12.z.string().optional().describe("Branch name for the worktree. Auto-generated if omitted (e.g. worktree-<timestamp>)"),
|
|
1662
|
+
path: import_zod12.z.string().optional().describe("Filesystem path for the worktree. Defaults to .worktrees/<branch> relative to the repo root")
|
|
1663
|
+
});
|
|
1664
|
+
function generateBranchName() {
|
|
1665
|
+
const ts = Date.now().toString(36);
|
|
1666
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
1667
|
+
return `worktree-${ts}-${rand}`;
|
|
1668
|
+
}
|
|
1669
|
+
function getRepoRoot(cwd) {
|
|
1670
|
+
return new Promise((resolve9, reject) => {
|
|
1671
|
+
(0, import_node_child_process2.exec)("git rev-parse --show-toplevel", { cwd }, (err, stdout) => {
|
|
1672
|
+
if (err) {
|
|
1673
|
+
reject(new Error("Not inside a git repository"));
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
resolve9(stdout.trim());
|
|
1677
|
+
});
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
async function execute10(input, ctx) {
|
|
1681
|
+
const cwd = ctx.workingDirectory;
|
|
1682
|
+
let repoRoot;
|
|
1683
|
+
try {
|
|
1684
|
+
repoRoot = await getRepoRoot(cwd);
|
|
1685
|
+
} catch {
|
|
1686
|
+
return { content: "Not inside a git repository", isError: true };
|
|
1687
|
+
}
|
|
1688
|
+
const branch = input.branch ?? generateBranchName();
|
|
1689
|
+
const worktreePath = input.path ? path8.resolve(cwd, input.path) : path8.join(repoRoot, ".worktrees", branch);
|
|
1690
|
+
fs8.mkdirSync(path8.dirname(worktreePath), { recursive: true });
|
|
1691
|
+
const cmd = `git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(branch)}`;
|
|
1692
|
+
return new Promise((resolve9) => {
|
|
1693
|
+
(0, import_node_child_process2.exec)(cmd, { cwd: repoRoot, timeout: DEFAULT_TIMEOUT3 }, (err, stdout, stderr) => {
|
|
1694
|
+
const output = (stdout + (stderr ? `
|
|
1695
|
+
${stderr}` : "")).trim();
|
|
1696
|
+
if (err) {
|
|
1697
|
+
resolve9({ content: output || err.message, isError: true });
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
resolve9({
|
|
1701
|
+
content: `Worktree created.
|
|
1702
|
+
Branch: ${branch}
|
|
1703
|
+
Path: ${worktreePath}`,
|
|
1704
|
+
metadata: { branch, path: worktreePath }
|
|
1705
|
+
});
|
|
1706
|
+
});
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
var enterWorktreeTool = {
|
|
1710
|
+
name: "EnterWorktree",
|
|
1711
|
+
description: `Creates an isolated git worktree so the agent can work in a separate directory without affecting the main working tree.
|
|
1712
|
+
|
|
1713
|
+
A worktree is a linked checkout of the same repository at a different path, on its own branch. This is useful for:
|
|
1714
|
+
- Running experimental changes without touching the current branch
|
|
1715
|
+
- Parallel work on multiple features
|
|
1716
|
+
- Safe exploration that can be discarded cleanly
|
|
1717
|
+
|
|
1718
|
+
The tool creates a new branch and checks it out in the worktree directory. Use ExitWorktree to clean up when done.
|
|
1719
|
+
|
|
1720
|
+
# Inputs
|
|
1721
|
+
|
|
1722
|
+
- \`branch\`: Name for the new branch. Auto-generated if omitted.
|
|
1723
|
+
- \`path\`: Filesystem path for the worktree. Defaults to \`.worktrees/<branch>\` under the repo root.
|
|
1724
|
+
|
|
1725
|
+
# Notes
|
|
1726
|
+
|
|
1727
|
+
- The worktree shares the same git object store as the main repo \u2014 commits, stashes, and refs are visible across all worktrees.
|
|
1728
|
+
- You cannot check out a branch that is already checked out in another worktree.
|
|
1729
|
+
- After creation, use the returned path as the working directory for subsequent tool calls.`,
|
|
1730
|
+
inputSchema: inputSchema11,
|
|
1731
|
+
execute: execute10,
|
|
1732
|
+
isReadOnly: false,
|
|
1733
|
+
isDestructive: false,
|
|
1734
|
+
requiresConfirmation: true,
|
|
1735
|
+
timeout: DEFAULT_TIMEOUT3
|
|
1736
|
+
};
|
|
1737
|
+
|
|
1738
|
+
// src/exit-worktree.ts
|
|
1739
|
+
var import_node_child_process3 = require("child_process");
|
|
1740
|
+
var path9 = __toESM(require("path"));
|
|
1741
|
+
var import_zod13 = require("zod");
|
|
1742
|
+
var DEFAULT_TIMEOUT4 = 3e4;
|
|
1743
|
+
var inputSchema12 = import_zod13.z.object({
|
|
1744
|
+
path: import_zod13.z.string().describe("Filesystem path of the worktree to exit"),
|
|
1745
|
+
keep: import_zod13.z.boolean().optional().default(false).describe("If true, keep the worktree on disk (only unregister from git). If false (default), remove the worktree directory entirely")
|
|
1746
|
+
});
|
|
1747
|
+
async function execute11(input, ctx) {
|
|
1748
|
+
const worktreePath = path9.resolve(ctx.workingDirectory, input.path);
|
|
1749
|
+
if (input.keep) {
|
|
1750
|
+
return {
|
|
1751
|
+
content: `Worktree kept at: ${worktreePath}
|
|
1752
|
+
The worktree directory and branch remain intact. Use \`git worktree remove <path>\` later to clean up, or \`git worktree prune\` after manually deleting the directory.`,
|
|
1753
|
+
metadata: { path: worktreePath, kept: true }
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
const cmd = `git worktree remove ${JSON.stringify(worktreePath)} --force`;
|
|
1757
|
+
return new Promise((resolve9) => {
|
|
1758
|
+
(0, import_node_child_process3.exec)(cmd, { cwd: ctx.workingDirectory, timeout: DEFAULT_TIMEOUT4 }, (err, stdout, stderr) => {
|
|
1759
|
+
const output = (stdout + (stderr ? `
|
|
1760
|
+
${stderr}` : "")).trim();
|
|
1761
|
+
if (err) {
|
|
1762
|
+
resolve9({ content: output || err.message, isError: true });
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
resolve9({
|
|
1766
|
+
content: `Worktree removed: ${worktreePath}`,
|
|
1767
|
+
metadata: { path: worktreePath, kept: false }
|
|
1768
|
+
});
|
|
1769
|
+
});
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
var exitWorktreeTool = {
|
|
1773
|
+
name: "ExitWorktree",
|
|
1774
|
+
description: `Removes or keeps a git worktree that was previously created with EnterWorktree.
|
|
1775
|
+
|
|
1776
|
+
# Behavior
|
|
1777
|
+
|
|
1778
|
+
- \`keep=false\` (default): Runs \`git worktree remove\` to delete the worktree directory and unregister it from git. Any uncommitted changes in the worktree will be lost.
|
|
1779
|
+
- \`keep=true\`: Leaves the worktree directory and branch intact. Returns a reminder of how to clean up manually later.
|
|
1780
|
+
|
|
1781
|
+
# Inputs
|
|
1782
|
+
|
|
1783
|
+
- \`path\`: The filesystem path of the worktree (as returned by EnterWorktree).
|
|
1784
|
+
- \`keep\`: Whether to preserve the worktree on disk (default: false).
|
|
1785
|
+
|
|
1786
|
+
# Notes
|
|
1787
|
+
|
|
1788
|
+
- The branch created by EnterWorktree is NOT deleted \u2014 only the worktree checkout is removed. Delete the branch separately with \`git branch -d <name>\` if no longer needed.
|
|
1789
|
+
- If the worktree has uncommitted changes and \`keep=false\`, the removal is forced.`,
|
|
1790
|
+
inputSchema: inputSchema12,
|
|
1791
|
+
execute: execute11,
|
|
1792
|
+
isReadOnly: false,
|
|
1793
|
+
isDestructive: true,
|
|
1794
|
+
requiresConfirmation: true,
|
|
1795
|
+
timeout: DEFAULT_TIMEOUT4
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
// src/lsp.ts
|
|
1799
|
+
var import_zod14 = require("zod");
|
|
1800
|
+
var actionEnum = import_zod14.z.enum([
|
|
1801
|
+
"goToDefinition",
|
|
1802
|
+
"findReferences",
|
|
1803
|
+
"hover",
|
|
1804
|
+
"documentSymbol",
|
|
1805
|
+
"workspaceSymbol"
|
|
1806
|
+
]);
|
|
1807
|
+
var inputSchema13 = import_zod14.z.object({
|
|
1808
|
+
action: actionEnum.describe(
|
|
1809
|
+
"The LSP action to perform: goToDefinition, findReferences, hover, documentSymbol, or workspaceSymbol"
|
|
1810
|
+
),
|
|
1811
|
+
file_path: import_zod14.z.string().optional().describe("Absolute path to the file (required for all actions except workspaceSymbol)"),
|
|
1812
|
+
line: import_zod14.z.number().optional().describe("0-based line number (required for goToDefinition, findReferences, hover)"),
|
|
1813
|
+
character: import_zod14.z.number().optional().describe("0-based character offset (required for goToDefinition, findReferences, hover)"),
|
|
1814
|
+
query: import_zod14.z.string().optional().describe("Search query for workspaceSymbol")
|
|
1815
|
+
}).superRefine((data, ctx) => {
|
|
1816
|
+
const positionActions = ["goToDefinition", "findReferences", "hover"];
|
|
1817
|
+
const needsPosition = positionActions.includes(data.action);
|
|
1818
|
+
if (data.action !== "workspaceSymbol" && !data.file_path) {
|
|
1819
|
+
ctx.addIssue({
|
|
1820
|
+
code: import_zod14.z.ZodIssueCode.custom,
|
|
1821
|
+
message: "file_path is required for this action",
|
|
1822
|
+
path: ["file_path"]
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
if (needsPosition && data.line === void 0) {
|
|
1826
|
+
ctx.addIssue({
|
|
1827
|
+
code: import_zod14.z.ZodIssueCode.custom,
|
|
1828
|
+
message: "line is required for this action",
|
|
1829
|
+
path: ["line"]
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
if (needsPosition && data.character === void 0) {
|
|
1833
|
+
ctx.addIssue({
|
|
1834
|
+
code: import_zod14.z.ZodIssueCode.custom,
|
|
1835
|
+
message: "character is required for this action",
|
|
1836
|
+
path: ["character"]
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
function filePathToUri(filePath) {
|
|
1841
|
+
const normalized = filePath.startsWith("/") ? filePath : `/${filePath}`;
|
|
1842
|
+
return `file://${normalized}`;
|
|
1843
|
+
}
|
|
1844
|
+
function buildLspRequest(input) {
|
|
1845
|
+
const uri = input.file_path ? filePathToUri(input.file_path) : void 0;
|
|
1846
|
+
switch (input.action) {
|
|
1847
|
+
case "goToDefinition":
|
|
1848
|
+
return {
|
|
1849
|
+
method: "textDocument/definition",
|
|
1850
|
+
params: {
|
|
1851
|
+
textDocument: { uri },
|
|
1852
|
+
position: { line: input.line, character: input.character }
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
case "findReferences":
|
|
1856
|
+
return {
|
|
1857
|
+
method: "textDocument/references",
|
|
1858
|
+
params: {
|
|
1859
|
+
textDocument: { uri },
|
|
1860
|
+
position: { line: input.line, character: input.character },
|
|
1861
|
+
context: { includeDeclaration: true }
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
case "hover":
|
|
1865
|
+
return {
|
|
1866
|
+
method: "textDocument/hover",
|
|
1867
|
+
params: {
|
|
1868
|
+
textDocument: { uri },
|
|
1869
|
+
position: { line: input.line, character: input.character }
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
case "documentSymbol":
|
|
1873
|
+
return {
|
|
1874
|
+
method: "textDocument/documentSymbol",
|
|
1875
|
+
params: {
|
|
1876
|
+
textDocument: { uri }
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
1879
|
+
case "workspaceSymbol":
|
|
1880
|
+
return {
|
|
1881
|
+
method: "workspace/symbol",
|
|
1882
|
+
params: {
|
|
1883
|
+
query: input.query ?? ""
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
var SYMBOL_KIND_MAP = {
|
|
1889
|
+
1: "File",
|
|
1890
|
+
2: "Module",
|
|
1891
|
+
3: "Namespace",
|
|
1892
|
+
4: "Package",
|
|
1893
|
+
5: "Class",
|
|
1894
|
+
6: "Method",
|
|
1895
|
+
7: "Property",
|
|
1896
|
+
8: "Field",
|
|
1897
|
+
9: "Constructor",
|
|
1898
|
+
10: "Enum",
|
|
1899
|
+
11: "Interface",
|
|
1900
|
+
12: "Function",
|
|
1901
|
+
13: "Variable",
|
|
1902
|
+
14: "Constant",
|
|
1903
|
+
15: "String",
|
|
1904
|
+
16: "Number",
|
|
1905
|
+
17: "Boolean",
|
|
1906
|
+
18: "Array",
|
|
1907
|
+
19: "Object",
|
|
1908
|
+
20: "Key",
|
|
1909
|
+
21: "Null",
|
|
1910
|
+
22: "EnumMember",
|
|
1911
|
+
23: "Struct",
|
|
1912
|
+
24: "Event",
|
|
1913
|
+
25: "Operator",
|
|
1914
|
+
26: "TypeParameter"
|
|
1915
|
+
};
|
|
1916
|
+
function symbolKindName(kind) {
|
|
1917
|
+
return SYMBOL_KIND_MAP[kind] ?? `Kind(${kind})`;
|
|
1918
|
+
}
|
|
1919
|
+
function formatLocation(loc) {
|
|
1920
|
+
const path10 = loc.uri.replace(/^file:\/\//, "");
|
|
1921
|
+
return `${path10}:${loc.range.start.line + 1}:${loc.range.start.character + 1}`;
|
|
1922
|
+
}
|
|
1923
|
+
function formatLocations(result) {
|
|
1924
|
+
if (!result) return "No results found";
|
|
1925
|
+
if (!Array.isArray(result) && typeof result === "object" && "uri" in result) {
|
|
1926
|
+
return formatLocation(result);
|
|
1927
|
+
}
|
|
1928
|
+
if (Array.isArray(result)) {
|
|
1929
|
+
if (result.length === 0) return "No results found";
|
|
1930
|
+
return result.map((loc) => formatLocation(loc)).join("\n");
|
|
1931
|
+
}
|
|
1932
|
+
return JSON.stringify(result, null, 2);
|
|
1933
|
+
}
|
|
1934
|
+
function formatHover(result) {
|
|
1935
|
+
if (!result) return "No hover information available";
|
|
1936
|
+
const hover = result;
|
|
1937
|
+
const contents = hover.contents;
|
|
1938
|
+
if (typeof contents === "string") return contents;
|
|
1939
|
+
if (typeof contents === "object" && contents !== null) {
|
|
1940
|
+
if ("value" in contents) {
|
|
1941
|
+
return contents.value;
|
|
1942
|
+
}
|
|
1943
|
+
if (Array.isArray(contents)) {
|
|
1944
|
+
return contents.map((c) => typeof c === "string" ? c : c.value ?? JSON.stringify(c)).join("\n\n");
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
return JSON.stringify(contents, null, 2);
|
|
1948
|
+
}
|
|
1949
|
+
function formatDocumentSymbols(result, indent = 0) {
|
|
1950
|
+
if (!result || Array.isArray(result) && result.length === 0) {
|
|
1951
|
+
return "No symbols found";
|
|
1952
|
+
}
|
|
1953
|
+
if (!Array.isArray(result)) return JSON.stringify(result, null, 2);
|
|
1954
|
+
const lines = [];
|
|
1955
|
+
const prefix = " ".repeat(indent);
|
|
1956
|
+
for (const sym of result) {
|
|
1957
|
+
if ("range" in sym) {
|
|
1958
|
+
const ds = sym;
|
|
1959
|
+
lines.push(
|
|
1960
|
+
`${prefix}${symbolKindName(ds.kind)} ${ds.name} (L${ds.range.start.line + 1}-${ds.range.end.line + 1})`
|
|
1961
|
+
);
|
|
1962
|
+
if (ds.children && ds.children.length > 0) {
|
|
1963
|
+
lines.push(formatDocumentSymbols(ds.children, indent + 1));
|
|
1964
|
+
}
|
|
1965
|
+
} else {
|
|
1966
|
+
const si = sym;
|
|
1967
|
+
const loc = formatLocation(si.location);
|
|
1968
|
+
const container = si.containerName ? ` [${si.containerName}]` : "";
|
|
1969
|
+
lines.push(`${prefix}${symbolKindName(si.kind)} ${si.name}${container} ${loc}`);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
return lines.join("\n");
|
|
1973
|
+
}
|
|
1974
|
+
function formatResult(action, result) {
|
|
1975
|
+
switch (action) {
|
|
1976
|
+
case "goToDefinition":
|
|
1977
|
+
case "findReferences":
|
|
1978
|
+
return formatLocations(result);
|
|
1979
|
+
case "hover":
|
|
1980
|
+
return formatHover(result);
|
|
1981
|
+
case "documentSymbol":
|
|
1982
|
+
case "workspaceSymbol":
|
|
1983
|
+
return formatDocumentSymbols(result);
|
|
1984
|
+
default:
|
|
1985
|
+
return JSON.stringify(result, null, 2);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
var NO_CONNECTION_MESSAGE = "LSP not available. Start a language server and pass its connection to the tool via createLspTool(connection).";
|
|
1989
|
+
function createLspTool(connection) {
|
|
1990
|
+
async function execute12(input, ctx) {
|
|
1991
|
+
if (ctx.abortSignal.aborted) {
|
|
1992
|
+
return { content: "Aborted", isError: true };
|
|
1993
|
+
}
|
|
1994
|
+
if (!connection) {
|
|
1995
|
+
return { content: NO_CONNECTION_MESSAGE, isError: true };
|
|
1996
|
+
}
|
|
1997
|
+
const { method, params } = buildLspRequest(input);
|
|
1998
|
+
try {
|
|
1999
|
+
const result = await connection.request(method, params);
|
|
2000
|
+
const formatted = formatResult(input.action, result);
|
|
2001
|
+
return {
|
|
2002
|
+
content: formatted,
|
|
2003
|
+
metadata: { action: input.action, method, raw: result }
|
|
2004
|
+
};
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2007
|
+
return { content: `LSP request failed: ${msg}`, isError: true };
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
return {
|
|
2011
|
+
name: "LSP",
|
|
2012
|
+
description: `Interacts with a Language Server via the Language Server Protocol (LSP) to navigate and understand code.
|
|
2013
|
+
|
|
2014
|
+
Supported actions:
|
|
2015
|
+
|
|
2016
|
+
- **goToDefinition** \u2014 Jump to the definition of the symbol at the given position. Returns file path and location of the definition. Useful for navigating to function implementations, type definitions, variable declarations, and imported symbols.
|
|
2017
|
+
|
|
2018
|
+
- **findReferences** \u2014 Find all references to the symbol at the given position across the workspace. Returns a list of file locations where the symbol is used. Useful for understanding impact before renaming or refactoring, and for tracing how a function or variable is consumed.
|
|
2019
|
+
|
|
2020
|
+
- **hover** \u2014 Get type information and documentation for the symbol at the given position. Returns type signatures, JSDoc/docstrings, and inferred types. Useful for understanding what a symbol is without navigating away from the current context.
|
|
2021
|
+
|
|
2022
|
+
- **documentSymbol** \u2014 List all symbols (functions, classes, variables, interfaces, etc.) defined in a file. Returns a hierarchical tree of symbols with their kinds and line ranges. Useful for getting an overview of a file's structure and finding specific declarations.
|
|
2023
|
+
|
|
2024
|
+
- **workspaceSymbol** \u2014 Search for symbols across the entire workspace by name. Returns matching symbols with their file locations. Useful for finding where a type, function, or class is defined when you don't know which file it's in.
|
|
2025
|
+
|
|
2026
|
+
# Position parameters
|
|
2027
|
+
|
|
2028
|
+
For goToDefinition, findReferences, and hover, provide the exact cursor position using 0-based \`line\` and \`character\` offsets. These correspond to the position in the file where the symbol of interest is located.
|
|
2029
|
+
|
|
2030
|
+
# File path
|
|
2031
|
+
|
|
2032
|
+
Provide the absolute file path for all actions except workspaceSymbol. The tool converts it to a file:// URI for the LSP request.
|
|
2033
|
+
|
|
2034
|
+
# When LSP is not available
|
|
2035
|
+
|
|
2036
|
+
If no language server connection has been configured, the tool returns an error explaining how to set one up. The connection is provided at tool creation time via \`createLspTool(connection)\`.`,
|
|
2037
|
+
inputSchema: inputSchema13,
|
|
2038
|
+
execute: execute12,
|
|
2039
|
+
isReadOnly: true,
|
|
2040
|
+
isDestructive: false,
|
|
2041
|
+
timeout: 3e4
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
|
|
392
2045
|
// src/index.ts
|
|
393
2046
|
var builtinTools = [
|
|
394
2047
|
bashTool,
|
|
@@ -397,16 +2050,26 @@ var builtinTools = [
|
|
|
397
2050
|
writeTool,
|
|
398
2051
|
globTool,
|
|
399
2052
|
grepTool,
|
|
400
|
-
webFetchTool
|
|
2053
|
+
webFetchTool,
|
|
2054
|
+
webSearchTool,
|
|
2055
|
+
enterWorktreeTool,
|
|
2056
|
+
exitWorktreeTool
|
|
401
2057
|
];
|
|
402
2058
|
// Annotate the CommonJS export names for ESM import in node:
|
|
403
2059
|
0 && (module.exports = {
|
|
404
2060
|
bashTool,
|
|
405
2061
|
builtinTools,
|
|
2062
|
+
createLspTool,
|
|
2063
|
+
createSubagentTool,
|
|
2064
|
+
createTaskTool,
|
|
406
2065
|
editTool,
|
|
2066
|
+
enterWorktreeTool,
|
|
2067
|
+
exitWorktreeTool,
|
|
407
2068
|
globTool,
|
|
408
2069
|
grepTool,
|
|
2070
|
+
notebookEditTool,
|
|
409
2071
|
readTool,
|
|
410
2072
|
webFetchTool,
|
|
2073
|
+
webSearchTool,
|
|
411
2074
|
writeTool
|
|
412
2075
|
});
|