@hasna/economy 0.3.28 → 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 (216) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +18 -22
  4. package/SECURITY.md +1 -1
  5. package/dist/chunks/agent-registry-store-tpnhx3wj.js +39 -0
  6. package/dist/chunks/billing-bkpxy4kv.js +13 -0
  7. package/dist/chunks/config-j9mv3ehk.js +15 -0
  8. package/dist/chunks/index-2hwzmfgv.js +3407 -0
  9. package/dist/chunks/index-axh32d30.js +1313 -0
  10. package/dist/chunks/index-emvs5ph2.js +2085 -0
  11. package/dist/chunks/index-ez9thsr7.js +3440 -0
  12. package/dist/chunks/index-hesxq90g.js +62 -0
  13. package/dist/chunks/index-k540hfww.js +258 -0
  14. package/dist/chunks/index-kgy9b6bf.js +451 -0
  15. package/dist/chunks/index-kn5572kd.js +124 -0
  16. package/dist/chunks/index-p35cyhc4.js +4 -0
  17. package/dist/chunks/index-w3bvv0ns.js +26 -0
  18. package/dist/chunks/index-zpvea08j.js +3372 -0
  19. package/dist/chunks/menubar-6xtkt17h.js +296 -0
  20. package/dist/chunks/open-projects-6z4pq6mx.js +39 -0
  21. package/dist/chunks/pricing-f80p81gr.js +29 -0
  22. package/dist/chunks/pricing-vf2pycmh.js +30 -0
  23. package/dist/chunks/pricing-y21mcjsa.js +30 -0
  24. package/dist/chunks/serve-0k69e4h9.js +4655 -0
  25. package/dist/chunks/sqlite-store-501f32ts.js +258 -0
  26. package/dist/chunks/sqlite-store-fq4natnf.js +256 -0
  27. package/dist/chunks/sqlite-store-st99garx.js +258 -0
  28. package/dist/chunks/third-party-sqlite-5e85fxa2.js +11 -0
  29. package/dist/chunks/tui-7f8f951w.js +22 -0
  30. package/dist/chunks/watch-mm1j39vy.js +196 -0
  31. package/dist/chunks/webhooks-k4fh2t9c.js +109 -0
  32. package/dist/cli/commands/brief.d.ts.map +1 -1
  33. package/dist/cli/commands/extras.d.ts.map +1 -1
  34. package/dist/cli/commands/journal.d.ts +3 -0
  35. package/dist/cli/commands/journal.d.ts.map +1 -0
  36. package/dist/cli/commands/menubar.d.ts +97 -4
  37. package/dist/cli/commands/menubar.d.ts.map +1 -1
  38. package/dist/cli/commands/tui.d.ts +1 -1
  39. package/dist/cli/commands/tui.d.ts.map +1 -1
  40. package/dist/cli/commands/watch.d.ts.map +1 -1
  41. package/dist/cli/index.js +1524 -9998
  42. package/dist/db/cloud.d.ts +18 -12
  43. package/dist/db/cloud.d.ts.map +1 -1
  44. package/dist/db/database.d.ts +33 -12
  45. package/dist/db/database.d.ts.map +1 -1
  46. package/dist/db/ingest-concurrency-worker.d.ts +2 -0
  47. package/dist/db/ingest-concurrency-worker.d.ts.map +1 -0
  48. package/dist/db/ingest-errors.d.ts +9 -0
  49. package/dist/db/ingest-errors.d.ts.map +1 -0
  50. package/dist/db/ingest-schema.d.ts +3 -0
  51. package/dist/db/ingest-schema.d.ts.map +1 -0
  52. package/dist/db/ingest-specs.d.ts +8 -0
  53. package/dist/db/ingest-specs.d.ts.map +1 -0
  54. package/dist/db/ingest-test-contract.d.ts +3 -0
  55. package/dist/db/ingest-test-contract.d.ts.map +1 -0
  56. package/dist/db/ingest-validation.d.ts +12 -0
  57. package/dist/db/ingest-validation.d.ts.map +1 -0
  58. package/dist/db/ingest.d.ts +27 -0
  59. package/dist/db/ingest.d.ts.map +1 -0
  60. package/dist/db/legacy-schema.d.ts +7 -0
  61. package/dist/db/legacy-schema.d.ts.map +1 -0
  62. package/dist/db/observation-contract.d.ts +3 -0
  63. package/dist/db/observation-contract.d.ts.map +1 -0
  64. package/dist/db/observation-provenance.d.ts +23 -0
  65. package/dist/db/observation-provenance.d.ts.map +1 -0
  66. package/dist/db/observation-schema.d.ts +24 -0
  67. package/dist/db/observation-schema.d.ts.map +1 -0
  68. package/dist/db/pg-connection.d.ts +10 -0
  69. package/dist/db/pg-connection.d.ts.map +1 -0
  70. package/dist/db/pg-migrate.d.ts +4 -9
  71. package/dist/db/pg-migrate.d.ts.map +1 -1
  72. package/dist/db/pg-migrations.d.ts.map +1 -1
  73. package/dist/db/pg-protocol.d.ts +28 -0
  74. package/dist/db/pg-protocol.d.ts.map +1 -0
  75. package/dist/db/sqlite-store.d.ts +4 -0
  76. package/dist/db/sqlite-store.d.ts.map +1 -0
  77. package/dist/db/sync-pg.d.ts +17 -21
  78. package/dist/db/sync-pg.d.ts.map +1 -1
  79. package/dist/db/third-party-sqlite.d.ts +10 -0
  80. package/dist/db/third-party-sqlite.d.ts.map +1 -0
  81. package/dist/index.d.ts +3 -0
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +1281 -2405
  84. package/dist/ingest/claude-quota.d.ts.map +1 -1
  85. package/dist/ingest/claude.d.ts.map +1 -1
  86. package/dist/ingest/codex-quota.d.ts.map +1 -1
  87. package/dist/ingest/codex.d.ts +25 -0
  88. package/dist/ingest/codex.d.ts.map +1 -1
  89. package/dist/ingest/collection-outcome.d.ts +17 -0
  90. package/dist/ingest/collection-outcome.d.ts.map +1 -0
  91. package/dist/ingest/cursor.d.ts.map +1 -1
  92. package/dist/ingest/gemini.d.ts.map +1 -1
  93. package/dist/ingest/hermes.d.ts +1 -1
  94. package/dist/ingest/hermes.d.ts.map +1 -1
  95. package/dist/ingest/loops.d.ts +9 -0
  96. package/dist/ingest/loops.d.ts.map +1 -1
  97. package/dist/ingest/opencode.d.ts +1 -1
  98. package/dist/ingest/opencode.d.ts.map +1 -1
  99. package/dist/ingest/pi.d.ts +1 -1
  100. package/dist/ingest/pi.d.ts.map +1 -1
  101. package/dist/ingest/quota-observation.d.ts +16 -0
  102. package/dist/ingest/quota-observation.d.ts.map +1 -0
  103. package/dist/ingest/source-time.d.ts +4 -0
  104. package/dist/ingest/source-time.d.ts.map +1 -0
  105. package/dist/lib/accounts-store.d.ts +109 -0
  106. package/dist/lib/accounts-store.d.ts.map +1 -0
  107. package/dist/lib/accounts.d.ts +2 -1
  108. package/dist/lib/accounts.d.ts.map +1 -1
  109. package/dist/lib/analytics.d.ts.map +1 -1
  110. package/dist/lib/api-display-url.d.ts +19 -0
  111. package/dist/lib/api-display-url.d.ts.map +1 -0
  112. package/dist/lib/autosync-gate.d.ts +22 -0
  113. package/dist/lib/autosync-gate.d.ts.map +1 -0
  114. package/dist/lib/billing-diff.d.ts +18 -40
  115. package/dist/lib/billing-diff.d.ts.map +1 -1
  116. package/dist/lib/brief.d.ts +2 -0
  117. package/dist/lib/brief.d.ts.map +1 -1
  118. package/dist/lib/budget-decimal.d.ts +14 -0
  119. package/dist/lib/budget-decimal.d.ts.map +1 -0
  120. package/dist/lib/cloud-ingest.d.ts +30 -1
  121. package/dist/lib/cloud-ingest.d.ts.map +1 -1
  122. package/dist/lib/cloud-storage.d.ts +166 -10
  123. package/dist/lib/cloud-storage.d.ts.map +1 -1
  124. package/dist/lib/gatherer.d.ts.map +1 -1
  125. package/dist/lib/ingest-batches.d.ts +21 -0
  126. package/dist/lib/ingest-batches.d.ts.map +1 -0
  127. package/dist/lib/ingest-journal-client.d.ts +37 -0
  128. package/dist/lib/ingest-journal-client.d.ts.map +1 -0
  129. package/dist/lib/ingest-journal-lock.d.ts +5 -0
  130. package/dist/lib/ingest-journal-lock.d.ts.map +1 -0
  131. package/dist/lib/ingest-journal-maintenance.d.ts +92 -0
  132. package/dist/lib/ingest-journal-maintenance.d.ts.map +1 -0
  133. package/dist/lib/ingest-journal-operation.d.ts +55 -0
  134. package/dist/lib/ingest-journal-operation.d.ts.map +1 -0
  135. package/dist/lib/ingest-journal.d.ts +73 -0
  136. package/dist/lib/ingest-journal.d.ts.map +1 -0
  137. package/dist/lib/money-display.d.ts +13 -0
  138. package/dist/lib/money-display.d.ts.map +1 -0
  139. package/dist/lib/periods.d.ts +12 -0
  140. package/dist/lib/periods.d.ts.map +1 -1
  141. package/dist/lib/pricing.d.ts +14 -0
  142. package/dist/lib/pricing.d.ts.map +1 -1
  143. package/dist/lib/savings.d.ts +64 -7
  144. package/dist/lib/savings.d.ts.map +1 -1
  145. package/dist/lib/serve-auth.d.ts.map +1 -1
  146. package/dist/lib/store/index.d.ts +29 -22
  147. package/dist/lib/store/index.d.ts.map +1 -1
  148. package/dist/lib/summary-contract.d.ts +32 -0
  149. package/dist/lib/summary-contract.d.ts.map +1 -0
  150. package/dist/lib/summary-test-contract.d.ts +3 -0
  151. package/dist/lib/summary-test-contract.d.ts.map +1 -0
  152. package/dist/lib/sync-all.d.ts +4 -0
  153. package/dist/lib/sync-all.d.ts.map +1 -1
  154. package/dist/lib/sync-maintenance.d.ts.map +1 -1
  155. package/dist/lib/sync-report.d.ts +4 -0
  156. package/dist/lib/sync-report.d.ts.map +1 -0
  157. package/dist/lib/test-keychain-fixture.d.ts +9 -0
  158. package/dist/lib/test-keychain-fixture.d.ts.map +1 -0
  159. package/dist/mcp/agent-registry-store.d.ts +30 -0
  160. package/dist/mcp/agent-registry-store.d.ts.map +1 -0
  161. package/dist/mcp/agent-registry.d.ts +78 -0
  162. package/dist/mcp/agent-registry.d.ts.map +1 -0
  163. package/dist/mcp/harness.d.ts +26 -0
  164. package/dist/mcp/harness.d.ts.map +1 -0
  165. package/dist/mcp/index.js +3219 -3357
  166. package/dist/mcp/ingest-journal-tool.d.ts +2 -0
  167. package/dist/mcp/ingest-journal-tool.d.ts.map +1 -0
  168. package/dist/mcp/server.d.ts.map +1 -1
  169. package/dist/openapi.d.ts.map +1 -1
  170. package/dist/otel/index.js +2197 -121
  171. package/dist/server/human-auth.d.ts +20 -0
  172. package/dist/server/human-auth.d.ts.map +1 -0
  173. package/dist/server/index.js +10477 -6378
  174. package/dist/server/legacy-inventory.d.ts +52 -0
  175. package/dist/server/legacy-inventory.d.ts.map +1 -0
  176. package/dist/server/legacy-operations.d.ts +64 -0
  177. package/dist/server/legacy-operations.d.ts.map +1 -0
  178. package/dist/server/legacy-plan.d.ts +55 -0
  179. package/dist/server/legacy-plan.d.ts.map +1 -0
  180. package/dist/server/legacy-preservation.d.ts +58 -0
  181. package/dist/server/legacy-preservation.d.ts.map +1 -0
  182. package/dist/server/pg-sync-worker.js +195 -21
  183. package/dist/server/serve.d.ts +20 -18
  184. package/dist/server/serve.d.ts.map +1 -1
  185. package/dist/server/service-admission.d.ts +10 -0
  186. package/dist/server/service-admission.d.ts.map +1 -0
  187. package/dist/server/service-dispatcher.d.ts +45 -0
  188. package/dist/server/service-dispatcher.d.ts.map +1 -0
  189. package/dist/server/service-operations.d.ts +5 -0
  190. package/dist/server/service-operations.d.ts.map +1 -0
  191. package/dist/server/service-protocol.d.ts +50 -0
  192. package/dist/server/service-protocol.d.ts.map +1 -0
  193. package/dist/server/service-worker.d.ts +2 -0
  194. package/dist/server/service-worker.d.ts.map +1 -0
  195. package/dist/server/service-worker.js +8029 -0
  196. package/dist/types/index.d.ts +66 -0
  197. package/dist/types/index.d.ts.map +1 -1
  198. package/docs/README.md +1 -1
  199. package/docs/cli.md +2 -3
  200. package/docs/configuration.md +25 -12
  201. package/docs/cost-reporting.md +49 -0
  202. package/docs/historical-data-reconciliation.md +203 -0
  203. package/docs/ingest-journal.md +108 -0
  204. package/docs/ingestion.md +61 -5
  205. package/docs/mcp.md +5 -3
  206. package/docs/native-budget-save.md +84 -0
  207. package/docs/native-human-auth.md +102 -0
  208. package/docs/otel.md +8 -2
  209. package/docs/rest-api.md +4 -4
  210. package/package.json +17 -20
  211. package/postinstall.js +52 -1
  212. package/dashboard/README.md +0 -26
  213. package/dist/cli/brains.d.ts +0 -3
  214. package/dist/cli/brains.d.ts.map +0 -1
  215. package/dist/lib/test-hermetic-accounts.d.ts +0 -6
  216. package/dist/lib/test-hermetic-accounts.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,2036 +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 { dataDir } from "@hasna/paths";
