@nexrall/code-core 1.4.71 → 1.4.73

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 (126) hide show
  1. package/dist/agent/agentRegistry.d.ts.map +1 -1
  2. package/dist/agent/agentRegistry.js +28 -10
  3. package/dist/agent/agentTypes.d.ts.map +1 -1
  4. package/dist/agent/agentTypes.js +159 -48
  5. package/dist/agent/askOnce.d.ts.map +1 -1
  6. package/dist/agent/askOnce.js +14 -5
  7. package/dist/agent/backgroundAgents.d.ts.map +1 -1
  8. package/dist/agent/backgroundAgents.js +22 -8
  9. package/dist/agent/claimEvidence.d.ts.map +1 -1
  10. package/dist/agent/claimEvidence.js +2 -1
  11. package/dist/agent/compaction.d.ts.map +1 -1
  12. package/dist/agent/compaction.js +31 -10
  13. package/dist/agent/crossProcessLock.d.ts.map +1 -1
  14. package/dist/agent/crossProcessLock.js +14 -6
  15. package/dist/agent/editCompleteness.d.ts.map +1 -1
  16. package/dist/agent/fileLocks.d.ts.map +1 -1
  17. package/dist/agent/fileLocks.js +14 -5
  18. package/dist/agent/flaky.d.ts.map +1 -1
  19. package/dist/agent/hooks.d.ts.map +1 -1
  20. package/dist/agent/hooks.js +284 -75
  21. package/dist/agent/iterationPolicy.d.ts.map +1 -1
  22. package/dist/agent/iterationPolicy.js +26 -23
  23. package/dist/agent/lifecycleHost.d.ts.map +1 -1
  24. package/dist/agent/lifecycleHost.js +35 -11
  25. package/dist/agent/loop.d.ts +22 -4
  26. package/dist/agent/loop.d.ts.map +1 -1
  27. package/dist/agent/loop.js +238 -51
  28. package/dist/agent/memory.d.ts.map +1 -1
  29. package/dist/agent/memory.js +13 -7
  30. package/dist/agent/modelCatalogue.d.ts.map +1 -1
  31. package/dist/agent/modelCatalogue.js +4 -1
  32. package/dist/agent/nestedInstructions.d.ts +122 -0
  33. package/dist/agent/nestedInstructions.d.ts.map +1 -0
  34. package/dist/agent/nestedInstructions.js +696 -0
  35. package/dist/agent/peerRegistry.d.ts.map +1 -1
  36. package/dist/agent/peerRegistry.js +44 -6
  37. package/dist/agent/peerTransport.d.ts.map +1 -1
  38. package/dist/agent/peerTransport.js +18 -6
  39. package/dist/agent/planMode.d.ts.map +1 -1
  40. package/dist/agent/planMode.js +194 -41
  41. package/dist/agent/readDedupe.d.ts.map +1 -1
  42. package/dist/agent/readDedupe.js +3 -1
  43. package/dist/agent/securityLint.d.ts.map +1 -1
  44. package/dist/agent/sharedTasks.d.ts.map +1 -1
  45. package/dist/agent/sharedTasks.js +20 -5
  46. package/dist/agent/skills.d.ts +79 -0
  47. package/dist/agent/skills.d.ts.map +1 -1
  48. package/dist/agent/skills.js +403 -14
  49. package/dist/agent/subAgentBudget.d.ts.map +1 -1
  50. package/dist/agent/subTask.d.ts.map +1 -1
  51. package/dist/agent/subTask.js +87 -37
  52. package/dist/agent/subTaskSupport.d.ts.map +1 -1
  53. package/dist/agent/subTaskSupport.js +43 -14
  54. package/dist/agent/testIntegrity.d.ts.map +1 -1
  55. package/dist/agent/testIntegrity.js +4 -2
  56. package/dist/agent/toolDescriptions.d.ts.map +1 -1
  57. package/dist/agent/toolPrefetch.d.ts.map +1 -1
  58. package/dist/agent/toolPrefetch.js +15 -11
  59. package/dist/agent/trust.d.ts.map +1 -1
  60. package/dist/agent/trust.js +6 -2
  61. package/dist/agent/worktree.d.ts.map +1 -1
  62. package/dist/agent/worktree.js +70 -16
  63. package/dist/agent/worktreeEnforcement.d.ts.map +1 -1
  64. package/dist/agent/worktreeEnforcement.js +9 -4
  65. package/dist/api/client.d.ts +6 -0
  66. package/dist/api/client.d.ts.map +1 -1
  67. package/dist/api/client.js +64 -31
  68. package/dist/auth/index.d.ts.map +1 -1
  69. package/dist/auth/index.js +3 -1
  70. package/dist/checkpoint/manager.d.ts.map +1 -1
  71. package/dist/checkpoint/manager.js +22 -7
  72. package/dist/commands/loader.d.ts.map +1 -1
  73. package/dist/commands/loader.js +6 -1
  74. package/dist/index.d.ts +2 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +2 -0
  77. package/dist/mcp/client.d.ts +42 -0
  78. package/dist/mcp/client.d.ts.map +1 -1
  79. package/dist/mcp/client.js +89 -24
  80. package/dist/mcp/httpClient.d.ts +24 -1
  81. package/dist/mcp/httpClient.d.ts.map +1 -1
  82. package/dist/mcp/httpClient.js +80 -14
  83. package/dist/mcp/instructions.d.ts +11 -0
  84. package/dist/mcp/instructions.d.ts.map +1 -0
  85. package/dist/mcp/instructions.js +42 -0
  86. package/dist/mcp/manager.d.ts +35 -0
  87. package/dist/mcp/manager.d.ts.map +1 -1
  88. package/dist/mcp/manager.js +135 -24
  89. package/dist/mcp/server.d.ts.map +1 -1
  90. package/dist/mcp/server.js +48 -14
  91. package/dist/mcp/sseClient.d.ts +9 -1
  92. package/dist/mcp/sseClient.d.ts.map +1 -1
  93. package/dist/mcp/sseClient.js +43 -10
  94. package/dist/mcp/stats.d.ts.map +1 -1
  95. package/dist/permissions/bashClassify.d.ts.map +1 -1
  96. package/dist/permissions/bashClassify.js +25 -3
  97. package/dist/permissions/destructive.d.ts.map +1 -1
  98. package/dist/permissions/destructive.js +7 -2
  99. package/dist/permissions/destructiveTokens.d.ts.map +1 -1
  100. package/dist/permissions/destructiveTokens.js +104 -24
  101. package/dist/permissions/modePolicy.d.ts.map +1 -1
  102. package/dist/permissions/modePolicy.js +40 -17
  103. package/dist/permissions/rules.d.ts.map +1 -1
  104. package/dist/permissions/rules.js +7 -5
  105. package/dist/plugins/data.d.ts.map +1 -1
  106. package/dist/plugins/data.js +6 -2
  107. package/dist/plugins/index.d.ts.map +1 -1
  108. package/dist/plugins/index.js +18 -7
  109. package/dist/plugins/installer.d.ts.map +1 -1
  110. package/dist/plugins/installer.js +17 -10
  111. package/dist/plugins/sources.d.ts.map +1 -1
  112. package/dist/plugins/sources.js +9 -3
  113. package/dist/tools/executor.d.ts.map +1 -1
  114. package/dist/tools/executor.js +454 -168
  115. package/dist/tools/sandbox.d.ts.map +1 -1
  116. package/dist/tools/sandbox.js +5 -1
  117. package/dist/tools/symbols.d.ts.map +1 -1
  118. package/dist/tools/symbols.js +30 -8
  119. package/dist/tools/tsLangService.d.ts.map +1 -1
  120. package/dist/tools/tsLangService.js +12 -4
  121. package/dist/types.d.ts +17 -0
  122. package/dist/types.d.ts.map +1 -1
  123. package/dist/util/frontmatter.d.ts.map +1 -1
  124. package/dist/util/miniYaml.d.ts.map +1 -1
  125. package/dist/util/miniYaml.js +3 -1
  126. package/package.json +1 -1
@@ -206,9 +206,7 @@ function isCommandBlocked(command) {
206
206
  // even when the agent runs in a sub-context with a different cwd.
207
207
  function resolvePath(inputPath, workDir) {
208
208
  const base = workDir ?? process.cwd();
209
- const resolved = path.isAbsolute(inputPath)
210
- ? inputPath
211
- : path.resolve(base, inputPath);
209
+ const resolved = path.isAbsolute(inputPath) ? inputPath : path.resolve(base, inputPath);
212
210
  // Path traversal guard: reject paths that escape the workspace root.
213
211
  // Absolute paths are allowed as-is (agent may legitimately reference /usr/bin etc.);
214
212
  // relative paths must stay within the resolved base.
@@ -253,7 +251,7 @@ function isBinaryFile(filePath) {
253
251
  nonText++;
254
252
  }
255
253
  // >30% non-text ASCII control chars → binary (was 10%, too aggressive)
256
- return nonText / bytesRead > 0.30;
254
+ return nonText / bytesRead > 0.3;
257
255
  }
258
256
  catch {
259
257
  return false;
@@ -372,10 +370,12 @@ async function readFileWindowed(resolved, offset, requestedLimit) {
372
370
  sawEof = true;
373
371
  finish();
374
372
  });
375
- stream.on('error', (err) => { if (!stopped) {
376
- stopped = true;
377
- resolve({ error: err.message });
378
- } });
373
+ stream.on('error', (err) => {
374
+ if (!stopped) {
375
+ stopped = true;
376
+ resolve({ error: err.message });
377
+ }
378
+ });
379
379
  });
380
380
  }
