@nexrall/code-core 1.4.71 → 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.
- package/dist/agent/agentRegistry.d.ts.map +1 -1
- package/dist/agent/agentRegistry.js +28 -10
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +159 -48
- package/dist/agent/askOnce.d.ts.map +1 -1
- package/dist/agent/askOnce.js +14 -5
- package/dist/agent/backgroundAgents.d.ts.map +1 -1
- package/dist/agent/backgroundAgents.js +22 -8
- package/dist/agent/claimEvidence.d.ts.map +1 -1
- package/dist/agent/claimEvidence.js +2 -1
- package/dist/agent/compaction.d.ts.map +1 -1
- package/dist/agent/compaction.js +31 -10
- package/dist/agent/crossProcessLock.d.ts.map +1 -1
- package/dist/agent/crossProcessLock.js +14 -6
- package/dist/agent/editCompleteness.d.ts.map +1 -1
- package/dist/agent/fileLocks.d.ts.map +1 -1
- package/dist/agent/fileLocks.js +14 -5
- package/dist/agent/flaky.d.ts.map +1 -1
- package/dist/agent/hooks.d.ts.map +1 -1
- package/dist/agent/hooks.js +284 -75
- package/dist/agent/iterationPolicy.d.ts.map +1 -1
- package/dist/agent/iterationPolicy.js +26 -23
- package/dist/agent/lifecycleHost.d.ts.map +1 -1
- package/dist/agent/lifecycleHost.js +35 -11
- package/dist/agent/loop.d.ts +22 -4
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +223 -51
- package/dist/agent/memory.d.ts.map +1 -1
- package/dist/agent/memory.js +13 -7
- package/dist/agent/modelCatalogue.d.ts.map +1 -1
- package/dist/agent/modelCatalogue.js +4 -1
- package/dist/agent/nestedInstructions.d.ts +122 -0
- package/dist/agent/nestedInstructions.d.ts.map +1 -0
- package/dist/agent/nestedInstructions.js +696 -0
- package/dist/agent/peerRegistry.d.ts.map +1 -1
- package/dist/agent/peerRegistry.js +44 -6
- package/dist/agent/peerTransport.d.ts.map +1 -1
- package/dist/agent/peerTransport.js +18 -6
- package/dist/agent/planMode.d.ts.map +1 -1
- package/dist/agent/planMode.js +194 -41
- package/dist/agent/readDedupe.d.ts.map +1 -1
- package/dist/agent/readDedupe.js +3 -1
- package/dist/agent/securityLint.d.ts.map +1 -1
- package/dist/agent/sharedTasks.d.ts.map +1 -1
- package/dist/agent/sharedTasks.js +20 -5
- package/dist/agent/skills.d.ts +79 -0
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +403 -14
- package/dist/agent/subAgentBudget.d.ts.map +1 -1
- package/dist/agent/subTask.d.ts.map +1 -1
- package/dist/agent/subTask.js +87 -37
- package/dist/agent/subTaskSupport.d.ts.map +1 -1
- package/dist/agent/subTaskSupport.js +43 -14
- package/dist/agent/testIntegrity.d.ts.map +1 -1
- package/dist/agent/testIntegrity.js +4 -2
- package/dist/agent/toolDescriptions.d.ts.map +1 -1
- package/dist/agent/toolPrefetch.d.ts.map +1 -1
- package/dist/agent/toolPrefetch.js +15 -11
- package/dist/agent/trust.d.ts.map +1 -1
- package/dist/agent/trust.js +6 -2
- package/dist/agent/worktree.d.ts.map +1 -1
- package/dist/agent/worktree.js +70 -16
- package/dist/agent/worktreeEnforcement.d.ts.map +1 -1
- package/dist/agent/worktreeEnforcement.js +9 -4
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +63 -31
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -1
- package/dist/checkpoint/manager.d.ts.map +1 -1
- package/dist/checkpoint/manager.js +22 -7
- package/dist/commands/loader.d.ts.map +1 -1
- package/dist/commands/loader.js +6 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/mcp/client.d.ts +34 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +89 -24
- package/dist/mcp/httpClient.d.ts +24 -1
- package/dist/mcp/httpClient.d.ts.map +1 -1
- package/dist/mcp/httpClient.js +80 -14
- package/dist/mcp/instructions.d.ts +11 -0
- package/dist/mcp/instructions.d.ts.map +1 -0
- package/dist/mcp/instructions.js +42 -0
- package/dist/mcp/manager.d.ts +24 -0
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +117 -24
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +48 -14
- package/dist/mcp/sseClient.d.ts +9 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +43 -10
- package/dist/mcp/stats.d.ts.map +1 -1
- package/dist/permissions/bashClassify.d.ts.map +1 -1
- package/dist/permissions/bashClassify.js +25 -3
- package/dist/permissions/destructive.d.ts.map +1 -1
- package/dist/permissions/destructive.js +7 -2
- package/dist/permissions/destructiveTokens.d.ts.map +1 -1
- package/dist/permissions/destructiveTokens.js +104 -24
- package/dist/permissions/modePolicy.d.ts.map +1 -1
- package/dist/permissions/modePolicy.js +40 -17
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +7 -5
- package/dist/plugins/data.d.ts.map +1 -1
- package/dist/plugins/data.js +6 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +18 -7
- package/dist/plugins/installer.d.ts.map +1 -1
- package/dist/plugins/installer.js +17 -10
- package/dist/plugins/sources.d.ts.map +1 -1
- package/dist/plugins/sources.js +9 -3
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +454 -168
- package/dist/tools/sandbox.d.ts.map +1 -1
- package/dist/tools/sandbox.js +5 -1
- package/dist/tools/symbols.d.ts.map +1 -1
- package/dist/tools/symbols.js +30 -8
- package/dist/tools/tsLangService.d.ts.map +1 -1
- package/dist/tools/tsLangService.js +12 -4
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/util/frontmatter.d.ts.map +1 -1
- package/dist/util/miniYaml.d.ts.map +1 -1
- package/dist/util/miniYaml.js +3 -1
- package/package.json +1 -1
package/dist/tools/executor.js
CHANGED
|
@@ -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.
|
|
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) => {
|
|
376
|
-
stopped
|
|
377
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
452
|
+
catch {
|
|
453
|
+
/* ignore */
|
|
454
|
+
}
|
|
445
455
|
try {
|
|
446
456
|
priorContent = fs.readFileSync(resolved, 'utf-8');
|
|
447
457
|
}
|
|
448
|
-
catch {
|
|
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 {
|
|
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 {
|
|
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
|
|
550
|
+
const { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType } = await Promise.resolve().then(() => __importStar(require('docx')));
|
|
533
551
|
const headingMap = {
|
|
534
|
-
1: HeadingLevel.HEADING_1,
|
|
535
|
-
|
|
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,
|
|
539
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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, {
|
|
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,
|
|
758
|
-
|
|
759
|
-
|
|
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 {
|
|
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 {
|
|
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) => {
|
|
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 {
|
|
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 {
|
|
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(() => {
|
|
887
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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({
|
|
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 {
|
|
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 {
|
|
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(() => {
|
|
1142
|
-
|
|
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 {
|
|
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 {
|
|
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) => {
|
|
1228
|
-
stderr
|
|
1229
|
-
|
|
1230
|
-
|
|
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 = (() => {
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
|
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
|
|
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', [
|
|
1312
|
-
'
|
|
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 {
|
|
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 :
|
|
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) ||
|
|
1367
|
-
|
|
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
|
|
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
|
-
})
|
|
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 ?
|
|
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
|
-
: [
|
|
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',
|
|
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 {
|
|
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 {
|
|
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') +
|
|
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('^' +
|
|
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',
|
|
1542
|
-
'
|
|
1543
|
-
'
|
|
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
|
|
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() ||
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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(/&/g, '&')
|
|
2098
|
+
.replace(/&/g, '&')
|
|
2099
|
+
.replace(/</g, '<')
|
|
2100
|
+
.replace(/>/g, '>')
|
|
2101
|
+
.replace(/"/g, '"')
|
|
2102
|
+
.replace(/'/g, "'")
|
|
2103
|
+
.replace(/ /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
|
|
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 {
|
|
2261
|
+
catch {
|
|
2262
|
+
/* fall through */
|
|
2263
|
+
}
|
|
2094
2264
|
if (r.status !== 200 || !json.text) {
|
|
2095
|
-
return {
|
|
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 {
|
|
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)
|
|
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'), {
|
|
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
|
-
|
|
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({
|
|
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) => {
|
|
2265
|
-
|
|
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 {
|
|
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 {
|
|
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') +
|
|
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) => {
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
?
|
|
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 =
|
|
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'
|
|
2747
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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) => {
|
|
2825
|
-
|
|
2826
|
-
|
|
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({
|
|
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', () => {
|
|
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) => {
|
|
2856
|
-
|
|
2857
|
-
|
|
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) &&
|
|
3095
|
+
if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) &&
|
|
3096
|
+
res.headers.location) {
|
|
2861
3097
|
req.destroy();
|
|
2862
|
-
done(() => {
|
|
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({
|
|
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 (_) {
|
|
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 (_) {
|
|
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
|
|
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 {
|
|
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 (_) {
|
|
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 (_) {
|
|
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 (_) {
|
|
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
|
-
|
|
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 {
|
|
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',
|
|
3215
|
-
'
|
|
3216
|
-
'
|
|
3217
|
-
'
|
|
3218
|
-
'
|
|
3219
|
-
'
|
|
3220
|
-
'
|
|
3221
|
-
'
|
|
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
|