598
- function normalizeMachineId(value) {
599
- const id = value?.trim().toLowerCase().split(".")[0];
600
- return id && id.length > 0 ? id : null;
601
- }
602
- function macHostMachineId() {
603
- if (platform() !== "darwin")
604
- return null;
605
- for (const key of ["LocalHostName", "ComputerName", "HostName"]) {
606
- try {
607
- const value = execFileSync("/usr/sbin/scutil", ["--get", key], {
608
- encoding: "utf8",
609
- stdio: ["ignore", "pipe", "ignore"],
610
- timeout: 1000
611
- });
612
- const id = normalizeMachineId(value);
613
- if (id && id !== "mac" && id !== "localhost")
614
- return id;
615
- } catch {}
616
- }
617
- return null;
618
- }
619
- function getMachineId() {
620
- const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
621
- if (envMachine)
622
- return envMachine;
623
- const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
624
- if (hostMachine === "mac" || hostMachine === "localhost")
625
- return macHostMachineId() ?? hostMachine;
626
- return hostMachine;
627
- }
628
- function getHomeDir() {
629
- const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
630
- if (!home)
631
- throw new Error("Could not resolve the user home directory");
632
- return home;
633
- }
634
- function getResolverDataRoot() {
635
- return dataDir({
636
- app: "economy",
637
- home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
638
- });
639
- }
640
- function getLegacyDataRoot() {
641
- return join(getHomeDir(), ".hasna", "economy");
642
- }
643
- function adoptResolverDataRoot(resolved, env = process.env) {
644
- const dataOverride = env.HASNA_DATA_HOME;
645
- if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
646
- return true;
647
- return existsSync(join(resolved, "economy.db"));
648
- }
649
- function getExactDataRoot() {
650
- const dir = process.env["HASNA_ECONOMY_HOME"] ?? process.env["ECONOMY_HOME"];
651
- if (dir && dir.trim())
652
- return dir.trim();
653
- return;
654
- }
655
- function getDataDir() {
656
- const exact = getExactDataRoot();
657
- const effective = exact ? resolve(exact) : adoptResolverDataRoot(getResolverDataRoot()) ? resolve(getResolverDataRoot()) : getLegacyDataRoot();
658
- const oldDir = join(getHomeDir(), ".economy");
659
- if (existsSync(oldDir) && !existsSync(effective)) {
660
- mkdirSync(effective, { recursive: true });
661
- for (const file of readdirSync(oldDir)) {
662
- const oldPath = join(oldDir, file);
663
- if (statSync(oldPath).isFile()) {
664
- copyFileSync(oldPath, join(effective, file));
665
- }
666
- }
667
- }
668
- mkdirSync(effective, { recursive: true });
669
- return effective;
670
- }
671
- function getDbPath() {
672
- if (process.env["HASNA_ECONOMY_DB_PATH"])
673
- return process.env["HASNA_ECONOMY_DB_PATH"];
674
- if (process.env["ECONOMY_DB"])
675
- return process.env["ECONOMY_DB"];
676
- return join(getDataDir(), "economy.db");
677
- }
678
- function isSqliteBusyError(error) {
679
- const candidate = error;
680
- const code = typeof candidate.code === "string" ? candidate.code : "";
681
- const message = typeof candidate.message === "string" ? candidate.message : String(error);
682
- return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
683
- }
684
- function retryDelayMs(attempt) {
685
- return Math.min(1000, 50 * 2 ** attempt);
686
- }
687
- function withSqliteBusyRetry(operation, context) {
688
- const maxAttempts = 8;
689
- let lastError;
690
- for (let attempt = 0;attempt < maxAttempts; attempt++) {
691
- try {
692
- return operation();
693
- } catch (error) {
694
- if (!isSqliteBusyError(error))
695
- throw error;
696
- lastError = error;
697
- if (attempt === maxAttempts - 1)
698
- break;
699
- Bun.sleepSync(retryDelayMs(attempt));
700
- }
701
- }
702
- 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 });
703
- }
704
- function openDatabase(dbPath, skipSeed = false) {
705
- const path = dbPath ?? getDbPath();
706
- if (path !== ":memory:") {
707
- const dir = path.substring(0, path.lastIndexOf("/"));
708
- if (dir && !existsSync(dir))
709
- mkdirSync(dir, { recursive: true });
710
- }
711
- const db = withSqliteBusyRetry(() => {
712
- const opened = new SqliteAdapter(path);
713
- try {
714
- opened.exec("PRAGMA busy_timeout = 10000");
715
- opened.exec("PRAGMA journal_mode = WAL");
716
- opened.exec("PRAGMA foreign_keys = ON");
717
- initSchema(opened);
718
- return opened;
719
- } catch (error) {
720
- try {
721
- opened.close();
722
- } catch {}
723
- throw error;
724
- }
725
- }, `opening ${path}`);
726
- if (!skipSeed) {
727
- Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
728
- }
729
- return db;
730
- }
731
- function quoteSqlIdent(identifier) {
732
- return `"${identifier.replace(/"/g, '""')}"`;
733
- }
734
- function hasColumn(db, table, column) {
735
- const columns = db.prepare(`PRAGMA table_info(${quoteSqlIdent(table)})`).all();
736
- return columns.some((c) => c.name === column);
737
- }
738
- function addColumnIfMissing(db, table, column, definition) {
739
- if (hasColumn(db, table, column))
740
- return false;
741
- try {
742
- db.exec(`ALTER TABLE ${quoteSqlIdent(table)} ADD COLUMN ${quoteSqlIdent(column)} ${definition}`);
743
- return true;
744
- } catch (error) {
745
- const message = error instanceof Error ? error.message : String(error);
746
- if (/duplicate column name/i.test(message))
747
- return true;
748
- throw error;
749
- }
750
- }
751
- function initSchema(db) {
752
- db.exec(`
753
- CREATE TABLE IF NOT EXISTS cost_centers (
754
- id TEXT PRIMARY KEY,
755
- kind TEXT NOT NULL,
756
- name TEXT NOT NULL,
757
- repo_path TEXT,
758
- labels_json TEXT DEFAULT '{}',
759
- created_at TEXT NOT NULL,
760
- updated_at TEXT NOT NULL
761
- );
762
-
763
- CREATE TABLE IF NOT EXISTS requests (
764
- id TEXT PRIMARY KEY,
765
- agent TEXT NOT NULL,
766
- session_id TEXT NOT NULL,
767
- model TEXT NOT NULL,
768
- input_tokens INTEGER DEFAULT 0,
769
- output_tokens INTEGER DEFAULT 0,
770
- cache_read_tokens INTEGER DEFAULT 0,
771
- cache_create_tokens INTEGER DEFAULT 0,
772
- cache_create_5m_tokens INTEGER DEFAULT 0,
773
- cache_create_1h_tokens INTEGER DEFAULT 0,
774
- cost_usd REAL NOT NULL DEFAULT 0,
775
- duration_ms INTEGER DEFAULT 0,
776
- timestamp TEXT NOT NULL,
777
- source_request_id TEXT,
778
- machine_id TEXT DEFAULT '',
779
- cost_center_id TEXT,
780
- account_key TEXT DEFAULT '',
781
- account_tool TEXT DEFAULT '',
782
- account_name TEXT DEFAULT '',
783
- account_email TEXT DEFAULT '',
784
- account_source TEXT DEFAULT ''
785
- );
786
-
787
- CREATE TABLE IF NOT EXISTS sessions (
788
- id TEXT PRIMARY KEY,
789
- agent TEXT NOT NULL,
790
- project_path TEXT DEFAULT '',
791
- project_name TEXT DEFAULT '',
792
- started_at TEXT NOT NULL,
793
- ended_at TEXT,
794
- total_cost_usd REAL DEFAULT 0,
795
- total_tokens INTEGER DEFAULT 0,
796
- request_count INTEGER DEFAULT 0,
797
- machine_id TEXT DEFAULT '',
798
- cost_center_id TEXT,
799
- account_key TEXT DEFAULT '',
800
- account_tool TEXT DEFAULT '',
801
- account_name TEXT DEFAULT '',
802
- account_email TEXT DEFAULT '',
803
- account_source TEXT DEFAULT ''
804
- );
805
-
806
- CREATE TABLE IF NOT EXISTS projects (
807
- id TEXT PRIMARY KEY,
808
- path TEXT UNIQUE NOT NULL,
809
- name TEXT NOT NULL,
810
- description TEXT,
811
- tags TEXT DEFAULT '[]',
812
- created_at TEXT NOT NULL
813
- );
814
-
815
- CREATE TABLE IF NOT EXISTS budgets (
816
- id TEXT PRIMARY KEY,
817
- project_path TEXT,
818
- agent TEXT,
819
- cost_center_id TEXT,
820
- period TEXT NOT NULL,
821
- limit_usd REAL NOT NULL,
822
- alert_at_percent INTEGER DEFAULT 80,
823
- created_at TEXT NOT NULL,
824
- updated_at TEXT NOT NULL
825
- );
826
-
827
- CREATE TABLE IF NOT EXISTS goals (
828
- id TEXT PRIMARY KEY,
829
- period TEXT NOT NULL,
830
- project_path TEXT,
831
- agent TEXT,
832
- limit_usd REAL NOT NULL,
833
- created_at TEXT NOT NULL,
834
- updated_at TEXT NOT NULL
835
- );
836
-
837
- CREATE TABLE IF NOT EXISTS ingest_state (
838
- source TEXT NOT NULL,
839
- key TEXT NOT NULL,
840
- value TEXT NOT NULL,
841
- PRIMARY KEY (source, key)
842
- );
843
-
844
- CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
845
- CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp);
846
- CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent);
847
- CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
848
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
849
- CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
850
- CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
851
-
852
- CREATE TABLE IF NOT EXISTS model_pricing (
853
- model TEXT PRIMARY KEY,
854
- input_per_1m REAL NOT NULL DEFAULT 0,
855
- output_per_1m REAL NOT NULL DEFAULT 0,
856
- cache_read_per_1m REAL NOT NULL DEFAULT 0,
857
- cache_write_per_1m REAL NOT NULL DEFAULT 0,
858
- cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
859
- cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
860
- updated_at TEXT NOT NULL
861
- );
862
-
863
- CREATE TABLE IF NOT EXISTS feedback (
864
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
865
- message TEXT NOT NULL,
866
- email TEXT,
867
- category TEXT DEFAULT 'general',
868
- version TEXT,
869
- machine_id TEXT,
870
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
871
- );
872
-
873
- CREATE TABLE IF NOT EXISTS billing_daily (
874
- date TEXT NOT NULL,
875
- provider TEXT NOT NULL,
876
- description TEXT DEFAULT '',
877
- cost_usd REAL NOT NULL DEFAULT 0,
878
- updated_at TEXT NOT NULL,
879
- PRIMARY KEY (date, provider, description)
880
- );
881
-
882
- CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
883
- CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
884
-
885
- CREATE TABLE IF NOT EXISTS subscriptions (
886
- id TEXT PRIMARY KEY,
887
- agent TEXT,
888
- provider TEXT NOT NULL,
889
- plan TEXT NOT NULL,
890
- monthly_fee_usd REAL NOT NULL DEFAULT 0,
891
- included_usage_usd REAL NOT NULL DEFAULT 0,
892
- billing_cycle_start TEXT,
893
- reset_policy TEXT DEFAULT 'monthly',
894
- active INTEGER NOT NULL DEFAULT 1,
895
- created_at TEXT NOT NULL,
896
- updated_at TEXT NOT NULL
897
- );
898
-
899
- CREATE TABLE IF NOT EXISTS usage_snapshots (
900
- id TEXT PRIMARY KEY,
901
- agent TEXT NOT NULL,
902
- date TEXT NOT NULL,
903
- metric TEXT NOT NULL,
904
- value REAL NOT NULL DEFAULT 0,
905
- unit TEXT DEFAULT '',
906
- machine_id TEXT DEFAULT '',
907
- updated_at TEXT NOT NULL
908
- );
909
-
910
- CREATE TABLE IF NOT EXISTS savings_daily (
911
- date TEXT NOT NULL,
912
- agent TEXT DEFAULT '',
913
- api_equivalent_usd REAL NOT NULL DEFAULT 0,
914
- subscription_fee_usd REAL NOT NULL DEFAULT 0,
915
- included_consumed_usd REAL NOT NULL DEFAULT 0,
916
- on_demand_usd REAL NOT NULL DEFAULT 0,
917
- saved_usd REAL NOT NULL DEFAULT 0,
918
- updated_at TEXT NOT NULL,
919
- PRIMARY KEY (date, agent)
920
- );
921
-
922
- CREATE TABLE IF NOT EXISTS machines (
923
- machine_id TEXT PRIMARY KEY,
924
- hostname TEXT NOT NULL,
925
- last_seen_at TEXT,
926
- last_push_at TEXT,
927
- last_pull_at TEXT,
928
- economy_version TEXT,
929
- updated_at TEXT NOT NULL
930
- );
931
-
932
- CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
933
- CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
934
- `);
935
- addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
936
- addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
937
- if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
938
- db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
939
- }
940
- addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
941
- addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
942
- addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
943
- if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
944
- db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
945
- }
946
- addColumnIfMissing(db, "requests", "cache_create_1h_tokens", "INTEGER DEFAULT 0");
947
- addColumnIfMissing(db, "requests", "cost_basis", `TEXT DEFAULT 'estimated'`);
948
- addColumnIfMissing(db, "requests", "attribution_tag", `TEXT DEFAULT ''`);
949
- if (addColumnIfMissing(db, "requests", "updated_at", `TEXT DEFAULT ''`)) {
950
- db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
951
- }
952
- addColumnIfMissing(db, "requests", "synced_at", `TEXT DEFAULT ''`);
953
- for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
954
- addColumnIfMissing(db, "requests", column, `TEXT DEFAULT ''`);
955
- }
956
- addColumnIfMissing(db, "sessions", "attribution_tag", `TEXT DEFAULT ''`);
957
- if (addColumnIfMissing(db, "sessions", "updated_at", `TEXT DEFAULT ''`)) {
958
- db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
959
- }
960
- addColumnIfMissing(db, "sessions", "synced_at", `TEXT DEFAULT ''`);
961
- for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
962
- addColumnIfMissing(db, "sessions", column, `TEXT DEFAULT ''`);
963
- }
964
- addColumnIfMissing(db, "model_pricing", "cache_write_1h_per_1m", "REAL NOT NULL DEFAULT 0");
965
- addColumnIfMissing(db, "model_pricing", "cache_storage_per_1m_hour", "REAL NOT NULL DEFAULT 0");
966
- db.exec(`
967
- CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
968
- CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
969
- CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
970
- CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
971
- CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
972
- CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
973
- CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
974
- CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
975
- `);
976
- }
977
- function periodWhere(period) {
978
- switch (period) {
979
- case "today":
980
- return `DATE(timestamp) = DATE('now')`;
981
- case "yesterday":
982
- return `DATE(timestamp) = DATE('now', '-1 day')`;
983
- case "week":
984
- return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
985
- case "month":
986
- return `timestamp >= DATE('now', 'start of month')`;
987
- case "year":
988
- return `timestamp >= DATE('now', 'start of year')`;
989
- case "all":
990
- return "1=1";
991
- }
992
- }
993
- function sessionPeriodWhere(period) {
994
- switch (period) {
995
- case "today":
996
- return `DATE(started_at) = DATE('now')`;
997
- case "yesterday":
998
- return `DATE(started_at) = DATE('now', '-1 day')`;
999
- case "week":
1000
- return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
1001
- case "month":
1002
- return `started_at >= DATE('now', 'start of month')`;
1003
- case "year":
1004
- return `started_at >= DATE('now', 'start of year')`;
1005
- case "all":
1006
- return "1=1";
1007
- }
1008
- }
1009
- function requestPeriodWhere(period) {
1010
- switch (period) {
1011
- case "today":
1012
- return `DATE(timestamp) = DATE('now')`;
1013
- case "yesterday":
1014
- return `DATE(timestamp) = DATE('now', '-1 day')`;
1015
- case "week":
1016
- return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
1017
- case "month":
1018
- return `timestamp >= DATE('now', 'start of month')`;
1019
- case "year":
1020
- return `timestamp >= DATE('now', 'start of year')`;
1021
- case "all":
1022
- return "1=1";
1023
- }
1024
- }
1025
- function querySessions(db, filter = {}) {
1026
- const conditions = [];
1027
- const params = [];
1028
- if (filter.agent) {
1029
- conditions.push("agent = ?");
1030
- params.push(filter.agent);
1031
- }
1032
- if (filter.project) {
1033
- conditions.push("project_path LIKE ?");
1034
- params.push(`%${filter.project}%`);
1035
- }
1036
- if (filter.account) {
1037
- const q = `%${filter.account}%`;
1038
- conditions.push("(account_key LIKE ? OR account_name LIKE ? OR account_email LIKE ?)");
1039
- params.push(q, q, q);
1040
- }
1041
- if (filter.since) {
1042
- conditions.push("started_at >= ?");
1043
- params.push(filter.since);
1044
- }
1045
- if (filter.machine) {
1046
- conditions.push("machine_id = ?");
1047
- params.push(filter.machine);
1048
- }
1049
- if (filter.search) {
1050
- const q = `%${filter.search}%`;
1051
- conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
1052
- params.push(q, q, `${filter.search}%`);
1053
- }
1054
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
1055
- const limit = filter.limit ?? 50;
1056
- const offset = filter.offset ?? 0;
1057
- return db.prepare(`
1058
- SELECT * FROM sessions ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?
1059
- `).all(...params, limit, offset);
1060
- }
1061
- function queryTopSessions(db, n = 10, agent, since) {
1062
- const conditions = [];
1063
- const params = [];
1064
- if (agent) {
1065
- conditions.push("agent = ?");
1066
- params.push(agent);
1067
- }
1068
- if (since) {
1069
- conditions.push("started_at >= ?");
1070
- params.push(since);
1071
- }
1072
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
1073
- return db.prepare(`SELECT * FROM sessions ${where} ORDER BY total_cost_usd DESC LIMIT ?`).all(...params, n);
1074
- }
1075
- function querySummary(db, period, machine, allMachines = false, agent) {
1076
- const rWhere = periodWhere(period);
1077
- const sWhere = sessionPeriodWhere(period);
1078
- const machineClause = !allMachines && machine ? " AND machine_id = ?" : "";
1079
- const agentClause = agent ? " AND agent = ?" : "";
1080
- const params = [];
1081
- if (!allMachines && machine)
1082
- params.push(machine);
1083
- if (agent)
1084
- params.push(agent);
1085
- const r = db.prepare(`
1086
- SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
1087
- COUNT(*) as requests,
1088
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
1089
- FROM requests WHERE ${rWhere}${machineClause}${agentClause}
1090
- `).get(...params);
1091
- const codexTotals = db.prepare(`
1092
- SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1093
- COALESCE(SUM(total_tokens), 0) as tokens,
1094
- COALESCE(SUM(request_count), 0) as requests,
1095
- COUNT(*) as sessions
1096
- FROM sessions
1097
- WHERE ${sWhere}${machineClause}${agentClause}
1098
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1099
- `).get(...params);
1100
- const requestSessionCount = db.prepare(`
1101
- SELECT COUNT(DISTINCT session_id) as sessions
1102
- FROM requests
1103
- WHERE ${rWhere}${machineClause}${agentClause}
1104
- `).get(...params);
1105
- return {
1106
- total_usd: r.total_usd + codexTotals.cost_usd,
1107
- requests: r.requests + codexTotals.requests,
1108
- tokens: r.tokens + codexTotals.tokens,
1109
- sessions: requestSessionCount.sessions + codexTotals.sessions,
1110
- period
1111
- };
1112
- }
1113
- function queryModelBreakdown(db) {
1114
- return db.prepare(`
1115
- SELECT model, agent,
1116
- COUNT(*) as requests,
1117
- COALESCE(SUM(input_tokens), 0) as input_tokens,
1118
- COALESCE(SUM(output_tokens), 0) as output_tokens,
1119
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1120
- COALESCE(SUM(cost_usd), 0) as cost_usd
1121
- FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
1122
- `).all();
1123
- }
1124
- function queryAgentBreakdown(db, period = "all", machine) {
1125
- const requestWhere = requestPeriodWhere(period);
1126
- const machineClause = machine ? " AND machine_id = ?" : "";
1127
- const machineParams = machine ? [machine] : [];
1128
- const groups = new Map;
1129
- const requestRows = db.prepare(`
1130
- SELECT agent,
1131
- COUNT(DISTINCT session_id) as sessions,
1132
- COUNT(*) as requests,
1133
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1134
- COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
1135
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1136
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1137
- COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1138
- COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
1139
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
1140
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1141
- MAX(timestamp) as last_active
1142
- FROM requests
1143
- WHERE ${requestWhere}${machineClause}
1144
- GROUP BY agent
1145
- ORDER BY api_equivalent_usd DESC
1146
- `).all(...machineParams);
1147
- for (const row of requestRows) {
1148
- groups.set(row.agent, row);
1149
- }
1150
- const sessionWhere = sessionPeriodWhere(period);
1151
- const sessionOnlyRows = db.prepare(`
1152
- SELECT agent,
1153
- COUNT(*) as sessions,
1154
- COALESCE(SUM(request_count), 0) as requests,
1155
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1156
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1157
- MAX(started_at) as last_active
1158
- FROM sessions
1159
- WHERE ${sessionWhere}${machineClause}
1160
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1161
- GROUP BY agent
1162
- `).all(...machineParams);
1163
- for (const row of sessionOnlyRows) {
1164
- const existing = groups.get(row.agent) ?? {
1165
- agent: row.agent,
1166
- sessions: 0,
1167
- requests: 0,
1168
- total_tokens: 0,
1169
- api_equivalent_usd: 0,
1170
- billable_usd: 0,
1171
- metered_api_usd: 0,
1172
- subscription_included_usd: 0,
1173
- estimated_usd: 0,
1174
- unknown_usd: 0,
1175
- cost_usd: 0,
1176
- last_active: ""
1177
- };
1178
- existing.sessions += row.sessions;
1179
- existing.requests += row.requests;
1180
- existing.total_tokens += row.total_tokens;
1181
- existing.api_equivalent_usd += row.cost_usd;
1182
- existing.estimated_usd += row.cost_usd;
1183
- existing.cost_usd += row.cost_usd;
1184
- if (!existing.last_active || row.last_active > existing.last_active)
1185
- existing.last_active = row.last_active;
1186
- groups.set(row.agent, existing);
1187
- }
1188
- return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
1189
- }
1190
- function pathProjectLabel(projectPath) {
1191
- if (!projectPath)
1192
- return "";
1193
- const segments = projectPath.split("/").filter(Boolean);
1194
- const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
1195
- for (const seg of segments) {
1196
- if (projectPrefix.test(seg))
1197
- return seg;
1198
- }
1199
- const generic = new Set([
1200
- "web",
1201
- "app",
1202
- "apps",
1203
- "packages",
1204
- "src",
1205
- "lib",
1206
- "server",
1207
- "client",
1208
- "api",
1209
- "frontend",
1210
- "backend",
1211
- "home",
1212
- "users",
1213
- "workspace",
1214
- "workspaces",
1215
- "hasna"
1216
- ]);
1217
- for (let i = segments.length - 1;i >= 0; i--) {
1218
- if (!generic.has(segments[i].toLowerCase()))
1219
- return segments[i];
1220
- }
1221
- return null;
1222
- }
1223
- function isRepoLikeLabel(label) {
1224
- return /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/.test(label) || label.includes("-");
1225
- }
1226
- function labelForPath(projectPath, projectName) {
1227
- const pathLabel = pathProjectLabel(projectPath);
1228
- if (pathLabel && (!projectName || projectName.trim() === "" || isRepoLikeLabel(pathLabel)))
1229
- return pathLabel;
1230
- if (projectName && projectName.trim() !== "")
1231
- return projectName;
1232
- if (pathLabel)
1233
- return pathLabel;
1234
- return projectPath;
1235
- }
1236
- function groupKeyForPath(projectPath, projectName) {
1237
- return labelForPath(projectPath, projectName).trim().toLowerCase();
1238
- }
1239
- function laterTimestamp(current, candidate) {
1240
- if (!candidate)
1241
- return current;
1242
- return candidate > current ? candidate : current;
1243
- }
1244
- function queryProjectBreakdownScoped(db, requestWhere, sessionWhere, requestScopeParams = [], sessionScopeParams = [], machine) {
1245
- const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
1246
- const requestMachineClause = machine ? " AND machine_id = ?" : "";
1247
- const sessionMachineParams = machine ? [machine, machine] : [];
1248
- const requestMachineParams = machine ? [machine] : [];
1249
- const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
1250
- const sessionOnlyMachineParams = machine ? [machine] : [];
1251
- const sessions = db.prepare(`
1252
- SELECT id, project_path, project_name
1253
- FROM sessions
1254
- WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
1255
- `).all(...sessionMachineParams);
1256
- const groups = new Map;
1257
- const groupKeyBySession = new Map;
1258
- for (const s of sessions) {
1259
- const label = labelForPath(s.project_path, s.project_name);
1260
- if (!label)
1261
- continue;
1262
- const key = groupKeyForPath(s.project_path, s.project_name);
1263
- const g = groups.get(key) ?? { label, samplePath: s.project_path, sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: "" };
1264
- if (!g.samplePath)
1265
- g.samplePath = s.project_path;
1266
- groups.set(key, g);
1267
- groupKeyBySession.set(s.id, key);
1268
- }
1269
- const groupFor = (sessionId) => {
1270
- const key = groupKeyBySession.get(sessionId);
1271
- return key === undefined ? undefined : groups.get(key);
1272
- };
1273
- const requestRows = db.prepare(`
1274
- SELECT session_id,
1275
- COUNT(*) as requests,
1276
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1277
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1278
- MAX(timestamp) as last_active
1279
- FROM requests
1280
- WHERE ${requestWhere}${requestMachineClause}
1281
- GROUP BY session_id
1282
- `).all(...requestScopeParams, ...requestMachineParams);
1283
- for (const row of requestRows) {
1284
- const g = groupFor(row.session_id);
1285
- if (!g)
1286
- continue;
1287
- g.sessions += 1;
1288
- g.requests += row.requests;
1289
- g.cost_usd += row.cost_usd;
1290
- g.total_tokens += row.total_tokens;
1291
- g.last_active = laterTimestamp(g.last_active, row.last_active);
1292
- }
1293
- const sessionsWithRequests = new Set(db.prepare(`SELECT DISTINCT session_id FROM requests`).all().map((row) => row.session_id));
1294
- const sessionOnlyRows = db.prepare(`
1295
- SELECT id,
1296
- COALESCE(request_count, 0) as request_count,
1297
- COALESCE(total_tokens, 0) as total_tokens,
1298
- COALESCE(total_cost_usd, 0) as total_cost_usd,
1299
- started_at
1300
- FROM sessions
1301
- WHERE ${sessionWhere}${sessionOnlyMachineClause}
1302
- `).all(...sessionScopeParams, ...sessionOnlyMachineParams);
1303
- for (const row of sessionOnlyRows) {
1304
- if (sessionsWithRequests.has(row.id))
1305
- continue;
1306
- const g = groupFor(row.id);
1307
- if (!g)
1308
- continue;
1309
- g.sessions += 1;
1310
- g.requests += row.request_count;
1311
- g.total_tokens += row.total_tokens;
1312
- g.cost_usd += row.total_cost_usd;
1313
- g.last_active = laterTimestamp(g.last_active, row.started_at);
1314
- }
1315
- const result = [];
1316
- for (const g of groups.values()) {
1317
- if (g.sessions === 0)
1318
- continue;
1319
- result.push({
1320
- project_path: g.samplePath,
1321
- project_name: g.label,
1322
- sessions: g.sessions,
1323
- requests: g.requests,
1324
- total_tokens: g.total_tokens,
1325
- cost_usd: g.cost_usd,
1326
- last_active: g.last_active
1327
- });
1328
- }
1329
- result.sort((a, b) => b.cost_usd - a.cost_usd);
1330
- return result;
1331
- }
1332
- function queryProjectBreakdown(db, period = "all", machine) {
1333
- return queryProjectBreakdownScoped(db, requestPeriodWhere(period), sessionPeriodWhere(period), [], [], machine);
1334
- }
1335
- function normalizeAccountEmail(email) {
1336
- return (email ?? "").trim().toLowerCase();
1337
- }
1338
- function accountIdentityKey(agent, accountKey, accountName, accountEmail) {
1339
- const identityAgent = (agent || "").trim();
1340
- const normalizedEmail = normalizeAccountEmail(accountEmail);
1341
- if (identityAgent && normalizedEmail)
1342
- return `${identityAgent}:${normalizedEmail}`;
1343
- if (identityAgent && accountName)
1344
- return `${identityAgent}:${accountName}`;
1345
- if (accountKey)
1346
- return accountKey;
1347
- return identityAgent ? `${identityAgent}:unknown` : "";
1348
- }
1349
- function addAccountBreakdownRow(groups, row, sessionOnly) {
1350
- const agent = row.agent || row.account_tool;
1351
- const email = normalizeAccountEmail(row.account_email);
1352
- const accountName = row.account_name || email || row.account_key;
1353
- const key = accountIdentityKey(agent, row.account_key, accountName, email);
1354
- if (!key)
1355
- return;
1356
- const group = groups.get(key) ?? {
1357
- account_key: key,
1358
- account_tool: agent,
1359
- account_name: accountName,
1360
- account_email: email || null,
1361
- account_source: row.account_source || "unknown",
1362
- sessionIds: new Set,
1363
- requests: 0,
1364
- total_tokens: 0,
1365
- api_equivalent_usd: 0,
1366
- metered_api_usd: 0,
1367
- subscription_included_usd: 0,
1368
- estimated_usd: 0,
1369
- unknown_usd: 0,
1370
- last_active: ""
1371
- };
1372
- if (!group.account_email && email)
1373
- group.account_email = email;
1374
- if (!group.account_name && accountName)
1375
- group.account_name = accountName;
1376
- if ((!group.account_source || group.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
1377
- group.account_source = row.account_source;
1378
- }
1379
- if (row.session_id)
1380
- group.sessionIds.add(row.session_id);
1381
- group.requests += row.requests;
1382
- group.total_tokens += row.total_tokens;
1383
- group.api_equivalent_usd += row.cost_usd;
1384
- if (sessionOnly) {
1385
- group.estimated_usd += row.cost_usd;
1386
- } else if (row.cost_basis === "metered_api") {
1387
- group.metered_api_usd += row.cost_usd;
1388
- } else if (row.cost_basis === "subscription_included") {
1389
- group.subscription_included_usd += row.cost_usd;
1390
- } else if (row.cost_basis === "unknown") {
1391
- group.unknown_usd += row.cost_usd;
1392
- } else {
1393
- group.estimated_usd += row.cost_usd;
1394
- }
1395
- if (!group.last_active || row.last_active > group.last_active)
1396
- group.last_active = row.last_active;
1397
- groups.set(key, group);
1398
- }
1399
- function queryAccountBreakdown(db, period = "all", machine) {
1400
- const requestWhere = requestPeriodWhere(period);
1401
- const sessionWhere = sessionPeriodWhere(period);
1402
- const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
1403
- const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
1404
- const requestMachineParams = machine ? [machine] : [];
1405
- const sessionMachineParams = machine ? [machine] : [];
1406
- const groups = new Map;
1407
- const requestRows = db.prepare(`
1408
- SELECT
1409
- r.session_id as session_id,
1410
- COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
1411
- COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
1412
- COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
1413
- COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
1414
- COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
1415
- COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
1416
- 1 as requests,
1417
- COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1418
- COALESCE(r.cost_usd, 0) as cost_usd,
1419
- COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1420
- r.timestamp as last_active
1421
- FROM requests r
1422
- LEFT JOIN sessions s ON s.id = r.session_id
1423
- WHERE ${requestWhere}${requestMachineClause}
1424
- AND (
1425
- COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
1426
- OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
1427
- OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
1428
- OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
1429
- )
1430
- `).all(...requestMachineParams);
1431
- for (const row of requestRows)
1432
- addAccountBreakdownRow(groups, row, false);
1433
- const sessionOnlyRows = db.prepare(`
1434
- SELECT
1435
- s.id as session_id,
1436
- s.agent as agent,
1437
- s.account_key as account_key,
1438
- s.account_tool as account_tool,
1439
- s.account_name as account_name,
1440
- s.account_email as account_email,
1441
- COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
1442
- COALESCE(s.request_count, 0) as requests,
1443
- COALESCE(s.total_tokens, 0) as total_tokens,
1444
- COALESCE(s.total_cost_usd, 0) as cost_usd,
1445
- 'estimated' as cost_basis,
1446
- s.started_at as last_active
1447
- FROM sessions s
1448
- WHERE ${sessionWhere}${sessionMachineClause}
1449
- AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1450
- AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
1451
- `).all(...sessionMachineParams);
1452
- for (const row of sessionOnlyRows)
1453
- addAccountBreakdownRow(groups, row, true);
1454
- const result = [...groups.values()].map((group) => ({
1455
- account_key: group.account_key,
1456
- account_tool: group.account_tool,
1457
- account_name: group.account_name,
1458
- account_email: group.account_email,
1459
- account_source: group.account_source,
1460
- sessions: group.sessionIds.size,
1461
- requests: group.requests,
1462
- total_tokens: group.total_tokens,
1463
- api_equivalent_usd: group.api_equivalent_usd,
1464
- billable_usd: group.metered_api_usd,
1465
- metered_api_usd: group.metered_api_usd,
1466
- subscription_included_usd: group.subscription_included_usd,
1467
- estimated_usd: group.estimated_usd,
1468
- unknown_usd: group.unknown_usd,
1469
- cost_usd: group.api_equivalent_usd,
1470
- last_active: group.last_active
1471
- }));
1472
- result.sort((a, b) => b.cost_usd - a.cost_usd);
1473
- return result;
1474
- }
1475
- function kindFromCostCenterId(id) {
1476
- const prefix = id.split(":", 1)[0] ?? "";
1477
- return ["loop", "app", "repo", "service", "team"].includes(prefix) ? prefix : "";
1478
- }
1479
- function addCostCenterBreakdownRow(groups, row, sessionOnly) {
1480
- if (!row.cost_center_id)
1481
- return;
1482
- const derivedKind = row.kind || kindFromCostCenterId(row.cost_center_id) || "service";
1483
- const group = groups.get(row.cost_center_id) ?? {
1484
- cost_center_id: row.cost_center_id,
1485
- kind: derivedKind,
1486
- name: row.name || row.cost_center_id,
1487
- repo_path: row.repo_path ?? null,
1488
- labels_json: row.labels_json || "{}",
1489
- sessionIds: new Set,
1490
- requests: 0,
1491
- total_tokens: 0,
1492
- api_equivalent_usd: 0,
1493
- metered_api_usd: 0,
1494
- subscription_included_usd: 0,
1495
- estimated_usd: 0,
1496
- unknown_usd: 0,
1497
- last_active: ""
1498
- };
1499
- group.kind = derivedKind;
1500
- if (row.name)
1501
- group.name = row.name;
1502
- if (row.repo_path && !group.repo_path)
1503
- group.repo_path = row.repo_path;
1504
- if (row.labels_json && row.labels_json !== "{}")
1505
- group.labels_json = row.labels_json;
1506
- if (row.session_id)
1507
- group.sessionIds.add(row.session_id);
1508
- group.requests += row.requests;
1509
- group.total_tokens += row.total_tokens;
1510
- group.api_equivalent_usd += row.cost_usd;
1511
- if (sessionOnly) {
1512
- group.estimated_usd += row.cost_usd;
1513
- } else if (row.cost_basis === "metered_api") {
1514
- group.metered_api_usd += row.cost_usd;
1515
- } else if (row.cost_basis === "subscription_included") {
1516
- group.subscription_included_usd += row.cost_usd;
1517
- } else if (row.cost_basis === "unknown") {
1518
- group.unknown_usd += row.cost_usd;
1519
- } else {
1520
- group.estimated_usd += row.cost_usd;
1521
- }
1522
- if (!group.last_active || row.last_active > group.last_active)
1523
- group.last_active = row.last_active;
1524
- groups.set(row.cost_center_id, group);
1525
- }
1526
- function queryCostCenterBreakdown(db, period = "all", filter = {}) {
1527
- const requestWhere = filter.since ? "r.timestamp >= ?" : requestPeriodWhere(period);
1528
- const sessionWhere = filter.since ? "s.started_at >= ?" : sessionPeriodWhere(period);
1529
- const requestMachineClause = filter.machine ? " AND r.machine_id = ?" : "";
1530
- const sessionMachineClause = filter.machine ? " AND s.machine_id = ?" : "";
1531
- const requestParams = [];
1532
- const sessionParams = [];
1533
- if (filter.since) {
1534
- requestParams.push(filter.since);
1535
- sessionParams.push(filter.since);
1536
- }
1537
- if (filter.machine) {
1538
- requestParams.push(filter.machine);
1539
- sessionParams.push(filter.machine);
1540
- }
1541
- const groups = new Map;
1542
- const requestRows = db.prepare(`
1543
- SELECT
1544
- COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) as cost_center_id,
1545
- COALESCE(cc.kind, '') as kind,
1546
- COALESCE(cc.name, COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))) as name,
1547
- cc.repo_path as repo_path,
1548
- COALESCE(cc.labels_json, '{}') as labels_json,
1549
- r.session_id as session_id,
1550
- 1 as requests,
1551
- COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1552
- COALESCE(r.cost_usd, 0) as cost_usd,
1553
- COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1554
- r.timestamp as last_active
1555
- FROM requests r
1556
- LEFT JOIN sessions s ON s.id = r.session_id
1557
- LEFT JOIN cost_centers cc ON cc.id = COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))
1558
- WHERE ${requestWhere}${requestMachineClause}
1559
- AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) IS NOT NULL
1560
- AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) != ''
1561
- `).all(...requestParams);
1562
- for (const row of requestRows)
1563
- addCostCenterBreakdownRow(groups, row, false);
1564
- const sessionOnlyRows = db.prepare(`
1565
- SELECT
1566
- s.cost_center_id as cost_center_id,
1567
- COALESCE(cc.kind, '') as kind,
1568
- COALESCE(cc.name, s.cost_center_id) as name,
1569
- cc.repo_path as repo_path,
1570
- COALESCE(cc.labels_json, '{}') as labels_json,
1571
- s.id as session_id,
1572
- COALESCE(s.request_count, 0) as requests,
1573
- COALESCE(s.total_tokens, 0) as total_tokens,
1574
- COALESCE(s.total_cost_usd, 0) as cost_usd,
1575
- 'estimated' as cost_basis,
1576
- s.started_at as last_active
1577
- FROM sessions s
1578
- LEFT JOIN cost_centers cc ON cc.id = s.cost_center_id
1579
- WHERE ${sessionWhere}${sessionMachineClause}
1580
- AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1581
- AND s.cost_center_id IS NOT NULL
1582
- AND s.cost_center_id != ''
1583
- `).all(...sessionParams);
1584
- for (const row of sessionOnlyRows)
1585
- addCostCenterBreakdownRow(groups, row, true);
1586
- const result = [...groups.values()].map((group) => ({
1587
- cost_center_id: group.cost_center_id,
1588
- kind: group.kind,
1589
- name: group.name,
1590
- repo_path: group.repo_path,
1591
- labels_json: group.labels_json,
1592
- sessions: group.sessionIds.size,
1593
- requests: group.requests,
1594
- total_tokens: group.total_tokens,
1595
- api_equivalent_usd: group.api_equivalent_usd,
1596
- billable_usd: group.metered_api_usd,
1597
- metered_api_usd: group.metered_api_usd,
1598
- subscription_included_usd: group.subscription_included_usd,
1599
- estimated_usd: group.estimated_usd,
1600
- unknown_usd: group.unknown_usd,
1601
- cost_usd: group.api_equivalent_usd,
1602
- last_active: group.last_active
1603
- }));
1604
- const filtered = filter.kind ? result.filter((row) => row.kind === filter.kind) : result;
1605
- filtered.sort((a, b) => b.cost_usd - a.cost_usd);
1606
- return filtered;
1607
- }
1608
- function queryProjectBreakdownSince(db, since, machine) {
1609
- return queryProjectBreakdownScoped(db, "timestamp >= ?", "started_at >= ?", [since], [since], machine);
1610
- }
1611
- function queryAgentBreakdownSince(db, since) {
1612
- return db.prepare(`
1613
- SELECT agent,
1614
- COUNT(DISTINCT session_id) as sessions,
1615
- COUNT(*) as requests,
1616
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1617
- COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
1618
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
1619
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1620
- MAX(timestamp) as last_active
1621
- FROM requests
1622
- WHERE timestamp >= ?
1623
- GROUP BY agent
1624
- ORDER BY api_equivalent_usd DESC
1625
- `).all(since);
1626
- }
1627
- function queryModelBreakdownSince(db, since) {
1628
- return db.prepare(`
1629
- SELECT model, agent,
1630
- COUNT(*) as requests,
1631
- COALESCE(SUM(input_tokens), 0) as input_tokens,
1632
- COALESCE(SUM(output_tokens), 0) as output_tokens,
1633
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1634
- COALESCE(SUM(cost_usd), 0) as cost_usd
1635
- FROM requests WHERE timestamp >= ?
1636
- GROUP BY model, agent ORDER BY cost_usd DESC
1637
- `).all(since);
1638
- }
1639
- function queryAccountBreakdownSince(db, since) {
1640
- return db.prepare(`
1641
- WITH request_rows AS (
1642
- SELECT
1643
- r.session_id as session_id,
1644
- COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
1645
- COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as raw_account_key,
1646
- COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as raw_account_name,
1647
- LOWER(TRIM(COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''))) as raw_account_email,
1648
- COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
1649
- 1 as requests,
1650
- COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1651
- COALESCE(r.cost_usd, 0) as cost_usd,
1652
- COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1653
- r.timestamp as last_active
1654
- FROM requests r
1655
- LEFT JOIN sessions s ON s.id = r.session_id
1656
- WHERE r.timestamp >= ?
1657
- AND (
1658
- COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
1659
- OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
1660
- OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
1661
- OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
1662
- )
1663
- ),
1664
- session_only_rows AS (
1665
- SELECT
1666
- s.id as session_id,
1667
- COALESCE(NULLIF(s.agent, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
1668
- s.account_key as raw_account_key,
1669
- s.account_name as raw_account_name,
1670
- LOWER(TRIM(COALESCE(s.account_email, ''))) as raw_account_email,
1671
- COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
1672
- COALESCE(s.request_count, 0) as requests,
1673
- COALESCE(s.total_tokens, 0) as total_tokens,
1674
- COALESCE(s.total_cost_usd, 0) as cost_usd,
1675
- 'estimated' as cost_basis,
1676
- s.started_at as last_active
1677
- FROM sessions s
1678
- WHERE s.started_at >= ?
1679
- AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1680
- AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
1681
- ),
1682
- normalized AS (
1683
- SELECT
1684
- CASE
1685
- WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
1686
- WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
1687
- ELSE raw_account_key
1688
- END as account_key,
1689
- account_agent as account_tool,
1690
- raw_account_name as account_name,
1691
- raw_account_email as account_email,
1692
- account_source,
1693
- session_id,
1694
- requests,
1695
- total_tokens,
1696
- cost_usd,
1697
- cost_basis,
1698
- last_active
1699
- FROM request_rows
1700
- UNION ALL
1701
- SELECT
1702
- CASE
1703
- WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
1704
- WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
1705
- ELSE raw_account_key
1706
- END as account_key,
1707
- account_agent as account_tool,
1708
- raw_account_name as account_name,
1709
- raw_account_email as account_email,
1710
- account_source,
1711
- session_id,
1712
- requests,
1713
- total_tokens,
1714
- cost_usd,
1715
- cost_basis,
1716
- last_active
1717
- FROM session_only_rows
1718
- )
1719
- SELECT account_key,
1720
- account_tool,
1721
- COALESCE(MAX(NULLIF(account_name, '')), MAX(NULLIF(account_email, '')), account_key) as account_name,
1722
- NULLIF(account_email, '') as account_email,
1723
- COALESCE(MAX(NULLIF(account_source, 'unknown')), 'unknown') as account_source,
1724
- COUNT(DISTINCT session_id) as sessions,
1725
- COALESCE(SUM(requests), 0) as requests,
1726
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1727
- COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
1728
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
1729
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1730
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1731
- COALESCE(SUM(CASE WHEN cost_basis NOT IN ('metered_api', 'subscription_included', 'unknown') THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1732
- COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
1733
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1734
- MAX(last_active) as last_active
1735
- FROM normalized
1736
- WHERE account_key != ''
1737
- GROUP BY account_key, account_tool, account_email
1738
- ORDER BY api_equivalent_usd DESC
1739
- `).all(since, since);
1740
- }
1741
- function queryDailyBreakdown(db, days = 30, machine) {
1742
- const machineClause = machine ? " AND machine_id = ?" : "";
1743
- const params = machine ? [`-${days}`, machine] : [`-${days}`];
1744
- return db.prepare(`
1745
- SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
1746
- FROM requests
1747
- WHERE timestamp >= DATE('now', ? || ' days')${machineClause}
1748
- GROUP BY DATE(timestamp), agent
1749
- ORDER BY date ASC
1750
- `).all(...params);
1751
- }
1752
- function upsertProject(db, project) {
1753
- db.prepare(`
1754
- INSERT OR REPLACE INTO projects (id, path, name, description, tags, created_at)
1755
- VALUES (?, ?, ?, ?, ?, ?)
1756
- `).run(project.id, project.path, project.name, project.description ?? null, JSON.stringify(project.tags), project.created_at);
1757
- }
1758
- function getProject(db, path) {
1759
- const row = db.prepare(`SELECT * FROM projects WHERE path = ?`).get(path);
1760
- if (!row)
1761
- return null;
1762
- return { ...row, tags: JSON.parse(row["tags"] ?? "[]") };
1763
- }
1764
- function deleteProject(db, path) {
1765
- db.prepare(`DELETE FROM projects WHERE path = ?`).run(path);
1766
- }
1767
- function upsertBudget(db, budget) {
1768
- db.prepare(`
1769
- INSERT OR REPLACE INTO budgets
1770
- (id, project_path, agent, cost_center_id, period, limit_usd, alert_at_percent, created_at, updated_at)
1771
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1772
- `).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);
1773
- }
1774
- function listBudgets(db) {
1775
- return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
1776
- }
1777
- function deleteBudget(db, id) {
1778
- db.prepare(`DELETE FROM budgets WHERE id = ?`).run(id);
1779
- }
1780
- function getBudgetStatuses(db) {
1781
- const budgets = listBudgets(db);
1782
- return budgets.map((b) => {
1783
- const periodStart = b.period === "daily" ? "DATE('now')" : b.period === "weekly" ? "DATE('now', '-7 days')" : "DATE('now', '-30 days')";
1784
- let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
1785
- const params = [];
1786
- if (b.project_path) {
1787
- spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
1788
- params.push(b.project_path);
1789
- }
1790
- if (b.agent) {
1791
- spendQuery += ` AND agent = ?`;
1792
- params.push(b.agent);
1793
- }
1794
- if (b.cost_center_id) {
1795
- spendQuery += ` AND COALESCE(NULLIF(cost_center_id, ''), (SELECT NULLIF(s.cost_center_id, '') FROM sessions s WHERE s.id = requests.session_id)) = ?`;
1796
- params.push(b.cost_center_id);
1797
- }
1798
- const row = db.prepare(spendQuery).get(...params);
1799
- const spend = row.spend;
1800
- const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
1801
- return {
1802
- ...b,
1803
- current_spend_usd: spend,
1804
- percent_used: percent,
1805
- is_over_limit: percent >= 100,
1806
- is_over_alert: percent >= b.alert_at_percent
1807
- };
1808
- });
1809
- }
1810
- function upsertGoal(db, goal) {
1811
- db.prepare(`
1812
- INSERT OR REPLACE INTO goals
1813
- (id, period, project_path, agent, limit_usd, created_at, updated_at)
1814
- VALUES (?, ?, ?, ?, ?, ?, ?)
1815
- `).run(goal.id, goal.period, goal.project_path ?? null, goal.agent ?? null, goal.limit_usd, goal.created_at, goal.updated_at);
1816
- }
1817
- function deleteGoal(db, id) {
1818
- db.prepare(`DELETE FROM goals WHERE id = ?`).run(id);
1819
- }
1820
- function listGoals(db) {
1821
- return db.prepare(`SELECT * FROM goals ORDER BY created_at DESC`).all();
1822
- }
1823
- function getGoalStatuses(db) {
1824
- const goals = listGoals(db);
1825
- return goals.map((g) => {
1826
- 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')";
1827
- let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
1828
- const params = [];
1829
- if (g.project_path) {
1830
- spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
1831
- params.push(g.project_path);
1832
- }
1833
- if (g.agent) {
1834
- spendQuery += ` AND agent = ?`;
1835
- params.push(g.agent);
1836
- }
1837
- const row = db.prepare(spendQuery).get(...params);
1838
- const spend = row.spend;
1839
- const percent = g.limit_usd > 0 ? spend / g.limit_usd * 100 : 0;
1840
- return {
1841
- ...g,
1842
- current_spend_usd: spend,
1843
- percent_used: percent,
1844
- is_on_track: percent < 70,
1845
- is_at_risk: percent >= 70 && percent <= 100,
1846
- is_over: percent > 100
1847
- };
1848
- });
1849
- }
1850
- function queryRequestsSince(db, since) {
1851
- return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
1852
- }
1853
- function insertFeedback(db, record) {
1854
- 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);
1855
- }
1856
- function billingPeriodWhere(period) {
1857
- 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";
1858
- }
1859
- function queryBillingSummary(db, period) {
1860
- const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${billingPeriodWhere(period)} GROUP BY provider`).all();
1861
- const by_provider = {};
1862
- let total = 0;
1863
- for (const r of rows) {
1864
- by_provider[r.provider] = r.cost;
1865
- total += r.cost;
1866
- }
1867
- return { total_usd: total, by_provider };
1868
- }
1869
- function countBillingRecords(db, period) {
1870
- const row = db.prepare(`SELECT COUNT(*) as c FROM billing_daily WHERE ${billingPeriodWhere(period)}`).get();
1871
- return row?.c ?? 0;
1872
- }
1873
- function getSessionDetail(db, id) {
1874
- const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `${id}%`);
1875
- if (!session)
1876
- return null;
1877
- const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC`).all(session["id"]);
1878
- return { session, requests };
1879
- }
1880
- function listMachines(db, period = "all") {
1881
- const rWhere = requestPeriodWhere(period);
1882
- const sWhere = sessionPeriodWhere(period);
1883
- return db.prepare(`
1884
- WITH request_stats AS (
1885
- SELECT
1886
- machine_id,
1887
- COUNT(DISTINCT session_id) as sessions,
1888
- COUNT(*) as requests,
1889
- COALESCE(SUM(cost_usd), 0) as total_cost_usd,
1890
- MAX(timestamp) as last_active
1891
- FROM requests
1892
- WHERE machine_id != ''
1893
- AND ${rWhere}
1894
- GROUP BY machine_id
1895
- ),
1896
- session_only_stats AS (
1897
- SELECT
1898
- machine_id,
1899
- COUNT(*) as sessions,
1900
- COALESCE(SUM(request_count), 0) as requests,
1901
- COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
1902
- MAX(started_at) as last_active
1903
- FROM sessions
1904
- WHERE machine_id != ''
1905
- AND ${sWhere}
1906
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1907
- GROUP BY machine_id
1908
- ),
1909
- combined AS (
1910
- SELECT * FROM request_stats
1911
- UNION ALL
1912
- SELECT * FROM session_only_stats
1913
- )
1914
- SELECT
1915
- machine_id,
1916
- COALESCE(SUM(sessions), 0) as sessions,
1917
- COALESCE(SUM(requests), 0) as requests,
1918
- COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
1919
- MAX(last_active) as last_active
1920
- FROM combined
1921
- GROUP BY machine_id
1922
- ORDER BY total_cost_usd DESC
1923
- `).all();
1924
- }
1925
- function upsertModelPricing(db, p) {
1926
- db.prepare(`
1927
- INSERT OR REPLACE INTO model_pricing
1928
- (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)
1929
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1930
- `).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);
1931
- }
1932
- function getModelPricing(db, model) {
1933
- return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
1934
- }
1935
- function listModelPricing(db) {
1936
- return db.prepare(`SELECT * FROM model_pricing ORDER BY model ASC`).all();
1937
- }
1938
- function deleteModelPricing(db, model) {
1939
- db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
1940
- }
1941
- function seedModelPricing(db, defaults) {
1942
- const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
1943
- const now = new Date().toISOString();
1944
- for (const [model, p] of Object.entries(defaults)) {
1945
- if (existing.has(model))
1946
- continue;
1947
- upsertModelPricing(db, {
1948
- model,
1949
- input_per_1m: p.inputPer1M,
1950
- output_per_1m: p.outputPer1M,
1951
- cache_read_per_1m: p.cacheReadPer1M,
1952
- cache_write_per_1m: p.cacheWritePer1M,
1953
- cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
1954
- cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
1955
- updated_at: now
1956
- });
1957
- }
1958
- }
1959
- function upsertSubscription(db, sub) {
1960
- db.prepare(`
1961
- INSERT OR REPLACE INTO subscriptions
1962
- (id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
1963
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1964
- `).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);
1965
- }
1966
- function listSubscriptions(db) {
1967
- return db.prepare(`SELECT * FROM subscriptions ORDER BY provider, plan`).all();
1968
- }
1969
- function deleteSubscription(db, id) {
1970
- db.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(id);
1971
- }
1972
- function queryUsageSnapshots(db, opts = {}) {
1973
- const conditions = [];
1974
- const params = [];
1975
- if (opts.agent) {
1976
- conditions.push("agent = ?");
1977
- params.push(opts.agent);
1978
- }
1979
- if (opts.date) {
1980
- conditions.push("date = ?");
1981
- params.push(opts.date);
1982
- }
1983
- if (opts.since) {
1984
- conditions.push("date >= ?");
1985
- params.push(opts.since);
1986
- }
1987
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
1988
- return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
1989
- }
1990
- function listMachineRegistry(db) {
1991
- return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
1992
- }
1993
- var init_database = __esm(() => {
1994
- init_sqlite_adapter();
1995
- });
1996
-
1997
- // src/lib/agents.ts
1998
- var AGENTS = [
1999
- "claude",
2000
- "takumi",
2001
- "codex",
2002
- "gemini",
2003
- "opencode",
2004
- "cursor",
2005
- "pi",
2006
- "hermes"
2007
- ];
2008
- var COST_BASIS = [
2009
- "metered_api",
2010
- "subscription_included",
2011
- "estimated",
2012
- "unknown"
2013
- ];
2014
- function isAgent(value) {
2015
- return AGENTS.includes(value);
2016
- }
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";
2017
59
  // src/lib/store/index.ts
