@nexrall/code-core 1.4.70 → 1.4.72

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 (125) 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 +15 -7
  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 -74
  21. package/dist/agent/iterationPolicy.d.ts.map +1 -1
  22. package/dist/agent/iterationPolicy.js +28 -25
  23. package/dist/agent/lifecycleHost.d.ts.map +1 -1
  24. package/dist/agent/lifecycleHost.js +36 -12
  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 +223 -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 +5 -3
  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.map +1 -1
  66. package/dist/api/client.js +67 -32
  67. package/dist/auth/index.d.ts.map +1 -1
  68. package/dist/auth/index.js +3 -1
  69. package/dist/checkpoint/manager.d.ts.map +1 -1
  70. package/dist/checkpoint/manager.js +23 -8
  71. package/dist/commands/loader.d.ts.map +1 -1
  72. package/dist/commands/loader.js +6 -1
  73. package/dist/index.d.ts +2 -0
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +2 -0
  76. package/dist/mcp/client.d.ts +34 -0
  77. package/dist/mcp/client.d.ts.map +1 -1
  78. package/dist/mcp/client.js +89 -24
  79. package/dist/mcp/httpClient.d.ts +24 -1
  80. package/dist/mcp/httpClient.d.ts.map +1 -1
  81. package/dist/mcp/httpClient.js +80 -14
  82. package/dist/mcp/instructions.d.ts +11 -0
  83. package/dist/mcp/instructions.d.ts.map +1 -0
  84. package/dist/mcp/instructions.js +42 -0
  85. package/dist/mcp/manager.d.ts +24 -0
  86. package/dist/mcp/manager.d.ts.map +1 -1
  87. package/dist/mcp/manager.js +117 -24
  88. package/dist/mcp/server.d.ts.map +1 -1
  89. package/dist/mcp/server.js +48 -14
  90. package/dist/mcp/sseClient.d.ts +9 -1
  91. package/dist/mcp/sseClient.d.ts.map +1 -1
  92. package/dist/mcp/sseClient.js +43 -10
  93. package/dist/mcp/stats.d.ts.map +1 -1
  94. package/dist/permissions/bashClassify.d.ts.map +1 -1
  95. package/dist/permissions/bashClassify.js +25 -3
  96. package/dist/permissions/destructive.d.ts.map +1 -1
  97. package/dist/permissions/destructive.js +7 -2
  98. package/dist/permissions/destructiveTokens.d.ts.map +1 -1
  99. package/dist/permissions/destructiveTokens.js +104 -24
  100. package/dist/permissions/modePolicy.d.ts.map +1 -1
  101. package/dist/permissions/modePolicy.js +40 -17
  102. package/dist/permissions/rules.d.ts.map +1 -1
  103. package/dist/permissions/rules.js +7 -5
  104. package/dist/plugins/data.d.ts.map +1 -1
  105. package/dist/plugins/data.js +6 -2
  106. package/dist/plugins/index.d.ts.map +1 -1
  107. package/dist/plugins/index.js +18 -7
  108. package/dist/plugins/installer.d.ts.map +1 -1
  109. package/dist/plugins/installer.js +17 -10
  110. package/dist/plugins/sources.d.ts.map +1 -1
  111. package/dist/plugins/sources.js +9 -3
  112. package/dist/tools/executor.d.ts.map +1 -1
  113. package/dist/tools/executor.js +470 -173
  114. package/dist/tools/sandbox.d.ts.map +1 -1
  115. package/dist/tools/sandbox.js +5 -1
  116. package/dist/tools/symbols.d.ts.map +1 -1
  117. package/dist/tools/symbols.js +30 -8
  118. package/dist/tools/tsLangService.d.ts.map +1 -1
  119. package/dist/tools/tsLangService.js +12 -4
  120. package/dist/types.d.ts +17 -0
  121. package/dist/types.d.ts.map +1 -1
  122. package/dist/util/frontmatter.d.ts.map +1 -1
  123. package/dist/util/miniYaml.d.ts.map +1 -1
  124. package/dist/util/miniYaml.js +4 -2
  125. 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,20 +536,29 @@ 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
- // Loaded lazily so the (fairly large) docx/exceljs/pptxgenjs bundles only
527
- // ever get pulled into memory for a session that actually uses them —
528
- // most agent sessions never touch Office files at all.
529
- const { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType, } = await Promise.resolve().then(() => __importStar(require('docx')));
544
+ // Loaded lazily so the (fairly large) docx/exceljs/pptxgenjs packages
545
+ // only ever get pulled in for a session that actually uses Office files -
546
+ // most agent sessions never touch them at all. How they load depends on
547
+ // the client: esbuild-bundled clients (VS Code, desktop) inline them,
548
+ // while the CLI keeps all three external as runtime dependencies (see
549
+ // packages/cli/build.js) so their real versions stay visible to audits.
550
+ const { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType } = await Promise.resolve().then(() => __importStar(require('docx')));
530
551
  const headingMap = {
531
- 1: HeadingLevel.HEADING_1, 2: HeadingLevel.HEADING_2,
532
- 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,
533
556
  };
534
557
  const alignMap = {
535
- left: AlignmentType.LEFT, center: AlignmentType.CENTER,
536
- right: AlignmentType.RIGHT, justify: AlignmentType.JUSTIFIED,
558
+ left: AlignmentType.LEFT,
559
+ center: AlignmentType.CENTER,
560
+ right: AlignmentType.RIGHT,
561
+ justify: AlignmentType.JUSTIFIED,
537
562
  };
538
563
  const children = [];
539
564
  if (title) {
@@ -554,7 +579,9 @@ async function writeDocx(input, workDir) {
554
579
  const resolved = resolvePath(filePath, workDir);
555
580
  const isNew = !fs.existsSync(resolved);
556
581
  atomicWriteBuffer(resolved, buffer);
557
- 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
+ };
558
585
  }
