@hasna/economy 0.2.21 → 0.2.22

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.
@@ -583,7 +583,12 @@ function initSchema(db) {
583
583
  duration_ms INTEGER DEFAULT 0,
584
584
  timestamp TEXT NOT NULL,
585
585
  source_request_id TEXT,
586
- machine_id TEXT DEFAULT ''
586
+ machine_id TEXT DEFAULT '',
587
+ account_key TEXT DEFAULT '',
588
+ account_tool TEXT DEFAULT '',
589
+ account_name TEXT DEFAULT '',
590
+ account_email TEXT DEFAULT '',
591
+ account_source TEXT DEFAULT ''
587
592
  );
588
593
 
589
594
  CREATE TABLE IF NOT EXISTS sessions (
@@ -596,7 +601,12 @@ function initSchema(db) {
596
601
  total_cost_usd REAL DEFAULT 0,
597
602
  total_tokens INTEGER DEFAULT 0,
598
603
  request_count INTEGER DEFAULT 0,
599
- machine_id TEXT DEFAULT ''
604
+ machine_id TEXT DEFAULT '',
605
+ account_key TEXT DEFAULT '',
606
+ account_tool TEXT DEFAULT '',
607
+ account_name TEXT DEFAULT '',
608
+ account_email TEXT DEFAULT '',
609
+ account_source TEXT DEFAULT ''
600
610
  );
601
611
 
602
612
  CREATE TABLE IF NOT EXISTS projects (
@@ -751,6 +761,11 @@ function initSchema(db) {
751
761
  if (!cols.some((c) => c.name === "synced_at")) {
752
762
  db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
753
763
  }
764
+ for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
765
+ if (!cols.some((c) => c.name === column)) {
766
+ db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
767
+ }
768
+ }
754
769
  const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
755
770
  if (!sessionCols.some((c) => c.name === "attribution_tag")) {
756
771
  db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
@@ -762,6 +777,11 @@ function initSchema(db) {
762
777
  if (!sessionCols.some((c) => c.name === "synced_at")) {
763
778
  db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
764
779
  }
780
+ for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
781
+ if (!sessionCols.some((c) => c.name === column)) {
782
+ db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
783
+ }
784
+ }
765
785
  const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
766
786
  if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
767
787
  db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
@@ -772,6 +792,8 @@ function initSchema(db) {
772
792
  db.exec(`
773
793
  CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
774
794
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
795
+ CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
796
+ CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
775
797
  `);
776
798
  }
777
799
  function periodWhere(period) {
@@ -806,6 +828,22 @@ function sessionPeriodWhere(period) {
806
828
  return "1=1";
807
829
  }
808
830
  }
831
+ function requestPeriodWhere(period) {
832
+ switch (period) {
833
+ case "today":
834
+ return `DATE(timestamp) = DATE('now')`;
835
+ case "yesterday":
836
+ return `DATE(timestamp) = DATE('now', '-1 day')`;
837
+ case "week":
838
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
839
+ case "month":
840
+ return `timestamp >= DATE('now', 'start of month')`;
841
+ case "year":
842
+ return `timestamp >= DATE('now', 'start of year')`;
843
+ case "all":
844
+ return "1=1";
845
+ }
846
+ }
809
847
  function upsertRequest(db, req) {
810
848
  const now = req.updated_at ?? new Date().toISOString();
811
849
  db.prepare(`
@@ -813,18 +851,20 @@ function upsertRequest(db, req) {
813
851
  (id, agent, session_id, model, input_tokens, output_tokens,
814
852
  cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
815
853
  cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
816
- source_request_id, machine_id, attribution_tag, updated_at, synced_at)
817
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
818
- `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", now, req.synced_at ?? "");
854
+ source_request_id, machine_id, attribution_tag, account_key, account_tool,
855
+ account_name, account_email, account_source, updated_at, synced_at)
856
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
857
+ `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
819
858
  }
820
859
  function upsertSession(db, session) {
821
860
  const now = session.updated_at ?? new Date().toISOString();
822
861
  db.prepare(`
823
862
  INSERT OR REPLACE INTO sessions
824
863
  (id, agent, project_path, project_name, started_at, ended_at,
825
- total_cost_usd, total_tokens, request_count, machine_id, attribution_tag, updated_at, synced_at)
826
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
827
- `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", now, session.synced_at ?? "");
864
+ total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
865
+ account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
866
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
867
+ `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
828
868
  }
829
869
  function rollupSession(db, sessionId) {
830
870
  db.prepare(`
@@ -835,9 +875,24 @@ function rollupSession(db, sessionId) {
835
875
  ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
836
876
  started_at = CASE WHEN started_at = '' OR started_at IS NULL
837
877
  THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
838
- ELSE started_at END
878
+ ELSE started_at END,
879
+ account_key = CASE WHEN account_key = '' OR account_key IS NULL
880
+ THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
881
+ ELSE account_key END,
882
+ account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
883
+ THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
884
+ ELSE account_tool END,
885
+ account_name = CASE WHEN account_name = '' OR account_name IS NULL
886
+ THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
887
+ ELSE account_name END,
888
+ account_email = CASE WHEN account_email = '' OR account_email IS NULL
889
+ THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
890
+ ELSE account_email END,
891
+ account_source = CASE WHEN account_source = '' OR account_source IS NULL
892
+ THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
893
+ ELSE account_source END
839
894
  WHERE id = ?
840
- `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
895
+ `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
841
896
  }
842
897
  function querySessions(db, filter = {}) {
843
898
  const conditions = [];
@@ -850,6 +905,11 @@ function querySessions(db, filter = {}) {
850
905
  conditions.push("project_path LIKE ?");
851
906
  params.push(`%${filter.project}%`);
852
907
  }
908
+ if (filter.account) {
909
+ const q = `%${filter.account}%`;
910
+ conditions.push("(account_key LIKE ? OR account_name LIKE ? OR account_email LIKE ?)");
911
+ params.push(q, q, q);
912
+ }
853
913
  if (filter.since) {
854
914
  conditions.push("started_at >= ?");
855
915
  params.push(filter.since);
@@ -914,6 +974,70 @@ function queryModelBreakdown(db) {
914
974
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
915
975
  `).all();
916
976
  }
977
+ function queryAgentBreakdown(db, period = "all") {
978
+ const requestWhere = requestPeriodWhere(period);
979
+ const groups = new Map;
980
+ const requestRows = db.prepare(`
981
+ SELECT agent,
982
+ COUNT(DISTINCT session_id) as sessions,
983
+ COUNT(*) as requests,
984
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
985
+ COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
986
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
987
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
988
+ COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
989
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
990
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
991
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
992
+ MAX(timestamp) as last_active
993
+ FROM requests
994
+ WHERE ${requestWhere}
995
+ GROUP BY agent
996
+ ORDER BY api_equivalent_usd DESC
997
+ `).all();
998
+ for (const row of requestRows) {
999
+ groups.set(row.agent, row);
1000
+ }
1001
+ const sessionWhere = sessionPeriodWhere(period);
1002
+ const sessionOnlyRows = db.prepare(`
1003
+ SELECT agent,
1004
+ COUNT(*) as sessions,
1005
+ COALESCE(SUM(request_count), 0) as requests,
1006
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
1007
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1008
+ MAX(started_at) as last_active
1009
+ FROM sessions
1010
+ WHERE ${sessionWhere}
1011
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1012
+ GROUP BY agent
1013
+ `).all();
1014
+ for (const row of sessionOnlyRows) {
1015
+ const existing = groups.get(row.agent) ?? {
1016
+ agent: row.agent,
1017
+ sessions: 0,
1018
+ requests: 0,
1019
+ total_tokens: 0,
1020
+ api_equivalent_usd: 0,
1021
+ billable_usd: 0,
1022
+ metered_api_usd: 0,
1023
+ subscription_included_usd: 0,
1024
+ estimated_usd: 0,
1025
+ unknown_usd: 0,
1026
+ cost_usd: 0,
1027
+ last_active: ""
1028
+ };
1029
+ existing.sessions += row.sessions;
1030
+ existing.requests += row.requests;
1031
+ existing.total_tokens += row.total_tokens;
1032
+ existing.api_equivalent_usd += row.cost_usd;
1033
+ existing.estimated_usd += row.cost_usd;
1034
+ existing.cost_usd += row.cost_usd;
1035
+ if (!existing.last_active || row.last_active > existing.last_active)
1036
+ existing.last_active = row.last_active;
1037
+ groups.set(row.agent, existing);
1038
+ }
1039
+ return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
1040
+ }
917
1041
  function labelForPath(projectPath, projectName) {
918
1042
  if (projectName && projectName.trim() !== "")
919
1043
  return projectName;
@@ -932,11 +1056,13 @@ function labelForPath(projectPath, projectName) {
932
1056
  }
933
1057
  return segments[segments.length - 1] ?? projectPath;
934
1058
  }
935
- function queryProjectBreakdown(db) {
1059
+ function queryProjectBreakdown(db, period = "all") {
1060
+ const where = sessionPeriodWhere(period);
936
1061
  const sessions = db.prepare(`
937
1062
  SELECT id, project_path, project_name, total_cost_usd, started_at
938
1063
  FROM sessions
939
- WHERE project_path != '' OR project_name != ''
1064
+ WHERE ${where}
1065
+ AND (project_path != '' OR project_name != '')
940
1066
  `).all();
941
1067
  const groups = new Map;
942
1068
  for (const s of sessions) {
@@ -975,6 +1101,87 @@ function queryProjectBreakdown(db) {
975
1101
  result.sort((a, b) => b.cost_usd - a.cost_usd);
976
1102
  return result;
977
1103
  }
1104
+ function queryAccountBreakdown(db, period = "all") {
1105
+ const sWhere = sessionPeriodWhere(period);
1106
+ const sessions = db.prepare(`
1107
+ SELECT id, account_key, account_tool, account_name, account_email, account_source,
1108
+ total_cost_usd, total_tokens, request_count, started_at
1109
+ FROM sessions
1110
+ WHERE ${sWhere}
1111
+ AND (account_key != '' OR account_tool != '' OR account_name != '' OR account_email != '')
1112
+ `).all();
1113
+ const groups = new Map;
1114
+ for (const session of sessions) {
1115
+ const key = session.account_key || `${session.account_tool}:${session.account_name}`;
1116
+ if (!key || key === ":")
1117
+ continue;
1118
+ const group = groups.get(key) ?? {
1119
+ sessionIds: [],
1120
+ account_tool: session.account_tool,
1121
+ account_name: session.account_name,
1122
+ account_email: session.account_email || null,
1123
+ account_source: session.account_source || "unknown",
1124
+ totalCost: 0,
1125
+ totalTokens: 0,
1126
+ requests: 0,
1127
+ lastActive: ""
1128
+ };
1129
+ group.sessionIds.push(session.id);
1130
+ group.totalCost += session.total_cost_usd || 0;
1131
+ group.totalTokens += session.total_tokens || 0;
1132
+ group.requests += session.request_count || 0;
1133
+ if (!group.lastActive || session.started_at > group.lastActive)
1134
+ group.lastActive = session.started_at;
1135
+ groups.set(key, group);
1136
+ }
1137
+ const result = [];
1138
+ for (const [key, group] of groups.entries()) {
1139
+ const placeholders = group.sessionIds.map(() => "?").join(",");
1140
+ const reqStats = placeholders ? db.prepare(`
1141
+ SELECT
1142
+ COUNT(*) as requests,
1143
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
1144
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1145
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1146
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1147
+ COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1148
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd
1149
+ FROM requests WHERE session_id IN (${placeholders})
1150
+ `).get(...group.sessionIds) : {
1151
+ requests: 0,
1152
+ cost_usd: 0,
1153
+ total_tokens: 0,
1154
+ metered_api_usd: 0,
1155
+ subscription_included_usd: 0,
1156
+ estimated_usd: 0,
1157
+ unknown_usd: 0
1158
+ };
1159
+ const hasRequestCosts = reqStats.requests > 0;
1160
+ const apiEquivalentUsd = hasRequestCosts ? reqStats.cost_usd : group.totalCost;
1161
+ const estimatedUsd = hasRequestCosts ? reqStats.estimated_usd : group.totalCost;
1162
+ const billableUsd = reqStats.metered_api_usd;
1163
+ result.push({
1164
+ account_key: key,
1165
+ account_tool: group.account_tool,
1166
+ account_name: group.account_name,
1167
+ account_email: group.account_email,
1168
+ account_source: group.account_source,
1169
+ sessions: group.sessionIds.length,
1170
+ requests: reqStats.requests || group.requests,
1171
+ total_tokens: reqStats.total_tokens || group.totalTokens,
1172
+ api_equivalent_usd: apiEquivalentUsd,
1173
+ billable_usd: billableUsd,
1174
+ metered_api_usd: reqStats.metered_api_usd,
1175
+ subscription_included_usd: reqStats.subscription_included_usd,
1176
+ estimated_usd: estimatedUsd,
1177
+ unknown_usd: reqStats.unknown_usd,
1178
+ cost_usd: apiEquivalentUsd,
1179
+ last_active: group.lastActive
1180
+ });
1181
+ }
1182
+ result.sort((a, b) => b.cost_usd - a.cost_usd);
1183
+ return result;
1184
+ }
978
1185
  function queryDailyBreakdown(db, days = 30) {
979
1186
  return db.prepare(`
980
1187
  SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
@@ -1228,7 +1435,12 @@ var init_pg_migrations = __esm(() => {
1228
1435
  duration_ms INTEGER DEFAULT 0,
1229
1436
  timestamp TEXT NOT NULL,
1230
1437
  source_request_id TEXT,
1231
- machine_id TEXT DEFAULT ''
1438
+ machine_id TEXT DEFAULT '',
1439
+ account_key TEXT DEFAULT '',
1440
+ account_tool TEXT DEFAULT '',
1441
+ account_name TEXT DEFAULT '',
1442
+ account_email TEXT DEFAULT '',
1443
+ account_source TEXT DEFAULT ''
1232
1444
  )`,
1233
1445
  `CREATE TABLE IF NOT EXISTS sessions (
1234
1446
  id TEXT PRIMARY KEY,
@@ -1240,7 +1452,12 @@ var init_pg_migrations = __esm(() => {
1240
1452
  total_cost_usd REAL DEFAULT 0,
1241
1453
  total_tokens INTEGER DEFAULT 0,
1242
1454
  request_count INTEGER DEFAULT 0,
1243
- machine_id TEXT DEFAULT ''
1455
+ machine_id TEXT DEFAULT '',
1456
+ account_key TEXT DEFAULT '',
1457
+ account_tool TEXT DEFAULT '',
1458
+ account_name TEXT DEFAULT '',
1459
+ account_email TEXT DEFAULT '',
1460
+ account_source TEXT DEFAULT ''
1244
1461
  )`,
1245
1462
  `CREATE TABLE IF NOT EXISTS projects (
1246
1463
  id TEXT PRIMARY KEY,
@@ -1361,13 +1578,25 @@ var init_pg_migrations = __esm(() => {
1361
1578
  )`,
1362
1579
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
1363
1580
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1581
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1582
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1583
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1584
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1585
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1364
1586
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1365
1587
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1366
1588
  `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1589
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1590
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1591
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1592
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1593
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1367
1594
  `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1368
1595
  `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1369
1596
  `CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
1370
- `CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`
1597
+ `CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
1598
+ `CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
1599
+ `CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`
1371
1600
  ];
1372
1601
  });
1373
1602
 
@@ -1378,7 +1607,7 @@ __export(exports_billing, {
1378
1607
  syncGeminiBilling: () => syncGeminiBilling,
1379
1608
  syncAnthropicBilling: () => syncAnthropicBilling
1380
1609
  });
1381
- import { readFileSync as readFileSync10 } from "fs";
1610
+ import { readFileSync as readFileSync9 } from "fs";
1382
1611
  function getAnthropicAdminKey() {
1383
1612
  return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
1384
1613
  }
@@ -1572,7 +1801,7 @@ async function syncGeminiBilling(db, opts = {}) {
1572
1801
  const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
1573
1802
  const fromDateStr = toISODate(start);
1574
1803
  const toDateStr = toISODate(end);
1575
- const rows = parseBillingRows(readFileSync10(exportPath, "utf-8"));
1804
+ const rows = parseBillingRows(readFileSync9(exportPath, "utf-8"));
1576
1805
  clearBillingRange(db, "gemini", fromDateStr, toDateStr);
1577
1806
  const updatedAt = new Date().toISOString();
1578
1807
  let totalUsd = 0;
@@ -1634,7 +1863,7 @@ var init_open_projects = __esm(() => {
1634
1863
  });
1635
1864
 
1636
1865
  // src/lib/config.ts
1637
- import { existsSync as existsSync10, readFileSync as readFileSync11, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
1866
+ import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
1638
1867
  import { dirname, join as join9 } from "path";
1639
1868
  function getConfigPath() {
1640
1869
  return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join9(getDataDir(), "config.json");
@@ -1643,7 +1872,7 @@ function loadConfig() {
1643
1872
  try {
1644
1873
  const configPath = getConfigPath();
1645
1874
  if (existsSync10(configPath)) {
1646
- const raw = readFileSync11(configPath, "utf-8");
1875
+ const raw = readFileSync10(configPath, "utf-8");
1647
1876
  return { ...DEFAULTS, ...JSON.parse(raw) };
1648
1877
  }
1649
1878
  } catch {}
@@ -1809,7 +2038,6 @@ function periodWhere2(period, column) {
1809
2038
  }
1810
2039
  function prorateMonthlyFee(monthlyFee, period) {
1811
2040
  const now = new Date;
1812
- const dayOfMonth = now.getDate();
1813
2041
  const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
1814
2042
  switch (period) {
1815
2043
  case "today":
@@ -1891,6 +2119,131 @@ function defaultCostBasisForAgent(agent) {
1891
2119
  return "estimated";
1892
2120
  }
1893
2121
 
2122
+ // src/lib/accounts.ts
2123
+ var AGENT_ACCOUNT_TOOLS = {
2124
+ claude: ["claude"],
2125
+ takumi: ["takumi", "claude"],
2126
+ codex: ["codex"],
2127
+ gemini: ["gemini"],
2128
+ opencode: ["opencode"],
2129
+ cursor: ["cursor"],
2130
+ pi: ["pi"],
2131
+ hermes: ["hermes"]
2132
+ };
2133
+ function accountKey(tool, name) {
2134
+ return `${tool}:${name}`;
2135
+ }
2136
+ function normalizeDir(value) {
2137
+ return value.replace(/\/+$/, "");
2138
+ }
2139
+ function fromProfile(profile, source) {
2140
+ return {
2141
+ account_key: accountKey(profile.tool, profile.name),
2142
+ account_tool: profile.tool,
2143
+ account_name: profile.name,
2144
+ ...profile.email ? { account_email: profile.email } : {},
2145
+ account_source: source
2146
+ };
2147
+ }
2148
+ function fromOverride(raw, agent) {
2149
+ const value = raw.trim();
2150
+ if (!value)
2151
+ return null;
2152
+ const candidateTool = AGENT_ACCOUNT_TOOLS[agent][0] ?? agent;
2153
+ const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
2154
+ if (!tool || !name)
2155
+ return null;
2156
+ return {
2157
+ account_key: accountKey(tool, name),
2158
+ account_tool: tool,
2159
+ account_name: name,
2160
+ account_source: "override"
2161
+ };
2162
+ }
2163
+ function envOverride(agent, env) {
2164
+ const agentPrefix = agent.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2165
+ const raw = env[`ECONOMY_${agentPrefix}_ACCOUNT_KEY`] ?? env[`ECONOMY_${agentPrefix}_ACCOUNT`] ?? env["ECONOMY_ACCOUNT_KEY"] ?? env["ECONOMY_ACCOUNT"];
2166
+ if (raw)
2167
+ return fromOverride(raw, agent);
2168
+ const tool = env[`ECONOMY_${agentPrefix}_ACCOUNT_TOOL`] ?? env["ECONOMY_ACCOUNT_TOOL"];
2169
+ const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
2170
+ if (!tool || !name)
2171
+ return null;
2172
+ return {
2173
+ account_key: accountKey(tool, name),
2174
+ account_tool: tool,
2175
+ account_name: name,
2176
+ account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
2177
+ account_source: "override"
2178
+ };
2179
+ }
2180
+ function knownToolIds(api) {
2181
+ try {
2182
+ return new Set(api.listTools().map((tool) => tool.id));
2183
+ } catch {
2184
+ return new Set;
2185
+ }
2186
+ }
2187
+ function profileForEnvDir(api, tool, env) {
2188
+ const configuredDir = env[tool.envVar];
2189
+ if (!configuredDir)
2190
+ return null;
2191
+ const normalized = normalizeDir(configuredDir);
2192
+ try {
2193
+ return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
2194
+ } catch {
2195
+ return null;
2196
+ }
2197
+ }
2198
+ async function resolveAccountForAgent(agent, env = process.env) {
2199
+ const override = envOverride(agent, env);
2200
+ if (override)
2201
+ return override;
2202
+ let api;
2203
+ try {
2204
+ api = await import("@hasna/accounts");
2205
+ } catch {
2206
+ return null;
2207
+ }
2208
+ const toolIds = knownToolIds(api);
2209
+ for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
2210
+ if (!toolIds.has(toolId))
2211
+ continue;
2212
+ let tool;
2213
+ try {
2214
+ tool = api.getTool(toolId);
2215
+ } catch {
2216
+ continue;
2217
+ }
2218
+ const envProfile = profileForEnvDir(api, tool, env);
2219
+ if (envProfile)
2220
+ return fromProfile(envProfile, "env");
2221
+ try {
2222
+ const applied = api.appliedProfile(toolId);
2223
+ if (applied)
2224
+ return fromProfile(applied, "applied");
2225
+ } catch {}
2226
+ try {
2227
+ const current = api.currentProfile(toolId);
2228
+ if (current)
2229
+ return fromProfile(current, "current");
2230
+ } catch {}
2231
+ }
2232
+ return null;
2233
+ }
2234
+ function withAccount(record, account) {
2235
+ if (!account)
2236
+ return record;
2237
+ return {
2238
+ ...record,
2239
+ account_key: account.account_key,
2240
+ account_tool: account.account_tool,
2241
+ account_name: account.account_name,
2242
+ account_email: account.account_email ?? "",
2243
+ account_source: account.account_source
2244
+ };
2245
+ }
2246
+
1894
2247
  // src/ingest/claude.ts
1895
2248
  function autoDetectProject(cwd, projects) {
1896
2249
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
@@ -1932,6 +2285,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1932
2285
  let totalRequests = 0;
1933
2286
  const touchedSessions = new Set;
1934
2287
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
2288
+ const account = await resolveAccountForAgent(agentName);
1935
2289
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
1936
2290
  for (const projectDirEntry of projectDirs) {
1937
2291
  const projectDirPath = join2(projectsDir, projectDirEntry.name);
@@ -1995,7 +2349,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1995
2349
  }
1996
2350
  const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
1997
2351
  const reqId = `${agentName}-${sourceRequestId}`;
1998
- upsertRequest(db, {
2352
+ upsertRequest(db, withAccount({
1999
2353
  id: reqId,
2000
2354
  agent: agentName,
2001
2355
  session_id: sessionId,
@@ -2012,7 +2366,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2012
2366
  timestamp,
2013
2367
  source_request_id: sourceRequestId,
2014
2368
  machine_id: machineId
2015
- });
2369
+ }, account));
2016
2370
  if (!touchedSessions.has(sessionId)) {
2017
2371
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
2018
2372
  if (!existing) {
@@ -2030,7 +2384,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2030
2384
  request_count: 0,
2031
2385
  machine_id: machineId
2032
2386
  };
2033
- upsertSession(db, session);
2387
+ upsertSession(db, withAccount(session, account));
2034
2388
  }
2035
2389
  touchedSessions.add(sessionId);
2036
2390
  }
@@ -2076,7 +2430,7 @@ import { join as join3, basename as basename2 } from "path";
2076
2430
  import { Database as BunDatabase } from "bun:sqlite";
2077
2431
  var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
2078
2432
  var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
2079
- var CODEX_INGEST_VERSION = "rollout-token-dedupe-v2";
2433
+ var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
2080
2434
  function codexDbPath() {
2081
2435
  return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
2082
2436
  }
@@ -2109,8 +2463,9 @@ function buildThreadQuery(codexDb) {
2109
2463
  function readTokenEvents(rolloutPath) {
2110
2464
  if (!rolloutPath || !existsSync3(rolloutPath))
2111
2465
  return [];
2112
- const events = [];
2113
- const seen = new Set;
2466
+ const fallbackUsages = new Map;
2467
+ let fallbackTimestamp;
2468
+ let aggregate = null;
2114
2469
  for (const line of readFileSync2(rolloutPath, "utf-8").split(`
2115
2470
  `)) {
2116
2471
  if (!line.trim())
@@ -2127,20 +2482,48 @@ function readTokenEvents(rolloutPath) {
2127
2482
  if (!payload || payload["type"] !== "token_count")
2128
2483
  continue;
2129
2484
  const info = payload["info"];
2485
+ const timestamp = entry["timestamp"];
2486
+ const entryTimestamp = typeof timestamp === "string" ? timestamp : undefined;
2487
+ const totalUsage = info?.["total_token_usage"];
2488
+ if (totalUsage && tokenTotal(totalUsage) > 0) {
2489
+ aggregate = { usage: totalUsage, timestamp: entryTimestamp };
2490
+ continue;
2491
+ }
2130
2492
  const usage = info?.["last_token_usage"];
2131
2493
  if (!usage)
2132
2494
  continue;
2133
- const total = usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
2134
- if (total <= 0)
2495
+ if (tokenTotal(usage) <= 0)
2135
2496
  continue;
2136
2497
  const key = JSON.stringify(usage);
2137
- if (seen.has(key))
2138
- continue;
2139
- seen.add(key);
2140
- const timestamp = entry["timestamp"];
2141
- events.push({ usage, timestamp: typeof timestamp === "string" ? timestamp : undefined });
2498
+ if (!fallbackUsages.has(key))
2499
+ fallbackUsages.set(key, usage);
2500
+ fallbackTimestamp = entryTimestamp ?? fallbackTimestamp;
2501
+ }
2502
+ if (aggregate)
2503
+ return [aggregate];
2504
+ if (fallbackUsages.size === 0)
2505
+ return [];
2506
+ return [{ usage: sumTokenUsages([...fallbackUsages.values()]), timestamp: fallbackTimestamp }];
2507
+ }
2508
+ function tokenTotal(usage) {
2509
+ return usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
2510
+ }
2511
+ function sumTokenUsages(usages) {
2512
+ const result = {
2513
+ input_tokens: 0,
2514
+ cached_input_tokens: 0,
2515
+ output_tokens: 0,
2516
+ reasoning_output_tokens: 0,
2517
+ total_tokens: 0
2518
+ };
2519
+ for (const usage of usages) {
2520
+ result.input_tokens = (result.input_tokens ?? 0) + (usage.input_tokens ?? 0);
2521
+ result.cached_input_tokens = (result.cached_input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
2522
+ result.output_tokens = (result.output_tokens ?? 0) + (usage.output_tokens ?? 0);
2523
+ result.reasoning_output_tokens = (result.reasoning_output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0);
2524
+ result.total_tokens = (result.total_tokens ?? 0) + tokenTotal(usage);
2142
2525
  }
2143
- return events;
2526
+ return result;
2144
2527
  }
2145
2528
  function fallbackEvents(totalTokens) {
2146
2529
  const inputTokens = Math.floor(totalTokens * 0.6);
@@ -2164,6 +2547,7 @@ async function ingestCodex(db, verbose = false) {
2164
2547
  let codexDb = null;
2165
2548
  let ingested = 0;
2166
2549
  let requests = 0;
2550
+ const account = await resolveAccountForAgent("codex");
2167
2551
  try {
2168
2552
  codexDb = new BunDatabase(dbPath, { readonly: true });
2169
2553
  const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
@@ -2178,7 +2562,7 @@ async function ingestCodex(db, verbose = false) {
2178
2562
  const sessionId = `codex-${thread.id}`;
2179
2563
  const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
2180
2564
  const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
2181
- upsertSession(db, {
2565
+ upsertSession(db, withAccount({
2182
2566
  id: sessionId,
2183
2567
  agent: "codex",
2184
2568
  project_path: projectPath,
@@ -2189,9 +2573,10 @@ async function ingestCodex(db, verbose = false) {
2189
2573
  total_tokens: 0,
2190
2574
  request_count: 0,
2191
2575
  machine_id: machineId
2192
- });
2576
+ }, account));
2193
2577
  const events = readTokenEvents(thread.rollout_path);
2194
2578
  const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
2579
+ const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
2195
2580
  db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
2196
2581
  tokenEvents.forEach((event, index) => {
2197
2582
  const usage = event.usage;
@@ -2202,7 +2587,7 @@ async function ingestCodex(db, verbose = false) {
2202
2587
  const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
2203
2588
  const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
2204
2589
  const requestId = `${sessionId}-${index}`;
2205
- upsertRequest(db, {
2590
+ upsertRequest(db, withAccount({
2206
2591
  id: requestId,
2207
2592
  agent: "codex",
2208
2593
  session_id: sessionId,
@@ -2217,14 +2602,14 @@ async function ingestCodex(db, verbose = false) {
2217
2602
  timestamp,
2218
2603
  source_request_id: requestId,
2219
2604
  machine_id: machineId
2220
- });
2605
+ }, account));
2221
2606
  requests++;
2222
2607
  });
2223
2608
  rollupSession(db, sessionId);
2224
2609
  setIngestState(db, "codex", thread.id, stateValue);
2225
2610
  ingested++;
2226
2611
  if (verbose)
2227
- console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens on ${model}`);
2612
+ console.log(`Codex session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
2228
2613
  }
2229
2614
  } finally {
2230
2615
  codexDb?.close();
@@ -2291,6 +2676,7 @@ async function ingestGemini(db, verbose) {
2291
2676
  let totalSessions = 0;
2292
2677
  let totalRequests = 0;
2293
2678
  const touchedSessions = new Set;
2679
+ const account = await resolveAccountForAgent("gemini");
2294
2680
  const projectDirs = listProjectDirs(tmpDir, historyDir);
2295
2681
  for (const projectDir of projectDirs) {
2296
2682
  const chatsDir = join4(projectDir, "chats");
@@ -2339,7 +2725,7 @@ async function ingestGemini(db, verbose) {
2339
2725
  request_count: 0,
2340
2726
  machine_id: machineId
2341
2727
  };
2342
- upsertSession(db, session);
2728
+ upsertSession(db, withAccount(session, account));
2343
2729
  totalSessions++;
2344
2730
  }
2345
2731
  touchedSessions.add(sessionId);
@@ -2363,7 +2749,7 @@ async function ingestGemini(db, verbose) {
2363
2749
  const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
2364
2750
  const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
2365
2751
  const requestId = `gemini-${sessionId}-${message.id ?? index}`;
2366
- upsertRequest(db, {
2752
+ upsertRequest(db, withAccount({
2367
2753
  id: requestId,
2368
2754
  agent: "gemini",
2369
2755
  session_id: sessionId,
@@ -2378,7 +2764,7 @@ async function ingestGemini(db, verbose) {
2378
2764
  timestamp,
2379
2765
  source_request_id: message.id ?? requestId,
2380
2766
  machine_id: machineId
2381
- });
2767
+ }, account));
2382
2768
  totalRequests++;
2383
2769
  }
2384
2770
  setIngestState(db, "gemini", stateKey, fileMtime);
@@ -2427,6 +2813,7 @@ async function ingestOpenCode(db, verbose = false) {
2427
2813
  const touched = new Set;
2428
2814
  const machineId = getMachineId();
2429
2815
  const now = new Date().toISOString();
2816
+ const account = await resolveAccountForAgent("opencode");
2430
2817
  for (const file of files) {
2431
2818
  const mtime = statSync4(file).mtimeMs;
2432
2819
  const stateKey = file;
@@ -2456,7 +2843,7 @@ async function ingestOpenCode(db, verbose = false) {
2456
2843
  const sourceId = file.replace(OPENCODE_STORAGE, "");
2457
2844
  const reqId = `opencode-${sourceId}`;
2458
2845
  const costUsd = usage.cost ?? computeCostFromDb(db, model, input, output, cacheRead, cacheWrite, 0);
2459
- upsertRequest(db, {
2846
+ upsertRequest(db, withAccount({
2460
2847
  id: reqId,
2461
2848
  agent: "opencode",
2462
2849
  session_id: sessionId,
@@ -2472,10 +2859,10 @@ async function ingestOpenCode(db, verbose = false) {
2472
2859
  source_request_id: sourceId,
2473
2860
  machine_id: machineId,
2474
2861
  updated_at: now
2475
- });
2862
+ }, account));
2476
2863
  requests++;
2477
2864
  if (!touched.has(sessionId)) {
2478
- upsertSession(db, {
2865
+ upsertSession(db, withAccount({
2479
2866
  id: sessionId,
2480
2867
  agent: "opencode",
2481
2868
  project_path: "",
@@ -2487,7 +2874,7 @@ async function ingestOpenCode(db, verbose = false) {
2487
2874
  request_count: 0,
2488
2875
  machine_id: machineId,
2489
2876
  updated_at: now
2490
- });
2877
+ }, account));
2491
2878
  touched.add(sessionId);
2492
2879
  }
2493
2880
  setIngestState(db, "opencode", stateKey, String(mtime));
@@ -2534,6 +2921,7 @@ async function ingestCursor(db, verbose = false) {
2534
2921
  const machineId = getMachineId();
2535
2922
  const now = new Date().toISOString();
2536
2923
  let snapshots = 0;
2924
+ const account = await resolveAccountForAgent("cursor");
2537
2925
  const usage = await cursorFetch("/api/usage", token);
2538
2926
  if (usage?.premiumRequests != null && usage.maxPremiumRequests) {
2539
2927
  upsertUsageSnapshot(db, {
@@ -2581,7 +2969,7 @@ async function ingestCursor(db, verbose = false) {
2581
2969
  }
2582
2970
  const sessionId = `cursor-${today}-${machineId}`;
2583
2971
  if (onDemand + included > 0) {
2584
- upsertSession(db, {
2972
+ upsertSession(db, withAccount({
2585
2973
  id: sessionId,
2586
2974
  agent: "cursor",
2587
2975
  project_path: "",
@@ -2593,8 +2981,8 @@ async function ingestCursor(db, verbose = false) {
2593
2981
  request_count: 1,
2594
2982
  machine_id: machineId,
2595
2983
  updated_at: now
2596
- });
2597
- upsertRequest(db, {
2984
+ }, account));
2985
+ upsertRequest(db, withAccount({
2598
2986
  id: `cursor-${today}-${machineId}-usage`,
2599
2987
  agent: "cursor",
2600
2988
  session_id: sessionId,
@@ -2610,7 +2998,7 @@ async function ingestCursor(db, verbose = false) {
2610
2998
  source_request_id: today,
2611
2999
  machine_id: machineId,
2612
3000
  updated_at: now
2613
- });
3001
+ }, account));
2614
3002
  rollupSession(db, sessionId);
2615
3003
  }
2616
3004
  setIngestState(db, "cursor", `sync-${today}`, now);
@@ -2643,6 +3031,7 @@ async function ingestPi(db, verbose = false) {
2643
3031
  const touched = new Set;
2644
3032
  const machineId = getMachineId();
2645
3033
  const now = new Date().toISOString();
3034
+ const account = await resolveAccountForAgent("pi");
2646
3035
  for (const file of files) {
2647
3036
  const mtime = statSync5(file).mtimeMs;
2648
3037
  const prev = getIngestState(db, "pi", file);
@@ -2668,7 +3057,7 @@ async function ingestPi(db, verbose = false) {
2668
3057
  const model = turn.model ?? turn.provider ?? "unknown";
2669
3058
  const timestamp = turn.timestamp ?? new Date(statSync5(file).mtime).toISOString();
2670
3059
  const reqId = `pi-${sessionId}-${i}`;
2671
- upsertRequest(db, {
3060
+ upsertRequest(db, withAccount({
2672
3061
  id: reqId,
2673
3062
  agent: "pi",
2674
3063
  session_id: sessionId,
@@ -2684,11 +3073,11 @@ async function ingestPi(db, verbose = false) {
2684
3073
  source_request_id: `${sessionId}-${i}`,
2685
3074
  machine_id: machineId,
2686
3075
  updated_at: now
2687
- });
3076
+ }, account));
2688
3077
  requests++;
2689
3078
  }
2690
3079
  if (turns.length > 0) {
2691
- upsertSession(db, {
3080
+ upsertSession(db, withAccount({
2692
3081
  id: sessionId,
2693
3082
  agent: "pi",
2694
3083
  project_path: "",
@@ -2700,7 +3089,7 @@ async function ingestPi(db, verbose = false) {
2700
3089
  request_count: 0,
2701
3090
  machine_id: machineId,
2702
3091
  updated_at: now
2703
- });
3092
+ }, account));
2704
3093
  touched.add(sessionId);
2705
3094
  }
2706
3095
  setIngestState(db, "pi", file, String(mtime));
@@ -2748,13 +3137,14 @@ async function ingestHermes(db, verbose = false) {
2748
3137
  const machineId = getMachineId();
2749
3138
  const now = new Date().toISOString();
2750
3139
  let requests = 0;
3140
+ const account = await resolveAccountForAgent("hermes");
2751
3141
  for (const row of rows) {
2752
3142
  const sessionId = `hermes-${row.id}`;
2753
3143
  const startedAt = new Date(row.started_at * 1000).toISOString();
2754
3144
  const endedAt = row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null;
2755
3145
  const cost = row.actual_cost_usd ?? row.estimated_cost_usd ?? 0;
2756
3146
  const tokens = row.input_tokens + row.output_tokens + row.cache_read_tokens + row.cache_write_tokens + row.reasoning_tokens;
2757
- upsertSession(db, {
3147
+ upsertSession(db, withAccount({
2758
3148
  id: sessionId,
2759
3149
  agent: "hermes",
2760
3150
  project_path: row.source ?? "",
@@ -2766,9 +3156,9 @@ async function ingestHermes(db, verbose = false) {
2766
3156
  request_count: 1,
2767
3157
  machine_id: machineId,
2768
3158
  updated_at: now
2769
- });
3159
+ }, account));
2770
3160
  const reqId = `hermes-${row.id}-rollup`;
2771
- upsertRequest(db, {
3161
+ upsertRequest(db, withAccount({
2772
3162
  id: reqId,
2773
3163
  agent: "hermes",
2774
3164
  session_id: sessionId,
@@ -2784,7 +3174,7 @@ async function ingestHermes(db, verbose = false) {
2784
3174
  source_request_id: row.id,
2785
3175
  machine_id: machineId,
2786
3176
  updated_at: now
2787
- });
3177
+ }, account));
2788
3178
  requests++;
2789
3179
  rollupSession(db, sessionId);
2790
3180
  if (verbose)
@@ -2804,7 +3194,7 @@ function statSyncSafe(path) {
2804
3194
 
2805
3195
  // src/ingest/claude-quota.ts
2806
3196
  init_database();
2807
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
3197
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
2808
3198
 
2809
3199
  // src/lib/paths.ts
2810
3200
  import { homedir as homedir8 } from "os";
@@ -2842,7 +3232,7 @@ function readClaudeToken() {
2842
3232
  if (!existsSync8(CREDENTIALS_PATH))
2843
3233
  return null;
2844
3234
  try {
2845
- const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
3235
+ const creds = JSON.parse(readFileSync6(CREDENTIALS_PATH, "utf-8"));
2846
3236
  const oauth = creds.claudeAiOauth;
2847
3237
  if (!oauth?.accessToken)
2848
3238
  return null;
@@ -2978,7 +3368,7 @@ async function ingestClaudeQuota(db, verbose = false) {
2978
3368
 
2979
3369
  // src/ingest/codex-quota.ts
2980
3370
  init_database();
2981
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
3371
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
2982
3372
  var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
2983
3373
  function readCodexAuth() {
2984
3374
  const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
@@ -2988,7 +3378,7 @@ function readCodexAuth() {
2988
3378
  if (!existsSync9(authPath))
2989
3379
  return null;
2990
3380
  try {
2991
- const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
3381
+ const auth = JSON.parse(readFileSync7(authPath, "utf-8"));
2992
3382
  const token = auth.tokens?.access_token;
2993
3383
  if (!token)
2994
3384
  return null;
@@ -3122,12 +3512,12 @@ init_database();
3122
3512
  init_database();
3123
3513
 
3124
3514
  // src/lib/package-metadata.ts
3125
- import { readFileSync as readFileSync9 } from "fs";
3515
+ import { readFileSync as readFileSync8 } from "fs";
3126
3516
  var cachedMetadata = null;
3127
3517
  function getPackageMetadata() {
3128
3518
  if (cachedMetadata)
3129
3519
  return cachedMetadata;
3130
- const raw = readFileSync9(new URL("../../package.json", import.meta.url), "utf8");
3520
+ const raw = readFileSync8(new URL("../../package.json", import.meta.url), "utf8");
3131
3521
  const parsed = JSON.parse(raw);
3132
3522
  cachedMetadata = {
3133
3523
  name: parsed.name ?? "@hasna/economy",
@@ -3179,44 +3569,27 @@ async function runCloudMigrations(cloud) {
3179
3569
  await cloud.run(sql);
3180
3570
  }
3181
3571
  }
3182
- function isCloudIncrementalEnabled() {
3183
- return process.env["ECONOMY_CLOUD_INCREMENTAL"] === "1" || process.env["ECONOMY_CLOUD_INCREMENTAL"] === "true";
3184
- }
3185
3572
  async function cloudPush(opts) {
3186
- const { syncPush, incrementalSyncPush, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
3573
+ const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
3187
3574
  const cloud = await getCloudPg();
3188
3575
  const local = new SqliteAdapter(getDbPath());
3189
3576
  await runCloudMigrations(cloud);
3190
3577
  const tables = opts?.tables ?? [...CLOUD_TABLES];
3191
- let rows = 0;
3192
- if (isCloudIncrementalEnabled()) {
3193
- ensureSyncMetaTable(local);
3194
- const results = incrementalSyncPush(local, cloud, tables, { conflictColumn: "updated_at" });
3195
- rows = results.reduce((s, r) => s + r.synced_rows, 0);
3196
- } else {
3197
- const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
3198
- rows = results.reduce((s, r) => s + r.rowsWritten, 0);
3199
- }
3578
+ const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
3579
+ const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
3200
3580
  touchMachineRegistry(local, "push");
3201
3581
  local.close();
3202
3582
  await cloud.close();
3203
3583
  return { rows, machine: getMachineId() };
3204
3584
  }
3205
3585
  async function cloudPull(opts) {
3206
- const { syncPull, incrementalSyncPull, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
3586
+ const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
3207
3587
  const cloud = await getCloudPg();
3208
3588
  const local = new SqliteAdapter(getDbPath());
3209
3589
  await runCloudMigrations(cloud);
3210
3590
  const tables = opts?.tables ?? [...CLOUD_TABLES];
3211
- let rows = 0;
3212
- if (isCloudIncrementalEnabled()) {
3213
- ensureSyncMetaTable(local);
3214
- const results = incrementalSyncPull(cloud, local, tables, { conflictColumn: "updated_at" });
3215
- rows = results.reduce((s, r) => s + r.synced_rows, 0);
3216
- } else {
3217
- const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
3218
- rows = results.reduce((s, r) => s + r.rowsWritten, 0);
3219
- }
3591
+ const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
3592
+ const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
3220
3593
  touchMachineRegistry(local, "pull");
3221
3594
  local.close();
3222
3595
  await cloud.close();
@@ -3526,6 +3899,7 @@ function createHandler(db) {
3526
3899
  const project = url.searchParams.get("project") ?? undefined;
3527
3900
  const search = url.searchParams.get("search") ?? undefined;
3528
3901
  const machine = url.searchParams.get("machine") ?? undefined;
3902
+ const account = url.searchParams.get("account") ?? undefined;
3529
3903
  const limit = Number(url.searchParams.get("limit") ?? 50);
3530
3904
  const offset = Number(url.searchParams.get("offset") ?? 0);
3531
3905
  const since = url.searchParams.get("since") ?? undefined;
@@ -3536,6 +3910,7 @@ function createHandler(db) {
3536
3910
  project,
3537
3911
  search,
3538
3912
  machine,
3913
+ account,
3539
3914
  limit,
3540
3915
  offset,
3541
3916
  since
@@ -3586,11 +3961,23 @@ function createHandler(db) {
3586
3961
  return ok(results);
3587
3962
  }
3588
3963
  if (path === "/api/projects" && method === "GET") {
3589
- return ok(queryProjectBreakdown(db));
3964
+ const period = url.searchParams.get("period") ?? "all";
3965
+ return ok(queryProjectBreakdown(db, period));
3966
+ }
3967
+ if (path === "/api/accounts" && method === "GET") {
3968
+ const period = url.searchParams.get("period") ?? "all";
3969
+ return ok(queryAccountBreakdown(db, period));
3590
3970
  }
3591
3971
  if (path === "/api/breakdown" && method === "GET") {
3592
3972
  const by = url.searchParams.get("by") ?? "model";
3593
- return ok(by === "project" ? queryProjectBreakdown(db) : queryModelBreakdown(db));
3973
+ const period = url.searchParams.get("period") ?? "all";
3974
+ if (by === "project")
3975
+ return ok(queryProjectBreakdown(db, period));
3976
+ if (by === "agent")
3977
+ return ok(queryAgentBreakdown(db, period));
3978
+ if (by === "account")
3979
+ return ok(queryAccountBreakdown(db, period));
3980
+ return ok(queryModelBreakdown(db));
3594
3981
  }
3595
3982
  if (path === "/api/budgets" && method === "GET") {
3596
3983
  return ok(getBudgetStatuses(db));