@promptctl/cc-candybar 1.42.1 → 1.43.0

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.
Files changed (126) hide show
  1. package/dist/index.mjs +72 -71
  2. package/package.json +5 -6
  3. package/src/check.ts +0 -478
  4. package/src/cli-flags.ts +0 -8
  5. package/src/click/wire.ts +0 -158
  6. package/src/config/action.ts +0 -329
  7. package/src/config/cli.ts +0 -71
  8. package/src/config/default-dsl-config.ts +0 -1645
  9. package/src/config/disclosure.ts +0 -170
  10. package/src/config/dsl-loader.ts +0 -339
  11. package/src/config/dsl-types.ts +0 -581
  12. package/src/config/edit-chrome.ts +0 -559
  13. package/src/config/help.ts +0 -151
  14. package/src/config/ident.ts +0 -22
  15. package/src/config/layout-ops.ts +0 -177
  16. package/src/config/loader/actions.ts +0 -972
  17. package/src/config/loader/cache.ts +0 -206
  18. package/src/config/loader/cross-ref.ts +0 -714
  19. package/src/config/loader/cycles.ts +0 -148
  20. package/src/config/loader/diagnostics.ts +0 -99
  21. package/src/config/loader/discovery.ts +0 -182
  22. package/src/config/loader/edit-mode.ts +0 -137
  23. package/src/config/loader/emit-schema.ts +0 -68
  24. package/src/config/loader/globals.ts +0 -269
  25. package/src/config/loader/helpers.ts +0 -48
  26. package/src/config/loader/layout.ts +0 -693
  27. package/src/config/loader/looks.ts +0 -96
  28. package/src/config/loader/menu-synth.ts +0 -435
  29. package/src/config/loader/merge.ts +0 -115
  30. package/src/config/loader/persist-target.ts +0 -67
  31. package/src/config/loader/presets.ts +0 -119
  32. package/src/config/loader/refs.ts +0 -100
  33. package/src/config/loader/reserved-namespace.ts +0 -38
  34. package/src/config/loader/segments.ts +0 -120
  35. package/src/config/loader/validate-core.ts +0 -737
  36. package/src/config/loader/variables.ts +0 -260
  37. package/src/config/menu-keys.ts +0 -139
  38. package/src/config/option-domain.ts +0 -164
  39. package/src/config/presets.ts +0 -326
  40. package/src/config/settings-menu.ts +0 -775
  41. package/src/daemon/acquire.ts +0 -684
  42. package/src/daemon/cache/git.ts +0 -649
  43. package/src/daemon/cache/render.ts +0 -623
  44. package/src/daemon/cache/session-usage-store.ts +0 -720
  45. package/src/daemon/cache/watchers.ts +0 -249
  46. package/src/daemon/client-debug.ts +0 -120
  47. package/src/daemon/client-stats.ts +0 -130
  48. package/src/daemon/client-transport.ts +0 -273
  49. package/src/daemon/client.ts +0 -78
  50. package/src/daemon/config-overrides-store.ts +0 -663
  51. package/src/daemon/debug-types.ts +0 -91
  52. package/src/daemon/debug.ts +0 -264
  53. package/src/daemon/fork-bomb-breaker.ts +0 -351
  54. package/src/daemon/limits.ts +0 -211
  55. package/src/daemon/log.ts +0 -81
  56. package/src/daemon/parent-watchdog.ts +0 -87
  57. package/src/daemon/paths.ts +0 -211
  58. package/src/daemon/process-fingerprint.ts +0 -146
  59. package/src/daemon/protocol.ts +0 -292
  60. package/src/daemon/render-payload.ts +0 -1256
  61. package/src/daemon/server.ts +0 -1330
  62. package/src/daemon/session-state-file.ts +0 -108
  63. package/src/daemon/session-state.ts +0 -237
  64. package/src/daemon/socket-lease.ts +0 -209
  65. package/src/daemon/socket-ownership.ts +0 -209
  66. package/src/daemon/stats.ts +0 -235
  67. package/src/daemon/verbs/config-validators.ts +0 -250
  68. package/src/daemon/verbs/index.ts +0 -706
  69. package/src/daemon/verbs/state-validators.ts +0 -249
  70. package/src/daemon/verbs/validator-registry.ts +0 -457
  71. package/src/demo/dsl.ts +0 -143
  72. package/src/demo/mock-data.ts +0 -67
  73. package/src/demo/statusline.json5 +0 -94
  74. package/src/dsl/node-registry.ts +0 -374
  75. package/src/dsl/render.ts +0 -803
  76. package/src/help-text.ts +0 -90
  77. package/src/index.ts +0 -210
  78. package/src/install/currency.ts +0 -197
  79. package/src/install/index.ts +0 -557
  80. package/src/proc/launch.ts +0 -459
  81. package/src/proc/stats-handle.ts +0 -13
  82. package/src/render/action.ts +0 -883
  83. package/src/render/active-segment.ts +0 -78
  84. package/src/render/diagnostic-style.ts +0 -23
  85. package/src/render/diagnostic-text.ts +0 -77
  86. package/src/render/error-glyph.ts +0 -53
  87. package/src/render/menu.ts +0 -257
  88. package/src/render/outcome-plan.ts +0 -45
  89. package/src/render/picker.ts +0 -372
  90. package/src/render/segment-color.ts +0 -74
  91. package/src/render/split-lines.ts +0 -51
  92. package/src/render/strip.ts +0 -228
  93. package/src/segments/cache.ts +0 -131
  94. package/src/segments/context.ts +0 -190
  95. package/src/segments/git.ts +0 -1084
  96. package/src/segments/metrics.ts +0 -187
  97. package/src/segments/pricing.ts +0 -452
  98. package/src/segments/session.ts +0 -23
  99. package/src/segments/tmux.ts +0 -74
  100. package/src/template-engine/cells.ts +0 -90
  101. package/src/template-engine/colors.ts +0 -124
  102. package/src/template-engine/engine.ts +0 -108
  103. package/src/template-engine/funcs.ts +0 -232
  104. package/src/template-engine/index.ts +0 -11
  105. package/src/template-engine/layout.ts +0 -133
  106. package/src/template-engine/scope.ts +0 -62
  107. package/src/template-engine/sparkline.ts +0 -79
  108. package/src/themes/index.ts +0 -20
  109. package/src/themes/palette-resolvers.ts +0 -84
  110. package/src/themes/policy.ts +0 -393
  111. package/src/utils/cache.ts +0 -206
  112. package/src/utils/claude.ts +0 -683
  113. package/src/utils/color-support.ts +0 -118
  114. package/src/utils/formatters.ts +0 -99
  115. package/src/utils/logger.ts +0 -5
  116. package/src/utils/outcome.ts +0 -33
  117. package/src/utils/schema-validator.ts +0 -126
  118. package/src/utils/single-flight.ts +0 -57
  119. package/src/utils/terminal-width.ts +0 -51
  120. package/src/utils/terminal.ts +0 -11
  121. package/src/utils/transcript-fs.ts +0 -279
  122. package/src/var-system/index.ts +0 -24
  123. package/src/var-system/sources.ts +0 -1047
  124. package/src/var-system/store.ts +0 -223
  125. package/src/var-system/types.ts +0 -57
  126. package/src/version.ts +0 -17