559
586
  catch (err) {
560
587
  return { error: `Failed to write .docx: ${err.message}` };
@@ -574,7 +601,14 @@ async function writeXlsx(input, workDir) {
574
601
  // rather than only ever creating brand-new files.
575
602
  const mode = input.mode === 'edit' ? 'edit' : 'create';
576
603
  try {
577
- const ExcelJS = await Promise.resolve().then(() => __importStar(require('exceljs')));
604
+ const exceljsModule = await Promise.resolve().then(() => __importStar(require('exceljs')));
605
+ // exceljs is CJS: esbuild-inlined clients get its exports on the import
606
+ // namespace directly, while Node loading the external package from
607
+ // node_modules (core's own tsc build; the CLI, which depends on it
608
+ // instead of inlining - see packages/cli/build.js) exposes them on
609
+ // `.default`. Prefer `.default` when present so `new ExcelJS.Workbook()`
610
+ // below works in BOTH loading modes.
611
+ const ExcelJS = exceljsModule.default ?? exceljsModule;
578
612
  const resolved = resolvePath(filePath, workDir);
579
613
  const isNew = !fs.existsSync(resolved);
580
614
  const workbook = new ExcelJS.Workbook();
@@ -587,7 +621,9 @@ async function writeXlsx(input, workDir) {
587
621
  }
588
622
  }
589
623
  else if (mode === 'edit' && isNew) {
590
- 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
+ };
591
627
  }
592
628
  let sheetsTouched = 0;
593
629
  for (const raw of sheetsRaw) {
@@ -665,7 +701,9 @@ async function writePptx(input, workDir) {
665
701
  // can still go through the same atomic temp+rename write as the other two.
666
702
  const buffer = (await pres.write({ outputType: 'nodebuffer' }));
667
703
  atomicWriteBuffer(resolved, buffer);
668
- 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
+ };
669
707
  }
670
708
  catch (err) {
671
709
  return { error: `Failed to write .pptx: ${err.message}` };
@@ -742,11 +780,26 @@ function startBackgroundShell(command, displayCommand, workDir, note) {
742
780
  // unconditional `read` guard, an interactive installer step) blocks
743
781
  // forever instead of getting an immediate EOF, and the "shell" just sits
744
782
  // there as 'running' with zero output forever.
745
- 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
+ });
746
790
  const shell = {
747
- id, command: displayCommand, child, output: '', readCursor: 0, truncated: false,
748
- status: 'running', exitCode: null,
749
- 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,
750
803
  };
751
804
  // Rolling buffer: keep the LAST MAX_OUTPUT_CHARS instead of the first, so a
752
805
  // long-lived watcher/dev-server's recent output (the useful part) is never
@@ -780,7 +833,9 @@ function startBackgroundShell(command, displayCommand, workDir, note) {
780
833
  if (buf.length > room)
781
834
  shell.spillFull = true;
782
835
  }
783
- catch { /* degrade to inline-only */ }
836
+ catch {
837
+ /* degrade to inline-only */
838
+ }
784
839
  }
785
840
  }
786
841
  }
@@ -815,7 +870,9 @@ function startBackgroundShell(command, displayCommand, workDir, note) {
815
870
  try {
816
871
  fs.closeSync(shell.spillFd);
817
872
  }
818
- catch { /* ignore */ }
873
+ catch {
874
+ /* ignore */
875
+ }
819
876
  shell.spillFd = null;
820
877
  }
821
878
  // Evict oldest exited shell once map grows past 50 to prevent session-long leak
@@ -828,10 +885,15 @@ function startBackgroundShell(command, displayCommand, workDir, note) {
828
885
  }
829
886
  }
830
887
  });
831
- 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
+ });
832
892
  _bgShells.set(id, shell);
833
893
  const noteLine = note ? `\n[${note}]` : '';
834
- 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
+ };
835
897
  }
836
898
  async function bashOutput(input) {
837
899
  const id = typeof input.shell_id === 'string' ? input.shell_id : '';
@@ -868,13 +930,17 @@ async function killShell(input) {
868
930
  try {
869
931
  shell.child.kill(sig);
870
932
  }
871
- catch { /* already dead */ }
933
+ catch {
934
+ /* already dead */
935
+ }
872
936
  }
873
937
  };
874
938
  killTree('SIGTERM');
875
939
  // Escalate to SIGKILL after 3 s if process group is still alive
876
- setTimeout(() => { if (shell.status === 'killed')
877
- killTree('SIGKILL'); }, 3000);
940
+ setTimeout(() => {
941
+ if (shell.status === 'killed')
942
+ killTree('SIGKILL');
943
+ }, 3000);
878
944
  }
879
945
  return { output: `Background shell ${id} ${shell.status}.` };
880
946
  }
@@ -961,7 +1027,9 @@ async function bash(input, abortSignal, sandbox, workDir, onStream) {
961
1027
  try {
962
1028
  child.kill(sig);
963
1029
  }
964
- catch { /* already dead */ }
1030
+ catch {
1031
+ /* already dead */
1032
+ }
965
1033
  }
966
1034
  };
967
1035
  // Keep HEAD + TAIL rather than head-only: test runners print the failure
@@ -1010,14 +1078,18 @@ async function bash(input, abortSignal, sandbox, workDir, onStream) {
1010
1078
  if (buf.length > room)
1011
1079
  spillFull = true;
1012
1080
  }
1013
- catch { /* disk full / closed — degrade to inline-only */ }
1081
+ catch {
1082
+ /* disk full / closed — degrade to inline-only */
1083
+ }
1014
1084
  };
1015
1085
  const closeSpill = () => {
1016
1086
  if (spillFd !== null) {
1017
1087
  try {
1018
1088
  fs.closeSync(spillFd);
1019
1089
  }
1020
- catch { /* ignore */ }
1090
+ catch {
1091
+ /* ignore */
1092
+ }
1021
1093
  spillFd = null;
1022
1094
  }
1023
1095
  };
@@ -1078,7 +1150,11 @@ async function bash(input, abortSignal, sandbox, workDir, onStream) {
1078
1150
  if (timedOut) {
1079
1151
  // Return the partial output alongside the error — it usually holds the
1080
1152
  // reason the command hung (a stuck test, a prompt, a slow step).
1081
- 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
+ }));
1082
1158
  }
1083
1159
  else if (abortSignal?.aborted) {
1084
1160
  resolve({ error: 'Command stopped by user', interrupted: true, output: body || undefined });
@@ -1106,7 +1182,9 @@ async function bash(input, abortSignal, sandbox, workDir, onStream) {
1106
1182
  try {
1107
1183
  onStream?.(text);
1108
1184
  }
1109
- catch { /* ignore */ }
1185
+ catch {
1186
+ /* ignore */
1187
+ }
1110
1188
  });
