@hasna/economy 0.2.19 → 0.2.20

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/menubar.d.ts.map +1 -1
  2. package/dist/cli/commands/watch.d.ts +0 -1
  3. package/dist/cli/commands/watch.d.ts.map +1 -1
  4. package/dist/cli/index.js +707 -5200
  5. package/dist/db/database.d.ts +1 -41
  6. package/dist/db/database.d.ts.map +1 -1
  7. package/dist/db/pg-migrations.d.ts.map +1 -1
  8. package/dist/index.d.ts +0 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +135 -1202
  11. package/dist/ingest/claude.d.ts +2 -13
  12. package/dist/ingest/claude.d.ts.map +1 -1
  13. package/dist/ingest/codex.d.ts +1 -2
  14. package/dist/ingest/codex.d.ts.map +1 -1
  15. package/dist/ingest/gemini.d.ts +1 -2
  16. package/dist/ingest/gemini.d.ts.map +1 -1
  17. package/dist/lib/config.d.ts.map +1 -1
  18. package/dist/lib/gatherer.d.ts.map +1 -1
  19. package/dist/lib/model-config.d.ts.map +1 -1
  20. package/dist/lib/pricing.d.ts +3 -3
  21. package/dist/lib/pricing.d.ts.map +1 -1
  22. package/dist/lib/webhooks.d.ts +1 -1
  23. package/dist/lib/webhooks.d.ts.map +1 -1
  24. package/dist/mcp/index.js +487 -2749
  25. package/dist/server/index.d.ts +0 -1
  26. package/dist/server/index.js +196 -3090
  27. package/dist/server/serve.d.ts +2 -10
  28. package/dist/server/serve.d.ts.map +1 -1
  29. package/dist/types/index.d.ts +6 -59
  30. package/dist/types/index.d.ts.map +1 -1
  31. package/package.json +1 -1
  32. package/dist/cli/commands/completion.d.ts +0 -2
  33. package/dist/cli/commands/completion.d.ts.map +0 -1
  34. package/dist/cli/commands/extras.d.ts +0 -4
  35. package/dist/cli/commands/extras.d.ts.map +0 -1
  36. package/dist/cli/commands/notification.d.ts +0 -8
  37. package/dist/cli/commands/notification.d.ts.map +0 -1
  38. package/dist/cli/commands/todos.d.ts +0 -26
  39. package/dist/cli/commands/todos.d.ts.map +0 -1
  40. package/dist/cli/commands/tui.d.ts +0 -10
  41. package/dist/cli/commands/tui.d.ts.map +0 -1
  42. package/dist/ingest/billing.d.ts +0 -27
  43. package/dist/ingest/billing.d.ts.map +0 -1
  44. package/dist/ingest/claude-quota.d.ts +0 -5
  45. package/dist/ingest/claude-quota.d.ts.map +0 -1
  46. package/dist/ingest/codex-quota.d.ts +0 -5
  47. package/dist/ingest/codex-quota.d.ts.map +0 -1
  48. package/dist/ingest/cursor.d.ts +0 -6
  49. package/dist/ingest/cursor.d.ts.map +0 -1
  50. package/dist/ingest/hermes.d.ts +0 -6
  51. package/dist/ingest/hermes.d.ts.map +0 -1
  52. package/dist/ingest/opencode.d.ts +0 -7
  53. package/dist/ingest/opencode.d.ts.map +0 -1
  54. package/dist/ingest/otel.d.ts +0 -20
  55. package/dist/ingest/otel.d.ts.map +0 -1
  56. package/dist/ingest/pi.d.ts +0 -7
  57. package/dist/ingest/pi.d.ts.map +0 -1
  58. package/dist/ingest/plugin.d.ts +0 -17
  59. package/dist/ingest/plugin.d.ts.map +0 -1
  60. package/dist/lib/agents.d.ts +0 -11
  61. package/dist/lib/agents.d.ts.map +0 -1
  62. package/dist/lib/billing-diff.d.ts +0 -22
  63. package/dist/lib/billing-diff.d.ts.map +0 -1
  64. package/dist/lib/cloud-sync.d.ts +0 -35
  65. package/dist/lib/cloud-sync.d.ts.map +0 -1
  66. package/dist/lib/open-projects.d.ts +0 -19
  67. package/dist/lib/open-projects.d.ts.map +0 -1
  68. package/dist/lib/package-metadata.d.ts +0 -8
  69. package/dist/lib/package-metadata.d.ts.map +0 -1
  70. package/dist/lib/paths.d.ts +0 -20
  71. package/dist/lib/paths.d.ts.map +0 -1
  72. package/dist/lib/savings.d.ts +0 -17
  73. package/dist/lib/savings.d.ts.map +0 -1
  74. package/dist/lib/serve-auth.d.ts +0 -4
  75. package/dist/lib/serve-auth.d.ts.map +0 -1
  76. package/dist/lib/spikes.d.ts +0 -18
  77. package/dist/lib/spikes.d.ts.map +0 -1
  78. package/dist/lib/sync-all.d.ts +0 -28
  79. package/dist/lib/sync-all.d.ts.map +0 -1
  80. package/dist/lib/watch-paths.d.ts +0 -3
  81. package/dist/lib/watch-paths.d.ts.map +0 -1
  82. package/dist/mcp/http.d.ts +0 -12
  83. package/dist/mcp/http.d.ts.map +0 -1
  84. package/dist/mcp/server.d.ts +0 -4
  85. package/dist/mcp/server.d.ts.map +0 -1
  86. package/dist/otel/index.d.ts +0 -3
  87. package/dist/otel/index.d.ts.map +0 -1
  88. package/dist/otel/index.js +0 -1372
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env bun
2
1
  // @bun
