@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.
@@ -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,
@@ -432,66 +374,23 @@ function queryModelBreakdown(db) {
432
374
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
433
375
  `).all();
434
376
  }
435
- function labelForPath(projectPath, projectName) {
436
- if (projectName && projectName.trim() !== "")
437
- return projectName;
438
- if (!projectPath)
439
- return "";
440
- const segments = projectPath.split("/").filter(Boolean);
441
- const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
442
- for (const seg of segments) {
443
- if (projectPrefix.test(seg))
444
- return seg;
445
- }
446
- const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
447
- for (let i = segments.length - 1;i >= 0; i--) {
448
- if (!generic.has(segments[i].toLowerCase()))
449
- return segments[i];
450
- }
451
- return segments[segments.length - 1] ?? projectPath;
452
- }
453
377
  function queryProjectBreakdown(db) {
454
- const sessions = db.prepare(`
455
- SELECT id, project_path, project_name, total_cost_usd, started_at
456
- FROM sessions
457
- WHERE project_path != '' OR project_name != ''
378
+ return db.prepare(`
379
+ SELECT
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
392
+ ORDER BY cost_usd DESC
458
393
  `).all();
459
- const groups = new Map;
460
- for (const s of sessions) {
461
- const label = labelForPath(s.project_path, s.project_name);
462
- if (!label)
463
- continue;
464
- const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
465
- g.sessionIds.push(s.id);
466
- g.totalCost += s.total_cost_usd || 0;
467
- if (!g.lastActive || s.started_at > g.lastActive)
468
- g.lastActive = s.started_at;
469
- if (!g.samplePath)
470
- g.samplePath = s.project_path;
471
- groups.set(label, g);
472
- }
473
- const result = [];
474
- for (const [label, g] of groups.entries()) {
475
- const placeholders = g.sessionIds.map(() => "?").join(",");
476
- const reqStats = placeholders.length ? db.prepare(`
477
- SELECT
478
- COUNT(*) as requests,
479
- COALESCE(SUM(cost_usd), 0) as cost_usd,
480
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
481
- FROM requests WHERE session_id IN (${placeholders})
482
- `).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
483
- result.push({
484
- project_path: g.samplePath,
485
- project_name: label,
486
- sessions: g.sessionIds.length,
487
- requests: reqStats.requests,
488
- total_tokens: reqStats.total_tokens,
489
- cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
490
- last_active: g.lastActive
491
- });
492
- }
493
- result.sort((a, b) => b.cost_usd - a.cost_usd);
494
- return result;
495
394
  }
