@wrongstack/tools 0.250.0 → 0.256.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 (54) hide show
  1. package/dist/audit.js +591 -48
  2. package/dist/audit.js.map +1 -1
  3. package/dist/{background-indexer-DwJsyAB0.d.ts → background-indexer-CJ5JiV5i.d.ts} +0 -8
  4. package/dist/bash.js +133 -23
  5. package/dist/bash.js.map +1 -1
  6. package/dist/batch-tool-use.js +1 -0
  7. package/dist/batch-tool-use.js.map +1 -1
  8. package/dist/builtin.d.ts +25 -1
  9. package/dist/builtin.js +782 -535
  10. package/dist/builtin.js.map +1 -1
  11. package/dist/codebase-index/index.d.ts +2 -2
  12. package/dist/codebase-index/index.js +16 -0
  13. package/dist/codebase-index/index.js.map +1 -1
  14. package/dist/codebase-index/worker.js +11 -6
  15. package/dist/codebase-index/worker.js.map +1 -1
  16. package/dist/document.js +1 -0
  17. package/dist/document.js.map +1 -1
  18. package/dist/exec.js +115 -5
  19. package/dist/exec.js.map +1 -1
  20. package/dist/format.js +590 -48
  21. package/dist/format.js.map +1 -1
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.js +380 -128
  24. package/dist/index.js.map +1 -1
  25. package/dist/install.js +590 -48
  26. package/dist/install.js.map +1 -1
  27. package/dist/json.js +1 -0
  28. package/dist/json.js.map +1 -1
  29. package/dist/lint.js +590 -47
  30. package/dist/lint.js.map +1 -1
  31. package/dist/logs.js +1 -0
  32. package/dist/logs.js.map +1 -1
  33. package/dist/memory.js +4 -0
  34. package/dist/memory.js.map +1 -1
  35. package/dist/mode.js +1 -0
  36. package/dist/mode.js.map +1 -1
  37. package/dist/outdated.js +17 -3
  38. package/dist/outdated.js.map +1 -1
  39. package/dist/pack.js +746 -527
  40. package/dist/pack.js.map +1 -1
  41. package/dist/test.d.ts +1 -0
  42. package/dist/test.js +605 -55
  43. package/dist/test.js.map +1 -1
  44. package/dist/todo.js +1 -0
  45. package/dist/todo.js.map +1 -1
  46. package/dist/tool-help.js +1 -0
  47. package/dist/tool-help.js.map +1 -1
  48. package/dist/tool-search.js +1 -0
  49. package/dist/tool-search.js.map +1 -1
  50. package/dist/tool-use.js +1 -0
  51. package/dist/tool-use.js.map +1 -1
  52. package/dist/typecheck.js +591 -48
  53. package/dist/typecheck.js.map +1 -1
  54. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@ import * as fs4 from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { resolve, sep, dirname, join } from 'node:path';
4
4
  import * as Core from '@wrongstack/core';
5
- import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, emptyTaskFile, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, mutateTasks, emptyPlan, computeTaskItemProgress, resolveWstackPaths, truncate } from '@wrongstack/core';
5
+ import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, emptyTaskFile, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, mutateTasks, emptyPlan, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
6
6
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
7
7
  import * as os from 'node:os';
8
8
  import * as fs7 from 'node:fs';
