@hasna/economy 0.2.3 → 0.2.4

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 DELETED
@@ -1,674 +0,0 @@
1
- // @bun
2
- var __defProp = Object.defineProperty;
3
- var __export = (target, all) => {
4
- for (var name in all)
5
- __defProp(target, name, {
6
- get: all[name],
7
- enumerable: true,
8
- configurable: true,
9
- set: (newValue) => all[name] = () => newValue
10
- });
11
- };
12
- var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
13
-
14
- // src/lib/pricing.ts
15
- var exports_pricing = {};
16
- __export(exports_pricing, {
17
- normalizeModelName: () => normalizeModelName,
18
- getPricingFromDb: () => getPricingFromDb,
19
- getPricing: () => getPricing,
20
- ensurePricingSeeded: () => ensurePricingSeeded,
21
- computeCostFromDb: () => computeCostFromDb,
22
- computeCost: () => computeCost,
23
- DEFAULT_PRICING: () => DEFAULT_PRICING
24
- });
25
- function normalizeModelName(raw) {
26
- return raw.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "").toLowerCase();
27
- }
28
- function ensurePricingSeeded(db) {
29
- seedModelPricing(db, DEFAULT_PRICING);
30
- }
31
- function getPricingFromDb(db, model) {
32
- const normalized = normalizeModelName(model);
33
- const row = getModelPricing(db, normalized);
34
- if (row) {
35
- return {
36
- inputPer1M: row.input_per_1m,
37
- outputPer1M: row.output_per_1m,
38
- cacheReadPer1M: row.cache_read_per_1m,
39
- cacheWritePer1M: row.cache_write_per_1m
40
- };
41
- }
42
- const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
43
- for (const r of allRows) {
44
- if (normalized.startsWith(r.model)) {
45
- return { inputPer1M: r.input_per_1m, outputPer1M: r.output_per_1m, cacheReadPer1M: r.cache_read_per_1m, cacheWritePer1M: r.cache_write_per_1m };
46
- }
47
- }
48
- return null;
49
- }
50
- function getPricing(model) {
51
- const normalized = normalizeModelName(model);
52
- if (DEFAULT_PRICING[normalized])
53
- return DEFAULT_PRICING[normalized] ?? null;
54
- for (const key of Object.keys(DEFAULT_PRICING)) {
55
- if (normalized.startsWith(key))
56
- return DEFAULT_PRICING[key] ?? null;
57
- }
58
- return null;
59
- }
60
- function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
61
- const pricing = getPricing(model);
62
- if (!pricing)
63
- return 0;
64
- return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
65
- }
66
- function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
67
- const pricing = getPricingFromDb(db, model) ?? getPricing(model);
68
- if (!pricing)
69
- return 0;
70
- return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
71
- }
72
- var DEFAULT_PRICING;
73
- var init_pricing = __esm(() => {
74
- init_database();
75
- DEFAULT_PRICING = {
76
- "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
77
- "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
78
- "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
79
- "claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
80
- "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
81
- "claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
82
- "claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
83
- "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
84
- "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
85
- "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
86
- "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
87
- "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
88
- "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
89
- "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
90
- "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
91
- o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
92
- "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
93
- o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
94
- "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
95
- "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
96
- };
97
- });
98
-
99
- // src/db/database.ts
100
- import { Database } from "bun:sqlite";
101
- import { existsSync, mkdirSync } from "fs";
102
- import { homedir } from "os";
103
- import { join } from "path";
104
- function getDbPath() {
105
- return process.env["ECONOMY_DB"] ?? join(homedir(), ".economy", "economy.db");
106
- }
107
- function openDatabase(dbPath, skipSeed = false) {
108
- const path = dbPath ?? getDbPath();
109
- if (path !== ":memory:") {
110
- const dir = path.substring(0, path.lastIndexOf("/"));
111
- if (dir && !existsSync(dir))
112
- mkdirSync(dir, { recursive: true });
113
- }
114
- const db = new Database(path);
115
- db.exec("PRAGMA journal_mode = WAL");
116
- db.exec("PRAGMA foreign_keys = ON");
117
- initSchema(db);
118
- if (!skipSeed) {
119
- Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
120
- }
121
- return db;
122
- }
123
- function initSchema(db) {
124
- db.exec(`
125
- CREATE TABLE IF NOT EXISTS requests (
126
- id TEXT PRIMARY KEY,
127
- agent TEXT NOT NULL,
128
- session_id TEXT NOT NULL,
129
- model TEXT NOT NULL,
130
- input_tokens INTEGER DEFAULT 0,
131
- output_tokens INTEGER DEFAULT 0,
132
- cache_read_tokens INTEGER DEFAULT 0,
133
- cache_create_tokens INTEGER DEFAULT 0,
134
- cost_usd REAL NOT NULL DEFAULT 0,
135
- duration_ms INTEGER DEFAULT 0,
136
- timestamp TEXT NOT NULL,
137
- source_request_id TEXT
138
- );
139
-
140
- CREATE TABLE IF NOT EXISTS sessions (
141
- id TEXT PRIMARY KEY,
142
- agent TEXT NOT NULL,
143
- project_path TEXT DEFAULT '',
144
- project_name TEXT DEFAULT '',
145
- started_at TEXT NOT NULL,
146
- ended_at TEXT,
147
- total_cost_usd REAL DEFAULT 0,
148
- total_tokens INTEGER DEFAULT 0,
149
- request_count INTEGER DEFAULT 0
150
- );
151
-
152
- CREATE TABLE IF NOT EXISTS projects (
153
- id TEXT PRIMARY KEY,
154
- path TEXT UNIQUE NOT NULL,
155
- name TEXT NOT NULL,
156
- description TEXT,
157
- tags TEXT DEFAULT '[]',
158
- created_at TEXT NOT NULL
159
- );
160
-
161
- CREATE TABLE IF NOT EXISTS budgets (
162
- id TEXT PRIMARY KEY,
163
- project_path TEXT,
164
- agent TEXT,
165
- period TEXT NOT NULL,
166
- limit_usd REAL NOT NULL,
167
- alert_at_percent INTEGER DEFAULT 80,
168
- created_at TEXT NOT NULL,
169
- updated_at TEXT NOT NULL
170
- );
171
-
172
- CREATE TABLE IF NOT EXISTS ingest_state (
173
- source TEXT NOT NULL,
174
- key TEXT NOT NULL,
175
- value TEXT NOT NULL,
176
- PRIMARY KEY (source, key)
177
- );
178
-
179
- CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
180
- CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp);
181
- CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent);
182
- CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
183
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
184
- CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
185
-
186
- CREATE TABLE IF NOT EXISTS model_pricing (
187
- model TEXT PRIMARY KEY,
188
- input_per_1m REAL NOT NULL DEFAULT 0,
189
- output_per_1m REAL NOT NULL DEFAULT 0,
190
- cache_read_per_1m REAL NOT NULL DEFAULT 0,
191
- cache_write_per_1m REAL NOT NULL DEFAULT 0,
192
- updated_at TEXT NOT NULL
193
- );
194
- `);
195
- }
196
- function periodWhere(period) {
197
- switch (period) {
198
- case "today":
199
- return `DATE(timestamp) = DATE('now')`;
200
- case "week":
201
- return `timestamp >= DATE('now', '-7 days')`;
202
- case "month":
203
- return `timestamp >= DATE('now', '-30 days')`;
204
- case "all":
205
- return "1=1";
206
- }
207
- }
208
- function sessionPeriodWhere(period) {
209
- switch (period) {
210
- case "today":
211
- return `DATE(started_at) = DATE('now')`;
212
- case "week":
213
- return `started_at >= DATE('now', '-7 days')`;
214
- case "month":
215
- return `started_at >= DATE('now', '-30 days')`;
216
- case "all":
217
- return "1=1";
218
- }
219
- }
220
- function upsertRequest(db, req) {
221
- db.prepare(`
222
- INSERT OR REPLACE INTO requests
223
- (id, agent, session_id, model, input_tokens, output_tokens,
224
- cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
225
- timestamp, source_request_id)
226
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
227
- `).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);
228
- }
229
- function upsertSession(db, session) {
230
- db.prepare(`
231
- INSERT OR REPLACE INTO sessions
232
- (id, agent, project_path, project_name, started_at, ended_at,
233
- total_cost_usd, total_tokens, request_count)
234
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
235
- `).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);
236
- }
237
- function rollupSession(db, sessionId) {
238
- db.prepare(`
239
- UPDATE sessions SET
240
- total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM requests WHERE session_id = ?),
241
- total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM requests WHERE session_id = ?),
242
- request_count = (SELECT COUNT(*) FROM requests WHERE session_id = ?),
243
- ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
244
- started_at = CASE WHEN started_at = '' OR started_at IS NULL
245
- THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
246
- ELSE started_at END
247
- WHERE id = ?
248
- `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
249
- }
250
- function querySessions(db, filter = {}) {
251
- const conditions = [];
252
- const params = [];
253
- if (filter.agent) {
254
- conditions.push("agent = ?");
255
- params.push(filter.agent);
256
- }
257
- if (filter.project) {
258
- conditions.push("project_path LIKE ?");
259
- params.push(`%${filter.project}%`);
260
- }
261
- if (filter.since) {
262
- conditions.push("started_at >= ?");
263
- params.push(filter.since);
264
- }
265
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
266
- const limit = filter.limit ?? 50;
267
- const offset = filter.offset ?? 0;
268
- return db.prepare(`
269
- SELECT * FROM sessions ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?
270
- `).all(...params, limit, offset);
271
- }
272
- function queryTopSessions(db, n = 10, agent) {
273
- if (agent) {
274
- return db.prepare(`SELECT * FROM sessions WHERE agent = ? ORDER BY total_cost_usd DESC LIMIT ?`).all(agent, n);
275
- }
276
- return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
277
- }
278
- function querySummary(db, period) {
279
- const rWhere = periodWhere(period);
280
- const sWhere = sessionPeriodWhere(period);
281
- const r = db.prepare(`
282
- SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
283
- COUNT(*) as requests,
284
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
285
- FROM requests WHERE ${rWhere}
286
- `).get();
287
- const codexTotals = db.prepare(`
288
- SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
289
- COALESCE(SUM(total_tokens), 0) as tokens,
290
- COUNT(*) as sessions
291
- FROM sessions
292
- WHERE ${sWhere}
293
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
294
- `).get();
295
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
296
- return {
297
- total_usd: r.total_usd + codexTotals.cost_usd,
298
- requests: r.requests,
299
- tokens: r.tokens + codexTotals.tokens,
300
- sessions: sessionCount.sessions,
301
- period
302
- };
303
- }
304
- function queryModelBreakdown(db) {
305
- return db.prepare(`
306
- SELECT model, agent,
307
- COUNT(*) as requests,
308
- COALESCE(SUM(input_tokens), 0) as input_tokens,
309
- COALESCE(SUM(output_tokens), 0) as output_tokens,
310
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
311
- COALESCE(SUM(cost_usd), 0) as cost_usd
312
- FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
313
- `).all();
314
- }
315
- function queryProjectBreakdown(db) {
316
- return db.prepare(`
317
- SELECT project_path, project_name,
318
- COUNT(*) as sessions,
319
- COALESCE(SUM(total_tokens), 0) as total_tokens,
320
- COALESCE(SUM(request_count), 0) as requests,
321
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
322
- MAX(started_at) as last_active
323
- FROM sessions
324
- GROUP BY project_path ORDER BY cost_usd DESC
325
- `).all();
326
- }
327
- function queryDailyBreakdown(db, days = 30) {
328
- return db.prepare(`
329
- SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
330
- FROM requests
331
- WHERE timestamp >= DATE('now', ? || ' days')
332
- GROUP BY DATE(timestamp), agent
333
- ORDER BY date ASC
334
- `).all(`-${days}`);
335
- }
336
- function upsertProject(db, project) {
337
- db.prepare(`
338
- INSERT OR REPLACE INTO projects (id, path, name, description, tags, created_at)
339
- VALUES (?, ?, ?, ?, ?, ?)
340
- `).run(project.id, project.path, project.name, project.description ?? null, JSON.stringify(project.tags), project.created_at);
341
- }
342
- function getProject(db, path) {
343
- const row = db.prepare(`SELECT * FROM projects WHERE path = ?`).get(path);
344
- if (!row)
345
- return null;
346
- return { ...row, tags: JSON.parse(row["tags"] ?? "[]") };
347
- }
348
- function listProjects(db) {
349
- return db.prepare(`SELECT * FROM projects ORDER BY created_at DESC`).all().map((row) => ({ ...row, tags: JSON.parse(row["tags"] ?? "[]") }));
350
- }
351
- function deleteProject(db, path) {
352
- db.prepare(`DELETE FROM projects WHERE path = ?`).run(path);
353
- }
354
- function upsertBudget(db, budget) {
355
- db.prepare(`
356
- INSERT OR REPLACE INTO budgets
357
- (id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
358
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
359
- `).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);
360
- }
361
- function listBudgets(db) {
362
- return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
363
- }
364
- function deleteBudget(db, id) {
365
- db.prepare(`DELETE FROM budgets WHERE id = ?`).run(id);
366
- }
367
- function getBudgetStatuses(db) {
368
- const budgets = listBudgets(db);
369
- return budgets.map((b) => {
370
- const periodStart = b.period === "daily" ? "DATE('now')" : b.period === "weekly" ? "DATE('now', '-7 days')" : "DATE('now', '-30 days')";
371
- let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
372
- const params = [];
373
- if (b.project_path) {
374
- spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
375
- params.push(b.project_path);
376
- }
377
- if (b.agent) {
378
- spendQuery += ` AND agent = ?`;
379
- params.push(b.agent);
380
- }
381
- const row = db.prepare(spendQuery).get(...params);
382
- const spend = row.spend;
383
- const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
384
- return {
385
- ...b,
386
- current_spend_usd: spend,
387
- percent_used: percent,
388
- is_over_limit: percent >= 100,
389
- is_over_alert: percent >= b.alert_at_percent
390
- };
391
- });
392
- }
393
- function getIngestState(db, source, key) {
394
- const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
395
- return row?.value ?? null;
396
- }
397
- function setIngestState(db, source, key, value) {
398
- db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
399
- }
400
- function queryRequestsSince(db, since) {
401
- return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
402
- }
403
- function upsertModelPricing(db, p) {
404
- db.prepare(`
405
- INSERT OR REPLACE INTO model_pricing
406
- (model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
407
- VALUES (?, ?, ?, ?, ?, ?)
408
- `).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.updated_at);
409
- }
410
- function getModelPricing(db, model) {
411
- return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
412
- }
413
- function listModelPricing(db) {
414
- return db.prepare(`SELECT * FROM model_pricing ORDER BY model ASC`).all();
415
- }
416
- function deleteModelPricing(db, model) {
417
- db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
418
- }
419
- function seedModelPricing(db, defaults) {
420
- const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
421
- if (existing.count > 0)
422
- return;
423
- const now = new Date().toISOString();
424
- for (const [model, p] of Object.entries(defaults)) {
425
- upsertModelPricing(db, {
426
- model,
427
- input_per_1m: p.inputPer1M,
428
- output_per_1m: p.outputPer1M,
429
- cache_read_per_1m: p.cacheReadPer1M,
430
- cache_write_per_1m: p.cacheWritePer1M,
431
- updated_at: now
432
- });
433
- }
434
- }
435
- var init_database = () => {};
436
-
437
- // src/index.ts
438
- init_database();
439
- init_pricing();
440
-
441
- // src/ingest/claude.ts
442
- init_database();
443
- init_pricing();
444
- import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
445
- import { homedir as homedir2 } from "os";
446
- import { join as join2, basename } from "path";
447
- var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
448
- function dirNameToPath(dirName) {
449
- return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
450
- }
451
- function collectJsonlFiles(projectDir) {
452
- const files = [];
453
- function walk(dir) {
454
- try {
455
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
456
- if (entry.isDirectory())
457
- walk(join2(dir, entry.name));
458
- else if (entry.name.endsWith(".jsonl"))
459
- files.push(join2(dir, entry.name));
460
- }
461
- } catch {}
462
- }
463
- walk(projectDir);
464
- return files;
465
- }
466
- async function ingestClaude(db, verbose = false, _telemetryDir) {
467
- if (!existsSync2(PROJECTS_DIR)) {
468
- if (verbose)
469
- console.log("Claude projects dir not found:", PROJECTS_DIR);
470
- return { files: 0, requests: 0, sessions: 0 };
471
- }
472
- let totalFiles = 0;
473
- let totalRequests = 0;
474
- const touchedSessions = new Set;
475
- const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
476
- for (const projectDirEntry of projectDirs) {
477
- const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
478
- const projectPath = dirNameToPath(projectDirEntry.name);
479
- const projectName = basename(projectPath);
480
- const jsonlFiles = collectJsonlFiles(projectDirPath);
481
- for (const filePath of jsonlFiles) {
482
- const stateKey = filePath.replace(PROJECTS_DIR, "");
483
- let fileMtime = "0";
484
- try {
485
- fileMtime = statSync(filePath).mtimeMs.toString();
486
- } catch {
487
- continue;
488
- }
489
- const processed = getIngestState(db, "claude", stateKey);
490
- if (processed === fileMtime)
491
- continue;
492
- let lines;
493
- try {
494
- lines = readFileSync(filePath, "utf-8").split(`
495
- `).filter((l) => l.trim());
496
- } catch {
497
- continue;
498
- }
499
- const fileBasename = basename(filePath, ".jsonl");
500
- const isUuid = /^[0-9a-f-]{36}$/.test(fileBasename);
501
- let sessionId = isUuid ? fileBasename : fileBasename.replace(/^agent-/, "");
502
- let sessionCwd = projectPath;
503
- for (const line of lines) {
504
- let entry;
505
- try {
506
- entry = JSON.parse(line);
507
- } catch {
508
- continue;
509
- }
510
- if (entry.sessionId)
511
- sessionId = entry.sessionId;
512
- if (entry.cwd)
513
- sessionCwd = entry.cwd;
514
- if (entry.message?.role !== "assistant")
515
- continue;
516
- const usage = entry.message.usage;
517
- if (!usage)
518
- continue;
519
- const model = entry.message.model;
520
- if (!model)
521
- continue;
522
- const inputTokens = usage.input_tokens ?? 0;
523
- const outputTokens = usage.output_tokens ?? 0;
524
- const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
525
- const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
526
- const timestamp = entry.timestamp ?? new Date().toISOString();
527
- if (inputTokens + outputTokens + cacheWriteTokens === 0)
528
- continue;
529
- const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
530
- const reqId = `claude-${sessionId}-${timestamp}`;
531
- upsertRequest(db, {
532
- id: reqId,
533
- agent: "claude",
534
- session_id: sessionId,
535
- model,
536
- input_tokens: inputTokens,
537
- output_tokens: outputTokens,
538
- cache_read_tokens: cacheReadTokens,
539
- cache_create_tokens: cacheWriteTokens,
540
- cost_usd: costUsd,
541
- duration_ms: 0,
542
- timestamp,
543
- source_request_id: reqId
544
- });
545
- if (!touchedSessions.has(sessionId)) {
546
- const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
547
- if (!existing) {
548
- const session = {
549
- id: sessionId,
550
- agent: "claude",
551
- project_path: sessionCwd || projectPath,
552
- project_name: basename(sessionCwd || projectPath),
553
- started_at: timestamp,
554
- ended_at: null,
555
- total_cost_usd: 0,
556
- total_tokens: 0,
557
- request_count: 0
558
- };
559
- upsertSession(db, session);
560
- }
561
- touchedSessions.add(sessionId);
562
- }
563
- totalRequests++;
564
- }
565
- setIngestState(db, "claude", stateKey, fileMtime);
566
- totalFiles++;
567
- }
568
- }
569
- for (const sessionId of touchedSessions) {
570
- rollupSession(db, sessionId);
571
- }
572
- return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
573
- }
574
- // src/ingest/codex.ts
575
- init_database();
576
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
577
- import { homedir as homedir3 } from "os";
578
- import { join as join3, basename as basename2 } from "path";
579
- import { Database as Database2 } from "bun:sqlite";
580
- var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
581
- var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
582
- function readCodexModel() {
583
- if (!existsSync3(CODEX_CONFIG_PATH))
584
- return "gpt-5.3-codex";
585
- try {
586
- const content = readFileSync2(CODEX_CONFIG_PATH, "utf-8");
587
- const match = content.match(/^model\s*=\s*"([^"]+)"/m);
588
- return match?.[1] ?? "gpt-5.3-codex";
589
- } catch {
590
- return "gpt-5.3-codex";
591
- }
592
- }
593
- async function ingestCodex(db, verbose = false) {
594
- if (!existsSync3(CODEX_DB_PATH)) {
595
- if (verbose)
596
- console.log("Codex DB not found:", CODEX_DB_PATH);
597
- return { sessions: 0 };
598
- }
599
- const model = readCodexModel();
600
- let codexDb = null;
601
- let ingested = 0;
602
- try {
603
- codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
604
- const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
605
- for (const thread of threads) {
606
- const stateKey = thread.id;
607
- const processed = getIngestState(db, "codex", stateKey);
608
- if (processed === "done")
609
- continue;
610
- const costUsd = 0;
611
- const projectPath = thread.cwd ?? "";
612
- const projectName = projectPath ? basename2(projectPath) : "unknown";
613
- const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
614
- const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
615
- upsertSession(db, {
616
- id: `codex-${thread.id}`,
617
- agent: "codex",
618
- project_path: projectPath,
619
- project_name: projectName,
620
- started_at: startedAt,
621
- ended_at: endedAt,
622
- total_cost_usd: costUsd,
623
- total_tokens: thread.tokens_used,
624
- request_count: 1
625
- });
626
- setIngestState(db, "codex", stateKey, "done");
627
- ingested++;
628
- if (verbose)
629
- console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens \u2192 $${costUsd.toFixed(4)}`);
630
- }
631
- } finally {
632
- codexDb?.close();
633
- }
634
- return { sessions: ingested };
635
- }
636
- export {
637
- upsertSession,
638
- upsertRequest,
639
- upsertProject,
640
- upsertModelPricing,
641
- upsertBudget,
642
- setIngestState,
643
- seedModelPricing,
644
- rollupSession,
645
- readCodexModel,
646
- queryTopSessions,
647
- querySummary,
648
- querySessions,
649
- queryRequestsSince,
650
- queryProjectBreakdown,
651
- queryModelBreakdown,
652
- queryDailyBreakdown,
653
- openDatabase,
654
- normalizeModelName,
655
- listProjects,
656
- listModelPricing,
657
- listBudgets,
658
- ingestCodex,
659
- ingestClaude,
660
- getProject,
661
- getPricingFromDb,
662
- getPricing,
663
- getModelPricing,
664
- getIngestState,
665
- getDbPath,
666
- getBudgetStatuses,
667
- ensurePricingSeeded,
668
- deleteProject,
669
- deleteModelPricing,
670
- deleteBudget,
671
- computeCostFromDb,
672
- computeCost,
673
- DEFAULT_PRICING
674
- };
@@ -1,7 +0,0 @@
1
- import { Database } from 'bun:sqlite';
2
- export declare function ingestClaude(db: Database, verbose?: boolean, _telemetryDir?: string): Promise<{
3
- files: number;
4
- requests: number;
5
- sessions: number;
6
- }>;
7
- //# sourceMappingURL=claude.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/ingest/claude.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAwDrC,wBAAsB,YAAY,CAChC,EAAE,EAAE,QAAQ,EACZ,OAAO,UAAQ,EACf,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAsHhE"}
@@ -1,7 +0,0 @@
1
- import { Database } from 'bun:sqlite';
2
- declare function readCodexModel(): string;
3
- export declare function ingestCodex(db: Database, verbose?: boolean): Promise<{
4
- sessions: number;
5
- }>;
6
- export { readCodexModel };
7
- //# sourceMappingURL=codex.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/ingest/codex.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAmBrC,iBAAS,cAAc,IAAI,MAAM,CAShC;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,UAAQ,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA0D9F;AAED,OAAO,EAAE,cAAc,EAAE,CAAA"}