3
2
  var __defProp = Object.defineProperty;
4
3
  var __returnValue = (v) => v;
@@ -29,503 +28,88 @@ __export(exports_pricing, {
29
28
  DEFAULT_PRICING: () => DEFAULT_PRICING
30
29
  });
31
30
  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;
31
+ return raw.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "").toLowerCase();
68
32
  }
69
33
  function ensurePricingSeeded(db) {
70
34
  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
35
  }
170
36
  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);
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
+ };
177
46
  }
178
47
  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
- };
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;
195
54
  }
196
55
  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");
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;
203
64
  }
204
- function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
65
+ function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
205
66
  const pricing = getPricing(model);
206
67
  if (!pricing)
207
68
  return 0;
208
- return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
69
+ return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
209
70
  }
210
- function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
71
+ function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
211
72
  const pricing = getPricingFromDb(db, model) ?? getPricing(model);
212
73
  if (!pricing)
213
74
  return 0;
214
- return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
75
+ return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
215
76
  }
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;
77
+ var DEFAULT_PRICING;
236
78
  var init_pricing = __esm(() => {
237
79
  init_database();
238
80
  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 = {
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 },
325
87
  "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 },
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 },
329
91
  "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 },
330
95
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
331
96
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
332
97
  "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 },
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 },
335
101
  "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
- }
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 }
512
105
  };
513
106
  });
514
107
 
515
108
  // src/db/database.ts
516
109
  import { SqliteAdapter as Database } from "@hasna/cloud";
517
110
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
518
- import { hostname } from "os";
519
111
  import { homedir } from "os";
520
112
  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
