@hasna/economy 0.2.17 → 0.2.18

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/cli/index.js CHANGED
@@ -79,7 +79,6 @@ var DEFAULT_PRICING;
79
79
  var init_pricing = __esm(() => {
80
80
  init_database();
81
81
  DEFAULT_PRICING = {
82
- "claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
83
82
  "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
84
83
  "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
85
84
  "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
@@ -90,95 +89,28 @@ var init_pricing = __esm(() => {
90
89
  "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
91
90
  "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
92
91
  "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
93
- "gemini-3.1-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
94
- "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
95
- "gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
96
92
  "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
93
+ "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
97
94
  "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
98
95
  "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
99
- "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
100
- "gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
101
- "gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
102
96
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
103
- "gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
104
97
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
105
98
  "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
106
- "gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
107
- "gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
108
99
  "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
109
100
  "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
110
101
  o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
111
102
  "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
112
103
  o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
113
104
  "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
114
- "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 },
115
- "qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
116
- "qwen3.6": { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
117
- "minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
118
- "minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
119
- "minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
120
- "grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
121
- "grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
122
- "glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
123
- "glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
124
- "kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
105
+ "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
125
106
  };
126
107
  });
127
108
 
128
109
  // src/db/database.ts
129
- var exports_database = {};
130
- __export(exports_database, {
131
- upsertSession: () => upsertSession,
132
- upsertRequest: () => upsertRequest,
133
- upsertProject: () => upsertProject,
134
- upsertModelPricing: () => upsertModelPricing,
135
- upsertGoal: () => upsertGoal,
136
- upsertBudget: () => upsertBudget,
137
- upsertBillingDaily: () => upsertBillingDaily,
138
- setIngestState: () => setIngestState,
139
- seedModelPricing: () => seedModelPricing,
140
- rollupSession: () => rollupSession,
141
- queryTopSessions: () => queryTopSessions,
142
- querySummary: () => querySummary,
143
- querySessions: () => querySessions,
144
- queryRequestsSince: () => queryRequestsSince,
145
- queryProjectBreakdown: () => queryProjectBreakdown,
146
- queryModelBreakdown: () => queryModelBreakdown,
147
- queryDailyBreakdown: () => queryDailyBreakdown,
148
- queryBillingSummary: () => queryBillingSummary,
149
- openDatabase: () => openDatabase,
150
- listProjects: () => listProjects,
151
- listModelPricing: () => listModelPricing,
152
- listMachines: () => listMachines,
153
- listGoals: () => listGoals,
154
- listBudgets: () => listBudgets,
155
- getProject: () => getProject,
156
- getModelPricing: () => getModelPricing,
157
- getMachineId: () => getMachineId,
158
- getIngestState: () => getIngestState,
159
- getGoalStatuses: () => getGoalStatuses,
160
- getDbPath: () => getDbPath,
161
- getDataDir: () => getDataDir,
162
- getBudgetStatuses: () => getBudgetStatuses,
163
- deleteProject: () => deleteProject,
164
- deleteModelPricing: () => deleteModelPricing,
165
- deleteGoal: () => deleteGoal,
166
- deleteBudget: () => deleteBudget,
167
- clearBillingRange: () => clearBillingRange
168
- });
169
110
  import { SqliteAdapter as Database } from "@hasna/cloud";
170
111
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
171
- import { hostname } from "os";
172
112
  import { homedir } from "os";
173
113
  import { join } from "path";
174
- function getMachineId() {
175
- if (process.env["ECONOMY_MACHINE_ID"])
176
- return process.env["ECONOMY_MACHINE_ID"];
177
- const h = hostname().toLowerCase();
178
- if (h.startsWith("spark") || h.startsWith("apple"))
179
- return h.split(".")[0];
180
- return h.split(".")[0];
181
- }
182
114
  function getDataDir() {
183
115
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
184
116
  const newDir = join(home, ".hasna", "economy");
@@ -211,7 +143,6 @@ function openDatabase(dbPath, skipSeed = false) {
211
143
  }
212
144
  const db = new Database(path);
213
145
  db.exec("PRAGMA journal_mode = WAL");
214
- db.exec("PRAGMA busy_timeout = 5000");
215
146
  db.exec("PRAGMA foreign_keys = ON");
216
147
  initSchema(db);
217
148
  if (!skipSeed) {
@@ -233,8 +164,7 @@ function initSchema(db) {
233
164
  cost_usd REAL NOT NULL DEFAULT 0,
234
165
  duration_ms INTEGER DEFAULT 0,
235
166
  timestamp TEXT NOT NULL,
236
- source_request_id TEXT,
237
- machine_id TEXT DEFAULT ''
167
+ source_request_id TEXT
238
168
  );
239
169
 
240
170
  CREATE TABLE IF NOT EXISTS sessions (
@@ -246,8 +176,7 @@ function initSchema(db) {
246
176
  ended_at TEXT,
247
177
  total_cost_usd REAL DEFAULT 0,
248
178
  total_tokens INTEGER DEFAULT 0,
249
- request_count INTEGER DEFAULT 0,
250
- machine_id TEXT DEFAULT ''
179
+ request_count INTEGER DEFAULT 0
251
180
  );
252
181
 
253
182
  CREATE TABLE IF NOT EXISTS projects (
@@ -312,27 +241,6 @@ function initSchema(db) {
312
241
  machine_id TEXT,
313
242
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
314
243
  );
315
-
316
- CREATE TABLE IF NOT EXISTS billing_daily (
317
- date TEXT NOT NULL,
318
- provider TEXT NOT NULL,
319
- description TEXT DEFAULT '',
320
- cost_usd REAL NOT NULL DEFAULT 0,
321
- updated_at TEXT NOT NULL,
322
- PRIMARY KEY (date, provider, description)
323
- );
324
-
325
- CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
326
- CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
327
- `);
328
- const cols = db.prepare(`PRAGMA table_info(requests)`).all();
329
- if (!cols.some((c) => c.name === "machine_id")) {
330
- db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
331
- db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
332
- }
333
- db.exec(`
334
- CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
335
- CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
336
244
  `);
337
245
  }
338
246
  function periodWhere(period) {
@@ -342,11 +250,11 @@ function periodWhere(period) {
342
250
  case "yesterday":
343
251
  return `DATE(timestamp) = DATE('now', '-1 day')`;
344
252
  case "week":
345
- return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
253
+ return `timestamp >= DATE('now', '-7 days')`;
346
254
  case "month":
347
- return `timestamp >= DATE('now', 'start of month')`;
255
+ return `timestamp >= DATE('now', '-30 days')`;
348
256
  case "year":
349
- return `timestamp >= DATE('now', 'start of year')`;
257
+ return `timestamp >= DATE('now', '-365 days')`;
350
258
  case "all":
351
259
  return "1=1";
352
260
  }
@@ -358,11 +266,11 @@ function sessionPeriodWhere(period) {
358
266
  case "yesterday":
359
267
  return `DATE(started_at) = DATE('now', '-1 day')`;
360
268
  case "week":
361
- return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
269
+ return `started_at >= DATE('now', '-7 days')`;
362
270
  case "month":
363
- return `started_at >= DATE('now', 'start of month')`;
271
+ return `started_at >= DATE('now', '-30 days')`;
364
272
  case "year":
365
- return `started_at >= DATE('now', 'start of year')`;
273
+ return `started_at >= DATE('now', '-365 days')`;
366
274
  case "all":
367
275
  return "1=1";
368
276
  }
@@ -372,17 +280,17 @@ function upsertRequest(db, req) {
372
280
  INSERT OR REPLACE INTO requests
373
281
  (id, agent, session_id, model, input_tokens, output_tokens,
374
282
  cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
375
- timestamp, source_request_id, machine_id)
376
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
377
- `).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, req.machine_id ?? "");
283
+ timestamp, source_request_id)
284
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
285
+ `).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);
378
286
  }
379
287
  function upsertSession(db, session) {
380
288
  db.prepare(`
381
289
  INSERT OR REPLACE INTO sessions
382
290
  (id, agent, project_path, project_name, started_at, ended_at,
383
- total_cost_usd, total_tokens, request_count, machine_id)
384
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
385
- `).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 ?? "");
291
+ total_cost_usd, total_tokens, request_count)
292
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
293
+ `).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);
386
294
  }
387
295
  function rollupSession(db, sessionId) {
388
296
  db.prepare(`
@@ -412,10 +320,6 @@ function querySessions(db, filter = {}) {
412
320
  conditions.push("started_at >= ?");
413
321
  params.push(filter.since);
414
322
  }
415
- if (filter.machine) {
416
- conditions.push("machine_id = ?");
417
- params.push(filter.machine);
418
- }
419
323
  if (filter.search) {
420
324
  const q = `%${filter.search}%`;
421
325
  conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
@@ -434,25 +338,24 @@ function queryTopSessions(db, n = 10, agent) {
434
338
  }
435
339
  return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
436
340
  }
437
- function querySummary(db, period, machine) {
341
+ function querySummary(db, period) {
438
342
  const rWhere = periodWhere(period);
439
343
  const sWhere = sessionPeriodWhere(period);
440
- const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
441
344
  const r = db.prepare(`
442
345
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
443
346
  COUNT(*) as requests,
444
347
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
445
- FROM requests WHERE ${rWhere}${machineClause}
348
+ FROM requests WHERE ${rWhere}
446
349
  `).get();
447
350
  const codexTotals = db.prepare(`
448
351
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
449
352
  COALESCE(SUM(total_tokens), 0) as tokens,
450
353
  COUNT(*) as sessions
451
354
  FROM sessions
452
- WHERE ${sWhere}${machineClause}
355
+ WHERE ${sWhere}
453
356
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
454
357
  `).get();
455
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
358
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
456
359
  return {
457
360
  total_usd: r.total_usd + codexTotals.cost_usd,
458
361
  requests: r.requests,
@@ -472,66 +375,23 @@ function queryModelBreakdown(db) {
472
375
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
473
376
  `).all();
474
377
  }
475
- function labelForPath(projectPath, projectName) {
476
- if (projectName && projectName.trim() !== "")
477
- return projectName;
478
- if (!projectPath)
479
- return "";
480
- const segments = projectPath.split("/").filter(Boolean);
481
- const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
482
- for (const seg of segments) {
483
- if (projectPrefix.test(seg))
484
- return seg;
485
- }
486
- const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
487
- for (let i = segments.length - 1;i >= 0; i--) {
488
- if (!generic.has(segments[i].toLowerCase()))
489
- return segments[i];
490
- }
491
- return segments[segments.length - 1] ?? projectPath;
492
- }
493
378
  function queryProjectBreakdown(db) {
494
- const sessions = db.prepare(`
495
- SELECT id, project_path, project_name, total_cost_usd, started_at
496
- FROM sessions
497
- WHERE project_path != '' OR project_name != ''
379
+ return db.prepare(`
380
+ SELECT
381
+ s.project_path,
382
+ COALESCE(p.name, s.project_name) as project_name,
383
+ COUNT(DISTINCT s.id) as sessions,
384
+ COUNT(r.id) as requests,
385
+ COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
386
+ COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
387
+ MAX(s.started_at) as last_active
388
+ FROM sessions s
389
+ LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
390
+ LEFT JOIN requests r ON r.session_id = s.id
391
+ WHERE s.project_path != '' OR s.project_name != ''
392
+ GROUP BY s.project_path
393
+ ORDER BY cost_usd DESC
498
394
  `).all();
499
- const groups = new Map;
500
- for (const s of sessions) {
501
- const label = labelForPath(s.project_path, s.project_name);
502
- if (!label)
503
- continue;
504
- const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
505
- g.sessionIds.push(s.id);
506
- g.totalCost += s.total_cost_usd || 0;
507
- if (!g.lastActive || s.started_at > g.lastActive)
508
- g.lastActive = s.started_at;
509
- if (!g.samplePath)
510
- g.samplePath = s.project_path;
511
- groups.set(label, g);
512
- }
513
- const result = [];
514
- for (const [label, g] of groups.entries()) {
515
- const placeholders = g.sessionIds.map(() => "?").join(",");
516
- const reqStats = placeholders.length ? db.prepare(`
517
- SELECT
518
- COUNT(*) as requests,
519
- COALESCE(SUM(cost_usd), 0) as cost_usd,
520
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
521
- FROM requests WHERE session_id IN (${placeholders})
522
- `).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
523
- result.push({
524
- project_path: g.samplePath,
525
- project_name: label,
526
- sessions: g.sessionIds.length,
527
- requests: reqStats.requests,
528
- total_tokens: reqStats.total_tokens,
529
- cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
530
- last_active: g.lastActive
531
- });
532
- }
533
- result.sort((a, b) => b.cost_usd - a.cost_usd);
534
- return result;
535
395
  }
536
396
  function queryDailyBreakdown(db, days = 30) {
537
397
  return db.prepare(`
@@ -649,40 +509,6 @@ function setIngestState(db, source, key, value) {
649
509
  function queryRequestsSince(db, since) {
650
510
  return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
651
511
  }
652
- function upsertBillingDaily(db, row) {
653
- db.prepare(`
654
- INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
655
- VALUES (?, ?, ?, ?, ?)
656
- `).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
657
- }
658
- function clearBillingRange(db, provider, fromDate, toDate) {
659
- db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
660
- }
661
- function queryBillingSummary(db, period) {
662
- 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";
663
- const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
664
- const by_provider = {};
665
- let total = 0;
666
- for (const r of rows) {
667
- by_provider[r.provider] = r.cost;
668
- total += r.cost;
669
- }
670
- return { total_usd: total, by_provider };
671
- }
672
- function listMachines(db) {
673
- return db.prepare(`
674
- SELECT
675
- s.machine_id,
676
- COUNT(DISTINCT s.id) as sessions,
677
- COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
678
- COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
679
- MAX(s.started_at) as last_active
680
- FROM sessions s
681
- WHERE s.machine_id != ''
682
- GROUP BY s.machine_id
683
- ORDER BY total_cost_usd DESC
684
- `).all();
685
- }
686
512
  function upsertModelPricing(db, p) {
687
513
  db.prepare(`
688
514
  INSERT OR REPLACE INTO model_pricing
@@ -700,11 +526,11 @@ function deleteModelPricing(db, model) {
700
526
  db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
701
527
  }
702
528
  function seedModelPricing(db, defaults) {
703
- const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
529
+ const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
530
+ if (existing.count > 0)
531
+ return;
704
532
  const now = new Date().toISOString();
705
533
  for (const [model, p] of Object.entries(defaults)) {
706
- if (existing.has(model))
707
- continue;
708
534
  upsertModelPricing(db, {
709
535
  model,
710
536
  input_per_1m: p.inputPer1M,
@@ -743,36 +569,29 @@ function collectJsonlFiles(projectDir) {
743
569
  return files;
744
570
  }
745
571
  async function ingestClaude(db, verbose = false, _telemetryDir) {
746
- return ingestJsonlProjects(db, CLAUDE_PROJECTS_DIR, "claude", verbose);
747
- }
748
- async function ingestTakumi(db, verbose = false) {
749
- return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
750
- }
751
- async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
752
- if (!existsSync3(projectsDir)) {
572
+ if (!existsSync3(PROJECTS_DIR)) {
753
573
  if (verbose)
754
- console.log(`${agentName} projects dir not found:`, projectsDir);
574
+ console.log("Claude projects dir not found:", PROJECTS_DIR);
755
575
  return { files: 0, requests: 0, sessions: 0 };
756
576
  }
757
- const machineId = getMachineId();
758
577
  let totalFiles = 0;
759
578
  let totalRequests = 0;
760
579
  const touchedSessions = new Set;
761
580
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
762
- const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
581
+ const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
763
582
  for (const projectDirEntry of projectDirs) {
764
- const projectDirPath = join4(projectsDir, projectDirEntry.name);
583
+ const projectDirPath = join4(PROJECTS_DIR, projectDirEntry.name);
765
584
  const projectPath = dirNameToPath(projectDirEntry.name);
766
585
  const jsonlFiles = collectJsonlFiles(projectDirPath);
767
586
  for (const filePath of jsonlFiles) {
768
- const stateKey = filePath.replace(projectsDir, "");
587
+ const stateKey = filePath.replace(PROJECTS_DIR, "");
769
588
  let fileMtime = "0";
770
589
  try {
771
590
  fileMtime = statSync2(filePath).mtimeMs.toString();
772
591
  } catch {
773
592
  continue;
774
593
  }
775
- const processed = getIngestState(db, agentName, stateKey);
594
+ const processed = getIngestState(db, "claude", stateKey);
776
595
  if (processed === fileMtime)
777
596
  continue;
778
597
  let lines;
@@ -813,10 +632,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
813
632
  if (inputTokens + outputTokens + cacheWriteTokens === 0)
814
633
  continue;
815
634
  const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
816
- const reqId = `${agentName}-${sessionId}-${timestamp}`;
635
+ const reqId = `claude-${sessionId}-${timestamp}`;
817
636
  upsertRequest(db, {
818
637
  id: reqId,
819
- agent: agentName,
638
+ agent: "claude",
820
639
  session_id: sessionId,
821
640
  model,
822
641
  input_tokens: inputTokens,
@@ -826,8 +645,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
826
645
  cost_usd: costUsd,
827
646
  duration_ms: 0,
828
647
  timestamp,
829
- source_request_id: reqId,
830
- machine_id: machineId
648
+ source_request_id: reqId
831
649
  });
832
650
  if (!touchedSessions.has(sessionId)) {
833
651
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
@@ -836,15 +654,14 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
836
654
  const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
837
655
  const session = {
838
656
  id: sessionId,
839
- agent: agentName,
657
+ agent: "claude",
840
658
  project_path: detectedProject ? detectedProject.path : effectiveCwd,
841
659
  project_name: detectedProject ? detectedProject.name : "",
842
660
  started_at: timestamp,
843
661
  ended_at: null,
844
662
  total_cost_usd: 0,
845
663
  total_tokens: 0,
846
- request_count: 0,
847
- machine_id: machineId
664
+ request_count: 0
848
665
  };
849
666
  upsertSession(db, session);
850
667
  }
@@ -852,7 +669,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
852
669
  }
853
670
  totalRequests++;
854
671
  }
855
- setIngestState(db, agentName, stateKey, fileMtime);
672
+ setIngestState(db, "claude", stateKey, fileMtime);
856
673
  totalFiles++;
857
674
  }
858
675
  }
@@ -861,30 +678,28 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
861
678
  }
862
679
  return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
863
680
  }
864
- var CLAUDE_PROJECTS_DIR, TAKUMI_PROJECTS_DIR;
681
+ var PROJECTS_DIR;
865
682
  var init_claude = __esm(() => {
866
683
  init_database();
867
684
  init_pricing();
868
- CLAUDE_PROJECTS_DIR = join4(homedir2(), ".claude", "projects");
869
- TAKUMI_PROJECTS_DIR = join4(homedir2(), ".takumi", "projects");
685
+ PROJECTS_DIR = join4(homedir2(), ".claude", "projects");
870
686
  });
871
687
 
872
688
  // src/ingest/codex.ts
873
689
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
874
690
  import { homedir as homedir3 } from "os";
875
691
  import { join as join5, basename as basename2 } from "path";
876
- import { Database as BunDatabase } from "bun:sqlite";
692
+ import { Database as Database2 } from "bun:sqlite";
877
693
  async function ingestCodex(db, verbose = false) {
878
694
  if (!existsSync4(CODEX_DB_PATH)) {
879
695
  if (verbose)
880
696
  console.log("Codex DB not found:", CODEX_DB_PATH);
881
697
  return { sessions: 0 };
882
698
  }
883
- const machineId = getMachineId();
884
699
  let codexDb = null;
885
700
  let ingested = 0;
886
701
  try {
887
- codexDb = new BunDatabase(CODEX_DB_PATH, { readonly: true });
702
+ codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
888
703
  const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
889
704
  for (const thread of threads) {
890
705
  const stateKey = thread.id;
@@ -905,8 +720,7 @@ async function ingestCodex(db, verbose = false) {
905
720
  ended_at: endedAt,
906
721
  total_cost_usd: costUsd,
907
722
  total_tokens: thread.tokens_used,
908
- request_count: 1,
909
- machine_id: machineId
723
+ request_count: 1
910
724
  });
911
725
  setIngestState(db, "codex", stateKey, "done");
912
726
  ingested++;
@@ -925,88 +739,6 @@ var init_codex = __esm(() => {
925
739
  CODEX_CONFIG_PATH = join5(homedir3(), ".codex", "config.toml");
926
740
  });
927
741
 
928
- // src/ingest/gemini.ts
929
- import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
930
- import { homedir as homedir4 } from "os";
931
- import { join as join6 } from "path";
932
- async function ingestGemini(db, verbose) {
933
- if (!existsSync5(GEMINI_TMP_DIR)) {
934
- if (verbose)
935
- console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
936
- return { sessions: 0 };
937
- }
938
- const machineId = getMachineId();
939
- let totalSessions = 0;
940
- const touchedSessions = new Set;
941
- let projectHashDirs = [];
942
- try {
943
- projectHashDirs = readdirSync3(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join6(GEMINI_TMP_DIR, d.name));
944
- } catch {
945
- return { sessions: 0 };
946
- }
947
- for (const projectDir of projectHashDirs) {
948
- const chatsDir = join6(projectDir, "chats");
949
- if (!existsSync5(chatsDir))
950
- continue;
951
- let chatFiles = [];
952
- try {
953
- chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join6(chatsDir, f));
954
- } catch {
955
- continue;
956
- }
957
- for (const filePath of chatFiles) {
958
- const stateKey = filePath.replace(homedir4(), "~");
959
- let fileMtime = "0";
960
- try {
961
- fileMtime = statSync3(filePath).mtimeMs.toString();
962
- } catch {
963
- continue;
964
- }
965
- const processed = getIngestState(db, "gemini", stateKey);
966
- if (processed === fileMtime)
967
- continue;
968
- let chatData;
969
- try {
970
- chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
971
- } catch {
972
- continue;
973
- }
974
- const sessionId = chatData.sessionId;
975
- if (!sessionId)
976
- continue;
977
- const startTime = chatData.startTime ?? new Date().toISOString();
978
- const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
979
- if (!existing) {
980
- const session = {
981
- id: sessionId,
982
- agent: "gemini",
983
- project_path: "",
984
- project_name: "",
985
- started_at: startTime,
986
- ended_at: chatData.lastUpdated ?? null,
987
- total_cost_usd: 0,
988
- total_tokens: 0,
989
- request_count: 0,
990
- machine_id: machineId
991
- };
992
- upsertSession(db, session);
993
- touchedSessions.add(sessionId);
994
- totalSessions++;
995
- }
996
- setIngestState(db, "gemini", stateKey, fileMtime);
997
- }
998
- }
999
- for (const sessionId of touchedSessions) {
1000
- rollupSession(db, sessionId);
1001
- }
1002
- return { sessions: totalSessions };
1003
- }
1004
- var GEMINI_TMP_DIR;
1005
- var init_gemini = __esm(() => {
1006
- init_database();
1007
- GEMINI_TMP_DIR = join6(homedir4(), ".gemini", "tmp");
1008
- });
1009
-
1010
742
  // src/lib/config.ts
1011
743
  var exports_config = {};
1012
744
  __export(exports_config, {
@@ -1015,12 +747,12 @@ __export(exports_config, {
1015
747
  loadConfig: () => loadConfig2,
1016
748
  getConfigValue: () => getConfigValue
1017
749
  });
1018
- import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
750
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
1019
751
  import { join as join7 } from "path";
1020
752
  function loadConfig2() {
1021
753
  try {
1022
754
  if (existsSync6(CONFIG_PATH2)) {
1023
- const raw = readFileSync6(CONFIG_PATH2, "utf-8");
755
+ const raw = readFileSync5(CONFIG_PATH2, "utf-8");
1024
756
  return { ...DEFAULTS, ...JSON.parse(raw) };
1025
757
  }
1026
758
  } catch {}
@@ -1230,20 +962,6 @@ function ok(data, meta) {
1230
962
  function err(message, status = 400) {
1231
963
  return json({ error: message }, status);
1232
964
  }
1233
- function normalizeBudgetPeriod(value) {
1234
- switch (value) {
1235
- case "day":
1236
- case "daily":
1237
- return "daily";
1238
- case "week":
1239
- case "weekly":
1240
- return "weekly";
1241
- case "month":
1242
- case "monthly":
1243
- default:
1244
- return "monthly";
1245
- }
1246
- }
1247
965
  function applyFields(obj, fields) {
1248
966
  if (!fields || fields.length === 0)
1249
967
  return obj;
@@ -1260,11 +978,7 @@ function createHandler(db) {
1260
978
  return ok({ status: "ok", ts: new Date().toISOString() });
1261
979
  if (path === "/api/summary" && method === "GET") {
1262
980
  const period = url.searchParams.get("period") ?? "today";
1263
- const machine = url.searchParams.get("machine") ?? undefined;
1264
- return ok(querySummary(db, period, machine));
1265
- }
1266
- if (path === "/api/machines" && method === "GET") {
1267
- return ok(listMachines(db), { current_machine: getMachineId() });
981
+ return ok(querySummary(db, period));
1268
982
  }
1269
983
  if (path === "/api/daily" && method === "GET") {
1270
984
  const days = Number(url.searchParams.get("days") ?? 30);
@@ -1273,22 +987,12 @@ function createHandler(db) {
1273
987
  if (path === "/api/sessions" && method === "GET") {
1274
988
  const agent = url.searchParams.get("agent");
1275
989
  const project = url.searchParams.get("project") ?? undefined;
1276
- const search = url.searchParams.get("search") ?? undefined;
1277
- const machine = url.searchParams.get("machine") ?? undefined;
1278
990
  const limit = Number(url.searchParams.get("limit") ?? 50);
1279
991
  const offset = Number(url.searchParams.get("offset") ?? 0);
1280
992
  const since = url.searchParams.get("since") ?? undefined;
1281
993
  const fieldsParam = url.searchParams.get("fields");
1282
994
  const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
1283
- const sessions = querySessions(db, {
1284
- agent: agent ?? undefined,
1285
- project,
1286
- search,
1287
- machine,
1288
- limit,
1289
- offset,
1290
- since
1291
- });
995
+ const sessions = querySessions(db, { agent: agent ?? undefined, project, limit, offset, since });
1292
996
  return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
1293
997
  }
1294
998
  if (path === "/api/top" && method === "GET") {
@@ -1316,7 +1020,7 @@ function createHandler(db) {
1316
1020
  id: randomUUID(),
1317
1021
  project_path: body["project_path"] ?? null,
1318
1022
  agent: body["agent"] ?? null,
1319
- period: normalizeBudgetPeriod(body["period"]),
1023
+ period: body["period"] ?? "monthly",
1320
1024
  limit_usd: Number(body["limit_usd"]),
1321
1025
  alert_at_percent: Number(body["alert_at_percent"] ?? 80),
1322
1026
  created_at: now,
@@ -1377,12 +1081,8 @@ function createHandler(db) {
1377
1081
  const results = {};
1378
1082
  if (sources === "all" || sources === "claude")
1379
1083
  results["claude"] = await ingestClaude(db);
1380
- if (sources === "all" || sources === "takumi")
1381
- results["takumi"] = await ingestTakumi(db);
1382
1084
  if (sources === "all" || sources === "codex")
1383
1085
  results["codex"] = await ingestCodex(db);
1384
- if (sources === "all" || sources === "gemini")
1385
- results["gemini"] = await ingestGemini(db);
1386
1086
  return ok(results);
1387
1087
  }
1388
1088
  const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
@@ -1455,7 +1155,6 @@ var init_serve = __esm(() => {
1455
1155
  init_database();
1456
1156
  init_claude();
1457
1157
  init_codex();
1458
- init_gemini();
1459
1158
  init_pricing();
1460
1159
  CORS = {
1461
1160
  "Access-Control-Allow-Origin": "*",
@@ -1593,102 +1292,6 @@ function menubarStop() {
1593
1292
  var APP_PATH = "/Applications/Economy Bar.app", REPO = "hasna/open-economy";
1594
1293
  var init_menubar = () => {};
1595
1294
 
1596
- // src/db/pg-migrations.ts
1597
- var exports_pg_migrations = {};
1598
- __export(exports_pg_migrations, {
1599
- PG_MIGRATIONS: () => PG_MIGRATIONS
1600
- });
1601
- var PG_MIGRATIONS;
1602
- var init_pg_migrations = __esm(() => {
1603
- PG_MIGRATIONS = [
1604
- `CREATE TABLE IF NOT EXISTS requests (
1605
- id TEXT PRIMARY KEY,
1606
- agent TEXT NOT NULL,
1607
- session_id TEXT NOT NULL,
1608
- model TEXT NOT NULL,
1609
- input_tokens INTEGER DEFAULT 0,
1610
- output_tokens INTEGER DEFAULT 0,
1611
- cache_read_tokens INTEGER DEFAULT 0,
1612
- cache_create_tokens INTEGER DEFAULT 0,
1613
- cost_usd REAL NOT NULL DEFAULT 0,
1614
- duration_ms INTEGER DEFAULT 0,
1615
- timestamp TEXT NOT NULL,
1616
- source_request_id TEXT,
1617
- machine_id TEXT DEFAULT ''
1618
- )`,
1619
- `CREATE TABLE IF NOT EXISTS sessions (
1620
- id TEXT PRIMARY KEY,
1621
- agent TEXT NOT NULL,
1622
- project_path TEXT DEFAULT '',
1623
- project_name TEXT DEFAULT '',
1624
- started_at TEXT NOT NULL,
1625
- ended_at TEXT,
1626
- total_cost_usd REAL DEFAULT 0,
1627
- total_tokens INTEGER DEFAULT 0,
1628
- request_count INTEGER DEFAULT 0,
1629
- machine_id TEXT DEFAULT ''
1630
- )`,
1631
- `CREATE TABLE IF NOT EXISTS projects (
1632
- id TEXT PRIMARY KEY,
1633
- path TEXT UNIQUE NOT NULL,
1634
- name TEXT NOT NULL,
1635
- description TEXT,
1636
- tags TEXT DEFAULT '[]',
1637
- created_at TEXT NOT NULL
1638
- )`,
1639
- `CREATE TABLE IF NOT EXISTS budgets (
1640
- id TEXT PRIMARY KEY,
1641
- project_path TEXT,
1642
- agent TEXT,
1643
- period TEXT NOT NULL,
1644
- limit_usd REAL NOT NULL,
1645
- alert_at_percent INTEGER DEFAULT 80,
1646
- created_at TEXT NOT NULL,
1647
- updated_at TEXT NOT NULL
1648
- )`,
1649
- `CREATE TABLE IF NOT EXISTS goals (
1650
- id TEXT PRIMARY KEY,
1651
- period TEXT NOT NULL,
1652
- project_path TEXT,
1653
- agent TEXT,
1654
- limit_usd REAL NOT NULL,
1655
- created_at TEXT NOT NULL,
1656
- updated_at TEXT NOT NULL
1657
- )`,
1658
- `CREATE TABLE IF NOT EXISTS ingest_state (
1659
- source TEXT NOT NULL,
1660
- key TEXT NOT NULL,
1661
- value TEXT NOT NULL,
1662
- PRIMARY KEY (source, key)
1663
- )`,
1664
- `CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
1665
- `CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
1666
- `CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
1667
- `CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
1668
- `CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
1669
- `CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
1670
- `CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
1671
- `CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
1672
- `CREATE TABLE IF NOT EXISTS model_pricing (
1673
- model TEXT PRIMARY KEY,
1674
- input_per_1m REAL NOT NULL DEFAULT 0,
1675
- output_per_1m REAL NOT NULL DEFAULT 0,
1676
- cache_read_per_1m REAL NOT NULL DEFAULT 0,
1677
- cache_write_per_1m REAL NOT NULL DEFAULT 0,
1678
- updated_at TEXT NOT NULL
1679
- )`,
1680
- `CREATE TABLE IF NOT EXISTS feedback (
1681
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
1682
- message TEXT NOT NULL,
1683
- email TEXT,
1684
- category TEXT DEFAULT 'general',
1685
- version TEXT,
1686
- machine_id TEXT,
1687
- created_at TEXT NOT NULL DEFAULT NOW()::text
1688
- )`
1689
- ];
1690
- });
1691
-
1692
1295
  // src/cli/index.ts
1693
1296
  import { Command } from "commander";
1694
1297
  import chalk4 from "chalk";
@@ -2047,148 +1650,94 @@ ${chalk.dim("Set it active:")} economy brains model set ${String(status["fine_tu
2047
1650
  init_database();
2048
1651
  init_claude();
2049
1652
  init_codex();
2050
- init_gemini();
2051
1653
 
2052
- // src/ingest/billing.ts
1654
+ // src/ingest/gemini.ts
2053
1655
  init_database();
2054
- function getAnthropicAdminKey() {
2055
- return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
2056
- }
2057
- function getOpenAIAdminKey() {
2058
- return process.env["HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY"] ?? process.env["OPENAI_ADMIN_API_KEY"] ?? null;
2059
- }
2060
- function toISODate(d) {
2061
- return d.toISOString().substring(0, 10);
2062
- }
2063
- async function syncAnthropicBilling(db, opts = {}) {
2064
- const key = getAnthropicAdminKey();
2065
- if (!key)
2066
- throw new Error("Missing Anthropic admin key (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY)");
2067
- const now = new Date;
2068
- const end = opts.toDate ? new Date(opts.toDate) : new Date(now.getTime() + 24 * 3600000);
2069
- const days = opts.days ?? 31;
2070
- const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
2071
- const startIso = start.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
2072
- const endIso = end.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
2073
- let totalUsd = 0;
2074
- const buckets = [];
2075
- let nextPage;
2076
- do {
2077
- const url = new URL("https://api.anthropic.com/v1/organizations/cost_report");
2078
- url.searchParams.set("starting_at", startIso);
2079
- url.searchParams.set("ending_at", endIso);
2080
- url.searchParams.set("bucket_width", "1d");
2081
- url.searchParams.set("limit", "31");
2082
- url.searchParams.append("group_by[]", "description");
2083
- if (nextPage)
2084
- url.searchParams.set("page", nextPage);
2085
- const res = await fetch(url.toString(), {
2086
- headers: { "anthropic-version": "2023-06-01", "x-api-key": key }
2087
- });
2088
- const data = await res.json();
2089
- if (data.error)
2090
- throw new Error(`Anthropic API: ${data.error.message}`);
2091
- if (data.data)
2092
- buckets.push(...data.data);
2093
- nextPage = data.has_more ? data.next_page : undefined;
2094
- } while (nextPage);
2095
- const fromDateStr = toISODate(start);
2096
- const toDateStr = toISODate(new Date(end.getTime() - 1000));
2097
- clearBillingRange(db, "anthropic", fromDateStr, toDateStr);
2098
- const updatedAt = new Date().toISOString();
2099
- for (const bucket of buckets) {
2100
- const date = bucket.starting_at.substring(0, 10);
2101
- for (const r of bucket.results) {
2102
- const usd = Number(r.amount) / 100;
2103
- if (usd === 0)
2104
- continue;
2105
- const desc = (r.description ?? "unknown").substring(0, 200);
2106
- upsertBillingDaily(db, { date, provider: "anthropic", description: desc, cost_usd: usd, updated_at: updatedAt });
2107
- totalUsd += usd;
2108
- }
1656
+ import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
1657
+ import { homedir as homedir4 } from "os";
1658
+ import { join as join6 } from "path";
1659
+ var GEMINI_TMP_DIR = join6(homedir4(), ".gemini", "tmp");
1660
+ async function ingestGemini(db, verbose) {
1661
+ if (!existsSync5(GEMINI_TMP_DIR)) {
1662
+ if (verbose)
1663
+ console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
1664
+ return { sessions: 0 };
2109
1665
  }
2110
- return { days: buckets.length, totalUsd };
2111
- }
2112
- async function syncOpenAIBilling(db, opts = {}) {
2113
- const key = getOpenAIAdminKey();
2114
- if (!key)
2115
- throw new Error("Missing OpenAI admin key (HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY)");
2116
- const now = new Date;
2117
- const end = opts.toDate ? new Date(opts.toDate) : now;
2118
- const days = opts.days ?? 31;
2119
- const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
2120
- const startSec = Math.floor(start.getTime() / 1000);
2121
- const endSec = Math.floor(end.getTime() / 1000);
2122
- let totalUsd = 0;
2123
- const buckets = [];
2124
- let nextPage;
2125
- do {
2126
- const url = new URL("https://api.openai.com/v1/organization/costs");
2127
- url.searchParams.set("start_time", String(startSec));
2128
- url.searchParams.set("end_time", String(endSec));
2129
- url.searchParams.set("bucket_width", "1d");
2130
- url.searchParams.set("limit", "31");
2131
- url.searchParams.append("group_by[]", "line_item");
2132
- if (nextPage)
2133
- url.searchParams.set("page", nextPage);
2134
- const res = await fetch(url.toString(), {
2135
- headers: { Authorization: `Bearer ${key}` }
2136
- });
2137
- const data = await res.json();
2138
- if (data.error)
2139
- throw new Error(`OpenAI API: ${data.error.message}`);
2140
- if (data.data)
2141
- buckets.push(...data.data);
2142
- nextPage = data.has_more ? data.next_page : undefined;
2143
- } while (nextPage);
2144
- const fromDateStr = toISODate(start);
2145
- const toDateStr = toISODate(new Date(end.getTime() - 1000));
2146
- clearBillingRange(db, "openai", fromDateStr, toDateStr);
2147
- const updatedAt = new Date().toISOString();
2148
- for (const bucket of buckets) {
2149
- const date = new Date(bucket.start_time * 1000).toISOString().substring(0, 10);
2150
- for (const r of bucket.results) {
2151
- const usd = Number(r.amount?.value ?? 0);
2152
- if (usd === 0)
1666
+ let totalSessions = 0;
1667
+ const touchedSessions = new Set;
1668
+ let projectHashDirs = [];
1669
+ try {
1670
+ projectHashDirs = readdirSync3(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join6(GEMINI_TMP_DIR, d.name));
1671
+ } catch {
1672
+ return { sessions: 0 };
1673
+ }
1674
+ for (const projectDir of projectHashDirs) {
1675
+ const chatsDir = join6(projectDir, "chats");
1676
+ if (!existsSync5(chatsDir))
1677
+ continue;
1678
+ let chatFiles = [];
1679
+ try {
1680
+ chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join6(chatsDir, f));
1681
+ } catch {
1682
+ continue;
1683
+ }
1684
+ for (const filePath of chatFiles) {
1685
+ const stateKey = filePath.replace(homedir4(), "~");
1686
+ let fileMtime = "0";
1687
+ try {
1688
+ fileMtime = statSync3(filePath).mtimeMs.toString();
1689
+ } catch {
1690
+ continue;
1691
+ }
1692
+ const processed = getIngestState(db, "gemini", stateKey);
1693
+ if (processed === fileMtime)
1694
+ continue;
1695
+ let chatData;
1696
+ try {
1697
+ chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
1698
+ } catch {
1699
+ continue;
1700
+ }
1701
+ const sessionId = chatData.sessionId;
1702
+ if (!sessionId)
2153
1703
  continue;
2154
- const desc = (r.line_item ?? "unknown").substring(0, 200);
2155
- upsertBillingDaily(db, { date, provider: "openai", description: desc, cost_usd: usd, updated_at: updatedAt });
2156
- totalUsd += usd;
1704
+ const startTime = chatData.startTime ?? new Date().toISOString();
1705
+ const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
1706
+ if (!existing) {
1707
+ const session = {
1708
+ id: sessionId,
1709
+ agent: "gemini",
1710
+ project_path: "",
1711
+ project_name: "",
1712
+ started_at: startTime,
1713
+ ended_at: chatData.lastUpdated ?? null,
1714
+ total_cost_usd: 0,
1715
+ total_tokens: 0,
1716
+ request_count: 0
1717
+ };
1718
+ upsertSession(db, session);
1719
+ touchedSessions.add(sessionId);
1720
+ totalSessions++;
1721
+ }
1722
+ setIngestState(db, "gemini", stateKey, fileMtime);
2157
1723
  }
2158
1724
  }
2159
- return { days: buckets.length, totalUsd };
2160
- }
2161
-
2162
- // src/cli/index.ts
2163
- init_database();
2164
-
2165
- // src/lib/package-metadata.ts
2166
- import { readFileSync as readFileSync5 } from "fs";
2167
- var cachedMetadata = null;
2168
- function getPackageMetadata() {
2169
- if (cachedMetadata)
2170
- return cachedMetadata;
2171
- const raw = readFileSync5(new URL("../../package.json", import.meta.url), "utf8");
2172
- const parsed = JSON.parse(raw);
2173
- cachedMetadata = {
2174
- name: parsed.name ?? "@hasna/economy",
2175
- version: parsed.version ?? "0.0.0"
2176
- };
2177
- return cachedMetadata;
1725
+ for (const sessionId of touchedSessions) {
1726
+ rollupSession(db, sessionId);
1727
+ }
1728
+ return { sessions: totalSessions };
2178
1729
  }
2179
- var packageMetadata = getPackageMetadata();
2180
1730
 
2181
1731
  // src/cli/index.ts
2182
1732
  init_pricing();
2183
1733
  import { randomUUID as randomUUID2 } from "crypto";
2184
1734
  import { execSync as execSync2 } from "child_process";
2185
1735
  var program = new Command;
2186
- program.name("economy").description("AI coding cost tracker \u2014 Claude Code, Codex, and Gemini").version(packageMetadata.version);
1736
+ program.name("economy").description("AI coding cost tracker \u2014 Claude Code, Codex, and Gemini").version("0.2.2");
2187
1737
  async function autoSync() {
2188
1738
  const db = openDatabase();
2189
1739
  ensurePricingSeeded(db);
2190
1740
  await ingestClaude(db);
2191
- await ingestTakumi(db);
2192
1741
  await ingestCodex(db);
2193
1742
  await ingestGemini(db);
2194
1743
  }
@@ -2301,7 +1850,7 @@ program.action(async () => {
2301
1850
  }
2302
1851
  console.log();
2303
1852
  });
2304
- program.command("sync").description("Ingest cost data from Claude Code, Codex, and Gemini").option("--claude", "Only ingest Claude Code telemetry").option("--takumi", "Only ingest Takumi sessions").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").option("--recalculate", "Recalculate costs for all requests with cost_usd = 0").action(async (opts) => {
1853
+ program.command("sync").description("Ingest cost data from Claude Code, Codex, and Gemini").option("--claude", "Only ingest Claude Code telemetry").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").action(async (opts) => {
2305
1854
  const db = openDatabase();
2306
1855
  ensurePricingSeeded(db);
2307
1856
  if (opts.force) {
@@ -2309,9 +1858,8 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
2309
1858
  if (opts.verbose)
2310
1859
  console.log(chalk4.dim("Cleared ingest cache"));
2311
1860
  }
2312
- const anySpecific = opts.claude || opts.takumi || opts.codex || opts.gemini;
1861
+ const anySpecific = opts.claude || opts.codex || opts.gemini;
2313
1862
  const doClaude = opts.claude || !anySpecific;
2314
- const doTakumi = opts.takumi || !anySpecific;
2315
1863
  const doCodex = opts.codex || !anySpecific;
2316
1864
  const doGemini = opts.gemini || !anySpecific;
2317
1865
  if (doClaude) {
@@ -2319,11 +1867,6 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
2319
1867
  const r = await ingestClaude(db, opts.verbose);
2320
1868
  console.log(chalk4.green(`\u2713 ${r.files} files, ${r.requests} requests, ${r.sessions} sessions`));
2321
1869
  }
2322
- if (doTakumi) {
2323
- process.stdout.write(chalk4.cyan("\u2192 Ingesting Takumi sessions... "));
2324
- const r = await ingestTakumi(db, opts.verbose);
2325
- console.log(chalk4.green(`\u2713 ${r.files} files, ${r.requests} requests, ${r.sessions} sessions`));
2326
- }
2327
1870
  if (doCodex) {
2328
1871
  process.stdout.write(chalk4.cyan("\u2192 Ingesting Codex sessions... "));
2329
1872
  const r = await ingestCodex(db, opts.verbose);
@@ -2334,35 +1877,6 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
2334
1877
  const r = await ingestGemini(db, opts.verbose);
2335
1878
  console.log(chalk4.green(`\u2713 ${r.sessions} sessions`));
2336
1879
  }
2337
- if (opts.backfillMachine) {
2338
- const machine = getMachineId();
2339
- const reqCount = db.prepare(`UPDATE requests SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
2340
- const sessCount = db.prepare(`UPDATE sessions SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
2341
- console.log(chalk4.cyan(`\u2192 Backfilled machine_id='${machine}': ${reqCount.changes} requests, ${sessCount.changes} sessions`));
2342
- }
2343
- if (opts.recalculate) {
2344
- const { computeCostFromDb: computeCostFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
2345
- const zeroRows = db.prepare(`SELECT id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens FROM requests WHERE cost_usd = 0 AND (input_tokens > 0 OR output_tokens > 0)`).all();
2346
- let fixed = 0;
2347
- for (const r of zeroRows) {
2348
- const cost = computeCostFromDb2(db, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, r.cache_create_tokens);
2349
- if (cost > 0) {
2350
- db.prepare(`UPDATE requests SET cost_usd = ? WHERE id = ?`).run(cost, r.id);
2351
- fixed++;
2352
- }
2353
- }
2354
- if (fixed > 0) {
2355
- const touchedSessions = new Set(zeroRows.map((r) => {
2356
- const row = db.prepare(`SELECT session_id FROM requests WHERE id = ?`).get(r.id);
2357
- return row?.session_id;
2358
- }).filter(Boolean));
2359
- const { rollupSession: rollupSession2 } = await Promise.resolve().then(() => (init_database(), exports_database));
2360
- for (const sid of touchedSessions) {
2361
- rollupSession2(db, sid);
2362
- }
2363
- }
2364
- console.log(chalk4.cyan(`\u2192 Recalculated: ${fixed}/${zeroRows.length} zero-cost requests now have pricing`));
2365
- }
2366
1880
  try {
2367
1881
  const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
2368
1882
  await checkAndFireWebhooks2(db);
@@ -2382,14 +1896,13 @@ program.command("month").description("Cost summary for this month").action(async
2382
1896
  await autoSync();
2383
1897
  printSummary("This Month", "month");
2384
1898
  });
2385
- program.command("sessions").description("List coding sessions with costs").option("--agent <agent>", "Filter by agent (claude|codex)").option("--project <path>", "Filter by project path").option("--machine <id>", "Filter by machine hostname (e.g. spark01, apple01)").option("--limit <n>", "Number of sessions", "20").option("--format <fmt>", "Output format: table|compact|csv|json", "table").option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--search <query>", "Search by project name, session id prefix, or agent").action(async (opts) => {
1899
+ program.command("sessions").description("List coding sessions with costs").option("--agent <agent>", "Filter by agent (claude|codex)").option("--project <path>", "Filter by project path").option("--limit <n>", "Number of sessions", "20").option("--format <fmt>", "Output format: table|compact|csv|json", "table").option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--search <query>", "Search by project name, session id prefix, or agent").action(async (opts) => {
2386
1900
  await autoSync();
2387
1901
  const db = openDatabase();
2388
1902
  const sinceDate = opts.since ? parseSinceDate(opts.since) : undefined;
2389
1903
  let sessions = querySessions(db, {
2390
1904
  agent: opts.agent,
2391
1905
  project: opts.project,
2392
- machine: opts.machine,
2393
1906
  limit: Number(opts.limit ?? 20),
2394
1907
  since: sinceDate,
2395
1908
  search: opts.search
@@ -2838,29 +2351,6 @@ program.command("session <id>").description("Show detailed breakdown of a single
2838
2351
  }
2839
2352
  console.log();
2840
2353
  });
2841
- program.command("machines").description("List all machines that have synced data").action(async () => {
2842
- await autoSync();
2843
- const db = openDatabase();
2844
- const machines = listMachines(db);
2845
- const current = getMachineId();
2846
- if (machines.length === 0) {
2847
- console.log(chalk4.yellow(`No machine data yet. Current machine: ${current}`));
2848
- return;
2849
- }
2850
- console.log();
2851
- console.log(chalk4.bold.cyan(" Machines"));
2852
- console.log();
2853
- printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"], machines.map((m) => [
2854
- m.machine_id === current ? chalk4.green(`${m.machine_id} (this)`) : chalk4.white(m.machine_id),
2855
- fmtCount(m.sessions),
2856
- fmtCount(m.requests),
2857
- fmt2(m.total_cost_usd),
2858
- chalk4.dim(m.last_active?.substring(0, 16) ?? "\u2014")
2859
- ]));
2860
- console.log(`
2861
- ${chalk4.dim("Current machine:")} ${chalk4.bold(current)}`);
2862
- console.log();
2863
- });
2864
2354
  program.command("export").description("Export data as CSV").option("--type <type>", "Data type: sessions or requests", "sessions").option("--period <period>", "Period: today|week|month|all", "month").option("--output <file>", "Output file path (default: stdout)").action(async (opts) => {
2865
2355
  await autoSync();
2866
2356
  const db = openDatabase();
@@ -3128,144 +2618,5 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
3128
2618
  process.exit(1);
3129
2619
  }
3130
2620
  });
3131
- var CLOUD_RDS_HOST = "hasnaxyz-prod-opensource.c4limg0qgqvk.us-east-1.rds.amazonaws.com";
3132
- var CLOUD_RDS_USER = "hasna_admin";
3133
- var CLOUD_RDS_DB = "economy";
3134
- var CLOUD_TABLES = ["requests", "sessions", "projects", "budgets", "goals", "model_pricing"];
3135
- async function getCloudPassword() {
3136
- if (process.env["ECONOMY_PG_PASSWORD"])
3137
- return process.env["ECONOMY_PG_PASSWORD"];
3138
- const { execSync: exec } = await import("child_process");
3139
- const secretJson = exec(`aws --profile hasna-xyz-hq secretsmanager get-secret-value --secret-id 'rds!db-7a451ce6-83a9-40fa-b24a-81e5d5943511' --query SecretString --output text`, { timeout: 1e4, encoding: "utf-8" });
3140
- return JSON.parse(secretJson).password;
3141
- }
3142
- async function getCloudPg() {
3143
- const { PgAdapterAsync } = await import("@hasna/cloud");
3144
- const pw = encodeURIComponent(await getCloudPassword());
3145
- return new PgAdapterAsync(`postgresql://${CLOUD_RDS_USER}:${pw}@${CLOUD_RDS_HOST}:5432/${CLOUD_RDS_DB}?sslmode=require`);
3146
- }
3147
- var cloudCmd = program.command("cloud").description("Cross-machine sync via cloud PostgreSQL");
3148
- cloudCmd.command("push").description("Push local economy data to cloud PostgreSQL").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
3149
- const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
3150
- const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
3151
- const cloud = await getCloudPg();
3152
- const local = new SqliteAdapter(getDbPath());
3153
- process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
3154
- for (const sql of PG_MIGRATIONS2) {
3155
- await cloud.run(sql);
3156
- }
3157
- console.log(chalk4.green("\u2713"));
3158
- const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : CLOUD_TABLES;
3159
- process.stdout.write(chalk4.cyan(`\u2192 Pushing ${tableList.join(", ")}... `));
3160
- const results = await syncPush(local, cloud, { tables: tableList });
3161
- const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
3162
- console.log(chalk4.green(`\u2713 ${totalRows} rows across ${tableList.length} tables`));
3163
- local.close();
3164
- await cloud.close();
3165
- console.log(chalk4.bold.green(`
3166
- \u2713 Push complete from ${getMachineId()}`));
3167
- });
3168
- cloudCmd.command("pull").description("Pull cloud PostgreSQL data to local").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
3169
- const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
3170
- const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
3171
- const cloud = await getCloudPg();
3172
- const local = new SqliteAdapter(getDbPath());
3173
- process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
3174
- for (const sql of PG_MIGRATIONS2) {
3175
- await cloud.run(sql);
3176
- }
3177
- console.log(chalk4.green("\u2713"));
3178
- const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : CLOUD_TABLES;
3179
- process.stdout.write(chalk4.cyan(`\u2192 Pulling ${tableList.join(", ")}... `));
3180
- const results = await syncPull(cloud, local, { tables: tableList });
3181
- const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
3182
- console.log(chalk4.green(`\u2713 ${totalRows} rows across ${tableList.length} tables`));
3183
- local.close();
3184
- await cloud.close();
3185
- console.log(chalk4.bold.green(`
3186
- \u2713 Pull complete to ${getMachineId()}`));
3187
- });
3188
- cloudCmd.command("sync").description("Full sync: ingest local \u2192 push to cloud \u2192 pull from cloud").action(async () => {
3189
- console.log(chalk4.bold.cyan(` Cloud Sync \u2014 ${getMachineId()}
3190
- `));
3191
- process.stdout.write(chalk4.cyan("\u2192 Ingesting local data... "));
3192
- await autoSync();
3193
- console.log(chalk4.green("\u2713"));
3194
- const { syncPush, syncPull, SqliteAdapter } = await import("@hasna/cloud");
3195
- const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
3196
- const cloud = await getCloudPg();
3197
- const local = new SqliteAdapter(getDbPath());
3198
- for (const sql of PG_MIGRATIONS2) {
3199
- await cloud.run(sql);
3200
- }
3201
- process.stdout.write(chalk4.cyan("\u2192 Pushing local \u2192 cloud... "));
3202
- const pushResults = await syncPush(local, cloud, { tables: CLOUD_TABLES });
3203
- console.log(chalk4.green(`\u2713 ${pushResults.reduce((s, r) => s + r.rowsWritten, 0)} rows`));
3204
- process.stdout.write(chalk4.cyan("\u2192 Pulling cloud \u2192 local... "));
3205
- const pullResults = await syncPull(cloud, local, { tables: CLOUD_TABLES });
3206
- console.log(chalk4.green(`\u2713 ${pullResults.reduce((s, r) => s + r.rowsWritten, 0)} rows`));
3207
- local.close();
3208
- await cloud.close();
3209
- console.log(chalk4.bold.green(`
3210
- \u2713 Cloud sync complete`));
3211
- });
3212
- cloudCmd.command("status").description("Check cloud connection status").action(async () => {
3213
- console.log();
3214
- console.log(` Machine: ${chalk4.white(getMachineId())}`);
3215
- console.log(` RDS Host: ${chalk4.white(CLOUD_RDS_HOST)}`);
3216
- console.log(` Database: ${chalk4.white(CLOUD_RDS_DB)}`);
3217
- try {
3218
- const cloud = await getCloudPg();
3219
- await cloud.get("SELECT 1 as ok");
3220
- const tables = await cloud.all("SELECT tablename FROM pg_tables WHERE schemaname = 'public'");
3221
- console.log(` PostgreSQL: ${chalk4.green("connected")}`);
3222
- console.log(` Tables: ${chalk4.white(tables.map((t) => t.tablename).join(", ") || "(none)")}`);
3223
- await cloud.close();
3224
- } catch (err2) {
3225
- console.log(` PostgreSQL: ${chalk4.red(`failed \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`)}`);
3226
- }
3227
- console.log();
3228
- });
3229
- var billingCmd = program.command("billing").description("Pull actual billing from provider admin APIs (ground truth)");
3230
- billingCmd.command("sync").description("Sync actual billing from Anthropic and OpenAI admin APIs").option("--days <n>", "Days of history to fetch", "31").option("--anthropic", "Only sync Anthropic").option("--openai", "Only sync OpenAI").action(async (opts) => {
3231
- const db = openDatabase();
3232
- const days = Number(opts.days ?? 31);
3233
- const doBoth = !opts.anthropic && !opts.openai;
3234
- if (opts.anthropic || doBoth) {
3235
- process.stdout.write(chalk4.cyan("\u2192 Syncing Anthropic billing... "));
3236
- try {
3237
- const r = await syncAnthropicBilling(db, { days });
3238
- console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
3239
- } catch (e) {
3240
- console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
3241
- }
3242
- }
3243
- if (opts.openai || doBoth) {
3244
- process.stdout.write(chalk4.cyan("\u2192 Syncing OpenAI billing... "));
3245
- try {
3246
- const r = await syncOpenAIBilling(db, { days });
3247
- console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
3248
- } catch (e) {
3249
- console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
3250
- }
3251
- }
3252
- });
3253
- billingCmd.command("show").description("Show actual billing totals vs our estimated costs").option("--period <p>", "Period: today|yesterday|week|month|year|all", "month").action((opts) => {
3254
- const db = openDatabase();
3255
- const period = opts.period ?? "month";
3256
- const actual = queryBillingSummary(db, period);
3257
- const estimated = querySummary(db, period);
3258
- console.log();
3259
- console.log(chalk4.bold.cyan(` Billing ${period} (actual from admin APIs)
3260
- `));
3261
- printTable(["Provider", "Actual (billed)"], Object.entries(actual.by_provider).map(([p, c]) => [chalk4.white(p), fmt2(c)]));
3262
- console.log();
3263
- console.log(` ${chalk4.bold("Actual total:")} ${fmt2(actual.total_usd)}`);
3264
- console.log(` ${chalk4.dim("Our estimate:")} ${fmt2(estimated.total_usd)}`);
3265
- const diff = estimated.total_usd - actual.total_usd;
3266
- const pct = actual.total_usd > 0 ? diff / actual.total_usd * 100 : 0;
3267
- console.log(` ${chalk4.dim("Difference:")} ${fmt2(Math.abs(diff))} (${diff >= 0 ? "+" : ""}${pct.toFixed(1)}%)`);
3268
- console.log();
3269
- });
3270
2621
  registerBrainsCommand(program);
3271
2622
  program.parse();