@visorcraft/idlehands 1.1.17 → 1.2.1

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 (212) hide show
  1. package/dist/agent/formatting.js +30 -13
  2. package/dist/agent/formatting.js.map +1 -1
  3. package/dist/agent/review-artifact.js +12 -8
  4. package/dist/agent/review-artifact.js.map +1 -1
  5. package/dist/agent/tool-calls.js +57 -20
  6. package/dist/agent/tool-calls.js.map +1 -1
  7. package/dist/agent/tool-loop-detection.js +310 -0
  8. package/dist/agent/tool-loop-detection.js.map +1 -0
  9. package/dist/agent/tool-loop-guard.js +251 -0
  10. package/dist/agent/tool-loop-guard.js.map +1 -0
  11. package/dist/agent.js +460 -144
  12. package/dist/agent.js.map +1 -1
  13. package/dist/anton/controller.js +46 -30
  14. package/dist/anton/controller.js.map +1 -1
  15. package/dist/anton/lock.js +5 -1
  16. package/dist/anton/lock.js.map +1 -1
  17. package/dist/anton/parser.js +18 -19
  18. package/dist/anton/parser.js.map +1 -1
  19. package/dist/anton/prompt.js +42 -11
  20. package/dist/anton/prompt.js.map +1 -1
  21. package/dist/anton/reporter.js.map +1 -1
  22. package/dist/anton/session.js.map +1 -1
  23. package/dist/anton/verifier.js +3 -5
  24. package/dist/anton/verifier.js.map +1 -1
  25. package/dist/bench/compare.js +53 -20
  26. package/dist/bench/compare.js.map +1 -1
  27. package/dist/bench/openclaw.js +4 -4
  28. package/dist/bench/openclaw.js.map +1 -1
  29. package/dist/bench/report.js +11 -3
  30. package/dist/bench/report.js.map +1 -1
  31. package/dist/bench/runner.js +20 -14
  32. package/dist/bench/runner.js.map +1 -1
  33. package/dist/bot/commands.js +65 -31
  34. package/dist/bot/commands.js.map +1 -1
  35. package/dist/bot/confirm-discord.js +32 -9
  36. package/dist/bot/confirm-discord.js.map +1 -1
  37. package/dist/bot/confirm-telegram.js +26 -10
  38. package/dist/bot/confirm-telegram.js.map +1 -1
  39. package/dist/bot/dir-guard.js +18 -3
  40. package/dist/bot/dir-guard.js.map +1 -1
  41. package/dist/bot/discord-routing.js +28 -4
  42. package/dist/bot/discord-routing.js.map +1 -1
  43. package/dist/bot/discord-streaming.js +3 -3
  44. package/dist/bot/discord-streaming.js.map +1 -1
  45. package/dist/bot/discord.js +82 -37
  46. package/dist/bot/discord.js.map +1 -1
  47. package/dist/bot/escalation.js +124 -0
  48. package/dist/bot/escalation.js.map +1 -0
  49. package/dist/bot/format.js +2 -5
  50. package/dist/bot/format.js.map +1 -1
  51. package/dist/bot/session-manager.js +17 -6
  52. package/dist/bot/session-manager.js.map +1 -1
  53. package/dist/bot/telegram.js +88 -28
  54. package/dist/bot/telegram.js.map +1 -1
  55. package/dist/cli/agent-turn.js +10 -4
  56. package/dist/cli/agent-turn.js.map +1 -1
  57. package/dist/cli/args.js +51 -9
  58. package/dist/cli/args.js.map +1 -1
  59. package/dist/cli/bot.js +19 -9
  60. package/dist/cli/bot.js.map +1 -1
  61. package/dist/cli/build-repl-context.js +60 -26
  62. package/dist/cli/build-repl-context.js.map +1 -1
  63. package/dist/cli/command-registry.js.map +1 -1
  64. package/dist/cli/commands/anton.js +5 -3
  65. package/dist/cli/commands/anton.js.map +1 -1
  66. package/dist/cli/commands/editing.js +27 -12
  67. package/dist/cli/commands/editing.js.map +1 -1
  68. package/dist/cli/commands/model.js +16 -7
  69. package/dist/cli/commands/model.js.map +1 -1
  70. package/dist/cli/commands/project.js +52 -17
  71. package/dist/cli/commands/project.js.map +1 -1
  72. package/dist/cli/commands/runtime.js +1 -1
  73. package/dist/cli/commands/runtime.js.map +1 -1
  74. package/dist/cli/commands/secrets.js +279 -0
  75. package/dist/cli/commands/secrets.js.map +1 -0
  76. package/dist/cli/commands/session.js +49 -1
  77. package/dist/cli/commands/session.js.map +1 -1
  78. package/dist/cli/commands/tools.js +3 -1
  79. package/dist/cli/commands/tools.js.map +1 -1
  80. package/dist/cli/commands/trifecta.js +1 -1
  81. package/dist/cli/commands/trifecta.js.map +1 -1
  82. package/dist/cli/commands/tui.js.map +1 -1
  83. package/dist/cli/init.js +50 -16
  84. package/dist/cli/init.js.map +1 -1
  85. package/dist/cli/input.js +25 -7
  86. package/dist/cli/input.js.map +1 -1
  87. package/dist/cli/oneshot.js +31 -19
  88. package/dist/cli/oneshot.js.map +1 -1
  89. package/dist/cli/repl-dispatch.js +10 -6
  90. package/dist/cli/repl-dispatch.js.map +1 -1
  91. package/dist/cli/runtime-cmds.js +110 -46
  92. package/dist/cli/runtime-cmds.js.map +1 -1
  93. package/dist/cli/service.js +3 -3
  94. package/dist/cli/service.js.map +1 -1
  95. package/dist/cli/session-state.js +12 -5
  96. package/dist/cli/session-state.js.map +1 -1
  97. package/dist/cli/setup.js +86 -33
  98. package/dist/cli/setup.js.map +1 -1
  99. package/dist/cli/shell.js +4 -4
  100. package/dist/cli/shell.js.map +1 -1
  101. package/dist/cli/status.js +56 -12
  102. package/dist/cli/status.js.map +1 -1
  103. package/dist/client.js +40 -21
  104. package/dist/client.js.map +1 -1
  105. package/dist/commands.js +1 -1
  106. package/dist/commands.js.map +1 -1
  107. package/dist/config.js +171 -15
  108. package/dist/config.js.map +1 -1
  109. package/dist/confirm/auto.js.map +1 -1
  110. package/dist/confirm/headless.js +13 -2
  111. package/dist/confirm/headless.js.map +1 -1
  112. package/dist/confirm/terminal.js +1 -5
  113. package/dist/confirm/terminal.js.map +1 -1
  114. package/dist/context.js +9 -3
  115. package/dist/context.js.map +1 -1
  116. package/dist/git.js +56 -61
  117. package/dist/git.js.map +1 -1
  118. package/dist/harnesses.js +137 -37
  119. package/dist/harnesses.js.map +1 -1
  120. package/dist/history.js +12 -4
  121. package/dist/history.js.map +1 -1
  122. package/dist/hooks/index.js +2 -2
  123. package/dist/hooks/index.js.map +1 -1
  124. package/dist/hooks/loader.js +6 -5
  125. package/dist/hooks/loader.js.map +1 -1
  126. package/dist/hooks/manager.js.map +1 -1
  127. package/dist/hooks/plugins/example-console.js.map +1 -1
  128. package/dist/hooks/scaffold.js +8 -6
  129. package/dist/hooks/scaffold.js.map +1 -1
  130. package/dist/index.js +120 -66
  131. package/dist/index.js.map +1 -1
  132. package/dist/indexer.js +6 -18
  133. package/dist/indexer.js.map +1 -1
  134. package/dist/jsonrpc.js.map +1 -1
  135. package/dist/lens.js +38 -16
  136. package/dist/lens.js.map +1 -1
  137. package/dist/lsp.js +60 -24
  138. package/dist/lsp.js.map +1 -1
  139. package/dist/markdown.js +6 -6
  140. package/dist/markdown.js.map +1 -1
  141. package/dist/mcp.js +15 -6
  142. package/dist/mcp.js.map +1 -1
  143. package/dist/model-customization.js +7 -3
  144. package/dist/model-customization.js.map +1 -1
  145. package/dist/progress/message-edit-scheduler.js +15 -3
  146. package/dist/progress/message-edit-scheduler.js.map +1 -1
  147. package/dist/progress/progress-message-renderer.js.map +1 -1
  148. package/dist/progress/progress-presenter.js +3 -3
  149. package/dist/progress/progress-presenter.js.map +1 -1
  150. package/dist/progress/serialize-telegram.js.map +1 -1
  151. package/dist/progress/tool-summary.js +3 -1
  152. package/dist/progress/tool-summary.js.map +1 -1
  153. package/dist/progress/turn-progress.js +3 -1
  154. package/dist/progress/turn-progress.js.map +1 -1
  155. package/dist/recovery.js +11 -3
  156. package/dist/recovery.js.map +1 -1
  157. package/dist/replay.js +9 -3
  158. package/dist/replay.js.map +1 -1
  159. package/dist/replay_cli.js +5 -3
  160. package/dist/replay_cli.js.map +1 -1
  161. package/dist/runtime/executor.js +66 -20
  162. package/dist/runtime/executor.js.map +1 -1
  163. package/dist/runtime/health.js.map +1 -1
  164. package/dist/runtime/host-runner.js +103 -0
  165. package/dist/runtime/host-runner.js.map +1 -0
  166. package/dist/runtime/planner.js +3 -1
  167. package/dist/runtime/planner.js.map +1 -1
  168. package/dist/runtime/secrets.js +102 -0
  169. package/dist/runtime/secrets.js.map +1 -0
  170. package/dist/runtime/store.js +95 -19
  171. package/dist/runtime/store.js.map +1 -1
  172. package/dist/safety.js +38 -21
  173. package/dist/safety.js.map +1 -1
  174. package/dist/spinner.js +7 -8
  175. package/dist/spinner.js.map +1 -1
  176. package/dist/sys/context.js +3 -3
  177. package/dist/sys/context.js.map +1 -1
  178. package/dist/term.js +1 -1
  179. package/dist/term.js.map +1 -1
  180. package/dist/themes.js +11 -5
  181. package/dist/themes.js.map +1 -1
  182. package/dist/tools/tool-error.js +2 -5
  183. package/dist/tools/tool-error.js.map +1 -1
  184. package/dist/tools.js +69 -34
  185. package/dist/tools.js.map +1 -1
  186. package/dist/tui/branch-picker.js +9 -3
  187. package/dist/tui/branch-picker.js.map +1 -1
  188. package/dist/tui/command-handler.js +88 -36
  189. package/dist/tui/command-handler.js.map +1 -1
  190. package/dist/tui/confirm.js.map +1 -1
  191. package/dist/tui/controller.js +234 -117
  192. package/dist/tui/controller.js.map +1 -1
  193. package/dist/tui/event-bridge.js.map +1 -1
  194. package/dist/tui/keymap.js +93 -71
  195. package/dist/tui/keymap.js.map +1 -1
  196. package/dist/tui/layout.js +9 -1
  197. package/dist/tui/layout.js.map +1 -1
  198. package/dist/tui/render.js +17 -5
  199. package/dist/tui/render.js.map +1 -1
  200. package/dist/tui/screen.js.map +1 -1
  201. package/dist/tui/state.js +129 -63
  202. package/dist/tui/state.js.map +1 -1
  203. package/dist/tui/theme.js +12 -3
  204. package/dist/tui/theme.js.map +1 -1
  205. package/dist/upgrade.js +28 -15
  206. package/dist/upgrade.js.map +1 -1
  207. package/dist/utils.js +8 -5
  208. package/dist/utils.js.map +1 -1
  209. package/dist/vault.js +48 -12
  210. package/dist/vault.js.map +1 -1
  211. package/dist/vim.js.map +1 -1
  212. package/package.json +11 -2
