@sma1lboy/kobe 0.5.13 → 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.
package/dist/bin/kobed.js CHANGED
@@ -1,6 +1,36 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
3
5
  var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
13
+ var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
21
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
22
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
+ for (let key of __getOwnPropNames(mod))
24
+ if (!__hasOwnProp.call(to, key))
25
+ __defProp(to, key, {
26
+ get: __accessProp.bind(mod, key),
27
+ enumerable: true
28
+ });
29
+ if (canCache)
30
+ cache.set(mod, to);
31
+ return to;
32
+ };
33
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
4
34
  var __returnValue = (v) => v;
5
35
  function __exportSetter(name, newValue) {
6
36
  this[name] = __returnValue.bind(null, newValue);
@@ -18,17 +48,38 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
48
  var __require = import.meta.require;
19
49
 
20
50
  // src/daemon/paths.ts
51
+ import { createHash } from "crypto";
21
52
  import { homedir, tmpdir } from "os";
22
53
  import { join } from "path";
23
- 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
+ }
24
72
  const runtimeDir = process.env.XDG_RUNTIME_DIR;
25
- if (runtimeDir && runtimeDir.length > 0)
26
- return join(runtimeDir, "kobe.sock");
27
- 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");
28
78
  }
29
79
  function defaultDaemonPidPath(homeDir = process.env.KOBE_HOME_DIR ?? homedir()) {
30
80
  return join(homeDir, ".kobe", "daemon.pid");
31
81
  }
82
+ var SOCKET_PATH_SAFETY_LIMIT = 100;
32
83
  var init_paths = () => {};
33
84
 
34
85
  // src/daemon/protocol.ts
@@ -48,6 +99,7 @@ function serializeTask(task) {
48
99
  pinned: task.pinned ?? false,
49
100
  permissionMode: task.permissionMode,
50
101
  model: task.model,
102
+ vendor: task.vendor,
51
103
  createdAt: task.createdAt,
52
104
  updatedAt: task.updatedAt
53
105
  };
@@ -315,6 +367,86 @@ var init_daemon_process = __esm(() => {
315
367
  init_client();
316
368
  });
317
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
+
318
450
  // src/engine/claude-code-local/binary.ts
319
451
  import { spawnSync } from "child_process";
320
452
  import { existsSync as existsSync2, statSync } from "fs";
@@ -419,10 +551,152 @@ var init_binary = __esm(() => {
419
551
  };
420
552
  });
421
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
+
422
696
  // src/engine/claude-code-local/history.ts
423
697
  import { randomUUID } from "crypto";
424
698
  import { appendFile, mkdir, readFile, readdir, unlink, writeFile } from "fs/promises";
425
- import { homedir as homedir3 } from "os";
699
+ import { homedir as homedir4 } from "os";
426
700
  import path2 from "path";
427
701
  function encodeCwd(cwd) {
428
702
  return cwd.replace(/[/.]/g, "-");
@@ -493,11 +767,11 @@ function extractMessage(record, fallbackSessionId) {
493
767
  return null;
494
768
  if (!("content" in inner))
495
769
  return null;
496
- const content = inner.content;
770
+ const blocks = normalizeClaudeContent(inner.content);
497
771
  const ts = typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString();
498
772
  const sid = typeof record.sessionId === "string" ? record.sessionId : fallbackSessionId;
499
773
  const usage = extractUsage(inner.usage);
500
- 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 };
501
775
  }
