@hasna/economy 0.2.20 → 0.2.22

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/LICENSE +1 -2
  2. package/README.md +5 -13
  3. package/dist/cli/commands/completion.d.ts +2 -0
  4. package/dist/cli/commands/completion.d.ts.map +1 -0
  5. package/dist/cli/commands/extras.d.ts +4 -0
  6. package/dist/cli/commands/extras.d.ts.map +1 -0
  7. package/dist/cli/commands/menubar.d.ts.map +1 -1
  8. package/dist/cli/commands/notification.d.ts +8 -0
  9. package/dist/cli/commands/notification.d.ts.map +1 -0
  10. package/dist/cli/commands/todos.d.ts +26 -0
  11. package/dist/cli/commands/todos.d.ts.map +1 -0
  12. package/dist/cli/commands/tui.d.ts +10 -0
  13. package/dist/cli/commands/tui.d.ts.map +1 -0
  14. package/dist/cli/commands/watch.d.ts +1 -0
  15. package/dist/cli/commands/watch.d.ts.map +1 -1
  16. package/dist/cli/index.js +5649 -708
  17. package/dist/db/database.d.ts +45 -3
  18. package/dist/db/database.d.ts.map +1 -1
  19. package/dist/db/pg-migrations.d.ts.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1576 -142
  23. package/dist/ingest/billing.d.ts +27 -0
  24. package/dist/ingest/billing.d.ts.map +1 -0
  25. package/dist/ingest/claude-quota.d.ts +5 -0
  26. package/dist/ingest/claude-quota.d.ts.map +1 -0
  27. package/dist/ingest/claude.d.ts +13 -2
  28. package/dist/ingest/claude.d.ts.map +1 -1
  29. package/dist/ingest/codex-quota.d.ts +5 -0
  30. package/dist/ingest/codex-quota.d.ts.map +1 -0
  31. package/dist/ingest/codex.d.ts +2 -1
  32. package/dist/ingest/codex.d.ts.map +1 -1
  33. package/dist/ingest/cursor.d.ts +6 -0
  34. package/dist/ingest/cursor.d.ts.map +1 -0
  35. package/dist/ingest/gemini.d.ts +2 -1
  36. package/dist/ingest/gemini.d.ts.map +1 -1
  37. package/dist/ingest/hermes.d.ts +6 -0
  38. package/dist/ingest/hermes.d.ts.map +1 -0
  39. package/dist/ingest/opencode.d.ts +7 -0
  40. package/dist/ingest/opencode.d.ts.map +1 -0
  41. package/dist/ingest/otel.d.ts +20 -0
  42. package/dist/ingest/otel.d.ts.map +1 -0
  43. package/dist/ingest/pi.d.ts +7 -0
  44. package/dist/ingest/pi.d.ts.map +1 -0
  45. package/dist/ingest/plugin.d.ts +17 -0
  46. package/dist/ingest/plugin.d.ts.map +1 -0
  47. package/dist/lib/accounts.d.ts +11 -0
  48. package/dist/lib/accounts.d.ts.map +1 -0
  49. package/dist/lib/agents.d.ts +11 -0
  50. package/dist/lib/agents.d.ts.map +1 -0
  51. package/dist/lib/billing-diff.d.ts +22 -0
  52. package/dist/lib/billing-diff.d.ts.map +1 -0
  53. package/dist/lib/cloud-sync.d.ts +35 -0
  54. package/dist/lib/cloud-sync.d.ts.map +1 -0
  55. package/dist/lib/config.d.ts.map +1 -1
  56. package/dist/lib/gatherer.d.ts.map +1 -1
  57. package/dist/lib/model-config.d.ts.map +1 -1
  58. package/dist/lib/open-projects.d.ts +19 -0
  59. package/dist/lib/open-projects.d.ts.map +1 -0
  60. package/dist/lib/package-metadata.d.ts +8 -0
  61. package/dist/lib/package-metadata.d.ts.map +1 -0
  62. package/dist/lib/paths.d.ts +20 -0
  63. package/dist/lib/paths.d.ts.map +1 -0
  64. package/dist/lib/pricing.d.ts +3 -3
  65. package/dist/lib/pricing.d.ts.map +1 -1
  66. package/dist/lib/savings.d.ts +17 -0
  67. package/dist/lib/savings.d.ts.map +1 -0
  68. package/dist/lib/serve-auth.d.ts +4 -0
  69. package/dist/lib/serve-auth.d.ts.map +1 -0
  70. package/dist/lib/spikes.d.ts +18 -0
  71. package/dist/lib/spikes.d.ts.map +1 -0
  72. package/dist/lib/sync-all.d.ts +28 -0
  73. package/dist/lib/sync-all.d.ts.map +1 -0
  74. package/dist/lib/watch-paths.d.ts +3 -0
  75. package/dist/lib/watch-paths.d.ts.map +1 -0
  76. package/dist/lib/webhooks.d.ts +1 -1
  77. package/dist/lib/webhooks.d.ts.map +1 -1
  78. package/dist/mcp/index.js +3063 -482
  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 +1423 -0
  82. package/dist/server/index.d.ts +1 -0
  83. package/dist/server/index.js +3550 -269
  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 +102 -6
  87. package/dist/types/index.d.ts.map +1 -1
  88. package/package.json +9 -4