package/dist/tools.js CHANGED
@@ -279,8 +279,10 @@ export async function undo_path(ctx, args) {
279
279
  const p = directPath ? resolvePath(ctx, directPath) : ctx.lastEditedPath;
280
280
  if (!p)
281
281
  throw new Error('undo: missing path');
282
+ const absCwd = path.resolve(ctx.cwd);
283
+ const redactedPath = redactPath(p, absCwd);
282
284
  if (!ctx.noConfirm && ctx.confirm) {
283
- const ok = await ctx.confirm(`Restore latest backup for:\n ${p}\nThis will overwrite the current file. Proceed? (y/N) `);
285
+ const ok = await ctx.confirm(`Restore latest backup for:\n ${redactedPath}\nThis will overwrite the current file. Proceed? (y/N) `);
284
286
  if (!ok)
285
287
  return 'undo: cancelled';
286
288
  }
@@ -288,11 +290,13 @@ export async function undo_path(ctx, args) {
288
290
  throw new Error('undo: confirmation required (run with --no-confirm/--yolo or in interactive mode)');
289
291
  }
290
292
  if (ctx.dryRun)
291
- return `dry-run: would restore latest backup for ${p}`;
293
+ return `dry-run: would restore latest backup for ${redactedPath}`;
292
294
  return await restoreLatestBackup(p, ctx);
293
295
  }
