@hasna/economy 0.1.0 → 0.1.1

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.
@@ -0,0 +1,8 @@
1
+ import type { Agent } from '../../types/index.js';
2
+ interface WatchOptions {
3
+ interval: number;
4
+ agent?: Agent;
5
+ }
6
+ export declare function watchCosts(opts: WatchOptions): Promise<void>;
7
+ export {};
8
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/watch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAEjD,UAAU,YAAY;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,KAAK,CAAA;CACd;AAuBD,wBAAsB,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiElE"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":""}
package/dist/cli/index.js CHANGED
@@ -1,21 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
- var __create = Object.create;
4
- var __getProtoOf = Object.getPrototypeOf;
5
3
  var __defProp = Object.defineProperty;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __toESM = (mod, isNodeMode, target) => {
9
- target = mod != null ? __create(__getProtoOf(mod)) : {};
10
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
- for (let key of __getOwnPropNames(mod))
12
- if (!__hasOwnProp.call(to, key))
13
- __defProp(to, key, {
14
- get: () => mod[key],
15
- enumerable: true
16
- });
17
- return to;
18
- };
19
4
  var __export = (target, all) => {
20
5
  for (var name in all)
21
6
  __defProp(target, name, {
@@ -90,19 +75,19 @@ var DEFAULT_PRICING;
90
75
  var init_pricing = __esm(() => {
91
76
  init_database();
92
77
  DEFAULT_PRICING = {
93
- "claude-opus-4-6": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
94
- "claude-opus-4-5": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
78
+ "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
79
+ "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
95
80
  "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
96
81
  "claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
97
- "claude-haiku-4-5": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1 },
82
+ "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
98
83
  "claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
99
- "claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1 },
84
+ "claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
100
85
  "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
101
86
  "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
102
87
  "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
103
- "gpt-5.3-codex": { inputPer1M: 30, outputPer1M: 120, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
104
- "gpt-5.2-codex": { inputPer1M: 30, outputPer1M: 120, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
105
- "gpt-5-codex": { inputPer1M: 30, outputPer1M: 120, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
88
+ "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
89
+ "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
90
+ "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
106
91
  "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
107
92
  "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
108
93
  o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
@@ -301,8 +286,22 @@ function querySummary(db, period) {
301
286
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
302
287
  FROM requests WHERE ${rWhere}
303
288
  `).get();
304
- const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
305
- return { total_usd: r.total_usd, requests: r.requests, tokens: r.tokens, sessions: s.sessions, period };
289
+ const codexTotals = db.prepare(`
290
+ SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
291
+ COALESCE(SUM(total_tokens), 0) as tokens,
292
+ COUNT(*) as sessions
293
+ FROM sessions
294
+ WHERE ${sWhere}
295
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
296
+ `).get();
297
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
298
+ return {
299
+ total_usd: r.total_usd + codexTotals.cost_usd,
300
+ requests: r.requests,
301
+ tokens: r.tokens + codexTotals.tokens,
302
+ sessions: sessionCount.sessions,
303
+ period
304
+ };
306
305
  }
307
306
  function queryModelBreakdown(db) {
308
307
  return db.prepare(`
@@ -317,14 +316,14 @@ function queryModelBreakdown(db) {
317
316
  }
318
317
  function queryProjectBreakdown(db) {
319
318
  return db.prepare(`
320
- SELECT s.project_path, s.project_name,
321
- COUNT(DISTINCT s.id) as sessions,
322
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
323
- COUNT(r.id) as requests,
324
- COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
325
- MAX(s.started_at) as last_active
326
- FROM sessions s LEFT JOIN requests r ON r.session_id = s.id
327
- GROUP BY s.project_path ORDER BY cost_usd DESC
319
+ SELECT project_path, project_name,
320
+ COUNT(*) as sessions,
321
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
322
+ COALESCE(SUM(request_count), 0) as requests,
323
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
324
+ MAX(started_at) as last_active
325
+ FROM sessions
326
+ GROUP BY project_path ORDER BY cost_usd DESC
328
327
  `).all();
329
328
  }
330
329
  function queryDailyBreakdown(db, days = 30) {
@@ -438,116 +437,139 @@ function seedModelPricing(db, defaults) {
438
437
  var init_database = () => {};
439
438
 
440
439
  // src/ingest/claude.ts
441
- import { readdirSync, readFileSync, existsSync as existsSync2 } from "fs";
440
+ import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
442
441
  import { homedir as homedir2 } from "os";
443
442
  import { join as join2, basename } from "path";
444
- import { randomUUID } from "crypto";
445
- function resolveProjectPath(sessionId) {
446
- if (!existsSync2(PROJECTS_DIR))
447
- return { projectPath: "", projectName: "unknown" };
448
- try {
449
- const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
450
- for (const dir of projectDirs) {
451
- const sessionFile = join2(PROJECTS_DIR, dir.name, `${sessionId}.jsonl`);
452
- if (existsSync2(sessionFile)) {
453
- const projectPath = dir.name.replace(/^-/, "/").replace(/-/g, "/");
454
- return { projectPath, projectName: basename(projectPath) };
443
+ function dirNameToPath(dirName) {
444
+ return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
445
+ }
446
+ function collectJsonlFiles(projectDir) {
447
+ const files = [];
448
+ function walk(dir) {
449
+ try {
450
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
451
+ if (entry.isDirectory())
452
+ walk(join2(dir, entry.name));
453
+ else if (entry.name.endsWith(".jsonl"))
454
+ files.push(join2(dir, entry.name));
455
455
  }
456
- }
457
- } catch {}
458
- return { projectPath: "", projectName: "unknown" };
456
+ } catch {}
457
+ }
458
+ walk(projectDir);
459
+ return files;
459
460
  }
460
- async function ingestClaude(db, verbose = false, telemetryDir = TELEMETRY_DIR) {
461
- if (!existsSync2(telemetryDir)) {
461
+ async function ingestClaude(db, verbose = false, _telemetryDir) {
462
+ if (!existsSync2(PROJECTS_DIR)) {
462
463
  if (verbose)
463
- console.log("Claude telemetry dir not found:", telemetryDir);
464
+ console.log("Claude projects dir not found:", PROJECTS_DIR);
464
465
  return { files: 0, requests: 0, sessions: 0 };
465
466
  }
466
- const files = readdirSync(telemetryDir).filter((f) => f.endsWith(".json"));
467
+ let totalFiles = 0;
467
468
  let totalRequests = 0;
468
- let processedFiles = 0;
469
469
  const touchedSessions = new Set;
470
- for (const filename of files) {
471
- const stateKey = filename;
472
- const processed = getIngestState(db, "claude", stateKey);
473
- if (processed === "done")
474
- continue;
475
- const filePath = join2(telemetryDir, filename);
476
- let events;
477
- try {
478
- const raw = readFileSync(filePath, "utf-8");
479
- events = JSON.parse(raw);
480
- if (!Array.isArray(events))
481
- events = [events];
482
- } catch {
483
- if (verbose)
484
- console.log("Skip unreadable:", filename);
485
- continue;
486
- }
487
- for (const event of events) {
488
- const ed = event.event_data;
489
- if (!ed || ed.event_name !== "tengu_api_success")
470
+ const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
471
+ for (const projectDirEntry of projectDirs) {
472
+ const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
473
+ const projectPath = dirNameToPath(projectDirEntry.name);
474
+ const projectName = basename(projectPath);
475
+ const jsonlFiles = collectJsonlFiles(projectDirPath);
476
+ for (const filePath of jsonlFiles) {
477
+ const stateKey = filePath.replace(PROJECTS_DIR, "");
478
+ let fileMtime = "0";
479
+ try {
480
+ fileMtime = statSync(filePath).mtimeMs.toString();
481
+ } catch {
482
+ continue;
483
+ }
484
+ const processed = getIngestState(db, "claude", stateKey);
485
+ if (processed === fileMtime)
490
486
  continue;
491
- const meta = ed.additional_metadata;
492
- if (!meta)
487
+ let lines;
488
+ try {
489
+ lines = readFileSync(filePath, "utf-8").split(`
490
+ `).filter((l) => l.trim());
491
+ } catch {
493
492
  continue;
494
- const sessionId = ed.session_id ?? randomUUID();
495
- const timestamp = ed.client_timestamp ?? new Date().toISOString();
496
- const model = meta.model ?? ed.model ?? "unknown";
497
- const costUsd = meta.costUSD ?? 0;
498
- const inputTokens = meta.inputTokens ?? (meta.uncachedInputTokens ?? 0);
499
- const outputTokens = meta.outputTokens ?? 0;
500
- const cacheReadTokens = meta.cachedInputTokens ?? 0;
501
- const requestId = meta.requestId ?? randomUUID();
502
- upsertRequest(db, {
503
- id: `claude-${requestId}`,
504
- agent: "claude",
505
- session_id: sessionId,
506
- model,
507
- input_tokens: inputTokens,
508
- output_tokens: outputTokens,
509
- cache_read_tokens: cacheReadTokens,
510
- cache_create_tokens: 0,
511
- cost_usd: costUsd,
512
- duration_ms: meta.durationMs ?? 0,
513
- timestamp,
514
- source_request_id: requestId
515
- });
516
- if (!touchedSessions.has(sessionId)) {
517
- const { projectPath, projectName } = resolveProjectPath(sessionId);
518
- const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
519
- if (!existing) {
520
- const session = {
521
- id: sessionId,
522
- agent: "claude",
523
- project_path: projectPath,
524
- project_name: projectName,
525
- started_at: timestamp,
526
- ended_at: null,
527
- total_cost_usd: 0,
528
- total_tokens: 0,
529
- request_count: 0
530
- };
531
- upsertSession(db, session);
493
+ }
494
+ const fileBasename = basename(filePath, ".jsonl");
495
+ const isUuid = /^[0-9a-f-]{36}$/.test(fileBasename);
496
+ let sessionId = isUuid ? fileBasename : fileBasename.replace(/^agent-/, "");
497
+ let sessionCwd = projectPath;
498
+ for (const line of lines) {
499
+ let entry;
500
+ try {
501
+ entry = JSON.parse(line);
502
+ } catch {
503
+ continue;
532
504
  }
533
- touchedSessions.add(sessionId);
505
+ if (entry.sessionId)
506
+ sessionId = entry.sessionId;
507
+ if (entry.cwd)
508
+ sessionCwd = entry.cwd;
509
+ if (entry.message?.role !== "assistant")
510
+ continue;
511
+ const usage = entry.message.usage;
512
+ if (!usage)
513
+ continue;
514
+ const model = entry.message.model;
515
+ if (!model)
516
+ continue;
517
+ const inputTokens = usage.input_tokens ?? 0;
518
+ const outputTokens = usage.output_tokens ?? 0;
519
+ const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
520
+ const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
521
+ const timestamp = entry.timestamp ?? new Date().toISOString();
522
+ if (inputTokens + outputTokens + cacheWriteTokens === 0)
523
+ continue;
524
+ const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
525
+ const reqId = `claude-${sessionId}-${timestamp}`;
526
+ upsertRequest(db, {
527
+ id: reqId,
528
+ agent: "claude",
529
+ session_id: sessionId,
530
+ model,
531
+ input_tokens: inputTokens,
532
+ output_tokens: outputTokens,
533
+ cache_read_tokens: cacheReadTokens,
534
+ cache_create_tokens: cacheWriteTokens,
535
+ cost_usd: costUsd,
536
+ duration_ms: 0,
537
+ timestamp,
538
+ source_request_id: reqId
539
+ });
540
+ if (!touchedSessions.has(sessionId)) {
541
+ const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
542
+ if (!existing) {
543
+ const session = {
544
+ id: sessionId,
545
+ agent: "claude",
546
+ project_path: sessionCwd || projectPath,
547
+ project_name: basename(sessionCwd || projectPath),
548
+ started_at: timestamp,
549
+ ended_at: null,
550
+ total_cost_usd: 0,
551
+ total_tokens: 0,
552
+ request_count: 0
553
+ };
554
+ upsertSession(db, session);
555
+ }
556
+ touchedSessions.add(sessionId);
557
+ }
558
+ totalRequests++;
534
559
  }
535
- totalRequests++;
560
+ setIngestState(db, "claude", stateKey, fileMtime);
561
+ totalFiles++;
536
562
  }
537
- setIngestState(db, "claude", stateKey, "done");
538
- processedFiles++;
539
- if (verbose)
540
- console.log(`Processed ${filename}: found ${events.length} events`);
541
563
  }
542
564
  for (const sessionId of touchedSessions) {
543
565
  rollupSession(db, sessionId);
544
566
  }
545
- return { files: processedFiles, requests: totalRequests, sessions: touchedSessions.size };
567
+ return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
546
568
  }
547
- var TELEMETRY_DIR, PROJECTS_DIR;
569
+ var PROJECTS_DIR;
548
570
  var init_claude = __esm(() => {
549
571
  init_database();
550
- TELEMETRY_DIR = join2(homedir2(), ".claude", "telemetry");
572
+ init_pricing();
551
573
  PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
552
574
  });
553
575
 
@@ -584,9 +606,7 @@ async function ingestCodex(db, verbose = false) {
584
606
  const processed = getIngestState(db, "codex", stateKey);
585
607
  if (processed === "done")
586
608
  continue;
587
- const inputTokens = Math.floor(thread.tokens_used * 0.6);
588
- const outputTokens = thread.tokens_used - inputTokens;
589
- const costUsd = computeCost(model, inputTokens, outputTokens);
609
+ const costUsd = 0;
590
610
  const projectPath = thread.cwd ?? "";
591
611
  const projectName = projectPath ? basename2(projectPath) : "unknown";
592
612
  const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
@@ -615,7 +635,6 @@ async function ingestCodex(db, verbose = false) {
615
635
  var CODEX_DB_PATH, CODEX_CONFIG_PATH;
616
636
  var init_codex = __esm(() => {
617
637
  init_database();
618
- init_pricing();
619
638
  CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
620
639
  CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
621
640
  });
@@ -703,7 +722,7 @@ var init_watch = __esm(() => {
703
722
  });
704
723
 
705
724
  // src/server/serve.ts
706
- import { randomUUID as randomUUID2 } from "crypto";
725
+ import { randomUUID } from "crypto";
707
726
  function json(data, status = 200) {
708
727
  return new Response(JSON.stringify(data), {
709
728
  status,
@@ -764,7 +783,7 @@ function createHandler(db) {
764
783
  const body = await req.json();
765
784
  const now = new Date().toISOString();
766
785
  upsertBudget(db, {
767
- id: randomUUID2(),
786
+ id: randomUUID(),
768
787
  project_path: body["project_path"] ?? null,
769
788
  agent: body["agent"] ?? null,
770
789
  period: body["period"] ?? "monthly",
@@ -788,7 +807,7 @@ function createHandler(db) {
788
807
  const { basename: basename3 } = await import("path");
789
808
  const projPath = body["path"];
790
809
  upsertProject(db, {
791
- id: randomUUID2(),
810
+ id: randomUUID(),
792
811
  path: projPath,
793
812
  name: body["name"] ?? basename3(projPath),
794
813
  description: body["description"] ?? null,
@@ -838,8 +857,32 @@ function createHandler(db) {
838
857
  function startServer(port = 3456) {
839
858
  const db = openDatabase();
840
859
  ensurePricingSeeded(db);
841
- const handler = createHandler(db);
842
- Bun.serve({ port, fetch: handler });
860
+ const apiHandler = createHandler(db);
861
+ const dashboardDir = new URL("../../dashboard/dist", import.meta.url).pathname;
862
+ Bun.serve({
863
+ port,
864
+ async fetch(req) {
865
+ const url = new URL(req.url);
866
+ if (url.pathname.startsWith("/api") || url.pathname === "/health") {
867
+ return apiHandler(req);
868
+ }
869
+ try {
870
+ const { existsSync: existsSync4 } = await import("fs");
871
+ if (existsSync4(dashboardDir)) {
872
+ let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
873
+ const fullPath = dashboardDir + filePath;
874
+ if (existsSync4(fullPath)) {
875
+ return new Response(Bun.file(fullPath));
876
+ }
877
+ const indexPath = dashboardDir + "/index.html";
878
+ if (existsSync4(indexPath)) {
879
+ return new Response(Bun.file(indexPath));
880
+ }
881
+ }
882
+ } catch {}
883
+ return apiHandler(req);
884
+ }
885
+ });
843
886
  console.log(`economy-serve listening on http://localhost:${port}`);
844
887
  }
845
888
  var CORS;
@@ -871,19 +914,30 @@ init_codex();
871
914
  init_pricing();
872
915
  import { Command } from "commander";
873
916
  import chalk2 from "chalk";
874
- import { randomUUID as randomUUID3 } from "crypto";
917
+ import { randomUUID as randomUUID2 } from "crypto";
875
918
  import { execSync } from "child_process";
876
919
  var program = new Command;
877
920
  program.name("economy").description("AI coding cost tracker \u2014 Claude Code, Codex, and Gemini").version("0.1.0");
878
921
  function fmt2(usd) {
879
- return chalk2.green(`$${usd.toFixed(4)}`);
922
+ let formatted;
923
+ if (usd >= 0.01) {
924
+ formatted = "$" + usd.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
925
+ } else {
926
+ formatted = "$" + usd.toFixed(6);
927
+ }
928
+ return chalk2.green(formatted);
880
929
  }
881
930
  function fmtTokens(n) {
931
+ if (n >= 1e9)
932
+ return `${(n / 1e9).toFixed(1)}B`;
882
933
  if (n >= 1e6)
883
- return `${(n / 1e6).toFixed(2)}M`;
934
+ return `${(n / 1e6).toFixed(1)}M`;
884
935
  if (n >= 1000)
885
936
  return `${(n / 1000).toFixed(1)}k`;
886
- return String(n);
937
+ return n.toLocaleString("en-US");
938
+ }
939
+ function fmtCount(n) {
940
+ return n.toLocaleString("en-US");
887
941
  }
888
942
  function printTable(headers, rows) {
889
943
  const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").replace(/\x1b\[[0-9;]*m/g, "").length)));
@@ -910,8 +964,8 @@ function printSummary(label, period) {
910
964
  console.log();
911
965
  printTable(["Metric", "Value"], [
912
966
  ["Total cost", fmt2(s.total_usd)],
913
- ["Sessions", chalk2.yellow(String(s.sessions))],
914
- ["Requests", chalk2.yellow(String(s.requests))],
967
+ ["Sessions", chalk2.yellow(fmtCount(s.sessions))],
968
+ ["Requests", chalk2.yellow(fmtCount(s.requests))],
915
969
  ["Tokens", chalk2.yellow(fmtTokens(s.tokens))]
916
970
  ]);
917
971
  console.log();
@@ -955,7 +1009,7 @@ program.command("sessions").description("List coding sessions with costs").optio
955
1009
  chalk2.white(s.project_name || chalk2.dim("unknown")),
956
1010
  fmt2(s.total_cost_usd),
957
1011
  chalk2.cyan(fmtTokens(s.total_tokens)),
958
- String(s.request_count),
1012
+ fmtCount(s.request_count),
959
1013
  chalk2.dim(s.started_at.substring(0, 16))
960
1014
  ]));
961
1015
  console.log();
@@ -1015,7 +1069,7 @@ budgetCmd.command("set").description("Set a budget").option("--project <path>",
1015
1069
  const db = openDatabase();
1016
1070
  const now = new Date().toISOString();
1017
1071
  upsertBudget(db, {
1018
- id: randomUUID3(),
1072
+ id: randomUUID2(),
1019
1073
  project_path: opts.project ?? null,
1020
1074
  agent: opts.agent ?? null,
1021
1075
  period: opts.period ?? "monthly",
@@ -1059,7 +1113,7 @@ projectCmd.command("add <path>").description("Add a project").option("--name <na
1059
1113
  const db = openDatabase();
1060
1114
  const { basename: basename3 } = __require("path");
1061
1115
  upsertProject(db, {
1062
- id: randomUUID3(),
1116
+ id: randomUUID2(),
1063
1117
  path,
1064
1118
  name: opts.name ?? basename3(path),
1065
1119
  description: null,
@@ -1142,13 +1196,48 @@ program.command("serve").description("Start the REST API server").option("-p, --
1142
1196
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
1143
1197
  startServer2(port2);
1144
1198
  });
1145
- program.command("dashboard").description("Open the web dashboard in browser").option("-p, --port <port>", "Server port", "3456").action((opts) => {
1146
- const port2 = opts.port ?? "3456";
1147
- console.log(chalk2.cyan(`Opening dashboard at http://localhost:${port2}`));
1199
+ program.command("dashboard").description("Open the web dashboard (auto-starts server if not running)").option("-p, --port <port>", "Server port", "3456").action(async (opts) => {
1200
+ const port2 = Number(opts.port ?? 3456);
1201
+ const url = `http://localhost:${port2}`;
1202
+ let serverRunning = false;
1203
+ try {
1204
+ const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(500) });
1205
+ serverRunning = res.ok;
1206
+ } catch {}
1207
+ if (!serverRunning) {
1208
+ console.log(chalk2.cyan(`\u2192 Starting economy server on port ${port2}...`));
1209
+ const { spawn } = await import("child_process");
1210
+ const { resolve, dirname: dirname2 } = await import("path");
1211
+ const serveScript = resolve(dirname2(process.argv[1]), "..", "server", "index.js");
1212
+ const child = spawn(process.execPath, [serveScript], {
1213
+ detached: true,
1214
+ stdio: "ignore",
1215
+ env: { ...process.env, ECONOMY_PORT: String(port2) }
1216
+ });
1217
+ child.unref();
1218
+ let attempts = 0;
1219
+ while (attempts < 20) {
1220
+ await new Promise((r) => setTimeout(r, 250));
1221
+ try {
1222
+ const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(300) });
1223
+ if (res.ok) {
1224
+ serverRunning = true;
1225
+ break;
1226
+ }
1227
+ } catch {}
1228
+ attempts++;
1229
+ }
1230
+ if (serverRunning) {
1231
+ console.log(chalk2.green(`\u2713 Server started`));
1232
+ } else {
1233
+ console.log(chalk2.yellow(`\u26A0 Server didn't respond \u2014 open ${url} manually after running \`economy serve\``));
1234
+ }
1235
+ }
1236
+ console.log(chalk2.cyan(`Opening ${url}`));
1148
1237
  try {
1149
- execSync(`open http://localhost:${port2}`);
1238
+ execSync(`open ${url}`);
1150
1239
  } catch {
1151
- console.log(chalk2.yellow(`Run \`economy serve\` first, then open http://localhost:${port2}`));
1240
+ console.log(chalk2.yellow(`Open your browser at ${url}`));
1152
1241
  }
1153
1242
  });
1154
1243
  program.command("mcp").description("Show MCP server install commands").option("--claude", "Install into Claude Code").option("--codex", "Install into Codex").option("--all", "Install into all agents").action(async (opts) => {
@@ -0,0 +1,47 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import type { EconomyRequest, EconomySession, EconomyProject, Budget, BudgetStatus, CostSummary, ModelBreakdown, ProjectBreakdown, Period, SessionFilter } from '../types/index.js';
3
+ export declare function getDbPath(): string;
4
+ export declare function openDatabase(dbPath?: string, skipSeed?: boolean): Database;
5
+ export declare function upsertRequest(db: Database, req: EconomyRequest): void;
6
+ export declare function upsertSession(db: Database, session: EconomySession): void;
7
+ export declare function rollupSession(db: Database, sessionId: string): void;
8
+ export declare function querySessions(db: Database, filter?: SessionFilter): EconomySession[];
9
+ export declare function queryTopSessions(db: Database, n?: number, agent?: string): EconomySession[];
10
+ export declare function querySummary(db: Database, period: Period): CostSummary;
11
+ export declare function queryModelBreakdown(db: Database): ModelBreakdown[];
12
+ export declare function queryProjectBreakdown(db: Database): ProjectBreakdown[];
13
+ export declare function queryDailyBreakdown(db: Database, days?: number): Array<{
14
+ date: string;
15
+ cost_usd: number;
16
+ agent: string;
17
+ }>;
18
+ export declare function upsertProject(db: Database, project: EconomyProject): void;
19
+ export declare function getProject(db: Database, path: string): EconomyProject | null;
20
+ export declare function listProjects(db: Database): EconomyProject[];
21
+ export declare function deleteProject(db: Database, path: string): void;
22
+ export declare function upsertBudget(db: Database, budget: Budget): void;
23
+ export declare function listBudgets(db: Database): Budget[];
24
+ export declare function deleteBudget(db: Database, id: string): void;
25
+ export declare function getBudgetStatuses(db: Database): BudgetStatus[];
26
+ export declare function getIngestState(db: Database, source: string, key: string): string | null;
27
+ export declare function setIngestState(db: Database, source: string, key: string, value: string): void;
28
+ export declare function queryRequestsSince(db: Database, since: string): EconomyRequest[];
29
+ export interface DbModelPricing {
30
+ model: string;
31
+ input_per_1m: number;
32
+ output_per_1m: number;
33
+ cache_read_per_1m: number;
34
+ cache_write_per_1m: number;
35
+ updated_at: string;
36
+ }
37
+ export declare function upsertModelPricing(db: Database, p: DbModelPricing): void;
38
+ export declare function getModelPricing(db: Database, model: string): DbModelPricing | null;
39
+ export declare function listModelPricing(db: Database): DbModelPricing[];
40
+ export declare function deleteModelPricing(db: Database, model: string): void;
41
+ export declare function seedModelPricing(db: Database, defaults: Record<string, {
42
+ inputPer1M: number;
43
+ outputPer1M: number;
44
+ cacheReadPer1M: number;
45
+ cacheWritePer1M: number;
46
+ }>): void;
47
+ //# sourceMappingURL=database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAIrC,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,cAAc,EACd,MAAM,EACN,YAAY,EACZ,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,MAAM,EACN,aAAa,EACd,MAAM,mBAAmB,CAAA;AAE1B,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,UAAQ,GAAG,QAAQ,CAexE;AAgGD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAarE;AAID,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAWzE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAYnE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,aAAkB,GAAG,cAAc,EAAE,CAYxF;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,SAAK,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CAKvF;AAID,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CA+BtE;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAUlE;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,GAAG,gBAAgB,EAAE,CAWtE;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,SAAK,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAQrH;AAID,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAKzE;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAI5E;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAG3D;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE9D;AAID,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAU/D;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,EAAE,CAElD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,YAAY,EAAE,CA2B9D;AAID,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGvF;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE7F;AAID,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAEhF;AAID,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,cAAc,GAAG,IAAI,CAMxE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAElF;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAE/D;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAEpE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,CAc3K"}
@@ -0,0 +1,6 @@
1
+ export * from './types/index.js';
2
+ export * from './db/database.js';
3
+ export * from './lib/pricing.js';
4
+ export * from './ingest/claude.js';
5
+ export * from './ingest/codex.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA"}