@hasna/economy 0.2.40 → 0.2.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -528,6 +528,7 @@ __export(exports_database, {
528
528
  setIngestState: () => setIngestState,
529
529
  seedModelPricing: () => seedModelPricing,
530
530
  rollupSession: () => rollupSession,
531
+ queryZeroCostTokenizedModels: () => queryZeroCostTokenizedModels,
531
532
  queryUsageSnapshots: () => queryUsageSnapshots,
532
533
  queryTopSessions: () => queryTopSessions,
533
534
  querySummary: () => querySummary,
@@ -623,6 +624,32 @@ function getDbPath() {
623
624
  return process.env["ECONOMY_DB"];
624
625
  return join(getDataDir(), "economy.db");
625
626
  }
627
+ function isSqliteBusyError(error) {
628
+ const candidate = error;
629
+ const code = typeof candidate.code === "string" ? candidate.code : "";
630
+ const message = typeof candidate.message === "string" ? candidate.message : String(error);
631
+ return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
632
+ }
633
+ function retryDelayMs(attempt) {
634
+ return Math.min(1000, 50 * 2 ** attempt);
635
+ }
636
+ function withSqliteBusyRetry(operation, context) {
637
+ const maxAttempts = 8;
638
+ let lastError;
639
+ for (let attempt = 0;attempt < maxAttempts; attempt++) {
640
+ try {
641
+ return operation();
642
+ } catch (error) {
643
+ if (!isSqliteBusyError(error))
644
+ throw error;
645
+ lastError = error;
646
+ if (attempt === maxAttempts - 1)
647
+ break;
648
+ Bun.sleepSync(retryDelayMs(attempt));
649
+ }
650
+ }
651
+ throw new Error(`SQLite database is locked after ${maxAttempts} attempts while ${context}. Another economy sync/merge may be recovering the database; retry shortly.`, { cause: lastError });
652
+ }
626
653
  function openDatabase(dbPath, skipSeed = false) {
627
654
  const path = dbPath ?? getDbPath();
628
655
  if (path !== ":memory:") {
@@ -630,11 +657,21 @@ function openDatabase(dbPath, skipSeed = false) {
630
657
  if (dir && !existsSync(dir))
631
658
  mkdirSync(dir, { recursive: true });
632
659
  }
633
- const db = new Database(path);
634
- db.exec("PRAGMA journal_mode = WAL");
635
- db.exec("PRAGMA busy_timeout = 5000");
636
- db.exec("PRAGMA foreign_keys = ON");
637
- initSchema(db);
660
+ const db = withSqliteBusyRetry(() => {
661
+ const opened = new Database(path);
662
+ try {
663
+ opened.exec("PRAGMA busy_timeout = 10000");
664
+ opened.exec("PRAGMA journal_mode = WAL");
665
+ opened.exec("PRAGMA foreign_keys = ON");
666
+ initSchema(opened);
667
+ return opened;
668
+ } catch (error) {
669
+ try {
670
+ opened.close();
671
+ } catch {}
672
+ throw error;
673
+ }
674
+ }, `opening ${path}`);
638
675
  if (!skipSeed) {
639
676
  Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
640
677
  }
@@ -996,36 +1033,50 @@ function querySessions(db, filter = {}) {
996
1033
  SELECT * FROM sessions ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?
997
1034
  `).all(...params, limit, offset);
998
1035
  }
999
- function queryTopSessions(db, n = 10, agent) {
1036
+ function queryTopSessions(db, n = 10, agent, since) {
1037
+ const conditions = [];
1038
+ const params = [];
1000
1039
  if (agent) {
1001
- return db.prepare(`SELECT * FROM sessions WHERE agent = ? ORDER BY total_cost_usd DESC LIMIT ?`).all(agent, n);
1040
+ conditions.push("agent = ?");
1041
+ params.push(agent);
1042
+ }
1043
+ if (since) {
1044
+ conditions.push("started_at >= ?");
1045
+ params.push(since);
1002
1046
  }
1003
- return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
1047
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
1048
+ return db.prepare(`SELECT * FROM sessions ${where} ORDER BY total_cost_usd DESC LIMIT ?`).all(...params, n);
1004
1049
  }
1005
- function querySummary(db, period, machine, allMachines = false) {
1050
+ function querySummary(db, period, machine, allMachines = false, agent) {
1006
1051
  const rWhere = periodWhere(period);
1007
1052
  const sWhere = sessionPeriodWhere(period);
1008
- const machineClause = !allMachines && machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
1053
+ const machineClause = !allMachines && machine ? " AND machine_id = ?" : "";
1054
+ const agentClause = agent ? " AND agent = ?" : "";
1055
+ const params = [];
1056
+ if (!allMachines && machine)
1057
+ params.push(machine);
1058
+ if (agent)
1059
+ params.push(agent);
1009
1060
  const r = db.prepare(`
1010
1061
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
1011
1062
  COUNT(*) as requests,
1012
1063
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
1013
- FROM requests WHERE ${rWhere}${machineClause}
1014
- `).get();
1064
+ FROM requests WHERE ${rWhere}${machineClause}${agentClause}
1065
+ `).get(...params);
1015
1066
  const codexTotals = db.prepare(`
1016
1067
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1017
1068
  COALESCE(SUM(total_tokens), 0) as tokens,
1018
1069
  COALESCE(SUM(request_count), 0) as requests,
1019
1070
  COUNT(*) as sessions
1020
1071
  FROM sessions
1021
- WHERE ${sWhere}${machineClause}
1072
+ WHERE ${sWhere}${machineClause}${agentClause}
1022
1073
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1023
- `).get();
1074
+ `).get(...params);
1024
1075
  const requestSessionCount = db.prepare(`
1025
1076
  SELECT COUNT(DISTINCT session_id) as sessions
1026
1077
  FROM requests
1027
- WHERE ${rWhere}${machineClause}
1028
- `).get();
1078
+ WHERE ${rWhere}${machineClause}${agentClause}
1079
+ `).get(...params);
1029
1080
  return {
1030
1081
  total_usd: r.total_usd + codexTotals.cost_usd,
1031
1082
  requests: r.requests + codexTotals.requests,
@@ -1034,6 +1085,21 @@ function querySummary(db, period, machine, allMachines = false) {
1034
1085
  period
1035
1086
  };
1036
1087
  }
1088
+ function queryZeroCostTokenizedModels(db, limit = 10) {
1089
+ return db.prepare(`
1090
+ SELECT agent,
1091
+ model,
1092
+ COUNT(*) as requests,
1093
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1094
+ MAX(timestamp) as last_seen
1095
+ FROM requests
1096
+ WHERE cost_usd = 0
1097
+ AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)
1098
+ GROUP BY agent, model
1099
+ ORDER BY requests DESC, total_tokens DESC
1100
+ LIMIT ?
1101
+ `).all(limit);
1102
+ }
1037
1103
  function queryModelBreakdown(db) {
1038
1104
  return db.prepare(`
1039
1105
  SELECT model, agent,
@@ -4270,15 +4336,15 @@ __export(exports_config, {
4270
4336
  loadConfig: () => loadConfig2,
4271
4337
  getConfigValue: () => getConfigValue
4272
4338
  });
4273
- import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
4274
- import { dirname as dirname2, join as join12 } from "path";
4339
+ import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
4340
+ import { dirname as dirname2, join as join13 } from "path";
4275
4341
  function getConfigPath() {
4276
- return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join12(getDataDir(), "config.json");
4342
+ return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join13(getDataDir(), "config.json");
4277
4343
  }
4278
4344
  function loadConfig2() {
4279
4345
  try {
4280
4346
  const configPath = getConfigPath();
4281
- if (existsSync13(configPath)) {
4347
+ if (existsSync14(configPath)) {
4282
4348
  const raw = readFileSync11(configPath, "utf-8");
4283
4349
  return { ...DEFAULTS, ...JSON.parse(raw) };
4284
4350
  }
@@ -4288,9 +4354,9 @@ function loadConfig2() {
4288
4354
  function saveConfig2(config) {
4289
4355
  const configPath = getConfigPath();
4290
4356
  const dir = dirname2(configPath);
4291
- if (!existsSync13(dir))
4292
- mkdirSync3(dir, { recursive: true });
4293
- writeFileSync2(configPath, JSON.stringify(config, null, 2) + `
4357
+ if (!existsSync14(dir))
4358
+ mkdirSync4(dir, { recursive: true });
4359
+ writeFileSync3(configPath, JSON.stringify(config, null, 2) + `
4294
4360
  `);
4295
4361
  }
4296
4362
  function getConfigValue(key) {
@@ -4432,7 +4498,7 @@ var init_webhooks = __esm(() => {
4432
4498
  });
4433
4499
 
4434
4500
  // src/lib/watch-paths.ts
4435
- import { existsSync as existsSync14 } from "fs";
4501
+ import { existsSync as existsSync15 } from "fs";
4436
4502
  function getWatchPaths() {
4437
4503
  const p = agentPaths();
4438
4504
  const candidates = [
@@ -4445,7 +4511,7 @@ function getWatchPaths() {
4445
4511
  p.piSessions,
4446
4512
  p.hermesDir
4447
4513
  ];
4448
- return candidates.filter((path) => existsSync14(path));
4514
+ return candidates.filter((path) => existsSync15(path));
4449
4515
  }
4450
4516
  var init_watch_paths = __esm(() => {
4451
4517
  init_paths();
@@ -4615,8 +4681,8 @@ __export(exports_serve, {
4615
4681
  createHandler: () => createHandler
4616
4682
  });
4617
4683
  import { randomUUID as randomUUID2 } from "crypto";
4618
- import { existsSync as existsSync15 } from "fs";
4619
- import { resolve, sep } from "path";
4684
+ import { existsSync as existsSync16 } from "fs";
4685
+ import { resolve as resolve2, sep } from "path";
4620
4686
  function json(data, status = 200) {
4621
4687
  return new Response(JSON.stringify(data), {
4622
4688
  status,
@@ -4670,8 +4736,8 @@ function dashboardPath(root, pathname) {
4670
4736
  return null;
4671
4737
  }
4672
4738
  const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
4673
- const rootPath = resolve(root);
4674
- const filePath = resolve(rootPath, relativePath);
4739
+ const rootPath = resolve2(root);
4740
+ const filePath = resolve2(rootPath, relativePath);
4675
4741
  return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
4676
4742
  }
4677
4743
  function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
@@ -4680,13 +4746,13 @@ function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
4680
4746
  if (url.pathname.startsWith("/api") || url.pathname === "/health") {
4681
4747
  return apiHandler(req);
4682
4748
  }
4683
- if (existsSync15(dashboardDir)) {
4749
+ if (existsSync16(dashboardDir)) {
4684
4750
  const filePath = dashboardPath(dashboardDir, url.pathname);
4685
- if (filePath && existsSync15(filePath)) {
4751
+ if (filePath && existsSync16(filePath)) {
4686
4752
  return new Response(Bun.file(filePath));
4687
4753
  }
4688
4754
  const indexPath = dashboardPath(dashboardDir, "/");
4689
- if (indexPath && existsSync15(indexPath)) {
4755
+ if (indexPath && existsSync16(indexPath)) {
4690
4756
  return new Response(Bun.file(indexPath));
4691
4757
  }
4692
4758
  }
@@ -4771,7 +4837,8 @@ function createHandler(db) {
4771
4837
  if (path === "/api/top" && method === "GET") {
4772
4838
  const n = Number(url.searchParams.get("n") ?? 10);
4773
4839
  const agent = url.searchParams.get("agent") ?? undefined;
4774
- return ok(queryTopSessions(db, n, agent));
4840
+ const since = url.searchParams.get("since") ?? undefined;
4841
+ return ok(queryTopSessions(db, n, agent, since));
4775
4842
  }
4776
4843
  if (path === "/api/models" && method === "GET") {
4777
4844
  return ok(queryModelBreakdown(db));
@@ -5101,14 +5168,14 @@ __export(exports_menubar, {
5101
5168
  });
5102
5169
  import chalk6 from "chalk";
5103
5170
  import { execFileSync as execFileSync3 } from "child_process";
5104
- import { cpSync, existsSync as existsSync16, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
5171
+ import { cpSync, existsSync as existsSync17, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
5105
5172
  import { tmpdir, arch } from "os";
5106
- import { join as join13 } from "path";
5173
+ import { join as join14 } from "path";
5107
5174
  function getArch() {
5108
5175
  return arch() === "arm64" ? "arm64" : "x86_64";
5109
5176
  }
5110
5177
  function isInstalled() {
5111
- return existsSync16(APP_PATH);
5178
+ return existsSync17(APP_PATH);
5112
5179
  }
5113
5180
  function isRunning() {
5114
5181
  try {
@@ -5145,15 +5212,15 @@ async function menubarInstall(opts) {
5145
5212
  console.error(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
5146
5213
  process.exit(1);
5147
5214
  }
5148
- const zipPath = join13(tmpdir(), `economy-bar-${cpuArch}.zip`);
5149
- const extractDir = join13(tmpdir(), "economy-bar-extracted");
5215
+ const zipPath = join14(tmpdir(), `economy-bar-${cpuArch}.zip`);
5216
+ const extractDir = join14(tmpdir(), "economy-bar-extracted");
5150
5217
  console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
5151
5218
  try {
5152
5219
  const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
5153
5220
  if (!res.ok)
5154
5221
  throw new Error(`Download failed: ${res.status}`);
5155
5222
  const buffer = await res.arrayBuffer();
5156
- writeFileSync3(zipPath, Buffer.from(buffer));
5223
+ writeFileSync4(zipPath, Buffer.from(buffer));
5157
5224
  console.log(chalk6.green(`\u2713 Downloaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`));
5158
5225
  } catch (e) {
5159
5226
  console.error(chalk6.red(`\u2717 Download failed: ${e instanceof Error ? e.message : String(e)}`));
@@ -5161,17 +5228,17 @@ async function menubarInstall(opts) {
5161
5228
  }
5162
5229
  console.log(chalk6.cyan("\u2192 Installing to /Applications..."));
5163
5230
  try {
5164
- rmSync(extractDir, { recursive: true, force: true });
5165
- mkdirSync4(extractDir, { recursive: true });
5231
+ rmSync2(extractDir, { recursive: true, force: true });
5232
+ mkdirSync5(extractDir, { recursive: true });
5166
5233
  execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
5167
5234
  if (isInstalled())
5168
- rmSync(APP_PATH, { recursive: true, force: true });
5169
- cpSync(join13(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
5235
+ rmSync2(APP_PATH, { recursive: true, force: true });
5236
+ cpSync(join14(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
5170
5237
  try {
5171
5238
  execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
5172
5239
  } catch {}
5173
- rmSync(zipPath, { force: true });
5174
- rmSync(extractDir, { recursive: true, force: true });
5240
+ rmSync2(zipPath, { force: true });
5241
+ rmSync2(extractDir, { recursive: true, force: true });
5175
5242
  console.log(chalk6.green(`\u2713 Installed to ${APP_PATH}`));
5176
5243
  } catch (e) {
5177
5244
  console.error(chalk6.red(`\u2717 Install failed: ${e instanceof Error ? e.message : String(e)}`));
@@ -5198,7 +5265,7 @@ function menubarUninstall() {
5198
5265
  execFileSync3("sleep", ["1"], { stdio: "ignore" });
5199
5266
  } catch {}
5200
5267
  }
5201
- rmSync(APP_PATH, { recursive: true, force: true });
5268
+ rmSync2(APP_PATH, { recursive: true, force: true });
5202
5269
  console.log(chalk6.green("\u2713 Economy Bar uninstalled"));
5203
5270
  }
5204
5271
  function menubarStart() {
@@ -6564,6 +6631,15 @@ init_paths();
6564
6631
  function fmt2(usd) {
6565
6632
  return "$" + usd.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
6566
6633
  }
6634
+ function fmtTokens(n) {
6635
+ if (n >= 1e9)
6636
+ return `${(n / 1e9).toFixed(1)}B`;
6637
+ if (n >= 1e6)
6638
+ return `${(n / 1e6).toFixed(1)}M`;
6639
+ if (n >= 1000)
6640
+ return `${(n / 1000).toFixed(1)}k`;
6641
+ return n.toLocaleString("en-US");
6642
+ }
6567
6643
  function parsePeriod(value, fallback = "month") {
6568
6644
  const p = value ?? fallback;
6569
6645
  if (!["today", "yesterday", "week", "month", "year", "all"].includes(p)) {
@@ -6578,7 +6654,7 @@ function registerExtendedCommands(program) {
6578
6654
  const period = parsePeriod(periodArg, "month");
6579
6655
  const agent = parseAgent(opts.agent, "--agent");
6580
6656
  const snaps = queryUsageSnapshots(db, { agent, ...usageSnapshotFilterForPeriod(period) });
6581
- const summary = querySummary(db, period, undefined, true);
6657
+ const summary = querySummary(db, period, undefined, true, agent);
6582
6658
  if (opts.json) {
6583
6659
  console.log(JSON.stringify({ period, agent: agent ?? "all", snapshots: snaps, summary }, null, 2));
6584
6660
  return;
@@ -6593,8 +6669,9 @@ function registerExtendedCommands(program) {
6593
6669
  console.log(` ${chalk4.white(s.agent.padEnd(10))} ${s.metric.padEnd(28)} ${s.value}${s.unit ? ` ${s.unit}` : ""} ${chalk4.dim(s.date)}`);
6594
6670
  }
6595
6671
  }
6672
+ const spendLabel = agent ? `${agent} API-equivalent spend:` : "Fleet API-equivalent spend:";
6596
6673
  console.log(`
6597
- ${chalk4.dim("Fleet API-equivalent spend:")} ${fmt2(summary.total_usd)}`);
6674
+ ${chalk4.dim(spendLabel)} ${fmt2(summary.total_usd)}`);
6598
6675
  console.log();
6599
6676
  });
6600
6677
  program.command("savings [period]").description("Subscription vs API-equivalent savings breakdown").option("--agent <agent>", "Filter by agent").option("--json", "Output JSON").action((periodArg, opts) => {
@@ -6676,8 +6753,19 @@ function registerExtendedCommands(program) {
6676
6753
  }
6677
6754
  checks.push({ ok: Boolean(process.env["CURSOR_SESSION_TOKEN"]), msg: `cursor token: ${process.env["CURSOR_SESSION_TOKEN"] ? "set" : "missing CURSOR_SESSION_TOKEN"}` });
6678
6755
  checks.push({ ok: Boolean(getCloudDatabaseUrl()), msg: `cloud: ${getCloudDatabaseUrl() ? "ECONOMY_CLOUD_DATABASE_URL set" : "not configured"}` });
6679
- const zeroCost = db.prepare(`SELECT COUNT(*) as c FROM requests WHERE cost_usd = 0 AND (input_tokens > 0 OR output_tokens > 0)`).get();
6680
- checks.push({ ok: zeroCost.c === 0, msg: `zero-cost requests with tokens: ${zeroCost.c}` });
6756
+ const zeroCostBuckets = queryZeroCostTokenizedModels(db, 5);
6757
+ const zeroCost = db.prepare(`
6758
+ SELECT COUNT(*) as c
6759
+ FROM requests
6760
+ WHERE cost_usd = 0
6761
+ AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)
6762
+ `).get();
6763
+ const zeroCostSuffix = zeroCostBuckets.length > 0 ? `; top buckets: ${zeroCostBuckets.map((row) => {
6764
+ const pricing = getPricingFromDb(db, row.model);
6765
+ const status = pricing ? "pricing configured" : "missing pricing";
6766
+ return `${row.agent}/${row.model} ${row.requests} req ${fmtTokens(row.total_tokens)} tok (${status})`;
6767
+ }).join("; ")}` : "";
6768
+ checks.push({ ok: zeroCost.c === 0, msg: `zero-cost requests with tokens: ${zeroCost.c}${zeroCostSuffix}` });
6681
6769
  const estimated = querySummary(db, "month", undefined, true);
6682
6770
  const actual = queryBillingSummary(db, "month");
6683
6771
  const drift = actual.total_usd > 0 ? Math.abs(estimated.total_usd - actual.total_usd) / actual.total_usd : 0;
@@ -7824,6 +7912,187 @@ function mergePeerDatabase(target, sourcePath, opts = {}) {
7824
7912
  };
7825
7913
  }
7826
7914
 
7915
+ // src/lib/fleet-sync.ts
7916
+ init_database();
7917
+ import { spawnSync } from "child_process";
7918
+ import { existsSync as existsSync13, mkdirSync as mkdirSync3, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
7919
+ import { join as join12, resolve } from "path";
7920
+ import { homedir as homedir9 } from "os";
7921
+ import { Database as Database2 } from "bun:sqlite";
7922
+ var DEFAULT_TIMEOUT_MS = 120000;
7923
+ var DEFAULT_MACHINES = ["spark02", "apple03"];
7924
+ function runEconomyFleetSync(options = {}) {
7925
+ const generatedAt = (options.now ?? new Date).toISOString();
7926
+ const timeoutMs = normalizePositiveInteger(options.timeoutMs, DEFAULT_TIMEOUT_MS);
7927
+ const runner = options.runner ?? spawnCommand;
7928
+ const dryRun = Boolean(options.dryRun);
7929
+ const machines = ((options.machines?.length) ? options.machines : DEFAULT_MACHINES).filter(Boolean);
7930
+ const cacheDir = resolve(options.cacheDir ?? join12(homedir9(), ".hasna", "economy", "fleet-sync-cache"));
7931
+ const remoteSnapshotDir = options.remoteSnapshotDir ?? "$HOME/.hasna/economy/fleet-sync-snapshots";
7932
+ const localSync = options.localSync !== false;
7933
+ const remoteSync = options.remoteSync !== false;
7934
+ const local = {
7935
+ status: dryRun ? "dry_run" : localSync ? "skipped" : "skipped",
7936
+ db_path: getDbPath()
7937
+ };
7938
+ if (localSync) {
7939
+ if (dryRun) {
7940
+ local.status = "dry_run";
7941
+ } else {
7942
+ local.sync = runner("economy", ["sync"], { timeoutMs });
7943
+ local.status = local.sync.code === 0 ? "synced" : "failed";
7944
+ }
7945
+ }
7946
+ const remote = machines.map((machine) => syncMachine(machine, {
7947
+ generatedAt,
7948
+ timeoutMs,
7949
+ runner,
7950
+ dryRun,
7951
+ remoteSync,
7952
+ cacheDir,
7953
+ remoteSnapshotDir
7954
+ }));
7955
+ return {
7956
+ schema: "open-economy.fleet-sync.v1",
7957
+ generated_at: generatedAt,
7958
+ dry_run: dryRun,
7959
+ local,
7960
+ remote,
7961
+ summary: {
7962
+ machines: remote.length,
7963
+ merged: remote.filter((row) => row.status === "merged").length,
7964
+ failed: remote.filter((row) => row.status === "failed").length,
7965
+ dry_run: remote.filter((row) => row.status === "dry_run").length + (dryRun ? 1 : 0)
7966
+ }
7967
+ };
7968
+ }
7969
+ function syncMachine(machine, context) {
7970
+ const safeMachine = machine.replace(/[^A-Za-z0-9_.-]/g, "_");
7971
+ const stamp = context.generatedAt.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
7972
+ const snapshotName = `${safeMachine}-${stamp}.economy.db`;
7973
+ const remoteSnapshotPath = `${context.remoteSnapshotDir.replace(/\/$/, "")}/${snapshotName}`;
7974
+ const tmpLocal = join12(context.cacheDir, `${snapshotName}.tmp`);
7975
+ const verifiedLocal = join12(context.cacheDir, snapshotName);
7976
+ if (context.dryRun) {
7977
+ return { machine, status: "dry_run", remote_snapshot_path: remoteSnapshotPath };
7978
+ }
7979
+ mkdirSync3(context.cacheDir, { recursive: true });
7980
+ rmSync(tmpLocal, { force: true });
7981
+ const result = {
7982
+ machine,
7983
+ status: "failed",
7984
+ remote_snapshot_path: remoteSnapshotPath
7985
+ };
7986
+ if (context.remoteSync) {
7987
+ result.remote_sync = context.runner("ssh", [machine, "economy sync"], { timeoutMs: context.timeoutMs });
7988
+ if (result.remote_sync.code !== 0) {
7989
+ result.error = "remote economy sync failed";
7990
+ return result;
7991
+ }
7992
+ }
7993
+ result.remote_snapshot = context.runner("ssh", [machine, remoteSnapshotCommand(context.remoteSnapshotDir, snapshotName)], { timeoutMs: context.timeoutMs });
7994
+ if (result.remote_snapshot.code !== 0) {
7995
+ result.error = "remote snapshot failed";
7996
+ return result;
7997
+ }
7998
+ const emittedPath = result.remote_snapshot.stdout.trim().split(/\r?\n/).at(-1)?.trim() || remoteSnapshotPath;
7999
+ if (!isSafeRemoteSnapshotPath(emittedPath, context.remoteSnapshotDir)) {
8000
+ result.error = `remote snapshot path refused: ${emittedPath}`;
8001
+ return result;
8002
+ }
8003
+ result.remote_snapshot_path = emittedPath;
8004
+ result.pull = context.runner("scp", [`${machine}:${emittedPath}`, tmpLocal], { timeoutMs: context.timeoutMs });
8005
+ context.runner("ssh", [machine, `rm -f ${shellQuote(emittedPath)}`], { timeoutMs: context.timeoutMs });
8006
+ if (result.pull.code !== 0) {
8007
+ result.error = "snapshot pull failed; no cached DB merged";
8008
+ rmSync(tmpLocal, { force: true });
8009
+ return result;
8010
+ }
8011
+ const integrity = verifySqliteIntegrity(tmpLocal);
8012
+ result.local_integrity = integrity;
8013
+ if (!integrity.ok) {
8014
+ result.error = "pulled snapshot failed local integrity check";
8015
+ rmSync(tmpLocal, { force: true });
8016
+ return result;
8017
+ }
8018
+ renameSync(tmpLocal, verifiedLocal);
8019
+ const db = openDatabase();
8020
+ result.merge = mergePeerDatabase(db, verifiedLocal, { sourceMachine: machine });
8021
+ result.status = "merged";
8022
+ return result;
8023
+ }
8024
+ function remoteSnapshotCommand(remoteDir, filename) {
8025
+ const dirExpr = remoteDir.startsWith("$HOME/") ? `"${remoteDir.replace(/"/g, "\\\"")}"` : shellQuote(remoteDir);
8026
+ const safeFilename = filename.replace(/[^A-Za-z0-9_.-]/g, "_");
8027
+ return [
8028
+ "set -euo pipefail",
8029
+ 'db="${HASNA_ECONOMY_DB_PATH:-${ECONOMY_DB:-$HOME/.hasna/economy/economy.db}}"',
8030
+ `snapshot_dir=${dirExpr}`,
8031
+ `out="$snapshot_dir/${safeFilename}"`,
8032
+ 'test -f "$db"',
8033
+ 'mkdir -p "$snapshot_dir"',
8034
+ 'rm -f "$out"',
8035
+ `sqlite3 "$db" "VACUUM INTO '$out'"`,
8036
+ 'test "$(sqlite3 "$out" "PRAGMA integrity_check;")" = "ok"',
8037
+ 'printf "%s\\n" "$out"'
8038
+ ].join("; ");
8039
+ }
8040
+ function isSafeRemoteSnapshotPath(path, remoteSnapshotDir = "$HOME/.hasna/economy/fleet-sync-snapshots") {
8041
+ if (!path || path.includes("\x00") || path.includes(`
8042
+ `) || path.includes("\r"))
8043
+ return false;
8044
+ if (!path.endsWith(".db"))
8045
+ return false;
8046
+ if (path.includes(".."))
8047
+ return false;
8048
+ const prefix = remoteSnapshotDir.replace(/\/$/, "");
8049
+ return path.startsWith(`${prefix}/`) || path.startsWith("$HOME/.hasna/economy/fleet-sync-snapshots/") || path.includes("/.hasna/economy/fleet-sync-snapshots/");
8050
+ }
8051
+ function verifySqliteIntegrity(path) {
8052
+ if (!existsSync13(path))
8053
+ return { ok: false, detail: "file does not exist" };
8054
+ let db;
8055
+ try {
8056
+ db = new Database2(path, { readonly: true });
8057
+ const row = db.query("PRAGMA integrity_check").get();
8058
+ const detail = Object.values(row ?? {})[0] ?? "";
8059
+ return { ok: detail.toLowerCase() === "ok", detail: detail || "no integrity result" };
8060
+ } catch (error) {
8061
+ return { ok: false, detail: error instanceof Error ? error.message : String(error) };
8062
+ } finally {
8063
+ db?.close();
8064
+ }
8065
+ }
8066
+ function writeFleetSyncReport(reportDir, result) {
8067
+ mkdirSync3(reportDir, { recursive: true });
8068
+ const path = join12(resolve(reportDir), `${result.generated_at.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z")}.json`);
8069
+ writeFileSync2(path, `${JSON.stringify(result, null, 2)}
8070
+ `);
8071
+ return path;
8072
+ }
8073
+ function spawnCommand(command, args, options) {
8074
+ const result = spawnSync(command, args, {
8075
+ encoding: "utf8",
8076
+ timeout: options.timeoutMs,
8077
+ maxBuffer: 1024 * 1024,
8078
+ stdio: ["ignore", "pipe", "pipe"]
8079
+ });
8080
+ return {
8081
+ code: result.status,
8082
+ stdout: result.stdout ?? "",
8083
+ stderr: result.stderr ?? "",
8084
+ ...result.error ? { error: result.error.message } : {}
8085
+ };
8086
+ }
8087
+ function shellQuote(value) {
8088
+ return `'${value.replace(/'/g, "'\\''")}'`;
8089
+ }
8090
+ function normalizePositiveInteger(value, fallback) {
8091
+ if (value == null || !Number.isFinite(value) || value < 1)
8092
+ return fallback;
8093
+ return Math.floor(value);
8094
+ }
8095
+
7827
8096
  // src/cli/index.ts
7828
8097
  init_database();
7829
8098
  init_database();
@@ -7863,7 +8132,7 @@ function fmt4(usd) {
7863
8132
  }
7864
8133
  return chalk7.green(formatted);
7865
8134
  }
7866
- function fmtTokens(n) {
8135
+ function fmtTokens2(n) {
7867
8136
  if (n >= 1e9)
7868
8137
  return `${(n / 1e9).toFixed(1)}B`;
7869
8138
  if (n >= 1e6)
@@ -7971,7 +8240,7 @@ function printAccountBreakdown(rows) {
7971
8240
  chalk7.dim(r.account_source || "unknown"),
7972
8241
  String(r.sessions),
7973
8242
  String(r.requests),
7974
- chalk7.cyan(fmtTokens(r.total_tokens)),
8243
+ chalk7.cyan(fmtTokens2(r.total_tokens)),
7975
8244
  fmt4(r.api_equivalent_usd),
7976
8245
  fmt4(r.billable_usd),
7977
8246
  fmt4(r.subscription_included_usd)
@@ -7998,7 +8267,7 @@ function printSummary(label, period) {
7998
8267
  ["Total cost", fmt4(s.total_usd)],
7999
8268
  ["Sessions", chalk7.yellow(fmtCount(s.sessions))],
8000
8269
  ["Requests", chalk7.yellow(fmtCount(s.requests))],
8001
- ["Tokens", chalk7.yellow(fmtTokens(s.tokens))]
8270
+ ["Tokens", chalk7.yellow(fmtTokens2(s.tokens))]
8002
8271
  ]);
8003
8272
  console.log();
8004
8273
  }
@@ -8018,9 +8287,9 @@ program.action(async () => {
8018
8287
  console.log(chalk7.bold.cyan(" Economy"));
8019
8288
  console.log();
8020
8289
  printTable(["Period", "Cost", "Sessions", "Requests", "Tokens"], [
8021
- ["Today", fmt4(t.total_usd), fmtCount(t.sessions), fmtCount(t.requests), fmtTokens(t.tokens)],
8022
- ["This Week", fmt4(w.total_usd), fmtCount(w.sessions), fmtCount(w.requests), fmtTokens(w.tokens)],
8023
- ["This Month", fmt4(m.total_usd), fmtCount(m.sessions), fmtCount(m.requests), fmtTokens(m.tokens)]
8290
+ ["Today", fmt4(t.total_usd), fmtCount(t.sessions), fmtCount(t.requests), fmtTokens2(t.tokens)],
8291
+ ["This Week", fmt4(w.total_usd), fmtCount(w.sessions), fmtCount(w.requests), fmtTokens2(w.tokens)],
8292
+ ["This Month", fmt4(m.total_usd), fmtCount(m.sessions), fmtCount(m.requests), fmtTokens2(m.tokens)]
8024
8293
  ]);
8025
8294
  if (dailyValues.length > 0) {
8026
8295
  console.log(`
@@ -8076,7 +8345,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
8076
8345
  console.log(chalk7.cyan(`\u2192 Backfilled machine_id='${machine}': ${reqCount.changes} requests, ${sessCount.changes} sessions`));
8077
8346
  }
8078
8347
  if (opts.recalculate) {
8079
- const { computeCostFromDb: computeCostFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
8348
+ const { computeCostFromDb: computeCostFromDb2, getPricingFromDb: getPricingFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
8080
8349
  const zeroRows = db.prepare(`SELECT id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, cache_create_5m_tokens, cache_create_1h_tokens FROM requests WHERE cost_usd = 0 AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)`).all();
8081
8350
  let fixed = 0;
8082
8351
  for (const r of zeroRows) {
@@ -8098,6 +8367,16 @@ program.command("sync").description("Ingest cost data from all coding agents").o
8098
8367
  }
8099
8368
  }
8100
8369
  console.log(chalk7.cyan(`\u2192 Recalculated: ${fixed}/${zeroRows.length} zero-cost requests now have pricing`));
8370
+ const zeroCostBuckets = queryZeroCostTokenizedModels(db, 8);
8371
+ if (zeroCostBuckets.length > 0) {
8372
+ console.log(chalk7.yellow("\u2192 Zero-cost token buckets remain:"));
8373
+ for (const row of zeroCostBuckets) {
8374
+ const pricing = getPricingFromDb2(db, row.model);
8375
+ const status = pricing ? pricing.inputPer1M === 0 && pricing.outputPer1M === 0 && pricing.cacheReadPer1M === 0 && pricing.cacheWritePer1M === 0 ? "explicit zero/free pricing" : "pricing configured; inspect/recalculate" : "missing pricing";
8376
+ console.log(chalk7.yellow(` ${row.agent}/${row.model}: ${row.requests} requests, ${fmtTokens2(row.total_tokens)} tokens (${status})`));
8377
+ }
8378
+ console.log(chalk7.dim(" Add pricing with `economy pricing set <model> --input ... --output ...`, then rerun `economy sync --recalculate`."));
8379
+ }
8101
8380
  }
8102
8381
  try {
8103
8382
  const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
@@ -8140,7 +8419,7 @@ program.command("sessions").description("List coding sessions with costs").optio
8140
8419
  const f = opts.format ?? "table";
8141
8420
  if (f === "compact") {
8142
8421
  for (const s of sessions)
8143
- process.stdout.write(`${s.id.slice(0, 8)} ${s.agent} ${fmt4(s.total_cost_usd)} ${fmtTokens(s.total_tokens)} ${s.project_name || "\u2014"}
8422
+ process.stdout.write(`${s.id.slice(0, 8)} ${s.agent} ${fmt4(s.total_cost_usd)} ${fmtTokens2(s.total_tokens)} ${s.project_name || "\u2014"}
8144
8423
  `);
8145
8424
  return;
8146
8425
  }
@@ -8160,7 +8439,7 @@ program.command("sessions").description("List coding sessions with costs").optio
8160
8439
  fmtAgent(s.agent),
8161
8440
  chalk7.white(s.project_name || chalk7.dim("unknown")),
8162
8441
  fmt4(s.total_cost_usd),
8163
- chalk7.cyan(fmtTokens(s.total_tokens)),
8442
+ chalk7.cyan(fmtTokens2(s.total_tokens)),
8164
8443
  fmtCount(s.request_count),
8165
8444
  chalk7.dim(s.started_at.substring(0, 16))
8166
8445
  ]));
@@ -8171,9 +8450,7 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
8171
8450
  const agent = parseOptionalCliAgent(opts.agent);
8172
8451
  const db = openDatabase();
8173
8452
  const sinceDate = opts.since ? parseSinceDate2(opts.since) : undefined;
8174
- let sessions = queryTopSessions(db, count, agent);
8175
- if (sinceDate)
8176
- sessions = sessions.filter((s) => s.started_at >= sinceDate);
8453
+ const sessions = queryTopSessions(db, count, agent, sinceDate);
8177
8454
  if (sessions.length === 0) {
8178
8455
  console.log(chalk7.yellow("No sessions found. Run `economy sync` first."));
8179
8456
  return;
@@ -8184,7 +8461,7 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
8184
8461
  chalk7.white(s.project_name || chalk7.dim("unknown")),
8185
8462
  fmtAgent(s.agent),
8186
8463
  fmt4(s.total_cost_usd),
8187
- chalk7.cyan(fmtTokens(s.total_tokens)),
8464
+ chalk7.cyan(fmtTokens2(s.total_tokens)),
8188
8465
  chalk7.dim(s.started_at.substring(0, 16))
8189
8466
  ]));
8190
8467
  console.log();
@@ -8208,7 +8485,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
8208
8485
  chalk7.white(r.project_name || chalk7.dim("unknown")),
8209
8486
  String(r.sessions),
8210
8487
  String(r.requests),
8211
- chalk7.cyan(fmtTokens(r.total_tokens)),
8488
+ chalk7.cyan(fmtTokens2(r.total_tokens)),
8212
8489
  fmt4(r.cost_usd)
8213
8490
  ]));
8214
8491
  } else if (opts.by === "agent") {
@@ -8230,7 +8507,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
8230
8507
  fmtAgent(r.agent),
8231
8508
  String(r.sessions),
8232
8509
  String(r.requests),
8233
- chalk7.cyan(fmtTokens(r.total_tokens)),
8510
+ chalk7.cyan(fmtTokens2(r.total_tokens)),
8234
8511
  fmt4(r.api_equivalent_usd),
8235
8512
  fmt4(r.billable_usd),
8236
8513
  fmt4(r.subscription_included_usd)
@@ -8352,7 +8629,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
8352
8629
  chalk7.white(r.model),
8353
8630
  fmtAgent(r.agent),
8354
8631
  String(r.requests),
8355
- chalk7.cyan(fmtTokens(r.total_tokens)),
8632
+ chalk7.cyan(fmtTokens2(r.total_tokens)),
8356
8633
  fmt4(r.cost_usd)
8357
8634
  ]));
8358
8635
  }
@@ -8514,7 +8791,7 @@ projectCmd.command("show <nameOrPath>").description("Detailed project breakdown
8514
8791
  printTable(["Metric", "Value"], [
8515
8792
  ["Total cost", fmt4(totalCost)],
8516
8793
  ["Sessions", fmtCount(sessions.length)],
8517
- ["Total tokens", fmtTokens(totalTokens)]
8794
+ ["Total tokens", fmtTokens2(totalTokens)]
8518
8795
  ]);
8519
8796
  if (dailyValues.length > 0) {
8520
8797
  console.log(`
@@ -8647,8 +8924,8 @@ program.command("dashboard").description("Open the web dashboard (auto-starts se
8647
8924
  if (!serverRunning) {
8648
8925
  console.log(chalk7.cyan(`\u2192 Starting economy server on port ${port}...`));
8649
8926
  const { spawn } = await import("child_process");
8650
- const { resolve: resolve2, dirname: dirname3 } = await import("path");
8651
- const serveScript = resolve2(dirname3(process.argv[1]), "..", "server", "index.js");
8927
+ const { resolve: resolve3, dirname: dirname3 } = await import("path");
8928
+ const serveScript = resolve3(dirname3(process.argv[1]), "..", "server", "index.js");
8652
8929
  const child = spawn(process.execPath, [serveScript], {
8653
8930
  detached: true,
8654
8931
  stdio: "ignore",
@@ -8719,7 +8996,7 @@ program.command("session <id>").description("Show detailed breakdown of a single
8719
8996
  ["Started", String(session["started_at"]).substring(0, 19)],
8720
8997
  ["Ended", session["ended_at"] ? String(session["ended_at"]).substring(0, 19) : "\u2014"],
8721
8998
  ["Total cost", fmt4(session["total_cost_usd"])],
8722
- ["Total tokens", fmtTokens(session["total_tokens"])],
8999
+ ["Total tokens", fmtTokens2(session["total_tokens"])],
8723
9000
  ["Requests", fmtCount(session["request_count"])]
8724
9001
  ]);
8725
9002
  if (requests.length > 0) {
@@ -8729,10 +9006,10 @@ program.command("session <id>").description("Show detailed breakdown of a single
8729
9006
  printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], requests.slice(0, 50).map((r) => [
8730
9007
  chalk7.dim(String(r["timestamp"]).substring(11, 19)),
8731
9008
  chalk7.white(String(r["model"]).substring(0, 22)),
8732
- fmtTokens(r["input_tokens"]),
8733
- fmtTokens(r["output_tokens"]),
8734
- fmtTokens(r["cache_read_tokens"]),
8735
- fmtTokens(r["cache_create_tokens"]),
9009
+ fmtTokens2(r["input_tokens"]),
9010
+ fmtTokens2(r["output_tokens"]),
9011
+ fmtTokens2(r["cache_read_tokens"]),
9012
+ fmtTokens2(r["cache_create_tokens"]),
8736
9013
  fmt4(r["cost_usd"])
8737
9014
  ]));
8738
9015
  if (requests.length > 50)
@@ -8763,6 +9040,31 @@ program.command("machines").description("List all machines that have synced data
8763
9040
  ${chalk7.dim("Current machine:")} ${chalk7.bold(current)}`);
8764
9041
  console.log();
8765
9042
  });
9043
+ program.command("fleet-sync").description("Loop-safe cross-machine sync using remote SQLite snapshots").option("--machine <ids>", "Comma-separated remote machines", "spark02,apple03").option("--no-local-sync", "Skip local economy sync before remote merge").option("--no-remote-sync", "Skip remote economy sync before snapshot").option("--cache-dir <path>", "Directory for verified pulled DB snapshots").option("--report-dir <path>", "Directory for JSON evidence reports").option("--remote-snapshot-dir <path>", "Remote snapshot directory", "$HOME/.hasna/economy/fleet-sync-snapshots").option("--timeout-ms <n>", "Per remote command timeout", "120000").option("--dry-run", "Plan without syncing, pulling, or merging").option("--json", "Output JSON").action((opts) => {
9044
+ const result = runEconomyFleetSync({
9045
+ machines: opts.machine?.split(",").map((entry) => entry.trim()).filter(Boolean),
9046
+ localSync: opts.localSync !== false,
9047
+ remoteSync: opts.remoteSync !== false,
9048
+ cacheDir: opts.cacheDir,
9049
+ remoteSnapshotDir: opts.remoteSnapshotDir,
9050
+ timeoutMs: parsePositiveCliInteger(opts.timeoutMs, "--timeout-ms"),
9051
+ dryRun: opts.dryRun
9052
+ });
9053
+ const reportPath = opts.reportDir ? writeFleetSyncReport(opts.reportDir, result) : undefined;
9054
+ if (opts.json) {
9055
+ console.log(JSON.stringify({ ...result, ...reportPath ? { report_path: reportPath } : {} }, null, 2));
9056
+ return;
9057
+ }
9058
+ const status = result.summary.failed === 0 ? chalk7.green("ok") : chalk7.red("failed");
9059
+ console.log(`${status} machines=${result.summary.machines} merged=${result.summary.merged} failed=${result.summary.failed} dry_run=${result.summary.dry_run}`);
9060
+ if (reportPath)
9061
+ console.log(chalk7.dim(`report=${reportPath}`));
9062
+ for (const row of result.remote.filter((entry) => entry.status !== "merged").slice(0, 20)) {
9063
+ console.log(`${row.status === "failed" ? chalk7.red("failed") : chalk7.yellow(row.status)} ${row.machine} ${chalk7.dim(row.error ?? "")}`);
9064
+ }
9065
+ if (result.local.status === "failed" || result.summary.failed > 0)
9066
+ process.exitCode = 1;
9067
+ });
8766
9068
  program.command("merge-db <source-db>").description("Merge another Economy SQLite database into this machine").option("--source-machine <id>", "Machine id to use for source rows that do not have one").option("--json", "Output JSON").action((sourceDb, opts) => {
8767
9069
  const db = openDatabase();
8768
9070
  const result = mergePeerDatabase(db, sourceDb, { sourceMachine: opts.sourceMachine });
@@ -8802,8 +9104,8 @@ program.command("export").description("Export data as CSV").option("--type <type
8802
9104
  }
8803
9105
  }
8804
9106
  if (opts.output) {
8805
- const { writeFileSync: writeFileSync4 } = await import("fs");
8806
- writeFileSync4(opts.output, csv);
9107
+ const { writeFileSync: writeFileSync5 } = await import("fs");
9108
+ writeFileSync5(opts.output, csv);
8807
9109
  console.log(chalk7.green(`\u2713 Exported to ${opts.output}`));
8808
9110
  } else {
8809
9111
  process.stdout.write(csv);
@@ -8875,7 +9177,7 @@ program.command("compare <period1> <period2>").description("Compare two periods
8875
9177
  ["Cost", fmt4(a.cost), fmt4(b.cost), delta(a.cost, b.cost)],
8876
9178
  ["Sessions", fmtCount(a.sessions), fmtCount(b.sessions), delta(a.sessions, b.sessions)],
8877
9179
  ["Requests", fmtCount(a.requests), fmtCount(b.requests), delta(a.requests, b.requests)],
8878
- ["Tokens", fmtTokens(a.tokens), fmtTokens(b.tokens), delta(a.tokens, b.tokens)]
9180
+ ["Tokens", fmtTokens2(a.tokens), fmtTokens2(b.tokens), delta(a.tokens, b.tokens)]
8879
9181
  ]);
8880
9182
  console.log();
8881
9183
  });