mini-coder 0.0.6 → 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 +430 -186
- package/package.json +2 -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.
|
|
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,11 +2993,25 @@ var createTool = {
|
|
|
2766
2993
|
};
|
|
2767
2994
|
|
|
2768
2995
|
// src/tools/glob.ts
|
|
2769
|
-
import { join as
|
|
2770
|
-
import { z as
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2996
|
+
import { join as join10, relative as relative3 } from "path";
|
|
2997
|
+
import { z as z4 } from "zod";
|
|
2998
|
+
|
|
2999
|
+
// src/tools/ignore.ts
|
|
3000
|
+
import { join as join9 } from "path";
|
|
3001
|
+
import ignore from "ignore";
|
|
3002
|
+
async function loadGitignore(cwd) {
|
|
3003
|
+
try {
|
|
3004
|
+
const gitignore = await Bun.file(join9(cwd, ".gitignore")).text();
|
|
3005
|
+
return ignore().add(gitignore);
|
|
3006
|
+
} catch {
|
|
3007
|
+
return null;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
// src/tools/glob.ts
|
|
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")
|
|
2774
3015
|
});
|
|
2775
3016
|
var MAX_RESULTS = 500;
|
|
2776
3017
|
var globTool = {
|
|
@@ -2779,26 +3020,21 @@ var globTool = {
|
|
|
2779
3020
|
schema: GlobSchema,
|
|
2780
3021
|
execute: async (input) => {
|
|
2781
3022
|
const cwd = input.cwd ?? process.cwd();
|
|
2782
|
-
const defaultIgnore = [
|
|
2783
|
-
"node_modules/**",
|
|
2784
|
-
".git/**",
|
|
2785
|
-
"dist/**",
|
|
2786
|
-
"*.db",
|
|
2787
|
-
"*.db-shm",
|
|
2788
|
-
"*.db-wal"
|
|
2789
|
-
];
|
|
3023
|
+
const defaultIgnore = [".git/**", "node_modules/**"];
|
|
2790
3024
|
const ignorePatterns = [...defaultIgnore, ...input.ignore ?? []];
|
|
3025
|
+
const ignoreGlobs = ignorePatterns.map((pat) => new Bun.Glob(pat));
|
|
3026
|
+
const ig = await loadGitignore(cwd);
|
|
2791
3027
|
const glob = new Bun.Glob(input.pattern);
|
|
2792
3028
|
const matches = [];
|
|
2793
|
-
for await (const file of glob.scan({ cwd, onlyFiles: true })) {
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3029
|
+
for await (const file of glob.scan({ cwd, onlyFiles: true, dot: true })) {
|
|
3030
|
+
if (ig?.ignores(file))
|
|
3031
|
+
continue;
|
|
3032
|
+
const firstSegment = file.split("/")[0] ?? "";
|
|
3033
|
+
const ignored = ignoreGlobs.some((g) => g.match(file) || g.match(firstSegment));
|
|
2798
3034
|
if (ignored)
|
|
2799
3035
|
continue;
|
|
2800
3036
|
try {
|
|
2801
|
-
const fullPath =
|
|
3037
|
+
const fullPath = join10(cwd, file);
|
|
2802
3038
|
const stat = await Bun.file(fullPath).stat?.() ?? null;
|
|
2803
3039
|
matches.push({ path: file, mtime: stat?.mtime?.getTime() ?? 0 });
|
|
2804
3040
|
} catch {
|
|
@@ -2811,14 +3047,14 @@ var globTool = {
|
|
|
2811
3047
|
if (truncated)
|
|
2812
3048
|
matches.pop();
|
|
2813
3049
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
2814
|
-
const files = matches.map((m) => relative3(cwd,
|
|
3050
|
+
const files = matches.map((m) => relative3(cwd, join10(cwd, m.path)));
|
|
2815
3051
|
return { files, count: files.length, truncated };
|
|
2816
3052
|
}
|
|
2817
3053
|
};
|
|
2818
3054
|
|
|
2819
3055
|
// src/tools/grep.ts
|
|
2820
|
-
import { join as
|
|
2821
|
-
import { z as
|
|
3056
|
+
import { join as join11 } from "path";
|
|
3057
|
+
import { z as z5 } from "zod";
|
|
2822
3058
|
|
|
2823
3059
|
// src/tools/hashline.ts
|
|
2824
3060
|
var FNV_OFFSET_BASIS = 2166136261;
|
|
@@ -2864,12 +3100,12 @@ function findLineByHash(lines, hash, hintLine) {
|
|
|
2864
3100
|
}
|
|
2865
3101
|
|
|
2866
3102
|
// src/tools/grep.ts
|
|
2867
|
-
var GrepSchema =
|
|
2868
|
-
pattern:
|
|
2869
|
-
include:
|
|
2870
|
-
contextLines:
|
|
2871
|
-
caseSensitive:
|
|
2872
|
-
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)
|
|
2873
3109
|
});
|
|
2874
3110
|
var DEFAULT_IGNORE = [
|
|
2875
3111
|
"node_modules",
|
|
@@ -2895,15 +3131,20 @@ var grepTool = {
|
|
|
2895
3131
|
const ignoreGlob = DEFAULT_IGNORE.map((p) => new Bun.Glob(p));
|
|
2896
3132
|
const allMatches = [];
|
|
2897
3133
|
let truncated = false;
|
|
3134
|
+
const ig = await loadGitignore(cwd);
|
|
2898
3135
|
outer:
|
|
2899
3136
|
for await (const relPath of fileGlob.scan({
|
|
2900
3137
|
cwd,
|
|
2901
|
-
onlyFiles: true
|
|
3138
|
+
onlyFiles: true,
|
|
3139
|
+
dot: true
|
|
2902
3140
|
})) {
|
|
2903
|
-
if (
|
|
3141
|
+
if (ig?.ignores(relPath))
|
|
3142
|
+
continue;
|
|
3143
|
+
const firstSegment = relPath.split("/")[0] ?? "";
|
|
3144
|
+
if (ignoreGlob.some((g) => g.match(relPath) || g.match(firstSegment))) {
|
|
2904
3145
|
continue;
|
|
2905
3146
|
}
|
|
2906
|
-
const fullPath =
|
|
3147
|
+
const fullPath = join11(cwd, relPath);
|
|
2907
3148
|
let text;
|
|
2908
3149
|
try {
|
|
2909
3150
|
text = await Bun.file(fullPath).text();
|
|
@@ -2954,8 +3195,8 @@ var grepTool = {
|
|
|
2954
3195
|
|
|
2955
3196
|
// src/tools/hooks.ts
|
|
2956
3197
|
import { constants, accessSync } from "fs";
|
|
2957
|
-
import { homedir as
|
|
2958
|
-
import { join as
|
|
3198
|
+
import { homedir as homedir7 } from "os";
|
|
3199
|
+
import { join as join12 } from "path";
|
|
2959
3200
|
function isExecutable(filePath) {
|
|
2960
3201
|
try {
|
|
2961
3202
|
accessSync(filePath, constants.X_OK);
|
|
@@ -2967,8 +3208,8 @@ function isExecutable(filePath) {
|
|
|
2967
3208
|
function findHook(toolName, cwd) {
|
|
2968
3209
|
const scriptName = `post-${toolName}`;
|
|
2969
3210
|
const candidates = [
|
|
2970
|
-
|
|
2971
|
-
|
|
3211
|
+
join12(cwd, ".agents", "hooks", scriptName),
|
|
3212
|
+
join12(homedir7(), ".agents", "hooks", scriptName)
|
|
2972
3213
|
];
|
|
2973
3214
|
for (const p of candidates) {
|
|
2974
3215
|
if (isExecutable(p))
|
|
@@ -3057,13 +3298,13 @@ function hookEnvForRead(input, cwd) {
|
|
|
3057
3298
|
}
|
|
3058
3299
|
|
|
3059
3300
|
// src/tools/insert.ts
|
|
3060
|
-
import { join as
|
|
3061
|
-
import { z as
|
|
3062
|
-
var InsertSchema =
|
|
3063
|
-
path:
|
|
3064
|
-
anchor:
|
|
3065
|
-
position:
|
|
3066
|
-
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")
|
|
3067
3308
|
});
|
|
3068
3309
|
var HASH_NOT_FOUND_ERROR = "Hash not found. Re-read the file to get current anchors.";
|
|
3069
3310
|
var insertTool = {
|
|
@@ -3072,7 +3313,7 @@ var insertTool = {
|
|
|
3072
3313
|
schema: InsertSchema,
|
|
3073
3314
|
execute: async (input) => {
|
|
3074
3315
|
const cwd = input.cwd ?? process.cwd();
|
|
3075
|
-
const filePath = input.path.startsWith("/") ? input.path :
|
|
3316
|
+
const filePath = input.path.startsWith("/") ? input.path : join13(cwd, input.path);
|
|
3076
3317
|
const relPath = relative4(cwd, filePath);
|
|
3077
3318
|
const file = Bun.file(filePath);
|
|
3078
3319
|
if (!await file.exists()) {
|
|
@@ -3117,12 +3358,12 @@ function parseAnchor(value) {
|
|
|
3117
3358
|
}
|
|
3118
3359
|
|
|
3119
3360
|
// src/tools/read.ts
|
|
3120
|
-
import { join as
|
|
3121
|
-
import { z as
|
|
3122
|
-
var ReadSchema =
|
|
3123
|
-
path:
|
|
3124
|
-
line:
|
|
3125
|
-
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)")
|
|
3126
3367
|
});
|
|
3127
3368
|
var MAX_COUNT = 500;
|
|
3128
3369
|
var MAX_BYTES = 1e6;
|
|
@@ -3132,7 +3373,7 @@ var readTool = {
|
|
|
3132
3373
|
schema: ReadSchema,
|
|
3133
3374
|
execute: async (input) => {
|
|
3134
3375
|
const cwd = input.cwd ?? process.cwd();
|
|
3135
|
-
const filePath = input.path.startsWith("/") ? input.path :
|
|
3376
|
+
const filePath = input.path.startsWith("/") ? input.path : join14(cwd, input.path);
|
|
3136
3377
|
const file = Bun.file(filePath);
|
|
3137
3378
|
const exists = await file.exists();
|
|
3138
3379
|
if (!exists) {
|
|
@@ -3165,13 +3406,13 @@ var readTool = {
|
|
|
3165
3406
|
};
|
|
3166
3407
|
|
|
3167
3408
|
// src/tools/replace.ts
|
|
3168
|
-
import { join as
|
|
3169
|
-
import { z as
|
|
3170
|
-
var ReplaceSchema =
|
|
3171
|
-
path:
|
|
3172
|
-
startAnchor:
|
|
3173
|
-
endAnchor:
|
|
3174
|
-
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.")
|
|
3175
3416
|
});
|
|
3176
3417
|
var HASH_NOT_FOUND_ERROR2 = "Hash not found. Re-read the file to get current anchors.";
|
|
3177
3418
|
var replaceTool = {
|
|
@@ -3180,7 +3421,7 @@ var replaceTool = {
|
|
|
3180
3421
|
schema: ReplaceSchema,
|
|
3181
3422
|
execute: async (input) => {
|
|
3182
3423
|
const cwd = input.cwd ?? process.cwd();
|
|
3183
|
-
const filePath = input.path.startsWith("/") ? input.path :
|
|
3424
|
+
const filePath = input.path.startsWith("/") ? input.path : join15(cwd, input.path);
|
|
3184
3425
|
const relPath = relative6(cwd, filePath);
|
|
3185
3426
|
const file = Bun.file(filePath);
|
|
3186
3427
|
if (!await file.exists()) {
|
|
@@ -3238,11 +3479,11 @@ function parseAnchor2(value, name) {
|
|
|
3238
3479
|
}
|
|
3239
3480
|
|
|
3240
3481
|
// src/tools/shell.ts
|
|
3241
|
-
import { z as
|
|
3242
|
-
var ShellSchema =
|
|
3243
|
-
command:
|
|
3244
|
-
timeout:
|
|
3245
|
-
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")
|
|
3246
3487
|
});
|
|
3247
3488
|
var MAX_OUTPUT_BYTES = 1e4;
|
|
3248
3489
|
var shellTool = {
|
|
@@ -3254,6 +3495,7 @@ var shellTool = {
|
|
|
3254
3495
|
const timeout = input.timeout ?? 30000;
|
|
3255
3496
|
const env = Object.assign({}, process.env, input.env ?? {});
|
|
3256
3497
|
let timedOut = false;
|
|
3498
|
+
const wasRaw = process.stdin.isTTY ? process.stdin.isRaw : false;
|
|
3257
3499
|
const proc = Bun.spawn(["bash", "-c", input.command], {
|
|
3258
3500
|
cwd,
|
|
3259
3501
|
env,
|
|
@@ -3307,6 +3549,11 @@ var shellTool = {
|
|
|
3307
3549
|
} finally {
|
|
3308
3550
|
clearTimeout(timer);
|
|
3309
3551
|
restoreTerminal();
|
|
3552
|
+
if (wasRaw) {
|
|
3553
|
+
try {
|
|
3554
|
+
process.stdin.setRawMode(true);
|
|
3555
|
+
} catch {}
|
|
3556
|
+
}
|
|
3310
3557
|
}
|
|
3311
3558
|
return {
|
|
3312
3559
|
stdout: stdout.trimEnd(),
|
|
@@ -3319,12 +3566,12 @@ var shellTool = {
|
|
|
3319
3566
|
};
|
|
3320
3567
|
|
|
3321
3568
|
// src/tools/subagent.ts
|
|
3322
|
-
import { z as
|
|
3323
|
-
var SubagentInput =
|
|
3324
|
-
prompt:
|
|
3325
|
-
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.")
|
|
3326
3573
|
});
|
|
3327
|
-
function createSubagentTool(runSubagent, availableAgents) {
|
|
3574
|
+
function createSubagentTool(runSubagent, availableAgents, parentLabel) {
|
|
3328
3575
|
const agentSection = availableAgents.size > 0 ? `
|
|
3329
3576
|
|
|
3330
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(", ")}.` : "";
|
|
@@ -3333,7 +3580,7 @@ When the user's message contains @<agent-name>, delegate to that agent by settin
|
|
|
3333
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}`,
|
|
3334
3581
|
schema: SubagentInput,
|
|
3335
3582
|
execute: async (input) => {
|
|
3336
|
-
return runSubagent(input.prompt, input.agentName);
|
|
3583
|
+
return runSubagent(input.prompt, input.agentName, parentLabel);
|
|
3337
3584
|
}
|
|
3338
3585
|
};
|
|
3339
3586
|
}
|
|
@@ -3382,7 +3629,7 @@ function buildToolSet(opts) {
|
|
|
3382
3629
|
const { cwd, onHook } = opts;
|
|
3383
3630
|
const depth = opts.depth ?? 0;
|
|
3384
3631
|
const lookupHook = createHookCache(HOOKABLE_TOOLS, cwd);
|
|
3385
|
-
|
|
3632
|
+
const tools = [
|
|
3386
3633
|
withHooks(withCwdDefault(globTool, cwd), lookupHook, cwd, (_, input) => hookEnvForGlob(input, cwd), onHook),
|
|
3387
3634
|
withHooks(withCwdDefault(grepTool, cwd), lookupHook, cwd, (_, input) => hookEnvForGrep(input, cwd), onHook),
|
|
3388
3635
|
withHooks(withCwdDefault(readTool, cwd), lookupHook, cwd, (_, input) => hookEnvForRead(input, cwd), onHook),
|
|
@@ -3394,17 +3641,25 @@ function buildToolSet(opts) {
|
|
|
3394
3641
|
if (depth >= MAX_SUBAGENT_DEPTH) {
|
|
3395
3642
|
throw new Error(`Subagent depth limit reached (max ${MAX_SUBAGENT_DEPTH}). ` + `Cannot spawn another subagent from depth ${depth}.`);
|
|
3396
3643
|
}
|
|
3397
|
-
return opts.runSubagent(prompt, depth + 1, agentName);
|
|
3398
|
-
}, opts.availableAgents)
|
|
3644
|
+
return opts.runSubagent(prompt, depth + 1, agentName, undefined, opts.parentLabel);
|
|
3645
|
+
}, opts.availableAgents, opts.parentLabel)
|
|
3399
3646
|
];
|
|
3647
|
+
if (process.env.EXA_API_KEY) {
|
|
3648
|
+
tools.push(webSearchTool, webContentTool);
|
|
3649
|
+
}
|
|
3650
|
+
return tools;
|
|
3400
3651
|
}
|
|
3401
3652
|
function buildReadOnlyToolSet(opts) {
|
|
3402
3653
|
const { cwd } = opts;
|
|
3403
|
-
|
|
3654
|
+
const tools = [
|
|
3404
3655
|
withCwdDefault(globTool, cwd),
|
|
3405
3656
|
withCwdDefault(grepTool, cwd),
|
|
3406
3657
|
withCwdDefault(readTool, cwd)
|
|
3407
3658
|
];
|
|
3659
|
+
if (process.env.EXA_API_KEY) {
|
|
3660
|
+
tools.push(webSearchTool, webContentTool);
|
|
3661
|
+
}
|
|
3662
|
+
return tools;
|
|
3408
3663
|
}
|
|
3409
3664
|
|
|
3410
3665
|
// src/agent/agent.ts
|
|
@@ -3426,9 +3681,9 @@ async function getGitBranch(cwd) {
|
|
|
3426
3681
|
}
|
|
3427
3682
|
function loadContextFile(cwd) {
|
|
3428
3683
|
const candidates = [
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3684
|
+
join16(cwd, "AGENTS.md"),
|
|
3685
|
+
join16(cwd, "CLAUDE.md"),
|
|
3686
|
+
join16(getConfigDir(), "AGENTS.md")
|
|
3432
3687
|
];
|
|
3433
3688
|
for (const p of candidates) {
|
|
3434
3689
|
if (existsSync6(p)) {
|
|
@@ -3505,7 +3760,9 @@ async function runAgent(opts) {
|
|
|
3505
3760
|
}
|
|
3506
3761
|
let turnIndex = getMaxTurnIndex(session.id) + 1;
|
|
3507
3762
|
const coreHistory = [...session.messages];
|
|
3508
|
-
|
|
3763
|
+
let nextLaneId = 1;
|
|
3764
|
+
const activeLanes = new Set;
|
|
3765
|
+
const runSubagent = async (prompt, depth = 0, agentName, modelOverride, parentLabel) => {
|
|
3509
3766
|
const allAgents = loadAgents(cwd);
|
|
3510
3767
|
const agentConfig = agentName ? allAgents.get(agentName) : undefined;
|
|
3511
3768
|
if (agentName && !agentConfig) {
|
|
@@ -3514,19 +3771,21 @@ async function runAgent(opts) {
|
|
|
3514
3771
|
const model = modelOverride ?? agentConfig?.model ?? currentModel;
|
|
3515
3772
|
const systemPrompt = agentConfig?.systemPrompt ?? buildSystemPrompt(cwd);
|
|
3516
3773
|
const subMessages = [{ role: "user", content: prompt }];
|
|
3774
|
+
const laneId = nextLaneId++;
|
|
3775
|
+
activeLanes.add(laneId);
|
|
3776
|
+
const laneLabel = formatSubagentLabel(laneId, parentLabel);
|
|
3517
3777
|
const subTools = buildToolSet({
|
|
3518
3778
|
cwd,
|
|
3519
3779
|
depth,
|
|
3520
3780
|
runSubagent,
|
|
3521
3781
|
onHook: renderHook,
|
|
3522
|
-
availableAgents: allAgents
|
|
3782
|
+
availableAgents: allAgents,
|
|
3783
|
+
parentLabel: laneLabel
|
|
3523
3784
|
});
|
|
3524
3785
|
const subLlm = resolveModel(model);
|
|
3525
3786
|
let result = "";
|
|
3526
3787
|
let inputTokens = 0;
|
|
3527
3788
|
let outputTokens = 0;
|
|
3528
|
-
const activity = [];
|
|
3529
|
-
const pendingCalls = new Map;
|
|
3530
3789
|
const events = runTurn({
|
|
3531
3790
|
model: subLlm,
|
|
3532
3791
|
messages: subMessages,
|
|
@@ -3534,32 +3793,18 @@ async function runAgent(opts) {
|
|
|
3534
3793
|
systemPrompt
|
|
3535
3794
|
});
|
|
3536
3795
|
for await (const event of events) {
|
|
3796
|
+
spinner.stop();
|
|
3797
|
+
renderSubagentEvent(event, { laneId, parentLabel, activeLanes });
|
|
3798
|
+
spinner.start("thinking");
|
|
3537
3799
|
if (event.type === "text-delta")
|
|
3538
3800
|
result += event.delta;
|
|
3539
|
-
if (event.type === "tool-call-start") {
|
|
3540
|
-
pendingCalls.set(event.toolCallId, {
|
|
3541
|
-
toolName: event.toolName,
|
|
3542
|
-
args: event.args
|
|
3543
|
-
});
|
|
3544
|
-
}
|
|
3545
|
-
if (event.type === "tool-result") {
|
|
3546
|
-
const pending = pendingCalls.get(event.toolCallId);
|
|
3547
|
-
if (pending) {
|
|
3548
|
-
pendingCalls.delete(event.toolCallId);
|
|
3549
|
-
activity.push({
|
|
3550
|
-
toolName: pending.toolName,
|
|
3551
|
-
args: pending.args,
|
|
3552
|
-
result: event.result,
|
|
3553
|
-
isError: event.isError
|
|
3554
|
-
});
|
|
3555
|
-
}
|
|
3556
|
-
}
|
|
3557
3801
|
if (event.type === "turn-complete") {
|
|
3558
3802
|
inputTokens = event.inputTokens;
|
|
3559
3803
|
outputTokens = event.outputTokens;
|
|
3560
3804
|
}
|
|
3561
3805
|
}
|
|
3562
|
-
|
|
3806
|
+
activeLanes.delete(laneId);
|
|
3807
|
+
return { result, inputTokens, outputTokens };
|
|
3563
3808
|
};
|
|
3564
3809
|
const agents = loadAgents(cwd);
|
|
3565
3810
|
const tools = buildToolSet({
|
|
@@ -3746,12 +3991,10 @@ ${out}
|
|
|
3746
3991
|
async function processUserInput(text, pastedImages = []) {
|
|
3747
3992
|
const abortController = new AbortController;
|
|
3748
3993
|
let wasAborted = false;
|
|
3749
|
-
|
|
3994
|
+
abortController.signal.addEventListener("abort", () => {
|
|
3750
3995
|
wasAborted = true;
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
};
|
|
3754
|
-
process.on("SIGINT", onSigInt);
|
|
3996
|
+
});
|
|
3997
|
+
const stopWatcher = watchForInterrupt(abortController);
|
|
3755
3998
|
const { text: resolvedText, images: refImages } = await resolveFileRefs(text, cwd);
|
|
3756
3999
|
const allImages = [...pastedImages, ...refImages];
|
|
3757
4000
|
const thisTurn = turnIndex++;
|
|
@@ -3773,7 +4016,7 @@ ${out}
|
|
|
3773
4016
|
]
|
|
3774
4017
|
} : { role: "user", content: coreContent };
|
|
3775
4018
|
if (wasAborted) {
|
|
3776
|
-
|
|
4019
|
+
stopWatcher();
|
|
3777
4020
|
const stubMsg = {
|
|
3778
4021
|
role: "assistant",
|
|
3779
4022
|
content: "[interrupted]"
|
|
@@ -3839,7 +4082,7 @@ ${out}
|
|
|
3839
4082
|
rollbackTurn();
|
|
3840
4083
|
throw err;
|
|
3841
4084
|
} finally {
|
|
3842
|
-
|
|
4085
|
+
stopWatcher();
|
|
3843
4086
|
if (wasAborted)
|
|
3844
4087
|
ralphMode = false;
|
|
3845
4088
|
}
|
|
@@ -3903,7 +4146,7 @@ ${skill.content}
|
|
|
3903
4146
|
result = result.slice(0, match.index) + replacement + result.slice((match.index ?? 0) + match[0].length);
|
|
3904
4147
|
continue;
|
|
3905
4148
|
}
|
|
3906
|
-
const filePath = ref.startsWith("/") ? ref :
|
|
4149
|
+
const filePath = ref.startsWith("/") ? ref : join16(cwd, ref);
|
|
3907
4150
|
if (isImageFilename(ref)) {
|
|
3908
4151
|
const attachment = await loadImageFile(filePath);
|
|
3909
4152
|
if (attachment) {
|
|
@@ -3931,6 +4174,7 @@ ${preview}
|
|
|
3931
4174
|
|
|
3932
4175
|
// src/index.ts
|
|
3933
4176
|
registerTerminalCleanup();
|
|
4177
|
+
initErrorLog();
|
|
3934
4178
|
function parseArgs(argv) {
|
|
3935
4179
|
const args = {
|
|
3936
4180
|
model: null,
|
|
@@ -4049,11 +4293,11 @@ async function main() {
|
|
|
4049
4293
|
agentOpts.initialPrompt = args.prompt;
|
|
4050
4294
|
await runAgent(agentOpts);
|
|
4051
4295
|
} catch (err) {
|
|
4052
|
-
renderError(err);
|
|
4296
|
+
renderError(err, "agent");
|
|
4053
4297
|
process.exit(1);
|
|
4054
4298
|
}
|
|
4055
4299
|
}
|
|
4056
4300
|
main().catch((err) => {
|
|
4057
|
-
|
|
4301
|
+
renderError(err, "main");
|
|
4058
4302
|
process.exit(1);
|
|
4059
4303
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mini-coder",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "A small, fast CLI coding agent",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"@ai-sdk/openai-compatible": "^2.0.30",
|
|
23
23
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
24
24
|
"ai": "^6.0.94",
|
|
25
|
+
"ignore": "^7.0.5",
|
|
25
26
|
"ollama-ai-provider": "^1.2.0",
|
|
26
27
|
"yoctocolors": "^2.1.1",
|
|
27
28
|
"zod": "^4.3.6"
|
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).
|