@abnersajr/claude-timeline 1.0.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/dist/capture.js +140 -0
  4. package/dist/classifier.d.ts +37 -0
  5. package/dist/classifier.d.ts.map +1 -0
  6. package/dist/classifier.test.d.ts +2 -0
  7. package/dist/classifier.test.d.ts.map +1 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +1328 -0
  10. package/dist/context-tracker.d.ts +44 -0
  11. package/dist/context-tracker.d.ts.map +1 -0
  12. package/dist/context-tracker.test.d.ts +2 -0
  13. package/dist/context-tracker.test.d.ts.map +1 -0
  14. package/dist/conversation-groups.d.ts +11 -0
  15. package/dist/conversation-groups.d.ts.map +1 -0
  16. package/dist/conversation-groups.test.d.ts +2 -0
  17. package/dist/conversation-groups.test.d.ts.map +1 -0
  18. package/dist/cost-stream-capture.d.ts +47 -0
  19. package/dist/cost-stream-capture.d.ts.map +1 -0
  20. package/dist/cost-stream-db.d.ts +87 -0
  21. package/dist/cost-stream-db.d.ts.map +1 -0
  22. package/dist/cost-stream-merger.d.ts +38 -0
  23. package/dist/cost-stream-merger.d.ts.map +1 -0
  24. package/dist/db-reader-BrPRGqww.mjs +1028 -0
  25. package/dist/db-reader-BrPRGqww.mjs.map +1 -0
  26. package/dist/db-reader-CPXmkt55.mjs +2 -0
  27. package/dist/db-reader.d.ts +58 -0
  28. package/dist/db-reader.d.ts.map +1 -0
  29. package/dist/db.js +100 -0
  30. package/dist/dedup.d.ts +16 -0
  31. package/dist/dedup.d.ts.map +1 -0
  32. package/dist/dedup.test.d.ts +2 -0
  33. package/dist/dedup.test.d.ts.map +1 -0
  34. package/dist/index.d.ts +20 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/jsonl-parser.d.ts +14 -0
  37. package/dist/jsonl-parser.d.ts.map +1 -0
  38. package/dist/jsonl-parser.test.d.ts +2 -0
  39. package/dist/jsonl-parser.test.d.ts.map +1 -0
  40. package/dist/merger.d.ts +31 -0
  41. package/dist/merger.d.ts.map +1 -0
  42. package/dist/model-parser.d.ts +25 -0
  43. package/dist/model-parser.d.ts.map +1 -0
  44. package/dist/model-parser.test.d.ts +2 -0
  45. package/dist/model-parser.test.d.ts.map +1 -0
  46. package/dist/noise-filter.d.ts +6 -0
  47. package/dist/noise-filter.d.ts.map +1 -0
  48. package/dist/pricing-B-rwfwDB.mjs +2 -0
  49. package/dist/pricing-DTmya3JY.mjs +273 -0
  50. package/dist/pricing-DTmya3JY.mjs.map +1 -0
  51. package/dist/pricing.d.ts +26 -0
  52. package/dist/pricing.d.ts.map +1 -0
  53. package/dist/server.cjs +31237 -0
  54. package/dist/session-state.d.ts +19 -0
  55. package/dist/session-state.d.ts.map +1 -0
  56. package/dist/session-state.test.d.ts +2 -0
  57. package/dist/session-state.test.d.ts.map +1 -0
  58. package/dist/subagent-locator.d.ts +30 -0
  59. package/dist/subagent-locator.d.ts.map +1 -0
  60. package/dist/subagent-locator.test.d.ts +2 -0
  61. package/dist/subagent-locator.test.d.ts.map +1 -0
  62. package/dist/subagent-resolver.d.ts +35 -0
  63. package/dist/subagent-resolver.d.ts.map +1 -0
  64. package/dist/subagent-resolver.test.d.ts +2 -0
  65. package/dist/subagent-resolver.test.d.ts.map +1 -0
  66. package/dist/tool-extraction.d.ts +34 -0
  67. package/dist/tool-extraction.d.ts.map +1 -0
  68. package/dist/tool-extraction.test.d.ts +2 -0
  69. package/dist/tool-extraction.test.d.ts.map +1 -0
  70. package/dist/tool-matcher.d.ts +35 -0
  71. package/dist/tool-matcher.d.ts.map +1 -0
  72. package/dist/types.d.ts +272 -0
  73. package/dist/types.d.ts.map +1 -0
  74. package/dist/utils.d.ts +24 -0
  75. package/dist/utils.d.ts.map +1 -0
  76. package/dist/web/assets/index-Dr0FGYfS.js +158 -0
  77. package/dist/web/assets/index-nXTIEelb.css +1 -0
  78. package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  79. package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  80. package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  81. package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  82. package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  83. package/dist/web/favicon-light.svg +14 -0
  84. package/dist/web/favicon.svg +14 -0
  85. package/dist/web/index.html +14 -0
  86. package/dist/web/logo.svg +20 -0
  87. package/package.json +73 -0
