mini-coder 0.0.12 → 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 +1356 -415
- package/docs/configs.md +1 -1
- package/docs/custom-commands.md +24 -1
- package/package.json +1 -1
- package/hanging-bug.md +0 -79
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
|
|
720
|
-
const
|
|
721
|
-
return
|
|
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.
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1343
|
-
|
|
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 (!
|
|
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
|
|
1434
|
-
|
|
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
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2036
|
-
import { join as
|
|
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
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
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 (
|
|
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
|
|
2079
|
-
-
|
|
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/
|
|
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 =
|
|
2292
|
-
path:
|
|
2293
|
-
content:
|
|
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 :
|
|
3154
|
+
const filePath = input.path.startsWith("/") ? input.path : join7(cwd, input.path);
|
|
2302
3155
|
const relPath = relative(cwd, filePath);
|
|
2303
|
-
const dir =
|
|
2304
|
-
if (!
|
|
2305
|
-
|
|
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
|
|
2317
|
-
import { z as
|
|
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
|
|
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(
|
|
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 =
|
|
2333
|
-
pattern:
|
|
2334
|
-
ignore:
|
|
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 =
|
|
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,
|
|
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
|
|
2377
|
-
import { z as
|
|
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 =
|
|
2424
|
-
pattern:
|
|
2425
|
-
include:
|
|
2426
|
-
contextLines:
|
|
2427
|
-
caseSensitive:
|
|
2428
|
-
maxResults:
|
|
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 =
|
|
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
|
|
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
|
-
|
|
2532
|
-
|
|
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
|
|
2622
|
-
import { z as
|
|
2623
|
-
var InsertSchema =
|
|
2624
|
-
path:
|
|
2625
|
-
anchor:
|
|
2626
|
-
position:
|
|
2627
|
-
content:
|
|
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 :
|
|
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
|
|
2683
|
-
import { z as
|
|
2684
|
-
var ReadSchema =
|
|
2685
|
-
path:
|
|
2686
|
-
line:
|
|
2687
|
-
count:
|
|
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 :
|
|
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
|
|
2731
|
-
import { z as
|
|
2732
|
-
var ReplaceSchema =
|
|
2733
|
-
path:
|
|
2734
|
-
startAnchor:
|
|
2735
|
-
endAnchor:
|
|
2736
|
-
newContent:
|
|
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 :
|
|
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
|
|
2805
|
-
var ShellSchema =
|
|
2806
|
-
command:
|
|
2807
|
-
timeout:
|
|
2808
|
-
env:
|
|
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
|
-
|
|
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
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
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
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 :
|
|
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
|
|
3508
|
-
const match =
|
|
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
|
|
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
|
|
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,
|
|
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 :
|
|
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:
|
|
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,
|