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