@carboncode/cli 0.1.0 → 0.1.2

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 (99) hide show
  1. package/README.md +15 -24
  2. package/README.zh-CN.md +13 -11
  3. package/dist/cli/{acp-35C4ME6Y.js → acp-6J54TVVC.js} +17 -16
  4. package/dist/cli/acp-6J54TVVC.js.map +1 -0
  5. package/dist/cli/{chat-A6UJDPGV.js → chat-636MFZ7W.js} +21 -21
  6. package/dist/cli/{chunk-JKGYMRX5.js → chunk-3N7FTZVE.js} +2 -2
  7. package/dist/cli/{chunk-4TVNJWMA.js → chunk-ACHQFKW2.js} +178 -18
  8. package/dist/cli/chunk-ACHQFKW2.js.map +1 -0
  9. package/dist/cli/{chunk-7L2WTRNU.js → chunk-ANVEA3RU.js} +2 -2
  10. package/dist/cli/{chunk-QVC75MR3.js → chunk-BXMMGFAL.js} +2 -2
  11. package/dist/cli/{chunk-UI66BH6D.js → chunk-COTWTQQZ.js} +2 -2
  12. package/dist/cli/{chunk-J5BYPUB5.js → chunk-CZCPIK5K.js} +1508 -1176
  13. package/dist/cli/chunk-CZCPIK5K.js.map +1 -0
  14. package/dist/cli/{chunk-XJ5SRLKK.js → chunk-D3ACJ6D5.js} +2 -2
  15. package/dist/cli/{chunk-WRN65TRD.js → chunk-DSQNSP7F.js} +2 -2
  16. package/dist/cli/{chunk-QJG7OF27.js → chunk-FKSYTVWZ.js} +27 -10
  17. package/dist/cli/chunk-FKSYTVWZ.js.map +1 -0
  18. package/dist/cli/{chunk-BSINVTTL.js → chunk-FXG7CSGY.js} +7 -7
  19. package/dist/cli/{chunk-4MQ3VURH.js → chunk-K43DXH3G.js} +52 -83
  20. package/dist/cli/chunk-K43DXH3G.js.map +1 -0
  21. package/dist/cli/{chunk-BSGCXZQN.js → chunk-LNU3CR7X.js} +2 -2
  22. package/dist/cli/{chunk-TH756VLN.js → chunk-MXUSER5C.js} +240 -191
  23. package/dist/cli/chunk-MXUSER5C.js.map +1 -0
  24. package/dist/cli/{chunk-3T6VBZCL.js → chunk-NQJYZKEU.js} +2 -2
  25. package/dist/cli/{chunk-IX6XI2RG.js → chunk-OB5XR5HG.js} +2 -2
  26. package/dist/cli/{chunk-ILJOIQ5W.js → chunk-OY5GGU6D.js} +2 -2
  27. package/dist/cli/{chunk-IAUOP25G.js → chunk-R677DIFU.js} +38 -22
  28. package/dist/cli/chunk-R677DIFU.js.map +1 -0
  29. package/dist/cli/{chunk-CPKCNHRR.js → chunk-RSQMO6CF.js} +5 -5
  30. package/dist/cli/{chunk-3OAR6NVL.js → chunk-RUPXIRNL.js} +2 -2
  31. package/dist/cli/{chunk-S2KIUQKQ.js → chunk-S4YD3N3X.js} +7 -6
  32. package/dist/cli/{chunk-S2KIUQKQ.js.map → chunk-S4YD3N3X.js.map} +1 -1
  33. package/dist/cli/{chunk-4IBIPQVB.js → chunk-T6SBUSG2.js} +3 -3
  34. package/dist/cli/{chunk-D5NFKRGO.js → chunk-UGPC4LPM.js} +2 -2
  35. package/dist/cli/{chunk-T5TQ4NDT.js → chunk-X4UJ6Q6M.js} +3 -3
  36. package/dist/cli/{code-4TUTAGO5.js → code-TBC3K5AZ.js} +24 -33
  37. package/dist/cli/code-TBC3K5AZ.js.map +1 -0
  38. package/dist/cli/{commands-KMOZEYCF.js → commands-HMQPRVNT.js} +4 -4
  39. package/dist/cli/{commit-DTFA56VQ.js → commit-WIY4B3X4.js} +3 -3
  40. package/dist/cli/{desktop-7N3MHNBD.js → desktop-MGOG3AWV.js} +17 -17
  41. package/dist/cli/{diff-E5OWTF4C.js → diff-57LRKCB7.js} +8 -8
  42. package/dist/cli/{doctor-IEJQRJMN.js → doctor-5FDRBIXE.js} +8 -8
  43. package/dist/cli/index.js +32 -32
  44. package/dist/cli/{mcp-PDI2PDLG.js → mcp-HJHTNRZF.js} +2 -2
  45. package/dist/cli/{mcp-browse-OSPXOFPZ.js → mcp-browse-C2PJRQBO.js} +2 -2
  46. package/dist/cli/{mcp-inspect-QRFVTHMF.js → mcp-inspect-JBFXV2II.js} +2 -2
  47. package/dist/cli/{prompt-3CDII3UO.js → prompt-U62OVZNY.js} +3 -3
  48. package/dist/cli/{replay-HYOSRQIV.js → replay-M3YKBVAM.js} +8 -8
  49. package/dist/cli/{run-2ZHADOUP.js → run-V6X5GXCR.js} +13 -13
  50. package/dist/cli/{server-X75PAZG5.js → server-5WVJQUOR.js} +10 -10
  51. package/dist/cli/{sessions-POOZA5CQ.js → sessions-B266WVM3.js} +12 -12
  52. package/dist/cli/{setup-YLPFI3OH.js → setup-SWX5E3W2.js} +5 -5
  53. package/dist/cli/{stats-NXJ3TO2D.js → stats-VPPKS6UF.js} +6 -6
  54. package/dist/cli/{version-NXXWE3WN.js → version-TVHAEHWY.js} +12 -12
  55. package/dist/index.d.ts +17 -2
  56. package/dist/index.js +360 -150
  57. package/dist/index.js.map +1 -1
  58. package/package.json +2 -2
  59. package/dist/cli/acp-35C4ME6Y.js.map +0 -1
  60. package/dist/cli/chunk-4MQ3VURH.js.map +0 -1
  61. package/dist/cli/chunk-4TVNJWMA.js.map +0 -1
  62. package/dist/cli/chunk-IAUOP25G.js.map +0 -1
  63. package/dist/cli/chunk-J5BYPUB5.js.map +0 -1
  64. package/dist/cli/chunk-QJG7OF27.js.map +0 -1
  65. package/dist/cli/chunk-TH756VLN.js.map +0 -1
  66. package/dist/cli/code-4TUTAGO5.js.map +0 -1
  67. /package/dist/cli/{chat-A6UJDPGV.js.map → chat-636MFZ7W.js.map} +0 -0
  68. /package/dist/cli/{chunk-JKGYMRX5.js.map → chunk-3N7FTZVE.js.map} +0 -0
  69. /package/dist/cli/{chunk-7L2WTRNU.js.map → chunk-ANVEA3RU.js.map} +0 -0
  70. /package/dist/cli/{chunk-QVC75MR3.js.map → chunk-BXMMGFAL.js.map} +0 -0
  71. /package/dist/cli/{chunk-UI66BH6D.js.map → chunk-COTWTQQZ.js.map} +0 -0
  72. /package/dist/cli/{chunk-XJ5SRLKK.js.map → chunk-D3ACJ6D5.js.map} +0 -0
  73. /package/dist/cli/{chunk-WRN65TRD.js.map → chunk-DSQNSP7F.js.map} +0 -0
  74. /package/dist/cli/{chunk-BSINVTTL.js.map → chunk-FXG7CSGY.js.map} +0 -0
  75. /package/dist/cli/{chunk-BSGCXZQN.js.map → chunk-LNU3CR7X.js.map} +0 -0
  76. /package/dist/cli/{chunk-3T6VBZCL.js.map → chunk-NQJYZKEU.js.map} +0 -0
  77. /package/dist/cli/{chunk-IX6XI2RG.js.map → chunk-OB5XR5HG.js.map} +0 -0
  78. /package/dist/cli/{chunk-ILJOIQ5W.js.map → chunk-OY5GGU6D.js.map} +0 -0
  79. /package/dist/cli/{chunk-CPKCNHRR.js.map → chunk-RSQMO6CF.js.map} +0 -0
  80. /package/dist/cli/{chunk-3OAR6NVL.js.map → chunk-RUPXIRNL.js.map} +0 -0
  81. /package/dist/cli/{chunk-4IBIPQVB.js.map → chunk-T6SBUSG2.js.map} +0 -0
  82. /package/dist/cli/{chunk-D5NFKRGO.js.map → chunk-UGPC4LPM.js.map} +0 -0
  83. /package/dist/cli/{chunk-T5TQ4NDT.js.map → chunk-X4UJ6Q6M.js.map} +0 -0
  84. /package/dist/cli/{commands-KMOZEYCF.js.map → commands-HMQPRVNT.js.map} +0 -0
  85. /package/dist/cli/{commit-DTFA56VQ.js.map → commit-WIY4B3X4.js.map} +0 -0
  86. /package/dist/cli/{desktop-7N3MHNBD.js.map → desktop-MGOG3AWV.js.map} +0 -0
  87. /package/dist/cli/{diff-E5OWTF4C.js.map → diff-57LRKCB7.js.map} +0 -0
  88. /package/dist/cli/{doctor-IEJQRJMN.js.map → doctor-5FDRBIXE.js.map} +0 -0
  89. /package/dist/cli/{mcp-PDI2PDLG.js.map → mcp-HJHTNRZF.js.map} +0 -0
  90. /package/dist/cli/{mcp-browse-OSPXOFPZ.js.map → mcp-browse-C2PJRQBO.js.map} +0 -0
  91. /package/dist/cli/{mcp-inspect-QRFVTHMF.js.map → mcp-inspect-JBFXV2II.js.map} +0 -0
  92. /package/dist/cli/{prompt-3CDII3UO.js.map → prompt-U62OVZNY.js.map} +0 -0
  93. /package/dist/cli/{replay-HYOSRQIV.js.map → replay-M3YKBVAM.js.map} +0 -0
  94. /package/dist/cli/{run-2ZHADOUP.js.map → run-V6X5GXCR.js.map} +0 -0
  95. /package/dist/cli/{server-X75PAZG5.js.map → server-5WVJQUOR.js.map} +0 -0
  96. /package/dist/cli/{sessions-POOZA5CQ.js.map → sessions-B266WVM3.js.map} +0 -0
  97. /package/dist/cli/{setup-YLPFI3OH.js.map → setup-SWX5E3W2.js.map} +0 -0
  98. /package/dist/cli/{stats-NXJ3TO2D.js.map → stats-VPPKS6UF.js.map} +0 -0
  99. /package/dist/cli/{version-NXXWE3WN.js.map → version-TVHAEHWY.js.map} +0 -0
