@hasna/economy 0.2.20 → 0.2.21

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 (88) hide show
  1. package/dist/cli/commands/completion.d.ts +2 -0
  2. package/dist/cli/commands/completion.d.ts.map +1 -0
  3. package/dist/cli/commands/extras.d.ts +4 -0
  4. package/dist/cli/commands/extras.d.ts.map +1 -0
  5. package/dist/cli/commands/menubar.d.ts.map +1 -1
  6. package/dist/cli/commands/notification.d.ts +8 -0
  7. package/dist/cli/commands/notification.d.ts.map +1 -0
  8. package/dist/cli/commands/todos.d.ts +26 -0
  9. package/dist/cli/commands/todos.d.ts.map +1 -0
  10. package/dist/cli/commands/tui.d.ts +10 -0
  11. package/dist/cli/commands/tui.d.ts.map +1 -0
  12. package/dist/cli/commands/watch.d.ts +1 -0
  13. package/dist/cli/commands/watch.d.ts.map +1 -1
  14. package/dist/cli/index.js +5134 -641
  15. package/dist/db/database.d.ts +41 -1
  16. package/dist/db/database.d.ts.map +1 -1
  17. package/dist/db/pg-migrations.d.ts.map +1 -1
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1202 -135
  21. package/dist/ingest/billing.d.ts +27 -0
  22. package/dist/ingest/billing.d.ts.map +1 -0
  23. package/dist/ingest/claude-quota.d.ts +5 -0
  24. package/dist/ingest/claude-quota.d.ts.map +1 -0
  25. package/dist/ingest/claude.d.ts +13 -2
  26. package/dist/ingest/claude.d.ts.map +1 -1
  27. package/dist/ingest/codex-quota.d.ts +5 -0
  28. package/dist/ingest/codex-quota.d.ts.map +1 -0
  29. package/dist/ingest/codex.d.ts +2 -1
  30. package/dist/ingest/codex.d.ts.map +1 -1
  31. package/dist/ingest/cursor.d.ts +6 -0
  32. package/dist/ingest/cursor.d.ts.map +1 -0
  33. package/dist/ingest/gemini.d.ts +2 -1
  34. package/dist/ingest/gemini.d.ts.map +1 -1
  35. package/dist/ingest/hermes.d.ts +6 -0
  36. package/dist/ingest/hermes.d.ts.map +1 -0
  37. package/dist/ingest/opencode.d.ts +7 -0
  38. package/dist/ingest/opencode.d.ts.map +1 -0
  39. package/dist/ingest/otel.d.ts +20 -0
  40. package/dist/ingest/otel.d.ts.map +1 -0
  41. package/dist/ingest/pi.d.ts +7 -0
  42. package/dist/ingest/pi.d.ts.map +1 -0
  43. package/dist/ingest/plugin.d.ts +17 -0
  44. package/dist/ingest/plugin.d.ts.map +1 -0
  45. package/dist/lib/agents.d.ts +11 -0
  46. package/dist/lib/agents.d.ts.map +1 -0
  47. package/dist/lib/billing-diff.d.ts +22 -0
  48. package/dist/lib/billing-diff.d.ts.map +1 -0
  49. package/dist/lib/cloud-sync.d.ts +35 -0
  50. package/dist/lib/cloud-sync.d.ts.map +1 -0
  51. package/dist/lib/config.d.ts.map +1 -1
  52. package/dist/lib/gatherer.d.ts.map +1 -1
  53. package/dist/lib/model-config.d.ts.map +1 -1
  54. package/dist/lib/open-projects.d.ts +19 -0
  55. package/dist/lib/open-projects.d.ts.map +1 -0
  56. package/dist/lib/package-metadata.d.ts +8 -0
  57. package/dist/lib/package-metadata.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 +3 -3
  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 +1 -1
  73. package/dist/lib/webhooks.d.ts.map +1 -1
  74. package/dist/mcp/http.d.ts +13 -0
  75. package/dist/mcp/http.d.ts.map +1 -0
  76. package/dist/mcp/index.js +2752 -490
  77. package/dist/mcp/server.d.ts +4 -0
  78. package/dist/mcp/server.d.ts.map +1 -0
  79. package/dist/otel/index.d.ts +3 -0
  80. package/dist/otel/index.d.ts.map +1 -0
  81. package/dist/otel/index.js +1372 -0
  82. package/dist/server/index.d.ts +1 -0
  83. package/dist/server/index.js +3095 -201
  84. package/dist/server/serve.d.ts +10 -2
  85. package/dist/server/serve.d.ts.map +1 -1
  86. package/dist/types/index.d.ts +59 -6
  87. package/dist/types/index.d.ts.map +1 -1
  88. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env bun
1
2
  // @bun
2
3
  var __defProp = Object.defineProperty;
3
4
  var __returnValue = (v) => v;
@@ -28,88 +29,503 @@ __export(exports_pricing, {
28
29
  DEFAULT_PRICING: () => DEFAULT_PRICING
29
30
  });
30
31
  function normalizeModelName(raw) {
31
- 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;
32
68
  }
33
69
  function ensurePricingSeeded(db) {
34
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);
35
169
  }
36
170
  function getPricingFromDb(db, model) {
37
- const normalized = normalizeModelName(model);
38
- const row = getModelPricing(db, normalized);
39
- if (row) {
40
- return {
41
- inputPer1M: row.input_per_1m,
42
- outputPer1M: row.output_per_1m,
43
- cacheReadPer1M: row.cache_read_per_1m,
44
- cacheWritePer1M: row.cache_write_per_1m
45
- };
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);
46
177
  }