@@ -0,0 +1,273 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ //#region src/model-parser.ts
5
+ /**
6
+ * Model name parser for multi-model sessions.
7
+ *
8
+ * Strips provider prefixes and date suffixes from raw model strings
9
+ * to produce a normalized key suitable for pricing lookups.
10
+ *
11
+ * Examples:
12
+ * "anthropic/claude-sonnet-4-20250514" → "claude-sonnet-4"
13
+ * "claude-opus-4-20250514" → "claude-opus-4"
14
+ * "claude-sonnet-4-6" → "claude-sonnet-4-6"
15
+ */
16
+ /** Date suffix pattern: 8-digit date at the end of the string */
17
+ const DATE_SUFFIX_RE = /^(.+)-\d{8}$/;
18
+ /**
19
+ * Parse a raw model string into a normalized form for pricing lookups.
20
+ *
21
+ * - Strips provider prefix (e.g. "anthropic/")
22
+ * - Strips date suffix (e.g. "-20250514")
23
+ * - Lowercases the result
24
+ * - Returns "unknown" for null/undefined/empty/whitespace
25
+ */
26
+ function parseModelName(raw) {
27
+ if (!raw) return "unknown";
28
+ const trimmed = raw.trim();
29
+ if (trimmed === "") return "unknown";
30
+ const slashIndex = trimmed.indexOf("/");
31
+ const withoutPrefix = slashIndex >= 0 ? trimmed.slice(slashIndex + 1) : trimmed;
32
+ const dateMatch = DATE_SUFFIX_RE.exec(withoutPrefix);
33
+ return (dateMatch ? dateMatch[1] : withoutPrefix).toLowerCase();
34
+ }
35
+ /**
36
+ * Alias for parseModelName — ensures consistent key format for pricing lookup.
37
+ */
38
+ const normalizeModelName = parseModelName;
39
+ //#endregion
40
+ //#region src/pricing.ts
41
+ const PRICING_DIR = join(homedir(), ".claude-timeline");
42
+ const PRICING_PATH = join(PRICING_DIR, "pricing.json");
43
+ const OPENROUTER_API = "https://openrouter.ai/api/v1/models";
44
+ const FALLBACK_TABLE = {
45
+ "claude-opus-4-8": {
46
+ model: "claude-opus-4-8",
47
+ inputPerMTok: 5,
48
+ outputPerMTok: 25,
49
+ cacheReadPerMTok: .5,
50
+ cacheWritePerMTok: 6.25
51
+ },
52
+ "claude-opus-4-7": {
53
+ model: "claude-opus-4-7",
54
+ inputPerMTok: 5,
55
+ outputPerMTok: 25,
56
+ cacheReadPerMTok: .5,
57
+ cacheWritePerMTok: 6.25
58
+ },
59
+ "claude-sonnet-4-6": {
60
+ model: "claude-sonnet-4-6",
61
+ inputPerMTok: 3,
62
+ outputPerMTok: 15,
63
+ cacheReadPerMTok: .3,
64
+ cacheWritePerMTok: 3.75
65
+ },
66
+ "claude-haiku-4-5": {
67
+ model: "claude-haiku-4-5",
68
+ inputPerMTok: 1,
69
+ outputPerMTok: 5,
70
+ cacheReadPerMTok: .1,
71
+ cacheWritePerMTok: 1.25
72
+ },
73
+ "claude-opus-4-6": {
74
+ model: "claude-opus-4-6",
75
+ inputPerMTok: 5,
76
+ outputPerMTok: 25,
77
+ cacheReadPerMTok: .5,
78
+ cacheWritePerMTok: 6.25
79
+ },
80
+ "claude-opus-4-5": {
81
+ model: "claude-opus-4-5",
82
+ inputPerMTok: 5,
83
+ outputPerMTok: 25,
84
+ cacheReadPerMTok: .5,
85
+ cacheWritePerMTok: 6.25
86
+ },
87
+ "claude-opus-4-1": {
88
+ model: "claude-opus-4-1",
89
+ inputPerMTok: 15,
90
+ outputPerMTok: 75,
91
+ cacheReadPerMTok: 1.5,
92
+ cacheWritePerMTok: 18.75
93
+ },
94
+ "claude-opus-4": {
95
+ model: "claude-opus-4",
96
+ inputPerMTok: 15,
97
+ outputPerMTok: 75,
98
+ cacheReadPerMTok: 1.5,
99
+ cacheWritePerMTok: 18.75
100
+ },
101
+ "claude-sonnet-4-5": {
102
+ model: "claude-sonnet-4-5",
103
+ inputPerMTok: 3,
104
+ outputPerMTok: 15,
105
+ cacheReadPerMTok: .3,
106
+ cacheWritePerMTok: 3.75
107
+ },
108
+ "claude-sonnet-4": {
109
+ model: "claude-sonnet-4",
110
+ inputPerMTok: 3,
111
+ outputPerMTok: 15,
112
+ cacheReadPerMTok: .3,
113
+ cacheWritePerMTok: 3.75
114
+ },
115
+ "claude-sonnet-3-7": {
116
+ model: "claude-sonnet-3-7",
117
+ inputPerMTok: 3,
118
+ outputPerMTok: 15,
119
+ cacheReadPerMTok: .3,
120
+ cacheWritePerMTok: 3.75
121
+ },
122
+ "claude-haiku-3-5": {
123
+ model: "claude-haiku-3-5",
124
+ inputPerMTok: .8,
125
+ outputPerMTok: 4,
126
+ cacheReadPerMTok: .08,
127
+ cacheWritePerMTok: 1
128
+ },
129
+ "claude-haiku-3": {
130
+ model: "claude-haiku-3",
131
+ inputPerMTok: .25,
132
+ outputPerMTok: 1.25,
133
+ cacheReadPerMTok: .03,
134
+ cacheWritePerMTok: .3
135
+ }
136
+ };
137
+ /** Normalize OpenRouter model ID to our internal format */
138
+ function normalizeOpenRouterId(id) {
139
+ return id.replace(/^anthropic\//, "").replace(/\./g, "-").toLowerCase();
140
+ }
141
+ /** Parse OpenRouter pricing string to number, returns NaN if invalid */
142
+ function parsePrice(s) {
143
+ if (!s) return NaN;
144
+ const n = Number.parseFloat(s);
145
+ return Number.isFinite(n) ? n : NaN;
146
+ }
147
+ /** Fetch pricing from OpenRouter API */
148
+ async function fetchFromOpenRouter() {
149
+ const res = await fetch(OPENROUTER_API, { headers: {
150
+ "User-Agent": "claude-timeline/1.0",
151
+ Accept: "application/json"
152
+ } });
153
+ if (!res.ok) throw new Error(`OpenRouter API error: ${res.status} ${res.statusText}`);
154
+ const json = await res.json();
155
+ const models = {};
156
+ for (const item of json.data) {
157
+ if (!item.id.startsWith("anthropic/claude")) continue;
158
+ const prompt = parsePrice(item.pricing.prompt);
159
+ const completion = parsePrice(item.pricing.completion);
160
+ const cacheRead = parsePrice(item.pricing.input_cache_read);
161
+ const cacheWrite = parsePrice(item.pricing.input_cache_write);
162
+ if ([
163
+ prompt,
164
+ completion,
165
+ cacheRead,
166
+ cacheWrite
167
+ ].some(Number.isNaN)) continue;
168
+ const model = normalizeOpenRouterId(item.id);
169
+ models[model] = {
170
+ model,
171
+ inputPerMTok: prompt * 1e6,
172
+ outputPerMTok: completion * 1e6,
173
+ cacheReadPerMTok: cacheRead * 1e6,
174
+ cacheWritePerMTok: cacheWrite * 1e6
175
+ };
176
+ }
177
+ if (Object.keys(models).length === 0) throw new Error("No Claude models found in OpenRouter response");
178
+ return models;
179
+ }
180
+ /** Save pricing file to disk */
181
+ function savePricingFile(models) {
182
+ if (!existsSync(PRICING_DIR)) mkdirSync(PRICING_DIR, { recursive: true });
183
+ const file = {
184
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
185
+ models
186
+ };
187
+ writeFileSync(PRICING_PATH, JSON.stringify(file, null, 2) + "\n", "utf-8");
188
+ }
189
+ /** Pricing table — loaded once at module init */
190
+ let PRICING_TABLE = FALLBACK_TABLE;
191
+ /** Force refresh pricing from OpenRouter (used by update-pricing CLI) */
192
+ async function refreshPricing() {
193
+ const models = await fetchFromOpenRouter();
194
+ savePricingFile(models);
195
+ PRICING_TABLE = models;
196
+ return models;
197
+ }
198
+ /**
199
+ * Look up pricing for a model.
200
+ * Falls back to claude-sonnet-4-6 rates if unknown.
201
+ */
202
+ function getPricing(modelName) {
203
+ const normalized = normalizeModelName(modelName);
204
+ const rate = PRICING_TABLE[normalized];
205
+ if (rate) return rate;
206
+ console.warn(`Unknown model "${normalized}" (raw: "${modelName}"), falling back to claude-sonnet-4-6 rates`);
207
+ return {
208
+ ...PRICING_TABLE["claude-sonnet-4-6"],
209
+ model: normalized
210
+ };
211
+ }
212
+ /**
213
+ * Calculate per-turn cost breakdown.
214
+ * Uses turn-level model detection when available, otherwise falls back to session default rate.
215
+ */
216
+ function calculateTurnCost(turn, sessionRate) {
217
+ const rate = turn.model ? getPricing(normalizeModelName(turn.model)) : sessionRate;
218
+ const inputCost = turn.tokenUsage.inputTokens / 1e6 * rate.inputPerMTok;
219
+ const outputCost = turn.tokenUsage.outputTokens / 1e6 * rate.outputPerMTok;
220
+ const cacheReadCost = turn.tokenUsage.cacheReadTokens / 1e6 * rate.cacheReadPerMTok;
221
+ const cacheWriteCost = (turn.tokenUsage.cacheCreation5mTokens + turn.tokenUsage.cacheCreation1hTokens) / 1e6 * rate.cacheWritePerMTok;
222
+ return {
223
+ inputCost,
224
+ outputCost,
225
+ cacheReadCost,
226
+ cacheWriteCost,
227
+ totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost
228
+ };
229
+ }
230
+ /**
231
+ * Calculate full session pricing from session metadata and turns
232
+ */
233
+ function calculateSessionCost(session, turns) {
234
+ const rate = getPricing(normalizeModelName(session.model));
235
+ const turnsPricing = turns.map((turn) => calculateTurnCost(turn, rate));
236
+ const totalCost = turnsPricing.reduce((sum, t) => sum + t.totalCost, 0);
237
+ const modelBreakdown = {};
238
+ for (let i = 0; i < turns.length; i++) {
239
+ const turn = turns[i];
240
+ const pricing = turnsPricing[i];
241
+ const model = normalizeModelName(turn.model ?? session.model);
242
+ if (!modelBreakdown[model]) modelBreakdown[model] = {
243
+ inputTokens: 0,
244
+ outputTokens: 0,
245
+ cacheReadTokens: 0,
246
+ cacheCreationTokens: 0,
247
+ cost: 0,
248
+ turnCount: 0
249
+ };
250
+ const entry = modelBreakdown[model];
251
+ entry.inputTokens += turn.tokenUsage.inputTokens;
252
+ entry.outputTokens += turn.tokenUsage.outputTokens;
253
+ entry.cacheReadTokens += turn.tokenUsage.cacheReadTokens;
254
+ entry.cacheCreationTokens += turn.tokenUsage.cacheCreation5mTokens + turn.tokenUsage.cacheCreation1hTokens;
255
+ entry.cost += pricing.totalCost;
256
+ entry.turnCount++;
257
+ }
258
+ return {
259
+ estimatedTotalCost: totalCost,
260
+ turnsPricing,
261
+ apiTotalCost: null,
262
+ apiSnapshotCount: 0,
263
+ apiLastSnapshotAt: null,
264
+ totalCost,
265
+ costSource: "estimated",
266
+ modelBreakdown,
267
+ pricingRate: rate
268
+ };
269
+ }
270
+ //#endregion
271
+ export { refreshPricing as a, getPricing as i, calculateTurnCost as n, savePricingFile as o, fetchFromOpenRouter as r, normalizeModelName as s, calculateSessionCost as t };
272
+
273
+ //# sourceMappingURL=pricing-DTmya3JY.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing-DTmya3JY.mjs","names":[],"sources":["../packages/extractor/src/model-parser.ts","../packages/extractor/src/pricing.ts"],"sourcesContent":["/**\n * Model name parser for multi-model sessions.\n *\n * Strips provider prefixes and date suffixes from raw model strings\n * to produce a normalized key suitable for pricing lookups.\n *\n * Examples:\n * \"anthropic/claude-sonnet-4-20250514\" → \"claude-sonnet-4\"\n * \"claude-opus-4-20250514\" → \"claude-opus-4\"\n * \"claude-sonnet-4-6\" → \"claude-sonnet-4-6\"\n */\n\n/** Date suffix pattern: 8-digit date at the end of the string */\nconst DATE_SUFFIX_RE = /^(.+)-\\d{8}$/\n\n/**\n * Parse a raw model string into a normalized form for pricing lookups.\n *\n * - Strips provider prefix (e.g. \"anthropic/\")\n * - Strips date suffix (e.g. \"-20250514\")\n * - Lowercases the result\n * - Returns \"unknown\" for null/undefined/empty/whitespace\n */\nexport function parseModelName(raw: string | null | undefined): string {\n if (!raw) return \"unknown\"\n\n const trimmed = raw.trim()\n if (trimmed === \"\") return \"unknown\"\n\n // Strip provider prefix (e.g. \"anthropic/\")\n const slashIndex = trimmed.indexOf(\"/\")\n const withoutPrefix = slashIndex >= 0 ? trimmed.slice(slashIndex + 1) : trimmed\n\n // Strip date suffix (e.g. \"-20250514\")\n const dateMatch = DATE_SUFFIX_RE.exec(withoutPrefix)\n const withoutDate = dateMatch ? dateMatch[1] : withoutPrefix\n\n return withoutDate.toLowerCase()\n}\n\n/**\n * Alias for parseModelName — ensures consistent key format for pricing lookup.\n */\nexport const normalizeModelName = parseModelName\n","import { existsSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport { homedir } from \"node:os\"\nimport { normalizeModelName } from \"./model-parser.js\"\nimport type { PricingFile, PricingRate, SessionMetadata, SessionPricing, Turn, TurnPricing } from \"./types.js\"\n\n// ── Constants ──────────────────────────────────────────────────────────\nconst PRICING_DIR = join(homedir(), \".claude-timeline\")\nconst PRICING_PATH = join(PRICING_DIR, \"pricing.json\")\nconst STALE_MS = 5 * 24 * 60 * 60 * 1000 // 5 days\nconst OPENROUTER_API = \"https://openrouter.ai/api/v1/models\"\n\n// ── Fallback table (compiled in) ────────────────────────────────────────\nconst FALLBACK_TABLE: Record<string, PricingRate> = {\n \"claude-opus-4-8\": {\n model: \"claude-opus-4-8\",\n inputPerMTok: 5.0,\n outputPerMTok: 25.0,\n cacheReadPerMTok: 0.5,\n cacheWritePerMTok: 6.25,\n },\n \"claude-opus-4-7\": {\n model: \"claude-opus-4-7\",\n inputPerMTok: 5.0,\n outputPerMTok: 25.0,\n cacheReadPerMTok: 0.5,\n cacheWritePerMTok: 6.25,\n },\n \"claude-sonnet-4-6\": {\n model: \"claude-sonnet-4-6\",\n inputPerMTok: 3.0,\n outputPerMTok: 15.0,\n cacheReadPerMTok: 0.3,\n cacheWritePerMTok: 3.75,\n },\n \"claude-haiku-4-5\": {\n model: \"claude-haiku-4-5\",\n inputPerMTok: 1.0,\n outputPerMTok: 5.0,\n cacheReadPerMTok: 0.1,\n cacheWritePerMTok: 1.25,\n },\n \"claude-opus-4-6\": {\n model: \"claude-opus-4-6\",\n inputPerMTok: 5.0,\n outputPerMTok: 25.0,\n cacheReadPerMTok: 0.5,\n cacheWritePerMTok: 6.25,\n },\n \"claude-opus-4-5\": {\n model: \"claude-opus-4-5\",\n inputPerMTok: 5.0,\n outputPerMTok: 25.0,\n cacheReadPerMTok: 0.5,\n cacheWritePerMTok: 6.25,\n },\n \"claude-opus-4-1\": {\n model: \"claude-opus-4-1\",\n inputPerMTok: 15.0,\n outputPerMTok: 75.0,\n cacheReadPerMTok: 1.5,\n cacheWritePerMTok: 18.75,\n },\n \"claude-opus-4\": {\n model: \"claude-opus-4\",\n inputPerMTok: 15.0,\n outputPerMTok: 75.0,\n cacheReadPerMTok: 1.5,\n cacheWritePerMTok: 18.75,\n },\n \"claude-sonnet-4-5\": {\n model: \"claude-sonnet-4-5\",\n inputPerMTok: 3.0,\n outputPerMTok: 15.0,\n cacheReadPerMTok: 0.3,\n cacheWritePerMTok: 3.75,\n },\n \"claude-sonnet-4\": {\n model: \"claude-sonnet-4\",\n inputPerMTok: 3.0,\n outputPerMTok: 15.0,\n cacheReadPerMTok: 0.3,\n cacheWritePerMTok: 3.75,\n },\n \"claude-sonnet-3-7\": {\n model: \"claude-sonnet-3-7\",\n inputPerMTok: 3.0,\n outputPerMTok: 15.0,\n cacheReadPerMTok: 0.3,\n cacheWritePerMTok: 3.75,\n },\n \"claude-haiku-3-5\": {\n model: \"claude-haiku-3-5\",\n inputPerMTok: 0.8,\n outputPerMTok: 4.0,\n cacheReadPerMTok: 0.08,\n cacheWritePerMTok: 1.0,\n },\n \"claude-haiku-3\": {\n model: \"claude-haiku-3\",\n inputPerMTok: 0.25,\n outputPerMTok: 1.25,\n cacheReadPerMTok: 0.03,\n cacheWritePerMTok: 0.3,\n },\n}\n\n// ── OpenRouter API ─────────────────────────────────────────────────────\n\ninterface OpenRouterModel {\n id: string\n name: string\n pricing: {\n prompt: string\n completion: string\n input_cache_read?: string\n input_cache_write?: string\n }\n}\n\n/** Normalize OpenRouter model ID to our internal format */\nfunction normalizeOpenRouterId(id: string): string {\n return id\n .replace(/^anthropic\\//, \"\") // strip provider prefix\n .replace(/\\./g, \"-\") // dots → hyphens\n .toLowerCase()\n}\n\n/** Parse OpenRouter pricing string to number, returns NaN if invalid */\nfunction parsePrice(s: string | undefined): number {\n if (!s) return Number.NaN\n const n = Number.parseFloat(s)\n return Number.isFinite(n) ? n : Number.NaN\n}\n\n/** Fetch pricing from OpenRouter API */\nexport async function fetchFromOpenRouter(): Promise<Record<string, PricingRate>> {\n const res = await fetch(OPENROUTER_API, {\n headers: {\n \"User-Agent\": \"claude-timeline/1.0\",\n Accept: \"application/json\",\n },\n })\n\n if (!res.ok) {\n throw new Error(`OpenRouter API error: ${res.status} ${res.statusText}`)\n }\n\n const json = (await res.json()) as { data: OpenRouterModel[] }\n const models: Record<string, PricingRate> = {}\n\n for (const item of json.data) {\n // Only include Anthropic/Claude models\n if (!item.id.startsWith(\"anthropic/claude\")) continue\n\n const prompt = parsePrice(item.pricing.prompt)\n const completion = parsePrice(item.pricing.completion)\n const cacheRead = parsePrice(item.pricing.input_cache_read)\n const cacheWrite = parsePrice(item.pricing.input_cache_write)\n\n // Skip if any required price is missing\n if ([prompt, completion, cacheRead, cacheWrite].some(Number.isNaN)) continue\n\n const model = normalizeOpenRouterId(item.id)\n models[model] = {\n model,\n inputPerMTok: prompt * 1_000_000,\n outputPerMTok: completion * 1_000_000,\n cacheReadPerMTok: cacheRead * 1_000_000,\n cacheWritePerMTok: cacheWrite * 1_000_000,\n }\n }\n\n if (Object.keys(models).length === 0) {\n throw new Error(\"No Claude models found in OpenRouter response\")\n }\n\n return models\n}\n\n// ── Cache Management ───────────────────────────────────────────────────\n\n/** Check if cached pricing data is stale (older than 5 days) */\nexport function isCacheStale(data: PricingFile): boolean {\n const fetchedAt = new Date(data.fetchedAt).getTime()\n return Date.now() - fetchedAt > STALE_MS\n}\n\n/** Load pricing file from disk, returns null if missing or invalid */\nfunction loadPricingFile(): PricingFile | null {\n try {\n if (!existsSync(PRICING_PATH)) return null\n const raw = readFileSync(PRICING_PATH, \"utf-8\")\n const data = JSON.parse(raw) as PricingFile\n if (!data.fetchedAt || !data.models || Object.keys(data.models).length === 0) return null\n return data\n } catch {\n return null\n }\n}\n\n/** Save pricing file to disk */\nexport function savePricingFile(models: Record<string, PricingRate>): void {\n if (!existsSync(PRICING_DIR)) mkdirSync(PRICING_DIR, { recursive: true })\n const file: PricingFile = {\n fetchedAt: new Date().toISOString(),\n models,\n }\n writeFileSync(PRICING_PATH, JSON.stringify(file, null, 2) + \"\\n\", \"utf-8\")\n}\n\n// ── Main Pricing Logic ─────────────────────────────────────────────────\n\n/** Pricing table — loaded once at module init */\nlet PRICING_TABLE: Record<string, PricingRate> = FALLBACK_TABLE\n\n/** Initialize pricing: load from cache or fetch from OpenRouter */\nexport async function initPricing(): Promise<void> {\n const cached = loadPricingFile()\n\n if (cached && !isCacheStale(cached)) {\n PRICING_TABLE = cached.models\n return\n }\n\n try {\n PRICING_TABLE = await fetchFromOpenRouter()\n savePricingFile(PRICING_TABLE)\n } catch (err) {\n console.warn(`Failed to fetch pricing from OpenRouter: ${(err as Error).message}`)\n if (cached) {\n console.warn(\"Using stale cached pricing data\")\n PRICING_TABLE = cached.models\n } else {\n console.warn(\"Using compiled-in fallback pricing\")\n }\n }\n}\n\n/** Force refresh pricing from OpenRouter (used by update-pricing CLI) */\nexport async function refreshPricing(): Promise<Record<string, PricingRate>> {\n const models = await fetchFromOpenRouter()\n savePricingFile(models)\n PRICING_TABLE = models\n return models\n}\n\n/**\n * Look up pricing for a model.\n * Falls back to claude-sonnet-4-6 rates if unknown.\n */\nexport function getPricing(modelName: string): PricingRate {\n const normalized = normalizeModelName(modelName)\n const rate = PRICING_TABLE[normalized]\n if (rate) return rate\n\n console.warn(`Unknown model \"${normalized}\" (raw: \"${modelName}\"), falling back to claude-sonnet-4-6 rates`)\n return { ...PRICING_TABLE[\"claude-sonnet-4-6\"], model: normalized }\n}\n\n/**\n * Calculate per-turn cost breakdown.\n * Uses turn-level model detection when available, otherwise falls back to session default rate.\n */\nexport function calculateTurnCost(turn: Turn, sessionRate: PricingRate): TurnPricing {\n const rate = turn.model\n ? getPricing(normalizeModelName(turn.model))\n : sessionRate\n\n const inputCost = (turn.tokenUsage.inputTokens / 1_000_000) * rate.inputPerMTok\n const outputCost = (turn.tokenUsage.outputTokens / 1_000_000) * rate.outputPerMTok\n const cacheReadCost = (turn.tokenUsage.cacheReadTokens / 1_000_000) * rate.cacheReadPerMTok\n const cacheWriteTokens = turn.tokenUsage.cacheCreation5mTokens + turn.tokenUsage.cacheCreation1hTokens\n const cacheWriteCost = (cacheWriteTokens / 1_000_000) * rate.cacheWritePerMTok\n const totalCost = inputCost + outputCost + cacheReadCost + cacheWriteCost\n\n return {\n inputCost,\n outputCost,\n cacheReadCost,\n cacheWriteCost,\n totalCost,\n }\n}\n\n/**\n * Calculate full session pricing from session metadata and turns\n */\nexport function calculateSessionCost(session: SessionMetadata, turns: Turn[]): SessionPricing {\n const rate = getPricing(normalizeModelName(session.model))\n const turnsPricing = turns.map((turn) => calculateTurnCost(turn, rate))\n const totalCost = turnsPricing.reduce((sum, t) => sum + t.totalCost, 0)\n\n // Compute per-model breakdown\n const modelBreakdown: SessionPricing[\"modelBreakdown\"] = {}\n for (let i = 0; i < turns.length; i++) {\n const turn = turns[i]\n const pricing = turnsPricing[i]\n const model = normalizeModelName(turn.model ?? session.model)\n\n if (!modelBreakdown[model]) {\n modelBreakdown[model] = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheCreationTokens: 0,\n cost: 0,\n turnCount: 0,\n }\n }\n\n const entry = modelBreakdown[model]\n entry.inputTokens += turn.tokenUsage.inputTokens\n entry.outputTokens += turn.tokenUsage.outputTokens\n entry.cacheReadTokens += turn.tokenUsage.cacheReadTokens\n entry.cacheCreationTokens += turn.tokenUsage.cacheCreation5mTokens + turn.tokenUsage.cacheCreation1hTokens\n entry.cost += pricing.totalCost\n entry.turnCount++\n }\n\n return {\n estimatedTotalCost: totalCost,\n turnsPricing,\n apiTotalCost: null,\n apiSnapshotCount: 0,\n apiLastSnapshotAt: null,\n totalCost,\n costSource: \"estimated\",\n modelBreakdown,\n pricingRate: rate,\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { PricingFile, PricingRate, SessionMetadata, SessionPricing, Turn, TurnPricing } from "./types.js";
2
+ /** Fetch pricing from OpenRouter API */
3
+ export declare function fetchFromOpenRouter(): Promise<Record<string, PricingRate>>;
4
+ /** Check if cached pricing data is stale (older than 5 days) */
5
+ export declare function isCacheStale(data: PricingFile): boolean;
6
+ /** Save pricing file to disk */
7
+ export declare function savePricingFile(models: Record<string, PricingRate>): void;
8
+ /** Initialize pricing: load from cache or fetch from OpenRouter */
9
+ export declare function initPricing(): Promise<void>;
10
+ /** Force refresh pricing from OpenRouter (used by update-pricing CLI) */
11
+ export declare function refreshPricing(): Promise<Record<string, PricingRate>>;
12
+ /**
13
+ * Look up pricing for a model.
14
+ * Falls back to claude-sonnet-4-6 rates if unknown.
15
+ */
16
+ export declare function getPricing(modelName: string): PricingRate;
17
+ /**
18
+ * Calculate per-turn cost breakdown.
19
+ * Uses turn-level model detection when available, otherwise falls back to session default rate.
20
+ */
21
+ export declare function calculateTurnCost(turn: Turn, sessionRate: PricingRate): TurnPricing;
22
+ /**
23
+ * Calculate full session pricing from session metadata and turns
24
+ */
25
+ export declare function calculateSessionCost(session: SessionMetadata, turns: Turn[]): SessionPricing;
26
+ //# sourceMappingURL=pricing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAmI9G,wCAAwC;AACxC,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CA0ChF;AAID,gEAAgE;AAChE,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAGvD;AAeD,gCAAgC;AAChC,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,IAAI,CAOzE;AAOD,mEAAmE;AACnE,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAoBjD;AAED,yEAAyE;AACzE,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAK3E;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,CAOzD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,GAAG,WAAW,CAmBnF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,cAAc,CA2C5F"}