@@ -1,187 +0,0 @@
1
- import type { ClaudeHookData, ParsedEntry } from "../utils/claude";
2
-
3
- // [LAW:one-source-of-truth] Metrics reads the transcript through the SAME
4
- // incremental append reader the usage store uses, folding only the bytes added
5
- // since last render into a running message count + a bounded recent-entry ring —
6
- // never a whole-file re-parse. Before this, metrics re-parsed the entire growing
7
- // transcript every render (free only while it piggybacked the store's cache hit);
8
- // once the store went incremental, a full re-parse here would have re-inherited
9
- // the very stall the store fix removed. Now both consumers are O(new bytes).
10
- import { readAppendedEntries, type TranscriptCursor } from "../utils/claude";
11
- import { statMtimeMs } from "../utils/transcript-fs";
12
- import { debug } from "../utils/logger";
13
- import { ABSENT, failed, ok, type Outcome } from "../utils/outcome";
14
-
15
- // [LAW:types-are-the-program] An `ok` MetricsInfo carries real values — "no
16
- // cost data at all" is the `absent` outcome arm, not a bag of nulls. The one
17
- // remaining null is lastResponseTime, where the domain genuinely has no
18
- // answer (no qualifying user→assistant pair in the recent window).
19
- export interface MetricsInfo {
20
- responseTime: number;
21
- lastResponseTime: number | null;
22
- sessionDuration: number;
23
- messageCount: number;
24
- linesAdded: number;
25
- linesRemoved: number;
26
- }
27
-
28
- // lastResponseTime only inspects the most recent turns for a user→assistant
29
- // pair; a bounded tail of non-sidechain entries is all it needs. The ring caps
30
- // per-session retained memory at O(1) regardless of transcript length.
31
- const RECENT_WINDOW = 20;
32
- // Bound the per-session fold map like the usage store's records — a long-lived
33
- // daemon sees many sessions; the ring is tiny, but the map must not grow without
34
- // limit. LRU by insertion order (delete+set on write), evict oldest past the cap.
35
- const MAX_SESSIONS = 256;
36
-
37
- // [LAW:one-source-of-truth] The incremental fold of one session's metrics: the
38
- // byte cursor already consumed, the running real-user message count, and the
39
- // recent-entry ring. Canonical; MetricsInfo's transcript-derived fields derive
40
- // from it, the hookData-derived fields (durations, lines) are always fresh.
41
- interface MetricsState {
42
- cursor: TranscriptCursor;
43
- messageCount: number;
44
- recent: ParsedEntry[];
45
- }
46
-
47
- // A real user turn vs. a tool_result echoed back as a "user" line. The
48
- // discriminator is metrics-local policy over the shared ParsedEntry scalars.
49
- function isRealUserMessage(entry: ParsedEntry): boolean {
50
- const messageType = entry.type || entry.message?.role || entry.message?.type;
51
- const isToolResult =
52
- entry.type === "user" && entry.message?.firstContentType === "tool_result";
53
- return messageType === "user" && !isToolResult;
54
- }
55
-
56
- export class MetricsProvider {
57
- // [LAW:no-shared-mutable-globals] Single owner, hard cap, LRU eviction — the
58
- // per-session incremental fold state. One instance per daemon.
59
- private readonly state = new Map<string, MetricsState>();
60
-
61
- private calculateLastResponseTime(entries: ParsedEntry[]): number | null {
62
- if (entries.length === 0) return null;
63
-
64
- let lastUserTime: Date | null = null;
65
- let bestResponseTime: number | null = null;
66
-
67
- for (const entry of entries) {
68
- const messageType =
69
- entry.type || entry.message?.role || entry.message?.type;
70
-
71
- if (isRealUserMessage(entry)) {
72
- lastUserTime = entry.timestamp;
73
- } else if (messageType === "assistant" && lastUserTime) {
74
- const responseTime =
75
- (entry.timestamp.getTime() - lastUserTime.getTime()) / 1000;
76
- if (responseTime > 0.1 && responseTime < 300) {
77
- bestResponseTime = responseTime;
78
- }
79
- }
80
- }
81
-
82
- return bestResponseTime;
83
- }
84
-
85
- // [LAW:dataflow-not-control-flow] Fold the appended entries onto the prior
86
- // state, producing a FRESH state (prior is never mutated) so two concurrent
87
- // renders of one session both fold from the same prior and last-writer-wins is
88
- // a correct answer, never a double-count. `reset` (a /compact rewrite) folds
89
- // from empty. Returns the transcript-derived fields the caller composes with
90
- // the always-fresh hookData fields.
91
- private async foldMetrics(
92
- sessionId: string,
93
- transcriptPath: string,
94
- ): Promise<
95
- // [LAW:types-are-the-program] Never `absent` — a missing transcript is a real
96
- // zero-count fold (ok), so the caller has exactly two arms to handle.
97
- | { kind: "ok"; value: { messageCount: number; recent: ParsedEntry[] } }
98
- | { kind: "failed"; reason: string }
99
- > {
100
- const prior = this.state.get(sessionId);
101
- const mtime = statMtimeMs(transcriptPath);
102
- // [LAW:no-ambient-temporal-coupling] Fast hit: the transcript is unchanged
103
- // since we last folded it, so the message count + ring stand.
104
- if (prior && mtime !== 0 && prior.cursor.mtimeMs === mtime) {
105
- // [LAW:no-ambient-temporal-coupling] Re-order on the hit so LRU eviction
106
- // reflects READ recency, not just write recency — an active session whose
107
- // transcript is momentarily unchanged must not be evicted ahead of idle
108
- // ones (matches the usage store's hit path).
109
- this.state.delete(sessionId);
110
- this.state.set(sessionId, prior);
111
- // [LAW:no-shared-mutable-globals] Return a COPY of the stored ring, never
112
- // the reference — a caller mutating the returned array would silently
113
- // corrupt the retained state (a heisenbug in the next render's
114
- // lastResponseTime). Every path (hit/miss/absent) copies. 20 entries.
115
- return {
116
- kind: "ok",
117
- value: { messageCount: prior.messageCount, recent: [...prior.recent] },
118
- };
119
- }
120
-
121
- const read = await readAppendedEntries(transcriptPath, prior?.cursor);
122
- if (read.kind === "failed") return read;
123
- if (read.kind === "absent") {
124
- // No transcript yet (fresh session) — keep prior if any, don't cache a
125
- // cursor so the next render retries when the file appears. Copy the ring
126
- // (like the hit/miss paths) so no stored reference escapes.
127
- return {
128
- kind: "ok",
129
- value: {
130
- messageCount: prior?.messageCount ?? 0,
131
- recent: [...(prior?.recent ?? [])],
132
- },
133
- };
134
- }
135
-
136
- const { entries, cursor, reset } = read.value;
137
- const base = prior && !reset ? prior : undefined;
138
- let messageCount = base?.messageCount ?? 0;
139
- let recent = base ? [...base.recent] : [];
140
- for (const entry of entries) {
141
- if (entry.isSidechain) continue;
142
- if (isRealUserMessage(entry)) messageCount++;
143
- recent.push(entry);
144
- }
145
- if (recent.length > RECENT_WINDOW) recent = recent.slice(-RECENT_WINDOW);
146
-
147
- this.state.delete(sessionId);
148
- this.state.set(sessionId, { cursor, messageCount, recent });
149
- while (this.state.size > MAX_SESSIONS) {
150
- const oldest = this.state.keys().next().value;
151
- if (oldest === undefined) break;
152
- this.state.delete(oldest);
153
- }
154
- // [LAW:no-shared-mutable-globals] `recent` is now the STORED array — copy it
155
- // out so the returned value shares no reference with retained state.
156
- return { kind: "ok", value: { messageCount, recent: [...recent] } };
157
- }
158
-
159
- // [LAW:no-silent-failure] A hook payload with no cost block is `absent`
160
- // (old clients); a transcript parse error is `failed`, carried to the
161
- // payload boundary — the old catch dressed it as the same all-null record
162
- // as "no data".
163
- async getMetricsInfo(
164
- sessionId: string,
165
- hookData: ClaudeHookData,
166
- ): Promise<Outcome<MetricsInfo>> {
167
- debug(`Getting metrics from hook data for session: ${sessionId}`);
168
-
169
- if (!hookData.cost) {
170
- return ABSENT;
171
- }
172
-
173
- const folded = await this.foldMetrics(sessionId, hookData.transcript_path);
174
- if (folded.kind === "failed") {
175
- return failed(`metrics (${sessionId}): ${folded.reason}`);
176
- }
177
-
178
- return ok({
179
- responseTime: hookData.cost.total_api_duration_ms / 1000,
180
- lastResponseTime: this.calculateLastResponseTime(folded.value.recent),
181
- sessionDuration: hookData.cost.total_duration_ms / 1000,
182
- messageCount: folded.value.messageCount,
183
- linesAdded: hookData.cost.total_lines_added,
184
- linesRemoved: hookData.cost.total_lines_removed,
185
- });
186
- }
187
- }
@@ -1,452 +0,0 @@
1
- import { debug } from "../utils/logger";
2
- import { get } from "node:https";
3
- import { URL } from "node:url";
4
- import { CacheManager } from "../utils/cache";
5
- import type { PrunedRaw } from "../utils/claude.js";
6
-
7
- export interface ModelPricing {
8
- name: string;
9
- input: number;
10
- cache_write_5m: number;
11
- cache_write_1h: number;
12
- cache_read: number;
13
- output: number;
14
- }
15
- const OFFLINE_PRICING_DATA: Record<string, ModelPricing> = {
16
- "claude-haiku-4-5-20251001": {
17
- name: "Claude Haiku 4.5",
18
- input: 1.0,
19
- output: 5.0,
20
- cache_write_5m: 1.25,
21
- cache_write_1h: 2.0,
22
- cache_read: 0.1,
23
- },
24
- "claude-haiku-4-5": {
25
- name: "Claude Haiku 4.5",
26
- input: 1.0,
27
- output: 5.0,
28
- cache_write_5m: 1.25,
29
- cache_write_1h: 2.0,
30
- cache_read: 0.1,
31
- },
32
- "claude-opus-4-20250514": {
33
- name: "Claude Opus 4",
34
- input: 15.0,
35
- output: 75.0,
36
- cache_write_5m: 18.75,
37
- cache_write_1h: 30.0,
38
- cache_read: 1.5,
39
- },
40
- "claude-opus-4-1": {
41
- name: "Claude Opus 4.1",
42
- input: 15.0,
43
- output: 75.0,
44
- cache_write_5m: 18.75,
45
- cache_write_1h: 30.0,
46
- cache_read: 1.5,
47
- },
48
- "claude-opus-4-1-20250805": {
49
- name: "Claude Opus 4.1",
50
- input: 15.0,
51
- output: 75.0,
52
- cache_write_5m: 18.75,
53
- cache_write_1h: 30.0,
54
- cache_read: 1.5,
55
- },
56
- "claude-sonnet-4-20250514": {
57
- name: "Claude Sonnet 4",
58
- input: 3.0,
59
- output: 15.0,
60
- cache_write_5m: 3.75,
61
- cache_write_1h: 6.0,
62
- cache_read: 0.3,
63
- },
64
- "claude-4-opus-20250514": {
65
- name: "Claude 4 Opus",
66
- input: 15.0,
67
- output: 75.0,
68
- cache_write_5m: 18.75,
69
- cache_write_1h: 30.0,
70
- cache_read: 1.5,
71
- },
72
- "claude-4-sonnet-20250514": {
73
- name: "Claude 4 Sonnet",
74
- input: 3.0,
75
- output: 15.0,
76
- cache_write_5m: 3.75,
77
- cache_write_1h: 6.0,
78
- cache_read: 0.3,
79
- },
80
- "claude-sonnet-4-5": {
81
- name: "Claude Sonnet 4.5",
82
- input: 3.0,
83
- output: 15.0,
84
- cache_write_5m: 3.75,
85
- cache_write_1h: 6.0,
86
- cache_read: 0.3,
87
- },
88
- "claude-sonnet-4-5-20250929": {
89
- name: "Claude Sonnet 4.5",
90
- input: 3.0,
91
- output: 15.0,
92
- cache_write_5m: 3.75,
93
- cache_write_1h: 6.0,
94
- cache_read: 0.3,
95
- },
96
- "claude-opus-4-5": {
97
- name: "Claude Opus 4.5",
98
- input: 5.0,
99
- output: 25.0,
100
- cache_write_5m: 6.25,
101
- cache_write_1h: 10.0,
102
- cache_read: 0.5,
103
- },
104
- "claude-opus-4-5-20251101": {
105
- name: "Claude Opus 4.5",
106
- input: 5.0,
107
- output: 25.0,
108
- cache_write_5m: 6.25,
109
- cache_write_1h: 10.0,
110
- cache_read: 0.5,
111
- },
112
- "claude-opus-4-6": {
113
- name: "Claude Opus 4.6",
114
- input: 5.0,
115
- output: 25.0,
116
- cache_write_5m: 6.25,
117
- cache_write_1h: 10.0,
118
- cache_read: 0.5,
119
- },
120
- "claude-opus-4-6-20260205": {
121
- name: "Claude Opus 4.6",
122
- input: 5.0,
123
- output: 25.0,
124
- cache_write_5m: 6.25,
125
- cache_write_1h: 10.0,
126
- cache_read: 0.5,
127
- },
128
- "claude-sonnet-4-6": {
129
- name: "Claude Sonnet 4.6",
130
- input: 3.0,
131
- output: 15.0,
132
- cache_write_5m: 3.75,
133
- cache_write_1h: 6.0,
134
- cache_read: 0.3,
135
- },
136
- };
137
-
138
- export class PricingService {
139
- private static executionCache: Record<string, ModelPricing> | null = null;
140
- private static modelPricingCache = new Map<string, ModelPricing>();
141
- private static readonly GITHUB_PRICING_URL =
142
- "https://raw.githubusercontent.com/promptctl/cc-candybar/main/pricing.json";
143
-
144
- private static async loadDiskCache(): Promise<Record<
145
- string,
146
- ModelPricing
147
- > | null> {
148
- const TTL_24H = 24 * 60 * 60 * 1000;
149
- const minValidTime = Date.now() - TTL_24H;
150
- return (await CacheManager.getUsageCache(
151
- "pricing",
152
- minValidTime,
153
- )) as Record<string, ModelPricing> | null;
154
- }
155
-
156
- private static async saveDiskCache(
157
- data: Record<string, ModelPricing>,
158
- ): Promise<void> {
159
- await CacheManager.setUsageCache("pricing", data);
160
- }
161
-
162
- private static async fetchPricingData(): Promise<Record<
163
- string,
164
- ModelPricing
165
- > | null> {
166
- return new Promise((resolve) => {
167
- const parsedUrl = new URL(this.GITHUB_PRICING_URL);
168
-
169
- const request = get(
170
- {
171
- hostname: parsedUrl.hostname,
172
- path: parsedUrl.pathname,
173
- headers: {
174
- "User-Agent": "cc-candybar",
175
- "Cache-Control": "no-cache",
176
- },
177
- timeout: 5000,
178
- },
179
- (response) => {
180
- if (response.statusCode !== 200) {
181
- debug(`HTTP ${response.statusCode}: ${response.statusMessage}`);
182
- resolve(null);
183
- return;
184
- }
185
-
186
- let data = "";
187
- let size = 0;
188
- const MAX_SIZE = 1024 * 1024;
189
-
190
- response.on("data", (chunk) => {
191
- size += chunk.length;
192
- if (size > MAX_SIZE) {
193
- debug("Response too large");
194
- request.destroy();
195
- resolve(null);
196
- return;
197
- }
198
- data += chunk;
199
- });
200
-
201
- response.on("end", () => {
202
- try {
203
- const json = JSON.parse(data);
204
- const dataObj = json as Record<string, unknown>;
205
- const meta = dataObj._meta as { updated?: string } | undefined;
206
-
207
- const pricingData: Record<string, unknown> = {};
208
- for (const [key, value] of Object.entries(dataObj)) {
209
- if (key !== "_meta") {
210
- pricingData[key] = value;
211
- }
212
- }
213
-
214
- if (this.validatePricingData(pricingData)) {
215
- debug(
216
- `Fetched fresh pricing from GitHub for ${Object.keys(pricingData).length} models`,
217
- );
218
- debug(`Pricing last updated: ${meta?.updated || "unknown"}`);
219
- resolve(pricingData);
220
- } else {
221
- debug("Invalid pricing data structure");
222
- resolve(null);
223
- }
224
- } catch (error) {
225
- debug("Failed to parse JSON:", error);
226
- resolve(null);
227
- }
228
- });
229
-
230
- response.on("error", (error) => {
231
- debug("Response error:", error);
232
- resolve(null);
233
- });
234
- },
235
- );
236
-
237
- request.on("error", (error) => {
238
- debug("Request error:", error);
239
- resolve(null);
240
- });
241
-
242
- request.on("timeout", () => {
243
- debug("Request timeout");
244
- request.destroy();
245
- resolve(null);
246
- });
247
-
248
- request.end();
249
- });
250
- }
251
-
252
- static async getCurrentPricing(): Promise<Record<string, ModelPricing>> {
253
- if (this.executionCache !== null) {
254
- debug(
255
- `[CACHE-HIT] Pricing execution cache: ${Object.keys(this.executionCache).length} models`,
256
- );
257
- return this.executionCache;
258
- }
259
-
260
- const diskCached = await this.loadDiskCache();
261
- if (diskCached) {
262
- debug(
263
- `[CACHE-HIT] Pricing disk cache: ${Object.keys(diskCached).length} models`,
264
- );
265
- this.executionCache = diskCached;
266
- debug(
267
- `[CACHE-SET] Pricing execution cache stored: ${Object.keys(diskCached).length} models`,
268
- );
269
- return diskCached;
270
- }
271
-
272
- const freshData = await this.fetchPricingData();
273
- if (freshData) {
274
- await this.saveDiskCache(freshData);
275
- debug(
276
- `[CACHE-SET] Pricing disk cache stored: ${Object.keys(freshData).length} models`,
277
- );
278
- this.executionCache = freshData;
279
- debug(
280
- `[CACHE-SET] Pricing execution cache stored: ${Object.keys(freshData).length} models`,
281
- );
282
- return freshData;
283
- }
284
-
285
- debug(
286
- `[CACHE-FALLBACK] Using offline pricing data: ${Object.keys(OFFLINE_PRICING_DATA).length} models`,
287
- );
288
- this.executionCache = OFFLINE_PRICING_DATA;
289
- debug(
290
- `[CACHE-SET] Pricing execution cache stored: ${Object.keys(OFFLINE_PRICING_DATA).length} models`,
291
- );
292
- return OFFLINE_PRICING_DATA;
293
- }
294
-
295
- private static validatePricingData(
296
- data: unknown,
297
- ): data is Record<string, ModelPricing> {
298
- if (!data || typeof data !== "object") return false;
299
-
300
- for (const [, value] of Object.entries(data)) {
301
- if (!value || typeof value !== "object") return false;
302
- const pricing = value as Record<string, unknown>;
303
-
304
- if (
305
- typeof pricing.input !== "number" ||
306
- typeof pricing.output !== "number" ||
307
- typeof pricing.cache_read !== "number"
308
- ) {
309
- return false;
310
- }
311
- }
312
-
313
- return true;
314
- }
315
-
316
- static async getModelPricing(modelId: string): Promise<ModelPricing> {
317
- if (this.modelPricingCache.has(modelId)) {
318
- debug(`[CACHE-HIT] Model pricing cache: ${modelId}`);
319
- return this.modelPricingCache.get(modelId)!;
320
- }
321
-
322
- const allPricing = await this.getCurrentPricing();
323
- let pricing: ModelPricing;
324
-
325
- if (allPricing[modelId]) {
326
- pricing = allPricing[modelId];
327
- } else {
328
- pricing = this.fuzzyMatchModel(modelId, allPricing);
329
- }
330
-
331
- this.modelPricingCache.set(modelId, pricing);
332
- debug(`[CACHE-SET] Model pricing cache: ${modelId}`);
333
- return pricing;
334
- }
335
-
336
- private static fuzzyMatchModel(
337
- modelId: string,
338
- allPricing: Record<string, ModelPricing>,
339
- ): ModelPricing {
340
- const lowerModelId = modelId.toLowerCase();
341
-
342
- for (const [key, pricing] of Object.entries(allPricing)) {
343
- if (key.toLowerCase() === lowerModelId) {
344
- return pricing;
345
- }
346
- }
347
- const patterns = [
348
- {
349
- pattern: ["opus-4-6", "claude-opus-4-6"],
350
- fallback: "claude-opus-4-6-20260205",
351
- },
352
- {
353
- pattern: ["opus-4-5", "claude-opus-4-5"],
354
- fallback: "claude-opus-4-5-20251101",
355
- },
356
- {
357
- pattern: ["opus-4-1", "claude-opus-4-1"],
358
- fallback: "claude-opus-4-1-20250805",
359
- },
360
- {
361
- pattern: ["opus-4", "claude-opus-4"],
362
- fallback: "claude-opus-4-20250514",
363
- },
364
- {
365
- pattern: ["sonnet-4-6", "sonnet-4.6", "claude-sonnet-4-6"],
366
- fallback: "claude-sonnet-4-6",
367
- },
368
- {
369
- pattern: ["sonnet-4.5", "4-5-sonnet", "sonnet-4-5"],
370
- fallback: "claude-sonnet-4-5-20250929",
371
- },
372
- {
373
- pattern: ["sonnet-4", "claude-sonnet-4"],
374
- fallback: "claude-sonnet-4-20250514",
375
- },
376
- {
377
- pattern: ["haiku-4.5", "4-5-haiku", "haiku-4-5"],
378
- fallback: "claude-haiku-4-5-20251001",
379
- },
380
- { pattern: ["haiku"], fallback: "claude-haiku-4-5-20251001" },
381
- { pattern: ["opus"], fallback: "claude-opus-4-20250514" },
382
- { pattern: ["sonnet"], fallback: "claude-sonnet-4-5-20250929" },
383
- ];
384
-
385
- for (const { pattern, fallback } of patterns) {
386
- if (pattern.some((p) => lowerModelId.includes(p))) {
387
- if (allPricing[fallback]) {
388
- return allPricing[fallback];
389
- }
390
- }
391
- }
392
-
393
- return (
394
- allPricing["claude-sonnet-4-5-20250929"] || {
395
- name: `${modelId} (Unknown Model)`,
396
- input: 3.0,
397
- cache_write_5m: 3.75,
398
- cache_write_1h: 6.0,
399
- cache_read: 0.3,
400
- output: 15.0,
401
- }
402
- );
403
- }
404
-
405
- static async calculateCostForEntry(entry: PrunedRaw): Promise<number> {
406
- const usage = entry.message?.usage;
407
- if (!usage) {
408
- return 0;
409
- }
410
-
411
- const modelId = this.extractModelId(entry);
412
- const pricing = await this.getModelPricing(modelId);
413
-
414
- const inputTokens = usage.input_tokens || 0;
415
- const outputTokens = usage.output_tokens || 0;
416
- const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
417
- const cacheReadTokens = usage.cache_read_input_tokens || 0;
418
-
419
- const inputCost = (inputTokens / 1_000_000) * pricing.input;
420
- const outputCost = (outputTokens / 1_000_000) * pricing.output;
421
- const cacheReadCost = (cacheReadTokens / 1_000_000) * pricing.cache_read;
422
- const cacheCreationCost =
423
- (cacheCreationTokens / 1_000_000) * pricing.cache_write_5m;
424
-
425
- return inputCost + outputCost + cacheCreationCost + cacheReadCost;
426
- }
427
-
428
- private static extractModelId(entry: Record<string, unknown>): string {
429
- if (entry.model && typeof entry.model === "string") {
430
- return entry.model;
431
- }
432
-
433
- const message = entry.message as Record<string, unknown> | undefined;
434
- if (message?.model) {
435
- const model = message.model;
436
- if (typeof model === "string") {
437
- return model;
438
- }
439
- const modelObj = model as Record<string, unknown> | undefined;
440
- return (
441
- (typeof modelObj?.id === "string" ? modelObj.id : null) ||
442
- "claude-sonnet-4-5-20250929"
443
- );
444
- }
445
-
446
- if (entry.model_id && typeof entry.model_id === "string") {
447
- return entry.model_id;
448
- }
449
-
450
- return "claude-sonnet-4-5-20250929";
451
- }
452
- }
@@ -1,23 +0,0 @@
1
- // [LAW:decomposition] The session usage CONTRACT — the shapes the daemon's
2
- // SessionUsageStore produces and the render payload reads. The transcript fold
3
- // that produces them is incremental and lives in the store (its single owner);
4
- // this module is the type seam both sides agree on.
5
-
6
- export interface TokenBreakdown {
7
- input: number;
8
- output: number;
9
- cacheCreation: number;
10
- cacheRead: number;
11
- }
12
-
13
- export interface SessionInfo {
14
- cost: number | null;
15
- calculatedCost: number | null;
16
- officialCost: number | null;
17
- tokens: number | null;
18
- tokenBreakdown: TokenBreakdown | null;
19
- }
20
-
21
- export interface UsageInfo {
22
- session: SessionInfo;
23
- }