2018
- init_database();
2019
- init_pricing();
2020
60
  import { randomUUID } from "crypto";
2021
61
  // package.json
2022
62
  var package_default = {
2023
63
  name: "@hasna/economy",
2024
- version: "0.3.28",
2025
- description: "AI coding cost tracker \u2014 CLI + MCP server + REST API + web dashboard for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
64
+ version: "0.5.0",
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",
2026
66
  repository: {
2027
67
  type: "git",
2028
- url: "git+https://github.com/hasna/economy.git"
68
+ url: "https://github.com/hasna/apps.git",
69
+ directory: "apps/economy"
2029
70
  },
2030
71
  bugs: {
2031
- url: "https://github.com/hasna/economy/issues"
72
+ url: "https://github.com/hasna/apps/issues"
2032
73
  },
2033
- homepage: "https://github.com/hasna/economy#readme",
74
+ homepage: "https://github.com/hasna/apps/tree/main/apps/economy#readme",
2034
75
  type: "module",
2035
76
  main: "dist/index.js",
2036
77
  types: "dist/index.d.ts",
@@ -2053,22 +94,21 @@ var package_default = {
2053
94
  files: [
2054
95
  "dist",
2055
96
  "docs",
2056
- "dashboard/dist",
2057
97
  "postinstall.js",
2058
98
  "LICENSE",
2059
99
  "README.md",
2060
100
  "CHANGELOG.md",
2061
101
  "SECURITY.md",
2062
102
  "CONTRIBUTING.md",
2063
- "CODE_OF_CONDUCT.md"
103
+ "CODE_OF_CONDUCT.md",
104
+ "!dist/lib/test-hermetic-*"
2064
105
  ],
2065
106
  scripts: {
2066
- build: "cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.ts --outdir dist/cli --target bun --packages external && bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/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",
2067
- "build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
2068
- "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
2069
- "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
2070
- "build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
2071
- "build:dashboard": "cd dashboard && bun run build",
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]"',
2072
112
  "artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
2073
113
  prepack: "bun run build && bun run artifact-scan",
2074
114
  typecheck: "tsc --noEmit",
@@ -2098,21 +138,18 @@ var package_default = {
2098
138
  access: "public"
2099
139
  },
2100
140
  dependencies: {
2101
- "@hasna/accounts": "0.2.23",
2102
- "@hasna/agent-registry": "^0.1.0",
2103
- "@hasna/contracts": "0.13.4",
141
+ "@hasna/contracts": "1.0.2",
2104
142
  "@hasna/events": "^0.1.16",
2105
- "@hasna/mcp-harness": "^0.1.0",
2106
- "@hasna/paths": "0.1.0",
2107
143
  "@hasna/projects": "1.0.0",
2108
144
  "@modelcontextprotocol/sdk": "^1.12.1",
2109
145
  chalk: "^5.4.1",
2110
146
  commander: "^13.1.0",
2111
147
  pg: "8.13.1",
2112
- zod: "^3.24.2"
148
+ zod: "^3.24.2",
149
+ "@hasna/trash": "0.2.3"
2113
150
  },
2114
151
  devDependencies: {
2115
- "@types/bun": "latest",
152
+ "@types/bun": "1.3.14",
2116
153
  "@types/pg": "^8.20.0",
2117
154
  "bun-types": "latest",
2118
155
  typescript: "^5.7.2"
@@ -2136,254 +173,277 @@ function getPackageMetadata() {
2136
173
  var packageMetadata = getPackageMetadata();
2137
174
 
2138
175
  // src/lib/savings.ts
2139
- function periodWhere2(period, column) {
2140
- switch (period) {
2141
- case "today":
2142
- return `DATE(${column}) = DATE('now')`;
2143
- case "yesterday":
2144
- return `DATE(${column}) = DATE('now', '-1 day')`;
2145
- case "week":
2146
- return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
2147
- case "month":
2148
- return `${column} >= DATE('now', 'start of month')`;
2149
- case "year":
2150
- return `${column} >= DATE('now', 'start of year')`;
2151
- case "all":
2152
- return "1=1";
2153
- }
2154
- }
2155
- function prorateMonthlyFee(monthlyFee, period) {
2156
- const now = new Date;
2157
- const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
2158
- switch (period) {
2159
- case "today":
2160
- case "yesterday":
2161
- return monthlyFee / daysInMonth;
2162
- case "week":
2163
- return monthlyFee / daysInMonth * 7;
2164
- case "month":
2165
- return monthlyFee;
2166
- case "year":
2167
- return monthlyFee * 12;
2168
- case "all":
2169
- return monthlyFee;
2170
- }
2171
- }
2172
- function proratedIncludedConsumed(includedUsage, includedCap, period) {
2173
- const cap = prorateMonthlyFee(includedCap, period);
2174
- return cap > 0 ? Math.min(includedUsage, cap) : includedUsage;
2175
- }
2176
- function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
2177
- 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;
2178
193
  }
2179
- function querySavingsSummary(db, period, agent) {
2180
- const where = periodWhere2(period, "timestamp");
2181
- const agentClause = agent ? " AND agent = ?" : "";
2182
- const params = agent ? [agent] : [];
2183
- const apiRow = db.prepare(`
2184
- SELECT COALESCE(SUM(cost_usd), 0) as total
2185
- FROM requests
2186
- WHERE ${where}${agentClause}
2187
- AND COALESCE(cost_basis, 'estimated') IN ('metered_api', 'estimated', 'unknown')
2188
- `).get(...params);
2189
- const includedRow = db.prepare(`
2190
- SELECT COALESCE(SUM(cost_usd), 0) as total
2191
- FROM requests
2192
- WHERE ${where}${agentClause}
2193
- AND cost_basis = 'subscription_included'
2194
- `).get(...params);
2195
- const subWhere = periodWhere2(period, "date");
2196
- const onDemandRow = db.prepare(`
2197
- SELECT COALESCE(SUM(value), 0) as total
2198
- FROM usage_snapshots
2199
- WHERE ${subWhere}${agent ? " AND agent = ?" : ""}
2200
- AND metric = 'on_demand_usd'
2201
- `).get(...params);
2202
- const subs = db.prepare(`
2203
- SELECT
2204
- COALESCE(SUM(monthly_fee_usd), 0) as fee,
2205
- COALESCE(SUM(included_usage_usd), 0) as included
2206
- FROM subscriptions
2207
- WHERE active = 1${agent ? " AND (agent = ? OR agent IS NULL)" : ""}
2208
- `).get(...agent ? [agent] : []);
2209
- const subscriptionFee = prorateMonthlyFee(subs.fee, period);
2210
- const apiEquivalent = apiRow.total + includedRow.total;
2211
- const includedConsumed = proratedIncludedConsumed(includedRow.total, subs.included, period);
2212
- const onDemand = onDemandRow.total;
2213
- const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
2214
- const byAgent = {};
2215
- if (!agent) {
2216
- const onDemandByAgent = new Map;
2217
- for (const row of db.prepare(`
2218
- SELECT agent, COALESCE(SUM(value), 0) as total
2219
- FROM usage_snapshots
2220
- WHERE ${subWhere}
2221
- AND metric = 'on_demand_usd'
2222
- GROUP BY agent
2223
- `).all()) {
2224
- 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;
2225
220
  }
2226
- const subscriptionByAgent = new Map;
2227
- for (const row of db.prepare(`
2228
- SELECT agent,
2229
- COALESCE(SUM(monthly_fee_usd), 0) as fee,
2230
- COALESCE(SUM(included_usage_usd), 0) as included
2231
- FROM subscriptions
2232
- WHERE active = 1 AND agent IS NOT NULL
2233
- GROUP BY agent
2234
- `).all()) {
2235
- 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" };
2236
243
  }
2237
- const globalSubs = db.prepare(`
2238
- SELECT
2239
- COALESCE(SUM(monthly_fee_usd), 0) as fee,
2240
- COALESCE(SUM(included_usage_usd), 0) as included
2241
- FROM subscriptions
2242
- WHERE active = 1 AND agent IS NULL
2243
- `).get();
2244
- const rows = db.prepare(`
2245
- SELECT agent,
2246
- COALESCE(SUM(cost_usd), 0) as api_eq,
2247
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as included
2248
- FROM requests WHERE ${where}
2249
- GROUP BY agent
2250
- `).all();
2251
- const totalAgentApiEq = rows.reduce((sum, row) => sum + row.api_eq, 0);
2252
- for (const row of db.prepare(`
2253
- SELECT agent,
2254
- COALESCE(SUM(cost_usd), 0) as api_eq,
2255
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as included
2256
- FROM requests WHERE ${where}
2257
- GROUP BY agent
2258
- `).all()) {
2259
- const agentSubs = subscriptionByAgent.get(row.agent) ?? { fee: 0, included: 0 };
2260
- const globalShare = totalAgentApiEq > 0 ? row.api_eq / totalAgentApiEq : 0;
2261
- const agentFee = prorateMonthlyFee(agentSubs.fee + globalSubs.fee * globalShare, period);
2262
- const agentIncludedCap = agentSubs.included + globalSubs.included * globalShare;
2263
- const agentIncludedConsumed = proratedIncludedConsumed(row.included, agentIncludedCap, period);
2264
- const agentOnDemand = onDemandByAgent.get(row.agent) ?? 0;
2265
- byAgent[row.agent] = {
2266
- api_equivalent_usd: row.api_eq,
2267
- subscription_fee_usd: agentFee,
2268
- included_consumed_usd: agentIncludedConsumed,
2269
- on_demand_usd: agentOnDemand,
2270
- saved_usd: computeSavedUsd(row.api_eq, agentOnDemand, agentFee)
2271
- };
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;
2272
325
  }
2273
326
  }
2274
327
  return {
2275
328
  period,
2276
- api_equivalent_usd: apiEquivalent,
2277
- subscription_fee_usd: subscriptionFee,
2278
- included_consumed_usd: includedConsumed,
2279
- on_demand_usd: onDemand,
2280
- 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,
2281
347
  by_agent: byAgent
2282
348
  };
2283
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
+ }
2284
384
 
2285
385
  // src/lib/billing-diff.ts
2286
- init_database();
2287
- var PROVIDER_TO_AGENT = {
2288
- anthropic: "claude",
2289
- openai: "codex",
2290
- gemini: "gemini",
2291
- google: "gemini"
2292
- };
2293
- function billingDeltaPct(estimatedUsd, actualUsd) {
2294
- if (!(actualUsd > 0))
2295
- return null;
2296
- return (estimatedUsd - actualUsd) / actualUsd * 100;
2297
- }
2298
386
  function queryBillingDiff(db, period, thresholdPct = 15) {
2299
- const estimated = querySummary(db, period, undefined, true);
2300
- const actual = queryBillingSummary(db, period);
2301
- const billingRecords = countBillingRecords(db, period);
2302
- const delta = estimated.total_usd - actual.total_usd;
2303
- const measuredPct = billingDeltaPct(estimated.total_usd, actual.total_usd);
2304
- const incomparableReason = measuredPct !== null ? null : billingRecords === 0 ? "no_billing_records" : "zero_actual_billing";
2305
- const comparable = incomparableReason === null;
2306
- const deltaPct = measuredPct ?? 0;
2307
- const agentRows = db.prepare(`
2308
- SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
2309
- FROM requests
2310
- WHERE ${periodWhere3(period, "timestamp")}
2311
- 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
2312
395
  `).all();
2313
- const by_agent = agentRows.map((row) => {
2314
- const provider = Object.entries(PROVIDER_TO_AGENT).find(([, a]) => a === row.agent)?.[0];
2315
- const actualUsd = provider ? actual.by_provider[provider] ?? 0 : 0;
2316
- const rowDelta = row.estimated_usd - actualUsd;
2317
- const rowPct = actualUsd > 0 ? rowDelta / actualUsd * 100 : 0;
2318
- return {
2319
- agent: row.agent,
2320
- estimated_usd: row.estimated_usd,
2321
- actual_usd: actualUsd,
2322
- delta_usd: rowDelta,
2323
- delta_pct: rowPct
2324
- };
2325
- }).sort((a, b) => Math.abs(b.delta_usd) - Math.abs(a.delta_usd));
2326
- return {
396
+ return requireCurrentBillingDiffSummary({
2327
397
  period,
2328
- estimated_usd: estimated.total_usd,
2329
- actual_usd: actual.total_usd,
2330
- delta_usd: delta,
2331
- 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,
2332
405
  threshold_pct: thresholdPct,
2333
- is_alert: comparable && Math.abs(deltaPct) > thresholdPct,
2334
- comparable,
2335
- incomparable_reason: incomparableReason,
2336
- by_agent,
2337
- by_provider: actual.by_provider
2338
- };
2339
- }
2340
- function periodWhere3(period, column) {
2341
- switch (period) {
2342
- case "today":
2343
- return `DATE(${column}) = DATE('now')`;
2344
- case "yesterday":
2345
- return `DATE(${column}) = DATE('now', '-1 day')`;
2346
- case "week":
2347
- return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
2348
- case "month":
2349
- return `${column} >= DATE('now', 'start of month')`;
2350
- case "year":
2351
- return `${column} >= DATE('now', 'start of year')`;
2352
- case "all":
2353
- return "1=1";
2354
- }
2355
- }
2356
-
2357
- // src/lib/periods.ts
2358
- function ymd(date) {
2359
- 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
+ });
2360
412
  }
2361
- function usageSnapshotFilterForPeriod(period) {
2362
- const now = new Date;
2363
- switch (period) {
2364
- case "today":
2365
- return { date: ymd(now) };
2366
- case "yesterday": {
2367
- const yesterday = new Date(now);
2368
- yesterday.setUTCDate(yesterday.getUTCDate() - 1);
2369
- return { date: ymd(yesterday) };
2370
- }
2371
- case "week": {
2372
- const weekAgo = new Date(now);
2373
- weekAgo.setUTCDate(weekAgo.getUTCDate() - 7);
2374
- return { since: ymd(weekAgo) };
2375
- }
2376
- case "month":
2377
- return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1))) };
2378
- case "year":
2379
- return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), 0, 1))) };
2380
- case "all":
2381
- return {};
2382
- }
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;
2383
444
  }
