@wrongstack/tools 0.265.1 → 0.268.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/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as fs4 from 'node:fs/promises';
2
+ import { toErrorMessage } from '@wrongstack/core/utils';
2
3
  import * as path from 'node:path';
3
4
  import { resolve, sep, dirname, join } from 'node:path';
4
5
  import * as Core from '@wrongstack/core';
5
- import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
6
- import { toErrorMessage } from '@wrongstack/core/utils';
6
+ import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, assessCommitSafety, recordPackageAction, detectPackageEcosystem, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
7
7
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
8
8
  import * as os from 'node:os';
9
9
  import * as fs7 from 'node:fs';
@@ -168,6 +168,8 @@ function normalizeCommandOutput(raw, opts = {}) {
168
168
  text = text.replace(/\n{3,}/g, "\n\n");
169
169
  return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);
170
170
  }
171
+
172
+ // src/read.ts
171
173
  var MAX_BYTES = 5 * 1024 * 1024;
172
174
  var readTool = {
173
175
  name: "read",
@@ -194,6 +196,11 @@ var readTool = {
194
196
  limit: {
195
197
  type: "integer",
196
198
  description: "Maximum number of lines to return (default is 2000)."
199
+ },
200
+ mode: {
201
+ type: "string",
202
+ enum: ["content", "summary"],
203
+ description: "Return full line-numbered content (default) or a compact file summary with imports/exports/symbols."
197
204
  }
198
205
  },
199
206
  required: ["path"]
@@ -207,14 +214,27 @@ var readTool = {
207
214
  } catch (err) {
208
215
  const code = err.code;
209
216
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
210
- throw new Error(
211
- `read: failed to stat "${input.path}": ${toErrorMessage(err)}`
212
- );
217
+ throw new Error(`read: failed to stat "${input.path}": ${toErrorMessage(err)}`);
213
218
  }
214
219
  if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
215
220
  if (stat11.size > MAX_BYTES) {
216
221
  throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES})`);
217
222
  }
223
+ const offset = Math.max(1, input.offset ?? 1);
224
+ const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
225
+ const prior = getReadRangeRecord(ctx, absPath);
226
+ const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
227
+ if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat11.mtimeMs, offset, requestedEnd)) {
228
+ ctx.recordRead(absPath, stat11.mtimeMs);
229
+ return {
230
+ text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat11.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
231
+ total_lines: prior.totalLines,
232
+ encoding: "utf8",
233
+ truncated: requestedEnd < prior.totalLines,
234
+ cached: true,
235
+ note: "Repeated read suppressed to save tokens."
236
+ };
237
+ }
218
238
  const buf = await fs4.readFile(absPath);
219
239
  if (isBinaryBuffer(buf)) {
220
240
  throw new Error(`read: "${input.path}" appears to be binary`);
@@ -222,17 +242,38 @@ var readTool = {
222
242
  const text = buf.toString("utf8");
223
243
  const allLines = text.split(/\r\n|\r|\n/);
224
244
  const total = allLines.length;
225
- const offset = Math.max(1, input.offset ?? 1);
226
- const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
245
+ if (input.mode === "summary") {
246
+ ctx.recordRead(absPath, stat11.mtimeMs);
247
+ rememberReadRange(ctx, absPath, stat11.mtimeMs, total, 1, Math.min(total, 200));
248
+ return {
249
+ text: summarizeFile(input.path, stat11.size, allLines),
250
+ total_lines: total,
251
+ encoding: "utf8",
252
+ truncated: total > 200,
253
+ note: "Summary mode returned compact structure instead of full file content."
254
+ };
255
+ }
227
256
  if (limit === 0) {
228
257
  ctx.recordRead(absPath, stat11.mtimeMs);
258
+ rememberReadRange(ctx, absPath, stat11.mtimeMs, total, 1, 0);
229
259
  return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
230
260
  }
261
+ if (offset > total) {
262
+ ctx.recordRead(absPath, stat11.mtimeMs);
263
+ rememberReadRange(ctx, absPath, stat11.mtimeMs, total, total + 1, total + 1);
264
+ return {
265
+ text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
266
+ total_lines: total,
267
+ encoding: "utf8",
268
+ truncated: false
269
+ };
270
+ }
231
271
  const slice = allLines.slice(offset - 1, offset - 1 + limit);
232
272
  const truncated = offset - 1 + slice.length < total;
233
273
  const width = String(offset + slice.length - 1).length;
234
274
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
235
275
  ctx.recordRead(absPath, stat11.mtimeMs);
276
+ rememberReadRange(ctx, absPath, stat11.mtimeMs, total, offset, offset + slice.length - 1);
236
277
  return {
237
278
  text: numbered,
238
279
  total_lines: total,
@@ -241,6 +282,62 @@ var readTool = {
241
282
  };
242
283
  }
243
284
  };
285
+ var READ_RANGES_META_KEY = "tools.read.ranges.v1";
286
+ function getReadRanges(ctx) {
287
+ const existing = ctx.meta[READ_RANGES_META_KEY];
288
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) {
289
+ return existing;
290
+ }
291
+ const next = {};
292
+ ctx.meta[READ_RANGES_META_KEY] = next;
293
+ return next;
294
+ }
295
+ function getReadRangeRecord(ctx, absPath) {
296
+ return getReadRanges(ctx)[absPath];
297
+ }
298
+ function rememberReadRange(ctx, absPath, mtimeMs, totalLines, start, end) {
299
+ if (end < start) return;
300
+ const ranges = getReadRanges(ctx);
301
+ const prior = ranges[absPath];
302
+ const nextRanges = prior && Math.abs(prior.mtimeMs - mtimeMs) <= 1 ? prior.ranges.slice() : [];
303
+ nextRanges.push({ start, end });
304
+ ranges[absPath] = {
305
+ mtimeMs,
306
+ totalLines,
307
+ ranges: mergeRanges(nextRanges)
308
+ };
309
+ }
310
+ function coversRange(record, mtimeMs, start, end) {
311
+ if (Math.abs(record.mtimeMs - mtimeMs) > 1) return false;
312
+ return record.ranges.some((range) => range.start <= start && range.end >= end);
313
+ }
314
+ function mergeRanges(ranges) {
315
+ const sorted = ranges.slice().sort((a, b) => a.start - b.start);
316
+ const merged = [];
317
+ for (const range of sorted) {
318
+ const last = merged[merged.length - 1];
319
+ if (!last || range.start > last.end + 1) {
320
+ merged.push({ ...range });
321
+ continue;
322
+ }
323
+ last.end = Math.max(last.end, range.end);
324
+ }
325
+ return merged;
326
+ }
327
+ function summarizeFile(filePath, bytes, lines) {
328
+ const interesting = lines.map((line, index) => ({ line: line.trim(), number: index + 1 })).filter(
329
+ ({ line }) => /^(import\s|export\s|class\s|interface\s|type\s|function\s|const\s+\w+\s*=|let\s+\w+\s*=|var\s+\w+\s*=|def\s+|async\s+function\s)/.test(
330
+ line
331
+ )
332
+ ).slice(0, 80).map(({ line, number }) => `${number}: ${line}`);
333
+ return [
334
+ `summary: ${filePath}`,
335
+ `bytes=${bytes}`,
336
+ `total_lines=${lines.length}`,
337
+ interesting.length > 0 ? `symbols/imports:
338
+ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
339
+ ].join("\n");
340
+ }
244
341
  var writeTool = {
245
342
  name: "write",
246
343
  category: "Filesystem",
@@ -309,8 +406,8 @@ var writeTool = {
309
406
  var editTool = {
310
407
  name: "edit",
311
408
  category: "Filesystem",
312
- description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It requires that you have previously called `read` on the file in the current session. Fails safely if the `old_string` appears more than once unless `replace_all` is set.",
313
- usageHint: "MANDATORY WORKFLOW:\n1. Call `read` on the target file first (in the same conversation).\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nThis tool is much safer than `write` for existing files because it works against the last-read version.",
409
+ description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It works best after a prior `read`, but can auto-read the current file when the replacement is still unambiguous. Fails safely if the `old_string` appears more than once unless `replace_all` is set.",
410
+ usageHint: "RECOMMENDED WORKFLOW:\n1. Prefer calling `read` on the target file first when planning an edit.\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nIf no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.",
314
411
  permission: "confirm",
315
412
  mutating: true,
316
413
  capabilities: ["fs.write"],
@@ -339,9 +436,7 @@ var editTool = {
339
436
  throw err;
340
437
  });
341
438
  if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
342
- if (!ctx.hasRead(absPath)) {
343
- throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
344
- }
439
+ const autoRead = !ctx.hasRead(absPath);
345
440
  const original = await fs4.readFile(absPath, "utf8");
346
441
  const updated = await fs4.stat(absPath);
347
442
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
@@ -349,15 +444,21 @@ var editTool = {
349
444
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
350
445
  throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
351
446
  }
447
+ if (autoRead && updated.mtimeMs > stat11.mtimeMs + mtimeTolerance) {
448
+ throw new Error(`edit: file "${input.path}" changed while being auto-read. Retry the edit.`);
449
+ }
450
+ const autoReadNote = autoRead ? `No prior read was recorded for "${input.path}"; edit auto-read the current file and applied the replacement only after the ambiguity checks passed.` : void 0;
352
451
  const style = detectNewlineStyle(original);
353
452
  const fileLf = normalizeToLf(original);
354
453
  const oldLf = normalizeToLf(input.old_string);
355
454
  const newLf = normalizeToLf(input.new_string);
356
455
  if (oldLf === newLf) {
456
+ if (autoRead) ctx.recordRead(absPath, updated.mtimeMs);
357
457
  return {
358
458
  path: absPath,
359
459
  replacements: 0,
360
- diff: "(no-op: old and new are identical)"
460
+ diff: "(no-op: old and new are identical)",
461
+ note: autoReadNote
361
462
  };
362
463
  }
363
464
  let count = 0;
@@ -398,7 +499,8 @@ var editTool = {
398
499
  return {
399
500
  path: absPath,
400
501
  replacements: input.replace_all ? count : 1,
401
- diff
502
+ diff,
503
+ note: autoReadNote
402
504
  };
403
505
  }
404
506
  };
@@ -609,13 +711,13 @@ async function globFiles(pattern, base, extraGlob) {
609
711
  return await globNative(pattern, base, extraGlob);
610
712
  }
611
713
  function checkRg() {
612
- return new Promise((resolve7) => {
714
+ return new Promise((resolve6) => {
613
715
  try {
614
716
  const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
615
- p.on("error", () => resolve7(false));
616
- p.on("close", (code) => resolve7(code === 0));
717
+ p.on("error", () => resolve6(false));
718
+ p.on("close", (code) => resolve6(code === 0));
617
719
  } catch {
618
- resolve7(false);
720
+ resolve6(false);
619
721
  }
620
722
  });
621
723
  }
@@ -632,10 +734,10 @@ function spawnRgFind(pattern, base) {
632
734
  buf += chunk.toString();
633
735
  });
634
736
  return {
635
- promise: new Promise((resolve7, reject) => {
737
+ promise: new Promise((resolve6, reject) => {
636
738
  child.on("error", reject);
637
739
  child.on("close", () => {
638
- resolve7(buf.split("\n").filter(Boolean));
740
+ resolve6(buf.split("\n").filter(Boolean));
639
741
  });
640
742
  })
641
743
  };
@@ -841,13 +943,13 @@ var grepTool = {
841
943
  }
842
944
  };
843
945
  async function detectRg(signal) {
844
- return new Promise((resolve7) => {
946
+ return new Promise((resolve6) => {
845
947
  try {
846
948
  const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal, windowsHide: true });
847
- p.on("error", () => resolve7(false));
848
- p.on("close", (code) => resolve7(code === 0));
949
+ p.on("error", () => resolve6(false));
950
+ p.on("close", (code) => resolve6(code === 0));
849
951
  } catch {
850
- resolve7(false);
952
+ resolve6(false);
851
953
  }
852
954
  });
853
955
  }
@@ -1922,10 +2024,10 @@ var bashTool = {
1922
2024
  queue.push(c);
1923
2025
  }
1924
2026
  };
1925
- const next = () => new Promise((resolve7) => {
2027
+ const next = () => new Promise((resolve6) => {
1926
2028
  const c = queue.shift();
1927
- if (c) resolve7(c);
1928
- else resolveNext = resolve7;
2029
+ if (c) resolve6(c);
2030
+ else resolveNext = resolve6;
1929
2031
  });
1930
2032
  let lastFlush = Date.now();
1931
2033
  const flush = () => {
@@ -2221,26 +2323,26 @@ var execTool = {
2221
2323
  allowed: false
2222
2324
  };
2223
2325
  }
2224
- const requestedCwd = input.cwd ? path.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
2225
- const rel = path.relative(ctx.projectRoot, requestedCwd);
2226
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
2326
+ let cwd;
2327
+ try {
2328
+ cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(ctx.cwd, ctx);
2329
+ } catch {
2227
2330
  return {
2228
2331
  command: cmd,
2229
2332
  args,
2230
2333
  stdout: "",
2231
- stderr: `cwd "${input.cwd}" resolves outside project root`,
2334
+ stderr: `cwd "${input.cwd ?? ctx.cwd}" resolves outside project root`,
2232
2335
  exitCode: 1,
2233
2336
  truncated: false,
2234
2337
  allowed: false
2235
2338
  };
2236
2339
  }
2237
- const cwd = requestedCwd;
2238
2340
  const signal = opts.signal;
2239
2341
  return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id);
2240
2342
  }
2241
2343
  };
2242
2344
  function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
2243
- return new Promise((resolve7) => {
2345
+ return new Promise((resolve6) => {
2244
2346
  let stdout = "";
2245
2347
  let stderr = "";
2246
2348
  let killed = false;
@@ -2295,7 +2397,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
2295
2397
  const exitCode = killed ? 124 : code ?? 1;
2296
2398
  registry.afterCall(durationMs, exitCode !== 0);
2297
2399
  const spooled = spool.finalize();
2298
- resolve7({
2400
+ resolve6({
2299
2401
  command: cmd,
2300
2402
  args,
2301
2403
  stdout: normalizeCommandOutput(stdout) + (spooled ? spoolNote(spooled) : ""),
@@ -2311,7 +2413,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
2311
2413
  if (typeof pid === "number") registry.unregister(pid);
2312
2414
  registry.afterCall(Date.now() - startedAt, true);
2313
2415
  spool.finalize();
2314
- resolve7({
2416
+ resolve6({
2315
2417
  command: cmd,
2316
2418
  args,
2317
2419
  stdout: normalizeCommandOutput(stdout),
@@ -3167,7 +3269,7 @@ var gitTool = {
3167
3269
  name: "git",
3168
3270
  category: "Git",
3169
3271
  description: "Safe wrapper around common git operations. Supports status, log, diff, commit, branch, checkout, stash, push, pull, fetch, reset, worktree, etc. This is the preferred way to interact with git instead of using the raw `bash` or `exec` tools.",
3170
- usageHint: "ALWAYS prefer this tool over raw shell git commands.\n\nKey fields:\n- `command`: one of the supported subcommands (status, log, diff, commit, etc.)\n- Use `message` only for commit operations.\n- Use `files` array for operations that take paths (status, diff, add, etc.).\n- Non-mutating commands (status, log, diff, branch, fetch) are still permission:confirm for safety.\nNever pass raw git flags through `args` for dangerous operations \u2014 use the structured fields.",
3272
+ usageHint: "ALWAYS prefer this tool over raw shell git commands.\n\nKey fields:\n- `command`: one of the supported subcommands (status, log, diff, commit, etc.)\n- Use `message` only for commit operations.\n- Use `files` array for operations that take paths (status, diff, add, etc.).\n- Non-mutating commands (status, log, diff, branch, fetch) are still permission:confirm for safety.\n- For `commit` in a possibly-shared working tree, pass an explicit `files` list scoped to what YOU changed. A bare commit (no `files`) includes ALL staged changes and may capture another agent's half-done work. Heed the `warning` field on the result.\nNever pass raw git flags through `args` for dangerous operations \u2014 use the structured fields.",
3171
3273
  permission: "confirm",
3172
3274
  icon: "git",
3173
3275
  // Conservative: any of these may mutate. The non-mutating commands
@@ -3256,6 +3358,22 @@ var gitTool = {
3256
3358
  };
3257
3359
  }
3258
3360
  const args = buildArgs(input);
3361
+ let safetyWarning;
3362
+ if (input.command === "commit") {
3363
+ try {
3364
+ const report = await assessCommitSafety({
3365
+ cwd: ctx.cwd,
3366
+ projectRoot: ctx.projectRoot,
3367
+ sessionId: ctx.session?.id,
3368
+ signal: opts.signal
3369
+ });
3370
+ if (report.warning) {
3371
+ const scopeNote = input.files ? "" : "\nNote: this commit has no explicit `files` list, so it will include ALL staged changes. Pass `files` to scope the commit to only what you changed.";
3372
+ safetyWarning = report.warning + scopeNote;
3373
+ }
3374
+ } catch {
3375
+ }
3376
+ }
3259
3377
  let stagedDiff;
3260
3378
  if (input.command === "commit" && !input.dry_run) {
3261
3379
  try {
@@ -3269,6 +3387,7 @@ var gitTool = {
3269
3387
  }
3270
3388
  const result = await runGit(args, gitDir, opts.signal);
3271
3389
  if (stagedDiff !== void 0) result.diff = stagedDiff;
3390
+ if (safetyWarning !== void 0) result.warning = safetyWarning;
3272
3391
  return result;
3273
3392
  }
3274
3393
  };
@@ -3380,7 +3499,7 @@ function buildArgs(input) {
3380
3499
  }
3381
3500
  }
3382
3501
  function runGit(args, cwd, signal) {
3383
- return new Promise((resolve7) => {
3502
+ return new Promise((resolve6) => {
3384
3503
  let stdout = "";
3385
3504
  let stderr = "";
3386
3505
  const child = spawn("git", args, {
@@ -3401,7 +3520,7 @@ function runGit(args, cwd, signal) {
3401
3520
  }
3402
3521
  });
3403
3522
  child.on("error", (err) => {
3404
- resolve7({
3523
+ resolve6({
3405
3524
  command: args[0],
3406
3525
  stdout: normalizeCommandOutput(stdout),
3407
3526
  stderr: err.message,
@@ -3410,7 +3529,7 @@ function runGit(args, cwd, signal) {
3410
3529
  });
3411
3530
  });
3412
3531
  child.on("close", (code) => {
3413
- resolve7({
3532
+ resolve6({
3414
3533
  command: args[0],
3415
3534
  stdout: normalizeCommandOutput(stdout),
3416
3535
  stderr: normalizeCommandOutput(stderr),
@@ -3510,7 +3629,7 @@ function stripPathComponents(p, strip) {
3510
3629
  return parts.slice(strip).join("/");
3511
3630
  }
3512
3631
  function runPatch(args, cwd, signal) {
3513
- return new Promise((resolve7) => {
3632
+ return new Promise((resolve6) => {
3514
3633
  let stdout = "";
3515
3634
  let stderr = "";
3516
3635
  const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
@@ -3521,8 +3640,8 @@ function runPatch(args, cwd, signal) {
3521
3640
  child.stderr?.on("data", (c) => {
3522
3641
  stderr += c.toString();
3523
3642
  });
3524
- child.on("close", (code) => resolve7({ exitCode: code ?? 1, stdout, stderr }));
3525
- child.on("error", (e) => resolve7({ exitCode: 1, stdout: "", stderr: e.message }));
3643
+ child.on("close", (code) => resolve6({ exitCode: code ?? 1, stdout, stderr }));
3644
+ child.on("error", (e) => resolve6({ exitCode: 1, stdout: "", stderr: e.message }));
3526
3645
  });
3527
3646
  }
3528
3647
  function extractPatchedFiles(output) {
@@ -3608,8 +3727,8 @@ var jsonTool = {
3608
3727
  };
3609
3728
  }
3610
3729
  };
3611
- function query(data, path21) {
3612
- const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
3730
+ function query(data, path20) {
3731
+ const parts = path20.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
3613
3732
  let current = data;
3614
3733
  for (const part of parts) {
3615
3734
  if (current === null || current === void 0) return void 0;
@@ -3750,7 +3869,7 @@ function findGitDir2(cwd) {
3750
3869
  return null;
3751
3870
  }
3752
3871
  function runGit2(args, cwd, signal) {
3753
- return new Promise((resolve7) => {
3872
+ return new Promise((resolve6) => {
3754
3873
  let stdout = "";
3755
3874
  let stderr = "";
3756
3875
  const child = spawn("git", args, {
@@ -3766,8 +3885,8 @@ function runGit2(args, cwd, signal) {
3766
3885
  child.stderr?.on("data", (c) => {
3767
3886
  stderr += c.toString();
3768
3887
  });
3769
- child.on("close", (code) => resolve7({ stdout, stderr, exitCode: code ?? 0 }));
3770
- child.on("error", (e) => resolve7({ stdout: "", stderr: e.message, exitCode: 1 }));
3888
+ child.on("close", (code) => resolve6({ stdout, stderr, exitCode: code ?? 0 }));
3889
+ child.on("error", (e) => resolve6({ stdout: "", stderr: e.message, exitCode: 1 }));
3771
3890
  });
3772
3891
  }
3773
3892
  async function fileDiff(input, ctx, _signal) {
@@ -4085,8 +4204,8 @@ async function* spawnStream(opts) {
4085
4204
  try {
4086
4205
  for (; ; ) {
4087
4206
  while (queue.length === 0) {
4088
- await new Promise((resolve7) => {
4089
- waiter = resolve7;
4207
+ await new Promise((resolve6) => {
4208
+ waiter = resolve6;
4090
4209
  });
4091
4210
  }
4092
4211
  const chunk = queue.shift();
@@ -4839,12 +4958,15 @@ var outdatedTool = {
4839
4958
  // fixed four sibling tools (mcp_control, shellcheck, shellcheck_scan,
4840
4959
  // web_search) but missed this one; applying the same contract here.
4841
4960
  mutating: true,
4842
- // Capability is just "network" — the tool only hits the package
4961
+ // Capability is outbound network — the tool only hits the package
4843
4962
  // registry over HTTP, never touches the filesystem or runs shell.
4963
+ // Use the canonical `net.outbound` capability (not the non-existent
4964
+ // `network` string) so the subagent allowlist recognises it and
4965
+ // permits read-only registry lookups under a director.
4844
4966
  // The H7 invariant test requires this array to be non-empty for
4845
4967
  // any mutating:true tool (meta-tools whitelisted). See
4846
4968
  // tests/permission-mutating-invariant.test.ts:92.
4847
- capabilities: ["network"],
4969
+ capabilities: ["net.outbound"],
4848
4970
  timeoutMs: 6e4,
4849
4971
  inputSchema: {
4850
4972
  type: "object",
@@ -4875,7 +4997,7 @@ var outdatedTool = {
4875
4997
  }
4876
4998
  };
4877
4999
  function runOutdated(manager, args, cwd, signal) {
4878
- return new Promise((resolve7) => {
5000
+ return new Promise((resolve6) => {
4879
5001
  let stdout = "";
4880
5002
  let stderr = "";
4881
5003
  const MAX = 1e5;
@@ -4891,10 +5013,10 @@ function runOutdated(manager, args, cwd, signal) {
4891
5013
  });
4892
5014
  child.on("close", (code) => {
4893
5015
  const result = parseOutdatedOutput(stdout, code ?? 0);
4894
- resolve7(result);
5016
+ resolve6(result);
4895
5017
  });
4896
5018
  child.on("error", (e) => {
4897
- resolve7({
5019
+ resolve6({
4898
5020
  exit_code: 1,
4899
5021
  packages: [],
4900
5022
  total: 0,
@@ -5020,7 +5142,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
5020
5142
  };
5021
5143
  }
5022
5144
  args.push("--timestamps", service);
5023
- return new Promise((resolve7) => {
5145
+ return new Promise((resolve6) => {
5024
5146
  let stdout = "";
5025
5147
  let stderr = "";
5026
5148
  const MAX = 2e5;
@@ -5036,7 +5158,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
5036
5158
  if (settled) return;
5037
5159
  settled = true;
5038
5160
  clearTimeout(timer);
5039
- resolve7(result);
5161
+ resolve6(result);
5040
5162
  };
5041
5163
  const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
5042
5164
  const timer = setTimeout(() => {
@@ -5071,7 +5193,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
5071
5193
  }
5072
5194
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
5073
5195
  var MAX_TAIL_LINES = 1e5;
5074
- async function fileLogs(path21, lines, filterRe, stream) {
5196
+ async function fileLogs(path20, lines, filterRe, stream) {
5075
5197
  const { createInterface } = await import('node:readline');
5076
5198
  const { createReadStream } = await import('node:fs');
5077
5199
  const entries = [];
@@ -5080,7 +5202,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
5080
5202
  let writeIdx = 0;
5081
5203
  let totalLines = 0;
5082
5204
  const rl = createInterface({
5083
- input: createReadStream(path21),
5205
+ input: createReadStream(path20),
5084
5206
  crlfDelay: Number.POSITIVE_INFINITY
5085
5207
  });
5086
5208
  for await (const line of rl) {
@@ -5101,7 +5223,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
5101
5223
  if (parsed) entries.push(parsed);
5102
5224
  }
5103
5225
  return {
5104
- source: path21,
5226
+ source: path20,
5105
5227
  entries,
5106
5228
  total: entries.length,
5107
5229
  truncated: totalLines > effLines,
@@ -8129,8 +8251,9 @@ async function loadGitignoreMatcher(projectRoot) {
8129
8251
 
8130
8252
  // src/codebase-index/indexer.ts
8131
8253
  var YIELD_EVERY_N = 50;
8254
+ var PARALLEL_BATCH = 20;
8132
8255
  function yieldEventLoop() {
8133
- return new Promise((resolve7) => setImmediate(resolve7));
8256
+ return new Promise((resolve6) => setImmediate(resolve6));
8134
8257
  }
8135
8258
  function throwIfAborted(signal) {
8136
8259
  if (!signal?.aborted) return;
@@ -8262,97 +8385,108 @@ async function runIndexerWithStore(store, opts) {
8262
8385
  if (!force) {
8263
8386
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
8264
8387
  }
8265
- for (let fi = 0; fi < files.length; fi++) {
8266
- const file = expectDefined(files[fi]);
8267
- opts.onProgress?.(fi + 1, files.length);
8268
- if (fi > 0 && fi % YIELD_EVERY_N === 0) {
8388
+ for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
8389
+ const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
8390
+ const batchFiles = files.slice(batchStart, batchEnd);
8391
+ opts.onProgress?.(batchEnd, files.length);
8392
+ if (batchStart > 0 && batchStart % YIELD_EVERY_N === 0) {
8269
8393
  await yieldEventLoop();
8270
8394
  throwIfAborted(signal);
8271
8395
  }
8272
- let stat11;
8273
- try {
8274
- const statOpts = signal ? { signal } : {};
8275
- stat11 = await fs4.stat(file, statOpts);
8276
- } catch (e) {
8277
- if (isAbortError(e)) throw e;
8278
- store.deleteFile(file);
8279
- continue;
8280
- }
8281
- if (!stat11.isFile()) continue;
8282
- const lang = detectLang(file);
8283
- if (!lang) continue;
8284
- const meta = existingMeta.get(file);
8285
- if (!force && meta && meta.mtimeMs === Math.floor(stat11.mtimeMs)) {
8286
- langStats[lang] = (langStats[lang] ?? 0) + meta.symbolCount;
8287
- symbolsIndexed += meta.symbolCount;
8288
- filesIndexed++;
8289
- continue;
8290
- }
8291
- store.deleteRefsForFile(file);
8292
- store.deleteSymbolsForFile(file);
8293
- let content;
8294
- try {
8295
- content = await fs4.readFile(file, { encoding: "utf8", signal });
8296
- } catch (e) {
8297
- if (isAbortError(e)) throw e;
8298
- errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
8299
- continue;
8300
- }
8301
- let parsed;
8302
- try {
8303
- parsed = await parseFile(file, content, lang);
8304
- } catch (e) {
8305
- errors.push(`parse error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
8306
- continue;
8307
- }
8308
- if (parsed.symbols.length === 0) {
8309
- store.upsertFile({
8310
- file,
8311
- lang,
8312
- mtimeMs: Math.floor(stat11.mtimeMs),
8313
- symbolCount: 0,
8314
- lastIndexed: Date.now()
8315
- });
8316
- filesIndexed++;
8317
- continue;
8318
- }
8319
- const nextId = store.getMaxSymbolId() + 1;
8320
- const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
8321
- store.insertSymbols(symbolsWithIds, nextId);
8322
- const count = symbolsWithIds.length;
8323
- symbolsIndexed += count;
8324
- langStats[lang] = (langStats[lang] ?? 0) + count;
8325
- if (parsed.refs && parsed.refs.length > 0) {
8326
- const refsByLine = /* @__PURE__ */ new Map();
8327
- for (const r of parsed.refs) {
8328
- let arr = refsByLine.get(r.line);
8329
- if (!arr) {
8330
- arr = [];
8331
- refsByLine.set(r.line, arr);
8396
+ const statOpts = signal ? { signal } : {};
8397
+ const statReadParse = await Promise.allSettled(
8398
+ batchFiles.map(async (file) => {
8399
+ let stat11;
8400
+ try {
8401
+ stat11 = await fs4.stat(file, statOpts);
8402
+ } catch (e) {
8403
+ if (isAbortError(e)) throw e;
8404
+ return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
8332
8405
  }
8333
- arr.push(r);
8334
- }
8335
- const batch = [];
8336
- for (const sym of symbolsWithIds) {
8337
- const symRefs = refsByLine.get(sym.line);
8338
- if (symRefs) {
8339
- for (const r of symRefs) {
8340
- batch.push({ ...r, fromId: sym.id });
8341
- }
8406
+ if (!stat11.isFile()) return { file, stat: stat11, lang: "", parsed: null };
8407
+ const lang = detectLang(file);
8408
+ if (!lang) return { file, stat: stat11, lang: "", parsed: null };
8409
+ let content;
8410
+ try {
8411
+ content = await fs4.readFile(file, { encoding: "utf8", signal });
8412
+ } catch (e) {
8413
+ if (isAbortError(e)) throw e;
8414
+ return { file, stat: stat11, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
8415
+ }
8416
+ let parsed;
8417
+ try {
8418
+ parsed = await parseFile(file, content, lang);
8419
+ } catch (e) {
8420
+ return { file, stat: stat11, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
8421
+ }
8422
+ return { file, stat: stat11, lang, parsed, content };
8423
+ })
8424
+ );
8425
+ for (let fi = 0; fi < statReadParse.length; fi++) {
8426
+ const settled = statReadParse[fi];
8427
+ const file = expectDefined(batchFiles[fi]);
8428
+ if (settled.status === "rejected") {
8429
+ const err = settled.reason;
8430
+ if (err instanceof Error && isAbortError(err)) throw err;
8431
+ errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
8432
+ continue;
8433
+ }
8434
+ const result = settled.value;
8435
+ if (result.error) {
8436
+ if (result.stat) store.deleteFile(file);
8437
+ if (result.error.includes("error:")) errors.push(result.error);
8438
+ continue;
8439
+ }
8440
+ const { stat: stat11, lang, parsed } = result;
8441
+ if (!lang || !parsed) {
8442
+ if (lang) {
8443
+ store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: 0, lastIndexed: Date.now() });
8444
+ filesIndexed++;
8342
8445
  }
8446
+ continue;
8343
8447
  }
8344
- if (batch.length > 0) {
8345
- store.insertRefsBatch(batch);
8448
+ const meta = existingMeta.get(file);
8449
+ if (!force && meta && meta.mtimeMs === Math.floor(stat11.mtimeMs)) {
8450
+ langStats[lang] = (langStats[lang] ?? 0) + meta.symbolCount;
8451
+ symbolsIndexed += meta.symbolCount;
8452
+ filesIndexed++;
8453
+ continue;
8454
+ }
8455
+ store.deleteRefsForFile(file);
8456
+ store.deleteSymbolsForFile(file);
8457
+ if (parsed.symbols.length === 0) {
8458
+ store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: 0, lastIndexed: Date.now() });
8459
+ filesIndexed++;
8460
+ continue;
8461
+ }
8462
+ const nextId = store.getMaxSymbolId() + 1;
8463
+ const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
8464
+ store.insertSymbols(symbolsWithIds, nextId);
8465
+ const count = symbolsWithIds.length;
8466
+ symbolsIndexed += count;
8467
+ langStats[lang] = (langStats[lang] ?? 0) + count;
8468
+ if (parsed.refs && parsed.refs.length > 0) {
8469
+ const refsByLine = /* @__PURE__ */ new Map();
8470
+ for (const r of parsed.refs) {
8471
+ let arr = refsByLine.get(r.line);
8472
+ if (!arr) {
8473
+ arr = [];
8474
+ refsByLine.set(r.line, arr);
8475
+ }
8476
+ arr.push(r);
8477
+ }
8478
+ const batch = [];
8479
+ for (const sym of symbolsWithIds) {
8480
+ const symRefs = refsByLine.get(sym.line);
8481
+ if (symRefs) {
8482
+ for (const r of symRefs) batch.push({ ...r, fromId: sym.id });
8483
+ }
8484
+ }
8485
+ if (batch.length > 0) store.insertRefsBatch(batch);
8346
8486
  }
8487
+ store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: count, lastIndexed: Date.now() });
8488
+ filesIndexed++;
8347
8489
  }
8348
- store.upsertFile({
8349
- file,
8350
- lang,
8351
- mtimeMs: Math.floor(stat11.mtimeMs),
8352
- symbolCount: count,
8353
- lastIndexed: Date.now()
8354
- });
8355
- filesIndexed++;
8356
8490
  }
8357
8491
  for (const [file_] of existingMeta) {
8358
8492
  try {
@@ -8534,7 +8668,7 @@ function shutdownCodebaseIndexHost() {
8534
8668
  function callIndexOp(op, args, opts) {
8535
8669
  const w = ensureWorker();
8536
8670
  if (!w) return callInline(op, args, opts);
8537
- return new Promise((resolve7, reject) => {
8671
+ return new Promise((resolve6, reject) => {
8538
8672
  const id = nextRpcId++;
8539
8673
  const timer = setTimeout(() => {
8540
8674
  pending.delete(id);
@@ -8557,7 +8691,7 @@ function callIndexOp(op, args, opts) {
8557
8691
  pending.set(id, {
8558
8692
  resolve: (v) => {
8559
8693
  cleanup();
8560
- resolve7(v);
8694
+ resolve6(v);
8561
8695
  },
8562
8696
  reject: (e) => {
8563
8697
  cleanup();
@@ -9566,8 +9700,8 @@ var TOOL_ICON_CONFIG = {
9566
9700
  // emerald
9567
9701
  folder: { icon: "folder", color: "#8b5cf6" },
9568
9702
  // violet
9569
- terminal: { icon: "terminal", color: "#ef4444" },
9570
- // red
9703
+ terminal: { icon: "terminal", color: "#fb923c" },
9704
+ // orange
9571
9705
  web: { icon: "web", color: "#06b6d4" },
9572
9706
  // cyan
9573
9707
  git: { icon: "git", color: "#f97316" },