linkshell-cli 0.5.12 → 0.5.13

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.
@@ -87,6 +87,7 @@ export declare class AgentWorkspaceProxy {
87
87
  private adoptConversationId;
88
88
  private emitItem;
89
89
  private emitConversation;
90
+ private applyConversationUsage;
90
91
  private emitStatus;
91
92
  private emitNotice;
92
93
  private rejectAgentAction;
@@ -53,6 +53,21 @@ function firstNumber(value, keys) {
53
53
  }
54
54
  return undefined;
55
55
  }
56
+ // Best-effort context-window size (tokens) for a model id, used as the "% used"
57
+ // denominator only when the provider doesn't report it directly. Conservative
58
+ // on purpose: returns undefined for anything we can't confidently map, so the
59
+ // UI shows a raw token count instead of a misleading percentage. Claude omits
60
+ // context_window in result.usage, so this fallback is what gives Claude a meter.
61
+ function contextWindowForModel(model) {
62
+ if (!model)
63
+ return undefined;
64
+ const m = model.toLowerCase();
65
+ if (m.includes("claude")) {
66
+ // 1M-context variants are tagged (e.g. "claude-opus-4-8[1m]"); else 200k.
67
+ return /\b1m\b|\[1m\]|-1m/.test(m) ? 1_000_000 : 200_000;
68
+ }
69
+ return undefined;
70
+ }
56
71
  function stringArray(value) {
57
72
  if (!Array.isArray(value))
58
73
  return [];
@@ -2477,12 +2492,22 @@ export class AgentWorkspaceProxy {
2477
2492
  }
2478
2493
  if (method.startsWith("account/") ||
2479
2494
  method.startsWith("mcpServer/startupStatus/") ||
2480
- method === "thread/tokenUsage/updated" ||
2481
2495
  method === "serverRequest/resolved" ||
2482
2496
  method === "mcpServer/oauthLogin/completed") {
2483
2497
  return;
2484
2498
  }
2485
2499
  const conversationId = this.conversationIdFromParams(params) ?? this.fallbackConversationId();
2500
+ if (method === "thread/tokenUsage/updated") {
2501
+ // Codex emits cumulative token usage for the thread. Carry it onto the
2502
+ // conversation so clients can show a context meter (parity with Codex's
2503
+ // own /status). Cost is Claude-only; Codex usually omits it.
2504
+ if (conversationId) {
2505
+ const raw = asRecord(params);
2506
+ const usage = asRecord(raw?.usage) ? raw?.usage : params;
2507
+ this.applyConversationUsage(conversationId, usage);
2508
+ }
2509
+ return;
2510
+ }
2486
2511
  if (method === "thread/status/changed") {
2487
2512
  const status = agentStatusFromThreadStatus(params);
2488
2513
  if (conversationId && status) {
@@ -2541,6 +2566,13 @@ export class AgentWorkspaceProxy {
2541
2566
  }
2542
2567
  if (method === "turn/completed") {
2543
2568
  if (conversationId) {
2569
+ // Claude reports cumulative usage + cost on turn completion (Codex sends
2570
+ // it via thread/tokenUsage/updated instead). Capture it for the meter.
2571
+ const raw = asRecord(params);
2572
+ const cost = firstNumber(raw, ["totalCostUsd", "total_cost_usd"]);
2573
+ if (raw?.usage != null || cost != null) {
2574
+ this.applyConversationUsage(conversationId, raw?.usage, cost);
2575
+ }
2544
2576
  this.forgetCurrentTurn(conversationId, this.extractTurnId(params));
2545
2577
  this.updateConversationStatus(conversationId, "idle");
2546
2578
  }
@@ -3371,6 +3403,52 @@ export class AgentWorkspaceProxy {
3371
3403
  payload: { conversationId: conversation.id, conversation: { ...conversation } },
3372
3404
  }));
3373
3405
  }
3406
+ // Normalize a provider usage payload (Claude result.usage, Codex
3407
+ // thread/tokenUsage/updated) into our wire shape, merge onto the conversation,
3408
+ // and emit. Field names vary by provider so we probe snake_case + camelCase.
3409
+ // Missing fields preserve the previous value (cumulative view). No-ops when
3410
+ // nothing meaningful resolves, so a stray empty payload won't blank the meter.
3411
+ applyConversationUsage(conversationId, raw, costUsd) {
3412
+ const conversation = this.conversations.get(conversationId);
3413
+ if (!conversation)
3414
+ return;
3415
+ const rec = asRecord(raw);
3416
+ const num = (...keys) => {
3417
+ for (const k of keys) {
3418
+ const v = rec?.[k];
3419
+ if (typeof v === "number" && Number.isFinite(v))
3420
+ return v;
3421
+ }
3422
+ return undefined;
3423
+ };
3424
+ const inputTokens = num("input_tokens", "inputTokens", "input");
3425
+ const outputTokens = num("output_tokens", "outputTokens", "output");
3426
+ const cacheReadTokens = num("cache_read_input_tokens", "cacheReadInputTokens", "cache_read_tokens", "cached_input_tokens", "cacheReadTokens");
3427
+ let totalTokens = num("total_tokens", "totalTokens", "total");
3428
+ if (totalTokens == null && (inputTokens != null || outputTokens != null)) {
3429
+ totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
3430
+ }
3431
+ const contextWindow = num("context_window", "contextWindow", "model_context_window", "window") ??
3432
+ contextWindowForModel(conversation.model);
3433
+ const prev = conversation.usage ?? {};
3434
+ const next = {
3435
+ inputTokens: inputTokens ?? prev.inputTokens,
3436
+ outputTokens: outputTokens ?? prev.outputTokens,
3437
+ cacheReadTokens: cacheReadTokens ?? prev.cacheReadTokens,
3438
+ totalTokens: totalTokens ?? prev.totalTokens,
3439
+ contextWindow: contextWindow ?? prev.contextWindow,
3440
+ totalCostUsd: costUsd ?? prev.totalCostUsd,
3441
+ updatedAt: Date.now(),
3442
+ };
3443
+ if (next.inputTokens == null &&
3444
+ next.outputTokens == null &&
3445
+ next.totalTokens == null &&
3446
+ next.totalCostUsd == null) {
3447
+ return;
3448
+ }
3449
+ conversation.usage = next;
3450
+ this.emitConversation(conversation);
3451
+ }
3374
3452
  emitStatus(conversationId, status, text) {
3375
3453
  this.addItem(conversationId, {
3376
3454
  id: id("status"),