294
296
  export async function read_file(ctx, args) {
295
297
  const p = resolvePath(ctx, args?.path);
298
+ const absCwd = path.resolve(ctx.cwd);
299
+ const redactedPath = redactPath(p, absCwd);
296
300
  const offset = args?.offset != null ? Number(args.offset) : undefined;
297
301
  const rawLimit = args?.limit != null ? Number(args.limit) : undefined;
298
302
  const limit = Number.isFinite(rawLimit) && rawLimit > 0
@@ -317,7 +321,7 @@ export async function read_file(ctx, args) {
317
321
  try {
318
322
  const stat = await fs.stat(p);
319
323
  if (stat.isDirectory()) {
320
- return `read_file: "${p}" is a directory, not a file. Use list_dir to see its contents, or search_files to find specific code.`;
324
+ return `read_file: "${redactedPath}" is a directory, not a file. Use list_dir to see its contents, or search_files to find specific code.`;
321
325
  }
322
326
  }
323
327
  catch (e) {
@@ -419,6 +423,8 @@ export async function read_files(ctx, args) {
419
423
  }
420
424
  export async function write_file(ctx, args) {
421
425
  const p = resolvePath(ctx, args?.path);
426
+ const absCwd = path.resolve(ctx.cwd);
427
+ const redactedPath = redactPath(p, absCwd);
422
428
  // Content may arrive as a string (normal) or as a parsed JSON object
423
429
  // (when llama-server's XML parser auto-parses JSON content values).
424
430
  const raw = args?.content;
@@ -444,7 +450,7 @@ export async function write_file(ctx, args) {
444
450
  }
445
451
  if (pathVerdict.tier === 'cautious' && !ctx.noConfirm) {
446
452
  if (ctx.confirm) {
447
- const ok = await ctx.confirm(pathVerdict.prompt || `Write to ${p}?`, { tool: 'write_file', args: { path: p } });
453
+ const ok = await ctx.confirm(pathVerdict.prompt || `Write to ${redactedPath}?`, { tool: 'write_file', args: { path: p } });
448
454
  if (!ok)
449
455
  throw new Error(`write_file: cancelled by user (${pathVerdict.reason})`);
450
456
  }
@@ -454,7 +460,7 @@ export async function write_file(ctx, args) {
454
460
  }
455
461
  const existingStat = await fs.stat(p).catch(() => null);
456
462
  if (existingStat?.isFile() && existingStat.size > 0 && !overwrite) {
457
- throw new Error(`write_file: refusing to overwrite existing non-empty file ${p} without explicit overwrite=true (or force=true). ` +
463
+ throw new Error(`write_file: refusing to overwrite existing non-empty file ${redactedPath} without explicit overwrite=true (or force=true). ` +
458
464
  `Use edit_range/apply_patch for surgical edits, or set overwrite=true for intentional full-file replacement.`);
459
465
  }
460
466
  if (ctx.dryRun) {
@@ -471,10 +477,12 @@ export async function write_file(ctx, args) {
471
477
  ctx.onMutation?.(p);
472
478
  const afterBuf = Buffer.from(content, 'utf8');
473
479
  const replayNote = await checkpointReplay(ctx, { op: 'write_file', filePath: p, before: beforeBuf, after: afterBuf });
474
- return `wrote ${p} (${Buffer.byteLength(content, 'utf8')} bytes)${replayNote}${cwdWarning}`;
480
+ return `wrote ${redactedPath} (${Buffer.byteLength(content, 'utf8')} bytes)${replayNote}${cwdWarning}`;
475
481
  }
476
482
  export async function insert_file(ctx, args) {
477
483
  const p = resolvePath(ctx, args?.path);
484
+ const absCwd = path.resolve(ctx.cwd);
485
+ const redactedPath = redactPath(p, absCwd);
478
486
  const line = Number(args?.line);
479
487
  const rawText = args?.text;
480
488
  const text = typeof rawText === 'string' ? rawText
@@ -493,7 +501,7 @@ export async function insert_file(ctx, args) {
493
501
  }
494
502
  if (pathVerdict.tier === 'cautious' && !ctx.noConfirm) {
495
503
  if (ctx.confirm) {
496
- const ok = await ctx.confirm(pathVerdict.prompt || `Insert into ${p}?`, { tool: 'insert_file', args: { path: p } });
504
+ const ok = await ctx.confirm(pathVerdict.prompt || `Insert into ${redactedPath}?`, { tool: 'insert_file', args: { path: p } });
497
505
  if (!ok)
498
506
  throw new Error(`insert_file: cancelled by user (${pathVerdict.reason})`);
499
507
  }
@@ -502,7 +510,7 @@ export async function insert_file(ctx, args) {
502
510
  }
503
511
  }
504
512
  if (ctx.dryRun)
505
- return `dry-run: would insert into ${p} at line=${line} (${Buffer.byteLength(text, 'utf8')} bytes)`;
513
+ return `dry-run: would insert into ${redactedPath} at line=${line} (${Buffer.byteLength(text, 'utf8')} bytes)`;
506
514
  // Phase 9d: snapshot /etc/ files before editing
507
515
  if (ctx.mode === 'sys' && ctx.vault) {
508
516
  await snapshotBeforeEdit(ctx.vault, p).catch(() => { });
@@ -523,7 +531,7 @@ export async function insert_file(ctx, args) {
523
531
  after: Buffer.from(out, 'utf8')
524
532
  });
525
533
  const cwdWarning = checkCwdWarning('insert_file', p, ctx);
526
- return `inserted into ${p} at 0${replayNote}${cwdWarning}`;
534
+ return `inserted into ${redactedPath} at 0${replayNote}${cwdWarning}`;
527
535
  }
528
536
  const lines = beforeText.split(/\r?\n/);
529
537
  let idx;
@@ -552,10 +560,12 @@ export async function insert_file(ctx, args) {
552
560
  after: Buffer.from(out, 'utf8')
553
561
  });
554
562
  const cwdWarning = checkCwdWarning('insert_file', p, ctx);
555
- return `inserted into ${p} at ${idx}${replayNote}${cwdWarning}`;
563
+ return `inserted into ${redactedPath} at ${idx}${replayNote}${cwdWarning}`;
556
564
  }
557
565
  export async function edit_file(ctx, args) {
558
566
  const p = resolvePath(ctx, args?.path);
567
+ const absCwd = path.resolve(ctx.cwd);
568
+ const redactedPath = redactPath(p, absCwd);
559
569
  const rawOld = args?.old_text;
560
570
  const oldText = typeof rawOld === 'string' ? rawOld
561
571
  : (rawOld != null && typeof rawOld === 'object' ? JSON.stringify(rawOld, null, 2) : undefined);
@@ -577,7 +587,7 @@ export async function edit_file(ctx, args) {
577
587
  }
578
588
  if (pathVerdict.tier === 'cautious' && !ctx.noConfirm) {
579
589
  if (ctx.confirm) {
580
- const ok = await ctx.confirm(pathVerdict.prompt || `Edit ${p}?`, { tool: 'edit_file', args: { path: p, old_text: oldText, new_text: newText } });
590
+ const ok = await ctx.confirm(pathVerdict.prompt || `Edit ${redactedPath}?`, { tool: 'edit_file', args: { path: p, old_text: oldText, new_text: newText } });
581
591
  if (!ok)
582
592
  throw new Error(`edit_file: cancelled by user (${pathVerdict.reason})`);
583
593
  }
@@ -590,7 +600,7 @@ export async function edit_file(ctx, args) {
590
600
  await snapshotBeforeEdit(ctx.vault, p).catch(() => { });
591
601
  }
592
602
  const cur = await fs.readFile(p, 'utf8').catch((e) => {
593
- throw new Error(`edit_file: cannot read ${p}: ${e?.message ?? String(e)}`);
603
+ throw new Error(`edit_file: cannot read ${redactedPath}: ${e?.message ?? String(e)}`);
594
604
  });
595
605
  const idx = cur.indexOf(oldText);
596
606
  if (idx === -1) {
@@ -626,11 +636,11 @@ export async function edit_file(ctx, args) {
626
636
  else {
627
637
  hint = `\nFile head (first 400 chars):\n${cur.slice(0, 400)}`;
628
638
  }
629
- throw new Error(`edit_file: old_text not found in ${p}. Re-read the file and retry with exact text.${hint}`);
639
+ throw new Error(`edit_file: old_text not found in ${redactedPath}. Re-read the file and retry with exact text.${hint}`);
630
640
  }
631
641
  const next = replaceAll ? cur.split(oldText).join(newText) : cur.slice(0, idx) + newText + cur.slice(idx + oldText.length);
632
642
  if (ctx.dryRun)
633
- return `dry-run: would edit ${p} (replace_all=${replaceAll})`;
643
+ return `dry-run: would edit ${redactedPath} (replace_all=${replaceAll})`;
634
644
  await backupFile(p, ctx);
635
645
  await atomicWrite(p, next);
636
646
  ctx.onMutation?.(p);
@@ -641,7 +651,7 @@ export async function edit_file(ctx, args) {
641
651
  after: Buffer.from(next, 'utf8')
642
652
  });
643
653
  const cwdWarning = checkCwdWarning('edit_file', p, ctx);
644
- return `edited ${p} (replace_all=${replaceAll})${replayNote}${cwdWarning}`;
654
+ return `edited ${redactedPath} (replace_all=${replaceAll})${replayNote}${cwdWarning}`;
645
655
  }
646
656
  function normalizePatchPath(p) {
647
657
  let s = String(p ?? '').trim();
@@ -891,7 +901,8 @@ export async function apply_patch(ctx, args) {
891
901
  if (chk.rc !== 0)
892
902
  throw new Error(`apply_patch: patch --dry-run failed:\n${chk.err || chk.out}`);
893
903
  }
894
- return `dry-run: patch would apply cleanly (${touched.paths.length} files): ${touched.paths.join(', ')}`;
904
+ const redactedPaths = touched.paths.map((rel) => redactPath(resolvePath(ctx, rel), path.resolve(ctx.cwd)));
905
+ return `dry-run: patch would apply cleanly (${touched.paths.length} files): ${redactedPaths.join(', ')}`;
895
906
  }
896
907
  // Snapshot + backup before applying
897
908
  const beforeMap = new Map();
@@ -937,7 +948,8 @@ export async function apply_patch(ctx, args) {
937
948
  replayNotes += replayNote;
938
949
  cwdWarnings += checkCwdWarning('apply_patch', abs, ctx);
939
950
  }
940
- return `applied patch (${touched.paths.length} files): ${touched.paths.join(', ')}${replayNotes}${cwdWarnings}`;
951
+ const redactedPaths = touched.paths.map((rel) => redactPath(resolvePath(ctx, rel), path.resolve(ctx.cwd)));
952
+ return `applied patch (${touched.paths.length} files): ${redactedPaths.join(', ')}${replayNotes}${cwdWarnings}`;
941
953
  }
942
954
  export async function list_dir(ctx, args) {
943
955
  const p = resolvePath(ctx, args?.path ?? '.');
@@ -945,6 +957,7 @@ export async function list_dir(ctx, args) {
945
957
  const maxEntries = Math.min(args?.max_entries ? Number(args.max_entries) : 200, 500);
946
958
  if (!p)
947
959
  throw new Error('list_dir: missing path');
960
+ const absCwd = path.resolve(ctx.cwd);
948
961
  const lines = [];
949
962
  let count = 0;
950
963
  async function walk(dir, depth) {
@@ -959,7 +972,7 @@ export async function list_dir(ctx, args) {
959
972
  const full = path.join(dir, ent.name);
960
973
  const st = await fs.lstat(full).catch(() => null);
961
974
  const kind = ent.isDirectory() ? 'dir' : ent.isSymbolicLink() ? 'link' : 'file';
962
- lines.push(`${kind}\t${st?.size ?? 0}\t${full}`);
975
+ lines.push(`${kind}\t${st?.size ?? 0}\t${redactPath(full, absCwd)}`);
963
976
  count++;
964
977
  if (recursive && ent.isDirectory() && depth < 3) {
965
978
  await walk(full, depth + 1);
@@ -970,7 +983,7 @@ export async function list_dir(ctx, args) {
970
983
  if (count >= maxEntries)
971
984
  lines.push(`[truncated after ${maxEntries} entries]`);
972
985
  if (!lines.length)
973
- return `[empty directory: ${p}]`;
986
+ return `[empty directory: ${redactPath(p, absCwd)}]`;
974
987
  return lines.join('\n');
975
988
  }
976
989
  export async function search_files(ctx, args) {
@@ -982,6 +995,7 @@ export async function search_files(ctx, args) {
982
995
  throw new Error('search_files: missing path');
983
996
  if (!pattern)
984
997
  throw new Error('search_files: missing pattern');
998
+ const absCwd = path.resolve(ctx.cwd);
985
999
  // Prefer rg if available (fast, bounded output)
986
1000
  if (await hasRg()) {
987
1001
  const cmd = ['rg', '-n', '--no-heading', '--color', 'never', pattern, root];
@@ -992,7 +1006,7 @@ export async function search_files(ctx, args) {
992
1006
  const parsed = JSON.parse(rawJson);
993
1007
  // rg exits 1 when no matches found (not an error), 2+ for real errors.
994
1008
  if (parsed.rc === 1 && !parsed.out?.trim()) {
995
- return `No matches for pattern "${pattern}" in ${root}. STOP — do NOT read files individually to search. Try a broader regex pattern, different keywords, or use exec: grep -rn "keyword" ${root}`;
1009
+ return `No matches for pattern \"${pattern}\" in ${root}. STOP — do NOT read files individually to search. Try a broader regex pattern, different keywords, or use exec: grep -rn \"keyword\" ${root}`;
996
1010
  }
997
1011
  if (parsed.rc >= 2) {
998
1012
  // Real rg error — fall through to regex fallback below
@@ -1003,7 +1017,16 @@ export async function search_files(ctx, args) {
1003
1017
  const lines = rgOutput.split(/\r?\n/).filter(Boolean).slice(0, maxResults);
1004
1018
  if (lines.length >= maxResults)
1005
1019
  lines.push(`[truncated after ${maxResults} results]`);
1006
- return lines.join('\n');
1020
+ // Redact paths in rg output
1021
+ const redactedLines = lines.map(line => {
1022
+ const colonIdx = line.indexOf(':');
1023
+ if (colonIdx === -1)
1024
+ return line;
1025
+ const filePath = line.substring(0, colonIdx);
1026
+ const rest = line.substring(colonIdx + 1);
1027
+ return redactPath(filePath, absCwd) + ':' + rest;
1028
+ });
1029
+ return redactedLines.join('\n');
1007
1030
  }
1008
1031
  }
1009
1032
  }
@@ -1017,7 +1040,7 @@ export async function search_files(ctx, args) {
1017
1040
  re = new RegExp(pattern);
1018
1041
  }
1019
1042
  catch (e) {
1020
- throw new ToolError('invalid_args', `search_files: invalid regex pattern: ${e?.message ?? String(e)}`, false, 'Escape regex metacharacters (\\, [, ], (, ), +, *, ?). If you intended literal text, use an escaped/literal pattern.');
1043
+ throw new ToolError('invalid_args', `search_files: invalid regex pattern: ${e?.message ?? String(e)}`, false, 'Escape regex metacharacters (\\\\, [, ], (, ), +, *, ?). If you intended literal text, use an escaped/literal pattern.');
1021
1044
  }
1022
1045
  const out = [];
1023
1046
  async function walk(dir, depth) {
@@ -1056,7 +1079,7 @@ export async function search_files(ctx, args) {
1056
1079
  const lines = buf.split(/\r?\n/);
1057
1080
  for (let i = 0; i < lines.length; i++) {
1058
1081
  if (re.test(lines[i])) {
1059
- out.push(`${full}:${i + 1}:${lines[i]}`);
1082
+ out.push(`${redactPath(full, absCwd)}:${i + 1}:${lines[i]}`);
1060
1083
  if (out.length >= maxResults)
1061
1084
  return;
1062
1085
  }
@@ -1066,10 +1089,9 @@ export async function search_files(ctx, args) {
1066
1089
  await walk(root, 0);
1067
1090
  if (out.length >= maxResults)
1068
1091
  out.push(`[truncated after ${maxResults} results]`);
1069
- const result = out.join('\n');
1070
- if (!result)
1071
- return `No matches for pattern "${pattern}" in ${root}. STOP — do NOT read files individually to search. Try a broader regex pattern, different keywords, or use exec: grep -rn "keyword" ${root}`;
1072
- return result;
1092
+ if (!out.length)
1093
+ return `No matches for pattern \"${pattern}\" in ${redactPath(root, absCwd)}.`;
1094
+ return out.join('\n');
1073
1095
  }
1074
1096
  function stripSimpleQuotedSegments(s) {
1075
1097
  // Best-effort quote stripping for lightweight shell pattern checks.
@@ -1404,7 +1426,7 @@ export async function exec(ctx, args) {
1404
1426
  }
1405
1427
  async function execWithPty(args) {
1406
1428
  const { pty, command, cwd, timeout, maxBytes, captureLimit, signal, execCwdWarning } = args;
1407
- const proc = pty.spawn(BASH_PATH, ['-lc', command], {
1429
+ const proc = pty.spawn(BASH_PATH, ['-c', command], {
1408
1430
  name: 'xterm-color',
1409
1431
  cwd,
1410
1432
  cols: 120,
@@ -1526,11 +1548,6 @@ export async function vault_search(ctx, args) {
1526
1548
  export async function sys_context(ctx, args) {
1527
1549
  return sysContextTool(ctx, args);
1528
1550
  }
1529
- function resolvePath(ctx, p) {
1530
- if (typeof p !== 'string' || !p.trim())
1531
- throw new Error('missing path');
1532
- return path.resolve(ctx.cwd, p);
1533
- }
1534
1551
  /**
1535
1552
  * Check if a target path is within a directory.
1536
1553
  * Handles the classic root directory edge case: when dir is `/`, every absolute path is valid.
@@ -1540,6 +1557,24 @@ function isWithinDir(target, dir) {
1540
1557
  return target.startsWith('/');
1541
1558
  return target === dir || target.startsWith(dir + path.sep);
1542
1559
  }
1560
+ function resolvePath(ctx, p) {
1561
+ if (typeof p !== 'string' || !p.trim())
1562
+ throw new Error('missing path');
1563
+ return path.resolve(ctx.cwd, p);
1564
+ }
1565
+ /**
1566
+ * Redact a path for safe output.
1567
+ * - Paths within cwd are shown as relative paths
1568
+ * - Paths outside cwd are redacted as [outside-cwd]/basename
1569
+ */
1570
+ function redactPath(filePath, absCwd) {
1571
+ const resolved = path.resolve(filePath);
1572
+ if (isWithinDir(resolved, absCwd)) {
1573
+ return path.relative(absCwd, resolved);
1574
+ }
1575
+ const basename = path.basename(resolved);
1576
+ return `[outside-cwd]/${basename}`;
1577
+ }
1543
1578
  /**
1544
1579
  * Check if a resolved path is outside the working directory.
1545
1580
  * Returns a model-visible warning string if so, empty string otherwise.
@@ -1600,7 +1635,7 @@ async function hasRg() {
1600
1635
  catch {
1601
1636
  // try PATH
1602
1637
  return await new Promise((resolve) => {
1603
- const c = spawn(BASH_PATH, ['-lc', 'command -v rg >/dev/null 2>&1'], { stdio: 'ignore' });
1638
+ const c = spawn(BASH_PATH, ['-c', 'command -v rg >/dev/null 2>&1'], { stdio: 'ignore' });
1604
1639
  c.on('error', () => resolve(false));
1605
1640
  c.on('close', (code) => resolve(code === 0));
1606
1641
  });