@sma1lboy/kobe 0.5.15 → 0.5.16

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.
Files changed (3) hide show
  1. package/dist/bin/kobed.js +1554 -310
  2. package/dist/cli/index.js +2404 -762
  3. package/package.json +3 -1
package/dist/bin/kobed.js CHANGED
@@ -48,17 +48,38 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
48
  var __require = import.meta.require;
49
49
 
50
50
  // src/daemon/paths.ts
51
+ import { createHash } from "crypto";
51
52
  import { homedir, tmpdir } from "os";
52
53
  import { join } from "path";
53
- function defaultDaemonSocketPath(homeDir = process.env.KOBE_HOME_DIR ?? homedir()) {
54
+ function shortHomeTag(homeDir) {
55
+ return createHash("sha1").update(homeDir).digest("hex").slice(0, 8);
56
+ }
57
+ function fitSocketPath(naturalPath, homeDir, role, pidTag) {
58
+ if (Buffer.byteLength(naturalPath, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
59
+ return naturalPath;
60
+ const tag = shortHomeTag(homeDir);
61
+ const suffix = pidTag === undefined ? "" : `-${pidTag}`;
62
+ const fallback = join(tmpdir(), `kobe-${tag}-${role}${suffix}.sock`);
63
+ if (Buffer.byteLength(fallback, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
64
+ return fallback;
65
+ throw new Error(`kobe socket path exceeds ${SOCKET_PATH_SAFETY_LIMIT} bytes even after fallback: ${fallback}`);
66
+ }
67
+ function defaultDaemonSocketPath(homeDir) {
68
+ const explicit = homeDir ?? process.env.KOBE_HOME_DIR;
69
+ if (explicit && explicit.length > 0) {
70
+ return fitSocketPath(join(explicit, ".kobe", "daemon.sock"), explicit, "daemon");
71
+ }
54
72
  const runtimeDir = process.env.XDG_RUNTIME_DIR;
55
- if (runtimeDir && runtimeDir.length > 0)
56
- return join(runtimeDir, "kobe.sock");
57
- return join(homeDir, ".kobe", "daemon.sock");
73
+ if (runtimeDir && runtimeDir.length > 0) {
74
+ return fitSocketPath(join(runtimeDir, "kobe.sock"), runtimeDir, "daemon");
75
+ }
76
+ const home = homedir();
77
+ return fitSocketPath(join(home, ".kobe", "daemon.sock"), home, "daemon");
58
78
  }
59
79
  function defaultDaemonPidPath(homeDir = process.env.KOBE_HOME_DIR ?? homedir()) {
60
80
  return join(homeDir, ".kobe", "daemon.pid");
61
81
  }
82
+ var SOCKET_PATH_SAFETY_LIMIT = 100;
62
83
  var init_paths = () => {};
63
84
 
64
85
  // src/daemon/protocol.ts
@@ -78,6 +99,7 @@ function serializeTask(task) {
78
99
  pinned: task.pinned ?? false,
79
100
  permissionMode: task.permissionMode,
80
101
  model: task.model,
102
+ vendor: task.vendor,
81
103
  createdAt: task.createdAt,
82
104
  updatedAt: task.updatedAt
83
105
  };
@@ -345,6 +367,86 @@ var init_daemon_process = __esm(() => {
345
367
  init_client();
346
368
  });
347
369
 
370
+ // src/session/usage-metrics.ts
371
+ function totalContextTokens(u) {
372
+ return u.input_tokens + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
373
+ }
374
+ function parseTimestampMs(value) {
375
+ const ms = new Date(value).getTime();
376
+ return Number.isFinite(ms) ? ms : null;
377
+ }
378
+ function mergeIntervals(intervals) {
379
+ if (intervals.length === 0)
380
+ return [];
381
+ const sorted = [...intervals].sort((a, b) => a.startMs - b.startMs);
382
+ const first = sorted[0];
383
+ if (!first)
384
+ return [];
385
+ const merged = [{ startMs: first.startMs, endMs: first.endMs }];
386
+ for (let i = 1;i < sorted.length; i++) {
387
+ const current = sorted[i];
388
+ const last = merged[merged.length - 1];
389
+ if (!current || !last)
390
+ continue;
391
+ if (current.startMs <= last.endMs) {
392
+ last.endMs = Math.max(last.endMs, current.endMs);
393
+ } else {
394
+ merged.push({ startMs: current.startMs, endMs: current.endMs });
395
+ }
396
+ }
397
+ return merged;
398
+ }
399
+ function durationMs(intervals) {
400
+ return intervals.reduce((total, interval) => total + (interval.endMs - interval.startMs), 0);
401
+ }
402
+ function deriveSessionUsageMetrics(past) {
403
+ let latestUsage;
404
+ let latestUsageTimestampMs = null;
405
+ let lastUserTimestampMs = null;
406
+ let inputTokens = 0;
407
+ let outputTokens = 0;
408
+ const intervals = [];
409
+ for (const message of past) {
410
+ const timestampMs = parseTimestampMs(message.timestamp);
411
+ if (message.role === "user" && timestampMs !== null) {
412
+ lastUserTimestampMs = timestampMs;
413
+ continue;
414
+ }
415
+ if (message.role !== "assistant" || !message.usage)
416
+ continue;
417
+ if (timestampMs !== null && (latestUsageTimestampMs === null || timestampMs > latestUsageTimestampMs)) {
418
+ latestUsageTimestampMs = timestampMs;
419
+ latestUsage = message.usage;
420
+ } else if (latestUsage === undefined) {
421
+ latestUsage = message.usage;
422
+ }
423
+ inputTokens += message.usage.input_tokens;
424
+ outputTokens += message.usage.output_tokens;
425
+ if (timestampMs !== null && lastUserTimestampMs !== null && timestampMs > lastUserTimestampMs) {
426
+ intervals.push({ startMs: lastUserTimestampMs, endMs: timestampMs });
427
+ }
428
+ }
429
+ if (!latestUsage)
430
+ return;
431
+ const totalDurationMs = durationMs(mergeIntervals(intervals));
432
+ if (totalDurationMs <= 0)
433
+ return latestUsage;
434
+ return {
435
+ ...latestUsage,
436
+ total_speed_tokens_per_second: (inputTokens + outputTokens) / (totalDurationMs / 1000)
437
+ };
438
+ }
439
+ function withTotalSpeedForTurn(usage, startedAtIso, endedAtIso) {
440
+ const startMs = startedAtIso ? parseTimestampMs(startedAtIso) : null;
441
+ const endMs = parseTimestampMs(endedAtIso);
442
+ if (startMs === null || endMs === null || endMs <= startMs)
443
+ return usage;
444
+ return {
445
+ ...usage,
446
+ total_speed_tokens_per_second: (usage.input_tokens + usage.output_tokens) / ((endMs - startMs) / 1000)
447
+ };
448
+ }
449
+
348
450
  // src/engine/claude-code-local/binary.ts
349
451
  import { spawnSync } from "child_process";
350
452
  import { existsSync as existsSync2, statSync } from "fs";
@@ -449,10 +551,152 @@ var init_binary = __esm(() => {
449
551
  };
450
552
  });
451
553
 
554
+ // src/engine/claude-code-local/models.ts
555
+ function parseContextWindowSize(modelIdentifier) {
556
+ const delimitedMatch = /(?:\(|\[)\s*(\d+(?:[,_]\d+)*(?:\.\d+)?)\s*([km])\s*(?:\)|\])/i.exec(modelIdentifier);
557
+ if (delimitedMatch?.[1] && delimitedMatch[2]) {
558
+ const parsed2 = Number.parseFloat(delimitedMatch[1].replace(/[,_]/g, ""));
559
+ if (Number.isFinite(parsed2) && parsed2 > 0) {
560
+ return Math.round(parsed2 * (delimitedMatch[2].toLowerCase() === "m" ? 1e6 : 1000));
561
+ }
562
+ }
563
+ const contextMatch = /\b(\d+(?:[,_]\d+)*(?:\.\d+)?)\s*([km])(?:\s*(?:token\s*)?context)?\b/i.exec(modelIdentifier);
564
+ if (!contextMatch?.[1] || !contextMatch[2])
565
+ return null;
566
+ const parsed = Number.parseFloat(contextMatch[1].replace(/[,_]/g, ""));
567
+ if (!Number.isFinite(parsed) || parsed <= 0)
568
+ return null;
569
+ return Math.round(parsed * (contextMatch[2].toLowerCase() === "m" ? 1e6 : 1000));
570
+ }
571
+ function claudeContextWindowFor(modelId) {
572
+ const parsed = parseContextWindowSize(modelId);
573
+ if (parsed !== null)
574
+ return parsed;
575
+ if (modelId.includes("[1m]"))
576
+ return LONG_CTX;
577
+ const inCatalog = CLAUDE_MODELS.some((m) => m.id === modelId);
578
+ if (inCatalog)
579
+ return STD_CTX;
580
+ if (modelId.includes("1m") || modelId.includes("[1M]"))
581
+ return LONG_CTX;
582
+ return STD_CTX;
583
+ }
584
+ var CLAUDE_MODELS, LONG_CTX = 1e6, STD_CTX = 200000;
585
+ var init_models = __esm(() => {
586
+ CLAUDE_MODELS = [
587
+ { vendor: "claude", id: "claude-opus-4-7[1m]", label: "Opus 4.7 1M", hint: "long context, default" },
588
+ { vendor: "claude", id: "claude-opus-4-7", label: "Opus 4.7", hint: "most capable, slowest" },
589
+ { vendor: "claude", id: "claude-sonnet-4-6[1m]", label: "sonnet 4.6 (1M)", hint: "long context" },
590
+ { vendor: "claude", id: "claude-sonnet-4-6", label: "sonnet 4.6" },
591
+ { vendor: "claude", id: "claude-haiku-4-5-20251001", label: "haiku 4.5", hint: "fastest, cheapest" }
592
+ ];
593
+ });
594
+
595
+ // src/engine/claude-code-local/settings.ts
596
+ import { readFileSync } from "fs";
597
+ import { homedir as homedir3 } from "os";
598
+ import { join as join3 } from "path";
599
+ function readClaudeSettings() {
600
+ if (cached !== undefined)
601
+ return cached;
602
+ try {
603
+ const raw = readFileSync(SETTINGS_PATH, "utf8");
604
+ const parsed = JSON.parse(raw);
605
+ if (!parsed || typeof parsed !== "object") {
606
+ cached = null;
607
+ return null;
608
+ }
609
+ const obj = parsed;
610
+ const model = typeof obj.model === "string" && obj.model.length > 0 ? obj.model : undefined;
611
+ cached = { model };
612
+ return cached;
613
+ } catch {
614
+ cached = null;
615
+ return null;
616
+ }
617
+ }
618
+ function resolveClaudeDefaultModelId() {
619
+ const settings = readClaudeSettings();
620
+ if (settings?.model && settings.model.length > 0)
621
+ return settings.model;
622
+ return CLAUDE_FALLBACK_DEFAULT_MODEL_ID;
623
+ }
624
+ var SETTINGS_PATH, cached, CLAUDE_FALLBACK_DEFAULT_MODEL_ID = "claude-opus-4-7[1m]";
625
+ var init_settings = __esm(() => {
626
+ SETTINGS_PATH = join3(homedir3(), ".claude", "settings.json");
627
+ });
628
+
629
+ // src/engine/claude-code-local/capabilities.ts
630
+ var claudeCapabilities, claudeIdentity;
631
+ var init_capabilities = __esm(() => {
632
+ init_models();
633
+ init_settings();
634
+ claudeCapabilities = {
635
+ vendorId: "claude",
636
+ label: "Claude Code",
637
+ models: CLAUDE_MODELS,
638
+ defaultModelId: resolveClaudeDefaultModelId,
639
+ contextWindowFor: claudeContextWindowFor
640
+ };
641
+ claudeIdentity = {
642
+ vendorId: "claude",
643
+ productName: "Claude Code",
644
+ shortName: "Claude",
645
+ assistantName: "Claude",
646
+ inputPlaceholder: "Ask Claude\u2026"
647
+ };
648
+ });
649
+
650
+ // src/engine/claude-code-local/normalize.ts
651
+ function normalizeClaudeContent(content) {
652
+ if (typeof content === "string") {
653
+ return content.length > 0 ? [{ type: "text", text: content }] : [];
654
+ }
655
+ if (!Array.isArray(content))
656
+ return [];
657
+ const out = [];
658
+ for (const block of content) {
659
+ if (typeof block === "string") {
660
+ if (block.length > 0)
661
+ out.push({ type: "text", text: block });
662
+ continue;
663
+ }
664
+ if (!block || typeof block !== "object")
665
+ continue;
666
+ const b = block;
667
+ if (b.type === "text" && typeof b.text === "string") {
668
+ out.push({ type: "text", text: b.text });
669
+ continue;
670
+ }
671
+ if (b.type === "tool_use") {
672
+ out.push({
673
+ type: "tool_call",
674
+ callId: typeof b.id === "string" ? b.id : "",
675
+ name: typeof b.name === "string" ? b.name : "",
676
+ input: b.input
677
+ });
678
+ continue;
679
+ }
680
+ if (b.type === "tool_result") {
681
+ out.push({
682
+ type: "tool_result",
683
+ callId: typeof b.tool_use_id === "string" ? b.tool_use_id : "",
684
+ output: b.content,
685
+ isError: b.is_error === true
686
+ });
687
+ continue;
688
+ }
689
+ if (b.type === "thinking" && typeof b.thinking === "string") {
690
+ out.push({ type: "thinking", text: b.thinking });
691
+ }
692
+ }
693
+ return out;
694
+ }
695
+
452
696
  // src/engine/claude-code-local/history.ts
453
697
  import { randomUUID } from "crypto";
454
698
  import { appendFile, mkdir, readFile, readdir, unlink, writeFile } from "fs/promises";
455
- import { homedir as homedir3 } from "os";
699
+ import { homedir as homedir4 } from "os";
456
700
  import path2 from "path";
457
701
  function encodeCwd(cwd) {
458
702
  return cwd.replace(/[/.]/g, "-");
@@ -523,11 +767,11 @@ function extractMessage(record, fallbackSessionId) {
523
767
  return null;
524
768
  if (!("content" in inner))
525
769
  return null;
526
- const content = inner.content;
770
+ const blocks = normalizeClaudeContent(inner.content);
527
771
  const ts = typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString();
528
772
  const sid = typeof record.sessionId === "string" ? record.sessionId : fallbackSessionId;
529
773
  const usage = extractUsage(inner.usage);
530
- return usage ? { role, content, timestamp: ts, sessionId: sid, usage } : { role, content, timestamp: ts, sessionId: sid };
774
+ return usage ? { role, blocks, timestamp: ts, sessionId: sid, usage } : { role, blocks, timestamp: ts, sessionId: sid };
531
775
  }
532
776
  function extractUsage(v) {
533
777
  if (!isObject(v))
@@ -622,7 +866,7 @@ var defaultDeps2;
622
866
  var init_history = __esm(() => {
623
867
  defaultDeps2 = {
624
868
  projectsDir() {
625
- return path2.join(homedir3(), ".claude", "projects");
869
+ return path2.join(homedir4(), ".claude", "projects");
626
870
  },
627
871
  async readdir(p) {
628
872
  try {
@@ -706,7 +950,7 @@ function delay(ms) {
706
950
 
707
951
  // src/engine/claude-code-local/sessions.ts
708
952
  import { readFile as readFile2, readdir as readdir2, stat } from "fs/promises";
709
- import { homedir as homedir4 } from "os";
953
+ import { homedir as homedir5 } from "os";
710
954
  import path3 from "path";
711
955
  async function listSessionsForCwd(cwd, deps = defaultDeps3) {
712
956
  const projectDir = path3.join(deps.projectsDir(), encodeCwd(cwd));
@@ -758,17 +1002,11 @@ function extractFirstUserMessage(lines) {
758
1002
  return null;
759
1003
  }
760
1004
  function stringifyContent(content) {
761
- if (typeof content === "string")
762
- return content.trim();
763
- if (!Array.isArray(content))
764
- return "";
1005
+ const blocks = normalizeClaudeContent(content);
765
1006
  const parts = [];
766
- for (const block of content) {
767
- if (!isObject2(block))
768
- continue;
769
- if (block.type === "text" && typeof block.text === "string") {
770
- parts.push(block.text);
771
- }
1007
+ for (const b of blocks) {
1008
+ if (b.type === "text")
1009
+ parts.push(b.text);
772
1010
  }
773
1011
  return parts.join(" ").trim();
774
1012
  }
@@ -780,7 +1018,7 @@ var init_sessions = __esm(() => {
780
1018
  init_history();
781
1019
  defaultDeps3 = {
782
1020
  projectsDir() {
783
- return path3.join(homedir4(), ".claude", "projects");
1021
+ return path3.join(homedir5(), ".claude", "projects");
784
1022
  },
785
1023
  async readdir(p) {
786
1024
  try {
@@ -893,47 +1131,981 @@ async function* parseStreamJson(lines, opts = {}) {
893
1131
  }
894
1132
  continue;
895
1133
  }
896
- if (type === "user") {
897
- const content = extractContentBlocks(msg);
898
- for (const block of content) {
899
- if (!isObject3(block))
1134
+ if (type === "user") {
1135
+ const content = extractContentBlocks(msg);
1136
+ for (const block of content) {
1137
+ if (!isObject3(block))
1138
+ continue;
1139
+ const blockType = typeof block.type === "string" ? block.type : undefined;
1140
+ if (blockType === "tool_result") {
1141
+ const id = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
1142
+ const name = id && toolNameById.get(id) || "tool";
1143
+ const output = "content" in block ? block.content : undefined;
1144
+ yield { type: "tool.result", name, output };
1145
+ }
1146
+ }
1147
+ continue;
1148
+ }
1149
+ if (type === "result") {
1150
+ const usage = isObject3(msg.usage) ? msg.usage : undefined;
1151
+ if (usage) {
1152
+ const inTok = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
1153
+ const outTok = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
1154
+ const cacheRead = typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : undefined;
1155
+ const cacheCreate = typeof usage.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : undefined;
1156
+ yield {
1157
+ type: "usage",
1158
+ input_tokens: inTok,
1159
+ output_tokens: outTok,
1160
+ ...cacheRead !== undefined ? { cache_read_input_tokens: cacheRead } : {},
1161
+ ...cacheCreate !== undefined ? { cache_creation_input_tokens: cacheCreate } : {}
1162
+ };
1163
+ }
1164
+ const subtype = typeof msg.subtype === "string" ? msg.subtype : "success";
1165
+ const isApiError = msg.is_error === true || typeof msg.api_error_status === "number";
1166
+ if (isApiError) {
1167
+ const status = typeof msg.api_error_status === "number" ? ` ${msg.api_error_status}` : "";
1168
+ const result = typeof msg.result === "string" ? msg.result.trim() : "";
1169
+ yield { type: "error", message: result ? `claude API error${status}: ${result}` : `claude API error${status}` };
1170
+ return;
1171
+ }
1172
+ if (subtype === "success") {
1173
+ yield { type: "done" };
1174
+ } else {
1175
+ yield { type: "error", message: `claude session ended: ${subtype}` };
1176
+ }
1177
+ return;
1178
+ }
1179
+ }
1180
+ }
1181
+ async function* readLines(stream) {
1182
+ let buf = "";
1183
+ for await (const chunk of stream) {
1184
+ const text = typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
1185
+ buf += text;
1186
+ let nl = buf.indexOf(`
1187
+ `);
1188
+ while (nl !== -1) {
1189
+ yield buf.slice(0, nl);
1190
+ buf = buf.slice(nl + 1);
1191
+ nl = buf.indexOf(`
1192
+ `);
1193
+ }
1194
+ }
1195
+ if (buf.length > 0)
1196
+ yield buf;
1197
+ }
1198
+ function isObject3(v) {
1199
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1200
+ }
1201
+ function extractContentBlocks(msg) {
1202
+ if (Array.isArray(msg.content))
1203
+ return msg.content;
1204
+ const inner = msg.message;
1205
+ if (isObject3(inner) && Array.isArray(inner.content))
1206
+ return inner.content;
1207
+ return [];
1208
+ }
1209
+ function stringifyErr(err) {
1210
+ if (err instanceof Error)
1211
+ return err.message;
1212
+ try {
1213
+ return JSON.stringify(err);
1214
+ } catch {
1215
+ return String(err);
1216
+ }
1217
+ }
1218
+
1219
+ // src/engine/claude-code-local/index.ts
1220
+ class ClaudeCodeLocal {
1221
+ identity = claudeIdentity;
1222
+ capabilities = claudeCapabilities;
1223
+ registry = new SessionRegistry;
1224
+ running = new Map;
1225
+ binaryPathResolver;
1226
+ stopGraceMs;
1227
+ constructor(opts = {}) {
1228
+ this.binaryPathResolver = opts.binaryPathResolver ?? findClaudeBinary;
1229
+ this.stopGraceMs = opts.stopGraceMs ?? 5000;
1230
+ }
1231
+ async spawn(cwd, prompt, opts) {
1232
+ return this.start({ cwd, prompt, opts });
1233
+ }
1234
+ async resume(sessionId, prompt, opts) {
1235
+ const cwd = opts?.cwd ?? opts?.env?.KOBE_RESUME_CWD ?? process.cwd();
1236
+ return this.start({ cwd, prompt, opts, resumeSessionId: sessionId });
1237
+ }
1238
+ stream(handle) {
1239
+ const sid = handle.sessionId;
1240
+ const self = this;
1241
+ return {
1242
+ async* [Symbol.asyncIterator]() {
1243
+ const session = self.running.get(sid);
1244
+ if (!session)
1245
+ return;
1246
+ let idx = 0;
1247
+ while (true) {
1248
+ if (idx < session.queue.length) {
1249
+ const ev = session.queue[idx++];
1250
+ if (!ev)
1251
+ continue;
1252
+ yield ev;
1253
+ if (ev.type === "done" || ev.type === "error")
1254
+ return;
1255
+ continue;
1256
+ }
1257
+ if (session.closed)
1258
+ return;
1259
+ await new Promise((resolve2) => session.waiters.push(resolve2));
1260
+ }
1261
+ }
1262
+ };
1263
+ }
1264
+ async readHistory(sessionId) {
1265
+ const messages = await readHistory(sessionId);
1266
+ const usageMetrics = deriveSessionUsageMetrics(messages);
1267
+ return { messages, ...usageMetrics ? { usageMetrics } : {} };
1268
+ }
1269
+ async deleteHistory(sessionId) {
1270
+ return deleteHistory(sessionId);
1271
+ }
1272
+ async listSessions(cwd) {
1273
+ return listSessionsForCwd(cwd);
1274
+ }
1275
+ async stop(handle) {
1276
+ const sid = handle.sessionId;
1277
+ const session = this.running.get(sid);
1278
+ const shouldRescue = !!session && !session.completedNaturally && session.prompt.trim().length > 0;
1279
+ const rescuePrompt = session?.prompt ?? "";
1280
+ const rescueCwd = session?.cwd ?? handle.cwd;
1281
+ await this.registry.kill(sid, this.stopGraceMs);
1282
+ if (session) {
1283
+ session.closed = true;
1284
+ this.notify(session);
1285
+ this.running.delete(sid);
1286
+ }
1287
+ if (shouldRescue) {
1288
+ try {
1289
+ await appendInterruptedUserPrompt(sid, rescueCwd, rescuePrompt);
1290
+ } catch {}
1291
+ }
1292
+ }
1293
+ async start(args) {
1294
+ const binaryPath = await this.binaryPathResolver();
1295
+ const cliPermissionMode = args.opts?.permissionMode === "plan" ? "plan" : "bypassPermissions";
1296
+ const spawned = spawnClaudeProcess({
1297
+ binaryPath,
1298
+ cwd: args.cwd,
1299
+ prompt: args.prompt,
1300
+ model: args.opts?.model,
1301
+ permissionMode: cliPermissionMode,
1302
+ env: args.opts?.env,
1303
+ resumeSessionId: args.resumeSessionId
1304
+ });
1305
+ let resolveHandle = () => {};
1306
+ let rejectHandle = () => {};
1307
+ const handlePromise = new Promise((res, rej) => {
1308
+ resolveHandle = res;
1309
+ rejectHandle = rej;
1310
+ });
1311
+ const queue = [];
1312
+ let session;
1313
+ let bound = false;
1314
+ const bind = (sessionId) => {
1315
+ if (bound)
1316
+ return;
1317
+ bound = true;
1318
+ session = {
1319
+ sessionId,
1320
+ cwd: args.cwd,
1321
+ spawned,
1322
+ queue,
1323
+ waiters: [],
1324
+ closed: false,
1325
+ completedNaturally: false,
1326
+ prompt: args.prompt,
1327
+ spawnedAtIso: new Date().toISOString()
1328
+ };
1329
+ this.running.set(sessionId, session);
1330
+ this.registry.register({
1331
+ sessionId,
1332
+ cwd: args.cwd,
1333
+ proc: spawned.proc,
1334
+ startedAt: Date.now(),
1335
+ prompt: args.prompt
1336
+ });
1337
+ resolveHandle({ sessionId, cwd: args.cwd });
1338
+ };
1339
+ if (args.resumeSessionId) {
1340
+ try {
1341
+ bind(args.resumeSessionId);
1342
+ } catch (err) {
1343
+ try {
1344
+ spawned.proc.kill("SIGKILL");
1345
+ } catch {}
1346
+ rejectHandle(err);
1347
+ throw err;
1348
+ }
1349
+ }
1350
+ (async () => {
1351
+ const events = parseStreamJson(readLines(spawned.stdout), {
1352
+ onSessionId: (sid) => bind(sid)
1353
+ });
1354
+ try {
1355
+ for await (const ev of events) {
1356
+ const enriched = enrichUsageEvent(ev, session?.spawnedAtIso);
1357
+ queue.push(enriched);
1358
+ if (ev.type === "done" && session) {
1359
+ session.completedNaturally = true;
1360
+ }
1361
+ if (session)
1362
+ this.notify(session);
1363
+ }
1364
+ } catch (err) {
1365
+ const ev = {
1366
+ type: "error",
1367
+ message: `parser failure: ${err instanceof Error ? err.message : String(err)}`
1368
+ };
1369
+ queue.push(ev);
1370
+ if (session)
1371
+ this.notify(session);
1372
+ } finally {
1373
+ if (session) {
1374
+ session.closed = true;
1375
+ this.notify(session);
1376
+ this.registry.unregister(session.sessionId);
1377
+ }
1378
+ if (!bound) {
1379
+ rejectHandle(new Error("claude exited without emitting a session id"));
1380
+ }
1381
+ }
1382
+ })();
1383
+ drainStream(spawned.stderr);
1384
+ spawned.proc.once("error", (err) => {
1385
+ if (!bound)
1386
+ rejectHandle(err);
1387
+ });
1388
+ spawned.proc.once("exit", () => {
1389
+ if (!bound) {
1390
+ rejectHandle(new Error("claude exited before session id was captured"));
1391
+ }
1392
+ });
1393
+ return handlePromise;
1394
+ }
1395
+ notify(session) {
1396
+ const waiters = session.waiters;
1397
+ session.waiters = [];
1398
+ for (const w of waiters)
1399
+ w();
1400
+ }
1401
+ }
1402
+ function drainStream(stream) {
1403
+ const s = stream;
1404
+ s.on("data", () => {});
1405
+ s.on("error", () => {});
1406
+ }
1407
+ function enrichUsageEvent(ev, startedAtIso) {
1408
+ if (ev.type !== "usage")
1409
+ return ev;
1410
+ return { type: "usage", ...withTotalSpeedForTurn(ev, startedAtIso, new Date().toISOString()) };
1411
+ }
1412
+ var init_claude_code_local = __esm(() => {
1413
+ init_binary();
1414
+ init_capabilities();
1415
+ init_history();
1416
+ init_sessions();
1417
+ init_spawn();
1418
+ });
1419
+
1420
+ // src/engine/codex-local/binary.ts
1421
+ import { spawnSync as spawnSync2 } from "child_process";
1422
+ import { existsSync as existsSync3, statSync as statSync2 } from "fs";
1423
+ import { homedir as homedir6 } from "os";
1424
+ import path4 from "path";
1425
+ async function findCodexBinary(deps = defaultDeps4) {
1426
+ const checked = [];
1427
+ const tryPath = (p) => {
1428
+ if (!p)
1429
+ return;
1430
+ checked.push(p);
1431
+ return deps.fileExists(p) ? p : undefined;
1432
+ };
1433
+ const whichResult = deps.which("codex");
1434
+ if (whichResult) {
1435
+ checked.push(`which:${whichResult}`);
1436
+ if (deps.fileExists(whichResult))
1437
+ return whichResult;
1438
+ }
1439
+ const home = deps.home();
1440
+ for (const p of ["/opt/homebrew/bin/codex", "/usr/local/bin/codex", "/usr/bin/codex", "/bin/codex"]) {
1441
+ const candidate = tryPath(p);
1442
+ if (candidate)
1443
+ return candidate;
1444
+ }
1445
+ const nvmBin = deps.env("NVM_BIN");
1446
+ if (nvmBin) {
1447
+ const candidate = tryPath(path4.join(nvmBin, "codex"));
1448
+ if (candidate)
1449
+ return candidate;
1450
+ }
1451
+ for (const rel of [".local/bin/codex", ".bun/bin/codex", "bin/codex"]) {
1452
+ const candidate = tryPath(path4.join(home, rel));
1453
+ if (candidate)
1454
+ return candidate;
1455
+ }
1456
+ throw new CodexBinaryNotFoundError(checked);
1457
+ }
1458
+ var CodexBinaryNotFoundError, defaultDeps4;
1459
+ var init_binary2 = __esm(() => {
1460
+ CodexBinaryNotFoundError = class CodexBinaryNotFoundError extends Error {
1461
+ checkedPaths;
1462
+ constructor(checkedPaths) {
1463
+ super(`Codex CLI binary not found. Checked: ${checkedPaths.join(", ")}. Ensure 'codex' is on PATH (e.g. \`brew install codex\` or the official installer).`);
1464
+ this.name = "CodexBinaryNotFoundError";
1465
+ this.checkedPaths = checkedPaths;
1466
+ }
1467
+ };
1468
+ defaultDeps4 = {
1469
+ fileExists(p) {
1470
+ try {
1471
+ return statSync2(p).isFile();
1472
+ } catch {
1473
+ return false;
1474
+ }
1475
+ },
1476
+ env(name) {
1477
+ return process.env[name];
1478
+ },
1479
+ home() {
1480
+ return homedir6();
1481
+ },
1482
+ which(name) {
1483
+ const cmd = process.platform === "win32" ? "where" : "which";
1484
+ const out = spawnSync2(cmd, [name], { encoding: "utf8" });
1485
+ if (out.status !== 0)
1486
+ return;
1487
+ const first = out.stdout.split(`
1488
+ `).map((l) => l.trim()).filter(Boolean)[0];
1489
+ if (!first)
1490
+ return;
1491
+ if (first.startsWith("codex:") && first.includes("aliased to")) {
1492
+ const aliasTarget = first.split("aliased to")[1]?.trim();
1493
+ return aliasTarget && existsSync3(aliasTarget) ? aliasTarget : undefined;
1494
+ }
1495
+ return first;
1496
+ },
1497
+ readdir(p) {
1498
+ try {
1499
+ const fs = __require("fs");
1500
+ return fs.readdirSync(p);
1501
+ } catch {
1502
+ return [];
1503
+ }
1504
+ }
1505
+ };
1506
+ });
1507
+
1508
+ // src/engine/codex-local/models.ts
1509
+ function codexContextWindowFor(_modelId) {
1510
+ return DEFAULT_CTX;
1511
+ }
1512
+ var CODEX_MODELS, CODEX_FALLBACK_DEFAULT_MODEL_ID = "gpt-5.4-mini", DEFAULT_CTX = 400000;
1513
+ var init_models2 = __esm(() => {
1514
+ CODEX_MODELS = [
1515
+ { vendor: "codex", id: "gpt-5.5", label: "GPT-5.5", hint: "latest, ChatGPT-account compatible" },
1516
+ { vendor: "codex", id: "gpt-5.4", label: "GPT-5.4", hint: "stable" },
1517
+ { vendor: "codex", id: "gpt-5.4-mini", label: "GPT-5.4 mini", hint: "fastest, always supported" }
1518
+ ];
1519
+ });
1520
+
1521
+ // src/engine/codex-local/settings.ts
1522
+ import { readFileSync as readFileSync2 } from "fs";
1523
+ import { homedir as homedir7 } from "os";
1524
+ import { join as join4 } from "path";
1525
+ function readModelFromConfig() {
1526
+ try {
1527
+ const raw = readFileSync2(CONFIG_PATH, "utf8");
1528
+ let inTable = false;
1529
+ for (const line of raw.split(`
1530
+ `)) {
1531
+ const t = line.trim();
1532
+ if (t.startsWith("[") && t.endsWith("]")) {
1533
+ inTable = true;
1534
+ continue;
1535
+ }
1536
+ if (inTable)
1537
+ continue;
1538
+ const m = /^model\s*=\s*"([^"]+)"/.exec(t);
1539
+ if (m)
1540
+ return m[1] ?? null;
1541
+ }
1542
+ return null;
1543
+ } catch {
1544
+ return null;
1545
+ }
1546
+ }
1547
+ function resolveCodexDefaultModelId() {
1548
+ if (cached2 === undefined)
1549
+ cached2 = readModelFromConfig();
1550
+ return cached2 ?? CODEX_FALLBACK_DEFAULT_MODEL_ID;
1551
+ }
1552
+ var CONFIG_PATH, cached2;
1553
+ var init_settings2 = __esm(() => {
1554
+ init_models2();
1555
+ CONFIG_PATH = join4(homedir7(), ".codex", "config.toml");
1556
+ });
1557
+
1558
+ // src/engine/codex-local/capabilities.ts
1559
+ var codexCapabilities, codexIdentity;
1560
+ var init_capabilities2 = __esm(() => {
1561
+ init_models2();
1562
+ init_settings2();
1563
+ codexCapabilities = {
1564
+ vendorId: "codex",
1565
+ label: "Codex",
1566
+ models: CODEX_MODELS,
1567
+ defaultModelId: resolveCodexDefaultModelId,
1568
+ contextWindowFor: codexContextWindowFor
1569
+ };
1570
+ codexIdentity = {
1571
+ vendorId: "codex",
1572
+ productName: "Codex",
1573
+ shortName: "Codex",
1574
+ assistantName: "Codex",
1575
+ inputPlaceholder: "Ask Codex\u2026"
1576
+ };
1577
+ });
1578
+
1579
+ // src/engine/codex-local/normalize.ts
1580
+ function normalizeCodexContent(raw) {
1581
+ if (typeof raw === "string") {
1582
+ return raw.length > 0 ? [{ type: "text", text: raw }] : [];
1583
+ }
1584
+ if (!Array.isArray(raw))
1585
+ return [];
1586
+ const blocks = [];
1587
+ for (const item of raw) {
1588
+ if (typeof item === "string") {
1589
+ if (item.length > 0)
1590
+ blocks.push({ type: "text", text: item });
1591
+ continue;
1592
+ }
1593
+ if (!isObject4(item))
1594
+ continue;
1595
+ const t = typeof item.type === "string" ? item.type : undefined;
1596
+ if (t === "input_text" || t === "output_text") {
1597
+ const text = typeof item.text === "string" ? item.text : "";
1598
+ if (text.length > 0)
1599
+ blocks.push({ type: "text", text });
1600
+ continue;
1601
+ }
1602
+ if (t)
1603
+ blocks.push({ type: "text", text: `[codex: ${t}]` });
1604
+ }
1605
+ return blocks;
1606
+ }
1607
+ function isObject4(v) {
1608
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1609
+ }
1610
+
1611
+ // src/engine/codex-local/history.ts
1612
+ import { readFile as readFile3, readdir as readdir3, unlink as unlink2 } from "fs/promises";
1613
+ import { homedir as homedir8 } from "os";
1614
+ import path5 from "path";
1615
+ async function listRolloutFiles(deps = defaultDeps5) {
1616
+ const root = deps.sessionsDir();
1617
+ const years = (await deps.readdir(root)).sort().reverse();
1618
+ const out = [];
1619
+ for (const y of years) {
1620
+ const yp = path5.join(root, y);
1621
+ const months = (await deps.readdir(yp)).sort().reverse();
1622
+ for (const m of months) {
1623
+ const mp = path5.join(yp, m);
1624
+ const days = (await deps.readdir(mp)).sort().reverse();
1625
+ for (const d of days) {
1626
+ const dp = path5.join(mp, d);
1627
+ const files = (await deps.readdir(dp)).filter((f) => f.startsWith("rollout-") && f.endsWith(".jsonl"));
1628
+ files.sort().reverse();
1629
+ for (const f of files)
1630
+ out.push(path5.join(dp, f));
1631
+ }
1632
+ }
1633
+ }
1634
+ return out;
1635
+ }
1636
+ async function findRolloutFile(sessionId, deps = defaultDeps5) {
1637
+ const all = await listRolloutFiles(deps);
1638
+ for (const p of all) {
1639
+ if (path5.basename(p).endsWith(`-${sessionId}.jsonl`))
1640
+ return p;
1641
+ }
1642
+ return;
1643
+ }
1644
+ async function readHistoryWithMetrics(sessionId, deps = defaultDeps5) {
1645
+ const file = await findRolloutFile(sessionId, deps);
1646
+ if (!file)
1647
+ return { messages: [] };
1648
+ let raw;
1649
+ try {
1650
+ raw = await deps.readFile(file);
1651
+ } catch {
1652
+ return { messages: [] };
1653
+ }
1654
+ const messages = sortByTimestamp2(parseJsonl2(raw, sessionId));
1655
+ const usageMetrics = deriveCodexUsageMetrics(raw);
1656
+ return { messages, ...usageMetrics ? { usageMetrics } : {} };
1657
+ }
1658
+ async function deleteHistory2(sessionId, deps = defaultDeps5) {
1659
+ const file = await findRolloutFile(sessionId, deps);
1660
+ if (!file)
1661
+ return;
1662
+ try {
1663
+ await unlink2(file);
1664
+ } catch (err) {
1665
+ if (err.code === "ENOENT")
1666
+ return;
1667
+ throw err;
1668
+ }
1669
+ }
1670
+ function sortByTimestamp2(messages) {
1671
+ return messages.map((msg, idx) => ({ msg, idx })).sort((a, b) => {
1672
+ if (a.msg.timestamp < b.msg.timestamp)
1673
+ return -1;
1674
+ if (a.msg.timestamp > b.msg.timestamp)
1675
+ return 1;
1676
+ return a.idx - b.idx;
1677
+ }).map((entry) => entry.msg);
1678
+ }
1679
+ function parseJsonl2(raw, sessionId) {
1680
+ const out = [];
1681
+ for (const line of raw.split(`
1682
+ `)) {
1683
+ const trimmed = line.trim();
1684
+ if (!trimmed)
1685
+ continue;
1686
+ let parsed;
1687
+ try {
1688
+ parsed = JSON.parse(trimmed);
1689
+ } catch {
1690
+ continue;
1691
+ }
1692
+ if (!isObject5(parsed))
1693
+ continue;
1694
+ if (parsed.type !== "response_item")
1695
+ continue;
1696
+ const payload = isObject5(parsed.payload) ? parsed.payload : undefined;
1697
+ if (!payload)
1698
+ continue;
1699
+ if (payload.type !== "message")
1700
+ continue;
1701
+ const role = payload.role;
1702
+ if (role !== "user" && role !== "assistant" && role !== "system")
1703
+ continue;
1704
+ const blocks = normalizeCodexContent(payload.content);
1705
+ if (role === "user" && isEnvironmentContextEnvelope(blocks))
1706
+ continue;
1707
+ const ts = typeof parsed.timestamp === "string" ? parsed.timestamp : new Date().toISOString();
1708
+ out.push({ role, blocks, timestamp: ts, sessionId });
1709
+ }
1710
+ return out;
1711
+ }
1712
+ function deriveCodexUsageMetrics(raw) {
1713
+ let latestUsage;
1714
+ let latestUsageTimestampMs = null;
1715
+ let lastUserTimestampMs = null;
1716
+ let inputTokens = 0;
1717
+ let outputTokens = 0;
1718
+ const intervals = [];
1719
+ for (const line of raw.split(`
1720
+ `)) {
1721
+ const trimmed = line.trim();
1722
+ if (!trimmed)
1723
+ continue;
1724
+ let parsed;
1725
+ try {
1726
+ parsed = JSON.parse(trimmed);
1727
+ } catch {
1728
+ continue;
1729
+ }
1730
+ if (!isObject5(parsed))
1731
+ continue;
1732
+ const timestampMs = typeof parsed.timestamp === "string" ? parseTimestampMs2(parsed.timestamp) : null;
1733
+ if (parsed.type === "response_item") {
1734
+ const payload = isObject5(parsed.payload) ? parsed.payload : undefined;
1735
+ if (payload?.type === "message" && payload.role === "user" && timestampMs !== null) {
1736
+ const blocks = normalizeCodexContent(payload.content);
1737
+ if (!isEnvironmentContextEnvelope(blocks))
1738
+ lastUserTimestampMs = timestampMs;
1739
+ }
1740
+ continue;
1741
+ }
1742
+ if (parsed.type !== "turn.completed")
1743
+ continue;
1744
+ const usage = isObject5(parsed.usage) ? parsed.usage : undefined;
1745
+ if (!usage)
1746
+ continue;
1747
+ const snapshot = codexUsageToSnapshot(usage);
1748
+ if (!snapshot)
1749
+ continue;
1750
+ if (timestampMs !== null && (latestUsageTimestampMs === null || timestampMs > latestUsageTimestampMs)) {
1751
+ latestUsageTimestampMs = timestampMs;
1752
+ latestUsage = snapshot;
1753
+ } else if (latestUsage === undefined) {
1754
+ latestUsage = snapshot;
1755
+ }
1756
+ inputTokens += snapshot.input_tokens;
1757
+ outputTokens += snapshot.output_tokens;
1758
+ if (timestampMs !== null && lastUserTimestampMs !== null && timestampMs > lastUserTimestampMs) {
1759
+ intervals.push({ startMs: lastUserTimestampMs, endMs: timestampMs });
1760
+ }
1761
+ }
1762
+ if (!latestUsage)
1763
+ return;
1764
+ const durationMs2 = mergedDurationMs(intervals);
1765
+ if (durationMs2 <= 0)
1766
+ return latestUsage;
1767
+ return {
1768
+ ...latestUsage,
1769
+ total_speed_tokens_per_second: (inputTokens + outputTokens) / (durationMs2 / 1000)
1770
+ };
1771
+ }
1772
+ function codexUsageToSnapshot(usage) {
1773
+ const input = numberOr(usage.input_tokens, 0);
1774
+ const output = numberOr(usage.output_tokens, 0) + numberOr(usage.reasoning_output_tokens, 0);
1775
+ const cacheRead = typeof usage.cached_input_tokens === "number" ? usage.cached_input_tokens : undefined;
1776
+ if (input <= 0 && output <= 0 && cacheRead === undefined)
1777
+ return;
1778
+ return {
1779
+ input_tokens: input,
1780
+ output_tokens: output,
1781
+ ...cacheRead !== undefined ? { cache_read_input_tokens: cacheRead } : {}
1782
+ };
1783
+ }
1784
+ function parseTimestampMs2(value) {
1785
+ const ms = new Date(value).getTime();
1786
+ return Number.isFinite(ms) ? ms : null;
1787
+ }
1788
+ function mergedDurationMs(intervals) {
1789
+ if (intervals.length === 0)
1790
+ return 0;
1791
+ const sorted = [...intervals].sort((a, b) => a.startMs - b.startMs);
1792
+ let total = 0;
1793
+ let current = sorted[0];
1794
+ if (!current)
1795
+ return 0;
1796
+ for (let i = 1;i < sorted.length; i++) {
1797
+ const next = sorted[i];
1798
+ if (!next)
1799
+ continue;
1800
+ if (next.startMs <= current.endMs) {
1801
+ current = { startMs: current.startMs, endMs: Math.max(current.endMs, next.endMs) };
1802
+ } else {
1803
+ total += current.endMs - current.startMs;
1804
+ current = next;
1805
+ }
1806
+ }
1807
+ total += current.endMs - current.startMs;
1808
+ return total;
1809
+ }
1810
+ function numberOr(v, fallback) {
1811
+ return typeof v === "number" && Number.isFinite(v) ? v : fallback;
1812
+ }
1813
+ function isEnvironmentContextEnvelope(blocks) {
1814
+ if (blocks.length === 0)
1815
+ return false;
1816
+ for (const b of blocks) {
1817
+ if (b.type !== "text")
1818
+ return false;
1819
+ const t = (b.text ?? "").trim();
1820
+ if (!t.startsWith("<environment_context>") || !t.endsWith("</environment_context>"))
1821
+ return false;
1822
+ }
1823
+ return true;
1824
+ }
1825
+ function isObject5(v) {
1826
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1827
+ }
1828
+ var defaultDeps5;
1829
+ var init_history2 = __esm(() => {
1830
+ defaultDeps5 = {
1831
+ sessionsDir() {
1832
+ return path5.join(homedir8(), ".codex", "sessions");
1833
+ },
1834
+ async readdir(p) {
1835
+ try {
1836
+ return await readdir3(p);
1837
+ } catch {
1838
+ return [];
1839
+ }
1840
+ },
1841
+ async readFile(p) {
1842
+ return await readFile3(p, "utf8");
1843
+ }
1844
+ };
1845
+ });
1846
+
1847
+ // src/engine/codex-local/sessions.ts
1848
+ import { open, stat as stat2 } from "fs/promises";
1849
+ async function listSessionsForCwd2(cwd, deps) {
1850
+ const files = await listRolloutFiles(deps);
1851
+ const out = [];
1852
+ for (const file of files) {
1853
+ const meta = await tryReadMeta(file);
1854
+ if (!meta)
1855
+ continue;
1856
+ if (meta.cwd !== cwd)
1857
+ continue;
1858
+ out.push({
1859
+ sessionId: meta.sessionId,
1860
+ mtimeMs: meta.mtimeMs,
1861
+ firstUserMessage: meta.firstUserMessage,
1862
+ messageCount: meta.messageCount
1863
+ });
1864
+ }
1865
+ out.sort((a, b) => b.mtimeMs - a.mtimeMs);
1866
+ return out;
1867
+ }
1868
+ async function tryReadMeta(file) {
1869
+ let st;
1870
+ try {
1871
+ st = await stat2(file);
1872
+ } catch {
1873
+ return null;
1874
+ }
1875
+ let sessionId;
1876
+ let cwd;
1877
+ let firstUser = null;
1878
+ let messageCount = 0;
1879
+ const handle = await open(file, "r").catch(() => null);
1880
+ if (!handle)
1881
+ return null;
1882
+ try {
1883
+ let buf = "";
1884
+ let lineCount = 0;
1885
+ const processLine = (line) => {
1886
+ lineCount++;
1887
+ const parsed = safeParse(line);
1888
+ if (parsed) {
1889
+ if (parsed.type === "session_meta") {
1890
+ const payload = parsed.payload;
1891
+ if (isObject6(payload)) {
1892
+ if (typeof payload.id === "string")
1893
+ sessionId = payload.id;
1894
+ if (typeof payload.cwd === "string")
1895
+ cwd = payload.cwd;
1896
+ }
1897
+ } else if (parsed.type === "response_item" && isObject6(parsed.payload)) {
1898
+ const p = parsed.payload;
1899
+ if (p.type === "message") {
1900
+ messageCount++;
1901
+ if (!firstUser && p.role === "user") {
1902
+ firstUser = extractText(p.content)?.slice(0, PREVIEW_CHAR_CAP) ?? null;
1903
+ }
1904
+ }
1905
+ }
1906
+ }
1907
+ return lineCount >= PREVIEW_HEAD_LINES;
1908
+ };
1909
+ const reader = handle.createReadStream({ encoding: "utf8" });
1910
+ outer:
1911
+ for await (const chunk of reader) {
1912
+ buf += chunk;
1913
+ let nl = buf.indexOf(`
1914
+ `);
1915
+ while (nl !== -1) {
1916
+ const line = buf.slice(0, nl);
1917
+ buf = buf.slice(nl + 1);
1918
+ nl = buf.indexOf(`
1919
+ `);
1920
+ if (processLine(line))
1921
+ break outer;
1922
+ }
1923
+ }
1924
+ if (lineCount < PREVIEW_HEAD_LINES && buf.trim())
1925
+ processLine(buf);
1926
+ } finally {
1927
+ await handle.close().catch(() => {});
1928
+ }
1929
+ if (!sessionId || !cwd)
1930
+ return null;
1931
+ return {
1932
+ sessionId,
1933
+ cwd,
1934
+ mtimeMs: st.mtimeMs,
1935
+ firstUserMessage: firstUser,
1936
+ messageCount
1937
+ };
1938
+ }
1939
+ function safeParse(line) {
1940
+ const t = line.trim();
1941
+ if (!t)
1942
+ return null;
1943
+ try {
1944
+ const v = JSON.parse(t);
1945
+ return isObject6(v) ? v : null;
1946
+ } catch {
1947
+ return null;
1948
+ }
1949
+ }
1950
+ function extractText(content) {
1951
+ if (typeof content === "string")
1952
+ return content;
1953
+ if (!Array.isArray(content))
1954
+ return null;
1955
+ for (const item of content) {
1956
+ if (typeof item === "string" && item.length > 0)
1957
+ return item;
1958
+ if (isObject6(item)) {
1959
+ const t = item.type;
1960
+ if ((t === "input_text" || t === "output_text" || t === "text") && typeof item.text === "string") {
1961
+ return item.text;
1962
+ }
1963
+ }
1964
+ }
1965
+ return null;
1966
+ }
1967
+ function isObject6(v) {
1968
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1969
+ }
1970
+ var PREVIEW_HEAD_LINES = 40, PREVIEW_CHAR_CAP = 200;
1971
+ var init_sessions2 = __esm(() => {
1972
+ init_history2();
1973
+ });
1974
+
1975
+ // src/engine/codex-local/spawn.ts
1976
+ import { spawn as spawn3 } from "child_process";
1977
+ function spawnCodexProcess(opts) {
1978
+ const args = buildArgs2(opts);
1979
+ const proc = spawn3(opts.binaryPath, args, {
1980
+ cwd: opts.cwd,
1981
+ env: { ...process.env, ...opts.env ?? {} },
1982
+ stdio: ["pipe", "pipe", "pipe"]
1983
+ });
1984
+ try {
1985
+ proc.stdin.end();
1986
+ } catch {}
1987
+ return {
1988
+ proc,
1989
+ stdout: proc.stdout,
1990
+ stderr: proc.stderr,
1991
+ args
1992
+ };
1993
+ }
1994
+ function buildArgs2(opts) {
1995
+ const isResume = !!opts.resumeSessionId;
1996
+ const args = ["exec"];
1997
+ if (isResume) {
1998
+ args.push("resume", opts.resumeSessionId);
1999
+ }
2000
+ args.push("--json", "--skip-git-repo-check");
2001
+ if (opts.model) {
2002
+ args.push("-m", opts.model);
2003
+ }
2004
+ if (!isResume) {
2005
+ args.push("-C", opts.cwd);
2006
+ if (opts.permissionMode === "plan")
2007
+ args.push("-s", "read-only");
2008
+ }
2009
+ if (opts.permissionMode !== "plan") {
2010
+ args.push("--dangerously-bypass-approvals-and-sandbox");
2011
+ }
2012
+ if (opts.extraArgs && opts.extraArgs.length > 0) {
2013
+ args.push(...opts.extraArgs);
2014
+ }
2015
+ args.push(opts.prompt);
2016
+ return args;
2017
+ }
2018
+ var init_spawn2 = () => {};
2019
+
2020
+ // src/engine/codex-local/stream.ts
2021
+ async function* parseStreamJson2(lines, opts = {}) {
2022
+ let sessionIdEmitted = false;
2023
+ const toolNameById = new Map;
2024
+ const startedByItemId = new Set;
2025
+ for await (const rawLine of lines) {
2026
+ const line = rawLine.trim();
2027
+ if (!line)
2028
+ continue;
2029
+ let msg;
2030
+ try {
2031
+ msg = JSON.parse(line);
2032
+ } catch (err) {
2033
+ yield { type: "error", message: `codex stream-json parse failed: ${stringifyErr2(err)}` };
2034
+ continue;
2035
+ }
2036
+ if (!isObject7(msg))
2037
+ continue;
2038
+ const type = typeof msg.type === "string" ? msg.type : undefined;
2039
+ if (!type)
2040
+ continue;
2041
+ if (type === "thread.started") {
2042
+ const sid = typeof msg.thread_id === "string" ? msg.thread_id : undefined;
2043
+ if (sid && !sessionIdEmitted) {
2044
+ sessionIdEmitted = true;
2045
+ opts.onSessionId?.(sid);
2046
+ }
2047
+ continue;
2048
+ }
2049
+ if (type === "turn.started")
2050
+ continue;
2051
+ if (type === "item.started" || type === "item.completed") {
2052
+ const item = isObject7(msg.item) ? msg.item : undefined;
2053
+ if (!item)
2054
+ continue;
2055
+ const itemId = typeof item.id === "string" ? item.id : undefined;
2056
+ const itemType = typeof item.type === "string" ? item.type : "tool";
2057
+ if (itemType === "agent_message") {
2058
+ if (type !== "item.completed")
900
2059
  continue;
901
- const blockType = typeof block.type === "string" ? block.type : undefined;
902
- if (blockType === "tool_result") {
903
- const id = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
904
- const name = id && toolNameById.get(id) || "tool";
905
- const output = "content" in block ? block.content : undefined;
906
- yield { type: "tool.result", name, output };
907
- }
2060
+ const text = typeof item.text === "string" ? item.text : "";
2061
+ if (text)
2062
+ yield { type: "assistant.delta", text };
2063
+ continue;
2064
+ }
2065
+ if (itemId) {
2066
+ toolNameById.set(itemId, itemType);
2067
+ }
2068
+ if (type === "item.started") {
2069
+ if (itemId)
2070
+ startedByItemId.add(itemId);
2071
+ const input = stripIdAndType(item);
2072
+ yield { type: "tool.start", name: itemType, input };
2073
+ continue;
2074
+ }
2075
+ if (itemId && !startedByItemId.has(itemId)) {
2076
+ const input = stripIdAndType(item);
2077
+ yield { type: "tool.start", name: itemType, input };
908
2078
  }
2079
+ if (itemId)
2080
+ startedByItemId.delete(itemId);
2081
+ const output = stripIdAndType(item);
2082
+ yield { type: "tool.result", name: itemType, output };
909
2083
  continue;
910
2084
  }
911
- if (type === "result") {
912
- const usage = isObject3(msg.usage) ? msg.usage : undefined;
2085
+ if (type === "turn.completed") {
2086
+ const usage = isObject7(msg.usage) ? msg.usage : undefined;
913
2087
  if (usage) {
914
- const inTok = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
915
- const outTok = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
916
- const cacheRead = typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : undefined;
917
- const cacheCreate = typeof usage.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : undefined;
2088
+ const inTok = numberOr2(usage.input_tokens, 0);
2089
+ const outTok = numberOr2(usage.output_tokens, 0) + numberOr2(usage.reasoning_output_tokens, 0);
2090
+ const cacheRead = typeof usage.cached_input_tokens === "number" ? usage.cached_input_tokens : undefined;
918
2091
  yield {
919
2092
  type: "usage",
920
2093
  input_tokens: inTok,
921
2094
  output_tokens: outTok,
922
- ...cacheRead !== undefined ? { cache_read_input_tokens: cacheRead } : {},
923
- ...cacheCreate !== undefined ? { cache_creation_input_tokens: cacheCreate } : {}
2095
+ ...cacheRead !== undefined ? { cache_read_input_tokens: cacheRead } : {}
924
2096
  };
925
2097
  }
926
- const subtype = typeof msg.subtype === "string" ? msg.subtype : "success";
927
- if (subtype === "success") {
928
- yield { type: "done" };
929
- } else {
930
- yield { type: "error", message: `claude session ended: ${subtype}` };
931
- }
2098
+ yield { type: "done" };
2099
+ return;
2100
+ }
2101
+ if (type === "error") {
2102
+ const message = typeof msg.message === "string" ? msg.message : "codex emitted an error";
2103
+ yield { type: "error", message };
932
2104
  return;
933
2105
  }
934
2106
  }
935
2107
  }
936
- async function* readLines(stream) {
2108
+ async function* readLines2(stream) {
937
2109
  let buf = "";
938
2110
  for await (const chunk of stream) {
939
2111
  const text = typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
@@ -950,18 +2122,17 @@ async function* readLines(stream) {
950
2122
  if (buf.length > 0)
951
2123
  yield buf;
952
2124
  }
953
- function isObject3(v) {
2125
+ function isObject7(v) {
954
2126
  return typeof v === "object" && v !== null && !Array.isArray(v);
955
2127
  }
956
- function extractContentBlocks(msg) {
957
- if (Array.isArray(msg.content))
958
- return msg.content;
959
- const inner = msg.message;
960
- if (isObject3(inner) && Array.isArray(inner.content))
961
- return inner.content;
962
- return [];
2128
+ function numberOr2(v, fallback) {
2129
+ return typeof v === "number" && Number.isFinite(v) ? v : fallback;
963
2130
  }
964
- function stringifyErr(err) {
2131
+ function stripIdAndType(item) {
2132
+ const { id: _id, type: _type, ...rest } = item;
2133
+ return rest;
2134
+ }
2135
+ function stringifyErr2(err) {
965
2136
  if (err instanceof Error)
966
2137
  return err.message;
967
2138
  try {
@@ -971,14 +2142,16 @@ function stringifyErr(err) {
971
2142
  }
972
2143
  }
973
2144
 
974
- // src/engine/claude-code-local/index.ts
975
- class ClaudeCodeLocal {
2145
+ // src/engine/codex-local/index.ts
2146
+ class CodexLocal {
2147
+ identity = codexIdentity;
2148
+ capabilities = codexCapabilities;
976
2149
  registry = new SessionRegistry;
977
2150
  running = new Map;
978
2151
  binaryPathResolver;
979
2152
  stopGraceMs;
980
2153
  constructor(opts = {}) {
981
- this.binaryPathResolver = opts.binaryPathResolver ?? findClaudeBinary;
2154
+ this.binaryPathResolver = opts.binaryPathResolver ?? findCodexBinary;
982
2155
  this.stopGraceMs = opts.stopGraceMs ?? 5000;
983
2156
  }
984
2157
  async spawn(cwd, prompt, opts) {
@@ -1015,41 +2188,32 @@ class ClaudeCodeLocal {
1015
2188
  };
1016
2189
  }
1017
2190
  async readHistory(sessionId) {
1018
- return readHistory(sessionId);
2191
+ return readHistoryWithMetrics(sessionId);
1019
2192
  }
1020
2193
  async deleteHistory(sessionId) {
1021
- return deleteHistory(sessionId);
2194
+ return deleteHistory2(sessionId);
1022
2195
  }
1023
2196
  async listSessions(cwd) {
1024
- return listSessionsForCwd(cwd);
2197
+ return listSessionsForCwd2(cwd);
1025
2198
  }
1026
2199
  async stop(handle) {
1027
2200
  const sid = handle.sessionId;
1028
- const session = this.running.get(sid);
1029
- const shouldRescue = !!session && !session.completedNaturally && session.prompt.trim().length > 0;
1030
- const rescuePrompt = session?.prompt ?? "";
1031
- const rescueCwd = session?.cwd ?? handle.cwd;
1032
2201
  await this.registry.kill(sid, this.stopGraceMs);
2202
+ const session = this.running.get(sid);
1033
2203
  if (session) {
1034
2204
  session.closed = true;
1035
2205
  this.notify(session);
1036
2206
  this.running.delete(sid);
1037
2207
  }
1038
- if (shouldRescue) {
1039
- try {
1040
- await appendInterruptedUserPrompt(sid, rescueCwd, rescuePrompt);
1041
- } catch {}
1042
- }
1043
2208
  }
1044
2209
  async start(args) {
1045
2210
  const binaryPath = await this.binaryPathResolver();
1046
- const cliPermissionMode = args.opts?.permissionMode === "plan" ? "plan" : "bypassPermissions";
1047
- const spawned = spawnClaudeProcess({
2211
+ const spawned = spawnCodexProcess({
1048
2212
  binaryPath,
1049
2213
  cwd: args.cwd,
1050
2214
  prompt: args.prompt,
1051
2215
  model: args.opts?.model,
1052
- permissionMode: cliPermissionMode,
2216
+ permissionMode: args.opts?.permissionMode,
1053
2217
  env: args.opts?.env,
1054
2218
  resumeSessionId: args.resumeSessionId
1055
2219
  });
@@ -1073,8 +2237,7 @@ class ClaudeCodeLocal {
1073
2237
  queue,
1074
2238
  waiters: [],
1075
2239
  closed: false,
1076
- completedNaturally: false,
1077
- prompt: args.prompt
2240
+ spawnedAtIso: new Date().toISOString()
1078
2241
  };
1079
2242
  this.running.set(sessionId, session);
1080
2243
  this.registry.register({
@@ -1098,22 +2261,20 @@ class ClaudeCodeLocal {
1098
2261
  }
1099
2262
  }
1100
2263
  (async () => {
1101
- const events = parseStreamJson(readLines(spawned.stdout), {
2264
+ const events = parseStreamJson2(readLines2(spawned.stdout), {
1102
2265
  onSessionId: (sid) => bind(sid)
1103
2266
  });
1104
2267
  try {
1105
2268
  for await (const ev of events) {
1106
- queue.push(ev);
1107
- if (ev.type === "done" && session) {
1108
- session.completedNaturally = true;
1109
- }
2269
+ const enriched = enrichUsageEvent2(ev, session?.spawnedAtIso);
2270
+ queue.push(enriched);
1110
2271
  if (session)
1111
2272
  this.notify(session);
1112
2273
  }
1113
2274
  } catch (err) {
1114
2275
  const ev = {
1115
2276
  type: "error",
1116
- message: `parser failure: ${err instanceof Error ? err.message : String(err)}`
2277
+ message: `codex parser failure: ${err instanceof Error ? err.message : String(err)}`
1117
2278
  };
1118
2279
  queue.push(ev);
1119
2280
  if (session)
@@ -1123,20 +2284,21 @@ class ClaudeCodeLocal {
1123
2284
  session.closed = true;
1124
2285
  this.notify(session);
1125
2286
  this.registry.unregister(session.sessionId);
2287
+ this.running.delete(session.sessionId);
1126
2288
  }
1127
2289
  if (!bound) {
1128
- rejectHandle(new Error("claude exited without emitting a session id"));
2290
+ rejectHandle(new Error("codex exited without emitting a session id"));
1129
2291
  }
1130
2292
  }
1131
2293
  })();
1132
- drainStream(spawned.stderr);
2294
+ drainStream2(spawned.stderr);
1133
2295
  spawned.proc.once("error", (err) => {
1134
2296
  if (!bound)
1135
2297
  rejectHandle(err);
1136
2298
  });
1137
2299
  spawned.proc.once("exit", () => {
1138
2300
  if (!bound) {
1139
- rejectHandle(new Error("claude exited before session id was captured"));
2301
+ rejectHandle(new Error("codex exited before session id was captured"));
1140
2302
  }
1141
2303
  });
1142
2304
  return handlePromise;
@@ -1148,25 +2310,31 @@ class ClaudeCodeLocal {
1148
2310
  w();
1149
2311
  }
1150
2312
  }
1151
- function drainStream(stream) {
2313
+ function drainStream2(stream) {
1152
2314
  const s = stream;
1153
2315
  s.on("data", () => {});
1154
2316
  s.on("error", () => {});
1155
2317
  }
1156
- var init_claude_code_local = __esm(() => {
1157
- init_binary();
1158
- init_history();
1159
- init_sessions();
1160
- init_spawn();
2318
+ function enrichUsageEvent2(ev, startedAtIso) {
2319
+ if (ev.type !== "usage")
2320
+ return ev;
2321
+ return { type: "usage", ...withTotalSpeedForTurn(ev, startedAtIso, new Date().toISOString()) };
2322
+ }
2323
+ var init_codex_local = __esm(() => {
2324
+ init_binary2();
2325
+ init_capabilities2();
2326
+ init_history2();
2327
+ init_sessions2();
2328
+ init_spawn2();
1161
2329
  });
1162
2330
 
1163
2331
  // src/orchestrator/bridge/server.ts
1164
- import { mkdir as mkdir2, unlink as unlink2 } from "fs/promises";
2332
+ import { mkdir as mkdir2, unlink as unlink3 } from "fs/promises";
1165
2333
  import { createServer } from "net";
1166
2334
  import { dirname as dirname2 } from "path";
1167
2335
  async function startBridgeServer(orch, socketPath) {
1168
2336
  await mkdir2(dirname2(socketPath), { recursive: true });
1169
- await unlink2(socketPath).catch(() => {});
2337
+ await unlink3(socketPath).catch(() => {});
1170
2338
  const conns = new Set;
1171
2339
  const server = createServer((conn) => {
1172
2340
  conns.add(conn);
@@ -1207,7 +2375,7 @@ async function startBridgeServer(orch, socketPath) {
1207
2375
  conn.destroy();
1208
2376
  conns.clear();
1209
2377
  await new Promise((resolve2) => server.close(() => resolve2()));
1210
- await unlink2(socketPath).catch(() => {});
2378
+ await unlink3(socketPath).catch(() => {});
1211
2379
  }
1212
2380
  };
1213
2381
  }
@@ -1300,23 +2468,19 @@ __export(exports_bridge, {
1300
2468
  bridgeSocketPathForHome: () => bridgeSocketPathForHome
1301
2469
  });
1302
2470
  import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
1303
- import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
1304
- import { join as join3 } from "path";
2471
+ import { homedir as homedir9 } from "os";
2472
+ import { join as join5 } from "path";
1305
2473
  import { fileURLToPath as fileURLToPath2 } from "url";
1306
2474
  function bridgeSocketPathForHome(home, pid = process.pid) {
1307
- const runDir = join3(home, ".kobe", "run");
1308
- const preferred = join3(runDir, `bridge-${pid}.sock`);
1309
- const macTempSocket = process.platform === "darwin" && preferred.startsWith(tmpdir2());
1310
- if (preferred.length <= UNIX_SOCKET_PATH_LIMIT && !macTempSocket)
1311
- return preferred;
1312
- const shortTmp = process.platform === "darwin" ? "/tmp" : tmpdir2();
1313
- return join3(shortTmp, `kobe-bridge-${pid}.sock`);
2475
+ const runDir = join5(home, ".kobe", "run");
2476
+ return fitSocketPath(join5(runDir, `bridge-${pid}.sock`), home, "bridge", pid);
1314
2477
  }
1315
2478
  async function startBridge(orch, opts = {}) {
1316
- const home = opts.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir5();
1317
- const runDir = join3(home, ".kobe", "run");
2479
+ const home = opts.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir9();
2480
+ const runDir = join5(home, ".kobe", "run");
1318
2481
  const socketPath = bridgeSocketPathForHome(home);
1319
- const mcpConfigPath = join3(runDir, `mcp-${process.pid}.json`);
2482
+ const mcpConfigPath = join5(runDir, `mcp-${process.pid}.json`);
2483
+ await mkdir3(runDir, { recursive: true });
1320
2484
  const server = await startBridgeServer(orch, socketPath);
1321
2485
  await mkdir3(runDir, { recursive: true });
1322
2486
  const moduleExt = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
@@ -1339,8 +2503,8 @@ async function startBridge(orch, opts = {}) {
1339
2503
  }
1340
2504
  };
1341
2505
  }
1342
- var UNIX_SOCKET_PATH_LIMIT = 103;
1343
2506
  var init_bridge = __esm(() => {
2507
+ init_paths();
1344
2508
  init_server();
1345
2509
  });
1346
2510
 
@@ -2572,134 +3736,81 @@ var init_dev = __esm(() => {
2572
3736
  }
2573
3737
  });
2574
3738
 
2575
- // src/engine/claude-settings.ts
2576
- import { readFileSync } from "fs";
2577
- import { homedir as homedir6 } from "os";
2578
- import { join as join4 } from "path";
2579
- function readClaudeSettings() {
2580
- if (cached !== undefined)
2581
- return cached;
2582
- try {
2583
- const raw = readFileSync(SETTINGS_PATH, "utf8");
2584
- const parsed = JSON.parse(raw);
2585
- if (!parsed || typeof parsed !== "object") {
2586
- cached = null;
2587
- return null;
2588
- }
2589
- const obj = parsed;
2590
- const model = typeof obj.model === "string" && obj.model.length > 0 ? obj.model : undefined;
2591
- cached = { model };
2592
- return cached;
2593
- } catch {
2594
- cached = null;
2595
- return null;
3739
+ // src/engine/registry.ts
3740
+ function getCapabilities(vendor) {
3741
+ return ENGINE_REGISTRY[vendor] ?? defaultCapabilities;
3742
+ }
3743
+ function getIdentity(vendor) {
3744
+ return ENGINE_IDENTITIES[vendor] ?? defaultIdentity;
3745
+ }
3746
+ function capabilitiesForModelId(modelId) {
3747
+ if (!modelId)
3748
+ return defaultCapabilities;
3749
+ for (const caps of Object.values(ENGINE_REGISTRY)) {
3750
+ if (!caps)
3751
+ continue;
3752
+ if (caps.models.some((m) => m.id === modelId))
3753
+ return caps;
2596
3754
  }
3755
+ return defaultCapabilities;
2597
3756
  }
2598
- function resolveDefaultModelId() {
2599
- const settings = readClaudeSettings();
2600
- if (settings?.model && settings.model.length > 0)
2601
- return settings.model;
2602
- return FALLBACK_DEFAULT_MODEL_ID;
2603
- }
2604
- var SETTINGS_PATH, cached, FALLBACK_DEFAULT_MODEL_ID = "claude-opus-4-7[1m]";
2605
- var init_claude_settings = __esm(() => {
2606
- SETTINGS_PATH = join4(homedir6(), ".claude", "settings.json");
2607
- });
2608
-
2609
- // src/session/usage-metrics.ts
2610
- function totalContextTokens(u) {
2611
- return u.input_tokens + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
2612
- }
2613
- function parseTimestampMs(value) {
2614
- const ms = new Date(value).getTime();
2615
- return Number.isFinite(ms) ? ms : null;
2616
- }
2617
- function mergeIntervals(intervals) {
2618
- if (intervals.length === 0)
2619
- return [];
2620
- const sorted = [...intervals].sort((a, b) => a.startMs - b.startMs);
2621
- const first = sorted[0];
2622
- if (!first)
2623
- return [];
2624
- const merged = [{ startMs: first.startMs, endMs: first.endMs }];
2625
- for (let i = 1;i < sorted.length; i++) {
2626
- const current = sorted[i];
2627
- const last = merged[merged.length - 1];
2628
- if (!current || !last)
3757
+ function allModels() {
3758
+ const seen = new Set;
3759
+ const out = [];
3760
+ for (const caps of Object.values(ENGINE_REGISTRY)) {
3761
+ if (!caps)
2629
3762
  continue;
2630
- if (current.startMs <= last.endMs) {
2631
- last.endMs = Math.max(last.endMs, current.endMs);
2632
- } else {
2633
- merged.push({ startMs: current.startMs, endMs: current.endMs });
3763
+ for (const m of caps.models) {
3764
+ const key = `${m.vendor}:${m.id}`;
3765
+ if (seen.has(key))
3766
+ continue;
3767
+ seen.add(key);
3768
+ out.push(m);
2634
3769
  }
2635
3770
  }
2636
- return merged;
2637
- }
2638
- function durationMs(intervals) {
2639
- return intervals.reduce((total, interval) => total + (interval.endMs - interval.startMs), 0);
3771
+ return out;
2640
3772
  }
2641
- function deriveSessionUsageMetrics(past) {
2642
- let latestUsage;
2643
- let latestUsageTimestampMs = null;
2644
- let lastUserTimestampMs = null;
2645
- let inputTokens = 0;
2646
- let outputTokens = 0;
2647
- const intervals = [];
2648
- for (const message of past) {
2649
- const timestampMs = parseTimestampMs(message.timestamp);
2650
- if (message.role === "user" && timestampMs !== null) {
2651
- lastUserTimestampMs = timestampMs;
2652
- continue;
2653
- }
2654
- if (message.role !== "assistant" || !message.usage)
3773
+ function modelLabelFor(modelId) {
3774
+ const resolved = modelId ?? defaultCapabilities.defaultModelId();
3775
+ for (const caps of Object.values(ENGINE_REGISTRY)) {
3776
+ if (!caps)
2655
3777
  continue;
2656
- if (timestampMs !== null && (latestUsageTimestampMs === null || timestampMs > latestUsageTimestampMs)) {
2657
- latestUsageTimestampMs = timestampMs;
2658
- latestUsage = message.usage;
2659
- } else if (latestUsage === undefined) {
2660
- latestUsage = message.usage;
2661
- }
2662
- inputTokens += message.usage.input_tokens;
2663
- outputTokens += message.usage.output_tokens;
2664
- if (timestampMs !== null && lastUserTimestampMs !== null && timestampMs > lastUserTimestampMs) {
2665
- intervals.push({ startMs: lastUserTimestampMs, endMs: timestampMs });
2666
- }
2667
- }
2668
- if (!latestUsage)
2669
- return;
2670
- const totalDurationMs = durationMs(mergeIntervals(intervals));
2671
- if (totalDurationMs <= 0)
2672
- return latestUsage;
2673
- return {
2674
- ...latestUsage,
2675
- total_speed_tokens_per_second: (inputTokens + outputTokens) / (totalDurationMs / 1000)
3778
+ const match = caps.models.find((m) => m.id === resolved);
3779
+ if (match)
3780
+ return match.label;
3781
+ }
3782
+ return resolved;
3783
+ }
3784
+ var ENGINE_REGISTRY, ENGINE_IDENTITIES, defaultCapabilities, defaultIdentity;
3785
+ var init_registry = __esm(() => {
3786
+ init_capabilities();
3787
+ init_capabilities2();
3788
+ ENGINE_REGISTRY = {
3789
+ claude: claudeCapabilities,
3790
+ codex: codexCapabilities
2676
3791
  };
2677
- }
2678
- function withTotalSpeedForTurn(usage, startedAtIso, endedAtIso) {
2679
- const startMs = startedAtIso ? parseTimestampMs(startedAtIso) : null;
2680
- const endMs = parseTimestampMs(endedAtIso);
2681
- if (startMs === null || endMs === null || endMs <= startMs)
2682
- return usage;
2683
- return {
2684
- ...usage,
2685
- total_speed_tokens_per_second: (usage.input_tokens + usage.output_tokens) / ((endMs - startMs) / 1000)
3792
+ ENGINE_IDENTITIES = {
3793
+ claude: claudeIdentity,
3794
+ codex: codexIdentity
2686
3795
  };
2687
- }
3796
+ defaultCapabilities = claudeCapabilities;
3797
+ defaultIdentity = claudeIdentity;
3798
+ });
2688
3799
 
2689
3800
  // src/env.ts
2690
- import { homedir as homedir7 } from "os";
2691
- import { join as join5 } from "path";
3801
+ import { homedir as homedir10 } from "os";
3802
+ import { join as join6 } from "path";
2692
3803
  function isDev() {
2693
3804
  return process.env.KOBE_DEV === "1";
2694
3805
  }
2695
3806
  function homeDir() {
2696
- return process.env.KOBE_HOME_DIR ?? homedir7();
3807
+ return process.env.KOBE_HOME_DIR ?? homedir10();
2697
3808
  }
2698
3809
  function kobeStateDir() {
2699
- return join5(homeDir(), ".kobe");
3810
+ return join6(homeDir(), ".kobe");
2700
3811
  }
2701
3812
  function kvStatePath() {
2702
- return join5(homeDir(), ".config", "kobe", "state.json");
3813
+ return join6(homeDir(), ".config", "kobe", "state.json");
2703
3814
  }
2704
3815
  var init_env = () => {};
2705
3816
 
@@ -2714,11 +3825,11 @@ __export(exports_repos, {
2714
3825
  getSavedRepos: () => getSavedRepos,
2715
3826
  addSavedRepo: () => addSavedRepo
2716
3827
  });
2717
- import { spawnSync as spawnSync2 } from "child_process";
2718
- import { mkdirSync, readFileSync as readFileSync2, realpathSync, renameSync, writeFileSync } from "fs";
3828
+ import { spawnSync as spawnSync3 } from "child_process";
3829
+ import { mkdirSync, readFileSync as readFileSync3, realpathSync, renameSync, writeFileSync } from "fs";
2719
3830
  import { dirname as dirname3 } from "path";
2720
3831
  function resolveRepoRoot(absPath) {
2721
- const r = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
3832
+ const r = spawnSync3("git", ["rev-parse", "--show-toplevel"], {
2722
3833
  cwd: absPath,
2723
3834
  encoding: "utf8",
2724
3835
  shell: false
@@ -2752,7 +3863,7 @@ function statePath() {
2752
3863
  }
2753
3864
  function load() {
2754
3865
  try {
2755
- const text = readFileSync2(statePath(), "utf8");
3866
+ const text = readFileSync3(statePath(), "utf8");
2756
3867
  const parsed = JSON.parse(text);
2757
3868
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2758
3869
  return parsed;
@@ -2761,11 +3872,11 @@ function load() {
2761
3872
  return {};
2762
3873
  }
2763
3874
  function save(state) {
2764
- const path4 = statePath();
2765
- mkdirSync(dirname3(path4), { recursive: true });
2766
- const tmp = `${path4}.tmp`;
3875
+ const path6 = statePath();
3876
+ mkdirSync(dirname3(path6), { recursive: true });
3877
+ const tmp = `${path6}.tmp`;
2767
3878
  writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
2768
- renameSync(tmp, path4);
3879
+ renameSync(tmp, path6);
2769
3880
  }
2770
3881
  function getSavedRepos() {
2771
3882
  const state = load();
@@ -2829,7 +3940,7 @@ function nextChatTabSeq(tabs) {
2829
3940
  max = t.seq;
2830
3941
  return max + 1;
2831
3942
  }
2832
- var toTaskId = (id) => id;
3943
+ var toTaskId = (id) => id, DEFAULT_TASK_VENDOR = "claude";
2833
3944
 
2834
3945
  // src/orchestrator/index/ulid.ts
2835
3946
  function encodeTime(now, len) {
@@ -2891,7 +4002,7 @@ var init_ulid = __esm(() => {
2891
4002
  });
2892
4003
 
2893
4004
  // src/orchestrator/metadata-suggester.ts
2894
- import { spawn as spawn3 } from "child_process";
4005
+ import { spawn as spawn4 } from "child_process";
2895
4006
 
2896
4007
  class MetadataSuggester {
2897
4008
  binaryPromise = null;
@@ -2920,7 +4031,7 @@ class MetadataSuggester {
2920
4031
  return new Promise((resolve2) => {
2921
4032
  let proc;
2922
4033
  try {
2923
- proc = spawn3(binary, ["-p", builder(trimmed)], {
4034
+ proc = spawn4(binary, ["-p", builder(trimmed)], {
2924
4035
  stdio: ["ignore", "pipe", "ignore"],
2925
4036
  env: process.env
2926
4037
  });
@@ -3069,10 +4180,10 @@ class InMemoryPendingInputBroker {
3069
4180
  }
3070
4181
 
3071
4182
  // src/orchestrator/pr/build.ts
3072
- import { spawnSync as spawnSync3 } from "child_process";
4183
+ import { spawnSync as spawnSync4 } from "child_process";
3073
4184
  function git(cwd, args) {
3074
4185
  try {
3075
- const out = spawnSync3("git", args.slice(), {
4186
+ const out = spawnSync4("git", args.slice(), {
3076
4187
  cwd,
3077
4188
  encoding: "utf8",
3078
4189
  timeout: GIT_TIMEOUT_MS
@@ -3122,7 +4233,7 @@ var init_build = () => {};
3122
4233
 
3123
4234
  // src/orchestrator/pr/instructions.ts
3124
4235
  import { promises as fs } from "fs";
3125
- import path4 from "path";
4236
+ import path6 from "path";
3126
4237
  function dirtyCountSentence(n) {
3127
4238
  if (n <= 0)
3128
4239
  return "There are no uncommitted changes.";
@@ -3150,7 +4261,7 @@ function renderPRInstructions(template, state) {
3150
4261
  async function loadPRInstructionsTemplate(worktreePath) {
3151
4262
  if (!worktreePath)
3152
4263
  return DEFAULT_PR_INSTRUCTIONS_TEMPLATE;
3153
- const file = path4.join(worktreePath, ".kobe", "pr-instructions.md");
4264
+ const file = path6.join(worktreePath, ".kobe", "pr-instructions.md");
3154
4265
  try {
3155
4266
  const text = await fs.readFile(file, "utf8");
3156
4267
  if (text.length === 0)
@@ -3201,10 +4312,11 @@ class SessionPump {
3201
4312
  }
3202
4313
  async run(taskId, tabId, handle) {
3203
4314
  const tabKey = chatRunStateKey(taskId, tabId);
4315
+ const engine = this.env.engineFor(taskId, tabId);
3204
4316
  let killedForInput = false;
3205
4317
  let terminalEvent = null;
3206
4318
  try {
3207
- for await (const ev of this.env.engine.stream(handle)) {
4319
+ for await (const ev of engine.stream(handle)) {
3208
4320
  const inputReq = detectUserInputFromEngineEvent(ev);
3209
4321
  if (inputReq) {
3210
4322
  this.env.dispatch(taskId, tabId, ev);
@@ -3218,7 +4330,7 @@ class SessionPump {
3218
4330
  });
3219
4331
  killedForInput = true;
3220
4332
  try {
3221
- await this.env.engine.stop(handle);
4333
+ await engine.stop(handle);
3222
4334
  } catch {}
3223
4335
  break;
3224
4336
  }
@@ -3231,7 +4343,7 @@ class SessionPump {
3231
4343
  } finally {
3232
4344
  if (!killedForInput) {
3233
4345
  try {
3234
- await this.env.engine.stop(handle);
4346
+ await engine.stop(handle);
3235
4347
  } catch {}
3236
4348
  }
3237
4349
  }
@@ -3291,7 +4403,8 @@ function summarizeWorktreeError(raw, repo, baseRef) {
3291
4403
  }
3292
4404
 
3293
4405
  class Orchestrator {
3294
- engine;
4406
+ engines;
4407
+ fallbackEngine;
3295
4408
  store;
3296
4409
  worktrees;
3297
4410
  metadataSuggester;
@@ -3309,7 +4422,26 @@ class Orchestrator {
3309
4422
  runStateAcc;
3310
4423
  setRunState;
3311
4424
  constructor(deps) {
3312
- this.engine = deps.engine;
4425
+ const built = {};
4426
+ let fallback;
4427
+ if (deps.engines) {
4428
+ for (const [vendor, eng] of Object.entries(deps.engines)) {
4429
+ if (!eng)
4430
+ continue;
4431
+ built[vendor] = eng;
4432
+ fallback ??= eng;
4433
+ }
4434
+ }
4435
+ if (deps.engine) {
4436
+ const v = deps.engine.capabilities.vendorId;
4437
+ built[v] ??= deps.engine;
4438
+ fallback ??= deps.engine;
4439
+ }
4440
+ if (!fallback) {
4441
+ throw new Error("Orchestrator: no usable engine found; both deps.engine and deps.engines were examined but contained no valid engines.");
4442
+ }
4443
+ this.engines = built;
4444
+ this.fallbackEngine = fallback;
3313
4445
  this.store = deps.store;
3314
4446
  this.worktrees = deps.worktrees;
3315
4447
  this.metadataSuggester = deps.metadataSuggester ?? new MetadataSuggester;
@@ -3323,13 +4455,86 @@ class Orchestrator {
3323
4455
  this.runStateAcc = runState;
3324
4456
  this.setRunState = (next) => setRunState(() => next);
3325
4457
  this.sessionPump = new SessionPump({
3326
- engine: this.engine,
4458
+ engineFor: (taskId, tabId) => this.engineForTaskTabId(taskId, tabId),
3327
4459
  broker: this.pendingInputBroker,
3328
4460
  dispatch: (taskId, tabId, ev) => this.dispatchEvent(taskId, tabId, ev),
3329
4461
  nextRequestId: () => `req-${++this.requestIdCounter}`,
3330
4462
  onPendingInputChange: () => this.bumpRunState()
3331
4463
  });
3332
4464
  }
4465
+ engineForVendor(vendor) {
4466
+ const v = vendor ?? DEFAULT_TASK_VENDOR;
4467
+ return this.engines[v] ?? this.fallbackEngine;
4468
+ }
4469
+ engineForTask(task) {
4470
+ return this.engineForVendor(task.vendor);
4471
+ }
4472
+ vendorForTab(task, tab) {
4473
+ return tab.vendor ?? task.vendor ?? "claude";
4474
+ }
4475
+ modelForTab(task, tab, engine) {
4476
+ return tab.model ?? task.model ?? engine.capabilities.defaultModelId();
4477
+ }
4478
+ engineForTab(task, tab) {
4479
+ return this.engineForVendor(this.vendorForTab(task, tab));
4480
+ }
4481
+ async engineForTabRun(task, tab) {
4482
+ if (!tab.sessionId || tab.vendor)
4483
+ return this.engineForTab(task, tab);
4484
+ const resolved = await this.findEngineWithHistory(tab.sessionId, this.vendorForTab(task, tab));
4485
+ if (resolved.vendor && resolved.vendor !== tab.vendor) {
4486
+ await this.updateTab(task.id, tab.id, { vendor: resolved.vendor });
4487
+ }
4488
+ return resolved.engine;
4489
+ }
4490
+ async findEngineWithHistory(sessionId, preferredVendor) {
4491
+ const candidates = [];
4492
+ if (preferredVendor)
4493
+ candidates.push([preferredVendor, this.engineForVendor(preferredVendor)]);
4494
+ for (const [vendor, engine] of Object.entries(this.engines)) {
4495
+ if (!engine || vendor === preferredVendor)
4496
+ continue;
4497
+ candidates.push([vendor, engine]);
4498
+ }
4499
+ if (candidates.length === 0)
4500
+ candidates.push([undefined, this.fallbackEngine]);
4501
+ let fallback = candidates[0] ?? [undefined, this.fallbackEngine];
4502
+ let fallbackHistory;
4503
+ for (const [vendor, engine] of candidates) {
4504
+ try {
4505
+ const history = await engine.readHistory(sessionId);
4506
+ if (!fallbackHistory) {
4507
+ fallback = [vendor, engine];
4508
+ fallbackHistory = history;
4509
+ }
4510
+ if (history.messages.length > 0 || history.usageMetrics)
4511
+ return { engine, vendor, history };
4512
+ } catch {}
4513
+ }
4514
+ return { engine: fallback[1], vendor: fallback[0], history: fallbackHistory };
4515
+ }
4516
+ engineForTaskId(taskId) {
4517
+ const task = this.store.get(taskId);
4518
+ return task ? this.engineForTask(task) : this.fallbackEngine;
4519
+ }
4520
+ engineForTaskTabId(taskId, tabId) {
4521
+ const task = this.store.get(taskId);
4522
+ if (!task)
4523
+ return this.fallbackEngine;
4524
+ const tab = task.tabs.find((t) => t.id === tabId);
4525
+ return tab ? this.engineForTab(task, tab) : this.engineForTask(task);
4526
+ }
4527
+ engineForSessionId(sessionId) {
4528
+ for (const task of this.store.list()) {
4529
+ for (const tab of task.tabs) {
4530
+ if (tab.sessionId === sessionId)
4531
+ return this.engineForTab(task, tab);
4532
+ }
4533
+ if (task.sessionId === sessionId)
4534
+ return this.engineForTask(task);
4535
+ }
4536
+ return this.fallbackEngine;
4537
+ }
3333
4538
  bumpRunState() {
3334
4539
  const next = new Map;
3335
4540
  for (const tabKey2 of this.pendingInputBroker.awaitingTabKeys()) {
@@ -3549,10 +4754,11 @@ class Orchestrator {
3549
4754
  if (prompt && prompt.trim().length > 0) {
3550
4755
  this.dispatchEvent(task.id, targetTab.id, { type: "user.inject", text: prompt });
3551
4756
  }
3552
- const modelToUse = task.model ?? resolveDefaultModelId();
4757
+ const engine = targetTab.sessionId ? await this.engineForTabRun(task, targetTab) : this.engineForTab(task, targetTab);
4758
+ const modelToUse = this.modelForTab(task, targetTab, engine);
3553
4759
  let handle;
3554
4760
  if (targetTab.sessionId) {
3555
- handle = await this.engine.resume(targetTab.sessionId, promptToSend, {
4761
+ handle = await engine.resume(targetTab.sessionId, promptToSend, {
3556
4762
  cwd: task.worktreePath,
3557
4763
  env: { KOBE_RESUME_CWD: task.worktreePath },
3558
4764
  permissionMode: task.permissionMode,
@@ -3565,7 +4771,7 @@ class Orchestrator {
3565
4771
  });
3566
4772
  this.firstSpawnLatches.set(key, latch);
3567
4773
  try {
3568
- handle = await this.engine.spawn(task.worktreePath, promptToSend, {
4774
+ handle = await engine.spawn(task.worktreePath, promptToSend, {
3569
4775
  permissionMode: task.permissionMode,
3570
4776
  model: modelToUse
3571
4777
  });
@@ -3640,7 +4846,7 @@ class Orchestrator {
3640
4846
  text: "(turn interrupted \u2014 sending new prompt)"
3641
4847
  });
3642
4848
  try {
3643
- await this.engine.stop(handle);
4849
+ await this.engineForTask(task).stop(handle);
3644
4850
  } finally {
3645
4851
  this.handles.delete(key);
3646
4852
  this.bumpRunState();
@@ -3682,11 +4888,13 @@ class Orchestrator {
3682
4888
  return;
3683
4889
  await this.store.update(task.id, { permissionMode: mode });
3684
4890
  }
3685
- async setModel(id, model) {
4891
+ async setModel(id, model, tabId) {
3686
4892
  const task = this.requireTask(id);
3687
- if (task.model === model)
4893
+ const tab = this.resolveTab(task, tabId);
4894
+ const vendor = model ? capabilitiesForModelId(model).vendorId : this.vendorForTab(task, tab);
4895
+ if (tab.model === model && this.vendorForTab(task, tab) === vendor)
3688
4896
  return;
3689
- await this.store.update(task.id, { model });
4897
+ await this.updateTab(task.id, tab.id, { model, vendor });
3690
4898
  }
3691
4899
  async setTitle(id, title) {
3692
4900
  const task = this.requireTask(id);
@@ -3750,7 +4958,7 @@ class Orchestrator {
3750
4958
  }
3751
4959
  if (task.sessionId) {
3752
4960
  try {
3753
- await this.engine.deleteHistory(task.sessionId);
4961
+ await this.engineForTask(task).deleteHistory(task.sessionId);
3754
4962
  } catch (err) {
3755
4963
  console.error(`[kobe orchestrator] deleteTask: deleteHistory failed for ${task.id}:`, err);
3756
4964
  }
@@ -3759,15 +4967,15 @@ class Orchestrator {
3759
4967
  await this.store.remove(task.id);
3760
4968
  }
3761
4969
  async readHistory(sessionId) {
3762
- try {
3763
- return await this.engine.readHistory(sessionId);
3764
- } catch {
3765
- return [];
3766
- }
4970
+ return (await this.readHistoryWithMetrics(sessionId)).messages;
3767
4971
  }
3768
4972
  async readHistoryWithMetrics(sessionId) {
3769
- const messages = await this.readHistory(sessionId);
3770
- const usageMetrics = deriveSessionUsageMetrics(messages);
4973
+ const preferred = this.engineForSessionId(sessionId);
4974
+ const { history } = await this.findEngineWithHistory(sessionId, preferred.capabilities.vendorId);
4975
+ if (!history)
4976
+ return { messages: [] };
4977
+ const messages = [...history.messages];
4978
+ const usageMetrics = history.usageMetrics;
3771
4979
  return {
3772
4980
  messages,
3773
4981
  ...usageMetrics ? { usageMetrics } : {}
@@ -3777,14 +4985,16 @@ class Orchestrator {
3777
4985
  const task = this.requireTask(id);
3778
4986
  if (!task.worktreePath)
3779
4987
  return [];
4988
+ const tab = this.resolveTab(task);
3780
4989
  try {
3781
- return await this.engine.listSessions(task.worktreePath);
4990
+ return await this.engineForTab(task, tab).listSessions(task.worktreePath);
3782
4991
  } catch {
3783
4992
  return [];
3784
4993
  }
3785
4994
  }
3786
4995
  async openSessionInTab(id, sessionId, opts = {}) {
3787
4996
  const task = this.requireTask(id);
4997
+ const active = this.resolveTab(task);
3788
4998
  const existing = task.tabs.find((t) => t.sessionId === sessionId);
3789
4999
  if (existing) {
3790
5000
  await this.setActiveTab(task.id, existing.id);
@@ -3795,6 +5005,8 @@ class Orchestrator {
3795
5005
  sessionId,
3796
5006
  seq: nextChatTabSeq(task.tabs),
3797
5007
  createdAt: new Date().toISOString(),
5008
+ model: active.model ?? task.model,
5009
+ vendor: this.vendorForTab(task, active),
3798
5010
  ...opts.title ? { title: opts.title } : {}
3799
5011
  };
3800
5012
  await this.store.update(task.id, { tabs: [...task.tabs, tab], activeTabId: tab.id });
@@ -3822,11 +5034,14 @@ class Orchestrator {
3822
5034
  }
3823
5035
  async createTab(id, opts = {}) {
3824
5036
  const task = this.requireTask(id);
5037
+ const active = this.resolveTab(task);
3825
5038
  const tab = {
3826
5039
  id: ulid(),
3827
5040
  sessionId: null,
3828
5041
  seq: nextChatTabSeq(task.tabs),
3829
5042
  createdAt: new Date().toISOString(),
5043
+ model: active.model ?? task.model,
5044
+ vendor: this.vendorForTab(task, active),
3830
5045
  ...opts.title ? { title: opts.title } : {}
3831
5046
  };
3832
5047
  const tabs = [...task.tabs, tab];
@@ -3906,7 +5121,7 @@ class Orchestrator {
3906
5121
  if (!handle)
3907
5122
  return;
3908
5123
  try {
3909
- await this.engine.stop(handle);
5124
+ await this.engineForTaskId(taskId).stop(handle);
3910
5125
  } finally {
3911
5126
  this.handles.delete(key);
3912
5127
  this.bumpRunState();
@@ -3915,12 +5130,13 @@ class Orchestrator {
3915
5130
  async stopAllTabsForTask(taskId) {
3916
5131
  const prefix = `${taskId}:`;
3917
5132
  const keys = Array.from(this.handles.keys()).filter((k) => k.startsWith(prefix));
5133
+ const engine = this.engineForTaskId(taskId);
3918
5134
  for (const key of keys) {
3919
5135
  const handle = this.handles.get(key);
3920
5136
  if (!handle)
3921
5137
  continue;
3922
5138
  try {
3923
- await this.engine.stop(handle);
5139
+ await engine.stop(handle);
3924
5140
  } catch {}
3925
5141
  this.handles.delete(key);
3926
5142
  }
@@ -4046,7 +5262,7 @@ function deriveTitleFromPrompt(prompt) {
4046
5262
  var CONCURRENCY_CAP = 20, PLACEHOLDER_TASK_TITLE = "(new task)", IllegalTransitionError, ConcurrencyCapError, PRPreconditionError, TaskNotFoundError, CannotDeleteMainTaskError, TITLE_CHAR_CAP = 40;
4047
5263
  var init_core = __esm(() => {
4048
5264
  init_dev();
4049
- init_claude_settings();
5265
+ init_registry();
4050
5266
  init_repos();
4051
5267
  init_ulid();
4052
5268
  init_metadata_suggester();
@@ -4091,9 +5307,9 @@ var init_core = __esm(() => {
4091
5307
  });
4092
5308
 
4093
5309
  // src/orchestrator/index/store.ts
4094
- import { mkdir as mkdir4, open, readFile as readFile3, rename, unlink as unlink3 } from "fs/promises";
4095
- import { homedir as homedir8 } from "os";
4096
- import { dirname as dirname4, join as join6 } from "path";
5310
+ import { mkdir as mkdir4, open as open2, readFile as readFile4, rename, unlink as unlink4 } from "fs/promises";
5311
+ import { homedir as homedir11 } from "os";
5312
+ import { dirname as dirname4, join as join7 } from "path";
4097
5313
 
4098
5314
  class TaskIndexStore {
4099
5315
  homeDir;
@@ -4105,9 +5321,9 @@ class TaskIndexStore {
4105
5321
  listeners = new Set;
4106
5322
  saveChain = Promise.resolve();
4107
5323
  constructor(options = {}) {
4108
- this.homeDir = options.homeDir ?? homedir8();
4109
- this.kobeDir = join6(this.homeDir, ".kobe");
4110
- this.path = join6(this.kobeDir, "tasks.json");
5324
+ this.homeDir = options.homeDir ?? homedir11();
5325
+ this.kobeDir = join7(this.homeDir, ".kobe");
5326
+ this.path = join7(this.kobeDir, "tasks.json");
4111
5327
  this.tmpPath = `${this.path}.tmp`;
4112
5328
  }
4113
5329
  subscribe(listener) {
@@ -4132,7 +5348,7 @@ class TaskIndexStore {
4132
5348
  async load() {
4133
5349
  let raw;
4134
5350
  try {
4135
- raw = await readFile3(this.path, "utf8");
5351
+ raw = await readFile4(this.path, "utf8");
4136
5352
  } catch (err) {
4137
5353
  const code = err.code;
4138
5354
  if (code === "ENOENT") {
@@ -4169,7 +5385,7 @@ class TaskIndexStore {
4169
5385
  const payload = this.snapshot();
4170
5386
  const json = `${JSON.stringify(payload, null, 2)}
4171
5387
  `;
4172
- const handle = await open(this.tmpPath, "w", 420);
5388
+ const handle = await open2(this.tmpPath, "w", 420);
4173
5389
  try {
4174
5390
  await handle.writeFile(json, "utf8");
4175
5391
  await handle.sync();
@@ -4197,7 +5413,16 @@ class TaskIndexStore {
4197
5413
  activeTabId = activeIn && tabsIn.some((t) => t.id === activeIn) ? activeIn : tabsIn[0]?.id ?? "";
4198
5414
  } else {
4199
5415
  const tabId = ulid();
4200
- tabs = [{ id: tabId, sessionId: sessionId ?? null, seq: 1, createdAt: now }];
5416
+ tabs = [
5417
+ {
5418
+ id: tabId,
5419
+ sessionId: sessionId ?? null,
5420
+ seq: 1,
5421
+ createdAt: now,
5422
+ ...rest.model ? { model: rest.model } : {},
5423
+ vendor: rest.vendor ?? DEFAULT_TASK_VENDOR
5424
+ }
5425
+ ];
4201
5426
  activeTabId = tabId;
4202
5427
  }
4203
5428
  const firstSession = tabs[0]?.sessionId ?? null;
@@ -4264,13 +5489,13 @@ class TaskIndexStore {
4264
5489
  }
4265
5490
  async _unlinkForTests() {
4266
5491
  try {
4267
- await unlink3(this.path);
5492
+ await unlink4(this.path);
4268
5493
  } catch (err) {
4269
5494
  if (err.code !== "ENOENT")
4270
5495
  throw err;
4271
5496
  }
4272
5497
  try {
4273
- await unlink3(this.tmpPath);
5498
+ await unlink4(this.tmpPath);
4274
5499
  } catch (err) {
4275
5500
  if (err.code !== "ENOENT")
4276
5501
  throw err;
@@ -4360,7 +5585,9 @@ function coerceTask(value) {
4360
5585
  sessionId: tt.sessionId ?? null,
4361
5586
  seq,
4362
5587
  createdAt: tt.createdAt,
4363
- ...typeof tt.title === "string" ? { title: tt.title } : {}
5588
+ ...typeof tt.title === "string" ? { title: tt.title } : {},
5589
+ ...typeof tt.model === "string" ? { model: tt.model } : {},
5590
+ ...isVendorId(tt.vendor) ? { vendor: tt.vendor } : {}
4364
5591
  };
4365
5592
  tabs.push(tab);
4366
5593
  }
@@ -4393,6 +5620,7 @@ function coerceTask(value) {
4393
5620
  kind: v.kind === "main" ? "main" : "task",
4394
5621
  permissionMode: isPermissionMode(v.permissionMode) ? v.permissionMode : undefined,
4395
5622
  model: typeof v.model === "string" ? v.model : undefined,
5623
+ vendor: resolveTaskVendor(v.vendor, typeof v.model === "string" ? v.model : undefined),
4396
5624
  createdAt: v.createdAt,
4397
5625
  updatedAt: v.updatedAt
4398
5626
  };
@@ -4400,20 +5628,33 @@ function coerceTask(value) {
4400
5628
  function isPermissionMode(v) {
4401
5629
  return v === "default" || v === "plan";
4402
5630
  }
5631
+ function isVendorId(v) {
5632
+ return typeof v === "string" && v in ENGINE_REGISTRY;
5633
+ }
5634
+ function resolveTaskVendor(rawVendor, modelId) {
5635
+ const stored = isVendorId(rawVendor) ? rawVendor : DEFAULT_TASK_VENDOR;
5636
+ if (!modelId)
5637
+ return stored;
5638
+ const matched = Object.values(ENGINE_REGISTRY).some((caps) => caps?.models.some((m) => m.id === modelId));
5639
+ if (!matched)
5640
+ return stored;
5641
+ return capabilitiesForModelId(modelId).vendorId;
5642
+ }
4403
5643
  function isTaskStatus(s) {
4404
5644
  return s === "backlog" || s === "in_progress" || s === "in_review" || s === "done" || s === "canceled" || s === "error";
4405
5645
  }
4406
5646
  var init_store = __esm(() => {
5647
+ init_registry();
4407
5648
  init_ulid();
4408
5649
  });
4409
5650
 
4410
5651
  // src/orchestrator/worktree/git.ts
4411
- import { spawnSync as spawnSync4 } from "child_process";
5652
+ import { spawnSync as spawnSync5 } from "child_process";
4412
5653
  function git2(args, opts) {
4413
5654
  if (!opts.cwd) {
4414
5655
  throw new Error("git(): cwd is required; refusing to inherit from process.cwd()");
4415
5656
  }
4416
- const proc = spawnSync4("git", [...args], {
5657
+ const proc = spawnSync5("git", [...args], {
4417
5658
  cwd: opts.cwd,
4418
5659
  env: opts.env ? { ...process.env, ...opts.env } : process.env,
4419
5660
  encoding: "utf8",
@@ -4451,32 +5692,32 @@ var init_git = __esm(() => {
4451
5692
 
4452
5693
  // src/orchestrator/worktree/paths.ts
4453
5694
  import fs2 from "fs";
4454
- import path5 from "path";
5695
+ import path7 from "path";
4455
5696
  function worktreeRootFor(repo) {
4456
- if (!path5.isAbsolute(repo)) {
5697
+ if (!path7.isAbsolute(repo)) {
4457
5698
  throw new Error(`worktreeRootFor: repo must be an absolute path, got: ${repo}`);
4458
5699
  }
4459
- return path5.join(repo, KOBE_WORKTREE_ROOT_SUBPATH);
5700
+ return path7.join(repo, KOBE_WORKTREE_ROOT_SUBPATH);
4460
5701
  }
4461
5702
  function worktreePathFor(repo, taskId) {
4462
5703
  if (!taskId || /[/\\\0]/.test(taskId)) {
4463
5704
  throw new Error(`worktreePathFor: invalid taskId: ${JSON.stringify(taskId)}`);
4464
5705
  }
4465
- return path5.join(worktreeRootFor(repo), taskId);
5706
+ return path7.join(worktreeRootFor(repo), taskId);
4466
5707
  }
4467
5708
  function isKobeManagedPath(repo, candidate) {
4468
- if (!path5.isAbsolute(repo) || !path5.isAbsolute(candidate))
5709
+ if (!path7.isAbsolute(repo) || !path7.isAbsolute(candidate))
4469
5710
  return false;
4470
5711
  const root = canonicalize(worktreeRootFor(repo));
4471
5712
  const target = canonicalize(candidate);
4472
- const rel = path5.relative(root, target);
4473
- return rel !== "" && !rel.startsWith("..") && !path5.isAbsolute(rel);
5713
+ const rel = path7.relative(root, target);
5714
+ return rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
4474
5715
  }
4475
5716
  function canonicalize(p) {
4476
5717
  try {
4477
5718
  return fs2.realpathSync(p);
4478
5719
  } catch {
4479
- return path5.resolve(p);
5720
+ return path7.resolve(p);
4480
5721
  }
4481
5722
  }
4482
5723
  var KOBE_WORKTREE_ROOT_SUBPATH = ".claude/worktrees";
@@ -4484,7 +5725,7 @@ var init_paths2 = () => {};
4484
5725
 
4485
5726
  // src/orchestrator/worktree/manager.ts
4486
5727
  import fs3 from "fs";
4487
- import path6 from "path";
5728
+ import path8 from "path";
4488
5729
 
4489
5730
  class GitWorktreeManager {
4490
5731
  async create(repo, branch, worktreePath, baseRef) {
@@ -4502,7 +5743,7 @@ class GitWorktreeManager {
4502
5743
  }
4503
5744
  throw new Error(`create(): ${worktreePath} exists but is not a registered git worktree`);
4504
5745
  }
4505
- fs3.mkdirSync(path6.dirname(worktreePath), { recursive: true });
5746
+ fs3.mkdirSync(path8.dirname(worktreePath), { recursive: true });
4506
5747
  const branchExists = this.branchExists(repo, branch);
4507
5748
  const args = branchExists ? ["worktree", "add", worktreePath, branch] : baseRef ? ["worktree", "add", "-b", branch, worktreePath, baseRef] : ["worktree", "add", "-b", branch, worktreePath];
4508
5749
  git2(args, { cwd: repo });
@@ -4557,8 +5798,8 @@ class GitWorktreeManager {
4557
5798
  if (!entry.branch || entry.detached)
4558
5799
  continue;
4559
5800
  const canonEntry = canonicalize2(entry.path);
4560
- const rel = path6.relative(canonRoot, canonEntry);
4561
- const callerPath = path6.join(callerRoot, rel);
5801
+ const rel = path8.relative(canonRoot, canonEntry);
5802
+ const callerPath = path8.join(callerRoot, rel);
4562
5803
  const dirty = await this.isDirty(entry.path);
4563
5804
  infos.push({
4564
5805
  path: callerPath,
@@ -4619,9 +5860,9 @@ class GitWorktreeManager {
4619
5860
  const gitDir = out.stdout.trim();
4620
5861
  if (!gitDir)
4621
5862
  return null;
4622
- const absolute = path6.isAbsolute(gitDir) ? gitDir : path6.resolve(worktreePath, gitDir);
4623
- const base = path6.basename(absolute);
4624
- return base === ".git" ? path6.dirname(absolute) : absolute;
5863
+ const absolute = path8.isAbsolute(gitDir) ? gitDir : path8.resolve(worktreePath, gitDir);
5864
+ const base = path8.basename(absolute);
5865
+ return base === ".git" ? path8.dirname(absolute) : absolute;
4625
5866
  } catch (err) {
4626
5867
  if (err instanceof GitCommandError)
4627
5868
  return null;
@@ -4661,7 +5902,7 @@ function parsePorcelain(out) {
4661
5902
  return records;
4662
5903
  }
4663
5904
  function requireAbsolute(name, value) {
4664
- if (!value || !path6.isAbsolute(value)) {
5905
+ if (!value || !path8.isAbsolute(value)) {
4665
5906
  throw new Error(`${name} must be an absolute path, got: ${JSON.stringify(value)}`);
4666
5907
  }
4667
5908
  }
@@ -4669,7 +5910,7 @@ function canonicalize2(p) {
4669
5910
  try {
4670
5911
  return fs3.realpathSync(p);
4671
5912
  } catch {
4672
- return path6.resolve(p);
5913
+ return path8.resolve(p);
4673
5914
  }
4674
5915
  }
4675
5916
  var init_manager = __esm(() => {
@@ -4680,22 +5921,26 @@ var init_manager = __esm(() => {
4680
5921
  // src/bin/kobed.ts
4681
5922
  init_daemon_process();
4682
5923
  init_client();
4683
- import { unlink as unlink5 } from "fs/promises";
5924
+ import { unlink as unlink6 } from "fs/promises";
4684
5925
 
4685
5926
  // src/core/index.ts
4686
5927
  init_claude_code_local();
5928
+ init_codex_local();
4687
5929
  init_bridge();
4688
5930
  init_core();
4689
5931
  init_store();
4690
5932
  init_manager();
4691
- import { homedir as homedir9 } from "os";
5933
+ import { homedir as homedir12 } from "os";
4692
5934
  async function createKobeCore(options = {}) {
4693
- const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir9();
5935
+ const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir12();
4694
5936
  const store = new TaskIndexStore({ homeDir: homeDir2 });
4695
5937
  await store.load();
4696
5938
  const worktrees = new GitWorktreeManager;
4697
- const engine = options.engine ?? new ClaudeCodeLocal;
4698
- const orchestrator = new Orchestrator({ engine, store, worktrees });
5939
+ const engines = options.engines ?? (options.engine ? { [options.engine.capabilities.vendorId]: options.engine } : {
5940
+ claude: new ClaudeCodeLocal,
5941
+ codex: new CodexLocal
5942
+ });
5943
+ const orchestrator = new Orchestrator({ engines, store, worktrees });
4699
5944
  const bridge = options.startMcpBridge === false ? null : await startBridge(orchestrator, { homeDir: homeDir2 });
4700
5945
  return {
4701
5946
  homeDir: homeDir2,
@@ -4716,16 +5961,16 @@ init_paths();
4716
5961
  // src/daemon/server.ts
4717
5962
  init_repos();
4718
5963
  init_paths();
4719
- import { mkdir as mkdir5, readFile as readFile5, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
5964
+ import { mkdir as mkdir5, readFile as readFile6, unlink as unlink5, writeFile as writeFile4 } from "fs/promises";
4720
5965
  import { createServer as createServer2 } from "net";
4721
5966
  import { dirname as dirname5 } from "path";
4722
5967
 
4723
5968
  // src/engine/claude-code-local/plan-usage.ts
4724
5969
  import { execFile } from "child_process";
4725
- import { createHash } from "crypto";
4726
- import { readFile as readFile4 } from "fs/promises";
4727
- import { homedir as homedir10, userInfo } from "os";
4728
- import { join as join7 } from "path";
5970
+ import { createHash as createHash2 } from "crypto";
5971
+ import { readFile as readFile5 } from "fs/promises";
5972
+ import { homedir as homedir13, userInfo } from "os";
5973
+ import { join as join8 } from "path";
4729
5974
  import { promisify } from "util";
4730
5975
  var execFileAsync = promisify(execFile);
4731
5976
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
@@ -4736,7 +5981,7 @@ function keychainServiceName() {
4736
5981
  const configDir = process.env.CLAUDE_CONFIG_DIR;
4737
5982
  if (!configDir)
4738
5983
  return `${KEYCHAIN_BASE}${KEYCHAIN_SUFFIX}`;
4739
- const hash = createHash("sha256").update(configDir).digest("hex").slice(0, 8);
5984
+ const hash = createHash2("sha256").update(configDir).digest("hex").slice(0, 8);
4740
5985
  return `${KEYCHAIN_BASE}${KEYCHAIN_SUFFIX}-${hash}`;
4741
5986
  }
4742
5987
  function keychainAccount() {
@@ -4760,10 +6005,10 @@ async function readKeychainToken() {
4760
6005
  }
4761
6006
  }
4762
6007
  async function readPlainTextToken() {
4763
- const configDir = process.env.CLAUDE_CONFIG_DIR ?? join7(homedir10(), ".claude");
4764
- const path7 = join7(configDir, ".credentials.json");
6008
+ const configDir = process.env.CLAUDE_CONFIG_DIR ?? join8(homedir13(), ".claude");
6009
+ const path9 = join8(configDir, ".credentials.json");
4765
6010
  try {
4766
- const raw = await readFile4(path7, "utf8");
6011
+ const raw = await readFile5(path9, "utf8");
4767
6012
  return parseStoredOAuth(raw);
4768
6013
  } catch {
4769
6014
  return null;
@@ -4876,7 +6121,7 @@ function createPlanUsagePoller(options) {
4876
6121
  }
4877
6122
  // src/daemon/rc-bridge.ts
4878
6123
  init_binary();
4879
- import { spawn as spawn4 } from "child_process";
6124
+ import { spawn as spawn5 } from "child_process";
4880
6125
  var ENV_ID_RE = /Environment ID:\s*(env_[A-Za-z0-9]+)/;
4881
6126
  var DEEPLINK_RE = /https:\/\/claude\.ai\/code\?environment=([A-Za-z0-9_]+)/;
4882
6127
  var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
@@ -4884,7 +6129,7 @@ function createRcBridge(options = {}) {
4884
6129
  const stopGraceMs = options.stopGraceMs ?? 5000;
4885
6130
  const readyTimeoutMs = options.readyTimeoutMs ?? 30000;
4886
6131
  const binaryPathResolver = options.binaryPathResolver ?? findClaudeBinary;
4887
- const spawner = options.spawner ?? ((cmd, args, cwd) => spawn4(cmd, [...args], {
6132
+ const spawner = options.spawner ?? ((cmd, args, cwd) => spawn5(cmd, [...args], {
4888
6133
  cwd,
4889
6134
  stdio: ["ignore", "pipe", "pipe"],
4890
6135
  env: { ...process.env }
@@ -5058,7 +6303,7 @@ async function startDaemonServer(orch, options = {}) {
5058
6303
  let nextClientId = 1;
5059
6304
  await mkdir5(dirname5(socketPath), { recursive: true });
5060
6305
  await mkdir5(dirname5(pidPath), { recursive: true });
5061
- await unlink4(socketPath).catch(() => {});
6306
+ await unlink5(socketPath).catch(() => {});
5062
6307
  const server = createServer2((socket) => {
5063
6308
  const client = {
5064
6309
  id: nextClientId++,
@@ -5103,8 +6348,8 @@ async function startDaemonServer(orch, options = {}) {
5103
6348
  client.socket.destroy();
5104
6349
  }
5105
6350
  await new Promise((resolve2) => server.close(() => resolve2()));
5106
- await unlink4(socketPath).catch(() => {});
5107
- await unlink4(pidPath).catch(() => {});
6351
+ await unlink5(socketPath).catch(() => {});
6352
+ await unlink5(pidPath).catch(() => {});
5108
6353
  }
5109
6354
  };
5110
6355
  planUsagePoller.start();
@@ -5218,7 +6463,7 @@ async function startDaemonServer(orch, options = {}) {
5218
6463
  }
5219
6464
  case "task.model": {
5220
6465
  const taskId = requireString2(payload, "taskId");
5221
- await orch.setModel(taskId, optionalString2(payload, "model"));
6466
+ await orch.setModel(taskId, optionalString2(payload, "model"), optionalString2(payload, "tabId"));
5222
6467
  broadcastTaskUpdated(orch, clients, taskId);
5223
6468
  return {};
5224
6469
  }
@@ -5417,7 +6662,7 @@ async function startDaemonServer(orch, options = {}) {
5417
6662
  }
5418
6663
  async function readPidFile(pidPath) {
5419
6664
  try {
5420
- const raw = await readFile5(pidPath, "utf8");
6665
+ const raw = await readFile6(pidPath, "utf8");
5421
6666
  const pid = Number(raw.trim());
5422
6667
  return Number.isFinite(pid) ? pid : null;
5423
6668
  } catch {
@@ -5455,8 +6700,7 @@ async function readTaskHistory(orch, taskId, requestedSessionId, limit, before)
5455
6700
  const sessionId = requestedSessionId ?? task?.tabs.find((t) => t.id === task.activeTabId)?.sessionId ?? task?.sessionId;
5456
6701
  if (!sessionId)
5457
6702
  return { messages: [], nextBefore: null, hasMore: false };
5458
- const messages = await orch.readHistory(sessionId);
5459
- const usageMetrics = deriveSessionUsageMetrics(messages);
6703
+ const { messages, usageMetrics } = await orch.readHistoryWithMetrics(sessionId);
5460
6704
  const beforeIdx = before ? messages.findIndex((m) => `${m.timestamp}:${m.sessionId}` === before) : -1;
5461
6705
  const end = beforeIdx >= 0 ? beforeIdx : messages.length;
5462
6706
  const start = Math.max(0, end - limit);
@@ -5602,7 +6846,7 @@ async function main() {
5602
6846
  await new Promise((resolve2) => setTimeout(resolve2, 100));
5603
6847
  } catch {}
5604
6848
  }
5605
- await unlink5(socketPath).catch(() => {});
6849
+ await unlink6(socketPath).catch(() => {});
5606
6850
  const next = await connectOrStartDaemon();
5607
6851
  next.close();
5608
6852
  console.log(`kobed: restarted, listening on ${socketPath}`);