mini-coder 0.0.7 → 0.0.8
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/mc.js +403 -173
- package/package.json +1 -1
- package/better-errors.md +0 -96
package/dist/mc.js
CHANGED
|
@@ -6,7 +6,7 @@ import * as c8 from "yoctocolors";
|
|
|
6
6
|
|
|
7
7
|
// src/agent/agent.ts
|
|
8
8
|
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
9
|
-
import { join as
|
|
9
|
+
import { join as join16 } from "path";
|
|
10
10
|
import * as c7 from "yoctocolors";
|
|
11
11
|
|
|
12
12
|
// src/cli/agents.ts
|
|
@@ -521,16 +521,146 @@ function generateSessionId() {
|
|
|
521
521
|
|
|
522
522
|
// src/cli/custom-commands.ts
|
|
523
523
|
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
524
|
-
import { homedir as
|
|
525
|
-
import { basename as basename2, join as
|
|
524
|
+
import { homedir as homedir5 } from "os";
|
|
525
|
+
import { basename as basename2, join as join4 } from "path";
|
|
526
526
|
|
|
527
527
|
// src/cli/config-conflicts.ts
|
|
528
528
|
import * as c3 from "yoctocolors";
|
|
529
529
|
|
|
530
530
|
// src/cli/output.ts
|
|
531
|
-
import { homedir as
|
|
531
|
+
import { homedir as homedir4 } from "os";
|
|
532
532
|
import * as c2 from "yoctocolors";
|
|
533
533
|
|
|
534
|
+
// src/cli/error-log.ts
|
|
535
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
536
|
+
import { homedir as homedir3 } from "os";
|
|
537
|
+
import { join as join3 } from "path";
|
|
538
|
+
var writer = null;
|
|
539
|
+
function initErrorLog() {
|
|
540
|
+
if (writer)
|
|
541
|
+
return;
|
|
542
|
+
const dirPath = join3(homedir3(), ".config", "mini-coder");
|
|
543
|
+
const logPath = join3(dirPath, "errors.log");
|
|
544
|
+
mkdirSync2(dirPath, { recursive: true });
|
|
545
|
+
writer = Bun.file(logPath).writer();
|
|
546
|
+
process.on("uncaughtException", (err) => {
|
|
547
|
+
logError(err, "uncaught");
|
|
548
|
+
process.exit(1);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
function isObject(v) {
|
|
552
|
+
return typeof v === "object" && v !== null;
|
|
553
|
+
}
|
|
554
|
+
function logError(err, context) {
|
|
555
|
+
if (!writer)
|
|
556
|
+
return;
|
|
557
|
+
let entry = `[${new Date().toISOString()}]`;
|
|
558
|
+
if (context)
|
|
559
|
+
entry += ` context=${context}`;
|
|
560
|
+
entry += `
|
|
561
|
+
`;
|
|
562
|
+
if (isObject(err)) {
|
|
563
|
+
if (typeof err.name === "string")
|
|
564
|
+
entry += ` name: ${err.name}
|
|
565
|
+
`;
|
|
566
|
+
if (typeof err.message === "string")
|
|
567
|
+
entry += ` message: ${err.message}
|
|
568
|
+
`;
|
|
569
|
+
if ("statusCode" in err)
|
|
570
|
+
entry += ` statusCode: ${err.statusCode}
|
|
571
|
+
`;
|
|
572
|
+
if ("url" in err)
|
|
573
|
+
entry += ` url: ${err.url}
|
|
574
|
+
`;
|
|
575
|
+
if ("isRetryable" in err)
|
|
576
|
+
entry += ` isRetryable: ${err.isRetryable}
|
|
577
|
+
`;
|
|
578
|
+
if (typeof err.stack === "string") {
|
|
579
|
+
const indentedStack = err.stack.split(`
|
|
580
|
+
`).map((line, i) => i === 0 ? line : ` ${line}`).join(`
|
|
581
|
+
`);
|
|
582
|
+
entry += ` stack: ${indentedStack}
|
|
583
|
+
`;
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
entry += ` value: ${String(err)}
|
|
587
|
+
`;
|
|
588
|
+
}
|
|
589
|
+
entry += `---
|
|
590
|
+
`;
|
|
591
|
+
writer.write(entry);
|
|
592
|
+
writer.flush();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/cli/error-parse.ts
|
|
596
|
+
import {
|
|
597
|
+
APICallError,
|
|
598
|
+
LoadAPIKeyError,
|
|
599
|
+
NoContentGeneratedError,
|
|
600
|
+
NoSuchModelError,
|
|
601
|
+
RetryError
|
|
602
|
+
} from "ai";
|
|
603
|
+
function parseAppError(err) {
|
|
604
|
+
if (typeof err === "string") {
|
|
605
|
+
return { headline: err };
|
|
606
|
+
}
|
|
607
|
+
if (err instanceof RetryError) {
|
|
608
|
+
const inner = parseAppError(err.lastError);
|
|
609
|
+
return {
|
|
610
|
+
headline: `Retries exhausted: ${inner.headline}`,
|
|
611
|
+
...inner.hint ? { hint: inner.hint } : {}
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
if (err instanceof APICallError) {
|
|
615
|
+
if (err.statusCode === 429) {
|
|
616
|
+
return {
|
|
617
|
+
headline: "Rate limit hit",
|
|
618
|
+
hint: "Wait a moment and retry, or switch model with /model"
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
if (err.statusCode === 401 || err.statusCode === 403) {
|
|
622
|
+
return {
|
|
623
|
+
headline: "Auth failed",
|
|
624
|
+
hint: "Check the relevant provider API key env var"
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
headline: `API error ${err.statusCode ?? "unknown"}`,
|
|
629
|
+
...err.url ? { hint: err.url } : {}
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
if (err instanceof NoContentGeneratedError) {
|
|
633
|
+
return {
|
|
634
|
+
headline: "Model returned empty response",
|
|
635
|
+
hint: "Try rephrasing or switching model with /model"
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
if (err instanceof LoadAPIKeyError) {
|
|
639
|
+
return {
|
|
640
|
+
headline: "API key not found",
|
|
641
|
+
hint: "Set the relevant provider env var"
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
if (err instanceof NoSuchModelError) {
|
|
645
|
+
return {
|
|
646
|
+
headline: "Model not found",
|
|
647
|
+
hint: "Use /model to pick a valid model"
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
const isObj = typeof err === "object" && err !== null;
|
|
651
|
+
const code = isObj && "code" in err ? String(err.code) : undefined;
|
|
652
|
+
const message = isObj && "message" in err ? String(err.message) : String(err);
|
|
653
|
+
if (code === "ECONNREFUSED" || message.includes("ECONNREFUSED")) {
|
|
654
|
+
return {
|
|
655
|
+
headline: "Connection failed",
|
|
656
|
+
hint: "Check network or local server"
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
const firstLine = message.split(`
|
|
660
|
+
`)[0]?.trim() || "Unknown error";
|
|
661
|
+
return { headline: firstLine };
|
|
662
|
+
}
|
|
663
|
+
|
|
534
664
|
// src/cli/markdown.ts
|
|
535
665
|
import * as c from "yoctocolors";
|
|
536
666
|
function renderInline(text) {
|
|
@@ -644,8 +774,8 @@ function renderChunk(text, inFence) {
|
|
|
644
774
|
}
|
|
645
775
|
|
|
646
776
|
// src/cli/output.ts
|
|
647
|
-
var HOME =
|
|
648
|
-
var PACKAGE_VERSION = "0.0.
|
|
777
|
+
var HOME = homedir4();
|
|
778
|
+
var PACKAGE_VERSION = "0.0.7";
|
|
649
779
|
function tildePath(p) {
|
|
650
780
|
return p.startsWith(HOME) ? `~${p.slice(HOME.length)}` : p;
|
|
651
781
|
}
|
|
@@ -668,8 +798,6 @@ function registerTerminalCleanup() {
|
|
|
668
798
|
process.exit(143);
|
|
669
799
|
});
|
|
670
800
|
process.on("SIGINT", () => {
|
|
671
|
-
if (process.listenerCount("SIGINT") > 1)
|
|
672
|
-
return;
|
|
673
801
|
cleanup();
|
|
674
802
|
process.exit(130);
|
|
675
803
|
});
|
|
@@ -856,10 +984,6 @@ function renderToolResultInline(toolName, result, isError, indent) {
|
|
|
856
984
|
return;
|
|
857
985
|
}
|
|
858
986
|
if (toolName === "subagent") {
|
|
859
|
-
const r = result;
|
|
860
|
-
if (r.inputTokens || r.outputTokens) {
|
|
861
|
-
writeln(`${indent}${G.ok} ${c2.dim(`\u2191${r.inputTokens ?? 0} \u2193${r.outputTokens ?? 0}`)}`);
|
|
862
|
-
}
|
|
863
987
|
return;
|
|
864
988
|
}
|
|
865
989
|
if (toolName.startsWith("mcp_")) {
|
|
@@ -876,16 +1000,48 @@ function renderToolResultInline(toolName, result, isError, indent) {
|
|
|
876
1000
|
const text = JSON.stringify(result);
|
|
877
1001
|
writeln(`${indent}${G.info} ${c2.dim(text.length > 80 ? `${text.slice(0, 77)}\u2026` : text)}`);
|
|
878
1002
|
}
|
|
879
|
-
function
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1003
|
+
function formatSubagentLabel(laneId, parentLabel) {
|
|
1004
|
+
const numStr = parentLabel ? `${parentLabel.replace(/[\[\]]/g, "")}.${laneId}` : `${laneId}`;
|
|
1005
|
+
return c2.dim(c2.cyan(`[${numStr}]`));
|
|
1006
|
+
}
|
|
1007
|
+
var laneBuffers = new Map;
|
|
1008
|
+
function renderSubagentEvent(event, opts) {
|
|
1009
|
+
const { laneId, parentLabel, activeLanes } = opts;
|
|
1010
|
+
const labelStr = formatSubagentLabel(laneId, parentLabel);
|
|
1011
|
+
const prefix = activeLanes.size > 1 ? `${labelStr} ` : "";
|
|
1012
|
+
if (event.type === "text-delta") {
|
|
1013
|
+
const buf = (laneBuffers.get(laneId) ?? "") + event.delta;
|
|
1014
|
+
const lines = buf.split(`
|
|
1015
|
+
`);
|
|
1016
|
+
if (lines.length > 1) {
|
|
1017
|
+
for (let i = 0;i < lines.length - 1; i++) {
|
|
1018
|
+
writeln(`${prefix}${lines[i]}`);
|
|
886
1019
|
}
|
|
1020
|
+
laneBuffers.set(laneId, lines[lines.length - 1] ?? "");
|
|
1021
|
+
} else {
|
|
1022
|
+
laneBuffers.set(laneId, buf);
|
|
1023
|
+
}
|
|
1024
|
+
} else if (event.type === "tool-call-start") {
|
|
1025
|
+
writeln(`${prefix}${toolCallLine(event.toolName, event.args)}`);
|
|
1026
|
+
} else if (event.type === "tool-result") {
|
|
1027
|
+
renderToolResultInline(event.toolName, event.result, event.isError, `${prefix} `);
|
|
1028
|
+
} else if (event.type === "turn-complete") {
|
|
1029
|
+
const buf = laneBuffers.get(laneId);
|
|
1030
|
+
if (buf) {
|
|
1031
|
+
writeln(`${prefix}${buf}`);
|
|
1032
|
+
laneBuffers.delete(laneId);
|
|
1033
|
+
}
|
|
1034
|
+
if (event.inputTokens > 0 || event.outputTokens > 0) {
|
|
1035
|
+
writeln(`${prefix}${c2.dim(`\u2191${event.inputTokens} \u2193${event.outputTokens}`)}`);
|
|
1036
|
+
}
|
|
1037
|
+
} else if (event.type === "turn-error") {
|
|
1038
|
+
laneBuffers.delete(laneId);
|
|
1039
|
+
logError(event.error, "turn");
|
|
1040
|
+
const parsed = parseAppError(event.error);
|
|
1041
|
+
writeln(`${prefix}${G.err} ${c2.red(parsed.headline)}`);
|
|
1042
|
+
if (parsed.hint) {
|
|
1043
|
+
writeln(`${prefix} ${c2.dim(parsed.hint)}`);
|
|
887
1044
|
}
|
|
888
|
-
renderToolResultInline(entry.toolName, entry.result, entry.isError, `${indent} `);
|
|
889
1045
|
}
|
|
890
1046
|
}
|
|
891
1047
|
function renderToolResult(toolName, result, isError) {
|
|
@@ -985,22 +1141,6 @@ function renderToolResult(toolName, result, isError) {
|
|
|
985
1141
|
return;
|
|
986
1142
|
}
|
|
987
1143
|
if (toolName === "subagent") {
|
|
988
|
-
const r = result;
|
|
989
|
-
if (r.activity?.length) {
|
|
990
|
-
renderSubagentActivity(r.activity, " ", 1);
|
|
991
|
-
}
|
|
992
|
-
if (r.result) {
|
|
993
|
-
const lines = r.result.split(`
|
|
994
|
-
`);
|
|
995
|
-
const preview = lines.slice(0, 8);
|
|
996
|
-
for (const line of preview)
|
|
997
|
-
writeln(` ${c2.dim("\u2502")} ${line}`);
|
|
998
|
-
if (lines.length > 8)
|
|
999
|
-
writeln(` ${c2.dim(`\u2502 \u2026 +${lines.length - 8} lines`)}`);
|
|
1000
|
-
}
|
|
1001
|
-
if (r.inputTokens || r.outputTokens) {
|
|
1002
|
-
writeln(` ${c2.dim(`\u2191${r.inputTokens ?? 0} \u2193${r.outputTokens ?? 0}`)}`);
|
|
1003
|
-
}
|
|
1004
1144
|
return;
|
|
1005
1145
|
}
|
|
1006
1146
|
if (toolName.startsWith("mcp_")) {
|
|
@@ -1159,9 +1299,12 @@ async function renderTurn(events, spinner) {
|
|
|
1159
1299
|
if (isAbort) {
|
|
1160
1300
|
writeln(`${G.warn} ${c2.dim("interrupted")}`);
|
|
1161
1301
|
} else {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
writeln(`${G.err} ${c2.red(
|
|
1302
|
+
logError(event.error, "turn");
|
|
1303
|
+
const parsed = parseAppError(event.error);
|
|
1304
|
+
writeln(`${G.err} ${c2.red(parsed.headline)}`);
|
|
1305
|
+
if (parsed.hint) {
|
|
1306
|
+
writeln(` ${c2.dim(parsed.hint)}`);
|
|
1307
|
+
}
|
|
1165
1308
|
}
|
|
1166
1309
|
break;
|
|
1167
1310
|
}
|
|
@@ -1211,9 +1354,13 @@ function renderBanner(model, cwd) {
|
|
|
1211
1354
|
writeln(` ${c2.dim("/help for commands \xB7 ctrl+d to exit")}`);
|
|
1212
1355
|
writeln();
|
|
1213
1356
|
}
|
|
1214
|
-
function renderError(err) {
|
|
1215
|
-
|
|
1216
|
-
|
|
1357
|
+
function renderError(err, context = "render") {
|
|
1358
|
+
logError(err, context);
|
|
1359
|
+
const parsed = parseAppError(err);
|
|
1360
|
+
writeln(`${G.err} ${c2.red(parsed.headline)}`);
|
|
1361
|
+
if (parsed.hint) {
|
|
1362
|
+
writeln(` ${c2.dim(parsed.hint)}`);
|
|
1363
|
+
}
|
|
1217
1364
|
}
|
|
1218
1365
|
function renderInfo(msg) {
|
|
1219
1366
|
writeln(`${G.info} ${c2.dim(msg)}`);
|
|
@@ -1301,7 +1448,7 @@ function loadFromDir2(dir, source) {
|
|
|
1301
1448
|
if (!entry.endsWith(".md"))
|
|
1302
1449
|
continue;
|
|
1303
1450
|
const name = basename2(entry, ".md");
|
|
1304
|
-
const filePath =
|
|
1451
|
+
const filePath = join4(dir, entry);
|
|
1305
1452
|
let raw;
|
|
1306
1453
|
try {
|
|
1307
1454
|
raw = readFileSync2(filePath, "utf-8");
|
|
@@ -1320,10 +1467,10 @@ function loadFromDir2(dir, source) {
|
|
|
1320
1467
|
return commands;
|
|
1321
1468
|
}
|
|
1322
1469
|
function loadCustomCommands(cwd) {
|
|
1323
|
-
const globalAgentsDir =
|
|
1324
|
-
const globalClaudeDir =
|
|
1325
|
-
const localAgentsDir =
|
|
1326
|
-
const localClaudeDir =
|
|
1470
|
+
const globalAgentsDir = join4(homedir5(), ".agents", "commands");
|
|
1471
|
+
const globalClaudeDir = join4(homedir5(), ".claude", "commands");
|
|
1472
|
+
const localAgentsDir = join4(cwd, ".agents", "commands");
|
|
1473
|
+
const localClaudeDir = join4(cwd, ".claude", "commands");
|
|
1327
1474
|
const globalAgents = loadFromDir2(globalAgentsDir, "global");
|
|
1328
1475
|
const globalClaude = loadFromDir2(globalClaudeDir, "global");
|
|
1329
1476
|
const localAgents = loadFromDir2(localAgentsDir, "local");
|
|
@@ -1378,8 +1525,8 @@ async function expandTemplate(template, args, cwd) {
|
|
|
1378
1525
|
|
|
1379
1526
|
// src/cli/skills.ts
|
|
1380
1527
|
import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync } from "fs";
|
|
1381
|
-
import { homedir as
|
|
1382
|
-
import { join as
|
|
1528
|
+
import { homedir as homedir6 } from "os";
|
|
1529
|
+
import { join as join5 } from "path";
|
|
1383
1530
|
function loadFromDir3(dir, source) {
|
|
1384
1531
|
const skills = new Map;
|
|
1385
1532
|
if (!existsSync4(dir))
|
|
@@ -1391,9 +1538,9 @@ function loadFromDir3(dir, source) {
|
|
|
1391
1538
|
return skills;
|
|
1392
1539
|
}
|
|
1393
1540
|
for (const entry of entries) {
|
|
1394
|
-
const skillFile =
|
|
1541
|
+
const skillFile = join5(dir, entry, "SKILL.md");
|
|
1395
1542
|
try {
|
|
1396
|
-
if (!statSync(
|
|
1543
|
+
if (!statSync(join5(dir, entry)).isDirectory())
|
|
1397
1544
|
continue;
|
|
1398
1545
|
if (!existsSync4(skillFile))
|
|
1399
1546
|
continue;
|
|
@@ -1411,10 +1558,10 @@ function loadFromDir3(dir, source) {
|
|
|
1411
1558
|
return skills;
|
|
1412
1559
|
}
|
|
1413
1560
|
function loadSkills(cwd) {
|
|
1414
|
-
const globalAgentsDir =
|
|
1415
|
-
const globalClaudeDir =
|
|
1416
|
-
const localAgentsDir =
|
|
1417
|
-
const localClaudeDir =
|
|
1561
|
+
const globalAgentsDir = join5(homedir6(), ".agents", "skills");
|
|
1562
|
+
const globalClaudeDir = join5(homedir6(), ".claude", "skills");
|
|
1563
|
+
const localAgentsDir = join5(cwd, ".agents", "skills");
|
|
1564
|
+
const localClaudeDir = join5(cwd, ".claude", "skills");
|
|
1418
1565
|
const globalAgents = loadFromDir3(globalAgentsDir, "global");
|
|
1419
1566
|
const globalClaude = loadFromDir3(globalClaudeDir, "global");
|
|
1420
1567
|
const localAgents = loadFromDir3(localAgentsDir, "local");
|
|
@@ -1609,10 +1756,6 @@ async function handleReview(ctx, args) {
|
|
|
1609
1756
|
writeln();
|
|
1610
1757
|
try {
|
|
1611
1758
|
const output = await ctx.runSubagent(REVIEW_PROMPT(ctx.cwd, focus));
|
|
1612
|
-
if (output.activity.length) {
|
|
1613
|
-
renderSubagentActivity(output.activity, " ", 1);
|
|
1614
|
-
writeln();
|
|
1615
|
-
}
|
|
1616
1759
|
write(renderMarkdown(output.result));
|
|
1617
1760
|
writeln();
|
|
1618
1761
|
return {
|
|
@@ -1641,10 +1784,6 @@ async function handleCustomCommand(cmd, args, ctx) {
|
|
|
1641
1784
|
writeln();
|
|
1642
1785
|
try {
|
|
1643
1786
|
const output = await ctx.runSubagent(prompt, cmd.model);
|
|
1644
|
-
if (output.activity.length) {
|
|
1645
|
-
renderSubagentActivity(output.activity, " ", 1);
|
|
1646
|
-
writeln();
|
|
1647
|
-
}
|
|
1648
1787
|
write(renderMarkdown(output.result));
|
|
1649
1788
|
writeln();
|
|
1650
1789
|
return {
|
|
@@ -1795,7 +1934,7 @@ async function loadImageFile(filePath) {
|
|
|
1795
1934
|
}
|
|
1796
1935
|
|
|
1797
1936
|
// src/cli/input.ts
|
|
1798
|
-
import { join as
|
|
1937
|
+
import { join as join6, relative } from "path";
|
|
1799
1938
|
import * as c5 from "yoctocolors";
|
|
1800
1939
|
var ESC = "\x1B";
|
|
1801
1940
|
var CSI = `${ESC}[`;
|
|
@@ -1847,7 +1986,7 @@ async function getAtCompletions(prefix, cwd) {
|
|
|
1847
1986
|
for await (const file of glob.scan({ cwd, onlyFiles: true })) {
|
|
1848
1987
|
if (file.includes("node_modules") || file.includes(".git"))
|
|
1849
1988
|
continue;
|
|
1850
|
-
results.push(`@${relative(cwd,
|
|
1989
|
+
results.push(`@${relative(cwd, join6(cwd, file))}`);
|
|
1851
1990
|
if (results.length >= MAX)
|
|
1852
1991
|
break;
|
|
1853
1992
|
}
|
|
@@ -1869,7 +2008,7 @@ async function tryExtractImageFromPaste(pasted, cwd) {
|
|
|
1869
2008
|
}
|
|
1870
2009
|
}
|
|
1871
2010
|
if (!trimmed.includes(" ") && isImageFilename(trimmed)) {
|
|
1872
|
-
const filePath = trimmed.startsWith("/") ? trimmed :
|
|
2011
|
+
const filePath = trimmed.startsWith("/") ? trimmed : join6(cwd, trimmed);
|
|
1873
2012
|
const attachment = await loadImageFile(filePath);
|
|
1874
2013
|
if (attachment) {
|
|
1875
2014
|
const name = filePath.split("/").pop() ?? trimmed;
|
|
@@ -1906,6 +2045,28 @@ async function readKey(reader) {
|
|
|
1906
2045
|
return "";
|
|
1907
2046
|
return new TextDecoder().decode(value);
|
|
1908
2047
|
}
|
|
2048
|
+
function watchForInterrupt(abortController) {
|
|
2049
|
+
if (!process.stdin.isTTY)
|
|
2050
|
+
return () => {};
|
|
2051
|
+
const onData = (chunk) => {
|
|
2052
|
+
for (const byte of chunk) {
|
|
2053
|
+
if (byte === 3) {
|
|
2054
|
+
cleanup();
|
|
2055
|
+
abortController.abort();
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2060
|
+
const cleanup = () => {
|
|
2061
|
+
process.stdin.removeListener("data", onData);
|
|
2062
|
+
process.stdin.setRawMode(false);
|
|
2063
|
+
process.stdin.pause();
|
|
2064
|
+
};
|
|
2065
|
+
process.stdin.setRawMode(true);
|
|
2066
|
+
process.stdin.resume();
|
|
2067
|
+
process.stdin.on("data", onData);
|
|
2068
|
+
return cleanup;
|
|
2069
|
+
}
|
|
1909
2070
|
var PASTE_SENTINEL = "\x00PASTE\x00";
|
|
1910
2071
|
var PASTE_SENTINEL_LEN = PASTE_SENTINEL.length;
|
|
1911
2072
|
function pasteLabel(text) {
|
|
@@ -2278,6 +2439,7 @@ async function* runTurn(options) {
|
|
|
2278
2439
|
...signal ? { abortSignal: signal } : {}
|
|
2279
2440
|
};
|
|
2280
2441
|
const result = streamText(streamOpts);
|
|
2442
|
+
result.response.catch(() => {});
|
|
2281
2443
|
for await (const chunk of result.fullStream) {
|
|
2282
2444
|
if (signal?.aborted)
|
|
2283
2445
|
break;
|
|
@@ -2402,7 +2564,7 @@ async function connectMcpServer(config) {
|
|
|
2402
2564
|
|
|
2403
2565
|
// src/tools/snapshot.ts
|
|
2404
2566
|
import { readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
|
|
2405
|
-
import { join as
|
|
2567
|
+
import { join as join7 } from "path";
|
|
2406
2568
|
async function gitBytes(args, cwd) {
|
|
2407
2569
|
try {
|
|
2408
2570
|
const proc = Bun.spawn(["git", ...args], {
|
|
@@ -2493,7 +2655,7 @@ async function takeSnapshot(cwd, sessionId, turnIndex) {
|
|
|
2493
2655
|
return false;
|
|
2494
2656
|
const files = [];
|
|
2495
2657
|
for (const entry of entries) {
|
|
2496
|
-
const absPath =
|
|
2658
|
+
const absPath = join7(repoRoot, entry.path);
|
|
2497
2659
|
if (!entry.existsOnDisk) {
|
|
2498
2660
|
const { bytes, code } = await gitBytes(["show", `HEAD:${entry.path}`], repoRoot);
|
|
2499
2661
|
if (code === 0) {
|
|
@@ -2542,7 +2704,7 @@ async function restoreSnapshot(cwd, sessionId, turnIndex) {
|
|
|
2542
2704
|
const root = repoRoot ?? cwd;
|
|
2543
2705
|
let anyFailed = false;
|
|
2544
2706
|
for (const file of files) {
|
|
2545
|
-
const absPath =
|
|
2707
|
+
const absPath = join7(root, file.path);
|
|
2546
2708
|
if (!file.existed) {
|
|
2547
2709
|
try {
|
|
2548
2710
|
if (await Bun.file(absPath).exists()) {
|
|
@@ -2609,10 +2771,75 @@ function getMostRecentSession() {
|
|
|
2609
2771
|
return sessions[0] ?? null;
|
|
2610
2772
|
}
|
|
2611
2773
|
|
|
2612
|
-
// src/tools/
|
|
2613
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
2614
|
-
import { dirname, join as join7, relative as relative2 } from "path";
|
|
2774
|
+
// src/tools/exa.ts
|
|
2615
2775
|
import { z as z2 } from "zod";
|
|
2776
|
+
var ExaSearchSchema = z2.object({
|
|
2777
|
+
query: z2.string().describe("The search query")
|
|
2778
|
+
});
|
|
2779
|
+
var webSearchTool = {
|
|
2780
|
+
name: "webSearch",
|
|
2781
|
+
description: "Search the web for a query using Exa.",
|
|
2782
|
+
schema: ExaSearchSchema,
|
|
2783
|
+
execute: async (input) => {
|
|
2784
|
+
const apiKey = process.env.EXA_API_KEY;
|
|
2785
|
+
if (!apiKey) {
|
|
2786
|
+
throw new Error("EXA_API_KEY is not set.");
|
|
2787
|
+
}
|
|
2788
|
+
const response = await fetch("https://api.exa.ai/search", {
|
|
2789
|
+
method: "POST",
|
|
2790
|
+
headers: {
|
|
2791
|
+
"Content-Type": "application/json",
|
|
2792
|
+
"x-api-key": apiKey
|
|
2793
|
+
},
|
|
2794
|
+
body: JSON.stringify({
|
|
2795
|
+
query: input.query,
|
|
2796
|
+
type: "auto",
|
|
2797
|
+
numResults: 10,
|
|
2798
|
+
contents: { text: { maxCharacters: 4000 } }
|
|
2799
|
+
})
|
|
2800
|
+
});
|
|
2801
|
+
if (!response.ok) {
|
|
2802
|
+
const errorBody = await response.text();
|
|
2803
|
+
throw new Error(`Exa API error: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
2804
|
+
}
|
|
2805
|
+
return await response.json();
|
|
2806
|
+
}
|
|
2807
|
+
};
|
|
2808
|
+
var ExaContentSchema = z2.object({
|
|
2809
|
+
urls: z2.array(z2.string()).max(3).describe("Array of URLs to retrieve content for (max 3)")
|
|
2810
|
+
});
|
|
2811
|
+
var webContentTool = {
|
|
2812
|
+
name: "webContent",
|
|
2813
|
+
description: "Get the full content of specific URLs using Exa.",
|
|
2814
|
+
schema: ExaContentSchema,
|
|
2815
|
+
execute: async (input) => {
|
|
2816
|
+
const apiKey = process.env.EXA_API_KEY;
|
|
2817
|
+
if (!apiKey) {
|
|
2818
|
+
throw new Error("EXA_API_KEY is not set.");
|
|
2819
|
+
}
|
|
2820
|
+
const response = await fetch("https://api.exa.ai/contents", {
|
|
2821
|
+
method: "POST",
|
|
2822
|
+
headers: {
|
|
2823
|
+
"Content-Type": "application/json",
|
|
2824
|
+
"x-api-key": apiKey
|
|
2825
|
+
},
|
|
2826
|
+
body: JSON.stringify({
|
|
2827
|
+
urls: input.urls,
|
|
2828
|
+
text: { maxCharacters: 1e4 }
|
|
2829
|
+
})
|
|
2830
|
+
});
|
|
2831
|
+
if (!response.ok) {
|
|
2832
|
+
const errorBody = await response.text();
|
|
2833
|
+
throw new Error(`Exa API error: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
2834
|
+
}
|
|
2835
|
+
return await response.json();
|
|
2836
|
+
}
|
|
2837
|
+
};
|
|
2838
|
+
|
|
2839
|
+
// src/tools/create.ts
|
|
2840
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
2841
|
+
import { dirname, join as join8, relative as relative2 } from "path";
|
|
2842
|
+
import { z as z3 } from "zod";
|
|
2616
2843
|
|
|
2617
2844
|
// src/tools/diff.ts
|
|
2618
2845
|
function generateDiff(filePath, before, after) {
|
|
@@ -2741,9 +2968,9 @@ ${lines.join(`
|
|
|
2741
2968
|
}
|
|
2742
2969
|
|
|
2743
2970
|
// src/tools/create.ts
|
|
2744
|
-
var CreateSchema =
|
|
2745
|
-
path:
|
|
2746
|
-
content:
|
|
2971
|
+
var CreateSchema = z3.object({
|
|
2972
|
+
path: z3.string().describe("File path to write (absolute or relative to cwd)"),
|
|
2973
|
+
content: z3.string().describe("Full content to write to the file")
|
|
2747
2974
|
});
|
|
2748
2975
|
var createTool = {
|
|
2749
2976
|
name: "create",
|
|
@@ -2751,11 +2978,11 @@ var createTool = {
|
|
|
2751
2978
|
schema: CreateSchema,
|
|
2752
2979
|
execute: async (input) => {
|
|
2753
2980
|
const cwd = input.cwd ?? process.cwd();
|
|
2754
|
-
const filePath = input.path.startsWith("/") ? input.path :
|
|
2981
|
+
const filePath = input.path.startsWith("/") ? input.path : join8(cwd, input.path);
|
|
2755
2982
|
const relPath = relative2(cwd, filePath);
|
|
2756
2983
|
const dir = dirname(filePath);
|
|
2757
2984
|
if (!existsSync5(dir))
|
|
2758
|
-
|
|
2985
|
+
mkdirSync3(dir, { recursive: true });
|
|
2759
2986
|
const file = Bun.file(filePath);
|
|
2760
2987
|
const created = !await file.exists();
|
|
2761
2988
|
const before = created ? "" : await file.text();
|
|
@@ -2766,15 +2993,15 @@ var createTool = {
|
|
|
2766
2993
|
};
|
|
2767
2994
|
|
|
2768
2995
|
// src/tools/glob.ts
|
|
2769
|
-
import { join as
|
|
2770
|
-
import { z as
|
|
2996
|
+
import { join as join10, relative as relative3 } from "path";
|
|
2997
|
+
import { z as z4 } from "zod";
|
|
2771
2998
|
|
|
2772
2999
|
// src/tools/ignore.ts
|
|
2773
|
-
import { join as
|
|
3000
|
+
import { join as join9 } from "path";
|
|
2774
3001
|
import ignore from "ignore";
|
|
2775
3002
|
async function loadGitignore(cwd) {
|
|
2776
3003
|
try {
|
|
2777
|
-
const gitignore = await Bun.file(
|
|
3004
|
+
const gitignore = await Bun.file(join9(cwd, ".gitignore")).text();
|
|
2778
3005
|
return ignore().add(gitignore);
|
|
2779
3006
|
} catch {
|
|
2780
3007
|
return null;
|
|
@@ -2782,9 +3009,9 @@ async function loadGitignore(cwd) {
|
|
|
2782
3009
|
}
|
|
2783
3010
|
|
|
2784
3011
|
// src/tools/glob.ts
|
|
2785
|
-
var GlobSchema =
|
|
2786
|
-
pattern:
|
|
2787
|
-
ignore:
|
|
3012
|
+
var GlobSchema = z4.object({
|
|
3013
|
+
pattern: z4.string().describe("Glob pattern to match files against, e.g. '**/*.ts'"),
|
|
3014
|
+
ignore: z4.array(z4.string()).optional().describe("Glob patterns to exclude")
|
|
2788
3015
|
});
|
|
2789
3016
|
var MAX_RESULTS = 500;
|
|
2790
3017
|
var globTool = {
|
|
@@ -2807,7 +3034,7 @@ var globTool = {
|
|
|
2807
3034
|
if (ignored)
|
|
2808
3035
|
continue;
|
|
2809
3036
|
try {
|
|
2810
|
-
const fullPath =
|
|
3037
|
+
const fullPath = join10(cwd, file);
|
|
2811
3038
|
const stat = await Bun.file(fullPath).stat?.() ?? null;
|
|
2812
3039
|
matches.push({ path: file, mtime: stat?.mtime?.getTime() ?? 0 });
|
|
2813
3040
|
} catch {
|
|
@@ -2820,14 +3047,14 @@ var globTool = {
|
|
|
2820
3047
|
if (truncated)
|
|
2821
3048
|
matches.pop();
|
|
2822
3049
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
2823
|
-
const files = matches.map((m) => relative3(cwd,
|
|
3050
|
+
const files = matches.map((m) => relative3(cwd, join10(cwd, m.path)));
|
|
2824
3051
|
return { files, count: files.length, truncated };
|
|
2825
3052
|
}
|
|
2826
3053
|
};
|
|
2827
3054
|
|
|
2828
3055
|
// src/tools/grep.ts
|
|
2829
|
-
import { join as
|
|
2830
|
-
import { z as
|
|
3056
|
+
import { join as join11 } from "path";
|
|
3057
|
+
import { z as z5 } from "zod";
|
|
2831
3058
|
|
|
2832
3059
|
// src/tools/hashline.ts
|
|
2833
3060
|
var FNV_OFFSET_BASIS = 2166136261;
|
|
@@ -2873,12 +3100,12 @@ function findLineByHash(lines, hash, hintLine) {
|
|
|
2873
3100
|
}
|
|
2874
3101
|
|
|
2875
3102
|
// src/tools/grep.ts
|
|
2876
|
-
var GrepSchema =
|
|
2877
|
-
pattern:
|
|
2878
|
-
include:
|
|
2879
|
-
contextLines:
|
|
2880
|
-
caseSensitive:
|
|
2881
|
-
maxResults:
|
|
3103
|
+
var GrepSchema = z5.object({
|
|
3104
|
+
pattern: z5.string().describe("Regular expression to search for"),
|
|
3105
|
+
include: z5.string().optional().describe("Glob pattern to filter files, e.g. '*.ts' or '*.{ts,tsx}'"),
|
|
3106
|
+
contextLines: z5.number().int().min(0).max(10).optional().default(2).describe("Lines of context to include around each match"),
|
|
3107
|
+
caseSensitive: z5.boolean().optional().default(true),
|
|
3108
|
+
maxResults: z5.number().int().min(1).max(200).optional().default(50)
|
|
2882
3109
|
});
|
|
2883
3110
|
var DEFAULT_IGNORE = [
|
|
2884
3111
|
"node_modules",
|
|
@@ -2917,7 +3144,7 @@ var grepTool = {
|
|
|
2917
3144
|
if (ignoreGlob.some((g) => g.match(relPath) || g.match(firstSegment))) {
|
|
2918
3145
|
continue;
|
|
2919
3146
|
}
|
|
2920
|
-
const fullPath =
|
|
3147
|
+
const fullPath = join11(cwd, relPath);
|
|
2921
3148
|
let text;
|
|
2922
3149
|
try {
|
|
2923
3150
|
text = await Bun.file(fullPath).text();
|
|
@@ -2968,8 +3195,8 @@ var grepTool = {
|
|
|
2968
3195
|
|
|
2969
3196
|
// src/tools/hooks.ts
|
|
2970
3197
|
import { constants, accessSync } from "fs";
|
|
2971
|
-
import { homedir as
|
|
2972
|
-
import { join as
|
|
3198
|
+
import { homedir as homedir7 } from "os";
|
|
3199
|
+
import { join as join12 } from "path";
|
|
2973
3200
|
function isExecutable(filePath) {
|
|
2974
3201
|
try {
|
|
2975
3202
|
accessSync(filePath, constants.X_OK);
|
|
@@ -2981,8 +3208,8 @@ function isExecutable(filePath) {
|
|
|
2981
3208
|
function findHook(toolName, cwd) {
|
|
2982
3209
|
const scriptName = `post-${toolName}`;
|
|
2983
3210
|
const candidates = [
|
|
2984
|
-
|
|
2985
|
-
|
|
3211
|
+
join12(cwd, ".agents", "hooks", scriptName),
|
|
3212
|
+
join12(homedir7(), ".agents", "hooks", scriptName)
|
|
2986
3213
|
];
|
|
2987
3214
|
for (const p of candidates) {
|
|
2988
3215
|
if (isExecutable(p))
|
|
@@ -3071,13 +3298,13 @@ function hookEnvForRead(input, cwd) {
|
|
|
3071
3298
|
}
|
|
3072
3299
|
|
|
3073
3300
|
// src/tools/insert.ts
|
|
3074
|
-
import { join as
|
|
3075
|
-
import { z as
|
|
3076
|
-
var InsertSchema =
|
|
3077
|
-
path:
|
|
3078
|
-
anchor:
|
|
3079
|
-
position:
|
|
3080
|
-
content:
|
|
3301
|
+
import { join as join13, relative as relative4 } from "path";
|
|
3302
|
+
import { z as z6 } from "zod";
|
|
3303
|
+
var InsertSchema = z6.object({
|
|
3304
|
+
path: z6.string().describe("File path to edit (absolute or relative to cwd)"),
|
|
3305
|
+
anchor: z6.string().describe('Anchor line from a prior read/grep, e.g. "11:a3"'),
|
|
3306
|
+
position: z6.enum(["before", "after"]).describe('Insert the content "before" or "after" the anchor line'),
|
|
3307
|
+
content: z6.string().describe("Text to insert")
|
|
3081
3308
|
});
|
|
3082
3309
|
var HASH_NOT_FOUND_ERROR = "Hash not found. Re-read the file to get current anchors.";
|
|
3083
3310
|
var insertTool = {
|
|
@@ -3086,7 +3313,7 @@ var insertTool = {
|
|
|
3086
3313
|
schema: InsertSchema,
|
|
3087
3314
|
execute: async (input) => {
|
|
3088
3315
|
const cwd = input.cwd ?? process.cwd();
|
|
3089
|
-
const filePath = input.path.startsWith("/") ? input.path :
|
|
3316
|
+
const filePath = input.path.startsWith("/") ? input.path : join13(cwd, input.path);
|
|
3090
3317
|
const relPath = relative4(cwd, filePath);
|
|
3091
3318
|
const file = Bun.file(filePath);
|
|
3092
3319
|
if (!await file.exists()) {
|
|
@@ -3131,12 +3358,12 @@ function parseAnchor(value) {
|
|
|
3131
3358
|
}
|
|
3132
3359
|
|
|
3133
3360
|
// src/tools/read.ts
|
|
3134
|
-
import { join as
|
|
3135
|
-
import { z as
|
|
3136
|
-
var ReadSchema =
|
|
3137
|
-
path:
|
|
3138
|
-
line:
|
|
3139
|
-
count:
|
|
3361
|
+
import { join as join14, relative as relative5 } from "path";
|
|
3362
|
+
import { z as z7 } from "zod";
|
|
3363
|
+
var ReadSchema = z7.object({
|
|
3364
|
+
path: z7.string().describe("File path to read (absolute or relative to cwd)"),
|
|
3365
|
+
line: z7.number().int().min(1).optional().describe("1-indexed starting line (default: 1)"),
|
|
3366
|
+
count: z7.number().int().min(1).max(500).optional().describe("Lines to read (default: 500, max: 500)")
|
|
3140
3367
|
});
|
|
3141
3368
|
var MAX_COUNT = 500;
|
|
3142
3369
|
var MAX_BYTES = 1e6;
|
|
@@ -3146,7 +3373,7 @@ var readTool = {
|
|
|
3146
3373
|
schema: ReadSchema,
|
|
3147
3374
|
execute: async (input) => {
|
|
3148
3375
|
const cwd = input.cwd ?? process.cwd();
|
|
3149
|
-
const filePath = input.path.startsWith("/") ? input.path :
|
|
3376
|
+
const filePath = input.path.startsWith("/") ? input.path : join14(cwd, input.path);
|
|
3150
3377
|
const file = Bun.file(filePath);
|
|
3151
3378
|
const exists = await file.exists();
|
|
3152
3379
|
if (!exists) {
|
|
@@ -3179,13 +3406,13 @@ var readTool = {
|
|
|
3179
3406
|
};
|
|
3180
3407
|
|
|
3181
3408
|
// src/tools/replace.ts
|
|
3182
|
-
import { join as
|
|
3183
|
-
import { z as
|
|
3184
|
-
var ReplaceSchema =
|
|
3185
|
-
path:
|
|
3186
|
-
startAnchor:
|
|
3187
|
-
endAnchor:
|
|
3188
|
-
newContent:
|
|
3409
|
+
import { join as join15, relative as relative6 } from "path";
|
|
3410
|
+
import { z as z8 } from "zod";
|
|
3411
|
+
var ReplaceSchema = z8.object({
|
|
3412
|
+
path: z8.string().describe("File path to edit (absolute or relative to cwd)"),
|
|
3413
|
+
startAnchor: z8.string().describe('Start anchor from a prior read/grep, e.g. "11:a3"'),
|
|
3414
|
+
endAnchor: z8.string().optional().describe('End anchor (inclusive), e.g. "33:0e". Omit to target only the startAnchor line.'),
|
|
3415
|
+
newContent: z8.string().optional().describe("Replacement text. Omit or pass empty string to delete the range.")
|
|
3189
3416
|
});
|
|
3190
3417
|
var HASH_NOT_FOUND_ERROR2 = "Hash not found. Re-read the file to get current anchors.";
|
|
3191
3418
|
var replaceTool = {
|
|
@@ -3194,7 +3421,7 @@ var replaceTool = {
|
|
|
3194
3421
|
schema: ReplaceSchema,
|
|
3195
3422
|
execute: async (input) => {
|
|
3196
3423
|
const cwd = input.cwd ?? process.cwd();
|
|
3197
|
-
const filePath = input.path.startsWith("/") ? input.path :
|
|
3424
|
+
const filePath = input.path.startsWith("/") ? input.path : join15(cwd, input.path);
|
|
3198
3425
|
const relPath = relative6(cwd, filePath);
|
|
3199
3426
|
const file = Bun.file(filePath);
|
|
3200
3427
|
if (!await file.exists()) {
|
|
@@ -3252,11 +3479,11 @@ function parseAnchor2(value, name) {
|
|
|
3252
3479
|
}
|
|
3253
3480
|
|
|
3254
3481
|
// src/tools/shell.ts
|
|
3255
|
-
import { z as
|
|
3256
|
-
var ShellSchema =
|
|
3257
|
-
command:
|
|
3258
|
-
timeout:
|
|
3259
|
-
env:
|
|
3482
|
+
import { z as z9 } from "zod";
|
|
3483
|
+
var ShellSchema = z9.object({
|
|
3484
|
+
command: z9.string().describe("Shell command to execute"),
|
|
3485
|
+
timeout: z9.number().int().min(1000).max(300000).optional().default(30000).describe("Timeout in milliseconds (default: 30s, max: 5min)"),
|
|
3486
|
+
env: z9.record(z9.string(), z9.string()).optional().describe("Additional environment variables to set")
|
|
3260
3487
|
});
|
|
3261
3488
|
var MAX_OUTPUT_BYTES = 1e4;
|
|
3262
3489
|
var shellTool = {
|
|
@@ -3268,6 +3495,7 @@ var shellTool = {
|
|
|
3268
3495
|
const timeout = input.timeout ?? 30000;
|
|
3269
3496
|
const env = Object.assign({}, process.env, input.env ?? {});
|
|
3270
3497
|
let timedOut = false;
|
|
3498
|
+
const wasRaw = process.stdin.isTTY ? process.stdin.isRaw : false;
|
|
3271
3499
|
const proc = Bun.spawn(["bash", "-c", input.command], {
|
|
3272
3500
|
cwd,
|
|
3273
3501
|
env,
|
|
@@ -3321,6 +3549,11 @@ var shellTool = {
|
|
|
3321
3549
|
} finally {
|
|
3322
3550
|
clearTimeout(timer);
|
|
3323
3551
|
restoreTerminal();
|
|
3552
|
+
if (wasRaw) {
|
|
3553
|
+
try {
|
|
3554
|
+
process.stdin.setRawMode(true);
|
|
3555
|
+
} catch {}
|
|
3556
|
+
}
|
|
3324
3557
|
}
|
|
3325
3558
|
return {
|
|
3326
3559
|
stdout: stdout.trimEnd(),
|
|
@@ -3333,12 +3566,12 @@ var shellTool = {
|
|
|
3333
3566
|
};
|
|
3334
3567
|
|
|
3335
3568
|
// src/tools/subagent.ts
|
|
3336
|
-
import { z as
|
|
3337
|
-
var SubagentInput =
|
|
3338
|
-
prompt:
|
|
3339
|
-
agentName:
|
|
3569
|
+
import { z as z10 } from "zod";
|
|
3570
|
+
var SubagentInput = z10.object({
|
|
3571
|
+
prompt: z10.string().describe("The task or question to give the subagent"),
|
|
3572
|
+
agentName: z10.string().optional().describe("Name of a custom agent to use (from .agents/agents/). Omit to use a generic subagent.")
|
|
3340
3573
|
});
|
|
3341
|
-
function createSubagentTool(runSubagent, availableAgents) {
|
|
3574
|
+
function createSubagentTool(runSubagent, availableAgents, parentLabel) {
|
|
3342
3575
|
const agentSection = availableAgents.size > 0 ? `
|
|
3343
3576
|
|
|
3344
3577
|
When the user's message contains @<agent-name>, delegate to that agent by setting agentName to the exact agent name. Available custom agents: ${[...availableAgents.entries()].map(([name, cfg]) => `"${name}" (${cfg.description})`).join(", ")}.` : "";
|
|
@@ -3347,7 +3580,7 @@ When the user's message contains @<agent-name>, delegate to that agent by settin
|
|
|
3347
3580
|
description: `Spawn a sub-agent to handle a focused subtask. Use this for parallel exploration, specialised analysis, or tasks that benefit from a fresh context window. The subagent has access to all the same tools.${agentSection}`,
|
|
3348
3581
|
schema: SubagentInput,
|
|
3349
3582
|
execute: async (input) => {
|
|
3350
|
-
return runSubagent(input.prompt, input.agentName);
|
|
3583
|
+
return runSubagent(input.prompt, input.agentName, parentLabel);
|
|
3351
3584
|
}
|
|
3352
3585
|
};
|
|
3353
3586
|
}
|
|
@@ -3396,7 +3629,7 @@ function buildToolSet(opts) {
|
|
|
3396
3629
|
const { cwd, onHook } = opts;
|
|
3397
3630
|
const depth = opts.depth ?? 0;
|
|
3398
3631
|
const lookupHook = createHookCache(HOOKABLE_TOOLS, cwd);
|
|
3399
|
-
|
|
3632
|
+
const tools = [
|
|
3400
3633
|
withHooks(withCwdDefault(globTool, cwd), lookupHook, cwd, (_, input) => hookEnvForGlob(input, cwd), onHook),
|
|
3401
3634
|
withHooks(withCwdDefault(grepTool, cwd), lookupHook, cwd, (_, input) => hookEnvForGrep(input, cwd), onHook),
|
|
3402
3635
|
withHooks(withCwdDefault(readTool, cwd), lookupHook, cwd, (_, input) => hookEnvForRead(input, cwd), onHook),
|
|
@@ -3408,17 +3641,25 @@ function buildToolSet(opts) {
|
|
|
3408
3641
|
if (depth >= MAX_SUBAGENT_DEPTH) {
|
|
3409
3642
|
throw new Error(`Subagent depth limit reached (max ${MAX_SUBAGENT_DEPTH}). ` + `Cannot spawn another subagent from depth ${depth}.`);
|
|
3410
3643
|
}
|
|
3411
|
-
return opts.runSubagent(prompt, depth + 1, agentName);
|
|
3412
|
-
}, opts.availableAgents)
|
|
3644
|
+
return opts.runSubagent(prompt, depth + 1, agentName, undefined, opts.parentLabel);
|
|
3645
|
+
}, opts.availableAgents, opts.parentLabel)
|
|
3413
3646
|
];
|
|
3647
|
+
if (process.env.EXA_API_KEY) {
|
|
3648
|
+
tools.push(webSearchTool, webContentTool);
|
|
3649
|
+
}
|
|
3650
|
+
return tools;
|
|
3414
3651
|
}
|
|
3415
3652
|
function buildReadOnlyToolSet(opts) {
|
|
3416
3653
|
const { cwd } = opts;
|
|
3417
|
-
|
|
3654
|
+
const tools = [
|
|
3418
3655
|
withCwdDefault(globTool, cwd),
|
|
3419
3656
|
withCwdDefault(grepTool, cwd),
|
|
3420
3657
|
withCwdDefault(readTool, cwd)
|
|
3421
3658
|
];
|
|
3659
|
+
if (process.env.EXA_API_KEY) {
|
|
3660
|
+
tools.push(webSearchTool, webContentTool);
|
|
3661
|
+
}
|
|
3662
|
+
return tools;
|
|
3422
3663
|
}
|
|
3423
3664
|
|
|
3424
3665
|
// src/agent/agent.ts
|
|
@@ -3440,9 +3681,9 @@ async function getGitBranch(cwd) {
|
|
|
3440
3681
|
}
|
|
3441
3682
|
function loadContextFile(cwd) {
|
|
3442
3683
|
const candidates = [
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3684
|
+
join16(cwd, "AGENTS.md"),
|
|
3685
|
+
join16(cwd, "CLAUDE.md"),
|
|
3686
|
+
join16(getConfigDir(), "AGENTS.md")
|
|
3446
3687
|
];
|
|
3447
3688
|
for (const p of candidates) {
|
|
3448
3689
|
if (existsSync6(p)) {
|
|
@@ -3519,7 +3760,9 @@ async function runAgent(opts) {
|
|
|
3519
3760
|
}
|
|
3520
3761
|
let turnIndex = getMaxTurnIndex(session.id) + 1;
|
|
3521
3762
|
const coreHistory = [...session.messages];
|
|
3522
|
-
|
|
3763
|
+
let nextLaneId = 1;
|
|
3764
|
+
const activeLanes = new Set;
|
|
3765
|
+
const runSubagent = async (prompt, depth = 0, agentName, modelOverride, parentLabel) => {
|
|
3523
3766
|
const allAgents = loadAgents(cwd);
|
|
3524
3767
|
const agentConfig = agentName ? allAgents.get(agentName) : undefined;
|
|
3525
3768
|
if (agentName && !agentConfig) {
|
|
@@ -3528,19 +3771,21 @@ async function runAgent(opts) {
|
|
|
3528
3771
|
const model = modelOverride ?? agentConfig?.model ?? currentModel;
|
|
3529
3772
|
const systemPrompt = agentConfig?.systemPrompt ?? buildSystemPrompt(cwd);
|
|
3530
3773
|
const subMessages = [{ role: "user", content: prompt }];
|
|
3774
|
+
const laneId = nextLaneId++;
|
|
3775
|
+
activeLanes.add(laneId);
|
|
3776
|
+
const laneLabel = formatSubagentLabel(laneId, parentLabel);
|
|
3531
3777
|
const subTools = buildToolSet({
|
|
3532
3778
|
cwd,
|
|
3533
3779
|
depth,
|
|
3534
3780
|
runSubagent,
|
|
3535
3781
|
onHook: renderHook,
|
|
3536
|
-
availableAgents: allAgents
|
|
3782
|
+
availableAgents: allAgents,
|
|
3783
|
+
parentLabel: laneLabel
|
|
3537
3784
|
});
|
|
3538
3785
|
const subLlm = resolveModel(model);
|
|
3539
3786
|
let result = "";
|
|
3540
3787
|
let inputTokens = 0;
|
|
3541
3788
|
let outputTokens = 0;
|
|
3542
|
-
const activity = [];
|
|
3543
|
-
const pendingCalls = new Map;
|
|
3544
3789
|
const events = runTurn({
|
|
3545
3790
|
model: subLlm,
|
|
3546
3791
|
messages: subMessages,
|
|
@@ -3548,32 +3793,18 @@ async function runAgent(opts) {
|
|
|
3548
3793
|
systemPrompt
|
|
3549
3794
|
});
|
|
3550
3795
|
for await (const event of events) {
|
|
3796
|
+
spinner.stop();
|
|
3797
|
+
renderSubagentEvent(event, { laneId, parentLabel, activeLanes });
|
|
3798
|
+
spinner.start("thinking");
|
|
3551
3799
|
if (event.type === "text-delta")
|
|
3552
3800
|
result += event.delta;
|
|
3553
|
-
if (event.type === "tool-call-start") {
|
|
3554
|
-
pendingCalls.set(event.toolCallId, {
|
|
3555
|
-
toolName: event.toolName,
|
|
3556
|
-
args: event.args
|
|
3557
|
-
});
|
|
3558
|
-
}
|
|
3559
|
-
if (event.type === "tool-result") {
|
|
3560
|
-
const pending = pendingCalls.get(event.toolCallId);
|
|
3561
|
-
if (pending) {
|
|
3562
|
-
pendingCalls.delete(event.toolCallId);
|
|
3563
|
-
activity.push({
|
|
3564
|
-
toolName: pending.toolName,
|
|
3565
|
-
args: pending.args,
|
|
3566
|
-
result: event.result,
|
|
3567
|
-
isError: event.isError
|
|
3568
|
-
});
|
|
3569
|
-
}
|
|
3570
|
-
}
|
|
3571
3801
|
if (event.type === "turn-complete") {
|
|
3572
3802
|
inputTokens = event.inputTokens;
|
|
3573
3803
|
outputTokens = event.outputTokens;
|
|
3574
3804
|
}
|
|
3575
3805
|
}
|
|
3576
|
-
|
|
3806
|
+
activeLanes.delete(laneId);
|
|
3807
|
+
return { result, inputTokens, outputTokens };
|
|
3577
3808
|
};
|
|
3578
3809
|
const agents = loadAgents(cwd);
|
|
3579
3810
|
const tools = buildToolSet({
|
|
@@ -3760,12 +3991,10 @@ ${out}
|
|
|
3760
3991
|
async function processUserInput(text, pastedImages = []) {
|
|
3761
3992
|
const abortController = new AbortController;
|
|
3762
3993
|
let wasAborted = false;
|
|
3763
|
-
|
|
3994
|
+
abortController.signal.addEventListener("abort", () => {
|
|
3764
3995
|
wasAborted = true;
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
};
|
|
3768
|
-
process.on("SIGINT", onSigInt);
|
|
3996
|
+
});
|
|
3997
|
+
const stopWatcher = watchForInterrupt(abortController);
|
|
3769
3998
|
const { text: resolvedText, images: refImages } = await resolveFileRefs(text, cwd);
|
|
3770
3999
|
const allImages = [...pastedImages, ...refImages];
|
|
3771
4000
|
const thisTurn = turnIndex++;
|
|
@@ -3787,7 +4016,7 @@ ${out}
|
|
|
3787
4016
|
]
|
|
3788
4017
|
} : { role: "user", content: coreContent };
|
|
3789
4018
|
if (wasAborted) {
|
|
3790
|
-
|
|
4019
|
+
stopWatcher();
|
|
3791
4020
|
const stubMsg = {
|
|
3792
4021
|
role: "assistant",
|
|
3793
4022
|
content: "[interrupted]"
|
|
@@ -3853,7 +4082,7 @@ ${out}
|
|
|
3853
4082
|
rollbackTurn();
|
|
3854
4083
|
throw err;
|
|
3855
4084
|
} finally {
|
|
3856
|
-
|
|
4085
|
+
stopWatcher();
|
|
3857
4086
|
if (wasAborted)
|
|
3858
4087
|
ralphMode = false;
|
|
3859
4088
|
}
|
|
@@ -3917,7 +4146,7 @@ ${skill.content}
|
|
|
3917
4146
|
result = result.slice(0, match.index) + replacement + result.slice((match.index ?? 0) + match[0].length);
|
|
3918
4147
|
continue;
|
|
3919
4148
|
}
|
|
3920
|
-
const filePath = ref.startsWith("/") ? ref :
|
|
4149
|
+
const filePath = ref.startsWith("/") ? ref : join16(cwd, ref);
|
|
3921
4150
|
if (isImageFilename(ref)) {
|
|
3922
4151
|
const attachment = await loadImageFile(filePath);
|
|
3923
4152
|
if (attachment) {
|
|
@@ -3945,6 +4174,7 @@ ${preview}
|
|
|
3945
4174
|
|
|
3946
4175
|
// src/index.ts
|
|
3947
4176
|
registerTerminalCleanup();
|
|
4177
|
+
initErrorLog();
|
|
3948
4178
|
function parseArgs(argv) {
|
|
3949
4179
|
const args = {
|
|
3950
4180
|
model: null,
|
|
@@ -4063,11 +4293,11 @@ async function main() {
|
|
|
4063
4293
|
agentOpts.initialPrompt = args.prompt;
|
|
4064
4294
|
await runAgent(agentOpts);
|
|
4065
4295
|
} catch (err) {
|
|
4066
|
-
renderError(err);
|
|
4296
|
+
renderError(err, "agent");
|
|
4067
4297
|
process.exit(1);
|
|
4068
4298
|
}
|
|
4069
4299
|
}
|
|
4070
4300
|
main().catch((err) => {
|
|
4071
|
-
|
|
4301
|
+
renderError(err, "main");
|
|
4072
4302
|
process.exit(1);
|
|
4073
4303
|
});
|
package/package.json
CHANGED
package/better-errors.md
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
# Better Errors — Implementation Plan
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
Add structured error logging to file and friendly error parsing for display. Three concerns: (1) log full error details to `~/.config/mini-coder/errors.log`, (2) map known AI SDK errors to terse user-facing messages, (3) wire both into existing error surfaces.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Files to Create
|
|
10
|
-
|
|
11
|
-
| File | Purpose |
|
|
12
|
-
|---|---|
|
|
13
|
-
| `src/cli/error-log.ts` | `initErrorLog()`, `logError()` |
|
|
14
|
-
| `src/cli/error-parse.ts` | `parseAppError()` |
|
|
15
|
-
|
|
16
|
-
## Files to Modify
|
|
17
|
-
|
|
18
|
-
| File | Change |
|
|
19
|
-
|---|---|
|
|
20
|
-
| `src/index.ts` | Call `initErrorLog()` at startup; use `logError` + `parseAppError` in top-level catch and `main().catch()` |
|
|
21
|
-
| `src/cli/output.ts` | Update `renderError(err: unknown)` to call log + parse; update `turn-error` branch in `renderTurn()` |
|
|
22
|
-
| `src/agent/agent.ts` | Pass `unknown` to `renderError()` — no logic change needed if signature widens correctly |
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## Implementation Steps
|
|
27
|
-
|
|
28
|
-
1. **Create `src/cli/error-log.ts`**
|
|
29
|
-
- Module-level `let writer: ReturnType<ReturnType<typeof Bun.file>['writer']> | null = null`
|
|
30
|
-
- `initErrorLog()`: if `writer` is not null, return early (idempotency). Otherwise resolve path `~/.config/mini-coder/errors.log`, open with `Bun.file(path).writer()` (truncates on open), assign to `writer`. Register `process.on('uncaughtException', (err) => { logError(err, 'uncaught'); process.exit(1) })`.
|
|
31
|
-
- `logError(err: unknown, context?: string)`: if `writer` is null, return. Build log entry string (see Log Format), call `writer.write(entry)`. Keep sync-ish by not awaiting — `write()` on a Bun file writer is buffered; call `writer.flush()` after each write so data lands before a crash.
|
|
32
|
-
- Extract error fields via type-narrowing helpers (not exported): `isObject(err)`, read `.name`, `.message`, `.stack`, `.statusCode`, `.url`, `.isRetryable` defensively.
|
|
33
|
-
|
|
34
|
-
2. **Create `src/cli/error-parse.ts`**
|
|
35
|
-
- Import AI SDK error classes: `APICallError`, `RetryError`, `NoContentGeneratedError`, `LoadAPIKeyError`, `NoSuchModelError` from `ai`.
|
|
36
|
-
- Export `parseAppError(err: unknown): { headline: string; hint?: string }`.
|
|
37
|
-
- Implement as a chain of `instanceof` checks (see Error Parse Table). For `RetryError`, recurse on `.lastError` and prepend `"Retries exhausted: "` to `headline`.
|
|
38
|
-
- Fallback: extract first non-empty line of `(err as any)?.message ?? String(err)`, no hint.
|
|
39
|
-
- Network check: before the fallback, check if `(err as any)?.code === 'ECONNREFUSED'` or message includes `'ECONNREFUSED'`.
|
|
40
|
-
|
|
41
|
-
3. **Update `src/cli/output.ts`**
|
|
42
|
-
- Change `renderError` signature from `(msg: string)` to `(err: unknown)`. Inside: call `logError(err, 'render')`, call `parseAppError(err)`, print `✖ red(headline)`, if `hint` print a dim indented hint line (e.g. ` dim(hint)`).
|
|
43
|
-
- In `renderTurn()` `turn-error` branch (non-abort path): replace raw `event.error.message` display with `logError(event.error, 'turn')` then `parseAppError(event.error)` → print `✖ red(headline)`, optional dim hint. Keep the abort quiet-note branch unchanged.
|
|
44
|
-
- All callers of `renderError` that currently pass a string (e.g. in `agent.ts`) — check each call site; if passing a plain string, wrap in `new Error(string)` or let `parseAppError` fallback handle a string gracefully (add string branch at top of `parseAppError`: `if (typeof err === 'string') return { headline: err }`).
|
|
45
|
-
|
|
46
|
-
4. **Update `src/index.ts`**
|
|
47
|
-
- After `registerTerminalCleanup()` (or equivalent startup call), add `initErrorLog()`.
|
|
48
|
-
- Top-level `catch` around `runAgent()`: replace any raw print with `logError(err, 'agent')` + `parseAppError(err)` → print `✖ red(headline)` + dim hint, then `process.exit(1)`.
|
|
49
|
-
- `main().catch()`: same pattern — `logError(err, 'main')` + parse + print + `process.exit(1)`. Remove bare `console.error(err)`.
|
|
50
|
-
|
|
51
|
-
5. **Tests (`src/cli/error-parse.test.ts`)**
|
|
52
|
-
- Test `parseAppError` for each mapped error type.
|
|
53
|
-
- Construct real instances where possible (e.g. `new APICallError({ ... })`); check `headline` and `hint` values.
|
|
54
|
-
- Test `RetryError` unwrapping.
|
|
55
|
-
- Test fallback for plain `Error` and plain string.
|
|
56
|
-
- No mocks, no file I/O, no server calls.
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## Error Parse Table
|
|
61
|
-
|
|
62
|
-
| Condition | `headline` | `hint` |
|
|
63
|
-
|---|---|---|
|
|
64
|
-
| `APICallError` with `statusCode === 429` | `"Rate limit hit"` | `"Wait a moment and retry, or switch model with /model"` |
|
|
65
|
-
| `APICallError` with `statusCode === 401 \|\| 403` | `"Auth failed"` | `"Check the relevant provider API key env var"` |
|
|
66
|
-
| `APICallError` other | `"API error \${statusCode}"` | `url` if present |
|
|
67
|
-
| `RetryError` | `"Retries exhausted: \${inner.headline}"` | inner `hint` |
|
|
68
|
-
| `NoContentGeneratedError` | `"Model returned empty response"` | `"Try rephrasing or switching model with /model"` |
|
|
69
|
-
| `LoadAPIKeyError` | `"API key not found"` | `"Set the relevant provider env var"` |
|
|
70
|
-
| `NoSuchModelError` | `"Model not found"` | `"Use /model to pick a valid model"` |
|
|
71
|
-
| `code === 'ECONNREFUSED'` or message contains `'ECONNREFUSED'` | `"Connection failed"` | `"Check network or local server"` |
|
|
72
|
-
| `string` input | string value | — |
|
|
73
|
-
| fallback | first line of `err.message` | — |
|
|
74
|
-
|
|
75
|
-
> `AbortError` is never passed to `parseAppError` — abort is handled at the call sites before reaching these functions.
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
## Log Format
|
|
80
|
-
|
|
81
|
-
```
|
|
82
|
-
[2026-02-25T22:38:53.123Z] context=turn
|
|
83
|
-
name: APICallError
|
|
84
|
-
message: 429 Too Many Requests
|
|
85
|
-
statusCode: 429
|
|
86
|
-
url: https://api.anthropic.com/v1/messages
|
|
87
|
-
isRetryable: true
|
|
88
|
-
stack: APICallError: 429 Too Many Requests
|
|
89
|
-
at ...
|
|
90
|
-
---
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
- Each entry ends with `---\n`.
|
|
94
|
-
- Extra fields (`statusCode`, `url`, `isRetryable`) are only emitted if present on the error object.
|
|
95
|
-
- Stack is indented two spaces per line.
|
|
96
|
-
- File is truncated on each app start (writer opened without append flag).
|