@wrongstack/tools 0.305.0 → 0.306.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/_shell-pick.d.ts +4 -5
  2. package/dist/_util.d.ts +22 -5
  3. package/dist/audit.d.ts +0 -1
  4. package/dist/audit.js +135 -46
  5. package/dist/bash.js +61 -37
  6. package/dist/browser/index.js +29 -9
  7. package/dist/browser/types.d.ts +7 -1
  8. package/dist/builtin.js +1280 -728
  9. package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
  10. package/dist/codebase-index/index.d.ts +1 -0
  11. package/dist/codebase-index/index.js +225 -153
  12. package/dist/codebase-index/project-server-endpoint.d.ts +1 -2
  13. package/dist/codebase-index/project-server.js +19 -20
  14. package/dist/diff.d.ts +5 -0
  15. package/dist/diff.js +78 -12
  16. package/dist/document.js +18 -6
  17. package/dist/edit.js +69 -16
  18. package/dist/exec.js +44 -22
  19. package/dist/fetch.js +13 -1
  20. package/dist/format.d.ts +4 -2
  21. package/dist/format.js +81 -31
  22. package/dist/glob.js +12 -4
  23. package/dist/grep.d.ts +2 -0
  24. package/dist/grep.js +15 -4
  25. package/dist/index.js +1357 -765
  26. package/dist/install.js +96 -37
  27. package/dist/kanban-tool-types.d.ts +6 -1
  28. package/dist/kanban.js +60 -0
  29. package/dist/languages/index.js +28 -13
  30. package/dist/lint.js +28 -13
  31. package/dist/logs.d.ts +0 -1
  32. package/dist/logs.js +44 -13
  33. package/dist/memory.d.ts +8 -0
  34. package/dist/memory.js +23 -3
  35. package/dist/mode.d.ts +1 -1
  36. package/dist/mode.js +3 -0
  37. package/dist/next-steps.d.ts +2 -3
  38. package/dist/next-steps.js +3 -3
  39. package/dist/outdated.d.ts +0 -3
  40. package/dist/outdated.js +89 -48
  41. package/dist/pack.js +1280 -728
  42. package/dist/plan.js +76 -3
  43. package/dist/process-registry.d.ts +8 -2
  44. package/dist/process-registry.js +28 -13
  45. package/dist/ps-slash.js +22 -12
  46. package/dist/read.js +10 -4
  47. package/dist/replace.d.ts +4 -0
  48. package/dist/replace.js +104 -7
  49. package/dist/search.d.ts +6 -0
  50. package/dist/search.js +47 -26
  51. package/dist/session-kanban.d.ts +25 -0
  52. package/dist/session-kanban.js +16 -0
  53. package/dist/skill.d.ts +6 -0
  54. package/dist/skill.js +9 -10
  55. package/dist/task.js +66 -2
  56. package/dist/test.js +28 -13
  57. package/dist/todo.js +64 -2
  58. package/dist/tool-icons.js +4 -2
  59. package/dist/tool-summary.d.ts +1 -1
  60. package/dist/tool-summary.js +76 -1
  61. package/dist/tool-tier.js +1280 -728
  62. package/dist/tree.js +9 -10
  63. package/dist/typecheck.d.ts +0 -2
  64. package/dist/typecheck.js +98 -31
  65. package/dist/write.js +58 -10
  66. package/package.json +4 -3
package/dist/builtin.js CHANGED
@@ -422,8 +422,11 @@ var init_redact_command = __esm({
422
422
  /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
423
423
  // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
424
424
  // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
425
+ // The value must be token-like (>= 8 chars) so ordinary combined flags such
426
+ // as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
427
+ // redacted, not just the first.
425
428
  // NOTE: synced with @wrongstack/core observability/redact-command.ts.
426
- /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
429
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
427
430
  // -p|-password|-a (redis auth) short flags: attached + separated + =value.
428
431
  // Same token-start anchor; over-redaction is an accepted tradeoff for a
429
432
  // redaction function. Synced with core copy.
@@ -431,8 +434,9 @@ var init_redact_command = __esm({
431
434
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
432
435
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
433
436
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
434
- // when preceded by a flag name (e.g. --github-token=EyJ...).
435
- /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/
437
+ // when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
438
+ // every such flag in the command line is redacted, not just the first.
439
+ /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
436
440
  ];
437
441
  }
438
442
  });