@@ -0,0 +1,1423 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true,
13
+ configurable: true,
14
+ set: __exportSetter.bind(all, name)
15
+ });
16
+ };
17
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+ var __require = import.meta.require;
19
+
20
+ // src/lib/pricing.ts
21
+ var exports_pricing = {};
22
+ __export(exports_pricing, {
23
+ normalizeModelName: () => normalizeModelName,
24
+ getPricingFromDb: () => getPricingFromDb,
25
+ getPricing: () => getPricing,
26
+ ensurePricingSeeded: () => ensurePricingSeeded,
27
+ computeCostFromDb: () => computeCostFromDb,
28
+ computeCost: () => computeCost,
29
+ DEFAULT_PRICING: () => DEFAULT_PRICING
30
+ });
31
+ function normalizeModelName(raw) {
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;
68
+ }
69
+ function ensurePricingSeeded(db) {
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);
169
+ }
170
+ function getPricingFromDb(db, model) {
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);
177
+ }
178
+ const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
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
+ };
195
+ }
196
+ function getPricing(model) {
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");
203
+ }
204
+ function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
205
+ const pricing = getPricing(model);
206
+ if (!pricing)
207
+ return 0;
208
+ return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
209
+ }
210
+ function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
211
+ const pricing = getPricingFromDb(db, model) ?? getPricing(model);
212
+ if (!pricing)
213
+ return 0;
214
+ return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
215
+ }
216
+ function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
217
+ if (isFreeModel(model))
218
+ return 0;
219
+ let effective = pricing;
220
+ const promptTier = bestModelMatch(model, Object.entries(GEMINI_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(QWEN_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(MINIMAX_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(XAI_PROMPT_TIERS)) ?? exactModelMatch(model, Object.entries(OPENAI_PROMPT_TIERS));
221
+ if (promptTier) {
222
+ const billablePromptTokens = inputTokens + cacheReadTokens + cacheWriteTokens + cacheWrite1hTokens;
223
+ if (billablePromptTokens > promptTier.threshold) {
224
+ effective = {
225
+ ...pricing,
226
+ inputPer1M: promptTier.inputPer1M ?? pricing.inputPer1M * (promptTier.inputMultiplier ?? 1),
227
+ outputPer1M: promptTier.outputPer1M ?? pricing.outputPer1M * (promptTier.outputMultiplier ?? 1),
228
+ cacheReadPer1M: promptTier.cacheReadPer1M ?? pricing.cacheReadPer1M * (promptTier.cacheReadMultiplier ?? 1),
229
+ cacheWritePer1M: promptTier.cacheWritePer1M ?? pricing.cacheWritePer1M * (promptTier.cacheWriteMultiplier ?? 1)
230
+ };
231
+ }
232
+ }
233
+ return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
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;
236
+ var init_pricing = __esm(() => {
237
+ init_database();
238
+ DEFAULT_PRICING = {
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 = {
325
+ "claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
326
+ "claude-opus-4": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
327
+ "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
328
+ "gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
329
+ "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
330
+ "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
331
+ "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
332
+ "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, 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 },
335
+ "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, 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
+ }
512
+ };
513
+ });
514
+
515
+ // src/db/database.ts
516
+ import { SqliteAdapter as Database } from "@hasna/cloud";
517
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
518
+ import { hostname } from "os";
519
+ import { homedir } from "os";
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
+ }
529
+ function getDataDir() {
530
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
531
+ const newDir = join(home, ".hasna", "economy");
532
+ const oldDir = join(home, ".economy");
533
+ if (existsSync(oldDir) && !existsSync(newDir)) {
534
+ mkdirSync(newDir, { recursive: true });
535
+ for (const file of readdirSync(oldDir)) {
536
+ const oldPath = join(oldDir, file);
537
+ if (statSync(oldPath).isFile()) {
538
+ copyFileSync(oldPath, join(newDir, file));
539
+ }
540
+ }
541
+ }
542
+ mkdirSync(newDir, { recursive: true });
543
+ return newDir;
544
+ }
545
+ function getDbPath() {
546
+ if (process.env["HASNA_ECONOMY_DB_PATH"])
547
+ return process.env["HASNA_ECONOMY_DB_PATH"];
548
+ if (process.env["ECONOMY_DB"])
549
+ return process.env["ECONOMY_DB"];
550
+ return join(getDataDir(), "economy.db");
551
+ }
552
+ function openDatabase(dbPath, skipSeed = false) {
553
+ const path = dbPath ?? getDbPath();
554
+ if (path !== ":memory:") {
555
+ const dir = path.substring(0, path.lastIndexOf("/"));
556
+ if (dir && !existsSync(dir))
557
+ mkdirSync(dir, { recursive: true });
558
+ }
559
+ const db = new Database(path);
560
+ db.exec("PRAGMA journal_mode = WAL");
561
+ db.exec("PRAGMA busy_timeout = 5000");
562
+ db.exec("PRAGMA foreign_keys = ON");
563
+ initSchema(db);
564
+ if (!skipSeed) {
565
+ Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
566
+ }
567
+ return db;
568
+ }
569
+ function initSchema(db) {
570
+ db.exec(`
571
+ CREATE TABLE IF NOT EXISTS requests (
572
+ id TEXT PRIMARY KEY,
573
+ agent TEXT NOT NULL,
574
+ session_id TEXT NOT NULL,
575
+ model TEXT NOT NULL,
576
+ input_tokens INTEGER DEFAULT 0,
577
+ output_tokens INTEGER DEFAULT 0,
578
+ cache_read_tokens INTEGER DEFAULT 0,
579
+ cache_create_tokens INTEGER DEFAULT 0,
580
+ cache_create_5m_tokens INTEGER DEFAULT 0,
581
+ cache_create_1h_tokens INTEGER DEFAULT 0,
582
+ cost_usd REAL NOT NULL DEFAULT 0,
583
+ duration_ms INTEGER DEFAULT 0,
584
+ timestamp TEXT NOT NULL,
585
+ source_request_id TEXT,
586
+ machine_id TEXT DEFAULT '',
587
+ account_key TEXT DEFAULT '',
588
+ account_tool TEXT DEFAULT '',
589
+ account_name TEXT DEFAULT '',
590
+ account_email TEXT DEFAULT '',
591
+ account_source TEXT DEFAULT ''
592
+ );
593
+
594
+ CREATE TABLE IF NOT EXISTS sessions (
595
+ id TEXT PRIMARY KEY,
596
+ agent TEXT NOT NULL,
597
+ project_path TEXT DEFAULT '',
598
+ project_name TEXT DEFAULT '',
599
+ started_at TEXT NOT NULL,
600
+ ended_at TEXT,
601
+ total_cost_usd REAL DEFAULT 0,
602
+ total_tokens INTEGER DEFAULT 0,
603
+ request_count INTEGER DEFAULT 0,
604
+ machine_id TEXT DEFAULT '',
605
+ account_key TEXT DEFAULT '',
606
+ account_tool TEXT DEFAULT '',
607
+ account_name TEXT DEFAULT '',
608
+ account_email TEXT DEFAULT '',
609
+ account_source TEXT DEFAULT ''
610
+ );
611
+
612
+ CREATE TABLE IF NOT EXISTS projects (
613
+ id TEXT PRIMARY KEY,
614
+ path TEXT UNIQUE NOT NULL,
615
+ name TEXT NOT NULL,
616
+ description TEXT,
617
+ tags TEXT DEFAULT '[]',
618
+ created_at TEXT NOT NULL
619
+ );
620
+
621
+ CREATE TABLE IF NOT EXISTS budgets (
622
+ id TEXT PRIMARY KEY,
623
+ project_path TEXT,
624
+ agent TEXT,
625
+ period TEXT NOT NULL,
626
+ limit_usd REAL NOT NULL,
627
+ alert_at_percent INTEGER DEFAULT 80,
628
+ created_at TEXT NOT NULL,
629
+ updated_at TEXT NOT NULL
630
+ );
631
+
632
+ CREATE TABLE IF NOT EXISTS goals (
633
+ id TEXT PRIMARY KEY,
634
+ period TEXT NOT NULL,
635
+ project_path TEXT,
636
+ agent TEXT,
637
+ limit_usd REAL NOT NULL,
638
+ created_at TEXT NOT NULL,
639
+ updated_at TEXT NOT NULL
640
+ );
641
+
642
+ CREATE TABLE IF NOT EXISTS ingest_state (
643
+ source TEXT NOT NULL,
644
+ key TEXT NOT NULL,
645
+ value TEXT NOT NULL,
646
+ PRIMARY KEY (source, key)
647
+ );
648
+
649
+ CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
650
+ CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp);
651
+ CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent);
652
+ CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
653
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
654
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
655
+
656
+ CREATE TABLE IF NOT EXISTS model_pricing (
657
+ model TEXT PRIMARY KEY,
658
+ input_per_1m REAL NOT NULL DEFAULT 0,
659
+ output_per_1m REAL NOT NULL DEFAULT 0,
660
+ cache_read_per_1m REAL NOT NULL DEFAULT 0,
661
+ cache_write_per_1m REAL NOT NULL DEFAULT 0,
662
+ cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
663
+ cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
664
+ updated_at TEXT NOT NULL
665
+ );
666
+
667
+ CREATE TABLE IF NOT EXISTS feedback (
668
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
669
+ message TEXT NOT NULL,
670
+ email TEXT,
671
+ category TEXT DEFAULT 'general',
672
+ version TEXT,
673
+ machine_id TEXT,
674
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
675
+ );
676
+
677
+ CREATE TABLE IF NOT EXISTS billing_daily (
678
+ date TEXT NOT NULL,
679
+ provider TEXT NOT NULL,
680
+ description TEXT DEFAULT '',
681
+ cost_usd REAL NOT NULL DEFAULT 0,
682
+ updated_at TEXT NOT NULL,
683
+ PRIMARY KEY (date, provider, description)
684
+ );
685
+
686
+ CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
687
+ CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
688
+
689
+ CREATE TABLE IF NOT EXISTS subscriptions (
690
+ id TEXT PRIMARY KEY,
691
+ agent TEXT,
692
+ provider TEXT NOT NULL,
693
+ plan TEXT NOT NULL,
694
+ monthly_fee_usd REAL NOT NULL DEFAULT 0,
695
+ included_usage_usd REAL NOT NULL DEFAULT 0,
696
+ billing_cycle_start TEXT,
697
+ reset_policy TEXT DEFAULT 'monthly',
698
+ active INTEGER NOT NULL DEFAULT 1,
699
+ created_at TEXT NOT NULL,
700
+ updated_at TEXT NOT NULL
701
+ );
702
+
703
+ CREATE TABLE IF NOT EXISTS usage_snapshots (
704
+ id TEXT PRIMARY KEY,
705
+ agent TEXT NOT NULL,
706
+ date TEXT NOT NULL,
707
+ metric TEXT NOT NULL,
708
+ value REAL NOT NULL DEFAULT 0,
709
+ unit TEXT DEFAULT '',
710
+ machine_id TEXT DEFAULT '',
711
+ updated_at TEXT NOT NULL
712
+ );
713
+
714
+ CREATE TABLE IF NOT EXISTS savings_daily (
715
+ date TEXT NOT NULL,
716
+ agent TEXT DEFAULT '',
717
+ api_equivalent_usd REAL NOT NULL DEFAULT 0,
718
+ subscription_fee_usd REAL NOT NULL DEFAULT 0,
719
+ included_consumed_usd REAL NOT NULL DEFAULT 0,
720
+ on_demand_usd REAL NOT NULL DEFAULT 0,
721
+ saved_usd REAL NOT NULL DEFAULT 0,
722
+ updated_at TEXT NOT NULL,
723
+ PRIMARY KEY (date, agent)
724
+ );
725
+
726
+ CREATE TABLE IF NOT EXISTS machines (
727
+ machine_id TEXT PRIMARY KEY,
728
+ hostname TEXT NOT NULL,
729
+ last_seen_at TEXT,
730
+ last_push_at TEXT,
731
+ last_pull_at TEXT,
732
+ economy_version TEXT,
733
+ updated_at TEXT NOT NULL
734
+ );
735
+
736
+ CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
737
+ CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
738
+ `);
739
+ const cols = db.prepare(`PRAGMA table_info(requests)`).all();
740
+ if (!cols.some((c) => c.name === "machine_id")) {
741
+ db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
742
+ db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
743
+ }
744
+ if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
745
+ db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
746
+ db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
747
+ }
748
+ if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
749
+ db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
750
+ }
751
+ if (!cols.some((c) => c.name === "cost_basis")) {
752
+ db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
753
+ }
754
+ if (!cols.some((c) => c.name === "attribution_tag")) {
755
+ db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
756
+ }
757
+ if (!cols.some((c) => c.name === "updated_at")) {
758
+ db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
759
+ db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
760
+ }
761
+ if (!cols.some((c) => c.name === "synced_at")) {
762
+ db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
763
+ }
764
+ for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
765
+ if (!cols.some((c) => c.name === column)) {
766
+ db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
767
+ }
768
+ }
769
+ const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
770
+ if (!sessionCols.some((c) => c.name === "attribution_tag")) {
771
+ db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
772
+ }
773
+ if (!sessionCols.some((c) => c.name === "updated_at")) {
774
+ db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
775
+ db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
776
+ }
777
+ if (!sessionCols.some((c) => c.name === "synced_at")) {
778
+ db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
779
+ }
780
+ for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
781
+ if (!sessionCols.some((c) => c.name === column)) {
782
+ db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
783
+ }
784
+ }
785
+ const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
786
+ if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
787
+ db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
788
+ }
789
+ if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
790
+ db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
791
+ }
792
+ db.exec(`
793
+ CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
794
+ CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
795
+ CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
796
+ CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
797
+ `);
798
+ }
799
+ function upsertRequest(db, req) {
800
+ const now = req.updated_at ?? new Date().toISOString();
801
+ db.prepare(`
802
+ INSERT OR REPLACE INTO requests
803
+ (id, agent, session_id, model, input_tokens, output_tokens,
804
+ cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
805
+ cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
806
+ source_request_id, machine_id, attribution_tag, account_key, account_tool,
807
+ account_name, account_email, account_source, updated_at, synced_at)
808
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
809
+ `).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"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
810
+ }
811
+ function upsertSession(db, session) {
812
+ const now = session.updated_at ?? new Date().toISOString();
813
+ db.prepare(`
814
+ INSERT OR REPLACE INTO sessions
815
+ (id, agent, project_path, project_name, started_at, ended_at,
816
+ total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
817
+ account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
818
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
819
+ `).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"] ?? "", session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
820
+ }
821
+ function rollupSession(db, sessionId) {
822
+ db.prepare(`
823
+ UPDATE sessions SET
824
+ total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM requests WHERE session_id = ?),
825
+ total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM requests WHERE session_id = ?),
826
+ request_count = (SELECT COUNT(*) FROM requests WHERE session_id = ?),
827
+ ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
828
+ started_at = CASE WHEN started_at = '' OR started_at IS NULL
829
+ THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
830
+ ELSE started_at END,
831
+ account_key = CASE WHEN account_key = '' OR account_key IS NULL
832
+ THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
833
+ ELSE account_key END,
834
+ account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
835
+ THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
836
+ ELSE account_tool END,
837
+ account_name = CASE WHEN account_name = '' OR account_name IS NULL
838
+ THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
839
+ ELSE account_name END,
840
+ account_email = CASE WHEN account_email = '' OR account_email IS NULL
841
+ THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
842
+ ELSE account_email END,
843
+ account_source = CASE WHEN account_source = '' OR account_source IS NULL
844
+ THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
845
+ ELSE account_source END
846
+ WHERE id = ?
847
+ `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
848
+ }
849
+ function upsertModelPricing(db, p) {
850
+ db.prepare(`
851
+ INSERT OR REPLACE INTO model_pricing
852
+ (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)
853
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
854
+ `).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);
855
+ }
856
+ function getModelPricing(db, model) {
857
+ return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
858
+ }
859
+ function deleteModelPricing(db, model) {
860
+ db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
861
+ }
862
+ function seedModelPricing(db, defaults) {
863
+ const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
864
+ const now = new Date().toISOString();
865
+ for (const [model, p] of Object.entries(defaults)) {
866
+ if (existing.has(model))
867
+ continue;
868
+ upsertModelPricing(db, {
869
+ model,
870
+ input_per_1m: p.inputPer1M,
871
+ output_per_1m: p.outputPer1M,
872
+ cache_read_per_1m: p.cacheReadPer1M,
873
+ cache_write_per_1m: p.cacheWritePer1M,
874
+ cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
875
+ cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
876
+ updated_at: now
877
+ });
878
+ }
879
+ }
880
+ var init_database = () => {};
881
+
882
+ // src/db/pg-migrations.ts
883
+ var exports_pg_migrations = {};
884
+ __export(exports_pg_migrations, {
885
+ PG_MIGRATIONS: () => PG_MIGRATIONS
886
+ });
887
+ var PG_MIGRATIONS;
888
+ var init_pg_migrations = __esm(() => {
889
+ PG_MIGRATIONS = [
890
+ `CREATE TABLE IF NOT EXISTS requests (
891
+ id TEXT PRIMARY KEY,
892
+ agent TEXT NOT NULL,
893
+ session_id TEXT NOT NULL,
894
+ model TEXT NOT NULL,
895
+ input_tokens INTEGER DEFAULT 0,
896
+ output_tokens INTEGER DEFAULT 0,
897
+ cache_read_tokens INTEGER DEFAULT 0,
898
+ cache_create_tokens INTEGER DEFAULT 0,
899
+ cache_create_5m_tokens INTEGER DEFAULT 0,
900
+ cache_create_1h_tokens INTEGER DEFAULT 0,
901
+ cost_usd REAL NOT NULL DEFAULT 0,
902
+ duration_ms INTEGER DEFAULT 0,
903
+ timestamp TEXT NOT NULL,
904
+ source_request_id TEXT,
905
+ machine_id TEXT DEFAULT '',
906
+ account_key TEXT DEFAULT '',
907
+ account_tool TEXT DEFAULT '',
908
+ account_name TEXT DEFAULT '',
909
+ account_email TEXT DEFAULT '',
910
+ account_source TEXT DEFAULT ''
911
+ )`,
912
+ `CREATE TABLE IF NOT EXISTS sessions (
913
+ id TEXT PRIMARY KEY,
914
+ agent TEXT NOT NULL,
915
+ project_path TEXT DEFAULT '',
916
+ project_name TEXT DEFAULT '',
917
+ started_at TEXT NOT NULL,
918
+ ended_at TEXT,
919
+ total_cost_usd REAL DEFAULT 0,
920
+ total_tokens INTEGER DEFAULT 0,
921
+ request_count INTEGER DEFAULT 0,
922
+ machine_id TEXT DEFAULT '',
923
+ account_key TEXT DEFAULT '',
924
+ account_tool TEXT DEFAULT '',
925
+ account_name TEXT DEFAULT '',
926
+ account_email TEXT DEFAULT '',
927
+ account_source TEXT DEFAULT ''
928
+ )`,
929
+ `CREATE TABLE IF NOT EXISTS projects (
930
+ id TEXT PRIMARY KEY,
931
+ path TEXT UNIQUE NOT NULL,
932
+ name TEXT NOT NULL,
933
+ description TEXT,
934
+ tags TEXT DEFAULT '[]',
935
+ created_at TEXT NOT NULL
936
+ )`,
937
+ `CREATE TABLE IF NOT EXISTS budgets (
938
+ id TEXT PRIMARY KEY,
939
+ project_path TEXT,
940
+ agent TEXT,
941
+ period TEXT NOT NULL,
942
+ limit_usd REAL NOT NULL,
943
+ alert_at_percent INTEGER DEFAULT 80,
944
+ created_at TEXT NOT NULL,
945
+ updated_at TEXT NOT NULL
946
+ )`,
947
+ `CREATE TABLE IF NOT EXISTS goals (
948
+ id TEXT PRIMARY KEY,
949
+ period TEXT NOT NULL,
950
+ project_path TEXT,
951
+ agent TEXT,
952
+ limit_usd REAL NOT NULL,
953
+ created_at TEXT NOT NULL,
954
+ updated_at TEXT NOT NULL
955
+ )`,
956
+ `CREATE TABLE IF NOT EXISTS ingest_state (
957
+ source TEXT NOT NULL,
958
+ key TEXT NOT NULL,
959
+ value TEXT NOT NULL,
960
+ PRIMARY KEY (source, key)
961
+ )`,
962
+ `CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
963
+ `CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
964
+ `CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
965
+ `CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
966
+ `CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
967
+ `CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
968
+ `CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
969
+ `CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
970
+ `CREATE TABLE IF NOT EXISTS model_pricing (
971
+ model TEXT PRIMARY KEY,
972
+ input_per_1m REAL NOT NULL DEFAULT 0,
973
+ output_per_1m REAL NOT NULL DEFAULT 0,
974
+ cache_read_per_1m REAL NOT NULL DEFAULT 0,
975
+ cache_write_per_1m REAL NOT NULL DEFAULT 0,
976
+ cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
977
+ cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
978
+ updated_at TEXT NOT NULL
979
+ )`,
980
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_5m_tokens INTEGER DEFAULT 0`,
981
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_1h_tokens INTEGER DEFAULT 0`,
982
+ `ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`,
983
+ `ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`,
984
+ `CREATE TABLE IF NOT EXISTS feedback (
985
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
986
+ message TEXT NOT NULL,
987
+ email TEXT,
988
+ category TEXT DEFAULT 'general',
989
+ version TEXT,
990
+ machine_id TEXT,
991
+ created_at TEXT NOT NULL DEFAULT NOW()::text
992
+ )`,
993
+ `CREATE TABLE IF NOT EXISTS billing_daily (
994
+ date TEXT NOT NULL,
995
+ provider TEXT NOT NULL,
996
+ description TEXT DEFAULT '',
997
+ cost_usd REAL NOT NULL DEFAULT 0,
998
+ updated_at TEXT NOT NULL,
999
+ PRIMARY KEY (date, provider, description)
1000
+ )`,
1001
+ `CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date)`,
1002
+ `CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider)`,
1003
+ `CREATE TABLE IF NOT EXISTS subscriptions (
1004
+ id TEXT PRIMARY KEY,
1005
+ agent TEXT,
1006
+ provider TEXT NOT NULL,
1007
+ plan TEXT NOT NULL,
1008
+ monthly_fee_usd REAL NOT NULL DEFAULT 0,
1009
+ included_usage_usd REAL NOT NULL DEFAULT 0,
1010
+ billing_cycle_start TEXT,
1011
+ reset_policy TEXT DEFAULT 'monthly',
1012
+ active INTEGER NOT NULL DEFAULT 1,
1013
+ created_at TEXT NOT NULL,
1014
+ updated_at TEXT NOT NULL
1015
+ )`,
1016
+ `CREATE TABLE IF NOT EXISTS usage_snapshots (
1017
+ id TEXT PRIMARY KEY,
1018
+ agent TEXT NOT NULL,
1019
+ date TEXT NOT NULL,
1020
+ metric TEXT NOT NULL,
1021
+ value REAL NOT NULL DEFAULT 0,
1022
+ unit TEXT DEFAULT '',
1023
+ machine_id TEXT DEFAULT '',
1024
+ updated_at TEXT NOT NULL
1025
+ )`,
1026
+ `CREATE TABLE IF NOT EXISTS savings_daily (
1027
+ date TEXT NOT NULL,
1028
+ agent TEXT DEFAULT '',
1029
+ api_equivalent_usd REAL NOT NULL DEFAULT 0,
1030
+ subscription_fee_usd REAL NOT NULL DEFAULT 0,
1031
+ included_consumed_usd REAL NOT NULL DEFAULT 0,
1032
+ on_demand_usd REAL NOT NULL DEFAULT 0,
1033
+ saved_usd REAL NOT NULL DEFAULT 0,
1034
+ updated_at TEXT NOT NULL,
1035
+ PRIMARY KEY (date, agent)
1036
+ )`,
1037
+ `CREATE TABLE IF NOT EXISTS machines (
1038
+ machine_id TEXT PRIMARY KEY,
1039
+ hostname TEXT NOT NULL,
1040
+ last_seen_at TEXT,
1041
+ last_push_at TEXT,
1042
+ last_pull_at TEXT,
1043
+ economy_version TEXT,
1044
+ updated_at TEXT NOT NULL
1045
+ )`,
1046
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
1047
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1048
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1049
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1050
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1051
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1052
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1053
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1054
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1055
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1056
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1057
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1058
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1059
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1060
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1061
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1062
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1063
+ `CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
1064
+ `CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
1065
+ `CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
1066
+ `CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`
1067
+ ];
1068
+ });
1069
+
1070
+ // src/otel/index.ts
1071
+ init_database();
1072
+
1073
+ // src/ingest/otel.ts
1074
+ init_database();
1075
+
1076
+ // src/lib/agents.ts
1077
+ var AGENTS = [
1078
+ "claude",
1079
+ "takumi",
1080
+ "codex",
1081
+ "gemini",
1082
+ "opencode",
1083
+ "cursor",
1084
+ "pi",
1085
+ "hermes"
1086
+ ];
1087
+ function isAgent(value) {
1088
+ return AGENTS.includes(value);
1089
+ }
1090
+
1091
+ // src/ingest/otel.ts
1092
+ function attrValue(attr) {
1093
+ if (!attr?.value)
1094
+ return;
1095
+ if (attr.value.stringValue != null)
1096
+ return attr.value.stringValue;
1097
+ if (attr.value.doubleValue != null)
1098
+ return attr.value.doubleValue;
1099
+ if (attr.value.intValue != null)
1100
+ return Number(attr.value.intValue);
1101
+ return;
1102
+ }
1103
+ function attrsMap(attributes) {
1104
+ const out = {};
1105
+ for (const a of attributes ?? []) {
1106
+ if (!a.key)
1107
+ continue;
1108
+ const v = attrValue(a);
1109
+ if (v != null)
1110
+ out[a.key] = v;
1111
+ }
1112
+ return out;
1113
+ }
1114
+ function metricKind(body) {
1115
+ const name = String(body["name"] ?? "").toLowerCase();
1116
+ if (name.includes("cost") || name.includes(".usd"))
1117
+ return "cost";
1118
+ if (name.includes("input") && name.includes("token"))
1119
+ return "input_tokens";
1120
+ if (name.includes("output") && name.includes("token"))
1121
+ return "output_tokens";
1122
+ if (name.endsWith(".token.usage") || name.includes("tokens.input"))
1123
+ return "input_tokens";
1124
+ if (name.includes("tokens.output"))
1125
+ return "output_tokens";
1126
+ return null;
1127
+ }
1128
+ function parseOtlpMetrics(body) {
1129
+ if (!body || typeof body !== "object")
1130
+ return [];
1131
+ const resourceMetrics = body["resourceMetrics"];
1132
+ if (!Array.isArray(resourceMetrics))
1133
+ return [];
1134
+ const partial = new Map;
1135
+ for (const rm of resourceMetrics) {
1136
+ if (!rm || typeof rm !== "object")
1137
+ continue;
1138
+ const resourceAttrs = attrsMap(rm["resource"] ? rm["resource"].attributes : undefined);
1139
+ const scopeMetrics = rm["scopeMetrics"];
1140
+ if (!Array.isArray(scopeMetrics))
1141
+ continue;
1142
+ for (const sm of scopeMetrics) {
1143
+ if (!sm || typeof sm !== "object")
1144
+ continue;
1145
+ const metrics = sm["metrics"];
1146
+ if (!Array.isArray(metrics))
1147
+ continue;
1148
+ for (const metric of metrics) {
1149
+ if (!metric || typeof metric !== "object")
1150
+ continue;
1151
+ const kind = metricKind(metric);
1152
+ if (!kind)
1153
+ continue;
1154
+ const sum = metric["sum"];
1155
+ const gauge = metric["gauge"];
1156
+ const dataPoints = sum?.dataPoints ?? gauge?.dataPoints ?? [];
1157
+ for (const dp of dataPoints) {
1158
+ if (!dp || typeof dp !== "object")
1159
+ continue;
1160
+ const pointAttrs = attrsMap(dp.attributes);
1161
+ const merged = { ...resourceAttrs, ...pointAttrs };
1162
+ const agentRaw = String(merged["agent"] ?? merged["ai.agent"] ?? "unknown");
1163
+ const agent = isAgent(agentRaw) ? agentRaw : "opencode";
1164
+ const sessionId = String(merged["session_id"] ?? merged["session.id"] ?? "otel-session");
1165
+ const model = String(merged["model"] ?? merged["ai.model"] ?? "unknown");
1166
+ const sourceId = String(merged["request_id"] ?? merged["event.id"] ?? `${sessionId}-${kind}`);
1167
+ const key = `${agent}:${sourceId}`;
1168
+ const row = partial.get(key) ?? {
1169
+ key,
1170
+ agent,
1171
+ session_id: sessionId,
1172
+ model,
1173
+ cost_usd: 0,
1174
+ input_tokens: 0,
1175
+ output_tokens: 0,
1176
+ timestamp: new Date().toISOString(),
1177
+ source_request_id: sourceId
1178
+ };
1179
+ const asDouble = dp.asDouble;
1180
+ const asInt = dp.asInt;
1181
+ const value = asDouble ?? (asInt != null ? Number(asInt) : 0);
1182
+ if (kind === "cost")
1183
+ row.cost_usd = value;
1184
+ if (kind === "input_tokens")
1185
+ row.input_tokens = Math.round(value);
1186
+ if (kind === "output_tokens")
1187
+ row.output_tokens = Math.round(value);
1188
+ const timeUnixNano = dp.timeUnixNano;
1189
+ if (timeUnixNano) {
1190
+ row.timestamp = new Date(Number(timeUnixNano) / 1e6).toISOString();
1191
+ }
1192
+ partial.set(key, row);
1193
+ }
1194
+ }
1195
+ }
1196
+ }
1197
+ return [...partial.values()].filter((r) => (r.cost_usd ?? 0) > 0 || (r.input_tokens ?? 0) + (r.output_tokens ?? 0) > 0).map((r) => ({
1198
+ agent: r.agent,
1199
+ session_id: r.session_id,
1200
+ model: r.model,
1201
+ cost_usd: r.cost_usd ?? 0,
1202
+ input_tokens: r.input_tokens ?? 0,
1203
+ output_tokens: r.output_tokens ?? 0,
1204
+ timestamp: r.timestamp,
1205
+ source_request_id: r.source_request_id
1206
+ }));
1207
+ }
1208
+ function parseSimpleIngest(body) {
1209
+ if (!body || typeof body !== "object")
1210
+ return null;
1211
+ const b = body;
1212
+ const agentRaw = String(b["agent"] ?? "");
1213
+ if (!isAgent(agentRaw))
1214
+ return null;
1215
+ const cost = Number(b["cost_usd"] ?? 0);
1216
+ const input = Number(b["input_tokens"] ?? 0);
1217
+ const output = Number(b["output_tokens"] ?? 0);
1218
+ if (cost <= 0 && input + output <= 0)
1219
+ return null;
1220
+ const sessionId = String(b["session_id"] ?? "otel-session");
1221
+ const sourceId = String(b["request_id"] ?? b["source_request_id"] ?? `${sessionId}-${Date.now()}`);
1222
+ return {
1223
+ agent: agentRaw,
1224
+ session_id: sessionId,
1225
+ model: String(b["model"] ?? "unknown"),
1226
+ cost_usd: cost,
1227
+ input_tokens: input,
1228
+ output_tokens: output,
1229
+ timestamp: String(b["timestamp"] ?? new Date().toISOString()),
1230
+ source_request_id: sourceId
1231
+ };
1232
+ }
1233
+ async function ingestOtelRows(db, rows) {
1234
+ const machineId = getMachineId();
1235
+ const sessions = new Set;
1236
+ let requests = 0;
1237
+ for (const row of rows) {
1238
+ const reqId = `otel-${row.agent}-${row.source_request_id}`;
1239
+ upsertRequest(db, {
1240
+ id: reqId,
1241
+ agent: row.agent,
1242
+ session_id: row.session_id,
1243
+ model: row.model,
1244
+ input_tokens: row.input_tokens,
1245
+ output_tokens: row.output_tokens,
1246
+ cache_read_tokens: 0,
1247
+ cache_create_tokens: 0,
1248
+ cost_usd: row.cost_usd,
1249
+ cost_basis: "estimated",
1250
+ duration_ms: 0,
1251
+ timestamp: row.timestamp,
1252
+ source_request_id: row.source_request_id,
1253
+ machine_id: machineId,
1254
+ attribution_tag: "otel"
1255
+ });
1256
+ if (!sessions.has(row.session_id)) {
1257
+ upsertSession(db, {
1258
+ id: row.session_id,
1259
+ agent: row.agent,
1260
+ project_path: "",
1261
+ project_name: "",
1262
+ started_at: row.timestamp,
1263
+ ended_at: null,
1264
+ total_cost_usd: 0,
1265
+ total_tokens: 0,
1266
+ request_count: 0,
1267
+ machine_id: machineId
1268
+ });
1269
+ sessions.add(row.session_id);
1270
+ }
1271
+ requests++;
1272
+ }
1273
+ for (const sessionId of sessions)
1274
+ rollupSession(db, sessionId);
1275
+ return { requests, sessions: sessions.size };
1276
+ }
1277
+
1278
+ // src/lib/cloud-sync.ts
1279
+ init_database();
1280
+
1281
+ // src/lib/package-metadata.ts
1282
+ import { readFileSync } from "fs";
1283
+ var cachedMetadata = null;
1284
+ function getPackageMetadata() {
1285
+ if (cachedMetadata)
1286
+ return cachedMetadata;
1287
+ const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf8");
1288
+ const parsed = JSON.parse(raw);
1289
+ cachedMetadata = {
1290
+ name: parsed.name ?? "@hasna/economy",
1291
+ version: parsed.version ?? "0.0.0"
1292
+ };
1293
+ return cachedMetadata;
1294
+ }
1295
+ var packageMetadata = getPackageMetadata();
1296
+
1297
+ // src/lib/cloud-sync.ts
1298
+ var CLOUD_TABLES = [
1299
+ "requests",
1300
+ "sessions",
1301
+ "projects",
1302
+ "budgets",
1303
+ "goals",
1304
+ "model_pricing",
1305
+ "billing_daily",
1306
+ "subscriptions",
1307
+ "usage_snapshots",
1308
+ "savings_daily",
1309
+ "machines",
1310
+ "ingest_state"
1311
+ ];
1312
+ function getCloudDatabaseUrl() {
1313
+ return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
1314
+ }
1315
+ function isCloudAutoEnabled() {
1316
+ return process.env["ECONOMY_CLOUD_AUTO"] === "1" || process.env["ECONOMY_CLOUD_AUTO"] === "true";
1317
+ }
1318
+ async function getCloudPg() {
1319
+ const url = getCloudDatabaseUrl();
1320
+ if (!url) {
1321
+ throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
1322
+ }
1323
+ const { PgAdapterAsync } = await import("@hasna/cloud");
1324
+ return new PgAdapterAsync(url);
1325
+ }
1326
+ async function runCloudMigrations(cloud) {
1327
+ const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
1328
+ for (const sql of PG_MIGRATIONS2) {
1329
+ await cloud.run(sql);
1330
+ }
1331
+ }
1332
+ async function cloudPush(opts) {
1333
+ const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
1334
+ const cloud = await getCloudPg();
1335
+ const local = new SqliteAdapter(getDbPath());
1336
+ await runCloudMigrations(cloud);
1337
+ const tables = opts?.tables ?? [...CLOUD_TABLES];
1338
+ const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
1339
+ const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
1340
+ touchMachineRegistry(local, "push");
1341
+ local.close();
1342
+ await cloud.close();
1343
+ return { rows, machine: getMachineId() };
1344
+ }
1345
+ async function maybePushAfterIngest() {
1346
+ if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
1347
+ return false;
1348
+ try {
1349
+ await cloudPush();
1350
+ return true;
1351
+ } catch {
1352
+ return false;
1353
+ }
1354
+ }
1355
+ function touchMachineRegistry(db, direction) {
1356
+ const now = new Date().toISOString();
1357
+ const machine = getMachineId();
1358
+ db.prepare(`
1359
+ INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
1360
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1361
+ ON CONFLICT(machine_id) DO UPDATE SET
1362
+ hostname = excluded.hostname,
1363
+ last_seen_at = excluded.last_seen_at,
1364
+ last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
1365
+ last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
1366
+ economy_version = excluded.economy_version,
1367
+ updated_at = excluded.updated_at
1368
+ `).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
1369
+ }
1370
+
1371
+ // src/otel/index.ts
1372
+ function resolvePort(argv) {
1373
+ for (let i = 0;i < argv.length; i++) {
1374
+ if (argv[i] === "--port" || argv[i] === "-p") {
1375
+ return Number(argv[i + 1] ?? 4318);
1376
+ }
1377
+ }
1378
+ return Number(process.env["ECONOMY_OTEL_PORT"] ?? 4318);
1379
+ }
1380
+ var args = process.argv.slice(2);
1381
+ if (args.includes("--help") || args.includes("-h")) {
1382
+ console.log(`Usage: economy-otel [--port 4318]
1383
+
1384
+ OTLP/HTTP metrics sidecar \u2014 ingests *.cost.* / *.token.* metrics into economy.db
1385
+
1386
+ Endpoints:
1387
+ POST /v1/metrics OTLP JSON metrics
1388
+ POST /ingest Simplified single-event JSON
1389
+ GET /health Health check`);
1390
+ process.exit(0);
1391
+ }
1392
+ var port = resolvePort(args);
1393
+ var db = openDatabase();
1394
+ var server = Bun.serve({
1395
+ port,
1396
+ hostname: process.env["ECONOMY_OTEL_BIND"] ?? "127.0.0.1",
1397
+ async fetch(req) {
1398
+ const url = new URL(req.url);
1399
+ if (req.method === "GET" && url.pathname === "/health") {
1400
+ return Response.json({ status: "ok", service: "economy-otel", version: packageMetadata.version });
1401
+ }
1402
+ if (req.method !== "POST") {
1403
+ return Response.json({ error: "method not allowed" }, { status: 405 });
1404
+ }
1405
+ let body;
1406
+ try {
1407
+ body = await req.json();
1408
+ } catch {
1409
+ return Response.json({ error: "invalid JSON" }, { status: 400 });
1410
+ }
1411
+ let rows = url.pathname === "/ingest" ? (() => {
1412
+ const one = parseSimpleIngest(body);
1413
+ return one ? [one] : [];
1414
+ })() : parseOtlpMetrics(body);
1415
+ if (rows.length === 0) {
1416
+ return Response.json({ ingested: 0, message: "no matching metrics" });
1417
+ }
1418
+ const result = await ingestOtelRows(db, rows);
1419
+ await maybePushAfterIngest();
1420
+ return Response.json({ ingested: result.requests, sessions: result.sessions });
1421
+ }
1422
+ });
1423
+ console.log(`economy-otel listening on http://127.0.0.1:${server.port}`);