noumen 0.1.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.
Files changed (127) hide show
  1. package/README.md +846 -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-1nFVUP9E.d.ts +1332 -0
  9. package/dist/cache-DsRqxx6v.d.ts +38 -0
  10. package/dist/chunk-3HEYCV26.js +10 -0
  11. package/dist/chunk-3HEYCV26.js.map +1 -0
  12. package/dist/chunk-3SK5GCI6.js +75 -0
  13. package/dist/chunk-3SK5GCI6.js.map +1 -0
  14. package/dist/chunk-42PHHZUA.js +132 -0
  15. package/dist/chunk-42PHHZUA.js.map +1 -0
  16. package/dist/chunk-4HW6LN6D.js +10365 -0
  17. package/dist/chunk-4HW6LN6D.js.map +1 -0
  18. package/dist/chunk-4SQA2UCV.js +26 -0
  19. package/dist/chunk-4SQA2UCV.js.map +1 -0
  20. package/dist/chunk-5GEX6ZSB.js +179 -0
  21. package/dist/chunk-5GEX6ZSB.js.map +1 -0
  22. package/dist/chunk-5JN4SPI7.js +94 -0
  23. package/dist/chunk-5JN4SPI7.js.map +1 -0
  24. package/dist/chunk-AMYIJSAZ.js +57 -0
  25. package/dist/chunk-AMYIJSAZ.js.map +1 -0
  26. package/dist/chunk-BZSFUEWM.js +43 -0
  27. package/dist/chunk-BZSFUEWM.js.map +1 -0
  28. package/dist/chunk-CS6WNDCF.js +171 -0
  29. package/dist/chunk-CS6WNDCF.js.map +1 -0
  30. package/dist/chunk-D43BWEZA.js +346 -0
  31. package/dist/chunk-D43BWEZA.js.map +1 -0
  32. package/dist/chunk-DGUM43GV.js +11 -0
  33. package/dist/chunk-DGUM43GV.js.map +1 -0
  34. package/dist/chunk-EKOGVTBT.js +472 -0
  35. package/dist/chunk-EKOGVTBT.js.map +1 -0
  36. package/dist/chunk-HEQQQGK5.js +131 -0
  37. package/dist/chunk-HEQQQGK5.js.map +1 -0
  38. package/dist/chunk-HL6JCRZJ.js +3112 -0
  39. package/dist/chunk-HL6JCRZJ.js.map +1 -0
  40. package/dist/chunk-JACGEMTF.js +43 -0
  41. package/dist/chunk-JACGEMTF.js.map +1 -0
  42. package/dist/chunk-JX7CLUCV.js +21 -0
  43. package/dist/chunk-JX7CLUCV.js.map +1 -0
  44. package/dist/chunk-KXDB56YW.js +39 -0
  45. package/dist/chunk-KXDB56YW.js.map +1 -0
  46. package/dist/chunk-L3L3FG5T.js +16 -0
  47. package/dist/chunk-L3L3FG5T.js.map +1 -0
  48. package/dist/chunk-OGXNFXFA.js +196 -0
  49. package/dist/chunk-OGXNFXFA.js.map +1 -0
  50. package/dist/chunk-UVSSQBDY.js +192 -0
  51. package/dist/chunk-UVSSQBDY.js.map +1 -0
  52. package/dist/chunk-Y45R3PQL.js +684 -0
  53. package/dist/chunk-Y45R3PQL.js.map +1 -0
  54. package/dist/cli/index.d.ts +1 -0
  55. package/dist/cli/index.js +874 -0
  56. package/dist/cli/index.js.map +1 -0
  57. package/dist/client/index.d.ts +64 -0
  58. package/dist/client/index.js +409 -0
  59. package/dist/client/index.js.map +1 -0
  60. package/dist/client-CRRO2376.js +10 -0
  61. package/dist/client-CRRO2376.js.map +1 -0
  62. package/dist/headless-FFU2DESQ.js +142 -0
  63. package/dist/headless-FFU2DESQ.js.map +1 -0
  64. package/dist/history-snip-64GYP4ZL.js +12 -0
  65. package/dist/history-snip-64GYP4ZL.js.map +1 -0
  66. package/dist/index.d.ts +1459 -422
  67. package/dist/index.js +398 -1757
  68. package/dist/index.js.map +1 -1
  69. package/dist/jsonrpc/index.d.ts +54 -0
  70. package/dist/jsonrpc/index.js +34 -0
  71. package/dist/jsonrpc/index.js.map +1 -0
  72. package/dist/lsp/index.d.ts +36 -0
  73. package/dist/lsp/index.js +16 -0
  74. package/dist/lsp/index.js.map +1 -0
  75. package/dist/lsp-PS3BWIHC.js +8 -0
  76. package/dist/lsp-PS3BWIHC.js.map +1 -0
  77. package/dist/manager-DLXK63XC.js +8 -0
  78. package/dist/manager-DLXK63XC.js.map +1 -0
  79. package/dist/mcp/index.d.ts +111 -0
  80. package/dist/mcp/index.js +105 -0
  81. package/dist/mcp/index.js.map +1 -0
  82. package/dist/mcp-auth-AEI2R4ZC.js +9 -0
  83. package/dist/mcp-auth-AEI2R4ZC.js.map +1 -0
  84. package/dist/provider-factory-KCLIF34X.js +20 -0
  85. package/dist/provider-factory-KCLIF34X.js.map +1 -0
  86. package/dist/providers/anthropic.d.ts +19 -0
  87. package/dist/providers/anthropic.js +35 -0
  88. package/dist/providers/anthropic.js.map +1 -0
  89. package/dist/providers/bedrock.d.ts +39 -0
  90. package/dist/providers/bedrock.js +56 -0
  91. package/dist/providers/bedrock.js.map +1 -0
  92. package/dist/providers/gemini.d.ts +17 -0
  93. package/dist/providers/gemini.js +262 -0
  94. package/dist/providers/gemini.js.map +1 -0
  95. package/dist/providers/ollama.d.ts +13 -0
  96. package/dist/providers/ollama.js +20 -0
  97. package/dist/providers/ollama.js.map +1 -0
  98. package/dist/providers/openai.d.ts +21 -0
  99. package/dist/providers/openai.js +9 -0
  100. package/dist/providers/openai.js.map +1 -0
  101. package/dist/providers/openrouter.d.ts +16 -0
  102. package/dist/providers/openrouter.js +24 -0
  103. package/dist/providers/openrouter.js.map +1 -0
  104. package/dist/providers/vertex.d.ts +42 -0
  105. package/dist/providers/vertex.js +67 -0
  106. package/dist/providers/vertex.js.map +1 -0
  107. package/dist/render-GRN4ZSSW.js +14 -0
  108. package/dist/render-GRN4ZSSW.js.map +1 -0
  109. package/dist/resolve-4JA2BBDA.js +14 -0
  110. package/dist/resolve-4JA2BBDA.js.map +1 -0
  111. package/dist/server/index.d.ts +143 -0
  112. package/dist/server/index.js +695 -0
  113. package/dist/server/index.js.map +1 -0
  114. package/dist/server-CHMxuWKq.d.ts +96 -0
  115. package/dist/spinner-OJNR6NFO.js +8 -0
  116. package/dist/spinner-OJNR6NFO.js.map +1 -0
  117. package/dist/types-2kTLUCnD.d.ts +107 -0
  118. package/dist/types-CD0rUKKT.d.ts +109 -0
  119. package/dist/types-LrU4LRmX.d.ts +575 -0
  120. package/dist/types-NIyVwQ4h.d.ts +109 -0
  121. package/dist/types-QwfylltH.d.ts +71 -0
  122. package/dist/types-RPKUTu1k.d.ts +645 -0
  123. package/dist/uuid-RVN2T26F.js +8 -0
  124. package/dist/uuid-RVN2T26F.js.map +1 -0
  125. package/dist/zod-7YXKWYMC.js +12 -0
  126. package/dist/zod-7YXKWYMC.js.map +1 -0
  127. package/package.json +141 -7
