@hasna/economy 0.4.0 → 0.5.0

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 (202) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +12 -9
  3. package/dist/chunks/agent-registry-store-tpnhx3wj.js +39 -0
  4. package/dist/chunks/billing-bkpxy4kv.js +13 -0
  5. package/dist/chunks/config-j9mv3ehk.js +15 -0
  6. package/dist/chunks/index-2hwzmfgv.js +3407 -0
  7. package/dist/chunks/index-axh32d30.js +1313 -0
  8. package/dist/chunks/index-emvs5ph2.js +2085 -0
  9. package/dist/chunks/index-ez9thsr7.js +3440 -0
  10. package/dist/chunks/index-hesxq90g.js +62 -0
  11. package/dist/chunks/index-k540hfww.js +258 -0
  12. package/dist/chunks/index-kgy9b6bf.js +451 -0
  13. package/dist/chunks/index-kn5572kd.js +124 -0
  14. package/dist/chunks/index-p35cyhc4.js +4 -0
  15. package/dist/chunks/index-w3bvv0ns.js +26 -0
  16. package/dist/chunks/index-zpvea08j.js +3372 -0
  17. package/dist/chunks/menubar-6xtkt17h.js +296 -0
  18. package/dist/chunks/open-projects-6z4pq6mx.js +39 -0
  19. package/dist/chunks/pricing-f80p81gr.js +29 -0
  20. package/dist/chunks/pricing-vf2pycmh.js +30 -0
  21. package/dist/chunks/pricing-y21mcjsa.js +30 -0
  22. package/dist/chunks/serve-0k69e4h9.js +4655 -0
  23. package/dist/chunks/sqlite-store-501f32ts.js +258 -0
  24. package/dist/chunks/sqlite-store-fq4natnf.js +256 -0
  25. package/dist/chunks/sqlite-store-st99garx.js +258 -0
  26. package/dist/chunks/third-party-sqlite-5e85fxa2.js +11 -0
  27. package/dist/chunks/tui-7f8f951w.js +22 -0
  28. package/dist/chunks/watch-mm1j39vy.js +196 -0
  29. package/dist/chunks/webhooks-k4fh2t9c.js +109 -0
  30. package/dist/cli/commands/brief.d.ts.map +1 -1
  31. package/dist/cli/commands/extras.d.ts.map +1 -1
  32. package/dist/cli/commands/journal.d.ts +3 -0
  33. package/dist/cli/commands/journal.d.ts.map +1 -0
  34. package/dist/cli/commands/menubar.d.ts +97 -4
  35. package/dist/cli/commands/menubar.d.ts.map +1 -1
  36. package/dist/cli/commands/tui.d.ts +1 -1
  37. package/dist/cli/commands/tui.d.ts.map +1 -1
  38. package/dist/cli/commands/watch.d.ts.map +1 -1
  39. package/dist/cli/index.js +1358 -10114
  40. package/dist/db/cloud.d.ts +7 -9
  41. package/dist/db/cloud.d.ts.map +1 -1
  42. package/dist/db/database.d.ts +14 -12
  43. package/dist/db/database.d.ts.map +1 -1
  44. package/dist/db/ingest-concurrency-worker.d.ts +2 -0
  45. package/dist/db/ingest-concurrency-worker.d.ts.map +1 -0
  46. package/dist/db/ingest-errors.d.ts +9 -0
  47. package/dist/db/ingest-errors.d.ts.map +1 -0
  48. package/dist/db/ingest-schema.d.ts +3 -0
  49. package/dist/db/ingest-schema.d.ts.map +1 -0
  50. package/dist/db/ingest-specs.d.ts +8 -0
  51. package/dist/db/ingest-specs.d.ts.map +1 -0
  52. package/dist/db/ingest-test-contract.d.ts +3 -0
  53. package/dist/db/ingest-test-contract.d.ts.map +1 -0
  54. package/dist/db/ingest-validation.d.ts +12 -0
  55. package/dist/db/ingest-validation.d.ts.map +1 -0
  56. package/dist/db/ingest.d.ts +27 -0
  57. package/dist/db/ingest.d.ts.map +1 -0
  58. package/dist/db/legacy-schema.d.ts +7 -0
  59. package/dist/db/legacy-schema.d.ts.map +1 -0
  60. package/dist/db/observation-contract.d.ts +3 -0
  61. package/dist/db/observation-contract.d.ts.map +1 -0
  62. package/dist/db/observation-provenance.d.ts +23 -0
  63. package/dist/db/observation-provenance.d.ts.map +1 -0
  64. package/dist/db/observation-schema.d.ts +24 -0
  65. package/dist/db/observation-schema.d.ts.map +1 -0
  66. package/dist/db/pg-connection.d.ts +10 -0
  67. package/dist/db/pg-connection.d.ts.map +1 -0
  68. package/dist/db/pg-migrate.d.ts +4 -9
  69. package/dist/db/pg-migrate.d.ts.map +1 -1
  70. package/dist/db/pg-migrations.d.ts.map +1 -1
  71. package/dist/db/pg-protocol.d.ts +28 -0
  72. package/dist/db/pg-protocol.d.ts.map +1 -0
  73. package/dist/db/sqlite-store.d.ts +4 -0
  74. package/dist/db/sqlite-store.d.ts.map +1 -0
  75. package/dist/db/sync-pg.d.ts +17 -21
  76. package/dist/db/sync-pg.d.ts.map +1 -1
  77. package/dist/db/third-party-sqlite.d.ts +10 -0
  78. package/dist/db/third-party-sqlite.d.ts.map +1 -0
  79. package/dist/index.d.ts +3 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +1161 -2464
  82. package/dist/ingest/claude-quota.d.ts.map +1 -1
  83. package/dist/ingest/claude.d.ts.map +1 -1
  84. package/dist/ingest/codex-quota.d.ts.map +1 -1
  85. package/dist/ingest/codex.d.ts +25 -0
  86. package/dist/ingest/codex.d.ts.map +1 -1
  87. package/dist/ingest/collection-outcome.d.ts +17 -0
  88. package/dist/ingest/collection-outcome.d.ts.map +1 -0
  89. package/dist/ingest/cursor.d.ts.map +1 -1
  90. package/dist/ingest/gemini.d.ts.map +1 -1
  91. package/dist/ingest/hermes.d.ts +1 -1
  92. package/dist/ingest/hermes.d.ts.map +1 -1
  93. package/dist/ingest/loops.d.ts.map +1 -1
  94. package/dist/ingest/opencode.d.ts +1 -1
  95. package/dist/ingest/opencode.d.ts.map +1 -1
  96. package/dist/ingest/pi.d.ts +1 -1
  97. package/dist/ingest/pi.d.ts.map +1 -1
  98. package/dist/ingest/quota-observation.d.ts +16 -0
  99. package/dist/ingest/quota-observation.d.ts.map +1 -0
  100. package/dist/ingest/source-time.d.ts +4 -0
  101. package/dist/ingest/source-time.d.ts.map +1 -0
  102. package/dist/lib/accounts-store.d.ts +39 -13
  103. package/dist/lib/accounts-store.d.ts.map +1 -1
  104. package/dist/lib/analytics.d.ts.map +1 -1
  105. package/dist/lib/api-display-url.d.ts +1 -1
  106. package/dist/lib/autosync-gate.d.ts +12 -5
  107. package/dist/lib/autosync-gate.d.ts.map +1 -1
  108. package/dist/lib/billing-diff.d.ts +18 -40
  109. package/dist/lib/billing-diff.d.ts.map +1 -1
  110. package/dist/lib/brief.d.ts +2 -0
  111. package/dist/lib/brief.d.ts.map +1 -1
  112. package/dist/lib/budget-decimal.d.ts +14 -0
  113. package/dist/lib/budget-decimal.d.ts.map +1 -0
  114. package/dist/lib/cloud-ingest.d.ts +15 -0
  115. package/dist/lib/cloud-ingest.d.ts.map +1 -1
  116. package/dist/lib/cloud-storage.d.ts.map +1 -1
  117. package/dist/lib/ingest-batches.d.ts +21 -0
  118. package/dist/lib/ingest-batches.d.ts.map +1 -0
  119. package/dist/lib/ingest-journal-client.d.ts +37 -0
  120. package/dist/lib/ingest-journal-client.d.ts.map +1 -0
  121. package/dist/lib/ingest-journal-lock.d.ts +5 -0
  122. package/dist/lib/ingest-journal-lock.d.ts.map +1 -0
  123. package/dist/lib/ingest-journal-maintenance.d.ts +92 -0
  124. package/dist/lib/ingest-journal-maintenance.d.ts.map +1 -0
  125. package/dist/lib/ingest-journal-operation.d.ts +55 -0
  126. package/dist/lib/ingest-journal-operation.d.ts.map +1 -0
  127. package/dist/lib/ingest-journal.d.ts +73 -0
  128. package/dist/lib/ingest-journal.d.ts.map +1 -0
  129. package/dist/lib/money-display.d.ts +13 -0
  130. package/dist/lib/money-display.d.ts.map +1 -0
  131. package/dist/lib/periods.d.ts +12 -0
  132. package/dist/lib/periods.d.ts.map +1 -1
  133. package/dist/lib/pricing.d.ts +14 -0
  134. package/dist/lib/pricing.d.ts.map +1 -1
  135. package/dist/lib/savings.d.ts +64 -7
  136. package/dist/lib/savings.d.ts.map +1 -1
  137. package/dist/lib/store/index.d.ts +25 -21
  138. package/dist/lib/store/index.d.ts.map +1 -1
  139. package/dist/lib/summary-contract.d.ts +32 -0
  140. package/dist/lib/summary-contract.d.ts.map +1 -0
  141. package/dist/lib/summary-test-contract.d.ts +3 -0
  142. package/dist/lib/summary-test-contract.d.ts.map +1 -0
  143. package/dist/lib/sync-all.d.ts +4 -0
  144. package/dist/lib/sync-all.d.ts.map +1 -1
  145. package/dist/lib/sync-maintenance.d.ts.map +1 -1
  146. package/dist/lib/sync-report.d.ts +4 -0
  147. package/dist/lib/sync-report.d.ts.map +1 -0
  148. package/dist/lib/test-keychain-fixture.d.ts +9 -0
  149. package/dist/lib/test-keychain-fixture.d.ts.map +1 -0
  150. package/dist/mcp/agent-registry-store.d.ts +30 -0
  151. package/dist/mcp/agent-registry-store.d.ts.map +1 -0
  152. package/dist/mcp/agent-registry.d.ts +9 -16
  153. package/dist/mcp/agent-registry.d.ts.map +1 -1
  154. package/dist/mcp/index.js +2281 -3457
  155. package/dist/mcp/ingest-journal-tool.d.ts +2 -0
  156. package/dist/mcp/ingest-journal-tool.d.ts.map +1 -0
  157. package/dist/mcp/server.d.ts.map +1 -1
  158. package/dist/openapi.d.ts.map +1 -1
  159. package/dist/otel/index.js +1950 -288
  160. package/dist/server/human-auth.d.ts +20 -0
  161. package/dist/server/human-auth.d.ts.map +1 -0
  162. package/dist/server/index.js +10401 -6695
  163. package/dist/server/legacy-inventory.d.ts +52 -0
  164. package/dist/server/legacy-inventory.d.ts.map +1 -0
  165. package/dist/server/legacy-operations.d.ts +64 -0
  166. package/dist/server/legacy-operations.d.ts.map +1 -0
  167. package/dist/server/legacy-plan.d.ts +55 -0
  168. package/dist/server/legacy-plan.d.ts.map +1 -0
  169. package/dist/server/legacy-preservation.d.ts +58 -0
  170. package/dist/server/legacy-preservation.d.ts.map +1 -0
  171. package/dist/server/pg-sync-worker.js +195 -21
  172. package/dist/server/serve.d.ts +20 -16
  173. package/dist/server/serve.d.ts.map +1 -1
  174. package/dist/server/service-admission.d.ts +10 -0
  175. package/dist/server/service-admission.d.ts.map +1 -0
  176. package/dist/server/service-dispatcher.d.ts +45 -0
  177. package/dist/server/service-dispatcher.d.ts.map +1 -0
  178. package/dist/server/service-operations.d.ts +5 -0
  179. package/dist/server/service-operations.d.ts.map +1 -0
  180. package/dist/server/service-protocol.d.ts +50 -0
  181. package/dist/server/service-protocol.d.ts.map +1 -0
  182. package/dist/server/service-worker.d.ts +2 -0
  183. package/dist/server/service-worker.d.ts.map +1 -0
  184. package/dist/server/service-worker.js +8029 -0
  185. package/dist/types/index.d.ts +66 -0
  186. package/dist/types/index.d.ts.map +1 -1
  187. package/docs/README.md +1 -1
  188. package/docs/cli.md +1 -1
  189. package/docs/configuration.md +5 -5
  190. package/docs/cost-reporting.md +49 -0
  191. package/docs/historical-data-reconciliation.md +203 -0
  192. package/docs/ingest-journal.md +108 -0
  193. package/docs/ingestion.md +60 -4
  194. package/docs/mcp.md +2 -2
  195. package/docs/native-budget-save.md +84 -0
  196. package/docs/native-human-auth.md +102 -0
  197. package/docs/rest-api.md +2 -2
  198. package/package.json +15 -12
  199. package/dist/lib/test-hermetic-accounts.d.ts +0 -7
  200. package/dist/lib/test-hermetic-accounts.d.ts.map +0 -1
  201. package/dist/lib/test-hermetic-fleet-env.d.ts +0 -4
  202. package/dist/lib/test-hermetic-fleet-env.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,2096 +1,77 @@
1
1
  // @bun
