@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/pack.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
2
2
  import * as Core from '@wrongstack/core';
3
- import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, loadTasks, emptyTaskFile, saveTasks, formatTaskList, formatPlan, mutateTasks, loadPlan, emptyPlan, savePlan, computeTaskItemProgress, resolveWstackPaths, truncate } from '@wrongstack/core';
3
+ import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, loadTasks, emptyTaskFile, saveTasks, formatTaskList, formatPlan, mutateTasks, loadPlan, emptyPlan, savePlan, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
4
4
  import * as fs from 'node:fs';
5
- import { statSync, writeFileSync, mkdirSync } from 'node:fs';
6
- import * as path2 from 'node:path';
7
- import { resolve, sep, dirname, join } from 'node:path';
5
+ import { statSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
8
6
  import * as fs14 from 'node:fs/promises';
7
+ import * as path3 from 'node:path';
8
+ import { resolve, sep, dirname, join } from 'node:path';
9
9
  import * as os from 'node:os';
10
10
  import { createRequire } from 'node:module';
11
11
  import { fileURLToPath } from 'node:url';
@@ -14,373 +14,109 @@ import * as ts from 'typescript';
14
14
  import * as dns from 'node:dns/promises';
15
15
  import * as net from 'node:net';
16
16
  import { Agent } from 'undici';
17
- import { randomUUID } from 'node:crypto';
18
-
19
- // src/_spawn-stream.ts
20
- function resolveWin32Command(cmd) {
21
- if (process.platform !== "win32") return cmd;
22
- if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd)) {
23
- return cmd;
24
- }
25
- const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
26
- const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
27
- for (const dir of pathDirs) {
28
- const base = path2.join(dir, cmd);
29
- for (const ext of pathext) {
30
- const full = `${base}${ext}`;
31
- try {
32
- fs.accessSync(full, fs.constants.X_OK);
33
- return full;
34
- } catch {
35
- }
36
- }
37
- }
38
- return cmd;
39
- }
40
-
41
- // src/_spawn-stream.ts
42
- async function* spawnStream(opts) {
43
- const max = opts.maxBytes ?? 2e5;
44
- const flushAt = opts.flushBytes ?? 4 * 1024;
45
- const maxQueue = opts.maxQueueSize ?? 500;
46
- let stdout = "";
47
- let stderr = "";
48
- let pending2 = "";
49
- let error;
50
- const cmd = resolveWin32Command(opts.cmd);
51
- const needsShell = process.platform === "win32" && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
52
- const child = spawn(cmd, opts.args, {
53
- cwd: opts.cwd,
54
- signal: opts.signal,
55
- env: buildChildEnv(),
56
- stdio: ["ignore", "pipe", "pipe"],
57
- windowsHide: true,
58
- ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
59
- });
60
- const queue = [];
61
- let waiter;
62
- let paused = false;
63
- const wake = () => {
64
- if (waiter) {
65
- const w = waiter;
66
- waiter = void 0;
67
- w();
68
- }
69
- };
70
- const resume = () => {
71
- if (paused && queue.length < maxQueue) {
72
- paused = false;
73
- child.stdout?.resume();
74
- child.stderr?.resume();
75
- }
76
- };
77
- child.stdout?.on("data", (c) => {
78
- const s = c.toString();
79
- if (stdout.length < max) stdout += s;
80
- queue.push({ kind: "out", data: s });
81
- wake();
82
- if (!paused && queue.length >= maxQueue) {
83
- paused = true;
84
- child.stdout?.pause();
85
- child.stderr?.pause();
86
- }
87
- });
88
- child.stderr?.on("data", (c) => {
89
- const s = c.toString();
90
- if (stderr.length < max) stderr += s;
91
- queue.push({ kind: "err", data: s });
92
- wake();
93
- if (!paused && queue.length >= maxQueue) {
94
- paused = true;
95
- child.stdout?.pause();
96
- child.stderr?.pause();
97
- }
98
- });
99
- child.on("error", (e) => {
100
- error = e.message;
101
- queue.push({ kind: "error", data: e.message });
102
- wake();
103
- });
104
- child.on("close", (code) => {
105
- queue.push({ kind: "close", data: "", code: code ?? 0 });
106
- wake();
107
- });
108
- let exitCode = 0;
109
- let spawnFailed = false;
110
- for (; ; ) {
111
- while (queue.length === 0) {
112
- await new Promise((resolve7) => {
113
- waiter = resolve7;
114
- });
115
- }
116
- const chunk = queue.shift();
117
- resume();
118
- if (chunk.kind === "close") {
119
- if (!spawnFailed) exitCode = chunk.code ?? 0;
120
- break;
121
- }
122
- if (chunk.kind === "error") {
123
- spawnFailed = true;
124
- exitCode = 1;
125
- continue;
126
- }
127
- pending2 += chunk.data;
128
- if (pending2.length >= flushAt) {
129
- yield { type: "partial_output", text: pending2 };
130
- pending2 = "";
131
- }
132
- }
133
- if (pending2.length > 0) {
134
- yield { type: "partial_output", text: pending2 };
135
- }
136
- return {
137
- stdout,
138
- stderr,
139
- exitCode,
140
- truncated: stdout.length >= max || stderr.length >= max,
141
- error
142
- };
143
- }
144
- async function detectPackageManager(cwd) {
145
- const { stat: stat10 } = await import('node:fs/promises');
146
- try {
147
- await stat10(`${cwd}/pnpm-lock.yaml`);
148
- return "pnpm";
149
- } catch {
150
- }
151
- try {
152
- await stat10(`${cwd}/yarn.lock`);
153
- return "yarn";
154
- } catch {
155
- }
156
- return "npm";
157
- }
158
- function resolvePath(input, ctx) {
159
- return path2.isAbsolute(input) ? path2.normalize(input) : path2.resolve(ctx.workingDir ?? ctx.cwd, input);
160
- }
161
- function ensureInsideRoot(absPath, ctx) {
162
- const root = path2.resolve(ctx.projectRoot);
163
- const target = path2.resolve(absPath);
164
- const rel = path2.relative(root, target);
165
- if (rel.startsWith("..") || path2.isAbsolute(rel)) {
166
- throw new Error(`Path "${absPath}" is outside project root "${root}"`);
167
- }
168
- return target;
169
- }
170
- function safeResolve(input, ctx) {
171
- return ensureInsideRoot(resolvePath(input, ctx), ctx);
172
- }
173
- async function assertRealInsideRoot(absPath, ctx) {
174
- const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => path2.resolve(ctx.projectRoot));
175
- let probe = absPath;
176
- for (; ; ) {
177
- let real;
178
- try {
179
- real = await fs14.realpath(probe);
180
- } catch (err) {
181
- if (err.code === "ENOENT") {
182
- const parent = path2.dirname(probe);
183
- if (parent === probe) return;
184
- probe = parent;
185
- continue;
186
- }
187
- throw err;
188
- }
189
- const rel = path2.relative(realRoot, real);
190
- if (rel.startsWith("..") || path2.isAbsolute(rel)) {
191
- throw new Error(
192
- `Path "${absPath}" resolves through a symlink outside project root "${realRoot}"`
193
- );
194
- }
195
- return;
196
- }
197
- }
198
- async function safeResolveReal(input, ctx) {
199
- const abs = safeResolve(input, ctx);
200
- await assertRealInsideRoot(abs, ctx);
201
- return abs;
202
- }
203
- function truncateMiddle(s, max) {
204
- if (Buffer.byteLength(s, "utf8") <= max) return s;
205
- const half = Math.floor(max / 2);
206
- return s.slice(0, half) + `
207
- \u2026[truncated ${Buffer.byteLength(s, "utf8") - max} bytes from middle]\u2026
208
- ` + s.slice(-half);
209
- }
210
- function isBinaryBuffer(buf) {
211
- const len = Math.min(buf.length, 8192);
212
- for (let i = 0; i < len; i++) {
213
- if (buf[i] === 0) return true;
214
- }
215
- return false;
216
- }
217
- var COMMAND_OUTPUT_MAX_BYTES = 32768;
218
- var REPEAT_RUN_THRESHOLD = 3;
219
- function collapseCarriageReturns(text) {
220
- const lf = text.replace(/\r\n/g, "\n");
221
- if (!lf.includes("\r")) return lf;
222
- return lf.split("\n").map((line) => line.includes("\r") ? line.slice(line.lastIndexOf("\r") + 1) : line).join("\n");
223
- }
224
- function collapseConsecutiveDuplicates(text, minRun = REPEAT_RUN_THRESHOLD) {
225
- const lines = text.split("\n");
226
- const out = [];
227
- let i = 0;
228
- while (i < lines.length) {
229
- let j = i + 1;
230
- while (j < lines.length && lines[j] === lines[i]) j++;
231
- const run = j - i;
232
- if (run >= minRun) {
233
- out.push(lines[i], `\u2026 \u27E8repeated ${run}\xD7\u27E9`);
234
- } else {
235
- for (let k = i; k < j; k++) out.push(lines[k]);
236
- }
237
- i = j;
238
- }
239
- return out.join("\n");
240
- }
241
- function takeHeadBytes(s, maxBytes) {
242
- if (maxBytes <= 0) return "";
243
- if (Buffer.byteLength(s, "utf8") <= maxBytes) return s;
244
- let lo = 0;
245
- let hi = s.length;
246
- while (lo < hi) {
247
- const mid = Math.ceil((lo + hi) / 2);
248
- if (Buffer.byteLength(s.slice(0, mid), "utf8") <= maxBytes) lo = mid;
249
- else hi = mid - 1;
250
- }
251
- return s.slice(0, lo);
252
- }
253
- function takeTailBytes(s, maxBytes) {
254
- if (maxBytes <= 0) return "";
255
- if (Buffer.byteLength(s, "utf8") <= maxBytes) return s;
256
- let lo = 0;
257
- let hi = s.length;
258
- while (lo < hi) {
259
- const mid = Math.ceil((lo + hi) / 2);
260
- if (Buffer.byteLength(s.slice(s.length - mid), "utf8") <= maxBytes) lo = mid;
261
- else hi = mid - 1;
262
- }
263
- return s.slice(s.length - lo);
264
- }
265
- function truncateHeadTail(s, maxBytes) {
266
- const total = Buffer.byteLength(s, "utf8");
267
- if (total <= maxBytes) return s;
268
- const MARKER_RESERVE = 64;
269
- const avail = Math.max(0, maxBytes - MARKER_RESERVE);
270
- const headBudget = Math.floor(avail * 0.45);
271
- const head = takeHeadBytes(s, headBudget);
272
- const tail = takeTailBytes(s, avail - Buffer.byteLength(head, "utf8"));
273
- const kept = Buffer.byteLength(head, "utf8") + Buffer.byteLength(tail, "utf8");
274
- return `${head}
275
- \u2026[truncated ${total - kept} bytes]\u2026
276
- ${tail}`;
277
- }
278
- function normalizeCommandOutput(raw, opts = {}) {
279
- if (!raw) return raw;
280
- let text = Core.stripAnsi(raw);
281
- text = collapseCarriageReturns(text);
282
- text = text.replace(/[ \t]+$/gm, "");
283
- text = collapseConsecutiveDuplicates(text);
284
- text = text.replace(/\n{3,}/g, "\n\n");
285
- return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);
286
- }
287
-
288
- // src/audit.ts
289
- var auditTool = {
290
- name: "audit",
291
- category: "Package Management",
292
- description: "Run a security audit against project dependencies (using pnpm/npm audit). Reports known vulnerabilities with severity.",
293
- 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.",
294
- permission: "confirm",
295
- mutating: false,
296
- timeoutMs: 6e4,
297
- inputSchema: {
298
- type: "object",
299
- properties: {
300
- cwd: { type: "string", description: "Working directory (default: cwd)" },
301
- level: {
302
- type: "string",
303
- enum: ["low", "moderate", "high", "critical"],
304
- description: "Minimum severity level to report"
305
- },
306
- fix: { type: "boolean", description: "Attempt to fix vulnerabilities (default: false)" },
307
- packages: { type: "string", description: "Specific package(s) to audit (comma-separated)" }
308
- }
309
- },
310
- async execute(input, ctx, opts) {
311
- let final;
312
- const executeStream = auditTool.executeStream;
313
- if (!executeStream) throw new Error("auditTool: stream execution unavailable");
314
- for await (const ev of executeStream(input, ctx, opts)) {
315
- if (ev.type === "final") final = ev.output;
316
- }
317
- if (!final) throw new Error("audit: stream ended without final event");
318
- return final;
319
- },
320
- async *executeStream(input, ctx, opts) {
321
- const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
322
- const manager = await detectPackageManager(cwd);
323
- yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
324
- const args = ["audit", "--json"];
325
- if (input.fix) args.push("--fix");
326
- if (input.packages) {
327
- const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(",");
328
- args.push(...pkgs.map((p) => p.trim()));
17
+ import { randomUUID } from 'node:crypto';
18
+
19
+ // src/_spawn-stream.ts
20
+ var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
21
+ var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
22
+ var sweepStarted = false;
23
+ function toolOutputDir() {
24
+ return path3.join(wstackGlobalRoot(), "tool-output");
25
+ }
26
+ function sweepOldSpoolFiles(dir) {
27
+ if (sweepStarted) return;
28
+ sweepStarted = true;
29
+ void (async () => {
30
+ try {
31
+ const now = Date.now();
32
+ for (const name of await fs14.readdir(dir)) {
33
+ if (!name.endsWith(".log")) continue;
34
+ const p = path3.join(dir, name);
35
+ try {
36
+ const st = await fs14.stat(p);
37
+ if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fs14.unlink(p);
38
+ } catch {
39
+ }
40
+ }
41
+ } catch {
329
42
  }
330
- const result = yield* spawnStream({
331
- cmd: manager,
332
- args,
333
- cwd,
334
- signal: opts.signal,
335
- maxBytes: 1e5
336
- });
337
- yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
338
- }
339
- };
340
- function parseAuditOutput(json, exitCode) {
341
- if (!json) {
342
- return {
343
- exit_code: exitCode,
344
- vulnerabilities: [],
345
- total: 0,
346
- summary: exitCode === 0 ? "No vulnerabilities found" : "Audit failed",
347
- output: "",
348
- truncated: false
349
- };
350
- }
351
- try {
352
- const data = JSON.parse(json);
353
- const advisories = [];
354
- const ads = data.advisories ?? {};
355
- for (const id of Object.keys(ads)) {
356
- const adv = ads[id];
357
- advisories.push({
358
- severity: adv.severity ?? "unknown",
359
- package: adv.module_name ?? id,
360
- title: adv.title ?? "Unknown vulnerability",
361
- url: adv.url ?? ""
43
+ })();
44
+ }
45
+ function spoolNote(info) {
46
+ const dropped = info.droppedBytes > 0 ? `, ~${info.droppedBytes} bytes dropped under backpressure` : "";
47
+ return `
48
+ [output truncated \u2014 full ${info.bytes} bytes at ${info.path}${dropped}; read/grep that file selectively instead of re-running with more output]`;
49
+ }
50
+ function createOutputSpool(opts) {
51
+ const threshold = opts.thresholdBytes ?? 32768;
52
+ const safeTool = opts.tool.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
53
+ let head = "";
54
+ let headBytes = 0;
55
+ let totalBytes = 0;
56
+ let droppedBytes = 0;
57
+ let stream = null;
58
+ let filePath = null;
59
+ let failed = false;
60
+ let finalized = false;
61
+ const open = () => {
62
+ if (stream || failed) return;
63
+ try {
64
+ const dir = toolOutputDir();
65
+ mkdirSync(dir, { recursive: true });
66
+ sweepOldSpoolFiles(dir);
67
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
68
+ const rand = Math.random().toString(36).slice(2, 6);
69
+ filePath = path3.join(dir, `${stamp}-${safeTool}-${rand}.log`);
70
+ stream = createWriteStream(filePath, { flags: "w", encoding: "utf8" });
71
+ stream.on("error", () => {
72
+ failed = true;
73
+ stream = null;
74
+ filePath = null;
362
75
  });
76
+ stream.write(head);
77
+ } catch {
78
+ failed = true;
79
+ stream = null;
80
+ filePath = null;
363
81
  }
364
- const total = advisories.length;
365
- const summary = total === 0 ? "No vulnerabilities found" : `Found ${total} vulnerabilities: ${advisories.filter((a) => a.severity === "critical").length} critical, ${advisories.filter((a) => a.severity === "high").length} high`;
366
- return {
367
- exit_code: exitCode,
368
- vulnerabilities: advisories,
369
- total,
370
- summary,
371
- output: json,
372
- truncated: json.length >= 1e5
373
- };
374
- } catch {
375
- return {
376
- exit_code: exitCode,
377
- vulnerabilities: [],
378
- total: 0,
379
- summary: "Could not parse audit output",
380
- output: json,
381
- truncated: false
382
- };
383
- }
82
+ };
83
+ return {
84
+ write(text) {
85
+ if (finalized || !text) return;
86
+ totalBytes += Buffer.byteLength(text, "utf8");
87
+ if (!stream && !failed) {
88
+ if (headBytes + text.length <= threshold) {
89
+ head += text;
90
+ headBytes += text.length;
91
+ return;
92
+ }
93
+ head += text;
94
+ open();
95
+ head = "";
96
+ return;
97
+ }
98
+ if (stream) {
99
+ if (stream.writableLength > SPOOL_WRITE_HWM_BYTES) {
100
+ droppedBytes += Buffer.byteLength(text, "utf8");
101
+ return;
102
+ }
103
+ stream.write(text);
104
+ }
105
+ },
106
+ finalize() {
107
+ if (finalized) {
108
+ return filePath ? { path: filePath, bytes: totalBytes, droppedBytes } : null;
109
+ }
110
+ finalized = true;
111
+ head = "";
112
+ if (!stream || !filePath) return null;
113
+ try {
114
+ stream.end();
115
+ } catch {
116
+ }
117
+ return { path: filePath, bytes: totalBytes, droppedBytes };
118
+ }
119
+ };
384
120
  }
385
121
 
386
122
  // src/circuit-breaker.ts
@@ -726,43 +462,465 @@ var ProcessRegistryImpl = class {
726
462
  }, graceMs);
727
463
  timer.unref?.();
728
464
  }
729
- } catch {
465
+ } catch {
466
+ }
467
+ p.killed = true;
468
+ return true;
469
+ }
470
+ /**
471
+ * Kill all tracked processes.
472
+ * Returns the PIDs that were kill()ed.
473
+ */
474
+ killAll(opts = {}) {
475
+ const pids = Array.from(this.processes.keys());
476
+ const killed = [];
477
+ for (const pid of pids) {
478
+ const p = this.processes.get(pid);
479
+ if (p && !p.protected && this.kill(pid, opts)) killed.push(pid);
480
+ }
481
+ return killed;
482
+ }
483
+ /**
484
+ * Kill all processes for a specific session.
485
+ * Returns the PIDs that were kill()ed.
486
+ */
487
+ killSession(sessionId, opts = {}) {
488
+ const pids = this.bySession(sessionId).map((p) => p.pid);
489
+ const killed = [];
490
+ for (const pid of pids) {
491
+ if (this.kill(pid, opts)) killed.push(pid);
492
+ }
493
+ return killed;
494
+ }
495
+ };
496
+ var _registry;
497
+ function getProcessRegistry() {
498
+ if (!_registry) {
499
+ _registry = new ProcessRegistryImpl();
500
+ }
501
+ return _registry;
502
+ }
503
+ function resolveWin32Command(cmd) {
504
+ if (process.platform !== "win32") return cmd;
505
+ if (cmd.includes("/") || cmd.includes("\\") || path3.extname(cmd)) {
506
+ return cmd;
507
+ }
508
+ const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
509
+ const pathDirs = (process.env["PATH"] ?? "").split(path3.delimiter);
510
+ for (const dir of pathDirs) {
511
+ const base = path3.join(dir, cmd);
512
+ for (const ext of pathext) {
513
+ const full = `${base}${ext}`;
514
+ try {
515
+ fs.accessSync(full, fs.constants.X_OK);
516
+ return full;
517
+ } catch {
518
+ }
519
+ }
520
+ }
521
+ return cmd;
522
+ }
523
+
524
+ // src/_spawn-stream.ts
525
+ async function* spawnStream(opts) {
526
+ const max = opts.maxBytes ?? 2e5;
527
+ const flushAt = opts.flushBytes ?? 4 * 1024;
528
+ const maxQueue = opts.maxQueueSize ?? 500;
529
+ let stdout = "";
530
+ let stderr = "";
531
+ let pending2 = "";
532
+ let error;
533
+ const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
534
+ const cmd = resolveWin32Command(opts.cmd);
535
+ const isWin = process.platform === "win32";
536
+ const needsShell = isWin && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
537
+ const child = spawn(cmd, opts.args, {
538
+ cwd: opts.cwd,
539
+ env: buildChildEnv(),
540
+ stdio: ["ignore", "pipe", "pipe"],
541
+ windowsHide: true,
542
+ ...isWin ? {} : { signal: opts.signal },
543
+ ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
544
+ });
545
+ const registry = getProcessRegistry();
546
+ const pid = child.pid;
547
+ if (typeof pid === "number") {
548
+ registry.register({
549
+ pid,
550
+ name: opts.cmd,
551
+ command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
552
+ startedAt: Date.now(),
553
+ child
554
+ });
555
+ }
556
+ const queue = [];
557
+ let waiter;
558
+ let paused = false;
559
+ const wake = () => {
560
+ if (waiter) {
561
+ const w = waiter;
562
+ waiter = void 0;
563
+ w();
564
+ }
565
+ };
566
+ const resume = () => {
567
+ if (paused && queue.length < maxQueue) {
568
+ paused = false;
569
+ child.stdout?.resume();
570
+ child.stderr?.resume();
571
+ }
572
+ };
573
+ const onOut = (c) => {
574
+ const s = c.toString();
575
+ if (stdout.length < max) stdout += s;
576
+ spool.write(s);
577
+ queue.push({ kind: "out", data: s });
578
+ wake();
579
+ if (!paused && queue.length >= maxQueue) {
580
+ paused = true;
581
+ child.stdout?.pause();
582
+ child.stderr?.pause();
583
+ }
584
+ };
585
+ const onErr = (c) => {
586
+ const s = c.toString();
587
+ if (stderr.length < max) stderr += s;
588
+ spool.write(s);
589
+ queue.push({ kind: "err", data: s });
590
+ wake();
591
+ if (!paused && queue.length >= maxQueue) {
592
+ paused = true;
593
+ child.stdout?.pause();
594
+ child.stderr?.pause();
595
+ }
596
+ };
597
+ child.stdout?.on("data", onOut);
598
+ child.stderr?.on("data", onErr);
599
+ child.on("error", (e) => {
600
+ error = e.message;
601
+ queue.push({ kind: "error", data: e.message });
602
+ wake();
603
+ });
604
+ child.on("close", (code) => {
605
+ if (typeof pid === "number") registry.unregister(pid);
606
+ queue.push({ kind: "close", data: "", code: code ?? 0 });
607
+ wake();
608
+ });
609
+ const onAbort = () => {
610
+ if (typeof pid === "number") {
611
+ registry.kill(pid, { force: true });
612
+ } else {
613
+ try {
614
+ child.kill("SIGKILL");
615
+ } catch {
616
+ }
617
+ }
618
+ queue.push({ kind: "close", data: "", code: 124 });
619
+ wake();
620
+ };
621
+ if (opts.signal.aborted) onAbort();
622
+ else opts.signal.addEventListener("abort", onAbort, { once: true });
623
+ let exitCode = 0;
624
+ let spawnFailed = false;
625
+ try {
626
+ for (; ; ) {
627
+ while (queue.length === 0) {
628
+ await new Promise((resolve7) => {
629
+ waiter = resolve7;
630
+ });
631
+ }
632
+ const chunk = queue.shift();
633
+ resume();
634
+ if (chunk.kind === "close") {
635
+ if (!spawnFailed) exitCode = chunk.code ?? 0;
636
+ break;
637
+ }
638
+ if (chunk.kind === "error") {
639
+ spawnFailed = true;
640
+ exitCode = 1;
641
+ continue;
642
+ }
643
+ pending2 += chunk.data;
644
+ if (pending2.length >= flushAt) {
645
+ yield { type: "partial_output", text: pending2 };
646
+ pending2 = "";
647
+ }
648
+ }
649
+ if (pending2.length > 0) {
650
+ yield { type: "partial_output", text: pending2 };
651
+ }
652
+ const spooled = spool.finalize();
653
+ return {
654
+ // The marker rides on stdout's tail so every consumer's head+tail
655
+ // normalization keeps it without per-tool changes.
656
+ stdout: spooled ? stdout + spoolNote(spooled) : stdout,
657
+ stderr,
658
+ exitCode,
659
+ truncated: stdout.length >= max || stderr.length >= max,
660
+ error,
661
+ spoolPath: spooled?.path,
662
+ spoolBytes: spooled?.bytes
663
+ };
664
+ } finally {
665
+ spool.finalize();
666
+ opts.signal.removeEventListener("abort", onAbort);
667
+ child.stdout?.off("data", onOut);
668
+ child.stderr?.off("data", onErr);
669
+ child.stdout?.destroy();
670
+ child.stderr?.destroy();
671
+ if (child.exitCode === null && !child.killed) {
672
+ if (typeof pid === "number") {
673
+ registry.kill(pid, { force: true });
674
+ } else {
675
+ try {
676
+ child.kill("SIGKILL");
677
+ } catch {
678
+ }
679
+ }
680
+ }
681
+ }
682
+ }
683
+ async function detectPackageManager(cwd) {
684
+ const { stat: stat11 } = await import('node:fs/promises');
685
+ try {
686
+ await stat11(`${cwd}/pnpm-lock.yaml`);
687
+ return "pnpm";
688
+ } catch {
689
+ }
690
+ try {
691
+ await stat11(`${cwd}/yarn.lock`);
692
+ return "yarn";
693
+ } catch {
694
+ }
695
+ return "npm";
696
+ }
697
+ function resolvePath(input, ctx) {
698
+ return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
699
+ }
700
+ function ensureInsideRoot(absPath, ctx) {
701
+ const root = path3.resolve(ctx.projectRoot);
702
+ const target = path3.resolve(absPath);
703
+ const rel = path3.relative(root, target);
704
+ if (rel.startsWith("..") || path3.isAbsolute(rel)) {
705
+ throw new Error(`Path "${absPath}" is outside project root "${root}"`);
706
+ }
707
+ return target;
708
+ }
709
+ function safeResolve(input, ctx) {
710
+ return ensureInsideRoot(resolvePath(input, ctx), ctx);
711
+ }
712
+ async function assertRealInsideRoot(absPath, ctx) {
713
+ const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => path3.resolve(ctx.projectRoot));
714
+ let probe = absPath;
715
+ for (; ; ) {
716
+ let real;
717
+ try {
718
+ real = await fs14.realpath(probe);
719
+ } catch (err) {
720
+ if (err.code === "ENOENT") {
721
+ const parent = path3.dirname(probe);
722
+ if (parent === probe) return;
723
+ probe = parent;
724
+ continue;
725
+ }
726
+ throw err;
730
727
  }
731
- p.killed = true;
732
- return true;
728
+ const rel = path3.relative(realRoot, real);
729
+ if (rel.startsWith("..") || path3.isAbsolute(rel)) {
730
+ throw new Error(
731
+ `Path "${absPath}" resolves through a symlink outside project root "${realRoot}"`
732
+ );
733
+ }
734
+ return;
733
735
  }