2384
445
 
2385
446
  // src/lib/brief.ts
2386
- init_database();
2387
447
  var ZERO_REQUEST_TOTALS = {
2388
448
  sessions: 0,
2389
449
  request_sessions: 0,
@@ -2427,15 +487,11 @@ function machineFilter(machine) {
2427
487
  return;
2428
488
  return machine;
2429
489
  }
2430
- function requestPeriodWhere2(period) {
2431
- if (period === "today")
2432
- return `DATE(timestamp) = DATE('now')`;
2433
- return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
490
+ function requestPeriodWhere(period) {
491
+ return calendarPeriodWhere(period, "timestamp");
2434
492
  }
2435
- function sessionPeriodWhere2(period) {
2436
- if (period === "today")
2437
- return `DATE(started_at) = DATE('now')`;
2438
- return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
493
+ function sessionPeriodWhere(period) {
494
+ return calendarPeriodWhere(period, "started_at");
2439
495
  }
2440
496
  function baseRequestTotals(row) {
2441
497
  return {
@@ -2506,7 +562,7 @@ function requestTotalsSql(where, machine) {
2506
562
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
2507
563
  COALESCE(SUM(cost_usd), 0) as cost_usd,
2508
564
  MAX(timestamp) as last_active
2509
- FROM requests
565
+ FROM economy_effective_requests
2510
566
  WHERE ${where}${machineClause}
2511
567
  `,
2512
568
  params
@@ -2527,9 +583,9 @@ function sessionOnlyTotalsSql(where, machine) {
2527
583
  COALESCE(SUM(total_tokens), 0) as total_tokens,
2528
584
  COALESCE(SUM(total_cost_usd), 0) as cost_usd,
2529
585
  MAX(started_at) as last_active
2530
- FROM sessions
586
+ FROM economy_effective_sessions
2531
587
  WHERE ${where}${machineClause}
2532
- 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)
2533
589
  `,
2534
590
  params
2535
591
  };
@@ -2544,15 +600,16 @@ function querySessionOnlyTotals(db, where, params, machine) {
2544
600
  }
2545
601
  function queryPeriodTotals(db, period, machine) {
2546
602
  const summary = querySummary(db, period, machine);
2547
- const requests = queryRequestTotals(db, requestPeriodWhere2(period), [], machine);
2548
- const sessions = querySessionOnlyTotals(db, sessionPeriodWhere2(period), [], machine);
603
+ const requests = queryRequestTotals(db, requestPeriodWhere(period), [], machine);
604
+ const sessions = querySessionOnlyTotals(db, sessionPeriodWhere(period), [], machine);
2549
605
  const totals = combineTotals(requests, sessions);
2550
606
  return {
2551
607
  ...totals,
2552
608
  sessions: summary.sessions,
2553
609
  requests: summary.requests,
2554
610
  total_tokens: summary.tokens,
2555
- cost_usd: summary.total_usd
611
+ cost_usd: summary.total_usd,
612
+ cost_summary: summary
2556
613
  };
2557
614
  }
2558
615
  function querySinceTotals(db, since, machine) {
@@ -2605,7 +662,7 @@ function queryMachineSinceRows(db, since, machine) {
2605
662
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
2606
663
  COALESCE(SUM(cost_usd), 0) as cost_usd,
2607
664
  MAX(timestamp) as last_active
2608
- FROM requests
665
+ FROM economy_effective_requests
2609
666
  WHERE timestamp >= ?
2610
667
  AND machine_id != ''
2611
668
  ${requestMachineClause}
@@ -2620,11 +677,11 @@ function queryMachineSinceRows(db, since, machine) {
2620
677
  COALESCE(SUM(total_tokens), 0) as total_tokens,
2621
678
  COALESCE(SUM(total_cost_usd), 0) as cost_usd,
2622
679
  MAX(started_at) as last_active
2623
- FROM sessions
680
+ FROM economy_effective_sessions
2624
681
  WHERE started_at >= ?
2625
682
  AND machine_id != ''
2626
683
  ${sessionMachineClause}
2627
- 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)
2628
685
  GROUP BY machine_id
2629
686
  `).all(...machine ? [since, machine] : [since]);
2630
687
  const groups = new Map;
@@ -2662,13 +719,13 @@ function queryMachineLastData(db, machine) {
2662
719
  const filter = machine ? " AND machine_id = ?" : "";
2663
720
  const requestRows = db.prepare(`
2664
721
  SELECT machine_id, MAX(timestamp) as last_at
2665
- FROM requests
722
+ FROM economy_effective_requests
2666
723
  WHERE machine_id != ''${filter}
2667
724
  GROUP BY machine_id
2668
725
  `).all(...machine ? [machine] : []);
2669
726
  const sessionRows = db.prepare(`
2670
727
  SELECT machine_id, MAX(started_at) as last_at
2671
- FROM sessions
728
+ FROM economy_effective_sessions
2672
729
  WHERE machine_id != ''${filter}
2673
730
  GROUP BY machine_id
2674
731
  `).all(...machine ? [machine] : []);
@@ -2696,7 +753,7 @@ function queryAgentSinceRows(db, since, machine) {
2696
753
  input_tokens + output_tokens + cache_read_tokens + cache_create_tokens as total_tokens,
2697
754
  cost_usd,
2698
755
  timestamp as last_active
2699
- FROM requests
756
+ FROM economy_effective_requests
2700
757
  WHERE timestamp >= ?${requestMachineClause}
2701
758
  ),
2702
759
  session_only_rows AS (
@@ -2713,9 +770,9 @@ function queryAgentSinceRows(db, since, machine) {
2713
770
  COALESCE(total_tokens, 0) as total_tokens,
2714
771
  COALESCE(total_cost_usd, 0) as cost_usd,
2715
772
  started_at as last_active
2716
- FROM sessions
773
+ FROM economy_effective_sessions
2717
774
  WHERE started_at >= ?${sessionMachineClause}
2718
- 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)
2719
776
  ),
2720
777
  combined AS (
2721
778
  SELECT * FROM request_rows
@@ -2759,12 +816,12 @@ function agentBreakdownToBriefRow(row) {
2759
816
  last_active: row.last_active
2760
817
  };
2761
818
  }
2762
- function normalizeAccountEmail2(email) {
819
+ function normalizeAccountEmail(email) {
2763
820
  return email.trim().toLowerCase();
2764
821
  }
2765
822
  function accountKey(row) {
2766
823
  const agent = row.agent || row.account_tool || "unknown";
2767
- const email = normalizeAccountEmail2(row.account_email);
824
+ const email = normalizeAccountEmail(row.account_email);
2768
825
  if (email)
2769
826
  return `${agent}:${email}`;
2770
827
  if (row.account_name)
@@ -2775,7 +832,7 @@ function addAccountRow(groups, row) {
2775
832
  if (!row.account_key && !row.account_tool && !row.account_name && !row.account_email)
2776
833
  return;
2777
834
  const key = accountKey(row);
2778
- const email = normalizeAccountEmail2(row.account_email);
835
+ const email = normalizeAccountEmail(row.account_email);
2779
836
  const current = groups.get(key) ?? {
2780
837
  account_key: key,
2781
838
  account_tool: row.agent || row.account_tool || "unknown",
@@ -2837,8 +894,8 @@ function queryAccountSinceRows(db, since, machine) {
2837
894
  COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
2838
895
  COALESCE(SUM(r.cost_usd), 0) as cost_usd,
2839
896
  MAX(r.timestamp) as last_active
2840
- FROM requests r
2841
- 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
2842
899
  WHERE r.timestamp >= ?${requestMachineClause}
2843
900
  GROUP BY
2844
901
  COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), ''),
@@ -2867,9 +924,9 @@ function queryAccountSinceRows(db, since, machine) {
2867
924
  COALESCE(SUM(s.total_tokens), 0) as total_tokens,
2868
925
  COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
2869
926
  MAX(s.started_at) as last_active
2870
- FROM sessions s
927
+ FROM economy_effective_sessions s
2871
928
  WHERE s.started_at >= ?${sessionMachineClause}
2872
- 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)
2873
930
  GROUP BY
2874
931
  s.agent,
2875
932
  s.account_key,
@@ -2913,7 +970,7 @@ function queryFreshness(db, now, machine, localSync) {
2913
970
  const requestFilter = machine ? " AND machine_id = ?" : "";
2914
971
  const requestRows = db.prepare(`
2915
972
  SELECT machine_id, MAX(timestamp) as max_request_at
2916
- FROM requests
973
+ FROM economy_effective_requests
2917
974
  WHERE machine_id != ''${requestFilter}
2918
975
  GROUP BY machine_id
2919
976
  `).all(...machine ? [machine] : []);
@@ -3007,7 +1064,7 @@ function buildBrief(db, opts = {}) {
3007
1064
  function queryProjectDetail(db, nameOrPath) {
3008
1065
  const like = `%${nameOrPath}%`;
3009
1066
  const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
3010
- FROM sessions
1067
+ FROM economy_effective_sessions
3011
1068
  WHERE project_name LIKE ? OR project_path LIKE ?
3012
1069
  ORDER BY started_at DESC`).all(like, like);
3013
1070
  if (sessions.length === 0)
@@ -3016,12 +1073,12 @@ function queryProjectDetail(db, nameOrPath) {
3016
1073
  const totalCost = sessions.reduce((s, r) => s + (r.total_cost_usd ?? 0), 0);
3017
1074
  const totalTokens = sessions.reduce((s, r) => s + (r.total_tokens ?? 0), 0);
3018
1075
  const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
3019
- 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
3020
1077
  WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
3021
1078
  AND r.timestamp >= DATE('now', '-14 days')
3022
1079
  GROUP BY date ORDER BY date ASC`).all(like, like);
3023
1080
  const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
3024
- 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
3025
1082
  WHERE s.project_name LIKE ? OR s.project_path LIKE ?
3026
1083
  GROUP BY r.model ORDER BY cost_usd DESC LIMIT 5`).all(like, like);
3027
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 }));
@@ -3037,45 +1094,39 @@ function queryProjectDetail(db, nameOrPath) {
3037
1094
  };
3038
1095
  }
3039
1096
  function exportDateWhere(column, period) {
3040
- switch (period) {
3041
- case "today":
3042
- return `DATE(${column}) = DATE('now')`;
3043
- case "week":
3044
- return `${column} >= DATE('now', '-7 days')`;
3045
- case "all":
3046
- return "1=1";
3047
- default:
3048
- return `${column} >= DATE('now', '-30 days')`;
1097
+ if (!["today", "yesterday", "week", "month", "year", "all"].includes(period)) {
1098
+ throw new RangeError("Invalid export period");
3049
1099
  }
1100
+ return calendarPeriodWhere(period, column);
3050
1101
  }
3051
1102
  function queryExportRows(db, type, period) {
3052
1103
  if (type === "requests") {
3053
1104
  const where2 = exportDateWhere("timestamp", period);
3054
- 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();
3055
1106
  }
3056
1107
  const where = exportDateWhere("started_at", period);
3057
- 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();
3058
1109
  }
3059
1110
  function queryRangeStats(db, from, to) {
3060
1111
  const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests,
3061
1112
  COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens
3062
- FROM requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
3063
- 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);
3064
1115
  return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
3065
1116
  }
3066
1117
  function queryForecast(db, now = new Date) {
3067
1118
  const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
3068
1119
  const dayOfMonth = now.getDate();
3069
1120
  const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
3070
- 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);
3071
1122
  const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
3072
1123
  const projected = dailyAvg * daysInMonth;
3073
1124
  const sevenDaysAgo = new Date(now);
3074
1125
  sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
3075
- 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));
3076
1127
  const last7DailyAvg = last7.cost / 7;
