@hasna/economy 0.2.16 → 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.
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env bun
2
1
  // @bun
3
2
  var __defProp = Object.defineProperty;
4
3
  var __returnValue = (v) => v;
@@ -79,7 +78,6 @@ var DEFAULT_PRICING;
79
78
  var init_pricing = __esm(() => {
80
79
  init_database();
81
80
  DEFAULT_PRICING = {
82
- "claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
83
81
  "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
84
82
  "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
85
83
  "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
@@ -90,55 +88,28 @@ var init_pricing = __esm(() => {
90
88
  "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
91
89
  "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
92
90
  "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
91
  "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
92
+ "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
97
93
  "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
98
94
  "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
95
  "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
96
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
105
97
  "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
98
  "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
109
99
  "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
110
100
  o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
111
101
  "o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
112
102
  o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
113
103
  "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 }
104
+ "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
125
105
  };
126
106
  });
127
107
 
128
108
  // src/db/database.ts
129
109
  import { SqliteAdapter as Database } from "@hasna/cloud";
130
110
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
131
- import { hostname } from "os";
132
111
  import { homedir } from "os";
133
112
  import { join } from "path";
134
- function getMachineId() {
135
- if (process.env["ECONOMY_MACHINE_ID"])
136
- return process.env["ECONOMY_MACHINE_ID"];
137
- const h = hostname().toLowerCase();
138
- if (h.startsWith("spark") || h.startsWith("apple"))
139
- return h.split(".")[0];
140
- return h.split(".")[0];
141
- }
142
113
  function getDataDir() {
143
114
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
144
115
  const newDir = join(home, ".hasna", "economy");
@@ -171,7 +142,6 @@ function openDatabase(dbPath, skipSeed = false) {
171
142
  }
172
143
  const db = new Database(path);
173
144
  db.exec("PRAGMA journal_mode = WAL");
174
- db.exec("PRAGMA busy_timeout = 5000");
175
145
  db.exec("PRAGMA foreign_keys = ON");
176
146
  initSchema(db);
177
147
  if (!skipSeed) {
@@ -193,8 +163,7 @@ function initSchema(db) {
193
163
  cost_usd REAL NOT NULL DEFAULT 0,
194
164
  duration_ms INTEGER DEFAULT 0,
195
165
  timestamp TEXT NOT NULL,
196
- source_request_id TEXT,
197
- machine_id TEXT DEFAULT ''
166
+ source_request_id TEXT
198
167
  );
199
168
 
200
169
  CREATE TABLE IF NOT EXISTS sessions (
@@ -206,8 +175,7 @@ function initSchema(db) {
206
175
  ended_at TEXT,
207
176
  total_cost_usd REAL DEFAULT 0,
208
177
  total_tokens INTEGER DEFAULT 0,
209
- request_count INTEGER DEFAULT 0,
210
- machine_id TEXT DEFAULT ''
178
+ request_count INTEGER DEFAULT 0
211
179
  );
212
180
 
213
181
  CREATE TABLE IF NOT EXISTS projects (
@@ -272,27 +240,6 @@ function initSchema(db) {
272
240
  machine_id TEXT,
273
241
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
274
242
  );
275
-
276
- CREATE TABLE IF NOT EXISTS billing_daily (
277
- date TEXT NOT NULL,
278
- provider TEXT NOT NULL,
279
- description TEXT DEFAULT '',
280
- cost_usd REAL NOT NULL DEFAULT 0,
281
- updated_at TEXT NOT NULL,
282
- PRIMARY KEY (date, provider, description)
283
- );
284
-
285
- CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
286
- CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
287
- `);
288
- const cols = db.prepare(`PRAGMA table_info(requests)`).all();
289
- if (!cols.some((c) => c.name === "machine_id")) {
290
- db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
291
- db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
292
- }
293
- db.exec(`
294
- CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
295
- CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
296
243
  `);
297
244
  }
298
245
  function periodWhere(period) {
@@ -302,11 +249,11 @@ function periodWhere(period) {
302
249
  case "yesterday":
303
250
  return `DATE(timestamp) = DATE('now', '-1 day')`;
304
251
  case "week":
305
- return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
252
+ return `timestamp >= DATE('now', '-7 days')`;
306
253
  case "month":
307
- return `timestamp >= DATE('now', 'start of month')`;
254
+ return `timestamp >= DATE('now', '-30 days')`;
308
255
  case "year":
309
- return `timestamp >= DATE('now', 'start of year')`;
256
+ return `timestamp >= DATE('now', '-365 days')`;
310
257
  case "all":
311
258
  return "1=1";
312
259
  }
@@ -318,11 +265,11 @@ function sessionPeriodWhere(period) {
318
265
  case "yesterday":
319
266
  return `DATE(started_at) = DATE('now', '-1 day')`;
320
267
  case "week":
321
- return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
268
+ return `started_at >= DATE('now', '-7 days')`;
322
269
  case "month":
323
- return `started_at >= DATE('now', 'start of month')`;
270
+ return `started_at >= DATE('now', '-30 days')`;
324
271
  case "year":
325
- return `started_at >= DATE('now', 'start of year')`;
272
+ return `started_at >= DATE('now', '-365 days')`;
326
273
  case "all":
327
274
  return "1=1";
328
275
  }
@@ -332,17 +279,17 @@ function upsertRequest(db, req) {
332
279
  INSERT OR REPLACE INTO requests
333
280
  (id, agent, session_id, model, input_tokens, output_tokens,
334
281
  cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
335
- timestamp, source_request_id, machine_id)
336
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
337
- `).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 ?? "");
282
+ timestamp, source_request_id)
283
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
284
+ `).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);
338
285
  }
339
286
  function upsertSession(db, session) {
340
287
  db.prepare(`
341
288
  INSERT OR REPLACE INTO sessions
342
289
  (id, agent, project_path, project_name, started_at, ended_at,
343
- total_cost_usd, total_tokens, request_count, machine_id)
344
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
345
- `).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 ?? "");
290
+ total_cost_usd, total_tokens, request_count)
291
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
292
+ `).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);
346
293
  }
