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