2
- var __defProp = Object.defineProperty;
3
- var __returnValue = (v) => v;
4
- function __exportSetter(name, newValue) {
5
- this[name] = __returnValue.bind(null, newValue);
6
- }
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, {
10
- get: all[name],
11
- enumerable: true,
12
- configurable: true,
13
- set: __exportSetter.bind(all, name)
14
- });
15
- };
16
- var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
-
18
- // src/db/sqlite-adapter.ts
19
- import { Database } from "bun:sqlite";
20
-
21
- class SqliteAdapter {
22
- db;
23
- constructor(path) {
24
- this.db = new Database(path, { create: true });
25
- this.db.exec("PRAGMA journal_mode=WAL");
26
- this.db.exec("PRAGMA foreign_keys=ON");
27
- }
28
- run(sql, ...params) {
29
- const result = this.db.prepare(sql).run(...params);
30
- return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
31
- }
32
- get(sql, ...params) {
33
- return this.db.prepare(sql).get(...params);
34
- }
35
- all(sql, ...params) {
36
- return this.db.prepare(sql).all(...params);
37
- }
38
- exec(sql) {
39
- this.db.exec(sql);
40
- }
41
- query(sql) {
42
- return this.db.query(sql);
43
- }
44
- prepare(sql) {
45
- const stmt = this.db.prepare(sql);
46
- return {
47
- run(...params) {
48
- const r = stmt.run(...params);
49
- return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
50
- },
51
- get(...params) {
52
- return stmt.get(...params);
53
- },
54
- all(...params) {
55
- return stmt.all(...params);
56
- },
57
- finalize() {
58
- stmt.finalize();
59
- }
60
- };
61
- }
62
- close() {
63
- this.db.close();
64
- }
65
- transaction(fn) {
66
- return this.db.transaction(fn)();
67
- }
68
- get raw() {
69
- return this.db;
70
- }
71
- }
72
- var init_sqlite_adapter = () => {};
73
-
74
- // src/lib/pricing.ts
75
- var exports_pricing = {};
76
- __export(exports_pricing, {
77
- normalizeModelName: () => normalizeModelName,
78
- getPricingFromRows: () => getPricingFromRows,
79
- getPricingFromDb: () => getPricingFromDb,
80
- getPricing: () => getPricing,
81
- estimateCostFromRows: () => estimateCostFromRows,
82
- ensurePricingSeeded: () => ensurePricingSeeded,
83
- computeCostFromDb: () => computeCostFromDb,
84
- computeCost: () => computeCost,
85
- DEFAULT_PRICING: () => DEFAULT_PRICING
86
- });
87
- function normalizeModelName(raw) {
88
- return raw.trim().toLowerCase().replace(/^models\//, "").replace(/^[a-z0-9_.-]+\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
89
- }
90
- function normalizeModelNamePreservingProvider(raw) {
91
- return raw.trim().toLowerCase().replace(/^models\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
92
- }
93
- function modelLookupKeys(raw) {
94
- const withProvider = normalizeModelNamePreservingProvider(raw);
95
- const withoutProvider = normalizeModelName(raw);
96
- return withProvider === withoutProvider ? [withoutProvider] : [withProvider, withoutProvider];
97
- }
98
- function bestPrefixMatch(normalized, entries) {
99
- let best = null;
100
- for (const entry of entries) {
101
- const [key] = entry;
102
- if (normalized !== key && !normalized.startsWith(`${key}-`))
103
- continue;
104
- if (!best || key.length > best[0].length)
105
- best = entry;
106
- }
107
- return best?.[1] ?? null;
108
- }
109
- function bestModelMatch(model, entries) {
110
- for (const key of modelLookupKeys(model)) {
111
- const match = bestPrefixMatch(key, entries);
112
- if (match)
113
- return match;
114
- }
115
- return null;
116
- }
117
- function exactModelMatch(model, entries) {
118
- for (const key of modelLookupKeys(model)) {
119
- const match = entries.find(([entryKey]) => entryKey === key);
120
- if (match)
121
- return match[1];
122
- }
123
- return null;
124
- }
125
- function ensurePricingSeeded(db) {
126
- seedModelPricing(db, DEFAULT_PRICING);
127
- repairLegacySeededPricing(db);
128
- repairMissingDefaultCacheWrite1h(db);
129
- repairMissingDefaultCacheStorage(db);
130
- removeDeprecatedDefaultPricing(db);
131
- }
132
- function repairLegacySeededPricing(db) {
133
- const now = new Date().toISOString();
134
- const legacyModels = new Set([
135
- ...Object.keys(LEGACY_DEFAULT_PRICING),
136
- ...Object.keys(ADDITIONAL_LEGACY_DEFAULT_PRICING)
137
- ]);
138
- for (const model of legacyModels) {
139
- const current = getModelPricing(db, model);
140
- const next = DEFAULT_PRICING[model];
141
- if (!current || !next)
142
- continue;
143
- const legacy = LEGACY_DEFAULT_PRICING[model];
144
- const legacyRows = [
145
- ...legacy ? [legacy] : [],
146
- ...ADDITIONAL_LEGACY_DEFAULT_PRICING[model] ?? []
147
- ];
148
- if (!legacyRows.some((row) => samePricing(current, row)))
149
- continue;
150
- upsertModelPricing(db, {
151
- model,
152
- input_per_1m: next.inputPer1M,
153
- output_per_1m: next.outputPer1M,
154
- cache_read_per_1m: next.cacheReadPer1M,
155
- cache_write_per_1m: next.cacheWritePer1M,
156
- cache_write_1h_per_1m: next.cacheWrite1hPer1M ?? 0,
157
- cache_storage_per_1m_hour: next.cacheStoragePer1MHour ?? 0,
158
- updated_at: now
159
- });
160
- }
161
- }
162
- function repairMissingDefaultCacheWrite1h(db) {
163
- const now = new Date().toISOString();
164
- for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
165
- if (!next.cacheWrite1hPer1M)
166
- continue;
167
- const current = getModelPricing(db, model);
168
- if (!current)
169
- continue;
170
- if ((current.cache_write_1h_per_1m ?? 0) !== 0)
171
- continue;
172
- if (!sameBasePricing(current, next))
173
- continue;
174
- upsertModelPricing(db, {
175
- model,
176
- input_per_1m: current.input_per_1m,
177
- output_per_1m: current.output_per_1m,
178
- cache_read_per_1m: current.cache_read_per_1m,
179
- cache_write_per_1m: current.cache_write_per_1m,
180
- cache_write_1h_per_1m: next.cacheWrite1hPer1M,
181
- cache_storage_per_1m_hour: current.cache_storage_per_1m_hour ?? next.cacheStoragePer1MHour ?? 0,
182
- updated_at: now
183
- });
184
- }
185
- }
186
- function repairMissingDefaultCacheStorage(db) {
187
- const now = new Date().toISOString();
188
- for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
189
- if (!next.cacheStoragePer1MHour)
190
- continue;
191
- const current = getModelPricing(db, model);
192
- if (!current)
193
- continue;
194
- if ((current.cache_storage_per_1m_hour ?? 0) !== 0)
195
- continue;
196
- if (!sameBasePricing(current, next))
197
- continue;
198
- upsertModelPricing(db, {
199
- model,
200
- input_per_1m: current.input_per_1m,
201
- output_per_1m: current.output_per_1m,
202
- cache_read_per_1m: current.cache_read_per_1m,
203
- cache_write_per_1m: current.cache_write_per_1m,
204
- cache_write_1h_per_1m: current.cache_write_1h_per_1m ?? next.cacheWrite1hPer1M ?? 0,
205
- cache_storage_per_1m_hour: next.cacheStoragePer1MHour,
206
- updated_at: now
207
- });
208
- }
209
- }
210
- function removeDeprecatedDefaultPricing(db) {
211
- for (const [model, removedRows] of Object.entries(REMOVED_DEFAULT_PRICING)) {
212
- const current = getModelPricing(db, model);
213
- if (!current)
214
- continue;
215
- if (!removedRows.some((row) => samePricing(current, row)))
216
- continue;
217
- deleteModelPricing(db, model);
218
- }
219
- }
220
- function sameBasePricing(row, pricing) {
221
- 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;
222
- }
223
- function samePricing(row, pricing) {
224
- 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);
225
- }
226
- function getPricingFromDb(db, model) {
227
- if (isFreeModel(model))
228
- return FREE_PRICING;
229
- for (const key of modelLookupKeys(model)) {
230
- const row = getModelPricing(db, key);
231
- if (row)
232
- return modelPricingFromDbRow(row);
233
- }
234
- const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
235
- const match = bestModelMatch(model, allRows.map((r) => [r.model, r]));
236
- if (!match)
237
- return null;
238
- return modelPricingFromDbRow(match);
239
- }
240
- function modelPricingFromDbRow(row) {
241
- const seeded = DEFAULT_PRICING[row.model];
242
- const cacheWrite1hPer1M = seeded?.cacheWrite1hPer1M && (row.cache_write_1h_per_1m ?? 0) === 0 && sameBasePricing(row, seeded) ? seeded.cacheWrite1hPer1M : row.cache_write_1h_per_1m ?? 0;
243
- return {
244
- inputPer1M: row.input_per_1m,
245
- outputPer1M: row.output_per_1m,
246
- cacheReadPer1M: row.cache_read_per_1m,
247
- cacheWritePer1M: row.cache_write_per_1m,
248
- cacheWrite1hPer1M,
249
- cacheStoragePer1MHour: row.cache_storage_per_1m_hour ?? seeded?.cacheStoragePer1MHour ?? 0
250
- };
251
- }
252
- function getPricing(model) {
253
- if (isFreeModel(model))
254
- return FREE_PRICING;
255
- return bestModelMatch(model, Object.entries(DEFAULT_PRICING));
256
- }
257
- function isFreeModel(model) {
258
- return model.trim().toLowerCase().endsWith(":free");
259
- }
260
- function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
261
- const pricing = getPricing(model);
262
- if (!pricing)
263
- return 0;
264
- return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
265
- }
266
- function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
267
- const pricing = getPricingFromDb(db, model) ?? getPricing(model);
268
- if (!pricing)
269
- return 0;
270
- return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
271
- }
272
- function getPricingFromRows(rows, model) {
273
- if (isFreeModel(model))
274
- return FREE_PRICING;
275
- for (const key of modelLookupKeys(model)) {
276
- const row = rows.find((r) => r.model === key);
277
- if (row)
278
- return modelPricingFromDbRow(row);
279
- }
280
- const match = bestModelMatch(model, rows.map((r) => [r.model, r]));
281
- if (!match)
282
- return null;
283
- return modelPricingFromDbRow(match);
284
- }
285
- function estimateCostFromRows(rows, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
286
- const pricing = getPricingFromRows(rows, model) ?? getPricing(model);
287
- if (!pricing)
288
- return 0;
289
- return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
290
- }
291
- function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
292
- if (isFreeModel(model))
293
- return 0;
294
- let effective = pricing;
295
- 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));
296
- if (promptTier) {
297
- const billablePromptTokens = inputTokens + cacheReadTokens + cacheWriteTokens + cacheWrite1hTokens;
298
- if (billablePromptTokens > promptTier.threshold) {
299
- effective = {
300
- ...pricing,
301
- inputPer1M: promptTier.inputPer1M ?? pricing.inputPer1M * (promptTier.inputMultiplier ?? 1),
302
- outputPer1M: promptTier.outputPer1M ?? pricing.outputPer1M * (promptTier.outputMultiplier ?? 1),
303
- cacheReadPer1M: promptTier.cacheReadPer1M ?? pricing.cacheReadPer1M * (promptTier.cacheReadMultiplier ?? 1),
304
- cacheWritePer1M: promptTier.cacheWritePer1M ?? pricing.cacheWritePer1M * (promptTier.cacheWriteMultiplier ?? 1)
305
- };
306
- }
307
- }
308
- return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
309
- }
310
- 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;
311
- var init_pricing = __esm(() => {
312
- init_database();
313
- DEFAULT_PRICING = {
314
- "claude-opus-4-8": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
315
- "claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
316
- "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
317
- "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
318
- "claude-opus-4-1": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
319
- "claude-opus-4": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
320
- "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
321
- "claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
322
- "claude-sonnet-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
323
- "claude-3-7-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
324
- "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25, cacheWrite1hPer1M: 2 },
325
- "claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1, cacheWrite1hPer1M: 1.6 },
326
- "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
327
- "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3, cacheWrite1hPer1M: 0.5 },
328
- "gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
329
- "gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
330
- "gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
331
- "gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
332
- "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
333
- "gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
334
- "gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
335
- "gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
336
- "gemini-2.0-flash-lite": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
337
- "google/gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0.375 },
338
- "google/gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
339
- "google/gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
340
- "google/gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0.08333333333333334 },
341
- "google/gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0.375 },
342
- "google/gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0.08333333333333334 },
343
- "google/gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0.08333333333333334 },
344
- "gpt-5.5": { inputPer1M: 5, outputPer1M: 30, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
345
- "gpt-5.5-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
346
- "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
347
- "gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
348
- "gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
349
- "gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
350
- "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
351
- "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
352
- "gpt-5.2-chat-latest": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
353
- "gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
354
- "gpt-5-codex": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
355
- "gpt-5-mini": { inputPer1M: 0.25, outputPer1M: 2, cacheReadPer1M: 0.025, cacheWritePer1M: 0 },
356
- "gpt-5": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
357
- "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
358
- "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
359
- o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
360
- "o1-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
361
- o3: { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
362
- "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
363
- "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
364
- "qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
365
- "qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
366
- "qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
367
- "qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
368
- "qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
369
- "qwen/qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
370
- "qwen/qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
371
- "qwen/qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
372
- "qwen/qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
373
- "qwen/qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
374
- "minimax-m2.7": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
375
- "minimax-m2.7-highspeed": { inputPer1M: 0.6, outputPer1M: 2.4, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
376
- "minimax/minimax-m2.7": { inputPer1M: 0.299, outputPer1M: 1.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
377
- "minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
378
- "minimax/minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
379
- "grok-4.3": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
380
- "grok-latest": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
381
- "grok-4.20": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
382
- "grok-4-1-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
383
- "grok-4-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
384
- "grok-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
385
- "grok-code-fast-1": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
386
- "grok-code-fast": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
387
- "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
388
- "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0.07, cacheWritePer1M: 0 },
389
- "glm-5.1": { inputPer1M: 1.4, outputPer1M: 4.4, cacheReadPer1M: 0.26, cacheWritePer1M: 0 },
390
- "glm-5": { inputPer1M: 1, outputPer1M: 3.2, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
391
- "z-ai/glm-5.1": { inputPer1M: 1.05, outputPer1M: 3.5, cacheReadPer1M: 0.525, cacheWritePer1M: 0 },
392
- "z-ai/glm-5": { inputPer1M: 0.6, outputPer1M: 1.92, cacheReadPer1M: 0.12, cacheWritePer1M: 0 },
393
- "kimi-k2.6": { inputPer1M: 0.95, outputPer1M: 4, cacheReadPer1M: 0.16, cacheWritePer1M: 0 },
394
- "kimi-k2.5": { inputPer1M: 0.6, outputPer1M: 3, cacheReadPer1M: 0.1, cacheWritePer1M: 0 },
395
- "kimi-k2": { inputPer1M: 0.6, outputPer1M: 2.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
396
- "moonshotai/kimi-k2.6": { inputPer1M: 0.75, outputPer1M: 3.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
397
- "moonshotai/kimi-k2.5": { inputPer1M: 0.44, outputPer1M: 2, cacheReadPer1M: 0.22, cacheWritePer1M: 0 },
398
- "moonshotai/kimi-k2": { inputPer1M: 0.57, outputPer1M: 2.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
399
- };
400
- LEGACY_DEFAULT_PRICING = {
401
- "claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
402
- "claude-opus-4": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
403
- "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
404
- "gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
405
- "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
406
- "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
407
- "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
408
- "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
409
- "gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
410
- "gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
411
- "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
412
- "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
413
- "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
414
- "qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
415
- "minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
416
- "minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
417
- "minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
418
- "glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
419
- "glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
420
- "kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
421
- o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 }
422
- };
423
- ADDITIONAL_LEGACY_DEFAULT_PRICING = {
424
- "gemini-2.5-pro": [
425
- { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 }
426
- ],
427
- "qwen3.6-plus": [
428
- { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
429
- { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
430
- ],
431
- "qwen3.6-flash": [
432
- { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
433
- ],
434
- "qwen3.6-max-preview": [
435
- { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
436
- { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
437
- ],
438
- "qwen/qwen3.6-plus": [
439
- { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
440
- { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
441
- ],
442
- "qwen/qwen3.6-flash": [
443
- { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
444
- ],
445
- "qwen/qwen3.6-max-preview": [
446
- { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
447
- { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
448
- ]
449
- };
450
- REMOVED_DEFAULT_PRICING = {
451
- "claude-3-5-sonnet": [
452
- { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
453
- { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
454
- ],
455
- "claude-3-sonnet": [
456
- { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
457
- { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
458
- ],
459
- "gemini-3.1-pro": [
460
- { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
461
- { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 }
462
- ],
463
- "gemini-1.5-pro": [
464
- { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 }
465
- ],
466
- "gemini-1.5-flash": [
467
- { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
468
- ],
469
- "gpt-5.3-chat": [
470
- { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
471
- { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 }
472
- ],
473
- "qwen3.6": [
474
- { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
475
- ]
476
- };
477
- FREE_PRICING = {
478
- inputPer1M: 0,
479
- outputPer1M: 0,
480
- cacheReadPer1M: 0,
481
- cacheWritePer1M: 0,
482
- cacheWrite1hPer1M: 0,
483
- cacheStoragePer1MHour: 0
484
- };
485
- GEMINI_PROMPT_TIERS = {
486
- "gemini-3.1-pro-preview": {
487
- threshold: 200000,
488
- inputPer1M: 4,
489
- outputPer1M: 18,
490
- cacheReadPer1M: 0.4
491
- },
492
- "gemini-2.5-pro": {
493
- threshold: 200000,
494
- inputPer1M: 2.5,
495
- outputPer1M: 15,
496
- cacheReadPer1M: 0.25
497
- }
498
- };
499
- OPENAI_PROMPT_TIERS = {
500
- "gpt-5.5": {
501
- threshold: 272000,
502
- inputMultiplier: 2,
503
- outputMultiplier: 1.5,
504
- cacheReadMultiplier: 2
505
- },
506
- "gpt-5.4-pro": {
507
- threshold: 272000,
508
- inputMultiplier: 2,
509
- outputMultiplier: 1.5,
510
- cacheReadMultiplier: 2
511
- },
512
- "gpt-5.4": {
513
- threshold: 272000,
514
- inputMultiplier: 2,
515
- outputMultiplier: 1.5,
516
- cacheReadMultiplier: 2
517
- }
518
- };
519
- QWEN_PROMPT_TIERS = {
520
- "qwen3.6-plus": {
521
- threshold: 256000,
522
- inputPer1M: 1.3,
523
- outputPer1M: 3.9,
524
- cacheReadPer1M: 0.13,
525
- cacheWritePer1M: 1.625
526
- },
527
- "qwen3.6-flash": {
528
- threshold: 256000,
529
- inputPer1M: 1,
530
- outputPer1M: 4,
531
- cacheReadPer1M: 0.1,
532
- cacheWritePer1M: 1.25
533
- },
534
- "qwen3.6-max-preview": {
535
- threshold: 128000,
536
- inputPer1M: 1.6,
537
- outputPer1M: 9.6,
538
- cacheReadPer1M: 0.16,
539
- cacheWritePer1M: 2
540
- }
541
- };
542
- MINIMAX_PROMPT_TIERS = {
543
- "minimax/minimax-m1": {
544
- threshold: Number.POSITIVE_INFINITY
545
- },
546
- "minimax-m1": {
547
- threshold: 200000,
548
- inputPer1M: 1.3
549
- }
550
- };
551
- XAI_PROMPT_TIERS = {
552
- "grok-4.3": {
553
- threshold: 200000,
554
- inputPer1M: 2.5,
555
- outputPer1M: 5,
556
- cacheReadPer1M: 0.4
557
- },
558
- "grok-latest": {
559
- threshold: 200000,
560
- inputPer1M: 2.5,
561
- outputPer1M: 5,
562
- cacheReadPer1M: 0.4
563
- },
564
- "grok-4.20": {
565
- threshold: 200000,
566
- inputPer1M: 2.5,
567
- outputPer1M: 5,
568
- cacheReadPer1M: 0.4
569
- },
570
- "grok-4-1-fast": {
571
- threshold: 128000,
572
- inputPer1M: 0.4,
573
- outputPer1M: 1,
574
- cacheReadPer1M: 0
575
- },
576
- "grok-4-fast": {
577
- threshold: 128000,
578
- inputPer1M: 0.4,
579
- outputPer1M: 1,
580
- cacheReadPer1M: 0
581
- },
582
- "grok-4": {
583
- threshold: 128000,
584
- inputPer1M: 6,
585
- outputPer1M: 30,
586
- cacheReadPer1M: 0
587
- }
588
- };
589
- });
590
-
591
- // src/db/database.ts
592
- import { execFileSync } from "child_process";
593
- import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
594
- import { hostname, platform } from "os";
595
- import { homedir } from "os";
596
- import { join, resolve } from "path";
597
- import { homedir as pathsResolverHomedir } from "os";
598
- import { join as pathsResolverJoin } from "path";
599
- function pathsResolverAssertApp(app) {
600
- if (typeof app !== "string" || app.length === 0) {
601
- throw new TypeError("paths: app must be a non-empty string");
602
- }
603
- if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
604
- throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
605
- }
606
- }
607
- function pathsResolverAssertKind(kind) {
608
- if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
609
- throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
610
- }
611
- }
612
- function pathsResolverBaseDir(kind, options) {
613
- pathsResolverAssertKind(kind);
614
- const env = options.env ?? process.env;
615
- const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
616
- if (typeof override === "string" && override.length > 0)
617
- return override;
618
- const home = options.home ?? pathsResolverHomedir();
619
- const platform2 = options.platform ?? process.platform;
620
- if (platform2 === "darwin") {
621
- switch (kind) {
622
- case "config":
623
- case "data":
624
- return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
625
- case "cache":
626
- return pathsResolverJoin(home, "Library", "Caches", "Hasna");
627
- case "state":
628
- return pathsResolverJoin(home, "Library", "Logs", "Hasna");
629
- }
630
- }
631
- switch (kind) {
632
- case "config":
633
- return pathsResolverJoin(home, ".config", "hasna");
634
- case "data":
635
- return pathsResolverJoin(home, ".local", "share", "hasna");
636
- case "state":
637
- return pathsResolverJoin(home, ".local", "state", "hasna");
638
- case "cache":
639
- return pathsResolverJoin(home, ".cache", "hasna");
640
- }
641
- }
642
- function pathsResolverResolve(kind, options) {
643
- pathsResolverAssertApp(options.app);
644
- const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
645
- return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
646
- }
647
- function dataDir(options) {
648
- return pathsResolverResolve("data", options);
649
- }
650
- function normalizeMachineId(value) {
651
- const id = value?.trim().toLowerCase().split(".")[0];
652
- return id && id.length > 0 ? id : null;
653
- }
654
- function macHostMachineId() {
655
- if (platform() !== "darwin")
656
- return null;
657
- for (const key of ["LocalHostName", "ComputerName", "HostName"]) {
658
- try {
659
- const value = execFileSync("/usr/sbin/scutil", ["--get", key], {
660
- encoding: "utf8",
661
- stdio: ["ignore", "pipe", "ignore"],
662
- timeout: 1000
663
- });
664
- const id = normalizeMachineId(value);
665
- if (id && id !== "mac" && id !== "localhost")
666
- return id;
667
- } catch {}
668
- }
669
- return null;
670
- }
671
- function getMachineId() {
672
- const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
673
- if (envMachine)
674
- return envMachine;
675
- const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
676
- if (hostMachine === "mac" || hostMachine === "localhost")
677
- return macHostMachineId() ?? hostMachine;
678
- return hostMachine;
679
- }
680
- function getHomeDir() {
681
- const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
682
- if (!home)
683
- throw new Error("Could not resolve the user home directory");
684
- return home;
685
- }
686
- function getResolverDataRoot() {
687
- return dataDir({
688
- app: "economy",
689
- home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
690
- });
691
- }
692
- function getLegacyDataRoot() {
693
- return join(getHomeDir(), ".hasna", "economy");
694
- }
695
- function adoptResolverDataRoot(resolved, env = process.env) {
696
- const dataOverride = env.HASNA_DATA_HOME;
697
- if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
698
- return true;
699
- return existsSync(join(resolved, "economy.db"));
700
- }
701
- function getExactDataRoot() {
702
- const dir = process.env["HASNA_ECONOMY_HOME"] ?? process.env["ECONOMY_HOME"];
703
- if (dir && dir.trim())
704
- return dir.trim();
705
- return;
706
- }
707
- function getDataDir() {
708
- const exact = getExactDataRoot();
709
- const effective = exact ? resolve(exact) : adoptResolverDataRoot(getResolverDataRoot()) ? resolve(getResolverDataRoot()) : getLegacyDataRoot();
710
- const oldDir = join(getHomeDir(), ".economy");
711
- if (existsSync(oldDir) && !existsSync(effective)) {
712
- mkdirSync(effective, { recursive: true });
713
- for (const file of readdirSync(oldDir)) {
714
- const oldPath = join(oldDir, file);
715
- if (statSync(oldPath).isFile()) {
716
- copyFileSync(oldPath, join(effective, file));
717
- }
718
- }
719
- }
720
- mkdirSync(effective, { recursive: true });
721
- return effective;
722
- }
723
- function getDbPath() {
724
- if (process.env["HASNA_ECONOMY_DB_PATH"])
725
- return process.env["HASNA_ECONOMY_DB_PATH"];
726
- if (process.env["ECONOMY_DB"])
727
- return process.env["ECONOMY_DB"];
728
- return join(getDataDir(), "economy.db");
729
- }
730
- function isSqliteBusyError(error) {
731
- const candidate = error;
732
- const code = typeof candidate.code === "string" ? candidate.code : "";
733
- const message = typeof candidate.message === "string" ? candidate.message : String(error);
734
- return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
735
- }
736
- function retryDelayMs(attempt) {
737
- return Math.min(1000, 50 * 2 ** attempt);
738
- }
739
- function withSqliteBusyRetry(operation, context) {
740
- const maxAttempts = 8;
741
- let lastError;
742
- for (let attempt = 0;attempt < maxAttempts; attempt++) {
743
- try {
744
- return operation();
745
- } catch (error) {
746
- if (!isSqliteBusyError(error))
747
- throw error;
748
- lastError = error;
749
- if (attempt === maxAttempts - 1)
750
- break;
751
- Bun.sleepSync(retryDelayMs(attempt));
752
- }
753
- }
754
- throw new Error(`SQLite database is locked after ${maxAttempts} attempts while ${context}. Another economy sync/merge may be recovering the database; retry shortly.`, { cause: lastError });
755
- }
756
- function openDatabase(dbPath, skipSeed = false) {
757
- const path = dbPath ?? getDbPath();
758
- if (path !== ":memory:") {
759
- const dir = path.substring(0, path.lastIndexOf("/"));
760
- if (dir && !existsSync(dir))
761
- mkdirSync(dir, { recursive: true });
762
- }
763
- const db = withSqliteBusyRetry(() => {
764
- const opened = new SqliteAdapter(path);
765
- try {
766
- opened.exec("PRAGMA busy_timeout = 10000");
767
- opened.exec("PRAGMA journal_mode = WAL");
768
- opened.exec("PRAGMA foreign_keys = ON");
769
- initSchema(opened);
770
- return opened;
771
- } catch (error) {
772
- try {
773
- opened.close();
774
- } catch {}
775
- throw error;
776
- }
777
- }, `opening ${path}`);
778
- if (!skipSeed) {
779
- Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
780
- }
781
- return db;
782
- }
783
- function quoteSqlIdent(identifier) {
784
- return `"${identifier.replace(/"/g, '""')}"`;
785
- }
786
- function hasColumn(db, table, column) {
787
- const columns = db.prepare(`PRAGMA table_info(${quoteSqlIdent(table)})`).all();
788
- return columns.some((c) => c.name === column);
789
- }
790
- function addColumnIfMissing(db, table, column, definition) {
791
- if (hasColumn(db, table, column))
792
- return false;
793
- try {
794
- db.exec(`ALTER TABLE ${quoteSqlIdent(table)} ADD COLUMN ${quoteSqlIdent(column)} ${definition}`);
795
- return true;
796
- } catch (error) {
797
- const message = error instanceof Error ? error.message : String(error);
798
- if (/duplicate column name/i.test(message))
799
- return true;
800
- throw error;
801
- }
802
- }
803
- function initSchema(db) {
804
- db.exec(`
805
- CREATE TABLE IF NOT EXISTS cost_centers (
806
- id TEXT PRIMARY KEY,
807
- kind TEXT NOT NULL,
808
- name TEXT NOT NULL,
809
- repo_path TEXT,
810
- labels_json TEXT DEFAULT '{}',
811
- created_at TEXT NOT NULL,
812
- updated_at TEXT NOT NULL
813
- );
814
-
815
- CREATE TABLE IF NOT EXISTS requests (
816
- id TEXT PRIMARY KEY,
817
- agent TEXT NOT NULL,
818
- session_id TEXT NOT NULL,
819
- model TEXT NOT NULL,
820
- input_tokens INTEGER DEFAULT 0,
821
- output_tokens INTEGER DEFAULT 0,
822
- cache_read_tokens INTEGER DEFAULT 0,
823
- cache_create_tokens INTEGER DEFAULT 0,
824
- cache_create_5m_tokens INTEGER DEFAULT 0,
825
- cache_create_1h_tokens INTEGER DEFAULT 0,
826
- cost_usd REAL NOT NULL DEFAULT 0,
827
- duration_ms INTEGER DEFAULT 0,
828
- timestamp TEXT NOT NULL,
829
- source_request_id TEXT,
830
- machine_id TEXT DEFAULT '',
831
- cost_center_id TEXT,
832
- account_key TEXT DEFAULT '',
833
- account_tool TEXT DEFAULT '',
834
- account_name TEXT DEFAULT '',
835
- account_email TEXT DEFAULT '',
836
- account_source TEXT DEFAULT ''
837
- );
838
-
839
- CREATE TABLE IF NOT EXISTS sessions (
840
- id TEXT PRIMARY KEY,
841
- agent TEXT NOT NULL,
842
- project_path TEXT DEFAULT '',
843
- project_name TEXT DEFAULT '',
844
- started_at TEXT NOT NULL,
845
- ended_at TEXT,
846
- total_cost_usd REAL DEFAULT 0,
847
- total_tokens INTEGER DEFAULT 0,
848
- request_count INTEGER DEFAULT 0,
849
- machine_id TEXT DEFAULT '',
850
- cost_center_id TEXT,
851
- account_key TEXT DEFAULT '',
852
- account_tool TEXT DEFAULT '',
853
- account_name TEXT DEFAULT '',
854
- account_email TEXT DEFAULT '',
855
- account_source TEXT DEFAULT ''
856
- );
857
-
858
- CREATE TABLE IF NOT EXISTS projects (
859
- id TEXT PRIMARY KEY,
860
- path TEXT UNIQUE NOT NULL,
861
- name TEXT NOT NULL,
862
- description TEXT,
863
- tags TEXT DEFAULT '[]',
864
- created_at TEXT NOT NULL
865
- );
866
-
867
- CREATE TABLE IF NOT EXISTS budgets (
868
- id TEXT PRIMARY KEY,
869
- project_path TEXT,
870
- agent TEXT,
871
- cost_center_id TEXT,
872
- period TEXT NOT NULL,
873
- limit_usd REAL NOT NULL,
874
- alert_at_percent INTEGER DEFAULT 80,
875
- created_at TEXT NOT NULL,
876
- updated_at TEXT NOT NULL
877
- );
878
-
879
- CREATE TABLE IF NOT EXISTS goals (
880
- id TEXT PRIMARY KEY,
881
- period TEXT NOT NULL,
882
- project_path TEXT,
883
- agent TEXT,
884
- limit_usd REAL NOT NULL,
885
- created_at TEXT NOT NULL,
886
- updated_at TEXT NOT NULL
887
- );
888
-
889
- CREATE TABLE IF NOT EXISTS ingest_state (
890
- source TEXT NOT NULL,
891
- key TEXT NOT NULL,
892
- value TEXT NOT NULL,
893
- PRIMARY KEY (source, key)
894
- );
895
-
896
- CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
897
- CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp);
898
- CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent);
899
- CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
900
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
901
- CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
902
- CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
903
-
904
- CREATE TABLE IF NOT EXISTS model_pricing (
905
- model TEXT PRIMARY KEY,
906
- input_per_1m REAL NOT NULL DEFAULT 0,
907
- output_per_1m REAL NOT NULL DEFAULT 0,
908
- cache_read_per_1m REAL NOT NULL DEFAULT 0,
909
- cache_write_per_1m REAL NOT NULL DEFAULT 0,
910
- cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
911
- cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
912
- updated_at TEXT NOT NULL
913
- );
914
-
915
- CREATE TABLE IF NOT EXISTS feedback (
916
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
917
- message TEXT NOT NULL,
918
- email TEXT,
919
- category TEXT DEFAULT 'general',
920
- version TEXT,
921
- machine_id TEXT,
922
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
923
- );
924
-
925
- CREATE TABLE IF NOT EXISTS billing_daily (
926
- date TEXT NOT NULL,
927
- provider TEXT NOT NULL,
928
- description TEXT DEFAULT '',
929
- cost_usd REAL NOT NULL DEFAULT 0,
930
- updated_at TEXT NOT NULL,
931
- PRIMARY KEY (date, provider, description)
932
- );
933
-
934
- CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
935
- CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
936
-
937
- CREATE TABLE IF NOT EXISTS subscriptions (
938
- id TEXT PRIMARY KEY,
939
- agent TEXT,
940
- provider TEXT NOT NULL,
941
- plan TEXT NOT NULL,
942
- monthly_fee_usd REAL NOT NULL DEFAULT 0,
943
- included_usage_usd REAL NOT NULL DEFAULT 0,
944
- billing_cycle_start TEXT,
945
- reset_policy TEXT DEFAULT 'monthly',
946
- active INTEGER NOT NULL DEFAULT 1,
947
- created_at TEXT NOT NULL,
948
- updated_at TEXT NOT NULL
949
- );
950
-
951
- CREATE TABLE IF NOT EXISTS usage_snapshots (
952
- id TEXT PRIMARY KEY,
953
- agent TEXT NOT NULL,
954
- date TEXT NOT NULL,
955
- metric TEXT NOT NULL,
956
- value REAL NOT NULL DEFAULT 0,
957
- unit TEXT DEFAULT '',
958
- machine_id TEXT DEFAULT '',
959
- updated_at TEXT NOT NULL
960
- );
961
-
962
- CREATE TABLE IF NOT EXISTS savings_daily (
963
- date TEXT NOT NULL,
964
- agent TEXT DEFAULT '',
965
- api_equivalent_usd REAL NOT NULL DEFAULT 0,
966
- subscription_fee_usd REAL NOT NULL DEFAULT 0,
967
- included_consumed_usd REAL NOT NULL DEFAULT 0,
968
- on_demand_usd REAL NOT NULL DEFAULT 0,
969
- saved_usd REAL NOT NULL DEFAULT 0,
970
- updated_at TEXT NOT NULL,
971
- PRIMARY KEY (date, agent)
972
- );
973
-
974
- CREATE TABLE IF NOT EXISTS machines (
975
- machine_id TEXT PRIMARY KEY,
976
- hostname TEXT NOT NULL,
977
- last_seen_at TEXT,
978
- last_push_at TEXT,
979
- last_pull_at TEXT,
980
- economy_version TEXT,
981
- updated_at TEXT NOT NULL
982
- );
983
-
984
- CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
985
- CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
986
- `);
987
- addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
988
- addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
989
- if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
990
- db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
991
- }
992
- addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
993
- addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
994
- addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
995
- if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
996
- db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
997
- }
998
- addColumnIfMissing(db, "requests", "cache_create_1h_tokens", "INTEGER DEFAULT 0");
999
- addColumnIfMissing(db, "requests", "cost_basis", `TEXT DEFAULT 'estimated'`);
1000
- addColumnIfMissing(db, "requests", "attribution_tag", `TEXT DEFAULT ''`);
1001
- if (addColumnIfMissing(db, "requests", "updated_at", `TEXT DEFAULT ''`)) {
1002
- db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
1003
- }
1004
- addColumnIfMissing(db, "requests", "synced_at", `TEXT DEFAULT ''`);
1005
- for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
1006
- addColumnIfMissing(db, "requests", column, `TEXT DEFAULT ''`);
1007
- }
1008
- addColumnIfMissing(db, "sessions", "attribution_tag", `TEXT DEFAULT ''`);
1009
- if (addColumnIfMissing(db, "sessions", "updated_at", `TEXT DEFAULT ''`)) {
1010
- db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
1011
- }
1012
- addColumnIfMissing(db, "sessions", "synced_at", `TEXT DEFAULT ''`);
1013
- for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
1014
- addColumnIfMissing(db, "sessions", column, `TEXT DEFAULT ''`);
1015
- }
1016
- addColumnIfMissing(db, "model_pricing", "cache_write_1h_per_1m", "REAL NOT NULL DEFAULT 0");
1017
- addColumnIfMissing(db, "model_pricing", "cache_storage_per_1m_hour", "REAL NOT NULL DEFAULT 0");
1018
- db.exec(`
1019
- CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
1020
- CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
1021
- CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
1022
- CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
1023
- CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
1024
- CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
1025
- CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
1026
- CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
1027
- `);
1028
- }
1029
- function periodWhere(period) {
1030
- switch (period) {
1031
- case "today":
1032
- return `DATE(timestamp) = DATE('now')`;
1033
- case "yesterday":
1034
- return `DATE(timestamp) = DATE('now', '-1 day')`;
1035
- case "week":
1036
- return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
1037
- case "month":
1038
- return `timestamp >= DATE('now', 'start of month')`;
1039
- case "year":
1040
- return `timestamp >= DATE('now', 'start of year')`;
1041
- case "all":
1042
- return "1=1";
1043
- }
1044
- }
1045
- function sessionPeriodWhere(period) {
1046
- switch (period) {
1047
- case "today":
1048
- return `DATE(started_at) = DATE('now')`;
1049
- case "yesterday":
1050
- return `DATE(started_at) = DATE('now', '-1 day')`;
1051
- case "week":
1052
- return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
1053
- case "month":
1054
- return `started_at >= DATE('now', 'start of month')`;
1055
- case "year":
1056
- return `started_at >= DATE('now', 'start of year')`;
1057
- case "all":
1058
- return "1=1";
1059
- }
1060
- }
1061
- function requestPeriodWhere(period) {
1062
- switch (period) {
1063
- case "today":
1064
- return `DATE(timestamp) = DATE('now')`;
1065
- case "yesterday":
1066
- return `DATE(timestamp) = DATE('now', '-1 day')`;
1067
- case "week":
1068
- return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
1069
- case "month":
1070
- return `timestamp >= DATE('now', 'start of month')`;
1071
- case "year":
1072
- return `timestamp >= DATE('now', 'start of year')`;
1073
- case "all":
1074
- return "1=1";
1075
- }
1076
- }
1077
- function querySessions(db, filter = {}) {
1078
- const conditions = [];
1079
- const params = [];
1080
- if (filter.agent) {
1081
- conditions.push("agent = ?");
1082
- params.push(filter.agent);
1083
- }
1084
- if (filter.project) {
1085
- conditions.push("project_path LIKE ?");
1086
- params.push(`%${filter.project}%`);
1087
- }
1088
- if (filter.account) {
1089
- const q = `%${filter.account}%`;
1090
- conditions.push("(account_key LIKE ? OR account_name LIKE ? OR account_email LIKE ?)");
1091
- params.push(q, q, q);
1092
- }
1093
- if (filter.since) {
1094
- conditions.push("started_at >= ?");
1095
- params.push(filter.since);
1096
- }
1097
- if (filter.machine) {
1098
- conditions.push("machine_id = ?");
1099
- params.push(filter.machine);
1100
- }
1101
- if (filter.search) {
1102
- const q = `%${filter.search}%`;
1103
- conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
1104
- params.push(q, q, `${filter.search}%`);
1105
- }
1106
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
1107
- const limit = filter.limit ?? 50;
1108
- const offset = filter.offset ?? 0;
1109
- return db.prepare(`
1110
- SELECT * FROM sessions ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?
1111
- `).all(...params, limit, offset);
1112
- }
1113
- function queryTopSessions(db, n = 10, agent, since) {
1114
- const conditions = [];
1115
- const params = [];
1116
- if (agent) {
1117
- conditions.push("agent = ?");
1118
- params.push(agent);
1119
- }
1120
- if (since) {
1121
- conditions.push("started_at >= ?");
1122
- params.push(since);
1123
- }
1124
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
1125
- return db.prepare(`SELECT * FROM sessions ${where} ORDER BY total_cost_usd DESC LIMIT ?`).all(...params, n);
1126
- }
1127
- function querySummary(db, period, machine, allMachines = false, agent) {
1128
- const rWhere = periodWhere(period);
1129
- const sWhere = sessionPeriodWhere(period);
1130
- const machineClause = !allMachines && machine ? " AND machine_id = ?" : "";
1131
- const agentClause = agent ? " AND agent = ?" : "";
1132
- const params = [];
1133
- if (!allMachines && machine)
1134
- params.push(machine);
1135
- if (agent)
1136
- params.push(agent);
1137
- const r = db.prepare(`
1138
- SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
1139
- COUNT(*) as requests,
1140
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
1141
- FROM requests WHERE ${rWhere}${machineClause}${agentClause}
1142
- `).get(...params);
1143
- const codexTotals = db.prepare(`
1144
- SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1145
- COALESCE(SUM(total_tokens), 0) as tokens,
1146
- COALESCE(SUM(request_count), 0) as requests,
1147
- COUNT(*) as sessions
1148
- FROM sessions
1149
- WHERE ${sWhere}${machineClause}${agentClause}
1150
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1151
- `).get(...params);
1152
- const requestSessionCount = db.prepare(`
1153
- SELECT COUNT(DISTINCT session_id) as sessions
1154
- FROM requests
1155
- WHERE ${rWhere}${machineClause}${agentClause}
1156
- `).get(...params);
1157
- return {
1158
- total_usd: r.total_usd + codexTotals.cost_usd,
1159
- requests: r.requests + codexTotals.requests,
1160
- tokens: r.tokens + codexTotals.tokens,
1161
- sessions: requestSessionCount.sessions + codexTotals.sessions,
1162
- period
1163
- };
1164
- }
1165
- function queryModelBreakdown(db) {
1166
- return db.prepare(`
1167
- SELECT model, agent,
1168
- COUNT(*) as requests,
1169
- COALESCE(SUM(input_tokens), 0) as input_tokens,
1170
- COALESCE(SUM(output_tokens), 0) as output_tokens,
1171
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1172
- COALESCE(SUM(cost_usd), 0) as cost_usd
1173
- FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
1174
- `).all();
1175
- }
1176
- function queryAgentBreakdown(db, period = "all", machine) {
1177
- const requestWhere = requestPeriodWhere(period);
1178
- const machineClause = machine ? " AND machine_id = ?" : "";
1179
- const machineParams = machine ? [machine] : [];
1180
- const groups = new Map;
1181
- const requestRows = db.prepare(`
1182
- SELECT agent,
1183
- COUNT(DISTINCT session_id) as sessions,
1184
- COUNT(*) as requests,
1185
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1186
- COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
1187
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1188
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1189
- COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1190
- COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
1191
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
1192
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1193
- MAX(timestamp) as last_active
1194
- FROM requests
1195
- WHERE ${requestWhere}${machineClause}
1196
- GROUP BY agent
1197
- ORDER BY api_equivalent_usd DESC
1198
- `).all(...machineParams);
1199
- for (const row of requestRows) {
1200
- groups.set(row.agent, row);
1201
- }
1202
- const sessionWhere = sessionPeriodWhere(period);
1203
- const sessionOnlyRows = db.prepare(`
1204
- SELECT agent,
1205
- COUNT(*) as sessions,
1206
- COALESCE(SUM(request_count), 0) as requests,
1207
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1208
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1209
- MAX(started_at) as last_active
1210
- FROM sessions
1211
- WHERE ${sessionWhere}${machineClause}
1212
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1213
- GROUP BY agent
1214
- `).all(...machineParams);
1215
- for (const row of sessionOnlyRows) {
1216
- const existing = groups.get(row.agent) ?? {
1217
- agent: row.agent,
1218
- sessions: 0,
1219
- requests: 0,
1220
- total_tokens: 0,
1221
- api_equivalent_usd: 0,
1222
- billable_usd: 0,
1223
- metered_api_usd: 0,
1224
- subscription_included_usd: 0,
1225
- estimated_usd: 0,
1226
- unknown_usd: 0,
1227
- cost_usd: 0,
1228
- last_active: ""
1229
- };
1230
- existing.sessions += row.sessions;
1231
- existing.requests += row.requests;
1232
- existing.total_tokens += row.total_tokens;
1233
- existing.api_equivalent_usd += row.cost_usd;
1234
- existing.estimated_usd += row.cost_usd;
1235
- existing.cost_usd += row.cost_usd;
1236
- if (!existing.last_active || row.last_active > existing.last_active)
1237
- existing.last_active = row.last_active;
1238
- groups.set(row.agent, existing);
1239
- }
1240
- return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
1241
- }
1242
- function pathProjectLabel(projectPath) {
1243
- if (!projectPath)
1244
- return "";
1245
- const segments = projectPath.split("/").filter(Boolean);
1246
- const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
1247
- for (const seg of segments) {
1248
- if (projectPrefix.test(seg))
1249
- return seg;
1250
- }
1251
- const generic = new Set([
1252
- "web",
1253
- "app",
1254
- "apps",
1255
- "packages",
1256
- "src",
1257
- "lib",
1258
- "server",
1259
- "client",
1260
- "api",
1261
- "frontend",
1262
- "backend",
1263
- "home",
1264
- "users",
1265
- "workspace",
1266
- "workspaces",
1267
- "hasna"
1268
- ]);
1269
- for (let i = segments.length - 1;i >= 0; i--) {
1270
- if (!generic.has(segments[i].toLowerCase()))
1271
- return segments[i];
1272
- }
1273
- return null;
1274
- }
1275
- function isRepoLikeLabel(label) {
1276
- return /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/.test(label) || label.includes("-");
1277
- }
1278
- function labelForPath(projectPath, projectName) {
1279
- const pathLabel = pathProjectLabel(projectPath);
1280
- if (pathLabel && (!projectName || projectName.trim() === "" || isRepoLikeLabel(pathLabel)))
1281
- return pathLabel;
1282
- if (projectName && projectName.trim() !== "")
1283
- return projectName;
1284
- if (pathLabel)
1285
- return pathLabel;
1286
- return projectPath;
1287
- }
1288
- function groupKeyForPath(projectPath, projectName) {
1289
- return labelForPath(projectPath, projectName).trim().toLowerCase();
1290
- }
1291
- function laterTimestamp(current, candidate) {
1292
- if (!candidate)
1293
- return current;
1294
- return candidate > current ? candidate : current;
1295
- }
1296
- function queryProjectBreakdownScoped(db, requestWhere, sessionWhere, requestScopeParams = [], sessionScopeParams = [], machine) {
1297
- const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
1298
- const requestMachineClause = machine ? " AND machine_id = ?" : "";
1299
- const sessionMachineParams = machine ? [machine, machine] : [];
1300
- const requestMachineParams = machine ? [machine] : [];
1301
- const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
1302
- const sessionOnlyMachineParams = machine ? [machine] : [];
1303
- const sessions = db.prepare(`
1304
- SELECT id, project_path, project_name
1305
- FROM sessions
1306
- WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
1307
- `).all(...sessionMachineParams);
1308
- const groups = new Map;
1309
- const groupKeyBySession = new Map;
1310
- for (const s of sessions) {
1311
- const label = labelForPath(s.project_path, s.project_name);
1312
- if (!label)
1313
- continue;
1314
- const key = groupKeyForPath(s.project_path, s.project_name);
1315
- const g = groups.get(key) ?? { label, samplePath: s.project_path, sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: "" };
1316
- if (!g.samplePath)
1317
- g.samplePath = s.project_path;
1318
- groups.set(key, g);
1319
- groupKeyBySession.set(s.id, key);
1320
- }
1321
- const groupFor = (sessionId) => {
1322
- const key = groupKeyBySession.get(sessionId);
1323
- return key === undefined ? undefined : groups.get(key);
1324
- };
1325
- const requestRows = db.prepare(`
1326
- SELECT session_id,
1327
- COUNT(*) as requests,
1328
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1329
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1330
- MAX(timestamp) as last_active
1331
- FROM requests
1332
- WHERE ${requestWhere}${requestMachineClause}
1333
- GROUP BY session_id
1334
- `).all(...requestScopeParams, ...requestMachineParams);
1335
- for (const row of requestRows) {
1336
- const g = groupFor(row.session_id);
1337
- if (!g)
1338
- continue;
1339
- g.sessions += 1;
1340
- g.requests += row.requests;
1341
- g.cost_usd += row.cost_usd;
1342
- g.total_tokens += row.total_tokens;
1343
- g.last_active = laterTimestamp(g.last_active, row.last_active);
1344
- }
1345
- const sessionsWithRequests = new Set(db.prepare(`SELECT DISTINCT session_id FROM requests`).all().map((row) => row.session_id));
1346
- const sessionOnlyRows = db.prepare(`
1347
- SELECT id,
1348
- COALESCE(request_count, 0) as request_count,
1349
- COALESCE(total_tokens, 0) as total_tokens,
1350
- COALESCE(total_cost_usd, 0) as total_cost_usd,
1351
- started_at
1352
- FROM sessions
1353
- WHERE ${sessionWhere}${sessionOnlyMachineClause}
1354
- `).all(...sessionScopeParams, ...sessionOnlyMachineParams);
1355
- for (const row of sessionOnlyRows) {
1356
- if (sessionsWithRequests.has(row.id))
1357
- continue;
1358
- const g = groupFor(row.id);
1359
- if (!g)
1360
- continue;
1361
- g.sessions += 1;
1362
- g.requests += row.request_count;
1363
- g.total_tokens += row.total_tokens;
1364
- g.cost_usd += row.total_cost_usd;
1365
- g.last_active = laterTimestamp(g.last_active, row.started_at);
1366
- }
1367
- const result = [];
1368
- for (const g of groups.values()) {
1369
- if (g.sessions === 0)
1370
- continue;
1371
- result.push({
1372
- project_path: g.samplePath,
1373
- project_name: g.label,
1374
- sessions: g.sessions,
1375
- requests: g.requests,
1376
- total_tokens: g.total_tokens,
1377
- cost_usd: g.cost_usd,
1378
- last_active: g.last_active
1379
- });
1380
- }
1381
- result.sort((a, b) => b.cost_usd - a.cost_usd);
1382
- return result;
1383
- }
1384
- function queryProjectBreakdown(db, period = "all", machine) {
1385
- return queryProjectBreakdownScoped(db, requestPeriodWhere(period), sessionPeriodWhere(period), [], [], machine);
1386
- }
1387
- function normalizeAccountEmail(email) {
1388
- return (email ?? "").trim().toLowerCase();
1389
- }
1390
- function accountIdentityKey(agent, accountKey, accountName, accountEmail) {
1391
- const identityAgent = (agent || "").trim();
1392
- const normalizedEmail = normalizeAccountEmail(accountEmail);
1393
- if (identityAgent && normalizedEmail)
1394
- return `${identityAgent}:${normalizedEmail}`;
1395
- if (identityAgent && accountName)
1396
- return `${identityAgent}:${accountName}`;
1397
- if (accountKey)
1398
- return accountKey;
1399
- return identityAgent ? `${identityAgent}:unknown` : "";
1400
- }
1401
- function addAccountBreakdownRow(groups, row, sessionOnly) {
1402
- const agent = row.agent || row.account_tool;
1403
- const email = normalizeAccountEmail(row.account_email);
1404
- const accountName = row.account_name || email || row.account_key;
1405
- const key = accountIdentityKey(agent, row.account_key, accountName, email);
1406
- if (!key)
1407
- return;
1408
- const group = groups.get(key) ?? {
1409
- account_key: key,
1410
- account_tool: agent,
1411
- account_name: accountName,
1412
- account_email: email || null,
1413
- account_source: row.account_source || "unknown",
1414
- sessionIds: new Set,
1415
- requests: 0,
1416
- total_tokens: 0,
1417
- api_equivalent_usd: 0,
1418
- metered_api_usd: 0,
1419
- subscription_included_usd: 0,
1420
- estimated_usd: 0,
1421
- unknown_usd: 0,
1422
- last_active: ""
1423
- };
1424
- if (!group.account_email && email)
1425
- group.account_email = email;
1426
- if (!group.account_name && accountName)
1427
- group.account_name = accountName;
1428
- if ((!group.account_source || group.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
1429
- group.account_source = row.account_source;
1430
- }
1431
- if (row.session_id)
1432
- group.sessionIds.add(row.session_id);
1433
- group.requests += row.requests;
1434
- group.total_tokens += row.total_tokens;
1435
- group.api_equivalent_usd += row.cost_usd;
1436
- if (sessionOnly) {
1437
- group.estimated_usd += row.cost_usd;
1438
- } else if (row.cost_basis === "metered_api") {
1439
- group.metered_api_usd += row.cost_usd;
1440
- } else if (row.cost_basis === "subscription_included") {
1441
- group.subscription_included_usd += row.cost_usd;
1442
- } else if (row.cost_basis === "unknown") {
1443
- group.unknown_usd += row.cost_usd;
1444
- } else {
1445
- group.estimated_usd += row.cost_usd;
1446
- }
1447
- if (!group.last_active || row.last_active > group.last_active)
1448
- group.last_active = row.last_active;
1449
- groups.set(key, group);
1450
- }
1451
- function queryAccountBreakdown(db, period = "all", machine) {
1452
- const requestWhere = requestPeriodWhere(period);
1453
- const sessionWhere = sessionPeriodWhere(period);
1454
- const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
1455
- const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
1456
- const requestMachineParams = machine ? [machine] : [];
1457
- const sessionMachineParams = machine ? [machine] : [];
1458
- const groups = new Map;
1459
- const requestRows = db.prepare(`
1460
- SELECT
1461
- r.session_id as session_id,
1462
- COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
1463
- COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
1464
- COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
1465
- COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
1466
- COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
1467
- COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
1468
- 1 as requests,
1469
- COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1470
- COALESCE(r.cost_usd, 0) as cost_usd,
1471
- COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1472
- r.timestamp as last_active
1473
- FROM requests r
1474
- LEFT JOIN sessions s ON s.id = r.session_id
1475
- WHERE ${requestWhere}${requestMachineClause}
1476
- AND (
1477
- COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
1478
- OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
1479
- OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
1480
- OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
1481
- )
1482
- `).all(...requestMachineParams);
1483
- for (const row of requestRows)
1484
- addAccountBreakdownRow(groups, row, false);
1485
- const sessionOnlyRows = db.prepare(`
1486
- SELECT
1487
- s.id as session_id,
1488
- s.agent as agent,
1489
- s.account_key as account_key,
1490
- s.account_tool as account_tool,
1491
- s.account_name as account_name,
1492
- s.account_email as account_email,
1493
- COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
1494
- COALESCE(s.request_count, 0) as requests,
1495
- COALESCE(s.total_tokens, 0) as total_tokens,
1496
- COALESCE(s.total_cost_usd, 0) as cost_usd,
1497
- 'estimated' as cost_basis,
1498
- s.started_at as last_active
1499
- FROM sessions s
1500
- WHERE ${sessionWhere}${sessionMachineClause}
1501
- AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1502
- AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
1503
- `).all(...sessionMachineParams);
1504
- for (const row of sessionOnlyRows)
1505
- addAccountBreakdownRow(groups, row, true);
1506
- const result = [...groups.values()].map((group) => ({
1507
- account_key: group.account_key,
1508
- account_tool: group.account_tool,
1509
- account_name: group.account_name,
1510
- account_email: group.account_email,
1511
- account_source: group.account_source,
1512
- sessions: group.sessionIds.size,
1513
- requests: group.requests,
1514
- total_tokens: group.total_tokens,
1515
- api_equivalent_usd: group.api_equivalent_usd,
1516
- billable_usd: group.metered_api_usd,
1517
- metered_api_usd: group.metered_api_usd,
1518
- subscription_included_usd: group.subscription_included_usd,
1519
- estimated_usd: group.estimated_usd,
1520
- unknown_usd: group.unknown_usd,
1521
- cost_usd: group.api_equivalent_usd,
1522
- last_active: group.last_active
1523
- }));
1524
- result.sort((a, b) => b.cost_usd - a.cost_usd);
1525
- return result;
1526
- }
1527
- function kindFromCostCenterId(id) {
1528
- const prefix = id.split(":", 1)[0] ?? "";
1529
- return ["loop", "app", "repo", "service", "team"].includes(prefix) ? prefix : "";
1530
- }
1531
- function addCostCenterBreakdownRow(groups, row, sessionOnly) {
1532
- if (!row.cost_center_id)
1533
- return;
1534
- const derivedKind = row.kind || kindFromCostCenterId(row.cost_center_id) || "service";
1535
- const group = groups.get(row.cost_center_id) ?? {
1536
- cost_center_id: row.cost_center_id,
1537
- kind: derivedKind,
1538
- name: row.name || row.cost_center_id,
1539
- repo_path: row.repo_path ?? null,
1540
- labels_json: row.labels_json || "{}",
1541
- sessionIds: new Set,
1542
- requests: 0,
1543
- total_tokens: 0,
1544
- api_equivalent_usd: 0,
1545
- metered_api_usd: 0,
1546
- subscription_included_usd: 0,
1547
- estimated_usd: 0,
1548
- unknown_usd: 0,
1549
- last_active: ""
1550
- };
1551
- group.kind = derivedKind;
1552
- if (row.name)
1553
- group.name = row.name;
1554
- if (row.repo_path && !group.repo_path)
1555
- group.repo_path = row.repo_path;
1556
- if (row.labels_json && row.labels_json !== "{}")
1557
- group.labels_json = row.labels_json;
1558
- if (row.session_id)
1559
- group.sessionIds.add(row.session_id);
1560
- group.requests += row.requests;
1561
- group.total_tokens += row.total_tokens;
1562
- group.api_equivalent_usd += row.cost_usd;
1563
- if (sessionOnly) {
1564
- group.estimated_usd += row.cost_usd;
1565
- } else if (row.cost_basis === "metered_api") {
1566
- group.metered_api_usd += row.cost_usd;
1567
- } else if (row.cost_basis === "subscription_included") {
1568
- group.subscription_included_usd += row.cost_usd;
1569
- } else if (row.cost_basis === "unknown") {
1570
- group.unknown_usd += row.cost_usd;
1571
- } else {
1572
- group.estimated_usd += row.cost_usd;
1573
- }
1574
- if (!group.last_active || row.last_active > group.last_active)
1575
- group.last_active = row.last_active;
1576
- groups.set(row.cost_center_id, group);
1577
- }
1578
- function queryCostCenterBreakdown(db, period = "all", filter = {}) {
1579
- const requestWhere = filter.since ? "r.timestamp >= ?" : requestPeriodWhere(period);
1580
- const sessionWhere = filter.since ? "s.started_at >= ?" : sessionPeriodWhere(period);
1581
- const requestMachineClause = filter.machine ? " AND r.machine_id = ?" : "";
1582
- const sessionMachineClause = filter.machine ? " AND s.machine_id = ?" : "";
1583
- const requestParams = [];
1584
- const sessionParams = [];
1585
- if (filter.since) {
1586
- requestParams.push(filter.since);
1587
- sessionParams.push(filter.since);
1588
- }
1589
- if (filter.machine) {
1590
- requestParams.push(filter.machine);
1591
- sessionParams.push(filter.machine);
1592
- }
1593
- const groups = new Map;
1594
- const requestRows = db.prepare(`
1595
- SELECT
1596
- COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) as cost_center_id,
1597
- COALESCE(cc.kind, '') as kind,
1598
- COALESCE(cc.name, COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))) as name,
1599
- cc.repo_path as repo_path,
1600
- COALESCE(cc.labels_json, '{}') as labels_json,
1601
- r.session_id as session_id,
1602
- 1 as requests,
1603
- COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1604
- COALESCE(r.cost_usd, 0) as cost_usd,
1605
- COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1606
- r.timestamp as last_active
1607
- FROM requests r
1608
- LEFT JOIN sessions s ON s.id = r.session_id
1609
- LEFT JOIN cost_centers cc ON cc.id = COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))
1610
- WHERE ${requestWhere}${requestMachineClause}
1611
- AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) IS NOT NULL
1612
- AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) != ''
1613
- `).all(...requestParams);
1614
- for (const row of requestRows)
1615
- addCostCenterBreakdownRow(groups, row, false);
1616
- const sessionOnlyRows = db.prepare(`
1617
- SELECT
1618
- s.cost_center_id as cost_center_id,
1619
- COALESCE(cc.kind, '') as kind,
1620
- COALESCE(cc.name, s.cost_center_id) as name,
1621
- cc.repo_path as repo_path,
1622
- COALESCE(cc.labels_json, '{}') as labels_json,
1623
- s.id as session_id,
1624
- COALESCE(s.request_count, 0) as requests,
1625
- COALESCE(s.total_tokens, 0) as total_tokens,
1626
- COALESCE(s.total_cost_usd, 0) as cost_usd,
1627
- 'estimated' as cost_basis,
1628
- s.started_at as last_active
1629
- FROM sessions s
1630
- LEFT JOIN cost_centers cc ON cc.id = s.cost_center_id
1631
- WHERE ${sessionWhere}${sessionMachineClause}
1632
- AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1633
- AND s.cost_center_id IS NOT NULL
1634
- AND s.cost_center_id != ''
1635
- `).all(...sessionParams);
1636
- for (const row of sessionOnlyRows)
1637
- addCostCenterBreakdownRow(groups, row, true);
1638
- const result = [...groups.values()].map((group) => ({
1639
- cost_center_id: group.cost_center_id,
1640
- kind: group.kind,
1641
- name: group.name,
1642
- repo_path: group.repo_path,
1643
- labels_json: group.labels_json,
1644
- sessions: group.sessionIds.size,
1645
- requests: group.requests,
1646
- total_tokens: group.total_tokens,
1647
- api_equivalent_usd: group.api_equivalent_usd,
1648
- billable_usd: group.metered_api_usd,
1649
- metered_api_usd: group.metered_api_usd,
1650
- subscription_included_usd: group.subscription_included_usd,
1651
- estimated_usd: group.estimated_usd,
1652
- unknown_usd: group.unknown_usd,
1653
- cost_usd: group.api_equivalent_usd,
1654
- last_active: group.last_active
1655
- }));
1656
- const filtered = filter.kind ? result.filter((row) => row.kind === filter.kind) : result;
1657
- filtered.sort((a, b) => b.cost_usd - a.cost_usd);
1658
- return filtered;
1659
- }
1660
- function queryProjectBreakdownSince(db, since, machine) {
1661
- return queryProjectBreakdownScoped(db, "timestamp >= ?", "started_at >= ?", [since], [since], machine);
1662
- }
1663
- function queryAgentBreakdownSince(db, since) {
1664
- return db.prepare(`
1665
- SELECT agent,
1666
- COUNT(DISTINCT session_id) as sessions,
1667
- COUNT(*) as requests,
1668
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1669
- COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
1670
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
1671
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1672
- MAX(timestamp) as last_active
1673
- FROM requests
1674
- WHERE timestamp >= ?
1675
- GROUP BY agent
1676
- ORDER BY api_equivalent_usd DESC
1677
- `).all(since);
1678
- }
1679
- function queryModelBreakdownSince(db, since) {
1680
- return db.prepare(`
1681
- SELECT model, agent,
1682
- COUNT(*) as requests,
1683
- COALESCE(SUM(input_tokens), 0) as input_tokens,
1684
- COALESCE(SUM(output_tokens), 0) as output_tokens,
1685
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1686
- COALESCE(SUM(cost_usd), 0) as cost_usd
1687
- FROM requests WHERE timestamp >= ?
1688
- GROUP BY model, agent ORDER BY cost_usd DESC
1689
- `).all(since);
1690
- }
1691
- function queryAccountBreakdownSince(db, since) {
1692
- return db.prepare(`
1693
- WITH request_rows AS (
1694
- SELECT
1695
- r.session_id as session_id,
1696
- COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
1697
- COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as raw_account_key,
1698
- COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as raw_account_name,
1699
- LOWER(TRIM(COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''))) as raw_account_email,
1700
- COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
1701
- 1 as requests,
1702
- COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1703
- COALESCE(r.cost_usd, 0) as cost_usd,
1704
- COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1705
- r.timestamp as last_active
1706
- FROM requests r
1707
- LEFT JOIN sessions s ON s.id = r.session_id
1708
- WHERE r.timestamp >= ?
1709
- AND (
1710
- COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
1711
- OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
1712
- OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
1713
- OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
1714
- )
1715
- ),
1716
- session_only_rows AS (
1717
- SELECT
1718
- s.id as session_id,
1719
- COALESCE(NULLIF(s.agent, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
1720
- s.account_key as raw_account_key,
1721
- s.account_name as raw_account_name,
1722
- LOWER(TRIM(COALESCE(s.account_email, ''))) as raw_account_email,
1723
- COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
1724
- COALESCE(s.request_count, 0) as requests,
1725
- COALESCE(s.total_tokens, 0) as total_tokens,
1726
- COALESCE(s.total_cost_usd, 0) as cost_usd,
1727
- 'estimated' as cost_basis,
1728
- s.started_at as last_active
1729
- FROM sessions s
1730
- WHERE s.started_at >= ?
1731
- AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1732
- AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
1733
- ),
1734
- normalized AS (
1735
- SELECT
1736
- CASE
1737
- WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
1738
- WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
1739
- ELSE raw_account_key
1740
- END as account_key,
1741
- account_agent as account_tool,
1742
- raw_account_name as account_name,
1743
- raw_account_email as account_email,
1744
- account_source,
1745
- session_id,
1746
- requests,
1747
- total_tokens,
1748
- cost_usd,
1749
- cost_basis,
1750
- last_active
1751
- FROM request_rows
1752
- UNION ALL
1753
- SELECT
1754
- CASE
1755
- WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
1756
- WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
1757
- ELSE raw_account_key
1758
- END as account_key,
1759
- account_agent as account_tool,
1760
- raw_account_name as account_name,
1761
- raw_account_email as account_email,
1762
- account_source,
1763
- session_id,
1764
- requests,
1765
- total_tokens,
1766
- cost_usd,
1767
- cost_basis,
1768
- last_active
1769
- FROM session_only_rows
1770
- )
1771
- SELECT account_key,
1772
- account_tool,
1773
- COALESCE(MAX(NULLIF(account_name, '')), MAX(NULLIF(account_email, '')), account_key) as account_name,
1774
- NULLIF(account_email, '') as account_email,
1775
- COALESCE(MAX(NULLIF(account_source, 'unknown')), 'unknown') as account_source,
1776
- COUNT(DISTINCT session_id) as sessions,
1777
- COALESCE(SUM(requests), 0) as requests,
1778
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1779
- COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
1780
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
1781
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1782
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1783
- COALESCE(SUM(CASE WHEN cost_basis NOT IN ('metered_api', 'subscription_included', 'unknown') THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1784
- COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
1785
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1786
- MAX(last_active) as last_active
1787
- FROM normalized
1788
- WHERE account_key != ''
1789
- GROUP BY account_key, account_tool, account_email
1790
- ORDER BY api_equivalent_usd DESC
1791
- `).all(since, since);
1792
- }
1793
- function queryDailyBreakdown(db, days = 30, machine) {
1794
- const machineClause = machine ? " AND machine_id = ?" : "";
1795
- const params = machine ? [`-${days}`, machine] : [`-${days}`];
1796
- return db.prepare(`
1797
- SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
1798
- FROM requests
1799
- WHERE timestamp >= DATE('now', ? || ' days')${machineClause}
1800
- GROUP BY DATE(timestamp), agent
1801
- ORDER BY date ASC
1802
- `).all(...params);
1803
- }
1804
- function upsertProject(db, project) {
1805
- db.prepare(`
1806
- INSERT OR REPLACE INTO projects (id, path, name, description, tags, created_at)
1807
- VALUES (?, ?, ?, ?, ?, ?)
1808
- `).run(project.id, project.path, project.name, project.description ?? null, JSON.stringify(project.tags), project.created_at);
1809
- }
1810
- function getProject(db, path) {
1811
- const row = db.prepare(`SELECT * FROM projects WHERE path = ?`).get(path);
1812
- if (!row)
1813
- return null;
1814
- return { ...row, tags: JSON.parse(row["tags"] ?? "[]") };
1815
- }
1816
- function deleteProject(db, path) {
1817
- db.prepare(`DELETE FROM projects WHERE path = ?`).run(path);
1818
- }
1819
- function upsertBudget(db, budget) {
1820
- db.prepare(`
1821
- INSERT OR REPLACE INTO budgets
1822
- (id, project_path, agent, cost_center_id, period, limit_usd, alert_at_percent, created_at, updated_at)
1823
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1824
- `).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.cost_center_id ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
1825
- }
1826
- function listBudgets(db) {
1827
- return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
1828
- }
1829
- function deleteBudget(db, id) {
1830
- db.prepare(`DELETE FROM budgets WHERE id = ?`).run(id);
1831
- }
1832
- function getBudgetStatuses(db) {
1833
- const budgets = listBudgets(db);
1834
- return budgets.map((b) => {
1835
- const periodStart = b.period === "daily" ? "DATE('now')" : b.period === "weekly" ? "DATE('now', '-7 days')" : "DATE('now', '-30 days')";
1836
- let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
1837
- const params = [];
1838
- if (b.project_path) {
1839
- spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
1840
- params.push(b.project_path);
1841
- }
1842
- if (b.agent) {
1843
- spendQuery += ` AND agent = ?`;
1844
- params.push(b.agent);
1845
- }
1846
- if (b.cost_center_id) {
1847
- spendQuery += ` AND COALESCE(NULLIF(cost_center_id, ''), (SELECT NULLIF(s.cost_center_id, '') FROM sessions s WHERE s.id = requests.session_id)) = ?`;
1848
- params.push(b.cost_center_id);
1849
- }
1850
- const row = db.prepare(spendQuery).get(...params);
1851
- const spend = row.spend;
1852
- const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
1853
- return {
1854
- ...b,
1855
- current_spend_usd: spend,
1856
- percent_used: percent,
1857
- is_over_limit: percent >= 100,
1858
- is_over_alert: percent >= b.alert_at_percent
1859
- };
1860
- });
1861
- }
1862
- function upsertGoal(db, goal) {
1863
- db.prepare(`
1864
- INSERT OR REPLACE INTO goals
1865
- (id, period, project_path, agent, limit_usd, created_at, updated_at)
1866
- VALUES (?, ?, ?, ?, ?, ?, ?)
1867
- `).run(goal.id, goal.period, goal.project_path ?? null, goal.agent ?? null, goal.limit_usd, goal.created_at, goal.updated_at);
1868
- }
1869
- function deleteGoal(db, id) {
1870
- db.prepare(`DELETE FROM goals WHERE id = ?`).run(id);
1871
- }
1872
- function listGoals(db) {
1873
- return db.prepare(`SELECT * FROM goals ORDER BY created_at DESC`).all();
1874
- }
1875
- function getGoalStatuses(db) {
1876
- const goals = listGoals(db);
1877
- return goals.map((g) => {
1878
- const periodStart = g.period === "day" ? "DATE('now')" : g.period === "week" ? "DATE('now', '-7 days')" : g.period === "month" ? "DATE('now', '-30 days')" : "DATE('now', '-365 days')";
1879
- let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
1880
- const params = [];
1881
- if (g.project_path) {
1882
- spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
1883
- params.push(g.project_path);
1884
- }
1885
- if (g.agent) {
1886
- spendQuery += ` AND agent = ?`;
1887
- params.push(g.agent);
1888
- }
1889
- const row = db.prepare(spendQuery).get(...params);
1890
- const spend = row.spend;
1891
- const percent = g.limit_usd > 0 ? spend / g.limit_usd * 100 : 0;
1892
- return {
1893
- ...g,
1894
- current_spend_usd: spend,
1895
- percent_used: percent,
1896
- is_on_track: percent < 70,
1897
- is_at_risk: percent >= 70 && percent <= 100,
1898
- is_over: percent > 100
1899
- };
1900
- });
1901
- }
1902
- function queryRequestsSince(db, since) {
1903
- return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
1904
- }
1905
- function insertFeedback(db, record) {
1906
- db.prepare(`INSERT INTO feedback (message, email, category, version, machine_id) VALUES (?, ?, ?, ?, ?)`).run(record.message, record.email ?? null, record.category ?? "general", record.version ?? null, record.machine_id ?? null);
1907
- }
1908
- function billingPeriodWhere(period) {
1909
- return 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";
1910
- }
1911
- function queryBillingSummary(db, period) {
1912
- const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${billingPeriodWhere(period)} GROUP BY provider`).all();
1913
- const by_provider = {};
1914
- let total = 0;
1915
- for (const r of rows) {
1916
- by_provider[r.provider] = r.cost;
1917
- total += r.cost;
1918
- }
1919
- return { total_usd: total, by_provider };
1920
- }
1921
- function countBillingRecords(db, period) {
1922
- const row = db.prepare(`SELECT COUNT(*) as c FROM billing_daily WHERE ${billingPeriodWhere(period)}`).get();
1923
- return row?.c ?? 0;
1924
- }
1925
- function getSessionDetail(db, id) {
1926
- const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `${id}%`);
1927
- if (!session)
1928
- return null;
1929
- const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC`).all(session["id"]);
1930
- return { session, requests };
1931
- }
1932
- function listMachines(db, period = "all") {
1933
- const rWhere = requestPeriodWhere(period);
1934
- const sWhere = sessionPeriodWhere(period);
1935
- return db.prepare(`
1936
- WITH request_stats AS (
1937
- SELECT
1938
- machine_id,
1939
- COUNT(DISTINCT session_id) as sessions,
1940
- COUNT(*) as requests,
1941
- COALESCE(SUM(cost_usd), 0) as total_cost_usd,
1942
- MAX(timestamp) as last_active
1943
- FROM requests
1944
- WHERE machine_id != ''
1945
- AND ${rWhere}
1946
- GROUP BY machine_id
1947
- ),
1948
- session_only_stats AS (
1949
- SELECT
1950
- machine_id,
1951
- COUNT(*) as sessions,
1952
- COALESCE(SUM(request_count), 0) as requests,
1953
- COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
1954
- MAX(started_at) as last_active
1955
- FROM sessions
1956
- WHERE machine_id != ''
1957
- AND ${sWhere}
1958
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1959
- GROUP BY machine_id
1960
- ),
1961
- combined AS (
1962
- SELECT * FROM request_stats
1963
- UNION ALL
1964
- SELECT * FROM session_only_stats
1965
- )
1966
- SELECT
1967
- machine_id,
1968
- COALESCE(SUM(sessions), 0) as sessions,
1969
- COALESCE(SUM(requests), 0) as requests,
1970
- COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
1971
- MAX(last_active) as last_active
1972
- FROM combined
1973
- GROUP BY machine_id
1974
- ORDER BY total_cost_usd DESC
1975
- `).all();
1976
- }
1977
- function upsertModelPricing(db, p) {
1978
- db.prepare(`
1979
- INSERT OR REPLACE INTO model_pricing
1980
- (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)
1981
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1982
- `).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);
1983
- }
1984
- function getModelPricing(db, model) {
1985
- return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
1986
- }
1987
- function listModelPricing(db) {
1988
- return db.prepare(`SELECT * FROM model_pricing ORDER BY model ASC`).all();
1989
- }
1990
- function deleteModelPricing(db, model) {
1991
- db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
1992
- }
1993
- function seedModelPricing(db, defaults) {
1994
- const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
1995
- const now = new Date().toISOString();
1996
- for (const [model, p] of Object.entries(defaults)) {
1997
- if (existing.has(model))
1998
- continue;
1999
- upsertModelPricing(db, {
2000
- model,
2001
- input_per_1m: p.inputPer1M,
2002
- output_per_1m: p.outputPer1M,
2003
- cache_read_per_1m: p.cacheReadPer1M,
2004
- cache_write_per_1m: p.cacheWritePer1M,
2005
- cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
2006
- cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
2007
- updated_at: now
2008
- });
2009
- }
2010
- }
2011
- function upsertSubscription(db, sub) {
2012
- db.prepare(`
2013
- INSERT OR REPLACE INTO subscriptions
2014
- (id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
2015
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2016
- `).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);
2017
- }
2018
- function listSubscriptions(db) {
2019
- return db.prepare(`SELECT * FROM subscriptions ORDER BY provider, plan`).all();
2020
- }
2021
- function deleteSubscription(db, id) {
2022
- db.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(id);
2023
- }
2024
- function queryUsageSnapshots(db, opts = {}) {
2025
- const conditions = [];
2026
- const params = [];
2027
- if (opts.agent) {
2028
- conditions.push("agent = ?");
2029
- params.push(opts.agent);
2030
- }
2031
- if (opts.date) {
2032
- conditions.push("date = ?");
2033
- params.push(opts.date);
2034
- }
2035
- if (opts.since) {
2036
- conditions.push("date >= ?");
2037
- params.push(opts.since);
2038
- }
2039
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
2040
- return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
2041
- }
2042
- function listMachineRegistry(db) {
2043
- return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
2044
- }
2045
- var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
2046
- var init_database = __esm(() => {
2047
- init_sqlite_adapter();
2048
- PATHS_RESOLVER_KIND_ENV = {
2049
- config: "HASNA_CONFIG_HOME",
2050
- data: "HASNA_DATA_HOME",
2051
- state: "HASNA_STATE_HOME",
2052
- cache: "HASNA_CACHE_HOME"
2053
- };
2054
- PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2055
- });
2056
-
2057
- // src/lib/agents.ts
2058
- var AGENTS = [
2059
- "claude",
2060
- "takumi",
2061
- "codex",
2062
- "gemini",
2063
- "opencode",
2064
- "cursor",
2065
- "pi",
2066
- "hermes"
2067
- ];
2068
- var COST_BASIS = [
2069
- "metered_api",
2070
- "subscription_included",
2071
- "estimated",
2072
- "unknown"
2073
- ];
2074
- function isAgent(value) {
2075
- return AGENTS.includes(value);
2076
- }
2
+ import {
3
+ AGENTS,
4
+ COST_BASIS,
5
+ MAX_INGEST_BYTES,
6
+ __require,
7
+ cacheDir,
8
+ calendarPeriodBounds,
9
+ calendarPeriodWhere,
10
+ canonicalIngestBodyHash,
11
+ deleteBudget,
12
+ deleteGoal,
13
+ deleteModelPricing,
14
+ deleteProject,
15
+ deleteSubscription,
16
+ ensurePricingSeeded,
17
+ estimateCostFromRows,
18
+ getBudgetStatuses,
19
+ getGoalStatuses,
20
+ getMachineId,
21
+ getProject,
22
+ getSessionDetail,
23
+ hasKnownUsagePrice,
24
+ insertFeedback,
25
+ isAgent,
26
+ listMachineRegistry,
27
+ listMachines,
28
+ listModelPricing,
29
+ listSubscriptions,
30
+ queryAccountBreakdown,
31
+ queryAccountBreakdownSince,
32
+ queryAgentBreakdown,
33
+ queryAgentBreakdownSince,
34
+ queryBillingSummary,
35
+ queryCostCenterBreakdown,
36
+ queryDailyBreakdown,
37
+ queryModelBreakdown,
38
+ queryModelBreakdownSince,
39
+ queryProjectBreakdown,
40
+ queryProjectBreakdownSince,
41
+ queryRequestsSince,
42
+ querySessions,
43
+ querySummary,
44
+ queryTopSessions,
45
+ queryUsageSnapshots,
46
+ requireCurrentBillingSummary,
47
+ requireCurrentCostSummary,
48
+ sessionOverlapWhere,
49
+ stableJson,
50
+ subscriptionId,
51
+ upsertBudget,
52
+ upsertGoal,
53
+ upsertModelPricing,
54
+ upsertProject,
55
+ upsertSubscription,
56
+ usageSnapshotFilterForPeriod,
57
+ validateIngest
58
+ } from "./chunks/index-zpvea08j.js";
2077
59
  // src/lib/store/index.ts
2078
- init_database();
2079
- init_pricing();
2080
60
  import { randomUUID } from "crypto";
2081
61
  // package.json
2082
62
  var package_default = {
2083
63
  name: "@hasna/economy",
2084
- version: "0.4.0",
64
+ version: "0.5.0",
2085
65
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
2086
66
  repository: {
2087
67
  type: "git",
2088
- url: "git+https://github.com/hasna/economy.git"
68
+ url: "https://github.com/hasna/apps.git",
69
+ directory: "apps/economy"
2089
70
  },
2090
71
  bugs: {
2091
- url: "https://github.com/hasna/economy/issues"
72
+ url: "https://github.com/hasna/apps/issues"
2092
73
  },
2093
- homepage: "https://github.com/hasna/economy#readme",
74
+ homepage: "https://github.com/hasna/apps/tree/main/apps/economy#readme",
2094
75
  type: "module",
2095
76
  main: "dist/index.js",
2096
77
  types: "dist/index.d.ts",
@@ -2119,14 +100,15 @@ var package_default = {
2119
100
  "CHANGELOG.md",
2120
101
  "SECURITY.md",
2121
102
  "CONTRIBUTING.md",
2122
- "CODE_OF_CONDUCT.md"
103
+ "CODE_OF_CONDUCT.md",
104
+ "!dist/lib/test-hermetic-*"
2123
105
  ],
2124
106
  scripts: {
2125
- build: "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/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/otel/index.ts --outdir dist/otel --target bun --packages external && bun build src/index.ts --outdir dist --target bun --packages external && tsc --emitDeclarationOnly --outDir dist",
2126
- "build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
2127
- "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
2128
- "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
2129
- "build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
107
+ build: 'bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external && bun build src/otel/index.ts --outdir dist/otel --target bun --packages external && bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && tsc --emitDeclarationOnly --outDir dist',
108
+ "build:cli": 'bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
109
+ "build:mcp": 'bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
110
+ "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external",
111
+ "build:lib": 'bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
2130
112
  "artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
2131
113
  prepack: "bun run build && bun run artifact-scan",
2132
114
  typecheck: "tsc --noEmit",
@@ -2163,7 +145,8 @@ var package_default = {
2163
145
  chalk: "^5.4.1",
2164
146
  commander: "^13.1.0",
2165
147
  pg: "8.13.1",
2166
- zod: "^3.24.2"
148
+ zod: "^3.24.2",
149
+ "@hasna/trash": "0.2.3"
2167
150
  },
2168
151
  devDependencies: {
2169
152
  "@types/bun": "1.3.14",
@@ -2190,254 +173,277 @@ function getPackageMetadata() {
2190
173
  var packageMetadata = getPackageMetadata();
2191
174
 
2192
175
  // src/lib/savings.ts
2193
- function periodWhere2(period, column) {
2194
- switch (period) {
2195
- case "today":
2196
- return `DATE(${column}) = DATE('now')`;
2197
- case "yesterday":
2198
- return `DATE(${column}) = DATE('now', '-1 day')`;
2199
- case "week":
2200
- return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
2201
- case "month":
2202
- return `${column} >= DATE('now', 'start of month')`;
2203
- case "year":
2204
- return `${column} >= DATE('now', 'start of year')`;
2205
- case "all":
2206
- return "1=1";
2207
- }
2208
- }
2209
- function prorateMonthlyFee(monthlyFee, period) {
2210
- const now = new Date;
2211
- const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
2212
- switch (period) {
2213
- case "today":
2214
- case "yesterday":
2215
- return monthlyFee / daysInMonth;
2216
- case "week":
2217
- return monthlyFee / daysInMonth * 7;
2218
- case "month":
2219
- return monthlyFee;
2220
- case "year":
2221
- return monthlyFee * 12;
2222
- case "all":
2223
- return monthlyFee;
2224
- }
2225
- }
2226
- function proratedIncludedConsumed(includedUsage, includedCap, period) {
2227
- const cap = prorateMonthlyFee(includedCap, period);
2228
- return cap > 0 ? Math.min(includedUsage, cap) : includedUsage;
2229
- }
2230
- function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
2231
- return Math.max(0, apiEquivalent - onDemand - subscriptionFee);
176
+ function requireCurrentSavingsReport(value) {
177
+ if (!value || typeof value !== "object")
178
+ throw new Error("SAVINGS_REPORT_UNAVAILABLE: Invalid savings response");
179
+ const report = value;
180
+ if (report.comparison_basis !== "included_usage_vs_current_plan_estimate" || !["estimated", "not_applicable", "unavailable"].includes(String(report.comparison_status))) {
181
+ throw new Error("SAVINGS_REPORT_UNAVAILABLE: The service must support the current subscription comparison contract");
182
+ }
183
+ for (const field of ["api_equivalent_usd", "subscription_fee_usd", "on_demand_usd", "saved_usd", "net_comparison_usd"]) {
184
+ const number = report[field];
185
+ if (number !== null && (typeof number !== "number" || !Number.isFinite(number))) {
186
+ throw new Error("SAVINGS_REPORT_UNAVAILABLE: Invalid monetary field " + field);
187
+ }
188
+ }
189
+ if (report.comparison_status === "unavailable" && (report.saved_usd !== null || report.net_comparison_usd !== null)) {
190
+ throw new Error("SAVINGS_REPORT_UNAVAILABLE: Unavailable comparison carries a monetary claim");
191
+ }
192
+ return value;
2232
193
  }
2233
- function querySavingsSummary(db, period, agent) {
2234
- const where = periodWhere2(period, "timestamp");
2235
- const agentClause = agent ? " AND agent = ?" : "";
2236
- const params = agent ? [agent] : [];
2237
- const apiRow = db.prepare(`
2238
- SELECT COALESCE(SUM(cost_usd), 0) as total
2239
- FROM requests
2240
- WHERE ${where}${agentClause}
2241
- AND COALESCE(cost_basis, 'estimated') IN ('metered_api', 'estimated', 'unknown')
2242
- `).get(...params);
2243
- const includedRow = db.prepare(`
2244
- SELECT COALESCE(SUM(cost_usd), 0) as total
2245
- FROM requests
2246
- WHERE ${where}${agentClause}
2247
- AND cost_basis = 'subscription_included'
2248
- `).get(...params);
2249
- const subWhere = periodWhere2(period, "date");
2250
- const onDemandRow = db.prepare(`
2251
- SELECT COALESCE(SUM(value), 0) as total
2252
- FROM usage_snapshots
2253
- WHERE ${subWhere}${agent ? " AND agent = ?" : ""}
2254
- AND metric = 'on_demand_usd'
2255
- `).get(...params);
2256
- const subs = db.prepare(`
2257
- SELECT
2258
- COALESCE(SUM(monthly_fee_usd), 0) as fee,
2259
- COALESCE(SUM(included_usage_usd), 0) as included
2260
- FROM subscriptions
2261
- WHERE active = 1${agent ? " AND (agent = ? OR agent IS NULL)" : ""}
2262
- `).get(...agent ? [agent] : []);
2263
- const subscriptionFee = prorateMonthlyFee(subs.fee, period);
2264
- const apiEquivalent = apiRow.total + includedRow.total;
2265
- const includedConsumed = proratedIncludedConsumed(includedRow.total, subs.included, period);
2266
- const onDemand = onDemandRow.total;
2267
- const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
2268
- const byAgent = {};
2269
- if (!agent) {
2270
- const onDemandByAgent = new Map;
2271
- for (const row of db.prepare(`
2272
- SELECT agent, COALESCE(SUM(value), 0) as total
2273
- FROM usage_snapshots
2274
- WHERE ${subWhere}
2275
- AND metric = 'on_demand_usd'
2276
- GROUP BY agent
2277
- `).all()) {
2278
- onDemandByAgent.set(row.agent, row.total);
194
+ function prorateMonthlyFee(monthlyFee, period, now = new Date) {
195
+ if (!Number.isFinite(monthlyFee) || monthlyFee < 0)
196
+ throw new RangeError("Monthly fee must be finite and nonnegative");
197
+ const bounds = calendarPeriodBounds(period, now);
198
+ if (bounds.start_date === null)
199
+ return null;
200
+ let cursor = new Date(bounds.start_date + "T00:00:00.000Z");
201
+ const end = new Date(bounds.end_date_exclusive + "T00:00:00.000Z");
202
+ let fee = 0;
203
+ while (cursor < end) {
204
+ const monthEnd = new Date(Date.UTC(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, 1));
205
+ const next = new Date(Math.min(end.getTime(), monthEnd.getTime()));
206
+ const days = (next.getTime() - cursor.getTime()) / 86400000;
207
+ const monthDays = new Date(Date.UTC(cursor.getUTCFullYear(), cursor.getUTCMonth() + 1, 0)).getUTCDate();
208
+ fee += monthlyFee / monthDays * days;
209
+ cursor = next;
210
+ }
211
+ return fee;
212
+ }
213
+ function feeFor(plans, period, now) {
214
+ let known = 0;
215
+ let complete = true;
216
+ for (const plan of plans) {
217
+ if (plan.fee_basis !== "observed" && plan.fee_basis !== "configured" || plan.monthly_fee_usd === null || !Number.isFinite(plan.monthly_fee_usd) || plan.monthly_fee_usd < 0) {
218
+ complete = false;
219
+ continue;
2279
220
  }
2280
- const subscriptionByAgent = new Map;
2281
- for (const row of db.prepare(`
2282
- SELECT agent,
2283
- COALESCE(SUM(monthly_fee_usd), 0) as fee,
2284
- COALESCE(SUM(included_usage_usd), 0) as included
2285
- FROM subscriptions
2286
- WHERE active = 1 AND agent IS NOT NULL
2287
- GROUP BY agent
2288
- `).all()) {
2289
- subscriptionByAgent.set(row.agent, row);
221
+ const amount = prorateMonthlyFee(plan.monthly_fee_usd, period, now);
222
+ if (amount === null || !Number.isFinite(amount) || !Number.isFinite(known + amount))
223
+ complete = false;
224
+ else
225
+ known += amount;
226
+ }
227
+ return { known, total: complete ? known : null };
228
+ }
229
+ function summarizeOnDemand(observations, period, now = new Date) {
230
+ const rows = observations.filter((row) => row.metric === "on_demand_usd");
231
+ if (!rows.length)
232
+ return { usd: null, status: "not_observed" };
233
+ const bounds = calendarPeriodBounds(period, now);
234
+ const start = bounds.start_date ? Date.parse(bounds.start_date + "T00:00:00Z") : -Infinity;
235
+ const end = bounds.end_date_exclusive ? Date.parse(bounds.end_date_exclusive + "T00:00:00Z") : Infinity;
236
+ const windows = new Map;
237
+ for (const row of rows) {
238
+ const from = Date.parse(row.window_start ?? "");
239
+ const until = Date.parse(row.window_end ?? "");
240
+ const at = Date.parse(row.source_timestamp ?? row.observed_at ?? "");
241
+ if (row.unit.toLowerCase() !== "usd" || !Number.isFinite(row.value) || row.value < 0 || !row.provider || !row.account_key || !Number.isFinite(from) || !Number.isFinite(until) || from >= until || !Number.isFinite(at) || row.value_kind !== "cumulative" && row.value_kind !== "delta") {
242
+ return { usd: null, status: "unavailable" };
2290
243
  }
2291
- const globalSubs = db.prepare(`
2292
- SELECT
2293
- COALESCE(SUM(monthly_fee_usd), 0) as fee,
2294
- COALESCE(SUM(included_usage_usd), 0) as included
2295
- FROM subscriptions
2296
- WHERE active = 1 AND agent IS NULL
2297
- `).get();
2298
- const rows = db.prepare(`
2299
- SELECT agent,
2300
- COALESCE(SUM(cost_usd), 0) as api_eq,
2301
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as included
2302
- FROM requests WHERE ${where}
2303
- GROUP BY agent
2304
- `).all();
2305
- const totalAgentApiEq = rows.reduce((sum, row) => sum + row.api_eq, 0);
2306
- for (const row of db.prepare(`
2307
- SELECT agent,
2308
- COALESCE(SUM(cost_usd), 0) as api_eq,
2309
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as included
2310
- FROM requests WHERE ${where}
2311
- GROUP BY agent
2312
- `).all()) {
2313
- const agentSubs = subscriptionByAgent.get(row.agent) ?? { fee: 0, included: 0 };
2314
- const globalShare = totalAgentApiEq > 0 ? row.api_eq / totalAgentApiEq : 0;
2315
- const agentFee = prorateMonthlyFee(agentSubs.fee + globalSubs.fee * globalShare, period);
2316
- const agentIncludedCap = agentSubs.included + globalSubs.included * globalShare;
2317
- const agentIncludedConsumed = proratedIncludedConsumed(row.included, agentIncludedCap, period);
2318
- const agentOnDemand = onDemandByAgent.get(row.agent) ?? 0;
2319
- byAgent[row.agent] = {
2320
- api_equivalent_usd: row.api_eq,
2321
- subscription_fee_usd: agentFee,
2322
- included_consumed_usd: agentIncludedConsumed,
2323
- on_demand_usd: agentOnDemand,
2324
- saved_usd: computeSavedUsd(row.api_eq, agentOnDemand, agentFee)
2325
- };
244
+ if (until <= start || from >= end)
245
+ continue;
246
+ if (from < start || until > end)
247
+ return { usd: null, status: "unavailable" };
248
+ const key = JSON.stringify([row.provider, row.account_key, from, until]);
249
+ const previous = windows.get(key);
250
+ if (previous && previous.row.value_kind !== row.value_kind)
251
+ return { usd: null, status: "unavailable" };
252
+ if (previous && ((row.value_kind === "delta" || at === previous.at) && previous.row.value !== row.value)) {
253
+ return { usd: null, status: "unavailable" };
254
+ }
255
+ if (!previous || at > previous.at)
256
+ windows.set(key, { row, at, start: from, end: until });
257
+ }
258
+ const byAccount = new Map;
259
+ let total = 0;
260
+ for (const window of windows.values()) {
261
+ const key = JSON.stringify([window.row.provider, window.row.account_key]);
262
+ const intervals = byAccount.get(key) ?? [];
263
+ if (intervals.some((other) => window.start < other.end && window.end > other.start)) {
264
+ return { usd: null, status: "unavailable" };
265
+ }
266
+ intervals.push(window);
267
+ byAccount.set(key, intervals);
268
+ total += window.row.value;
269
+ }
270
+ return windows.size ? { usd: total, status: "observed" } : { usd: null, status: "not_observed" };
271
+ }
272
+ function buildSavingsSummary(period, usage, plans, observations, agent, now = new Date, prices = []) {
273
+ const selected = usage.filter((row) => !agent || row.agent === agent);
274
+ const activePlans = plans.filter((row) => row.active === 1);
275
+ const selectedPlans = activePlans.filter((row) => !agent || row.agent === agent);
276
+ const sharedPlans = activePlans.filter((row) => !row.agent || !row.account_key);
277
+ const fee = feeFor(selectedPlans, period, now);
278
+ const unallocated = feeFor(sharedPlans, period, now);
279
+ const reasons = new Set;
280
+ let priced = 0, metered = 0, estimated = 0, included = 0, unknownPrices = 0, unknownBilling = 0, estimatedSessions = 0;
281
+ for (const row of selected) {
282
+ if (row.session_count) {
283
+ estimatedSessions += row.session_count;
284
+ reasons.add("billing_route_unknown");
285
+ }
286
+ if (!hasKnownUsagePrice(row, prices)) {
287
+ unknownPrices += row.requests;
288
+ reasons.add("model_price_unknown");
289
+ continue;
290
+ }
291
+ priced += row.cost_usd;
292
+ if (row.cost_basis === "metered_api")
293
+ metered += row.cost_usd;
294
+ else if (row.cost_basis === "subscription_included") {
295
+ included += row.cost_usd;
296
+ const matches = selectedPlans.filter((plan) => !!row.account_key && plan.account_key === row.account_key);
297
+ if (matches.length === 0)
298
+ reasons.add("subscription_account_unmapped");
299
+ if (matches.length > 1)
300
+ reasons.add("subscription_account_ambiguous");
301
+ } else {
302
+ estimated += row.cost_usd;
303
+ unknownBilling += row.requests;
304
+ reasons.add("billing_route_unknown");
305
+ }
306
+ }
307
+ if (unknownPrices)
308
+ reasons.add("model_price_unknown");
309
+ if (unknownBilling)
310
+ reasons.add("billing_route_unknown");
311
+ if (fee.total === null)
312
+ reasons.add(period === "all" ? "historical_plan_fees_unavailable" : "plan_fee_unknown");
313
+ if (included > 0 && !agent && sharedPlans.some((plan) => !plan.account_key))
314
+ reasons.add("subscription_account_unmapped");
315
+ const demand = summarizeOnDemand(observations.filter((row) => !agent || row.agent === agent), period, now);
316
+ const applicable = selectedPlans.length > 0 || selected.some((row) => row.cost_basis === "subscription_included");
317
+ const net = reasons.size ? null : applicable ? included - fee.known : 0;
318
+ const byAgent = {};
319
+ if (!agent) {
320
+ const agents = new Set([...usage.map((row) => row.agent), ...activePlans.flatMap((row) => row.agent ? [row.agent] : [])]);
321
+ for (const name of [...agents].sort()) {
322
+ const result = buildSavingsSummary(period, usage, plans, observations, name, now, prices);
323
+ const { by_agent: _, ...scalars } = result;
324
+ byAgent[name] = scalars;
2326
325
  }
2327
326
  }
2328
327
  return {
2329
328
  period,
2330
- api_equivalent_usd: apiEquivalent,
2331
- subscription_fee_usd: subscriptionFee,
2332
- included_consumed_usd: includedConsumed,
2333
- on_demand_usd: onDemand,
2334
- saved_usd: saved,
329
+ api_equivalent_usd: reasons.has("model_price_unknown") ? null : priced,
330
+ priced_usage_value_usd: priced,
331
+ recorded_metered_api_usd: metered,
332
+ estimated_usage_value_usd: estimated,
333
+ subscription_fee_usd: fee.total,
334
+ known_subscription_fee_usd: fee.known,
335
+ unallocated_subscription_fee_usd: unallocated.total,
336
+ included_consumed_usd: included,
337
+ on_demand_usd: demand.usd,
338
+ on_demand_status: demand.status,
339
+ saved_usd: net === null ? null : Math.max(0, net),
340
+ net_comparison_usd: net,
341
+ comparison_status: reasons.size ? "unavailable" : applicable ? "estimated" : "not_applicable",
342
+ comparison_basis: "included_usage_vs_current_plan_estimate",
343
+ unavailable_reasons: [...reasons].sort(),
344
+ unknown_price_requests: unknownPrices,
345
+ unknown_billing_requests: unknownBilling,
346
+ estimated_only_sessions: estimatedSessions,
2335
347
  by_agent: byAgent
2336
348
  };
2337
349
  }
350
+ function querySavingsSummary(db, period, agent) {
351
+ const now = new Date;
352
+ const usage = db.prepare(`
353
+ SELECT agent, account_key, cost_basis, model,
354
+ CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END AS zero_cost,
355
+ COALESCE(SUM(cost_usd), 0) AS cost_usd, COUNT(*) AS requests,
356
+ COALESCE(SUM(input_tokens),0) AS input_tokens, COALESCE(SUM(output_tokens),0) AS output_tokens,
357
+ COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens, COALESCE(SUM(cache_create_tokens),0) AS cache_create_tokens,
358
+ COALESCE(SUM(cache_create_1h_tokens),0) AS cache_create_1h_tokens
359
+ FROM economy_effective_requests WHERE ${calendarPeriodWhere(period, "timestamp", now)}
360
+ GROUP BY agent, account_key, cost_basis, model, CASE WHEN cost_usd = 0 THEN 1 WHEN cost_usd < 0 THEN 2 ELSE 0 END
361
+ `).all();
362
+ const plans = db.prepare("SELECT * FROM subscriptions WHERE active = 1").all();
363
+ usage.push(...db.prepare(`
364
+ SELECT s.agent, s.account_key, 'estimated' AS cost_basis,
365
+ COALESCE(SUM(s.total_cost_usd), 0) AS cost_usd,
366
+ COALESCE(SUM(s.request_count), 0) AS requests, COUNT(*) AS session_count
367
+ FROM economy_effective_sessions s WHERE ${sessionOverlapWhere(period, "s", now)}
368
+ AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = s.id)
369
+ GROUP BY s.agent, s.account_key
370
+ `).all());
371
+ const bounds = calendarPeriodBounds(period, now);
372
+ const windowWhere = bounds.start_date === null ? "1=1" : `(
373
+ (window_start IS NOT NULL AND window_end IS NOT NULL
374
+ AND window_start < '${bounds.end_date_exclusive}T00:00:00.000Z'
375
+ AND window_end > '${bounds.start_date}T00:00:00.000Z')
376
+ OR ((window_start IS NULL OR window_end IS NULL) AND ${calendarPeriodWhere(period, "date", now)})
377
+ )`;
378
+ const observations = db.prepare(`
379
+ SELECT * FROM usage_snapshots WHERE metric = 'on_demand_usd'
380
+ AND ${windowWhere}
381
+ `).all();
382
+ return buildSavingsSummary(period, usage, plans, observations, agent, now, listModelPricing(db));
383
+ }
2338
384
 
2339
385
  // src/lib/billing-diff.ts
2340
- init_database();
2341
- var PROVIDER_TO_AGENT = {
2342
- anthropic: "claude",
2343
- openai: "codex",
2344
- gemini: "gemini",
2345
- google: "gemini"
2346
- };
2347
- function billingDeltaPct(estimatedUsd, actualUsd) {
2348
- if (!(actualUsd > 0))
2349
- return null;
2350
- return (estimatedUsd - actualUsd) / actualUsd * 100;
2351
- }
2352
386
  function queryBillingDiff(db, period, thresholdPct = 15) {
2353
- const estimated = querySummary(db, period, undefined, true);
2354
- const actual = queryBillingSummary(db, period);
2355
- const billingRecords = countBillingRecords(db, period);
2356
- const delta = estimated.total_usd - actual.total_usd;
2357
- const measuredPct = billingDeltaPct(estimated.total_usd, actual.total_usd);
2358
- const incomparableReason = measuredPct !== null ? null : billingRecords === 0 ? "no_billing_records" : "zero_actual_billing";
2359
- const comparable = incomparableReason === null;
2360
- const deltaPct = measuredPct ?? 0;
2361
- const agentRows = db.prepare(`
2362
- SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
2363
- FROM requests
2364
- WHERE ${periodWhere3(period, "timestamp")}
2365
- GROUP BY agent
387
+ if (!Number.isFinite(thresholdPct) || thresholdPct < 0)
388
+ throw new RangeError("thresholdPct must be a finite nonnegative number");
389
+ const usage = querySummary(db, period, undefined, true);
390
+ const billing = queryBillingSummary(db, period);
391
+ const rows = db.prepare(`
392
+ SELECT agent, COALESCE(SUM(cost_usd), 0) AS estimated_usd
393
+ FROM economy_effective_requests WHERE ${calendarPeriodWhere(period, "timestamp")}
394
+ GROUP BY agent ORDER BY agent
2366
395
  `).all();
2367
- const by_agent = agentRows.map((row) => {
2368
- const provider = Object.entries(PROVIDER_TO_AGENT).find(([, a]) => a === row.agent)?.[0];
2369
- const actualUsd = provider ? actual.by_provider[provider] ?? 0 : 0;
2370
- const rowDelta = row.estimated_usd - actualUsd;
2371
- const rowPct = actualUsd > 0 ? rowDelta / actualUsd * 100 : 0;
2372
- return {
2373
- agent: row.agent,
2374
- estimated_usd: row.estimated_usd,
2375
- actual_usd: actualUsd,
2376
- delta_usd: rowDelta,
2377
- delta_pct: rowPct
2378
- };
2379
- }).sort((a, b) => Math.abs(b.delta_usd) - Math.abs(a.delta_usd));
2380
- return {
396
+ return requireCurrentBillingDiffSummary({
2381
397
  period,
2382
- estimated_usd: estimated.total_usd,
2383
- actual_usd: actual.total_usd,
2384
- delta_usd: delta,
2385
- delta_pct: deltaPct,
398
+ basis: "unmatched_subtotals",
399
+ estimated_usd: usage.total_usd,
400
+ actual_usd: billing.total_usd,
401
+ usage_summary: usage,
402
+ billing_summary: billing,
403
+ delta_usd: null,
404
+ delta_pct: null,
2386
405
  threshold_pct: thresholdPct,
2387
- is_alert: comparable && Math.abs(deltaPct) > thresholdPct,
2388
- comparable,
2389
- incomparable_reason: incomparableReason,
2390
- by_agent,
2391
- by_provider: actual.by_provider
2392
- };
2393
- }
2394
- function periodWhere3(period, column) {
2395
- switch (period) {
2396
- case "today":
2397
- return `DATE(${column}) = DATE('now')`;
2398
- case "yesterday":
2399
- return `DATE(${column}) = DATE('now', '-1 day')`;
2400
- case "week":
2401
- return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
2402
- case "month":
2403
- return `${column} >= DATE('now', 'start of month')`;
2404
- case "year":
2405
- return `${column} >= DATE('now', 'start of year')`;
2406
- case "all":
2407
- return "1=1";
2408
- }
2409
- }
2410
-
2411
- // src/lib/periods.ts
2412
- function ymd(date) {
2413
- return date.toISOString().substring(0, 10);
406
+ is_alert: null,
407
+ comparable: false,
408
+ incomparable_reason: billing.record_count === 0 ? "no_billing_records" : "account_window_source_unverified",
409
+ by_agent: rows.map((row) => ({ ...row, basis: "stored_request_rows_subtotal", actual_usd: null, delta_usd: null, delta_pct: null })),
410
+ by_provider: billing.by_provider
411
+ });
2414
412
  }
2415
- function usageSnapshotFilterForPeriod(period) {
2416
- const now = new Date;
2417
- switch (period) {
2418
- case "today":
2419
- return { date: ymd(now) };
2420
- case "yesterday": {
2421
- const yesterday = new Date(now);
2422
- yesterday.setUTCDate(yesterday.getUTCDate() - 1);
2423
- return { date: ymd(yesterday) };
2424
- }
2425
- case "week": {
2426
- const weekAgo = new Date(now);
2427
- weekAgo.setUTCDate(weekAgo.getUTCDate() - 7);
2428
- return { since: ymd(weekAgo) };
2429
- }
2430
- case "month":
2431
- return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1))) };
2432
- case "year":
2433
- return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), 0, 1))) };
2434
- case "all":
2435
- return {};
2436
- }
413
+ function requireCurrentBillingDiffSummary(value) {
414
+ const invalid = () => {
415
+ throw new Error("BILLING_COMPARISON_UNAVAILABLE: The service must return scoped unmatched subtotals");
416
+ };
417
+ if (!value || typeof value !== "object" || Array.isArray(value))
418
+ return invalid();
419
+ const s = value;
420
+ if (s.basis !== "unmatched_subtotals" || s.comparable !== false || s.delta_usd !== null || s.delta_pct !== null || s.is_alert !== null)
421
+ return invalid();
422
+ if (!["today", "yesterday", "week", "month", "year", "all"].includes(String(s.period)) || typeof s.threshold_pct !== "number" || !Number.isFinite(s.threshold_pct) || s.threshold_pct < 0)
423
+ return invalid();
424
+ let usage, billing;
425
+ try {
426
+ usage = requireCurrentCostSummary(s.usage_summary);
427
+ billing = requireCurrentBillingSummary(s.billing_summary);
428
+ } catch {
429
+ return invalid();
430
+ }
431
+ if (usage.period !== s.period || s.estimated_usd !== usage.total_usd || s.actual_usd !== billing.total_usd || billing.recorded_total_usd !== (billing.record_count ? billing.total_usd : null))
432
+ return invalid();
433
+ if (s.incomparable_reason !== (billing.record_count === 0 ? "no_billing_records" : "account_window_source_unverified"))
434
+ return invalid();
435
+ const providers = s.by_provider;
436
+ if (!providers || typeof providers !== "object" || Array.isArray(providers) || !billing.by_provider || typeof billing.by_provider !== "object")
437
+ return invalid();
438
+ const expected = Object.entries(billing.by_provider);
439
+ if (Object.keys(providers).length !== expected.length || expected.some(([key, amount]) => typeof amount !== "number" || !Number.isFinite(amount) || providers[key] !== amount))
440
+ return invalid();
441
+ if (!Array.isArray(s.by_agent) || s.by_agent.some((row) => !row || typeof row !== "object" || typeof row.agent !== "string" || row.basis !== "stored_request_rows_subtotal" || typeof row.estimated_usd !== "number" || !Number.isFinite(row.estimated_usd) || row.actual_usd !== null || row.delta_usd !== null || row.delta_pct !== null))
442
+ return invalid();
443
+ return value;
2437
444
  }
2438
445
 
2439
446
  // src/lib/brief.ts
2440
- init_database();
2441
447
  var ZERO_REQUEST_TOTALS = {
2442
448
  sessions: 0,
2443
449
  request_sessions: 0,
@@ -2481,15 +487,11 @@ function machineFilter(machine) {
2481
487
  return;
2482
488
  return machine;
2483
489
  }
2484
- function requestPeriodWhere2(period) {
2485
- if (period === "today")
2486
- return `DATE(timestamp) = DATE('now')`;
2487
- return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
490
+ function requestPeriodWhere(period) {
491
+ return calendarPeriodWhere(period, "timestamp");
2488
492
  }
2489
- function sessionPeriodWhere2(period) {
2490
- if (period === "today")
2491
- return `DATE(started_at) = DATE('now')`;
2492
- return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
493
+ function sessionPeriodWhere(period) {
494
+ return calendarPeriodWhere(period, "started_at");
2493
495
  }
2494
496
  function baseRequestTotals(row) {
2495
497
  return {
@@ -2560,7 +562,7 @@ function requestTotalsSql(where, machine) {
2560
562
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
2561
563
  COALESCE(SUM(cost_usd), 0) as cost_usd,
2562
564
  MAX(timestamp) as last_active
2563
- FROM requests
565
+ FROM economy_effective_requests
2564
566
  WHERE ${where}${machineClause}
2565
567
  `,
2566
568
  params
@@ -2581,9 +583,9 @@ function sessionOnlyTotalsSql(where, machine) {
2581
583
  COALESCE(SUM(total_tokens), 0) as total_tokens,
2582
584
  COALESCE(SUM(total_cost_usd), 0) as cost_usd,
2583
585
  MAX(started_at) as last_active
2584
- FROM sessions
586
+ FROM economy_effective_sessions
2585
587
  WHERE ${where}${machineClause}
2586
- AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
588
+ AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
2587
589
  `,
2588
590
  params
2589
591
  };
@@ -2598,15 +600,16 @@ function querySessionOnlyTotals(db, where, params, machine) {
2598
600
  }
2599
601
  function queryPeriodTotals(db, period, machine) {
2600
602
  const summary = querySummary(db, period, machine);
2601
- const requests = queryRequestTotals(db, requestPeriodWhere2(period), [], machine);
2602
- const sessions = querySessionOnlyTotals(db, sessionPeriodWhere2(period), [], machine);
603
+ const requests = queryRequestTotals(db, requestPeriodWhere(period), [], machine);
604
+ const sessions = querySessionOnlyTotals(db, sessionPeriodWhere(period), [], machine);
2603
605
  const totals = combineTotals(requests, sessions);
2604
606
  return {
2605
607
  ...totals,
2606
608
  sessions: summary.sessions,
2607
609
  requests: summary.requests,
2608
610
  total_tokens: summary.tokens,
2609
- cost_usd: summary.total_usd
611
+ cost_usd: summary.total_usd,
612
+ cost_summary: summary
2610
613
  };
2611
614
  }
2612
615
  function querySinceTotals(db, since, machine) {
@@ -2659,7 +662,7 @@ function queryMachineSinceRows(db, since, machine) {
2659
662
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
2660
663
  COALESCE(SUM(cost_usd), 0) as cost_usd,
2661
664
  MAX(timestamp) as last_active
2662
- FROM requests
665
+ FROM economy_effective_requests
2663
666
  WHERE timestamp >= ?
2664
667
  AND machine_id != ''
2665
668
  ${requestMachineClause}
@@ -2674,11 +677,11 @@ function queryMachineSinceRows(db, since, machine) {
2674
677
  COALESCE(SUM(total_tokens), 0) as total_tokens,
2675
678
  COALESCE(SUM(total_cost_usd), 0) as cost_usd,
2676
679
  MAX(started_at) as last_active
2677
- FROM sessions
680
+ FROM economy_effective_sessions
2678
681
  WHERE started_at >= ?
2679
682
  AND machine_id != ''
2680
683
  ${sessionMachineClause}
2681
- AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
684
+ AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
2682
685
  GROUP BY machine_id
2683
686
  `).all(...machine ? [since, machine] : [since]);
2684
687
  const groups = new Map;
@@ -2716,13 +719,13 @@ function queryMachineLastData(db, machine) {
2716
719
  const filter = machine ? " AND machine_id = ?" : "";
2717
720
  const requestRows = db.prepare(`
2718
721
  SELECT machine_id, MAX(timestamp) as last_at
2719
- FROM requests
722
+ FROM economy_effective_requests
2720
723
  WHERE machine_id != ''${filter}
2721
724
  GROUP BY machine_id
2722
725
  `).all(...machine ? [machine] : []);
2723
726
  const sessionRows = db.prepare(`
2724
727
  SELECT machine_id, MAX(started_at) as last_at
2725
- FROM sessions
728
+ FROM economy_effective_sessions
2726
729
  WHERE machine_id != ''${filter}
2727
730
  GROUP BY machine_id
2728
731
  `).all(...machine ? [machine] : []);
@@ -2750,7 +753,7 @@ function queryAgentSinceRows(db, since, machine) {
2750
753
  input_tokens + output_tokens + cache_read_tokens + cache_create_tokens as total_tokens,
2751
754
  cost_usd,
2752
755
  timestamp as last_active
2753
- FROM requests
756
+ FROM economy_effective_requests
2754
757
  WHERE timestamp >= ?${requestMachineClause}
2755
758
  ),
2756
759
  session_only_rows AS (
@@ -2767,9 +770,9 @@ function queryAgentSinceRows(db, since, machine) {
2767
770
  COALESCE(total_tokens, 0) as total_tokens,
2768
771
  COALESCE(total_cost_usd, 0) as cost_usd,
2769
772
  started_at as last_active
2770
- FROM sessions
773
+ FROM economy_effective_sessions
2771
774
  WHERE started_at >= ?${sessionMachineClause}
2772
- AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
775
+ AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = economy_effective_sessions.id)
2773
776
  ),
2774
777
  combined AS (
2775
778
  SELECT * FROM request_rows
@@ -2813,12 +816,12 @@ function agentBreakdownToBriefRow(row) {
2813
816
  last_active: row.last_active
2814
817
  };
2815
818
  }
2816
- function normalizeAccountEmail2(email) {
819
+ function normalizeAccountEmail(email) {
2817
820
  return email.trim().toLowerCase();
2818
821
  }
2819
822
  function accountKey(row) {
2820
823
  const agent = row.agent || row.account_tool || "unknown";
2821
- const email = normalizeAccountEmail2(row.account_email);
824
+ const email = normalizeAccountEmail(row.account_email);
2822
825
  if (email)
2823
826
  return `${agent}:${email}`;
2824
827
  if (row.account_name)
@@ -2829,7 +832,7 @@ function addAccountRow(groups, row) {
2829
832
  if (!row.account_key && !row.account_tool && !row.account_name && !row.account_email)
2830
833
  return;
2831
834
  const key = accountKey(row);
2832
- const email = normalizeAccountEmail2(row.account_email);
835
+ const email = normalizeAccountEmail(row.account_email);
2833
836
  const current = groups.get(key) ?? {
2834
837
  account_key: key,
2835
838
  account_tool: row.agent || row.account_tool || "unknown",
@@ -2891,8 +894,8 @@ function queryAccountSinceRows(db, since, machine) {
2891
894
  COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
2892
895
  COALESCE(SUM(r.cost_usd), 0) as cost_usd,
2893
896
  MAX(r.timestamp) as last_active
2894
- FROM requests r
2895
- LEFT JOIN sessions s ON s.id = r.session_id
897
+ FROM economy_effective_requests r
898
+ LEFT JOIN economy_effective_sessions s ON s.id = r.session_id
2896
899
  WHERE r.timestamp >= ?${requestMachineClause}
2897
900
  GROUP BY
2898
901
  COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), ''),
@@ -2921,9 +924,9 @@ function queryAccountSinceRows(db, since, machine) {
2921
924
  COALESCE(SUM(s.total_tokens), 0) as total_tokens,
2922
925
  COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
2923
926
  MAX(s.started_at) as last_active
2924
- FROM sessions s
927
+ FROM economy_effective_sessions s
2925
928
  WHERE s.started_at >= ?${sessionMachineClause}
2926
- AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = s.id)
929
+ AND NOT EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id = s.id)
2927
930
  GROUP BY
2928
931
  s.agent,
2929
932
  s.account_key,
@@ -2967,7 +970,7 @@ function queryFreshness(db, now, machine, localSync) {
2967
970
  const requestFilter = machine ? " AND machine_id = ?" : "";
2968
971
  const requestRows = db.prepare(`
2969
972
  SELECT machine_id, MAX(timestamp) as max_request_at
2970
- FROM requests
973
+ FROM economy_effective_requests
2971
974
  WHERE machine_id != ''${requestFilter}
2972
975
  GROUP BY machine_id
2973
976
  `).all(...machine ? [machine] : []);
@@ -3061,7 +1064,7 @@ function buildBrief(db, opts = {}) {
3061
1064
  function queryProjectDetail(db, nameOrPath) {
3062
1065
  const like = `%${nameOrPath}%`;
3063
1066
  const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
3064
- FROM sessions
1067
+ FROM economy_effective_sessions
3065
1068
  WHERE project_name LIKE ? OR project_path LIKE ?
3066
1069
  ORDER BY started_at DESC`).all(like, like);
3067
1070
  if (sessions.length === 0)
@@ -3070,12 +1073,12 @@ function queryProjectDetail(db, nameOrPath) {
3070
1073
  const totalCost = sessions.reduce((s, r) => s + (r.total_cost_usd ?? 0), 0);
3071
1074
  const totalTokens = sessions.reduce((s, r) => s + (r.total_tokens ?? 0), 0);
3072
1075
  const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
3073
- FROM requests r JOIN sessions s ON r.session_id = s.id
1076
+ FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
3074
1077
  WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
3075
1078
  AND r.timestamp >= DATE('now', '-14 days')
3076
1079
  GROUP BY date ORDER BY date ASC`).all(like, like);
3077
1080
  const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
3078
- FROM requests r JOIN sessions s ON r.session_id = s.id
1081
+ FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
3079
1082
  WHERE s.project_name LIKE ? OR s.project_path LIKE ?
3080
1083
  GROUP BY r.model ORDER BY cost_usd DESC LIMIT 5`).all(like, like);
3081
1084
  const top_sessions = [...sessions].sort((a, b) => (b.total_cost_usd ?? 0) - (a.total_cost_usd ?? 0)).slice(0, 5).map((s) => ({ id: s.id, total_cost_usd: s.total_cost_usd ?? 0, started_at: s.started_at }));
@@ -3091,45 +1094,39 @@ function queryProjectDetail(db, nameOrPath) {
3091
1094
  };
3092
1095
  }
3093
1096
  function exportDateWhere(column, period) {
3094
- switch (period) {
3095
- case "today":
3096
- return `DATE(${column}) = DATE('now')`;
3097
- case "week":
3098
- return `${column} >= DATE('now', '-7 days')`;
3099
- case "all":
3100
- return "1=1";
3101
- default:
3102
- return `${column} >= DATE('now', '-30 days')`;
1097
+ if (!["today", "yesterday", "week", "month", "year", "all"].includes(period)) {
1098
+ throw new RangeError("Invalid export period");
3103
1099
  }
1100
+ return calendarPeriodWhere(period, column);
3104
1101
  }
3105
1102
  function queryExportRows(db, type, period) {
3106
1103
  if (type === "requests") {
3107
1104
  const where2 = exportDateWhere("timestamp", period);
3108
- return db.prepare(`SELECT * FROM requests WHERE ${where2} ORDER BY timestamp ASC`).all();
1105
+ return db.prepare(`SELECT * FROM economy_effective_requests WHERE ${where2} ORDER BY timestamp ASC`).all();
3109
1106
  }
3110
1107
  const where = exportDateWhere("started_at", period);
3111
- return db.prepare(`SELECT * FROM sessions WHERE ${where} ORDER BY started_at DESC`).all();
1108
+ return db.prepare(`SELECT * FROM economy_effective_sessions WHERE ${where} ORDER BY started_at DESC`).all();
3112
1109
  }
3113
1110
  function queryRangeStats(db, from, to) {
3114
1111
  const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests,
3115
1112
  COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens
3116
- FROM requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
3117
- const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
1113
+ FROM economy_effective_requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
1114
+ const s = db.prepare(`SELECT COUNT(*) as sessions FROM economy_effective_sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
3118
1115
  return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
3119
1116
  }
3120
1117
  function queryForecast(db, now = new Date) {
3121
1118
  const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
3122
1119
  const dayOfMonth = now.getDate();
3123
1120
  const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
3124
- const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(monthStart);
1121
+ const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(monthStart);
3125
1122
  const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
3126
1123
  const projected = dailyAvg * daysInMonth;
3127
1124
  const sevenDaysAgo = new Date(now);
3128
1125
  sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
3129
- const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
1126
+ const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
3130
1127
  const last7DailyAvg = last7.cost / 7;
3131
1128
  const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
3132
- FROM requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
1129
+ FROM economy_effective_requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
3133
1130
  return {
3134
1131
  day_of_month: dayOfMonth,
3135
1132
  days_in_month: daysInMonth,
@@ -3150,7 +1147,7 @@ function queryModelEfficiency(db) {
3150
1147
  COALESCE(SUM(cache_create_tokens),0) as cache_write,
3151
1148
  COUNT(*) as requests,
3152
1149
  COALESCE(SUM(cost_usd),0) as cost
3153
- FROM requests GROUP BY model ORDER BY cost DESC`).all();
1150
+ FROM economy_effective_requests GROUP BY model ORDER BY cost DESC`).all();
3154
1151
  }
3155
1152
 
3156
1153
  // src/lib/cloud-storage.ts
@@ -3167,18 +1164,6 @@ import {
3167
1164
  } from "@hasna/contracts/client";
3168
1165
  import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
3169
1166
  var ECONOMY_APP = "economy";
3170
- var RETIRED_STORAGE_MODE_KEYS = [
3171
- "HASNA_ECONOMY_STORAGE_MODE",
3172
- "HASNA_ECONOMY_MODE",
3173
- "ECONOMY_STORAGE_MODE",
3174
- "ECONOMY_MODE"
3175
- ];
3176
- function assertNoRetiredStorageMode(env) {
3177
- const legacyKey = RETIRED_STORAGE_MODE_KEYS.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
3178
- if (!legacyKey)
3179
- return;
3180
- throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client routes through the HTTP API resolved by @hasna/contracts ` + `(HASNA_ECONOMY_API_URL + HASNA_ECONOMY_API_KEY, the Keychain, or ` + `~/.hasna/economy/config/credentials), or serves the local SQLite store only when ` + `HASNA_ECONOMY_LOCAL=1 is set.`);
3181
- }
3182
1167
  var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
3183
1168
  function envFlagSet(env, key) {
3184
1169
  const raw = env[key];
@@ -3253,7 +1238,7 @@ function resolveEconomyStorageClient(name, env, overrides = {}) {
3253
1238
  };
3254
1239
  }
3255
1240
  function localEconomyNotice() {
3256
- return `economy: local mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 reading and writing the on-box ` + `SQLite store, not the hosted fleet. Data in the shared economy API is NOT included.`;
1241
+ return `economy: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 reading and writing the on-box ` + `SQLite store, not the hosted fleet. Data in the shared economy API is NOT included.`;
3257
1242
  }
3258
1243
  function credentialRequiredError(env, cause) {
3259
1244
  const keys = clientTransportEnvKeys(ECONOMY_APP);
@@ -3265,7 +1250,6 @@ function credentialRequiredError(env, cause) {
3265
1250
  var cache;
3266
1251
  var localNoticePrinted = false;
3267
1252
  function resolveEconomyCloudStorage(env = process.env, overrides) {
3268
- assertNoRetiredStorageMode(env);
3269
1253
  const clientEnv = env;
3270
1254
  if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
3271
1255
  return { active: false, client: null };
@@ -3318,94 +1302,116 @@ async function cloudObject(storage, path, query) {
3318
1302
  }
3319
1303
 
3320
1304
  // src/lib/store/index.ts
1305
+ function configuredSubscription(input) {
1306
+ if (typeof input.account_key !== "string" || !input.account_key.trim()) {
1307
+ throw new TypeError("account_key must identify the provider account; accepted: nonempty account key");
1308
+ }
1309
+ const now = new Date().toISOString();
1310
+ const row = {
1311
+ ...input,
1312
+ id: input.id ?? subscriptionId(input),
1313
+ monthly_fee_usd: input.monthly_fee_usd === undefined ? 0 : input.monthly_fee_usd,
1314
+ included_usage_usd: input.included_usage_usd === undefined ? 0 : input.included_usage_usd,
1315
+ fee_basis: input.monthly_fee_usd === undefined ? "unknown" : "configured",
1316
+ included_basis: input.included_usage_usd === undefined ? "unknown" : "configured",
1317
+ observed_station: input.observed_station ?? null,
1318
+ observed_at: now,
1319
+ created_at: now,
1320
+ updated_at: now
1321
+ };
1322
+ validateIngest({ subscriptions: [row] });
1323
+ return row;
1324
+ }
1325
+
3321
1326
  class LocalStore {
3322
1327
  transport = "local";
3323
1328
  _db;
3324
- db() {
1329
+ async db() {
3325
1330
  if (!this._db) {
1331
+ const { openDatabase } = await import("./chunks/sqlite-store-fq4natnf.js");
3326
1332
  this._db = openDatabase();
3327
1333
  ensurePricingSeeded(this._db);
3328
1334
  }
3329
1335
  return this._db;
3330
1336
  }
3331
1337
  async summary(period, machine) {
3332
- return querySummary(this.db(), period, machine);
1338
+ return querySummary(await this.db(), period, machine);
3333
1339
  }
3334
1340
  async sessions(filter) {
3335
- return querySessions(this.db(), filter);
1341
+ return querySessions(await this.db(), filter);
3336
1342
  }
3337
1343
  async topSessions(n, agent, since) {
3338
- return queryTopSessions(this.db(), n, agent, since);
1344
+ return queryTopSessions(await this.db(), n, agent, since);
3339
1345
  }
3340
1346
  async sessionDetail(id) {
3341
- return getSessionDetail(this.db(), id);
1347
+ return getSessionDetail(await this.db(), id);
3342
1348
  }
3343
1349
  async modelBreakdown(query = {}) {
3344
- return query.since ? queryModelBreakdownSince(this.db(), query.since) : queryModelBreakdown(this.db());
1350
+ return query.since ? queryModelBreakdownSince(await this.db(), query.since) : queryModelBreakdown(await this.db());
3345
1351
  }
3346
1352
  async agentBreakdown(query = {}) {
3347
- return query.since ? queryAgentBreakdownSince(this.db(), query.since) : queryAgentBreakdown(this.db(), query.period ?? "all", query.machine);
1353
+ return query.since ? queryAgentBreakdownSince(await this.db(), query.since) : queryAgentBreakdown(await this.db(), query.period ?? "all", query.machine);
3348
1354
  }
3349
1355
  async projectBreakdown(query = {}) {
3350
- return query.since ? queryProjectBreakdownSince(this.db(), query.since, query.machine) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
1356
+ return query.since ? queryProjectBreakdownSince(await this.db(), query.since, query.machine) : queryProjectBreakdown(await this.db(), query.period ?? "all", query.machine);
3351
1357
  }
3352
1358
  async accountBreakdown(query = {}) {
3353
- return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
1359
+ return query.since ? queryAccountBreakdownSince(await this.db(), query.since) : queryAccountBreakdown(await this.db(), query.period ?? "all", query.machine);
3354
1360
  }
3355
1361
  async costCenterBreakdown(query = {}) {
3356
- return queryCostCenterBreakdown(this.db(), query.period ?? "all", {
1362
+ return queryCostCenterBreakdown(await this.db(), query.period ?? "all", {
3357
1363
  kind: query.kind,
3358
1364
  machine: query.machine,
3359
1365
  since: query.since
3360
1366
  });
3361
1367
  }
3362
1368
  async accounts(period) {
3363
- return queryAccountBreakdown(this.db(), period);
1369
+ return queryAccountBreakdown(await this.db(), period);
3364
1370
  }
3365
1371
  async daily(days, machine) {
3366
- return queryDailyBreakdown(this.db(), days, machine);
1372
+ return queryDailyBreakdown(await this.db(), days, machine);
3367
1373
  }
3368
1374
  async machines() {
3369
- return listMachines(this.db());
1375
+ return listMachines(await this.db());
3370
1376
  }
3371
1377
  async fleet(period) {
3372
1378
  return {
3373
- summary: querySummary(this.db(), period, undefined, true),
3374
- machines: listMachines(this.db(), period),
3375
- registry: listMachineRegistry(this.db())
1379
+ summary: querySummary(await this.db(), period, undefined, true),
1380
+ machines: listMachines(await this.db(), period),
1381
+ registry: listMachineRegistry(await this.db())
3376
1382
  };
3377
1383
  }
3378
1384
  async billingSummary(period) {
3379
- return queryBillingSummary(this.db(), period);
1385
+ return queryBillingSummary(await this.db(), period);
3380
1386
  }
3381
1387
  async billingDiff(period) {
3382
- return queryBillingDiff(this.db(), period);
1388
+ return queryBillingDiff(await this.db(), period);
3383
1389
  }
3384
1390
  async usage(period, agent) {
3385
- const snapshots = queryUsageSnapshots(this.db(), { agent, ...usageSnapshotFilterForPeriod(period) });
3386
- const summary = querySummary(this.db(), period, undefined, true, agent);
1391
+ const snapshots = queryUsageSnapshots(await this.db(), { agent, ...usageSnapshotFilterForPeriod(period) });
1392
+ const summary = querySummary(await this.db(), period, undefined, true, agent);
3387
1393
  return { snapshots, summary };
3388
1394
  }
3389
1395
  async savings(period, agent) {
3390
- return querySavingsSummary(this.db(), period, agent);
1396
+ return querySavingsSummary(await this.db(), period, agent);
3391
1397
  }
3392
1398
  async listBudgets() {
3393
- return getBudgetStatuses(this.db());
1399
+ return getBudgetStatuses(await this.db());
3394
1400
  }
3395
1401
  async listGoals() {
3396
- return getGoalStatuses(this.db());
1402
+ return getGoalStatuses(await this.db());
3397
1403
  }
3398
1404
  async listPricing() {
3399
- return listModelPricing(this.db());
1405
+ return listModelPricing(await this.db());
3400
1406
  }
3401
1407
  async listSubscriptions() {
3402
- return listSubscriptions(this.db());
1408
+ return listSubscriptions(await this.db());
3403
1409
  }
3404
1410
  async listProjects() {
3405
- return queryProjectBreakdown(this.db());
1411
+ return queryProjectBreakdown(await this.db());
3406
1412
  }
3407
1413
  async brief(query = {}) {
3408
- return buildBrief(this.db(), {
1414
+ return buildBrief(await this.db(), {
3409
1415
  since: query.since,
3410
1416
  machine: query.machine,
3411
1417
  currentMachineId: query.currentMachineId,
@@ -3413,71 +1419,59 @@ class LocalStore {
3413
1419
  });
3414
1420
  }
3415
1421
  async projectDetail(nameOrPath) {
3416
- return queryProjectDetail(this.db(), nameOrPath);
1422
+ return queryProjectDetail(await this.db(), nameOrPath);
3417
1423
  }
3418
1424
  async exportRows(type, period) {
3419
- return queryExportRows(this.db(), type, period);
1425
+ return queryExportRows(await this.db(), type, period);
3420
1426
  }
3421
1427
  async rangeStats(from, to) {
3422
- return queryRangeStats(this.db(), from, to);
1428
+ return queryRangeStats(await this.db(), from, to);
3423
1429
  }
3424
1430
  async forecast() {
3425
- return queryForecast(this.db());
1431
+ return queryForecast(await this.db());
3426
1432
  }
3427
1433
  async efficiency() {
3428
- return queryModelEfficiency(this.db());
1434
+ return queryModelEfficiency(await this.db());
3429
1435
  }
3430
1436
  async recentRequests(since) {
3431
- return queryRequestsSince(this.db(), since);
1437
+ return queryRequestsSince(await this.db(), since);
3432
1438
  }
3433
1439
  async estimate(input) {
3434
- return estimateCostFromRows(listModelPricing(this.db()), input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
1440
+ return estimateCostFromRows(listModelPricing(await this.db()), input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
3435
1441
  }
3436
1442
  async setBudget(input) {
3437
1443
  const now = new Date().toISOString();
3438
1444
  const budget = { id: randomUUID(), ...input, created_at: now, updated_at: now };
3439
- upsertBudget(this.db(), budget);
1445
+ upsertBudget(await this.db(), budget);
3440
1446
  return budget.id;
3441
1447
  }
3442
1448
  async removeBudget(id) {
3443
- deleteBudget(this.db(), id);
1449
+ deleteBudget(await this.db(), id);
3444
1450
  }
3445
1451
  async setGoal(input) {
3446
1452
  const now = new Date().toISOString();
3447
- upsertGoal(this.db(), { id: randomUUID(), ...input, created_at: now, updated_at: now });
1453
+ upsertGoal(await this.db(), { id: randomUUID(), ...input, created_at: now, updated_at: now });
3448
1454
  }
3449
1455
  async removeGoal(id) {
3450
- deleteGoal(this.db(), id);
1456
+ deleteGoal(await this.db(), id);
3451
1457
  }
3452
1458
  async setPricing(input) {
3453
- upsertModelPricing(this.db(), { ...input, updated_at: new Date().toISOString() });
1459
+ upsertModelPricing(await this.db(), { ...input, updated_at: new Date().toISOString() });
3454
1460
  }
3455
1461
  async removePricing(model) {
3456
- deleteModelPricing(this.db(), model);
1462
+ deleteModelPricing(await this.db(), model);
3457
1463
  }
3458
1464
  async setSubscription(input) {
3459
- const now = new Date().toISOString();
3460
- const row = {
3461
- id: input.id ?? randomUUID(),
3462
- provider: input.provider,
3463
- plan: input.plan,
3464
- agent: input.agent,
3465
- monthly_fee_usd: input.monthly_fee_usd,
3466
- included_usage_usd: input.included_usage_usd,
3467
- billing_cycle_start: input.billing_cycle_start,
3468
- reset_policy: input.reset_policy,
3469
- active: input.active,
3470
- created_at: now,
3471
- updated_at: now
3472
- };
3473
- upsertSubscription(this.db(), row);
3474
- return row;
1465
+ const row = configuredSubscription(input);
1466
+ const db = await this.db();
1467
+ upsertSubscription(db, row);
1468
+ return db.get("SELECT * FROM subscriptions WHERE id = ?", row.id);
3475
1469
  }
3476
1470
  async removeSubscription(id) {
3477
- deleteSubscription(this.db(), id);
1471
+ deleteSubscription(await this.db(), id);
3478
1472
  }
3479
1473
  async addProject(path, name) {
3480
- upsertProject(this.db(), {
1474
+ upsertProject(await this.db(), {
3481
1475
  id: randomUUID(),
3482
1476
  path,
3483
1477
  name,
@@ -3487,16 +1481,16 @@ class LocalStore {
3487
1481
  });
3488
1482
  }
3489
1483
  async renameProject(path, name) {
3490
- const existing = getProject(this.db(), path);
1484
+ const existing = getProject(await this.db(), path);
3491
1485
  if (!existing)
3492
1486
  throw new Error("Project not found");
3493
- upsertProject(this.db(), { ...existing, name });
1487
+ upsertProject(await this.db(), { ...existing, name });
3494
1488
  }
3495
1489
  async removeProject(path) {
3496
- deleteProject(this.db(), path);
1490
+ deleteProject(await this.db(), path);
3497
1491
  }
3498
1492
  async sendFeedback(input) {
3499
- insertFeedback(this.db(), {
1493
+ insertFeedback(await this.db(), {
3500
1494
  message: input.message,
3501
1495
  email: input.email ?? null,
3502
1496
  category: input.category ?? "general",
@@ -3520,7 +1514,7 @@ class ApiStore {
3520
1514
  this.cloud = cloud;
3521
1515
  }
3522
1516
  async summary(period, machine) {
3523
- return cloudObject(this.cloud, "/summary", q({ period, machine }));
1517
+ return requireCurrentCostSummary(await cloudObject(this.cloud, "/summary", q({ period, machine })));
3524
1518
  }
3525
1519
  async sessions(filter) {
3526
1520
  return cloudListItems(this.cloud, "sessions", q({ ...filter }));
@@ -3568,22 +1562,23 @@ class ApiStore {
3568
1562
  async fleet(period) {
3569
1563
  const data = await cloudObject(this.cloud, "/fleet", q({ period }));
3570
1564
  return {
3571
- summary: data.summary,
1565
+ summary: requireCurrentCostSummary(data.summary),
3572
1566
  machines: data.machines ?? [],
3573
1567
  registry: data.registry ?? []
3574
1568
  };
3575
1569
  }
3576
1570
  async billingSummary(period) {
3577
- return cloudObject(this.cloud, "/billing", q({ period }));
1571
+ return requireCurrentBillingSummary(await cloudObject(this.cloud, "/billing", q({ period })));
3578
1572
  }
3579
1573
  async billingDiff(period) {
3580
- return cloudObject(this.cloud, "/billing/diff", q({ period }));
1574
+ return requireCurrentBillingDiffSummary(await cloudObject(this.cloud, "/billing/diff", q({ period })));
3581
1575
  }
3582
1576
  async usage(period, agent) {
3583
- return cloudObject(this.cloud, "/usage", q({ period, agent }));
1577
+ const data = await cloudObject(this.cloud, "/usage", q({ period, agent }));
1578
+ return { ...data, summary: requireCurrentCostSummary(data.summary) };
3584
1579
  }
3585
1580
  async savings(period, agent) {
3586
- return cloudObject(this.cloud, "/savings", q({ period, agent }));
1581
+ return requireCurrentSavingsReport(await cloudObject(this.cloud, "/savings", q({ period, agent })));
3587
1582
  }
3588
1583
  async listBudgets() {
3589
1584
  return cloudListItems(this.cloud, "budgets");
@@ -3657,22 +1652,8 @@ class ApiStore {
3657
1652
  await this.cloud.client.delete("pricing", model);
3658
1653
  }
3659
1654
  async setSubscription(input) {
3660
- const now = new Date().toISOString();
3661
- const row = {
3662
- id: input.id ?? randomUUID(),
3663
- provider: input.provider,
3664
- plan: input.plan,
3665
- agent: input.agent,
3666
- monthly_fee_usd: input.monthly_fee_usd,
3667
- included_usage_usd: input.included_usage_usd,
3668
- billing_cycle_start: input.billing_cycle_start,
3669
- reset_policy: input.reset_policy,
3670
- active: input.active,
3671
- created_at: now,
3672
- updated_at: now
3673
- };
3674
- await this.cloud.client.create("subscriptions", row);
3675
- return row;
1655
+ const row = configuredSubscription(input);
1656
+ return this.cloud.client.create("subscriptions", row);
3676
1657
  }
3677
1658
  async removeSubscription(id) {
3678
1659
  await this.cloud.client.delete("subscriptions", id);
@@ -3702,9 +1683,725 @@ function getStore(env = process.env) {
3702
1683
  function isCloudStore(env = process.env) {
3703
1684
  return economyCloudStorage(env).active;
3704
1685
  }
1686
+ // src/lib/ingest-journal-client.ts
1687
+ import { z as z2 } from "zod";
1688
+
1689
+ // src/lib/ingest-journal.ts
1690
+ import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
1691
+ import { createHash } from "crypto";
1692
+ import { join, resolve } from "path";
1693
+ var MAX_COMMANDS = 1000;
1694
+ var MAX_BYTES = 64 * 1024 * 1024;
1695
+ var RECEIPT_RESERVE = 4096;
1696
+ var MAINTENANCE_RESERVE = 16384;
1697
+ var sha = (s) => createHash("sha256").update(s).digest("hex");
1698
+
1699
+ class PendingIngestError extends Error {
1700
+ pending_keys;
1701
+ code = "INGEST_RECONCILIATION_REQUIRED";
1702
+ constructor(pending_keys) {
1703
+ super("Pending ingestion requires hosted receipt reconciliation; no command was replayed");
1704
+ this.pending_keys = pending_keys;
1705
+ this.name = "PendingIngestError";
1706
+ }
1707
+ }
1708
+ function definitiveIngestRefusal(status, body) {
1709
+ const reasons = { 400: "invalid_ingest", 409: "ingest_conflict", 413: "ingest_too_large", 422: "unprocessable_ingest" };
1710
+ if (!Object.hasOwn(reasons, status))
1711
+ return null;
1712
+ const legacy = body?.expected === "legacy_aggregate_requires_migration";
1713
+ return {
1714
+ status,
1715
+ code: legacy ? "legacy_aggregate_requires_migration" : "ingest_refused",
1716
+ reason: legacy ? "legacy_aggregate_requires_migration" : reasons[status]
1717
+ };
1718
+ }
1719
+ function getIngestJournalPath() {
1720
+ return process.env.HASNA_ECONOMY_INGEST_JOURNAL ?? join(cacheDir({ app: "economy", home: process.env.HOME || process.env.USERPROFILE }), "pending-ingest");
1721
+ }
1722
+ function targetId(cloud) {
1723
+ const url = new URL(cloud.client.transport.baseUrl);
1724
+ if (url.username || url.password || url.search || url.hash)
1725
+ throw new Error("Ingest authority must not contain URL credentials or capabilities");
1726
+ return sha(url.toString().replace(/\/+$/, ""));
1727
+ }
1728
+ function secureDirectory(path) {
1729
+ mkdirSync(path, { recursive: true, mode: 448 });
1730
+ const stat = lstatSync(path);
1731
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
1732
+ throw new Error("Ingest journal must be a private owner directory");
1733
+ }
1734
+ function readJson(path, max) {
1735
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
1736
+ try {
1737
+ const st = fstatSync(fd);
1738
+ if (!st.isFile() || st.size > max || (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid())
1739
+ throw new Error("Unsafe ingest journal record");
1740
+ return JSON.parse(readFileSync(fd, "utf8"));
1741
+ } finally {
1742
+ closeSync(fd);
1743
+ }
1744
+ }
1745
+ function createExact(path, value) {
1746
+ const bytes = JSON.stringify(value);
1747
+ let fd;
1748
+ try {
1749
+ fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
1750
+ } catch (error) {
1751
+ if (error.code === "EEXIST")
1752
+ return false;
1753
+ throw error;
1754
+ }
1755
+ try {
1756
+ writeFileSync(fd, bytes);
1757
+ fsyncSync(fd);
1758
+ } finally {
1759
+ closeSync(fd);
1760
+ }
1761
+ if (JSON.stringify(readJson(path, MAX_INGEST_BYTES + 4096)) !== bytes)
1762
+ throw new Error("Ingest journal readback failed");
1763
+ const directory = openSync(resolve(path, ".."), constants.O_RDONLY);
1764
+ try {
1765
+ fsyncSync(directory);
1766
+ } finally {
1767
+ closeSync(directory);
1768
+ }
1769
+ return true;
1770
+ }
1771
+ function unwrap(raw) {
1772
+ return raw && typeof raw === "object" && "data" in raw ? raw.data : raw;
1773
+ }
1774
+ function verifyIngestReceipt(raw, command) {
1775
+ const receipt = unwrap(raw);
1776
+ if (!receipt || typeof receipt !== "object" || !receipt.ingested || typeof receipt.ingested !== "object" || Array.isArray(receipt.ingested) || !Number.isSafeInteger(receipt.total) || receipt.total < 0 || typeof receipt.operation_key !== "string" || !/^[a-f0-9]{64}$/.test(receipt.operation_key) || typeof receipt.replayed !== "boolean")
1777
+ throw new Error("Invalid ingest receipt");
1778
+ let sum = 0;
1779
+ for (const [table, count] of Object.entries(receipt.ingested)) {
1780
+ if (!Array.isArray(command[table]) || typeof count !== "number" || !Number.isSafeInteger(count) || count < 0 || count > command[table].length)
1781
+ throw new Error("Invalid ingest receipt");
1782
+ sum += count;
1783
+ }
1784
+ if (sum !== receipt.total)
1785
+ throw new Error("Invalid ingest receipt");
1786
+ const projected = {
1787
+ ingested: Object.fromEntries(Object.entries(receipt.ingested)),
1788
+ total: receipt.total,
1789
+ operation_key: receipt.operation_key,
1790
+ replayed: receipt.replayed,
1791
+ stale: receipt.stale ?? 0,
1792
+ unchanged: receipt.unchanged ?? 0
1793
+ };
1794
+ const rows = Object.values(command).reduce((n, v) => n + (Array.isArray(v) ? v.length : 0), 0);
1795
+ if (![projected.stale, projected.unchanged].every((n) => Number.isSafeInteger(n) && n >= 0 && n <= rows) || Buffer.byteLength(JSON.stringify(projected)) > RECEIPT_RESERVE - 256)
1796
+ throw new Error("Invalid ingest receipt");
1797
+ return projected;
1798
+ }
1799
+ function sameReceipt(left, right) {
1800
+ const meaning = (value) => ({ ingested: value?.ingested, total: value?.total, operation_key: value?.operation_key, stale: value?.stale ?? 0, unchanged: value?.unchanged ?? 0 });
1801
+ return stableJson(meaning(left)) === stableJson(meaning(right));
1802
+ }
1803
+ async function lookupJournalReceipt(cloud, key, bodyHash, timeoutMs = 1e4) {
1804
+ const query = new URLSearchParams({ idempotency_key: key, body_hash: bodyHash });
1805
+ const raw = unwrap(await cloud.client.transport.get(`/ingest/receipts?${query}`, { retry: false, timeoutMs }));
1806
+ if (!raw || raw.body_hash !== bodyHash)
1807
+ throw new Error("Invalid reconciliation response");
1808
+ if (raw.status === "absent")
1809
+ return null;
1810
+ if (raw.status !== "committed")
1811
+ throw new Error("Invalid reconciliation response");
1812
+ return raw.receipt;
1813
+ }
1814
+
1815
+ class IngestJournal {
1816
+ directory;
1817
+ target;
1818
+ inventoryBytes = 0;
1819
+ freshlyStaged = new Set;
1820
+ constructor(cloud, path = getIngestJournalPath()) {
1821
+ this.target = targetId(cloud);
1822
+ secureDirectory(path);
1823
+ this.directory = join(realpathSync(path), this.target);
1824
+ secureDirectory(this.directory);
1825
+ }
1826
+ names() {
1827
+ const names = [];
1828
+ const directory = opendirSync(this.directory);
1829
+ try {
1830
+ for (let entry;entry = directory.readSync(); ) {
1831
+ if (entry.name === ".lease" || entry.name === ".maintenance" || entry.name === ".authority.json" || entry.name === ".recovery")
1832
+ continue;
1833
+ names.push(entry.name);
1834
+ if (names.length > MAX_COMMANDS * 2)
1835
+ throw new Error("Ingest journal capacity reached; preserve and reconcile records");
1836
+ }
1837
+ } finally {
1838
+ directory.closeSync();
1839
+ }
1840
+ this.inventoryBytes = 0;
1841
+ const set = new Set(names);
1842
+ for (const name of names) {
1843
+ if (!/^[a-f0-9]{64}\.(intent|receipt|refusal)\.json$/.test(name))
1844
+ throw new Error("Unexpected ingest journal entry");
1845
+ if (!name.endsWith(".intent.json") && !set.has(name.replace(/\.(receipt|refusal)\./, ".intent.")))
1846
+ throw new Error("Orphaned ingest outcome requires maintenance recovery");
1847
+ if (name.endsWith(".refusal.json") && set.has(name.replace(".refusal.", ".receipt.")))
1848
+ throw new Error("Conflicting ingest receipt and refusal");
1849
+ const stat = lstatSync(join(this.directory, name));
1850
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0)
1851
+ throw new Error("Unsafe ingest journal record");
1852
+ if (stat.size > MAX_INGEST_BYTES + 4096)
1853
+ throw new Error("Oversized ingest journal record");
1854
+ this.inventoryBytes += stat.size;
1855
+ if (name.endsWith(".refusal.json"))
1856
+ this.refusal(this.intent(name.replace(".refusal.", ".intent.")).command);
1857
+ }
1858
+ return names;
1859
+ }
1860
+ file(key, kind) {
1861
+ return join(this.directory, `${sha(key)}.${kind}.json`);
1862
+ }
1863
+ intent(name) {
1864
+ const record = readJson(join(this.directory, name), MAX_INGEST_BYTES + 4096);
1865
+ if (record.version !== 1 || record.target !== this.target || !record.command || typeof record.command.idempotency_key !== "string" || record.body_hash !== canonicalIngestBodyHash(record.command) || name !== `${sha(record.command.idempotency_key)}.intent.json`)
1866
+ throw new Error("Invalid ingest journal intent");
1867
+ validateIngest(record.command);
1868
+ return record;
1869
+ }
1870
+ status() {
1871
+ const names = this.names(), intents = names.filter((n) => n.endsWith(".intent.json"));
1872
+ const set = new Set(names);
1873
+ const completed = intents.filter((name) => set.has(name.replace(".intent.", ".receipt."))).length;
1874
+ const rejected = intents.filter((name) => set.has(name.replace(".intent.", ".refusal."))).length;
1875
+ const bytes = this.inventoryBytes;
1876
+ return { target: this.target, commands: intents.length, completed, rejected, pending: intents.length - completed - rejected, bytes, reserved_bytes: bytes + (intents.length - completed - rejected) * RECEIPT_RESERVE + MAINTENANCE_RESERVE };
1877
+ }
1878
+ canStage(command, status = this.status()) {
1879
+ const name = `${sha(command.idempotency_key)}.intent.json`;
1880
+ try {
1881
+ if (this.intent(name).body_hash !== canonicalIngestBodyHash(command))
1882
+ throw new Error("Ingest command key is already bound to different content");
1883
+ return true;
1884
+ } catch (error) {
1885
+ if (error.code !== "ENOENT")
1886
+ throw error;
1887
+ }
1888
+ const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), command };
1889
+ return status.commands < MAX_COMMANDS && status.reserved_bytes + Buffer.byteLength(JSON.stringify(record)) + RECEIPT_RESERVE <= MAX_BYTES;
1890
+ }
1891
+ receipt(command) {
1892
+ if (this.refusal(command))
1893
+ throw new Error("Conflicting ingest receipt and refusal");
1894
+ const record = readJson(this.file(command.idempotency_key, "receipt"), RECEIPT_RESERVE);
1895
+ if (record.version !== 1 || record.target !== this.target || record.body_hash !== canonicalIngestBodyHash(command))
1896
+ throw new Error("Invalid receipt journal binding");
1897
+ return verifyIngestReceipt(record.receipt, command);
1898
+ }
1899
+ stage(command) {
1900
+ validateIngest(command);
1901
+ const path = this.file(command.idempotency_key, "intent");
1902
+ const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), command };
1903
+ if (!this.canStage(command))
1904
+ throw new Error("Ingest journal capacity reached; run economy ingest journal maintain; originals preserved");
1905
+ if (createExact(path, record)) {
1906
+ this.freshlyStaged.add(command.idempotency_key);
1907
+ return true;
1908
+ }
1909
+ this.freshlyStaged.delete(command.idempotency_key);
1910
+ const old = this.intent(`${sha(command.idempotency_key)}.intent.json`);
1911
+ if (old.body_hash !== record.body_hash)
1912
+ throw new Error("Ingest command key is already bound to different content");
1913
+ return false;
1914
+ }
1915
+ accept(command, raw) {
1916
+ if (this.refusal(command))
1917
+ throw new Error("A refused command cannot accept a receipt");
1918
+ this.freshlyStaged.delete(command.idempotency_key);
1919
+ const receipt = verifyIngestReceipt(raw, command);
1920
+ const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`);
1921
+ if (intent.body_hash !== canonicalIngestBodyHash(command))
1922
+ throw new Error("Ingest receipt content conflicts with exact intent");
1923
+ const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), receipt };
1924
+ const path = this.file(command.idempotency_key, "receipt");
1925
+ if (!this.names().includes(`${sha(command.idempotency_key)}.receipt.json`) && this.inventoryBytes + Buffer.byteLength(JSON.stringify(record)) > MAX_BYTES)
1926
+ throw new Error("Receipt capacity exhausted; exact intent retained for reconciliation");
1927
+ if (!createExact(path, record)) {
1928
+ const old = readJson(path, MAX_INGEST_BYTES);
1929
+ if (old.body_hash !== record.body_hash || old.target !== this.target || !sameReceipt(old.receipt, receipt))
1930
+ throw new Error("Ingest receipt journal conflict");
1931
+ }
1932
+ return receipt;
1933
+ }
1934
+ recordRefusal(command, refusal) {
1935
+ if (!this.freshlyStaged.delete(command.idempotency_key))
1936
+ throw new Error("Refusal requires this invocation's fresh command");
1937
+ const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`);
1938
+ if (intent.body_hash !== canonicalIngestBodyHash(command))
1939
+ throw new Error("Refusal conflicts with exact intent");
1940
+ const expected = definitiveIngestRefusal(refusal.status, refusal.code === "legacy_aggregate_requires_migration" ? { expected: refusal.code } : null);
1941
+ if (!expected || stableJson(expected) !== stableJson(refusal))
1942
+ throw new Error("Invalid definitive refusal");
1943
+ const names = this.names();
1944
+ if (names.includes(`${sha(command.idempotency_key)}.receipt.json`))
1945
+ throw new Error("A completed command cannot be refused");
1946
+ const record = { version: 1, target: this.target, body_hash: intent.body_hash, refusal };
1947
+ if (Buffer.byteLength(JSON.stringify(record)) > RECEIPT_RESERVE || this.inventoryBytes + Buffer.byteLength(JSON.stringify(record)) > MAX_BYTES)
1948
+ throw new Error("Refusal capacity exhausted; exact intent retained");
1949
+ if (!createExact(this.file(command.idempotency_key, "refusal"), record))
1950
+ throw new Error("Existing refusal requires inspection");
1951
+ this.refusal(command);
1952
+ }
1953
+ refusal(command) {
1954
+ let record;
1955
+ try {
1956
+ record = readJson(this.file(command.idempotency_key, "refusal"), RECEIPT_RESERVE);
1957
+ } catch (error) {
1958
+ if (error.code === "ENOENT")
1959
+ return null;
1960
+ throw error;
1961
+ }
1962
+ const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`), r = record?.refusal;
1963
+ const expected = definitiveIngestRefusal(r?.status, r?.code === "legacy_aggregate_requires_migration" ? { expected: r.code } : null);
1964
+ if (record?.version !== 1 || record.target !== this.target || record.body_hash !== intent.body_hash || intent.body_hash !== canonicalIngestBodyHash(command) || !expected || stableJson(expected) !== stableJson(r))
1965
+ throw new Error("Invalid refusal journal binding");
1966
+ try {
1967
+ lstatSync(this.file(command.idempotency_key, "receipt"));
1968
+ throw new Error("Conflicting ingest receipt and refusal");
1969
+ } catch (error) {
1970
+ if (error.code !== "ENOENT")
1971
+ throw error;
1972
+ }
1973
+ return expected;
1974
+ }
1975
+ async reconcileCommand(cloud, command, timeoutMs = 1e4) {
1976
+ const receipt = await this.lookupReceipt(cloud, command, timeoutMs);
1977
+ return receipt ? this.accept(command, receipt) : null;
1978
+ }
1979
+ assertCloud(cloud) {
1980
+ if (targetId(cloud) !== this.target)
1981
+ throw new Error("Ingest journal authority changed");
1982
+ }
1983
+ async lookupReceipt(cloud, command, timeoutMs = 1e4) {
1984
+ this.assertCloud(cloud);
1985
+ const receipt = await lookupJournalReceipt(cloud, command.idempotency_key, canonicalIngestBodyHash(command), timeoutMs);
1986
+ return receipt === null ? null : verifyIngestReceipt(receipt, command);
1987
+ }
1988
+ async reconcilePending(cloud) {
1989
+ const names = this.names(), unresolved = [];
1990
+ const deadline = performance.now() + 30000;
1991
+ for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
1992
+ const intent = this.intent(name);
1993
+ if (names.includes(`${sha(intent.command.idempotency_key)}.refusal.json`)) {
1994
+ this.refusal(intent.command);
1995
+ continue;
1996
+ }
1997
+ if (names.includes(`${sha(intent.command.idempotency_key)}.receipt.json`)) {
1998
+ const record = readJson(this.file(intent.command.idempotency_key, "receipt"), MAX_INGEST_BYTES);
1999
+ if (record.target !== this.target || record.body_hash !== intent.body_hash)
2000
+ throw new Error("Invalid receipt journal binding");
2001
+ verifyIngestReceipt(record.receipt, intent.command);
2002
+ continue;
2003
+ }
2004
+ if (performance.now() >= deadline) {
2005
+ unresolved.push(intent.command.idempotency_key);
2006
+ continue;
2007
+ }
2008
+ try {
2009
+ if (!await this.reconcileCommand(cloud, intent.command, Math.max(1, Math.min(1e4, deadline - performance.now()))))
2010
+ unresolved.push(intent.command.idempotency_key);
2011
+ } catch {
2012
+ unresolved.push(intent.command.idempotency_key);
2013
+ }
2014
+ }
2015
+ if (unresolved.length)
2016
+ throw new PendingIngestError(unresolved);
2017
+ }
2018
+ }
2019
+
2020
+ // src/lib/ingest-journal-maintenance.ts
2021
+ import { constants as constants2, closeSync as closeSync2, fstatSync as fstatSync2, lstatSync as lstatSync2, openSync as openSync2, readFileSync as readFileSync2, readdirSync } from "fs";
2022
+ import { createHash as createHash2 } from "crypto";
2023
+ import { join as join2 } from "path";
2024
+ var uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
2025
+ var digest = /^[a-f0-9]{64}$/;
2026
+ var sha2 = (bytes) => createHash2("sha256").update(bytes).digest("hex");
2027
+
2028
+ class JournalMaintenanceError extends Error {
2029
+ reason;
2030
+ target;
2031
+ code = "INGEST_JOURNAL_MAINTENANCE_REQUIRED";
2032
+ constructor(reason, target) {
2033
+ super(`Ingest journal maintenance required (${reason}); run economy ingest journal maintain; originals or Trash recovery state preserved`);
2034
+ this.reason = reason;
2035
+ this.target = target;
2036
+ this.name = "JournalMaintenanceError";
2037
+ }
2038
+ }
2039
+ function exists(path) {
2040
+ try {
2041
+ lstatSync2(path);
2042
+ return true;
2043
+ } catch (error) {
2044
+ if (error.code === "ENOENT")
2045
+ return false;
2046
+ throw error;
2047
+ }
2048
+ }
2049
+ function snapshot(path) {
2050
+ const fd = openSync2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
2051
+ try {
2052
+ const stat = fstatSync2(fd);
2053
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 4 * 1024 * 1024 + 4096 || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
2054
+ throw new Error("Unsafe maintenance source");
2055
+ const bytes = readFileSync2(fd), after = fstatSync2(fd);
2056
+ if (after.ctimeMs !== stat.ctimeMs || bytes.length !== stat.size)
2057
+ throw new Error("Maintenance source changed");
2058
+ return { sha256: sha2(bytes), sizeBytes: stat.size, mode: stat.mode & 511, device: String(stat.dev), inode: String(stat.ino), ctime: stat.ctimeMs };
2059
+ } finally {
2060
+ closeSync2(fd);
2061
+ }
2062
+ }
2063
+ function sameSource(path, expected) {
2064
+ const { capture_sha256: _capture, ...original } = expected;
2065
+ if (JSON.stringify(snapshot(path)) !== JSON.stringify(original))
2066
+ throw new Error("Maintenance source changed; preserve for inspection");
2067
+ }
2068
+ function validAnchor(value, target) {
2069
+ const a = value;
2070
+ if (a?.version !== 1 || a.target !== target || !/^[A-Za-z0-9._:-]{1,200}$/.test(a.key) || !digest.test(a.body_hash) || !digest.test(a.receipt?.operation_key))
2071
+ throw new Error("Invalid journal authority anchor");
2072
+ return a;
2073
+ }
2074
+ async function confirm(cloud, anchor, deadline) {
2075
+ if (Date.now() >= deadline)
2076
+ throw new Error("Maintenance deadline reached");
2077
+ const current = await lookupJournalReceipt(cloud, anchor.key, anchor.body_hash, Math.max(1, Math.min(1e4, deadline - Date.now())));
2078
+ if (current === null || !sameReceipt(current, anchor.receipt))
2079
+ throw new Error("Exact Economy receipt is unavailable or changed");
2080
+ }
2081
+ async function verifyJournalAuthority(journal, cloud) {
2082
+ journal.assertCloud(cloud);
2083
+ const path = join2(journal.directory, ".authority.json");
2084
+ if (!exists(path)) {
2085
+ const names = journal.names();
2086
+ const name = names.find((n) => n.endsWith(".intent.json") && names.includes(n.replace(".intent.", ".receipt.")));
2087
+ if (!name)
2088
+ return;
2089
+ const intent = journal.intent(name), receipt = journal.receipt(intent.command);
2090
+ createExact(path, { version: 1, target: journal.target, key: intent.command.idempotency_key, body_hash: intent.body_hash, receipt });
2091
+ }
2092
+ await confirm(cloud, validAnchor(readJson(path, 4096), journal.target), Date.now() + 1e4);
2093
+ }
2094
+ function recoveryFactory(cloud, env = process.env) {
2095
+ return async (operationRoot, checkpoint, deadline) => {
2096
+ if (env.HASNA_ECONOMY_INGEST_RECOVERY !== "trash")
2097
+ throw new Error("recovery_not_configured");
2098
+ const economy = new URL(cloud.client.transport.baseUrl);
2099
+ const fleet = economy.origin === "https://api.hasna.com" && /^\/economy(?:\/v1)?\/?$/.test(economy.pathname);
2100
+ if (!fleet && !env.HASNA_TRASH_API_URL)
2101
+ throw new Error("self_host_trash_authority_required");
2102
+ const sdk = await import("@hasna/trash/sdk");
2103
+ if (sdk.VERSION !== "0.2.3")
2104
+ throw new Error("trash_version_mismatch");
2105
+ const boundedFetch = (input, init) => {
2106
+ const remaining = deadline - Date.now();
2107
+ if (remaining <= 0)
2108
+ throw new Error("maintenance_deadline");
2109
+ const signal = AbortSignal.timeout(remaining);
2110
+ return fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal, redirect: "error" });
2111
+ };
2112
+ return Object.assign(sdk.createTrash({ env, operationRoot, checkpoint, fetchImpl: boundedFetch, transferFetch: boundedFetch, lockTimeoutMs: 100 }), { snapshot: sdk.snapshotIdentity });
2113
+ };
2114
+ }
2115
+
2116
+ class IngestJournalMaintenance {
2117
+ journal;
2118
+ cloud;
2119
+ factory;
2120
+ control;
2121
+ operationRoot;
2122
+ constructor(journal, cloud, factory = recoveryFactory(cloud)) {
2123
+ this.journal = journal;
2124
+ this.cloud = cloud;
2125
+ this.factory = factory;
2126
+ journal.assertCloud(cloud);
2127
+ this.control = join2(journal.directory, ".maintenance");
2128
+ this.operationRoot = join2(journal.directory, ".recovery");
2129
+ }
2130
+ manifest() {
2131
+ secureDirectory(this.control);
2132
+ const names = readdirSync(this.control);
2133
+ if (names.some((n) => !["manifest.json", "intent.operation.json", "receipt.operation.json"].includes(n)))
2134
+ throw new Error("Unexpected maintenance entry");
2135
+ let size = 0;
2136
+ for (const name of names)
2137
+ size += lstatSync2(join2(this.control, name)).size;
2138
+ if (size > MAINTENANCE_RESERVE)
2139
+ throw new Error("Maintenance metadata exceeds reserve");
2140
+ const m = validAnchor(readJson(join2(this.control, "manifest.json"), 8192), this.journal.target);
2141
+ for (const part of [m.intent, m.receipt_file])
2142
+ if (!part || !digest.test(part.sha256) || !digest.test(part.capture_sha256 ?? "") || !Number.isSafeInteger(part.sizeBytes) || part.sizeBytes < 1 || part.sizeBytes > 4198400 || part.mode !== 384 || !/^\d+$/.test(part.device) || !/^\d+$/.test(part.inode) || !Number.isFinite(part.ctime))
2143
+ throw new Error("Invalid maintenance snapshot");
2144
+ return m;
2145
+ }
2146
+ pending(backend) {
2147
+ const pending = backend.pending(4);
2148
+ if (pending.length > 3 || pending.some((p) => !uuid.test(p.id) || p.phase === "unreadable"))
2149
+ throw new Error("Unexpected Trash recovery state");
2150
+ return pending;
2151
+ }
2152
+ async backend(deadline) {
2153
+ const backend = await this.factory(this.operationRoot, async (point, operation) => {
2154
+ if (operation.path === this.control)
2155
+ return;
2156
+ const m = this.manifest();
2157
+ const kind = operation.path === this.journal.file(m.key, "receipt") ? "receipt" : operation.path === this.journal.file(m.key, "intent") ? "intent" : null;
2158
+ if (!kind || operation.kind !== "capture" || !uuid.test(operation.id) || operation.id !== operation.entryId)
2159
+ throw new Error("Unexpected Trash operation");
2160
+ const expected = kind === "receipt" ? m.receipt_file : m.intent;
2161
+ if (point === "afterSnapshot") {
2162
+ if (!operation.receipt || operation.receipt.sha256 !== expected.capture_sha256 || operation.receipt.sizeBytes !== expected.sizeBytes || operation.receipt.mode !== expected.mode)
2163
+ throw new Error("Trash snapshot mismatch");
2164
+ sameSource(operation.path, expected);
2165
+ const path = join2(this.control, `${kind}.operation.json`);
2166
+ createExact(path, { id: operation.id });
2167
+ if (readJson(path, 1024).id !== operation.id)
2168
+ throw new Error("Conflicting Trash operation");
2169
+ }
2170
+ if (point === "beforeStage") {
2171
+ await confirm(this.cloud, m, deadline);
2172
+ sameSource(operation.path, expected);
2173
+ }
2174
+ }, deadline);
2175
+ const status = await backend.doctor();
2176
+ if (!status.station || !uuid.test(status.station.id) || !Number.isInteger(status.retentionDays) || status.retentionDays < 1)
2177
+ throw new Error("Trash station authority unavailable");
2178
+ return backend;
2179
+ }
2180
+ async entry(backend, id, path, expected) {
2181
+ const e = await backend.api.get(id);
2182
+ if (e.id !== id || e.originalPath !== path || e.kind !== "file" || e.sha256 !== expected.capture_sha256 || e.sizeBytes !== expected.sizeBytes || e.mode !== expected.mode || e.state !== "trashed")
2183
+ throw new Error("Trash preservation readback refused");
2184
+ if (exists(path))
2185
+ throw new Error("Retired source reappeared; preserve for inspection");
2186
+ }
2187
+ async pairFile(backend, m, kind, deadline) {
2188
+ await confirm(this.cloud, m, deadline);
2189
+ const path = this.journal.file(m.key, kind), expected = kind === "receipt" ? m.receipt_file : m.intent;
2190
+ const saved = join2(this.control, `${kind}.operation.json`);
2191
+ let id;
2192
+ if (exists(saved))
2193
+ id = readJson(saved, 1024).id;
2194
+ const pending = this.pending(backend);
2195
+ const matches = pending.filter((p) => p.path === path);
2196
+ if (matches.length > 1 || id && !uuid.test(id))
2197
+ throw new Error("Conflicting Trash operation");
2198
+ if (matches[0]) {
2199
+ if (id && id !== matches[0].id)
2200
+ throw new Error("Conflicting Trash operation");
2201
+ id = matches[0].id;
2202
+ const operation = backend.journal.read(id);
2203
+ if (operation.kind !== "capture" || operation.path !== path || operation.entryId !== id)
2204
+ throw new Error("Invalid Trash operation binding");
2205
+ if (exists(path))
2206
+ sameSource(path, expected);
2207
+ await backend.recover(id, { takeAbandonedLock: true });
2208
+ } else if (!id) {
2209
+ sameSource(path, expected);
2210
+ const status = await backend.doctor();
2211
+ const e = await backend.put(path, { retentionDays: status.retentionDays });
2212
+ id = e.id;
2213
+ if (!exists(saved) || readJson(saved, 1024).id !== id)
2214
+ throw new Error("Trash did not persist its capture binding");
2215
+ }
2216
+ await this.entry(backend, id, path, expected);
2217
+ }
2218
+ async recover() {
2219
+ if (!exists(this.control) && !exists(this.operationRoot))
2220
+ return;
2221
+ if (exists(this.operationRoot))
2222
+ secureDirectory(this.operationRoot);
2223
+ if (!exists(this.control) && readdirSync(this.operationRoot).every((n) => n === ".locks"))
2224
+ return;
2225
+ const deadline = Date.now() + 30000;
2226
+ try {
2227
+ await this.finish(await this.backend(deadline), deadline);
2228
+ } catch (error) {
2229
+ if (error instanceof JournalMaintenanceError)
2230
+ throw error;
2231
+ throw new JournalMaintenanceError("recovery_unresolved", this.journal.target);
2232
+ }
2233
+ }
2234
+ async finish(backend, deadline) {
2235
+ const pending = this.pending(backend);
2236
+ const control = pending.filter((p) => p.path === this.control);
2237
+ if (control.length) {
2238
+ if (control.length !== 1 || pending.length !== 1)
2239
+ throw new Error("Conflicting maintenance cleanup");
2240
+ const op = backend.journal.read(control[0].id);
2241
+ if (op.kind !== "capture" || op.path !== this.control)
2242
+ throw new Error("Invalid maintenance cleanup");
2243
+ await backend.recover(op.id, { takeAbandonedLock: true });
2244
+ if (exists(this.control) || this.pending(backend).length)
2245
+ throw new Error("Maintenance cleanup unresolved");
2246
+ return;
2247
+ }
2248
+ if (!exists(this.control)) {
2249
+ if (pending.length)
2250
+ throw new Error("Orphaned maintenance operation");
2251
+ return;
2252
+ }
2253
+ if (readdirSync(this.control).length === 0) {
2254
+ if (pending.length)
2255
+ throw new Error("Orphaned maintenance operation");
2256
+ const status2 = await backend.doctor();
2257
+ await backend.put(this.control, { retentionDays: status2.retentionDays });
2258
+ return;
2259
+ }
2260
+ const m = this.manifest();
2261
+ if (pending.some((p) => p.path !== this.journal.file(m.key, "receipt") && p.path !== this.journal.file(m.key, "intent")))
2262
+ throw new Error("Unexpected recovery path");
2263
+ await this.pairFile(backend, m, "receipt", deadline);
2264
+ await this.pairFile(backend, m, "intent", deadline);
2265
+ if (this.pending(backend).length)
2266
+ throw new Error("Unfinished pair capture");
2267
+ await confirm(this.cloud, m, deadline);
2268
+ const status = await backend.doctor();
2269
+ await backend.put(this.control, { retentionDays: status.retentionDays });
2270
+ if (exists(this.control) || this.pending(backend).length)
2271
+ throw new Error("Maintenance cleanup unresolved");
2272
+ }
2273
+ async maintain(limit = 32) {
2274
+ if (!Number.isInteger(limit) || limit < 1 || limit > 32)
2275
+ throw new Error("Journal maintenance limit must be 1\u201332");
2276
+ await this.recover();
2277
+ await verifyJournalAuthority(this.journal, this.cloud);
2278
+ const deadline = Date.now() + 30000;
2279
+ let retired = 0, bytes = 0;
2280
+ let backend;
2281
+ try {
2282
+ const names = this.journal.names();
2283
+ for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
2284
+ if (retired >= limit || Date.now() >= deadline || retired > 0 && bytes >= 1048576)
2285
+ break;
2286
+ if (!names.includes(name.replace(".intent.", ".receipt.")))
2287
+ continue;
2288
+ const intent = this.journal.intent(name), receipt = this.journal.receipt(intent.command);
2289
+ backend ??= await this.backend(deadline);
2290
+ const m = {
2291
+ version: 1,
2292
+ target: this.journal.target,
2293
+ key: intent.command.idempotency_key,
2294
+ body_hash: intent.body_hash,
2295
+ receipt,
2296
+ intent: snapshot(join2(this.journal.directory, name)),
2297
+ receipt_file: snapshot(this.journal.file(intent.command.idempotency_key, "receipt"))
2298
+ };
2299
+ await confirm(this.cloud, m, deadline);
2300
+ m.intent.capture_sha256 = backend.snapshot(this.journal.file(m.key, "intent")).sha256;
2301
+ m.receipt_file.capture_sha256 = backend.snapshot(this.journal.file(m.key, "receipt")).sha256;
2302
+ secureDirectory(this.control);
2303
+ if (!createExact(join2(this.control, "manifest.json"), m))
2304
+ throw new Error("Existing maintenance transaction");
2305
+ await this.finish(backend, deadline);
2306
+ retired++;
2307
+ bytes += m.intent.sizeBytes + m.receipt_file.sizeBytes;
2308
+ }
2309
+ return { retired, remaining: this.journal.status().completed };
2310
+ } catch (error) {
2311
+ if (error instanceof JournalMaintenanceError)
2312
+ throw error;
2313
+ const reason = error instanceof Error && ["recovery_not_configured", "self_host_trash_authority_required", "trash_version_mismatch"].includes(error.message) ? error.message : backend ? "recovery_unresolved" : "recovery_authority_unavailable";
2314
+ throw new JournalMaintenanceError(reason, this.journal.target);
2315
+ }
2316
+ }
2317
+ async admit(command) {
2318
+ const status = this.journal.status();
2319
+ const fits = this.journal.canStage(command, status);
2320
+ if (status.commands < 64 && status.reserved_bytes < 50331648 && fits)
2321
+ return;
2322
+ try {
2323
+ await this.maintain();
2324
+ } catch (error) {
2325
+ if (exists(this.control) || !this.journal.canStage(command))
2326
+ throw error;
2327
+ }
2328
+ if (!this.journal.canStage(command))
2329
+ throw new JournalMaintenanceError("capacity", this.journal.target);
2330
+ }
2331
+ }
2332
+
2333
+ // src/lib/ingest-journal-operation.ts
2334
+ import { existsSync, readdirSync as readdirSync2 } from "fs";
2335
+ import { z } from "zod";
2336
+
2337
+ // src/lib/ingest-journal-lock.ts
2338
+ import { constants as constants3, closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3 } from "fs";
2339
+ import { join as join3 } from "path";
2340
+ async function withIngestJournalLock(directory, action) {
2341
+ if (process.platform !== "linux" && process.platform !== "darwin")
2342
+ throw new Error("Ingest journal locking requires Linux or macOS");
2343
+ const { dlopen, FFIType } = await import("bun:ffi");
2344
+ const library = dlopen(process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6", {
2345
+ flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 }
2346
+ });
2347
+ const path = join3(directory, ".lease");
2348
+ let fd;
2349
+ try {
2350
+ fd = openSync3(path, constants3.O_RDWR | constants3.O_CREAT | constants3.O_NOFOLLOW | constants3.O_NONBLOCK, 384);
2351
+ const stat = fstatSync3(fd), current = lstatSync3(path);
2352
+ if (!stat.isFile() || stat.size !== 0 || stat.nlink !== 1 || stat.ino !== current.ino || stat.dev !== current.dev || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
2353
+ throw new Error("Unsafe ingest journal lease");
2354
+ if (library.symbols.flock(fd, 6) !== 0) {
2355
+ const error = new Error("Ingest journal is busy; retry after the current operation finishes");
2356
+ Object.assign(error, { code: "INGEST_JOURNAL_BUSY" });
2357
+ throw error;
2358
+ }
2359
+ return await action();
2360
+ } finally {
2361
+ if (fd !== undefined)
2362
+ closeSync3(fd);
2363
+ library.close();
2364
+ }
2365
+ }
2366
+
2367
+ // src/lib/ingest-journal-operation.ts
2368
+ var ingestJournalOperationSchema = z.discriminatedUnion("action", [
2369
+ z.object({ action: z.literal("status") }).strict(),
2370
+ z.object({ action: z.literal("reconcile") }).strict(),
2371
+ z.object({ action: z.literal("maintain"), confirm: z.literal(true), limit: z.number().int().min(1).max(32).default(32) }).strict()
2372
+ ]);
2373
+ async function runIngestJournalOperation(cloud, input) {
2374
+ const options = ingestJournalOperationSchema.parse(input);
2375
+ const journal = new IngestJournal(cloud);
2376
+ return withIngestJournalLock(journal.directory, async () => {
2377
+ const maintenance = new IngestJournalMaintenance(journal, cloud);
2378
+ const unfinished = existsSync(maintenance.control) || existsSync(maintenance.operationRoot) && readdirSync2(maintenance.operationRoot).some((n) => n !== ".locks");
2379
+ if (options.action === "status" && unfinished)
2380
+ throw new JournalMaintenanceError("recovery_pending", journal.target);
2381
+ if (options.action !== "status") {
2382
+ await maintenance.recover();
2383
+ if (options.action === "reconcile")
2384
+ await journal.reconcilePending(cloud);
2385
+ await verifyJournalAuthority(journal, cloud);
2386
+ }
2387
+ const sweep = options.action === "maintain" ? await maintenance.maintain(options.limit) : { retired: 0 };
2388
+ return { schema: "economy.ingest-journal.v1", action: options.action, ...journal.status(), ...sweep };
2389
+ });
2390
+ }
2391
+
2392
+ // src/lib/ingest-journal-client.ts
2393
+ async function ingestJournal(input) {
2394
+ const operation = ingestJournalOperationSchema.parse(input);
2395
+ const cloud = economyCloudStorage();
2396
+ if (!cloud.active)
2397
+ throw Object.assign(new Error("Journal operations require the hosted Economy authority"), { code: "INGEST_HOSTED_AUTHORITY_REQUIRED" });
2398
+ return runIngestJournalOperation(cloud, operation);
2399
+ }
3705
2400
  export {
3706
2401
  isCloudStore,
3707
2402
  isAgent,
2403
+ ingestJournalOperationSchema,
2404
+ ingestJournal,
3708
2405
  getStore,
3709
2406
  LocalStore,
3710
2407
  COST_BASIS,