@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/index.js CHANGED
@@ -582,7 +582,12 @@ function initSchema(db) {
582
582
  duration_ms INTEGER DEFAULT 0,
583
583
  timestamp TEXT NOT NULL,
584
584
  source_request_id TEXT,
585
- machine_id TEXT DEFAULT ''
585
+ machine_id TEXT DEFAULT '',
586
+ account_key TEXT DEFAULT '',
587
+ account_tool TEXT DEFAULT '',
588
+ account_name TEXT DEFAULT '',
589
+ account_email TEXT DEFAULT '',
590
+ account_source TEXT DEFAULT ''
586
591
  );
587
592
 
588
593
  CREATE TABLE IF NOT EXISTS sessions (
@@ -595,7 +600,12 @@ function initSchema(db) {
595
600
  total_cost_usd REAL DEFAULT 0,
596
601
  total_tokens INTEGER DEFAULT 0,
597
602
  request_count INTEGER DEFAULT 0,
598
- machine_id TEXT DEFAULT ''
603
+ machine_id TEXT DEFAULT '',
604
+ account_key TEXT DEFAULT '',
605
+ account_tool TEXT DEFAULT '',
606
+ account_name TEXT DEFAULT '',
607
+ account_email TEXT DEFAULT '',
608
+ account_source TEXT DEFAULT ''
599
609
  );
600
610
 
601
611
  CREATE TABLE IF NOT EXISTS projects (
@@ -750,6 +760,11 @@ function initSchema(db) {
750
760
  if (!cols.some((c) => c.name === "synced_at")) {
751
761
  db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
752
762
  }
763
+ for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
764
+ if (!cols.some((c) => c.name === column)) {
765
+ db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
766
+ }
767
+ }
753
768
  const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
754
769
  if (!sessionCols.some((c) => c.name === "attribution_tag")) {
755
770
  db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
@@ -761,6 +776,11 @@ function initSchema(db) {
761
776
  if (!sessionCols.some((c) => c.name === "synced_at")) {
762
777
  db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
763
778
  }
779
+ for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
780
+ if (!sessionCols.some((c) => c.name === column)) {
781
+ db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
782
+ }
783
+ }
764
784
  const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
765
785
  if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
766
786
  db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
@@ -771,6 +791,8 @@ function initSchema(db) {
771
791
  db.exec(`
772
792
  CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
773
793
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
794
+ CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
795
+ CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
774
796
  `);
775
797
  }
776
798
  function periodWhere(period) {
@@ -805,6 +827,22 @@ function sessionPeriodWhere(period) {
805
827
  return "1=1";
806
828
  }
807
829
  }
830
+ function requestPeriodWhere(period) {
831
+ switch (period) {
832
+ case "today":
833
+ return `DATE(timestamp) = DATE('now')`;
834
+ case "yesterday":
835
+ return `DATE(timestamp) = DATE('now', '-1 day')`;
836
+ case "week":
837
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
838
+ case "month":
839
+ return `timestamp >= DATE('now', 'start of month')`;
840
+ case "year":
841
+ return `timestamp >= DATE('now', 'start of year')`;
842
+ case "all":
843
+ return "1=1";
844
+ }
845
+ }
808
846
  function upsertRequest(db, req) {
809
847
  const now = req.updated_at ?? new Date().toISOString();
810
848
  db.prepare(`
@@ -812,18 +850,20 @@ function upsertRequest(db, req) {
812
850
  (id, agent, session_id, model, input_tokens, output_tokens,
813
851
  cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
814
852
  cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
815
- source_request_id, machine_id, attribution_tag, updated_at, synced_at)
816
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
817
- `).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 ?? "");
853
+ source_request_id, machine_id, attribution_tag, account_key, account_tool,
854
+ account_name, account_email, account_source, updated_at, synced_at)
855
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
856
+ `).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 ?? "");
818
857
  }
819
858
  function upsertSession(db, session) {
820
859
  const now = session.updated_at ?? new Date().toISOString();
821
860
  db.prepare(`
822
861
  INSERT OR REPLACE INTO sessions
823
862
  (id, agent, project_path, project_name, started_at, ended_at,
824
- total_cost_usd, total_tokens, request_count, machine_id, attribution_tag, updated_at, synced_at)
825
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
826
- `).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 ?? "");
863
+ total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
864
+ account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
865
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
866
+ `).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 ?? "");
827
867
  }
