mini-coder 0.0.13 → 0.0.14

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
@@ -716,15 +716,32 @@ function renderToolResult(toolName, result, isError) {
716
716
  const text = JSON.stringify(result);
717
717
  writeln(` ${c3.dim(text.length > 120 ? `${text.slice(0, 117)}\u2026` : text)}`);
718
718
  }
719
- function formatSubagentLabel(laneId, parentLabel) {
720
- const numStr = parentLabel ? `${parentLabel.replace(/[\[\]]/g, "")}.${laneId}` : `${laneId}`;
721
- return c3.dim(c3.cyan(`[${numStr}]`));
719
+ function stripAnsiSgr(text) {
720
+ const esc = String.fromCharCode(27);
721
+ return text.replace(new RegExp(`${esc}\\[[0-9;]*m`, "g"), "");
722
+ }
723
+ function normalizeParentLaneLabel(parentLabel) {
724
+ const plain = stripAnsiSgr(parentLabel);
725
+ const match = plain.match(/\[([^\]]+)\]/);
726
+ const inner = (match?.[1] ?? plain).trim();
727
+ const lanePath = (inner.split("\xB7")[0] ?? inner).trim();
728
+ return lanePath || inner;
729
+ }
730
+ function shortWorktreeBranch(branch) {
731
+ const match = branch.match(/^(mc-sub-\d+)-\d+$/);
732
+ return match?.[1] ?? branch;
733
+ }
734
+ function formatSubagentLabel(laneId, parentLabel, worktreeBranch) {
735
+ const parent = parentLabel ? normalizeParentLaneLabel(parentLabel) : "";
736
+ const numStr = parent ? `${parent}.${laneId}` : `${laneId}`;
737
+ const branchHint = worktreeBranch ? `\xB7${shortWorktreeBranch(worktreeBranch)}` : "";
738
+ return c3.dim(c3.cyan(`[${numStr}${branchHint}]`));
722
739
  }
723
740
  var laneBuffers = new Map;
