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