1111
1189
  child.stderr?.on('data', (chunk) => {
1112
1190
  const text = chunk.toString('utf-8');
@@ -1114,7 +1192,9 @@ async function bash(input, abortSignal, sandbox, workDir, onStream) {
1114
1192
  try {
1115
1193
  onStream?.(text);
1116
1194
  }
1117
- catch { /* ignore */ }
1195
+ catch {
1196
+ /* ignore */
1197
+ }
1118
1198
  });
1119
1199
  child.on('close', (code, signal) => done(code, signal));
1120
1200
  child.on('error', (err) => {
@@ -1128,8 +1208,10 @@ async function bash(input, abortSignal, sandbox, workDir, onStream) {
1128
1208
  if (settled)
1129
1209
  return;
1130
1210
  killTree('SIGTERM');
1131
- setTimeout(() => { if (!settled)
1132
- killTree('SIGKILL'); }, 3000);
1211
+ setTimeout(() => {
1212
+ if (!settled)
1213
+ killTree('SIGKILL');
1214
+ }, 3000);
1133
1215
  done(null, null, true);
1134
1216
  }, timeoutMs);
1135
1217
  child.on('close', () => clearTimeout(timer));
@@ -1163,7 +1245,9 @@ function capExternalOutput(output, label, maxChars = MAX_OUTPUT_CHARS) {
1163
1245
  fs.writeFileSync(spillPath, (0, safeSlice_1.sliceSafeEnd)(output, MAX_SPILL_BYTES));
1164
1246
  spillNote = ` Full output (${output.length} chars) saved to ${spillPath} — use read_file with offset/limit to inspect any section.`;
1165
1247
  }
1166
- catch { /* spill is best-effort */ }
1248
+ catch {
1249
+ /* spill is best-effort */
1250
+ }
1167
1251
  const headLen = Math.floor(maxChars * 0.7);
1168
1252
  const tailLen = maxChars - headLen;
1169
1253
  const omitted = output.length - headLen - tailLen;
@@ -1197,7 +1281,9 @@ function runCapture(cmd, args, opts) {
1197
1281
  try {
1198
1282
  child.kill('SIGTERM');
1199
1283
  }
1200
- catch { /* already gone */ }
1284
+ catch {
1285
+ /* already gone */
1286
+ }
1201
1287
  };
1202
1288
  const timer = setTimeout(() => kill('ETIMEDOUT'), opts.timeout);
1203
1289
  child.stdout.setEncoding('utf-8');
@@ -1214,10 +1300,15 @@ function runCapture(cmd, args, opts) {
1214
1300
  }
1215
1301
  stdout += chunk;
1216
1302
  });
1217
- child.stderr.on('data', (chunk) => { if (stderr.length < 64 * 1024)
1218
- stderr += chunk; });
1219
- child.on('error', (e) => { if (!error)
1220
- 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
+ });
1221
1312
  child.on('close', (code, signal) => finish(code, signal));
1222
1313
  });
1223
1314
  }
@@ -1258,27 +1349,33 @@ async function crossFileBreakageWarning(editedAbsPath, oldContent, newContent, w
1258
1349
  return '';
1259
1350
  let removed;
1260
1351
  try {
1261
- removed = (0, crossFile_1.findRemovedExports)(editedAbsPath, oldContent, newContent)
1262
- .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));
1263
1353
  }
1264
1354
  catch {
1265
1355
  return '';
1266
1356
  }
1267
1357
  if (!removed.length)
1268
1358
  return '';
1269
- const editedReal = (() => { try {
1270
- return fs.realpathSync(editedAbsPath);
1271
- }
1272
- catch {
1273
- return editedAbsPath;
1274
- } })();
1359
+ const editedReal = (() => {
1360
+ try {
1361
+ return fs.realpathSync(editedAbsPath);
1362
+ }
1363
+ catch {
1364
+ return editedAbsPath;
1365
+ }
1366
+ })();
1275
1367
  const hits = [];
1276
1368
  const MAX_SYMBOLS = 8;
1277
1369
  // Scans run concurrently (they used to be up to 8 sequential blocking rg calls).