3077
1128
  const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
3078
- 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);
3079
1130
  return {
3080
1131
  day_of_month: dayOfMonth,
3081
1132
  days_in_month: daysInMonth,
@@ -3096,22 +1147,134 @@ function queryModelEfficiency(db) {
3096
1147
  COALESCE(SUM(cache_create_tokens),0) as cache_write,
3097
1148
  COUNT(*) as requests,
3098
1149
  COALESCE(SUM(cost_usd),0) as cost
3099
- FROM requests GROUP BY model ORDER BY cost DESC`).all();
1150
+ FROM economy_effective_requests GROUP BY model ORDER BY cost DESC`).all();
3100
1151
  }
3101
1152
 
3102
- // src/lib/contracts-client/storage.ts
3103
- import { resolveStorageClient } from "@hasna/contracts/client/storage";
3104
-
3105
1153
  // src/lib/cloud-storage.ts
1154
+ import {
1155
+ appConfigDiskValue,
1156
+ ClientTransportConfigurationError,
1157
+ clientTransportEnvKeys,
1158
+ createClientTransport,
1159
+ credentialDiskSources,
1160
+ credentialOverrideEnvKey,
1161
+ credentialPointerEnvKey,
1162
+ CREDENTIAL_PROFILE_ENV_KEY,
1163
+ keychainConfigValue
1164
+ } from "@hasna/contracts/client";
1165
+ import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
3106
1166
  var ECONOMY_APP = "economy";
