@wrongstack/tools 0.305.1 → 0.306.2

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 (63) 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 +1294 -726
  9. package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
  10. package/dist/codebase-index/index.js +223 -152
  11. package/dist/codebase-index/project-server.js +10 -11
  12. package/dist/diff.d.ts +5 -0
  13. package/dist/diff.js +78 -12
  14. package/dist/document.js +18 -6
  15. package/dist/edit.js +69 -16
  16. package/dist/exec.js +44 -22
  17. package/dist/fetch.js +13 -1
  18. package/dist/format.d.ts +4 -2
  19. package/dist/format.js +81 -31
  20. package/dist/glob.js +12 -4
  21. package/dist/grep.d.ts +2 -0
  22. package/dist/grep.js +15 -4
  23. package/dist/index.js +1359 -762
  24. package/dist/install.js +96 -37
  25. package/dist/kanban-tool-types.d.ts +6 -1
  26. package/dist/kanban.js +60 -0
  27. package/dist/languages/index.js +28 -13
  28. package/dist/lint.js +28 -13
  29. package/dist/logs.d.ts +0 -1
  30. package/dist/logs.js +44 -13
  31. package/dist/memory.d.ts +8 -0
  32. package/dist/memory.js +23 -3
  33. package/dist/mode.d.ts +1 -1
  34. package/dist/mode.js +3 -0
  35. package/dist/next-steps.d.ts +2 -3
  36. package/dist/next-steps.js +3 -3
  37. package/dist/outdated.d.ts +0 -3
  38. package/dist/outdated.js +89 -48
  39. package/dist/pack.js +1294 -726
  40. package/dist/plan.js +91 -3
  41. package/dist/process-registry.d.ts +8 -2
  42. package/dist/process-registry.js +28 -13
  43. package/dist/ps-slash.js +22 -12
  44. package/dist/read.js +10 -3
  45. package/dist/replace.d.ts +4 -0
  46. package/dist/replace.js +104 -7
  47. package/dist/search.d.ts +6 -0
  48. package/dist/search.js +47 -26
  49. package/dist/session-kanban.js +3 -1
  50. package/dist/skill.d.ts +6 -0
  51. package/dist/skill.js +9 -10
  52. package/dist/task.js +81 -2
  53. package/dist/test.js +28 -13
  54. package/dist/todo.js +79 -2
  55. package/dist/tool-icons.js +4 -2
  56. package/dist/tool-summary.d.ts +1 -1
  57. package/dist/tool-summary.js +76 -1
  58. package/dist/tool-tier.js +1294 -726
  59. package/dist/tree.js +9 -10
  60. package/dist/typecheck.d.ts +0 -2
  61. package/dist/typecheck.js +98 -31
  62. package/dist/write.js +58 -10
  63. package/package.json +4 -4
