@sma1lboy/kobe 0.5.8 → 0.5.10

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
@@ -362,7 +362,8 @@ var init_binary = __esm(() => {
362
362
  });
363
363
 
364
364
  // src/engine/claude-code-local/history.ts
365
- import { readFile, readdir, unlink } from "fs/promises";
365
+ import { randomUUID } from "crypto";
366
+ import { appendFile, mkdir, readFile, readdir, unlink, writeFile } from "fs/promises";
366
367
  import { homedir as homedir3 } from "os";
367
368
  import path2 from "path";
368
369
  function encodeCwd(cwd) {
@@ -459,6 +460,76 @@ function extractUsage(v) {
459
460
  function isObject(v) {
460
461
  return typeof v === "object" && v !== null && !Array.isArray(v);
461
462
  }
463
+ async function appendInterruptedUserPrompt(sessionId, cwd, prompt, deps = defaultDeps2) {
464
+ if (!prompt || prompt.trim().length === 0)
465
+ return;
466
+ const projectDir = path2.join(deps.projectsDir(), encodeCwd(cwd));
467
+ const filePath = path2.join(projectDir, `${sessionId}.jsonl`);
468
+ let lines = [];
469
+ try {
470
+ const raw = await readFile(filePath, "utf8");
471
+ lines = raw.split(`
472
+ `).filter((l) => l.length > 0);
473
+ } catch (err) {
474
+ if (err.code !== "ENOENT")
475
+ throw err;
476
+ await mkdir(projectDir, { recursive: true });
477
+ }
478
+ let lastConvIdx = -1;
479
+ let lastConvRecord = null;
480
+ let lastConvRole = null;
481
+ for (let i = lines.length - 1;i >= 0; i--) {
482
+ let parsed;
483
+ try {
484
+ parsed = JSON.parse(lines[i]);
485
+ } catch {
486
+ continue;
487
+ }
488
+ if (!isObject(parsed))
489
+ continue;
490
+ const inner = isObject(parsed.message) ? parsed.message : parsed;
491
+ const role = inner.role;
492
+ if (role === "user" || role === "assistant") {
493
+ lastConvIdx = i;
494
+ lastConvRecord = parsed;
495
+ lastConvRole = role;
496
+ break;
497
+ }
498
+ }
499
+ const now = new Date().toISOString();
500
+ if (lastConvRole === "user" && lastConvRecord && lastConvIdx >= 0) {
501
+ const inner = isObject(lastConvRecord.message) ? lastConvRecord.message : lastConvRecord;
502
+ const existing = typeof inner.content === "string" ? inner.content : "";
503
+ if (existing === prompt || existing.endsWith(`
504
+
505
+ ${prompt}`))
506
+ return;
507
+ inner.content = existing.length > 0 ? `${existing}
508
+
509
+ ${prompt}` : prompt;
510
+ lastConvRecord.timestamp = now;
511
+ lines[lastConvIdx] = JSON.stringify(lastConvRecord);
512
+ await writeFile(filePath, `${lines.join(`
513
+ `)}
514
+ `);
515
+ return;
516
+ }
517
+ const parentUuid = lastConvRecord && typeof lastConvRecord.uuid === "string" ? lastConvRecord.uuid : null;
518
+ const record = {
519
+ type: "user",
520
+ message: { role: "user", content: prompt },
521
+ uuid: randomUUID(),
522
+ parentUuid,
523
+ sessionId,
524
+ cwd,
525
+ timestamp: now,
526
+ isSidechain: false,
527
+ userType: "external",
528
+ version: "1.0.0"
529
+ };
530
+ await appendFile(filePath, `${JSON.stringify(record)}
531
+ `);
532
+ }
462
533
  var defaultDeps2;
463
534
  var init_history = __esm(() => {
464
535
  defaultDeps2 = {
@@ -866,13 +937,21 @@ class ClaudeCodeLocal {
866
937
  }
867
938
  async stop(handle) {
868
939
  const sid = handle.sessionId;
869
- await this.registry.kill(sid, this.stopGraceMs);
870
940
  const session = this.running.get(sid);
941
+ const shouldRescue = !!session && !session.completedNaturally && session.prompt.trim().length > 0;
942
+ const rescuePrompt = session?.prompt ?? "";
943
+ const rescueCwd = session?.cwd ?? handle.cwd;
944
+ await this.registry.kill(sid, this.stopGraceMs);
871
945
  if (session) {
872
946
  session.closed = true;
873
947
  this.notify(session);
874
948
  this.running.delete(sid);
875
949
  }
950
+ if (shouldRescue) {
951
+ try {
952
+ await appendInterruptedUserPrompt(sid, rescueCwd, rescuePrompt);
953
+ } catch {}
954
+ }
876
955
  }
877
956
  async start(args) {
878
957
  const binaryPath = await this.binaryPathResolver();
@@ -905,14 +984,17 @@ class ClaudeCodeLocal {
905
984
  spawned,
906
985
  queue,
907
986
  waiters: [],
908
- closed: false
987
+ closed: false,
988
+ completedNaturally: false,
989
+ prompt: args.prompt
909
990
  };
910
991
  this.running.set(sessionId, session);
911
992
  this.registry.register({
912
993
  sessionId,
913
994
  cwd: args.cwd,
914
995
  proc: spawned.proc,
915
- startedAt: Date.now()
996
+ startedAt: Date.now(),
997
+ prompt: args.prompt
916
998
  });
917
999
  resolveHandle({ sessionId, cwd: args.cwd });
918
1000
  };
@@ -934,6 +1016,9 @@ class ClaudeCodeLocal {
934
1016
  try {
935
1017
  for await (const ev of events) {
936
1018
  queue.push(ev);
1019
+ if (ev.type === "done" && session) {
1020
+ session.completedNaturally = true;
1021
+ }
937
1022
  if (session)
938
1023
  this.notify(session);
939
1024
  }
@@ -988,11 +1073,11 @@ var init_claude_code_local = __esm(() => {
988
1073
  });
989
1074
 
990
1075
  // src/orchestrator/bridge/server.ts
991
- import { mkdir, unlink as unlink2 } from "fs/promises";
1076
+ import { mkdir as mkdir2, unlink as unlink2 } from "fs/promises";
992
1077
  import { createServer } from "net";
993
1078
  import { dirname as dirname2 } from "path";
994
1079
  async function startBridgeServer(orch, socketPath) {
995
- await mkdir(dirname2(socketPath), { recursive: true });
1080
+ await mkdir2(dirname2(socketPath), { recursive: true });
996
1081
  await unlink2(socketPath).catch(() => {});
997
1082
  const conns = new Set;
998
1083
  const server = createServer((conn) => {
@@ -1125,7 +1210,7 @@ var exports_bridge = {};
1125
1210
  __export(exports_bridge, {
1126
1211
  startBridge: () => startBridge
1127
1212
  });
1128
- import { writeFile } from "fs/promises";
1213
+ import { writeFile as writeFile2 } from "fs/promises";
1129
1214
  import { homedir as homedir5 } from "os";
1130
1215
  import { join as join3 } from "path";
1131
1216
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -1145,7 +1230,7 @@ async function startBridge(orch, opts = {}) {
1145
1230
  }
1146
1231
  }
1147
1232
  };
1148
- await writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
1233
+ await writeFile2(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
1149
1234
  process.env.KOBE_MCP_CONFIG = mcpConfigPath;
1150
1235
  return {
1151
1236
  socketPath,
@@ -2421,6 +2506,86 @@ var init_claude_settings = __esm(() => {
2421
2506
  SETTINGS_PATH = join4(homedir6(), ".claude", "settings.json");
2422
2507
  });
2423
2508
 
2509
+ // src/session/usage-metrics.ts
2510
+ function totalContextTokens(u) {
2511
+ return u.input_tokens + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
2512
+ }
2513
+ function parseTimestampMs(value) {
2514
+ const ms = new Date(value).getTime();
2515
+ return Number.isFinite(ms) ? ms : null;
2516
+ }
2517
+ function mergeIntervals(intervals) {
2518
+ if (intervals.length === 0)
2519
+ return [];
2520
+ const sorted = [...intervals].sort((a, b) => a.startMs - b.startMs);
2521
+ const first = sorted[0];
2522
+ if (!first)
2523
+ return [];
2524
+ const merged = [{ startMs: first.startMs, endMs: first.endMs }];
2525
+ for (let i = 1;i < sorted.length; i++) {
2526
+ const current = sorted[i];
2527
+ const last = merged[merged.length - 1];
2528
+ if (!current || !last)
2529
+ continue;
2530
+ if (current.startMs <= last.endMs) {
2531
+ last.endMs = Math.max(last.endMs, current.endMs);
2532
+ } else {
2533
+ merged.push({ startMs: current.startMs, endMs: current.endMs });
2534
+ }
2535
+ }
2536
+ return merged;
2537
+ }
2538
+ function durationMs(intervals) {
2539
+ return intervals.reduce((total, interval) => total + (interval.endMs - interval.startMs), 0);
2540
+ }
2541
+ function deriveSessionUsageMetrics(past) {
2542
+ let latestUsage;
2543
+ let latestUsageTimestampMs = null;
2544
+ let lastUserTimestampMs = null;
2545
+ let inputTokens = 0;
2546
+ let outputTokens = 0;
2547
+ const intervals = [];
2548
+ for (const message of past) {
2549
+ const timestampMs = parseTimestampMs(message.timestamp);
2550
+ if (message.role === "user" && timestampMs !== null) {
2551
+ lastUserTimestampMs = timestampMs;
2552
+ continue;
2553
+ }
2554
+ if (message.role !== "assistant" || !message.usage)
2555
+ continue;
2556
+ if (timestampMs !== null && (latestUsageTimestampMs === null || timestampMs > latestUsageTimestampMs)) {
2557
+ latestUsageTimestampMs = timestampMs;
2558
+ latestUsage = message.usage;
2559
+ } else if (latestUsage === undefined) {
2560
+ latestUsage = message.usage;
2561
+ }
2562
+ inputTokens += message.usage.input_tokens;
2563
+ outputTokens += message.usage.output_tokens;
2564
+ if (timestampMs !== null && lastUserTimestampMs !== null && timestampMs > lastUserTimestampMs) {
2565
+ intervals.push({ startMs: lastUserTimestampMs, endMs: timestampMs });
2566
+ }
2567
+ }
2568
+ if (!latestUsage)
2569
+ return;
2570
+ const totalDurationMs = durationMs(mergeIntervals(intervals));
2571
+ if (totalDurationMs <= 0)
2572
+ return latestUsage;
2573
+ return {
2574
+ ...latestUsage,
2575
+ total_speed_tokens_per_second: (inputTokens + outputTokens) / (totalDurationMs / 1000)
2576
+ };
2577
+ }
2578
+ function withTotalSpeedForTurn(usage, startedAtIso, endedAtIso) {
2579
+ const startMs = startedAtIso ? parseTimestampMs(startedAtIso) : null;
2580
+ const endMs = parseTimestampMs(endedAtIso);
2581
+ if (startMs === null || endMs === null || endMs <= startMs)
2582
+ return usage;
2583
+ return {
2584
+ ...usage,
2585
+ total_speed_tokens_per_second: (usage.input_tokens + usage.output_tokens) / ((endMs - startMs) / 1000)
2586
+ };
2587
+ }
2588
+
2424
2589
  // src/env.ts
2425
2590
  import { homedir as homedir7 } from "os";
2426
2591
  import { join as join5 } from "path";
@@ -3034,6 +3199,7 @@ class Orchestrator {
3034
3199
  worktrees;
3035
3200
  metadataSuggester;
3036
3201
  handles = new Map;
3202
+ firstSpawnLatches = new Map;
3037
3203
  subscribers = new Map;
3038
3204
  pumps = new Map;
3039
3205
  pendingInputBroker = new InMemoryPendingInputBroker;
@@ -3266,8 +3432,16 @@ class Orchestrator {
3266
3432
  const renameTabId = this.resolveTab(task, tabId).id;
3267
3433
  this.maybeRenameTempBranch(task.id, renameTabId, prompt);
3268
3434
  }
3269
- const targetTab = this.resolveTab(task, tabId);
3435
+ let targetTab = this.resolveTab(task, tabId);
3270
3436
  const key = tabKey(task.id, targetTab.id);
3437
+ if (!targetTab.sessionId) {
3438
+ const inflight = this.firstSpawnLatches.get(key);
3439
+ if (inflight) {
3440
+ await inflight.catch(() => {});
3441
+ task = this.requireTask(id);
3442
+ targetTab = this.resolveTab(task, tabId);
3443
+ }
3444
+ }
3271
3445
  if (this.handles.has(key) === false) {
3272
3446
  const running = this.countRunning();
3273
3447
  if (running >= CONCURRENCY_CAP) {
@@ -3288,18 +3462,28 @@ class Orchestrator {
3288
3462
  model: modelToUse
3289
3463
  });
3290
3464
  } else {
3291
- handle = await this.engine.spawn(task.worktreePath, promptToSend, {
3292
- permissionMode: task.permissionMode,
3293
- model: modelToUse
3465
+ let releaseLatch = () => {};
3466
+ const latch = new Promise((resolve2) => {
3467
+ releaseLatch = resolve2;
3294
3468
  });
3295
- await this.updateTab(task.id, targetTab.id, { sessionId: handle.sessionId });
3296
- if (task.title === PLACEHOLDER_TASK_TITLE && prompt && prompt.trim().length > 0) {
3297
- const derived = deriveTitleFromPrompt(prompt);
3298
- if (derived)
3299
- await this.store.update(task.id, { title: derived });
3300
- }
3301
- if (prompt && prompt.trim().length > 0) {
3302
- this.maybeUpgradeTitle(task.id, prompt);
3469
+ this.firstSpawnLatches.set(key, latch);
3470
+ try {
3471
+ handle = await this.engine.spawn(task.worktreePath, promptToSend, {
3472
+ permissionMode: task.permissionMode,
3473
+ model: modelToUse
3474
+ });
3475
+ await this.updateTab(task.id, targetTab.id, { sessionId: handle.sessionId });
3476
+ if (task.title === PLACEHOLDER_TASK_TITLE && prompt && prompt.trim().length > 0) {
3477
+ const derived = deriveTitleFromPrompt(prompt);
3478
+ if (derived)
3479
+ await this.store.update(task.id, { title: derived });
3480
+ }
3481
+ if (prompt && prompt.trim().length > 0) {
3482
+ this.maybeUpgradeTitle(task.id, prompt);
3483
+ }
3484
+ } finally {
3485
+ releaseLatch();
3486
+ this.firstSpawnLatches.delete(key);
3303
3487
  }
3304
3488
  }
3305
3489
  this.handles.set(key, handle);
@@ -3366,6 +3550,12 @@ class Orchestrator {
3366
3550
  }
3367
3551
  this.dispatchEvent(task.id, targetTab.id, { type: "done" });
3368
3552
  }
3553
+ async steerTask(id, prompt, tabId) {
3554
+ const task = this.requireTask(id);
3555
+ const targetTab = this.resolveTab(task, tabId);
3556
+ await this.interruptTask(task.id, targetTab.id);
3557
+ await this.runTask(task.id, prompt, targetTab.id);
3558
+ }
3369
3559
  async pauseTask(id) {
3370
3560
  const task = this.requireTask(id);
3371
3561
  if (task.status !== "in_progress") {
@@ -3478,6 +3668,14 @@ class Orchestrator {
3478
3668
  return [];
3479
3669
  }
3480
3670
  }
3671
+ async readHistoryWithMetrics(sessionId) {
3672
+ const messages = await this.readHistory(sessionId);
3673
+ const usageMetrics = deriveSessionUsageMetrics(messages);
3674
+ return {
3675
+ messages,
3676
+ ...usageMetrics ? { usageMetrics } : {}
3677
+ };
3678
+ }
3481
3679
  async listSessions(id) {
3482
3680
  const task = this.requireTask(id);
3483
3681
  if (!task.worktreePath)
@@ -3796,7 +3994,7 @@ var init_core = __esm(() => {
3796
3994
  });
3797
3995
 
3798
3996
  // src/orchestrator/index/store.ts
3799
- import { mkdir as mkdir2, open, readFile as readFile3, rename, unlink as unlink3 } from "fs/promises";
3997
+ import { mkdir as mkdir3, open, readFile as readFile3, rename, unlink as unlink3 } from "fs/promises";
3800
3998
  import { homedir as homedir8 } from "os";
3801
3999
  import { dirname as dirname4, join as join6 } from "path";
3802
4000
 
@@ -3870,7 +4068,7 @@ class TaskIndexStore {
3870
4068
  return next;
3871
4069
  }
3872
4070
  async doSave() {
3873
- await mkdir2(dirname4(this.path), { recursive: true });
4071
+ await mkdir3(dirname4(this.path), { recursive: true });
3874
4072
  const payload = this.snapshot();
3875
4073
  const json = `${JSON.stringify(payload, null, 2)}
3876
4074
  `;
@@ -4421,7 +4619,7 @@ init_paths();
4421
4619
  // src/daemon/server.ts
4422
4620
  init_repos();
4423
4621
  init_paths();
4424
- import { mkdir as mkdir3, readFile as readFile5, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
4622
+ import { mkdir as mkdir4, readFile as readFile5, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
4425
4623
  import { createServer as createServer2 } from "net";
4426
4624
  import { dirname as dirname5 } from "path";
4427
4625
 
@@ -4761,8 +4959,8 @@ async function startDaemonServer(orch, options = {}) {
4761
4959
  const startedAt = options.startedAt ?? new Date;
4762
4960
  const clients = new Set;
4763
4961
  let nextClientId = 1;
4764
- await mkdir3(dirname5(socketPath), { recursive: true });
4765
- await mkdir3(dirname5(pidPath), { recursive: true });
4962
+ await mkdir4(dirname5(socketPath), { recursive: true });
4963
+ await mkdir4(dirname5(pidPath), { recursive: true });
4766
4964
  await unlink4(socketPath).catch(() => {});
4767
4965
  const server = createServer2((socket) => {
4768
4966
  const client = {
@@ -4820,7 +5018,7 @@ async function startDaemonServer(orch, options = {}) {
4820
5018
  resolve2();
4821
5019
  });
4822
5020
  });
4823
- await writeFile3(pidPath, `${process.pid}
5021
+ await writeFile4(pidPath, `${process.pid}
4824
5022
  `, "utf8");
4825
5023
  async function stopSoon() {
4826
5024
  await options.onStop?.();
@@ -4990,6 +5188,10 @@ async function startDaemonServer(orch, options = {}) {
4990
5188
  await orch.interruptTask(requireString2(payload, "taskId"), optionalString2(payload, "tabId"));
4991
5189
  return {};
4992
5190
  }
5191
+ case "chat.steer": {
5192
+ await orch.steerTask(requireString2(payload, "taskId"), requireString2(payload, "text"), optionalString2(payload, "tabId"));
5193
+ return {};
5194
+ }
4993
5195
  case "chat.input.pending": {
4994
5196
  return { pending: orch.peekPendingInput(requireString2(payload, "taskId")) };
4995
5197
  }
@@ -5009,6 +5211,7 @@ async function startDaemonServer(orch, options = {}) {
5009
5211
  const result = await readTaskHistory(orch, taskId, sessionId, limit, before);
5010
5212
  return {
5011
5213
  messages: serializeMessages(result.messages),
5214
+ ...result.usageMetrics ? { usageMetrics: result.usageMetrics } : {},
5012
5215
  nextBefore: result.nextBefore,
5013
5216
  hasMore: result.hasMore
5014
5217
  };
@@ -5156,6 +5359,7 @@ async function readTaskHistory(orch, taskId, requestedSessionId, limit, before)
5156
5359
  if (!sessionId)
5157
5360
  return { messages: [], nextBefore: null, hasMore: false };
5158
5361
  const messages = await orch.readHistory(sessionId);
5362
+ const usageMetrics = deriveSessionUsageMetrics(messages);
5159
5363
  const beforeIdx = before ? messages.findIndex((m) => `${m.timestamp}:${m.sessionId}` === before) : -1;
5160
5364
  const end = beforeIdx >= 0 ? beforeIdx : messages.length;
5161
5365
  const start = Math.max(0, end - limit);
@@ -5163,7 +5367,7 @@ async function readTaskHistory(orch, taskId, requestedSessionId, limit, before)
5163
5367
  const hasMore = start > 0;
5164
5368
  const first = page[0];
5165
5369
  const nextBefore = hasMore && first ? `${first.timestamp}:${first.sessionId}` : null;
5166
- return { messages: page, nextBefore, hasMore };
5370
+ return { messages: page, ...usageMetrics ? { usageMetrics } : {}, nextBefore, hasMore };
5167
5371
  }
5168
5372
  function writeFrame(client, frame) {
5169
5373
  client.socket.write(frameToLine(frame));