347
294
  function rollupSession(db, sessionId) {
348
295
  db.prepare(`
@@ -372,10 +319,6 @@ function querySessions(db, filter = {}) {
372
319
  conditions.push("started_at >= ?");
373
320
  params.push(filter.since);
374
321
  }
375
- if (filter.machine) {
376
- conditions.push("machine_id = ?");
377
- params.push(filter.machine);
378
- }
379
322
  if (filter.search) {
380
323
  const q = `%${filter.search}%`;
381
324
  conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
@@ -394,25 +337,24 @@ function queryTopSessions(db, n = 10, agent) {
394
337
  }
395
338
  return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
396
339
  }
397
- function querySummary(db, period, machine) {
340
+ function querySummary(db, period) {
398
341
  const rWhere = periodWhere(period);
399
342
  const sWhere = sessionPeriodWhere(period);
400
- const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
401
343
  const r = db.prepare(`
402
344
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
403
345
  COUNT(*) as requests,
404
346
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
405
- FROM requests WHERE ${rWhere}${machineClause}
347
+ FROM requests WHERE ${rWhere}
406
348
  `).get();
407
349
  const codexTotals = db.prepare(`
408
350
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
409
351
  COALESCE(SUM(total_tokens), 0) as tokens,
410
352
  COUNT(*) as sessions
411
353
  FROM sessions
412
- WHERE ${sWhere}${machineClause}
354
+ WHERE ${sWhere}
413
355
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
414
356
  `).get();
415
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
357
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
416
358
  return {
417
359
  total_usd: r.total_usd + codexTotals.cost_usd,
418
360
  requests: r.requests,
@@ -434,39 +376,19 @@ function queryModelBreakdown(db) {
434
376
  }
435
377
  function queryProjectBreakdown(db) {
436
378
  return db.prepare(`
437
- WITH labeled AS (
438
- SELECT
439
- s.id,
440
- s.project_path,
441
- s.total_cost_usd,
442
- s.started_at,
443
- COALESCE(
444
- NULLIF(s.project_name, ''),
445
- CASE
446
- WHEN s.project_path LIKE '%/%'
447
- THEN substr(s.project_path, length(rtrim(s.project_path, replace(s.project_path, '/', ''))) + 1)
448
- ELSE s.project_path
449
- END
450
- ) as label
451
- FROM sessions s
452
- WHERE s.project_path != '' OR s.project_name != ''
453
- )
454
379
  SELECT
455
- MIN(l.project_path) as project_path,
456
- l.label as project_name,
457
- COUNT(DISTINCT l.id) as sessions,
458
- COALESCE((SELECT COUNT(*) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)), 0) as requests,
459
- COALESCE(
460
- (SELECT SUM(r.cost_usd) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
461
- SUM(l.total_cost_usd)
462
- ) as cost_usd,
463
- COALESCE(
464
- (SELECT SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens) FROM requests r WHERE r.session_id IN (SELECT id FROM labeled WHERE label = l.label)),
465
- 0
466
- ) as total_tokens,
467
- MAX(l.started_at) as last_active
468
- FROM labeled l
469
- GROUP BY l.label
380
+ s.project_path,
381
+ COALESCE(p.name, s.project_name) as project_name,
382
+ COUNT(DISTINCT s.id) as sessions,
383
+ COUNT(r.id) as requests,
384
+ COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
385
+ COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
386
+ MAX(s.started_at) as last_active
387
+ FROM sessions s
388
+ LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
389
+ LEFT JOIN requests r ON r.session_id = s.id
390
+ WHERE s.project_path != '' OR s.project_name != ''
391
+ GROUP BY s.project_path
470
392
  ORDER BY cost_usd DESC
471
393
  `).all();
472
394
  }
@@ -577,20 +499,6 @@ function getIngestState(db, source, key) {
577
499
  function setIngestState(db, source, key, value) {
578
500
  db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
579
501
  }
580
- function listMachines(db) {
581
- return db.prepare(`
582
- SELECT
583
- s.machine_id,
584
- COUNT(DISTINCT s.id) as sessions,
585
- COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
586
- COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
587
- MAX(s.started_at) as last_active
588
- FROM sessions s
589
- WHERE s.machine_id != ''
590
- GROUP BY s.machine_id
591
- ORDER BY total_cost_usd DESC
592
- `).all();
593
- }
594
502
  function upsertModelPricing(db, p) {
595
503
  db.prepare(`
596
504
  INSERT OR REPLACE INTO model_pricing
@@ -608,11 +516,11 @@ function deleteModelPricing(db, model) {
608
516
  db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
609
517
  }
610
518
  function seedModelPricing(db, defaults) {
611
- const existing = new Set(db.prepare(`SELECT model FROM model_pricing`).all().map((r) => r.model));
519
+ const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
520
+ if (existing.count > 0)
521
+ return;
612
522
  const now = new Date().toISOString();
613
523
  for (const [model, p] of Object.entries(defaults)) {
614
- if (existing.has(model))
615
- continue;
616
524
  upsertModelPricing(db, {
617
525
  model,
618
526
  input_per_1m: p.inputPer1M,
@@ -637,8 +545,7 @@ import { join as join2, basename } from "path";
637
545
  function autoDetectProject(cwd, projects) {
638
546
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
639
547
  }
640
- var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
641
- var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
548
+ var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
642
549
  function dirNameToPath(dirName) {
643
550
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
644
551
  }
@@ -658,36 +565,29 @@ function collectJsonlFiles(projectDir) {
658
565
  return files;
659
566
  }
660
567
  async function ingestClaude(db, verbose = false, _telemetryDir) {
661
- return ingestJsonlProjects(db, CLAUDE_PROJECTS_DIR, "claude", verbose);
662
- }
663
- async function ingestTakumi(db, verbose = false) {
664
- return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
665
- }
666
- async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
667
- if (!existsSync2(projectsDir)) {
568
+ if (!existsSync2(PROJECTS_DIR)) {
668
569
  if (verbose)
669
- console.log(`${agentName} projects dir not found:`, projectsDir);
570
+ console.log("Claude projects dir not found:", PROJECTS_DIR);
670
571
  return { files: 0, requests: 0, sessions: 0 };
671
572
  }
672
- const machineId = getMachineId();
673
573
  let totalFiles = 0;
674
574
  let totalRequests = 0;
675
575
  const touchedSessions = new Set;
676
576
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
677
- const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
577
+ const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
678
578
  for (const projectDirEntry of projectDirs) {
679
- const projectDirPath = join2(projectsDir, projectDirEntry.name);
579
+ const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
680
580
  const projectPath = dirNameToPath(projectDirEntry.name);
681
581
  const jsonlFiles = collectJsonlFiles(projectDirPath);
682
582
  for (const filePath of jsonlFiles) {
683
- const stateKey = filePath.replace(projectsDir, "");
583
+ const stateKey = filePath.replace(PROJECTS_DIR, "");
684
584
  let fileMtime = "0";
685
585
  try {
686
586
  fileMtime = statSync2(filePath).mtimeMs.toString();
687
587
  } catch {
688
588
  continue;
689
589
  }
690
- const processed = getIngestState(db, agentName, stateKey);
590
+ const processed = getIngestState(db, "claude", stateKey);
691
591
  if (processed === fileMtime)
692
592
  continue;
693
593
  let lines;
@@ -728,10 +628,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
728
628
  if (inputTokens + outputTokens + cacheWriteTokens === 0)
729
629
  continue;
730
630
  const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
731
- const reqId = `${agentName}-${sessionId}-${timestamp}`;
631
+ const reqId = `claude-${sessionId}-${timestamp}`;
732
632
  upsertRequest(db, {
733
633
  id: reqId,
734
- agent: agentName,
634
+ agent: "claude",
735
635
  session_id: sessionId,
736
636
  model,
737
637
  input_tokens: inputTokens,
@@ -741,8 +641,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
741
641
  cost_usd: costUsd,
742
642
  duration_ms: 0,
743
643
  timestamp,
744
- source_request_id: reqId,
745
- machine_id: machineId
644
+ source_request_id: reqId
746
645
  });
747
646
  if (!touchedSessions.has(sessionId)) {
748
647
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
@@ -751,15 +650,14 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
751
650
  const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
752
651
  const session = {
753
652
  id: sessionId,
754
- agent: agentName,
653
+ agent: "claude",
755
654
  project_path: detectedProject ? detectedProject.path : effectiveCwd,
756
655
  project_name: detectedProject ? detectedProject.name : "",
757
656
  started_at: timestamp,
758
657
  ended_at: null,
759
658
  total_cost_usd: 0,
760
659
  total_tokens: 0,
761
- request_count: 0,
762
- machine_id: machineId
660
+ request_count: 0
763
661
  };
764
662
  upsertSession(db, session);
765
663
  }
@@ -767,7 +665,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
767
665
  }
768
666
  totalRequests++;
769
667
  }
770
- setIngestState(db, agentName, stateKey, fileMtime);
668
+ setIngestState(db, "claude", stateKey, fileMtime);
771
669
  totalFiles++;
772
670
  }
773
671
  }
@@ -782,7 +680,7 @@ init_database();
782
680
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
783
681
  import { homedir as homedir3 } from "os";
784
682
  import { join as join3, basename as basename2 } from "path";
785
- import { Database as BunDatabase } from "bun:sqlite";
683
+ import { Database as Database2 } from "bun:sqlite";
786
684
  var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
787
685
  var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
788
686
  async function ingestCodex(db, verbose = false) {
@@ -791,11 +689,10 @@ async function ingestCodex(db, verbose = false) {
791
689
  console.log("Codex DB not found:", CODEX_DB_PATH);
792
690
  return { sessions: 0 };
793
691
  }
794
- const machineId = getMachineId();
795
692
  let codexDb = null;
796
693
  let ingested = 0;
797
694
  try {
798
- codexDb = new BunDatabase(CODEX_DB_PATH, { readonly: true });
695
+ codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
799
696
  const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
800
697
  for (const thread of threads) {
801
698
  const stateKey = thread.id;
@@ -816,8 +713,7 @@ async function ingestCodex(db, verbose = false) {
816
713
  ended_at: endedAt,
817
714
  total_cost_usd: costUsd,
818
715
  total_tokens: thread.tokens_used,
819
- request_count: 1,
820
- machine_id: machineId
716
+ request_count: 1
821
717
  });
822
718
  setIngestState(db, "codex", stateKey, "done");
823
719
  ingested++;
@@ -830,85 +726,6 @@ async function ingestCodex(db, verbose = false) {
830
726
  return { sessions: ingested };
831
727
  }
832
728
 
833
- // src/ingest/gemini.ts
834
- init_database();
835
- import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
836
- import { homedir as homedir4 } from "os";
837
- import { join as join4 } from "path";
838
- var GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
839
- async function ingestGemini(db, verbose) {
840
- if (!existsSync4(GEMINI_TMP_DIR)) {
841
- if (verbose)
842
- console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
843
- return { sessions: 0 };
844
- }
845
- const machineId = getMachineId();
846
- let totalSessions = 0;
847
- const touchedSessions = new Set;
848
- let projectHashDirs = [];
849
- try {
850
- projectHashDirs = readdirSync3(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join4(GEMINI_TMP_DIR, d.name));
851
- } catch {
852
- return { sessions: 0 };
853
- }
854
- for (const projectDir of projectHashDirs) {
855
- const chatsDir = join4(projectDir, "chats");
856
- if (!existsSync4(chatsDir))
857
- continue;
858
- let chatFiles = [];
859
- try {
860
- chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join4(chatsDir, f));
861
- } catch {
862
- continue;
863
- }
864
- for (const filePath of chatFiles) {
865
- const stateKey = filePath.replace(homedir4(), "~");
866
- let fileMtime = "0";
867
- try {
868
- fileMtime = statSync3(filePath).mtimeMs.toString();
869
- } catch {
870
- continue;
871
- }
872
- const processed = getIngestState(db, "gemini", stateKey);
873
- if (processed === fileMtime)
874
- continue;
875
- let chatData;
876
- try {
877
- chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
878
- } catch {
879
- continue;
880
- }
881
- const sessionId = chatData.sessionId;
882
- if (!sessionId)
883
- continue;
884
- const startTime = chatData.startTime ?? new Date().toISOString();
885
- const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
886
- if (!existing) {
887
- const session = {
888
- id: sessionId,
889
- agent: "gemini",
890
- project_path: "",
891
- project_name: "",
892
- started_at: startTime,
893
- ended_at: chatData.lastUpdated ?? null,
894
- total_cost_usd: 0,
895
- total_tokens: 0,
896
- request_count: 0,
897
- machine_id: machineId
898
- };
899
- upsertSession(db, session);
900
- touchedSessions.add(sessionId);
901
- totalSessions++;
902
- }
903
- setIngestState(db, "gemini", stateKey, fileMtime);
904
- }
905
- }
906
- for (const sessionId of touchedSessions) {
907
- rollupSession(db, sessionId);
908
- }
909
- return { sessions: totalSessions };
910
- }
911
-
912
729
  // src/server/serve.ts
913
730
  init_pricing();
914
731
  import { randomUUID } from "crypto";
@@ -929,20 +746,6 @@ function ok(data, meta) {
929
746
  function err(message, status = 400) {
930
747
  return json({ error: message }, status);
931
748
  }
932
- function normalizeBudgetPeriod(value) {
933
- switch (value) {
934
- case "day":
935
- case "daily":
936
- return "daily";
937
- case "week":
938
- case "weekly":
939
- return "weekly";
940
- case "month":
941
- case "monthly":
942
- default:
943
- return "monthly";
944
- }
945
- }
946
749
  function applyFields(obj, fields) {
947
750
  if (!fields || fields.length === 0)
948
751
  return obj;
@@ -959,11 +762,7 @@ function createHandler(db) {
959
762
  return ok({ status: "ok", ts: new Date().toISOString() });
960
763
  if (path === "/api/summary" && method === "GET") {
961
764
  const period = url.searchParams.get("period") ?? "today";
962
- const machine = url.searchParams.get("machine") ?? undefined;
963
- return ok(querySummary(db, period, machine));
964
- }
965
- if (path === "/api/machines" && method === "GET") {
966
- return ok(listMachines(db), { current_machine: getMachineId() });
765
+ return ok(querySummary(db, period));
967
766
  }
968
767
  if (path === "/api/daily" && method === "GET") {
969
768
  const days = Number(url.searchParams.get("days") ?? 30);
@@ -972,22 +771,12 @@ function createHandler(db) {
972
771
  if (path === "/api/sessions" && method === "GET") {
973
772
  const agent = url.searchParams.get("agent");
974
773
  const project = url.searchParams.get("project") ?? undefined;
975
- const search = url.searchParams.get("search") ?? undefined;
976
- const machine = url.searchParams.get("machine") ?? undefined;
977
774
  const limit = Number(url.searchParams.get("limit") ?? 50);
978
775
  const offset = Number(url.searchParams.get("offset") ?? 0);
979
776
  const since = url.searchParams.get("since") ?? undefined;
980
777
  const fieldsParam = url.searchParams.get("fields");
981
778
  const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
982
- const sessions = querySessions(db, {
983
- agent: agent ?? undefined,
984
- project,
985
- search,
986
- machine,
987
- limit,
988
- offset,
989
- since
990
- });
779
+ const sessions = querySessions(db, { agent: agent ?? undefined, project, limit, offset, since });
991
780
  return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
992
781
  }
993
782
  if (path === "/api/top" && method === "GET") {
@@ -1015,7 +804,7 @@ function createHandler(db) {
1015
804
  id: randomUUID(),
1016
805
  project_path: body["project_path"] ?? null,
1017
806
  agent: body["agent"] ?? null,
1018
- period: normalizeBudgetPeriod(body["period"]),
807
+ period: body["period"] ?? "monthly",
1019
808
  limit_usd: Number(body["limit_usd"]),
1020
809
  alert_at_percent: Number(body["alert_at_percent"] ?? 80),
1021
810
  created_at: now,
@@ -1076,12 +865,8 @@ function createHandler(db) {
1076
865
  const results = {};
1077
866
  if (sources === "all" || sources === "claude")
1078
867
  results["claude"] = await ingestClaude(db);
1079
- if (sources === "all" || sources === "takumi")
1080
- results["takumi"] = await ingestTakumi(db);
1081
868
  if (sources === "all" || sources === "codex")
1082
869
  results["codex"] = await ingestCodex(db);
1083
- if (sources === "all" || sources === "gemini")
1084
- results["gemini"] = await ingestGemini(db);
1085
870
  return ok(results);
1086
871
  }
1087
872
  const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
@@ -1131,15 +916,15 @@ function startServer(port = 3456) {
1131
916
  return apiHandler(req);
1132
917
  }
1133
918
  try {
1134
- const { existsSync: existsSync5 } = await import("fs");
1135
- if (existsSync5(dashboardDir)) {
919
+ const { existsSync: existsSync4 } = await import("fs");
920
+ if (existsSync4(dashboardDir)) {
1136
921
  let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
1137
922
  const fullPath = dashboardDir + filePath;
1138
- if (existsSync5(fullPath)) {
923
+ if (existsSync4(fullPath)) {
1139
924
  return new Response(Bun.file(fullPath));
1140
925
  }
1141
926
  const indexPath = dashboardDir + "/index.html";
1142
- if (existsSync5(indexPath)) {
927
+ if (existsSync4(indexPath)) {
1143
928
  return new Response(Bun.file(indexPath));
1144
929
  }
1145
930
  }
@@ -1150,62 +935,6 @@ function startServer(port = 3456) {
1150
935
  console.log(`economy-serve listening on http://localhost:${port}`);
1151
936
  }
1152
937
 
1153
- // src/lib/package-metadata.ts
1154
- import { readFileSync as readFileSync4 } from "fs";
1155
- var cachedMetadata = null;
1156
- function getPackageMetadata() {
1157
- if (cachedMetadata)
1158
- return cachedMetadata;
1159
- const raw = readFileSync4(new URL("../../package.json", import.meta.url), "utf8");
1160
- const parsed = JSON.parse(raw);
1161
- cachedMetadata = {
1162
- name: parsed.name ?? "@hasna/economy",
1163
- version: parsed.version ?? "0.0.0"
1164
- };
1165
- return cachedMetadata;
1166
- }
1167
- var packageMetadata = getPackageMetadata();
1168
-
1169
938
  // src/server/index.ts
1170
- function printHelp() {
1171
- console.log(`Usage: economy-serve [options]
1172
-
1173
- REST API server for ${packageMetadata.name}
1174
-
1175
- Options:
1176
- -p, --port <port> Port to bind (default: ECONOMY_PORT or 3456)
1177
- -V, --version output the version number
1178
- -h, --help display help for command`);
1179
- }
1180
- function resolvePort(argv) {
1181
- for (let i = 0;i < argv.length; i++) {
1182
- const arg = argv[i];
1183
- if ((arg === "--port" || arg === "-p") && argv[i + 1]) {
1184
- const value2 = Number(argv[i + 1]);
1185
- if (!Number.isFinite(value2) || value2 <= 0) {
1186
- throw new Error(`Invalid port: ${argv[i + 1]}`);
1187
- }
1188
- return value2;
1189
- }
1190
- }
1191
- const value = Number(process.env["ECONOMY_PORT"] ?? 3456);
1192
- if (!Number.isFinite(value) || value <= 0) {
1193
- throw new Error(`Invalid ECONOMY_PORT: ${process.env["ECONOMY_PORT"]}`);
1194
- }
1195
- return value;
1196
- }
1197
- var args = process.argv.slice(2);
1198
- if (args.includes("--help") || args.includes("-h")) {
1199
- printHelp();
1200
- process.exit(0);
1201
- }
1202
- if (args.includes("--version") || args.includes("-V")) {
1203
- console.log(packageMetadata.version);
1204
- process.exit(0);
1205
- }
1206
- try {
1207
- startServer(resolvePort(args));
1208
- } catch (error) {
1209
- console.error(error instanceof Error ? error.message : String(error));
1210
- process.exit(1);
1211
- }
939
+ var port = Number(process.env["ECONOMY_PORT"] ?? 3456);
940
+ startServer(port);