@hasna/economy 0.2.21 → 0.2.23

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
@@ -536,6 +536,8 @@ __export(exports_database, {
536
536
  queryModelBreakdown: () => queryModelBreakdown,
537
537
  queryDailyBreakdown: () => queryDailyBreakdown,
538
538
  queryBillingSummary: () => queryBillingSummary,
539
+ queryAgentBreakdown: () => queryAgentBreakdown,
540
+ queryAccountBreakdown: () => queryAccountBreakdown,
539
541
  openDatabase: () => openDatabase,
540
542
  listSubscriptions: () => listSubscriptions,
541
543
  listProjects: () => listProjects,
@@ -630,7 +632,12 @@ function initSchema(db) {
630
632
  duration_ms INTEGER DEFAULT 0,
631
633
  timestamp TEXT NOT NULL,
632
634
  source_request_id TEXT,
633
- machine_id TEXT DEFAULT ''
635
+ machine_id TEXT DEFAULT '',
636
+ account_key TEXT DEFAULT '',
637
+ account_tool TEXT DEFAULT '',
638
+ account_name TEXT DEFAULT '',
639
+ account_email TEXT DEFAULT '',
640
+ account_source TEXT DEFAULT ''
634
641
  );
635
642
 
636
643
  CREATE TABLE IF NOT EXISTS sessions (
@@ -643,7 +650,12 @@ function initSchema(db) {
643
650
  total_cost_usd REAL DEFAULT 0,
644
651
  total_tokens INTEGER DEFAULT 0,
645
652
  request_count INTEGER DEFAULT 0,
646
- machine_id TEXT DEFAULT ''
653
+ machine_id TEXT DEFAULT '',
654
+ account_key TEXT DEFAULT '',
655
+ account_tool TEXT DEFAULT '',
656
+ account_name TEXT DEFAULT '',
657
+ account_email TEXT DEFAULT '',
658
+ account_source TEXT DEFAULT ''
647
659
  );
648
660
 
649
661
  CREATE TABLE IF NOT EXISTS projects (
@@ -798,6 +810,11 @@ function initSchema(db) {
798
810
  if (!cols.some((c) => c.name === "synced_at")) {
799
811
  db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
800
812
  }
813
+ for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
814
+ if (!cols.some((c) => c.name === column)) {
815
+ db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
816
+ }
817
+ }
801
818
  const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
802
819
  if (!sessionCols.some((c) => c.name === "attribution_tag")) {
803
820
  db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
@@ -809,6 +826,11 @@ function initSchema(db) {
809
826
  if (!sessionCols.some((c) => c.name === "synced_at")) {
810
827
  db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
811
828
  }
829
+ for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
830
+ if (!sessionCols.some((c) => c.name === column)) {
831
+ db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
832
+ }
833
+ }
812
834
  const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
813
835
  if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
814
836
  db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
@@ -819,6 +841,8 @@ function initSchema(db) {
819
841
  db.exec(`
820
842
  CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
821
843
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
844
+ CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
845
+ CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
822
846
  `);
823
847
  }
824
848
  function periodWhere(period) {
@@ -853,6 +877,22 @@ function sessionPeriodWhere(period) {
853
877
  return "1=1";
854
878
  }
855
879
  }
880
+ function requestPeriodWhere(period) {
881
+ switch (period) {
882
+ case "today":
883
+ return `DATE(timestamp) = DATE('now')`;
884
+ case "yesterday":
885
+ return `DATE(timestamp) = DATE('now', '-1 day')`;
886
+ case "week":
887
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
888
+ case "month":
889
+ return `timestamp >= DATE('now', 'start of month')`;
890
+ case "year":
891
+ return `timestamp >= DATE('now', 'start of year')`;
892
+ case "all":
893
+ return "1=1";
894
+ }
895
+ }
856
896
  function upsertRequest(db, req) {
857
897
  const now = req.updated_at ?? new Date().toISOString();
858
898
  db.prepare(`
@@ -860,18 +900,20 @@ function upsertRequest(db, req) {
860
900
  (id, agent, session_id, model, input_tokens, output_tokens,
861
901
  cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
862
902
  cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
863
- source_request_id, machine_id, attribution_tag, updated_at, synced_at)
864
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
865
- `).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 ?? "");
903
+ source_request_id, machine_id, attribution_tag, account_key, account_tool,
904
+ account_name, account_email, account_source, updated_at, synced_at)
905
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
906
+ `).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 ?? "");
866
907
  }
867
908
  function upsertSession(db, session) {
868
909
  const now = session.updated_at ?? new Date().toISOString();
869
910
  db.prepare(`
870
911
  INSERT OR REPLACE INTO sessions
871
912
  (id, agent, project_path, project_name, started_at, ended_at,
872
- total_cost_usd, total_tokens, request_count, machine_id, attribution_tag, updated_at, synced_at)
873
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
874
- `).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 ?? "");
913
+ total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
914
+ account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
915
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
916
+ `).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 ?? "");
875
917
  }
876
918
  function rollupSession(db, sessionId) {
877
919
  db.prepare(`
@@ -882,9 +924,24 @@ function rollupSession(db, sessionId) {
882
924
  ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
883
925
  started_at = CASE WHEN started_at = '' OR started_at IS NULL
884
926
  THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
885
- ELSE started_at END
927
+ ELSE started_at END,
928
+ account_key = CASE WHEN account_key = '' OR account_key IS NULL
929
+ THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
930
+ ELSE account_key END,
931
+ account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
932
+ THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
933
+ ELSE account_tool END,
934
+ account_name = CASE WHEN account_name = '' OR account_name IS NULL
935
+ THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
936
+ ELSE account_name END,
937
+ account_email = CASE WHEN account_email = '' OR account_email IS NULL
938
+ THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
939
+ ELSE account_email END,
940
+ account_source = CASE WHEN account_source = '' OR account_source IS NULL
941
+ THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
942
+ ELSE account_source END
886
943
  WHERE id = ?
887
- `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
944
+ `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
888
945
  }
889
946
  function querySessions(db, filter = {}) {
890
947
  const conditions = [];
@@ -897,6 +954,11 @@ function querySessions(db, filter = {}) {
897
954
  conditions.push("project_path LIKE ?");
898
955
  params.push(`%${filter.project}%`);
899
956
  }
957
+ if (filter.account) {
958
+ const q = `%${filter.account}%`;
959
+ conditions.push("(account_key LIKE ? OR account_name LIKE ? OR account_email LIKE ?)");
960
+ params.push(q, q, q);
961
+ }
900
962
  if (filter.since) {
901
963
  conditions.push("started_at >= ?");
902
964
  params.push(filter.since);
@@ -961,6 +1023,70 @@ function queryModelBreakdown(db) {
961
1023
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
962
1024
  `).all();
963
1025
  }
1026
+ function queryAgentBreakdown(db, period = "all") {
1027
+ const requestWhere = requestPeriodWhere(period);
1028
+ const groups = new Map;
1029
+ const requestRows = db.prepare(`
1030
+ SELECT agent,
1031
+ COUNT(DISTINCT session_id) as sessions,
1032
+ COUNT(*) as requests,
1033
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1034
+ COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
1035
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1036
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1037
+ COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1038
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
1039
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
1040
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
1041
+ MAX(timestamp) as last_active
1042
+ FROM requests
1043
+ WHERE ${requestWhere}
1044
+ GROUP BY agent
1045
+ ORDER BY api_equivalent_usd DESC
1046
+ `).all();
1047
+ for (const row of requestRows) {
1048
+ groups.set(row.agent, row);
1049
+ }
1050
+ const sessionWhere = sessionPeriodWhere(period);
1051
+ const sessionOnlyRows = db.prepare(`
1052
+ SELECT agent,
1053
+ COUNT(*) as sessions,
1054
+ COALESCE(SUM(request_count), 0) as requests,
1055
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
1056
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1057
+ MAX(started_at) as last_active
1058
+ FROM sessions
1059
+ WHERE ${sessionWhere}
1060
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1061
+ GROUP BY agent
1062
+ `).all();
1063
+ for (const row of sessionOnlyRows) {
1064
+ const existing = groups.get(row.agent) ?? {
1065
+ agent: row.agent,
1066
+ sessions: 0,
1067
+ requests: 0,
1068
+ total_tokens: 0,
1069
+ api_equivalent_usd: 0,
1070
+ billable_usd: 0,
1071
+ metered_api_usd: 0,
1072
+ subscription_included_usd: 0,
1073
+ estimated_usd: 0,
1074
+ unknown_usd: 0,
1075
+ cost_usd: 0,
1076
+ last_active: ""
1077
+ };
1078
+ existing.sessions += row.sessions;
1079
+ existing.requests += row.requests;
1080
+ existing.total_tokens += row.total_tokens;
1081
+ existing.api_equivalent_usd += row.cost_usd;
1082
+ existing.estimated_usd += row.cost_usd;
1083
+ existing.cost_usd += row.cost_usd;
1084
+ if (!existing.last_active || row.last_active > existing.last_active)
1085
+ existing.last_active = row.last_active;
1086
+ groups.set(row.agent, existing);
1087
+ }
1088
+ return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
1089
+ }
964
1090
  function labelForPath(projectPath, projectName) {
965
1091
  if (projectName && projectName.trim() !== "")
966
1092
  return projectName;
@@ -979,11 +1105,13 @@ function labelForPath(projectPath, projectName) {
979
1105
  }
980
1106
  return segments[segments.length - 1] ?? projectPath;
981
1107
  }
982
- function queryProjectBreakdown(db) {
1108
+ function queryProjectBreakdown(db, period = "all") {
1109
+ const where = sessionPeriodWhere(period);
983
1110
  const sessions = db.prepare(`
984
1111
  SELECT id, project_path, project_name, total_cost_usd, started_at
985
1112
  FROM sessions
986
- WHERE project_path != '' OR project_name != ''
1113
+ WHERE ${where}
1114
+ AND (project_path != '' OR project_name != '')
987
1115
  `).all();
988
1116
  const groups = new Map;
989
1117
  for (const s of sessions) {
@@ -1022,6 +1150,87 @@ function queryProjectBreakdown(db) {
1022
1150
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1023
1151
  return result;
1024
1152
  }
1153
+ function queryAccountBreakdown(db, period = "all") {
1154
+ const sWhere = sessionPeriodWhere(period);
1155
+ const sessions = db.prepare(`
1156
+ SELECT id, account_key, account_tool, account_name, account_email, account_source,
1157
+ total_cost_usd, total_tokens, request_count, started_at
1158
+ FROM sessions
1159
+ WHERE ${sWhere}
1160
+ AND (account_key != '' OR account_tool != '' OR account_name != '' OR account_email != '')
1161
+ `).all();
1162
+ const groups = new Map;
1163
+ for (const session of sessions) {
1164
+ const key = session.account_key || `${session.account_tool}:${session.account_name}`;
1165
+ if (!key || key === ":")
1166
+ continue;
1167
+ const group = groups.get(key) ?? {
1168
+ sessionIds: [],
1169
+ account_tool: session.account_tool,
1170
+ account_name: session.account_name,
1171
+ account_email: session.account_email || null,
1172
+ account_source: session.account_source || "unknown",
1173
+ totalCost: 0,
1174
+ totalTokens: 0,
1175
+ requests: 0,
1176
+ lastActive: ""
1177
+ };
1178
+ group.sessionIds.push(session.id);
1179
+ group.totalCost += session.total_cost_usd || 0;
1180
+ group.totalTokens += session.total_tokens || 0;
1181
+ group.requests += session.request_count || 0;
1182
+ if (!group.lastActive || session.started_at > group.lastActive)
1183
+ group.lastActive = session.started_at;
1184
+ groups.set(key, group);
1185
+ }
1186
+ const result = [];
1187
+ for (const [key, group] of groups.entries()) {
1188
+ const placeholders = group.sessionIds.map(() => "?").join(",");
1189
+ const reqStats = placeholders ? db.prepare(`
1190
+ SELECT
1191
+ COUNT(*) as requests,
1192
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
1193
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1194
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1195
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1196
+ COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1197
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd
1198
+ FROM requests WHERE session_id IN (${placeholders})
1199
+ `).get(...group.sessionIds) : {
1200
+ requests: 0,
1201
+ cost_usd: 0,
1202
+ total_tokens: 0,
1203
+ metered_api_usd: 0,
1204
+ subscription_included_usd: 0,
1205
+ estimated_usd: 0,
1206
+ unknown_usd: 0
1207
+ };
1208
+ const hasRequestCosts = reqStats.requests > 0;
1209
+ const apiEquivalentUsd = hasRequestCosts ? reqStats.cost_usd : group.totalCost;
1210
+ const estimatedUsd = hasRequestCosts ? reqStats.estimated_usd : group.totalCost;
1211
+ const billableUsd = reqStats.metered_api_usd;
1212
+ result.push({
1213
+ account_key: key,
1214
+ account_tool: group.account_tool,
1215
+ account_name: group.account_name,
1216
+ account_email: group.account_email,
1217
+ account_source: group.account_source,
1218
+ sessions: group.sessionIds.length,
1219
+ requests: reqStats.requests || group.requests,
1220
+ total_tokens: reqStats.total_tokens || group.totalTokens,
1221
+ api_equivalent_usd: apiEquivalentUsd,
1222
+ billable_usd: billableUsd,
1223
+ metered_api_usd: reqStats.metered_api_usd,
1224
+ subscription_included_usd: reqStats.subscription_included_usd,
1225
+ estimated_usd: estimatedUsd,
1226
+ unknown_usd: reqStats.unknown_usd,
1227
+ cost_usd: apiEquivalentUsd,
1228
+ last_active: group.lastActive
1229
+ });
1230
+ }
1231
+ result.sort((a, b) => b.cost_usd - a.cost_usd);
1232
+ return result;
1233
+ }
1025
1234
  function queryDailyBreakdown(db, days = 30) {
1026
1235
  return db.prepare(`
1027
1236
  SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
@@ -1286,7 +1495,6 @@ function periodWhere2(period, column) {
1286
1495
  }
1287
1496
  function prorateMonthlyFee(monthlyFee, period) {
1288
1497
  const now = new Date;
1289
- const dayOfMonth = now.getDate();
1290
1498
  const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
1291
1499
  switch (period) {
1292
1500
  case "today":
@@ -1473,7 +1681,12 @@ var init_pg_migrations = __esm(() => {
1473
1681
  duration_ms INTEGER DEFAULT 0,
1474
1682
  timestamp TEXT NOT NULL,
1475
1683
  source_request_id TEXT,
1476
- machine_id TEXT DEFAULT ''
1684
+ machine_id TEXT DEFAULT '',
1685
+ account_key TEXT DEFAULT '',
1686
+ account_tool TEXT DEFAULT '',
1687
+ account_name TEXT DEFAULT '',
1688
+ account_email TEXT DEFAULT '',
1689
+ account_source TEXT DEFAULT ''
1477
1690
  )`,
1478
1691
  `CREATE TABLE IF NOT EXISTS sessions (
1479
1692
  id TEXT PRIMARY KEY,
@@ -1485,7 +1698,12 @@ var init_pg_migrations = __esm(() => {
1485
1698
  total_cost_usd REAL DEFAULT 0,
1486
1699
  total_tokens INTEGER DEFAULT 0,
1487
1700
  request_count INTEGER DEFAULT 0,
1488
- machine_id TEXT DEFAULT ''
1701
+ machine_id TEXT DEFAULT '',
1702
+ account_key TEXT DEFAULT '',
1703
+ account_tool TEXT DEFAULT '',
1704
+ account_name TEXT DEFAULT '',
1705
+ account_email TEXT DEFAULT '',
1706
+ account_source TEXT DEFAULT ''
1489
1707
  )`,
1490
1708
  `CREATE TABLE IF NOT EXISTS projects (
1491
1709
  id TEXT PRIMARY KEY,
@@ -1606,13 +1824,25 @@ var init_pg_migrations = __esm(() => {
1606
1824
  )`,
1607
1825
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
1608
1826
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1827
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1828
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1829
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1830
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1831
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1609
1832
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1610
1833
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1611
1834
  `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1835
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1836
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1837
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1838
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1839
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1612
1840
  `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1613
1841
  `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1614
1842
  `CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
1615
- `CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`
1843
+ `CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
1844
+ `CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
1845
+ `CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`
1616
1846
  ];
1617
1847
  });
1618
1848
 
@@ -1644,44 +1874,27 @@ async function runCloudMigrations(cloud) {
1644
1874
  await cloud.run(sql);
1645
1875
  }
1646
1876
  }
1647
- function isCloudIncrementalEnabled() {
1648
- return process.env["ECONOMY_CLOUD_INCREMENTAL"] === "1" || process.env["ECONOMY_CLOUD_INCREMENTAL"] === "true";
1649
- }
1650
1877
  async function cloudPush(opts) {
1651
- const { syncPush, incrementalSyncPush, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
1878
+ const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
1652
1879
  const cloud = await getCloudPg();
1653
1880
  const local = new SqliteAdapter(getDbPath());
1654
1881
  await runCloudMigrations(cloud);
1655
1882
  const tables = opts?.tables ?? [...CLOUD_TABLES];
1656
- let rows = 0;
1657
- if (isCloudIncrementalEnabled()) {
1658
- ensureSyncMetaTable(local);
1659
- const results = incrementalSyncPush(local, cloud, tables, { conflictColumn: "updated_at" });
1660
- rows = results.reduce((s, r) => s + r.synced_rows, 0);
1661
- } else {
1662
- const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
1663
- rows = results.reduce((s, r) => s + r.rowsWritten, 0);
1664
- }
1883
+ const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
1884
+ const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
1665
1885
  touchMachineRegistry(local, "push");
1666
1886
  local.close();
1667
1887
  await cloud.close();
1668
1888
  return { rows, machine: getMachineId() };
1669
1889
  }
1670
1890
  async function cloudPull(opts) {
1671
- const { syncPull, incrementalSyncPull, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
1891
+ const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
1672
1892
  const cloud = await getCloudPg();
1673
1893
  const local = new SqliteAdapter(getDbPath());
1674
1894
  await runCloudMigrations(cloud);
1675
1895
  const tables = opts?.tables ?? [...CLOUD_TABLES];
1676
- let rows = 0;
1677
- if (isCloudIncrementalEnabled()) {
1678
- ensureSyncMetaTable(local);
1679
- const results = incrementalSyncPull(cloud, local, tables, { conflictColumn: "updated_at" });
1680
- rows = results.reduce((s, r) => s + r.synced_rows, 0);
1681
- } else {
1682
- const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
1683
- rows = results.reduce((s, r) => s + r.rowsWritten, 0);
1684
- }
1896
+ const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
1897
+ const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
1685
1898
  touchMachineRegistry(local, "pull");
1686
1899
  local.close();
1687
1900
  await cloud.close();
@@ -1970,6 +2183,134 @@ function agentPaths() {
1970
2183
  }
1971
2184
  var init_paths = () => {};
1972
2185
 
2186
+ // src/lib/accounts.ts
2187
+ function accountKey(tool, name) {
2188
+ return `${tool}:${name}`;
2189
+ }
2190
+ function normalizeDir(value) {
2191
+ return value.replace(/\/+$/, "");
2192
+ }
2193
+ function fromProfile(profile, source) {
2194
+ return {
2195
+ account_key: accountKey(profile.tool, profile.name),
2196
+ account_tool: profile.tool,
2197
+ account_name: profile.name,
2198
+ ...profile.email ? { account_email: profile.email } : {},
2199
+ account_source: source
2200
+ };
2201
+ }
2202
+ function fromOverride(raw, agent) {
2203
+ const value = raw.trim();
2204
+ if (!value)
2205
+ return null;
2206
+ const candidateTool = AGENT_ACCOUNT_TOOLS[agent][0] ?? agent;
2207
+ const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
2208
+ if (!tool || !name)
2209
+ return null;
2210
+ return {
2211
+ account_key: accountKey(tool, name),
2212
+ account_tool: tool,
2213
+ account_name: name,
2214
+ account_source: "override"
2215
+ };
2216
+ }
2217
+ function envOverride(agent, env) {
2218
+ const agentPrefix = agent.toUpperCase().replace(/[^A-Z0-9]/g, "_");
2219
+ const raw = env[`ECONOMY_${agentPrefix}_ACCOUNT_KEY`] ?? env[`ECONOMY_${agentPrefix}_ACCOUNT`] ?? env["ECONOMY_ACCOUNT_KEY"] ?? env["ECONOMY_ACCOUNT"];
2220
+ if (raw)
2221
+ return fromOverride(raw, agent);
2222
+ const tool = env[`ECONOMY_${agentPrefix}_ACCOUNT_TOOL`] ?? env["ECONOMY_ACCOUNT_TOOL"];
2223
+ const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
2224
+ if (!tool || !name)
2225
+ return null;
2226
+ return {
2227
+ account_key: accountKey(tool, name),
2228
+ account_tool: tool,
2229
+ account_name: name,
2230
+ account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
2231
+ account_source: "override"
2232
+ };
2233
+ }
2234
+ function knownToolIds(api) {
2235
+ try {
2236
+ return new Set(api.listTools().map((tool) => tool.id));
2237
+ } catch {
2238
+ return new Set;
2239
+ }
2240
+ }
2241
+ function profileForEnvDir(api, tool, env) {
2242
+ const configuredDir = env[tool.envVar];
2243
+ if (!configuredDir)
2244
+ return null;
2245
+ const normalized = normalizeDir(configuredDir);
2246
+ try {
2247
+ return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
2248
+ } catch {
2249
+ return null;
2250
+ }
2251
+ }
2252
+ async function resolveAccountForAgent(agent, env = process.env) {
2253
+ const override = envOverride(agent, env);
2254
+ if (override)
2255
+ return override;
2256
+ let api;
2257
+ try {
2258
+ api = await import("@hasna/accounts");
2259
+ } catch {
2260
+ return null;
2261
+ }
2262
+ const toolIds = knownToolIds(api);
2263
+ for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
2264
+ if (!toolIds.has(toolId))
2265
+ continue;
2266
+ let tool;
2267
+ try {
2268
+ tool = api.getTool(toolId);
2269
+ } catch {
2270
+ continue;
2271
+ }
2272
+ const envProfile = profileForEnvDir(api, tool, env);
2273
+ if (envProfile)
2274
+ return fromProfile(envProfile, "env");
2275
+ try {
2276
+ const applied = api.appliedProfile(toolId);
2277
+ if (applied)
2278
+ return fromProfile(applied, "applied");
2279
+ } catch {}
2280
+ try {
2281
+ const current = api.currentProfile(toolId);
2282
+ if (current)
2283
+ return fromProfile(current, "current");
2284
+ } catch {}
2285
+ }
2286
+ return null;
2287
+ }
2288
+ function withAccount(record, account) {
2289
+ if (!account)
2290
+ return record;
2291
+ return {
2292
+ ...record,
2293
+ account_key: account.account_key,
2294
+ account_tool: account.account_tool,
2295
+ account_name: account.account_name,
2296
+ account_email: account.account_email ?? "",
2297
+ account_source: account.account_source
2298
+ };
2299
+ }
2300
+ var AGENT_ACCOUNT_TOOLS;
2301
+ var init_accounts = __esm(() => {
2302
+ AGENT_ACCOUNT_TOOLS = {
2303
+ claude: ["claude"],
2304
+ takumi: ["takumi", "claude"],
2305
+ codex: ["codex"],
2306
+ gemini: ["gemini"],
2307
+ opencode: ["opencode"],
2308
+ cursor: ["cursor"],
2309
+ pi: ["pi"],
2310
+ hermes: ["hermes"]
2311
+ };
2312
+ });
2313
+
1973
2314
  // src/ingest/claude.ts
1974
2315
  import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync2 } from "fs";
1975
2316
  import { homedir as homedir3 } from "os";
@@ -2012,6 +2353,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2012
2353
  let totalRequests = 0;
2013
2354
  const touchedSessions = new Set;
2014
2355
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
2356
+ const account = await resolveAccountForAgent(agentName);
2015
2357
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
2016
2358
  for (const projectDirEntry of projectDirs) {
2017
2359
  const projectDirPath = join6(projectsDir, projectDirEntry.name);
@@ -2075,7 +2417,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2075
2417
  }
2076
2418
  const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
2077
2419
  const reqId = `${agentName}-${sourceRequestId}`;
2078
- upsertRequest(db, {
2420
+ upsertRequest(db, withAccount({
2079
2421
  id: reqId,
2080
2422
  agent: agentName,
2081
2423
  session_id: sessionId,
@@ -2092,7 +2434,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2092
2434
  timestamp,
2093
2435
  source_request_id: sourceRequestId,
2094
2436
  machine_id: machineId
2095
- });
2437
+ }, account));
2096
2438
  if (!touchedSessions.has(sessionId)) {
2097
2439
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
2098
2440
  if (!existing) {
@@ -2110,7 +2452,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2110
2452
  request_count: 0,
2111
2453
  machine_id: machineId
2112
2454
  };
2113
- upsertSession(db, session);
2455
+ upsertSession(db, withAccount(session, account));
2114
2456
  }
2115
2457
  touchedSessions.add(sessionId);
2116
2458
  }
@@ -2150,6 +2492,7 @@ var CLAUDE_PROJECTS_DIR, TAKUMI_PROJECTS_DIR;
2150
2492
  var init_claude = __esm(() => {
2151
2493
  init_database();
2152
2494
  init_pricing();
2495
+ init_accounts();
2153
2496
  CLAUDE_PROJECTS_DIR = join6(homedir3(), ".claude", "projects");
2154
2497
  TAKUMI_PROJECTS_DIR = join6(homedir3(), ".takumi", "projects");
2155
2498
  });
@@ -2191,8 +2534,9 @@ function buildThreadQuery(codexDb) {
2191
2534
  function readTokenEvents(rolloutPath) {
2192
2535
  if (!rolloutPath || !existsSync5(rolloutPath))
2193
2536
  return [];
2194
- const events = [];
2195
- const seen = new Set;
2537
+ const fallbackUsages = new Map;
2538
+ let fallbackTimestamp;
2539
+ let aggregate = null;
2196
2540
  for (const line of readFileSync4(rolloutPath, "utf-8").split(`
2197
2541
  `)) {
2198
2542
  if (!line.trim())
@@ -2209,20 +2553,48 @@ function readTokenEvents(rolloutPath) {
2209
2553
  if (!payload || payload["type"] !== "token_count")
2210
2554
  continue;
2211
2555
  const info = payload["info"];
2556
+ const timestamp = entry["timestamp"];
2557
+ const entryTimestamp = typeof timestamp === "string" ? timestamp : undefined;
2558
+ const totalUsage = info?.["total_token_usage"];
2559
+ if (totalUsage && tokenTotal(totalUsage) > 0) {
2560
+ aggregate = { usage: totalUsage, timestamp: entryTimestamp };
2561
+ continue;
2562
+ }
2212
2563
  const usage = info?.["last_token_usage"];
2213
2564
  if (!usage)
2214
2565
  continue;
2215
- const total = usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
2216
- if (total <= 0)
2566
+ if (tokenTotal(usage) <= 0)
2217
2567
  continue;
2218
2568
  const key = JSON.stringify(usage);
2219
- if (seen.has(key))
2220
- continue;
2221
- seen.add(key);
2222
- const timestamp = entry["timestamp"];
2223
- events.push({ usage, timestamp: typeof timestamp === "string" ? timestamp : undefined });
2569
+ if (!fallbackUsages.has(key))
2570
+ fallbackUsages.set(key, usage);
2571
+ fallbackTimestamp = entryTimestamp ?? fallbackTimestamp;
2572
+ }
2573
+ if (aggregate)
2574
+ return [aggregate];
2575
+ if (fallbackUsages.size === 0)
2576
+ return [];
2577
+ return [{ usage: sumTokenUsages([...fallbackUsages.values()]), timestamp: fallbackTimestamp }];
2578
+ }
2579
+ function tokenTotal(usage) {
2580
+ return usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
2581
+ }
2582
+ function sumTokenUsages(usages) {
2583
+ const result = {
2584
+ input_tokens: 0,
2585
+ cached_input_tokens: 0,
2586
+ output_tokens: 0,
2587
+ reasoning_output_tokens: 0,
2588
+ total_tokens: 0
2589
+ };
2590
+ for (const usage of usages) {
2591
+ result.input_tokens = (result.input_tokens ?? 0) + (usage.input_tokens ?? 0);
2592
+ result.cached_input_tokens = (result.cached_input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
2593
+ result.output_tokens = (result.output_tokens ?? 0) + (usage.output_tokens ?? 0);
2594
+ result.reasoning_output_tokens = (result.reasoning_output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0);
2595
+ result.total_tokens = (result.total_tokens ?? 0) + tokenTotal(usage);
2224
2596
  }
2225
- return events;
2597
+ return result;
2226
2598
  }
2227
2599
  function fallbackEvents(totalTokens) {
2228
2600
  const inputTokens = Math.floor(totalTokens * 0.6);
@@ -2246,6 +2618,7 @@ async function ingestCodex(db, verbose = false) {
2246
2618
  let codexDb = null;
2247
2619
  let ingested = 0;
2248
2620
  let requests = 0;
2621
+ const account = await resolveAccountForAgent("codex");
2249
2622
  try {
2250
2623
  codexDb = new BunDatabase(dbPath, { readonly: true });
2251
2624
  const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
@@ -2260,7 +2633,7 @@ async function ingestCodex(db, verbose = false) {
2260
2633
  const sessionId = `codex-${thread.id}`;
2261
2634
  const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
2262
2635
  const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
2263
- upsertSession(db, {
2636
+ upsertSession(db, withAccount({
2264
2637
  id: sessionId,
2265
2638
  agent: "codex",
2266
2639
  project_path: projectPath,
@@ -2271,9 +2644,10 @@ async function ingestCodex(db, verbose = false) {
2271
2644
  total_tokens: 0,
2272
2645
  request_count: 0,
2273
2646
  machine_id: machineId
2274
- });
2647
+ }, account));
2275
2648
  const events = readTokenEvents(thread.rollout_path);
2276
2649
  const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
2650
+ const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
2277
2651
  db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
2278
2652
  tokenEvents.forEach((event, index) => {
2279
2653
  const usage = event.usage;
@@ -2284,7 +2658,7 @@ async function ingestCodex(db, verbose = false) {
2284
2658
  const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
2285
2659
  const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
2286
2660
  const requestId = `${sessionId}-${index}`;
2287
- upsertRequest(db, {
2661
+ upsertRequest(db, withAccount({
2288
2662
  id: requestId,
2289
2663
  agent: "codex",
2290
2664
  session_id: sessionId,
@@ -2299,24 +2673,25 @@ async function ingestCodex(db, verbose = false) {
2299
2673
  timestamp,
2300
2674
  source_request_id: requestId,
2301
2675
  machine_id: machineId
2302
- });
2676
+ }, account));
2303
2677
  requests++;
2304
2678
  });
2305
2679
  rollupSession(db, sessionId);
2306
2680
  setIngestState(db, "codex", thread.id, stateValue);
2307
2681
  ingested++;
2308
2682
  if (verbose)
2309
- console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens on ${model}`);
2683
+ console.log(`Codex session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
2310
2684
  }
2311
2685
  } finally {
2312
2686
  codexDb?.close();
2313
2687
  }
2314
2688
  return { sessions: ingested, requests };
2315
2689
  }
2316
- var DEFAULT_CODEX_DB_PATH, DEFAULT_CODEX_CONFIG_PATH, CODEX_INGEST_VERSION = "rollout-token-dedupe-v2";
2690
+ var DEFAULT_CODEX_DB_PATH, DEFAULT_CODEX_CONFIG_PATH, CODEX_INGEST_VERSION = "rollout-aggregate-v3";
2317
2691
  var init_codex = __esm(() => {
2318
2692
  init_database();
2319
2693
  init_pricing();
2694
+ init_accounts();
2320
2695
  DEFAULT_CODEX_DB_PATH = join7(homedir4(), ".codex", "state_5.sqlite");
2321
2696
  DEFAULT_CODEX_CONFIG_PATH = join7(homedir4(), ".codex", "config.toml");
2322
2697
  });
@@ -2376,6 +2751,7 @@ async function ingestGemini(db, verbose) {
2376
2751
  let totalSessions = 0;
2377
2752
  let totalRequests = 0;
2378
2753
  const touchedSessions = new Set;
2754
+ const account = await resolveAccountForAgent("gemini");
2379
2755
  const projectDirs = listProjectDirs(tmpDir, historyDir);
2380
2756
  for (const projectDir of projectDirs) {
2381
2757
  const chatsDir = join8(projectDir, "chats");
@@ -2424,7 +2800,7 @@ async function ingestGemini(db, verbose) {
2424
2800
  request_count: 0,
2425
2801
  machine_id: machineId
2426
2802
  };
2427
- upsertSession(db, session);
2803
+ upsertSession(db, withAccount(session, account));
2428
2804
  totalSessions++;
2429
2805
  }
2430
2806
  touchedSessions.add(sessionId);
@@ -2448,7 +2824,7 @@ async function ingestGemini(db, verbose) {
2448
2824
  const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
2449
2825
  const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
2450
2826
  const requestId = `gemini-${sessionId}-${message.id ?? index}`;
2451
- upsertRequest(db, {
2827
+ upsertRequest(db, withAccount({
2452
2828
  id: requestId,
2453
2829
  agent: "gemini",
2454
2830
  session_id: sessionId,
@@ -2463,7 +2839,7 @@ async function ingestGemini(db, verbose) {
2463
2839
  timestamp,
2464
2840
  source_request_id: message.id ?? requestId,
2465
2841
  machine_id: machineId
2466
- });
2842
+ }, account));
2467
2843
  totalRequests++;
2468
2844
  }
2469
2845
  setIngestState(db, "gemini", stateKey, fileMtime);
@@ -2478,6 +2854,7 @@ var DEFAULT_GEMINI_TMP_DIR, DEFAULT_GEMINI_HISTORY_DIR;
2478
2854
  var init_gemini = __esm(() => {
2479
2855
  init_database();
2480
2856
  init_pricing();
2857
+ init_accounts();
2481
2858
  DEFAULT_GEMINI_TMP_DIR = join8(homedir5(), ".gemini", "tmp");
2482
2859
  DEFAULT_GEMINI_HISTORY_DIR = join8(homedir5(), ".gemini", "history");
2483
2860
  });
@@ -2516,6 +2893,7 @@ async function ingestOpenCode(db, verbose = false) {
2516
2893
  const touched = new Set;
2517
2894
  const machineId = getMachineId();
2518
2895
  const now = new Date().toISOString();
2896
+ const account = await resolveAccountForAgent("opencode");
2519
2897
  for (const file of files) {
2520
2898
  const mtime = statSync4(file).mtimeMs;
2521
2899
  const stateKey = file;
@@ -2545,7 +2923,7 @@ async function ingestOpenCode(db, verbose = false) {
2545
2923
  const sourceId = file.replace(OPENCODE_STORAGE, "");
2546
2924
  const reqId = `opencode-${sourceId}`;
2547
2925
  const costUsd = usage.cost ?? computeCostFromDb(db, model, input, output, cacheRead, cacheWrite, 0);
2548
- upsertRequest(db, {
2926
+ upsertRequest(db, withAccount({
2549
2927
  id: reqId,
2550
2928
  agent: "opencode",
2551
2929
  session_id: sessionId,
@@ -2561,10 +2939,10 @@ async function ingestOpenCode(db, verbose = false) {
2561
2939
  source_request_id: sourceId,
2562
2940
  machine_id: machineId,
2563
2941
  updated_at: now
2564
- });
2942
+ }, account));
2565
2943
  requests++;
2566
2944
  if (!touched.has(sessionId)) {
2567
- upsertSession(db, {
2945
+ upsertSession(db, withAccount({
2568
2946
  id: sessionId,
2569
2947
  agent: "opencode",
2570
2948
  project_path: "",
@@ -2576,7 +2954,7 @@ async function ingestOpenCode(db, verbose = false) {
2576
2954
  request_count: 0,
2577
2955
  machine_id: machineId,
2578
2956
  updated_at: now
2579
- });
2957
+ }, account));
2580
2958
  touched.add(sessionId);
2581
2959
  }
2582
2960
  setIngestState(db, "opencode", stateKey, String(mtime));
@@ -2591,6 +2969,7 @@ var OPENCODE_STORAGE;
2591
2969
  var init_opencode = __esm(() => {
2592
2970
  init_database();
2593
2971
  init_pricing();
2972
+ init_accounts();
2594
2973
  OPENCODE_STORAGE = join9(homedir6(), ".local", "share", "opencode", "storage");
2595
2974
  });
2596
2975
 
@@ -2628,6 +3007,7 @@ async function ingestCursor(db, verbose = false) {
2628
3007
  const machineId = getMachineId();
2629
3008
  const now = new Date().toISOString();
2630
3009
  let snapshots = 0;
3010
+ const account = await resolveAccountForAgent("cursor");
2631
3011
  const usage = await cursorFetch("/api/usage", token);
2632
3012
  if (usage?.premiumRequests != null && usage.maxPremiumRequests) {
2633
3013
  upsertUsageSnapshot(db, {
@@ -2675,7 +3055,7 @@ async function ingestCursor(db, verbose = false) {
2675
3055
  }
2676
3056
  const sessionId = `cursor-${today}-${machineId}`;
2677
3057
  if (onDemand + included > 0) {
2678
- upsertSession(db, {
3058
+ upsertSession(db, withAccount({
2679
3059
  id: sessionId,
2680
3060
  agent: "cursor",
2681
3061
  project_path: "",
@@ -2687,8 +3067,8 @@ async function ingestCursor(db, verbose = false) {
2687
3067
  request_count: 1,
2688
3068
  machine_id: machineId,
2689
3069
  updated_at: now
2690
- });
2691
- upsertRequest(db, {
3070
+ }, account));
3071
+ upsertRequest(db, withAccount({
2692
3072
  id: `cursor-${today}-${machineId}-usage`,
2693
3073
  agent: "cursor",
2694
3074
  session_id: sessionId,
@@ -2704,7 +3084,7 @@ async function ingestCursor(db, verbose = false) {
2704
3084
  source_request_id: today,
2705
3085
  machine_id: machineId,
2706
3086
  updated_at: now
2707
- });
3087
+ }, account));
2708
3088
  rollupSession(db, sessionId);
2709
3089
  }
2710
3090
  setIngestState(db, "cursor", `sync-${today}`, now);
@@ -2714,6 +3094,7 @@ async function ingestCursor(db, verbose = false) {
2714
3094
  }
2715
3095
  var init_cursor = __esm(() => {
2716
3096
  init_database();
3097
+ init_accounts();
2717
3098
  });
2718
3099
 
2719
3100
  // src/ingest/pi.ts
@@ -2738,6 +3119,7 @@ async function ingestPi(db, verbose = false) {
2738
3119
  const touched = new Set;
2739
3120
  const machineId = getMachineId();
2740
3121
  const now = new Date().toISOString();
3122
+ const account = await resolveAccountForAgent("pi");
2741
3123
  for (const file of files) {
2742
3124
  const mtime = statSync5(file).mtimeMs;
2743
3125
  const prev = getIngestState(db, "pi", file);
@@ -2763,7 +3145,7 @@ async function ingestPi(db, verbose = false) {
2763
3145
  const model = turn.model ?? turn.provider ?? "unknown";
2764
3146
  const timestamp = turn.timestamp ?? new Date(statSync5(file).mtime).toISOString();
2765
3147
  const reqId = `pi-${sessionId}-${i}`;
2766
- upsertRequest(db, {
3148
+ upsertRequest(db, withAccount({
2767
3149
  id: reqId,
2768
3150
  agent: "pi",
2769
3151
  session_id: sessionId,
@@ -2779,11 +3161,11 @@ async function ingestPi(db, verbose = false) {
2779
3161
  source_request_id: `${sessionId}-${i}`,
2780
3162
  machine_id: machineId,
2781
3163
  updated_at: now
2782
- });
3164
+ }, account));
2783
3165
  requests++;
2784
3166
  }
2785
3167
  if (turns.length > 0) {
2786
- upsertSession(db, {
3168
+ upsertSession(db, withAccount({
2787
3169
  id: sessionId,
2788
3170
  agent: "pi",
2789
3171
  project_path: "",
@@ -2795,7 +3177,7 @@ async function ingestPi(db, verbose = false) {
2795
3177
  request_count: 0,
2796
3178
  machine_id: machineId,
2797
3179
  updated_at: now
2798
- });
3180
+ }, account));
2799
3181
  touched.add(sessionId);
2800
3182
  }
2801
3183
  setIngestState(db, "pi", file, String(mtime));
@@ -2809,6 +3191,7 @@ async function ingestPi(db, verbose = false) {
2809
3191
  var PI_SESSION_DIR;
2810
3192
  var init_pi = __esm(() => {
2811
3193
  init_database();
3194
+ init_accounts();
2812
3195
  PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join10(homedir7(), ".pi", "agent", "sessions");
2813
3196
  });
2814
3197
 
@@ -2846,13 +3229,14 @@ async function ingestHermes(db, verbose = false) {
2846
3229
  const machineId = getMachineId();
2847
3230
  const now = new Date().toISOString();
2848
3231
  let requests = 0;
3232
+ const account = await resolveAccountForAgent("hermes");
2849
3233
  for (const row of rows) {
2850
3234
  const sessionId = `hermes-${row.id}`;
2851
3235
  const startedAt = new Date(row.started_at * 1000).toISOString();
2852
3236
  const endedAt = row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null;
2853
3237
  const cost = row.actual_cost_usd ?? row.estimated_cost_usd ?? 0;
2854
3238
  const tokens = row.input_tokens + row.output_tokens + row.cache_read_tokens + row.cache_write_tokens + row.reasoning_tokens;
2855
- upsertSession(db, {
3239
+ upsertSession(db, withAccount({
2856
3240
  id: sessionId,
2857
3241
  agent: "hermes",
2858
3242
  project_path: row.source ?? "",
@@ -2864,9 +3248,9 @@ async function ingestHermes(db, verbose = false) {
2864
3248
  request_count: 1,
2865
3249
  machine_id: machineId,
2866
3250
  updated_at: now
2867
- });
3251
+ }, account));
2868
3252
  const reqId = `hermes-${row.id}-rollup`;
2869
- upsertRequest(db, {
3253
+ upsertRequest(db, withAccount({
2870
3254
  id: reqId,
2871
3255
  agent: "hermes",
2872
3256
  session_id: sessionId,
@@ -2882,7 +3266,7 @@ async function ingestHermes(db, verbose = false) {
2882
3266
  source_request_id: row.id,
2883
3267
  machine_id: machineId,
2884
3268
  updated_at: now
2885
- });
3269
+ }, account));
2886
3270
  requests++;
2887
3271
  rollupSession(db, sessionId);
2888
3272
  if (verbose)
@@ -2902,11 +3286,12 @@ function statSyncSafe(path) {
2902
3286
  var HERMES_DB;
2903
3287
  var init_hermes = __esm(() => {
2904
3288
  init_database();
3289
+ init_accounts();
2905
3290
  HERMES_DB = join11(homedir8(), ".hermes", "state.db");
2906
3291
  });
2907
3292
 
2908
3293
  // src/ingest/claude-quota.ts
2909
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
3294
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
2910
3295
  function readClaudeToken() {
2911
3296
  const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
2912
3297
  if (fromEnv)
@@ -2914,7 +3299,7 @@ function readClaudeToken() {
2914
3299
  if (!existsSync10(CREDENTIALS_PATH))
2915
3300
  return null;
2916
3301
  try {
2917
- const creds = JSON.parse(readFileSync9(CREDENTIALS_PATH, "utf-8"));
3302
+ const creds = JSON.parse(readFileSync8(CREDENTIALS_PATH, "utf-8"));
2918
3303
  const oauth = creds.claudeAiOauth;
2919
3304
  if (!oauth?.accessToken)
2920
3305
  return null;
@@ -3055,7 +3440,7 @@ var init_claude_quota = __esm(() => {
3055
3440
  });
3056
3441
 
3057
3442
  // src/ingest/codex-quota.ts
3058
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
3443
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
3059
3444
  function readCodexAuth() {
3060
3445
  const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
3061
3446
  if (fromEnv)
@@ -3064,7 +3449,7 @@ function readCodexAuth() {
3064
3449
  if (!existsSync11(authPath))
3065
3450
  return null;
3066
3451
  try {
3067
- const auth = JSON.parse(readFileSync10(authPath, "utf-8"));
3452
+ const auth = JSON.parse(readFileSync9(authPath, "utf-8"));
3068
3453
  const token = auth.tokens?.access_token;
3069
3454
  if (!token)
3070
3455
  return null;
@@ -3248,7 +3633,7 @@ __export(exports_billing, {
3248
3633
  syncGeminiBilling: () => syncGeminiBilling,
3249
3634
  syncAnthropicBilling: () => syncAnthropicBilling
3250
3635
  });
3251
- import { readFileSync as readFileSync11 } from "fs";
3636
+ import { readFileSync as readFileSync10 } from "fs";
3252
3637
  function getAnthropicAdminKey() {
3253
3638
  return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
3254
3639
  }
@@ -3442,7 +3827,7 @@ async function syncGeminiBilling(db, opts = {}) {
3442
3827
  const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
3443
3828
  const fromDateStr = toISODate(start);
3444
3829
  const toDateStr = toISODate(end);
3445
- const rows = parseBillingRows(readFileSync11(exportPath, "utf-8"));
3830
+ const rows = parseBillingRows(readFileSync10(exportPath, "utf-8"));
3446
3831
  clearBillingRange(db, "gemini", fromDateStr, toDateStr);
3447
3832
  const updatedAt = new Date().toISOString();
3448
3833
  let totalUsd = 0;
@@ -3511,7 +3896,7 @@ __export(exports_config, {
3511
3896
  loadConfig: () => loadConfig2,
3512
3897
  getConfigValue: () => getConfigValue
3513
3898
  });
3514
- import { existsSync as existsSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
3899
+ import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
3515
3900
  import { dirname as dirname2, join as join12 } from "path";
3516
3901
  function getConfigPath() {
3517
3902
  return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join12(getDataDir(), "config.json");
@@ -3520,7 +3905,7 @@ function loadConfig2() {
3520
3905
  try {
3521
3906
  const configPath = getConfigPath();
3522
3907
  if (existsSync12(configPath)) {
3523
- const raw = readFileSync12(configPath, "utf-8");
3908
+ const raw = readFileSync11(configPath, "utf-8");
3524
3909
  return { ...DEFAULTS, ...JSON.parse(raw) };
3525
3910
  }
3526
3911
  } catch {}
@@ -3976,6 +4361,7 @@ function createHandler(db) {
3976
4361
  const project = url.searchParams.get("project") ?? undefined;
3977
4362
  const search = url.searchParams.get("search") ?? undefined;
3978
4363
  const machine = url.searchParams.get("machine") ?? undefined;
4364
+ const account = url.searchParams.get("account") ?? undefined;
3979
4365
  const limit = Number(url.searchParams.get("limit") ?? 50);
3980
4366
  const offset = Number(url.searchParams.get("offset") ?? 0);
3981
4367
  const since = url.searchParams.get("since") ?? undefined;
@@ -3986,6 +4372,7 @@ function createHandler(db) {
3986
4372
  project,
3987
4373
  search,
3988
4374
  machine,
4375
+ account,
3989
4376
  limit,
3990
4377
  offset,
3991
4378
  since
@@ -4036,11 +4423,23 @@ function createHandler(db) {
4036
4423
  return ok(results);
4037
4424
  }
4038
4425
  if (path === "/api/projects" && method === "GET") {
4039
- return ok(queryProjectBreakdown(db));
4426
+ const period = url.searchParams.get("period") ?? "all";
4427
+ return ok(queryProjectBreakdown(db, period));
4428
+ }
4429
+ if (path === "/api/accounts" && method === "GET") {
4430
+ const period = url.searchParams.get("period") ?? "all";
4431
+ return ok(queryAccountBreakdown(db, period));
4040
4432
  }
4041
4433
  if (path === "/api/breakdown" && method === "GET") {
4042
4434
  const by = url.searchParams.get("by") ?? "model";
4043
- return ok(by === "project" ? queryProjectBreakdown(db) : queryModelBreakdown(db));
4435
+ const period = url.searchParams.get("period") ?? "all";
4436
+ if (by === "project")
4437
+ return ok(queryProjectBreakdown(db, period));
4438
+ if (by === "agent")
4439
+ return ok(queryAgentBreakdown(db, period));
4440
+ if (by === "account")
4441
+ return ok(queryAccountBreakdown(db, period));
4442
+ return ok(queryModelBreakdown(db));
4044
4443
  }
4045
4444
  if (path === "/api/budgets" && method === "GET") {
4046
4445
  return ok(getBudgetStatuses(db));
@@ -4175,6 +4574,50 @@ function createHandler(db) {
4175
4574
  const agent = url.searchParams.get("agent") ?? undefined;
4176
4575
  return ok(querySavingsSummary(db, period, agent && isAgent(agent) ? agent : undefined));
4177
4576
  }
4577
+ if (path === "/api/subscriptions" && method === "GET") {
4578
+ return ok(listSubscriptions(db));
4579
+ }
4580
+ if (path === "/api/subscriptions" && method === "POST") {
4581
+ const body = await jsonBody(req);
4582
+ if (!body)
4583
+ return err("invalid JSON body");
4584
+ const provider = optionalString(body["provider"])?.trim();
4585
+ const plan = optionalString(body["plan"])?.trim();
4586
+ if (!provider)
4587
+ return err("provider is required");
4588
+ if (!plan)
4589
+ return err("plan is required");
4590
+ const monthlyFee = finiteNumber(body["monthly_fee_usd"] ?? body["fee_usd"] ?? 0);
4591
+ const includedUsage = finiteNumber(body["included_usage_usd"] ?? 0);
4592
+ if (monthlyFee == null || monthlyFee < 0)
4593
+ return err("monthly_fee_usd must be a non-negative number");
4594
+ if (includedUsage == null || includedUsage < 0)
4595
+ return err("included_usage_usd must be a non-negative number");
4596
+ const agent = optionalAgent(body["agent"]);
4597
+ if (agent === undefined)
4598
+ return err(AGENT_ERROR);
4599
+ const now = new Date().toISOString();
4600
+ const subscription = {
4601
+ id: optionalString(body["id"])?.trim() || randomUUID2(),
4602
+ agent,
4603
+ provider,
4604
+ plan,
4605
+ monthly_fee_usd: monthlyFee,
4606
+ included_usage_usd: includedUsage,
4607
+ billing_cycle_start: optionalString(body["billing_cycle_start"]),
4608
+ reset_policy: optionalString(body["reset_policy"]) ?? "monthly",
4609
+ active: body["active"] === false || body["active"] === 0 ? 0 : 1,
4610
+ created_at: optionalString(body["created_at"]) ?? now,
4611
+ updated_at: now
4612
+ };
4613
+ upsertSubscription(db, subscription);
4614
+ return ok(subscription);
4615
+ }
4616
+ const subscriptionMatch = path.match(/^\/api\/subscriptions\/(.+)$/);
4617
+ if (subscriptionMatch && method === "DELETE") {
4618
+ deleteSubscription(db, decodeURIComponent(subscriptionMatch[1]));
4619
+ return ok({ ok: true });
4620
+ }
4178
4621
  const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
4179
4622
  if (sessionRequestsMatch && method === "GET") {
4180
4623
  const sessionId = decodeURIComponent(sessionRequestsMatch[1]);
@@ -6305,7 +6748,7 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
6305
6748
  ]));
6306
6749
  console.log();
6307
6750
  });
6308
- program.command("breakdown").description("Cost breakdown by model, agent, or project").option("--by <dimension>", "Dimension: model|agent|project", "model").option("--since <date>", "Filter since date or relative (e.g. 2026-03-01, 7d, 30d)").action((opts) => {
6751
+ program.command("breakdown").description("Cost breakdown by model, agent, project, or account").option("--by <dimension>", "Dimension: model|agent|project|account", "model").option("--since <date>", "Filter since date or relative (e.g. 2026-03-01, 7d, 30d)").action((opts) => {
6309
6752
  const db = openDatabase();
6310
6753
  const sinceDate = opts.since ? parseSinceDate(opts.since) : undefined;
6311
6754
  console.log();
@@ -6327,6 +6770,55 @@ program.command("breakdown").description("Cost breakdown by model, agent, or pro
6327
6770
  chalk7.cyan(fmtTokens(r.total_tokens)),
6328
6771
  fmt4(r.cost_usd)
6329
6772
  ]));
6773
+ } else if (opts.by === "agent") {
6774
+ const rows = sinceDate ? db.prepare(`
6775
+ SELECT agent,
6776
+ COUNT(DISTINCT session_id) as sessions,
6777
+ COUNT(*) as requests,
6778
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
6779
+ COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
6780
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
6781
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
6782
+ MAX(timestamp) as last_active
6783
+ FROM requests
6784
+ WHERE timestamp >= ?
6785
+ GROUP BY agent
6786
+ ORDER BY api_equivalent_usd DESC
6787
+ `).all(sinceDate) : queryAgentBreakdown(db);
6788
+ printTable(["Agent", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], rows.map((r) => [
6789
+ fmtAgent(r.agent),
6790
+ String(r.sessions),
6791
+ String(r.requests),
6792
+ chalk7.cyan(fmtTokens(r.total_tokens)),
6793
+ fmt4(r.api_equivalent_usd),
6794
+ fmt4(r.billable_usd),
6795
+ fmt4(r.subscription_included_usd)
6796
+ ]));
6797
+ } else if (opts.by === "account") {
6798
+ const rows = sinceDate ? db.prepare(`
6799
+ SELECT account_key, account_tool, account_name, account_email, account_source,
6800
+ COUNT(DISTINCT session_id) as sessions,
6801
+ COUNT(*) as requests,
6802
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
6803
+ COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
6804
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
6805
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
6806
+ MAX(timestamp) as last_active
6807
+ FROM requests
6808
+ WHERE timestamp >= ?
6809
+ AND (account_key != '' OR account_tool != '' OR account_name != '' OR account_email != '')
6810
+ GROUP BY account_key, account_tool, account_name, account_email, account_source
6811
+ ORDER BY api_equivalent_usd DESC
6812
+ `).all(sinceDate) : queryAccountBreakdown(db);
6813
+ printTable(["Account", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], rows.map((r) => [
6814
+ chalk7.white(r.account_key || r.account_name || chalk7.dim("unknown")),
6815
+ String(r.sessions),
6816
+ String(r.requests),
6817
+ chalk7.cyan(fmtTokens(r.total_tokens)),
6818
+ fmt4(r.api_equivalent_usd),
6819
+ fmt4("billable_usd" in r ? Number(r.billable_usd) : r.metered_api_usd),
6820
+ fmt4(r.subscription_included_usd)
6821
+ ]));
6330
6822
  } else {
6331
6823
  const rows = sinceDate ? db.prepare(`
6332
6824
  SELECT model, agent,