47
178
  const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
48
- for (const r of allRows) {
49
- if (normalized.startsWith(r.model)) {
50
- return { inputPer1M: r.input_per_1m, outputPer1M: r.output_per_1m, cacheReadPer1M: r.cache_read_per_1m, cacheWritePer1M: r.cache_write_per_1m };
51
- }
52
- }
53
- 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
+ };
54
195
  }
55
196
  function getPricing(model) {
56
- const normalized = normalizeModelName(model);
57
- if (DEFAULT_PRICING[normalized])
58
- return DEFAULT_PRICING[normalized] ?? null;
59
- for (const key of Object.keys(DEFAULT_PRICING)) {
60
- if (normalized.startsWith(key))
61
- return DEFAULT_PRICING[key] ?? null;
62
- }
63
- 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");
64
203
  }
65
- function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
204
+ function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
66
205
  const pricing = getPricing(model);
67
206
  if (!pricing)
68
207
  return 0;
69
- 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);
70
209
  }
71
- 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) {
72
211
  const pricing = getPricingFromDb(db, model) ?? getPricing(model);
73
212
  if (!pricing)
74
213
  return 0;
75
- 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);
76
215
  }
77
- var DEFAULT_PRICING;
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;
234
+ }
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;
78
236
  var init_pricing = __esm(() => {
79
237
  init_database();
80
238
  DEFAULT_PRICING = {
81
- "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
82
- "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
83
- "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
84
- "claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
85
- "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
86
- "claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
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 },
270
+ "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
271
+ "gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
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 = {
87
325
  "claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
88
- "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
89
- "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
90
- "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
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 },
91
329
  "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
92
- "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
93
- "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
94
- "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
95
330
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
96
331
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
97
332
  "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
98
- "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
99
- "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
100
- o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
333
+ "gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
334
+ "gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
101
335
  "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
102
- o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
103
- "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
104
- "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 },
338
+ "qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
339
+ "minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
340
+ "minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
341
+ "minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
342
+ "glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
343
+ "glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, 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
+ }
105
512
  };
106
513
  });
107
514
 
108
515
  // src/db/database.ts
109
516
  import { SqliteAdapter as Database } from "@hasna/cloud";
110
517
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
518
+ import { hostname } from "os";
111
519
  import { homedir } from "os";
112
520
  import { join } from "path";