113
  function getDataDir() {
530
114
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
531
115
  const newDir = join(home, ".hasna", "economy");
@@ -558,7 +142,6 @@ function openDatabase(dbPath, skipSeed = false) {
558
142
  }
559
143
  const db = new Database(path);
560
144
  db.exec("PRAGMA journal_mode = WAL");
561
- db.exec("PRAGMA busy_timeout = 5000");
562
145
  db.exec("PRAGMA foreign_keys = ON");
563
146
  initSchema(db);
564
147
  if (!skipSeed) {
@@ -577,13 +160,10 @@ function initSchema(db) {
577
160
  output_tokens INTEGER DEFAULT 0,
578
161
  cache_read_tokens INTEGER DEFAULT 0,
579
162
  cache_create_tokens INTEGER DEFAULT 0,
580
- cache_create_5m_tokens INTEGER DEFAULT 0,
581
- cache_create_1h_tokens INTEGER DEFAULT 0,
582
163
  cost_usd REAL NOT NULL DEFAULT 0,
583
164
  duration_ms INTEGER DEFAULT 0,
584
165
  timestamp TEXT NOT NULL,
585
- source_request_id TEXT,
586
- machine_id TEXT DEFAULT ''
166
+ source_request_id TEXT
587
167
  );
588
168
 
589
169
  CREATE TABLE IF NOT EXISTS sessions (
@@ -595,8 +175,7 @@ function initSchema(db) {
595
175
  ended_at TEXT,
596
176
  total_cost_usd REAL DEFAULT 0,
597
177
  total_tokens INTEGER DEFAULT 0,
598
- request_count INTEGER DEFAULT 0,
599
- machine_id TEXT DEFAULT ''
178
+ request_count INTEGER DEFAULT 0
600
179
  );
601
180
 
602
181
  CREATE TABLE IF NOT EXISTS projects (
@@ -649,8 +228,6 @@ function initSchema(db) {
649
228
  output_per_1m REAL NOT NULL DEFAULT 0,
650
229
  cache_read_per_1m REAL NOT NULL DEFAULT 0,
651
230
  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,
654
231
  updated_at TEXT NOT NULL
655
232
  );
656
233
 
@@ -663,115 +240,6 @@ function initSchema(db) {
663
240
  machine_id TEXT,
664
241
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
665
242
  );
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);
775
243
  `);
776
244
  }
777
245
  function periodWhere(period) {
@@ -781,11 +249,11 @@ function periodWhere(period) {
781
249
  case "yesterday":
782
250
  return `DATE(timestamp) = DATE('now', '-1 day')`;
783
251
  case "week":
784
- return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
252
+ return `timestamp >= DATE('now', '-7 days')`;
785
253
  case "month":
786
- return `timestamp >= DATE('now', 'start of month')`;
254
+ return `timestamp >= DATE('now', '-30 days')`;
787
255
  case "year":
788
- return `timestamp >= DATE('now', 'start of year')`;
256
+ return `timestamp >= DATE('now', '-365 days')`;
789
257
  case "all":
790
258
  return "1=1";
791
259
  }
@@ -797,34 +265,31 @@ function sessionPeriodWhere(period) {
797
265
  case "yesterday":
798
266
  return `DATE(started_at) = DATE('now', '-1 day')`;
799
267
  case "week":
800
- return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
268
+ return `started_at >= DATE('now', '-7 days')`;
801
269
  case "month":
802
- return `started_at >= DATE('now', 'start of month')`;
270
+ return `started_at >= DATE('now', '-30 days')`;
803
271
  case "year":
804
- return `started_at >= DATE('now', 'start of year')`;
272
+ return `started_at >= DATE('now', '-365 days')`;
805
273
  case "all":
806
274
  return "1=1";
807
275
  }
808
276
  }
809
277
  function upsertRequest(db, req) {
810
- const now = req.updated_at ?? new Date().toISOString();
811
278
  db.prepare(`
812
279
  INSERT OR REPLACE INTO requests
813
280
  (id, agent, session_id, model, input_tokens, output_tokens,
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 ?? "");
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);
819
285
  }
820
286
  function upsertSession(db, session) {
821
- const now = session.updated_at ?? new Date().toISOString();
822
287
  db.prepare(`
823
288
  INSERT OR REPLACE INTO sessions
824
289
  (id, agent, project_path, project_name, started_at, ended_at,
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 ?? "");
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);
828
293
  }
829
294
  function rollupSession(db, sessionId) {
830
295
  db.prepare(`
@@ -854,10 +319,6 @@ function querySessions(db, filter = {}) {
854
319
  conditions.push("started_at >= ?");
855
320
  params.push(filter.since);
856
321
  }
857
- if (filter.machine) {
858
- conditions.push("machine_id = ?");
859
- params.push(filter.machine);
860
- }
861
322
  if (filter.search) {
862
323
  const q = `%${filter.search}%`;
863
324
  conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
@@ -876,25 +337,24 @@ function queryTopSessions(db, n = 10, agent) {
876
337
  }
877
338
  return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
878
339
  }
879
- function querySummary(db, period, machine, allMachines = false) {
340
+ function querySummary(db, period) {
880
341
  const rWhere = periodWhere(period);
881
342
  const sWhere = sessionPeriodWhere(period);
882
- const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
883
343
  const r = db.prepare(`
884
344
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
885
345
  COUNT(*) as requests,
886
346
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
887
- FROM requests WHERE ${rWhere}${machineClause}
347
+ FROM requests WHERE ${rWhere}
888
348
  `).get();
889
349
  const codexTotals = db.prepare(`
890
350
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
891
351
  COALESCE(SUM(total_tokens), 0) as tokens,
892
352
  COUNT(*) as sessions
893
353
  FROM sessions
894
- WHERE ${sWhere}${machineClause}
354
+ WHERE ${sWhere}
895
355
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
896
356
  `).get();
897
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
357
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
898
358
  return {
899
359
  total_usd: r.total_usd + codexTotals.cost_usd,
900
360
  requests: r.requests,
@@ -914,66 +374,23 @@ function queryModelBreakdown(db) {
914
374
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
915
375
  `).all();
916
376
  }
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
- }
935
377
  function queryProjectBreakdown(db) {
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 != ''
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
940
393
  `).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;
977
394
  }
978
395
  function queryDailyBreakdown(db, days = 30) {
979
396
  return db.prepare(`
@@ -1082,46 +499,12 @@ function getIngestState(db, source, key) {
1082
499
  function setIngestState(db, source, key, value) {
1083
500
  db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
1084
501
  }
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
- }
1119
502
  function upsertModelPricing(db, p) {
1120
503
  db.prepare(`
1121
504
  INSERT OR REPLACE INTO model_pricing
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);
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);
1125
508
  }
1126
509
  function getModelPricing(db, model) {
1127
510
  return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
@@ -1133,655 +516,25 @@ function deleteModelPricing(db, model) {
1133
516
  db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
1134
517
  }
1135
518
  function seedModelPricing(db, defaults) {
1136
- const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
519
+ const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
520
+ if (existing.count > 0)
521
+ return;
1137
522
  const now = new Date().toISOString();
1138
523
  for (const [model, p] of Object.entries(defaults)) {
1139
- if (existing.has(model))
1140
- continue;
1141
524
  upsertModelPricing(db, {
1142
525
  model,
1143
526
  input_per_1m: p.inputPer1M,
1144
527
  output_per_1m: p.outputPer1M,
1145
528
  cache_read_per_1m: p.cacheReadPer1M,
1146
529
  cache_write_per_1m: p.cacheWritePer1M,
1147
- cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
1148
- cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
1149
530
  updated_at: now
1150
531
  });
1151
532
  }
1152
533
  }
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
- }
1206
534
  var init_database = () => {};
1207
535
 
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
-
1767
536
  // src/server/serve.ts
1768
537
  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
- }
1785
538
 
1786
539
  // src/ingest/claude.ts
1787
540
  init_database();
@@ -1789,114 +542,10 @@ init_pricing();
1789
542
  import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
1790
543
  import { homedir as homedir2 } from "os";
1791
544
  import { join as join2, basename } from "path";
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
- }
545
+ function autoDetectProject(cwd, projects) {
546
+ return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
1809
547
  }
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");
548
+ var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
1900
549
  function dirNameToPath(dirName) {
1901
550
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
1902
551
  }
@@ -1915,37 +564,30 @@ function collectJsonlFiles(projectDir) {
1915
564
  walk(projectDir);
1916
565
  return files;
1917
566
  }
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)) {
567
+ async function ingestClaude(db, verbose = false, _telemetryDir) {
568
+ if (!existsSync2(PROJECTS_DIR)) {
1926
569
  if (verbose)
1927
- console.log(`${agentName} projects dir not found:`, projectsDir);
570
+ console.log("Claude projects dir not found:", PROJECTS_DIR);
1928
571
  return { files: 0, requests: 0, sessions: 0 };
1929
572
  }
1930
- const machineId = getMachineId();
1931
573
  let totalFiles = 0;
1932
574
  let totalRequests = 0;
1933
575
  const touchedSessions = new Set;
1934
576
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
1935
- const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
577
+ const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
1936
578
  for (const projectDirEntry of projectDirs) {
1937
- const projectDirPath = join2(projectsDir, projectDirEntry.name);
579
+ const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
1938
580
  const projectPath = dirNameToPath(projectDirEntry.name);
1939
581
  const jsonlFiles = collectJsonlFiles(projectDirPath);
1940
582
  for (const filePath of jsonlFiles) {
1941
- const stateKey = filePath.replace(projectsDir, "");
583
+ const stateKey = filePath.replace(PROJECTS_DIR, "");
1942
584
  let fileMtime = "0";
1943
585
  try {
1944
586
  fileMtime = statSync2(filePath).mtimeMs.toString();
1945
587
  } catch {
1946
588
  continue;
1947
589
  }
1948
- const processed = getIngestState(db, agentName, stateKey);
590
+ const processed = getIngestState(db, "claude", stateKey);
1949
591
  if (processed === fileMtime)
1950
592
  continue;
1951
593
  let lines;
@@ -1980,38 +622,26 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1980
622
  continue;
1981
623
  const inputTokens = usage.input_tokens ?? 0;
1982
624
  const outputTokens = usage.output_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;
625
+ const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
1986
626
  const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
1987
627
  const timestamp = entry.timestamp ?? new Date().toISOString();
1988
- if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
628
+ if (inputTokens + outputTokens + cacheWriteTokens === 0)
1989
629
  continue;
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}`;
630
+ const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
631
+ const reqId = `claude-${sessionId}-${timestamp}`;
1998
632
  upsertRequest(db, {
1999
633
  id: reqId,
2000
- agent: agentName,
634
+ agent: "claude",
2001
635
  session_id: sessionId,
2002
636
  model,
2003
637
  input_tokens: inputTokens,
2004
638
  output_tokens: outputTokens,
2005
639
  cache_read_tokens: cacheReadTokens,
2006
640
  cache_create_tokens: cacheWriteTokens,
2007
- cache_create_5m_tokens: cacheWrite5mTokens,
2008
- cache_create_1h_tokens: cacheWrite1hTokens,
2009
641
  cost_usd: costUsd,
2010
- cost_basis: defaultCostBasisForAgent(agentName),
2011
642
  duration_ms: 0,
2012
643
  timestamp,
2013
- source_request_id: sourceRequestId,
2014
- machine_id: machineId
644
+ source_request_id: reqId
2015
645
  });
2016
646
  if (!touchedSessions.has(sessionId)) {
2017
647
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
@@ -2020,15 +650,14 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2020
650
  const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
2021
651
  const session = {
2022
652
  id: sessionId,
2023
- agent: agentName,
653
+ agent: "claude",
2024
654
  project_path: detectedProject ? detectedProject.path : effectiveCwd,
2025
655
  project_name: detectedProject ? detectedProject.name : "",
2026
656
  started_at: timestamp,
2027
657
  ended_at: null,
2028
658
  total_cost_usd: 0,
2029
659
  total_tokens: 0,
2030
- request_count: 0,
2031
- machine_id: machineId
660
+ request_count: 0
2032
661
  };
2033
662
  upsertSession(db, session);
2034
663
  }
@@ -2036,7 +665,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2036
665
  }
2037
666
  totalRequests++;
2038
667
  }
2039
- setIngestState(db, agentName, stateKey, fileMtime);
668
+ setIngestState(db, "claude", stateKey, fileMtime);
2040
669
  totalFiles++;
2041
670
  }
2042
671
  }
@@ -2045,1369 +674,66 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2045
674
  }
2046
675
  return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
2047
676
  }
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
- }
2069
677
 
2070
678
  // src/ingest/codex.ts
2071
679
  init_database();
2072
- init_pricing();
2073
680
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2074
681
  import { homedir as homedir3 } from "os";
2075
682
  import { join as join3, basename as basename2 } from "path";
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
- }
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");
2156
686
  async function ingestCodex(db, verbose = false) {
2157
- const dbPath = codexDbPath();
2158
- if (!existsSync3(dbPath)) {
687
+ if (!existsSync3(CODEX_DB_PATH)) {
2159
688
  if (verbose)
2160
- console.log("Codex DB not found:", dbPath);
2161
- return { sessions: 0, requests: 0 };
689
+ console.log("Codex DB not found:", CODEX_DB_PATH);
690
+ return { sessions: 0 };
2162
691
  }
2163
- const machineId = getMachineId();
2164
692
  let codexDb = null;
2165
693
  let ingested = 0;
2166
- let requests = 0;
2167
694
  try {
2168
- codexDb = new BunDatabase(dbPath, { readonly: true });
2169
- const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
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();
2170
697
  for (const thread of threads) {
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)
698
+ const stateKey = thread.id;
699
+ const processed = getIngestState(db, "codex", stateKey);
700
+ if (processed === "done")
2175
701
  continue;
702
+ const costUsd = 0;
2176
703
  const projectPath = thread.cwd ?? "";
2177
704
  const projectName = projectPath ? basename2(projectPath) : "unknown";
2178
- const sessionId = `codex-${thread.id}`;
2179
705
  const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
2180
706
  const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
2181
707
  upsertSession(db, {
2182
- id: sessionId,
708
+ id: `codex-${thread.id}`,
2183
709
  agent: "codex",
2184
710
  project_path: projectPath,
2185
711
  project_name: projectName,
2186
712
  started_at: startedAt,
2187
713
  ended_at: endedAt,
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++;
714
+ total_cost_usd: costUsd,
715
+ total_tokens: thread.tokens_used,
716
+ request_count: 1
2222
717
  });
2223
- rollupSession(db, sessionId);
2224
- setIngestState(db, "codex", thread.id, stateValue);
718
+ setIngestState(db, "codex", stateKey, "done");
2225
719
  ingested++;
2226
720
  if (verbose)
2227
- console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens on ${model}`);
721
+ console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens \u2192 $${costUsd.toFixed(4)}`);
2228
722
  }
2229
723
  } finally {
2230
724
  codexDb?.close();
2231
725
  }
2232
- return { sessions: ingested, requests };
2233
- }
2234
-
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();
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;
726
+ return { sessions: ingested };
3397
727
  }
3398
728
 
3399
729
  // src/server/serve.ts
730
+ init_pricing();
3400
731
  import { randomUUID } from "crypto";
3401
- import { existsSync as existsSync11 } from "fs";
3402
- import { resolve, sep } from "path";
3403
732
  var CORS = {
3404
733
  "Access-Control-Allow-Origin": "*",
3405
734
  "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
3406
- "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Economy-Token"
735
+ "Access-Control-Allow-Headers": "Content-Type"
3407
736
  };
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;
3411
737
  function json(data, status = 200) {
3412
738
  return new Response(JSON.stringify(data), {
3413
739
  status,
@@ -3420,70 +746,6 @@ function ok(data, meta) {
3420
746
  function err(message, status = 400) {
3421
747
  return json({ error: message }, status);
3422
748
  }
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
- }
3487
749
  function applyFields(obj, fields) {
3488
750
  if (!fields || fields.length === 0)
3489
751
  return obj;
@@ -3496,26 +758,11 @@ function createHandler(db) {
3496
758
  const method = req.method;
3497
759
  if (method === "OPTIONS")
3498
760
  return new Response(null, { status: 204, headers: CORS });
3499
- if (!isAuthorizedRequest(req, path))
3500
- return err("Unauthorized", 401);
3501
761
  if (path === "/health")
3502
762
  return ok({ status: "ok", ts: new Date().toISOString() });
3503
763
  if (path === "/api/summary" && method === "GET") {
3504
764
  const period = url.searchParams.get("period") ?? "today";
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
- });
765
+ return ok(querySummary(db, period));
3519
766
  }
3520
767
  if (path === "/api/daily" && method === "GET") {
3521
768
  const days = Number(url.searchParams.get("days") ?? 30);
@@ -3524,22 +771,12 @@ function createHandler(db) {
3524
771
  if (path === "/api/sessions" && method === "GET") {
3525
772
  const agent = url.searchParams.get("agent");
3526
773
  const project = url.searchParams.get("project") ?? undefined;
3527
- const search = url.searchParams.get("search") ?? undefined;
3528
- const machine = url.searchParams.get("machine") ?? undefined;
3529
774
  const limit = Number(url.searchParams.get("limit") ?? 50);
3530
775
  const offset = Number(url.searchParams.get("offset") ?? 0);
3531
776
  const since = url.searchParams.get("since") ?? undefined;
3532
777
  const fieldsParam = url.searchParams.get("fields");
3533
778
  const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
3534
- const sessions = querySessions(db, {
3535
- agent: agent ?? undefined,
3536
- project,
3537
- search,
3538
- machine,
3539
- limit,
3540
- offset,
3541
- since
3542
- });
779
+ const sessions = querySessions(db, { agent: agent ?? undefined, project, limit, offset, since });
3543
780
  return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
3544
781
  }
3545
782
  if (path === "/api/top" && method === "GET") {
@@ -3550,41 +787,6 @@ function createHandler(db) {
3550
787
  if (path === "/api/models" && method === "GET") {
3551
788
  return ok(queryModelBreakdown(db));
3552
789
  }
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
- }
3588
790
  if (path === "/api/projects" && method === "GET") {
3589
791
  return ok(queryProjectBreakdown(db));
3590
792
  }
@@ -3596,54 +798,38 @@ function createHandler(db) {
3596
798
  return ok(getBudgetStatuses(db));
3597
799
  }
3598
800
  if (path === "/api/budgets" && method === "POST") {
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);
801
+ const body = await req.json();
3611
802
  const now = new Date().toISOString();
3612
- const budget = {
803
+ upsertBudget(db, {
3613
804
  id: randomUUID(),
3614
805
  project_path: body["project_path"] ?? null,
3615
- agent,
3616
- period: normalizeBudgetPeriod(body["period"]),
3617
- limit_usd: limitUsd,
3618
- alert_at_percent: alertAtPercent,
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),
3619
810
  created_at: now,
3620
811
  updated_at: now
3621
- };
3622
- upsertBudget(db, budget);
3623
- return ok(getBudgetStatuses(db).find((b) => b.id === budget.id) ?? budget);
812
+ });
813
+ return ok({ ok: true });
3624
814
  }
3625
815
  const budgetMatch = path.match(/^\/api\/budgets\/(.+)$/);
3626
816
  if (budgetMatch && method === "DELETE") {
3627
- deleteBudget(db, decodeURIComponent(budgetMatch[1]));
817
+ deleteBudget(db, budgetMatch[1]);
3628
818
  return ok({ ok: true });
3629
819
  }
3630
820
  if (path === "/api/project-registry" && method === "GET") {
3631
821
  return ok(listProjects(db));
3632
822
  }
3633
823
  if (path === "/api/project-registry" && method === "POST") {
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");
824
+ const body = await req.json();
825
+ const { basename: basename3 } = await import("path");
826
+ const projPath = body["path"];
3641
827
  upsertProject(db, {
3642
828
  id: randomUUID(),
3643
829
  path: projPath,
3644
- name: optionalString(body["name"]) ?? basename4(projPath),
3645
- description: optionalString(body["description"]),
3646
- tags: stringArray(body["tags"]),
830
+ name: body["name"] ?? basename3(projPath),
831
+ description: body["description"] ?? null,
832
+ tags: body["tags"] ?? [],
3647
833
  created_at: new Date().toISOString()
3648
834
  });
3649
835
  return ok({ ok: true });
@@ -3657,33 +843,16 @@ function createHandler(db) {
3657
843
  return ok(listModelPricing(db));
3658
844
  }
3659
845
  if (path === "/api/pricing" && method === "POST") {
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,
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),
3683
853
  updated_at: new Date().toISOString()
3684
- };
3685
- upsertModelPricing(db, pricing);
3686
- return ok(pricing);
854
+ });
855
+ return ok({ ok: true });
3687
856
  }
3688
857
  const pricingMatch = path.match(/^\/api\/pricing\/(.+)$/);
3689
858
  if (pricingMatch && method === "DELETE") {
@@ -3691,43 +860,18 @@ function createHandler(db) {
3691
860
  return ok({ ok: true });
3692
861
  }
3693
862
  if (path === "/api/sync" && method === "POST") {
3694
- const body = await jsonBody(req) ?? {};
863
+ const body = await req.json().catch(() => ({}));
3695
864
  const sources = body["sources"] ?? "all";
3696
- if (!SYNC_SOURCES.includes(sources))
3697
- return err("invalid sync source");
3698
865
  const results = {};
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 {}
866
+ if (sources === "all" || sources === "claude")
867
+ results["claude"] = await ingestClaude(db);
868
+ if (sources === "all" || sources === "codex")
869
+ results["codex"] = await ingestCodex(db);
3712
870
  return ok(results);
3713
871
  }
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
- }
3728
872
  const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
3729
873
  if (sessionRequestsMatch && method === "GET") {
3730
- const sessionId = decodeURIComponent(sessionRequestsMatch[1]);
874
+ const sessionId = sessionRequestsMatch[1];
3731
875
  const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(sessionId, `${sessionId}%`);
3732
876
  if (!session)
3733
877
  return err("Session not found", 404);
@@ -3738,97 +882,59 @@ function createHandler(db) {
3738
882
  return ok(getGoalStatuses(db));
3739
883
  }
3740
884
  if (path === "/api/goals" && method === "POST") {
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);
885
+ const body = await req.json();
3753
886
  const now = new Date().toISOString();
3754
- const goal = {
887
+ upsertGoal(db, {
3755
888
  id: randomUUID(),
3756
- period,
3757
- project_path: optionalString(body["project_path"]),
3758
- agent,
3759
- limit_usd: limitUsd,
889
+ period: body["period"] ?? "month",
890
+ project_path: body["project_path"] ?? null,
891
+ agent: body["agent"] ?? null,
892
+ limit_usd: Number(body["limit_usd"]),
3760
893
  created_at: now,
3761
894
  updated_at: now
3762
- };
3763
- upsertGoal(db, goal);
3764
- return ok(getGoalStatuses(db).find((g) => g.id === goal.id) ?? goal);
895
+ });
896
+ return ok({ ok: true });
3765
897
  }
3766
898
  const goalMatch = path.match(/^\/api\/goals\/(.+)$/);
3767
899
  if (goalMatch && method === "DELETE") {
3768
- deleteGoal(db, decodeURIComponent(goalMatch[1]));
900
+ deleteGoal(db, goalMatch[1]);
3769
901
  return ok({ ok: true });
3770
902
  }
3771
903
  return err("Not found", 404);
3772
904
  };
3773
905
  }
3774
- function startServer(port = 3456, options = {}) {
3775
- const db = options.db ?? openDatabase();
906
+ function startServer(port = 3456) {
907
+ const db = openDatabase();
3776
908
  ensurePricingSeeded(db);
3777
909
  const apiHandler = createHandler(db);
3778
- const hostname2 = options.hostname ?? getServeBindHost();
3779
- const server = Bun.serve({
910
+ const dashboardDir = new URL("../../dashboard/dist", import.meta.url).pathname;
911
+ Bun.serve({
3780
912
  port,
3781
- hostname: hostname2,
3782
- fetch: createServerFetch(apiHandler, options.dashboardDir)
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
+ }
3783
934
  });
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;
935
+ console.log(`economy-serve listening on http://localhost:${port}`);
3788
936
  }
3789
937
 
3790
938
  // src/server/index.ts
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
- }
939
+ var port = Number(process.env["ECONOMY_PORT"] ?? 3456);
940
+ startServer(port);