1167
+ var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
1168
+ function envFlagSet(env, key) {
1169
+ const raw = env[key];
1170
+ if (raw === undefined)
1171
+ return false;
1172
+ const value = raw.trim().toLowerCase();
1173
+ return value !== "" && value !== "0" && value !== "false" && value !== "no" && value !== "off";
1174
+ }
1175
+ function localStorageExplicitlyOptedIn(env) {
1176
+ return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => envFlagSet(env, key));
1177
+ }
1178
+ function economyAuthorityEnvKeys() {
1179
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
1180
+ return [
1181
+ ...keys.apiUrlKeys,
1182
+ ...keys.apiKeyKeys,
1183
+ credentialOverrideEnvKey(ECONOMY_APP),
1184
+ credentialPointerEnvKey(ECONOMY_APP),
1185
+ CREDENTIAL_PROFILE_ENV_KEY
1186
+ ];
1187
+ }
1188
+ function hasEconomyEnvAuthorityIntent(env) {
1189
+ return economyAuthorityEnvKeys().some((key) => (env[key] ?? "").trim().length > 0);
1190
+ }
1191
+ function hostedAuthorityConfigured(env, options = {}) {
1192
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
1193
+ if (keys.apiUrlKeys.some((key) => (env[key] ?? "").trim().length > 0))
1194
+ return true;
1195
+ if (keychainConfigValue(ECONOMY_APP, env, options.credentials?.keychain) !== null)
1196
+ return true;
1197
+ return appConfigDiskValue(ECONOMY_APP, env, keys.apiUrlKeys) !== null;
1198
+ }
1199
+ function economyResolverEnv(env) {
1200
+ const blanks = economyAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
1201
+ if (blanks.length === 0)
1202
+ return env;
1203
+ const next = { ...env };
1204
+ for (const key of blanks)
1205
+ delete next[key];
1206
+ return next;
1207
+ }
1208
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
1209
+ function isAmbientEconomyEnv(env) {
1210
+ if (typeof process !== "undefined" && env === process.env)
1211
+ return true;
1212
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
1213
+ }
1214
+ function economyResolverInputs(env, credentials = {}) {
1215
+ const normalised = economyResolverEnv(env);
1216
+ if (normalised === env)
1217
+ return { env: normalised, credentials };
1218
+ const keychain = { ...credentials.keychain };
1219
+ if (keychain.enabled === undefined && keychain.run === undefined) {
1220
+ keychain.enabled = isAmbientEconomyEnv(env);
1221
+ }
1222
+ return { env: normalised, credentials: { ...credentials, keychain } };
1223
+ }
1224
+ function resolveEconomyStorageClient(name, env, overrides = {}) {
1225
+ const { env: resolverEnv, credentials } = economyResolverInputs(env, overrides.credentials);
1226
+ const wired = createClientTransport(name, resolverEnv, {
1227
+ credentials,
1228
+ ...overrides.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
1229
+ ...overrides.headers ? { headers: overrides.headers } : {},
1230
+ ...overrides.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
1231
+ ...overrides.retry !== undefined ? { retry: overrides.retry } : {},
1232
+ ...overrides.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
1233
+ });
1234
+ return {
1235
+ transport: "http",
1236
+ client: createHasnaStorageClient(name, wired.client),
1237
+ resolution: wired.resolution
1238
+ };
1239
+ }
1240
+ function localEconomyNotice() {
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.`;
1242
+ }
1243
+ function credentialRequiredError(env, cause) {
1244
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
1245
+ const clientEnv = env;
1246
+ const detail = cause instanceof Error ? cause.message : `No API key could be resolved for '${ECONOMY_APP}'. Looked in the Keychain (macOS only), ` + `then for a credential file at ${credentialDiskSources(ECONOMY_APP, clientEnv).join(" or ") || "<no HOME set>"}, ` + `then for ${keys.apiKeyKeys[0]} in the environment.`;
1247
+ const wayOut = hostedAuthorityConfigured(clientEnv) ? `${keys.apiUrlKeys[0]} (or the Keychain hasna.credentials.${ECONOMY_APP}.api-url item, or ` + `~/.hasna/${ECONOMY_APP}/config/credentials) selects a hosted service, so ${LOCAL_STORAGE_OPT_IN_KEYS[0]} ` + `does not apply; unset the authority to use the local store.` : `Or set ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 to explicitly opt into the local store.`;
1248
+ return new Error(`Economy fails closed instead of silently serving the local SQLite store: ${detail} ` + `Set ${keys.apiUrlKeys[0]} and ${keys.apiKeyKeys[0]} (or store the key in the Keychain item ` + `hasna.credentials.${ECONOMY_APP}.api-key, or in ~/.hasna/${ECONOMY_APP}/config/credentials) to use the ` + `hosted economy API at ${keys.apiUrlKeys[0]} or the default fleet gateway. ` + wayOut);
1249
+ }
3107
1250
  var cache;
1251
+ var localNoticePrinted = false;
3108
1252
  function resolveEconomyCloudStorage(env = process.env, overrides) {
3109
- const resolved = resolveStorageClient(ECONOMY_APP, env, overrides);
3110
- return resolved.transport === "http" ? { active: true, client: resolved.client } : { active: false, client: null };
1253
+ const clientEnv = env;
1254
+ if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
1255
+ return { active: false, client: null };
1256
+ }
1257
+ let resolved;
1258
+ try {
1259
+ resolved = resolveEconomyStorageClient(ECONOMY_APP, clientEnv, overrides ?? {});
1260
+ } catch (error) {
1261
+ if (error instanceof ClientTransportConfigurationError) {
1262
+ throw credentialRequiredError(env, error);
1263
+ }
1264
+ throw error;
1265
+ }
1266
+ return { active: true, client: resolved.client };
3111
1267
  }
3112
1268
  function economyCloudStorage(env = process.env) {
3113
- if (cache === undefined)
1269
+ if (cache === undefined) {
3114
1270
  cache = resolveEconomyCloudStorage(env);
1271
+ if (!cache.active && !localNoticePrinted) {
1272
+ localNoticePrinted = true;
1273
+ if (typeof process !== "undefined")
1274
+ process.stderr.write(`${localEconomyNotice()}
1275
+ `);
1276
+ }
1277
+ }
3115
1278
  return cache;
3116
1279
  }
3117
1280
  function cleanQuery(query) {
@@ -3139,94 +1302,116 @@ async function cloudObject(storage, path, query) {
3139
1302
  }
3140
1303
 
3141
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
+
3142
1326
  class LocalStore {
3143
1327
  transport = "local";
3144
1328
  _db;
3145
- db() {
1329
+ async db() {
3146
1330
  if (!this._db) {
1331
+ const { openDatabase } = await import("./chunks/sqlite-store-fq4natnf.js");
3147
1332
  this._db = openDatabase();
3148
1333
  ensurePricingSeeded(this._db);
3149
1334
  }
3150
1335
  return this._db;
3151
1336
  }
3152
1337
  async summary(period, machine) {
3153
- return querySummary(this.db(), period, machine);
1338
+ return querySummary(await this.db(), period, machine);
3154
1339
  }
3155
1340
  async sessions(filter) {
3156
- return querySessions(this.db(), filter);
1341
+ return querySessions(await this.db(), filter);
3157
1342
  }
3158
1343
  async topSessions(n, agent, since) {
3159
- return queryTopSessions(this.db(), n, agent, since);
1344
+ return queryTopSessions(await this.db(), n, agent, since);
3160
1345
  }
3161
1346
  async sessionDetail(id) {
3162
- return getSessionDetail(this.db(), id);
1347
+ return getSessionDetail(await this.db(), id);
3163
1348
  }
3164
1349
  async modelBreakdown(query = {}) {
3165
- 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());
3166
1351
  }
3167
1352
  async agentBreakdown(query = {}) {
3168
- 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);
3169
1354
  }
3170
1355
  async projectBreakdown(query = {}) {
3171
- 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);
3172
1357
  }
3173
1358
  async accountBreakdown(query = {}) {
3174
- 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);
3175
1360
  }
3176
1361
  async costCenterBreakdown(query = {}) {
3177
- return queryCostCenterBreakdown(this.db(), query.period ?? "all", {
1362
+ return queryCostCenterBreakdown(await this.db(), query.period ?? "all", {
3178
1363
  kind: query.kind,
3179
1364
  machine: query.machine,
3180
1365
  since: query.since
3181
1366
  });
3182
1367
  }
3183
1368
  async accounts(period) {
3184
- return queryAccountBreakdown(this.db(), period);
1369
+ return queryAccountBreakdown(await this.db(), period);
3185
1370
  }
3186
1371
  async daily(days, machine) {
3187
- return queryDailyBreakdown(this.db(), days, machine);
1372
+ return queryDailyBreakdown(await this.db(), days, machine);
3188
1373
  }
3189
1374
  async machines() {
3190
- return listMachines(this.db());
1375
+ return listMachines(await this.db());
3191
1376
  }
3192
1377
  async fleet(period) {
3193
1378
  return {
3194
- summary: querySummary(this.db(), period, undefined, true),
3195
- machines: listMachines(this.db(), period),
3196
- 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())
3197
1382
  };
3198
1383
  }
3199
1384
  async billingSummary(period) {
3200
- return queryBillingSummary(this.db(), period);
1385
+ return queryBillingSummary(await this.db(), period);
3201
1386
  }
3202
1387
  async billingDiff(period) {
3203
- return queryBillingDiff(this.db(), period);
1388
+ return queryBillingDiff(await this.db(), period);
3204
1389
  }
3205
1390
  async usage(period, agent) {
3206
- const snapshots = queryUsageSnapshots(this.db(), { agent, ...usageSnapshotFilterForPeriod(period) });
3207
- 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);
3208
1393
  return { snapshots, summary };
3209
1394
  }
3210
1395
  async savings(period, agent) {
3211
- return querySavingsSummary(this.db(), period, agent);
1396
+ return querySavingsSummary(await this.db(), period, agent);
3212
1397
  }
3213
1398
  async listBudgets() {
3214
- return getBudgetStatuses(this.db());
1399
+ return getBudgetStatuses(await this.db());
3215
1400
  }
3216
1401
  async listGoals() {
3217
- return getGoalStatuses(this.db());
1402
+ return getGoalStatuses(await this.db());
3218
1403
  }
3219
1404
  async listPricing() {
3220
- return listModelPricing(this.db());
1405
+ return listModelPricing(await this.db());
3221
1406
  }
3222
1407
  async listSubscriptions() {
3223
- return listSubscriptions(this.db());
1408
+ return listSubscriptions(await this.db());
3224
1409
  }
3225
1410
  async listProjects() {
3226
- return queryProjectBreakdown(this.db());
1411
+ return queryProjectBreakdown(await this.db());
3227
1412
  }
3228
1413
  async brief(query = {}) {
3229
- return buildBrief(this.db(), {
1414
+ return buildBrief(await this.db(), {
3230
1415
  since: query.since,
3231
1416
  machine: query.machine,
3232
1417
  currentMachineId: query.currentMachineId,
@@ -3234,71 +1419,59 @@ class LocalStore {
3234
1419
  });
3235
1420
  }
3236
1421
  async projectDetail(nameOrPath) {
3237
- return queryProjectDetail(this.db(), nameOrPath);
1422
+ return queryProjectDetail(await this.db(), nameOrPath);
3238
1423
  }
3239
1424
  async exportRows(type, period) {
3240
- return queryExportRows(this.db(), type, period);
1425
+ return queryExportRows(await this.db(), type, period);
3241
1426
  }
3242
1427
  async rangeStats(from, to) {
3243
- return queryRangeStats(this.db(), from, to);
1428
+ return queryRangeStats(await this.db(), from, to);
3244
1429
  }
3245
1430
  async forecast() {
3246
- return queryForecast(this.db());
1431
+ return queryForecast(await this.db());
3247
1432
  }
3248
1433
  async efficiency() {
3249
- return queryModelEfficiency(this.db());
1434
+ return queryModelEfficiency(await this.db());
3250
1435
  }
3251
1436
  async recentRequests(since) {
3252
- return queryRequestsSince(this.db(), since);
1437
+ return queryRequestsSince(await this.db(), since);
3253
1438
  }
3254
1439
  async estimate(input) {
3255
- 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);
3256
1441
  }
3257
1442
  async setBudget(input) {
3258
1443
  const now = new Date().toISOString();
3259
1444
  const budget = { id: randomUUID(), ...input, created_at: now, updated_at: now };
3260
- upsertBudget(this.db(), budget);
1445
+ upsertBudget(await this.db(), budget);
3261
1446
  return budget.id;
3262
1447
  }
3263
1448
  async removeBudget(id) {
3264
- deleteBudget(this.db(), id);
1449
+ deleteBudget(await this.db(), id);
3265
1450
  }
3266
1451
  async setGoal(input) {
3267
1452
  const now = new Date().toISOString();
3268
- 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 });
3269
1454
  }
3270
1455
  async removeGoal(id) {
3271
- deleteGoal(this.db(), id);
1456
+ deleteGoal(await this.db(), id);
3272
1457
  }
3273
1458
  async setPricing(input) {
3274
- upsertModelPricing(this.db(), { ...input, updated_at: new Date().toISOString() });
1459
+ upsertModelPricing(await this.db(), { ...input, updated_at: new Date().toISOString() });
3275
1460
  }
3276
1461
  async removePricing(model) {
3277
- deleteModelPricing(this.db(), model);
1462
+ deleteModelPricing(await this.db(), model);
3278
1463
  }
3279
1464
  async setSubscription(input) {
3280
- const now = new Date().toISOString();
3281
- const row = {
3282
- id: input.id ?? randomUUID(),
3283
- provider: input.provider,
3284
- plan: input.plan,
3285
- agent: input.agent,
3286
- monthly_fee_usd: input.monthly_fee_usd,
3287
- included_usage_usd: input.included_usage_usd,
3288
- billing_cycle_start: input.billing_cycle_start,
3289
- reset_policy: input.reset_policy,
3290
- active: input.active,
3291
- created_at: now,
3292
- updated_at: now
3293
- };
3294
- upsertSubscription(this.db(), row);
3295
- 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);
3296
1469
  }
3297
1470
  async removeSubscription(id) {
3298
- deleteSubscription(this.db(), id);
1471
+ deleteSubscription(await this.db(), id);
3299
1472
  }
3300
1473
  async addProject(path, name) {
3301
- upsertProject(this.db(), {
1474
+ upsertProject(await this.db(), {
3302
1475
  id: randomUUID(),
3303
1476
  path,
3304
1477
  name,
@@ -3308,16 +1481,16 @@ class LocalStore {
3308
1481
  });
3309
1482
  }
3310
1483
  async renameProject(path, name) {
3311
- const existing = getProject(this.db(), path);
1484
+ const existing = getProject(await this.db(), path);
3312
1485
  if (!existing)
3313
1486
  throw new Error("Project not found");
3314
- upsertProject(this.db(), { ...existing, name });
1487
+ upsertProject(await this.db(), { ...existing, name });
3315
1488
  }
3316
1489
  async removeProject(path) {
3317
- deleteProject(this.db(), path);
1490
+ deleteProject(await this.db(), path);
3318
1491
  }
3319
1492
  async sendFeedback(input) {
3320
- insertFeedback(this.db(), {
1493
+ insertFeedback(await this.db(), {
3321
1494
  message: input.message,
3322
1495
  email: input.email ?? null,
3323
1496
  category: input.category ?? "general",
@@ -3341,7 +1514,7 @@ class ApiStore {
3341
1514
  this.cloud = cloud;
3342
1515
  }
3343
1516
  async summary(period, machine) {
3344
- return cloudObject(this.cloud, "/summary", q({ period, machine }));
1517
+ return requireCurrentCostSummary(await cloudObject(this.cloud, "/summary", q({ period, machine })));
3345
1518
  }
3346
1519
  async sessions(filter) {
3347
1520
  return cloudListItems(this.cloud, "sessions", q({ ...filter }));
@@ -3389,22 +1562,23 @@ class ApiStore {
3389
1562
  async fleet(period) {
3390
1563
  const data = await cloudObject(this.cloud, "/fleet", q({ period }));
3391
1564
  return {
3392
- summary: data.summary,
1565
+ summary: requireCurrentCostSummary(data.summary),
3393
1566
  machines: data.machines ?? [],
3394
1567
  registry: data.registry ?? []
3395
1568
  };
3396
1569
  }
3397
1570
  async billingSummary(period) {
3398
- return cloudObject(this.cloud, "/billing", q({ period }));
1571
+ return requireCurrentBillingSummary(await cloudObject(this.cloud, "/billing", q({ period })));
3399
1572
  }
3400
1573
  async billingDiff(period) {
3401
- return cloudObject(this.cloud, "/billing/diff", q({ period }));
1574
+ return requireCurrentBillingDiffSummary(await cloudObject(this.cloud, "/billing/diff", q({ period })));
3402
1575
  }
3403
1576
  async usage(period, agent) {
3404
- 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) };
3405
1579
  }
3406
1580
  async savings(period, agent) {
3407
- return cloudObject(this.cloud, "/savings", q({ period, agent }));
1581
+ return requireCurrentSavingsReport(await cloudObject(this.cloud, "/savings", q({ period, agent })));
3408
1582
  }
3409
1583
  async listBudgets() {
3410
1584
  return cloudListItems(this.cloud, "budgets");
@@ -3478,22 +1652,8 @@ class ApiStore {
3478
1652
  await this.cloud.client.delete("pricing", model);
3479
1653
  }
3480
1654
  async setSubscription(input) {
3481
- const now = new Date().toISOString();
3482
- const row = {
3483
- id: input.id ?? randomUUID(),
3484
- provider: input.provider,
3485
- plan: input.plan,
3486
- agent: input.agent,
3487
- monthly_fee_usd: input.monthly_fee_usd,
3488
- included_usage_usd: input.included_usage_usd,
3489
- billing_cycle_start: input.billing_cycle_start,
3490
- reset_policy: input.reset_policy,
3491
- active: input.active,
3492
- created_at: now,
3493
- updated_at: now
3494
- };
3495
- await this.cloud.client.create("subscriptions", row);
3496
- return row;
1655
+ const row = configuredSubscription(input);
1656
+ return this.cloud.client.create("subscriptions", row);
3497
1657
  }
3498
1658
  async removeSubscription(id) {
3499
1659
  await this.cloud.client.delete("subscriptions", id);
@@ -3523,9 +1683,725 @@ function getStore(env = process.env) {
3523
1683
  function isCloudStore(env = process.env) {
3524
1684
  return economyCloudStorage(env).active;
3525
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
+ }
3526
2400
  export {
3527
2401
  isCloudStore,
3528
2402
  isAgent,
2403
+ ingestJournalOperationSchema,
2404
+ ingestJournal,
3529
2405
  getStore,
3530
2406
  LocalStore,
3531
2407
  COST_BASIS,