828
868
  function rollupSession(db, sessionId) {
829
869
  db.prepare(`
@@ -834,9 +874,24 @@ function rollupSession(db, sessionId) {
834
874
  ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
835
875
  started_at = CASE WHEN started_at = '' OR started_at IS NULL
836
876
  THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
837
- ELSE started_at END
877
+ ELSE started_at END,
878
+ account_key = CASE WHEN account_key = '' OR account_key IS NULL
879
+ THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
880
+ ELSE account_key END,
881
+ account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
882
+ THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
883
+ ELSE account_tool END,
884
+ account_name = CASE WHEN account_name = '' OR account_name IS NULL
885
+ THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
886
+ ELSE account_name END,
887
+ account_email = CASE WHEN account_email = '' OR account_email IS NULL
888
+ THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
889
+ ELSE account_email END,
890
+ account_source = CASE WHEN account_source = '' OR account_source IS NULL
891
+ THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
892
+ ELSE account_source END
838
893
  WHERE id = ?
839
- `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
894
+ `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
840
895
  }
841
896
  function querySessions(db, filter = {}) {
842
897
  const conditions = [];
@@ -849,6 +904,11 @@ function querySessions(db, filter = {}) {
849
904
  conditions.push("project_path LIKE ?");
850
905
  params.push(`%${filter.project}%`);
851
906
  }
907
+ if (filter.account) {
908
+ const q = `%${filter.account}%`;
909
+ conditions.push("(account_key LIKE ? OR account_name LIKE ? OR account_email LIKE ?)");
910
+ params.push(q, q, q);
911
+ }
852
912
  if (filter.since) {
853
913
  conditions.push("started_at >= ?");
854
914
  params.push(filter.since);
@@ -913,6 +973,70 @@ function queryModelBreakdown(db) {
913
973
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
914
974
  `).all();
915
975
  }
976
+ function queryAgentBreakdown(db, period = "all") {
977
+ const requestWhere = requestPeriodWhere(period);
978
+ const groups = new Map;
979
+ const requestRows = db.prepare(`
980
+ SELECT agent,
981
+ COUNT(DISTINCT session_id) as sessions,
982
+ COUNT(*) as requests,
983
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
984
+ COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
985
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
986
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
987
+ COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
988
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
989
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
990
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
991
+ MAX(timestamp) as last_active
992
+ FROM requests
993
+ WHERE ${requestWhere}
994
+ GROUP BY agent
995
+ ORDER BY api_equivalent_usd DESC
996
+ `).all();
997
+ for (const row of requestRows) {
998
+ groups.set(row.agent, row);
999
+ }
1000
+ const sessionWhere = sessionPeriodWhere(period);
1001
+ const sessionOnlyRows = db.prepare(`
1002
+ SELECT agent,
1003
+ COUNT(*) as sessions,
1004
+ COALESCE(SUM(request_count), 0) as requests,
1005
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
1006
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1007
+ MAX(started_at) as last_active
1008
+ FROM sessions
1009
+ WHERE ${sessionWhere}
1010
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1011
+ GROUP BY agent
1012
+ `).all();
1013
+ for (const row of sessionOnlyRows) {
1014
+ const existing = groups.get(row.agent) ?? {
1015
+ agent: row.agent,
1016
+ sessions: 0,
1017
+ requests: 0,
1018
+ total_tokens: 0,
1019
+ api_equivalent_usd: 0,
1020
+ billable_usd: 0,
1021
+ metered_api_usd: 0,
1022
+ subscription_included_usd: 0,
1023
+ estimated_usd: 0,
1024
+ unknown_usd: 0,
1025
+ cost_usd: 0,
1026
+ last_active: ""
1027
+ };
1028
+ existing.sessions += row.sessions;
1029
+ existing.requests += row.requests;
1030
+ existing.total_tokens += row.total_tokens;
1031
+ existing.api_equivalent_usd += row.cost_usd;
1032
+ existing.estimated_usd += row.cost_usd;
1033
+ existing.cost_usd += row.cost_usd;
1034
+ if (!existing.last_active || row.last_active > existing.last_active)
1035
+ existing.last_active = row.last_active;
1036
+ groups.set(row.agent, existing);
1037
+ }
1038
+ return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
1039
+ }
916
1040
  function labelForPath(projectPath, projectName) {
917
1041
  if (projectName && projectName.trim() !== "")
918
1042
  return projectName;
@@ -931,11 +1055,13 @@ function labelForPath(projectPath, projectName) {
931
1055
  }
932
1056
  return segments[segments.length - 1] ?? projectPath;
933
1057
  }
