@hasna/economy 0.2.18 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +2 -1
  2. package/README.md +13 -0
  3. package/dist/cli/commands/completion.d.ts +2 -0
  4. package/dist/cli/commands/completion.d.ts.map +1 -0
  5. package/dist/cli/commands/extras.d.ts +4 -0
  6. package/dist/cli/commands/extras.d.ts.map +1 -0
  7. package/dist/cli/commands/menubar.d.ts.map +1 -1
  8. package/dist/cli/commands/notification.d.ts +8 -0
  9. package/dist/cli/commands/notification.d.ts.map +1 -0
  10. package/dist/cli/commands/todos.d.ts +26 -0
  11. package/dist/cli/commands/todos.d.ts.map +1 -0
  12. package/dist/cli/commands/tui.d.ts +10 -0
  13. package/dist/cli/commands/tui.d.ts.map +1 -0
  14. package/dist/cli/commands/watch.d.ts +1 -0
  15. package/dist/cli/commands/watch.d.ts.map +1 -1
  16. package/dist/cli/index.js +5134 -641
  17. package/dist/db/database.d.ts +41 -1
  18. package/dist/db/database.d.ts.map +1 -1
  19. package/dist/db/pg-migrations.d.ts.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1202 -135
  23. package/dist/ingest/billing.d.ts +27 -0
  24. package/dist/ingest/billing.d.ts.map +1 -0
  25. package/dist/ingest/claude-quota.d.ts +5 -0
  26. package/dist/ingest/claude-quota.d.ts.map +1 -0
  27. package/dist/ingest/claude.d.ts +13 -2
  28. package/dist/ingest/claude.d.ts.map +1 -1
  29. package/dist/ingest/codex-quota.d.ts +5 -0
  30. package/dist/ingest/codex-quota.d.ts.map +1 -0
  31. package/dist/ingest/codex.d.ts +2 -1
  32. package/dist/ingest/codex.d.ts.map +1 -1
  33. package/dist/ingest/cursor.d.ts +6 -0
  34. package/dist/ingest/cursor.d.ts.map +1 -0
  35. package/dist/ingest/gemini.d.ts +2 -1
  36. package/dist/ingest/gemini.d.ts.map +1 -1
  37. package/dist/ingest/hermes.d.ts +6 -0
  38. package/dist/ingest/hermes.d.ts.map +1 -0
  39. package/dist/ingest/opencode.d.ts +7 -0
  40. package/dist/ingest/opencode.d.ts.map +1 -0
  41. package/dist/ingest/otel.d.ts +20 -0
  42. package/dist/ingest/otel.d.ts.map +1 -0
  43. package/dist/ingest/pi.d.ts +7 -0
  44. package/dist/ingest/pi.d.ts.map +1 -0
  45. package/dist/ingest/plugin.d.ts +17 -0
  46. package/dist/ingest/plugin.d.ts.map +1 -0
  47. package/dist/lib/agents.d.ts +11 -0
  48. package/dist/lib/agents.d.ts.map +1 -0
  49. package/dist/lib/billing-diff.d.ts +22 -0
  50. package/dist/lib/billing-diff.d.ts.map +1 -0
  51. package/dist/lib/cloud-sync.d.ts +35 -0
  52. package/dist/lib/cloud-sync.d.ts.map +1 -0
  53. package/dist/lib/config.d.ts.map +1 -1
  54. package/dist/lib/gatherer.d.ts.map +1 -1
  55. package/dist/lib/model-config.d.ts.map +1 -1
  56. package/dist/lib/open-projects.d.ts +19 -0
  57. package/dist/lib/open-projects.d.ts.map +1 -0
  58. package/dist/lib/package-metadata.d.ts +8 -0
  59. package/dist/lib/package-metadata.d.ts.map +1 -0
  60. package/dist/lib/paths.d.ts +20 -0
  61. package/dist/lib/paths.d.ts.map +1 -0
  62. package/dist/lib/pricing.d.ts +3 -3
  63. package/dist/lib/pricing.d.ts.map +1 -1
  64. package/dist/lib/savings.d.ts +17 -0
  65. package/dist/lib/savings.d.ts.map +1 -0
  66. package/dist/lib/serve-auth.d.ts +4 -0
  67. package/dist/lib/serve-auth.d.ts.map +1 -0
  68. package/dist/lib/spikes.d.ts +18 -0
  69. package/dist/lib/spikes.d.ts.map +1 -0
  70. package/dist/lib/sync-all.d.ts +28 -0
  71. package/dist/lib/sync-all.d.ts.map +1 -0
  72. package/dist/lib/watch-paths.d.ts +3 -0
  73. package/dist/lib/watch-paths.d.ts.map +1 -0
  74. package/dist/lib/webhooks.d.ts +1 -1
  75. package/dist/lib/webhooks.d.ts.map +1 -1
  76. package/dist/mcp/http.d.ts +12 -0
  77. package/dist/mcp/http.d.ts.map +1 -0
  78. package/dist/mcp/index.js +2752 -490
  79. package/dist/mcp/server.d.ts +4 -0
  80. package/dist/mcp/server.d.ts.map +1 -0
  81. package/dist/otel/index.d.ts +3 -0
  82. package/dist/otel/index.d.ts.map +1 -0
  83. package/dist/otel/index.js +1372 -0
  84. package/dist/server/index.d.ts +1 -0
  85. package/dist/server/index.js +3095 -201
  86. package/dist/server/serve.d.ts +10 -2
  87. package/dist/server/serve.d.ts.map +1 -1
  88. package/dist/types/index.d.ts +59 -6
  89. package/dist/types/index.d.ts.map +1 -1
  90. package/package.json +1 -1
  91. package/dashboard/dist/assets/index-5mUN0CPj.css +0 -1
  92. package/dashboard/dist/assets/index-L1FgNQ4t.js +0 -93
  93. package/dashboard/dist/index.html +0 -14
  94. package/dashboard/dist/logo.jpg +0 -0
  95. package/dashboard/dist/vite.svg +0 -1
package/dist/mcp/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
  var __defProp = Object.defineProperty;
4
- var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
5
4
  var __returnValue = (v) => v;
6
5
  function __exportSetter(name, newValue) {
7
6
  this[name] = __returnValue.bind(null, newValue);
@@ -30,88 +29,503 @@ __export(exports_pricing, {
30
29
  DEFAULT_PRICING: () => DEFAULT_PRICING
31
30
  });
32
31
  function normalizeModelName(raw) {
33
- return raw.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "").toLowerCase();
32
+ return raw.trim().toLowerCase().replace(/^models\//, "").replace(/^[a-z0-9_.-]+\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
33
+ }
34
+ function normalizeModelNamePreservingProvider(raw) {
35
+ return raw.trim().toLowerCase().replace(/^models\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
36
+ }
37
+ function modelLookupKeys(raw) {
38
+ const withProvider = normalizeModelNamePreservingProvider(raw);
39
+ const withoutProvider = normalizeModelName(raw);
40
+ return withProvider === withoutProvider ? [withoutProvider] : [withProvider, withoutProvider];
41
+ }
42
+ function bestPrefixMatch(normalized, entries) {
43
+ let best = null;
44
+ for (const entry of entries) {
45
+ const [key] = entry;
46
+ if (normalized !== key && !normalized.startsWith(`${key}-`))
47
+ continue;
48
+ if (!best || key.length > best[0].length)
49
+ best = entry;
50
+ }
51
+ return best?.[1] ?? null;
52
+ }
53
+ function bestModelMatch(model, entries) {
54
+ for (const key of modelLookupKeys(model)) {
55
+ const match = bestPrefixMatch(key, entries);
56
+ if (match)
57
+ return match;
58
+ }
59
+ return null;
60
+ }
61
+ function exactModelMatch(model, entries) {
62
+ for (const key of modelLookupKeys(model)) {
63
+ const match = entries.find(([entryKey]) => entryKey === key);
64
+ if (match)
65
+ return match[1];
66
+ }
67
+ return null;
34
68
  }
35
69
  function ensurePricingSeeded(db) {
36
70
  seedModelPricing(db, DEFAULT_PRICING);
71
+ repairLegacySeededPricing(db);
72
+ repairMissingDefaultCacheWrite1h(db);
73
+ repairMissingDefaultCacheStorage(db);
74
+ removeDeprecatedDefaultPricing(db);
75
+ }
76
+ function repairLegacySeededPricing(db) {
77
+ const now = new Date().toISOString();
78
+ const legacyModels = new Set([
79
+ ...Object.keys(LEGACY_DEFAULT_PRICING),
80
+ ...Object.keys(ADDITIONAL_LEGACY_DEFAULT_PRICING)
81
+ ]);
82
+ for (const model of legacyModels) {
83
+ const current = getModelPricing(db, model);
84
+ const next = DEFAULT_PRICING[model];
85
+ if (!current || !next)
86
+ continue;
87
+ const legacy = LEGACY_DEFAULT_PRICING[model];
88
+ const legacyRows = [
89
+ ...legacy ? [legacy] : [],
90
+ ...ADDITIONAL_LEGACY_DEFAULT_PRICING[model] ?? []
91
+ ];
92
+ if (!legacyRows.some((row) => samePricing(current, row)))
93
+ continue;
94
+ upsertModelPricing(db, {
95
+ model,
96
+ input_per_1m: next.inputPer1M,
97
+ output_per_1m: next.outputPer1M,
98
+ cache_read_per_1m: next.cacheReadPer1M,
99
+ cache_write_per_1m: next.cacheWritePer1M,
100
+ cache_write_1h_per_1m: next.cacheWrite1hPer1M ?? 0,
101
+ cache_storage_per_1m_hour: next.cacheStoragePer1MHour ?? 0,
102
+ updated_at: now
103
+ });
104
+ }
105
+ }
106
+ function repairMissingDefaultCacheWrite1h(db) {
107
+ const now = new Date().toISOString();
108
+ for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
109
+ if (!next.cacheWrite1hPer1M)
110
+ continue;
111
+ const current = getModelPricing(db, model);
112
+ if (!current)
113
+ continue;
114
+ if ((current.cache_write_1h_per_1m ?? 0) !== 0)
115
+ continue;
116
+ if (!sameBasePricing(current, next))
117
+ continue;
118
+ upsertModelPricing(db, {
119
+ model,
120
+ input_per_1m: current.input_per_1m,
121
+ output_per_1m: current.output_per_1m,
122
+ cache_read_per_1m: current.cache_read_per_1m,
123
+ cache_write_per_1m: current.cache_write_per_1m,
124
+ cache_write_1h_per_1m: next.cacheWrite1hPer1M,
125
+ cache_storage_per_1m_hour: current.cache_storage_per_1m_hour ?? next.cacheStoragePer1MHour ?? 0,
126
+ updated_at: now
127
+ });
128
+ }
129
+ }
130
+ function repairMissingDefaultCacheStorage(db) {
131
+ const now = new Date().toISOString();
132
+ for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
133
+ if (!next.cacheStoragePer1MHour)
134
+ continue;
135
+ const current = getModelPricing(db, model);
136
+ if (!current)
137
+ continue;
138
+ if ((current.cache_storage_per_1m_hour ?? 0) !== 0)
139
+ continue;
140
+ if (!sameBasePricing(current, next))
141
+ continue;
142
+ upsertModelPricing(db, {
143
+ model,
144
+ input_per_1m: current.input_per_1m,
145
+ output_per_1m: current.output_per_1m,
146
+ cache_read_per_1m: current.cache_read_per_1m,
147
+ cache_write_per_1m: current.cache_write_per_1m,
148
+ cache_write_1h_per_1m: current.cache_write_1h_per_1m ?? next.cacheWrite1hPer1M ?? 0,
149
+ cache_storage_per_1m_hour: next.cacheStoragePer1MHour,
150
+ updated_at: now
151
+ });
152
+ }
153
+ }
154
+ function removeDeprecatedDefaultPricing(db) {
155
+ for (const [model, removedRows] of Object.entries(REMOVED_DEFAULT_PRICING)) {
156
+ const current = getModelPricing(db, model);
157
+ if (!current)
158
+ continue;
159
+ if (!removedRows.some((row) => samePricing(current, row)))
160
+ continue;
161
+ deleteModelPricing(db, model);
162
+ }
163
+ }
164
+ function sameBasePricing(row, pricing) {
165
+ return row.input_per_1m === pricing.inputPer1M && row.output_per_1m === pricing.outputPer1M && row.cache_read_per_1m === pricing.cacheReadPer1M && row.cache_write_per_1m === pricing.cacheWritePer1M;
166
+ }
167
+ function samePricing(row, pricing) {
168
+ return row.input_per_1m === pricing.inputPer1M && row.output_per_1m === pricing.outputPer1M && row.cache_read_per_1m === pricing.cacheReadPer1M && row.cache_write_per_1m === pricing.cacheWritePer1M && (row.cache_write_1h_per_1m ?? 0) === (pricing.cacheWrite1hPer1M ?? 0) && (row.cache_storage_per_1m_hour ?? 0) === (pricing.cacheStoragePer1MHour ?? 0);
37
169
  }
38
170
  function getPricingFromDb(db, model) {
39
- const normalized = normalizeModelName(model);
40
- const row = getModelPricing(db, normalized);
41
- if (row) {
42
- return {
43
- inputPer1M: row.input_per_1m,
44
- outputPer1M: row.output_per_1m,
45
- cacheReadPer1M: row.cache_read_per_1m,
46
- cacheWritePer1M: row.cache_write_per_1m
47
- };
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);
48
177
  }
49
178
  const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
50
- for (const r of allRows) {
51
- if (normalized.startsWith(r.model)) {
52
- return { inputPer1M: r.input_per_1m, outputPer1M: r.output_per_1m, cacheReadPer1M: r.cache_read_per_1m, cacheWritePer1M: r.cache_write_per_1m };
53
- }
54
- }
55
- return null;
179
+ const match = bestModelMatch(model, allRows.map((r) => [r.model, r]));
180
+ if (!match)
181
+ return null;
182
+ return modelPricingFromDbRow(match);
183
+ }
184
+ function modelPricingFromDbRow(row) {
185
+ const seeded = DEFAULT_PRICING[row.model];
186
+ const cacheWrite1hPer1M = seeded?.cacheWrite1hPer1M && (row.cache_write_1h_per_1m ?? 0) === 0 && sameBasePricing(row, seeded) ? seeded.cacheWrite1hPer1M : row.cache_write_1h_per_1m ?? 0;
187
+ return {
188
+ inputPer1M: row.input_per_1m,
189
+ outputPer1M: row.output_per_1m,
190
+ cacheReadPer1M: row.cache_read_per_1m,
191
+ cacheWritePer1M: row.cache_write_per_1m,
192
+ cacheWrite1hPer1M,
193
+ cacheStoragePer1MHour: row.cache_storage_per_1m_hour ?? seeded?.cacheStoragePer1MHour ?? 0
194
+ };
56
195
  }
57
196
  function getPricing(model) {
58
- const normalized = normalizeModelName(model);
59
- if (DEFAULT_PRICING[normalized])
60
- return DEFAULT_PRICING[normalized] ?? null;
61
- for (const key of Object.keys(DEFAULT_PRICING)) {
62
- if (normalized.startsWith(key))
63
- return DEFAULT_PRICING[key] ?? null;
64
- }
65
- return null;
197
+ if (isFreeModel(model))
198
+ return FREE_PRICING;
199
+ return bestModelMatch(model, Object.entries(DEFAULT_PRICING));
66
200
  }
67
- function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
201
+ function isFreeModel(model) {
202
+ return model.trim().toLowerCase().endsWith(":free");
203
+ }
204
+ function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
68
205
  const pricing = getPricing(model);
69
206
  if (!pricing)
70
207
  return 0;
71
- return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
208
+ return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
72
209
  }
73
- function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
210
+ function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
74
211
  const pricing = getPricingFromDb(db, model) ?? getPricing(model);
75
212
  if (!pricing)
76
213
  return 0;
