zidane 5.14.4 → 6.0.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 (202) hide show
  1. package/dist/{acp-24uU2WDs.js → acp-Irby04Us.js} +7 -8
  2. package/dist/{acp-24uU2WDs.js.map → acp-Irby04Us.js.map} +1 -1
  3. package/dist/acp-cli.js +7 -7
  4. package/dist/acp.d.ts +2 -3
  5. package/dist/acp.d.ts.map +1 -1
  6. package/dist/acp.js +1 -1
  7. package/dist/{agent-Cch2ayTt.js → agent-DkSmGkDM.js} +10871 -5411
  8. package/dist/agent-DkSmGkDM.js.map +1 -0
  9. package/dist/agent.d.ts +1 -2
  10. package/dist/agent.js +2 -2
  11. package/dist/{anthropic-RGqsJlC8.js → anthropic-CTHsdFG9.js} +46 -6
  12. package/dist/anthropic-CTHsdFG9.js.map +1 -0
  13. package/dist/{auth-C-ms5N2x.js → auth-BDSu_0t3.js} +13 -86
  14. package/dist/auth-BDSu_0t3.js.map +1 -0
  15. package/dist/chat/pure.d.ts +4 -4
  16. package/dist/chat/pure.js +4 -3
  17. package/dist/chat.d.ts +45 -8
  18. package/dist/chat.d.ts.map +1 -1
  19. package/dist/chat.js +9 -6
  20. package/dist/chat.js.map +1 -1
  21. package/dist/completion-core-CXYSVhZo.js +147 -0
  22. package/dist/completion-core-CXYSVhZo.js.map +1 -0
  23. package/dist/{context-breakdown-kO-pDsay.js → context-breakdown-Dzo25N45.js} +2 -23
  24. package/dist/context-breakdown-Dzo25N45.js.map +1 -0
  25. package/dist/contexts/daytona.d.ts +3 -3
  26. package/dist/contexts/daytona.js +1 -1
  27. package/dist/contexts/docker.d.ts +10 -2
  28. package/dist/contexts/docker.d.ts.map +1 -1
  29. package/dist/contexts/docker.js +142 -103
  30. package/dist/contexts/docker.js.map +1 -1
  31. package/dist/contexts/e2b.d.ts +2 -2
  32. package/dist/contexts/e2b.js +1 -1
  33. package/dist/contexts/sandbox.d.ts +1 -1
  34. package/dist/contexts/sandbox.js +4 -1
  35. package/dist/contexts/sandbox.js.map +1 -1
  36. package/dist/{contexts-Lzf5huID.js → contexts-Vone3qQQ.js} +26 -37
  37. package/dist/contexts-Vone3qQQ.js.map +1 -0
  38. package/dist/contexts.d.ts +3 -3
  39. package/dist/contexts.js +1 -1
  40. package/dist/errors.d.ts +1 -2
  41. package/dist/errors.js +1 -1
  42. package/dist/eval.d.ts +1 -1
  43. package/dist/eval.js +3 -3
  44. package/dist/exec-file-size-PXkr-MGA.js +63 -0
  45. package/dist/exec-file-size-PXkr-MGA.js.map +1 -0
  46. package/dist/extensions.d.ts +2 -0
  47. package/dist/extensions.js +2 -0
  48. package/dist/generate-data-WL_yGVL6.js +324 -0
  49. package/dist/generate-data-WL_yGVL6.js.map +1 -0
  50. package/dist/headless.d.ts +1 -1
  51. package/dist/headless.js +15 -10
  52. package/dist/headless.js.map +1 -1
  53. package/dist/index-B8UtiZy7.d.ts +14967 -0
  54. package/dist/index-B8UtiZy7.d.ts.map +1 -0
  55. package/dist/{index-B3ciFUZx.d.ts → index-DsPQbyUn.d.ts} +2 -2
  56. package/dist/index-DsPQbyUn.d.ts.map +1 -0
  57. package/dist/index.d.ts +6 -12
  58. package/dist/index.js +20 -43
  59. package/dist/index.js.map +1 -1
  60. package/dist/{logger-Ktm-lj1s.js → logger-DBX9uYOw.js} +108 -5
  61. package/dist/logger-DBX9uYOw.js.map +1 -0
  62. package/dist/login-DOF8pMVM.js +539 -0
  63. package/dist/login-DOF8pMVM.js.map +1 -0
  64. package/dist/{mcp-Dn5W65Lv.js → mcp-CcvuVXB9.js} +2 -2
  65. package/dist/{mcp-Dn5W65Lv.js.map → mcp-CcvuVXB9.js.map} +1 -1
  66. package/dist/mcp.d.ts +1 -1
  67. package/dist/mcp.js +1 -1
  68. package/dist/{messages-YZQLKaz2.js → messages-CwujiS74.js} +4 -4
  69. package/dist/messages-CwujiS74.js.map +1 -0
  70. package/dist/{openai-compat-0Y7jcncn.js → openai-compat-BvgYGSR1.js} +7 -6
  71. package/dist/{openai-compat-0Y7jcncn.js.map → openai-compat-BvgYGSR1.js.map} +1 -1
  72. package/dist/output/stream-json.d.ts +1 -2
  73. package/dist/output/stream-json.d.ts.map +1 -1
  74. package/dist/output/terminal.d.ts +1 -2
  75. package/dist/output/terminal.d.ts.map +1 -1
  76. package/dist/{glob-DluQFSw1.js → output-cap-kBUHBlnq.js} +50 -2
  77. package/dist/output-cap-kBUHBlnq.js.map +1 -0
  78. package/dist/presets.d.ts +1 -2
  79. package/dist/presets.js +1 -1
  80. package/dist/providers/anthropic.d.ts +2 -2
  81. package/dist/providers/anthropic.js +2 -2
  82. package/dist/providers/arcee.d.ts +1 -1
  83. package/dist/providers/arcee.js +1 -1
  84. package/dist/providers/baseten.d.ts +1 -1
  85. package/dist/providers/baseten.js +1 -1
  86. package/dist/providers/cerebras.d.ts +1 -1
  87. package/dist/providers/cerebras.js +1 -1
  88. package/dist/providers/local.d.ts +1 -1
  89. package/dist/providers/local.js +1 -1
  90. package/dist/providers/openai-compat.d.ts +1 -1
  91. package/dist/providers/openai-compat.js +1 -1
  92. package/dist/providers/openai.d.ts +1 -1
  93. package/dist/providers/openai.js +4 -4
  94. package/dist/providers/openai.js.map +1 -1
  95. package/dist/providers/openrouter.d.ts +1 -1
  96. package/dist/providers/openrouter.js +1 -1
  97. package/dist/providers/xai.d.ts +1 -1
  98. package/dist/providers/xai.js +1 -1
  99. package/dist/{providers-DgmdYGKm.js → providers-z6LDMAr3.js} +3 -3
  100. package/dist/{providers-DgmdYGKm.js.map → providers-z6LDMAr3.js.map} +1 -1
  101. package/dist/providers.d.ts +1 -1
  102. package/dist/providers.js +4 -4
  103. package/dist/{read-state-BymF41Jb.js → read-state-Dq_TXUX1.js} +2 -2
  104. package/dist/{read-state-BymF41Jb.js.map → read-state-Dq_TXUX1.js.map} +1 -1
  105. package/dist/{interpolate-BtIgcCuz.js → resolve-Cr8JSL_O.js} +562 -136
  106. package/dist/resolve-Cr8JSL_O.js.map +1 -0
  107. package/dist/restate.d.ts +2 -2
  108. package/dist/restate.js +2 -2
  109. package/dist/{sandbox-Dgy-m6UF.d.ts → sandbox-CgjG2VvQ.d.ts} +2 -2
  110. package/dist/{sandbox-Dgy-m6UF.d.ts.map → sandbox-CgjG2VvQ.d.ts.map} +1 -1
  111. package/dist/session/sqlite.d.ts +1 -1
  112. package/dist/{session-CqrAFAKJ.js → session-CsdDIPY8.js} +108 -3
  113. package/dist/session-CsdDIPY8.js.map +1 -0
  114. package/dist/session.d.ts +2 -2
  115. package/dist/session.js +3 -3
  116. package/dist/skills-Bsb0rvWI.js +295 -0
  117. package/dist/skills-Bsb0rvWI.js.map +1 -0
  118. package/dist/skills.d.ts +2 -3
  119. package/dist/skills.js +3 -24
  120. package/dist/{errors-CkTvg97v.d.ts → timeout-8vSIRjzl.d.ts} +10 -2
  121. package/dist/timeout-8vSIRjzl.d.ts.map +1 -0
  122. package/dist/{timeout-kGGSXagK.js → timeout-BDetbuN6.js} +59 -2
  123. package/dist/timeout-BDetbuN6.js.map +1 -0
  124. package/dist/tool-formatters-DY0TDb1r.d.ts +378 -0
  125. package/dist/tool-formatters-DY0TDb1r.d.ts.map +1 -0
  126. package/dist/tools/fetch-url.d.ts +1 -1
  127. package/dist/tools/web-search.d.ts +1 -1
  128. package/dist/tools.d.ts +2 -3
  129. package/dist/tools.js +3 -4
  130. package/dist/{transcript-anchors-BHIgMlPq.d.ts → transcript-anchors-Cb-bDbNH.d.ts} +233 -640
  131. package/dist/transcript-anchors-Cb-bDbNH.d.ts.map +1 -0
  132. package/dist/{transcript-anchors-DsoBHg43.js → transcript-anchors-DSRPWjiH.js} +391 -961
  133. package/dist/transcript-anchors-DSRPWjiH.js.map +1 -0
  134. package/dist/tui.d.ts +335 -11
  135. package/dist/tui.d.ts.map +1 -1
  136. package/dist/tui.js +6375 -1892
  137. package/dist/tui.js.map +1 -1
  138. package/dist/{turn-operations-Dw02ZoSQ.d.ts → turn-operations-CgMROLsr.d.ts} +29 -4
  139. package/dist/turn-operations-CgMROLsr.d.ts.map +1 -0
  140. package/dist/{turn-operations-tG0vuBNc.js → turn-operations-Dq9Kx7m2.js} +321 -81
  141. package/dist/turn-operations-Dq9Kx7m2.js.map +1 -0
  142. package/dist/{types-BqdTeBaC.d.ts → types-BDccavwj.d.ts} +42 -1
  143. package/dist/types-BDccavwj.d.ts.map +1 -0
  144. package/dist/{types-CyVGdbia.js → types-BIarq1qE.js} +90 -2
  145. package/dist/types-BIarq1qE.js.map +1 -0
  146. package/dist/types.d.ts +5 -6
  147. package/dist/types.js +1 -1
  148. package/dist/{xai-tPdbAGkq.js → xai-B_e6TOA8.js} +2 -2
  149. package/dist/{xai-tPdbAGkq.js.map → xai-B_e6TOA8.js.map} +1 -1
  150. package/docs/ARCHITECTURE.md +1 -1
  151. package/docs/CHAT.md +7 -6
  152. package/docs/EXECUTION_CONTEXT.md +16 -0
  153. package/docs/EXTENSIONS.md +1063 -0
  154. package/docs/SKILL.md +16 -0
  155. package/docs/TUI.md +10 -3
  156. package/package.json +12 -3
  157. package/dist/agent-Cch2ayTt.js.map +0 -1
  158. package/dist/agent-EFWt6uQ_.d.ts +0 -6914
  159. package/dist/agent-EFWt6uQ_.d.ts.map +0 -1
  160. package/dist/anthropic-RGqsJlC8.js.map +0 -1
  161. package/dist/auth-C-ms5N2x.js.map +0 -1
  162. package/dist/context-breakdown-kO-pDsay.js.map +0 -1
  163. package/dist/contexts-Lzf5huID.js.map +0 -1
  164. package/dist/errors-CkTvg97v.d.ts.map +0 -1
  165. package/dist/glob-DluQFSw1.js.map +0 -1
  166. package/dist/glob-shell-rJMoCIGb.js +0 -21
  167. package/dist/glob-shell-rJMoCIGb.js.map +0 -1
  168. package/dist/index-B3ciFUZx.d.ts.map +0 -1
  169. package/dist/index-BtzE3Uaq.d.ts +0 -2738
  170. package/dist/index-BtzE3Uaq.d.ts.map +0 -1
  171. package/dist/index-tSzOaWNt.d.ts +0 -316
  172. package/dist/index-tSzOaWNt.d.ts.map +0 -1
  173. package/dist/interpolate-BtIgcCuz.js.map +0 -1
  174. package/dist/logger-DGiGf7DW.d.ts +0 -102
  175. package/dist/logger-DGiGf7DW.d.ts.map +0 -1
  176. package/dist/logger-Ktm-lj1s.js.map +0 -1
  177. package/dist/login-CCA-1lgK.js +0 -267
  178. package/dist/login-CCA-1lgK.js.map +0 -1
  179. package/dist/messages-YZQLKaz2.js.map +0 -1
  180. package/dist/policy-DcGlpaNs.d.ts +0 -129
  181. package/dist/policy-DcGlpaNs.d.ts.map +0 -1
  182. package/dist/presets-BQWYMHdd.js +0 -114
  183. package/dist/presets-BQWYMHdd.js.map +0 -1
  184. package/dist/run-summary-DkN8KsHM.d.ts +0 -249
  185. package/dist/run-summary-DkN8KsHM.d.ts.map +0 -1
  186. package/dist/session-CqrAFAKJ.js.map +0 -1
  187. package/dist/shell-quote-BmnhZmdM.js +0 -33
  188. package/dist/shell-quote-BmnhZmdM.js.map +0 -1
  189. package/dist/skills.js.map +0 -1
  190. package/dist/timeout-CpFm0jJ6.d.ts +0 -10
  191. package/dist/timeout-CpFm0jJ6.d.ts.map +0 -1
  192. package/dist/timeout-kGGSXagK.js.map +0 -1
  193. package/dist/tool-formatters-C6iLgc9f.d.ts +0 -1454
  194. package/dist/tool-formatters-C6iLgc9f.d.ts.map +0 -1
  195. package/dist/tools-CCd8e-HT.js +0 -1739
  196. package/dist/tools-CCd8e-HT.js.map +0 -1
  197. package/dist/transcript-anchors-BHIgMlPq.d.ts.map +0 -1
  198. package/dist/transcript-anchors-DsoBHg43.js.map +0 -1
  199. package/dist/turn-operations-Dw02ZoSQ.d.ts.map +0 -1
  200. package/dist/turn-operations-tG0vuBNc.js.map +0 -1
  201. package/dist/types-BqdTeBaC.d.ts.map +0 -1
  202. package/dist/types-CyVGdbia.js.map +0 -1