package/dist/plan.js CHANGED
@@ -426,6 +426,7 @@ function sourceStatus(task) {
426
426
  function todoStatus(task) {
427
427
  const status = sourceStatus(task);
428
428
  if (status === "completed") return "completed";
429
+ if (status === "review" && task.assignment?.status === "completed") return "completed";
429
430
  if (status === "in_progress" || status === "review") return "in_progress";
430
431
  return "pending";
431
432
  }
@@ -1390,6 +1391,18 @@ var KANBAN_INPUT_SCHEMA = {
1390
1391
  },
1391
1392
  transitionAction: { type: "string" },
1392
1393
  transitionComment: { type: "string" },
1394
+ tickChecks: {
1395
+ type: "array",
1396
+ items: {
1397
+ type: "object",
1398
+ properties: {
1399
+ checkId: { type: "string" },
1400
+ checkStatus: { type: "string", enum: ["passed", "failed", "skipped"] }
1401
+ },
1402
+ required: ["checkId", "checkStatus"]
1403
+ },
1404
+ 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."
1405
+ },
1393
1406
  attachmentUrl: { type: "string" },
1394
1407
  attachmentTitle: { type: "string" },
1395
1408
  attachmentType: {
@@ -1577,6 +1590,9 @@ var kanbanTool = {
1577
1590
  description: KANBAN_TOOL_DESCRIPTION,
1578
1591
  usageHint: KANBAN_TOOL_USAGE_HINT,
1579
1592
  permission: "confirm",
1593
+ // WS-046: gives permission decisions something to key on.
1594
+ // The action performed; kanban has no single file or path subject.
1595
+ subjectKey: "action",
1580
1596
  mutating: true,
1581
1597
  capabilities: ["fs.write"],
1582
1598
  icon: "task",
@@ -2018,6 +2034,7 @@ var kanbanTool = {
2018
2034
  actor: input.author,
2019
2035
  comment: input.transitionComment,
2020
2036
  ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
2037
+ ...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
2021
2038
  ...input.attachmentUrl !== void 0 ? {
2022
2039
  attachment: {
2023
2040
  url: input.attachmentUrl,
@@ -2410,8 +2427,52 @@ var kanbanTool = {
2410
2427
  } catch (err) {
2411
2428
  return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
2412
2429
  }
2430
+ },
2431
+ serialize(output, input) {
2432
+ return serializeKanbanOutput(output, input);
2413
2433
  }
2414
2434
  };
2435
+ var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
2436
+ var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
2437
+ "get_board",
2438
+ "export_markdown",
2439
+ "export_task_graph"
2440
+ ]);
2441
+ function serializeKanbanOutput(output, input) {
2442
+ const action = input && typeof input === "object" ? input.action : void 0;
2443
+ const board = output.board;
2444
+ if (board) {
2445
+ const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
2446
+ let boardBytes = 0;
2447
+ if (!keepFull) {
2448
+ try {
2449
+ boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
2450
+ } catch {
2451
+ boardBytes = 0;
2452
+ }
2453
+ }
2454
+ if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
2455
+ const columns = {};
2456
+ for (const column of board.columns) {
2457
+ columns[column.title || column.id] = board.tasks.filter(
2458
+ (task) => task.columnId === column.id
2459
+ ).length;
2460
+ }
2461
+ const compact = {
2462
+ ...output,
2463
+ board: {
2464
+ id: board.id,
2465
+ title: board.title,
2466
+ columns,
2467
+ totalTasks: board.tasks.length,
2468
+ note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
2469
+ }
2470
+ };
2471
+ return JSON.stringify(compact, null, 2);
2472
+ }
2473
+ }
2474
+ return JSON.stringify(output, null, 2);
2475
+ }
2415
2476
 
2416
2477
  // src/todo.ts
2417
2478
  function normalizedTitle(value) {
@@ -2539,6 +2600,20 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
2539
2600
  }
2540
2601
  const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
2541
2602
  if (!task || task.status === "completed") continue;
2603
+ const stage = task.lifecycle?.currentStage;
2604
+ if (stage === "backlog" || stage === "todo") {
2605
+ const started = await execute({
2606
+ action: "start_task",
2607
+ boardId: board.id,
2608
+ taskId: task.id,
2609
+ author: actor,
2610
+ agentId: actor,
2611
+ transitionComment: `Auto-started for completion: ${item.content}`
2612
+ });
2613
+ if (!started.ok) {
2614
+ continue;
2615
+ }
2616
+ }
2542
2617
  await execute({
2543
2618
  action: "mark_assignment",
2544
2619
  boardId: board.id,
@@ -2750,7 +2825,8 @@ var todoTool = {
2750
2825
  }
2751
2826
  for (const planId of completedPlanIds) {
2752
2827
  if (pendingPlanIds.has(planId)) continue;
2753
- const planPath = ctx.meta["plan.path"];
2828
+ const meta = ctx.meta;
2829
+ const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
2754
2830
  if (typeof planPath !== "string" || !planPath) continue;
2755
2831
  try {
2756
2832
  const plan = await loadPlan2(planPath);
@@ -2763,7 +2839,8 @@ var todoTool = {
2763
2839
  }
2764
2840
  for (const taskId of completedTaskIds) {
2765
2841
  if (pendingTaskIds.has(taskId)) continue;
2766
- const taskPath = ctx.meta["task.path"];
2842
+ const meta = ctx.meta;
2843
+ const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
2767
2844
  if (typeof taskPath !== "string" || !taskPath) continue;
2768
2845
  try {
2769
2846
  const file = await loadTasks3(taskPath);
@@ -2879,7 +2956,16 @@ var planTool = {
2879
2956
  sessionPlanPath.lastIndexOf("/"),
2880
2957
  sessionPlanPath.lastIndexOf("\\")
2881
2958
  );
2882
- planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
2959
+ if (lastSep < 0) {
2960
+ return {
2961
+ ok: false,
2962
+ message: `Cannot derive the project-scoped plan path: session plan path "${sessionPlanPath}" has no directory component.`,
2963
+ plan: "",
2964
+ count: 0,
2965
+ open: 0
2966
+ };
2967
+ }
2968
+ planPath = sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json";
2883
2969
  }
2884
2970
  } else {
2885
2971
  planPath = sessionPlanPath;
@@ -3072,6 +3158,7 @@ var planTool = {
3072
3158
  open: 0
3073
3159
  };
3074
3160
  }
3161
+ ctx.meta["plan.path.resolved"] = planPath;
3075
3162
  await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
3076
3163
  if (todosToReplace) {
3077
3164
  await todoTool.execute({ todos: todosToReplace }, ctx, {
@@ -3104,6 +3191,7 @@ var planTool = {
3104
3191
  });
3105
3192
  return f;
3106
3193
  });
3194
+ ctx.meta["task.path.resolved"] = taskPath;
3107
3195
  return mkResult(
3108
3196
  plan,
3109
3197
  true,
@@ -11,8 +11,14 @@ export interface TrackedProcess {
11
11
  sessionId?: string | undefined;
12
12
  /** The raw ChildProcess handle. Never call .kill() directly on this —
13
13
  * use `kill()` below which handles process groups correctly on POSIX
14
- * and degrades gracefully on Windows. */
15
- child: ChildProcess;
14
+ * and degrades gracefully on Windows.
15
+ *
16
+ * `null` for entries mirrored from the persistent registry
17
+ * (process-registry-persistent.ts): those track processes owned by OTHER
18
+ * host instances, so there is no live handle in this process. All
19
+ * registry paths must tolerate a null child (kill by PID, liveness by
20
+ * `process.kill(pid, 0)` probe). */
21
+ child: ChildProcess | null;
16
22
  /** True only when this child was spawned as a POSIX process-group/session
17
23
  * leader (for example `spawn(..., { detached: true })`) and `pid` is the
18
24
  * actual `child.pid`. Negative-PID signaling is host-wide dangerous for
@@ -201,8 +201,11 @@ var SENSITIVE_FLAG_PATTERNS = [
201
201
  /--(?: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,
202
202
  // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
203
203
  // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
204
+ // The value must be token-like (>= 8 chars) so ordinary combined flags such
205
+ // as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
206
+ // redacted, not just the first.
204
207
  // NOTE: synced with @wrongstack/core observability/redact-command.ts.
205
- /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
208
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
206
209
  // -p|-password|-a (redis auth) short flags: attached + separated + =value.
207
210
  // Same token-start anchor; over-redaction is an accepted tradeoff for a
208
211
  // redaction function. Synced with core copy.
@@ -210,8 +213,9 @@ var SENSITIVE_FLAG_PATTERNS = [
210
213
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
211
214
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
212
215
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
213
- // when preceded by a flag name (e.g. --github-token=EyJ...).
214
- /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/
216
+ // when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
217
+ // every such flag in the command line is redacted, not just the first.
218
+ /--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
215
219
  ];
216
220
  function redactCommand(cmd) {
217
221
  let result = cmd;
@@ -300,11 +304,15 @@ var ProcessRegistryImpl = class {
300
304
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
301
305
  }
302
306
  _canSignalProcessGroup(p) {
303
- return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
307
+ return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
304
308
  }
305
309
  _killChildDirect(p, signal) {
306
310
  try {
307
- p.child.kill(signal);
311
+ if (p.child) {
312
+ p.child.kill(signal);
313
+ return;
314
+ }
315
+ if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
308
316
  } catch {
309
317
  }
310
318
  }
@@ -502,15 +510,15 @@ var ProcessRegistryImpl = class {
502
510
  this._pruneStale(pid);
503
511
  const p = this.processes.get(pid);
504
512
  if (!p) return false;
505
- if (p.killed) return true;
513
+ if (p.killed && opts.force !== true) return true;
506
514
  if (p.protected && opts.includeProtected !== true) return false;
507
515
  if (opts.preserveBackground && p.background) return false;
508
516
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
509
517
  const isWin = os.platform() === "win32";
510
518
  if (isWin) {
511
- const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
519
+ const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
512
520
  const directFallback = () => {
513
- if (p.child.exitCode === null) {
521
+ if (p.child && p.child.exitCode === null) {
514
522
  try {
515
523
  p.child.kill("SIGKILL");
516
524
  } catch {
@@ -522,10 +530,7 @@ var ProcessRegistryImpl = class {
522
530
  onSettled: directFallback
523
531
  })) {
524
532
  } else {
525
- try {
526
- p.child.kill(force ? "SIGKILL" : "SIGTERM");
527
- } catch {
528
- }
533
+ this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
529
534
  }
530
535
  p.killed = true;
531
536
  return true;
@@ -536,7 +541,7 @@ var ProcessRegistryImpl = class {
536
541
  } else {
537
542
  this._killPosix(p, "SIGTERM");
538
543
  const timer = setTimeout(() => {
539
- if (this.processes.has(pid) && !p.child.killed) {
544
+ if (this.processes.has(pid) && !p.child?.killed) {
540
545
  this._killPosix(p, "SIGKILL");
541
546
  }
542
547
  }, graceMs);
@@ -591,6 +596,16 @@ var ProcessRegistryImpl = class {
591
596
  * before reusing a PID, but we want to clean up before that becomes a risk.
592
597
  */
593
598
  _isStaleEntry(entry) {
599
+ if (entry.child === null) {
600
+ if (Date.now() - entry.startedAt <= 6e4) return false;
601
+ if (os.platform() === "win32") return false;
602
+ try {
603
+ process.kill(entry.pid, 0);
604
+ return false;
605
+ } catch (err) {
606
+ return err.code !== "EPERM";
607
+ }
608
+ }
594
609
  return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
595
610
  }
596
611
  /**
package/dist/ps-slash.js CHANGED
@@ -270,11 +270,15 @@ var ProcessRegistryImpl = class {
270
270
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
271
271
  }
272
272
  _canSignalProcessGroup(p) {
273
- return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
273
+ return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
274
274
  }
275
275
  _killChildDirect(p, signal) {
276
276
  try {
277
- p.child.kill(signal);
277
+ if (p.child) {
278
+ p.child.kill(signal);
279
+ return;
280
+ }
281
+ if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
278
282
  } catch {
279
283
  }
280
284
  }
@@ -472,15 +476,15 @@ var ProcessRegistryImpl = class {
472
476
  this._pruneStale(pid);
473
477
  const p = this.processes.get(pid);
474
478
  if (!p) return false;
475
- if (p.killed) return true;
479
+ if (p.killed && opts.force !== true) return true;
476
480
  if (p.protected && opts.includeProtected !== true) return false;
477
481
  if (opts.preserveBackground && p.background) return false;
478
482
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
479
483
  const isWin = os.platform() === "win32";
480
484
  if (isWin) {
481
- const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
485
+ const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
482
486
  const directFallback = () => {
483
- if (p.child.exitCode === null) {
487
+ if (p.child && p.child.exitCode === null) {
484
488
  try {
485
489
  p.child.kill("SIGKILL");
486
490
  } catch {
@@ -492,10 +496,7 @@ var ProcessRegistryImpl = class {
492
496
  onSettled: directFallback
493
497
  })) {
494
498
  } else {
495
- try {
496
- p.child.kill(force ? "SIGKILL" : "SIGTERM");
497
- } catch {
498
- }
499
+ this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
499
500
  }
500
501
  p.killed = true;
501
502
  return true;
@@ -506,7 +507,7 @@ var ProcessRegistryImpl = class {
506
507
  } else {
507
508
  this._killPosix(p, "SIGTERM");
508
509
  const timer = setTimeout(() => {
509
- if (this.processes.has(pid) && !p.child.killed) {
510
+ if (this.processes.has(pid) && !p.child?.killed) {
510
511
  this._killPosix(p, "SIGKILL");
511
512
  }
512
513
  }, graceMs);
@@ -561,6 +562,16 @@ var ProcessRegistryImpl = class {
561
562
  * before reusing a PID, but we want to clean up before that becomes a risk.
562
563
  */
563
564
  _isStaleEntry(entry) {
565
+ if (entry.child === null) {
566
+ if (Date.now() - entry.startedAt <= 6e4) return false;
567
+ if (os.platform() === "win32") return false;
568
+ try {
569
+ process.kill(entry.pid, 0);
570
+ return false;
571
+ } catch (err) {
572
+ return err.code !== "EPERM";
573
+ }
574
+ }
564
575
  return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
565
576
  }
566
577
  /**
@@ -854,7 +865,6 @@ var PersistentProcessRegistry = class {
854
865
  try {
855
866
  const data = await readRegistryFile(this.registryPath);
856
867
  data.instances.set(String(entry.pid), entry);
857
- const child = null;
858
868
  this.baseRegistry.register({
859
869
  pid: entry.pid,
860
870
  name: entry.name,
@@ -862,7 +872,7 @@ var PersistentProcessRegistry = class {
862
872
  startedAt: entry.startedAt,
863
873
  sessionId: entry.sessionId,
864
874
  protected: entry.protected,
865
- child
875
+ child: null
866
876
  });
867
877
  await writeRegistryFile(this.registryPath, data);
868
878
  } finally {
package/dist/read.js CHANGED
@@ -8253,7 +8253,7 @@ var MAX_BYTES = 5 * 1024 * 1024;
8253
8253
  var readTool = {
8254
8254
  name: "read",
8255
8255
  category: "Filesystem",
8256
- 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).",
8256
+ 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).",
8257
8257
  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.",
8258
8258
  selection: {
8259
8259
  doNotUseWhen: "you need to search many files for matching content.",
@@ -8274,11 +8274,13 @@ var readTool = {
8274
8274
  },
8275
8275
  offset: {
8276
8276
  type: "integer",
8277
+ minimum: 1,
8277
8278
  description: "1-based starting line number. Use together with `limit` for large files."
8278
8279
  },
8279
8280
  limit: {
8280
8281
  type: "integer",
8281
- description: "Maximum number of lines to return (default is 2000)."
8282
+ minimum: 0,
8283
+ description: "Maximum number of lines to return (default 2000). Values above 5000 are clamped to 5000 \u2014 page with `offset` for more."
8282
8284
  },
8283
8285
  mode: {
8284
8286
  type: "string",
@@ -8357,7 +8359,12 @@ var readTool = {
8357
8359
  }
8358
8360
  const buf = await fs13.readFile(absPath);
8359
8361
  if (isBinaryBuffer(buf)) {
8360
- throw new Error(`read: "${input.path}" appears to be binary`);
8362
+ throw new FsError({
8363
+ message: `read: "${input.path}" appears to be binary`,
8364
+ code: "FS_READ_FAILED",
8365
+ path: absPath,
8366
+ context: { reason: "binary" }
8367
+ });
8361
8368
  }
8362
8369
  const text = buf.toString("utf8");
8363
8370
  const contentHash = sha256hex(text);
package/dist/replace.d.ts CHANGED
@@ -16,7 +16,11 @@ interface ReplaceOutput {
16
16
  diff?: string | undefined;
17
17
  }[];
18
18
  dry_run: boolean;
19
+ /** Set when the combined diff payload was truncated to the output budget. */
20
+ note?: string | undefined;
19
21
  }
20
22
  export declare const replaceTool: Tool<ReplaceInput, ReplaceOutput>;
23
+ /** Test-only: forget the cached rg availability so mocks can vary per test. */
24
+ export declare function __resetRgDetectionForTests(): void;
21
25
  export {};
22
26
  //# sourceMappingURL=replace.d.ts.map
package/dist/replace.js CHANGED
@@ -165,6 +165,20 @@ function ensureInsideRoot(absPath, ctx) {
165
165
  function safeResolve(input, ctx) {
166
166
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
167
167
  }
168
+ function truncateDiffPayload(diff, maxBytes) {
169
+ const total = Buffer.byteLength(diff, "utf8");
170
+ if (total <= maxBytes) return { text: diff, truncated: false };
171
+ const MARKER_RESERVE = 96;
172
+ let head = takeHeadBytes(diff, Math.max(0, maxBytes - MARKER_RESERVE));
173
+ const nl = head.lastIndexOf("\n");
174
+ if (nl > 0) head = head.slice(0, nl);
175
+ const kept = Buffer.byteLength(head, "utf8");
176
+ return {
177
+ text: `${head}
178
+ \u2026[diff truncated: ${total - kept} of ${total} bytes omitted]`,
179
+ truncated: true
180
+ };
181
+ }
168
182
  function isBinaryBuffer(buf) {
169
183
  const len = Math.min(buf.length, 8192);
170
184
  for (let i = 0; i < len; i++) {
@@ -172,14 +186,27 @@ function isBinaryBuffer(buf) {
172
186
  }
173
187
  return false;
174
188
  }
189
+ function takeHeadBytes(s, maxBytes) {
190
+ if (maxBytes <= 0) return "";
191
+ if (Buffer.byteLength(s, "utf8") <= maxBytes) return s;
192
+ let lo = 0;
193
+ let hi = s.length;
194
+ while (lo < hi) {
195
+ const mid = Math.ceil((lo + hi) / 2);
196
+ if (Buffer.byteLength(s.slice(0, mid), "utf8") <= maxBytes) lo = mid;
197
+ else hi = mid - 1;
198
+ }
199
+ return s.slice(0, lo);
200
+ }
175
201
 
176
202
  // src/replace.ts
203
+ var MAX_DIFF_BYTES = 262144;
177
204
  var DEFAULT_IGNORE = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
178
205
  var replaceTool = {
179
206
  name: "replace",
180
207
  category: "Transform",
181
208
  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.",
182
- 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.",
209
+ 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.",
183
210
  permission: "confirm",
184
211
  // WS-046: gives permission decisions something to key on.
185
212
  // The file scope being rewritten, not the pattern: a trust rule should say
@@ -189,11 +216,15 @@ var replaceTool = {
189
216
  capabilities: ["fs.write"],
190
217
  icon: "edit",
191
218
  timeoutMs: 3e4,
219
+ maxOutputBytes: 262144,
192
220
  inputSchema: {
193
221
  type: "object",
194
222
  properties: {
195
223
  pattern: { type: "string", description: "Regex pattern to match" },
196
- replacement: { type: "string", description: "Replacement string" },
224
+ replacement: {
225
+ type: "string",
226
+ description: "Replacement string. Supports `$1`\u2013`$9` (capture groups), `$&` (whole match), and `$$` (literal dollar sign) \u2014 same semantics as JavaScript String.replace."
227
+ },
197
228
  files: {
198
229
  type: "string",
199
230
  description: "File(s) to target: single path, comma-separated list, or glob pattern"
@@ -242,6 +273,9 @@ var replaceTool = {
242
273
  const realRoot = await fs.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
243
274
  const results = [];
244
275
  let totalReplacements = 0;
276
+ let diffBytesUsed = 0;
277
+ let diffsOmitted = 0;
278
+ let diffsTruncated = 0;
245
279
  for (const absPath of fileList) {
246
280
  const lstat2 = await fs.lstat(absPath).catch((err) => {
247
281
  if (err.code === "ENOENT") return null;
@@ -277,7 +311,7 @@ var replaceTool = {
277
311
  let newContentLf = contentLf;
278
312
  for (let i = matches.length - 1; i >= 0; i--) {
279
313
  const m = expectDefined(matches[i]);
280
- newContentLf = newContentLf.slice(0, m.index) + input.replacement + newContentLf.slice(expectDefined(m.index) + m[0].length);
314
+ newContentLf = newContentLf.slice(0, m.index) + expandReplacement(input.replacement, m) + newContentLf.slice(expectDefined(m.index) + m[0].length);
281
315
  }
282
316
  re.lastIndex = 0;
283
317
  totalReplacements += count;
@@ -295,24 +329,76 @@ var replaceTool = {
295
329
  after: newContent
296
330
  });
297
331
  }
298
- const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
332
+ let diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
299
333
  fromFile: absPath,
300
334
  toFile: absPath
301
335
  }) : void 0;
336
+ if (diff !== void 0) {
337
+ const remaining = MAX_DIFF_BYTES - diffBytesUsed;
338
+ if (remaining <= 0) {
339
+ diff = void 0;
340
+ diffsOmitted++;
341
+ } else {
342
+ const capped = truncateDiffPayload(diff, remaining);
343
+ if (capped.truncated) diffsTruncated++;
344
+ diff = capped.text;
345
+ diffBytesUsed += Buffer.byteLength(diff, "utf8");
346
+ }
347
+ }
302
348
  results.push({
303
349
  path: absPath,
304
350
  replacements: matches.length,
305
351
  diff
306
352
  });
307
353
  }
354
+ const overBudget = diffsOmitted > 0 || diffsTruncated > 0;
308
355
  return {
309
356
  files_modified: results.length,
310
357
  total_replacements: totalReplacements,
311
358
  results,
312
- dry_run: dryRun
359
+ dry_run: dryRun,
360
+ 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
313
361
  };
314
362
  }
315
363
  };
364
+ function expandReplacement(template, match) {
365
+ if (!template.includes("$")) return template;
366
+ let out = "";
367
+ for (let i = 0; i < template.length; i++) {
368
+ const ch = template[i];
369
+ if (ch !== "$") {
370
+ out += ch;
371
+ continue;
372
+ }
373
+ const next = template[i + 1];
374
+ if (next === "$") {
375
+ out += "$";
376
+ i++;
377
+ } else if (next === "&") {
378
+ out += match[0];
379
+ i++;
380
+ } else if (next !== void 0 && next >= "1" && next <= "9") {
381
+ const idx = next.charCodeAt(0) - 48;
382
+ if (idx < match.length) {
383
+ out += match[idx] ?? "";
384
+ i++;
385
+ } else {
386
+ out += "$";
387
+ }
388
+ } else {
389
+ out += "$";
390
+ }
391
+ }
392
+ return out;
393
+ }
394
+ function passesExtraGlob(extraGlob, name, full) {
395
+ extraGlob.lastIndex = 0;
396
+ const nameMatch = extraGlob.test(name);
397
+ extraGlob.lastIndex = 0;
398
+ const fullMatch = extraGlob.test(full);
399
+ extraGlob.lastIndex = 0;
400
+ return nameMatch || fullMatch;
401
+ }
316
402
  async function resolveFiles(filesInput, ctx, extraGlob) {
317
403
  const base = ctx.cwd;
318
404
  const normalized = filesInput.trim();
@@ -323,6 +409,7 @@ async function resolveFiles(filesInput, ctx, extraGlob) {
323
409
  const resolved = [];
324
410
  for (const p of parts) {
325
411
  const absPath = safeResolve(p, ctx);
412
+ if (extraGlob && !passesExtraGlob(extraGlob, path2.basename(absPath), absPath)) continue;
326
413
  const stat2 = await fs.stat(absPath).catch(() => null);
327
414
  if (stat2?.isFile()) {
328
415
  resolved.push(absPath);
@@ -335,14 +422,22 @@ async function globFiles(pattern, base, extraGlob) {
335
422
  if (rgAvailable) {
336
423
  try {
337
424
  const { promise } = spawnRgFind(pattern, base);
338
- return await promise;
425
+ const files = await promise;
426
+ if (extraGlob) {
427
+ return files.filter((f) => passesExtraGlob(extraGlob, path2.basename(f), f));
428
+ }
429
+ return files;
339
430
  } catch {
340
431
  }
341
432
  }
342
433
  return await globNative(pattern, base, extraGlob);
343
434
  }
435
+ var rgAvailabilityCache;
436
+ function __resetRgDetectionForTests() {
437
+ rgAvailabilityCache = void 0;
438
+ }
344
439
  function checkRg() {
345
- return new Promise((resolve2) => {
440
+ rgAvailabilityCache ??= new Promise((resolve2) => {
346
441
  try {
347
442
  const p = spawn("rg", ["--version"], {
348
443
  env: buildChildEnv(),
@@ -355,6 +450,7 @@ function checkRg() {
355
450
  resolve2(false);
356
451
  }
357
452
  });
453
+ return rgAvailabilityCache;
358
454
  }
359
455
  function spawnRgFind(pattern, base) {
360
456
  const args = ["--files", "--glob", pattern, base];
@@ -421,6 +517,7 @@ async function globNative(pattern, base, extraGlob) {
421
517
  return results;
422
518
  }
423
519
  export {
520
+ __resetRgDetectionForTests,
424
521
  replaceTool
425
522
  };
426
523
  //# sourceMappingURL=replace.js.map
package/dist/search.d.ts CHANGED
@@ -15,6 +15,12 @@ interface SearchOutput {
15
15
  source: string;
16
16
  truncated: boolean;
17
17
  cached: boolean;
18
+ /**
19
+ * Present when the search engine could not be reached or returned an
20
+ * unusable response. `results` is empty in that case — the failure is never
21
+ * disguised as a synthetic result entry.
22
+ */
23
+ error?: string | undefined;
18
24
  }
19
25
  export declare const searchTool: Tool<SearchInput, SearchOutput>;
20
26
  /** Exposed for tests so they can reset the module-level cache between cases. */