@@ -0,0 +1,3112 @@
1
+ import {
2
+ formatZodValidationError
3
+ } from "./chunk-3SK5GCI6.js";
4
+ import {
5
+ IMAGE_EXTENSIONS,
6
+ compressImageBufferWithTokenLimit,
7
+ createImageMetadataText,
8
+ maybeResizeAndDownsampleImageBuffer
9
+ } from "./chunk-5GEX6ZSB.js";
10
+ import {
11
+ contentToString
12
+ } from "./chunk-JACGEMTF.js";
13
+
14
+ // src/tools/tool-search.ts
15
+ var TOOL_SEARCH_NAME = "ToolSearch";
16
+ function isDeferredTool(tool) {
17
+ if (tool.alwaysLoad === true) return false;
18
+ if (tool.mcpInfo !== void 0) return true;
19
+ if (tool.name === TOOL_SEARCH_NAME) return false;
20
+ return tool.shouldDefer === true;
21
+ }
22
+ function formatDeferredToolLine(tool) {
23
+ const desc = tool.description.split(".")[0];
24
+ return `- ${tool.name}: ${desc}`;
25
+ }
26
+ function parseToolName(name) {
27
+ if (name.startsWith("mcp__") || name.includes("__")) {
28
+ const withoutPrefix = name.replace(/^mcp__/, "").toLowerCase();
29
+ const parts2 = withoutPrefix.split("__").flatMap((p) => p.split("_"));
30
+ return { parts: parts2.filter(Boolean), full: withoutPrefix.replace(/__/g, " ").replace(/_/g, " "), isMcp: true };
31
+ }
32
+ const parts = name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/_/g, " ").toLowerCase().split(/\s+/).filter(Boolean);
33
+ return { parts, full: parts.join(" "), isMcp: false };
34
+ }
35
+ function escapeRegExp(s) {
36
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37
+ }
38
+ function searchToolsWithKeywords(query, deferredTools, allTools, maxResults) {
39
+ const queryLower = query.toLowerCase().trim();
40
+ const exactMatch = deferredTools.find((t) => t.name.toLowerCase() === queryLower) ?? allTools.find((t) => t.name.toLowerCase() === queryLower);
41
+ if (exactMatch) return [exactMatch.name];
42
+ if (queryLower.startsWith("mcp__") && queryLower.length > 5) {
43
+ const prefixMatches = deferredTools.filter((t) => t.name.toLowerCase().startsWith(queryLower)).slice(0, maxResults).map((t) => t.name);
44
+ if (prefixMatches.length > 0) return prefixMatches;
45
+ }
46
+ const queryTerms = queryLower.split(/\s+/).filter((t) => t.length > 0);
47
+ const requiredTerms = [];
48
+ const optionalTerms = [];
49
+ for (const term of queryTerms) {
50
+ if (term.startsWith("+") && term.length > 1) {
51
+ requiredTerms.push(term.slice(1));
52
+ } else {
53
+ optionalTerms.push(term);
54
+ }
55
+ }
56
+ const allScoringTerms = requiredTerms.length > 0 ? [...requiredTerms, ...optionalTerms] : queryTerms;
57
+ const termPatterns = /* @__PURE__ */ new Map();
58
+ for (const term of allScoringTerms) {
59
+ if (!termPatterns.has(term)) {
60
+ termPatterns.set(term, new RegExp(`\\b${escapeRegExp(term)}\\b`));
61
+ }
62
+ }
63
+ let candidates = deferredTools;
64
+ if (requiredTerms.length > 0) {
65
+ candidates = deferredTools.filter((tool) => {
66
+ const parsed = parseToolName(tool.name);
67
+ const descLower = tool.description.toLowerCase();
68
+ return requiredTerms.every((term) => {
69
+ const pattern = termPatterns.get(term);
70
+ return parsed.parts.includes(term) || parsed.parts.some((part) => part.includes(term)) || pattern.test(descLower);
71
+ });
72
+ });
73
+ }
74
+ const scored = candidates.map((tool) => {
75
+ const parsed = parseToolName(tool.name);
76
+ const descLower = tool.description.toLowerCase();
77
+ let score = 0;
78
+ for (const term of allScoringTerms) {
79
+ const pattern = termPatterns.get(term);
80
+ if (parsed.parts.includes(term)) {
81
+ score += parsed.isMcp ? 12 : 10;
82
+ } else if (parsed.parts.some((part) => part.includes(term))) {
83
+ score += parsed.isMcp ? 6 : 5;
84
+ }
85
+ if (parsed.full.includes(term) && score === 0) {
86
+ score += 3;
87
+ }
88
+ if (pattern.test(descLower)) {
89
+ score += 2;
90
+ }
91
+ }
92
+ return { name: tool.name, score };
93
+ });
94
+ return scored.filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, maxResults).map((item) => item.name);
95
+ }
96
+ function formatToolSchemas(tools) {
97
+ if (tools.length === 0) return "No matching deferred tools found.";
98
+ const lines = tools.map((t) => {
99
+ const schema = {
100
+ description: t.description,
101
+ name: t.name,
102
+ parameters: t.parameters
103
+ };
104
+ return `<function>${JSON.stringify(schema)}</function>`;
105
+ });
106
+ return `<functions>
107
+ ${lines.join("\n")}
108
+ </functions>`;
109
+ }
110
+ function createToolSearchTool(getDeferredTools, getAllTools, getToolsByNames, onDiscovered) {
111
+ return {
112
+ name: TOOL_SEARCH_NAME,
113
+ 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',
114
+ isReadOnly: true,
115
+ isConcurrencySafe: true,
116
+ parameters: {
117
+ type: "object",
118
+ properties: {
119
+ query: {
120
+ type: "string",
121
+ description: 'Query to find deferred tools. Use "select:<tool_name>" for direct selection, or keywords to search.'
122
+ },
123
+ max_results: {
124
+ type: "number",
125
+ description: "Maximum number of results to return (default: 5)"
126
+ }
127
+ },
128
+ required: ["query"]
129
+ },
130
+ async call(args) {
131
+ const query = args.query;
132
+ const maxResults = args.max_results ?? 5;
133
+ const deferredTools = getDeferredTools();
134
+ const allTools = getAllTools();
135
+ const selectMatch = query.match(/^select:(.+)$/i);
136
+ if (selectMatch) {
137
+ const requested = selectMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
138
+ const found = [];
139
+ for (const toolName of requested) {
140
+ const match = deferredTools.find((t) => t.name.toLowerCase() === toolName.toLowerCase()) ?? allTools.find((t) => t.name.toLowerCase() === toolName.toLowerCase());
141
+ if (match && !found.includes(match.name)) {
142
+ found.push(match.name);
143
+ }
144
+ }
145
+ if (found.length === 0) {
146
+ return {
147
+ content: JSON.stringify({
148
+ matches: [],
149
+ query,
150
+ total_deferred_tools: deferredTools.length
151
+ })
152
+ };
153
+ }
154
+ onDiscovered(found);
155
+ const matchedTools2 = getToolsByNames(found);
156
+ return { content: formatToolSchemas(matchedTools2) };
157
+ }
158
+ const matches = searchToolsWithKeywords(query, deferredTools, allTools, maxResults);
159
+ if (matches.length === 0) {
160
+ return {
161
+ content: JSON.stringify({
162
+ matches: [],
163
+ query,
164
+ total_deferred_tools: deferredTools.length
165
+ })
166
+ };
167
+ }
168
+ onDiscovered(matches);
169
+ const matchedTools = getToolsByNames(matches);
170
+ return { content: formatToolSchemas(matchedTools) };
171
+ }
172
+ };
173
+ }
174
+
175
+ // src/tools/prompts/read.ts
176
+ var READ_PROMPT = `Reads a file from the local filesystem. You can access any file directly by using this tool.
177
+ 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.
178
+
179
+ Usage:
180
+ - The file_path parameter must be an absolute path, not a relative path.
181
+ - By default, it reads the entire file. Use offset and limit to read specific portions of large files.
182
+ - Lines in the output are numbered with the format: LINE_NUMBER|LINE_CONTENT
183
+ - If you read a file that exists but has empty contents you will receive a notice in place of file contents.
184
+ - This tool can read image files (e.g. PNG, JPG) when the provider supports multimodal input.
185
+ - This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs.
186
+ - This tool can only read files, not directories. To list a directory, use an ls command via the Bash tool.
187
+ - If the file has not changed since the last read, a "file_unchanged" result is returned to save context tokens.
188
+ `;
189
+
190
+ // src/tools/read.ts
191
+ import * as path from "path";
192
+ var DEFAULT_MAX_IMAGE_TOKENS = 1600;
193
+ var MAX_FILE_SIZE = 256 * 1024;
194
+ var BLOCKED_DEVICE_PATHS = /* @__PURE__ */ new Set([
195
+ "/dev/zero",
196
+ "/dev/random",
197
+ "/dev/urandom",
198
+ "/dev/full",
199
+ "/dev/stdin",
200
+ "/dev/tty",
201
+ "/dev/console",
202
+ "/dev/stdout",
203
+ "/dev/stderr",
204
+ "/dev/fd/0",
205
+ "/dev/fd/1",
206
+ "/dev/fd/2"
207
+ ]);
208
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
209
+ ".exe",
210
+ ".dll",
211
+ ".so",
212
+ ".dylib",
213
+ ".bin",
214
+ ".zip",
215
+ ".tar",
216
+ ".gz",
217
+ ".bz2",
218
+ ".xz",
219
+ ".7z",
220
+ ".rar",
221
+ ".wasm",
222
+ ".o",
223
+ ".a",
224
+ ".obj",
225
+ ".lib",
226
+ ".class",
227
+ ".pyc",
228
+ ".pyo",
229
+ ".jar",
230
+ ".war",
231
+ ".ear",
232
+ ".iso",
233
+ ".img",
234
+ ".dmg",
235
+ ".msi",
236
+ ".deb",
237
+ ".rpm",
238
+ ".apk",
239
+ ".ipa"
240
+ ]);
241
+ var readFileTool = {
242
+ name: "ReadFile",
243
+ 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.",
244
+ prompt: READ_PROMPT,
245
+ isReadOnly: true,
246
+ isConcurrencySafe: true,
247
+ parameters: {
248
+ type: "object",
249
+ properties: {
250
+ file_path: {
251
+ type: "string",
252
+ description: "The path of the file to read (absolute or relative to cwd)"
253
+ },
254
+ offset: {
255
+ type: "number",
256
+ description: "Line number to start reading from (1-indexed). Defaults to 1.",
257
+ minimum: 1
258
+ },
259
+ limit: {
260
+ type: "number",
261
+ description: "Maximum number of lines to read. If omitted, reads entire file.",
262
+ minimum: 1
263
+ }
264
+ },
265
+ required: ["file_path"]
266
+ },
267
+ async call(args, ctx) {
268
+ const filePath = args.file_path;
269
+ const offset = args.offset ?? 1;
270
+ const limit = args.limit;
271
+ if (filePath.startsWith("\\\\") || filePath.startsWith("//")) {
272
+ return { content: "Error: UNC paths are not allowed", isError: true };
273
+ }
274
+ try {
275
+ const resolved = path.resolve(ctx.cwd, filePath);
276
+ if (BLOCKED_DEVICE_PATHS.has(resolved)) {
277
+ return {
278
+ content: `Error: Cannot read device file ${filePath}.`,
279
+ isError: true
280
+ };
281
+ }
282
+ if (resolved.startsWith("/proc/") && (resolved.endsWith("/fd/0") || resolved.endsWith("/fd/1") || resolved.endsWith("/fd/2"))) {
283
+ return {
284
+ content: `Error: Cannot read process file descriptor ${filePath}.`,
285
+ isError: true
286
+ };
287
+ }
288
+ const ext = path.extname(filePath).toLowerCase();
289
+ if (BINARY_EXTENSIONS.has(ext)) {
290
+ return {
291
+ content: `Error: Cannot read binary ${ext} file. This tool only reads text files.`,
292
+ isError: true
293
+ };
294
+ }
295
+ if (IMAGE_EXTENSIONS.has(ext) && ctx.fs.readFileBytes) {
296
+ return readImageFile(filePath, ext, ctx);
297
+ }
298
+ try {
299
+ const stat = await ctx.fs.stat(filePath);
300
+ if (stat.size !== void 0 && stat.size > MAX_FILE_SIZE && !limit) {
301
+ return {
302
+ content: `Error: File is too large (${Math.round(stat.size / 1024)}KB, max ${MAX_FILE_SIZE / 1024}KB). Use offset/limit to read specific portions.`,
303
+ isError: true
304
+ };
305
+ }
306
+ } catch {
307
+ }
308
+ if (ctx.fileStateCache) {
309
+ const cached = ctx.fileStateCache.get(filePath);
310
+ if (cached && !cached.isPartialView && cached.offset !== void 0 && cached.offset === offset && cached.limit === limit) {
311
+ try {
312
+ const stat = await ctx.fs.stat(filePath);
313
+ const mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
314
+ if (mtime === cached.timestamp) {
315
+ return { content: "file_unchanged" };
316
+ }
317
+ } catch {
318
+ }
319
+ }
320
+ }
321
+ const maxReadBytes = limit ? Math.min((limit + (offset - 1)) * 500, 10 * 1024 * 1024) : void 0;
322
+ const content = await ctx.fs.readFile(
323
+ filePath,
324
+ maxReadBytes ? { maxBytes: maxReadBytes } : void 0
325
+ );
326
+ const lines = content.split("\n");
327
+ const startIdx = Math.max(0, offset - 1);
328
+ const endIdx = limit ? Math.min(lines.length, startIdx + limit) : lines.length;
329
+ const selectedLines = lines.slice(startIdx, endIdx);
330
+ const numbered = selectedLines.map(
331
+ (line, i) => `${String(startIdx + i + 1).padStart(6)}|${line}`
332
+ );
333
+ let result = numbered.join("\n");
334
+ if (endIdx < lines.length) {
335
+ result += `
336
+ ... ${lines.length - endIdx} lines not shown ...`;
337
+ }
338
+ if (ctx.fileStateCache) {
339
+ let mtime = 0;
340
+ try {
341
+ const stat = await ctx.fs.stat(filePath);
342
+ mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
343
+ } catch {
344
+ }
345
+ ctx.fileStateCache.set(filePath, {
346
+ content: selectedLines.join("\n"),
347
+ timestamp: mtime,
348
+ offset,
349
+ limit,
350
+ isPartialView: !!(limit || offset > 1)
351
+ });
352
+ }
353
+ return { content: result || "File is empty." };
354
+ } catch (err) {
355
+ return {
356
+ content: `Error reading file: ${err instanceof Error ? err.message : String(err)}`,
357
+ isError: true
358
+ };
359
+ }
360
+ }
361
+ };
362
+ async function readImageFile(filePath, ext, ctx) {
363
+ const imageBuffer = await ctx.fs.readFileBytes(filePath);
364
+ const originalSize = imageBuffer.length;
365
+ const formatExt = ext.replace(/^\./, "");
366
+ const resized = await maybeResizeAndDownsampleImageBuffer(
367
+ imageBuffer,
368
+ originalSize,
369
+ formatExt
370
+ );
371
+ let base64 = resized.buffer.toString("base64");
372
+ let mediaType = resized.mediaType;
373
+ const estimatedTokens = Math.ceil(base64.length * 0.125);
374
+ if (estimatedTokens > DEFAULT_MAX_IMAGE_TOKENS) {
375
+ try {
376
+ const compressed = await compressImageBufferWithTokenLimit(
377
+ imageBuffer,
378
+ DEFAULT_MAX_IMAGE_TOKENS,
379
+ `image/${formatExt}`
380
+ );
381
+ base64 = compressed.base64;
382
+ mediaType = compressed.mediaType;
383
+ } catch {
384
+ }
385
+ }
386
+ const parts = [
387
+ {
388
+ type: "image",
389
+ data: base64,
390
+ media_type: `image/${mediaType}`
391
+ }
392
+ ];
393
+ if (resized.dimensions) {
394
+ parts.push({
395
+ type: "text",
396
+ text: createImageMetadataText(resized.dimensions)
397
+ });
398
+ }
399
+ return { content: parts };
400
+ }
401
+
402
+ // src/permissions/types.ts
403
+ var RULE_SOURCE_PRECEDENCE = [
404
+ "policy",
405
+ "project",
406
+ "user",
407
+ "session"
408
+ ];
409
+
410
+ // src/permissions/rules.ts
411
+ import * as path2 from "path";
412
+ import * as fs from "fs";
413
+ function toolMatchesRule(toolName, rule, mcpInfo) {
414
+ if (rule.toolName === toolName) return true;
415
+ if (mcpInfo) {
416
+ const serverPrefix = parseMcpServerPrefix(rule.toolName);
417
+ if (serverPrefix && serverPrefix === mcpInfo.serverName) {
418
+ return true;
419
+ }
420
+ }
421
+ return false;
422
+ }
423
+ function parseMcpServerPrefix(ruleName) {
424
+ const parts = ruleName.split("__");
425
+ if (parts.length !== 2 || parts[0] !== "mcp" || !parts[1]) return null;
426
+ return parts[1];
427
+ }
428
+ var SAFE_WRAPPERS = ["timeout", "time", "nice", "nohup", "stdbuf"];
429
+ var COMPOUND_OPERATORS_RE = /\s*(?:;|&&|\|\||\|)\s*/;
430
+ function stripForRuleMatching(command) {
431
+ let cmd = command.trim();
432
+ while (/^[A-Za-z_][A-Za-z0-9_]*=\S*\s/.test(cmd)) {
433
+ cmd = cmd.replace(/^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/, "");
434
+ }
435
+ let changed = true;
436
+ while (changed) {
437
+ changed = false;
438
+ for (const wrapper of SAFE_WRAPPERS) {
439
+ if (cmd.startsWith(wrapper + " ")) {
440
+ cmd = cmd.slice(wrapper.length).trim();
441
+ while (cmd.startsWith("-")) {
442
+ const spaceIdx = cmd.indexOf(" ");
443
+ if (spaceIdx === -1) break;
444
+ cmd = cmd.slice(spaceIdx).trim();
445
+ }
446
+ while (/^[A-Za-z_][A-Za-z0-9_]*=\S*\s/.test(cmd)) {
447
+ cmd = cmd.replace(/^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/, "");
448
+ }
449
+ changed = true;
450
+ }
451
+ }
452
+ }
453
+ return cmd;
454
+ }
455
+ function isCompoundCommand(content) {
456
+ return COMPOUND_OPERATORS_RE.test(content);
457
+ }
458
+ function contentMatchesRule(content, ruleContent) {
459
+ if (ruleContent.endsWith(":*")) {
460
+ const prefix = ruleContent.slice(0, -2);
461
+ const matches = content === prefix || content.startsWith(prefix + " ");
462
+ if (matches && isCompoundCommand(content)) return false;
463
+ if (matches) return true;
464
+ const stripped2 = stripForRuleMatching(content);
465
+ if (stripped2 !== content) {
466
+ const strippedMatches = stripped2 === prefix || stripped2.startsWith(prefix + " ");
467
+ if (strippedMatches && isCompoundCommand(stripped2)) return false;
468
+ return strippedMatches;
469
+ }
470
+ return false;
471
+ }
472
+ if (ruleContent.includes("*")) {
473
+ if (matchSimpleGlob(ruleContent, content)) return true;
474
+ const stripped2 = stripForRuleMatching(content);
475
+ if (stripped2 !== content) return matchSimpleGlob(ruleContent, stripped2);
476
+ return false;
477
+ }
478
+ if (ruleContent === content) return true;
479
+ const stripped = stripForRuleMatching(content);
480
+ return stripped !== content && ruleContent === stripped;
481
+ }
482
+ function matchSimpleGlob(pattern, value) {
483
+ let regex = "^";
484
+ let i = 0;
485
+ while (i < pattern.length) {
486
+ if (pattern[i] === "*" && pattern[i + 1] === "*") {
487
+ regex += ".*";
488
+ i += 2;
489
+ if (pattern[i] === "/") i++;
490
+ } else if (pattern[i] === "*") {
491
+ regex += "[^/]*";
492
+ i++;
493
+ } else if (pattern[i] === "?") {
494
+ regex += "[^/]";
495
+ i++;
496
+ } else {
497
+ regex += escapeRegex(pattern[i]);
498
+ i++;
499
+ }
500
+ }
501
+ regex += "$";
502
+ return new RegExp(regex).test(value);
503
+ }
504
+ function escapeRegex(s) {
505
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
506
+ }
507
+ function getMatchingRules(context, toolName, behavior, content, mcpInfo) {
508
+ const matched = context.rules.filter((rule) => {
509
+ if (rule.behavior !== behavior) return false;
510
+ if (!toolMatchesRule(toolName, rule, mcpInfo)) return false;
511
+ if (rule.ruleContent !== void 0) {
512
+ if (content === void 0) return false;
513
+ return contentMatchesRule(content, rule.ruleContent);
514
+ }
515
+ return true;
516
+ });
517
+ matched.sort((a, b) => {
518
+ const aIdx = a.source ? RULE_SOURCE_PRECEDENCE.indexOf(a.source) : RULE_SOURCE_PRECEDENCE.length;
519
+ const bIdx = b.source ? RULE_SOURCE_PRECEDENCE.indexOf(b.source) : RULE_SOURCE_PRECEDENCE.length;
520
+ return aIdx - bIdx;
521
+ });
522
+ return matched;
523
+ }
524
+ function containsShellExpansion(p) {
525
+ if (p.includes("$") || p.includes("%") || p.startsWith("=")) return true;
526
+ if (p.includes("`")) return true;
527
+ if (/^~[^/]/.test(p)) return true;
528
+ if (p.startsWith("\\\\")) return true;
529
+ return false;
530
+ }
531
+ function isPathInWorkingDirectories(filePath, workingDirectories) {
532
+ if (workingDirectories.length === 0) return false;
533
+ if (containsShellExpansion(filePath)) return false;
534
+ const normalized = normalizePath(filePath);
535
+ return workingDirectories.some((dir) => {
536
+ const normalizedDir = normalizePath(dir);
537
+ return normalized === normalizedDir || normalized.startsWith(normalizedDir + "/");
538
+ });
539
+ }
540
+ function normalizePath(p) {
541
+ let result = path2.resolve(p);
542
+ try {
543
+ result = fs.realpathSync(result);
544
+ } catch {
545
+ }
546
+ while (result.endsWith("/") && result.length > 1) {
547
+ result = result.slice(0, -1);
548
+ }
549
+ if (process.platform === "darwin" || process.platform === "win32") {
550
+ result = result.toLowerCase();
551
+ }
552
+ return result;
553
+ }
554
+
555
+ // src/permissions/classifier.ts
556
+ var DEFAULT_CLASSIFIER_PROMPT = `You are a security classifier for an AI coding agent.
557
+ Your job is to determine whether a tool call should be automatically approved or blocked.
558
+
559
+ Automatically APPROVE tool calls that:
560
+ - Read files within the project directory
561
+ - Write/edit files within the project directory
562
+ - Run common development commands (build, test, lint, format, git status/diff/log)
563
+ - Search for files or code patterns
564
+ - Create or update task items
565
+
566
+ Automatically BLOCK tool calls that:
567
+ - Execute potentially destructive commands (rm -rf, drop database, force push)
568
+ - Access files outside the project directory
569
+ - Make network requests to unknown hosts
570
+ - Run commands that could affect the system (install packages globally, modify system files)
571
+ - Access secrets, credentials, or environment variables
572
+
573
+ Respond with a JSON object: {"shouldBlock": boolean, "reason": "brief explanation"}`;
574
+ async function classifyPermission(toolName, args, recentMessages, provider, opts) {
575
+ const model = opts?.classifierModel ?? opts?.model;
576
+ if (!model) {
577
+ return { shouldBlock: true, reason: "No model configured for classifier." };
578
+ }
579
+ const contextWindow = recentMessages.slice(-6);
580
+ const contextText = contextWindow.map((m) => `${m.role}: ${contentToString(m.content).slice(0, 200)}`).join("\n");
581
+ const userPrompt = `Tool: ${toolName}
582
+ Arguments: ${JSON.stringify(args, null, 2).slice(0, 1e3)}
583
+
584
+ Recent conversation context:
585
+ ${contextText}`;
586
+ const params = {
587
+ model,
588
+ system: opts?.classifierPrompt ?? DEFAULT_CLASSIFIER_PROMPT,
589
+ messages: [{ role: "user", content: userPrompt }],
590
+ max_tokens: 256,
591
+ temperature: 0,
592
+ outputFormat: {
593
+ type: "json_schema",
594
+ schema: {
595
+ type: "object",
596
+ properties: {
597
+ shouldBlock: { type: "boolean" },
598
+ reason: { type: "string" }
599
+ },
600
+ required: ["shouldBlock", "reason"],
601
+ additionalProperties: false
602
+ },
603
+ name: "classifier_result",
604
+ strict: true
605
+ }
606
+ };
607
+ try {
608
+ let text = "";
609
+ for await (const chunk of provider.chat(params)) {
610
+ if (opts?.signal?.aborted) break;
611
+ for (const choice of chunk.choices) {
612
+ if (choice.delta.content) {
613
+ text += choice.delta.content;
614
+ }
615
+ }
616
+ }
617
+ if (opts?.signal?.aborted) {
618
+ throw new DOMException("Aborted", "AbortError");
619
+ }
620
+ const parsed = JSON.parse(text);
621
+ return {
622
+ shouldBlock: parsed.shouldBlock ?? false,
623
+ reason: parsed.reason ?? "unknown"
624
+ };
625
+ } catch {
626
+ return { shouldBlock: true, reason: "Classifier failed; defaulting to block." };
627
+ }
628
+ }
629
+
630
+ // src/tools/shell-safety/git-safety.ts
631
+ var GIT_INTERNAL_PATTERNS = [
632
+ /\.git\/hooks\//,
633
+ /\.git\/config$/,
634
+ /\.git\/info\//,
635
+ /\.git\/objects\//,
636
+ /\.git\/refs\//,
637
+ /\.git\/HEAD$/,
638
+ /\.git\/index$/,
639
+ /\.git\/packed-refs$/,
640
+ /\.git\/shallow$/,
641
+ /\.git\/modules\//
642
+ ];
643
+ function isGitInternalPath(path6) {
644
+ const normalized = path6.replace(/\\/g, "/");
645
+ return GIT_INTERNAL_PATTERNS.some((p) => p.test(normalized));
646
+ }
647
+ var BARE_REPO_MARKERS = ["HEAD", "objects", "refs"];
648
+ function looksLikeBareRepo(dirEntries) {
649
+ const entrySet = new Set(dirEntries.map((e) => e.replace(/\/$/, "")));
650
+ if (entrySet.has(".git")) return false;
651
+ return BARE_REPO_MARKERS.every((m) => entrySet.has(m));
652
+ }
653
+ var BARE_REPO_INTERNAL_PATTERNS = [
654
+ /^hooks\//,
655
+ /^config$/,
656
+ /^info\//,
657
+ /^objects\//,
658
+ /^refs\//,
659
+ /^HEAD$/,
660
+ /^index$/,
661
+ /^packed-refs$/,
662
+ /^shallow$/,
663
+ /^modules\//
664
+ ];
665
+ function isBareRepoInternalPath(filePath) {
666
+ const normalized = filePath.replace(/\\/g, "/").replace(/^\.\//, "");
667
+ return BARE_REPO_INTERNAL_PATTERNS.some((p) => p.test(normalized));
668
+ }
669
+ function commandWritesGitInternals(command) {
670
+ const redirectPattern = /(?:>{1,2}|tee\s+)\s*(\S+)/g;
671
+ let match;
672
+ while ((match = redirectPattern.exec(command)) !== null) {
673
+ if (isGitInternalPath(match[1]) || isBareRepoInternalPath(match[1])) return true;
674
+ }
675
+ const copyPatternDotGit = /\b(?:cp|mv|ln)\b.*\s(\S*\.git\/\S+)/;
676
+ const copyMatchDotGit = command.match(copyPatternDotGit);
677
+ if (copyMatchDotGit && isGitInternalPath(copyMatchDotGit[1])) return true;
678
+ const copyPatternBare = /\b(?:cp|mv|ln)\b.*\s(\S+)/g;
679
+ let bareMatch;
680
+ while ((bareMatch = copyPatternBare.exec(command)) !== null) {
681
+ if (isBareRepoInternalPath(bareMatch[1])) return true;
682
+ }
683
+ return false;
684
+ }
685
+
686
+ // src/tools/shell-safety/command-classification.ts
687
+ var READ_ONLY_COMMANDS = /* @__PURE__ */ new Set([
688
+ "cat",
689
+ "head",
690
+ "tail",
691
+ "less",
692
+ "more",
693
+ "wc",
694
+ "file",
695
+ "which",
696
+ "whence",
697
+ "where",
698
+ "whereis",
699
+ "type",
700
+ "pwd",
701
+ "uname",
702
+ "whoami",
703
+ "id",
704
+ "groups",
705
+ "ls",
706
+ "ll",
707
+ "la",
708
+ "dir",
709
+ "stat",
710
+ "du",
711
+ "df",
712
+ "free",
713
+ "uptime",
714
+ "ps",
715
+ "top",
716
+ "htop",
717
+ "lsof",
718
+ "ss",
719
+ "netstat",
720
+ "ifconfig",
721
+ "ip",
722
+ "ping",
723
+ "dig",
724
+ "nslookup",
725
+ "host",
726
+ "traceroute",
727
+ "grep",
728
+ "egrep",
729
+ "fgrep",
730
+ "rg",
731
+ "ag",
732
+ "ack",
733
+ "locate",
734
+ "readlink",
735
+ "realpath",
736
+ "basename",
737
+ "dirname",
738
+ "diff",
739
+ "comm",
740
+ "sort",
741
+ "uniq",
742
+ "cut",
743
+ "tr",
744
+ "jq",
745
+ "yq",
746
+ "xxd",
747
+ "hexdump",
748
+ "od",
749
+ "md5sum",
750
+ "sha256sum",
751
+ "shasum",
752
+ "base64",
753
+ "true",
754
+ "false",
755
+ "test",
756
+ "[",
757
+ "[[",
758
+ "man",
759
+ "help",
760
+ "nproc",
761
+ "arch",
762
+ "lscpu",
763
+ "lsb_release",
764
+ "sw_vers",
765
+ "sysctl",
766
+ "getconf"
767
+ ]);
768
+ var CONDITIONAL_READ_ONLY = {
769
+ awk: () => false,
770
+ // awk has system() — never read-only
771
+ sed: (_cmd, tokens) => !tokens.some(
772
+ (t) => t === "-i" || t === "--in-place" || t.startsWith("-") && !t.startsWith("--") && t.includes("i")
773
+ ),
774
+ find: (cmd) => !/\b(-exec\b|-execdir\b|-ok\b|-okdir\b|-delete\b|-fprint\b|-fls\b|-fprintf\b)/.test(cmd),
775
+ fd: (_cmd, tokens) => !tokens.some((t) => ["-x", "--exec", "-X", "--exec-batch"].includes(t)),
776
+ fdfind: (_cmd, tokens) => !tokens.some((t) => ["-x", "--exec", "-X", "--exec-batch"].includes(t)),
777
+ date: (_cmd, tokens) => !tokens.some((t) => ["-s", "--set"].includes(t)),
778
+ hostname: (_cmd, tokens) => {
779
+ const positional = tokens.filter((t) => !t.startsWith("-"));
780
+ return positional.length === 0;
781
+ },
782
+ info: (_cmd, tokens) => !tokens.some((t) => ["-o", "--output", "--dribble", "--init-file"].includes(t)),
783
+ tree: (_cmd, tokens) => !tokens.some((t) => t === "-R"),
784
+ dotnet: (_cmd, tokens) => {
785
+ const positional = tokens.filter((t) => !t.startsWith("-"));
786
+ if (positional.length === 0) return true;
787
+ const sub = positional[0];
788
+ return ["--version", "--info", "--list-sdks", "--list-runtimes"].includes(sub) || tokens.includes("--version") || tokens.includes("--info") || tokens.includes("--list-sdks") || tokens.includes("--list-runtimes");
789
+ }
790
+ };
791
+ var GIT_READ_ONLY_SUBCOMMANDS = /* @__PURE__ */ new Set([
792
+ "status",
793
+ "log",
794
+ "diff",
795
+ "show",
796
+ "blame",
797
+ "shortlog",
798
+ "describe",
799
+ "rev-parse",
800
+ "rev-list",
801
+ "cat-file",
802
+ "ls-files",
803
+ "ls-tree",
804
+ "ls-remote",
805
+ "name-rev",
806
+ "for-each-ref",
807
+ "count-objects",
808
+ "fsck",
809
+ "verify-pack",
810
+ "stash",
811
+ // "stash list" / "stash show" — stash apply/pop are not here
812
+ "reflog",
813
+ // bare / "reflog show" / "reflog list" — expire/delete caught below
814
+ "tag",
815
+ // "tag -l" is safe; "tag <name>" creates — caught below
816
+ "branch",
817
+ // "branch --list" is safe; "branch <name>" creates — caught below
818
+ "remote",
819
+ // "remote -v" safe; "remote add/remove" — caught below
820
+ "config",
821
+ // "config --list/--get" safe
822
+ "help",
823
+ "version",
824
+ "--version",
825
+ "--help"
826
+ ]);
827
+ var GIT_READ_ONLY_WRITE_FLAGS = /* @__PURE__ */ new Set([
828
+ "--output"
829
+ ]);
830
+ var GIT_MUTATING_SUBCOMMANDS = /* @__PURE__ */ new Set([
831
+ "push",
832
+ "pull",
833
+ "fetch",
834
+ "merge",
835
+ "rebase",
836
+ "cherry-pick",
837
+ "revert",
838
+ "commit",
839
+ "add",
840
+ "rm",
841
+ "mv",
842
+ "init",
843
+ "clone",
844
+ "checkout",
845
+ "switch",
846
+ "restore",
847
+ "reset",
848
+ "clean",
849
+ "bisect",
850
+ "am",
851
+ "apply",
852
+ "format-patch",
853
+ "submodule",
854
+ "worktree"
855
+ ]);
856
+ var DESTRUCTIVE_PATTERNS = [
857
+ // rm -rf / rm -r / rm --recursive (but not plain rm single-file)
858
+ /\brm\s+(-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)\b/,
859
+ // rm on root-like paths
860
+ /\brm\s+.*\s+\/($|\s)/,
861
+ // git force operations
862
+ /\bgit\s+push\s+.*--force\b/,
863
+ /\bgit\s+push\s+-f\b/,
864
+ /\bgit\s+reset\s+--hard\b/,
865
+ /\bgit\s+clean\s+.*-[a-zA-Z]*f/,
866
+ /\bgit\s+checkout\s+--\s+\./,
867
+ // Filesystem destruction
868
+ /\bchmod\s+(-[a-zA-Z]*R[a-zA-Z]*|--recursive)\s+777\b/,
869
+ /\bchown\s+(-[a-zA-Z]*R[a-zA-Z]*|--recursive)\b/,
870
+ /\bdd\s+/,
871
+ /\bmkfs\b/,
872
+ /\bformat\b/,
873
+ /\bfdisk\b/,
874
+ // Dangerous redirects
875
+ />\s*\/dev\/sd[a-z]/,
876
+ // Database destructive operations
877
+ /\bDROP\s+(TABLE|DATABASE|SCHEMA)\b/i,
878
+ /\bTRUNCATE\s+TABLE\b/i,
879
+ /\bDELETE\s+FROM\b/i,
880
+ // sed in-place (matches -i anywhere in args, not just first flag)
881
+ /\bsed\b.*\s(-[a-zA-Z]*i[a-zA-Z]*|--in-place)\b/,
882
+ // Container/system destruction
883
+ /\bdocker\s+(rm|rmi|system\s+prune|volume\s+rm)\b/,
884
+ /\bkubectl\s+delete\b/,
885
+ // Kill processes
886
+ /\bkill\s+-9\b/,
887
+ /\bkillall\b/,
888
+ /\bpkill\b/,
889
+ // Recursive operations on root
890
+ /\bfind\s+\/\s+.*-delete\b/,
891
+ /\bfind\s+\/\s+.*-exec\s+rm\b/
892
+ ];
893
+ var SAFE_ECHO_RE = /^(?:echo|printf)(?:\s+(?:'[^']*'|[^|;&`$(){}><#\\!"'\s]+))*(?:\s+2>&1)?\s*$/;
894
+ function hasTokenFlag(tokens, ...flags) {
895
+ return tokens.some((t) => flags.includes(t));
896
+ }
897
+ function splitCompoundCommand(command) {
898
+ return command.split(/\s*(?:;|&&|\|\||(?<!\|)\|(?!\|))\s*/).map((s) => s.trim()).filter(Boolean);
899
+ }
900
+ var DANGEROUS_ENV_VARS = /* @__PURE__ */ new Set([
901
+ "GIT_CONFIG_GLOBAL",
902
+ "GIT_CONFIG_SYSTEM",
903
+ "GIT_DIR",
904
+ "GIT_WORK_TREE",
905
+ "GIT_EXEC_PATH",
906
+ "GIT_TEMPLATE_DIR",
907
+ "LD_PRELOAD",
908
+ "LD_LIBRARY_PATH",
909
+ "PATH",
910
+ "PYTHONPATH",
911
+ "NODE_PATH",
912
+ "PERL5LIB"
913
+ ]);
914
+ function hasDangerousEnvVars(command) {
915
+ const envPattern = /^[A-Za-z_][A-Za-z0-9_]*(?==)/;
916
+ let cmd = command.trim();
917
+ while (/^[A-Za-z_][A-Za-z0-9_]*=\S*\s/.test(cmd)) {
918
+ const match = cmd.match(envPattern);
919
+ if (match && DANGEROUS_ENV_VARS.has(match[0])) return true;
920
+ cmd = cmd.replace(/^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/, "");
921
+ }
922
+ return false;
923
+ }
924
+ var ZSH_DANGEROUS_COMMANDS = /* @__PURE__ */ new Set([
925
+ "zmodload",
926
+ "emulate",
927
+ "sysopen",
928
+ "sysread",
929
+ "syswrite",
930
+ "sysseek",
931
+ "zpty",
932
+ "ztcp",
933
+ "zsocket",
934
+ "zf_rm",
935
+ "zf_mv",
936
+ "zf_ln",
937
+ "zf_chmod",
938
+ "zf_chown",
939
+ "zf_mkdir",
940
+ "zf_rmdir",
941
+ "zf_chgrp"
942
+ ]);
943
+ var MAX_SUBCOMMANDS = 50;
944
+ function detectInjectionPatterns(command) {
945
+ if (/>\(/.test(command)) return "Output process substitution >(...)";
946
+ if (/=\(/.test(command)) return "Zsh =(...) process substitution";
947
+ if (/\$\{[^}]*[`$]/.test(command)) return "Nested expansion in ${...}";
948
+ if (/[\x00-\x08\x0e-\x1f\x7f]/.test(command)) return "Control character injection";
949
+ if (/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000\u200B-\u200D\uFEFF]/.test(command)) {
950
+ return "Unicode whitespace injection";
951
+ }
952
+ if (/\w#/.test(command) && !/['"][^'"]*#/.test(command)) {
953
+ const stripped = command.replace(/'[^']*'/g, "").replace(/"[^"]*"/g, "");
954
+ if (/\w#/.test(stripped)) return "Mid-word comment injection";
955
+ }
956
+ if (/\\n/.test(command)) {
957
+ const stripped = command.replace(/'[^']*'/g, "");
958
+ if (/\$'[^']*\\n/.test(stripped)) return "Escaped newline in $'...' string";
959
+ }
960
+ return null;
961
+ }
962
+ function hasCommandSubstitution(command) {
963
+ return /\$\(/.test(command) || /`[^`]+`/.test(command) || /<\(/.test(command);
964
+ }
965
+ function hasUnquotedExpansion(command) {
966
+ let inSingle = false;
967
+ for (let i = 0; i < command.length; i++) {
968
+ const ch = command[i];
969
+ if (ch === "'" && !inSingle) {
970
+ inSingle = true;
971
+ continue;
972
+ }
973
+ if (ch === "'" && inSingle) {
974
+ inSingle = false;
975
+ continue;
976
+ }
977
+ if (inSingle) continue;
978
+ if (ch === "\\" && i + 1 < command.length) {
979
+ i++;
980
+ continue;
981
+ }
982
+ if (ch === "$" && i + 1 < command.length) {
983
+ const next = command[i + 1];
984
+ if (next === "(") continue;
985
+ if (next === "{" || next >= "A" && next <= "Z" || next >= "a" && next <= "z" || next === "_") {
986
+ return true;
987
+ }
988
+ }
989
+ }
990
+ return false;
991
+ }
992
+ var WRAPPER_COMMANDS = ["sudo", "env", "nohup", "time", "nice", "ionice", "strace", "ltrace", "stdbuf"];
993
+ var WRAPPER_WITH_DURATION = /* @__PURE__ */ new Set(["timeout"]);
994
+ function stripEnvVars(cmd) {
995
+ let result = cmd.trim();
996
+ while (/^[A-Za-z_][A-Za-z0-9_]*=\S*\s/.test(result)) {
997
+ result = result.replace(/^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/, "");
998
+ }
999
+ return result;
1000
+ }
1001
+ function stripWrappers(cmd) {
1002
+ let result = cmd.trim();
1003
+ let prev = "";
1004
+ while (prev !== result) {
1005
+ prev = result;
1006
+ for (const prefix of WRAPPER_COMMANDS) {
1007
+ if (result.startsWith(prefix + " ")) {
1008
+ result = result.slice(prefix.length).trim();
1009
+ while (result.startsWith("-")) {
1010
+ const spaceIdx = result.indexOf(" ");
1011
+ if (spaceIdx === -1) break;
1012
+ result = result.slice(spaceIdx).trim();
1013
+ }
1014
+ }
1015
+ }
1016
+ for (const prefix of WRAPPER_WITH_DURATION) {
1017
+ if (result.startsWith(prefix + " ")) {
1018
+ result = result.slice(prefix.length).trim();
1019
+ while (result.startsWith("-")) {
1020
+ const spaceIdx = result.indexOf(" ");
1021
+ if (spaceIdx === -1) break;
1022
+ result = result.slice(spaceIdx).trim();
1023
+ }
1024
+ if (result && !result.startsWith("-")) {
1025
+ const spaceIdx = result.indexOf(" ");
1026
+ if (spaceIdx !== -1) {
1027
+ result = result.slice(spaceIdx).trim();
1028
+ }
1029
+ }
1030
+ }
1031
+ }
1032
+ }
1033
+ return result;
1034
+ }
1035
+ function stripPrefixes(command) {
1036
+ let cmd = command.trim();
1037
+ let prev = "";
1038
+ while (prev !== cmd) {
1039
+ prev = cmd;
1040
+ cmd = stripEnvVars(cmd);
1041
+ cmd = stripWrappers(cmd);
1042
+ }
1043
+ return cmd;
1044
+ }
1045
+ function extractCommandName(command) {
1046
+ const cmd = stripPrefixes(command);
1047
+ const firstToken = cmd.split(/\s/)[0] ?? "";
1048
+ const base = firstToken.includes("/") ? firstToken.split("/").pop() : firstToken;
1049
+ return base;
1050
+ }
1051
+ function classifyGitCommand(command) {
1052
+ if (/\bgit\s+--version\b/.test(command)) {
1053
+ return { isReadOnly: true, isDestructive: false, reason: "git --version is read-only" };
1054
+ }
1055
+ if (/\bgit\s+--help\b/.test(command)) {
1056
+ return { isReadOnly: true, isDestructive: false, reason: "git --help is read-only" };
1057
+ }
1058
+ if (/\bgit\s+(-c\s|--exec-path=|--config-env=)/.test(command)) {
1059
+ return { isReadOnly: false, isDestructive: true, reason: "git config injection vector (-c/--exec-path/--config-env)" };
1060
+ }
1061
+ const match = command.match(/\bgit\s+(?:--[a-z-]+=?\S*\s+)*([a-z][a-z-]*)/);
1062
+ if (!match) {
1063
+ return { isReadOnly: false, isDestructive: false, reason: "Cannot parse git subcommand" };
1064
+ }
1065
+ const subcommand = match[1];
1066
+ if (GIT_READ_ONLY_SUBCOMMANDS.has(subcommand)) {
1067
+ const afterSubcmd = command.slice(command.indexOf(subcommand) + subcommand.length).trim();
1068
+ const tokens = afterSubcmd.split(/\s+/).filter(Boolean);
1069
+ const positional = tokens.filter((t) => !t.startsWith("-"));
1070
+ const flags = tokens.filter((t) => t.startsWith("-"));
1071
+ if (subcommand === "branch") {
1072
+ if (hasTokenFlag(flags, "--list", "-l")) {
1073
+ return { isReadOnly: true, isDestructive: false, reason: "git branch --list is read-only" };
1074
+ }
1075
+ if (hasTokenFlag(flags, "-d", "-D", "--delete")) {
1076
+ return { isReadOnly: false, isDestructive: true, reason: "git branch delete" };
1077
+ }
1078
+ if (positional.length > 0) {
1079
+ return { isReadOnly: false, isDestructive: false, reason: "git branch create" };
1080
+ }
1081
+ }
1082
+ if (subcommand === "tag") {
1083
+ if (hasTokenFlag(flags, "-l", "--list")) {
1084
+ return { isReadOnly: true, isDestructive: false, reason: "git tag --list is read-only" };
1085
+ }
1086
+ if (hasTokenFlag(flags, "-d", "-D", "--delete")) {
1087
+ return { isReadOnly: false, isDestructive: true, reason: "git tag delete" };
1088
+ }
1089
+ if (positional.length > 0) {
1090
+ return { isReadOnly: false, isDestructive: false, reason: "git tag create" };
1091
+ }
1092
+ }
1093
+ if (subcommand === "stash") {
1094
+ const stashSubcmd = positional[0];
1095
+ if (stashSubcmd === "list" || stashSubcmd === "show") {
1096
+ return { isReadOnly: true, isDestructive: false, reason: `git stash ${stashSubcmd} is read-only` };
1097
+ }
1098
+ if (stashSubcmd === "drop" || stashSubcmd === "clear") {
1099
+ return { isReadOnly: false, isDestructive: true, reason: "git stash destructive operation" };
1100
+ }
1101
+ return { isReadOnly: false, isDestructive: false, reason: "git stash mutating operation" };
1102
+ }
1103
+ if (subcommand === "reflog") {
1104
+ const reflogSubcmd = positional[0];
1105
+ if (!reflogSubcmd || reflogSubcmd === "show" || reflogSubcmd === "list") {
1106
+ return { isReadOnly: true, isDestructive: false, reason: `git reflog${reflogSubcmd ? " " + reflogSubcmd : ""} is read-only` };
1107
+ }
1108
+ if (reflogSubcmd === "expire" || reflogSubcmd === "delete") {
1109
+ return { isReadOnly: false, isDestructive: true, reason: `git reflog ${reflogSubcmd} is destructive` };
1110
+ }
1111
+ return { isReadOnly: false, isDestructive: false, reason: `git reflog ${reflogSubcmd} is mutating` };
1112
+ }
1113
+ if (subcommand === "config") {
1114
+ if (hasTokenFlag(flags, "--set", "--add", "--unset", "--unset-all", "--replace-all", "--rename-section", "--remove-section")) {
1115
+ return { isReadOnly: false, isDestructive: false, reason: "git config write operation" };
1116
+ }
1117
+ if (positional.length >= 2) {
1118
+ return { isReadOnly: false, isDestructive: false, reason: "git config set key value" };
1119
+ }
1120
+ }
1121
+ if (subcommand === "remote") {
1122
+ const remoteSubcmd = positional[0];
1123
+ if (remoteSubcmd && ["add", "remove", "rename", "set-url", "set-branches", "prune"].includes(remoteSubcmd)) {
1124
+ return { isReadOnly: false, isDestructive: false, reason: "git remote mutating operation" };
1125
+ }
1126
+ }
1127
+ for (const flag of flags) {
1128
+ const flagName = flag.split("=")[0];
1129
+ if (GIT_READ_ONLY_WRITE_FLAGS.has(flagName)) {
1130
+ return { isReadOnly: false, isDestructive: false, reason: `git ${subcommand} with ${flagName} may write files` };
1131
+ }
1132
+ }
1133
+ return { isReadOnly: true, isDestructive: false, reason: `git ${subcommand} is read-only` };
1134
+ }
1135
+ if (GIT_MUTATING_SUBCOMMANDS.has(subcommand)) {
1136
+ for (const pattern of DESTRUCTIVE_PATTERNS) {
1137
+ if (pattern.test(command)) {
1138
+ return { isReadOnly: false, isDestructive: true, reason: `Destructive: ${pattern.source}` };
1139
+ }
1140
+ }
1141
+ return { isReadOnly: false, isDestructive: false, reason: `git ${subcommand} is mutating` };
1142
+ }
1143
+ return { isReadOnly: false, isDestructive: false, reason: `Unknown git subcommand: ${subcommand}` };
1144
+ }
1145
+ function classifySingleCommand(command, config) {
1146
+ const name = extractCommandName(command);
1147
+ if (!name) {
1148
+ return { isReadOnly: false, isDestructive: false, reason: "Empty command" };
1149
+ }
1150
+ const injectionReason = detectInjectionPatterns(command);
1151
+ if (injectionReason) {
1152
+ return {
1153
+ isReadOnly: false,
1154
+ isDestructive: true,
1155
+ reason: `Injection detected: ${injectionReason}`
1156
+ };
1157
+ }
1158
+ if (ZSH_DANGEROUS_COMMANDS.has(name)) {
1159
+ return {
1160
+ isReadOnly: false,
1161
+ isDestructive: true,
1162
+ reason: `Zsh dangerous command: ${name}`
1163
+ };
1164
+ }
1165
+ const allDestructive = [
1166
+ ...DESTRUCTIVE_PATTERNS,
1167
+ ...config?.extraDestructivePatterns ?? []
1168
+ ];
1169
+ for (const pattern of allDestructive) {
1170
+ if (pattern.test(command)) {
1171
+ return {
1172
+ isReadOnly: false,
1173
+ isDestructive: true,
1174
+ reason: `Matches destructive pattern: ${pattern.source}`
1175
+ };
1176
+ }
1177
+ }
1178
+ if (name === "git") {
1179
+ return classifyGitCommand(command);
1180
+ }
1181
+ if (name === "xargs" && /\bgit\b/.test(command)) {
1182
+ return classifyGitCommand(command);
1183
+ }
1184
+ if (hasCommandSubstitution(command)) {
1185
+ return { isReadOnly: false, isDestructive: false, reason: `Command contains command substitution` };
1186
+ }
1187
+ if (hasUnquotedExpansion(command)) {
1188
+ return { isReadOnly: false, isDestructive: false, reason: `Command contains unquoted variable expansion` };
1189
+ }
1190
+ if (hasDangerousEnvVars(command)) {
1191
+ return { isReadOnly: false, isDestructive: false, reason: `Command uses dangerous environment variable prefix` };
1192
+ }
1193
+ if ((name === "echo" || name === "printf") && SAFE_ECHO_RE.test(stripPrefixes(command).trim())) {
1194
+ return { isReadOnly: true, isDestructive: false, reason: `${name} with safe arguments is read-only` };
1195
+ }
1196
+ const conditionalCheck = CONDITIONAL_READ_ONLY[name];
1197
+ if (conditionalCheck) {
1198
+ const stripped = stripPrefixes(command).trim();
1199
+ const tokens = stripped.split(/\s+/).slice(1);
1200
+ if (conditionalCheck(command, tokens)) {
1201
+ return { isReadOnly: true, isDestructive: false, reason: `${name} is read-only (flags validated)` };
1202
+ }
1203
+ return { isReadOnly: false, isDestructive: false, reason: `${name} has potentially dangerous flags` };
1204
+ }
1205
+ const extraReadOnly = new Set(config?.extraReadOnlyCommands ?? []);
1206
+ if (READ_ONLY_COMMANDS.has(name) || extraReadOnly.has(name)) {
1207
+ return { isReadOnly: true, isDestructive: false, reason: `${name} is read-only` };
1208
+ }
1209
+ return {
1210
+ isReadOnly: false,
1211
+ isDestructive: false,
1212
+ reason: `${name} is not in the read-only allowlist`
1213
+ };
1214
+ }
1215
+ function classifyCommand(command, config) {
1216
+ if (!command.trim()) {
1217
+ return { isReadOnly: true, isDestructive: false, reason: "Empty command" };
1218
+ }
1219
+ const subCommands = splitCompoundCommand(command);
1220
+ if (subCommands.length === 0) {
1221
+ return { isReadOnly: true, isDestructive: false, reason: "Empty command" };
1222
+ }
1223
+ if (subCommands.length > MAX_SUBCOMMANDS) {
1224
+ return {
1225
+ isReadOnly: false,
1226
+ isDestructive: false,
1227
+ reason: `Too many subcommands (${subCommands.length} > ${MAX_SUBCOMMANDS})`
1228
+ };
1229
+ }
1230
+ if (subCommands.length > 1) {
1231
+ const hasCd = subCommands.some((s) => /^(cd|pushd)\s/.test(s.trim()));
1232
+ const hasGit = subCommands.some((s) => {
1233
+ const n = extractCommandName(s);
1234
+ return n === "git" || n === "xargs" && /\bgit\b/.test(s);
1235
+ });
1236
+ if (hasCd && hasGit) {
1237
+ return {
1238
+ isReadOnly: false,
1239
+ isDestructive: false,
1240
+ reason: "cd + git compound may escape working directory (bare-repo risk)"
1241
+ };
1242
+ }
1243
+ if (hasGit && commandWritesGitInternals(command)) {
1244
+ return {
1245
+ isReadOnly: false,
1246
+ isDestructive: true,
1247
+ reason: "Compound command writes to git internal paths before running git"
1248
+ };
1249
+ }
1250
+ }
1251
+ let allReadOnly = true;
1252
+ let anyDestructive = false;
1253
+ const reasons = [];
1254
+ for (const sub of subCommands) {
1255
+ const result = classifySingleCommand(sub, config);
1256
+ if (!result.isReadOnly) allReadOnly = false;
1257
+ if (result.isDestructive) anyDestructive = true;
1258
+ if (result.reason) reasons.push(result.reason);
1259
+ }
1260
+ return {
1261
+ isReadOnly: allReadOnly,
1262
+ isDestructive: anyDestructive,
1263
+ reason: reasons.join("; ")
1264
+ };
1265
+ }
1266
+
1267
+ // src/permissions/pipeline.ts
1268
+ import * as path5 from "path";
1269
+ import * as fs2 from "fs";
1270
+
1271
+ // src/tools/write.ts
1272
+ import * as nodePath from "path";
1273
+
1274
+ // src/tools/prompts/write.ts
1275
+ var WRITE_PROMPT = `Writes a file to the local filesystem. Parent directories are created automatically if they don't exist.
1276
+
1277
+ Usage:
1278
+ - This tool will overwrite the existing file if there is one at the provided path.
1279
+ - 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.
1280
+ - 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.
1281
+ - NEVER create documentation files (*.md) or README files unless explicitly requested by the User.
1282
+ - Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
1283
+ `;
1284
+
1285
+ // src/tools/file-lock.ts
1286
+ var fileLocks = /* @__PURE__ */ new Map();
1287
+ async function withFileLock(filePath, fn) {
1288
+ const prev = fileLocks.get(filePath) ?? Promise.resolve();
1289
+ let release;
1290
+ const lock = new Promise((resolve4) => {
1291
+ release = resolve4;
1292
+ });
1293
+ fileLocks.set(filePath, lock);
1294
+ await prev;
1295
+ try {
1296
+ return await fn();
1297
+ } finally {
1298
+ release();
1299
+ if (fileLocks.get(filePath) === lock) {
1300
+ fileLocks.delete(filePath);
1301
+ }
1302
+ }
1303
+ }
1304
+
1305
+ // src/tools/write.ts
1306
+ var writeFileTool = {
1307
+ name: "WriteFile",
1308
+ description: "Create or overwrite a file with the given content. Parent directories are created automatically if they don't exist.",
1309
+ prompt: WRITE_PROMPT,
1310
+ isReadOnly: false,
1311
+ checkPermissions(args, ctx) {
1312
+ const filePath = args.file_path;
1313
+ if (filePath.startsWith("\\\\") || filePath.startsWith("//")) {
1314
+ return {
1315
+ behavior: "deny",
1316
+ message: "Error: UNC paths are not allowed"
1317
+ };
1318
+ }
1319
+ if (isDangerousPath(filePath, ctx.cwd)) {
1320
+ return {
1321
+ behavior: "ask",
1322
+ message: `Write targets sensitive path: ${filePath}`,
1323
+ reason: "safetyCheck"
1324
+ };
1325
+ }
1326
+ return {
1327
+ behavior: "passthrough",
1328
+ message: `Write to ${filePath}`
1329
+ };
1330
+ },
1331
+ parameters: {
1332
+ type: "object",
1333
+ properties: {
1334
+ file_path: {
1335
+ type: "string",
1336
+ description: "The path of the file to write (absolute or relative to cwd)"
1337
+ },
1338
+ content: {
1339
+ type: "string",
1340
+ description: "The content to write to the file"
1341
+ }
1342
+ },
1343
+ required: ["file_path", "content"]
1344
+ },
1345
+ async call(args, ctx) {
1346
+ const filePath = args.file_path;
1347
+ const content = args.content;
1348
+ try {
1349
+ if (ctx.checkpointManager && ctx.currentMessageId) {
1350
+ await ctx.checkpointManager.trackEdit(filePath, ctx.currentMessageId, ctx.sessionId ?? "");
1351
+ }
1352
+ const existed = await ctx.fs.exists(filePath);
1353
+ if (existed && ctx.fileStateCache) {
1354
+ const cached = ctx.fileStateCache.get(filePath);
1355
+ if (!cached) {
1356
+ return {
1357
+ content: `Error: File ${filePath} exists but has not been read yet. Read it first before overwriting.`,
1358
+ isError: true
1359
+ };
1360
+ }
1361
+ try {
1362
+ const stat = await ctx.fs.stat(filePath);
1363
+ const mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
1364
+ if (mtime > cached.timestamp) {
1365
+ const currentContent = await ctx.fs.readFile(filePath);
1366
+ if (currentContent !== cached.content) {
1367
+ return {
1368
+ content: `Error: ${filePath} has been modified since last read. Re-read the file before overwriting.`,
1369
+ isError: true
1370
+ };
1371
+ }
1372
+ }
1373
+ } catch {
1374
+ }
1375
+ }
1376
+ const dir = nodePath.dirname(filePath);
1377
+ if (dir && dir !== "." && dir !== "/") {
1378
+ await ctx.fs.mkdir(dir, { recursive: true }).catch(() => {
1379
+ });
1380
+ }
1381
+ await withFileLock(filePath, async () => {
1382
+ await ctx.fs.writeFile(filePath, content);
1383
+ });
1384
+ ctx.notifyHook?.("FileWrite", {
1385
+ event: "FileWrite",
1386
+ sessionId: ctx.sessionId ?? "",
1387
+ toolName: "WriteFile",
1388
+ filePath,
1389
+ isNew: !existed
1390
+ }).catch(() => {
1391
+ });
1392
+ if (ctx.fileStateCache) {
1393
+ let mtime = 0;
1394
+ try {
1395
+ const stat = await ctx.fs.stat(filePath);
1396
+ mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
1397
+ } catch {
1398
+ }
1399
+ ctx.fileStateCache.set(filePath, {
1400
+ content,
1401
+ timestamp: mtime
1402
+ });
1403
+ }
1404
+ return {
1405
+ content: existed ? `File updated successfully at: ${filePath}` : `File created successfully at: ${filePath}`
1406
+ };
1407
+ } catch (err) {
1408
+ return {
1409
+ content: `Error writing file: ${err instanceof Error ? err.message : String(err)}`,
1410
+ isError: true
1411
+ };
1412
+ }
1413
+ }
1414
+ };
1415
+
1416
+ // src/tools/edit.ts
1417
+ import * as nodePath2 from "path";
1418
+
1419
+ // src/tools/edit-utils.ts
1420
+ var LEFT_SINGLE_CURLY = "\u2018";
1421
+ var RIGHT_SINGLE_CURLY = "\u2019";
1422
+ var LEFT_DOUBLE_CURLY = "\u201C";
1423
+ var RIGHT_DOUBLE_CURLY = "\u201D";
1424
+ function normalizeQuotes(str) {
1425
+ return str.replaceAll(LEFT_SINGLE_CURLY, "'").replaceAll(RIGHT_SINGLE_CURLY, "'").replaceAll(LEFT_DOUBLE_CURLY, '"').replaceAll(RIGHT_DOUBLE_CURLY, '"');
1426
+ }
1427
+ function findActualString(fileContent, searchString) {
1428
+ if (fileContent.includes(searchString)) {
1429
+ return searchString;
1430
+ }
1431
+ const normalizedSearch = normalizeQuotes(searchString);
1432
+ const normalizedFile = normalizeQuotes(fileContent);
1433
+ const searchIndex = normalizedFile.indexOf(normalizedSearch);
1434
+ if (searchIndex !== -1) {
1435
+ return fileContent.substring(searchIndex, searchIndex + searchString.length);
1436
+ }
1437
+ return null;
1438
+ }
1439
+ function countOccurrences(haystack, needle) {
1440
+ const normalizedNeedle = normalizeQuotes(needle);
1441
+ const normalizedHaystack = normalizeQuotes(haystack);
1442
+ let count = 0;
1443
+ let pos = 0;
1444
+ while (true) {
1445
+ const idx = normalizedHaystack.indexOf(normalizedNeedle, pos);
1446
+ if (idx === -1) break;
1447
+ count++;
1448
+ pos = idx + 1;
1449
+ }
1450
+ return count;
1451
+ }
1452
+ function usesCurlyQuotes(str) {
1453
+ return {
1454
+ singleCurly: str.includes(LEFT_SINGLE_CURLY) || str.includes(RIGHT_SINGLE_CURLY),
1455
+ doubleCurly: str.includes(LEFT_DOUBLE_CURLY) || str.includes(RIGHT_DOUBLE_CURLY)
1456
+ };
1457
+ }
1458
+ function preserveQuoteStyle(oldString, actualOldString, newString) {
1459
+ if (oldString === actualOldString) {
1460
+ return newString;
1461
+ }
1462
+ const fileStyle = usesCurlyQuotes(actualOldString);
1463
+ let result = newString;
1464
+ if (fileStyle.singleCurly) {
1465
+ result = convertStraightToCurlySingle(result);
1466
+ }
1467
+ if (fileStyle.doubleCurly) {
1468
+ result = convertStraightToCurlyDouble(result);
1469
+ }
1470
+ return result;
1471
+ }
1472
+ function convertStraightToCurlySingle(str) {
1473
+ let result = "";
1474
+ let inWord = false;
1475
+ for (let i = 0; i < str.length; i++) {
1476
+ const ch = str[i];
1477
+ if (ch === "'") {
1478
+ const prev = i > 0 ? str[i - 1] : " ";
1479
+ if (/\s/.test(prev) || prev === "(" || prev === "[" || prev === "{") {
1480
+ result += LEFT_SINGLE_CURLY;
1481
+ inWord = true;
1482
+ } else {
1483
+ result += RIGHT_SINGLE_CURLY;
1484
+ inWord = false;
1485
+ }
1486
+ } else {
1487
+ result += ch;
1488
+ inWord = /\w/.test(ch);
1489
+ }
1490
+ }
1491
+ return result;
1492
+ }
1493
+ function convertStraightToCurlyDouble(str) {
1494
+ let result = "";
1495
+ let open = true;
1496
+ for (let i = 0; i < str.length; i++) {
1497
+ const ch = str[i];
1498
+ if (ch === '"') {
1499
+ result += open ? LEFT_DOUBLE_CURLY : RIGHT_DOUBLE_CURLY;
1500
+ open = !open;
1501
+ } else {
1502
+ result += ch;
1503
+ }
1504
+ }
1505
+ return result;
1506
+ }
1507
+ function stripTrailingWhitespace(str) {
1508
+ const parts = str.split(/(\r\n|\n|\r)/);
1509
+ const result = [];
1510
+ for (let i = 0; i < parts.length; i++) {
1511
+ const part = parts[i];
1512
+ if (i % 2 === 0) {
1513
+ result.push(part.replace(/[\t ]+$/, ""));
1514
+ } else {
1515
+ result.push(part);
1516
+ }
1517
+ }
1518
+ return result.join("");
1519
+ }
1520
+
1521
+ // src/tools/prompts/edit.ts
1522
+ var EDIT_PROMPT = `Performs exact string replacements in files.
1523
+
1524
+ Usage:
1525
+ - 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.
1526
+ - 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.
1527
+ - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
1528
+ - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
1529
+ - 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\`.
1530
+ - Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
1531
+ `;
1532
+
1533
+ // src/tools/edit.ts
1534
+ var editFileTool = {
1535
+ name: "EditFile",
1536
+ 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.",
1537
+ prompt: EDIT_PROMPT,
1538
+ isReadOnly: false,
1539
+ checkPermissions(args, ctx) {
1540
+ const filePath = args.file_path;
1541
+ if (filePath.startsWith("\\\\") || filePath.startsWith("//")) {
1542
+ return {
1543
+ behavior: "deny",
1544
+ message: "Error: UNC paths are not allowed"
1545
+ };
1546
+ }
1547
+ if (isDangerousPath(filePath, ctx.cwd)) {
1548
+ return {
1549
+ behavior: "ask",
1550
+ message: `Edit targets sensitive path: ${filePath}`,
1551
+ reason: "safetyCheck"
1552
+ };
1553
+ }
1554
+ return {
1555
+ behavior: "passthrough",
1556
+ message: `Edit ${filePath}`
1557
+ };
1558
+ },
1559
+ parameters: {
1560
+ type: "object",
1561
+ properties: {
1562
+ file_path: {
1563
+ type: "string",
1564
+ description: "The path of the file to edit"
1565
+ },
1566
+ old_string: {
1567
+ type: "string",
1568
+ description: "The exact string to find and replace"
1569
+ },
1570
+ new_string: {
1571
+ type: "string",
1572
+ description: "The replacement string"
1573
+ },
1574
+ replace_all: {
1575
+ type: "boolean",
1576
+ description: "If true, replace all occurrences of old_string. Defaults to false."
1577
+ }
1578
+ },
1579
+ required: ["file_path", "old_string", "new_string"]
1580
+ },
1581
+ async call(args, ctx) {
1582
+ const filePath = args.file_path;
1583
+ const oldString = args.old_string;
1584
+ const newString = args.new_string;
1585
+ const replaceAll = args.replace_all ?? false;
1586
+ if (filePath.endsWith(".ipynb")) {
1587
+ return {
1588
+ content: `Error: ${filePath} is a Jupyter Notebook. Use the NotebookEdit tool to edit notebook files.`,
1589
+ isError: true
1590
+ };
1591
+ }
1592
+ if (oldString === newString) {
1593
+ return {
1594
+ content: "No changes to make: old_string and new_string are exactly the same.",
1595
+ isError: true
1596
+ };
1597
+ }
1598
+ if (oldString === "") {
1599
+ const exists = await ctx.fs.exists(filePath);
1600
+ if (!exists) {
1601
+ if (ctx.checkpointManager && ctx.currentMessageId) {
1602
+ await ctx.checkpointManager.trackEdit(filePath, ctx.currentMessageId, ctx.sessionId ?? "");
1603
+ }
1604
+ await ctx.fs.writeFile(filePath, newString);
1605
+ ctx.notifyHook?.("FileWrite", {
1606
+ event: "FileWrite",
1607
+ sessionId: ctx.sessionId ?? "",
1608
+ toolName: "EditFile",
1609
+ filePath,
1610
+ isNew: true
1611
+ }).catch(() => {
1612
+ });
1613
+ if (ctx.fileStateCache) {
1614
+ ctx.fileStateCache.set(filePath, { content: newString, timestamp: Date.now() });
1615
+ }
1616
+ return { content: `Created new file ${filePath}.` };
1617
+ }
1618
+ const existing = await ctx.fs.readFile(filePath);
1619
+ if (existing.trim() !== "") {
1620
+ return {
1621
+ content: "Error: old_string is empty but file already has content. Use WriteFile to overwrite, or provide the exact text to replace.",
1622
+ isError: true
1623
+ };
1624
+ }
1625
+ if (ctx.checkpointManager && ctx.currentMessageId) {
1626
+ await ctx.checkpointManager.trackEdit(filePath, ctx.currentMessageId, ctx.sessionId ?? "");
1627
+ }
1628
+ await ctx.fs.writeFile(filePath, newString);
1629
+ if (ctx.fileStateCache) {
1630
+ ctx.fileStateCache.set(filePath, { content: newString, timestamp: Date.now() });
1631
+ }
1632
+ return { content: `File ${filePath} has been updated successfully.` };
1633
+ }
1634
+ const MAX_EDIT_FILE_SIZE = 1024 * 1024 * 1024;
1635
+ try {
1636
+ try {
1637
+ const stat = await ctx.fs.stat(filePath);
1638
+ if (stat.size !== void 0 && stat.size > MAX_EDIT_FILE_SIZE) {
1639
+ return {
1640
+ content: `Error: File is too large to edit (${Math.round(stat.size / 1024 / 1024)} MiB). Max: 1 GiB.`,
1641
+ isError: true
1642
+ };
1643
+ }
1644
+ } catch {
1645
+ }
1646
+ if (ctx.fileStateCache) {
1647
+ const cached = ctx.fileStateCache.get(filePath);
1648
+ if (!cached || cached.isPartialView) {
1649
+ return {
1650
+ content: `Error: File has not been read yet. Use ReadFile on ${filePath} before editing.`,
1651
+ isError: true
1652
+ };
1653
+ }
1654
+ try {
1655
+ const stat = await ctx.fs.stat(filePath);
1656
+ const mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
1657
+ if (mtime > cached.timestamp) {
1658
+ const currentContent = await ctx.fs.readFile(filePath);
1659
+ if (currentContent !== cached.content) {
1660
+ return {
1661
+ content: `Error: ${filePath} has been modified since last read. Re-read the file before editing.`,
1662
+ isError: true
1663
+ };
1664
+ }
1665
+ }
1666
+ } catch {
1667
+ }
1668
+ }
1669
+ if (ctx.checkpointManager && ctx.currentMessageId) {
1670
+ await ctx.checkpointManager.trackEdit(filePath, ctx.currentMessageId, ctx.sessionId ?? "");
1671
+ }
1672
+ const dir = nodePath2.dirname(filePath);
1673
+ if (dir && dir !== "." && dir !== "/") {
1674
+ await ctx.fs.mkdir(dir, { recursive: true }).catch(() => {
1675
+ });
1676
+ }
1677
+ const updated = await withFileLock(filePath, async () => {
1678
+ const rawContent = await ctx.fs.readFile(filePath);
1679
+ const hasCRLF = rawContent.includes("\r\n");
1680
+ const content = hasCRLF ? rawContent.replaceAll("\r\n", "\n") : rawContent;
1681
+ const actualOldString = findActualString(content, oldString);
1682
+ if (!actualOldString) {
1683
+ return {
1684
+ error: `Error: old_string not found in ${filePath}. Make sure the string matches exactly, including whitespace and indentation.`
1685
+ };
1686
+ }
1687
+ if (!replaceAll) {
1688
+ const count = content.split(actualOldString).length - 1;
1689
+ if (count > 1) {
1690
+ return {
1691
+ error: `Error: old_string appears ${count} times in ${filePath}. Provide more context to make it unique, or set replace_all to true.`
1692
+ };
1693
+ }
1694
+ }
1695
+ const actualNewString = preserveQuoteStyle(oldString, actualOldString, newString);
1696
+ let result;
1697
+ if (replaceAll) {
1698
+ result = content.split(actualOldString).join(actualNewString);
1699
+ } else if (actualNewString === "") {
1700
+ const hasTrailingNewline = !actualOldString.endsWith("\n") && content.includes(actualOldString + "\n");
1701
+ const deleteTarget = hasTrailingNewline ? actualOldString + "\n" : actualOldString;
1702
+ result = content.replace(deleteTarget, () => actualNewString);
1703
+ } else {
1704
+ result = content.replace(actualOldString, () => actualNewString);
1705
+ }
1706
+ if (hasCRLF) {
1707
+ result = result.replaceAll("\n", "\r\n");
1708
+ }
1709
+ await ctx.fs.writeFile(filePath, result);
1710
+ return { content: result };
1711
+ });
1712
+ if ("error" in updated) {
1713
+ return { content: String(updated.error), isError: true };
1714
+ }
1715
+ const editedContent = updated.content;
1716
+ ctx.notifyHook?.("FileWrite", {
1717
+ event: "FileWrite",
1718
+ sessionId: ctx.sessionId ?? "",
1719
+ toolName: "EditFile",
1720
+ filePath,
1721
+ isNew: false
1722
+ }).catch(() => {
1723
+ });
1724
+ if (ctx.fileStateCache) {
1725
+ let mtime = 0;
1726
+ try {
1727
+ const stat = await ctx.fs.stat(filePath);
1728
+ mtime = stat.modifiedAt ? Math.floor(stat.modifiedAt.getTime()) : 0;
1729
+ } catch {
1730
+ }
1731
+ ctx.fileStateCache.set(filePath, {
1732
+ content: editedContent,
1733
+ timestamp: mtime
1734
+ });
1735
+ }
1736
+ return {
1737
+ content: `File ${filePath} has been updated successfully.`
1738
+ };
1739
+ } catch (err) {
1740
+ return {
1741
+ content: `Error editing file: ${err instanceof Error ? err.message : String(err)}`,
1742
+ isError: true
1743
+ };
1744
+ }
1745
+ }
1746
+ };
1747
+
1748
+ // src/tools/shell-safety/git-tracking.ts
1749
+ function detectGitOperations(command, stdout) {
1750
+ const events = [];
1751
+ const cmd = command.trim();
1752
+ if (/\bgit\s+commit\b/.test(cmd)) {
1753
+ const shaMatch = stdout.match(/\[[\w/.-]+\s+([0-9a-f]{7,40})\]/);
1754
+ const sha = shaMatch ? shaMatch[1] : "unknown";
1755
+ events.push({ type: "commit", details: `commit ${sha}` });
1756
+ }
1757
+ if (/\bgit\s+merge\b/.test(cmd)) {
1758
+ const branchMatch = cmd.match(/\bgit\s+merge\s+(?:--\S+\s+)*(\S+)/);
1759
+ const branch = branchMatch ? branchMatch[1] : "unknown";
1760
+ events.push({ type: "merge", details: `merge ${branch}` });
1761
+ }
1762
+ if (/\bgit\s+rebase\b/.test(cmd)) {
1763
+ const branchMatch = cmd.match(/\bgit\s+rebase\s+(?:--\S+\s+)*(\S+)/);
1764
+ const branch = branchMatch ? branchMatch[1] : "unknown";
1765
+ events.push({ type: "rebase", details: `rebase onto ${branch}` });
1766
+ }
1767
+ if (/\bgit\s+push\b/.test(cmd)) {
1768
+ const remoteMatch = cmd.match(/\bgit\s+push\s+(?:--\S+\s+)*(\S+)/);
1769
+ const remote = remoteMatch ? remoteMatch[1] : "origin";
1770
+ const branchMatch = stdout.match(/\S+\s+->\s+(\S+)/);
1771
+ const branch = branchMatch ? branchMatch[1] : "";
1772
+ events.push({
1773
+ type: "push",
1774
+ details: `push to ${remote}${branch ? ` (${branch})` : ""}`
1775
+ });
1776
+ }
1777
+ if (/\b(gh\s+pr\s+create|glab\s+mr\s+create)\b/.test(cmd)) {
1778
+ const urlMatch = stdout.match(/(https?:\/\/\S+(?:pull|merge_requests)\/\d+)/);
1779
+ const url = urlMatch ? urlMatch[1] : "";
1780
+ events.push({
1781
+ type: "pr_create",
1782
+ details: url ? `PR created: ${url}` : "PR created"
1783
+ });
1784
+ }
1785
+ return events;
1786
+ }
1787
+ function hasGitIndexLockError(output) {
1788
+ return /\.git\/index\.lock/.test(output);
1789
+ }
1790
+
1791
+ // src/tools/prompts/bash.ts
1792
+ var BASH_PROMPT = `Executes a given bash command and returns its output.
1793
+
1794
+ The working directory persists between commands, but shell state does not.
1795
+
1796
+ 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:
1797
+
1798
+ - File search: Use Glob (NOT find or ls)
1799
+ - Content search: Use Grep (NOT grep or rg)
1800
+ - Read files: Use ReadFile (NOT cat/head/tail)
1801
+ - Edit files: Use EditFile (NOT sed/awk)
1802
+ - Write files: Use WriteFile (NOT echo >/cat <<EOF)
1803
+ - Communication: Output text directly (NOT echo/printf)
1804
+
1805
+ 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.
1806
+
1807
+ # Instructions
1808
+
1809
+ - 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.
1810
+ - Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt").
1811
+ - 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.
1812
+ - You may specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). By default, your command will timeout after 30000ms (0.5 minutes).
1813
+ - When issuing multiple commands:
1814
+ - If the commands are independent and can run in parallel, make multiple Bash tool calls in a single message.
1815
+ - If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together.
1816
+ - Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.
1817
+ - DO NOT use newlines to separate commands (newlines are ok in quoted strings).
1818
+ - For git commands:
1819
+ - Prefer to create a new commit rather than amending an existing commit.
1820
+ - 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.
1821
+ - 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.
1822
+ - Avoid unnecessary \`sleep\` commands:
1823
+ - Do not sleep between commands that can run immediately \u2014 just run them.
1824
+ - Do not retry failing commands in a sleep loop \u2014 diagnose the root cause.
1825
+ - If you must sleep, keep the duration short (1-5 seconds) to avoid blocking the user.
1826
+ `;
1827
+
1828
+ // src/tools/bash.ts
1829
+ var MAX_OUTPUT_CHARS = 1e5;
1830
+ var NON_ERROR_EXIT1_COMMANDS = /* @__PURE__ */ new Set([
1831
+ "grep",
1832
+ "egrep",
1833
+ "fgrep",
1834
+ "rg",
1835
+ "ag",
1836
+ "ack",
1837
+ "diff",
1838
+ "comm"
1839
+ ]);
1840
+ function isExpectedNonZeroExit(command, exitCode) {
1841
+ if (exitCode !== 1) return false;
1842
+ const name = extractCommandName(command);
1843
+ return NON_ERROR_EXIT1_COMMANDS.has(name);
1844
+ }
1845
+ var bashTool = {
1846
+ name: "Bash",
1847
+ description: "Execute a bash shell command. Use this for running scripts, installing packages, git operations, and other system commands.",
1848
+ prompt: BASH_PROMPT,
1849
+ isReadOnly(args) {
1850
+ const command = args.command;
1851
+ return classifyCommand(command).isReadOnly;
1852
+ },
1853
+ isDestructive(args) {
1854
+ const command = args.command;
1855
+ return classifyCommand(command).isDestructive;
1856
+ },
1857
+ checkPermissions(args) {
1858
+ const command = args.command;
1859
+ const classification = classifyCommand(command);
1860
+ if (classification.isDestructive) {
1861
+ return {
1862
+ behavior: "ask",
1863
+ message: `Destructive command: ${command}${classification.reason ? ` (${classification.reason})` : ""}`
1864
+ };
1865
+ }
1866
+ if (commandWritesGitInternals(command)) {
1867
+ return {
1868
+ behavior: "ask",
1869
+ message: `Command writes to .git/ internals: ${command}`
1870
+ };
1871
+ }
1872
+ return {
1873
+ behavior: "passthrough",
1874
+ message: `Execute: ${command}`
1875
+ };
1876
+ },
1877
+ parameters: {
1878
+ type: "object",
1879
+ properties: {
1880
+ command: {
1881
+ type: "string",
1882
+ description: "The bash command to execute"
1883
+ },
1884
+ timeout: {
1885
+ type: "number",
1886
+ description: "Timeout in milliseconds (default: 30000)"
1887
+ },
1888
+ description: {
1889
+ type: "string",
1890
+ description: "Short description of what this command does (5-10 words)"
1891
+ }
1892
+ },
1893
+ required: ["command"]
1894
+ },
1895
+ async call(args, ctx) {
1896
+ const command = args.command;
1897
+ const timeout = args.timeout;
1898
+ try {
1899
+ const result = await ctx.computer.executeCommand(command, {
1900
+ timeout,
1901
+ cwd: ctx.cwd
1902
+ });
1903
+ let output = "";
1904
+ if (result.stdout) {
1905
+ output += result.stdout;
1906
+ }
1907
+ if (result.stderr) {
1908
+ if (output) output += "\n";
1909
+ output += `STDERR:
1910
+ ${result.stderr}`;
1911
+ }
1912
+ if (!output.trim()) {
1913
+ output = "(no output)";
1914
+ }
1915
+ if (output.length > MAX_OUTPUT_CHARS) {
1916
+ const headSize = Math.floor(MAX_OUTPUT_CHARS * 0.8);
1917
+ const tailSize = MAX_OUTPUT_CHARS - headSize;
1918
+ const dropped = output.length - MAX_OUTPUT_CHARS;
1919
+ output = output.slice(0, headSize) + `
1920
+
1921
+ ... ${dropped} chars truncated ...
1922
+
1923
+ ` + output.slice(-tailSize);
1924
+ }
1925
+ const isSemanticError = result.exitCode !== 0 && !isExpectedNonZeroExit(command, result.exitCode);
1926
+ if (result.exitCode !== 0) {
1927
+ output = `Exit code: ${result.exitCode}
1928
+ ${output}`;
1929
+ }
1930
+ const toolResult = {
1931
+ content: output,
1932
+ isError: isSemanticError
1933
+ };
1934
+ if (result.exitCode === 0) {
1935
+ const gitOps = detectGitOperations(command, result.stdout ?? "");
1936
+ if (gitOps.length > 0) {
1937
+ toolResult.metadata = { gitOperations: gitOps };
1938
+ }
1939
+ }
1940
+ return toolResult;
1941
+ } catch (err) {
1942
+ return {
1943
+ content: `Error executing command: ${err instanceof Error ? err.message : String(err)}`,
1944
+ isError: true
1945
+ };
1946
+ }
1947
+ }
1948
+ };
1949
+
1950
+ // src/tools/glob.ts
1951
+ import * as path3 from "path";
1952
+
1953
+ // src/tools/prompts/glob.ts
1954
+ var GLOB_PROMPT = `Fast file pattern matching tool that works with any codebase size.
1955
+
1956
+ - Supports glob patterns like "**/*.js" or "src/**/*.ts"
1957
+ - Returns matching file paths sorted by modification time
1958
+ - Use this tool when you need to find files by name patterns
1959
+ - When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead
1960
+ `;
1961
+
1962
+ // src/utils/shell-escape.ts
1963
+ function shellEscape(s) {
1964
+ return "'" + s.replace(/'/g, "'\\''") + "'";
1965
+ }
1966
+
1967
+ // src/tools/glob.ts
1968
+ var MAX_RESULTS = 200;
1969
+ var globTool = {
1970
+ name: "Glob",
1971
+ 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.",
1972
+ prompt: GLOB_PROMPT,
1973
+ isReadOnly: true,
1974
+ isConcurrencySafe: true,
1975
+ parameters: {
1976
+ type: "object",
1977
+ properties: {
1978
+ pattern: {
1979
+ type: "string",
1980
+ description: 'Glob pattern to match files (e.g. "*.ts", "src/**/*.tsx")'
1981
+ },
1982
+ path: {
1983
+ type: "string",
1984
+ description: "Directory to search in (defaults to cwd)"
1985
+ }
1986
+ },
1987
+ required: ["pattern"]
1988
+ },
1989
+ async call(args, ctx) {
1990
+ const pattern = args.pattern;
1991
+ const searchPath = args.path ?? ctx.cwd;
1992
+ const fullPattern = pattern.startsWith("**/") || path3.isAbsolute(pattern) ? pattern : `**/${pattern}`;
1993
+ const resolvedPath = searchPath === ctx.cwd ? "." : searchPath;
1994
+ const command = `rg --files --hidden --glob ${shellEscape(fullPattern)} --sortr=modified ${shellEscape(resolvedPath)} | head -n ${String(MAX_RESULTS + 1)}`;
1995
+ try {
1996
+ let result = await ctx.computer.executeCommand(command, {
1997
+ cwd: ctx.cwd
1998
+ });
1999
+ if (result.exitCode === 127 || result.stderr?.includes("not found")) {
2000
+ const findCommand = `find ${shellEscape(resolvedPath)} -name ${shellEscape(pattern)} -type f | head -n ${String(MAX_RESULTS + 1)}`;
2001
+ result = await ctx.computer.executeCommand(findCommand, {
2002
+ cwd: ctx.cwd
2003
+ });
2004
+ }
2005
+ if (result.exitCode > 1) {
2006
+ return {
2007
+ content: `Glob error: ${result.stderr || result.stdout}`,
2008
+ isError: true
2009
+ };
2010
+ }
2011
+ const lines = result.stdout.split("\n").filter((l) => l.trim() !== "");
2012
+ if (lines.length === 0) {
2013
+ return { content: "No files found matching the pattern." };
2014
+ }
2015
+ const truncated = lines.length > MAX_RESULTS;
2016
+ const files = truncated ? lines.slice(0, MAX_RESULTS) : lines;
2017
+ let output = files.join("\n");
2018
+ if (truncated) {
2019
+ output += `
2020
+
2021
+ (Results truncated. More than ${MAX_RESULTS} files match.)`;
2022
+ }
2023
+ return { content: output };
2024
+ } catch (err) {
2025
+ return {
2026
+ content: `Error searching files: ${err instanceof Error ? err.message : String(err)}`,
2027
+ isError: true
2028
+ };
2029
+ }
2030
+ }
2031
+ };
2032
+
2033
+ // src/tools/prompts/grep.ts
2034
+ var GREP_PROMPT = `A powerful search tool built on ripgrep.
2035
+
2036
+ Usage:
2037
+ - 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.
2038
+ - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
2039
+ - Filter files with the glob parameter (e.g., "*.js", "**/*.tsx")
2040
+ - Returns matching lines with file paths and line numbers
2041
+ - Use the Agent tool for open-ended searches requiring multiple rounds
2042
+ - Pattern syntax: Uses ripgrep (not grep) \u2014 literal braces need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)
2043
+ - Multiline matching: By default patterns match within single lines only. For cross-line patterns like \`struct \\{[\\s\\S]*?field\`, pass a context_lines parameter.
2044
+ `;
2045
+
2046
+ // src/tools/grep.ts
2047
+ var MAX_MATCHES = 250;
2048
+ var grepTool = {
2049
+ name: "Grep",
2050
+ description: "Search file contents using ripgrep (rg). Supports regex patterns. Returns matching lines with file paths and line numbers.",
2051
+ prompt: GREP_PROMPT,
2052
+ isReadOnly: true,
2053
+ isConcurrencySafe: true,
2054
+ parameters: {
2055
+ type: "object",
2056
+ properties: {
2057
+ pattern: {
2058
+ type: "string",
2059
+ description: "Regular expression pattern to search for"
2060
+ },
2061
+ path: {
2062
+ type: "string",
2063
+ description: "File or directory to search in (defaults to cwd)"
2064
+ },
2065
+ glob: {
2066
+ type: "string",
2067
+ description: 'Glob pattern to filter files (e.g. "*.ts", "*.{js,jsx}")'
2068
+ },
2069
+ case_insensitive: {
2070
+ type: "boolean",
2071
+ description: "Case insensitive search (default: false)"
2072
+ },
2073
+ context_lines: {
2074
+ type: "number",
2075
+ description: "Number of context lines to show before and after each match"
2076
+ }
2077
+ },
2078
+ required: ["pattern"]
2079
+ },
2080
+ async call(args, ctx) {
2081
+ const pattern = args.pattern;
2082
+ const searchPath = args.path ?? ctx.cwd;
2083
+ const glob = args.glob;
2084
+ const caseInsensitive = args.case_insensitive;
2085
+ const contextLines = args.context_lines;
2086
+ const rgArgs = [
2087
+ "rg",
2088
+ "--line-number",
2089
+ "--no-heading",
2090
+ "--color=never",
2091
+ "--hidden",
2092
+ "--glob",
2093
+ "'!.git'",
2094
+ "--glob",
2095
+ "'!.svn'",
2096
+ "--glob",
2097
+ "'!.hg'",
2098
+ "--glob",
2099
+ "'!.bzr'",
2100
+ "--glob",
2101
+ "'!.jj'",
2102
+ "--glob",
2103
+ "'!.sl'",
2104
+ "--max-columns",
2105
+ "500",
2106
+ `--max-count=${MAX_MATCHES}`
2107
+ ];
2108
+ if (caseInsensitive) rgArgs.push("-i");
2109
+ if (contextLines !== void 0) rgArgs.push(`-C${contextLines}`);
2110
+ if (glob) rgArgs.push(`--glob`, shellEscape(glob));
2111
+ const resolvedPath = searchPath === ctx.cwd ? "." : searchPath;
2112
+ rgArgs.push("--", shellEscape(pattern), shellEscape(resolvedPath));
2113
+ const command = rgArgs.join(" ");
2114
+ try {
2115
+ const result = await ctx.computer.executeCommand(command, {
2116
+ cwd: ctx.cwd
2117
+ });
2118
+ if (result.exitCode === 1 && !result.stdout.trim()) {
2119
+ return { content: "No matches found." };
2120
+ }
2121
+ if (result.exitCode > 1) {
2122
+ return {
2123
+ content: `Grep error: ${result.stderr || result.stdout}`,
2124
+ isError: true
2125
+ };
2126
+ }
2127
+ const lines = result.stdout.split("\n");
2128
+ let output = result.stdout;
2129
+ if (lines.length > MAX_MATCHES) {
2130
+ output = lines.slice(0, MAX_MATCHES).join("\n") + `
2131
+
2132
+ (Results truncated at ${MAX_MATCHES} matches.)`;
2133
+ }
2134
+ return { content: output || "No matches found." };
2135
+ } catch (err) {
2136
+ return {
2137
+ content: `Error searching: ${err instanceof Error ? err.message : String(err)}`,
2138
+ isError: true
2139
+ };
2140
+ }
2141
+ }
2142
+ };
2143
+
2144
+ // src/tools/web-fetch.ts
2145
+ import * as dns from "dns";
2146
+
2147
+ // src/tools/prompts/web-fetch.ts
2148
+ var WEB_FETCH_PROMPT = `Fetches content from a specified URL and returns it in a readable format.
2149
+
2150
+ - Takes a URL and fetches the page content, converting HTML to markdown
2151
+ - Returns the processed content for analysis
2152
+ - Use this tool when you need to retrieve and analyze web content
2153
+
2154
+ Usage notes:
2155
+ - The URL must be a fully-formed valid URL
2156
+ - HTTP URLs will be automatically upgraded to HTTPS
2157
+ - This tool is read-only and does not modify any files
2158
+ - Results may be summarized if the content is very large
2159
+ - 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.
2160
+ - For GitHub URLs, prefer using the gh CLI via Bash instead (e.g., gh pr view, gh issue view, gh api).
2161
+ `;
2162
+
2163
+ // src/tools/web-fetch.ts
2164
+ function stripWww(hostname) {
2165
+ return hostname.replace(/^www\./, "");
2166
+ }
2167
+ var MAX_CONTENT_LENGTH = 5 * 1024 * 1024;
2168
+ var FETCH_TIMEOUT_MS = 3e4;
2169
+ var MAX_OUTPUT_CHARS2 = 1e5;
2170
+ var MAX_REDIRECTS = 5;
2171
+ function isPrivateIP(ip) {
2172
+ const stripped = ip.replace(/^\[|\]$/g, "");
2173
+ if (stripped === "::1" || stripped === "0.0.0.0" || stripped === "::") return true;
2174
+ if (stripped.startsWith("fe80:")) return true;
2175
+ const firstTwo = stripped.slice(0, 2).toLowerCase();
2176
+ if (firstTwo === "fc" || firstTwo === "fd") return true;
2177
+ if (stripped.toLowerCase().startsWith("::ffff:")) {
2178
+ const embedded = stripped.slice(7);
2179
+ return embedded.includes(".") ? isPrivateIP(embedded) : true;
2180
+ }
2181
+ const parts = stripped.split(".");
2182
+ if (parts.length === 4 && parts.every((p) => /^\d+$/.test(p))) {
2183
+ const [a, b] = parts.map(Number);
2184
+ if (a === 127) return true;
2185
+ if (a === 10) return true;
2186
+ if (a === 172 && b >= 16 && b <= 31) return true;
2187
+ if (a === 192 && b === 168) return true;
2188
+ if (a === 169 && b === 254) return true;
2189
+ if (a === 0) return true;
2190
+ }
2191
+ return false;
2192
+ }
2193
+ function isPrivateHost(hostname) {
2194
+ if (hostname === "localhost" || hostname === "[::1]" || hostname === "0.0.0.0") {
2195
+ return true;
2196
+ }
2197
+ if (isPrivateIP(hostname)) return true;
2198
+ if (hostname.startsWith("fe80:") || hostname.startsWith("[fe80:")) return true;
2199
+ return false;
2200
+ }
2201
+ async function checkDnsRebinding(hostname) {
2202
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname) || hostname.includes(":")) {
2203
+ return isPrivateIP(hostname) ? hostname : null;
2204
+ }
2205
+ try {
2206
+ const addrs = await dns.promises.resolve4(hostname);
2207
+ for (const addr of addrs) {
2208
+ if (isPrivateIP(addr)) return addr;
2209
+ }
2210
+ } catch {
2211
+ }
2212
+ try {
2213
+ const addrs6 = await dns.promises.resolve6(hostname);
2214
+ for (const addr of addrs6) {
2215
+ if (isPrivateIP(addr)) return addr;
2216
+ }
2217
+ } catch {
2218
+ }
2219
+ return null;
2220
+ }
2221
+ var webFetchTool = {
2222
+ name: "WebFetch",
2223
+ 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.",
2224
+ prompt: WEB_FETCH_PROMPT,
2225
+ isReadOnly: true,
2226
+ isConcurrencySafe: true,
2227
+ parameters: {
2228
+ type: "object",
2229
+ properties: {
2230
+ url: {
2231
+ type: "string",
2232
+ description: "The URL to fetch (must be a valid http/https URL)"
2233
+ },
2234
+ prompt: {
2235
+ type: "string",
2236
+ description: "Optional instruction for what to extract from the page content"
2237
+ }
2238
+ },
2239
+ required: ["url"]
2240
+ },
2241
+ async call(args, _ctx) {
2242
+ const url = args.url;
2243
+ const prompt = args.prompt;
2244
+ let parsedUrl;
2245
+ try {
2246
+ parsedUrl = new URL(url);
2247
+ } catch {
2248
+ return { content: `Invalid URL: ${url}`, isError: true };
2249
+ }
2250
+ if (parsedUrl.username || parsedUrl.password) {
2251
+ return { content: `Blocked: URLs with embedded credentials are not allowed`, isError: true };
2252
+ }
2253
+ if (parsedUrl.protocol === "http:") {
2254
+ parsedUrl.protocol = "https:";
2255
+ }
2256
+ if (isPrivateHost(parsedUrl.hostname)) {
2257
+ return { content: `Blocked: "${parsedUrl.hostname}" resolves to a private/internal address`, isError: true };
2258
+ }
2259
+ const rebindIP = await checkDnsRebinding(parsedUrl.hostname);
2260
+ if (rebindIP) {
2261
+ return { content: `Blocked: "${parsedUrl.hostname}" resolves to private address ${rebindIP}`, isError: true };
2262
+ }
2263
+ try {
2264
+ const controller = new AbortController();
2265
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
2266
+ let currentUrl = parsedUrl.toString();
2267
+ const originalHost = stripWww(parsedUrl.hostname);
2268
+ let response;
2269
+ for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects++) {
2270
+ response = await fetch(currentUrl, {
2271
+ signal: controller.signal,
2272
+ headers: {
2273
+ "User-Agent": "noumen-agent/1.0",
2274
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.7"
2275
+ },
2276
+ redirect: "manual"
2277
+ });
2278
+ if (response.status >= 300 && response.status < 400) {
2279
+ const location = response.headers.get("location");
2280
+ if (!location) break;
2281
+ const redirectUrl = new URL(location, currentUrl);
2282
+ if (stripWww(redirectUrl.hostname) !== originalHost) {
2283
+ clearTimeout(timeoutId);
2284
+ return { content: `Blocked: redirect to different host "${redirectUrl.hostname}" (original: "${parsedUrl.hostname}")`, isError: true };
2285
+ }
2286
+ if (isPrivateHost(redirectUrl.hostname)) {
2287
+ clearTimeout(timeoutId);
2288
+ return { content: `Blocked: redirect to private/internal address "${redirectUrl.hostname}"`, isError: true };
2289
+ }
2290
+ const redirectRebind = await checkDnsRebinding(redirectUrl.hostname);
2291
+ if (redirectRebind) {
2292
+ clearTimeout(timeoutId);
2293
+ return { content: `Blocked: redirect target "${redirectUrl.hostname}" resolves to private address ${redirectRebind}`, isError: true };
2294
+ }
2295
+ currentUrl = redirectUrl.toString();
2296
+ continue;
2297
+ }
2298
+ break;
2299
+ }
2300
+ clearTimeout(timeoutId);
2301
+ if (!response || !response.ok) {
2302
+ return {
2303
+ content: `HTTP ${response?.status ?? "unknown"}: ${response?.statusText ?? "no response"}`,
2304
+ isError: true
2305
+ };
2306
+ }
2307
+ const contentType = response.headers.get("content-type") ?? "";
2308
+ const contentLength = parseInt(
2309
+ response.headers.get("content-length") ?? "0",
2310
+ 10
2311
+ );
2312
+ if (contentLength > MAX_CONTENT_LENGTH) {
2313
+ return {
2314
+ content: `Response too large (${contentLength} bytes, limit ${MAX_CONTENT_LENGTH})`,
2315
+ isError: true
2316
+ };
2317
+ }
2318
+ let text = "";
2319
+ let bytesRead = 0;
2320
+ const reader = response.body?.getReader();
2321
+ const decoder = new TextDecoder();
2322
+ if (reader) {
2323
+ while (true) {
2324
+ const { done, value } = await reader.read();
2325
+ if (done) break;
2326
+ bytesRead += value.byteLength;
2327
+ if (bytesRead > MAX_CONTENT_LENGTH) {
2328
+ reader.cancel();
2329
+ return {
2330
+ content: `Response too large (>${MAX_CONTENT_LENGTH} bytes streamed, limit ${MAX_CONTENT_LENGTH})`,
2331
+ isError: true
2332
+ };
2333
+ }
2334
+ text += decoder.decode(value, { stream: true });
2335
+ }
2336
+ text += decoder.decode();
2337
+ } else {
2338
+ text = await response.text();
2339
+ }
2340
+ let markdown;
2341
+ if (contentType.includes("text/html") || contentType.includes("xhtml")) {
2342
+ const { NodeHtmlMarkdown } = await import("node-html-markdown");
2343
+ markdown = NodeHtmlMarkdown.translate(text);
2344
+ } else {
2345
+ markdown = text;
2346
+ }
2347
+ if (markdown.length > MAX_OUTPUT_CHARS2) {
2348
+ const totalChars = markdown.length;
2349
+ markdown = markdown.slice(0, MAX_OUTPUT_CHARS2) + `
2350
+
2351
+ ... content truncated (${totalChars} total chars)`;
2352
+ }
2353
+ let result = `# Content from ${url}
2354
+
2355
+ ${markdown}`;
2356
+ if (prompt) {
2357
+ result = `## Extraction prompt: ${prompt}
2358
+
2359
+ ${result}`;
2360
+ }
2361
+ return { content: result };
2362
+ } catch (err) {
2363
+ if (err instanceof Error && err.name === "AbortError") {
2364
+ return { content: `Fetch timed out after ${FETCH_TIMEOUT_MS}ms`, isError: true };
2365
+ }
2366
+ return {
2367
+ content: `Fetch error: ${err instanceof Error ? err.message : String(err)}`,
2368
+ isError: true
2369
+ };
2370
+ }
2371
+ }
2372
+ };
2373
+
2374
+ // src/tools/prompts/notebook.ts
2375
+ 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.`;
2376
+
2377
+ // src/tools/notebook.ts
2378
+ var notebookEditTool = {
2379
+ name: "NotebookEdit",
2380
+ description: "Edit a Jupyter notebook (.ipynb) file. Can replace, insert, or delete cells. The notebook is pure JSON \u2014 no kernel execution.",
2381
+ prompt: NOTEBOOK_PROMPT,
2382
+ isReadOnly: false,
2383
+ isConcurrencySafe: false,
2384
+ parameters: {
2385
+ type: "object",
2386
+ properties: {
2387
+ notebook_path: {
2388
+ type: "string",
2389
+ description: "Path to the .ipynb file"
2390
+ },
2391
+ cell_index: {
2392
+ type: "number",
2393
+ description: "0-based index of the cell to edit. For insert, the new cell is placed at this index."
2394
+ },
2395
+ new_source: {
2396
+ type: "string",
2397
+ description: "The new cell source content. Each line becomes an element in the source array."
2398
+ },
2399
+ cell_type: {
2400
+ type: "string",
2401
+ description: 'Cell type: "code" or "markdown" (default: "code")'
2402
+ },
2403
+ edit_mode: {
2404
+ type: "string",
2405
+ 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)'
2406
+ }
2407
+ },
2408
+ required: ["notebook_path", "cell_index"]
2409
+ },
2410
+ async call(args, ctx) {
2411
+ const path6 = args.notebook_path;
2412
+ const cellIndex = args.cell_index;
2413
+ const newSource = args.new_source ?? "";
2414
+ const cellType = args.cell_type ?? "code";
2415
+ const editMode = args.edit_mode ?? "replace";
2416
+ try {
2417
+ const raw = await ctx.fs.readFile(path6);
2418
+ let notebook;
2419
+ try {
2420
+ notebook = JSON.parse(raw);
2421
+ } catch {
2422
+ return { content: `Not a valid JSON notebook: ${path6}`, isError: true };
2423
+ }
2424
+ if (!Array.isArray(notebook.cells)) {
2425
+ return { content: "Notebook has no cells array.", isError: true };
2426
+ }
2427
+ const sourceLines = newSource.split("\n").map(
2428
+ (line, i, arr) => i < arr.length - 1 ? line + "\n" : line
2429
+ );
2430
+ switch (editMode) {
2431
+ case "replace": {
2432
+ if (cellIndex < 0 || cellIndex >= notebook.cells.length) {
2433
+ return {
2434
+ content: `Cell index ${cellIndex} out of range (0-${notebook.cells.length - 1}).`,
2435
+ isError: true
2436
+ };
2437
+ }
2438
+ notebook.cells[cellIndex].source = sourceLines;
2439
+ notebook.cells[cellIndex].cell_type = cellType;
2440
+ break;
2441
+ }
2442
+ case "insert": {
2443
+ if (cellIndex < 0 || cellIndex > notebook.cells.length) {
2444
+ return {
2445
+ content: `Insert index ${cellIndex} out of range (0-${notebook.cells.length}).`,
2446
+ isError: true
2447
+ };
2448
+ }
2449
+ const newCell = {
2450
+ cell_type: cellType,
2451
+ source: sourceLines,
2452
+ metadata: {},
2453
+ ...cellType === "code" ? { outputs: [], execution_count: null } : {}
2454
+ };
2455
+ notebook.cells.splice(cellIndex, 0, newCell);
2456
+ break;
2457
+ }
2458
+ case "delete": {
2459
+ if (cellIndex < 0 || cellIndex >= notebook.cells.length) {
2460
+ return {
2461
+ content: `Cell index ${cellIndex} out of range (0-${notebook.cells.length - 1}).`,
2462
+ isError: true
2463
+ };
2464
+ }
2465
+ notebook.cells.splice(cellIndex, 1);
2466
+ break;
2467
+ }
2468
+ default:
2469
+ return {
2470
+ content: `Unknown edit_mode: ${editMode}. Use "replace", "insert", or "delete".`,
2471
+ isError: true
2472
+ };
2473
+ }
2474
+ await ctx.fs.writeFile(path6, JSON.stringify(notebook, null, 1) + "\n");
2475
+ const action = editMode === "delete" ? `Deleted cell ${cellIndex}` : editMode === "insert" ? `Inserted new ${cellType} cell at index ${cellIndex}` : `Replaced cell ${cellIndex} content`;
2476
+ return { content: `${action} in ${path6}. Notebook now has ${notebook.cells.length} cells.` };
2477
+ } catch (err) {
2478
+ return {
2479
+ content: `Error editing notebook: ${err instanceof Error ? err.message : String(err)}`,
2480
+ isError: true
2481
+ };
2482
+ }
2483
+ }
2484
+ };
2485
+
2486
+ // src/tools/ask-user.ts
2487
+ var askUserTool = {
2488
+ name: "AskUser",
2489
+ description: "Ask the user a question and wait for their response. Use when you need clarification, confirmation, or additional information before proceeding.",
2490
+ isReadOnly: true,
2491
+ isConcurrencySafe: false,
2492
+ requiresUserInteraction: true,
2493
+ parameters: {
2494
+ type: "object",
2495
+ properties: {
2496
+ question: {
2497
+ type: "string",
2498
+ description: "The question to ask the user"
2499
+ }
2500
+ },
2501
+ required: ["question"]
2502
+ },
2503
+ async call(args, ctx) {
2504
+ const question = args.question;
2505
+ if (!ctx.userInputHandler) {
2506
+ return {
2507
+ content: "Cannot ask user: no userInputHandler configured. Set userInputHandler in AgentOptions or ThreadConfig.",
2508
+ isError: true
2509
+ };
2510
+ }
2511
+ try {
2512
+ const answer = await ctx.userInputHandler(question);
2513
+ return { content: answer };
2514
+ } catch (err) {
2515
+ return {
2516
+ content: `Error getting user input: ${err instanceof Error ? err.message : String(err)}`,
2517
+ isError: true
2518
+ };
2519
+ }
2520
+ }
2521
+ };
2522
+
2523
+ // src/tools/registry.ts
2524
+ function resolveToolPrompt(tool) {
2525
+ if (tool.prompt === void 0) return tool.description;
2526
+ return typeof tool.prompt === "function" ? tool.prompt() : tool.prompt;
2527
+ }
2528
+ function resolveToolFlag(flag, args, defaultValue = false) {
2529
+ if (flag === void 0) return defaultValue;
2530
+ if (typeof flag === "function") {
2531
+ try {
2532
+ return flag(args);
2533
+ } catch {
2534
+ return defaultValue;
2535
+ }
2536
+ }
2537
+ return flag;
2538
+ }
2539
+ var ToolRegistry = class {
2540
+ tools = /* @__PURE__ */ new Map();
2541
+ _discoveredTools = /* @__PURE__ */ new Set();
2542
+ _toolSearchEnabled = false;
2543
+ constructor(additionalTools) {
2544
+ const builtIn = [
2545
+ readFileTool,
2546
+ writeFileTool,
2547
+ editFileTool,
2548
+ bashTool,
2549
+ globTool,
2550
+ grepTool,
2551
+ webFetchTool,
2552
+ notebookEditTool,
2553
+ askUserTool
2554
+ ];
2555
+ for (const tool of builtIn) {
2556
+ this.tools.set(tool.name, tool);
2557
+ }
2558
+ if (additionalTools) {
2559
+ for (const tool of additionalTools) {
2560
+ this.tools.set(tool.name, tool);
2561
+ }
2562
+ }
2563
+ }
2564
+ enableToolSearch() {
2565
+ this._toolSearchEnabled = true;
2566
+ }
2567
+ register(tool) {
2568
+ this.tools.set(tool.name, tool);
2569
+ }
2570
+ get(name) {
2571
+ return this.tools.get(name);
2572
+ }
2573
+ async execute(name, args, ctx) {
2574
+ const tool = this.tools.get(name);
2575
+ if (!tool) {
2576
+ return {
2577
+ content: `Unknown tool: ${name}`,
2578
+ isError: true
2579
+ };
2580
+ }
2581
+ let validatedArgs = args;
2582
+ if (tool.inputSchema) {
2583
+ const parsed = tool.inputSchema.safeParse(args);
2584
+ if (!parsed.success) {
2585
+ return {
2586
+ content: formatZodValidationError(name, parsed.error),
2587
+ isError: true
2588
+ };
2589
+ }
2590
+ validatedArgs = parsed.data;
2591
+ }
2592
+ try {
2593
+ return await tool.call(validatedArgs, ctx);
2594
+ } catch (err) {
2595
+ return {
2596
+ content: `Error executing ${name}: ${err instanceof Error ? err.message : String(err)}`,
2597
+ isError: true
2598
+ };
2599
+ }
2600
+ }
2601
+ toToolDefinitions() {
2602
+ return Array.from(this.tools.values()).map((tool) => ({
2603
+ type: "function",
2604
+ function: {
2605
+ name: tool.name,
2606
+ description: resolveToolPrompt(tool),
2607
+ parameters: tool.parameters
2608
+ }
2609
+ }));
2610
+ }
2611
+ /**
2612
+ * Get tool definitions filtered by tool search. Eager tools (always sent)
2613
+ * plus any deferred tools the model has discovered via ToolSearch.
2614
+ * Falls back to all tools when tool search is not enabled.
2615
+ */
2616
+ getActiveToolDefinitions() {
2617
+ if (!this._toolSearchEnabled) return this.toToolDefinitions();
2618
+ return Array.from(this.tools.values()).filter((tool) => !isDeferredTool(tool) || this._discoveredTools.has(tool.name)).map((tool) => ({
2619
+ type: "function",
2620
+ function: {
2621
+ name: tool.name,
2622
+ description: resolveToolPrompt(tool),
2623
+ parameters: tool.parameters
2624
+ }
2625
+ }));
2626
+ }
2627
+ getEagerTools() {
2628
+ return Array.from(this.tools.values()).filter((tool) => !isDeferredTool(tool));
2629
+ }
2630
+ getDeferredTools() {
2631
+ return Array.from(this.tools.values()).filter(isDeferredTool);
2632
+ }
2633
+ getToolsByNames(names) {
2634
+ return names.map((name) => this.tools.get(name)).filter((t) => t !== void 0);
2635
+ }
2636
+ markDiscovered(names) {
2637
+ for (const name of names) {
2638
+ this._discoveredTools.add(name);
2639
+ }
2640
+ }
2641
+ get discoveredTools() {
2642
+ return this._discoveredTools;
2643
+ }
2644
+ listTools() {
2645
+ return Array.from(this.tools.values());
2646
+ }
2647
+ };
2648
+
2649
+ // src/permissions/helpers.ts
2650
+ import * as path4 from "path";
2651
+ var ACCEPT_EDITS_BASH_ALLOWLIST = /* @__PURE__ */ new Set([
2652
+ "mkdir",
2653
+ "touch",
2654
+ "mv",
2655
+ "cp",
2656
+ "sed",
2657
+ "chmod"
2658
+ ]);
2659
+ function extractContentHint(tool, input) {
2660
+ if (typeof input.file_path === "string") return input.file_path;
2661
+ if (typeof input.command === "string") return input.command;
2662
+ if (typeof input.path === "string") return input.path;
2663
+ return void 0;
2664
+ }
2665
+ function resolveAcceptEditsDecision(params) {
2666
+ const { toolName, input, effectiveInput, isReadOnly, isDestructive, workingDirectories } = params;
2667
+ if (isDestructive) {
2668
+ return {
2669
+ behavior: "ask",
2670
+ message: `Tool "${toolName}" is destructive and requires approval in acceptEdits mode.`,
2671
+ reason: "mode"
2672
+ };
2673
+ }
2674
+ if (toolName === "Bash") {
2675
+ const cmd = typeof input.command === "string" ? input.command : "";
2676
+ const subCommands = splitCompoundCommand(cmd);
2677
+ for (const sub of subCommands) {
2678
+ const baseName = extractCommandName(sub);
2679
+ if (!ACCEPT_EDITS_BASH_ALLOWLIST.has(baseName)) {
2680
+ return {
2681
+ behavior: "ask",
2682
+ message: `Tool "${toolName}" (${baseName}) is not in the acceptEdits allowlist.`,
2683
+ reason: "mode"
2684
+ };
2685
+ }
2686
+ }
2687
+ if (workingDirectories.length > 0) {
2688
+ for (const sub of subCommands) {
2689
+ const tokens = sub.trim().split(/\s+/).slice(1);
2690
+ for (const token of tokens) {
2691
+ if (token.startsWith("-")) continue;
2692
+ if (path4.isAbsolute(token) && !isPathInWorkingDirectories(token, workingDirectories)) {
2693
+ return {
2694
+ behavior: "ask",
2695
+ message: `Bash command references path "${token}" outside working directories.`,
2696
+ reason: "workingDirectory"
2697
+ };
2698
+ }
2699
+ }
2700
+ }
2701
+ }
2702
+ }
2703
+ if (workingDirectories.length > 0) {
2704
+ const filePath = typeof input.file_path === "string" ? input.file_path : typeof input.path === "string" ? input.path : void 0;
2705
+ if (filePath && !isPathInWorkingDirectories(filePath, workingDirectories)) {
2706
+ return {
2707
+ behavior: "ask",
2708
+ message: `Path "${filePath}" is outside working directories in acceptEdits mode.`,
2709
+ reason: "workingDirectory"
2710
+ };
2711
+ }
2712
+ }
2713
+ const hasFilePath = typeof input.file_path === "string" || typeof input.path === "string";
2714
+ if (!isReadOnly && !hasFilePath && toolName !== "Bash") {
2715
+ return {
2716
+ behavior: "ask",
2717
+ message: `Tool "${toolName}" requires approval in acceptEdits mode.`,
2718
+ reason: "mode"
2719
+ };
2720
+ }
2721
+ return {
2722
+ behavior: "allow",
2723
+ updatedInput: effectiveInput,
2724
+ reason: "mode"
2725
+ };
2726
+ }
2727
+ function resolveAutoModeDecision(params) {
2728
+ const { toolName, effectiveInput, classifierResult, denialTracker, requiresUserInteraction } = params;
2729
+ if (classifierResult.shouldBlock) {
2730
+ if (denialTracker) {
2731
+ denialTracker.recordDenial();
2732
+ const fallback = denialTracker.shouldFallback();
2733
+ if (fallback.triggered) {
2734
+ if (fallback.reason === "repeated_consecutive") {
2735
+ return {
2736
+ behavior: "deny",
2737
+ message: `Auto-mode classifier denied too many actions without user approval. Aborting.`,
2738
+ reason: "denial_limit"
2739
+ };
2740
+ }
2741
+ denialTracker.resetAfterFallback(fallback.reason);
2742
+ return {
2743
+ behavior: "ask",
2744
+ message: `Auto-mode classifier denied too many consecutive actions. Falling back to user prompt.`,
2745
+ reason: "denial_limit"
2746
+ };
2747
+ }
2748
+ }
2749
+ return {
2750
+ behavior: "deny",
2751
+ message: `Auto-mode classifier flagged this call: ${classifierResult.reason}`,
2752
+ reason: "classifier"
2753
+ };
2754
+ }
2755
+ if (requiresUserInteraction) {
2756
+ return {
2757
+ behavior: "ask",
2758
+ message: `Tool "${toolName}" requires user interaction.`,
2759
+ reason: "interaction"
2760
+ };
2761
+ }
2762
+ denialTracker?.recordSuccess();
2763
+ return {
2764
+ behavior: "allow",
2765
+ updatedInput: effectiveInput,
2766
+ reason: "classifier"
2767
+ };
2768
+ }
2769
+
2770
+ // src/permissions/pipeline.ts
2771
+ var DANGEROUS_PATH_PATTERNS = [
2772
+ /(?:^|\/)\.git(?:\/|$)/,
2773
+ /(?:^|\/)\.bashrc$/,
2774
+ /(?:^|\/)\.bash_profile$/,
2775
+ /(?:^|\/)\.zshrc$/,
2776
+ /(?:^|\/)\.zprofile$/,
2777
+ /(?:^|\/)\.profile$/,
2778
+ /(?:^|\/)\.ssh(?:\/|$)/,
2779
+ /(?:^|\/)\.env$/,
2780
+ /(?:^|\/)\.npmrc$/,
2781
+ /(?:^|\/)\.vscode(?:\/|$)/,
2782
+ /(?:^|\/)\.idea(?:\/|$)/,
2783
+ /(?:^|\/)\.claude(?:\/|$)/,
2784
+ /(?:^|\/)\.noumen(?:\/|$)/,
2785
+ /(?:^|\/)\.gitconfig$/,
2786
+ /(?:^|\/)\.gitmodules$/,
2787
+ /(?:^|\/)\.mcp\.json$/,
2788
+ /(?:^|\/)\.ripgreprc$/,
2789
+ /(?:^|\/)\.noumen\.json$/
2790
+ ];
2791
+ async function resolvePermission(tool, input, ctx, permCtx, opts) {
2792
+ const toolName = tool.name;
2793
+ const contentHint = extractContentHint(tool, input);
2794
+ const wholeDenyRules = getMatchingRules(
2795
+ permCtx,
2796
+ toolName,
2797
+ "deny",
2798
+ void 0,
2799
+ tool.mcpInfo
2800
+ );
2801
+ if (wholeDenyRules.length > 0) {
2802
+ return {
2803
+ behavior: "deny",
2804
+ message: `Tool "${toolName}" is denied by rule.`,
2805
+ reason: "rule"
2806
+ };
2807
+ }
2808
+ if (contentHint !== void 0) {
2809
+ const contentDenyRules = getMatchingRules(
2810
+ permCtx,
2811
+ toolName,
2812
+ "deny",
2813
+ contentHint,
2814
+ tool.mcpInfo
2815
+ );
2816
+ if (contentDenyRules.length > 0) {
2817
+ return {
2818
+ behavior: "deny",
2819
+ message: `Tool "${toolName}" with "${contentHint}" is denied by rule.`,
2820
+ reason: "rule"
2821
+ };
2822
+ }
2823
+ }
2824
+ const wholeAskRules = getMatchingRules(
2825
+ permCtx,
2826
+ toolName,
2827
+ "ask",
2828
+ void 0,
2829
+ tool.mcpInfo
2830
+ );
2831
+ if (wholeAskRules.length > 0) {
2832
+ return {
2833
+ behavior: "ask",
2834
+ message: `Tool "${toolName}" requires approval.`,
2835
+ reason: "rule"
2836
+ };
2837
+ }
2838
+ if (contentHint !== void 0) {
2839
+ const contentAskRules = getMatchingRules(
2840
+ permCtx,
2841
+ toolName,
2842
+ "ask",
2843
+ contentHint,
2844
+ tool.mcpInfo
2845
+ );
2846
+ if (contentAskRules.length > 0) {
2847
+ return {
2848
+ behavior: "ask",
2849
+ message: `Tool "${toolName}" with "${contentHint}" requires approval.`,
2850
+ reason: "rule"
2851
+ };
2852
+ }
2853
+ }
2854
+ const dangerousFilePath = typeof input.file_path === "string" ? input.file_path : typeof input.path === "string" ? input.path : void 0;
2855
+ if (dangerousFilePath && isDangerousPath(dangerousFilePath, ctx.cwd)) {
2856
+ return {
2857
+ behavior: "ask",
2858
+ message: `Path "${dangerousFilePath}" targets a sensitive location.`,
2859
+ reason: "safetyCheck"
2860
+ };
2861
+ }
2862
+ if (toolName === "Bash" && typeof input.command === "string") {
2863
+ const subCommands = splitCompoundCommand(input.command);
2864
+ for (const sub of subCommands) {
2865
+ const tokens = sub.trim().split(/\s+/);
2866
+ for (const token of tokens) {
2867
+ if (token.startsWith("-")) continue;
2868
+ if (isDangerousPath(token, ctx.cwd)) {
2869
+ return {
2870
+ behavior: "ask",
2871
+ message: `Bash command references sensitive path "${token}".`,
2872
+ reason: "safetyCheck"
2873
+ };
2874
+ }
2875
+ }
2876
+ }
2877
+ }
2878
+ let toolResult;
2879
+ if (tool.checkPermissions) {
2880
+ if (opts?.signal?.aborted) {
2881
+ throw new DOMException("Aborted", "AbortError");
2882
+ }
2883
+ try {
2884
+ toolResult = await tool.checkPermissions(input, ctx);
2885
+ } catch (err) {
2886
+ if (err instanceof DOMException && err.name === "AbortError") throw err;
2887
+ if (err instanceof Error && err.name === "AbortError") throw err;
2888
+ console.warn(`[noumen/permissions] checkPermissions error for "${toolName}":`, err);
2889
+ }
2890
+ if (toolResult?.behavior === "deny") {
2891
+ return {
2892
+ behavior: "deny",
2893
+ message: toolResult.message,
2894
+ reason: toolResult.reason ?? "tool"
2895
+ };
2896
+ }
2897
+ if (toolResult?.behavior === "ask") {
2898
+ const isSafetyCheck = toolResult.reason === "safetyCheck";
2899
+ const isInteractive = !!tool.requiresUserInteraction;
2900
+ if (isSafetyCheck || isInteractive) {
2901
+ return {
2902
+ behavior: "ask",
2903
+ message: toolResult.message,
2904
+ reason: toolResult.reason ?? "tool",
2905
+ suggestions: toolResult.suggestions
2906
+ };
2907
+ }
2908
+ if (permCtx.mode !== "bypassPermissions") {
2909
+ }
2910
+ }
2911
+ }
2912
+ const effectiveInput = toolResult?.behavior === "allow" && toolResult.updatedInput ? toolResult.updatedInput : input;
2913
+ if (tool.requiresUserInteraction && permCtx.mode === "bypassPermissions") {
2914
+ return {
2915
+ behavior: "ask",
2916
+ message: `Tool "${toolName}" requires user interaction.`,
2917
+ reason: "interaction"
2918
+ };
2919
+ }
2920
+ if (permCtx.mode === "bypassPermissions") {
2921
+ return {
2922
+ behavior: "allow",
2923
+ updatedInput: effectiveInput,
2924
+ reason: "mode"
2925
+ };
2926
+ }
2927
+ const isReadOnly = resolveToolFlag(tool.isReadOnly, input);
2928
+ const isDestructive = resolveToolFlag(tool.isDestructive, input);
2929
+ if (permCtx.mode === "plan" && !isReadOnly) {
2930
+ return {
2931
+ behavior: "deny",
2932
+ message: `Tool "${toolName}" is not allowed in plan mode (read-only).`,
2933
+ reason: "mode"
2934
+ };
2935
+ }
2936
+ if (permCtx.mode === "acceptEdits") {
2937
+ return resolveAcceptEditsDecision({
2938
+ toolName,
2939
+ input,
2940
+ effectiveInput,
2941
+ isReadOnly,
2942
+ isDestructive,
2943
+ workingDirectories: permCtx.workingDirectories
2944
+ });
2945
+ }
2946
+ if (permCtx.mode === "auto" && opts?.autoModeConfig) {
2947
+ if (!opts.provider) {
2948
+ return {
2949
+ behavior: "ask",
2950
+ message: `Auto-mode requires an AI provider for classification. Falling back to ask.`,
2951
+ reason: "classifier"
2952
+ };
2953
+ }
2954
+ const classifierResult = await classifyPermission(
2955
+ toolName,
2956
+ input,
2957
+ opts.recentMessages ?? [],
2958
+ opts.provider,
2959
+ {
2960
+ classifierPrompt: opts.autoModeConfig.classifierPrompt,
2961
+ classifierModel: opts.autoModeConfig.classifierModel,
2962
+ model: opts.model,
2963
+ signal: opts.signal
2964
+ }
2965
+ );
2966
+ return resolveAutoModeDecision({
2967
+ toolName,
2968
+ effectiveInput,
2969
+ classifierResult,
2970
+ denialTracker: opts.denialTracker,
2971
+ requiresUserInteraction: !!tool.requiresUserInteraction
2972
+ });
2973
+ }
2974
+ if (toolResult?.behavior === "allow") {
2975
+ return {
2976
+ behavior: "allow",
2977
+ updatedInput: effectiveInput,
2978
+ reason: toolResult.reason ?? "tool"
2979
+ };
2980
+ }
2981
+ if (isReadOnly && toolResult?.behavior !== "ask") {
2982
+ return {
2983
+ behavior: "allow",
2984
+ updatedInput: effectiveInput,
2985
+ reason: "readOnly"
2986
+ };
2987
+ }
2988
+ if (permCtx.workingDirectories.length > 0) {
2989
+ const filePath = typeof input.file_path === "string" ? input.file_path : typeof input.path === "string" ? input.path : void 0;
2990
+ if (filePath && !isPathInWorkingDirectories(filePath, permCtx.workingDirectories)) {
2991
+ return {
2992
+ behavior: "ask",
2993
+ message: `Path "${filePath}" is outside configured working directories.`,
2994
+ reason: "workingDirectory"
2995
+ };
2996
+ }
2997
+ }
2998
+ if (contentHint !== void 0) {
2999
+ const contentAllowRules = getMatchingRules(
3000
+ permCtx,
3001
+ toolName,
3002
+ "allow",
3003
+ contentHint,
3004
+ tool.mcpInfo
3005
+ );
3006
+ if (contentAllowRules.length > 0) {
3007
+ return {
3008
+ behavior: "allow",
3009
+ updatedInput: effectiveInput,
3010
+ reason: "rule"
3011
+ };
3012
+ }
3013
+ }
3014
+ const wholeAllowRules = getMatchingRules(
3015
+ permCtx,
3016
+ toolName,
3017
+ "allow",
3018
+ void 0,
3019
+ tool.mcpInfo
3020
+ );
3021
+ if (wholeAllowRules.length > 0) {
3022
+ return {
3023
+ behavior: "allow",
3024
+ updatedInput: effectiveInput,
3025
+ reason: "rule"
3026
+ };
3027
+ }
3028
+ let finalAsk;
3029
+ if (toolResult?.behavior === "ask") {
3030
+ finalAsk = {
3031
+ behavior: "ask",
3032
+ message: toolResult.message,
3033
+ reason: toolResult.reason ?? "tool",
3034
+ suggestions: toolResult.suggestions
3035
+ };
3036
+ }
3037
+ if (!finalAsk) {
3038
+ const message = toolResult?.behavior === "passthrough" ? toolResult.message : `Tool "${toolName}" requires approval.`;
3039
+ const suggestions = toolResult?.behavior === "passthrough" ? toolResult.suggestions : void 0;
3040
+ finalAsk = {
3041
+ behavior: "ask",
3042
+ message,
3043
+ reason: "default",
3044
+ suggestions
3045
+ };
3046
+ }
3047
+ if (permCtx.mode === "dontAsk") {
3048
+ return {
3049
+ behavior: "deny",
3050
+ message: `Tool "${toolName}" requires approval, but mode is "dontAsk".`,
3051
+ reason: "mode"
3052
+ };
3053
+ }
3054
+ return finalAsk;
3055
+ }
3056
+ function isDangerousPath(filePath, basePath) {
3057
+ const base = basePath ?? process.cwd();
3058
+ const resolved = path5.resolve(base, filePath);
3059
+ const relative2 = path5.relative(base, resolved);
3060
+ const candidate = (relative2.startsWith("..") ? resolved.replace(/^\/+/, "") : relative2).toLowerCase();
3061
+ if (DANGEROUS_PATH_PATTERNS.some((p) => p.test(candidate))) return true;
3062
+ try {
3063
+ const realPath = fs2.realpathSync(resolved);
3064
+ if (realPath !== resolved) {
3065
+ const realRelative = path5.relative(base, realPath);
3066
+ const realCandidate = (realRelative.startsWith("..") ? realPath.replace(/^\/+/, "") : realRelative).toLowerCase();
3067
+ if (DANGEROUS_PATH_PATTERNS.some((p) => p.test(realCandidate))) return true;
3068
+ }
3069
+ } catch {
3070
+ }
3071
+ return false;
3072
+ }
3073
+
3074
+ export {
3075
+ TOOL_SEARCH_NAME,
3076
+ isDeferredTool,
3077
+ formatDeferredToolLine,
3078
+ searchToolsWithKeywords,
3079
+ createToolSearchTool,
3080
+ readFileTool,
3081
+ RULE_SOURCE_PRECEDENCE,
3082
+ toolMatchesRule,
3083
+ contentMatchesRule,
3084
+ matchSimpleGlob,
3085
+ getMatchingRules,
3086
+ isPathInWorkingDirectories,
3087
+ classifyPermission,
3088
+ isGitInternalPath,
3089
+ looksLikeBareRepo,
3090
+ commandWritesGitInternals,
3091
+ extractCommandName,
3092
+ classifyCommand,
3093
+ resolvePermission,
3094
+ writeFileTool,
3095
+ normalizeQuotes,
3096
+ findActualString,
3097
+ countOccurrences,
3098
+ preserveQuoteStyle,
3099
+ stripTrailingWhitespace,
3100
+ editFileTool,
3101
+ detectGitOperations,
3102
+ hasGitIndexLockError,
3103
+ bashTool,
3104
+ globTool,
3105
+ grepTool,
3106
+ webFetchTool,
3107
+ notebookEditTool,
3108
+ askUserTool,
3109
+ resolveToolFlag,
3110
+ ToolRegistry
3111
+ };
3112
+ //# sourceMappingURL=chunk-HL6JCRZJ.js.map