381
381
  async function readFile(input, workDir) {
@@ -391,7 +391,9 @@ async function readFile(input, workDir) {
391
391
  // (isBinaryFile only samples the first 8KB, so this is cheap regardless of file size.)
392
392
  if (isBinaryFile(resolved)) {
393
393
  const ext = path.extname(resolved).toLowerCase();
394
- return { error: `Cannot read binary file: ${resolved} (${ext || 'no extension'}). Use a text-based tool or convert it first.` };
394
+ return {
395
+ error: `Cannot read binary file: ${resolved} (${ext || 'no extension'}). Use a text-based tool or convert it first.`,
396
+ };
395
397
  }
396
398
  // No byte-size gate — a file of any size can be read; it's always streamed
397
399
  // line-by-line so memory is bounded regardless of file size (matches Claude
@@ -418,7 +420,9 @@ async function writeFile(input, workDir) {
418
420
  const isNew = !fs.existsSync(resolved);
419
421
  // Guard against accidentally overwriting an existing file with empty content
420
422
  if (!isNew && content === '') {
421
- return { error: `Refusing to overwrite ${resolved} with empty content. Provide the full file content or use edit_file for targeted changes.` };
423
+ return {
424
+ error: `Refusing to overwrite ${resolved} with empty content. Provide the full file content or use edit_file for targeted changes.`,
425
+ };
422
426
  }
423
427
  // Edit-completeness guard: refuse writes that elide code with a placeholder
424
428
  // ("// ... rest of the code unchanged") — a silent, irreversible data loss —
@@ -428,9 +432,13 @@ async function writeFile(input, workDir) {
428
432
  const problem = (0, editCompleteness_1.checkEditCompleteness)(content, !isNew);
429
433
  if (problem) {
430
434
  if (problem.kind === 'elision-placeholder') {
431
- return { error: `Refusing to write ${resolved}: ${problem.reason}.\nOffending line: ${problem.sample}\nwrite_file must contain the COMPLETE file — never a "rest unchanged" placeholder. Send the full content, or use edit_file/multi_edit for a targeted change so surrounding code is preserved.` };
435
+ return {
436
+ error: `Refusing to write ${resolved}: ${problem.reason}.\nOffending line: ${problem.sample}\nwrite_file must contain the COMPLETE file — never a "rest unchanged" placeholder. Send the full content, or use edit_file/multi_edit for a targeted change so surrounding code is preserved.`,
437
+ };
432
438
  }
433
- return { error: `Refusing to write ${resolved}: ${problem.reason}.\nOffending line: ${problem.sample}\nResolve the conflict (remove <<<<<<< / ======= / >>>>>>> markers and keep the intended code) before writing.` };
439
+ return {
440
+ error: `Refusing to write ${resolved}: ${problem.reason}.\nOffending line: ${problem.sample}\nResolve the conflict (remove <<<<<<< / ======= / >>>>>>> markers and keep the intended code) before writing.`,
441
+ };
434
442
  }
435
443
  }
436
444
  // Preserve file permissions (mode bits) when overwriting an existing file.
@@ -441,11 +449,15 @@ async function writeFile(input, workDir) {
441
449
  try {
442
450
  existingMode = fs.statSync(resolved).mode;
443
451
  }
444
- catch { /* ignore */ }
452
+ catch {
453
+ /* ignore */
454
+ }
445
455
  try {
446
456
  priorContent = fs.readFileSync(resolved, 'utf-8');
447
457
  }
448
- catch { /* ignore */ }
458
+ catch {
459
+ /* ignore */
460
+ }
449
461
  }
450
462
  // Atomic (temp + rename), matching edit_file/multi_edit. A plain
451
463
  // writeFileSync on the target truncates it first and then streams the
@@ -465,7 +477,11 @@ async function writeFile(input, workDir) {
465
477
  const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(content));
466
478
  if (isNew) {
467
479
  // A brand-new file has no "removed" side — every line is an addition.
468
- return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)${sec}`, linesAdded: lines, linesRemoved: 0 };
480
+ return {
481
+ output: `Created ${resolved} (${lines} lines, ${bytes} bytes)${sec}`,
482
+ linesAdded: lines,
483
+ linesRemoved: 0,
484
+ };
469
485
  }
470
486
  const xfile = await crossFileBreakageWarning(resolved, normalizeLF(priorContent), normalizeLF(content), workDir ?? process.cwd());
471
487
  // Reward-hacking guard: a write_file that OVERWRITES an existing test file can
@@ -520,7 +536,9 @@ async function writeDocx(input, workDir) {
520
536
  const title = typeof input.title === 'string' ? input.title : undefined;
521
537
  const paragraphsRaw = input.paragraphs;
522
538
  if (!Array.isArray(paragraphsRaw) || paragraphsRaw.length === 0) {
523
- return { error: 'Missing or empty required parameter: paragraphs (array of { text, heading?, bold?, italic?, bullet?, align? })' };
539
+ return {
540
+ error: 'Missing or empty required parameter: paragraphs (array of { text, heading?, bold?, italic?, bullet?, align? })',
541
+ };
524
542
  }
525
543
  try {
526
544
  // Loaded lazily so the (fairly large) docx/exceljs/pptxgenjs packages
@@ -529,14 +547,18 @@ async function writeDocx(input, workDir) {
529
547
  // the client: esbuild-bundled clients (VS Code, desktop) inline them,
530
548
  // while the CLI keeps all three external as runtime dependencies (see
531
549
  // packages/cli/build.js) so their real versions stay visible to audits.
532
- const { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType, } = await Promise.resolve().then(() => __importStar(require('docx')));
550
+ const { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType } = await Promise.resolve().then(() => __importStar(require('docx')));
533
551
  const headingMap = {
534
- 1: HeadingLevel.HEADING_1, 2: HeadingLevel.HEADING_2,
535
- 3: HeadingLevel.HEADING_3, 4: HeadingLevel.HEADING_4,
552
+ 1: HeadingLevel.HEADING_1,
553
+ 2: HeadingLevel.HEADING_2,
554
+ 3: HeadingLevel.HEADING_3,
555
+ 4: HeadingLevel.HEADING_4,
536
556
  };
537
557
  const alignMap = {
538
- left: AlignmentType.LEFT, center: AlignmentType.CENTER,
539
- right: AlignmentType.RIGHT, justify: AlignmentType.JUSTIFIED,
558
+ left: AlignmentType.LEFT,
559
+ center: AlignmentType.CENTER,
560
+ right: AlignmentType.RIGHT,
561
+ justify: AlignmentType.JUSTIFIED,
540
562
  };
541
563
  const children = [];
542
564
  if (title) {
@@ -557,7 +579,9 @@ async function writeDocx(input, workDir) {
557
579
  const resolved = resolvePath(filePath, workDir);
558
580
  const isNew = !fs.existsSync(resolved);
559
581
  atomicWriteBuffer(resolved, buffer);
560
- return { output: `${isNew ? 'Created' : 'Overwrote'} ${resolved} (${paragraphsRaw.length} paragraph(s), ${buffer.length} bytes)` };
582
+ return {
583
+ output: `${isNew ? 'Created' : 'Overwrote'} ${resolved} (${paragraphsRaw.length} paragraph(s), ${buffer.length} bytes)`,
584
+ };
561
585
  }
562
586
  catch (err) {
563
587
  return { error: `Failed to write .docx: ${err.message}` };
@@ -597,7 +621,9 @@ async function writeXlsx(input, workDir) {
597
621
  }
598
622
  }
599
623
  else if (mode === 'edit' && isNew) {
600
- return { error: `mode: 'edit' but ${resolved} does not exist yet — use mode: 'create' (or omit mode) for a new file.` };
624
+ return {
625
+ error: `mode: 'edit' but ${resolved} does not exist yet — use mode: 'create' (or omit mode) for a new file.`,
626
+ };
601
627
  }
602
628
  let sheetsTouched = 0;
603
629
  for (const raw of sheetsRaw) {
@@ -675,7 +701,9 @@ async function writePptx(input, workDir) {
675
701
  // can still go through the same atomic temp+rename write as the other two.
676
702
  const buffer = (await pres.write({ outputType: 'nodebuffer' }));
677
703
  atomicWriteBuffer(resolved, buffer);
678
- return { output: `${isNew ? 'Created' : 'Overwrote'} ${resolved} (${slidesRaw.length} slide(s), ${buffer.length} bytes)` };
704
+ return {
705
+ output: `${isNew ? 'Created' : 'Overwrote'} ${resolved} (${slidesRaw.length} slide(s), ${buffer.length} bytes)`,
706
+ };
679
707
  }
680
708
  catch (err) {
681
709
  return { error: `Failed to write .pptx: ${err.message}` };
@@ -752,11 +780,26 @@ function startBackgroundShell(command, displayCommand, workDir, note) {
752
780
  // unconditional `read` guard, an interactive installer step) blocks
753
781
  // forever instead of getting an immediate EOF, and the "shell" just sits
754
782
  // there as 'running' with zero output forever.
755
- const child = (0, child_process_1.spawn)(command, { shell: true, cwd: workDir ?? process.cwd(), env: process.env, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
783
+ const child = (0, child_process_1.spawn)(command, {
784
+ shell: true,
785
+ cwd: workDir ?? process.cwd(),
786
+ env: process.env,
787
+ detached: true,
788
+ stdio: ['ignore', 'pipe', 'pipe'],
789
+ });
756
790
  const shell = {
757
- id, command: displayCommand, child, output: '', readCursor: 0, truncated: false,
758
- status: 'running', exitCode: null,
759
- spillPath: null, spillFd: null, spillBytes: 0, spillFull: false,
791
+ id,
792
+ command: displayCommand,
793
+ child,
794
+ output: '',
795
+ readCursor: 0,
796
+ truncated: false,
797
+ status: 'running',
798
+ exitCode: null,
799
+ spillPath: null,
800
+ spillFd: null,
801
+ spillBytes: 0,
802
+ spillFull: false,
760
803
  };
761
804
  // Rolling buffer: keep the LAST MAX_OUTPUT_CHARS instead of the first, so a
762
805
  // long-lived watcher/dev-server's recent output (the useful part) is never
@@ -790,7 +833,9 @@ function startBackgroundShell(command, displayCommand, workDir, note) {
790
833
  if (buf.length > room)
791
834
  shell.spillFull = true;
792
835
  }
793
- catch { /* degrade to inline-only */ }
836
+ catch {
837
+ /* degrade to inline-only */
838
+ }
794
839
  }
795
840
  }
796
841
  }
@@ -825,7 +870,9 @@ function startBackgroundShell(command, displayCommand, workDir, note) {
825
870
  try {
826
871
  fs.closeSync(shell.spillFd);
827
872
  }
828
- catch { /* ignore */ }
873
+ catch {
874
+ /* ignore */
875
+ }
829
876
  shell.spillFd = null;
830
877
  }
831
878
  // Evict oldest exited shell once map grows past 50 to prevent session-long leak
@@ -838,10 +885,15 @@ function startBackgroundShell(command, displayCommand, workDir, note) {
838
885
  }
839
886
  }
840
887
  });
841
- child.on('error', (err) => { append(`\n[spawn error: ${err.message}]`); shell.status = 'exited'; });
888
+ child.on('error', (err) => {
889
+ append(`\n[spawn error: ${err.message}]`);
890
+ shell.status = 'exited';
891
+ });
842
892
  _bgShells.set(id, shell);
843
893
  const noteLine = note ? `\n[${note}]` : '';
844
- return { output: `Started background shell ${id}: ${displayCommand}${noteLine}\nUse bash_output with shell_id "${id}" to read output, kill_shell to stop it.` };
894
+ return {
895
+ output: `Started background shell ${id}: ${displayCommand}${noteLine}\nUse bash_output with shell_id "${id}" to read output, kill_shell to stop it.`,
896
+ };
845
897
  }
846
898
  async function bashOutput(input) {
847
899
  const id = typeof input.shell_id === 'string' ? input.shell_id : '';
@@ -878,13 +930,17 @@ async function killShell(input) {
878
930
  try {
879
931
  shell.child.kill(sig);
880
932
  }
881
- catch { /* already dead */ }
933
+ catch {
934
+ /* already dead */
935
+ }
882
936
  }
883
937
  };
884
938
  killTree('SIGTERM');
885
939
  // Escalate to SIGKILL after 3 s if process group is still alive
886
- setTimeout(() => { if (shell.status === 'killed')
887
- killTree('SIGKILL'); }, 3000);
940
+ setTimeout(() => {
941
+ if (shell.status === 'killed')
942
+ killTree('SIGKILL');
943
+ }, 3000);
888
944
  }
889
945
  return { output: `Background shell ${id} ${shell.status}.` };
890
946
  }
@@ -971,7 +1027,9 @@ async function bash(input, abortSignal, sandbox, workDir, onStream) {
971
1027
  try {
972
1028
  child.kill(sig);
973
1029
  }
974
- catch { /* already dead */ }
1030
+ catch {
1031
+ /* already dead */
1032
+ }
975
1033
  }
976
1034
  };
977
1035
  // Keep HEAD + TAIL rather than head-only: test runners print the failure
@@ -1020,14 +1078,18 @@ async function bash(input, abortSignal, sandbox, workDir, onStream) {
1020
1078
  if (buf.length > room)
1021
1079
  spillFull = true;
1022
1080
  }
1023
- catch { /* disk full / closed — degrade to inline-only */ }
1081
+ catch {
1082
+ /* disk full / closed — degrade to inline-only */
1083
+ }
1024
1084
  };
1025
1085
  const closeSpill = () => {
1026
1086
  if (spillFd !== null) {
1027
1087
  try {
1028
1088
  fs.closeSync(spillFd);
1029
1089
  }
1030
- catch { /* ignore */ }
1090
+ catch {
1091
+ /* ignore */
1092
+ }
1031
1093
  spillFd = null;
1032
1094
  }
1033
1095
  };
@@ -1088,7 +1150,11 @@ async function bash(input, abortSignal, sandbox, workDir, onStream) {
1088
1150
  if (timedOut) {
1089
1151
  // Return the partial output alongside the error — it usually holds the
1090
1152
  // reason the command hung (a stuck test, a prompt, a slow step).
1091
- resolve(prependNote({ error: `Command timed out after ${timeoutMs}ms`, interrupted: true, output: body ? `Partial output before timeout:\n${body}` : undefined }));
1153
+ resolve(prependNote({
1154
+ error: `Command timed out after ${timeoutMs}ms`,
1155
+ interrupted: true,
1156
+ output: body ? `Partial output before timeout:\n${body}` : undefined,
1157
+ }));
1092
1158
  }
1093
1159
  else if (abortSignal?.aborted) {
1094
1160
  resolve({ error: 'Command stopped by user', interrupted: true, output: body || undefined });
@@ -1116,7 +1182,9 @@ async function bash(input, abortSignal, sandbox, workDir, onStream) {
1116
1182
  try {
1117
1183
  onStream?.(text);
1118
1184
  }
1119
- catch { /* ignore */ }
1185
+ catch {
1186
+ /* ignore */
1187
+ }
1120
1188
  });
1121
1189
  child.stderr?.on('data', (chunk) => {
1122
1190
  const text = chunk.toString('utf-8');
@@ -1124,7 +1192,9 @@ async function bash(input, abortSignal, sandbox, workDir, onStream) {
1124
1192
  try {
1125
1193
  onStream?.(text);
1126
1194
  }
1127
- catch { /* ignore */ }
1195
+ catch {
1196
+ /* ignore */
1197
+ }
1128
1198
  });
1129
1199
  child.on('close', (code, signal) => done(code, signal));
1130
1200
  child.on('error', (err) => {
@@ -1138,8 +1208,10 @@ async function bash(input, abortSignal, sandbox, workDir, onStream) {
1138
1208
  if (settled)
1139
1209
  return;
1140
1210
  killTree('SIGTERM');
1141
- setTimeout(() => { if (!settled)
1142
- killTree('SIGKILL'); }, 3000);
1211
+ setTimeout(() => {
1212
+ if (!settled)
1213
+ killTree('SIGKILL');
1214
+ }, 3000);
1143
1215
  done(null, null, true);
1144
1216
  }, timeoutMs);
1145
1217
  child.on('close', () => clearTimeout(timer));
@@ -1173,7 +1245,9 @@ function capExternalOutput(output, label, maxChars = MAX_OUTPUT_CHARS) {
1173
1245
  fs.writeFileSync(spillPath, (0, safeSlice_1.sliceSafeEnd)(output, MAX_SPILL_BYTES));
1174
1246
  spillNote = ` Full output (${output.length} chars) saved to ${spillPath} — use read_file with offset/limit to inspect any section.`;
1175
1247
  }
1176
- catch { /* spill is best-effort */ }
1248
+ catch {
1249
+ /* spill is best-effort */
1250
+ }
1177
1251
  const headLen = Math.floor(maxChars * 0.7);
1178
1252
  const tailLen = maxChars - headLen;
1179
1253
  const omitted = output.length - headLen - tailLen;
@@ -1207,7 +1281,9 @@ function runCapture(cmd, args, opts) {
1207
1281
  try {
1208
1282
  child.kill('SIGTERM');
1209
1283
  }
1210
- catch { /* already gone */ }
1284
+ catch {
1285
+ /* already gone */
1286
+ }
1211
1287
  };
1212
1288
  const timer = setTimeout(() => kill('ETIMEDOUT'), opts.timeout);
1213
1289
  child.stdout.setEncoding('utf-8');
@@ -1224,10 +1300,15 @@ function runCapture(cmd, args, opts) {
1224
1300
  }
1225
1301
  stdout += chunk;
1226
1302
  });
1227
- child.stderr.on('data', (chunk) => { if (stderr.length < 64 * 1024)
1228
- stderr += chunk; });
1229
- child.on('error', (e) => { if (!error)
1230
- error = e; finish(null, null); });
1303
+ child.stderr.on('data', (chunk) => {
1304
+ if (stderr.length < 64 * 1024)
1305
+ stderr += chunk;
1306
+ });
1307
+ child.on('error', (e) => {
1308
+ if (!error)
1309
+ error = e;
1310
+ finish(null, null);
1311
+ });
1231
1312
  child.on('close', (code, signal) => finish(code, signal));
1232
1313
  });
1233
1314
  }
@@ -1268,27 +1349,33 @@ async function crossFileBreakageWarning(editedAbsPath, oldContent, newContent, w
1268
1349
  return '';
1269
1350
  let removed;
1270
1351
  try {
1271
- removed = (0, crossFile_1.findRemovedExports)(editedAbsPath, oldContent, newContent)
1272
- .filter((s) => (0, crossFile_1.isCheckableSymbol)(s.name));
1352
+ removed = (0, crossFile_1.findRemovedExports)(editedAbsPath, oldContent, newContent).filter((s) => (0, crossFile_1.isCheckableSymbol)(s.name));
1273
1353
  }
1274
1354
  catch {
1275
1355
  return '';
1276
1356
  }
1277
1357
  if (!removed.length)
1278
1358
  return '';
1279
- const editedReal = (() => { try {
1280
- return fs.realpathSync(editedAbsPath);
1281
- }
1282
- catch {
1283
- return editedAbsPath;
1284
- } })();
1359
+ const editedReal = (() => {
1360
+ try {
1361
+ return fs.realpathSync(editedAbsPath);
1362
+ }
1363
+ catch {
1364
+ return editedAbsPath;
1365
+ }
1366
+ })();
1285
1367
  const hits = [];
1286
1368
  const MAX_SYMBOLS = 8;
1287
1369
  // Scans run concurrently (they used to be up to 8 sequential blocking rg calls).
1288
- const scanned = await Promise.all(removed.slice(0, MAX_SYMBOLS).map(async (sym) => ({ sym, refs: await scanReferences(sym.name, workDir, editedReal) })));
1370
+ const scanned = await Promise.all(removed
1371
+ .slice(0, MAX_SYMBOLS)
1372
+ .map(async (sym) => ({ sym, refs: await scanReferences(sym.name, workDir, editedReal) })));
1289
1373
  for (const { sym, refs } of scanned) {
1290
1374
  if (refs.length) {
1291
- const shown = refs.slice(0, 3).map((r) => ` ${r}`).join('\n');
1375
+ const shown = refs
1376
+ .slice(0, 3)
1377
+ .map((r) => ` ${r}`)
1378
+ .join('\n');
1292
1379
  const more = refs.length > 3 ? `\n … and ${refs.length - 3} more` : '';
1293
1380
  hits.push(` • ${sym.kind} "${sym.name}" (removed/renamed) still referenced in:\n${shown}${more}`);
1294
1381
  }
@@ -1308,8 +1395,16 @@ async function scanReferences(name, workDir, excludeRealPath) {
1308
1395
  const opts = { cwd: workDir, timeout: 8000, maxBuffer: 8 * 1024 * 1024 };
1309
1396
  const r = ripgrepAvailable()
1310
1397
  ? await runCapture(rgBin(), ['-l', '--no-messages', '-e', pattern, '.'], opts)
1311
- : await runCapture('grep', ['-rlI', '--exclude-dir=node_modules', '--exclude-dir=.git',
1312
- '--exclude-dir=dist', '--exclude-dir=.next', '-E', pattern, '.'], opts);
1398
+ : await runCapture('grep', [
1399
+ '-rlI',
1400
+ '--exclude-dir=node_modules',
1401
+ '--exclude-dir=.git',
1402
+ '--exclude-dir=dist',
1403
+ '--exclude-dir=.next',
1404
+ '-E',
1405
+ pattern,
1406
+ '.',
1407
+ ], opts);
1313
1408
  if (r.stdout)
1314
1409
  for (const line of r.stdout.split('\n')) {
1315
1410
  if (line.trim())
@@ -1326,7 +1421,9 @@ async function scanReferences(name, workDir, excludeRealPath) {
1326
1421
  try {
1327
1422
  real = fs.realpathSync(abs);
1328
1423
  }
1329
- catch { /* keep abs */ }
1424
+ catch {
1425
+ /* keep abs */
1426
+ }
1330
1427
  if (real === excludeRealPath)
1331
1428
  continue; // the file we just edited
1332
1429
  rel.push(path.relative(workDir, abs) || f);
@@ -1341,7 +1438,7 @@ async function searchFiles(input, workDir) {
1341
1438
  // Default is "files" (paths of matching files only), like Claude Code's Grep
1342
1439
  // files_with_matches: a content dump of a common pattern was ~50× larger. Asking for
1343
1440
  // context_lines implies the caller wants content.
1344
- const searchType = typeof input.type === 'string' ? input.type : (contextLines > 0 ? 'content' : 'files');
1441
+ const searchType = typeof input.type === 'string' ? input.type : contextLines > 0 ? 'content' : 'files';
1345
1442
  const filesOnly = searchType === 'files' || searchType === 'files_with_matches';
1346
1443
  const include = typeof input.include === 'string' ? input.include : ''; // e.g. "*.ts"
1347
1444
  if (!pattern)
@@ -1363,18 +1460,24 @@ async function searchFiles(input, workDir) {
1363
1460
  const rel = path.relative(resolved, filePath).replace(/\\/g, '/');
1364
1461
  const relLower = rel.toLowerCase();
1365
1462
  // Support glob-style wildcards (* and ?) in addition to substring + regex
1366
- if (nameLower.includes(patternLower) || matchesGlob(name, pattern) || matchesPattern(name, pattern) ||
1367
- relLower.includes(patternLower) || matchesGlob(rel, pattern) || matchesPattern(rel, pattern)) {
1463
+ if (nameLower.includes(patternLower) ||
1464
+ matchesGlob(name, pattern) ||
1465
+ matchesPattern(name, pattern) ||
1466
+ relLower.includes(patternLower) ||
1467
+ matchesGlob(rel, pattern) ||
1468
+ matchesPattern(rel, pattern)) {
1368
1469
  matches.push(filePath);
1369
1470
  }
1370
1471
  });
1371
1472
  if (matches.length === 0)
1372
1473
  return { output: 'No matching files found.' };
1373
1474
  const relBase = workDir ? path.resolve(workDir) : process.cwd();
1374
- let out = matches.map((f) => {
1475
+ let out = matches
1476
+ .map((f) => {
1375
1477
  const r = path.relative(relBase, f);
1376
1478
  return r.startsWith('..') || path.isAbsolute(r) ? f : r.replace(/\\/g, '/');
1377
- }).join('\n');
1479
+ })
1480
+ .join('\n');
1378
1481
  if (out.length > MAX_OUTPUT_CHARS)
1379
1482
  out = (0, safeSlice_1.sliceSafeEnd)(out, MAX_OUTPUT_CHARS) + '\n[Truncated]';
1380
1483
  if (listing.truncated)
@@ -1394,15 +1497,24 @@ async function searchFiles(input, workDir) {
1394
1497
  const base = workDir ? path.resolve(workDir) : process.cwd();
1395
1498
  const relTarget = path.relative(base, resolved);
1396
1499
  const inside = !relTarget.startsWith('..') && !path.isAbsolute(relTarget);
1397
- const target = inside ? (relTarget || '.') : resolved;
1500
+ const target = inside ? relTarget || '.' : resolved;
1398
1501
  const spawnOpts = { timeout: DEFAULT_TIMEOUT_MS, maxBuffer: SEARCH_MAX_BUFFER, cwd: base };
1399
1502
  let result;
1400
1503
  if (ripgrepAvailable()) {
1401
1504
  const args = filesOnly
1402
1505
  ? ['--files-with-matches', '--color=never', '--hidden', '--glob', '!.git']
1403
- : ['--line-number', '--no-heading', '--color=never', '--hidden', '--glob', '!.git',
1506
+ : [
1507
+ '--line-number',
1508
+ '--no-heading',
1509
+ '--color=never',
1510
+ '--hidden',
1511
+ '--glob',
1512
+ '!.git',
1404
1513
  // One minified-bundle hit could otherwise fill the whole output cap.
1405
- '--max-columns', '500', '--max-columns-preview'];
1514
+ '--max-columns',
1515
+ '500',
1516
+ '--max-columns-preview',
1517
+ ];
1406
1518
  if (ignoreCase)
1407
1519
  args.push('-i');
1408
1520
  if (contextLines > 0 && !filesOnly)
@@ -1446,12 +1558,16 @@ async function searchFiles(input, workDir) {
1446
1558
  // first misreported a too-large result as a timeout.
1447
1559
  if (spawnErr.code === 'ETIMEDOUT' || (result.signal === 'SIGTERM' && spawnErr.code !== 'ENOBUFS')) {
1448
1560
  const partial = output ? `\n\nPartial results before timeout:\n${globalCapMatches(output)}` : '';
1449
- return { error: `Search timed out after ${Math.round(DEFAULT_TIMEOUT_MS / 1000)}s — narrow the path or pattern (or add an "include" filter).${partial}` };
1561
+ return {
1562
+ error: `Search timed out after ${Math.round(DEFAULT_TIMEOUT_MS / 1000)}s — narrow the path or pattern (or add an "include" filter).${partial}`,
1563
+ };
1450
1564
  }
1451
1565
  if (spawnErr.code === 'ENOBUFS') {
1452
1566
  // We still captured up to maxBuffer of output — return the capped head
1453
1567
  // instead of failing blind.
1454
- return { output: `${globalCapMatches(output)}\n[Too many matches — output exceeded ${SEARCH_MAX_BUFFER / (1024 * 1024)}MB and was capped. Narrow the pattern/path.]` };
1568
+ return {
1569
+ output: `${globalCapMatches(output)}\n[Too many matches — output exceeded ${SEARCH_MAX_BUFFER / (1024 * 1024)}MB and was capped. Narrow the pattern/path.]`,
1570
+ };
1455
1571
  }
1456
1572
  return { error: `search failed: ${spawnErr.message}` };
1457
1573
  }
@@ -1475,10 +1591,12 @@ async function searchFiles(input, workDir) {
1475
1591
  return { output: 'No matches found.' };
1476
1592
  const shown = files.slice(0, MAX_GLOB_RESULTS);
1477
1593
  const more = files.length > shown.length
1478
- ? `\n[${files.length - shown.length} more files not shown — narrow the pattern/path]` : '';
1594
+ ? `\n[${files.length - shown.length} more files not shown — narrow the pattern/path]`
1595
+ : '';
1479
1596
  return {
1480
1597
  output: `${files.length} file(s) contain matches (type="content" for the matching lines):\n` +
1481
- shown.join('\n') + more,
1598
+ shown.join('\n') +
1599
+ more,
1482
1600
  };
1483
1601
  }
1484
1602
  output = globalCapMatches(output);
@@ -1515,7 +1633,12 @@ function globalCapMatches(output) {
1515
1633
  function matchesGlob(name, pattern) {
1516
1634
  // Convert simple glob (* → .*, ? → .) to regex
1517
1635
  try {
1518
- const re = new RegExp('^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
1636
+ const re = new RegExp('^' +
1637
+ pattern
1638
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
1639
+ .replace(/\*/g, '.*')
1640
+ .replace(/\?/g, '.') +
1641
+ '$', 'i');
1519
1642
  return re.test(name);
1520
1643
  }
1521
1644
  catch {
@@ -1538,9 +1661,27 @@ function matchesPattern(name, pattern) {
1538
1661
  // Noise directories never worth walking. .venv/venv matter most: a single Python virtualenv
1539
1662
  // is hundreds of MB of third-party files that drowned glob / filename-search results.
1540
1663
  const WALK_SKIP_DIRS = new Set([
1541
- '.git', 'node_modules', 'dist', '.next', '__pycache__', '.turbo', 'coverage',
1542
- '.venv', 'venv', '.tox', '.mypy_cache', '.pytest_cache', '.ruff_cache', '.gradle',
1543
- '.cache', '.parcel-cache', '.svelte-kit', '.nuxt', 'target', '.idea', '.DS_Store',
1664
+ '.git',
1665
+ 'node_modules',
1666
+ 'dist',
1667
+ '.next',
1668
+ '__pycache__',
1669
+ '.turbo',
1670
+ 'coverage',
1671
+ '.venv',
1672
+ 'venv',
1673
+ '.tox',
1674
+ '.mypy_cache',
1675
+ '.pytest_cache',
1676
+ '.ruff_cache',
1677
+ '.gradle',
1678
+ '.cache',
1679
+ '.parcel-cache',
1680
+ '.svelte-kit',
1681
+ '.nuxt',
1682
+ 'target',
1683
+ '.idea',
1684
+ '.DS_Store',
1544
1685
  ]);
1545
1686
  /** Hard ceiling on files visited by the fallback directory walk (no ripgrep). */
1546
1687
  const WALK_MAX_FILES = 50000;
@@ -1571,7 +1712,10 @@ async function listFiles(root) {
1571
1712
  // Exit 2 = "some path errored" (a symlink loop, an unreadable dir) — the list of
1572
1713
  // everything else is still complete, so keep it rather than re-walking the tree.
1573
1714
  if (!r.error && (r.status === 0 || r.status === 1 || (r.status === 2 && r.stdout))) {
1574
- const files = r.stdout.split('\n').filter(Boolean).map((f) => path.join(root, f.replace(/^\.\//, '')));
1715
+ const files = r.stdout
1716
+ .split('\n')
1717
+ .filter(Boolean)
1718
+ .map((f) => path.join(root, f.replace(/^\.\//, '')));
1575
1719
  return { files, truncated: false };
1576
1720
  }
1577
1721
  // Anything else (timeout, overflow, spawn failure): fall through to the walk.
@@ -1603,14 +1747,16 @@ function walkDir(dirPath, callback, _visited = new Set()) {
1603
1747
  const fullPath = path.join(dirPath, entry.name);
1604
1748
  // isDirectory() returns false for symlinks — use isSymbolicLink() to detect
1605
1749
  // symlinked directories and walk them too (with cycle protection above).
1606
- const isDir = entry.isDirectory() || (entry.isSymbolicLink() && (() => {
1607
- try {
1608
- return fs.statSync(fullPath).isDirectory();
1609
- }
1610
- catch {
1611
- return false;
1612
- }
1613
- })());
1750
+ const isDir = entry.isDirectory() ||
1751
+ (entry.isSymbolicLink() &&
1752
+ (() => {
1753
+ try {
1754
+ return fs.statSync(fullPath).isDirectory();
1755
+ }
1756
+ catch {
1757
+ return false;
1758
+ }
1759
+ })());
1614
1760
  if (isDir) {
1615
1761
  // Skip common noise directories
1616
1762
  if (WALK_SKIP_DIRS.has(entry.name))
@@ -1715,7 +1861,9 @@ function preflightEdit(resolved) {
1715
1861
  if (stat.isDirectory())
1716
1862
  return { error: `Path is a directory, not a file: ${resolved}.` };
1717
1863
  if (stat.size > MAX_EDIT_BYTES) {
1718
- return { error: `File too large to edit (${(stat.size / 1024 / 1024).toFixed(1)} MB, max ${MAX_EDIT_BYTES / 1024 / 1024} MB). Use bash (sed/awk) for very large files.` };
1864
+ return {
1865
+ error: `File too large to edit (${(stat.size / 1024 / 1024).toFixed(1)} MB, max ${MAX_EDIT_BYTES / 1024 / 1024} MB). Use bash (sed/awk) for very large files.`,
1866
+ };
1719
1867
  }
1720
1868
  if (isBinaryFile(resolved)) {
1721
1869
  return { error: `Refusing to edit binary file ${resolved} — a text replace would corrupt it.` };
@@ -1782,7 +1930,9 @@ function atomicWrite(resolved, data, mode) {
1782
1930
  try {
1783
1931
  fs.chmodSync(target, mode);
1784
1932
  }
1785
- catch { /* non-fatal */ }
1933
+ catch {
1934
+ /* non-fatal */
1935
+ }
1786
1936
  }
1787
1937
  return;
1788
1938
  }
@@ -1791,7 +1941,9 @@ function atomicWrite(resolved, data, mode) {
1791
1941
  try {
1792
1942
  fs.rmSync(tmp, { force: true });
1793
1943
  }
1794
- catch { /* best effort */ }
1944
+ catch {
1945
+ /* best effort */
1946
+ }
1795
1947
  throw err;
1796
1948
  }
1797
1949
  // ── Commit ───────────────────────────────────────────────────────────────
@@ -1800,7 +1952,9 @@ function atomicWrite(resolved, data, mode) {
1800
1952
  try {
1801
1953
  fs.chmodSync(tmp, mode);
1802
1954
  }
1803
- catch { /* non-fatal — keep going */ }
1955
+ catch {
1956
+ /* non-fatal — keep going */
1957
+ }
1804
1958
  }
1805
1959
  fs.renameSync(tmp, target);
1806
1960
  }
@@ -1811,7 +1965,9 @@ function atomicWrite(resolved, data, mode) {
1811
1965
  try {
1812
1966
  fs.rmSync(tmp, { force: true });
1813
1967
  }
1814
- catch { /* best effort */ }
1968
+ catch {
1969
+ /* best effort */
1970
+ }
1815
1971
  throw err;
1816
1972
  }
1817
1973
  }
@@ -1847,7 +2003,9 @@ async function editFile(input, workDir) {
1847
2003
  return { error: `old_string not found in ${resolved}. Read the file first to verify the exact text to replace.` };
1848
2004
  }
1849
2005
  if (count > 1) {
1850
- return { error: `old_string appears ${count} times in ${resolved} — it must be unique. Add more surrounding context to make it unambiguous.` };
2006
+ return {
2007
+ error: `old_string appears ${count} times in ${resolved} — it must be unique. Add more surrounding context to make it unambiguous.`,
2008
+ };
1851
2009
  }
1852
2010
  let updated = literalReplace(origNorm, oldNorm, newNorm);
1853
2011
  if (wasCRLF)
@@ -1909,7 +2067,9 @@ async function deleteFile(input, workDir) {
1909
2067
  const resolved = resolvePath(filePath, workDir);
1910
2068
  const stat = fs.statSync(resolved);
1911
2069
  if (stat.isDirectory()) {
1912
- return { error: `Path is a directory, not a file: ${resolved}. Use a recursive delete tool or remove contents first.` };
2070
+ return {
2071
+ error: `Path is a directory, not a file: ${resolved}. Use a recursive delete tool or remove contents first.`,
2072
+ };
1913
2073
  }
1914
2074
  fs.unlinkSync(resolved);
1915
2075
  return { output: `Deleted: ${resolved}` };
@@ -1920,7 +2080,7 @@ async function deleteFile(input, workDir) {
1920
2080
  }
1921
2081
  // Strip HTML tags → readable plain text (like Claude Code's WebFetch)
1922
2082
  function stripHtml(html) {
1923
- return html
2083
+ return (html
1924
2084
  // Remove <script> and <style> blocks entirely
1925
2085
  .replace(/<script[\s\S]*?<\/script>/gi, '')
1926
2086
  .replace(/<style[\s\S]*?<\/style>/gi, '')
@@ -1935,10 +2095,15 @@ function stripHtml(html) {
1935
2095
  // Remove all remaining tags
1936
2096
  .replace(/<[^>]+>/g, '')
1937
2097
  // Decode common HTML entities
1938
- .replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, ' ')
2098
+ .replace(/&amp;/g, '&')
2099
+ .replace(/&lt;/g, '<')
2100
+ .replace(/&gt;/g, '>')
2101
+ .replace(/&quot;/g, '"')
2102
+ .replace(/&#39;/g, "'")
2103
+ .replace(/&nbsp;/g, ' ')
1939
2104
  // Collapse 3+ blank lines → 2
1940
2105
  .replace(/\n{3,}/g, '\n\n')
1941
- .trim();
2106
+ .trim());
1942
2107
  }
1943
2108
  const MAX_REDIRECTS = 10;
1944
2109
  const FETCH_USER_AGENT = 'Nexrall-Code/1.0 (+https://nexrall.com)';
@@ -2058,7 +2223,10 @@ function fetchBlocklistCheck(parsedUrl) {
2058
2223
  if (!raw)
2059
2224
  return null;
2060
2225
  const host = parsedUrl.hostname.toLowerCase().replace(/^\[|\]$/g, '');
2061
- const entries = raw.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
2226
+ const entries = raw
2227
+ .split(',')
2228
+ .map((s) => s.trim().toLowerCase())
2229
+ .filter(Boolean);
2062
2230
  for (const entry of entries) {
2063
2231
  if (host === entry || host.endsWith(`.${entry}`)) {
2064
2232
  return `Refusing to fetch "${host}" — blocked by NEXRALL_FETCH_BLOCKLIST (matched "${entry}").`;
@@ -2090,9 +2258,14 @@ async function fetchUrlTool(input, workDir, abortSignal) {
2090
2258
  try {
2091
2259
  json = JSON.parse(r.body);
2092
2260
  }
2093
- catch { /* fall through */ }
2261
+ catch {
2262
+ /* fall through */
2263
+ }
2094
2264
  if (r.status !== 200 || !json.text) {
2095
- return { ...raw, output: `${raw.output ?? ''}\n\n[prompt extraction unavailable (${json.error ?? `HTTP ${r.status}`}) — raw page text shown instead]` };
2265
+ return {
2266
+ ...raw,
2267
+ output: `${raw.output ?? ''}\n\n[prompt extraction unavailable (${json.error ?? `HTTP ${r.status}`}) — raw page text shown instead]`,
2268
+ };
2096
2269
  }
2097
2270
  return {
2098
2271
  output: `[Answer extracted from ${capture.url ?? input.url} by ${json.model ?? 'a low-cost model'} for: "${prompt.slice(0, 200)}"]\n\n` +
@@ -2102,7 +2275,10 @@ async function fetchUrlTool(input, workDir, abortSignal) {
2102
2275
  catch (err) {
2103
2276
  if (err.name === 'AbortError')
2104
2277
  return { error: 'fetch_url stopped by user', interrupted: true };
2105
- return { ...raw, output: `${raw.output ?? ''}\n\n[prompt extraction failed (${err.message}) — raw page text shown instead]` };
2278
+ return {
2279
+ ...raw,
2280
+ output: `${raw.output ?? ''}\n\n[prompt extraction failed (${err.message}) — raw page text shown instead]`,
2281
+ };
2106
2282
  }
2107
2283
  }
2108
2284
  async function fetchUrl(input, _workDir, _redirectCount = 0, abortSignal,
@@ -2147,9 +2323,13 @@ capture) {
2147
2323
  return dns.lookup(hostname, options, (err, address, family) => {
2148
2324
  if (err)
2149
2325
  return callback(err, address, family);
2150
- const addrs = Array.isArray(address) ? address.map((a) => a.address) : [address];
2326
+ const addrs = Array.isArray(address)
2327
+ ? address.map((a) => a.address)
2328
+ : [address];
2151
2329
  if (!allowLocal && addrs.some((a) => isPrivateIp(a))) {
2152
- return callback(Object.assign(new Error('SSRF guard: host resolved to a private address at connect time'), { code: 'ESSRFBLOCKED' }), address, family);
2330
+ return callback(Object.assign(new Error('SSRF guard: host resolved to a private address at connect time'), {
2331
+ code: 'ESSRFBLOCKED',
2332
+ }), address, family);
2153
2333
  }
2154
2334
  return callback(null, address, family);
2155
2335
  });
@@ -2193,7 +2373,7 @@ capture) {
2193
2373
  headers: {
2194
2374
  'User-Agent': FETCH_USER_AGENT,
2195
2375
  'Accept-Encoding': 'identity', // opt-out of gzip/deflate — we read raw Buffer as UTF-8
2196
- 'Accept': 'text/html,text/plain,application/json,*/*',
2376
+ Accept: 'text/html,text/plain,application/json,*/*',
2197
2377
  },
2198
2378
  }, (res) => {
2199
2379
  // Follow redirects (301/302/307/308) with depth counter to prevent infinite loops
@@ -2237,7 +2417,9 @@ capture) {
2237
2417
  // (anyone can put "ignore previous instructions..." on a page). Wrap it in an
2238
2418
  // explicit untrusted-data delimiter so the model treats it as DATA to read, not
2239
2419
  // as instructions to follow — defense-in-depth alongside the system-prompt rule.
2240
- finish({ output: `HTTP ${status}\n\n<untrusted_web_content url="${url}">\n${body}${truncNote}\n</untrusted_web_content>` });
2420
+ finish({
2421
+ output: `HTTP ${status}\n\n<untrusted_web_content url="${url}">\n${body}${truncNote}\n</untrusted_web_content>`,
2422
+ });
2241
2423
  };
2242
2424
  res.on('data', (chunk) => {
2243
2425
  if (totalBytes + chunk.length > MAX_FETCH_BYTES) {
@@ -2261,11 +2443,15 @@ capture) {
2261
2443
  res.on('end', emitBody);
2262
2444
  // Ignore errors caused by our own intentional destroy (truncation / redirect);
2263
2445
  // only real transport failures should surface.
2264
- res.on('error', (err) => { if (!selfAborted)
2265
- finish({ error: err.message }); });
2446
+ res.on('error', (err) => {
2447
+ if (!selfAborted)
2448
+ finish({ error: err.message });
2449
+ });
2450
+ });
2451
+ req.on('error', (err) => {
2452
+ if (!selfAborted)
2453
+ finish({ error: err.message });
2266
2454
  });
2267
- req.on('error', (err) => { if (!selfAborted)
2268
- finish({ error: err.message }); });
2269
2455
  req.on('timeout', () => {
2270
2456
  selfAborted = true;
2271
2457
  req.destroy();
@@ -2294,10 +2480,10 @@ function buildDiff(filePath, oldStr, newStr, fileContent) {
2294
2480
  }
2295
2481
  }
2296
2482
  const lines = [`--- ${filePath}${hunkHeader}`, `+++ ${filePath}${hunkHeader}`];
2297
- oldLines.slice(0, MAX_SHOW).forEach(l => lines.push(`- ${l}`));
2483
+ oldLines.slice(0, MAX_SHOW).forEach((l) => lines.push(`- ${l}`));
2298
2484
  if (oldLines.length > MAX_SHOW)
2299
2485
  lines.push(` … (${oldLines.length - MAX_SHOW} more removed lines)`);
2300
- newLines.slice(0, MAX_SHOW).forEach(l => lines.push(`+ ${l}`));
2486
+ newLines.slice(0, MAX_SHOW).forEach((l) => lines.push(`+ ${l}`));
2301
2487
  if (newLines.length > MAX_SHOW)
2302
2488
  lines.push(` … (${newLines.length - MAX_SHOW} more added lines)`);
2303
2489
  return lines.join('\n');
@@ -2336,10 +2522,14 @@ async function multiEdit(input, workDir) {
2336
2522
  // indexOf-based count — safe for regex metacharacters like ( . $ * + ?
2337
2523
  const count = countOccurrences(content, oldStr);
2338
2524
  if (count === 0) {
2339
- return { error: `Edit #${i + 1}: old_string not found (after applying ${i} prior edit(s)). Re-read the file and retry.` };
2525
+ return {
2526
+ error: `Edit #${i + 1}: old_string not found (after applying ${i} prior edit(s)). Re-read the file and retry.`,
2527
+ };
2340
2528
  }
2341
2529
  if (count > 1) {
2342
- return { error: `Edit #${i + 1}: old_string appears ${count} times — it must be unique. Add more surrounding context.` };
2530
+ return {
2531
+ error: `Edit #${i + 1}: old_string appears ${count} times — it must be unique. Add more surrounding context.`,
2532
+ };
2343
2533
  }
2344
2534
  // literalReplace: $ signs in newStr are never treated as patterns
2345
2535
  // Pass the pre-replace content so line numbers reflect the file state at
@@ -2359,7 +2549,9 @@ async function multiEdit(input, workDir) {
2359
2549
  const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(insertedText.join('\n')));
2360
2550
  return {
2361
2551
  output: `Applied ${edits.length} edit(s) to ${resolved}:\n` +
2362
- diffs.map((d, i) => `\n--- edit #${i + 1} ---\n${d}`).join('\n') + xfile + sec,
2552
+ diffs.map((d, i) => `\n--- edit #${i + 1} ---\n${d}`).join('\n') +
2553
+ xfile +
2554
+ sec,
2363
2555
  linesAdded,
2364
2556
  linesRemoved,
2365
2557
  };
@@ -2396,18 +2588,25 @@ async function glob(input, workDir) {
2396
2588
  return { output: `No files matched pattern: ${pattern}${scanNote}` };
2397
2589
  // Most recently modified first, capped — same shape as Claude Code's Glob. Paths are
2398
2590
  // relative to the search dir: absolute prefixes were ~25% of the output's tokens.
2399
- const mtime = (f) => { try {
2400
- return fs.statSync(f).mtimeMs;
2401
- }
2402
- catch {
2403
- return 0;
2404
- } };
2405
- const sorted = matches.map((f) => ({ f, t: mtime(f) })).sort((a, b) => b.t - a.t).map((x) => x.f);
2591
+ const mtime = (f) => {
2592
+ try {
2593
+ return fs.statSync(f).mtimeMs;
2594
+ }
2595
+ catch {
2596
+ return 0;
2597
+ }
2598
+ };
2599
+ const sorted = matches
2600
+ .map((f) => ({ f, t: mtime(f) }))
2601
+ .sort((a, b) => b.t - a.t)
2602
+ .map((x) => x.f);
2406
2603
  const shown = sorted.slice(0, MAX_GLOB_RESULTS).map((f) => path.relative(resolved, f).replace(/\\/g, '/'));
2407
2604
  const more = sorted.length > shown.length
2408
2605
  ? `\n[${sorted.length - shown.length} more not shown — narrow the pattern or path]`
2409
2606
  : '';
2410
- return { output: `${matches.length} file(s) matched "${pattern}" under ${resolved}:\n${shown.join('\n')}${more}${scanNote}` };
2607
+ return {
2608
+ output: `${matches.length} file(s) matched "${pattern}" under ${resolved}:\n${shown.join('\n')}${more}${scanNote}`,
2609
+ };
2411
2610
  }
2412
2611
  catch (err) {
2413
2612
  return { error: err.message };
@@ -2488,7 +2687,7 @@ async function todoWrite(input, scopeKey) {
2488
2687
  for (const t of todos) {
2489
2688
  const item = t;
2490
2689
  const content = typeof item.content === 'string' ? item.content.trim() : '';
2491
- const status = (item.status === 'in_progress' || item.status === 'completed') ? item.status : 'pending';
2690
+ const status = item.status === 'in_progress' || item.status === 'completed' ? item.status : 'pending';
2492
2691
  if (!content)
2493
2692
  continue;
2494
2693
  scope.items.push({ id: scope.nextId++, content, status });
@@ -2505,8 +2704,8 @@ function formatTodos(scope) {
2505
2704
  if (!scope.items.length)
2506
2705
  return 'Task list is empty.';
2507
2706
  const icons = { pending: '☐', in_progress: '▶', completed: '✓' };
2508
- const lines = scope.items.map(t => `${icons[t.status] ?? '☐'} [${t.id}] ${t.content} (${t.status})`);
2509
- return `Tasks (${scope.items.filter(t => t.status === 'completed').length}/${scope.items.length} done):\n${lines.join('\n')}`;
2707
+ const lines = scope.items.map((t) => `${icons[t.status] ?? '☐'} [${t.id}] ${t.content} (${t.status})`);
2708
+ return `Tasks (${scope.items.filter((t) => t.status === 'completed').length}/${scope.items.length} done):\n${lines.join('\n')}`;
2510
2709
  }
2511
2710
  // ─── Shared task list (cross-SESSION coordination — see agent/sharedTasks.ts) ──
2512
2711
  //
@@ -2555,7 +2754,7 @@ async function claimSharedTaskTool(input, workDir) {
2555
2754
  const reasons = {
2556
2755
  'not-found': `No shared task with id "${taskId}" exists for this project.`,
2557
2756
  'already-claimed': `Task "${taskId}" is already claimed by another session. Pick a different task, or check back later.`,
2558
- 'blocked': `Task "${taskId}" is blocked — one or more tasks it depends on are not completed yet.`,
2757
+ blocked: `Task "${taskId}" is blocked — one or more tasks it depends on are not completed yet.`,
2559
2758
  'not-pending': `Task "${taskId}" is already completed.`,
2560
2759
  };
2561
2760
  return { error: reasons[res.reason] ?? `Could not claim task "${taskId}" (${res.reason}).` };
@@ -2578,7 +2777,9 @@ async function updateSharedTaskStatusTool(input, workDir) {
2578
2777
  if (res.reason === 'not-found')
2579
2778
  return { error: `No shared task with id "${taskId}" exists for this project.` };
2580
2779
  if (res.reason === 'already-claimed')
2581
- return { error: `Task "${taskId}" is claimed by a different session — only the session that claimed it may change its status.` };
2780
+ return {
2781
+ error: `Task "${taskId}" is claimed by a different session — only the session that claimed it may change its status.`,
2782
+ };
2582
2783
  return { error: `Could not update task "${taskId}" (${res.reason}).` };
2583
2784
  }
2584
2785
  return { output: `Updated [${res.task.id}] ${res.task.content} → ${res.task.status}.` };
@@ -2613,7 +2814,9 @@ async function messagePeerSessionTool(input, selfPeer) {
2613
2814
  return { error: 'This session is not registered as a discoverable peer, so it cannot send messages.' };
2614
2815
  const target = (0, peerRegistry_1.findPeerByName)(targetName);
2615
2816
  if (!target)
2616
- return { error: `No live session named "${targetName}" was found. Use list_peer_sessions to see who is currently reachable.` };
2817
+ return {
2818
+ error: `No live session named "${targetName}" was found. Use list_peer_sessions to see who is currently reachable.`,
2819
+ };
2617
2820
  if (target.id === selfPeer.id)
2618
2821
  return { error: 'Cannot message yourself.' };
2619
2822
  if (!target.socketPath)
@@ -2679,7 +2882,7 @@ function formatNbOutput(out) {
2679
2882
  if (out.traceback) {
2680
2883
  const clean = out.traceback
2681
2884
  // eslint-disable-next-line no-control-regex -- ANSI parser: the ESC bytes are the grammar
2682
- .map(l => l.replace(/\x1b\[[0-9;]*[mGKH]/g, ''))
2885
+ .map((l) => l.replace(/\x1b\[[0-9;]*[mGKH]/g, ''))
2683
2886
  .join('\n');
2684
2887
  lines.push((0, safeSlice_1.sliceSafeEnd)(clean, 1500) + (clean.length > 1500 ? '\n[truncated]' : ''));
2685
2888
  }
@@ -2708,7 +2911,9 @@ async function notebookRead(input, workDir) {
2708
2911
  nb.cells.forEach((cell, i) => {
2709
2912
  const src = nbJoin(cell.source);
2710
2913
  const execLabel = cell.cell_type === 'code'
2711
- ? (cell.execution_count != null ? ` (exec #${cell.execution_count})` : ' (not executed)')
2914
+ ? cell.execution_count != null
2915
+ ? ` (exec #${cell.execution_count})`
2916
+ : ' (not executed)'
2712
2917
  : '';
2713
2918
  lines.push('');
2714
2919
  lines.push(`${'─'.repeat(60)}`);
@@ -2718,7 +2923,7 @@ async function notebookRead(input, workDir) {
2718
2923
  if (cell.cell_type === 'code' && cell.outputs?.length) {
2719
2924
  lines.push('');
2720
2925
  lines.push('▶ Output:');
2721
- cell.outputs.forEach(o => {
2926
+ cell.outputs.forEach((o) => {
2722
2927
  const formatted = formatNbOutput(o);
2723
2928
  if (formatted)
2724
2929
  lines.push(formatted);
@@ -2727,7 +2932,9 @@ async function notebookRead(input, workDir) {
2727
2932
  });
2728
2933
  let output = lines.join('\n');
2729
2934
  if (output.length > MAX_OUTPUT_CHARS) {
2730
- output = (0, safeSlice_1.sliceSafeEnd)(output, MAX_OUTPUT_CHARS) + `\n\n[Notebook output truncated at ${MAX_OUTPUT_CHARS.toLocaleString('en-US')} chars]`;
2935
+ output =
2936
+ (0, safeSlice_1.sliceSafeEnd)(output, MAX_OUTPUT_CHARS) +
2937
+ `\n\n[Notebook output truncated at ${MAX_OUTPUT_CHARS.toLocaleString('en-US')} chars]`;
2731
2938
  }
2732
2939
  return { output };
2733
2940
  }
@@ -2743,8 +2950,10 @@ async function notebookEdit(input, workDir) {
2743
2950
  const editType = typeof input.edit_type === 'string' ? input.edit_type : '';
2744
2951
  // Accept cell_index as number OR numeric string (model sometimes sends "0" as string)
2745
2952
  const rawIdx = input.cell_index;
2746
- const cellIndex = typeof rawIdx === 'number' ? Math.floor(rawIdx)
2747
- : typeof rawIdx === 'string' && rawIdx.trim() !== '' ? parseInt(rawIdx, 10)
2953
+ const cellIndex = typeof rawIdx === 'number'
2954
+ ? Math.floor(rawIdx)
2955
+ : typeof rawIdx === 'string' && rawIdx.trim() !== ''
2956
+ ? parseInt(rawIdx, 10)
2748
2957
  : -1;
2749
2958
  const source = typeof input.source === 'string' ? input.source : null;
2750
2959
  const cellType = (typeof input.cell_type === 'string' ? input.cell_type : 'code');
@@ -2774,14 +2983,18 @@ async function notebookEdit(input, workDir) {
2774
2983
  cell.execution_count = null;
2775
2984
  }
2776
2985
  save();
2777
- return { output: `Replaced source of cell ${cellIndex} in ${resolved}.${cell.cell_type === 'code' ? ' Outputs cleared.' : ''}` };
2986
+ return {
2987
+ output: `Replaced source of cell ${cellIndex} in ${resolved}.${cell.cell_type === 'code' ? ' Outputs cleared.' : ''}`,
2988
+ };
2778
2989
  }
2779
2990
  if (editType === 'delete') {
2780
2991
  if (cellIndex < 0 || cellIndex >= n)
2781
2992
  return { error: `cell_index ${cellIndex} out of range (0–${n - 1})` };
2782
2993
  const removed = nb.cells.splice(cellIndex, 1)[0];
2783
2994
  save();
2784
- return { output: `Deleted cell ${cellIndex} [${removed.cell_type}] from ${resolved}. Notebook now has ${nb.cells.length} cells.` };
2995
+ return {
2996
+ output: `Deleted cell ${cellIndex} [${removed.cell_type}] from ${resolved}. Notebook now has ${nb.cells.length} cells.`,
2997
+ };
2785
2998
  }
2786
2999
  // insert: inserts AFTER cell_index, or at end if -1
2787
3000
  if (source === null)
@@ -2795,7 +3008,9 @@ async function notebookEdit(input, workDir) {
2795
3008
  };
2796
3009
  nb.cells.splice(insertAt, 0, newCell);
2797
3010
  save();
2798
- return { output: `Inserted new ${cellType} cell at index ${insertAt} in ${resolved}. Notebook now has ${nb.cells.length} cells.` };
3011
+ return {
3012
+ output: `Inserted new ${cellType} cell at index ${insertAt} in ${resolved}. Notebook now has ${nb.cells.length} cells.`,
3013
+ };
2799
3014
  }
2800
3015
  catch (err) {
2801
3016
  if (err instanceof SyntaxError)
@@ -2821,17 +3036,32 @@ function _httpsPost(url, body, headers, timeoutMs = 30000, abortSignal) {
2821
3036
  const chunks = [];
2822
3037
  let settled = false;
2823
3038
  let abortPoll;
2824
- const done = (fn) => { if (settled)
2825
- return; settled = true; if (abortPoll)
2826
- clearInterval(abortPoll); fn(); };
3039
+ const done = (fn) => {
3040
+ if (settled)
3041
+ return;
3042
+ settled = true;
3043
+ if (abortPoll)
3044
+ clearInterval(abortPoll);
3045
+ fn();
3046
+ };
2827
3047
  // http for an http:// API base (local dev / tests) — https.request would reject it —
2828
3048
  // and the explicit port, which was silently dropped (so :3000 went to :443).
2829
3049
  const mod = u.protocol === 'http:' ? http : https;
2830
- const req = mod.request({ hostname: u.hostname, ...(u.port ? { port: Number(u.port) } : {}), path: u.pathname + u.search, method: 'POST', headers: { ...headers, 'Content-Length': Buffer.byteLength(body) }, timeout: timeoutMs }, (res) => {
3050
+ const req = mod.request({
3051
+ hostname: u.hostname,
3052
+ ...(u.port ? { port: Number(u.port) } : {}),
3053
+ path: u.pathname + u.search,
3054
+ method: 'POST',
3055
+ headers: { ...headers, 'Content-Length': Buffer.byteLength(body) },
3056
+ timeout: timeoutMs,
3057
+ }, (res) => {
2831
3058
  res.on('data', (c) => chunks.push(c));
2832
3059
  res.on('end', () => done(() => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString('utf-8') })));
2833
3060
  });
2834
- req.on('timeout', () => { req.destroy(); done(() => reject(new Error(`Request timed out after ${timeoutMs}ms`))); });
3061
+ req.on('timeout', () => {
3062
+ req.destroy();
3063
+ done(() => reject(new Error(`Request timed out after ${timeoutMs}ms`)));
3064
+ });
2835
3065
  req.on('error', (err) => done(() => reject(err)));
2836
3066
  if (abortSignal) {
2837
3067
  abortPoll = setInterval(() => {
@@ -2852,20 +3082,35 @@ function _httpsGet(url, headers, timeoutMs = 20000, abortSignal) {
2852
3082
  const chunks = [];
2853
3083
  let settled = false;
2854
3084
  let abortPoll;
2855
- const done = (fn) => { if (settled)
2856
- return; settled = true; if (abortPoll)
2857
- clearInterval(abortPoll); fn(); };
3085
+ const done = (fn) => {
3086
+ if (settled)
3087
+ return;
3088
+ settled = true;
3089
+ if (abortPoll)
3090
+ clearInterval(abortPoll);
3091
+ fn();
3092
+ };
2858
3093
  const req = transport.get({ hostname: u.hostname, path: u.pathname + u.search, headers, timeout: timeoutMs }, (res) => {
2859
3094
  // Follow redirects once
2860
- if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) && res.headers.location) {
3095
+ if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) &&
3096
+ res.headers.location) {
2861
3097
  req.destroy();
2862
- done(() => { _httpsGet(new URL(res.headers.location, url).href, headers, timeoutMs, abortSignal).then(resolve, reject); });
3098
+ done(() => {
3099
+ _httpsGet(new URL(res.headers.location, url).href, headers, timeoutMs, abortSignal).then(resolve, reject);
3100
+ });
2863
3101
  return;
2864
3102
  }
2865
3103
  res.on('data', (c) => chunks.push(c));
2866
- res.on('end', () => done(() => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks), contentType: res.headers['content-type'] ?? '' })));
3104
+ res.on('end', () => done(() => resolve({
3105
+ status: res.statusCode ?? 0,
3106
+ body: Buffer.concat(chunks),
3107
+ contentType: res.headers['content-type'] ?? '',
3108
+ })));
3109
+ });
3110
+ req.on('timeout', () => {
3111
+ req.destroy();
3112
+ done(() => reject(new Error(`Download timed out after ${timeoutMs}ms`)));
2867
3113
  });
2868
- req.on('timeout', () => { req.destroy(); done(() => reject(new Error(`Download timed out after ${timeoutMs}ms`))); });
2869
3114
  req.on('error', (err) => done(() => reject(err)));
2870
3115
  if (abortSignal) {
2871
3116
  abortPoll = setInterval(() => {
@@ -2887,8 +3132,7 @@ function _httpsGet(url, headers, timeoutMs = 20000, abortSignal) {
2887
3132
  async function generateImage(input, workDir, abortSignal) {
2888
3133
  const prompt = typeof input.prompt === 'string' ? input.prompt.trim() : '';
2889
3134
  const relPath = typeof input.path === 'string' ? input.path.trim() : '';
2890
- const aspectRatio = typeof input.aspect_ratio === 'string' && VALID_ASPECT_RATIOS.has(input.aspect_ratio)
2891
- ? input.aspect_ratio : '1:1';
3135
+ const aspectRatio = typeof input.aspect_ratio === 'string' && VALID_ASPECT_RATIOS.has(input.aspect_ratio) ? input.aspect_ratio : '1:1';
2892
3136
  if (!prompt)
2893
3137
  return { error: 'Missing required parameter: prompt' };
2894
3138
  if (!relPath)
@@ -2909,7 +3153,9 @@ async function generateImage(input, workDir, abortSignal) {
2909
3153
  try {
2910
3154
  json = JSON.parse(r.body);
2911
3155
  }
2912
- catch (_) { /* body may be absent or non-JSON */ }
3156
+ catch (_) {
3157
+ /* body may be absent or non-JSON */
3158
+ }
2913
3159
  if (r.status !== 200)
2914
3160
  return { error: json.error || `Server error ${r.status}` };
2915
3161
  imageUrl = json.url ?? '';
@@ -2928,19 +3174,26 @@ async function generateImage(input, workDir, abortSignal) {
2928
3174
  try {
2929
3175
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
2930
3176
  }
2931
- catch (_) { /* best-effort */ }
3177
+ catch (_) {
3178
+ /* best-effort */
3179
+ }
2932
3180
  try {
2933
3181
  const dl = await _httpsGet(imageUrl, { 'User-Agent': FETCH_USER_AGENT }, 60000, abortSignal);
2934
3182
  if (dl.status !== 200)
2935
3183
  return { error: `Failed to download generated image (HTTP ${dl.status}).` };
2936
3184
  fs.writeFileSync(destPath, dl.body);
2937
3185
  const kb = (dl.body.length / 1024).toFixed(1);
2938
- const costNote = cost != null ? ` ($${cost.toFixed(2)} charged` + (balance != null ? `, balance $${balance.toFixed(2)})` : ')') : '';
3186
+ const costNote = cost != null
3187
+ ? ` ($${cost.toFixed(2)} charged` + (balance != null ? `, balance $${balance.toFixed(2)})` : ')')
3188
+ : '';
2939
3189
  return { output: `Generated and saved image to ${destPath} (${kb} KB)${costNote}.` };
2940
3190
  }
2941
3191
  catch (err) {
2942
3192
  if (err.name === 'AbortError')
2943
- return { error: 'generate_image stopped by user (image was generated and billed, but the download was interrupted)', interrupted: true };
3193
+ return {
3194
+ error: 'generate_image stopped by user (image was generated and billed, but the download was interrupted)',
3195
+ interrupted: true,
3196
+ };
2944
3197
  return { error: `Failed to save image to ${destPath}: ${err.message}` };
2945
3198
  }
2946
3199
  }
@@ -2973,7 +3226,9 @@ async function stockPhoto(input, workDir, abortSignal) {
2973
3226
  try {
2974
3227
  json = JSON.parse(r.body.toString('utf-8'));
2975
3228
  }
2976
- catch (_) { /* body may be absent or non-JSON */ }
3229
+ catch (_) {
3230
+ /* body may be absent or non-JSON */
3231
+ }
2977
3232
  if (r.status !== 200)
2978
3233
  return { error: json.error || `Server error ${r.status}` };
2979
3234
  photos = (json.photos ?? []);
@@ -2994,7 +3249,9 @@ async function stockPhoto(input, workDir, abortSignal) {
2994
3249
  try {
2995
3250
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
2996
3251
  }
2997
- catch (_) { /* best-effort */ }
3252
+ catch (_) {
3253
+ /* best-effort */
3254
+ }
2998
3255
  try {
2999
3256
  const dl = await _httpsGet(top.url, { 'User-Agent': FETCH_USER_AGENT }, 20000, abortSignal);
3000
3257
  if (dl.status !== 200)
@@ -3044,7 +3301,9 @@ async function webSearch(input, _workDir, abortSignal) {
3044
3301
  try {
3045
3302
  json = JSON.parse(r.body);
3046
3303
  }
3047
- catch (_) { /* body may be absent or non-JSON */ }
3304
+ catch (_) {
3305
+ /* body may be absent or non-JSON */
3306
+ }
3048
3307
  if (r.status !== 200)
3049
3308
  return { error: json.error || `Server error ${r.status}` };
3050
3309
  const text = json.text ?? '';
@@ -3108,7 +3367,9 @@ async function useSkill(input, workDir) {
3108
3367
  return { error: 'name is required (the skill to invoke)' };
3109
3368
  const dir = workDir ?? process.cwd();
3110
3369
  const skills = (0, skills_1.loadSkills)(dir);
3111
- const skill = (0, skills_1.findSkill)(skills, name);
3370
+ // `<dir>:<name>` = a nested skill (see skills.ts); the list above only holds
3371
+ // top-level ones, so a scoped miss falls through to the name-driven lookup.
3372
+ const skill = (0, skills_1.findSkill)(skills, name) ?? (0, skills_1.resolveNestedSkill)(dir, name);
3112
3373
  if (!skill) {
3113
3374
  const known = skills.map((s) => s.name).join(', ') || '(none defined)';
3114
3375
  return { error: `Unknown skill "${name}". Available: ${known}.` };
@@ -3133,7 +3394,12 @@ function semanticPreamble(input, workDir) {
3133
3394
  return { ok: false, err: { error: e.message } };
3134
3395
  }
3135
3396
  if (!(0, tsLangService_1.isTsLike)(resolved)) {
3136
- return { ok: false, err: { error: `Semantic navigation currently supports TypeScript/JavaScript only. For ${path.extname(resolved) || 'this file type'} use get_symbols / get_workspace_symbols (regex-based).` } };
3397
+ return {
3398
+ ok: false,
3399
+ err: {
3400
+ error: `Semantic navigation currently supports TypeScript/JavaScript only. For ${path.extname(resolved) || 'this file type'} use get_symbols / get_workspace_symbols (regex-based).`,
3401
+ },
3402
+ };
3137
3403
  }
3138
3404
  return { ok: true, args: { workDir: workDir ?? process.cwd(), filePath: resolved, line, character } };
3139
3405
  }
@@ -3211,14 +3477,34 @@ const TOOL_MAP = {
3211
3477
  // their own dedicated branch above (which passes abortSignal too) — kept out
3212
3478
  // of this set so there's exactly one dispatch path per tool, not two.
3213
3479
  const WORKDIR_TOOLS = new Set([
3214
- 'read_file', 'write_file', 'write_docx', 'write_xlsx', 'write_pptx',
3215
- 'list_directory', 'search_files', 'glob',
3216
- 'create_directory', 'edit_file', 'multi_edit', 'copy_file', 'move_file',
3217
- 'delete_file', 'notebook_read', 'notebook_edit',
3218
- 'get_symbols', 'get_workspace_symbols',
3219
- 'go_to_definition', 'find_references', 'get_hover',
3220
- 'memory_write', 'memory_read', 'use_skill',
3221
- 'list_shared_tasks', 'create_shared_task', 'claim_shared_task', 'update_shared_task_status',
3480
+ 'read_file',
3481
+ 'write_file',
3482
+ 'write_docx',
3483
+ 'write_xlsx',
3484
+ 'write_pptx',
3485
+ 'list_directory',
3486
+ 'search_files',
3487
+ 'glob',
3488
+ 'create_directory',
3489
+ 'edit_file',
3490
+ 'multi_edit',
3491
+ 'copy_file',
3492
+ 'move_file',
3493
+ 'delete_file',
3494
+ 'notebook_read',
3495
+ 'notebook_edit',
3496
+ 'get_symbols',
3497
+ 'get_workspace_symbols',
3498
+ 'go_to_definition',
3499
+ 'find_references',
3500
+ 'get_hover',
3501
+ 'memory_write',
3502
+ 'memory_read',
3503
+ 'use_skill',
3504
+ 'list_shared_tasks',
3505
+ 'create_shared_task',
3506
+ 'claim_shared_task',
3507
+ 'update_shared_task_status',
3222
3508
  ]);
3223
3509
  async function executeTool(name, input, abortSignal, sandbox, workDir, agentScope, onStream,
3224
3510
  // This session's own peer identity (see agent/peerRegistry.ts), if it is