noumen 0.1.0 → 0.2.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.
Files changed (118) hide show
  1. package/README.md +767 -51
  2. package/dist/a2a/index.d.ts +148 -0
  3. package/dist/a2a/index.js +579 -0
  4. package/dist/a2a/index.js.map +1 -0
  5. package/dist/acp/index.d.ts +129 -0
  6. package/dist/acp/index.js +498 -0
  7. package/dist/acp/index.js.map +1 -0
  8. package/dist/agent-BrkbZyOT.d.ts +1028 -0
  9. package/dist/cache-DVqaCX8v.d.ts +38 -0
  10. package/dist/chunk-2ZTGQLYK.js +356 -0
  11. package/dist/chunk-2ZTGQLYK.js.map +1 -0
  12. package/dist/chunk-42PHHZUA.js +132 -0
  13. package/dist/chunk-42PHHZUA.js.map +1 -0
  14. package/dist/chunk-4SQA2UCV.js +26 -0
  15. package/dist/chunk-4SQA2UCV.js.map +1 -0
  16. package/dist/chunk-5GEX6ZSB.js +179 -0
  17. package/dist/chunk-5GEX6ZSB.js.map +1 -0
  18. package/dist/chunk-7ZMN7XJE.js +94 -0
  19. package/dist/chunk-7ZMN7XJE.js.map +1 -0
  20. package/dist/chunk-AMYIJSAZ.js +57 -0
  21. package/dist/chunk-AMYIJSAZ.js.map +1 -0
  22. package/dist/chunk-BGG2E6JD.js +10 -0
  23. package/dist/chunk-BGG2E6JD.js.map +1 -0
  24. package/dist/chunk-BZSFUEWM.js +43 -0
  25. package/dist/chunk-BZSFUEWM.js.map +1 -0
  26. package/dist/chunk-CPFHEPW4.js +139 -0
  27. package/dist/chunk-CPFHEPW4.js.map +1 -0
  28. package/dist/chunk-D43BWEZA.js +346 -0
  29. package/dist/chunk-D43BWEZA.js.map +1 -0
  30. package/dist/chunk-DGUM43GV.js +11 -0
  31. package/dist/chunk-DGUM43GV.js.map +1 -0
  32. package/dist/chunk-JACGEMTF.js +43 -0
  33. package/dist/chunk-JACGEMTF.js.map +1 -0
  34. package/dist/chunk-JX7CLUCV.js +21 -0
  35. package/dist/chunk-JX7CLUCV.js.map +1 -0
  36. package/dist/chunk-KXDB56YW.js +39 -0
  37. package/dist/chunk-KXDB56YW.js.map +1 -0
  38. package/dist/chunk-KY6ZPWHO.js +112 -0
  39. package/dist/chunk-KY6ZPWHO.js.map +1 -0
  40. package/dist/chunk-NBDFQYUZ.js +7992 -0
  41. package/dist/chunk-NBDFQYUZ.js.map +1 -0
  42. package/dist/chunk-OGXNFXFA.js +196 -0
  43. package/dist/chunk-OGXNFXFA.js.map +1 -0
  44. package/dist/chunk-QTJ7VTJY.js +1994 -0
  45. package/dist/chunk-QTJ7VTJY.js.map +1 -0
  46. package/dist/chunk-UVSSQBDY.js +192 -0
  47. package/dist/chunk-UVSSQBDY.js.map +1 -0
  48. package/dist/chunk-Y45R3PQL.js +684 -0
  49. package/dist/chunk-Y45R3PQL.js.map +1 -0
  50. package/dist/cli/index.d.ts +1 -0
  51. package/dist/cli/index.js +868 -0
  52. package/dist/cli/index.js.map +1 -0
  53. package/dist/client/index.d.ts +64 -0
  54. package/dist/client/index.js +409 -0
  55. package/dist/client/index.js.map +1 -0
  56. package/dist/client-CRRO2376.js +10 -0
  57. package/dist/client-CRRO2376.js.map +1 -0
  58. package/dist/headless-Q7XHHZIW.js +143 -0
  59. package/dist/headless-Q7XHHZIW.js.map +1 -0
  60. package/dist/history-snip-64GYP4ZL.js +12 -0
  61. package/dist/history-snip-64GYP4ZL.js.map +1 -0
  62. package/dist/index.d.ts +1305 -418
  63. package/dist/index.js +384 -1757
  64. package/dist/index.js.map +1 -1
  65. package/dist/jsonrpc/index.d.ts +54 -0
  66. package/dist/jsonrpc/index.js +34 -0
  67. package/dist/jsonrpc/index.js.map +1 -0
  68. package/dist/lsp/index.d.ts +36 -0
  69. package/dist/lsp/index.js +16 -0
  70. package/dist/lsp/index.js.map +1 -0
  71. package/dist/lsp-PS3BWIHC.js +8 -0
  72. package/dist/lsp-PS3BWIHC.js.map +1 -0
  73. package/dist/manager-DLXK63XC.js +8 -0
  74. package/dist/manager-DLXK63XC.js.map +1 -0
  75. package/dist/mcp/index.d.ts +111 -0
  76. package/dist/mcp/index.js +104 -0
  77. package/dist/mcp/index.js.map +1 -0
  78. package/dist/mcp-auth-AEI2R4ZC.js +9 -0
  79. package/dist/mcp-auth-AEI2R4ZC.js.map +1 -0
  80. package/dist/ollama-YNXAYP3R.js +18 -0
  81. package/dist/ollama-YNXAYP3R.js.map +1 -0
  82. package/dist/provider-factory-34MSWJZ3.js +20 -0
  83. package/dist/provider-factory-34MSWJZ3.js.map +1 -0
  84. package/dist/providers/anthropic.d.ts +19 -0
  85. package/dist/providers/anthropic.js +33 -0
  86. package/dist/providers/anthropic.js.map +1 -0
  87. package/dist/providers/bedrock.d.ts +39 -0
  88. package/dist/providers/bedrock.js +54 -0
  89. package/dist/providers/bedrock.js.map +1 -0
  90. package/dist/providers/gemini.d.ts +16 -0
  91. package/dist/providers/gemini.js +224 -0
  92. package/dist/providers/gemini.js.map +1 -0
  93. package/dist/providers/openai.d.ts +18 -0
  94. package/dist/providers/openai.js +8 -0
  95. package/dist/providers/openai.js.map +1 -0
  96. package/dist/providers/openrouter.d.ts +16 -0
  97. package/dist/providers/openrouter.js +23 -0
  98. package/dist/providers/openrouter.js.map +1 -0
  99. package/dist/providers/vertex.d.ts +40 -0
  100. package/dist/providers/vertex.js +64 -0
  101. package/dist/providers/vertex.js.map +1 -0
  102. package/dist/render-GRN4ZSSW.js +14 -0
  103. package/dist/render-GRN4ZSSW.js.map +1 -0
  104. package/dist/resolve-XM52G7YE.js +14 -0
  105. package/dist/resolve-XM52G7YE.js.map +1 -0
  106. package/dist/server/index.d.ts +128 -0
  107. package/dist/server/index.js +626 -0
  108. package/dist/server/index.js.map +1 -0
  109. package/dist/server-Cg1yWGaV.d.ts +96 -0
  110. package/dist/spinner-OJNR6NFO.js +8 -0
  111. package/dist/spinner-OJNR6NFO.js.map +1 -0
  112. package/dist/types-2kTLUCnD.d.ts +107 -0
  113. package/dist/types-3c88cRKH.d.ts +547 -0
  114. package/dist/types-CwKKucOF.d.ts +620 -0
  115. package/dist/types-DwdzmXfs.d.ts +107 -0
  116. package/dist/types-NIyVwQ4h.d.ts +109 -0
  117. package/dist/types-QwfylltH.d.ts +71 -0
  118. package/package.json +134 -6
