@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.
- package/README.md +4 -0
- package/dist/index.mjs +81 -80
- package/package.json +5 -6
- package/src/check.ts +0 -478
- package/src/cli-flags.ts +0 -8
- package/src/click/wire.ts +0 -158
- package/src/config/action.ts +0 -329
- package/src/config/cli.ts +0 -71
- package/src/config/default-dsl-config.ts +0 -1645
- package/src/config/disclosure.ts +0 -170
- package/src/config/dsl-loader.ts +0 -339
- package/src/config/dsl-types.ts +0 -581
- package/src/config/edit-chrome.ts +0 -559
- package/src/config/help.ts +0 -151
- package/src/config/ident.ts +0 -22
- package/src/config/layout-ops.ts +0 -177
- package/src/config/loader/actions.ts +0 -972
- package/src/config/loader/cache.ts +0 -206
- package/src/config/loader/cross-ref.ts +0 -714
- package/src/config/loader/cycles.ts +0 -148
- package/src/config/loader/diagnostics.ts +0 -99
- package/src/config/loader/discovery.ts +0 -182
- package/src/config/loader/edit-mode.ts +0 -137
- package/src/config/loader/emit-schema.ts +0 -68
- package/src/config/loader/globals.ts +0 -269
- package/src/config/loader/helpers.ts +0 -48
- package/src/config/loader/layout.ts +0 -693
- package/src/config/loader/looks.ts +0 -96
- package/src/config/loader/menu-synth.ts +0 -435
- package/src/config/loader/merge.ts +0 -115
- package/src/config/loader/persist-target.ts +0 -67
- package/src/config/loader/presets.ts +0 -119
- package/src/config/loader/refs.ts +0 -100
- package/src/config/loader/reserved-namespace.ts +0 -38
- package/src/config/loader/segments.ts +0 -120
- package/src/config/loader/validate-core.ts +0 -737
- package/src/config/loader/variables.ts +0 -260
- package/src/config/menu-keys.ts +0 -139
- package/src/config/option-domain.ts +0 -164
- package/src/config/presets.ts +0 -326
- package/src/config/settings-menu.ts +0 -775
- package/src/daemon/acquire.ts +0 -684
- package/src/daemon/cache/git.ts +0 -649
- package/src/daemon/cache/render.ts +0 -623
- package/src/daemon/cache/session-usage-store.ts +0 -720
- package/src/daemon/cache/watchers.ts +0 -249
- package/src/daemon/client-debug.ts +0 -120
- package/src/daemon/client-stats.ts +0 -130
- package/src/daemon/client-transport.ts +0 -273
- package/src/daemon/client.ts +0 -78
- package/src/daemon/config-overrides-store.ts +0 -663
- package/src/daemon/debug-types.ts +0 -91
- package/src/daemon/debug.ts +0 -264
- package/src/daemon/fork-bomb-breaker.ts +0 -351
- package/src/daemon/limits.ts +0 -211
- package/src/daemon/log.ts +0 -81
- package/src/daemon/parent-watchdog.ts +0 -87
- package/src/daemon/paths.ts +0 -211
- package/src/daemon/process-fingerprint.ts +0 -146
- package/src/daemon/protocol.ts +0 -292
- package/src/daemon/render-payload.ts +0 -1256
- package/src/daemon/server.ts +0 -1330
- package/src/daemon/session-state-file.ts +0 -108
- package/src/daemon/session-state.ts +0 -237
- package/src/daemon/socket-lease.ts +0 -209
- package/src/daemon/socket-ownership.ts +0 -209
- package/src/daemon/stats.ts +0 -235
- package/src/daemon/verbs/config-validators.ts +0 -250
- package/src/daemon/verbs/index.ts +0 -706
- package/src/daemon/verbs/state-validators.ts +0 -249
- package/src/daemon/verbs/validator-registry.ts +0 -457
- package/src/demo/dsl.ts +0 -143
- package/src/demo/mock-data.ts +0 -67
- package/src/demo/statusline.json5 +0 -94
- package/src/dsl/node-registry.ts +0 -374
- package/src/dsl/render.ts +0 -803
- package/src/help-text.ts +0 -90
- package/src/index.ts +0 -210
- package/src/install/index.ts +0 -541
- package/src/proc/launch.ts +0 -459
- package/src/proc/stats-handle.ts +0 -13
- package/src/render/action.ts +0 -883
- package/src/render/active-segment.ts +0 -78
- package/src/render/diagnostic-style.ts +0 -23
- package/src/render/diagnostic-text.ts +0 -77
- package/src/render/error-glyph.ts +0 -53
- package/src/render/menu.ts +0 -257
- package/src/render/outcome-plan.ts +0 -45
- package/src/render/picker.ts +0 -372
- package/src/render/segment-color.ts +0 -74
- package/src/render/split-lines.ts +0 -51
- package/src/render/strip.ts +0 -228
- package/src/segments/cache.ts +0 -131
- package/src/segments/context.ts +0 -190
- package/src/segments/git.ts +0 -1084
- package/src/segments/metrics.ts +0 -187
- package/src/segments/pricing.ts +0 -452
- package/src/segments/session.ts +0 -23
- package/src/segments/tmux.ts +0 -74
- package/src/template-engine/cells.ts +0 -90
- package/src/template-engine/colors.ts +0 -124
- package/src/template-engine/engine.ts +0 -108
- package/src/template-engine/funcs.ts +0 -232
- package/src/template-engine/index.ts +0 -11
- package/src/template-engine/layout.ts +0 -133
- package/src/template-engine/scope.ts +0 -62
- package/src/template-engine/sparkline.ts +0 -79
- package/src/themes/index.ts +0 -20
- package/src/themes/palette-resolvers.ts +0 -84
- package/src/themes/policy.ts +0 -393
- package/src/utils/cache.ts +0 -206
- package/src/utils/claude.ts +0 -683
- package/src/utils/color-support.ts +0 -118
- package/src/utils/formatters.ts +0 -99
- package/src/utils/logger.ts +0 -5
- package/src/utils/outcome.ts +0 -33
- package/src/utils/schema-validator.ts +0 -126
- package/src/utils/single-flight.ts +0 -57
- package/src/utils/terminal-width.ts +0 -51
- package/src/utils/terminal.ts +0 -11
- package/src/utils/transcript-fs.ts +0 -279
- package/src/var-system/index.ts +0 -24
- package/src/var-system/sources.ts +0 -1047
- package/src/var-system/store.ts +0 -223
- package/src/var-system/types.ts +0 -57
- package/src/version.ts +0 -17
package/src/segments/metrics.ts
DELETED
|
@@ -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
|
-
}
|
package/src/segments/pricing.ts
DELETED
|
@@ -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
|
-
}
|
package/src/segments/session.ts
DELETED
|
@@ -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
|
-
}
|