@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/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(3e4).describe("Timeout in milliseconds")
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
- return new Promise((resolve5) => {
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
- resolve5({ content: "Command aborted", isError: true });
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
- resolve5({ content: `Command timed out after ${timeout}ms
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
- resolve5({ content: output || err.message, isError: true, metadata: { exitCode: err.code } });
55
+ resolve9({ content: output || err.message, isError: true, metadata: { exitCode: err.code, sandboxed } });
29
56
  return;
30
57
  }
31
- resolve5({ content: output || "(no output)" });
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: "bash",
42
- description: "Execute a shell command and return its stdout/stderr output",
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: 3e4
124
+ timeout: DEFAULT_TIMEOUT
48
125
  };
49
126
 
50
127
  // src/read.ts
51
- import * as fs from "fs/promises";
52
- import * as path from "path";
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
- path: z2.string().describe("Absolute or relative file path to read"),
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 = path.resolve(ctx.workingDirectory, input.path);
63
- if (!filePath.startsWith(ctx.workingDirectory + path.sep) && filePath !== ctx.workingDirectory) {
64
- return { content: `Error: path traversal denied \u2014 ${input.path} escapes working directory`, isError: true };
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 fs.readFile(filePath, "utf-8");
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
- if (input.limit !== void 0) {
73
- lines = lines.slice(0, input.limit);
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: "read",
86
- description: "Read file contents with optional line offset and limit, returning numbered lines",
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 fs2 from "fs/promises";
95
- import * as path2 from "path";
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
- path: z3.string().describe("Absolute or relative file path to edit"),
99
- oldString: z3.string().describe("Exact string to find and replace (must be unique in file)"),
100
- newString: z3.string().describe("Replacement string")
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 = path2.resolve(ctx.workingDirectory, input.path);
105
- if (!filePath.startsWith(ctx.workingDirectory + path2.sep) && filePath !== ctx.workingDirectory) {
106
- return { content: `Error: path traversal denied \u2014 ${input.path} escapes working directory`, isError: true };
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 fs2.readFile(filePath, "utf-8");
110
- const occurrences = content.split(input.oldString).length - 1;
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: oldString not found in file", isError: true };
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: oldString found ${occurrences} times \u2014 must be unique. Provide more context to disambiguate.`,
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
- const updated = content.replace(input.oldString, input.newString);
121
- await fs2.writeFile(filePath, updated, "utf-8");
122
- return { content: `Successfully edited ${filePath}` };
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: "edit",
130
- description: "Edit a file by replacing a unique string occurrence with a new string",
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 fs3 from "fs/promises";
140
- import * as path3 from "path";
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
- path: z4.string().describe("Absolute or relative file path to write"),
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 = path3.resolve(ctx.workingDirectory, input.path);
149
- if (!filePath.startsWith(ctx.workingDirectory + path3.sep) && filePath !== ctx.workingDirectory) {
150
- return { content: `Error: path traversal denied \u2014 ${input.path} escapes working directory`, isError: true };
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 fs3.mkdir(path3.dirname(filePath), { recursive: true });
154
- await fs3.writeFile(filePath, input.content, "utf-8");
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: "write",
165
- description: "Write content to a file, creating parent directories as needed",
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
- cwd: z5.string().optional().describe("Directory to search in")
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.cwd ?? ctx.workingDirectory;
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
- files.sort();
193
- if (files.length === 0) {
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 = files.join("\n").slice(0, MAX_RESULT_SIZE3);
197
- return { content, metadata: { matchCount: files.length } };
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: "glob",
205
- description: "Find files matching a glob pattern, excluding node_modules and .git",
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 fs4 from "fs/promises";
214
- import * as path4 from "path";
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("Directory or file to search in"),
222
- glob: z6.string().optional().describe("Glob filter for files (e.g. *.ts)")
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 ? path4.resolve(ctx.workingDirectory, input.path) : ctx.workingDirectory;
227
- const globPattern = input.glob ?? "**/*";
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
- const regex = new RegExp(input.pattern);
230
- const stat2 = await fs4.stat(searchPath);
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 (stat2.isFile()) {
632
+ if (stat3.isFile()) {
233
633
  files = [searchPath];
234
634
  } else {
235
- files = await fg2(globPattern, {
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 matches = [];
244
- let totalSize = 0;
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 fs4.readFile(file, "utf-8");
678
+ const content = await fs6.readFile(file, "utf-8");
249
679
  const lines = content.split("\n");
250
- for (let i = 0; i < lines.length; i++) {
251
- if (regex.test(lines[i])) {
252
- const rel = path4.relative(ctx.workingDirectory, file);
253
- const line = `${rel}:${i + 1}: ${lines[i]}`;
254
- totalSize += line.length;
255
- if (totalSize > MAX_RESULT_SIZE4) break;
256
- matches.push(line);
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 (matches.length === 0) {
723
+ if (entries.length === 0) {
264
724
  return { content: "No matches found" };
265
725
  }
266
- return { content: matches.join("\n"), metadata: { matchCount: matches.length } };
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: "grep",
274
- description: "Search file contents using regex, returning matching lines with file:line format",
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 MAX_RESULT_SIZE5 = 5e4;
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) => `![${alt}](${src})`);
827
+ md = md.replace(/<img[^>]+src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, (_, src, alt) => `![${alt}](${src})`);
828
+ md = md.replace(/<img[^>]+src="([^"]*)"[^>]*\/?>/gi, (_, src) => `![](${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(input.url)) {
312
- return { content: `Error: request to private/internal address denied \u2014 ${input.url}`, isError: true };
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 ${input.url}`, isError: true };
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(input.url, {
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
- const text = await res.text();
325
- const truncated = text.slice(0, MAX_RESULT_SIZE5);
326
- const suffix = text.length > MAX_RESULT_SIZE5 ? "\n...(truncated)" : "";
327
- const content = `HTTP ${res.status} ${res.statusText}
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
- return {
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: "web_fetch",
342
- description: "Make HTTP requests and return the response body",
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(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&nbsp;/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
  };