mini-coder 0.0.7 → 0.0.9

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 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 join15 } from "path";
9
+ import { join as join16 } from "path";
10
10
  import * as c7 from "yoctocolors";
11
11
 
12
12
  // src/cli/agents.ts
@@ -94,7 +94,7 @@ function zenEndpointFor(modelId) {
94
94
  if (modelId.startsWith("claude-"))
95
95
  return zenAnthropic()(modelId);
96
96
  if (modelId.startsWith("gpt-"))
97
- return zenOpenAI()(modelId);
97
+ return zenOpenAI().responses(modelId);
98
98
  if (modelId.startsWith("gemini-"))
99
99
  return zenGoogle()(modelId);
100
100
  return zenCompat()(modelId);
@@ -176,6 +176,15 @@ function directGoogle() {
176
176
  }
177
177
  return _directGoogle;
178
178
  }
179
+ function parseModelString(modelString) {
180
+ const slashIdx = modelString.indexOf("/");
181
+ if (slashIdx === -1)
182
+ return { provider: modelString, modelId: "" };
183
+ return {
184
+ provider: modelString.slice(0, slashIdx),
185
+ modelId: modelString.slice(slashIdx + 1)
186
+ };
187
+ }
179
188
  var CONTEXT_WINDOW_TABLE = [
180
189
  [/^claude-/, 200000],
181
190
  [/^gemini-/, 1e6],
@@ -187,7 +196,7 @@ var CONTEXT_WINDOW_TABLE = [
187
196
  [/^qwen3-/, 131000]
188
197
  ];
189
198
  function getContextWindow(modelString) {
190
- const modelId = modelString.includes("/") ? modelString.slice(modelString.indexOf("/") + 1) : modelString;
199
+ const { modelId } = parseModelString(modelString);
191
200
  for (const [pattern, tokens] of CONTEXT_WINDOW_TABLE) {
192
201
  if (pattern.test(modelId))
193
202
  return tokens;
@@ -208,7 +217,7 @@ function resolveModel(modelString) {
208
217
  case "anthropic":
209
218
  return directAnthropic()(modelId);
210
219
  case "openai":
211
- return directOpenAI()(modelId);
220
+ return modelId.startsWith("gpt-") ? directOpenAI().responses(modelId) : directOpenAI()(modelId);
212
221
  case "google":
213
222
  return directGoogle()(modelId);
214
223
  case "ollama": {
@@ -521,16 +530,146 @@ function generateSessionId() {
521
530
 
522
531
  // src/cli/custom-commands.ts
523
532
  import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
524
- import { homedir as homedir4 } from "os";
525
- import { basename as basename2, join as join3 } from "path";
533
+ import { homedir as homedir5 } from "os";
534
+ import { basename as basename2, join as join4 } from "path";
526
535
 
527
536
  // src/cli/config-conflicts.ts
528
537
  import * as c3 from "yoctocolors";
529
538
 
530
539
  // src/cli/output.ts
531
- import { homedir as homedir3 } from "os";
540
+ import { homedir as homedir4 } from "os";
532
541
  import * as c2 from "yoctocolors";
533
542
 
543
+ // src/cli/error-log.ts
544
+ import { mkdirSync as mkdirSync2 } from "fs";
545
+ import { homedir as homedir3 } from "os";
546
+ import { join as join3 } from "path";
547
+ var writer = null;
548
+ function initErrorLog() {
549
+ if (writer)
550
+ return;
551
+ const dirPath = join3(homedir3(), ".config", "mini-coder");
552
+ const logPath = join3(dirPath, "errors.log");
553
+ mkdirSync2(dirPath, { recursive: true });
554
+ writer = Bun.file(logPath).writer();
555
+ process.on("uncaughtException", (err) => {
556
+ logError(err, "uncaught");
557
+ process.exit(1);
558
+ });
559
+ }
560
+ function isObject(v) {
561
+ return typeof v === "object" && v !== null;
562
+ }
563
+ function logError(err, context) {
564
+ if (!writer)
565
+ return;
566
+ let entry = `[${new Date().toISOString()}]`;
567
+ if (context)
568
+ entry += ` context=${context}`;
569
+ entry += `
570
+ `;
571
+ if (isObject(err)) {
572
+ if (typeof err.name === "string")
573
+ entry += ` name: ${err.name}
574
+ `;
575
+ if (typeof err.message === "string")
576
+ entry += ` message: ${err.message}
577
+ `;
578
+ if ("statusCode" in err)
579
+ entry += ` statusCode: ${err.statusCode}
580
+ `;
581
+ if ("url" in err)
582
+ entry += ` url: ${err.url}
583
+ `;
584
+ if ("isRetryable" in err)
585
+ entry += ` isRetryable: ${err.isRetryable}
586
+ `;
587
+ if (typeof err.stack === "string") {
588
+ const indentedStack = err.stack.split(`
589
+ `).map((line, i) => i === 0 ? line : ` ${line}`).join(`
590
+ `);
591
+ entry += ` stack: ${indentedStack}
592
+ `;
593
+ }
594
+ } else {
595
+ entry += ` value: ${String(err)}
596
+ `;
597
+ }
598
+ entry += `---
599
+ `;
600
+ writer.write(entry);
601
+ writer.flush();
602
+ }
603
+
604
+ // src/cli/error-parse.ts
605
+ import {
606
+ APICallError,
607
+ LoadAPIKeyError,
608
+ NoContentGeneratedError,
609
+ NoSuchModelError,
610
+ RetryError
611
+ } from "ai";
612
+ function parseAppError(err) {
613
+ if (typeof err === "string") {
614
+ return { headline: err };
615
+ }
616
+ if (err instanceof RetryError) {
617
+ const inner = parseAppError(err.lastError);
618
+ return {
619
+ headline: `Retries exhausted: ${inner.headline}`,
620
+ ...inner.hint ? { hint: inner.hint } : {}
621
+ };
622
+ }
623
+ if (err instanceof APICallError) {
624
+ if (err.statusCode === 429) {
625
+ return {
626
+ headline: "Rate limit hit",
627
+ hint: "Wait a moment and retry, or switch model with /model"
628
+ };
629
+ }
630
+ if (err.statusCode === 401 || err.statusCode === 403) {
631
+ return {
632
+ headline: "Auth failed",
633
+ hint: "Check the relevant provider API key env var"
634
+ };
635
+ }
636
+ return {
637
+ headline: `API error ${err.statusCode ?? "unknown"}`,
638
+ ...err.url ? { hint: err.url } : {}
639
+ };
640
+ }
641
+ if (err instanceof NoContentGeneratedError) {
642
+ return {
643
+ headline: "Model returned empty response",
644
+ hint: "Try rephrasing or switching model with /model"
645
+ };
646
+ }
647
+ if (err instanceof LoadAPIKeyError) {
648
+ return {
649
+ headline: "API key not found",
650
+ hint: "Set the relevant provider env var"
651
+ };
652
+ }
653
+ if (err instanceof NoSuchModelError) {
654
+ return {
655
+ headline: "Model not found",
656
+ hint: "Use /model to pick a valid model"
657
+ };
658
+ }
659
+ const isObj = typeof err === "object" && err !== null;
660
+ const code = isObj && "code" in err ? String(err.code) : undefined;
661
+ const message = isObj && "message" in err ? String(err.message) : String(err);
662
+ if (code === "ECONNREFUSED" || message.includes("ECONNREFUSED")) {
663
+ return {
664
+ headline: "Connection failed",
665
+ hint: "Check network or local server"
666
+ };
667
+ }
668
+ const firstLine = message.split(`
669
+ `)[0]?.trim() || "Unknown error";
670
+ return { headline: firstLine };
671
+ }
672
+
534
673
  // src/cli/markdown.ts
535
674
  import * as c from "yoctocolors";
536
675
  function renderInline(text) {
@@ -644,8 +783,8 @@ function renderChunk(text, inFence) {
644
783
  }
645
784
 
646
785
  // src/cli/output.ts
647
- var HOME = homedir3();
648
- var PACKAGE_VERSION = "0.0.6";
786
+ var HOME = homedir4();
787
+ var PACKAGE_VERSION = "0.0.8";
649
788
  function tildePath(p) {
650
789
  return p.startsWith(HOME) ? `~${p.slice(HOME.length)}` : p;
651
790
  }
@@ -668,8 +807,6 @@ function registerTerminalCleanup() {
668
807
  process.exit(143);
669
808
  });
670
809
  process.on("SIGINT", () => {
671
- if (process.listenerCount("SIGINT") > 1)
672
- return;
673
810
  cleanup();
674
811
  process.exit(130);
675
812
  });
@@ -856,10 +993,6 @@ function renderToolResultInline(toolName, result, isError, indent) {
856
993
  return;
857
994
  }
858
995
  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
996
  return;
864
997
  }
865
998
  if (toolName.startsWith("mcp_")) {
@@ -876,16 +1009,48 @@ function renderToolResultInline(toolName, result, isError, indent) {
876
1009
  const text = JSON.stringify(result);
877
1010
  writeln(`${indent}${G.info} ${c2.dim(text.length > 80 ? `${text.slice(0, 77)}\u2026` : text)}`);
878
1011
  }
879
- function renderSubagentActivity(activity, indent, maxDepth) {
880
- for (const entry of activity) {
881
- writeln(`${indent}${toolCallLine(entry.toolName, entry.args)}`);
882
- if (entry.toolName === "subagent" && maxDepth > 0) {
883
- const nested = entry.result;
884
- if (nested?.activity?.length) {
885
- renderSubagentActivity(nested.activity, `${indent} `, maxDepth - 1);
1012
+ function formatSubagentLabel(laneId, parentLabel) {
1013
+ const numStr = parentLabel ? `${parentLabel.replace(/[\[\]]/g, "")}.${laneId}` : `${laneId}`;
1014
+ return c2.dim(c2.cyan(`[${numStr}]`));
1015
+ }
1016
+ var laneBuffers = new Map;
1017
+ function renderSubagentEvent(event, opts) {
1018
+ const { laneId, parentLabel, activeLanes } = opts;
1019
+ const labelStr = formatSubagentLabel(laneId, parentLabel);
1020
+ const prefix = activeLanes.size > 1 ? `${labelStr} ` : "";
1021
+ if (event.type === "text-delta") {
1022
+ const buf = (laneBuffers.get(laneId) ?? "") + event.delta;
1023
+ const lines = buf.split(`
1024
+ `);
1025
+ if (lines.length > 1) {
1026
+ for (let i = 0;i < lines.length - 1; i++) {
1027
+ writeln(`${prefix}${lines[i]}`);
886
1028
  }
1029
+ laneBuffers.set(laneId, lines[lines.length - 1] ?? "");
1030
+ } else {
1031
+ laneBuffers.set(laneId, buf);
1032
+ }
1033
+ } else if (event.type === "tool-call-start") {
1034
+ writeln(`${prefix}${toolCallLine(event.toolName, event.args)}`);
1035
+ } else if (event.type === "tool-result") {
1036
+ renderToolResultInline(event.toolName, event.result, event.isError, `${prefix} `);
1037
+ } else if (event.type === "turn-complete") {
1038
+ const buf = laneBuffers.get(laneId);
1039
+ if (buf) {
1040
+ writeln(`${prefix}${buf}`);
1041
+ laneBuffers.delete(laneId);
1042
+ }
1043
+ if (event.inputTokens > 0 || event.outputTokens > 0) {
1044
+ writeln(`${prefix}${c2.dim(`\u2191${event.inputTokens} \u2193${event.outputTokens}`)}`);
1045
+ }
1046
+ } else if (event.type === "turn-error") {
1047
+ laneBuffers.delete(laneId);
1048
+ logError(event.error, "turn");
1049
+ const parsed = parseAppError(event.error);
1050
+ writeln(`${prefix}${G.err} ${c2.red(parsed.headline)}`);
1051
+ if (parsed.hint) {
1052
+ writeln(`${prefix} ${c2.dim(parsed.hint)}`);
887
1053
  }
888
- renderToolResultInline(entry.toolName, entry.result, entry.isError, `${indent} `);
889
1054
  }
890
1055
  }
891
1056
  function renderToolResult(toolName, result, isError) {
@@ -985,22 +1150,6 @@ function renderToolResult(toolName, result, isError) {
985
1150
  return;
986
1151
  }
987
1152
  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
1153
  return;
1005
1154
  }
1006
1155
  if (toolName.startsWith("mcp_")) {
@@ -1159,9 +1308,12 @@ async function renderTurn(events, spinner) {
1159
1308
  if (isAbort) {
1160
1309
  writeln(`${G.warn} ${c2.dim("interrupted")}`);
1161
1310
  } else {
1162
- const msg = event.error.message.split(`
1163
- `)[0] ?? event.error.message;
1164
- writeln(`${G.err} ${c2.red(msg)}`);
1311
+ logError(event.error, "turn");
1312
+ const parsed = parseAppError(event.error);
1313
+ writeln(`${G.err} ${c2.red(parsed.headline)}`);
1314
+ if (parsed.hint) {
1315
+ writeln(` ${c2.dim(parsed.hint)}`);
1316
+ }
1165
1317
  }
1166
1318
  break;
1167
1319
  }
@@ -1211,9 +1363,13 @@ function renderBanner(model, cwd) {
1211
1363
  writeln(` ${c2.dim("/help for commands \xB7 ctrl+d to exit")}`);
1212
1364
  writeln();
1213
1365
  }
1214
- function renderError(err) {
1215
- const msg = err instanceof Error ? err.message : String(err);
1216
- writeln(`${G.err} ${c2.red(msg)}`);
1366
+ function renderError(err, context = "render") {
1367
+ logError(err, context);
1368
+ const parsed = parseAppError(err);
1369
+ writeln(`${G.err} ${c2.red(parsed.headline)}`);
1370
+ if (parsed.hint) {
1371
+ writeln(` ${c2.dim(parsed.hint)}`);
1372
+ }
1217
1373
  }
1218
1374
  function renderInfo(msg) {
1219
1375
  writeln(`${G.info} ${c2.dim(msg)}`);
@@ -1301,7 +1457,7 @@ function loadFromDir2(dir, source) {
1301
1457
  if (!entry.endsWith(".md"))
1302
1458
  continue;
1303
1459
  const name = basename2(entry, ".md");
1304
- const filePath = join3(dir, entry);
1460
+ const filePath = join4(dir, entry);
1305
1461
  let raw;
1306
1462
  try {
1307
1463
  raw = readFileSync2(filePath, "utf-8");
@@ -1320,10 +1476,10 @@ function loadFromDir2(dir, source) {
1320
1476
  return commands;
1321
1477
  }
1322
1478
  function loadCustomCommands(cwd) {
1323
- const globalAgentsDir = join3(homedir4(), ".agents", "commands");
1324
- const globalClaudeDir = join3(homedir4(), ".claude", "commands");
1325
- const localAgentsDir = join3(cwd, ".agents", "commands");
1326
- const localClaudeDir = join3(cwd, ".claude", "commands");
1479
+ const globalAgentsDir = join4(homedir5(), ".agents", "commands");
1480
+ const globalClaudeDir = join4(homedir5(), ".claude", "commands");
1481
+ const localAgentsDir = join4(cwd, ".agents", "commands");
1482
+ const localClaudeDir = join4(cwd, ".claude", "commands");
1327
1483
  const globalAgents = loadFromDir2(globalAgentsDir, "global");
1328
1484
  const globalClaude = loadFromDir2(globalClaudeDir, "global");
1329
1485
  const localAgents = loadFromDir2(localAgentsDir, "local");
@@ -1378,8 +1534,8 @@ async function expandTemplate(template, args, cwd) {
1378
1534
 
1379
1535
  // src/cli/skills.ts
1380
1536
  import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync } from "fs";
1381
- import { homedir as homedir5 } from "os";
1382
- import { join as join4 } from "path";
1537
+ import { homedir as homedir6 } from "os";
1538
+ import { join as join5 } from "path";
1383
1539
  function loadFromDir3(dir, source) {
1384
1540
  const skills = new Map;
1385
1541
  if (!existsSync4(dir))
@@ -1391,9 +1547,9 @@ function loadFromDir3(dir, source) {
1391
1547
  return skills;
1392
1548
  }
1393
1549
  for (const entry of entries) {
1394
- const skillFile = join4(dir, entry, "SKILL.md");
1550
+ const skillFile = join5(dir, entry, "SKILL.md");
1395
1551
  try {
1396
- if (!statSync(join4(dir, entry)).isDirectory())
1552
+ if (!statSync(join5(dir, entry)).isDirectory())
1397
1553
  continue;
1398
1554
  if (!existsSync4(skillFile))
1399
1555
  continue;
@@ -1411,10 +1567,10 @@ function loadFromDir3(dir, source) {
1411
1567
  return skills;
1412
1568
  }
1413
1569
  function loadSkills(cwd) {
1414
- const globalAgentsDir = join4(homedir5(), ".agents", "skills");
1415
- const globalClaudeDir = join4(homedir5(), ".claude", "skills");
1416
- const localAgentsDir = join4(cwd, ".agents", "skills");
1417
- const localClaudeDir = join4(cwd, ".claude", "skills");
1570
+ const globalAgentsDir = join5(homedir6(), ".agents", "skills");
1571
+ const globalClaudeDir = join5(homedir6(), ".claude", "skills");
1572
+ const localAgentsDir = join5(cwd, ".agents", "skills");
1573
+ const localClaudeDir = join5(cwd, ".claude", "skills");
1418
1574
  const globalAgents = loadFromDir3(globalAgentsDir, "global");
1419
1575
  const globalClaude = loadFromDir3(globalClaudeDir, "global");
1420
1576
  const localAgents = loadFromDir3(localAgentsDir, "local");
@@ -1609,10 +1765,6 @@ async function handleReview(ctx, args) {
1609
1765
  writeln();
1610
1766
  try {
1611
1767
  const output = await ctx.runSubagent(REVIEW_PROMPT(ctx.cwd, focus));
1612
- if (output.activity.length) {
1613
- renderSubagentActivity(output.activity, " ", 1);
1614
- writeln();
1615
- }
1616
1768
  write(renderMarkdown(output.result));
1617
1769
  writeln();
1618
1770
  return {
@@ -1641,10 +1793,6 @@ async function handleCustomCommand(cmd, args, ctx) {
1641
1793
  writeln();
1642
1794
  try {
1643
1795
  const output = await ctx.runSubagent(prompt, cmd.model);
1644
- if (output.activity.length) {
1645
- renderSubagentActivity(output.activity, " ", 1);
1646
- writeln();
1647
- }
1648
1796
  write(renderMarkdown(output.result));
1649
1797
  writeln();
1650
1798
  return {
@@ -1795,7 +1943,7 @@ async function loadImageFile(filePath) {
1795
1943
  }
1796
1944
 
1797
1945
  // src/cli/input.ts
1798
- import { join as join5, relative } from "path";
1946
+ import { join as join6, relative } from "path";
1799
1947
  import * as c5 from "yoctocolors";
1800
1948
  var ESC = "\x1B";
1801
1949
  var CSI = `${ESC}[`;
@@ -1847,7 +1995,7 @@ async function getAtCompletions(prefix, cwd) {
1847
1995
  for await (const file of glob.scan({ cwd, onlyFiles: true })) {
1848
1996
  if (file.includes("node_modules") || file.includes(".git"))
1849
1997
  continue;
1850
- results.push(`@${relative(cwd, join5(cwd, file))}`);
1998
+ results.push(`@${relative(cwd, join6(cwd, file))}`);
1851
1999
  if (results.length >= MAX)
1852
2000
  break;
1853
2001
  }
@@ -1869,7 +2017,7 @@ async function tryExtractImageFromPaste(pasted, cwd) {
1869
2017
  }
1870
2018
  }
1871
2019
  if (!trimmed.includes(" ") && isImageFilename(trimmed)) {
1872
- const filePath = trimmed.startsWith("/") ? trimmed : join5(cwd, trimmed);
2020
+ const filePath = trimmed.startsWith("/") ? trimmed : join6(cwd, trimmed);
1873
2021
  const attachment = await loadImageFile(filePath);
1874
2022
  if (attachment) {
1875
2023
  const name = filePath.split("/").pop() ?? trimmed;
@@ -1906,6 +2054,28 @@ async function readKey(reader) {
1906
2054
  return "";
1907
2055
  return new TextDecoder().decode(value);
1908
2056
  }
2057
+ function watchForInterrupt(abortController) {
2058
+ if (!process.stdin.isTTY)
2059
+ return () => {};
2060
+ const onData = (chunk) => {
2061
+ for (const byte of chunk) {
2062
+ if (byte === 3) {
2063
+ cleanup();
2064
+ abortController.abort();
2065
+ return;
2066
+ }
2067
+ }
2068
+ };
2069
+ const cleanup = () => {
2070
+ process.stdin.removeListener("data", onData);
2071
+ process.stdin.setRawMode(false);
2072
+ process.stdin.pause();
2073
+ };
2074
+ process.stdin.setRawMode(true);
2075
+ process.stdin.resume();
2076
+ process.stdin.on("data", onData);
2077
+ return cleanup;
2078
+ }
1909
2079
  var PASTE_SENTINEL = "\x00PASTE\x00";
1910
2080
  var PASTE_SENTINEL_LEN = PASTE_SENTINEL.length;
1911
2081
  function pasteLabel(text) {
@@ -2240,30 +2410,51 @@ var MAX_STEPS = 50;
2240
2410
  function isZodSchema(s) {
2241
2411
  return s !== null && typeof s === "object" && "_def" in s;
2242
2412
  }
2243
- function toCoreTool(def) {
2413
+ function toCoreTool(def, claimWarning) {
2244
2414
  const schema = isZodSchema(def.schema) ? def.schema : jsonSchema(def.schema);
2245
2415
  return dynamicTool({
2246
2416
  description: def.description,
2247
2417
  inputSchema: schema,
2248
2418
  execute: async (input) => {
2249
2419
  try {
2250
- return await def.execute(input);
2420
+ const result = await def.execute(input);
2421
+ if (claimWarning()) {
2422
+ const warning = `
2423
+
2424
+ <system-message>You have reached the maximum number of tool calls. ` + "No more tools will be available after this result. " + "Respond with a status update and list what still needs to be done.</system-message>";
2425
+ const str = typeof result === "string" ? result : JSON.stringify(result);
2426
+ return str + warning;
2427
+ }
2428
+ return result;
2251
2429
  } catch (err) {
2252
2430
  throw err instanceof Error ? err : new Error(String(err));
2253
2431
  }
2254
2432
  }
2255
2433
  });
2256
2434
  }
2435
+ function isOpenAIGPT(modelString) {
2436
+ const { provider, modelId } = parseModelString(modelString);
2437
+ return (provider === "openai" || provider === "zen") && modelId.startsWith("gpt-");
2438
+ }
2257
2439
  async function* runTurn(options) {
2258
- const { model, messages, tools, systemPrompt, signal } = options;
2440
+ const { model, modelString, messages, tools, systemPrompt, signal } = options;
2441
+ let stepCount = 0;
2442
+ let warningClaimed = false;
2443
+ function claimWarning() {
2444
+ if (stepCount !== MAX_STEPS - 2 || warningClaimed)
2445
+ return false;
2446
+ warningClaimed = true;
2447
+ return true;
2448
+ }
2259
2449
  const toolSet = {};
2260
2450
  for (const def of tools) {
2261
- toolSet[def.name] = toCoreTool(def);
2451
+ toolSet[def.name] = toCoreTool(def, claimWarning);
2262
2452
  }
2263
2453
  let inputTokens = 0;
2264
2454
  let outputTokens = 0;
2265
2455
  let contextTokens = 0;
2266
2456
  try {
2457
+ const useInstructions = systemPrompt !== undefined && isOpenAIGPT(modelString);
2267
2458
  const streamOpts = {
2268
2459
  model,
2269
2460
  messages,
@@ -2273,11 +2464,28 @@ async function* runTurn(options) {
2273
2464
  inputTokens += step.usage?.inputTokens ?? 0;
2274
2465
  outputTokens += step.usage?.outputTokens ?? 0;
2275
2466
  contextTokens = step.usage?.inputTokens ?? contextTokens;
2467
+ stepCount++;
2468
+ warningClaimed = false;
2276
2469
  },
2277
- ...systemPrompt ? { system: systemPrompt } : {},
2470
+ prepareStep: ({ stepNumber }) => {
2471
+ if (stepNumber >= MAX_STEPS - 1) {
2472
+ return { activeTools: [] };
2473
+ }
2474
+ return;
2475
+ },
2476
+ ...systemPrompt && !useInstructions ? { system: systemPrompt } : {},
2477
+ ...useInstructions ? {
2478
+ providerOptions: {
2479
+ openai: {
2480
+ instructions: systemPrompt,
2481
+ store: false
2482
+ }
2483
+ }
2484
+ } : {},
2278
2485
  ...signal ? { abortSignal: signal } : {}
2279
2486
  };
2280
2487
  const result = streamText(streamOpts);
2488
+ result.response.catch(() => {});
2281
2489
  for await (const chunk of result.fullStream) {
2282
2490
  if (signal?.aborted)
2283
2491
  break;
@@ -2402,7 +2610,7 @@ async function connectMcpServer(config) {
2402
2610
 
2403
2611
  // src/tools/snapshot.ts
2404
2612
  import { readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
2405
- import { join as join6 } from "path";
2613
+ import { join as join7 } from "path";
2406
2614
  async function gitBytes(args, cwd) {
2407
2615
  try {
2408
2616
  const proc = Bun.spawn(["git", ...args], {
@@ -2493,7 +2701,7 @@ async function takeSnapshot(cwd, sessionId, turnIndex) {
2493
2701
  return false;
2494
2702
  const files = [];
2495
2703
  for (const entry of entries) {
2496
- const absPath = join6(repoRoot, entry.path);
2704
+ const absPath = join7(repoRoot, entry.path);
2497
2705
  if (!entry.existsOnDisk) {
2498
2706
  const { bytes, code } = await gitBytes(["show", `HEAD:${entry.path}`], repoRoot);
2499
2707
  if (code === 0) {
@@ -2542,7 +2750,7 @@ async function restoreSnapshot(cwd, sessionId, turnIndex) {
2542
2750
  const root = repoRoot ?? cwd;
2543
2751
  let anyFailed = false;
2544
2752
  for (const file of files) {
2545
- const absPath = join6(root, file.path);
2753
+ const absPath = join7(root, file.path);
2546
2754
  if (!file.existed) {
2547
2755
  try {
2548
2756
  if (await Bun.file(absPath).exists()) {
@@ -2609,10 +2817,75 @@ function getMostRecentSession() {
2609
2817
  return sessions[0] ?? null;
2610
2818
  }
2611
2819
 
2612
- // src/tools/create.ts
2613
- import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
2614
- import { dirname, join as join7, relative as relative2 } from "path";
2820
+ // src/tools/exa.ts
2615
2821
  import { z as z2 } from "zod";
2822
+ var ExaSearchSchema = z2.object({
2823
+ query: z2.string().describe("The search query")
2824
+ });
2825
+ var webSearchTool = {
2826
+ name: "webSearch",
2827
+ description: "Search the web for a query using Exa.",
2828
+ schema: ExaSearchSchema,
2829
+ execute: async (input) => {
2830
+ const apiKey = process.env.EXA_API_KEY;
2831
+ if (!apiKey) {
2832
+ throw new Error("EXA_API_KEY is not set.");
2833
+ }
2834
+ const response = await fetch("https://api.exa.ai/search", {
2835
+ method: "POST",
2836
+ headers: {
2837
+ "Content-Type": "application/json",
2838
+ "x-api-key": apiKey
2839
+ },
2840
+ body: JSON.stringify({
2841
+ query: input.query,
2842
+ type: "auto",
2843
+ numResults: 10,
2844
+ contents: { text: { maxCharacters: 4000 } }
2845
+ })
2846
+ });
2847
+ if (!response.ok) {
2848
+ const errorBody = await response.text();
2849
+ throw new Error(`Exa API error: ${response.status} ${response.statusText} - ${errorBody}`);
2850
+ }
2851
+ return await response.json();
2852
+ }
2853
+ };
2854
+ var ExaContentSchema = z2.object({
2855
+ urls: z2.array(z2.string()).max(3).describe("Array of URLs to retrieve content for (max 3)")
2856
+ });
2857
+ var webContentTool = {
2858
+ name: "webContent",
2859
+ description: "Get the full content of specific URLs using Exa.",
2860
+ schema: ExaContentSchema,
2861
+ execute: async (input) => {
2862
+ const apiKey = process.env.EXA_API_KEY;
2863
+ if (!apiKey) {
2864
+ throw new Error("EXA_API_KEY is not set.");
2865
+ }
2866
+ const response = await fetch("https://api.exa.ai/contents", {
2867
+ method: "POST",
2868
+ headers: {
2869
+ "Content-Type": "application/json",
2870
+ "x-api-key": apiKey
2871
+ },
2872
+ body: JSON.stringify({
2873
+ urls: input.urls,
2874
+ text: { maxCharacters: 1e4 }
2875
+ })
2876
+ });
2877
+ if (!response.ok) {
2878
+ const errorBody = await response.text();
2879
+ throw new Error(`Exa API error: ${response.status} ${response.statusText} - ${errorBody}`);
2880
+ }
2881
+ return await response.json();
2882
+ }
2883
+ };
2884
+
2885
+ // src/tools/create.ts
2886
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
2887
+ import { dirname, join as join8, relative as relative2 } from "path";
2888
+ import { z as z3 } from "zod";
2616
2889
 
2617
2890
  // src/tools/diff.ts
2618
2891
  function generateDiff(filePath, before, after) {
@@ -2741,9 +3014,9 @@ ${lines.join(`
2741
3014
  }
2742
3015
 
2743
3016
  // src/tools/create.ts
2744
- var CreateSchema = z2.object({
2745
- path: z2.string().describe("File path to write (absolute or relative to cwd)"),
2746
- content: z2.string().describe("Full content to write to the file")
3017
+ var CreateSchema = z3.object({
3018
+ path: z3.string().describe("File path to write (absolute or relative to cwd)"),
3019
+ content: z3.string().describe("Full content to write to the file")
2747
3020
  });
2748
3021
  var createTool = {
2749
3022
  name: "create",
@@ -2751,11 +3024,11 @@ var createTool = {
2751
3024
  schema: CreateSchema,
2752
3025
  execute: async (input) => {
2753
3026
  const cwd = input.cwd ?? process.cwd();
2754
- const filePath = input.path.startsWith("/") ? input.path : join7(cwd, input.path);
3027
+ const filePath = input.path.startsWith("/") ? input.path : join8(cwd, input.path);
2755
3028
  const relPath = relative2(cwd, filePath);
2756
3029
  const dir = dirname(filePath);
2757
3030
  if (!existsSync5(dir))
2758
- mkdirSync2(dir, { recursive: true });
3031
+ mkdirSync3(dir, { recursive: true });
2759
3032
  const file = Bun.file(filePath);
2760
3033
  const created = !await file.exists();
2761
3034
  const before = created ? "" : await file.text();
@@ -2766,15 +3039,15 @@ var createTool = {
2766
3039
  };
2767
3040
 
2768
3041
  // src/tools/glob.ts
2769
- import { join as join9, relative as relative3 } from "path";
2770
- import { z as z3 } from "zod";
3042
+ import { join as join10, relative as relative3 } from "path";
3043
+ import { z as z4 } from "zod";
2771
3044
 
2772
3045
  // src/tools/ignore.ts
2773
- import { join as join8 } from "path";
3046
+ import { join as join9 } from "path";
2774
3047
  import ignore from "ignore";
2775
3048
  async function loadGitignore(cwd) {
2776
3049
  try {
2777
- const gitignore = await Bun.file(join8(cwd, ".gitignore")).text();
3050
+ const gitignore = await Bun.file(join9(cwd, ".gitignore")).text();
2778
3051
  return ignore().add(gitignore);
2779
3052
  } catch {
2780
3053
  return null;
@@ -2782,9 +3055,9 @@ async function loadGitignore(cwd) {
2782
3055
  }
2783
3056
 
2784
3057
  // src/tools/glob.ts
2785
- var GlobSchema = z3.object({
2786
- pattern: z3.string().describe("Glob pattern to match files against, e.g. '**/*.ts'"),
2787
- ignore: z3.array(z3.string()).optional().describe("Glob patterns to exclude")
3058
+ var GlobSchema = z4.object({
3059
+ pattern: z4.string().describe("Glob pattern to match files against, e.g. '**/*.ts'"),
3060
+ ignore: z4.array(z4.string()).optional().describe("Glob patterns to exclude")
2788
3061
  });
2789
3062
  var MAX_RESULTS = 500;
2790
3063
  var globTool = {
@@ -2807,7 +3080,7 @@ var globTool = {
2807
3080
  if (ignored)
2808
3081
  continue;
2809
3082
  try {
2810
- const fullPath = join9(cwd, file);
3083
+ const fullPath = join10(cwd, file);
2811
3084
  const stat = await Bun.file(fullPath).stat?.() ?? null;
2812
3085
  matches.push({ path: file, mtime: stat?.mtime?.getTime() ?? 0 });
2813
3086
  } catch {
@@ -2820,14 +3093,14 @@ var globTool = {
2820
3093
  if (truncated)
2821
3094
  matches.pop();
2822
3095
  matches.sort((a, b) => b.mtime - a.mtime);
2823
- const files = matches.map((m) => relative3(cwd, join9(cwd, m.path)));
3096
+ const files = matches.map((m) => relative3(cwd, join10(cwd, m.path)));
2824
3097
  return { files, count: files.length, truncated };
2825
3098
  }
2826
3099
  };
2827
3100
 
2828
3101
  // src/tools/grep.ts
2829
- import { join as join10 } from "path";
2830
- import { z as z4 } from "zod";
3102
+ import { join as join11 } from "path";
3103
+ import { z as z5 } from "zod";
2831
3104
 
2832
3105
  // src/tools/hashline.ts
2833
3106
  var FNV_OFFSET_BASIS = 2166136261;
@@ -2873,12 +3146,12 @@ function findLineByHash(lines, hash, hintLine) {
2873
3146
  }
2874
3147
 
2875
3148
  // src/tools/grep.ts
2876
- var GrepSchema = z4.object({
2877
- pattern: z4.string().describe("Regular expression to search for"),
2878
- include: z4.string().optional().describe("Glob pattern to filter files, e.g. '*.ts' or '*.{ts,tsx}'"),
2879
- contextLines: z4.number().int().min(0).max(10).optional().default(2).describe("Lines of context to include around each match"),
2880
- caseSensitive: z4.boolean().optional().default(true),
2881
- maxResults: z4.number().int().min(1).max(200).optional().default(50)
3149
+ var GrepSchema = z5.object({
3150
+ pattern: z5.string().describe("Regular expression to search for"),
3151
+ include: z5.string().optional().describe("Glob pattern to filter files, e.g. '*.ts' or '*.{ts,tsx}'"),
3152
+ contextLines: z5.number().int().min(0).max(10).optional().default(2).describe("Lines of context to include around each match"),
3153
+ caseSensitive: z5.boolean().optional().default(true),
3154
+ maxResults: z5.number().int().min(1).max(200).optional().default(50)
2882
3155
  });
2883
3156
  var DEFAULT_IGNORE = [
2884
3157
  "node_modules",
@@ -2917,7 +3190,7 @@ var grepTool = {
2917
3190
  if (ignoreGlob.some((g) => g.match(relPath) || g.match(firstSegment))) {
2918
3191
  continue;
2919
3192
  }
2920
- const fullPath = join10(cwd, relPath);
3193
+ const fullPath = join11(cwd, relPath);
2921
3194
  let text;
2922
3195
  try {
2923
3196
  text = await Bun.file(fullPath).text();
@@ -2968,8 +3241,8 @@ var grepTool = {
2968
3241
 
2969
3242
  // src/tools/hooks.ts
2970
3243
  import { constants, accessSync } from "fs";
2971
- import { homedir as homedir6 } from "os";
2972
- import { join as join11 } from "path";
3244
+ import { homedir as homedir7 } from "os";
3245
+ import { join as join12 } from "path";
2973
3246
  function isExecutable(filePath) {
2974
3247
  try {
2975
3248
  accessSync(filePath, constants.X_OK);
@@ -2981,8 +3254,8 @@ function isExecutable(filePath) {
2981
3254
  function findHook(toolName, cwd) {
2982
3255
  const scriptName = `post-${toolName}`;
2983
3256
  const candidates = [
2984
- join11(cwd, ".agents", "hooks", scriptName),
2985
- join11(homedir6(), ".agents", "hooks", scriptName)
3257
+ join12(cwd, ".agents", "hooks", scriptName),
3258
+ join12(homedir7(), ".agents", "hooks", scriptName)
2986
3259
  ];
2987
3260
  for (const p of candidates) {
2988
3261
  if (isExecutable(p))
@@ -3071,13 +3344,13 @@ function hookEnvForRead(input, cwd) {
3071
3344
  }
3072
3345
 
3073
3346
  // src/tools/insert.ts
3074
- import { join as join12, relative as relative4 } from "path";
3075
- import { z as z5 } from "zod";
3076
- var InsertSchema = z5.object({
3077
- path: z5.string().describe("File path to edit (absolute or relative to cwd)"),
3078
- anchor: z5.string().describe('Anchor line from a prior read/grep, e.g. "11:a3"'),
3079
- position: z5.enum(["before", "after"]).describe('Insert the content "before" or "after" the anchor line'),
3080
- content: z5.string().describe("Text to insert")
3347
+ import { join as join13, relative as relative4 } from "path";
3348
+ import { z as z6 } from "zod";
3349
+ var InsertSchema = z6.object({
3350
+ path: z6.string().describe("File path to edit (absolute or relative to cwd)"),
3351
+ anchor: z6.string().describe('Anchor line from a prior read/grep, e.g. "11:a3"'),
3352
+ position: z6.enum(["before", "after"]).describe('Insert the content "before" or "after" the anchor line'),
3353
+ content: z6.string().describe("Text to insert")
3081
3354
  });
3082
3355
  var HASH_NOT_FOUND_ERROR = "Hash not found. Re-read the file to get current anchors.";
3083
3356
  var insertTool = {
@@ -3086,7 +3359,7 @@ var insertTool = {
3086
3359
  schema: InsertSchema,
3087
3360
  execute: async (input) => {
3088
3361
  const cwd = input.cwd ?? process.cwd();
3089
- const filePath = input.path.startsWith("/") ? input.path : join12(cwd, input.path);
3362
+ const filePath = input.path.startsWith("/") ? input.path : join13(cwd, input.path);
3090
3363
  const relPath = relative4(cwd, filePath);
3091
3364
  const file = Bun.file(filePath);
3092
3365
  if (!await file.exists()) {
@@ -3131,12 +3404,12 @@ function parseAnchor(value) {
3131
3404
  }
3132
3405
 
3133
3406
  // src/tools/read.ts
3134
- import { join as join13, relative as relative5 } from "path";
3135
- import { z as z6 } from "zod";
3136
- var ReadSchema = z6.object({
3137
- path: z6.string().describe("File path to read (absolute or relative to cwd)"),
3138
- line: z6.number().int().min(1).optional().describe("1-indexed starting line (default: 1)"),
3139
- count: z6.number().int().min(1).max(500).optional().describe("Lines to read (default: 500, max: 500)")
3407
+ import { join as join14, relative as relative5 } from "path";
3408
+ import { z as z7 } from "zod";
3409
+ var ReadSchema = z7.object({
3410
+ path: z7.string().describe("File path to read (absolute or relative to cwd)"),
3411
+ line: z7.number().int().min(1).optional().describe("1-indexed starting line (default: 1)"),
3412
+ count: z7.number().int().min(1).max(500).optional().describe("Lines to read (default: 500, max: 500)")
3140
3413
  });
3141
3414
  var MAX_COUNT = 500;
3142
3415
  var MAX_BYTES = 1e6;
@@ -3146,7 +3419,7 @@ var readTool = {
3146
3419
  schema: ReadSchema,
3147
3420
  execute: async (input) => {
3148
3421
  const cwd = input.cwd ?? process.cwd();
3149
- const filePath = input.path.startsWith("/") ? input.path : join13(cwd, input.path);
3422
+ const filePath = input.path.startsWith("/") ? input.path : join14(cwd, input.path);
3150
3423
  const file = Bun.file(filePath);
3151
3424
  const exists = await file.exists();
3152
3425
  if (!exists) {
@@ -3179,13 +3452,13 @@ var readTool = {
3179
3452
  };
3180
3453
 
3181
3454
  // src/tools/replace.ts
3182
- import { join as join14, relative as relative6 } from "path";
3183
- import { z as z7 } from "zod";
3184
- var ReplaceSchema = z7.object({
3185
- path: z7.string().describe("File path to edit (absolute or relative to cwd)"),
3186
- startAnchor: z7.string().describe('Start anchor from a prior read/grep, e.g. "11:a3"'),
3187
- endAnchor: z7.string().optional().describe('End anchor (inclusive), e.g. "33:0e". Omit to target only the startAnchor line.'),
3188
- newContent: z7.string().optional().describe("Replacement text. Omit or pass empty string to delete the range.")
3455
+ import { join as join15, relative as relative6 } from "path";
3456
+ import { z as z8 } from "zod";
3457
+ var ReplaceSchema = z8.object({
3458
+ path: z8.string().describe("File path to edit (absolute or relative to cwd)"),
3459
+ startAnchor: z8.string().describe('Start anchor from a prior read/grep, e.g. "11:a3"'),
3460
+ endAnchor: z8.string().optional().describe('End anchor (inclusive), e.g. "33:0e". Omit to target only the startAnchor line.'),
3461
+ newContent: z8.string().optional().describe("Replacement text. Omit or pass empty string to delete the range.")
3189
3462
  });
3190
3463
  var HASH_NOT_FOUND_ERROR2 = "Hash not found. Re-read the file to get current anchors.";
3191
3464
  var replaceTool = {
@@ -3194,7 +3467,7 @@ var replaceTool = {
3194
3467
  schema: ReplaceSchema,
3195
3468
  execute: async (input) => {
3196
3469
  const cwd = input.cwd ?? process.cwd();
3197
- const filePath = input.path.startsWith("/") ? input.path : join14(cwd, input.path);
3470
+ const filePath = input.path.startsWith("/") ? input.path : join15(cwd, input.path);
3198
3471
  const relPath = relative6(cwd, filePath);
3199
3472
  const file = Bun.file(filePath);
3200
3473
  if (!await file.exists()) {
@@ -3252,11 +3525,11 @@ function parseAnchor2(value, name) {
3252
3525
  }
3253
3526
 
3254
3527
  // src/tools/shell.ts
3255
- import { z as z8 } from "zod";
3256
- var ShellSchema = z8.object({
3257
- command: z8.string().describe("Shell command to execute"),
3258
- timeout: z8.number().int().min(1000).max(300000).optional().default(30000).describe("Timeout in milliseconds (default: 30s, max: 5min)"),
3259
- env: z8.record(z8.string(), z8.string()).optional().describe("Additional environment variables to set")
3528
+ import { z as z9 } from "zod";
3529
+ var ShellSchema = z9.object({
3530
+ command: z9.string().describe("Shell command to execute"),
3531
+ timeout: z9.number().int().min(1000).max(300000).optional().default(30000).describe("Timeout in milliseconds (default: 30s, max: 5min)"),
3532
+ env: z9.record(z9.string(), z9.string()).optional().describe("Additional environment variables to set")
3260
3533
  });
3261
3534
  var MAX_OUTPUT_BYTES = 1e4;
3262
3535
  var shellTool = {
@@ -3268,6 +3541,7 @@ var shellTool = {
3268
3541
  const timeout = input.timeout ?? 30000;
3269
3542
  const env = Object.assign({}, process.env, input.env ?? {});
3270
3543
  let timedOut = false;
3544
+ const wasRaw = process.stdin.isTTY ? process.stdin.isRaw : false;
3271
3545
  const proc = Bun.spawn(["bash", "-c", input.command], {
3272
3546
  cwd,
3273
3547
  env,
@@ -3321,6 +3595,11 @@ var shellTool = {
3321
3595
  } finally {
3322
3596
  clearTimeout(timer);
3323
3597
  restoreTerminal();
3598
+ if (wasRaw) {
3599
+ try {
3600
+ process.stdin.setRawMode(true);
3601
+ } catch {}
3602
+ }
3324
3603
  }
3325
3604
  return {
3326
3605
  stdout: stdout.trimEnd(),
@@ -3333,12 +3612,12 @@ var shellTool = {
3333
3612
  };
3334
3613
 
3335
3614
  // src/tools/subagent.ts
3336
- import { z as z9 } from "zod";
3337
- var SubagentInput = z9.object({
3338
- prompt: z9.string().describe("The task or question to give the subagent"),
3339
- agentName: z9.string().optional().describe("Name of a custom agent to use (from .agents/agents/). Omit to use a generic subagent.")
3615
+ import { z as z10 } from "zod";
3616
+ var SubagentInput = z10.object({
3617
+ prompt: z10.string().describe("The task or question to give the subagent"),
3618
+ agentName: z10.string().optional().describe("Name of a custom agent to use (from .agents/agents/). Omit to use a generic subagent.")
3340
3619
  });
3341
- function createSubagentTool(runSubagent, availableAgents) {
3620
+ function createSubagentTool(runSubagent, availableAgents, parentLabel) {
3342
3621
  const agentSection = availableAgents.size > 0 ? `
3343
3622
 
3344
3623
  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 +3626,7 @@ When the user's message contains @<agent-name>, delegate to that agent by settin
3347
3626
  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
3627
  schema: SubagentInput,
3349
3628
  execute: async (input) => {
3350
- return runSubagent(input.prompt, input.agentName);
3629
+ return runSubagent(input.prompt, input.agentName, parentLabel);
3351
3630
  }
3352
3631
  };
3353
3632
  }
@@ -3396,7 +3675,7 @@ function buildToolSet(opts) {
3396
3675
  const { cwd, onHook } = opts;
3397
3676
  const depth = opts.depth ?? 0;
3398
3677
  const lookupHook = createHookCache(HOOKABLE_TOOLS, cwd);
3399
- return [
3678
+ const tools = [
3400
3679
  withHooks(withCwdDefault(globTool, cwd), lookupHook, cwd, (_, input) => hookEnvForGlob(input, cwd), onHook),
3401
3680
  withHooks(withCwdDefault(grepTool, cwd), lookupHook, cwd, (_, input) => hookEnvForGrep(input, cwd), onHook),
3402
3681
  withHooks(withCwdDefault(readTool, cwd), lookupHook, cwd, (_, input) => hookEnvForRead(input, cwd), onHook),
@@ -3408,17 +3687,25 @@ function buildToolSet(opts) {
3408
3687
  if (depth >= MAX_SUBAGENT_DEPTH) {
3409
3688
  throw new Error(`Subagent depth limit reached (max ${MAX_SUBAGENT_DEPTH}). ` + `Cannot spawn another subagent from depth ${depth}.`);
3410
3689
  }
3411
- return opts.runSubagent(prompt, depth + 1, agentName);
3412
- }, opts.availableAgents)
3690
+ return opts.runSubagent(prompt, depth + 1, agentName, undefined, opts.parentLabel);
3691
+ }, opts.availableAgents, opts.parentLabel)
3413
3692
  ];
3693
+ if (process.env.EXA_API_KEY) {
3694
+ tools.push(webSearchTool, webContentTool);
3695
+ }
3696
+ return tools;
3414
3697
  }
3415
3698
  function buildReadOnlyToolSet(opts) {
3416
3699
  const { cwd } = opts;
3417
- return [
3700
+ const tools = [
3418
3701
  withCwdDefault(globTool, cwd),
3419
3702
  withCwdDefault(grepTool, cwd),
3420
3703
  withCwdDefault(readTool, cwd)
3421
3704
  ];
3705
+ if (process.env.EXA_API_KEY) {
3706
+ tools.push(webSearchTool, webContentTool);
3707
+ }
3708
+ return tools;
3422
3709
  }
3423
3710
 
3424
3711
  // src/agent/agent.ts
@@ -3440,9 +3727,9 @@ async function getGitBranch(cwd) {
3440
3727
  }
3441
3728
  function loadContextFile(cwd) {
3442
3729
  const candidates = [
3443
- join15(cwd, "AGENTS.md"),
3444
- join15(cwd, "CLAUDE.md"),
3445
- join15(getConfigDir(), "AGENTS.md")
3730
+ join16(cwd, "AGENTS.md"),
3731
+ join16(cwd, "CLAUDE.md"),
3732
+ join16(getConfigDir(), "AGENTS.md")
3446
3733
  ];
3447
3734
  for (const p of candidates) {
3448
3735
  if (existsSync6(p)) {
@@ -3453,7 +3740,19 @@ function loadContextFile(cwd) {
3453
3740
  }
3454
3741
  return null;
3455
3742
  }
3456
- function buildSystemPrompt(cwd) {
3743
+ var CODEX_AUTONOMY = `
3744
+ # Autonomy and persistence
3745
+ - You are an autonomous senior engineer. Once given a direction, proactively gather context, implement, test, and refine without waiting for additional prompts at each step.
3746
+ - Persist until the task is fully handled end-to-end within the current turn: do not stop at analysis or partial work; carry changes through to implementation and verification.
3747
+ - Bias to action: default to implementing with reasonable assumptions. Do not end your turn with clarifications or requests to "proceed" unless you are truly blocked on information only the user can provide.
3748
+ - Do NOT output an upfront plan, preamble, or status update before working. Start making tool calls immediately.
3749
+ - Do NOT ask "shall I proceed?", "shall I start?", "reply X to continue", or any equivalent. Just start.
3750
+ - If something is ambiguous, pick the most reasonable interpretation, implement it, and note the assumption at the end.`;
3751
+ function isCodexModel(modelString) {
3752
+ const { modelId } = parseModelString(modelString);
3753
+ return modelId.includes("codex");
3754
+ }
3755
+ function buildSystemPrompt(cwd, modelString) {
3457
3756
  const contextFile = loadContextFile(cwd);
3458
3757
  const cwdDisplay = tildePath(cwd);
3459
3758
  const now = new Date().toLocaleString(undefined, { hour12: false });
@@ -3468,8 +3767,10 @@ Guidelines:
3468
3767
  - Prefer small, targeted edits over large rewrites.
3469
3768
  - Always read a file before editing it.
3470
3769
  - Use glob to discover files, grep to find patterns, read to inspect contents.
3471
- - Use shell for tests, builds, and git operations.
3472
- - When in doubt, ask the user before making destructive changes.`;
3770
+ - Use shell for tests, builds, and git operations.`;
3771
+ if (modelString && isCodexModel(modelString)) {
3772
+ prompt += CODEX_AUTONOMY;
3773
+ }
3473
3774
  if (contextFile) {
3474
3775
  prompt += `
3475
3776
 
@@ -3519,61 +3820,52 @@ async function runAgent(opts) {
3519
3820
  }
3520
3821
  let turnIndex = getMaxTurnIndex(session.id) + 1;
3521
3822
  const coreHistory = [...session.messages];
3522
- const runSubagent = async (prompt, depth = 0, agentName, modelOverride) => {
3823
+ let nextLaneId = 1;
3824
+ const activeLanes = new Set;
3825
+ const runSubagent = async (prompt, depth = 0, agentName, modelOverride, parentLabel) => {
3523
3826
  const allAgents = loadAgents(cwd);
3524
3827
  const agentConfig = agentName ? allAgents.get(agentName) : undefined;
3525
3828
  if (agentName && !agentConfig) {
3526
3829
  throw new Error(`Unknown agent "${agentName}". Available agents: ${[...allAgents.keys()].join(", ") || "(none)"}`);
3527
3830
  }
3528
3831
  const model = modelOverride ?? agentConfig?.model ?? currentModel;
3529
- const systemPrompt = agentConfig?.systemPrompt ?? buildSystemPrompt(cwd);
3832
+ const systemPrompt = agentConfig?.systemPrompt ?? buildSystemPrompt(cwd, model);
3530
3833
  const subMessages = [{ role: "user", content: prompt }];
3834
+ const laneId = nextLaneId++;
3835
+ activeLanes.add(laneId);
3836
+ const laneLabel = formatSubagentLabel(laneId, parentLabel);
3531
3837
  const subTools = buildToolSet({
3532
3838
  cwd,
3533
3839
  depth,
3534
3840
  runSubagent,
3535
3841
  onHook: renderHook,
3536
- availableAgents: allAgents
3842
+ availableAgents: allAgents,
3843
+ parentLabel: laneLabel
3537
3844
  });
3538
3845
  const subLlm = resolveModel(model);
3539
3846
  let result = "";
3540
3847
  let inputTokens = 0;
3541
3848
  let outputTokens = 0;
3542
- const activity = [];
3543
- const pendingCalls = new Map;
3544
3849
  const events = runTurn({
3545
3850
  model: subLlm,
3851
+ modelString: model,
3546
3852
  messages: subMessages,
3547
3853
  tools: subTools,
3548
3854
  systemPrompt
3549
3855
  });
3550
3856
  for await (const event of events) {
3857
+ spinner.stop();
3858
+ renderSubagentEvent(event, { laneId, parentLabel, activeLanes });
3859
+ spinner.start("thinking");
3551
3860
  if (event.type === "text-delta")
3552
3861
  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
3862
  if (event.type === "turn-complete") {
3572
3863
  inputTokens = event.inputTokens;
3573
3864
  outputTokens = event.outputTokens;
3574
3865
  }
3575
3866
  }
3576
- return { result, inputTokens, outputTokens, activity };
3867
+ activeLanes.delete(laneId);
3868
+ return { result, inputTokens, outputTokens };
3577
3869
  };
3578
3870
  const agents = loadAgents(cwd);
3579
3871
  const tools = buildToolSet({
@@ -3760,12 +4052,10 @@ ${out}
3760
4052
  async function processUserInput(text, pastedImages = []) {
3761
4053
  const abortController = new AbortController;
3762
4054
  let wasAborted = false;
3763
- const onSigInt = () => {
4055
+ abortController.signal.addEventListener("abort", () => {
3764
4056
  wasAborted = true;
3765
- abortController.abort();
3766
- process.removeListener("SIGINT", onSigInt);
3767
- };
3768
- process.on("SIGINT", onSigInt);
4057
+ });
4058
+ const stopWatcher = watchForInterrupt(abortController);
3769
4059
  const { text: resolvedText, images: refImages } = await resolveFileRefs(text, cwd);
3770
4060
  const allImages = [...pastedImages, ...refImages];
3771
4061
  const thisTurn = turnIndex++;
@@ -3774,7 +4064,7 @@ ${out}
3774
4064
 
3775
4065
  <system-message>PLAN MODE ACTIVE: Help the user gather context for the plan -- READ ONLY</system-message>` : ralphMode ? `${resolvedText}
3776
4066
 
3777
- <system-message>RALPH MODE: You are in an autonomous loop. When the task is fully complete (all tests pass, no outstanding issues), output exactly \`/ralph\` as your final message to exit the loop. Otherwise, keep working.</system-message>` : resolvedText;
4067
+ <system-message>RALPH MODE: You are in an autonomous loop. You MUST make actual file changes (create, edit, or write files) to complete the requested task before outputting \`/ralph\`. Reading files, running tests, or exploring the codebase does NOT count as doing the work. Only output \`/ralph\` as your final message after all requested changes are implemented and tests pass.</system-message>` : resolvedText;
3778
4068
  const userMsg = allImages.length > 0 ? {
3779
4069
  role: "user",
3780
4070
  content: [
@@ -3787,11 +4077,8 @@ ${out}
3787
4077
  ]
3788
4078
  } : { role: "user", content: coreContent };
3789
4079
  if (wasAborted) {
3790
- process.removeListener("SIGINT", onSigInt);
3791
- const stubMsg = {
3792
- role: "assistant",
3793
- content: "[interrupted]"
3794
- };
4080
+ stopWatcher();
4081
+ const stubMsg = makeInterruptMessage("user");
3795
4082
  session.messages.push(userMsg, stubMsg);
3796
4083
  saveMessages(session.id, [userMsg, stubMsg], thisTurn);
3797
4084
  coreHistory.push(userMsg, stubMsg);
@@ -3803,26 +4090,15 @@ ${out}
3803
4090
  saveMessages(session.id, [userMsg], thisTurn);
3804
4091
  coreHistory.push(userMsg);
3805
4092
  const llm = resolveModel(currentModel);
3806
- const systemPrompt = buildSystemPrompt(cwd);
4093
+ const systemPrompt = buildSystemPrompt(cwd, currentModel);
3807
4094
  let lastAssistantText = "";
3808
- let turnRolledBack = false;
3809
- const rollbackTurn = () => {
3810
- if (turnRolledBack)
3811
- return;
3812
- turnRolledBack = true;
3813
- coreHistory.pop();
3814
- session.messages.pop();
3815
- deleteLastTurn(session.id, thisTurn);
3816
- if (snapped)
3817
- deleteSnapshot(session.id, thisTurn);
3818
- snapshotStack.pop();
3819
- turnIndex--;
3820
- };
4095
+ let errorStubSaved = false;
3821
4096
  try {
3822
4097
  snapshotStack.push(snapped ? thisTurn : null);
3823
4098
  spinner.start("thinking");
3824
4099
  const events = runTurn({
3825
4100
  model: llm,
4101
+ modelString: currentModel,
3826
4102
  messages: coreHistory,
3827
4103
  tools: planMode ? [...buildReadOnlyToolSet({ cwd }), ...mcpTools] : tools,
3828
4104
  systemPrompt,
@@ -3833,16 +4109,17 @@ ${out}
3833
4109
  coreHistory.push(...newMessages);
3834
4110
  session.messages.push(...newMessages);
3835
4111
  saveMessages(session.id, newMessages, thisTurn);
3836
- } else if (wasAborted) {
3837
- const stubMsg = {
3838
- role: "assistant",
3839
- content: "[interrupted]"
3840
- };
4112
+ if (wasAborted) {
4113
+ const note = makeInterruptMessage("user");
4114
+ coreHistory.push(note);
4115
+ session.messages.push(note);
4116
+ saveMessages(session.id, [note], thisTurn);
4117
+ }
4118
+ } else {
4119
+ const stubMsg = makeInterruptMessage("user");
3841
4120
  coreHistory.push(stubMsg);
3842
4121
  session.messages.push(stubMsg);
3843
4122
  saveMessages(session.id, [stubMsg], thisTurn);
3844
- } else {
3845
- rollbackTurn();
3846
4123
  }
3847
4124
  lastAssistantText = extractAssistantText(newMessages);
3848
4125
  totalIn += inputTokens;
@@ -3850,10 +4127,16 @@ ${out}
3850
4127
  lastContextTokens = contextTokens;
3851
4128
  touchActiveSession(session);
3852
4129
  } catch (err) {
3853
- rollbackTurn();
4130
+ if (!errorStubSaved) {
4131
+ errorStubSaved = true;
4132
+ const stubMsg = makeInterruptMessage("error");
4133
+ coreHistory.push(stubMsg);
4134
+ session.messages.push(stubMsg);
4135
+ saveMessages(session.id, [stubMsg], thisTurn);
4136
+ }
3854
4137
  throw err;
3855
4138
  } finally {
3856
- process.removeListener("SIGINT", onSigInt);
4139
+ stopWatcher();
3857
4140
  if (wasAborted)
3858
4141
  ralphMode = false;
3859
4142
  }
@@ -3878,6 +4161,10 @@ ${out}
3878
4161
  });
3879
4162
  }
3880
4163
  }
4164
+ function makeInterruptMessage(reason) {
4165
+ const text = reason === "user" ? "<system-message>Response was interrupted by the user.</system-message>" : "<system-message>Response was interrupted due to an error.</system-message>";
4166
+ return { role: "assistant", content: text };
4167
+ }
3881
4168
  function extractAssistantText(newMessages) {
3882
4169
  const parts = [];
3883
4170
  for (const msg of newMessages) {
@@ -3917,7 +4204,7 @@ ${skill.content}
3917
4204
  result = result.slice(0, match.index) + replacement + result.slice((match.index ?? 0) + match[0].length);
3918
4205
  continue;
3919
4206
  }
3920
- const filePath = ref.startsWith("/") ? ref : join15(cwd, ref);
4207
+ const filePath = ref.startsWith("/") ? ref : join16(cwd, ref);
3921
4208
  if (isImageFilename(ref)) {
3922
4209
  const attachment = await loadImageFile(filePath);
3923
4210
  if (attachment) {
@@ -3945,6 +4232,7 @@ ${preview}
3945
4232
 
3946
4233
  // src/index.ts
3947
4234
  registerTerminalCleanup();
4235
+ initErrorLog();
3948
4236
  function parseArgs(argv) {
3949
4237
  const args = {
3950
4238
  model: null,
@@ -4063,11 +4351,11 @@ async function main() {
4063
4351
  agentOpts.initialPrompt = args.prompt;
4064
4352
  await runAgent(agentOpts);
4065
4353
  } catch (err) {
4066
- renderError(err);
4354
+ renderError(err, "agent");
4067
4355
  process.exit(1);
4068
4356
  }
4069
4357
  }
4070
4358
  main().catch((err) => {
4071
- console.error(err);
4359
+ renderError(err, "main");
4072
4360
  process.exit(1);
4073
4361
  });