@sma1lboy/kobe 0.5.9 → 0.5.11

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/README.md CHANGED
@@ -68,6 +68,7 @@ Once you're in, the keys you'll use most:
68
68
  | `ctrl+1` / `2` / `3` / `4` | Jump straight to a pane (sidebar, workspace, files, terminal) |
69
69
  | `tab` | Cycle focus to the next pane |
70
70
  | `ctrl+q` | Detach back to the sidebar (your task keeps streaming) |
71
+ | `ctrl+o` | Open the active task's worktree in your editor |
71
72
  | `?` | Show the full keybinding help dialog |
72
73
  | `,` or `ctrl+,` | Open Settings (theme, transparent background, dev reset) |
73
74
  | `q` | Quit (with confirm) |
@@ -87,6 +88,29 @@ Inside the chat composer:
87
88
  A given task can host **multiple chat tabs** on the same worktree — useful when
88
89
  you want a parallel sub-conversation without losing the main thread.
89
90
 
91
+ ## Opening tasks in your editor
92
+
93
+ The top bar shows an `[Open] <editor>` chip when kobe can find an editor for the
94
+ active task. Click it, use `ctrl+o`, or run **Open task in editor** from the
95
+ command palette to open the task's worktree.
96
+
97
+ Detection order is:
98
+
99
+ 1. `KOBE_OPEN_EDITOR`
100
+ 2. `code` (VS Code)
101
+ 3. `cursor`
102
+ 4. `windsurf`
103
+ 5. `zed`
104
+ 6. platform fallback (`open` on macOS, `xdg-open` on Linux)
105
+
106
+ Set `KOBE_OPEN_EDITOR` globally if you want to force a specific tool:
107
+
108
+ ```bash
109
+ export KOBE_OPEN_EDITOR=cursor
110
+ export KOBE_OPEN_EDITOR=code
111
+ export KOBE_OPEN_EDITOR=/Applications/Cursor.app/Contents/Resources/app/bin/cursor
112
+ ```
113
+
90
114
  For the full feature manifest, see [`CHANGELOG.md`](./CHANGELOG.md).
91
115
 
92
116
  ## Custom themes
@@ -152,8 +176,8 @@ If you want to hack on kobe itself rather than just use it:
152
176
  ```bash
153
177
  bun install
154
178
  bun run dev # boots the 5-pane TUI under KOBE_DEV=1 (no update chip, etc.)
155
- bun run test # unit + type tests
156
- bun run test:behavior # PTY-driven; spawns kobe as a real binary
179
+ bun run test # normal suite: fast tests + serial socket tests
180
+ bun run test:behavior # slow PTY suite; only run for user-visible TUI behavior
157
181
  bun run typecheck # strict tsc
158
182
  bun run build # produces ./dist/index.js for `npm publish`
159
183
  ```
package/dist/bin/kobed.js CHANGED
@@ -1208,18 +1208,29 @@ var init_server = () => {};
1208
1208
  // src/orchestrator/bridge/index.ts
1209
1209
  var exports_bridge = {};
1210
1210
  __export(exports_bridge, {
1211
- startBridge: () => startBridge
1211
+ startBridge: () => startBridge,
1212
+ bridgeSocketPathForHome: () => bridgeSocketPathForHome
1212
1213
  });
1213
- import { writeFile as writeFile2 } from "fs/promises";
1214
- import { homedir as homedir5 } from "os";
1214
+ import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
1215
+ import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
1215
1216
  import { join as join3 } from "path";
1216
1217
  import { fileURLToPath as fileURLToPath2 } from "url";
1218
+ function bridgeSocketPathForHome(home, pid = process.pid) {
1219
+ const runDir = join3(home, ".kobe", "run");
1220
+ const preferred = join3(runDir, `bridge-${pid}.sock`);
1221
+ const macTempSocket = process.platform === "darwin" && preferred.startsWith(tmpdir2());
1222
+ if (preferred.length <= UNIX_SOCKET_PATH_LIMIT && !macTempSocket)
1223
+ return preferred;
1224
+ const shortTmp = process.platform === "darwin" ? "/tmp" : tmpdir2();
1225
+ return join3(shortTmp, `kobe-bridge-${pid}.sock`);
1226
+ }
1217
1227
  async function startBridge(orch, opts = {}) {
1218
1228
  const home = opts.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir5();
1219
1229
  const runDir = join3(home, ".kobe", "run");
1220
- const socketPath = join3(runDir, `bridge-${process.pid}.sock`);
1230
+ const socketPath = bridgeSocketPathForHome(home);
1221
1231
  const mcpConfigPath = join3(runDir, `mcp-${process.pid}.json`);
1222
1232
  const server = await startBridgeServer(orch, socketPath);
1233
+ await mkdir3(runDir, { recursive: true });
1223
1234
  const moduleExt = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
1224
1235
  const entry = fileURLToPath2(new URL(`../../cli/index${moduleExt}`, import.meta.url));
1225
1236
  const mcpConfig = {
@@ -1240,6 +1251,7 @@ async function startBridge(orch, opts = {}) {
1240
1251
  }
1241
1252
  };
1242
1253
  }
