@hasna/economy 0.2.41 → 0.2.43

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();
@@ -4607,16 +4673,275 @@ var init_watch = __esm(() => {
4607
4673
  init_notification();
4608
4674
  });
4609
4675
 
4676
+ // src/db/sync-pg.ts
4677
+ import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
4678
+ import { fileURLToPath } from "url";
4679
+ import { dirname as dirname3, join as join14 } from "path";
4680
+ import { existsSync as existsSync16 } from "fs";
4681
+ import { translateSql, translateParams } from "@hasna/cloud";
4682
+ function translateSqliteDates(sql) {
4683
+ let s = sql;
4684
+ const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
4685
+ s = s.replace(/DATE\('now',\s*'weekday 0',\s*'-7 days'\)/gi, isoDate("(date_trunc('week', now() + interval '1 day') - interval '1 day')"));
4686
+ s = s.replace(/DATE\('now',\s*'start of month'\)/gi, isoDate("date_trunc('month', now())"));
4687
+ s = s.replace(/DATE\('now',\s*'start of year'\)/gi, isoDate("date_trunc('year', now())"));
4688
+ s = s.replace(/DATE\('now',\s*'(-?\d+)\s*days?'\)/gi, (_m, n) => isoDate(`now() + interval '${n} days'`));
4689
+ s = s.replace(/DATE\('now',\s*(\$\d+)\s*\|\|\s*' days'\)/gi, (_m, p) => isoDate(`now() + ((${p}) || ' days')::interval`));
4690
+ s = s.replace(/DATETIME\('now',\s*(\$\d+)\)/gi, (_m, p) => `(now() + (${p})::interval)`);
4691
+ s = s.replace(/DATETIME\('now',\s*'([^']+)'\)/gi, (_m, lit) => `(now() + interval '${lit}')`);
4692
+ const strftimeMap = { "%H": "HH24", "%Y": "YYYY", "%m": "MM", "%d": "DD", "%M": "MI", "%S": "SS" };
4693
+ s = s.replace(/STRFTIME\('([^']+)',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, fmt4, col) => {
4694
+ const pgFmt = Object.entries(strftimeMap).reduce((acc, [k, v]) => acc.split(k).join(v), fmt4);
4695
+ return `to_char(CAST("${col}" AS timestamptz), '${pgFmt}')`;
4696
+ });
4697
+ s = s.replace(/\bDATE\('now'\)/gi, isoDate("now()"));
4698
+ s = s.replace(/\bDATETIME\('now'\)/gi, "now()");
4699
+ s = s.replace(/\bDATETIME\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `CAST("${col}" AS timestamptz)`);
4700
+ s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `substr("${col}", 1, 10)`);
4701
+ return s;
4702
+ }
4703
+ function resolveWorkerPath() {
4704
+ const here = dirname3(fileURLToPath(import.meta.url));
4705
+ const candidates = [
4706
+ join14(here, "pg-sync-worker.ts"),
4707
+ join14(here, "pg-sync-worker.js"),
4708
+ join14(here, "..", "db", "pg-sync-worker.js")
4709
+ ];
4710
+ for (const candidate of candidates)
4711
+ if (existsSync16(candidate))
4712
+ return candidate;
4713
+ return candidates[0];
4714
+ }
4715
+
4716
+ class SyncPgAdapter {
4717
+ worker;
4718
+ port;
4719
+ counter = 0;
4720
+ constructor(dsn) {
4721
+ const workerPath = resolveWorkerPath();
4722
+ this.worker = new Worker(workerPath, { workerData: { dsn } });
4723
+ this.worker.on("error", (err) => console.error(JSON.stringify({ evt: "pg_worker_error", message: err.message })));
4724
+ const channel = new MessageChannel;
4725
+ this.port = channel.port1;
4726
+ this.worker.postMessage({ type: "init", port: channel.port2 }, [channel.port2]);
4727
+ }
4728
+ query(sql, params) {
4729
+ const pgSql = translateSqliteDates(translateSql(sql, "pg"));
4730
+ const pgParams = translateParams(params);
4731
+ const id = ++this.counter;
4732
+ const signal = new SharedArrayBuffer(4);
4733
+ const sig = new Int32Array(signal);
4734
+ Atomics.store(sig, 0, 0);
4735
+ this.worker.postMessage({ id, sql: pgSql, params: pgParams, signal });
4736
+ Atomics.wait(sig, 0, 0);
4737
+ let received = receiveMessageOnPort(this.port);
4738
+ for (let attempt = 0;!received && attempt < 1e6; attempt++)
4739
+ received = receiveMessageOnPort(this.port);
4740
+ if (!received)
4741
+ throw new Error("SyncPgAdapter: no reply from worker");
4742
+ const reply = received.message;
4743
+ if (!reply.ok)
4744
+ throw new Error(reply.error);
4745
+ return { rows: reply.rows, rowCount: reply.rowCount };
4746
+ }
4747
+ run(sql, ...params) {
4748
+ const result = this.query(sql, params);
4749
+ return { changes: result.rowCount, lastInsertRowid: result.rows?.[0]?.["id"] ?? 0 };
4750
+ }
4751
+ get(sql, ...params) {
4752
+ return this.query(sql, params).rows[0] ?? null;
4753
+ }
4754
+ all(sql, ...params) {
4755
+ return this.query(sql, params).rows;
4756
+ }
4757
+ exec(sql) {
4758
+ this.query(sql, []);
4759
+ }
4760
+ prepare(sql) {
4761
+ const self = this;
4762
+ return {
4763
+ run: (...params) => self.run(sql, ...params),
4764
+ get: (...params) => self.get(sql, ...params),
4765
+ all: (...params) => self.all(sql, ...params),
4766
+ finalize: () => {}
4767
+ };
4768
+ }
4769
+ transaction(fn) {
4770
+ return fn();
4771
+ }
4772
+ close() {
4773
+ this.worker.terminate();
4774
+ }
4775
+ }
4776
+ var init_sync_pg = () => {};
4777
+
4778
+ // src/db/cloud.ts
4779
+ import pg from "pg";
4780
+ function getCloudDatabaseUrl2() {
4781
+ return process.env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || process.env["ECONOMY_DATABASE_URL"]?.trim() || process.env["DATABASE_URL"]?.trim() || undefined;
4782
+ }
4783
+ function isCloudMode() {
4784
+ const mode = process.env["HASNA_ECONOMY_STORAGE_MODE"]?.trim().toLowerCase();
4785
+ if (mode === "cloud")
4786
+ return true;
4787
+ if (mode === "local")
4788
+ return false;
4789
+ return Boolean(getCloudDatabaseUrl2());
4790
+ }
4791
+ function resolveSigningSecret() {
4792
+ return process.env["HASNA_ECONOMY_API_SIGNING_KEY"]?.trim() || process.env["HASNA_API_SIGNING_KEY"]?.trim() || process.env["API_KEY_SIGNING_SECRET"]?.trim() || undefined;
4793
+ }
4794
+ function openCloudDatabase(dsn = getCloudDatabaseUrl2()) {
4795
+ if (!dsn) {
4796
+ throw new Error("economy cloud mode requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
4797
+ }
4798
+ return new SyncPgAdapter(dsn);
4799
+ }
4800
+ function createCloudPool(dsn = getCloudDatabaseUrl2()) {
4801
+ if (!dsn)
4802
+ throw new Error("economy cloud mode requires a Postgres DSN (HASNA_ECONOMY_DATABASE_URL)");
4803
+ const ssl = dsn.includes("sslmode=require") || dsn.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
4804
+ const pool = new pg.Pool({ connectionString: dsn, ssl, max: Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5"), connectionTimeoutMillis: 1e4 });
4805
+ pool.on("error", (err) => {
4806
+ console.error(JSON.stringify({ evt: "pg_pool_error", message: err.message }));
4807
+ });
4808
+ return pool;
4809
+ }
4810
+ function authClientFromPool(pool) {
4811
+ return {
4812
+ async many(sql, params = []) {
4813
+ const res = await pool.query(sql, params);
4814
+ return res.rows;
4815
+ },
4816
+ async get(sql, params = []) {
4817
+ const res = await pool.query(sql, params);
4818
+ return res.rows[0] ?? null;
4819
+ },
4820
+ async execute(sql, params = []) {
4821
+ await pool.query(sql, params);
4822
+ }
4823
+ };
4824
+ }
4825
+ var init_cloud = __esm(() => {
4826
+ init_sync_pg();
4827
+ init_pg_migrations();
4828
+ });
4829
+
4830
+ // src/openapi.ts
4831
+ var openApiSpec;
4832
+ var init_openapi = __esm(() => {
4833
+ openApiSpec = {
4834
+ openapi: "3.0.3",
4835
+ info: {
4836
+ title: "@hasna/economy self-hosted API",
4837
+ version: "0.0.0",
4838
+ description: "AI coding cost tracker \u2014 self-hosted control plane. Cloud mode is PURE REMOTE per Amendment A1: the serve process reads/writes the shared RDS Postgres directly with API-key auth via @hasna/contracts. All /v1 routes require a valid `economy:*` scoped API key (x-api-key or Authorization: Bearer). The foundation probes /health, /ready, /version are open."
4839
+ },
4840
+ servers: [{ url: "https://economy.hasna.xyz", description: "prod" }, { url: "http://localhost:3456", description: "local" }],
4841
+ security: [{ apiKey: [] }, { bearer: [] }],
4842
+ components: {
4843
+ securitySchemes: {
4844
+ apiKey: { type: "apiKey", in: "header", name: "x-api-key" },
4845
+ bearer: { type: "http", scheme: "bearer" }
4846
+ },
4847
+ schemas: {
4848
+ Envelope: { type: "object", properties: { data: {}, meta: { type: "object", additionalProperties: true } }, required: ["data"] },
4849
+ Foundation: { type: "object", properties: { status: { type: "string" }, version: { type: "string" }, mode: { type: "string" }, service: { type: "string" } }, required: ["status", "version", "mode"] },
4850
+ Error: { type: "object", properties: { error: { type: "string" }, message: { type: "string" } }, required: ["error"] },
4851
+ MutationOk: { type: "object", properties: { ok: { type: "boolean" } } },
4852
+ CostSummary: { type: "object", properties: { total_cost_usd: { type: "number" }, total_tokens: { type: "number" }, request_count: { type: "number" }, session_count: { type: "number" }, period: { type: "string" } }, additionalProperties: true },
4853
+ Session: { type: "object", properties: { id: { type: "string" }, agent: { type: "string" }, project_path: { type: "string" }, project_name: { type: "string" }, started_at: { type: "string" }, ended_at: { type: "string", nullable: true }, total_cost_usd: { type: "number" }, total_tokens: { type: "number" }, request_count: { type: "number" } }, additionalProperties: true },
4854
+ ModelBreakdown: { type: "object", additionalProperties: true },
4855
+ ProjectBreakdown: { type: "object", additionalProperties: true },
4856
+ AgentBreakdown: { type: "object", additionalProperties: true },
4857
+ AccountBreakdown: { type: "object", additionalProperties: true },
4858
+ DailyPoint: { type: "object", additionalProperties: true },
4859
+ BudgetStatus: { type: "object", properties: { id: { type: "string" }, limit_usd: { type: "number" }, period: { type: "string" }, spent_usd: { type: "number" } }, additionalProperties: true },
4860
+ CreateBudgetInput: { type: "object", properties: { limit_usd: { type: "number" }, alert_at_percent: { type: "number" }, period: { type: "string", enum: ["daily", "weekly", "monthly", "day", "week", "month"] }, project_path: { type: "string", nullable: true }, agent: { type: "string", nullable: true } }, required: ["limit_usd"] },
4861
+ GoalStatus: { type: "object", additionalProperties: true },
4862
+ CreateGoalInput: { type: "object", properties: { period: { type: "string", enum: ["day", "week", "month", "year"] }, limit_usd: { type: "number" }, project_path: { type: "string", nullable: true }, agent: { type: "string", nullable: true } }, required: ["limit_usd"] },
4863
+ ModelPricing: { type: "object", additionalProperties: true },
4864
+ CreatePricingInput: { type: "object", properties: { model: { type: "string" }, input_per_1m: { type: "number" }, output_per_1m: { type: "number" } }, required: ["model", "input_per_1m", "output_per_1m"] },
4865
+ Subscription: { type: "object", additionalProperties: true },
4866
+ CreateSubscriptionInput: { type: "object", properties: { provider: { type: "string" }, plan: { type: "string" }, monthly_fee_usd: { type: "number" }, included_usage_usd: { type: "number" }, agent: { type: "string", nullable: true } }, required: ["provider", "plan"] },
4867
+ Project: { type: "object", additionalProperties: true },
4868
+ CreateProjectInput: { type: "object", properties: { path: { type: "string" }, name: { type: "string" }, description: { type: "string" }, tags: { type: "array", items: { type: "string" } } }, required: ["path"] },
4869
+ MachineInfo: { type: "object", additionalProperties: true },
4870
+ BillingSummary: { type: "object", additionalProperties: true }
4871
+ }
4872
+ },
4873
+ paths: {
4874
+ "/health": { get: { operationId: "health", summary: "Liveness probe", security: [], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Foundation" } } } } } } },
4875
+ "/ready": { get: { operationId: "ready", summary: "Readiness probe (storage reachable + migrated)", security: [], responses: { "200": { description: "ready", content: { "application/json": { schema: { $ref: "#/components/schemas/Foundation" } } } }, "503": { description: "not ready", content: { "application/json": { schema: { $ref: "#/components/schemas/Foundation" } } } } } } },
4876
+ "/version": { get: { operationId: "version", summary: "Version probe", security: [], responses: { "200": { description: "version", content: { "application/json": { schema: { $ref: "#/components/schemas/Foundation" } } } } } } },
4877
+ "/v1/summary": { get: { operationId: "getSummary", summary: "Cost summary for a period", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/CostSummary" } } } } } } } } },
4878
+ "/v1/sessions": { get: { operationId: "listSessions", summary: "List sessions", parameters: [{ name: "agent", in: "query", schema: { type: "string" } }, { name: "project", in: "query", schema: { type: "string" } }, { name: "search", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }, { name: "account", in: "query", schema: { type: "string" } }, { name: "limit", in: "query", schema: { type: "integer" } }, { name: "offset", in: "query", schema: { type: "integer" } }, { name: "since", in: "query", schema: { type: "string" } }, { name: "fields", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/Session" } } } } } } } } } },
4879
+ "/v1/sessions/{id}/requests": { get: { operationId: "getSessionRequests", summary: "Requests for a session", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } }, "404": { description: "not found", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } } } },
4880
+ "/v1/top": { get: { operationId: "topSessions", summary: "Top sessions by cost", parameters: [{ name: "n", in: "query", schema: { type: "integer" } }, { name: "agent", in: "query", schema: { type: "string" } }, { name: "since", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/Session" } } } } } } } } } },
4881
+ "/v1/models": { get: { operationId: "modelBreakdown", summary: "Cost by model", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/ModelBreakdown" } } } } } } } } } },
4882
+ "/v1/projects": { get: { operationId: "projectBreakdown", summary: "Cost by project", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/ProjectBreakdown" } } } } } } } } } },
4883
+ "/v1/accounts": { get: { operationId: "accountBreakdown", summary: "Cost by account", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/AccountBreakdown" } } } } } } } } } },
4884
+ "/v1/breakdown": { get: { operationId: "breakdown", summary: "Cost breakdown by dimension", parameters: [{ name: "by", in: "query", schema: { type: "string", enum: ["model", "project", "agent", "account"] } }, { name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
4885
+ "/v1/daily": { get: { operationId: "daily", summary: "Daily cost points", parameters: [{ name: "days", in: "query", schema: { type: "integer" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/DailyPoint" } } } } } } } } } },
4886
+ "/v1/hourly": { get: { operationId: "hourly", summary: "Hourly cost points", parameters: [{ name: "hours", in: "query", schema: { type: "integer" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
4887
+ "/v1/machines": { get: { operationId: "listMachines", summary: "Known machines", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/MachineInfo" } } } } } } } } } },
4888
+ "/v1/fleet": { get: { operationId: "fleet", summary: "Fleet summary", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
4889
+ "/v1/billing": { get: { operationId: "billing", summary: "Provider billing summary", parameters: [{ name: "period", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/BillingSummary" } } } } } } } } },
4890
+ "/v1/billing/diff": { get: { operationId: "billingDiff", summary: "Billing vs tracked diff", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "threshold", in: "query", schema: { type: "number" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
4891
+ "/v1/usage": { get: { operationId: "usage", summary: "Usage snapshots + summary", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "agent", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
4892
+ "/v1/savings": { get: { operationId: "savings", summary: "Savings summary", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "agent", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
4893
+ "/v1/budgets": {
4894
+ get: { operationId: "listBudgets", summary: "List budgets", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/BudgetStatus" } } } } } } } } },
4895
+ post: { operationId: "createBudget", summary: "Create a budget", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateBudgetInput" } } } }, responses: { "200": { description: "created", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/BudgetStatus" } } } } } } } }
4896
+ },
4897
+ "/v1/budgets/{id}": { delete: { operationId: "deleteBudget", summary: "Delete a budget", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
4898
+ "/v1/goals": {
4899
+ get: { operationId: "listGoals", summary: "List goals", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/GoalStatus" } } } } } } } } },
4900
+ post: { operationId: "createGoal", summary: "Create a goal", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateGoalInput" } } } }, responses: { "200": { description: "created", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/GoalStatus" } } } } } } } }
4901
+ },
4902
+ "/v1/goals/{id}": { delete: { operationId: "deleteGoal", summary: "Delete a goal", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
4903
+ "/v1/pricing": {
4904
+ get: { operationId: "listPricing", summary: "List model pricing", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/ModelPricing" } } } } } } } } },
4905
+ post: { operationId: "upsertPricing", summary: "Upsert model pricing", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreatePricingInput" } } } }, responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/ModelPricing" } } } } } } } }
4906
+ },
4907
+ "/v1/pricing/{model}": { delete: { operationId: "deletePricing", summary: "Delete model pricing", parameters: [{ name: "model", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
4908
+ "/v1/subscriptions": {
4909
+ get: { operationId: "listSubscriptions", summary: "List subscriptions", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/Subscription" } } } } } } } } },
4910
+ post: { operationId: "upsertSubscription", summary: "Upsert a subscription", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateSubscriptionInput" } } } }, responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/Subscription" } } } } } } } }
4911
+ },
4912
+ "/v1/subscriptions/{id}": { delete: { operationId: "deleteSubscription", summary: "Delete a subscription", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
4913
+ "/v1/project-registry": {
4914
+ get: { operationId: "listProjectRegistry", summary: "List registered projects", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/Project" } } } } } } } } },
4915
+ post: { operationId: "upsertProject", summary: "Register a project", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateProjectInput" } } } }, responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } }
4916
+ },
4917
+ "/v1/project-registry/{id}": { delete: { operationId: "deleteProject", summary: "Delete a registered project", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
4918
+ "/v1/sync": { post: { operationId: "sync", summary: "Trigger an ingest sync", requestBody: { required: false, content: { "application/json": { schema: { type: "object", properties: { sources: { type: "string" } } } } } }, responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } }
4919
+ }
4920
+ };
4921
+ });
4922
+
4610
4923
  // src/server/serve.ts
4611
4924
  var exports_serve = {};
4612
4925
  __export(exports_serve, {
4613
4926
  startServer: () => startServer,
4927
+ openApiDocument: () => openApiDocument,
4614
4928
  createServerFetch: () => createServerFetch,
4615
4929
  createHandler: () => createHandler
4616
4930
  });
4617
4931
  import { randomUUID as randomUUID2 } from "crypto";
4618
- import { existsSync as existsSync15 } from "fs";
4619
- import { resolve, sep } from "path";
4932
+ import { existsSync as existsSync17 } from "fs";
4933
+ import { resolve as resolve2, sep } from "path";
4934
+ import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
4935
+ function openApiDocument() {
4936
+ const spec = openApiSpec;
4937
+ return { ...spec, info: { ...spec["info"], version: packageMetadata.version } };
4938
+ }
4939
+ function serveMode() {
4940
+ return isCloudMode() ? "self_hosted" : "local";
4941
+ }
4942
+ function foundationEnvelope(status, extra = {}) {
4943
+ return { status, version: packageMetadata.version, mode: serveMode(), service: "economy", ...extra };
4944
+ }
4620
4945
  function json(data, status = 200) {
4621
4946
  return new Response(JSON.stringify(data), {
4622
4947
  status,
@@ -4670,23 +4995,23 @@ function dashboardPath(root, pathname) {
4670
4995
  return null;
4671
4996
  }
4672
4997
  const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
4673
- const rootPath = resolve(root);
4674
- const filePath = resolve(rootPath, relativePath);
4998
+ const rootPath = resolve2(root);
4999
+ const filePath = resolve2(rootPath, relativePath);
4675
5000
  return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
4676
5001
  }
4677
5002
  function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
4678
5003
  return async function fetch2(req) {
4679
5004
  const url = new URL(req.url);
4680
- if (url.pathname.startsWith("/api") || url.pathname === "/health") {
5005
+ if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
4681
5006
  return apiHandler(req);
4682
5007
  }
4683
- if (existsSync15(dashboardDir)) {
5008
+ if (existsSync17(dashboardDir)) {
4684
5009
  const filePath = dashboardPath(dashboardDir, url.pathname);
4685
- if (filePath && existsSync15(filePath)) {
5010
+ if (filePath && existsSync17(filePath)) {
4686
5011
  return new Response(Bun.file(filePath));
4687
5012
  }
4688
5013
  const indexPath = dashboardPath(dashboardDir, "/");
4689
- if (indexPath && existsSync15(indexPath)) {
5014
+ if (indexPath && existsSync17(indexPath)) {
4690
5015
  return new Response(Bun.file(indexPath));
4691
5016
  }
4692
5017
  }
@@ -4698,15 +5023,34 @@ function applyFields(obj, fields) {
4698
5023
  return obj;
4699
5024
  return Object.fromEntries(fields.map((f) => [f, obj[f] ?? null]));
4700
5025
  }
4701
- function createHandler(db) {
5026
+ function createHandler(db, options = {}) {
4702
5027
  return async function handler(req) {
4703
5028
  const url = new URL(req.url);
4704
- const path = url.pathname;
5029
+ const rawPath = url.pathname;
5030
+ const path = rawPath.startsWith("/v1/") ? "/api" + rawPath.slice(3) : rawPath === "/v1" ? "/api" : rawPath;
4705
5031
  const method = req.method;
4706
5032
  if (method === "OPTIONS")
4707
5033
  return new Response(null, { status: 204, headers: CORS });
4708
- if (!isAuthorizedRequest(req, path))
5034
+ if (method === "GET" && (rawPath === "/health" || rawPath === "/healthz")) {
5035
+ return json(foundationEnvelope("ok"));
5036
+ }
5037
+ if (method === "GET" && (rawPath === "/version" || rawPath === "/v1/version")) {
5038
+ return json(foundationEnvelope("ok"));
5039
+ }
5040
+ if (method === "GET" && (rawPath === "/ready" || rawPath === "/readyz")) {
5041
+ const result = options.readyCheck ? await options.readyCheck() : { ready: true };
5042
+ return json(foundationEnvelope(result.ready ? "ready" : "not_ready", result.detail ? { detail: result.detail } : {}), result.ready ? 200 : 503);
5043
+ }
5044
+ if (method === "GET" && rawPath === "/openapi.json") {
5045
+ return json(openApiDocument());
5046
+ }
5047
+ if (options.authenticator) {
5048
+ const decision = await options.authenticator.authenticate(req.headers, { method, path: rawPath });
5049
+ if (!decision.ok)
5050
+ return json({ error: decision.reason ?? "unauthorized", message: decision.message }, decision.status || 401);
5051
+ } else if (!isAuthorizedRequest(req, path)) {
4709
5052
  return err("Unauthorized", 401);
5053
+ }
4710
5054
  if (path === "/health")
4711
5055
  return ok({ status: "ok", ts: new Date().toISOString() });
4712
5056
  if (path === "/api/summary" && method === "GET") {
@@ -4771,7 +5115,8 @@ function createHandler(db) {
4771
5115
  if (path === "/api/top" && method === "GET") {
4772
5116
  const n = Number(url.searchParams.get("n") ?? 10);
4773
5117
  const agent = url.searchParams.get("agent") ?? undefined;
4774
- return ok(queryTopSessions(db, n, agent));
5118
+ const since = url.searchParams.get("since") ?? undefined;
5119
+ return ok(queryTopSessions(db, n, agent, since));
4775
5120
  }
4776
5121
  if (path === "/api/models" && method === "GET") {
4777
5122
  return ok(queryModelBreakdown(db));
@@ -5058,22 +5403,56 @@ function createHandler(db) {
5058
5403
  return err("Not found", 404);
5059
5404
  };
5060
5405
  }
5406
+ function isLocalHost(host) {
5407
+ return ["127.0.0.1", "localhost", "::1"].includes(host);
5408
+ }
5061
5409
  function startServer(port = 3456, options = {}) {
5062
- const db = options.db ?? openDatabase();
5063
- ensurePricingSeeded(db);
5064
- const apiHandler = createHandler(db);
5065
- const hostname2 = options.hostname ?? getServeBindHost();
5410
+ const cloud = options.db ? false : isCloudMode();
5411
+ const hostname2 = options.hostname ?? (cloud ? process.env["ECONOMY_HOST"] ?? "0.0.0.0" : getServeBindHost());
5412
+ const log = options.log ?? console.log;
5413
+ const handlerOptions = {};
5414
+ let db;
5415
+ if (cloud) {
5416
+ db = openCloudDatabase();
5417
+ const pool = createCloudPool();
5418
+ const signingSecret = resolveSigningSecret();
5419
+ if (signingSecret) {
5420
+ const keys = new ApiKeyStore(authClientFromPool(pool));
5421
+ keys.ensureSchema().catch((e) => log(`api_keys ensureSchema: ${e instanceof Error ? e.message : String(e)}`));
5422
+ handlerOptions.authenticator = verifyApiKey({
5423
+ app: APP,
5424
+ signingSecret,
5425
+ isRevoked: keys.statusChecker(),
5426
+ audit: (event) => log(JSON.stringify({ evt: "api_auth", outcome: event.outcome, kid: event.kid, reason: event.reason, path: event.path, status: event.status }))
5427
+ });
5428
+ } else if (!isLocalHost(hostname2)) {
5429
+ throw new Error("economy-serve on a non-local host requires an API signing secret (HASNA_ECONOMY_API_SIGNING_KEY / API_KEY_SIGNING_SECRET)");
5430
+ }
5431
+ handlerOptions.readyCheck = async () => {
5432
+ try {
5433
+ const res = await pool.query("SELECT to_regclass($1) AS reg", ["public.requests"]);
5434
+ if (!res.rows[0]?.reg)
5435
+ return { ready: false, detail: "pending_migrations:requests" };
5436
+ return { ready: true };
5437
+ } catch (error) {
5438
+ return { ready: false, detail: error instanceof Error ? error.message : "storage_unreachable" };
5439
+ }
5440
+ };
5441
+ } else {
5442
+ db = options.db ?? openDatabase();
5443
+ ensurePricingSeeded(db);
5444
+ }
5445
+ const apiHandler = createHandler(db, handlerOptions);
5066
5446
  const server = Bun.serve({
5067
5447
  port,
5068
5448
  hostname: hostname2,
5069
5449
  fetch: createServerFetch(apiHandler, options.dashboardDir)
5070
5450
  });
5071
5451
  const address = `http://${hostname2 === "0.0.0.0" ? "localhost" : hostname2}:${server.port}`;
5072
- const log = options.log ?? console.log;
5073
5452
  log(`economy-serve listening on ${address}`);
5074
5453
  return server;
5075
5454
  }
5076
- var CORS, AGENT_ERROR, SYNC_SOURCES, DEFAULT_DASHBOARD_DIR;
5455
+ var CORS, AGENT_ERROR, SYNC_SOURCES, DEFAULT_DASHBOARD_DIR, FOUNDATION_PATHS, APP = "economy";
5077
5456
  var init_serve = __esm(() => {
5078
5457
  init_database();
5079
5458
  init_pricing();
@@ -5081,6 +5460,9 @@ var init_serve = __esm(() => {
5081
5460
  init_sync_all();
5082
5461
  init_billing_diff();
5083
5462
  init_database();
5463
+ init_package_metadata();
5464
+ init_cloud();
5465
+ init_openapi();
5084
5466
  CORS = {
5085
5467
  "Access-Control-Allow-Origin": "*",
5086
5468
  "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
@@ -5089,6 +5471,7 @@ var init_serve = __esm(() => {
5089
5471
  AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
5090
5472
  SYNC_SOURCES = ["all", ...AGENTS];
5091
5473
  DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
5474
+ FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
5092
5475
  });
5093
5476
 
5094
5477
  // src/cli/commands/menubar.ts
@@ -5101,14 +5484,14 @@ __export(exports_menubar, {
5101
5484
  });
5102
5485
  import chalk6 from "chalk";
5103
5486
  import { execFileSync as execFileSync3 } from "child_process";
5104
- import { cpSync, existsSync as existsSync16, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
5487
+ import { cpSync, existsSync as existsSync18, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
5105
5488
  import { tmpdir, arch } from "os";
5106
- import { join as join13 } from "path";
5489
+ import { join as join15 } from "path";
5107
5490
  function getArch() {
5108
5491
  return arch() === "arm64" ? "arm64" : "x86_64";
5109
5492
  }
5110
5493
  function isInstalled() {
5111
- return existsSync16(APP_PATH);
5494
+ return existsSync18(APP_PATH);
5112
5495
  }
5113
5496
  function isRunning() {
5114
5497
  try {
@@ -5145,15 +5528,15 @@ async function menubarInstall(opts) {
5145
5528
  console.error(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
5146
5529
  process.exit(1);
5147
5530
  }
5148
- const zipPath = join13(tmpdir(), `economy-bar-${cpuArch}.zip`);
5149
- const extractDir = join13(tmpdir(), "economy-bar-extracted");
5531
+ const zipPath = join15(tmpdir(), `economy-bar-${cpuArch}.zip`);
5532
+ const extractDir = join15(tmpdir(), "economy-bar-extracted");
5150
5533
  console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
5151
5534
  try {
5152
5535
  const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
5153
5536
  if (!res.ok)
5154
5537
  throw new Error(`Download failed: ${res.status}`);
5155
5538
  const buffer = await res.arrayBuffer();
5156
- writeFileSync3(zipPath, Buffer.from(buffer));
5539
+ writeFileSync4(zipPath, Buffer.from(buffer));
5157
5540
  console.log(chalk6.green(`\u2713 Downloaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`));
5158
5541
  } catch (e) {
5159
5542
  console.error(chalk6.red(`\u2717 Download failed: ${e instanceof Error ? e.message : String(e)}`));
@@ -5161,17 +5544,17 @@ async function menubarInstall(opts) {
5161
5544
  }
5162
5545
  console.log(chalk6.cyan("\u2192 Installing to /Applications..."));
5163
5546
  try {
5164
- rmSync(extractDir, { recursive: true, force: true });
5165
- mkdirSync4(extractDir, { recursive: true });
5547
+ rmSync2(extractDir, { recursive: true, force: true });
5548
+ mkdirSync5(extractDir, { recursive: true });
5166
5549
  execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
5167
5550
  if (isInstalled())
5168
- rmSync(APP_PATH, { recursive: true, force: true });
5169
- cpSync(join13(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
5551
+ rmSync2(APP_PATH, { recursive: true, force: true });
5552
+ cpSync(join15(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
5170
5553
  try {
5171
5554
  execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
5172
5555
  } catch {}
5173
- rmSync(zipPath, { force: true });
5174
- rmSync(extractDir, { recursive: true, force: true });
5556
+ rmSync2(zipPath, { force: true });
5557
+ rmSync2(extractDir, { recursive: true, force: true });
5175
5558
  console.log(chalk6.green(`\u2713 Installed to ${APP_PATH}`));
5176
5559
  } catch (e) {
5177
5560
  console.error(chalk6.red(`\u2717 Install failed: ${e instanceof Error ? e.message : String(e)}`));
@@ -5198,7 +5581,7 @@ function menubarUninstall() {
5198
5581
  execFileSync3("sleep", ["1"], { stdio: "ignore" });
5199
5582
  } catch {}
5200
5583
  }
5201
- rmSync(APP_PATH, { recursive: true, force: true });
5584
+ rmSync2(APP_PATH, { recursive: true, force: true });
5202
5585
  console.log(chalk6.green("\u2713 Economy Bar uninstalled"));
5203
5586
  }
5204
5587
  function menubarStart() {
@@ -6564,6 +6947,15 @@ init_paths();
6564
6947
  function fmt2(usd) {
6565
6948
  return "$" + usd.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
6566
6949
  }
6950
+ function fmtTokens(n) {
6951
+ if (n >= 1e9)
6952
+ return `${(n / 1e9).toFixed(1)}B`;
6953
+ if (n >= 1e6)
6954
+ return `${(n / 1e6).toFixed(1)}M`;
6955
+ if (n >= 1000)
6956
+ return `${(n / 1000).toFixed(1)}k`;
6957
+ return n.toLocaleString("en-US");
6958
+ }
6567
6959
  function parsePeriod(value, fallback = "month") {
6568
6960
  const p = value ?? fallback;
6569
6961
  if (!["today", "yesterday", "week", "month", "year", "all"].includes(p)) {
@@ -6578,7 +6970,7 @@ function registerExtendedCommands(program) {
6578
6970
  const period = parsePeriod(periodArg, "month");
6579
6971
  const agent = parseAgent(opts.agent, "--agent");
6580
6972
  const snaps = queryUsageSnapshots(db, { agent, ...usageSnapshotFilterForPeriod(period) });
6581
- const summary = querySummary(db, period, undefined, true);
6973
+ const summary = querySummary(db, period, undefined, true, agent);
6582
6974
  if (opts.json) {
6583
6975
  console.log(JSON.stringify({ period, agent: agent ?? "all", snapshots: snaps, summary }, null, 2));
6584
6976
  return;
@@ -6593,8 +6985,9 @@ function registerExtendedCommands(program) {
6593
6985
  console.log(` ${chalk4.white(s.agent.padEnd(10))} ${s.metric.padEnd(28)} ${s.value}${s.unit ? ` ${s.unit}` : ""} ${chalk4.dim(s.date)}`);
6594
6986
  }
6595
6987
  }
6988
+ const spendLabel = agent ? `${agent} API-equivalent spend:` : "Fleet API-equivalent spend:";
6596
6989
  console.log(`
6597
- ${chalk4.dim("Fleet API-equivalent spend:")} ${fmt2(summary.total_usd)}`);
6990
+ ${chalk4.dim(spendLabel)} ${fmt2(summary.total_usd)}`);
6598
6991
  console.log();
6599
6992
  });
6600
6993
  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 +7069,19 @@ function registerExtendedCommands(program) {
6676
7069
  }
6677
7070
  checks.push({ ok: Boolean(process.env["CURSOR_SESSION_TOKEN"]), msg: `cursor token: ${process.env["CURSOR_SESSION_TOKEN"] ? "set" : "missing CURSOR_SESSION_TOKEN"}` });
6678
7071
  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}` });
7072
+ const zeroCostBuckets = queryZeroCostTokenizedModels(db, 5);
7073
+ const zeroCost = db.prepare(`
7074
+ SELECT COUNT(*) as c
7075
+ FROM requests
7076
+ WHERE cost_usd = 0
7077
+ AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)
7078
+ `).get();
7079
+ const zeroCostSuffix = zeroCostBuckets.length > 0 ? `; top buckets: ${zeroCostBuckets.map((row) => {
7080
+ const pricing = getPricingFromDb(db, row.model);
7081
+ const status = pricing ? "pricing configured" : "missing pricing";
7082
+ return `${row.agent}/${row.model} ${row.requests} req ${fmtTokens(row.total_tokens)} tok (${status})`;
7083
+ }).join("; ")}` : "";
7084
+ checks.push({ ok: zeroCost.c === 0, msg: `zero-cost requests with tokens: ${zeroCost.c}${zeroCostSuffix}` });
6681
7085
  const estimated = querySummary(db, "month", undefined, true);
6682
7086
  const actual = queryBillingSummary(db, "month");
6683
7087
  const drift = actual.total_usd > 0 ? Math.abs(estimated.total_usd - actual.total_usd) / actual.total_usd : 0;
@@ -7824,6 +8228,187 @@ function mergePeerDatabase(target, sourcePath, opts = {}) {
7824
8228
  };
7825
8229
  }
7826
8230
 
8231
+ // src/lib/fleet-sync.ts
8232
+ init_database();
8233
+ import { spawnSync } from "child_process";
8234
+ import { existsSync as existsSync13, mkdirSync as mkdirSync3, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
8235
+ import { join as join12, resolve } from "path";
8236
+ import { homedir as homedir9 } from "os";
8237
+ import { Database as Database2 } from "bun:sqlite";
8238
+ var DEFAULT_TIMEOUT_MS = 120000;
8239
+ var DEFAULT_MACHINES = ["spark02", "apple03"];
8240
+ function runEconomyFleetSync(options = {}) {
8241
+ const generatedAt = (options.now ?? new Date).toISOString();
8242
+ const timeoutMs = normalizePositiveInteger(options.timeoutMs, DEFAULT_TIMEOUT_MS);
8243
+ const runner = options.runner ?? spawnCommand;
8244
+ const dryRun = Boolean(options.dryRun);
8245
+ const machines = ((options.machines?.length) ? options.machines : DEFAULT_MACHINES).filter(Boolean);
8246
+ const cacheDir = resolve(options.cacheDir ?? join12(homedir9(), ".hasna", "economy", "fleet-sync-cache"));
8247
+ const remoteSnapshotDir = options.remoteSnapshotDir ?? "$HOME/.hasna/economy/fleet-sync-snapshots";
8248
+ const localSync = options.localSync !== false;
8249
+ const remoteSync = options.remoteSync !== false;
8250
+ const local = {
8251
+ status: dryRun ? "dry_run" : localSync ? "skipped" : "skipped",
8252
+ db_path: getDbPath()
8253
+ };
8254
+ if (localSync) {
8255
+ if (dryRun) {
8256
+ local.status = "dry_run";
8257
+ } else {
8258
+ local.sync = runner("economy", ["sync"], { timeoutMs });
8259
+ local.status = local.sync.code === 0 ? "synced" : "failed";
8260
+ }
8261
+ }
8262
+ const remote = machines.map((machine) => syncMachine(machine, {
8263
+ generatedAt,
8264
+ timeoutMs,
8265
+ runner,
8266
+ dryRun,
8267
+ remoteSync,
8268
+ cacheDir,
8269
+ remoteSnapshotDir
8270
+ }));
8271
+ return {
8272
+ schema: "open-economy.fleet-sync.v1",
8273
+ generated_at: generatedAt,
8274
+ dry_run: dryRun,
8275
+ local,
8276
+ remote,
8277
+ summary: {
8278
+ machines: remote.length,
8279
+ merged: remote.filter((row) => row.status === "merged").length,
8280
+ failed: remote.filter((row) => row.status === "failed").length,
8281
+ dry_run: remote.filter((row) => row.status === "dry_run").length + (dryRun ? 1 : 0)
8282
+ }
8283
+ };
8284
+ }
8285
+ function syncMachine(machine, context) {
8286
+ const safeMachine = machine.replace(/[^A-Za-z0-9_.-]/g, "_");
8287
+ const stamp = context.generatedAt.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
8288
+ const snapshotName = `${safeMachine}-${stamp}.economy.db`;
8289
+ const remoteSnapshotPath = `${context.remoteSnapshotDir.replace(/\/$/, "")}/${snapshotName}`;
8290
+ const tmpLocal = join12(context.cacheDir, `${snapshotName}.tmp`);
8291
+ const verifiedLocal = join12(context.cacheDir, snapshotName);
8292
+ if (context.dryRun) {
8293
+ return { machine, status: "dry_run", remote_snapshot_path: remoteSnapshotPath };
8294
+ }
8295
+ mkdirSync3(context.cacheDir, { recursive: true });
8296
+ rmSync(tmpLocal, { force: true });
8297
+ const result = {
8298
+ machine,
8299
+ status: "failed",
8300
+ remote_snapshot_path: remoteSnapshotPath
8301
+ };
8302
+ if (context.remoteSync) {
8303
+ result.remote_sync = context.runner("ssh", [machine, "economy sync"], { timeoutMs: context.timeoutMs });
8304
+ if (result.remote_sync.code !== 0) {
8305
+ result.error = "remote economy sync failed";
8306
+ return result;
8307
+ }
8308
+ }
8309
+ result.remote_snapshot = context.runner("ssh", [machine, remoteSnapshotCommand(context.remoteSnapshotDir, snapshotName)], { timeoutMs: context.timeoutMs });
8310
+ if (result.remote_snapshot.code !== 0) {
8311
+ result.error = "remote snapshot failed";
8312
+ return result;
8313
+ }
8314
+ const emittedPath = result.remote_snapshot.stdout.trim().split(/\r?\n/).at(-1)?.trim() || remoteSnapshotPath;
8315
+ if (!isSafeRemoteSnapshotPath(emittedPath, context.remoteSnapshotDir)) {
8316
+ result.error = `remote snapshot path refused: ${emittedPath}`;
8317
+ return result;
8318
+ }
8319
+ result.remote_snapshot_path = emittedPath;
8320
+ result.pull = context.runner("scp", [`${machine}:${emittedPath}`, tmpLocal], { timeoutMs: context.timeoutMs });
8321
+ context.runner("ssh", [machine, `rm -f ${shellQuote(emittedPath)}`], { timeoutMs: context.timeoutMs });
8322
+ if (result.pull.code !== 0) {
8323
+ result.error = "snapshot pull failed; no cached DB merged";
8324
+ rmSync(tmpLocal, { force: true });
8325
+ return result;
8326
+ }
8327
+ const integrity = verifySqliteIntegrity(tmpLocal);
8328
+ result.local_integrity = integrity;
8329
+ if (!integrity.ok) {
8330
+ result.error = "pulled snapshot failed local integrity check";
8331
+ rmSync(tmpLocal, { force: true });
8332
+ return result;
8333
+ }
8334
+ renameSync(tmpLocal, verifiedLocal);
8335
+ const db = openDatabase();
8336
+ result.merge = mergePeerDatabase(db, verifiedLocal, { sourceMachine: machine });
8337
+ result.status = "merged";
8338
+ return result;
8339
+ }
8340
+ function remoteSnapshotCommand(remoteDir, filename) {
8341
+ const dirExpr = remoteDir.startsWith("$HOME/") ? `"${remoteDir.replace(/"/g, "\\\"")}"` : shellQuote(remoteDir);
8342
+ const safeFilename = filename.replace(/[^A-Za-z0-9_.-]/g, "_");
8343
+ return [
8344
+ "set -euo pipefail",
8345
+ 'db="${HASNA_ECONOMY_DB_PATH:-${ECONOMY_DB:-$HOME/.hasna/economy/economy.db}}"',
8346
+ `snapshot_dir=${dirExpr}`,
8347
+ `out="$snapshot_dir/${safeFilename}"`,
8348
+ 'test -f "$db"',
8349
+ 'mkdir -p "$snapshot_dir"',
8350
+ 'rm -f "$out"',
8351
+ `sqlite3 "$db" "VACUUM INTO '$out'"`,
8352
+ 'test "$(sqlite3 "$out" "PRAGMA integrity_check;")" = "ok"',
8353
+ 'printf "%s\\n" "$out"'
8354
+ ].join("; ");
8355
+ }
8356
+ function isSafeRemoteSnapshotPath(path, remoteSnapshotDir = "$HOME/.hasna/economy/fleet-sync-snapshots") {
8357
+ if (!path || path.includes("\x00") || path.includes(`
8358
+ `) || path.includes("\r"))
8359
+ return false;
8360
+ if (!path.endsWith(".db"))
8361
+ return false;
8362
+ if (path.includes(".."))
8363
+ return false;
8364
+ const prefix = remoteSnapshotDir.replace(/\/$/, "");
8365
+ return path.startsWith(`${prefix}/`) || path.startsWith("$HOME/.hasna/economy/fleet-sync-snapshots/") || path.includes("/.hasna/economy/fleet-sync-snapshots/");
8366
+ }
8367
+ function verifySqliteIntegrity(path) {
8368
+ if (!existsSync13(path))
8369
+ return { ok: false, detail: "file does not exist" };
8370
+ let db;
8371
+ try {
8372
+ db = new Database2(path, { readonly: true });
8373
+ const row = db.query("PRAGMA integrity_check").get();
8374
+ const detail = Object.values(row ?? {})[0] ?? "";
8375
+ return { ok: detail.toLowerCase() === "ok", detail: detail || "no integrity result" };
8376
+ } catch (error) {
8377
+ return { ok: false, detail: error instanceof Error ? error.message : String(error) };
8378
+ } finally {
8379
+ db?.close();
8380
+ }
8381
+ }
8382
+ function writeFleetSyncReport(reportDir, result) {
8383
+ mkdirSync3(reportDir, { recursive: true });
8384
+ const path = join12(resolve(reportDir), `${result.generated_at.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z")}.json`);
8385
+ writeFileSync2(path, `${JSON.stringify(result, null, 2)}
8386
+ `);
8387
+ return path;
8388
+ }
8389
+ function spawnCommand(command, args, options) {
8390
+ const result = spawnSync(command, args, {
8391
+ encoding: "utf8",
8392
+ timeout: options.timeoutMs,
8393
+ maxBuffer: 1024 * 1024,
8394
+ stdio: ["ignore", "pipe", "pipe"]
8395
+ });
8396
+ return {
8397
+ code: result.status,
8398
+ stdout: result.stdout ?? "",
8399
+ stderr: result.stderr ?? "",
8400
+ ...result.error ? { error: result.error.message } : {}
8401
+ };
8402
+ }
8403
+ function shellQuote(value) {
8404
+ return `'${value.replace(/'/g, "'\\''")}'`;
8405
+ }
8406
+ function normalizePositiveInteger(value, fallback) {
8407
+ if (value == null || !Number.isFinite(value) || value < 1)
8408
+ return fallback;
8409
+ return Math.floor(value);
8410
+ }
8411
+
7827
8412
  // src/cli/index.ts
7828
8413
  init_database();
7829
8414
  init_database();
@@ -7863,7 +8448,7 @@ function fmt4(usd) {
7863
8448
  }
7864
8449
  return chalk7.green(formatted);
7865
8450
  }
7866
- function fmtTokens(n) {
8451
+ function fmtTokens2(n) {
7867
8452
  if (n >= 1e9)
7868
8453
  return `${(n / 1e9).toFixed(1)}B`;
7869
8454
  if (n >= 1e6)
@@ -7971,7 +8556,7 @@ function printAccountBreakdown(rows) {
7971
8556
  chalk7.dim(r.account_source || "unknown"),
7972
8557
  String(r.sessions),
7973
8558
  String(r.requests),
7974
- chalk7.cyan(fmtTokens(r.total_tokens)),
8559
+ chalk7.cyan(fmtTokens2(r.total_tokens)),
7975
8560
  fmt4(r.api_equivalent_usd),
7976
8561
  fmt4(r.billable_usd),
7977
8562
  fmt4(r.subscription_included_usd)
@@ -7998,7 +8583,7 @@ function printSummary(label, period) {
7998
8583
  ["Total cost", fmt4(s.total_usd)],
7999
8584
  ["Sessions", chalk7.yellow(fmtCount(s.sessions))],
8000
8585
  ["Requests", chalk7.yellow(fmtCount(s.requests))],
8001
- ["Tokens", chalk7.yellow(fmtTokens(s.tokens))]
8586
+ ["Tokens", chalk7.yellow(fmtTokens2(s.tokens))]
8002
8587
  ]);
8003
8588
  console.log();
8004
8589
  }
@@ -8018,9 +8603,9 @@ program.action(async () => {
8018
8603
  console.log(chalk7.bold.cyan(" Economy"));
8019
8604
  console.log();
8020
8605
  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)]
8606
+ ["Today", fmt4(t.total_usd), fmtCount(t.sessions), fmtCount(t.requests), fmtTokens2(t.tokens)],
8607
+ ["This Week", fmt4(w.total_usd), fmtCount(w.sessions), fmtCount(w.requests), fmtTokens2(w.tokens)],
8608
+ ["This Month", fmt4(m.total_usd), fmtCount(m.sessions), fmtCount(m.requests), fmtTokens2(m.tokens)]
8024
8609
  ]);
8025
8610
  if (dailyValues.length > 0) {
8026
8611
  console.log(`
@@ -8076,7 +8661,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
8076
8661
  console.log(chalk7.cyan(`\u2192 Backfilled machine_id='${machine}': ${reqCount.changes} requests, ${sessCount.changes} sessions`));
8077
8662
  }
8078
8663
  if (opts.recalculate) {
8079
- const { computeCostFromDb: computeCostFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
8664
+ const { computeCostFromDb: computeCostFromDb2, getPricingFromDb: getPricingFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
8080
8665
  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
8666
  let fixed = 0;
8082
8667
  for (const r of zeroRows) {
@@ -8098,6 +8683,16 @@ program.command("sync").description("Ingest cost data from all coding agents").o
8098
8683
  }
8099
8684
  }
8100
8685
  console.log(chalk7.cyan(`\u2192 Recalculated: ${fixed}/${zeroRows.length} zero-cost requests now have pricing`));
8686
+ const zeroCostBuckets = queryZeroCostTokenizedModels(db, 8);
8687
+ if (zeroCostBuckets.length > 0) {
8688
+ console.log(chalk7.yellow("\u2192 Zero-cost token buckets remain:"));
8689
+ for (const row of zeroCostBuckets) {
8690
+ const pricing = getPricingFromDb2(db, row.model);
8691
+ 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";
8692
+ console.log(chalk7.yellow(` ${row.agent}/${row.model}: ${row.requests} requests, ${fmtTokens2(row.total_tokens)} tokens (${status})`));
8693
+ }
8694
+ console.log(chalk7.dim(" Add pricing with `economy pricing set <model> --input ... --output ...`, then rerun `economy sync --recalculate`."));
8695
+ }
8101
8696
  }
8102
8697
  try {
8103
8698
  const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
@@ -8140,7 +8735,7 @@ program.command("sessions").description("List coding sessions with costs").optio
8140
8735
  const f = opts.format ?? "table";
8141
8736
  if (f === "compact") {
8142
8737
  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"}
8738
+ process.stdout.write(`${s.id.slice(0, 8)} ${s.agent} ${fmt4(s.total_cost_usd)} ${fmtTokens2(s.total_tokens)} ${s.project_name || "\u2014"}
8144
8739
  `);
8145
8740
  return;
8146
8741
  }
@@ -8160,7 +8755,7 @@ program.command("sessions").description("List coding sessions with costs").optio
8160
8755
  fmtAgent(s.agent),
8161
8756
  chalk7.white(s.project_name || chalk7.dim("unknown")),
8162
8757
  fmt4(s.total_cost_usd),
8163
- chalk7.cyan(fmtTokens(s.total_tokens)),
8758
+ chalk7.cyan(fmtTokens2(s.total_tokens)),
8164
8759
  fmtCount(s.request_count),
8165
8760
  chalk7.dim(s.started_at.substring(0, 16))
8166
8761
  ]));
@@ -8171,9 +8766,7 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
8171
8766
  const agent = parseOptionalCliAgent(opts.agent);
8172
8767
  const db = openDatabase();
8173
8768
  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);
8769
+ const sessions = queryTopSessions(db, count, agent, sinceDate);
8177
8770
  if (sessions.length === 0) {
8178
8771
  console.log(chalk7.yellow("No sessions found. Run `economy sync` first."));
8179
8772
  return;
@@ -8184,7 +8777,7 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
8184
8777
  chalk7.white(s.project_name || chalk7.dim("unknown")),
8185
8778
  fmtAgent(s.agent),
8186
8779
  fmt4(s.total_cost_usd),
8187
- chalk7.cyan(fmtTokens(s.total_tokens)),
8780
+ chalk7.cyan(fmtTokens2(s.total_tokens)),
8188
8781
  chalk7.dim(s.started_at.substring(0, 16))
8189
8782
  ]));
8190
8783
  console.log();
@@ -8208,7 +8801,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
8208
8801
  chalk7.white(r.project_name || chalk7.dim("unknown")),
8209
8802
  String(r.sessions),
8210
8803
  String(r.requests),
8211
- chalk7.cyan(fmtTokens(r.total_tokens)),
8804
+ chalk7.cyan(fmtTokens2(r.total_tokens)),
8212
8805
  fmt4(r.cost_usd)
8213
8806
  ]));
8214
8807
  } else if (opts.by === "agent") {
@@ -8230,7 +8823,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
8230
8823
  fmtAgent(r.agent),
8231
8824
  String(r.sessions),
8232
8825
  String(r.requests),
8233
- chalk7.cyan(fmtTokens(r.total_tokens)),
8826
+ chalk7.cyan(fmtTokens2(r.total_tokens)),
8234
8827
  fmt4(r.api_equivalent_usd),
8235
8828
  fmt4(r.billable_usd),
8236
8829
  fmt4(r.subscription_included_usd)
@@ -8352,7 +8945,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
8352
8945
  chalk7.white(r.model),
8353
8946
  fmtAgent(r.agent),
8354
8947
  String(r.requests),
8355
- chalk7.cyan(fmtTokens(r.total_tokens)),
8948
+ chalk7.cyan(fmtTokens2(r.total_tokens)),
8356
8949
  fmt4(r.cost_usd)
8357
8950
  ]));
8358
8951
  }
@@ -8514,7 +9107,7 @@ projectCmd.command("show <nameOrPath>").description("Detailed project breakdown
8514
9107
  printTable(["Metric", "Value"], [
8515
9108
  ["Total cost", fmt4(totalCost)],
8516
9109
  ["Sessions", fmtCount(sessions.length)],
8517
- ["Total tokens", fmtTokens(totalTokens)]
9110
+ ["Total tokens", fmtTokens2(totalTokens)]
8518
9111
  ]);
8519
9112
  if (dailyValues.length > 0) {
8520
9113
  console.log(`
@@ -8647,8 +9240,8 @@ program.command("dashboard").description("Open the web dashboard (auto-starts se
8647
9240
  if (!serverRunning) {
8648
9241
  console.log(chalk7.cyan(`\u2192 Starting economy server on port ${port}...`));
8649
9242
  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");
9243
+ const { resolve: resolve3, dirname: dirname4 } = await import("path");
9244
+ const serveScript = resolve3(dirname4(process.argv[1]), "..", "server", "index.js");
8652
9245
  const child = spawn(process.execPath, [serveScript], {
8653
9246
  detached: true,
8654
9247
  stdio: "ignore",
@@ -8719,7 +9312,7 @@ program.command("session <id>").description("Show detailed breakdown of a single
8719
9312
  ["Started", String(session["started_at"]).substring(0, 19)],
8720
9313
  ["Ended", session["ended_at"] ? String(session["ended_at"]).substring(0, 19) : "\u2014"],
8721
9314
  ["Total cost", fmt4(session["total_cost_usd"])],
8722
- ["Total tokens", fmtTokens(session["total_tokens"])],
9315
+ ["Total tokens", fmtTokens2(session["total_tokens"])],
8723
9316
  ["Requests", fmtCount(session["request_count"])]
8724
9317
  ]);
8725
9318
  if (requests.length > 0) {
@@ -8729,10 +9322,10 @@ program.command("session <id>").description("Show detailed breakdown of a single
8729
9322
  printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], requests.slice(0, 50).map((r) => [
8730
9323
  chalk7.dim(String(r["timestamp"]).substring(11, 19)),
8731
9324
  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"]),
9325
+ fmtTokens2(r["input_tokens"]),
9326
+ fmtTokens2(r["output_tokens"]),
9327
+ fmtTokens2(r["cache_read_tokens"]),
9328
+ fmtTokens2(r["cache_create_tokens"]),
8736
9329
  fmt4(r["cost_usd"])
8737
9330
  ]));
8738
9331
  if (requests.length > 50)
@@ -8763,6 +9356,31 @@ program.command("machines").description("List all machines that have synced data
8763
9356
  ${chalk7.dim("Current machine:")} ${chalk7.bold(current)}`);
8764
9357
  console.log();
8765
9358
  });
9359
+ 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) => {
9360
+ const result = runEconomyFleetSync({
9361
+ machines: opts.machine?.split(",").map((entry) => entry.trim()).filter(Boolean),
9362
+ localSync: opts.localSync !== false,
9363
+ remoteSync: opts.remoteSync !== false,
9364
+ cacheDir: opts.cacheDir,
9365
+ remoteSnapshotDir: opts.remoteSnapshotDir,
9366
+ timeoutMs: parsePositiveCliInteger(opts.timeoutMs, "--timeout-ms"),
9367
+ dryRun: opts.dryRun
9368
+ });
9369
+ const reportPath = opts.reportDir ? writeFleetSyncReport(opts.reportDir, result) : undefined;
9370
+ if (opts.json) {
9371
+ console.log(JSON.stringify({ ...result, ...reportPath ? { report_path: reportPath } : {} }, null, 2));
9372
+ return;
9373
+ }
9374
+ const status = result.summary.failed === 0 ? chalk7.green("ok") : chalk7.red("failed");
9375
+ console.log(`${status} machines=${result.summary.machines} merged=${result.summary.merged} failed=${result.summary.failed} dry_run=${result.summary.dry_run}`);
9376
+ if (reportPath)
9377
+ console.log(chalk7.dim(`report=${reportPath}`));
9378
+ for (const row of result.remote.filter((entry) => entry.status !== "merged").slice(0, 20)) {
9379
+ console.log(`${row.status === "failed" ? chalk7.red("failed") : chalk7.yellow(row.status)} ${row.machine} ${chalk7.dim(row.error ?? "")}`);
9380
+ }
9381
+ if (result.local.status === "failed" || result.summary.failed > 0)
9382
+ process.exitCode = 1;
9383
+ });
8766
9384
  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
9385
  const db = openDatabase();
8768
9386
  const result = mergePeerDatabase(db, sourceDb, { sourceMachine: opts.sourceMachine });
@@ -8802,8 +9420,8 @@ program.command("export").description("Export data as CSV").option("--type <type
8802
9420
  }
8803
9421
  }
8804
9422
  if (opts.output) {
8805
- const { writeFileSync: writeFileSync4 } = await import("fs");
8806
- writeFileSync4(opts.output, csv);
9423
+ const { writeFileSync: writeFileSync5 } = await import("fs");
9424
+ writeFileSync5(opts.output, csv);
8807
9425
  console.log(chalk7.green(`\u2713 Exported to ${opts.output}`));
8808
9426
  } else {
8809
9427
  process.stdout.write(csv);
@@ -8875,7 +9493,7 @@ program.command("compare <period1> <period2>").description("Compare two periods
8875
9493
  ["Cost", fmt4(a.cost), fmt4(b.cost), delta(a.cost, b.cost)],
8876
9494
  ["Sessions", fmtCount(a.sessions), fmtCount(b.sessions), delta(a.sessions, b.sessions)],
8877
9495
  ["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)]
9496
+ ["Tokens", fmtTokens2(a.tokens), fmtTokens2(b.tokens), delta(a.tokens, b.tokens)]
8879
9497
  ]);
8880
9498
  console.log();
8881
9499
  });