mini-coder 0.0.13 → 0.0.15

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
@@ -349,17 +349,6 @@ function renderMarkdown(text) {
349
349
  }).join(`
350
350
  `);
351
351
  }
352
- function renderChunk(text, inFence) {
353
- let fence = inFence;
354
- const output = text.split(`
355
- `).map((raw) => {
356
- const r = renderLine(raw, fence);
357
- fence = r.inFence;
358
- return r.output;
359
- }).join(`
360
- `);
361
- return { output, inFence: fence };
362
- }
363
352
 
364
353
  // src/cli/tool-render.ts
365
354
  import { homedir as homedir2 } from "os";
@@ -433,7 +422,7 @@ function toolCallLine(name, args) {
433
422
  if (name === "shell") {
434
423
  const cmd = String(a.command ?? "");
435
424
  const shortCmd = cmd.length > 72 ? `${cmd.slice(0, 69)}\u2026` : cmd;
436
- return `${G.run} ${c3.dim("$")} ${shortCmd}`;
425
+ return `${G.run} ${shortCmd}`;
437
426
  }
438
427
  if (name === "subagent") {
439
428
  const prompt = String(a.prompt ?? "");
@@ -474,19 +463,25 @@ function renderDiff(diff) {
474
463
  }
475
464
  }
476
465
  }
