@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/builtin.js CHANGED
@@ -1,6 +1,6 @@
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, isPrivateIPv4, isPrivateIPv6, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
3
+ import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
4
4
  import * as fs from 'node:fs';
5
5
  import { statSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
6
6
  import * as fs14 from 'node:fs/promises';
@@ -763,8 +763,8 @@ async function* spawnStream(opts) {
763
763
  try {
764
764
  for (; ; ) {
765
765
  while (queue.length === 0) {
766
- await new Promise((resolve7) => {
767
- waiter = resolve7;
766
+ await new Promise((resolve6) => {
767
+ waiter = resolve6;
768
768
  });
769
769
  }
770
770
  const chunk = queue.shift();
@@ -1319,10 +1319,10 @@ var bashTool = {
1319
1319
  queue.push(c);
1320
1320
  }
1321
1321
  };
1322
- const next = () => new Promise((resolve7) => {
1322
+ const next = () => new Promise((resolve6) => {
1323
1323
  const c = queue.shift();
1324
- if (c) resolve7(c);
1325
- else resolveNext = resolve7;
1324
+ if (c) resolve6(c);
1325
+ else resolveNext = resolve6;
1326
1326
  });
1327
1327
  let lastFlush = Date.now();
1328
1328
  const flush = () => {
@@ -3530,8 +3530,9 @@ async function loadGitignoreMatcher(projectRoot) {
3530
3530
 
3531
3531
  // src/codebase-index/indexer.ts
3532
3532
  var YIELD_EVERY_N = 50;
3533
+ var PARALLEL_BATCH = 20;
3533
3534
  function yieldEventLoop() {
3534
- return new Promise((resolve7) => setImmediate(resolve7));
3535
+ return new Promise((resolve6) => setImmediate(resolve6));
3535
3536
  }
3536
3537
  function throwIfAborted(signal) {
3537
3538
  if (!signal?.aborted) return;
@@ -3663,97 +3664,108 @@ async function runIndexerWithStore(store, opts) {
3663
3664
  if (!force) {
3664
3665
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
3665
3666
  }
3666
- for (let fi = 0; fi < files.length; fi++) {
3667
- const file = expectDefined(files[fi]);
3668
- opts.onProgress?.(fi + 1, files.length);
3669
- if (fi > 0 && fi % YIELD_EVERY_N === 0) {
3667
+ for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
3668
+ const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
3669
+ const batchFiles = files.slice(batchStart, batchEnd);
3670
+ opts.onProgress?.(batchEnd, files.length);
3671
+ if (batchStart > 0 && batchStart % YIELD_EVERY_N === 0) {
3670
3672
  await yieldEventLoop();
3671
3673
  throwIfAborted(signal);
3672
3674
  }
3673
- let stat11;
3674
- try {
3675
- const statOpts = signal ? { signal } : {};
3676
- stat11 = await fs14.stat(file, statOpts);
3677
- } catch (e) {
3678
- if (isAbortError(e)) throw e;
3679
- store.deleteFile(file);
3680
- continue;
3681
- }
3682
- if (!stat11.isFile()) continue;
3683
- const lang = detectLang(file);
3684
- if (!lang) continue;
3685
- const meta = existingMeta.get(file);
3686
- if (!force && meta && meta.mtimeMs === Math.floor(stat11.mtimeMs)) {
3687
- langStats[lang] = (langStats[lang] ?? 0) + meta.symbolCount;
3688
- symbolsIndexed += meta.symbolCount;
3689
- filesIndexed++;
3690
- continue;
3691
- }
3692
- store.deleteRefsForFile(file);
3693
- store.deleteSymbolsForFile(file);
3694
- let content;
3695
- try {
3696
- content = await fs14.readFile(file, { encoding: "utf8", signal });
3697
- } catch (e) {
3698
- if (isAbortError(e)) throw e;
3699
- errors.push(`read error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
3700
- continue;
3701
- }
3702
- let parsed;
3703
- try {
3704
- parsed = await parseFile(file, content, lang);
3705
- } catch (e) {
3706
- errors.push(`parse error: ${file}: ${e instanceof Error ? e.message : String(e)}`);
3707
- continue;
3708
- }
3709
- if (parsed.symbols.length === 0) {
3710
- store.upsertFile({
3711
- file,
3712
- lang,
3713
- mtimeMs: Math.floor(stat11.mtimeMs),
3714
- symbolCount: 0,
3715
- lastIndexed: Date.now()
3716
- });
3717
- filesIndexed++;
3718
- continue;
3719
- }
3720
- const nextId = store.getMaxSymbolId() + 1;
3721
- const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
3722
- store.insertSymbols(symbolsWithIds, nextId);
3723
- const count = symbolsWithIds.length;
3724
- symbolsIndexed += count;
3725
- langStats[lang] = (langStats[lang] ?? 0) + count;
3726
- if (parsed.refs && parsed.refs.length > 0) {
3727
- const refsByLine = /* @__PURE__ */ new Map();
3728
- for (const r of parsed.refs) {
3729
- let arr = refsByLine.get(r.line);
3730
- if (!arr) {
3731
- arr = [];
3732
- refsByLine.set(r.line, arr);
3675
+ const statOpts = signal ? { signal } : {};
3676
+ const statReadParse = await Promise.allSettled(
3677
+ batchFiles.map(async (file) => {
3678
+ let stat11;
3679
+ try {
3680
+ stat11 = await fs14.stat(file, statOpts);
3681
+ } catch (e) {
3682
+ if (isAbortError(e)) throw e;
3683
+ return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
3733
3684
  }
3734
- arr.push(r);
3735
- }
3736
- const batch = [];
3737
- for (const sym of symbolsWithIds) {
3738
- const symRefs = refsByLine.get(sym.line);
3739
- if (symRefs) {
3740
- for (const r of symRefs) {
3741
- batch.push({ ...r, fromId: sym.id });
3742
- }
3685
+ if (!stat11.isFile()) return { file, stat: stat11, lang: "", parsed: null };
3686
+ const lang = detectLang(file);
3687
+ if (!lang) return { file, stat: stat11, lang: "", parsed: null };
3688
+ let content;
3689
+ try {
3690
+ content = await fs14.readFile(file, { encoding: "utf8", signal });
3691
+ } catch (e) {
3692
+ if (isAbortError(e)) throw e;
3693
+ return { file, stat: stat11, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
3694
+ }
3695
+ let parsed;
3696
+ try {
3697
+ parsed = await parseFile(file, content, lang);
3698
+ } catch (e) {
3699
+ return { file, stat: stat11, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
3743
3700
  }
3701
+ return { file, stat: stat11, lang, parsed, content };
3702
+ })
3703
+ );
3704
+ for (let fi = 0; fi < statReadParse.length; fi++) {
3705
+ const settled = statReadParse[fi];
3706
+ const file = expectDefined(batchFiles[fi]);
3707
+ if (settled.status === "rejected") {
3708
+ const err = settled.reason;
3709
+ if (err instanceof Error && isAbortError(err)) throw err;
3710
+ errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
3711
+ continue;
3712
+ }
3713
+ const result = settled.value;
3714
+ if (result.error) {
3715
+ if (result.stat) store.deleteFile(file);
3716
+ if (result.error.includes("error:")) errors.push(result.error);
3717
+ continue;
3744
3718
  }
3745
- if (batch.length > 0) {
3746
- store.insertRefsBatch(batch);
3719
+ const { stat: stat11, lang, parsed } = result;
3720
+ if (!lang || !parsed) {
3721
+ if (lang) {
3722
+ store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: 0, lastIndexed: Date.now() });
3723
+ filesIndexed++;
3724
+ }
3725
+ continue;
3747
3726
  }
3727
+ const meta = existingMeta.get(file);
3728
+ if (!force && meta && meta.mtimeMs === Math.floor(stat11.mtimeMs)) {
3729
+ langStats[lang] = (langStats[lang] ?? 0) + meta.symbolCount;
3730
+ symbolsIndexed += meta.symbolCount;
3731
+ filesIndexed++;
3732
+ continue;
3733
+ }
3734
+ store.deleteRefsForFile(file);
3735
+ store.deleteSymbolsForFile(file);
3736
+ if (parsed.symbols.length === 0) {
3737
+ store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: 0, lastIndexed: Date.now() });
3738
+ filesIndexed++;
3739
+ continue;
3740
+ }
3741
+ const nextId = store.getMaxSymbolId() + 1;
3742
+ const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
3743
+ store.insertSymbols(symbolsWithIds, nextId);
3744
+ const count = symbolsWithIds.length;
3745
+ symbolsIndexed += count;
3746
+ langStats[lang] = (langStats[lang] ?? 0) + count;
3747
+ if (parsed.refs && parsed.refs.length > 0) {
3748
+ const refsByLine = /* @__PURE__ */ new Map();
3749
+ for (const r of parsed.refs) {
3750
+ let arr = refsByLine.get(r.line);
3751
+ if (!arr) {
3752
+ arr = [];
3753
+ refsByLine.set(r.line, arr);
3754
+ }
3755
+ arr.push(r);
3756
+ }
3757
+ const batch = [];
3758
+ for (const sym of symbolsWithIds) {
3759
+ const symRefs = refsByLine.get(sym.line);
3760
+ if (symRefs) {
3761
+ for (const r of symRefs) batch.push({ ...r, fromId: sym.id });
3762
+ }
3763
+ }
3764
+ if (batch.length > 0) store.insertRefsBatch(batch);
3765
+ }
3766
+ store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: count, lastIndexed: Date.now() });
3767
+ filesIndexed++;
3748
3768
  }
3749
- store.upsertFile({
3750
- file,
3751
- lang,
3752
- mtimeMs: Math.floor(stat11.mtimeMs),
3753
- symbolCount: count,
3754
- lastIndexed: Date.now()
3755
- });
3756
- filesIndexed++;
3757
3769
  }
3758
3770
  for (const [file_] of existingMeta) {
3759
3771
  try {
@@ -3920,7 +3932,7 @@ function terminateWorker(reason) {
3920
3932
  function callIndexOp(op, args, opts) {
3921
3933
  const w = ensureWorker();
3922
3934
  if (!w) return callInline(op, args, opts);
3923
- return new Promise((resolve7, reject) => {
3935
+ return new Promise((resolve6, reject) => {
3924
3936
  const id = nextRpcId++;
3925
3937
  const timer = setTimeout(() => {
3926
3938
  pending.delete(id);
@@ -3943,7 +3955,7 @@ function callIndexOp(op, args, opts) {
3943
3955
  pending.set(id, {
3944
3956
  resolve: (v) => {
3945
3957
  cleanup();
3946
- resolve7(v);
3958
+ resolve6(v);
3947
3959
  },
3948
3960
  reject: (e) => {
3949
3961
  cleanup();
@@ -4369,7 +4381,7 @@ function findGitDir(cwd) {
4369
4381
  return null;
4370
4382
  }
4371
4383
  function runGit(args, cwd, signal) {
4372
- return new Promise((resolve7) => {
4384
+ return new Promise((resolve6) => {
4373
4385
  let stdout = "";
4374
4386
  let stderr = "";
4375
4387
  const child = spawn("git", args, {
@@ -4385,8 +4397,8 @@ function runGit(args, cwd, signal) {
4385
4397
  child.stderr?.on("data", (c) => {
4386
4398
  stderr += c.toString();
4387
4399
  });
4388
- child.on("close", (code) => resolve7({ stdout, stderr, exitCode: code ?? 0 }));
4389
- child.on("error", (e) => resolve7({ stdout: "", stderr: e.message, exitCode: 1 }));
4400
+ child.on("close", (code) => resolve6({ stdout, stderr, exitCode: code ?? 0 }));
4401
+ child.on("error", (e) => resolve6({ stdout: "", stderr: e.message, exitCode: 1 }));
4390
4402
  });
4391
4403
  }
4392
4404
  async function fileDiff(input, ctx, _signal) {
@@ -4571,8 +4583,8 @@ function processFile(content, absPath, _style, _overwrite, target) {
4571
4583
  var editTool = {
4572
4584
  name: "edit",
4573
4585
  category: "Filesystem",
4574
- 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.",
4575
- 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.",
4586
+ 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.",
4587
+ 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.",
4576
4588
  permission: "confirm",
4577
4589
  mutating: true,
4578
4590
  capabilities: ["fs.write"],
@@ -4601,9 +4613,7 @@ var editTool = {
4601
4613
  throw err;
4602
4614
  });
4603
4615
  if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
4604
- if (!ctx.hasRead(absPath)) {
4605
- throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
4606
- }
4616
+ const autoRead = !ctx.hasRead(absPath);
4607
4617
  const original = await fs14.readFile(absPath, "utf8");
4608
4618
  const updated = await fs14.stat(absPath);
4609
4619
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
@@ -4611,15 +4621,21 @@ var editTool = {
4611
4621
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
4612
4622
  throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
4613
4623
  }
4624
+ if (autoRead && updated.mtimeMs > stat11.mtimeMs + mtimeTolerance) {
4625
+ throw new Error(`edit: file "${input.path}" changed while being auto-read. Retry the edit.`);
4626
+ }
4627
+ 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;
4614
4628
  const style = detectNewlineStyle(original);
4615
4629
  const fileLf = normalizeToLf(original);
4616
4630
  const oldLf = normalizeToLf(input.old_string);
4617
4631
  const newLf = normalizeToLf(input.new_string);
4618
4632
  if (oldLf === newLf) {
4633
+ if (autoRead) ctx.recordRead(absPath, updated.mtimeMs);
4619
4634
  return {
4620
4635
  path: absPath,
4621
4636
  replacements: 0,
4622
- diff: "(no-op: old and new are identical)"
4637
+ diff: "(no-op: old and new are identical)",
4638
+ note: autoReadNote
4623
4639
  };
4624
4640
  }
4625
4641
  let count = 0;
@@ -4660,7 +4676,8 @@ var editTool = {
4660
4676
  return {
4661
4677
  path: absPath,
4662
4678
  replacements: input.replace_all ? count : 1,
4663
- diff
4679
+ diff,
4680
+ note: autoReadNote
4664
4681
  };
4665
4682
  }
4666
4683
  };
@@ -4872,26 +4889,26 @@ var execTool = {
4872
4889
  allowed: false
4873
4890
  };
4874
4891
  }
4875
- const requestedCwd = input.cwd ? path3.resolve(ctx.projectRoot, input.cwd) : ctx.cwd;
4876
- const rel = path3.relative(ctx.projectRoot, requestedCwd);
4877
- if (rel.startsWith("..") || path3.isAbsolute(rel)) {
4892
+ let cwd;
4893
+ try {
4894
+ cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(ctx.cwd, ctx);
4895
+ } catch {
4878
4896
  return {
4879
4897
  command: cmd,
4880
4898
  args,
4881
4899
  stdout: "",
4882
- stderr: `cwd "${input.cwd}" resolves outside project root`,
4900
+ stderr: `cwd "${input.cwd ?? ctx.cwd}" resolves outside project root`,
4883
4901
  exitCode: 1,
4884
4902
  truncated: false,
4885
4903
  allowed: false
4886
4904
  };
4887
4905
  }
4888
- const cwd = requestedCwd;
4889
4906
  const signal = opts.signal;
4890
4907
  return runCommand(cmd, args, cwd, timeout, signal, ctx.session?.id);
4891
4908
  }
4892
4909
  };
4893
4910
  function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
4894
- return new Promise((resolve7) => {
4911
+ return new Promise((resolve6) => {
4895
4912
  let stdout = "";
4896
4913
  let stderr = "";
4897
4914
  let killed = false;
@@ -4946,7 +4963,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
4946
4963
  const exitCode = killed ? 124 : code ?? 1;
4947
4964
  registry.afterCall(durationMs, exitCode !== 0);
4948
4965
  const spooled = spool.finalize();
4949
- resolve7({
4966
+ resolve6({
4950
4967
  command: cmd,
4951
4968
  args,
4952
4969
  stdout: normalizeCommandOutput(stdout) + (spooled ? spoolNote(spooled) : ""),
@@ -4962,7 +4979,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
4962
4979
  if (typeof pid === "number") registry.unregister(pid);
4963
4980
  registry.afterCall(Date.now() - startedAt, true);
4964
4981
  spool.finalize();
4965
- resolve7({
4982
+ resolve6({
4966
4983
  command: cmd,
4967
4984
  args,
4968
4985
  stdout: normalizeCommandOutput(stdout),
@@ -5336,7 +5353,7 @@ var gitTool = {
5336
5353
  name: "git",
5337
5354
  category: "Git",
5338
5355
  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.",
5339
- 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.",
5356
+ 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.",
5340
5357
  permission: "confirm",
5341
5358
  icon: "git",
5342
5359
  // Conservative: any of these may mutate. The non-mutating commands
@@ -5425,6 +5442,22 @@ var gitTool = {
5425
5442
  };
5426
5443
  }
5427
5444
  const args = buildArgs(input);
5445
+ let safetyWarning;
5446
+ if (input.command === "commit") {
5447
+ try {
5448
+ const report = await assessCommitSafety({
5449
+ cwd: ctx.cwd,
5450
+ projectRoot: ctx.projectRoot,
5451
+ sessionId: ctx.session?.id,
5452
+ signal: opts.signal
5453
+ });
5454
+ if (report.warning) {
5455
+ 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.";
5456
+ safetyWarning = report.warning + scopeNote;
5457
+ }
5458
+ } catch {
5459
+ }
5460
+ }
5428
5461
  let stagedDiff;
5429
5462
  if (input.command === "commit" && !input.dry_run) {
5430
5463
  try {
@@ -5438,6 +5471,7 @@ var gitTool = {
5438
5471
  }
5439
5472
  const result = await runGit2(args, gitDir, opts.signal);
5440
5473
  if (stagedDiff !== void 0) result.diff = stagedDiff;
5474
+ if (safetyWarning !== void 0) result.warning = safetyWarning;
5441
5475
  return result;
5442
5476
  }
5443
5477
  };
@@ -5549,7 +5583,7 @@ function buildArgs(input) {
5549
5583
  }
5550
5584
  }
5551
5585
  function runGit2(args, cwd, signal) {
5552
- return new Promise((resolve7) => {
5586
+ return new Promise((resolve6) => {
5553
5587
  let stdout = "";
5554
5588
  let stderr = "";
5555
5589
  const child = spawn("git", args, {
@@ -5570,7 +5604,7 @@ function runGit2(args, cwd, signal) {
5570
5604
  }
5571
5605
  });
5572
5606
  child.on("error", (err) => {
5573
- resolve7({
5607
+ resolve6({
5574
5608
  command: args[0],
5575
5609
  stdout: normalizeCommandOutput(stdout),
5576
5610
  stderr: err.message,
@@ -5579,7 +5613,7 @@ function runGit2(args, cwd, signal) {
5579
5613
  });
5580
5614
  });
5581
5615
  child.on("close", (code) => {
5582
- resolve7({
5616
+ resolve6({
5583
5617
  command: args[0],
5584
5618
  stdout: normalizeCommandOutput(stdout),
5585
5619
  stderr: normalizeCommandOutput(stderr),
@@ -5802,13 +5836,13 @@ var grepTool = {
5802
5836
  }
5803
5837
  };
5804
5838
  async function detectRg(signal) {
5805
- return new Promise((resolve7) => {
5839
+ return new Promise((resolve6) => {
5806
5840
  try {
5807
5841
  const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", signal, windowsHide: true });
5808
- p.on("error", () => resolve7(false));
5809
- p.on("close", (code) => resolve7(code === 0));
5842
+ p.on("error", () => resolve6(false));
5843
+ p.on("close", (code) => resolve6(code === 0));
5810
5844
  } catch {
5811
- resolve7(false);
5845
+ resolve6(false);
5812
5846
  }
5813
5847
  });
5814
5848
  }
@@ -6223,8 +6257,8 @@ var jsonTool = {
6223
6257
  };
6224
6258
  }
6225
6259
  };
6226
- function query(data, path21) {
6227
- const parts = path21.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
6260
+ function query(data, path20) {
6261
+ const parts = path20.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
6228
6262
  let current = data;
6229
6263
  for (const part of parts) {
6230
6264
  if (current === null || current === void 0) return void 0;
@@ -6450,7 +6484,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
6450
6484
  };
6451
6485
  }
6452
6486
  args.push("--timestamps", service);
6453
- return new Promise((resolve7) => {
6487
+ return new Promise((resolve6) => {
6454
6488
  let stdout = "";
6455
6489
  let stderr = "";
6456
6490
  const MAX = 2e5;
@@ -6466,7 +6500,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
6466
6500
  if (settled) return;
6467
6501
  settled = true;
6468
6502
  clearTimeout(timer);
6469
- resolve7(result);
6503
+ resolve6(result);
6470
6504
  };
6471
6505
  const child = spawn("docker", args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
6472
6506
  const timer = setTimeout(() => {
@@ -6501,7 +6535,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
6501
6535
  }
6502
6536
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
6503
6537
  var MAX_TAIL_LINES = 1e5;
6504
- async function fileLogs(path21, lines, filterRe, stream) {
6538
+ async function fileLogs(path20, lines, filterRe, stream) {
6505
6539
  const { createInterface } = await import('node:readline');
6506
6540
  const { createReadStream } = await import('node:fs');
6507
6541
  const entries = [];
@@ -6510,7 +6544,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
6510
6544
  let writeIdx = 0;
6511
6545
  let totalLines = 0;
6512
6546
  const rl = createInterface({
6513
- input: createReadStream(path21),
6547
+ input: createReadStream(path20),
6514
6548
  crlfDelay: Number.POSITIVE_INFINITY
6515
6549
  });
6516
6550
  for await (const line of rl) {
@@ -6531,7 +6565,7 @@ async function fileLogs(path21, lines, filterRe, stream) {
6531
6565
  if (parsed) entries.push(parsed);
6532
6566
  }
6533
6567
  return {
6534
- source: path21,
6568
+ source: path20,
6535
6569
  entries,
6536
6570
  total: entries.length,
6537
6571
  truncated: totalLines > effLines,
@@ -6589,12 +6623,15 @@ var outdatedTool = {
6589
6623
  // fixed four sibling tools (mcp_control, shellcheck, shellcheck_scan,
6590
6624
  // web_search) but missed this one; applying the same contract here.
6591
6625
  mutating: true,
6592
- // Capability is just "network" — the tool only hits the package
6626
+ // Capability is outbound network — the tool only hits the package
6593
6627
  // registry over HTTP, never touches the filesystem or runs shell.
6628
+ // Use the canonical `net.outbound` capability (not the non-existent
6629
+ // `network` string) so the subagent allowlist recognises it and
6630
+ // permits read-only registry lookups under a director.
6594
6631
  // The H7 invariant test requires this array to be non-empty for
6595
6632
  // any mutating:true tool (meta-tools whitelisted). See
6596
6633
  // tests/permission-mutating-invariant.test.ts:92.
6597
- capabilities: ["network"],
6634
+ capabilities: ["net.outbound"],
6598
6635
  timeoutMs: 6e4,
6599
6636
  inputSchema: {
6600
6637
  type: "object",
@@ -6625,7 +6662,7 @@ var outdatedTool = {
6625
6662
  }
6626
6663
  };
6627
6664
  function runOutdated(manager, args, cwd, signal) {
6628
- return new Promise((resolve7) => {
6665
+ return new Promise((resolve6) => {
6629
6666
  let stdout = "";
6630
6667
  let stderr = "";
6631
6668
  const MAX = 1e5;
@@ -6641,10 +6678,10 @@ function runOutdated(manager, args, cwd, signal) {
6641
6678
  });
6642
6679
  child.on("close", (code) => {
6643
6680
  const result = parseOutdatedOutput(stdout, code ?? 0);
6644
- resolve7(result);
6681
+ resolve6(result);
6645
6682
  });
6646
6683
  child.on("error", (e) => {
6647
- resolve7({
6684
+ resolve6({
6648
6685
  exit_code: 1,
6649
6686
  packages: [],
6650
6687
  total: 0,
@@ -6778,7 +6815,7 @@ function stripPathComponents(p, strip) {
6778
6815
  return parts.slice(strip).join("/");
6779
6816
  }
6780
6817
  function runPatch(args, cwd, signal) {
6781
- return new Promise((resolve7) => {
6818
+ return new Promise((resolve6) => {
6782
6819
  let stdout = "";
6783
6820
  let stderr = "";
6784
6821
  const env = { ...buildChildEnv(), LANG: "C", LC_ALL: "C" };
@@ -6789,8 +6826,8 @@ function runPatch(args, cwd, signal) {
6789
6826
  child.stderr?.on("data", (c) => {
6790
6827
  stderr += c.toString();
6791
6828
  });
6792
- child.on("close", (code) => resolve7({ exitCode: code ?? 1, stdout, stderr }));
6793
- child.on("error", (e) => resolve7({ exitCode: 1, stdout: "", stderr: e.message }));
6829
+ child.on("close", (code) => resolve6({ exitCode: code ?? 1, stdout, stderr }));
6830
+ child.on("error", (e) => resolve6({ exitCode: 1, stdout: "", stderr: e.message }));
6794
6831
  });
6795
6832
  }
6796
6833
  function extractPatchedFiles(output) {
@@ -7087,6 +7124,11 @@ var readTool = {
7087
7124
  limit: {
7088
7125
  type: "integer",
7089
7126
  description: "Maximum number of lines to return (default is 2000)."
7127
+ },
7128
+ mode: {
7129
+ type: "string",
7130
+ enum: ["content", "summary"],
7131
+ description: "Return full line-numbered content (default) or a compact file summary with imports/exports/symbols."
7090
7132
  }
7091
7133
  },
7092
7134
  required: ["path"]
@@ -7100,14 +7142,27 @@ var readTool = {
7100
7142
  } catch (err) {
7101
7143
  const code = err.code;
7102
7144
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
7103
- throw new Error(
7104
- `read: failed to stat "${input.path}": ${toErrorMessage(err)}`
7105
- );
7145
+ throw new Error(`read: failed to stat "${input.path}": ${toErrorMessage(err)}`);
7106
7146
  }
7107
7147
  if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
7108
7148
  if (stat11.size > MAX_BYTES2) {
7109
7149
  throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES2})`);
7110
7150
  }
7151
+ const offset = Math.max(1, input.offset ?? 1);
7152
+ const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
7153
+ const prior = getReadRangeRecord(ctx, absPath);
7154
+ const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
7155
+ if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat11.mtimeMs, offset, requestedEnd)) {
7156
+ ctx.recordRead(absPath, stat11.mtimeMs);
7157
+ return {
7158
+ 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.]`,
7159
+ total_lines: prior.totalLines,
7160
+ encoding: "utf8",
7161
+ truncated: requestedEnd < prior.totalLines,
7162
+ cached: true,
7163
+ note: "Repeated read suppressed to save tokens."
7164
+ };
7165
+ }
7111
7166
  const buf = await fs14.readFile(absPath);
7112
7167
  if (isBinaryBuffer(buf)) {
7113
7168
  throw new Error(`read: "${input.path}" appears to be binary`);
@@ -7115,17 +7170,38 @@ var readTool = {
7115
7170
  const text = buf.toString("utf8");
7116
7171
  const allLines = text.split(/\r\n|\r|\n/);
7117
7172
  const total = allLines.length;
7118
- const offset = Math.max(1, input.offset ?? 1);
7119
- const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
7173
+ if (input.mode === "summary") {
7174
+ ctx.recordRead(absPath, stat11.mtimeMs);
7175
+ rememberReadRange(ctx, absPath, stat11.mtimeMs, total, 1, Math.min(total, 200));
7176
+ return {
7177
+ text: summarizeFile(input.path, stat11.size, allLines),
7178
+ total_lines: total,
7179
+ encoding: "utf8",
7180
+ truncated: total > 200,
7181
+ note: "Summary mode returned compact structure instead of full file content."
7182
+ };
7183
+ }
7120
7184
  if (limit === 0) {
7121
7185
  ctx.recordRead(absPath, stat11.mtimeMs);
7186
+ rememberReadRange(ctx, absPath, stat11.mtimeMs, total, 1, 0);
7122
7187
  return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
7123
7188
  }
7189
+ if (offset > total) {
7190
+ ctx.recordRead(absPath, stat11.mtimeMs);
7191
+ rememberReadRange(ctx, absPath, stat11.mtimeMs, total, total + 1, total + 1);
7192
+ return {
7193
+ text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
7194
+ total_lines: total,
7195
+ encoding: "utf8",
7196
+ truncated: false
7197
+ };
7198
+ }
7124
7199
  const slice = allLines.slice(offset - 1, offset - 1 + limit);
7125
7200
  const truncated = offset - 1 + slice.length < total;
7126
7201
  const width = String(offset + slice.length - 1).length;
7127
7202
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
7128
7203
  ctx.recordRead(absPath, stat11.mtimeMs);
7204
+ rememberReadRange(ctx, absPath, stat11.mtimeMs, total, offset, offset + slice.length - 1);
7129
7205
  return {
7130
7206
  text: numbered,
7131
7207
  total_lines: total,
@@ -7134,6 +7210,62 @@ var readTool = {
7134
7210
  };
7135
7211
  }
7136
7212
  };
7213
+ var READ_RANGES_META_KEY = "tools.read.ranges.v1";
7214
+ function getReadRanges(ctx) {
7215
+ const existing = ctx.meta[READ_RANGES_META_KEY];
7216
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) {
7217
+ return existing;
7218
+ }
7219
+ const next = {};
7220
+ ctx.meta[READ_RANGES_META_KEY] = next;
7221
+ return next;
7222
+ }
7223
+ function getReadRangeRecord(ctx, absPath) {
7224
+ return getReadRanges(ctx)[absPath];
7225
+ }
7226
+ function rememberReadRange(ctx, absPath, mtimeMs, totalLines, start, end) {
7227
+ if (end < start) return;
7228
+ const ranges = getReadRanges(ctx);
7229
+ const prior = ranges[absPath];
7230
+ const nextRanges = prior && Math.abs(prior.mtimeMs - mtimeMs) <= 1 ? prior.ranges.slice() : [];
7231
+ nextRanges.push({ start, end });
7232
+ ranges[absPath] = {
7233
+ mtimeMs,
7234
+ totalLines,
7235
+ ranges: mergeRanges(nextRanges)
7236
+ };
7237
+ }
7238
+ function coversRange(record, mtimeMs, start, end) {
7239
+ if (Math.abs(record.mtimeMs - mtimeMs) > 1) return false;
7240
+ return record.ranges.some((range) => range.start <= start && range.end >= end);
7241
+ }
7242
+ function mergeRanges(ranges) {
7243
+ const sorted = ranges.slice().sort((a, b) => a.start - b.start);
7244
+ const merged = [];
7245
+ for (const range of sorted) {
7246
+ const last = merged[merged.length - 1];
7247
+ if (!last || range.start > last.end + 1) {
7248
+ merged.push({ ...range });
7249
+ continue;
7250
+ }
7251
+ last.end = Math.max(last.end, range.end);
7252
+ }
7253
+ return merged;
7254
+ }
7255
+ function summarizeFile(filePath, bytes, lines) {
7256
+ const interesting = lines.map((line, index) => ({ line: line.trim(), number: index + 1 })).filter(
7257
+ ({ 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(
7258
+ line
7259
+ )
7260
+ ).slice(0, 80).map(({ line, number }) => `${number}: ${line}`);
7261
+ return [
7262
+ `summary: ${filePath}`,
7263
+ `bytes=${bytes}`,
7264
+ `total_lines=${lines.length}`,
7265
+ interesting.length > 0 ? `symbols/imports:
7266
+ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
7267
+ ].join("\n");
7268
+ }
7137
7269
  var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
7138
7270
  var replaceTool = {
7139
7271
  name: "replace",
@@ -7270,13 +7402,13 @@ async function globFiles(pattern, base, extraGlob) {
7270
7402
  return await globNative(pattern, base, extraGlob);
7271
7403
  }
7272
7404
  function checkRg() {
7273
- return new Promise((resolve7) => {
7405
+ return new Promise((resolve6) => {
7274
7406
  try {
7275
7407
  const p = spawn("rg", ["--version"], { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
7276
- p.on("error", () => resolve7(false));
7277
- p.on("close", (code) => resolve7(code === 0));
7408
+ p.on("error", () => resolve6(false));
7409
+ p.on("close", (code) => resolve6(code === 0));
7278
7410
  } catch {
7279
- resolve7(false);
7411
+ resolve6(false);
7280
7412
  }
7281
7413
  });
7282
7414
  }
@@ -7293,10 +7425,10 @@ function spawnRgFind(pattern, base) {
7293
7425
  buf += chunk.toString();
7294
7426
  });
7295
7427
  return {
7296
- promise: new Promise((resolve7, reject) => {
7428
+ promise: new Promise((resolve6, reject) => {
7297
7429
  child.on("error", reject);
7298
7430
  child.on("close", () => {
7299
- resolve7(buf.split("\n").filter(Boolean));
7431
+ resolve6(buf.split("\n").filter(Boolean));
7300
7432
  });
7301
7433
  })
7302
7434
  };