724
741
  function renderSubagentEvent(event, opts) {
725
- const { laneId, parentLabel, activeLanes } = opts;
726
- const labelStr = formatSubagentLabel(laneId, parentLabel);
727
- const prefix = activeLanes.size > 1 ? `${labelStr} ` : "";
742
+ const { laneId, parentLabel, worktreeBranch, activeLanes } = opts;
743
+ const labelStr = formatSubagentLabel(laneId, parentLabel, worktreeBranch);
744
+ const prefix = activeLanes.size > 1 || worktreeBranch ? `${labelStr} ` : "";
728
745
  if (event.type === "text-delta") {
729
746
  const buf = (laneBuffers.get(laneId) ?? "") + event.delta;
730
747
  const lines = buf.split(`
@@ -978,7 +995,7 @@ function renderError(err, context = "render") {
978
995
 
979
996
  // src/cli/output.ts
980
997
  var HOME2 = homedir3();
981
- var PACKAGE_VERSION = "0.0.12";
998
+ var PACKAGE_VERSION = "0.0.14";
982
999
  function tildePath(p) {
983
1000
  return p.startsWith(HOME2) ? `~${p.slice(HOME2.length)}` : p;
984
1001
  }
@@ -1068,6 +1085,8 @@ function parseFrontmatter(raw) {
1068
1085
  meta.description = val;
1069
1086
  if (key === "model")
1070
1087
  meta.model = val;
1088
+ if (key === "execution")
1089
+ meta.execution = val;
1071
1090
  }
1072
1091
  return { meta, body: (m[2] ?? "").trim() };
1073
1092
  }
@@ -1207,6 +1226,779 @@ function logApiEvent(event, data) {
1207
1226
  writer2.flush();
1208
1227
  }
1209
1228
 
1229
+ // src/session/db/connection.ts
1230
+ import { Database } from "bun:sqlite";
1231
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, unlinkSync } from "fs";
1232
+ import { homedir as homedir6 } from "os";
1233
+ import { join as join4 } from "path";
1234
+ function getConfigDir() {
1235
+ return join4(homedir6(), ".config", "mini-coder");
1236
+ }
1237
+ function getDbPath() {
1238
+ const dir = getConfigDir();
1239
+ if (!existsSync2(dir))
1240
+ mkdirSync3(dir, { recursive: true });
1241
+ return join4(dir, "sessions.db");
1242
+ }
1243
+ var DB_VERSION = 3;
1244
+ var SCHEMA = `
1245
+ CREATE TABLE IF NOT EXISTS sessions (
1246
+ id TEXT PRIMARY KEY,
1247
+ title TEXT NOT NULL DEFAULT '',
1248
+ cwd TEXT NOT NULL,
1249
+ model TEXT NOT NULL,
1250
+ created_at INTEGER NOT NULL,
1251
+ updated_at INTEGER NOT NULL
1252
+ );
1253
+
1254
+ CREATE TABLE IF NOT EXISTS messages (
1255
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1256
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
1257
+ payload TEXT NOT NULL,
1258
+ turn_index INTEGER NOT NULL DEFAULT 0,
1259
+ created_at INTEGER NOT NULL
1260
+ );
1261
+
1262
+ CREATE TABLE IF NOT EXISTS prompt_history (
1263
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1264
+ text TEXT NOT NULL,
1265
+ created_at INTEGER NOT NULL
1266
+ );
1267
+
1268
+ CREATE TABLE IF NOT EXISTS mcp_servers (
1269
+ name TEXT PRIMARY KEY,
1270
+ transport TEXT NOT NULL,
1271
+ url TEXT,
1272
+ command TEXT,
1273
+ args TEXT,
1274
+ env TEXT,
1275
+ created_at INTEGER NOT NULL
1276
+ );
1277
+
1278
+ CREATE INDEX IF NOT EXISTS idx_messages_session
1279
+ ON messages(session_id, id);
1280
+
1281
+ CREATE INDEX IF NOT EXISTS idx_messages_turn
1282
+ ON messages(session_id, turn_index);
1283
+
1284
+ CREATE INDEX IF NOT EXISTS idx_sessions_updated
1285
+ ON sessions(updated_at DESC);
1286
+
1287
+ CREATE TABLE IF NOT EXISTS settings (
1288
+ key TEXT PRIMARY KEY,
1289
+ value TEXT NOT NULL
1290
+ );
1291
+ CREATE TABLE IF NOT EXISTS model_capabilities (
1292
+ canonical_model_id TEXT PRIMARY KEY,
1293
+ context_window INTEGER,
1294
+ reasoning INTEGER NOT NULL,
1295
+ source_provider TEXT,
1296
+ raw_json TEXT,
1297
+ updated_at INTEGER NOT NULL
1298
+ );
1299
+
1300
+ CREATE TABLE IF NOT EXISTS provider_models (
1301
+ provider TEXT NOT NULL,
1302
+ provider_model_id TEXT NOT NULL,
1303
+ display_name TEXT NOT NULL,
1304
+ canonical_model_id TEXT,
1305
+ context_window INTEGER,
1306
+ free INTEGER,
1307
+ updated_at INTEGER NOT NULL,
1308
+ PRIMARY KEY (provider, provider_model_id)
1309
+ );
1310
+
1311
+ CREATE INDEX IF NOT EXISTS idx_provider_models_provider
1312
+ ON provider_models(provider);
1313
+
1314
+ CREATE INDEX IF NOT EXISTS idx_provider_models_canonical
1315
+ ON provider_models(canonical_model_id);
1316
+
1317
+ CREATE TABLE IF NOT EXISTS model_info_state (
1318
+ key TEXT PRIMARY KEY,
1319
+ value TEXT NOT NULL
1320
+ );
1321
+
1322
+
1323
+
1324
+ CREATE TABLE IF NOT EXISTS snapshots (
1325
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1326
+ session_id TEXT NOT NULL,
1327
+ turn_index INTEGER NOT NULL,
1328
+ path TEXT NOT NULL,
1329
+ content BLOB,
1330
+ existed INTEGER NOT NULL
1331
+ );
1332
+
1333
+ CREATE INDEX IF NOT EXISTS idx_snapshots_turn
1334
+ ON snapshots(session_id, turn_index);
1335
+ `;
1336
+ var _db = null;
1337
+ function getDb() {
1338
+ if (!_db) {
1339
+ const dbPath = getDbPath();
1340
+ let db = new Database(dbPath, { create: true });
1341
+ db.exec("PRAGMA journal_mode=WAL;");
1342
+ db.exec("PRAGMA foreign_keys=ON;");
1343
+ const version = db.query("PRAGMA user_version").get()?.user_version ?? 0;
1344
+ if (version !== DB_VERSION) {
1345
+ try {
1346
+ db.close();
1347
+ } catch {}
1348
+ for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
1349
+ if (existsSync2(path))
1350
+ unlinkSync(path);
1351
+ }
1352
+ db = new Database(dbPath, { create: true });
1353
+ db.exec("PRAGMA journal_mode=WAL;");
1354
+ db.exec("PRAGMA foreign_keys=ON;");
1355
+ db.exec(SCHEMA);
1356
+ db.exec(`PRAGMA user_version = ${DB_VERSION};`);
1357
+ } else {
1358
+ db.exec(SCHEMA);
1359
+ }
1360
+ _db = db;
1361
+ }
1362
+ return _db;
1363
+ }
1364
+
1365
+ // src/session/db/model-info-repo.ts
1366
+ function listModelCapabilities() {
1367
+ return getDb().query("SELECT canonical_model_id, context_window, reasoning, source_provider, raw_json, updated_at FROM model_capabilities").all();
1368
+ }
1369
+ function replaceModelCapabilities(rows) {
1370
+ const db = getDb();
1371
+ const insertStmt = db.prepare(`INSERT INTO model_capabilities (
1372
+ canonical_model_id,
1373
+ context_window,
1374
+ reasoning,
1375
+ source_provider,
1376
+ raw_json,
1377
+ updated_at
1378
+ ) VALUES (?, ?, ?, ?, ?, ?)`);
1379
+ const run = db.transaction(() => {
1380
+ db.run("DELETE FROM model_capabilities");
1381
+ for (const row of rows) {
1382
+ insertStmt.run(row.canonical_model_id, row.context_window, row.reasoning, row.source_provider, row.raw_json, row.updated_at);
1383
+ }
1384
+ });
1385
+ run();
1386
+ }
1387
+ function listProviderModels() {
1388
+ 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();
1389
+ }
1390
+ function replaceProviderModels(provider, rows) {
1391
+ const db = getDb();
1392
+ const insertStmt = db.prepare(`INSERT INTO provider_models (
1393
+ provider,
1394
+ provider_model_id,
1395
+ display_name,
1396
+ canonical_model_id,
1397
+ context_window,
1398
+ free,
1399
+ updated_at
1400
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
1401
+ ON CONFLICT(provider, provider_model_id) DO UPDATE SET
1402
+ display_name = excluded.display_name,
1403
+ canonical_model_id = excluded.canonical_model_id,
1404
+ context_window = excluded.context_window,
1405
+ free = excluded.free,
1406
+ updated_at = excluded.updated_at`);
1407
+ const run = db.transaction(() => {
1408
+ db.run("DELETE FROM provider_models WHERE provider = ?", [provider]);
1409
+ for (const row of rows) {
1410
+ insertStmt.run(provider, row.provider_model_id, row.display_name, row.canonical_model_id, row.context_window, row.free, row.updated_at);
1411
+ }
1412
+ });
1413
+ run();
1414
+ }
1415
+ function setModelInfoState(key, value) {
1416
+ getDb().run(`INSERT INTO model_info_state (key, value) VALUES (?, ?)
1417
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`, [key, value]);
1418
+ }
1419
+ function listModelInfoState() {
1420
+ return getDb().query("SELECT key, value FROM model_info_state").all();
1421
+ }
1422
+
1423
+ // src/llm-api/model-info.ts
1424
+ var ZEN_BASE = "https://opencode.ai/zen/v1";
1425
+ var OPENAI_BASE = "https://api.openai.com";
1426
+ var ANTHROPIC_BASE = "https://api.anthropic.com";
1427
+ var GOOGLE_BASE = "https://generativelanguage.googleapis.com/v1beta";
1428
+ var MODELS_DEV_URL = "https://models.dev/api.json";
1429
+ var MODELS_DEV_SYNC_KEY = "last_models_dev_sync_at";
1430
+ var PROVIDER_SYNC_KEY_PREFIX = "last_provider_sync_at:";
1431
+ var MODEL_INFO_TTL_MS = 7 * 24 * 60 * 60 * 1000;
1432
+ var runtimeCache = emptyRuntimeCache();
1433
+ var loaded = false;
1434
+ var refreshInFlight = null;
1435
+ function emptyRuntimeCache() {
1436
+ return {
1437
+ capabilitiesByCanonical: new Map,
1438
+ providerModelsByKey: new Map,
1439
+ providerModelUniqIndex: new Map,
1440
+ matchIndex: {
1441
+ exact: new Map,
1442
+ alias: new Map
1443
+ },
1444
+ state: new Map
1445
+ };
1446
+ }
1447
+ function isRecord(value) {
1448
+ return typeof value === "object" && value !== null;
1449
+ }
1450
+ function parseModelStringLoose(modelString) {
1451
+ const slash = modelString.indexOf("/");
1452
+ if (slash === -1) {
1453
+ return { provider: null, modelId: modelString };
1454
+ }
1455
+ const provider = modelString.slice(0, slash).trim().toLowerCase();
1456
+ const modelId = modelString.slice(slash + 1);
1457
+ return { provider: provider || null, modelId };
1458
+ }
1459
+ function providerModelKey(provider, modelId) {
1460
+ return `${provider}/${modelId}`;
1461
+ }
1462
+ function basename2(value) {
1463
+ const idx = value.lastIndexOf("/");
1464
+ return idx === -1 ? value : value.slice(idx + 1);
1465
+ }
1466
+ function normalizeModelId(modelId) {
1467
+ let out = modelId.trim().toLowerCase();
1468
+ while (out.startsWith("models/")) {
1469
+ out = out.slice("models/".length);
1470
+ }
1471
+ return out;
1472
+ }
1473
+ function parseContextWindow(model) {
1474
+ const limit = model.limit;
1475
+ if (!isRecord(limit))
1476
+ return null;
1477
+ const context = limit.context;
1478
+ if (typeof context !== "number" || !Number.isFinite(context))
1479
+ return null;
1480
+ return Math.max(0, Math.trunc(context));
1481
+ }
1482
+ function parseModelsDevCapabilities(payload, updatedAt) {
1483
+ if (!isRecord(payload))
1484
+ return [];
1485
+ const merged = new Map;
1486
+ for (const [provider, providerValue] of Object.entries(payload)) {
1487
+ if (!isRecord(providerValue))
1488
+ continue;
1489
+ const models = providerValue.models;
1490
+ if (!isRecord(models))
1491
+ continue;
1492
+ for (const [modelKey, modelValue] of Object.entries(models)) {
1493
+ if (!isRecord(modelValue))
1494
+ continue;
1495
+ const explicitId = typeof modelValue.id === "string" && modelValue.id.trim().length > 0 ? modelValue.id : modelKey;
1496
+ const canonicalModelId = normalizeModelId(explicitId);
1497
+ if (!canonicalModelId)
1498
+ continue;
1499
+ const contextWindow = parseContextWindow(modelValue);
1500
+ const reasoning = modelValue.reasoning === true;
1501
+ const rawJson = JSON.stringify(modelValue);
1502
+ const prev = merged.get(canonicalModelId);
1503
+ if (!prev) {
1504
+ merged.set(canonicalModelId, {
1505
+ canonicalModelId,
1506
+ contextWindow,
1507
+ reasoning,
1508
+ sourceProvider: provider,
1509
+ rawJson
1510
+ });
1511
+ continue;
1512
+ }
1513
+ merged.set(canonicalModelId, {
1514
+ canonicalModelId,
1515
+ contextWindow: prev.contextWindow ?? contextWindow,
1516
+ reasoning: prev.reasoning || reasoning,
1517
+ sourceProvider: prev.sourceProvider,
1518
+ rawJson: prev.rawJson ?? rawJson
1519
+ });
1520
+ }
1521
+ }
1522
+ return Array.from(merged.values()).map((entry) => ({
1523
+ canonical_model_id: entry.canonicalModelId,
1524
+ context_window: entry.contextWindow,
1525
+ reasoning: entry.reasoning ? 1 : 0,
1526
+ source_provider: entry.sourceProvider,
1527
+ raw_json: entry.rawJson,
1528
+ updated_at: updatedAt
1529
+ }));
1530
+ }
1531
+ function buildModelMatchIndex(canonicalModelIds) {
1532
+ const exact = new Map;
1533
+ const aliasCandidates = new Map;
1534
+ for (const rawCanonical of canonicalModelIds) {
1535
+ const canonical = normalizeModelId(rawCanonical);
1536
+ if (!canonical)
1537
+ continue;
1538
+ exact.set(canonical, canonical);
1539
+ const short = basename2(canonical);
1540
+ if (!short)
1541
+ continue;
1542
+ let set = aliasCandidates.get(short);
1543
+ if (!set) {
1544
+ set = new Set;
1545
+ aliasCandidates.set(short, set);
1546
+ }
1547
+ set.add(canonical);
1548
+ }
1549
+ const alias = new Map;
1550
+ for (const [short, candidates] of aliasCandidates) {
1551
+ if (candidates.size === 1) {
1552
+ for (const value of candidates) {
1553
+ alias.set(short, value);
1554
+ }
1555
+ } else {
1556
+ alias.set(short, null);
1557
+ }
1558
+ }
1559
+ return { exact, alias };
1560
+ }
1561
+ function matchCanonicalModelId(providerModelId, index) {
1562
+ const normalized = normalizeModelId(providerModelId);
1563
+ if (!normalized)
1564
+ return null;
1565
+ const exactMatch = index.exact.get(normalized);
1566
+ if (exactMatch)
1567
+ return exactMatch;
1568
+ const short = basename2(normalized);
1569
+ if (!short)
1570
+ return null;
1571
+ const alias = index.alias.get(short);
1572
+ return alias ?? null;
1573
+ }
1574
+ function isStaleTimestamp(timestamp, now = Date.now(), ttlMs = MODEL_INFO_TTL_MS) {
1575
+ if (timestamp === null)
1576
+ return true;
1577
+ return now - timestamp > ttlMs;
1578
+ }
1579
+ function buildRuntimeCache(capabilityRows, providerRows, stateRows) {
1580
+ const capabilitiesByCanonical = new Map;
1581
+ for (const row of capabilityRows) {
1582
+ const canonical = normalizeModelId(row.canonical_model_id);
1583
+ if (!canonical)
1584
+ continue;
1585
+ capabilitiesByCanonical.set(canonical, {
1586
+ canonicalModelId: canonical,
1587
+ contextWindow: row.context_window,
1588
+ reasoning: row.reasoning === 1,
1589
+ sourceProvider: row.source_provider
1590
+ });
1591
+ }
1592
+ const providerModelsByKey = new Map;
1593
+ const providerModelUniqIndex = new Map;
1594
+ for (const row of providerRows) {
1595
+ const provider = row.provider.trim().toLowerCase();
1596
+ const providerModelId = normalizeModelId(row.provider_model_id);
1597
+ if (!provider || !providerModelId)
1598
+ continue;
1599
+ const key = providerModelKey(provider, providerModelId);
1600
+ providerModelsByKey.set(key, {
1601
+ provider,
1602
+ providerModelId,
1603
+ displayName: row.display_name,
1604
+ canonicalModelId: row.canonical_model_id ? normalizeModelId(row.canonical_model_id) : null,
1605
+ contextWindow: row.context_window,
1606
+ free: row.free === 1
1607
+ });
1608
+ const prev = providerModelUniqIndex.get(providerModelId);
1609
+ if (prev === undefined) {
1610
+ providerModelUniqIndex.set(providerModelId, key);
1611
+ } else if (prev !== key) {
1612
+ providerModelUniqIndex.set(providerModelId, null);
1613
+ }
1614
+ }
1615
+ const matchIndex = buildModelMatchIndex(capabilitiesByCanonical.keys());
1616
+ const state = new Map;
1617
+ for (const row of stateRows) {
1618
+ state.set(row.key, row.value);
1619
+ }
1620
+ return {
1621
+ capabilitiesByCanonical,
1622
+ providerModelsByKey,
1623
+ providerModelUniqIndex,
1624
+ matchIndex,
1625
+ state
1626
+ };
1627
+ }
1628
+ function loadCacheFromDb() {
1629
+ runtimeCache = buildRuntimeCache(listModelCapabilities(), listProviderModels(), listModelInfoState());
1630
+ loaded = true;
1631
+ }
1632
+ function ensureLoaded() {
1633
+ if (!loaded)
1634
+ loadCacheFromDb();
1635
+ }
1636
+ function initModelInfoCache() {
1637
+ loadCacheFromDb();
1638
+ }
1639
+ function parseStateInt(key) {
1640
+ const raw = runtimeCache.state.get(key);
1641
+ if (!raw)
1642
+ return null;
1643
+ const value = Number.parseInt(raw, 10);
1644
+ if (!Number.isFinite(value))
1645
+ return null;
1646
+ return value;
1647
+ }
1648
+ function getRemoteProvidersFromEnv(env) {
1649
+ const providers = [];
1650
+ if (env.OPENCODE_API_KEY)
1651
+ providers.push("zen");
1652
+ if (env.OPENAI_API_KEY)
1653
+ providers.push("openai");
1654
+ if (env.ANTHROPIC_API_KEY)
1655
+ providers.push("anthropic");
1656
+ if (env.GOOGLE_API_KEY ?? env.GEMINI_API_KEY)
1657
+ providers.push("google");
1658
+ return providers;
1659
+ }
1660
+ function getProvidersToRefreshFromEnv(env) {
1661
+ return [...getRemoteProvidersFromEnv(env), "ollama"];
1662
+ }
1663
+ function getVisibleProvidersForSnapshotFromEnv(env) {
1664
+ return new Set(getProvidersToRefreshFromEnv(env));
1665
+ }
1666
+ function getConfiguredProvidersForSync() {
1667
+ return getProvidersToRefreshFromEnv(process.env);
1668
+ }
1669
+ function getProvidersRequiredForFreshness() {
1670
+ return getRemoteProvidersFromEnv(process.env);
1671
+ }
1672
+ function getProviderSyncKey(provider) {
1673
+ return `${PROVIDER_SYNC_KEY_PREFIX}${provider}`;
1674
+ }
1675
+ function isModelInfoStale(now = Date.now()) {
1676
+ ensureLoaded();
1677
+ if (isStaleTimestamp(parseStateInt(MODELS_DEV_SYNC_KEY), now))
1678
+ return true;
1679
+ for (const provider of getProvidersRequiredForFreshness()) {
1680
+ const providerSync = parseStateInt(getProviderSyncKey(provider));
1681
+ if (isStaleTimestamp(providerSync, now))
1682
+ return true;
1683
+ }
1684
+ return false;
1685
+ }
1686
+ function getLastSyncAt() {
1687
+ let latest = parseStateInt(MODELS_DEV_SYNC_KEY);
1688
+ for (const provider of getProvidersRequiredForFreshness()) {
1689
+ const value = parseStateInt(getProviderSyncKey(provider));
1690
+ if (value !== null && (latest === null || value > latest))
1691
+ latest = value;
1692
+ }
1693
+ return latest;
1694
+ }
1695
+ async function fetchJson(url, init, timeoutMs) {
1696
+ try {
1697
+ const response = await fetch(url, {
1698
+ ...init,
1699
+ signal: AbortSignal.timeout(timeoutMs)
1700
+ });
1701
+ if (!response.ok)
1702
+ return null;
1703
+ return await response.json();
1704
+ } catch {
1705
+ return null;
1706
+ }
1707
+ }
1708
+ async function fetchModelsDevPayload() {
1709
+ return fetchJson(MODELS_DEV_URL, {}, 1e4);
1710
+ }
1711
+ async function fetchZenModels() {
1712
+ const key = process.env.OPENCODE_API_KEY;
1713
+ if (!key)
1714
+ return null;
1715
+ const payload = await fetchJson(`${ZEN_BASE}/models`, { headers: { Authorization: `Bearer ${key}` } }, 8000);
1716
+ if (!isRecord(payload))
1717
+ return null;
1718
+ const data = payload.data;
1719
+ if (!Array.isArray(data))
1720
+ return null;
1721
+ const out = [];
1722
+ for (const item of data) {
1723
+ if (!isRecord(item) || typeof item.id !== "string")
1724
+ continue;
1725
+ const modelId = normalizeModelId(item.id);
1726
+ if (!modelId)
1727
+ continue;
1728
+ const contextWindow = typeof item.context_window === "number" && Number.isFinite(item.context_window) ? Math.max(0, Math.trunc(item.context_window)) : null;
1729
+ out.push({
1730
+ providerModelId: modelId,
1731
+ displayName: item.id,
1732
+ contextWindow,
1733
+ free: item.id.endsWith("-free") || item.id === "gpt-5-nano" || item.id === "big-pickle"
1734
+ });
1735
+ }
1736
+ return out;
1737
+ }
1738
+ async function fetchOpenAIModels() {
1739
+ const key = process.env.OPENAI_API_KEY;
1740
+ if (!key)
1741
+ return null;
1742
+ const payload = await fetchJson(`${OPENAI_BASE}/v1/models`, { headers: { Authorization: `Bearer ${key}` } }, 6000);
1743
+ if (!isRecord(payload) || !Array.isArray(payload.data))
1744
+ return null;
1745
+ const out = [];
1746
+ for (const item of payload.data) {
1747
+ if (!isRecord(item) || typeof item.id !== "string")
1748
+ continue;
1749
+ const modelId = normalizeModelId(item.id);
1750
+ if (!modelId)
1751
+ continue;
1752
+ out.push({
1753
+ providerModelId: modelId,
1754
+ displayName: item.id,
1755
+ contextWindow: null,
1756
+ free: false
1757
+ });
1758
+ }
1759
+ return out;
1760
+ }
1761
+ async function fetchAnthropicModels() {
1762
+ const key = process.env.ANTHROPIC_API_KEY;
1763
+ if (!key)
1764
+ return null;
1765
+ const payload = await fetchJson(`${ANTHROPIC_BASE}/v1/models`, {
1766
+ headers: {
1767
+ "x-api-key": key,
1768
+ "anthropic-version": "2023-06-01"
1769
+ }
1770
+ }, 6000);
1771
+ if (!isRecord(payload) || !Array.isArray(payload.data))
1772
+ return null;
1773
+ const out = [];
1774
+ for (const item of payload.data) {
1775
+ if (!isRecord(item) || typeof item.id !== "string")
1776
+ continue;
1777
+ const modelId = normalizeModelId(item.id);
1778
+ if (!modelId)
1779
+ continue;
1780
+ const displayName = typeof item.display_name === "string" && item.display_name.trim().length > 0 ? item.display_name : item.id;
1781
+ out.push({
1782
+ providerModelId: modelId,
1783
+ displayName,
1784
+ contextWindow: null,
1785
+ free: false
1786
+ });
1787
+ }
1788
+ return out;
1789
+ }
1790
+ async function fetchGoogleModels() {
1791
+ const key = process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
1792
+ if (!key)
1793
+ return null;
1794
+ const payload = await fetchJson(`${GOOGLE_BASE}/models?key=${encodeURIComponent(key)}`, {}, 6000);
1795
+ if (!isRecord(payload) || !Array.isArray(payload.models))
1796
+ return null;
1797
+ const out = [];
1798
+ for (const item of payload.models) {
1799
+ if (!isRecord(item) || typeof item.name !== "string")
1800
+ continue;
1801
+ const modelId = normalizeModelId(item.name);
1802
+ if (!modelId)
1803
+ continue;
1804
+ const displayName = typeof item.displayName === "string" && item.displayName.trim().length > 0 ? item.displayName : modelId;
1805
+ const contextWindow = typeof item.inputTokenLimit === "number" && Number.isFinite(item.inputTokenLimit) ? Math.max(0, Math.trunc(item.inputTokenLimit)) : null;
1806
+ out.push({
1807
+ providerModelId: modelId,
1808
+ displayName,
1809
+ contextWindow,
1810
+ free: false
1811
+ });
1812
+ }
1813
+ return out;
1814
+ }
1815
+ async function fetchOllamaModels() {
1816
+ const base = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
1817
+ const payload = await fetchJson(`${base}/api/tags`, {}, 3000);
1818
+ if (!isRecord(payload) || !Array.isArray(payload.models))
1819
+ return null;
1820
+ const out = [];
1821
+ for (const item of payload.models) {
1822
+ if (!isRecord(item) || typeof item.name !== "string")
1823
+ continue;
1824
+ const modelId = normalizeModelId(item.name);
1825
+ if (!modelId)
1826
+ continue;
1827
+ const details = item.details;
1828
+ let sizeSuffix = "";
1829
+ if (isRecord(details) && typeof details.parameter_size === "string") {
1830
+ sizeSuffix = ` (${details.parameter_size})`;
1831
+ }
1832
+ out.push({
1833
+ providerModelId: modelId,
1834
+ displayName: `${item.name}${sizeSuffix}`,
1835
+ contextWindow: null,
1836
+ free: false
1837
+ });
1838
+ }
1839
+ return out;
1840
+ }
1841
+ async function fetchProviderCandidates(provider) {
1842
+ switch (provider) {
1843
+ case "zen":
1844
+ return fetchZenModels();
1845
+ case "openai":
1846
+ return fetchOpenAIModels();
1847
+ case "anthropic":
1848
+ return fetchAnthropicModels();
1849
+ case "google":
1850
+ return fetchGoogleModels();
1851
+ case "ollama":
1852
+ return fetchOllamaModels();
1853
+ default:
1854
+ return null;
1855
+ }
1856
+ }
1857
+ function providerRowsFromCandidates(candidates, matchIndex, updatedAt) {
1858
+ return candidates.map((candidate) => ({
1859
+ provider_model_id: candidate.providerModelId,
1860
+ display_name: candidate.displayName,
1861
+ canonical_model_id: matchCanonicalModelId(candidate.providerModelId, matchIndex),
1862
+ context_window: candidate.contextWindow,
1863
+ free: candidate.free ? 1 : 0,
1864
+ updated_at: updatedAt
1865
+ }));
1866
+ }
1867
+ async function refreshModelInfoInternal() {
1868
+ ensureLoaded();
1869
+ const now = Date.now();
1870
+ const providers = getConfiguredProvidersForSync();
1871
+ const providerResults = await Promise.all(providers.map(async (provider) => ({
1872
+ provider,
1873
+ candidates: await fetchProviderCandidates(provider)
1874
+ })));
1875
+ const modelsDevPayload = await fetchModelsDevPayload();
1876
+ let matchIndex = runtimeCache.matchIndex;
1877
+ if (modelsDevPayload !== null) {
1878
+ const capabilityRows = parseModelsDevCapabilities(modelsDevPayload, now);
1879
+ if (capabilityRows.length > 0) {
1880
+ replaceModelCapabilities(capabilityRows);
1881
+ setModelInfoState(MODELS_DEV_SYNC_KEY, String(now));
1882
+ matchIndex = buildModelMatchIndex(capabilityRows.map((row) => row.canonical_model_id));
1883
+ }
1884
+ }
1885
+ for (const result of providerResults) {
1886
+ if (result.candidates === null)
1887
+ continue;
1888
+ const rows = providerRowsFromCandidates(result.candidates, matchIndex, now);
1889
+ replaceProviderModels(result.provider, rows);
1890
+ setModelInfoState(getProviderSyncKey(result.provider), String(now));
1891
+ }
1892
+ loadCacheFromDb();
1893
+ }
1894
+ function refreshModelInfoInBackground(opts) {
1895
+ ensureLoaded();
1896
+ const force = opts?.force ?? false;
1897
+ if (!force && !isModelInfoStale())
1898
+ return Promise.resolve();
1899
+ if (refreshInFlight)
1900
+ return refreshInFlight;
1901
+ refreshInFlight = refreshModelInfoInternal().finally(() => {
1902
+ refreshInFlight = null;
1903
+ });
1904
+ return refreshInFlight;
1905
+ }
1906
+ function isModelInfoRefreshing() {
1907
+ return refreshInFlight !== null;
1908
+ }
1909
+ function resolveFromProviderRow(row, cache) {
1910
+ if (row.canonicalModelId) {
1911
+ const capability = cache.capabilitiesByCanonical.get(row.canonicalModelId);
1912
+ if (capability) {
1913
+ return {
1914
+ canonicalModelId: capability.canonicalModelId,
1915
+ contextWindow: capability.contextWindow,
1916
+ reasoning: capability.reasoning
1917
+ };
1918
+ }
1919
+ }
1920
+ return {
1921
+ canonicalModelId: row.canonicalModelId,
1922
+ contextWindow: row.contextWindow,
1923
+ reasoning: false
1924
+ };
1925
+ }
1926
+ function resolveModelInfoInCache(modelString, cache) {
1927
+ const parsed = parseModelStringLoose(modelString);
1928
+ const normalizedModelId = normalizeModelId(parsed.modelId);
1929
+ if (!normalizedModelId)
1930
+ return null;
1931
+ if (parsed.provider) {
1932
+ const providerRow = cache.providerModelsByKey.get(providerModelKey(parsed.provider, normalizedModelId));
1933
+ if (providerRow)
1934
+ return resolveFromProviderRow(providerRow, cache);
1935
+ }
1936
+ const canonical = matchCanonicalModelId(normalizedModelId, cache.matchIndex);
1937
+ if (canonical) {
1938
+ const capability = cache.capabilitiesByCanonical.get(canonical);
1939
+ if (capability) {
1940
+ return {
1941
+ canonicalModelId: capability.canonicalModelId,
1942
+ contextWindow: capability.contextWindow,
1943
+ reasoning: capability.reasoning
1944
+ };
1945
+ }
1946
+ }
1947
+ if (!parsed.provider) {
1948
+ const uniqueProviderKey = cache.providerModelUniqIndex.get(normalizedModelId);
1949
+ if (uniqueProviderKey) {
1950
+ const providerRow = cache.providerModelsByKey.get(uniqueProviderKey);
1951
+ if (providerRow)
1952
+ return resolveFromProviderRow(providerRow, cache);
1953
+ }
1954
+ }
1955
+ return null;
1956
+ }
1957
+ function resolveModelInfo(modelString) {
1958
+ ensureLoaded();
1959
+ return resolveModelInfoInCache(modelString, runtimeCache);
1960
+ }
1961
+ function getContextWindow(modelString) {
1962
+ return resolveModelInfo(modelString)?.contextWindow ?? null;
1963
+ }
1964
+ function supportsThinking(modelString) {
1965
+ return resolveModelInfo(modelString)?.reasoning ?? false;
1966
+ }
1967
+ function readLiveModelsFromCache() {
1968
+ const models = [];
1969
+ const visibleProviders = getVisibleProvidersForSnapshotFromEnv(process.env);
1970
+ for (const row of runtimeCache.providerModelsByKey.values()) {
1971
+ if (!visibleProviders.has(row.provider))
1972
+ continue;
1973
+ const info = resolveFromProviderRow(row, runtimeCache);
1974
+ models.push({
1975
+ id: `${row.provider}/${row.providerModelId}`,
1976
+ displayName: row.displayName,
1977
+ provider: row.provider,
1978
+ context: info.contextWindow ?? undefined,
1979
+ free: row.free ? true : undefined
1980
+ });
1981
+ }
1982
+ models.sort((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
1983
+ return models;
1984
+ }
1985
+ async function fetchAvailableModelsSnapshot() {
1986
+ ensureLoaded();
1987
+ if (isModelInfoStale() && !isModelInfoRefreshing()) {
1988
+ if (runtimeCache.providerModelsByKey.size === 0) {
1989
+ await refreshModelInfoInBackground({ force: true });
1990
+ } else {
1991
+ refreshModelInfoInBackground();
1992
+ }
1993
+ }
1994
+ return {
1995
+ models: readLiveModelsFromCache(),
1996
+ stale: isModelInfoStale(),
1997
+ refreshing: isModelInfoRefreshing(),
1998
+ lastSyncAt: getLastSyncAt()
1999
+ };
2000
+ }
2001
+
1210
2002
  // src/llm-api/providers.ts
1211
2003
  function getFetchWithLogging() {
1212
2004
  const customFetch = async (input, init) => {
@@ -1233,7 +2025,7 @@ function getFetchWithLogging() {
1233
2025
  };
1234
2026
  return customFetch;
1235
2027
  }
1236
- var ZEN_BASE = "https://opencode.ai/zen/v1";
2028
+ var ZEN_BASE2 = "https://opencode.ai/zen/v1";
1237
2029
  function zenEndpointFor(modelId) {
1238
2030
  if (modelId.startsWith("claude-"))
1239
2031
  return zenAnthropic()(modelId);
@@ -1258,7 +2050,7 @@ function zenAnthropic() {
1258
2050
  _zenAnthropic = createAnthropic({
1259
2051
  fetch: getFetchWithLogging(),
1260
2052
  apiKey: getZenApiKey(),
1261
- baseURL: ZEN_BASE
2053
+ baseURL: ZEN_BASE2
1262
2054
  });
1263
2055
  }
1264
2056
  return _zenAnthropic;
@@ -1268,7 +2060,7 @@ function zenOpenAI() {
1268
2060
  _zenOpenAI = createOpenAI({
1269
2061
  fetch: getFetchWithLogging(),
1270
2062
  apiKey: getZenApiKey(),
1271
- baseURL: ZEN_BASE
2063
+ baseURL: ZEN_BASE2
1272
2064
  });
1273
2065
  }
1274
2066
  return _zenOpenAI;
@@ -1278,7 +2070,7 @@ function zenGoogle() {
1278
2070
  _zenGoogle = createGoogleGenerativeAI({
1279
2071
  fetch: getFetchWithLogging(),
1280
2072
  apiKey: getZenApiKey(),
1281
- baseURL: ZEN_BASE
2073
+ baseURL: ZEN_BASE2
1282
2074
  });
1283
2075
  }
1284
2076
  return _zenGoogle;
@@ -1289,7 +2081,7 @@ function zenCompat() {
1289
2081
  fetch: getFetchWithLogging(),
1290
2082
  name: "zen-compat",
1291
2083
  apiKey: getZenApiKey(),
1292
- baseURL: ZEN_BASE
2084
+ baseURL: ZEN_BASE2
1293
2085
  });
1294
2086
  }
1295
2087
  return _zenCompat;
@@ -1339,31 +2131,8 @@ function parseModelString(modelString) {
1339
2131
  modelId: modelString.slice(slashIdx + 1)
1340
2132
  };
1341
2133
  }
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));
2134
+ function supportsThinking2(modelString) {
2135
+ return supportsThinking(modelString);
1367
2136
  }
1368
2137
  var ANTHROPIC_BUDGET = {
1369
2138
  low: 4096,
@@ -1378,7 +2147,7 @@ function clampEffort(effort, max) {
1378
2147
  return ORDER[Math.min(i, m)];
1379
2148
  }
1380
2149
  function getThinkingProviderOptions(modelString, effort) {
1381
- if (!supportsThinking(modelString))
2150
+ if (!supportsThinking2(modelString))
1382
2151
  return null;
1383
2152
  const { provider, modelId } = parseModelString(modelString);
1384
2153
  if (provider === "anthropic" || provider === "zen" && modelId.startsWith("claude-")) {
@@ -1430,13 +2199,8 @@ function getThinkingProviderOptions(modelString, effort) {
1430
2199
  }
1431
2200
  return null;
1432
2201
  }
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;
2202
+ function getContextWindow2(modelString) {
2203
+ return getContextWindow(modelString);
1440
2204
  }
1441
2205
  function resolveModel(modelString) {
1442
2206
  const slashIdx = modelString.indexOf("/");
@@ -1464,229 +2228,79 @@ function resolveModel(modelString) {
1464
2228
  return ollamaProvider(modelId);
1465
2229
  }
1466
2230
  default:
1467
- throw new Error(`Unknown provider "${provider}". Supported: zen, anthropic, openai, google, ollama`);
1468
- }
1469
- }
1470
- function autoDiscoverModel() {
1471
- if (process.env.OPENCODE_API_KEY)
1472
- return "zen/claude-sonnet-4-6";
1473
- if (process.env.ANTHROPIC_API_KEY)
1474
- return "anthropic/claude-sonnet-4-5-20250929";
1475
- if (process.env.OPENAI_API_KEY)
1476
- return "openai/gpt-4o";
1477
- if (process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY)
1478
- return "google/gemini-2.0-flash";
1479
- return "ollama/llama3.2";
1480
- }
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
- async function fetchAvailableModels() {
1524
- const [zen, ollama] = await Promise.all([
1525
- fetchZenModels(),
1526
- fetchOllamaModels()
1527
- ]);
1528
- return [...zen, ...ollama];
1529
- }
1530
-
1531
- // src/mcp/client.ts
1532
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1533
- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
1534
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1535
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1536
- async function connectMcpServer(config) {
1537
- const client = new Client({ name: "mini-coder", version: "0.1.0" });
1538
- if (config.transport === "http") {
1539
- if (!config.url) {
1540
- throw new Error(`MCP server "${config.name}" requires a url`);
1541
- }
1542
- const url = new URL(config.url);
1543
- let transport;
1544
- try {
1545
- const streamable = new StreamableHTTPClientTransport(url);
1546
- transport = streamable;
1547
- await client.connect(transport);
1548
- } catch {
1549
- transport = new SSEClientTransport(url);
1550
- await client.connect(transport);
1551
- }
1552
- } else if (config.transport === "stdio") {
1553
- if (!config.command) {
1554
- throw new Error(`MCP server "${config.name}" requires a command`);
1555
- }
1556
- const stdioParams = config.env ? { command: config.command, args: config.args ?? [], env: config.env } : { command: config.command, args: config.args ?? [] };
1557
- const transport = new StdioClientTransport(stdioParams);
1558
- await client.connect(transport);
1559
- } else {
1560
- throw new Error(`Unknown MCP transport: ${config.transport}`);
1561
- }
1562
- const { tools: mcpTools } = await client.listTools();
1563
- const tools = mcpTools.map((t) => ({
1564
- name: `mcp_${config.name}_${t.name}`,
1565
- description: `[MCP:${config.name}] ${t.description ?? t.name}`,
1566
- schema: t.inputSchema,
1567
- execute: async (input) => {
1568
- const result = await client.callTool({
1569
- name: t.name,
1570
- arguments: input
1571
- });
1572
- if (result.isError) {
1573
- const content = result.content;
1574
- const errText = content.filter((b) => b.type === "text").map((b) => b.text ?? "").join(`
1575
- `);
1576
- throw new Error(errText || "MCP tool returned an error");
1577
- }
1578
- return result.content;
1579
- }
1580
- }));
1581
- return {
1582
- name: config.name,
1583
- tools,
1584
- close: () => client.close()
1585
- };
1586
- }
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
- );
2231
+ throw new Error(`Unknown provider "${provider}". Supported: zen, anthropic, openai, google, ollama`);
2232
+ }
2233
+ }
2234
+ function autoDiscoverModel() {
2235
+ if (process.env.OPENCODE_API_KEY)
2236
+ return "zen/claude-sonnet-4-6";
2237
+ if (process.env.ANTHROPIC_API_KEY)
2238
+ return "anthropic/claude-sonnet-4-5-20250929";
2239
+ if (process.env.OPENAI_API_KEY)
2240
+ return "openai/gpt-4o";
2241
+ if (process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY)
2242
+ return "google/gemini-2.0-flash";
2243
+ return "ollama/llama3.2";
2244
+ }
2245
+ async function fetchAvailableModels() {
2246
+ return fetchAvailableModelsSnapshot();
2247
+ }
1659
2248
 
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);
2249
+ // src/mcp/client.ts
2250
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2251
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
2252
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2253
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
2254
+ async function connectMcpServer(config) {
2255
+ const client = new Client({ name: "mini-coder", version: "0.1.0" });
2256
+ if (config.transport === "http") {
2257
+ if (!config.url) {
2258
+ throw new Error(`MCP server "${config.name}" requires a url`);
1686
2259
  }
1687
- _db = db;
2260
+ const url = new URL(config.url);
2261
+ let transport;
2262
+ try {
2263
+ const streamable = new StreamableHTTPClientTransport(url);
2264
+ transport = streamable;
2265
+ await client.connect(transport);
2266
+ } catch {
2267
+ transport = new SSEClientTransport(url);
2268
+ await client.connect(transport);
2269
+ }
2270
+ } else if (config.transport === "stdio") {
2271
+ if (!config.command) {
2272
+ throw new Error(`MCP server "${config.name}" requires a command`);
2273
+ }
2274
+ const stdioParams = config.env ? { command: config.command, args: config.args ?? [], env: config.env } : { command: config.command, args: config.args ?? [] };
2275
+ const transport = new StdioClientTransport(stdioParams);
2276
+ await client.connect(transport);
2277
+ } else {
2278
+ throw new Error(`Unknown MCP transport: ${config.transport}`);
1688
2279
  }
1689
- return _db;
2280
+ const { tools: mcpTools } = await client.listTools();
2281
+ const tools = mcpTools.map((t) => ({
2282
+ name: `mcp_${config.name}_${t.name}`,
2283
+ description: `[MCP:${config.name}] ${t.description ?? t.name}`,
2284
+ schema: t.inputSchema,
2285
+ execute: async (input) => {
2286
+ const result = await client.callTool({
2287
+ name: t.name,
2288
+ arguments: input
2289
+ });
2290
+ if (result.isError) {
2291
+ const content = result.content;
2292
+ const errText = content.filter((b) => b.type === "text").map((b) => b.text ?? "").join(`
2293
+ `);
2294
+ throw new Error(errText || "MCP tool returned an error");
2295
+ }
2296
+ return result.content;
2297
+ }
2298
+ }));
2299
+ return {
2300
+ name: config.name,
2301
+ tools,
2302
+ close: () => client.close()
2303
+ };
1690
2304
  }
1691
2305
  // src/session/db/session-repo.ts
1692
2306
  function createSession(opts) {
@@ -1848,6 +2462,10 @@ function deleteSnapshot(sessionId, turnIndex) {
1848
2462
  function deleteAllSnapshots(sessionId) {
1849
2463
  getDb().run("DELETE FROM snapshots WHERE session_id = ?", [sessionId]);
1850
2464
  }
2465
+ // src/agent/subagent-runner.ts
2466
+ import { tmpdir as tmpdir2 } from "os";
2467
+ import { join as join15 } from "path";
2468
+
1851
2469
  // src/llm-api/turn.ts
1852
2470
  import { dynamicTool, jsonSchema, stepCountIs, streamText } from "ai";
1853
2471
  import { z } from "zod";
@@ -2031,17 +2649,205 @@ async function* runTurn(options) {
2031
2649
  }
2032
2650
  }
2033
2651
 
2652
+ // src/tools/worktree.ts
2653
+ import {
2654
+ chmodSync,
2655
+ copyFileSync,
2656
+ existsSync as existsSync3,
2657
+ lstatSync,
2658
+ mkdirSync as mkdirSync4,
2659
+ mkdtempSync,
2660
+ readlinkSync,
2661
+ rmSync,
2662
+ symlinkSync,
2663
+ writeFileSync
2664
+ } from "fs";
2665
+ import { tmpdir } from "os";
2666
+ import { dirname, join as join5 } from "path";
2667
+ async function runGit(cwd, args) {
2668
+ try {
2669
+ const proc = Bun.spawn(["git", ...args], {
2670
+ cwd,
2671
+ stdout: "pipe",
2672
+ stderr: "pipe"
2673
+ });
2674
+ const [stdout, stderr, exitCode] = await Promise.all([
2675
+ new Response(proc.stdout).text(),
2676
+ new Response(proc.stderr).text(),
2677
+ proc.exited
2678
+ ]);
2679
+ return { stdout, stderr, exitCode };
2680
+ } catch {
2681
+ return { stdout: "", stderr: "failed to execute git", exitCode: -1 };
2682
+ }
2683
+ }
2684
+ function gitError(action, detail) {
2685
+ return new Error(`${action}: ${detail || "unknown git error"}`);
2686
+ }
2687
+ function splitNonEmptyLines(text) {
2688
+ return text.split(`
2689
+ `).map((line) => line.trim()).filter((line) => line.length > 0);
2690
+ }
2691
+ async function listUnmergedFiles(cwd) {
2692
+ const conflictResult = await runGit(cwd, [
2693
+ "diff",
2694
+ "--name-only",
2695
+ "--diff-filter=U"
2696
+ ]);
2697
+ if (conflictResult.exitCode !== 0)
2698
+ return [];
2699
+ return splitNonEmptyLines(conflictResult.stdout);
2700
+ }
2701
+ async function hasMergeInProgress(cwd) {
2702
+ const mergeHead = await runGit(cwd, [
2703
+ "rev-parse",
2704
+ "-q",
2705
+ "--verify",
2706
+ "MERGE_HEAD"
2707
+ ]);
2708
+ return mergeHead.exitCode === 0;
2709
+ }
2710
+ async function isGitRepo(cwd) {
2711
+ const result = await runGit(cwd, ["rev-parse", "--git-dir"]);
2712
+ return result.exitCode === 0;
2713
+ }
2714
+ function splitNullSeparated(text) {
2715
+ return text.split("\x00").filter((value) => value.length > 0);
2716
+ }
2717
+ async function getRepoRoot(cwd) {
2718
+ const result = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
2719
+ if (result.exitCode !== 0) {
2720
+ throw gitError("Failed to resolve repository root", (result.stderr || result.stdout).trim());
2721
+ }
2722
+ return result.stdout.trim();
2723
+ }
2724
+ async function applyPatch(cwd, patch, args) {
2725
+ if (patch.trim().length === 0)
2726
+ return;
2727
+ const tempDir = mkdtempSync(join5(tmpdir(), "mc-worktree-patch-"));
2728
+ const patchPath = join5(tempDir, "changes.patch");
2729
+ try {
2730
+ writeFileSync(patchPath, patch);
2731
+ const result = await runGit(cwd, ["apply", ...args, patchPath]);
2732
+ if (result.exitCode !== 0) {
2733
+ throw gitError("Failed to apply dirty-state patch to worktree", (result.stderr || result.stdout).trim());
2734
+ }
2735
+ } finally {
2736
+ rmSync(tempDir, { recursive: true, force: true });
2737
+ }
2738
+ }
2739
+ function copyUntrackedPath(source, destination) {
2740
+ const stat = lstatSync(source);
2741
+ mkdirSync4(dirname(destination), { recursive: true });
2742
+ if (stat.isSymbolicLink()) {
2743
+ rmSync(destination, { recursive: true, force: true });
2744
+ symlinkSync(readlinkSync(source), destination);
2745
+ return;
2746
+ }
2747
+ copyFileSync(source, destination);
2748
+ chmodSync(destination, stat.mode);
2749
+ }
2750
+ function copyFileIfMissing(source, destination) {
2751
+ if (!existsSync3(source) || existsSync3(destination))
2752
+ return;
2753
+ mkdirSync4(dirname(destination), { recursive: true });
2754
+ copyFileSync(source, destination);
2755
+ }
2756
+ function linkDirectoryIfMissing(source, destination) {
2757
+ if (!existsSync3(source) || existsSync3(destination))
2758
+ return;
2759
+ mkdirSync4(dirname(destination), { recursive: true });
2760
+ symlinkSync(source, destination, process.platform === "win32" ? "junction" : "dir");
2761
+ }
2762
+ async function initializeWorktree(mainCwd, worktreeCwd) {
2763
+ const [mainRoot, worktreeRoot] = await Promise.all([
2764
+ getRepoRoot(mainCwd),
2765
+ getRepoRoot(worktreeCwd)
2766
+ ]);
2767
+ if (!existsSync3(join5(mainRoot, "package.json")))
2768
+ return;
2769
+ for (const lockfile of ["bun.lock", "bun.lockb"]) {
2770
+ copyFileIfMissing(join5(mainRoot, lockfile), join5(worktreeRoot, lockfile));
2771
+ }
2772
+ linkDirectoryIfMissing(join5(mainRoot, "node_modules"), join5(worktreeRoot, "node_modules"));
2773
+ }
2774
+ async function syncDirtyStateToWorktree(mainCwd, worktreeCwd) {
2775
+ const [staged, unstaged, untracked, mainRoot, worktreeRoot] = await Promise.all([
2776
+ runGit(mainCwd, ["diff", "--binary", "--cached"]),
2777
+ runGit(mainCwd, ["diff", "--binary"]),
2778
+ runGit(mainCwd, ["ls-files", "--others", "--exclude-standard", "-z"]),
2779
+ getRepoRoot(mainCwd),
2780
+ getRepoRoot(worktreeCwd)
2781
+ ]);
2782
+ if (staged.exitCode !== 0) {
2783
+ throw gitError("Failed to read staged changes", (staged.stderr || staged.stdout).trim());
2784
+ }
2785
+ if (unstaged.exitCode !== 0) {
2786
+ throw gitError("Failed to read unstaged changes", (unstaged.stderr || unstaged.stdout).trim());
2787
+ }
2788
+ if (untracked.exitCode !== 0) {
2789
+ throw gitError("Failed to list untracked files", (untracked.stderr || untracked.stdout).trim());
2790
+ }
2791
+ await applyPatch(worktreeRoot, staged.stdout, ["--index"]);
2792
+ await applyPatch(worktreeRoot, unstaged.stdout, []);
2793
+ for (const relPath of splitNullSeparated(untracked.stdout)) {
2794
+ copyUntrackedPath(join5(mainRoot, relPath), join5(worktreeRoot, relPath));
2795
+ }
2796
+ }
2797
+ async function createWorktree(mainCwd, branch, path) {
2798
+ const result = await runGit(mainCwd, ["worktree", "add", path, "-b", branch]);
2799
+ if (result.exitCode !== 0) {
2800
+ throw gitError(`Failed to create worktree for branch "${branch}"`, (result.stderr || result.stdout).trim());
2801
+ }
2802
+ return path;
2803
+ }
2804
+
2805
+ class MergeInProgressError extends Error {
2806
+ conflictFiles;
2807
+ constructor(branch, conflictFiles) {
2808
+ super(`Cannot merge branch "${branch}" because another merge is already in progress. Resolve it first before merging this branch.`);
2809
+ this.name = "MergeInProgressError";
2810
+ this.conflictFiles = conflictFiles;
2811
+ }
2812
+ }
2813
+ async function mergeWorktree(mainCwd, branch) {
2814
+ if (await hasMergeInProgress(mainCwd)) {
2815
+ const conflictFiles2 = await listUnmergedFiles(mainCwd);
2816
+ throw new MergeInProgressError(branch, conflictFiles2);
2817
+ }
2818
+ const merge = await runGit(mainCwd, ["merge", "--no-ff", branch]);
2819
+ if (merge.exitCode === 0)
2820
+ return { success: true };
2821
+ const conflictFiles = await listUnmergedFiles(mainCwd);
2822
+ if (conflictFiles.length > 0) {
2823
+ return { success: false, conflictFiles };
2824
+ }
2825
+ throw gitError(`Failed to merge branch "${branch}"`, (merge.stderr || merge.stdout).trim());
2826
+ }
2827
+ async function removeWorktree(mainCwd, path) {
2828
+ const result = await runGit(mainCwd, ["worktree", "remove", "--force", path]);
2829
+ if (result.exitCode !== 0) {
2830
+ throw gitError(`Failed to remove worktree "${path}"`, (result.stderr || result.stdout).trim());
2831
+ }
2832
+ }
2833
+ async function cleanupBranch(mainCwd, branch) {
2834
+ const result = await runGit(mainCwd, ["branch", "-D", branch]);
2835
+ if (result.exitCode !== 0) {
2836
+ throw gitError(`Failed to delete branch "${branch}"`, (result.stderr || result.stdout).trim());
2837
+ }
2838
+ }
2839
+
2034
2840
  // src/agent/system-prompt.ts
2035
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2036
- import { join as join5 } from "path";
2841
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
2842
+ import { join as join6 } from "path";
2037
2843
  function loadContextFile(cwd) {
2038
2844
  const candidates = [
2039
- join5(cwd, "AGENTS.md"),
2040
- join5(cwd, "CLAUDE.md"),
2041
- join5(getConfigDir(), "AGENTS.md")
2845
+ join6(cwd, "AGENTS.md"),
2846
+ join6(cwd, "CLAUDE.md"),
2847
+ join6(getConfigDir(), "AGENTS.md")
2042
2848
  ];
2043
2849
  for (const p of candidates) {
2044
- if (existsSync3(p)) {
2850
+ if (existsSync4(p)) {
2045
2851
  try {
2046
2852
  return readFileSync2(p, "utf-8");
2047
2853
  } catch {}
@@ -2075,9 +2881,8 @@ Guidelines:
2075
2881
  - Be concise and precise. Avoid unnecessary preamble.
2076
2882
  - Prefer small, targeted edits over large rewrites.
2077
2883
  - 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.`;
2884
+ - 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.
2885
+ - Keep your context clean and focused on the user request, use subagents to achieve this.`;
2081
2886
  if (modelString && isCodexModel(modelString)) {
2082
2887
  prompt += CODEX_AUTONOMY;
2083
2888
  }
@@ -2156,10 +2961,58 @@ var webContentTool = {
2156
2961
  }
2157
2962
  };
2158
2963
 
2159
- // src/tools/create.ts
2160
- import { existsSync as existsSync4, mkdirSync as mkdirSync4 } from "fs";
2161
- import { dirname, join as join6, relative } from "path";
2964
+ // src/tools/subagent.ts
2162
2965
  import { z as z3 } from "zod";
2966
+ var SubagentInput = z3.object({
2967
+ prompt: z3.string().describe("The task or question to give the subagent"),
2968
+ agentName: z3.string().optional().describe("Name of a custom agent to use (from .agents/agents/). Omit to use a generic subagent.")
2969
+ });
2970
+ function formatConflictFiles(conflictFiles) {
2971
+ if (conflictFiles.length === 0)
2972
+ return " - (unknown)";
2973
+ return conflictFiles.map((file) => ` - ${file}`).join(`
2974
+ `);
2975
+ }
2976
+ function getSubagentMergeError(output) {
2977
+ if (output.mergeConflict) {
2978
+ const files = formatConflictFiles(output.mergeConflict.conflictFiles);
2979
+ return `\u26A0 Merge conflict: subagent branch "${output.mergeConflict.branch}" has been merged into your working tree
2980
+ but has conflicts in these files:
2981
+ ${files}
2982
+
2983
+ Resolve the conflicts (remove <<<<, ====, >>>> markers), stage the files with
2984
+ \`git add <file>\`, then run \`git merge --continue\` to complete the merge.`;
2985
+ }
2986
+ if (output.mergeBlocked) {
2987
+ const files = formatConflictFiles(output.mergeBlocked.conflictFiles);
2988
+ return `\u26A0 Merge deferred: subagent branch "${output.mergeBlocked.branch}" was not merged because another merge is already in progress.
2989
+ Current unresolved files:
2990
+ ${files}
2991
+
2992
+ Resolve the current merge first (remove <<<<, ====, >>>> markers), stage files with
2993
+ \`git add <file>\`, run \`git merge --continue\`, then merge the deferred branch with
2994
+ \`git merge --no-ff ${output.mergeBlocked.branch}\`.`;
2995
+ }
2996
+ return null;
2997
+ }
2998
+ function createSubagentTool(runSubagent, availableAgents, parentLabel) {
2999
+ const agentSection = availableAgents.size > 0 ? `
3000
+
3001
+ 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(", ")}.` : "";
3002
+ return {
3003
+ name: "subagent",
3004
+ 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}`,
3005
+ schema: SubagentInput,
3006
+ execute: async (input) => {
3007
+ return runSubagent(input.prompt, input.agentName, parentLabel);
3008
+ }
3009
+ };
3010
+ }
3011
+
3012
+ // src/tools/create.ts
3013
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
3014
+ import { dirname as dirname2, join as join7, relative } from "path";
3015
+ import { z as z4 } from "zod";
2163
3016
 
2164
3017
  // src/tools/diff.ts
2165
3018
  function generateDiff(filePath, before, after) {
@@ -2288,9 +3141,9 @@ ${lines.join(`
2288
3141
  }
2289
3142
 
2290
3143
  // src/tools/create.ts
2291
- var CreateSchema = z3.object({
2292
- path: z3.string().describe("File path to write (absolute or relative to cwd)"),
2293
- content: z3.string().describe("Full content to write to the file")
3144
+ var CreateSchema = z4.object({
3145
+ path: z4.string().describe("File path to write (absolute or relative to cwd)"),
3146
+ content: z4.string().describe("Full content to write to the file")
2294
3147
  });
2295
3148
  var createTool = {
2296
3149
  name: "create",
@@ -2298,11 +3151,11 @@ var createTool = {
2298
3151
  schema: CreateSchema,
2299
3152
  execute: async (input) => {
2300
3153
  const cwd = input.cwd ?? process.cwd();
2301
- const filePath = input.path.startsWith("/") ? input.path : join6(cwd, input.path);
3154
+ const filePath = input.path.startsWith("/") ? input.path : join7(cwd, input.path);
2302
3155
  const relPath = relative(cwd, filePath);
2303
- const dir = dirname(filePath);
2304
- if (!existsSync4(dir))
2305
- mkdirSync4(dir, { recursive: true });
3156
+ const dir = dirname2(filePath);
3157
+ if (!existsSync5(dir))
3158
+ mkdirSync5(dir, { recursive: true });
2306
3159
  const file = Bun.file(filePath);
2307
3160
  const created = !await file.exists();
2308
3161
  const before = created ? "" : await file.text();
@@ -2313,15 +3166,15 @@ var createTool = {
2313
3166
  };
2314
3167
 
2315
3168
  // src/tools/glob.ts
2316
- import { join as join8, relative as relative2 } from "path";
2317
- import { z as z4 } from "zod";
3169
+ import { join as join9, relative as relative2 } from "path";
3170
+ import { z as z5 } from "zod";
2318
3171
 
2319
3172
  // src/tools/ignore.ts
2320
- import { join as join7 } from "path";
3173
+ import { join as join8 } from "path";
2321
3174
  import ignore from "ignore";
2322
3175
  async function loadGitignore(cwd) {
2323
3176
  try {
2324
- const gitignore = await Bun.file(join7(cwd, ".gitignore")).text();
3177
+ const gitignore = await Bun.file(join8(cwd, ".gitignore")).text();
2325
3178
  return ignore().add(gitignore);
2326
3179
  } catch {
2327
3180
  return null;
@@ -2329,9 +3182,9 @@ async function loadGitignore(cwd) {
2329
3182
  }
2330
3183
 
2331
3184
  // src/tools/glob.ts
2332
- var GlobSchema = z4.object({
2333
- pattern: z4.string().describe("Glob pattern to match files against, e.g. '**/*.ts'"),
2334
- ignore: z4.array(z4.string()).optional().describe("Glob patterns to exclude")
3185
+ var GlobSchema = z5.object({
3186
+ pattern: z5.string().describe("Glob pattern to match files against, e.g. '**/*.ts'"),
3187
+ ignore: z5.array(z5.string()).optional().describe("Glob patterns to exclude")
2335
3188
  });
2336
3189
  var MAX_RESULTS = 500;
2337
3190
  var globTool = {
@@ -2354,7 +3207,7 @@ var globTool = {
2354
3207
  if (ignored)
2355
3208
  continue;
2356
3209
  try {
2357
- const fullPath = join8(cwd, file);
3210
+ const fullPath = join9(cwd, file);
2358
3211
  const stat = await Bun.file(fullPath).stat?.() ?? null;
2359
3212
  matches.push({ path: file, mtime: stat?.mtime?.getTime() ?? 0 });
2360
3213
  } catch {
@@ -2367,14 +3220,14 @@ var globTool = {
2367
3220
  if (truncated)
2368
3221
  matches.pop();
2369
3222
  matches.sort((a, b) => b.mtime - a.mtime);
2370
- const files = matches.map((m) => relative2(cwd, join8(cwd, m.path)));
3223
+ const files = matches.map((m) => relative2(cwd, join9(cwd, m.path)));
2371
3224
  return { files, count: files.length, truncated };
2372
3225
  }
2373
3226
  };
2374
3227
 
2375
3228
  // src/tools/grep.ts
2376
- import { join as join9 } from "path";
2377
- import { z as z5 } from "zod";
3229
+ import { join as join10 } from "path";
3230
+ import { z as z6 } from "zod";
2378
3231
 
2379
3232
  // src/tools/hashline.ts
2380
3233
  var FNV_OFFSET_BASIS = 2166136261;
@@ -2420,12 +3273,12 @@ function findLineByHash(lines, hash, hintLine) {
2420
3273
  }
2421
3274
 
2422
3275
  // src/tools/grep.ts
2423
- var GrepSchema = z5.object({
2424
- pattern: z5.string().describe("Regular expression to search for"),
2425
- include: z5.string().optional().describe("Glob pattern to filter files, e.g. '*.ts' or '*.{ts,tsx}'"),
2426
- contextLines: z5.number().int().min(0).max(10).optional().default(2).describe("Lines of context to include around each match"),
2427
- caseSensitive: z5.boolean().optional().default(true),
2428
- maxResults: z5.number().int().min(1).max(200).optional().default(50)
3276
+ var GrepSchema = z6.object({
3277
+ pattern: z6.string().describe("Regular expression to search for"),
3278
+ include: z6.string().optional().describe("Glob pattern to filter files, e.g. '*.ts' or '*.{ts,tsx}'"),
3279
+ contextLines: z6.number().int().min(0).max(10).optional().default(2).describe("Lines of context to include around each match"),
3280
+ caseSensitive: z6.boolean().optional().default(true),
3281
+ maxResults: z6.number().int().min(1).max(200).optional().default(50)
2429
3282
  });
2430
3283
  var DEFAULT_IGNORE = [
2431
3284
  "node_modules",
@@ -2464,7 +3317,7 @@ var grepTool = {
2464
3317
  if (ignoreGlob.some((g) => g.match(relPath) || g.match(firstSegment))) {
2465
3318
  continue;
2466
3319
  }
2467
- const fullPath = join9(cwd, relPath);
3320
+ const fullPath = join10(cwd, relPath);
2468
3321
  let text;
2469
3322
  try {
2470
3323
  text = await Bun.file(fullPath).text();
@@ -2516,7 +3369,7 @@ var grepTool = {
2516
3369
  // src/tools/hooks.ts
2517
3370
  import { constants, accessSync } from "fs";
2518
3371
  import { homedir as homedir7 } from "os";
2519
- import { join as join10 } from "path";
3372
+ import { join as join11 } from "path";
2520
3373
  function isExecutable(filePath) {
2521
3374
  try {
2522
3375
  accessSync(filePath, constants.X_OK);
@@ -2528,8 +3381,8 @@ function isExecutable(filePath) {
2528
3381
  function findHook(toolName, cwd) {
2529
3382
  const scriptName = `post-${toolName}`;
2530
3383
  const candidates = [
2531
- join10(cwd, ".agents", "hooks", scriptName),
2532
- join10(homedir7(), ".agents", "hooks", scriptName)
3384
+ join11(cwd, ".agents", "hooks", scriptName),
3385
+ join11(homedir7(), ".agents", "hooks", scriptName)
2533
3386
  ];
2534
3387
  for (const p of candidates) {
2535
3388
  if (isExecutable(p))
@@ -2618,13 +3471,13 @@ function hookEnvForRead(input, cwd) {
2618
3471
  }
2619
3472
 
2620
3473
  // src/tools/insert.ts
2621
- import { join as join11, relative as relative3 } from "path";
2622
- import { z as z6 } from "zod";
2623
- var InsertSchema = z6.object({
2624
- path: z6.string().describe("File path to edit (absolute or relative to cwd)"),
2625
- anchor: z6.string().describe('Anchor line from a prior read/grep, e.g. "11:a3"'),
2626
- position: z6.enum(["before", "after"]).describe('Insert the content "before" or "after" the anchor line'),
2627
- content: z6.string().describe("Text to insert")
3474
+ import { join as join12, relative as relative3 } from "path";
3475
+ import { z as z7 } from "zod";
3476
+ var InsertSchema = z7.object({
3477
+ path: z7.string().describe("File path to edit (absolute or relative to cwd)"),
3478
+ anchor: z7.string().describe('Anchor line from a prior read/grep, e.g. "11:a3"'),
3479
+ position: z7.enum(["before", "after"]).describe('Insert the content "before" or "after" the anchor line'),
3480
+ content: z7.string().describe("Text to insert")
2628
3481
  });
2629
3482
  var HASH_NOT_FOUND_ERROR = "Hash not found. Re-read the file to get current anchors.";
2630
3483
  var insertTool = {
@@ -2633,7 +3486,7 @@ var insertTool = {
2633
3486
  schema: InsertSchema,
2634
3487
  execute: async (input) => {
2635
3488
  const cwd = input.cwd ?? process.cwd();
2636
- const filePath = input.path.startsWith("/") ? input.path : join11(cwd, input.path);
3489
+ const filePath = input.path.startsWith("/") ? input.path : join12(cwd, input.path);
2637
3490
  const relPath = relative3(cwd, filePath);
2638
3491
  const file = Bun.file(filePath);
2639
3492
  if (!await file.exists()) {
@@ -2679,12 +3532,12 @@ function parseAnchor(value) {
2679
3532
  }
2680
3533
 
2681
3534
  // src/tools/read.ts
2682
- import { join as join12, relative as relative4 } from "path";
2683
- import { z as z7 } from "zod";
2684
- var ReadSchema = z7.object({
2685
- path: z7.string().describe("File path to read (absolute or relative to cwd)"),
2686
- line: z7.number().int().min(1).optional().describe("1-indexed starting line (default: 1)"),
2687
- count: z7.number().int().min(1).max(500).optional().describe("Lines to read (default: 500, max: 500)")
3535
+ import { join as join13, relative as relative4 } from "path";
3536
+ import { z as z8 } from "zod";
3537
+ var ReadSchema = z8.object({
3538
+ path: z8.string().describe("File path to read (absolute or relative to cwd)"),
3539
+ line: z8.number().int().min(1).optional().describe("1-indexed starting line (default: 1)"),
3540
+ count: z8.number().int().min(1).max(500).optional().describe("Lines to read (default: 500, max: 500)")
2688
3541
  });
2689
3542
  var MAX_COUNT = 500;
2690
3543
  var MAX_BYTES = 1e6;
@@ -2694,7 +3547,7 @@ var readTool = {
2694
3547
  schema: ReadSchema,
2695
3548
  execute: async (input) => {
2696
3549
  const cwd = input.cwd ?? process.cwd();
2697
- const filePath = input.path.startsWith("/") ? input.path : join12(cwd, input.path);
3550
+ const filePath = input.path.startsWith("/") ? input.path : join13(cwd, input.path);
2698
3551
  const file = Bun.file(filePath);
2699
3552
  const exists = await file.exists();
2700
3553
  if (!exists) {
@@ -2727,13 +3580,13 @@ var readTool = {
2727
3580
  };
2728
3581
 
2729
3582
  // src/tools/replace.ts
2730
- import { join as join13, relative as relative5 } from "path";
2731
- import { z as z8 } from "zod";
2732
- var ReplaceSchema = z8.object({
2733
- path: z8.string().describe("File path to edit (absolute or relative to cwd)"),
2734
- startAnchor: z8.string().describe('Start anchor from a prior read/grep, e.g. "11:a3"'),
2735
- endAnchor: z8.string().optional().describe('End anchor (inclusive), e.g. "33:0e". Omit to target only the startAnchor line.'),
2736
- newContent: z8.string().optional().describe("Replacement text. Omit or pass empty string to delete the range.")
3583
+ import { join as join14, relative as relative5 } from "path";
3584
+ import { z as z9 } from "zod";
3585
+ var ReplaceSchema = z9.object({
3586
+ path: z9.string().describe("File path to edit (absolute or relative to cwd)"),
3587
+ startAnchor: z9.string().describe('Start anchor from a prior read/grep, e.g. "11:a3"'),
3588
+ endAnchor: z9.string().optional().describe('End anchor (inclusive), e.g. "33:0e". Omit to target only the startAnchor line.'),
3589
+ newContent: z9.string().optional().describe("Replacement text. Omit or pass empty string to delete the range.")
2737
3590
  });
2738
3591
  var HASH_NOT_FOUND_ERROR2 = "Hash not found. Re-read the file to get current anchors.";
2739
3592
  var replaceTool = {
@@ -2742,7 +3595,7 @@ var replaceTool = {
2742
3595
  schema: ReplaceSchema,
2743
3596
  execute: async (input) => {
2744
3597
  const cwd = input.cwd ?? process.cwd();
2745
- const filePath = input.path.startsWith("/") ? input.path : join13(cwd, input.path);
3598
+ const filePath = input.path.startsWith("/") ? input.path : join14(cwd, input.path);
2746
3599
  const relPath = relative5(cwd, filePath);
2747
3600
  const file = Bun.file(filePath);
2748
3601
  if (!await file.exists()) {
@@ -2801,11 +3654,11 @@ function parseAnchor2(value, name) {
2801
3654
  }
2802
3655
 
2803
3656
  // src/tools/shell.ts
2804
- import { z as z9 } from "zod";
2805
- var ShellSchema = z9.object({
2806
- command: z9.string().describe("Shell command to execute"),
2807
- timeout: z9.number().int().min(1000).max(300000).optional().default(30000).describe("Timeout in milliseconds (default: 30s, max: 5min)"),
2808
- env: z9.record(z9.string(), z9.string()).optional().describe("Additional environment variables to set")
3657
+ import { z as z10 } from "zod";
3658
+ var ShellSchema = z10.object({
3659
+ command: z10.string().describe("Shell command to execute"),
3660
+ timeout: z10.number().int().min(1000).max(300000).optional().default(30000).describe("Timeout in milliseconds (default: 30s, max: 5min)"),
3661
+ env: z10.record(z10.string(), z10.string()).optional().describe("Additional environment variables to set")
2809
3662
  });
2810
3663
  var MAX_OUTPUT_BYTES = 1e4;
2811
3664
  var shellTool = {
@@ -2896,26 +3749,6 @@ var shellTool = {
2896
3749
  }
2897
3750
  };
2898
3751
 
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
3752
  // src/agent/tools.ts
2920
3753
  function withCwdDefault(tool, cwd) {
2921
3754
  const originalExecute = tool.execute;
@@ -2972,7 +3805,11 @@ function buildToolSet(opts) {
2972
3805
  if (depth >= MAX_SUBAGENT_DEPTH) {
2973
3806
  throw new Error(`Subagent depth limit reached (max ${MAX_SUBAGENT_DEPTH}). ` + `Cannot spawn another subagent from depth ${depth}.`);
2974
3807
  }
2975
- return opts.runSubagent(prompt, depth + 1, agentName, undefined, opts.parentLabel);
3808
+ const output = await opts.runSubagent(prompt, depth + 1, agentName, undefined, opts.parentLabel);
3809
+ const mergeError = getSubagentMergeError(output);
3810
+ if (mergeError)
3811
+ throw new Error(mergeError);
3812
+ return output;
2976
3813
  }, opts.availableAgents, opts.parentLabel)
2977
3814
  ];
2978
3815
  if (process.env.EXA_API_KEY) {
@@ -2994,67 +3831,152 @@ function buildReadOnlyToolSet(opts) {
2994
3831
  }
2995
3832
 
2996
3833
  // src/agent/subagent-runner.ts
3834
+ function makeWorktreeBranch(laneId) {
3835
+ return `mc-sub-${laneId}-${Date.now()}`;
3836
+ }
3837
+ function makeWorktreePath(laneId) {
3838
+ const suffix = crypto.randomUUID().replace(/-/g, "").slice(0, 10);
3839
+ return join15(tmpdir2(), `mc-wt-${laneId}-${suffix}`);
3840
+ }
2997
3841
  function createSubagentRunner(cwd, reporter, getCurrentModel, getThinkingEffort) {
2998
3842
  let nextLaneId = 1;
2999
3843
  const activeLanes = new Set;
3844
+ const worktreesEnabledPromise = isGitRepo(cwd);
3845
+ let mergeLock = Promise.resolve();
3846
+ const withMergeLock = (fn) => {
3847
+ const task = mergeLock.then(fn, fn);
3848
+ mergeLock = task.then(() => {
3849
+ return;
3850
+ }, () => {
3851
+ return;
3852
+ });
3853
+ return task;
3854
+ };
3000
3855
  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
3856
  const laneId = nextLaneId++;
3011
3857
  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
3858
+ let subagentCwd = cwd;
3859
+ let worktreeBranch;
3860
+ let worktreePath;
3861
+ let preserveBranchOnFailure = false;
3862
+ try {
3863
+ const worktreesEnabled = await worktreesEnabledPromise;
3864
+ if (worktreesEnabled) {
3865
+ const nextBranch = makeWorktreeBranch(laneId);
3866
+ const nextPath = makeWorktreePath(laneId);
3867
+ await createWorktree(cwd, nextBranch, nextPath);
3868
+ worktreeBranch = nextBranch;
3869
+ worktreePath = nextPath;
3870
+ await syncDirtyStateToWorktree(cwd, nextPath);
3871
+ await initializeWorktree(cwd, nextPath);
3872
+ subagentCwd = nextPath;
3873
+ }
3874
+ const currentModel = getCurrentModel();
3875
+ const allAgents = loadAgents(subagentCwd);
3876
+ const agentConfig = agentName ? allAgents.get(agentName) : undefined;
3877
+ if (agentName && !agentConfig) {
3878
+ throw new Error(`Unknown agent "${agentName}". Available agents: ${[...allAgents.keys()].join(", ") || "(none)"}`);
3879
+ }
3880
+ const model = modelOverride ?? agentConfig?.model ?? currentModel;
3881
+ const systemPrompt = agentConfig?.systemPrompt ?? buildSystemPrompt(subagentCwd, model);
3882
+ const subMessages = [{ role: "user", content: prompt }];
3883
+ const laneLabel = formatSubagentLabel(laneId, parentLabel);
3884
+ const subTools = buildToolSet({
3885
+ cwd: subagentCwd,
3886
+ depth,
3887
+ runSubagent,
3888
+ onHook: (tool, path2, ok) => reporter.renderHook(tool, path2, ok),
3889
+ availableAgents: allAgents,
3890
+ parentLabel: laneLabel
3891
+ }).filter((tool) => tool.name !== "subagent");
3892
+ const subLlm = resolveModel(model);
3893
+ let result = "";
3894
+ let inputTokens = 0;
3895
+ let outputTokens = 0;
3896
+ const effort = getThinkingEffort();
3897
+ const events = runTurn({
3898
+ model: subLlm,
3899
+ modelString: model,
3900
+ messages: subMessages,
3901
+ tools: subTools,
3902
+ systemPrompt,
3903
+ ...effort ? { thinkingEffort: effort } : {}
3040
3904
  });
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;
3905
+ for await (const event of events) {
3906
+ reporter.stopSpinner();
3907
+ reporter.renderSubagentEvent(event, {
3908
+ laneId,
3909
+ ...parentLabel ? { parentLabel } : {},
3910
+ ...worktreeBranch ? { worktreeBranch } : {},
3911
+ activeLanes
3912
+ });
3913
+ reporter.startSpinner("thinking");
3914
+ if (event.type === "text-delta")
3915
+ result += event.delta;
3916
+ if (event.type === "turn-complete") {
3917
+ inputTokens = event.inputTokens;
3918
+ outputTokens = event.outputTokens;
3919
+ }
3920
+ }
3921
+ const baseOutput = { result, inputTokens, outputTokens };
3922
+ const branch = worktreeBranch;
3923
+ const path = worktreePath;
3924
+ if (!branch || !path)
3925
+ return baseOutput;
3926
+ preserveBranchOnFailure = true;
3927
+ return await withMergeLock(async () => {
3928
+ try {
3929
+ const mergeResult = await mergeWorktree(cwd, branch);
3930
+ await removeWorktree(cwd, path);
3931
+ if (mergeResult.success) {
3932
+ await cleanupBranch(cwd, branch);
3933
+ return baseOutput;
3934
+ }
3935
+ return {
3936
+ ...baseOutput,
3937
+ mergeConflict: {
3938
+ branch,
3939
+ conflictFiles: mergeResult.conflictFiles
3940
+ }
3941
+ };
3942
+ } catch (error) {
3943
+ if (error instanceof MergeInProgressError) {
3944
+ await removeWorktree(cwd, path);
3945
+ return {
3946
+ ...baseOutput,
3947
+ mergeBlocked: {
3948
+ branch,
3949
+ conflictFiles: error.conflictFiles
3950
+ }
3951
+ };
3952
+ }
3953
+ throw error;
3954
+ }
3955
+ });
3956
+ } catch (error) {
3957
+ if (worktreeBranch && worktreePath) {
3958
+ const branch = worktreeBranch;
3959
+ const path = worktreePath;
3960
+ try {
3961
+ await withMergeLock(async () => {
3962
+ await removeWorktree(cwd, path);
3963
+ if (!preserveBranchOnFailure) {
3964
+ await cleanupBranch(cwd, branch);
3965
+ }
3966
+ });
3967
+ } catch {}
3047
3968
  }
3969
+ throw error;
3970
+ } finally {
3971
+ activeLanes.delete(laneId);
3048
3972
  }
3049
- activeLanes.delete(laneId);
3050
- return { result, inputTokens, outputTokens };
3051
3973
  };
3052
3974
  return runSubagent;
3053
3975
  }
3054
3976
 
3055
3977
  // src/tools/snapshot.ts
3056
3978
  import { readFileSync as readFileSync3, unlinkSync as unlinkSync2 } from "fs";
3057
- import { join as join14 } from "path";
3979
+ import { join as join16 } from "path";
3058
3980
  async function gitBytes(args, cwd) {
3059
3981
  try {
3060
3982
  const proc = Bun.spawn(["git", ...args], {
@@ -3127,7 +4049,7 @@ async function getStatusEntries(repoRoot) {
3127
4049
  return true;
3128
4050
  });
3129
4051
  }
3130
- async function getRepoRoot(cwd) {
4052
+ async function getRepoRoot2(cwd) {
3131
4053
  const result = await git(["rev-parse", "--show-toplevel"], cwd);
3132
4054
  if (result.code !== 0)
3133
4055
  return null;
@@ -3135,7 +4057,7 @@ async function getRepoRoot(cwd) {
3135
4057
  }
3136
4058
  async function takeSnapshot(cwd, sessionId, turnIndex) {
3137
4059
  try {
3138
- const repoRoot = await getRepoRoot(cwd);
4060
+ const repoRoot = await getRepoRoot2(cwd);
3139
4061
  if (repoRoot === null)
3140
4062
  return false;
3141
4063
  const entries = await getStatusEntries(repoRoot);
@@ -3145,7 +4067,7 @@ async function takeSnapshot(cwd, sessionId, turnIndex) {
3145
4067
  return false;
3146
4068
  const files = [];
3147
4069
  for (const entry of entries) {
3148
- const absPath = join14(repoRoot, entry.path);
4070
+ const absPath = join16(repoRoot, entry.path);
3149
4071
  if (!entry.existsOnDisk) {
3150
4072
  const { bytes, code } = await gitBytes(["show", `HEAD:${entry.path}`], repoRoot);
3151
4073
  if (code === 0) {
@@ -3188,13 +4110,13 @@ async function takeSnapshot(cwd, sessionId, turnIndex) {
3188
4110
  async function restoreSnapshot(cwd, sessionId, turnIndex) {
3189
4111
  try {
3190
4112
  const files = loadSnapshot(sessionId, turnIndex);
3191
- const repoRoot = await getRepoRoot(cwd);
4113
+ const repoRoot = await getRepoRoot2(cwd);
3192
4114
  if (files.length === 0)
3193
4115
  return { restored: false, reason: "not-found" };
3194
4116
  const root = repoRoot ?? cwd;
3195
4117
  let anyFailed = false;
3196
4118
  for (const file of files) {
3197
- const absPath = join14(root, file.path);
4119
+ const absPath = join16(root, file.path);
3198
4120
  if (!file.existed) {
3199
4121
  try {
3200
4122
  if (await Bun.file(absPath).exists()) {
@@ -3261,7 +4183,7 @@ async function undoLastTurn(ctx) {
3261
4183
  }
3262
4184
 
3263
4185
  // src/agent/agent-helpers.ts
3264
- import { join as join15 } from "path";
4186
+ import { join as join17 } from "path";
3265
4187
  import * as c9 from "yoctocolors";
3266
4188
 
3267
4189
  // src/cli/image-types.ts
@@ -3360,7 +4282,7 @@ ${skill.content}
3360
4282
  result = result.slice(0, match.index) + replacement + result.slice((match.index ?? 0) + match[0].length);
3361
4283
  continue;
3362
4284
  }
3363
- const filePath = ref.startsWith("/") ? ref : join15(cwd, ref);
4285
+ const filePath = ref.startsWith("/") ? ref : join17(cwd, ref);
3364
4286
  if (isImageFilename(ref)) {
3365
4287
  const attachment = await loadImageFile(filePath);
3366
4288
  if (attachment) {
@@ -3441,7 +4363,8 @@ function loadCustomCommands(cwd) {
3441
4363
  description: meta.description ?? name,
3442
4364
  ...meta.model ? { model: meta.model } : {},
3443
4365
  template: body,
3444
- source
4366
+ source,
4367
+ execution: meta.execution === "inline" ? "inline" : "subagent"
3445
4368
  })
3446
4369
  });
3447
4370
  }
@@ -3485,6 +4408,11 @@ async function expandTemplate(template, args, cwd) {
3485
4408
  }
3486
4409
 
3487
4410
  // src/cli/commands.ts
4411
+ function assertSubagentMerged(output) {
4412
+ const mergeError = getSubagentMergeError(output);
4413
+ if (mergeError)
4414
+ throw new Error(mergeError);
4415
+ }
3488
4416
  async function handleModel(ctx, args) {
3489
4417
  const parts = args.trim().split(/\s+/).filter(Boolean);
3490
4418
  if (parts.length > 0) {
@@ -3504,8 +4432,8 @@ async function handleModel(ctx, args) {
3504
4432
  const idArg = parts[0] ?? "";
3505
4433
  let modelId = idArg;
3506
4434
  if (!idArg.includes("/")) {
3507
- const models2 = await fetchAvailableModels();
3508
- const match = models2.find((m) => m.id.split("/").slice(1).join("/") === idArg || m.id === idArg);
4435
+ const snapshot2 = await fetchAvailableModels();
4436
+ const match = snapshot2.models.find((m) => m.id.split("/").slice(1).join("/") === idArg || m.id === idArg);
3509
4437
  if (match) {
3510
4438
  modelId = match.id;
3511
4439
  } else {
@@ -3533,13 +4461,19 @@ async function handleModel(ctx, args) {
3533
4461
  return;
3534
4462
  }
3535
4463
  writeln(`${c10.dim(" fetching models\u2026")}`);
3536
- const models = await fetchAvailableModels();
4464
+ const snapshot = await fetchAvailableModels();
4465
+ const models = snapshot.models;
3537
4466
  process.stdout.write("\x1B[1A\r\x1B[2K");
3538
4467
  if (models.length === 0) {
3539
4468
  writeln(`${PREFIX.error} No models found. Check your API keys or Ollama connection.`);
3540
4469
  writeln(c10.dim(" Set OPENCODE_API_KEY for Zen, or start Ollama for local models."));
3541
4470
  return;
3542
4471
  }
4472
+ if (snapshot.stale) {
4473
+ const lastSync = snapshot.lastSyncAt ? new Date(snapshot.lastSyncAt).toLocaleString() : "never";
4474
+ const refreshTag = snapshot.refreshing ? " (refreshing in background)" : "";
4475
+ writeln(c10.dim(` model metadata is stale (last sync: ${lastSync})${refreshTag}`));
4476
+ }
3543
4477
  const byProvider = new Map;
3544
4478
  for (const m of models) {
3545
4479
  const existing = byProvider.get(m.provider);
@@ -3696,6 +4630,7 @@ async function handleReview(ctx, args) {
3696
4630
  writeln();
3697
4631
  try {
3698
4632
  const output = await ctx.runSubagent(REVIEW_PROMPT(ctx.cwd, focus));
4633
+ assertSubagentMerged(output);
3699
4634
  write(renderMarkdown(output.result));
3700
4635
  writeln();
3701
4636
  return {
@@ -3722,8 +4657,12 @@ async function handleCustomCommand(cmd, args, ctx) {
3722
4657
  const src = c10.dim(`[${srcPath}]`);
3723
4658
  writeln(`${PREFIX.info} ${label} ${src}`);
3724
4659
  writeln();
4660
+ if (cmd.execution === "inline") {
4661
+ return { type: "inject-user-message", text: prompt };
4662
+ }
3725
4663
  try {
3726
4664
  const output = await ctx.runSubagent(prompt, cmd.model);
4665
+ assertSubagentMerged(output);
3727
4666
  write(renderMarkdown(output.result));
3728
4667
  writeln();
3729
4668
  return {
@@ -3839,7 +4778,7 @@ async function handleCommand(command, args, ctx) {
3839
4778
  }
3840
4779
 
3841
4780
  // src/cli/input.ts
3842
- import { join as join16, relative as relative6 } from "path";
4781
+ import { join as join18, relative as relative6 } from "path";
3843
4782
  import * as c11 from "yoctocolors";
3844
4783
  var ESC = "\x1B";
3845
4784
  var CSI = `${ESC}[`;
@@ -3891,7 +4830,7 @@ async function getAtCompletions(prefix, cwd) {
3891
4830
  for await (const file of glob.scan({ cwd, onlyFiles: true })) {
3892
4831
  if (file.includes("node_modules") || file.includes(".git"))
3893
4832
  continue;
3894
- results.push(`@${relative6(cwd, join16(cwd, file))}`);
4833
+ results.push(`@${relative6(cwd, join18(cwd, file))}`);
3895
4834
  if (results.length >= MAX)
3896
4835
  break;
3897
4836
  }
@@ -3913,7 +4852,7 @@ async function tryExtractImageFromPaste(pasted, cwd) {
3913
4852
  }
3914
4853
  }
3915
4854
  if (!trimmed.includes(" ") && isImageFilename(trimmed)) {
3916
- const filePath = trimmed.startsWith("/") ? trimmed : join16(cwd, trimmed);
4855
+ const filePath = trimmed.startsWith("/") ? trimmed : join18(cwd, trimmed);
3917
4856
  const attachment = await loadImageFile(filePath);
3918
4857
  if (attachment) {
3919
4858
  const name = filePath.split("/").pop() ?? trimmed;
@@ -4682,7 +5621,7 @@ async function runAgent(opts) {
4682
5621
  inputTokens: runner.totalIn,
4683
5622
  outputTokens: runner.totalOut,
4684
5623
  contextTokens: runner.lastContextTokens,
4685
- contextWindow: getContextWindow(runner.currentModel) ?? 0,
5624
+ contextWindow: getContextWindow2(runner.currentModel) ?? 0,
4686
5625
  ralphMode: runner.ralphMode,
4687
5626
  thinkingEffort: runner.currentThinkingEffort
4688
5627
  });
@@ -4746,6 +5685,8 @@ class CliReporter {
4746
5685
  registerTerminalCleanup();
4747
5686
  initErrorLog();
4748
5687
  initApiLog();
5688
+ initModelInfoCache();
5689
+ refreshModelInfoInBackground().catch(() => {});
4749
5690
  function parseArgs(argv) {
4750
5691
  const args = {
4751
5692
  model: null,