466
+ function formatErrorBadge(result) {
467
+ const msg = typeof result === "string" ? result : result instanceof Error ? result.message : JSON.stringify(result);
468
+ const oneLiner = msg.split(`
469
+ `)[0] ?? msg;
470
+ return `${G.err} ${c3.red(oneLiner)}`;
471
+ }
472
+ function formatShellBadge(r) {
473
+ return r.timedOut ? c3.yellow("timeout") : r.success ? c3.green(`\u2714 ${r.exitCode}`) : c3.red(`\u2716 ${r.exitCode}`);
474
+ }
477
475
  function renderToolResultInline(toolName, result, isError, indent) {
478
476
  if (isError) {
479
- const msg = typeof result === "string" ? result : result instanceof Error ? result.message : JSON.stringify(result);
480
- const oneLiner = msg.split(`
481
- `)[0] ?? msg;
482
- writeln(`${indent}${G.err} ${c3.red(oneLiner)}`);
477
+ writeln(`${indent}${formatErrorBadge(result)}`);
483
478
  return;
484
479
  }
485
480
  if (toolName === "glob") {
486
- const r = result;
487
- if (Array.isArray(r?.files)) {
488
- const n = r.files.length;
489
- writeln(`${indent}${G.info} ${c3.dim(n === 0 ? "no matches" : `${n} file${n === 1 ? "" : "s"}${r.truncated ? " (capped)" : ""}`)}`);
481
+ const res = result;
482
+ if (Array.isArray(res?.files)) {
483
+ const n = res.files.length;
484
+ writeln(`${indent}${G.info} ${c3.dim(n === 0 ? "no matches" : `${n} file${n === 1 ? "" : "s"}${res.truncated ? " (capped)" : ""}`)}`);
490
485
  return;
491
486
  }
492
487
  }
@@ -517,7 +512,7 @@ function renderToolResultInline(toolName, result, isError, indent) {
517
512
  }
518
513
  if (toolName === "shell") {
519
514
  const r = result;
520
- const badge = r.timedOut ? c3.yellow("timeout") : r.success ? c3.green(`\u2714 ${r.exitCode}`) : c3.red(`\u2716 ${r.exitCode}`);
515
+ const badge = formatShellBadge(r);
521
516
  writeln(`${indent}${badge}`);
522
517
  return;
523
518
  }
@@ -556,10 +551,7 @@ function renderToolResultInline(toolName, result, isError, indent) {
556
551
  }
557
552
  function renderToolResult(toolName, result, isError) {
558
553
  if (isError) {
559
- const msg = typeof result === "string" ? result : result instanceof Error ? result.message : JSON.stringify(result);
560
- const oneLiner = msg.split(`
561
- `)[0] ?? msg;
562
- writeln(` ${G.err} ${c3.red(oneLiner)}`);
554
+ writeln(` ${formatErrorBadge(result)}`);
563
555
  return;
564
556
  }
565
557
  if (toolName === "glob") {
@@ -635,7 +627,7 @@ function renderToolResult(toolName, result, isError) {
635
627
  }
636
628
  if (toolName === "shell") {
637
629
  const r = result;
638
- const badge = r.timedOut ? c3.yellow("timeout") : r.success ? c3.green(`\u2714 ${r.exitCode}`) : c3.red(`\u2716 ${r.exitCode}`);
630
+ const badge = formatShellBadge(r);
639
631
  writeln(` ${badge}`);
640
632
  const outLines = r.stdout ? r.stdout.split(`
641
633
  `) : [];
@@ -716,15 +708,27 @@ function renderToolResult(toolName, result, isError) {
716
708
  const text = JSON.stringify(result);
717
709
  writeln(` ${c3.dim(text.length > 120 ? `${text.slice(0, 117)}\u2026` : text)}`);
718
710
  }
711
+ function stripAnsiSgr(text) {
712
+ const esc = String.fromCharCode(27);
713
+ return text.replace(new RegExp(`${esc}\\[[0-9;]*m`, "g"), "");
714
+ }
715
+ function normalizeParentLaneLabel(parentLabel) {
716
+ const plain = stripAnsiSgr(parentLabel);
717
+ const match = plain.match(/\[([^\]]+)\]/);
718
+ const inner = (match?.[1] ?? plain).trim();
719
+ const lanePath = (inner.split("\xB7")[0] ?? inner).trim();
720
+ return lanePath || inner;
721
+ }
719
722
  function formatSubagentLabel(laneId, parentLabel) {
720
- const numStr = parentLabel ? `${parentLabel.replace(/[\[\]]/g, "")}.${laneId}` : `${laneId}`;
723
+ const parent = parentLabel ? normalizeParentLaneLabel(parentLabel) : "";
724
+ const numStr = parent ? `${parent}.${laneId}` : `${laneId}`;
721
725
  return c3.dim(c3.cyan(`[${numStr}]`));
722
726
  }
723
727
  var laneBuffers = new Map;
724
728
  function renderSubagentEvent(event, opts) {
725
- const { laneId, parentLabel, activeLanes } = opts;
729
+ const { laneId, parentLabel, hasWorktree, activeLanes } = opts;
726
730
  const labelStr = formatSubagentLabel(laneId, parentLabel);
727
- const prefix = activeLanes.size > 1 ? `${labelStr} ` : "";
731
+ const prefix = activeLanes.size > 1 || hasWorktree ? `${labelStr} ` : "";
728
732
  if (event.type === "text-delta") {
729
733
  const buf = (laneBuffers.get(laneId) ?? "") + event.delta;
730
734
  const lines = buf.split(`
@@ -766,82 +770,46 @@ async function renderTurn(events, spinner) {
766
770
  let inText = false;
767
771
  let rawBuffer = "";
768
772
  let inFence = false;
769
- let printQueue = "";
770
- let printPos = 0;
771
- let tickerHandle = null;
772
773
  let inputTokens = 0;
773
774
  let outputTokens = 0;
774
775
  let contextTokens = 0;
775
776
  let newMessages = [];
776
- function enqueuePrint(ansi) {
777
- printQueue += ansi;
778
- if (tickerHandle === null) {
779
- scheduleTick();
780
- }
781
- }
782
- function tick() {
783
- tickerHandle = null;
784
- if (printPos < printQueue.length) {
785
- process.stdout.write(printQueue[printPos]);
786
- printPos++;
787
- scheduleTick();
788
- } else {
789
- printQueue = "";
790
- printPos = 0;
791
- }
792
- }
793
- function scheduleTick() {
794
- tickerHandle = setTimeout(tick, 8);
795
- }
796
- function drainQueue() {
797
- if (tickerHandle !== null) {
798
- clearTimeout(tickerHandle);
799
- tickerHandle = null;
800
- }
801
- if (printPos < printQueue.length) {
802
- process.stdout.write(printQueue.slice(printPos));
803
- }
804
- printQueue = "";
805
- printPos = 0;
806
- }
807
- function renderAndTrim(end) {
808
- const chunk = rawBuffer.slice(0, end);
809
- const rendered = renderChunk(chunk, inFence);
777
+ function renderAndWrite(raw, endWithNewline) {
778
+ const rendered = renderLine(raw, inFence);
810
779
  inFence = rendered.inFence;
811
- enqueuePrint(rendered.output);
812
- rawBuffer = rawBuffer.slice(end);
780
+ write(rendered.output);
781
+ if (endWithNewline) {
782
+ write(`
783
+ `);
784
+ }
813
785
  }
814
- function flushChunks() {
786
+ function flushCompleteLines() {
815
787
  let boundary = rawBuffer.indexOf(`
816
-
817
788
  `);
818
- if (boundary !== -1) {
819
- renderAndTrim(boundary + 2);
820
- flushChunks();
821
- return;
822
- }
823
- boundary = rawBuffer.lastIndexOf(`
789
+ while (boundary !== -1) {
790
+ renderAndWrite(rawBuffer.slice(0, boundary), true);
791
+ rawBuffer = rawBuffer.slice(boundary + 1);
792
+ boundary = rawBuffer.indexOf(`
824
793
  `);
825
- if (boundary !== -1) {
826
- renderAndTrim(boundary + 1);
827
794
  }
828
795
  }
829
796
  function flushAll() {
830
- if (rawBuffer) {
831
- renderAndTrim(rawBuffer.length);
797
+ if (!rawBuffer) {
798
+ return;
832
799
  }
833
- drainQueue();
800
+ renderAndWrite(rawBuffer, false);
801
+ rawBuffer = "";
834
802
  }
835
803
  for await (const event of events) {
836
804
  switch (event.type) {
837
805
  case "text-delta": {
838
806
  if (!inText) {
839
807
  spinner.stop();
840
- process.stdout.write(`${G.reply} `);
808
+ write(`${G.reply} `);
841
809
  inText = true;
842
810
  }
843
811
  rawBuffer += event.delta;
844
- flushChunks();
812
+ flushCompleteLines();
845
813
  break;
846
814
  }
847
815
  case "tool-call-start": {
@@ -940,7 +908,7 @@ function renderStatusBar(opts) {
940
908
  left.push(c5.magenta("\u21BB ralph"));
941
909
  const right = [];
942
910
  if (opts.inputTokens > 0 || opts.outputTokens > 0) {
943
- right.push(c5.dim(`\u2191${fmtTokens(opts.inputTokens)} \u2193${fmtTokens(opts.outputTokens)}`));
911
+ right.push(c5.dim(`\u2191 ${fmtTokens(opts.inputTokens)} \u2193 ${fmtTokens(opts.outputTokens)}`));
944
912
  }
945
913
  if (opts.contextTokens > 0) {
946
914
  const ctxRaw = fmtTokens(opts.contextTokens);
@@ -978,7 +946,7 @@ function renderError(err, context = "render") {
978
946
 
979
947
  // src/cli/output.ts
980
948
  var HOME2 = homedir3();
981
- var PACKAGE_VERSION = "0.0.12";
949
+ var PACKAGE_VERSION = "0.0.15";
982
950
  function tildePath(p) {
983
951
  return p.startsWith(HOME2) ? `~${p.slice(HOME2.length)}` : p;
984
952
  }
@@ -1024,7 +992,7 @@ function renderBanner(model, cwd) {
1024
992
  writeln();
1025
993
  writeln(` ${c7.cyan("mc")} ${c7.dim(`mini-coder \xB7 v${PACKAGE_VERSION}`)}`);
1026
994
  writeln(` ${c7.dim(model)} ${c7.dim("\xB7")} ${c7.dim(cwd)}`);
1027
- writeln(` ${c7.dim("/help for commands \xB7 ctrl+d to exit")}`);
995
+ writeln(` ${c7.dim("/help for commands \xB7 esc cancel \xB7 ctrl+c/ctrl+d exit")}`);
1028
996
  writeln();
1029
997
  }
1030
998
  function renderInfo(msg) {
@@ -1068,6 +1036,8 @@ function parseFrontmatter(raw) {
1068
1036
  meta.description = val;
1069
1037
  if (key === "model")
1070
1038
  meta.model = val;
1039
+ if (key === "execution")
1040
+ meta.execution = val;
1071
1041
  }
1072
1042
  return { meta, body: (m[2] ?? "").trim() };
1073
1043
  }
@@ -1207,6 +1177,745 @@ function logApiEvent(event, data) {
1207
1177
  writer2.flush();
1208
1178
  }
1209
1179
 
1180
+ // src/session/db/connection.ts
1181
+ import { Database } from "bun:sqlite";
1182
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, unlinkSync } from "fs";
1183
+ import { homedir as homedir6 } from "os";
1184
+ import { join as join4 } from "path";
1185
+ function getConfigDir() {
1186
+ return join4(homedir6(), ".config", "mini-coder");
1187
+ }
1188
+ function getDbPath() {
1189
+ const dir = getConfigDir();
1190
+ if (!existsSync2(dir))
1191
+ mkdirSync3(dir, { recursive: true });
1192
+ return join4(dir, "sessions.db");
1193
+ }
1194
+ var DB_VERSION = 3;
1195
+ var SCHEMA = `
1196
+ CREATE TABLE IF NOT EXISTS sessions (
1197
+ id TEXT PRIMARY KEY,
1198
+ title TEXT NOT NULL DEFAULT '',
1199
+ cwd TEXT NOT NULL,
1200
+ model TEXT NOT NULL,
1201
+ created_at INTEGER NOT NULL,
1202
+ updated_at INTEGER NOT NULL
1203
+ );
1204
+
1205
+ CREATE TABLE IF NOT EXISTS messages (
1206
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1207
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
1208
+ payload TEXT NOT NULL,
1209
+ turn_index INTEGER NOT NULL DEFAULT 0,
1210
+ created_at INTEGER NOT NULL
1211
+ );
1212
+
1213
+ CREATE TABLE IF NOT EXISTS prompt_history (
1214
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1215
+ text TEXT NOT NULL,
1216
+ created_at INTEGER NOT NULL
1217
+ );
1218
+
1219
+ CREATE TABLE IF NOT EXISTS mcp_servers (
1220
+ name TEXT PRIMARY KEY,
1221
+ transport TEXT NOT NULL,
1222
+ url TEXT,
1223
+ command TEXT,
1224
+ args TEXT,
1225
+ env TEXT,
1226
+ created_at INTEGER NOT NULL
1227
+ );
1228
+
1229
+ CREATE INDEX IF NOT EXISTS idx_messages_session
1230
+ ON messages(session_id, id);
1231
+
1232
+ CREATE INDEX IF NOT EXISTS idx_messages_turn
1233
+ ON messages(session_id, turn_index);
1234
+
1235
+ CREATE INDEX IF NOT EXISTS idx_sessions_updated
1236
+ ON sessions(updated_at DESC);
1237
+
1238
+ CREATE TABLE IF NOT EXISTS settings (
1239
+ key TEXT PRIMARY KEY,
1240
+ value TEXT NOT NULL
1241
+ );
1242
+ CREATE TABLE IF NOT EXISTS model_capabilities (
1243
+ canonical_model_id TEXT PRIMARY KEY,
1244
+ context_window INTEGER,
1245
+ reasoning INTEGER NOT NULL,
1246
+ source_provider TEXT,
1247
+ raw_json TEXT,
1248
+ updated_at INTEGER NOT NULL
1249
+ );
1250
+
1251
+ CREATE TABLE IF NOT EXISTS provider_models (
1252
+ provider TEXT NOT NULL,
1253
+ provider_model_id TEXT NOT NULL,
1254
+ display_name TEXT NOT NULL,
1255
+ canonical_model_id TEXT,
1256
+ context_window INTEGER,
1257
+ free INTEGER,
1258
+ updated_at INTEGER NOT NULL,
1259
+ PRIMARY KEY (provider, provider_model_id)
1260
+ );
1261
+
1262
+ CREATE INDEX IF NOT EXISTS idx_provider_models_provider
1263
+ ON provider_models(provider);
1264
+
1265
+ CREATE INDEX IF NOT EXISTS idx_provider_models_canonical
1266
+ ON provider_models(canonical_model_id);
1267
+
1268
+ CREATE TABLE IF NOT EXISTS model_info_state (
1269
+ key TEXT PRIMARY KEY,
1270
+ value TEXT NOT NULL
1271
+ );
1272
+
1273
+
1274
+
1275
+ CREATE TABLE IF NOT EXISTS snapshots (
1276
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1277
+ session_id TEXT NOT NULL,
1278
+ turn_index INTEGER NOT NULL,
1279
+ path TEXT NOT NULL,
1280
+ content BLOB,
1281
+ existed INTEGER NOT NULL
1282
+ );
1283
+
1284
+ CREATE INDEX IF NOT EXISTS idx_snapshots_turn
1285
+ ON snapshots(session_id, turn_index);
1286
+ `;
1287
+ var _db = null;
1288
+ function getDb() {
1289
+ if (!_db) {
1290
+ const dbPath = getDbPath();
1291
+ let db = new Database(dbPath, { create: true });
1292
+ db.exec("PRAGMA journal_mode=WAL;");
1293
+ db.exec("PRAGMA foreign_keys=ON;");
1294
+ const version = db.query("PRAGMA user_version").get()?.user_version ?? 0;
1295
+ if (version !== DB_VERSION) {
1296
+ try {
1297
+ db.close();
1298
+ } catch {}
1299
+ for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
1300
+ if (existsSync2(path))
1301
+ unlinkSync(path);
1302
+ }
1303
+ db = new Database(dbPath, { create: true });
1304
+ db.exec("PRAGMA journal_mode=WAL;");
1305
+ db.exec("PRAGMA foreign_keys=ON;");
1306
+ db.exec(SCHEMA);
1307
+ db.exec(`PRAGMA user_version = ${DB_VERSION};`);
1308
+ } else {
1309
+ db.exec(SCHEMA);
1310
+ }
1311
+ _db = db;
1312
+ }
1313
+ return _db;
1314
+ }
1315
+
1316
+ // src/session/db/model-info-repo.ts
1317
+ function listModelCapabilities() {
1318
+ return getDb().query("SELECT canonical_model_id, context_window, reasoning, source_provider, raw_json, updated_at FROM model_capabilities").all();
1319
+ }
1320
+ function replaceModelCapabilities(rows) {
1321
+ const db = getDb();
1322
+ const insertStmt = db.prepare(`INSERT INTO model_capabilities (
1323
+ canonical_model_id,
1324
+ context_window,
1325
+ reasoning,
1326
+ source_provider,
1327
+ raw_json,
1328
+ updated_at
1329
+ ) VALUES (?, ?, ?, ?, ?, ?)`);
1330
+ const run = db.transaction(() => {
1331
+ db.run("DELETE FROM model_capabilities");
1332
+ for (const row of rows) {
1333
+ insertStmt.run(row.canonical_model_id, row.context_window, row.reasoning, row.source_provider, row.raw_json, row.updated_at);
1334
+ }
1335
+ });
1336
+ run();
1337
+ }
1338
+ function listProviderModels() {
1339
+ return getDb().query("SELECT provider, provider_model_id, display_name, canonical_model_id, context_window, free, updated_at FROM provider_models ORDER BY provider ASC, display_name ASC").all();
1340
+ }
1341
+ function replaceProviderModels(provider, rows) {
1342
+ const db = getDb();
1343
+ const insertStmt = db.prepare(`INSERT INTO provider_models (
1344
+ provider,
1345
+ provider_model_id,
1346
+ display_name,
1347
+ canonical_model_id,
1348
+ context_window,
1349
+ free,
1350
+ updated_at
1351
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
1352
+ ON CONFLICT(provider, provider_model_id) DO UPDATE SET
1353
+ display_name = excluded.display_name,
1354
+ canonical_model_id = excluded.canonical_model_id,
1355
+ context_window = excluded.context_window,
1356
+ free = excluded.free,
1357
+ updated_at = excluded.updated_at`);
1358
+ const run = db.transaction(() => {
1359
+ db.run("DELETE FROM provider_models WHERE provider = ?", [provider]);
1360
+ for (const row of rows) {
1361
+ insertStmt.run(provider, row.provider_model_id, row.display_name, row.canonical_model_id, row.context_window, row.free, row.updated_at);
1362
+ }
1363
+ });
1364
+ run();
1365
+ }
1366
+ function setModelInfoState(key, value) {
1367
+ getDb().run(`INSERT INTO model_info_state (key, value) VALUES (?, ?)
1368
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`, [key, value]);
1369
+ }
1370
+ function listModelInfoState() {
1371
+ return getDb().query("SELECT key, value FROM model_info_state").all();
1372
+ }
1373
+
1374
+ // src/llm-api/model-info.ts
1375
+ var ZEN_BASE = "https://opencode.ai/zen/v1";
1376
+ var OPENAI_BASE = "https://api.openai.com";
1377
+ var ANTHROPIC_BASE = "https://api.anthropic.com";
1378
+ var GOOGLE_BASE = "https://generativelanguage.googleapis.com/v1beta";
1379
+ var MODELS_DEV_URL = "https://models.dev/api.json";
1380
+ var MODELS_DEV_SYNC_KEY = "last_models_dev_sync_at";
1381
+ var PROVIDER_SYNC_KEY_PREFIX = "last_provider_sync_at:";
1382
+ var MODEL_INFO_TTL_MS = 7 * 24 * 60 * 60 * 1000;
1383
+ var runtimeCache = emptyRuntimeCache();
1384
+ var loaded = false;
1385
+ var refreshInFlight = null;
1386
+ function emptyRuntimeCache() {
1387
+ return {
1388
+ capabilitiesByCanonical: new Map,
1389
+ providerModelsByKey: new Map,
1390
+ providerModelUniqIndex: new Map,
1391
+ matchIndex: {
1392
+ exact: new Map,
1393
+ alias: new Map
1394
+ },
1395
+ state: new Map
1396
+ };
1397
+ }
1398
+ function isRecord(value) {
1399
+ return typeof value === "object" && value !== null;
1400
+ }
1401
+ function parseModelStringLoose(modelString) {
1402
+ const slash = modelString.indexOf("/");
1403
+ if (slash === -1) {
1404
+ return { provider: null, modelId: modelString };
1405
+ }
1406
+ const provider = modelString.slice(0, slash).trim().toLowerCase();
1407
+ const modelId = modelString.slice(slash + 1);
1408
+ return { provider: provider || null, modelId };
1409
+ }
1410
+ function providerModelKey(provider, modelId) {
1411
+ return `${provider}/${modelId}`;
1412
+ }
1413
+ function basename2(value) {
1414
+ const idx = value.lastIndexOf("/");
1415
+ return idx === -1 ? value : value.slice(idx + 1);
1416
+ }
1417
+ function normalizeModelId(modelId) {
1418
+ let out = modelId.trim().toLowerCase();
1419
+ while (out.startsWith("models/")) {
1420
+ out = out.slice("models/".length);
1421
+ }
1422
+ return out;
1423
+ }
1424
+ function parseContextWindow(model) {
1425
+ const limit = model.limit;
1426
+ if (!isRecord(limit))
1427
+ return null;
1428
+ const context = limit.context;
1429
+ if (typeof context !== "number" || !Number.isFinite(context))
1430
+ return null;
1431
+ return Math.max(0, Math.trunc(context));
1432
+ }
1433
+ function parseModelsDevCapabilities(payload, updatedAt) {
1434
+ if (!isRecord(payload))
1435
+ return [];
1436
+ const merged = new Map;
1437
+ for (const [provider, providerValue] of Object.entries(payload)) {
1438
+ if (!isRecord(providerValue))
1439
+ continue;
1440
+ const models = providerValue.models;
1441
+ if (!isRecord(models))
1442
+ continue;
1443
+ for (const [modelKey, modelValue] of Object.entries(models)) {
1444
+ if (!isRecord(modelValue))
1445
+ continue;
1446
+ const explicitId = typeof modelValue.id === "string" && modelValue.id.trim().length > 0 ? modelValue.id : modelKey;
1447
+ const canonicalModelId = normalizeModelId(explicitId);
1448
+ if (!canonicalModelId)
1449
+ continue;
1450
+ const contextWindow = parseContextWindow(modelValue);
1451
+ const reasoning = modelValue.reasoning === true;
1452
+ const rawJson = JSON.stringify(modelValue);
1453
+ const prev = merged.get(canonicalModelId);
1454
+ if (!prev) {
1455
+ merged.set(canonicalModelId, {
1456
+ canonicalModelId,
1457
+ contextWindow,
1458
+ reasoning,
1459
+ sourceProvider: provider,
1460
+ rawJson
1461
+ });
1462
+ continue;
1463
+ }
1464
+ merged.set(canonicalModelId, {
1465
+ canonicalModelId,
1466
+ contextWindow: prev.contextWindow ?? contextWindow,
1467
+ reasoning: prev.reasoning || reasoning,
1468
+ sourceProvider: prev.sourceProvider,
1469
+ rawJson: prev.rawJson ?? rawJson
1470
+ });
1471
+ }
1472
+ }
1473
+ return Array.from(merged.values()).map((entry) => ({
1474
+ canonical_model_id: entry.canonicalModelId,
1475
+ context_window: entry.contextWindow,
1476
+ reasoning: entry.reasoning ? 1 : 0,
1477
+ source_provider: entry.sourceProvider,
1478
+ raw_json: entry.rawJson,
1479
+ updated_at: updatedAt
1480
+ }));
1481
+ }
1482
+ function buildModelMatchIndex(canonicalModelIds) {
1483
+ const exact = new Map;
1484
+ const aliasCandidates = new Map;
1485
+ for (const rawCanonical of canonicalModelIds) {
1486
+ const canonical = normalizeModelId(rawCanonical);
1487
+ if (!canonical)
1488
+ continue;
1489
+ exact.set(canonical, canonical);
1490
+ const short = basename2(canonical);
1491
+ if (!short)
1492
+ continue;
1493
+ let set = aliasCandidates.get(short);
1494
+ if (!set) {
1495
+ set = new Set;
1496
+ aliasCandidates.set(short, set);
1497
+ }
1498
+ set.add(canonical);
1499
+ }
1500
+ const alias = new Map;
1501
+ for (const [short, candidates] of aliasCandidates) {
1502
+ if (candidates.size === 1) {
1503
+ for (const value of candidates) {
1504
+ alias.set(short, value);
1505
+ }
1506
+ } else {
1507
+ alias.set(short, null);
1508
+ }
1509
+ }
1510
+ return { exact, alias };
1511
+ }
1512
+ function matchCanonicalModelId(providerModelId, index) {
1513
+ const normalized = normalizeModelId(providerModelId);
1514
+ if (!normalized)
1515
+ return null;
1516
+ const exactMatch = index.exact.get(normalized);
1517
+ if (exactMatch)
1518
+ return exactMatch;
1519
+ const short = basename2(normalized);
1520
+ if (!short)
1521
+ return null;
1522
+ const alias = index.alias.get(short);
1523
+ return alias ?? null;
1524
+ }
1525
+ function isStaleTimestamp(timestamp, now = Date.now(), ttlMs = MODEL_INFO_TTL_MS) {
1526
+ if (timestamp === null)
1527
+ return true;
1528
+ return now - timestamp > ttlMs;
1529
+ }
1530
+ function buildRuntimeCache(capabilityRows, providerRows, stateRows) {
1531
+ const capabilitiesByCanonical = new Map;
1532
+ for (const row of capabilityRows) {
1533
+ const canonical = normalizeModelId(row.canonical_model_id);
1534
+ if (!canonical)
1535
+ continue;
1536
+ capabilitiesByCanonical.set(canonical, {
1537
+ canonicalModelId: canonical,
1538
+ contextWindow: row.context_window,
1539
+ reasoning: row.reasoning === 1,
1540
+ sourceProvider: row.source_provider
1541
+ });
1542
+ }
1543
+ const providerModelsByKey = new Map;
1544
+ const providerModelUniqIndex = new Map;
1545
+ for (const row of providerRows) {
1546
+ const provider = row.provider.trim().toLowerCase();
1547
+ const providerModelId = normalizeModelId(row.provider_model_id);
1548
+ if (!provider || !providerModelId)
1549
+ continue;
1550
+ const key = providerModelKey(provider, providerModelId);
1551
+ providerModelsByKey.set(key, {
1552
+ provider,
1553
+ providerModelId,
1554
+ displayName: row.display_name,
1555
+ canonicalModelId: row.canonical_model_id ? normalizeModelId(row.canonical_model_id) : null,
1556
+ contextWindow: row.context_window,
1557
+ free: row.free === 1
1558
+ });
1559
+ const prev = providerModelUniqIndex.get(providerModelId);
1560
+ if (prev === undefined) {
1561
+ providerModelUniqIndex.set(providerModelId, key);
1562
+ } else if (prev !== key) {
1563
+ providerModelUniqIndex.set(providerModelId, null);
1564
+ }
1565
+ }
1566
+ const matchIndex = buildModelMatchIndex(capabilitiesByCanonical.keys());
1567
+ const state = new Map;
1568
+ for (const row of stateRows) {
1569
+ state.set(row.key, row.value);
1570
+ }
1571
+ return {
1572
+ capabilitiesByCanonical,
1573
+ providerModelsByKey,
1574
+ providerModelUniqIndex,
1575
+ matchIndex,
1576
+ state
1577
+ };
1578
+ }
1579
+ function loadCacheFromDb() {
1580
+ runtimeCache = buildRuntimeCache(listModelCapabilities(), listProviderModels(), listModelInfoState());
1581
+ loaded = true;
1582
+ }
1583
+ function ensureLoaded() {
1584
+ if (!loaded)
1585
+ loadCacheFromDb();
1586
+ }
1587
+ function initModelInfoCache() {
1588
+ loadCacheFromDb();
1589
+ }
1590
+ function parseStateInt(key) {
1591
+ const raw = runtimeCache.state.get(key);
1592
+ if (!raw)
1593
+ return null;
1594
+ const value = Number.parseInt(raw, 10);
1595
+ if (!Number.isFinite(value))
1596
+ return null;
1597
+ return value;
1598
+ }
1599
+ function getRemoteProvidersFromEnv(env) {
1600
+ const providers = [];
1601
+ if (env.OPENCODE_API_KEY)
1602
+ providers.push("zen");
1603
+ if (env.OPENAI_API_KEY)
1604
+ providers.push("openai");
1605
+ if (env.ANTHROPIC_API_KEY)
1606
+ providers.push("anthropic");
1607
+ if (env.GOOGLE_API_KEY ?? env.GEMINI_API_KEY)
1608
+ providers.push("google");
1609
+ return providers;
1610
+ }
1611
+ function getProvidersToRefreshFromEnv(env) {
1612
+ return [...getRemoteProvidersFromEnv(env), "ollama"];
1613
+ }
1614
+ function getVisibleProvidersForSnapshotFromEnv(env) {
1615
+ return new Set(getProvidersToRefreshFromEnv(env));
1616
+ }
1617
+ function getConfiguredProvidersForSync() {
1618
+ return getProvidersToRefreshFromEnv(process.env);
1619
+ }
1620
+ function getProvidersRequiredForFreshness() {
1621
+ return getRemoteProvidersFromEnv(process.env);
1622
+ }
1623
+ function getProviderSyncKey(provider) {
1624
+ return `${PROVIDER_SYNC_KEY_PREFIX}${provider}`;
1625
+ }
1626
+ function isModelInfoStale(now = Date.now()) {
1627
+ ensureLoaded();
1628
+ if (isStaleTimestamp(parseStateInt(MODELS_DEV_SYNC_KEY), now))
1629
+ return true;
1630
+ for (const provider of getProvidersRequiredForFreshness()) {
1631
+ const providerSync = parseStateInt(getProviderSyncKey(provider));
1632
+ if (isStaleTimestamp(providerSync, now))
1633
+ return true;
1634
+ }
1635
+ return false;
1636
+ }
1637
+ function getLastSyncAt() {
1638
+ let latest = parseStateInt(MODELS_DEV_SYNC_KEY);
1639
+ for (const provider of getProvidersRequiredForFreshness()) {
1640
+ const value = parseStateInt(getProviderSyncKey(provider));
1641
+ if (value !== null && (latest === null || value > latest))
1642
+ latest = value;
1643
+ }
1644
+ return latest;
1645
+ }
1646
+ async function fetchJson(url, init, timeoutMs) {
1647
+ try {
1648
+ const response = await fetch(url, {
1649
+ ...init,
1650
+ signal: AbortSignal.timeout(timeoutMs)
1651
+ });
1652
+ if (!response.ok)
1653
+ return null;
1654
+ return await response.json();
1655
+ } catch {
1656
+ return null;
1657
+ }
1658
+ }
1659
+ async function fetchModelsDevPayload() {
1660
+ return fetchJson(MODELS_DEV_URL, {}, 1e4);
1661
+ }
1662
+ async function fetchZenModels() {
1663
+ const key = process.env.OPENCODE_API_KEY;
1664
+ if (!key)
1665
+ return null;
1666
+ const payload = await fetchJson(`${ZEN_BASE}/models`, { headers: { Authorization: `Bearer ${key}` } }, 8000);
1667
+ return processModelsList(payload, "data", "id", (item, modelId) => {
1668
+ const contextWindow = typeof item.context_window === "number" && Number.isFinite(item.context_window) ? Math.max(0, Math.trunc(item.context_window)) : null;
1669
+ return {
1670
+ providerModelId: modelId,
1671
+ displayName: item.id,
1672
+ contextWindow,
1673
+ free: item.id.endsWith("-free") || item.id === "gpt-5-nano" || item.id === "big-pickle"
1674
+ };
1675
+ });
1676
+ }
1677
+ function processModelsList(payload, arrayKey, idKey, mapper) {
1678
+ if (!isRecord(payload) || !Array.isArray(payload[arrayKey]))
1679
+ return null;
1680
+ const out = [];
1681
+ for (const item of payload[arrayKey]) {
1682
+ if (!isRecord(item) || typeof item[idKey] !== "string")
1683
+ continue;
1684
+ const modelId = normalizeModelId(item[idKey]);
1685
+ if (!modelId)
1686
+ continue;
1687
+ const mapped = mapper(item, modelId);
1688
+ if (mapped)
1689
+ out.push(mapped);
1690
+ }
1691
+ return out;
1692
+ }
1693
+ async function fetchOpenAIModels() {
1694
+ const key = process.env.OPENAI_API_KEY;
1695
+ if (!key)
1696
+ return null;
1697
+ const payload = await fetchJson(`${OPENAI_BASE}/v1/models`, { headers: { Authorization: `Bearer ${key}` } }, 6000);
1698
+ return processModelsList(payload, "data", "id", (item, modelId) => ({
1699
+ providerModelId: modelId,
1700
+ displayName: item.id,
1701
+ contextWindow: null,
1702
+ free: false
1703
+ }));
1704
+ }
1705
+ async function fetchAnthropicModels() {
1706
+ const key = process.env.ANTHROPIC_API_KEY;
1707
+ if (!key)
1708
+ return null;
1709
+ const payload = await fetchJson(`${ANTHROPIC_BASE}/v1/models`, {
1710
+ headers: {
1711
+ "x-api-key": key,
1712
+ "anthropic-version": "2023-06-01"
1713
+ }
1714
+ }, 6000);
1715
+ return processModelsList(payload, "data", "id", (item, modelId) => {
1716
+ const displayName = typeof item.display_name === "string" && item.display_name.trim().length > 0 ? item.display_name : item.id;
1717
+ return {
1718
+ providerModelId: modelId,
1719
+ displayName,
1720
+ contextWindow: null,
1721
+ free: false
1722
+ };
1723
+ });
1724
+ }
1725
+ async function fetchGoogleModels() {
1726
+ const key = process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
1727
+ if (!key)
1728
+ return null;
1729
+ const payload = await fetchJson(`${GOOGLE_BASE}/models?key=${encodeURIComponent(key)}`, {}, 6000);
1730
+ return processModelsList(payload, "models", "name", (item, modelId) => {
1731
+ const displayName = typeof item.displayName === "string" && item.displayName.trim().length > 0 ? item.displayName : modelId;
1732
+ const contextWindow = typeof item.inputTokenLimit === "number" && Number.isFinite(item.inputTokenLimit) ? Math.max(0, Math.trunc(item.inputTokenLimit)) : null;
1733
+ return {
1734
+ providerModelId: modelId,
1735
+ displayName,
1736
+ contextWindow,
1737
+ free: false
1738
+ };
1739
+ });
1740
+ }
1741
+ async function fetchOllamaModels() {
1742
+ const base = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
1743
+ const payload = await fetchJson(`${base}/api/tags`, {}, 3000);
1744
+ return processModelsList(payload, "models", "name", (item, modelId) => {
1745
+ const details = item.details;
1746
+ let sizeSuffix = "";
1747
+ if (isRecord(details) && typeof details.parameter_size === "string") {
1748
+ sizeSuffix = ` (${details.parameter_size})`;
1749
+ }
1750
+ return {
1751
+ providerModelId: modelId,
1752
+ displayName: `${item.name}${sizeSuffix}`,
1753
+ contextWindow: null,
1754
+ free: false
1755
+ };
1756
+ });
1757
+ }
1758
+ async function fetchProviderCandidates(provider) {
1759
+ switch (provider) {
1760
+ case "zen":
1761
+ return fetchZenModels();
1762
+ case "openai":
1763
+ return fetchOpenAIModels();
1764
+ case "anthropic":
1765
+ return fetchAnthropicModels();
1766
+ case "google":
1767
+ return fetchGoogleModels();
1768
+ case "ollama":
1769
+ return fetchOllamaModels();
1770
+ default:
1771
+ return null;
1772
+ }
1773
+ }
1774
+ function providerRowsFromCandidates(candidates, matchIndex, updatedAt) {
1775
+ return candidates.map((candidate) => ({
1776
+ provider_model_id: candidate.providerModelId,
1777
+ display_name: candidate.displayName,
1778
+ canonical_model_id: matchCanonicalModelId(candidate.providerModelId, matchIndex),
1779
+ context_window: candidate.contextWindow,
1780
+ free: candidate.free ? 1 : 0,
1781
+ updated_at: updatedAt
1782
+ }));
1783
+ }
1784
+ async function refreshModelInfoInternal() {
1785
+ ensureLoaded();
1786
+ const now = Date.now();
1787
+ const providers = getConfiguredProvidersForSync();
1788
+ const providerResults = await Promise.all(providers.map(async (provider) => ({
1789
+ provider,
1790
+ candidates: await fetchProviderCandidates(provider)
1791
+ })));
1792
+ const modelsDevPayload = await fetchModelsDevPayload();
1793
+ let matchIndex = runtimeCache.matchIndex;
1794
+ if (modelsDevPayload !== null) {
1795
+ const capabilityRows = parseModelsDevCapabilities(modelsDevPayload, now);
1796
+ if (capabilityRows.length > 0) {
1797
+ replaceModelCapabilities(capabilityRows);
1798
+ setModelInfoState(MODELS_DEV_SYNC_KEY, String(now));
1799
+ matchIndex = buildModelMatchIndex(capabilityRows.map((row) => row.canonical_model_id));
1800
+ }
1801
+ }
1802
+ for (const result of providerResults) {
1803
+ if (result.candidates === null)
1804
+ continue;
1805
+ const rows = providerRowsFromCandidates(result.candidates, matchIndex, now);
1806
+ replaceProviderModels(result.provider, rows);
1807
+ setModelInfoState(getProviderSyncKey(result.provider), String(now));
1808
+ }
1809
+ loadCacheFromDb();
1810
+ }
1811
+ function refreshModelInfoInBackground(opts) {
1812
+ ensureLoaded();
1813
+ const force = opts?.force ?? false;
1814
+ if (!force && !isModelInfoStale())
1815
+ return Promise.resolve();
1816
+ if (refreshInFlight)
1817
+ return refreshInFlight;
1818
+ refreshInFlight = refreshModelInfoInternal().finally(() => {
1819
+ refreshInFlight = null;
1820
+ });
1821
+ return refreshInFlight;
1822
+ }
1823
+ function isModelInfoRefreshing() {
1824
+ return refreshInFlight !== null;
1825
+ }
1826
+ function resolveFromProviderRow(row, cache) {
1827
+ if (row.canonicalModelId) {
1828
+ const capability = cache.capabilitiesByCanonical.get(row.canonicalModelId);
1829
+ if (capability) {
1830
+ return {
1831
+ canonicalModelId: capability.canonicalModelId,
1832
+ contextWindow: capability.contextWindow,
1833
+ reasoning: capability.reasoning
1834
+ };
1835
+ }
1836
+ }
1837
+ return {
1838
+ canonicalModelId: row.canonicalModelId,
1839
+ contextWindow: row.contextWindow,
1840
+ reasoning: false
1841
+ };
1842
+ }
1843
+ function resolveModelInfoInCache(modelString, cache) {
1844
+ const parsed = parseModelStringLoose(modelString);
1845
+ const normalizedModelId = normalizeModelId(parsed.modelId);
1846
+ if (!normalizedModelId)
1847
+ return null;
1848
+ if (parsed.provider) {
1849
+ const providerRow = cache.providerModelsByKey.get(providerModelKey(parsed.provider, normalizedModelId));
1850
+ if (providerRow)
1851
+ return resolveFromProviderRow(providerRow, cache);
1852
+ }
1853
+ const canonical = matchCanonicalModelId(normalizedModelId, cache.matchIndex);
1854
+ if (canonical) {
1855
+ const capability = cache.capabilitiesByCanonical.get(canonical);
1856
+ if (capability) {
1857
+ return {
1858
+ canonicalModelId: capability.canonicalModelId,
1859
+ contextWindow: capability.contextWindow,
1860
+ reasoning: capability.reasoning
1861
+ };
1862
+ }
1863
+ }
1864
+ if (!parsed.provider) {
1865
+ const uniqueProviderKey = cache.providerModelUniqIndex.get(normalizedModelId);
1866
+ if (uniqueProviderKey) {
1867
+ const providerRow = cache.providerModelsByKey.get(uniqueProviderKey);
1868
+ if (providerRow)
1869
+ return resolveFromProviderRow(providerRow, cache);
1870
+ }
1871
+ }
1872
+ return null;
1873
+ }
1874
+ function resolveModelInfo(modelString) {
1875
+ ensureLoaded();
1876
+ return resolveModelInfoInCache(modelString, runtimeCache);
1877
+ }
1878
+ function getContextWindow(modelString) {
1879
+ return resolveModelInfo(modelString)?.contextWindow ?? null;
1880
+ }
1881
+ function supportsThinking(modelString) {
1882
+ return resolveModelInfo(modelString)?.reasoning ?? false;
1883
+ }
1884
+ function readLiveModelsFromCache() {
1885
+ const models = [];
1886
+ const visibleProviders = getVisibleProvidersForSnapshotFromEnv(process.env);
1887
+ for (const row of runtimeCache.providerModelsByKey.values()) {
1888
+ if (!visibleProviders.has(row.provider))
1889
+ continue;
1890
+ const info = resolveFromProviderRow(row, runtimeCache);
1891
+ models.push({
1892
+ id: `${row.provider}/${row.providerModelId}`,
1893
+ displayName: row.displayName,
1894
+ provider: row.provider,
1895
+ context: info.contextWindow ?? undefined,
1896
+ free: row.free ? true : undefined
1897
+ });
1898
+ }
1899
+ models.sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
1900
+ return models;
1901
+ }
1902
+ async function fetchAvailableModelsSnapshot() {
1903
+ ensureLoaded();
1904
+ if (isModelInfoStale() && !isModelInfoRefreshing()) {
1905
+ if (runtimeCache.providerModelsByKey.size === 0) {
1906
+ await refreshModelInfoInBackground({ force: true });
1907
+ } else {
1908
+ refreshModelInfoInBackground();
1909
+ }
1910
+ }
1911
+ return {
1912
+ models: readLiveModelsFromCache(),
1913
+ stale: isModelInfoStale(),
1914
+ refreshing: isModelInfoRefreshing(),
1915
+ lastSyncAt: getLastSyncAt()
1916
+ };
1917
+ }
1918
+
1210
1919
  // src/llm-api/providers.ts
1211
1920
  function getFetchWithLogging() {
1212
1921
  const customFetch = async (input, init) => {
@@ -1233,7 +1942,7 @@ function getFetchWithLogging() {
1233
1942
  };
1234
1943
  return customFetch;
1235
1944
  }
1236
- var ZEN_BASE = "https://opencode.ai/zen/v1";
1945
+ var ZEN_BASE2 = "https://opencode.ai/zen/v1";
1237
1946
  function zenEndpointFor(modelId) {
1238
1947
  if (modelId.startsWith("claude-"))
1239
1948
  return zenAnthropic()(modelId);
@@ -1258,7 +1967,7 @@ function zenAnthropic() {
1258
1967
  _zenAnthropic = createAnthropic({
1259
1968
  fetch: getFetchWithLogging(),
1260
1969
  apiKey: getZenApiKey(),
1261
- baseURL: ZEN_BASE
1970
+ baseURL: ZEN_BASE2
1262
1971
  });
1263
1972
  }
1264
1973
  return _zenAnthropic;
@@ -1268,7 +1977,7 @@ function zenOpenAI() {
1268
1977
  _zenOpenAI = createOpenAI({
1269
1978
  fetch: getFetchWithLogging(),
1270
1979
  apiKey: getZenApiKey(),
1271
- baseURL: ZEN_BASE
1980
+ baseURL: ZEN_BASE2
1272
1981
  });
1273
1982
  }
1274
1983
  return _zenOpenAI;
@@ -1278,7 +1987,7 @@ function zenGoogle() {
1278
1987
  _zenGoogle = createGoogleGenerativeAI({
1279
1988
  fetch: getFetchWithLogging(),
1280
1989
  apiKey: getZenApiKey(),
1281
- baseURL: ZEN_BASE
1990
+ baseURL: ZEN_BASE2
1282
1991
  });
1283
1992
  }
1284
1993
  return _zenGoogle;
@@ -1289,7 +1998,7 @@ function zenCompat() {
1289
1998
  fetch: getFetchWithLogging(),
1290
1999
  name: "zen-compat",
1291
2000
  apiKey: getZenApiKey(),
1292
- baseURL: ZEN_BASE
2001
+ baseURL: ZEN_BASE2
1293
2002
  });
1294
2003
  }
1295
2004
  return _zenCompat;
@@ -1339,31 +2048,8 @@ function parseModelString(modelString) {
1339
2048
  modelId: modelString.slice(slashIdx + 1)
1340
2049
  };
1341
2050
  }
1342
- var CONTEXT_WINDOW_TABLE = [
1343
- [/^claude-/, 200000],
1344
- [/^gemini-/, 1e6],
1345
- [/^gpt-5/, 128000],
1346
- [/^gpt-4/, 128000],
1347
- [/^kimi-k2/, 262000],
1348
- [/^minimax-m2/, 196000],
1349
- [/^glm-/, 128000],
1350
- [/^qwen3-/, 131000]
1351
- ];
1352
- var REASONING_MODELS = [
1353
- /^claude-3-5-sonnet/,
1354
- /^claude-3-7/,
1355
- /^claude-sonnet-4/,
1356
- /^claude-opus-4/,
1357
- /^o1/,
1358
- /^o3/,
1359
- /^o4/,
1360
- /^gpt-5/,
1361
- /^gemini-2\.5/,
1362
- /^gemini-3/
1363
- ];
1364
- function supportsThinking(modelString) {
1365
- const { modelId } = parseModelString(modelString);
1366
- return REASONING_MODELS.some((p) => p.test(modelId));
2051
+ function supportsThinking2(modelString) {
2052
+ return supportsThinking(modelString);
1367
2053
  }
1368
2054
  var ANTHROPIC_BUDGET = {
1369
2055
  low: 4096,
@@ -1378,7 +2064,7 @@ function clampEffort(effort, max) {
1378
2064
  return ORDER[Math.min(i, m)];
1379
2065
  }
1380
2066
  function getThinkingProviderOptions(modelString, effort) {
1381
- if (!supportsThinking(modelString))
2067
+ if (!supportsThinking2(modelString))
1382
2068
  return null;
1383
2069
  const { provider, modelId } = parseModelString(modelString);
1384
2070
  if (provider === "anthropic" || provider === "zen" && modelId.startsWith("claude-")) {
@@ -1430,13 +2116,8 @@ function getThinkingProviderOptions(modelString, effort) {
1430
2116
  }
1431
2117
  return null;
1432
2118
  }
1433
- function getContextWindow(modelString) {
1434
- const { modelId } = parseModelString(modelString);
1435
- for (const [pattern, tokens] of CONTEXT_WINDOW_TABLE) {
1436
- if (pattern.test(modelId))
1437
- return tokens;
1438
- }
1439
- return null;
2119
+ function getContextWindow2(modelString) {
2120
+ return getContextWindow(modelString);
1440
2121
  }
1441
2122
  function resolveModel(modelString) {
1442
2123
  const slashIdx = modelString.indexOf("/");
@@ -1478,54 +2159,8 @@ function autoDiscoverModel() {
1478
2159
  return "google/gemini-2.0-flash";
1479
2160
  return "ollama/llama3.2";
1480
2161
  }
1481
- async function fetchZenModels() {
1482
- const key = process.env.OPENCODE_API_KEY;
1483
- if (!key)
1484
- return [];
1485
- try {
1486
- const res = await fetch(`${ZEN_BASE}/models`, {
1487
- headers: { Authorization: `Bearer ${key}` },
1488
- signal: AbortSignal.timeout(8000)
1489
- });
1490
- if (!res.ok)
1491
- return [];
1492
- const json = await res.json();
1493
- const models = json.data ?? [];
1494
- return models.map((m) => ({
1495
- id: `zen/${m.id}`,
1496
- displayName: m.id,
1497
- provider: "zen",
1498
- context: m.context_window,
1499
- free: m.id.endsWith("-free") || m.id === "gpt-5-nano" || m.id === "big-pickle"
1500
- }));
1501
- } catch {
1502
- return [];
1503
- }
1504
- }
1505
- async function fetchOllamaModels() {
1506
- const base = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
1507
- try {
1508
- const res = await fetch(`${base}/api/tags`, {
1509
- signal: AbortSignal.timeout(3000)
1510
- });
1511
- if (!res.ok)
1512
- return [];
1513
- const json = await res.json();
1514
- return (json.models ?? []).map((m) => ({
1515
- id: `ollama/${m.name}`,
1516
- displayName: m.name + (m.details?.parameter_size ? ` (${m.details.parameter_size})` : ""),
1517
- provider: "ollama"
1518
- }));
1519
- } catch {
1520
- return [];
1521
- }
1522
- }
1523
2162
  async function fetchAvailableModels() {
1524
- const [zen, ollama] = await Promise.all([
1525
- fetchZenModels(),
1526
- fetchOllamaModels()
1527
- ]);
1528
- return [...zen, ...ollama];
2163
+ return fetchAvailableModelsSnapshot();
1529
2164
  }
1530
2165
 
1531
2166
  // src/mcp/client.ts
@@ -1584,110 +2219,6 @@ async function connectMcpServer(config) {
1584
2219
  close: () => client.close()
1585
2220
  };
1586
2221
  }
1587
-
1588
- // src/session/db/connection.ts
1589
- import { Database } from "bun:sqlite";
1590
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, unlinkSync } from "fs";
1591
- import { homedir as homedir6 } from "os";
1592
- import { join as join4 } from "path";
1593
- function getConfigDir() {
1594
- return join4(homedir6(), ".config", "mini-coder");
1595
- }
1596
- function getDbPath() {
1597
- const dir = getConfigDir();
1598
- if (!existsSync2(dir))
1599
- mkdirSync3(dir, { recursive: true });
1600
- return join4(dir, "sessions.db");
1601
- }
1602
- var DB_VERSION = 3;
1603
- var SCHEMA = `
1604
- CREATE TABLE IF NOT EXISTS sessions (
1605
- id TEXT PRIMARY KEY,
1606
- title TEXT NOT NULL DEFAULT '',
1607
- cwd TEXT NOT NULL,
1608
- model TEXT NOT NULL,
1609
- created_at INTEGER NOT NULL,
1610
- updated_at INTEGER NOT NULL
1611
- );
1612
-
1613
- CREATE TABLE IF NOT EXISTS messages (
1614
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1615
- session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
1616
- payload TEXT NOT NULL,
1617
- turn_index INTEGER NOT NULL DEFAULT 0,
1618
- created_at INTEGER NOT NULL
1619
- );
1620
-
1621
- CREATE TABLE IF NOT EXISTS prompt_history (
1622
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1623
- text TEXT NOT NULL,
1624
- created_at INTEGER NOT NULL
1625
- );
1626
-
1627
- CREATE TABLE IF NOT EXISTS mcp_servers (
1628
- name TEXT PRIMARY KEY,
1629
- transport TEXT NOT NULL,
1630
- url TEXT,
1631
- command TEXT,
1632
- args TEXT,
1633
- env TEXT,
1634
- created_at INTEGER NOT NULL
1635
- );
1636
-
1637
- CREATE INDEX IF NOT EXISTS idx_messages_session
1638
- ON messages(session_id, id);
1639
-
1640
- CREATE INDEX IF NOT EXISTS idx_messages_turn
1641
- ON messages(session_id, turn_index);
1642
-
1643
- CREATE INDEX IF NOT EXISTS idx_sessions_updated
1644
- ON sessions(updated_at DESC);
1645
-
1646
- CREATE TABLE IF NOT EXISTS settings (
1647
- key TEXT PRIMARY KEY,
1648
- value TEXT NOT NULL
1649
- );
1650
-
1651
- CREATE TABLE IF NOT EXISTS snapshots (
1652
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1653
- session_id TEXT NOT NULL,
1654
- turn_index INTEGER NOT NULL,
1655
- path TEXT NOT NULL,
1656
- content BLOB,
1657
- existed INTEGER NOT NULL
1658
- );
1659
-
1660
- CREATE INDEX IF NOT EXISTS idx_snapshots_turn
1661
- ON snapshots(session_id, turn_index);
1662
- `;
1663
- var _db = null;
1664
- function getDb() {
1665
- if (!_db) {
1666
- const dbPath = getDbPath();
1667
- let db = new Database(dbPath, { create: true });
1668
- db.exec("PRAGMA journal_mode=WAL;");
1669
- db.exec("PRAGMA foreign_keys=ON;");
1670
- const version = db.query("PRAGMA user_version").get()?.user_version ?? 0;
1671
- if (version !== DB_VERSION) {
1672
- try {
1673
- db.close();
1674
- } catch {}
1675
- for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
1676
- if (existsSync2(path))
1677
- unlinkSync(path);
1678
- }
1679
- db = new Database(dbPath, { create: true });
1680
- db.exec("PRAGMA journal_mode=WAL;");
1681
- db.exec("PRAGMA foreign_keys=ON;");
1682
- db.exec(SCHEMA);
1683
- db.exec(`PRAGMA user_version = ${DB_VERSION};`);
1684
- } else {
1685
- db.exec(SCHEMA);
1686
- }
1687
- _db = db;
1688
- }
1689
- return _db;
1690
- }
1691
2222
  // src/session/db/session-repo.ts
1692
2223
  function createSession(opts) {
1693
2224
  const db = getDb();
@@ -1848,29 +2379,24 @@ function deleteSnapshot(sessionId, turnIndex) {
1848
2379
  function deleteAllSnapshots(sessionId) {
1849
2380
  getDb().run("DELETE FROM snapshots WHERE session_id = ?", [sessionId]);
1850
2381
  }
2382
+ // src/agent/subagent-runner.ts
2383
+ import { tmpdir as tmpdir2 } from "os";
2384
+ import { join as join10 } from "path";
2385
+
1851
2386
  // src/llm-api/turn.ts
1852
2387
  import { dynamicTool, jsonSchema, stepCountIs, streamText } from "ai";
1853
- import { z } from "zod";
1854
2388
  var MAX_STEPS = 50;
1855
2389
  function isZodSchema(s) {
1856
2390
  return s !== null && typeof s === "object" && "_def" in s;
1857
2391
  }
1858
- function toCoreTool(def, claimWarning) {
2392
+ function toCoreTool(def) {
1859
2393
  const schema = isZodSchema(def.schema) ? def.schema : jsonSchema(def.schema);
1860
2394
  return dynamicTool({
1861
2395
  description: def.description,
1862
2396
  inputSchema: schema,
1863
2397
  execute: async (input) => {
1864
2398
  try {
1865
- const result = await def.execute(input);
1866
- if (claimWarning()) {
1867
- const warning = `
1868
-
1869
- <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>";
1870
- const str = typeof result === "string" ? result : JSON.stringify(result);
1871
- return str + warning;
1872
- }
1873
- return result;
2399
+ return await def.execute(input);
1874
2400
  } catch (err) {
1875
2401
  throw err instanceof Error ? err : new Error(String(err));
1876
2402
  }
@@ -1892,16 +2418,9 @@ async function* runTurn(options) {
1892
2418
  thinkingEffort
1893
2419
  } = options;
1894
2420
  let stepCount = 0;
1895
- let warningClaimed = false;
1896
- function claimWarning() {
1897
- if (stepCount !== MAX_STEPS - 2 || warningClaimed)
1898
- return false;
1899
- warningClaimed = true;
1900
- return true;
1901
- }
1902
2421
  const toolSet = {};
1903
2422
  for (const def of tools) {
1904
- toolSet[def.name] = toCoreTool(def, claimWarning);
2423
+ toolSet[def.name] = toCoreTool(def);
1905
2424
  }
1906
2425
  let inputTokens = 0;
1907
2426
  let outputTokens = 0;
@@ -1936,7 +2455,6 @@ async function* runTurn(options) {
1936
2455
  outputTokens += step.usage?.outputTokens ?? 0;
1937
2456
  contextTokens = step.usage?.inputTokens ?? contextTokens;
1938
2457
  stepCount++;
1939
- warningClaimed = false;
1940
2458
  },
1941
2459
  prepareStep: ({ stepNumber }) => {
1942
2460
  if (stepNumber >= MAX_STEPS - 1) {
@@ -1992,7 +2510,7 @@ async function* runTurn(options) {
1992
2510
  };
1993
2511
  break;
1994
2512
  }
1995
- case "tool-error": {
2513
+ case "tool-error":
1996
2514
  yield {
1997
2515
  type: "tool-result",
1998
2516
  toolCallId: String(c9.toolCallId ?? ""),
@@ -2001,15 +2519,13 @@ async function* runTurn(options) {
2001
2519
  isError: true
2002
2520
  };
2003
2521
  break;
2004
- }
2005
2522
  case "error": {
2006
2523
  const err = c9.error;
2007
2524
  throw err instanceof Error ? err : new Error(String(err));
2008
2525
  }
2009
2526
  }
2010
2527
  }
2011
- const finalResponse = await result.response;
2012
- const newMessages = finalResponse?.messages ?? [];
2528
+ const newMessages = (await result.response)?.messages ?? [];
2013
2529
  logApiEvent("turn complete", {
2014
2530
  newMessagesCount: newMessages.length,
2015
2531
  inputTokens,
@@ -2031,17 +2547,205 @@ async function* runTurn(options) {
2031
2547
  }
2032
2548
  }
2033
2549
 
2550
+ // src/tools/worktree.ts
2551
+ import {
2552
+ chmodSync,
2553
+ copyFileSync,
2554
+ existsSync as existsSync3,
2555
+ lstatSync,
2556
+ mkdirSync as mkdirSync4,
2557
+ mkdtempSync,
2558
+ readlinkSync,
2559
+ rmSync,
2560
+ symlinkSync,
2561
+ writeFileSync
2562
+ } from "fs";
2563
+ import { tmpdir } from "os";
2564
+ import { dirname, join as join5 } from "path";
2565
+ async function runGit(cwd, args) {
2566
+ try {
2567
+ const proc = Bun.spawn(["git", ...args], {
2568
+ cwd,
2569
+ stdout: "pipe",
2570
+ stderr: "pipe"
2571
+ });
2572
+ const [stdout, stderr, exitCode] = await Promise.all([
2573
+ new Response(proc.stdout).text(),
2574
+ new Response(proc.stderr).text(),
2575
+ proc.exited
2576
+ ]);
2577
+ return { stdout, stderr, exitCode };
2578
+ } catch {
2579
+ return { stdout: "", stderr: "failed to execute git", exitCode: -1 };
2580
+ }
2581
+ }
2582
+ function gitError(action, detail) {
2583
+ return new Error(`${action}: ${detail || "unknown git error"}`);
2584
+ }
2585
+ function splitNonEmptyLines(text) {
2586
+ return text.split(`
2587
+ `).map((line) => line.trim()).filter((line) => line.length > 0);
2588
+ }
2589
+ async function listUnmergedFiles(cwd) {
2590
+ const conflictResult = await runGit(cwd, [
2591
+ "diff",
2592
+ "--name-only",
2593
+ "--diff-filter=U"
2594
+ ]);
2595
+ if (conflictResult.exitCode !== 0)
2596
+ return [];
2597
+ return splitNonEmptyLines(conflictResult.stdout);
2598
+ }
2599
+ async function hasMergeInProgress(cwd) {
2600
+ const mergeHead = await runGit(cwd, [
2601
+ "rev-parse",
2602
+ "-q",
2603
+ "--verify",
2604
+ "MERGE_HEAD"
2605
+ ]);
2606
+ return mergeHead.exitCode === 0;
2607
+ }
2608
+ async function isGitRepo(cwd) {
2609
+ const result = await runGit(cwd, ["rev-parse", "--git-dir"]);
2610
+ return result.exitCode === 0;
2611
+ }
2612
+ function splitNullSeparated(text) {
2613
+ return text.split("\x00").filter((value) => value.length > 0);
2614
+ }
2615
+ async function getRepoRoot(cwd) {
2616
+ const result = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
2617
+ if (result.exitCode !== 0) {
2618
+ throw gitError("Failed to resolve repository root", (result.stderr || result.stdout).trim());
2619
+ }
2620
+ return result.stdout.trim();
2621
+ }
2622
+ async function applyPatch(cwd, patch, args) {
2623
+ if (patch.trim().length === 0)
2624
+ return;
2625
+ const tempDir = mkdtempSync(join5(tmpdir(), "mc-worktree-patch-"));
2626
+ const patchPath = join5(tempDir, "changes.patch");
2627
+ try {
2628
+ writeFileSync(patchPath, patch);
2629
+ const result = await runGit(cwd, ["apply", ...args, patchPath]);
2630
+ if (result.exitCode !== 0) {
2631
+ throw gitError("Failed to apply dirty-state patch to worktree", (result.stderr || result.stdout).trim());
2632
+ }
2633
+ } finally {
2634
+ rmSync(tempDir, { recursive: true, force: true });
2635
+ }
2636
+ }
2637
+ function copyUntrackedPath(source, destination) {
2638
+ const stat = lstatSync(source);
2639
+ mkdirSync4(dirname(destination), { recursive: true });
2640
+ if (stat.isSymbolicLink()) {
2641
+ rmSync(destination, { recursive: true, force: true });
2642
+ symlinkSync(readlinkSync(source), destination);
2643
+ return;
2644
+ }
2645
+ copyFileSync(source, destination);
2646
+ chmodSync(destination, stat.mode);
2647
+ }
2648
+ function copyFileIfMissing(source, destination) {
2649
+ if (!existsSync3(source) || existsSync3(destination))
2650
+ return;
2651
+ mkdirSync4(dirname(destination), { recursive: true });
2652
+ copyFileSync(source, destination);
2653
+ }
2654
+ function linkDirectoryIfMissing(source, destination) {
2655
+ if (!existsSync3(source) || existsSync3(destination))
2656
+ return;
2657
+ mkdirSync4(dirname(destination), { recursive: true });
2658
+ symlinkSync(source, destination, process.platform === "win32" ? "junction" : "dir");
2659
+ }
2660
+ async function initializeWorktree(mainCwd, worktreeCwd) {
2661
+ const [mainRoot, worktreeRoot] = await Promise.all([
2662
+ getRepoRoot(mainCwd),
2663
+ getRepoRoot(worktreeCwd)
2664
+ ]);
2665
+ if (!existsSync3(join5(mainRoot, "package.json")))
2666
+ return;
2667
+ for (const lockfile of ["bun.lock", "bun.lockb"]) {
2668
+ copyFileIfMissing(join5(mainRoot, lockfile), join5(worktreeRoot, lockfile));
2669
+ }
2670
+ linkDirectoryIfMissing(join5(mainRoot, "node_modules"), join5(worktreeRoot, "node_modules"));
2671
+ }
2672
+ async function syncDirtyStateToWorktree(mainCwd, worktreeCwd) {
2673
+ const [staged, unstaged, untracked, mainRoot, worktreeRoot] = await Promise.all([
2674
+ runGit(mainCwd, ["diff", "--binary", "--cached"]),
2675
+ runGit(mainCwd, ["diff", "--binary"]),
2676
+ runGit(mainCwd, ["ls-files", "--others", "--exclude-standard", "-z"]),
2677
+ getRepoRoot(mainCwd),
2678
+ getRepoRoot(worktreeCwd)
2679
+ ]);
2680
+ if (staged.exitCode !== 0) {
2681
+ throw gitError("Failed to read staged changes", (staged.stderr || staged.stdout).trim());
2682
+ }
2683
+ if (unstaged.exitCode !== 0) {
2684
+ throw gitError("Failed to read unstaged changes", (unstaged.stderr || unstaged.stdout).trim());
2685
+ }
2686
+ if (untracked.exitCode !== 0) {
2687
+ throw gitError("Failed to list untracked files", (untracked.stderr || untracked.stdout).trim());
2688
+ }
2689
+ await applyPatch(worktreeRoot, staged.stdout, ["--index"]);
2690
+ await applyPatch(worktreeRoot, unstaged.stdout, []);
2691
+ for (const relPath of splitNullSeparated(untracked.stdout)) {
2692
+ copyUntrackedPath(join5(mainRoot, relPath), join5(worktreeRoot, relPath));
2693
+ }
2694
+ }
2695
+ async function createWorktree(mainCwd, branch, path) {
2696
+ const result = await runGit(mainCwd, ["worktree", "add", path, "-b", branch]);
2697
+ if (result.exitCode !== 0) {
2698
+ throw gitError(`Failed to create worktree for branch "${branch}"`, (result.stderr || result.stdout).trim());
2699
+ }
2700
+ return path;
2701
+ }
2702
+
2703
+ class MergeInProgressError extends Error {
2704
+ conflictFiles;
2705
+ constructor(branch, conflictFiles) {
2706
+ super(`Cannot merge branch "${branch}" because another merge is already in progress. Resolve it first before merging this branch.`);
2707
+ this.name = "MergeInProgressError";
2708
+ this.conflictFiles = conflictFiles;
2709
+ }
2710
+ }
2711
+ async function mergeWorktree(mainCwd, branch) {
2712
+ if (await hasMergeInProgress(mainCwd)) {
2713
+ const conflictFiles2 = await listUnmergedFiles(mainCwd);
2714
+ throw new MergeInProgressError(branch, conflictFiles2);
2715
+ }
2716
+ const merge = await runGit(mainCwd, ["merge", "--no-ff", branch]);
2717
+ if (merge.exitCode === 0)
2718
+ return { success: true };
2719
+ const conflictFiles = await listUnmergedFiles(mainCwd);
2720
+ if (conflictFiles.length > 0) {
2721
+ return { success: false, conflictFiles };
2722
+ }
2723
+ throw gitError(`Failed to merge branch "${branch}"`, (merge.stderr || merge.stdout).trim());
2724
+ }
2725
+ async function removeWorktree(mainCwd, path) {
2726
+ const result = await runGit(mainCwd, ["worktree", "remove", "--force", path]);
2727
+ if (result.exitCode !== 0) {
2728
+ throw gitError(`Failed to remove worktree "${path}"`, (result.stderr || result.stdout).trim());
2729
+ }
2730
+ }
2731
+ async function cleanupBranch(mainCwd, branch) {
2732
+ const result = await runGit(mainCwd, ["branch", "-D", branch]);
2733
+ if (result.exitCode !== 0) {
2734
+ throw gitError(`Failed to delete branch "${branch}"`, (result.stderr || result.stdout).trim());
2735
+ }
2736
+ }
2737
+
2034
2738
  // src/agent/system-prompt.ts
2035
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2036
- import { join as join5 } from "path";
2739
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
2740
+ import { join as join6 } from "path";
2037
2741
  function loadContextFile(cwd) {
2038
2742
  const candidates = [
2039
- join5(cwd, "AGENTS.md"),
2040
- join5(cwd, "CLAUDE.md"),
2041
- join5(getConfigDir(), "AGENTS.md")
2743
+ join6(cwd, "AGENTS.md"),
2744
+ join6(cwd, "CLAUDE.md"),
2745
+ join6(getConfigDir(), "AGENTS.md")
2042
2746
  ];
2043
2747
  for (const p of candidates) {
2044
- if (existsSync3(p)) {
2748
+ if (existsSync4(p)) {
2045
2749
  try {
2046
2750
  return readFileSync2(p, "utf-8");
2047
2751
  } catch {}
@@ -2075,9 +2779,8 @@ Guidelines:
2075
2779
  - Be concise and precise. Avoid unnecessary preamble.
2076
2780
  - Prefer small, targeted edits over large rewrites.
2077
2781
  - Always read a file before editing it.
2078
- - Use glob to discover files, grep to find patterns, read to inspect contents.
2079
- - Use shell for tests, builds, and git operations.
2080
- - Use subagents for parallel execution and handling large subtasks to protect your context limit.`;
2782
+ - Use subagents for all tasks that require a lot of context, like searching code, the web or using shell commands that produce a lot of output.
2783
+ - Keep your context clean and focused on the user request, use subagents to achieve this.`;
2081
2784
  if (modelString && isCodexModel(modelString)) {
2082
2785
  prompt += CODEX_AUTONOMY;
2083
2786
  }
@@ -2092,73 +2795,108 @@ ${contextFile}`;
2092
2795
  }
2093
2796
 
2094
2797
  // src/tools/exa.ts
2095
- import { z as z2 } from "zod";
2096
- var ExaSearchSchema = z2.object({
2097
- query: z2.string().describe("The search query")
2798
+ import { z } from "zod";
2799
+ var ExaSearchSchema = z.object({
2800
+ query: z.string().describe("The search query")
2098
2801
  });
2802
+ async function fetchExa(endpoint, body) {
2803
+ const apiKey = process.env.EXA_API_KEY;
2804
+ if (!apiKey) {
2805
+ throw new Error("EXA_API_KEY is not set.");
2806
+ }
2807
+ const response = await fetch(`https://api.exa.ai/${endpoint}`, {
2808
+ method: "POST",
2809
+ headers: {
2810
+ "Content-Type": "application/json",
2811
+ "x-api-key": apiKey
2812
+ },
2813
+ body: JSON.stringify(body)
2814
+ });
2815
+ if (!response.ok) {
2816
+ const errorBody = await response.text();
2817
+ throw new Error(`Exa API error: ${response.status} ${response.statusText} - ${errorBody}`);
2818
+ }
2819
+ return await response.json();
2820
+ }
2099
2821
  var webSearchTool = {
2100
2822
  name: "webSearch",
2101
2823
  description: "Search the web for a query using Exa.",
2102
2824
  schema: ExaSearchSchema,
2103
2825
  execute: async (input) => {
2104
- const apiKey = process.env.EXA_API_KEY;
2105
- if (!apiKey) {
2106
- throw new Error("EXA_API_KEY is not set.");
2107
- }
2108
- const response = await fetch("https://api.exa.ai/search", {
2109
- method: "POST",
2110
- headers: {
2111
- "Content-Type": "application/json",
2112
- "x-api-key": apiKey
2113
- },
2114
- body: JSON.stringify({
2115
- query: input.query,
2116
- type: "auto",
2117
- numResults: 10,
2118
- contents: { text: { maxCharacters: 4000 } }
2119
- })
2826
+ return fetchExa("search", {
2827
+ query: input.query,
2828
+ type: "auto",
2829
+ numResults: 10,
2830
+ contents: { text: { maxCharacters: 4000 } }
2120
2831
  });
2121
- if (!response.ok) {
2122
- const errorBody = await response.text();
2123
- throw new Error(`Exa API error: ${response.status} ${response.statusText} - ${errorBody}`);
2124
- }
2125
- return await response.json();
2126
2832
  }
2127
2833
  };
2128
- var ExaContentSchema = z2.object({
2129
- urls: z2.array(z2.string()).max(3).describe("Array of URLs to retrieve content for (max 3)")
2834
+ var ExaContentSchema = z.object({
2835
+ urls: z.array(z.string()).max(3).describe("Array of URLs to retrieve content for (max 3)")
2130
2836
  });
2131
2837
  var webContentTool = {
2132
2838
  name: "webContent",
2133
2839
  description: "Get the full content of specific URLs using Exa.",
2134
2840
  schema: ExaContentSchema,
2135
2841
  execute: async (input) => {
2136
- const apiKey = process.env.EXA_API_KEY;
2137
- if (!apiKey) {
2138
- throw new Error("EXA_API_KEY is not set.");
2139
- }
2140
- const response = await fetch("https://api.exa.ai/contents", {
2141
- method: "POST",
2142
- headers: {
2143
- "Content-Type": "application/json",
2144
- "x-api-key": apiKey
2145
- },
2146
- body: JSON.stringify({
2147
- urls: input.urls,
2148
- text: { maxCharacters: 1e4 }
2149
- })
2842
+ return fetchExa("contents", {
2843
+ urls: input.urls,
2844
+ text: { maxCharacters: 1e4 }
2150
2845
  });
2151
- if (!response.ok) {
2152
- const errorBody = await response.text();
2153
- throw new Error(`Exa API error: ${response.status} ${response.statusText} - ${errorBody}`);
2154
- }
2155
- return await response.json();
2156
2846
  }
2157
2847
  };
2158
2848
 
2849
+ // src/tools/subagent.ts
2850
+ import { z as z2 } from "zod";
2851
+ var SubagentInput = z2.object({
2852
+ prompt: z2.string().describe("The task or question to give the subagent"),
2853
+ agentName: z2.string().optional().describe("Name of a custom agent to use (from .agents/agents/). Omit to use a generic subagent.")
2854
+ });
2855
+ function formatConflictFiles(conflictFiles) {
2856
+ if (conflictFiles.length === 0)
2857
+ return " - (unknown)";
2858
+ return conflictFiles.map((file) => ` - ${file}`).join(`
2859
+ `);
2860
+ }
2861
+ function getSubagentMergeError(output) {
2862
+ if (output.mergeConflict) {
2863
+ const files = formatConflictFiles(output.mergeConflict.conflictFiles);
2864
+ return `\u26A0 Merge conflict: subagent branch "${output.mergeConflict.branch}" has been merged into your working tree
2865
+ but has conflicts in these files:
2866
+ ${files}
2867
+
2868
+ Resolve the conflicts (remove <<<<, ====, >>>> markers), stage the files with
2869
+ \`git add <file>\`, then run \`git merge --continue\` to complete the merge.`;
2870
+ }
2871
+ if (output.mergeBlocked) {
2872
+ const files = formatConflictFiles(output.mergeBlocked.conflictFiles);
2873
+ return `\u26A0 Merge deferred: subagent branch "${output.mergeBlocked.branch}" was not merged because another merge is already in progress.
2874
+ Current unresolved files:
2875
+ ${files}
2876
+
2877
+ Resolve the current merge first (remove <<<<, ====, >>>> markers), stage files with
2878
+ \`git add <file>\`, run \`git merge --continue\`, then merge the deferred branch with
2879
+ \`git merge --no-ff ${output.mergeBlocked.branch}\`.`;
2880
+ }
2881
+ return null;
2882
+ }
2883
+ function createSubagentTool(runSubagent, availableAgents, parentLabel) {
2884
+ const agentSection = availableAgents.size > 0 ? `
2885
+
2886
+ 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(", ")}.` : "";
2887
+ return {
2888
+ name: "subagent",
2889
+ 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. ${agentSection}`,
2890
+ schema: SubagentInput,
2891
+ execute: async (input) => {
2892
+ return runSubagent(input.prompt, input.agentName, parentLabel);
2893
+ }
2894
+ };
2895
+ }
2896
+
2159
2897
  // src/tools/create.ts
2160
- import { existsSync as existsSync4, mkdirSync as mkdirSync4 } from "fs";
2161
- import { dirname, join as join6, relative } from "path";
2898
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
2899
+ import { dirname as dirname2 } from "path";
2162
2900
  import { z as z3 } from "zod";
2163
2901
 
2164
2902
  // src/tools/diff.ts
@@ -2287,6 +3025,39 @@ ${lines.join(`
2287
3025
  `);
2288
3026
  }
2289
3027
 
3028
+ // src/tools/shared.ts
3029
+ import { join as join7, relative } from "path";
3030
+ function resolvePath(cwdInput, pathInput) {
3031
+ const cwd = cwdInput ?? process.cwd();
3032
+ const filePath = pathInput.startsWith("/") ? pathInput : join7(cwd, pathInput);
3033
+ const relPath = relative(cwd, filePath);
3034
+ return { cwd, filePath, relPath };
3035
+ }
3036
+ async function resolveExistingFile(cwdInput, pathInput) {
3037
+ const { cwd, filePath, relPath } = resolvePath(cwdInput, pathInput);
3038
+ const file = Bun.file(filePath);
3039
+ if (!await file.exists()) {
3040
+ throw new Error(`File not found: "${relPath}". To create a new file use the \`create\` tool.`);
3041
+ }
3042
+ return { file, filePath, relPath, cwd };
3043
+ }
3044
+ function parseAnchor(value, name = "anchor") {
3045
+ const normalized = value.trim().endsWith("|") ? value.trim().slice(0, -1) : value;
3046
+ const match = /^\s*(\d+):([0-9a-fA-F]{2})\s*$/.exec(normalized);
3047
+ if (!match) {
3048
+ throw new Error(`Invalid ${name}. Expected format: "line:hh" (e.g. "11:a3").`);
3049
+ }
3050
+ const line = Number(match[1]);
3051
+ if (!Number.isInteger(line) || line < 1) {
3052
+ throw new Error(`Invalid ${name} line number.`);
3053
+ }
3054
+ const hash = match[2];
3055
+ if (!hash) {
3056
+ throw new Error(`Invalid ${name}. Expected format: "line:hh" (e.g. "11:a3").`);
3057
+ }
3058
+ return { line, hash: hash.toLowerCase() };
3059
+ }
3060
+
2290
3061
  // src/tools/create.ts
2291
3062
  var CreateSchema = z3.object({
2292
3063
  path: z3.string().describe("File path to write (absolute or relative to cwd)"),
@@ -2297,12 +3068,10 @@ var createTool = {
2297
3068
  description: "Create a new file or fully overwrite an existing file with the given content. " + "Use this only for new files. " + "For targeted line edits on existing files, **use `replace` or `insert` instead**.",
2298
3069
  schema: CreateSchema,
2299
3070
  execute: async (input) => {
2300
- const cwd = input.cwd ?? process.cwd();
2301
- const filePath = input.path.startsWith("/") ? input.path : join6(cwd, input.path);
2302
- const relPath = relative(cwd, filePath);
2303
- const dir = dirname(filePath);
2304
- if (!existsSync4(dir))
2305
- mkdirSync4(dir, { recursive: true });
3071
+ const { filePath, relPath } = resolvePath(input.cwd, input.path);
3072
+ const dir = dirname2(filePath);
3073
+ if (!existsSync5(dir))
3074
+ mkdirSync5(dir, { recursive: true });
2306
3075
  const file = Bun.file(filePath);
2307
3076
  const created = !await file.exists();
2308
3077
  const before = created ? "" : await file.text();
@@ -2313,21 +3082,51 @@ var createTool = {
2313
3082
  };
2314
3083
 
2315
3084
  // src/tools/glob.ts
2316
- import { join as join8, relative as relative2 } from "path";
3085
+ import { resolve as resolve2 } from "path";
2317
3086
  import { z as z4 } from "zod";
2318
3087
 
2319
3088
  // src/tools/ignore.ts
2320
- import { join as join7 } from "path";
3089
+ import { join as join8 } from "path";
2321
3090
  import ignore from "ignore";
2322
3091
  async function loadGitignore(cwd) {
2323
3092
  try {
2324
- const gitignore = await Bun.file(join7(cwd, ".gitignore")).text();
3093
+ const gitignore = await Bun.file(join8(cwd, ".gitignore")).text();
2325
3094
  return ignore().add(gitignore);
2326
3095
  } catch {
2327
3096
  return null;
2328
3097
  }
2329
3098
  }
2330
3099
 
3100
+ // src/tools/scan-path.ts
3101
+ import { relative as relative2, resolve, sep } from "path";
3102
+ var LEADING_PARENT_SEGMENTS = /^(?:\.\.\/)+/;
3103
+ function getScannedPathInfo(cwd, scanPath) {
3104
+ const cwdAbsolute = resolve(cwd);
3105
+ const absolute = resolve(cwdAbsolute, scanPath);
3106
+ const relativePath = relative2(cwdAbsolute, absolute).replaceAll("\\", "/");
3107
+ const inCwd = absolute === cwdAbsolute || absolute.startsWith(cwdAbsolute === sep ? sep : `${cwdAbsolute}${sep}`);
3108
+ const ignoreTargets = getIgnoreTargets(relativePath, inCwd);
3109
+ return {
3110
+ absolute,
3111
+ relativePath,
3112
+ ignoreTargets,
3113
+ inCwd
3114
+ };
3115
+ }
3116
+ function getIgnoreTargets(path, inCwd) {
3117
+ if (inCwd)
3118
+ return [path];
3119
+ const normalized = path.replace(LEADING_PARENT_SEGMENTS, "");
3120
+ const segments = normalized.split("/").filter(Boolean);
3121
+ if (segments.length === 0)
3122
+ return [normalized];
3123
+ const targets = new Set([normalized]);
3124
+ for (let i = 1;i < segments.length; i++) {
3125
+ targets.add(segments.slice(i).join("/"));
3126
+ }
3127
+ return [...targets];
3128
+ }
3129
+
2331
3130
  // src/tools/glob.ts
2332
3131
  var GlobSchema = z4.object({
2333
3132
  pattern: z4.string().describe("Glob pattern to match files against, e.g. '**/*.ts'"),
@@ -2347,18 +3146,22 @@ var globTool = {
2347
3146
  const glob = new Bun.Glob(input.pattern);
2348
3147
  const matches = [];
2349
3148
  for await (const file of glob.scan({ cwd, onlyFiles: true, dot: true })) {
2350
- if (ig?.ignores(file))
3149
+ const { relativePath, ignoreTargets } = getScannedPathInfo(cwd, file);
3150
+ const firstSegment = relativePath.split("/")[0] ?? "";
3151
+ if (ignoreTargets.some((path) => ig?.ignores(path)))
2351
3152
  continue;
2352
- const firstSegment = file.split("/")[0] ?? "";
2353
- const ignored = ignoreGlobs.some((g) => g.match(file) || g.match(firstSegment));
3153
+ const ignored = ignoreTargets.some((candidate) => ignoreGlobs.some((g) => g.match(candidate) || g.match(firstSegment)));
2354
3154
  if (ignored)
2355
3155
  continue;
2356
3156
  try {
2357
- const fullPath = join8(cwd, file);
3157
+ const fullPath = resolve2(cwd, relativePath);
2358
3158
  const stat = await Bun.file(fullPath).stat?.() ?? null;
2359
- matches.push({ path: file, mtime: stat?.mtime?.getTime() ?? 0 });
3159
+ matches.push({
3160
+ path: relativePath,
3161
+ mtime: stat?.mtime?.getTime() ?? 0
3162
+ });
2360
3163
  } catch {
2361
- matches.push({ path: file, mtime: 0 });
3164
+ matches.push({ path: relativePath, mtime: 0 });
2362
3165
  }
2363
3166
  if (matches.length >= MAX_RESULTS + 1)
2364
3167
  break;
@@ -2367,13 +3170,12 @@ var globTool = {
2367
3170
  if (truncated)
2368
3171
  matches.pop();
2369
3172
  matches.sort((a, b) => b.mtime - a.mtime);
2370
- const files = matches.map((m) => relative2(cwd, join8(cwd, m.path)));
3173
+ const files = matches.map((m) => m.path);
2371
3174
  return { files, count: files.length, truncated };
2372
3175
  }
2373
3176
  };
2374
3177
 
2375
3178
  // src/tools/grep.ts
2376
- import { join as join9 } from "path";
2377
3179
  import { z as z5 } from "zod";
2378
3180
 
2379
3181
  // src/tools/hashline.ts
@@ -2428,9 +3230,9 @@ var GrepSchema = z5.object({
2428
3230
  maxResults: z5.number().int().min(1).max(200).optional().default(50)
2429
3231
  });
2430
3232
  var DEFAULT_IGNORE = [
2431
- "node_modules",
2432
- ".git",
2433
- "dist",
3233
+ "node_modules/**",
3234
+ ".git/**",
3235
+ "dist/**",
2434
3236
  "*.db",
2435
3237
  "*.db-shm",
2436
3238
  "*.db-wal",
@@ -2453,18 +3255,19 @@ var grepTool = {
2453
3255
  let truncated = false;
2454
3256
  const ig = await loadGitignore(cwd);
2455
3257
  outer:
2456
- for await (const relPath of fileGlob.scan({
3258
+ for await (const fromPath of fileGlob.scan({
2457
3259
  cwd,
2458
3260
  onlyFiles: true,
2459
3261
  dot: true
2460
3262
  })) {
2461
- if (ig?.ignores(relPath))
3263
+ const { absolute, relativePath, ignoreTargets } = getScannedPathInfo(cwd, fromPath);
3264
+ const firstSegment = relativePath.split("/")[0] ?? "";
3265
+ if (ignoreTargets.some((path) => ig?.ignores(path)))
2462
3266
  continue;
2463
- const firstSegment = relPath.split("/")[0] ?? "";
2464
- if (ignoreGlob.some((g) => g.match(relPath) || g.match(firstSegment))) {
3267
+ if (ignoreTargets.some((candidate) => ignoreGlob.some((g) => g.match(candidate) || g.match(firstSegment)))) {
2465
3268
  continue;
2466
3269
  }
2467
- const fullPath = join9(cwd, relPath);
3270
+ const fullPath = absolute;
2468
3271
  let text;
2469
3272
  try {
2470
3273
  text = await Bun.file(fullPath).text();
@@ -2491,7 +3294,7 @@ var grepTool = {
2491
3294
  });
2492
3295
  }
2493
3296
  allMatches.push({
2494
- file: relPath,
3297
+ file: relativePath,
2495
3298
  line: i + 1,
2496
3299
  column: match.index + 1,
2497
3300
  text: formatHashLine(i + 1, line),
@@ -2516,7 +3319,7 @@ var grepTool = {
2516
3319
  // src/tools/hooks.ts
2517
3320
  import { constants, accessSync } from "fs";
2518
3321
  import { homedir as homedir7 } from "os";
2519
- import { join as join10 } from "path";
3322
+ import { join as join9 } from "path";
2520
3323
  function isExecutable(filePath) {
2521
3324
  try {
2522
3325
  accessSync(filePath, constants.X_OK);
@@ -2528,8 +3331,8 @@ function isExecutable(filePath) {
2528
3331
  function findHook(toolName, cwd) {
2529
3332
  const scriptName = `post-${toolName}`;
2530
3333
  const candidates = [
2531
- join10(cwd, ".agents", "hooks", scriptName),
2532
- join10(homedir7(), ".agents", "hooks", scriptName)
3334
+ join9(cwd, ".agents", "hooks", scriptName),
3335
+ join9(homedir7(), ".agents", "hooks", scriptName)
2533
3336
  ];
2534
3337
  for (const p of candidates) {
2535
3338
  if (isExecutable(p))
@@ -2618,7 +3421,6 @@ function hookEnvForRead(input, cwd) {
2618
3421
  }
2619
3422
 
2620
3423
  // src/tools/insert.ts
2621
- import { join as join11, relative as relative3 } from "path";
2622
3424
  import { z as z6 } from "zod";
2623
3425
  var InsertSchema = z6.object({
2624
3426
  path: z6.string().describe("File path to edit (absolute or relative to cwd)"),
@@ -2632,13 +3434,7 @@ var insertTool = {
2632
3434
  description: "Insert new lines before or after an anchor line in an existing file. " + "The anchor line itself is not modified. " + 'Anchors come from the `read` or `grep` tools (format: "line:hash", e.g. "11:a3"). ' + "To replace or delete lines use `replace`. To create a file use `create`.",
2633
3435
  schema: InsertSchema,
2634
3436
  execute: async (input) => {
2635
- const cwd = input.cwd ?? process.cwd();
2636
- const filePath = input.path.startsWith("/") ? input.path : join11(cwd, input.path);
2637
- const relPath = relative3(cwd, filePath);
2638
- const file = Bun.file(filePath);
2639
- if (!await file.exists()) {
2640
- throw new Error(`File not found: "${relPath}". To create a new file use the \`create\` tool.`);
2641
- }
3437
+ const { file, filePath, relPath } = await resolveExistingFile(input.cwd, input.path);
2642
3438
  const parsed = parseAnchor(input.anchor);
2643
3439
  const original = await file.text();
2644
3440
  const lines = original.split(`
@@ -2661,25 +3457,8 @@ var insertTool = {
2661
3457
  return { path: relPath, diff };
2662
3458
  }
2663
3459
  };
2664
- function parseAnchor(value) {
2665
- const normalized = value.trim().endsWith("|") ? value.trim().slice(0, -1) : value;
2666
- const match = /^\s*(\d+):([0-9a-fA-F]{2})\s*$/.exec(normalized);
2667
- if (!match) {
2668
- throw new Error(`Invalid anchor. Expected format: "line:hh" (e.g. "11:a3").`);
2669
- }
2670
- const line = Number(match[1]);
2671
- if (!Number.isInteger(line) || line < 1) {
2672
- throw new Error("Invalid anchor line number.");
2673
- }
2674
- const hash = match[2];
2675
- if (!hash) {
2676
- throw new Error(`Invalid anchor. Expected format: "line:hh" (e.g. "11:a3").`);
2677
- }
2678
- return { line, hash: hash.toLowerCase() };
2679
- }
2680
3460
 
2681
3461
  // src/tools/read.ts
2682
- import { join as join12, relative as relative4 } from "path";
2683
3462
  import { z as z7 } from "zod";
2684
3463
  var ReadSchema = z7.object({
2685
3464
  path: z7.string().describe("File path to read (absolute or relative to cwd)"),
@@ -2693,8 +3472,7 @@ var readTool = {
2693
3472
  description: "Read a file's contents. `line` sets the starting line (1-indexed, default 1). " + "`count` sets how many lines to read (default 500, max 500). " + "Check `truncated` and `totalLines` in the result to detect when more content exists; " + "paginate by incrementing `line`.",
2694
3473
  schema: ReadSchema,
2695
3474
  execute: async (input) => {
2696
- const cwd = input.cwd ?? process.cwd();
2697
- const filePath = input.path.startsWith("/") ? input.path : join12(cwd, input.path);
3475
+ const { filePath, relPath } = resolvePath(input.cwd, input.path);
2698
3476
  const file = Bun.file(filePath);
2699
3477
  const exists = await file.exists();
2700
3478
  if (!exists) {
@@ -2717,7 +3495,7 @@ var readTool = {
2717
3495
  const content = selectedLines.map((line, i) => formatHashLine(clampedStart + i, line)).join(`
2718
3496
  `);
2719
3497
  return {
2720
- path: relative4(cwd, filePath),
3498
+ path: relPath,
2721
3499
  content,
2722
3500
  totalLines,
2723
3501
  line: clampedStart,
@@ -2727,7 +3505,6 @@ var readTool = {
2727
3505
  };
2728
3506
 
2729
3507
  // src/tools/replace.ts
2730
- import { join as join13, relative as relative5 } from "path";
2731
3508
  import { z as z8 } from "zod";
2732
3509
  var ReplaceSchema = z8.object({
2733
3510
  path: z8.string().describe("File path to edit (absolute or relative to cwd)"),
@@ -2741,15 +3518,9 @@ var replaceTool = {
2741
3518
  description: "Replace or delete a range of lines in an existing file using hashline anchors. " + 'Anchors come from the `read` or `grep` tools (format: "line:hash", e.g. "11:a3"). ' + "Provide startAnchor alone to target a single line, or add endAnchor for a range. " + "Set newContent to the replacement text, or omit it to delete the range. " + "To create a file use `create`. To insert without replacing any lines use `insert`.",
2742
3519
  schema: ReplaceSchema,
2743
3520
  execute: async (input) => {
2744
- const cwd = input.cwd ?? process.cwd();
2745
- const filePath = input.path.startsWith("/") ? input.path : join13(cwd, input.path);
2746
- const relPath = relative5(cwd, filePath);
2747
- const file = Bun.file(filePath);
2748
- if (!await file.exists()) {
2749
- throw new Error(`File not found: "${relPath}". To create a new file use the \`create\` tool.`);
2750
- }
2751
- const start = parseAnchor2(input.startAnchor, "startAnchor");
2752
- const end = input.endAnchor ? parseAnchor2(input.endAnchor, "endAnchor") : null;
3521
+ const { file, filePath, relPath } = await resolveExistingFile(input.cwd, input.path);
3522
+ const start = parseAnchor(input.startAnchor, "startAnchor");
3523
+ const end = input.endAnchor ? parseAnchor(input.endAnchor, "endAnchor") : null;
2753
3524
  if (end && end.line < start.line) {
2754
3525
  throw new Error("endAnchor line number must be >= startAnchor line number.");
2755
3526
  }
@@ -2783,22 +3554,6 @@ var replaceTool = {
2783
3554
  return { path: relPath, diff, deleted: replacement.length === 0 };
2784
3555
  }
2785
3556
  };
2786
- function parseAnchor2(value, name) {
2787
- const normalized = value.trim().endsWith("|") ? value.trim().slice(0, -1) : value;
2788
- const match = /^\s*(\d+):([0-9a-fA-F]{2})\s*$/.exec(normalized);
2789
- if (!match) {
2790
- throw new Error(`Invalid ${name}. Expected format: "line:hh" (e.g. "11:a3").`);
2791
- }
2792
- const line = Number(match[1]);
2793
- if (!Number.isInteger(line) || line < 1) {
2794
- throw new Error(`Invalid ${name} line number.`);
2795
- }
2796
- const hash = match[2];
2797
- if (!hash) {
2798
- throw new Error(`Invalid ${name}. Expected format: "line:hh" (e.g. "11:a3").`);
2799
- }
2800
- return { line, hash: hash.toLowerCase() };
2801
- }
2802
3557
 
2803
3558
  // src/tools/shell.ts
2804
3559
  import { z as z9 } from "zod";
@@ -2896,26 +3651,6 @@ var shellTool = {
2896
3651
  }
2897
3652
  };
2898
3653
 
2899
- // src/tools/subagent.ts
2900
- import { z as z10 } from "zod";
2901
- var SubagentInput = z10.object({
2902
- prompt: z10.string().describe("The task or question to give the subagent"),
2903
- agentName: z10.string().optional().describe("Name of a custom agent to use (from .agents/agents/). Omit to use a generic subagent.")
2904
- });
2905
- function createSubagentTool(runSubagent, availableAgents, parentLabel) {
2906
- const agentSection = availableAgents.size > 0 ? `
2907
-
2908
- 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(", ")}.` : "";
2909
- return {
2910
- name: "subagent",
2911
- 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. ${agentSection}`,
2912
- schema: SubagentInput,
2913
- execute: async (input) => {
2914
- return runSubagent(input.prompt, input.agentName, parentLabel);
2915
- }
2916
- };
2917
- }
2918
-
2919
3654
  // src/agent/tools.ts
2920
3655
  function withCwdDefault(tool, cwd) {
2921
3656
  const originalExecute = tool.execute;
@@ -2972,7 +3707,11 @@ function buildToolSet(opts) {
2972
3707
  if (depth >= MAX_SUBAGENT_DEPTH) {
2973
3708
  throw new Error(`Subagent depth limit reached (max ${MAX_SUBAGENT_DEPTH}). ` + `Cannot spawn another subagent from depth ${depth}.`);
2974
3709
  }
2975
- return opts.runSubagent(prompt, depth + 1, agentName, undefined, opts.parentLabel);
3710
+ const output = await opts.runSubagent(prompt, depth + 1, agentName, undefined, opts.parentLabel);
3711
+ const mergeError = getSubagentMergeError(output);
3712
+ if (mergeError)
3713
+ throw new Error(mergeError);
3714
+ return output;
2976
3715
  }, opts.availableAgents, opts.parentLabel)
2977
3716
  ];
2978
3717
  if (process.env.EXA_API_KEY) {
@@ -2994,67 +3733,150 @@ function buildReadOnlyToolSet(opts) {
2994
3733
  }
2995
3734
 
2996
3735
  // src/agent/subagent-runner.ts
3736
+ function makeWorktreeBranch(laneId) {
3737
+ return `mc-sub-${laneId}`;
3738
+ }
3739
+ function makeWorktreePath(laneId) {
3740
+ return join10(tmpdir2(), `mc-wt-${laneId}`);
3741
+ }
2997
3742
  function createSubagentRunner(cwd, reporter, getCurrentModel, getThinkingEffort) {
2998
- let nextLaneId = 1;
2999
3743
  const activeLanes = new Set;
3744
+ const worktreesEnabledPromise = isGitRepo(cwd);
3745
+ let mergeLock = Promise.resolve();
3746
+ const withMergeLock = (fn) => {
3747
+ const task = mergeLock.then(fn, fn);
3748
+ mergeLock = task.then(() => {
3749
+ return;
3750
+ }, () => {
3751
+ return;
3752
+ });
3753
+ return task;
3754
+ };
3000
3755
  const runSubagent = async (prompt, depth = 0, agentName, modelOverride, parentLabel) => {
3001
- const currentModel = getCurrentModel();
3002
- const allAgents = loadAgents(cwd);
3003
- const agentConfig = agentName ? allAgents.get(agentName) : undefined;
3004
- if (agentName && !agentConfig) {
3005
- throw new Error(`Unknown agent "${agentName}". Available agents: ${[...allAgents.keys()].join(", ") || "(none)"}`);
3006
- }
3007
- const model = modelOverride ?? agentConfig?.model ?? currentModel;
3008
- const systemPrompt = agentConfig?.systemPrompt ?? buildSystemPrompt(cwd, model);
3009
- const subMessages = [{ role: "user", content: prompt }];
3010
- const laneId = nextLaneId++;
3756
+ const laneId = crypto.randomUUID().split("-")[0];
3011
3757
  activeLanes.add(laneId);
3012
- const laneLabel = formatSubagentLabel(laneId, parentLabel);
3013
- const subTools = buildToolSet({
3014
- cwd,
3015
- depth,
3016
- runSubagent,
3017
- onHook: (tool, path, ok) => reporter.renderHook(tool, path, ok),
3018
- availableAgents: allAgents,
3019
- parentLabel: laneLabel
3020
- }).filter((tool) => tool.name !== "subagent");
3021
- const subLlm = resolveModel(model);
3022
- let result = "";
3023
- let inputTokens = 0;
3024
- let outputTokens = 0;
3025
- const effort = getThinkingEffort();
3026
- const events = runTurn({
3027
- model: subLlm,
3028
- modelString: model,
3029
- messages: subMessages,
3030
- tools: subTools,
3031
- systemPrompt,
3032
- ...effort ? { thinkingEffort: effort } : {}
3033
- });
3034
- for await (const event of events) {
3035
- reporter.stopSpinner();
3036
- reporter.renderSubagentEvent(event, {
3037
- laneId,
3038
- ...parentLabel ? { parentLabel } : {},
3039
- activeLanes
3758
+ let subagentCwd = cwd;
3759
+ let worktreeBranch;
3760
+ let worktreePath;
3761
+ let preserveBranchOnFailure = false;
3762
+ try {
3763
+ const worktreesEnabled = await worktreesEnabledPromise;
3764
+ if (worktreesEnabled) {
3765
+ const nextBranch = makeWorktreeBranch(laneId);
3766
+ const nextPath = makeWorktreePath(laneId);
3767
+ await createWorktree(cwd, nextBranch, nextPath);
3768
+ worktreeBranch = nextBranch;
3769
+ worktreePath = nextPath;
3770
+ await syncDirtyStateToWorktree(cwd, nextPath);
3771
+ await initializeWorktree(cwd, nextPath);
3772
+ subagentCwd = nextPath;
3773
+ }
3774
+ const currentModel = getCurrentModel();
3775
+ const allAgents = loadAgents(subagentCwd);
3776
+ const agentConfig = agentName ? allAgents.get(agentName) : undefined;
3777
+ if (agentName && !agentConfig) {
3778
+ throw new Error(`Unknown agent "${agentName}". Available agents: ${[...allAgents.keys()].join(", ") || "(none)"}`);
3779
+ }
3780
+ const model = modelOverride ?? agentConfig?.model ?? currentModel;
3781
+ const systemPrompt = agentConfig?.systemPrompt ?? buildSystemPrompt(subagentCwd, model);
3782
+ const subMessages = [{ role: "user", content: prompt }];
3783
+ const laneLabel = formatSubagentLabel(laneId, parentLabel);
3784
+ const subTools = buildToolSet({
3785
+ cwd: subagentCwd,
3786
+ depth,
3787
+ runSubagent,
3788
+ onHook: (tool, path2, ok) => reporter.renderHook(tool, path2, ok),
3789
+ availableAgents: allAgents,
3790
+ parentLabel: laneLabel
3791
+ }).filter((tool) => tool.name !== "subagent");
3792
+ const subLlm = resolveModel(model);
3793
+ let result = "";
3794
+ let inputTokens = 0;
3795
+ let outputTokens = 0;
3796
+ const effort = getThinkingEffort();
3797
+ const events = runTurn({
3798
+ model: subLlm,
3799
+ modelString: model,
3800
+ messages: subMessages,
3801
+ tools: subTools,
3802
+ systemPrompt,
3803
+ ...effort ? { thinkingEffort: effort } : {}
3040
3804
  });
3041
- reporter.startSpinner("thinking");
3042
- if (event.type === "text-delta")
3043
- result += event.delta;
3044
- if (event.type === "turn-complete") {
3045
- inputTokens = event.inputTokens;
3046
- outputTokens = event.outputTokens;
3805
+ for await (const event of events) {
3806
+ reporter.stopSpinner();
3807
+ reporter.renderSubagentEvent(event, {
3808
+ laneId,
3809
+ ...parentLabel ? { parentLabel } : {},
3810
+ hasWorktree: !!worktreeBranch,
3811
+ activeLanes
3812
+ });
3813
+ reporter.startSpinner("thinking");
3814
+ if (event.type === "text-delta")
3815
+ result += event.delta;
3816
+ if (event.type === "turn-complete") {
3817
+ inputTokens = event.inputTokens;
3818
+ outputTokens = event.outputTokens;
3819
+ }
3820
+ }
3821
+ const baseOutput = { result, inputTokens, outputTokens };
3822
+ const branch = worktreeBranch;
3823
+ const path = worktreePath;
3824
+ if (!branch || !path)
3825
+ return baseOutput;
3826
+ preserveBranchOnFailure = true;
3827
+ return await withMergeLock(async () => {
3828
+ try {
3829
+ const mergeResult = await mergeWorktree(cwd, branch);
3830
+ await removeWorktree(cwd, path);
3831
+ if (mergeResult.success) {
3832
+ await cleanupBranch(cwd, branch);
3833
+ return baseOutput;
3834
+ }
3835
+ return {
3836
+ ...baseOutput,
3837
+ mergeConflict: {
3838
+ branch,
3839
+ conflictFiles: mergeResult.conflictFiles
3840
+ }
3841
+ };
3842
+ } catch (error) {
3843
+ if (error instanceof MergeInProgressError) {
3844
+ await removeWorktree(cwd, path);
3845
+ return {
3846
+ ...baseOutput,
3847
+ mergeBlocked: {
3848
+ branch,
3849
+ conflictFiles: error.conflictFiles
3850
+ }
3851
+ };
3852
+ }
3853
+ throw error;
3854
+ }
3855
+ });
3856
+ } catch (error) {
3857
+ if (worktreeBranch && worktreePath) {
3858
+ const branch = worktreeBranch;
3859
+ const path = worktreePath;
3860
+ try {
3861
+ await withMergeLock(async () => {
3862
+ await removeWorktree(cwd, path);
3863
+ if (!preserveBranchOnFailure) {
3864
+ await cleanupBranch(cwd, branch);
3865
+ }
3866
+ });
3867
+ } catch {}
3047
3868
  }
3869
+ throw error;
3870
+ } finally {
3871
+ activeLanes.delete(laneId);
3048
3872
  }
3049
- activeLanes.delete(laneId);
3050
- return { result, inputTokens, outputTokens };
3051
3873
  };
3052
3874
  return runSubagent;
3053
3875
  }
3054
3876
 
3055
3877
  // src/tools/snapshot.ts
3056
3878
  import { readFileSync as readFileSync3, unlinkSync as unlinkSync2 } from "fs";
3057
- import { join as join14 } from "path";
3879
+ import { join as join11 } from "path";
3058
3880
  async function gitBytes(args, cwd) {
3059
3881
  try {
3060
3882
  const proc = Bun.spawn(["git", ...args], {
@@ -3127,7 +3949,7 @@ async function getStatusEntries(repoRoot) {
3127
3949
  return true;
3128
3950
  });
3129
3951
  }
3130
- async function getRepoRoot(cwd) {
3952
+ async function getRepoRoot2(cwd) {
3131
3953
  const result = await git(["rev-parse", "--show-toplevel"], cwd);
3132
3954
  if (result.code !== 0)
3133
3955
  return null;
@@ -3135,7 +3957,7 @@ async function getRepoRoot(cwd) {
3135
3957
  }
3136
3958
  async function takeSnapshot(cwd, sessionId, turnIndex) {
3137
3959
  try {
3138
- const repoRoot = await getRepoRoot(cwd);
3960
+ const repoRoot = await getRepoRoot2(cwd);
3139
3961
  if (repoRoot === null)
3140
3962
  return false;
3141
3963
  const entries = await getStatusEntries(repoRoot);
@@ -3145,7 +3967,7 @@ async function takeSnapshot(cwd, sessionId, turnIndex) {
3145
3967
  return false;
3146
3968
  const files = [];
3147
3969
  for (const entry of entries) {
3148
- const absPath = join14(repoRoot, entry.path);
3970
+ const absPath = join11(repoRoot, entry.path);
3149
3971
  if (!entry.existsOnDisk) {
3150
3972
  const { bytes, code } = await gitBytes(["show", `HEAD:${entry.path}`], repoRoot);
3151
3973
  if (code === 0) {
@@ -3188,13 +4010,13 @@ async function takeSnapshot(cwd, sessionId, turnIndex) {
3188
4010
  async function restoreSnapshot(cwd, sessionId, turnIndex) {
3189
4011
  try {
3190
4012
  const files = loadSnapshot(sessionId, turnIndex);
3191
- const repoRoot = await getRepoRoot(cwd);
4013
+ const repoRoot = await getRepoRoot2(cwd);
3192
4014
  if (files.length === 0)
3193
4015
  return { restored: false, reason: "not-found" };
3194
4016
  const root = repoRoot ?? cwd;
3195
4017
  let anyFailed = false;
3196
4018
  for (const file of files) {
3197
- const absPath = join14(root, file.path);
4019
+ const absPath = join11(root, file.path);
3198
4020
  if (!file.existed) {
3199
4021
  try {
3200
4022
  if (await Bun.file(absPath).exists()) {
@@ -3261,7 +4083,7 @@ async function undoLastTurn(ctx) {
3261
4083
  }
3262
4084
 
3263
4085
  // src/agent/agent-helpers.ts
3264
- import { join as join15 } from "path";
4086
+ import { join as join12 } from "path";
3265
4087
  import * as c9 from "yoctocolors";
3266
4088
 
3267
4089
  // src/cli/image-types.ts
@@ -3360,7 +4182,7 @@ ${skill.content}
3360
4182
  result = result.slice(0, match.index) + replacement + result.slice((match.index ?? 0) + match[0].length);
3361
4183
  continue;
3362
4184
  }
3363
- const filePath = ref.startsWith("/") ? ref : join15(cwd, ref);
4185
+ const filePath = ref.startsWith("/") ? ref : join12(cwd, ref);
3364
4186
  if (isImageFilename(ref)) {
3365
4187
  const attachment = await loadImageFile(filePath);
3366
4188
  if (attachment) {
@@ -3441,7 +4263,8 @@ function loadCustomCommands(cwd) {
3441
4263
  description: meta.description ?? name,
3442
4264
  ...meta.model ? { model: meta.model } : {},
3443
4265
  template: body,
3444
- source
4266
+ source,
4267
+ execution: meta.execution === "inline" ? "inline" : "subagent"
3445
4268
  })
3446
4269
  });
3447
4270
  }
@@ -3485,6 +4308,11 @@ async function expandTemplate(template, args, cwd) {
3485
4308
  }
3486
4309
 
3487
4310
  // src/cli/commands.ts
4311
+ function assertSubagentMerged(output) {
4312
+ const mergeError = getSubagentMergeError(output);
4313
+ if (mergeError)
4314
+ throw new Error(mergeError);
4315
+ }
3488
4316
  async function handleModel(ctx, args) {
3489
4317
  const parts = args.trim().split(/\s+/).filter(Boolean);
3490
4318
  if (parts.length > 0) {
@@ -3504,8 +4332,8 @@ async function handleModel(ctx, args) {
3504
4332
  const idArg = parts[0] ?? "";
3505
4333
  let modelId = idArg;
3506
4334
  if (!idArg.includes("/")) {
3507
- const models2 = await fetchAvailableModels();
3508
- const match = models2.find((m) => m.id.split("/").slice(1).join("/") === idArg || m.id === idArg);
4335
+ const snapshot2 = await fetchAvailableModels();
4336
+ const match = snapshot2.models.find((m) => m.id.split("/").slice(1).join("/") === idArg || m.id === idArg);
3509
4337
  if (match) {
3510
4338
  modelId = match.id;
3511
4339
  } else {
@@ -3533,13 +4361,19 @@ async function handleModel(ctx, args) {
3533
4361
  return;
3534
4362
  }
3535
4363
  writeln(`${c10.dim(" fetching models\u2026")}`);
3536
- const models = await fetchAvailableModels();
4364
+ const snapshot = await fetchAvailableModels();
4365
+ const models = snapshot.models;
3537
4366
  process.stdout.write("\x1B[1A\r\x1B[2K");
3538
4367
  if (models.length === 0) {
3539
4368
  writeln(`${PREFIX.error} No models found. Check your API keys or Ollama connection.`);
3540
4369
  writeln(c10.dim(" Set OPENCODE_API_KEY for Zen, or start Ollama for local models."));
3541
4370
  return;
3542
4371
  }
4372
+ if (snapshot.stale) {
4373
+ const lastSync = snapshot.lastSyncAt ? new Date(snapshot.lastSyncAt).toLocaleString() : "never";
4374
+ const refreshTag = snapshot.refreshing ? " (refreshing in background)" : "";
4375
+ writeln(c10.dim(` model metadata is stale (last sync: ${lastSync})${refreshTag}`));
4376
+ }
3543
4377
  const byProvider = new Map;
3544
4378
  for (const m of models) {
3545
4379
  const existing = byProvider.get(m.provider);
@@ -3696,6 +4530,7 @@ async function handleReview(ctx, args) {
3696
4530
  writeln();
3697
4531
  try {
3698
4532
  const output = await ctx.runSubagent(REVIEW_PROMPT(ctx.cwd, focus));
4533
+ assertSubagentMerged(output);
3699
4534
  write(renderMarkdown(output.result));
3700
4535
  writeln();
3701
4536
  return {
@@ -3722,8 +4557,12 @@ async function handleCustomCommand(cmd, args, ctx) {
3722
4557
  const src = c10.dim(`[${srcPath}]`);
3723
4558
  writeln(`${PREFIX.info} ${label} ${src}`);
3724
4559
  writeln();
4560
+ if (cmd.execution === "inline") {
4561
+ return { type: "inject-user-message", text: prompt };
4562
+ }
3725
4563
  try {
3726
4564
  const output = await ctx.runSubagent(prompt, cmd.model);
4565
+ assertSubagentMerged(output);
3727
4566
  write(renderMarkdown(output.result));
3728
4567
  writeln();
3729
4568
  return {
@@ -3792,7 +4631,7 @@ function handleHelp(ctx, custom) {
3792
4631
  writeln(` ${c10.green("@file".padEnd(26))} ${c10.dim("inject file contents into prompt (Tab to complete)")}`);
3793
4632
  writeln(` ${c10.green("!cmd".padEnd(26))} ${c10.dim("run shell command, output added as context")}`);
3794
4633
  writeln();
3795
- writeln(` ${c10.dim("ctrl+c")} cancel ${c10.dim("\xB7")} ${c10.dim("ctrl+d")} exit ${c10.dim("\xB7")} ${c10.dim("ctrl+r")} history search ${c10.dim("\xB7")} ${c10.dim("\u2191\u2193")} history`);
4634
+ writeln(` ${c10.dim("esc")} cancel response ${c10.dim("\xB7")} ${c10.dim("ctrl+c")} exit ${c10.dim("\xB7")} ${c10.dim("ctrl+d")} exit ${c10.dim("\xB7")} ${c10.dim("ctrl+r")} history search ${c10.dim("\xB7")} ${c10.dim("\u2191\u2193")} history`);
3796
4635
  writeln();
3797
4636
  }
3798
4637
  async function handleCommand(command, args, ctx) {
@@ -3839,7 +4678,7 @@ async function handleCommand(command, args, ctx) {
3839
4678
  }
3840
4679
 
3841
4680
  // src/cli/input.ts
3842
- import { join as join16, relative as relative6 } from "path";
4681
+ import { join as join13, relative as relative3 } from "path";
3843
4682
  import * as c11 from "yoctocolors";
3844
4683
  var ESC = "\x1B";
3845
4684
  var CSI = `${ESC}[`;
@@ -3868,6 +4707,8 @@ var CTRL_K = "\v";
3868
4707
  var CTRL_L = "\f";
3869
4708
  var CTRL_R = "\x12";
3870
4709
  var TAB = "\t";
4710
+ var ESC_BYTE = 27;
4711
+ var CTRL_C_BYTE = 3;
3871
4712
  async function getAtCompletions(prefix, cwd) {
3872
4713
  const query = prefix.startsWith("@") ? prefix.slice(1) : prefix;
3873
4714
  const results = [];
@@ -3891,7 +4732,7 @@ async function getAtCompletions(prefix, cwd) {
3891
4732
  for await (const file of glob.scan({ cwd, onlyFiles: true })) {
3892
4733
  if (file.includes("node_modules") || file.includes(".git"))
3893
4734
  continue;
3894
- results.push(`@${relative6(cwd, join16(cwd, file))}`);
4735
+ results.push(`@${relative3(cwd, join13(cwd, file))}`);
3895
4736
  if (results.length >= MAX)
3896
4737
  break;
3897
4738
  }
@@ -3913,7 +4754,7 @@ async function tryExtractImageFromPaste(pasted, cwd) {
3913
4754
  }
3914
4755
  }
3915
4756
  if (!trimmed.includes(" ") && isImageFilename(trimmed)) {
3916
- const filePath = trimmed.startsWith("/") ? trimmed : join16(cwd, trimmed);
4757
+ const filePath = trimmed.startsWith("/") ? trimmed : join13(cwd, trimmed);
3917
4758
  const attachment = await loadImageFile(filePath);
3918
4759
  if (attachment) {
3919
4760
  const name = filePath.split("/").pop() ?? trimmed;
@@ -3950,35 +4791,67 @@ async function readKey(reader) {
3950
4791
  return "";
3951
4792
  return new TextDecoder().decode(value);
3952
4793
  }
3953
- function watchForInterrupt(abortController) {
3954
- if (!process.stdin.isTTY)
4794
+ function getTurnControlAction(chunk) {
4795
+ for (const byte of chunk) {
4796
+ if (byte === ESC_BYTE)
4797
+ return "cancel";
4798
+ if (byte === CTRL_C_BYTE)
4799
+ return "quit";
4800
+ }
4801
+ return null;
4802
+ }
4803
+ function exitOnCtrlC(opts) {
4804
+ if (opts?.printNewline)
4805
+ process.stdout.write(`
4806
+ `);
4807
+ if (opts?.disableBracketedPaste)
4808
+ process.stdout.write(BPASTE_DISABLE);
4809
+ terminal.setInterruptHandler(null);
4810
+ terminal.setRawMode(false);
4811
+ process.stdin.pause();
4812
+ terminal.restoreTerminal();
4813
+ process.exit(130);
4814
+ }
4815
+ function watchForCancel(abortController) {
4816
+ if (!terminal.isTTY)
3955
4817
  return () => {};
3956
- const onInterrupt = () => {
4818
+ const onCancel = () => {
3957
4819
  cleanup();
3958
4820
  abortController.abort();
3959
4821
  };
3960
4822
  const onData = (chunk) => {
3961
- for (const byte of chunk) {
3962
- if (byte === 3) {
3963
- onInterrupt();
3964
- return;
3965
- }
4823
+ const action = getTurnControlAction(chunk);
4824
+ if (action === "cancel") {
4825
+ onCancel();
4826
+ return;
4827
+ }
4828
+ if (action === "quit") {
4829
+ cleanup();
4830
+ exitOnCtrlC({ printNewline: true });
3966
4831
  }
3967
4832
  };
3968
4833
  const cleanup = () => {
3969
4834
  process.stdin.removeListener("data", onData);
3970
4835
  terminal.setInterruptHandler(null);
3971
- process.stdin.setRawMode(false);
4836
+ terminal.setRawMode(false);
3972
4837
  process.stdin.pause();
3973
4838
  };
3974
- terminal.setInterruptHandler(onInterrupt);
3975
- process.stdin.setRawMode(true);
4839
+ terminal.setInterruptHandler(onCancel);
4840
+ terminal.setRawMode(true);
3976
4841
  process.stdin.resume();
3977
4842
  process.stdin.on("data", onData);
3978
4843
  return cleanup;
3979
4844
  }
3980
- var PASTE_SENTINEL = "\x00PASTE\x00";
3981
- var PASTE_SENTINEL_LEN = PASTE_SENTINEL.length;
4845
+ var PASTE_TOKEN_START = 57344;
4846
+ var PASTE_TOKEN_END = 63743;
4847
+ function createPasteToken(buf, pasteTokens) {
4848
+ for (let code = PASTE_TOKEN_START;code <= PASTE_TOKEN_END; code++) {
4849
+ const token = String.fromCharCode(code);
4850
+ if (!buf.includes(token) && !pasteTokens.has(token))
4851
+ return token;
4852
+ }
4853
+ throw new Error("Too many pasted chunks in a single prompt");
4854
+ }
3982
4855
  function pasteLabel(text) {
3983
4856
  const lines = text.split(`
3984
4857
  `);
@@ -3988,6 +4861,58 @@ function pasteLabel(text) {
3988
4861
  const more = extra > 0 ? ` +${extra} more line${extra === 1 ? "" : "s"}` : "";
3989
4862
  return `[pasted: "${preview}"${more}]`;
3990
4863
  }
4864
+ function processInputBuffer(buf, pasteTokens, replacer) {
4865
+ let out = "";
4866
+ for (let i = 0;i < buf.length; i++) {
4867
+ const ch = buf[i] ?? "";
4868
+ out += replacer(ch, pasteTokens.get(ch));
4869
+ }
4870
+ return out;
4871
+ }
4872
+ function renderInputBuffer(buf, pasteTokens) {
4873
+ return processInputBuffer(buf, pasteTokens, (ch, pasted) => pasted ? pasteLabel(pasted) : ch);
4874
+ }
4875
+ function expandInputBuffer(buf, pasteTokens) {
4876
+ return processInputBuffer(buf, pasteTokens, (ch, pasted) => pasted ?? ch);
4877
+ }
4878
+ function pruneInputPasteTokens(pasteTokens, ...buffers) {
4879
+ const referenced = buffers.join("");
4880
+ const next = new Map;
4881
+ for (const [token, text] of pasteTokens) {
4882
+ if (referenced.includes(token))
4883
+ next.set(token, text);
4884
+ }
4885
+ return next;
4886
+ }
4887
+ function getVisualCursor(buf, cursor, pasteTokens) {
4888
+ let visual = 0;
4889
+ for (let i = 0;i < Math.min(cursor, buf.length); i++) {
4890
+ const ch = buf[i] ?? "";
4891
+ const pasted = pasteTokens.get(ch);
4892
+ visual += pasted ? pasteLabel(pasted).length : 1;
4893
+ }
4894
+ return visual;
4895
+ }
4896
+ function buildPromptDisplay(text, cursor, maxLen) {
4897
+ const clampedCursor = Math.max(0, Math.min(cursor, text.length));
4898
+ if (maxLen <= 0)
4899
+ return { display: "", cursor: 0 };
4900
+ if (text.length <= maxLen)
4901
+ return { display: text, cursor: clampedCursor };
4902
+ let start = Math.max(0, clampedCursor - maxLen);
4903
+ const end = Math.min(text.length, start + maxLen);
4904
+ if (end - start < maxLen)
4905
+ start = Math.max(0, end - maxLen);
4906
+ let display = text.slice(start, end);
4907
+ if (start > 0 && display.length > 0)
4908
+ display = `\u2026${display.slice(1)}`;
4909
+ if (end < text.length && display.length > 0)
4910
+ display = `${display.slice(0, -1)}\u2026`;
4911
+ return {
4912
+ display,
4913
+ cursor: Math.min(clampedCursor - start, display.length)
4914
+ };
4915
+ }
3991
4916
  var PROMPT = c11.green("\u25B6 ");
3992
4917
  var PROMPT_PLAN = c11.yellow("\u2B22 ");
3993
4918
  var PROMPT_RALPH = c11.magenta("\u21BB ");
@@ -4001,24 +4926,44 @@ async function readline(opts) {
4001
4926
  let savedInput = "";
4002
4927
  let searchMode = false;
4003
4928
  let searchQuery = "";
4004
- let pasteBuffer = null;
4929
+ const pasteTokens = new Map;
4005
4930
  const imageAttachments = [];
4006
- process.stdin.setRawMode(true);
4931
+ function prunePasteTokens() {
4932
+ const next = pruneInputPasteTokens(pasteTokens, buf, savedInput);
4933
+ pasteTokens.clear();
4934
+ for (const [token, text] of next)
4935
+ pasteTokens.set(token, text);
4936
+ }
4937
+ function insertText(text) {
4938
+ buf = buf.slice(0, cursor) + text + buf.slice(cursor);
4939
+ cursor += text.length;
4940
+ }
4941
+ function deleteWordBackward() {
4942
+ const end = cursor;
4943
+ while (cursor > 0 && buf[cursor - 1] === " ")
4944
+ cursor--;
4945
+ while (cursor > 0 && buf[cursor - 1] !== " ")
4946
+ cursor--;
4947
+ buf = buf.slice(0, cursor) + buf.slice(end);
4948
+ prunePasteTokens();
4949
+ renderPrompt();
4950
+ }
4951
+ function insertPasteToken(text) {
4952
+ const token = createPasteToken(buf, pasteTokens);
4953
+ pasteTokens.set(token, text);
4954
+ insertText(token);
4955
+ }
4956
+ terminal.setRawMode(true);
4007
4957
  process.stdin.resume();
4008
4958
  process.stdout.write(BPASTE_ENABLE);
4009
4959
  const reader = getStdinReader();
4010
4960
  function renderPrompt() {
4011
4961
  const cols = process.stdout.columns ?? 80;
4012
- const visualBuf = (pasteBuffer ? buf.replace(PASTE_SENTINEL, c11.dim(pasteLabel(pasteBuffer))) : buf).replace(/\[image: [^\]]+\]/g, (m) => c11.dim(c11.cyan(m)));
4013
- const visualCursor = pasteBuffer ? (() => {
4014
- const sentinelPos = buf.indexOf(PASTE_SENTINEL);
4015
- if (sentinelPos === -1 || cursor <= sentinelPos)
4016
- return cursor;
4017
- return cursor - PASTE_SENTINEL_LEN + pasteLabel(pasteBuffer).length;
4018
- })() : cursor;
4019
- const display = visualBuf.length > cols - PROMPT_RAW_LEN - 2 ? `\u2026${visualBuf.slice(-(cols - PROMPT_RAW_LEN - 3))}` : visualBuf;
4962
+ const visualBuf = renderInputBuffer(buf, pasteTokens);
4963
+ const visualCursor = getVisualCursor(buf, cursor, pasteTokens);
4964
+ const { display, cursor: displayCursor } = buildPromptDisplay(visualBuf, visualCursor, Math.max(1, cols - PROMPT_RAW_LEN - 2));
4020
4965
  const prompt = opts.planMode ? PROMPT_PLAN : opts.ralphMode ? PROMPT_RALPH : PROMPT;
4021
- process.stdout.write(`${CLEAR_LINE}${prompt}${display}${CSI}${PROMPT_RAW_LEN + visualCursor + 1}G`);
4966
+ process.stdout.write(`${CLEAR_LINE}${prompt}${display}${CSI}${PROMPT_RAW_LEN + displayCursor + 1}G`);
4022
4967
  }
4023
4968
  function renderSearchPrompt() {
4024
4969
  process.stdout.write(`${CLEAR_LINE}${c11.cyan("search:")} ${searchQuery}\u2588`);
@@ -4030,6 +4975,7 @@ async function readline(opts) {
4030
4975
  buf = savedInput;
4031
4976
  }
4032
4977
  cursor = buf.length;
4978
+ prunePasteTokens();
4033
4979
  renderPrompt();
4034
4980
  }
4035
4981
  renderPrompt();
@@ -4079,14 +5025,11 @@ async function readline(opts) {
4079
5025
  const imageResult = await tryExtractImageFromPaste(pasted, cwd);
4080
5026
  if (imageResult) {
4081
5027
  imageAttachments.push(imageResult.attachment);
4082
- buf = buf.slice(0, cursor) + imageResult.label + buf.slice(cursor);
4083
- cursor += imageResult.label.length;
5028
+ insertText(imageResult.label);
4084
5029
  renderPrompt();
4085
5030
  continue;
4086
5031
  }
4087
- pasteBuffer = pasted;
4088
- buf = buf.slice(0, cursor) + PASTE_SENTINEL + buf.slice(cursor);
4089
- cursor += PASTE_SENTINEL_LEN;
5032
+ insertPasteToken(pasted);
4090
5033
  renderPrompt();
4091
5034
  continue;
4092
5035
  }
@@ -4138,15 +5081,7 @@ async function readline(opts) {
4138
5081
  continue;
4139
5082
  }
4140
5083
  if (raw === `${ESC}${BACKSPACE}`) {
4141
- const end = cursor;
4142
- while (cursor > 0 && buf[cursor - 1] === " ")
4143
- cursor--;
4144
- while (cursor > 0 && buf[cursor - 1] !== " ")
4145
- cursor--;
4146
- buf = buf.slice(0, cursor) + buf.slice(end);
4147
- if (pasteBuffer && !buf.includes(PASTE_SENTINEL))
4148
- pasteBuffer = null;
4149
- renderPrompt();
5084
+ deleteWordBackward();
4150
5085
  continue;
4151
5086
  }
4152
5087
  if (raw === ESC) {
@@ -4157,9 +5092,7 @@ async function readline(opts) {
4157
5092
  continue;
4158
5093
  }
4159
5094
  if (raw === CTRL_C) {
4160
- process.stdout.write(`
4161
- `);
4162
- return { type: "interrupt" };
5095
+ exitOnCtrlC({ printNewline: true, disableBracketedPaste: true });
4163
5096
  }
4164
5097
  if (raw === CTRL_D) {
4165
5098
  process.stdout.write(`
@@ -4177,29 +5110,19 @@ async function readline(opts) {
4177
5110
  continue;
4178
5111
  }
4179
5112
  if (raw === CTRL_W) {
4180
- const end = cursor;
4181
- while (cursor > 0 && buf[cursor - 1] === " ")
4182
- cursor--;
4183
- while (cursor > 0 && buf[cursor - 1] !== " ")
4184
- cursor--;
4185
- buf = buf.slice(0, cursor) + buf.slice(end);
4186
- if (pasteBuffer && !buf.includes(PASTE_SENTINEL))
4187
- pasteBuffer = null;
4188
- renderPrompt();
5113
+ deleteWordBackward();
4189
5114
  continue;
4190
5115
  }
4191
5116
  if (raw === CTRL_U) {
4192
5117
  buf = buf.slice(cursor);
4193
5118
  cursor = 0;
4194
- if (pasteBuffer && !buf.includes(PASTE_SENTINEL))
4195
- pasteBuffer = null;
5119
+ prunePasteTokens();
4196
5120
  renderPrompt();
4197
5121
  continue;
4198
5122
  }
4199
5123
  if (raw === CTRL_K) {
4200
5124
  buf = buf.slice(0, cursor);
4201
- if (pasteBuffer && !buf.includes(PASTE_SENTINEL))
4202
- pasteBuffer = null;
5125
+ prunePasteTokens();
4203
5126
  renderPrompt();
4204
5127
  continue;
4205
5128
  }
@@ -4218,8 +5141,7 @@ async function readline(opts) {
4218
5141
  if (cursor > 0) {
4219
5142
  buf = buf.slice(0, cursor - 1) + buf.slice(cursor);
4220
5143
  cursor--;
4221
- if (pasteBuffer && !buf.includes(PASTE_SENTINEL))
4222
- pasteBuffer = null;
5144
+ prunePasteTokens();
4223
5145
  renderPrompt();
4224
5146
  }
4225
5147
  continue;
@@ -4246,8 +5168,8 @@ async function readline(opts) {
4246
5168
  continue;
4247
5169
  }
4248
5170
  if (raw === ENTER || raw === NEWLINE) {
4249
- const expanded = pasteBuffer ? buf.replace(PASTE_SENTINEL, pasteBuffer) : buf;
4250
- pasteBuffer = null;
5171
+ const expanded = expandInputBuffer(buf, pasteTokens);
5172
+ pasteTokens.clear();
4251
5173
  const text = expanded.trim();
4252
5174
  process.stdout.write(`
4253
5175
  `);
@@ -4276,30 +5198,31 @@ async function readline(opts) {
4276
5198
  };
4277
5199
  }
4278
5200
  if (raw.length > 1) {
4279
- const pasted = raw.replace(/\r?\n$/, "");
4280
- const imageResult = await tryExtractImageFromPaste(pasted, cwd);
5201
+ const chunk = raw.replace(/\r?\n$/, "");
5202
+ const imageResult = await tryExtractImageFromPaste(chunk, cwd);
4281
5203
  if (imageResult) {
4282
5204
  imageAttachments.push(imageResult.attachment);
4283
- buf = buf.slice(0, cursor) + imageResult.label + buf.slice(cursor);
4284
- cursor += imageResult.label.length;
5205
+ insertText(imageResult.label);
4285
5206
  renderPrompt();
4286
5207
  continue;
4287
5208
  }
4288
- pasteBuffer = pasted;
4289
- buf = buf.slice(0, cursor) + PASTE_SENTINEL + buf.slice(cursor);
4290
- cursor += PASTE_SENTINEL_LEN;
5209
+ if (chunk.includes(`
5210
+ `) || chunk.includes("\r")) {
5211
+ insertPasteToken(chunk);
5212
+ } else {
5213
+ insertText(chunk);
5214
+ }
4291
5215
  renderPrompt();
4292
5216
  continue;
4293
5217
  }
4294
5218
  if (raw >= " " || raw === "\t") {
4295
- buf = buf.slice(0, cursor) + raw + buf.slice(cursor);
4296
- cursor++;
5219
+ insertText(raw);
4297
5220
  renderPrompt();
4298
5221
  }
4299
5222
  }
4300
5223
  } finally {
4301
5224
  process.stdout.write(BPASTE_DISABLE);
4302
- process.stdin.setRawMode(false);
5225
+ terminal.setRawMode(false);
4303
5226
  process.stdin.pause();
4304
5227
  }
4305
5228
  }
@@ -4483,7 +5406,7 @@ class SessionRunner {
4483
5406
  abortController.signal.addEventListener("abort", () => {
4484
5407
  wasAborted = true;
4485
5408
  });
4486
- const stopWatcher = watchForInterrupt(abortController);
5409
+ const stopWatcher = watchForCancel(abortController);
4487
5410
  const { text: resolvedText, images: refImages } = await resolveFileRefs(text, this.cwd);
4488
5411
  const allImages = [...pastedImages, ...refImages];
4489
5412
  const thisTurn = this.turnIndex++;
@@ -4682,7 +5605,7 @@ async function runAgent(opts) {
4682
5605
  inputTokens: runner.totalIn,
4683
5606
  outputTokens: runner.totalOut,
4684
5607
  contextTokens: runner.lastContextTokens,
4685
- contextWindow: getContextWindow(runner.currentModel) ?? 0,
5608
+ contextWindow: getContextWindow2(runner.currentModel) ?? 0,
4686
5609
  ralphMode: runner.ralphMode,
4687
5610
  thinkingEffort: runner.currentThinkingEffort
4688
5611
  });
@@ -4746,6 +5669,8 @@ class CliReporter {
4746
5669
  registerTerminalCleanup();
4747
5670
  initErrorLog();
4748
5671
  initApiLog();
5672
+ initModelInfoCache();
5673
+ refreshModelInfoInBackground().catch(() => {});
4749
5674
  function parseArgs(argv) {
4750
5675
  const args = {
4751
5676
  model: null,