9
- import { statSync, writeFileSync, mkdirSync } from 'node:fs';
9
+ import { statSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
10
10
  import * as dns from 'node:dns/promises';
11
11
  import * as net from 'node:net';
12
12
  import { Agent } from 'undici';
@@ -18,14 +18,14 @@ import { randomUUID } from 'node:crypto';
18
18
 
19
19
  // src/read.ts
20
20
  async function detectPackageManager(cwd) {
21
- const { stat: stat10 } = await import('node:fs/promises');
21
+ const { stat: stat11 } = await import('node:fs/promises');
22
22
  try {
23
- await stat10(`${cwd}/pnpm-lock.yaml`);
23
+ await stat11(`${cwd}/pnpm-lock.yaml`);
24
24
  return "pnpm";
25
25
  } catch {
26
26
  }
27
27
  try {
28
- await stat10(`${cwd}/yarn.lock`);
28
+ await stat11(`${cwd}/yarn.lock`);
29
29
  return "yarn";
30
30
  } catch {
31
31
  }
@@ -194,9 +194,9 @@ var readTool = {
194
194
  async execute(input, ctx) {
195
195
  if (!input?.path) throw new Error("read: path is required");
196
196
  const absPath = await safeResolveReal(input.path, ctx);
197
- let stat10;
197
+ let stat11;
198
198
  try {
199
- stat10 = await fs4.stat(absPath);
199
+ stat11 = await fs4.stat(absPath);
200
200
  } catch (err) {
201
201
  const code = err.code;
202
202
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
@@ -204,9 +204,9 @@ var readTool = {
204
204
  `read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
205
205
  );
206
206
  }
207
- if (!stat10.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
208
- if (stat10.size > MAX_BYTES) {
209
- throw new Error(`read: file too large (${stat10.size} bytes, limit ${MAX_BYTES})`);
207
+ if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
208
+ if (stat11.size > MAX_BYTES) {
209
+ throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES})`);
210
210
  }
211
211
  const buf = await fs4.readFile(absPath);
212
212
  if (isBinaryBuffer(buf)) {
@@ -218,14 +218,14 @@ var readTool = {
218
218
  const offset = Math.max(1, input.offset ?? 1);
219
219
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
220
220
  if (limit === 0) {
221
- ctx.recordRead(absPath, stat10.mtimeMs);
221
+ ctx.recordRead(absPath, stat11.mtimeMs);
222
222
  return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
223
223
  }
224
224
  const slice = allLines.slice(offset - 1, offset - 1 + limit);
225
225
  const truncated = offset - 1 + slice.length < total;
226
226
  const width = String(offset + slice.length - 1).length;
227
227
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
228
- ctx.recordRead(absPath, stat10.mtimeMs);
228
+ ctx.recordRead(absPath, stat11.mtimeMs);
229
229
  return {
230
230
  text: numbered,
231
231
  total_lines: total,
@@ -264,12 +264,12 @@ var writeTool = {
264
264
  let existed = false;
265
265
  let prev = "";
266
266
  try {
267
- const stat11 = await fs4.stat(absPath);
268
- existed = stat11.isFile();
267
+ const stat12 = await fs4.stat(absPath);
268
+ existed = stat12.isFile();
269
269
  if (existed) {
270
270
  if (!ctx.hasRead(absPath)) {
271
271
  prev = await fs4.readFile(absPath, "utf8");
272
- ctx.recordRead(absPath, stat11.mtimeMs);
272
+ ctx.recordRead(absPath, stat12.mtimeMs);
273
273
  } else {
274
274
  prev = await fs4.readFile(absPath, "utf8");
275
275
  }
@@ -282,8 +282,8 @@ var writeTool = {
282
282
  await atomicWrite(absPath, input.content);
283
283
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
284
284
  + (new file, ${input.content.split("\n").length} lines)`;
285
- const stat10 = await fs4.stat(absPath);
286
- ctx.recordRead(absPath, stat10.mtimeMs);
285
+ const stat11 = await fs4.stat(absPath);
286
+ ctx.recordRead(absPath, stat11.mtimeMs);
287
287
  ctx.session.recordFileChange({
288
288
  path: absPath,
289
289
  action: existed ? "modified" : "created",
@@ -323,13 +323,13 @@ var editTool = {
323
323
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
324
324
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
325
325
  const absPath = await safeResolveReal(input.path, ctx);
326
- const stat10 = await fs4.stat(absPath).catch((err) => {
326
+ const stat11 = await fs4.stat(absPath).catch((err) => {
327
327
  if (err.code === "ENOENT") {
328
328
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
329
329
  }
330
330
  throw err;
331
331
  });
332
- if (!stat10.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
332
+ if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
333
333
  if (!ctx.hasRead(absPath)) {
334
334
  throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
335
335
  }
@@ -524,8 +524,8 @@ var replaceTool = {
524
524
  }
525
525
  const rel = path.relative(realRoot, realPath);
526
526
  if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
527
- const stat10 = await fs4.stat(realPath).catch(() => null);
528
- if (!stat10 || !stat10.isFile()) continue;
527
+ const stat11 = await fs4.stat(realPath).catch(() => null);
528
+ if (!stat11 || !stat11.isFile()) continue;
529
529
  let content;
530
530
  try {
531
531
  const buf = await fs4.readFile(realPath);
@@ -550,7 +550,7 @@ var replaceTool = {
550
550
  totalReplacements += count;
551
551
  if (!dryRun) {
552
552
  const newContent = toStyle(newContentLf, style);
553
- await atomicWrite(realPath, newContent, { mode: stat10.mode & 511 });
553
+ await atomicWrite(realPath, newContent, { mode: stat11.mode & 511 });
554
554
  }
555
555
  const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
556
556
  fromFile: absPath,
@@ -580,8 +580,8 @@ async function resolveFiles(filesInput, ctx, extraGlob) {
580
580
  const resolved = [];
581
581
  for (const p of parts) {
582
582
  const absPath = safeResolve(p, ctx);
583
- const stat10 = await fs4.stat(absPath).catch(() => null);
584
- if (stat10?.isFile()) {
583
+ const stat11 = await fs4.stat(absPath).catch(() => null);
584
+ if (stat11?.isFile()) {
585
585
  resolved.push(absPath);
586
586
  }
587
587
  }
@@ -644,8 +644,8 @@ async function globNative(pattern, base, extraGlob) {
644
644
  if (DEFAULT_IGNORE.includes(e.name)) continue;
645
645
  const full = path.join(dir, e.name);
646
646
  try {
647
- const stat10 = await fs4.lstat(full);
648
- if (stat10.isSymbolicLink()) continue;
647
+ const stat11 = await fs4.lstat(full);
648
+ if (stat11.isSymbolicLink()) continue;
649
649
  } catch {
650
650
  continue;
651
651
  }
@@ -996,8 +996,8 @@ async function runNative(input, base, mode, limit, signal) {
996
996
  if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
997
997
  if (globRe) globRe.lastIndex = 0;
998
998
  try {
999
- const stat10 = await fs4.stat(full);
1000
- if (stat10.size > 1e6) continue;
999
+ const stat11 = await fs4.stat(full);
1000
+ if (stat11.size > 1e6) continue;
1001
1001
  const head = await fs4.readFile(full);
1002
1002
  if (isBinaryBuffer(head)) continue;
1003
1003
  const text = head.toString("utf8");
@@ -1037,6 +1037,107 @@ async function runNative(input, base, mode, limit, signal) {
1037
1037
  used: "native"
1038
1038
  };
1039
1039
  }
1040
+ var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
1041
+ var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
1042
+ var sweepStarted = false;
1043
+ function toolOutputDir() {
1044
+ return path.join(wstackGlobalRoot(), "tool-output");
1045
+ }
1046
+ function sweepOldSpoolFiles(dir) {
1047
+ if (sweepStarted) return;
1048
+ sweepStarted = true;
1049
+ void (async () => {
1050
+ try {
1051
+ const now = Date.now();
1052
+ for (const name of await fs4.readdir(dir)) {
1053
+ if (!name.endsWith(".log")) continue;
1054
+ const p = path.join(dir, name);
1055
+ try {
1056
+ const st = await fs4.stat(p);
1057
+ if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fs4.unlink(p);
1058
+ } catch {
1059
+ }
1060
+ }
1061
+ } catch {
1062
+ }
1063
+ })();
1064
+ }
1065
+ function spoolNote(info) {
1066
+ const dropped = info.droppedBytes > 0 ? `, ~${info.droppedBytes} bytes dropped under backpressure` : "";
1067
+ return `
1068
+ [output truncated \u2014 full ${info.bytes} bytes at ${info.path}${dropped}; read/grep that file selectively instead of re-running with more output]`;
1069
+ }
1070
+ function createOutputSpool(opts) {
1071
+ const threshold = opts.thresholdBytes ?? 32768;
1072
+ const safeTool = opts.tool.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
1073
+ let head = "";
1074
+ let headBytes = 0;
1075
+ let totalBytes = 0;
1076
+ let droppedBytes = 0;
1077
+ let stream = null;
1078
+ let filePath = null;
1079
+ let failed = false;
1080
+ let finalized = false;
1081
+ const open = () => {
1082
+ if (stream || failed) return;
1083
+ try {
1084
+ const dir = toolOutputDir();
1085
+ mkdirSync(dir, { recursive: true });
1086
+ sweepOldSpoolFiles(dir);
1087
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1088
+ const rand = Math.random().toString(36).slice(2, 6);
1089
+ filePath = path.join(dir, `${stamp}-${safeTool}-${rand}.log`);
1090
+ stream = createWriteStream(filePath, { flags: "w", encoding: "utf8" });
1091
+ stream.on("error", () => {
1092
+ failed = true;
1093
+ stream = null;
1094
+ filePath = null;
1095
+ });
1096
+ stream.write(head);
1097
+ } catch {
1098
+ failed = true;
1099
+ stream = null;
1100
+ filePath = null;
1101
+ }
1102
+ };
1103
+ return {
1104
+ write(text) {
1105
+ if (finalized || !text) return;
1106
+ totalBytes += Buffer.byteLength(text, "utf8");
1107
+ if (!stream && !failed) {
1108
+ if (headBytes + text.length <= threshold) {
1109
+ head += text;
1110
+ headBytes += text.length;
1111
+ return;
1112
+ }
1113
+ head += text;
1114
+ open();
1115
+ head = "";
1116
+ return;
1117
+ }
1118
+ if (stream) {
1119
+ if (stream.writableLength > SPOOL_WRITE_HWM_BYTES) {
1120
+ droppedBytes += Buffer.byteLength(text, "utf8");
1121
+ return;
1122
+ }
1123
+ stream.write(text);
1124
+ }
1125
+ },
1126
+ finalize() {
1127
+ if (finalized) {
1128
+ return filePath ? { path: filePath, bytes: totalBytes, droppedBytes } : null;
1129
+ }
1130
+ finalized = true;
1131
+ head = "";
1132
+ if (!stream || !filePath) return null;
1133
+ try {
1134
+ stream.end();
1135
+ } catch {
1136
+ }
1137
+ return { path: filePath, bytes: totalBytes, droppedBytes };
1138
+ }
1139
+ };
1140
+ }
1040
1141
 
1041
1142
  // src/circuit-breaker.ts
1042
1143
  var DEFAULT_MAX_CONSECUTIVE_FAILURES = 5;
@@ -1516,7 +1617,7 @@ var bashTool = {
1516
1617
  })();
1517
1618
  const args = isWin ? ["/c", input.command] : ["-c", input.command];
1518
1619
  const env = buildChildEnv(ctx.session?.id);
1519
- const detached = isWin ? !!input.background : true;
1620
+ const detached = !isWin;
1520
1621
  const startedAt = Date.now();
1521
1622
  if (input.background) {
1522
1623
  let buf2 = "";
@@ -1525,10 +1626,14 @@ var bashTool = {
1525
1626
  cwd: ctx.projectRoot,
1526
1627
  env,
1527
1628
  stdio: ["ignore", "pipe", "pipe"],
1528
- detached: true,
1529
- // Detached console children on Windows allocate their own VISIBLE
1530
- // console window (one per background command test suites flash
1531
- // dozens). CREATE_NO_WINDOW suppresses it; no-op elsewhere.
1629
+ // win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
1630
+ // DETACHED_PROCESS (detached: true) is set, so the console-less
1631
+ // cmd.exe's grandchildren (node, dev servers) each allocate a fresh
1632
+ // VISIBLE console window. detached: false lets CREATE_NO_WINDOW
1633
+ // apply: the child gets a hidden console that grandchildren inherit.
1634
+ // Windows children survive parent exit either way. POSIX keeps
1635
+ // detached for the process-group kill semantics.
1636
+ detached: !isWin,
1532
1637
  windowsHide: true,
1533
1638
  signal: opts.signal
1534
1639
  });
@@ -1544,24 +1649,22 @@ var bashTool = {
1544
1649
  });
1545
1650
  child2.on("close", () => registry.unregister(pid2));
1546
1651
  }
1547
- child2.stdout?.on("data", (chunk) => {
1548
- if (!truncated) {
1549
- const remain = MAX_OUTPUT - buf2.length;
1550
- if (remain > 0) {
1551
- buf2 += chunk.toString().slice(0, remain);
1552
- }
1553
- if (buf2.length >= MAX_OUTPUT) truncated = true;
1652
+ const onBgData = (chunk) => {
1653
+ if (truncated) return;
1654
+ const remain = MAX_OUTPUT - buf2.length;
1655
+ if (remain > 0) {
1656
+ buf2 += chunk.toString().slice(0, remain);
1554
1657
  }
1555
- });
1556
- child2.stderr?.on("data", (chunk) => {
1557
- if (!truncated) {
1558
- const remain = MAX_OUTPUT - buf2.length;
1559
- if (remain > 0) {
1560
- buf2 += chunk.toString().slice(0, remain);
1561
- }
1562
- if (buf2.length >= MAX_OUTPUT) truncated = true;
1658
+ if (buf2.length >= MAX_OUTPUT) {
1659
+ truncated = true;
1660
+ child2.stdout?.off("data", onBgData);
1661
+ child2.stderr?.off("data", onBgData);
1563
1662
  }
1564
- });
1663
+ };
1664
+ child2.stdout?.on("data", onBgData);
1665
+ child2.stderr?.on("data", onBgData);
1666
+ child2.stdout?.unref?.();
1667
+ child2.stderr?.unref?.();
1565
1668
  child2.on("close", () => {
1566
1669
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
1567
1670
  });
@@ -1600,6 +1703,7 @@ var bashTool = {
1600
1703
  let pending2 = "";
1601
1704
  let timedOut = false;
1602
1705
  const timers = [];
1706
+ const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
1603
1707
  function killWithTimeout(child2, timeoutMs2) {
1604
1708
  if (isWin) {
1605
1709
  if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
@@ -1705,6 +1809,7 @@ var bashTool = {
1705
1809
  if (buf.length < MAX_OUTPUT) {
1706
1810
  buf += text.slice(0, MAX_OUTPUT - buf.length);
1707
1811
  }
1812
+ spool.write(text);
1708
1813
  pending2 += text;
1709
1814
  push({ kind: "data", text });
1710
1815
  pauseIfFlooded();
@@ -1732,10 +1837,11 @@ var bashTool = {
1732
1837
  if (remainder !== null) {
1733
1838
  yield { type: "partial_output", text: remainder };
1734
1839
  }
1840
+ const spooled = spool.finalize();
1735
1841
  yield {
1736
1842
  type: "final",
1737
1843
  output: {
1738
- output: normalizeCommandOutput(buf),
1844
+ output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : ""),
1739
1845
  exit_code: c.code,
1740
1846
  timed_out: timedOut
1741
1847
  }
@@ -1750,6 +1856,7 @@ var bashTool = {
1750
1856
  }
1751
1857
  } finally {
1752
1858
  for (const t of timers) clearTimeout(t);
1859
+ spool.finalize();
1753
1860
  if (isWin) opts.signal.removeEventListener("abort", onAbort);
1754
1861
  child.stdout?.off("data", onData);
1755
1862
  child.stderr?.off("data", onData);
@@ -1990,6 +2097,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
1990
2097
  let stderr = "";
1991
2098
  let killed = false;
1992
2099
  const startedAt = Date.now();
2100
+ const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
1993
2101
  const resolved = resolveWin32Command(cmd);
1994
2102
  const isWin = process.platform === "win32";
1995
2103
  const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
@@ -2022,10 +2130,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
2022
2130
  else signal.addEventListener("abort", onAbort, { once: true });
2023
2131
  }
2024
2132
  child.stdout?.on("data", (chunk) => {
2025
- if (stdout.length < MAX_OUTPUT2) stdout += chunk.toString();
2133
+ const text = chunk.toString();
2134
+ if (stdout.length < MAX_OUTPUT2) stdout += text;
2135
+ spool.write(text);
2026
2136
  });
2027
2137
  child.stderr?.on("data", (chunk) => {
2028
- if (stderr.length < MAX_OUTPUT2) stderr += chunk.toString();
2138
+ const text = chunk.toString();
2139
+ if (stderr.length < MAX_OUTPUT2) stderr += text;
2140
+ spool.write(text);
2029
2141
  });
2030
2142
  child.on("close", (code) => {
2031
2143
  clearTimeout(timer);
@@ -2034,10 +2146,11 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
2034
2146
  const durationMs = Date.now() - startedAt;
2035
2147
  const exitCode = killed ? 124 : code ?? 1;
2036
2148
  registry.afterCall(durationMs, exitCode !== 0);
2149
+ const spooled = spool.finalize();
2037
2150
  resolve7({
2038
2151
  command: cmd,
2039
2152
  args,
2040
- stdout: normalizeCommandOutput(stdout),
2153
+ stdout: normalizeCommandOutput(stdout) + (spooled ? spoolNote(spooled) : ""),
2041
2154
  stderr: normalizeCommandOutput(stderr),
2042
2155
  exitCode,
2043
2156
  truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
@@ -2049,6 +2162,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
2049
2162
  if (isWin) signal.removeEventListener("abort", onAbort);
2050
2163
  if (typeof pid === "number") registry.unregister(pid);
2051
2164
  registry.afterCall(Date.now() - startedAt, true);
2165
+ spool.finalize();
2052
2166
  resolve7({
2053
2167
  command: cmd,
2054
2168
  args,
@@ -2614,6 +2728,7 @@ var todoTool = {
2614
2728
  mutating: false,
2615
2729
  // mutates only conversation state (ctx.todos), not external state — no confirmation needed
2616
2730
  timeoutMs: 1e3,
2731
+ capabilities: ["session.todo"],
2617
2732
  inputSchema: {
2618
2733
  type: "object",
2619
2734
  properties: {
@@ -3071,8 +3186,8 @@ function findGitDir(cwd, projectRoot) {
3071
3186
  let dir = cwd;
3072
3187
  for (let i = 0; i < 20; i++) {
3073
3188
  try {
3074
- const stat10 = statSync(`${dir}/.git`);
3075
- if (stat10.isDirectory() || stat10.isFile()) return dir;
3189
+ const stat11 = statSync(`${dir}/.git`);
3190
+ if (stat11.isDirectory() || stat11.isFile()) return dir;
3076
3191
  } catch {
3077
3192
  }
3078
3193
  if (dir === root) break;
@@ -3311,6 +3426,7 @@ var jsonTool = {
3311
3426
  permission: "auto",
3312
3427
  mutating: false,
3313
3428
  timeoutMs: 5e3,
3429
+ capabilities: ["fs.read"],
3314
3430
  inputSchema: {
3315
3431
  type: "object",
3316
3432
  properties: {
@@ -3375,8 +3491,8 @@ var jsonTool = {
3375
3491
  };
3376
3492
  }
3377
3493
  };
3378
- function query(data, path20) {
3379
- const parts = path20.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
3494
+ function query(data, path21) {
3495
+ const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
3380
3496
  let current = data;
3381
3497
  for (const part of parts) {
3382
3498
  if (current === null || current === void 0) return void 0;
@@ -3505,8 +3621,8 @@ function findGitDir2(cwd) {
3505
3621
  let dir = cwd;
3506
3622
  for (let i = 0; i < 20; i++) {
3507
3623
  try {
3508
- const stat10 = statSync(path.join(dir, ".git"));
3509
- if (stat10.isDirectory()) return dir;
3624
+ const stat11 = statSync(path.join(dir, ".git"));
3625
+ if (stat11.isDirectory()) return dir;
3510
3626
  } catch {
3511
3627
  }
3512
3628
  const parent = path.dirname(dir);
@@ -3550,8 +3666,8 @@ async function fileDiff(input, ctx, _signal) {
3550
3666
  const results = [];
3551
3667
  for (const file of files) {
3552
3668
  const absPath = safeResolve(file, ctx);
3553
- const stat10 = await fs4.stat(absPath).catch(() => null);
3554
- if (!stat10?.isFile()) continue;
3669
+ const stat11 = await fs4.stat(absPath).catch(() => null);
3670
+ if (!stat11?.isFile()) continue;
3555
3671
  const content = await fs4.readFile(absPath, "utf8");
3556
3672
  const lines = content.split(/\r?\n/);
3557
3673
  results.push(formatWithLineNumbers(file, lines));
@@ -3754,16 +3870,29 @@ async function* spawnStream(opts) {
3754
3870
  let stderr = "";
3755
3871
  let pending2 = "";
3756
3872
  let error;
3873
+ const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
3757
3874
  const cmd = resolveWin32Command(opts.cmd);
3758
- const needsShell = process.platform === "win32" && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
3875
+ const isWin = process.platform === "win32";
3876
+ const needsShell = isWin && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
3759
3877
  const child = spawn(cmd, opts.args, {
3760
3878
  cwd: opts.cwd,
3761
- signal: opts.signal,
3762
3879
  env: buildChildEnv(),
3763
3880
  stdio: ["ignore", "pipe", "pipe"],
3764
3881
  windowsHide: true,
3882
+ ...isWin ? {} : { signal: opts.signal },
3765
3883
  ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
3766
3884
  });
3885
+ const registry = getProcessRegistry();
3886
+ const pid = child.pid;
3887
+ if (typeof pid === "number") {
3888
+ registry.register({
3889
+ pid,
3890
+ name: opts.cmd,
3891
+ command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
3892
+ startedAt: Date.now(),
3893
+ child
3894
+ });
3895
+ }
3767
3896
  const queue = [];
3768
3897
  let waiter;
3769
3898
  let paused = false;
@@ -3781,9 +3910,10 @@ async function* spawnStream(opts) {
3781
3910
  child.stderr?.resume();
3782
3911
  }
3783
3912
  };
3784
- child.stdout?.on("data", (c) => {
3913
+ const onOut = (c) => {
3785
3914
  const s = c.toString();
3786
3915
  if (stdout.length < max) stdout += s;
3916
+ spool.write(s);
3787
3917
  queue.push({ kind: "out", data: s });
3788
3918
  wake();
3789
3919
  if (!paused && queue.length >= maxQueue) {
@@ -3791,10 +3921,11 @@ async function* spawnStream(opts) {
3791
3921
  child.stdout?.pause();
3792
3922
  child.stderr?.pause();
3793
3923
  }
3794
- });
3795
- child.stderr?.on("data", (c) => {
3924
+ };
3925
+ const onErr = (c) => {
3796
3926
  const s = c.toString();
3797
3927
  if (stderr.length < max) stderr += s;
3928
+ spool.write(s);
3798
3929
  queue.push({ kind: "err", data: s });
3799
3930
  wake();
3800
3931
  if (!paused && queue.length >= maxQueue) {
@@ -3802,51 +3933,92 @@ async function* spawnStream(opts) {
3802
3933
  child.stdout?.pause();
3803
3934
  child.stderr?.pause();
3804
3935
  }
3805
- });
3936
+ };
3937
+ child.stdout?.on("data", onOut);
3938
+ child.stderr?.on("data", onErr);
3806
3939
  child.on("error", (e) => {
3807
3940
  error = e.message;
3808
3941
  queue.push({ kind: "error", data: e.message });
3809
3942
  wake();
3810
3943
  });
3811
3944
  child.on("close", (code) => {
3945
+ if (typeof pid === "number") registry.unregister(pid);
3812
3946
  queue.push({ kind: "close", data: "", code: code ?? 0 });
3813
3947
  wake();
3814
3948
  });
3949
+ const onAbort = () => {
3950
+ if (typeof pid === "number") {
3951
+ registry.kill(pid, { force: true });
3952
+ } else {
3953
+ try {
3954
+ child.kill("SIGKILL");
3955
+ } catch {
3956
+ }
3957
+ }
3958
+ queue.push({ kind: "close", data: "", code: 124 });
3959
+ wake();
3960
+ };
3961
+ if (opts.signal.aborted) onAbort();
3962
+ else opts.signal.addEventListener("abort", onAbort, { once: true });
3815
3963
  let exitCode = 0;
3816
3964
  let spawnFailed = false;
3817
- for (; ; ) {
3818
- while (queue.length === 0) {
3819
- await new Promise((resolve7) => {
3820
- waiter = resolve7;
3821
- });
3822
- }
3823
- const chunk = queue.shift();
3824
- resume();
3825
- if (chunk.kind === "close") {
3826
- if (!spawnFailed) exitCode = chunk.code ?? 0;
3827
- break;
3828
- }
3829
- if (chunk.kind === "error") {
3830
- spawnFailed = true;
3831
- exitCode = 1;
3832
- continue;
3965
+ try {
3966
+ for (; ; ) {
3967
+ while (queue.length === 0) {
3968
+ await new Promise((resolve7) => {
3969
+ waiter = resolve7;
3970
+ });
3971
+ }
3972
+ const chunk = queue.shift();
3973
+ resume();
3974
+ if (chunk.kind === "close") {
3975
+ if (!spawnFailed) exitCode = chunk.code ?? 0;
3976
+ break;
3977
+ }
3978
+ if (chunk.kind === "error") {
3979
+ spawnFailed = true;
3980
+ exitCode = 1;
3981
+ continue;
3982
+ }
3983
+ pending2 += chunk.data;
3984
+ if (pending2.length >= flushAt) {
3985
+ yield { type: "partial_output", text: pending2 };
3986
+ pending2 = "";
3987
+ }
3833
3988
  }
3834
- pending2 += chunk.data;
3835
- if (pending2.length >= flushAt) {
3989
+ if (pending2.length > 0) {
3836
3990
  yield { type: "partial_output", text: pending2 };
3837
- pending2 = "";
3991
+ }
3992
+ const spooled = spool.finalize();
3993
+ return {
3994
+ // The marker rides on stdout's tail so every consumer's head+tail
3995
+ // normalization keeps it without per-tool changes.
3996
+ stdout: spooled ? stdout + spoolNote(spooled) : stdout,
3997
+ stderr,
3998
+ exitCode,
3999
+ truncated: stdout.length >= max || stderr.length >= max,
4000
+ error,
4001
+ spoolPath: spooled?.path,
4002
+ spoolBytes: spooled?.bytes
4003
+ };
4004
+ } finally {
4005
+ spool.finalize();
4006
+ opts.signal.removeEventListener("abort", onAbort);
4007
+ child.stdout?.off("data", onOut);
4008
+ child.stderr?.off("data", onErr);
4009
+ child.stdout?.destroy();
4010
+ child.stderr?.destroy();
4011
+ if (child.exitCode === null && !child.killed) {
4012
+ if (typeof pid === "number") {
4013
+ registry.kill(pid, { force: true });
4014
+ } else {
4015
+ try {
4016
+ child.kill("SIGKILL");
4017
+ } catch {
4018
+ }
4019
+ }
3838
4020
  }
3839
4021
  }
3840
- if (pending2.length > 0) {
3841
- yield { type: "partial_output", text: pending2 };
3842
- }
3843
- return {
3844
- stdout,
3845
- stderr,
3846
- exitCode,
3847
- truncated: stdout.length >= max || stderr.length >= max,
3848
- error
3849
- };
3850
4022
  }
3851
4023
 
3852
4024
  // src/lint.ts
@@ -3858,6 +4030,7 @@ var lintTool = {
3858
4030
  permission: "confirm",
3859
4031
  mutating: false,
3860
4032
  timeoutMs: 6e4,
4033
+ capabilities: ["shell.restricted"],
3861
4034
  inputSchema: {
3862
4035
  type: "object",
3863
4036
  properties: {
@@ -3929,11 +4102,11 @@ var lintTool = {
3929
4102
  }
3930
4103
  };
3931
4104
  async function detectLinter(cwd) {
3932
- const { stat: stat10 } = await import('node:fs/promises');
4105
+ const { stat: stat11 } = await import('node:fs/promises');
3933
4106
  const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
3934
4107
  for (const f of checks) {
3935
4108
  try {
3936
- await stat10(`${cwd}/${f}`);
4109
+ await stat11(`${cwd}/${f}`);
3937
4110
  if (f.includes("biome")) return "biome";
3938
4111
  if (f.includes("eslint")) return "eslint";
3939
4112
  if (f.includes("tslint")) return "tslint";
@@ -4031,13 +4204,13 @@ var formatTool = {
4031
4204
  }
4032
4205
  };
4033
4206
  async function detectFixer(cwd) {
4034
- const { stat: stat10 } = await import('node:fs/promises');
4207
+ const { stat: stat11 } = await import('node:fs/promises');
4035
4208
  try {
4036
- await stat10(`${cwd}/biome.json`);
4209
+ await stat11(`${cwd}/biome.json`);
4037
4210
  return "biome";
4038
4211
  } catch {
4039
4212
  try {
4040
- await stat10(`${cwd}/.prettierrc`);
4213
+ await stat11(`${cwd}/.prettierrc`);
4041
4214
  return "prettier";
4042
4215
  } catch {
4043
4216
  return "biome";
@@ -4052,6 +4225,7 @@ var typecheckTool = {
4052
4225
  permission: "confirm",
4053
4226
  mutating: false,
4054
4227
  timeoutMs: 12e4,
4228
+ capabilities: ["shell.restricted"],
4055
4229
  inputSchema: {
4056
4230
  type: "object",
4057
4231
  properties: {
@@ -4115,11 +4289,11 @@ var typecheckTool = {
4115
4289
  }
4116
4290
  };
4117
4291
  async function findTsConfig(cwd) {
4118
- const { stat: stat10 } = await import('node:fs/promises');
4292
+ const { stat: stat11 } = await import('node:fs/promises');
4119
4293
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
4120
4294
  for (const f of candidates) {
4121
4295
  try {
4122
- const s = await stat10(path.join(cwd, f));
4296
+ const s = await stat11(path.join(cwd, f));
4123
4297
  if (s.isFile()) return path.join(cwd, f);
4124
4298
  } catch {
4125
4299
  }
@@ -4134,6 +4308,7 @@ var testTool = {
4134
4308
  permission: "confirm",
4135
4309
  mutating: false,
4136
4310
  timeoutMs: 12e4,
4311
+ capabilities: ["shell.restricted"],
4137
4312
  inputSchema: {
4138
4313
  type: "object",
4139
4314
  properties: {
@@ -4150,7 +4325,11 @@ var testTool = {
4150
4325
  coverage: { type: "boolean", description: "Generate coverage report (default: false)" },
4151
4326
  cwd: { type: "string", description: "Working directory (default: cwd)" },
4152
4327
  grep: { type: "string", description: "Filter tests by name pattern (default: none)" },
4153
- timeout: { type: "integer", description: "Test timeout in ms (default: 30000)" }
4328
+ timeout: { type: "integer", description: "Test timeout in ms (default: 30000)" },
4329
+ verbose: {
4330
+ type: "boolean",
4331
+ description: "Per-test verbose reporter output (default: false \u2014 the summary reporter is used; full output is always saved to a log file referenced in the result)"
4332
+ }
4154
4333
  }
4155
4334
  },
4156
4335
  async execute(input, ctx, opts) {
@@ -4198,11 +4377,11 @@ var testTool = {
4198
4377
  }
4199
4378
  };
4200
4379
  async function detectRunner(cwd) {
4201
- const { stat: stat10 } = await import('node:fs/promises');
4380
+ const { stat: stat11 } = await import('node:fs/promises');
4202
4381
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
4203
4382
  for (const f of candidates) {
4204
4383
  try {
4205
- await stat10(path.join(cwd, f));
4384
+ await stat11(path.join(cwd, f));
4206
4385
  if (f.includes("vitest")) return "vitest";
4207
4386
  if (f.includes("jest")) return "jest";
4208
4387
  if (f.includes("mocha")) return "mocha";
@@ -4216,17 +4395,14 @@ function buildArgs2(runner, input) {
4216
4395
  const timeout = input.timeout ?? 3e4;
4217
4396
  switch (runner) {
4218
4397
  case "vitest":
4219
- args.push("run", "--reporter=verbose");
4220
- if (input.watch) {
4221
- args[1] = "";
4222
- args.push("watch");
4223
- }
4398
+ args.push(input.watch ? "watch" : "run");
4399
+ if (input.verbose) args.push("--reporter=verbose");
4224
4400
  if (input.coverage) args.push("--coverage");
4225
4401
  if (input.grep) args.push("--testNamePattern", input.grep);
4226
4402
  args.push("--testTimeout", String(timeout));
4227
4403
  break;
4228
4404
  case "jest":
4229
- args.push("--verbose");
4405
+ if (input.verbose) args.push("--verbose");
4230
4406
  if (input.watch) args.push("--watch");
4231
4407
  if (input.coverage) args.push("--coverage");
4232
4408
  if (input.grep) args.push("--testPathPattern", input.grep);
@@ -4270,7 +4446,13 @@ function parseResult(runner, result, duration) {
4270
4446
  passed,
4271
4447
  failed,
4272
4448
  duration_ms: duration,
4273
- output: normalizeCommandOutput(result.stdout || result.error || ""),
4449
+ // A passing run only needs the tail summary in chat history — counts are
4450
+ // already parsed above and the FULL log is on disk (spool marker rides
4451
+ // the stdout tail). Failures keep the standard command-output cap so
4452
+ // the agent sees the failure details inline.
4453
+ output: normalizeCommandOutput(result.stdout || result.error || "", {
4454
+ maxBytes: result.exitCode === 0 ? 4096 : void 0
4455
+ }),
4274
4456
  truncated: result.truncated
4275
4457
  };
4276
4458
  }
@@ -4419,6 +4601,7 @@ var auditTool = {
4419
4601
  usageHint: "CRITICAL SECURITY TOOL:\n\n- Run regularly and especially before any release.\n- Use `level` to focus on high/critical issues.\n- `fix` can attempt automatic remediation for some vulnerabilities.\nThis is one of the most important tools for supply chain security.",
4420
4602
  permission: "confirm",
4421
4603
  mutating: false,
4604
+ capabilities: ["shell.restricted"],
4422
4605
  timeoutMs: 6e4,
4423
4606
  inputSchema: {
4424
4607
  type: "object",
@@ -4512,9 +4695,23 @@ var outdatedTool = {
4512
4695
  name: "outdated",
4513
4696
  category: "Package Management",
4514
4697
  description: "Check for outdated dependencies in the project. Reports current, wanted (semver range), and latest versions available.",
4515
- usageHint: "MAINTENANCE & SECURITY TOOL:\n\n- Run periodically or before dependency-related work.\n- Helps surface packages that may need updates for security or features.\n- Safe, read-only operation.\nUse the output to decide on upgrades. Prefer this over manual shell commands for dependency hygiene.",
4516
- permission: "auto",
4517
- mutating: false,
4698
+ usageHint: "MAINTENANCE & SECURITY TOOL:\n\n- Run periodically or before dependency-related work.\n- Helps surface packages that may need updates for security or features.\n- Hits the package registry over HTTP, so it is NOT purely local \u2014 flagged as mutating for the confirmation gate.\nUse the output to decide on upgrades. Prefer this over manual shell commands for dependency hygiene.",
4699
+ permission: "confirm",
4700
+ // Network side-effecting (registry HTTP). Pairs with `mutating: true`
4701
+ // so the H7 invariant test (`no auto-permission tool declares
4702
+ // mutating: true`) passes — a tool claiming `'auto'` must be purely
4703
+ // read-only, but `outdated` makes outbound HTTP calls to the
4704
+ // registry. The 'confirm' permission routes the call through the
4705
+ // tool.confirm_needed flow on every invocation. M-1 originally
4706
+ // fixed four sibling tools (mcp_control, shellcheck, shellcheck_scan,
4707
+ // web_search) but missed this one; applying the same contract here.
4708
+ mutating: true,
4709
+ // Capability is just "network" — the tool only hits the package
4710
+ // registry over HTTP, never touches the filesystem or runs shell.
4711
+ // The H7 invariant test requires this array to be non-empty for
4712
+ // any mutating:true tool (meta-tools whitelisted). See
4713
+ // tests/permission-mutating-invariant.test.ts:92.
4714
+ capabilities: ["network"],
4518
4715
  timeoutMs: 6e4,
4519
4716
  inputSchema: {
4520
4717
  type: "object",
@@ -4615,6 +4812,7 @@ var logsTool = {
4615
4812
  permission: "confirm",
4616
4813
  mutating: false,
4617
4814
  timeoutMs: 3e4,
4815
+ capabilities: ["shell.restricted"],
4618
4816
  inputSchema: {
4619
4817
  type: "object",
4620
4818
  properties: {
@@ -4736,7 +4934,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
4736
4934
  }
4737
4935
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
4738
4936
  var MAX_TAIL_LINES = 1e5;
4739
- async function fileLogs(path20, lines, filterRe, stream) {
4937
+ async function fileLogs(path21, lines, filterRe, stream) {
4740
4938
  const { createInterface } = await import('node:readline');
4741
4939
  const { createReadStream } = await import('node:fs');
4742
4940
  const entries = [];
@@ -4745,7 +4943,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
4745
4943
  let writeIdx = 0;
4746
4944
  let totalLines = 0;
4747
4945
  const rl = createInterface({
4748
- input: createReadStream(path20),
4946
+ input: createReadStream(path21),
4749
4947
  crlfDelay: Number.POSITIVE_INFINITY
4750
4948
  });
4751
4949
  for await (const line of rl) {
@@ -4766,7 +4964,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
4766
4964
  if (parsed) entries.push(parsed);
4767
4965
  }
4768
4966
  return {
4769
- source: path20,
4967
+ source: path21,
4770
4968
  entries,
4771
4969
  total: entries.length,
4772
4970
  truncated: totalLines > effLines,
@@ -4816,6 +5014,7 @@ var documentTool = {
4816
5014
  permission: "auto",
4817
5015
  mutating: false,
4818
5016
  timeoutMs: 3e4,
5017
+ capabilities: ["fs.read"],
4819
5018
  inputSchema: {
4820
5019
  type: "object",
4821
5020
  properties: {
@@ -4889,8 +5088,8 @@ async function resolveFiles2(filesInput, cwd) {
4889
5088
  for (const f of files) {
4890
5089
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
4891
5090
  try {
4892
- const stat10 = await fs4.stat(absPath);
4893
- if (stat10.isFile()) resolved.push(absPath);
5091
+ const stat11 = await fs4.stat(absPath);
5092
+ if (stat11.isFile()) resolved.push(absPath);
4894
5093
  } catch {
4895
5094
  }
4896
5095
  }
@@ -5149,6 +5348,7 @@ var toolSearchTool = {
5149
5348
  permission: "auto",
5150
5349
  mutating: false,
5151
5350
  timeoutMs: 1e3,
5351
+ capabilities: ["tool.meta"],
5152
5352
  inputSchema: {
5153
5353
  type: "object",
5154
5354
  properties: {
@@ -5227,6 +5427,7 @@ var toolUseTool = {
5227
5427
  permission: "confirm",
5228
5428
  mutating: true,
5229
5429
  timeoutMs: 6e4,
5430
+ capabilities: ["tool.mutate.any"],
5230
5431
  inputSchema: {
5231
5432
  type: "object",
5232
5433
  properties: {
@@ -5296,6 +5497,7 @@ var batchToolUseTool = {
5296
5497
  permission: "confirm",
5297
5498
  mutating: true,
5298
5499
  timeoutMs: 12e4,
5500
+ capabilities: ["tool.mutate.any"],
5299
5501
  inputSchema: {
5300
5502
  type: "object",
5301
5503
  properties: {
@@ -5400,6 +5602,7 @@ var toolHelpTool = {
5400
5602
  permission: "auto",
5401
5603
  mutating: false,
5402
5604
  timeoutMs: 5e3,
5605
+ capabilities: ["tool.meta"],
5403
5606
  inputSchema: {
5404
5607
  type: "object",
5405
5608
  properties: {
@@ -5523,6 +5726,7 @@ function rememberTool(memory) {
5523
5726
  permission: "auto",
5524
5727
  mutating: true,
5525
5728
  timeoutMs: 2e3,
5729
+ capabilities: ["memory.write"],
5526
5730
  inputSchema: {
5527
5731
  type: "object",
5528
5732
  properties: {
@@ -5574,6 +5778,7 @@ function forgetTool(memory) {
5574
5778
  permission: "confirm",
5575
5779
  mutating: true,
5576
5780
  timeoutMs: 2e3,
5781
+ capabilities: ["memory.delete"],
5577
5782
  inputSchema: {
5578
5783
  type: "object",
5579
5784
  properties: {
@@ -5599,6 +5804,7 @@ function searchMemoryTool(memory) {
5599
5804
  permission: "auto",
5600
5805
  mutating: false,
5601
5806
  timeoutMs: 2e3,
5807
+ capabilities: ["memory.read"],
5602
5808
  inputSchema: {
5603
5809
  type: "object",
5604
5810
  properties: {
@@ -5645,6 +5851,7 @@ function relatedMemoryTool(memory) {
5645
5851
  permission: "auto",
5646
5852
  mutating: false,
5647
5853
  timeoutMs: 2e3,
5854
+ capabilities: ["memory.read"],
5648
5855
  inputSchema: {
5649
5856
  type: "object",
5650
5857
  properties: {
@@ -5693,6 +5900,7 @@ function createModeTool(modeStore) {
5693
5900
  permission: "confirm",
5694
5901
  mutating: true,
5695
5902
  timeoutMs: 5e3,
5903
+ capabilities: ["session.mode"],
5696
5904
  inputSchema: {
5697
5905
  type: "object",
5698
5906
  properties: {
@@ -7902,20 +8110,20 @@ async function runIndexerWithStore(store, opts) {
7902
8110
  await yieldEventLoop();
7903
8111
  throwIfAborted(signal);
7904
8112
  }
7905
- let stat10;
8113
+ let stat11;
7906
8114
  try {
7907
8115
  const statOpts = signal ? { signal } : {};
7908
- stat10 = await fs4.stat(file, statOpts);
8116
+ stat11 = await fs4.stat(file, statOpts);
7909
8117
  } catch (e) {
7910
8118
  if (isAbortError(e)) throw e;
7911
8119
  store.deleteFile(file);
7912
8120
  continue;
7913
8121
  }
7914
- if (!stat10.isFile()) continue;
8122
+ if (!stat11.isFile()) continue;
7915
8123
  const lang = detectLang(file);
7916
8124
  if (!lang) continue;
7917
8125
  const meta = existingMeta.get(file);
7918
- if (!force && meta && meta.mtimeMs === Math.floor(stat10.mtimeMs)) {
8126
+ if (!force && meta && meta.mtimeMs === Math.floor(stat11.mtimeMs)) {
7919
8127
  langStats[lang] = (langStats[lang] ?? 0) + meta.symbolCount;
7920
8128
  symbolsIndexed += meta.symbolCount;
7921
8129
  filesIndexed++;
@@ -7942,7 +8150,7 @@ async function runIndexerWithStore(store, opts) {
7942
8150
  store.upsertFile({
7943
8151
  file,
7944
8152
  lang,
7945
- mtimeMs: Math.floor(stat10.mtimeMs),
8153
+ mtimeMs: Math.floor(stat11.mtimeMs),
7946
8154
  symbolCount: 0,
7947
8155
  lastIndexed: Date.now()
7948
8156
  });
@@ -7968,7 +8176,7 @@ async function runIndexerWithStore(store, opts) {
7968
8176
  store.upsertFile({
7969
8177
  file,
7970
8178
  lang,
7971
- mtimeMs: Math.floor(stat10.mtimeMs),
8179
+ mtimeMs: Math.floor(stat11.mtimeMs),
7972
8180
  symbolCount: count,
7973
8181
  lastIndexed: Date.now()
7974
8182
  });
@@ -8249,6 +8457,13 @@ function debounceKey(indexDir, file) {
8249
8457
  function isIndexableFile(filePath) {
8250
8458
  return detectLang(filePath) !== null;
8251
8459
  }
8460
+ function isUniqueConstraintError(err) {
8461
+ if (err instanceof Error) {
8462
+ const msg = err.message.toLowerCase();
8463
+ return msg.includes("unique constraint") || msg.includes("UNIQUE constraint");
8464
+ }
8465
+ return false;
8466
+ }
8252
8467
  async function runStartupIndex(opts) {
8253
8468
  if (!indexCircuitBreaker.allowRequest()) throw circuitOpenError();
8254
8469
  _indexing = true;
@@ -8278,6 +8493,15 @@ async function runStartupIndex(opts) {
8278
8493
  return result;
8279
8494
  } catch (err) {
8280
8495
  _lastError = err instanceof Error ? err.message : String(err);
8496
+ if (isUniqueConstraintError(err) && !opts.force) {
8497
+ _lastError = null;
8498
+ const rebuildResult = await runStartupIndex({
8499
+ ...opts,
8500
+ force: true
8501
+ });
8502
+ _ready = true;
8503
+ return rebuildResult;
8504
+ }
8281
8505
  _ready = true;
8282
8506
  if (!opts.signal?.aborted) indexCircuitBreaker.recordFailure(err);
8283
8507
  throw err;
@@ -8884,6 +9108,34 @@ ${formatTaskList(file.tasks)}` : file.tasks.length > 0 ? formatTaskList(file.tas
8884
9108
  };
8885
9109
 
8886
9110
  // src/builtin.ts
9111
+ var OPTIONAL_TOOLS = [
9112
+ installTool,
9113
+ auditTool,
9114
+ outdatedTool,
9115
+ logsTool,
9116
+ documentTool,
9117
+ scaffoldTool,
9118
+ toolSearchTool,
9119
+ toolUseTool,
9120
+ batchToolUseTool,
9121
+ toolHelpTool,
9122
+ codebaseIndexTool,
9123
+ codebaseSearchTool,
9124
+ codebaseStatsTool,
9125
+ setWorkingDirTool
9126
+ ];
9127
+ var TIER1_TOOLS = [
9128
+ readTool,
9129
+ writeTool,
9130
+ editTool,
9131
+ bashTool,
9132
+ grepTool,
9133
+ globTool,
9134
+ diffTool,
9135
+ patchTool,
9136
+ jsonTool,
9137
+ searchTool
9138
+ ];
8887
9139
  var builtinTools = [
8888
9140
  readTool,
8889
9141
  writeTool,
@@ -8930,6 +9182,6 @@ var builtinToolsPack = {
8930
9182
  tools: builtinTools
8931
9183
  };
8932
9184
 
8933
- export { CircuitBreaker, CircuitOpenError, IndexCircuitBreaker, IndexTimeoutError, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetIndexCircuitBreaker, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
9185
+ export { CircuitBreaker, CircuitOpenError, IndexCircuitBreaker, IndexTimeoutError, OPTIONAL_TOOLS, TIER1_TOOLS, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetIndexCircuitBreaker, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
8934
9186
  //# sourceMappingURL=index.js.map
8935
9187
  //# sourceMappingURL=index.js.map