1254
+ var UNIX_SOCKET_PATH_LIMIT = 103;
1243
1255
  var init_bridge = __esm(() => {
1244
1256
  init_server();
1245
1257
  });
@@ -2506,6 +2518,86 @@ var init_claude_settings = __esm(() => {
2506
2518
  SETTINGS_PATH = join4(homedir6(), ".claude", "settings.json");
2507
2519
  });
2508
2520
 
2521
+ // src/session/usage-metrics.ts
2522
+ function totalContextTokens(u) {
2523
+ return u.input_tokens + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
2524
+ }
2525
+ function parseTimestampMs(value) {
2526
+ const ms = new Date(value).getTime();
2527
+ return Number.isFinite(ms) ? ms : null;
2528
+ }
2529
+ function mergeIntervals(intervals) {
2530
+ if (intervals.length === 0)
2531
+ return [];
2532
+ const sorted = [...intervals].sort((a, b) => a.startMs - b.startMs);
2533
+ const first = sorted[0];
2534
+ if (!first)
2535
+ return [];
2536
+ const merged = [{ startMs: first.startMs, endMs: first.endMs }];
2537
+ for (let i = 1;i < sorted.length; i++) {
2538
+ const current = sorted[i];
2539
+ const last = merged[merged.length - 1];
2540
+ if (!current || !last)
2541
+ continue;
2542
+ if (current.startMs <= last.endMs) {
2543
+ last.endMs = Math.max(last.endMs, current.endMs);
2544
+ } else {
2545
+ merged.push({ startMs: current.startMs, endMs: current.endMs });
2546
+ }
2547
+ }
2548
+ return merged;
2549
+ }
2550
+ function durationMs(intervals) {
2551
+ return intervals.reduce((total, interval) => total + (interval.endMs - interval.startMs), 0);
2552
+ }
2553
+ function deriveSessionUsageMetrics(past) {
2554
+ let latestUsage;
2555
+ let latestUsageTimestampMs = null;
2556
+ let lastUserTimestampMs = null;
2557
+ let inputTokens = 0;
2558
+ let outputTokens = 0;
2559
+ const intervals = [];
2560
+ for (const message of past) {
2561
+ const timestampMs = parseTimestampMs(message.timestamp);
2562
+ if (message.role === "user" && timestampMs !== null) {
2563
+ lastUserTimestampMs = timestampMs;
2564
+ continue;
2565
+ }
2566
+ if (message.role !== "assistant" || !message.usage)
2567
+ continue;
2568
+ if (timestampMs !== null && (latestUsageTimestampMs === null || timestampMs > latestUsageTimestampMs)) {
2569
+ latestUsageTimestampMs = timestampMs;
2570
+ latestUsage = message.usage;
2571
+ } else if (latestUsage === undefined) {
2572
+ latestUsage = message.usage;
2573
+ }
2574
+ inputTokens += message.usage.input_tokens;
2575
+ outputTokens += message.usage.output_tokens;
2576
+ if (timestampMs !== null && lastUserTimestampMs !== null && timestampMs > lastUserTimestampMs) {
2577
+ intervals.push({ startMs: lastUserTimestampMs, endMs: timestampMs });
2578
+ }
2579
+ }
2580
+ if (!latestUsage)
2581
+ return;
2582
+ const totalDurationMs = durationMs(mergeIntervals(intervals));
2583
+ if (totalDurationMs <= 0)
2584
+ return latestUsage;
2585
+ return {
2586
+ ...latestUsage,
2587
+ total_speed_tokens_per_second: (inputTokens + outputTokens) / (totalDurationMs / 1000)
2588
+ };
2589
+ }
2590
+ function withTotalSpeedForTurn(usage, startedAtIso, endedAtIso) {
2591
+ const startMs = startedAtIso ? parseTimestampMs(startedAtIso) : null;
2592
+ const endMs = parseTimestampMs(endedAtIso);
2593
+ if (startMs === null || endMs === null || endMs <= startMs)
2594
+ return usage;
2595
+ return {
2596
+ ...usage,
2597
+ total_speed_tokens_per_second: (usage.input_tokens + usage.output_tokens) / ((endMs - startMs) / 1000)
2598
+ };
2599
+ }
2600
+
2509
2601
  // src/env.ts
