@hasna/economy 0.2.17 → 0.2.19

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 (86) hide show
  1. package/LICENSE +187 -13
  2. package/README.md +199 -13
  3. package/dist/cli/commands/completion.d.ts +2 -0
  4. package/dist/cli/commands/completion.d.ts.map +1 -0
  5. package/dist/cli/commands/extras.d.ts +4 -0
  6. package/dist/cli/commands/extras.d.ts.map +1 -0
  7. package/dist/cli/commands/menubar.d.ts.map +1 -1
  8. package/dist/cli/commands/notification.d.ts +8 -0
  9. package/dist/cli/commands/notification.d.ts.map +1 -0
  10. package/dist/cli/commands/todos.d.ts +26 -0
  11. package/dist/cli/commands/todos.d.ts.map +1 -0
  12. package/dist/cli/commands/tui.d.ts +10 -0
  13. package/dist/cli/commands/tui.d.ts.map +1 -0
  14. package/dist/cli/commands/watch.d.ts +1 -0
  15. package/dist/cli/commands/watch.d.ts.map +1 -1
  16. package/dist/cli/index.js +4845 -1001
  17. package/dist/db/database.d.ts +19 -1
  18. package/dist/db/database.d.ts.map +1 -1
  19. package/dist/db/pg-migrations.d.ts.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1023 -108
  23. package/dist/ingest/billing.d.ts +9 -0
  24. package/dist/ingest/billing.d.ts.map +1 -1
  25. package/dist/ingest/claude-quota.d.ts +5 -0
  26. package/dist/ingest/claude-quota.d.ts.map +1 -0
  27. package/dist/ingest/claude.d.ts +8 -2
  28. package/dist/ingest/claude.d.ts.map +1 -1
  29. package/dist/ingest/codex-quota.d.ts +5 -0
  30. package/dist/ingest/codex-quota.d.ts.map +1 -0
  31. package/dist/ingest/codex.d.ts +1 -0
  32. package/dist/ingest/codex.d.ts.map +1 -1
  33. package/dist/ingest/cursor.d.ts +6 -0
  34. package/dist/ingest/cursor.d.ts.map +1 -0
  35. package/dist/ingest/gemini.d.ts +1 -0
  36. package/dist/ingest/gemini.d.ts.map +1 -1
  37. package/dist/ingest/hermes.d.ts +6 -0
  38. package/dist/ingest/hermes.d.ts.map +1 -0
  39. package/dist/ingest/opencode.d.ts +7 -0
  40. package/dist/ingest/opencode.d.ts.map +1 -0
  41. package/dist/ingest/otel.d.ts +20 -0
  42. package/dist/ingest/otel.d.ts.map +1 -0
  43. package/dist/ingest/pi.d.ts +7 -0
  44. package/dist/ingest/pi.d.ts.map +1 -0
  45. package/dist/ingest/plugin.d.ts +17 -0
  46. package/dist/ingest/plugin.d.ts.map +1 -0
  47. package/dist/lib/agents.d.ts +11 -0
  48. package/dist/lib/agents.d.ts.map +1 -0
  49. package/dist/lib/billing-diff.d.ts +22 -0
  50. package/dist/lib/billing-diff.d.ts.map +1 -0
  51. package/dist/lib/cloud-sync.d.ts +35 -0
  52. package/dist/lib/cloud-sync.d.ts.map +1 -0
  53. package/dist/lib/config.d.ts.map +1 -1
  54. package/dist/lib/gatherer.d.ts.map +1 -1
  55. package/dist/lib/model-config.d.ts.map +1 -1
  56. package/dist/lib/open-projects.d.ts +19 -0
  57. package/dist/lib/open-projects.d.ts.map +1 -0
  58. package/dist/lib/paths.d.ts +20 -0
  59. package/dist/lib/paths.d.ts.map +1 -0
  60. package/dist/lib/pricing.d.ts +2 -2
  61. package/dist/lib/pricing.d.ts.map +1 -1
  62. package/dist/lib/savings.d.ts +17 -0
  63. package/dist/lib/savings.d.ts.map +1 -0
  64. package/dist/lib/serve-auth.d.ts +4 -0
  65. package/dist/lib/serve-auth.d.ts.map +1 -0
  66. package/dist/lib/spikes.d.ts +18 -0
  67. package/dist/lib/spikes.d.ts.map +1 -0
  68. package/dist/lib/sync-all.d.ts +28 -0
  69. package/dist/lib/sync-all.d.ts.map +1 -0
  70. package/dist/lib/watch-paths.d.ts +3 -0
  71. package/dist/lib/watch-paths.d.ts.map +1 -0
  72. package/dist/lib/webhooks.d.ts.map +1 -1
  73. package/dist/mcp/http.d.ts +12 -0
  74. package/dist/mcp/http.d.ts.map +1 -0
  75. package/dist/mcp/index.js +2518 -472
  76. package/dist/mcp/server.d.ts +4 -0
  77. package/dist/mcp/server.d.ts.map +1 -0
  78. package/dist/otel/index.d.ts +3 -0
  79. package/dist/otel/index.d.ts.map +1 -0
  80. package/dist/otel/index.js +1372 -0
  81. package/dist/server/index.js +2818 -218
  82. package/dist/server/serve.d.ts +9 -1
  83. package/dist/server/serve.d.ts.map +1 -1
  84. package/dist/types/index.d.ts +56 -6
  85. package/dist/types/index.d.ts.map +1 -1
  86. package/package.json +7 -3
@@ -29,99 +29,486 @@ __export(exports_pricing, {
29
29
  DEFAULT_PRICING: () => DEFAULT_PRICING
30
30
  });
31
31
  function normalizeModelName(raw) {
32
- return raw.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "").toLowerCase();
32
+ return raw.trim().toLowerCase().replace(/^models\//, "").replace(/^[a-z0-9_.-]+\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
33
+ }
34
+ function normalizeModelNamePreservingProvider(raw) {
35
+ return raw.trim().toLowerCase().replace(/^models\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
36
+ }
37
+ function modelLookupKeys(raw) {
38
+ const withProvider = normalizeModelNamePreservingProvider(raw);
39
+ const withoutProvider = normalizeModelName(raw);
40
+ return withProvider === withoutProvider ? [withoutProvider] : [withProvider, withoutProvider];
41
+ }
42
+ function bestPrefixMatch(normalized, entries) {
43
+ let best = null;
44
+ for (const entry of entries) {
45
+ const [key] = entry;
46
+ if (normalized !== key && !normalized.startsWith(`${key}-`))
47
+ continue;
48
+ if (!best || key.length > best[0].length)
49
+ best = entry;
50
+ }
51
+ return best?.[1] ?? null;
52
+ }
53
+ function bestModelMatch(model, entries) {
54
+ for (const key of modelLookupKeys(model)) {
55
+ const match = bestPrefixMatch(key, entries);
56
+ if (match)
57
+ return match;
58
+ }
59
+ return null;
60
+ }
61
+ function exactModelMatch(model, entries) {
62
+ for (const key of modelLookupKeys(model)) {
63
+ const match = entries.find(([entryKey]) => entryKey === key);
64
+ if (match)
65
+ return match[1];
66
+ }
67
+ return null;
33
68
  }
34
69
  function ensurePricingSeeded(db) {
35
70
  seedModelPricing(db, DEFAULT_PRICING);
71
+ repairLegacySeededPricing(db);
72
+ repairMissingDefaultCacheWrite1h(db);
73
+ repairMissingDefaultCacheStorage(db);
74
+ removeDeprecatedDefaultPricing(db);
75
+ }
76
+ function repairLegacySeededPricing(db) {
77
+ const now = new Date().toISOString();
78
+ const legacyModels = new Set([
79
+ ...Object.keys(LEGACY_DEFAULT_PRICING),
80
+ ...Object.keys(ADDITIONAL_LEGACY_DEFAULT_PRICING)
81
+ ]);
82
+ for (const model of legacyModels) {
83
+ const current = getModelPricing(db, model);
84
+ const next = DEFAULT_PRICING[model];
85
+ if (!current || !next)
86
+ continue;
87
+ const legacy = LEGACY_DEFAULT_PRICING[model];
88
+ const legacyRows = [
89
+ ...legacy ? [legacy] : [],
90
+ ...ADDITIONAL_LEGACY_DEFAULT_PRICING[model] ?? []
91
+ ];
92
+ if (!legacyRows.some((row) => samePricing(current, row)))
93
+ continue;
94
+ upsertModelPricing(db, {
95
+ model,
96
+ input_per_1m: next.inputPer1M,
97
+ output_per_1m: next.outputPer1M,
98
+ cache_read_per_1m: next.cacheReadPer1M,
99
+ cache_write_per_1m: next.cacheWritePer1M,
100
+ cache_write_1h_per_1m: next.cacheWrite1hPer1M ?? 0,
101
+ cache_storage_per_1m_hour: next.cacheStoragePer1MHour ?? 0,
102
+ updated_at: now
103
+ });
104
+ }
105
+ }
106
+ function repairMissingDefaultCacheWrite1h(db) {
107
+ const now = new Date().toISOString();
108
+ for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
109
+ if (!next.cacheWrite1hPer1M)
110
+ continue;
111
+ const current = getModelPricing(db, model);
112
+ if (!current)
113
+ continue;
114
+ if ((current.cache_write_1h_per_1m ?? 0) !== 0)
115
+ continue;
116
+ if (!sameBasePricing(current, next))
117
+ continue;
118
+ upsertModelPricing(db, {
119
+ model,
120
+ input_per_1m: current.input_per_1m,
121
+ output_per_1m: current.output_per_1m,
122
+ cache_read_per_1m: current.cache_read_per_1m,
123
+ cache_write_per_1m: current.cache_write_per_1m,
124
+ cache_write_1h_per_1m: next.cacheWrite1hPer1M,
125
+ cache_storage_per_1m_hour: current.cache_storage_per_1m_hour ?? next.cacheStoragePer1MHour ?? 0,
126
+ updated_at: now
127
+ });
128
+ }
129
+ }
130
+ function repairMissingDefaultCacheStorage(db) {
131
+ const now = new Date().toISOString();
132
+ for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
133
+ if (!next.cacheStoragePer1MHour)
134
+ continue;
135
+ const current = getModelPricing(db, model);
136
+ if (!current)
137
+ continue;
138
+ if ((current.cache_storage_per_1m_hour ?? 0) !== 0)
139
+ continue;
140
+ if (!sameBasePricing(current, next))
141
+ continue;
142
+ upsertModelPricing(db, {
143
+ model,
144
+ input_per_1m: current.input_per_1m,
145
+ output_per_1m: current.output_per_1m,
146
+ cache_read_per_1m: current.cache_read_per_1m,
147
+ cache_write_per_1m: current.cache_write_per_1m,
148
+ cache_write_1h_per_1m: current.cache_write_1h_per_1m ?? next.cacheWrite1hPer1M ?? 0,
149
+ cache_storage_per_1m_hour: next.cacheStoragePer1MHour,
150
+ updated_at: now
151
+ });
152
+ }
153
+ }
154
+ function removeDeprecatedDefaultPricing(db) {
155
+ for (const [model, removedRows] of Object.entries(REMOVED_DEFAULT_PRICING)) {
156
+ const current = getModelPricing(db, model);
157
+ if (!current)
158
+ continue;
159
+ if (!removedRows.some((row) => samePricing(current, row)))
160
+ continue;
161
+ deleteModelPricing(db, model);
162
+ }
163
+ }
164
+ function sameBasePricing(row, pricing) {
165
+ return row.input_per_1m === pricing.inputPer1M && row.output_per_1m === pricing.outputPer1M && row.cache_read_per_1m === pricing.cacheReadPer1M && row.cache_write_per_1m === pricing.cacheWritePer1M;
166
+ }
167
+ function samePricing(row, pricing) {
168
+ return row.input_per_1m === pricing.inputPer1M && row.output_per_1m === pricing.outputPer1M && row.cache_read_per_1m === pricing.cacheReadPer1M && row.cache_write_per_1m === pricing.cacheWritePer1M && (row.cache_write_1h_per_1m ?? 0) === (pricing.cacheWrite1hPer1M ?? 0) && (row.cache_storage_per_1m_hour ?? 0) === (pricing.cacheStoragePer1MHour ?? 0);
36
169
  }
37
170
  function getPricingFromDb(db, model) {
38
- const normalized = normalizeModelName(model);
39
- const row = getModelPricing(db, normalized);
40
- if (row) {
41
- return {
42
- inputPer1M: row.input_per_1m,
43
- outputPer1M: row.output_per_1m,
44
- cacheReadPer1M: row.cache_read_per_1m,
45
- cacheWritePer1M: row.cache_write_per_1m
46
- };
171
+ if (isFreeModel(model))
172
+ return FREE_PRICING;
173
+ for (const key of modelLookupKeys(model)) {
174
+ const row = getModelPricing(db, key);
175
+ if (row)
176
+ return modelPricingFromDbRow(row);
47
177
  }
48
178
  const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
49
- for (const r of allRows) {
50
- if (normalized.startsWith(r.model)) {
51
- return { inputPer1M: r.input_per_1m, outputPer1M: r.output_per_1m, cacheReadPer1M: r.cache_read_per_1m, cacheWritePer1M: r.cache_write_per_1m };
52
- }
53
- }
54
- return null;
179
+ const match = bestModelMatch(model, allRows.map((r) => [r.model, r]));
180
+ if (!match)
181
+ return null;
182
+ return modelPricingFromDbRow(match);
183
+ }
184
+ function modelPricingFromDbRow(row) {
185
+ const seeded = DEFAULT_PRICING[row.model];
186
+ const cacheWrite1hPer1M = seeded?.cacheWrite1hPer1M && (row.cache_write_1h_per_1m ?? 0) === 0 && sameBasePricing(row, seeded) ? seeded.cacheWrite1hPer1M : row.cache_write_1h_per_1m ?? 0;
187
+ return {
188
+ inputPer1M: row.input_per_1m,
189
+ outputPer1M: row.output_per_1m,
190
+ cacheReadPer1M: row.cache_read_per_1m,
191
+ cacheWritePer1M: row.cache_write_per_1m,
192
+ cacheWrite1hPer1M,
193
+ cacheStoragePer1MHour: row.cache_storage_per_1m_hour ?? seeded?.cacheStoragePer1MHour ?? 0
194
+ };
55
195
  }
56
196
  function getPricing(model) {
57
- const normalized = normalizeModelName(model);
58
- if (DEFAULT_PRICING[normalized])
59
- return DEFAULT_PRICING[normalized] ?? null;
60
- for (const key of Object.keys(DEFAULT_PRICING)) {
61
- if (normalized.startsWith(key))
62
- return DEFAULT_PRICING[key] ?? null;
63
- }
64
- return null;
197
+ if (isFreeModel(model))
198
+ return FREE_PRICING;
199
+ return bestModelMatch(model, Object.entries(DEFAULT_PRICING));
200
+ }
201
+ function isFreeModel(model) {
202
+ return model.trim().toLowerCase().endsWith(":free");
65
203
  }
66
- function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
204
+ function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
67
205
  const pricing = getPricing(model);
68
206
  if (!pricing)
69
207
  return 0;
70
- return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
208
+ return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
71
209
  }
72
- function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
210
+ function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
73
211
  const pricing = getPricingFromDb(db, model) ?? getPricing(model);
74
212
  if (!pricing)
75
213
  return 0;