77
- return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
214
+ return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
215
+ }
216
+ function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
217
+ if (isFreeModel(model))
218
+ return 0;
219
+ let effective = pricing;
220
+ const promptTier = bestModelMatch(model, Object.entries(GEMINI_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(QWEN_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(MINIMAX_PROMPT_TIERS)) ?? bestModelMatch(model, Object.entries(XAI_PROMPT_TIERS)) ?? exactModelMatch(model, Object.entries(OPENAI_PROMPT_TIERS));
221
+ if (promptTier) {
222
+ const billablePromptTokens = inputTokens + cacheReadTokens + cacheWriteTokens + cacheWrite1hTokens;
223
+ if (billablePromptTokens > promptTier.threshold) {
224
+ effective = {
225
+ ...pricing,
226
+ inputPer1M: promptTier.inputPer1M ?? pricing.inputPer1M * (promptTier.inputMultiplier ?? 1),
227
+ outputPer1M: promptTier.outputPer1M ?? pricing.outputPer1M * (promptTier.outputMultiplier ?? 1),
228
+ cacheReadPer1M: promptTier.cacheReadPer1M ?? pricing.cacheReadPer1M * (promptTier.cacheReadMultiplier ?? 1),
229
+ cacheWritePer1M: promptTier.cacheWritePer1M ?? pricing.cacheWritePer1M * (promptTier.cacheWriteMultiplier ?? 1)
230
+ };
231
+ }
232
+ }
233
+ return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
78
234
  }
79
- var DEFAULT_PRICING;
235
+ var DEFAULT_PRICING, LEGACY_DEFAULT_PRICING, ADDITIONAL_LEGACY_DEFAULT_PRICING, REMOVED_DEFAULT_PRICING, FREE_PRICING, GEMINI_PROMPT_TIERS, OPENAI_PROMPT_TIERS, QWEN_PROMPT_TIERS, MINIMAX_PROMPT_TIERS, XAI_PROMPT_TIERS;
80
236
  var init_pricing = __esm(() => {
81
237
  init_database();
82
238
  DEFAULT_PRICING = {
83
- "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
84
- "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
85
- "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
86
- "claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
87
- "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
88
- "claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
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 = {
89
325
  "claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
90
- "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
91
- "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
92
- "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
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 },
93
329
  "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
94
- "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
95
- "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
96
- "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
97
330
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
98
331
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
99
332
  "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
100
- "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
101
- "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
102
- o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
333
+ "gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
334
+ "gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
103
335
  "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
104
- o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
105
- "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
106
- "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
336
+ "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
337
+ "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
338
+ "qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
339
+ "minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
340
+ "minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
341
+ "minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
342
+ "glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
343
+ "glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
344
+ "kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
345
+ o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 }
346
+ };
347
+ ADDITIONAL_LEGACY_DEFAULT_PRICING = {
348
+ "gemini-2.5-pro": [
349
+ { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 }
350
+ ],
351
+ "qwen3.6-plus": [
352
+ { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
353
+ { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
354
+ ],
355
+ "qwen3.6-flash": [
356
+ { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
357
+ ],
358
+ "qwen3.6-max-preview": [
359
+ { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
360
+ { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
361
+ ],
362
+ "qwen/qwen3.6-plus": [
363
+ { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
364
+ { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
365
+ ],
366
+ "qwen/qwen3.6-flash": [
367
+ { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
368
+ ],
369
+ "qwen/qwen3.6-max-preview": [
370
+ { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
371
+ { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
372
+ ]
373
+ };
374
+ REMOVED_DEFAULT_PRICING = {
375
+ "claude-3-5-sonnet": [
376
+ { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
377
+ { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
378
+ ],
379
+ "claude-3-sonnet": [
380
+ { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
381
+ { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
382
+ ],
383
+ "gemini-3.1-pro": [
384
+ { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
385
+ { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 }
386
+ ],
387
+ "gemini-1.5-pro": [
388
+ { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 }
389
+ ],
390
+ "gemini-1.5-flash": [
391
+ { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
392
+ ],
393
+ "gpt-5.3-chat": [
394
+ { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
395
+ { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 }
396
+ ],
397
+ "qwen3.6": [
398
+ { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
399
+ ]
400
+ };
401
+ FREE_PRICING = {
402
+ inputPer1M: 0,
403
+ outputPer1M: 0,
404
+ cacheReadPer1M: 0,
405
+ cacheWritePer1M: 0,
406
+ cacheWrite1hPer1M: 0,
407
+ cacheStoragePer1MHour: 0
408
+ };
409
+ GEMINI_PROMPT_TIERS = {
410
+ "gemini-3.1-pro-preview": {
411
+ threshold: 200000,
412
+ inputPer1M: 4,
413
+ outputPer1M: 18,
414
+ cacheReadPer1M: 0.4
415
+ },
416
+ "gemini-2.5-pro": {
417
+ threshold: 200000,
418
+ inputPer1M: 2.5,
419
+ outputPer1M: 15,
420
+ cacheReadPer1M: 0.25
421
+ }
422
+ };
423
+ OPENAI_PROMPT_TIERS = {
424
+ "gpt-5.5": {
425
+ threshold: 272000,
426
+ inputMultiplier: 2,
427
+ outputMultiplier: 1.5,
428
+ cacheReadMultiplier: 2
429
+ },
430
+ "gpt-5.4-pro": {
431
+ threshold: 272000,
432
+ inputMultiplier: 2,
433
+ outputMultiplier: 1.5,
434
+ cacheReadMultiplier: 2
435
+ },
436
+ "gpt-5.4": {
437
+ threshold: 272000,
438
+ inputMultiplier: 2,
439
+ outputMultiplier: 1.5,
440
+ cacheReadMultiplier: 2
441
+ }
442
+ };
443
+ QWEN_PROMPT_TIERS = {
444
+ "qwen3.6-plus": {
445
+ threshold: 256000,
446
+ inputPer1M: 1.3,
447
+ outputPer1M: 3.9,
448
+ cacheReadPer1M: 0.13,
449
+ cacheWritePer1M: 1.625
450
+ },
451
+ "qwen3.6-flash": {
452
+ threshold: 256000,
453
+ inputPer1M: 1,
454
+ outputPer1M: 4,
455
+ cacheReadPer1M: 0.1,
456
+ cacheWritePer1M: 1.25
457
+ },
458
+ "qwen3.6-max-preview": {
459
+ threshold: 128000,
460
+ inputPer1M: 1.6,
461
+ outputPer1M: 9.6,
462
+ cacheReadPer1M: 0.16,
463
+ cacheWritePer1M: 2
464
+ }
465
+ };
466
+ MINIMAX_PROMPT_TIERS = {
467
+ "minimax/minimax-m1": {
468
+ threshold: Number.POSITIVE_INFINITY
469
+ },
470
+ "minimax-m1": {
471
+ threshold: 200000,
472
+ inputPer1M: 1.3
473
+ }
474
+ };
475
+ XAI_PROMPT_TIERS = {
476
+ "grok-4.3": {
477
+ threshold: 200000,
478
+ inputPer1M: 2.5,
479
+ outputPer1M: 5,
480
+ cacheReadPer1M: 0.4
481
+ },
482
+ "grok-latest": {
483
+ threshold: 200000,
484
+ inputPer1M: 2.5,
485
+ outputPer1M: 5,
486
+ cacheReadPer1M: 0.4
487
+ },
488
+ "grok-4.20": {
489
+ threshold: 200000,
490
+ inputPer1M: 2.5,
491
+ outputPer1M: 5,
492
+ cacheReadPer1M: 0.4
493
+ },
494
+ "grok-4-1-fast": {
495
+ threshold: 128000,
496
+ inputPer1M: 0.4,
497
+ outputPer1M: 1,
498
+ cacheReadPer1M: 0
499
+ },
500
+ "grok-4-fast": {
501
+ threshold: 128000,
502
+ inputPer1M: 0.4,
503
+ outputPer1M: 1,
504
+ cacheReadPer1M: 0
505
+ },
506
+ "grok-4": {
507
+ threshold: 128000,
508
+ inputPer1M: 6,
509
+ outputPer1M: 30,
510
+ cacheReadPer1M: 0
511
+ }
107
512
  };
108
513
  });
109
514
 
110
515
  // src/db/database.ts
111
516
  import { SqliteAdapter as Database } from "@hasna/cloud";
112
517
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
518
+ import { hostname } from "os";
113
519
  import { homedir } from "os";
114
520
  import { join } from "path";
521
+ function getMachineId() {
522
+ if (process.env["ECONOMY_MACHINE_ID"])
523
+ return process.env["ECONOMY_MACHINE_ID"];
524
+ const h = hostname().toLowerCase();
525
+ if (h.startsWith("spark") || h.startsWith("apple"))
526
+ return h.split(".")[0];
527
+ return h.split(".")[0];
528
+ }
115
529
  function getDataDir() {
116
530
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
117
531
  const newDir = join(home, ".hasna", "economy");
@@ -144,6 +558,7 @@ function openDatabase(dbPath, skipSeed = false) {
144
558
  }
145
559
  const db = new Database(path);
146
560
  db.exec("PRAGMA journal_mode = WAL");
561
+ db.exec("PRAGMA busy_timeout = 5000");
147
562
  db.exec("PRAGMA foreign_keys = ON");
148
563
  initSchema(db);
149
564
  if (!skipSeed) {
@@ -162,10 +577,13 @@ function initSchema(db) {
162
577
  output_tokens INTEGER DEFAULT 0,
163
578
  cache_read_tokens INTEGER DEFAULT 0,
164
579
  cache_create_tokens INTEGER DEFAULT 0,
580
+ cache_create_5m_tokens INTEGER DEFAULT 0,
581
+ cache_create_1h_tokens INTEGER DEFAULT 0,
165
582
  cost_usd REAL NOT NULL DEFAULT 0,
166
583
  duration_ms INTEGER DEFAULT 0,
167
584
  timestamp TEXT NOT NULL,
168
- source_request_id TEXT
585
+ source_request_id TEXT,
586
+ machine_id TEXT DEFAULT ''
169
587
  );
170
588
 
171
589
  CREATE TABLE IF NOT EXISTS sessions (
@@ -177,7 +595,8 @@ function initSchema(db) {
177
595
  ended_at TEXT,
178
596
  total_cost_usd REAL DEFAULT 0,
179
597
  total_tokens INTEGER DEFAULT 0,
180
- request_count INTEGER DEFAULT 0
598
+ request_count INTEGER DEFAULT 0,
599
+ machine_id TEXT DEFAULT ''
181
600
  );
182
601
 
183
602
  CREATE TABLE IF NOT EXISTS projects (
@@ -230,6 +649,8 @@ function initSchema(db) {
230
649
  output_per_1m REAL NOT NULL DEFAULT 0,
231
650
  cache_read_per_1m REAL NOT NULL DEFAULT 0,
232
651
  cache_write_per_1m REAL NOT NULL DEFAULT 0,
652
+ cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
653
+ cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
233
654
  updated_at TEXT NOT NULL
234
655
  );
235
656
 
@@ -242,6 +663,115 @@ function initSchema(db) {
242
663
  machine_id TEXT,
243
664
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
244
665
  );
666
+
667
+ CREATE TABLE IF NOT EXISTS billing_daily (
668
+ date TEXT NOT NULL,
669
+ provider TEXT NOT NULL,
670
+ description TEXT DEFAULT '',
671
+ cost_usd REAL NOT NULL DEFAULT 0,
672
+ updated_at TEXT NOT NULL,
673
+ PRIMARY KEY (date, provider, description)
674
+ );
675
+
676
+ CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
677
+ CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
678
+
679
+ CREATE TABLE IF NOT EXISTS subscriptions (
680
+ id TEXT PRIMARY KEY,
681
+ agent TEXT,
682
+ provider TEXT NOT NULL,
683
+ plan TEXT NOT NULL,
684
+ monthly_fee_usd REAL NOT NULL DEFAULT 0,
685
+ included_usage_usd REAL NOT NULL DEFAULT 0,
686
+ billing_cycle_start TEXT,
687
+ reset_policy TEXT DEFAULT 'monthly',
688
+ active INTEGER NOT NULL DEFAULT 1,
689
+ created_at TEXT NOT NULL,
690
+ updated_at TEXT NOT NULL
691
+ );
692
+
693
+ CREATE TABLE IF NOT EXISTS usage_snapshots (
694
+ id TEXT PRIMARY KEY,
695
+ agent TEXT NOT NULL,
696
+ date TEXT NOT NULL,
697
+ metric TEXT NOT NULL,
698
+ value REAL NOT NULL DEFAULT 0,
699
+ unit TEXT DEFAULT '',
700
+ machine_id TEXT DEFAULT '',
701
+ updated_at TEXT NOT NULL
702
+ );
703
+
704
+ CREATE TABLE IF NOT EXISTS savings_daily (
705
+ date TEXT NOT NULL,
706
+ agent TEXT DEFAULT '',
707
+ api_equivalent_usd REAL NOT NULL DEFAULT 0,
708
+ subscription_fee_usd REAL NOT NULL DEFAULT 0,
709
+ included_consumed_usd REAL NOT NULL DEFAULT 0,
710
+ on_demand_usd REAL NOT NULL DEFAULT 0,
711
+ saved_usd REAL NOT NULL DEFAULT 0,
712
+ updated_at TEXT NOT NULL,
713
+ PRIMARY KEY (date, agent)
714
+ );
715
+
716
+ CREATE TABLE IF NOT EXISTS machines (
717
+ machine_id TEXT PRIMARY KEY,
718
+ hostname TEXT NOT NULL,
719
+ last_seen_at TEXT,
720
+ last_push_at TEXT,
721
+ last_pull_at TEXT,
722
+ economy_version TEXT,
723
+ updated_at TEXT NOT NULL
724
+ );
725
+
726
+ CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
727
+ CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
728
+ `);
729
+ const cols = db.prepare(`PRAGMA table_info(requests)`).all();
730
+ if (!cols.some((c) => c.name === "machine_id")) {
731
+ db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
732
+ db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
733
+ }
734
+ if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
735
+ db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
736
+ db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
737
+ }
738
+ if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
739
+ db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
740
+ }
741
+ if (!cols.some((c) => c.name === "cost_basis")) {
742
+ db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
743
+ }
744
+ if (!cols.some((c) => c.name === "attribution_tag")) {
745
+ db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
746
+ }
747
+ if (!cols.some((c) => c.name === "updated_at")) {
748
+ db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
749
+ db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
750
+ }
751
+ if (!cols.some((c) => c.name === "synced_at")) {
752
+ db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
753
+ }
754
+ const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
755
+ if (!sessionCols.some((c) => c.name === "attribution_tag")) {
756
+ db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
757
+ }
758
+ if (!sessionCols.some((c) => c.name === "updated_at")) {
759
+ db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
760
+ db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
761
+ }
762
+ if (!sessionCols.some((c) => c.name === "synced_at")) {
763
+ db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
764
+ }
765
+ const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
766
+ if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
767
+ db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
768
+ }
769
+ if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
770
+ db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
771
+ }
772
+ db.exec(`
773
+ CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
774
+ CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
245
775
  `);
246
776
  }
247
777
  function periodWhere(period) {
@@ -251,11 +781,11 @@ function periodWhere(period) {
251
781
  case "yesterday":
252
782
  return `DATE(timestamp) = DATE('now', '-1 day')`;
253
783
  case "week":
254
- return `timestamp >= DATE('now', '-7 days')`;
784
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
255
785
  case "month":
256
- return `timestamp >= DATE('now', '-30 days')`;
786
+ return `timestamp >= DATE('now', 'start of month')`;
257
787
  case "year":
258
- return `timestamp >= DATE('now', '-365 days')`;
788
+ return `timestamp >= DATE('now', 'start of year')`;
259
789
  case "all":
260
790
  return "1=1";
261
791
  }
@@ -267,31 +797,34 @@ function sessionPeriodWhere(period) {
267
797
  case "yesterday":
268
798
  return `DATE(started_at) = DATE('now', '-1 day')`;
269
799
  case "week":
270
- return `started_at >= DATE('now', '-7 days')`;
800
+ return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
271
801
  case "month":
272
- return `started_at >= DATE('now', '-30 days')`;
802
+ return `started_at >= DATE('now', 'start of month')`;
273
803
  case "year":
274
- return `started_at >= DATE('now', '-365 days')`;
804
+ return `started_at >= DATE('now', 'start of year')`;
275
805
  case "all":
276
806
  return "1=1";
277
807
  }
278
808
  }
279
809
  function upsertRequest(db, req) {
810
+ const now = req.updated_at ?? new Date().toISOString();
280
811
  db.prepare(`
281
812
  INSERT OR REPLACE INTO requests
282
813
  (id, agent, session_id, model, input_tokens, output_tokens,
283
- cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
284
- timestamp, source_request_id)
285
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
286
- `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id);
814
+ cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
815
+ cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
816
+ source_request_id, machine_id, attribution_tag, updated_at, synced_at)
817
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
818
+ `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", now, req.synced_at ?? "");
287
819
  }
288
820
  function upsertSession(db, session) {
821
+ const now = session.updated_at ?? new Date().toISOString();
289
822
  db.prepare(`
290
823
  INSERT OR REPLACE INTO sessions
291
824
  (id, agent, project_path, project_name, started_at, ended_at,
292
- total_cost_usd, total_tokens, request_count)
293
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
294
- `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count);
825
+ total_cost_usd, total_tokens, request_count, machine_id, attribution_tag, updated_at, synced_at)
826
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
827
+ `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", now, session.synced_at ?? "");
295
828
  }
296
829
  function rollupSession(db, sessionId) {
297
830
  db.prepare(`
@@ -321,6 +854,10 @@ function querySessions(db, filter = {}) {
321
854
  conditions.push("started_at >= ?");
322
855
  params.push(filter.since);
323
856
  }
857
+ if (filter.machine) {
858
+ conditions.push("machine_id = ?");
859
+ params.push(filter.machine);
860
+ }
324
861
  if (filter.search) {
325
862
  const q = `%${filter.search}%`;
326
863
  conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
@@ -339,24 +876,25 @@ function queryTopSessions(db, n = 10, agent) {
339
876
  }
340
877
  return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
341
878
  }
342
- function querySummary(db, period) {
879
+ function querySummary(db, period, machine, allMachines = false) {
343
880
  const rWhere = periodWhere(period);
344
881
  const sWhere = sessionPeriodWhere(period);
882
+ const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
345
883
  const r = db.prepare(`
346
884
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
347
885
  COUNT(*) as requests,
348
886
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
349
- FROM requests WHERE ${rWhere}
887
+ FROM requests WHERE ${rWhere}${machineClause}
350
888
  `).get();
351
889
  const codexTotals = db.prepare(`
352
890
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
353
891
  COALESCE(SUM(total_tokens), 0) as tokens,
354
892
  COUNT(*) as sessions
355
893
  FROM sessions
356
- WHERE ${sWhere}
894
+ WHERE ${sWhere}${machineClause}
357
895
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
358
896
  `).get();
359
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
897
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
360
898
  return {
361
899
  total_usd: r.total_usd + codexTotals.cost_usd,
362
900
  requests: r.requests,
@@ -376,23 +914,66 @@ function queryModelBreakdown(db) {
376
914
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
377
915
  `).all();
378
916
  }
917
+ function labelForPath(projectPath, projectName) {
918
+ if (projectName && projectName.trim() !== "")
919
+ return projectName;
920
+ if (!projectPath)
921
+ return "";
922
+ const segments = projectPath.split("/").filter(Boolean);
923
+ const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
924
+ for (const seg of segments) {
925
+ if (projectPrefix.test(seg))
926
+ return seg;
927
+ }
928
+ const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
929
+ for (let i = segments.length - 1;i >= 0; i--) {
930
+ if (!generic.has(segments[i].toLowerCase()))
931
+ return segments[i];
932
+ }
933
+ return segments[segments.length - 1] ?? projectPath;
934
+ }
379
935
  function queryProjectBreakdown(db) {
380
- return db.prepare(`
381
- SELECT
382
- s.project_path,
383
- COALESCE(p.name, s.project_name) as project_name,
384
- COUNT(DISTINCT s.id) as sessions,
385
- COUNT(r.id) as requests,
386
- COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
387
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
388
- MAX(s.started_at) as last_active
389
- FROM sessions s
390
- LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
391
- LEFT JOIN requests r ON r.session_id = s.id
392
- WHERE s.project_path != '' OR s.project_name != ''
393
- GROUP BY s.project_path
394
- ORDER BY cost_usd DESC
936
+ const sessions = db.prepare(`
937
+ SELECT id, project_path, project_name, total_cost_usd, started_at
938
+ FROM sessions
939
+ WHERE project_path != '' OR project_name != ''
395
940
  `).all();
941
+ const groups = new Map;
942
+ for (const s of sessions) {
943
+ const label = labelForPath(s.project_path, s.project_name);
944
+ if (!label)
945
+ continue;
946
+ const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
947
+ g.sessionIds.push(s.id);
948
+ g.totalCost += s.total_cost_usd || 0;
949
+ if (!g.lastActive || s.started_at > g.lastActive)
950
+ g.lastActive = s.started_at;
951
+ if (!g.samplePath)
952
+ g.samplePath = s.project_path;
953
+ groups.set(label, g);
954
+ }
955
+ const result = [];
956
+ for (const [label, g] of groups.entries()) {
957
+ const placeholders = g.sessionIds.map(() => "?").join(",");
958
+ const reqStats = placeholders.length ? db.prepare(`
959
+ SELECT
960
+ COUNT(*) as requests,
961
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
962
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
963
+ FROM requests WHERE session_id IN (${placeholders})
964
+ `).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
965
+ result.push({
966
+ project_path: g.samplePath,
967
+ project_name: label,
968
+ sessions: g.sessionIds.length,
969
+ requests: reqStats.requests,
970
+ total_tokens: reqStats.total_tokens,
971
+ cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
972
+ last_active: g.lastActive
973
+ });
974
+ }
975
+ result.sort((a, b) => b.cost_usd - a.cost_usd);
976
+ return result;
396
977
  }
397
978
  function queryDailyBreakdown(db, days = 30) {
398
979
  return db.prepare(`
@@ -403,9 +984,19 @@ function queryDailyBreakdown(db, days = 30) {
403
984
  ORDER BY date ASC
404
985
  `).all(`-${days}`);
405
986
  }
987
+ function upsertBudget(db, budget) {
988
+ db.prepare(`
989
+ INSERT OR REPLACE INTO budgets
990
+ (id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
991
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
992
+ `).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
993
+ }
406
994
  function listBudgets(db) {
407
995
  return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
408
996
  }
997
+ function deleteBudget(db, id) {
998
+ db.prepare(`DELETE FROM budgets WHERE id = ?`).run(id);
999
+ }
409
1000
  function getBudgetStatuses(db) {
410
1001
  const budgets = listBudgets(db);
411
1002
  return budgets.map((b) => {
@@ -479,120 +1070,424 @@ function getIngestState(db, source, key) {
479
1070
  function setIngestState(db, source, key, value) {
480
1071
  db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
481
1072
  }
1073
+ function queryBillingSummary(db, period) {
1074
+ 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";
1075
+ const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
1076
+ const by_provider = {};
1077
+ let total = 0;
1078
+ for (const r of rows) {
1079
+ by_provider[r.provider] = r.cost;
1080
+ total += r.cost;
1081
+ }
1082
+ return { total_usd: total, by_provider };
1083
+ }
1084
+ function listMachines(db) {
1085
+ return db.prepare(`
1086
+ SELECT
1087
+ s.machine_id,
1088
+ COUNT(DISTINCT s.id) as sessions,
1089
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
1090
+ COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
1091
+ MAX(s.started_at) as last_active
1092
+ FROM sessions s
1093
+ WHERE s.machine_id != ''
1094
+ GROUP BY s.machine_id
1095
+ ORDER BY total_cost_usd DESC
1096
+ `).all();
1097
+ }
482
1098
  function upsertModelPricing(db, p) {
483
1099
  db.prepare(`
484
1100
  INSERT OR REPLACE INTO model_pricing
485
- (model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
486
- VALUES (?, ?, ?, ?, ?, ?)
487
- `).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.updated_at);
1101
+ (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)
1102
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1103
+ `).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);
488
1104
  }
489
1105
  function getModelPricing(db, model) {
490
1106
  return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
491
1107
  }
1108
+ function listModelPricing(db) {
1109
+ return db.prepare(`SELECT * FROM model_pricing ORDER BY model ASC`).all();
1110
+ }
1111
+ function deleteModelPricing(db, model) {
1112
+ db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
1113
+ }
492
1114
  function seedModelPricing(db, defaults) {
493
- const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
494
- if (existing.count > 0)
495
- return;
1115
+ const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
496
1116
  const now = new Date().toISOString();
497
1117
  for (const [model, p] of Object.entries(defaults)) {
1118
+ if (existing.has(model))
1119
+ continue;
498
1120
  upsertModelPricing(db, {
499
1121
  model,
500
1122
  input_per_1m: p.inputPer1M,
501
1123
  output_per_1m: p.outputPer1M,
502
1124
  cache_read_per_1m: p.cacheReadPer1M,
503
1125
  cache_write_per_1m: p.cacheWritePer1M,
1126
+ cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
1127
+ cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
504
1128
  updated_at: now
505
1129
  });
506
1130
  }
507
1131
  }
1132
+ function upsertSubscription(db, sub) {
1133
+ db.prepare(`
1134
+ INSERT OR REPLACE INTO subscriptions
1135
+ (id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
1136
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1137
+ `).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);
1138
+ }
1139
+ function upsertUsageSnapshot(db, snap) {
1140
+ const now = snap.updated_at ?? new Date().toISOString();
1141
+ const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
1142
+ db.prepare(`
1143
+ INSERT OR REPLACE INTO usage_snapshots (id, agent, date, metric, value, unit, machine_id, updated_at)
1144
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1145
+ `).run(id, snap.agent, snap.date, snap.metric, snap.value, snap.unit, snap.machine_id, now);
1146
+ }
1147
+ function queryUsageSnapshots(db, opts = {}) {
1148
+ const conditions = [];
1149
+ const params = [];
1150
+ if (opts.agent) {
1151
+ conditions.push("agent = ?");
1152
+ params.push(opts.agent);
1153
+ }
1154
+ if (opts.date) {
1155
+ conditions.push("date = ?");
1156
+ params.push(opts.date);
1157
+ }
1158
+ if (opts.since) {
1159
+ conditions.push("date >= ?");
1160
+ params.push(opts.since);
1161
+ }
1162
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
1163
+ return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
1164
+ }
1165
+ function dedupeRequests(db) {
1166
+ const dupes = db.prepare(`
1167
+ SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
1168
+ FROM requests
1169
+ WHERE source_request_id != '' AND source_request_id IS NOT NULL
1170
+ GROUP BY source_request_id, agent
1171
+ HAVING cnt > 1
1172
+ `).all();
1173
+ let removed = 0;
1174
+ for (const row of dupes) {
1175
+ const result = db.prepare(`
1176
+ DELETE FROM requests WHERE source_request_id = ? AND agent = ? AND id != ?
1177
+ `).run(row.source_request_id, row.agent, row.keep_id);
1178
+ removed += result.changes;
1179
+ }
1180
+ return removed;
1181
+ }
508
1182
  var init_database = () => {};
509
1183
 
510
- // package.json
511
- var require_package = __commonJS((exports, module) => {
512
- module.exports = {
513
- name: "@hasna/economy",
514
- version: "0.2.10",
515
- description: "AI coding cost tracker \u2014 CLI + MCP server + REST API + web dashboard for Claude Code, Codex, and Gemini",
516
- type: "module",
517
- main: "dist/index.js",
518
- types: "dist/index.d.ts",
519
- bin: {
520
- economy: "dist/cli/index.js",
521
- "economy-mcp": "dist/mcp/index.js",
522
- "economy-serve": "dist/server/index.js"
523
- },
524
- exports: {
525
- ".": {
526
- types: "./dist/index.d.ts",
527
- import: "./dist/index.js"
528
- }
529
- },
530
- files: [
531
- "dist",
532
- "LICENSE"
533
- ],
534
- scripts: {
535
- build: "cd dashboard && bun run build && cd .. && bun build src/cli/index.ts --outdir dist/cli --target bun --packages external && bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/index.ts --outdir dist --target bun --packages external && tsc --emitDeclarationOnly --outDir dist",
536
- "build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
537
- "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
538
- "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
539
- "build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
540
- "build:dashboard": "cd dashboard && bun run build",
541
- typecheck: "tsc --noEmit",
542
- test: "bun test",
543
- "dev:cli": "bun run src/cli/index.ts",
544
- "dev:mcp": "bun run src/mcp/index.ts",
545
- "dev:serve": "bun run src/server/index.ts",
546
- postinstall: "mkdir -p $HOME/.hasna/economy/training 2>/dev/null || true"
547
- },
548
- keywords: [
549
- "economy",
550
- "cost",
551
- "ai",
552
- "claude",
553
- "codex",
554
- "gemini",
555
- "mcp",
556
- "cli",
557
- "budget",
558
- "tracking"
559
- ],
560
- author: "hasna",
561
- license: "Apache-2.0",
562
- publishConfig: {
563
- registry: "https://registry.npmjs.org",
564
- access: "public"
565
- },
566
- dependencies: {
567
- "@hasna/cloud": "^0.1.0",
568
- "@modelcontextprotocol/sdk": "^1.12.1",
569
- chalk: "^5.4.1",
570
- commander: "^13.1.0"
571
- },
572
- devDependencies: {
573
- "@types/bun": "latest",
574
- "bun-types": "latest",
575
- typescript: "^5.7.2"
576
- }
577
- };
1184
+ // src/db/pg-migrations.ts
1185
+ var exports_pg_migrations = {};
1186
+ __export(exports_pg_migrations, {
1187
+ PG_MIGRATIONS: () => PG_MIGRATIONS
1188
+ });
1189
+ var PG_MIGRATIONS;
1190
+ var init_pg_migrations = __esm(() => {
1191
+ PG_MIGRATIONS = [
1192
+ `CREATE TABLE IF NOT EXISTS requests (
1193
+ id TEXT PRIMARY KEY,
1194
+ agent TEXT NOT NULL,
1195
+ session_id TEXT NOT NULL,
1196
+ model TEXT NOT NULL,
1197
+ input_tokens INTEGER DEFAULT 0,
1198
+ output_tokens INTEGER DEFAULT 0,
1199
+ cache_read_tokens INTEGER DEFAULT 0,
1200
+ cache_create_tokens INTEGER DEFAULT 0,
1201
+ cache_create_5m_tokens INTEGER DEFAULT 0,
1202
+ cache_create_1h_tokens INTEGER DEFAULT 0,
1203
+ cost_usd REAL NOT NULL DEFAULT 0,
1204
+ duration_ms INTEGER DEFAULT 0,
1205
+ timestamp TEXT NOT NULL,
1206
+ source_request_id TEXT,
1207
+ machine_id TEXT DEFAULT ''
1208
+ )`,
1209
+ `CREATE TABLE IF NOT EXISTS sessions (
1210
+ id TEXT PRIMARY KEY,
1211
+ agent TEXT NOT NULL,
1212
+ project_path TEXT DEFAULT '',
1213
+ project_name TEXT DEFAULT '',
1214
+ started_at TEXT NOT NULL,
1215
+ ended_at TEXT,
1216
+ total_cost_usd REAL DEFAULT 0,
1217
+ total_tokens INTEGER DEFAULT 0,
1218
+ request_count INTEGER DEFAULT 0,
1219
+ machine_id TEXT DEFAULT ''
1220
+ )`,
1221
+ `CREATE TABLE IF NOT EXISTS projects (
1222
+ id TEXT PRIMARY KEY,
1223
+ path TEXT UNIQUE NOT NULL,
1224
+ name TEXT NOT NULL,
1225
+ description TEXT,
1226
+ tags TEXT DEFAULT '[]',
1227
+ created_at TEXT NOT NULL
1228
+ )`,
1229
+ `CREATE TABLE IF NOT EXISTS budgets (
1230
+ id TEXT PRIMARY KEY,
1231
+ project_path TEXT,
1232
+ agent TEXT,
1233
+ period TEXT NOT NULL,
1234
+ limit_usd REAL NOT NULL,
1235
+ alert_at_percent INTEGER DEFAULT 80,
1236
+ created_at TEXT NOT NULL,
1237
+ updated_at TEXT NOT NULL
1238
+ )`,
1239
+ `CREATE TABLE IF NOT EXISTS goals (
1240
+ id TEXT PRIMARY KEY,
1241
+ period TEXT NOT NULL,
1242
+ project_path TEXT,
1243
+ agent TEXT,
1244
+ limit_usd REAL NOT NULL,
1245
+ created_at TEXT NOT NULL,
1246
+ updated_at TEXT NOT NULL
1247
+ )`,
1248
+ `CREATE TABLE IF NOT EXISTS ingest_state (
1249
+ source TEXT NOT NULL,
1250
+ key TEXT NOT NULL,
1251
+ value TEXT NOT NULL,
1252
+ PRIMARY KEY (source, key)
1253
+ )`,
1254
+ `CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
1255
+ `CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
1256
+ `CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
1257
+ `CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
1258
+ `CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
1259
+ `CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
1260
+ `CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
1261
+ `CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
1262
+ `CREATE TABLE IF NOT EXISTS model_pricing (
1263
+ model TEXT PRIMARY KEY,
1264
+ input_per_1m REAL NOT NULL DEFAULT 0,
1265
+ output_per_1m REAL NOT NULL DEFAULT 0,
1266
+ cache_read_per_1m REAL NOT NULL DEFAULT 0,
1267
+ cache_write_per_1m REAL NOT NULL DEFAULT 0,
1268
+ cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
1269
+ cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
1270
+ updated_at TEXT NOT NULL
1271
+ )`,
1272
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_5m_tokens INTEGER DEFAULT 0`,
1273
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_1h_tokens INTEGER DEFAULT 0`,
1274
+ `ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`,
1275
+ `ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`,
1276
+ `CREATE TABLE IF NOT EXISTS feedback (
1277
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
1278
+ message TEXT NOT NULL,
1279
+ email TEXT,
1280
+ category TEXT DEFAULT 'general',
1281
+ version TEXT,
1282
+ machine_id TEXT,
1283
+ created_at TEXT NOT NULL DEFAULT NOW()::text
1284
+ )`,
1285
+ `CREATE TABLE IF NOT EXISTS billing_daily (
1286
+ date TEXT NOT NULL,
1287
+ provider TEXT NOT NULL,
1288
+ description TEXT DEFAULT '',
1289
+ cost_usd REAL NOT NULL DEFAULT 0,
1290
+ updated_at TEXT NOT NULL,
1291
+ PRIMARY KEY (date, provider, description)
1292
+ )`,
1293
+ `CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date)`,
1294
+ `CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider)`,
1295
+ `CREATE TABLE IF NOT EXISTS subscriptions (
1296
+ id TEXT PRIMARY KEY,
1297
+ agent TEXT,
1298
+ provider TEXT NOT NULL,
1299
+ plan TEXT NOT NULL,
1300
+ monthly_fee_usd REAL NOT NULL DEFAULT 0,
1301
+ included_usage_usd REAL NOT NULL DEFAULT 0,
1302
+ billing_cycle_start TEXT,
1303
+ reset_policy TEXT DEFAULT 'monthly',
1304
+ active INTEGER NOT NULL DEFAULT 1,
1305
+ created_at TEXT NOT NULL,
1306
+ updated_at TEXT NOT NULL
1307
+ )`,
1308
+ `CREATE TABLE IF NOT EXISTS usage_snapshots (
1309
+ id TEXT PRIMARY KEY,
1310
+ agent TEXT NOT NULL,
1311
+ date TEXT NOT NULL,
1312
+ metric TEXT NOT NULL,
1313
+ value REAL NOT NULL DEFAULT 0,
1314
+ unit TEXT DEFAULT '',
1315
+ machine_id TEXT DEFAULT '',
1316
+ updated_at TEXT NOT NULL
1317
+ )`,
1318
+ `CREATE TABLE IF NOT EXISTS savings_daily (
1319
+ date TEXT NOT NULL,
1320
+ agent TEXT DEFAULT '',
1321
+ api_equivalent_usd REAL NOT NULL DEFAULT 0,
1322
+ subscription_fee_usd REAL NOT NULL DEFAULT 0,
1323
+ included_consumed_usd REAL NOT NULL DEFAULT 0,
1324
+ on_demand_usd REAL NOT NULL DEFAULT 0,
1325
+ saved_usd REAL NOT NULL DEFAULT 0,
1326
+ updated_at TEXT NOT NULL,
1327
+ PRIMARY KEY (date, agent)
1328
+ )`,
1329
+ `CREATE TABLE IF NOT EXISTS machines (
1330
+ machine_id TEXT PRIMARY KEY,
1331
+ hostname TEXT NOT NULL,
1332
+ last_seen_at TEXT,
1333
+ last_push_at TEXT,
1334
+ last_pull_at TEXT,
1335
+ economy_version TEXT,
1336
+ updated_at TEXT NOT NULL
1337
+ )`,
1338
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
1339
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1340
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1341
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1342
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1343
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1344
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1345
+ `CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
1346
+ `CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`
1347
+ ];
578
1348
  });
579
1349
 
580
1350
  // src/mcp/index.ts
581
- init_database();
582
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
583
1351
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
584
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
1352
+
1353
+ // src/lib/package-metadata.ts
1354
+ import { readFileSync } from "fs";
1355
+ var cachedMetadata = null;
1356
+ function getPackageMetadata() {
1357
+ if (cachedMetadata)
1358
+ return cachedMetadata;
1359
+ const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf8");
1360
+ const parsed = JSON.parse(raw);
1361
+ cachedMetadata = {
1362
+ name: parsed.name ?? "@hasna/economy",
1363
+ version: parsed.version ?? "0.0.0"
1364
+ };
1365
+ return cachedMetadata;
1366
+ }
1367
+ var packageMetadata = getPackageMetadata();
1368
+
1369
+ // src/mcp/server.ts
1370
+ init_database();
1371
+ init_pg_migrations();
1372
+ import { randomUUID } from "crypto";
1373
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1374
+ import { registerCloudTools } from "@hasna/cloud";
1375
+ import { z } from "zod";
585
1376
 
586
1377
  // src/ingest/claude.ts
587
1378
  init_database();
588
1379
  init_pricing();
589
- import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
1380
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync2, statSync as statSync2 } from "fs";
590
1381
  import { homedir as homedir2 } from "os";
591
1382
  import { join as join2, basename } from "path";
1383
+
1384
+ // src/lib/savings.ts
1385
+ function periodWhere2(period, column) {
1386
+ switch (period) {
1387
+ case "today":
1388
+ return `DATE(${column}) = DATE('now')`;
1389
+ case "yesterday":
1390
+ return `DATE(${column}) = DATE('now', '-1 day')`;
1391
+ case "week":
1392
+ return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
1393
+ case "month":
1394
+ return `${column} >= DATE('now', 'start of month')`;
1395
+ case "year":
1396
+ return `${column} >= DATE('now', 'start of year')`;
1397
+ case "all":
1398
+ return "1=1";
1399
+ }
1400
+ }
1401
+ function prorateMonthlyFee(monthlyFee, period) {
1402
+ const now = new Date;
1403
+ const dayOfMonth = now.getDate();
1404
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
1405
+ switch (period) {
1406
+ case "today":
1407
+ case "yesterday":
1408
+ return monthlyFee / daysInMonth;
1409
+ case "week":
1410
+ return monthlyFee / daysInMonth * 7;
1411
+ case "month":
1412
+ return monthlyFee;
1413
+ case "year":
1414
+ return monthlyFee * 12;
1415
+ case "all":
1416
+ return monthlyFee;
1417
+ }
1418
+ }
1419
+ function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
1420
+ return Math.max(0, apiEquivalent - onDemand - subscriptionFee);
1421
+ }
1422
+ function querySavingsSummary(db, period, agent) {
1423
+ const where = periodWhere2(period, "timestamp");
1424
+ const agentClause = agent ? " AND agent = ?" : "";
1425
+ const params = agent ? [agent] : [];
1426
+ const apiRow = db.prepare(`
1427
+ SELECT COALESCE(SUM(cost_usd), 0) as total
1428
+ FROM requests
1429
+ WHERE ${where}${agentClause}
1430
+ AND COALESCE(cost_basis, 'estimated') IN ('metered_api', 'estimated', 'unknown')
1431
+ `).get(...params);
1432
+ const includedRow = db.prepare(`
1433
+ SELECT COALESCE(SUM(cost_usd), 0) as total
1434
+ FROM requests
1435
+ WHERE ${where}${agentClause}
1436
+ AND cost_basis = 'subscription_included'
1437
+ `).get(...params);
1438
+ const subWhere = periodWhere2(period, "date");
1439
+ const onDemandRow = db.prepare(`
1440
+ SELECT COALESCE(SUM(value), 0) as total
1441
+ FROM usage_snapshots
1442
+ WHERE ${subWhere}${agent ? " AND agent = ?" : ""}
1443
+ AND metric = 'on_demand_usd'
1444
+ `).get(...params);
1445
+ const subs = db.prepare(`
1446
+ SELECT COALESCE(SUM(monthly_fee_usd), 0) as total
1447
+ FROM subscriptions
1448
+ WHERE active = 1${agent ? " AND agent = ?" : ""}
1449
+ `).get(...agent ? [agent] : []);
1450
+ const subscriptionFee = prorateMonthlyFee(subs.total, period);
1451
+ const apiEquivalent = apiRow.total + includedRow.total;
1452
+ const onDemand = onDemandRow.total;
1453
+ const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
1454
+ const byAgent = {};
1455
+ if (!agent) {
1456
+ for (const row of db.prepare(`
1457
+ SELECT agent, COALESCE(SUM(cost_usd), 0) as api_eq
1458
+ FROM requests WHERE ${where}
1459
+ GROUP BY agent
1460
+ `).all()) {
1461
+ byAgent[row.agent] = {
1462
+ api_equivalent_usd: row.api_eq,
1463
+ saved_usd: row.api_eq
1464
+ };
1465
+ }
1466
+ }
1467
+ return {
1468
+ period,
1469
+ api_equivalent_usd: apiEquivalent,
1470
+ subscription_fee_usd: subscriptionFee,
1471
+ included_consumed_usd: includedRow.total,
1472
+ on_demand_usd: onDemand,
1473
+ saved_usd: saved,
1474
+ by_agent: byAgent
1475
+ };
1476
+ }
1477
+ function defaultCostBasisForAgent(agent) {
1478
+ if (agent === "claude")
1479
+ return "metered_api";
1480
+ if (agent === "cursor")
1481
+ return "subscription_included";
1482
+ return "estimated";
1483
+ }
1484
+
1485
+ // src/ingest/claude.ts
592
1486
  function autoDetectProject(cwd, projects) {
593
1487
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
594
1488
  }
595
- var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
1489
+ var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
1490
+ var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
596
1491
  function dirNameToPath(dirName) {
597
1492
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
598
1493
  }
@@ -611,35 +1506,42 @@ function collectJsonlFiles(projectDir) {
611
1506
  walk(projectDir);
612
1507
  return files;
613
1508
  }
614
- async function ingestClaude(db, verbose = false, _telemetryDir) {
615
- if (!existsSync2(PROJECTS_DIR)) {
1509
+ async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
1510
+ return ingestJsonlProjects(db, projectsDir, "claude", verbose);
1511
+ }
1512
+ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
1513
+ return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
1514
+ }
1515
+ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
1516
+ if (!existsSync2(projectsDir)) {
616
1517
  if (verbose)
617
- console.log("Claude projects dir not found:", PROJECTS_DIR);
1518
+ console.log(`${agentName} projects dir not found:`, projectsDir);
618
1519
  return { files: 0, requests: 0, sessions: 0 };
619
1520
  }
1521
+ const machineId = getMachineId();
620
1522
  let totalFiles = 0;
621
1523
  let totalRequests = 0;
622
1524
  const touchedSessions = new Set;
623
1525
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
624
- const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
1526
+ const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
625
1527
  for (const projectDirEntry of projectDirs) {
626
- const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
1528
+ const projectDirPath = join2(projectsDir, projectDirEntry.name);
627
1529
  const projectPath = dirNameToPath(projectDirEntry.name);
628
1530
  const jsonlFiles = collectJsonlFiles(projectDirPath);
629
1531
  for (const filePath of jsonlFiles) {
630
- const stateKey = filePath.replace(PROJECTS_DIR, "");
1532
+ const stateKey = filePath.replace(projectsDir, "");
631
1533
  let fileMtime = "0";
632
1534
  try {
633
1535
  fileMtime = statSync2(filePath).mtimeMs.toString();
634
1536
  } catch {
635
1537
  continue;
636
1538
  }
637
- const processed = getIngestState(db, "claude", stateKey);
1539
+ const processed = getIngestState(db, agentName, stateKey);
638
1540
  if (processed === fileMtime)
639
1541
  continue;
640
1542
  let lines;
641
1543
  try {
642
- lines = readFileSync(filePath, "utf-8").split(`
1544
+ lines = readFileSync2(filePath, "utf-8").split(`
643
1545
  `).filter((l) => l.trim());
644
1546
  } catch {
645
1547
  continue;
@@ -669,26 +1571,38 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
669
1571
  continue;
670
1572
  const inputTokens = usage.input_tokens ?? 0;
671
1573
  const outputTokens = usage.output_tokens ?? 0;
672
- const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
1574
+ const cacheWrite5mTokens = usage.cache_creation?.ephemeral_5m_input_tokens ?? usage.cache_creation_input_tokens ?? 0;
1575
+ const cacheWrite1hTokens = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
1576
+ const cacheWriteTokens = cacheWrite5mTokens + cacheWrite1hTokens;
673
1577
  const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
674
1578
  const timestamp = entry.timestamp ?? new Date().toISOString();
675
- if (inputTokens + outputTokens + cacheWriteTokens === 0)
1579
+ if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
676
1580
  continue;
677
- const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
678
- const reqId = `claude-${sessionId}-${timestamp}`;
1581
+ let costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens);
1582
+ costUsd = applyClaudeModifiers(costUsd, model, usage, entry);
1583
+ const serverToolUse = usage.server_tool_use;
1584
+ if (serverToolUse?.web_search_requests) {
1585
+ costUsd += serverToolUse.web_search_requests * 0.01;
1586
+ }
1587
+ const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
1588
+ const reqId = `${agentName}-${sourceRequestId}`;
679
1589
  upsertRequest(db, {
680
1590
  id: reqId,
681
- agent: "claude",
1591
+ agent: agentName,
682
1592
  session_id: sessionId,
683
1593
  model,
684
1594
  input_tokens: inputTokens,
685
1595
  output_tokens: outputTokens,
686
1596
  cache_read_tokens: cacheReadTokens,
687
1597
  cache_create_tokens: cacheWriteTokens,
1598
+ cache_create_5m_tokens: cacheWrite5mTokens,
1599
+ cache_create_1h_tokens: cacheWrite1hTokens,
688
1600
  cost_usd: costUsd,
1601
+ cost_basis: defaultCostBasisForAgent(agentName),
689
1602
  duration_ms: 0,
690
1603
  timestamp,
691
- source_request_id: reqId
1604
+ source_request_id: sourceRequestId,
1605
+ machine_id: machineId
692
1606
  });
693
1607
  if (!touchedSessions.has(sessionId)) {
694
1608
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
@@ -697,14 +1611,15 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
697
1611
  const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
698
1612
  const session = {
699
1613
  id: sessionId,
700
- agent: "claude",
1614
+ agent: agentName,
701
1615
  project_path: detectedProject ? detectedProject.path : effectiveCwd,
702
1616
  project_name: detectedProject ? detectedProject.name : "",
703
1617
  started_at: timestamp,
704
1618
  ended_at: null,
705
1619
  total_cost_usd: 0,
706
1620
  total_tokens: 0,
707
- request_count: 0
1621
+ request_count: 0,
1622
+ machine_id: machineId
708
1623
  };
709
1624
  upsertSession(db, session);
710
1625
  }
@@ -712,7 +1627,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
712
1627
  }
713
1628
  totalRequests++;
714
1629
  }
715
- setIngestState(db, "claude", stateKey, fileMtime);
1630
+ setIngestState(db, agentName, stateKey, fileMtime);
716
1631
  totalFiles++;
717
1632
  }
718
1633
  }
@@ -721,79 +1636,254 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
721
1636
  }
722
1637
  return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
723
1638
  }
1639
+ function applyClaudeModifiers(costUsd, model, usage, entry) {
1640
+ let multiplier = 1;
1641
+ const speed = usage.speed ?? entry.message?.speed ?? entry.speed;
1642
+ if (speed === "fast" && model.includes("opus-4-6")) {
1643
+ multiplier *= 6;
1644
+ }
1645
+ const inferenceGeo = usage.inference_geo ?? entry.message?.inference_geo ?? entry.inference_geo;
1646
+ if (inferenceGeo && ["us", "us-only", "us_only"].includes(inferenceGeo) && supportsClaudeDataResidencyPricing(model)) {
1647
+ multiplier *= 1.1;
1648
+ }
1649
+ return costUsd * multiplier;
1650
+ }
1651
+ function supportsClaudeDataResidencyPricing(model) {
1652
+ const normalized = normalizeModelName(model);
1653
+ const match = normalized.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?(?:-|$)/);
1654
+ if (!match)
1655
+ return false;
1656
+ const major = Number(match[2]);
1657
+ const minor = match[3] ? Number(match[3]) : 0;
1658
+ return major > 4 || major === 4 && minor >= 6;
1659
+ }
724
1660
 
725
1661
  // src/ingest/codex.ts
726
1662
  init_database();
727
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1663
+ init_pricing();
1664
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
728
1665
  import { homedir as homedir3 } from "os";
729
1666
  import { join as join3, basename as basename2 } from "path";
730
- import { Database as Database2 } from "bun:sqlite";
731
- var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
732
- var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
733
- async function ingestCodex(db, verbose = false) {
734
- if (!existsSync3(CODEX_DB_PATH)) {
735
- if (verbose)
736
- console.log("Codex DB not found:", CODEX_DB_PATH);
737
- return { sessions: 0 };
738
- }
739
- let codexDb = null;
740
- let ingested = 0;
1667
+ import { Database as BunDatabase } from "bun:sqlite";
1668
+ var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
1669
+ var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
1670
+ var CODEX_INGEST_VERSION = "rollout-token-dedupe-v2";
1671
+ function codexDbPath() {
1672
+ return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
1673
+ }
1674
+ function codexConfigPath() {
1675
+ return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
1676
+ }
1677
+ function readCodexModel() {
1678
+ const configPath = codexConfigPath();
1679
+ if (!existsSync3(configPath))
1680
+ return "gpt-5-codex";
741
1681
  try {
742
- codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
743
- const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
1682
+ const content = readFileSync3(configPath, "utf-8");
1683
+ const match = content.match(/^model\s*=\s*"([^"]+)"/m);
1684
+ return match?.[1] ?? "gpt-5-codex";
1685
+ } catch {
1686
+ return "gpt-5-codex";
1687
+ }
1688
+ }
1689
+ function buildThreadQuery(codexDb) {
1690
+ const cols = new Set(codexDb.prepare(`PRAGMA table_info(threads)`).all().map((c) => c.name));
1691
+ const modelSelect = cols.has("model") ? "model" : "NULL AS model";
1692
+ const rolloutSelect = cols.has("rollout_path") ? "rollout_path" : "NULL AS rollout_path";
1693
+ const providerSelect = cols.has("model_provider") ? "model_provider" : "NULL AS model_provider";
1694
+ return `
1695
+ SELECT id, ${rolloutSelect}, cwd, created_at, updated_at, tokens_used, title,
1696
+ ${providerSelect}, ${modelSelect}
1697
+ FROM threads WHERE tokens_used > 0
1698
+ `;
1699
+ }
1700
+ function readTokenEvents(rolloutPath) {
1701
+ if (!rolloutPath || !existsSync3(rolloutPath))
1702
+ return [];
1703
+ const events = [];
1704
+ const seen = new Set;
1705
+ for (const line of readFileSync3(rolloutPath, "utf-8").split(`
1706
+ `)) {
1707
+ if (!line.trim())
1708
+ continue;
1709
+ let entry;
1710
+ try {
1711
+ entry = JSON.parse(line);
1712
+ } catch {
1713
+ continue;
1714
+ }
1715
+ if (!entry || typeof entry !== "object")
1716
+ continue;
1717
+ const payload = entry["payload"];
1718
+ if (!payload || payload["type"] !== "token_count")
1719
+ continue;
1720
+ const info = payload["info"];
1721
+ const usage = info?.["last_token_usage"];
1722
+ if (!usage)
1723
+ continue;
1724
+ const total = usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
1725
+ if (total <= 0)
1726
+ continue;
1727
+ const key = JSON.stringify(usage);
1728
+ if (seen.has(key))
1729
+ continue;
1730
+ seen.add(key);
1731
+ const timestamp = entry["timestamp"];
1732
+ events.push({ usage, timestamp: typeof timestamp === "string" ? timestamp : undefined });
1733
+ }
1734
+ return events;
1735
+ }
1736
+ function fallbackEvents(totalTokens) {
1737
+ const inputTokens = Math.floor(totalTokens * 0.6);
1738
+ return [{
1739
+ usage: {
1740
+ input_tokens: inputTokens,
1741
+ cached_input_tokens: 0,
1742
+ output_tokens: totalTokens - inputTokens,
1743
+ total_tokens: totalTokens
1744
+ }
1745
+ }];
1746
+ }
1747
+ async function ingestCodex(db, verbose = false) {
1748
+ const dbPath = codexDbPath();
1749
+ if (!existsSync3(dbPath)) {
1750
+ if (verbose)
1751
+ console.log("Codex DB not found:", dbPath);
1752
+ return { sessions: 0, requests: 0 };
1753
+ }
1754
+ const machineId = getMachineId();
1755
+ let codexDb = null;
1756
+ let ingested = 0;
1757
+ let requests = 0;
1758
+ try {
1759
+ codexDb = new BunDatabase(dbPath, { readonly: true });
1760
+ const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
744
1761
  for (const thread of threads) {
745
- const stateKey = thread.id;
746
- const processed = getIngestState(db, "codex", stateKey);
747
- if (processed === "done")
1762
+ const model = thread.model ?? readCodexModel();
1763
+ const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
1764
+ const processed = getIngestState(db, "codex", thread.id);
1765
+ if (processed === stateValue)
748
1766
  continue;
749
- const costUsd = 0;
750
1767
  const projectPath = thread.cwd ?? "";
751
1768
  const projectName = projectPath ? basename2(projectPath) : "unknown";
1769
+ const sessionId = `codex-${thread.id}`;
752
1770
  const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
753
1771
  const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
754
1772
  upsertSession(db, {
755
- id: `codex-${thread.id}`,
1773
+ id: sessionId,
756
1774
  agent: "codex",
757
1775
  project_path: projectPath,
758
1776
  project_name: projectName,
759
1777
  started_at: startedAt,
760
1778
  ended_at: endedAt,
761
- total_cost_usd: costUsd,
762
- total_tokens: thread.tokens_used,
763
- request_count: 1
1779
+ total_cost_usd: 0,
1780
+ total_tokens: 0,
1781
+ request_count: 0,
1782
+ machine_id: machineId
764
1783
  });
765
- setIngestState(db, "codex", stateKey, "done");
1784
+ const events = readTokenEvents(thread.rollout_path);
1785
+ const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
1786
+ db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
1787
+ tokenEvents.forEach((event, index) => {
1788
+ const usage = event.usage;
1789
+ const inputTotal = usage.input_tokens ?? 0;
1790
+ const cacheReadTokens = usage.cached_input_tokens ?? 0;
1791
+ const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
1792
+ const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
1793
+ const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
1794
+ const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
1795
+ const requestId = `${sessionId}-${index}`;
1796
+ upsertRequest(db, {
1797
+ id: requestId,
1798
+ agent: "codex",
1799
+ session_id: sessionId,
1800
+ model,
1801
+ input_tokens: inputTokens,
1802
+ output_tokens: outputTokens,
1803
+ cache_read_tokens: cacheReadTokens,
1804
+ cache_create_tokens: 0,
1805
+ cost_usd: costUsd,
1806
+ cost_basis: defaultCostBasisForAgent("codex"),
1807
+ duration_ms: 0,
1808
+ timestamp,
1809
+ source_request_id: requestId,
1810
+ machine_id: machineId
1811
+ });
1812
+ requests++;
1813
+ });
1814
+ rollupSession(db, sessionId);
1815
+ setIngestState(db, "codex", thread.id, stateValue);
766
1816
  ingested++;
767
1817
  if (verbose)
768
- console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens \u2192 $${costUsd.toFixed(4)}`);
1818
+ console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens on ${model}`);
769
1819
  }
770
1820
  } finally {
771
1821
  codexDb?.close();
772
1822
  }
773
- return { sessions: ingested };
1823
+ return { sessions: ingested, requests };
774
1824
  }
775
1825
 
776
1826
  // src/ingest/gemini.ts
777
1827
  init_database();
778
- import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
1828
+ init_pricing();
1829
+ import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync4, statSync as statSync3 } from "fs";
779
1830
  import { homedir as homedir4 } from "os";
780
- import { join as join4 } from "path";
781
- var GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
1831
+ import { join as join4, basename as basename3 } from "path";
1832
+ var DEFAULT_GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
1833
+ var DEFAULT_GEMINI_HISTORY_DIR = join4(homedir4(), ".gemini", "history");
1834
+ function geminiTmpDir() {
1835
+ return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
1836
+ }
1837
+ function geminiHistoryDir() {
1838
+ return process.env["HASNA_ECONOMY_GEMINI_HISTORY_DIR"] ?? DEFAULT_GEMINI_HISTORY_DIR;
1839
+ }
1840
+ function numberField(...values) {
1841
+ for (const value of values) {
1842
+ if (typeof value === "number" && Number.isFinite(value))
1843
+ return value;
1844
+ }
1845
+ return 0;
1846
+ }
1847
+ function listProjectDirs(...roots) {
1848
+ const dirs = new Set;
1849
+ for (const root of roots) {
1850
+ if (!existsSync4(root))
1851
+ continue;
1852
+ try {
1853
+ for (const entry of readdirSync3(root, { withFileTypes: true })) {
1854
+ if (entry.isDirectory())
1855
+ dirs.add(join4(root, entry.name));
1856
+ }
1857
+ } catch {}
1858
+ }
1859
+ return [...dirs];
1860
+ }
1861
+ function projectRoot(projectDir, chatData) {
1862
+ if (chatData.projectPath)
1863
+ return chatData.projectPath;
1864
+ if (chatData.project_path)
1865
+ return chatData.project_path;
1866
+ const rootFile = join4(projectDir, ".project_root");
1867
+ try {
1868
+ if (existsSync4(rootFile))
1869
+ return readFileSync4(rootFile, "utf-8").trim();
1870
+ } catch {}
1871
+ return "";
1872
+ }
782
1873
  async function ingestGemini(db, verbose) {
783
- if (!existsSync4(GEMINI_TMP_DIR)) {
1874
+ const tmpDir = geminiTmpDir();
1875
+ const historyDir = geminiHistoryDir();
1876
+ if (!existsSync4(tmpDir) && !existsSync4(historyDir)) {
784
1877
  if (verbose)
785
- console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
786
- return { sessions: 0 };
1878
+ console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
1879
+ return { sessions: 0, requests: 0 };
787
1880
  }
1881
+ const machineId = getMachineId();
788
1882
  let totalSessions = 0;
1883
+ let totalRequests = 0;
789
1884
  const touchedSessions = new Set;
790
- let projectHashDirs = [];
791
- try {
792
- projectHashDirs = readdirSync3(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join4(GEMINI_TMP_DIR, d.name));
793
- } catch {
794
- return { sessions: 0 };
795
- }
796
- for (const projectDir of projectHashDirs) {
1885
+ const projectDirs = listProjectDirs(tmpDir, historyDir);
1886
+ for (const projectDir of projectDirs) {
797
1887
  const chatsDir = join4(projectDir, "chats");
798
1888
  if (!existsSync4(chatsDir))
799
1889
  continue;
@@ -816,311 +1906,1483 @@ async function ingestGemini(db, verbose) {
816
1906
  continue;
817
1907
  let chatData;
818
1908
  try {
819
- chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
1909
+ chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
820
1910
  } catch {
821
1911
  continue;
822
1912
  }
823
- const sessionId = chatData.sessionId;
1913
+ const sessionId = chatData.sessionId ?? chatData.id ?? basename3(filePath, ".json");
824
1914
  if (!sessionId)
825
1915
  continue;
826
1916
  const startTime = chatData.startTime ?? new Date().toISOString();
1917
+ const projectPath = projectRoot(projectDir, chatData);
1918
+ const projectName = projectPath ? basename3(projectPath) : "";
827
1919
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
828
1920
  if (!existing) {
829
1921
  const session = {
830
1922
  id: sessionId,
831
1923
  agent: "gemini",
832
- project_path: "",
833
- project_name: "",
1924
+ project_path: projectPath,
1925
+ project_name: projectName,
834
1926
  started_at: startTime,
835
1927
  ended_at: chatData.lastUpdated ?? null,
836
1928
  total_cost_usd: 0,
837
1929
  total_tokens: 0,
838
- request_count: 0
1930
+ request_count: 0,
1931
+ machine_id: machineId
839
1932
  };
840
1933
  upsertSession(db, session);
841
- touchedSessions.add(sessionId);
842
1934
  totalSessions++;
843
1935
  }
1936
+ touchedSessions.add(sessionId);
1937
+ for (const [index, message] of (chatData.messages ?? []).entries()) {
1938
+ const usage = message.usage ?? message.usageMetadata ?? message.response?.usageMetadata;
1939
+ if (!usage)
1940
+ continue;
1941
+ const model = message.model ?? message.response?.modelVersion ?? message.response?.model ?? chatData.model;
1942
+ if (!model)
1943
+ continue;
1944
+ const toolUsePromptTokens = numberField(usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count);
1945
+ const inputTotal = numberField(usage.inputTokens, usage.input_tokens, usage.promptTokenCount, usage.prompt_token_count) + toolUsePromptTokens;
1946
+ const cacheReadTokens = numberField(usage.cachedInputTokens, usage.cache_read_tokens, usage.cachedContentTokenCount, usage.cached_content_token_count);
1947
+ const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
1948
+ const thoughtsTokens = numberField(usage.thoughtsTokenCount, usage.thoughts_token_count);
1949
+ const outputTokens = numberField(usage.outputTokens, usage.output_tokens, usage.candidatesTokenCount, usage.candidates_token_count) + thoughtsTokens;
1950
+ const totalTokens = numberField(usage.totalTokens, usage.total_tokens, usage.totalTokenCount, usage.total_token_count);
1951
+ if (inputTokens + outputTokens + cacheReadTokens + totalTokens === 0)
1952
+ continue;
1953
+ const computedCost = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
1954
+ const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
1955
+ const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
1956
+ const requestId = `gemini-${sessionId}-${message.id ?? index}`;
1957
+ upsertRequest(db, {
1958
+ id: requestId,
1959
+ agent: "gemini",
1960
+ session_id: sessionId,
1961
+ model,
1962
+ input_tokens: inputTokens,
1963
+ output_tokens: outputTokens,
1964
+ cache_read_tokens: cacheReadTokens,
1965
+ cache_create_tokens: 0,
1966
+ cost_usd: costUsd,
1967
+ cost_basis: defaultCostBasisForAgent("gemini"),
1968
+ duration_ms: 0,
1969
+ timestamp,
1970
+ source_request_id: message.id ?? requestId,
1971
+ machine_id: machineId
1972
+ });
1973
+ totalRequests++;
1974
+ }
844
1975
  setIngestState(db, "gemini", stateKey, fileMtime);
845
1976
  }
846
1977
  }
847
1978
  for (const sessionId of touchedSessions) {
848
1979
  rollupSession(db, sessionId);
849
1980
  }
850
- return { sessions: totalSessions };
1981
+ return { sessions: totalSessions, requests: totalRequests };
851
1982
  }
852
1983
 
853
- // src/mcp/index.ts
1984
+ // src/ingest/opencode.ts
1985
+ init_database();
854
1986
  init_pricing();
855
- var db = openDatabase();
856
- ensurePricingSeeded(db);
857
- var server = new Server({ name: "economy", version: "0.2.2" }, { capabilities: { tools: {} } });
858
- var fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
859
- var fmtTok = (n) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
860
- function fmtSession(s) {
861
- const id = String(s["id"] ?? "").slice(0, 8);
862
- const agent = String(s["agent"] ?? "");
863
- const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
864
- const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
865
- const tok = fmtTok(Number(s["total_tokens"] ?? 0));
866
- return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
867
- }
868
- var TOOLS = [
869
- { name: "get_cost_summary", description: "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all", inputSchema: { type: "object", properties: { period: { type: "string", enum: ["today", "week", "month", "year", "all"] } } } },
870
- { name: "get_sessions", description: "List sessions. Returns compact table. Params: agent, project, limit(20)", inputSchema: { type: "object", properties: { agent: { type: "string" }, project: { type: "string" }, limit: { type: "number" } } } },
871
- { name: "get_top_sessions", description: "Top sessions by cost. Params: n(10), agent", inputSchema: { type: "object", properties: { n: { type: "number" }, agent: { type: "string" } } } },
872
- { name: "get_model_breakdown", description: "Cost per model. No params.", inputSchema: { type: "object", properties: {} } },
873
- { name: "get_project_breakdown", description: "Cost per project. No params.", inputSchema: { type: "object", properties: {} } },
874
- { name: "get_budget_status", description: "Budget limits vs spend, percent used, alert flags. No params.", inputSchema: { type: "object", properties: {} } },
875
- { name: "get_daily", description: "Daily cost table by agent. Params: days(30)", inputSchema: { type: "object", properties: { days: { type: "number" } } } },
876
- { name: "get_session_detail", description: "Per-request breakdown of a single session. Params: session_id (prefix ok)", inputSchema: { type: "object", properties: { session_id: { type: "string" } }, required: ["session_id"] } },
877
- { name: "sync", description: "Ingest new cost data. sources: all|claude|codex|gemini", inputSchema: { type: "object", properties: { sources: { type: "string", enum: ["all", "claude", "codex", "gemini"] } } } },
878
- { name: "search_tools", description: "List tool names matching query. Use first to find relevant tools.", inputSchema: { type: "object", properties: { query: { type: "string" } } } },
879
- { name: "describe_tools", description: "Get param hints for specific tools by name.", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" } } }, required: ["names"] } },
880
- { name: "get_goals", description: "All spending goals with current progress. No params.", inputSchema: { type: "object", properties: {} } },
881
- { name: "set_goal", description: "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", inputSchema: { type: "object", properties: { period: { type: "string" }, limit_usd: { type: "number" }, project_path: { type: "string" }, agent: { type: "string" } }, required: ["period", "limit_usd"] } },
882
- { name: "remove_goal", description: "Delete a goal by id.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
883
- { name: "register_agent", description: "Register agent session.", inputSchema: { type: "object", properties: { name: { type: "string" }, session_id: { type: "string" } }, required: ["name"] } },
884
- { name: "heartbeat", description: "Update last_seen_at.", inputSchema: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] } },
885
- { name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } },
886
- { name: "list_agents", description: "List all registered agents.", inputSchema: { type: "object", properties: {} } },
887
- { name: "send_feedback", description: "Send feedback about this service.", inputSchema: { type: "object", properties: { message: { type: "string" }, email: { type: "string" }, category: { type: "string", enum: ["bug", "feature", "general"] } }, required: ["message"] } }
1987
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
1988
+ import { homedir as homedir5 } from "os";
1989
+ import { join as join5 } from "path";
1990
+ var OPENCODE_STORAGE = join5(homedir5(), ".local", "share", "opencode", "storage");
1991
+ function walkJsonFiles(dir, acc = []) {
1992
+ if (!existsSync5(dir))
1993
+ return acc;
1994
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
1995
+ const p = join5(dir, entry.name);
1996
+ if (entry.isDirectory())
1997
+ walkJsonFiles(p, acc);
1998
+ else if (entry.name.endsWith(".json"))
1999
+ acc.push(p);
2000
+ }
2001
+ return acc;
2002
+ }
2003
+ function parseSessionIdFromPath(filePath) {
2004
+ const parts = filePath.split("/");
2005
+ const msgIdx = parts.indexOf("message");
2006
+ if (msgIdx >= 0 && parts[msgIdx + 1])
2007
+ return parts[msgIdx + 1];
2008
+ const sessionIdx = parts.indexOf("session");
2009
+ if (sessionIdx >= 0 && parts[parts.length - 1]?.endsWith(".json")) {
2010
+ return parts[parts.length - 1].replace(/\.json$/, "");
2011
+ }
2012
+ return null;
2013
+ }
2014
+ async function ingestOpenCode(db, verbose = false) {
2015
+ const messageDir = join5(OPENCODE_STORAGE, "message");
2016
+ const files = walkJsonFiles(messageDir);
2017
+ let requests = 0;
2018
+ const touched = new Set;
2019
+ const machineId = getMachineId();
2020
+ const now = new Date().toISOString();
2021
+ for (const file of files) {
2022
+ const mtime = statSync4(file).mtimeMs;
2023
+ const stateKey = file;
2024
+ const prev = getIngestState(db, "opencode", stateKey);
2025
+ if (prev && Number(prev) >= mtime)
2026
+ continue;
2027
+ let parsed;
2028
+ try {
2029
+ parsed = JSON.parse(readFileSync5(file, "utf-8"));
2030
+ } catch {
2031
+ continue;
2032
+ }
2033
+ if (parsed.role !== "assistant")
2034
+ continue;
2035
+ const usage = parsed.usage;
2036
+ if (!usage)
2037
+ continue;
2038
+ const sessionId = parseSessionIdFromPath(file) ?? `opencode-${statSync4(file).ino}`;
2039
+ const model = normalizeModelName(parsed.model ?? "unknown");
2040
+ const input = usage.inputTokens ?? 0;
2041
+ const output = usage.outputTokens ?? 0;
2042
+ const cacheRead = usage.cacheReadTokens ?? 0;
2043
+ const cacheWrite = usage.cacheWriteTokens ?? 0;
2044
+ if (input + output + cacheRead + cacheWrite === 0 && !usage.cost)
2045
+ continue;
2046
+ const timestamp = usage && parsed.time?.created ? new Date(parsed.time.created).toISOString() : new Date(statSync4(file).mtime).toISOString();
2047
+ const sourceId = file.replace(OPENCODE_STORAGE, "");
2048
+ const reqId = `opencode-${sourceId}`;
2049
+ const costUsd = usage.cost ?? computeCostFromDb(db, model, input, output, cacheRead, cacheWrite, 0);
2050
+ upsertRequest(db, {
2051
+ id: reqId,
2052
+ agent: "opencode",
2053
+ session_id: sessionId,
2054
+ model,
2055
+ input_tokens: input,
2056
+ output_tokens: output,
2057
+ cache_read_tokens: cacheRead,
2058
+ cache_create_tokens: cacheWrite,
2059
+ cost_usd: costUsd,
2060
+ cost_basis: defaultCostBasisForAgent("opencode"),
2061
+ duration_ms: 0,
2062
+ timestamp,
2063
+ source_request_id: sourceId,
2064
+ machine_id: machineId,
2065
+ updated_at: now
2066
+ });
2067
+ requests++;
2068
+ if (!touched.has(sessionId)) {
2069
+ upsertSession(db, {
2070
+ id: sessionId,
2071
+ agent: "opencode",
2072
+ project_path: "",
2073
+ project_name: "",
2074
+ started_at: timestamp,
2075
+ ended_at: null,
2076
+ total_cost_usd: 0,
2077
+ total_tokens: 0,
2078
+ request_count: 0,
2079
+ machine_id: machineId,
2080
+ updated_at: now
2081
+ });
2082
+ touched.add(sessionId);
2083
+ }
2084
+ setIngestState(db, "opencode", stateKey, String(mtime));
2085
+ if (verbose)
2086
+ console.log(` opencode: ${reqId} ${model} $${costUsd.toFixed(4)}`);
2087
+ }
2088
+ for (const sid of touched)
2089
+ rollupSession(db, sid);
2090
+ return { files: files.length, requests, sessions: touched.size };
2091
+ }
2092
+
2093
+ // src/ingest/cursor.ts
2094
+ init_database();
2095
+ function getCursorSessionToken() {
2096
+ return process.env["CURSOR_SESSION_TOKEN"] ?? process.env["CURSOR_API_TOKEN"] ?? null;
2097
+ }
2098
+ async function cursorFetch(path, token) {
2099
+ try {
2100
+ const res = await fetch(`https://cursor.com${path}`, {
2101
+ headers: {
2102
+ Cookie: `WorkosCursorSessionToken=${token}`,
2103
+ Accept: "application/json"
2104
+ },
2105
+ signal: AbortSignal.timeout(1e4)
2106
+ });
2107
+ if (!res.ok)
2108
+ return null;
2109
+ return await res.json();
2110
+ } catch {
2111
+ return null;
2112
+ }
2113
+ }
2114
+ async function ingestCursor(db, verbose = false) {
2115
+ const token = getCursorSessionToken();
2116
+ if (!token) {
2117
+ if (verbose)
2118
+ console.log(" cursor: skipped \u2014 set CURSOR_SESSION_TOKEN");
2119
+ return { requests: 0, snapshots: 0 };
2120
+ }
2121
+ const today = new Date().toISOString().substring(0, 10);
2122
+ const prev = getIngestState(db, "cursor", `sync-${today}`);
2123
+ if (prev)
2124
+ return { requests: 0, snapshots: 0 };
2125
+ const machineId = getMachineId();
2126
+ const now = new Date().toISOString();
2127
+ let snapshots = 0;
2128
+ const usage = await cursorFetch("/api/usage", token);
2129
+ if (usage?.premiumRequests != null && usage.maxPremiumRequests) {
2130
+ upsertUsageSnapshot(db, {
2131
+ agent: "cursor",
2132
+ date: today,
2133
+ metric: "premium_requests_used",
2134
+ value: usage.premiumRequests,
2135
+ unit: "count",
2136
+ machine_id: machineId
2137
+ });
2138
+ upsertUsageSnapshot(db, {
2139
+ agent: "cursor",
2140
+ date: today,
2141
+ metric: "premium_requests_limit",
2142
+ value: usage.maxPremiumRequests,
2143
+ unit: "count",
2144
+ machine_id: machineId
2145
+ });
2146
+ snapshots += 2;
2147
+ }
2148
+ const summary = await cursorFetch("/api/usage-summary", token);
2149
+ const onDemand = summary?.individualUsage?.spend ?? summary?.teamUsage?.spend ?? 0;
2150
+ const included = summary?.individualUsage?.includedSpend ?? 0;
2151
+ if (onDemand > 0) {
2152
+ upsertUsageSnapshot(db, {
2153
+ agent: "cursor",
2154
+ date: today,
2155
+ metric: "on_demand_usd",
2156
+ value: onDemand,
2157
+ unit: "usd",
2158
+ machine_id: machineId
2159
+ });
2160
+ snapshots++;
2161
+ }
2162
+ if (included > 0) {
2163
+ upsertUsageSnapshot(db, {
2164
+ agent: "cursor",
2165
+ date: today,
2166
+ metric: "included_consumed_usd",
2167
+ value: included,
2168
+ unit: "usd",
2169
+ machine_id: machineId
2170
+ });
2171
+ snapshots++;
2172
+ }
2173
+ const sessionId = `cursor-${today}-${machineId}`;
2174
+ if (onDemand + included > 0) {
2175
+ upsertSession(db, {
2176
+ id: sessionId,
2177
+ agent: "cursor",
2178
+ project_path: "",
2179
+ project_name: "Cursor subscription",
2180
+ started_at: `${today}T00:00:00.000Z`,
2181
+ ended_at: now,
2182
+ total_cost_usd: onDemand + included,
2183
+ total_tokens: 0,
2184
+ request_count: 1,
2185
+ machine_id: machineId,
2186
+ updated_at: now
2187
+ });
2188
+ upsertRequest(db, {
2189
+ id: `cursor-${today}-${machineId}-usage`,
2190
+ agent: "cursor",
2191
+ session_id: sessionId,
2192
+ model: "cursor-subscription",
2193
+ input_tokens: 0,
2194
+ output_tokens: 0,
2195
+ cache_read_tokens: 0,
2196
+ cache_create_tokens: 0,
2197
+ cost_usd: onDemand + included,
2198
+ cost_basis: "subscription_included",
2199
+ duration_ms: 0,
2200
+ timestamp: now,
2201
+ source_request_id: today,
2202
+ machine_id: machineId,
2203
+ updated_at: now
2204
+ });
2205
+ rollupSession(db, sessionId);
2206
+ }
2207
+ setIngestState(db, "cursor", `sync-${today}`, now);
2208
+ if (verbose)
2209
+ console.log(` cursor: on-demand $${onDemand.toFixed(2)}, included $${included.toFixed(2)}`);
2210
+ return { requests: onDemand + included > 0 ? 1 : 0, snapshots };
2211
+ }
2212
+
2213
+ // src/ingest/pi.ts
2214
+ init_database();
2215
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
2216
+ import { homedir as homedir6 } from "os";
2217
+ import { join as join6 } from "path";
2218
+ var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join6(homedir6(), ".pi", "agent", "sessions");
2219
+ function walkSessions(dir, acc = []) {
2220
+ if (!existsSync6(dir))
2221
+ return acc;
2222
+ for (const entry of readdirSync5(dir, { withFileTypes: true })) {
2223
+ const p = join6(dir, entry.name);
2224
+ if (entry.isDirectory())
2225
+ walkSessions(p, acc);
2226
+ else if (entry.name.endsWith(".json"))
2227
+ acc.push(p);
2228
+ }
2229
+ return acc;
2230
+ }
2231
+ async function ingestPi(db, verbose = false) {
2232
+ const files = walkSessions(PI_SESSION_DIR);
2233
+ let requests = 0;
2234
+ const touched = new Set;
2235
+ const machineId = getMachineId();
2236
+ const now = new Date().toISOString();
2237
+ for (const file of files) {
2238
+ const mtime = statSync5(file).mtimeMs;
2239
+ const prev = getIngestState(db, "pi", file);
2240
+ if (prev && Number(prev) >= mtime)
2241
+ continue;
2242
+ let data;
2243
+ try {
2244
+ data = JSON.parse(readFileSync6(file, "utf-8"));
2245
+ } catch {
2246
+ continue;
2247
+ }
2248
+ const sessionId = data.id ?? file.replace(/\.json$/, "").split("/").pop() ?? `pi-${statSync5(file).ino}`;
2249
+ const turns = data.turns ?? data.messages?.filter((m) => m.role === "assistant") ?? [];
2250
+ for (let i = 0;i < turns.length; i++) {
2251
+ const turn = turns[i];
2252
+ const usage = turn.usage;
2253
+ if (!usage)
2254
+ continue;
2255
+ const input = usage.input ?? 0;
2256
+ const output = usage.output ?? 0;
2257
+ if (input + output === 0 && !usage.cost)
2258
+ continue;
2259
+ const model = turn.model ?? turn.provider ?? "unknown";
2260
+ const timestamp = turn.timestamp ?? new Date(statSync5(file).mtime).toISOString();
2261
+ const reqId = `pi-${sessionId}-${i}`;
2262
+ upsertRequest(db, {
2263
+ id: reqId,
2264
+ agent: "pi",
2265
+ session_id: sessionId,
2266
+ model,
2267
+ input_tokens: input,
2268
+ output_tokens: output,
2269
+ cache_read_tokens: usage.cacheRead ?? 0,
2270
+ cache_create_tokens: usage.cacheWrite ?? 0,
2271
+ cost_usd: usage.cost ?? 0,
2272
+ cost_basis: defaultCostBasisForAgent("pi"),
2273
+ duration_ms: 0,
2274
+ timestamp,
2275
+ source_request_id: `${sessionId}-${i}`,
2276
+ machine_id: machineId,
2277
+ updated_at: now
2278
+ });
2279
+ requests++;
2280
+ }
2281
+ if (turns.length > 0) {
2282
+ upsertSession(db, {
2283
+ id: sessionId,
2284
+ agent: "pi",
2285
+ project_path: "",
2286
+ project_name: "",
2287
+ started_at: turns[0]?.timestamp ?? now,
2288
+ ended_at: null,
2289
+ total_cost_usd: 0,
2290
+ total_tokens: 0,
2291
+ request_count: 0,
2292
+ machine_id: machineId,
2293
+ updated_at: now
2294
+ });
2295
+ touched.add(sessionId);
2296
+ }
2297
+ setIngestState(db, "pi", file, String(mtime));
2298
+ if (verbose)
2299
+ console.log(` pi: ${sessionId} (${turns.length} turns)`);
2300
+ }
2301
+ for (const sid of touched)
2302
+ rollupSession(db, sid);
2303
+ return { files: files.length, requests, sessions: touched.size };
2304
+ }
2305
+
2306
+ // src/ingest/hermes.ts
2307
+ init_database();
2308
+ import { existsSync as existsSync7, statSync as statSync6 } from "fs";
2309
+ import { homedir as homedir7 } from "os";
2310
+ import { join as join7 } from "path";
2311
+ var HERMES_DB = join7(homedir7(), ".hermes", "state.db");
2312
+ function mapCostBasis(billingMode) {
2313
+ if (billingMode === "subscription")
2314
+ return "subscription_included";
2315
+ if (billingMode === "api")
2316
+ return "metered_api";
2317
+ return defaultCostBasisForAgent("hermes");
2318
+ }
2319
+ async function ingestHermes(db, verbose = false) {
2320
+ if (!existsSync7(HERMES_DB)) {
2321
+ return { sessions: 0, requests: 0 };
2322
+ }
2323
+ const { Database: Sqlite } = await import("bun:sqlite");
2324
+ const hermes = new Sqlite(HERMES_DB, { readonly: true });
2325
+ const rows = hermes.prepare(`
2326
+ SELECT id, source, model, started_at, ended_at,
2327
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
2328
+ reasoning_tokens, estimated_cost_usd, actual_cost_usd, billing_mode, parent_session_id
2329
+ FROM sessions
2330
+ ORDER BY started_at DESC
2331
+ `).all();
2332
+ const stateKey = "state.db";
2333
+ const mtime = statSyncSafe(HERMES_DB);
2334
+ const prev = getIngestState(db, "hermes", stateKey);
2335
+ if (prev && Number(prev) >= mtime && rows.length === 0) {
2336
+ hermes.close();
2337
+ return { sessions: 0, requests: 0 };
2338
+ }
2339
+ const machineId = getMachineId();
2340
+ const now = new Date().toISOString();
2341
+ let requests = 0;
2342
+ for (const row of rows) {
2343
+ const sessionId = `hermes-${row.id}`;
2344
+ const startedAt = new Date(row.started_at * 1000).toISOString();
2345
+ const endedAt = row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null;
2346
+ const cost = row.actual_cost_usd ?? row.estimated_cost_usd ?? 0;
2347
+ const tokens = row.input_tokens + row.output_tokens + row.cache_read_tokens + row.cache_write_tokens + row.reasoning_tokens;
2348
+ upsertSession(db, {
2349
+ id: sessionId,
2350
+ agent: "hermes",
2351
+ project_path: row.source ?? "",
2352
+ project_name: row.source ?? "",
2353
+ started_at: startedAt,
2354
+ ended_at: endedAt,
2355
+ total_cost_usd: cost,
2356
+ total_tokens: tokens,
2357
+ request_count: 1,
2358
+ machine_id: machineId,
2359
+ updated_at: now
2360
+ });
2361
+ const reqId = `hermes-${row.id}-rollup`;
2362
+ upsertRequest(db, {
2363
+ id: reqId,
2364
+ agent: "hermes",
2365
+ session_id: sessionId,
2366
+ model: row.model ?? "unknown",
2367
+ input_tokens: row.input_tokens,
2368
+ output_tokens: row.output_tokens + row.reasoning_tokens,
2369
+ cache_read_tokens: row.cache_read_tokens,
2370
+ cache_create_tokens: row.cache_write_tokens,
2371
+ cost_usd: cost,
2372
+ cost_basis: mapCostBasis(row.billing_mode),
2373
+ duration_ms: 0,
2374
+ timestamp: endedAt ?? startedAt,
2375
+ source_request_id: row.id,
2376
+ machine_id: machineId,
2377
+ updated_at: now
2378
+ });
2379
+ requests++;
2380
+ rollupSession(db, sessionId);
2381
+ if (verbose)
2382
+ console.log(` hermes: ${sessionId} $${cost.toFixed(4)}`);
2383
+ }
2384
+ setIngestState(db, "hermes", stateKey, String(mtime));
2385
+ hermes.close();
2386
+ return { sessions: rows.length, requests };
2387
+ }
2388
+ function statSyncSafe(path) {
2389
+ try {
2390
+ return statSync6(path).mtimeMs;
2391
+ } catch {
2392
+ return 0;
2393
+ }
2394
+ }
2395
+
2396
+ // src/ingest/claude-quota.ts
2397
+ init_database();
2398
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
2399
+
2400
+ // src/lib/paths.ts
2401
+ import { homedir as homedir8 } from "os";
2402
+ import { join as join8 } from "path";
2403
+ function getHomeDir() {
2404
+ return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir8();
2405
+ }
2406
+ function agentPaths() {
2407
+ const home = getHomeDir();
2408
+ return {
2409
+ claudeProjects: join8(home, ".claude", "projects"),
2410
+ claudeCredentials: join8(home, ".claude", ".credentials.json"),
2411
+ takumiProjects: join8(home, ".takumi", "projects"),
2412
+ codexDir: join8(home, ".codex"),
2413
+ codexDb: join8(home, ".codex", "state_5.sqlite"),
2414
+ codexAuth: join8(home, ".codex", "auth.json"),
2415
+ codexConfig: join8(home, ".codex", "config.toml"),
2416
+ geminiTmp: join8(home, ".gemini", "tmp"),
2417
+ geminiHistory: join8(home, ".gemini", "history"),
2418
+ opencodeMessages: join8(home, ".local", "share", "opencode", "storage", "message"),
2419
+ piSessions: join8(home, ".pi", "agent", "sessions"),
2420
+ hermesDir: join8(home, ".hermes"),
2421
+ hermesDb: join8(home, ".hermes", "state.db")
2422
+ };
2423
+ }
2424
+
2425
+ // src/ingest/claude-quota.ts
2426
+ var CREDENTIALS_PATH = agentPaths().claudeCredentials;
2427
+ var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
2428
+ var OAUTH_BETA = "oauth-2025-04-20";
2429
+ function readClaudeToken() {
2430
+ const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
2431
+ if (fromEnv)
2432
+ return { token: fromEnv };
2433
+ if (!existsSync8(CREDENTIALS_PATH))
2434
+ return null;
2435
+ try {
2436
+ const creds = JSON.parse(readFileSync8(CREDENTIALS_PATH, "utf-8"));
2437
+ const oauth = creds.claudeAiOauth;
2438
+ if (!oauth?.accessToken)
2439
+ return null;
2440
+ return {
2441
+ token: oauth.accessToken,
2442
+ subscriptionType: oauth.subscriptionType,
2443
+ rateLimitTier: oauth.rateLimitTier
2444
+ };
2445
+ } catch {
2446
+ return null;
2447
+ }
2448
+ }
2449
+ function inferMonthlyFee(subscriptionType, rateLimitTier) {
2450
+ const tier = `${subscriptionType ?? ""} ${rateLimitTier ?? ""}`.toLowerCase();
2451
+ if (tier.includes("max") && tier.includes("20"))
2452
+ return 200;
2453
+ if (tier.includes("max"))
2454
+ return 100;
2455
+ if (tier.includes("pro"))
2456
+ return 20;
2457
+ if (tier.includes("team"))
2458
+ return 30;
2459
+ return 20;
2460
+ }
2461
+ async function fetchClaudeOAuthUsage(token) {
2462
+ try {
2463
+ const res = await fetch(USAGE_URL, {
2464
+ headers: {
2465
+ Authorization: `Bearer ${token}`,
2466
+ "anthropic-beta": OAUTH_BETA,
2467
+ Accept: "application/json"
2468
+ },
2469
+ signal: AbortSignal.timeout(1e4)
2470
+ });
2471
+ if (!res.ok)
2472
+ return null;
2473
+ return await res.json();
2474
+ } catch {
2475
+ return null;
2476
+ }
2477
+ }
2478
+ async function ingestClaudeQuota(db, verbose = false) {
2479
+ const auth = readClaudeToken();
2480
+ if (!auth) {
2481
+ if (verbose)
2482
+ console.log(" claude quota: skipped \u2014 no OAuth token (~/.claude/.credentials.json or CLAUDE_OAUTH_TOKEN)");
2483
+ return { snapshots: 0 };
2484
+ }
2485
+ const today = new Date().toISOString().substring(0, 10);
2486
+ const prev = getIngestState(db, "claude", `quota-${today}`);
2487
+ if (prev)
2488
+ return { snapshots: 0 };
2489
+ const usage = await fetchClaudeOAuthUsage(auth.token);
2490
+ if (!usage) {
2491
+ if (verbose)
2492
+ console.log(" claude quota: OAuth usage endpoint unavailable");
2493
+ return { snapshots: 0 };
2494
+ }
2495
+ const machineId = getMachineId();
2496
+ let snapshots = 0;
2497
+ const windows = [
2498
+ ["five_hour_utilization", usage.five_hour],
2499
+ ["seven_day_utilization", usage.seven_day],
2500
+ ["seven_day_sonnet_utilization", usage.seven_day_sonnet],
2501
+ ["seven_day_opus_utilization", usage.seven_day_opus]
2502
+ ];
2503
+ for (const [metric, window] of windows) {
2504
+ if (window?.utilization == null)
2505
+ continue;
2506
+ upsertUsageSnapshot(db, {
2507
+ agent: "claude",
2508
+ date: today,
2509
+ metric,
2510
+ value: Math.round(window.utilization * 1000) / 10,
2511
+ unit: "percent",
2512
+ machine_id: machineId
2513
+ });
2514
+ snapshots++;
2515
+ if (window.resets_at) {
2516
+ upsertUsageSnapshot(db, {
2517
+ agent: "claude",
2518
+ date: today,
2519
+ metric: `${metric}_resets_at`,
2520
+ value: Date.parse(window.resets_at),
2521
+ unit: "epoch_ms",
2522
+ machine_id: machineId
2523
+ });
2524
+ snapshots++;
2525
+ }
2526
+ }
2527
+ if (usage.extra_usage?.spend != null) {
2528
+ upsertUsageSnapshot(db, {
2529
+ agent: "claude",
2530
+ date: today,
2531
+ metric: "on_demand_usd",
2532
+ value: usage.extra_usage.spend,
2533
+ unit: "usd",
2534
+ machine_id: machineId
2535
+ });
2536
+ snapshots++;
2537
+ }
2538
+ if (usage.extra_usage?.limit != null) {
2539
+ upsertUsageSnapshot(db, {
2540
+ agent: "claude",
2541
+ date: today,
2542
+ metric: "on_demand_limit_usd",
2543
+ value: usage.extra_usage.limit,
2544
+ unit: "usd",
2545
+ machine_id: machineId
2546
+ });
2547
+ snapshots++;
2548
+ }
2549
+ const monthlyFee = inferMonthlyFee(auth.subscriptionType, auth.rateLimitTier);
2550
+ const now = new Date().toISOString();
2551
+ upsertSubscription(db, {
2552
+ id: "anthropic-claude-oauth",
2553
+ provider: "anthropic",
2554
+ agent: "claude",
2555
+ plan: auth.rateLimitTier ?? auth.subscriptionType ?? "claude_pro",
2556
+ monthly_fee_usd: monthlyFee,
2557
+ included_usage_usd: monthlyFee,
2558
+ billing_cycle_start: null,
2559
+ reset_policy: "monthly",
2560
+ active: 1,
2561
+ created_at: now,
2562
+ updated_at: now
2563
+ });
2564
+ setIngestState(db, "claude", `quota-${today}`, new Date().toISOString());
2565
+ if (verbose)
2566
+ console.log(` claude quota: ${snapshots} snapshots`);
2567
+ return { snapshots };
2568
+ }
2569
+
2570
+ // src/ingest/codex-quota.ts
2571
+ init_database();
2572
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
2573
+ var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
2574
+ function readCodexAuth() {
2575
+ const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
2576
+ if (fromEnv)
2577
+ return { token: fromEnv, authMode: "chatgpt" };
2578
+ const authPath = agentPaths().codexAuth;
2579
+ if (!existsSync9(authPath))
2580
+ return null;
2581
+ try {
2582
+ const auth = JSON.parse(readFileSync9(authPath, "utf-8"));
2583
+ const token = auth.tokens?.access_token;
2584
+ if (!token)
2585
+ return null;
2586
+ return {
2587
+ token,
2588
+ accountId: auth.tokens?.account_id,
2589
+ authMode: auth.auth_mode ?? "chatgpt"
2590
+ };
2591
+ } catch {
2592
+ return null;
2593
+ }
2594
+ }
2595
+ function planMonthlyFee(planType) {
2596
+ const plan = (planType ?? "").toLowerCase();
2597
+ if (plan.includes("pro"))
2598
+ return 200;
2599
+ if (plan.includes("plus"))
2600
+ return 20;
2601
+ if (plan.includes("team"))
2602
+ return 30;
2603
+ return 20;
2604
+ }
2605
+ async function fetchCodexUsage(token, accountId) {
2606
+ try {
2607
+ const headers = {
2608
+ Authorization: `Bearer ${token}`,
2609
+ Accept: "application/json",
2610
+ "User-Agent": "economy-cli"
2611
+ };
2612
+ if (accountId)
2613
+ headers["ChatGPT-Account-Id"] = accountId;
2614
+ const res = await fetch(WHAM_USAGE_URL, {
2615
+ headers,
2616
+ signal: AbortSignal.timeout(1e4)
2617
+ });
2618
+ if (!res.ok)
2619
+ return null;
2620
+ return await res.json();
2621
+ } catch {
2622
+ return null;
2623
+ }
2624
+ }
2625
+ async function ingestCodexQuota(db, verbose = false) {
2626
+ const auth = readCodexAuth();
2627
+ if (!auth) {
2628
+ if (verbose)
2629
+ console.log(" codex quota: skipped \u2014 no ~/.codex/auth.json or CODEX_OAUTH_TOKEN");
2630
+ return { snapshots: 0 };
2631
+ }
2632
+ if (auth.authMode === "api_key" || auth.authMode === "api") {
2633
+ if (verbose)
2634
+ console.log(" codex quota: skipped \u2014 API key mode (no subscription quota)");
2635
+ return { snapshots: 0 };
2636
+ }
2637
+ const today = new Date().toISOString().substring(0, 10);
2638
+ const prev = getIngestState(db, "codex", `quota-${today}`);
2639
+ if (prev)
2640
+ return { snapshots: 0 };
2641
+ const usage = await fetchCodexUsage(auth.token, auth.accountId);
2642
+ if (!usage) {
2643
+ if (verbose)
2644
+ console.log(" codex quota: wham/usage endpoint unavailable");
2645
+ return { snapshots: 0 };
2646
+ }
2647
+ const machineId = getMachineId();
2648
+ let snapshots = 0;
2649
+ const now = new Date().toISOString();
2650
+ const windows = [
2651
+ ["five_hour_utilization", usage.rate_limit?.primary_window],
2652
+ ["seven_day_utilization", usage.rate_limit?.secondary_window]
2653
+ ];
2654
+ for (const [metric, window] of windows) {
2655
+ if (window?.used_percent == null)
2656
+ continue;
2657
+ upsertUsageSnapshot(db, {
2658
+ agent: "codex",
2659
+ date: today,
2660
+ metric,
2661
+ value: window.used_percent,
2662
+ unit: "percent",
2663
+ machine_id: machineId
2664
+ });
2665
+ snapshots++;
2666
+ if (window.reset_at) {
2667
+ upsertUsageSnapshot(db, {
2668
+ agent: "codex",
2669
+ date: today,
2670
+ metric: `${metric}_resets_at`,
2671
+ value: window.reset_at * 1000,
2672
+ unit: "epoch_ms",
2673
+ machine_id: machineId
2674
+ });
2675
+ snapshots++;
2676
+ }
2677
+ }
2678
+ if (usage.credits?.balance != null) {
2679
+ upsertUsageSnapshot(db, {
2680
+ agent: "codex",
2681
+ date: today,
2682
+ metric: "credits_balance_usd",
2683
+ value: usage.credits.balance,
2684
+ unit: "usd",
2685
+ machine_id: machineId
2686
+ });
2687
+ snapshots++;
2688
+ }
2689
+ const monthlyFee = planMonthlyFee(usage.plan_type);
2690
+ upsertSubscription(db, {
2691
+ id: "openai-codex-oauth",
2692
+ provider: "openai",
2693
+ agent: "codex",
2694
+ plan: usage.plan_type ?? "chatgpt_plus",
2695
+ monthly_fee_usd: monthlyFee,
2696
+ included_usage_usd: monthlyFee,
2697
+ billing_cycle_start: null,
2698
+ reset_policy: "monthly",
2699
+ active: 1,
2700
+ created_at: now,
2701
+ updated_at: now
2702
+ });
2703
+ setIngestState(db, "codex", `quota-${today}`, now);
2704
+ if (verbose)
2705
+ console.log(` codex quota: ${snapshots} snapshots (${usage.plan_type ?? "unknown plan"})`);
2706
+ return { snapshots };
2707
+ }
2708
+
2709
+ // src/lib/sync-all.ts
2710
+ init_database();
2711
+
2712
+ // src/lib/cloud-sync.ts
2713
+ init_database();
2714
+ var CLOUD_TABLES = [
2715
+ "requests",
2716
+ "sessions",
2717
+ "projects",
2718
+ "budgets",
2719
+ "goals",
2720
+ "model_pricing",
2721
+ "billing_daily",
2722
+ "subscriptions",
2723
+ "usage_snapshots",
2724
+ "savings_daily",
2725
+ "machines",
2726
+ "ingest_state"
888
2727
  ];
889
- var TOOL_DESCRIPTIONS = {
890
- get_cost_summary: "period(today|week|month|year|all) \u2192 {total_usd, sessions, requests, tokens, summary}",
891
- get_sessions: "agent(claude|codex), project(partial), limit(20) \u2192 compact session table",
892
- get_top_sessions: "n(10), agent(claude|codex) \u2192 top sessions by cost",
893
- get_model_breakdown: "no params \u2192 model, requests, tokens, cost",
894
- get_project_breakdown: "no params \u2192 project_name, sessions, cost",
895
- get_budget_status: "no params \u2192 budget limits, current spend, percent_used, is_over_alert",
896
- get_daily: "days(30) \u2192 daily cost table grouped by date and agent",
897
- get_session_detail: "session_id(prefix ok) \u2192 per-request breakdown with model, tokens, cost",
898
- sync: "sources(all|claude|codex|gemini) \u2192 {files, requests, sessions} ingested",
899
- get_goals: "no params \u2192 period, scope, limit, spent, percent, status(ON TRACK/AT RISK/OVER)",
900
- set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? \u2192 creates/updates goal",
901
- remove_goal: "id \u2192 deletes goal"
902
- };
903
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
904
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
905
- const { name, arguments: args } = req.params;
906
- const a = args ?? {};
2728
+ function getCloudDatabaseUrl() {
2729
+ return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
2730
+ }
2731
+ function isCloudAutoEnabled() {
2732
+ return process.env["ECONOMY_CLOUD_AUTO"] === "1" || process.env["ECONOMY_CLOUD_AUTO"] === "true";
2733
+ }
2734
+ function getCloudPullIntervalMinutes() {
2735
+ const raw = process.env["ECONOMY_CLOUD_PULL_INTERVAL"];
2736
+ if (!raw)
2737
+ return 15;
2738
+ const n = Number(raw);
2739
+ return Number.isFinite(n) && n > 0 ? n : 15;
2740
+ }
2741
+ async function getCloudPg() {
2742
+ const url = getCloudDatabaseUrl();
2743
+ if (!url) {
2744
+ throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
2745
+ }
2746
+ const { PgAdapterAsync } = await import("@hasna/cloud");
2747
+ return new PgAdapterAsync(url);
2748
+ }
2749
+ async function runCloudMigrations(cloud) {
2750
+ const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
2751
+ for (const sql of PG_MIGRATIONS2) {
2752
+ await cloud.run(sql);
2753
+ }
2754
+ }
2755
+ function isCloudIncrementalEnabled() {
2756
+ return process.env["ECONOMY_CLOUD_INCREMENTAL"] === "1" || process.env["ECONOMY_CLOUD_INCREMENTAL"] === "true";
2757
+ }
2758
+ async function cloudPush(opts) {
2759
+ const { syncPush, incrementalSyncPush, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
2760
+ const cloud = await getCloudPg();
2761
+ const local = new SqliteAdapter(getDbPath());
2762
+ await runCloudMigrations(cloud);
2763
+ const tables = opts?.tables ?? [...CLOUD_TABLES];
2764
+ let rows = 0;
2765
+ if (isCloudIncrementalEnabled()) {
2766
+ ensureSyncMetaTable(local);
2767
+ const results = incrementalSyncPush(local, cloud, tables, { conflictColumn: "updated_at" });
2768
+ rows = results.reduce((s, r) => s + r.synced_rows, 0);
2769
+ } else {
2770
+ const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
2771
+ rows = results.reduce((s, r) => s + r.rowsWritten, 0);
2772
+ }
2773
+ touchMachineRegistry(local, "push");
2774
+ local.close();
2775
+ await cloud.close();
2776
+ return { rows, machine: getMachineId() };
2777
+ }
2778
+ async function cloudPull(opts) {
2779
+ const { syncPull, incrementalSyncPull, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
2780
+ const cloud = await getCloudPg();
2781
+ const local = new SqliteAdapter(getDbPath());
2782
+ await runCloudMigrations(cloud);
2783
+ const tables = opts?.tables ?? [...CLOUD_TABLES];
2784
+ let rows = 0;
2785
+ if (isCloudIncrementalEnabled()) {
2786
+ ensureSyncMetaTable(local);
2787
+ const results = incrementalSyncPull(cloud, local, tables, { conflictColumn: "updated_at" });
2788
+ rows = results.reduce((s, r) => s + r.synced_rows, 0);
2789
+ } else {
2790
+ const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
2791
+ rows = results.reduce((s, r) => s + r.rowsWritten, 0);
2792
+ }
2793
+ touchMachineRegistry(local, "pull");
2794
+ local.close();
2795
+ await cloud.close();
2796
+ setLastCloudPull();
2797
+ return { rows, machine: getMachineId() };
2798
+ }
2799
+ function setLastCloudPull(at = new Date().toISOString()) {
2800
+ const db = openDatabase();
2801
+ db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
2802
+ }
2803
+ function getLastCloudPull() {
2804
+ const db = openDatabase();
2805
+ const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
2806
+ return row?.value ?? null;
2807
+ }
2808
+ function shouldPullFromCloud() {
2809
+ if (!getCloudDatabaseUrl())
2810
+ return false;
2811
+ const last = getLastCloudPull();
2812
+ if (!last)
2813
+ return true;
2814
+ const ageMs = Date.now() - new Date(last).getTime();
2815
+ return ageMs > getCloudPullIntervalMinutes() * 60000;
2816
+ }
2817
+ async function maybePullFromCloud() {
2818
+ if (!shouldPullFromCloud())
2819
+ return false;
907
2820
  try {
908
- switch (name) {
909
- case "search_tools": {
910
- const q = a["query"]?.toLowerCase();
911
- const names = TOOLS.map((t) => t.name);
912
- const matches = q ? names.filter((n) => n.includes(q)) : names;
913
- return { content: [{ type: "text", text: matches.join(", ") }] };
914
- }
915
- case "describe_tools": {
916
- const names = a["names"] ?? [];
917
- const result = names.map((n) => `${n}: ${TOOL_DESCRIPTIONS[n] ?? "see tool schema"}`).join(`
918
- `);
919
- return { content: [{ type: "text", text: result }] };
920
- }
921
- case "get_cost_summary": {
922
- const period = a["period"] ?? "today";
923
- const s = querySummary(db, period);
924
- const text = [
925
- `period: ${period}`,
926
- `cost: ${fmtUsd(s.total_usd)}`,
927
- `sessions: ${s.sessions}`,
928
- `requests: ${s.requests.toLocaleString()}`,
929
- `tokens: ${fmtTok(s.tokens)}`,
930
- `summary: You've spent ${fmtUsd(s.total_usd)} ${period === "all" ? "total" : period} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
931
- ].join(`
2821
+ await cloudPull();
2822
+ return true;
2823
+ } catch {
2824
+ return false;
2825
+ }
2826
+ }
2827
+ async function maybePushAfterIngest() {
2828
+ if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
2829
+ return false;
2830
+ try {
2831
+ await cloudPush();
2832
+ return true;
2833
+ } catch {
2834
+ return false;
2835
+ }
2836
+ }
2837
+ function touchMachineRegistry(db, direction) {
2838
+ const now = new Date().toISOString();
2839
+ const machine = getMachineId();
2840
+ db.prepare(`
2841
+ INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
2842
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2843
+ ON CONFLICT(machine_id) DO UPDATE SET
2844
+ hostname = excluded.hostname,
2845
+ last_seen_at = excluded.last_seen_at,
2846
+ last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
2847
+ last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
2848
+ economy_version = excluded.economy_version,
2849
+ updated_at = excluded.updated_at
2850
+ `).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
2851
+ }
2852
+
2853
+ // src/lib/sync-all.ts
2854
+ async function syncAll(db, opts = {}) {
2855
+ const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
2856
+ const all = !anySpecific;
2857
+ await maybePullFromCloud();
2858
+ const result = { deduped: 0, cloudPulled: false, cloudPushed: false };
2859
+ if (all || opts.claude) {
2860
+ result.claude = await ingestClaude(db, opts.verbose);
2861
+ result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
2862
+ }
2863
+ if (all || opts.takumi)
2864
+ result.takumi = await ingestTakumi(db, opts.verbose);
2865
+ if (all || opts.codex) {
2866
+ result.codex = await ingestCodex(db, opts.verbose);
2867
+ result.codexQuota = await ingestCodexQuota(db, opts.verbose);
2868
+ }
2869
+ if (all || opts.gemini)
2870
+ result.gemini = await ingestGemini(db, opts.verbose);
2871
+ if (all || opts.opencode)
2872
+ result.opencode = await ingestOpenCode(db, opts.verbose);
2873
+ if (all || opts.cursor)
2874
+ result.cursor = await ingestCursor(db, opts.verbose);
2875
+ if (all || opts.pi)
2876
+ result.pi = await ingestPi(db, opts.verbose);
2877
+ if (all || opts.hermes)
2878
+ result.hermes = await ingestHermes(db, opts.verbose);
2879
+ result.deduped = dedupeRequests(db);
2880
+ result.cloudPushed = await maybePushAfterIngest();
2881
+ return result;
2882
+ }
2883
+
2884
+ // src/lib/agents.ts
2885
+ var AGENTS = [
2886
+ "claude",
2887
+ "takumi",
2888
+ "codex",
2889
+ "gemini",
2890
+ "opencode",
2891
+ "cursor",
2892
+ "pi",
2893
+ "hermes"
2894
+ ];
2895
+
2896
+ // src/mcp/server.ts
2897
+ init_database();
2898
+ init_pricing();
2899
+ init_pricing();
2900
+ var MCP_NAME = "economy";
2901
+ var DEFAULT_MCP_HTTP_PORT = 8815;
2902
+ function buildServer() {
2903
+ const db = openDatabase();
2904
+ ensurePricingSeeded(db);
2905
+ const server = new McpServer({
2906
+ name: MCP_NAME,
2907
+ version: packageMetadata.version
2908
+ });
2909
+ const _econAgents = new Map;
2910
+ const TOOL_NAMES = [
2911
+ "get_cost_summary",
2912
+ "get_sessions",
2913
+ "get_top_sessions",
2914
+ "get_model_breakdown",
2915
+ "get_project_breakdown",
2916
+ "get_budget_status",
2917
+ "set_budget",
2918
+ "remove_budget",
2919
+ "get_pricing",
2920
+ "set_pricing",
2921
+ "remove_pricing",
2922
+ "get_daily",
2923
+ "get_billing_summary",
2924
+ "get_session_detail",
2925
+ "sync",
2926
+ "search_tools",
2927
+ "describe_tools",
2928
+ "get_goals",
2929
+ "set_goal",
2930
+ "remove_goal",
2931
+ "list_machines",
2932
+ "register_agent",
2933
+ "heartbeat",
2934
+ "set_focus",
2935
+ "list_agents",
2936
+ "send_feedback"
2937
+ ];
2938
+ const TOOL_DESCRIPTIONS = {
2939
+ get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
2940
+ get_sessions: "agent(claude|takumi|codex|gemini), project(partial), machine?(hostname), limit(20) -> compact session table",
2941
+ get_top_sessions: "n(10), agent(claude|takumi|codex|gemini) -> top sessions by cost",
2942
+ list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
2943
+ get_model_breakdown: "no params -> model, requests, tokens, cost",
2944
+ get_project_breakdown: "no params -> project_name, sessions, cost",
2945
+ get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
2946
+ set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, alert_at_percent? -> create budget",
2947
+ remove_budget: "id -> delete budget",
2948
+ get_pricing: "no params -> model pricing rows with input, output, cache read/write, and cache storage rates",
2949
+ set_pricing: "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? -> create/update pricing",
2950
+ remove_pricing: "model -> delete pricing row",
2951
+ get_daily: "days(30) -> daily cost table grouped by date and agent",
2952
+ get_billing_summary: "period(today|yesterday|week|month|year|all) -> actual provider billing totals",
2953
+ get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
2954
+ sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
2955
+ search_tools: "query substring -> tool name list",
2956
+ describe_tools: "names[] -> one-line parameter hints",
2957
+ get_goals: "no params -> goal progress summary",
2958
+ set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
2959
+ remove_goal: "id -> delete goal",
2960
+ register_agent: "name, session_id? -> register agent session",
2961
+ heartbeat: "agent_id -> update last_seen_at",
2962
+ set_focus: "agent_id, project_id? -> set active project context",
2963
+ list_agents: "no params -> registered agent list",
2964
+ send_feedback: "message, email?, category? -> save feedback locally"
2965
+ };
2966
+ const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
2967
+ const fmtTok = (n) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
2968
+ function fmtSession(s) {
2969
+ const id = String(s["id"] ?? "").slice(0, 8);
2970
+ const agent = String(s["agent"] ?? "");
2971
+ const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
2972
+ const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
2973
+ const tok = fmtTok(Number(s["total_tokens"] ?? 0));
2974
+ return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
2975
+ }
2976
+ function text(text2) {
2977
+ return { content: [{ type: "text", text: text2 }] };
2978
+ }
2979
+ function textError(message) {
2980
+ return { content: [{ type: "text", text: message }], isError: true };
2981
+ }
2982
+ server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z.string().optional() }, async ({ query }) => {
2983
+ const q = query?.toLowerCase();
2984
+ const matches = q ? TOOL_NAMES.filter((name) => name.includes(q)) : [...TOOL_NAMES];
2985
+ return text(matches.join(", "));
2986
+ });
2987
+ server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z.array(z.string()) }, async ({ names }) => {
2988
+ const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
932
2989
  `);
933
- return { content: [{ type: "text", text }] };
934
- }
935
- case "get_sessions": {
936
- const sessions = querySessions(db, {
937
- agent: a["agent"],
938
- project: a["project"],
939
- limit: Number(a["limit"] ?? 20)
940
- });
941
- const lines = ["id agent cost tokens project"];
942
- for (const s of sessions)
943
- lines.push(fmtSession(s));
944
- return { content: [{ type: "text", text: lines.join(`
945
- `) }] };
946
- }
947
- case "get_top_sessions": {
948
- const sessions = queryTopSessions(db, Number(a["n"] ?? 10), a["agent"]);
949
- const lines = ["rank id agent cost tokens project"];
950
- sessions.forEach((s, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(s)}`));
951
- return { content: [{ type: "text", text: lines.join(`
952
- `) }] };
953
- }
954
- case "get_model_breakdown": {
955
- const rows = queryModelBreakdown(db);
956
- const lines = ["model reqs tokens cost"];
957
- for (const r of rows) {
958
- lines.push(`${String(r["model"]).slice(0, 30).padEnd(31)}${String(r["requests"]).padEnd(8)}${fmtTok(Number(r["total_tokens"])).padEnd(9)}${fmtUsd(Number(r["cost_usd"]))}`);
959
- }
960
- return { content: [{ type: "text", text: lines.join(`
961
- `) }] };
962
- }
963
- case "get_project_breakdown": {
964
- const rows = queryProjectBreakdown(db);
965
- const lines = ["project sessions tokens cost"];
966
- for (const r of rows) {
967
- const name2 = String(r["project_name"] || r["project_path"] || "\u2014").slice(0, 20);
968
- lines.push(`${name2.padEnd(21)}${String(r["sessions"]).padEnd(9)}${fmtTok(Number(r["total_tokens"])).padEnd(9)}${fmtUsd(Number(r["cost_usd"]))}`);
969
- }
970
- return { content: [{ type: "text", text: lines.join(`
971
- `) }] };
972
- }
973
- case "get_budget_status": {
974
- const budgets = getBudgetStatuses(db);
975
- if (budgets.length === 0)
976
- return { content: [{ type: "text", text: "No budgets set." }] };
977
- const lines = ["scope period spent limit used% status"];
978
- for (const b of budgets) {
979
- const scope = String(b["project_path"] ?? "global").slice(0, 20);
980
- const pct = Number(b["percent_used"]).toFixed(1);
981
- const status = b["is_over_limit"] ? "OVER" : b["is_over_alert"] ? "ALERT" : "OK";
982
- lines.push(`${scope.padEnd(21)}${String(b["period"]).padEnd(9)}${fmtUsd(Number(b["current_spend_usd"])).padEnd(11)}${fmtUsd(Number(b["limit_usd"])).padEnd(11)}${pct}%`.padEnd(49) + ` ${status}`);
983
- }
984
- return { content: [{ type: "text", text: lines.join(`
985
- `) }] };
986
- }
987
- case "get_daily": {
988
- const days = Number(a["days"] ?? 30);
989
- const rows = queryDailyBreakdown(db, days);
990
- const lines = ["date claude codex gemini total"];
991
- const byDate = new Map;
992
- for (const r of rows) {
993
- const d = String(r["date"]);
994
- const entry = byDate.get(d) ?? { claude: 0, codex: 0, gemini: 0 };
995
- if (r["agent"] === "claude")
996
- entry.claude += Number(r["cost_usd"]);
997
- else if (r["agent"] === "codex")
998
- entry.codex += Number(r["cost_usd"]);
999
- else if (r["agent"] === "gemini")
1000
- entry.gemini += Number(r["cost_usd"]);
1001
- byDate.set(d, entry);
1002
- }
1003
- for (const [date, costs] of [...byDate.entries()].sort()) {
1004
- const total = costs.claude + costs.codex + costs.gemini;
1005
- lines.push(`${date} ${fmtUsd(costs.claude).padEnd(11)}${fmtUsd(costs.codex).padEnd(11)}${fmtUsd(costs.gemini).padEnd(11)}${fmtUsd(total)}`);
1006
- }
1007
- return { content: [{ type: "text", text: lines.join(`
1008
- `) }] };
1009
- }
1010
- case "get_session_detail": {
1011
- const sid = String(a["session_id"] ?? "");
1012
- const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(sid, `${sid}%`);
1013
- if (!session)
1014
- return { content: [{ type: "text", text: `Session not found: ${sid}` }], isError: true };
1015
- const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
1016
- const lines = [
1017
- `session: ${String(session["id"]).slice(0, 16)}`,
1018
- `agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
1019
- `cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
1020
- "",
1021
- "time model input output cost"
1022
- ];
1023
- for (const r of requests) {
1024
- lines.push(`${String(r["timestamp"]).slice(11, 19)} ${String(r["model"]).slice(0, 22).padEnd(23)}${fmtTok(Number(r["input_tokens"])).padEnd(9)}${fmtTok(Number(r["output_tokens"])).padEnd(9)}${fmtUsd(Number(r["cost_usd"]))}`);
1025
- }
1026
- return { content: [{ type: "text", text: lines.join(`
1027
- `) }] };
1028
- }
1029
- case "sync": {
1030
- const sources = a["sources"] ?? "all";
1031
- const parts = [];
1032
- if (sources === "all" || sources === "claude") {
1033
- const r = await ingestClaude(db);
1034
- parts.push(`claude: ${r["files"]} files, ${r["requests"]} requests, ${r["sessions"]} sessions`);
1035
- }
1036
- if (sources === "all" || sources === "codex") {
1037
- const r = await ingestCodex(db);
1038
- parts.push(`codex: ${r["sessions"]} sessions`);
1039
- }
1040
- if (sources === "all" || sources === "gemini") {
1041
- const r = await ingestGemini(db);
1042
- parts.push(`gemini: ${r["sessions"]} sessions`);
1043
- }
1044
- return { content: [{ type: "text", text: parts.join(`
1045
- `) || "done" }] };
1046
- }
1047
- case "get_goals": {
1048
- const goals = getGoalStatuses(db);
1049
- if (goals.length === 0)
1050
- return { content: [{ type: "text", text: "No goals set." }] };
1051
- const lines = ["period scope limit spent used% status"];
1052
- for (const g of goals) {
1053
- const scope = String(g["project_path"] ?? g["agent"] ?? "global").slice(0, 20);
1054
- const pct = Number(g["percent_used"]).toFixed(1);
1055
- const status = g["is_over"] ? "OVER" : g["is_at_risk"] ? "AT RISK" : "ON TRACK";
1056
- lines.push(`${String(g["period"]).padEnd(9)}${scope.padEnd(21)}${fmtUsd(Number(g["limit_usd"])).padEnd(11)}${fmtUsd(Number(g["current_spend_usd"])).padEnd(11)}${pct}% ${status}`);
1057
- }
1058
- return { content: [{ type: "text", text: lines.join(`
1059
- `) }] };
1060
- }
1061
- case "set_goal": {
1062
- const { randomUUID } = await import("crypto");
1063
- const now = new Date().toISOString();
1064
- upsertGoal(db, {
1065
- id: randomUUID(),
1066
- period: String(a["period"] ?? "month"),
1067
- project_path: a["project_path"] ?? null,
1068
- agent: a["agent"] ?? null,
1069
- limit_usd: Number(a["limit_usd"]),
1070
- created_at: now,
1071
- updated_at: now
1072
- });
1073
- return { content: [{ type: "text", text: `Goal set: ${a["period"]} $${a["limit_usd"]}` }] };
1074
- }
1075
- case "remove_goal": {
1076
- deleteGoal(db, String(a["id"] ?? ""));
1077
- return { content: [{ type: "text", text: "Goal removed." }] };
1078
- }
1079
- case "register_agent": {
1080
- const n = String(args["name"] ?? "");
1081
- const ex = [..._econAgents.values()].find((x) => x.name === n);
1082
- if (ex) {
1083
- ex.last_seen_at = new Date().toISOString();
1084
- return { content: [{ type: "text", text: JSON.stringify(ex) }] };
1085
- }
1086
- const id = Math.random().toString(36).slice(2, 10);
1087
- const ag = { id, name: n, last_seen_at: new Date().toISOString() };
1088
- _econAgents.set(id, ag);
1089
- return { content: [{ type: "text", text: JSON.stringify(ag) }] };
1090
- }
1091
- case "heartbeat": {
1092
- const ag = _econAgents.get(String(args["agent_id"] ?? ""));
1093
- if (!ag)
1094
- return { content: [{ type: "text", text: `Agent not found` }], isError: true };
1095
- ag.last_seen_at = new Date().toISOString();
1096
- return { content: [{ type: "text", text: `\u2665 ${ag.name}` }] };
1097
- }
1098
- case "set_focus": {
1099
- const ag = _econAgents.get(String(args["agent_id"] ?? ""));
1100
- if (!ag)
1101
- return { content: [{ type: "text", text: `Agent not found` }], isError: true };
1102
- ag["project_id"] = args["project_id"];
1103
- return { content: [{ type: "text", text: String(args["project_id"] ? `Focus: ${args["project_id"]}` : "Focus cleared") }] };
1104
- }
1105
- case "list_agents": {
1106
- return { content: [{ type: "text", text: JSON.stringify([..._econAgents.values()]) }] };
1107
- }
1108
- case "send_feedback": {
1109
- try {
1110
- const pkg = require_package();
1111
- db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(String(a["message"]), a["email"] || null, a["category"] || "general", pkg.version);
1112
- return { content: [{ type: "text", text: "Feedback saved. Thank you!" }] };
1113
- } catch (e) {
1114
- return { content: [{ type: "text", text: String(e) }], isError: true };
1115
- }
1116
- }
1117
- default:
1118
- return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
2990
+ return text(result);
2991
+ });
2992
+ server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all. machine: filter by hostname.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), machine: z.string().optional() }, async ({ period, machine }) => {
2993
+ const resolved = period ?? "today";
2994
+ const s = querySummary(db, resolved, machine);
2995
+ const machineLabel = machine ? ` on ${machine}` : "";
2996
+ return text([
2997
+ `period: ${resolved}${machineLabel}`,
2998
+ `cost: ${fmtUsd(s.total_usd)}`,
2999
+ `sessions: ${s.sessions}`,
3000
+ `requests: ${s.requests.toLocaleString()}`,
3001
+ `tokens: ${fmtTok(s.tokens)}`,
3002
+ `summary: You've spent ${fmtUsd(s.total_usd)} ${resolved === "all" ? "total" : resolved}${machineLabel} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
3003
+ ].join(`
3004
+ `));
3005
+ });
3006
+ server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, machine, limit(20)", {
3007
+ agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional(),
3008
+ project: z.string().optional(),
3009
+ machine: z.string().optional(),
3010
+ limit: z.number().int().positive().max(100).optional()
3011
+ }, async ({ agent, project, machine, limit }) => {
3012
+ const sessions = querySessions(db, {
3013
+ agent,
3014
+ project,
3015
+ machine,
3016
+ limit: limit ?? 20
3017
+ });
3018
+ const lines = ["id agent cost tokens project"];
3019
+ for (const session of sessions)
3020
+ lines.push(fmtSession(session));
3021
+ return text(lines.join(`
3022
+ `));
3023
+ });
3024
+ server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
3025
+ n: z.number().int().positive().max(100).optional(),
3026
+ agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional()
3027
+ }, async ({ n, agent }) => {
3028
+ const sessions = queryTopSessions(db, n ?? 10, agent);
3029
+ const lines = ["rank id agent cost tokens project"];
3030
+ sessions.forEach((session, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(session)}`));
3031
+ return text(lines.join(`
3032
+ `));
3033
+ });
3034
+ server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
3035
+ const rows = queryModelBreakdown(db);
3036
+ const lines = ["model reqs tokens cost"];
3037
+ for (const row of rows) {
3038
+ lines.push(`${String(row["model"]).slice(0, 30).padEnd(31)}${String(row["requests"]).padEnd(8)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
3039
+ }
3040
+ return text(lines.join(`
3041
+ `));
3042
+ });
3043
+ server.tool("get_project_breakdown", "Cost per project. No params.", {}, async () => {
3044
+ const rows = queryProjectBreakdown(db);
3045
+ const lines = ["project sessions tokens cost"];
3046
+ for (const row of rows) {
3047
+ const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
3048
+ lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
3049
+ }
3050
+ return text(lines.join(`
3051
+ `));
3052
+ });
3053
+ server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
3054
+ const budgets = getBudgetStatuses(db);
3055
+ if (budgets.length === 0)
3056
+ return text("No budgets set.");
3057
+ const lines = ["scope period spent limit used% status"];
3058
+ for (const budget of budgets) {
3059
+ const scope = String(budget["project_path"] ?? "global").slice(0, 20);
3060
+ const pct = Number(budget["percent_used"]).toFixed(1);
3061
+ const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
3062
+ lines.push(`${scope.padEnd(21)}${String(budget["period"]).padEnd(9)}${fmtUsd(Number(budget["current_spend_usd"])).padEnd(11)}${fmtUsd(Number(budget["limit_usd"])).padEnd(11)}${pct}%`.padEnd(49) + ` ${status}`);
3063
+ }
3064
+ return text(lines.join(`
3065
+ `));
3066
+ });
3067
+ server.tool("set_budget", "Create a spending budget. period: daily|weekly|monthly. limit_usd must be positive. alert_at_percent defaults to 80.", {
3068
+ period: z.enum(["daily", "weekly", "monthly"]),
3069
+ limit_usd: z.number().positive(),
3070
+ project_path: z.string().optional(),
3071
+ agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional(),
3072
+ alert_at_percent: z.number().positive().max(100).optional()
3073
+ }, async ({ period, limit_usd, project_path, agent, alert_at_percent }) => {
3074
+ const now = new Date().toISOString();
3075
+ const id = randomUUID();
3076
+ upsertBudget(db, {
3077
+ id,
3078
+ project_path: project_path ?? null,
3079
+ agent: agent ?? null,
3080
+ period,
3081
+ limit_usd,
3082
+ alert_at_percent: alert_at_percent ?? 80,
3083
+ created_at: now,
3084
+ updated_at: now
3085
+ });
3086
+ return text(`Budget set: ${id}`);
3087
+ });
3088
+ server.tool("remove_budget", "Delete a budget by id.", { id: z.string() }, async ({ id }) => {
3089
+ deleteBudget(db, id);
3090
+ return text("Budget removed.");
3091
+ });
3092
+ server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage.", {}, async () => {
3093
+ const rows = listModelPricing(db);
3094
+ const lines = ["model input output cache-r cache-w cache-1h storage-h"];
3095
+ for (const row of rows) {
3096
+ lines.push(`${row.model.slice(0, 30).padEnd(31)}` + `${fmtUsd(row.input_per_1m).padEnd(9)}` + `${fmtUsd(row.output_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_read_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_1h_per_1m ?? 0).padEnd(9)}` + `${fmtUsd(row.cache_storage_per_1m_hour ?? 0)}`);
3097
+ }
3098
+ return text(lines.join(`
3099
+ `));
3100
+ });
3101
+ server.tool("set_pricing", "Create or update a model pricing row. Values are USD per 1M tokens except cache_storage_per_1m_hour.", {
3102
+ model: z.string().min(1),
3103
+ input_per_1m: z.number().nonnegative(),
3104
+ output_per_1m: z.number().nonnegative(),
3105
+ cache_read_per_1m: z.number().nonnegative().optional(),
3106
+ cache_write_per_1m: z.number().nonnegative().optional(),
3107
+ cache_write_1h_per_1m: z.number().nonnegative().optional(),
3108
+ cache_storage_per_1m_hour: z.number().nonnegative().optional()
3109
+ }, async (input) => {
3110
+ const model = input.model.trim();
3111
+ if (!model)
3112
+ return textError("model is required");
3113
+ upsertModelPricing(db, {
3114
+ model,
3115
+ input_per_1m: input.input_per_1m,
3116
+ output_per_1m: input.output_per_1m,
3117
+ cache_read_per_1m: input.cache_read_per_1m ?? 0,
3118
+ cache_write_per_1m: input.cache_write_per_1m ?? 0,
3119
+ cache_write_1h_per_1m: input.cache_write_1h_per_1m ?? 0,
3120
+ cache_storage_per_1m_hour: input.cache_storage_per_1m_hour ?? 0,
3121
+ updated_at: new Date().toISOString()
3122
+ });
3123
+ return text(`Pricing set: ${model}`);
3124
+ });
3125
+ server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z.string() }, async ({ model }) => {
3126
+ deleteModelPricing(db, model);
3127
+ return text("Pricing removed.");
3128
+ });
3129
+ server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
3130
+ const rows = queryDailyBreakdown(db, days ?? 30);
3131
+ const byDate = new Map;
3132
+ for (const row of rows) {
3133
+ const date = String(row["date"]);
3134
+ const entry = byDate.get(date) ?? { claude: 0, takumi: 0, codex: 0, gemini: 0 };
3135
+ if (row["agent"] === "claude")
3136
+ entry.claude += Number(row["cost_usd"]);
3137
+ else if (row["agent"] === "takumi")
3138
+ entry.takumi += Number(row["cost_usd"]);
3139
+ else if (row["agent"] === "codex")
3140
+ entry.codex += Number(row["cost_usd"]);
3141
+ else if (row["agent"] === "gemini")
3142
+ entry.gemini += Number(row["cost_usd"]);
3143
+ byDate.set(date, entry);
3144
+ }
3145
+ const lines = ["date claude takumi codex gemini total"];
3146
+ for (const [date, costs] of [...byDate.entries()].sort()) {
3147
+ const total = costs.claude + costs.takumi + costs.codex + costs.gemini;
3148
+ lines.push(`${date} ${fmtUsd(costs.claude).padEnd(11)}${fmtUsd(costs.takumi).padEnd(11)}${fmtUsd(costs.codex).padEnd(11)}${fmtUsd(costs.gemini).padEnd(11)}${fmtUsd(total)}`);
3149
+ }
3150
+ return text(lines.join(`
3151
+ `));
3152
+ });
3153
+ server.tool("get_billing_summary", "Actual provider billing totals from admin API sync. Params: period(today|yesterday|week|month|year|all)", { period: z.enum(["today", "yesterday", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
3154
+ const summary = queryBillingSummary(db, period ?? "month");
3155
+ const lines = ["provider billed"];
3156
+ for (const [provider, cost] of Object.entries(summary.by_provider)) {
3157
+ lines.push(`${provider.padEnd(11)}${fmtUsd(cost)}`);
3158
+ }
3159
+ lines.push(`total ${fmtUsd(summary.total_usd)}`);
3160
+ return text(lines.join(`
3161
+ `));
3162
+ });
3163
+ server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
3164
+ const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(session_id, `${session_id}%`);
3165
+ if (!session)
3166
+ return textError(`Session not found: ${session_id}`);
3167
+ const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
3168
+ const lines = [
3169
+ `session: ${String(session["id"]).slice(0, 16)}`,
3170
+ `agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
3171
+ `cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
3172
+ "",
3173
+ "time model input output cache-r cache-5m cache-1h cost"
3174
+ ];
3175
+ for (const request of requests) {
3176
+ lines.push(`${String(request["timestamp"]).slice(11, 19)} ` + `${String(request["model"]).slice(0, 22).padEnd(23)}` + `${fmtTok(Number(request["input_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["output_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_read_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_create_5m_tokens"] ?? request["cache_create_tokens"] ?? 0)).padEnd(9)}` + `${fmtTok(Number(request["cache_create_1h_tokens"] ?? 0)).padEnd(9)}` + `${fmtUsd(Number(request["cost_usd"]))}`);
3177
+ }
3178
+ return text(lines.join(`
3179
+ `));
3180
+ });
3181
+ server.tool("sync", `Ingest new cost data. sources: all|${AGENTS.join("|")}`, { sources: z.enum(["all", ...AGENTS]).optional() }, async ({ sources }) => {
3182
+ const selected = sources ?? "all";
3183
+ const opts = selected === "all" ? {} : { [selected]: true };
3184
+ const result = await syncAll(db, opts);
3185
+ return text(JSON.stringify(result, null, 2));
3186
+ });
3187
+ server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month", { period: z.enum(["today", "week", "month"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
3188
+ const p = period ?? "month";
3189
+ const snaps = queryUsageSnapshots(db, { agent });
3190
+ const summary = querySummary(db, p, undefined, true);
3191
+ return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
3192
+ });
3193
+ server.tool("get_savings", "Subscription vs API savings summary", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
3194
+ return text(JSON.stringify(querySavingsSummary(db, period ?? "month", agent), null, 2));
3195
+ });
3196
+ server.tool("estimate_cost", "Pre-flight cost estimate for token counts", { model: z.string(), input_tokens: z.number().optional(), output_tokens: z.number().optional() }, async ({ model, input_tokens, output_tokens }) => {
3197
+ const cost = computeCostFromDb(db, model, input_tokens ?? 0, output_tokens ?? 0, 0, 0, 0);
3198
+ return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
3199
+ });
3200
+ server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
3201
+ const goals = getGoalStatuses(db);
3202
+ if (goals.length === 0)
3203
+ return text("No goals set.");
3204
+ const lines = ["period scope limit spent used% status"];
3205
+ for (const goal of goals) {
3206
+ const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
3207
+ const pct = Number(goal["percent_used"]).toFixed(1);
3208
+ const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
3209
+ lines.push(`${String(goal["period"]).padEnd(9)}${scope.padEnd(21)}${fmtUsd(Number(goal["limit_usd"])).padEnd(11)}${fmtUsd(Number(goal["current_spend_usd"])).padEnd(11)}${pct}% ${status}`);
3210
+ }
3211
+ return text(lines.join(`
3212
+ `));
3213
+ });
3214
+ server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
3215
+ period: z.enum(["day", "week", "month", "year"]),
3216
+ limit_usd: z.number().positive(),
3217
+ project_path: z.string().optional(),
3218
+ agent: z.string().optional()
3219
+ }, async ({ period, limit_usd, project_path, agent }) => {
3220
+ const now = new Date().toISOString();
3221
+ upsertGoal(db, {
3222
+ id: randomUUID(),
3223
+ period,
3224
+ project_path: project_path ?? null,
3225
+ agent: agent ?? null,
3226
+ limit_usd,
3227
+ created_at: now,
3228
+ updated_at: now
3229
+ });
3230
+ return text(`Goal set: ${period} $${limit_usd}`);
3231
+ });
3232
+ server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
3233
+ deleteGoal(db, id);
3234
+ return text("Goal removed.");
3235
+ });
3236
+ server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
3237
+ const machines = listMachines(db);
3238
+ if (machines.length === 0)
3239
+ return text(`No machine data yet. Current machine: ${getMachineId()}`);
3240
+ const lines = ["machine sessions requests cost last_active"];
3241
+ for (const m of machines) {
3242
+ lines.push(`${m.machine_id.padEnd(17)}${String(m.sessions).padEnd(10)}${String(m.requests).padEnd(10)}${fmtUsd(m.total_cost_usd).padEnd(12)}${m.last_active?.substring(0, 16) ?? "\u2014"}`);
3243
+ }
3244
+ lines.push(`
3245
+ current machine: ${getMachineId()}`);
3246
+ return text(lines.join(`
3247
+ `));
3248
+ });
3249
+ server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
3250
+ const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
3251
+ if (existing) {
3252
+ existing.last_seen_at = new Date().toISOString();
3253
+ return text(JSON.stringify(existing));
3254
+ }
3255
+ const id = Math.random().toString(36).slice(2, 10);
3256
+ const agent = { id, name, last_seen_at: new Date().toISOString() };
3257
+ _econAgents.set(id, agent);
3258
+ return text(JSON.stringify(agent));
3259
+ });
3260
+ server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
3261
+ const agent = _econAgents.get(agent_id);
3262
+ if (!agent)
3263
+ return textError("Agent not found");
3264
+ agent.last_seen_at = new Date().toISOString();
3265
+ return text(`\u2665 ${agent.name}`);
3266
+ });
3267
+ server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
3268
+ const agent = _econAgents.get(agent_id);
3269
+ if (!agent)
3270
+ return textError("Agent not found");
3271
+ agent.project_id = project_id ?? undefined;
3272
+ return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
3273
+ });
3274
+ server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
3275
+ server.tool("send_feedback", "Send feedback about this service.", {
3276
+ message: z.string(),
3277
+ email: z.string().optional(),
3278
+ category: z.enum(["bug", "feature", "general"]).optional()
3279
+ }, async ({ message, email, category }) => {
3280
+ try {
3281
+ db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(message, email ?? null, category ?? "general", packageMetadata.version);
3282
+ return text("Feedback saved. Thank you!");
3283
+ } catch (error) {
3284
+ return textError(String(error));
3285
+ }
3286
+ });
3287
+ registerCloudTools(server, MCP_NAME, {
3288
+ dbPath: getDbPath(),
3289
+ migrations: PG_MIGRATIONS
3290
+ });
3291
+ return server;
3292
+ }
3293
+
3294
+ // src/mcp/http.ts
3295
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3296
+ function isHttpMode(argv = process.argv.slice(2)) {
3297
+ return argv.includes("--http") || process.env["MCP_HTTP"] === "1";
3298
+ }
3299
+ function resolveHttpPort(argv = process.argv.slice(2)) {
3300
+ for (let i = 0;i < argv.length; i++) {
3301
+ const arg = argv[i];
3302
+ if (arg === "--port" || arg === "-p") {
3303
+ const raw = argv[i + 1];
3304
+ if (!raw)
3305
+ throw new Error(`Invalid port: ${raw ?? ""}`);
3306
+ return parsePort(raw, "port");
1119
3307
  }
1120
- } catch (e) {
1121
- return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
1122
3308
  }
3309
+ const fromEnv = process.env["MCP_HTTP_PORT"];
3310
+ if (fromEnv)
3311
+ return parsePort(fromEnv, "MCP_HTTP_PORT");
3312
+ return DEFAULT_MCP_HTTP_PORT;
3313
+ }
3314
+ function parsePort(raw, label) {
3315
+ const value = Number(raw);
3316
+ if (!Number.isInteger(value) || value < 1 || value > 65535) {
3317
+ throw new Error(`Invalid ${label}: ${raw}`);
3318
+ }
3319
+ return value;
3320
+ }
3321
+ async function handleMcpHttpRequest(req) {
3322
+ const url = new URL(req.url);
3323
+ if (url.pathname === "/health" && req.method === "GET") {
3324
+ return Response.json({ status: "ok", name: MCP_NAME });
3325
+ }
3326
+ if (url.pathname === "/mcp") {
3327
+ const transport = new WebStandardStreamableHTTPServerTransport({
3328
+ sessionIdGenerator: undefined
3329
+ });
3330
+ const server = buildServer();
3331
+ await server.connect(transport);
3332
+ return transport.handleRequest(req);
3333
+ }
3334
+ return new Response("Not Found", { status: 404 });
3335
+ }
3336
+ function startHttpServer(options = {}) {
3337
+ const port = options.port ?? DEFAULT_MCP_HTTP_PORT;
3338
+ const hostname2 = options.hostname ?? "127.0.0.1";
3339
+ const log = options.log ?? console.error;
3340
+ const server = Bun.serve({
3341
+ port,
3342
+ hostname: hostname2,
3343
+ fetch: handleMcpHttpRequest
3344
+ });
3345
+ const address = `http://${hostname2}:${server.port}`;
3346
+ log(`${MCP_NAME}-mcp HTTP listening on ${address}/mcp (health: ${address}/health)`);
3347
+ return server;
3348
+ }
3349
+
3350
+ // src/mcp/index.ts
3351
+ function printHelp() {
3352
+ console.log(`Usage: economy-mcp [options]
3353
+
3354
+ Runs the ${packageMetadata.name} MCP server (stdio by default).
3355
+
3356
+ Options:
3357
+ --http Serve MCP over Streamable HTTP on 127.0.0.1
3358
+ -p, --port <port> HTTP port (default: MCP_HTTP_PORT or 8815)
3359
+ -V, --version output the version number
3360
+ -h, --help display help for command
3361
+
3362
+ Environment:
3363
+ MCP_HTTP=1 Enable HTTP mode
3364
+ MCP_HTTP_PORT Override default HTTP port (8815)`);
3365
+ }
3366
+ var args = process.argv.slice(2);
3367
+ if (args.includes("--help") || args.includes("-h")) {
3368
+ printHelp();
3369
+ process.exit(0);
3370
+ }
3371
+ if (args.includes("--version") || args.includes("-V")) {
3372
+ console.log(packageMetadata.version);
3373
+ process.exit(0);
3374
+ }
3375
+ async function main() {
3376
+ if (isHttpMode(args)) {
3377
+ startHttpServer({ port: resolveHttpPort(args) });
3378
+ await new Promise(() => {});
3379
+ return;
3380
+ }
3381
+ const server = buildServer();
3382
+ const transport = new StdioServerTransport;
3383
+ await server.connect(transport);
3384
+ }
3385
+ main().catch((error) => {
3386
+ console.error("MCP server error:", error);
3387
+ process.exit(1);
1123
3388
  });
1124
- var _econAgents = new Map;
1125
- var transport = new StdioServerTransport;
1126
- await server.connect(transport);