734
- /**
735
- * Kill all tracked processes.
736
- * Returns the PIDs that were kill()ed.
737
- */
738
- killAll(opts = {}) {
739
- const pids = Array.from(this.processes.keys());
740
- const killed = [];
741
- for (const pid of pids) {
742
- const p = this.processes.get(pid);
743
- if (p && !p.protected && this.kill(pid, opts)) killed.push(pid);
736
+ }
737
+ async function safeResolveReal(input, ctx) {
738
+ const abs = safeResolve(input, ctx);
739
+ await assertRealInsideRoot(abs, ctx);
740
+ return abs;
741
+ }
742
+ function truncateMiddle(s, max) {
743
+ if (Buffer.byteLength(s, "utf8") <= max) return s;
744
+ const half = Math.floor(max / 2);
745
+ return s.slice(0, half) + `
746
+ \u2026[truncated ${Buffer.byteLength(s, "utf8") - max} bytes from middle]\u2026
747
+ ` + s.slice(-half);
748
+ }
749
+ function isBinaryBuffer(buf) {
750
+ const len = Math.min(buf.length, 8192);
751
+ for (let i = 0; i < len; i++) {
752
+ if (buf[i] === 0) return true;
753
+ }
754
+ return false;
755
+ }
756
+ var COMMAND_OUTPUT_MAX_BYTES = 32768;
757
+ var REPEAT_RUN_THRESHOLD = 3;
758
+ function collapseCarriageReturns(text) {
759
+ const lf = text.replace(/\r\n/g, "\n");
760
+ if (!lf.includes("\r")) return lf;
761
+ return lf.split("\n").map((line) => line.includes("\r") ? line.slice(line.lastIndexOf("\r") + 1) : line).join("\n");
762
+ }
763
+ function collapseConsecutiveDuplicates(text, minRun = REPEAT_RUN_THRESHOLD) {
764
+ const lines = text.split("\n");
765
+ const out = [];
766
+ let i = 0;
767
+ while (i < lines.length) {
768
+ let j = i + 1;
769
+ while (j < lines.length && lines[j] === lines[i]) j++;
770
+ const run = j - i;
771
+ if (run >= minRun) {
772
+ out.push(lines[i], `\u2026 \u27E8repeated ${run}\xD7\u27E9`);
773
+ } else {
774
+ for (let k = i; k < j; k++) out.push(lines[k]);
744
775
  }
745
- return killed;
776
+ i = j;
746
777
  }
747
- /**
748
- * Kill all processes for a specific session.
749
- * Returns the PIDs that were kill()ed.
750
- */
751
- killSession(sessionId, opts = {}) {
752
- const pids = this.bySession(sessionId).map((p) => p.pid);
753
- const killed = [];
754
- for (const pid of pids) {
755
- if (this.kill(pid, opts)) killed.push(pid);
778
+ return out.join("\n");
779
+ }
780
+ function takeHeadBytes(s, maxBytes) {
781
+ if (maxBytes <= 0) return "";
782
+ if (Buffer.byteLength(s, "utf8") <= maxBytes) return s;
783
+ let lo = 0;
784
+ let hi = s.length;
785
+ while (lo < hi) {
786
+ const mid = Math.ceil((lo + hi) / 2);
787
+ if (Buffer.byteLength(s.slice(0, mid), "utf8") <= maxBytes) lo = mid;
788
+ else hi = mid - 1;
789
+ }
790
+ return s.slice(0, lo);
791
+ }
792
+ function takeTailBytes(s, maxBytes) {
793
+ if (maxBytes <= 0) return "";
794
+ if (Buffer.byteLength(s, "utf8") <= maxBytes) return s;
795
+ let lo = 0;
796
+ let hi = s.length;
797
+ while (lo < hi) {
798
+ const mid = Math.ceil((lo + hi) / 2);
799
+ if (Buffer.byteLength(s.slice(s.length - mid), "utf8") <= maxBytes) lo = mid;
800
+ else hi = mid - 1;
801
+ }
802
+ return s.slice(s.length - lo);
803
+ }
804
+ function truncateHeadTail(s, maxBytes) {
805
+ const total = Buffer.byteLength(s, "utf8");
806
+ if (total <= maxBytes) return s;
807
+ const MARKER_RESERVE = 64;
808
+ const avail = Math.max(0, maxBytes - MARKER_RESERVE);
809
+ const headBudget = Math.floor(avail * 0.45);
810
+ const head = takeHeadBytes(s, headBudget);
811
+ const tail = takeTailBytes(s, avail - Buffer.byteLength(head, "utf8"));
812
+ const kept = Buffer.byteLength(head, "utf8") + Buffer.byteLength(tail, "utf8");
813
+ return `${head}
814
+ \u2026[truncated ${total - kept} bytes]\u2026
815
+ ${tail}`;
816
+ }
817
+ function normalizeCommandOutput(raw, opts = {}) {
818
+ if (!raw) return raw;
819
+ let text = Core.stripAnsi(raw);
820
+ text = collapseCarriageReturns(text);
821
+ text = text.replace(/[ \t]+$/gm, "");
822
+ text = collapseConsecutiveDuplicates(text);
823
+ text = text.replace(/\n{3,}/g, "\n\n");
824
+ return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);
825
+ }
826
+
827
+ // src/audit.ts
828
+ var auditTool = {
829
+ name: "audit",
830
+ category: "Package Management",
831
+ description: "Run a security audit against project dependencies (using pnpm/npm audit). Reports known vulnerabilities with severity.",
832
+ 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.",
833
+ permission: "confirm",
834
+ mutating: false,
835
+ capabilities: ["shell.restricted"],
836
+ timeoutMs: 6e4,
837
+ inputSchema: {
838
+ type: "object",
839
+ properties: {
840
+ cwd: { type: "string", description: "Working directory (default: cwd)" },
841
+ level: {
842
+ type: "string",
843
+ enum: ["low", "moderate", "high", "critical"],
844
+ description: "Minimum severity level to report"
845
+ },
846
+ fix: { type: "boolean", description: "Attempt to fix vulnerabilities (default: false)" },
847
+ packages: { type: "string", description: "Specific package(s) to audit (comma-separated)" }
756
848
  }
757
- return killed;
849
+ },
850
+ async execute(input, ctx, opts) {
851
+ let final;
852
+ const executeStream = auditTool.executeStream;
853
+ if (!executeStream) throw new Error("auditTool: stream execution unavailable");
854
+ for await (const ev of executeStream(input, ctx, opts)) {
855
+ if (ev.type === "final") final = ev.output;
856
+ }
857
+ if (!final) throw new Error("audit: stream ended without final event");
858
+ return final;
859
+ },
860
+ async *executeStream(input, ctx, opts) {
861
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
862
+ const manager = await detectPackageManager(cwd);
863
+ yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
864
+ const args = ["audit", "--json"];
865
+ if (input.fix) args.push("--fix");
866
+ if (input.packages) {
867
+ const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(",");
868
+ args.push(...pkgs.map((p) => p.trim()));
869
+ }
870
+ const result = yield* spawnStream({
871
+ cmd: manager,
872
+ args,
873
+ cwd,
874
+ signal: opts.signal,
875
+ maxBytes: 1e5
876
+ });
877
+ yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
758
878
  }
759
879
  };
760
- var _registry;
761
- function getProcessRegistry() {
762
- if (!_registry) {
763
- _registry = new ProcessRegistryImpl();
880
+ function parseAuditOutput(json, exitCode) {
881
+ if (!json) {
882
+ return {
883
+ exit_code: exitCode,
884
+ vulnerabilities: [],
885
+ total: 0,
886
+ summary: exitCode === 0 ? "No vulnerabilities found" : "Audit failed",
887
+ output: "",
888
+ truncated: false
889
+ };
890
+ }
891
+ try {
892
+ const data = JSON.parse(json);
893
+ const advisories = [];
894
+ const ads = data.advisories ?? {};
895
+ for (const id of Object.keys(ads)) {
896
+ const adv = ads[id];
897
+ advisories.push({
898
+ severity: adv.severity ?? "unknown",
899
+ package: adv.module_name ?? id,
900
+ title: adv.title ?? "Unknown vulnerability",
901
+ url: adv.url ?? ""
902
+ });
903
+ }
904
+ const total = advisories.length;
905
+ const summary = total === 0 ? "No vulnerabilities found" : `Found ${total} vulnerabilities: ${advisories.filter((a) => a.severity === "critical").length} critical, ${advisories.filter((a) => a.severity === "high").length} high`;
906
+ return {
907
+ exit_code: exitCode,
908
+ vulnerabilities: advisories,
909
+ total,
910
+ summary,
911
+ output: json,
912
+ truncated: json.length >= 1e5
913
+ };
914
+ } catch {
915
+ return {
916
+ exit_code: exitCode,
917
+ vulnerabilities: [],
918
+ total: 0,
919
+ summary: "Could not parse audit output",
920
+ output: json,
921
+ truncated: false
922
+ };
764
923
  }
765
- return _registry;
766
924
  }
767
925
 
768
926
  // src/bash.ts
@@ -858,7 +1016,7 @@ var bashTool = {
858
1016
  })();
859
1017
  const args = isWin ? ["/c", input.command] : ["-c", input.command];
860
1018
  const env = buildChildEnv(ctx.session?.id);
861
- const detached = isWin ? !!input.background : true;
1019
+ const detached = !isWin;
862
1020
  const startedAt = Date.now();
863
1021
  if (input.background) {
864
1022
  let buf2 = "";
@@ -867,10 +1025,14 @@ var bashTool = {
867
1025
  cwd: ctx.projectRoot,
868
1026
  env,
869
1027
  stdio: ["ignore", "pipe", "pipe"],
870
- detached: true,
871
- // Detached console children on Windows allocate their own VISIBLE
872
- // console window (one per background command test suites flash
873
- // dozens). CREATE_NO_WINDOW suppresses it; no-op elsewhere.
1028
+ // win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
1029
+ // DETACHED_PROCESS (detached: true) is set, so the console-less
1030
+ // cmd.exe's grandchildren (node, dev servers) each allocate a fresh
1031
+ // VISIBLE console window. detached: false lets CREATE_NO_WINDOW
1032
+ // apply: the child gets a hidden console that grandchildren inherit.
1033
+ // Windows children survive parent exit either way. POSIX keeps
1034
+ // detached for the process-group kill semantics.
1035
+ detached: !isWin,
874
1036
  windowsHide: true,
875
1037
  signal: opts.signal
876
1038
  });
@@ -886,24 +1048,22 @@ var bashTool = {
886
1048
  });
887
1049
  child2.on("close", () => registry.unregister(pid2));
888
1050
  }
889
- child2.stdout?.on("data", (chunk) => {
890
- if (!truncated) {
891
- const remain = MAX_OUTPUT - buf2.length;
892
- if (remain > 0) {
893
- buf2 += chunk.toString().slice(0, remain);
894
- }
895
- if (buf2.length >= MAX_OUTPUT) truncated = true;
1051
+ const onBgData = (chunk) => {
1052
+ if (truncated) return;
1053
+ const remain = MAX_OUTPUT - buf2.length;
1054
+ if (remain > 0) {
1055
+ buf2 += chunk.toString().slice(0, remain);
896
1056
  }
897
- });
898
- child2.stderr?.on("data", (chunk) => {
899
- if (!truncated) {
900
- const remain = MAX_OUTPUT - buf2.length;
901
- if (remain > 0) {
902
- buf2 += chunk.toString().slice(0, remain);
903
- }
904
- if (buf2.length >= MAX_OUTPUT) truncated = true;
1057
+ if (buf2.length >= MAX_OUTPUT) {
1058
+ truncated = true;
1059
+ child2.stdout?.off("data", onBgData);
1060
+ child2.stderr?.off("data", onBgData);
905
1061
  }
906
- });
1062
+ };
1063
+ child2.stdout?.on("data", onBgData);
1064
+ child2.stderr?.on("data", onBgData);
1065
+ child2.stdout?.unref?.();
1066
+ child2.stderr?.unref?.();
907
1067
  child2.on("close", () => {
908
1068
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
909
1069
  });
@@ -942,6 +1102,7 @@ var bashTool = {
942
1102
  let pending2 = "";
943
1103
  let timedOut = false;
944
1104
  const timers = [];
1105
+ const spool = createOutputSpool({ tool: "bash", thresholdBytes: MAX_OUTPUT });
945
1106
  function killWithTimeout(child2, timeoutMs2) {
946
1107
  if (isWin) {
947
1108
  if (typeof child2.pid === "number" && child2.exitCode === null && killWin32Tree(child2.pid)) {
@@ -1047,6 +1208,7 @@ var bashTool = {
1047
1208
  if (buf.length < MAX_OUTPUT) {
1048
1209
  buf += text.slice(0, MAX_OUTPUT - buf.length);
1049
1210
  }
1211
+ spool.write(text);
1050
1212
  pending2 += text;
1051
1213
  push({ kind: "data", text });
1052
1214
  pauseIfFlooded();
@@ -1074,10 +1236,11 @@ var bashTool = {
1074
1236
  if (remainder !== null) {
1075
1237
  yield { type: "partial_output", text: remainder };
1076
1238
  }
1239
+ const spooled = spool.finalize();
1077
1240
  yield {
1078
1241
  type: "final",
1079
1242
  output: {
1080
- output: normalizeCommandOutput(buf),
1243
+ output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : ""),
1081
1244
  exit_code: c.code,
1082
1245
  timed_out: timedOut
1083
1246
  }
@@ -1092,6 +1255,7 @@ var bashTool = {
1092
1255
  }
1093
1256
  } finally {
1094
1257
  for (const t of timers) clearTimeout(t);
1258
+ spool.finalize();
1095
1259
  if (isWin) opts.signal.removeEventListener("abort", onAbort);
1096
1260
  child.stdout?.off("data", onData);
1097
1261
  child.stderr?.off("data", onData);
@@ -1114,6 +1278,7 @@ var batchToolUseTool = {
1114
1278
  permission: "confirm",
1115
1279
  mutating: true,
1116
1280
  timeoutMs: 12e4,
1281
+ capabilities: ["tool.mutate.any"],
1117
1282
  inputSchema: {
1118
1283
  type: "object",
1119
1284
  properties: {
@@ -1515,7 +1680,7 @@ var IndexStore = class {
1515
1680
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
1516
1681
  fs.mkdirSync(this.indexDir, { recursive: true });
1517
1682
  const Database = loadDatabaseSync();
1518
- this.db = new Database(path2.join(this.indexDir, DB_FILE));
1683
+ this.db = new Database(path3.join(this.indexDir, DB_FILE));
1519
1684
  try {
1520
1685
  this.db.exec("PRAGMA journal_mode = WAL");
1521
1686
  this.db.exec("PRAGMA busy_timeout = 5000");
@@ -1955,7 +2120,7 @@ var IndexStore = class {
1955
2120
  }));
1956
2121
  }
1957
2122
  sizeBytes() {
1958
- const dbPath = path2.join(this.indexDir, DB_FILE);
2123
+ const dbPath = path3.join(this.indexDir, DB_FILE);
1959
2124
  try {
1960
2125
  return fs.statSync(dbPath).size;
1961
2126
  } catch {
@@ -2394,10 +2559,10 @@ func formatType(t ast.Expr) string {
2394
2559
  }
2395
2560
  `;
2396
2561
  function syncGoParse(filePath, content, lang) {
2397
- const tmpDir = path2.join(os.tmpdir(), "ws-go-parse");
2562
+ const tmpDir = path3.join(os.tmpdir(), "ws-go-parse");
2398
2563
  try {
2399
2564
  mkdirSync(tmpDir, { recursive: true });
2400
- const scriptPath = path2.join(tmpDir, "parse.go");
2565
+ const scriptPath = path3.join(tmpDir, "parse.go");
2401
2566
  writeFileSync(scriptPath, GO_PARSE_SCRIPT, "utf8");
2402
2567
  const stdout = execFileSync("go", ["run", scriptPath], {
2403
2568
  input: content,
@@ -2641,9 +2806,9 @@ print(json.dumps([s.to_dict() for s in syms]))
2641
2806
  `;
2642
2807
  function syncPyParse(filePath, lang) {
2643
2808
  try {
2644
- const tmpDir = path2.join(os.tmpdir(), "ws-py-parse");
2809
+ const tmpDir = path3.join(os.tmpdir(), "ws-py-parse");
2645
2810
  mkdirSync(tmpDir, { recursive: true });
2646
- const scriptPath = path2.join(tmpDir, "parse.py");
2811
+ const scriptPath = path3.join(tmpDir, "parse.py");
2647
2812
  writeFileSync(scriptPath, PY_PARSE_SCRIPT, "utf8");
2648
2813
  const stdout = execFileSync("python", [scriptPath, filePath], {
2649
2814
  timeout: 15e3,
@@ -2684,7 +2849,7 @@ function parseSymbols4(opts) {
2684
2849
  function checkNativeParser() {
2685
2850
  try {
2686
2851
  execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
2687
- const toolsDir = path2.join(process.cwd(), "tools");
2852
+ const toolsDir = path3.join(process.cwd(), "tools");
2688
2853
  try {
2689
2854
  execFileSync(
2690
2855
  "cargo",
@@ -2694,7 +2859,7 @@ function checkNativeParser() {
2694
2859
  "--format-version",
2695
2860
  "1",
2696
2861
  "--manifest-path",
2697
- path2.join(toolsDir, "Cargo.toml")
2862
+ path3.join(toolsDir, "Cargo.toml")
2698
2863
  ],
2699
2864
  { stdio: "pipe", windowsHide: true }
2700
2865
  );
@@ -2708,13 +2873,13 @@ function checkNativeParser() {
2708
2873
  }
2709
2874
  function tryNativeParse(file, content) {
2710
2875
  try {
2711
- const toolsDir = path2.join(process.cwd(), "tools");
2712
- const crateDir = path2.join(toolsDir, "syn-parser");
2713
- const tmpFile = path2.join(crateDir, "src", "input.rs");
2876
+ const toolsDir = path3.join(process.cwd(), "tools");
2877
+ const crateDir = path3.join(toolsDir, "syn-parser");
2878
+ const tmpFile = path3.join(crateDir, "src", "input.rs");
2714
2879
  writeFileSync(tmpFile, content, "utf8");
2715
2880
  const result = spawnSync(
2716
2881
  "cargo",
2717
- ["run", "--manifest-path", path2.join(toolsDir, "Cargo.toml")],
2882
+ ["run", "--manifest-path", path3.join(toolsDir, "Cargo.toml")],
2718
2883
  {
2719
2884
  cwd: process.cwd(),
2720
2885
  encoding: "utf8",
@@ -2813,7 +2978,7 @@ function parseSymbols5(opts) {
2813
2978
  function regexParse2(opts) {
2814
2979
  const { file, content, lang } = opts;
2815
2980
  const symbols = [];
2816
- const basename2 = path2.basename(file).toLowerCase();
2981
+ const basename2 = path3.basename(file).toLowerCase();
2817
2982
  const isPackageJson = basename2 === "package.json";
2818
2983
  const isTsconfig = basename2 === "tsconfig.json" || basename2 === "tsconfig.build.json";
2819
2984
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
@@ -2839,11 +3004,11 @@ function regexParse2(opts) {
2839
3004
  const line = lineFromOffset(offset);
2840
3005
  symbols.push(
2841
3006
  makeSymbol({
2842
- name: path2.basename(file),
3007
+ name: path3.basename(file),
2843
3008
  kind: "object",
2844
3009
  line,
2845
3010
  col: 0,
2846
- signature: `"${path2.basename(file)}" = { ... }`,
3011
+ signature: `"${path3.basename(file)}" = { ... }`,
2847
3012
  file,
2848
3013
  lang
2849
3014
  })
@@ -3193,7 +3358,7 @@ function compileGitignore(lines) {
3193
3358
  async function loadGitignoreMatcher(projectRoot) {
3194
3359
  let lines = [];
3195
3360
  try {
3196
- const raw = await fs14.readFile(path2.join(projectRoot, ".gitignore"), "utf8");
3361
+ const raw = await fs14.readFile(path3.join(projectRoot, ".gitignore"), "utf8");
3197
3362
  lines = raw.split("\n");
3198
3363
  } catch {
3199
3364
  }
@@ -3257,14 +3422,14 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
3257
3422
  dirCount++;
3258
3423
  for (const e of entries) {
3259
3424
  if (ignoreSet.has(e.name)) continue;
3260
- const full = path2.join(dir, e.name);
3261
- const rel = path2.relative(projectRoot, full).replace(/\\/g, "/");
3425
+ const full = path3.join(dir, e.name);
3426
+ const rel = path3.relative(projectRoot, full).replace(/\\/g, "/");
3262
3427
  if (e.isDirectory()) {
3263
3428
  if (isGitIgnored(rel, true)) continue;
3264
3429
  await walk(full);
3265
3430
  } else if (e.isFile()) {
3266
3431
  if (isGitIgnored(rel, false)) continue;
3267
- const ext = path2.extname(e.name);
3432
+ const ext = path3.extname(e.name);
3268
3433
  for (const { ext: extName, pat } of globs) {
3269
3434
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
3270
3435
  results.push(full);
@@ -3319,7 +3484,7 @@ async function runIndexerWithStore(store, opts) {
3319
3484
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
3320
3485
  let files;
3321
3486
  if (opts.files && opts.files.length > 0) {
3322
- files = opts.files.map((f) => path2.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path2.relative(projectRoot, f).replace(/\\/g, "/"), false));
3487
+ files = opts.files.map((f) => path3.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path3.relative(projectRoot, f).replace(/\\/g, "/"), false));
3323
3488
  } else {
3324
3489
  files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
3325
3490
  }
@@ -3342,20 +3507,20 @@ async function runIndexerWithStore(store, opts) {
3342
3507
  await yieldEventLoop();
3343
3508
  throwIfAborted(signal);
3344
3509
  }
3345
- let stat10;
3510
+ let stat11;
3346
3511
  try {
3347
3512
  const statOpts = signal ? { signal } : {};
3348
- stat10 = await fs14.stat(file, statOpts);
3513
+ stat11 = await fs14.stat(file, statOpts);
3349
3514
  } catch (e) {
3350
3515
  if (isAbortError(e)) throw e;
3351
3516
  store.deleteFile(file);
3352
3517
  continue;
3353
3518
  }
3354
- if (!stat10.isFile()) continue;
3519
+ if (!stat11.isFile()) continue;
3355
3520
  const lang = detectLang(file);
3356
3521
  if (!lang) continue;
3357
3522
  const meta = existingMeta.get(file);
3358
- if (!force && meta && meta.mtimeMs === Math.floor(stat10.mtimeMs)) {
3523
+ if (!force && meta && meta.mtimeMs === Math.floor(stat11.mtimeMs)) {
3359
3524
  langStats[lang] = (langStats[lang] ?? 0) + meta.symbolCount;
3360
3525
  symbolsIndexed += meta.symbolCount;
3361
3526
  filesIndexed++;
@@ -3382,7 +3547,7 @@ async function runIndexerWithStore(store, opts) {
3382
3547
  store.upsertFile({
3383
3548
  file,
3384
3549
  lang,
3385
- mtimeMs: Math.floor(stat10.mtimeMs),
3550
+ mtimeMs: Math.floor(stat11.mtimeMs),
3386
3551
  symbolCount: 0,
3387
3552
  lastIndexed: Date.now()
3388
3553
  });
@@ -3408,7 +3573,7 @@ async function runIndexerWithStore(store, opts) {
3408
3573
  store.upsertFile({
3409
3574
  file,
3410
3575
  lang,
3411
- mtimeMs: Math.floor(stat10.mtimeMs),
3576
+ mtimeMs: Math.floor(stat11.mtimeMs),
3412
3577
  symbolCount: count,
3413
3578
  lastIndexed: Date.now()
3414
3579
  });
@@ -3666,6 +3831,13 @@ function circuitOpenError() {
3666
3831
  "Codebase indexing is temporarily paused after repeated failures" + (c.lastFailure ? ` (last: ${c.lastFailure})` : "") + (c.cooldownRemainingMs > 0 ? `; auto-retry in ${Math.ceil(c.cooldownRemainingMs / 1e3)}s` : "") + ". Use /codebase-reindex to retry now."
3667
3832
  );
3668
3833
  }
3834
+ function isUniqueConstraintError(err) {
3835
+ if (err instanceof Error) {
3836
+ const msg = err.message.toLowerCase();
3837
+ return msg.includes("unique constraint") || msg.includes("UNIQUE constraint");
3838
+ }
3839
+ return false;
3840
+ }
3669
3841
  async function runStartupIndex(opts) {
3670
3842
  if (!indexCircuitBreaker.allowRequest()) throw circuitOpenError();
3671
3843
  _indexing = true;
@@ -3695,6 +3867,15 @@ async function runStartupIndex(opts) {
3695
3867
  return result;
3696
3868
  } catch (err) {
3697
3869
  _lastError = err instanceof Error ? err.message : String(err);
3870
+ if (isUniqueConstraintError(err) && !opts.force) {
3871
+ _lastError = null;
3872
+ const rebuildResult = await runStartupIndex({
3873
+ ...opts,
3874
+ force: true
3875
+ });
3876
+ _ready = true;
3877
+ return rebuildResult;
3878
+ }
3698
3879
  _ready = true;
3699
3880
  if (!opts.signal?.aborted) indexCircuitBreaker.recordFailure(err);
3700
3881
  throw err;
@@ -3997,11 +4178,11 @@ function findGitDir(cwd) {
3997
4178
  let dir = cwd;
3998
4179
  for (let i = 0; i < 20; i++) {
3999
4180
  try {
4000
- const stat10 = statSync(path2.join(dir, ".git"));
4001
- if (stat10.isDirectory()) return dir;
4181
+ const stat11 = statSync(path3.join(dir, ".git"));
4182
+ if (stat11.isDirectory()) return dir;
4002
4183
  } catch {
4003
4184
  }
4004
- const parent = path2.dirname(dir);
4185
+ const parent = path3.dirname(dir);
4005
4186
  if (parent === dir) break;
4006
4187
  dir = parent;
4007
4188
  }
@@ -4042,8 +4223,8 @@ async function fileDiff(input, ctx, _signal) {
4042
4223
  const results = [];
4043
4224
  for (const file of files) {
4044
4225
  const absPath = safeResolve(file, ctx);
4045
- const stat10 = await fs14.stat(absPath).catch(() => null);
4046
- if (!stat10?.isFile()) continue;
4226
+ const stat11 = await fs14.stat(absPath).catch(() => null);
4227
+ if (!stat11?.isFile()) continue;
4047
4228
  const content = await fs14.readFile(absPath, "utf8");
4048
4229
  const lines = content.split(/\r?\n/);
4049
4230
  results.push(formatWithLineNumbers(file, lines));
@@ -4069,6 +4250,7 @@ var documentTool = {
4069
4250
  permission: "auto",
4070
4251
  mutating: false,
4071
4252
  timeoutMs: 3e4,
4253
+ capabilities: ["fs.read"],
4072
4254
  inputSchema: {
4073
4255
  type: "object",
4074
4256
  properties: {
@@ -4142,8 +4324,8 @@ async function resolveFiles(filesInput, cwd) {
4142
4324
  for (const f of files) {
4143
4325
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
4144
4326
  try {
4145
- const stat10 = await fs14.stat(absPath);
4146
- if (stat10.isFile()) resolved.push(absPath);
4327
+ const stat11 = await fs14.stat(absPath);
4328
+ if (stat11.isFile()) resolved.push(absPath);
4147
4329
  } catch {
4148
4330
  }
4149
4331
  }
@@ -4234,13 +4416,13 @@ var editTool = {
4234
4416
  if (input.new_string === void 0) throw new Error("edit: new_string is required");
4235
4417
  if (input.old_string === "") throw new Error("edit: old_string cannot be empty");
4236
4418
  const absPath = await safeResolveReal(input.path, ctx);
4237
- const stat10 = await fs14.stat(absPath).catch((err) => {
4419
+ const stat11 = await fs14.stat(absPath).catch((err) => {
4238
4420
  if (err.code === "ENOENT") {
4239
4421
  throw new Error(`edit: file "${input.path}" does not exist. Use \`write\` instead.`);
4240
4422
  }
4241
4423
  throw err;
4242
4424
  });
4243
- if (!stat10.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
4425
+ if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
4244
4426
  if (!ctx.hasRead(absPath)) {
4245
4427
  throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
4246
4428
  }
@@ -4510,9 +4692,9 @@ var execTool = {
4510
4692
  allowed: false
4511
4693
  };
4512
4694
  }
4513
- const requestedCwd = input.cwd ? path2.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
4514
- const rel = path2.relative(ctx.projectRoot, requestedCwd);
4515
- if (rel.startsWith("..") || path2.isAbsolute(rel)) {
4695
+ const requestedCwd = input.cwd ? path3.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
4696
+ const rel = path3.relative(ctx.projectRoot, requestedCwd);
4697
+ if (rel.startsWith("..") || path3.isAbsolute(rel)) {
4516
4698
  return {
4517
4699
  command: cmd,
4518
4700
  args,
@@ -4534,6 +4716,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
4534
4716
  let stderr = "";
4535
4717
  let killed = false;
4536
4718
  const startedAt = Date.now();
4719
+ const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
4537
4720
  const resolved = resolveWin32Command(cmd);
4538
4721
  const isWin = process.platform === "win32";
4539
4722
  const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
@@ -4566,10 +4749,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
4566
4749
  else signal.addEventListener("abort", onAbort, { once: true });
4567
4750
  }
4568
4751
  child.stdout?.on("data", (chunk) => {
4569
- if (stdout.length < MAX_OUTPUT2) stdout += chunk.toString();
4752
+ const text = chunk.toString();
4753
+ if (stdout.length < MAX_OUTPUT2) stdout += text;
4754
+ spool.write(text);
4570
4755
  });
4571
4756
  child.stderr?.on("data", (chunk) => {
4572
- if (stderr.length < MAX_OUTPUT2) stderr += chunk.toString();
4757
+ const text = chunk.toString();
4758
+ if (stderr.length < MAX_OUTPUT2) stderr += text;
4759
+ spool.write(text);
4573
4760
  });
4574
4761
  child.on("close", (code) => {
4575
4762
  clearTimeout(timer);
@@ -4578,10 +4765,11 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
4578
4765
  const durationMs = Date.now() - startedAt;
4579
4766
  const exitCode = killed ? 124 : code ?? 1;
4580
4767
  registry.afterCall(durationMs, exitCode !== 0);
4768
+ const spooled = spool.finalize();
4581
4769
  resolve7({
4582
4770
  command: cmd,
4583
4771
  args,
4584
- stdout: normalizeCommandOutput(stdout),
4772
+ stdout: normalizeCommandOutput(stdout) + (spooled ? spoolNote(spooled) : ""),
4585
4773
  stderr: normalizeCommandOutput(stderr),
4586
4774
  exitCode,
4587
4775
  truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES,
@@ -4593,6 +4781,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
4593
4781
  if (isWin) signal.removeEventListener("abort", onAbort);
4594
4782
  if (typeof pid === "number") registry.unregister(pid);
4595
4783
  registry.afterCall(Date.now() - startedAt, true);
4784
+ spool.finalize();
4596
4785
  resolve7({
4597
4786
  command: cmd,
4598
4787
  args,
@@ -5023,13 +5212,13 @@ var formatTool = {
5023
5212
  }
5024
5213
  };
5025
5214
  async function detectFixer(cwd) {
5026
- const { stat: stat10 } = await import('node:fs/promises');
5215
+ const { stat: stat11 } = await import('node:fs/promises');
5027
5216
  try {
5028
- await stat10(`${cwd}/biome.json`);
5217
+ await stat11(`${cwd}/biome.json`);
5029
5218
  return "biome";
5030
5219
  } catch {
5031
5220
  try {
5032
- await stat10(`${cwd}/.prettierrc`);
5221
+ await stat11(`${cwd}/.prettierrc`);
5033
5222
  return "prettier";
5034
5223
  } catch {
5035
5224
  return "biome";
@@ -5172,8 +5361,8 @@ function findGitDir2(cwd, projectRoot) {
5172
5361
  let dir = cwd;
5173
5362
  for (let i = 0; i < 20; i++) {
5174
5363
  try {
5175
- const stat10 = statSync(`${dir}/.git`);
5176
- if (stat10.isDirectory() || stat10.isFile()) return dir;
5364
+ const stat11 = statSync(`${dir}/.git`);
5365
+ if (stat11.isDirectory() || stat11.isFile()) return dir;
5177
5366
  } catch {
5178
5367
  }
5179
5368
  if (dir === root) break;
@@ -5347,7 +5536,7 @@ var globTool = {
5347
5536
  if (DEFAULT_IGNORE2.includes(name)) continue;
5348
5537
  if (ignored.includes(name)) continue;
5349
5538
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
5350
- const full = path2.join(dir, name);
5539
+ const full = path3.join(dir, name);
5351
5540
  if (e.isDirectory()) {
5352
5541
  await walk(full, rel);
5353
5542
  if (truncated) return;
@@ -5373,7 +5562,7 @@ var globTool = {
5373
5562
  };
5374
5563
  async function readGitignore(dir) {
5375
5564
  try {
5376
- const raw = await fs14.readFile(path2.join(dir, ".gitignore"), "utf8");
5565
+ const raw = await fs14.readFile(path3.join(dir, ".gitignore"), "utf8");
5377
5566
  return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
5378
5567
  } catch {
5379
5568
  return [];
@@ -5665,15 +5854,15 @@ async function runNative(input, base, mode, limit, signal) {
5665
5854
  if (stopped) return;
5666
5855
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
5667
5856
  if (e.isSymbolicLink()) continue;
5668
- const full = path2.join(dir, e.name);
5857
+ const full = path3.join(dir, e.name);
5669
5858
  if (e.isDirectory()) {
5670
5859
  await walk(full);
5671
5860
  } else if (e.isFile()) {
5672
5861
  if (globRe && !globRe.test(e.name) && !globRe.test(full)) continue;
5673
5862
  if (globRe) globRe.lastIndex = 0;
5674
5863
  try {
5675
- const stat10 = await fs14.stat(full);
5676
- if (stat10.size > 1e6) continue;
5864
+ const stat11 = await fs14.stat(full);
5865
+ if (stat11.size > 1e6) continue;
5677
5866
  const head = await fs14.readFile(full);
5678
5867
  if (isBinaryBuffer(head)) continue;
5679
5868
  const text = head.toString("utf8");
@@ -5857,6 +6046,7 @@ var jsonTool = {
5857
6046
  permission: "auto",
5858
6047
  mutating: false,
5859
6048
  timeoutMs: 5e3,
6049
+ capabilities: ["fs.read"],
5860
6050
  inputSchema: {
5861
6051
  type: "object",
5862
6052
  properties: {
@@ -5921,8 +6111,8 @@ var jsonTool = {
5921
6111
  };
5922
6112
  }
5923
6113
  };
5924
- function query(data, path20) {
5925
- const parts = path20.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
6114
+ function query(data, path21) {
6115
+ const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
5926
6116
  let current = data;
5927
6117
  for (const part of parts) {
5928
6118
  if (current === null || current === void 0) return void 0;
@@ -5980,6 +6170,7 @@ var lintTool = {
5980
6170
  permission: "confirm",
5981
6171
  mutating: false,
5982
6172
  timeoutMs: 6e4,
6173
+ capabilities: ["shell.restricted"],
5983
6174
  inputSchema: {
5984
6175
  type: "object",
5985
6176
  properties: {
@@ -6051,11 +6242,11 @@ var lintTool = {
6051
6242
  }
6052
6243
  };
6053
6244
  async function detectLinter(cwd) {
6054
- const { stat: stat10 } = await import('node:fs/promises');
6245
+ const { stat: stat11 } = await import('node:fs/promises');
6055
6246
  const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
6056
6247
  for (const f of checks) {
6057
6248
  try {
6058
- await stat10(`${cwd}/${f}`);
6249
+ await stat11(`${cwd}/${f}`);
6059
6250
  if (f.includes("biome")) return "biome";
6060
6251
  if (f.includes("eslint")) return "eslint";
6061
6252
  if (f.includes("tslint")) return "tslint";
@@ -6072,6 +6263,7 @@ var logsTool = {
6072
6263
  permission: "confirm",
6073
6264
  mutating: false,
6074
6265
  timeoutMs: 3e4,
6266
+ capabilities: ["shell.restricted"],
6075
6267
  inputSchema: {
6076
6268
  type: "object",
6077
6269
  properties: {
@@ -6193,7 +6385,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
6193
6385
  }
6194
6386
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
6195
6387
  var MAX_TAIL_LINES = 1e5;
6196
- async function fileLogs(path20, lines, filterRe, stream) {
6388
+ async function fileLogs(path21, lines, filterRe, stream) {
6197
6389
  const { createInterface } = await import('node:readline');
6198
6390
  const { createReadStream } = await import('node:fs');
6199
6391
  const entries = [];
@@ -6202,7 +6394,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
6202
6394
  let writeIdx = 0;
6203
6395
  let totalLines = 0;
6204
6396
  const rl = createInterface({
6205
- input: createReadStream(path20),
6397
+ input: createReadStream(path21),
6206
6398
  crlfDelay: Number.POSITIVE_INFINITY
6207
6399
  });
6208
6400
  for await (const line of rl) {
@@ -6223,7 +6415,7 @@ async function fileLogs(path20, lines, filterRe, stream) {
6223
6415
  if (parsed) entries.push(parsed);
6224
6416
  }
6225
6417
  return {
6226
- source: path20,
6418
+ source: path21,
6227
6419
  entries,
6228
6420
  total: entries.length,
6229
6421
  truncated: totalLines > effLines,
@@ -6269,9 +6461,23 @@ var outdatedTool = {
6269
6461
  name: "outdated",
6270
6462
  category: "Package Management",
6271
6463
  description: "Check for outdated dependencies in the project. Reports current, wanted (semver range), and latest versions available.",
6272
- 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.",
6273
- permission: "auto",
6274
- mutating: false,
6464
+ 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.",
6465
+ permission: "confirm",
6466
+ // Network side-effecting (registry HTTP). Pairs with `mutating: true`
6467
+ // so the H7 invariant test (`no auto-permission tool declares
6468
+ // mutating: true`) passes — a tool claiming `'auto'` must be purely
6469
+ // read-only, but `outdated` makes outbound HTTP calls to the
6470
+ // registry. The 'confirm' permission routes the call through the
6471
+ // tool.confirm_needed flow on every invocation. M-1 originally
6472
+ // fixed four sibling tools (mcp_control, shellcheck, shellcheck_scan,
6473
+ // web_search) but missed this one; applying the same contract here.
6474
+ mutating: true,
6475
+ // Capability is just "network" — the tool only hits the package
6476
+ // registry over HTTP, never touches the filesystem or runs shell.
6477
+ // The H7 invariant test requires this array to be non-empty for
6478
+ // any mutating:true tool (meta-tools whitelisted). See
6479
+ // tests/permission-mutating-invariant.test.ts:92.
6480
+ capabilities: ["network"],
6275
6481
  timeoutMs: 6e4,
6276
6482
  inputSchema: {
6277
6483
  type: "object",
@@ -6392,9 +6598,9 @@ var patchTool = {
6392
6598
  for (const t of targets) {
6393
6599
  const stripped = stripPathComponents(t, strip);
6394
6600
  if (!stripped) continue;
6395
- const candidate = path2.resolve(dir, stripped);
6396
- const rel = path2.relative(ctx.projectRoot, candidate);
6397
- if (rel.startsWith("..") || path2.isAbsolute(rel)) {
6601
+ const candidate = path3.resolve(dir, stripped);
6602
+ const rel = path3.relative(ctx.projectRoot, candidate);
6603
+ if (rel.startsWith("..") || path3.isAbsolute(rel)) {
6398
6604
  return {
6399
6605
  applied: 0,
6400
6606
  rejected: 1,
@@ -6404,11 +6610,11 @@ var patchTool = {
6404
6610
  };
6405
6611
  }
6406
6612
  }
6407
- const tmpDir = await fs14.mkdtemp(path2.join(os.tmpdir(), ".wstack_patch_"));
6613
+ const tmpDir = await fs14.mkdtemp(path3.join(os.tmpdir(), ".wstack_patch_"));
6408
6614
  try {
6409
6615
  await fs14.chmod(tmpDir, 448).catch(() => {
6410
6616
  });
6411
- const patchFile = path2.join(tmpDir, "in.diff");
6617
+ const patchFile = path3.join(tmpDir, "in.diff");
6412
6618
  await fs14.writeFile(patchFile, input.patch, { mode: 384 });
6413
6619
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
6414
6620
  const result = await runPatch(args, dir, opts.signal);
@@ -6730,9 +6936,9 @@ var readTool = {
6730
6936
  async execute(input, ctx) {
6731
6937
  if (!input?.path) throw new Error("read: path is required");
6732
6938
  const absPath = await safeResolveReal(input.path, ctx);
6733
- let stat10;
6939
+ let stat11;
6734
6940
  try {
6735
- stat10 = await fs14.stat(absPath);
6941
+ stat11 = await fs14.stat(absPath);
6736
6942
  } catch (err) {
6737
6943
  const code = err.code;
6738
6944
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
@@ -6740,9 +6946,9 @@ var readTool = {
6740
6946
  `read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
6741
6947
  );
6742
6948
  }
6743
- if (!stat10.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
6744
- if (stat10.size > MAX_BYTES2) {
6745
- throw new Error(`read: file too large (${stat10.size} bytes, limit ${MAX_BYTES2})`);
6949
+ if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
6950
+ if (stat11.size > MAX_BYTES2) {
6951
+ throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES2})`);
6746
6952
  }
6747
6953
  const buf = await fs14.readFile(absPath);
6748
6954
  if (isBinaryBuffer(buf)) {
@@ -6754,14 +6960,14 @@ var readTool = {
6754
6960
  const offset = Math.max(1, input.offset ?? 1);
6755
6961
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
6756
6962
  if (limit === 0) {
6757
- ctx.recordRead(absPath, stat10.mtimeMs);
6963
+ ctx.recordRead(absPath, stat11.mtimeMs);
6758
6964
  return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
6759
6965
  }
6760
6966
  const slice = allLines.slice(offset - 1, offset - 1 + limit);
6761
6967
  const truncated = offset - 1 + slice.length < total;
6762
6968
  const width = String(offset + slice.length - 1).length;
6763
6969
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
6764
- ctx.recordRead(absPath, stat10.mtimeMs);
6970
+ ctx.recordRead(absPath, stat11.mtimeMs);
6765
6971
  return {
6766
6972
  text: numbered,
6767
6973
  total_lines: total,
@@ -6828,10 +7034,10 @@ var replaceTool = {
6828
7034
  } catch {
6829
7035
  continue;
6830
7036
  }
6831
- const rel = path2.relative(realRoot, realPath);
6832
- if (rel.startsWith("..") || path2.isAbsolute(rel)) continue;
6833
- const stat10 = await fs14.stat(realPath).catch(() => null);
6834
- if (!stat10 || !stat10.isFile()) continue;
7037
+ const rel = path3.relative(realRoot, realPath);
7038
+ if (rel.startsWith("..") || path3.isAbsolute(rel)) continue;
7039
+ const stat11 = await fs14.stat(realPath).catch(() => null);
7040
+ if (!stat11 || !stat11.isFile()) continue;
6835
7041
  let content;
6836
7042
  try {
6837
7043
  const buf = await fs14.readFile(realPath);
@@ -6856,7 +7062,7 @@ var replaceTool = {
6856
7062
  totalReplacements += count;
6857
7063
  if (!dryRun) {
6858
7064
  const newContent = toStyle(newContentLf, style);
6859
- await atomicWrite(realPath, newContent, { mode: stat10.mode & 511 });
7065
+ await atomicWrite(realPath, newContent, { mode: stat11.mode & 511 });
6860
7066
  }
6861
7067
  const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
6862
7068
  fromFile: absPath,
@@ -6886,8 +7092,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
6886
7092
  const resolved = [];
6887
7093
  for (const p of parts) {
6888
7094
  const absPath = safeResolve(p, ctx);
6889
- const stat10 = await fs14.stat(absPath).catch(() => null);
6890
- if (stat10?.isFile()) {
7095
+ const stat11 = await fs14.stat(absPath).catch(() => null);
7096
+ if (stat11?.isFile()) {
6891
7097
  resolved.push(absPath);
6892
7098
  }
6893
7099
  }
@@ -6948,10 +7154,10 @@ async function globNative(pattern, base, extraGlob) {
6948
7154
  }
6949
7155
  for (const e of entries) {
6950
7156
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
6951
- const full = path2.join(dir, e.name);
7157
+ const full = path3.join(dir, e.name);
6952
7158
  try {
6953
- const stat10 = await fs14.lstat(full);
6954
- if (stat10.isSymbolicLink()) continue;
7159
+ const stat11 = await fs14.lstat(full);
7160
+ if (stat11.isSymbolicLink()) continue;
6955
7161
  } catch {
6956
7162
  continue;
6957
7163
  }
@@ -7118,16 +7324,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
7118
7324
  let filesCreated = 0;
7119
7325
  for (const [filePath, content] of Object.entries(templateFiles)) {
7120
7326
  const resolvedPath = substituteVars(filePath, name, vars);
7121
- const joinedPath = path2.join(cwd, resolvedPath);
7122
- const root = path2.resolve(ctx.projectRoot);
7123
- const target = path2.resolve(joinedPath);
7124
- const rel = path2.relative(root, target);
7125
- if (rel.startsWith("..") || path2.isAbsolute(rel)) {
7327
+ const joinedPath = path3.join(cwd, resolvedPath);
7328
+ const root = path3.resolve(ctx.projectRoot);
7329
+ const target = path3.resolve(joinedPath);
7330
+ const rel = path3.relative(root, target);
7331
+ if (rel.startsWith("..") || path3.isAbsolute(rel)) {
7126
7332
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
7127
7333
  }
7128
7334
  const fullPath = target;
7129
7335
  if (!dryRun) {
7130
- await fs14.mkdir(path2.dirname(fullPath), { recursive: true });
7336
+ await fs14.mkdir(path3.dirname(fullPath), { recursive: true });
7131
7337
  await atomicWrite(fullPath, substituteVars(content, name, vars));
7132
7338
  }
7133
7339
  files.push(resolvedPath);
@@ -7724,6 +7930,7 @@ var testTool = {
7724
7930
  permission: "confirm",
7725
7931
  mutating: false,
7726
7932
  timeoutMs: 12e4,
7933
+ capabilities: ["shell.restricted"],
7727
7934
  inputSchema: {
7728
7935
  type: "object",
7729
7936
  properties: {
@@ -7740,7 +7947,11 @@ var testTool = {
7740
7947
  coverage: { type: "boolean", description: "Generate coverage report (default: false)" },
7741
7948
  cwd: { type: "string", description: "Working directory (default: cwd)" },
7742
7949
  grep: { type: "string", description: "Filter tests by name pattern (default: none)" },
7743
- timeout: { type: "integer", description: "Test timeout in ms (default: 30000)" }
7950
+ timeout: { type: "integer", description: "Test timeout in ms (default: 30000)" },
7951
+ verbose: {
7952
+ type: "boolean",
7953
+ 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)"
7954
+ }
7744
7955
  }
7745
7956
  },
7746
7957
  async execute(input, ctx, opts) {
@@ -7788,11 +7999,11 @@ var testTool = {
7788
7999
  }
7789
8000
  };
7790
8001
  async function detectRunner(cwd) {
7791
- const { stat: stat10 } = await import('node:fs/promises');
8002
+ const { stat: stat11 } = await import('node:fs/promises');
7792
8003
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
7793
8004
  for (const f of candidates) {
7794
8005
  try {
7795
- await stat10(path2.join(cwd, f));
8006
+ await stat11(path3.join(cwd, f));
7796
8007
  if (f.includes("vitest")) return "vitest";
7797
8008
  if (f.includes("jest")) return "jest";
7798
8009
  if (f.includes("mocha")) return "mocha";
@@ -7806,17 +8017,14 @@ function buildArgs2(runner, input) {
7806
8017
  const timeout = input.timeout ?? 3e4;
7807
8018
  switch (runner) {
7808
8019
  case "vitest":
7809
- args.push("run", "--reporter=verbose");
7810
- if (input.watch) {
7811
- args[1] = "";
7812
- args.push("watch");
7813
- }
8020
+ args.push(input.watch ? "watch" : "run");
8021
+ if (input.verbose) args.push("--reporter=verbose");
7814
8022
  if (input.coverage) args.push("--coverage");
7815
8023
  if (input.grep) args.push("--testNamePattern", input.grep);
7816
8024
  args.push("--testTimeout", String(timeout));
7817
8025
  break;
7818
8026
  case "jest":
7819
- args.push("--verbose");
8027
+ if (input.verbose) args.push("--verbose");
7820
8028
  if (input.watch) args.push("--watch");
7821
8029
  if (input.coverage) args.push("--coverage");
7822
8030
  if (input.grep) args.push("--testPathPattern", input.grep);
@@ -7860,7 +8068,13 @@ function parseResult(runner, result, duration) {
7860
8068
  passed,
7861
8069
  failed,
7862
8070
  duration_ms: duration,
7863
- output: normalizeCommandOutput(result.stdout || result.error || ""),
8071
+ // A passing run only needs the tail summary in chat history — counts are
8072
+ // already parsed above and the FULL log is on disk (spool marker rides
8073
+ // the stdout tail). Failures keep the standard command-output cap so
8074
+ // the agent sees the failure details inline.
8075
+ output: normalizeCommandOutput(result.stdout || result.error || "", {
8076
+ maxBytes: result.exitCode === 0 ? 4096 : void 0
8077
+ }),
7864
8078
  truncated: result.truncated
7865
8079
  };
7866
8080
  }
@@ -7873,6 +8087,7 @@ var todoTool = {
7873
8087
  mutating: false,
7874
8088
  // mutates only conversation state (ctx.todos), not external state — no confirmation needed
7875
8089
  timeoutMs: 1e3,
8090
+ capabilities: ["session.todo"],
7876
8091
  inputSchema: {
7877
8092
  type: "object",
7878
8093
  properties: {
@@ -7980,6 +8195,7 @@ var toolHelpTool = {
7980
8195
  permission: "auto",
7981
8196
  mutating: false,
7982
8197
  timeoutMs: 5e3,
8198
+ capabilities: ["tool.meta"],
7983
8199
  inputSchema: {
7984
8200
  type: "object",
7985
8201
  properties: {
@@ -8102,6 +8318,7 @@ var toolSearchTool = {
8102
8318
  permission: "auto",
8103
8319
  mutating: false,
8104
8320
  timeoutMs: 1e3,
8321
+ capabilities: ["tool.meta"],
8105
8322
  inputSchema: {
8106
8323
  type: "object",
8107
8324
  properties: {
@@ -8180,6 +8397,7 @@ var toolUseTool = {
8180
8397
  permission: "confirm",
8181
8398
  mutating: true,
8182
8399
  timeoutMs: 6e4,
8400
+ capabilities: ["tool.mutate.any"],
8183
8401
  inputSchema: {
8184
8402
  type: "object",
8185
8403
  properties: {
@@ -8408,7 +8626,7 @@ async function walkDir(dir, depth, opts) {
8408
8626
  opts.lines.push(opts.prefix + branch + displayName);
8409
8627
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
8410
8628
  const childPrefix = opts.prefix + connector;
8411
- await walkDir(path2.join(dir, entry.name), depth + 1, {
8629
+ await walkDir(path3.join(dir, entry.name), depth + 1, {
8412
8630
  ...opts,
8413
8631
  prefix: childPrefix,
8414
8632
  isLast
@@ -8424,6 +8642,7 @@ var typecheckTool = {
8424
8642
  permission: "confirm",
8425
8643
  mutating: false,
8426
8644
  timeoutMs: 12e4,
8645
+ capabilities: ["shell.restricted"],
8427
8646
  inputSchema: {
8428
8647
  type: "object",
8429
8648
  properties: {
@@ -8487,12 +8706,12 @@ var typecheckTool = {
8487
8706
  }
8488
8707
  };
8489
8708
  async function findTsConfig(cwd) {
8490
- const { stat: stat10 } = await import('node:fs/promises');
8709
+ const { stat: stat11 } = await import('node:fs/promises');
8491
8710
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
8492
8711
  for (const f of candidates) {
8493
8712
  try {
8494
- const s = await stat10(path2.join(cwd, f));
8495
- if (s.isFile()) return path2.join(cwd, f);
8713
+ const s = await stat11(path3.join(cwd, f));
8714
+ if (s.isFile()) return path3.join(cwd, f);
8496
8715
  } catch {
8497
8716
  }
8498
8717
  }
@@ -8528,12 +8747,12 @@ var writeTool = {
8528
8747
  let existed = false;
8529
8748
  let prev = "";
8530
8749
  try {
8531
- const stat11 = await fs14.stat(absPath);
8532
- existed = stat11.isFile();
8750
+ const stat12 = await fs14.stat(absPath);
8751
+ existed = stat12.isFile();
8533
8752
  if (existed) {
8534
8753
  if (!ctx.hasRead(absPath)) {
8535
8754
  prev = await fs14.readFile(absPath, "utf8");
8536
- ctx.recordRead(absPath, stat11.mtimeMs);
8755
+ ctx.recordRead(absPath, stat12.mtimeMs);
8537
8756
  } else {
8538
8757
  prev = await fs14.readFile(absPath, "utf8");
8539
8758
  }
@@ -8546,8 +8765,8 @@ var writeTool = {
8546
8765
  await atomicWrite(absPath, input.content);
8547
8766
  const diff = existed ? unifiedDiff(prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
8548
8767
  + (new file, ${input.content.split("\n").length} lines)`;
8549
- const stat10 = await fs14.stat(absPath);
8550
- ctx.recordRead(absPath, stat10.mtimeMs);
8768
+ const stat11 = await fs14.stat(absPath);
8769
+ ctx.recordRead(absPath, stat11.mtimeMs);
8551
8770
  ctx.session.recordFileChange({
8552
8771
  path: absPath,
8553
8772
  action: existed ? "modified" : "created",