1278
- 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) })));
1279
1373
  for (const { sym, refs } of scanned) {
1280
1374
  if (refs.length) {
1281
- 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');
1282
1379
  const more = refs.length > 3 ? `\n … and ${refs.length - 3} more` : '';
1283
1380
  hits.push(` • ${sym.kind} "${sym.name}" (removed/renamed) still referenced in:\n${shown}${more}`);
1284
1381
  }
@@ -1298,8 +1395,16 @@ async function scanReferences(name, workDir, excludeRealPath) {
1298
1395
  const opts = { cwd: workDir, timeout: 8000, maxBuffer: 8 * 1024 * 1024 };
1299
1396
  const r = ripgrepAvailable()
1300
1397
  ? await runCapture(rgBin(), ['-l', '--no-messages', '-e', pattern, '.'], opts)
1301
- : await runCapture('grep', ['-rlI', '--exclude-dir=node_modules', '--exclude-dir=.git',
1302
- '--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);
1303
1408
  if (r.stdout)
1304
1409
  for (const line of r.stdout.split('\n')) {
1305
1410
  if (line.trim())
@@ -1316,7 +1421,9 @@ async function scanReferences(name, workDir, excludeRealPath) {
1316
1421
  try {
1317
1422
  real = fs.realpathSync(abs);
1318
1423
  }
1319
- catch { /* keep abs */ }
1424
+ catch {
1425
+ /* keep abs */
1426
+ }
1320
1427
  if (real === excludeRealPath)
1321
1428
  continue; // the file we just edited
1322
1429
  rel.push(path.relative(workDir, abs) || f);
@@ -1331,7 +1438,7 @@ async function searchFiles(input, workDir) {
1331
1438
  // Default is "files" (paths of matching files only), like Claude Code's Grep
1332
1439
  // files_with_matches: a content dump of a common pattern was ~50× larger. Asking for
1333
1440
  // context_lines implies the caller wants content.
1334
- 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';
1335
1442
  const filesOnly = searchType === 'files' || searchType === 'files_with_matches';
1336
1443
  const include = typeof input.include === 'string' ? input.include : ''; // e.g. "*.ts"
1337
1444
  if (!pattern)
@@ -1353,18 +1460,24 @@ async function searchFiles(input, workDir) {
1353
1460
  const rel = path.relative(resolved, filePath).replace(/\\/g, '/');
1354
1461
  const relLower = rel.toLowerCase();
1355
1462
  // Support glob-style wildcards (* and ?) in addition to substring + regex
1356
- if (nameLower.includes(patternLower) || matchesGlob(name, pattern) || matchesPattern(name, pattern) ||
1357
- 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)) {
1358
1469
  matches.push(filePath);
1359
1470
  }
1360
1471
  });
1361
1472
  if (matches.length === 0)
1362
1473
  return { output: 'No matching files found.' };
1363
1474
  const relBase = workDir ? path.resolve(workDir) : process.cwd();
1364
- let out = matches.map((f) => {
1475
+ let out = matches
1476
+ .map((f) => {
1365
1477
  const r = path.relative(relBase, f);
1366
1478
  return r.startsWith('..') || path.isAbsolute(r) ? f : r.replace(/\\/g, '/');
1367
- }).join('\n');
1479
+ })
1480
+ .join('\n');
1368
1481
  if (out.length > MAX_OUTPUT_CHARS)
1369
1482
  out = (0, safeSlice_1.sliceSafeEnd)(out, MAX_OUTPUT_CHARS) + '\n[Truncated]';
1370
1483
  if (listing.truncated)
@@ -1384,15 +1497,24 @@ async function searchFiles(input, workDir) {
1384
1497
  const base = workDir ? path.resolve(workDir) : process.cwd();
1385
1498
  const relTarget = path.relative(base, resolved);
1386
1499
  const inside = !relTarget.startsWith('..') && !path.isAbsolute(relTarget);
1387
- const target = inside ? (relTarget || '.') : resolved;
1500
+ const target = inside ? relTarget || '.' : resolved;
1388
1501
  const spawnOpts = { timeout: DEFAULT_TIMEOUT_MS, maxBuffer: SEARCH_MAX_BUFFER, cwd: base };
1389
1502
  let result;
1390
1503
  if (ripgrepAvailable()) {
1391
1504
  const args = filesOnly
1392
1505
  ? ['--files-with-matches', '--color=never', '--hidden', '--glob', '!.git']
1393
- : ['--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',
1394
1513
  // One minified-bundle hit could otherwise fill the whole output cap.
1395
- '--max-columns', '500', '--max-columns-preview'];
1514
+ '--max-columns',
1515
+ '500',
1516
+ '--max-columns-preview',
1517
+ ];
1396
1518
  if (ignoreCase)
1397
1519
  args.push('-i');
1398
1520
  if (contextLines > 0 && !filesOnly)
@@ -1436,12 +1558,16 @@ async function searchFiles(input, workDir) {
1436
1558
  // first misreported a too-large result as a timeout.
1437
1559
  if (spawnErr.code === 'ETIMEDOUT' || (result.signal === 'SIGTERM' && spawnErr.code !== 'ENOBUFS')) {
1438
1560
  const partial = output ? `\n\nPartial results before timeout:\n${globalCapMatches(output)}` : '';
1439
- 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
+ };
1440
1564
  }
1441
1565
  if (spawnErr.code === 'ENOBUFS') {
1442
1566
  // We still captured up to maxBuffer of output — return the capped head
1443
1567
  // instead of failing blind.
1444
- 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
+ };
1445
1571
  }
1446
1572
  return { error: `search failed: ${spawnErr.message}` };
1447
1573
  }
@@ -1465,10 +1591,12 @@ async function searchFiles(input, workDir) {
1465
1591
  return { output: 'No matches found.' };
1466
1592
  const shown = files.slice(0, MAX_GLOB_RESULTS);
1467
1593
  const more = files.length > shown.length
1468
- ? `\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
+ : '';
1469
1596
  return {
1470
1597
  output: `${files.length} file(s) contain matches (type="content" for the matching lines):\n` +
1471
- shown.join('\n') + more,
1598
+ shown.join('\n') +
1599
+ more,
1472
1600
  };
1473
1601
  }
1474
1602
  output = globalCapMatches(output);
@@ -1505,7 +1633,12 @@ function globalCapMatches(output) {
1505
1633
  function matchesGlob(name, pattern) {
1506
1634
  // Convert simple glob (* → .*, ? → .) to regex
1507
1635
  try {
1508
- 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');
1509
1642
  return re.test(name);
1510
1643
  }
1511
1644
  catch {
@@ -1528,9 +1661,27 @@ function matchesPattern(name, pattern) {
1528
1661
  // Noise directories never worth walking. .venv/venv matter most: a single Python virtualenv
1529
1662
  // is hundreds of MB of third-party files that drowned glob / filename-search results.
1530
1663
  const WALK_SKIP_DIRS = new Set([
1531
- '.git', 'node_modules', 'dist', '.next', '__pycache__', '.turbo', 'coverage',
1532
- '.venv', 'venv', '.tox', '.mypy_cache', '.pytest_cache', '.ruff_cache', '.gradle',
1533
- '.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',
1534
1685
  ]);
1535
1686
  /** Hard ceiling on files visited by the fallback directory walk (no ripgrep). */
1536
1687
  const WALK_MAX_FILES = 50000;
@@ -1561,7 +1712,10 @@ async function listFiles(root) {
1561
1712
  // Exit 2 = "some path errored" (a symlink loop, an unreadable dir) — the list of
1562
1713
  // everything else is still complete, so keep it rather than re-walking the tree.
1563
1714
  if (!r.error && (r.status === 0 || r.status === 1 || (r.status === 2 && r.stdout))) {
1564
- 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(/^\.\//, '')));
1565
1719
  return { files, truncated: false };
1566
1720
  }
1567
1721
  // Anything else (timeout, overflow, spawn failure): fall through to the walk.
@@ -1593,14 +1747,16 @@ function walkDir(dirPath, callback, _visited = new Set()) {
1593
1747
  const fullPath = path.join(dirPath, entry.name);
1594
1748
  // isDirectory() returns false for symlinks — use isSymbolicLink() to detect
1595
1749
  // symlinked directories and walk them too (with cycle protection above).
1596
- const isDir = entry.isDirectory() || (entry.isSymbolicLink() && (() => {
1597
- try {
1598
- return fs.statSync(fullPath).isDirectory();
1599
- }
1600
- catch {
1601
- return false;
1602
- }
1603
- })());
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
+ })());
1604
1760
  if (isDir) {
1605
1761
  // Skip common noise directories
1606
1762
  if (WALK_SKIP_DIRS.has(entry.name))
@@ -1705,7 +1861,9 @@ function preflightEdit(resolved) {
1705
1861
  if (stat.isDirectory())
1706
1862
  return { error: `Path is a directory, not a file: ${resolved}.` };
1707
1863
  if (stat.size > MAX_EDIT_BYTES) {
1708
- 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
+ };
1709
1867
  }
1710
1868
  if (isBinaryFile(resolved)) {
1711
1869
  return { error: `Refusing to edit binary file ${resolved} — a text replace would corrupt it.` };
@@ -1772,7 +1930,9 @@ function atomicWrite(resolved, data, mode) {
1772
1930
  try {
1773
1931
  fs.chmodSync(target, mode);
1774
1932
  }
1775
- catch { /* non-fatal */ }
1933
+ catch {
1934
+ /* non-fatal */
1935
+ }
1776
1936
  }
1777
1937
  return;
1778
1938
  }
@@ -1781,7 +1941,9 @@ function atomicWrite(resolved, data, mode) {
1781
1941
  try {
1782
1942
  fs.rmSync(tmp, { force: true });
1783
1943
  }
1784
- catch { /* best effort */ }
1944
+ catch {
1945
+ /* best effort */
1946
+ }
1785
1947
  throw err;
1786
1948
  }
1787
1949
  // ── Commit ───────────────────────────────────────────────────────────────
@@ -1790,7 +1952,9 @@ function atomicWrite(resolved, data, mode) {
1790
1952
  try {
1791
1953
  fs.chmodSync(tmp, mode);
1792
1954
  }
1793
- catch { /* non-fatal — keep going */ }
1955
+ catch {
1956
+ /* non-fatal — keep going */
1957
+ }
1794
1958
  }
1795
1959
  fs.renameSync(tmp, target);
1796
1960
  }
@@ -1801,7 +1965,9 @@ function atomicWrite(resolved, data, mode) {
1801
1965
  try {
1802
1966
  fs.rmSync(tmp, { force: true });
1803
1967
  }
1804
- catch { /* best effort */ }
1968
+ catch {
1969
+ /* best effort */
1970
+ }
1805
1971
  throw err;
1806
1972
  }
1807
1973
  }
@@ -1837,7 +2003,9 @@ async function editFile(input, workDir) {
1837
2003
  return { error: `old_string not found in ${resolved}. Read the file first to verify the exact text to replace.` };
1838
2004
  }
1839
2005
  if (count > 1) {
1840
- 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
+ };
1841
2009
  }
1842
2010
  let updated = literalReplace(origNorm, oldNorm, newNorm);
1843
2011
  if (wasCRLF)
@@ -1899,7 +2067,9 @@ async function deleteFile(input, workDir) {
1899
2067
  const resolved = resolvePath(filePath, workDir);
1900
2068
  const stat = fs.statSync(resolved);
1901
2069
  if (stat.isDirectory()) {
1902
- 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
+ };
1903
2073
  }
1904
2074
  fs.unlinkSync(resolved);
1905
2075
  return { output: `Deleted: ${resolved}` };
@@ -1910,7 +2080,7 @@ async function deleteFile(input, workDir) {
1910
2080
  }
1911
2081
  // Strip HTML tags → readable plain text (like Claude Code's WebFetch)
1912
2082
  function stripHtml(html) {
1913
- return html
2083
+ return (html
1914
2084
  // Remove <script> and <style> blocks entirely
1915
2085
  .replace(/<script[\s\S]*?<\/script>/gi, '')
1916
2086
  .replace(/<style[\s\S]*?<\/style>/gi, '')
@@ -1925,10 +2095,15 @@ function stripHtml(html) {
1925
2095
  // Remove all remaining tags
1926
2096
  .replace(/<[^>]+>/g, '')
1927
2097
  // Decode common HTML entities
1928
- .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, ' ')
1929
2104
  // Collapse 3+ blank lines → 2
1930
2105
  .replace(/\n{3,}/g, '\n\n')
1931
- .trim();
2106
+ .trim());
1932
2107
  }
1933
2108
  const MAX_REDIRECTS = 10;
1934
2109
  const FETCH_USER_AGENT = 'Nexrall-Code/1.0 (+https://nexrall.com)';
@@ -2048,7 +2223,10 @@ function fetchBlocklistCheck(parsedUrl) {
2048
2223
  if (!raw)
2049
2224
  return null;
2050
2225
  const host = parsedUrl.hostname.toLowerCase().replace(/^\[|\]$/g, '');
2051
- 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);
2052
2230
  for (const entry of entries) {
2053
2231
  if (host === entry || host.endsWith(`.${entry}`)) {
2054
2232
  return `Refusing to fetch "${host}" — blocked by NEXRALL_FETCH_BLOCKLIST (matched "${entry}").`;
@@ -2080,9 +2258,14 @@ async function fetchUrlTool(input, workDir, abortSignal) {
2080
2258
  try {
2081
2259
  json = JSON.parse(r.body);
2082
2260
  }
2083
- catch { /* fall through */ }
2261
+ catch {
2262
+ /* fall through */
2263
+ }
2084
2264
  if (r.status !== 200 || !json.text) {
2085
- 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
+ };
2086
2269
  }
2087
2270
  return {
2088
2271
  output: `[Answer extracted from ${capture.url ?? input.url} by ${json.model ?? 'a low-cost model'} for: "${prompt.slice(0, 200)}"]\n\n` +
@@ -2092,7 +2275,10 @@ async function fetchUrlTool(input, workDir, abortSignal) {
2092
2275
  catch (err) {
2093
2276
  if (err.name === 'AbortError')
2094
2277
  return { error: 'fetch_url stopped by user', interrupted: true };
2095
- 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
+ };
2096
2282
  }
2097
2283
  }
2098
2284
  async function fetchUrl(input, _workDir, _redirectCount = 0, abortSignal,
@@ -2137,9 +2323,13 @@ capture) {
2137
2323
  return dns.lookup(hostname, options, (err, address, family) => {
2138
2324
  if (err)
2139
2325
  return callback(err, address, family);
2140
- 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];
2141
2329
  if (!allowLocal && addrs.some((a) => isPrivateIp(a))) {
2142
- 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);
2143
2333
  }
2144
2334
  return callback(null, address, family);
2145
2335
  });
@@ -2183,7 +2373,7 @@ capture) {
2183
2373
  headers: {
2184
2374
  'User-Agent': FETCH_USER_AGENT,
2185
2375
  'Accept-Encoding': 'identity', // opt-out of gzip/deflate — we read raw Buffer as UTF-8
2186
- 'Accept': 'text/html,text/plain,application/json,*/*',
2376
+ Accept: 'text/html,text/plain,application/json,*/*',
2187
2377
  },
2188
2378
  }, (res) => {
2189
2379
  // Follow redirects (301/302/307/308) with depth counter to prevent infinite loops
@@ -2200,7 +2390,7 @@ capture) {
2200
2390
  finish({ error: `Invalid redirect location: ${res.headers.location}` });
2201
2391
  return;
2202
2392
  }
2203
- fetchUrl({ url: nextUrl }, undefined, _redirectCount + 1, abortSignal, capture).then(finish);
2393
+ void fetchUrl({ url: nextUrl }, undefined, _redirectCount + 1, abortSignal, capture).then(finish);
2204
2394
  return;
2205
2395
  }
2206
2396
  const contentType = res.headers['content-type'] ?? '';
@@ -2227,7 +2417,9 @@ capture) {
2227
2417
  // (anyone can put "ignore previous instructions..." on a page). Wrap it in an
2228
2418
  // explicit untrusted-data delimiter so the model treats it as DATA to read, not
2229
2419
  // as instructions to follow — defense-in-depth alongside the system-prompt rule.
2230
- 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
+ });
2231
2423
  };
2232
2424
  res.on('data', (chunk) => {
2233
2425
  if (totalBytes + chunk.length > MAX_FETCH_BYTES) {
@@ -2251,11 +2443,15 @@ capture) {
2251
2443
  res.on('end', emitBody);
2252
2444
  // Ignore errors caused by our own intentional destroy (truncation / redirect);
2253
2445
  // only real transport failures should surface.
2254
- res.on('error', (err) => { if (!selfAborted)
2255
- 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 });
2256
2454
  });
2257
- req.on('error', (err) => { if (!selfAborted)
2258
- finish({ error: err.message }); });
2259
2455
  req.on('timeout', () => {
2260
2456
  selfAborted = true;
2261
2457
  req.destroy();
@@ -2284,10 +2480,10 @@ function buildDiff(filePath, oldStr, newStr, fileContent) {
2284
2480
  }
2285
2481
  }
2286
2482
  const lines = [`--- ${filePath}${hunkHeader}`, `+++ ${filePath}${hunkHeader}`];
2287
- oldLines.slice(0, MAX_SHOW).forEach(l => lines.push(`- ${l}`));
2483
+ oldLines.slice(0, MAX_SHOW).forEach((l) => lines.push(`- ${l}`));
2288
2484
  if (oldLines.length > MAX_SHOW)
2289
2485
  lines.push(` … (${oldLines.length - MAX_SHOW} more removed lines)`);
2290
- newLines.slice(0, MAX_SHOW).forEach(l => lines.push(`+ ${l}`));
2486
+ newLines.slice(0, MAX_SHOW).forEach((l) => lines.push(`+ ${l}`));
2291
2487
  if (newLines.length > MAX_SHOW)
2292
2488
  lines.push(` … (${newLines.length - MAX_SHOW} more added lines)`);
2293
2489
  return lines.join('\n');
@@ -2326,10 +2522,14 @@ async function multiEdit(input, workDir) {
2326
2522
  // indexOf-based count — safe for regex metacharacters like ( . $ * + ?
2327
2523
  const count = countOccurrences(content, oldStr);
2328
2524
  if (count === 0) {
2329
- 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
+ };
2330
2528
  }
2331
2529
  if (count > 1) {
2332
- 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
+ };
2333
2533
  }
2334
2534
  // literalReplace: $ signs in newStr are never treated as patterns
2335
2535
  // Pass the pre-replace content so line numbers reflect the file state at
@@ -2349,7 +2549,9 @@ async function multiEdit(input, workDir) {
2349
2549
  const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(insertedText.join('\n')));
2350
2550
  return {
2351
2551
  output: `Applied ${edits.length} edit(s) to ${resolved}:\n` +
2352
- 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,
2353
2555
  linesAdded,
2354
2556
  linesRemoved,
2355
2557
  };
@@ -2386,18 +2588,25 @@ async function glob(input, workDir) {
2386
2588
  return { output: `No files matched pattern: ${pattern}${scanNote}` };
2387
2589
  // Most recently modified first, capped — same shape as Claude Code's Glob. Paths are
2388
2590
  // relative to the search dir: absolute prefixes were ~25% of the output's tokens.
2389
- const mtime = (f) => { try {
2390
- return fs.statSync(f).mtimeMs;
2391
- }
2392
- catch {
2393
- return 0;
2394
- } };
2395
- 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);
2396
2603
  const shown = sorted.slice(0, MAX_GLOB_RESULTS).map((f) => path.relative(resolved, f).replace(/\\/g, '/'));
2397
2604
  const more = sorted.length > shown.length
2398
2605
  ? `\n[${sorted.length - shown.length} more not shown — narrow the pattern or path]`
2399
2606
  : '';
2400
- 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
+ };
2401
2610
  }
2402
2611
  catch (err) {
2403
2612
  return { error: err.message };
@@ -2478,7 +2687,7 @@ async function todoWrite(input, scopeKey) {
2478
2687
  for (const t of todos) {
2479
2688
  const item = t;
2480
2689
  const content = typeof item.content === 'string' ? item.content.trim() : '';
2481
- 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';
2482
2691
  if (!content)
2483
2692
  continue;
2484
2693
  scope.items.push({ id: scope.nextId++, content, status });
@@ -2495,8 +2704,8 @@ function formatTodos(scope) {
2495
2704
  if (!scope.items.length)
2496
2705
  return 'Task list is empty.';
2497
2706
  const icons = { pending: '☐', in_progress: '▶', completed: '✓' };
2498
- const lines = scope.items.map(t => `${icons[t.status] ?? '☐'} [${t.id}] ${t.content} (${t.status})`);
2499
- 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')}`;
2500
2709
  }
2501
2710
  // ─── Shared task list (cross-SESSION coordination — see agent/sharedTasks.ts) ──
2502
2711
  //
@@ -2545,7 +2754,7 @@ async function claimSharedTaskTool(input, workDir) {
2545
2754
  const reasons = {
2546
2755
  'not-found': `No shared task with id "${taskId}" exists for this project.`,
2547
2756
  'already-claimed': `Task "${taskId}" is already claimed by another session. Pick a different task, or check back later.`,
2548
- '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.`,
2549
2758
  'not-pending': `Task "${taskId}" is already completed.`,
2550
2759
  };
2551
2760
  return { error: reasons[res.reason] ?? `Could not claim task "${taskId}" (${res.reason}).` };
@@ -2568,7 +2777,9 @@ async function updateSharedTaskStatusTool(input, workDir) {
2568
2777
  if (res.reason === 'not-found')
2569
2778
  return { error: `No shared task with id "${taskId}" exists for this project.` };
2570
2779
  if (res.reason === 'already-claimed')
2571
- 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
+ };
2572
2783
  return { error: `Could not update task "${taskId}" (${res.reason}).` };
2573
2784
  }
2574
2785
  return { output: `Updated [${res.task.id}] ${res.task.content} → ${res.task.status}.` };
@@ -2603,7 +2814,9 @@ async function messagePeerSessionTool(input, selfPeer) {
2603
2814
  return { error: 'This session is not registered as a discoverable peer, so it cannot send messages.' };
2604
2815
  const target = (0, peerRegistry_1.findPeerByName)(targetName);
2605
2816
  if (!target)
2606
- 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
+ };
2607
2820
  if (target.id === selfPeer.id)
2608
2821
  return { error: 'Cannot message yourself.' };
2609
2822
  if (!target.socketPath)
@@ -2668,7 +2881,8 @@ function formatNbOutput(out) {
2668
2881
  lines.push(`[error] ${out.ename ?? 'Error'}: ${out.evalue ?? ''}`);
2669
2882
  if (out.traceback) {
2670
2883
  const clean = out.traceback
2671
- .map(l => l.replace(/\x1b\[[0-9;]*[mGKH]/g, ''))
2884
+ // eslint-disable-next-line no-control-regex -- ANSI parser: the ESC bytes are the grammar
2885
+ .map((l) => l.replace(/\x1b\[[0-9;]*[mGKH]/g, ''))
2672
2886
  .join('\n');
2673
2887
  lines.push((0, safeSlice_1.sliceSafeEnd)(clean, 1500) + (clean.length > 1500 ? '\n[truncated]' : ''));
2674
2888
  }
@@ -2697,7 +2911,9 @@ async function notebookRead(input, workDir) {
2697
2911
  nb.cells.forEach((cell, i) => {
2698
2912
  const src = nbJoin(cell.source);
2699
2913
  const execLabel = cell.cell_type === 'code'
2700
- ? (cell.execution_count != null ? ` (exec #${cell.execution_count})` : ' (not executed)')
2914
+ ? cell.execution_count != null
2915
+ ? ` (exec #${cell.execution_count})`
2916
+ : ' (not executed)'
2701
2917
  : '';
2702
2918
  lines.push('');
2703
2919
  lines.push(`${'─'.repeat(60)}`);
@@ -2707,7 +2923,7 @@ async function notebookRead(input, workDir) {
2707
2923
  if (cell.cell_type === 'code' && cell.outputs?.length) {
2708
2924
  lines.push('');
2709
2925
  lines.push('▶ Output:');
2710
- cell.outputs.forEach(o => {
2926
+ cell.outputs.forEach((o) => {
2711
2927
  const formatted = formatNbOutput(o);
2712
2928
  if (formatted)
2713
2929
  lines.push(formatted);
@@ -2716,7 +2932,9 @@ async function notebookRead(input, workDir) {
2716
2932
  });
2717
2933
  let output = lines.join('\n');
2718
2934
  if (output.length > MAX_OUTPUT_CHARS) {
2719
- 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]`;
2720
2938
  }
2721
2939
  return { output };
2722
2940
  }
@@ -2732,8 +2950,10 @@ async function notebookEdit(input, workDir) {
2732
2950
  const editType = typeof input.edit_type === 'string' ? input.edit_type : '';
2733
2951
  // Accept cell_index as number OR numeric string (model sometimes sends "0" as string)
2734
2952
  const rawIdx = input.cell_index;
2735
- const cellIndex = typeof rawIdx === 'number' ? Math.floor(rawIdx)
2736
- : 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)
2737
2957
  : -1;
2738
2958
  const source = typeof input.source === 'string' ? input.source : null;
2739
2959
  const cellType = (typeof input.cell_type === 'string' ? input.cell_type : 'code');
@@ -2763,14 +2983,18 @@ async function notebookEdit(input, workDir) {
2763
2983
  cell.execution_count = null;
2764
2984
  }
2765
2985
  save();
2766
- 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
+ };
2767
2989
  }
2768
2990
  if (editType === 'delete') {
2769
2991
  if (cellIndex < 0 || cellIndex >= n)
2770
2992
  return { error: `cell_index ${cellIndex} out of range (0–${n - 1})` };
2771
2993
  const removed = nb.cells.splice(cellIndex, 1)[0];
2772
2994
  save();
2773
- 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
+ };
2774
2998
  }
2775
2999
  // insert: inserts AFTER cell_index, or at end if -1
2776
3000
  if (source === null)
@@ -2784,7 +3008,9 @@ async function notebookEdit(input, workDir) {
2784
3008
  };
2785
3009
  nb.cells.splice(insertAt, 0, newCell);
2786
3010
  save();
2787
- 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
+ };
2788
3014
  }
2789
3015
  catch (err) {
2790
3016
  if (err instanceof SyntaxError)
@@ -2810,17 +3036,32 @@ function _httpsPost(url, body, headers, timeoutMs = 30000, abortSignal) {
2810
3036
  const chunks = [];
2811
3037
  let settled = false;
2812
3038
  let abortPoll;
2813
- const done = (fn) => { if (settled)
2814
- return; settled = true; if (abortPoll)
2815
- clearInterval(abortPoll); fn(); };
3039
+ const done = (fn) => {
3040
+ if (settled)
3041
+ return;
3042
+ settled = true;
3043
+ if (abortPoll)
3044
+ clearInterval(abortPoll);
3045
+ fn();
3046
+ };
2816
3047
  // http for an http:// API base (local dev / tests) — https.request would reject it —
2817
3048
  // and the explicit port, which was silently dropped (so :3000 went to :443).
2818
3049
  const mod = u.protocol === 'http:' ? http : https;
2819
- 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) => {
2820
3058
  res.on('data', (c) => chunks.push(c));
2821
3059
  res.on('end', () => done(() => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString('utf-8') })));
2822
3060
  });
2823
- 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
+ });
2824
3065
  req.on('error', (err) => done(() => reject(err)));
2825
3066
  if (abortSignal) {
2826
3067
  abortPoll = setInterval(() => {
@@ -2841,20 +3082,35 @@ function _httpsGet(url, headers, timeoutMs = 20000, abortSignal) {
2841
3082
  const chunks = [];
2842
3083
  let settled = false;
2843
3084
  let abortPoll;
2844
- const done = (fn) => { if (settled)
2845
- return; settled = true; if (abortPoll)
2846
- clearInterval(abortPoll); fn(); };
3085
+ const done = (fn) => {
3086
+ if (settled)
3087
+ return;
3088
+ settled = true;
3089
+ if (abortPoll)
3090
+ clearInterval(abortPoll);
3091
+ fn();
3092
+ };
2847
3093
  const req = transport.get({ hostname: u.hostname, path: u.pathname + u.search, headers, timeout: timeoutMs }, (res) => {
2848
3094
  // Follow redirects once
2849
- 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) {
2850
3097
  req.destroy();
2851
- 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
+ });
2852
3101
  return;
2853
3102
  }
2854
3103
  res.on('data', (c) => chunks.push(c));
2855
- 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`)));
2856
3113
  });
2857
- req.on('timeout', () => { req.destroy(); done(() => reject(new Error(`Download timed out after ${timeoutMs}ms`))); });
2858
3114
  req.on('error', (err) => done(() => reject(err)));
2859
3115
  if (abortSignal) {
2860
3116
  abortPoll = setInterval(() => {
@@ -2876,8 +3132,7 @@ function _httpsGet(url, headers, timeoutMs = 20000, abortSignal) {
2876
3132
  async function generateImage(input, workDir, abortSignal) {
2877
3133
  const prompt = typeof input.prompt === 'string' ? input.prompt.trim() : '';
2878
3134
  const relPath = typeof input.path === 'string' ? input.path.trim() : '';
2879
- const aspectRatio = typeof input.aspect_ratio === 'string' && VALID_ASPECT_RATIOS.has(input.aspect_ratio)
2880
- ? 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';
2881
3136
  if (!prompt)
2882
3137
  return { error: 'Missing required parameter: prompt' };
2883
3138
  if (!relPath)
@@ -2898,7 +3153,9 @@ async function generateImage(input, workDir, abortSignal) {
2898
3153
  try {
2899
3154
  json = JSON.parse(r.body);
2900
3155
  }
2901
- catch (_) { }
3156
+ catch (_) {
3157
+ /* body may be absent or non-JSON */
3158
+ }
2902
3159
  if (r.status !== 200)
2903
3160
  return { error: json.error || `Server error ${r.status}` };
2904
3161
  imageUrl = json.url ?? '';
@@ -2917,19 +3174,26 @@ async function generateImage(input, workDir, abortSignal) {
2917
3174
  try {
2918
3175
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
2919
3176
  }
2920
- catch (_) { }
3177
+ catch (_) {
3178
+ /* best-effort */
3179
+ }
2921
3180
  try {
2922
3181
  const dl = await _httpsGet(imageUrl, { 'User-Agent': FETCH_USER_AGENT }, 60000, abortSignal);
2923
3182
  if (dl.status !== 200)
2924
3183
  return { error: `Failed to download generated image (HTTP ${dl.status}).` };
2925
3184
  fs.writeFileSync(destPath, dl.body);
2926
3185
  const kb = (dl.body.length / 1024).toFixed(1);
2927
- 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
+ : '';
2928
3189
  return { output: `Generated and saved image to ${destPath} (${kb} KB)${costNote}.` };
2929
3190
  }
2930
3191
  catch (err) {
2931
3192
  if (err.name === 'AbortError')
2932
- 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
+ };
2933
3197
  return { error: `Failed to save image to ${destPath}: ${err.message}` };
2934
3198
  }
2935
3199
  }
@@ -2962,7 +3226,9 @@ async function stockPhoto(input, workDir, abortSignal) {
2962
3226
  try {
2963
3227
  json = JSON.parse(r.body.toString('utf-8'));
2964
3228
  }
2965
- catch (_) { }
3229
+ catch (_) {
3230
+ /* body may be absent or non-JSON */
3231
+ }
2966
3232
  if (r.status !== 200)
2967
3233
  return { error: json.error || `Server error ${r.status}` };
2968
3234
  photos = (json.photos ?? []);
@@ -2983,7 +3249,9 @@ async function stockPhoto(input, workDir, abortSignal) {
2983
3249
  try {
2984
3250
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
2985
3251
  }
2986
- catch (_) { }
3252
+ catch (_) {
3253
+ /* best-effort */
3254
+ }
2987
3255
  try {
2988
3256
  const dl = await _httpsGet(top.url, { 'User-Agent': FETCH_USER_AGENT }, 20000, abortSignal);
2989
3257
  if (dl.status !== 200)
@@ -3033,7 +3301,9 @@ async function webSearch(input, _workDir, abortSignal) {
3033
3301
  try {
3034
3302
  json = JSON.parse(r.body);
3035
3303
  }
3036
- catch (_) { }
3304
+ catch (_) {
3305
+ /* body may be absent or non-JSON */
3306
+ }
3037
3307
  if (r.status !== 200)
3038
3308
  return { error: json.error || `Server error ${r.status}` };
3039
3309
  const text = json.text ?? '';
@@ -3097,7 +3367,9 @@ async function useSkill(input, workDir) {
3097
3367
  return { error: 'name is required (the skill to invoke)' };
3098
3368
  const dir = workDir ?? process.cwd();
3099
3369
  const skills = (0, skills_1.loadSkills)(dir);
3100
- 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);
3101
3373
  if (!skill) {
3102
3374
  const known = skills.map((s) => s.name).join(', ') || '(none defined)';
3103
3375
  return { error: `Unknown skill "${name}". Available: ${known}.` };
@@ -3122,7 +3394,12 @@ function semanticPreamble(input, workDir) {
3122
3394
  return { ok: false, err: { error: e.message } };
3123
3395
  }
3124
3396
  if (!(0, tsLangService_1.isTsLike)(resolved)) {
3125
- 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
+ };
3126
3403
  }
3127
3404
  return { ok: true, args: { workDir: workDir ?? process.cwd(), filePath: resolved, line, character } };
3128
3405
  }
@@ -3200,14 +3477,34 @@ const TOOL_MAP = {
3200
3477
  // their own dedicated branch above (which passes abortSignal too) — kept out
3201
3478
  // of this set so there's exactly one dispatch path per tool, not two.
3202
3479
  const WORKDIR_TOOLS = new Set([
3203
- 'read_file', 'write_file', 'write_docx', 'write_xlsx', 'write_pptx',
3204
- 'list_directory', 'search_files', 'glob',
3205
- 'create_directory', 'edit_file', 'multi_edit', 'copy_file', 'move_file',
3206
- 'delete_file', 'notebook_read', 'notebook_edit',
3207
- 'get_symbols', 'get_workspace_symbols',
3208
- 'go_to_definition', 'find_references', 'get_hover',
3209
- 'memory_write', 'memory_read', 'use_skill',
3210
- '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',
3211
3508
  ]);
3212
3509
  async function executeTool(name, input, abortSignal, sandbox, workDir, agentScope, onStream,
3213
3510
  // This session's own peer identity (see agent/peerRegistry.ts), if it is