@sma1lboy/kobe 0.5.0 → 0.5.1

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
@@ -353,7 +353,24 @@ function extractMessage(record, fallbackSessionId) {
353
353
  const content = inner.content;
354
354
  const ts = typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString();
355
355
  const sid = typeof record.sessionId === "string" ? record.sessionId : fallbackSessionId;
356
- return { role, content, timestamp: ts, sessionId: sid };
356
+ const usage = extractUsage(inner.usage);
357
+ return usage ? { role, content, timestamp: ts, sessionId: sid, usage } : { role, content, timestamp: ts, sessionId: sid };
358
+ }
359
+ function extractUsage(v) {
360
+ if (!isObject(v))
361
+ return;
362
+ const inTok = typeof v.input_tokens === "number" ? v.input_tokens : undefined;
363
+ const outTok = typeof v.output_tokens === "number" ? v.output_tokens : undefined;
364
+ if (inTok === undefined || outTok === undefined)
365
+ return;
366
+ const cacheRead = typeof v.cache_read_input_tokens === "number" ? v.cache_read_input_tokens : undefined;
367
+ const cacheCreate = typeof v.cache_creation_input_tokens === "number" ? v.cache_creation_input_tokens : undefined;
368
+ return {
369
+ input_tokens: inTok,
370
+ output_tokens: outTok,
371
+ ...cacheRead !== undefined ? { cache_read_input_tokens: cacheRead } : {},
372
+ ...cacheCreate !== undefined ? { cache_creation_input_tokens: cacheCreate } : {}
373
+ };
357
374
  }
