@hasna/economy 0.3.6 → 0.3.7

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.
package/dist/storage.js DELETED
@@ -1,2746 +0,0 @@
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
- var __require = import.meta.require;
18
-
19
- // src/db/storage-adapter.ts
20
- import { Database as BunDatabase } from "bun:sqlite";
21
-
22
- class SqliteAdapter {
23
- db;
24
- constructor(path) {
25
- this.db = new BunDatabase(path, { create: true });
26
- }
27
- run(sql, ...params) {
28
- const result = this.db.prepare(sql).run(...params);
29
- return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
30
- }
31
- get(sql, ...params) {
32
- return this.db.prepare(sql).get(...params);
33
- }
34
- all(sql, ...params) {
35
- return this.db.prepare(sql).all(...params);
36
- }
37
- exec(sql) {
38
- this.db.exec(sql);
39
- }
40
- query(sql) {
41
- return this.db.query(sql);
42
- }
43
- prepare(sql) {
44
- const statement = this.db.prepare(sql);
45
- return {
46
- run(...params) {
47
- const result = statement.run(...params);
48
- return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
49
- },
50
- get(...params) {
51
- return statement.get(...params);
52
- },
53
- all(...params) {
54
- return statement.all(...params);
55
- },
56
- finalize() {
57
- statement.finalize();
58
- }
59
- };
60
- }
61
- close() {
62
- this.db.close();
63
- }
64
- transaction(fn) {
65
- return this.db.transaction(fn)();
66
- }
67
- get raw() {
68
- return this.db;
69
- }
70
- }
71
- var init_storage_adapter = () => {};
72
-
73
- // src/lib/pricing.ts
74
- var exports_pricing = {};
75
- __export(exports_pricing, {
76
- normalizeModelName: () => normalizeModelName,
77
- getPricingFromDb: () => getPricingFromDb,
78
- getPricing: () => getPricing,
79
- ensurePricingSeeded: () => ensurePricingSeeded,
80
- computeCostFromDb: () => computeCostFromDb,
81
- computeCost: () => computeCost,
82
- DEFAULT_PRICING: () => DEFAULT_PRICING
83
- });
84
- function normalizeModelName(raw) {
85
- return raw.trim().toLowerCase().replace(/^models\//, "").replace(/^[a-z0-9_.-]+\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
86
- }
87
- function normalizeModelNamePreservingProvider(raw) {
88
- return raw.trim().toLowerCase().replace(/^models\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
89
- }
90
- function modelLookupKeys(raw) {
91
- const withProvider = normalizeModelNamePreservingProvider(raw);
92
- const withoutProvider = normalizeModelName(raw);
93
- return withProvider === withoutProvider ? [withoutProvider] : [withProvider, withoutProvider];
94
- }
95
- function bestPrefixMatch(normalized, entries) {
96
- let best = null;
97
- for (const entry of entries) {
98
- const [key] = entry;
99
- if (normalized !== key && !normalized.startsWith(`${key}-`))
100
- continue;
101
- if (!best || key.length > best[0].length)
102
- best = entry;
103
- }
104
- return best?.[1] ?? null;
105
- }
106
- function bestModelMatch(model, entries) {
107
- for (const key of modelLookupKeys(model)) {
108
- const match = bestPrefixMatch(key, entries);
109
- if (match)
110
- return match;
111
- }
112
- return null;
113
- }
114
- function exactModelMatch(model, entries) {
115
- for (const key of modelLookupKeys(model)) {
116
- const match = entries.find(([entryKey]) => entryKey === key);
117
- if (match)
118
- return match[1];
119
- }
120
- return null;
121
- }
122
- function ensurePricingSeeded(db) {
123
- seedModelPricing(db, DEFAULT_PRICING);
124
- repairLegacySeededPricing(db);
125
- repairMissingDefaultCacheWrite1h(db);
126
- repairMissingDefaultCacheStorage(db);
127
- removeDeprecatedDefaultPricing(db);
128
- }
129
- function repairLegacySeededPricing(db) {
130
- const now = new Date().toISOString();
131
- const legacyModels = new Set([
132
- ...Object.keys(LEGACY_DEFAULT_PRICING),
133
- ...Object.keys(ADDITIONAL_LEGACY_DEFAULT_PRICING)
134
- ]);
135
- for (const model of legacyModels) {
136
- const current = getModelPricing(db, model);
137
- const next = DEFAULT_PRICING[model];
138
- if (!current || !next)
139
- continue;
140
- const legacy = LEGACY_DEFAULT_PRICING[model];
141
- const legacyRows = [
142
- ...legacy ? [legacy] : [],
143
- ...ADDITIONAL_LEGACY_DEFAULT_PRICING[model] ?? []
144
- ];
145
- if (!legacyRows.some((row) => samePricing(current, row)))
146
- continue;
147
- upsertModelPricing(db, {
148
- model,
149
- input_per_1m: next.inputPer1M,
150
- output_per_1m: next.outputPer1M,
151
- cache_read_per_1m: next.cacheReadPer1M,
152
- cache_write_per_1m: next.cacheWritePer1M,
153
- cache_write_1h_per_1m: next.cacheWrite1hPer1M ?? 0,
154
- cache_storage_per_1m_hour: next.cacheStoragePer1MHour ?? 0,
155
- updated_at: now
156
- });
157
- }
158
- }
159
- function repairMissingDefaultCacheWrite1h(db) {
160
- const now = new Date().toISOString();
161
- for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
162
- if (!next.cacheWrite1hPer1M)
163
- continue;
164
- const current = getModelPricing(db, model);
165
- if (!current)
166
- continue;
167
- if ((current.cache_write_1h_per_1m ?? 0) !== 0)
168
- continue;
169
- if (!sameBasePricing(current, next))
170
- continue;
171
- upsertModelPricing(db, {
172
- model,
173
- input_per_1m: current.input_per_1m,
174
- output_per_1m: current.output_per_1m,
175
- cache_read_per_1m: current.cache_read_per_1m,
176
- cache_write_per_1m: current.cache_write_per_1m,
177
- cache_write_1h_per_1m: next.cacheWrite1hPer1M,
178
- cache_storage_per_1m_hour: current.cache_storage_per_1m_hour ?? next.cacheStoragePer1MHour ?? 0,
179
- updated_at: now
180
- });
181
- }
182
- }
183
- function repairMissingDefaultCacheStorage(db) {
184
- const now = new Date().toISOString();
185
- for (const [model, next] of Object.entries(DEFAULT_PRICING)) {
186
- if (!next.cacheStoragePer1MHour)
187
- continue;
188
- const current = getModelPricing(db, model);
189
- if (!current)
190
- continue;
191
- if ((current.cache_storage_per_1m_hour ?? 0) !== 0)
192
- continue;
193
- if (!sameBasePricing(current, next))
194
- continue;
195
- upsertModelPricing(db, {
196
- model,
197
- input_per_1m: current.input_per_1m,
198
- output_per_1m: current.output_per_1m,
199
- cache_read_per_1m: current.cache_read_per_1m,
200
- cache_write_per_1m: current.cache_write_per_1m,
201
- cache_write_1h_per_1m: current.cache_write_1h_per_1m ?? next.cacheWrite1hPer1M ?? 0,
202
- cache_storage_per_1m_hour: next.cacheStoragePer1MHour,
203
- updated_at: now
204
- });
205
- }
206
- }
207
- function removeDeprecatedDefaultPricing(db) {
208
- for (const [model, removedRows] of Object.entries(REMOVED_DEFAULT_PRICING)) {
209
- const current = getModelPricing(db, model);
210
- if (!current)
211
- continue;
212
- if (!removedRows.some((row) => samePricing(current, row)))
213
- continue;
214
- deleteModelPricing(db, model);
215
- }
216
- }
217
- function sameBasePricing(row, pricing) {
218
- 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;
219
- }
220
- function samePricing(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 && (row.cache_write_1h_per_1m ?? 0) === (pricing.cacheWrite1hPer1M ?? 0) && (row.cache_storage_per_1m_hour ?? 0) === (pricing.cacheStoragePer1MHour ?? 0);
222
- }
223
- function getPricingFromDb(db, model) {
224
- if (isFreeModel(model))
225
- return FREE_PRICING;
226
- for (const key of modelLookupKeys(model)) {
227
- const row = getModelPricing(db, key);
228
- if (row)
229
- return modelPricingFromDbRow(row);
230
- }
231
- const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
232
- const match = bestModelMatch(model, allRows.map((r) => [r.model, r]));
233
- if (!match)
234
- return null;
235
- return modelPricingFromDbRow(match);
236
- }
237
- function modelPricingFromDbRow(row) {
238
- const seeded = DEFAULT_PRICING[row.model];
239
- const cacheWrite1hPer1M = seeded?.cacheWrite1hPer1M && (row.cache_write_1h_per_1m ?? 0) === 0 && sameBasePricing(row, seeded) ? seeded.cacheWrite1hPer1M : row.cache_write_1h_per_1m ?? 0;
240
- return {
241
- inputPer1M: row.input_per_1m,
242
- outputPer1M: row.output_per_1m,
243
- cacheReadPer1M: row.cache_read_per_1m,
244
- cacheWritePer1M: row.cache_write_per_1m,
245
- cacheWrite1hPer1M,
246
- cacheStoragePer1MHour: row.cache_storage_per_1m_hour ?? seeded?.cacheStoragePer1MHour ?? 0
247
- };
248
- }
249
- function getPricing(model) {
250
- if (isFreeModel(model))
251
- return FREE_PRICING;
252
- return bestModelMatch(model, Object.entries(DEFAULT_PRICING));
253
- }
254
- function isFreeModel(model) {
255
- return model.trim().toLowerCase().endsWith(":free");
256
- }
257
- function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
258
- const pricing = getPricing(model);
259
- if (!pricing)
260
- return 0;
261
- return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
262
- }
263
- function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
264
- const pricing = getPricingFromDb(db, model) ?? getPricing(model);
265
- if (!pricing)
266
- return 0;
267
- return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
268
- }
269
- function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
270
- if (isFreeModel(model))
271
- return 0;
272
- let effective = pricing;
273
- 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));
274
- if (promptTier) {
275
- const billablePromptTokens = inputTokens + cacheReadTokens + cacheWriteTokens + cacheWrite1hTokens;
276
- if (billablePromptTokens > promptTier.threshold) {
277
- effective = {
278
- ...pricing,
279
- inputPer1M: promptTier.inputPer1M ?? pricing.inputPer1M * (promptTier.inputMultiplier ?? 1),
280
- outputPer1M: promptTier.outputPer1M ?? pricing.outputPer1M * (promptTier.outputMultiplier ?? 1),
281
- cacheReadPer1M: promptTier.cacheReadPer1M ?? pricing.cacheReadPer1M * (promptTier.cacheReadMultiplier ?? 1),
282
- cacheWritePer1M: promptTier.cacheWritePer1M ?? pricing.cacheWritePer1M * (promptTier.cacheWriteMultiplier ?? 1)
283
- };
284
- }
285
- }
286
- return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
287
- }
288
- 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;
289
- var init_pricing = __esm(() => {
290
- init_database();
291
- DEFAULT_PRICING = {
292
- "claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
293
- "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
294
- "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25, cacheWrite1hPer1M: 10 },
295
- "claude-opus-4-1": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
296
- "claude-opus-4": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
297
- "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
298
- "claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
299
- "claude-sonnet-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
300
- "claude-3-7-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
301
- "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25, cacheWrite1hPer1M: 2 },
302
- "claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1, cacheWrite1hPer1M: 1.6 },
303
- "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75, cacheWrite1hPer1M: 30 },
304
- "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3, cacheWrite1hPer1M: 0.5 },
305
- "gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
306
- "gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
307
- "gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
308
- "gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
309
- "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0, cacheStoragePer1MHour: 4.5 },
310
- "gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
311
- "gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
312
- "gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.025, cacheWritePer1M: 0, cacheStoragePer1MHour: 1 },
313
- "gemini-2.0-flash-lite": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
314
- "google/gemini-3.1-pro-preview": { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0.375 },
315
- "google/gemini-3.1-flash-lite-preview": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
316
- "google/gemini-3.1-flash-lite": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.08333333333333334 },
317
- "google/gemini-3-flash-preview": { inputPer1M: 0.5, outputPer1M: 3, cacheReadPer1M: 0.05, cacheWritePer1M: 0.08333333333333334 },
318
- "google/gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0.375 },
319
- "google/gemini-2.5-flash": { inputPer1M: 0.3, outputPer1M: 2.5, cacheReadPer1M: 0.03, cacheWritePer1M: 0.08333333333333334 },
320
- "google/gemini-2.5-flash-lite": { inputPer1M: 0.1, outputPer1M: 0.4, cacheReadPer1M: 0.01, cacheWritePer1M: 0.08333333333333334 },
321
- "gpt-5.5": { inputPer1M: 5, outputPer1M: 30, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
322
- "gpt-5.5-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
323
- "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
324
- "gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
325
- "gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
326
- "gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
327
- "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
328
- "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
329
- "gpt-5.2-chat-latest": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
330
- "gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
331
- "gpt-5-codex": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
332
- "gpt-5-mini": { inputPer1M: 0.25, outputPer1M: 2, cacheReadPer1M: 0.025, cacheWritePer1M: 0 },
333
- "gpt-5": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.125, cacheWritePer1M: 0 },
334
- "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
335
- "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
336
- o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
337
- "o1-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
338
- o3: { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
339
- "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
340
- "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
341
- "qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
342
- "qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
343
- "qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
344
- "qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
345
- "qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
346
- "qwen/qwen3.6-plus": { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.0325, cacheWritePer1M: 0.40625 },
347
- "qwen/qwen3.6-flash": { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0.025, cacheWritePer1M: 0.3125 },
348
- "qwen/qwen3.6-35b-a3b": { inputPer1M: 0.15, outputPer1M: 1, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
349
- "qwen/qwen3.6-max-preview": { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.104, cacheWritePer1M: 1.3 },
350
- "qwen/qwen3.6-27b": { inputPer1M: 0.32, outputPer1M: 3.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
351
- "minimax-m2.7": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
352
- "minimax-m2.7-highspeed": { inputPer1M: 0.6, outputPer1M: 2.4, cacheReadPer1M: 0.06, cacheWritePer1M: 0.375 },
353
- "minimax/minimax-m2.7": { inputPer1M: 0.299, outputPer1M: 1.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
354
- "minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
355
- "minimax/minimax-m1": { inputPer1M: 0.4, outputPer1M: 2.2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
356
- "grok-4.3": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
357
- "grok-latest": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
358
- "grok-4.20": { inputPer1M: 1.25, outputPer1M: 2.5, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
359
- "grok-4-1-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
360
- "grok-4-fast": { inputPer1M: 0.2, outputPer1M: 0.5, cacheReadPer1M: 0.05, cacheWritePer1M: 0 },
361
- "grok-4": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
362
- "grok-code-fast-1": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
363
- "grok-code-fast": { inputPer1M: 0.2, outputPer1M: 1.5, cacheReadPer1M: 0.02, cacheWritePer1M: 0 },
364
- "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.75, cacheWritePer1M: 0 },
365
- "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0.07, cacheWritePer1M: 0 },
366
- "glm-5.1": { inputPer1M: 1.4, outputPer1M: 4.4, cacheReadPer1M: 0.26, cacheWritePer1M: 0 },
367
- "glm-5": { inputPer1M: 1, outputPer1M: 3.2, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
368
- "z-ai/glm-5.1": { inputPer1M: 1.05, outputPer1M: 3.5, cacheReadPer1M: 0.525, cacheWritePer1M: 0 },
369
- "z-ai/glm-5": { inputPer1M: 0.6, outputPer1M: 1.92, cacheReadPer1M: 0.12, cacheWritePer1M: 0 },
370
- "kimi-k2.6": { inputPer1M: 0.95, outputPer1M: 4, cacheReadPer1M: 0.16, cacheWritePer1M: 0 },
371
- "kimi-k2.5": { inputPer1M: 0.6, outputPer1M: 3, cacheReadPer1M: 0.1, cacheWritePer1M: 0 },
372
- "kimi-k2": { inputPer1M: 0.6, outputPer1M: 2.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
373
- "moonshotai/kimi-k2.6": { inputPer1M: 0.75, outputPer1M: 3.5, cacheReadPer1M: 0.15, cacheWritePer1M: 0 },
374
- "moonshotai/kimi-k2.5": { inputPer1M: 0.44, outputPer1M: 2, cacheReadPer1M: 0.22, cacheWritePer1M: 0 },
375
- "moonshotai/kimi-k2": { inputPer1M: 0.57, outputPer1M: 2.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
376
- };
377
- LEGACY_DEFAULT_PRICING = {
378
- "claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
379
- "claude-opus-4": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
380
- "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
381
- "gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
382
- "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
383
- "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
384
- "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
385
- "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
386
- "gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
387
- "gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
388
- "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
389
- "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
390
- "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
391
- "qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
392
- "minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
393
- "minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
394
- "minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
395
- "glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
396
- "glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
397
- "kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
398
- o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 }
399
- };
400
- ADDITIONAL_LEGACY_DEFAULT_PRICING = {
401
- "gemini-2.5-pro": [
402
- { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 }
403
- ],
404
- "qwen3.6-plus": [
405
- { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
406
- { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
407
- ],
408
- "qwen3.6-flash": [
409
- { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
410
- ],
411
- "qwen3.6-max-preview": [
412
- { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
413
- { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
414
- ],
415
- "qwen/qwen3.6-plus": [
416
- { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0, cacheWritePer1M: 0.40625 },
417
- { inputPer1M: 0.325, outputPer1M: 1.95, cacheReadPer1M: 0.05, cacheWritePer1M: 0.40625 }
418
- ],
419
- "qwen/qwen3.6-flash": [
420
- { inputPer1M: 0.25, outputPer1M: 1.5, cacheReadPer1M: 0, cacheWritePer1M: 0.3125 }
421
- ],
422
- "qwen/qwen3.6-max-preview": [
423
- { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0, cacheWritePer1M: 1.3 },
424
- { inputPer1M: 1.04, outputPer1M: 6.24, cacheReadPer1M: 0.13, cacheWritePer1M: 1.3 }
425
- ]
426
- };
427
- REMOVED_DEFAULT_PRICING = {
428
- "claude-3-5-sonnet": [
429
- { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
430
- { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
431
- ],
432
- "claude-3-sonnet": [
433
- { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 6 },
434
- { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75, cacheWrite1hPer1M: 0 }
435
- ],
436
- "gemini-3.1-pro": [
437
- { inputPer1M: 2, outputPer1M: 12, cacheReadPer1M: 0.2, cacheWritePer1M: 0 },
438
- { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 }
439
- ],
440
- "gemini-1.5-pro": [
441
- { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 }
442
- ],
443
- "gemini-1.5-flash": [
444
- { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 }
445
- ],
446
- "gpt-5.3-chat": [
447
- { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.175, cacheWritePer1M: 0 },
448
- { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 }
449
- ],
450
- "qwen3.6": [
451
- { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
452
- ]
453
- };
454
- FREE_PRICING = {
455
- inputPer1M: 0,
456
- outputPer1M: 0,
457
- cacheReadPer1M: 0,
458
- cacheWritePer1M: 0,
459
- cacheWrite1hPer1M: 0,
460
- cacheStoragePer1MHour: 0
461
- };
462
- GEMINI_PROMPT_TIERS = {
463
- "gemini-3.1-pro-preview": {
464
- threshold: 200000,
465
- inputPer1M: 4,
466
- outputPer1M: 18,
467
- cacheReadPer1M: 0.4
468
- },
469
- "gemini-2.5-pro": {
470
- threshold: 200000,
471
- inputPer1M: 2.5,
472
- outputPer1M: 15,
473
- cacheReadPer1M: 0.25
474
- }
475
- };
476
- OPENAI_PROMPT_TIERS = {
477
- "gpt-5.5": {
478
- threshold: 272000,
479
- inputMultiplier: 2,
480
- outputMultiplier: 1.5,
481
- cacheReadMultiplier: 2
482
- },
483
- "gpt-5.4-pro": {
484
- threshold: 272000,
485
- inputMultiplier: 2,
486
- outputMultiplier: 1.5,
487
- cacheReadMultiplier: 2
488
- },
489
- "gpt-5.4": {
490
- threshold: 272000,
491
- inputMultiplier: 2,
492
- outputMultiplier: 1.5,
493
- cacheReadMultiplier: 2
494
- }
495
- };
496
- QWEN_PROMPT_TIERS = {
497
- "qwen3.6-plus": {
498
- threshold: 256000,
499
- inputPer1M: 1.3,
500
- outputPer1M: 3.9,
501
- cacheReadPer1M: 0.13,
502
- cacheWritePer1M: 1.625
503
- },
504
- "qwen3.6-flash": {
505
- threshold: 256000,
506
- inputPer1M: 1,
507
- outputPer1M: 4,
508
- cacheReadPer1M: 0.1,
509
- cacheWritePer1M: 1.25
510
- },
511
- "qwen3.6-max-preview": {
512
- threshold: 128000,
513
- inputPer1M: 1.6,
514
- outputPer1M: 9.6,
515
- cacheReadPer1M: 0.16,
516
- cacheWritePer1M: 2
517
- }
518
- };
519
- MINIMAX_PROMPT_TIERS = {
520
- "minimax/minimax-m1": {
521
- threshold: Number.POSITIVE_INFINITY
522
- },
523
- "minimax-m1": {
524
- threshold: 200000,
525
- inputPer1M: 1.3
526
- }
527
- };
528
- XAI_PROMPT_TIERS = {
529
- "grok-4.3": {
530
- threshold: 200000,
531
- inputPer1M: 2.5,
532
- outputPer1M: 5,
533
- cacheReadPer1M: 0.4
534
- },
535
- "grok-latest": {
536
- threshold: 200000,
537
- inputPer1M: 2.5,
538
- outputPer1M: 5,
539
- cacheReadPer1M: 0.4
540
- },
541
- "grok-4.20": {
542
- threshold: 200000,
543
- inputPer1M: 2.5,
544
- outputPer1M: 5,
545
- cacheReadPer1M: 0.4
546
- },
547
- "grok-4-1-fast": {
548
- threshold: 128000,
549
- inputPer1M: 0.4,
550
- outputPer1M: 1,
551
- cacheReadPer1M: 0
552
- },
553
- "grok-4-fast": {
554
- threshold: 128000,
555
- inputPer1M: 0.4,
556
- outputPer1M: 1,
557
- cacheReadPer1M: 0
558
- },
559
- "grok-4": {
560
- threshold: 128000,
561
- inputPer1M: 6,
562
- outputPer1M: 30,
563
- cacheReadPer1M: 0
564
- }
565
- };
566
- });
567
-
568
- // src/db/database.ts
569
- import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
570
- import { hostname } from "os";
571
- import { homedir } from "os";
572
- import { join } from "path";
573
- function getMachineId() {
574
- if (process.env["ECONOMY_MACHINE_ID"])
575
- return process.env["ECONOMY_MACHINE_ID"];
576
- const h = hostname().toLowerCase();
577
- if (h.startsWith("spark") || h.startsWith("apple"))
578
- return h.split(".")[0];
579
- return h.split(".")[0];
580
- }
581
- function getDataDir() {
582
- const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
583
- const newDir = join(home, ".hasna", "economy");
584
- const oldDir = join(home, ".economy");
585
- if (existsSync(oldDir) && !existsSync(newDir)) {
586
- mkdirSync(newDir, { recursive: true });
587
- for (const file of readdirSync(oldDir)) {
588
- const oldPath = join(oldDir, file);
589
- if (statSync(oldPath).isFile()) {
590
- copyFileSync(oldPath, join(newDir, file));
591
- }
592
- }
593
- }
594
- mkdirSync(newDir, { recursive: true });
595
- return newDir;
596
- }
597
- function getDbPath() {
598
- if (process.env["HASNA_ECONOMY_DB_PATH"])
599
- return process.env["HASNA_ECONOMY_DB_PATH"];
600
- if (process.env["ECONOMY_DB"])
601
- return process.env["ECONOMY_DB"];
602
- return join(getDataDir(), "economy.db");
603
- }
604
- function openDatabase(dbPath, skipSeed = false) {
605
- const path = dbPath ?? getDbPath();
606
- if (path !== ":memory:") {
607
- const dir = path.substring(0, path.lastIndexOf("/"));
608
- if (dir && !existsSync(dir))
609
- mkdirSync(dir, { recursive: true });
610
- }
611
- const db = new SqliteAdapter(path);
612
- db.exec("PRAGMA journal_mode = WAL");
613
- db.exec("PRAGMA busy_timeout = 5000");
614
- db.exec("PRAGMA foreign_keys = ON");
615
- initSchema(db);
616
- if (!skipSeed) {
617
- Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
618
- }
619
- return db;
620
- }
621
- function quoteSqlIdent(identifier) {
622
- return `"${identifier.replace(/"/g, '""')}"`;
623
- }
624
- function hasColumn(db, table, column) {
625
- const columns = db.prepare(`PRAGMA table_info(${quoteSqlIdent(table)})`).all();
626
- return columns.some((c) => c.name === column);
627
- }
628
- function addColumnIfMissing(db, table, column, definition) {
629
- if (hasColumn(db, table, column))
630
- return false;
631
- try {
632
- db.exec(`ALTER TABLE ${quoteSqlIdent(table)} ADD COLUMN ${quoteSqlIdent(column)} ${definition}`);
633
- return true;
634
- } catch (error) {
635
- const message = error instanceof Error ? error.message : String(error);
636
- if (/duplicate column name/i.test(message))
637
- return true;
638
- throw error;
639
- }
640
- }
641
- function initSchema(db) {
642
- db.exec(`
643
- CREATE TABLE IF NOT EXISTS requests (
644
- id TEXT PRIMARY KEY,
645
- agent TEXT NOT NULL,
646
- session_id TEXT NOT NULL,
647
- model TEXT NOT NULL,
648
- input_tokens INTEGER DEFAULT 0,
649
- output_tokens INTEGER DEFAULT 0,
650
- cache_read_tokens INTEGER DEFAULT 0,
651
- cache_create_tokens INTEGER DEFAULT 0,
652
- cache_create_5m_tokens INTEGER DEFAULT 0,
653
- cache_create_1h_tokens INTEGER DEFAULT 0,
654
- cost_usd REAL NOT NULL DEFAULT 0,
655
- duration_ms INTEGER DEFAULT 0,
656
- timestamp TEXT NOT NULL,
657
- source_request_id TEXT,
658
- machine_id TEXT DEFAULT '',
659
- account_key TEXT DEFAULT '',
660
- account_tool TEXT DEFAULT '',
661
- account_name TEXT DEFAULT '',
662
- account_email TEXT DEFAULT '',
663
- account_source TEXT DEFAULT ''
664
- );
665
-
666
- CREATE TABLE IF NOT EXISTS sessions (
667
- id TEXT PRIMARY KEY,
668
- agent TEXT NOT NULL,
669
- project_path TEXT DEFAULT '',
670
- project_name TEXT DEFAULT '',
671
- started_at TEXT NOT NULL,
672
- ended_at TEXT,
673
- total_cost_usd REAL DEFAULT 0,
674
- total_tokens INTEGER DEFAULT 0,
675
- request_count INTEGER DEFAULT 0,
676
- machine_id TEXT DEFAULT '',
677
- account_key TEXT DEFAULT '',
678
- account_tool TEXT DEFAULT '',
679
- account_name TEXT DEFAULT '',
680
- account_email TEXT DEFAULT '',
681
- account_source TEXT DEFAULT ''
682
- );
683
-
684
- CREATE TABLE IF NOT EXISTS projects (
685
- id TEXT PRIMARY KEY,
686
- path TEXT UNIQUE NOT NULL,
687
- name TEXT NOT NULL,
688
- description TEXT,
689
- tags TEXT DEFAULT '[]',
690
- created_at TEXT NOT NULL
691
- );
692
-
693
- CREATE TABLE IF NOT EXISTS budgets (
694
- id TEXT PRIMARY KEY,
695
- project_path TEXT,
696
- agent TEXT,
697
- period TEXT NOT NULL,
698
- limit_usd REAL NOT NULL,
699
- alert_at_percent INTEGER DEFAULT 80,
700
- created_at TEXT NOT NULL,
701
- updated_at TEXT NOT NULL
702
- );
703
-
704
- CREATE TABLE IF NOT EXISTS goals (
705
- id TEXT PRIMARY KEY,
706
- period TEXT NOT NULL,
707
- project_path TEXT,
708
- agent TEXT,
709
- limit_usd REAL NOT NULL,
710
- created_at TEXT NOT NULL,
711
- updated_at TEXT NOT NULL
712
- );
713
-
714
- CREATE TABLE IF NOT EXISTS ingest_state (
715
- source TEXT NOT NULL,
716
- key TEXT NOT NULL,
717
- value TEXT NOT NULL,
718
- PRIMARY KEY (source, key)
719
- );
720
-
721
- CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
722
- CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp);
723
- CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent);
724
- CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
725
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
726
- CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
727
-
728
- CREATE TABLE IF NOT EXISTS model_pricing (
729
- model TEXT PRIMARY KEY,
730
- input_per_1m REAL NOT NULL DEFAULT 0,
731
- output_per_1m REAL NOT NULL DEFAULT 0,
732
- cache_read_per_1m REAL NOT NULL DEFAULT 0,
733
- cache_write_per_1m REAL NOT NULL DEFAULT 0,
734
- cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
735
- cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
736
- updated_at TEXT NOT NULL
737
- );
738
-
739
- CREATE TABLE IF NOT EXISTS feedback (
740
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
741
- message TEXT NOT NULL,
742
- email TEXT,
743
- category TEXT DEFAULT 'general',
744
- version TEXT,
745
- machine_id TEXT,
746
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
747
- );
748
-
749
- CREATE TABLE IF NOT EXISTS billing_daily (
750
- date TEXT NOT NULL,
751
- provider TEXT NOT NULL,
752
- description TEXT DEFAULT '',
753
- cost_usd REAL NOT NULL DEFAULT 0,
754
- updated_at TEXT NOT NULL,
755
- PRIMARY KEY (date, provider, description)
756
- );
757
-
758
- CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
759
- CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
760
-
761
- CREATE TABLE IF NOT EXISTS subscriptions (
762
- id TEXT PRIMARY KEY,
763
- agent TEXT,
764
- provider TEXT NOT NULL,
765
- plan TEXT NOT NULL,
766
- monthly_fee_usd REAL NOT NULL DEFAULT 0,
767
- included_usage_usd REAL NOT NULL DEFAULT 0,
768
- billing_cycle_start TEXT,
769
- reset_policy TEXT DEFAULT 'monthly',
770
- active INTEGER NOT NULL DEFAULT 1,
771
- created_at TEXT NOT NULL,
772
- updated_at TEXT NOT NULL
773
- );
774
-
775
- CREATE TABLE IF NOT EXISTS usage_snapshots (
776
- id TEXT PRIMARY KEY,
777
- agent TEXT NOT NULL,
778
- date TEXT NOT NULL,
779
- metric TEXT NOT NULL,
780
- value REAL NOT NULL DEFAULT 0,
781
- unit TEXT DEFAULT '',
782
- machine_id TEXT DEFAULT '',
783
- updated_at TEXT NOT NULL
784
- );
785
-
786
- CREATE TABLE IF NOT EXISTS savings_daily (
787
- date TEXT NOT NULL,
788
- agent TEXT DEFAULT '',
789
- api_equivalent_usd REAL NOT NULL DEFAULT 0,
790
- subscription_fee_usd REAL NOT NULL DEFAULT 0,
791
- included_consumed_usd REAL NOT NULL DEFAULT 0,
792
- on_demand_usd REAL NOT NULL DEFAULT 0,
793
- saved_usd REAL NOT NULL DEFAULT 0,
794
- updated_at TEXT NOT NULL,
795
- PRIMARY KEY (date, agent)
796
- );
797
-
798
- CREATE TABLE IF NOT EXISTS machines (
799
- machine_id TEXT PRIMARY KEY,
800
- hostname TEXT NOT NULL,
801
- last_seen_at TEXT,
802
- last_push_at TEXT,
803
- last_pull_at TEXT,
804
- economy_version TEXT,
805
- updated_at TEXT NOT NULL
806
- );
807
-
808
- CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
809
- CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
810
- `);
811
- addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
812
- addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
813
- if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
814
- db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
815
- }
816
- addColumnIfMissing(db, "requests", "cache_create_1h_tokens", "INTEGER DEFAULT 0");
817
- addColumnIfMissing(db, "requests", "cost_basis", `TEXT DEFAULT 'estimated'`);
818
- addColumnIfMissing(db, "requests", "attribution_tag", `TEXT DEFAULT ''`);
819
- if (addColumnIfMissing(db, "requests", "updated_at", `TEXT DEFAULT ''`)) {
820
- db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
821
- }
822
- addColumnIfMissing(db, "requests", "synced_at", `TEXT DEFAULT ''`);
823
- for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
824
- addColumnIfMissing(db, "requests", column, `TEXT DEFAULT ''`);
825
- }
826
- addColumnIfMissing(db, "sessions", "attribution_tag", `TEXT DEFAULT ''`);
827
- if (addColumnIfMissing(db, "sessions", "updated_at", `TEXT DEFAULT ''`)) {
828
- db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
829
- }
830
- addColumnIfMissing(db, "sessions", "synced_at", `TEXT DEFAULT ''`);
831
- for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
832
- addColumnIfMissing(db, "sessions", column, `TEXT DEFAULT ''`);
833
- }
834
- addColumnIfMissing(db, "model_pricing", "cache_write_1h_per_1m", "REAL NOT NULL DEFAULT 0");
835
- addColumnIfMissing(db, "model_pricing", "cache_storage_per_1m_hour", "REAL NOT NULL DEFAULT 0");
836
- db.exec(`
837
- CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
838
- CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
839
- CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
840
- CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
841
- `);
842
- }
843
- function periodWhere(period) {
844
- switch (period) {
845
- case "today":
846
- return `DATE(timestamp) = DATE('now')`;
847
- case "yesterday":
848
- return `DATE(timestamp) = DATE('now', '-1 day')`;
849
- case "week":
850
- return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
851
- case "month":
852
- return `timestamp >= DATE('now', 'start of month')`;
853
- case "year":
854
- return `timestamp >= DATE('now', 'start of year')`;
855
- case "all":
856
- return "1=1";
857
- }
858
- }
859
- function sessionPeriodWhere(period) {
860
- switch (period) {
861
- case "today":
862
- return `DATE(started_at) = DATE('now')`;
863
- case "yesterday":
864
- return `DATE(started_at) = DATE('now', '-1 day')`;
865
- case "week":
866
- return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
867
- case "month":
868
- return `started_at >= DATE('now', 'start of month')`;
869
- case "year":
870
- return `started_at >= DATE('now', 'start of year')`;
871
- case "all":
872
- return "1=1";
873
- }
874
- }
875
- function requestPeriodWhere(period) {
876
- switch (period) {
877
- case "today":
878
- return `DATE(timestamp) = DATE('now')`;
879
- case "yesterday":
880
- return `DATE(timestamp) = DATE('now', '-1 day')`;
881
- case "week":
882
- return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
883
- case "month":
884
- return `timestamp >= DATE('now', 'start of month')`;
885
- case "year":
886
- return `timestamp >= DATE('now', 'start of year')`;
887
- case "all":
888
- return "1=1";
889
- }
890
- }
891
- function upsertRequest(db, req) {
892
- const now = req.updated_at ?? new Date().toISOString();
893
- db.prepare(`
894
- INSERT OR REPLACE INTO requests
895
- (id, agent, session_id, model, input_tokens, output_tokens,
896
- cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
897
- cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
898
- source_request_id, machine_id, attribution_tag, account_key, account_tool,
899
- account_name, account_email, account_source, updated_at, synced_at)
900
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
901
- `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
902
- }
903
- function upsertSession(db, session) {
904
- const now = session.updated_at ?? new Date().toISOString();
905
- db.prepare(`
906
- INSERT OR REPLACE INTO sessions
907
- (id, agent, project_path, project_name, started_at, ended_at,
908
- total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
909
- account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
910
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
911
- `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
912
- }
913
- function rollupSession(db, sessionId) {
914
- db.prepare(`
915
- UPDATE sessions SET
916
- total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM requests WHERE session_id = ?),
917
- total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM requests WHERE session_id = ?),
918
- request_count = (SELECT COUNT(*) FROM requests WHERE session_id = ?),
919
- ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
920
- started_at = CASE WHEN started_at = '' OR started_at IS NULL
921
- THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
922
- ELSE started_at END,
923
- account_key = CASE WHEN account_key = '' OR account_key IS NULL
924
- THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
925
- ELSE account_key END,
926
- account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
927
- THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
928
- ELSE account_tool END,
929
- account_name = CASE WHEN account_name = '' OR account_name IS NULL
930
- THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
931
- ELSE account_name END,
932
- account_email = CASE WHEN account_email = '' OR account_email IS NULL
933
- THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
934
- ELSE account_email END,
935
- account_source = CASE WHEN account_source = '' OR account_source IS NULL
936
- THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
937
- ELSE account_source END
938
- WHERE id = ?
939
- `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
940
- }
941
- function querySessions(db, filter = {}) {
942
- const conditions = [];
943
- const params = [];
944
- if (filter.agent) {
945
- conditions.push("agent = ?");
946
- params.push(filter.agent);
947
- }
948
- if (filter.project) {
949
- conditions.push("project_path LIKE ?");
950
- params.push(`%${filter.project}%`);
951
- }
952
- if (filter.account) {
953
- const q = `%${filter.account}%`;
954
- conditions.push("(account_key LIKE ? OR account_name LIKE ? OR account_email LIKE ?)");
955
- params.push(q, q, q);
956
- }
957
- if (filter.since) {
958
- conditions.push("started_at >= ?");
959
- params.push(filter.since);
960
- }
961
- if (filter.machine) {
962
- conditions.push("machine_id = ?");
963
- params.push(filter.machine);
964
- }
965
- if (filter.search) {
966
- const q = `%${filter.search}%`;
967
- conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
968
- params.push(q, q, `${filter.search}%`);
969
- }
970
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
971
- const limit = filter.limit ?? 50;
972
- const offset = filter.offset ?? 0;
973
- return db.prepare(`
974
- SELECT * FROM sessions ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?
975
- `).all(...params, limit, offset);
976
- }
977
- function queryTopSessions(db, n = 10, agent) {
978
- if (agent) {
979
- return db.prepare(`SELECT * FROM sessions WHERE agent = ? ORDER BY total_cost_usd DESC LIMIT ?`).all(agent, n);
980
- }
981
- return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
982
- }
983
- function querySummary(db, period, machine, allMachines = false) {
984
- const rWhere = periodWhere(period);
985
- const sWhere = sessionPeriodWhere(period);
986
- const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
987
- const r = db.prepare(`
988
- SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
989
- COUNT(*) as requests,
990
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
991
- FROM requests WHERE ${rWhere}${machineClause}
992
- `).get();
993
- const codexTotals = db.prepare(`
994
- SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
995
- COALESCE(SUM(total_tokens), 0) as tokens,
996
- COALESCE(SUM(request_count), 0) as requests,
997
- COUNT(*) as sessions
998
- FROM sessions
999
- WHERE ${sWhere}${machineClause}
1000
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1001
- `).get();
1002
- const requestSessionCount = db.prepare(`
1003
- SELECT COUNT(DISTINCT session_id) as sessions
1004
- FROM requests
1005
- WHERE ${rWhere}${machineClause}
1006
- `).get();
1007
- return {
1008
- total_usd: r.total_usd + codexTotals.cost_usd,
1009
- requests: r.requests + codexTotals.requests,
1010
- tokens: r.tokens + codexTotals.tokens,
1011
- sessions: requestSessionCount.sessions + codexTotals.sessions,
1012
- period
1013
- };
1014
- }
1015
- function queryModelBreakdown(db) {
1016
- return db.prepare(`
1017
- SELECT model, agent,
1018
- COUNT(*) as requests,
1019
- COALESCE(SUM(input_tokens), 0) as input_tokens,
1020
- COALESCE(SUM(output_tokens), 0) as output_tokens,
1021
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1022
- COALESCE(SUM(cost_usd), 0) as cost_usd
1023
- FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
1024
- `).all();
1025
- }
1026
- function queryAgentBreakdown(db, period = "all", machine) {
1027
- const requestWhere = requestPeriodWhere(period);
1028
- const machineClause = machine ? " AND machine_id = ?" : "";
1029
- const machineParams = machine ? [machine] : [];
1030
- const groups = new Map;
1031
- const requestRows = db.prepare(`
1032
- SELECT agent,
1033
- COUNT(DISTINCT session_id) as sessions,
1034
- COUNT(*) as requests,
1035
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1036
- COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
1037
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1038
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1039
- COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1040
- COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
1041
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
1042
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1043
- MAX(timestamp) as last_active
1044
- FROM requests
1045
- WHERE ${requestWhere}${machineClause}
1046
- GROUP BY agent
1047
- ORDER BY api_equivalent_usd DESC
1048
- `).all(...machineParams);
1049
- for (const row of requestRows) {
1050
- groups.set(row.agent, row);
1051
- }
1052
- const sessionWhere = sessionPeriodWhere(period);
1053
- const sessionOnlyRows = db.prepare(`
1054
- SELECT agent,
1055
- COUNT(*) as sessions,
1056
- COALESCE(SUM(request_count), 0) as requests,
1057
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1058
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1059
- MAX(started_at) as last_active
1060
- FROM sessions
1061
- WHERE ${sessionWhere}${machineClause}
1062
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1063
- GROUP BY agent
1064
- `).all(...machineParams);
1065
- for (const row of sessionOnlyRows) {
1066
- const existing = groups.get(row.agent) ?? {
1067
- agent: row.agent,
1068
- sessions: 0,
1069
- requests: 0,
1070
- total_tokens: 0,
1071
- api_equivalent_usd: 0,
1072
- billable_usd: 0,
1073
- metered_api_usd: 0,
1074
- subscription_included_usd: 0,
1075
- estimated_usd: 0,
1076
- unknown_usd: 0,
1077
- cost_usd: 0,
1078
- last_active: ""
1079
- };
1080
- existing.sessions += row.sessions;
1081
- existing.requests += row.requests;
1082
- existing.total_tokens += row.total_tokens;
1083
- existing.api_equivalent_usd += row.cost_usd;
1084
- existing.estimated_usd += row.cost_usd;
1085
- existing.cost_usd += row.cost_usd;
1086
- if (!existing.last_active || row.last_active > existing.last_active)
1087
- existing.last_active = row.last_active;
1088
- groups.set(row.agent, existing);
1089
- }
1090
- return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
1091
- }
1092
- function pathProjectLabel(projectPath) {
1093
- if (!projectPath)
1094
- return "";
1095
- const segments = projectPath.split("/").filter(Boolean);
1096
- const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
1097
- for (const seg of segments) {
1098
- if (projectPrefix.test(seg))
1099
- return seg;
1100
- }
1101
- const generic = new Set([
1102
- "web",
1103
- "app",
1104
- "apps",
1105
- "packages",
1106
- "src",
1107
- "lib",
1108
- "server",
1109
- "client",
1110
- "api",
1111
- "frontend",
1112
- "backend",
1113
- "home",
1114
- "users",
1115
- "workspace",
1116
- "workspaces",
1117
- "hasna"
1118
- ]);
1119
- for (let i = segments.length - 1;i >= 0; i--) {
1120
- if (!generic.has(segments[i].toLowerCase()))
1121
- return segments[i];
1122
- }
1123
- return null;
1124
- }
1125
- function isRepoLikeLabel(label) {
1126
- return /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/.test(label) || label.includes("-");
1127
- }
1128
- function labelForPath(projectPath, projectName) {
1129
- const pathLabel = pathProjectLabel(projectPath);
1130
- if (pathLabel && (!projectName || projectName.trim() === "" || isRepoLikeLabel(pathLabel)))
1131
- return pathLabel;
1132
- if (projectName && projectName.trim() !== "")
1133
- return projectName;
1134
- if (pathLabel)
1135
- return pathLabel;
1136
- return projectPath;
1137
- }
1138
- function groupKeyForPath(projectPath, projectName) {
1139
- return labelForPath(projectPath, projectName).trim().toLowerCase();
1140
- }
1141
- function queryProjectBreakdown(db, period = "all", machine) {
1142
- const requestWhere = requestPeriodWhere(period);
1143
- const sessionWhere = sessionPeriodWhere(period);
1144
- const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
1145
- const requestMachineClause = machine ? " AND machine_id = ?" : "";
1146
- const sessionMachineParams = machine ? [machine, machine] : [];
1147
- const requestMachineParams = machine ? [machine] : [];
1148
- const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
1149
- const sessionOnlyMachineParams = machine ? [machine] : [];
1150
- const sessions = db.prepare(`
1151
- SELECT id, project_path, project_name, total_cost_usd, started_at
1152
- FROM sessions
1153
- WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
1154
- `).all(...sessionMachineParams);
1155
- const groups = new Map;
1156
- for (const s of sessions) {
1157
- const label = labelForPath(s.project_path, s.project_name);
1158
- if (!label)
1159
- continue;
1160
- const key = groupKeyForPath(s.project_path, s.project_name);
1161
- const g = groups.get(key) ?? { label, sessionIds: [], samplePath: s.project_path };
1162
- g.sessionIds.push(s.id);
1163
- if (!g.samplePath)
1164
- g.samplePath = s.project_path;
1165
- groups.set(key, g);
1166
- }
1167
- const result = [];
1168
- for (const g of groups.values()) {
1169
- const placeholders = g.sessionIds.map(() => "?").join(",");
1170
- const reqStats = placeholders.length ? db.prepare(`
1171
- SELECT
1172
- COUNT(DISTINCT session_id) as sessions,
1173
- COUNT(*) as requests,
1174
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1175
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1176
- MAX(timestamp) as last_active
1177
- FROM requests
1178
- WHERE session_id IN (${placeholders})
1179
- AND ${requestWhere}
1180
- ${requestMachineClause}
1181
- `).get(...g.sessionIds, ...requestMachineParams) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
1182
- const sessionOnlyStats = placeholders.length ? db.prepare(`
1183
- SELECT
1184
- COUNT(*) as sessions,
1185
- COALESCE(SUM(request_count), 0) as requests,
1186
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1187
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1188
- MAX(started_at) as last_active
1189
- FROM sessions
1190
- WHERE id IN (${placeholders})
1191
- AND ${sessionWhere}
1192
- ${sessionOnlyMachineClause}
1193
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1194
- `).get(...g.sessionIds, ...sessionOnlyMachineParams) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1195
- const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
1196
- if (totalSessions === 0)
1197
- continue;
1198
- const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
1199
- result.push({
1200
- project_path: g.samplePath,
1201
- project_name: g.label,
1202
- sessions: totalSessions,
1203
- requests: reqStats.requests + sessionOnlyStats.requests,
1204
- total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
1205
- cost_usd: reqStats.cost_usd + sessionOnlyStats.cost_usd,
1206
- last_active: lastActive
1207
- });
1208
- }
1209
- result.sort((a, b) => b.cost_usd - a.cost_usd);
1210
- return result;
1211
- }
1212
- function normalizeAccountEmail(email) {
1213
- return (email ?? "").trim().toLowerCase();
1214
- }
1215
- function accountIdentityKey(agent, accountKey, accountName, accountEmail) {
1216
- const identityAgent = (agent || "").trim();
1217
- const normalizedEmail = normalizeAccountEmail(accountEmail);
1218
- if (identityAgent && normalizedEmail)
1219
- return `${identityAgent}:${normalizedEmail}`;
1220
- if (identityAgent && accountName)
1221
- return `${identityAgent}:${accountName}`;
1222
- if (accountKey)
1223
- return accountKey;
1224
- return identityAgent ? `${identityAgent}:unknown` : "";
1225
- }
1226
- function addAccountBreakdownRow(groups, row, sessionOnly) {
1227
- const agent = row.agent || row.account_tool;
1228
- const email = normalizeAccountEmail(row.account_email);
1229
- const accountName = row.account_name || email || row.account_key;
1230
- const key = accountIdentityKey(agent, row.account_key, accountName, email);
1231
- if (!key)
1232
- return;
1233
- const group = groups.get(key) ?? {
1234
- account_key: key,
1235
- account_tool: agent,
1236
- account_name: accountName,
1237
- account_email: email || null,
1238
- account_source: row.account_source || "unknown",
1239
- sessionIds: new Set,
1240
- requests: 0,
1241
- total_tokens: 0,
1242
- api_equivalent_usd: 0,
1243
- metered_api_usd: 0,
1244
- subscription_included_usd: 0,
1245
- estimated_usd: 0,
1246
- unknown_usd: 0,
1247
- last_active: ""
1248
- };
1249
- if (!group.account_email && email)
1250
- group.account_email = email;
1251
- if (!group.account_name && accountName)
1252
- group.account_name = accountName;
1253
- if ((!group.account_source || group.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
1254
- group.account_source = row.account_source;
1255
- }
1256
- if (row.session_id)
1257
- group.sessionIds.add(row.session_id);
1258
- group.requests += row.requests;
1259
- group.total_tokens += row.total_tokens;
1260
- group.api_equivalent_usd += row.cost_usd;
1261
- if (sessionOnly) {
1262
- group.estimated_usd += row.cost_usd;
1263
- } else if (row.cost_basis === "metered_api") {
1264
- group.metered_api_usd += row.cost_usd;
1265
- } else if (row.cost_basis === "subscription_included") {
1266
- group.subscription_included_usd += row.cost_usd;
1267
- } else if (row.cost_basis === "unknown") {
1268
- group.unknown_usd += row.cost_usd;
1269
- } else {
1270
- group.estimated_usd += row.cost_usd;
1271
- }
1272
- if (!group.last_active || row.last_active > group.last_active)
1273
- group.last_active = row.last_active;
1274
- groups.set(key, group);
1275
- }
1276
- function queryAccountBreakdown(db, period = "all", machine) {
1277
- const requestWhere = requestPeriodWhere(period);
1278
- const sessionWhere = sessionPeriodWhere(period);
1279
- const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
1280
- const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
1281
- const requestMachineParams = machine ? [machine] : [];
1282
- const sessionMachineParams = machine ? [machine] : [];
1283
- const groups = new Map;
1284
- const requestRows = db.prepare(`
1285
- SELECT
1286
- r.session_id as session_id,
1287
- COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
1288
- COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
1289
- COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
1290
- COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
1291
- COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
1292
- COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
1293
- 1 as requests,
1294
- COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1295
- COALESCE(r.cost_usd, 0) as cost_usd,
1296
- COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1297
- r.timestamp as last_active
1298
- FROM requests r
1299
- LEFT JOIN sessions s ON s.id = r.session_id
1300
- WHERE ${requestWhere}${requestMachineClause}
1301
- AND (
1302
- COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
1303
- OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
1304
- OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
1305
- OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
1306
- )
1307
- `).all(...requestMachineParams);
1308
- for (const row of requestRows)
1309
- addAccountBreakdownRow(groups, row, false);
1310
- const sessionOnlyRows = db.prepare(`
1311
- SELECT
1312
- s.id as session_id,
1313
- s.agent as agent,
1314
- s.account_key as account_key,
1315
- s.account_tool as account_tool,
1316
- s.account_name as account_name,
1317
- s.account_email as account_email,
1318
- COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
1319
- COALESCE(s.request_count, 0) as requests,
1320
- COALESCE(s.total_tokens, 0) as total_tokens,
1321
- COALESCE(s.total_cost_usd, 0) as cost_usd,
1322
- 'estimated' as cost_basis,
1323
- s.started_at as last_active
1324
- FROM sessions s
1325
- WHERE ${sessionWhere}${sessionMachineClause}
1326
- AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1327
- AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
1328
- `).all(...sessionMachineParams);
1329
- for (const row of sessionOnlyRows)
1330
- addAccountBreakdownRow(groups, row, true);
1331
- const result = [...groups.values()].map((group) => ({
1332
- account_key: group.account_key,
1333
- account_tool: group.account_tool,
1334
- account_name: group.account_name,
1335
- account_email: group.account_email,
1336
- account_source: group.account_source,
1337
- sessions: group.sessionIds.size,
1338
- requests: group.requests,
1339
- total_tokens: group.total_tokens,
1340
- api_equivalent_usd: group.api_equivalent_usd,
1341
- billable_usd: group.metered_api_usd,
1342
- metered_api_usd: group.metered_api_usd,
1343
- subscription_included_usd: group.subscription_included_usd,
1344
- estimated_usd: group.estimated_usd,
1345
- unknown_usd: group.unknown_usd,
1346
- cost_usd: group.api_equivalent_usd,
1347
- last_active: group.last_active
1348
- }));
1349
- result.sort((a, b) => b.cost_usd - a.cost_usd);
1350
- return result;
1351
- }
1352
- function queryDailyBreakdown(db, days = 30, machine) {
1353
- const machineClause = machine ? " AND machine_id = ?" : "";
1354
- const params = machine ? [`-${days}`, machine] : [`-${days}`];
1355
- return db.prepare(`
1356
- SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
1357
- FROM requests
1358
- WHERE timestamp >= DATE('now', ? || ' days')${machineClause}
1359
- GROUP BY DATE(timestamp), agent
1360
- ORDER BY date ASC
1361
- `).all(...params);
1362
- }
1363
- function queryHourlyBreakdown(db, machine, hours) {
1364
- const clauses = hours == null ? [`DATE(timestamp) = DATE('now')`] : [`DATETIME(timestamp) >= DATETIME('now', ?)`];
1365
- const params = hours == null ? [] : [`-${hours} hours`];
1366
- if (machine) {
1367
- clauses.push("machine_id = ?");
1368
- params.push(machine);
1369
- }
1370
- return db.prepare(`
1371
- SELECT STRFTIME('%H', timestamp) as hour, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
1372
- FROM requests
1373
- WHERE ${clauses.join(" AND ")}
1374
- GROUP BY STRFTIME('%H', timestamp), agent
1375
- ORDER BY hour ASC
1376
- `).all(...params);
1377
- }
1378
- function upsertProject(db, project) {
1379
- db.prepare(`
1380
- INSERT OR REPLACE INTO projects (id, path, name, description, tags, created_at)
1381
- VALUES (?, ?, ?, ?, ?, ?)
1382
- `).run(project.id, project.path, project.name, project.description ?? null, JSON.stringify(project.tags), project.created_at);
1383
- }
1384
- function getProject(db, path) {
1385
- const row = db.prepare(`SELECT * FROM projects WHERE path = ?`).get(path);
1386
- if (!row)
1387
- return null;
1388
- return { ...row, tags: JSON.parse(row["tags"] ?? "[]") };
1389
- }
1390
- function listProjects(db) {
1391
- return db.prepare(`SELECT * FROM projects ORDER BY created_at DESC`).all().map((row) => ({ ...row, tags: JSON.parse(row["tags"] ?? "[]") }));
1392
- }
1393
- function deleteProject(db, path) {
1394
- db.prepare(`DELETE FROM projects WHERE path = ?`).run(path);
1395
- }
1396
- function upsertBudget(db, budget) {
1397
- db.prepare(`
1398
- INSERT OR REPLACE INTO budgets
1399
- (id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
1400
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1401
- `).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
1402
- }
1403
- function listBudgets(db) {
1404
- return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
1405
- }
1406
- function deleteBudget(db, id) {
1407
- db.prepare(`DELETE FROM budgets WHERE id = ?`).run(id);
1408
- }
1409
- function getBudgetStatuses(db) {
1410
- const budgets = listBudgets(db);
1411
- return budgets.map((b) => {
1412
- const periodStart = b.period === "daily" ? "DATE('now')" : b.period === "weekly" ? "DATE('now', '-7 days')" : "DATE('now', '-30 days')";
1413
- let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
1414
- const params = [];
1415
- if (b.project_path) {
1416
- spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
1417
- params.push(b.project_path);
1418
- }
1419
- if (b.agent) {
1420
- spendQuery += ` AND agent = ?`;
1421
- params.push(b.agent);
1422
- }
1423
- const row = db.prepare(spendQuery).get(...params);
1424
- const spend = row.spend;
1425
- const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
1426
- return {
1427
- ...b,
1428
- current_spend_usd: spend,
1429
- percent_used: percent,
1430
- is_over_limit: percent >= 100,
1431
- is_over_alert: percent >= b.alert_at_percent
1432
- };
1433
- });
1434
- }
1435
- function upsertGoal(db, goal) {
1436
- db.prepare(`
1437
- INSERT OR REPLACE INTO goals
1438
- (id, period, project_path, agent, limit_usd, created_at, updated_at)
1439
- VALUES (?, ?, ?, ?, ?, ?, ?)
1440
- `).run(goal.id, goal.period, goal.project_path ?? null, goal.agent ?? null, goal.limit_usd, goal.created_at, goal.updated_at);
1441
- }
1442
- function deleteGoal(db, id) {
1443
- db.prepare(`DELETE FROM goals WHERE id = ?`).run(id);
1444
- }
1445
- function listGoals(db) {
1446
- return db.prepare(`SELECT * FROM goals ORDER BY created_at DESC`).all();
1447
- }
1448
- function getGoalStatuses(db) {
1449
- const goals = listGoals(db);
1450
- return goals.map((g) => {
1451
- 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')";
1452
- let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
1453
- const params = [];
1454
- if (g.project_path) {
1455
- spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
1456
- params.push(g.project_path);
1457
- }
1458
- if (g.agent) {
1459
- spendQuery += ` AND agent = ?`;
1460
- params.push(g.agent);
1461
- }
1462
- const row = db.prepare(spendQuery).get(...params);
1463
- const spend = row.spend;
1464
- const percent = g.limit_usd > 0 ? spend / g.limit_usd * 100 : 0;
1465
- return {
1466
- ...g,
1467
- current_spend_usd: spend,
1468
- percent_used: percent,
1469
- is_on_track: percent < 70,
1470
- is_at_risk: percent >= 70 && percent <= 100,
1471
- is_over: percent > 100
1472
- };
1473
- });
1474
- }
1475
- function getIngestState(db, source, key) {
1476
- const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
1477
- return row?.value ?? null;
1478
- }
1479
- function setIngestState(db, source, key, value) {
1480
- db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
1481
- }
1482
- function queryRequestsSince(db, since) {
1483
- return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
1484
- }
1485
- function upsertBillingDaily(db, row) {
1486
- db.prepare(`
1487
- INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
1488
- VALUES (?, ?, ?, ?, ?)
1489
- `).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
1490
- }
1491
- function clearBillingRange(db, provider, fromDate, toDate) {
1492
- db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
1493
- }
1494
- function queryBillingSummary(db, period) {
1495
- const where = period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
1496
- const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
1497
- const by_provider = {};
1498
- let total = 0;
1499
- for (const r of rows) {
1500
- by_provider[r.provider] = r.cost;
1501
- total += r.cost;
1502
- }
1503
- return { total_usd: total, by_provider };
1504
- }
1505
- function listMachines(db, period = "all") {
1506
- const rWhere = requestPeriodWhere(period);
1507
- const sWhere = sessionPeriodWhere(period);
1508
- return db.prepare(`
1509
- WITH request_stats AS (
1510
- SELECT
1511
- machine_id,
1512
- COUNT(DISTINCT session_id) as sessions,
1513
- COUNT(*) as requests,
1514
- COALESCE(SUM(cost_usd), 0) as total_cost_usd,
1515
- MAX(timestamp) as last_active
1516
- FROM requests
1517
- WHERE machine_id != ''
1518
- AND ${rWhere}
1519
- GROUP BY machine_id
1520
- ),
1521
- session_only_stats AS (
1522
- SELECT
1523
- machine_id,
1524
- COUNT(*) as sessions,
1525
- COALESCE(SUM(request_count), 0) as requests,
1526
- COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
1527
- MAX(started_at) as last_active
1528
- FROM sessions
1529
- WHERE machine_id != ''
1530
- AND ${sWhere}
1531
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1532
- GROUP BY machine_id
1533
- ),
1534
- combined AS (
1535
- SELECT * FROM request_stats
1536
- UNION ALL
1537
- SELECT * FROM session_only_stats
1538
- )
1539
- SELECT
1540
- machine_id,
1541
- COALESCE(SUM(sessions), 0) as sessions,
1542
- COALESCE(SUM(requests), 0) as requests,
1543
- COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
1544
- MAX(last_active) as last_active
1545
- FROM combined
1546
- GROUP BY machine_id
1547
- ORDER BY total_cost_usd DESC
1548
- `).all();
1549
- }
1550
- function upsertModelPricing(db, p) {
1551
- db.prepare(`
1552
- INSERT OR REPLACE INTO model_pricing
1553
- (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)
1554
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1555
- `).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);
1556
- }
1557
- function getModelPricing(db, model) {
1558
- return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
1559
- }
1560
- function listModelPricing(db) {
1561
- return db.prepare(`SELECT * FROM model_pricing ORDER BY model ASC`).all();
1562
- }
1563
- function deleteModelPricing(db, model) {
1564
- db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
1565
- }
1566
- function seedModelPricing(db, defaults) {
1567
- const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
1568
- const now = new Date().toISOString();
1569
- for (const [model, p] of Object.entries(defaults)) {
1570
- if (existing.has(model))
1571
- continue;
1572
- upsertModelPricing(db, {
1573
- model,
1574
- input_per_1m: p.inputPer1M,
1575
- output_per_1m: p.outputPer1M,
1576
- cache_read_per_1m: p.cacheReadPer1M,
1577
- cache_write_per_1m: p.cacheWritePer1M,
1578
- cache_write_1h_per_1m: p.cacheWrite1hPer1M ?? 0,
1579
- cache_storage_per_1m_hour: p.cacheStoragePer1MHour ?? 0,
1580
- updated_at: now
1581
- });
1582
- }
1583
- }
1584
- function upsertSubscription(db, sub) {
1585
- db.prepare(`
1586
- INSERT OR REPLACE INTO subscriptions
1587
- (id, agent, provider, plan, monthly_fee_usd, included_usage_usd, billing_cycle_start, reset_policy, active, created_at, updated_at)
1588
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1589
- `).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);
1590
- }
1591
- function listSubscriptions(db) {
1592
- return db.prepare(`SELECT * FROM subscriptions ORDER BY provider, plan`).all();
1593
- }
1594
- function deleteSubscription(db, id) {
1595
- db.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(id);
1596
- }
1597
- function upsertUsageSnapshot(db, snap) {
1598
- const now = snap.updated_at ?? new Date().toISOString();
1599
- const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
1600
- db.prepare(`
1601
- INSERT OR REPLACE INTO usage_snapshots (id, agent, date, metric, value, unit, machine_id, updated_at)
1602
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1603
- `).run(id, snap.agent, snap.date, snap.metric, snap.value, snap.unit, snap.machine_id, now);
1604
- }
1605
- function queryUsageSnapshots(db, opts = {}) {
1606
- const conditions = [];
1607
- const params = [];
1608
- if (opts.agent) {
1609
- conditions.push("agent = ?");
1610
- params.push(opts.agent);
1611
- }
1612
- if (opts.date) {
1613
- conditions.push("date = ?");
1614
- params.push(opts.date);
1615
- }
1616
- if (opts.since) {
1617
- conditions.push("date >= ?");
1618
- params.push(opts.since);
1619
- }
1620
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
1621
- return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
1622
- }
1623
- function listMachineRegistry(db) {
1624
- return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
1625
- }
1626
- function dedupeRequests(db) {
1627
- const dupes = db.prepare(`
1628
- SELECT source_request_id, agent, COALESCE(machine_id, '') as machine_id, MIN(id) as keep_id, COUNT(*) as cnt
1629
- FROM requests
1630
- WHERE source_request_id != '' AND source_request_id IS NOT NULL
1631
- GROUP BY source_request_id, agent, COALESCE(machine_id, '')
1632
- HAVING cnt > 1
1633
- `).all();
1634
- let removed = 0;
1635
- for (const row of dupes) {
1636
- const result = db.prepare(`
1637
- DELETE FROM requests
1638
- WHERE source_request_id = ?
1639
- AND agent = ?
1640
- AND COALESCE(machine_id, '') = ?
1641
- AND id != ?
1642
- `).run(row.source_request_id, row.agent, row.machine_id, row.keep_id);
1643
- removed += result.changes;
1644
- }
1645
- return removed;
1646
- }
1647
- var init_database = __esm(() => {
1648
- init_storage_adapter();
1649
- init_storage_adapter();
1650
- });
1651
-
1652
- // src/db/pg-migrations.ts
1653
- var exports_pg_migrations = {};
1654
- __export(exports_pg_migrations, {
1655
- PG_MIGRATIONS: () => PG_MIGRATIONS
1656
- });
1657
- var PG_MIGRATIONS;
1658
- var init_pg_migrations = __esm(() => {
1659
- PG_MIGRATIONS = [
1660
- `CREATE TABLE IF NOT EXISTS requests (
1661
- id TEXT PRIMARY KEY,
1662
- agent TEXT NOT NULL,
1663
- session_id TEXT NOT NULL,
1664
- model TEXT NOT NULL,
1665
- input_tokens INTEGER DEFAULT 0,
1666
- output_tokens INTEGER DEFAULT 0,
1667
- cache_read_tokens INTEGER DEFAULT 0,
1668
- cache_create_tokens INTEGER DEFAULT 0,
1669
- cache_create_5m_tokens INTEGER DEFAULT 0,
1670
- cache_create_1h_tokens INTEGER DEFAULT 0,
1671
- cost_usd REAL NOT NULL DEFAULT 0,
1672
- duration_ms INTEGER DEFAULT 0,
1673
- timestamp TEXT NOT NULL,
1674
- source_request_id TEXT,
1675
- machine_id TEXT DEFAULT '',
1676
- account_key TEXT DEFAULT '',
1677
- account_tool TEXT DEFAULT '',
1678
- account_name TEXT DEFAULT '',
1679
- account_email TEXT DEFAULT '',
1680
- account_source TEXT DEFAULT ''
1681
- )`,
1682
- `CREATE TABLE IF NOT EXISTS sessions (
1683
- id TEXT PRIMARY KEY,
1684
- agent TEXT NOT NULL,
1685
- project_path TEXT DEFAULT '',
1686
- project_name TEXT DEFAULT '',
1687
- started_at TEXT NOT NULL,
1688
- ended_at TEXT,
1689
- total_cost_usd REAL DEFAULT 0,
1690
- total_tokens INTEGER DEFAULT 0,
1691
- request_count INTEGER DEFAULT 0,
1692
- machine_id TEXT DEFAULT '',
1693
- account_key TEXT DEFAULT '',
1694
- account_tool TEXT DEFAULT '',
1695
- account_name TEXT DEFAULT '',
1696
- account_email TEXT DEFAULT '',
1697
- account_source TEXT DEFAULT ''
1698
- )`,
1699
- `CREATE TABLE IF NOT EXISTS projects (
1700
- id TEXT PRIMARY KEY,
1701
- path TEXT UNIQUE NOT NULL,
1702
- name TEXT NOT NULL,
1703
- description TEXT,
1704
- tags TEXT DEFAULT '[]',
1705
- created_at TEXT NOT NULL
1706
- )`,
1707
- `CREATE TABLE IF NOT EXISTS budgets (
1708
- id TEXT PRIMARY KEY,
1709
- project_path TEXT,
1710
- agent TEXT,
1711
- period TEXT NOT NULL,
1712
- limit_usd REAL NOT NULL,
1713
- alert_at_percent INTEGER DEFAULT 80,
1714
- created_at TEXT NOT NULL,
1715
- updated_at TEXT NOT NULL
1716
- )`,
1717
- `CREATE TABLE IF NOT EXISTS goals (
1718
- id TEXT PRIMARY KEY,
1719
- period TEXT NOT NULL,
1720
- project_path TEXT,
1721
- agent TEXT,
1722
- limit_usd REAL NOT NULL,
1723
- created_at TEXT NOT NULL,
1724
- updated_at TEXT NOT NULL
1725
- )`,
1726
- `CREATE TABLE IF NOT EXISTS ingest_state (
1727
- source TEXT NOT NULL,
1728
- key TEXT NOT NULL,
1729
- value TEXT NOT NULL,
1730
- PRIMARY KEY (source, key)
1731
- )`,
1732
- `CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
1733
- `CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
1734
- `CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
1735
- `CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
1736
- `CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
1737
- `CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
1738
- `CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
1739
- `CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
1740
- `CREATE TABLE IF NOT EXISTS model_pricing (
1741
- model TEXT PRIMARY KEY,
1742
- input_per_1m REAL NOT NULL DEFAULT 0,
1743
- output_per_1m REAL NOT NULL DEFAULT 0,
1744
- cache_read_per_1m REAL NOT NULL DEFAULT 0,
1745
- cache_write_per_1m REAL NOT NULL DEFAULT 0,
1746
- cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
1747
- cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
1748
- updated_at TEXT NOT NULL
1749
- )`,
1750
- `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_5m_tokens INTEGER DEFAULT 0`,
1751
- `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_1h_tokens INTEGER DEFAULT 0`,
1752
- `ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`,
1753
- `ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`,
1754
- `CREATE TABLE IF NOT EXISTS feedback (
1755
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
1756
- message TEXT NOT NULL,
1757
- email TEXT,
1758
- category TEXT DEFAULT 'general',
1759
- version TEXT,
1760
- machine_id TEXT,
1761
- created_at TEXT NOT NULL DEFAULT NOW()::text
1762
- )`,
1763
- `CREATE TABLE IF NOT EXISTS billing_daily (
1764
- date TEXT NOT NULL,
1765
- provider TEXT NOT NULL,
1766
- description TEXT DEFAULT '',
1767
- cost_usd REAL NOT NULL DEFAULT 0,
1768
- updated_at TEXT NOT NULL,
1769
- PRIMARY KEY (date, provider, description)
1770
- )`,
1771
- `CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date)`,
1772
- `CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider)`,
1773
- `CREATE TABLE IF NOT EXISTS subscriptions (
1774
- id TEXT PRIMARY KEY,
1775
- agent TEXT,
1776
- provider TEXT NOT NULL,
1777
- plan TEXT NOT NULL,
1778
- monthly_fee_usd REAL NOT NULL DEFAULT 0,
1779
- included_usage_usd REAL NOT NULL DEFAULT 0,
1780
- billing_cycle_start TEXT,
1781
- reset_policy TEXT DEFAULT 'monthly',
1782
- active INTEGER NOT NULL DEFAULT 1,
1783
- created_at TEXT NOT NULL,
1784
- updated_at TEXT NOT NULL
1785
- )`,
1786
- `CREATE TABLE IF NOT EXISTS usage_snapshots (
1787
- id TEXT PRIMARY KEY,
1788
- agent TEXT NOT NULL,
1789
- date TEXT NOT NULL,
1790
- metric TEXT NOT NULL,
1791
- value REAL NOT NULL DEFAULT 0,
1792
- unit TEXT DEFAULT '',
1793
- machine_id TEXT DEFAULT '',
1794
- updated_at TEXT NOT NULL
1795
- )`,
1796
- `CREATE TABLE IF NOT EXISTS savings_daily (
1797
- date TEXT NOT NULL,
1798
- agent TEXT DEFAULT '',
1799
- api_equivalent_usd REAL NOT NULL DEFAULT 0,
1800
- subscription_fee_usd REAL NOT NULL DEFAULT 0,
1801
- included_consumed_usd REAL NOT NULL DEFAULT 0,
1802
- on_demand_usd REAL NOT NULL DEFAULT 0,
1803
- saved_usd REAL NOT NULL DEFAULT 0,
1804
- updated_at TEXT NOT NULL,
1805
- PRIMARY KEY (date, agent)
1806
- )`,
1807
- `CREATE TABLE IF NOT EXISTS machines (
1808
- machine_id TEXT PRIMARY KEY,
1809
- hostname TEXT NOT NULL,
1810
- last_seen_at TEXT,
1811
- last_push_at TEXT,
1812
- last_pull_at TEXT,
1813
- economy_version TEXT,
1814
- updated_at TEXT NOT NULL
1815
- )`,
1816
- `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
1817
- `ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1818
- `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1819
- `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1820
- `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1821
- `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1822
- `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1823
- `ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1824
- `ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1825
- `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1826
- `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1827
- `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1828
- `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1829
- `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1830
- `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1831
- `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1832
- `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1833
- `CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
1834
- `CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
1835
- `CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
1836
- `CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`
1837
- ];
1838
- });
1839
-
1840
- // src/lib/native-storage.ts
1841
- init_database();
1842
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
1843
- import { homedir as homedir2, platform } from "os";
1844
- import { dirname, join as join2 } from "path";
1845
-
1846
- // src/lib/package-metadata.ts
1847
- import { readFileSync } from "fs";
1848
- var cachedMetadata = null;
1849
- function readPackageMetadata() {
1850
- const candidates = [
1851
- new URL("../package.json", import.meta.url),
1852
- new URL("../../package.json", import.meta.url),
1853
- new URL("../../../package.json", import.meta.url)
1854
- ];
1855
- for (const candidate of candidates) {
1856
- try {
1857
- return JSON.parse(readFileSync(candidate, "utf8"));
1858
- } catch {}
1859
- }
1860
- return {};
1861
- }
1862
- function getPackageMetadata() {
1863
- if (cachedMetadata)
1864
- return cachedMetadata;
1865
- const parsed = readPackageMetadata();
1866
- cachedMetadata = {
1867
- name: parsed.name ?? "@hasna/economy",
1868
- version: parsed.version ?? "0.0.0"
1869
- };
1870
- return cachedMetadata;
1871
- }
1872
- var packageMetadata = getPackageMetadata();
1873
-
1874
- // src/lib/remote-storage.ts
1875
- import pg from "pg";
1876
- function translatePlaceholders(sql) {
1877
- let index = 0;
1878
- return sql.replace(/\?/g, () => `$${++index}`);
1879
- }
1880
- function normalizeParams(params) {
1881
- const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
1882
- return flat.map((value) => value === undefined ? null : value);
1883
- }
1884
- function sslConfigFor(connectionString) {
1885
- return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
1886
- }
1887
-
1888
- class PgAdapterAsync {
1889
- pool;
1890
- constructor(source) {
1891
- this.pool = typeof source === "string" ? new pg.Pool({ connectionString: source, ssl: sslConfigFor(source) }) : source;
1892
- }
1893
- async run(sql, ...params) {
1894
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
1895
- return { changes: result.rowCount ?? 0, lastInsertRowid: 0 };
1896
- }
1897
- async get(sql, ...params) {
1898
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
1899
- return result.rows[0] ?? null;
1900
- }
1901
- async all(sql, ...params) {
1902
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
1903
- return result.rows;
1904
- }
1905
- async exec(sql) {
1906
- await this.pool.query(translatePlaceholders(sql));
1907
- }
1908
- async close() {
1909
- await this.pool.end();
1910
- }
1911
- async transaction(fn) {
1912
- const client = await this.pool.connect();
1913
- try {
1914
- await client.query("BEGIN");
1915
- const result = await fn(client);
1916
- await client.query("COMMIT");
1917
- return result;
1918
- } catch (error) {
1919
- await client.query("ROLLBACK");
1920
- throw error;
1921
- } finally {
1922
- client.release();
1923
- }
1924
- }
1925
- get raw() {
1926
- return this.pool;
1927
- }
1928
- }
1929
-
1930
- // src/lib/storage-sync.ts
1931
- async function syncPush(local, remote, options) {
1932
- const tables = await getTableOrder(remote, options.tables);
1933
- return syncTransfer(local, remote, { ...options, tables }, "push");
1934
- }
1935
- async function syncPull(remote, local, options) {
1936
- const tables = await getTableOrder(remote, options.tables);
1937
- return syncTransfer(remote, local, { ...options, tables }, "pull");
1938
- }
1939
- function quoteIdent(identifier) {
1940
- return `"${identifier.replace(/"/g, '""')}"`;
1941
- }
1942
- async function getTableOrder(remote, tables) {
1943
- if (tables.length <= 1)
1944
- return tables;
1945
- try {
1946
- const rows = await remote.all(`
1947
- SELECT DISTINCT
1948
- tc.table_name AS source_table,
1949
- ccu.table_name AS referenced_table
1950
- FROM information_schema.table_constraints tc
1951
- JOIN information_schema.constraint_column_usage ccu
1952
- ON tc.constraint_name = ccu.constraint_name
1953
- AND tc.table_schema = ccu.table_schema
1954
- WHERE tc.constraint_type = 'FOREIGN KEY'
1955
- AND tc.table_schema = 'public'
1956
- `);
1957
- if (rows.length > 0)
1958
- return topoSort(tables, rows);
1959
- } catch {}
1960
- return tables;
1961
- }
1962
- function topoSort(tables, foreignKeys) {
1963
- const allowed = new Set(tables);
1964
- const deps = new Map;
1965
- for (const table of tables)
1966
- deps.set(table, new Set);
1967
- for (const fk of foreignKeys) {
1968
- if (allowed.has(fk.source_table) && allowed.has(fk.referenced_table)) {
1969
- deps.get(fk.source_table)?.add(fk.referenced_table);
1970
- }
1971
- }
1972
- const sorted = [];
1973
- const visited = new Set;
1974
- const visiting = new Set;
1975
- function visit(table) {
1976
- if (visited.has(table))
1977
- return;
1978
- if (visiting.has(table)) {
1979
- visited.add(table);
1980
- sorted.push(table);
1981
- return;
1982
- }
1983
- visiting.add(table);
1984
- for (const dep of deps.get(table) ?? [])
1985
- visit(dep);
1986
- visiting.delete(table);
1987
- visited.add(table);
1988
- sorted.push(table);
1989
- }
1990
- for (const table of tables)
1991
- visit(table);
1992
- return sorted;
1993
- }
1994
- async function resolvePrimaryKeys(source, target, table, option) {
1995
- if (option)
1996
- return Array.isArray(option) ? option : [option];
1997
- const sourceKeys = await detectPrimaryKeys(source, table);
1998
- if (sourceKeys.length > 0)
1999
- return sourceKeys;
2000
- return detectPrimaryKeys(target, table);
2001
- }
2002
- async function detectPrimaryKeys(adapter, table) {
2003
- if (isAsyncAdapter(adapter)) {
2004
- try {
2005
- const rows = await adapter.all(`
2006
- SELECT kcu.column_name, kcu.ordinal_position
2007
- FROM information_schema.table_constraints tc
2008
- JOIN information_schema.key_column_usage kcu
2009
- ON tc.constraint_name = kcu.constraint_name
2010
- AND tc.table_schema = kcu.table_schema
2011
- WHERE tc.constraint_type = 'PRIMARY KEY'
2012
- AND tc.table_schema = 'public'
2013
- AND tc.table_name = ?
2014
- ORDER BY kcu.ordinal_position
2015
- `, table);
2016
- return rows.map((row) => row.column_name);
2017
- } catch {
2018
- return [];
2019
- }
2020
- }
2021
- try {
2022
- const rows = adapter.all(`PRAGMA table_info(${quoteIdent(table)})`);
2023
- return rows.filter((row) => row.pk > 0).sort((a, b) => a.pk - b.pk).map((row) => row.name);
2024
- } catch {
2025
- return [];
2026
- }
2027
- }
2028
- async function ensureTablesExist(source, target, tables) {
2029
- if (!isAsyncAdapter(source) || isAsyncAdapter(target))
2030
- return;
2031
- for (const table of tables)
2032
- await ensureTableInSqliteFromPg(target, source, table);
2033
- }
2034
- async function ensureTableInSqliteFromPg(target, source, table) {
2035
- const existing = target.all(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table);
2036
- if (existing.length > 0)
2037
- return;
2038
- const columns = await source.all(`
2039
- SELECT column_name, data_type, is_nullable
2040
- FROM information_schema.columns
2041
- WHERE table_schema = 'public' AND table_name = ?
2042
- ORDER BY ordinal_position
2043
- `, table);
2044
- if (columns.length === 0)
2045
- return;
2046
- const primaryKeys = new Set(await detectPrimaryKeys(source, table));
2047
- const definitions = columns.filter((column) => !["tsvector", "tsquery"].includes(column.data_type.toLowerCase())).map((column) => {
2048
- const type = pgTypeToSqlite(column.data_type);
2049
- const notNull = column.is_nullable === "NO" && !primaryKeys.has(column.column_name) ? " NOT NULL" : "";
2050
- return `${quoteIdent(column.column_name)} ${type}${notNull}`;
2051
- });
2052
- if (primaryKeys.size > 0) {
2053
- definitions.push(`PRIMARY KEY (${[...primaryKeys].map(quoteIdent).join(", ")})`);
2054
- }
2055
- target.exec(`CREATE TABLE IF NOT EXISTS ${quoteIdent(table)} (${definitions.join(", ")})`);
2056
- }
2057
- function pgTypeToSqlite(pgType) {
2058
- const type = pgType.toLowerCase();
2059
- if (type.includes("int") || ["bigint", "smallint", "serial", "bigserial"].includes(type))
2060
- return "INTEGER";
2061
- if (type.includes("bool"))
2062
- return "INTEGER";
2063
- if (type.includes("float") || type.includes("double") || ["real", "numeric", "decimal"].includes(type))
2064
- return "REAL";
2065
- if (type === "bytea")
2066
- return "BLOB";
2067
- return "TEXT";
2068
- }
2069
- async function filterColumnsForTarget(target, table, columns) {
2070
- if (columns.includes("machine_id") && table !== "machines")
2071
- await ensureMachineIdColumnInTarget(target, table);
2072
- try {
2073
- if (isAsyncAdapter(target)) {
2074
- const rows2 = await target.all(`
2075
- SELECT column_name
2076
- FROM information_schema.columns
2077
- WHERE table_schema = 'public' AND table_name = ?
2078
- `, table);
2079
- if (rows2.length === 0)
2080
- return columns;
2081
- const targetColumns2 = new Set(rows2.map((row) => row.column_name));
2082
- return columns.filter((column) => targetColumns2.has(column));
2083
- }
2084
- const rows = target.all(`PRAGMA table_info(${quoteIdent(table)})`);
2085
- if (rows.length === 0)
2086
- return columns;
2087
- const targetColumns = new Set(rows.map((row) => row.name));
2088
- return columns.filter((column) => targetColumns.has(column));
2089
- } catch {
2090
- return columns;
2091
- }
2092
- }
2093
- async function ensureMachineIdColumnInTarget(target, table) {
2094
- if (isAsyncAdapter(target)) {
2095
- const rows2 = await target.all(`
2096
- SELECT column_name
2097
- FROM information_schema.columns
2098
- WHERE table_schema = 'public' AND table_name = ? AND column_name = 'machine_id'
2099
- `, table);
2100
- if (rows2.length === 0)
2101
- await target.exec(`ALTER TABLE ${quoteIdent(table)} ADD COLUMN machine_id TEXT DEFAULT ''`);
2102
- return;
2103
- }
2104
- const rows = target.all(`PRAGMA table_info(${quoteIdent(table)})`);
2105
- if (!rows.some((row) => row.name === "machine_id")) {
2106
- target.exec(`ALTER TABLE ${quoteIdent(table)} ADD COLUMN machine_id TEXT DEFAULT ''`);
2107
- }
2108
- }
2109
- async function syncTransfer(source, target, options, _direction) {
2110
- const { tables, onProgress, batchSize = 100, conflictColumn = "updated_at", primaryKey } = options;
2111
- const results = [];
2112
- const sqliteTarget = isAsyncAdapter(target) ? null : target;
2113
- await ensureTablesExist(source, target, tables);
2114
- if (sqliteTarget) {
2115
- try {
2116
- sqliteTarget.exec("PRAGMA foreign_keys = OFF");
2117
- } catch {}
2118
- }
2119
- try {
2120
- for (let i = 0;i < tables.length; i++) {
2121
- const table = tables[i];
2122
- const result = { table, rowsRead: 0, rowsWritten: 0, rowsSkipped: 0, errors: [] };
2123
- try {
2124
- onProgress?.({ table, phase: "reading", rowsRead: 0, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
2125
- const rows = await readAll(source, `SELECT * FROM ${quoteIdent(table)}`);
2126
- result.rowsRead = rows.length;
2127
- if (rows.length === 0) {
2128
- onProgress?.({ table, phase: "done", rowsRead: 0, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
2129
- results.push(result);
2130
- continue;
2131
- }
2132
- const sourceColumns = Object.keys(rows[0]);
2133
- const columns = await filterColumnsForTarget(target, table, sourceColumns);
2134
- const primaryKeys = await resolvePrimaryKeys(source, target, table, primaryKey);
2135
- if (primaryKeys.length === 0) {
2136
- result.errors.push(`Table "${table}" has no primary key; inserted without conflict handling`);
2137
- for (const batch of batches(rows, batchSize)) {
2138
- await insertBatch(target, table, columns, batch);
2139
- result.rowsWritten += batch.length;
2140
- }
2141
- results.push(result);
2142
- continue;
2143
- }
2144
- const missingKeys = primaryKeys.filter((key) => !columns.includes(key));
2145
- if (missingKeys.length > 0) {
2146
- result.errors.push(`Table "${table}" missing primary key column(s): ${missingKeys.join(", ")}`);
2147
- results.push(result);
2148
- continue;
2149
- }
2150
- onProgress?.({ table, phase: "writing", rowsRead: result.rowsRead, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
2151
- const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
2152
- const newestWinsColumn = columns.includes(conflictColumn) ? conflictColumn : undefined;
2153
- for (const batch of batches(rows, batchSize)) {
2154
- await upsertBatch(target, table, columns, updateColumns, primaryKeys, batch, newestWinsColumn);
2155
- result.rowsWritten += batch.length;
2156
- onProgress?.({ table, phase: "writing", rowsRead: result.rowsRead, rowsWritten: result.rowsWritten, totalTables: tables.length, currentTableIndex: i });
2157
- }
2158
- onProgress?.({ table, phase: "done", rowsRead: result.rowsRead, rowsWritten: result.rowsWritten, totalTables: tables.length, currentTableIndex: i });
2159
- } catch (error) {
2160
- result.errors.push(error instanceof Error ? error.message : String(error));
2161
- }
2162
- results.push(result);
2163
- }
2164
- } finally {
2165
- if (sqliteTarget) {
2166
- try {
2167
- sqliteTarget.exec("PRAGMA foreign_keys = ON");
2168
- } catch {}
2169
- }
2170
- }
2171
- return results;
2172
- }
2173
- function batches(rows, size) {
2174
- const result = [];
2175
- for (let offset = 0;offset < rows.length; offset += size)
2176
- result.push(rows.slice(offset, offset + size));
2177
- return result;
2178
- }
2179
- async function upsertBatch(target, table, columns, updateColumns, primaryKeys, batch, conflictColumn) {
2180
- if (batch.length === 0 || columns.length === 0)
2181
- return;
2182
- const fallbackKey = primaryKeys[0] ?? columns[0] ?? "id";
2183
- const columnList = columns.map(quoteIdent).join(", ");
2184
- const keyList = primaryKeys.map(quoteIdent).join(", ");
2185
- const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
2186
- const whereClause = conflictColumn && updateColumns.includes(conflictColumn) ? ` WHERE ${quoteIdent(table)}.${quoteIdent(conflictColumn)} IS NULL OR EXCLUDED.${quoteIdent(conflictColumn)} >= ${quoteIdent(table)}.${quoteIdent(conflictColumn)}` : "";
2187
- if (isAsyncAdapter(target)) {
2188
- const placeholders2 = batch.map((_, rowIndex) => `(${columns.map((__, columnIndex) => `$${rowIndex * columns.length + columnIndex + 1}`).join(", ")})`).join(", ");
2189
- const params2 = batch.flatMap((row) => columns.map((column) => row[column] ?? null));
2190
- await target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders2}
2191
- ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}${whereClause}`, ...params2);
2192
- return;
2193
- }
2194
- const placeholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
2195
- const params = batch.flatMap((row) => columns.map((column) => coerceForSqlite(row[column])));
2196
- target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders}
2197
- ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}${whereClause}`, ...params);
2198
- }
2199
- async function insertBatch(target, table, columns, batch) {
2200
- if (batch.length === 0 || columns.length === 0)
2201
- return;
2202
- const columnList = columns.map(quoteIdent).join(", ");
2203
- if (isAsyncAdapter(target)) {
2204
- const placeholders2 = batch.map((_, rowIndex) => `(${columns.map((__, columnIndex) => `$${rowIndex * columns.length + columnIndex + 1}`).join(", ")})`).join(", ");
2205
- const params2 = batch.flatMap((row) => columns.map((column) => row[column] ?? null));
2206
- await target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders2}`, ...params2);
2207
- return;
2208
- }
2209
- const placeholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
2210
- const params = batch.flatMap((row) => columns.map((column) => coerceForSqlite(row[column])));
2211
- target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders}`, ...params);
2212
- }
2213
- function coerceForSqlite(value) {
2214
- if (value === null || value === undefined)
2215
- return null;
2216
- if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
2217
- return value;
2218
- if (value instanceof Date)
2219
- return value.toISOString();
2220
- if (Buffer.isBuffer(value) || value instanceof Uint8Array)
2221
- return value;
2222
- if (typeof value === "object")
2223
- return JSON.stringify(value);
2224
- return String(value);
2225
- }
2226
- function isAsyncAdapter(adapter) {
2227
- return adapter instanceof PgAdapterAsync;
2228
- }
2229
- async function readAll(adapter, sql) {
2230
- const rows = adapter.all(sql);
2231
- return rows instanceof Promise ? await rows : rows;
2232
- }
2233
-
2234
- // src/lib/native-storage.ts
2235
- var ECONOMY_STORAGE_TABLES = [
2236
- "requests",
2237
- "sessions",
2238
- "projects",
2239
- "budgets",
2240
- "goals",
2241
- "model_pricing",
2242
- "billing_daily",
2243
- "subscriptions",
2244
- "usage_snapshots",
2245
- "savings_daily",
2246
- "machines"
2247
- ];
2248
- var STORAGE_SYNC_TABLES = ECONOMY_STORAGE_TABLES;
2249
- var STORAGE_TABLES = ECONOMY_STORAGE_TABLES;
2250
- var ECONOMY_STORAGE_ENV = {
2251
- mode: "HASNA_ECONOMY_STORAGE_MODE",
2252
- databaseUrl: "HASNA_ECONOMY_DATABASE_URL",
2253
- syncAuto: "HASNA_ECONOMY_SYNC_AUTO",
2254
- syncPullInterval: "HASNA_ECONOMY_SYNC_PULL_INTERVAL",
2255
- syncIncremental: "HASNA_ECONOMY_SYNC_INCREMENTAL"
2256
- };
2257
- var ECONOMY_STORAGE_FALLBACK_ENV = {
2258
- mode: "ECONOMY_STORAGE_MODE",
2259
- databaseUrl: "ECONOMY_DATABASE_URL",
2260
- syncAuto: "ECONOMY_SYNC_AUTO",
2261
- syncPullInterval: "ECONOMY_SYNC_PULL_INTERVAL",
2262
- syncIncremental: "ECONOMY_SYNC_INCREMENTAL"
2263
- };
2264
- var CANONICAL_ECONOMY_RDS_CLUSTER = "hasna-xyz-infra-apps-prod-postgres";
2265
- var CANONICAL_ECONOMY_RDS_DATABASE = "economy";
2266
- var CANONICAL_ECONOMY_RDS_RUNTIME_PATH = "hasna/xyz/opensource/economy/prod/rds";
2267
- function getCanonicalEconomyRdsConfig() {
2268
- return {
2269
- cluster: CANONICAL_ECONOMY_RDS_CLUSTER,
2270
- database: CANONICAL_ECONOMY_RDS_DATABASE,
2271
- runtimeSecretPath: CANONICAL_ECONOMY_RDS_RUNTIME_PATH,
2272
- primaryEnv: ECONOMY_STORAGE_ENV.databaseUrl,
2273
- fallbackEnv: ECONOMY_STORAGE_FALLBACK_ENV.databaseUrl
2274
- };
2275
- }
2276
- function readStorageEnv(key, env = process.env) {
2277
- const canonical = env[ECONOMY_STORAGE_ENV[key]];
2278
- if (canonical != null && canonical !== "")
2279
- return canonical;
2280
- const fallback = env[ECONOMY_STORAGE_FALLBACK_ENV[key]];
2281
- if (fallback != null && fallback !== "")
2282
- return fallback;
2283
- return;
2284
- }
2285
- function isTruthyStorageEnv(value) {
2286
- return value === "1" || value?.toLowerCase() === "true";
2287
- }
2288
- function getStorageMode(env = process.env) {
2289
- const raw = readStorageEnv("mode", env)?.trim().toLowerCase();
2290
- if (raw === "local" || raw === "remote" || raw === "hybrid")
2291
- return raw;
2292
- return getStorageDatabaseUrl(env) ? "remote" : "local";
2293
- }
2294
- var getEconomyStorageMode = getStorageMode;
2295
- function getStorageEnvName(key, env = process.env) {
2296
- const canonical = ECONOMY_STORAGE_ENV[key];
2297
- const fallback = ECONOMY_STORAGE_FALLBACK_ENV[key];
2298
- const hasCanonical = env[canonical] != null && env[canonical] !== "";
2299
- const hasFallback = env[fallback] != null && env[fallback] !== "";
2300
- return hasCanonical || !hasFallback ? canonical : fallback;
2301
- }
2302
- function getStorageDatabaseEnv(env = process.env) {
2303
- return getStorageEnvName("databaseUrl", env);
2304
- }
2305
- var getEconomyStorageDatabaseEnv = getStorageDatabaseEnv;
2306
- function getStorageDatabaseUrl(env = process.env) {
2307
- return readStorageEnv("databaseUrl", env) ?? null;
2308
- }
2309
- var getEconomyStorageDatabaseUrl = getStorageDatabaseUrl;
2310
- function redactDatabaseUrl(value) {
2311
- return value?.replace(/:[^:@/]+@/, ":***@") ?? null;
2312
- }
2313
- function storageEnvStatus(key, env) {
2314
- const activeName = getStorageEnvName(key, env);
2315
- return {
2316
- name: ECONOMY_STORAGE_ENV[key],
2317
- active_name: activeName,
2318
- configured: env[activeName] != null && env[activeName] !== ""
2319
- };
2320
- }
2321
- function getStorageStatus(env = process.env) {
2322
- const databaseUrl = getStorageDatabaseUrl(env);
2323
- const mode = getStorageMode(env);
2324
- const issues = [];
2325
- if ((mode === "remote" || mode === "hybrid") && !databaseUrl) {
2326
- issues.push(`Missing ${ECONOMY_STORAGE_ENV.databaseUrl}`);
2327
- }
2328
- return {
2329
- ok: issues.length === 0,
2330
- service: "economy",
2331
- mode,
2332
- local_default: mode === "local",
2333
- remote_enabled: mode === "remote" || mode === "hybrid",
2334
- database: {
2335
- configured: Boolean(databaseUrl),
2336
- redacted_url: redactDatabaseUrl(databaseUrl)
2337
- },
2338
- tables: ECONOMY_STORAGE_TABLES,
2339
- env: {
2340
- mode: storageEnvStatus("mode", env),
2341
- databaseUrl: storageEnvStatus("databaseUrl", env),
2342
- syncAuto: storageEnvStatus("syncAuto", env),
2343
- syncPullInterval: storageEnvStatus("syncPullInterval", env),
2344
- syncIncremental: storageEnvStatus("syncIncremental", env)
2345
- },
2346
- canonical: getCanonicalEconomyRdsConfig(),
2347
- issues,
2348
- warnings: [],
2349
- no_network: true
2350
- };
2351
- }
2352
- var getEconomyStorageStatus = getStorageStatus;
2353
- function isStorageAutoEnabled() {
2354
- return isTruthyStorageEnv(readStorageEnv("syncAuto"));
2355
- }
2356
- function getStoragePullIntervalMinutes() {
2357
- const raw = readStorageEnv("syncPullInterval");
2358
- if (!raw)
2359
- return 15;
2360
- const n = Number(raw);
2361
- return Number.isFinite(n) && n > 0 ? n : 15;
2362
- }
2363
- async function getStoragePg() {
2364
- const url = getStorageDatabaseUrl();
2365
- if (!url) {
2366
- throw new Error("Missing HASNA_ECONOMY_DATABASE_URL");
2367
- }
2368
- return new PgAdapterAsync(url);
2369
- }
2370
- async function runStorageMigrations(remote) {
2371
- const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
2372
- for (const sql of PG_MIGRATIONS2) {
2373
- await remote.run(sql);
2374
- }
2375
- }
2376
- function isStorageIncrementalEnabled() {
2377
- return isTruthyStorageEnv(readStorageEnv("syncIncremental"));
2378
- }
2379
- async function storagePush(opts) {
2380
- const remote = await getStoragePg();
2381
- const local = openDatabase(getDbPath(), true);
2382
- try {
2383
- await runStorageMigrations(remote);
2384
- touchMachineRegistry(local, "push");
2385
- const tables = resolveStorageTables(opts?.tables);
2386
- const results = await syncPush(local, remote, { tables, conflictColumn: "updated_at" });
2387
- const rows = results.reduce((sum, result) => sum + result.rowsWritten, 0);
2388
- return { rows, machine: getMachineId() };
2389
- } finally {
2390
- local.close();
2391
- await remote.close();
2392
- }
2393
- }
2394
- async function storagePull(opts) {
2395
- const remote = await getStoragePg();
2396
- const local = openDatabase(getDbPath(), true);
2397
- try {
2398
- await runStorageMigrations(remote);
2399
- const tables = resolveStorageTables(opts?.tables);
2400
- const results = await syncPull(remote, local, { tables, conflictColumn: "updated_at" });
2401
- const rows = results.reduce((sum, result) => sum + result.rowsWritten, 0);
2402
- touchMachineRegistry(local, "pull");
2403
- setLastStoragePull();
2404
- return { rows, machine: getMachineId() };
2405
- } finally {
2406
- local.close();
2407
- await remote.close();
2408
- }
2409
- }
2410
- async function storageSyncFull() {
2411
- const push = await storagePush();
2412
- const pull = await storagePull();
2413
- return { push: push.rows, pull: pull.rows, machine: getMachineId() };
2414
- }
2415
- function setLastStoragePull(at = new Date().toISOString()) {
2416
- const db = openDatabase(undefined, true);
2417
- try {
2418
- db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('storage', 'last_pull_at', ?)`).run(at);
2419
- } finally {
2420
- db.close();
2421
- }
2422
- }
2423
- function getLastStoragePull() {
2424
- const db = openDatabase(undefined, true);
2425
- try {
2426
- const row = db.prepare(`
2427
- SELECT value FROM ingest_state
2428
- WHERE key = 'last_pull_at' AND source = 'storage'
2429
- LIMIT 1
2430
- `).get();
2431
- return row?.value ?? null;
2432
- } finally {
2433
- db.close();
2434
- }
2435
- }
2436
- function shouldPullFromStorage() {
2437
- if (!getStorageDatabaseUrl())
2438
- return false;
2439
- const last = getLastStoragePull();
2440
- if (!last)
2441
- return true;
2442
- const ageMs = Date.now() - new Date(last).getTime();
2443
- return ageMs > getStoragePullIntervalMinutes() * 60000;
2444
- }
2445
- async function maybePullFromStorage() {
2446
- if (!shouldPullFromStorage())
2447
- return false;
2448
- try {
2449
- await storagePull();
2450
- return true;
2451
- } catch {
2452
- return false;
2453
- }
2454
- }
2455
- async function maybePushAfterIngest() {
2456
- if (!isStorageAutoEnabled() || !getStorageDatabaseUrl())
2457
- return false;
2458
- try {
2459
- await storagePush();
2460
- return true;
2461
- } catch {
2462
- return false;
2463
- }
2464
- }
2465
- function touchMachineRegistry(db, direction) {
2466
- const now = new Date().toISOString();
2467
- const machine = getMachineId();
2468
- db.prepare(`
2469
- INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
2470
- VALUES (?, ?, ?, ?, ?, ?, ?)
2471
- ON CONFLICT(machine_id) DO UPDATE SET
2472
- hostname = excluded.hostname,
2473
- last_seen_at = excluded.last_seen_at,
2474
- last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
2475
- last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
2476
- economy_version = excluded.economy_version,
2477
- updated_at = excluded.updated_at
2478
- `).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
2479
- }
2480
- function resolveStorageTables(tables) {
2481
- if (!tables || tables.length === 0)
2482
- return [...ECONOMY_STORAGE_TABLES];
2483
- const allowed = new Set(ECONOMY_STORAGE_TABLES);
2484
- const requested = tables.map((table) => table.trim()).filter(Boolean);
2485
- const invalid = requested.filter((table) => !allowed.has(table));
2486
- if (invalid.length > 0) {
2487
- throw new Error(`Unknown economy sync table(s): ${invalid.join(", ")}`);
2488
- }
2489
- return requested;
2490
- }
2491
- var SCHEDULE_SERVICE_NAME = "hasna-economy-storage-sync";
2492
- var SCHEDULE_CONFIG_DIR = join2(homedir2(), ".hasna", "economy");
2493
- var SCHEDULE_CONFIG_PATH = join2(SCHEDULE_CONFIG_DIR, "storage-sync-schedule.json");
2494
- async function registerStorageSchedule(intervalMinutes) {
2495
- if (!Number.isFinite(intervalMinutes) || intervalMinutes <= 0) {
2496
- throw new Error("Storage sync interval must be greater than 0 minutes");
2497
- }
2498
- mkdirSync2(SCHEDULE_CONFIG_DIR, { recursive: true });
2499
- if (platform() === "darwin") {
2500
- await registerLaunchd(intervalMinutes);
2501
- } else if (platform() === "linux") {
2502
- await registerSystemd(intervalMinutes);
2503
- } else {
2504
- throw new Error(`Automatic economy storage sync is not supported on ${platform()}`);
2505
- }
2506
- writeFileSync(SCHEDULE_CONFIG_PATH, JSON.stringify({ intervalMinutes, updatedAt: new Date().toISOString() }, null, 2));
2507
- }
2508
- async function removeStorageSchedule() {
2509
- if (platform() === "darwin")
2510
- await removeLaunchd();
2511
- if (platform() === "linux")
2512
- await removeSystemd();
2513
- try {
2514
- unlinkSync(SCHEDULE_CONFIG_PATH);
2515
- } catch {}
2516
- }
2517
- async function getStorageScheduleStatus() {
2518
- const mechanism = platform() === "darwin" ? "launchd" : platform() === "linux" ? "systemd" : "none";
2519
- const interval = readScheduleInterval();
2520
- const registered = mechanism === "launchd" ? existsSync2(getLaunchdPlistPath()) : mechanism === "systemd" ? existsSync2(join2(getSystemdDir(), `${SCHEDULE_SERVICE_NAME}.timer`)) : false;
2521
- return {
2522
- registered,
2523
- schedule_minutes: interval,
2524
- cron_expression: interval > 0 ? minutesToCron(interval) : null,
2525
- mechanism
2526
- };
2527
- }
2528
- function readScheduleInterval() {
2529
- try {
2530
- const parsed = JSON.parse(readFileSync2(SCHEDULE_CONFIG_PATH, "utf8"));
2531
- return typeof parsed.intervalMinutes === "number" && parsed.intervalMinutes > 0 ? parsed.intervalMinutes : 0;
2532
- } catch {
2533
- return 0;
2534
- }
2535
- }
2536
- function minutesToCron(minutes) {
2537
- if (minutes < 60)
2538
- return `*/${minutes} * * * *`;
2539
- const hours = Math.floor(minutes / 60);
2540
- const remainder = minutes % 60;
2541
- return remainder === 0 && hours <= 24 ? `0 */${hours} * * *` : `*/${minutes} * * * *`;
2542
- }
2543
- function getModuleDir() {
2544
- return typeof import.meta.dir === "string" ? import.meta.dir : dirname(new URL(import.meta.url).pathname);
2545
- }
2546
- function getBunPath() {
2547
- const candidates = [
2548
- join2(homedir2(), ".bun", "bin", "bun"),
2549
- "/opt/homebrew/bin/bun",
2550
- "/usr/local/bin/bun",
2551
- "/usr/bin/bun"
2552
- ];
2553
- return candidates.find((candidate) => existsSync2(candidate)) ?? "bun";
2554
- }
2555
- function getEconomySyncCommand() {
2556
- const dir = getModuleDir();
2557
- const candidates = [
2558
- join2(dir, "..", "cli", "index.js"),
2559
- join2(dir, "..", "cli", "index.ts")
2560
- ];
2561
- const cliPath = candidates.find((candidate) => existsSync2(candidate));
2562
- return cliPath ? [getBunPath(), "run", cliPath, "storage", "sync"] : ["economy", "storage", "sync"];
2563
- }
2564
- function scheduleEnvironment() {
2565
- const keys = [
2566
- "HASNA_ECONOMY_DATABASE_URL",
2567
- "ECONOMY_DATABASE_URL",
2568
- "HASNA_ECONOMY_STORAGE_MODE",
2569
- "ECONOMY_STORAGE_MODE",
2570
- "HASNA_ECONOMY_DB_PATH",
2571
- "ECONOMY_DB",
2572
- "ECONOMY_MACHINE_ID",
2573
- "HASNA_ECONOMY_SYNC_AUTO",
2574
- "ECONOMY_SYNC_AUTO",
2575
- "HASNA_ECONOMY_SYNC_PULL_INTERVAL",
2576
- "ECONOMY_SYNC_PULL_INTERVAL",
2577
- "HASNA_ECONOMY_SYNC_INCREMENTAL",
2578
- "ECONOMY_SYNC_INCREMENTAL"
2579
- ];
2580
- const env = {
2581
- HOME: homedir2(),
2582
- PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin"
2583
- };
2584
- for (const key of keys) {
2585
- const value = process.env[key];
2586
- if (value)
2587
- env[key] = value;
2588
- }
2589
- return env;
2590
- }
2591
- function xmlEscape(value) {
2592
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
2593
- }
2594
- function getLaunchdPlistPath() {
2595
- return join2(homedir2(), "Library", "LaunchAgents", "com.hasna.economy-storage-sync.plist");
2596
- }
2597
- function createLaunchdPlist(intervalMinutes) {
2598
- const args = getEconomySyncCommand();
2599
- const env = scheduleEnvironment();
2600
- const stdout = join2(SCHEDULE_CONFIG_DIR, "storage-sync.log");
2601
- const stderr = join2(SCHEDULE_CONFIG_DIR, "storage-sync-error.log");
2602
- const programArgs = args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join(`
2603
- `);
2604
- const environment = Object.entries(env).map(([key, value]) => ` <key>${xmlEscape(key)}</key>
2605
- <string>${xmlEscape(value)}</string>`).join(`
2606
- `);
2607
- return `<?xml version="1.0" encoding="UTF-8"?>
2608
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2609
- <plist version="1.0">
2610
- <dict>
2611
- <key>Label</key>
2612
- <string>com.hasna.economy-storage-sync</string>
2613
- <key>ProgramArguments</key>
2614
- <array>
2615
- ${programArgs}
2616
- </array>
2617
- <key>StartInterval</key>
2618
- <integer>${Math.round(intervalMinutes * 60)}</integer>
2619
- <key>RunAtLoad</key>
2620
- <true/>
2621
- <key>StandardOutPath</key>
2622
- <string>${xmlEscape(stdout)}</string>
2623
- <key>StandardErrorPath</key>
2624
- <string>${xmlEscape(stderr)}</string>
2625
- <key>EnvironmentVariables</key>
2626
- <dict>
2627
- ${environment}
2628
- </dict>
2629
- </dict>
2630
- </plist>`;
2631
- }
2632
- async function registerLaunchd(intervalMinutes) {
2633
- const plistPath = getLaunchdPlistPath();
2634
- mkdirSync2(dirname(plistPath), { recursive: true });
2635
- try {
2636
- await Bun.spawn(["launchctl", "unload", plistPath]).exited;
2637
- } catch {}
2638
- writeFileSync(plistPath, createLaunchdPlist(intervalMinutes));
2639
- await Bun.spawn(["launchctl", "load", plistPath]).exited;
2640
- }
2641
- async function removeLaunchd() {
2642
- const plistPath = getLaunchdPlistPath();
2643
- try {
2644
- await Bun.spawn(["launchctl", "unload", plistPath]).exited;
2645
- } catch {}
2646
- try {
2647
- unlinkSync(plistPath);
2648
- } catch {}
2649
- }
2650
- function shellArg(value) {
2651
- return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
2652
- }
2653
- function getSystemdDir() {
2654
- return join2(homedir2(), ".config", "systemd", "user");
2655
- }
2656
- function createSystemdService() {
2657
- const command = getEconomySyncCommand().map(shellArg).join(" ");
2658
- const environment = Object.entries(scheduleEnvironment()).map(([key, value]) => `Environment=${key}=${shellArg(value)}`).join(`
2659
- `);
2660
- return `[Unit]
2661
- Description=Hasna Economy Storage Sync
2662
- After=network.target
2663
-
2664
- [Service]
2665
- Type=oneshot
2666
- ExecStart=${command}
2667
- ${environment}
2668
-
2669
- [Install]
2670
- WantedBy=default.target
2671
- `;
2672
- }
2673
- function createSystemdTimer(intervalMinutes) {
2674
- return `[Unit]
2675
- Description=Hasna Economy Storage Sync Timer
2676
-
2677
- [Timer]
2678
- OnBootSec=${intervalMinutes}min
2679
- OnUnitActiveSec=${intervalMinutes}min
2680
- Persistent=true
2681
-
2682
- [Install]
2683
- WantedBy=timers.target
2684
- `;
2685
- }
2686
- async function registerSystemd(intervalMinutes) {
2687
- const dir = getSystemdDir();
2688
- mkdirSync2(dir, { recursive: true });
2689
- writeFileSync(join2(dir, `${SCHEDULE_SERVICE_NAME}.service`), createSystemdService());
2690
- writeFileSync(join2(dir, `${SCHEDULE_SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
2691
- await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
2692
- await Bun.spawn(["systemctl", "--user", "enable", "--now", `${SCHEDULE_SERVICE_NAME}.timer`]).exited;
2693
- }
2694
- async function removeSystemd() {
2695
- try {
2696
- await Bun.spawn(["systemctl", "--user", "disable", "--now", `${SCHEDULE_SERVICE_NAME}.timer`]).exited;
2697
- } catch {}
2698
- const dir = getSystemdDir();
2699
- try {
2700
- unlinkSync(join2(dir, `${SCHEDULE_SERVICE_NAME}.service`));
2701
- } catch {}
2702
- try {
2703
- unlinkSync(join2(dir, `${SCHEDULE_SERVICE_NAME}.timer`));
2704
- } catch {}
2705
- try {
2706
- await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
2707
- } catch {}
2708
- }
2709
- export {
2710
- syncPush,
2711
- syncPull,
2712
- storageSyncFull,
2713
- storagePush,
2714
- storagePull,
2715
- shouldPullFromStorage,
2716
- setLastStoragePull,
2717
- runStorageMigrations,
2718
- removeStorageSchedule,
2719
- registerStorageSchedule,
2720
- maybePushAfterIngest,
2721
- maybePullFromStorage,
2722
- isStorageIncrementalEnabled,
2723
- isStorageAutoEnabled,
2724
- getStorageStatus,
2725
- getStorageScheduleStatus,
2726
- getStoragePullIntervalMinutes,
2727
- getStoragePg,
2728
- getStorageMode,
2729
- getStorageDatabaseUrl,
2730
- getStorageDatabaseEnv,
2731
- getLastStoragePull,
2732
- getEconomyStorageStatus,
2733
- getEconomyStorageMode,
2734
- getEconomyStorageDatabaseUrl,
2735
- getEconomyStorageDatabaseEnv,
2736
- getCanonicalEconomyRdsConfig,
2737
- STORAGE_TABLES,
2738
- STORAGE_SYNC_TABLES,
2739
- PgAdapterAsync,
2740
- ECONOMY_STORAGE_TABLES,
2741
- ECONOMY_STORAGE_FALLBACK_ENV,
2742
- ECONOMY_STORAGE_ENV,
2743
- CANONICAL_ECONOMY_RDS_RUNTIME_PATH,
2744
- CANONICAL_ECONOMY_RDS_DATABASE,
2745
- CANONICAL_ECONOMY_RDS_CLUSTER
2746
- };