521
+ function getMachineId() {
522
+ if (process.env["ECONOMY_MACHINE_ID"])
523
+ return process.env["ECONOMY_MACHINE_ID"];
524
+ const h = hostname().toLowerCase();
525
+ if (h.startsWith("spark") || h.startsWith("apple"))
526
+ return h.split(".")[0];
527
+ return h.split(".")[0];
528
+ }
113
529
  function getDataDir() {
114
530
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
115
531
  const newDir = join(home, ".hasna", "economy");
@@ -142,6 +558,7 @@ function openDatabase(dbPath, skipSeed = false) {
142
558
  }
143
559
  const db = new Database(path);
144
560
  db.exec("PRAGMA journal_mode = WAL");
561
+ db.exec("PRAGMA busy_timeout = 5000");
145
562
  db.exec("PRAGMA foreign_keys = ON");
146
563
  initSchema(db);
147
564
  if (!skipSeed) {
@@ -160,10 +577,13 @@ function initSchema(db) {
160
577
  output_tokens INTEGER DEFAULT 0,
161
578
  cache_read_tokens INTEGER DEFAULT 0,
162
579
  cache_create_tokens INTEGER DEFAULT 0,
580
+ cache_create_5m_tokens INTEGER DEFAULT 0,
581
+ cache_create_1h_tokens INTEGER DEFAULT 0,
163
582
  cost_usd REAL NOT NULL DEFAULT 0,
164
583
  duration_ms INTEGER DEFAULT 0,
165
584
  timestamp TEXT NOT NULL,
166
- source_request_id TEXT
585
+ source_request_id TEXT,
586
+ machine_id TEXT DEFAULT ''
167
587
  );
168
588
 
169
589
  CREATE TABLE IF NOT EXISTS sessions (
@@ -175,7 +595,8 @@ function initSchema(db) {
175
595
  ended_at TEXT,
176
596
  total_cost_usd REAL DEFAULT 0,
177
597
  total_tokens INTEGER DEFAULT 0,
178
- request_count INTEGER DEFAULT 0
598
+ request_count INTEGER DEFAULT 0,
599
+ machine_id TEXT DEFAULT ''
179
600
  );
180
601
 
181
602
  CREATE TABLE IF NOT EXISTS projects (
@@ -228,6 +649,8 @@ function initSchema(db) {
228
649
  output_per_1m REAL NOT NULL DEFAULT 0,
229
650
  cache_read_per_1m REAL NOT NULL DEFAULT 0,
230
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,
231
654
  updated_at TEXT NOT NULL
232
655
  );
233
656
 
@@ -240,6 +663,115 @@ function initSchema(db) {
240
663
  machine_id TEXT,
241
664
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
242
665
  );
666
+
667
+ CREATE TABLE IF NOT EXISTS billing_daily (
668
+ date TEXT NOT NULL,
669
+ provider TEXT NOT NULL,
670
+ description TEXT DEFAULT '',
671
+ cost_usd REAL NOT NULL DEFAULT 0,
672
+ updated_at TEXT NOT NULL,
673
+ PRIMARY KEY (date, provider, description)
674
+ );
675
+
676
+ CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
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);
728
+ `);
729
+ const cols = db.prepare(`PRAGMA table_info(requests)`).all();
730
+ if (!cols.some((c) => c.name === "machine_id")) {
731
+ db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
732
+ db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
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
+ }
772
+ db.exec(`
773
+ CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
774
+ CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
243
775
  `);
244
776
  }
245
777
  function periodWhere(period) {
@@ -249,11 +781,11 @@ function periodWhere(period) {
249
781
  case "yesterday":
250
782
  return `DATE(timestamp) = DATE('now', '-1 day')`;
251
783
  case "week":
252
- return `timestamp >= DATE('now', '-7 days')`;
784
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
253
785
  case "month":
254
- return `timestamp >= DATE('now', '-30 days')`;
786
+ return `timestamp >= DATE('now', 'start of month')`;
255
787
  case "year":
256
- return `timestamp >= DATE('now', '-365 days')`;
788
+ return `timestamp >= DATE('now', 'start of year')`;
257
789
  case "all":
258
790
  return "1=1";
259
791
  }
@@ -265,31 +797,34 @@ function sessionPeriodWhere(period) {
265
797
  case "yesterday":
266
798
  return `DATE(started_at) = DATE('now', '-1 day')`;
267
799
  case "week":
268
- return `started_at >= DATE('now', '-7 days')`;
800
+ return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
269
801
  case "month":
270
- return `started_at >= DATE('now', '-30 days')`;
802
+ return `started_at >= DATE('now', 'start of month')`;
271
803
  case "year":
272
- return `started_at >= DATE('now', '-365 days')`;
804
+ return `started_at >= DATE('now', 'start of year')`;
273
805
  case "all":
274
806
  return "1=1";
275
807
  }
276
808
  }
277
809
  function upsertRequest(db, req) {
810
+ const now = req.updated_at ?? new Date().toISOString();
278
811
  db.prepare(`
279
812
  INSERT OR REPLACE INTO requests
280
813
  (id, agent, session_id, model, input_tokens, output_tokens,
281
- cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
282
- timestamp, source_request_id)
283
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
284
- `).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);
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 ?? "");
285
819
  }
286
820
  function upsertSession(db, session) {
821
+ const now = session.updated_at ?? new Date().toISOString();
287
822
  db.prepare(`
288
823
  INSERT OR REPLACE INTO sessions
289
824
  (id, agent, project_path, project_name, started_at, ended_at,
290
- total_cost_usd, total_tokens, request_count)
291
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
292
- `).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);
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 ?? "");
293
828
  }
294
829
  function rollupSession(db, sessionId) {
295
830
  db.prepare(`
@@ -319,6 +854,10 @@ function querySessions(db, filter = {}) {
319
854
  conditions.push("started_at >= ?");
320
855
  params.push(filter.since);
321
856
  }
857
+ if (filter.machine) {
858
+ conditions.push("machine_id = ?");
859
+ params.push(filter.machine);
860
+ }
322
861
  if (filter.search) {
323
862
  const q = `%${filter.search}%`;
324
863
  conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
@@ -337,24 +876,25 @@ function queryTopSessions(db, n = 10, agent) {
337
876
  }
338
877
  return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
339
878
  }
340
- function querySummary(db, period) {
879
+ function querySummary(db, period, machine, allMachines = false) {
341
880
  const rWhere = periodWhere(period);
342
881
  const sWhere = sessionPeriodWhere(period);
882
+ const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
343
883
  const r = db.prepare(`
344
884
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
345
885
  COUNT(*) as requests,
346
886
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
347
- FROM requests WHERE ${rWhere}
887
+ FROM requests WHERE ${rWhere}${machineClause}
348
888
  `).get();
349
889
  const codexTotals = db.prepare(`
350
890
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
351
891
  COALESCE(SUM(total_tokens), 0) as tokens,
352
892
  COUNT(*) as sessions
353
893
  FROM sessions
354
- WHERE ${sWhere}
894
+ WHERE ${sWhere}${machineClause}
355
895
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
356
896
  `).get();
357
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
897
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
358
898
  return {
359
899
  total_usd: r.total_usd + codexTotals.cost_usd,
360
900
  requests: r.requests,
@@ -374,23 +914,66 @@ function queryModelBreakdown(db) {
374
914
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
375
915
  `).all();
376
916
  }
917
+ function labelForPath(projectPath, projectName) {
918
+ if (projectName && projectName.trim() !== "")
919
+ return projectName;
920
+ if (!projectPath)
921
+ return "";
922
+ const segments = projectPath.split("/").filter(Boolean);
923
+ const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
924
+ for (const seg of segments) {
925
+ if (projectPrefix.test(seg))
926
+ return seg;
927
+ }
928
+ const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
929
+ for (let i = segments.length - 1;i >= 0; i--) {
930
+ if (!generic.has(segments[i].toLowerCase()))
931
+ return segments[i];
932
+ }
933
+ return segments[segments.length - 1] ?? projectPath;
934
+ }
377
935
  function queryProjectBreakdown(db) {
378
- return db.prepare(`
379
- SELECT
380
- s.project_path,
381
- COALESCE(p.name, s.project_name) as project_name,
382
- COUNT(DISTINCT s.id) as sessions,
383
- COUNT(r.id) as requests,
384
- COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
385
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
386
- MAX(s.started_at) as last_active
387
- FROM sessions s
388
- LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
389
- LEFT JOIN requests r ON r.session_id = s.id
390
- WHERE s.project_path != '' OR s.project_name != ''
391
- GROUP BY s.project_path
392
- ORDER BY cost_usd DESC
936
+ const sessions = db.prepare(`
937
+ SELECT id, project_path, project_name, total_cost_usd, started_at
938
+ FROM sessions
939
+ WHERE project_path != '' OR project_name != ''
393
940
  `).all();
941
+ const groups = new Map;
942
+ for (const s of sessions) {
943
+ const label = labelForPath(s.project_path, s.project_name);
944
+ if (!label)
945
+ continue;
946
+ const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
947
+ g.sessionIds.push(s.id);
948
+ g.totalCost += s.total_cost_usd || 0;
949
+ if (!g.lastActive || s.started_at > g.lastActive)
950
+ g.lastActive = s.started_at;
951
+ if (!g.samplePath)
952
+ g.samplePath = s.project_path;
953
+ groups.set(label, g);
954
+ }
955
+ const result = [];
956
+ for (const [label, g] of groups.entries()) {
957
+ const placeholders = g.sessionIds.map(() => "?").join(",");
958
+ const reqStats = placeholders.length ? db.prepare(`
959
+ SELECT
960
+ COUNT(*) as requests,
961
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
962
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
963
+ FROM requests WHERE session_id IN (${placeholders})
964
+ `).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
965
+ result.push({
966
+ project_path: g.samplePath,
967
+ project_name: label,
968
+ sessions: g.sessionIds.length,
969
+ requests: reqStats.requests,
970
+ total_tokens: reqStats.total_tokens,
971
+ cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
972
+ last_active: g.lastActive
973
+ });
974
+ }
975
+ result.sort((a, b) => b.cost_usd - a.cost_usd);
976
+ return result;
394
977
  }
395
978
  function queryDailyBreakdown(db, days = 30) {
396
979
  return db.prepare(`
@@ -499,12 +1082,46 @@ function getIngestState(db, source, key) {
499
1082
  function setIngestState(db, source, key, value) {
500
1083
  db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
501
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
+ }
1105
+ function listMachines(db) {
1106
+ return db.prepare(`
1107
+ SELECT
1108
+ s.machine_id,
1109
+ COUNT(DISTINCT s.id) as sessions,
1110
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
1111
+ COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
1112
+ MAX(s.started_at) as last_active
1113
+ FROM sessions s
1114
+ WHERE s.machine_id != ''
1115
+ GROUP BY s.machine_id
1116
+ ORDER BY total_cost_usd DESC
1117
+ `).all();
1118
+ }
502
1119
  function upsertModelPricing(db, p) {
503
1120
  db.prepare(`
504
1121
  INSERT OR REPLACE INTO model_pricing
505
- (model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
506
- VALUES (?, ?, ?, ?, ?, ?)
507
- `).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);
508
1125
  }
509
1126
  function getModelPricing(db, model) {
510
1127
  return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
@@ -516,25 +1133,655 @@ function deleteModelPricing(db, model) {
516
1133
  db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
517
1134
  }
518
1135
  function seedModelPricing(db, defaults) {
519
- const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
520
- if (existing.count > 0)
521
- return;
1136
+ const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
522
1137
  const now = new Date().toISOString();
523
1138
  for (const [model, p] of Object.entries(defaults)) {
1139
+ if (existing.has(model))
1140
+ continue;
524
1141
  upsertModelPricing(db, {
525
1142
  model,
526
1143
  input_per_1m: p.inputPer1M,
527
1144
  output_per_1m: p.outputPer1M,
528
1145
  cache_read_per_1m: p.cacheReadPer1M,
529
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,
530
1149
  updated_at: now
531
1150
  });
532
1151
  }
533
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
+ }
534
1206
  var init_database = () => {};
535
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
+
536
1767
  // src/server/serve.ts
537
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
+ }
538
1785
 
539
1786
  // src/ingest/claude.ts
540
1787
  init_database();
@@ -542,15 +1789,119 @@ init_pricing();
542
1789
  import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
543
1790
  import { homedir as homedir2 } from "os";
544
1791
  import { join as join2, basename } from "path";
545
- function autoDetectProject(cwd, projects) {
546
- return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
547
- }
548
- var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
549
- function dirNameToPath(dirName) {
550
- 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
+ }
551
1809
  }
552
- function collectJsonlFiles(projectDir) {
553
- const files = [];
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 = [];
554
1905
  function walk(dir) {
555
1906
  try {
556
1907
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
@@ -564,30 +1915,37 @@ function collectJsonlFiles(projectDir) {
564
1915
  walk(projectDir);
565
1916
  return files;
566
1917
  }
567
- async function ingestClaude(db, verbose = false, _telemetryDir) {
568
- if (!existsSync2(PROJECTS_DIR)) {
1918
+ async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
1919
+ return ingestJsonlProjects(db, projectsDir, "claude", verbose);
1920
+ }
1921
+ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
1922
+ return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
1923
+ }
1924
+ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
1925
+ if (!existsSync2(projectsDir)) {
569
1926
  if (verbose)
570
- console.log("Claude projects dir not found:", PROJECTS_DIR);
1927
+ console.log(`${agentName} projects dir not found:`, projectsDir);
571
1928
  return { files: 0, requests: 0, sessions: 0 };
572
1929
  }
1930
+ const machineId = getMachineId();
573
1931
  let totalFiles = 0;
574
1932
  let totalRequests = 0;
575
1933
  const touchedSessions = new Set;
576
1934
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
577
- const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
1935
+ const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
578
1936
  for (const projectDirEntry of projectDirs) {
579
- const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
1937
+ const projectDirPath = join2(projectsDir, projectDirEntry.name);
580
1938
  const projectPath = dirNameToPath(projectDirEntry.name);
581
1939
  const jsonlFiles = collectJsonlFiles(projectDirPath);
582
1940
  for (const filePath of jsonlFiles) {
583
- const stateKey = filePath.replace(PROJECTS_DIR, "");
1941
+ const stateKey = filePath.replace(projectsDir, "");
584
1942
  let fileMtime = "0";
585
1943
  try {
586
1944
  fileMtime = statSync2(filePath).mtimeMs.toString();
587
1945
  } catch {
588
1946
  continue;
589
1947
  }
590
- const processed = getIngestState(db, "claude", stateKey);
1948
+ const processed = getIngestState(db, agentName, stateKey);
591
1949
  if (processed === fileMtime)
592
1950
  continue;
593
1951
  let lines;
@@ -622,26 +1980,38 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
622
1980
  continue;
623
1981
  const inputTokens = usage.input_tokens ?? 0;
624
1982
  const outputTokens = usage.output_tokens ?? 0;
625
- 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;
626
1986
  const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
627
1987
  const timestamp = entry.timestamp ?? new Date().toISOString();
628
- if (inputTokens + outputTokens + cacheWriteTokens === 0)
1988
+ if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
629
1989
  continue;
630
- const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
631
- const reqId = `claude-${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}`;
632
1998
  upsertRequest(db, {
633
1999
  id: reqId,
634
- agent: "claude",
2000
+ agent: agentName,
635
2001
  session_id: sessionId,
636
2002
  model,
637
2003
  input_tokens: inputTokens,
638
2004
  output_tokens: outputTokens,
639
2005
  cache_read_tokens: cacheReadTokens,
640
2006
  cache_create_tokens: cacheWriteTokens,
2007
+ cache_create_5m_tokens: cacheWrite5mTokens,
2008
+ cache_create_1h_tokens: cacheWrite1hTokens,
641
2009
  cost_usd: costUsd,
2010
+ cost_basis: defaultCostBasisForAgent(agentName),
642
2011
  duration_ms: 0,
643
2012
  timestamp,
644
- source_request_id: reqId
2013
+ source_request_id: sourceRequestId,
2014
+ machine_id: machineId
645
2015
  });
646
2016
  if (!touchedSessions.has(sessionId)) {
647
2017
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
@@ -650,14 +2020,15 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
650
2020
  const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
651
2021
  const session = {
652
2022
  id: sessionId,
653
- agent: "claude",
2023
+ agent: agentName,
654
2024
  project_path: detectedProject ? detectedProject.path : effectiveCwd,
655
2025
  project_name: detectedProject ? detectedProject.name : "",
656
2026
  started_at: timestamp,
657
2027
  ended_at: null,
658
2028
  total_cost_usd: 0,
659
2029
  total_tokens: 0,
660
- request_count: 0
2030
+ request_count: 0,
2031
+ machine_id: machineId
661
2032
  };
662
2033
  upsertSession(db, session);
663
2034
  }
@@ -665,7 +2036,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
665
2036
  }
666
2037
  totalRequests++;
667
2038
  }
668
- setIngestState(db, "claude", stateKey, fileMtime);
2039
+ setIngestState(db, agentName, stateKey, fileMtime);
669
2040
  totalFiles++;
670
2041
  }
671
2042
  }
@@ -674,66 +2045,1369 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
674
2045
  }
675
2046
  return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
676
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
+ }
677
2069
 
678
2070
  // src/ingest/codex.ts
679
2071
  init_database();
2072
+ init_pricing();
680
2073
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
681
2074
  import { homedir as homedir3 } from "os";
682
2075
  import { join as join3, basename as basename2 } from "path";
683
- import { Database as Database2 } from "bun:sqlite";
684
- var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
685
- var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
2076
+ import { Database as BunDatabase } from "bun:sqlite";
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
+ }
686
2156
  async function ingestCodex(db, verbose = false) {
687
- if (!existsSync3(CODEX_DB_PATH)) {
2157
+ const dbPath = codexDbPath();
2158
+ if (!existsSync3(dbPath)) {
688
2159
  if (verbose)
689
- console.log("Codex DB not found:", CODEX_DB_PATH);
690
- return { sessions: 0 };
2160
+ console.log("Codex DB not found:", dbPath);
2161
+ return { sessions: 0, requests: 0 };
691
2162
  }
2163
+ const machineId = getMachineId();
692
2164
  let codexDb = null;
693
2165
  let ingested = 0;
2166
+ let requests = 0;
694
2167
  try {
695
- codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
696
- 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();
697
2170
  for (const thread of threads) {
698
- const stateKey = thread.id;
699
- const processed = getIngestState(db, "codex", stateKey);
700
- 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)
701
2175
  continue;
702
- const costUsd = 0;
703
2176
  const projectPath = thread.cwd ?? "";
704
2177
  const projectName = projectPath ? basename2(projectPath) : "unknown";
2178
+ const sessionId = `codex-${thread.id}`;
705
2179
  const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
706
2180
  const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
707
2181
  upsertSession(db, {
708
- id: `codex-${thread.id}`,
2182
+ id: sessionId,
709
2183
  agent: "codex",
710
2184
  project_path: projectPath,
711
2185
  project_name: projectName,
712
2186
  started_at: startedAt,
713
2187
  ended_at: endedAt,
714
- total_cost_usd: costUsd,
715
- total_tokens: thread.tokens_used,
716
- request_count: 1
2188
+ total_cost_usd: 0,
2189
+ total_tokens: 0,
2190
+ request_count: 0,
2191
+ machine_id: machineId
2192
+ });
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++;
717
2222
  });
718
- setIngestState(db, "codex", stateKey, "done");
2223
+ rollupSession(db, sessionId);
2224
+ setIngestState(db, "codex", thread.id, stateValue);
719
2225
  ingested++;
720
2226
  if (verbose)
721
- 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}`);
722
2228
  }
723
2229
  } finally {
724
2230
  codexDb?.close();
725
2231
  }
726
- return { sessions: ingested };
2232
+ return { sessions: ingested, requests };
727
2233
  }
728
2234
 
729
- // src/server/serve.ts
2235
+ // src/ingest/gemini.ts
2236
+ init_database();
2237
+ init_pricing();
2238
+ import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
2239
+ import { homedir as homedir4 } from "os";
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
+ }
2282
+ async function ingestGemini(db, verbose) {
2283
+ const tmpDir = geminiTmpDir();
2284
+ const historyDir = geminiHistoryDir();
2285
+ if (!existsSync4(tmpDir) && !existsSync4(historyDir)) {
2286
+ if (verbose)
2287
+ console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
2288
+ return { sessions: 0, requests: 0 };
2289
+ }
2290
+ const machineId = getMachineId();
2291
+ let totalSessions = 0;
2292
+ let totalRequests = 0;
2293
+ const touchedSessions = new Set;
2294
+ const projectDirs = listProjectDirs(tmpDir, historyDir);
2295
+ for (const projectDir of projectDirs) {
2296
+ const chatsDir = join4(projectDir, "chats");
2297
+ if (!existsSync4(chatsDir))
2298
+ continue;
2299
+ let chatFiles = [];
2300
+ try {
2301
+ chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join4(chatsDir, f));
2302
+ } catch {
2303
+ continue;
2304
+ }
2305
+ for (const filePath of chatFiles) {
2306
+ const stateKey = filePath.replace(homedir4(), "~");
2307
+ let fileMtime = "0";
2308
+ try {
2309
+ fileMtime = statSync3(filePath).mtimeMs.toString();
2310
+ } catch {
2311
+ continue;
2312
+ }
2313
+ const processed = getIngestState(db, "gemini", stateKey);
2314
+ if (processed === fileMtime)
2315
+ continue;
2316
+ let chatData;
2317
+ try {
2318
+ chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
2319
+ } catch {
2320
+ continue;
2321
+ }
2322
+ const sessionId = chatData.sessionId ?? chatData.id ?? basename3(filePath, ".json");
2323
+ if (!sessionId)
2324
+ continue;
2325
+ const startTime = chatData.startTime ?? new Date().toISOString();
2326
+ const projectPath = projectRoot(projectDir, chatData);
2327
+ const projectName = projectPath ? basename3(projectPath) : "";
2328
+ const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
2329
+ if (!existing) {
2330
+ const session = {
2331
+ id: sessionId,
2332
+ agent: "gemini",
2333
+ project_path: projectPath,
2334
+ project_name: projectName,
2335
+ started_at: startTime,
2336
+ ended_at: chatData.lastUpdated ?? null,
2337
+ total_cost_usd: 0,
2338
+ total_tokens: 0,
2339
+ request_count: 0,
2340
+ machine_id: machineId
2341
+ };
2342
+ upsertSession(db, session);
2343
+ totalSessions++;
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
+ }
2384
+ setIngestState(db, "gemini", stateKey, fileMtime);
2385
+ }
2386
+ }
2387
+ for (const sessionId of touchedSessions) {
2388
+ rollupSession(db, sessionId);
2389
+ }
2390
+ return { sessions: totalSessions, requests: totalRequests };
2391
+ }
2392
+
2393
+ // src/ingest/opencode.ts
2394
+ init_database();
730
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
731
3400
  import { randomUUID } from "crypto";
3401
+ import { existsSync as existsSync11 } from "fs";
3402
+ import { resolve, sep } from "path";
732
3403
  var CORS = {
733
3404
  "Access-Control-Allow-Origin": "*",
734
3405
  "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
735
- "Access-Control-Allow-Headers": "Content-Type"
3406
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Economy-Token"
736
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;
737
3411
  function json(data, status = 200) {
738
3412
  return new Response(JSON.stringify(data), {
739
3413
  status,
@@ -746,6 +3420,70 @@ function ok(data, meta) {
746
3420
  function err(message, status = 400) {
747
3421
  return json({ error: message }, status);
748
3422
  }
3423
+ function normalizeBudgetPeriod(value) {
3424
+ switch (value) {
3425
+ case "day":
3426
+ case "daily":
3427
+ return "daily";
3428
+ case "week":
3429
+ case "weekly":
3430
+ return "weekly";
3431
+ case "month":
3432
+ case "monthly":
3433
+ default:
3434
+ return "monthly";
3435
+ }
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
+ }
749
3487
  function applyFields(obj, fields) {
750
3488
  if (!fields || fields.length === 0)
751
3489
  return obj;
@@ -758,11 +3496,26 @@ function createHandler(db) {
758
3496
  const method = req.method;
759
3497
  if (method === "OPTIONS")
760
3498
  return new Response(null, { status: 204, headers: CORS });
3499
+ if (!isAuthorizedRequest(req, path))
3500
+ return err("Unauthorized", 401);
761
3501
  if (path === "/health")
762
3502
  return ok({ status: "ok", ts: new Date().toISOString() });
763
3503
  if (path === "/api/summary" && method === "GET") {
764
3504
  const period = url.searchParams.get("period") ?? "today";
765
- return ok(querySummary(db, period));
3505
+ const machine = url.searchParams.get("machine") ?? undefined;
3506
+ return ok(querySummary(db, period, machine));
3507
+ }
3508
+ if (path === "/api/machines" && method === "GET") {
3509
+ return ok(listMachines(db), { current_machine: getMachineId() });
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
+ });
766
3519
  }
767
3520
  if (path === "/api/daily" && method === "GET") {
768
3521
  const days = Number(url.searchParams.get("days") ?? 30);
@@ -771,12 +3524,22 @@ function createHandler(db) {
771
3524
  if (path === "/api/sessions" && method === "GET") {
772
3525
  const agent = url.searchParams.get("agent");
773
3526
  const project = url.searchParams.get("project") ?? undefined;
3527
+ const search = url.searchParams.get("search") ?? undefined;
3528
+ const machine = url.searchParams.get("machine") ?? undefined;
774
3529
  const limit = Number(url.searchParams.get("limit") ?? 50);
775
3530
  const offset = Number(url.searchParams.get("offset") ?? 0);
776
3531
  const since = url.searchParams.get("since") ?? undefined;
777
3532
  const fieldsParam = url.searchParams.get("fields");
778
3533
  const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
779
- const sessions = querySessions(db, { agent: agent ?? undefined, project, limit, offset, since });
3534
+ const sessions = querySessions(db, {
3535
+ agent: agent ?? undefined,
3536
+ project,
3537
+ search,
3538
+ machine,
3539
+ limit,
3540
+ offset,
3541
+ since
3542
+ });
780
3543
  return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
781
3544
  }
782
3545
  if (path === "/api/top" && method === "GET") {
@@ -787,6 +3550,41 @@ function createHandler(db) {
787
3550
  if (path === "/api/models" && method === "GET") {
788
3551
  return ok(queryModelBreakdown(db));
789
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
+ }
790
3588
  if (path === "/api/projects" && method === "GET") {
791
3589
  return ok(queryProjectBreakdown(db));
792
3590
  }
@@ -798,38 +3596,54 @@ function createHandler(db) {
798
3596
  return ok(getBudgetStatuses(db));
799
3597
  }
800
3598
  if (path === "/api/budgets" && method === "POST") {
801
- 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);
802
3611
  const now = new Date().toISOString();
803
- upsertBudget(db, {
3612
+ const budget = {
804
3613
  id: randomUUID(),
805
3614
  project_path: body["project_path"] ?? null,
806
- agent: body["agent"] ?? null,
807
- period: body["period"] ?? "monthly",
808
- limit_usd: Number(body["limit_usd"]),
809
- alert_at_percent: Number(body["alert_at_percent"] ?? 80),
3615
+ agent,
3616
+ period: normalizeBudgetPeriod(body["period"]),
3617
+ limit_usd: limitUsd,
3618
+ alert_at_percent: alertAtPercent,
810
3619
  created_at: now,
811
3620
  updated_at: now
812
- });
813
- return ok({ ok: true });
3621
+ };
3622
+ upsertBudget(db, budget);
3623
+ return ok(getBudgetStatuses(db).find((b) => b.id === budget.id) ?? budget);
814
3624
  }
815
3625
  const budgetMatch = path.match(/^\/api\/budgets\/(.+)$/);
816
3626
  if (budgetMatch && method === "DELETE") {
817
- deleteBudget(db, budgetMatch[1]);
3627
+ deleteBudget(db, decodeURIComponent(budgetMatch[1]));
818
3628
  return ok({ ok: true });
819
3629
  }
820
3630
  if (path === "/api/project-registry" && method === "GET") {
821
3631
  return ok(listProjects(db));
822
3632
  }
823
3633
  if (path === "/api/project-registry" && method === "POST") {
824
- const body = await req.json();
825
- const { basename: basename3 } = await import("path");
826
- 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");
827
3641
  upsertProject(db, {
828
3642
  id: randomUUID(),
829
3643
  path: projPath,
830
- name: body["name"] ?? basename3(projPath),
831
- description: body["description"] ?? null,
832
- tags: body["tags"] ?? [],
3644
+ name: optionalString(body["name"]) ?? basename4(projPath),
3645
+ description: optionalString(body["description"]),
3646
+ tags: stringArray(body["tags"]),
833
3647
  created_at: new Date().toISOString()
834
3648
  });
835
3649
  return ok({ ok: true });
@@ -843,16 +3657,33 @@ function createHandler(db) {
843
3657
  return ok(listModelPricing(db));
844
3658
  }
845
3659
  if (path === "/api/pricing" && method === "POST") {
846
- const body = await req.json();
847
- upsertModelPricing(db, {
848
- model: body["model"],
849
- input_per_1m: Number(body["input_per_1m"]),
850
- output_per_1m: Number(body["output_per_1m"]),
851
- cache_read_per_1m: Number(body["cache_read_per_1m"] ?? 0),
852
- 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,
853
3683
  updated_at: new Date().toISOString()
854
- });
855
- return ok({ ok: true });
3684
+ };
3685
+ upsertModelPricing(db, pricing);
3686
+ return ok(pricing);
856
3687
  }
857
3688
  const pricingMatch = path.match(/^\/api\/pricing\/(.+)$/);
858
3689
  if (pricingMatch && method === "DELETE") {
@@ -860,18 +3691,43 @@ function createHandler(db) {
860
3691
  return ok({ ok: true });
861
3692
  }
862
3693
  if (path === "/api/sync" && method === "POST") {
863
- const body = await req.json().catch(() => ({}));
3694
+ const body = await jsonBody(req) ?? {};
864
3695
  const sources = body["sources"] ?? "all";
3696
+ if (!SYNC_SOURCES.includes(sources))
3697
+ return err("invalid sync source");
865
3698
  const results = {};
866
- if (sources === "all" || sources === "claude")
867
- results["claude"] = await ingestClaude(db);
868
- if (sources === "all" || sources === "codex")
869
- results["codex"] = await ingestCodex(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 {}
870
3712
  return ok(results);
871
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
+ }
872
3728
  const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
873
3729
  if (sessionRequestsMatch && method === "GET") {
874
- const sessionId = sessionRequestsMatch[1];
3730
+ const sessionId = decodeURIComponent(sessionRequestsMatch[1]);
875
3731
  const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(sessionId, `${sessionId}%`);
876
3732
  if (!session)
877
3733
  return err("Session not found", 404);
@@ -882,59 +3738,97 @@ function createHandler(db) {
882
3738
  return ok(getGoalStatuses(db));
883
3739
  }
884
3740
  if (path === "/api/goals" && method === "POST") {
885
- 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);
886
3753
  const now = new Date().toISOString();
887
- upsertGoal(db, {
3754
+ const goal = {
888
3755
  id: randomUUID(),
889
- period: body["period"] ?? "month",
890
- project_path: body["project_path"] ?? null,
891
- agent: body["agent"] ?? null,
892
- limit_usd: Number(body["limit_usd"]),
3756
+ period,
3757
+ project_path: optionalString(body["project_path"]),
3758
+ agent,
3759
+ limit_usd: limitUsd,
893
3760
  created_at: now,
894
3761
  updated_at: now
895
- });
896
- return ok({ ok: true });
3762
+ };
3763
+ upsertGoal(db, goal);
3764
+ return ok(getGoalStatuses(db).find((g) => g.id === goal.id) ?? goal);
897
3765
  }
898
3766
  const goalMatch = path.match(/^\/api\/goals\/(.+)$/);
899
3767
  if (goalMatch && method === "DELETE") {
900
- deleteGoal(db, goalMatch[1]);
3768
+ deleteGoal(db, decodeURIComponent(goalMatch[1]));
901
3769
  return ok({ ok: true });
902
3770
  }
903
3771
  return err("Not found", 404);
904
3772
  };
905
3773
  }
906
- function startServer(port = 3456) {
907
- const db = openDatabase();
3774
+ function startServer(port = 3456, options = {}) {
3775
+ const db = options.db ?? openDatabase();
908
3776
  ensurePricingSeeded(db);
909
3777
  const apiHandler = createHandler(db);
910
- const dashboardDir = new URL("../../dashboard/dist", import.meta.url).pathname;
911
- Bun.serve({
3778
+ const hostname2 = options.hostname ?? getServeBindHost();
3779
+ const server = Bun.serve({
912
3780
  port,
913
- async fetch(req) {
914
- const url = new URL(req.url);
915
- if (url.pathname.startsWith("/api") || url.pathname === "/health") {
916
- return apiHandler(req);
917
- }
918
- try {
919
- const { existsSync: existsSync4 } = await import("fs");
920
- if (existsSync4(dashboardDir)) {
921
- let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
922
- const fullPath = dashboardDir + filePath;
923
- if (existsSync4(fullPath)) {
924
- return new Response(Bun.file(fullPath));
925
- }
926
- const indexPath = dashboardDir + "/index.html";
927
- if (existsSync4(indexPath)) {
928
- return new Response(Bun.file(indexPath));
929
- }
930
- }
931
- } catch {}
932
- return apiHandler(req);
933
- }
3781
+ hostname: hostname2,
3782
+ fetch: createServerFetch(apiHandler, options.dashboardDir)
934
3783
  });
935
- 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;
936
3788
  }
937
3789
 
938
3790
  // src/server/index.ts
939
- var port = Number(process.env["ECONOMY_PORT"] ?? 3456);
940
- startServer(port);
3791
+ function printHelp() {
3792
+ console.log(`Usage: economy-serve [options]
3793
+
3794
+ REST API server for ${packageMetadata.name}
3795
+
3796
+ Options:
3797
+ -p, --port <port> Port to bind (default: ECONOMY_PORT or 3456)
3798
+ -V, --version output the version number
3799
+ -h, --help display help for command`);
3800
+ }
3801
+ function resolvePort(argv) {
3802
+ for (let i = 0;i < argv.length; i++) {
3803
+ const arg = argv[i];
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");
3809
+ }
3810
+ }
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}`);
3817
+ }
3818
+ return value;
3819
+ }
3820
+ var args = process.argv.slice(2);
3821
+ if (args.includes("--help") || args.includes("-h")) {
3822
+ printHelp();
3823
+ process.exit(0);
3824
+ }
3825
+ if (args.includes("--version") || args.includes("-V")) {
3826
+ console.log(packageMetadata.version);
3827
+ process.exit(0);
3828
+ }
3829
+ try {
3830
+ startServer(resolvePort(args));
3831
+ } catch (error) {
3832
+ console.error(error instanceof Error ? error.message : String(error));
3833
+ process.exit(1);
3834
+ }