358
375
  function isObject(v) {
359
376
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -2961,6 +2978,9 @@ class Orchestrator {
2961
2978
  tasksSignal() {
2962
2979
  return this.tasksAcc;
2963
2980
  }
2981
+ planUsageSignal() {
2982
+ return () => null;
2983
+ }
2964
2984
  subscribeTasks(listener) {
2965
2985
  return this.store.subscribe(listener);
2966
2986
  }
@@ -4294,9 +4314,166 @@ init_paths2();
4294
4314
 
4295
4315
  // src/daemon/server.ts
4296
4316
  init_paths2();
4297
- import { mkdir as mkdir3, readFile as readFile4, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
4317
+ import { mkdir as mkdir3, readFile as readFile5, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
4298
4318
  import { createServer as createServer2 } from "net";
4299
4319
  import { dirname as dirname4 } from "path";
4320
+
4321
+ // src/engine/claude-code-local/plan-usage.ts
4322
+ import { execFile } from "child_process";
4323
+ import { createHash } from "crypto";
4324
+ import { readFile as readFile4 } from "fs/promises";
4325
+ import { homedir as homedir10, userInfo } from "os";
4326
+ import { join as join6 } from "path";
4327
+ import { promisify } from "util";
4328
+ var execFileAsync = promisify(execFile);
4329
+ var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
4330
+ var FETCH_TIMEOUT_MS = 5000;
4331
+ var KEYCHAIN_BASE = "Claude Code";
4332
+ var KEYCHAIN_SUFFIX = "-credentials";
4333
+ function keychainServiceName() {
4334
+ const configDir = process.env.CLAUDE_CONFIG_DIR;
4335
+ if (!configDir)
4336
+ return `${KEYCHAIN_BASE}${KEYCHAIN_SUFFIX}`;
4337
+ const hash = createHash("sha256").update(configDir).digest("hex").slice(0, 8);
4338
+ return `${KEYCHAIN_BASE}${KEYCHAIN_SUFFIX}-${hash}`;
4339
+ }
4340
+ function keychainAccount() {
4341
+ return process.env.USER || userInfo().username || "claude-code-user";
4342
+ }
4343
+ async function readKeychainToken() {
4344
+ if (process.platform !== "darwin")
4345
+ return null;
4346
+ try {
4347
+ const { stdout } = await execFileAsync("security", [
4348
+ "find-generic-password",
4349
+ "-a",
4350
+ keychainAccount(),
4351
+ "-w",
4352
+ "-s",
4353
+ keychainServiceName()
4354
+ ]);
4355
+ return parseStoredOAuth(stdout);
4356
+ } catch {
4357
+ return null;
4358
+ }
4359
+ }
4360
+ async function readPlainTextToken() {
4361
+ const configDir = process.env.CLAUDE_CONFIG_DIR ?? join6(homedir10(), ".claude");
4362
+ const path7 = join6(configDir, ".credentials.json");
4363
+ try {
4364
+ const raw = await readFile4(path7, "utf8");
4365
+ return parseStoredOAuth(raw);
4366
+ } catch {
4367
+ return null;
4368
+ }
4369
+ }
4370
+ function parseStoredOAuth(raw) {
4371
+ const trimmed = raw.trim();
4372
+ if (!trimmed)
4373
+ return null;
4374
+ try {
4375
+ const parsed = JSON.parse(trimmed);
4376
+ const tok = parsed.claudeAiOauth;
4377
+ if (!tok || typeof tok.accessToken !== "string" || tok.accessToken.length === 0)
4378
+ return null;
4379
+ return tok;
4380
+ } catch {
4381
+ return null;
4382
+ }
4383
+ }
4384
+ async function loadToken() {
4385
+ return await readKeychainToken() ?? await readPlainTextToken();
4386
+ }
4387
+ function normalizeBucket(b) {
4388
+ if (!b)
4389
+ return null;
4390
+ return {
4391
+ utilization: typeof b.utilization === "number" && Number.isFinite(b.utilization) ? b.utilization : null,
4392
+ resetsAt: typeof b.resets_at === "string" && b.resets_at.length > 0 ? b.resets_at : null
4393
+ };
4394
+ }
4395
+ async function fetchPlanUsage(now = Date.now()) {
4396
+ const token = await loadToken();
4397
+ if (!token)
4398
+ return null;
4399
+ if (typeof token.expiresAt === "number" && token.expiresAt > 0 && token.expiresAt < now)
4400
+ return null;
4401
+ const ctrl = new AbortController;
4402
+ const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
4403
+ try {
4404
+ const res = await fetch(USAGE_URL, {
4405
+ signal: ctrl.signal,
4406
+ headers: {
4407
+ accept: "application/json",
4408
+ "content-type": "application/json",
4409
+ "user-agent": "kobe-plan-usage",
4410
+ "anthropic-beta": "oauth-2025-04-20",
4411
+ authorization: `Bearer ${token.accessToken}`
4412
+ }
4413
+ });
4414
+ if (!res.ok)
4415
+ return null;
4416
+ const body = await res.json();
4417
+ return {
4418
+ fiveHour: normalizeBucket(body.five_hour),
4419
+ sevenDay: normalizeBucket(body.seven_day),
4420
+ sevenDayOpus: normalizeBucket(body.seven_day_opus),
4421
+ sevenDaySonnet: normalizeBucket(body.seven_day_sonnet),
4422
+ fetchedAt: new Date(now).toISOString()
4423
+ };
4424
+ } catch {
4425
+ return null;
4426
+ } finally {
4427
+ clearTimeout(timer);
4428
+ }
4429
+ }
4430
+
4431
+ // src/daemon/plan-usage-poller.ts
4432
+ var DEFAULT_INTERVAL_MS = 60000;
4433
+ function createPlanUsagePoller(options) {
4434
+ const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
4435
+ const fetcher = options.fetcher ?? fetchPlanUsage;
4436
+ let timer = null;
4437
+ let last = null;
4438
+ let inflight = false;
4439
+ async function tick() {
4440
+ if (inflight)
4441
+ return;
4442
+ inflight = true;
4443
+ try {
4444
+ const usage = await fetcher();
4445
+ if (usage) {
4446
+ last = usage;
4447
+ options.onUpdate(usage);
4448
+ }
4449
+ } finally {
4450
+ inflight = false;
4451
+ }
4452
+ }
4453
+ return {
4454
+ start() {
4455
+ if (timer)
4456
+ return;
4457
+ tick();
4458
+ timer = setInterval(() => void tick(), intervalMs);
4459
+ timer.unref?.();
4460
+ },
4461
+ stop() {
4462
+ if (timer) {
4463
+ clearInterval(timer);
4464
+ timer = null;
4465
+ }
4466
+ },
4467
+ current() {
4468
+ return last;
4469
+ },
4470
+ async refresh() {
4471
+ await tick();
4472
+ }
4473
+ };
4474
+ }
4475
+
4476
+ // src/daemon/server.ts
4300
4477
  async function startDaemonServer(orch, options = {}) {
4301
4478
  const socketPath = options.socketPath ?? defaultDaemonSocketPath(options.homeDir);
4302
4479
  const pidPath = options.pidPath ?? defaultDaemonPidPath(options.homeDir);
@@ -4327,12 +4504,16 @@ async function startDaemonServer(orch, options = {}) {
4327
4504
  clients.delete(client);
4328
4505
  });
4329
4506
  });
4507
+ const planUsagePoller = options.planUsagePoller ?? createPlanUsagePoller({
4508
+ onUpdate: (usage) => broadcast(clients, { type: "event", name: "plan.usage", payload: { usage } })
4509
+ });
4330
4510
  const serverApi = {
4331
4511
  socketPath,
4332
4512
  pidPath,
4333
4513
  startedAt,
4334
4514
  clients,
4335
4515
  async close() {
4516
+ planUsagePoller.stop();
4336
4517
  broadcast(clients, { type: "event", name: "daemon.stopping", payload: {} });
4337
4518
  await new Promise((resolve) => server.close(() => resolve()));
4338
4519
  for (const client of Array.from(clients)) {
@@ -4345,6 +4526,7 @@ async function startDaemonServer(orch, options = {}) {
4345
4526
  await unlink4(pidPath).catch(() => {});
4346
4527
  }
4347
4528
  };
4529
+ planUsagePoller.start();
4348
4530
  await new Promise((resolve, reject) => {
4349
4531
  server.once("error", reject);
4350
4532
  server.listen(socketPath, () => {
@@ -4380,7 +4562,8 @@ async function startDaemonServer(orch, options = {}) {
4380
4562
  clientId: client.id,
4381
4563
  tasks: tasks.map(serializeTask),
4382
4564
  pending,
4383
- runState
4565
+ runState,
4566
+ planUsage: planUsagePoller.current()
4384
4567
  };
4385
4568
  }
4386
4569
  case "daemon.status":
@@ -4606,7 +4789,7 @@ async function startDaemonServer(orch, options = {}) {
4606
4789
  }
4607
4790
  async function readPidFile(pidPath) {
4608
4791
  try {
4609
- const raw = await readFile4(pidPath, "utf8");
4792
+ const raw = await readFile5(pidPath, "utf8");
4610
4793
  const pid = Number(raw.trim());
4611
4794
  return Number.isFinite(pid) ? pid : null;
4612
4795
  } catch {
package/dist/cli/index.js CHANGED
@@ -8825,7 +8825,7 @@ var init_package = __esm(() => {
8825
8825
  package_default = {
8826
8826
  $schema: "https://json.schemastore.org/package.json",
8827
8827
  name: "@sma1lboy/kobe",
8828
- version: "0.5.0",
8828
+ version: "0.5.1",
8829
8829
  description: "TUI orchestrator for Claude Code (codename)",
8830
8830
  type: "module",
8831
8831
  packageManager: "bun@1.3.13",
@@ -10584,6 +10584,9 @@ class Orchestrator {
10584
10584
  tasksSignal() {
10585
10585
  return this.tasksAcc;
10586
10586
  }
10587
+ planUsageSignal() {
10588
+ return () => null;
10589
+ }
10587
10590
  subscribeTasks(listener2) {
10588
10591
  return this.store.subscribe(listener2);
10589
10592
  }
@@ -11287,16 +11290,21 @@ class RemoteOrchestrator {
11287
11290
  setTasks;
11288
11291
  runStateAcc;
11289
11292
  setRunState;
11293
+ planUsageAcc;
11294
+ setPlanUsage;
11290
11295
  subscribers = new Map;
11291
11296
  pendingInputBroker = new InMemoryPendingInputBroker;
11292
11297
  constructor(client) {
11293
11298
  this.client = client;
11294
11299
  const [tasks, setTasks] = createSignal([]);
11295
11300
  const [runState, setRunState] = createSignal(new Map);
11301
+ const [planUsage, setPlanUsage] = createSignal(null);
11296
11302
  this.tasksAcc = tasks;
11297
11303
  this.setTasks = (next) => setTasks(() => next);
11298
11304
  this.runStateAcc = runState;
11299
11305
  this.setRunState = (next) => setRunState(() => next);
11306
+ this.planUsageAcc = planUsage;
11307
+ this.setPlanUsage = (next) => setPlanUsage(() => next);
11300
11308
  this.client.on("*", (frame) => this.handleEvent(frame.name, frame.payload));
11301
11309
  }
11302
11310
  async init() {
@@ -11316,6 +11324,8 @@ class RemoteOrchestrator {
11316
11324
  if (seed.size > 0)
11317
11325
  this.setRunState(seed);
11318
11326
  }
11327
+ if (hello.planUsage)
11328
+ this.setPlanUsage(hello.planUsage);
11319
11329
  await this.client.request("subscribe", { taskIds: "all" });
11320
11330
  if (hello.pending) {
11321
11331
  for (const [taskId, entries] of Object.entries(hello.pending)) {
@@ -11345,6 +11355,9 @@ class RemoteOrchestrator {
11345
11355
  chatRunStateSignal() {
11346
11356
  return this.runStateAcc;
11347
11357
  }
11358
+ planUsageSignal() {
11359
+ return this.planUsageAcc;
11360
+ }
11348
11361
  listTasks() {
11349
11362
  return this.tasksAcc().slice();
11350
11363
  }
@@ -11477,6 +11490,11 @@ class RemoteOrchestrator {
11477
11490
  }
11478
11491
  return;
11479
11492
  }
11493
+ if (name === "plan.usage") {
11494
+ const usage = obj.usage;
11495
+ this.setPlanUsage(usage ?? null);
11496
+ return;
11497
+ }
11480
11498
  const taskId = obj.taskId;
11481
11499
  const tabId = obj.tabId;
11482
11500
  if (!taskId || !tabId)
@@ -14704,7 +14722,24 @@ function extractMessage(record, fallbackSessionId) {
14704
14722
  const content = inner.content;
14705
14723
  const ts = typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString();
14706
14724
  const sid = typeof record.sessionId === "string" ? record.sessionId : fallbackSessionId;
14707
- return { role, content, timestamp: ts, sessionId: sid };
14725
+ const usage = extractUsage(inner.usage);
14726
+ return usage ? { role, content, timestamp: ts, sessionId: sid, usage } : { role, content, timestamp: ts, sessionId: sid };
14727
+ }
14728
+ function extractUsage(v) {
14729
+ if (!isObject(v))
14730
+ return;
14731
+ const inTok = typeof v.input_tokens === "number" ? v.input_tokens : undefined;
14732
+ const outTok = typeof v.output_tokens === "number" ? v.output_tokens : undefined;
14733
+ if (inTok === undefined || outTok === undefined)
14734
+ return;
14735
+ const cacheRead = typeof v.cache_read_input_tokens === "number" ? v.cache_read_input_tokens : undefined;
14736
+ const cacheCreate = typeof v.cache_creation_input_tokens === "number" ? v.cache_creation_input_tokens : undefined;
14737
+ return {
14738
+ input_tokens: inTok,
14739
+ output_tokens: outTok,
14740
+ ...cacheRead !== undefined ? { cache_read_input_tokens: cacheRead } : {},
14741
+ ...cacheCreate !== undefined ? { cache_creation_input_tokens: cacheCreate } : {}
14742
+ };
14708
14743
  }
14709
14744
  function isObject(v) {
14710
14745
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -15598,6 +15633,27 @@ var init_engine_bootstrap = __esm(() => {
15598
15633
  init_claude_code_local();
15599
15634
  });
15600
15635
 
15636
+ // src/tui/lib/format-plan-usage.ts
15637
+ function pct(value) {
15638
+ if (typeof value !== "number" || !Number.isFinite(value))
15639
+ return null;
15640
+ return `${Math.round(value)}%`;
15641
+ }
15642
+ function formatPlanUsageCompact(usage) {
15643
+ if (!usage)
15644
+ return null;
15645
+ const fiveHour = pct(usage.fiveHour?.utilization);
15646
+ const sevenDay = pct(usage.sevenDay?.utilization);
15647
+ const parts = [];
15648
+ if (fiveHour)
15649
+ parts.push(`5h ${fiveHour}`);
15650
+ if (sevenDay)
15651
+ parts.push(`7d ${sevenDay}`);
15652
+ if (parts.length === 0)
15653
+ return null;
15654
+ return `Plan ${parts.join(" \xB7 ")}`;
15655
+ }
15656
+
15601
15657
  // src/tui/lib/use-pane-sizes.ts
15602
15658
  function usePaneSizes(kv) {
15603
15659
  const dims = useTerminalDimensions();
@@ -20349,7 +20405,7 @@ var init_user_slashes = () => {};
20349
20405
 
20350
20406
  // src/tui/panes/chat/context-meter.ts
20351
20407
  function totalContextTokens(u) {
20352
- return u.input_tokens + u.output_tokens + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
20408
+ return u.input_tokens + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
20353
20409
  }
20354
20410
  function contextWindowTokensForModel(modelId) {
20355
20411
  const id = modelId ?? resolveDefaultModelId();
@@ -20378,8 +20434,8 @@ function formatContextUsageCompact(u, modelId) {
20378
20434
  const total = totalContextTokens(u);
20379
20435
  if (total <= 0 || window <= 0)
20380
20436
  return null;
20381
- const pct = Math.min(100, Math.max(0, Math.round(total / window * 100)));
20382
- return `${pct}% \xB7 ${formatTokShort(total)}/${formatTokShort(window)}`;
20437
+ const pct2 = Math.min(100, Math.max(0, Math.round(total / window * 100)));
20438
+ return `${pct2}% \xB7 ${formatTokShort(total)}/${formatTokShort(window)}`;
20383
20439
  }
20384
20440
  var LONG_CTX = 1e6, STD_CTX = 200000;
20385
20441
  var init_context_meter = __esm(() => {
@@ -20431,7 +20487,19 @@ function setMessagesFromHistory(state, past) {
20431
20487
  for (const m of past) {
20432
20488
  appendRowsFromMessage(rows, toolIndexById, m);
20433
20489
  }
20434
- return { ...state, messages: capMessages(rows, new Date().toISOString()) };
20490
+ let latestUsage;
20491
+ for (let i = past.length - 1;i >= 0; i--) {
20492
+ const u = past[i]?.usage;
20493
+ if (u) {
20494
+ latestUsage = u;
20495
+ break;
20496
+ }
20497
+ }
20498
+ return {
20499
+ ...state,
20500
+ messages: capMessages(rows, new Date().toISOString()),
20501
+ ...latestUsage ? { lastUsage: latestUsage } : {}
20502
+ };
20435
20503
  }
20436
20504
  function enqueuePrompt(state, prompt, nowIso = new Date().toISOString()) {
20437
20505
  if (state.queue.length >= QUEUE_SOFT_CAP)
@@ -22351,6 +22419,8 @@ function Shell(props) {
22351
22419
  const [selectedId, setSelectedId] = createSignal(null);
22352
22420
  const [pendingPrompt, setPendingPrompt] = createSignal(null);
22353
22421
  const [workspaceContextAside, setWorkspaceContextAside] = createSignal(null);
22422
+ const planUsageAcc = props.orchestrator.planUsageSignal();
22423
+ const workspacePlanAside = createMemo(() => formatPlanUsageCompact(planUsageAcc()));
22354
22424
  const [updateInfo, setUpdateInfo] = createSignal(null);
22355
22425
  onMount(() => {
22356
22426
  checkLatestVersion().then((info) => {
@@ -22433,6 +22503,14 @@ function Shell(props) {
22433
22503
  selectFileTab,
22434
22504
  closeFileTab
22435
22505
  } = workspaceTabs;
22506
+ const workspaceAsideRight = createMemo(() => {
22507
+ const plan = workspacePlanAside();
22508
+ const ctx3 = isChatTabActive() ? workspaceContextAside() : null;
22509
+ const parts = [plan, ctx3].filter((v) => Boolean(v));
22510
+ if (parts.length === 0)
22511
+ return;
22512
+ return parts.join(" \u2022 ");
22513
+ });
22436
22514
  let pendingPersistedId = persistedSelectedId ?? null;
22437
22515
  createEffect(() => {
22438
22516
  const tasks = tasksAcc();
@@ -22565,7 +22643,7 @@ function Shell(props) {
22565
22643
  return activeTask()?.title ?? "no task";
22566
22644
  },
22567
22645
  get asideRight() {
22568
- return memo2(() => !!isChatTabActive())() ? workspaceContextAside() ?? undefined : undefined;
22646
+ return workspaceAsideRight();
22569
22647
  },
22570
22648
  get focused() {
22571
22649
  return focusedPane() === "workspace";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@sma1lboy/kobe",
4
- "version": "0.5.0",
4
+ "version": "0.5.1",
5
5
  "description": "TUI orchestrator for Claude Code (codename)",
6
6
  "type": "module",
7
7
  "packageManager": "bun@1.3.13",