@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 +26 -2
- package/dist/bin/kobed.js +112 -10
- package/dist/cli/index.js +630 -277
- package/package.json +5 -3
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 #
|
|
156
|
-
bun run test:behavior # PTY
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4875
|
-
await
|
|
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));
|