@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.
- 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 +15 -7
- 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 -74
- package/dist/agent/iterationPolicy.d.ts.map +1 -1
- package/dist/agent/iterationPolicy.js +28 -25
- package/dist/agent/lifecycleHost.d.ts.map +1 -1
- package/dist/agent/lifecycleHost.js +36 -12
- 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 +5 -3
- 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 +67 -32
- 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 +23 -8
- 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 +470 -173
- 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 +4 -2
- 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,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 {
|
|
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
|
|
527
|
-
// ever get pulled
|
|
528
|
-
// most agent sessions never touch
|
|
529
|
-
|
|
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,
|
|
532
|
-
|
|
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,
|
|
536
|
-
|
|
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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
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, {
|
|
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,
|
|
748
|
-
|
|
749
|
-
|
|
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 {
|
|
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 {
|
|
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) => {
|
|
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 {
|
|
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 {
|
|
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(() => {
|
|
877
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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({
|
|
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 {
|
|
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 {
|
|
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(() => {
|
|
1132
|
-
|
|
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 {
|
|
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 {
|
|
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) => {
|
|
1218
|
-
stderr
|
|
1219
|
-
|
|
1220
|
-
|
|
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 = (() => {
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
|
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
|
|
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', [
|
|
1302
|
-
'
|
|
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 {
|
|
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 :
|
|
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) ||
|
|
1357
|
-
|
|
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
|
|
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
|
-
})
|
|
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 ?
|
|
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
|
-
: [
|
|
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',
|
|
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 {
|
|
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 {
|
|
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') +
|
|
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('^' +
|
|
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',
|
|
1532
|
-
'
|
|
1533
|
-
'
|
|
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
|
|
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() ||
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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(/&/g, '&')
|
|
2098
|
+
.replace(/&/g, '&')
|
|
2099
|
+
.replace(/</g, '<')
|
|
2100
|
+
.replace(/>/g, '>')
|
|
2101
|
+
.replace(/"/g, '"')
|
|
2102
|
+
.replace(/'/g, "'")
|
|
2103
|
+
.replace(/ /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
|
|
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 {
|
|
2261
|
+
catch {
|
|
2262
|
+
/* fall through */
|
|
2263
|
+
}
|
|
2084
2264
|
if (r.status !== 200 || !json.text) {
|
|
2085
|
-
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
|
+
};
|
|
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 {
|
|
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)
|
|
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'), {
|
|
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
|
-
|
|
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({
|
|
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) => {
|
|
2255
|
-
|
|
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 {
|
|
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 {
|
|
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') +
|
|
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) => {
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
?
|
|
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 =
|
|
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'
|
|
2736
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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) => {
|
|
2814
|
-
|
|
2815
|
-
|
|
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({
|
|
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', () => {
|
|
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) => {
|
|
2845
|
-
|
|
2846
|
-
|
|
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) &&
|
|
3095
|
+
if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) &&
|
|
3096
|
+
res.headers.location) {
|
|
2850
3097
|
req.destroy();
|
|
2851
|
-
done(() => {
|
|
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({
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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',
|
|
3204
|
-
'
|
|
3205
|
-
'
|
|
3206
|
-
'
|
|
3207
|
-
'
|
|
3208
|
-
'
|
|
3209
|
-
'
|
|
3210
|
-
'
|
|
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
|