@@ -518,11 +522,15 @@ var init_process_registry = __esm({
518
522
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
519
523
  }
520
524
  _canSignalProcessGroup(p) {
521
- return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
525
+ return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
522
526
  }
523
527
  _killChildDirect(p, signal) {
524
528
  try {
525
- p.child.kill(signal);
529
+ if (p.child) {
530
+ p.child.kill(signal);
531
+ return;
532
+ }
533
+ if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
526
534
  } catch {
527
535
  }
528
536
  }
@@ -720,15 +728,15 @@ var init_process_registry = __esm({
720
728
  this._pruneStale(pid);
721
729
  const p = this.processes.get(pid);
722
730
  if (!p) return false;
723
- if (p.killed) return true;
731
+ if (p.killed && opts.force !== true) return true;
724
732
  if (p.protected && opts.includeProtected !== true) return false;
725
733
  if (opts.preserveBackground && p.background) return false;
726
734
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
727
735
  const isWin5 = os.platform() === "win32";
728
736
  if (isWin5) {
729
- const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
737
+ const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
730
738
  const directFallback = () => {
731
- if (p.child.exitCode === null) {
739
+ if (p.child && p.child.exitCode === null) {
732
740
  try {
733
741
  p.child.kill("SIGKILL");
734
742
  } catch {
@@ -740,10 +748,7 @@ var init_process_registry = __esm({
740
748
  onSettled: directFallback
741
749
  })) {
742
750
  } else {
743
- try {
744
- p.child.kill(force ? "SIGKILL" : "SIGTERM");
745
- } catch {
746
- }
751
+ this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
747
752
  }
748
753
  p.killed = true;
749
754
  return true;
@@ -754,7 +759,7 @@ var init_process_registry = __esm({
754
759
  } else {
755
760
  this._killPosix(p, "SIGTERM");
756
761
  const timer = setTimeout(() => {
757
- if (this.processes.has(pid) && !p.child.killed) {
762
+ if (this.processes.has(pid) && !p.child?.killed) {
758
763
  this._killPosix(p, "SIGKILL");
759
764
  }
760
765
  }, graceMs);
@@ -809,6 +814,16 @@ var init_process_registry = __esm({
809
814
  * before reusing a PID, but we want to clean up before that becomes a risk.
810
815
  */
811
816
  _isStaleEntry(entry) {
817
+ if (entry.child === null) {
818
+ if (Date.now() - entry.startedAt <= 6e4) return false;
819
+ if (os.platform() === "win32") return false;
820
+ try {
821
+ process.kill(entry.pid, 0);
822
+ return false;
823
+ } catch (err) {
824
+ return err.code !== "EPERM";
825
+ }
826
+ }
812
827
  return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
813
828
  }
814
829
  /**
@@ -1010,8 +1025,8 @@ async function* spawnStream(opts) {
1010
1025
  try {
1011
1026
  for (; ; ) {
1012
1027
  while (queue.length === 0) {
1013
- await new Promise((resolve16) => {
1014
- waiter = resolve16;
1028
+ await new Promise((resolve17) => {
1029
+ waiter = resolve17;
1015
1030
  });
1016
1031
  }
1017
1032
  const chunk = queue.shift();
@@ -1099,19 +1114,48 @@ import * as Core from "@wrongstack/core/utils";
1099
1114
  function sha256hex(content) {
1100
1115
  return createHash("sha256").update(content, "utf8").digest("hex");
1101
1116
  }
1102
- async function detectPackageManager(cwd) {
1103
- const { stat: stat18 } = await import("node:fs/promises");
1104
- try {
1105
- await stat18(`${cwd}/pnpm-lock.yaml`);
1106
- return "pnpm";
1107
- } catch {
1117
+ async function detectPackageManager(cwd, stopAt) {
1118
+ let dir = path3.resolve(cwd);
1119
+ const stop = stopAt ? path3.resolve(stopAt) : dir;
1120
+ for (; ; ) {
1121
+ const found = await detectPackageManagerInDir(dir);
1122
+ if (found) return found;
1123
+ if (dir === stop) break;
1124
+ const parent = path3.dirname(dir);
1125
+ const relParent = path3.relative(stop, parent);
1126
+ if (parent === dir || relParent.startsWith("..") || path3.isAbsolute(relParent)) break;
1127
+ dir = parent;
1108
1128
  }
1129
+ return "npm";
1130
+ }
1131
+ async function detectPackageManagerInDir(dir) {
1132
+ const fs35 = await import("node:fs/promises");
1109
1133
  try {
1110
- await stat18(`${cwd}/yarn.lock`);
1111
- return "yarn";
1134
+ const raw = await fs35.readFile(path3.join(dir, "package.json"), "utf8");
1135
+ const declared = JSON.parse(raw).packageManager;
1136
+ if (typeof declared === "string") {
1137
+ const name = declared.split("@")[0] ?? "";
1138
+ if (name === "pnpm" || name === "yarn") return name;
1139
+ if (name === "npm" || name === "bun") return "npm";
1140
+ }
1112
1141
  } catch {
1113
1142
  }
1114
- return "npm";
1143
+ const lockfiles = [
1144
+ ["pnpm-lock.yaml", "pnpm"],
1145
+ ["yarn.lock", "yarn"],
1146
+ ["bun.lockb", "npm"],
1147
+ ["bun.lock", "npm"],
1148
+ ["package-lock.json", "npm"],
1149
+ ["npm-shrinkwrap.json", "npm"]
1150
+ ];
1151
+ for (const [file, manager] of lockfiles) {
1152
+ try {
1153
+ await fs35.stat(`${dir}/${file}`);
1154
+ return manager;
1155
+ } catch {
1156
+ }
1157
+ }
1158
+ return null;
1115
1159
  }
1116
1160
  function resolvePath(input, ctx) {
1117
1161
  return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
@@ -1170,6 +1214,20 @@ async function safeResolveReal(input, ctx) {
1170
1214
  const abs = safeResolve(input, ctx);
1171
1215
  return await resolveRealInsideRoot(abs, ctx);
1172
1216
  }
1217
+ function truncateDiffPayload(diff, maxBytes) {
1218
+ const total = Buffer.byteLength(diff, "utf8");
1219
+ if (total <= maxBytes) return { text: diff, truncated: false };
1220
+ const MARKER_RESERVE = 96;
1221
+ let head = takeHeadBytes(diff, Math.max(0, maxBytes - MARKER_RESERVE));
1222
+ const nl = head.lastIndexOf("\n");
1223
+ if (nl > 0) head = head.slice(0, nl);
1224
+ const kept = Buffer.byteLength(head, "utf8");
1225
+ return {
1226
+ text: `${head}
1227
+ \u2026[diff truncated: ${total - kept} of ${total} bytes omitted]`,
1228
+ truncated: true
1229
+ };
1230
+ }
1173
1231
  function truncateMiddle(s, max) {
1174
1232
  if (Buffer.byteLength(s, "utf8") <= max) return s;
1175
1233
  const half = Math.floor(max / 2);
@@ -2732,8 +2790,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
2732
2790
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2733
2791
  }
2734
2792
  }
2735
- function collectFileEvidence(directory, fullPath, basename13, profiles, state) {
2736
- const lower = basename13.toLowerCase();
2793
+ function collectFileEvidence(directory, fullPath, basename14, profiles, state) {
2794
+ const lower = basename14.toLowerCase();
2737
2795
  const extension = path4.extname(lower);
2738
2796
  for (const profile of profiles) {
2739
2797
  const detector = profile.detectors.find(
@@ -2744,7 +2802,7 @@ function collectFileEvidence(directory, fullPath, basename13, profiles, state) {
2744
2802
  candidate.evidence.push({
2745
2803
  kind: detector.kind,
2746
2804
  path: fullPath,
2747
- value: basename13,
2805
+ value: basename14,
2748
2806
  weight: detector.weight
2749
2807
  });
2750
2808
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -2872,8 +2930,8 @@ function normalizeLimits(input) {
2872
2930
  async function canonicalDirectory(input) {
2873
2931
  const resolved = path4.resolve(input);
2874
2932
  const real = await fs2.realpath(resolved);
2875
- const stat18 = await fs2.stat(real);
2876
- if (!stat18.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2933
+ const stat19 = await fs2.stat(real);
2934
+ if (!stat19.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2877
2935
  return real;
2878
2936
  }
2879
2937
  async function canonicalInside(input, root, label) {
@@ -3772,8 +3830,8 @@ async function executeInternal(options, startedAt) {
3772
3830
  const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
3773
3831
  if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
3774
3832
  const safeTarget = await assertContainedFile(target, options.projectRoot);
3775
- const stat18 = await fs4.stat(safeTarget);
3776
- if (stat18.size > MAX_INTERNAL_SOURCE_BYTES) {
3833
+ const stat19 = await fs4.stat(safeTarget);
3834
+ if (stat19.size > MAX_INTERNAL_SOURCE_BYTES) {
3777
3835
  return unavailableResult(
3778
3836
  options,
3779
3837
  `Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
@@ -4060,8 +4118,8 @@ async function snapshotPaths(paths) {
4060
4118
  const existing = [];
4061
4119
  for (const candidate of paths) {
4062
4120
  try {
4063
- const stat18 = await fs4.stat(candidate);
4064
- if (!stat18.isFile()) continue;
4121
+ const stat19 = await fs4.stat(candidate);
4122
+ if (!stat19.isFile()) continue;
4065
4123
  existing.push(candidate);
4066
4124
  } catch {
4067
4125
  }
@@ -4072,15 +4130,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
4072
4130
  const beforeSet = new Set(before);
4073
4131
  const afterSet = new Set(after);
4074
4132
  const changed = /* @__PURE__ */ new Set();
4075
- for (const path39 of after) {
4076
- if (!beforeSet.has(path39)) changed.add(path39);
4133
+ for (const path40 of after) {
4134
+ if (!beforeSet.has(path40)) changed.add(path40);
4077
4135
  }
4078
- for (const path39 of before) {
4079
- if (!afterSet.has(path39)) changed.add(path39);
4136
+ for (const path40 of before) {
4137
+ if (!afterSet.has(path40)) changed.add(path40);
4080
4138
  }
4081
4139
  if (beforeSizes && afterSizes) {
4082
- for (const path39 of after) {
4083
- if (beforeSizes.get(path39) !== afterSizes.get(path39)) changed.add(path39);
4140
+ for (const path40 of after) {
4141
+ if (beforeSizes.get(path40) !== afterSizes.get(path40)) changed.add(path40);
4084
4142
  }
4085
4143
  }
4086
4144
  return [...changed].sort();
@@ -4089,8 +4147,8 @@ async function snapshotSizes(paths) {
4089
4147
  const sizes = /* @__PURE__ */ new Map();
4090
4148
  for (const candidate of paths) {
4091
4149
  try {
4092
- const stat18 = await fs4.stat(candidate);
4093
- if (stat18.isFile()) sizes.set(candidate, stat18.size);
4150
+ const stat19 = await fs4.stat(candidate);
4151
+ if (stat19.isFile()) sizes.set(candidate, stat19.size);
4094
4152
  } catch {
4095
4153
  }
4096
4154
  }
@@ -5545,7 +5603,7 @@ async function syncGoParse(filePath, content, lang) {
5545
5603
  }
5546
5604
  const goBinary = resolveWin32Command("go");
5547
5605
  const goResult = await new Promise(
5548
- (resolve16, reject) => {
5606
+ (resolve17, reject) => {
5549
5607
  let settled = false;
5550
5608
  const proc = spawn5(goBinary, ["run", scriptPath], {
5551
5609
  stdio: ["pipe", "pipe", "pipe"],
@@ -5574,7 +5632,7 @@ async function syncGoParse(filePath, content, lang) {
5574
5632
  if (settled) return;
5575
5633
  settled = true;
5576
5634
  clearTimeout(timer);
5577
- resolve16({ code: code2, stdout: stdout2 });
5635
+ resolve17({ code: code2, stdout: stdout2 });
5578
5636
  });
5579
5637
  }
5580
5638
  );
@@ -6239,7 +6297,7 @@ async function resolvePython() {
6239
6297
  return null;
6240
6298
  }
6241
6299
  function commandIsAvailable(command) {
6242
- return new Promise((resolve16) => {
6300
+ return new Promise((resolve17) => {
6243
6301
  let settled = false;
6244
6302
  const proc = spawn6(command, ["--version"], {
6245
6303
  stdio: "ignore",
@@ -6249,7 +6307,7 @@ function commandIsAvailable(command) {
6249
6307
  if (settled) return;
6250
6308
  settled = true;
6251
6309
  clearTimeout(timer);
6252
- resolve16(available);
6310
+ resolve17(available);
6253
6311
  };
6254
6312
  const timer = setTimeout(() => {
6255
6313
  proc.kill("SIGKILL");
@@ -6261,7 +6319,7 @@ function commandIsAvailable(command) {
6261
6319
  });
6262
6320
  }
6263
6321
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
6264
- return new Promise((resolve16, reject) => {
6322
+ return new Promise((resolve17, reject) => {
6265
6323
  let settled = false;
6266
6324
  const proc = spawn6(pyBinary, [scriptPath, filePath], {
6267
6325
  stdio: ["pipe", "pipe", "pipe"],
@@ -6290,7 +6348,7 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
6290
6348
  if (settled) return;
6291
6349
  settled = true;
6292
6350
  clearTimeout(timer);
6293
- resolve16({ code, stdout });
6351
+ resolve17({ code, stdout });
6294
6352
  });
6295
6353
  });
6296
6354
  }
@@ -6699,9 +6757,9 @@ function parseSymbols6(opts) {
6699
6757
  function regexParse2(opts) {
6700
6758
  const { file, content, lang } = opts;
6701
6759
  const symbols = [];
6702
- const basename13 = path22.basename(file).toLowerCase();
6703
- const isPackageJson = basename13 === "package.json";
6704
- const isTsconfig = basename13 === "tsconfig.json" || basename13 === "tsconfig.build.json";
6760
+ const basename14 = path22.basename(file).toLowerCase();
6761
+ const isPackageJson = basename14 === "package.json";
6762
+ const isTsconfig = basename14 === "tsconfig.json" || basename14 === "tsconfig.build.json";
6705
6763
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
6706
6764
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
6707
6765
  const lines = content.split("\n");
@@ -7631,11 +7689,18 @@ var init_tree_sitter_parser = __esm({
7631
7689
  init_spawn_stream();
7632
7690
  init_util();
7633
7691
  init_legacy_bridge();
7692
+ var SEVERITY_RANK = {
7693
+ info: 0,
7694
+ low: 1,
7695
+ moderate: 2,
7696
+ high: 3,
7697
+ critical: 4
7698
+ };
7634
7699
  var auditTool = {
7635
7700
  name: "audit",
7636
7701
  category: "Package Management",
7637
7702
  description: "Run a security audit against project dependencies (using pnpm/npm audit). Reports known vulnerabilities with severity.",
7638
- 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.",
7703
+ usageHint: "CRITICAL SECURITY TOOL:\n\n- Run regularly and especially before any release.\n- Use `level` to focus on high/critical issues.\n- This tool is read-only: to remediate, use `install` (or `language_package`) to upgrade the affected packages.\nThis is one of the most important tools for supply chain security.",
7639
7704
  permission: "confirm",
7640
7705
  mutating: false,
7641
7706
  capabilities: ["shell.restricted"],
@@ -7650,8 +7715,10 @@ var auditTool = {
7650
7715
  enum: ["low", "moderate", "high", "critical"],
7651
7716
  description: "Minimum severity level to report"
7652
7717
  },
7653
- fix: { type: "boolean", description: "Attempt to fix vulnerabilities (default: false)" },
7654
- packages: { type: "string", description: "Specific package(s) to audit (comma-separated)" }
7718
+ fix: {
7719
+ type: "boolean",
7720
+ description: "Deprecated and rejected \u2014 this tool is read-only and never modifies dependencies. Use `install` (or `language_package`) to remediate vulnerabilities."
7721
+ }
7655
7722
  }
7656
7723
  },
7657
7724
  async execute(input, ctx, opts) {
@@ -7665,6 +7732,11 @@ var auditTool = {
7665
7732
  return final;
7666
7733
  },
7667
7734
  async *executeStream(input, ctx, opts) {
7735
+ if (input.fix === true) {
7736
+ throw new Error(
7737
+ "audit: `fix: true` is not supported \u2014 this tool is read-only (mutating: false). To remediate vulnerabilities, upgrade the affected packages with the `install` tool (or `language_package` for non-JS ecosystems)."
7738
+ );
7739
+ }
7668
7740
  const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
7669
7741
  const bridge = await tryLegacyPackageOperation("package-audit", {
7670
7742
  cwd,
@@ -7694,13 +7766,11 @@ var auditTool = {
7694
7766
  };
7695
7767
  return;
7696
7768
  }
7697
- const manager = await detectPackageManager(cwd);
7769
+ const manager = await detectPackageManager(cwd, ctx.projectRoot);
7698
7770
  yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
7699
7771
  const args = ["audit", "--json"];
7700
- if (input.fix) args.push("--fix");
7701
- if (input.packages) {
7702
- const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(",");
7703
- args.push(...pkgs.map((p) => p.trim()));
7772
+ if (input.level && (manager === "npm" || manager === "pnpm")) {
7773
+ args.push(`--audit-level=${input.level}`);
7704
7774
  }
7705
7775
  const result = yield* spawnStream({
7706
7776
  cmd: manager,
@@ -7709,10 +7779,16 @@ var auditTool = {
7709
7779
  signal: opts.signal,
7710
7780
  maxBytes: 1e5
7711
7781
  });
7712
- yield { type: "final", output: parseAuditOutput(result.stdout, result.exitCode) };
7782
+ yield {
7783
+ type: "final",
7784
+ output: parseAuditOutput(result.stdout, result.exitCode, {
7785
+ level: input.level,
7786
+ spawnTruncated: result.truncated
7787
+ })
7788
+ };
7713
7789
  }
7714
7790
  };
7715
- function parseAuditOutput(json2, exitCode) {
7791
+ function parseAuditOutput(json2, exitCode, opts = {}) {
7716
7792
  if (!json2) {
7717
7793
  return {
7718
7794
  exit_code: exitCode,
@@ -7723,18 +7799,14 @@ function parseAuditOutput(json2, exitCode) {
7723
7799
  truncated: false
7724
7800
  };
7725
7801
  }
7802
+ const cappedOutput = normalizeCommandOutput(json2);
7803
+ const truncated = opts.spawnTruncated === true || Buffer.byteLength(json2, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
7726
7804
  try {
7727
7805
  const data = JSON.parse(json2);
7728
- const advisories = [];
7729
- const ads = data.advisories ?? {};
7730
- for (const id of Object.keys(ads)) {
7731
- const adv = ads[id];
7732
- advisories.push({
7733
- severity: adv.severity ?? "unknown",
7734
- package: adv.module_name ?? id,
7735
- title: adv.title ?? "Unknown vulnerability",
7736
- url: adv.url ?? ""
7737
- });
7806
+ let advisories = extractAdvisories(data);
7807
+ const minRank = opts.level ? SEVERITY_RANK[opts.level] ?? 0 : 0;
7808
+ if (minRank > 0) {
7809
+ advisories = advisories.filter((a) => (SEVERITY_RANK[a.severity] ?? 0) >= minRank);
7738
7810
  }
7739
7811
  const total = advisories.length;
7740
7812
  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`;
@@ -7743,8 +7815,8 @@ function parseAuditOutput(json2, exitCode) {
7743
7815
  vulnerabilities: advisories,
7744
7816
  total,
7745
7817
  summary,
7746
- output: json2,
7747
- truncated: json2.length > 1e5
7818
+ output: cappedOutput,
7819
+ truncated
7748
7820
  };
7749
7821
  } catch {
7750
7822
  return {
@@ -7752,15 +7824,47 @@ function parseAuditOutput(json2, exitCode) {
7752
7824
  vulnerabilities: [],
7753
7825
  total: 0,
7754
7826
  summary: "Could not parse audit output",
7755
- output: json2,
7756
- truncated: false
7827
+ output: cappedOutput,
7828
+ truncated
7757
7829
  };
7758
7830
  }
7759
7831
  }
7832
+ function extractAdvisories(data) {
7833
+ const advisories = [];
7834
+ const ads = data["advisories"];
7835
+ if (ads && typeof ads === "object") {
7836
+ for (const [id, value] of Object.entries(ads)) {
7837
+ const adv = value ?? {};
7838
+ advisories.push({
7839
+ severity: typeof adv["severity"] === "string" ? adv["severity"] : "unknown",
7840
+ package: typeof adv["module_name"] === "string" ? adv["module_name"] : id,
7841
+ title: typeof adv["title"] === "string" ? adv["title"] : "Unknown vulnerability",
7842
+ url: typeof adv["url"] === "string" ? adv["url"] : ""
7843
+ });
7844
+ }
7845
+ return advisories;
7846
+ }
7847
+ const vulns = data["vulnerabilities"];
7848
+ if (vulns && typeof vulns === "object") {
7849
+ for (const [pkg, value] of Object.entries(vulns)) {
7850
+ const vuln = value ?? {};
7851
+ const via = Array.isArray(vuln["via"]) ? vuln["via"] : [];
7852
+ const detail = via.find((v) => !!v && typeof v === "object");
7853
+ advisories.push({
7854
+ severity: typeof vuln["severity"] === "string" ? vuln["severity"] : "unknown",
7855
+ package: pkg,
7856
+ title: detail && typeof detail["title"] === "string" ? detail["title"] : "Unknown vulnerability",
7857
+ url: detail && typeof detail["url"] === "string" ? detail["url"] : ""
7858
+ });
7859
+ }
7860
+ }
7861
+ return advisories;
7862
+ }
7760
7863
 
7761
7864
  // src/bash.ts
7762
7865
  import { spawn as spawn3 } from "node:child_process";
7763
7866
  import * as os4 from "node:os";
7867
+ import { StringDecoder } from "node:string_decoder";
7764
7868
  import {
7765
7869
  emitProcessCompleted as emitProcessCompleted2,
7766
7870
  emitProcessOutput as emitProcessOutput2,
@@ -8041,7 +8145,6 @@ var PersistentProcessRegistry = class {
8041
8145
  try {
8042
8146
  const data = await readRegistryFile(this.registryPath);
8043
8147
  data.instances.set(String(entry.pid), entry);
8044
- const child = null;
8045
8148
  this.baseRegistry.register({
8046
8149
  pid: entry.pid,
8047
8150
  name: entry.name,
@@ -8049,7 +8152,7 @@ var PersistentProcessRegistry = class {
8049
8152
  startedAt: entry.startedAt,
8050
8153
  sessionId: entry.sessionId,
8051
8154
  protected: entry.protected,
8052
- child
8155
+ child: null
8053
8156
  });
8054
8157
  await writeRegistryFile(this.registryPath, data);
8055
8158
  } finally {
@@ -8609,7 +8712,7 @@ function looksLikePowerShell(command) {
8609
8712
  return true;
8610
8713
  }
8611
8714
  if (PS_VERB_RE.test(trimmed)) return true;
8612
- if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps|sl|rm|cat|cp|mv)\b/i.test(trimmed)) {
8715
+ if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps)\b/i.test(trimmed)) {
8613
8716
  return true;
8614
8717
  }
8615
8718
  if (looksLikePowerShellExtended(command)) return true;
@@ -8701,7 +8804,7 @@ var bashTool = {
8701
8804
  name: "bash",
8702
8805
  category: "Shell",
8703
8806
  description: "Execute an arbitrary command in the user's default shell (bash/zsh/pwsh/cmd). stdout and stderr are merged into one stream. This is the most powerful and dangerous tool \u2014 it gives the model full access to the developer's machine. Prefer specialized tools whenever possible.",
8704
- usageHint: "SECURITY WARNING: This tool runs with the full privileges of the current user.\n\nBest practices for the model:\n- Strongly prefer `exec` for known safe commands (node, npm, pnpm, tsc, git, etc.).\n- Use bash only when you genuinely need shell features (pipes, redirection, complex one-liners).\n- Prefer single focused commands over huge `&&` chains.\n- Use `background: true` only for long-running processes (dev servers, watchers).\n- The working directory is the project root.\n- Output may be truncated in the middle for very large results.",
8807
+ usageHint: "SECURITY WARNING: This tool runs with the full privileges of the current user.\n\nBest practices for the model:\n- Strongly prefer `exec` for known safe commands (node, npm, pnpm, tsc, git, etc.).\n- Use bash only when you genuinely need shell features (pipes, redirection, complex one-liners).\n- Prefer single focused commands over huge `&&` chains.\n- Use `background: true` only for long-running processes (dev servers, watchers).\n- The working directory is the session working dir (changed via `set_working_dir`), defaulting to the project root.\n- Output may be truncated in the middle for very large results.",
8705
8808
  selection: {
8706
8809
  doNotUseWhen: "the command is allowlisted and does not require pipes, redirection, or shell expansion.",
8707
8810
  useInstead: ["exec"]
@@ -8715,7 +8818,14 @@ var bashTool = {
8715
8818
  // explicitly removes the implicit cross-tool aliasing.
8716
8819
  subjectKey: "command",
8717
8820
  capabilities: ["shell.arbitrary"],
8718
- timeoutMs: 3e5,
8821
+ // Executor-level abort ceiling. Must sit ABOVE the per-call `timeout_ms`
8822
+ // ceiling (600_000): the tool's own timer tree-kills and returns a
8823
+ // structured `timed_out: true` result, while the executor's
8824
+ // AbortSignal.timeout is a blunt abort. The old value (300_000) meant any
8825
+ // timeout_ms > 5min was silently cut short by the executor. The 10s margin
8826
+ // covers the kill/teardown window. (The executor additionally clamps to
8827
+ // config `tools.maxToolTimeoutMs`.)
8828
+ timeoutMs: 61e4,
8719
8829
  maxOutputBytes: MAX_OUTPUT,
8720
8830
  estimatedDurationMs: 3e4,
8721
8831
  inputSchema: {
@@ -8727,7 +8837,7 @@ var bashTool = {
8727
8837
  },
8728
8838
  timeout_ms: {
8729
8839
  type: "integer",
8730
- description: "Optional timeout for this specific command in milliseconds."
8840
+ description: "Optional timeout for this specific command in milliseconds (default 300000, max 600000)."
8731
8841
  },
8732
8842
  background: {
8733
8843
  type: "boolean",
@@ -8778,16 +8888,7 @@ var bashTool = {
8778
8888
  return;
8779
8889
  }
8780
8890
  const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
8781
- if (PIPE_TO_SHELL_PATTERN.test(input.command)) {
8782
- console.warn(JSON.stringify({
8783
- level: "warn",
8784
- event: "bash.pipe_to_shell_detected",
8785
- message: "Detected pipe-to-shell pattern. Consider reviewing the full command before confirming.",
8786
- command_prefix: input.command.slice(0, 100),
8787
- // Log first 100 chars for review
8788
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
8789
- }));
8790
- }
8891
+ const pipeToShellNote = PIPE_TO_SHELL_PATTERN.test(input.command) ? "\n\n[wrongstack] Caution: this command pipes output into a shell interpreter (pipe-to-shell). Piped content executes as arbitrary code \u2014 review the source before trusting the result, and prefer downloading to a file and inspecting it first." : "";
8791
8892
  const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS2, 6e5));
8792
8893
  const isWin5 = os4.platform() === "win32";
8793
8894
  let plan;
@@ -8821,11 +8922,12 @@ var bashTool = {
8821
8922
  const shell = plan.bin;
8822
8923
  const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
8823
8924
  const env = buildChildEnv2(ctx.session?.id);
8925
+ const spawnCwd = ctx.workingDir ?? ctx.projectRoot;
8824
8926
  const detached = !isWin5;
8825
8927
  const startedAt = Date.now();
8826
8928
  if (input.background) {
8827
8929
  const child2 = spawn3(shell, args, {
8828
- cwd: ctx.projectRoot,
8930
+ cwd: spawnCwd,
8829
8931
  env,
8830
8932
  // PowerShell takes the script on stdin (no argv quoting); cmd.exe
8831
8933
  // and POSIX shells ignore stdin when given the command inline.
@@ -8856,7 +8958,7 @@ var bashTool = {
8856
8958
  parentPid: process.pid,
8857
8959
  command: redactCommand(`${shell} ${args.join(" ")}`),
8858
8960
  args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
8859
- cwd: ctx.projectRoot,
8961
+ cwd: spawnCwd,
8860
8962
  background: true,
8861
8963
  startedAt: new Date(startedAt).toISOString()
8862
8964
  });
@@ -8900,7 +9002,9 @@ var bashTool = {
8900
9002
  yield {
8901
9003
  type: "final",
8902
9004
  output: {
8903
- output: "",
9005
+ // Background runs have no captured output; the pipe-to-shell caution
9006
+ // (when present) is the only thing worth surfacing.
9007
+ output: pipeToShellNote.trim(),
8904
9008
  exit_code: null,
8905
9009
  timed_out: false,
8906
9010
  pid: pid2
@@ -8917,7 +9021,7 @@ var bashTool = {
8917
9021
  return;
8918
9022
  }
8919
9023
  const child = spawn3(shell, args, {
8920
- cwd: ctx.projectRoot,
9024
+ cwd: spawnCwd,
8921
9025
  env,
8922
9026
  // PowerShell takes the script on stdin (no argv quoting); cmd.exe
8923
9027
  // and POSIX shells ignore stdin when given the command inline.
@@ -8942,7 +9046,7 @@ var bashTool = {
8942
9046
  parentPid: process.pid,
8943
9047
  command: redactCommand(`${shell} ${args.join(" ")}`),
8944
9048
  args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
8945
- cwd: ctx.projectRoot,
9049
+ cwd: spawnCwd,
8946
9050
  background: false,
8947
9051
  startedAt: new Date(startedAt).toISOString()
8948
9052
  });
@@ -9033,10 +9137,10 @@ var bashTool = {
9033
9137
  queue.push(c);
9034
9138
  }
9035
9139
  };
9036
- const next = () => new Promise((resolve16) => {
9140
+ const next = () => new Promise((resolve17) => {
9037
9141
  const c = queue.shift();
9038
- if (c) resolve16(c);
9039
- else resolveNext = resolve16;
9142
+ if (c) resolve17(c);
9143
+ else resolveNext = resolve17;
9040
9144
  });
9041
9145
  let lastFlush = Date.now();
9042
9146
  const flush = () => {
@@ -9061,8 +9165,10 @@ var bashTool = {
9061
9165
  child.stderr?.resume();
9062
9166
  }
9063
9167
  };
9168
+ const stdoutDecoder = new StringDecoder("utf8");
9169
+ const stderrDecoder = new StringDecoder("utf8");
9064
9170
  const onData = (chunk, stream) => {
9065
- const text = chunk.toString();
9171
+ const text = (stream === "stdout" ? stdoutDecoder : stderrDecoder).write(chunk);
9066
9172
  if (stream === "stdout") stdoutBytes += chunk.byteLength;
9067
9173
  else stderrBytes += chunk.byteLength;
9068
9174
  emitProcessOutput2({ pid, stream, chunk });
@@ -9089,6 +9195,12 @@ var bashTool = {
9089
9195
  if (typeof pid === "number") registry.unregister(pid);
9090
9196
  registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
9091
9197
  completeForeground(timedOut ? 124 : code ?? (signal ? 1 : 0), signal ?? void 0);
9198
+ const tail = stdoutDecoder.end() + stderrDecoder.end();
9199
+ if (tail) {
9200
+ if (buf.length < MAX_OUTPUT) buf += tail.slice(0, MAX_OUTPUT - buf.length);
9201
+ spool.write(tail);
9202
+ pending2 += tail;
9203
+ }
9092
9204
  push({ kind: "end", code });
9093
9205
  });
9094
9206
  try {
@@ -9108,7 +9220,7 @@ var bashTool = {
9108
9220
  output: {
9109
9221
  output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : "") + (hint ? `
9110
9222
 
9111
- ${hint}` : ""),
9223
+ ${hint}` : "") + pipeToShellNote,
9112
9224
  exit_code: c.code,
9113
9225
  timed_out: timedOut
9114
9226
  }
@@ -9170,7 +9282,7 @@ ${hint}` : ""),
9170
9282
  if (!sessionId) return;
9171
9283
  for (const entry of registry.bySession(sessionId)) {
9172
9284
  if (entry.name !== "bash") continue;
9173
- if (entry.child.exitCode !== null) continue;
9285
+ if (entry.child && entry.child.exitCode !== null) continue;
9174
9286
  if (entry.background) continue;
9175
9287
  if (entry.protected) continue;
9176
9288
  registry.kill(entry.pid, { force: true });
@@ -9415,8 +9527,8 @@ function sweepOldArtifacts(root) {
9415
9527
  for (const name of names) {
9416
9528
  const target = path10.join(dir, name);
9417
9529
  try {
9418
- const stat18 = await fs7.stat(target);
9419
- if (stat18.isFile() && stat18.mtimeMs < cutoff) {
9530
+ const stat19 = await fs7.stat(target);
9531
+ if (stat19.isFile() && stat19.mtimeMs < cutoff) {
9420
9532
  await fs7.rm(target, { force: true });
9421
9533
  removed++;
9422
9534
  }
@@ -9444,14 +9556,14 @@ var BrowserArtifactStore = class {
9444
9556
  }
9445
9557
  async record(id, sessionId, kind, target, mimeType) {
9446
9558
  await fs7.chmod(target, 384).catch(() => void 0);
9447
- const [stat18, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
9559
+ const [stat19, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
9448
9560
  const artifact = {
9449
9561
  id,
9450
9562
  kind,
9451
9563
  sensitivity: "sensitive",
9452
9564
  path: target,
9453
9565
  mimeType,
9454
- sizeBytes: stat18.size,
9566
+ sizeBytes: stat19.size,
9455
9567
  sha256,
9456
9568
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
9457
9569
  };
@@ -9491,11 +9603,11 @@ var BrowserArtifactStore = class {
9491
9603
  };
9492
9604
  async function hashFile(target) {
9493
9605
  const hash = createHash3("sha256");
9494
- await new Promise((resolve16, reject) => {
9606
+ await new Promise((resolve17, reject) => {
9495
9607
  const stream = createReadStream(target);
9496
9608
  stream.on("data", (chunk) => hash.update(chunk));
9497
9609
  stream.once("error", reject);
9498
- stream.once("end", resolve16);
9610
+ stream.once("end", resolve17);
9499
9611
  });
9500
9612
  return hash.digest("hex");
9501
9613
  }
@@ -9645,7 +9757,7 @@ var BrowserNetworkGuardProxy = class {
9645
9757
  async start() {
9646
9758
  if (this.url) return this.url;
9647
9759
  if (this.startPromise) return this.startPromise;
9648
- this.startPromise = new Promise((resolve16, reject) => {
9760
+ this.startPromise = new Promise((resolve17, reject) => {
9649
9761
  const onError = (error) => {
9650
9762
  this.server.off("listening", onListening);
9651
9763
  reject(error);
@@ -9658,7 +9770,7 @@ var BrowserNetworkGuardProxy = class {
9658
9770
  return;
9659
9771
  }
9660
9772
  this.url = `http://127.0.0.1:${address.port}`;
9661
- resolve16(this.url);
9773
+ resolve17(this.url);
9662
9774
  };
9663
9775
  this.server.once("error", onError);
9664
9776
  this.server.once("listening", onListening);
@@ -9673,7 +9785,7 @@ var BrowserNetworkGuardProxy = class {
9673
9785
  for (const socket of this.sockets) socket.destroy();
9674
9786
  this.sockets.clear();
9675
9787
  if (!this.server.listening) return;
9676
- await new Promise((resolve16) => this.server.close(() => resolve16()));
9788
+ await new Promise((resolve17) => this.server.close(() => resolve17()));
9677
9789
  }
9678
9790
  async forwardHttp(request2, response) {
9679
9791
  try {
@@ -9703,8 +9815,8 @@ var BrowserNetworkGuardProxy = class {
9703
9815
  upstream.on("error", () => writeProxyError(response, 502, "Bad Gateway"));
9704
9816
  request2.on("aborted", () => upstream.destroy());
9705
9817
  request2.pipe(upstream);
9706
- } catch {
9707
- writeProxyError(response, 403, "Blocked by browser network policy");
9818
+ } catch (error) {
9819
+ writeProxyError(response, 403, policyBlockMessage(error));
9708
9820
  }
9709
9821
  }
9710
9822
  async forwardConnect(request2, client, head) {
@@ -9728,8 +9840,8 @@ var BrowserNetworkGuardProxy = class {
9728
9840
  pipeDuplexPair(upstream, client);
9729
9841
  });
9730
9842
  client.once("close", () => upstream.destroy());
9731
- } catch {
9732
- client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
9843
+ } catch (error) {
9844
+ client.end(rawForbiddenResponse(policyBlockMessage(error)));
9733
9845
  }
9734
9846
  }
9735
9847
  async forwardUpgrade(request2, client, head) {
@@ -9770,9 +9882,9 @@ var BrowserNetworkGuardProxy = class {
9770
9882
  });
9771
9883
  client.once("close", () => upstream?.destroy());
9772
9884
  upstream.end();
9773
- } catch {
9885
+ } catch (error) {
9774
9886
  upstream?.destroy();
9775
- client.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
9887
+ client.end(rawForbiddenResponse(policyBlockMessage(error)));
9776
9888
  }
9777
9889
  }
9778
9890
  resolve(rawUrl) {
@@ -9812,6 +9924,18 @@ function writeProxyError(response, status, message) {
9812
9924
  response.writeHead(status, { "content-type": "text/plain", connection: "close" });
9813
9925
  response.end(message);
9814
9926
  }
9927
+ function policyBlockMessage(error) {
9928
+ const reason = error instanceof Error ? error.message : String(error);
9929
+ return reason ? `Blocked by browser network policy: ${reason}` : "Blocked by browser network policy";
9930
+ }
9931
+ function rawForbiddenResponse(message) {
9932
+ return `HTTP/1.1 403 Forbidden\r
9933
+ Content-Type: text/plain\r
9934
+ Content-Length: ${Buffer.byteLength(message)}\r
9935
+ Connection: close\r
9936
+ \r
9937
+ ` + message;
9938
+ }
9815
9939
 
9816
9940
  // src/browser/manager.ts
9817
9941
  var DEFAULT_OPERATION_TIMEOUT_MS = 3e4;
@@ -10039,13 +10163,21 @@ var BrowserSessionManager = class {
10039
10163
  if (relative12.startsWith("..") || path11.isAbsolute(relative12)) {
10040
10164
  throw new Error("browser: upload files must stay inside the project root");
10041
10165
  }
10042
- const realFile = await fs8.realpath(absolute);
10166
+ let realFile;
10167
+ try {
10168
+ realFile = await fs8.realpath(absolute);
10169
+ } catch (error) {
10170
+ if (error?.code === "ENOENT") {
10171
+ throw new Error(`browser: upload file not found: ${file}`);
10172
+ }
10173
+ throw error;
10174
+ }
10043
10175
  const realRelative = path11.relative(realRoot, realFile);
10044
10176
  if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
10045
10177
  throw new Error("browser: upload files must not escape the project root through a symlink");
10046
10178
  }
10047
- const stat18 = await fs8.stat(realFile);
10048
- if (!stat18.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
10179
+ const stat19 = await fs8.stat(realFile);
10180
+ if (!stat19.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
10049
10181
  resolved.push(realFile);
10050
10182
  }
10051
10183
  await this.runPageOperation(
@@ -10226,7 +10358,7 @@ function pushBounded(target, value, limit) {
10226
10358
  }
10227
10359
  async function abortable(signal, operation, onAbort) {
10228
10360
  signal.throwIfAborted();
10229
- return new Promise((resolve16, reject) => {
10361
+ return new Promise((resolve17, reject) => {
10230
10362
  let settled = false;
10231
10363
  let aborting = false;
10232
10364
  const finish = (fn) => {
@@ -10244,7 +10376,7 @@ async function abortable(signal, operation, onAbort) {
10244
10376
  signal.addEventListener("abort", abort, { once: true });
10245
10377
  operation().then(
10246
10378
  (value) => {
10247
- if (!aborting) finish(() => resolve16(value));
10379
+ if (!aborting) finish(() => resolve17(value));
10248
10380
  },
10249
10381
  (err) => {
10250
10382
  if (!aborting) finish(() => reject(err));
@@ -10301,7 +10433,7 @@ var sessionIdSchema = {
10301
10433
  };
10302
10434
  var browserOpenTool = {
10303
10435
  name: "browser_open",
10304
- description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL.",
10436
+ description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL. Private/localhost origins are blocked by default; enable specific origins via the WRONGSTACK_BROWSER_PRIVATE_ORIGINS env allowlist.",
10305
10437
  usageHint: "browser_open({ url?, width?, height?, trace? })",
10306
10438
  permission: "confirm",
10307
10439
  mutating: true,
@@ -10354,7 +10486,7 @@ var browserStatusTool = {
10354
10486
  };
10355
10487
  var browserNavigateTool = {
10356
10488
  name: "browser_navigate",
10357
- description: "Navigate an owned browser session to an approved http(s) URL.",
10489
+ description: "Navigate an owned browser session to an approved http(s) URL. Private/localhost origins are blocked by default; enable specific origins via the WRONGSTACK_BROWSER_PRIVATE_ORIGINS env allowlist.",
10358
10490
  usageHint: "browser_navigate({ sessionId, url })",
10359
10491
  permission: "confirm",
10360
10492
  mutating: true,
@@ -10693,7 +10825,7 @@ for (const tool of browserTools) tool.timeoutMs ??= 6e4;
10693
10825
  import { spawn as spawn4 } from "node:child_process";
10694
10826
  import * as fs13 from "node:fs";
10695
10827
  import * as net3 from "node:net";
10696
- import { StringDecoder } from "node:string_decoder";
10828
+ import { StringDecoder as StringDecoder2 } from "node:string_decoder";
10697
10829
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10698
10830
  import { checkUnixSocketPath } from "@wrongstack/core/utils";
10699
10831
 
@@ -10800,7 +10932,6 @@ import * as fs12 from "node:fs";
10800
10932
  import * as os5 from "node:os";
10801
10933
  import * as path17 from "node:path";
10802
10934
  import { fileURLToPath } from "node:url";
10803
- import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
10804
10935
 
10805
10936
  // src/codebase-index/writer.ts
10806
10937
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
@@ -13735,12 +13866,12 @@ function projectIndexServerBuildId(entrypoint) {
13735
13866
  const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
13736
13867
  const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path17.resolve(cleanHref);
13737
13868
  try {
13738
- const stat18 = fs12.statSync(file);
13739
- if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat18.mtimeMs && buildIdCache.size === stat18.size) {
13869
+ const stat19 = fs12.statSync(file);
13870
+ if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat19.mtimeMs && buildIdCache.size === stat19.size) {
13740
13871
  return buildIdCache.buildId;
13741
13872
  }
13742
13873
  const buildId = createHash4("sha256").update(fs12.readFileSync(file)).digest("hex").slice(0, 24);
13743
- buildIdCache = { file, mtimeMs: stat18.mtimeMs, size: stat18.size, buildId };
13874
+ buildIdCache = { file, mtimeMs: stat19.mtimeMs, size: stat19.size, buildId };
13744
13875
  return buildId;
13745
13876
  } catch {
13746
13877
  return `unreadable:${path17.basename(file)}`;
@@ -13882,8 +14013,8 @@ function isProjectIndexServerHealth(value) {
13882
14013
  return typeof health.checkedAt === "number" && typeof health.uptimeMs === "number" && typeof memory?.rss === "number" && typeof memory.heapUsed === "number" && typeof memory.heapTotal === "number" && typeof memory.external === "number" && typeof health.clients === "number" && typeof health.activeRequests === "number" && typeof health.activeWrites === "number" && typeof health.queuedWrites === "number" && typeof health.pendingExternalFiles === "number" && typeof health.watchingExternal === "boolean" && typeof activity?.indexing === "boolean" && typeof activity.currentFile === "number" && typeof activity.totalFiles === "number" && typeof activity.generation === "number";
13883
14014
  }
13884
14015
  function delay(ms) {
13885
- return new Promise((resolve16) => {
13886
- const timer = setTimeout(resolve16, ms);
14016
+ return new Promise((resolve17) => {
14017
+ const timer = setTimeout(resolve17, ms);
13887
14018
  timer.unref?.();
13888
14019
  });
13889
14020
  }
@@ -14087,7 +14218,7 @@ var ProjectServerConnection = class {
14087
14218
  return Promise.reject(new Error("codebase-index server connection is not available"));
14088
14219
  }
14089
14220
  const id = this.nextId++;
14090
- return new Promise((resolve16, reject) => {
14221
+ return new Promise((resolve17, reject) => {
14091
14222
  const timer = setTimeout(() => {
14092
14223
  const entry = this.pending.get(id);
14093
14224
  if (!entry) return;
@@ -14110,7 +14241,7 @@ var ProjectServerConnection = class {
14110
14241
  entry.reject(cancellationError(signal));
14111
14242
  } : void 0;
14112
14243
  this.pending.set(id, {
14113
- resolve: resolve16,
14244
+ resolve: resolve17,
14114
14245
  reject,
14115
14246
  timer,
14116
14247
  signal,
@@ -14179,7 +14310,7 @@ var ProjectServerConnection = class {
14179
14310
  this.binaryBuffer = [];
14180
14311
  this.useBinary = false;
14181
14312
  this.textDecoder = null;
14182
- return new Promise((resolve16, reject) => {
14313
+ return new Promise((resolve17, reject) => {
14183
14314
  const socket = net3.createConnection(this.endpoint);
14184
14315
  this.socket = socket;
14185
14316
  const timer = setTimeout(() => {
@@ -14191,7 +14322,7 @@ var ProjectServerConnection = class {
14191
14322
  clearTimeout(timer);
14192
14323
  this.connectResolve = null;
14193
14324
  this.connectReject = null;
14194
- resolve16();
14325
+ resolve17();
14195
14326
  };
14196
14327
  const finishReject = (error) => {
14197
14328
  clearTimeout(timer);
@@ -14214,7 +14345,7 @@ var ProjectServerConnection = class {
14214
14345
  this.onBinaryData(socket, chunk);
14215
14346
  return;
14216
14347
  }
14217
- if (!this.textDecoder) this.textDecoder = new StringDecoder("utf8");
14348
+ if (!this.textDecoder) this.textDecoder = new StringDecoder2("utf8");
14218
14349
  this.buffer += this.textDecoder.write(chunk);
14219
14350
  while (true) {
14220
14351
  const newline = this.buffer.indexOf("\n");
@@ -15205,9 +15336,9 @@ var ParserWorkerPool = class {
15205
15336
  for (let i = 0; i < files.length; i++) {
15206
15337
  chunks[i % workerCount].push(files[i]);
15207
15338
  }
15208
- return new Promise((resolve16, reject) => {
15339
+ return new Promise((resolve17, reject) => {
15209
15340
  this.pending.set(batchId, {
15210
- resolve: resolve16,
15341
+ resolve: resolve17,
15211
15342
  reject,
15212
15343
  accumulated: [],
15213
15344
  expectedWorkers: workerCount,
@@ -15238,10 +15369,10 @@ var ParserWorkerPool = class {
15238
15369
  await Promise.allSettled(
15239
15370
  workers.map(
15240
15371
  (w) => Promise.race([
15241
- new Promise((resolve16) => {
15242
- w.once("exit", () => resolve16());
15372
+ new Promise((resolve17) => {
15373
+ w.once("exit", () => resolve17());
15243
15374
  }),
15244
- new Promise((resolve16) => setTimeout(() => resolve16(), 2e3))
15375
+ new Promise((resolve17) => setTimeout(() => resolve17(), 2e3))
15245
15376
  ]).then(() => {
15246
15377
  if (!w.threadId) return;
15247
15378
  return w.terminate().catch(() => {
@@ -15305,7 +15436,7 @@ function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
15305
15436
  return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
15306
15437
  }
15307
15438
  function yieldEventLoop() {
15308
- return new Promise((resolve16) => setImmediate(resolve16));
15439
+ return new Promise((resolve17) => setImmediate(resolve17));
15309
15440
  }
15310
15441
  function throwIfAborted(signal) {
15311
15442
  if (!signal?.aborted) return;
@@ -15333,7 +15464,7 @@ function normalizeComparablePath(value) {
15333
15464
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
15334
15465
  }
15335
15466
  function gitOutput(projectRoot, args) {
15336
- return new Promise((resolve16, reject) => {
15467
+ return new Promise((resolve17, reject) => {
15337
15468
  execFile(
15338
15469
  "git",
15339
15470
  ["-C", projectRoot, ...args],
@@ -15344,7 +15475,7 @@ function gitOutput(projectRoot, args) {
15344
15475
  },
15345
15476
  (error, stdout) => {
15346
15477
  if (error) reject(error);
15347
- else resolve16(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
15478
+ else resolve17(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
15348
15479
  }
15349
15480
  );
15350
15481
  });
@@ -15576,9 +15707,9 @@ async function runIndexerWithStore(store, opts) {
15576
15707
  const statReadParse = await Promise.allSettled(
15577
15708
  batchFiles.map(
15578
15709
  async (file) => {
15579
- let stat18;
15710
+ let stat19;
15580
15711
  try {
15581
- stat18 = await fs18.stat(file, statOpts);
15712
+ stat19 = await fs18.stat(file, statOpts);
15582
15713
  } catch (e) {
15583
15714
  if (isAbortError(e)) throw e;
15584
15715
  return {
@@ -15590,21 +15721,21 @@ async function runIndexerWithStore(store, opts) {
15590
15721
  missing: isMissingPathError(e)
15591
15722
  };
15592
15723
  }
15593
- if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
15724
+ if (!stat19.isFile()) return { file, stat: stat19, lang: "", parsed: null };
15594
15725
  const lang = detectLang(file);
15595
- if (!lang) return { file, stat: stat18, lang: "", parsed: null };
15596
- if (stat18.size > MAX_INDEX_FILE_BYTES) {
15726
+ if (!lang) return { file, stat: stat19, lang: "", parsed: null };
15727
+ if (stat19.size > MAX_INDEX_FILE_BYTES) {
15597
15728
  return {
15598
15729
  file,
15599
- stat: stat18,
15730
+ stat: stat19,
15600
15731
  lang,
15601
15732
  parsed: null,
15602
- error: `file too large (${stat18.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
15733
+ error: `file too large (${stat19.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
15603
15734
  };
15604
15735
  }
15605
15736
  const meta = existingMeta.get(file);
15606
- if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
15607
- return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
15737
+ if (!force && meta && meta.mtimeMs === Math.floor(stat19.mtimeMs)) {
15738
+ return { file, stat: stat19, lang, parsed: null, skippedMeta: meta };
15608
15739
  }
15609
15740
  let content;
15610
15741
  try {
@@ -15613,7 +15744,7 @@ async function runIndexerWithStore(store, opts) {
15613
15744
  if (isAbortError(e)) throw e;
15614
15745
  return {
15615
15746
  file,
15616
- stat: stat18,
15747
+ stat: stat19,
15617
15748
  lang,
15618
15749
  parsed: null,
15619
15750
  error: `read error: ${e instanceof Error ? e.message : String(e)}`
@@ -15623,15 +15754,15 @@ async function runIndexerWithStore(store, opts) {
15623
15754
  if (!force && meta && meta.contentHash && contentHash === meta.contentHash) {
15624
15755
  return {
15625
15756
  file,
15626
- stat: stat18,
15757
+ stat: stat19,
15627
15758
  lang,
15628
15759
  parsed: null,
15629
15760
  content,
15630
15761
  contentHash,
15631
- skippedMeta: { ...meta, mtimeMs: Math.floor(stat18.mtimeMs) }
15762
+ skippedMeta: { ...meta, mtimeMs: Math.floor(stat19.mtimeMs) }
15632
15763
  };
15633
15764
  }
15634
- return { file, stat: stat18, lang, parsed: null, content, contentHash };
15765
+ return { file, stat: stat19, lang, parsed: null, content, contentHash };
15635
15766
  }
15636
15767
  )
15637
15768
  );
@@ -15710,7 +15841,7 @@ async function runIndexerWithStore(store, opts) {
15710
15841
  filesFailed++;
15711
15842
  continue;
15712
15843
  }
15713
- const { stat: stat18, lang, parsed } = result;
15844
+ const { stat: stat19, lang, parsed } = result;
15714
15845
  if (result.skippedMeta) {
15715
15846
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
15716
15847
  symbolsIndexed += result.skippedMeta.symbolCount;
@@ -15734,7 +15865,7 @@ async function runIndexerWithStore(store, opts) {
15734
15865
  store.upsertFile({
15735
15866
  file,
15736
15867
  lang,
15737
- mtimeMs: Math.floor(stat18.mtimeMs),
15868
+ mtimeMs: Math.floor(stat19.mtimeMs),
15738
15869
  symbolCount: 0,
15739
15870
  lastIndexed: Date.now(),
15740
15871
  contentHash: result.contentHash ?? ""
@@ -15748,7 +15879,7 @@ async function runIndexerWithStore(store, opts) {
15748
15879
  store.replaceEmptyFile({
15749
15880
  file,
15750
15881
  lang,
15751
- mtimeMs: Math.floor(stat18.mtimeMs),
15882
+ mtimeMs: Math.floor(stat19.mtimeMs),
15752
15883
  symbolCount: 0,
15753
15884
  lastIndexed: Date.now(),
15754
15885
  contentHash: result.contentHash ?? ""
@@ -15762,7 +15893,7 @@ async function runIndexerWithStore(store, opts) {
15762
15893
  lang,
15763
15894
  symbols: parsed.symbols,
15764
15895
  refs: parsed.refs ?? [],
15765
- mtimeMs: Math.floor(stat18.mtimeMs),
15896
+ mtimeMs: Math.floor(stat19.mtimeMs),
15766
15897
  symbolCount: parsed.symbols.length,
15767
15898
  contentHash: result.contentHash ?? ""
15768
15899
  });
@@ -16087,7 +16218,7 @@ function callIndexOp(op, args, opts) {
16087
16218
  opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
16088
16219
  );
16089
16220
  }
16090
- return new Promise((resolve16, reject) => {
16221
+ return new Promise((resolve17, reject) => {
16091
16222
  const id = nextRpcId++;
16092
16223
  const timer = setTimeout(() => {
16093
16224
  pending.delete(id);
@@ -16109,7 +16240,7 @@ function callIndexOp(op, args, opts) {
16109
16240
  pending.set(id, {
16110
16241
  resolve: (v) => {
16111
16242
  cleanup();
16112
- resolve16(v);
16243
+ resolve17(v);
16113
16244
  },
16114
16245
  reject: (e) => {
16115
16246
  cleanup();
@@ -16260,6 +16391,160 @@ async function outgoingCallsService2(args) {
16260
16391
  }
16261
16392
 
16262
16393
  // src/codebase-index/codebase-index-tool.ts
16394
+ import { ToolValidationError } from "@wrongstack/core/types";
16395
+
16396
+ // src/codebase-index/codebase-search-tool.ts
16397
+ import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
16398
+ var INDEXABLE_LANG_IDS = [
16399
+ "ts",
16400
+ "tsx",
16401
+ "js",
16402
+ "jsx",
16403
+ "go",
16404
+ "py",
16405
+ "rs",
16406
+ "json",
16407
+ "yaml"
16408
+ ];
16409
+ var codebaseSearchTool = {
16410
+ name: "codebase-search",
16411
+ category: "Project",
16412
+ icon: "index",
16413
+ description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Prefer this before broad `tree`, `glob`, or `grep` exploration when finding code by name or concept. Use `grep` instead for exact text, regexes, unsupported content, or concrete usage sites. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
16414
+ usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.",
16415
+ permission: "auto",
16416
+ mutating: false,
16417
+ capabilities: ["fs.read"],
16418
+ // The index host has its own 30s read watchdog. Leave enough headroom for
16419
+ // worker teardown and structured timeout reporting.
16420
+ timeoutMs: 35e3,
16421
+ inputSchema: {
16422
+ type: "object",
16423
+ properties: {
16424
+ query: {
16425
+ type: "string",
16426
+ description: "Search query \u2014 searches symbol names, signatures, and doc comments"
16427
+ },
16428
+ kind: {
16429
+ type: "string",
16430
+ enum: [
16431
+ "class",
16432
+ "interface",
16433
+ "enum",
16434
+ "type",
16435
+ "function",
16436
+ "method",
16437
+ "var",
16438
+ "const",
16439
+ "let",
16440
+ "property",
16441
+ "parameter",
16442
+ "namespace",
16443
+ "object",
16444
+ "literal",
16445
+ "schema",
16446
+ "struct",
16447
+ "trait",
16448
+ "impl",
16449
+ "static",
16450
+ "mod"
16451
+ ],
16452
+ description: "Filter by indexed symbol kind"
16453
+ },
16454
+ lang: {
16455
+ type: "string",
16456
+ enum: [...INDEXABLE_LANG_IDS],
16457
+ description: "Filter by indexed language"
16458
+ },
16459
+ lspKind: {
16460
+ type: "integer",
16461
+ description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
16462
+ },
16463
+ file: {
16464
+ type: "string",
16465
+ description: "Filter to files matching this path substring"
16466
+ },
16467
+ limit: {
16468
+ type: "integer",
16469
+ description: "Maximum results to return (default 20, max 100)",
16470
+ minimum: 1,
16471
+ maximum: 100
16472
+ },
16473
+ preferLsp: {
16474
+ type: "boolean",
16475
+ description: "Prefer live LSP results over the index. Ignored unless the LSP plugin is active; when it is active and this is true, results come from live workspaceSymbol queries."
16476
+ }
16477
+ },
16478
+ required: ["query"]
16479
+ },
16480
+ async execute(input, ctx, execOpts) {
16481
+ const state = getIndexState();
16482
+ if (state.indexing && !state.ready) {
16483
+ return {
16484
+ results: [],
16485
+ total: 0,
16486
+ query: input.query,
16487
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
16488
+ };
16489
+ }
16490
+ if (state.lastError) {
16491
+ const circuit = state.circuit;
16492
+ const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s); the user can run /codebase-reindex to retry now.` : "Try /codebase-reindex.";
16493
+ return {
16494
+ results: [],
16495
+ total: 0,
16496
+ query: input.query,
16497
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
16498
+ };
16499
+ }
16500
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
16501
+ let searched;
16502
+ try {
16503
+ searched = await searchCodebaseIndex(
16504
+ {
16505
+ projectRoot: ctx.projectRoot,
16506
+ indexDir: codebaseIndexDirOverride(ctx),
16507
+ query: input.query,
16508
+ kind: input.kind?.toLowerCase(),
16509
+ lang: input.lang?.toLowerCase(),
16510
+ file: input.file,
16511
+ lspKind: input.lspKind,
16512
+ limit
16513
+ },
16514
+ { signal: execOpts?.signal }
16515
+ );
16516
+ } catch (err) {
16517
+ if (execOpts?.signal?.aborted) throw err;
16518
+ return {
16519
+ results: [],
16520
+ total: 0,
16521
+ query: input.query,
16522
+ indexStatus: `Index query failed: ${toErrorMessage3(err)}. Fall back to grep/glob for this lookup.`
16523
+ };
16524
+ }
16525
+ const { results, total } = searched;
16526
+ let hasPersistedIndex = state.ready || total > 0;
16527
+ if (!hasPersistedIndex) {
16528
+ try {
16529
+ const stats = await codebaseIndexStats(
16530
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
16531
+ { signal: execOpts?.signal }
16532
+ );
16533
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
16534
+ } catch {
16535
+ }
16536
+ }
16537
+ return {
16538
+ results,
16539
+ total,
16540
+ query: input.query,
16541
+ ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
16542
+ };
16543
+ }
16544
+ };
16545
+
16546
+ // src/codebase-index/codebase-index-tool.ts
16547
+ var MAX_REPORTED_ERRORS = 20;
16263
16548
  var codebaseIndexTool = {
16264
16549
  name: "codebase-index",
16265
16550
  category: "Project",
@@ -16285,12 +16570,23 @@ var codebaseIndexTool = {
16285
16570
  },
16286
16571
  langs: {
16287
16572
  type: "array",
16288
- items: { type: "string" },
16289
- description: "Limit reindex to specific languages: ts, tsx, js, jsx, go, py, rs"
16573
+ items: { type: "string", enum: [...INDEXABLE_LANG_IDS] },
16574
+ description: `Limit reindex to specific languages: ${INDEXABLE_LANG_IDS.join(", ")}`
16290
16575
  }
16291
16576
  }
16292
16577
  },
16293
16578
  async execute(input, ctx, execOpts) {
16579
+ if (input.langs) {
16580
+ const unknown = input.langs.filter(
16581
+ (lang) => !INDEXABLE_LANG_IDS.includes(lang)
16582
+ );
16583
+ if (unknown.length > 0) {
16584
+ throw new ToolValidationError({
16585
+ message: `codebase-index: unknown lang(s) ${unknown.map((l) => `"${l}"`).join(", ")}. Valid ids: ${INDEXABLE_LANG_IDS.join(", ")}.`,
16586
+ field: "langs"
16587
+ });
16588
+ }
16589
+ }
16294
16590
  if (isIndexing()) {
16295
16591
  return {
16296
16592
  filesIndexed: 0,
@@ -16312,23 +16608,32 @@ var codebaseIndexTool = {
16312
16608
  note: `Codebase indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}). Auto-retry possible in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s; the user can run /codebase-reindex to retry immediately.`
16313
16609
  };
16314
16610
  }
16315
- return await runStartupIndex({
16611
+ const result = await runStartupIndex({
16316
16612
  projectRoot: ctx.projectRoot,
16317
16613
  force: input.force ?? false,
16318
16614
  langs: input.langs,
16319
16615
  indexDir: codebaseIndexDirOverride(ctx),
16320
16616
  signal: execOpts?.signal
16321
16617
  });
16618
+ if (result.errors.length > MAX_REPORTED_ERRORS) {
16619
+ const hidden = result.errors.length - MAX_REPORTED_ERRORS;
16620
+ return {
16621
+ ...result,
16622
+ errors: [...result.errors.slice(0, MAX_REPORTED_ERRORS), `+${hidden} more`]
16623
+ };
16624
+ }
16625
+ return result;
16322
16626
  }
16323
16627
  };
16324
16628
 
16325
16629
  // src/codebase-index/codebase-incoming-calls-tool.ts
16630
+ import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
16326
16631
  var codebaseIncomingCallsTool = {
16327
16632
  name: "codebase-incoming-calls",
16328
16633
  category: "Project",
16329
16634
  icon: "index",
16330
- description: "Find all callers of a function, method, or symbol \u2014 who invokes or references it. Uses the codebase index ref graph for instant, exact results. Always use this instead of grep when checking impact of a change.",
16331
- usageHint: 'CALL THIS BEFORE REFACTORING OR CHANGING ANY FUNCTION:\n\n- NEVER use grep or manual line reading to check where a function is called.\n- ALWAYS call codebase-incoming-calls({ symbol: "funcName" }) first.\n- Returns exact files, line numbers, caller signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Combine with codebase-outgoing-calls to see what the symbol itself calls.\nIf the index is not built, run codebase-index first.',
16635
+ description: "Find all callers of a function, method, or symbol \u2014 who invokes or references it. Uses the codebase index ref graph for instant, exact results. Prefer this over grep for change-impact checks when the index is available.",
16636
+ usageHint: 'CALL THIS BEFORE REFACTORING OR CHANGING ANY FUNCTION:\n\n- Prefer this over grep when the index is available; fall back to grep when the index is cold/unavailable or for dynamic dispatch the ref graph cannot see.\n- Call codebase-incoming-calls({ symbol: "funcName" }) before editing the symbol.\n- Returns exact files, line numbers, caller signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Combine with codebase-outgoing-calls to see what the symbol itself calls.\nIf the index is not built, run codebase-index first.',
16332
16637
  permission: "auto",
16333
16638
  mutating: false,
16334
16639
  capabilities: ["fs.read"],
@@ -16380,16 +16685,27 @@ var codebaseIncomingCallsTool = {
16380
16685
  }
16381
16686
  const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
16382
16687
  const transitive = input.transitive === true;
16383
- const { calls, symbolFound, ambiguous, totalMatches } = await incomingCallsService2(
16384
- {
16385
- projectRoot: ctx.projectRoot,
16386
- indexDir: codebaseIndexDirOverride(ctx),
16688
+ let serviced;
16689
+ try {
16690
+ serviced = await incomingCallsService2(
16691
+ {
16692
+ projectRoot: ctx.projectRoot,
16693
+ indexDir: codebaseIndexDirOverride(ctx),
16694
+ symbol: input.symbol,
16695
+ file: input.file,
16696
+ limit,
16697
+ transitive
16698
+ }
16699
+ );
16700
+ } catch (err) {
16701
+ return {
16387
16702
  symbol: input.symbol,
16388
- file: input.file,
16389
- limit,
16390
- transitive
16391
- }
16392
- );
16703
+ calls: [],
16704
+ total: 0,
16705
+ indexStatus: `Index query failed: ${toErrorMessage4(err)}. Fall back to grep for this lookup.`
16706
+ };
16707
+ }
16708
+ const { calls, symbolFound, ambiguous, totalMatches } = serviced;
16393
16709
  if (!symbolFound) {
16394
16710
  let hasPersistedIndex = state.ready;
16395
16711
  if (!hasPersistedIndex) {
@@ -16434,12 +16750,13 @@ var codebaseIncomingCallsTool = {
16434
16750
  };
16435
16751
 
16436
16752
  // src/codebase-index/codebase-outgoing-calls-tool.ts
16753
+ import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
16437
16754
  var codebaseOutgoingCallsTool = {
16438
16755
  name: "codebase-outgoing-calls",
16439
16756
  category: "Project",
16440
16757
  icon: "index",
16441
16758
  description: "Find all functions/methods/symbols that a given symbol calls or depends on \u2014 its callees. Uses the codebase index ref graph for instant, exact results. Use this to understand a function's dependencies before modifying it.",
16442
- usageHint: 'USE THIS TO UNDERSTAND A FUNCTION\'S DEPENDENCIES:\n\n- Call codebase-outgoing-calls({ symbol: "funcName" }) to see everything it calls.\n- Returns exact files, line numbers, callee signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Pair with codebase-incoming-calls for a complete impact picture: incoming = who calls you, outgoing = what you call.\nIf the index is not built, run codebase-index first.',
16759
+ usageHint: 'USE THIS TO UNDERSTAND A FUNCTION\'S DEPENDENCIES:\n\n- Prefer this over grep when the index is available; fall back to grep when the index is cold/unavailable or for dynamic dispatch the ref graph cannot see.\n- Call codebase-outgoing-calls({ symbol: "funcName" }) to see everything it calls.\n- Returns exact files, line numbers, callee signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Pair with codebase-incoming-calls for a complete impact picture: incoming = who calls you, outgoing = what you call.\nIf the index is not built, run codebase-index first.',
16443
16760
  permission: "auto",
16444
16761
  mutating: false,
16445
16762
  capabilities: ["fs.read"],
@@ -16491,16 +16808,27 @@ var codebaseOutgoingCallsTool = {
16491
16808
  }
16492
16809
  const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
16493
16810
  const transitive = input.transitive === true;
16494
- const { calls, symbolFound, unresolvedCount, totalMatches } = await outgoingCallsService2(
16495
- {
16496
- projectRoot: ctx.projectRoot,
16497
- indexDir: codebaseIndexDirOverride(ctx),
16811
+ let serviced;
16812
+ try {
16813
+ serviced = await outgoingCallsService2(
16814
+ {
16815
+ projectRoot: ctx.projectRoot,
16816
+ indexDir: codebaseIndexDirOverride(ctx),
16817
+ symbol: input.symbol,
16818
+ file: input.file,
16819
+ limit,
16820
+ transitive
16821
+ }
16822
+ );
16823
+ } catch (err) {
16824
+ return {
16498
16825
  symbol: input.symbol,
16499
- file: input.file,
16500
- limit,
16501
- transitive
16502
- }
16503
- );
16826
+ calls: [],
16827
+ total: 0,
16828
+ indexStatus: `Index query failed: ${toErrorMessage5(err)}. Fall back to grep for this lookup.`
16829
+ };
16830
+ }
16831
+ const { calls, symbolFound, unresolvedCount, totalMatches } = serviced;
16504
16832
  if (!symbolFound) {
16505
16833
  let hasPersistedIndex = state.ready;
16506
16834
  if (!hasPersistedIndex) {
@@ -16544,135 +16872,9 @@ var codebaseOutgoingCallsTool = {
16544
16872
  }
16545
16873
  };
16546
16874
 
16547
- // src/codebase-index/codebase-search-tool.ts
16548
- var codebaseSearchTool = {
16549
- name: "codebase-search",
16550
- category: "Project",
16551
- icon: "index",
16552
- description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Prefer this before broad `tree`, `glob`, or `grep` exploration when finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
16553
- usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.\nUse `grep` afterwards for exact text, regexes, unsupported content, or concrete usage sites.",
16554
- permission: "auto",
16555
- mutating: false,
16556
- capabilities: ["fs.read"],
16557
- // The index host has its own 30s read watchdog. Leave enough headroom for
16558
- // worker teardown and structured timeout reporting.
16559
- timeoutMs: 35e3,
16560
- inputSchema: {
16561
- type: "object",
16562
- properties: {
16563
- query: {
16564
- type: "string",
16565
- description: "Search query \u2014 searches symbol names, signatures, and doc comments"
16566
- },
16567
- kind: {
16568
- type: "string",
16569
- enum: [
16570
- "class",
16571
- "interface",
16572
- "enum",
16573
- "type",
16574
- "function",
16575
- "method",
16576
- "var",
16577
- "const",
16578
- "let",
16579
- "property",
16580
- "parameter",
16581
- "namespace",
16582
- "object",
16583
- "literal",
16584
- "schema",
16585
- "struct",
16586
- "trait",
16587
- "impl",
16588
- "static",
16589
- "mod"
16590
- ],
16591
- description: "Filter by indexed symbol kind"
16592
- },
16593
- lang: {
16594
- type: "string",
16595
- enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
16596
- description: "Filter by indexed language"
16597
- },
16598
- lspKind: {
16599
- type: "integer",
16600
- description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
16601
- },
16602
- file: {
16603
- type: "string",
16604
- description: "Filter to files matching this path substring"
16605
- },
16606
- limit: {
16607
- type: "integer",
16608
- description: "Maximum results to return (default 20, max 100)",
16609
- minimum: 1,
16610
- maximum: 100
16611
- },
16612
- preferLsp: {
16613
- type: "boolean",
16614
- description: "Prefer live LSP results over the index. Index-only when the LSP plugin is not active. When the LSP plugin is active and this is true, results come from live workspaceSymbol queries."
16615
- }
16616
- },
16617
- required: ["query"]
16618
- },
16619
- async execute(input, ctx, execOpts) {
16620
- const state = getIndexState();
16621
- if (state.indexing && !state.ready) {
16622
- return {
16623
- results: [],
16624
- total: 0,
16625
- query: input.query,
16626
- indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
16627
- };
16628
- }
16629
- if (state.lastError) {
16630
- const circuit = state.circuit;
16631
- const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s); the user can run /codebase-reindex to retry now.` : "Try /codebase-reindex.";
16632
- return {
16633
- results: [],
16634
- total: 0,
16635
- query: input.query,
16636
- indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
16637
- };
16638
- }
16639
- const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
16640
- const { results, total } = await searchCodebaseIndex(
16641
- {
16642
- projectRoot: ctx.projectRoot,
16643
- indexDir: codebaseIndexDirOverride(ctx),
16644
- query: input.query,
16645
- kind: input.kind?.toLowerCase(),
16646
- lang: input.lang?.toLowerCase(),
16647
- file: input.file,
16648
- lspKind: input.lspKind,
16649
- limit
16650
- },
16651
- { signal: execOpts?.signal }
16652
- );
16653
- let hasPersistedIndex = state.ready || total > 0;
16654
- if (!hasPersistedIndex) {
16655
- try {
16656
- const stats = await codebaseIndexStats(
16657
- { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
16658
- { signal: execOpts?.signal }
16659
- );
16660
- hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
16661
- } catch {
16662
- }
16663
- }
16664
- return {
16665
- results,
16666
- total,
16667
- query: input.query,
16668
- ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
16669
- };
16670
- }
16671
- };
16672
-
16673
- // src/codebase-index/codebase-stats-tool.ts
16674
- var codebaseStatsTool = {
16675
- name: "codebase-stats",
16875
+ // src/codebase-index/codebase-stats-tool.ts
16876
+ var codebaseStatsTool = {
16877
+ name: "codebase-stats",
16676
16878
  category: "Project",
16677
16879
  icon: "index",
16678
16880
  description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
@@ -16763,9 +16965,9 @@ import * as path25 from "node:path";
16763
16965
  var deadCodeScanTool = {
16764
16966
  name: "dead-code-scan",
16765
16967
  category: "Project",
16766
- icon: "search",
16968
+ icon: "index",
16767
16969
  description: "Scan TypeScript/JavaScript source files for exported symbols that appear unused anywhere in the project. Uses the codebase-index reference graph (import/call/type-ref edges) to compute transitive reachability from package.json entry points. Requires a built codebase-index (run `codebase-index` first if you get no results).",
16768
- usageHint: 'PASS `path` TO SCOPE THE SCAN:\n\n- Defaults to the project root (all indexed files).\n- `path` can be a directory to scope the scan (e.g. "src/util/").\n- `entryPoints` overrides auto-detected entry points (comma-separated file paths).\n\nThe scan runs against the existing index; results are best-effort.',
16970
+ usageHint: "SCANS ALL INDEXED FILES UNDER THE PROJECT ROOT:\n\n- `projectRoot` defaults to the current project root; `indexDir` overrides the resolved index location.\n- `entryPoints` is an array of file paths that AUGMENTS the auto-discovered entry points (package.json bin/main/exports/types plus conventional src/index.ts-style files) \u2014 it does not replace them.\n\nThe scan runs against the existing index; results are best-effort (dynamic imports, external consumers, and config-driven registration are invisible).",
16769
16971
  permission: "auto",
16770
16972
  mutating: false,
16771
16973
  capabilities: ["fs.read"],
@@ -17528,12 +17730,14 @@ import { statSync as statSync3 } from "node:fs";
17528
17730
  import * as fs22 from "node:fs/promises";
17529
17731
  import * as path27 from "node:path";
17530
17732
  import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
17733
+ import { ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
17531
17734
  var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
17735
+ var MAX_GIT_DIFF_CHARS = 1e5;
17532
17736
  var diffTool = {
17533
17737
  name: "diff",
17534
17738
  category: "Filesystem",
17535
17739
  description: "Show file content with line numbers, staged/working-tree diffs via git, or commit/branch diffs. A safer and more structured alternative to raw `git diff` via shell.",
17536
- usageHint: 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\n\n- `files` + no `a`/`b` \u2192 show file content with line numbers (NOT a unified diff; no +/- prefixes).\n- `a` and/or `b` \u2192 git-style commit/branch diff (unified format, real +/- prefixes).\n- `staged: true` \u2192 only show staged changes.\n- `mode` can be "unified", "stat", or "side-by-side" (only affects the git-diff path).\n\nNOTE: For a true file-vs-file unified diff, supply `a` and `b` so the tool delegates to `git diff`. The `files`-only path is a line-numbered dump, not a diff.\n\nThis tool has important safety guards against flag injection (see previous security findings).',
17740
+ usageHint: 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\n\n- `files` + no `a`/`b` \u2192 show file content with line numbers (NOT a unified diff; no +/- prefixes). Result `mode` is "dump".\n- `a` and/or `b` \u2192 git-style commit/branch diff (unified format, real +/- prefixes).\n- `staged: true` \u2192 only show staged changes.\n- `mode` only affects the git-diff path: "stat" runs `git diff --stat`; "side-by-side" is not supported and falls back to unified (result `mode` reports what was produced).\n- `context` sets the unified-diff context line count on the git path (`-U<n>`); the dump path has no context notion.\n\nNOTE: For a true file-vs-file unified diff, supply `a` and `b` so the tool delegates to `git diff`. The `files`-only path is a line-numbered dump, not a diff.\n\nThis tool has important safety guards against flag injection (see previous security findings).',
17537
17741
  permission: "auto",
17538
17742
  mutating: false,
17539
17743
  maxOutputBytes: 262144,
@@ -17566,11 +17770,12 @@ var diffTool = {
17566
17770
  mode: {
17567
17771
  type: "string",
17568
17772
  enum: ["unified", "side-by-side", "stat"],
17569
- description: 'Output format. "unified" is default, "stat" shows summary only.'
17773
+ description: 'Output format for the git-diff path. "unified" is default; "stat" shows a summary only; "side-by-side" is not supported and falls back to unified. The `files`-only dump path ignores this.'
17570
17774
  },
17571
17775
  context: {
17572
17776
  type: "integer",
17573
- description: "Number of context lines for unified diffs (default: 3)."
17777
+ minimum: 0,
17778
+ description: "Number of context lines for git unified diffs (default: 3, passed as -U<n>). Ignored by the `files`-only dump path."
17574
17779
  }
17575
17780
  }
17576
17781
  },
@@ -17583,16 +17788,31 @@ var diffTool = {
17583
17788
  };
17584
17789
  async function gitDiff(input, ctx, signal) {
17585
17790
  if (input.a?.startsWith("-")) {
17586
- throw new Error(`diff: unsafe ref "${input.a}" \u2014 refs may not begin with '-' (flag injection)`);
17791
+ throw new ToolValidationError2({
17792
+ message: `diff: unsafe ref "${input.a}" \u2014 refs may not begin with '-' (flag injection)`,
17793
+ field: "a"
17794
+ });
17587
17795
  }
17588
17796
  if (input.b?.startsWith("-")) {
17589
- throw new Error(`diff: unsafe ref "${input.b}" \u2014 refs may not begin with '-' (flag injection)`);
17797
+ throw new ToolValidationError2({
17798
+ message: `diff: unsafe ref "${input.b}" \u2014 refs may not begin with '-' (flag injection)`,
17799
+ field: "b"
17800
+ });
17590
17801
  }
17802
+ const requestedMode = input.mode ?? "unified";
17803
+ const statMode = requestedMode === "stat";
17804
+ const effectiveMode = statMode ? "stat" : "unified";
17805
+ const sideBySideNote = requestedMode === "side-by-side" ? "side-by-side output is not supported; a unified diff was produced instead." : void 0;
17591
17806
  const gitDir = findGitDir(ctx.cwd);
17592
17807
  if (!gitDir) {
17593
- return { diff: "", files: [], truncated: false, mode: "unified" };
17808
+ return { diff: "", files: [], truncated: false, mode: effectiveMode };
17594
17809
  }
17595
17810
  const args = ["diff", "--no-color"];
17811
+ if (statMode) args.push("--stat");
17812
+ if (!statMode && input.context !== void 0) {
17813
+ const contextLines = Math.max(0, Math.floor(input.context));
17814
+ if (Number.isFinite(contextLines)) args.push(`-U${contextLines}`);
17815
+ }
17596
17816
  if (input.staged) args.push("--staged");
17597
17817
  if (input.a) args.push(input.a);
17598
17818
  if (input.b) args.push(input.b);
@@ -17601,19 +17821,30 @@ async function gitDiff(input, ctx, signal) {
17601
17821
  args.push("--", ...files.map((f) => f.trim()));
17602
17822
  }
17603
17823
  const result = await runGit(args, gitDir, signal);
17824
+ let diff = result.stdout;
17825
+ let truncated = false;
17826
+ if (diff.length > MAX_GIT_DIFF_CHARS) {
17827
+ let clipped = diff.slice(0, MAX_GIT_DIFF_CHARS);
17828
+ const nl = clipped.lastIndexOf("\n");
17829
+ if (nl > 0) clipped = clipped.slice(0, nl);
17830
+ diff = `${clipped}
17831
+ \u2026[git diff truncated: ${result.stdout.length - clipped.length} of ${result.stdout.length} characters omitted]`;
17832
+ truncated = true;
17833
+ }
17604
17834
  return {
17605
- diff: result.stdout,
17835
+ diff,
17606
17836
  files: [],
17607
- truncated: result.stdout.length > 1e5,
17608
- mode: "unified"
17837
+ truncated,
17838
+ mode: effectiveMode,
17839
+ note: sideBySideNote
17609
17840
  };
17610
17841
  }
17611
17842
  function findGitDir(cwd) {
17612
17843
  let dir = cwd;
17613
17844
  for (let i = 0; i < 20; i++) {
17614
17845
  try {
17615
- const stat18 = statSync3(path27.join(dir, ".git"));
17616
- if (stat18.isDirectory()) return dir;
17846
+ const stat19 = statSync3(path27.join(dir, ".git"));
17847
+ if (stat19.isDirectory()) return dir;
17617
17848
  } catch {
17618
17849
  }
17619
17850
  const parent = path27.dirname(dir);
@@ -17623,7 +17854,7 @@ function findGitDir(cwd) {
17623
17854
  return null;
17624
17855
  }
17625
17856
  function runGit(args, cwd, signal) {
17626
- return new Promise((resolve16) => {
17857
+ return new Promise((resolve17) => {
17627
17858
  let stdout = "";
17628
17859
  let stderr = "";
17629
17860
  const child = spawn7("git", args, {
@@ -17639,8 +17870,8 @@ function runGit(args, cwd, signal) {
17639
17870
  child.stderr?.on("data", (c) => {
17640
17871
  stderr += c.toString();
17641
17872
  });
17642
- child.on("close", (code) => resolve16({ stdout, stderr, exitCode: code ?? 0 }));
17643
- child.on("error", (e) => resolve16({ stdout: "", stderr: e.message, exitCode: 1 }));
17873
+ child.on("close", (code) => resolve17({ stdout, stderr, exitCode: code ?? 0 }));
17874
+ child.on("error", (e) => resolve17({ stdout: "", stderr: e.message, exitCode: 1 }));
17644
17875
  });
17645
17876
  }
17646
17877
  async function fileDiff(input, ctx, _signal) {
@@ -17651,19 +17882,19 @@ async function fileDiff(input, ctx, _signal) {
17651
17882
  diff: "No files specified",
17652
17883
  files: [],
17653
17884
  truncated: false,
17654
- mode: input.mode ?? "unified"
17885
+ mode: "dump"
17655
17886
  };
17656
17887
  }
17657
17888
  const results = [];
17658
17889
  let truncated = false;
17659
17890
  for (const file of files) {
17660
- const absPath = safeResolve(file, ctx);
17661
- const stat18 = await fs22.stat(absPath).catch(() => null);
17662
- if (!stat18?.isFile()) continue;
17663
- if (stat18.size > MAX_FILE_DUMP_BYTES) {
17891
+ const absPath = await safeResolveReal(file, ctx);
17892
+ const stat19 = await fs22.stat(absPath).catch(() => null);
17893
+ if (!stat19?.isFile()) continue;
17894
+ if (stat19.size > MAX_FILE_DUMP_BYTES) {
17664
17895
  truncated = true;
17665
17896
  results.push(
17666
- `--- ${file} (skipped: ${stat18.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
17897
+ `--- ${file} (skipped: ${stat19.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
17667
17898
  );
17668
17899
  continue;
17669
17900
  }
@@ -17675,7 +17906,10 @@ async function fileDiff(input, ctx, _signal) {
17675
17906
  diff: results.join("\n\n"),
17676
17907
  files,
17677
17908
  truncated,
17678
- mode: input.mode ?? "unified"
17909
+ // Honest mode: this path always produces a line-numbered dump — it never
17910
+ // honors `mode`, so it must not echo the requested value back.
17911
+ mode: "dump",
17912
+ note: input.mode !== void 0 ? "The `files`-only path is a line-numbered dump; `mode` only affects the git-diff path (`a`/`b`)." : void 0
17679
17913
  };
17680
17914
  }
17681
17915
  function formatWithLineNumbers(file, lines) {
@@ -17688,11 +17922,12 @@ ${numbered}`;
17688
17922
  // src/document.ts
17689
17923
  init_util();
17690
17924
  import * as fs23 from "node:fs/promises";
17925
+ import * as path28 from "node:path";
17691
17926
  var documentTool = {
17692
17927
  name: "document",
17693
17928
  category: "Project",
17694
- description: "DEPRECATED \u2014 use the `auto_doc` tool with `dryRun: true` instead. This tool is a read-only preview stub that returns `skipped` candidates without generating real docstrings.",
17695
- usageHint: "Deprecated: prefer `auto_doc` with `dryRun: true` for previewing, or `auto_doc` without dryRun for writing. This tool only lists undocumented symbols with placeholder comments \u2014 it does not generate real JSDoc/TSDoc.",
17929
+ description: "DEPRECATED \u2014 read-only preview stub that lists undocumented symbols as `skipped` candidates. It never writes files and does not generate real docstrings. If the auto-doc plugin is enabled, use its `auto_doc` tool (with `dry_run: true` to preview) instead.",
17930
+ usageHint: "Deprecated: this tool only lists undocumented symbols with placeholder comments \u2014 it does not generate real JSDoc/TSDoc and writes nothing. When the auto-doc plugin is enabled, prefer its `auto_doc` tool (`dry_run: true` for previewing, without it for writing).",
17696
17931
  permission: "auto",
17697
17932
  mutating: false,
17698
17933
  timeoutMs: 3e4,
@@ -17728,7 +17963,11 @@ var documentTool = {
17728
17963
  const results = [];
17729
17964
  let filesProcessed = 0;
17730
17965
  let itemsDocumented = 0;
17731
- const fileList = input.files ? await resolveFiles(Array.isArray(input.files) ? input.files.join(",") : input.files, cwd) : input.path ? [safeResolve(input.path, ctx)] : [];
17966
+ const fileList = input.files ? await resolveFiles(
17967
+ Array.isArray(input.files) ? input.files.join(",") : input.files,
17968
+ cwd,
17969
+ ctx
17970
+ ) : input.path ? [safeResolve(input.path, ctx)] : [];
17732
17971
  for (const absPath of fileList) {
17733
17972
  try {
17734
17973
  const content = await fs23.readFile(absPath, "utf8");
@@ -17761,14 +18000,21 @@ var documentTool = {
17761
18000
  };
17762
18001
  }
17763
18002
  };
17764
- async function resolveFiles(filesInput, cwd) {
17765
- const files = Array.isArray(filesInput) ? filesInput : filesInput.split(",");
18003
+ async function resolveFiles(filesInput, cwd, ctx) {
18004
+ const files = filesInput.split(",");
17766
18005
  const resolved = [];
17767
18006
  for (const f of files) {
17768
- const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
18007
+ const entry = f.trim();
18008
+ if (!entry) continue;
18009
+ let absPath;
17769
18010
  try {
17770
- const stat18 = await fs23.stat(absPath);
17771
- if (stat18.isFile()) resolved.push(absPath);
18011
+ absPath = ensureInsideRoot(path28.resolve(cwd, entry), ctx);
18012
+ } catch {
18013
+ continue;
18014
+ }
18015
+ try {
18016
+ const stat19 = await fs23.stat(absPath);
18017
+ if (stat19.isFile()) resolved.push(absPath);
17772
18018
  } catch {
17773
18019
  }
17774
18020
  }
@@ -17838,7 +18084,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
17838
18084
  // src/e2e.ts
17839
18085
  init_util();
17840
18086
  import { open, readdir as readdir7 } from "node:fs/promises";
17841
- import * as path28 from "node:path";
18087
+ import * as path29 from "node:path";
17842
18088
  async function readBoundedText(filePath, maxBytes) {
17843
18089
  let handle;
17844
18090
  try {
@@ -17888,8 +18134,8 @@ var MAX_PACKAGE_BYTES = 512 * 1024;
17888
18134
  var MAX_CONFIG_BYTES = 512 * 1024;
17889
18135
  var MAX_SPEC_SAMPLES = 20;
17890
18136
  function relativePath(root, target) {
17891
- const value = path28.relative(root, target) || ".";
17892
- return value.split(path28.sep).join("/");
18137
+ const value = path29.relative(root, target) || ".";
18138
+ return value.split(path29.sep).join("/");
17893
18139
  }
17894
18140
  function escapeRegExp(value) {
17895
18141
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -17961,7 +18207,7 @@ async function scanWorkspace(root, maxDepth, signal) {
17961
18207
  }
17962
18208
  for (const entry of entries) {
17963
18209
  signal.throwIfAborted();
17964
- const absolutePath = path28.join(current.directory, entry.name);
18210
+ const absolutePath = path29.join(current.directory, entry.name);
17965
18211
  if (entry.isFile()) {
17966
18212
  if (entry.name === "package.json") result.packageFiles.push(absolutePath);
17967
18213
  const framework = CONFIG_NAMES.get(entry.name);
@@ -18022,9 +18268,9 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
18022
18268
  if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
18023
18269
  if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
18024
18270
  if (directory === scanRoot) break;
18025
- const parent = path28.dirname(directory);
18026
- const relativeParent = path28.relative(scanRoot, parent);
18027
- if (parent === directory || relativeParent.startsWith("..") || path28.isAbsolute(relativeParent)) {
18271
+ const parent = path29.dirname(directory);
18272
+ const relativeParent = path29.relative(scanRoot, parent);
18273
+ if (parent === directory || relativeParent.startsWith("..") || path29.isAbsolute(relativeParent)) {
18028
18274
  break;
18029
18275
  }
18030
18276
  directory = parent;
@@ -18069,7 +18315,7 @@ function isSpec(framework, filename) {
18069
18315
  return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
18070
18316
  }
18071
18317
  async function collectSpecs(root, framework, testDirectory, signal) {
18072
- const roots = testDirectory ? [path28.resolve(root, testDirectory)] : framework === "cypress" ? [path28.join(root, "cypress", "e2e"), path28.join(root, "cypress", "integration")] : [path28.join(root, "tests")];
18318
+ const roots = testDirectory ? [path29.resolve(root, testDirectory)] : framework === "cypress" ? [path29.join(root, "cypress", "e2e"), path29.join(root, "cypress", "integration")] : [path29.join(root, "tests")];
18073
18319
  const samples = [];
18074
18320
  let count = 0;
18075
18321
  let scanned = 0;
@@ -18088,7 +18334,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
18088
18334
  }
18089
18335
  for (const entry of entries) {
18090
18336
  signal.throwIfAborted();
18091
- const target = path28.join(directory, entry.name);
18337
+ const target = path29.join(directory, entry.name);
18092
18338
  if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
18093
18339
  else if (entry.isFile() && isSpec(framework, entry.name)) {
18094
18340
  count += 1;
@@ -18108,7 +18354,7 @@ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
18108
18354
  const found = packagesByDirectory.get(directory);
18109
18355
  if (found) return found;
18110
18356
  if (directory === scanRoot) return void 0;
18111
- const parent = path28.dirname(directory);
18357
+ const parent = path29.dirname(directory);
18112
18358
  if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
18113
18359
  directory = parent;
18114
18360
  }
@@ -18119,13 +18365,13 @@ async function discoverE2EProjects(root, options) {
18119
18365
  const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
18120
18366
  (info) => Boolean(info)
18121
18367
  );
18122
- const packagesByDirectory = new Map(packages.map((info) => [path28.dirname(info.path), info]));
18368
+ const packagesByDirectory = new Map(packages.map((info) => [path29.dirname(info.path), info]));
18123
18369
  const candidates = /* @__PURE__ */ new Map();
18124
18370
  for (const config of scan.configs) {
18125
18371
  if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
18126
18372
  continue;
18127
18373
  }
18128
- const projectRoot = path28.dirname(config.absolutePath);
18374
+ const projectRoot = path29.dirname(config.absolutePath);
18129
18375
  candidates.set(`${config.framework}:${projectRoot}`, {
18130
18376
  framework: config.framework,
18131
18377
  root: projectRoot,
@@ -18133,7 +18379,7 @@ async function discoverE2EProjects(root, options) {
18133
18379
  });
18134
18380
  }
18135
18381
  for (const info of packages) {
18136
- const projectRoot = path28.dirname(info.path);
18382
+ const projectRoot = path29.dirname(info.path);
18137
18383
  for (const framework of frameworkFromPackage(info)) {
18138
18384
  if (options.framework && options.framework !== "all" && framework !== options.framework)
18139
18385
  continue;
@@ -18149,10 +18395,10 @@ async function discoverE2EProjects(root, options) {
18149
18395
  const scripts = matchingScripts(info, candidate.framework);
18150
18396
  const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
18151
18397
  const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
18152
- const resolvedTestDirectory = testDirectory ? path28.resolve(candidate.root, testDirectory) : void 0;
18153
- const relativeTestDirectory = resolvedTestDirectory ? path28.relative(root, resolvedTestDirectory) : void 0;
18398
+ const resolvedTestDirectory = testDirectory ? path29.resolve(candidate.root, testDirectory) : void 0;
18399
+ const relativeTestDirectory = resolvedTestDirectory ? path29.relative(root, resolvedTestDirectory) : void 0;
18154
18400
  const unsafeTestDirectory = Boolean(
18155
- relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path28.isAbsolute(relativeTestDirectory))
18401
+ relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path29.isAbsolute(relativeTestDirectory))
18156
18402
  );
18157
18403
  const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
18158
18404
  const warnings = [];
@@ -18260,7 +18506,7 @@ import {
18260
18506
  toStyle,
18261
18507
  unifiedDiff
18262
18508
  } from "@wrongstack/core/utils";
18263
- import { ToolValidationError } from "@wrongstack/core/types";
18509
+ import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
18264
18510
 
18265
18511
  // src/_edit-match.ts
18266
18512
  var TIER_LABEL = {
@@ -18488,7 +18734,7 @@ function prefixSimilarity(a, b) {
18488
18734
  }
18489
18735
 
18490
18736
  // src/_syntax-check.ts
18491
- import * as path29 from "node:path";
18737
+ import * as path30 from "node:path";
18492
18738
  var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
18493
18739
  var MAX_CHECK_CHARS = 15e5;
18494
18740
  var MAX_ERRORS = 5;
@@ -18512,15 +18758,15 @@ async function checkSyntax(filePath, content, previousContent) {
18512
18758
  return { errors, preExisting };
18513
18759
  }
18514
18760
  function isJsoncFile(filePath) {
18515
- const base = path29.basename(filePath).toLowerCase();
18761
+ const base = path30.basename(filePath).toLowerCase();
18516
18762
  if (base.endsWith(".jsonc")) return true;
18517
18763
  if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
18518
- const dir = path29.basename(path29.dirname(filePath)).toLowerCase();
18764
+ const dir = path30.basename(path30.dirname(filePath)).toLowerCase();
18519
18765
  return dir === ".vscode";
18520
18766
  }
18521
18767
  async function parseErrors(filePath, content) {
18522
18768
  if (content.length > MAX_CHECK_CHARS) return void 0;
18523
- const ext = path29.extname(filePath).toLowerCase();
18769
+ const ext = path30.extname(filePath).toLowerCase();
18524
18770
  if (ext === ".json" || ext === ".jsonc") {
18525
18771
  try {
18526
18772
  JSON.parse(content);
@@ -18547,7 +18793,7 @@ async function parseErrors(filePath, content) {
18547
18793
  ts2.ScriptKind.JSX
18548
18794
  );
18549
18795
  const sourceFile = ts2.createSourceFile(
18550
- path29.basename(filePath),
18796
+ path30.basename(filePath),
18551
18797
  content,
18552
18798
  ts2.ScriptTarget.Latest,
18553
18799
  /* setParentNodes */
@@ -18574,6 +18820,7 @@ function formatDiag(ts2, diag, content, sourceFile) {
18574
18820
 
18575
18821
  // src/edit.ts
18576
18822
  init_util();
18823
+ var MAX_DIFF_BYTES = 262144;
18577
18824
  var editTool = {
18578
18825
  name: "edit",
18579
18826
  category: "Filesystem",
@@ -18584,46 +18831,62 @@ var editTool = {
18584
18831
  useInstead: ["write", "patch"]
18585
18832
  },
18586
18833
  permission: "confirm",
18834
+ // WS-046: gives permission decisions something to key on — the file being
18835
+ // edited, so trust rules can scope by path.
18836
+ subjectKey: "path",
18587
18837
  mutating: true,
18588
18838
  capabilities: ["fs.write"],
18589
18839
  icon: "edit",
18590
18840
  timeoutMs: 5e3,
18841
+ maxOutputBytes: 262144,
18591
18842
  inputSchema: {
18592
18843
  type: "object",
18593
18844
  properties: {
18594
- path: { type: "string" },
18595
- old_string: { type: "string" },
18596
- new_string: { type: "string" },
18597
- replace_all: { type: "boolean" }
18845
+ path: {
18846
+ type: "string",
18847
+ description: "Path to the file to edit \u2014 relative to the project root, or absolute inside it."
18848
+ },
18849
+ old_string: {
18850
+ type: "string",
18851
+ description: "The exact text to replace, including whitespace and indentation. Must be unique in the file unless `replace_all` is set \u2014 add surrounding lines to disambiguate."
18852
+ },
18853
+ new_string: {
18854
+ type: "string",
18855
+ description: "The exact replacement text (may be empty to delete `old_string`)."
18856
+ },
18857
+ replace_all: {
18858
+ type: "boolean",
18859
+ description: "Replace every occurrence instead of requiring a unique match. Only allowed when `old_string` matches exactly (or up to trailing whitespace) \u2014 fuzzy matches stay single-target."
18860
+ }
18598
18861
  },
18599
18862
  required: ["path", "old_string", "new_string"]
18600
18863
  },
18601
18864
  async execute(input, ctx, opts) {
18602
18865
  if (!input?.path) {
18603
- throw new ToolValidationError({ message: "edit: path is required", field: "path" });
18866
+ throw new ToolValidationError3({ message: "edit: path is required", field: "path" });
18604
18867
  }
18605
18868
  if (input.old_string === void 0) {
18606
- throw new ToolValidationError({
18869
+ throw new ToolValidationError3({
18607
18870
  message: "edit: old_string is required",
18608
18871
  field: "old_string"
18609
18872
  });
18610
18873
  }
18611
18874
  if (input.new_string === void 0) {
18612
- throw new ToolValidationError({
18875
+ throw new ToolValidationError3({
18613
18876
  message: "edit: new_string is required",
18614
18877
  field: "new_string"
18615
18878
  });
18616
18879
  }
18617
18880
  if (input.old_string === "") {
18618
- throw new ToolValidationError({
18881
+ throw new ToolValidationError3({
18619
18882
  message: "edit: old_string cannot be empty",
18620
18883
  field: "old_string"
18621
18884
  });
18622
18885
  }
18623
18886
  const absPath = await safeResolveReal(input.path, ctx);
18624
- const stat18 = await fs24.stat(absPath).catch((err) => {
18887
+ const stat19 = await fs24.stat(absPath).catch((err) => {
18625
18888
  if (err.code === "ENOENT") {
18626
- throw new ToolValidationError({
18889
+ throw new ToolValidationError3({
18627
18890
  message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
18628
18891
  field: "path",
18629
18892
  context: { exists: false }
@@ -18631,8 +18894,8 @@ var editTool = {
18631
18894
  }
18632
18895
  throw err;
18633
18896
  });
18634
- if (!stat18.isFile()) {
18635
- throw new ToolValidationError({
18897
+ if (!stat19.isFile()) {
18898
+ throw new ToolValidationError3({
18636
18899
  message: `edit: "${input.path}" is not a regular file`,
18637
18900
  field: "path"
18638
18901
  });
@@ -18645,7 +18908,7 @@ var editTool = {
18645
18908
  const lastReadHash = ctx.lastReadHash?.(absPath);
18646
18909
  if (lastReadHash !== void 0) {
18647
18910
  if (lastReadHash !== originalHash) {
18648
- throw new ToolValidationError({
18911
+ throw new ToolValidationError3({
18649
18912
  message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
18650
18913
  field: "path",
18651
18914
  context: { reason: "external_modification" }
@@ -18654,15 +18917,15 @@ var editTool = {
18654
18917
  } else {
18655
18918
  const lastReadMtime = ctx.lastReadMtime(absPath);
18656
18919
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
18657
- throw new ToolValidationError({
18920
+ throw new ToolValidationError3({
18658
18921
  message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
18659
18922
  field: "path",
18660
18923
  context: { reason: "external_modification" }
18661
18924
  });
18662
18925
  }
18663
18926
  }
18664
- if (autoRead && updated.mtimeMs > stat18.mtimeMs + mtimeTolerance) {
18665
- throw new ToolValidationError({
18927
+ if (autoRead && updated.mtimeMs > stat19.mtimeMs + mtimeTolerance) {
18928
+ throw new ToolValidationError3({
18666
18929
  message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
18667
18930
  field: "path",
18668
18931
  context: { reason: "auto_read_race" }
@@ -18674,6 +18937,9 @@ var editTool = {
18674
18937
  const oldLf = normalizeToLf(input.old_string);
18675
18938
  const newLf = normalizeToLf(input.new_string);
18676
18939
  if (oldLf === newLf) {
18940
+ if (!fileLf.includes(oldLf)) {
18941
+ throw noMatchError(input.path, fileLf, oldLf);
18942
+ }
18677
18943
  if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
18678
18944
  return {
18679
18945
  path: absPath,
@@ -18687,26 +18953,20 @@ var editTool = {
18687
18953
  const ladder = findLadderMatches(fileLf, oldLf);
18688
18954
  if (!ladder) {
18689
18955
  opts?.signal?.throwIfAborted();
18690
- const hint = nearestMatchHint(fileLf, oldLf);
18691
- throw new ToolValidationError({
18692
- message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
18693
- ${hint.snippet}
18694
- Compare this against your old_string and retry with the file's actual text.` : ""}`,
18695
- field: "old_string"
18696
- });
18956
+ throw noMatchError(input.path, fileLf, oldLf);
18697
18957
  }
18698
18958
  const { tier, matches } = ladder;
18699
18959
  const count = matches.length;
18700
18960
  if (ladder.ambiguous) {
18701
18961
  const lines = matches.map((m) => m.startLine);
18702
- throw new ToolValidationError({
18962
+ throw new ToolValidationError3({
18703
18963
  message: `edit: old_string only matched fuzzily and ${count} candidate blocks scored too close to distinguish (lines: ${lines.join(", ")}) in "${input.path}". Re-read the file and use the exact text of the intended block.`,
18704
18964
  field: "old_string",
18705
18965
  context: { occurrences: count, matchTier: tier }
18706
18966
  });
18707
18967
  }
18708
18968
  if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
18709
- throw new ToolValidationError({
18969
+ throw new ToolValidationError3({
18710
18970
  message: `edit: old_string only matched via ${TIER_LABEL[tier]} in "${input.path}", but replace_all requires an exact (or trailing-whitespace) match. Re-read the file and use its exact text.`,
18711
18971
  field: "old_string",
18712
18972
  context: { matchTier: tier }
@@ -18714,7 +18974,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
18714
18974
  }
18715
18975
  if (count > 1 && !input.replace_all) {
18716
18976
  const lines = matches.map((m) => m.startLine);
18717
- throw new ToolValidationError({
18977
+ throw new ToolValidationError3({
18718
18978
  message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")})${tier === "exact" ? "" : ` via ${TIER_LABEL[tier]}`}. Add more context to make it unique, or set replace_all: true.`,
18719
18979
  field: "old_string",
18720
18980
  context: { occurrences: count, matchTier: tier }
@@ -18755,16 +19015,20 @@ Compare this against your old_string and retry with the file's actual text.` : "
18755
19015
  after: newFile
18756
19016
  });
18757
19017
  opts?.signal?.throwIfAborted();
18758
- const diff = unifiedDiff(original, newFile, {
18759
- fromFile: input.path,
18760
- toFile: input.path
18761
- });
19018
+ const { text: diff, truncated: diffTruncated } = truncateDiffPayload(
19019
+ unifiedDiff(original, newFile, {
19020
+ fromFile: input.path,
19021
+ toFile: input.path
19022
+ }),
19023
+ MAX_DIFF_BYTES
19024
+ );
19025
+ const diffNote = diffTruncated ? "Diff truncated to the 256 KiB output budget \u2014 the full edit is on disk." : void 0;
18762
19026
  const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
18763
19027
  let syntaxNote;
18764
19028
  if (syntax && syntax.errors.length > 0) {
18765
19029
  syntaxNote = syntax.preExisting ? `Syntax check: the file still has parse errors (they pre-date this edit) \u2014 see syntax_errors.` : `Syntax check: this edit introduced ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`;
18766
19030
  }
18767
- const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
19031
+ const notes = [autoReadNote, tierNote, diffNote, syntaxNote].filter(Boolean);
18768
19032
  return {
18769
19033
  path: absPath,
18770
19034
  replacements: input.replace_all ? count : 1,
@@ -18775,6 +19039,15 @@ Compare this against your old_string and retry with the file's actual text.` : "
18775
19039
  };
18776
19040
  }
18777
19041
  };
19042
+ function noMatchError(inputPath, fileLf, oldLf) {
19043
+ const hint = nearestMatchHint(fileLf, oldLf);
19044
+ return new ToolValidationError3({
19045
+ message: `edit: no match for old_string in "${inputPath}".${hint ? ` Nearest match near line ${hint.line}:
19046
+ ${hint.snippet}
19047
+ Compare this against your old_string and retry with the file's actual text.` : ""}`,
19048
+ field: "old_string"
19049
+ });
19050
+ }
18778
19051
 
18779
19052
  // src/exec.ts
18780
19053
  import { spawn as spawn8 } from "node:child_process";
@@ -18783,7 +19056,7 @@ import {
18783
19056
  emitProcessOutput as emitProcessOutput3,
18784
19057
  emitProcessStarted as emitProcessStarted3
18785
19058
  } from "@wrongstack/core/observability";
18786
- import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
19059
+ import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils/error";
18787
19060
 
18788
19061
  // src/_danger-detect.ts
18789
19062
  var argHas = (args, value) => args.includes(value);
@@ -19115,7 +19388,7 @@ init_win32_resolve();
19115
19388
 
19116
19389
  // src/exec-kill-guard.ts
19117
19390
  import * as os8 from "node:os";
19118
- import * as path30 from "node:path";
19391
+ import * as path31 from "node:path";
19119
19392
  var isWin3 = os8.platform() === "win32";
19120
19393
  async function checkExecKillCommand(cmd, args) {
19121
19394
  if (!cmd) return { blocked: false };
@@ -19328,7 +19601,7 @@ async function checkKillTarget(target) {
19328
19601
  reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
19329
19602
  };
19330
19603
  }
19331
- const currentImage = path30.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
19604
+ const currentImage = path31.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
19332
19605
  const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
19333
19606
  if (targetsNodeRuntime && currentImage === "node") {
19334
19607
  return {
@@ -19950,6 +20223,7 @@ function isExecCommandAllowed(cmd) {
19950
20223
  var MAX_ARGS = 20;
19951
20224
  var MAX_OUTPUT2 = 2e5;
19952
20225
  var DEFAULT_TIMEOUT_MS3 = 3e4;
20226
+ var MAX_TIMEOUT_MS = 6e5;
19953
20227
  var BLOCKED_ARG_PATTERNS = {
19954
20228
  python: [],
19955
20229
  // git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
@@ -20075,8 +20349,8 @@ var SAFE_DANGER = { level: "safe", reasons: [] };
20075
20349
  var execTool = {
20076
20350
  name: "exec",
20077
20351
  category: "Shell",
20078
- description: "Execute a **whitelisted, restricted set of commands** with strict argument validation. This is the **preferred and safer** alternative to the `bash` tool for running development tools (node, npm, pnpm, tsc, git, tests, linters, etc.). It prevents arbitrary command injection and limits what the model can do.",
20079
- usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be in the allowlist. Defaults cover JS (node/npm/pnpm/yarn/bun/deno/tsc/vitest/eslint/biome), Go (`go build`/`go test`), Rust (cargo), Python (python/pip), Ruby (gem/bundle), JVM (java/mvn/gradle), .NET (dotnet), native (make/cmake), and git. Users can extend it via `tools.exec.allow` in config.\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- If a command is not allowlisted, the error explains how to add it; for one-off arbitrary commands, fall back to `bash` (with strong justification).\nThis tool significantly reduces the risk compared to full shell access.",
20352
+ description: "Execute a command from a **curated command roster** with argument validation and confirm gating. This is the **preferred** alternative to the `bash` tool for running development tools (node, npm, pnpm, tsc, git, tests, linters, etc.). It is NOT a sandbox \u2014 several rostered commands (node, python, powershell, \u2026) can run arbitrary code \u2014 so prefer least-privilege commands.",
20353
+ usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be in the allowlist. Defaults cover JS (node/npm/pnpm/yarn/bun/deno/tsc/vitest/eslint/biome), Go (`go build`/`go test`), Rust (cargo), Python (python/pip), Ruby (gem/bundle), JVM (java/mvn/gradle), .NET (dotnet), native (make/cmake), and git. Users can extend it via `tools.exec.allow` in config.\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- If a command is not allowlisted, the error explains how to add it; for one-off arbitrary commands, fall back to `bash` (with strong justification).\nThe curated roster + confirm gating narrows the surface compared to full shell access, but this is not a sandbox \u2014 prefer least-privilege commands.",
20080
20354
  selection: {
20081
20355
  doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
20082
20356
  useInstead: ["bash"]
@@ -20092,7 +20366,13 @@ var execTool = {
20092
20366
  subjectKey: "command",
20093
20367
  mutating: true,
20094
20368
  riskTier: "standard",
20095
- timeoutMs: DEFAULT_TIMEOUT_MS3,
20369
+ // Executor-level abort ceiling. Must sit ABOVE the per-call timeout ceiling
20370
+ // (MAX_TIMEOUT_MS): the tool's own timer resolves with exit 124 + registry
20371
+ // tree-kill; the executor's AbortSignal.timeout is a blunt abort that would
20372
+ // otherwise fire first and discard the structured timeout result. The 10s
20373
+ // margin covers the kill/teardown window. (The executor additionally clamps
20374
+ // to config `tools.maxToolTimeoutMs`.)
20375
+ timeoutMs: MAX_TIMEOUT_MS + 1e4,
20096
20376
  capabilities: ["shell.restricted"],
20097
20377
  icon: "terminal",
20098
20378
  inputSchema: {
@@ -20113,7 +20393,7 @@ var execTool = {
20113
20393
  },
20114
20394
  timeout: {
20115
20395
  type: "integer",
20116
- description: "Per-command timeout in milliseconds."
20396
+ description: "Per-command timeout in milliseconds (default 30000, max 600000)."
20117
20397
  }
20118
20398
  },
20119
20399
  required: ["command"]
@@ -20157,7 +20437,7 @@ var execTool = {
20157
20437
  };
20158
20438
  }
20159
20439
  const args = (input.args ?? []).slice(0, MAX_ARGS);
20160
- const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS3, DEFAULT_TIMEOUT_MS3));
20440
+ const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS3, MAX_TIMEOUT_MS));
20161
20441
  const danger = detectDanger(cmd, args, dangerBypass);
20162
20442
  const killCheck = await checkExecKillCommand(cmd, args);
20163
20443
  if (killCheck.blocked) {
@@ -20185,15 +20465,16 @@ var execTool = {
20185
20465
  danger
20186
20466
  };
20187
20467
  }
20468
+ const defaultCwd = ctx.workingDir ?? ctx.cwd;
20188
20469
  let cwd;
20189
20470
  try {
20190
- cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(ctx.cwd, ctx);
20471
+ cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(defaultCwd, ctx);
20191
20472
  } catch {
20192
20473
  return {
20193
20474
  command: cmd,
20194
20475
  args,
20195
20476
  stdout: "",
20196
- stderr: `cwd "${input.cwd ?? ctx.cwd}" resolves outside project root`,
20477
+ stderr: `cwd "${input.cwd ?? defaultCwd}" resolves outside project root`,
20197
20478
  exitCode: 1,
20198
20479
  truncated: false,
20199
20480
  allowed: false,
@@ -20205,7 +20486,7 @@ var execTool = {
20205
20486
  }
20206
20487
  };
20207
20488
  function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
20208
- return new Promise((resolve16) => {
20489
+ return new Promise((resolve17) => {
20209
20490
  let stdout = "";
20210
20491
  let stderr = "";
20211
20492
  let killed = false;
@@ -20213,7 +20494,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
20213
20494
  const finish = (result) => {
20214
20495
  if (resolvedOnce.value) return;
20215
20496
  resolvedOnce.value = true;
20216
- resolve16(result);
20497
+ resolve17(result);
20217
20498
  };
20218
20499
  const startedAt = Date.now();
20219
20500
  let stdoutBytes = 0;
@@ -20265,7 +20546,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
20265
20546
  command: cmd,
20266
20547
  args,
20267
20548
  stdout: "",
20268
- stderr: `spawn failed: ${toErrorMessage3(err)}`,
20549
+ stderr: `spawn failed: ${toErrorMessage6(err)}`,
20269
20550
  exitCode: 1,
20270
20551
  truncated: false,
20271
20552
  allowed: true,
@@ -20368,7 +20649,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
20368
20649
  }
20369
20650
 
20370
20651
  // src/fetch.ts
20371
- import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
20652
+ import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
20372
20653
  import TurndownService from "turndown";
20373
20654
 
20374
20655
  // src/_fetch-guard.ts
@@ -20378,7 +20659,7 @@ import {
20378
20659
  isPrivateIPv4 as isPrivateIPv42,
20379
20660
  isPrivateIPv6 as isPrivateIPv62
20380
20661
  } from "@wrongstack/core/utils";
20381
- import { FetchError, ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
20662
+ import { FetchError, ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
20382
20663
  import { Agent, fetch as undiciFetch } from "undici";
20383
20664
  var nativeGlobalFetch = globalThis.fetch;
20384
20665
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
@@ -20449,13 +20730,13 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
20449
20730
  for (; ; ) {
20450
20731
  const parsed = new URL(currentUrl);
20451
20732
  if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
20452
- throw new ToolValidationError2({
20733
+ throw new ToolValidationError4({
20453
20734
  message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
20454
20735
  field: "url"
20455
20736
  });
20456
20737
  }
20457
20738
  if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
20458
- throw new ToolValidationError2({
20739
+ throw new ToolValidationError4({
20459
20740
  message: "fetch: redirect to http:// blocked (HTTPS required by default)",
20460
20741
  field: "url"
20461
20742
  });
@@ -20471,6 +20752,10 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
20471
20752
  if (res.status < 300 || res.status > 399) {
20472
20753
  return res;
20473
20754
  }
20755
+ try {
20756
+ await res.body?.cancel();
20757
+ } catch {
20758
+ }
20474
20759
  redirectCount++;
20475
20760
  if (redirectCount > maxRedirects) {
20476
20761
  throw new FetchError({
@@ -20494,7 +20779,7 @@ async function assertNotPrivate(hostname2) {
20494
20779
  if (ALLOW_PRIVATE) return;
20495
20780
  const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
20496
20781
  if (host === "localhost" || host.endsWith(".localhost")) {
20497
- throw new ToolValidationError2({
20782
+ throw new ToolValidationError4({
20498
20783
  message: "fetch: blocked localhost target",
20499
20784
  field: "url"
20500
20785
  });
@@ -20502,14 +20787,14 @@ async function assertNotPrivate(hostname2) {
20502
20787
  const ipVersion = net4.isIP(host);
20503
20788
  if (ipVersion === 4) {
20504
20789
  if (isPrivateIPv42(host)) {
20505
- throw new ToolValidationError2({
20790
+ throw new ToolValidationError4({
20506
20791
  message: `fetch: blocked private/loopback address "${host}"`,
20507
20792
  field: "url"
20508
20793
  });
20509
20794
  }
20510
20795
  } else if (ipVersion === 6) {
20511
20796
  if (isPrivateIPv62(host)) {
20512
- throw new ToolValidationError2({
20797
+ throw new ToolValidationError4({
20513
20798
  message: `fetch: blocked private/loopback address "${host}"`,
20514
20799
  field: "url"
20515
20800
  });
@@ -20520,14 +20805,14 @@ async function assertNotPrivate(hostname2) {
20520
20805
  for (const r of records) {
20521
20806
  const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
20522
20807
  if (bad) {
20523
- throw new ToolValidationError2({
20808
+ throw new ToolValidationError4({
20524
20809
  message: `fetch: resolved to private address ${r.address}`,
20525
20810
  field: "url"
20526
20811
  });
20527
20812
  }
20528
20813
  }
20529
20814
  } catch (err) {
20530
- if (err instanceof ToolValidationError2) throw err;
20815
+ if (err instanceof ToolValidationError4) throw err;
20531
20816
  }
20532
20817
  }
20533
20818
  }
@@ -20544,6 +20829,8 @@ TD.addRule("stripDangerousElements", {
20544
20829
  filter: ["script", "style", "noscript"],
20545
20830
  replacement: () => ""
20546
20831
  });
20832
+ var PRUNED_BOILERPLATE_TAGS = /* @__PURE__ */ new Set(["nav", "header", "footer", "aside", "svg", "iframe"]);
20833
+ TD.remove((node) => PRUNED_BOILERPLATE_TAGS.has(node.nodeName.toLowerCase()));
20547
20834
  var MAX_BYTES = 131072;
20548
20835
  var TIMEOUT_MS = 2e4;
20549
20836
  var combineSignals = (signals) => AbortSignal.any(signals);
@@ -20573,7 +20860,7 @@ var fetchTool = {
20573
20860
  format: {
20574
20861
  type: "string",
20575
20862
  enum: ["markdown", "text", "raw"],
20576
- description: 'Output format. "markdown" is recommended for HTML pages.'
20863
+ description: 'Output format. "markdown" is recommended for HTML pages; for non-HTML content types it falls back to plain text (JSON is pretty-printed).'
20577
20864
  }
20578
20865
  },
20579
20866
  required: ["url"]
@@ -20602,20 +20889,26 @@ var fetchTool = {
20602
20889
  },
20603
20890
  async *executeStream(input, ctx, opts) {
20604
20891
  if (!input?.url) {
20605
- throw new ToolValidationError3({
20892
+ throw new ToolValidationError5({
20606
20893
  message: "fetch: url is required",
20607
20894
  field: "url"
20608
20895
  });
20609
20896
  }
20610
20897
  const u = new URL(input.url);
20898
+ if (u.username || u.password) {
20899
+ throw new ToolValidationError5({
20900
+ message: "fetch: URLs with embedded credentials (user:pass@host) are not allowed",
20901
+ field: "url"
20902
+ });
20903
+ }
20611
20904
  if (u.protocol !== "https:" && u.protocol !== "http:") {
20612
- throw new ToolValidationError3({
20905
+ throw new ToolValidationError5({
20613
20906
  message: `fetch: unsupported protocol "${u.protocol}"`,
20614
20907
  field: "url"
20615
20908
  });
20616
20909
  }
20617
20910
  if (u.protocol === "http:" && !ALLOW_PRIVATE) {
20618
- throw new ToolValidationError3({
20911
+ throw new ToolValidationError5({
20619
20912
  message: "fetch: http:// blocked (HTTPS required by default)",
20620
20913
  field: "url"
20621
20914
  });
@@ -20805,8 +21098,9 @@ var formatTool = {
20805
21098
  type: "final",
20806
21099
  output: {
20807
21100
  fixer: bridge.language,
20808
- files_checked: 0,
20809
- files_changed: run.summary.errors > 0 ? 0 : 1,
21101
+ // Language-bridge runs don't report per-file counts.
21102
+ files_checked: void 0,
21103
+ files_changed: void 0,
20810
21104
  output: normalizeCommandOutput(run.output || run.error || ""),
20811
21105
  truncated: run.truncated
20812
21106
  }
@@ -20833,11 +21127,14 @@ var formatTool = {
20833
21127
  text: `Running ${detected}\u2026`,
20834
21128
  data: { fixer: detected, check: !!input.check }
20835
21129
  };
20836
- const args = ["format", "--write"];
20837
- if (input.check) args[args.length - 1] = "--check";
20838
- if (input.files) {
20839
- const files = Array.isArray(input.files) ? input.files : input.files.split(",");
20840
- args.push("--", ...files.map((f) => f.trim()));
21130
+ const fileList = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim()) : [];
21131
+ let args;
21132
+ if (detected === "prettier") {
21133
+ args = [input.check ? "--check" : "--write"];
21134
+ args.push(...fileList.length > 0 ? fileList : ["."]);
21135
+ } else {
21136
+ args = ["format", input.check ? "--check" : "--write"];
21137
+ if (fileList.length > 0) args.push("--", ...fileList);
20841
21138
  }
20842
21139
  const result = yield* spawnStream({
20843
21140
  cmd: detected,
@@ -20846,39 +21143,70 @@ var formatTool = {
20846
21143
  signal: opts.signal,
20847
21144
  maxBytes: 1e5
20848
21145
  });
20849
- const changed = [...result.stdout.matchAll(/\bchanged\b/gi)].length;
21146
+ const combinedOut = `${result.stdout}
21147
+ ${result.stderr}`;
21148
+ const counts = parseFormatterCounts(detected, combinedOut);
20850
21149
  yield {
20851
21150
  type: "final",
20852
21151
  output: {
20853
21152
  fixer: detected,
20854
- files_checked: 0,
20855
- files_changed: changed,
21153
+ files_checked: counts.checked,
21154
+ files_changed: counts.changed,
20856
21155
  output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
20857
21156
  truncated: result.truncated
20858
21157
  }
20859
21158
  };
20860
21159
  }
20861
21160
  };
21161
+ function parseFormatterCounts(fixer, output) {
21162
+ if (fixer !== "biome") return { checked: void 0, changed: void 0 };
21163
+ const checkedMatch = /\b(?:Checked|Formatted)\s+(\d+)\s+files?\b/i.exec(output);
21164
+ const changedMatch = /\bFixed\s+(\d+)\s+files?\b/i.exec(output);
21165
+ return {
21166
+ checked: checkedMatch?.[1] !== void 0 ? Number(checkedMatch[1]) : void 0,
21167
+ changed: changedMatch?.[1] !== void 0 ? Number(changedMatch[1]) : void 0
21168
+ };
21169
+ }
20862
21170
  async function detectFixer(cwd) {
20863
- const { stat: stat18 } = await import("node:fs/promises");
20864
- try {
20865
- await stat18(`${cwd}/biome.json`);
20866
- return "biome";
20867
- } catch {
21171
+ const fs35 = await import("node:fs/promises");
21172
+ const exists = async (file) => {
20868
21173
  try {
20869
- await stat18(`${cwd}/.prettierrc`);
20870
- return "prettier";
21174
+ await fs35.stat(`${cwd}/${file}`);
21175
+ return true;
20871
21176
  } catch {
20872
- return "biome";
21177
+ return false;
20873
21178
  }
21179
+ };
21180
+ if (await exists("biome.json") || await exists("biome.jsonc")) return "biome";
21181
+ const PRETTIER_CONFIGS = [
21182
+ ".prettierrc",
21183
+ ".prettierrc.json",
21184
+ ".prettierrc.yml",
21185
+ ".prettierrc.yaml",
21186
+ ".prettierrc.js",
21187
+ ".prettierrc.cjs",
21188
+ ".prettierrc.mjs",
21189
+ "prettier.config.js",
21190
+ "prettier.config.cjs",
21191
+ "prettier.config.mjs"
21192
+ ];
21193
+ for (const cfg of PRETTIER_CONFIGS) {
21194
+ if (await exists(cfg)) return "prettier";
21195
+ }
21196
+ try {
21197
+ const raw = await fs35.readFile(`${cwd}/package.json`, "utf8");
21198
+ const pkg = JSON.parse(raw);
21199
+ if (pkg["prettier"] !== void 0) return "prettier";
21200
+ } catch {
20874
21201
  }
21202
+ return "biome";
20875
21203
  }
20876
21204
 
20877
21205
  // src/git.ts
20878
21206
  init_util();
20879
21207
  import { spawn as spawn9 } from "node:child_process";
20880
21208
  import { statSync as statSync4 } from "node:fs";
20881
- import { dirname as dirname14, resolve as resolve13, sep as sep6 } from "node:path";
21209
+ import { dirname as dirname13, resolve as resolve14, sep as sep6 } from "node:path";
20882
21210
  import { assessCommitSafety } from "@wrongstack/core/coordination";
20883
21211
  import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
20884
21212
  var TIMEOUT_MS2 = 3e4;
@@ -21039,8 +21367,8 @@ function validateWorktreeInput(input, projectRoot) {
21039
21367
  return reject(`unsafe worktree path: ${input.worktreePath}`);
21040
21368
  }
21041
21369
  if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
21042
- const root = resolve13(projectRoot);
21043
- const abs = resolve13(root, input.worktreePath);
21370
+ const root = resolve14(projectRoot);
21371
+ const abs = resolve14(root, input.worktreePath);
21044
21372
  if (abs !== root && !abs.startsWith(root + sep6)) {
21045
21373
  return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
21046
21374
  }
@@ -21052,12 +21380,12 @@ function findGitDir2(cwd, projectRoot) {
21052
21380
  let dir = cwd;
21053
21381
  for (let i = 0; i < 20; i++) {
21054
21382
  try {
21055
- const stat18 = statSync4(`${dir}/.git`);
21056
- if (stat18.isDirectory() || stat18.isFile()) return dir;
21383
+ const stat19 = statSync4(`${dir}/.git`);
21384
+ if (stat19.isDirectory() || stat19.isFile()) return dir;
21057
21385
  } catch {
21058
21386
  }
21059
21387
  if (dir === root) break;
21060
- const parent = dirname14(dir);
21388
+ const parent = dirname13(dir);
21061
21389
  if (parent === dir) break;
21062
21390
  dir = parent;
21063
21391
  }
@@ -21134,7 +21462,7 @@ function buildArgs(input) {
21134
21462
  }
21135
21463
  }
21136
21464
  function runGit2(args, cwd, signal) {
21137
- return new Promise((resolve16) => {
21465
+ return new Promise((resolve17) => {
21138
21466
  let stdout = "";
21139
21467
  let stderr = "";
21140
21468
  const child = spawn9("git", args, {
@@ -21155,7 +21483,7 @@ function runGit2(args, cwd, signal) {
21155
21483
  }
21156
21484
  });
21157
21485
  child.on("error", (err) => {
21158
- resolve16({
21486
+ resolve17({
21159
21487
  command: args[0],
21160
21488
  stdout: normalizeCommandOutput(stdout),
21161
21489
  stderr: err.message,
@@ -21164,7 +21492,7 @@ function runGit2(args, cwd, signal) {
21164
21492
  });
21165
21493
  });
21166
21494
  child.on("close", (code) => {
21167
- resolve16({
21495
+ resolve17({
21168
21496
  command: args[0],
21169
21497
  stdout: normalizeCommandOutput(stdout),
21170
21498
  stderr: normalizeCommandOutput(stderr),
@@ -21177,8 +21505,9 @@ function runGit2(args, cwd, signal) {
21177
21505
 
21178
21506
  // src/glob.ts
21179
21507
  import * as fs25 from "node:fs/promises";
21180
- import * as path31 from "node:path";
21508
+ import * as path32 from "node:path";
21181
21509
  import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS2 } from "@wrongstack/core/utils";
21510
+ import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
21182
21511
 
21183
21512
  // src/_concurrency.ts
21184
21513
  async function mapWithConcurrency2(items, limit, fn) {
@@ -21204,7 +21533,7 @@ var WALK_CONCURRENCY = 16;
21204
21533
  var globTool = {
21205
21534
  name: "glob",
21206
21535
  category: "Filesystem",
21207
- description: "Find files by path pattern. Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
21536
+ description: "Find files by path pattern. Results are sorted by modification time, newest first, so when the list is truncated at `limit` it is the oldest files that are dropped (recency-biased). Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
21208
21537
  usageHint: "PATH DISCOVERY AND SEARCH SCOPING:\n\n- When `codebase-search` is live, use it first for code concepts; use `glob` for filenames, path patterns, and non-indexed files.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
21209
21538
  selection: {
21210
21539
  doNotUseWhen: "you need to search inside file contents.",
@@ -21215,7 +21544,7 @@ var globTool = {
21215
21544
  capabilities: ["fs.read"],
21216
21545
  icon: "folder",
21217
21546
  maxOutputBytes: 65536,
21218
- timeoutMs: 5e3,
21547
+ timeoutMs: 15e3,
21219
21548
  inputSchema: {
21220
21549
  type: "object",
21221
21550
  properties: {
@@ -21229,13 +21558,20 @@ var globTool = {
21229
21558
  },
21230
21559
  limit: {
21231
21560
  type: "integer",
21232
- description: "Maximum number of results to return (default 1000, max 5000)."
21561
+ minimum: 1,
21562
+ maximum: 5e3,
21563
+ description: "Maximum number of results to return (default 1000, max 5000). Results are sorted by mtime descending, so truncation keeps the most recently modified files."
21233
21564
  }
21234
21565
  },
21235
21566
  required: ["pattern"]
21236
21567
  },
21237
21568
  async execute(input, ctx, opts) {
21238
- if (!input?.pattern) throw new Error("glob: pattern is required");
21569
+ if (!input?.pattern) {
21570
+ throw new ToolValidationError6({
21571
+ message: "glob: pattern is required",
21572
+ field: "pattern"
21573
+ });
21574
+ }
21239
21575
  const signal = opts?.signal;
21240
21576
  const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
21241
21577
  const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
@@ -21280,7 +21616,7 @@ var globTool = {
21280
21616
  const name = e.name;
21281
21617
  if (DEFAULT_IGNORE2.has(name)) continue;
21282
21618
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
21283
- const full = path31.join(dir, name);
21619
+ const full = path32.join(dir, name);
21284
21620
  if (e.isDirectory()) {
21285
21621
  if (isGitIgnored(rel, true)) continue;
21286
21622
  subdirs.push({ full, rel });
@@ -21330,8 +21666,8 @@ var globTool = {
21330
21666
  // src/grep.ts
21331
21667
  import { spawn as spawn10 } from "node:child_process";
21332
21668
  import * as fs26 from "node:fs/promises";
21333
- import * as path32 from "node:path";
21334
- import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
21669
+ import * as path33 from "node:path";
21670
+ import { ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
21335
21671
  import {
21336
21672
  buildChildEnv as buildChildEnv5,
21337
21673
  compileGlob as compileGlob3,
@@ -21535,7 +21871,7 @@ var grepTool = {
21535
21871
  },
21536
21872
  async *executeStream(input, ctx, opts) {
21537
21873
  if (!input?.pattern) {
21538
- throw new ToolValidationError4({
21874
+ throw new ToolValidationError7({
21539
21875
  message: "grep: pattern is required",
21540
21876
  field: "pattern"
21541
21877
  });
@@ -21545,12 +21881,12 @@ var grepTool = {
21545
21881
  const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
21546
21882
  const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
21547
21883
  if (!validation.ok) {
21548
- throw new ToolValidationError4({
21884
+ throw new ToolValidationError7({
21549
21885
  message: `grep: ${validation.reason}`,
21550
21886
  field: "pattern"
21551
21887
  });
21552
21888
  }
21553
- const rgAvailable = await detectRg(opts.signal);
21889
+ const rgAvailable = await detectRg();
21554
21890
  if (rgAvailable) {
21555
21891
  try {
21556
21892
  yield* runRgStream(input, base, mode, limit, opts.signal);
@@ -21563,16 +21899,23 @@ var grepTool = {
21563
21899
  yield { type: "final", output: out };
21564
21900
  }
21565
21901
  };
21566
- async function detectRg(signal) {
21567
- return new Promise((resolve16) => {
21902
+ var rgAvailabilityCache;
21903
+ function detectRg() {
21904
+ rgAvailabilityCache ??= new Promise((resolve17) => {
21568
21905
  try {
21569
- const p = spawn10("rg", ["--version"], { env: buildChildEnv5(), stdio: "ignore", signal, windowsHide: true });
21570
- p.on("error", () => resolve16(false));
21571
- p.on("close", (code) => resolve16(code === 0));
21906
+ const p = spawn10("rg", ["--version"], {
21907
+ env: buildChildEnv5(),
21908
+ stdio: "ignore",
21909
+ signal: AbortSignal.timeout(1e4),
21910
+ windowsHide: true
21911
+ });
21912
+ p.on("error", () => resolve17(false));
21913
+ p.on("close", (code) => resolve17(code === 0));
21572
21914
  } catch {
21573
- resolve16(false);
21915
+ resolve17(false);
21574
21916
  }
21575
21917
  });
21918
+ return rgAvailabilityCache;
21576
21919
  }
21577
21920
  async function* runRgStream(input, base, mode, limit, signal) {
21578
21921
  const args = ["--no-heading"];
@@ -21586,7 +21929,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
21586
21929
  for (const ignored of DEFAULT_IGNORE3) {
21587
21930
  args.push("--glob", `!${ignored}/**`, "--glob", `!**/${ignored}/**`);
21588
21931
  }
21589
- const gitignorePath = path32.join(base, ".gitignore");
21932
+ const gitignorePath = path33.join(base, ".gitignore");
21590
21933
  if (await fs26.access(gitignorePath).then(() => true, () => false)) {
21591
21934
  args.push("--ignore-file", gitignorePath);
21592
21935
  }
@@ -21758,7 +22101,7 @@ async function runNative(input, base, mode, limit, signal) {
21758
22101
  const flags = input.case_insensitive ? "i" : "";
21759
22102
  const compiled = compileUserRegex(input.pattern, flags);
21760
22103
  if (!compiled.ok) {
21761
- throw new ToolValidationError4({
22104
+ throw new ToolValidationError7({
21762
22105
  message: `grep: ${compiled.reason}`,
21763
22106
  field: "pattern"
21764
22107
  });
@@ -21776,8 +22119,8 @@ async function runNative(input, base, mode, limit, signal) {
21776
22119
  if (globRe && !globRe.test(name) && !globRe.test(full)) return;
21777
22120
  if (globRe) globRe.lastIndex = 0;
21778
22121
  try {
21779
- const stat18 = await fs26.stat(full);
21780
- if (!stat18.isFile() || stat18.size > maxBytes || stopped || signal.aborted) return;
22122
+ const stat19 = await fs26.stat(full);
22123
+ if (!stat19.isFile() || stat19.size > maxBytes || stopped || signal.aborted) return;
21781
22124
  const file = await fs26.open(full, "r");
21782
22125
  try {
21783
22126
  let bytesReadTotal = 0;
@@ -21868,7 +22211,7 @@ async function runNative(input, base, mode, limit, signal) {
21868
22211
  if (DEFAULT_IGNORE3.has(e.name)) continue;
21869
22212
  if (e.isSymbolicLink()) continue;
21870
22213
  const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
21871
- const full = path32.join(dir, e.name);
22214
+ const full = path33.join(dir, e.name);
21872
22215
  if (e.isDirectory()) {
21873
22216
  if (isGitIgnored(rel, true)) continue;
21874
22217
  subdirs.push({ full, rel });
@@ -21988,26 +22331,14 @@ var installTool = {
21988
22331
  return;
21989
22332
  }
21990
22333
  }
21991
- const pkgManager = await detectPackageManager(cwd);
22334
+ const pkgManager = await detectPackageManager(cwd, ctx.projectRoot);
21992
22335
  yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
21993
- const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
21994
22336
  const globalFlag = input.global ? ["-g"] : [];
21995
22337
  const ignoreScripts = input.lifecycleScripts !== true;
21996
- const args = [];
21997
- if (input.dry_run) args.push("--dry-run");
21998
- if (ignoreScripts) args.push("--ignore-scripts");
21999
- if (pkgManager === "pnpm") {
22000
- if (save) args.push(save);
22001
- args.push("add", ...globalFlag);
22002
- } else if (pkgManager === "yarn") {
22003
- args.push("add", ...globalFlag);
22004
- } else {
22005
- args.push("install", ...globalFlag);
22006
- }
22007
22338
  const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
22008
22339
  (p) => p.trim()
22009
22340
  ) : [];
22010
- const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i;
22341
+ const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9^~><=*.+-]+)?$/i;
22011
22342
  for (const pkg of pkgList) {
22012
22343
  if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-") || pkg.length > 200) {
22013
22344
  yield {
@@ -22023,7 +22354,34 @@ var installTool = {
22023
22354
  return;
22024
22355
  }
22025
22356
  }
22026
- if (pkgList.length > 0) args.push(...pkgList);
22357
+ const hasPkgs = pkgList.length > 0;
22358
+ const args = [];
22359
+ if (input.dry_run) args.push("--dry-run");
22360
+ if (ignoreScripts) args.push("--ignore-scripts");
22361
+ if (pkgManager === "pnpm") {
22362
+ if (hasPkgs) {
22363
+ if (input.save === "dev") args.push("-D");
22364
+ else if (input.save === "optional") args.push("-O");
22365
+ args.push("add", ...globalFlag);
22366
+ } else {
22367
+ args.push("install", ...globalFlag);
22368
+ }
22369
+ } else if (pkgManager === "yarn") {
22370
+ if (hasPkgs) {
22371
+ args.push("add", ...globalFlag);
22372
+ if (input.save === "dev") args.push("--dev");
22373
+ else if (input.save === "optional") args.push("--optional");
22374
+ } else {
22375
+ args.push("install", ...globalFlag);
22376
+ }
22377
+ } else {
22378
+ args.push("install", ...globalFlag);
22379
+ if (hasPkgs) {
22380
+ if (input.save === "dev") args.push("--save-dev");
22381
+ else if (input.save === "optional") args.push("--save-optional");
22382
+ }
22383
+ }
22384
+ if (hasPkgs) args.push(...pkgList);
22027
22385
  yield {
22028
22386
  type: "log",
22029
22387
  text: `Fetching ${pkgList.length || "all"} packages\u2026`,
@@ -22104,9 +22462,9 @@ var JsonFileTooLargeError = class extends Error {
22104
22462
  };
22105
22463
  async function readJsonFileBounded(filePath, ctx) {
22106
22464
  const resolved = await safeResolveReal(filePath, ctx);
22107
- const stat18 = await fs27.stat(resolved);
22108
- if (stat18.size > MAX_JSON_FILE_BYTES) {
22109
- throw new JsonFileTooLargeError(filePath, stat18.size);
22465
+ const stat19 = await fs27.stat(resolved);
22466
+ if (stat19.size > MAX_JSON_FILE_BYTES) {
22467
+ throw new JsonFileTooLargeError(filePath, stat19.size);
22110
22468
  }
22111
22469
  return fs27.readFile(resolved, "utf8");
22112
22470
  }
@@ -22512,60 +22870,60 @@ function jmespathSearch(data, query) {
22512
22870
  }
22513
22871
  function validateJsonSchema(data, schema) {
22514
22872
  const errors = [];
22515
- function check(value, s, path39) {
22873
+ function check(value, s, path40) {
22516
22874
  if (s["type"]) {
22517
22875
  const expectedType = s["type"];
22518
22876
  const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
22519
22877
  if (expectedType === "integer") {
22520
- if (!Number.isInteger(value)) errors.push(`${path39}: expected integer, got ${actualType}`);
22878
+ if (!Number.isInteger(value)) errors.push(`${path40}: expected integer, got ${actualType}`);
22521
22879
  } else if (expectedType !== actualType) {
22522
- errors.push(`${path39}: expected ${expectedType}, got ${actualType}`);
22880
+ errors.push(`${path40}: expected ${expectedType}, got ${actualType}`);
22523
22881
  }
22524
22882
  }
22525
22883
  if (typeof value === "string" && s["format"] === "uri" && value) {
22526
22884
  try {
22527
22885
  new URL(value);
22528
22886
  } catch {
22529
- errors.push(`${path39}: not a valid URI`);
22887
+ errors.push(`${path40}: not a valid URI`);
22530
22888
  }
22531
22889
  }
22532
22890
  if (typeof value === "string" && s["pattern"]) {
22533
22891
  const compiled = compileUserRegex(s["pattern"], "");
22534
22892
  if (!compiled.ok) {
22535
- errors.push(`${path39}: invalid schema pattern \u2014 ${compiled.reason}`);
22893
+ errors.push(`${path40}: invalid schema pattern \u2014 ${compiled.reason}`);
22536
22894
  } else if (!compiled.regex.test(capSubject(value))) {
22537
- errors.push(`${path39}: does not match pattern ${s["pattern"]}`);
22895
+ errors.push(`${path40}: does not match pattern ${s["pattern"]}`);
22538
22896
  }
22539
22897
  }
22540
22898
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
22541
- errors.push(`${path39}: string too short (min ${s["minLength"]})`);
22899
+ errors.push(`${path40}: string too short (min ${s["minLength"]})`);
22542
22900
  }
22543
22901
  if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
22544
- errors.push(`${path39}: string too long (max ${s["maxLength"]})`);
22902
+ errors.push(`${path40}: string too long (max ${s["maxLength"]})`);
22545
22903
  }
22546
22904
  if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
22547
- errors.push(`${path39}: below minimum ${s["minimum"]}`);
22905
+ errors.push(`${path40}: below minimum ${s["minimum"]}`);
22548
22906
  }
22549
22907
  if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
22550
- errors.push(`${path39}: above maximum ${s["maximum"]}`);
22908
+ errors.push(`${path40}: above maximum ${s["maximum"]}`);
22551
22909
  }
22552
22910
  if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
22553
22911
  for (let i = 0; i < value.length; i++) {
22554
- check(value[i], s["items"], `${path39}[${i}]`);
22912
+ check(value[i], s["items"], `${path40}[${i}]`);
22555
22913
  }
22556
22914
  }
22557
22915
  if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
22558
22916
  const props = s["properties"];
22559
22917
  for (const [k, propSchema] of Object.entries(props)) {
22560
- check(value[k], propSchema, `${path39}.${k}`);
22918
+ check(value[k], propSchema, `${path40}.${k}`);
22561
22919
  }
22562
22920
  }
22563
22921
  }
22564
22922
  check(data, schema, "$");
22565
22923
  return { valid: errors.length === 0, errors };
22566
22924
  }
22567
- function simpleQuery(data, path39) {
22568
- const parts = path39.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
22925
+ function simpleQuery(data, path40) {
22926
+ const parts = path40.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
22569
22927
  let current = data;
22570
22928
  for (const part of parts) {
22571
22929
  if (current === null || current === void 0) return void 0;
@@ -23476,6 +23834,18 @@ var KANBAN_INPUT_SCHEMA = {
23476
23834
  },
23477
23835
  transitionAction: { type: "string" },
23478
23836
  transitionComment: { type: "string" },
23837
+ tickChecks: {
23838
+ type: "array",
23839
+ items: {
23840
+ type: "object",
23841
+ properties: {
23842
+ checkId: { type: "string" },
23843
+ checkStatus: { type: "string", enum: ["passed", "failed", "skipped"] }
23844
+ },
23845
+ required: ["checkId", "checkStatus"]
23846
+ },
23847
+ description: "`transition_task` (to=done only): flip one or more manual criteria to `passed` before the gate fires. Read ids from kanban get_task. Non-manual criteria are refused."
23848
+ },
23479
23849
  attachmentUrl: { type: "string" },
23480
23850
  attachmentTitle: { type: "string" },
23481
23851
  attachmentType: {
@@ -24176,6 +24546,9 @@ var kanbanTool = {
24176
24546
  description: KANBAN_TOOL_DESCRIPTION,
24177
24547
  usageHint: KANBAN_TOOL_USAGE_HINT,
24178
24548
  permission: "confirm",
24549
+ // WS-046: gives permission decisions something to key on.
24550
+ // The action performed; kanban has no single file or path subject.
24551
+ subjectKey: "action",
24179
24552
  mutating: true,
24180
24553
  capabilities: ["fs.write"],
24181
24554
  icon: "task",
@@ -24617,6 +24990,7 @@ var kanbanTool = {
24617
24990
  actor: input.author,
24618
24991
  comment: input.transitionComment,
24619
24992
  ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
24993
+ ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
24620
24994
  ...input.attachmentUrl !== void 0 ? {
24621
24995
  attachment: {
24622
24996
  url: input.attachmentUrl,
@@ -25009,8 +25383,52 @@ var kanbanTool = {
25009
25383
  } catch (err) {
25010
25384
  return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
25011
25385
  }
25386
+ },
25387
+ serialize(output, input) {
25388
+ return serializeKanbanOutput(output, input);
25012
25389
  }
25013
25390
  };
25391
+ var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
25392
+ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
25393
+ "get_board",
25394
+ "export_markdown",
25395
+ "export_task_graph"
25396
+ ]);
25397
+ function serializeKanbanOutput(output, input) {
25398
+ const action = input && typeof input === "object" ? input.action : void 0;
25399
+ const board = output.board;
25400
+ if (board) {
25401
+ const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
25402
+ let boardBytes = 0;
25403
+ if (!keepFull) {
25404
+ try {
25405
+ boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
25406
+ } catch {
25407
+ boardBytes = 0;
25408
+ }
25409
+ }
25410
+ if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
25411
+ const columns = {};
25412
+ for (const column of board.columns) {
25413
+ columns[column.title || column.id] = board.tasks.filter(
25414
+ (task) => task.columnId === column.id
25415
+ ).length;
25416
+ }
25417
+ const compact = {
25418
+ ...output,
25419
+ board: {
25420
+ id: board.id,
25421
+ title: board.title,
25422
+ columns,
25423
+ totalTasks: board.tasks.length,
25424
+ note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
25425
+ }
25426
+ };
25427
+ return JSON.stringify(compact, null, 2);
25428
+ }
25429
+ }
25430
+ return JSON.stringify(output, null, 2);
25431
+ }
25014
25432
 
25015
25433
  // src/builtin.ts
25016
25434
  init_execute_tool();
@@ -25125,11 +25543,11 @@ var lintTool = {
25125
25543
  }
25126
25544
  };
25127
25545
  async function detectLinter(cwd) {
25128
- const { stat: stat18 } = await import("node:fs/promises");
25546
+ const { stat: stat19 } = await import("node:fs/promises");
25129
25547
  const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
25130
25548
  for (const f of checks) {
25131
25549
  try {
25132
- await stat18(`${cwd}/${f}`);
25550
+ await stat19(`${cwd}/${f}`);
25133
25551
  if (f.includes("biome")) return "biome";
25134
25552
  if (f.includes("eslint")) return "eslint";
25135
25553
  if (f.includes("tslint")) return "tslint";
@@ -25146,11 +25564,12 @@ init_util();
25146
25564
  var logsTool = {
25147
25565
  name: "logs",
25148
25566
  category: "Logs",
25149
- description: "Read or stream logs from files, Docker containers, or systemd services. Useful for debugging running applications.",
25150
- usageHint: "DEBUGGING TOOL \u2014 USE CAREFULLY IN AUTONOMOUS MODE:\n\n- Prefer `path` for local files or `service` for containers/systemd.\n- `stream: true` = live tail (can be expensive).\n- Always use `filter` (regex) when possible to reduce noise and token usage.\n- Long-running streams should be avoided unless the user explicitly wants live logs.",
25567
+ description: "Read logs from files or Docker containers. Useful for debugging running applications.",
25568
+ usageHint: "DEBUGGING TOOL \u2014 USE CAREFULLY IN AUTONOMOUS MODE:\n\n- Prefer `path` for local files or `service` for Docker containers.\n- Always use `filter` (regex) when possible to reduce noise and token usage.\n- `since` narrows Docker logs to a recent window.",
25151
25569
  permission: "confirm",
25152
25570
  mutating: false,
25153
25571
  timeoutMs: 3e4,
25572
+ maxOutputBytes: 262144,
25154
25573
  capabilities: ["shell.restricted"],
25155
25574
  icon: "logs",
25156
25575
  inputSchema: {
@@ -25158,7 +25577,7 @@ var logsTool = {
25158
25577
  properties: {
25159
25578
  service: {
25160
25579
  type: "string",
25161
- description: "Service name for Docker or systemd journal"
25580
+ description: "Docker container name (passed to `docker logs`)"
25162
25581
  },
25163
25582
  path: {
25164
25583
  type: "string",
@@ -25170,10 +25589,6 @@ var logsTool = {
25170
25589
  minimum: 0,
25171
25590
  maximum: 1e4
25172
25591
  },
25173
- stream: {
25174
- type: "boolean",
25175
- description: "Stream logs continuously (like tail -f) (default: false)"
25176
- },
25177
25592
  filter: {
25178
25593
  type: "string",
25179
25594
  description: "Regex pattern to filter log lines"
@@ -25181,7 +25596,7 @@ var logsTool = {
25181
25596
  since: {
25182
25597
  type: "string",
25183
25598
  enum: ["1h", "6h", "24h", "all"],
25184
- description: "Only show logs since duration"
25599
+ description: 'Only show Docker logs since duration (ignored for files; "all" = no limit)'
25185
25600
  },
25186
25601
  cwd: { type: "string", description: "Working directory (default: cwd)" }
25187
25602
  }
@@ -25198,10 +25613,10 @@ var logsTool = {
25198
25613
  filterRe = compiled.regex;
25199
25614
  }
25200
25615
  if (input.service) {
25201
- return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);
25616
+ return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal, input.since);
25202
25617
  }
25203
25618
  if (input.path) {
25204
- return await fileLogs(safeResolve(input.path, ctx), lines, filterRe, input.stream ?? false);
25619
+ return await fileLogs(await safeResolveReal(input.path, ctx), lines, filterRe);
25205
25620
  }
25206
25621
  return {
25207
25622
  source: "none",
@@ -25215,7 +25630,7 @@ var logsTool = {
25215
25630
  async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
25216
25631
  const args = ["logs"];
25217
25632
  if (lines > 0) args.push("--tail", String(lines));
25218
- if (since) {
25633
+ if (since && since !== "all") {
25219
25634
  const sinceMap = { "1h": "1h", "6h": "6h", "24h": "24h" };
25220
25635
  args.push("--since", sinceMap[since] ?? "1h");
25221
25636
  }
@@ -25229,7 +25644,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
25229
25644
  };
25230
25645
  }
25231
25646
  args.push("--timestamps", service);
25232
- return new Promise((resolve16) => {
25647
+ return new Promise((resolve17) => {
25233
25648
  let stdout = "";
25234
25649
  let stderr = "";
25235
25650
  const MAX = 2e5;
@@ -25245,7 +25660,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
25245
25660
  if (settled) return;
25246
25661
  settled = true;
25247
25662
  clearTimeout(timer);
25248
- resolve16(result);
25663
+ resolve17(result);
25249
25664
  };
25250
25665
  const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
25251
25666
  const timer = setTimeout(() => {
@@ -25280,7 +25695,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
25280
25695
  }
25281
25696
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
25282
25697
  var MAX_TAIL_LINES = 1e5;
25283
- async function fileLogs(path39, lines, filterRe, stream) {
25698
+ async function fileLogs(path40, lines, filterRe) {
25284
25699
  const { createInterface } = await import("node:readline");
25285
25700
  const { createReadStream: createReadStream2 } = await import("node:fs");
25286
25701
  const entries = [];
@@ -25289,7 +25704,7 @@ async function fileLogs(path39, lines, filterRe, stream) {
25289
25704
  let writeIdx = 0;
25290
25705
  let totalLines = 0;
25291
25706
  const rl = createInterface({
25292
- input: createReadStream2(path39),
25707
+ input: createReadStream2(path40),
25293
25708
  crlfDelay: Number.POSITIVE_INFINITY
25294
25709
  });
25295
25710
  for await (const line of rl) {
@@ -25310,11 +25725,11 @@ async function fileLogs(path39, lines, filterRe, stream) {
25310
25725
  if (parsed) entries.push(parsed);
25311
25726
  }
25312
25727
  return {
25313
- source: path39,
25728
+ source: path40,
25314
25729
  entries,
25315
25730
  total: entries.length,
25316
25731
  truncated: totalLines > effLines,
25317
- stream_mode: stream
25732
+ stream_mode: false
25318
25733
  };
25319
25734
  }
25320
25735
  function parseLogLines(output, filterRe) {
@@ -25390,25 +25805,12 @@ var outdatedTool = {
25390
25805
  inputSchema: {
25391
25806
  type: "object",
25392
25807
  properties: {
25393
- cwd: { type: "string", description: "Working directory (default: cwd)" },
25394
- format: {
25395
- type: "string",
25396
- enum: ["list", "table"],
25397
- description: "Output format (default: list)"
25398
- },
25399
- include_deprecated: {
25400
- type: "boolean",
25401
- description: "Include deprecated packages (default: false)"
25402
- },
25403
- check: {
25404
- type: "string",
25405
- description: "Specific package(s) to check (comma-separated)"
25406
- }
25808
+ cwd: { type: "string", description: "Working directory (default: cwd)" }
25407
25809
  }
25408
25810
  },
25409
25811
  async execute(input, ctx, opts) {
25410
25812
  const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
25411
- const manager = await detectPackageManager(cwd);
25813
+ const manager = await detectPackageManager(cwd, ctx.projectRoot);
25412
25814
  if (manager === "npm") {
25413
25815
  try {
25414
25816
  const { detectNonJsEcosystem: detectNonJsEcosystem2 } = await Promise.resolve().then(() => (init_legacy_bridge(), legacy_bridge_exports));
@@ -25461,13 +25863,11 @@ var outdatedTool = {
25461
25863
  }
25462
25864
  }
25463
25865
  const args = ["outdated", "--json"];
25464
- if (input.format === "table") args.push("--table");
25465
- if (input.include_deprecated) args.push("--include", "deprecated");
25466
25866
  return runOutdated(manager, args, cwd, opts.signal);
25467
25867
  }
25468
25868
  };
25469
25869
  function runOutdated(manager, args, cwd, signal) {
25470
- return new Promise((resolve16) => {
25870
+ return new Promise((resolve17) => {
25471
25871
  let stdout = "";
25472
25872
  let stderr = "";
25473
25873
  const MAX = 1e5;
@@ -25492,10 +25892,10 @@ function runOutdated(manager, args, cwd, signal) {
25492
25892
  });
25493
25893
  child.on("close", (code) => {
25494
25894
  const result = parseOutdatedOutput(stdout, code ?? 0);
25495
- resolve16(result);
25895
+ resolve17(result);
25496
25896
  });
25497
25897
  child.on("error", (e) => {
25498
- resolve16({
25898
+ resolve17({
25499
25899
  exit_code: 1,
25500
25900
  packages: [],
25501
25901
  total: 0,
@@ -25516,27 +25916,39 @@ function parseOutdatedOutput(json2, exitCode) {
25516
25916
  truncated: false
25517
25917
  };
25518
25918
  }
25919
+ const truncated = json2.length >= 1e5 || Buffer.byteLength(json2, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
25920
+ let parsedOk = false;
25519
25921
  try {
25520
25922
  const data = JSON.parse(json2);
25923
+ parsedOk = true;
25521
25924
  for (const name of Object.keys(data)) {
25522
- const info = data[name];
25925
+ const info = data[name] ?? {};
25926
+ const str = (v) => typeof v === "string" ? v : void 0;
25523
25927
  packages.push({
25524
25928
  name,
25525
- current: info.current ?? "unknown",
25526
- latest: info.latest ?? "unknown",
25527
- wanted: info.wanted ?? "unknown",
25528
- type: info.type ?? "unknown",
25529
- location: info.location ?? name
25929
+ current: str(info["current"]) ?? "unknown",
25930
+ latest: str(info["latest"]) ?? "unknown",
25931
+ wanted: str(info["wanted"]) ?? "unknown",
25932
+ // npm calls it `type`; pnpm calls it `dependencyType`.
25933
+ type: str(info["type"]) ?? str(info["dependencyType"]) ?? "unknown",
25934
+ location: str(info["location"]) ?? name
25530
25935
  });
25531
25936
  }
25532
25937
  } catch {
25938
+ }
25939
+ const outdatedFound = parsedOk && exitCode === 1;
25940
+ let output = normalizeCommandOutput(json2);
25941
+ if (outdatedFound) {
25942
+ output = `${output}
25943
+
25944
+ Note: exit code 1 from \`outdated\` means outdated packages were found (expected); treated as success.`;
25533
25945
  }
25534
25946
  return {
25535
- exit_code: exitCode,
25947
+ exit_code: outdatedFound ? 0 : exitCode,
25536
25948
  packages,
25537
25949
  total: packages.length,
25538
- output: json2,
25539
- truncated: json2.length >= 1e5
25950
+ output,
25951
+ truncated
25540
25952
  };
25541
25953
  }
25542
25954
 
@@ -25545,8 +25957,8 @@ init_util();
25545
25957
  import { spawn as spawn13 } from "node:child_process";
25546
25958
  import * as fs28 from "node:fs/promises";
25547
25959
  import * as os9 from "node:os";
25548
- import * as path33 from "node:path";
25549
- import { buildChildEnv as buildChildEnv8, toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
25960
+ import * as path34 from "node:path";
25961
+ import { buildChildEnv as buildChildEnv8, toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
25550
25962
  var patchTool = {
25551
25963
  name: "patch",
25552
25964
  category: "Filesystem",
@@ -25590,26 +26002,26 @@ var patchTool = {
25590
26002
  try {
25591
26003
  dir = input.directory ? await safeResolveReal(input.directory, ctx) : ctx.cwd;
25592
26004
  } catch (err) {
25593
- return refuse(`patch refused: ${toErrorMessage4(err)}`);
26005
+ return refuse(`patch refused: ${toErrorMessage7(err)}`);
25594
26006
  }
25595
- const realRoot = await fs28.realpath(ctx.projectRoot).catch(() => path33.resolve(ctx.projectRoot));
26007
+ const realRoot = await fs28.realpath(ctx.projectRoot).catch(() => path34.resolve(ctx.projectRoot));
25596
26008
  const targets = extractDiffTargets(input.patch);
25597
26009
  const resolvedTargets = [];
25598
26010
  for (const t of targets) {
25599
26011
  const stripped = stripPathComponents(t.raw, strip);
25600
26012
  if (!stripped) continue;
25601
- if (path33.isAbsolute(stripped)) {
26013
+ if (path34.isAbsolute(stripped)) {
25602
26014
  return refuse(`patch refused: target "${t.raw}" strips to absolute path`);
25603
26015
  }
25604
- const candidate = path33.resolve(dir, stripped);
26016
+ const candidate = path34.resolve(dir, stripped);
25605
26017
  let real;
25606
26018
  try {
25607
26019
  real = await resolveRealInsideRoot(candidate, ctx);
25608
26020
  } catch (err) {
25609
- return refuse(`patch refused: target "${t.raw}" ${toErrorMessage4(err)}`);
26021
+ return refuse(`patch refused: target "${t.raw}" ${toErrorMessage7(err)}`);
25610
26022
  }
25611
- const rel = path33.relative(realRoot, real);
25612
- if (rel.startsWith("..") || path33.isAbsolute(rel)) {
26023
+ const rel = path34.relative(realRoot, real);
26024
+ if (rel.startsWith("..") || path34.isAbsolute(rel)) {
25613
26025
  return refuse(`patch refused: target "${t.raw}" resolves outside project root`);
25614
26026
  }
25615
26027
  resolvedTargets.push({ raw: t.raw, deleted: t.deleted, abs: real });
@@ -25623,11 +26035,11 @@ var patchTool = {
25623
26035
  beforeContents.set(target.abs, await readTextForTracking(target.abs));
25624
26036
  }
25625
26037
  }
25626
- const tmpDir = await fs28.mkdtemp(path33.join(os9.tmpdir(), ".wstack_patch_"));
26038
+ const tmpDir = await fs28.mkdtemp(path34.join(os9.tmpdir(), ".wstack_patch_"));
25627
26039
  try {
25628
26040
  await fs28.chmod(tmpDir, 448).catch(() => {
25629
26041
  });
25630
- const patchFile = path33.join(tmpDir, "in.diff");
26042
+ const patchFile = path34.join(tmpDir, "in.diff");
25631
26043
  await fs28.writeFile(patchFile, input.patch, { mode: 384 });
25632
26044
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
25633
26045
  const result = await runPatch(args, dir, opts.signal, {
@@ -25640,8 +26052,8 @@ var patchTool = {
25640
26052
  for (const target of resolvedTargets) {
25641
26053
  const abs = target.abs;
25642
26054
  const before = beforeContents.get(abs) ?? null;
25643
- const stat18 = await fs28.stat(abs).catch(() => null);
25644
- if (!stat18?.isFile()) {
26055
+ const stat19 = await fs28.stat(abs).catch(() => null);
26056
+ if (!stat19?.isFile()) {
25645
26057
  if (beforeExisted.has(abs)) {
25646
26058
  touched.push(abs);
25647
26059
  ctx.session?.recordFileChange?.({
@@ -25656,7 +26068,7 @@ var patchTool = {
25656
26068
  const after = await readTextForTracking(abs);
25657
26069
  if (after === null || after === before) continue;
25658
26070
  touched.push(abs);
25659
- ctx.recordRead?.(abs, stat18.mtimeMs, "write", sha256hex(after));
26071
+ ctx.recordRead?.(abs, stat19.mtimeMs, "write", sha256hex(after));
25660
26072
  ctx.session?.recordFileChange?.({
25661
26073
  path: abs,
25662
26074
  action: before === null ? "created" : "modified",
@@ -25667,7 +26079,7 @@ var patchTool = {
25667
26079
  }
25668
26080
  if (result.exitCode !== 0) {
25669
26081
  if (!dryRun) {
25670
- const partial = touched.length > 0 ? ` ${touched.length} file(s) were still modified on disk and have been recorded for rewind: ${touched.map((p) => path33.relative(realRoot, p) || p).join(", ")}.` : "";
26082
+ const partial = touched.length > 0 ? ` ${touched.length} file(s) were still modified on disk and have been recorded for rewind: ${touched.map((p) => path34.relative(realRoot, p) || p).join(", ")}.` : "";
25671
26083
  return {
25672
26084
  applied: touched.length,
25673
26085
  rejected: 1,
@@ -25675,7 +26087,7 @@ var patchTool = {
25675
26087
  // success path (which returns GNU patch's dir-relative names).
25676
26088
  // `touched` entries are realpaths from resolveRealInsideRoot, and
25677
26089
  // realRoot is also a realpath, so path.relative is like-for-like.
25678
- files: touched.map((p) => path33.relative(realRoot, p) || p),
26090
+ files: touched.map((p) => path34.relative(realRoot, p) || p),
25679
26091
  dry_run: dryRun,
25680
26092
  message: `patch failed: ${result.stderr || result.stdout}${partial}`
25681
26093
  };
@@ -25691,7 +26103,7 @@ var patchTool = {
25691
26103
  }
25692
26104
  const patched = result.engine === "git" ? [
25693
26105
  ...new Set(
25694
- resolvedTargets.map((target) => path33.relative(dir, target.abs) || target.abs)
26106
+ resolvedTargets.map((target) => path34.relative(dir, target.abs) || target.abs)
25695
26107
  )
25696
26108
  ] : extractPatchedFiles(result.stdout);
25697
26109
  return {
@@ -25710,8 +26122,8 @@ var patchTool = {
25710
26122
  var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
25711
26123
  async function readTextForTracking(absPath) {
25712
26124
  try {
25713
- const stat18 = await fs28.stat(absPath);
25714
- if (!stat18.isFile() || stat18.size > MAX_TRACKING_BYTES) return null;
26125
+ const stat19 = await fs28.stat(absPath);
26126
+ if (!stat19.isFile() || stat19.size > MAX_TRACKING_BYTES) return null;
25715
26127
  const buf = await fs28.readFile(absPath);
25716
26128
  if (buf.includes(0)) return null;
25717
26129
  return buf.toString("utf8");
@@ -25796,7 +26208,7 @@ function runPatch(args, cwd, signal, fallback) {
25796
26208
  });
25797
26209
  }
25798
26210
  function runPatchProcess(command, args, cwd, signal) {
25799
- return new Promise((resolve16) => {
26211
+ return new Promise((resolve17) => {
25800
26212
  let stdout = "";
25801
26213
  let stderr = "";
25802
26214
  const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
@@ -25815,11 +26227,11 @@ function runPatchProcess(command, args, cwd, signal) {
25815
26227
  });
25816
26228
  child.on(
25817
26229
  "close",
25818
- (code) => resolve16({ exitCode: code ?? 1, stdout, stderr, unavailable: false })
26230
+ (code) => resolve17({ exitCode: code ?? 1, stdout, stderr, unavailable: false })
25819
26231
  );
25820
26232
  child.on(
25821
26233
  "error",
25822
- (e) => resolve16({
26234
+ (e) => resolve17({
25823
26235
  exitCode: 1,
25824
26236
  stdout: "",
25825
26237
  stderr: e.message,
@@ -26197,7 +26609,8 @@ var todoTool = {
26197
26609
  }
26198
26610
  for (const planId of completedPlanIds) {
26199
26611
  if (pendingPlanIds.has(planId)) continue;
26200
- const planPath = ctx.meta["plan.path"];
26612
+ const meta = ctx.meta;
26613
+ const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
26201
26614
  if (typeof planPath !== "string" || !planPath) continue;
26202
26615
  try {
26203
26616
  const plan = await loadPlan2(planPath);
@@ -26210,7 +26623,8 @@ var todoTool = {
26210
26623
  }
26211
26624
  for (const taskId of completedTaskIds) {
26212
26625
  if (pendingTaskIds.has(taskId)) continue;
26213
- const taskPath = ctx.meta["task.path"];
26626
+ const meta = ctx.meta;
26627
+ const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
26214
26628
  if (typeof taskPath !== "string" || !taskPath) continue;
26215
26629
  try {
26216
26630
  const file = await loadTasks3(taskPath);
@@ -26326,7 +26740,16 @@ var planTool = {
26326
26740
  sessionPlanPath.lastIndexOf("/"),
26327
26741
  sessionPlanPath.lastIndexOf("\\")
26328
26742
  );
26329
- planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
26743
+ if (lastSep < 0) {
26744
+ return {
26745
+ ok: false,
26746
+ message: `Cannot derive the project-scoped plan path: session plan path "${sessionPlanPath}" has no directory component.`,
26747
+ plan: "",
26748
+ count: 0,
26749
+ open: 0
26750
+ };
26751
+ }
26752
+ planPath = sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json";
26330
26753
  }
26331
26754
  } else {
26332
26755
  planPath = sessionPlanPath;
@@ -26519,6 +26942,7 @@ var planTool = {
26519
26942
  open: 0
26520
26943
  };
26521
26944
  }
26945
+ ctx.meta["plan.path.resolved"] = planPath;
26522
26946
  await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
26523
26947
  if (todosToReplace) {
26524
26948
  await todoTool.execute({ todos: todosToReplace }, ctx, {
@@ -26551,6 +26975,7 @@ var planTool = {
26551
26975
  });
26552
26976
  return f;
26553
26977
  });
26978
+ ctx.meta["task.path.resolved"] = taskPath;
26554
26979
  return mkResult(
26555
26980
  plan,
26556
26981
  true,
@@ -26584,14 +27009,14 @@ function mkResult(plan, ok, message, todos) {
26584
27009
  // src/read.ts
26585
27010
  init_util();
26586
27011
  import * as fs29 from "node:fs/promises";
26587
- import { FsError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
26588
- import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
27012
+ import { FsError, ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
27013
+ import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
26589
27014
  var ADVANCED_MODE_META_KEY = "tools.read.advancedMode";
26590
27015
  var MAX_BYTES2 = 5 * 1024 * 1024;
26591
27016
  var readTool = {
26592
27017
  name: "read",
26593
27018
  category: "Filesystem",
26594
- description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits. When advanced mode is on or `includeSymbols` is set, the result also includes a `symbols` field listing codebase-index symbol names, kinds, and line numbers for the file (not file content).",
27019
+ description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed in the form `N\u2192content` (line number, then a `\u2192` separator, then the raw line). The `N\u2192` prefix is display-only \u2014 always strip it before reusing the text, e.g. never include it in `edit.old_string`. When advanced mode is on or `includeSymbols` is set, the result also includes a `symbols` field listing codebase-index symbol names, kinds, and line numbers for the file (not file content).",
26595
27020
  usageHint: "FOUNDATIONAL TOOL \u2014 call this before almost any edit operation.\n\nBest practices:\n- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\n- Use `offset` + `limit` for very large files instead of reading everything at once.\n- Default limit is generous (2000 lines) but can be increased.\n- The output format is designed to be directly usable as context for `edit` operations.\n- Set `includeSymbols: true` to also receive the codebase-index symbol listing for the file.\n- Enable advanced mode (`ctx.meta['tools.read.advancedMode'] = true`) to auto-inject symbols on every read.",
26596
27021
  selection: {
26597
27022
  doNotUseWhen: "you need to search many files for matching content.",
@@ -26612,11 +27037,13 @@ var readTool = {
26612
27037
  },
26613
27038
  offset: {
26614
27039
  type: "integer",
27040
+ minimum: 1,
26615
27041
  description: "1-based starting line number. Use together with `limit` for large files."
26616
27042
  },
26617
27043
  limit: {
26618
27044
  type: "integer",
26619
- description: "Maximum number of lines to return (default is 2000)."
27045
+ minimum: 0,
27046
+ description: "Maximum number of lines to return (default 2000). Values above 5000 are clamped to 5000 \u2014 page with `offset` for more."
26620
27047
  },
26621
27048
  mode: {
26622
27049
  type: "string",
@@ -26632,16 +27059,16 @@ var readTool = {
26632
27059
  },
26633
27060
  async execute(input, ctx, execOpts) {
26634
27061
  if (!input?.path) {
26635
- throw new ToolValidationError5({
27062
+ throw new ToolValidationError8({
26636
27063
  message: "read: path is required",
26637
27064
  field: "path"
26638
27065
  });
26639
27066
  }
26640
27067
  const absPath = await safeResolveReal(input.path, ctx);
26641
27068
  const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
26642
- let stat18;
27069
+ let stat19;
26643
27070
  try {
26644
- stat18 = await fs29.stat(absPath);
27071
+ stat19 = await fs29.stat(absPath);
26645
27072
  } catch (err) {
26646
27073
  const code = err.code;
26647
27074
  if (code === "ENOENT") {
@@ -26653,14 +27080,14 @@ var readTool = {
26653
27080
  });
26654
27081
  }
26655
27082
  throw new FsError({
26656
- message: `read: failed to stat "${input.path}": ${toErrorMessage5(err)}`,
27083
+ message: `read: failed to stat "${input.path}": ${toErrorMessage8(err)}`,
26657
27084
  code: "FS_READ_FAILED",
26658
27085
  path: absPath,
26659
27086
  context: { errno: code },
26660
27087
  cause: err
26661
27088
  });
26662
27089
  }
26663
- if (!stat18.isFile()) {
27090
+ if (!stat19.isFile()) {
26664
27091
  throw new FsError({
26665
27092
  message: `read: "${input.path}" is not a regular file`,
26666
27093
  code: "FS_READ_FAILED",
@@ -26668,23 +27095,23 @@ var readTool = {
26668
27095
  context: { reason: "not-a-regular-file" }
26669
27096
  });
26670
27097
  }
26671
- if (stat18.size > MAX_BYTES2) {
27098
+ if (stat19.size > MAX_BYTES2) {
26672
27099
  throw new FsError({
26673
- message: `read: file too large (${stat18.size} bytes, limit ${MAX_BYTES2})`,
27100
+ message: `read: file too large (${stat19.size} bytes, limit ${MAX_BYTES2})`,
26674
27101
  code: "FS_READ_FAILED",
26675
27102
  path: absPath,
26676
- context: { size: stat18.size, limit: MAX_BYTES2, reason: "too-large" }
27103
+ context: { size: stat19.size, limit: MAX_BYTES2, reason: "too-large" }
26677
27104
  });
26678
27105
  }
26679
27106
  const offset = Math.max(1, input.offset ?? 1);
26680
27107
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
26681
27108
  const prior = getReadRangeRecord(ctx, absPath);
26682
27109
  const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
26683
- if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat18.mtimeMs, offset, requestedEnd)) {
26684
- ctx.recordRead(absPath, stat18.mtimeMs);
27110
+ if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat19.mtimeMs, offset, requestedEnd)) {
27111
+ ctx.recordRead(absPath, stat19.mtimeMs);
26685
27112
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
26686
27113
  return {
26687
- text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat18.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
27114
+ text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat19.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
26688
27115
  total_lines: prior.totalLines,
26689
27116
  encoding: "utf8",
26690
27117
  truncated: requestedEnd < prior.totalLines,
@@ -26695,18 +27122,23 @@ var readTool = {
26695
27122
  }
26696
27123
  const buf = await fs29.readFile(absPath);
26697
27124
  if (isBinaryBuffer(buf)) {
26698
- throw new Error(`read: "${input.path}" appears to be binary`);
27125
+ throw new FsError({
27126
+ message: `read: "${input.path}" appears to be binary`,
27127
+ code: "FS_READ_FAILED",
27128
+ path: absPath,
27129
+ context: { reason: "binary" }
27130
+ });
26699
27131
  }
26700
27132
  const text = buf.toString("utf8");
26701
27133
  const contentHash = sha256hex(text);
26702
27134
  const allLines = text.split(/\r\n|\r|\n/);
26703
27135
  const total = allLines.length;
26704
27136
  if (input.mode === "summary") {
26705
- ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
26706
- rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, Math.min(total, 200));
27137
+ ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
27138
+ rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, Math.min(total, 200));
26707
27139
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
26708
27140
  return {
26709
- text: summarizeFile(input.path, stat18.size, allLines),
27141
+ text: summarizeFile(input.path, stat19.size, allLines),
26710
27142
  total_lines: total,
26711
27143
  encoding: "utf8",
26712
27144
  truncated: total > 200,
@@ -26718,8 +27150,8 @@ var readTool = {
26718
27150
  };
26719
27151
  }
26720
27152
  if (limit === 0) {
26721
- ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
26722
- rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, 0);
27153
+ ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
27154
+ rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, 0);
26723
27155
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
26724
27156
  return {
26725
27157
  text: "",
@@ -26731,8 +27163,8 @@ var readTool = {
26731
27163
  };
26732
27164
  }
26733
27165
  if (offset > total) {
26734
- ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
26735
- rememberReadRange(ctx, absPath, stat18.mtimeMs, total, total + 1, total + 1);
27166
+ ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
27167
+ rememberReadRange(ctx, absPath, stat19.mtimeMs, total, total + 1, total + 1);
26736
27168
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
26737
27169
  return {
26738
27170
  text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
@@ -26747,8 +27179,8 @@ var readTool = {
26747
27179
  const truncated = offset - 1 + slice.length < total;
26748
27180
  const width = String(offset + slice.length - 1).length;
26749
27181
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
26750
- ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
26751
- rememberReadRange(ctx, absPath, stat18.mtimeMs, total, offset, offset + slice.length - 1);
27182
+ ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
27183
+ rememberReadRange(ctx, absPath, stat19.mtimeMs, total, offset, offset + slice.length - 1);
26752
27184
  const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
26753
27185
  return {
26754
27186
  text: numbered,
@@ -26856,8 +27288,8 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
26856
27288
  // src/replace.ts
26857
27289
  import { spawn as spawn14 } from "node:child_process";
26858
27290
  import * as fs30 from "node:fs/promises";
26859
- import * as path34 from "node:path";
26860
- import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
27291
+ import * as path35 from "node:path";
27292
+ import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core/types";
26861
27293
  import {
26862
27294
  atomicWrite as atomicWrite3,
26863
27295
  buildChildEnv as buildChildEnv9,
@@ -26869,12 +27301,13 @@ import {
26869
27301
  unifiedDiff as unifiedDiff2
26870
27302
  } from "@wrongstack/core/utils";
26871
27303
  init_util();
27304
+ var MAX_DIFF_BYTES2 = 262144;
26872
27305
  var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
26873
27306
  var replaceTool = {
26874
27307
  name: "replace",
26875
27308
  category: "Transform",
26876
27309
  description: "Perform a search-and-replace across multiple files using a regex pattern. This is a powerful bulk transformation tool. Dry-run is ON by default \u2014 set `dry_run: false` to apply changes.",
26877
- usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Run without `dry_run: false` first to see exactly what would change (dry-run is the default).\n2. Review the diff output, then re-run with `dry_run: false` to apply.\n3. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n4. `replace_all` controls whether only the first match per file or all matches are replaced.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
27310
+ usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Run without `dry_run: false` first to see exactly what would change (dry-run is the default).\n2. Review the diff output, then re-run with `dry_run: false` to apply.\n3. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n4. `replace_all` controls whether only the first match per file or all matches are replaced.\n5. `replacement` supports regex substitutions: `$1`\u2013`$9` insert capture groups, `$&` inserts the whole match, and `$$` inserts a literal dollar sign.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
26878
27311
  permission: "confirm",
26879
27312
  // WS-046: gives permission decisions something to key on.
26880
27313
  // The file scope being rewritten, not the pattern: a trust rule should say
@@ -26884,11 +27317,15 @@ var replaceTool = {
26884
27317
  capabilities: ["fs.write"],
26885
27318
  icon: "edit",
26886
27319
  timeoutMs: 3e4,
27320
+ maxOutputBytes: 262144,
26887
27321
  inputSchema: {
26888
27322
  type: "object",
26889
27323
  properties: {
26890
27324
  pattern: { type: "string", description: "Regex pattern to match" },
26891
- replacement: { type: "string", description: "Replacement string" },
27325
+ replacement: {
27326
+ type: "string",
27327
+ description: "Replacement string. Supports `$1`\u2013`$9` (capture groups), `$&` (whole match), and `$$` (literal dollar sign) \u2014 same semantics as JavaScript String.replace."
27328
+ },
26892
27329
  files: {
26893
27330
  type: "string",
26894
27331
  description: "File(s) to target: single path, comma-separated list, or glob pattern"
@@ -26904,19 +27341,19 @@ var replaceTool = {
26904
27341
  },
26905
27342
  async execute(input, ctx) {
26906
27343
  if (!input?.pattern) {
26907
- throw new ToolValidationError6({
27344
+ throw new ToolValidationError9({
26908
27345
  message: "replace: pattern is required",
26909
27346
  field: "pattern"
26910
27347
  });
26911
27348
  }
26912
27349
  if (input.replacement === void 0) {
26913
- throw new ToolValidationError6({
27350
+ throw new ToolValidationError9({
26914
27351
  message: "replace: replacement is required",
26915
27352
  field: "replacement"
26916
27353
  });
26917
27354
  }
26918
27355
  if (!input?.files) {
26919
- throw new ToolValidationError6({
27356
+ throw new ToolValidationError9({
26920
27357
  message: "replace: files is required",
26921
27358
  field: "files"
26922
27359
  });
@@ -26924,7 +27361,7 @@ var replaceTool = {
26924
27361
  const replaceAll = input.replace_all ?? true;
26925
27362
  const compiled = compileUserRegex(input.pattern, "g");
26926
27363
  if (!compiled.ok) {
26927
- throw new ToolValidationError6({
27364
+ throw new ToolValidationError9({
26928
27365
  message: `replace: ${compiled.reason}`,
26929
27366
  field: "pattern"
26930
27367
  });
@@ -26937,6 +27374,9 @@ var replaceTool = {
26937
27374
  const realRoot = await fs30.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
26938
27375
  const results = [];
26939
27376
  let totalReplacements = 0;
27377
+ let diffBytesUsed = 0;
27378
+ let diffsOmitted = 0;
27379
+ let diffsTruncated = 0;
26940
27380
  for (const absPath of fileList) {
26941
27381
  const lstat2 = await fs30.lstat(absPath).catch((err) => {
26942
27382
  if (err.code === "ENOENT") return null;
@@ -26950,10 +27390,10 @@ var replaceTool = {
26950
27390
  } catch {
26951
27391
  continue;
26952
27392
  }
26953
- const rel = path34.relative(realRoot, realPath);
26954
- if (rel.startsWith("..") || path34.isAbsolute(rel)) continue;
26955
- const stat18 = await fs30.stat(realPath).catch(() => null);
26956
- if (!stat18?.isFile()) continue;
27393
+ const rel = path35.relative(realRoot, realPath);
27394
+ if (rel.startsWith("..") || path35.isAbsolute(rel)) continue;
27395
+ const stat19 = await fs30.stat(realPath).catch(() => null);
27396
+ if (!stat19?.isFile()) continue;
26957
27397
  let content;
26958
27398
  try {
26959
27399
  const buf = await fs30.readFile(realPath);
@@ -26972,13 +27412,13 @@ var replaceTool = {
26972
27412
  let newContentLf = contentLf;
26973
27413
  for (let i = matches.length - 1; i >= 0; i--) {
26974
27414
  const m = expectDefined8(matches[i]);
26975
- newContentLf = newContentLf.slice(0, m.index) + input.replacement + newContentLf.slice(expectDefined8(m.index) + m[0].length);
27415
+ newContentLf = newContentLf.slice(0, m.index) + expandReplacement(input.replacement, m) + newContentLf.slice(expectDefined8(m.index) + m[0].length);
26976
27416
  }
26977
27417
  re.lastIndex = 0;
26978
27418
  totalReplacements += count;
26979
27419
  if (!dryRun) {
26980
27420
  const newContent = toStyle2(newContentLf, style);
26981
- await atomicWrite3(realPath, newContent, { mode: stat18.mode & 511 });
27421
+ await atomicWrite3(realPath, newContent, { mode: stat19.mode & 511 });
26982
27422
  const written = await fs30.stat(realPath).catch(() => null);
26983
27423
  if (written) {
26984
27424
  ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
@@ -26990,24 +27430,76 @@ var replaceTool = {
26990
27430
  after: newContent
26991
27431
  });
26992
27432
  }
26993
- const diff = dryRun || matches.length > 0 ? unifiedDiff2(content, toStyle2(newContentLf, style), {
27433
+ let diff = dryRun || matches.length > 0 ? unifiedDiff2(content, toStyle2(newContentLf, style), {
26994
27434
  fromFile: absPath,
26995
27435
  toFile: absPath
26996
27436
  }) : void 0;
27437
+ if (diff !== void 0) {
27438
+ const remaining = MAX_DIFF_BYTES2 - diffBytesUsed;
27439
+ if (remaining <= 0) {
27440
+ diff = void 0;
27441
+ diffsOmitted++;
27442
+ } else {
27443
+ const capped = truncateDiffPayload(diff, remaining);
27444
+ if (capped.truncated) diffsTruncated++;
27445
+ diff = capped.text;
27446
+ diffBytesUsed += Buffer.byteLength(diff, "utf8");
27447
+ }
27448
+ }
26997
27449
  results.push({
26998
27450
  path: absPath,
26999
27451
  replacements: matches.length,
27000
27452
  diff
27001
27453
  });
27002
27454
  }
27455
+ const overBudget = diffsOmitted > 0 || diffsTruncated > 0;
27003
27456
  return {
27004
27457
  files_modified: results.length,
27005
27458
  total_replacements: totalReplacements,
27006
27459
  results,
27007
- dry_run: dryRun
27460
+ dry_run: dryRun,
27461
+ note: overBudget ? `Diff payload exceeded the 256 KiB output budget: ${diffsTruncated} diff(s) truncated, ${diffsOmitted} diff(s) omitted. Replacement counts are complete; use the read tool to inspect individual files.` : void 0
27008
27462
  };
27009
27463
  }
27010
27464
  };
27465
+ function expandReplacement(template, match) {
27466
+ if (!template.includes("$")) return template;
27467
+ let out = "";
27468
+ for (let i = 0; i < template.length; i++) {
27469
+ const ch = template[i];
27470
+ if (ch !== "$") {
27471
+ out += ch;
27472
+ continue;
27473
+ }
27474
+ const next = template[i + 1];
27475
+ if (next === "$") {
27476
+ out += "$";
27477
+ i++;
27478
+ } else if (next === "&") {
27479
+ out += match[0];
27480
+ i++;
27481
+ } else if (next !== void 0 && next >= "1" && next <= "9") {
27482
+ const idx = next.charCodeAt(0) - 48;
27483
+ if (idx < match.length) {
27484
+ out += match[idx] ?? "";
27485
+ i++;
27486
+ } else {
27487
+ out += "$";
27488
+ }
27489
+ } else {
27490
+ out += "$";
27491
+ }
27492
+ }
27493
+ return out;
27494
+ }
27495
+ function passesExtraGlob(extraGlob, name, full) {
27496
+ extraGlob.lastIndex = 0;
27497
+ const nameMatch = extraGlob.test(name);
27498
+ extraGlob.lastIndex = 0;
27499
+ const fullMatch = extraGlob.test(full);
27500
+ extraGlob.lastIndex = 0;
27501
+ return nameMatch || fullMatch;
27502
+ }
27011
27503
  async function resolveFiles2(filesInput, ctx, extraGlob) {
27012
27504
  const base = ctx.cwd;
27013
27505
  const normalized = filesInput.trim();
@@ -27018,8 +27510,9 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
27018
27510
  const resolved = [];
27019
27511
  for (const p of parts) {
27020
27512
  const absPath = safeResolve(p, ctx);
27021
- const stat18 = await fs30.stat(absPath).catch(() => null);
27022
- if (stat18?.isFile()) {
27513
+ if (extraGlob && !passesExtraGlob(extraGlob, path35.basename(absPath), absPath)) continue;
27514
+ const stat19 = await fs30.stat(absPath).catch(() => null);
27515
+ if (stat19?.isFile()) {
27023
27516
  resolved.push(absPath);
27024
27517
  }
27025
27518
  }
@@ -27030,26 +27523,32 @@ async function globFiles(pattern, base, extraGlob) {
27030
27523
  if (rgAvailable) {
27031
27524
  try {
27032
27525
  const { promise } = spawnRgFind(pattern, base);
27033
- return await promise;
27526
+ const files = await promise;
27527
+ if (extraGlob) {
27528
+ return files.filter((f) => passesExtraGlob(extraGlob, path35.basename(f), f));
27529
+ }
27530
+ return files;
27034
27531
  } catch {
27035
27532
  }
27036
27533
  }
27037
27534
  return await globNative(pattern, base, extraGlob);
27038
27535
  }
27536
+ var rgAvailabilityCache2;
27039
27537
  function checkRg() {
27040
- return new Promise((resolve16) => {
27538
+ rgAvailabilityCache2 ??= new Promise((resolve17) => {
27041
27539
  try {
27042
27540
  const p = spawn14("rg", ["--version"], {
27043
27541
  env: buildChildEnv9(),
27044
27542
  stdio: "ignore",
27045
27543
  windowsHide: true
27046
27544
  });
27047
- p.on("error", () => resolve16(false));
27048
- p.on("close", (code) => resolve16(code === 0));
27545
+ p.on("error", () => resolve17(false));
27546
+ p.on("close", (code) => resolve17(code === 0));
27049
27547
  } catch {
27050
- resolve16(false);
27548
+ resolve17(false);
27051
27549
  }
27052
27550
  });
27551
+ return rgAvailabilityCache2;
27053
27552
  }
27054
27553
  function spawnRgFind(pattern, base) {
27055
27554
  const args = ["--files", "--glob", pattern, base];
@@ -27072,10 +27571,10 @@ function spawnRgFind(pattern, base) {
27072
27571
  }
27073
27572
  });
27074
27573
  return {
27075
- promise: new Promise((resolve16, reject) => {
27574
+ promise: new Promise((resolve17, reject) => {
27076
27575
  child.on("error", reject);
27077
27576
  child.on("close", () => {
27078
- resolve16(buf.split("\n").filter(Boolean));
27577
+ resolve17(buf.split("\n").filter(Boolean));
27079
27578
  });
27080
27579
  })
27081
27580
  };
@@ -27092,10 +27591,10 @@ async function globNative(pattern, base, extraGlob) {
27092
27591
  }
27093
27592
  for (const e of entries) {
27094
27593
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
27095
- const full = path34.join(dir, e.name);
27594
+ const full = path35.join(dir, e.name);
27096
27595
  try {
27097
- const stat18 = await fs30.lstat(full);
27098
- if (stat18.isSymbolicLink()) continue;
27596
+ const stat19 = await fs30.lstat(full);
27597
+ if (stat19.isSymbolicLink()) continue;
27099
27598
  } catch {
27100
27599
  continue;
27101
27600
  }
@@ -27119,7 +27618,7 @@ async function globNative(pattern, base, extraGlob) {
27119
27618
  // src/scaffold.ts
27120
27619
  init_util();
27121
27620
  import * as fs31 from "node:fs/promises";
27122
- import * as path35 from "node:path";
27621
+ import * as path36 from "node:path";
27123
27622
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
27124
27623
  var BUILT_IN_TEMPLATES = {
27125
27624
  "npm-package": {
@@ -27270,16 +27769,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
27270
27769
  let filesCreated = 0;
27271
27770
  for (const [filePath, content] of Object.entries(templateFiles)) {
27272
27771
  const resolvedPath = substituteVars(filePath, name, vars);
27273
- const joinedPath = path35.join(cwd, resolvedPath);
27274
- const root = path35.resolve(ctx.projectRoot);
27275
- const target = path35.resolve(joinedPath);
27276
- const rel = path35.relative(root, target);
27277
- if (rel.startsWith("..") || path35.isAbsolute(rel)) {
27772
+ const joinedPath = path36.join(cwd, resolvedPath);
27773
+ const root = path36.resolve(ctx.projectRoot);
27774
+ const target = path36.resolve(joinedPath);
27775
+ const rel = path36.relative(root, target);
27776
+ if (rel.startsWith("..") || path36.isAbsolute(rel)) {
27278
27777
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
27279
27778
  }
27280
27779
  const fullPath = target;
27281
27780
  if (!dryRun) {
27282
- await fs31.mkdir(path35.dirname(fullPath), { recursive: true });
27781
+ await fs31.mkdir(path36.dirname(fullPath), { recursive: true });
27283
27782
  await atomicWrite4(fullPath, substituteVars(content, name, vars));
27284
27783
  }
27285
27784
  files.push(resolvedPath);
@@ -27310,11 +27809,12 @@ function substituteVars(content, name, vars) {
27310
27809
  }
27311
27810
 
27312
27811
  // src/search.ts
27313
- import { FetchError as FetchError3, ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
27812
+ import { FetchError as FetchError3, ToolValidationError as ToolValidationError10 } from "@wrongstack/core/types";
27314
27813
  import { expectDefined as expectDefined9 } from "@wrongstack/core/utils";
27315
- import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
27814
+ import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
27316
27815
  var DEFAULT_NUM = 10;
27317
27816
  var MAX_RESULTS = 50;
27817
+ var MAX_SNIPPET_CHARS = 300;
27318
27818
  var TIMEOUT_MS3 = 15e3;
27319
27819
  var CACHE_TTL_MS = 3e5;
27320
27820
  var CACHE_MAX_ENTRIES = 200;
@@ -27322,7 +27822,7 @@ var cache = /* @__PURE__ */ new Map();
27322
27822
  var searchTool = {
27323
27823
  name: "search",
27324
27824
  category: "Search",
27325
- description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL.",
27825
+ description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL. google and bing are best-effort HTML scrapes that fall back to duckduckgo when they return nothing usable.",
27326
27826
  usageHint: "Good for: API documentation, error messages, library usage examples, current best practices.\n\n- Prefer specific queries over very broad ones.\n- Results go through the guarded fetch system (same protections as the `fetch` tool).\n- Supports duckduckgo (default), google, and bing sources.\n- Set `skip_cache: true` to force a fresh search.\n- This is often better than the model trying to recall outdated knowledge.",
27327
27827
  permission: "auto",
27328
27828
  mutating: false,
@@ -27363,7 +27863,7 @@ var searchTool = {
27363
27863
  },
27364
27864
  async *executeStream(input, _ctx, opts) {
27365
27865
  if (!input?.query || input.query.trim() === "") {
27366
- throw new ToolValidationError7({
27866
+ throw new ToolValidationError10({
27367
27867
  message: "search: query is required and must be a non-empty string",
27368
27868
  field: "query"
27369
27869
  });
@@ -27396,7 +27896,7 @@ var searchTool = {
27396
27896
  query: input.query,
27397
27897
  results: results.slice(0, num),
27398
27898
  source: entry.source,
27399
- truncated: results.length >= num,
27899
+ truncated: results.length > num,
27400
27900
  cached: true
27401
27901
  }
27402
27902
  };
@@ -27408,41 +27908,45 @@ var searchTool = {
27408
27908
  text: `Querying ${source} for "${input.query}"\u2026`,
27409
27909
  data: { source, query: input.query, cached: false }
27410
27910
  };
27411
- let rawResults;
27911
+ let engine;
27412
27912
  let effectiveSource = source;
27413
27913
  switch (source) {
27414
27914
  case "duckduckgo":
27415
- rawResults = await duckduckgoSearch(input.query, num, opts.signal);
27915
+ engine = await duckduckgoSearch(input.query, opts.signal);
27416
27916
  break;
27417
27917
  case "google":
27418
- rawResults = await googleSearch(input.query, num, opts.signal);
27918
+ engine = await googleSearch(input.query, opts.signal);
27419
27919
  break;
27420
27920
  case "bing":
27421
- rawResults = await bingSearch(input.query, num, opts.signal);
27921
+ engine = await bingSearch(input.query, opts.signal);
27422
27922
  break;
27423
27923
  default:
27424
- throw new ToolValidationError7({
27924
+ throw new ToolValidationError10({
27425
27925
  message: `search: unknown source "${source}"`,
27426
27926
  field: "source"
27427
27927
  });
27428
27928
  }
27429
- let ranked = rankSearchResults(rawResults, input.query);
27929
+ let ranked = rankSearchResults(engine.results, input.query);
27930
+ let engineError = engine.error;
27430
27931
  if (source !== "duckduckgo" && shouldFallbackToDuckDuckGo(ranked, input.query)) {
27431
27932
  yield {
27432
27933
  type: "log",
27433
27934
  text: `${source} returned no relevant static results; falling back to duckduckgo`,
27434
27935
  data: { source, fallback: "duckduckgo", query: input.query }
27435
27936
  };
27436
- rawResults = await duckduckgoSearch(input.query, num, opts.signal);
27437
- ranked = rankSearchResults(rawResults, input.query);
27937
+ const fallback = await duckduckgoSearch(input.query, opts.signal);
27938
+ ranked = rankSearchResults(fallback.results, input.query);
27939
+ engineError = fallback.error;
27438
27940
  effectiveSource = "duckduckgo";
27439
27941
  }
27440
27942
  const finalResults = ranked.slice(0, num);
27441
- cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
27442
- pruneCacheEntries();
27943
+ if (!engineError) {
27944
+ cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
27945
+ pruneCacheEntries();
27946
+ }
27443
27947
  yield {
27444
27948
  type: "partial_output",
27445
- text: `${finalResults.length} results from ${effectiveSource}`,
27949
+ text: engineError ? `search failed: ${engineError}` : `${finalResults.length} results from ${effectiveSource}`,
27446
27950
  data: { count: finalResults.length, cached: false, source: effectiveSource }
27447
27951
  };
27448
27952
  yield {
@@ -27455,8 +27959,9 @@ var searchTool = {
27455
27959
  snippet: r.snippet
27456
27960
  })),
27457
27961
  source: effectiveSource,
27458
- truncated: finalResults.length >= num,
27459
- cached: false
27962
+ truncated: ranked.length > num,
27963
+ cached: false,
27964
+ ...engineError ? { error: engineError } : {}
27460
27965
  }
27461
27966
  };
27462
27967
  }
@@ -27507,18 +28012,18 @@ function shouldFallbackToDuckDuckGo(results, query) {
27507
28012
  return terms.some((term) => haystack.includes(term));
27508
28013
  });
27509
28014
  }
27510
- async function duckduckgoSearch(query, num, signal) {
28015
+ async function duckduckgoSearch(query, signal) {
27511
28016
  const encoded = encodeURIComponent(query);
27512
28017
  const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
27513
28018
  try {
27514
28019
  const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
27515
28020
  const html = await response.text();
27516
- return parseDuckDuckGo(html, num);
28021
+ return { results: parseDuckDuckGo(html, MAX_RESULTS) };
27517
28022
  } catch (err) {
27518
28023
  console.log(
27519
- JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage6(err) })
28024
+ JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage9(err) })
27520
28025
  );
27521
- return [{ title: "Search unavailable", url: "https://duckduckgo.com/unavailable", snippet: "Could not reach DuckDuckGo", score: 0 }];
28026
+ return { results: [], error: `duckduckgo unreachable: ${toErrorMessage9(err)}` };
27522
28027
  }
27523
28028
  }
27524
28029
  function takeFrom(iter, max) {
@@ -27552,7 +28057,7 @@ function parseDuckDuckGo(html, num) {
27552
28057
  results.push({
27553
28058
  title: entry.title ?? "",
27554
28059
  url: entry.url ?? "",
27555
- snippet: snippetMatches[i] ?? "",
28060
+ snippet: capSnippet(snippetMatches[i] ?? ""),
27556
28061
  score: 1
27557
28062
  });
27558
28063
  }
@@ -27577,11 +28082,15 @@ function normalizeDuckDuckGoUrl(raw) {
27577
28082
  return raw;
27578
28083
  }
27579
28084
  }
27580
- async function googleSearch(query, num, signal) {
28085
+ async function googleSearch(query, signal) {
27581
28086
  const encoded = encodeURIComponent(query);
27582
28087
  const url = `https://www.google.com/search?q=${encoded}&hl=en`;
27583
- const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text()).catch(() => "");
27584
- return parseGoogleResults(html, num);
28088
+ try {
28089
+ const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
28090
+ return { results: parseGoogleResults(html, MAX_RESULTS) };
28091
+ } catch (err) {
28092
+ return { results: [], error: `google unreachable: ${toErrorMessage9(err)}` };
28093
+ }
27585
28094
  }
27586
28095
  function parseGoogleResults(html, num) {
27587
28096
  const results = [];
@@ -27604,17 +28113,21 @@ function parseGoogleResults(html, num) {
27604
28113
  results.push({
27605
28114
  title: titles[i] ?? "",
27606
28115
  url: urls[i] ?? "",
27607
- snippet: snippets[i] ?? "",
28116
+ snippet: capSnippet(snippets[i] ?? ""),
27608
28117
  score: 1
27609
28118
  });
27610
28119
  }
27611
28120
  return results;
27612
28121
  }
27613
- async function bingSearch(query, num, signal) {
28122
+ async function bingSearch(query, signal) {
27614
28123
  const encoded = encodeURIComponent(query);
27615
28124
  const url = `https://www.bing.com/search?q=${encoded}`;
27616
- const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text()).catch(() => "");
27617
- return parseBingResults(html, num);
28125
+ try {
28126
+ const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
28127
+ return { results: parseBingResults(html, MAX_RESULTS) };
28128
+ } catch (err) {
28129
+ return { results: [], error: `bing unreachable: ${toErrorMessage9(err)}` };
28130
+ }
27618
28131
  }
27619
28132
  function parseBingResults(html, num) {
27620
28133
  const results = [];
@@ -27627,7 +28140,7 @@ function parseBingResults(html, num) {
27627
28140
  const title = stripTags(expectDefined9(titleMatch[2]));
27628
28141
  if (!href || !title) return [];
27629
28142
  const snippetMatch = /<p\b[^>]*class=(["'])[^"']*\b(?:b_paractl|b_lineclamp\d*)\b[^"']*\1[^>]*>([\s\S]*?)<\/p>/i.exec(block) ?? /<p\b[^>]*>([\s\S]*?)<\/p>/i.exec(block);
27630
- const snippet = snippetMatch ? stripTags(expectDefined9(snippetMatch.at(-1))) : "";
28143
+ const snippet = snippetMatch ? capSnippet(stripTags(expectDefined9(snippetMatch.at(-1)))) : "";
27631
28144
  return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];
27632
28145
  }), num);
27633
28146
  for (let i = 0; i < entries.length; i++) {
@@ -27694,17 +28207,20 @@ function anySignal(...signals) {
27694
28207
  function stripTags(html) {
27695
28208
  return decodeHtmlEntities(html.replace(/<[^>]+>/g, "")).trim();
27696
28209
  }
28210
+ function capSnippet(snippet) {
28211
+ return snippet.length > MAX_SNIPPET_CHARS ? `${snippet.slice(0, MAX_SNIPPET_CHARS - 1)}\u2026` : snippet;
28212
+ }
27697
28213
  function decodeHtmlEntities(text) {
27698
28214
  return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
27699
28215
  }
27700
28216
 
27701
28217
  // src/set-working-dir.ts
27702
28218
  import * as fs32 from "node:fs/promises";
27703
- import { toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
28219
+ import { toErrorMessage as toErrorMessage10 } from "@wrongstack/core/utils";
27704
28220
  var setWorkingDirTool = {
27705
28221
  name: "set_working_dir",
27706
28222
  category: "Context",
27707
- description: "Change the current working directory for all subsequent file operations. The new directory must be inside the project root. Use this to navigate between subdirectories when working on files in different parts of the project.",
28223
+ description: "Change the current working directory for subsequent file operations and shell tools (`bash`/`exec` spawn in this directory unless given an explicit cwd). The new directory must be inside the project root. Use this to navigate between subdirectories when working on files in different parts of the project.",
27708
28224
  usageHint: "Change the working directory so relative paths in subsequent tool calls resolve from a different directory. Pass `path` to set a new directory, or omit to query the current one. The directory must exist and be inside the project root.",
27709
28225
  permission: "confirm",
27710
28226
  mutating: true,
@@ -27734,19 +28250,23 @@ var setWorkingDirTool = {
27734
28250
  } catch (err) {
27735
28251
  return {
27736
28252
  current: ctx.workingDir,
27737
- error: toErrorMessage7(err)
28253
+ error: toErrorMessage10(err)
27738
28254
  };
27739
28255
  }
28256
+ let isDirectory = false;
27740
28257
  try {
27741
- await fs32.access(resolved);
28258
+ isDirectory = (await fs32.stat(resolved)).isDirectory();
27742
28259
  } catch {
28260
+ isDirectory = false;
28261
+ }
28262
+ if (!isDirectory) {
27743
28263
  try {
27744
28264
  ctx.setWorkingDir(previous);
27745
28265
  } catch {
27746
28266
  }
27747
28267
  return {
27748
28268
  current: ctx.workingDir,
27749
- error: `Directory does not exist: ${resolved}`
28269
+ error: `Directory does not exist (or is not a directory): ${resolved}`
27750
28270
  };
27751
28271
  }
27752
28272
  return {
@@ -28201,6 +28721,7 @@ var taskTool = {
28201
28721
  inProgress: 0
28202
28722
  };
28203
28723
  }
28724
+ ctx.meta["task.path.resolved"] = taskPath;
28204
28725
  if (todosToReplace) {
28205
28726
  await todoTool.execute({ todos: todosToReplace }, ctx, {
28206
28727
  signal: AbortSignal.timeout(3e4)
@@ -28225,6 +28746,7 @@ var taskTool = {
28225
28746
  formatted = formatPlan2(updated);
28226
28747
  return updated;
28227
28748
  });
28749
+ ctx.meta["plan.path.resolved"] = planPath;
28228
28750
  } catch (err) {
28229
28751
  return {
28230
28752
  ok: false,
@@ -28268,7 +28790,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
28268
28790
  init_spawn_stream();
28269
28791
  init_util();
28270
28792
  init_legacy_bridge();
28271
- import * as path36 from "node:path";
28793
+ import * as path37 from "node:path";
28272
28794
  var testTool = {
28273
28795
  name: "test",
28274
28796
  category: "Code Quality",
@@ -28371,11 +28893,11 @@ var testTool = {
28371
28893
  }
28372
28894
  };
28373
28895
  async function detectRunner(cwd) {
28374
- const { stat: stat18 } = await import("node:fs/promises");
28896
+ const { stat: stat19 } = await import("node:fs/promises");
28375
28897
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
28376
28898
  for (const f of candidates) {
28377
28899
  try {
28378
- await stat18(path36.join(cwd, f));
28900
+ await stat19(path37.join(cwd, f));
28379
28901
  if (f.includes("vitest")) return "vitest";
28380
28902
  if (f.includes("jest")) return "jest";
28381
28903
  if (f.includes("mocha")) return "mocha";
@@ -28753,7 +29275,7 @@ var toolUseTool = {
28753
29275
  // src/tree.ts
28754
29276
  init_util();
28755
29277
  import * as fs33 from "node:fs/promises";
28756
- import * as path37 from "node:path";
29278
+ import * as path38 from "node:path";
28757
29279
  import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
28758
29280
  var DEFAULT_IGNORE5 = /* @__PURE__ */ new Set([
28759
29281
  ...DEFAULT_WALK_IGNORE_DIRS4,
@@ -28773,6 +29295,7 @@ var treeTool = {
28773
29295
  mutating: false,
28774
29296
  capabilities: ["fs.read"],
28775
29297
  icon: "tree",
29298
+ maxOutputBytes: 262144,
28776
29299
  timeoutMs: 15e3,
28777
29300
  inputSchema: {
28778
29301
  type: "object",
@@ -28921,17 +29444,15 @@ async function walkDir(dir, depth, opts) {
28921
29444
  if (opts.exclude.has(e.name)) return false;
28922
29445
  return true;
28923
29446
  });
28924
- if (depth > 0) {
28925
- let dirCount = 0;
28926
- let fileCount = 0;
28927
- for (const e of filtered) {
28928
- if (e.isDirectory()) dirCount++;
28929
- else if (e.isFile()) fileCount++;
28930
- }
28931
- opts.totalDirs.value += dirCount;
28932
- opts.totalFiles.value += fileCount;
28933
- opts.onProgress?.();
28934
- }
29447
+ let dirCount = 0;
29448
+ let fileCount = 0;
29449
+ for (const e of filtered) {
29450
+ if (e.isDirectory()) dirCount++;
29451
+ else if (e.isFile()) fileCount++;
29452
+ }
29453
+ opts.totalDirs.value += dirCount;
29454
+ opts.totalFiles.value += fileCount;
29455
+ opts.onProgress?.();
28935
29456
  const items = filtered.sort((a, b) => {
28936
29457
  if (a.isDirectory() && !b.isDirectory()) return -1;
28937
29458
  if (!a.isDirectory() && b.isDirectory()) return 1;
@@ -28959,7 +29480,7 @@ async function walkDir(dir, depth, opts) {
28959
29480
  opts.retention.outputBytes += lineBytes;
28960
29481
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
28961
29482
  const childPrefix = opts.prefix + connector;
28962
- await walkDir(path37.join(dir, entry.name), depth + 1, {
29483
+ await walkDir(path38.join(dir, entry.name), depth + 1, {
28963
29484
  ...opts,
28964
29485
  prefix: childPrefix,
28965
29486
  isLast
@@ -28972,7 +29493,7 @@ async function walkDir(dir, depth, opts) {
28972
29493
  init_spawn_stream();
28973
29494
  init_util();
28974
29495
  init_legacy_bridge();
28975
- import * as path38 from "node:path";
29496
+ import * as path39 from "node:path";
28976
29497
  var typecheckTool = {
28977
29498
  name: "typecheck",
28978
29499
  category: "Code Quality",
@@ -28994,11 +29515,7 @@ var typecheckTool = {
28994
29515
  },
28995
29516
  all: {
28996
29517
  type: "boolean",
28997
- description: "Type-check all projects (pnpm -r) (default: false)"
28998
- },
28999
- json: {
29000
- type: "boolean",
29001
- description: "Emit JSON output from tsc (default: false)"
29518
+ description: "Type-check all workspace packages (pnpm workspaces run `pnpm -r exec tsc --noEmit`; other setups run a single `tsc --noEmit` at cwd) (default: false)"
29002
29519
  }
29003
29520
  }
29004
29521
  },
@@ -29034,29 +29551,42 @@ var typecheckTool = {
29034
29551
  };
29035
29552
  return;
29036
29553
  }
29037
- let args;
29554
+ let cmd;
29555
+ let cmdArgs;
29038
29556
  let project;
29039
29557
  if (input.all) {
29040
- args = ["--noEmit"];
29041
29558
  project = "workspace";
29559
+ const tscArgs = ["--noEmit"];
29560
+ if (input.strict) tscArgs.push("--strict");
29561
+ const manager = await detectPackageManager(cwd, ctx.projectRoot);
29562
+ if (manager === "pnpm") {
29563
+ cmd = "pnpm";
29564
+ cmdArgs = ["-r", "--no-bail", "exec", "tsc", ...tscArgs];
29565
+ } else {
29566
+ cmd = "npx";
29567
+ cmdArgs = ["tsc", ...tscArgs];
29568
+ }
29042
29569
  } else {
29043
29570
  const tsconfig = input.project ? safeResolve(input.project, ctx) : await findTsConfig(cwd);
29044
- args = ["--noEmit"];
29045
- if (input.strict) args.push("--strict");
29046
- if (tsconfig) args.push("--project", tsconfig);
29571
+ const tscArgs = ["--noEmit"];
29572
+ if (input.strict) tscArgs.push("--strict");
29573
+ if (tsconfig) tscArgs.push("--project", tsconfig);
29047
29574
  project = tsconfig ?? "default";
29575
+ cmd = "npx";
29576
+ cmdArgs = ["tsc", ...tscArgs];
29048
29577
  }
29049
- if (input.json) args.push("--json");
29050
- yield { type: "log", text: `tsc ${args.join(" ")}`, data: { project } };
29578
+ yield { type: "log", text: `${cmd} ${cmdArgs.join(" ")}`, data: { project } };
29051
29579
  const result = yield* spawnStream({
29052
- cmd: "npx",
29053
- args: ["tsc", ...args],
29580
+ cmd,
29581
+ args: cmdArgs,
29054
29582
  cwd,
29055
29583
  signal: opts.signal,
29056
29584
  maxBytes: 2e5
29057
29585
  });
29058
- const errors = [...result.stdout.matchAll(/\berror\b/gi)].length;
29059
- const warnings = [...result.stdout.matchAll(/\bwarning\b/gi)].length;
29586
+ const combined = `${result.stdout}
29587
+ ${result.stderr}`;
29588
+ const errors = [...combined.matchAll(/^.*\berror TS\d+:/gm)].length;
29589
+ const warnings = [...combined.matchAll(/^.*\bwarning TS\d+:/gm)].length;
29060
29590
  yield {
29061
29591
  type: "final",
29062
29592
  output: {
@@ -29071,12 +29601,12 @@ var typecheckTool = {
29071
29601
  }
29072
29602
  };
29073
29603
  async function findTsConfig(cwd) {
29074
- const { stat: stat18 } = await import("node:fs/promises");
29604
+ const { stat: stat19 } = await import("node:fs/promises");
29075
29605
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
29076
29606
  for (const f of candidates) {
29077
29607
  try {
29078
- const s = await stat18(path38.join(cwd, f));
29079
- if (s.isFile()) return path38.join(cwd, f);
29608
+ const s = await stat19(path39.join(cwd, f));
29609
+ if (s.isFile()) return path39.join(cwd, f);
29080
29610
  } catch {
29081
29611
  }
29082
29612
  }
@@ -29085,21 +29615,32 @@ async function findTsConfig(cwd) {
29085
29615
 
29086
29616
  // src/write.ts
29087
29617
  import * as fs34 from "node:fs/promises";
29088
- import { ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
29089
- import { atomicWrite as atomicWrite5, unifiedDiff as unifiedDiff3 } from "@wrongstack/core/utils";
29618
+ import { ToolValidationError as ToolValidationError11 } from "@wrongstack/core/types";
29619
+ import {
29620
+ atomicWrite as atomicWrite5,
29621
+ detectNewlineStyle as detectNewlineStyle3,
29622
+ normalizeToLf as normalizeToLf3,
29623
+ toStyle as toStyle3,
29624
+ unifiedDiff as unifiedDiff3
29625
+ } from "@wrongstack/core/utils";
29090
29626
  init_util();
29627
+ var MAX_DIFF_BYTES3 = 262144;
29091
29628
  var writeTool = {
29092
29629
  name: "write",
29093
29630
  category: "Filesystem",
29094
29631
  description: "Write or completely overwrite a file on disk. This is a high-privilege operation. For modifying existing files, you should almost always prefer the `edit` tool instead, because `edit` is safer and works on the last-read version of the file.",
29095
- usageHint: "RULES FOR CORRECT USAGE:\n- Use `write` primarily for **new files** or when you want to replace the entire content.\n- For any existing file, strongly prefer `edit` (it requires a prior `read` in the same session and is more precise).\n- You MUST have called `read` on the file earlier in the conversation before using `write` on an existing path (the system enforces this for safety).\n- The path is resolved relative to the project root and protected against escaping the workspace.",
29632
+ usageHint: "RULES FOR CORRECT USAGE:\n- Use `write` primarily for **new files** or when you want to replace the entire content.\n- For any existing file, strongly prefer `edit` (it requires a prior `read` in the same session and is more precise).\n- When overwriting an existing file, the tool reads the current content itself to compute the diff \u2014 but still `read` the file first before large rewrites so you know what you are replacing.\n- When overwriting an existing file, the content is normalized to the dominant line-ending style (CRLF/LF) of the existing file; new files are written verbatim.\n- The path is resolved relative to the project root and protected against escaping the workspace.",
29096
29633
  selection: {
29097
29634
  doNotUseWhen: "making a precise change to part of an existing file.",
29098
29635
  useInstead: ["edit"]
29099
29636
  },
29100
29637
  permission: "confirm",
29638
+ // WS-046: gives permission decisions something to key on — the file being
29639
+ // written, so trust rules can scope by path.
29640
+ subjectKey: "path",
29101
29641
  mutating: true,
29102
29642
  timeoutMs: 5e3,
29643
+ maxOutputBytes: 262144,
29103
29644
  capabilities: ["fs.write"],
29104
29645
  icon: "file",
29105
29646
  inputSchema: {
@@ -29135,13 +29676,13 @@ async function writeFile6(input, ctx, signal) {
29135
29676
  }
29136
29677
  async function prepareWrite(input, ctx) {
29137
29678
  if (!input?.path) {
29138
- throw new ToolValidationError8({
29679
+ throw new ToolValidationError11({
29139
29680
  message: "write: path is required",
29140
29681
  field: "path"
29141
29682
  });
29142
29683
  }
29143
29684
  if (input.content === void 0) {
29144
- throw new ToolValidationError8({
29685
+ throw new ToolValidationError11({
29145
29686
  message: "write: content is required",
29146
29687
  field: "content"
29147
29688
  });
@@ -29150,12 +29691,12 @@ async function prepareWrite(input, ctx) {
29150
29691
  let existed = false;
29151
29692
  let prev = "";
29152
29693
  try {
29153
- const stat18 = await fs34.stat(absPath);
29154
- existed = stat18.isFile();
29694
+ const stat19 = await fs34.stat(absPath);
29695
+ existed = stat19.isFile();
29155
29696
  if (existed) {
29156
29697
  if (!ctx.hasRead(absPath)) {
29157
29698
  prev = await fs34.readFile(absPath, "utf8");
29158
- ctx.recordRead(absPath, stat18.mtimeMs, "write", sha256hex(prev));
29699
+ ctx.recordRead(absPath, stat19.mtimeMs, "write", sha256hex(prev));
29159
29700
  } else {
29160
29701
  prev = await fs34.readFile(absPath, "utf8");
29161
29702
  }
@@ -29168,31 +29709,42 @@ async function prepareWrite(input, ctx) {
29168
29709
  return { absPath, existed, prev };
29169
29710
  }
29170
29711
  async function finishWrite(input, ctx, prepared, signal) {
29712
+ const content = prepared.existed ? toStyle3(normalizeToLf3(input.content), detectNewlineStyle3(prepared.prev)) : input.content;
29171
29713
  signal?.throwIfAborted();
29172
- await atomicWrite5(prepared.absPath, input.content);
29173
- const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
29174
- + (new file, ${input.content.split("\n").length} lines)`;
29175
- const stat18 = await fs34.stat(prepared.absPath);
29176
- ctx.recordRead(prepared.absPath, stat18.mtimeMs, "write", sha256hex(input.content));
29714
+ await atomicWrite5(prepared.absPath, content);
29715
+ const rawDiff = prepared.existed ? unifiedDiff3(prepared.prev, content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
29716
+ + (new file, ${content.split("\n").length} lines)`;
29717
+ const { text: diff, truncated: diffTruncated } = truncateDiffPayload(rawDiff, MAX_DIFF_BYTES3);
29718
+ const stat19 = await fs34.stat(prepared.absPath);
29719
+ ctx.recordRead(prepared.absPath, stat19.mtimeMs, "write", sha256hex(content));
29177
29720
  ctx.session.recordFileChange({
29178
29721
  path: prepared.absPath,
29179
29722
  action: prepared.existed ? "modified" : "created",
29180
29723
  before: prepared.existed ? prepared.prev : null,
29181
- after: input.content
29724
+ after: content
29182
29725
  });
29183
29726
  const syntax = await checkSyntax(
29184
29727
  prepared.absPath,
29185
- input.content,
29728
+ content,
29186
29729
  prepared.existed ? prepared.prev : void 0
29187
29730
  ).catch(() => void 0);
29188
29731
  const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
29732
+ const notes = [];
29733
+ if (diffTruncated) {
29734
+ notes.push("Diff truncated to the 256 KiB output budget \u2014 the full write is on disk.");
29735
+ }
29736
+ if (hasSyntaxErrors) {
29737
+ notes.push(
29738
+ syntax.preExisting ? "Syntax check: the file still has parse errors (they pre-date this write) \u2014 see syntax_errors." : `Syntax check: the written content has ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`
29739
+ );
29740
+ }
29189
29741
  return {
29190
29742
  path: prepared.absPath,
29191
- bytes_written: Buffer.byteLength(input.content, "utf8"),
29743
+ bytes_written: Buffer.byteLength(content, "utf8"),
29192
29744
  created: !prepared.existed,
29193
29745
  diff,
29194
29746
  syntax_errors: hasSyntaxErrors ? syntax.errors : void 0,
29195
- note: hasSyntaxErrors ? syntax.preExisting ? "Syntax check: the file still has parse errors (they pre-date this write) \u2014 see syntax_errors." : `Syntax check: the written content has ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.` : void 0
29747
+ note: notes.length > 0 ? notes.join("\n") : void 0
29196
29748
  };
29197
29749
  }
29198
29750