@promptctl/cc-candybar 1.42.0 → 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/README.md +4 -0
  2. package/dist/index.mjs +81 -80
  3. package/package.json +5 -6
  4. package/src/check.ts +0 -478
  5. package/src/cli-flags.ts +0 -8
  6. package/src/click/wire.ts +0 -158
  7. package/src/config/action.ts +0 -329
  8. package/src/config/cli.ts +0 -71
  9. package/src/config/default-dsl-config.ts +0 -1645
  10. package/src/config/disclosure.ts +0 -170
  11. package/src/config/dsl-loader.ts +0 -339
  12. package/src/config/dsl-types.ts +0 -581
  13. package/src/config/edit-chrome.ts +0 -559
  14. package/src/config/help.ts +0 -151
  15. package/src/config/ident.ts +0 -22
  16. package/src/config/layout-ops.ts +0 -177
  17. package/src/config/loader/actions.ts +0 -972
  18. package/src/config/loader/cache.ts +0 -206
  19. package/src/config/loader/cross-ref.ts +0 -714
  20. package/src/config/loader/cycles.ts +0 -148
  21. package/src/config/loader/diagnostics.ts +0 -99
  22. package/src/config/loader/discovery.ts +0 -182
  23. package/src/config/loader/edit-mode.ts +0 -137
  24. package/src/config/loader/emit-schema.ts +0 -68
  25. package/src/config/loader/globals.ts +0 -269
  26. package/src/config/loader/helpers.ts +0 -48
  27. package/src/config/loader/layout.ts +0 -693
  28. package/src/config/loader/looks.ts +0 -96
  29. package/src/config/loader/menu-synth.ts +0 -435
  30. package/src/config/loader/merge.ts +0 -115
  31. package/src/config/loader/persist-target.ts +0 -67
  32. package/src/config/loader/presets.ts +0 -119
  33. package/src/config/loader/refs.ts +0 -100
  34. package/src/config/loader/reserved-namespace.ts +0 -38
  35. package/src/config/loader/segments.ts +0 -120
  36. package/src/config/loader/validate-core.ts +0 -737
  37. package/src/config/loader/variables.ts +0 -260
  38. package/src/config/menu-keys.ts +0 -139
  39. package/src/config/option-domain.ts +0 -164
  40. package/src/config/presets.ts +0 -326
  41. package/src/config/settings-menu.ts +0 -775
  42. package/src/daemon/acquire.ts +0 -684
  43. package/src/daemon/cache/git.ts +0 -649
  44. package/src/daemon/cache/render.ts +0 -623
  45. package/src/daemon/cache/session-usage-store.ts +0 -720
  46. package/src/daemon/cache/watchers.ts +0 -249
  47. package/src/daemon/client-debug.ts +0 -120
  48. package/src/daemon/client-stats.ts +0 -130
  49. package/src/daemon/client-transport.ts +0 -273
  50. package/src/daemon/client.ts +0 -78
  51. package/src/daemon/config-overrides-store.ts +0 -663
  52. package/src/daemon/debug-types.ts +0 -91
  53. package/src/daemon/debug.ts +0 -264
  54. package/src/daemon/fork-bomb-breaker.ts +0 -351
  55. package/src/daemon/limits.ts +0 -211
  56. package/src/daemon/log.ts +0 -81
  57. package/src/daemon/parent-watchdog.ts +0 -87
  58. package/src/daemon/paths.ts +0 -211
  59. package/src/daemon/process-fingerprint.ts +0 -146
  60. package/src/daemon/protocol.ts +0 -292
  61. package/src/daemon/render-payload.ts +0 -1256
  62. package/src/daemon/server.ts +0 -1330
  63. package/src/daemon/session-state-file.ts +0 -108
  64. package/src/daemon/session-state.ts +0 -237
  65. package/src/daemon/socket-lease.ts +0 -209
  66. package/src/daemon/socket-ownership.ts +0 -209
  67. package/src/daemon/stats.ts +0 -235
  68. package/src/daemon/verbs/config-validators.ts +0 -250
  69. package/src/daemon/verbs/index.ts +0 -706
  70. package/src/daemon/verbs/state-validators.ts +0 -249
  71. package/src/daemon/verbs/validator-registry.ts +0 -457
  72. package/src/demo/dsl.ts +0 -143
  73. package/src/demo/mock-data.ts +0 -67
  74. package/src/demo/statusline.json5 +0 -94
  75. package/src/dsl/node-registry.ts +0 -374
  76. package/src/dsl/render.ts +0 -803
  77. package/src/help-text.ts +0 -90
  78. package/src/index.ts +0 -210
  79. package/src/install/index.ts +0 -541
  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
- }