496
395
  function queryDailyBreakdown(db, days = 30) {
497
396
  return db.prepare(`
@@ -600,20 +499,6 @@ function getIngestState(db, source, key) {
600
499
  function setIngestState(db, source, key, value) {
601
500
  db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
602
501
  }
603
- function listMachines(db) {
604
- return db.prepare(`
605
- SELECT
606
- s.machine_id,
607
- COUNT(DISTINCT s.id) as sessions,
608
- COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
609
- COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
610
- MAX(s.started_at) as last_active
611
- FROM sessions s
612
- WHERE s.machine_id != ''
613
- GROUP BY s.machine_id
614
- ORDER BY total_cost_usd DESC
615
- `).all();
616
- }
617
502
  function upsertModelPricing(db, p) {
618
503
  db.prepare(`
619
504
  INSERT OR REPLACE INTO model_pricing
@@ -631,11 +516,11 @@ function deleteModelPricing(db, model) {
631
516
  db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
632
517
  }
633
518
  function seedModelPricing(db, defaults) {
634
- 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;
635
522
  const now = new Date().toISOString();
636
523
  for (const [model, p] of Object.entries(defaults)) {
637
- if (existing.has(model))
638
- continue;
639
524
  upsertModelPricing(db, {
640
525
  model,
641
526
  input_per_1m: p.inputPer1M,
@@ -660,8 +545,7 @@ import { join as join2, basename } from "path";
660
545
  function autoDetectProject(cwd, projects) {
661
546
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
662
547
  }
663
- var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
664
- var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
548
+ var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
665
549
  function dirNameToPath(dirName) {
666
550
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
667
551
  }
@@ -681,36 +565,29 @@ function collectJsonlFiles(projectDir) {
681
565
  return files;
682
566
  }
683
567
  async function ingestClaude(db, verbose = false, _telemetryDir) {
684
- return ingestJsonlProjects(db, CLAUDE_PROJECTS_DIR, "claude", verbose);
685
- }
686
- async function ingestTakumi(db, verbose = false) {
687
- return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
688
- }
689
- async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
690
- if (!existsSync2(projectsDir)) {
568
+ if (!existsSync2(PROJECTS_DIR)) {
691
569
  if (verbose)
692
- console.log(`${agentName} projects dir not found:`, projectsDir);
570
+ console.log("Claude projects dir not found:", PROJECTS_DIR);
693
571
  return { files: 0, requests: 0, sessions: 0 };
694
572
  }
695
- const machineId = getMachineId();
696
573
  let totalFiles = 0;
697
574
  let totalRequests = 0;
698
575
  const touchedSessions = new Set;
699
576
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
700
- const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
577
+ const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
701
578
  for (const projectDirEntry of projectDirs) {
702
- const projectDirPath = join2(projectsDir, projectDirEntry.name);
579
+ const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
703
580
  const projectPath = dirNameToPath(projectDirEntry.name);
704
581
  const jsonlFiles = collectJsonlFiles(projectDirPath);
705
582
  for (const filePath of jsonlFiles) {
706
- const stateKey = filePath.replace(projectsDir, "");
583
+ const stateKey = filePath.replace(PROJECTS_DIR, "");
707
584
  let fileMtime = "0";
708
585
  try {
709
586
  fileMtime = statSync2(filePath).mtimeMs.toString();
710
587
  } catch {
711
588
  continue;
712
589
  }
713
- const processed = getIngestState(db, agentName, stateKey);
590
+ const processed = getIngestState(db, "claude", stateKey);
714
591
  if (processed === fileMtime)
715
592
  continue;
716
593
  let lines;
@@ -751,10 +628,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
751
628
  if (inputTokens + outputTokens + cacheWriteTokens === 0)
752
629
  continue;
753
630
  const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
754
- const reqId = `${agentName}-${sessionId}-${timestamp}`;
631
+ const reqId = `claude-${sessionId}-${timestamp}`;
755
632
  upsertRequest(db, {
756
633
  id: reqId,
757
- agent: agentName,
634
+ agent: "claude",
758
635
  session_id: sessionId,
759
636
  model,
760
637
  input_tokens: inputTokens,
@@ -764,8 +641,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
764
641
  cost_usd: costUsd,
765
642
  duration_ms: 0,
766
643
  timestamp,
767
- source_request_id: reqId,
768
- machine_id: machineId
644
+ source_request_id: reqId
769
645
  });
770
646
  if (!touchedSessions.has(sessionId)) {
771
647
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
@@ -774,15 +650,14 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
774
650
  const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
775
651
  const session = {
776
652
  id: sessionId,
777
- agent: agentName,
653
+ agent: "claude",
778
654
  project_path: detectedProject ? detectedProject.path : effectiveCwd,
779
655
  project_name: detectedProject ? detectedProject.name : "",
780
656
  started_at: timestamp,
781
657
  ended_at: null,
782
658
  total_cost_usd: 0,
783
659
  total_tokens: 0,
784
- request_count: 0,
785
- machine_id: machineId
660
+ request_count: 0
786
661
  };
787
662
  upsertSession(db, session);
788
663
  }
@@ -790,7 +665,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
790
665
  }
791
666
  totalRequests++;
792
667
  }
793
- setIngestState(db, agentName, stateKey, fileMtime);
668
+ setIngestState(db, "claude", stateKey, fileMtime);
794
669
  totalFiles++;
795
670
  }
796
671
  }
@@ -805,7 +680,7 @@ init_database();
805
680
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
806
681
  import { homedir as homedir3 } from "os";
807
682
  import { join as join3, basename as basename2 } from "path";
808
- import { Database as BunDatabase } from "bun:sqlite";
683
+ import { Database as Database2 } from "bun:sqlite";
809
684
  var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
810
685
  var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
811
686
  async function ingestCodex(db, verbose = false) {
@@ -814,11 +689,10 @@ async function ingestCodex(db, verbose = false) {
814
689
  console.log("Codex DB not found:", CODEX_DB_PATH);
815
690
  return { sessions: 0 };
816
691
  }
817
- const machineId = getMachineId();
818
692
  let codexDb = null;
819
693
  let ingested = 0;
820
694
  try {
821
- codexDb = new BunDatabase(CODEX_DB_PATH, { readonly: true });
695
+ codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
822
696
  const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
823
697
  for (const thread of threads) {
824
698
  const stateKey = thread.id;
@@ -839,8 +713,7 @@ async function ingestCodex(db, verbose = false) {
839
713
  ended_at: endedAt,
840
714
  total_cost_usd: costUsd,
841
715
  total_tokens: thread.tokens_used,
842
- request_count: 1,
843
- machine_id: machineId
716
+ request_count: 1
844
717
  });
845
718
  setIngestState(db, "codex", stateKey, "done");
846
719
  ingested++;
@@ -853,85 +726,6 @@ async function ingestCodex(db, verbose = false) {
853
726
  return { sessions: ingested };
854
727
  }
855
728
 
856
- // src/ingest/gemini.ts
857
- init_database();
858
- import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
859
- import { homedir as homedir4 } from "os";
860
- import { join as join4 } from "path";
861
- var GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
862
- async function ingestGemini(db, verbose) {
863
- if (!existsSync4(GEMINI_TMP_DIR)) {
864
- if (verbose)
865
- console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
866
- return { sessions: 0 };
867
- }
868
- const machineId = getMachineId();
869
- let totalSessions = 0;
870
- const touchedSessions = new Set;
871
- let projectHashDirs = [];
872
- try {
873
- 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));
874
- } catch {
875
- return { sessions: 0 };
876
- }
877
- for (const projectDir of projectHashDirs) {
878
- const chatsDir = join4(projectDir, "chats");
879
- if (!existsSync4(chatsDir))
880
- continue;
881
- let chatFiles = [];
882
- try {
883
- chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join4(chatsDir, f));
884
- } catch {
885
- continue;
886
- }
887
- for (const filePath of chatFiles) {
888
- const stateKey = filePath.replace(homedir4(), "~");
889
- let fileMtime = "0";
890
- try {
891
- fileMtime = statSync3(filePath).mtimeMs.toString();
892
- } catch {
893
- continue;
894
- }
895
- const processed = getIngestState(db, "gemini", stateKey);
896
- if (processed === fileMtime)
897
- continue;
898
- let chatData;
899
- try {
900
- chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
901
- } catch {
902
- continue;
903
- }
904
- const sessionId = chatData.sessionId;
905
- if (!sessionId)
906
- continue;
907
- const startTime = chatData.startTime ?? new Date().toISOString();
908
- const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
909
- if (!existing) {
910
- const session = {
911
- id: sessionId,
912
- agent: "gemini",
913
- project_path: "",
914
- project_name: "",
915
- started_at: startTime,
916
- ended_at: chatData.lastUpdated ?? null,
917
- total_cost_usd: 0,
918
- total_tokens: 0,
919
- request_count: 0,
920
- machine_id: machineId
921
- };
922
- upsertSession(db, session);
923
- touchedSessions.add(sessionId);
924
- totalSessions++;
925
- }
926
- setIngestState(db, "gemini", stateKey, fileMtime);
927
- }
928
- }
929
- for (const sessionId of touchedSessions) {
930
- rollupSession(db, sessionId);
931
- }
932
- return { sessions: totalSessions };
933
- }
934
-
935
729
  // src/server/serve.ts
936
730
  init_pricing();
937
731
  import { randomUUID } from "crypto";
@@ -952,20 +746,6 @@ function ok(data, meta) {
952
746
  function err(message, status = 400) {
953
747
  return json({ error: message }, status);
954
748
  }
955
- function normalizeBudgetPeriod(value) {
956
- switch (value) {
957
- case "day":
958
- case "daily":
959
- return "daily";
960
- case "week":
961
- case "weekly":
962
- return "weekly";
963
- case "month":
964
- case "monthly":
965
- default:
966
- return "monthly";
967
- }
968
- }
969
749
  function applyFields(obj, fields) {
970
750
  if (!fields || fields.length === 0)
971
751
  return obj;
@@ -982,11 +762,7 @@ function createHandler(db) {
982
762
  return ok({ status: "ok", ts: new Date().toISOString() });
983
763
  if (path === "/api/summary" && method === "GET") {
984
764
  const period = url.searchParams.get("period") ?? "today";
985
- const machine = url.searchParams.get("machine") ?? undefined;
986
- return ok(querySummary(db, period, machine));
987
- }
988
- if (path === "/api/machines" && method === "GET") {
989
- return ok(listMachines(db), { current_machine: getMachineId() });
765
+ return ok(querySummary(db, period));
990
766
  }
991
767
  if (path === "/api/daily" && method === "GET") {
992
768
  const days = Number(url.searchParams.get("days") ?? 30);
@@ -995,22 +771,12 @@ function createHandler(db) {
995
771
  if (path === "/api/sessions" && method === "GET") {
996
772
  const agent = url.searchParams.get("agent");
997
773
  const project = url.searchParams.get("project") ?? undefined;
998
- const search = url.searchParams.get("search") ?? undefined;
999
- const machine = url.searchParams.get("machine") ?? undefined;
1000
774
  const limit = Number(url.searchParams.get("limit") ?? 50);
1001
775
  const offset = Number(url.searchParams.get("offset") ?? 0);
1002
776
  const since = url.searchParams.get("since") ?? undefined;
1003
777
  const fieldsParam = url.searchParams.get("fields");
1004
778
  const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
1005
- const sessions = querySessions(db, {
1006
- agent: agent ?? undefined,
1007
- project,
1008
- search,
1009
- machine,
1010
- limit,
1011
- offset,
1012
- since
1013
- });
779
+ const sessions = querySessions(db, { agent: agent ?? undefined, project, limit, offset, since });
1014
780
  return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
1015
781
  }
1016
782
  if (path === "/api/top" && method === "GET") {
@@ -1038,7 +804,7 @@ function createHandler(db) {
1038
804
  id: randomUUID(),
1039
805
  project_path: body["project_path"] ?? null,
1040
806
  agent: body["agent"] ?? null,
1041
- period: normalizeBudgetPeriod(body["period"]),
807
+ period: body["period"] ?? "monthly",
1042
808
  limit_usd: Number(body["limit_usd"]),
1043
809
  alert_at_percent: Number(body["alert_at_percent"] ?? 80),
1044
810
  created_at: now,
@@ -1099,12 +865,8 @@ function createHandler(db) {
1099
865
  const results = {};
1100
866
  if (sources === "all" || sources === "claude")
1101
867
  results["claude"] = await ingestClaude(db);
1102
- if (sources === "all" || sources === "takumi")
1103
- results["takumi"] = await ingestTakumi(db);
1104
868
  if (sources === "all" || sources === "codex")
1105
869
  results["codex"] = await ingestCodex(db);
1106
- if (sources === "all" || sources === "gemini")
1107
- results["gemini"] = await ingestGemini(db);
1108
870
  return ok(results);
1109
871
  }
1110
872
  const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
@@ -1154,15 +916,15 @@ function startServer(port = 3456) {
1154
916
  return apiHandler(req);
1155
917
  }
1156
918
  try {
1157
- const { existsSync: existsSync5 } = await import("fs");
1158
- if (existsSync5(dashboardDir)) {
919
+ const { existsSync: existsSync4 } = await import("fs");
920
+ if (existsSync4(dashboardDir)) {
1159
921
  let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
1160
922
  const fullPath = dashboardDir + filePath;
1161
- if (existsSync5(fullPath)) {
923
+ if (existsSync4(fullPath)) {
1162
924
  return new Response(Bun.file(fullPath));
1163
925
  }
1164
926
  const indexPath = dashboardDir + "/index.html";
1165
- if (existsSync5(indexPath)) {
927
+ if (existsSync4(indexPath)) {
1166
928
  return new Response(Bun.file(indexPath));
1167
929
  }
1168
930
  }
@@ -1173,62 +935,6 @@ function startServer(port = 3456) {
1173
935
  console.log(`economy-serve listening on http://localhost:${port}`);
1174
936
  }
1175
937
 
1176
- // src/lib/package-metadata.ts
1177
- import { readFileSync as readFileSync4 } from "fs";
1178
- var cachedMetadata = null;
1179
- function getPackageMetadata() {
1180
- if (cachedMetadata)
1181
- return cachedMetadata;
1182
- const raw = readFileSync4(new URL("../../package.json", import.meta.url), "utf8");
1183
- const parsed = JSON.parse(raw);
1184
- cachedMetadata = {
1185
- name: parsed.name ?? "@hasna/economy",
1186
- version: parsed.version ?? "0.0.0"
1187
- };
1188
- return cachedMetadata;
1189
- }
1190
- var packageMetadata = getPackageMetadata();
1191
-
1192
938
  // src/server/index.ts
1193
- function printHelp() {
1194
- console.log(`Usage: economy-serve [options]
1195
-
1196
- REST API server for ${packageMetadata.name}
1197
-
1198
- Options:
1199
- -p, --port <port> Port to bind (default: ECONOMY_PORT or 3456)
1200
- -V, --version output the version number
1201
- -h, --help display help for command`);
1202
- }
1203
- function resolvePort(argv) {
1204
- for (let i = 0;i < argv.length; i++) {
1205
- const arg = argv[i];
1206
- if ((arg === "--port" || arg === "-p") && argv[i + 1]) {
1207
- const value2 = Number(argv[i + 1]);
1208
- if (!Number.isFinite(value2) || value2 <= 0) {
1209
- throw new Error(`Invalid port: ${argv[i + 1]}`);
1210
- }
1211
- return value2;
1212
- }
1213
- }
1214
- const value = Number(process.env["ECONOMY_PORT"] ?? 3456);
1215
- if (!Number.isFinite(value) || value <= 0) {
1216
- throw new Error(`Invalid ECONOMY_PORT: ${process.env["ECONOMY_PORT"]}`);
1217
- }
1218
- return value;
1219
- }
1220
- var args = process.argv.slice(2);
1221
- if (args.includes("--help") || args.includes("-h")) {
1222
- printHelp();
1223
- process.exit(0);
1224
- }
1225
- if (args.includes("--version") || args.includes("-V")) {
1226
- console.log(packageMetadata.version);
1227
- process.exit(0);
1228
- }
1229
- try {
1230
- startServer(resolvePort(args));
1231
- } catch (error) {
1232
- console.error(error instanceof Error ? error.message : String(error));
1233
- process.exit(1);
1234
- }
939
+ var port = Number(process.env["ECONOMY_PORT"] ?? 3456);
940
+ startServer(port);