934
- function queryProjectBreakdown(db) {
1058
+ function queryProjectBreakdown(db, period = "all") {
1059
+ const where = sessionPeriodWhere(period);
935
1060
  const sessions = db.prepare(`
936
1061
  SELECT id, project_path, project_name, total_cost_usd, started_at
937
1062
  FROM sessions
938
- WHERE project_path != '' OR project_name != ''
1063
+ WHERE ${where}
1064
+ AND (project_path != '' OR project_name != '')
939
1065
  `).all();
940
1066
  const groups = new Map;
941
1067
  for (const s of sessions) {
@@ -974,6 +1100,87 @@ function queryProjectBreakdown(db) {
974
1100
  result.sort((a, b) => b.cost_usd - a.cost_usd);
975
1101
  return result;
976
1102
  }
1103
+ function queryAccountBreakdown(db, period = "all") {
1104
+ const sWhere = sessionPeriodWhere(period);
1105
+ const sessions = db.prepare(`
1106
+ SELECT id, account_key, account_tool, account_name, account_email, account_source,
1107
+ total_cost_usd, total_tokens, request_count, started_at
1108
+ FROM sessions
1109
+ WHERE ${sWhere}
1110
+ AND (account_key != '' OR account_tool != '' OR account_name != '' OR account_email != '')
1111
+ `).all();
1112
+ const groups = new Map;
1113
+ for (const session of sessions) {
1114
+ const key = session.account_key || `${session.account_tool}:${session.account_name}`;
1115
+ if (!key || key === ":")
1116
+ continue;
1117
+ const group = groups.get(key) ?? {
1118
+ sessionIds: [],
1119
+ account_tool: session.account_tool,
1120
+ account_name: session.account_name,
1121
+ account_email: session.account_email || null,
1122
+ account_source: session.account_source || "unknown",
1123
+ totalCost: 0,
1124
+ totalTokens: 0,
1125
+ requests: 0,
1126
+ lastActive: ""
1127
+ };
1128
+ group.sessionIds.push(session.id);
1129
+ group.totalCost += session.total_cost_usd || 0;
1130
+ group.totalTokens += session.total_tokens || 0;
1131
+ group.requests += session.request_count || 0;
1132
+ if (!group.lastActive || session.started_at > group.lastActive)
1133
+ group.lastActive = session.started_at;
1134
+ groups.set(key, group);
1135
+ }
1136
+ const result = [];
1137
+ for (const [key, group] of groups.entries()) {
1138
+ const placeholders = group.sessionIds.map(() => "?").join(",");
1139
+ const reqStats = placeholders ? db.prepare(`
1140
+ SELECT
1141
+ COUNT(*) as requests,
1142
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
1143
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1144
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1145
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1146
+ COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1147
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd
1148
+ FROM requests WHERE session_id IN (${placeholders})
1149
+ `).get(...group.sessionIds) : {
1150
+ requests: 0,
1151
+ cost_usd: 0,
1152
+ total_tokens: 0,
1153
+ metered_api_usd: 0,
1154
+ subscription_included_usd: 0,
1155
+ estimated_usd: 0,
1156
+ unknown_usd: 0
1157
+ };
1158
+ const hasRequestCosts = reqStats.requests > 0;
1159
+ const apiEquivalentUsd = hasRequestCosts ? reqStats.cost_usd : group.totalCost;
1160
+ const estimatedUsd = hasRequestCosts ? reqStats.estimated_usd : group.totalCost;
1161
+ const billableUsd = reqStats.metered_api_usd;
1162
+ result.push({
1163
+ account_key: key,
1164
+ account_tool: group.account_tool,
1165
+ account_name: group.account_name,
1166
+ account_email: group.account_email,
1167
+ account_source: group.account_source,
1168
+ sessions: group.sessionIds.length,
1169
+ requests: reqStats.requests || group.requests,
1170
+ total_tokens: reqStats.total_tokens || group.totalTokens,
1171
+ api_equivalent_usd: apiEquivalentUsd,
1172
+ billable_usd: billableUsd,
1173
+ metered_api_usd: reqStats.metered_api_usd,
1174
+ subscription_included_usd: reqStats.subscription_included_usd,
1175
+ estimated_usd: estimatedUsd,
1176
+ unknown_usd: reqStats.unknown_usd,
1177
+ cost_usd: apiEquivalentUsd,
1178
+ last_active: group.lastActive
1179
+ });
1180
+ }
1181
+ result.sort((a, b) => b.cost_usd - a.cost_usd);
1182
+ return result;
1183
+ }
977
1184
  function queryDailyBreakdown(db, days = 30) {
978
1185
  return db.prepare(`
979
1186
  SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
@@ -1511,6 +1718,131 @@ function defaultCostBasisForAgent(agent) {
1511
1718
  return "estimated";
1512
1719
  }
1513
1720
 
1721
+ // src/lib/accounts.ts
1722
+ var AGENT_ACCOUNT_TOOLS = {
1723
+ claude: ["claude"],
1724
+ takumi: ["takumi", "claude"],
1725
+ codex: ["codex"],
1726
+ gemini: ["gemini"],
1727
+ opencode: ["opencode"],
1728
+ cursor: ["cursor"],
1729
+ pi: ["pi"],
1730
+ hermes: ["hermes"]
1731
+ };
1732
+ function accountKey(tool, name) {
1733
+ return `${tool}:${name}`;
1734
+ }
1735
+ function normalizeDir(value) {
1736
+ return value.replace(/\/+$/, "");
1737
+ }
1738
+ function fromProfile(profile, source) {
1739
+ return {
1740
+ account_key: accountKey(profile.tool, profile.name),
1741
+ account_tool: profile.tool,
1742
+ account_name: profile.name,
1743
+ ...profile.email ? { account_email: profile.email } : {},
1744
+ account_source: source
1745
+ };
1746
+ }
1747
+ function fromOverride(raw, agent) {
1748
+ const value = raw.trim();
1749
+ if (!value)
1750
+ return null;
1751
+ const candidateTool = AGENT_ACCOUNT_TOOLS[agent][0] ?? agent;
1752
+ const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
1753
+ if (!tool || !name)
1754
+ return null;
1755
+ return {
1756
+ account_key: accountKey(tool, name),
1757
+ account_tool: tool,
1758
+ account_name: name,
1759
+ account_source: "override"
1760
+ };
1761
+ }
1762
+ function envOverride(agent, env) {
1763
+ const agentPrefix = agent.toUpperCase().replace(/[^A-Z0-9]/g, "_");
1764
+ const raw = env[`ECONOMY_${agentPrefix}_ACCOUNT_KEY`] ?? env[`ECONOMY_${agentPrefix}_ACCOUNT`] ?? env["ECONOMY_ACCOUNT_KEY"] ?? env["ECONOMY_ACCOUNT"];
1765
+ if (raw)
1766
+ return fromOverride(raw, agent);
1767
+ const tool = env[`ECONOMY_${agentPrefix}_ACCOUNT_TOOL`] ?? env["ECONOMY_ACCOUNT_TOOL"];
1768
+ const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
1769
+ if (!tool || !name)
1770
+ return null;
1771
+ return {
1772
+ account_key: accountKey(tool, name),
1773
+ account_tool: tool,
1774
+ account_name: name,
1775
+ account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
1776
+ account_source: "override"
1777
+ };
1778
+ }
1779
+ function knownToolIds(api) {
1780
+ try {
1781
+ return new Set(api.listTools().map((tool) => tool.id));
1782
+ } catch {
1783
+ return new Set;
1784
+ }
1785
+ }
1786
+ function profileForEnvDir(api, tool, env) {
1787
+ const configuredDir = env[tool.envVar];
1788
+ if (!configuredDir)
1789
+ return null;
1790
+ const normalized = normalizeDir(configuredDir);
1791
+ try {
1792
+ return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
1793
+ } catch {
1794
+ return null;
1795
+ }
1796
+ }
1797
+ async function resolveAccountForAgent(agent, env = process.env) {
1798
+ const override = envOverride(agent, env);
1799
+ if (override)
1800
+ return override;
1801
+ let api;
1802
+ try {
1803
+ api = await import("@hasna/accounts");
1804
+ } catch {
1805
+ return null;
1806
+ }
1807
+ const toolIds = knownToolIds(api);
1808
+ for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
1809
+ if (!toolIds.has(toolId))
1810
+ continue;
1811
+ let tool;
1812
+ try {
1813
+ tool = api.getTool(toolId);
1814
+ } catch {
1815
+ continue;
1816
+ }
1817
+ const envProfile = profileForEnvDir(api, tool, env);
1818
+ if (envProfile)
1819
+ return fromProfile(envProfile, "env");
1820
+ try {
1821
+ const applied = api.appliedProfile(toolId);
1822
+ if (applied)
1823
+ return fromProfile(applied, "applied");
1824
+ } catch {}
1825
+ try {
1826
+ const current = api.currentProfile(toolId);
1827
+ if (current)
1828
+ return fromProfile(current, "current");
1829
+ } catch {}
1830
+ }
1831
+ return null;
1832
+ }
1833
+ function withAccount(record, account) {
1834
+ if (!account)
1835
+ return record;
1836
+ return {
1837
+ ...record,
1838
+ account_key: account.account_key,
1839
+ account_tool: account.account_tool,
1840
+ account_name: account.account_name,
1841
+ account_email: account.account_email ?? "",
1842
+ account_source: account.account_source
1843
+ };
1844
+ }
1845
+
1514
1846
  // src/ingest/claude.ts
1515
1847
  function autoDetectProject(cwd, projects) {
1516
1848
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
@@ -1552,6 +1884,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1552
1884
  let totalRequests = 0;
1553
1885
  const touchedSessions = new Set;
1554
1886
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
1887
+ const account = await resolveAccountForAgent(agentName);
1555
1888
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
1556
1889
  for (const projectDirEntry of projectDirs) {
1557
1890
  const projectDirPath = join3(projectsDir, projectDirEntry.name);
@@ -1615,7 +1948,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1615
1948
  }
1616
1949
  const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
1617
1950
  const reqId = `${agentName}-${sourceRequestId}`;
1618
- upsertRequest(db, {
1951
+ upsertRequest(db, withAccount({
1619
1952
  id: reqId,
1620
1953
  agent: agentName,
1621
1954
  session_id: sessionId,
@@ -1632,7 +1965,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1632
1965
  timestamp,
1633
1966
  source_request_id: sourceRequestId,
1634
1967
  machine_id: machineId
1635
- });
1968
+ }, account));
1636
1969
  if (!touchedSessions.has(sessionId)) {
1637
1970
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
1638
1971
  if (!existing) {
@@ -1650,7 +1983,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1650
1983
  request_count: 0,
1651
1984
  machine_id: machineId
1652
1985
  };
1653
- upsertSession(db, session);
1986
+ upsertSession(db, withAccount(session, account));
1654
1987
  }
1655
1988
  touchedSessions.add(sessionId);
1656
1989
  }
@@ -1695,7 +2028,7 @@ import { join as join4, basename as basename2 } from "path";
1695
2028
  import { Database as BunDatabase } from "bun:sqlite";
1696
2029
  var DEFAULT_CODEX_DB_PATH = join4(homedir3(), ".codex", "state_5.sqlite");
1697
2030
  var DEFAULT_CODEX_CONFIG_PATH = join4(homedir3(), ".codex", "config.toml");
1698
- var CODEX_INGEST_VERSION = "rollout-token-dedupe-v2";
2031
+ var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
1699
2032
  function codexDbPath() {
1700
2033
  return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
1701
2034
  }
@@ -1728,8 +2061,9 @@ function buildThreadQuery(codexDb) {
1728
2061
  function readTokenEvents(rolloutPath) {
1729
2062
  if (!rolloutPath || !existsSync4(rolloutPath))
1730
2063
  return [];
1731
- const events = [];
1732
- const seen = new Set;
2064
+ const fallbackUsages = new Map;
2065
+ let fallbackTimestamp;
2066
+ let aggregate = null;
1733
2067
  for (const line of readFileSync3(rolloutPath, "utf-8").split(`
1734
2068
  `)) {
1735
2069
  if (!line.trim())
@@ -1746,20 +2080,48 @@ function readTokenEvents(rolloutPath) {
1746
2080
  if (!payload || payload["type"] !== "token_count")
1747
2081
  continue;
1748
2082
  const info = payload["info"];
2083
+ const timestamp = entry["timestamp"];
2084
+ const entryTimestamp = typeof timestamp === "string" ? timestamp : undefined;
2085
+ const totalUsage = info?.["total_token_usage"];
2086
+ if (totalUsage && tokenTotal(totalUsage) > 0) {
2087
+ aggregate = { usage: totalUsage, timestamp: entryTimestamp };
2088
+ continue;
2089
+ }
1749
2090
  const usage = info?.["last_token_usage"];
1750
2091
  if (!usage)
1751
2092
  continue;
1752
- const total = usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
1753
- if (total <= 0)
2093
+ if (tokenTotal(usage) <= 0)
1754
2094
  continue;
1755
2095
  const key = JSON.stringify(usage);
1756
- if (seen.has(key))
1757
- continue;
1758
- seen.add(key);
1759
- const timestamp = entry["timestamp"];
1760
- events.push({ usage, timestamp: typeof timestamp === "string" ? timestamp : undefined });
2096
+ if (!fallbackUsages.has(key))
2097
+ fallbackUsages.set(key, usage);
2098
+ fallbackTimestamp = entryTimestamp ?? fallbackTimestamp;
1761
2099
  }
1762
- return events;
2100
+ if (aggregate)
2101
+ return [aggregate];
2102
+ if (fallbackUsages.size === 0)
2103
+ return [];
2104
+ return [{ usage: sumTokenUsages([...fallbackUsages.values()]), timestamp: fallbackTimestamp }];
2105
+ }
2106
+ function tokenTotal(usage) {
2107
+ return usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
2108
+ }
2109
+ function sumTokenUsages(usages) {
2110
+ const result = {
2111
+ input_tokens: 0,
2112
+ cached_input_tokens: 0,
2113
+ output_tokens: 0,
2114
+ reasoning_output_tokens: 0,
2115
+ total_tokens: 0
2116
+ };
2117
+ for (const usage of usages) {
2118
+ result.input_tokens = (result.input_tokens ?? 0) + (usage.input_tokens ?? 0);
2119
+ result.cached_input_tokens = (result.cached_input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
2120
+ result.output_tokens = (result.output_tokens ?? 0) + (usage.output_tokens ?? 0);
2121
+ result.reasoning_output_tokens = (result.reasoning_output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0);
2122
+ result.total_tokens = (result.total_tokens ?? 0) + tokenTotal(usage);
2123
+ }
2124
+ return result;
1763
2125
  }
1764
2126
  function fallbackEvents(totalTokens) {
1765
2127
  const inputTokens = Math.floor(totalTokens * 0.6);
@@ -1783,6 +2145,7 @@ async function ingestCodex(db, verbose = false) {
1783
2145
  let codexDb = null;
1784
2146
  let ingested = 0;
1785
2147
  let requests = 0;
2148
+ const account = await resolveAccountForAgent("codex");
1786
2149
  try {
1787
2150
  codexDb = new BunDatabase(dbPath, { readonly: true });
1788
2151
  const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
@@ -1797,7 +2160,7 @@ async function ingestCodex(db, verbose = false) {
1797
2160
  const sessionId = `codex-${thread.id}`;
1798
2161
  const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
1799
2162
  const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
1800
- upsertSession(db, {
2163
+ upsertSession(db, withAccount({
1801
2164
  id: sessionId,
1802
2165
  agent: "codex",
1803
2166
  project_path: projectPath,
@@ -1808,9 +2171,10 @@ async function ingestCodex(db, verbose = false) {
1808
2171
  total_tokens: 0,
1809
2172
  request_count: 0,
1810
2173
  machine_id: machineId
1811
- });
2174
+ }, account));
1812
2175
  const events = readTokenEvents(thread.rollout_path);
1813
2176
  const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
2177
+ const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
1814
2178
  db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
1815
2179
  tokenEvents.forEach((event, index) => {
1816
2180
  const usage = event.usage;
@@ -1821,7 +2185,7 @@ async function ingestCodex(db, verbose = false) {
1821
2185
  const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
1822
2186
  const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
1823
2187
  const requestId = `${sessionId}-${index}`;
1824
- upsertRequest(db, {
2188
+ upsertRequest(db, withAccount({
1825
2189
  id: requestId,
1826
2190
  agent: "codex",
1827
2191
  session_id: sessionId,
@@ -1836,14 +2200,14 @@ async function ingestCodex(db, verbose = false) {
1836
2200
  timestamp,
1837
2201
  source_request_id: requestId,
1838
2202
  machine_id: machineId
1839
- });
2203
+ }, account));
1840
2204
  requests++;
1841
2205
  });
1842
2206
  rollupSession(db, sessionId);
1843
2207
  setIngestState(db, "codex", thread.id, stateValue);
1844
2208
  ingested++;
1845
2209
  if (verbose)
1846
- console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens on ${model}`);
2210
+ console.log(`Codex session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
1847
2211
  }
1848
2212
  } finally {
1849
2213
  codexDb?.close();
@@ -1909,6 +2273,7 @@ async function ingestGemini(db, verbose) {
1909
2273
  let totalSessions = 0;
1910
2274
  let totalRequests = 0;
1911
2275
  const touchedSessions = new Set;
2276
+ const account = await resolveAccountForAgent("gemini");
1912
2277
  const projectDirs = listProjectDirs(tmpDir, historyDir);
1913
2278
  for (const projectDir of projectDirs) {
1914
2279
  const chatsDir = join5(projectDir, "chats");
@@ -1957,7 +2322,7 @@ async function ingestGemini(db, verbose) {
1957
2322
  request_count: 0,
1958
2323
  machine_id: machineId
1959
2324
  };
1960
- upsertSession(db, session);
2325
+ upsertSession(db, withAccount(session, account));
1961
2326
  totalSessions++;
1962
2327
  }
1963
2328
  touchedSessions.add(sessionId);
@@ -1981,7 +2346,7 @@ async function ingestGemini(db, verbose) {
1981
2346
  const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
1982
2347
  const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
1983
2348
  const requestId = `gemini-${sessionId}-${message.id ?? index}`;
1984
- upsertRequest(db, {
2349
+ upsertRequest(db, withAccount({
1985
2350
  id: requestId,
1986
2351
  agent: "gemini",
1987
2352
  session_id: sessionId,
@@ -1996,7 +2361,7 @@ async function ingestGemini(db, verbose) {
1996
2361
  timestamp,
1997
2362
  source_request_id: message.id ?? requestId,
1998
2363
  machine_id: machineId
1999
- });
2364
+ }, account));
2000
2365
  totalRequests++;
2001
2366
  }
2002
2367
  setIngestState(db, "gemini", stateKey, fileMtime);
@@ -2032,6 +2397,8 @@ export {
2032
2397
  queryModelBreakdown,
2033
2398
  queryDailyBreakdown,
2034
2399
  queryBillingSummary,
2400
+ queryAgentBreakdown,
2401
+ queryAccountBreakdown,
2035
2402
  openDatabase,
2036
2403
  normalizeModelName,
2037
2404
  listSubscriptions,
@@ -1 +1 @@
1
- {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/ingest/claude.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAW7D,OAAO,KAAK,EAAkB,KAAK,EAAE,MAAM,mBAAmB,CAAA;AA+D9D,wBAAsB,YAAY,CAChC,EAAE,EAAE,QAAQ,EACZ,OAAO,UAAQ,EACf,WAAW,SAAsB,GAChC,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAEhE;AAED,wBAAsB,YAAY,CAChC,EAAE,EAAE,QAAQ,EACZ,OAAO,UAAQ,EACf,WAAW,SAAsB,GAChC,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAEhE;AAED,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,QAAQ,EACZ,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,KAAK,EAChB,OAAO,UAAQ,GACd,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAwIhE"}
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/ingest/claude.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAY7D,OAAO,KAAK,EAAkB,KAAK,EAAE,MAAM,mBAAmB,CAAA;AA+D9D,wBAAsB,YAAY,CAChC,EAAE,EAAE,QAAQ,EACZ,OAAO,UAAQ,EACf,WAAW,SAAsB,GAChC,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAEhE;AAED,wBAAsB,YAAY,CAChC,EAAE,EAAE,QAAQ,EACZ,OAAO,UAAQ,EACf,WAAW,SAAsB,GAChC,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAEhE;AAED,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,QAAQ,EACZ,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,KAAK,EAChB,OAAO,UAAQ,GACd,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAyIhE"}
@@ -1 +1 @@
1
- {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/ingest/codex.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AA4C7D,iBAAS,cAAc,IAAI,MAAM,CAUhC;AAsDD,wBAAsB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,UAAQ,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA0FhH;AAED,OAAO,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/ingest/codex.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AA6C7D,iBAAS,cAAc,IAAI,MAAM,CAUhC;AAoFD,wBAAsB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,UAAQ,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA4FhH;AAED,OAAO,EAAE,cAAc,EAAE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../src/ingest/cursor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAsC7D,wBAAsB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,UAAQ,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAoGlH"}
1
+ {"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../src/ingest/cursor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAsC7D,wBAAsB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,UAAQ,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAqGlH"}