@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.
- package/dist/audit.js +591 -48
- package/dist/audit.js.map +1 -1
- package/dist/{background-indexer-DwJsyAB0.d.ts → background-indexer-CJ5JiV5i.d.ts} +0 -8
- package/dist/bash.js +133 -23
- package/dist/bash.js.map +1 -1
- package/dist/batch-tool-use.js +1 -0
- package/dist/batch-tool-use.js.map +1 -1
- package/dist/builtin.d.ts +25 -1
- package/dist/builtin.js +782 -535
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.d.ts +2 -2
- package/dist/codebase-index/index.js +16 -0
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +11 -6
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/document.js +1 -0
- package/dist/document.js.map +1 -1
- package/dist/exec.js +115 -5
- package/dist/exec.js.map +1 -1
- package/dist/format.js +590 -48
- package/dist/format.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +380 -128
- package/dist/index.js.map +1 -1
- package/dist/install.js +590 -48
- package/dist/install.js.map +1 -1
- package/dist/json.js +1 -0
- package/dist/json.js.map +1 -1
- package/dist/lint.js +590 -47
- package/dist/lint.js.map +1 -1
- package/dist/logs.js +1 -0
- package/dist/logs.js.map +1 -1
- package/dist/memory.js +4 -0
- package/dist/memory.js.map +1 -1
- package/dist/mode.js +1 -0
- package/dist/mode.js.map +1 -1
- package/dist/outdated.js +17 -3
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +746 -527
- package/dist/pack.js.map +1 -1
- package/dist/test.d.ts +1 -0
- package/dist/test.js +605 -55
- package/dist/test.js.map +1 -1
- package/dist/todo.js +1 -0
- package/dist/todo.js.map +1 -1
- package/dist/tool-help.js +1 -0
- package/dist/tool-help.js.map +1 -1
- package/dist/tool-search.js +1 -0
- package/dist/tool-search.js.map +1 -1
- package/dist/tool-use.js +1 -0
- package/dist/tool-use.js.map +1 -1
- package/dist/typecheck.js +591 -48
- package/dist/typecheck.js.map +1 -1
- 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,
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
732
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
|
|
776
|
+
i = j;
|
|
746
777
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
|
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
|
-
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
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
|
-
|
|
890
|
-
if (
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
2562
|
+
const tmpDir = path3.join(os.tmpdir(), "ws-go-parse");
|
|
2398
2563
|
try {
|
|
2399
2564
|
mkdirSync(tmpDir, { recursive: true });
|
|
2400
|
-
const scriptPath =
|
|
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 =
|
|
2809
|
+
const tmpDir = path3.join(os.tmpdir(), "ws-py-parse");
|
|
2645
2810
|
mkdirSync(tmpDir, { recursive: true });
|
|
2646
|
-
const scriptPath =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
2712
|
-
const crateDir =
|
|
2713
|
-
const tmpFile =
|
|
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",
|
|
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 =
|
|
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:
|
|
3007
|
+
name: path3.basename(file),
|
|
2843
3008
|
kind: "object",
|
|
2844
3009
|
line,
|
|
2845
3010
|
col: 0,
|
|
2846
|
-
signature: `"${
|
|
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(
|
|
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 =
|
|
3261
|
-
const rel =
|
|
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 =
|
|
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) =>
|
|
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
|
|
3510
|
+
let stat11;
|
|
3346
3511
|
try {
|
|
3347
3512
|
const statOpts = signal ? { signal } : {};
|
|
3348
|
-
|
|
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 (!
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
4001
|
-
if (
|
|
4181
|
+
const stat11 = statSync(path3.join(dir, ".git"));
|
|
4182
|
+
if (stat11.isDirectory()) return dir;
|
|
4002
4183
|
} catch {
|
|
4003
4184
|
}
|
|
4004
|
-
const parent =
|
|
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
|
|
4046
|
-
if (!
|
|
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
|
|
4146
|
-
if (
|
|
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
|
|
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 (!
|
|
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 ?
|
|
4514
|
-
const rel =
|
|
4515
|
-
if (rel.startsWith("..") ||
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
5215
|
+
const { stat: stat11 } = await import('node:fs/promises');
|
|
5027
5216
|
try {
|
|
5028
|
-
await
|
|
5217
|
+
await stat11(`${cwd}/biome.json`);
|
|
5029
5218
|
return "biome";
|
|
5030
5219
|
} catch {
|
|
5031
5220
|
try {
|
|
5032
|
-
await
|
|
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
|
|
5176
|
-
if (
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
5676
|
-
if (
|
|
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,
|
|
5925
|
-
const parts =
|
|
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:
|
|
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
|
|
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(
|
|
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(
|
|
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:
|
|
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-
|
|
6273
|
-
permission: "
|
|
6274
|
-
mutating:
|
|
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 =
|
|
6396
|
-
const rel =
|
|
6397
|
-
if (rel.startsWith("..") ||
|
|
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(
|
|
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 =
|
|
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
|
|
6939
|
+
let stat11;
|
|
6734
6940
|
try {
|
|
6735
|
-
|
|
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 (!
|
|
6744
|
-
if (
|
|
6745
|
-
throw new Error(`read: file too large (${
|
|
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,
|
|
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,
|
|
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 =
|
|
6832
|
-
if (rel.startsWith("..") ||
|
|
6833
|
-
const
|
|
6834
|
-
if (!
|
|
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:
|
|
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
|
|
6890
|
-
if (
|
|
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 =
|
|
7157
|
+
const full = path3.join(dir, e.name);
|
|
6952
7158
|
try {
|
|
6953
|
-
const
|
|
6954
|
-
if (
|
|
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 =
|
|
7122
|
-
const root =
|
|
7123
|
-
const target =
|
|
7124
|
-
const rel =
|
|
7125
|
-
if (rel.startsWith("..") ||
|
|
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(
|
|
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:
|
|
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
|
|
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("
|
|
7810
|
-
if (input.
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
|
8495
|
-
if (s.isFile()) return
|
|
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
|
|
8532
|
-
existed =
|
|
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,
|
|
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
|
|
8550
|
-
ctx.recordRead(absPath,
|
|
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",
|