@@ -2,7 +2,7 @@
2
2
  import { createRequire as __cr } from 'node:module'; if (typeof globalThis.require === 'undefined') { globalThis.require = __cr(import.meta.url); }
3
3
  import {
4
4
  addProjectShellAllowed
5
- } from "./chunk-4MQ3VURH.js";
5
+ } from "./chunk-K43DXH3G.js";
6
6
 
7
7
  // src/core/pause-gate.ts
8
8
  var PauseGate = class {
@@ -128,23 +128,211 @@ function safeCancelVerdict(kind) {
128
128
  }
129
129
  var pauseGate = new PauseGate();
130
130
 
131
+ // src/tools/fs/edit.ts
132
+ import { promises as fs } from "fs";
133
+ import * as pathMod from "path";
134
+ function displayRel(rootDir, full) {
135
+ return pathMod.relative(rootDir, full).replaceAll("\\", "/");
136
+ }
137
+ async function applyEdit(rootDir, abs, args) {
138
+ if (args.search.length === 0) {
139
+ throw new Error("edit_file: search cannot be empty");
140
+ }
141
+ const before = await fs.readFile(abs, "utf8");
142
+ const le = before.includes("\r\n") ? "\r\n" : "\n";
143
+ const adaptedSearch = args.search.replace(/\r?\n/g, le);
144
+ const adaptedReplace = args.replace.replace(/\r?\n/g, le);
145
+ const firstIdx = before.indexOf(adaptedSearch);
146
+ if (firstIdx < 0) {
147
+ throw new Error(`edit_file: search text not found in ${displayRel(rootDir, abs)}`);
148
+ }
149
+ const nextIdx = before.indexOf(adaptedSearch, firstIdx + 1);
150
+ if (nextIdx >= 0) {
151
+ throw new Error(
152
+ `edit_file: search text appears multiple times in ${displayRel(rootDir, abs)} \u2014 include more context to disambiguate`
153
+ );
154
+ }
155
+ const after = before.slice(0, firstIdx) + adaptedReplace + before.slice(firstIdx + adaptedSearch.length);
156
+ await fs.writeFile(abs, after, "utf8");
157
+ const rel = displayRel(rootDir, abs);
158
+ const header = `edited ${rel} (${adaptedSearch.length}\u2192${adaptedReplace.length} chars)`;
159
+ const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
160
+ const diff = renderEditDiff(adaptedSearch, adaptedReplace, startLine);
161
+ return `${header}
162
+ ${diff}`;
163
+ }
164
+ async function applyMultiEdit(rootDir, edits) {
165
+ if (edits.length === 0) {
166
+ throw new Error("multi_edit: edits must contain at least one entry");
167
+ }
168
+ const filesByPath = /* @__PURE__ */ new Map();
169
+ for (let i = 0; i < edits.length; i++) {
170
+ const e = edits[i];
171
+ if (typeof e.abs !== "string" || e.abs.length === 0) {
172
+ throw new Error(`multi_edit: edit #${i + 1} requires a string \`path\` (no edits applied)`);
173
+ }
174
+ if (typeof e.search !== "string") {
175
+ throw new Error(`multi_edit: edit #${i + 1} requires a string \`search\` (no edits applied)`);
176
+ }
177
+ if (typeof e.replace !== "string") {
178
+ throw new Error(
179
+ `multi_edit: edit #${i + 1} requires a string \`replace\` (no edits applied)`
180
+ );
181
+ }
182
+ const rel = displayRel(rootDir, e.abs);
183
+ let state = filesByPath.get(e.abs);
184
+ if (!state) {
185
+ let before;
186
+ if (e.search.length === 0) {
187
+ try {
188
+ await fs.readFile(e.abs, "utf8");
189
+ throw new Error("empty SEARCH only creates new files \u2014 this file already exists");
190
+ } catch (err) {
191
+ const code = err.code;
192
+ if (code !== "ENOENT") {
193
+ throw new Error(
194
+ `multi_edit: edit #${i + 1} cannot create ${rel}: ${err.message} (no edits applied)`
195
+ );
196
+ }
197
+ }
198
+ state = { buf: "", le: "\n", hunks: [], deltaChars: 0, touched: 0, created: true };
199
+ filesByPath.set(e.abs, state);
200
+ } else {
201
+ try {
202
+ before = await fs.readFile(e.abs, "utf8");
203
+ } catch (err) {
204
+ throw new Error(
205
+ `multi_edit: edit #${i + 1} cannot read ${rel}: ${err.message} (no edits applied)`
206
+ );
207
+ }
208
+ const le = before.includes("\r\n") ? "\r\n" : "\n";
209
+ state = { buf: before, le, hunks: [], deltaChars: 0, touched: 0, created: false };
210
+ filesByPath.set(e.abs, state);
211
+ }
212
+ }
213
+ if (e.search.length === 0 && (!state.created || state.touched > 0)) {
214
+ throw new Error(
215
+ `multi_edit: edit #${i + 1} (${rel}) empty search only creates new files (no edits applied)`
216
+ );
217
+ }
218
+ const adaptedSearch = e.search.replace(/\r?\n/g, state.le);
219
+ const adaptedReplace = e.replace.replace(/\r?\n/g, state.le);
220
+ if (adaptedSearch.length === 0) {
221
+ state.buf = adaptedReplace;
222
+ state.hunks.push(`# ${rel}
223
+ ${renderCreateDiff(adaptedReplace)}`);
224
+ state.deltaChars += adaptedReplace.length;
225
+ state.touched++;
226
+ continue;
227
+ }
228
+ const firstIdx = state.buf.indexOf(adaptedSearch);
229
+ if (firstIdx < 0) {
230
+ throw new Error(
231
+ `multi_edit: edit #${i + 1} search text not found in ${rel} \u2014 no edits applied (multi_edit is atomic)`
232
+ );
233
+ }
234
+ const nextIdx = state.buf.indexOf(adaptedSearch, firstIdx + 1);
235
+ if (nextIdx >= 0) {
236
+ throw new Error(
237
+ `multi_edit: edit #${i + 1} search text appears multiple times in ${rel} \u2014 include more context to disambiguate (no edits applied)`
238
+ );
239
+ }
240
+ const startLine = state.buf.slice(0, firstIdx).split(/\r?\n/).length;
241
+ state.buf = state.buf.slice(0, firstIdx) + adaptedReplace + state.buf.slice(firstIdx + adaptedSearch.length);
242
+ state.hunks.push(`# ${rel}
243
+ ${renderEditDiff(adaptedSearch, adaptedReplace, startLine)}`);
244
+ state.deltaChars += adaptedReplace.length - adaptedSearch.length;
245
+ state.touched++;
246
+ }
247
+ for (const [abs, state] of filesByPath) {
248
+ if (state.created) await fs.mkdir(pathMod.dirname(abs), { recursive: true });
249
+ await fs.writeFile(abs, state.buf, "utf8");
250
+ }
251
+ const fileCount = filesByPath.size;
252
+ const editCount = edits.length;
253
+ let totalDelta = 0;
254
+ const allHunks = [];
255
+ for (const state of filesByPath.values()) {
256
+ totalDelta += state.deltaChars;
257
+ allHunks.push(...state.hunks);
258
+ }
259
+ const sign = totalDelta >= 0 ? "+" : "";
260
+ const editNoun = editCount === 1 ? "edit" : "edits";
261
+ const fileNoun = fileCount === 1 ? "file" : "files";
262
+ const header = `multi_edit: applied ${editCount} ${editNoun} across ${fileCount} ${fileNoun} (${sign}${totalDelta} chars)`;
263
+ return `${header}
264
+ ${allHunks.join("\n")}`;
265
+ }
266
+ function renderEditDiff(search, replace, startLine) {
267
+ const a = search.split(/\r?\n/);
268
+ const b = replace.split(/\r?\n/);
269
+ const diff = lineDiff(a, b);
270
+ const hunk = `@@ -${startLine},${a.length} +${startLine},${b.length} @@`;
271
+ const body = diff.map((d) => `${d.op === " " ? " " : d.op} ${d.line}`).join("\n");
272
+ return `${hunk}
273
+ ${body}`;
274
+ }
275
+ function renderCreateDiff(replace) {
276
+ const lines = replace.length === 0 ? [] : replace.split(/\r?\n/);
277
+ const hunk = `@@ -1,0 +1,${lines.length} @@`;
278
+ const body = lines.map((line) => `+ ${line}`).join("\n");
279
+ return body ? `${hunk}
280
+ ${body}` : hunk;
281
+ }
282
+ function lineDiff(a, b) {
283
+ const n = a.length;
284
+ const m = b.length;
285
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
286
+ for (let i2 = 1; i2 <= n; i2++) {
287
+ for (let j2 = 1; j2 <= m; j2++) {
288
+ if (a[i2 - 1] === b[j2 - 1]) dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
289
+ else dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
290
+ }
291
+ }
292
+ const out = [];
293
+ let i = n;
294
+ let j = m;
295
+ while (i > 0 && j > 0) {
296
+ if (a[i - 1] === b[j - 1]) {
297
+ out.unshift({ op: " ", line: a[i - 1] });
298
+ i--;
299
+ j--;
300
+ } else if ((dp[i - 1][j] ?? 0) > (dp[i][j - 1] ?? 0)) {
301
+ out.unshift({ op: "-", line: a[i - 1] });
302
+ i--;
303
+ } else {
304
+ out.unshift({ op: "+", line: b[j - 1] });
305
+ j--;
306
+ }
307
+ }
308
+ while (i > 0) {
309
+ out.unshift({ op: "-", line: a[i - 1] });
310
+ i--;
311
+ }
312
+ while (j > 0) {
313
+ out.unshift({ op: "+", line: b[j - 1] });
314
+ j--;
315
+ }
316
+ return out;
317
+ }
318
+
131
319
  // src/tools/jobs.ts
132
320
  import { spawn as spawn3 } from "child_process";
133
- import * as pathMod5 from "path";
321
+ import * as pathMod6 from "path";
134
322
 
135
323
  // src/tools/shell.ts
136
- import * as pathMod4 from "path";
324
+ import * as pathMod5 from "path";
137
325
 
138
326
  // src/tools/shell/exec.ts
139
327
  import { spawn as spawn2, spawnSync } from "child_process";
140
328
  import { existsSync, statSync } from "fs";
141
- import * as pathMod3 from "path";
329
+ import * as pathMod4 from "path";
142
330
 
143
331
  // src/tools/shell-chain.ts
144
332
  import { spawn } from "child_process";
145
333
  import { closeSync, openSync } from "fs";
146
334
  import { devNull } from "os";
147
- import * as pathMod from "path";
335
+ import * as pathMod2 from "path";
148
336
  var UnsupportedSyntaxError = class extends Error {
149
337
  constructor(detail) {
150
338
  super(`run_command: ${detail}`);
@@ -417,7 +605,7 @@ function openRedirects(redirects, cwd) {
417
605
  let bothFd = null;
418
606
  const toClose = [];
419
607
  const open = (target, flags) => {
420
- const resolved = isNullDeviceAlias(target) ? devNull : pathMod.resolve(cwd, target);
608
+ const resolved = isNullDeviceAlias(target) ? devNull : pathMod2.resolve(cwd, target);
421
609
  const fd = openSync(resolved, flags);
422
610
  toClose.push(fd);
423
611
  return fd;
@@ -572,7 +760,7 @@ var OutputBuffer = class {
572
760
 
573
761
  // src/tools/shell/parse.ts
574
762
  import { homedir } from "os";
575
- import * as pathMod2 from "path";
763
+ import * as pathMod3 from "path";
576
764
  var BUILTIN_ALLOWLIST = [
577
765
  // Repo inspection
578
766
  "git status",
@@ -772,16 +960,16 @@ function resolveSensitivePath(token, projectRoot) {
772
960
  return null;
773
961
  let expanded = token;
774
962
  if (expanded.startsWith("~")) {
775
- expanded = pathMod2.join(homedir(), expanded.slice(1));
963
+ expanded = pathMod3.join(homedir(), expanded.slice(1));
776
964
  }
777
- return pathMod2.resolve(projectRoot, expanded);
965
+ return pathMod3.resolve(projectRoot, expanded);
778
966
  }
779
967
  function expandPrefix(prefix) {
780
- if (prefix.startsWith("~")) return pathMod2.join(homedir(), prefix.slice(1));
781
- return pathMod2.resolve(prefix);
968
+ if (prefix.startsWith("~")) return pathMod3.join(homedir(), prefix.slice(1));
969
+ return pathMod3.resolve(prefix);
782
970
  }
783
971
  function pathStartsWithPrefix(normalized, prefix) {
784
- return normalized === prefix || normalized.startsWith(`${prefix}${pathMod2.sep}`);
972
+ return normalized === prefix || normalized.startsWith(`${prefix}${pathMod3.sep}`);
785
973
  }
786
974
  function matchesGlob(name, pattern) {
787
975
  const regex = new RegExp(
@@ -796,18 +984,18 @@ function hasSensitivePathArgs(argv, projectRoot, extraPrefixes = [], extraPatter
796
984
  for (const token of argv) {
797
985
  const resolved = resolveSensitivePath(token, projectRoot);
798
986
  if (!resolved) continue;
799
- const normalized = pathMod2.normalize(resolved);
987
+ const normalized = pathMod3.normalize(resolved);
800
988
  for (const pfx of prefixes) {
801
989
  if (pathStartsWithPrefix(normalized, pfx)) return true;
802
990
  }
803
- const base = pathMod2.basename(normalized);
991
+ const base = pathMod3.basename(normalized);
804
992
  for (const pat of patterns) {
805
993
  if (matchesGlob(base, pat)) return true;
806
994
  }
807
995
  }
808
996
  return false;
809
997
  }
810
- function isAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
998
+ function isAllowed(cmd, extra = [], projectRoot, sensitivePathConfig, opts = {}) {
811
999
  let argv;
812
1000
  try {
813
1001
  argv = tokenizeCommand(cmd);
@@ -815,7 +1003,7 @@ function isAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
815
1003
  return false;
816
1004
  }
817
1005
  if (argv.length === 0) return false;
818
- const allowlist = [...BUILTIN_ALLOWLIST, ...extra];
1006
+ const allowlist = [...opts.includeBuiltin === false ? [] : BUILTIN_ALLOWLIST, ...extra];
819
1007
  for (const prefix of allowlist) {
820
1008
  const prefixTokens = prefix.split(" ");
821
1009
  if (argv.length < prefixTokens.length) continue;
@@ -840,15 +1028,18 @@ function isAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
840
1028
  }
841
1029
  return false;
842
1030
  }
843
- function isCommandAllowed(cmd, extra = [], projectRoot, sensitivePathConfig) {
1031
+ function isCommandAllowed(cmd, extra = [], projectRoot, sensitivePathConfig, opts = {}) {
844
1032
  let chain;
845
1033
  try {
846
1034
  chain = parseCommandChain(cmd);
847
1035
  } catch {
848
1036
  return false;
849
1037
  }
850
- if (chain === null) return isAllowed(cmd, extra, projectRoot, sensitivePathConfig);
851
- return chainAllowed(chain, (seg) => isAllowed(seg, extra, projectRoot, sensitivePathConfig));
1038
+ if (chain === null) return isAllowed(cmd, extra, projectRoot, sensitivePathConfig, opts);
1039
+ return chainAllowed(
1040
+ chain,
1041
+ (seg) => isAllowed(seg, extra, projectRoot, sensitivePathConfig, opts)
1042
+ );
852
1043
  }
853
1044
 
854
1045
  // src/tools/shell/exec.ts
@@ -992,16 +1183,16 @@ function resolveExecutable(cmd, opts = {}) {
992
1183
  const platform = opts.platform ?? process.platform;
993
1184
  if (platform !== "win32") return cmd;
994
1185
  if (!cmd) return cmd;
995
- if (cmd.includes("/") || cmd.includes("\\") || pathMod3.isAbsolute(cmd)) return cmd;
996
- if (pathMod3.extname(cmd)) return cmd;
1186
+ if (cmd.includes("/") || cmd.includes("\\") || pathMod4.isAbsolute(cmd)) return cmd;
1187
+ if (pathMod4.extname(cmd)) return cmd;
997
1188
  const env = opts.env ?? process.env;
998
1189
  const pathExt = (getEnvCaseInsensitive(env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
999
- const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod3.delimiter);
1190
+ const delimiter2 = opts.pathDelimiter ?? (platform === "win32" ? ";" : pathMod4.delimiter);
1000
1191
  const pathDirs = (getEnvCaseInsensitive(env, "PATH") ?? "").split(delimiter2).filter(Boolean);
1001
1192
  const isFile = opts.isFile ?? defaultIsFile;
1002
1193
  for (const dir of pathDirs) {
1003
1194
  for (const ext of pathExt) {
1004
- const full = pathMod3.win32.join(dir, cmd + ext);
1195
+ const full = pathMod4.win32.join(dir, cmd + ext);
1005
1196
  if (isFile(full)) return full;
1006
1197
  }
1007
1198
  }
@@ -1117,8 +1308,8 @@ function withUtf8Codepage(cmdline) {
1117
1308
  function isBareWindowsName(s) {
1118
1309
  if (!s) return false;
1119
1310
  if (s.includes("/") || s.includes("\\")) return false;
1120
- if (pathMod3.isAbsolute(s)) return false;
1121
- if (pathMod3.extname(s)) return false;
1311
+ if (pathMod4.isAbsolute(s)) return false;
1312
+ if (pathMod4.extname(s)) return false;
1122
1313
  return true;
1123
1314
  }
1124
1315
  function quoteForCmdExe(arg) {
@@ -1129,7 +1320,7 @@ function quoteForCmdExe(arg) {
1129
1320
 
1130
1321
  // src/tools/shell.ts
1131
1322
  function registerShellTools(registry, opts) {
1132
- const rootDir = pathMod4.resolve(opts.rootDir);
1323
+ const rootDir = pathMod5.resolve(opts.rootDir);
1133
1324
  const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
1134
1325
  const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
1135
1326
  const jobs = opts.jobs ?? new JobRegistry();
@@ -1138,9 +1329,19 @@ function registerShellTools(registry, opts) {
1138
1329
  return () => snapshot2;
1139
1330
  })();
1140
1331
  const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
1332
+ const isAutoAllowed = (cmd) => isCommandAllowed(cmd, getExtraAllowed(), rootDir, opts.sensitivePaths, {
1333
+ includeBuiltin: opts.requireApprovalForBuiltin !== true
1334
+ });
1335
+ const approvalPolicy = opts.requireApprovalForBuiltin ? "Model-requested shell commands ask the user before they run unless the user has explicitly always-allowed a project prefix or yolo mode is active. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify." : "Allowlisted read-only / test / lint / typecheck commands run immediately; anything that could mutate state, install deps, or touch the network is gated by user confirmation. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.";
1141
1336
  registry.register({
1142
1337
  name: "run_command",
1143
- description: "Run a shell command in the project root; returns combined stdout+stderr. Allowlisted read-only / test / lint / typecheck commands run immediately; anything that could mutate state, install deps, or touch the network is gated by user confirmation. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.\n\nConstraints (no real shell \u2014 argv is parsed natively for cross-platform parity):\n\u2022 Supported: chain ops `|` / `||` / `&&` / `;` (each segment allowlist-checked individually), file redirects `>` / `>>` / `<` / `2>` / `2>>` / `2>&1` / `&>` (target paths resolve relative to project root, max one redirect per fd per segment).\n\u2022 NOT supported: background `&`, heredoc `<<`, command substitution `$(\u2026)`, subshells `(\u2026)`, process substitution `<(\u2026)`, `$VAR` env expansion, glob expansion. To pass an operator char as literal arg, quote it (`grep \"a|b\" file`).\n\u2022 `cd` does NOT persist \u2014 between calls OR within a chain like `cd dir && cmd`. Use the binary's own cwd flag: `npm --prefix <dir>`, `git -C <dir>`, `cargo -C <dir>`, `pytest <dir>/tests`.\n\u2022 Filter at source \u2014 unbounded output (`netstat -ano`, `find /`) wastes tokens. Use `grep -c`, `wc -l`, narrower paths, etc.",
1338
+ description: `Run a shell command in the project root; returns combined stdout+stderr. ${approvalPolicy}
1339
+
1340
+ Constraints (no real shell \u2014 argv is parsed natively for cross-platform parity):
1341
+ \u2022 Supported: chain ops \`|\` / \`||\` / \`&&\` / \`;\` (each segment allowlist-checked individually), file redirects \`>\` / \`>>\` / \`<\` / \`2>\` / \`2>>\` / \`2>&1\` / \`&>\` (target paths resolve relative to project root, max one redirect per fd per segment).
1342
+ \u2022 NOT supported: background \`&\`, heredoc \`<<\`, command substitution \`$(\u2026)\`, subshells \`(\u2026)\`, process substitution \`<(\u2026)\`, \`$VAR\` env expansion, glob expansion. To pass an operator char as literal arg, quote it (\`grep "a|b" file\`).
1343
+ \u2022 \`cd\` does NOT persist \u2014 between calls OR within a chain like \`cd dir && cmd\`. Use the binary's own cwd flag: \`npm --prefix <dir>\`, \`git -C <dir>\`, \`cargo -C <dir>\`, \`pytest <dir>/tests\`.
1344
+ \u2022 Filter at source \u2014 unbounded output (\`netstat -ano\`, \`find /\`) wastes tokens. Use \`grep -c\`, \`wc -l\`, narrower paths, etc.`,
1144
1345
  // Plan-mode gate: allow allowlisted commands through (git status,
1145
1346
  // cargo check, ls, grep …) so the model can actually investigate
1146
1347
  // during planning. Anything that would otherwise trigger a
@@ -1169,12 +1370,14 @@ function registerShellTools(registry, opts) {
1169
1370
  const cmd = args.command.trim();
1170
1371
  if (!cmd) throw new Error("run_command: empty command");
1171
1372
  const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
1172
- if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed(), rootDir, opts.sensitivePaths)) {
1373
+ if (!isAllowAll() && !isAutoAllowed(cmd)) {
1173
1374
  const gate = ctx?.confirmationGate ?? pauseGate;
1375
+ const waitStartedAt = Date.now();
1174
1376
  const choice = await gate.ask({
1175
1377
  kind: "run_command",
1176
1378
  payload: { command: cmd, cwd: rootDir, timeoutSec: effectiveTimeout }
1177
1379
  });
1380
+ ctx?.onInteractiveWait?.(Date.now() - waitStartedAt);
1178
1381
  if (choice.type === "deny") {
1179
1382
  throw new Error(
1180
1383
  `user denied: ${cmd}${choice.denyContext ? ` \u2014 ${choice.denyContext}` : ""}`
@@ -1218,12 +1421,14 @@ function registerShellTools(registry, opts) {
1218
1421
  const cmd = args.command.trim();
1219
1422
  if (!cmd) throw new Error("run_background: empty command");
1220
1423
  const cwd = resolveCwdInsideRoot(rootDir, args.cwd);
1221
- if (!isAllowAll() && !isCommandAllowed(cmd, getExtraAllowed(), rootDir, opts.sensitivePaths)) {
1424
+ if (!isAllowAll() && !isAutoAllowed(cmd)) {
1222
1425
  const gate = ctx?.confirmationGate ?? pauseGate;
1426
+ const waitStartedAt = Date.now();
1223
1427
  const choice = await gate.ask({
1224
1428
  kind: "run_background",
1225
1429
  payload: { command: cmd, cwd, waitSec: args.waitSec }
1226
1430
  });
1431
+ ctx?.onInteractiveWait?.(Date.now() - waitStartedAt);
1227
1432
  if (choice.type === "deny") {
1228
1433
  throw new Error(
1229
1434
  `user denied: ${cmd}${choice.denyContext ? ` \u2014 ${choice.denyContext}` : ""}`
@@ -1342,11 +1547,11 @@ function registerShellTools(registry, opts) {
1342
1547
  return registry;
1343
1548
  }
1344
1549
  function resolveCwdInsideRoot(rootDir, raw) {
1345
- const root = pathMod4.resolve(rootDir);
1550
+ const root = pathMod5.resolve(rootDir);
1346
1551
  if (!raw || !raw.trim()) return root;
1347
- const resolved = pathMod4.resolve(root, raw);
1348
- const rel = pathMod4.relative(root, resolved);
1349
- if (rel.startsWith("..") || pathMod4.isAbsolute(rel)) {
1552
+ const resolved = pathMod5.resolve(root, raw);
1553
+ const rel = pathMod5.relative(root, resolved);
1554
+ if (rel.startsWith("..") || pathMod5.isAbsolute(rel)) {
1350
1555
  throw new Error(
1351
1556
  `run_background: cwd "${raw}" resolves outside the workspace root (${root}). Pass a workspace-relative path.`
1352
1557
  );
@@ -1453,7 +1658,7 @@ var JobRegistry = class {
1453
1658
  const maxBytes = opts.maxBufferBytes ?? DEFAULT_OUTPUT_CAP_BYTES;
1454
1659
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
1455
1660
  const spawnOpts = {
1456
- cwd: pathMod5.resolve(opts.cwd),
1661
+ cwd: pathMod6.resolve(opts.cwd),
1457
1662
  shell: false,
1458
1663
  windowsHide: true,
1459
1664
  env: process.env,
@@ -1754,162 +1959,6 @@ function latestOutputSince(before, after) {
1754
1959
  return after;
1755
1960
  }
1756
1961
 
1757
- // src/tools/fs/edit.ts
1758
- import { promises as fs } from "fs";
1759
- import * as pathMod6 from "path";
1760
- function displayRel(rootDir, full) {
1761
- return pathMod6.relative(rootDir, full).replaceAll("\\", "/");
1762
- }
1763
- async function applyEdit(rootDir, abs, args) {
1764
- if (args.search.length === 0) {
1765
- throw new Error("edit_file: search cannot be empty");
1766
- }
1767
- const before = await fs.readFile(abs, "utf8");
1768
- const le = before.includes("\r\n") ? "\r\n" : "\n";
1769
- const adaptedSearch = args.search.replace(/\r?\n/g, le);
1770
- const adaptedReplace = args.replace.replace(/\r?\n/g, le);
1771
- const firstIdx = before.indexOf(adaptedSearch);
1772
- if (firstIdx < 0) {
1773
- throw new Error(`edit_file: search text not found in ${displayRel(rootDir, abs)}`);
1774
- }
1775
- const nextIdx = before.indexOf(adaptedSearch, firstIdx + 1);
1776
- if (nextIdx >= 0) {
1777
- throw new Error(
1778
- `edit_file: search text appears multiple times in ${displayRel(rootDir, abs)} \u2014 include more context to disambiguate`
1779
- );
1780
- }
1781
- const after = before.slice(0, firstIdx) + adaptedReplace + before.slice(firstIdx + adaptedSearch.length);
1782
- await fs.writeFile(abs, after, "utf8");
1783
- const rel = displayRel(rootDir, abs);
1784
- const header = `edited ${rel} (${adaptedSearch.length}\u2192${adaptedReplace.length} chars)`;
1785
- const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
1786
- const diff = renderEditDiff(adaptedSearch, adaptedReplace, startLine);
1787
- return `${header}
1788
- ${diff}`;
1789
- }
1790
- async function applyMultiEdit(rootDir, edits) {
1791
- if (edits.length === 0) {
1792
- throw new Error("multi_edit: edits must contain at least one entry");
1793
- }
1794
- const filesByPath = /* @__PURE__ */ new Map();
1795
- for (let i = 0; i < edits.length; i++) {
1796
- const e = edits[i];
1797
- if (typeof e.abs !== "string" || e.abs.length === 0) {
1798
- throw new Error(`multi_edit: edit #${i + 1} requires a string \`path\` (no edits applied)`);
1799
- }
1800
- if (typeof e.search !== "string") {
1801
- throw new Error(`multi_edit: edit #${i + 1} requires a string \`search\` (no edits applied)`);
1802
- }
1803
- if (typeof e.replace !== "string") {
1804
- throw new Error(
1805
- `multi_edit: edit #${i + 1} requires a string \`replace\` (no edits applied)`
1806
- );
1807
- }
1808
- const rel = displayRel(rootDir, e.abs);
1809
- if (e.search.length === 0) {
1810
- throw new Error(
1811
- `multi_edit: edit #${i + 1} (${rel}) search cannot be empty (no edits applied)`
1812
- );
1813
- }
1814
- let state = filesByPath.get(e.abs);
1815
- if (!state) {
1816
- let before;
1817
- try {
1818
- before = await fs.readFile(e.abs, "utf8");
1819
- } catch (err) {
1820
- throw new Error(
1821
- `multi_edit: edit #${i + 1} cannot read ${rel}: ${err.message} (no edits applied)`
1822
- );
1823
- }
1824
- const le = before.includes("\r\n") ? "\r\n" : "\n";
1825
- state = { buf: before, le, hunks: [], deltaChars: 0, touched: 0 };
1826
- filesByPath.set(e.abs, state);
1827
- }
1828
- const adaptedSearch = e.search.replace(/\r?\n/g, state.le);
1829
- const adaptedReplace = e.replace.replace(/\r?\n/g, state.le);
1830
- const firstIdx = state.buf.indexOf(adaptedSearch);
1831
- if (firstIdx < 0) {
1832
- throw new Error(
1833
- `multi_edit: edit #${i + 1} search text not found in ${rel} \u2014 no edits applied (multi_edit is atomic)`
1834
- );
1835
- }
1836
- const nextIdx = state.buf.indexOf(adaptedSearch, firstIdx + 1);
1837
- if (nextIdx >= 0) {
1838
- throw new Error(
1839
- `multi_edit: edit #${i + 1} search text appears multiple times in ${rel} \u2014 include more context to disambiguate (no edits applied)`
1840
- );
1841
- }
1842
- const startLine = state.buf.slice(0, firstIdx).split(/\r?\n/).length;
1843
- state.buf = state.buf.slice(0, firstIdx) + adaptedReplace + state.buf.slice(firstIdx + adaptedSearch.length);
1844
- state.hunks.push(`# ${rel}
1845
- ${renderEditDiff(adaptedSearch, adaptedReplace, startLine)}`);
1846
- state.deltaChars += adaptedReplace.length - adaptedSearch.length;
1847
- state.touched++;
1848
- }
1849
- for (const [abs, state] of filesByPath) {
1850
- await fs.writeFile(abs, state.buf, "utf8");
1851
- }
1852
- const fileCount = filesByPath.size;
1853
- const editCount = edits.length;
1854
- let totalDelta = 0;
1855
- const allHunks = [];
1856
- for (const state of filesByPath.values()) {
1857
- totalDelta += state.deltaChars;
1858
- allHunks.push(...state.hunks);
1859
- }
1860
- const sign = totalDelta >= 0 ? "+" : "";
1861
- const editNoun = editCount === 1 ? "edit" : "edits";
1862
- const fileNoun = fileCount === 1 ? "file" : "files";
1863
- const header = `multi_edit: applied ${editCount} ${editNoun} across ${fileCount} ${fileNoun} (${sign}${totalDelta} chars)`;
1864
- return `${header}
1865
- ${allHunks.join("\n")}`;
1866
- }
1867
- function renderEditDiff(search, replace, startLine) {
1868
- const a = search.split(/\r?\n/);
1869
- const b = replace.split(/\r?\n/);
1870
- const diff = lineDiff(a, b);
1871
- const hunk = `@@ -${startLine},${a.length} +${startLine},${b.length} @@`;
1872
- const body = diff.map((d) => `${d.op === " " ? " " : d.op} ${d.line}`).join("\n");
1873
- return `${hunk}
1874
- ${body}`;
1875
- }
1876
- function lineDiff(a, b) {
1877
- const n = a.length;
1878
- const m = b.length;
1879
- const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
1880
- for (let i2 = 1; i2 <= n; i2++) {
1881
- for (let j2 = 1; j2 <= m; j2++) {
1882
- if (a[i2 - 1] === b[j2 - 1]) dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
1883
- else dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
1884
- }
1885
- }
1886
- const out = [];
1887
- let i = n;
1888
- let j = m;
1889
- while (i > 0 && j > 0) {
1890
- if (a[i - 1] === b[j - 1]) {
1891
- out.unshift({ op: " ", line: a[i - 1] });
1892
- i--;
1893
- j--;
1894
- } else if ((dp[i - 1][j] ?? 0) > (dp[i][j - 1] ?? 0)) {
1895
- out.unshift({ op: "-", line: a[i - 1] });
1896
- i--;
1897
- } else {
1898
- out.unshift({ op: "+", line: b[j - 1] });
1899
- j--;
1900
- }
1901
- }
1902
- while (i > 0) {
1903
- out.unshift({ op: "-", line: a[i - 1] });
1904
- i--;
1905
- }
1906
- while (j > 0) {
1907
- out.unshift({ op: "+", line: b[j - 1] });
1908
- j--;
1909
- }
1910
- return out;
1911
- }
1912
-
1913
1962
  export {
1914
1963
  pauseGate,
1915
1964
  applyEdit,
@@ -1921,4 +1970,4 @@ export {
1921
1970
  registerShellTools,
1922
1971
  formatCommandResult
1923
1972
  };
1924
- //# sourceMappingURL=chunk-TH756VLN.js.map
1973
+ //# sourceMappingURL=chunk-MXUSER5C.js.map