@@ -1,1739 +0,0 @@
1
- import { c as resolveReadStateMap, r as hashContent, s as readStateKey } from "./read-state-BymF41Jb.js";
2
- import { i as styleReplacementForVia, n as resolveOldString, r as stripLineNumberPrefixes, t as describeVia } from "./edit-utils-EGosADZq.js";
3
- import { J as resolveAttachmentFromTurns, c as looksBinary, g as pathArgError, q as parseAttachmentHandle, t as createAgent } from "./agent-Cch2ayTt.js";
4
- import { a as formatTaskStatus, i as formatDuration, s as previewLine } from "./format-BNOXpl-1.js";
5
- import { i as parseGitLsFiles, r as GIT_LS_FILES_COMMAND } from "./glob-DluQFSw1.js";
6
- import { l as errorMessage, u as isAbortLikeError } from "./errors-DJUxZg9b.js";
7
- import { n as reconcileImageMediaType } from "./media-sniff-Bn76JxAu.js";
8
- import { n as flattenTurns, r as formatTokenUsage } from "./stats-DAKBEKjc.js";
9
- import { n as shellQuote, t as alwaysQuote } from "./shell-quote-BmnhZmdM.js";
10
- import { Buffer } from "node:buffer";
11
- //#region src/behaviors/read-file-defaults.ts
12
- function normalizeReadFileInteger(value, fallback) {
13
- if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
14
- if (value < 0) return fallback;
15
- return Math.floor(value);
16
- }
17
- function resolveReadFileDefaults(behavior, fallbacks) {
18
- return {
19
- lineLimit: normalizeReadFileInteger(behavior?.readFileDefaults?.limit, fallbacks.lineLimit),
20
- textMaxBytes: normalizeReadFileInteger(behavior?.readFileDefaults?.maxBytes, fallbacks.textMaxBytes),
21
- attachmentMaxBytes: normalizeReadFileInteger(behavior?.readFileDefaults?.maxBytes, fallbacks.attachmentMaxBytes)
22
- };
23
- }
24
- //#endregion
25
- //#region src/tools/path-suggest.ts
26
- function entryPath$2(entry) {
27
- return typeof entry === "string" ? entry : entry.path;
28
- }
29
- /**
30
- * Find a sibling file in the same directory sharing `path`'s basename
31
- * (sans extension), excluding the missing path itself. Returns just the
32
- * filename (not the full path) when found, otherwise `null`.
33
- *
34
- * Silent on errors — a missing parent directory or a `listFiles` failure
35
- * means we have no suggestion, not that anything is wrong.
36
- */
37
- async function findSimilarFile(execution, handle, path) {
38
- const slash = path.lastIndexOf("/");
39
- const dir = slash === -1 ? "." : path.slice(0, slash) || "/";
40
- const target = slash === -1 ? path : path.slice(slash + 1);
41
- const dot = target.lastIndexOf(".");
42
- const targetBase = dot === -1 ? target : target.slice(0, dot);
43
- if (targetBase.length === 0) return null;
44
- let entries;
45
- try {
46
- entries = await execution.listFiles(handle, dir);
47
- } catch {
48
- return null;
49
- }
50
- for (const item of entries) {
51
- const entry = entryPath$2(item);
52
- if (entry === target) continue;
53
- const entryDot = entry.lastIndexOf(".");
54
- if ((entryDot === -1 ? entry : entry.slice(0, entryDot)) === targetBase) return entry;
55
- }
56
- return null;
57
- }
58
- /**
59
- * Format a `Did you mean X?` suffix for missing-file errors. Returns an empty
60
- * string when no suggestion is available so callers can string-concat
61
- * unconditionally.
62
- */
63
- async function suggestionFor(execution, handle, path) {
64
- const sibling = await findSimilarFile(execution, handle, path);
65
- return sibling ? ` Did you mean ${sibling}?` : "";
66
- }
67
- //#endregion
68
- //#region src/tools/edit.ts
69
- /**
70
- * Surgical edit — replace `old_string` with `new_string` in a single file.
71
- *
72
- * Mirrors Claude Code's `Edit` semantics so models post-trained on Anthropic's
73
- * tool surface need no relearning. Fails clearly when `old_string` isn't unique
74
- * (unless `replace_all: true`) and when not found, with a nearest-match preview
75
- * so the model can recover without a separate `read_file` round-trip.
76
- */
77
- const edit = {
78
- spec: {
79
- name: "edit",
80
- description: "Replace exact `old_string` with `new_string` in a file. Fails if `old_string` is not unique unless `replace_all: true`. Prefer over `write_file` for surgical changes — preserves the rest of the file. Tolerates `read_file` line-number prefixes (`<N>\\t…`, `<N>|…`, or `<N>→…`) in `old_string` / `new_string` — they are stripped before matching/writing, so you can paste a numbered chunk verbatim.",
81
- inputSchema: {
82
- type: "object",
83
- properties: {
84
- path: {
85
- type: "string",
86
- description: "File path (relative to the execution-context cwd, or absolute)."
87
- },
88
- old_string: {
89
- type: "string",
90
- description: "Exact substring to find."
91
- },
92
- new_string: {
93
- type: "string",
94
- description: "Replacement substring."
95
- },
96
- replace_all: {
97
- type: "boolean",
98
- description: "Replace every occurrence. Default: false."
99
- }
100
- },
101
- required: [
102
- "path",
103
- "old_string",
104
- "new_string"
105
- ]
106
- }
107
- },
108
- async execute({ path, old_string, new_string, replace_all }, ctx) {
109
- const target = path;
110
- const find = old_string;
111
- const replacement = new_string;
112
- const replaceAll = replace_all === true;
113
- const pathErr = pathArgError(target);
114
- if (pathErr) {
115
- ctx.reportOutcome?.("failed");
116
- return `Edit error: ${pathErr}`;
117
- }
118
- if (find === replacement) return `Edit error: old_string and new_string are identical — nothing to change in ${target}.`;
119
- if (find.length === 0) return `Edit error: old_string is empty. Use write_file to create or fully overwrite a file.`;
120
- let original;
121
- try {
122
- original = await ctx.execution.readFile(ctx.handle, target);
123
- } catch {
124
- const hint = await suggestionFor(ctx.execution, ctx.handle, target);
125
- ctx.reportOutcome?.("failed");
126
- return `Edit error: file not found: ${target}.${hint}`;
127
- }
128
- if (ctx.behavior?.requireReadBeforeEdit) {
129
- const readState = resolveReadStateMap(ctx);
130
- if (readState) {
131
- const absKey = readStateKey(ctx.handle.cwd, target);
132
- const prior = readState.get(absKey);
133
- if (!prior) return `Edit error: ${target} has not been read in this session. Call read_file first so the edit applies against the current contents.`;
134
- if (prior.contentHash !== hashContent(original)) return `Edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
135
- }
136
- }
137
- const match = resolveOldString(original, find);
138
- if (!match) {
139
- const preview = nearestMatchPreview(original, find);
140
- ctx.reportOutcome?.("failed");
141
- return preview ? `Edit error: old_string not found in ${target}. Closest match in the file: ${preview}` : `Edit error: old_string not found in ${target}.`;
142
- }
143
- const { actual, occurrences, via } = match;
144
- if (occurrences > 1 && !replaceAll) return `Edit error: old_string appears ${occurrences} times in ${target}. Pass replace_all=true or expand old_string for uniqueness.`;
145
- const styledReplacement = styleReplacementForVia(replacement, via, actual);
146
- const updated = replaceAll ? original.split(actual).join(styledReplacement) : original.replace(actual, styledReplacement);
147
- if (updated === original) {
148
- ctx.reportOutcome?.("noop");
149
- return `Edit error: replacement produced no change in ${target}.`;
150
- }
151
- await ctx.execution.writeFile(ctx.handle, target, updated);
152
- const readState = resolveReadStateMap(ctx);
153
- if (readState) {
154
- const absKey = readStateKey(ctx.handle.cwd, target);
155
- const prior = readState.get(absKey);
156
- if (prior) readState.set(absKey, {
157
- ...prior,
158
- contentHash: hashContent(updated),
159
- mtimeMs: Date.now()
160
- });
161
- }
162
- ctx.reportOutcome?.("edited");
163
- const viaNote = via === "exact" ? "" : ` (old_string was not found verbatim; matched via ${describeVia(via)})`;
164
- return `Edited ${target}: replaced ${occurrences} occurrence${occurrences === 1 ? "" : "s"}.${viaNote}`;
165
- }
166
- };
167
- /**
168
- * Find the line that shares the longest common prefix with the needle's first
169
- * line. Cheap heuristic — better than nothing for the common "model has a typo"
170
- * case (off-by-one indent, trailing whitespace, escape mismatch).
171
- *
172
- * Returns a "line N: <preview>" snippet or null when no line shares a useful
173
- * prefix. Strips line-number prefixes from the needle first so a model that
174
- * pasted a numbered `read_file` chunk verbatim still gets a useful diagnostic.
175
- */
176
- function nearestMatchPreview(haystack, needle) {
177
- const needleFirstLine = stripLineNumberPrefixes(needle).split("\n")[0];
178
- if (needleFirstLine.length < 3) return null;
179
- const lines = haystack.split("\n");
180
- let bestScore = 0;
181
- let bestIdx = -1;
182
- for (let i = 0; i < lines.length; i++) {
183
- const score = sharedPrefixLength(lines[i], needleFirstLine);
184
- if (score > bestScore) {
185
- bestScore = score;
186
- bestIdx = i;
187
- }
188
- }
189
- if (bestIdx < 0 || bestScore < Math.min(8, Math.floor(needleFirstLine.length / 2))) return null;
190
- const snippet = lines[bestIdx].slice(0, 80);
191
- return `line ${bestIdx + 1}: ${JSON.stringify(snippet)}`;
192
- }
193
- function sharedPrefixLength(a, b) {
194
- const max = Math.min(a.length, b.length);
195
- let i = 0;
196
- while (i < max && a.charCodeAt(i) === b.charCodeAt(i)) i++;
197
- return i;
198
- }
199
- //#endregion
200
- //#region src/tools/glob.ts
201
- /**
202
- * Glob-pattern file matching.
203
- *
204
- * Delegates matching to {@link ExecutionContext.listFiles} with `{ glob:
205
- * true }`. This keeps filesystem semantics inside the execution context
206
- * (process / docker / sandbox) instead of reaching around it from the tool.
207
- *
208
- * Results are capped at 1000 entries to keep model input bounded. The
209
- * cap applies after the directory filter, so dirs don't consume the
210
- * file budget.
211
- *
212
- * Patterns are deliberately NOT confined to the workspace: like
213
- * `read_file`, `../`-prefixed patterns may match outside the cwd — the
214
- * execution context is the sandbox boundary, not this tool.
215
- *
216
- * By default, contexts that can cheaply provide metadata return
217
- * `<path>\t<size>\t<mtime>` rows. Contexts without cheap metadata support return
218
- * paths only. Pass `metadata: false` to request plain newline-separated paths.
219
- */
220
- const DEFAULT_LIMIT = 1e3;
221
- /** Backstop so a pathological walk can never hang the agent's turn. */
222
- const GLOB_TIMEOUT_MS = 2e4;
223
- function entryPath$1(entry) {
224
- return typeof entry === "string" ? entry : entry.path;
225
- }
226
- function entryMetadata(entry) {
227
- return typeof entry === "string" ? void 0 : entry;
228
- }
229
- function renderEntry(entry, wantMetadata) {
230
- const path = entryPath$1(entry);
231
- const meta = entryMetadata(entry);
232
- if (!wantMetadata || typeof meta?.size !== "number" || typeof meta.mtimeMs !== "number") return path;
233
- return `${path}\t${meta.size}\t${new Date(meta.mtimeMs).toISOString()}`;
234
- }
235
- const glob = {
236
- isConcurrencySafe: true,
237
- spec: {
238
- name: "glob",
239
- description: "Match files by glob pattern (supports **, *, ?). Relative to the execution context cwd; patterns may reach outside the workspace (e.g. \"../shared/**/*.ts\"). By default each row is `<path>\\t<size-bytes>\\t<mtime-iso>`; set `metadata: false` for a plain newline-separated list of paths. Always sorted.",
240
- inputSchema: {
241
- type: "object",
242
- properties: {
243
- pattern: {
244
- type: "string",
245
- description: "Glob pattern (e.g. \"src/**/*.ts\", \"*.md\", \"test/**/fixtures/*\")."
246
- },
247
- limit: {
248
- type: "number",
249
- description: `Maximum number of matches to return. Default: ${DEFAULT_LIMIT}.`
250
- },
251
- metadata: {
252
- type: "boolean",
253
- description: "Append size (bytes) and mtime (ISO) per row, tab-separated. Default: true. In-process only — non-process execution contexts always return paths."
254
- },
255
- gitignore: {
256
- type: "boolean",
257
- description: "Skip files git ignores (.gitignore, .git/info/exclude, global excludes). Default: true. Set false to include ignored files like node_modules. Honored where git is available; a no-op otherwise."
258
- }
259
- },
260
- required: ["pattern"]
261
- }
262
- },
263
- async execute({ pattern, limit, metadata, gitignore }, ctx) {
264
- const pat = pattern;
265
- const max = typeof limit === "number" && limit > 0 ? limit : DEFAULT_LIMIT;
266
- const wantMetadata = metadata !== false;
267
- try {
268
- if (ctx.execution.capabilities.glob !== true) throw new Error(`The active execution context (${ctx.execution.type}) does not support glob matching.`);
269
- const controller = new AbortController();
270
- const onAbort = () => controller.abort();
271
- ctx.signal.addEventListener("abort", onAbort, { once: true });
272
- let timer;
273
- const timeout = new Promise((resolve) => {
274
- timer = setTimeout(() => {
275
- controller.abort();
276
- resolve("timeout");
277
- }, GLOB_TIMEOUT_MS);
278
- });
279
- try {
280
- const outcome = await Promise.race([ctx.execution.listFiles(ctx.handle, pat, {
281
- glob: true,
282
- limit: max,
283
- metadata: wantMetadata,
284
- gitignore,
285
- signal: controller.signal
286
- }), timeout]);
287
- if (outcome === "timeout") return `Glob timed out after ${GLOB_TIMEOUT_MS}ms — narrow the pattern (anchor a literal prefix like \`src/**\`) or scope it to a subdirectory.`;
288
- if (outcome.length === 0) return "(no matches)";
289
- return outcome.map((entry) => renderEntry(entry, wantMetadata)).join("\n");
290
- } finally {
291
- clearTimeout(timer);
292
- ctx.signal.removeEventListener("abort", onAbort);
293
- }
294
- } catch (err) {
295
- return `Glob error: ${errorMessage(err)}`;
296
- }
297
- }
298
- };
299
- //#endregion
300
- //#region src/tools/grep.ts
301
- /**
302
- * Search file contents by regex.
303
- *
304
- * Wraps ripgrep (`rg`) when available; when it's not, falls back to a
305
- * regex search that enumerates files via the execution context's shell
306
- * (`find` over `exec`) and reads them via `ctx.execution.readFile`. Both
307
- * legs go through the {@link ToolContext.execution} seam, so the search
308
- * runs wherever the context's files actually live — in-process, a
309
- * container, or a remote sandbox — without the tool ever touching the host
310
- * filesystem directly.
311
- *
312
- * The tool surface mirrors Claude Code's `Grep` so models authored against the
313
- * Anthropic tool surface need no relearning. Output modes:
314
- * - `files_with_matches` (default) — newline-separated paths.
315
- * - `content` — `path:line:match` (line numbers on by default).
316
- * - `count` — `path:N` per matching file.
317
- *
318
- * Results are capped via `head_limit` (default 250) to keep model input
319
- * bounded; `offset` lets the caller page through. Pagination operates on
320
- * OUTPUT LINES, not match groups — in content mode, context lines and `--`
321
- * separators consume the budget too (documented in the schema).
322
- *
323
- * A line cap is not enough on its own: ripgrep is line-oriented, so a minified
324
- * / single-line file collapses to ONE match line that is the entire file, and
325
- * `head_limit` would happily return megabytes — enough to blow the model's
326
- * context window. Output is therefore ALSO byte-bounded: each line is clipped
327
- * to `MAX_LINE_BYTES` and the total to `OUTPUT_BYTE_CAP`, mirroring the byte
328
- * discipline `read_file` already enforces.
329
- *
330
- * `path` is deliberately NOT confined to the workspace: like `read_file`,
331
- * absolute paths and `../` traversal are allowed — the execution context is
332
- * the sandbox boundary, not this tool.
333
- */
334
- const DEFAULT_HEAD_LIMIT = 250;
335
- /**
336
- * Per-line byte cap. ripgrep reports a match as the whole matching LINE, so a
337
- * minified / single-line file (one line = the entire file) would otherwise be
338
- * returned in full. Clip each line so a single line can't dump unbounded bytes
339
- * into the model context.
340
- */
341
- const MAX_LINE_BYTES = 2e3;
342
- /** Total output byte cap, mirroring read_file's `DEFAULT_BYTE_CAP`. */
343
- const OUTPUT_BYTE_CAP = 262144;
344
- const DEFAULT_OUTPUT_MODE = "files_with_matches";
345
- const grep = {
346
- isConcurrencySafe: true,
347
- spec: {
348
- name: "grep",
349
- description: "Search file contents by regex. Returns matching paths (default), match content, or per-file counts. Backed by ripgrep when available, with a portable shell-listing + regex fallback when `rg` is absent.",
350
- inputSchema: {
351
- type: "object",
352
- properties: {
353
- "pattern": {
354
- type: "string",
355
- description: "Regex (PCRE-flavored via ripgrep, JS regex via fallback)."
356
- },
357
- "path": {
358
- type: "string",
359
- description: "File or directory to search. Default: \".\". May be absolute or outside the workspace (no confinement — consistent with read_file)."
360
- },
361
- "glob": {
362
- type: "string",
363
- description: "Restrict to files matching this glob, e.g. \"**/*.ts\"."
364
- },
365
- "type": {
366
- type: "string",
367
- description: "rg file type filter, e.g. \"ts\", \"py\", \"rust\". Ignored by the fallback."
368
- },
369
- "output_mode": {
370
- type: "string",
371
- enum: [
372
- "content",
373
- "files_with_matches",
374
- "count"
375
- ],
376
- description: "Default: \"files_with_matches\"."
377
- },
378
- "-i": {
379
- type: "boolean",
380
- description: "Case-insensitive match."
381
- },
382
- "-n": {
383
- type: "boolean",
384
- description: "Show line numbers (content mode). Default: true."
385
- },
386
- "-A": {
387
- type: "integer",
388
- description: "Lines of trailing context (content mode)."
389
- },
390
- "-B": {
391
- type: "integer",
392
- description: "Lines of leading context (content mode)."
393
- },
394
- "-C": {
395
- type: "integer",
396
- description: "Lines of surrounding context (content mode). Overridden by -A/-B if set."
397
- },
398
- "multiline": {
399
- type: "boolean",
400
- description: "Allow patterns to match across line boundaries."
401
- },
402
- "gitignore": {
403
- type: "boolean",
404
- description: "Skip files git ignores (.gitignore, .git/info/exclude, global excludes). Default: true. Set false to also search ignored files like node_modules."
405
- },
406
- "head_limit": {
407
- type: "integer",
408
- description: `Cap output lines. Default: 250. Set 0 for unlimited lines. In content mode, context lines (-A/-B/-C) and "--" separators count toward the cap, so the number of matches shown may be lower. Output is independently byte-bounded: each line is clipped to ${MAX_LINE_BYTES} bytes and the total to ${OUTPUT_BYTE_CAP} bytes, so long (e.g. minified) lines never overflow the context window.`
409
- },
410
- "offset": {
411
- type: "integer",
412
- description: "Skip first N output lines. Default: 0."
413
- }
414
- },
415
- required: ["pattern"]
416
- }
417
- },
418
- async execute(rawInput, ctx) {
419
- const input = rawInput;
420
- try {
421
- if (await isRipgrepAvailable(ctx)) return await runViaRipgrep(input, ctx);
422
- return await runFallback(input, ctx);
423
- } catch (err) {
424
- if (isAbortLikeError(err)) throw err;
425
- return `grep error: ${errorMessage(err)}`;
426
- }
427
- }
428
- };
429
- /**
430
- * Probe ripgrep availability **per call**. The probe is intentionally not
431
- * cached: caching at module scope would leak across execution contexts (an
432
- * orchestrator running an in-process agent and a docker agent in the same
433
- * Node process must be able to differ on whether `rg` exists), and caching
434
- * per handle adds bookkeeping for negligible savings — `rg --version` is
435
- * ~5 ms, and grep is invoked at most a few times per turn.
436
- */
437
- async function isRipgrepAvailable(ctx) {
438
- return (await ctx.execution.exec(ctx.handle, "rg --version", { signal: ctx.signal })).exitCode === 0;
439
- }
440
- async function runViaRipgrep(input, ctx) {
441
- const args = ["rg"];
442
- const mode = input.output_mode ?? DEFAULT_OUTPUT_MODE;
443
- if (mode === "files_with_matches") args.push("--files-with-matches");
444
- else if (mode === "count") args.push("--count");
445
- else args.push(input["-n"] ?? true ? "--line-number" : "--no-line-number");
446
- if (input["-i"]) args.push("-i");
447
- if (mode === "content") {
448
- if (typeof input["-A"] === "number") args.push("-A", String(input["-A"]));
449
- if (typeof input["-B"] === "number") args.push("-B", String(input["-B"]));
450
- if (typeof input["-C"] === "number" && typeof input["-A"] !== "number" && typeof input["-B"] !== "number") args.push("-C", String(input["-C"]));
451
- }
452
- if (input.multiline) args.push("--multiline", "--multiline-dotall");
453
- if (input.gitignore === false) args.push("--no-ignore");
454
- if (input.glob) args.push("--glob", input.glob);
455
- if (input.type) args.push("--type", input.type);
456
- args.push("--", input.pattern);
457
- args.push(input.path ?? ".");
458
- const command = args.map(shellQuote).join(" ");
459
- const result = await ctx.execution.exec(ctx.handle, command, { signal: ctx.signal });
460
- if (result.exitCode !== 0 && result.exitCode !== 1) return `grep error: ${result.stderr.trim() || `rg exited with code ${result.exitCode}`}`;
461
- return formatPaginated(result.stdout, input);
462
- }
463
- async function runFallback(input, ctx) {
464
- const mode = input.output_mode ?? DEFAULT_OUTPUT_MODE;
465
- const flags = `${input["-i"] ? "i" : ""}${input.multiline ? "s" : ""}${mode !== "content" ? "" : "g"}`;
466
- let regex;
467
- try {
468
- regex = new RegExp(input.pattern, flags || void 0);
469
- } catch (err) {
470
- return `grep error: invalid regex: ${errorMessage(err)}`;
471
- }
472
- const files = await enumerateFiles(input, ctx);
473
- const showLineNumbers = input["-n"] ?? true;
474
- const before = input["-B"] ?? input["-C"] ?? 0;
475
- const after = input["-A"] ?? input["-C"] ?? 0;
476
- const lines = [];
477
- for (const path of files) {
478
- let content;
479
- try {
480
- content = await ctx.execution.readFile(ctx.handle, path);
481
- } catch (err) {
482
- if (isAbortLikeError(err)) throw err;
483
- continue;
484
- }
485
- if (input.multiline) {
486
- const allMatches = [...content.matchAll(new RegExp(regex.source, `${flags.replace(/g/, "")}g`))];
487
- if (allMatches.length === 0) continue;
488
- if (mode === "files_with_matches") {
489
- lines.push(path);
490
- continue;
491
- }
492
- if (mode === "count") {
493
- lines.push(`${path}:${allMatches.length}`);
494
- continue;
495
- }
496
- for (const m of allMatches) {
497
- const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
498
- const lineEnd = content.indexOf("\n", m.index);
499
- const snippet = content.slice(lineStart, lineEnd === -1 ? void 0 : lineEnd);
500
- const lineNo = content.slice(0, m.index).split("\n").length;
501
- lines.push(formatContentLine(path, lineNo, snippet, showLineNumbers));
502
- }
503
- continue;
504
- }
505
- const fileLines = content.split("\n");
506
- const matched = [];
507
- for (let i = 0; i < fileLines.length; i++) {
508
- regex.lastIndex = 0;
509
- if (regex.test(fileLines[i])) matched.push(i);
510
- }
511
- if (matched.length === 0) continue;
512
- if (mode === "files_with_matches") {
513
- lines.push(path);
514
- continue;
515
- }
516
- if (mode === "count") {
517
- lines.push(`${path}:${matched.length}`);
518
- continue;
519
- }
520
- const includeLineNos = /* @__PURE__ */ new Set();
521
- for (const m of matched) for (let i = Math.max(0, m - before); i <= Math.min(fileLines.length - 1, m + after); i++) includeLineNos.add(i);
522
- const sorted = [...includeLineNos].sort((a, b) => a - b);
523
- let prev = -2;
524
- for (const lineNo of sorted) {
525
- if (lineNo > prev + 1 && lines.length > 0) lines.push("--");
526
- const snippet = fileLines[lineNo];
527
- lines.push(formatContentLine(path, lineNo + 1, snippet, showLineNumbers));
528
- prev = lineNo;
529
- }
530
- }
531
- return formatPaginated(lines.join("\n"), input);
532
- }
533
- function formatContentLine(path, lineNo, snippet, showLineNumbers) {
534
- return showLineNumbers ? `${path}:${lineNo}:${snippet}` : `${path}:${snippet}`;
535
- }
536
- async function enumerateFiles(input, ctx) {
537
- const root = input.path ?? ".";
538
- if (input.path && !input.path.includes("*") && !input.path.includes("?")) {
539
- if ((await ctx.execution.exec(ctx.handle, `test -f ${shellQuote(input.path)} && echo file || echo dir`, { signal: ctx.signal })).stdout.trim() === "file") return [input.path];
540
- }
541
- const result = await ctx.execution.exec(ctx.handle, buildFindCommand(root, input.glob), { signal: ctx.signal });
542
- if (result.exitCode !== 0 && !result.stdout.trim()) throw new Error(result.stderr.trim() || `find exited with code ${result.exitCode}`);
543
- const files = result.stdout.split("\n").map((line) => line.replace(/^\.\//, "")).filter((line) => line.length > 0).sort();
544
- if (input.gitignore === false) return files;
545
- return filterByGitignore(files, root, ctx);
546
- }
547
- /**
548
- * Best-effort intersection of `files` with `git ls-files` (non-ignored set).
549
- * Returns `files` unchanged when git is unavailable, the dir isn't a repo, or
550
- * `root` escapes the repo (absolute / `..`) — the filter can only fail open.
551
- */
552
- async function filterByGitignore(files, root, ctx) {
553
- if (files.length === 0) return files;
554
- if (root !== "." && (root.startsWith("/") || root.split("/").includes(".."))) return files;
555
- try {
556
- const res = await ctx.execution.exec(ctx.handle, GIT_LS_FILES_COMMAND, { signal: ctx.signal });
557
- if (res.exitCode !== 0) return files;
558
- const allowed = new Set(parseGitLsFiles(res.stdout));
559
- if (allowed.size === 0) return files;
560
- return files.filter((file) => allowed.has(file));
561
- } catch {
562
- return files;
563
- }
564
- }
565
- /**
566
- * Build a `find` command listing files under `root`, optionally filtered by
567
- * `glob`. Mirrors `glob`'s shell fallback:
568
- * - a bare basename, or one behind a leading recursive-wildcard segment,
569
- * with no inner slash → `-name` (matches at any depth, including the top
570
- * level — preserving the recursive-glob semantics).
571
- * - anything with a slash → `-path` against the full relative path. `find`'s
572
- * `*` spans `/`, so the single- and double-star forms collapse to the
573
- * same matcher; close enough for the no-`rg` fallback.
574
- */
575
- function buildFindCommand(root, glob) {
576
- const findRoot = normalizeFindRoot(root);
577
- const base = shellQuote(findRoot);
578
- if (!glob) return `find ${base} -type f`;
579
- const anyDepth = glob.match(/^(?:\*\*\/)?([^/]+)$/);
580
- if (anyDepth) return `find ${base} -type f -name ${shellQuote(anyDepth[1])}`;
581
- const normRoot = findRoot.replace(/\/$/, "");
582
- return `find ${base} -type f -path ${shellQuote(root === "." ? `./${glob}` : `${normRoot}/${glob}`)}`;
583
- }
584
- function normalizeFindRoot(root) {
585
- return root.startsWith("-") ? `./${root}` : root;
586
- }
587
- function formatPaginated(text, input) {
588
- const headLimit = typeof input.head_limit === "number" && input.head_limit >= 0 ? input.head_limit : DEFAULT_HEAD_LIMIT;
589
- const offset = typeof input.offset === "number" && input.offset > 0 ? Math.floor(input.offset) : 0;
590
- if (!text.trim()) return "(no matches)";
591
- const lines = text.split("\n").filter((l) => l.length > 0);
592
- const total = lines.length;
593
- const sliced = headLimit === 0 ? lines.slice(offset) : lines.slice(offset, offset + headLimit);
594
- if (sliced.length === 0) return "(no matches in this slice)";
595
- const capped = [];
596
- let bytesUsed = 0;
597
- let byteCut = false;
598
- for (const line of sliced) {
599
- const clipped = clipLine(line);
600
- const lineBytes = Buffer.byteLength(clipped) + 1;
601
- if (bytesUsed + lineBytes > OUTPUT_BYTE_CAP && capped.length > 0) {
602
- byteCut = true;
603
- break;
604
- }
605
- capped.push(clipped);
606
- bytesUsed += lineBytes;
607
- }
608
- const shown = capped.length;
609
- const truncatedHead = offset > 0;
610
- const lineCutTail = headLimit > 0 && offset + headLimit < total;
611
- let out = capped.join("\n");
612
- if (truncatedHead) out = `…(${offset} earlier lines skipped)…\n${out}`;
613
- if (byteCut) out = `${out}\n…(output truncated at ${OUTPUT_BYTE_CAP} bytes after ${shown} of ${total - offset} matching lines; narrow the pattern or page with offset=${offset + shown})`;
614
- else if (lineCutTail) out = `${out}\n…(${total - offset - headLimit} more lines; re-run with offset=${offset + headLimit} or larger head_limit)`;
615
- return out;
616
- }
617
- /**
618
- * Clip one output line to `MAX_LINE_BYTES` at a UTF-8-safe boundary, appending
619
- * a marker that reports how many bytes were withheld. Lines within budget pass
620
- * through untouched.
621
- */
622
- function clipLine(line) {
623
- if (Buffer.byteLength(line) <= MAX_LINE_BYTES) return line;
624
- let cut = Math.min(line.length, MAX_LINE_BYTES);
625
- while (cut > 0 && Buffer.byteLength(line.slice(0, cut)) > MAX_LINE_BYTES) cut--;
626
- const omitted = Buffer.byteLength(line) - Buffer.byteLength(line.slice(0, cut));
627
- return `${line.slice(0, cut)}… [line clipped, ${omitted} more bytes]`;
628
- }
629
- //#endregion
630
- //#region src/tools/interaction.ts
631
- function abortReason(signal) {
632
- return typeof signal.reason === "string" && signal.reason.length > 0 ? signal.reason : "Interaction aborted";
633
- }
634
- /**
635
- * Create an interaction tool that lets the agent request structured input.
636
- *
637
- * The model calls this tool with a payload matching the schema.
638
- * `onRequest` is called with the payload and should return the response
639
- * (string or object) that gets sent back to the model as the tool result.
640
- */
641
- function createInteractionTool(options) {
642
- return {
643
- spec: {
644
- name: options.name ?? "interaction",
645
- description: options.description ?? "Request structured input from the user or external system.",
646
- inputSchema: options.schema
647
- },
648
- async execute(input, ctx) {
649
- const { signal } = ctx;
650
- if (signal?.aborted) throw new Error(abortReason(signal));
651
- let onAbort;
652
- const abortPromise = signal ? new Promise((_, reject) => {
653
- onAbort = () => reject(new Error(abortReason(signal)));
654
- signal.addEventListener("abort", onAbort, { once: true });
655
- }) : void 0;
656
- try {
657
- const result = abortPromise ? await Promise.race([options.onRequest(input, ctx), abortPromise]) : await options.onRequest(input, ctx);
658
- return typeof result === "string" ? result : JSON.stringify(result);
659
- } finally {
660
- if (signal && onAbort) signal.removeEventListener("abort", onAbort);
661
- }
662
- }
663
- };
664
- }
665
- //#endregion
666
- //#region src/tools/list-files.ts
667
- function entryPath(entry) {
668
- return typeof entry === "string" ? entry : entry.path;
669
- }
670
- const listFiles = {
671
- isConcurrencySafe: true,
672
- spec: {
673
- name: "list_files",
674
- description: "List the immediate entries (files and subdirectories) at literal directory `path`. Returns a newline-separated list, or `(empty directory)` when the directory exists but is empty. Returns `Directory not found: <path>` for a missing path. Non-recursive: use `glob` for pattern matching across nested directories.",
675
- inputSchema: {
676
- type: "object",
677
- properties: { path: {
678
- type: "string",
679
- description: "Directory path (relative to the execution-context cwd, or absolute). Defaults to `.`."
680
- } },
681
- required: []
682
- }
683
- },
684
- async execute({ path }, ctx) {
685
- try {
686
- return (await ctx.execution.listFiles(ctx.handle, path || ".")).map(entryPath).join("\n") || "(empty directory)";
687
- } catch {
688
- return `Directory not found: ${path}`;
689
- }
690
- }
691
- };
692
- //#endregion
693
- //#region src/tools/multi-edit.ts
694
- /**
695
- * Inline annotation builder — kept local to avoid importing from
696
- * `chat/edit-approval.ts` (that's a renderer-side module; tools live
697
- * one layer below). Line shape matches `parseEditOutcomesFromResult`'s
698
- * regex so the round-trip is lossless.
699
- *
700
- * Newlines in `reason` are folded to spaces because the parser is line-
701
- * scoped (`body.split('\n')`); a multi-line reason would split into a
702
- * "trailing prose" line and trip the malformed-block guard, losing every
703
- * outcome below it. Static reasons in this file are single-line; the
704
- * sanitize is a guard against a pathological `target` (file path
705
- * containing a newline) leaking into `old_string not found in <target>`.
706
- */
707
- function annotationFor(outcomes) {
708
- const lines = ["<edit-outcomes>"];
709
- for (let i = 0; i < outcomes.length; i++) {
710
- const o = outcomes[i];
711
- const reason = o.reason ? `: ${o.reason.replace(/\r?\n/g, " ")}` : "";
712
- lines.push(`#${i + 1} ${o.kind}${reason}`);
713
- }
714
- lines.push("</edit-outcomes>");
715
- return lines.join("\n");
716
- }
717
- const multiEdit = {
718
- spec: {
719
- name: "multi_edit",
720
- description: "Apply a sequential list of edits to a file. Each edit operates on the result of the previous APPLIED edit. Prefer this over multiple `edit` calls when several non-overlapping changes are needed in the same file. Edits run **best-effort**: a per-step failure (`old_string` not found, ambiguous match without `replace_all`, identical strings) is reported in the result but does NOT block the remaining steps. The file is written iff at least one step applied. The result lists per-hunk outcomes (`applied` / `failed`) so the model can re-issue just the failures without resending the whole batch. Each step tolerates `read_file` line-number prefixes (`<N>\\t…`, `<N>|…`, or `<N>→…`) in `old_string` / `new_string`.",
721
- inputSchema: {
722
- type: "object",
723
- properties: {
724
- path: {
725
- type: "string",
726
- description: "File path (relative to the execution-context cwd, or absolute)."
727
- },
728
- edits: {
729
- type: "array",
730
- description: "List of edits applied in order; each operates on the previous applied edit's output.",
731
- items: {
732
- type: "object",
733
- properties: {
734
- old_string: { type: "string" },
735
- new_string: { type: "string" },
736
- replace_all: { type: "boolean" }
737
- },
738
- required: ["old_string", "new_string"]
739
- }
740
- }
741
- },
742
- required: ["path", "edits"]
743
- }
744
- },
745
- async execute({ path, edits }, ctx) {
746
- const target = path;
747
- const steps = edits;
748
- const pathErr = pathArgError(target);
749
- if (pathErr) {
750
- ctx.reportOutcome?.("failed");
751
- return `multi_edit error: ${pathErr}`;
752
- }
753
- if (!Array.isArray(steps) || steps.length === 0) {
754
- ctx.reportOutcome?.("failed");
755
- return `multi_edit error: edits must be a non-empty array.`;
756
- }
757
- let current;
758
- try {
759
- current = await ctx.execution.readFile(ctx.handle, target);
760
- } catch {
761
- const hint = await suggestionFor(ctx.execution, ctx.handle, target);
762
- ctx.reportOutcome?.("failed");
763
- return `multi_edit error: file not found: ${target}.${hint}`;
764
- }
765
- if (ctx.behavior?.requireReadBeforeEdit) {
766
- const readState = resolveReadStateMap(ctx);
767
- if (readState) {
768
- const absKey = readStateKey(ctx.handle.cwd, target);
769
- const prior = readState.get(absKey);
770
- if (!prior) {
771
- ctx.reportOutcome?.("failed");
772
- return `multi_edit error: ${target} has not been read in this session. Call read_file first so the edits apply against the current contents.`;
773
- }
774
- if (prior.contentHash !== hashContent(current)) {
775
- ctx.reportOutcome?.("failed");
776
- return `multi_edit error: ${target} has changed on disk since the last read. Re-read the file before editing.`;
777
- }
778
- }
779
- }
780
- const outcomes = [];
781
- const viaNotes = [];
782
- let totalReplacements = 0;
783
- for (let i = 0; i < steps.length; i++) {
784
- const step = steps[i];
785
- const find = step.old_string;
786
- const replacement = step.new_string;
787
- const replaceAll = step.replace_all === true;
788
- if (typeof find !== "string" || typeof replacement !== "string") {
789
- outcomes.push({
790
- kind: "failed",
791
- reason: "missing old_string or new_string"
792
- });
793
- continue;
794
- }
795
- if (find.length === 0) {
796
- outcomes.push({
797
- kind: "failed",
798
- reason: "empty old_string (use write_file to fully replace a file)"
799
- });
800
- continue;
801
- }
802
- if (find === replacement) {
803
- outcomes.push({
804
- kind: "failed",
805
- reason: "old_string and new_string are identical"
806
- });
807
- continue;
808
- }
809
- const match = resolveOldString(current, find);
810
- if (!match) {
811
- outcomes.push({
812
- kind: "failed",
813
- reason: `old_string not found in ${target}`
814
- });
815
- continue;
816
- }
817
- const { actual, occurrences, via } = match;
818
- if (occurrences > 1 && !replaceAll) {
819
- outcomes.push({
820
- kind: "failed",
821
- reason: `old_string appears ${occurrences} times — pass replace_all=true on this edit or expand old_string for uniqueness`
822
- });
823
- continue;
824
- }
825
- const styledReplacement = styleReplacementForVia(replacement, via, actual);
826
- current = replaceAll ? current.split(actual).join(styledReplacement) : current.replace(actual, styledReplacement);
827
- totalReplacements += occurrences;
828
- if (via !== "exact") viaNotes.push(`edit #${i + 1}: old_string was not found verbatim; matched via ${describeVia(via)}`);
829
- outcomes.push({ kind: "applied" });
830
- }
831
- const appliedCount = outcomes.reduce((n, o) => o.kind === "applied" ? n + 1 : n, 0);
832
- const failedCount = outcomes.length - appliedCount;
833
- if (appliedCount > 0) {
834
- await ctx.execution.writeFile(ctx.handle, target, current);
835
- ctx.reportOutcome?.("edited");
836
- const readState = resolveReadStateMap(ctx);
837
- if (readState) {
838
- const absKey = readStateKey(ctx.handle.cwd, target);
839
- const prior = readState.get(absKey);
840
- if (prior) readState.set(absKey, {
841
- ...prior,
842
- contentHash: hashContent(current),
843
- mtimeMs: Date.now()
844
- });
845
- }
846
- } else ctx.reportOutcome?.("failed");
847
- const n = steps.length;
848
- let header;
849
- if (appliedCount === n) header = `Edited ${target}: applied ${n} edit${n === 1 ? "" : "s"} (${totalReplacements} replacement${totalReplacements === 1 ? "" : "s"}).`;
850
- else if (appliedCount > 0) header = `Edited ${target}: applied ${appliedCount} of ${n} edits (${totalReplacements} replacement${totalReplacements === 1 ? "" : "s"}).`;
851
- else header = `multi_edit error: no edits applied to ${target} (${n} attempted).`;
852
- const failureLines = [];
853
- for (let i = 0; i < outcomes.length; i++) {
854
- const o = outcomes[i];
855
- if (o.kind === "failed") failureLines.push(`edit #${i + 1} failed: ${o.reason}`);
856
- }
857
- const parts = [header];
858
- if (viaNotes.length > 0) parts.push(viaNotes.join("\n"));
859
- if (failureLines.length > 0) parts.push(failureLines.join("\n"));
860
- if (failedCount > 0) parts.push(annotationFor(outcomes));
861
- return parts.join("\n\n");
862
- }
863
- };
864
- //#endregion
865
- //#region src/tools/binary-read.ts
866
- /**
867
- * Best-effort guess at IANA media type from a file extension. Covers the
868
- * extensions Zidane's `read_file` actually dispatches to image blocks
869
- * (png/jpg/jpeg/gif/webp). Other extensions return `undefined` and the
870
- * caller short-circuits the binary route.
871
- */
872
- function imageMediaTypeFor(path) {
873
- const dot = path.lastIndexOf(".");
874
- if (dot === -1) return void 0;
875
- switch (path.slice(dot + 1).toLowerCase()) {
876
- case "png": return "image/png";
877
- case "jpg":
878
- case "jpeg": return "image/jpeg";
879
- case "gif": return "image/gif";
880
- case "webp": return "image/webp";
881
- default: return;
882
- }
883
- }
884
- /**
885
- * Read a file as base64. Prefers `ExecutionContext.readFileBinary` (zero
886
- * subprocess overhead in-process) and falls back to `base64 < path` via
887
- * the shell seam — works on docker / sandbox without an interface change.
888
- *
889
- * Returns `{ base64, byteLength }`. `byteLength` is the *decoded* byte count
890
- * so callers can size-budget against the original file, not the inflated
891
- * base64 representation (which is ~4/3× larger).
892
- */
893
- async function readFileAsBase64(execution, handle, path) {
894
- if (execution.readFileBinary) {
895
- const bytes = await execution.readFileBinary(handle, path);
896
- return {
897
- base64: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64"),
898
- byteLength: bytes.byteLength
899
- };
900
- }
901
- const cmd = `base64 < ${alwaysQuote(path)}`;
902
- const result = await execution.exec(handle, cmd);
903
- if (result.exitCode !== 0) throw new Error(`base64 read failed: ${result.stderr || `exit ${result.exitCode}`}`);
904
- const b64 = result.stdout.replace(/\s+/g, "");
905
- return {
906
- base64: b64,
907
- byteLength: decodedBase64ByteLength(b64)
908
- };
909
- }
910
- /**
911
- * Decoded byte length of a (whitespace-stripped) base64 string. Accounts for
912
- * `=` padding so the value matches the original file size to the byte —
913
- * `Math.floor(len * 3 / 4)` over-reports by 1–2 bytes on padded payloads.
914
- */
915
- function decodedBase64ByteLength(b64) {
916
- if (b64.length === 0) return 0;
917
- let pad = 0;
918
- if (b64.endsWith("==")) pad = 2;
919
- else if (b64.endsWith("=")) pad = 1;
920
- return Math.max(0, b64.length * 3 / 4 - pad);
921
- }
922
- //#endregion
923
- //#region src/tools/read-file.ts
924
- /**
925
- * Read a file with line-based offset/limit and a hard byte cap.
926
- *
927
- * Defaults are tuned for source code: 2000 lines / 256 KiB. A typical source
928
- * file, lockfile, or large config fits in one read; logs and very large
929
- * fixtures get truncated with a footer that documents how to fetch the
930
- * remainder.
931
- *
932
- * Binary files are detected on the leading bytes — if the buffer contains a
933
- * NUL or has an unreasonable proportion of non-printable bytes, we skip text
934
- * decoding and return a marker so the model doesn't drown in mojibake.
935
- */
936
- const DEFAULT_LINE_LIMIT = 2e3;
937
- const DEFAULT_BYTE_CAP = 262144;
938
- /**
939
- * Hard upper bound on raw attachment bytes we'll inline as a base64 block.
940
- * Above this, we return a marker instead — the model won't get useful
941
- * information from a 10 MB+ screenshot rendered as one tool result, and
942
- * the wire bill gets ugly. Override via the `maxBytes` parameter on the
943
- * tool call.
944
- */
945
- const DEFAULT_ATTACHMENT_BYTE_CAP = 5 * 1024 * 1024;
946
- function attachmentByteLength(data, encoding) {
947
- if (encoding === "text") return Buffer.byteLength(data);
948
- if (data.length === 0) return 0;
949
- const pad = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
950
- return Math.max(0, Math.floor(data.length * 3 / 4) - pad);
951
- }
952
- const readFile = {
953
- isConcurrencySafe: true,
954
- spec: {
955
- name: "read_file",
956
- description: "Read a file by path. Text files return lines [offset..offset+limit). Default offset=1, limit=2000. Each line is prefixed with its 1-indexed line number followed by a tab (e.g. `42\\tconst foo = bar`); the prefix is metadata, not part of the file. Mirrors Claude Code's `cat -n`-style compact output for token efficiency. A trailing footer explains how to read the rest when truncated. Images (png/jpg/gif/webp) and PDFs return structured attachments for capable models; other binary files return a short marker rather than mojibake.",
957
- inputSchema: {
958
- type: "object",
959
- properties: {
960
- path: {
961
- type: "string",
962
- description: "File path (relative to the execution-context cwd, or absolute)."
963
- },
964
- offset: {
965
- type: "integer",
966
- description: "1-indexed line number to start from. Default: 1."
967
- },
968
- limit: {
969
- type: "integer",
970
- description: "Max lines to return. Default: 2000. Set 0 for unlimited."
971
- },
972
- maxBytes: {
973
- type: "integer",
974
- description: "Hard byte cap on file content read, regardless of line count. Default: 262144 for text, 5242880 for image/PDF attachments. Set 0 for unlimited. The rendered output may be slightly larger than this cap when `lineNumbers` is on (each line carries a `<N>\\t` prefix)."
975
- },
976
- lineNumbers: {
977
- type: "boolean",
978
- description: "Prefix each line with its 1-indexed line number. Default: true. Override the agent-wide `behavior.readLineNumbers` for this call."
979
- }
980
- },
981
- required: ["path"]
982
- }
983
- },
984
- async execute({ path, offset, limit, maxBytes, lineNumbers }, ctx) {
985
- const defaults = resolveReadFileDefaults(ctx.behavior, {
986
- lineLimit: DEFAULT_LINE_LIMIT,
987
- textMaxBytes: DEFAULT_BYTE_CAP,
988
- attachmentMaxBytes: DEFAULT_ATTACHMENT_BYTE_CAP
989
- });
990
- const handleHash = typeof path === "string" ? parseAttachmentHandle(path) : null;
991
- if (handleHash) {
992
- const turns = ctx.session?.turns;
993
- if (!turns) return `Attachment ${path} cannot be resolved: no session is bound to this run.`;
994
- const found = resolveAttachmentFromTurns(turns, handleHash);
995
- if (!found) return `Attachment ${path} not found in this session's history (it may predate this session, or only ever lived as a host-offloaded reference).`;
996
- const sizeCap = normalizeReadFileInteger(maxBytes, defaults.attachmentMaxBytes);
997
- const byteLength = attachmentByteLength(found.data, found.encoding);
998
- if (sizeCap > 0 && byteLength > sizeCap) return `[attachment too large to re-inline: ${path}, ${byteLength} bytes (cap ${sizeCap}). Raise maxBytes if you need the original bytes.]`;
999
- return [{
1000
- type: "text",
1001
- text: `${found.kind === "image" ? "Image" : "Document"} re-resolved from session history (${found.mediaType}${found.name ? `, "${found.name}"` : ""}).`
1002
- }, found.kind === "image" ? {
1003
- type: "image",
1004
- mediaType: found.mediaType,
1005
- data: found.data,
1006
- ...found.name ? { name: found.name } : {}
1007
- } : {
1008
- type: "document",
1009
- mediaType: found.mediaType,
1010
- data: found.data,
1011
- encoding: found.encoding ?? "base64",
1012
- ...found.name ? { name: found.name } : {}
1013
- }];
1014
- }
1015
- const pathErr = pathArgError(path);
1016
- if (pathErr) return `Read error: ${pathErr}`;
1017
- const extMedia = imageMediaTypeFor(path);
1018
- if (extMedia) {
1019
- const sizeCap = normalizeReadFileInteger(maxBytes, defaults.attachmentMaxBytes);
1020
- try {
1021
- const { base64, byteLength } = await readFileAsBase64(ctx.execution, ctx.handle, path);
1022
- if (sizeCap > 0 && byteLength > sizeCap) return `[image too large to inline: ${path}, ${byteLength} bytes (cap ${sizeCap}). Raise maxBytes, or use shell to inspect.]`;
1023
- const imgMedia = reconcileImageMediaType(extMedia, base64);
1024
- return [{
1025
- type: "text",
1026
- text: `Image: ${path} (${byteLength} bytes, ${imgMedia})`
1027
- }, {
1028
- type: "image",
1029
- mediaType: imgMedia,
1030
- data: base64
1031
- }];
1032
- } catch (err) {
1033
- const hint = await suggestionFor(ctx.execution, ctx.handle, path);
1034
- return `Image read failed: ${path} — ${errorMessage(err)}.${hint}`;
1035
- }
1036
- }
1037
- const docMedia = documentMediaTypeFor(path);
1038
- if (docMedia) {
1039
- const sizeCap = normalizeReadFileInteger(maxBytes, defaults.attachmentMaxBytes);
1040
- try {
1041
- const { base64, byteLength } = await readFileAsBase64(ctx.execution, ctx.handle, path);
1042
- if (sizeCap > 0 && byteLength > sizeCap) return `[document too large to attach: ${path}, ${byteLength} bytes (cap ${sizeCap}). Raise maxBytes, or use shell/read text extraction to inspect.]`;
1043
- if (docMedia === "application/pdf" && !isPdfBase64(base64)) return `[binary file: ${path}, ${byteLength} bytes; extension suggests PDF but file header does not start with %PDF-]`;
1044
- return [{
1045
- type: "text",
1046
- text: `Document: ${path} (${byteLength} bytes, ${docMedia})`
1047
- }, {
1048
- type: "document",
1049
- mediaType: docMedia,
1050
- data: base64,
1051
- encoding: "base64",
1052
- name: fileNameForPath(path)
1053
- }];
1054
- } catch (err) {
1055
- const hint = await suggestionFor(ctx.execution, ctx.handle, path);
1056
- return `Document read failed: ${path} — ${errorMessage(err)}.${hint}`;
1057
- }
1058
- }
1059
- let raw;
1060
- try {
1061
- raw = await ctx.execution.readFile(ctx.handle, path);
1062
- } catch {
1063
- return `File not found: ${path}.${await suggestionFor(ctx.execution, ctx.handle, path)}`;
1064
- }
1065
- const totalBytes = Buffer.byteLength(raw);
1066
- const dedupEnabled = ctx.behavior?.dedupReads !== false;
1067
- const gateEnabled = ctx.behavior?.requireReadBeforeEdit === true;
1068
- const readState = dedupEnabled || gateEnabled ? resolveReadStateMap(ctx) : void 0;
1069
- const absKey = readStateKey(ctx.handle.cwd, path);
1070
- const offsetForKey = normalizeReadFileInteger(offset, 1);
1071
- const limitForKey = normalizeReadFileInteger(limit, defaults.lineLimit);
1072
- const maxBytesForKey = normalizeReadFileInteger(maxBytes, defaults.textMaxBytes);
1073
- const showLineNumbers = typeof lineNumbers === "boolean" ? lineNumbers : ctx.behavior?.readLineNumbers ?? true;
1074
- const currentHash = readState ? hashContent(raw) : "";
1075
- const rememberRead = () => {
1076
- if (!readState) return;
1077
- readState.set(absKey, {
1078
- contentHash: currentHash,
1079
- offset: offsetForKey,
1080
- limit: limitForKey,
1081
- maxBytes: maxBytesForKey,
1082
- lineNumbers: showLineNumbers,
1083
- mtimeMs: Date.now()
1084
- });
1085
- };
1086
- if (dedupEnabled && readState) {
1087
- const prior = readState.get(absKey);
1088
- if (prior && prior.contentHash === currentHash && prior.offset === offsetForKey && prior.limit === limitForKey && prior.maxBytes === maxBytesForKey && prior.lineNumbers === showLineNumbers && prior.elided !== true) {
1089
- rememberRead();
1090
- return `File ${path} unchanged since the previous read in this session — the prior result above is still current, so this duplicate read was skipped to save tokens.`;
1091
- }
1092
- }
1093
- if (looksBinary(raw)) return `[binary file: ${path}, ${totalBytes} bytes; use shell with hexdump | xxd | od to inspect]`;
1094
- const offsetN = offsetForKey;
1095
- const limitN = limitForKey;
1096
- const maxBytesN = maxBytesForKey;
1097
- const lines = raw.split("\n");
1098
- const totalLines = lines.length;
1099
- const startIdx = Math.max(0, offsetN - 1);
1100
- const endIdx = limitN > 0 ? Math.min(totalLines, startIdx + limitN) : totalLines;
1101
- let slice = lines.slice(startIdx, endIdx);
1102
- let bytesCut = false;
1103
- if (maxBytesN > 0) {
1104
- const truncatedSlice = [];
1105
- let bytesUsed = 0;
1106
- for (const line of slice) {
1107
- const lineBytes = Buffer.byteLength(line) + 1;
1108
- if (bytesUsed + lineBytes > maxBytesN && truncatedSlice.length > 0) {
1109
- bytesCut = true;
1110
- break;
1111
- }
1112
- truncatedSlice.push(line);
1113
- bytesUsed += lineBytes;
1114
- if (bytesUsed >= maxBytesN) break;
1115
- }
1116
- if (truncatedSlice.length < slice.length) bytesCut = true;
1117
- slice = truncatedSlice;
1118
- }
1119
- let midLineCut = false;
1120
- if (maxBytesN > 0 && slice.length > 0) {
1121
- if (Buffer.byteLength(slice.join("\n")) > maxBytesN) {
1122
- const lastIdx = slice.length - 1;
1123
- const lastLine = slice[lastIdx];
1124
- const otherBytes = lastIdx > 0 ? Buffer.byteLength(slice.slice(0, lastIdx).join("\n")) + 1 : 0;
1125
- const budgetForLast = Math.max(0, maxBytesN - otherBytes);
1126
- let cut = Math.min(lastLine.length, budgetForLast);
1127
- while (cut > 0 && Buffer.byteLength(lastLine.slice(0, cut)) > budgetForLast) cut--;
1128
- slice[lastIdx] = lastLine.slice(0, cut);
1129
- midLineCut = true;
1130
- bytesCut = true;
1131
- }
1132
- }
1133
- const lastLineRead = startIdx + slice.length;
1134
- const body = showLineNumbers ? slice.map((line, i) => `${startIdx + i + 1}\t${line}`).join("\n") : slice.join("\n");
1135
- rememberRead();
1136
- const linesTruncated = endIdx < totalLines || bytesCut;
1137
- if (!linesTruncated && offsetN === 1) return body;
1138
- if (!linesTruncated) return `${body}\n\n…read lines ${offsetN}-${lastLineRead} of ${totalLines}.`;
1139
- if (midLineCut) return `${body}\n\n…truncated mid-line at line ${lastLineRead} (byte cap ${maxBytesN} reached). File has ${totalLines} lines, ${totalBytes} bytes total. Raise maxBytes to read the full line.`;
1140
- return `${body}\n\n…truncated at line ${lastLineRead} (${bytesCut ? `byte cap (${maxBytesN}) reached` : `line limit (${limitN}) reached`}). File has ${totalLines} lines, ${totalBytes} bytes total — re-read with offset=${lastLineRead + 1} to continue.`;
1141
- }
1142
- };
1143
- function documentMediaTypeFor(path) {
1144
- const dot = path.lastIndexOf(".");
1145
- if (dot === -1) return void 0;
1146
- return path.slice(dot + 1).toLowerCase() === "pdf" ? "application/pdf" : void 0;
1147
- }
1148
- function fileNameForPath(path) {
1149
- const normalized = path.replace(/\\/g, "/");
1150
- const slash = normalized.lastIndexOf("/");
1151
- return slash === -1 ? normalized : normalized.slice(slash + 1);
1152
- }
1153
- function isPdfBase64(base64) {
1154
- return Buffer.from(base64.slice(0, 16), "base64").toString("ascii").startsWith("%PDF-");
1155
- }
1156
- //#endregion
1157
- //#region src/tools/shell-kill.ts
1158
- const shellKill = {
1159
- isConcurrencySafe: true,
1160
- spec: {
1161
- name: "shell_kill",
1162
- description: [
1163
- "Terminate a running background task started by `shell({ run_in_background: true })`.",
1164
- "Sends SIGTERM to the whole process group so the shell wrapper AND its child commands die together.",
1165
- "Returns the final exit info (status, exit code, output path) or a \"no such task\" message when the id is unknown / already cleaned up.",
1166
- "Idempotent — calling on a task that has already terminated returns the cached exit info without re-killing."
1167
- ].join("\n"),
1168
- inputSchema: {
1169
- type: "object",
1170
- properties: { task_id: {
1171
- type: "string",
1172
- description: "The task id returned by a prior `shell({ run_in_background: true })` call."
1173
- } },
1174
- required: ["task_id"],
1175
- additionalProperties: false
1176
- }
1177
- },
1178
- async execute(input, ctx) {
1179
- const taskId = input.task_id;
1180
- if (!ctx.execution.killBackground) return `shell_kill error: the active execution context (${ctx.execution.type}) does not support background tasks.`;
1181
- const info = await ctx.execution.killBackground(ctx.handle, taskId);
1182
- if (!info) return `shell_kill: no such task "${taskId}". It may have already exited and been cleaned up, or it was never started in this session.`;
1183
- return [
1184
- `Killed ${info.taskId} — ${formatTaskStatus(info)} after ${formatDuration(info.durationMs)}.`,
1185
- ` command: ${previewLine(info.command, 60)}`,
1186
- ` output: ${info.outputPath}`
1187
- ].join("\n");
1188
- }
1189
- };
1190
- //#endregion
1191
- //#region src/tools/spawn.ts
1192
- const BUBBLED_EVENTS = [
1193
- "stream:text",
1194
- "stream:thinking",
1195
- "stream:end",
1196
- "stream:error",
1197
- "stream:server_tool_use",
1198
- "stream:server_tool_result",
1199
- "tool:dispatched",
1200
- "tool:before",
1201
- "tool:after",
1202
- "tool:error",
1203
- "tool:cancelled",
1204
- "background:start",
1205
- "background:exit",
1206
- "background:reassign",
1207
- "turn:after"
1208
- ];
1209
- const BUBBLED_MUTABLE_EVENTS = [
1210
- "content-ref:resolve",
1211
- "tool:gate",
1212
- "mcp:tool:gate",
1213
- "tool:transform"
1214
- ];
1215
- const CHILD_EVENT_NAME = {
1216
- "stream:text": "child:stream:text",
1217
- "stream:thinking": "child:stream:thinking",
1218
- "stream:end": "child:stream:end",
1219
- "stream:error": "child:stream:error",
1220
- "stream:server_tool_use": "child:stream:server_tool_use",
1221
- "stream:server_tool_result": "child:stream:server_tool_result",
1222
- "tool:dispatched": "child:tool:dispatched",
1223
- "tool:before": "child:tool:before",
1224
- "tool:after": "child:tool:after",
1225
- "tool:error": "child:tool:error",
1226
- "tool:cancelled": "child:tool:cancelled",
1227
- "background:start": "child:background:start",
1228
- "background:exit": "child:background:exit",
1229
- "background:reassign": "child:background:reassign",
1230
- "turn:after": "child:turn:after"
1231
- };
1232
- const CHILD_MUTABLE_EVENT_NAME = {
1233
- "content-ref:resolve": "content-ref:resolve",
1234
- "tool:gate": "child:tool:gate",
1235
- "mcp:tool:gate": "child:mcp:tool:gate",
1236
- "tool:transform": "child:tool:transform"
1237
- };
1238
- const sessionChildCounters = /* @__PURE__ */ new WeakMap();
1239
- function reserveChildLabel(session) {
1240
- let counter = sessionChildCounters.get(session);
1241
- if (counter === void 0) counter = session.runs.filter((r) => (r.depth ?? 0) > 0).length;
1242
- counter += 1;
1243
- sessionChildCounters.set(session, counter);
1244
- return `child-${counter}`;
1245
- }
1246
- function extractText(message) {
1247
- if (!message || typeof message !== "object") return "";
1248
- const msg = message;
1249
- if (typeof msg.content === "string") return msg.content;
1250
- if (Array.isArray(msg.content)) return msg.content.filter((block) => !!block && typeof block === "object" && block.type === "text").map((block) => block.text).join("\n");
1251
- return "";
1252
- }
1253
- /**
1254
- * Read-only tool whitelist applied when a subagent preset has
1255
- * `readonly: true` and no explicit `tools` list. Intentionally narrow —
1256
- * only obviously non-mutating built-ins. Hosts wanting a different
1257
- * read-only profile (e.g. include `glob` for a code-search agent) should
1258
- * pass an explicit `tools` array on the subagent def.
1259
- */
1260
- const READONLY_TOOL_DEFAULTS = [
1261
- "read_file",
1262
- "grep",
1263
- "glob",
1264
- "list_files"
1265
- ];
1266
- /**
1267
- * Apply a subagent preset's tool filter to the parent's tool registry.
1268
- * Always a strict subset of the input — the child agent never gains
1269
- * tools the parent doesn't have. Names that don't match a parent tool
1270
- * are silently dropped (matches MCP's lenient `enabledTools` behavior).
1271
- *
1272
- * Precedence: `def.tools` (explicit list) > `def.readonly: true`
1273
- * (built-in read-only set) > unfiltered.
1274
- */
1275
- function filterToolsForSubagent(parentTools, def) {
1276
- if (!parentTools) return parentTools;
1277
- const explicit = def.tools;
1278
- if (explicit && explicit.length > 0) {
1279
- const wanted = new Set(explicit);
1280
- const filtered = {};
1281
- for (const [registryKey, t] of Object.entries(parentTools)) if (wanted.has(t.spec.name)) filtered[registryKey] = t;
1282
- return filtered;
1283
- }
1284
- if (def.readonly) {
1285
- const wanted = new Set(READONLY_TOOL_DEFAULTS);
1286
- const filtered = {};
1287
- for (const [registryKey, t] of Object.entries(parentTools)) if (wanted.has(t.spec.name)) filtered[registryKey] = t;
1288
- return filtered;
1289
- }
1290
- return parentTools;
1291
- }
1292
- /**
1293
- * Render the per-type descriptions into a single schema-field
1294
- * description string the model reads when choosing a `subagent_type`.
1295
- * Falls back to a generic line when the host didn't supply any per-type
1296
- * descriptions.
1297
- */
1298
- function buildSubagentTypeDescription(registry) {
1299
- const lines = [];
1300
- let hasAny = false;
1301
- for (const [key, def] of Object.entries(registry)) if (def.description) {
1302
- lines.push(`- "${key}": ${def.description}`);
1303
- hasAny = true;
1304
- } else lines.push(`- "${key}"`);
1305
- lines.push("- \"general-purpose\": no specialization; uses the spawn tool's default config.");
1306
- if (!hasAny) return `Optional subagent preset. One of: ${lines.map((l) => l.replace(/^- /, "").replace(/:.*$/, "")).join(", ")}.`;
1307
- return `Optional subagent preset that overlays the spawn tool's defaults.\n${lines.join("\n")}`;
1308
- }
1309
- /**
1310
- * Race `task` (an already-running child `agent.run()` promise) against a
1311
- * timer. Does NOT race against the parent abort signal — the child agent
1312
- * already observes the same signal internally and handles its own aborted
1313
- * bookkeeping, so racing here would detach the spawn from the child's
1314
- * session-persisting finally block.
1315
- *
1316
- * On timeout: rejects with `SpawnTimeoutError`; caller is expected to call
1317
- * `agent.abort()` and subsequently `await` the original `task` so the
1318
- * child's session state (runs, turns, status) gets flushed before the
1319
- * parent moves on.
1320
- */
1321
- async function raceWithTimeout(task, timeoutMs) {
1322
- if (!timeoutMs || timeoutMs <= 0) return task;
1323
- let timer;
1324
- try {
1325
- return await new Promise((resolve, reject) => {
1326
- timer = setTimeout(() => reject(new SpawnTimeoutError(timeoutMs)), timeoutMs);
1327
- task.then(resolve, reject);
1328
- });
1329
- } finally {
1330
- if (timer) clearTimeout(timer);
1331
- }
1332
- }
1333
- var SpawnTimeoutError = class extends Error {
1334
- timeoutMs;
1335
- constructor(timeoutMs) {
1336
- super(`Child agent timed out after ${timeoutMs}ms`);
1337
- this.name = "SpawnTimeoutError";
1338
- this.timeoutMs = timeoutMs;
1339
- }
1340
- };
1341
- /**
1342
- * Wire child's hooks to bubble into `parentHooks` as `child:*` events.
1343
- *
1344
- * Three kinds of forwarding:
1345
- *
1346
- * 1. **Originating observational events** (`stream:text`, `tool:before`, …)
1347
- * → rewrite to the matching `child:*` event, inject `{ childId, depth }`,
1348
- * fire on the parent's hook bus as a **spread copy** (so parent listeners
1349
- * can't accidentally mutate the child's ctx).
1350
- * 2. **Originating gate events** (`tool:gate`, `mcp:tool:gate`) → forward
1351
- * the **same ctx reference**, augmented with `childId` / `depth`, so a
1352
- * parent listener writing `ctx.block = true` lands on the gate the
1353
- * child's loop is awaiting on. The bubble itself `await`s the parent's
1354
- * callHook so any async approval (e.g. a TUI picker) completes before
1355
- * the child's loop sees the decision.
1356
- * 3. **Re-bubbled `child:*` events** from a grandchild already carry the
1357
- * originating `childId` + `depth`. Forward verbatim so a top-level
1358
- * listener sees true ancestry, not the immediate parent's.
1359
- *
1360
- * Returns a function that unregisters every listener registered here.
1361
- * Called before `agent.run()` starts, torn down in a finally block — so
1362
- * nothing leaks even if the child throws mid-run.
1363
- */
1364
- /**
1365
- * Surface a thrown observational-bubble listener without killing the
1366
- * process. Observational events (`child:stream:text`, `child:tool:after`,
1367
- * `child:turn:after`, …) are fire-and-forget — a rejected promise here
1368
- * has no caller. Without this handler, a buggy parent listener bubbles
1369
- * up to an unhandled-rejection, and Node/Bun under
1370
- * `--unhandled-rejections=strict` terminates the host.
1371
- *
1372
- * Gated on `ZIDANE_DEBUG` so production logs stay quiet; debug builds
1373
- * still get the tagged line.
1374
- */
1375
- function swallowBubbleError(eventName, err) {
1376
- if (!process.env.ZIDANE_DEBUG) return;
1377
- const message = err instanceof Error ? err.stack ?? err.message : String(err);
1378
- process.stderr.write(`[zidane/spawn] parent listener for "${eventName}" rejected: ${message}\n`);
1379
- }
1380
- function bubbleHooks(childHooks, parentHooks, childId, depth) {
1381
- const unregisters = [];
1382
- const fire = parentHooks.callHook;
1383
- for (const evt of BUBBLED_EVENTS) {
1384
- const parentEvt = CHILD_EVENT_NAME[evt];
1385
- const unregister = childHooks.hook(evt, (ctx) => {
1386
- Promise.resolve(fire(parentEvt, {
1387
- ...ctx,
1388
- childId,
1389
- depth
1390
- })).catch((err) => swallowBubbleError(parentEvt, err));
1391
- });
1392
- unregisters.push(unregister);
1393
- }
1394
- const tagOnCtx = (ctx) => {
1395
- ctx.childId ??= childId;
1396
- ctx.depth ??= depth;
1397
- };
1398
- for (const evt of BUBBLED_MUTABLE_EVENTS) {
1399
- const parentEvt = CHILD_MUTABLE_EVENT_NAME[evt];
1400
- const unregister = childHooks.hook(evt, async (ctx) => {
1401
- tagOnCtx(ctx);
1402
- await fire(parentEvt, ctx);
1403
- });
1404
- unregisters.push(unregister);
1405
- }
1406
- const chainHook = childHooks.hook;
1407
- for (const evt of BUBBLED_EVENTS) {
1408
- const parentEvt = CHILD_EVENT_NAME[evt];
1409
- unregisters.push(chainHook(parentEvt, (ctx) => {
1410
- Promise.resolve(fire(parentEvt, ctx)).catch((err) => swallowBubbleError(parentEvt, err));
1411
- }));
1412
- }
1413
- for (const evt of BUBBLED_MUTABLE_EVENTS) {
1414
- const parentEvt = CHILD_MUTABLE_EVENT_NAME[evt];
1415
- if (parentEvt === evt) continue;
1416
- unregisters.push(chainHook(parentEvt, async (ctx) => {
1417
- await fire(parentEvt, ctx);
1418
- }));
1419
- }
1420
- return () => {
1421
- for (const u of unregisters) u();
1422
- };
1423
- }
1424
- /**
1425
- * Create a configured spawn tool.
1426
- *
1427
- * State (`children`, `totalChildStats`, counters, active count) is scoped to
1428
- * the returned instance. Multiple parent agents using the same instance will
1429
- * share counters + stats + concurrency slots — call `createSpawnTool()` per
1430
- * agent (or use the stateless default `spawn`) to keep them isolated.
1431
- */
1432
- function createSpawnTool(options = {}) {
1433
- const localChildren = /* @__PURE__ */ new Map();
1434
- let localCounter = 0;
1435
- let localActiveCount = 0;
1436
- const maxConcurrent = options.maxConcurrent ?? 3;
1437
- const maxDepth = options.maxDepth ?? 3;
1438
- const forwardHooks = options.forwardHooks ?? true;
1439
- const localStats = {
1440
- totalIn: 0,
1441
- totalOut: 0,
1442
- totalCacheRead: 0,
1443
- totalCacheCreation: 0,
1444
- turns: 0,
1445
- elapsed: 0
1446
- };
1447
- const subagentRegistry = options.subagents;
1448
- const subagentTypeKeys = !!subagentRegistry && Object.keys(subagentRegistry).length > 0 ? [...new Set([...Object.keys(subagentRegistry), "general-purpose"])] : [];
1449
- return {
1450
- get children() {
1451
- return localChildren;
1452
- },
1453
- get totalChildStats() {
1454
- return { ...localStats };
1455
- },
1456
- isConcurrencySafe: true,
1457
- spec: {
1458
- name: "spawn",
1459
- description: "Spawn a sub-agent for a self-contained task that benefits from isolation (separate context window, separate retries) — for example, a deep research dive or a long codegen pass on a specific file. The sub-agent runs independently with its own tool access and returns its final response. Do NOT spawn for sequential steps you could do yourself.",
1460
- inputSchema: {
1461
- type: "object",
1462
- properties: {
1463
- task: {
1464
- type: "string",
1465
- description: "The task prompt for the sub-agent. Be specific about what you want it to accomplish."
1466
- },
1467
- system: {
1468
- type: "string",
1469
- description: "Optional system prompt override for this specific sub-agent."
1470
- },
1471
- ...subagentTypeKeys.length > 0 ? { subagent_type: {
1472
- type: "string",
1473
- enum: subagentTypeKeys,
1474
- description: buildSubagentTypeDescription(subagentRegistry)
1475
- } } : {}
1476
- },
1477
- required: ["task"]
1478
- }
1479
- },
1480
- async execute(input, ctx) {
1481
- const task = input.task;
1482
- const systemOverride = input.system;
1483
- const requestedSubagentType = typeof input.subagent_type === "string" ? input.subagent_type : void 0;
1484
- const subagentDef = requestedSubagentType && subagentRegistry ? subagentRegistry[requestedSubagentType] ?? void 0 : void 0;
1485
- const parentDepth = ctx.depth ?? 0;
1486
- const childDepth = parentDepth + 1;
1487
- if (childDepth > maxDepth) return `Cannot spawn: maxDepth=${maxDepth} reached (parent depth=${parentDepth}). Deepen the cap with createSpawnTool({ maxDepth }).`;
1488
- if (localActiveCount >= maxConcurrent) return `Cannot spawn: ${localActiveCount}/${maxConcurrent} sub-agents already running. Wait for one to complete.`;
1489
- if (ctx.signal.aborted) return `[sub-agent pre-aborted] Parent signal was already aborted — skipped "${task.slice(0, 80)}"`;
1490
- const id = ctx.session ? reserveChildLabel(ctx.session) : `child-${++localCounter}`;
1491
- localActiveCount++;
1492
- const child = {
1493
- id,
1494
- task,
1495
- startedAt: Date.now(),
1496
- depth: childDepth
1497
- };
1498
- localChildren.set(id, child);
1499
- let destroyError;
1500
- let childRunStatus = "completed";
1501
- let finalStats;
1502
- let result = "";
1503
- let unbubble;
1504
- try {
1505
- const filteredTools = subagentDef ? filterToolsForSubagent(ctx.tools, subagentDef) : ctx.tools;
1506
- const parentPreset = {
1507
- ...ctx.name !== void 0 ? { name: ctx.name } : {},
1508
- ...ctx.system !== void 0 ? { system: ctx.system } : {},
1509
- tools: filteredTools,
1510
- ...ctx.toolAliases !== void 0 ? { toolAliases: ctx.toolAliases } : {},
1511
- ...ctx.mcpServers !== void 0 ? { mcpServers: ctx.mcpServers } : {},
1512
- ...ctx.skills !== void 0 ? { skills: ctx.skills } : {},
1513
- ...ctx.behavior !== void 0 ? { behavior: ctx.behavior } : {}
1514
- };
1515
- const sharedReadState = options.shareReadState ? resolveReadStateMap(ctx) : void 0;
1516
- const agent = createAgent({
1517
- ...parentPreset,
1518
- ...options.preset,
1519
- provider: ctx.provider,
1520
- execution: ctx.execution,
1521
- ...options.persist && ctx.session ? { session: ctx.session } : {},
1522
- ...sharedReadState ? { readState: sharedReadState } : {}
1523
- });
1524
- if (forwardHooks) {
1525
- const unregisterEnricher = agent.hooks.hook("tool:before", async (toolCtx) => {
1526
- if (toolCtx.name !== "write_file" && toolCtx.name !== "edit" && toolCtx.name !== "multi_edit") return;
1527
- if (!agent.handle) return;
1528
- const inputPath = toolCtx.input?.path;
1529
- if (typeof inputPath !== "string") return;
1530
- try {
1531
- toolCtx.priorContent = await agent.execution.readFile(agent.handle, inputPath);
1532
- } catch {}
1533
- });
1534
- const unbubbleInner = bubbleHooks(agent.hooks, ctx.hooks, id, childDepth);
1535
- unbubble = () => {
1536
- unregisterEnricher();
1537
- unbubbleInner();
1538
- };
1539
- }
1540
- options.onSpawn?.(child);
1541
- const spawnHookCtx = {
1542
- id,
1543
- task,
1544
- depth: childDepth,
1545
- tracingContext: {}
1546
- };
1547
- await ctx.hooks.callHook("spawn:before", spawnHookCtx);
1548
- const propagatedTracing = Object.keys(spawnHookCtx.tracingContext).length > 0 ? Object.freeze({ ...spawnHookCtx.tracingContext }) : void 0;
1549
- const effectiveSystem = systemOverride ?? subagentDef?.system ?? options.system;
1550
- const runPromise = agent.run({
1551
- prompt: task,
1552
- model: options.model ?? ctx.model,
1553
- system: effectiveSystem,
1554
- thinking: options.thinking,
1555
- signal: ctx.signal,
1556
- depth: childDepth,
1557
- ...options.persist && ctx.runId ? { parentRunId: ctx.runId } : {},
1558
- ...propagatedTracing ? { tracingContext: propagatedTracing } : {}
1559
- });
1560
- try {
1561
- finalStats = await raceWithTimeout(runPromise, options.timeoutMs);
1562
- const treeTurns = flattenTurns(finalStats).length;
1563
- const usage = formatTokenUsage(finalStats);
1564
- if (ctx.signal.aborted) {
1565
- childRunStatus = "aborted";
1566
- result = [`[sub-agent ${id}] Aborted after ${treeTurns} turns (${finalStats.elapsed}ms)`, `Tokens: ${usage}`].join("\n");
1567
- } else {
1568
- const response = extractText(agent.turns.at(-1));
1569
- result = [
1570
- `[sub-agent ${id}] Completed in ${treeTurns} turns (${finalStats.elapsed}ms)`,
1571
- `Tokens: ${usage}`,
1572
- "",
1573
- response || "(no text response)"
1574
- ].join("\n");
1575
- }
1576
- } catch (err) {
1577
- if (err instanceof SpawnTimeoutError) {
1578
- childRunStatus = "timeout";
1579
- agent.abort();
1580
- try {
1581
- finalStats = await runPromise;
1582
- } catch {
1583
- finalStats = {
1584
- totalIn: 0,
1585
- totalOut: 0,
1586
- totalCacheRead: 0,
1587
- totalCacheCreation: 0,
1588
- turns: 0,
1589
- elapsed: err.timeoutMs
1590
- };
1591
- }
1592
- result = `[sub-agent ${id}] Timed out after ${err.timeoutMs}ms`;
1593
- } else {
1594
- const error = err instanceof Error ? err : new Error(String(err));
1595
- childRunStatus = "error";
1596
- finalStats = {
1597
- totalIn: 0,
1598
- totalOut: 0,
1599
- totalCacheRead: 0,
1600
- totalCacheCreation: 0,
1601
- turns: 0,
1602
- elapsed: 0
1603
- };
1604
- result = `[sub-agent ${id}] Error: ${error.message}`;
1605
- await ctx.hooks.callHook("spawn:error", {
1606
- id,
1607
- task,
1608
- depth: childDepth,
1609
- error
1610
- });
1611
- }
1612
- } finally {
1613
- const childHandle = agent.handle;
1614
- if (childHandle && ctx.execution.reassignBackgroundTasks) try {
1615
- const reassigned = await ctx.execution.reassignBackgroundTasks(childHandle, ctx.handle, (info) => {
1616
- Promise.resolve(ctx.hooks.callHook("background:exit", info)).catch(() => {});
1617
- });
1618
- for (const entry of reassigned) await ctx.hooks.callHook("background:reassign", {
1619
- taskId: entry.taskId,
1620
- fromHandleId: childHandle.id,
1621
- toHandleId: ctx.handle.id,
1622
- childId: id,
1623
- pid: entry.pid,
1624
- command: entry.command,
1625
- outputPath: entry.outputPath,
1626
- startedAt: entry.startedAt
1627
- });
1628
- } catch (err) {
1629
- if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/spawn] reassignBackgroundTasks failed: ${err instanceof Error ? err.message : String(err)}\n`);
1630
- }
1631
- try {
1632
- await agent.destroy();
1633
- } catch (err) {
1634
- destroyError = err instanceof Error ? err : new Error(String(err));
1635
- }
1636
- }
1637
- if (finalStats) {
1638
- localStats.totalIn += finalStats.totalIn;
1639
- localStats.totalOut += finalStats.totalOut;
1640
- localStats.totalCacheRead += finalStats.totalCacheRead;
1641
- localStats.totalCacheCreation += finalStats.totalCacheCreation;
1642
- localStats.turns += finalStats.turns;
1643
- localStats.elapsed += finalStats.elapsed;
1644
- }
1645
- const childRunStats = {
1646
- id,
1647
- task,
1648
- stats: finalStats,
1649
- depth: childDepth,
1650
- status: childRunStatus,
1651
- ...finalStats.output ? { output: finalStats.output } : {}
1652
- };
1653
- options.onComplete?.(child, finalStats, childRunStatus);
1654
- await ctx.hooks.callHook("spawn:complete", childRunStats);
1655
- if (destroyError) await ctx.hooks.callHook("spawn:error", {
1656
- id,
1657
- task,
1658
- depth: childDepth,
1659
- error: destroyError
1660
- });
1661
- return result;
1662
- } finally {
1663
- unbubble?.();
1664
- localActiveCount--;
1665
- localChildren.delete(id);
1666
- }
1667
- }
1668
- };
1669
- }
1670
- //#endregion
1671
- //#region src/tools/write-file.ts
1672
- /**
1673
- * Write a file, with an idempotency signal when the content is unchanged.
1674
- *
1675
- * Three return shapes — chosen so the model can recognize a no-op without a
1676
- * separate read:
1677
- * - `Created path (N bytes)` — file did not exist
1678
- * - `Updated path (N bytes)` — content differed from on-disk
1679
- * - `No change needed: path already at target state (N bytes)` — equal
1680
- *
1681
- * Race window: in non-process execution contexts (docker, sandbox) shared by
1682
- * multiple agents, another writer can mutate the file between our read and
1683
- * our write. Local process context is single-writer per agent so the race is
1684
- * a non-issue there. Documented rather than locked because the cost of
1685
- * cross-context locking outweighs the cost of a stale "No change" message.
1686
- */
1687
- const writeFile = {
1688
- spec: {
1689
- name: "write_file",
1690
- description: "Write `content` to `path`, creating any missing parent directories. Overwrites existing files in full; prefer `edit` / `multi_edit` for surgical changes when you only want to alter part of a file. Returns one of: `Created <path> (N bytes)` (file did not exist), `Updated <path> (N bytes)` (content differed), or `No change needed: <path> already at target state (N bytes)` — so the model can detect no-ops without a separate `read_file`.",
1691
- inputSchema: {
1692
- type: "object",
1693
- properties: {
1694
- path: {
1695
- type: "string",
1696
- description: "File path (relative to the execution-context cwd, or absolute)."
1697
- },
1698
- content: {
1699
- type: "string",
1700
- description: "Complete file content. Overwrites any existing file at `path`."
1701
- }
1702
- },
1703
- required: ["path", "content"]
1704
- }
1705
- },
1706
- async execute({ path, content }, ctx) {
1707
- const targetPath = path;
1708
- const targetContent = content;
1709
- const pathErr = pathArgError(targetPath);
1710
- if (pathErr) {
1711
- ctx.reportOutcome?.("failed");
1712
- return `Write error: ${pathErr}`;
1713
- }
1714
- let existing;
1715
- try {
1716
- existing = await ctx.execution.readFile(ctx.handle, targetPath);
1717
- } catch {}
1718
- const bytes = Buffer.byteLength(targetContent);
1719
- if (existing === targetContent) {
1720
- ctx.reportOutcome?.("noop");
1721
- return `No change needed: ${targetPath} already at target state (${bytes} bytes).`;
1722
- }
1723
- await ctx.execution.writeFile(ctx.handle, targetPath, targetContent);
1724
- const readState = resolveReadStateMap(ctx);
1725
- if (readState) readState.set(readStateKey(ctx.handle.cwd, targetPath), {
1726
- contentHash: hashContent(targetContent),
1727
- offset: 0,
1728
- limit: Number.POSITIVE_INFINITY,
1729
- maxBytes: Number.POSITIVE_INFINITY,
1730
- mtimeMs: Date.now()
1731
- });
1732
- ctx.reportOutcome?.(existing === void 0 ? "created" : "updated");
1733
- return existing === void 0 ? `Created ${targetPath} (${bytes} bytes).` : `Updated ${targetPath} (${bytes} bytes).`;
1734
- }
1735
- };
1736
- //#endregion
1737
- export { multiEdit as a, grep as c, readFile as i, glob as l, createSpawnTool as n, listFiles as o, shellKill as r, createInteractionTool as s, writeFile as t, edit as u };
1738
-
1739
- //# sourceMappingURL=tools-CCd8e-HT.js.map