76
- return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
214
+ return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
215
+ }
216
+ function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
217
+ if (isFreeModel(model))
218
+ return 0;
219
+ let effective = pricing;
220
+ const promptTier = bestModelMatch(model, Object.entries(GEMINI_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(QWEN_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(MINIMAX_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(XAI_PROMPT_TIERS)) ?? exactModelMatch(model, Object.entries(OPENAI_PROMPT_TIERS));
221
+ if (promptTier) {
222
+ const billablePromptTokens = inputTokens + cacheReadTokens + cacheWriteTokens + cacheWrite1hTokens;
223
+ if (billablePromptTokens > promptTier.threshold) {
224
+ effective = {
225
+ ...pricing,
226
+ inputPer1M: promptTier.inputPer1M ?? pricing.inputPer1M * (promptTier.inputMultiplier ?? 1),
227
+ outputPer1M: promptTier.outputPer1M ?? pricing.outputPer1M * (promptTier.outputMultiplier ?? 1),
228
+ cacheReadPer1M: promptTier.cacheReadPer1M ?? pricing.cacheReadPer1M * (promptTier.cacheReadMultiplier ?? 1),
229
+ cacheWritePer1M: promptTier.cacheWritePer1M ?? pricing.cacheWritePer1M * (promptTier.cacheWriteMultiplier ?? 1)
230
+ };
231
+ }
232
+ }
233
+ return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
77
234
  }
78
- var DEFAULT_PRICING;
235
+ var DEFAULT_PRICING, LEGACY_DEFAULT_PRICING, ADDITIONAL_LEGACY_DEFAULT_PRICING, REMOVED_DEFAULT_PRICING, FREE_PRICING, GEMINI_PROMPT_TIERS, OPENAI_PROMPT_TIERS, QWEN_PROMPT_TIERS, MINIMAX_PROMPT_TIERS, XAI_PROMPT_TIERS;
79
236
  var init_pricing = __esm(() => {
80
237
  init_database();
81
238
  DEFAULT_PRICING = {
82
- "claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
83
- "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
84
- "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
85
- "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
86
- "claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
87
- "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
88
- "claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
89
- "claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
90
- "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
91
- "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
92
- "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
93
- "gemini-3.1-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
94
- "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
95
- "gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
96
- "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
97
- "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
98
- "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
239
+ "claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
240
+ "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
241
+ "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
242
+ "claude-opus-4-1": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
243
+ "claude-opus-4": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
244
+ "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
245
+ "claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
246
+ "claude-sonnet-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
247
+ "claude-3-7-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
248
+ "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25, cacheWrite1hPer1M: 2 },
249
+ "claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1, cacheWrite1hPer1M: 1.6 },
250
+ "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
251
+ "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3, cacheWrite1hPer1M: 0.5 },
252
+ "gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
253
+ "gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
254
+ "gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
255
+ "gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
256
+ "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
257
+ "gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
258
+ "gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
259
+ "gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
260
+ "gemini-2.0-flash-lite": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
261
+ "google/gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0.375 },
262
+ "google/gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
263
+ "google/gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
264
+ "google/gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0.08333333333333334 },
265
+ "google/gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0.375 },
266
+ "google/gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0.08333333333333334 },
267
+ "google/gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0.08333333333333334 },
268
+ "gpt-5.5": { inputPer1M: 5, outputPer1M: 30, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
269
+ "gpt-5.5-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
99
270
  "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
100
271
  "gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
101
272
  "gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
273
+ "gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
274
+ "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
275
+ "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
276
+ "gpt-5.2-chat-latest": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
277
+ "gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
278
+ "gpt-5-codex": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
279
+ "gpt-5-mini": { inputPer1M: 0.25, outputPer1M: 2, cacheReadPer1M: 0.025, cacheWritePer1M: 0 },
280
+ "gpt-5": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
281
+ "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
282
+ "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
283
+ o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
284
+ "o1-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
285
+ o3: { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
286
+ "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
287
+ "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
288
+ "qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
289
+ "qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
290
+ "qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
291
+ "qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
292
+ "qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
293
+ "qwen/qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
294
+ "qwen/qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
295
+ "qwen/qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
296
+ "qwen/qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
297
+ "qwen/qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
298
+ "minimax-m2.7": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
299
+ "minimax-m2.7-highspeed": { inputPer1M: 0.6, outputPer1M: 2.4, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
300
+ "minimax/minimax-m2.7": { inputPer1M: 0.299, outputPer1M: 1.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
301
+ "minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
302
+ "minimax/minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
303
+ "grok-4.3": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
304
+ "grok-latest": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
305
+ "grok-4.20": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
306
+ "grok-4-1-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
307
+ "grok-4-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
308
+ "grok-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
309
+ "grok-code-fast-1": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
310
+ "grok-code-fast": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
311
+ "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
312
+ "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0.07, cacheWritePer1M: 0 },
313
+ "glm-5.1": { inputPer1M: 1.4, outputPer1M: 4.4, cacheReadPer1M: 0.26, cacheWritePer1M: 0 },
314
+ "glm-5": { inputPer1M: 1, outputPer1M: 3.2, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
315
+ "z-ai/glm-5.1": { inputPer1M: 1.05, outputPer1M: 3.5, cacheReadPer1M: 0.525, cacheWritePer1M: 0 },
316
+ "z-ai/glm-5": { inputPer1M: 0.6, outputPer1M: 1.92, cacheReadPer1M: 0.12, cacheWritePer1M: 0 },
317
+ "kimi-k2.6": { inputPer1M: 0.95, outputPer1M: 4, cacheReadPer1M: 0.16, cacheWritePer1M: 0 },
318
+ "kimi-k2.5": { inputPer1M: 0.6, outputPer1M: 3, cacheReadPer1M: 0.1, cacheWritePer1M: 0 },
319
+ "kimi-k2": { inputPer1M: 0.6, outputPer1M: 2.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
320
+ "moonshotai/kimi-k2.6": { inputPer1M: 0.75, outputPer1M: 3.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
321
+ "moonshotai/kimi-k2.5": { inputPer1M: 0.44, outputPer1M: 2, cacheReadPer1M: 0.22, cacheWritePer1M: 0 },
322
+ "moonshotai/kimi-k2": { inputPer1M: 0.57, outputPer1M: 2.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
323
+ };
324
+ LEGACY_DEFAULT_PRICING = {
325
+ "claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
326
+ "claude-opus-4": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
327
+ "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
328
+ "gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
329
+ "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
102
330
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
103
- "gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
104
331
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
105
332
  "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
106
333
  "gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
107
334
  "gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
108
- "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
109
- "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
110
- o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
111
335
  "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
112
- o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
113
- "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
114
- "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
336
+ "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
337
+ "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
115
338
  "qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
116
- "qwen3.6": { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
117
339
  "minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
118
340
  "minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
119
341
  "minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
120
- "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
121
- "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
122
342
  "glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
123
343
  "glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
124
- "kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
344
+ "kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
345
+ o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 }
346
+ };
347
+ ADDITIONAL_LEGACY_DEFAULT_PRICING = {
348
+ "gemini-2.5-pro": [
349
+ { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 }
350
+ ],
351
+ "qwen3.6-plus": [
352
+ { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
353
+ { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
354
+ ],
355
+ "qwen3.6-flash": [
356
+ { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
357
+ ],
358
+ "qwen3.6-max-preview": [
359
+ { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
360
+ { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
361
+ ],
362
+ "qwen/qwen3.6-plus": [
363
+ { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
364
+ { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
365
+ ],
366
+ "qwen/qwen3.6-flash": [
367
+ { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
368
+ ],
369
+ "qwen/qwen3.6-max-preview": [
370
+ { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
371
+ { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
372
+ ]
373
+ };
374
+ REMOVED_DEFAULT_PRICING = {
375
+ "claude-3-5-sonnet": [
376
+ { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
377
+ { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
378
+ ],
379
+ "claude-3-sonnet": [
380
+ { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
381
+ { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
382
+ ],
383
+ "gemini-3.1-pro": [
384
+ { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
385
+ { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 }
386
+ ],
387
+ "gemini-1.5-pro": [
388
+ { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 }
389
+ ],
390
+ "gemini-1.5-flash": [
391
+ { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
392
+ ],
393
+ "gpt-5.3-chat": [
394
+ { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
395
+ { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 }
396
+ ],
397
+ "qwen3.6": [
398
+ { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
399
+ ]
400
+ };
401
+ FREE_PRICING = {
402
+ inputPer1M: 0,
403
+ outputPer1M: 0,
404
+ cacheReadPer1M: 0,
405
+ cacheWritePer1M: 0,
406
+ cacheWrite1hPer1M: 0,
407
+ cacheStoragePer1MHour: 0
408
+ };
409
+ GEMINI_PROMPT_TIERS = {
410
+ "gemini-3.1-pro-preview": {
411
+ threshold: 200000,
412
+ inputPer1M: 4,
413
+ outputPer1M: 18,
414
+ cacheReadPer1M: 0.4
415
+ },
416
+ "gemini-2.5-pro": {
417
+ threshold: 200000,
418
+ inputPer1M: 2.5,
419
+ outputPer1M: 15,
420
+ cacheReadPer1M: 0.25
421
+ }
422
+ };
423
+ OPENAI_PROMPT_TIERS = {
424
+ "gpt-5.5": {
425
+ threshold: 272000,
426
+ inputMultiplier: 2,
427
+ outputMultiplier: 1.5,
428
+ cacheReadMultiplier: 2
429
+ },
430
+ "gpt-5.4-pro": {
431
+ threshold: 272000,
432
+ inputMultiplier: 2,
433
+ outputMultiplier: 1.5,
434
+ cacheReadMultiplier: 2
435
+ },
436
+ "gpt-5.4": {
437
+ threshold: 272000,
438
+ inputMultiplier: 2,
439
+ outputMultiplier: 1.5,
440
+ cacheReadMultiplier: 2
441
+ }
442
+ };
443
+ QWEN_PROMPT_TIERS = {
444
+ "qwen3.6-plus": {
445
+ threshold: 256000,
446
+ inputPer1M: 1.3,
447
+ outputPer1M: 3.9,
448
+ cacheReadPer1M: 0.13,
449
+ cacheWritePer1M: 1.625
450
+ },
451
+ "qwen3.6-flash": {
452
+ threshold: 256000,
453
+ inputPer1M: 1,
454
+ outputPer1M: 4,
455
+ cacheReadPer1M: 0.1,
456
+ cacheWritePer1M: 1.25
457
+ },
458
+ "qwen3.6-max-preview": {
459
+ threshold: 128000,
460
+ inputPer1M: 1.6,
461
+ outputPer1M: 9.6,
462
+ cacheReadPer1M: 0.16,
463
+ cacheWritePer1M: 2
464
+ }
465
+ };
466
+ MINIMAX_PROMPT_TIERS = {
467
+ "minimax/minimax-m1": {
468
+ threshold: Number.POSITIVE_INFINITY
469
+ },
470
+ "minimax-m1": {
471
+ threshold: 200000,
472
+ inputPer1M: 1.3
473
+ }
474
+ };
475
+ XAI_PROMPT_TIERS = {
476
+ "grok-4.3": {
477
+ threshold: 200000,
478
+ inputPer1M: 2.5,
479
+ outputPer1M: 5,
480
+ cacheReadPer1M: 0.4
481
+ },
482
+ "grok-latest": {
483
+ threshold: 200000,
484
+ inputPer1M: 2.5,
485
+ outputPer1M: 5,
486
+ cacheReadPer1M: 0.4
487
+ },
488
+ "grok-4.20": {
489
+ threshold: 200000,
490
+ inputPer1M: 2.5,
491
+ outputPer1M: 5,
492
+ cacheReadPer1M: 0.4
493
+ },
494
+ "grok-4-1-fast": {
495
+ threshold: 128000,
496
+ inputPer1M: 0.4,
497
+ outputPer1M: 1,
498
+ cacheReadPer1M: 0
499
+ },
500
+ "grok-4-fast": {
501
+ threshold: 128000,
502
+ inputPer1M: 0.4,
503
+ outputPer1M: 1,
504
+ cacheReadPer1M: 0
505
+ },
506
+ "grok-4": {
507
+ threshold: 128000,
508
+ inputPer1M: 6,
509
+ outputPer1M: 30,
510
+ cacheReadPer1M: 0
511
+ }
125
512
  };
126
513
  });
127
514
 
@@ -190,6 +577,8 @@ function initSchema(db) {
190
577
  output_tokens INTEGER DEFAULT 0,
191
578
  cache_read_tokens INTEGER DEFAULT 0,
192
579
  cache_create_tokens INTEGER DEFAULT 0,
580
+ cache_create_5m_tokens INTEGER DEFAULT 0,
581
+ cache_create_1h_tokens INTEGER DEFAULT 0,
193
582
  cost_usd REAL NOT NULL DEFAULT 0,
194
583
  duration_ms INTEGER DEFAULT 0,
195
584
  timestamp TEXT NOT NULL,
@@ -260,6 +649,8 @@ function initSchema(db) {
260
649
  output_per_1m REAL NOT NULL DEFAULT 0,
261
650
  cache_read_per_1m REAL NOT NULL DEFAULT 0,
262
651
  cache_write_per_1m REAL NOT NULL DEFAULT 0,
652
+ cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
653
+ cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
263
654
  updated_at TEXT NOT NULL
264
655
  );
265
656
 
@@ -284,12 +675,100 @@ function initSchema(db) {
284
675
 
285
676
  CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
286
677
  CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
678
+
679
+ CREATE TABLE IF NOT EXISTS subscriptions (
680
+ id TEXT PRIMARY KEY,
681
+ agent TEXT,
682
+ provider TEXT NOT NULL,
683
+ plan TEXT NOT NULL,
684
+ monthly_fee_usd REAL NOT NULL DEFAULT 0,
685
+ included_usage_usd REAL NOT NULL DEFAULT 0,
686
+ billing_cycle_start TEXT,
687
+ reset_policy TEXT DEFAULT 'monthly',
688
+ active INTEGER NOT NULL DEFAULT 1,
689
+ created_at TEXT NOT NULL,
690
+ updated_at TEXT NOT NULL
691
+ );
692
+
693
+ CREATE TABLE IF NOT EXISTS usage_snapshots (
694
+ id TEXT PRIMARY KEY,
695
+ agent TEXT NOT NULL,
696
+ date TEXT NOT NULL,
697
+ metric TEXT NOT NULL,
698
+ value REAL NOT NULL DEFAULT 0,
699
+ unit TEXT DEFAULT '',
700
+ machine_id TEXT DEFAULT '',
701
+ updated_at TEXT NOT NULL
702
+ );
703
+
704
+ CREATE TABLE IF NOT EXISTS savings_daily (
705
+ date TEXT NOT NULL,
706
+ agent TEXT DEFAULT '',
707
+ api_equivalent_usd REAL NOT NULL DEFAULT 0,
708
+ subscription_fee_usd REAL NOT NULL DEFAULT 0,
709
+ included_consumed_usd REAL NOT NULL DEFAULT 0,
710
+ on_demand_usd REAL NOT NULL DEFAULT 0,
711
+ saved_usd REAL NOT NULL DEFAULT 0,
712
+ updated_at TEXT NOT NULL,
713
+ PRIMARY KEY (date, agent)
714
+ );
715
+
716
+ CREATE TABLE IF NOT EXISTS machines (
717
+ machine_id TEXT PRIMARY KEY,
718
+ hostname TEXT NOT NULL,
719
+ last_seen_at TEXT,
720
+ last_push_at TEXT,
721
+ last_pull_at TEXT,
722
+ economy_version TEXT,
723
+ updated_at TEXT NOT NULL
724
+ );
725
+
726
+ CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
727
+ CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
287
728
  `);
288
729
  const cols = db.prepare(`PRAGMA table_info(requests)`).all();
289
730
  if (!cols.some((c) => c.name === "machine_id")) {
290
731
  db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
291
732
  db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
292
733
  }
734
+ if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
735
+ db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
736
+ db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
737
+ }
738
+ if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
739
+ db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
740
+ }
741
+ if (!cols.some((c) => c.name === "cost_basis")) {
742
+ db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
743
+ }
744
+ if (!cols.some((c) => c.name === "attribution_tag")) {
745
+ db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
746
+ }
747
+ if (!cols.some((c) => c.name === "updated_at")) {
748
+ db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
749
+ db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
750
+ }
751
+ if (!cols.some((c) => c.name === "synced_at")) {
752
+ db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
753
+ }
754
+ const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
755
+ if (!sessionCols.some((c) => c.name === "attribution_tag")) {
756
+ db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
757
+ }
758
+ if (!sessionCols.some((c) => c.name === "updated_at")) {
759
+ db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
760
+ db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
761
+ }
762
+ if (!sessionCols.some((c) => c.name === "synced_at")) {
763
+ db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
764
+ }
765
+ const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
766
+ if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
767
+ db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
768
+ }
769
+ if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
770
+ db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
771
+ }
293
772
  db.exec(`
294
773
  CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
295
774
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
@@ -328,21 +807,24 @@ function sessionPeriodWhere(period) {
328
807
  }
329
808
  }
330
809
  function upsertRequest(db, req) {
810
+ const now = req.updated_at ?? new Date().toISOString();
331
811
  db.prepare(`
332
812
  INSERT OR REPLACE INTO requests
333
813
  (id, agent, session_id, model, input_tokens, output_tokens,
334
- cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
335
- timestamp, source_request_id, machine_id)
336
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
337
- `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "");
814
+ cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
815
+ cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
816
+ source_request_id, machine_id, attribution_tag, updated_at, synced_at)
817
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
818
+ `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", now, req.synced_at ?? "");
338
819
  }
339
820
  function upsertSession(db, session) {
821
+ const now = session.updated_at ?? new Date().toISOString();
340
822
  db.prepare(`
341
823
  INSERT OR REPLACE INTO sessions
342
824
  (id, agent, project_path, project_name, started_at, ended_at,
343
- total_cost_usd, total_tokens, request_count, machine_id)
344
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
345
- `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "");
825
+ total_cost_usd, total_tokens, request_count, machine_id, attribution_tag, updated_at, synced_at)
826
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
827
+ `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", now, session.synced_at ?? "");
346
828
  }
347
829
  function rollupSession(db, sessionId) {
348
830
  db.prepare(`
@@ -394,10 +876,10 @@ function queryTopSessions(db, n = 10, agent) {
394
876
  }
395
877
  return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
396
878
  }
397
- function querySummary(db, period, machine) {
879
+ function querySummary(db, period, machine, allMachines = false) {
398
880
  const rWhere = periodWhere(period);
399
881
  const sWhere = sessionPeriodWhere(period);
400
- const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
882
+ const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
401
883
  const r = db.prepare(`
402
884
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
403
885
  COUNT(*) as requests,
@@ -600,6 +1082,26 @@ function getIngestState(db, source, key) {
600
1082
  function setIngestState(db, source, key, value) {
601
1083
  db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
602
1084
  }
1085
+ function upsertBillingDaily(db, row) {
1086
+ db.prepare(`
1087
+ INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
1088
+ VALUES (?, ?, ?, ?, ?)
1089
+ `).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
1090
+ }
1091
+ function clearBillingRange(db, provider, fromDate, toDate) {
1092
+ db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
1093
+ }
1094
+ function queryBillingSummary(db, period) {
1095
+ const where = period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
1096
+ const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
1097
+ const by_provider = {};
1098
+ let total = 0;
1099
+ for (const r of rows) {
1100
+ by_provider[r.provider] = r.cost;
1101
+ total += r.cost;
1102
+ }
1103
+ return { total_usd: total, by_provider };
1104
+ }
603
1105
  function listMachines(db) {
604
1106
  return db.prepare(`
605
1107
  SELECT
@@ -617,9 +1119,9 @@ function listMachines(db) {
617
1119
  function upsertModelPricing(db, p) {
618
1120
  db.prepare(`
619
1121
  INSERT OR REPLACE INTO model_pricing
620
- (model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
621
- VALUES (?, ?, ?, ?, ?, ?)
622
- `).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.updated_at);
1122
+ (model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, cache_write_1h_per_1m, cache_storage_per_1m_hour, updated_at)
1123
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1124
+ `).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.cache_write_1h_per_1m ?? 0, p.cache_storage_per_1m_hour ?? 0, p.updated_at);
623
1125
  }
624
1126
  function getModelPricing(db, model) {
625
1127
  return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
@@ -642,14 +1144,644 @@ function seedModelPricing(db, defaults) {
642
1144
  output_per_1m: p.outputPer1M,
643
1145
  cache_read_per_1m: p.cacheReadPer1M,
644
1146
  cache_write_per_1m: p.cacheWritePer1M,
1147
+ cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
1148
+ cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
645
1149
  updated_at: now
646
1150
  });
647
1151
  }
648
1152
  }
1153
+ function upsertSubscription(db, sub) {
1154
+ db.prepare(`
1155
+ INSERT OR REPLACE INTO subscriptions
1156
+ (id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
1157
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1158
+ `).run(sub.id, sub.agent, sub.provider, sub.plan, sub.monthly_fee_usd, sub.included_usage_usd, sub.billing_cycle_start, sub.reset_policy, sub.active, sub.created_at, sub.updated_at);
1159
+ }
1160
+ function upsertUsageSnapshot(db, snap) {
1161
+ const now = snap.updated_at ?? new Date().toISOString();
1162
+ const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
1163
+ db.prepare(`
1164
+ INSERT OR REPLACE INTO usage_snapshots (id, agent, date, metric, value, unit, machine_id, updated_at)
1165
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1166
+ `).run(id, snap.agent, snap.date, snap.metric, snap.value, snap.unit, snap.machine_id, now);
1167
+ }
1168
+ function queryUsageSnapshots(db, opts = {}) {
1169
+ const conditions = [];
1170
+ const params = [];
1171
+ if (opts.agent) {
1172
+ conditions.push("agent = ?");
1173
+ params.push(opts.agent);
1174
+ }
1175
+ if (opts.date) {
1176
+ conditions.push("date = ?");
1177
+ params.push(opts.date);
1178
+ }
1179
+ if (opts.since) {
1180
+ conditions.push("date >= ?");
1181
+ params.push(opts.since);
1182
+ }
1183
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
1184
+ return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
1185
+ }
1186
+ function listMachineRegistry(db) {
1187
+ return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
1188
+ }
1189
+ function dedupeRequests(db) {
1190
+ const dupes = db.prepare(`
1191
+ SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
1192
+ FROM requests
1193
+ WHERE source_request_id != '' AND source_request_id IS NOT NULL
1194
+ GROUP BY source_request_id, agent
1195
+ HAVING cnt > 1
1196
+ `).all();
1197
+ let removed = 0;
1198
+ for (const row of dupes) {
1199
+ const result = db.prepare(`
1200
+ DELETE FROM requests WHERE source_request_id = ? AND agent = ? AND id != ?
1201
+ `).run(row.source_request_id, row.agent, row.keep_id);
1202
+ removed += result.changes;
1203
+ }
1204
+ return removed;
1205
+ }
649
1206
  var init_database = () => {};
650
1207
 
1208
+ // src/db/pg-migrations.ts
1209
+ var exports_pg_migrations = {};
1210
+ __export(exports_pg_migrations, {
1211
+ PG_MIGRATIONS: () => PG_MIGRATIONS
1212
+ });
1213
+ var PG_MIGRATIONS;
1214
+ var init_pg_migrations = __esm(() => {
1215
+ PG_MIGRATIONS = [
1216
+ `CREATE TABLE IF NOT EXISTS requests (
1217
+ id TEXT PRIMARY KEY,
1218
+ agent TEXT NOT NULL,
1219
+ session_id TEXT NOT NULL,
1220
+ model TEXT NOT NULL,
1221
+ input_tokens INTEGER DEFAULT 0,
1222
+ output_tokens INTEGER DEFAULT 0,
1223
+ cache_read_tokens INTEGER DEFAULT 0,
1224
+ cache_create_tokens INTEGER DEFAULT 0,
1225
+ cache_create_5m_tokens INTEGER DEFAULT 0,
1226
+ cache_create_1h_tokens INTEGER DEFAULT 0,
1227
+ cost_usd REAL NOT NULL DEFAULT 0,
1228
+ duration_ms INTEGER DEFAULT 0,
1229
+ timestamp TEXT NOT NULL,
1230
+ source_request_id TEXT,
1231
+ machine_id TEXT DEFAULT ''
1232
+ )`,
1233
+ `CREATE TABLE IF NOT EXISTS sessions (
1234
+ id TEXT PRIMARY KEY,
1235
+ agent TEXT NOT NULL,
1236
+ project_path TEXT DEFAULT '',
1237
+ project_name TEXT DEFAULT '',
1238
+ started_at TEXT NOT NULL,
1239
+ ended_at TEXT,
1240
+ total_cost_usd REAL DEFAULT 0,
1241
+ total_tokens INTEGER DEFAULT 0,
1242
+ request_count INTEGER DEFAULT 0,
1243
+ machine_id TEXT DEFAULT ''
1244
+ )`,
1245
+ `CREATE TABLE IF NOT EXISTS projects (
1246
+ id TEXT PRIMARY KEY,
1247
+ path TEXT UNIQUE NOT NULL,
1248
+ name TEXT NOT NULL,
1249
+ description TEXT,
1250
+ tags TEXT DEFAULT '[]',
1251
+ created_at TEXT NOT NULL
1252
+ )`,
1253
+ `CREATE TABLE IF NOT EXISTS budgets (
1254
+ id TEXT PRIMARY KEY,
1255
+ project_path TEXT,
1256
+ agent TEXT,
1257
+ period TEXT NOT NULL,
1258
+ limit_usd REAL NOT NULL,
1259
+ alert_at_percent INTEGER DEFAULT 80,
1260
+ created_at TEXT NOT NULL,
1261
+ updated_at TEXT NOT NULL
1262
+ )`,
1263
+ `CREATE TABLE IF NOT EXISTS goals (
1264
+ id TEXT PRIMARY KEY,
1265
+ period TEXT NOT NULL,
1266
+ project_path TEXT,
1267
+ agent TEXT,
1268
+ limit_usd REAL NOT NULL,
1269
+ created_at TEXT NOT NULL,
1270
+ updated_at TEXT NOT NULL
1271
+ )`,
1272
+ `CREATE TABLE IF NOT EXISTS ingest_state (
1273
+ source TEXT NOT NULL,
1274
+ key TEXT NOT NULL,
1275
+ value TEXT NOT NULL,
1276
+ PRIMARY KEY (source, key)
1277
+ )`,
1278
+ `CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
1279
+ `CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
1280
+ `CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
1281
+ `CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
1282
+ `CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
1283
+ `CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
1284
+ `CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
1285
+ `CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
1286
+ `CREATE TABLE IF NOT EXISTS model_pricing (
1287
+ model TEXT PRIMARY KEY,
1288
+ input_per_1m REAL NOT NULL DEFAULT 0,
1289
+ output_per_1m REAL NOT NULL DEFAULT 0,
1290
+ cache_read_per_1m REAL NOT NULL DEFAULT 0,
1291
+ cache_write_per_1m REAL NOT NULL DEFAULT 0,
1292
+ cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
1293
+ cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
1294
+ updated_at TEXT NOT NULL
1295
+ )`,
1296
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_5m_tokens INTEGER DEFAULT 0`,
1297
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_1h_tokens INTEGER DEFAULT 0`,
1298
+ `ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`,
1299
+ `ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`,
1300
+ `CREATE TABLE IF NOT EXISTS feedback (
1301
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
1302
+ message TEXT NOT NULL,
1303
+ email TEXT,
1304
+ category TEXT DEFAULT 'general',
1305
+ version TEXT,
1306
+ machine_id TEXT,
1307
+ created_at TEXT NOT NULL DEFAULT NOW()::text
1308
+ )`,
1309
+ `CREATE TABLE IF NOT EXISTS billing_daily (
1310
+ date TEXT NOT NULL,
1311
+ provider TEXT NOT NULL,
1312
+ description TEXT DEFAULT '',
1313
+ cost_usd REAL NOT NULL DEFAULT 0,
1314
+ updated_at TEXT NOT NULL,
1315
+ PRIMARY KEY (date, provider, description)
1316
+ )`,
1317
+ `CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date)`,
1318
+ `CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider)`,
1319
+ `CREATE TABLE IF NOT EXISTS subscriptions (
1320
+ id TEXT PRIMARY KEY,
1321
+ agent TEXT,
1322
+ provider TEXT NOT NULL,
1323
+ plan TEXT NOT NULL,
1324
+ monthly_fee_usd REAL NOT NULL DEFAULT 0,
1325
+ included_usage_usd REAL NOT NULL DEFAULT 0,
1326
+ billing_cycle_start TEXT,
1327
+ reset_policy TEXT DEFAULT 'monthly',
1328
+ active INTEGER NOT NULL DEFAULT 1,
1329
+ created_at TEXT NOT NULL,
1330
+ updated_at TEXT NOT NULL
1331
+ )`,
1332
+ `CREATE TABLE IF NOT EXISTS usage_snapshots (
1333
+ id TEXT PRIMARY KEY,
1334
+ agent TEXT NOT NULL,
1335
+ date TEXT NOT NULL,
1336
+ metric TEXT NOT NULL,
1337
+ value REAL NOT NULL DEFAULT 0,
1338
+ unit TEXT DEFAULT '',
1339
+ machine_id TEXT DEFAULT '',
1340
+ updated_at TEXT NOT NULL
1341
+ )`,
1342
+ `CREATE TABLE IF NOT EXISTS savings_daily (
1343
+ date TEXT NOT NULL,
1344
+ agent TEXT DEFAULT '',
1345
+ api_equivalent_usd REAL NOT NULL DEFAULT 0,
1346
+ subscription_fee_usd REAL NOT NULL DEFAULT 0,
1347
+ included_consumed_usd REAL NOT NULL DEFAULT 0,
1348
+ on_demand_usd REAL NOT NULL DEFAULT 0,
1349
+ saved_usd REAL NOT NULL DEFAULT 0,
1350
+ updated_at TEXT NOT NULL,
1351
+ PRIMARY KEY (date, agent)
1352
+ )`,
1353
+ `CREATE TABLE IF NOT EXISTS machines (
1354
+ machine_id TEXT PRIMARY KEY,
1355
+ hostname TEXT NOT NULL,
1356
+ last_seen_at TEXT,
1357
+ last_push_at TEXT,
1358
+ last_pull_at TEXT,
1359
+ economy_version TEXT,
1360
+ updated_at TEXT NOT NULL
1361
+ )`,
1362
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
1363
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1364
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1365
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1366
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1367
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1368
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1369
+ `CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
1370
+ `CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`
1371
+ ];
1372
+ });
1373
+
1374
+ // src/ingest/billing.ts
1375
+ var exports_billing = {};
1376
+ __export(exports_billing, {
1377
+ syncOpenAIBilling: () => syncOpenAIBilling,
1378
+ syncGeminiBilling: () => syncGeminiBilling,
1379
+ syncAnthropicBilling: () => syncAnthropicBilling
1380
+ });
1381
+ import { readFileSync as readFileSync10 } from "fs";
1382
+ function getAnthropicAdminKey() {
1383
+ return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
1384
+ }
1385
+ function getOpenAIAdminKey() {
1386
+ return process.env["HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY"] ?? process.env["OPENAI_ADMIN_API_KEY"] ?? null;
1387
+ }
1388
+ function getGeminiBillingExportPath() {
1389
+ return process.env["HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["HASNAXYZ_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["GEMINI_BILLING_EXPORT_PATH"] ?? null;
1390
+ }
1391
+ function toISODate(d) {
1392
+ return d.toISOString().substring(0, 10);
1393
+ }
1394
+ function parseDate(value) {
1395
+ if (typeof value !== "string" || !value.trim())
1396
+ return null;
1397
+ const d = new Date(value);
1398
+ if (Number.isNaN(d.getTime()))
1399
+ return value.substring(0, 10);
1400
+ return toISODate(d);
1401
+ }
1402
+ function parseCsv(content) {
1403
+ const lines = content.split(/\r?\n/).filter((line) => line.trim());
1404
+ if (lines.length < 2)
1405
+ return [];
1406
+ const headers = parseCsvLine(lines[0]).map((h) => h.trim());
1407
+ return lines.slice(1).map((line) => {
1408
+ const values = parseCsvLine(line);
1409
+ return Object.fromEntries(headers.map((header, i) => [header, values[i]?.trim() ?? ""]));
1410
+ });
1411
+ }
1412
+ function parseCsvLine(line) {
1413
+ const values = [];
1414
+ let value = "";
1415
+ let quoted = false;
1416
+ for (let i = 0;i < line.length; i++) {
1417
+ const char = line[i];
1418
+ if (char === '"') {
1419
+ if (quoted && line[i + 1] === '"') {
1420
+ value += '"';
1421
+ i++;
1422
+ } else {
1423
+ quoted = !quoted;
1424
+ }
1425
+ } else if (char === "," && !quoted) {
1426
+ values.push(value);
1427
+ value = "";
1428
+ } else {
1429
+ value += char;
1430
+ }
1431
+ }
1432
+ values.push(value);
1433
+ return values;
1434
+ }
1435
+ function parseBillingRows(content) {
1436
+ const trimmed = content.trim();
1437
+ if (!trimmed)
1438
+ return [];
1439
+ try {
1440
+ const parsed = JSON.parse(trimmed);
1441
+ if (Array.isArray(parsed))
1442
+ return parsed;
1443
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed["rows"])) {
1444
+ return parsed["rows"];
1445
+ }
1446
+ } catch {}
1447
+ const jsonlRows = [];
1448
+ for (const line of trimmed.split(/\r?\n/)) {
1449
+ try {
1450
+ const parsed = JSON.parse(line);
1451
+ if (parsed && typeof parsed === "object")
1452
+ jsonlRows.push(parsed);
1453
+ } catch {
1454
+ jsonlRows.length = 0;
1455
+ break;
1456
+ }
1457
+ }
1458
+ if (jsonlRows.length > 0)
1459
+ return jsonlRows;
1460
+ return parseCsv(content);
1461
+ }
1462
+ async function syncAnthropicBilling(db, opts = {}) {
1463
+ const key = getAnthropicAdminKey();
1464
+ if (!key)
1465
+ throw new Error("Missing Anthropic admin key (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY)");
1466
+ const now = new Date;
1467
+ const end = opts.toDate ? new Date(opts.toDate) : new Date(now.getTime() + 24 * 3600000);
1468
+ const days = opts.days ?? 31;
1469
+ const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
1470
+ const startIso = start.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
1471
+ const endIso = end.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
1472
+ let totalUsd = 0;
1473
+ const buckets = [];
1474
+ let nextPage;
1475
+ do {
1476
+ const url = new URL("https://api.anthropic.com/v1/organizations/cost_report");
1477
+ url.searchParams.set("starting_at", startIso);
1478
+ url.searchParams.set("ending_at", endIso);
1479
+ url.searchParams.set("bucket_width", "1d");
1480
+ url.searchParams.set("limit", "31");
1481
+ url.searchParams.append("group_by[]", "description");
1482
+ if (nextPage)
1483
+ url.searchParams.set("page", nextPage);
1484
+ const res = await fetch(url.toString(), {
1485
+ headers: { "anthropic-version": "2023-06-01", "x-api-key": key }
1486
+ });
1487
+ const data = await res.json();
1488
+ if (data.error)
1489
+ throw new Error(`Anthropic API: ${data.error.message}`);
1490
+ if (data.data)
1491
+ buckets.push(...data.data);
1492
+ nextPage = data.has_more ? data.next_page : undefined;
1493
+ } while (nextPage);
1494
+ const fromDateStr = toISODate(start);
1495
+ const toDateStr = toISODate(new Date(end.getTime() - 1000));
1496
+ clearBillingRange(db, "anthropic", fromDateStr, toDateStr);
1497
+ const updatedAt = new Date().toISOString();
1498
+ for (const bucket of buckets) {
1499
+ const date = bucket.starting_at.substring(0, 10);
1500
+ for (const r of bucket.results) {
1501
+ const usd = Number(r.amount) / 100;
1502
+ if (usd === 0)
1503
+ continue;
1504
+ const desc = (r.description ?? "unknown").substring(0, 200);
1505
+ upsertBillingDaily(db, { date, provider: "anthropic", description: desc, cost_usd: usd, updated_at: updatedAt });
1506
+ totalUsd += usd;
1507
+ }
1508
+ }
1509
+ return { days: buckets.length, totalUsd };
1510
+ }
1511
+ async function syncOpenAIBilling(db, opts = {}) {
1512
+ const key = getOpenAIAdminKey();
1513
+ if (!key)
1514
+ throw new Error("Missing OpenAI admin key (HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY)");
1515
+ const now = new Date;
1516
+ const end = opts.toDate ? new Date(opts.toDate) : now;
1517
+ const days = opts.days ?? 31;
1518
+ const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
1519
+ const startSec = Math.floor(start.getTime() / 1000);
1520
+ const endSec = Math.floor(end.getTime() / 1000);
1521
+ let totalUsd = 0;
1522
+ const buckets = [];
1523
+ let nextPage;
1524
+ do {
1525
+ const url = new URL("https://api.openai.com/v1/organization/costs");
1526
+ url.searchParams.set("start_time", String(startSec));
1527
+ url.searchParams.set("end_time", String(endSec));
1528
+ url.searchParams.set("bucket_width", "1d");
1529
+ url.searchParams.set("limit", "31");
1530
+ url.searchParams.append("group_by[]", "line_item");
1531
+ if (nextPage)
1532
+ url.searchParams.set("page", nextPage);
1533
+ const res = await fetch(url.toString(), {
1534
+ headers: { Authorization: `Bearer ${key}` }
1535
+ });
1536
+ const data = await res.json();
1537
+ if (data.error)
1538
+ throw new Error(`OpenAI API: ${data.error.message}`);
1539
+ if (data.data)
1540
+ buckets.push(...data.data);
1541
+ nextPage = data.has_more ? data.next_page : undefined;
1542
+ } while (nextPage);
1543
+ const fromDateStr = toISODate(start);
1544
+ const toDateStr = toISODate(new Date(end.getTime() - 1000));
1545
+ clearBillingRange(db, "openai", fromDateStr, toDateStr);
1546
+ const updatedAt = new Date().toISOString();
1547
+ for (const bucket of buckets) {
1548
+ const date = new Date(bucket.start_time * 1000).toISOString().substring(0, 10);
1549
+ for (const r of bucket.results) {
1550
+ const usd = Number(r.amount?.value ?? 0);
1551
+ if (usd === 0)
1552
+ continue;
1553
+ const desc = (r.line_item ?? "unknown").substring(0, 200);
1554
+ upsertBillingDaily(db, { date, provider: "openai", description: desc, cost_usd: usd, updated_at: updatedAt });
1555
+ totalUsd += usd;
1556
+ }
1557
+ }
1558
+ return { days: buckets.length, totalUsd };
1559
+ }
1560
+ async function syncGeminiBilling(db, opts = {}) {
1561
+ const exportPath = getGeminiBillingExportPath();
1562
+ if (!exportPath) {
1563
+ return {
1564
+ days: 0,
1565
+ totalUsd: 0,
1566
+ skipped: "Missing Gemini billing export path (HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH, HASNAXYZ_ECONOMY_GEMINI_BILLING_EXPORT_PATH, or GEMINI_BILLING_EXPORT_PATH)"
1567
+ };
1568
+ }
1569
+ const now = new Date;
1570
+ const end = opts.toDate ? new Date(opts.toDate) : now;
1571
+ const days = opts.days ?? 31;
1572
+ const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
1573
+ const fromDateStr = toISODate(start);
1574
+ const toDateStr = toISODate(end);
1575
+ const rows = parseBillingRows(readFileSync10(exportPath, "utf-8"));
1576
+ clearBillingRange(db, "gemini", fromDateStr, toDateStr);
1577
+ const updatedAt = new Date().toISOString();
1578
+ let totalUsd = 0;
1579
+ const seenDays = new Set;
1580
+ for (const row of rows) {
1581
+ const date = parseDate(row["date"] ?? row["usage_start_time"] ?? row["start_time"] ?? row["invoice.month"]);
1582
+ if (!date || date < fromDateStr || date > toDateStr)
1583
+ continue;
1584
+ const rawCost = row["cost_usd"] ?? row["costUsd"] ?? row["cost"] ?? row["amount"];
1585
+ const costUsd = Number(rawCost);
1586
+ if (!Number.isFinite(costUsd) || costUsd === 0)
1587
+ continue;
1588
+ const service = row["service.description"] ?? row["service"] ?? row["provider"] ?? "";
1589
+ const sku = row["sku.description"] ?? row["sku"] ?? row["description"] ?? "Gemini API";
1590
+ const description = `${String(service || "Google AI")}: ${String(sku)}`.substring(0, 200);
1591
+ upsertBillingDaily(db, { date, provider: "gemini", description, cost_usd: costUsd, updated_at: updatedAt });
1592
+ totalUsd += costUsd;
1593
+ seenDays.add(date);
1594
+ }
1595
+ return { days: seenDays.size, totalUsd };
1596
+ }
1597
+ var init_billing = __esm(() => {
1598
+ init_database();
1599
+ });
1600
+
1601
+ // src/lib/open-projects.ts
1602
+ var exports_open_projects = {};
1603
+ __export(exports_open_projects, {
1604
+ syncOpenProjectsRegistry: () => syncOpenProjectsRegistry
1605
+ });
1606
+ async function syncOpenProjectsRegistry(db, listActiveProjects) {
1607
+ let listProjects2 = listActiveProjects;
1608
+ if (!listProjects2) {
1609
+ const projectsApi = await import("@hasna/projects");
1610
+ listProjects2 = projectsApi.listProjects;
1611
+ }
1612
+ const projects = listProjects2({ status: "active", limit: 5000 });
1613
+ let imported = 0;
1614
+ let skipped = 0;
1615
+ for (const project of projects) {
1616
+ if (!project.path) {
1617
+ skipped++;
1618
+ continue;
1619
+ }
1620
+ upsertProject(db, {
1621
+ id: project.id,
1622
+ path: project.path,
1623
+ name: project.name,
1624
+ description: project.description,
1625
+ tags: project.tags ?? [],
1626
+ created_at: project.created_at
1627
+ });
1628
+ imported++;
1629
+ }
1630
+ return { imported, skipped };
1631
+ }
1632
+ var init_open_projects = __esm(() => {
1633
+ init_database();
1634
+ });
1635
+
1636
+ // src/lib/config.ts
1637
+ import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
1638
+ import { dirname, join as join9 } from "path";
1639
+ function getConfigPath() {
1640
+ return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join9(getDataDir(), "config.json");
1641
+ }
1642
+ function loadConfig() {
1643
+ try {
1644
+ const configPath = getConfigPath();
1645
+ if (existsSync10(configPath)) {
1646
+ const raw = readFileSync11(configPath, "utf-8");
1647
+ return { ...DEFAULTS, ...JSON.parse(raw) };
1648
+ }
1649
+ } catch {}
1650
+ return { ...DEFAULTS };
1651
+ }
1652
+ var DEFAULTS;
1653
+ var init_config = __esm(() => {
1654
+ init_database();
1655
+ DEFAULTS = {
1656
+ port: 3456,
1657
+ "default-period": "today",
1658
+ "auto-sync": true,
1659
+ "sync-interval": 30,
1660
+ "alert-thresholds": [5, 10, 25, 50, 100],
1661
+ "webhook-url": null
1662
+ };
1663
+ });
1664
+
1665
+ // src/lib/spikes.ts
1666
+ function detectCostSpikes(dailyTotals, opts) {
1667
+ const windowDays = opts?.windowDays ?? 7;
1668
+ const multiplier = opts?.multiplier ?? 2;
1669
+ const sorted = [...dailyTotals].sort((a, b) => a.date.localeCompare(b.date));
1670
+ const spikes = [];
1671
+ for (let i = windowDays;i < sorted.length; i++) {
1672
+ const window = sorted.slice(i - windowDays, i);
1673
+ const avg = window.reduce((s, d) => s + d.cost_usd, 0) / window.length;
1674
+ const current = sorted[i];
1675
+ if (avg > 0 && current.cost_usd > avg * multiplier) {
1676
+ spikes.push({
1677
+ date: current.date,
1678
+ cost_usd: current.cost_usd,
1679
+ average_usd: avg,
1680
+ ratio: current.cost_usd / avg
1681
+ });
1682
+ }
1683
+ }
1684
+ return spikes;
1685
+ }
1686
+ function queryRecentSpikes(db, days = 14) {
1687
+ const rows = db.prepare(`
1688
+ SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd), 0) as cost_usd
1689
+ FROM requests
1690
+ WHERE timestamp >= DATE('now', '-' || ? || ' days')
1691
+ GROUP BY DATE(timestamp)
1692
+ ORDER BY date ASC
1693
+ `).all(days);
1694
+ return detectCostSpikes(rows);
1695
+ }
1696
+ function getTodaySpike(db) {
1697
+ const today = new Date().toISOString().substring(0, 10);
1698
+ const spikes = queryRecentSpikes(db, 14);
1699
+ return spikes.find((s) => s.date === today) ?? null;
1700
+ }
1701
+
1702
+ // src/lib/webhooks.ts
1703
+ var exports_webhooks = {};
1704
+ __export(exports_webhooks, {
1705
+ checkAndFireWebhooks: () => checkAndFireWebhooks
1706
+ });
1707
+ async function checkAndFireWebhooks(db) {
1708
+ const config = loadConfig();
1709
+ const url = config["webhook-url"];
1710
+ if (!url)
1711
+ return;
1712
+ const statuses = getBudgetStatuses(db);
1713
+ for (const b of statuses) {
1714
+ if (!b.is_over_alert)
1715
+ continue;
1716
+ const key = `webhook-budget-${b.id}-${b.period}`;
1717
+ const lastFired = getIngestState(db, "webhook", key);
1718
+ const pctBucket = Math.floor(b.percent_used / 10) * 10;
1719
+ if (lastFired === String(pctBucket))
1720
+ continue;
1721
+ const delivered = await fireWebhook(url, {
1722
+ event: "budget_alert",
1723
+ budget_id: b.id,
1724
+ project: b.project_path ?? "global",
1725
+ period: b.period,
1726
+ spend: b.current_spend_usd,
1727
+ limit: b.limit_usd,
1728
+ percent: Math.round(b.percent_used * 10) / 10
1729
+ });
1730
+ if (delivered)
1731
+ setIngestState(db, "webhook", key, String(pctBucket));
1732
+ }
1733
+ const spike = getTodaySpike(db);
1734
+ if (spike) {
1735
+ const key = `webhook-spike-${spike.date}`;
1736
+ if (getIngestState(db, "webhook", key) !== "1") {
1737
+ const delivered = await fireWebhook(url, {
1738
+ event: "cost_spike",
1739
+ date: spike.date,
1740
+ cost_usd: spike.cost_usd,
1741
+ average_usd: spike.average_usd,
1742
+ ratio: Math.round(spike.ratio * 100) / 100
1743
+ });
1744
+ if (delivered)
1745
+ setIngestState(db, "webhook", key, "1");
1746
+ }
1747
+ }
1748
+ }
1749
+ async function fireWebhook(url, payload) {
1750
+ try {
1751
+ const response = await fetch(url, {
1752
+ method: "POST",
1753
+ headers: { "Content-Type": "application/json" },
1754
+ body: JSON.stringify(payload),
1755
+ signal: AbortSignal.timeout(5000)
1756
+ });
1757
+ return response.ok;
1758
+ } catch {
1759
+ return false;
1760
+ }
1761
+ }
1762
+ var init_webhooks = __esm(() => {
1763
+ init_config();
1764
+ init_database();
1765
+ });
1766
+
651
1767
  // src/server/serve.ts
652
1768
  init_database();
1769
+ init_pricing();
1770
+
1771
+ // src/lib/agents.ts
1772
+ var AGENTS = [
1773
+ "claude",
1774
+ "takumi",
1775
+ "codex",
1776
+ "gemini",
1777
+ "opencode",
1778
+ "cursor",
1779
+ "pi",
1780
+ "hermes"
1781
+ ];
1782
+ function isAgent(value) {
1783
+ return AGENTS.includes(value);
1784
+ }
653
1785
 
654
1786
  // src/ingest/claude.ts
655
1787
  init_database();
@@ -657,17 +1789,120 @@ init_pricing();
657
1789
  import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
658
1790
  import { homedir as homedir2 } from "os";
659
1791
  import { join as join2, basename } from "path";
660
- function autoDetectProject(cwd, projects) {
661
- return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
662
- }
663
- var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
664
- var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
665
- function dirNameToPath(dirName) {
666
- return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
1792
+
1793
+ // src/lib/savings.ts
1794
+ function periodWhere2(period, column) {
1795
+ switch (period) {
1796
+ case "today":
1797
+ return `DATE(${column}) = DATE('now')`;
1798
+ case "yesterday":
1799
+ return `DATE(${column}) = DATE('now', '-1 day')`;
1800
+ case "week":
1801
+ return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
1802
+ case "month":
1803
+ return `${column} >= DATE('now', 'start of month')`;
1804
+ case "year":
1805
+ return `${column} >= DATE('now', 'start of year')`;
1806
+ case "all":
1807
+ return "1=1";
1808
+ }
667
1809
  }
668
- function collectJsonlFiles(projectDir) {
669
- const files = [];
670
- function walk(dir) {
1810
+ function prorateMonthlyFee(monthlyFee, period) {
1811
+ const now = new Date;
1812
+ const dayOfMonth = now.getDate();
1813
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
1814
+ switch (period) {
1815
+ case "today":
1816
+ case "yesterday":
1817
+ return monthlyFee / daysInMonth;
1818
+ case "week":
1819
+ return monthlyFee / daysInMonth * 7;
1820
+ case "month":
1821
+ return monthlyFee;
1822
+ case "year":
1823
+ return monthlyFee * 12;
1824
+ case "all":
1825
+ return monthlyFee;
1826
+ }
1827
+ }
1828
+ function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
1829
+ return Math.max(0, apiEquivalent - onDemand - subscriptionFee);
1830
+ }
1831
+ function querySavingsSummary(db, period, agent) {
1832
+ const where = periodWhere2(period, "timestamp");
1833
+ const agentClause = agent ? " AND agent = ?" : "";
1834
+ const params = agent ? [agent] : [];
1835
+ const apiRow = db.prepare(`
1836
+ SELECT COALESCE(SUM(cost_usd), 0) as total
1837
+ FROM requests
1838
+ WHERE ${where}${agentClause}
1839
+ AND COALESCE(cost_basis, 'estimated') IN ('metered_api', 'estimated', 'unknown')
1840
+ `).get(...params);
1841
+ const includedRow = db.prepare(`
1842
+ SELECT COALESCE(SUM(cost_usd), 0) as total
1843
+ FROM requests
1844
+ WHERE ${where}${agentClause}
1845
+ AND cost_basis = 'subscription_included'
1846
+ `).get(...params);
1847
+ const subWhere = periodWhere2(period, "date");
1848
+ const onDemandRow = db.prepare(`
1849
+ SELECT COALESCE(SUM(value), 0) as total
1850
+ FROM usage_snapshots
1851
+ WHERE ${subWhere}${agent ? " AND agent = ?" : ""}
1852
+ AND metric = 'on_demand_usd'
1853
+ `).get(...params);
1854
+ const subs = db.prepare(`
1855
+ SELECT COALESCE(SUM(monthly_fee_usd), 0) as total
1856
+ FROM subscriptions
1857
+ WHERE active = 1${agent ? " AND agent = ?" : ""}
1858
+ `).get(...agent ? [agent] : []);
1859
+ const subscriptionFee = prorateMonthlyFee(subs.total, period);
1860
+ const apiEquivalent = apiRow.total + includedRow.total;
1861
+ const onDemand = onDemandRow.total;
1862
+ const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
1863
+ const byAgent = {};
1864
+ if (!agent) {
1865
+ for (const row of db.prepare(`
1866
+ SELECT agent, COALESCE(SUM(cost_usd), 0) as api_eq
1867
+ FROM requests WHERE ${where}
1868
+ GROUP BY agent
1869
+ `).all()) {
1870
+ byAgent[row.agent] = {
1871
+ api_equivalent_usd: row.api_eq,
1872
+ saved_usd: row.api_eq
1873
+ };
1874
+ }
1875
+ }
1876
+ return {
1877
+ period,
1878
+ api_equivalent_usd: apiEquivalent,
1879
+ subscription_fee_usd: subscriptionFee,
1880
+ included_consumed_usd: includedRow.total,
1881
+ on_demand_usd: onDemand,
1882
+ saved_usd: saved,
1883
+ by_agent: byAgent
1884
+ };
1885
+ }
1886
+ function defaultCostBasisForAgent(agent) {
1887
+ if (agent === "claude")
1888
+ return "metered_api";
1889
+ if (agent === "cursor")
1890
+ return "subscription_included";
1891
+ return "estimated";
1892
+ }
1893
+
1894
+ // src/ingest/claude.ts
1895
+ function autoDetectProject(cwd, projects) {
1896
+ return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
1897
+ }
1898
+ var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
1899
+ var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
1900
+ function dirNameToPath(dirName) {
1901
+ return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
1902
+ }
1903
+ function collectJsonlFiles(projectDir) {
1904
+ const files = [];
1905
+ function walk(dir) {
671
1906
  try {
672
1907
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
673
1908
  if (entry.isDirectory())
@@ -680,11 +1915,11 @@ function collectJsonlFiles(projectDir) {
680
1915
  walk(projectDir);
681
1916
  return files;
682
1917
  }
683
- async function ingestClaude(db, verbose = false, _telemetryDir) {
684
- return ingestJsonlProjects(db, CLAUDE_PROJECTS_DIR, "claude", verbose);
1918
+ async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
1919
+ return ingestJsonlProjects(db, projectsDir, "claude", verbose);
685
1920
  }
686
- async function ingestTakumi(db, verbose = false) {
687
- return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
1921
+ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
1922
+ return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
688
1923
  }
689
1924
  async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
690
1925
  if (!existsSync2(projectsDir)) {
@@ -745,13 +1980,21 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
745
1980
  continue;
746
1981
  const inputTokens = usage.input_tokens ?? 0;
747
1982
  const outputTokens = usage.output_tokens ?? 0;
748
- const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
1983
+ const cacheWrite5mTokens = usage.cache_creation?.ephemeral_5m_input_tokens ?? usage.cache_creation_input_tokens ?? 0;
1984
+ const cacheWrite1hTokens = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
1985
+ const cacheWriteTokens = cacheWrite5mTokens + cacheWrite1hTokens;
749
1986
  const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
750
1987
  const timestamp = entry.timestamp ?? new Date().toISOString();
751
- if (inputTokens + outputTokens + cacheWriteTokens === 0)
1988
+ if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
752
1989
  continue;
753
- const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
754
- const reqId = `${agentName}-${sessionId}-${timestamp}`;
1990
+ let costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens);
1991
+ costUsd = applyClaudeModifiers(costUsd, model, usage, entry);
1992
+ const serverToolUse = usage.server_tool_use;
1993
+ if (serverToolUse?.web_search_requests) {
1994
+ costUsd += serverToolUse.web_search_requests * 0.01;
1995
+ }
1996
+ const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
1997
+ const reqId = `${agentName}-${sourceRequestId}`;
755
1998
  upsertRequest(db, {
756
1999
  id: reqId,
757
2000
  agent: agentName,
@@ -761,10 +2004,13 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
761
2004
  output_tokens: outputTokens,
762
2005
  cache_read_tokens: cacheReadTokens,
763
2006
  cache_create_tokens: cacheWriteTokens,
2007
+ cache_create_5m_tokens: cacheWrite5mTokens,
2008
+ cache_create_1h_tokens: cacheWrite1hTokens,
764
2009
  cost_usd: costUsd,
2010
+ cost_basis: defaultCostBasisForAgent(agentName),
765
2011
  duration_ms: 0,
766
2012
  timestamp,
767
- source_request_id: reqId,
2013
+ source_request_id: sourceRequestId,
768
2014
  machine_id: machineId
769
2015
  });
770
2016
  if (!touchedSessions.has(sessionId)) {
@@ -799,82 +2045,254 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
799
2045
  }
800
2046
  return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
801
2047
  }
2048
+ function applyClaudeModifiers(costUsd, model, usage, entry) {
2049
+ let multiplier = 1;
2050
+ const speed = usage.speed ?? entry.message?.speed ?? entry.speed;
2051
+ if (speed === "fast" && model.includes("opus-4-6")) {
2052
+ multiplier *= 6;
2053
+ }
2054
+ const inferenceGeo = usage.inference_geo ?? entry.message?.inference_geo ?? entry.inference_geo;
2055
+ if (inferenceGeo && ["us", "us-only", "us_only"].includes(inferenceGeo) && supportsClaudeDataResidencyPricing(model)) {
2056
+ multiplier *= 1.1;
2057
+ }
2058
+ return costUsd * multiplier;
2059
+ }
2060
+ function supportsClaudeDataResidencyPricing(model) {
2061
+ const normalized = normalizeModelName(model);
2062
+ const match = normalized.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?(?:-|$)/);
2063
+ if (!match)
2064
+ return false;
2065
+ const major = Number(match[2]);
2066
+ const minor = match[3] ? Number(match[3]) : 0;
2067
+ return major > 4 || major === 4 && minor >= 6;
2068
+ }
802
2069
 
803
2070
  // src/ingest/codex.ts
804
2071
  init_database();
2072
+ init_pricing();
805
2073
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
806
2074
  import { homedir as homedir3 } from "os";
807
2075
  import { join as join3, basename as basename2 } from "path";
808
2076
  import { Database as BunDatabase } from "bun:sqlite";
809
- var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
810
- var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
2077
+ var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
2078
+ var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
2079
+ var CODEX_INGEST_VERSION = "rollout-token-dedupe-v2";
2080
+ function codexDbPath() {
2081
+ return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
2082
+ }
2083
+ function codexConfigPath() {
2084
+ return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
2085
+ }
2086
+ function readCodexModel() {
2087
+ const configPath = codexConfigPath();
2088
+ if (!existsSync3(configPath))
2089
+ return "gpt-5-codex";
2090
+ try {
2091
+ const content = readFileSync2(configPath, "utf-8");
2092
+ const match = content.match(/^model\s*=\s*"([^"]+)"/m);
2093
+ return match?.[1] ?? "gpt-5-codex";
2094
+ } catch {
2095
+ return "gpt-5-codex";
2096
+ }
2097
+ }
2098
+ function buildThreadQuery(codexDb) {
2099
+ const cols = new Set(codexDb.prepare(`PRAGMA table_info(threads)`).all().map((c) => c.name));
2100
+ const modelSelect = cols.has("model") ? "model" : "NULL AS model";
2101
+ const rolloutSelect = cols.has("rollout_path") ? "rollout_path" : "NULL AS rollout_path";
2102
+ const providerSelect = cols.has("model_provider") ? "model_provider" : "NULL AS model_provider";
2103
+ return `
2104
+ SELECT id, ${rolloutSelect}, cwd, created_at, updated_at, tokens_used, title,
2105
+ ${providerSelect}, ${modelSelect}
2106
+ FROM threads WHERE tokens_used > 0
2107
+ `;
2108
+ }
2109
+ function readTokenEvents(rolloutPath) {
2110
+ if (!rolloutPath || !existsSync3(rolloutPath))
2111
+ return [];
2112
+ const events = [];
2113
+ const seen = new Set;
2114
+ for (const line of readFileSync2(rolloutPath, "utf-8").split(`
2115
+ `)) {
2116
+ if (!line.trim())
2117
+ continue;
2118
+ let entry;
2119
+ try {
2120
+ entry = JSON.parse(line);
2121
+ } catch {
2122
+ continue;
2123
+ }
2124
+ if (!entry || typeof entry !== "object")
2125
+ continue;
2126
+ const payload = entry["payload"];
2127
+ if (!payload || payload["type"] !== "token_count")
2128
+ continue;
2129
+ const info = payload["info"];
2130
+ const usage = info?.["last_token_usage"];
2131
+ if (!usage)
2132
+ continue;
2133
+ const total = usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
2134
+ if (total <= 0)
2135
+ continue;
2136
+ const key = JSON.stringify(usage);
2137
+ if (seen.has(key))
2138
+ continue;
2139
+ seen.add(key);
2140
+ const timestamp = entry["timestamp"];
2141
+ events.push({ usage, timestamp: typeof timestamp === "string" ? timestamp : undefined });
2142
+ }
2143
+ return events;
2144
+ }
2145
+ function fallbackEvents(totalTokens) {
2146
+ const inputTokens = Math.floor(totalTokens * 0.6);
2147
+ return [{
2148
+ usage: {
2149
+ input_tokens: inputTokens,
2150
+ cached_input_tokens: 0,
2151
+ output_tokens: totalTokens - inputTokens,
2152
+ total_tokens: totalTokens
2153
+ }
2154
+ }];
2155
+ }
811
2156
  async function ingestCodex(db, verbose = false) {
812
- if (!existsSync3(CODEX_DB_PATH)) {
2157
+ const dbPath = codexDbPath();
2158
+ if (!existsSync3(dbPath)) {
813
2159
  if (verbose)
814
- console.log("Codex DB not found:", CODEX_DB_PATH);
815
- return { sessions: 0 };
2160
+ console.log("Codex DB not found:", dbPath);
2161
+ return { sessions: 0, requests: 0 };
816
2162
  }
817
2163
  const machineId = getMachineId();
818
2164
  let codexDb = null;
819
2165
  let ingested = 0;
2166
+ let requests = 0;
820
2167
  try {
821
- codexDb = new BunDatabase(CODEX_DB_PATH, { readonly: true });
822
- const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
2168
+ codexDb = new BunDatabase(dbPath, { readonly: true });
2169
+ const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
823
2170
  for (const thread of threads) {
824
- const stateKey = thread.id;
825
- const processed = getIngestState(db, "codex", stateKey);
826
- if (processed === "done")
2171
+ const model = thread.model ?? readCodexModel();
2172
+ const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
2173
+ const processed = getIngestState(db, "codex", thread.id);
2174
+ if (processed === stateValue)
827
2175
  continue;
828
- const costUsd = 0;
829
2176
  const projectPath = thread.cwd ?? "";
830
2177
  const projectName = projectPath ? basename2(projectPath) : "unknown";
2178
+ const sessionId = `codex-${thread.id}`;
831
2179
  const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
832
2180
  const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
833
2181
  upsertSession(db, {
834
- id: `codex-${thread.id}`,
2182
+ id: sessionId,
835
2183
  agent: "codex",
836
2184
  project_path: projectPath,
837
2185
  project_name: projectName,
838
2186
  started_at: startedAt,
839
2187
  ended_at: endedAt,
840
- total_cost_usd: costUsd,
841
- total_tokens: thread.tokens_used,
842
- request_count: 1,
2188
+ total_cost_usd: 0,
2189
+ total_tokens: 0,
2190
+ request_count: 0,
843
2191
  machine_id: machineId
844
2192
  });
845
- setIngestState(db, "codex", stateKey, "done");
2193
+ const events = readTokenEvents(thread.rollout_path);
2194
+ const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
2195
+ db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
2196
+ tokenEvents.forEach((event, index) => {
2197
+ const usage = event.usage;
2198
+ const inputTotal = usage.input_tokens ?? 0;
2199
+ const cacheReadTokens = usage.cached_input_tokens ?? 0;
2200
+ const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
2201
+ const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
2202
+ const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
2203
+ const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
2204
+ const requestId = `${sessionId}-${index}`;
2205
+ upsertRequest(db, {
2206
+ id: requestId,
2207
+ agent: "codex",
2208
+ session_id: sessionId,
2209
+ model,
2210
+ input_tokens: inputTokens,
2211
+ output_tokens: outputTokens,
2212
+ cache_read_tokens: cacheReadTokens,
2213
+ cache_create_tokens: 0,
2214
+ cost_usd: costUsd,
2215
+ cost_basis: defaultCostBasisForAgent("codex"),
2216
+ duration_ms: 0,
2217
+ timestamp,
2218
+ source_request_id: requestId,
2219
+ machine_id: machineId
2220
+ });
2221
+ requests++;
2222
+ });
2223
+ rollupSession(db, sessionId);
2224
+ setIngestState(db, "codex", thread.id, stateValue);
846
2225
  ingested++;
847
2226
  if (verbose)
848
- console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens \u2192 $${costUsd.toFixed(4)}`);
2227
+ console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens on ${model}`);
849
2228
  }
850
2229
  } finally {
851
2230
  codexDb?.close();
852
2231
  }
853
- return { sessions: ingested };
2232
+ return { sessions: ingested, requests };
854
2233
  }
855
2234
 
856
2235
  // src/ingest/gemini.ts
857
2236
  init_database();
2237
+ init_pricing();
858
2238
  import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
859
2239
  import { homedir as homedir4 } from "os";
860
- import { join as join4 } from "path";
861
- var GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
2240
+ import { join as join4, basename as basename3 } from "path";
2241
+ var DEFAULT_GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
2242
+ var DEFAULT_GEMINI_HISTORY_DIR = join4(homedir4(), ".gemini", "history");
2243
+ function geminiTmpDir() {
2244
+ return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
2245
+ }
2246
+ function geminiHistoryDir() {
2247
+ return process.env["HASNA_ECONOMY_GEMINI_HISTORY_DIR"] ?? DEFAULT_GEMINI_HISTORY_DIR;
2248
+ }
2249
+ function numberField(...values) {
2250
+ for (const value of values) {
2251
+ if (typeof value === "number" && Number.isFinite(value))
2252
+ return value;
2253
+ }
2254
+ return 0;
2255
+ }
2256
+ function listProjectDirs(...roots) {
2257
+ const dirs = new Set;
2258
+ for (const root of roots) {
2259
+ if (!existsSync4(root))
2260
+ continue;
2261
+ try {
2262
+ for (const entry of readdirSync3(root, { withFileTypes: true })) {
2263
+ if (entry.isDirectory())
2264
+ dirs.add(join4(root, entry.name));
2265
+ }
2266
+ } catch {}
2267
+ }
2268
+ return [...dirs];
2269
+ }
2270
+ function projectRoot(projectDir, chatData) {
2271
+ if (chatData.projectPath)
2272
+ return chatData.projectPath;
2273
+ if (chatData.project_path)
2274
+ return chatData.project_path;
2275
+ const rootFile = join4(projectDir, ".project_root");
2276
+ try {
2277
+ if (existsSync4(rootFile))
2278
+ return readFileSync3(rootFile, "utf-8").trim();
2279
+ } catch {}
2280
+ return "";
2281
+ }
862
2282
  async function ingestGemini(db, verbose) {
863
- if (!existsSync4(GEMINI_TMP_DIR)) {
2283
+ const tmpDir = geminiTmpDir();
2284
+ const historyDir = geminiHistoryDir();
2285
+ if (!existsSync4(tmpDir) && !existsSync4(historyDir)) {
864
2286
  if (verbose)
865
- console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
866
- return { sessions: 0 };
2287
+ console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
2288
+ return { sessions: 0, requests: 0 };
867
2289
  }
868
2290
  const machineId = getMachineId();
869
2291
  let totalSessions = 0;
2292
+ let totalRequests = 0;
870
2293
  const touchedSessions = new Set;
871
- let projectHashDirs = [];
872
- try {
873
- projectHashDirs = readdirSync3(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join4(GEMINI_TMP_DIR, d.name));
874
- } catch {
875
- return { sessions: 0 };
876
- }
877
- for (const projectDir of projectHashDirs) {
2294
+ const projectDirs = listProjectDirs(tmpDir, historyDir);
2295
+ for (const projectDir of projectDirs) {
878
2296
  const chatsDir = join4(projectDir, "chats");
879
2297
  if (!existsSync4(chatsDir))
880
2298
  continue;
@@ -901,17 +2319,19 @@ async function ingestGemini(db, verbose) {
901
2319
  } catch {
902
2320
  continue;
903
2321
  }
904
- const sessionId = chatData.sessionId;
2322
+ const sessionId = chatData.sessionId ?? chatData.id ?? basename3(filePath, ".json");
905
2323
  if (!sessionId)
906
2324
  continue;
907
2325
  const startTime = chatData.startTime ?? new Date().toISOString();
2326
+ const projectPath = projectRoot(projectDir, chatData);
2327
+ const projectName = projectPath ? basename3(projectPath) : "";
908
2328
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
909
2329
  if (!existing) {
910
2330
  const session = {
911
2331
  id: sessionId,
912
2332
  agent: "gemini",
913
- project_path: "",
914
- project_name: "",
2333
+ project_path: projectPath,
2334
+ project_name: projectName,
915
2335
  started_at: startTime,
916
2336
  ended_at: chatData.lastUpdated ?? null,
917
2337
  total_cost_usd: 0,
@@ -920,26 +2340,1074 @@ async function ingestGemini(db, verbose) {
920
2340
  machine_id: machineId
921
2341
  };
922
2342
  upsertSession(db, session);
923
- touchedSessions.add(sessionId);
924
2343
  totalSessions++;
925
2344
  }
2345
+ touchedSessions.add(sessionId);
2346
+ for (const [index, message] of (chatData.messages ?? []).entries()) {
2347
+ const usage = message.usage ?? message.usageMetadata ?? message.response?.usageMetadata;
2348
+ if (!usage)
2349
+ continue;
2350
+ const model = message.model ?? message.response?.modelVersion ?? message.response?.model ?? chatData.model;
2351
+ if (!model)
2352
+ continue;
2353
+ const toolUsePromptTokens = numberField(usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count);
2354
+ const inputTotal = numberField(usage.inputTokens, usage.input_tokens, usage.promptTokenCount, usage.prompt_token_count) + toolUsePromptTokens;
2355
+ const cacheReadTokens = numberField(usage.cachedInputTokens, usage.cache_read_tokens, usage.cachedContentTokenCount, usage.cached_content_token_count);
2356
+ const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
2357
+ const thoughtsTokens = numberField(usage.thoughtsTokenCount, usage.thoughts_token_count);
2358
+ const outputTokens = numberField(usage.outputTokens, usage.output_tokens, usage.candidatesTokenCount, usage.candidates_token_count) + thoughtsTokens;
2359
+ const totalTokens = numberField(usage.totalTokens, usage.total_tokens, usage.totalTokenCount, usage.total_token_count);
2360
+ if (inputTokens + outputTokens + cacheReadTokens + totalTokens === 0)
2361
+ continue;
2362
+ const computedCost = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
2363
+ const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
2364
+ const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
2365
+ const requestId = `gemini-${sessionId}-${message.id ?? index}`;
2366
+ upsertRequest(db, {
2367
+ id: requestId,
2368
+ agent: "gemini",
2369
+ session_id: sessionId,
2370
+ model,
2371
+ input_tokens: inputTokens,
2372
+ output_tokens: outputTokens,
2373
+ cache_read_tokens: cacheReadTokens,
2374
+ cache_create_tokens: 0,
2375
+ cost_usd: costUsd,
2376
+ cost_basis: defaultCostBasisForAgent("gemini"),
2377
+ duration_ms: 0,
2378
+ timestamp,
2379
+ source_request_id: message.id ?? requestId,
2380
+ machine_id: machineId
2381
+ });
2382
+ totalRequests++;
2383
+ }
926
2384
  setIngestState(db, "gemini", stateKey, fileMtime);
927
2385
  }
928
2386
  }
929
2387
  for (const sessionId of touchedSessions) {
930
2388
  rollupSession(db, sessionId);
931
2389
  }
932
- return { sessions: totalSessions };
2390
+ return { sessions: totalSessions, requests: totalRequests };
933
2391
  }
934
2392
 
935
- // src/server/serve.ts
2393
+ // src/ingest/opencode.ts
2394
+ init_database();
936
2395
  init_pricing();
2396
+ import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
2397
+ import { homedir as homedir5 } from "os";
2398
+ import { join as join5 } from "path";
2399
+ var OPENCODE_STORAGE = join5(homedir5(), ".local", "share", "opencode", "storage");
2400
+ function walkJsonFiles(dir, acc = []) {
2401
+ if (!existsSync5(dir))
2402
+ return acc;
2403
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
2404
+ const p = join5(dir, entry.name);
2405
+ if (entry.isDirectory())
2406
+ walkJsonFiles(p, acc);
2407
+ else if (entry.name.endsWith(".json"))
2408
+ acc.push(p);
2409
+ }
2410
+ return acc;
2411
+ }
2412
+ function parseSessionIdFromPath(filePath) {
2413
+ const parts = filePath.split("/");
2414
+ const msgIdx = parts.indexOf("message");
2415
+ if (msgIdx >= 0 && parts[msgIdx + 1])
2416
+ return parts[msgIdx + 1];
2417
+ const sessionIdx = parts.indexOf("session");
2418
+ if (sessionIdx >= 0 && parts[parts.length - 1]?.endsWith(".json")) {
2419
+ return parts[parts.length - 1].replace(/\.json$/, "");
2420
+ }
2421
+ return null;
2422
+ }
2423
+ async function ingestOpenCode(db, verbose = false) {
2424
+ const messageDir = join5(OPENCODE_STORAGE, "message");
2425
+ const files = walkJsonFiles(messageDir);
2426
+ let requests = 0;
2427
+ const touched = new Set;
2428
+ const machineId = getMachineId();
2429
+ const now = new Date().toISOString();
2430
+ for (const file of files) {
2431
+ const mtime = statSync4(file).mtimeMs;
2432
+ const stateKey = file;
2433
+ const prev = getIngestState(db, "opencode", stateKey);
2434
+ if (prev && Number(prev) >= mtime)
2435
+ continue;
2436
+ let parsed;
2437
+ try {
2438
+ parsed = JSON.parse(readFileSync4(file, "utf-8"));
2439
+ } catch {
2440
+ continue;
2441
+ }
2442
+ if (parsed.role !== "assistant")
2443
+ continue;
2444
+ const usage = parsed.usage;
2445
+ if (!usage)
2446
+ continue;
2447
+ const sessionId = parseSessionIdFromPath(file) ?? `opencode-${statSync4(file).ino}`;
2448
+ const model = normalizeModelName(parsed.model ?? "unknown");
2449
+ const input = usage.inputTokens ?? 0;
2450
+ const output = usage.outputTokens ?? 0;
2451
+ const cacheRead = usage.cacheReadTokens ?? 0;
2452
+ const cacheWrite = usage.cacheWriteTokens ?? 0;
2453
+ if (input + output + cacheRead + cacheWrite === 0 && !usage.cost)
2454
+ continue;
2455
+ const timestamp = usage && parsed.time?.created ? new Date(parsed.time.created).toISOString() : new Date(statSync4(file).mtime).toISOString();
2456
+ const sourceId = file.replace(OPENCODE_STORAGE, "");
2457
+ const reqId = `opencode-${sourceId}`;
2458
+ const costUsd = usage.cost ?? computeCostFromDb(db, model, input, output, cacheRead, cacheWrite, 0);
2459
+ upsertRequest(db, {
2460
+ id: reqId,
2461
+ agent: "opencode",
2462
+ session_id: sessionId,
2463
+ model,
2464
+ input_tokens: input,
2465
+ output_tokens: output,
2466
+ cache_read_tokens: cacheRead,
2467
+ cache_create_tokens: cacheWrite,
2468
+ cost_usd: costUsd,
2469
+ cost_basis: defaultCostBasisForAgent("opencode"),
2470
+ duration_ms: 0,
2471
+ timestamp,
2472
+ source_request_id: sourceId,
2473
+ machine_id: machineId,
2474
+ updated_at: now
2475
+ });
2476
+ requests++;
2477
+ if (!touched.has(sessionId)) {
2478
+ upsertSession(db, {
2479
+ id: sessionId,
2480
+ agent: "opencode",
2481
+ project_path: "",
2482
+ project_name: "",
2483
+ started_at: timestamp,
2484
+ ended_at: null,
2485
+ total_cost_usd: 0,
2486
+ total_tokens: 0,
2487
+ request_count: 0,
2488
+ machine_id: machineId,
2489
+ updated_at: now
2490
+ });
2491
+ touched.add(sessionId);
2492
+ }
2493
+ setIngestState(db, "opencode", stateKey, String(mtime));
2494
+ if (verbose)
2495
+ console.log(` opencode: ${reqId} ${model} $${costUsd.toFixed(4)}`);
2496
+ }
2497
+ for (const sid of touched)
2498
+ rollupSession(db, sid);
2499
+ return { files: files.length, requests, sessions: touched.size };
2500
+ }
2501
+
2502
+ // src/ingest/cursor.ts
2503
+ init_database();
2504
+ function getCursorSessionToken() {
2505
+ return process.env["CURSOR_SESSION_TOKEN"] ?? process.env["CURSOR_API_TOKEN"] ?? null;
2506
+ }
2507
+ async function cursorFetch(path, token) {
2508
+ try {
2509
+ const res = await fetch(`https://cursor.com${path}`, {
2510
+ headers: {
2511
+ Cookie: `WorkosCursorSessionToken=${token}`,
2512
+ Accept: "application/json"
2513
+ },
2514
+ signal: AbortSignal.timeout(1e4)
2515
+ });
2516
+ if (!res.ok)
2517
+ return null;
2518
+ return await res.json();
2519
+ } catch {
2520
+ return null;
2521
+ }
2522
+ }
2523
+ async function ingestCursor(db, verbose = false) {
2524
+ const token = getCursorSessionToken();
2525
+ if (!token) {
2526
+ if (verbose)
2527
+ console.log(" cursor: skipped \u2014 set CURSOR_SESSION_TOKEN");
2528
+ return { requests: 0, snapshots: 0 };
2529
+ }
2530
+ const today = new Date().toISOString().substring(0, 10);
2531
+ const prev = getIngestState(db, "cursor", `sync-${today}`);
2532
+ if (prev)
2533
+ return { requests: 0, snapshots: 0 };
2534
+ const machineId = getMachineId();
2535
+ const now = new Date().toISOString();
2536
+ let snapshots = 0;
2537
+ const usage = await cursorFetch("/api/usage", token);
2538
+ if (usage?.premiumRequests != null && usage.maxPremiumRequests) {
2539
+ upsertUsageSnapshot(db, {
2540
+ agent: "cursor",
2541
+ date: today,
2542
+ metric: "premium_requests_used",
2543
+ value: usage.premiumRequests,
2544
+ unit: "count",
2545
+ machine_id: machineId
2546
+ });
2547
+ upsertUsageSnapshot(db, {
2548
+ agent: "cursor",
2549
+ date: today,
2550
+ metric: "premium_requests_limit",
2551
+ value: usage.maxPremiumRequests,
2552
+ unit: "count",
2553
+ machine_id: machineId
2554
+ });
2555
+ snapshots += 2;
2556
+ }
2557
+ const summary = await cursorFetch("/api/usage-summary", token);
2558
+ const onDemand = summary?.individualUsage?.spend ?? summary?.teamUsage?.spend ?? 0;
2559
+ const included = summary?.individualUsage?.includedSpend ?? 0;
2560
+ if (onDemand > 0) {
2561
+ upsertUsageSnapshot(db, {
2562
+ agent: "cursor",
2563
+ date: today,
2564
+ metric: "on_demand_usd",
2565
+ value: onDemand,
2566
+ unit: "usd",
2567
+ machine_id: machineId
2568
+ });
2569
+ snapshots++;
2570
+ }
2571
+ if (included > 0) {
2572
+ upsertUsageSnapshot(db, {
2573
+ agent: "cursor",
2574
+ date: today,
2575
+ metric: "included_consumed_usd",
2576
+ value: included,
2577
+ unit: "usd",
2578
+ machine_id: machineId
2579
+ });
2580
+ snapshots++;
2581
+ }
2582
+ const sessionId = `cursor-${today}-${machineId}`;
2583
+ if (onDemand + included > 0) {
2584
+ upsertSession(db, {
2585
+ id: sessionId,
2586
+ agent: "cursor",
2587
+ project_path: "",
2588
+ project_name: "Cursor subscription",
2589
+ started_at: `${today}T00:00:00.000Z`,
2590
+ ended_at: now,
2591
+ total_cost_usd: onDemand + included,
2592
+ total_tokens: 0,
2593
+ request_count: 1,
2594
+ machine_id: machineId,
2595
+ updated_at: now
2596
+ });
2597
+ upsertRequest(db, {
2598
+ id: `cursor-${today}-${machineId}-usage`,
2599
+ agent: "cursor",
2600
+ session_id: sessionId,
2601
+ model: "cursor-subscription",
2602
+ input_tokens: 0,
2603
+ output_tokens: 0,
2604
+ cache_read_tokens: 0,
2605
+ cache_create_tokens: 0,
2606
+ cost_usd: onDemand + included,
2607
+ cost_basis: "subscription_included",
2608
+ duration_ms: 0,
2609
+ timestamp: now,
2610
+ source_request_id: today,
2611
+ machine_id: machineId,
2612
+ updated_at: now
2613
+ });
2614
+ rollupSession(db, sessionId);
2615
+ }
2616
+ setIngestState(db, "cursor", `sync-${today}`, now);
2617
+ if (verbose)
2618
+ console.log(` cursor: on-demand $${onDemand.toFixed(2)}, included $${included.toFixed(2)}`);
2619
+ return { requests: onDemand + included > 0 ? 1 : 0, snapshots };
2620
+ }
2621
+
2622
+ // src/ingest/pi.ts
2623
+ init_database();
2624
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
2625
+ import { homedir as homedir6 } from "os";
2626
+ import { join as join6 } from "path";
2627
+ var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join6(homedir6(), ".pi", "agent", "sessions");
2628
+ function walkSessions(dir, acc = []) {
2629
+ if (!existsSync6(dir))
2630
+ return acc;
2631
+ for (const entry of readdirSync5(dir, { withFileTypes: true })) {
2632
+ const p = join6(dir, entry.name);
2633
+ if (entry.isDirectory())
2634
+ walkSessions(p, acc);
2635
+ else if (entry.name.endsWith(".json"))
2636
+ acc.push(p);
2637
+ }
2638
+ return acc;
2639
+ }
2640
+ async function ingestPi(db, verbose = false) {
2641
+ const files = walkSessions(PI_SESSION_DIR);
2642
+ let requests = 0;
2643
+ const touched = new Set;
2644
+ const machineId = getMachineId();
2645
+ const now = new Date().toISOString();
2646
+ for (const file of files) {
2647
+ const mtime = statSync5(file).mtimeMs;
2648
+ const prev = getIngestState(db, "pi", file);
2649
+ if (prev && Number(prev) >= mtime)
2650
+ continue;
2651
+ let data;
2652
+ try {
2653
+ data = JSON.parse(readFileSync5(file, "utf-8"));
2654
+ } catch {
2655
+ continue;
2656
+ }
2657
+ const sessionId = data.id ?? file.replace(/\.json$/, "").split("/").pop() ?? `pi-${statSync5(file).ino}`;
2658
+ const turns = data.turns ?? data.messages?.filter((m) => m.role === "assistant") ?? [];
2659
+ for (let i = 0;i < turns.length; i++) {
2660
+ const turn = turns[i];
2661
+ const usage = turn.usage;
2662
+ if (!usage)
2663
+ continue;
2664
+ const input = usage.input ?? 0;
2665
+ const output = usage.output ?? 0;
2666
+ if (input + output === 0 && !usage.cost)
2667
+ continue;
2668
+ const model = turn.model ?? turn.provider ?? "unknown";
2669
+ const timestamp = turn.timestamp ?? new Date(statSync5(file).mtime).toISOString();
2670
+ const reqId = `pi-${sessionId}-${i}`;
2671
+ upsertRequest(db, {
2672
+ id: reqId,
2673
+ agent: "pi",
2674
+ session_id: sessionId,
2675
+ model,
2676
+ input_tokens: input,
2677
+ output_tokens: output,
2678
+ cache_read_tokens: usage.cacheRead ?? 0,
2679
+ cache_create_tokens: usage.cacheWrite ?? 0,
2680
+ cost_usd: usage.cost ?? 0,
2681
+ cost_basis: defaultCostBasisForAgent("pi"),
2682
+ duration_ms: 0,
2683
+ timestamp,
2684
+ source_request_id: `${sessionId}-${i}`,
2685
+ machine_id: machineId,
2686
+ updated_at: now
2687
+ });
2688
+ requests++;
2689
+ }
2690
+ if (turns.length > 0) {
2691
+ upsertSession(db, {
2692
+ id: sessionId,
2693
+ agent: "pi",
2694
+ project_path: "",
2695
+ project_name: "",
2696
+ started_at: turns[0]?.timestamp ?? now,
2697
+ ended_at: null,
2698
+ total_cost_usd: 0,
2699
+ total_tokens: 0,
2700
+ request_count: 0,
2701
+ machine_id: machineId,
2702
+ updated_at: now
2703
+ });
2704
+ touched.add(sessionId);
2705
+ }
2706
+ setIngestState(db, "pi", file, String(mtime));
2707
+ if (verbose)
2708
+ console.log(` pi: ${sessionId} (${turns.length} turns)`);
2709
+ }
2710
+ for (const sid of touched)
2711
+ rollupSession(db, sid);
2712
+ return { files: files.length, requests, sessions: touched.size };
2713
+ }
2714
+
2715
+ // src/ingest/hermes.ts
2716
+ init_database();
2717
+ import { existsSync as existsSync7, statSync as statSync6 } from "fs";
2718
+ import { homedir as homedir7 } from "os";
2719
+ import { join as join7 } from "path";
2720
+ var HERMES_DB = join7(homedir7(), ".hermes", "state.db");
2721
+ function mapCostBasis(billingMode) {
2722
+ if (billingMode === "subscription")
2723
+ return "subscription_included";
2724
+ if (billingMode === "api")
2725
+ return "metered_api";
2726
+ return defaultCostBasisForAgent("hermes");
2727
+ }
2728
+ async function ingestHermes(db, verbose = false) {
2729
+ if (!existsSync7(HERMES_DB)) {
2730
+ return { sessions: 0, requests: 0 };
2731
+ }
2732
+ const { Database: Sqlite } = await import("bun:sqlite");
2733
+ const hermes = new Sqlite(HERMES_DB, { readonly: true });
2734
+ const rows = hermes.prepare(`
2735
+ SELECT id, source, model, started_at, ended_at,
2736
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
2737
+ reasoning_tokens, estimated_cost_usd, actual_cost_usd, billing_mode, parent_session_id
2738
+ FROM sessions
2739
+ ORDER BY started_at DESC
2740
+ `).all();
2741
+ const stateKey = "state.db";
2742
+ const mtime = statSyncSafe(HERMES_DB);
2743
+ const prev = getIngestState(db, "hermes", stateKey);
2744
+ if (prev && Number(prev) >= mtime && rows.length === 0) {
2745
+ hermes.close();
2746
+ return { sessions: 0, requests: 0 };
2747
+ }
2748
+ const machineId = getMachineId();
2749
+ const now = new Date().toISOString();
2750
+ let requests = 0;
2751
+ for (const row of rows) {
2752
+ const sessionId = `hermes-${row.id}`;
2753
+ const startedAt = new Date(row.started_at * 1000).toISOString();
2754
+ const endedAt = row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null;
2755
+ const cost = row.actual_cost_usd ?? row.estimated_cost_usd ?? 0;
2756
+ const tokens = row.input_tokens + row.output_tokens + row.cache_read_tokens + row.cache_write_tokens + row.reasoning_tokens;
2757
+ upsertSession(db, {
2758
+ id: sessionId,
2759
+ agent: "hermes",
2760
+ project_path: row.source ?? "",
2761
+ project_name: row.source ?? "",
2762
+ started_at: startedAt,
2763
+ ended_at: endedAt,
2764
+ total_cost_usd: cost,
2765
+ total_tokens: tokens,
2766
+ request_count: 1,
2767
+ machine_id: machineId,
2768
+ updated_at: now
2769
+ });
2770
+ const reqId = `hermes-${row.id}-rollup`;
2771
+ upsertRequest(db, {
2772
+ id: reqId,
2773
+ agent: "hermes",
2774
+ session_id: sessionId,
2775
+ model: row.model ?? "unknown",
2776
+ input_tokens: row.input_tokens,
2777
+ output_tokens: row.output_tokens + row.reasoning_tokens,
2778
+ cache_read_tokens: row.cache_read_tokens,
2779
+ cache_create_tokens: row.cache_write_tokens,
2780
+ cost_usd: cost,
2781
+ cost_basis: mapCostBasis(row.billing_mode),
2782
+ duration_ms: 0,
2783
+ timestamp: endedAt ?? startedAt,
2784
+ source_request_id: row.id,
2785
+ machine_id: machineId,
2786
+ updated_at: now
2787
+ });
2788
+ requests++;
2789
+ rollupSession(db, sessionId);
2790
+ if (verbose)
2791
+ console.log(` hermes: ${sessionId} $${cost.toFixed(4)}`);
2792
+ }
2793
+ setIngestState(db, "hermes", stateKey, String(mtime));
2794
+ hermes.close();
2795
+ return { sessions: rows.length, requests };
2796
+ }
2797
+ function statSyncSafe(path) {
2798
+ try {
2799
+ return statSync6(path).mtimeMs;
2800
+ } catch {
2801
+ return 0;
2802
+ }
2803
+ }
2804
+
2805
+ // src/ingest/claude-quota.ts
2806
+ init_database();
2807
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
2808
+
2809
+ // src/lib/paths.ts
2810
+ import { homedir as homedir8 } from "os";
2811
+ import { join as join8 } from "path";
2812
+ function getHomeDir() {
2813
+ return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir8();
2814
+ }
2815
+ function agentPaths() {
2816
+ const home = getHomeDir();
2817
+ return {
2818
+ claudeProjects: join8(home, ".claude", "projects"),
2819
+ claudeCredentials: join8(home, ".claude", ".credentials.json"),
2820
+ takumiProjects: join8(home, ".takumi", "projects"),
2821
+ codexDir: join8(home, ".codex"),
2822
+ codexDb: join8(home, ".codex", "state_5.sqlite"),
2823
+ codexAuth: join8(home, ".codex", "auth.json"),
2824
+ codexConfig: join8(home, ".codex", "config.toml"),
2825
+ geminiTmp: join8(home, ".gemini", "tmp"),
2826
+ geminiHistory: join8(home, ".gemini", "history"),
2827
+ opencodeMessages: join8(home, ".local", "share", "opencode", "storage", "message"),
2828
+ piSessions: join8(home, ".pi", "agent", "sessions"),
2829
+ hermesDir: join8(home, ".hermes"),
2830
+ hermesDb: join8(home, ".hermes", "state.db")
2831
+ };
2832
+ }
2833
+
2834
+ // src/ingest/claude-quota.ts
2835
+ var CREDENTIALS_PATH = agentPaths().claudeCredentials;
2836
+ var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
2837
+ var OAUTH_BETA = "oauth-2025-04-20";
2838
+ function readClaudeToken() {
2839
+ const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
2840
+ if (fromEnv)
2841
+ return { token: fromEnv };
2842
+ if (!existsSync8(CREDENTIALS_PATH))
2843
+ return null;
2844
+ try {
2845
+ const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
2846
+ const oauth = creds.claudeAiOauth;
2847
+ if (!oauth?.accessToken)
2848
+ return null;
2849
+ return {
2850
+ token: oauth.accessToken,
2851
+ subscriptionType: oauth.subscriptionType,
2852
+ rateLimitTier: oauth.rateLimitTier
2853
+ };
2854
+ } catch {
2855
+ return null;
2856
+ }
2857
+ }
2858
+ function inferMonthlyFee(subscriptionType, rateLimitTier) {
2859
+ const tier = `${subscriptionType ?? ""} ${rateLimitTier ?? ""}`.toLowerCase();
2860
+ if (tier.includes("max") && tier.includes("20"))
2861
+ return 200;
2862
+ if (tier.includes("max"))
2863
+ return 100;
2864
+ if (tier.includes("pro"))
2865
+ return 20;
2866
+ if (tier.includes("team"))
2867
+ return 30;
2868
+ return 20;
2869
+ }
2870
+ async function fetchClaudeOAuthUsage(token) {
2871
+ try {
2872
+ const res = await fetch(USAGE_URL, {
2873
+ headers: {
2874
+ Authorization: `Bearer ${token}`,
2875
+ "anthropic-beta": OAUTH_BETA,
2876
+ Accept: "application/json"
2877
+ },
2878
+ signal: AbortSignal.timeout(1e4)
2879
+ });
2880
+ if (!res.ok)
2881
+ return null;
2882
+ return await res.json();
2883
+ } catch {
2884
+ return null;
2885
+ }
2886
+ }
2887
+ async function ingestClaudeQuota(db, verbose = false) {
2888
+ const auth = readClaudeToken();
2889
+ if (!auth) {
2890
+ if (verbose)
2891
+ console.log(" claude quota: skipped \u2014 no OAuth token (~/.claude/.credentials.json or CLAUDE_OAUTH_TOKEN)");
2892
+ return { snapshots: 0 };
2893
+ }
2894
+ const today = new Date().toISOString().substring(0, 10);
2895
+ const prev = getIngestState(db, "claude", `quota-${today}`);
2896
+ if (prev)
2897
+ return { snapshots: 0 };
2898
+ const usage = await fetchClaudeOAuthUsage(auth.token);
2899
+ if (!usage) {
2900
+ if (verbose)
2901
+ console.log(" claude quota: OAuth usage endpoint unavailable");
2902
+ return { snapshots: 0 };
2903
+ }
2904
+ const machineId = getMachineId();
2905
+ let snapshots = 0;
2906
+ const windows = [
2907
+ ["five_hour_utilization", usage.five_hour],
2908
+ ["seven_day_utilization", usage.seven_day],
2909
+ ["seven_day_sonnet_utilization", usage.seven_day_sonnet],
2910
+ ["seven_day_opus_utilization", usage.seven_day_opus]
2911
+ ];
2912
+ for (const [metric, window] of windows) {
2913
+ if (window?.utilization == null)
2914
+ continue;
2915
+ upsertUsageSnapshot(db, {
2916
+ agent: "claude",
2917
+ date: today,
2918
+ metric,
2919
+ value: Math.round(window.utilization * 1000) / 10,
2920
+ unit: "percent",
2921
+ machine_id: machineId
2922
+ });
2923
+ snapshots++;
2924
+ if (window.resets_at) {
2925
+ upsertUsageSnapshot(db, {
2926
+ agent: "claude",
2927
+ date: today,
2928
+ metric: `${metric}_resets_at`,
2929
+ value: Date.parse(window.resets_at),
2930
+ unit: "epoch_ms",
2931
+ machine_id: machineId
2932
+ });
2933
+ snapshots++;
2934
+ }
2935
+ }
2936
+ if (usage.extra_usage?.spend != null) {
2937
+ upsertUsageSnapshot(db, {
2938
+ agent: "claude",
2939
+ date: today,
2940
+ metric: "on_demand_usd",
2941
+ value: usage.extra_usage.spend,
2942
+ unit: "usd",
2943
+ machine_id: machineId
2944
+ });
2945
+ snapshots++;
2946
+ }
2947
+ if (usage.extra_usage?.limit != null) {
2948
+ upsertUsageSnapshot(db, {
2949
+ agent: "claude",
2950
+ date: today,
2951
+ metric: "on_demand_limit_usd",
2952
+ value: usage.extra_usage.limit,
2953
+ unit: "usd",
2954
+ machine_id: machineId
2955
+ });
2956
+ snapshots++;
2957
+ }
2958
+ const monthlyFee = inferMonthlyFee(auth.subscriptionType, auth.rateLimitTier);
2959
+ const now = new Date().toISOString();
2960
+ upsertSubscription(db, {
2961
+ id: "anthropic-claude-oauth",
2962
+ provider: "anthropic",
2963
+ agent: "claude",
2964
+ plan: auth.rateLimitTier ?? auth.subscriptionType ?? "claude_pro",
2965
+ monthly_fee_usd: monthlyFee,
2966
+ included_usage_usd: monthlyFee,
2967
+ billing_cycle_start: null,
2968
+ reset_policy: "monthly",
2969
+ active: 1,
2970
+ created_at: now,
2971
+ updated_at: now
2972
+ });
2973
+ setIngestState(db, "claude", `quota-${today}`, new Date().toISOString());
2974
+ if (verbose)
2975
+ console.log(` claude quota: ${snapshots} snapshots`);
2976
+ return { snapshots };
2977
+ }
2978
+
2979
+ // src/ingest/codex-quota.ts
2980
+ init_database();
2981
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
2982
+ var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
2983
+ function readCodexAuth() {
2984
+ const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
2985
+ if (fromEnv)
2986
+ return { token: fromEnv, authMode: "chatgpt" };
2987
+ const authPath = agentPaths().codexAuth;
2988
+ if (!existsSync9(authPath))
2989
+ return null;
2990
+ try {
2991
+ const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
2992
+ const token = auth.tokens?.access_token;
2993
+ if (!token)
2994
+ return null;
2995
+ return {
2996
+ token,
2997
+ accountId: auth.tokens?.account_id,
2998
+ authMode: auth.auth_mode ?? "chatgpt"
2999
+ };
3000
+ } catch {
3001
+ return null;
3002
+ }
3003
+ }
3004
+ function planMonthlyFee(planType) {
3005
+ const plan = (planType ?? "").toLowerCase();
3006
+ if (plan.includes("pro"))
3007
+ return 200;
3008
+ if (plan.includes("plus"))
3009
+ return 20;
3010
+ if (plan.includes("team"))
3011
+ return 30;
3012
+ return 20;
3013
+ }
3014
+ async function fetchCodexUsage(token, accountId) {
3015
+ try {
3016
+ const headers = {
3017
+ Authorization: `Bearer ${token}`,
3018
+ Accept: "application/json",
3019
+ "User-Agent": "economy-cli"
3020
+ };
3021
+ if (accountId)
3022
+ headers["ChatGPT-Account-Id"] = accountId;
3023
+ const res = await fetch(WHAM_USAGE_URL, {
3024
+ headers,
3025
+ signal: AbortSignal.timeout(1e4)
3026
+ });
3027
+ if (!res.ok)
3028
+ return null;
3029
+ return await res.json();
3030
+ } catch {
3031
+ return null;
3032
+ }
3033
+ }
3034
+ async function ingestCodexQuota(db, verbose = false) {
3035
+ const auth = readCodexAuth();
3036
+ if (!auth) {
3037
+ if (verbose)
3038
+ console.log(" codex quota: skipped \u2014 no ~/.codex/auth.json or CODEX_OAUTH_TOKEN");
3039
+ return { snapshots: 0 };
3040
+ }
3041
+ if (auth.authMode === "api_key" || auth.authMode === "api") {
3042
+ if (verbose)
3043
+ console.log(" codex quota: skipped \u2014 API key mode (no subscription quota)");
3044
+ return { snapshots: 0 };
3045
+ }
3046
+ const today = new Date().toISOString().substring(0, 10);
3047
+ const prev = getIngestState(db, "codex", `quota-${today}`);
3048
+ if (prev)
3049
+ return { snapshots: 0 };
3050
+ const usage = await fetchCodexUsage(auth.token, auth.accountId);
3051
+ if (!usage) {
3052
+ if (verbose)
3053
+ console.log(" codex quota: wham/usage endpoint unavailable");
3054
+ return { snapshots: 0 };
3055
+ }
3056
+ const machineId = getMachineId();
3057
+ let snapshots = 0;
3058
+ const now = new Date().toISOString();
3059
+ const windows = [
3060
+ ["five_hour_utilization", usage.rate_limit?.primary_window],
3061
+ ["seven_day_utilization", usage.rate_limit?.secondary_window]
3062
+ ];
3063
+ for (const [metric, window] of windows) {
3064
+ if (window?.used_percent == null)
3065
+ continue;
3066
+ upsertUsageSnapshot(db, {
3067
+ agent: "codex",
3068
+ date: today,
3069
+ metric,
3070
+ value: window.used_percent,
3071
+ unit: "percent",
3072
+ machine_id: machineId
3073
+ });
3074
+ snapshots++;
3075
+ if (window.reset_at) {
3076
+ upsertUsageSnapshot(db, {
3077
+ agent: "codex",
3078
+ date: today,
3079
+ metric: `${metric}_resets_at`,
3080
+ value: window.reset_at * 1000,
3081
+ unit: "epoch_ms",
3082
+ machine_id: machineId
3083
+ });
3084
+ snapshots++;
3085
+ }
3086
+ }
3087
+ if (usage.credits?.balance != null) {
3088
+ upsertUsageSnapshot(db, {
3089
+ agent: "codex",
3090
+ date: today,
3091
+ metric: "credits_balance_usd",
3092
+ value: usage.credits.balance,
3093
+ unit: "usd",
3094
+ machine_id: machineId
3095
+ });
3096
+ snapshots++;
3097
+ }
3098
+ const monthlyFee = planMonthlyFee(usage.plan_type);
3099
+ upsertSubscription(db, {
3100
+ id: "openai-codex-oauth",
3101
+ provider: "openai",
3102
+ agent: "codex",
3103
+ plan: usage.plan_type ?? "chatgpt_plus",
3104
+ monthly_fee_usd: monthlyFee,
3105
+ included_usage_usd: monthlyFee,
3106
+ billing_cycle_start: null,
3107
+ reset_policy: "monthly",
3108
+ active: 1,
3109
+ created_at: now,
3110
+ updated_at: now
3111
+ });
3112
+ setIngestState(db, "codex", `quota-${today}`, now);
3113
+ if (verbose)
3114
+ console.log(` codex quota: ${snapshots} snapshots (${usage.plan_type ?? "unknown plan"})`);
3115
+ return { snapshots };
3116
+ }
3117
+
3118
+ // src/lib/sync-all.ts
3119
+ init_database();
3120
+
3121
+ // src/lib/cloud-sync.ts
3122
+ init_database();
3123
+
3124
+ // src/lib/package-metadata.ts
3125
+ import { readFileSync as readFileSync9 } from "fs";
3126
+ var cachedMetadata = null;
3127
+ function getPackageMetadata() {
3128
+ if (cachedMetadata)
3129
+ return cachedMetadata;
3130
+ const raw = readFileSync9(new URL("../../package.json", import.meta.url), "utf8");
3131
+ const parsed = JSON.parse(raw);
3132
+ cachedMetadata = {
3133
+ name: parsed.name ?? "@hasna/economy",
3134
+ version: parsed.version ?? "0.0.0"
3135
+ };
3136
+ return cachedMetadata;
3137
+ }
3138
+ var packageMetadata = getPackageMetadata();
3139
+
3140
+ // src/lib/cloud-sync.ts
3141
+ var CLOUD_TABLES = [
3142
+ "requests",
3143
+ "sessions",
3144
+ "projects",
3145
+ "budgets",
3146
+ "goals",
3147
+ "model_pricing",
3148
+ "billing_daily",
3149
+ "subscriptions",
3150
+ "usage_snapshots",
3151
+ "savings_daily",
3152
+ "machines",
3153
+ "ingest_state"
3154
+ ];
3155
+ function getCloudDatabaseUrl() {
3156
+ return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
3157
+ }
3158
+ function isCloudAutoEnabled() {
3159
+ return process.env["ECONOMY_CLOUD_AUTO"] === "1" || process.env["ECONOMY_CLOUD_AUTO"] === "true";
3160
+ }
3161
+ function getCloudPullIntervalMinutes() {
3162
+ const raw = process.env["ECONOMY_CLOUD_PULL_INTERVAL"];
3163
+ if (!raw)
3164
+ return 15;
3165
+ const n = Number(raw);
3166
+ return Number.isFinite(n) && n > 0 ? n : 15;
3167
+ }
3168
+ async function getCloudPg() {
3169
+ const url = getCloudDatabaseUrl();
3170
+ if (!url) {
3171
+ throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
3172
+ }
3173
+ const { PgAdapterAsync } = await import("@hasna/cloud");
3174
+ return new PgAdapterAsync(url);
3175
+ }
3176
+ async function runCloudMigrations(cloud) {
3177
+ const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
3178
+ for (const sql of PG_MIGRATIONS2) {
3179
+ await cloud.run(sql);
3180
+ }
3181
+ }
3182
+ function isCloudIncrementalEnabled() {
3183
+ return process.env["ECONOMY_CLOUD_INCREMENTAL"] === "1" || process.env["ECONOMY_CLOUD_INCREMENTAL"] === "true";
3184
+ }
3185
+ async function cloudPush(opts) {
3186
+ const { syncPush, incrementalSyncPush, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
3187
+ const cloud = await getCloudPg();
3188
+ const local = new SqliteAdapter(getDbPath());
3189
+ await runCloudMigrations(cloud);
3190
+ const tables = opts?.tables ?? [...CLOUD_TABLES];
3191
+ let rows = 0;
3192
+ if (isCloudIncrementalEnabled()) {
3193
+ ensureSyncMetaTable(local);
3194
+ const results = incrementalSyncPush(local, cloud, tables, { conflictColumn: "updated_at" });
3195
+ rows = results.reduce((s, r) => s + r.synced_rows, 0);
3196
+ } else {
3197
+ const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
3198
+ rows = results.reduce((s, r) => s + r.rowsWritten, 0);
3199
+ }
3200
+ touchMachineRegistry(local, "push");
3201
+ local.close();
3202
+ await cloud.close();
3203
+ return { rows, machine: getMachineId() };
3204
+ }
3205
+ async function cloudPull(opts) {
3206
+ const { syncPull, incrementalSyncPull, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
3207
+ const cloud = await getCloudPg();
3208
+ const local = new SqliteAdapter(getDbPath());
3209
+ await runCloudMigrations(cloud);
3210
+ const tables = opts?.tables ?? [...CLOUD_TABLES];
3211
+ let rows = 0;
3212
+ if (isCloudIncrementalEnabled()) {
3213
+ ensureSyncMetaTable(local);
3214
+ const results = incrementalSyncPull(cloud, local, tables, { conflictColumn: "updated_at" });
3215
+ rows = results.reduce((s, r) => s + r.synced_rows, 0);
3216
+ } else {
3217
+ const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
3218
+ rows = results.reduce((s, r) => s + r.rowsWritten, 0);
3219
+ }
3220
+ touchMachineRegistry(local, "pull");
3221
+ local.close();
3222
+ await cloud.close();
3223
+ setLastCloudPull();
3224
+ return { rows, machine: getMachineId() };
3225
+ }
3226
+ function setLastCloudPull(at = new Date().toISOString()) {
3227
+ const db = openDatabase();
3228
+ db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
3229
+ }
3230
+ function getLastCloudPull() {
3231
+ const db = openDatabase();
3232
+ const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
3233
+ return row?.value ?? null;
3234
+ }
3235
+ function shouldPullFromCloud() {
3236
+ if (!getCloudDatabaseUrl())
3237
+ return false;
3238
+ const last = getLastCloudPull();
3239
+ if (!last)
3240
+ return true;
3241
+ const ageMs = Date.now() - new Date(last).getTime();
3242
+ return ageMs > getCloudPullIntervalMinutes() * 60000;
3243
+ }
3244
+ async function maybePullFromCloud() {
3245
+ if (!shouldPullFromCloud())
3246
+ return false;
3247
+ try {
3248
+ await cloudPull();
3249
+ return true;
3250
+ } catch {
3251
+ return false;
3252
+ }
3253
+ }
3254
+ async function maybePushAfterIngest() {
3255
+ if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
3256
+ return false;
3257
+ try {
3258
+ await cloudPush();
3259
+ return true;
3260
+ } catch {
3261
+ return false;
3262
+ }
3263
+ }
3264
+ function touchMachineRegistry(db, direction) {
3265
+ const now = new Date().toISOString();
3266
+ const machine = getMachineId();
3267
+ db.prepare(`
3268
+ INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
3269
+ VALUES (?, ?, ?, ?, ?, ?, ?)
3270
+ ON CONFLICT(machine_id) DO UPDATE SET
3271
+ hostname = excluded.hostname,
3272
+ last_seen_at = excluded.last_seen_at,
3273
+ last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
3274
+ last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
3275
+ economy_version = excluded.economy_version,
3276
+ updated_at = excluded.updated_at
3277
+ `).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
3278
+ }
3279
+
3280
+ // src/lib/sync-all.ts
3281
+ async function syncAll(db, opts = {}) {
3282
+ const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
3283
+ const all = !anySpecific;
3284
+ await maybePullFromCloud();
3285
+ const result = { deduped: 0, cloudPulled: false, cloudPushed: false };
3286
+ if (all || opts.claude) {
3287
+ result.claude = await ingestClaude(db, opts.verbose);
3288
+ result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
3289
+ }
3290
+ if (all || opts.takumi)
3291
+ result.takumi = await ingestTakumi(db, opts.verbose);
3292
+ if (all || opts.codex) {
3293
+ result.codex = await ingestCodex(db, opts.verbose);
3294
+ result.codexQuota = await ingestCodexQuota(db, opts.verbose);
3295
+ }
3296
+ if (all || opts.gemini)
3297
+ result.gemini = await ingestGemini(db, opts.verbose);
3298
+ if (all || opts.opencode)
3299
+ result.opencode = await ingestOpenCode(db, opts.verbose);
3300
+ if (all || opts.cursor)
3301
+ result.cursor = await ingestCursor(db, opts.verbose);
3302
+ if (all || opts.pi)
3303
+ result.pi = await ingestPi(db, opts.verbose);
3304
+ if (all || opts.hermes)
3305
+ result.hermes = await ingestHermes(db, opts.verbose);
3306
+ result.deduped = dedupeRequests(db);
3307
+ result.cloudPushed = await maybePushAfterIngest();
3308
+ return result;
3309
+ }
3310
+
3311
+ // src/lib/billing-diff.ts
3312
+ init_database();
3313
+ var PROVIDER_TO_AGENT = {
3314
+ anthropic: "claude",
3315
+ openai: "codex",
3316
+ gemini: "gemini",
3317
+ google: "gemini"
3318
+ };
3319
+ function queryBillingDiff(db, period, thresholdPct = 15) {
3320
+ const estimated = querySummary(db, period, undefined, true);
3321
+ const actual = queryBillingSummary(db, period);
3322
+ const delta = estimated.total_usd - actual.total_usd;
3323
+ const deltaPct = actual.total_usd > 0 ? delta / actual.total_usd * 100 : 0;
3324
+ const agentRows = db.prepare(`
3325
+ SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
3326
+ FROM requests
3327
+ WHERE ${periodWhere3(period, "timestamp")}
3328
+ GROUP BY agent
3329
+ `).all();
3330
+ const by_agent = agentRows.map((row) => {
3331
+ const provider = Object.entries(PROVIDER_TO_AGENT).find(([, a]) => a === row.agent)?.[0];
3332
+ const actualUsd = provider ? actual.by_provider[provider] ?? 0 : 0;
3333
+ const rowDelta = row.estimated_usd - actualUsd;
3334
+ const rowPct = actualUsd > 0 ? rowDelta / actualUsd * 100 : 0;
3335
+ return {
3336
+ agent: row.agent,
3337
+ estimated_usd: row.estimated_usd,
3338
+ actual_usd: actualUsd,
3339
+ delta_usd: rowDelta,
3340
+ delta_pct: rowPct
3341
+ };
3342
+ }).sort((a, b) => Math.abs(b.delta_usd) - Math.abs(a.delta_usd));
3343
+ return {
3344
+ period,
3345
+ estimated_usd: estimated.total_usd,
3346
+ actual_usd: actual.total_usd,
3347
+ delta_usd: delta,
3348
+ delta_pct: deltaPct,
3349
+ threshold_pct: thresholdPct,
3350
+ is_alert: Math.abs(deltaPct) > thresholdPct,
3351
+ by_agent,
3352
+ by_provider: actual.by_provider
3353
+ };
3354
+ }
3355
+ function periodWhere3(period, column) {
3356
+ switch (period) {
3357
+ case "today":
3358
+ return `DATE(${column}) = DATE('now')`;
3359
+ case "yesterday":
3360
+ return `DATE(${column}) = DATE('now', '-1 day')`;
3361
+ case "week":
3362
+ return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
3363
+ case "month":
3364
+ return `${column} >= DATE('now', 'start of month')`;
3365
+ case "year":
3366
+ return `${column} >= DATE('now', 'start of year')`;
3367
+ case "all":
3368
+ return "1=1";
3369
+ }
3370
+ }
3371
+
3372
+ // src/server/serve.ts
3373
+ init_database();
3374
+
3375
+ // src/lib/serve-auth.ts
3376
+ function getServeApiToken() {
3377
+ return process.env["ECONOMY_API_TOKEN"] ?? process.env["HASNA_ECONOMY_API_TOKEN"];
3378
+ }
3379
+ function getServeBindHost() {
3380
+ const explicit = process.env["ECONOMY_BIND"] ?? process.env["ECONOMY_HOST"];
3381
+ if (explicit)
3382
+ return explicit;
3383
+ return getServeApiToken() ? "127.0.0.1" : "0.0.0.0";
3384
+ }
3385
+ function isAuthorizedRequest(req, path) {
3386
+ const token = getServeApiToken();
3387
+ if (!token)
3388
+ return true;
3389
+ if (path === "/health")
3390
+ return true;
3391
+ const auth = req.headers.get("Authorization");
3392
+ if (auth === `Bearer ${token}`)
3393
+ return true;
3394
+ if (req.headers.get("X-Economy-Token") === token)
3395
+ return true;
3396
+ return false;
3397
+ }
3398
+
3399
+ // src/server/serve.ts
937
3400
  import { randomUUID } from "crypto";
3401
+ import { existsSync as existsSync11 } from "fs";
3402
+ import { resolve, sep } from "path";
938
3403
  var CORS = {
939
3404
  "Access-Control-Allow-Origin": "*",
940
3405
  "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
941
- "Access-Control-Allow-Headers": "Content-Type"
3406
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Economy-Token"
942
3407
  };
3408
+ var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
3409
+ var SYNC_SOURCES = ["all", ...AGENTS];
3410
+ var DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
943
3411
  function json(data, status = 200) {
944
3412
  return new Response(JSON.stringify(data), {
945
3413
  status,
@@ -966,6 +3434,56 @@ function normalizeBudgetPeriod(value) {
966
3434
  return "monthly";
967
3435
  }
968
3436
  }
3437
+ function finiteNumber(value) {
3438
+ const n = Number(value);
3439
+ return Number.isFinite(n) ? n : null;
3440
+ }
3441
+ async function jsonBody(req) {
3442
+ const body = await req.json().catch(() => null);
3443
+ return body && typeof body === "object" && !Array.isArray(body) ? body : null;
3444
+ }
3445
+ function optionalString(value) {
3446
+ return typeof value === "string" ? value : null;
3447
+ }
3448
+ function optionalAgent(value) {
3449
+ if (value == null || value === "")
3450
+ return null;
3451
+ return typeof value === "string" && AGENTS.includes(value) ? value : undefined;
3452
+ }
3453
+ function stringArray(value) {
3454
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
3455
+ }
3456
+ function dashboardPath(root, pathname) {
3457
+ let decoded;
3458
+ try {
3459
+ decoded = decodeURIComponent(pathname);
3460
+ } catch {
3461
+ return null;
3462
+ }
3463
+ const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
3464
+ const rootPath = resolve(root);
3465
+ const filePath = resolve(rootPath, relativePath);
3466
+ return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
3467
+ }
3468
+ function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
3469
+ return async function fetch2(req) {
3470
+ const url = new URL(req.url);
3471
+ if (url.pathname.startsWith("/api") || url.pathname === "/health") {
3472
+ return apiHandler(req);
3473
+ }
3474
+ if (existsSync11(dashboardDir)) {
3475
+ const filePath = dashboardPath(dashboardDir, url.pathname);
3476
+ if (filePath && existsSync11(filePath)) {
3477
+ return new Response(Bun.file(filePath));
3478
+ }
3479
+ const indexPath = dashboardPath(dashboardDir, "/");
3480
+ if (indexPath && existsSync11(indexPath)) {
3481
+ return new Response(Bun.file(indexPath));
3482
+ }
3483
+ }
3484
+ return apiHandler(req);
3485
+ };
3486
+ }
969
3487
  function applyFields(obj, fields) {
970
3488
  if (!fields || fields.length === 0)
971
3489
  return obj;
@@ -978,6 +3496,8 @@ function createHandler(db) {
978
3496
  const method = req.method;
979
3497
  if (method === "OPTIONS")
980
3498
  return new Response(null, { status: 204, headers: CORS });
3499
+ if (!isAuthorizedRequest(req, path))
3500
+ return err("Unauthorized", 401);
981
3501
  if (path === "/health")
982
3502
  return ok({ status: "ok", ts: new Date().toISOString() });
983
3503
  if (path === "/api/summary" && method === "GET") {
@@ -988,6 +3508,15 @@ function createHandler(db) {
988
3508
  if (path === "/api/machines" && method === "GET") {
989
3509
  return ok(listMachines(db), { current_machine: getMachineId() });
990
3510
  }
3511
+ if (path === "/api/fleet" && method === "GET") {
3512
+ const period = url.searchParams.get("period") ?? "month";
3513
+ return ok({
3514
+ summary: querySummary(db, period, undefined, true),
3515
+ machines: listMachines(db),
3516
+ registry: listMachineRegistry(db),
3517
+ current_machine: getMachineId()
3518
+ });
3519
+ }
991
3520
  if (path === "/api/daily" && method === "GET") {
992
3521
  const days = Number(url.searchParams.get("days") ?? 30);
993
3522
  return ok(queryDailyBreakdown(db, days));
@@ -1021,6 +3550,41 @@ function createHandler(db) {
1021
3550
  if (path === "/api/models" && method === "GET") {
1022
3551
  return ok(queryModelBreakdown(db));
1023
3552
  }
3553
+ if (path === "/api/billing" && method === "GET") {
3554
+ const period = url.searchParams.get("period") ?? "month";
3555
+ return ok(queryBillingSummary(db, period));
3556
+ }
3557
+ if (path === "/api/billing/diff" && method === "GET") {
3558
+ const period = url.searchParams.get("period") ?? "month";
3559
+ const threshold = Number(url.searchParams.get("threshold") ?? 15);
3560
+ return ok(queryBillingDiff(db, period, Number.isFinite(threshold) ? threshold : 15));
3561
+ }
3562
+ if (path === "/api/billing/sync" && method === "POST") {
3563
+ const body = await jsonBody(req) ?? {};
3564
+ const days = Number(body["days"] ?? 31);
3565
+ if (!Number.isFinite(days) || days <= 0 || days > 366)
3566
+ return err("days must be between 1 and 366");
3567
+ const providers = Array.isArray(body["providers"]) ? body["providers"] : ["anthropic", "openai", "gemini"];
3568
+ const allowedProviders = new Set(["anthropic", "openai", "gemini"]);
3569
+ if (providers.some((provider) => !allowedProviders.has(provider)))
3570
+ return err("invalid billing provider");
3571
+ const results = {};
3572
+ const { syncAnthropicBilling: syncAnthropicBilling2, syncOpenAIBilling: syncOpenAIBilling2, syncGeminiBilling: syncGeminiBilling2 } = await Promise.resolve().then(() => (init_billing(), exports_billing));
3573
+ async function capture(provider, fn) {
3574
+ try {
3575
+ results[provider] = await fn();
3576
+ } catch (e) {
3577
+ results[provider] = { error: e instanceof Error ? e.message : String(e) };
3578
+ }
3579
+ }
3580
+ if (providers.includes("anthropic"))
3581
+ await capture("anthropic", () => syncAnthropicBilling2(db, { days }));
3582
+ if (providers.includes("openai"))
3583
+ await capture("openai", () => syncOpenAIBilling2(db, { days }));
3584
+ if (providers.includes("gemini"))
3585
+ await capture("gemini", () => syncGeminiBilling2(db, { days }));
3586
+ return ok(results);
3587
+ }
1024
3588
  if (path === "/api/projects" && method === "GET") {
1025
3589
  return ok(queryProjectBreakdown(db));
1026
3590
  }
@@ -1032,38 +3596,54 @@ function createHandler(db) {
1032
3596
  return ok(getBudgetStatuses(db));
1033
3597
  }
1034
3598
  if (path === "/api/budgets" && method === "POST") {
1035
- const body = await req.json();
3599
+ const body = await jsonBody(req);
3600
+ if (!body)
3601
+ return err("invalid JSON body");
3602
+ const limitUsd = finiteNumber(body["limit_usd"]);
3603
+ const alertAtPercent = finiteNumber(body["alert_at_percent"] ?? 80);
3604
+ if (limitUsd == null || limitUsd <= 0)
3605
+ return err("limit_usd must be a positive number");
3606
+ if (alertAtPercent == null || alertAtPercent <= 0 || alertAtPercent > 100)
3607
+ return err("alert_at_percent must be between 1 and 100");
3608
+ const agent = optionalAgent(body["agent"]);
3609
+ if (agent === undefined)
3610
+ return err(AGENT_ERROR);
1036
3611
  const now = new Date().toISOString();
1037
- upsertBudget(db, {
3612
+ const budget = {
1038
3613
  id: randomUUID(),
1039
3614
  project_path: body["project_path"] ?? null,
1040
- agent: body["agent"] ?? null,
3615
+ agent,
1041
3616
  period: normalizeBudgetPeriod(body["period"]),
1042
- limit_usd: Number(body["limit_usd"]),
1043
- alert_at_percent: Number(body["alert_at_percent"] ?? 80),
3617
+ limit_usd: limitUsd,
3618
+ alert_at_percent: alertAtPercent,
1044
3619
  created_at: now,
1045
3620
  updated_at: now
1046
- });
1047
- return ok({ ok: true });
3621
+ };
3622
+ upsertBudget(db, budget);
3623
+ return ok(getBudgetStatuses(db).find((b) => b.id === budget.id) ?? budget);
1048
3624
  }
1049
3625
  const budgetMatch = path.match(/^\/api\/budgets\/(.+)$/);
1050
3626
  if (budgetMatch && method === "DELETE") {
1051
- deleteBudget(db, budgetMatch[1]);
3627
+ deleteBudget(db, decodeURIComponent(budgetMatch[1]));
1052
3628
  return ok({ ok: true });
1053
3629
  }
1054
3630
  if (path === "/api/project-registry" && method === "GET") {
1055
3631
  return ok(listProjects(db));
1056
3632
  }
1057
3633
  if (path === "/api/project-registry" && method === "POST") {
1058
- const body = await req.json();
1059
- const { basename: basename3 } = await import("path");
1060
- const projPath = body["path"];
3634
+ const body = await jsonBody(req);
3635
+ if (!body)
3636
+ return err("invalid JSON body");
3637
+ const { basename: basename4 } = await import("path");
3638
+ const projPath = optionalString(body["path"])?.trim();
3639
+ if (!projPath)
3640
+ return err("path is required");
1061
3641
  upsertProject(db, {
1062
3642
  id: randomUUID(),
1063
3643
  path: projPath,
1064
- name: body["name"] ?? basename3(projPath),
1065
- description: body["description"] ?? null,
1066
- tags: body["tags"] ?? [],
3644
+ name: optionalString(body["name"]) ?? basename4(projPath),
3645
+ description: optionalString(body["description"]),
3646
+ tags: stringArray(body["tags"]),
1067
3647
  created_at: new Date().toISOString()
1068
3648
  });
1069
3649
  return ok({ ok: true });
@@ -1077,16 +3657,33 @@ function createHandler(db) {
1077
3657
  return ok(listModelPricing(db));
1078
3658
  }
1079
3659
  if (path === "/api/pricing" && method === "POST") {
1080
- const body = await req.json();
1081
- upsertModelPricing(db, {
1082
- model: body["model"],
1083
- input_per_1m: Number(body["input_per_1m"]),
1084
- output_per_1m: Number(body["output_per_1m"]),
1085
- cache_read_per_1m: Number(body["cache_read_per_1m"] ?? 0),
1086
- cache_write_per_1m: Number(body["cache_write_per_1m"] ?? 0),
3660
+ const body = await jsonBody(req);
3661
+ if (!body)
3662
+ return err("invalid JSON body");
3663
+ const model = String(body["model"] ?? "").trim();
3664
+ if (!model)
3665
+ return err("model is required");
3666
+ const input = finiteNumber(body["input_per_1m"]);
3667
+ const output = finiteNumber(body["output_per_1m"]);
3668
+ const cacheRead = finiteNumber(body["cache_read_per_1m"] ?? 0);
3669
+ const cacheWrite = finiteNumber(body["cache_write_per_1m"] ?? 0);
3670
+ const cacheWrite1h = finiteNumber(body["cache_write_1h_per_1m"] ?? 0);
3671
+ const cacheStorage = finiteNumber(body["cache_storage_per_1m_hour"] ?? 0);
3672
+ if ([input, output, cacheRead, cacheWrite, cacheWrite1h, cacheStorage].some((v) => v == null || v < 0)) {
3673
+ return err("pricing values must be non-negative numbers");
3674
+ }
3675
+ const pricing = {
3676
+ model,
3677
+ input_per_1m: input,
3678
+ output_per_1m: output,
3679
+ cache_read_per_1m: cacheRead,
3680
+ cache_write_per_1m: cacheWrite,
3681
+ cache_write_1h_per_1m: cacheWrite1h,
3682
+ cache_storage_per_1m_hour: cacheStorage,
1087
3683
  updated_at: new Date().toISOString()
1088
- });
1089
- return ok({ ok: true });
3684
+ };
3685
+ upsertModelPricing(db, pricing);
3686
+ return ok(pricing);
1090
3687
  }
1091
3688
  const pricingMatch = path.match(/^\/api\/pricing\/(.+)$/);
1092
3689
  if (pricingMatch && method === "DELETE") {
@@ -1094,22 +3691,43 @@ function createHandler(db) {
1094
3691
  return ok({ ok: true });
1095
3692
  }
1096
3693
  if (path === "/api/sync" && method === "POST") {
1097
- const body = await req.json().catch(() => ({}));
3694
+ const body = await jsonBody(req) ?? {};
1098
3695
  const sources = body["sources"] ?? "all";
3696
+ if (!SYNC_SOURCES.includes(sources))
3697
+ return err("invalid sync source");
1099
3698
  const results = {};
1100
- if (sources === "all" || sources === "claude")
1101
- results["claude"] = await ingestClaude(db);
1102
- if (sources === "all" || sources === "takumi")
1103
- results["takumi"] = await ingestTakumi(db);
1104
- if (sources === "all" || sources === "codex")
1105
- results["codex"] = await ingestCodex(db);
1106
- if (sources === "all" || sources === "gemini")
1107
- results["gemini"] = await ingestGemini(db);
3699
+ if (sources === "all") {
3700
+ try {
3701
+ const { syncOpenProjectsRegistry: syncOpenProjectsRegistry2 } = await Promise.resolve().then(() => (init_open_projects(), exports_open_projects));
3702
+ results["projects"] = await syncOpenProjectsRegistry2(db);
3703
+ } catch {}
3704
+ }
3705
+ const selected = sources === "all" ? {} : { [sources]: true };
3706
+ const syncResult = await syncAll(db, selected);
3707
+ Object.assign(results, syncResult);
3708
+ try {
3709
+ const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
3710
+ await checkAndFireWebhooks2(db);
3711
+ } catch {}
1108
3712
  return ok(results);
1109
3713
  }
3714
+ if (path === "/api/usage" && method === "GET") {
3715
+ const period = url.searchParams.get("period") ?? "month";
3716
+ const agent = url.searchParams.get("agent") ?? undefined;
3717
+ const since = period === "month" ? new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().substring(0, 10) : undefined;
3718
+ return ok({
3719
+ snapshots: queryUsageSnapshots(db, { agent: agent && isAgent(agent) ? agent : undefined, since }),
3720
+ summary: querySummary(db, period, undefined, true)
3721
+ });
3722
+ }
3723
+ if (path === "/api/savings" && method === "GET") {
3724
+ const period = url.searchParams.get("period") ?? "month";
3725
+ const agent = url.searchParams.get("agent") ?? undefined;
3726
+ return ok(querySavingsSummary(db, period, agent && isAgent(agent) ? agent : undefined));
3727
+ }
1110
3728
  const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
1111
3729
  if (sessionRequestsMatch && method === "GET") {
1112
- const sessionId = sessionRequestsMatch[1];
3730
+ const sessionId = decodeURIComponent(sessionRequestsMatch[1]);
1113
3731
  const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(sessionId, `${sessionId}%`);
1114
3732
  if (!session)
1115
3733
  return err("Session not found", 404);
@@ -1120,75 +3738,55 @@ function createHandler(db) {
1120
3738
  return ok(getGoalStatuses(db));
1121
3739
  }
1122
3740
  if (path === "/api/goals" && method === "POST") {
1123
- const body = await req.json();
3741
+ const body = await jsonBody(req);
3742
+ if (!body)
3743
+ return err("invalid JSON body");
3744
+ const period = body["period"] ?? "month";
3745
+ if (!["day", "week", "month", "year"].includes(String(period)))
3746
+ return err("period must be day, week, month, or year");
3747
+ const limitUsd = finiteNumber(body["limit_usd"]);
3748
+ if (limitUsd == null || limitUsd <= 0)
3749
+ return err("limit_usd must be a positive number");
3750
+ const agent = optionalAgent(body["agent"]);
3751
+ if (agent === undefined)
3752
+ return err(AGENT_ERROR);
1124
3753
  const now = new Date().toISOString();
1125
- upsertGoal(db, {
3754
+ const goal = {
1126
3755
  id: randomUUID(),
1127
- period: body["period"] ?? "month",
1128
- project_path: body["project_path"] ?? null,
1129
- agent: body["agent"] ?? null,
1130
- limit_usd: Number(body["limit_usd"]),
3756
+ period,
3757
+ project_path: optionalString(body["project_path"]),
3758
+ agent,
3759
+ limit_usd: limitUsd,
1131
3760
  created_at: now,
1132
3761
  updated_at: now
1133
- });
1134
- return ok({ ok: true });
3762
+ };
3763
+ upsertGoal(db, goal);
3764
+ return ok(getGoalStatuses(db).find((g) => g.id === goal.id) ?? goal);
1135
3765
  }
1136
3766
  const goalMatch = path.match(/^\/api\/goals\/(.+)$/);
1137
3767
  if (goalMatch && method === "DELETE") {
1138
- deleteGoal(db, goalMatch[1]);
3768
+ deleteGoal(db, decodeURIComponent(goalMatch[1]));
1139
3769
  return ok({ ok: true });
1140
3770
  }
1141
3771
  return err("Not found", 404);
1142
3772
  };
1143
3773
  }
1144
- function startServer(port = 3456) {
1145
- const db = openDatabase();
3774
+ function startServer(port = 3456, options = {}) {
3775
+ const db = options.db ?? openDatabase();
1146
3776
  ensurePricingSeeded(db);
1147
3777
  const apiHandler = createHandler(db);
1148
- const dashboardDir = new URL("../../dashboard/dist", import.meta.url).pathname;
1149
- Bun.serve({
3778
+ const hostname2 = options.hostname ?? getServeBindHost();
3779
+ const server = Bun.serve({
1150
3780
  port,
1151
- async fetch(req) {
1152
- const url = new URL(req.url);
1153
- if (url.pathname.startsWith("/api") || url.pathname === "/health") {
1154
- return apiHandler(req);
1155
- }
1156
- try {
1157
- const { existsSync: existsSync5 } = await import("fs");
1158
- if (existsSync5(dashboardDir)) {
1159
- let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
1160
- const fullPath = dashboardDir + filePath;
1161
- if (existsSync5(fullPath)) {
1162
- return new Response(Bun.file(fullPath));
1163
- }
1164
- const indexPath = dashboardDir + "/index.html";
1165
- if (existsSync5(indexPath)) {
1166
- return new Response(Bun.file(indexPath));
1167
- }
1168
- }
1169
- } catch {}
1170
- return apiHandler(req);
1171
- }
3781
+ hostname: hostname2,
3782
+ fetch: createServerFetch(apiHandler, options.dashboardDir)
1172
3783
  });
1173
- console.log(`economy-serve listening on http://localhost:${port}`);
3784
+ const address = `http://${hostname2 === "0.0.0.0" ? "localhost" : hostname2}:${server.port}`;
3785
+ const log = options.log ?? console.log;
3786
+ log(`economy-serve listening on ${address}`);
3787
+ return server;
1174
3788
  }
1175
3789
 
1176
- // src/lib/package-metadata.ts
1177
- import { readFileSync as readFileSync4 } from "fs";
1178
- var cachedMetadata = null;
1179
- function getPackageMetadata() {
1180
- if (cachedMetadata)
1181
- return cachedMetadata;
1182
- const raw = readFileSync4(new URL("../../package.json", import.meta.url), "utf8");
1183
- const parsed = JSON.parse(raw);
1184
- cachedMetadata = {
1185
- name: parsed.name ?? "@hasna/economy",
1186
- version: parsed.version ?? "0.0.0"
1187
- };
1188
- return cachedMetadata;
1189
- }
1190
- var packageMetadata = getPackageMetadata();
1191
-
1192
3790
  // src/server/index.ts
1193
3791
  function printHelp() {
1194
3792
  console.log(`Usage: economy-serve [options]
@@ -1203,17 +3801,19 @@ Options:
1203
3801
  function resolvePort(argv) {
1204
3802
  for (let i = 0;i < argv.length; i++) {
1205
3803
  const arg = argv[i];
1206
- if ((arg === "--port" || arg === "-p") && argv[i + 1]) {
1207
- const value2 = Number(argv[i + 1]);
1208
- if (!Number.isFinite(value2) || value2 <= 0) {
1209
- throw new Error(`Invalid port: ${argv[i + 1]}`);
1210
- }
1211
- return value2;
3804
+ if (arg === "--port" || arg === "-p") {
3805
+ const raw = argv[i + 1];
3806
+ if (!raw)
3807
+ throw new Error(`Invalid port: ${raw ?? ""}`);
3808
+ return parsePort(raw, "port");
1212
3809
  }
1213
3810
  }
1214
- const value = Number(process.env["ECONOMY_PORT"] ?? 3456);
1215
- if (!Number.isFinite(value) || value <= 0) {
1216
- throw new Error(`Invalid ECONOMY_PORT: ${process.env["ECONOMY_PORT"]}`);
3811
+ return parsePort(process.env["ECONOMY_PORT"] ?? "3456", "ECONOMY_PORT");
3812
+ }
3813
+ function parsePort(raw, label) {
3814
+ const value = Number(raw);
3815
+ if (!Number.isInteger(value) || value < 1 || value > 65535) {
3816
+ throw new Error(`Invalid ${label}: ${raw}`);
1217
3817
  }
1218
3818
  return value;
1219
3819
  }