@@ -0,0 +1,1994 @@
1
+ import {
2
+ IMAGE_EXTENSIONS,
3
+ compressImageBufferWithTokenLimit,
4
+ createImageMetadataText,
5
+ maybeResizeAndDownsampleImageBuffer
6
+ } from "./chunk-5GEX6ZSB.js";
7
+
8
+ // src/tools/tool-search.ts
9
+ var TOOL_SEARCH_NAME = "ToolSearch";
10
+ function isDeferredTool(tool) {
11
+ if (tool.alwaysLoad === true) return false;
12
+ if (tool.mcpInfo !== void 0) return true;
13
+ if (tool.name === TOOL_SEARCH_NAME) return false;
14
+ return tool.shouldDefer === true;
15
+ }
16
+ function formatDeferredToolLine(tool) {
17
+ const desc = tool.description.split(".")[0];
18
+ return `- ${tool.name}: ${desc}`;
19
+ }
20
+ function parseToolName(name) {
21
+ if (name.startsWith("mcp__") || name.includes("__")) {
22
+ const withoutPrefix = name.replace(/^mcp__/, "").toLowerCase();
23
+ const parts2 = withoutPrefix.split("__").flatMap((p) => p.split("_"));
24
+ return { parts: parts2.filter(Boolean), full: withoutPrefix.replace(/__/g, " ").replace(/_/g, " "), isMcp: true };
25
+ }
26
+ const parts = name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").toLowerCase().split(/\s+/).filter(Boolean);
27
+ return { parts, full: parts.join(" "), isMcp: false };
28
+ }
29
+ function escapeRegExp(s) {
30
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31
+ }
32
+ function searchToolsWithKeywords(query, deferredTools, allTools, maxResults) {
33
+ const queryLower = query.toLowerCase().trim();
34
+ const exactMatch = deferredTools.find((t) => t.name.toLowerCase() === queryLower) ?? allTools.find((t) => t.name.toLowerCase() === queryLower);
35
+ if (exactMatch) return [exactMatch.name];
36
+ if (queryLower.startsWith("mcp__") && queryLower.length > 5) {
37
+ const prefixMatches = deferredTools.filter((t) => t.name.toLowerCase().startsWith(queryLower)).slice(0, maxResults).map((t) => t.name);
38
+ if (prefixMatches.length > 0) return prefixMatches;
39
+ }
40
+ const queryTerms = queryLower.split(/\s+/).filter((t) => t.length > 0);
41
+ const requiredTerms = [];
42
+ const optionalTerms = [];
43
+ for (const term of queryTerms) {
44
+ if (term.startsWith("+") && term.length > 1) {
45
+ requiredTerms.push(term.slice(1));
46
+ } else {
47
+ optionalTerms.push(term);
48
+ }
49
+ }
50
+ const allScoringTerms = requiredTerms.length > 0 ? [...requiredTerms, ...optionalTerms] : queryTerms;
51
+ const termPatterns = /* @__PURE__ */ new Map();
52
+ for (const term of allScoringTerms) {
53
+ if (!termPatterns.has(term)) {
54
+ termPatterns.set(term, new RegExp(`\\b${escapeRegExp(term)}\\b`));
55
+ }
56
+ }
57
+ let candidates = deferredTools;
58
+ if (requiredTerms.length > 0) {
59
+ candidates = deferredTools.filter((tool) => {
60
+ const parsed = parseToolName(tool.name);
61
+ const descLower = tool.description.toLowerCase();
62
+ return requiredTerms.every((term) => {
63
+ const pattern = termPatterns.get(term);
64
+ return parsed.parts.includes(term) || parsed.parts.some((part) => part.includes(term)) || pattern.test(descLower);
65
+ });
66
+ });
67
+ }
68
+ const scored = candidates.map((tool) => {
69
+ const parsed = parseToolName(tool.name);
70
+ const descLower = tool.description.toLowerCase();
71
+ let score = 0;
72
+ for (const term of allScoringTerms) {
73
+ const pattern = termPatterns.get(term);
74
+ if (parsed.parts.includes(term)) {
75
+ score += parsed.isMcp ? 12 : 10;
76
+ } else if (parsed.parts.some((part) => part.includes(term))) {
77
+ score += parsed.isMcp ? 6 : 5;
78
+ }
79
+ if (parsed.full.includes(term) && score === 0) {
80
+ score += 3;
81
+ }
82
+ if (pattern.test(descLower)) {
83
+ score += 2;
84
+ }
85
+ }
86
+ return { name: tool.name, score };
87
+ });
88
+ return scored.filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, maxResults).map((item) => item.name);
89
+ }
90
+ function formatToolSchemas(tools) {
91
+ if (tools.length === 0) return "No matching deferred tools found.";
92
+ const lines = tools.map((t) => {
93
+ const schema = {
94
+ description: t.description,
95
+ name: t.name,
96
+ parameters: t.parameters
97
+ };
98
+ return `<function>${JSON.stringify(schema)}</function>`;
99
+ });
100
+ return `<functions>
101
+ ${lines.join("\n")}
102
+ </functions>`;
103
+ }
104
+ function createToolSearchTool(getDeferredTools, getAllTools, getToolsByNames, onDiscovered) {
105
+ return {
106
+ name: TOOL_SEARCH_NAME,
107
+ description: 'Fetches full schema definitions for deferred tools so they can be called. Deferred tools appear by name in <available-deferred-tools> sections. Until fetched, only the name is known \u2014 there is no parameter schema, so the tool cannot be invoked. Use this tool to load tool schemas.\n\nQuery forms:\n- "select:Read,Edit,Grep" \u2014 fetch these exact tools by name\n- "notebook jupyter" \u2014 keyword search, up to max_results best matches\n- "+slack send" \u2014 require "slack" in the name, rank by remaining terms',
108
+ isReadOnly: true,
109
+ isConcurrencySafe: true,
110
+ parameters: {
111
+ type: "object",
112
+ properties: {
113
+ query: {
114
+ type: "string",
115
+ description: 'Query to find deferred tools. Use "select:<tool_name>" for direct selection, or keywords to search.'
116
+ },
117
+ max_results: {
118
+ type: "number",
119
+ description: "Maximum number of results to return (default: 5)"
120
+ }
121
+ },
122
+ required: ["query"]
123
+ },
124
+ async call(args) {
125
+ const query = args.query;
126
+ const maxResults = args.max_results ?? 5;
127
+ const deferredTools = getDeferredTools();
128
+ const allTools = getAllTools();
129
+ const selectMatch = query.match(/^select:(.+)$/i);
130
+ if (selectMatch) {
131
+ const requested = selectMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
132
+ const found = [];
133
+ for (const toolName of requested) {
134
+ const match = deferredTools.find((t) => t.name.toLowerCase() === toolName.toLowerCase()) ?? allTools.find((t) => t.name.toLowerCase() === toolName.toLowerCase());
135
+ if (match && !found.includes(match.name)) {
136
+ found.push(match.name);
137
+ }
138
+ }
139
+ if (found.length === 0) {
140
+ return {
141
+ content: JSON.stringify({
142
+ matches: [],
143
+ query,
144
+ total_deferred_tools: deferredTools.length
145
+ })
146
+ };
147
+ }
148
+ onDiscovered(found);
149
+ const matchedTools2 = getToolsByNames(found);
150
+ return { content: formatToolSchemas(matchedTools2) };
151
+ }
152
+ const matches = searchToolsWithKeywords(query, deferredTools, allTools, maxResults);
153
+ if (matches.length === 0) {
154
+ return {
155
+ content: JSON.stringify({
156
+ matches: [],
157
+ query,
158
+ total_deferred_tools: deferredTools.length
159
+ })
160
+ };
161
+ }
162
+ onDiscovered(matches);
163
+ const matchedTools = getToolsByNames(matches);
164
+ return { content: formatToolSchemas(matchedTools) };
165
+ }
166
+ };
167
+ }
168
+
169
+ // src/utils/zod.ts
170
+ var cache = /* @__PURE__ */ new WeakMap();
171
+ function zodToJsonSchema(schema) {
172
+ const hit = cache.get(schema);
173
+ if (hit) return hit;
174
+ const zod = schema._zod ? schema : void 0;
175
+ if (!zod) {
176
+ throw new Error(
177
+ "zodToJsonSchema requires a Zod v4 schema. Install zod and pass a z.object(\u2026) schema."
178
+ );
179
+ }
180
+ let toJSONSchema;
181
+ try {
182
+ const sAny = schema;
183
+ if (typeof sAny._toJSONSchema === "function") {
184
+ const result = sAny._toJSONSchema();
185
+ cache.set(schema, result);
186
+ return result;
187
+ }
188
+ toJSONSchema = globalThis.__noumen_toJSONSchema;
189
+ } catch {
190
+ }
191
+ if (toJSONSchema) {
192
+ const result = toJSONSchema(schema);
193
+ cache.set(schema, result);
194
+ return result;
195
+ }
196
+ throw new Error(
197
+ "Could not convert Zod schema to JSON Schema. Call `registerZodToJsonSchema(toJSONSchema)` from zod/v4 or upgrade to Zod v4."
198
+ );
199
+ }
200
+ function registerZodToJsonSchema(fn) {
201
+ globalThis.__noumen_toJSONSchema = fn;
202
+ }
203
+ function formatZodValidationError(toolName, issues) {
204
+ if (!issues || !issues.issues.length) {
205
+ return `${toolName}: validation failed with unknown error`;
206
+ }
207
+ const parts = [];
208
+ const missing = issues.issues.filter(
209
+ (i) => i.code === "invalid_type" && i.message.includes("required")
210
+ );
211
+ const unrecognized = issues.issues.filter(
212
+ (i) => i.code === "unrecognized_keys"
213
+ );
214
+ const other = issues.issues.filter(
215
+ (i) => !missing.includes(i) && !unrecognized.includes(i)
216
+ );
217
+ if (missing.length) {
218
+ parts.push(
219
+ `Missing required parameter${missing.length > 1 ? "s" : ""}: ${missing.map((m) => formatPath(m.path)).join(", ")}`
220
+ );
221
+ }
222
+ if (unrecognized.length) {
223
+ parts.push(
224
+ `Unrecognized parameter${unrecognized.length > 1 ? "s" : ""}: ${unrecognized.map((u) => u.message).join(", ")}`
225
+ );
226
+ }
227
+ for (const issue of other) {
228
+ const path2 = formatPath(issue.path);
229
+ parts.push(`${path2 ? path2 + ": " : ""}${issue.message}`);
230
+ }
231
+ return `${toolName} failed due to the following ${parts.length > 1 ? "issues" : "issue"}:
232
+ ${parts.join("\n")}`;
233
+ }
234
+ function formatPath(path2) {
235
+ return path2.map((p, i) => typeof p === "number" ? `[${p}]` : i > 0 ? `.${p}` : p).join("");
236
+ }
237
+
238
+ // src/tools/prompts/read.ts
239
+ var READ_PROMPT = `Reads a file from the local filesystem. You can access any file directly by using this tool.
240
+ Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
241
+
242
+ Usage:
243
+ - The file_path parameter must be an absolute path, not a relative path.
244
+ - By default, it reads the entire file. Use offset and limit to read specific portions of large files.
245
+ - Lines in the output are numbered with the format: LINE_NUMBER|LINE_CONTENT
246
+ - If you read a file that exists but has empty contents you will receive a notice in place of file contents.
247
+ - This tool can read image files (e.g. PNG, JPG) when the provider supports multimodal input.
248
+ - This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs.
249
+ - This tool can only read files, not directories. To list a directory, use an ls command via the Bash tool.
250
+ - If the file has not changed since the last read, a "file_unchanged" result is returned to save context tokens.
251
+ `;
252
+
253
+ // src/tools/read.ts
254
+ import * as path from "path";
255
+ var DEFAULT_MAX_IMAGE_TOKENS = 1600;
256
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
257
+ var BLOCKED_DEVICE_PATHS = /* @__PURE__ */ new Set([
258
+ "/dev/zero",
259
+ "/dev/random",
260
+ "/dev/urandom",
261
+ "/dev/full",
262
+ "/dev/stdin",
263
+ "/dev/tty",
264
+ "/dev/console",
265
+ "/dev/stdout",
266
+ "/dev/stderr",
267
+ "/dev/fd/0",
268
+ "/dev/fd/1",
269
+ "/dev/fd/2"
270
+ ]);
271
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
272
+ ".exe",
273
+ ".dll",
274
+ ".so",
275
+ ".dylib",
276
+ ".bin",
277
+ ".zip",
278
+ ".tar",
279
+ ".gz",
280
+ ".bz2",
281
+ ".xz",
282
+ ".7z",
283
+ ".rar",
284
+ ".wasm",
285
+ ".o",
286
+ ".a",
287
+ ".obj",
288
+ ".lib",
289
+ ".class",
290
+ ".pyc",
291
+ ".pyo",
292
+ ".jar",
293
+ ".war",
294
+ ".ear",
295
+ ".iso",
296
+ ".img",
297
+ ".dmg",
298
+ ".msi",
299
+ ".deb",
300
+ ".rpm",
301
+ ".apk",
302
+ ".ipa"
303
+ ]);
304
+ var readFileTool = {
305
+ name: "ReadFile",
306
+ description: "Read a file from the filesystem. Returns the file content with line numbers. For image files (.png, .jpg, .jpeg, .gif, .webp), returns the image data directly. Use offset and limit to read specific portions of large text files.",
307
+ prompt: READ_PROMPT,
308
+ isReadOnly: true,
309
+ isConcurrencySafe: true,
310
+ parameters: {
311
+ type: "object",
312
+ properties: {
313
+ file_path: {
314
+ type: "string",
315
+ description: "The path of the file to read (absolute or relative to cwd)"
316
+ },
317
+ offset: {
318
+ type: "number",
319
+ description: "Line number to start reading from (1-indexed). Defaults to 1.",
320
+ minimum: 1
321
+ },
322
+ limit: {
323
+ type: "number",
324
+ description: "Maximum number of lines to read. If omitted, reads entire file.",
325
+ minimum: 1
326
+ }
327
+ },
328
+ required: ["file_path"]
329
+ },
330
+ async call(args, ctx) {
331
+ const filePath = args.file_path;
332
+ const offset = args.offset ?? 1;
333
+ const limit = args.limit;
334
+ try {
335
+ const resolved = path.resolve(ctx.cwd, filePath);
336
+ if (BLOCKED_DEVICE_PATHS.has(resolved)) {
337
+ return {
338
+ content: `Error: Cannot read device file ${filePath}.`,
339
+ isError: true
340
+ };
341
+ }
342
+ const ext = path.extname(filePath).toLowerCase();
343
+ if (BINARY_EXTENSIONS.has(ext)) {
344
+ return {
345
+ content: `Error: Cannot read binary ${ext} file. This tool only reads text files.`,
346
+ isError: true
347
+ };
348
+ }
349
+ if (IMAGE_EXTENSIONS.has(ext) && ctx.fs.readFileBytes) {
350
+ return readImageFile(filePath, ext, ctx);
351
+ }
352
+ try {
353
+ const stat = await ctx.fs.stat(filePath);
354
+ if (stat.size !== void 0 && stat.size > MAX_FILE_SIZE) {
355
+ return {
356
+ content: `Error: File is too large (${Math.round(stat.size / 1024 / 1024)}MB). Use offset/limit to read specific portions.`,
357
+ isError: true
358
+ };
359
+ }
360
+ } catch {
361
+ }
362
+ if (ctx.fileStateCache) {
363
+ const cached = ctx.fileStateCache.get(filePath);
364
+ if (cached && !cached.isPartialView && cached.offset !== void 0 && cached.offset === offset && cached.limit === limit) {
365
+ try {
366
+ const stat = await ctx.fs.stat(filePath);
367
+ const mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
368
+ if (mtime === cached.timestamp) {
369
+ return { content: "file_unchanged" };
370
+ }
371
+ } catch {
372
+ }
373
+ }
374
+ }
375
+ const content = await ctx.fs.readFile(filePath);
376
+ const lines = content.split("\n");
377
+ const startIdx = Math.max(0, offset - 1);
378
+ const endIdx = limit ? Math.min(lines.length, startIdx + limit) : lines.length;
379
+ const selectedLines = lines.slice(startIdx, endIdx);
380
+ const numbered = selectedLines.map(
381
+ (line, i) => `${String(startIdx + i + 1).padStart(6)}|${line}`
382
+ );
383
+ let result = numbered.join("\n");
384
+ if (endIdx < lines.length) {
385
+ result += `
386
+ ... ${lines.length - endIdx} lines not shown ...`;
387
+ }
388
+ if (ctx.fileStateCache) {
389
+ let mtime = 0;
390
+ try {
391
+ const stat = await ctx.fs.stat(filePath);
392
+ mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
393
+ } catch {
394
+ }
395
+ ctx.fileStateCache.set(filePath, {
396
+ content: selectedLines.join("\n"),
397
+ timestamp: mtime,
398
+ offset,
399
+ limit
400
+ });
401
+ }
402
+ return { content: result || "File is empty." };
403
+ } catch (err) {
404
+ return {
405
+ content: `Error reading file: ${err instanceof Error ? err.message : String(err)}`,
406
+ isError: true
407
+ };
408
+ }
409
+ }
410
+ };
411
+ async function readImageFile(filePath, ext, ctx) {
412
+ const imageBuffer = await ctx.fs.readFileBytes(filePath);
413
+ const originalSize = imageBuffer.length;
414
+ const formatExt = ext.replace(/^\./, "");
415
+ const resized = await maybeResizeAndDownsampleImageBuffer(
416
+ imageBuffer,
417
+ originalSize,
418
+ formatExt
419
+ );
420
+ let base64 = resized.buffer.toString("base64");
421
+ let mediaType = resized.mediaType;
422
+ const estimatedTokens = Math.ceil(base64.length * 0.125);
423
+ if (estimatedTokens > DEFAULT_MAX_IMAGE_TOKENS) {
424
+ try {
425
+ const compressed = await compressImageBufferWithTokenLimit(
426
+ imageBuffer,
427
+ DEFAULT_MAX_IMAGE_TOKENS,
428
+ `image/${formatExt}`
429
+ );
430
+ base64 = compressed.base64;
431
+ mediaType = compressed.mediaType;
432
+ } catch {
433
+ }
434
+ }
435
+ const parts = [
436
+ {
437
+ type: "image",
438
+ data: base64,
439
+ media_type: `image/${mediaType}`
440
+ }
441
+ ];
442
+ if (resized.dimensions) {
443
+ parts.push({
444
+ type: "text",
445
+ text: createImageMetadataText(resized.dimensions)
446
+ });
447
+ }
448
+ return { content: parts };
449
+ }
450
+
451
+ // src/tools/prompts/write.ts
452
+ var WRITE_PROMPT = `Writes a file to the local filesystem. Parent directories are created automatically if they don't exist.
453
+
454
+ Usage:
455
+ - This tool will overwrite the existing file if there is one at the provided path.
456
+ - If this is an existing file, you MUST use the ReadFile tool first to read the file's contents. This tool will fail if you did not read the file first.
457
+ - Prefer the EditFile tool for modifying existing files \u2014 it only sends the diff. Only use this tool to create new files or for complete rewrites.
458
+ - NEVER create documentation files (*.md) or README files unless explicitly requested by the User.
459
+ - Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
460
+ `;
461
+
462
+ // src/tools/write.ts
463
+ var writeFileTool = {
464
+ name: "WriteFile",
465
+ description: "Create or overwrite a file with the given content. Parent directories are created automatically if they don't exist.",
466
+ prompt: WRITE_PROMPT,
467
+ isReadOnly: false,
468
+ checkPermissions(args) {
469
+ const filePath = args.file_path;
470
+ return {
471
+ behavior: "passthrough",
472
+ message: `Write to ${filePath}`
473
+ };
474
+ },
475
+ parameters: {
476
+ type: "object",
477
+ properties: {
478
+ file_path: {
479
+ type: "string",
480
+ description: "The path of the file to write (absolute or relative to cwd)"
481
+ },
482
+ content: {
483
+ type: "string",
484
+ description: "The content to write to the file"
485
+ }
486
+ },
487
+ required: ["file_path", "content"]
488
+ },
489
+ async call(args, ctx) {
490
+ const filePath = args.file_path;
491
+ const content = args.content;
492
+ try {
493
+ if (ctx.checkpointManager && ctx.currentMessageId) {
494
+ await ctx.checkpointManager.trackEdit(filePath, ctx.currentMessageId, ctx.sessionId ?? "");
495
+ }
496
+ const existed = await ctx.fs.exists(filePath);
497
+ if (existed && ctx.fileStateCache) {
498
+ const cached = ctx.fileStateCache.get(filePath);
499
+ if (!cached) {
500
+ return {
501
+ content: `Error: File ${filePath} exists but has not been read yet. Read it first before overwriting.`,
502
+ isError: true
503
+ };
504
+ }
505
+ }
506
+ await ctx.fs.writeFile(filePath, content);
507
+ ctx.notifyHook?.("FileWrite", {
508
+ event: "FileWrite",
509
+ sessionId: ctx.sessionId ?? "",
510
+ toolName: "WriteFile",
511
+ filePath,
512
+ isNew: !existed
513
+ }).catch(() => {
514
+ });
515
+ if (ctx.fileStateCache) {
516
+ let mtime = 0;
517
+ try {
518
+ const stat = await ctx.fs.stat(filePath);
519
+ mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
520
+ } catch {
521
+ }
522
+ ctx.fileStateCache.set(filePath, {
523
+ content,
524
+ timestamp: mtime
525
+ });
526
+ }
527
+ return {
528
+ content: existed ? `File updated successfully at: ${filePath}` : `File created successfully at: ${filePath}`
529
+ };
530
+ } catch (err) {
531
+ return {
532
+ content: `Error writing file: ${err instanceof Error ? err.message : String(err)}`,
533
+ isError: true
534
+ };
535
+ }
536
+ }
537
+ };
538
+
539
+ // src/tools/edit-utils.ts
540
+ var LEFT_SINGLE_CURLY = "\u2018";
541
+ var RIGHT_SINGLE_CURLY = "\u2019";
542
+ var LEFT_DOUBLE_CURLY = "\u201C";
543
+ var RIGHT_DOUBLE_CURLY = "\u201D";
544
+ function normalizeQuotes(str) {
545
+ return str.replaceAll(LEFT_SINGLE_CURLY, "'").replaceAll(RIGHT_SINGLE_CURLY, "'").replaceAll(LEFT_DOUBLE_CURLY, '"').replaceAll(RIGHT_DOUBLE_CURLY, '"');
546
+ }
547
+ function findActualString(fileContent, searchString) {
548
+ if (fileContent.includes(searchString)) {
549
+ return searchString;
550
+ }
551
+ const normalizedSearch = normalizeQuotes(searchString);
552
+ const normalizedFile = normalizeQuotes(fileContent);
553
+ const searchIndex = normalizedFile.indexOf(normalizedSearch);
554
+ if (searchIndex !== -1) {
555
+ return fileContent.substring(searchIndex, searchIndex + searchString.length);
556
+ }
557
+ return null;
558
+ }
559
+ function countOccurrences(haystack, needle) {
560
+ const normalizedNeedle = normalizeQuotes(needle);
561
+ const normalizedHaystack = normalizeQuotes(haystack);
562
+ let count = 0;
563
+ let pos = 0;
564
+ while (true) {
565
+ const idx = normalizedHaystack.indexOf(normalizedNeedle, pos);
566
+ if (idx === -1) break;
567
+ count++;
568
+ pos = idx + 1;
569
+ }
570
+ return count;
571
+ }
572
+ function usesCurlyQuotes(str) {
573
+ return {
574
+ singleCurly: str.includes(LEFT_SINGLE_CURLY) || str.includes(RIGHT_SINGLE_CURLY),
575
+ doubleCurly: str.includes(LEFT_DOUBLE_CURLY) || str.includes(RIGHT_DOUBLE_CURLY)
576
+ };
577
+ }
578
+ function preserveQuoteStyle(oldString, actualOldString, newString) {
579
+ if (oldString === actualOldString) {
580
+ return newString;
581
+ }
582
+ const fileStyle = usesCurlyQuotes(actualOldString);
583
+ let result = newString;
584
+ if (fileStyle.singleCurly) {
585
+ result = convertStraightToCurlySingle(result);
586
+ }
587
+ if (fileStyle.doubleCurly) {
588
+ result = convertStraightToCurlyDouble(result);
589
+ }
590
+ return result;
591
+ }
592
+ function convertStraightToCurlySingle(str) {
593
+ let result = "";
594
+ let inWord = false;
595
+ for (let i = 0; i < str.length; i++) {
596
+ const ch = str[i];
597
+ if (ch === "'") {
598
+ const prev = i > 0 ? str[i - 1] : " ";
599
+ if (/\s/.test(prev) || prev === "(" || prev === "[" || prev === "{") {
600
+ result += LEFT_SINGLE_CURLY;
601
+ inWord = true;
602
+ } else {
603
+ result += RIGHT_SINGLE_CURLY;
604
+ inWord = false;
605
+ }
606
+ } else {
607
+ result += ch;
608
+ inWord = /\w/.test(ch);
609
+ }
610
+ }
611
+ return result;
612
+ }
613
+ function convertStraightToCurlyDouble(str) {
614
+ let result = "";
615
+ let open = true;
616
+ for (let i = 0; i < str.length; i++) {
617
+ const ch = str[i];
618
+ if (ch === '"') {
619
+ result += open ? LEFT_DOUBLE_CURLY : RIGHT_DOUBLE_CURLY;
620
+ open = !open;
621
+ } else {
622
+ result += ch;
623
+ }
624
+ }
625
+ return result;
626
+ }
627
+ function stripTrailingWhitespace(str) {
628
+ const parts = str.split(/(\r\n|\n|\r)/);
629
+ const result = [];
630
+ for (let i = 0; i < parts.length; i++) {
631
+ const part = parts[i];
632
+ if (i % 2 === 0) {
633
+ result.push(part.replace(/[\t ]+$/, ""));
634
+ } else {
635
+ result.push(part);
636
+ }
637
+ }
638
+ return result.join("");
639
+ }
640
+
641
+ // src/tools/prompts/edit.ts
642
+ var EDIT_PROMPT = `Performs exact string replacements in files.
643
+
644
+ Usage:
645
+ - You must use the ReadFile tool at least once before editing a file. This tool will error if you attempt an edit without reading the file first.
646
+ - When editing text from ReadFile output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + pipe. Everything after that is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
647
+ - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
648
+ - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
649
+ - The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
650
+ - Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
651
+ `;
652
+
653
+ // src/tools/edit.ts
654
+ var editFileTool = {
655
+ name: "EditFile",
656
+ description: "Edit a file by replacing an exact string match with new content. The old_string must match exactly (including whitespace and indentation). Set replace_all to true to replace all occurrences.",
657
+ prompt: EDIT_PROMPT,
658
+ isReadOnly: false,
659
+ checkPermissions(args) {
660
+ const filePath = args.file_path;
661
+ return {
662
+ behavior: "passthrough",
663
+ message: `Edit ${filePath}`
664
+ };
665
+ },
666
+ parameters: {
667
+ type: "object",
668
+ properties: {
669
+ file_path: {
670
+ type: "string",
671
+ description: "The path of the file to edit"
672
+ },
673
+ old_string: {
674
+ type: "string",
675
+ description: "The exact string to find and replace"
676
+ },
677
+ new_string: {
678
+ type: "string",
679
+ description: "The replacement string"
680
+ },
681
+ replace_all: {
682
+ type: "boolean",
683
+ description: "If true, replace all occurrences of old_string. Defaults to false."
684
+ }
685
+ },
686
+ required: ["file_path", "old_string", "new_string"]
687
+ },
688
+ async call(args, ctx) {
689
+ const filePath = args.file_path;
690
+ const oldString = args.old_string;
691
+ const newString = args.new_string;
692
+ const replaceAll = args.replace_all ?? false;
693
+ if (filePath.endsWith(".ipynb")) {
694
+ return {
695
+ content: `Error: ${filePath} is a Jupyter Notebook. Use the NotebookEdit tool to edit notebook files.`,
696
+ isError: true
697
+ };
698
+ }
699
+ if (oldString === newString) {
700
+ return {
701
+ content: "No changes to make: old_string and new_string are exactly the same.",
702
+ isError: true
703
+ };
704
+ }
705
+ try {
706
+ if (ctx.fileStateCache) {
707
+ const cached = ctx.fileStateCache.get(filePath);
708
+ if (!cached || cached.isPartialView) {
709
+ return {
710
+ content: `Error: File has not been read yet. Use ReadFile on ${filePath} before editing.`,
711
+ isError: true
712
+ };
713
+ }
714
+ try {
715
+ const stat = await ctx.fs.stat(filePath);
716
+ const mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
717
+ if (mtime > cached.timestamp) {
718
+ const currentContent = await ctx.fs.readFile(filePath);
719
+ if (currentContent !== cached.content) {
720
+ return {
721
+ content: `Error: ${filePath} has been modified since last read. Re-read the file before editing.`,
722
+ isError: true
723
+ };
724
+ }
725
+ }
726
+ } catch {
727
+ }
728
+ }
729
+ if (ctx.checkpointManager && ctx.currentMessageId) {
730
+ await ctx.checkpointManager.trackEdit(filePath, ctx.currentMessageId, ctx.sessionId ?? "");
731
+ }
732
+ const content = await ctx.fs.readFile(filePath);
733
+ const actualOldString = findActualString(content, oldString);
734
+ if (!actualOldString) {
735
+ return {
736
+ content: `Error: old_string not found in ${filePath}. Make sure the string matches exactly, including whitespace and indentation.`,
737
+ isError: true
738
+ };
739
+ }
740
+ if (!replaceAll) {
741
+ const count = content.split(actualOldString).length - 1;
742
+ if (count > 1) {
743
+ return {
744
+ content: `Error: old_string appears ${count} times in ${filePath}. Provide more context to make it unique, or set replace_all to true.`,
745
+ isError: true
746
+ };
747
+ }
748
+ }
749
+ const actualNewString = preserveQuoteStyle(oldString, actualOldString, newString);
750
+ let updated;
751
+ if (replaceAll) {
752
+ updated = content.split(actualOldString).join(actualNewString);
753
+ } else if (actualNewString === "") {
754
+ const hasTrailingNewline = !actualOldString.endsWith("\n") && content.includes(actualOldString + "\n");
755
+ const deleteTarget = hasTrailingNewline ? actualOldString + "\n" : actualOldString;
756
+ updated = content.replace(deleteTarget, () => actualNewString);
757
+ } else {
758
+ updated = content.replace(actualOldString, () => actualNewString);
759
+ }
760
+ await ctx.fs.writeFile(filePath, updated);
761
+ ctx.notifyHook?.("FileWrite", {
762
+ event: "FileWrite",
763
+ sessionId: ctx.sessionId ?? "",
764
+ toolName: "EditFile",
765
+ filePath,
766
+ isNew: false
767
+ }).catch(() => {
768
+ });
769
+ if (ctx.fileStateCache) {
770
+ let mtime = 0;
771
+ try {
772
+ const stat = await ctx.fs.stat(filePath);
773
+ mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
774
+ } catch {
775
+ }
776
+ ctx.fileStateCache.set(filePath, {
777
+ content: updated,
778
+ timestamp: mtime
779
+ });
780
+ }
781
+ return {
782
+ content: `File ${filePath} has been updated successfully.`
783
+ };
784
+ } catch (err) {
785
+ return {
786
+ content: `Error editing file: ${err instanceof Error ? err.message : String(err)}`,
787
+ isError: true
788
+ };
789
+ }
790
+ }
791
+ };
792
+
793
+ // src/tools/shell-safety/git-safety.ts
794
+ var GIT_INTERNAL_PATTERNS = [
795
+ /\.git\/hooks\//,
796
+ /\.git\/config$/,
797
+ /\.git\/info\//,
798
+ /\.git\/objects\//,
799
+ /\.git\/refs\//,
800
+ /\.git\/HEAD$/,
801
+ /\.git\/index$/,
802
+ /\.git\/packed-refs$/,
803
+ /\.git\/shallow$/,
804
+ /\.git\/modules\//
805
+ ];
806
+ function isGitInternalPath(path2) {
807
+ const normalized = path2.replace(/\\/g, "/");
808
+ return GIT_INTERNAL_PATTERNS.some((p) => p.test(normalized));
809
+ }
810
+ var BARE_REPO_MARKERS = ["HEAD", "objects", "refs"];
811
+ function looksLikeBareRepo(dirEntries) {
812
+ const entrySet = new Set(dirEntries.map((e) => e.replace(/\/$/, "")));
813
+ if (entrySet.has(".git")) return false;
814
+ return BARE_REPO_MARKERS.every((m) => entrySet.has(m));
815
+ }
816
+ function commandWritesGitInternals(command) {
817
+ const redirectPattern = /(?:>{1,2}|tee\s+)\s*(\S+)/g;
818
+ let match;
819
+ while ((match = redirectPattern.exec(command)) !== null) {
820
+ if (isGitInternalPath(match[1])) return true;
821
+ }
822
+ const copyPattern = /\b(?:cp|mv|ln)\b.*\s(\S*\.git\/\S+)/;
823
+ const copyMatch = command.match(copyPattern);
824
+ if (copyMatch && isGitInternalPath(copyMatch[1])) return true;
825
+ return false;
826
+ }
827
+
828
+ // src/tools/shell-safety/command-classification.ts
829
+ var READ_ONLY_COMMANDS = /* @__PURE__ */ new Set([
830
+ "cat",
831
+ "head",
832
+ "tail",
833
+ "less",
834
+ "more",
835
+ "wc",
836
+ "file",
837
+ "which",
838
+ "whence",
839
+ "where",
840
+ "whereis",
841
+ "type",
842
+ "pwd",
843
+ "date",
844
+ "uname",
845
+ "hostname",
846
+ "whoami",
847
+ "id",
848
+ "groups",
849
+ "ls",
850
+ "ll",
851
+ "la",
852
+ "dir",
853
+ "tree",
854
+ "stat",
855
+ "du",
856
+ "df",
857
+ "free",
858
+ "uptime",
859
+ "ps",
860
+ "top",
861
+ "htop",
862
+ "lsof",
863
+ "ss",
864
+ "netstat",
865
+ "ifconfig",
866
+ "ip",
867
+ "ping",
868
+ "dig",
869
+ "nslookup",
870
+ "host",
871
+ "traceroute",
872
+ "grep",
873
+ "egrep",
874
+ "fgrep",
875
+ "rg",
876
+ "ag",
877
+ "ack",
878
+ "find",
879
+ "fd",
880
+ "fdfind",
881
+ "locate",
882
+ "readlink",
883
+ "realpath",
884
+ "basename",
885
+ "dirname",
886
+ "diff",
887
+ "comm",
888
+ "sort",
889
+ "uniq",
890
+ "cut",
891
+ "tr",
892
+ "awk",
893
+ "sed",
894
+ // sed -i is destructive but caught by destructive patterns
895
+ "jq",
896
+ "yq",
897
+ "xxd",
898
+ "hexdump",
899
+ "od",
900
+ "md5sum",
901
+ "sha256sum",
902
+ "shasum",
903
+ "base64",
904
+ "true",
905
+ "false",
906
+ "test",
907
+ "[",
908
+ "[[",
909
+ "man",
910
+ "help",
911
+ "info",
912
+ "nproc",
913
+ "arch",
914
+ "lscpu",
915
+ "lsb_release",
916
+ "sw_vers",
917
+ "sysctl",
918
+ "getconf",
919
+ "dotnet"
920
+ // dotnet --info, dotnet --list-sdks
921
+ ]);
922
+ var GIT_READ_ONLY_SUBCOMMANDS = /* @__PURE__ */ new Set([
923
+ "status",
924
+ "log",
925
+ "diff",
926
+ "show",
927
+ "blame",
928
+ "shortlog",
929
+ "describe",
930
+ "rev-parse",
931
+ "rev-list",
932
+ "cat-file",
933
+ "ls-files",
934
+ "ls-tree",
935
+ "ls-remote",
936
+ "name-rev",
937
+ "for-each-ref",
938
+ "count-objects",
939
+ "fsck",
940
+ "verify-pack",
941
+ "reflog",
942
+ "stash",
943
+ // "stash list" / "stash show" — stash apply/pop are not here
944
+ "tag",
945
+ // "tag -l" is safe; "tag <name>" creates — caught below
946
+ "branch",
947
+ // "branch --list" is safe; "branch <name>" creates — caught below
948
+ "remote",
949
+ // "remote -v" safe; "remote add/remove" — caught below
950
+ "config",
951
+ // "config --list/--get" safe
952
+ "help",
953
+ "version",
954
+ "--version",
955
+ "--help"
956
+ ]);
957
+ var GIT_MUTATING_SUBCOMMANDS = /* @__PURE__ */ new Set([
958
+ "push",
959
+ "pull",
960
+ "fetch",
961
+ "merge",
962
+ "rebase",
963
+ "cherry-pick",
964
+ "revert",
965
+ "commit",
966
+ "add",
967
+ "rm",
968
+ "mv",
969
+ "init",
970
+ "clone",
971
+ "checkout",
972
+ "switch",
973
+ "restore",
974
+ "reset",
975
+ "clean",
976
+ "bisect",
977
+ "am",
978
+ "apply",
979
+ "format-patch",
980
+ "submodule",
981
+ "worktree"
982
+ ]);
983
+ var DESTRUCTIVE_PATTERNS = [
984
+ // rm -rf / rm -r / rm --recursive (but not plain rm single-file)
985
+ /\brm\s+(-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)\b/,
986
+ // rm on root-like paths
987
+ /\brm\s+.*\s+\/($|\s)/,
988
+ // git force operations
989
+ /\bgit\s+push\s+.*--force\b/,
990
+ /\bgit\s+push\s+-f\b/,
991
+ /\bgit\s+reset\s+--hard\b/,
992
+ /\bgit\s+clean\s+.*-[a-zA-Z]*f/,
993
+ /\bgit\s+checkout\s+--\s+\./,
994
+ // Filesystem destruction
995
+ /\bchmod\s+(-[a-zA-Z]*R[a-zA-Z]*|--recursive)\s+777\b/,
996
+ /\bchown\s+(-[a-zA-Z]*R[a-zA-Z]*|--recursive)\b/,
997
+ /\bdd\s+/,
998
+ /\bmkfs\b/,
999
+ /\bformat\b/,
1000
+ /\bfdisk\b/,
1001
+ // Dangerous redirects
1002
+ />\s*\/dev\/sd[a-z]/,
1003
+ // Database destructive operations
1004
+ /\bDROP\s+(TABLE|DATABASE|SCHEMA)\b/i,
1005
+ /\bTRUNCATE\s+TABLE\b/i,
1006
+ /\bDELETE\s+FROM\b/i,
1007
+ // sed in-place
1008
+ /\bsed\s+(-[a-zA-Z]*i[a-zA-Z]*|--in-place)\b/,
1009
+ // Container/system destruction
1010
+ /\bdocker\s+(rm|rmi|system\s+prune|volume\s+rm)\b/,
1011
+ /\bkubectl\s+delete\b/,
1012
+ // Kill processes
1013
+ /\bkill\s+-9\b/,
1014
+ /\bkillall\b/,
1015
+ /\bpkill\b/,
1016
+ // Recursive operations on root
1017
+ /\bfind\s+\/\s+.*-delete\b/,
1018
+ /\bfind\s+\/\s+.*-exec\s+rm\b/
1019
+ ];
1020
+ var SAFE_ECHO_RE = /^(?:echo|printf)(?:\s+(?:'[^']*'|[^|;&`$(){}><#\\!"'\s]+))*(?:\s+2>&1)?\s*$/;
1021
+ function hasTokenFlag(tokens, ...flags) {
1022
+ return tokens.some((t) => flags.includes(t));
1023
+ }
1024
+ function splitCompoundCommand(command) {
1025
+ return command.split(/\s*(?:;|&&|\|\||(?<!\|)\|(?!\|))\s*/).map((s) => s.trim()).filter(Boolean);
1026
+ }
1027
+ function stripPrefixes(command) {
1028
+ let cmd = command.trim();
1029
+ while (/^[A-Za-z_][A-Za-z0-9_]*=\S*\s/.test(cmd)) {
1030
+ cmd = cmd.replace(/^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/, "");
1031
+ }
1032
+ for (const prefix of ["sudo", "env", "nohup", "time", "nice", "ionice", "strace", "ltrace"]) {
1033
+ if (cmd.startsWith(prefix + " ")) {
1034
+ cmd = cmd.slice(prefix.length).trim();
1035
+ while (cmd.startsWith("-")) {
1036
+ const spaceIdx = cmd.indexOf(" ");
1037
+ if (spaceIdx === -1) break;
1038
+ cmd = cmd.slice(spaceIdx).trim();
1039
+ }
1040
+ while (/^[A-Za-z_][A-Za-z0-9_]*=\S*\s/.test(cmd)) {
1041
+ cmd = cmd.replace(/^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/, "");
1042
+ }
1043
+ }
1044
+ }
1045
+ return cmd;
1046
+ }
1047
+ function extractCommandName(command) {
1048
+ const cmd = stripPrefixes(command);
1049
+ const firstToken = cmd.split(/\s/)[0] ?? "";
1050
+ const base = firstToken.includes("/") ? firstToken.split("/").pop() : firstToken;
1051
+ return base;
1052
+ }
1053
+ function classifyGitCommand(command) {
1054
+ if (/\bgit\s+--version\b/.test(command)) {
1055
+ return { isReadOnly: true, isDestructive: false, reason: "git --version is read-only" };
1056
+ }
1057
+ if (/\bgit\s+--help\b/.test(command)) {
1058
+ return { isReadOnly: true, isDestructive: false, reason: "git --help is read-only" };
1059
+ }
1060
+ if (/\bgit\s+(-c\s|--exec-path=|--config-env=)/.test(command)) {
1061
+ return { isReadOnly: false, isDestructive: true, reason: "git config injection vector (-c/--exec-path/--config-env)" };
1062
+ }
1063
+ const match = command.match(/\bgit\s+(?:--[a-z-]+=?\S*\s+)*([a-z][a-z-]*)/);
1064
+ if (!match) {
1065
+ return { isReadOnly: false, isDestructive: false, reason: "Cannot parse git subcommand" };
1066
+ }
1067
+ const subcommand = match[1];
1068
+ if (GIT_READ_ONLY_SUBCOMMANDS.has(subcommand)) {
1069
+ const afterSubcmd = command.slice(command.indexOf(subcommand) + subcommand.length).trim();
1070
+ const tokens = afterSubcmd.split(/\s+/).filter(Boolean);
1071
+ const positional = tokens.filter((t) => !t.startsWith("-"));
1072
+ const flags = tokens.filter((t) => t.startsWith("-"));
1073
+ if (subcommand === "branch") {
1074
+ if (hasTokenFlag(flags, "--list", "-l")) {
1075
+ return { isReadOnly: true, isDestructive: false, reason: "git branch --list is read-only" };
1076
+ }
1077
+ if (hasTokenFlag(flags, "-d", "-D", "--delete")) {
1078
+ return { isReadOnly: false, isDestructive: true, reason: "git branch delete" };
1079
+ }
1080
+ if (positional.length > 0) {
1081
+ return { isReadOnly: false, isDestructive: false, reason: "git branch create" };
1082
+ }
1083
+ }
1084
+ if (subcommand === "tag") {
1085
+ if (hasTokenFlag(flags, "-l", "--list")) {
1086
+ return { isReadOnly: true, isDestructive: false, reason: "git tag --list is read-only" };
1087
+ }
1088
+ if (hasTokenFlag(flags, "-d", "-D", "--delete")) {
1089
+ return { isReadOnly: false, isDestructive: true, reason: "git tag delete" };
1090
+ }
1091
+ if (positional.length > 0) {
1092
+ return { isReadOnly: false, isDestructive: false, reason: "git tag create" };
1093
+ }
1094
+ }
1095
+ if (subcommand === "stash") {
1096
+ const stashSubcmd = positional[0];
1097
+ if (stashSubcmd === "list" || stashSubcmd === "show") {
1098
+ return { isReadOnly: true, isDestructive: false, reason: `git stash ${stashSubcmd} is read-only` };
1099
+ }
1100
+ if (stashSubcmd === "drop" || stashSubcmd === "clear") {
1101
+ return { isReadOnly: false, isDestructive: true, reason: "git stash destructive operation" };
1102
+ }
1103
+ return { isReadOnly: false, isDestructive: false, reason: "git stash mutating operation" };
1104
+ }
1105
+ if (subcommand === "config") {
1106
+ if (hasTokenFlag(flags, "--set", "--add", "--unset", "--unset-all", "--replace-all", "--rename-section", "--remove-section")) {
1107
+ return { isReadOnly: false, isDestructive: false, reason: "git config write operation" };
1108
+ }
1109
+ if (positional.length >= 2) {
1110
+ return { isReadOnly: false, isDestructive: false, reason: "git config set key value" };
1111
+ }
1112
+ }
1113
+ if (subcommand === "remote") {
1114
+ const remoteSubcmd = positional[0];
1115
+ if (remoteSubcmd && ["add", "remove", "rename", "set-url", "set-branches", "prune"].includes(remoteSubcmd)) {
1116
+ return { isReadOnly: false, isDestructive: false, reason: "git remote mutating operation" };
1117
+ }
1118
+ }
1119
+ return { isReadOnly: true, isDestructive: false, reason: `git ${subcommand} is read-only` };
1120
+ }
1121
+ if (GIT_MUTATING_SUBCOMMANDS.has(subcommand)) {
1122
+ for (const pattern of DESTRUCTIVE_PATTERNS) {
1123
+ if (pattern.test(command)) {
1124
+ return { isReadOnly: false, isDestructive: true, reason: `Destructive: ${pattern.source}` };
1125
+ }
1126
+ }
1127
+ return { isReadOnly: false, isDestructive: false, reason: `git ${subcommand} is mutating` };
1128
+ }
1129
+ return { isReadOnly: false, isDestructive: false, reason: `Unknown git subcommand: ${subcommand}` };
1130
+ }
1131
+ function classifySingleCommand(command, config) {
1132
+ const name = extractCommandName(command);
1133
+ if (!name) {
1134
+ return { isReadOnly: false, isDestructive: false, reason: "Empty command" };
1135
+ }
1136
+ const allDestructive = [
1137
+ ...DESTRUCTIVE_PATTERNS,
1138
+ ...config?.extraDestructivePatterns ?? []
1139
+ ];
1140
+ for (const pattern of allDestructive) {
1141
+ if (pattern.test(command)) {
1142
+ return {
1143
+ isReadOnly: false,
1144
+ isDestructive: true,
1145
+ reason: `Matches destructive pattern: ${pattern.source}`
1146
+ };
1147
+ }
1148
+ }
1149
+ if (name === "git") {
1150
+ return classifyGitCommand(command);
1151
+ }
1152
+ if (name === "xargs" && /\bgit\b/.test(command)) {
1153
+ return classifyGitCommand(command);
1154
+ }
1155
+ if ((name === "echo" || name === "printf") && SAFE_ECHO_RE.test(stripPrefixes(command).trim())) {
1156
+ return { isReadOnly: true, isDestructive: false, reason: `${name} with safe arguments is read-only` };
1157
+ }
1158
+ const extraReadOnly = new Set(config?.extraReadOnlyCommands ?? []);
1159
+ if (READ_ONLY_COMMANDS.has(name) || extraReadOnly.has(name)) {
1160
+ return { isReadOnly: true, isDestructive: false, reason: `${name} is read-only` };
1161
+ }
1162
+ return {
1163
+ isReadOnly: false,
1164
+ isDestructive: false,
1165
+ reason: `${name} is not in the read-only allowlist`
1166
+ };
1167
+ }
1168
+ function classifyCommand(command, config) {
1169
+ if (!command.trim()) {
1170
+ return { isReadOnly: true, isDestructive: false, reason: "Empty command" };
1171
+ }
1172
+ const subCommands = splitCompoundCommand(command);
1173
+ if (subCommands.length === 0) {
1174
+ return { isReadOnly: true, isDestructive: false, reason: "Empty command" };
1175
+ }
1176
+ if (subCommands.length > 1) {
1177
+ const hasCd = subCommands.some((s) => /^(cd|pushd)\s/.test(s.trim()));
1178
+ const hasGit = subCommands.some((s) => {
1179
+ const n = extractCommandName(s);
1180
+ return n === "git" || n === "xargs" && /\bgit\b/.test(s);
1181
+ });
1182
+ if (hasCd && hasGit) {
1183
+ return {
1184
+ isReadOnly: false,
1185
+ isDestructive: false,
1186
+ reason: "cd + git compound may escape working directory (bare-repo risk)"
1187
+ };
1188
+ }
1189
+ if (hasGit && commandWritesGitInternals(command)) {
1190
+ return {
1191
+ isReadOnly: false,
1192
+ isDestructive: true,
1193
+ reason: "Compound command writes to git internal paths before running git"
1194
+ };
1195
+ }
1196
+ }
1197
+ let allReadOnly = true;
1198
+ let anyDestructive = false;
1199
+ const reasons = [];
1200
+ for (const sub of subCommands) {
1201
+ const result = classifySingleCommand(sub, config);
1202
+ if (!result.isReadOnly) allReadOnly = false;
1203
+ if (result.isDestructive) anyDestructive = true;
1204
+ if (result.reason) reasons.push(result.reason);
1205
+ }
1206
+ return {
1207
+ isReadOnly: allReadOnly,
1208
+ isDestructive: anyDestructive,
1209
+ reason: reasons.join("; ")
1210
+ };
1211
+ }
1212
+
1213
+ // src/tools/shell-safety/git-tracking.ts
1214
+ function detectGitOperations(command, stdout) {
1215
+ const events = [];
1216
+ const cmd = command.trim();
1217
+ if (/\bgit\s+commit\b/.test(cmd)) {
1218
+ const shaMatch = stdout.match(/\[[\w/.-]+\s+([0-9a-f]{7,40})\]/);
1219
+ const sha = shaMatch ? shaMatch[1] : "unknown";
1220
+ events.push({ type: "commit", details: `commit ${sha}` });
1221
+ }
1222
+ if (/\bgit\s+merge\b/.test(cmd)) {
1223
+ const branchMatch = cmd.match(/\bgit\s+merge\s+(?:--\S+\s+)*(\S+)/);
1224
+ const branch = branchMatch ? branchMatch[1] : "unknown";
1225
+ events.push({ type: "merge", details: `merge ${branch}` });
1226
+ }
1227
+ if (/\bgit\s+rebase\b/.test(cmd)) {
1228
+ const branchMatch = cmd.match(/\bgit\s+rebase\s+(?:--\S+\s+)*(\S+)/);
1229
+ const branch = branchMatch ? branchMatch[1] : "unknown";
1230
+ events.push({ type: "rebase", details: `rebase onto ${branch}` });
1231
+ }
1232
+ if (/\bgit\s+push\b/.test(cmd)) {
1233
+ const remoteMatch = cmd.match(/\bgit\s+push\s+(?:--\S+\s+)*(\S+)/);
1234
+ const remote = remoteMatch ? remoteMatch[1] : "origin";
1235
+ const branchMatch = stdout.match(/\S+\s+->\s+(\S+)/);
1236
+ const branch = branchMatch ? branchMatch[1] : "";
1237
+ events.push({
1238
+ type: "push",
1239
+ details: `push to ${remote}${branch ? ` (${branch})` : ""}`
1240
+ });
1241
+ }
1242
+ if (/\b(gh\s+pr\s+create|glab\s+mr\s+create)\b/.test(cmd)) {
1243
+ const urlMatch = stdout.match(/(https?:\/\/\S+(?:pull|merge_requests)\/\d+)/);
1244
+ const url = urlMatch ? urlMatch[1] : "";
1245
+ events.push({
1246
+ type: "pr_create",
1247
+ details: url ? `PR created: ${url}` : "PR created"
1248
+ });
1249
+ }
1250
+ return events;
1251
+ }
1252
+ function hasGitIndexLockError(output) {
1253
+ return /\.git\/index\.lock/.test(output);
1254
+ }
1255
+
1256
+ // src/tools/prompts/bash.ts
1257
+ var BASH_PROMPT = `Executes a given bash command and returns its output.
1258
+
1259
+ The working directory persists between commands, but shell state does not.
1260
+
1261
+ IMPORTANT: Avoid using this tool to run \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool:
1262
+
1263
+ - File search: Use Glob (NOT find or ls)
1264
+ - Content search: Use Grep (NOT grep or rg)
1265
+ - Read files: Use ReadFile (NOT cat/head/tail)
1266
+ - Edit files: Use EditFile (NOT sed/awk)
1267
+ - Write files: Use WriteFile (NOT echo >/cat <<EOF)
1268
+ - Communication: Output text directly (NOT echo/printf)
1269
+
1270
+ While the Bash tool can do similar things, the built-in tools are preferred as they provide better structured output and integrate with the permission system.
1271
+
1272
+ # Instructions
1273
+
1274
+ - If your command will create new directories or files, first use this tool to run \`ls\` to verify the parent directory exists and is the correct location.
1275
+ - Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt").
1276
+ - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.
1277
+ - You may specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). By default, your command will timeout after 30000ms (0.5 minutes).
1278
+ - When issuing multiple commands:
1279
+ - If the commands are independent and can run in parallel, make multiple Bash tool calls in a single message.
1280
+ - If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together.
1281
+ - Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.
1282
+ - DO NOT use newlines to separate commands (newlines are ok in quoted strings).
1283
+ - For git commands:
1284
+ - Prefer to create a new commit rather than amending an existing commit.
1285
+ - Before running destructive operations (e.g., git reset --hard, git push --force, git checkout --), consider whether there is a safer alternative. Only use destructive operations when they are truly the best approach.
1286
+ - Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.
1287
+ - Avoid unnecessary \`sleep\` commands:
1288
+ - Do not sleep between commands that can run immediately \u2014 just run them.
1289
+ - Do not retry failing commands in a sleep loop \u2014 diagnose the root cause.
1290
+ - If you must sleep, keep the duration short (1-5 seconds) to avoid blocking the user.
1291
+ `;
1292
+
1293
+ // src/tools/bash.ts
1294
+ var MAX_OUTPUT_CHARS = 1e5;
1295
+ var bashTool = {
1296
+ name: "Bash",
1297
+ description: "Execute a bash shell command. Use this for running scripts, installing packages, git operations, and other system commands.",
1298
+ prompt: BASH_PROMPT,
1299
+ isReadOnly(args) {
1300
+ const command = args.command;
1301
+ return classifyCommand(command).isReadOnly;
1302
+ },
1303
+ isDestructive(args) {
1304
+ const command = args.command;
1305
+ return classifyCommand(command).isDestructive;
1306
+ },
1307
+ checkPermissions(args) {
1308
+ const command = args.command;
1309
+ const classification = classifyCommand(command);
1310
+ if (classification.isDestructive) {
1311
+ return {
1312
+ behavior: "ask",
1313
+ message: `Destructive command: ${command}${classification.reason ? ` (${classification.reason})` : ""}`
1314
+ };
1315
+ }
1316
+ if (commandWritesGitInternals(command)) {
1317
+ return {
1318
+ behavior: "ask",
1319
+ message: `Command writes to .git/ internals: ${command}`
1320
+ };
1321
+ }
1322
+ return {
1323
+ behavior: "passthrough",
1324
+ message: `Execute: ${command}`
1325
+ };
1326
+ },
1327
+ parameters: {
1328
+ type: "object",
1329
+ properties: {
1330
+ command: {
1331
+ type: "string",
1332
+ description: "The bash command to execute"
1333
+ },
1334
+ timeout: {
1335
+ type: "number",
1336
+ description: "Timeout in milliseconds (default: 30000)"
1337
+ },
1338
+ description: {
1339
+ type: "string",
1340
+ description: "Short description of what this command does (5-10 words)"
1341
+ }
1342
+ },
1343
+ required: ["command"]
1344
+ },
1345
+ async call(args, ctx) {
1346
+ const command = args.command;
1347
+ const timeout = args.timeout;
1348
+ try {
1349
+ const result = await ctx.computer.executeCommand(command, {
1350
+ timeout,
1351
+ cwd: ctx.cwd
1352
+ });
1353
+ let output = "";
1354
+ if (result.stdout) {
1355
+ output += result.stdout;
1356
+ }
1357
+ if (result.stderr) {
1358
+ if (output) output += "\n";
1359
+ output += `STDERR:
1360
+ ${result.stderr}`;
1361
+ }
1362
+ if (!output.trim()) {
1363
+ output = "(no output)";
1364
+ }
1365
+ if (output.length > MAX_OUTPUT_CHARS) {
1366
+ const totalChars = output.length;
1367
+ output = output.slice(0, MAX_OUTPUT_CHARS) + `
1368
+ ... output truncated (${totalChars} total chars)`;
1369
+ }
1370
+ if (result.exitCode !== 0) {
1371
+ output = `Exit code: ${result.exitCode}
1372
+ ${output}`;
1373
+ }
1374
+ const toolResult = {
1375
+ content: output,
1376
+ isError: result.exitCode !== 0
1377
+ };
1378
+ if (result.exitCode === 0) {
1379
+ const gitOps = detectGitOperations(command, result.stdout ?? "");
1380
+ if (gitOps.length > 0) {
1381
+ toolResult.metadata = { gitOperations: gitOps };
1382
+ }
1383
+ }
1384
+ return toolResult;
1385
+ } catch (err) {
1386
+ return {
1387
+ content: `Error executing command: ${err instanceof Error ? err.message : String(err)}`,
1388
+ isError: true
1389
+ };
1390
+ }
1391
+ }
1392
+ };
1393
+
1394
+ // src/tools/prompts/glob.ts
1395
+ var GLOB_PROMPT = `Fast file pattern matching tool that works with any codebase size.
1396
+
1397
+ - Supports glob patterns like "**/*.js" or "src/**/*.ts"
1398
+ - Returns matching file paths sorted by modification time
1399
+ - Use this tool when you need to find files by name patterns
1400
+ - When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead
1401
+ `;
1402
+
1403
+ // src/utils/shell-escape.ts
1404
+ function shellEscape(s) {
1405
+ return "'" + s.replace(/'/g, "'\\''") + "'";
1406
+ }
1407
+
1408
+ // src/tools/glob.ts
1409
+ var MAX_RESULTS = 200;
1410
+ var globTool = {
1411
+ name: "Glob",
1412
+ description: "Find files matching a glob pattern. Uses ripgrep (rg --files --glob) for fast, gitignore-aware file discovery. Returns matching file paths sorted by modification time.",
1413
+ prompt: GLOB_PROMPT,
1414
+ isReadOnly: true,
1415
+ isConcurrencySafe: true,
1416
+ parameters: {
1417
+ type: "object",
1418
+ properties: {
1419
+ pattern: {
1420
+ type: "string",
1421
+ description: 'Glob pattern to match files (e.g. "*.ts", "src/**/*.tsx")'
1422
+ },
1423
+ path: {
1424
+ type: "string",
1425
+ description: "Directory to search in (defaults to cwd)"
1426
+ }
1427
+ },
1428
+ required: ["pattern"]
1429
+ },
1430
+ async call(args, ctx) {
1431
+ const pattern = args.pattern;
1432
+ const searchPath = args.path ?? ctx.cwd;
1433
+ const fullPattern = pattern.startsWith("**/") ? pattern : `**/${pattern}`;
1434
+ const command = `rg --files --glob ${shellEscape(fullPattern)} --sort=modified | head -n ${String(MAX_RESULTS + 1)}`;
1435
+ try {
1436
+ const result = await ctx.computer.executeCommand(command, {
1437
+ cwd: searchPath
1438
+ });
1439
+ if (result.exitCode > 1) {
1440
+ return {
1441
+ content: `Glob error: ${result.stderr || result.stdout}`,
1442
+ isError: true
1443
+ };
1444
+ }
1445
+ const lines = result.stdout.split("\n").filter((l) => l.trim() !== "");
1446
+ if (lines.length === 0) {
1447
+ return { content: "No files found matching the pattern." };
1448
+ }
1449
+ const truncated = lines.length > MAX_RESULTS;
1450
+ const files = truncated ? lines.slice(0, MAX_RESULTS) : lines;
1451
+ let output = files.join("\n");
1452
+ if (truncated) {
1453
+ output += `
1454
+
1455
+ (Results truncated. More than ${MAX_RESULTS} files match.)`;
1456
+ }
1457
+ return { content: output };
1458
+ } catch (err) {
1459
+ return {
1460
+ content: `Error searching files: ${err instanceof Error ? err.message : String(err)}`,
1461
+ isError: true
1462
+ };
1463
+ }
1464
+ }
1465
+ };
1466
+
1467
+ // src/tools/prompts/grep.ts
1468
+ var GREP_PROMPT = `A powerful search tool built on ripgrep.
1469
+
1470
+ Usage:
1471
+ - 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.
1472
+ - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
1473
+ - Filter files with the glob parameter (e.g., "*.js", "**/*.tsx")
1474
+ - Returns matching lines with file paths and line numbers
1475
+ - Use the Agent tool for open-ended searches requiring multiple rounds
1476
+ - Pattern syntax: Uses ripgrep (not grep) \u2014 literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)
1477
+ - Multiline matching: By default patterns match within single lines only. For cross-line patterns like \`struct \\{[\\s\\S]*?field\`, pass a context_lines parameter.
1478
+ `;
1479
+
1480
+ // src/tools/grep.ts
1481
+ var MAX_MATCHES = 250;
1482
+ var grepTool = {
1483
+ name: "Grep",
1484
+ description: "Search file contents using ripgrep (rg). Supports regex patterns. Returns matching lines with file paths and line numbers.",
1485
+ prompt: GREP_PROMPT,
1486
+ isReadOnly: true,
1487
+ isConcurrencySafe: true,
1488
+ parameters: {
1489
+ type: "object",
1490
+ properties: {
1491
+ pattern: {
1492
+ type: "string",
1493
+ description: "Regular expression pattern to search for"
1494
+ },
1495
+ path: {
1496
+ type: "string",
1497
+ description: "File or directory to search in (defaults to cwd)"
1498
+ },
1499
+ glob: {
1500
+ type: "string",
1501
+ description: 'Glob pattern to filter files (e.g. "*.ts", "*.{js,jsx}")'
1502
+ },
1503
+ case_insensitive: {
1504
+ type: "boolean",
1505
+ description: "Case insensitive search (default: false)"
1506
+ },
1507
+ context_lines: {
1508
+ type: "number",
1509
+ description: "Number of context lines to show before and after each match"
1510
+ }
1511
+ },
1512
+ required: ["pattern"]
1513
+ },
1514
+ async call(args, ctx) {
1515
+ const pattern = args.pattern;
1516
+ const searchPath = args.path ?? ctx.cwd;
1517
+ const glob = args.glob;
1518
+ const caseInsensitive = args.case_insensitive;
1519
+ const contextLines = args.context_lines;
1520
+ const rgArgs = [
1521
+ "rg",
1522
+ "--line-number",
1523
+ "--no-heading",
1524
+ "--color=never",
1525
+ `--max-count=${MAX_MATCHES}`
1526
+ ];
1527
+ if (caseInsensitive) rgArgs.push("-i");
1528
+ if (contextLines !== void 0) rgArgs.push(`-C${contextLines}`);
1529
+ if (glob) rgArgs.push(`--glob`, shellEscape(glob));
1530
+ rgArgs.push("--", shellEscape(pattern), ".");
1531
+ const command = rgArgs.join(" ");
1532
+ try {
1533
+ const result = await ctx.computer.executeCommand(command, {
1534
+ cwd: searchPath
1535
+ });
1536
+ if (result.exitCode === 1 && !result.stdout.trim()) {
1537
+ return { content: "No matches found." };
1538
+ }
1539
+ if (result.exitCode > 1) {
1540
+ return {
1541
+ content: `Grep error: ${result.stderr || result.stdout}`,
1542
+ isError: true
1543
+ };
1544
+ }
1545
+ const lines = result.stdout.split("\n");
1546
+ let output = result.stdout;
1547
+ if (lines.length > MAX_MATCHES) {
1548
+ output = lines.slice(0, MAX_MATCHES).join("\n") + `
1549
+
1550
+ (Results truncated at ${MAX_MATCHES} matches.)`;
1551
+ }
1552
+ return { content: output || "No matches found." };
1553
+ } catch (err) {
1554
+ return {
1555
+ content: `Error searching: ${err instanceof Error ? err.message : String(err)}`,
1556
+ isError: true
1557
+ };
1558
+ }
1559
+ }
1560
+ };
1561
+
1562
+ // src/tools/prompts/web-fetch.ts
1563
+ var WEB_FETCH_PROMPT = `Fetches content from a specified URL and returns it in a readable format.
1564
+
1565
+ - Takes a URL and fetches the page content, converting HTML to markdown
1566
+ - Returns the processed content for analysis
1567
+ - Use this tool when you need to retrieve and analyze web content
1568
+
1569
+ Usage notes:
1570
+ - The URL must be a fully-formed valid URL
1571
+ - HTTP URLs will be automatically upgraded to HTTPS
1572
+ - This tool is read-only and does not modify any files
1573
+ - Results may be summarized if the content is very large
1574
+ - When a URL redirects to a different host, the tool will inform you and provide the redirect URL. You should then make a new WebFetch request with the redirect URL.
1575
+ - For GitHub URLs, prefer using the gh CLI via Bash instead (e.g., gh pr view, gh issue view, gh api).
1576
+ `;
1577
+
1578
+ // src/tools/web-fetch.ts
1579
+ var MAX_CONTENT_LENGTH = 5 * 1024 * 1024;
1580
+ var FETCH_TIMEOUT_MS = 3e4;
1581
+ var MAX_OUTPUT_CHARS2 = 1e5;
1582
+ var webFetchTool = {
1583
+ name: "WebFetch",
1584
+ description: "Fetch a URL and return its contents as markdown. Useful for reading web pages, documentation, API responses, and other online content. Provide an optional prompt to extract specific information.",
1585
+ prompt: WEB_FETCH_PROMPT,
1586
+ isReadOnly: true,
1587
+ isConcurrencySafe: true,
1588
+ parameters: {
1589
+ type: "object",
1590
+ properties: {
1591
+ url: {
1592
+ type: "string",
1593
+ description: "The URL to fetch (must be a valid http/https URL)"
1594
+ },
1595
+ prompt: {
1596
+ type: "string",
1597
+ description: "Optional instruction for what to extract from the page content"
1598
+ }
1599
+ },
1600
+ required: ["url"]
1601
+ },
1602
+ async call(args, _ctx) {
1603
+ const url = args.url;
1604
+ const prompt = args.prompt;
1605
+ try {
1606
+ new URL(url);
1607
+ } catch {
1608
+ return { content: `Invalid URL: ${url}`, isError: true };
1609
+ }
1610
+ try {
1611
+ const controller = new AbortController();
1612
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1613
+ const response = await fetch(url, {
1614
+ signal: controller.signal,
1615
+ headers: {
1616
+ "User-Agent": "noumen-agent/1.0",
1617
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.7"
1618
+ },
1619
+ redirect: "follow"
1620
+ });
1621
+ clearTimeout(timeoutId);
1622
+ if (!response.ok) {
1623
+ return {
1624
+ content: `HTTP ${response.status}: ${response.statusText}`,
1625
+ isError: true
1626
+ };
1627
+ }
1628
+ const contentType = response.headers.get("content-type") ?? "";
1629
+ const contentLength = parseInt(
1630
+ response.headers.get("content-length") ?? "0",
1631
+ 10
1632
+ );
1633
+ if (contentLength > MAX_CONTENT_LENGTH) {
1634
+ return {
1635
+ content: `Response too large (${contentLength} bytes, limit ${MAX_CONTENT_LENGTH})`,
1636
+ isError: true
1637
+ };
1638
+ }
1639
+ let text = "";
1640
+ let bytesRead = 0;
1641
+ const reader = response.body?.getReader();
1642
+ const decoder = new TextDecoder();
1643
+ if (reader) {
1644
+ while (true) {
1645
+ const { done, value } = await reader.read();
1646
+ if (done) break;
1647
+ bytesRead += value.byteLength;
1648
+ if (bytesRead > MAX_CONTENT_LENGTH) {
1649
+ reader.cancel();
1650
+ return {
1651
+ content: `Response too large (>${MAX_CONTENT_LENGTH} bytes streamed, limit ${MAX_CONTENT_LENGTH})`,
1652
+ isError: true
1653
+ };
1654
+ }
1655
+ text += decoder.decode(value, { stream: true });
1656
+ }
1657
+ text += decoder.decode();
1658
+ } else {
1659
+ text = await response.text();
1660
+ }
1661
+ let markdown;
1662
+ if (contentType.includes("text/html") || contentType.includes("xhtml")) {
1663
+ const { NodeHtmlMarkdown } = await import("node-html-markdown");
1664
+ markdown = NodeHtmlMarkdown.translate(text);
1665
+ } else {
1666
+ markdown = text;
1667
+ }
1668
+ if (markdown.length > MAX_OUTPUT_CHARS2) {
1669
+ const totalChars = markdown.length;
1670
+ markdown = markdown.slice(0, MAX_OUTPUT_CHARS2) + `
1671
+
1672
+ ... content truncated (${totalChars} total chars)`;
1673
+ }
1674
+ let result = `# Content from ${url}
1675
+
1676
+ ${markdown}`;
1677
+ if (prompt) {
1678
+ result = `## Extraction prompt: ${prompt}
1679
+
1680
+ ${result}`;
1681
+ }
1682
+ return { content: result };
1683
+ } catch (err) {
1684
+ if (err instanceof Error && err.name === "AbortError") {
1685
+ return { content: `Fetch timed out after ${FETCH_TIMEOUT_MS}ms`, isError: true };
1686
+ }
1687
+ return {
1688
+ content: `Fetch error: ${err instanceof Error ? err.message : String(err)}`,
1689
+ isError: true
1690
+ };
1691
+ }
1692
+ }
1693
+ };
1694
+
1695
+ // src/tools/prompts/notebook.ts
1696
+ var NOTEBOOK_PROMPT = `Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_number is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_number. Use edit_mode=delete to delete the cell at the index specified by cell_number.`;
1697
+
1698
+ // src/tools/notebook.ts
1699
+ var notebookEditTool = {
1700
+ name: "NotebookEdit",
1701
+ description: "Edit a Jupyter notebook (.ipynb) file. Can replace, insert, or delete cells. The notebook is pure JSON \u2014 no kernel execution.",
1702
+ prompt: NOTEBOOK_PROMPT,
1703
+ isReadOnly: false,
1704
+ isConcurrencySafe: false,
1705
+ parameters: {
1706
+ type: "object",
1707
+ properties: {
1708
+ notebook_path: {
1709
+ type: "string",
1710
+ description: "Path to the .ipynb file"
1711
+ },
1712
+ cell_index: {
1713
+ type: "number",
1714
+ description: "0-based index of the cell to edit. For insert, the new cell is placed at this index."
1715
+ },
1716
+ new_source: {
1717
+ type: "string",
1718
+ description: "The new cell source content. Each line becomes an element in the source array."
1719
+ },
1720
+ cell_type: {
1721
+ type: "string",
1722
+ description: 'Cell type: "code" or "markdown" (default: "code")'
1723
+ },
1724
+ edit_mode: {
1725
+ type: "string",
1726
+ description: '"replace" (default) \u2014 replace existing cell source; "insert" \u2014 insert a new cell at cell_index; "delete" \u2014 delete the cell at cell_index (new_source is ignored)'
1727
+ }
1728
+ },
1729
+ required: ["notebook_path", "cell_index"]
1730
+ },
1731
+ async call(args, ctx) {
1732
+ const path2 = args.notebook_path;
1733
+ const cellIndex = args.cell_index;
1734
+ const newSource = args.new_source ?? "";
1735
+ const cellType = args.cell_type ?? "code";
1736
+ const editMode = args.edit_mode ?? "replace";
1737
+ try {
1738
+ const raw = await ctx.fs.readFile(path2);
1739
+ let notebook;
1740
+ try {
1741
+ notebook = JSON.parse(raw);
1742
+ } catch {
1743
+ return { content: `Not a valid JSON notebook: ${path2}`, isError: true };
1744
+ }
1745
+ if (!Array.isArray(notebook.cells)) {
1746
+ return { content: "Notebook has no cells array.", isError: true };
1747
+ }
1748
+ const sourceLines = newSource.split("\n").map(
1749
+ (line, i, arr) => i < arr.length - 1 ? line + "\n" : line
1750
+ );
1751
+ switch (editMode) {
1752
+ case "replace": {
1753
+ if (cellIndex < 0 || cellIndex >= notebook.cells.length) {
1754
+ return {
1755
+ content: `Cell index ${cellIndex} out of range (0-${notebook.cells.length - 1}).`,
1756
+ isError: true
1757
+ };
1758
+ }
1759
+ notebook.cells[cellIndex].source = sourceLines;
1760
+ notebook.cells[cellIndex].cell_type = cellType;
1761
+ break;
1762
+ }
1763
+ case "insert": {
1764
+ if (cellIndex < 0 || cellIndex > notebook.cells.length) {
1765
+ return {
1766
+ content: `Insert index ${cellIndex} out of range (0-${notebook.cells.length}).`,
1767
+ isError: true
1768
+ };
1769
+ }
1770
+ const newCell = {
1771
+ cell_type: cellType,
1772
+ source: sourceLines,
1773
+ metadata: {},
1774
+ ...cellType === "code" ? { outputs: [], execution_count: null } : {}
1775
+ };
1776
+ notebook.cells.splice(cellIndex, 0, newCell);
1777
+ break;
1778
+ }
1779
+ case "delete": {
1780
+ if (cellIndex < 0 || cellIndex >= notebook.cells.length) {
1781
+ return {
1782
+ content: `Cell index ${cellIndex} out of range (0-${notebook.cells.length - 1}).`,
1783
+ isError: true
1784
+ };
1785
+ }
1786
+ notebook.cells.splice(cellIndex, 1);
1787
+ break;
1788
+ }
1789
+ default:
1790
+ return {
1791
+ content: `Unknown edit_mode: ${editMode}. Use "replace", "insert", or "delete".`,
1792
+ isError: true
1793
+ };
1794
+ }
1795
+ await ctx.fs.writeFile(path2, JSON.stringify(notebook, null, 1) + "\n");
1796
+ const action = editMode === "delete" ? `Deleted cell ${cellIndex}` : editMode === "insert" ? `Inserted new ${cellType} cell at index ${cellIndex}` : `Replaced cell ${cellIndex} content`;
1797
+ return { content: `${action} in ${path2}. Notebook now has ${notebook.cells.length} cells.` };
1798
+ } catch (err) {
1799
+ return {
1800
+ content: `Error editing notebook: ${err instanceof Error ? err.message : String(err)}`,
1801
+ isError: true
1802
+ };
1803
+ }
1804
+ }
1805
+ };
1806
+
1807
+ // src/tools/ask-user.ts
1808
+ var askUserTool = {
1809
+ name: "AskUser",
1810
+ description: "Ask the user a question and wait for their response. Use when you need clarification, confirmation, or additional information before proceeding.",
1811
+ isReadOnly: true,
1812
+ isConcurrencySafe: false,
1813
+ requiresUserInteraction: true,
1814
+ parameters: {
1815
+ type: "object",
1816
+ properties: {
1817
+ question: {
1818
+ type: "string",
1819
+ description: "The question to ask the user"
1820
+ }
1821
+ },
1822
+ required: ["question"]
1823
+ },
1824
+ async call(args, ctx) {
1825
+ const question = args.question;
1826
+ if (!ctx.userInputHandler) {
1827
+ return {
1828
+ content: "Cannot ask user: no userInputHandler configured. Set userInputHandler in AgentOptions or ThreadConfig.",
1829
+ isError: true
1830
+ };
1831
+ }
1832
+ try {
1833
+ const answer = await ctx.userInputHandler(question);
1834
+ return { content: answer };
1835
+ } catch (err) {
1836
+ return {
1837
+ content: `Error getting user input: ${err instanceof Error ? err.message : String(err)}`,
1838
+ isError: true
1839
+ };
1840
+ }
1841
+ }
1842
+ };
1843
+
1844
+ // src/tools/registry.ts
1845
+ function resolveToolPrompt(tool) {
1846
+ if (tool.prompt === void 0) return tool.description;
1847
+ return typeof tool.prompt === "function" ? tool.prompt() : tool.prompt;
1848
+ }
1849
+ function resolveToolFlag(flag, args, defaultValue = false) {
1850
+ if (flag === void 0) return defaultValue;
1851
+ if (typeof flag === "function") {
1852
+ try {
1853
+ return flag(args);
1854
+ } catch {
1855
+ return defaultValue;
1856
+ }
1857
+ }
1858
+ return flag;
1859
+ }
1860
+ var ToolRegistry = class {
1861
+ tools = /* @__PURE__ */ new Map();
1862
+ _discoveredTools = /* @__PURE__ */ new Set();
1863
+ _toolSearchEnabled = false;
1864
+ constructor(additionalTools) {
1865
+ const builtIn = [
1866
+ readFileTool,
1867
+ writeFileTool,
1868
+ editFileTool,
1869
+ bashTool,
1870
+ globTool,
1871
+ grepTool,
1872
+ webFetchTool,
1873
+ notebookEditTool,
1874
+ askUserTool
1875
+ ];
1876
+ for (const tool of builtIn) {
1877
+ this.tools.set(tool.name, tool);
1878
+ }
1879
+ if (additionalTools) {
1880
+ for (const tool of additionalTools) {
1881
+ this.tools.set(tool.name, tool);
1882
+ }
1883
+ }
1884
+ }
1885
+ enableToolSearch() {
1886
+ this._toolSearchEnabled = true;
1887
+ }
1888
+ register(tool) {
1889
+ this.tools.set(tool.name, tool);
1890
+ }
1891
+ get(name) {
1892
+ return this.tools.get(name);
1893
+ }
1894
+ async execute(name, args, ctx) {
1895
+ const tool = this.tools.get(name);
1896
+ if (!tool) {
1897
+ return {
1898
+ content: `Unknown tool: ${name}`,
1899
+ isError: true
1900
+ };
1901
+ }
1902
+ if (tool.inputSchema) {
1903
+ const parsed = tool.inputSchema.safeParse(args);
1904
+ if (!parsed.success) {
1905
+ return {
1906
+ content: formatZodValidationError(name, parsed.error),
1907
+ isError: true
1908
+ };
1909
+ }
1910
+ }
1911
+ return tool.call(args, ctx);
1912
+ }
1913
+ toToolDefinitions() {
1914
+ return Array.from(this.tools.values()).map((tool) => ({
1915
+ type: "function",
1916
+ function: {
1917
+ name: tool.name,
1918
+ description: resolveToolPrompt(tool),
1919
+ parameters: tool.parameters
1920
+ }
1921
+ }));
1922
+ }
1923
+ /**
1924
+ * Get tool definitions filtered by tool search. Eager tools (always sent)
1925
+ * plus any deferred tools the model has discovered via ToolSearch.
1926
+ * Falls back to all tools when tool search is not enabled.
1927
+ */
1928
+ getActiveToolDefinitions() {
1929
+ if (!this._toolSearchEnabled) return this.toToolDefinitions();
1930
+ return Array.from(this.tools.values()).filter((tool) => !isDeferredTool(tool) || this._discoveredTools.has(tool.name)).map((tool) => ({
1931
+ type: "function",
1932
+ function: {
1933
+ name: tool.name,
1934
+ description: resolveToolPrompt(tool),
1935
+ parameters: tool.parameters
1936
+ }
1937
+ }));
1938
+ }
1939
+ getEagerTools() {
1940
+ return Array.from(this.tools.values()).filter((tool) => !isDeferredTool(tool));
1941
+ }
1942
+ getDeferredTools() {
1943
+ return Array.from(this.tools.values()).filter(isDeferredTool);
1944
+ }
1945
+ getToolsByNames(names) {
1946
+ return names.map((name) => this.tools.get(name)).filter((t) => t !== void 0);
1947
+ }
1948
+ markDiscovered(names) {
1949
+ for (const name of names) {
1950
+ this._discoveredTools.add(name);
1951
+ }
1952
+ }
1953
+ get discoveredTools() {
1954
+ return this._discoveredTools;
1955
+ }
1956
+ listTools() {
1957
+ return Array.from(this.tools.values());
1958
+ }
1959
+ };
1960
+
1961
+ export {
1962
+ TOOL_SEARCH_NAME,
1963
+ isDeferredTool,
1964
+ formatDeferredToolLine,
1965
+ searchToolsWithKeywords,
1966
+ createToolSearchTool,
1967
+ zodToJsonSchema,
1968
+ registerZodToJsonSchema,
1969
+ formatZodValidationError,
1970
+ readFileTool,
1971
+ writeFileTool,
1972
+ normalizeQuotes,
1973
+ findActualString,
1974
+ countOccurrences,
1975
+ preserveQuoteStyle,
1976
+ stripTrailingWhitespace,
1977
+ editFileTool,
1978
+ isGitInternalPath,
1979
+ looksLikeBareRepo,
1980
+ commandWritesGitInternals,
1981
+ extractCommandName,
1982
+ classifyCommand,
1983
+ detectGitOperations,
1984
+ hasGitIndexLockError,
1985
+ bashTool,
1986
+ globTool,
1987
+ grepTool,
1988
+ webFetchTool,
1989
+ notebookEditTool,
1990
+ askUserTool,
1991
+ resolveToolFlag,
1992
+ ToolRegistry
1993
+ };
1994
+ //# sourceMappingURL=chunk-QTJ7VTJY.js.map