2510
2602
  import { homedir as homedir7 } from "os";
2511
2603
  import { join as join5 } from "path";
@@ -3588,6 +3680,14 @@ class Orchestrator {
3588
3680
  return [];
3589
3681
  }
3590
3682
  }
3683
+ async readHistoryWithMetrics(sessionId) {
3684
+ const messages = await this.readHistory(sessionId);
3685
+ const usageMetrics = deriveSessionUsageMetrics(messages);
3686
+ return {
3687
+ messages,
3688
+ ...usageMetrics ? { usageMetrics } : {}
3689
+ };
3690
+ }
3591
3691
  async listSessions(id) {
3592
3692
  const task = this.requireTask(id);
3593
3693
  if (!task.worktreePath)
@@ -3906,7 +4006,7 @@ var init_core = __esm(() => {
3906
4006
  });
3907
4007
 
3908
4008
  // src/orchestrator/index/store.ts
3909
- import { mkdir as mkdir3, open, readFile as readFile3, rename, unlink as unlink3 } from "fs/promises";
4009
+ import { mkdir as mkdir4, open, readFile as readFile3, rename, unlink as unlink3 } from "fs/promises";
3910
4010
  import { homedir as homedir8 } from "os";
3911
4011
  import { dirname as dirname4, join as join6 } from "path";
3912
4012
 
@@ -3980,7 +4080,7 @@ class TaskIndexStore {
3980
4080
  return next;
3981
4081
  }
3982
4082
  async doSave() {
3983
- await mkdir3(dirname4(this.path), { recursive: true });
4083
+ await mkdir4(dirname4(this.path), { recursive: true });
3984
4084
  const payload = this.snapshot();
3985
4085
  const json = `${JSON.stringify(payload, null, 2)}
3986
4086
  `;
@@ -4531,7 +4631,7 @@ init_paths();
4531
4631
  // src/daemon/server.ts
4532
4632
  init_repos();
4533
4633
  init_paths();
4534
- import { mkdir as mkdir4, readFile as readFile5, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
4634
+ import { mkdir as mkdir5, readFile as readFile5, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
4535
4635
  import { createServer as createServer2 } from "net";
4536
4636
  import { dirname as dirname5 } from "path";
4537
4637
 
@@ -4871,8 +4971,8 @@ async function startDaemonServer(orch, options = {}) {
4871
4971
  const startedAt = options.startedAt ?? new Date;
4872
4972
  const clients = new Set;
4873
4973
  let nextClientId = 1;
4874
- await mkdir4(dirname5(socketPath), { recursive: true });
4875
- await mkdir4(dirname5(pidPath), { recursive: true });
4974
+ await mkdir5(dirname5(socketPath), { recursive: true });
4975
+ await mkdir5(dirname5(pidPath), { recursive: true });
4876
4976
  await unlink4(socketPath).catch(() => {});
4877
4977
  const server = createServer2((socket) => {
4878
4978
  const client = {
@@ -5123,6 +5223,7 @@ async function startDaemonServer(orch, options = {}) {
5123
5223
  const result = await readTaskHistory(orch, taskId, sessionId, limit, before);
5124
5224
  return {
5125
5225
  messages: serializeMessages(result.messages),
5226
+ ...result.usageMetrics ? { usageMetrics: result.usageMetrics } : {},
5126
5227
  nextBefore: result.nextBefore,
5127
5228
  hasMore: result.hasMore
5128
5229
  };
@@ -5270,6 +5371,7 @@ async function readTaskHistory(orch, taskId, requestedSessionId, limit, before)
5270
5371
  if (!sessionId)
5271
5372
  return { messages: [], nextBefore: null, hasMore: false };
5272
5373
  const messages = await orch.readHistory(sessionId);
5374
+ const usageMetrics = deriveSessionUsageMetrics(messages);
5273
5375
  const beforeIdx = before ? messages.findIndex((m) => `${m.timestamp}:${m.sessionId}` === before) : -1;
5274
5376
  const end = beforeIdx >= 0 ? beforeIdx : messages.length;
5275
5377
  const start = Math.max(0, end - limit);
@@ -5277,7 +5379,7 @@ async function readTaskHistory(orch, taskId, requestedSessionId, limit, before)
5277
5379
  const hasMore = start > 0;
5278
5380
  const first = page[0];
5279
5381
  const nextBefore = hasMore && first ? `${first.timestamp}:${first.sessionId}` : null;
5280
- return { messages: page, nextBefore, hasMore };
5382
+ return { messages: page, ...usageMetrics ? { usageMetrics } : {}, nextBefore, hasMore };
5281
5383
  }
5282
5384
  function writeFrame(client, frame) {
5283
5385
  client.socket.write(frameToLine(frame));