@hasna/economy 0.2.7 → 0.2.8

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/index.js ADDED
@@ -0,0 +1,1010 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+
18
+ // src/lib/pricing.ts
19
+ var exports_pricing = {};
20
+ __export(exports_pricing, {
21
+ normalizeModelName: () => normalizeModelName,
22
+ getPricingFromDb: () => getPricingFromDb,
23
+ getPricing: () => getPricing,
24
+ ensurePricingSeeded: () => ensurePricingSeeded,
25
+ computeCostFromDb: () => computeCostFromDb,
26
+ computeCost: () => computeCost,
27
+ DEFAULT_PRICING: () => DEFAULT_PRICING
28
+ });
29
+ function normalizeModelName(raw) {
30
+ return raw.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "").toLowerCase();
31
+ }
32
+ function ensurePricingSeeded(db) {
33
+ seedModelPricing(db, DEFAULT_PRICING);
34
+ }
35
+ function getPricingFromDb(db, model) {
36
+ const normalized = normalizeModelName(model);
37
+ const row = getModelPricing(db, normalized);
38
+ if (row) {
39
+ return {
40
+ inputPer1M: row.input_per_1m,
41
+ outputPer1M: row.output_per_1m,
42
+ cacheReadPer1M: row.cache_read_per_1m,
43
+ cacheWritePer1M: row.cache_write_per_1m
44
+ };
45
+ }
46
+ const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
47
+ for (const r of allRows) {
48
+ if (normalized.startsWith(r.model)) {
49
+ return { inputPer1M: r.input_per_1m, outputPer1M: r.output_per_1m, cacheReadPer1M: r.cache_read_per_1m, cacheWritePer1M: r.cache_write_per_1m };
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+ function getPricing(model) {
55
+ const normalized = normalizeModelName(model);
56
+ if (DEFAULT_PRICING[normalized])
57
+ return DEFAULT_PRICING[normalized] ?? null;
58
+ for (const key of Object.keys(DEFAULT_PRICING)) {
59
+ if (normalized.startsWith(key))
60
+ return DEFAULT_PRICING[key] ?? null;
61
+ }
62
+ return null;
63
+ }
64
+ function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
65
+ const pricing = getPricing(model);
66
+ if (!pricing)
67
+ return 0;
68
+ return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
69
+ }
70
+ function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
71
+ const pricing = getPricingFromDb(db, model) ?? getPricing(model);
72
+ if (!pricing)
73
+ return 0;
74
+ return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
75
+ }
76
+ var DEFAULT_PRICING;
77
+ var init_pricing = __esm(() => {
78
+ init_database();
79
+ DEFAULT_PRICING = {
80
+ "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
81
+ "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
82
+ "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
83
+ "claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
84
+ "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
85
+ "claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
86
+ "claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
87
+ "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
88
+ "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
89
+ "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
90
+ "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
91
+ "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
92
+ "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
93
+ "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
94
+ "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
95
+ "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
96
+ "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
97
+ "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
98
+ "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
99
+ o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
100
+ "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
101
+ o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
102
+ "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
103
+ "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
104
+ };
105
+ });
106
+
107
+ // src/db/database.ts
108
+ import { SqliteAdapter as Database } from "@hasna/cloud";
109
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
110
+ import { homedir } from "os";
111
+ import { join } from "path";
112
+ function getDataDir() {
113
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
114
+ const newDir = join(home, ".hasna", "economy");
115
+ const oldDir = join(home, ".economy");
116
+ if (existsSync(oldDir) && !existsSync(newDir)) {
117
+ mkdirSync(newDir, { recursive: true });
118
+ for (const file of readdirSync(oldDir)) {
119
+ const oldPath = join(oldDir, file);
120
+ if (statSync(oldPath).isFile()) {
121
+ copyFileSync(oldPath, join(newDir, file));
122
+ }
123
+ }
124
+ }
125
+ mkdirSync(newDir, { recursive: true });
126
+ return newDir;
127
+ }
128
+ function getDbPath() {
129
+ if (process.env["HASNA_ECONOMY_DB_PATH"])
130
+ return process.env["HASNA_ECONOMY_DB_PATH"];
131
+ if (process.env["ECONOMY_DB"])
132
+ return process.env["ECONOMY_DB"];
133
+ return join(getDataDir(), "economy.db");
134
+ }
135
+ function openDatabase(dbPath, skipSeed = false) {
136
+ const path = dbPath ?? getDbPath();
137
+ if (path !== ":memory:") {
138
+ const dir = path.substring(0, path.lastIndexOf("/"));
139
+ if (dir && !existsSync(dir))
140
+ mkdirSync(dir, { recursive: true });
141
+ }
142
+ const db = new Database(path);
143
+ db.exec("PRAGMA journal_mode = WAL");
144
+ db.exec("PRAGMA foreign_keys = ON");
145
+ initSchema(db);
146
+ if (!skipSeed) {
147
+ Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
148
+ }
149
+ return db;
150
+ }
151
+ function initSchema(db) {
152
+ db.exec(`
153
+ CREATE TABLE IF NOT EXISTS requests (
154
+ id TEXT PRIMARY KEY,
155
+ agent TEXT NOT NULL,
156
+ session_id TEXT NOT NULL,
157
+ model TEXT NOT NULL,
158
+ input_tokens INTEGER DEFAULT 0,
159
+ output_tokens INTEGER DEFAULT 0,
160
+ cache_read_tokens INTEGER DEFAULT 0,
161
+ cache_create_tokens INTEGER DEFAULT 0,
162
+ cost_usd REAL NOT NULL DEFAULT 0,
163
+ duration_ms INTEGER DEFAULT 0,
164
+ timestamp TEXT NOT NULL,
165
+ source_request_id TEXT
166
+ );
167
+
168
+ CREATE TABLE IF NOT EXISTS sessions (
169
+ id TEXT PRIMARY KEY,
170
+ agent TEXT NOT NULL,
171
+ project_path TEXT DEFAULT '',
172
+ project_name TEXT DEFAULT '',
173
+ started_at TEXT NOT NULL,
174
+ ended_at TEXT,
175
+ total_cost_usd REAL DEFAULT 0,
176
+ total_tokens INTEGER DEFAULT 0,
177
+ request_count INTEGER DEFAULT 0
178
+ );
179
+
180
+ CREATE TABLE IF NOT EXISTS projects (
181
+ id TEXT PRIMARY KEY,
182
+ path TEXT UNIQUE NOT NULL,
183
+ name TEXT NOT NULL,
184
+ description TEXT,
185
+ tags TEXT DEFAULT '[]',
186
+ created_at TEXT NOT NULL
187
+ );
188
+
189
+ CREATE TABLE IF NOT EXISTS budgets (
190
+ id TEXT PRIMARY KEY,
191
+ project_path TEXT,
192
+ agent TEXT,
193
+ period TEXT NOT NULL,
194
+ limit_usd REAL NOT NULL,
195
+ alert_at_percent INTEGER DEFAULT 80,
196
+ created_at TEXT NOT NULL,
197
+ updated_at TEXT NOT NULL
198
+ );
199
+
200
+ CREATE TABLE IF NOT EXISTS goals (
201
+ id TEXT PRIMARY KEY,
202
+ period TEXT NOT NULL,
203
+ project_path TEXT,
204
+ agent TEXT,
205
+ limit_usd REAL NOT NULL,
206
+ created_at TEXT NOT NULL,
207
+ updated_at TEXT NOT NULL
208
+ );
209
+
210
+ CREATE TABLE IF NOT EXISTS ingest_state (
211
+ source TEXT NOT NULL,
212
+ key TEXT NOT NULL,
213
+ value TEXT NOT NULL,
214
+ PRIMARY KEY (source, key)
215
+ );
216
+
217
+ CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
218
+ CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp);
219
+ CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent);
220
+ CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
221
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
222
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
223
+
224
+ CREATE TABLE IF NOT EXISTS model_pricing (
225
+ model TEXT PRIMARY KEY,
226
+ input_per_1m REAL NOT NULL DEFAULT 0,
227
+ output_per_1m REAL NOT NULL DEFAULT 0,
228
+ cache_read_per_1m REAL NOT NULL DEFAULT 0,
229
+ cache_write_per_1m REAL NOT NULL DEFAULT 0,
230
+ updated_at TEXT NOT NULL
231
+ );
232
+
233
+ CREATE TABLE IF NOT EXISTS feedback (
234
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
235
+ message TEXT NOT NULL,
236
+ email TEXT,
237
+ category TEXT DEFAULT 'general',
238
+ version TEXT,
239
+ machine_id TEXT,
240
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
241
+ );
242
+ `);
243
+ }
244
+ function periodWhere(period) {
245
+ switch (period) {
246
+ case "today":
247
+ return `DATE(timestamp) = DATE('now')`;
248
+ case "yesterday":
249
+ return `DATE(timestamp) = DATE('now', '-1 day')`;
250
+ case "week":
251
+ return `timestamp >= DATE('now', '-7 days')`;
252
+ case "month":
253
+ return `timestamp >= DATE('now', '-30 days')`;
254
+ case "year":
255
+ return `timestamp >= DATE('now', '-365 days')`;
256
+ case "all":
257
+ return "1=1";
258
+ }
259
+ }
260
+ function sessionPeriodWhere(period) {
261
+ switch (period) {
262
+ case "today":
263
+ return `DATE(started_at) = DATE('now')`;
264
+ case "yesterday":
265
+ return `DATE(started_at) = DATE('now', '-1 day')`;
266
+ case "week":
267
+ return `started_at >= DATE('now', '-7 days')`;
268
+ case "month":
269
+ return `started_at >= DATE('now', '-30 days')`;
270
+ case "year":
271
+ return `started_at >= DATE('now', '-365 days')`;
272
+ case "all":
273
+ return "1=1";
274
+ }
275
+ }
276
+ function upsertRequest(db, req) {
277
+ db.prepare(`
278
+ INSERT OR REPLACE INTO requests
279
+ (id, agent, session_id, model, input_tokens, output_tokens,
280
+ cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
281
+ timestamp, source_request_id)
282
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
283
+ `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id);
284
+ }
285
+ function upsertSession(db, session) {
286
+ db.prepare(`
287
+ INSERT OR REPLACE INTO sessions
288
+ (id, agent, project_path, project_name, started_at, ended_at,
289
+ total_cost_usd, total_tokens, request_count)
290
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
291
+ `).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);
292
+ }
293
+ function rollupSession(db, sessionId) {
294
+ db.prepare(`
295
+ UPDATE sessions SET
296
+ total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM requests WHERE session_id = ?),
297
+ total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM requests WHERE session_id = ?),
298
+ request_count = (SELECT COUNT(*) FROM requests WHERE session_id = ?),
299
+ ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
300
+ started_at = CASE WHEN started_at = '' OR started_at IS NULL
301
+ THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
302
+ ELSE started_at END
303
+ WHERE id = ?
304
+ `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
305
+ }
306
+ function querySessions(db, filter = {}) {
307
+ const conditions = [];
308
+ const params = [];
309
+ if (filter.agent) {
310
+ conditions.push("agent = ?");
311
+ params.push(filter.agent);
312
+ }
313
+ if (filter.project) {
314
+ conditions.push("project_path LIKE ?");
315
+ params.push(`%${filter.project}%`);
316
+ }
317
+ if (filter.since) {
318
+ conditions.push("started_at >= ?");
319
+ params.push(filter.since);
320
+ }
321
+ if (filter.search) {
322
+ const q = `%${filter.search}%`;
323
+ conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
324
+ params.push(q, q, `${filter.search}%`);
325
+ }
326
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
327
+ const limit = filter.limit ?? 50;
328
+ const offset = filter.offset ?? 0;
329
+ return db.prepare(`
330
+ SELECT * FROM sessions ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?
331
+ `).all(...params, limit, offset);
332
+ }
333
+ function queryTopSessions(db, n = 10, agent) {
334
+ if (agent) {
335
+ return db.prepare(`SELECT * FROM sessions WHERE agent = ? ORDER BY total_cost_usd DESC LIMIT ?`).all(agent, n);
336
+ }
337
+ return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
338
+ }
339
+ function querySummary(db, period) {
340
+ const rWhere = periodWhere(period);
341
+ const sWhere = sessionPeriodWhere(period);
342
+ const r = db.prepare(`
343
+ SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
344
+ COUNT(*) as requests,
345
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
346
+ FROM requests WHERE ${rWhere}
347
+ `).get();
348
+ const codexTotals = db.prepare(`
349
+ SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
350
+ COALESCE(SUM(total_tokens), 0) as tokens,
351
+ COUNT(*) as sessions
352
+ FROM sessions
353
+ WHERE ${sWhere}
354
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
355
+ `).get();
356
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
357
+ return {
358
+ total_usd: r.total_usd + codexTotals.cost_usd,
359
+ requests: r.requests,
360
+ tokens: r.tokens + codexTotals.tokens,
361
+ sessions: sessionCount.sessions,
362
+ period
363
+ };
364
+ }
365
+ function queryModelBreakdown(db) {
366
+ return db.prepare(`
367
+ SELECT model, agent,
368
+ COUNT(*) as requests,
369
+ COALESCE(SUM(input_tokens), 0) as input_tokens,
370
+ COALESCE(SUM(output_tokens), 0) as output_tokens,
371
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
372
+ COALESCE(SUM(cost_usd), 0) as cost_usd
373
+ FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
374
+ `).all();
375
+ }
376
+ function queryProjectBreakdown(db) {
377
+ return db.prepare(`
378
+ SELECT
379
+ s.project_path,
380
+ COALESCE(p.name, s.project_name) as project_name,
381
+ COUNT(DISTINCT s.id) as sessions,
382
+ COUNT(r.id) as requests,
383
+ COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
384
+ COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
385
+ MAX(s.started_at) as last_active
386
+ FROM sessions s
387
+ LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
388
+ LEFT JOIN requests r ON r.session_id = s.id
389
+ WHERE s.project_path != '' OR s.project_name != ''
390
+ GROUP BY s.project_path
391
+ ORDER BY cost_usd DESC
392
+ `).all();
393
+ }
394
+ function queryDailyBreakdown(db, days = 30) {
395
+ return db.prepare(`
396
+ SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
397
+ FROM requests
398
+ WHERE timestamp >= DATE('now', ? || ' days')
399
+ GROUP BY DATE(timestamp), agent
400
+ ORDER BY date ASC
401
+ `).all(`-${days}`);
402
+ }
403
+ function upsertProject(db, project) {
404
+ db.prepare(`
405
+ INSERT OR REPLACE INTO projects (id, path, name, description, tags, created_at)
406
+ VALUES (?, ?, ?, ?, ?, ?)
407
+ `).run(project.id, project.path, project.name, project.description ?? null, JSON.stringify(project.tags), project.created_at);
408
+ }
409
+ function getProject(db, path) {
410
+ const row = db.prepare(`SELECT * FROM projects WHERE path = ?`).get(path);
411
+ if (!row)
412
+ return null;
413
+ return { ...row, tags: JSON.parse(row["tags"] ?? "[]") };
414
+ }
415
+ function listProjects(db) {
416
+ return db.prepare(`SELECT * FROM projects ORDER BY created_at DESC`).all().map((row) => ({ ...row, tags: JSON.parse(row["tags"] ?? "[]") }));
417
+ }
418
+ function deleteProject(db, path) {
419
+ db.prepare(`DELETE FROM projects WHERE path = ?`).run(path);
420
+ }
421
+ function upsertBudget(db, budget) {
422
+ db.prepare(`
423
+ INSERT OR REPLACE INTO budgets
424
+ (id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
425
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
426
+ `).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);
427
+ }
428
+ function listBudgets(db) {
429
+ return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
430
+ }
431
+ function deleteBudget(db, id) {
432
+ db.prepare(`DELETE FROM budgets WHERE id = ?`).run(id);
433
+ }
434
+ function getBudgetStatuses(db) {
435
+ const budgets = listBudgets(db);
436
+ return budgets.map((b) => {
437
+ const periodStart = b.period === "daily" ? "DATE('now')" : b.period === "weekly" ? "DATE('now', '-7 days')" : "DATE('now', '-30 days')";
438
+ let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
439
+ const params = [];
440
+ if (b.project_path) {
441
+ spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
442
+ params.push(b.project_path);
443
+ }
444
+ if (b.agent) {
445
+ spendQuery += ` AND agent = ?`;
446
+ params.push(b.agent);
447
+ }
448
+ const row = db.prepare(spendQuery).get(...params);
449
+ const spend = row.spend;
450
+ const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
451
+ return {
452
+ ...b,
453
+ current_spend_usd: spend,
454
+ percent_used: percent,
455
+ is_over_limit: percent >= 100,
456
+ is_over_alert: percent >= b.alert_at_percent
457
+ };
458
+ });
459
+ }
460
+ function upsertGoal(db, goal) {
461
+ db.prepare(`
462
+ INSERT OR REPLACE INTO goals
463
+ (id, period, project_path, agent, limit_usd, created_at, updated_at)
464
+ VALUES (?, ?, ?, ?, ?, ?, ?)
465
+ `).run(goal.id, goal.period, goal.project_path ?? null, goal.agent ?? null, goal.limit_usd, goal.created_at, goal.updated_at);
466
+ }
467
+ function deleteGoal(db, id) {
468
+ db.prepare(`DELETE FROM goals WHERE id = ?`).run(id);
469
+ }
470
+ function listGoals(db) {
471
+ return db.prepare(`SELECT * FROM goals ORDER BY created_at DESC`).all();
472
+ }
473
+ function getGoalStatuses(db) {
474
+ const goals = listGoals(db);
475
+ return goals.map((g) => {
476
+ 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')";
477
+ let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
478
+ const params = [];
479
+ if (g.project_path) {
480
+ spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
481
+ params.push(g.project_path);
482
+ }
483
+ if (g.agent) {
484
+ spendQuery += ` AND agent = ?`;
485
+ params.push(g.agent);
486
+ }
487
+ const row = db.prepare(spendQuery).get(...params);
488
+ const spend = row.spend;
489
+ const percent = g.limit_usd > 0 ? spend / g.limit_usd * 100 : 0;
490
+ return {
491
+ ...g,
492
+ current_spend_usd: spend,
493
+ percent_used: percent,
494
+ is_on_track: percent < 70,
495
+ is_at_risk: percent >= 70 && percent <= 100,
496
+ is_over: percent > 100
497
+ };
498
+ });
499
+ }
500
+ function getIngestState(db, source, key) {
501
+ const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
502
+ return row?.value ?? null;
503
+ }
504
+ function setIngestState(db, source, key, value) {
505
+ db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
506
+ }
507
+ function queryRequestsSince(db, since) {
508
+ return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
509
+ }
510
+ function upsertModelPricing(db, p) {
511
+ db.prepare(`
512
+ INSERT OR REPLACE INTO model_pricing
513
+ (model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
514
+ VALUES (?, ?, ?, ?, ?, ?)
515
+ `).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.updated_at);
516
+ }
517
+ function getModelPricing(db, model) {
518
+ return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
519
+ }
520
+ function listModelPricing(db) {
521
+ return db.prepare(`SELECT * FROM model_pricing ORDER BY model ASC`).all();
522
+ }
523
+ function deleteModelPricing(db, model) {
524
+ db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
525
+ }
526
+ function seedModelPricing(db, defaults) {
527
+ const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
528
+ if (existing.count > 0)
529
+ return;
530
+ const now = new Date().toISOString();
531
+ for (const [model, p] of Object.entries(defaults)) {
532
+ upsertModelPricing(db, {
533
+ model,
534
+ input_per_1m: p.inputPer1M,
535
+ output_per_1m: p.outputPer1M,
536
+ cache_read_per_1m: p.cacheReadPer1M,
537
+ cache_write_per_1m: p.cacheWritePer1M,
538
+ updated_at: now
539
+ });
540
+ }
541
+ }
542
+ var init_database = () => {};
543
+
544
+ // src/index.ts
545
+ init_database();
546
+ init_pricing();
547
+
548
+ // src/lib/gatherer.ts
549
+ init_database();
550
+ var SYSTEM_PROMPT = "You are a cost-aware AI assistant that tracks API usage, identifies expensive patterns, and helps optimize spending.";
551
+ var gatherTrainingData = async (options = {}) => {
552
+ const limit = options.limit ?? 500;
553
+ const examples = [];
554
+ try {
555
+ const db = openDatabase();
556
+ const periods = ["today", "week", "month", "all"];
557
+ for (const period of periods) {
558
+ try {
559
+ const s = querySummary(db, period);
560
+ examples.push({
561
+ messages: [
562
+ { role: "system", content: SYSTEM_PROMPT },
563
+ { role: "user", content: `What did I spend on AI ${period === "all" ? "in total" : period}?` },
564
+ {
565
+ role: "assistant",
566
+ content: `${period === "all" ? "Total" : period.charAt(0).toUpperCase() + period.slice(1)} AI spending: $${s.total_usd.toFixed(4)} across ${s.sessions} session(s), ${s.requests} request(s), ${s.tokens.toLocaleString()} tokens.`
567
+ }
568
+ ]
569
+ });
570
+ } catch {}
571
+ }
572
+ const sessions = querySessions(db, {
573
+ limit: Math.min(Math.floor(limit / 4), 50),
574
+ since: options.since?.toISOString().substring(0, 10)
575
+ });
576
+ for (const s of sessions) {
577
+ examples.push({
578
+ messages: [
579
+ { role: "system", content: SYSTEM_PROMPT },
580
+ {
581
+ role: "user",
582
+ content: `How much did the session "${s.id.substring(0, 12)}" cost?`
583
+ },
584
+ {
585
+ role: "assistant",
586
+ content: `Session ${s.id.substring(0, 12)} (${s.agent}, project: ${s.project_name || "unknown"}): $${s.total_cost_usd.toFixed(4)}, ${s.total_tokens.toLocaleString()} tokens, ${s.request_count} requests. Started: ${s.started_at.substring(0, 16)}.`
587
+ }
588
+ ]
589
+ });
590
+ examples.push({
591
+ messages: [
592
+ { role: "system", content: SYSTEM_PROMPT },
593
+ {
594
+ role: "user",
595
+ content: `What was the token usage for session ${s.id.substring(0, 12)}?`
596
+ },
597
+ {
598
+ role: "assistant",
599
+ content: `Session ${s.id.substring(0, 12)} used ${s.total_tokens.toLocaleString()} tokens across ${s.request_count} requests on project "${s.project_name || "unknown"}" (${s.agent}).`
600
+ }
601
+ ]
602
+ });
603
+ if (examples.length >= limit)
604
+ break;
605
+ }
606
+ const modelBreakdown = queryModelBreakdown(db);
607
+ if (modelBreakdown.length > 0) {
608
+ const topModels = modelBreakdown.slice(0, 5);
609
+ examples.push({
610
+ messages: [
611
+ { role: "system", content: SYSTEM_PROMPT },
612
+ { role: "user", content: "Which AI models have I spent the most on?" },
613
+ {
614
+ role: "assistant",
615
+ content: `Model cost breakdown (top ${topModels.length}):
616
+ ${topModels.map((m) => `- ${m.model} (${m.agent}): $${m.cost_usd.toFixed(4)}, ${m.requests} requests, ${m.total_tokens.toLocaleString()} tokens`).join(`
617
+ `)}`
618
+ }
619
+ ]
620
+ });
621
+ for (const m of topModels) {
622
+ examples.push({
623
+ messages: [
624
+ { role: "system", content: SYSTEM_PROMPT },
625
+ { role: "user", content: `How much have I spent on ${m.model}?` },
626
+ {
627
+ role: "assistant",
628
+ content: `${m.model} (${m.agent}): $${m.cost_usd.toFixed(4)} total across ${m.requests.toLocaleString()} requests and ${m.total_tokens.toLocaleString()} tokens.`
629
+ }
630
+ ]
631
+ });
632
+ }
633
+ }
634
+ const projectBreakdown = queryProjectBreakdown(db);
635
+ if (projectBreakdown.length > 0) {
636
+ const topProjects = projectBreakdown.slice(0, 5);
637
+ examples.push({
638
+ messages: [
639
+ { role: "system", content: SYSTEM_PROMPT },
640
+ { role: "user", content: "Which projects are costing the most?" },
641
+ {
642
+ role: "assistant",
643
+ content: `Project cost breakdown (top ${topProjects.length}):
644
+ ${topProjects.map((p) => `- ${p.project_name || "unknown"}: $${p.cost_usd.toFixed(4)}, ${p.sessions} sessions`).join(`
645
+ `)}`
646
+ }
647
+ ]
648
+ });
649
+ for (const p of topProjects.slice(0, 3)) {
650
+ examples.push({
651
+ messages: [
652
+ { role: "system", content: SYSTEM_PROMPT },
653
+ { role: "user", content: `What is the AI spend for project "${p.project_name}"?` },
654
+ {
655
+ role: "assistant",
656
+ content: `Project "${p.project_name}": $${p.cost_usd.toFixed(4)} across ${p.sessions} session(s) and ${p.requests.toLocaleString()} requests. Last active: ${p.last_active?.substring(0, 10) ?? "unknown"}.`
657
+ }
658
+ ]
659
+ });
660
+ }
661
+ }
662
+ try {
663
+ const budgets = getBudgetStatuses(db);
664
+ if (budgets.length > 0) {
665
+ examples.push({
666
+ messages: [
667
+ { role: "system", content: SYSTEM_PROMPT },
668
+ { role: "user", content: "How am I tracking against my AI spending budgets?" },
669
+ {
670
+ role: "assistant",
671
+ content: `Budget status:
672
+ ${budgets.map((b) => `- ${b.project_path ?? "global"} (${b.period}): $${b.current_spend_usd.toFixed(4)} / $${b.limit_usd.toFixed(2)} (${b.percent_used.toFixed(1)}%) \u2014 ${b.is_over_limit ? "OVER LIMIT" : b.is_over_alert ? "ALERT" : "OK"}`).join(`
673
+ `)}`
674
+ }
675
+ ]
676
+ });
677
+ }
678
+ } catch {}
679
+ try {
680
+ const goals = getGoalStatuses(db);
681
+ if (goals.length > 0) {
682
+ examples.push({
683
+ messages: [
684
+ { role: "system", content: SYSTEM_PROMPT },
685
+ { role: "user", content: "Am I on track with my AI cost reduction goals?" },
686
+ {
687
+ role: "assistant",
688
+ content: `Goal progress:
689
+ ${goals.map((g) => `- ${g.period} goal (${g.project_path ?? g.agent ?? "global"}): $${g.current_spend_usd.toFixed(4)} / $${g.limit_usd.toFixed(2)} (${g.percent_used.toFixed(1)}%) \u2014 ${g.is_over ? "OVER" : g.is_at_risk ? "AT RISK" : "ON TRACK"}`).join(`
690
+ `)}`
691
+ }
692
+ ]
693
+ });
694
+ }
695
+ } catch {}
696
+ const weekSummary = querySummary(db, "week");
697
+ const monthSummary = querySummary(db, "month");
698
+ if (weekSummary.total_usd > 0) {
699
+ const projectedMonthly = weekSummary.total_usd / 7 * 30;
700
+ examples.push({
701
+ messages: [
702
+ { role: "system", content: SYSTEM_PROMPT },
703
+ { role: "user", content: "Based on my usage, what will I spend this month?" },
704
+ {
705
+ role: "assistant",
706
+ content: `Based on your last 7 days ($${weekSummary.total_usd.toFixed(4)}), projected monthly spend is approximately $${projectedMonthly.toFixed(2)}. Current month-to-date: $${monthSummary.total_usd.toFixed(4)}.`
707
+ }
708
+ ]
709
+ });
710
+ }
711
+ if (modelBreakdown.length > 1) {
712
+ const expensive = modelBreakdown[0];
713
+ const cheaper = modelBreakdown[modelBreakdown.length - 1];
714
+ examples.push({
715
+ messages: [
716
+ { role: "system", content: SYSTEM_PROMPT },
717
+ { role: "user", content: "How can I reduce my AI API costs?" },
718
+ {
719
+ role: "assistant",
720
+ content: `Your most expensive model is ${expensive.model} at $${expensive.cost_usd.toFixed(4)}. Consider switching some workloads to ${cheaper.model} ($${cheaper.cost_usd.toFixed(4)}) for cost savings. Cache frequently repeated prompts to reduce cache-miss costs.`
721
+ }
722
+ ]
723
+ });
724
+ }
725
+ } catch {}
726
+ const finalExamples = examples.slice(0, limit);
727
+ return { source: "economy", examples: finalExamples, count: finalExamples.length };
728
+ };
729
+ // src/lib/model-config.ts
730
+ init_database();
731
+ import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
732
+ import { join as join2 } from "path";
733
+ var DEFAULT_MODEL = "gpt-4o-mini";
734
+ var CONFIG_PATH = join2(getDataDir(), "config.json");
735
+ function loadConfig() {
736
+ try {
737
+ if (existsSync2(CONFIG_PATH)) {
738
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
739
+ }
740
+ } catch {}
741
+ return {};
742
+ }
743
+ function saveConfig(config) {
744
+ const dir = getDataDir();
745
+ if (!existsSync2(dir))
746
+ mkdirSync2(dir, { recursive: true });
747
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + `
748
+ `);
749
+ }
750
+ function getActiveModel() {
751
+ return loadConfig().activeModel ?? DEFAULT_MODEL;
752
+ }
753
+ function setActiveModel(id) {
754
+ const config = loadConfig();
755
+ config.activeModel = id;
756
+ saveConfig(config);
757
+ }
758
+ function clearActiveModel() {
759
+ const config = loadConfig();
760
+ delete config.activeModel;
761
+ saveConfig(config);
762
+ }
763
+ // src/ingest/claude.ts
764
+ init_database();
765
+ init_pricing();
766
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync3, statSync as statSync2 } from "fs";
767
+ import { homedir as homedir2 } from "os";
768
+ import { join as join3, basename } from "path";
769
+ function autoDetectProject(cwd, projects) {
770
+ return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
771
+ }
772
+ var PROJECTS_DIR = join3(homedir2(), ".claude", "projects");
773
+ function dirNameToPath(dirName) {
774
+ return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
775
+ }
776
+ function collectJsonlFiles(projectDir) {
777
+ const files = [];
778
+ function walk(dir) {
779
+ try {
780
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
781
+ if (entry.isDirectory())
782
+ walk(join3(dir, entry.name));
783
+ else if (entry.name.endsWith(".jsonl"))
784
+ files.push(join3(dir, entry.name));
785
+ }
786
+ } catch {}
787
+ }
788
+ walk(projectDir);
789
+ return files;
790
+ }
791
+ async function ingestClaude(db, verbose = false, _telemetryDir) {
792
+ if (!existsSync3(PROJECTS_DIR)) {
793
+ if (verbose)
794
+ console.log("Claude projects dir not found:", PROJECTS_DIR);
795
+ return { files: 0, requests: 0, sessions: 0 };
796
+ }
797
+ let totalFiles = 0;
798
+ let totalRequests = 0;
799
+ const touchedSessions = new Set;
800
+ const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
801
+ const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
802
+ for (const projectDirEntry of projectDirs) {
803
+ const projectDirPath = join3(PROJECTS_DIR, projectDirEntry.name);
804
+ const projectPath = dirNameToPath(projectDirEntry.name);
805
+ const jsonlFiles = collectJsonlFiles(projectDirPath);
806
+ for (const filePath of jsonlFiles) {
807
+ const stateKey = filePath.replace(PROJECTS_DIR, "");
808
+ let fileMtime = "0";
809
+ try {
810
+ fileMtime = statSync2(filePath).mtimeMs.toString();
811
+ } catch {
812
+ continue;
813
+ }
814
+ const processed = getIngestState(db, "claude", stateKey);
815
+ if (processed === fileMtime)
816
+ continue;
817
+ let lines;
818
+ try {
819
+ lines = readFileSync2(filePath, "utf-8").split(`
820
+ `).filter((l) => l.trim());
821
+ } catch {
822
+ continue;
823
+ }
824
+ const fileBasename = basename(filePath, ".jsonl");
825
+ const isUuid = /^[0-9a-f-]{36}$/.test(fileBasename);
826
+ let sessionId = isUuid ? fileBasename : fileBasename.replace(/^agent-/, "");
827
+ let sessionCwd = projectPath;
828
+ for (const line of lines) {
829
+ let entry;
830
+ try {
831
+ entry = JSON.parse(line);
832
+ } catch {
833
+ continue;
834
+ }
835
+ if (entry.sessionId)
836
+ sessionId = entry.sessionId;
837
+ if (entry.cwd)
838
+ sessionCwd = entry.cwd;
839
+ if (entry.message?.role !== "assistant")
840
+ continue;
841
+ const usage = entry.message.usage;
842
+ if (!usage)
843
+ continue;
844
+ const model = entry.message.model;
845
+ if (!model)
846
+ continue;
847
+ const inputTokens = usage.input_tokens ?? 0;
848
+ const outputTokens = usage.output_tokens ?? 0;
849
+ const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
850
+ const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
851
+ const timestamp = entry.timestamp ?? new Date().toISOString();
852
+ if (inputTokens + outputTokens + cacheWriteTokens === 0)
853
+ continue;
854
+ const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
855
+ const reqId = `claude-${sessionId}-${timestamp}`;
856
+ upsertRequest(db, {
857
+ id: reqId,
858
+ agent: "claude",
859
+ session_id: sessionId,
860
+ model,
861
+ input_tokens: inputTokens,
862
+ output_tokens: outputTokens,
863
+ cache_read_tokens: cacheReadTokens,
864
+ cache_create_tokens: cacheWriteTokens,
865
+ cost_usd: costUsd,
866
+ duration_ms: 0,
867
+ timestamp,
868
+ source_request_id: reqId
869
+ });
870
+ if (!touchedSessions.has(sessionId)) {
871
+ const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
872
+ if (!existing) {
873
+ const effectiveCwd = sessionCwd || projectPath;
874
+ const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
875
+ const session = {
876
+ id: sessionId,
877
+ agent: "claude",
878
+ project_path: detectedProject ? detectedProject.path : effectiveCwd,
879
+ project_name: detectedProject ? detectedProject.name : "",
880
+ started_at: timestamp,
881
+ ended_at: null,
882
+ total_cost_usd: 0,
883
+ total_tokens: 0,
884
+ request_count: 0
885
+ };
886
+ upsertSession(db, session);
887
+ }
888
+ touchedSessions.add(sessionId);
889
+ }
890
+ totalRequests++;
891
+ }
892
+ setIngestState(db, "claude", stateKey, fileMtime);
893
+ totalFiles++;
894
+ }
895
+ }
896
+ for (const sessionId of touchedSessions) {
897
+ rollupSession(db, sessionId);
898
+ }
899
+ return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
900
+ }
901
+ // src/ingest/codex.ts
902
+ init_database();
903
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
904
+ import { homedir as homedir3 } from "os";
905
+ import { join as join4, basename as basename2 } from "path";
906
+ import { Database as Database2 } from "bun:sqlite";
907
+ var CODEX_DB_PATH = join4(homedir3(), ".codex", "state_5.sqlite");
908
+ var CODEX_CONFIG_PATH = join4(homedir3(), ".codex", "config.toml");
909
+ function readCodexModel() {
910
+ if (!existsSync4(CODEX_CONFIG_PATH))
911
+ return "gpt-5.3-codex";
912
+ try {
913
+ const content = readFileSync3(CODEX_CONFIG_PATH, "utf-8");
914
+ const match = content.match(/^model\s*=\s*"([^"]+)"/m);
915
+ return match?.[1] ?? "gpt-5.3-codex";
916
+ } catch {
917
+ return "gpt-5.3-codex";
918
+ }
919
+ }
920
+ async function ingestCodex(db, verbose = false) {
921
+ if (!existsSync4(CODEX_DB_PATH)) {
922
+ if (verbose)
923
+ console.log("Codex DB not found:", CODEX_DB_PATH);
924
+ return { sessions: 0 };
925
+ }
926
+ let codexDb = null;
927
+ let ingested = 0;
928
+ try {
929
+ codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
930
+ const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
931
+ for (const thread of threads) {
932
+ const stateKey = thread.id;
933
+ const processed = getIngestState(db, "codex", stateKey);
934
+ if (processed === "done")
935
+ continue;
936
+ const costUsd = 0;
937
+ const projectPath = thread.cwd ?? "";
938
+ const projectName = projectPath ? basename2(projectPath) : "unknown";
939
+ const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
940
+ const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
941
+ upsertSession(db, {
942
+ id: `codex-${thread.id}`,
943
+ agent: "codex",
944
+ project_path: projectPath,
945
+ project_name: projectName,
946
+ started_at: startedAt,
947
+ ended_at: endedAt,
948
+ total_cost_usd: costUsd,
949
+ total_tokens: thread.tokens_used,
950
+ request_count: 1
951
+ });
952
+ setIngestState(db, "codex", stateKey, "done");
953
+ ingested++;
954
+ if (verbose)
955
+ console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens \u2192 $${costUsd.toFixed(4)}`);
956
+ }
957
+ } finally {
958
+ codexDb?.close();
959
+ }
960
+ return { sessions: ingested };
961
+ }
962
+ export {
963
+ upsertSession,
964
+ upsertRequest,
965
+ upsertProject,
966
+ upsertModelPricing,
967
+ upsertGoal,
968
+ upsertBudget,
969
+ setIngestState,
970
+ setActiveModel,
971
+ seedModelPricing,
972
+ rollupSession,
973
+ readCodexModel,
974
+ queryTopSessions,
975
+ querySummary,
976
+ querySessions,
977
+ queryRequestsSince,
978
+ queryProjectBreakdown,
979
+ queryModelBreakdown,
980
+ queryDailyBreakdown,
981
+ openDatabase,
982
+ normalizeModelName,
983
+ listProjects,
984
+ listModelPricing,
985
+ listGoals,
986
+ listBudgets,
987
+ ingestCodex,
988
+ ingestClaude,
989
+ getProject,
990
+ getPricingFromDb,
991
+ getPricing,
992
+ getModelPricing,
993
+ getIngestState,
994
+ getGoalStatuses,
995
+ getDbPath,
996
+ getDataDir,
997
+ getBudgetStatuses,
998
+ getActiveModel,
999
+ gatherTrainingData,
1000
+ ensurePricingSeeded,
1001
+ deleteProject,
1002
+ deleteModelPricing,
1003
+ deleteGoal,
1004
+ deleteBudget,
1005
+ computeCostFromDb,
1006
+ computeCost,
1007
+ clearActiveModel,
1008
+ DEFAULT_PRICING,
1009
+ DEFAULT_MODEL
1010
+ };