502
776
  function extractUsage(v) {
503
777
  if (!isObject(v))
@@ -592,7 +866,7 @@ var defaultDeps2;
592
866
  var init_history = __esm(() => {
593
867
  defaultDeps2 = {
594
868
  projectsDir() {
595
- return path2.join(homedir3(), ".claude", "projects");
869
+ return path2.join(homedir4(), ".claude", "projects");
596
870
  },
597
871
  async readdir(p) {
598
872
  try {
@@ -676,7 +950,7 @@ function delay(ms) {
676
950
 
677
951
  // src/engine/claude-code-local/sessions.ts
678
952
  import { readFile as readFile2, readdir as readdir2, stat } from "fs/promises";
679
- import { homedir as homedir4 } from "os";
953
+ import { homedir as homedir5 } from "os";
680
954
  import path3 from "path";
681
955
  async function listSessionsForCwd(cwd, deps = defaultDeps3) {
682
956
  const projectDir = path3.join(deps.projectsDir(), encodeCwd(cwd));
@@ -728,17 +1002,11 @@ function extractFirstUserMessage(lines) {
728
1002
  return null;
729
1003
  }
730
1004
  function stringifyContent(content) {
731
- if (typeof content === "string")
732
- return content.trim();
733
- if (!Array.isArray(content))
734
- return "";
1005
+ const blocks = normalizeClaudeContent(content);
735
1006
  const parts = [];
736
- for (const block of content) {
737
- if (!isObject2(block))
738
- continue;
739
- if (block.type === "text" && typeof block.text === "string") {
740
- parts.push(block.text);
741
- }
1007
+ for (const b of blocks) {
1008
+ if (b.type === "text")
1009
+ parts.push(b.text);
742
1010
  }
743
1011
  return parts.join(" ").trim();
744
1012
  }
@@ -750,7 +1018,7 @@ var init_sessions = __esm(() => {
750
1018
  init_history();
751
1019
  defaultDeps3 = {
752
1020
  projectsDir() {
753
- return path3.join(homedir4(), ".claude", "projects");
1021
+ return path3.join(homedir5(), ".claude", "projects");
754
1022
  },
755
1023
  async readdir(p) {
756
1024
  try {
@@ -863,47 +1131,981 @@ async function* parseStreamJson(lines, opts = {}) {
863
1131
  }
864
1132
  continue;
865
1133
  }
866
- if (type === "user") {
867
- const content = extractContentBlocks(msg);
868
- for (const block of content) {
869
- 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")
870
2059
  continue;
871
- const blockType = typeof block.type === "string" ? block.type : undefined;
872
- if (blockType === "tool_result") {
873
- const id = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
874
- const name = id && toolNameById.get(id) || "tool";
875
- const output = "content" in block ? block.content : undefined;
876
- yield { type: "tool.result", name, output };
877
- }
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;
878
2074
  }
2075
+ if (itemId && !startedByItemId.has(itemId)) {
2076
+ const input = stripIdAndType(item);
2077
+ yield { type: "tool.start", name: itemType, input };
2078
+ }
2079
+ if (itemId)
2080
+ startedByItemId.delete(itemId);
2081
+ const output = stripIdAndType(item);
2082
+ yield { type: "tool.result", name: itemType, output };
879
2083
  continue;
880
2084
  }
881
- if (type === "result") {
882
- const usage = isObject3(msg.usage) ? msg.usage : undefined;
2085
+ if (type === "turn.completed") {
2086
+ const usage = isObject7(msg.usage) ? msg.usage : undefined;
883
2087
  if (usage) {
884
- const inTok = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
885
- const outTok = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
886
- const cacheRead = typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : undefined;
887
- 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;
888
2091
  yield {
889
2092
  type: "usage",
890
2093
  input_tokens: inTok,
891
2094
  output_tokens: outTok,
892
- ...cacheRead !== undefined ? { cache_read_input_tokens: cacheRead } : {},
893
- ...cacheCreate !== undefined ? { cache_creation_input_tokens: cacheCreate } : {}
2095
+ ...cacheRead !== undefined ? { cache_read_input_tokens: cacheRead } : {}
894
2096
  };
895
2097
  }
896
- const subtype = typeof msg.subtype === "string" ? msg.subtype : "success";
897
- if (subtype === "success") {
898
- yield { type: "done" };
899
- } else {
900
- yield { type: "error", message: `claude session ended: ${subtype}` };
901
- }
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 };
902
2104
  return;
903
2105
  }
904
2106
  }
905
2107
  }
906
- async function* readLines(stream) {
2108
+ async function* readLines2(stream) {
907
2109
  let buf = "";
908
2110
  for await (const chunk of stream) {
909
2111
  const text = typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
@@ -920,18 +2122,17 @@ async function* readLines(stream) {
920
2122
  if (buf.length > 0)
921
2123
  yield buf;
922
2124
  }
923
- function isObject3(v) {
2125
+ function isObject7(v) {
924
2126
  return typeof v === "object" && v !== null && !Array.isArray(v);
925
2127
  }
926
- function extractContentBlocks(msg) {
927
- if (Array.isArray(msg.content))
928
- return msg.content;
929
- const inner = msg.message;
930
- if (isObject3(inner) && Array.isArray(inner.content))
931
- return inner.content;
932
- return [];
2128
+ function numberOr2(v, fallback) {
2129
+ return typeof v === "number" && Number.isFinite(v) ? v : fallback;
933
2130
  }
934
- function stringifyErr(err) {
2131
+ function stripIdAndType(item) {
2132
+ const { id: _id, type: _type, ...rest } = item;
2133
+ return rest;
2134
+ }
2135
+ function stringifyErr2(err) {
935
2136
  if (err instanceof Error)
936
2137
  return err.message;
937
2138
  try {
@@ -941,14 +2142,16 @@ function stringifyErr(err) {
941
2142
  }
942
2143
  }
943
2144
 
944
- // src/engine/claude-code-local/index.ts
945
- class ClaudeCodeLocal {
2145
+ // src/engine/codex-local/index.ts
2146
+ class CodexLocal {
2147
+ identity = codexIdentity;
2148
+ capabilities = codexCapabilities;
946
2149
  registry = new SessionRegistry;
947
2150
  running = new Map;
948
2151
  binaryPathResolver;
949
2152
  stopGraceMs;
950
2153
  constructor(opts = {}) {
951
- this.binaryPathResolver = opts.binaryPathResolver ?? findClaudeBinary;
2154
+ this.binaryPathResolver = opts.binaryPathResolver ?? findCodexBinary;
952
2155
  this.stopGraceMs = opts.stopGraceMs ?? 5000;
953
2156
  }
954
2157
  async spawn(cwd, prompt, opts) {
@@ -985,41 +2188,32 @@ class ClaudeCodeLocal {
985
2188
  };
986
2189
  }
987
2190
  async readHistory(sessionId) {
988
- return readHistory(sessionId);
2191
+ return readHistoryWithMetrics(sessionId);
989
2192
  }
990
2193
  async deleteHistory(sessionId) {
991
- return deleteHistory(sessionId);
2194
+ return deleteHistory2(sessionId);
992
2195
  }
993
2196
  async listSessions(cwd) {
994
- return listSessionsForCwd(cwd);
2197
+ return listSessionsForCwd2(cwd);
995
2198
  }
996
2199
  async stop(handle) {
997
2200
  const sid = handle.sessionId;
998
- const session = this.running.get(sid);
999
- const shouldRescue = !!session && !session.completedNaturally && session.prompt.trim().length > 0;
1000
- const rescuePrompt = session?.prompt ?? "";
1001
- const rescueCwd = session?.cwd ?? handle.cwd;
1002
2201
  await this.registry.kill(sid, this.stopGraceMs);
2202
+ const session = this.running.get(sid);
1003
2203
  if (session) {
1004
2204
  session.closed = true;
1005
2205
  this.notify(session);
1006
2206
  this.running.delete(sid);
1007
2207
  }
1008
- if (shouldRescue) {
1009
- try {
1010
- await appendInterruptedUserPrompt(sid, rescueCwd, rescuePrompt);
1011
- } catch {}
1012
- }
1013
2208
  }
1014
2209
  async start(args) {
1015
2210
  const binaryPath = await this.binaryPathResolver();
1016
- const cliPermissionMode = args.opts?.permissionMode === "plan" ? "plan" : "bypassPermissions";
1017
- const spawned = spawnClaudeProcess({
2211
+ const spawned = spawnCodexProcess({
1018
2212
  binaryPath,
1019
2213
  cwd: args.cwd,
1020
2214
  prompt: args.prompt,
1021
2215
  model: args.opts?.model,
1022
- permissionMode: cliPermissionMode,
2216
+ permissionMode: args.opts?.permissionMode,
1023
2217
  env: args.opts?.env,
1024
2218
  resumeSessionId: args.resumeSessionId
1025
2219
  });
@@ -1043,8 +2237,7 @@ class ClaudeCodeLocal {
1043
2237
  queue,
1044
2238
  waiters: [],
1045
2239
  closed: false,
1046
- completedNaturally: false,
1047
- prompt: args.prompt
2240
+ spawnedAtIso: new Date().toISOString()
1048
2241
  };
1049
2242
  this.running.set(sessionId, session);
1050
2243
  this.registry.register({
@@ -1068,22 +2261,20 @@ class ClaudeCodeLocal {
1068
2261
  }
1069
2262
  }
1070
2263
  (async () => {
1071
- const events = parseStreamJson(readLines(spawned.stdout), {
2264
+ const events = parseStreamJson2(readLines2(spawned.stdout), {
1072
2265
  onSessionId: (sid) => bind(sid)
1073
2266
  });
1074
2267
  try {
1075
2268
  for await (const ev of events) {
1076
- queue.push(ev);
1077
- if (ev.type === "done" && session) {
1078
- session.completedNaturally = true;
1079
- }
2269
+ const enriched = enrichUsageEvent2(ev, session?.spawnedAtIso);
2270
+ queue.push(enriched);
1080
2271
  if (session)
1081
2272
  this.notify(session);
1082
2273
  }
1083
2274
  } catch (err) {
1084
2275
  const ev = {
1085
2276
  type: "error",
1086
- message: `parser failure: ${err instanceof Error ? err.message : String(err)}`
2277
+ message: `codex parser failure: ${err instanceof Error ? err.message : String(err)}`
1087
2278
  };
1088
2279
  queue.push(ev);
1089
2280
  if (session)
@@ -1093,20 +2284,21 @@ class ClaudeCodeLocal {
1093
2284
  session.closed = true;
1094
2285
  this.notify(session);
1095
2286
  this.registry.unregister(session.sessionId);
2287
+ this.running.delete(session.sessionId);
1096
2288
  }
1097
2289
  if (!bound) {
1098
- rejectHandle(new Error("claude exited without emitting a session id"));
2290
+ rejectHandle(new Error("codex exited without emitting a session id"));
1099
2291
  }
1100
2292
  }
1101
2293
  })();
1102
- drainStream(spawned.stderr);
2294
+ drainStream2(spawned.stderr);
1103
2295
  spawned.proc.once("error", (err) => {
1104
2296
  if (!bound)
1105
2297
  rejectHandle(err);
1106
2298
  });
1107
2299
  spawned.proc.once("exit", () => {
1108
2300
  if (!bound) {
1109
- rejectHandle(new Error("claude exited before session id was captured"));
2301
+ rejectHandle(new Error("codex exited before session id was captured"));
1110
2302
  }
1111
2303
  });
1112
2304
  return handlePromise;
@@ -1118,25 +2310,31 @@ class ClaudeCodeLocal {
1118
2310
  w();
1119
2311
  }
1120
2312
  }
1121
- function drainStream(stream) {
2313
+ function drainStream2(stream) {
1122
2314
  const s = stream;
1123
2315
  s.on("data", () => {});
1124
2316
  s.on("error", () => {});
1125
2317
  }
1126
- var init_claude_code_local = __esm(() => {
1127
- init_binary();
1128
- init_history();
1129
- init_sessions();
1130
- 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();
1131
2329
  });
1132
2330
 
1133
2331
  // src/orchestrator/bridge/server.ts
1134
- import { mkdir as mkdir2, unlink as unlink2 } from "fs/promises";
2332
+ import { mkdir as mkdir2, unlink as unlink3 } from "fs/promises";
1135
2333
  import { createServer } from "net";
1136
2334
  import { dirname as dirname2 } from "path";
1137
2335
  async function startBridgeServer(orch, socketPath) {
1138
2336
  await mkdir2(dirname2(socketPath), { recursive: true });
1139
- await unlink2(socketPath).catch(() => {});
2337
+ await unlink3(socketPath).catch(() => {});
1140
2338
  const conns = new Set;
1141
2339
  const server = createServer((conn) => {
1142
2340
  conns.add(conn);
@@ -1177,7 +2375,7 @@ async function startBridgeServer(orch, socketPath) {
1177
2375
  conn.destroy();
1178
2376
  conns.clear();
1179
2377
  await new Promise((resolve2) => server.close(() => resolve2()));
1180
- await unlink2(socketPath).catch(() => {});
2378
+ await unlink3(socketPath).catch(() => {});
1181
2379
  }
1182
2380
  };
1183
2381
  }
@@ -1270,23 +2468,19 @@ __export(exports_bridge, {
1270
2468
  bridgeSocketPathForHome: () => bridgeSocketPathForHome
1271
2469
  });
1272
2470
  import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
1273
- import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
1274
- import { join as join3 } from "path";
2471
+ import { homedir as homedir9 } from "os";
2472
+ import { join as join5 } from "path";
1275
2473
  import { fileURLToPath as fileURLToPath2 } from "url";
1276
2474
  function bridgeSocketPathForHome(home, pid = process.pid) {
1277
- const runDir = join3(home, ".kobe", "run");
1278
- const preferred = join3(runDir, `bridge-${pid}.sock`);
1279
- const macTempSocket = process.platform === "darwin" && preferred.startsWith(tmpdir2());
1280
- if (preferred.length <= UNIX_SOCKET_PATH_LIMIT && !macTempSocket)
1281
- return preferred;
1282
- const shortTmp = process.platform === "darwin" ? "/tmp" : tmpdir2();
1283
- 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);
1284
2477
  }
1285
2478
  async function startBridge(orch, opts = {}) {
1286
- const home = opts.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir5();
1287
- const runDir = join3(home, ".kobe", "run");
2479
+ const home = opts.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir9();
2480
+ const runDir = join5(home, ".kobe", "run");
1288
2481
  const socketPath = bridgeSocketPathForHome(home);
1289
- const mcpConfigPath = join3(runDir, `mcp-${process.pid}.json`);
2482
+ const mcpConfigPath = join5(runDir, `mcp-${process.pid}.json`);
2483
+ await mkdir3(runDir, { recursive: true });
1290
2484
  const server = await startBridgeServer(orch, socketPath);
1291
2485
  await mkdir3(runDir, { recursive: true });
1292
2486
  const moduleExt = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
@@ -1309,8 +2503,8 @@ async function startBridge(orch, opts = {}) {
1309
2503
  }
1310
2504
  };
1311
2505
  }
1312
- var UNIX_SOCKET_PATH_LIMIT = 103;
1313
2506
  var init_bridge = __esm(() => {
2507
+ init_paths();
1314
2508
  init_server();
1315
2509
  });
1316
2510
 
@@ -2542,137 +3736,81 @@ var init_dev = __esm(() => {
2542
3736
  }
2543
3737
  });
2544
3738
 
2545
- // src/engine/claude-settings.ts
2546
- import { readFileSync } from "fs";
2547
- import { homedir as homedir6 } from "os";
2548
- import { join as join4 } from "path";
2549
- function readClaudeSettings() {
2550
- if (cached !== undefined)
2551
- return cached;
2552
- try {
2553
- const raw = readFileSync(SETTINGS_PATH, "utf8");
2554
- const parsed = JSON.parse(raw);
2555
- if (!parsed || typeof parsed !== "object") {
2556
- cached = null;
2557
- return null;
2558
- }
2559
- const obj = parsed;
2560
- const model = typeof obj.model === "string" && obj.model.length > 0 ? obj.model : undefined;
2561
- cached = { model };
2562
- return cached;
2563
- } catch {
2564
- cached = null;
2565
- 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;
2566
3754
  }
3755
+ return defaultCapabilities;
2567
3756
  }
2568
- function resolveDefaultModelId() {
2569
- const settings = readClaudeSettings();
2570
- if (settings?.model && settings.model.length > 0)
2571
- return settings.model;
2572
- return FALLBACK_DEFAULT_MODEL_ID;
2573
- }
2574
- var SETTINGS_PATH, cached, FALLBACK_DEFAULT_MODEL_ID = "claude-opus-4-7[1m]";
2575
- var init_claude_settings = __esm(() => {
2576
- SETTINGS_PATH = join4(homedir6(), ".claude", "settings.json");
2577
- });
2578
-
2579
- // src/session/usage-metrics.ts
2580
- function totalContextTokens(u) {
2581
- return u.input_tokens + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
2582
- }
2583
- function parseTimestampMs(value) {
2584
- const ms = new Date(value).getTime();
2585
- return Number.isFinite(ms) ? ms : null;
2586
- }
2587
- function mergeIntervals(intervals) {
2588
- if (intervals.length === 0)
2589
- return [];
2590
- const sorted = [...intervals].sort((a, b) => a.startMs - b.startMs);
2591
- const first = sorted[0];
2592
- if (!first)
2593
- return [];
2594
- const merged = [{ startMs: first.startMs, endMs: first.endMs }];
2595
- for (let i = 1;i < sorted.length; i++) {
2596
- const current = sorted[i];
2597
- const last = merged[merged.length - 1];
2598
- 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)
2599
3762
  continue;
2600
- if (current.startMs <= last.endMs) {
2601
- last.endMs = Math.max(last.endMs, current.endMs);
2602
- } else {
2603
- 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);
2604
3769
  }
2605
3770
  }
2606
- return merged;
2607
- }
2608
- function durationMs(intervals) {
2609
- return intervals.reduce((total, interval) => total + (interval.endMs - interval.startMs), 0);
3771
+ return out;
2610
3772
  }
2611
- function deriveSessionUsageMetrics(past) {
2612
- let latestUsage;
2613
- let latestUsageTimestampMs = null;
2614
- let lastUserTimestampMs = null;
2615
- let inputTokens = 0;
2616
- let outputTokens = 0;
2617
- const intervals = [];
2618
- for (const message of past) {
2619
- const timestampMs = parseTimestampMs(message.timestamp);
2620
- if (message.role === "user" && timestampMs !== null) {
2621
- lastUserTimestampMs = timestampMs;
2622
- continue;
2623
- }
2624
- 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)
2625
3777
  continue;
2626
- if (timestampMs !== null && (latestUsageTimestampMs === null || timestampMs > latestUsageTimestampMs)) {
2627
- latestUsageTimestampMs = timestampMs;
2628
- latestUsage = message.usage;
2629
- } else if (latestUsage === undefined) {
2630
- latestUsage = message.usage;
2631
- }
2632
- inputTokens += message.usage.input_tokens;
2633
- outputTokens += message.usage.output_tokens;
2634
- if (timestampMs !== null && lastUserTimestampMs !== null && timestampMs > lastUserTimestampMs) {
2635
- intervals.push({ startMs: lastUserTimestampMs, endMs: timestampMs });
2636
- }
2637
- }
2638
- if (!latestUsage)
2639
- return;
2640
- const totalDurationMs = durationMs(mergeIntervals(intervals));
2641
- if (totalDurationMs <= 0)
2642
- return latestUsage;
2643
- return {
2644
- ...latestUsage,
2645
- 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
2646
3791
  };
2647
- }
2648
- function withTotalSpeedForTurn(usage, startedAtIso, endedAtIso) {
2649
- const startMs = startedAtIso ? parseTimestampMs(startedAtIso) : null;
2650
- const endMs = parseTimestampMs(endedAtIso);
2651
- if (startMs === null || endMs === null || endMs <= startMs)
2652
- return usage;
2653
- return {
2654
- ...usage,
2655
- total_speed_tokens_per_second: (usage.input_tokens + usage.output_tokens) / ((endMs - startMs) / 1000)
3792
+ ENGINE_IDENTITIES = {
3793
+ claude: claudeIdentity,
3794
+ codex: codexIdentity
2656
3795
  };
2657
- }
3796
+ defaultCapabilities = claudeCapabilities;
3797
+ defaultIdentity = claudeIdentity;
3798
+ });
2658
3799
 
2659
3800
  // src/env.ts
2660
- import { homedir as homedir7 } from "os";
2661
- import { join as join5 } from "path";
3801
+ import { homedir as homedir10 } from "os";
3802
+ import { join as join6 } from "path";
2662
3803
  function isDev() {
2663
3804
  return process.env.KOBE_DEV === "1";
2664
3805
  }
2665
3806
  function homeDir() {
2666
- return process.env.KOBE_HOME_DIR ?? homedir7();
3807
+ return process.env.KOBE_HOME_DIR ?? homedir10();
2667
3808
  }
2668
3809
  function kobeStateDir() {
2669
- return join5(homeDir(), ".kobe");
3810
+ return join6(homeDir(), ".kobe");
2670
3811
  }
2671
3812
  function kvStatePath() {
2672
- return join5(homeDir(), ".config", "kobe", "state.json");
2673
- }
2674
- function tmuxBin() {
2675
- return process.env.KOBE_TMUX_BIN ?? "tmux";
3813
+ return join6(homeDir(), ".config", "kobe", "state.json");
2676
3814
  }
2677
3815
  var init_env = () => {};
2678
3816
 
@@ -2687,11 +3825,11 @@ __export(exports_repos, {
2687
3825
  getSavedRepos: () => getSavedRepos,
2688
3826
  addSavedRepo: () => addSavedRepo
2689
3827
  });
2690
- import { spawnSync as spawnSync2 } from "child_process";
2691
- 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";
2692
3830
  import { dirname as dirname3 } from "path";
2693
3831
  function resolveRepoRoot(absPath) {
2694
- const r = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
3832
+ const r = spawnSync3("git", ["rev-parse", "--show-toplevel"], {
2695
3833
  cwd: absPath,
2696
3834
  encoding: "utf8",
2697
3835
  shell: false
@@ -2725,7 +3863,7 @@ function statePath() {
2725
3863
  }
2726
3864
  function load() {
2727
3865
  try {
2728
- const text = readFileSync2(statePath(), "utf8");
3866
+ const text = readFileSync3(statePath(), "utf8");
2729
3867
  const parsed = JSON.parse(text);
2730
3868
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2731
3869
  return parsed;
@@ -2734,11 +3872,11 @@ function load() {
2734
3872
  return {};
2735
3873
  }
2736
3874
  function save(state) {
2737
- const path4 = statePath();
2738
- mkdirSync(dirname3(path4), { recursive: true });
2739
- const tmp = `${path4}.tmp`;
3875
+ const path6 = statePath();
3876
+ mkdirSync(dirname3(path6), { recursive: true });
3877
+ const tmp = `${path6}.tmp`;
2740
3878
  writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
2741
- renameSync(tmp, path4);
3879
+ renameSync(tmp, path6);
2742
3880
  }
2743
3881
  function getSavedRepos() {
2744
3882
  const state = load();
@@ -2802,7 +3940,7 @@ function nextChatTabSeq(tabs) {
2802
3940
  max = t.seq;
2803
3941
  return max + 1;
2804
3942
  }
2805
- var toTaskId = (id) => id;
3943
+ var toTaskId = (id) => id, DEFAULT_TASK_VENDOR = "claude";
2806
3944
 
2807
3945
  // src/orchestrator/index/ulid.ts
2808
3946
  function encodeTime(now, len) {
@@ -2864,7 +4002,7 @@ var init_ulid = __esm(() => {
2864
4002
  });
2865
4003
 
2866
4004
  // src/orchestrator/metadata-suggester.ts
2867
- import { spawn as spawn3 } from "child_process";
4005
+ import { spawn as spawn4 } from "child_process";
2868
4006
 
2869
4007
  class MetadataSuggester {
2870
4008
  binaryPromise = null;
@@ -2893,7 +4031,7 @@ class MetadataSuggester {
2893
4031
  return new Promise((resolve2) => {
2894
4032
  let proc;
2895
4033
  try {
2896
- proc = spawn3(binary, ["-p", builder(trimmed)], {
4034
+ proc = spawn4(binary, ["-p", builder(trimmed)], {
2897
4035
  stdio: ["ignore", "pipe", "ignore"],
2898
4036
  env: process.env
2899
4037
  });
@@ -3042,10 +4180,10 @@ class InMemoryPendingInputBroker {
3042
4180
  }
3043
4181
 
3044
4182
  // src/orchestrator/pr/build.ts
3045
- import { spawnSync as spawnSync3 } from "child_process";
4183
+ import { spawnSync as spawnSync4 } from "child_process";
3046
4184
  function git(cwd, args) {
3047
4185
  try {
3048
- const out = spawnSync3("git", args.slice(), {
4186
+ const out = spawnSync4("git", args.slice(), {
3049
4187
  cwd,
3050
4188
  encoding: "utf8",
3051
4189
  timeout: GIT_TIMEOUT_MS
@@ -3095,7 +4233,7 @@ var init_build = () => {};
3095
4233
 
3096
4234
  // src/orchestrator/pr/instructions.ts
3097
4235
  import { promises as fs } from "fs";
3098
- import path4 from "path";
4236
+ import path6 from "path";
3099
4237
  function dirtyCountSentence(n) {
3100
4238
  if (n <= 0)
3101
4239
  return "There are no uncommitted changes.";
@@ -3123,7 +4261,7 @@ function renderPRInstructions(template, state) {
3123
4261
  async function loadPRInstructionsTemplate(worktreePath) {
3124
4262
  if (!worktreePath)
3125
4263
  return DEFAULT_PR_INSTRUCTIONS_TEMPLATE;
3126
- const file = path4.join(worktreePath, ".kobe", "pr-instructions.md");
4264
+ const file = path6.join(worktreePath, ".kobe", "pr-instructions.md");
3127
4265
  try {
3128
4266
  const text = await fs.readFile(file, "utf8");
3129
4267
  if (text.length === 0)
@@ -3174,10 +4312,11 @@ class SessionPump {
3174
4312
  }
3175
4313
  async run(taskId, tabId, handle) {
3176
4314
  const tabKey = chatRunStateKey(taskId, tabId);
4315
+ const engine = this.env.engineFor(taskId, tabId);
3177
4316
  let killedForInput = false;
3178
4317
  let terminalEvent = null;
3179
4318
  try {
3180
- for await (const ev of this.env.engine.stream(handle)) {
4319
+ for await (const ev of engine.stream(handle)) {
3181
4320
  const inputReq = detectUserInputFromEngineEvent(ev);
3182
4321
  if (inputReq) {
3183
4322
  this.env.dispatch(taskId, tabId, ev);
@@ -3191,7 +4330,7 @@ class SessionPump {
3191
4330
  });
3192
4331
  killedForInput = true;
3193
4332
  try {
3194
- await this.env.engine.stop(handle);
4333
+ await engine.stop(handle);
3195
4334
  } catch {}
3196
4335
  break;
3197
4336
  }
@@ -3204,7 +4343,7 @@ class SessionPump {
3204
4343
  } finally {
3205
4344
  if (!killedForInput) {
3206
4345
  try {
3207
- await this.env.engine.stop(handle);
4346
+ await engine.stop(handle);
3208
4347
  } catch {}
3209
4348
  }
3210
4349
  }
@@ -3264,7 +4403,8 @@ function summarizeWorktreeError(raw, repo, baseRef) {
3264
4403
  }
3265
4404
 
3266
4405
  class Orchestrator {
3267
- engine;
4406
+ engines;
4407
+ fallbackEngine;
3268
4408
  store;
3269
4409
  worktrees;
3270
4410
  metadataSuggester;
@@ -3282,7 +4422,26 @@ class Orchestrator {
3282
4422
  runStateAcc;
3283
4423
  setRunState;
3284
4424
  constructor(deps) {
3285
- 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;
3286
4445
  this.store = deps.store;
3287
4446
  this.worktrees = deps.worktrees;
3288
4447
  this.metadataSuggester = deps.metadataSuggester ?? new MetadataSuggester;
@@ -3296,13 +4455,86 @@ class Orchestrator {
3296
4455
  this.runStateAcc = runState;
3297
4456
  this.setRunState = (next) => setRunState(() => next);
3298
4457
  this.sessionPump = new SessionPump({
3299
- engine: this.engine,
4458
+ engineFor: (taskId, tabId) => this.engineForTaskTabId(taskId, tabId),
3300
4459
  broker: this.pendingInputBroker,
3301
4460
  dispatch: (taskId, tabId, ev) => this.dispatchEvent(taskId, tabId, ev),
3302
4461
  nextRequestId: () => `req-${++this.requestIdCounter}`,
3303
4462
  onPendingInputChange: () => this.bumpRunState()
3304
4463
  });
3305
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
+ }
3306
4538
  bumpRunState() {
3307
4539
  const next = new Map;
3308
4540
  for (const tabKey2 of this.pendingInputBroker.awaitingTabKeys()) {
@@ -3522,10 +4754,11 @@ class Orchestrator {
3522
4754
  if (prompt && prompt.trim().length > 0) {
3523
4755
  this.dispatchEvent(task.id, targetTab.id, { type: "user.inject", text: prompt });
3524
4756
  }
3525
- 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);
3526
4759
  let handle;
3527
4760
  if (targetTab.sessionId) {
3528
- handle = await this.engine.resume(targetTab.sessionId, promptToSend, {
4761
+ handle = await engine.resume(targetTab.sessionId, promptToSend, {
3529
4762
  cwd: task.worktreePath,
3530
4763
  env: { KOBE_RESUME_CWD: task.worktreePath },
3531
4764
  permissionMode: task.permissionMode,
@@ -3538,7 +4771,7 @@ class Orchestrator {
3538
4771
  });
3539
4772
  this.firstSpawnLatches.set(key, latch);
3540
4773
  try {
3541
- handle = await this.engine.spawn(task.worktreePath, promptToSend, {
4774
+ handle = await engine.spawn(task.worktreePath, promptToSend, {
3542
4775
  permissionMode: task.permissionMode,
3543
4776
  model: modelToUse
3544
4777
  });
@@ -3613,7 +4846,7 @@ class Orchestrator {
3613
4846
  text: "(turn interrupted \u2014 sending new prompt)"
3614
4847
  });
3615
4848
  try {
3616
- await this.engine.stop(handle);
4849
+ await this.engineForTask(task).stop(handle);
3617
4850
  } finally {
3618
4851
  this.handles.delete(key);
3619
4852
  this.bumpRunState();
@@ -3655,11 +4888,13 @@ class Orchestrator {
3655
4888
  return;
3656
4889
  await this.store.update(task.id, { permissionMode: mode });
3657
4890
  }
3658
- async setModel(id, model) {
4891
+ async setModel(id, model, tabId) {
3659
4892
  const task = this.requireTask(id);
3660
- 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)
3661
4896
  return;
3662
- await this.store.update(task.id, { model });
4897
+ await this.updateTab(task.id, tab.id, { model, vendor });
3663
4898
  }
3664
4899
  async setTitle(id, title) {
3665
4900
  const task = this.requireTask(id);
@@ -3723,7 +4958,7 @@ class Orchestrator {
3723
4958
  }
3724
4959
  if (task.sessionId) {
3725
4960
  try {
3726
- await this.engine.deleteHistory(task.sessionId);
4961
+ await this.engineForTask(task).deleteHistory(task.sessionId);
3727
4962
  } catch (err) {
3728
4963
  console.error(`[kobe orchestrator] deleteTask: deleteHistory failed for ${task.id}:`, err);
3729
4964
  }
@@ -3732,15 +4967,15 @@ class Orchestrator {
3732
4967
  await this.store.remove(task.id);
3733
4968
  }
3734
4969
  async readHistory(sessionId) {
3735
- try {
3736
- return await this.engine.readHistory(sessionId);
3737
- } catch {
3738
- return [];
3739
- }
4970
+ return (await this.readHistoryWithMetrics(sessionId)).messages;
3740
4971
  }
3741
4972
  async readHistoryWithMetrics(sessionId) {
3742
- const messages = await this.readHistory(sessionId);
3743
- 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;
3744
4979
  return {
3745
4980
  messages,
3746
4981
  ...usageMetrics ? { usageMetrics } : {}
@@ -3750,14 +4985,16 @@ class Orchestrator {
3750
4985
  const task = this.requireTask(id);
3751
4986
  if (!task.worktreePath)
3752
4987
  return [];
4988
+ const tab = this.resolveTab(task);
3753
4989
  try {
3754
- return await this.engine.listSessions(task.worktreePath);
4990
+ return await this.engineForTab(task, tab).listSessions(task.worktreePath);
3755
4991
  } catch {
3756
4992
  return [];
3757
4993
  }
3758
4994
  }
3759
4995
  async openSessionInTab(id, sessionId, opts = {}) {
3760
4996
  const task = this.requireTask(id);
4997
+ const active = this.resolveTab(task);
3761
4998
  const existing = task.tabs.find((t) => t.sessionId === sessionId);
3762
4999
  if (existing) {
3763
5000
  await this.setActiveTab(task.id, existing.id);
@@ -3768,6 +5005,8 @@ class Orchestrator {
3768
5005
  sessionId,
3769
5006
  seq: nextChatTabSeq(task.tabs),
3770
5007
  createdAt: new Date().toISOString(),
5008
+ model: active.model ?? task.model,
5009
+ vendor: this.vendorForTab(task, active),
3771
5010
  ...opts.title ? { title: opts.title } : {}
3772
5011
  };
3773
5012
  await this.store.update(task.id, { tabs: [...task.tabs, tab], activeTabId: tab.id });
@@ -3795,11 +5034,14 @@ class Orchestrator {
3795
5034
  }
3796
5035
  async createTab(id, opts = {}) {
3797
5036
  const task = this.requireTask(id);
5037
+ const active = this.resolveTab(task);
3798
5038
  const tab = {
3799
5039
  id: ulid(),
3800
5040
  sessionId: null,
3801
5041
  seq: nextChatTabSeq(task.tabs),
3802
5042
  createdAt: new Date().toISOString(),
5043
+ model: active.model ?? task.model,
5044
+ vendor: this.vendorForTab(task, active),
3803
5045
  ...opts.title ? { title: opts.title } : {}
3804
5046
  };
3805
5047
  const tabs = [...task.tabs, tab];
@@ -3879,7 +5121,7 @@ class Orchestrator {
3879
5121
  if (!handle)
3880
5122
  return;
3881
5123
  try {
3882
- await this.engine.stop(handle);
5124
+ await this.engineForTaskId(taskId).stop(handle);
3883
5125
  } finally {
3884
5126
  this.handles.delete(key);
3885
5127
  this.bumpRunState();
@@ -3888,12 +5130,13 @@ class Orchestrator {
3888
5130
  async stopAllTabsForTask(taskId) {
3889
5131
  const prefix = `${taskId}:`;
3890
5132
  const keys = Array.from(this.handles.keys()).filter((k) => k.startsWith(prefix));
5133
+ const engine = this.engineForTaskId(taskId);
3891
5134
  for (const key of keys) {
3892
5135
  const handle = this.handles.get(key);
3893
5136
  if (!handle)
3894
5137
  continue;
3895
5138
  try {
3896
- await this.engine.stop(handle);
5139
+ await engine.stop(handle);
3897
5140
  } catch {}
3898
5141
  this.handles.delete(key);
3899
5142
  }
@@ -4019,7 +5262,7 @@ function deriveTitleFromPrompt(prompt) {
4019
5262
  var CONCURRENCY_CAP = 20, PLACEHOLDER_TASK_TITLE = "(new task)", IllegalTransitionError, ConcurrencyCapError, PRPreconditionError, TaskNotFoundError, CannotDeleteMainTaskError, TITLE_CHAR_CAP = 40;
4020
5263
  var init_core = __esm(() => {
4021
5264
  init_dev();
4022
- init_claude_settings();
5265
+ init_registry();
4023
5266
  init_repos();
4024
5267
  init_ulid();
4025
5268
  init_metadata_suggester();
@@ -4064,9 +5307,9 @@ var init_core = __esm(() => {
4064
5307
  });
4065
5308
 
4066
5309
  // src/orchestrator/index/store.ts
4067
- import { mkdir as mkdir4, open, readFile as readFile3, rename, unlink as unlink3 } from "fs/promises";
4068
- import { homedir as homedir8 } from "os";
4069
- 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";
4070
5313
 
4071
5314
  class TaskIndexStore {
4072
5315
  homeDir;
@@ -4078,9 +5321,9 @@ class TaskIndexStore {
4078
5321
  listeners = new Set;
4079
5322
  saveChain = Promise.resolve();
4080
5323
  constructor(options = {}) {
4081
- this.homeDir = options.homeDir ?? homedir8();
4082
- this.kobeDir = join6(this.homeDir, ".kobe");
4083
- 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");
4084
5327
  this.tmpPath = `${this.path}.tmp`;
4085
5328
  }
4086
5329
  subscribe(listener) {
@@ -4105,7 +5348,7 @@ class TaskIndexStore {
4105
5348
  async load() {
4106
5349
  let raw;
4107
5350
  try {
4108
- raw = await readFile3(this.path, "utf8");
5351
+ raw = await readFile4(this.path, "utf8");
4109
5352
  } catch (err) {
4110
5353
  const code = err.code;
4111
5354
  if (code === "ENOENT") {
@@ -4142,7 +5385,7 @@ class TaskIndexStore {
4142
5385
  const payload = this.snapshot();
4143
5386
  const json = `${JSON.stringify(payload, null, 2)}
4144
5387
  `;
4145
- const handle = await open(this.tmpPath, "w", 420);
5388
+ const handle = await open2(this.tmpPath, "w", 420);
4146
5389
  try {
4147
5390
  await handle.writeFile(json, "utf8");
4148
5391
  await handle.sync();
@@ -4170,7 +5413,16 @@ class TaskIndexStore {
4170
5413
  activeTabId = activeIn && tabsIn.some((t) => t.id === activeIn) ? activeIn : tabsIn[0]?.id ?? "";
4171
5414
  } else {
4172
5415
  const tabId = ulid();
4173
- 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
+ ];
4174
5426
  activeTabId = tabId;
4175
5427
  }
4176
5428
  const firstSession = tabs[0]?.sessionId ?? null;
@@ -4237,13 +5489,13 @@ class TaskIndexStore {
4237
5489
  }
4238
5490
  async _unlinkForTests() {
4239
5491
  try {
4240
- await unlink3(this.path);
5492
+ await unlink4(this.path);
4241
5493
  } catch (err) {
4242
5494
  if (err.code !== "ENOENT")
4243
5495
  throw err;
4244
5496
  }
4245
5497
  try {
4246
- await unlink3(this.tmpPath);
5498
+ await unlink4(this.tmpPath);
4247
5499
  } catch (err) {
4248
5500
  if (err.code !== "ENOENT")
4249
5501
  throw err;
@@ -4333,7 +5585,9 @@ function coerceTask(value) {
4333
5585
  sessionId: tt.sessionId ?? null,
4334
5586
  seq,
4335
5587
  createdAt: tt.createdAt,
4336
- ...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 } : {}
4337
5591
  };
4338
5592
  tabs.push(tab);
4339
5593
  }
@@ -4366,6 +5620,7 @@ function coerceTask(value) {
4366
5620
  kind: v.kind === "main" ? "main" : "task",
4367
5621
  permissionMode: isPermissionMode(v.permissionMode) ? v.permissionMode : undefined,
4368
5622
  model: typeof v.model === "string" ? v.model : undefined,
5623
+ vendor: resolveTaskVendor(v.vendor, typeof v.model === "string" ? v.model : undefined),
4369
5624
  createdAt: v.createdAt,
4370
5625
  updatedAt: v.updatedAt
4371
5626
  };
@@ -4373,20 +5628,33 @@ function coerceTask(value) {
4373
5628
  function isPermissionMode(v) {
4374
5629
  return v === "default" || v === "plan";
4375
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
+ }
4376
5643
  function isTaskStatus(s) {
4377
5644
  return s === "backlog" || s === "in_progress" || s === "in_review" || s === "done" || s === "canceled" || s === "error";
4378
5645
  }
4379
5646
  var init_store = __esm(() => {
5647
+ init_registry();
4380
5648
  init_ulid();
4381
5649
  });
4382
5650
 
4383
5651
  // src/orchestrator/worktree/git.ts
4384
- import { spawnSync as spawnSync4 } from "child_process";
5652
+ import { spawnSync as spawnSync5 } from "child_process";
4385
5653
  function git2(args, opts) {
4386
5654
  if (!opts.cwd) {
4387
5655
  throw new Error("git(): cwd is required; refusing to inherit from process.cwd()");
4388
5656
  }
4389
- const proc = spawnSync4("git", [...args], {
5657
+ const proc = spawnSync5("git", [...args], {
4390
5658
  cwd: opts.cwd,
4391
5659
  env: opts.env ? { ...process.env, ...opts.env } : process.env,
4392
5660
  encoding: "utf8",
@@ -4424,32 +5692,32 @@ var init_git = __esm(() => {
4424
5692
 
4425
5693
  // src/orchestrator/worktree/paths.ts
4426
5694
  import fs2 from "fs";
4427
- import path5 from "path";
5695
+ import path7 from "path";
4428
5696
  function worktreeRootFor(repo) {
4429
- if (!path5.isAbsolute(repo)) {
5697
+ if (!path7.isAbsolute(repo)) {
4430
5698
  throw new Error(`worktreeRootFor: repo must be an absolute path, got: ${repo}`);
4431
5699
  }
4432
- return path5.join(repo, KOBE_WORKTREE_ROOT_SUBPATH);
5700
+ return path7.join(repo, KOBE_WORKTREE_ROOT_SUBPATH);
4433
5701
  }
4434
5702
  function worktreePathFor(repo, taskId) {
4435
5703
  if (!taskId || /[/\\\0]/.test(taskId)) {
4436
5704
  throw new Error(`worktreePathFor: invalid taskId: ${JSON.stringify(taskId)}`);
4437
5705
  }
4438
- return path5.join(worktreeRootFor(repo), taskId);
5706
+ return path7.join(worktreeRootFor(repo), taskId);
4439
5707
  }
4440
5708
  function isKobeManagedPath(repo, candidate) {
4441
- if (!path5.isAbsolute(repo) || !path5.isAbsolute(candidate))
5709
+ if (!path7.isAbsolute(repo) || !path7.isAbsolute(candidate))
4442
5710
  return false;
4443
5711
  const root = canonicalize(worktreeRootFor(repo));
4444
5712
  const target = canonicalize(candidate);
4445
- const rel = path5.relative(root, target);
4446
- return rel !== "" && !rel.startsWith("..") && !path5.isAbsolute(rel);
5713
+ const rel = path7.relative(root, target);
5714
+ return rel !== "" && !rel.startsWith("..") && !path7.isAbsolute(rel);
4447
5715
  }
4448
5716
  function canonicalize(p) {
4449
5717
  try {
4450
5718
  return fs2.realpathSync(p);
4451
5719
  } catch {
4452
- return path5.resolve(p);
5720
+ return path7.resolve(p);
4453
5721
  }
4454
5722
  }
4455
5723
  var KOBE_WORKTREE_ROOT_SUBPATH = ".claude/worktrees";
@@ -4457,7 +5725,7 @@ var init_paths2 = () => {};
4457
5725
 
4458
5726
  // src/orchestrator/worktree/manager.ts
4459
5727
  import fs3 from "fs";
4460
- import path6 from "path";
5728
+ import path8 from "path";
4461
5729
 
4462
5730
  class GitWorktreeManager {
4463
5731
  async create(repo, branch, worktreePath, baseRef) {
@@ -4475,7 +5743,7 @@ class GitWorktreeManager {
4475
5743
  }
4476
5744
  throw new Error(`create(): ${worktreePath} exists but is not a registered git worktree`);
4477
5745
  }
4478
- fs3.mkdirSync(path6.dirname(worktreePath), { recursive: true });
5746
+ fs3.mkdirSync(path8.dirname(worktreePath), { recursive: true });
4479
5747
  const branchExists = this.branchExists(repo, branch);
4480
5748
  const args = branchExists ? ["worktree", "add", worktreePath, branch] : baseRef ? ["worktree", "add", "-b", branch, worktreePath, baseRef] : ["worktree", "add", "-b", branch, worktreePath];
4481
5749
  git2(args, { cwd: repo });
@@ -4530,8 +5798,8 @@ class GitWorktreeManager {
4530
5798
  if (!entry.branch || entry.detached)
4531
5799
  continue;
4532
5800
  const canonEntry = canonicalize2(entry.path);
4533
- const rel = path6.relative(canonRoot, canonEntry);
4534
- const callerPath = path6.join(callerRoot, rel);
5801
+ const rel = path8.relative(canonRoot, canonEntry);
5802
+ const callerPath = path8.join(callerRoot, rel);
4535
5803
  const dirty = await this.isDirty(entry.path);
4536
5804
  infos.push({
4537
5805
  path: callerPath,
@@ -4592,9 +5860,9 @@ class GitWorktreeManager {
4592
5860
  const gitDir = out.stdout.trim();
4593
5861
  if (!gitDir)
4594
5862
  return null;
4595
- const absolute = path6.isAbsolute(gitDir) ? gitDir : path6.resolve(worktreePath, gitDir);
4596
- const base = path6.basename(absolute);
4597
- 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;
4598
5866
  } catch (err) {
4599
5867
  if (err instanceof GitCommandError)
4600
5868
  return null;
@@ -4634,7 +5902,7 @@ function parsePorcelain(out) {
4634
5902
  return records;
4635
5903
  }
4636
5904
  function requireAbsolute(name, value) {
4637
- if (!value || !path6.isAbsolute(value)) {
5905
+ if (!value || !path8.isAbsolute(value)) {
4638
5906
  throw new Error(`${name} must be an absolute path, got: ${JSON.stringify(value)}`);
4639
5907
  }
4640
5908
  }
@@ -4642,7 +5910,7 @@ function canonicalize2(p) {
4642
5910
  try {
4643
5911
  return fs3.realpathSync(p);
4644
5912
  } catch {
4645
- return path6.resolve(p);
5913
+ return path8.resolve(p);
4646
5914
  }
4647
5915
  }
4648
5916
  var init_manager = __esm(() => {
@@ -4653,22 +5921,26 @@ var init_manager = __esm(() => {
4653
5921
  // src/bin/kobed.ts
4654
5922
  init_daemon_process();
4655
5923
  init_client();
4656
- import { unlink as unlink5 } from "fs/promises";
5924
+ import { unlink as unlink6 } from "fs/promises";
4657
5925
 
4658
5926
  // src/core/index.ts
4659
5927
  init_claude_code_local();
5928
+ init_codex_local();
4660
5929
  init_bridge();
4661
5930
  init_core();
4662
5931
  init_store();
4663
5932
  init_manager();
4664
- import { homedir as homedir9 } from "os";
5933
+ import { homedir as homedir12 } from "os";
4665
5934
  async function createKobeCore(options = {}) {
4666
- const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir9();
5935
+ const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir12();
4667
5936
  const store = new TaskIndexStore({ homeDir: homeDir2 });
4668
5937
  await store.load();
4669
5938
  const worktrees = new GitWorktreeManager;
4670
- const engine = options.engine ?? new ClaudeCodeLocal;
4671
- 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 });
4672
5944
  const bridge = options.startMcpBridge === false ? null : await startBridge(orchestrator, { homeDir: homeDir2 });
4673
5945
  return {
4674
5946
  homeDir: homeDir2,
@@ -4689,16 +5961,16 @@ init_paths();
4689
5961
  // src/daemon/server.ts
4690
5962
  init_repos();
4691
5963
  init_paths();
4692
- 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";
4693
5965
  import { createServer as createServer2 } from "net";
4694
5966
  import { dirname as dirname5 } from "path";
4695
5967
 
4696
5968
  // src/engine/claude-code-local/plan-usage.ts
4697
5969
  import { execFile } from "child_process";
4698
- import { createHash } from "crypto";
4699
- import { readFile as readFile4 } from "fs/promises";
4700
- import { homedir as homedir10, userInfo } from "os";
4701
- 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";
4702
5974
  import { promisify } from "util";
4703
5975
  var execFileAsync = promisify(execFile);
4704
5976
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
@@ -4709,7 +5981,7 @@ function keychainServiceName() {
4709
5981
  const configDir = process.env.CLAUDE_CONFIG_DIR;
4710
5982
  if (!configDir)
4711
5983
  return `${KEYCHAIN_BASE}${KEYCHAIN_SUFFIX}`;
4712
- const hash = createHash("sha256").update(configDir).digest("hex").slice(0, 8);
5984
+ const hash = createHash2("sha256").update(configDir).digest("hex").slice(0, 8);
4713
5985
  return `${KEYCHAIN_BASE}${KEYCHAIN_SUFFIX}-${hash}`;
4714
5986
  }
4715
5987
  function keychainAccount() {
@@ -4733,10 +6005,10 @@ async function readKeychainToken() {
4733
6005
  }
4734
6006
  }
4735
6007
  async function readPlainTextToken() {
4736
- const configDir = process.env.CLAUDE_CONFIG_DIR ?? join7(homedir10(), ".claude");
4737
- const path7 = join7(configDir, ".credentials.json");
6008
+ const configDir = process.env.CLAUDE_CONFIG_DIR ?? join8(homedir13(), ".claude");
6009
+ const path9 = join8(configDir, ".credentials.json");
4738
6010
  try {
4739
- const raw = await readFile4(path7, "utf8");
6011
+ const raw = await readFile5(path9, "utf8");
4740
6012
  return parseStoredOAuth(raw);
4741
6013
  } catch {
4742
6014
  return null;
@@ -4849,7 +6121,7 @@ function createPlanUsagePoller(options) {
4849
6121
  }
4850
6122
  // src/daemon/rc-bridge.ts
4851
6123
  init_binary();
4852
- import { spawn as spawn4 } from "child_process";
6124
+ import { spawn as spawn5 } from "child_process";
4853
6125
  var ENV_ID_RE = /Environment ID:\s*(env_[A-Za-z0-9]+)/;
4854
6126
  var DEEPLINK_RE = /https:\/\/claude\.ai\/code\?environment=([A-Za-z0-9_]+)/;
4855
6127
  var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
@@ -4857,7 +6129,7 @@ function createRcBridge(options = {}) {
4857
6129
  const stopGraceMs = options.stopGraceMs ?? 5000;
4858
6130
  const readyTimeoutMs = options.readyTimeoutMs ?? 30000;
4859
6131
  const binaryPathResolver = options.binaryPathResolver ?? findClaudeBinary;
4860
- const spawner = options.spawner ?? ((cmd, args, cwd) => spawn4(cmd, [...args], {
6132
+ const spawner = options.spawner ?? ((cmd, args, cwd) => spawn5(cmd, [...args], {
4861
6133
  cwd,
4862
6134
  stdio: ["ignore", "pipe", "pipe"],
4863
6135
  env: { ...process.env }
@@ -5031,7 +6303,7 @@ async function startDaemonServer(orch, options = {}) {
5031
6303
  let nextClientId = 1;
5032
6304
  await mkdir5(dirname5(socketPath), { recursive: true });
5033
6305
  await mkdir5(dirname5(pidPath), { recursive: true });
5034
- await unlink4(socketPath).catch(() => {});
6306
+ await unlink5(socketPath).catch(() => {});
5035
6307
  const server = createServer2((socket) => {
5036
6308
  const client = {
5037
6309
  id: nextClientId++,
@@ -5076,8 +6348,8 @@ async function startDaemonServer(orch, options = {}) {
5076
6348
  client.socket.destroy();
5077
6349
  }
5078
6350
  await new Promise((resolve2) => server.close(() => resolve2()));
5079
- await unlink4(socketPath).catch(() => {});
5080
- await unlink4(pidPath).catch(() => {});
6351
+ await unlink5(socketPath).catch(() => {});
6352
+ await unlink5(pidPath).catch(() => {});
5081
6353
  }
5082
6354
  };
5083
6355
  planUsagePoller.start();
@@ -5191,7 +6463,7 @@ async function startDaemonServer(orch, options = {}) {
5191
6463
  }
5192
6464
  case "task.model": {
5193
6465
  const taskId = requireString2(payload, "taskId");
5194
- await orch.setModel(taskId, optionalString2(payload, "model"));
6466
+ await orch.setModel(taskId, optionalString2(payload, "model"), optionalString2(payload, "tabId"));
5195
6467
  broadcastTaskUpdated(orch, clients, taskId);
5196
6468
  return {};
5197
6469
  }
@@ -5390,7 +6662,7 @@ async function startDaemonServer(orch, options = {}) {
5390
6662
  }
5391
6663
  async function readPidFile(pidPath) {
5392
6664
  try {
5393
- const raw = await readFile5(pidPath, "utf8");
6665
+ const raw = await readFile6(pidPath, "utf8");
5394
6666
  const pid = Number(raw.trim());
5395
6667
  return Number.isFinite(pid) ? pid : null;
5396
6668
  } catch {
@@ -5428,8 +6700,7 @@ async function readTaskHistory(orch, taskId, requestedSessionId, limit, before)
5428
6700
  const sessionId = requestedSessionId ?? task?.tabs.find((t) => t.id === task.activeTabId)?.sessionId ?? task?.sessionId;
5429
6701
  if (!sessionId)
5430
6702
  return { messages: [], nextBefore: null, hasMore: false };
5431
- const messages = await orch.readHistory(sessionId);
5432
- const usageMetrics = deriveSessionUsageMetrics(messages);
6703
+ const { messages, usageMetrics } = await orch.readHistoryWithMetrics(sessionId);
5433
6704
  const beforeIdx = before ? messages.findIndex((m) => `${m.timestamp}:${m.sessionId}` === before) : -1;
5434
6705
  const end = beforeIdx >= 0 ? beforeIdx : messages.length;
5435
6706
  const start = Math.max(0, end - limit);
@@ -5575,7 +6846,7 @@ async function main() {
5575
6846
  await new Promise((resolve2) => setTimeout(resolve2, 100));
5576
6847
  } catch {}
5577
6848
  }
5578
- await unlink5(socketPath).catch(() => {});
6849
+ await unlink6(socketPath).catch(() => {});
5579
6850
  const next = await connectOrStartDaemon();
5580
6851
  next.close();
5581
6852
  console.log(`kobed: restarted, listening on ${socketPath}`);