@hasna/economy 0.2.27 → 0.2.29

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.
@@ -566,6 +566,26 @@ function openDatabase(dbPath, skipSeed = false) {
566
566
  }
567
567
  return db;
568
568
  }
569
+ function quoteSqlIdent(identifier) {
570
+ return `"${identifier.replace(/"/g, '""')}"`;
571
+ }
572
+ function hasColumn(db, table, column) {
573
+ const columns = db.prepare(`PRAGMA table_info(${quoteSqlIdent(table)})`).all();
574
+ return columns.some((c) => c.name === column);
575
+ }
576
+ function addColumnIfMissing(db, table, column, definition) {
577
+ if (hasColumn(db, table, column))
578
+ return false;
579
+ try {
580
+ db.exec(`ALTER TABLE ${quoteSqlIdent(table)} ADD COLUMN ${quoteSqlIdent(column)} ${definition}`);
581
+ return true;
582
+ } catch (error) {
583
+ const message = error instanceof Error ? error.message : String(error);
584
+ if (/duplicate column name/i.test(message))
585
+ return true;
586
+ throw error;
587
+ }
588
+ }
569
589
  function initSchema(db) {
570
590
  db.exec(`
571
591
  CREATE TABLE IF NOT EXISTS requests (
@@ -736,59 +756,31 @@ function initSchema(db) {
736
756
  CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
737
757
  CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
738
758
  `);
739
- const cols = db.prepare(`PRAGMA table_info(requests)`).all();
740
- if (!cols.some((c) => c.name === "machine_id")) {
741
- db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
742
- db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
743
- }
744
- if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
745
- db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
759
+ addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
760
+ addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
761
+ if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
746
762
  db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
747
763
  }
748
- if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
749
- db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
750
- }
751
- if (!cols.some((c) => c.name === "cost_basis")) {
752
- db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
753
- }
754
- if (!cols.some((c) => c.name === "attribution_tag")) {
755
- db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
756
- }
757
- if (!cols.some((c) => c.name === "updated_at")) {
758
- db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
764
+ addColumnIfMissing(db, "requests", "cache_create_1h_tokens", "INTEGER DEFAULT 0");
765
+ addColumnIfMissing(db, "requests", "cost_basis", `TEXT DEFAULT 'estimated'`);
766
+ addColumnIfMissing(db, "requests", "attribution_tag", `TEXT DEFAULT ''`);
767
+ if (addColumnIfMissing(db, "requests", "updated_at", `TEXT DEFAULT ''`)) {
759
768
  db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
760
769
  }
761
- if (!cols.some((c) => c.name === "synced_at")) {
762
- db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
763
- }
770
+ addColumnIfMissing(db, "requests", "synced_at", `TEXT DEFAULT ''`);
764
771
  for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
765
- if (!cols.some((c) => c.name === column)) {
766
- db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
767
- }
768
- }
769
- const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
770
- if (!sessionCols.some((c) => c.name === "attribution_tag")) {
771
- db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
772
+ addColumnIfMissing(db, "requests", column, `TEXT DEFAULT ''`);
772
773
  }
773
- if (!sessionCols.some((c) => c.name === "updated_at")) {
774
- db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
774
+ addColumnIfMissing(db, "sessions", "attribution_tag", `TEXT DEFAULT ''`);
775
+ if (addColumnIfMissing(db, "sessions", "updated_at", `TEXT DEFAULT ''`)) {
775
776
  db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
776
777
  }
777
- if (!sessionCols.some((c) => c.name === "synced_at")) {
778
- db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
779
- }
778
+ addColumnIfMissing(db, "sessions", "synced_at", `TEXT DEFAULT ''`);
780
779
  for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
781
- if (!sessionCols.some((c) => c.name === column)) {
782
- db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
783
- }
784
- }
785
- const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
786
- if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
787
- db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
788
- }
789
- if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
790
- db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
780
+ addColumnIfMissing(db, "sessions", column, `TEXT DEFAULT ''`);
791
781
  }
782
+ addColumnIfMissing(db, "model_pricing", "cache_write_1h_per_1m", "REAL NOT NULL DEFAULT 0");
783
+ addColumnIfMissing(db, "model_pricing", "cache_storage_per_1m_hour", "REAL NOT NULL DEFAULT 0");
792
784
  db.exec(`
793
785
  CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
794
786
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
@@ -949,17 +941,22 @@ function querySummary(db, period, machine, allMachines = false) {
949
941
  const codexTotals = db.prepare(`
950
942
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
951
943
  COALESCE(SUM(total_tokens), 0) as tokens,
944
+ COALESCE(SUM(request_count), 0) as requests,
952
945
  COUNT(*) as sessions
953
946
  FROM sessions
954
947
  WHERE ${sWhere}${machineClause}
955
948
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
956
949
  `).get();
957
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
950
+ const requestSessionCount = db.prepare(`
951
+ SELECT COUNT(DISTINCT session_id) as sessions
952
+ FROM requests
953
+ WHERE ${rWhere}${machineClause}
954
+ `).get();
958
955
  return {
959
956
  total_usd: r.total_usd + codexTotals.cost_usd,
960
- requests: r.requests,
957
+ requests: r.requests + codexTotals.requests,
961
958
  tokens: r.tokens + codexTotals.tokens,
962
- sessions: sessionCount.sessions,
959
+ sessions: requestSessionCount.sessions + codexTotals.sessions,
963
960
  period
964
961
  };
965
962
  }
@@ -974,8 +971,10 @@ function queryModelBreakdown(db) {
974
971
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
975
972
  `).all();
976
973
  }
977
- function queryAgentBreakdown(db, period = "all") {
974
+ function queryAgentBreakdown(db, period = "all", machine) {
978
975
  const requestWhere = requestPeriodWhere(period);
976
+ const machineClause = machine ? " AND machine_id = ?" : "";
977
+ const machineParams = machine ? [machine] : [];
979
978
  const groups = new Map;
980
979
  const requestRows = db.prepare(`
981
980
  SELECT agent,
@@ -991,10 +990,10 @@ function queryAgentBreakdown(db, period = "all") {
991
990
  COALESCE(SUM(cost_usd), 0) as cost_usd,
992
991
  MAX(timestamp) as last_active
993
992
  FROM requests
994
- WHERE ${requestWhere}
993
+ WHERE ${requestWhere}${machineClause}
995
994
  GROUP BY agent
996
995
  ORDER BY api_equivalent_usd DESC
997
- `).all();
996
+ `).all(...machineParams);
998
997
  for (const row of requestRows) {
999
998
  groups.set(row.agent, row);
1000
999
  }
@@ -1007,10 +1006,10 @@ function queryAgentBreakdown(db, period = "all") {
1007
1006
  COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1008
1007
  MAX(started_at) as last_active
1009
1008
  FROM sessions
1010
- WHERE ${sessionWhere}
1009
+ WHERE ${sessionWhere}${machineClause}
1011
1010
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1012
1011
  GROUP BY agent
1013
- `).all();
1012
+ `).all(...machineParams);
1014
1013
  for (const row of sessionOnlyRows) {
1015
1014
  const existing = groups.get(row.agent) ?? {
1016
1015
  agent: row.agent,
@@ -1087,14 +1086,20 @@ function labelForPath(projectPath, projectName) {
1087
1086
  function groupKeyForPath(projectPath, projectName) {
1088
1087
  return labelForPath(projectPath, projectName).trim().toLowerCase();
1089
1088
  }
1090
- function queryProjectBreakdown(db, period = "all") {
1089
+ function queryProjectBreakdown(db, period = "all", machine) {
1091
1090
  const requestWhere = requestPeriodWhere(period);
1092
1091
  const sessionWhere = sessionPeriodWhere(period);
1092
+ const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
1093
+ const requestMachineClause = machine ? " AND machine_id = ?" : "";
1094
+ const sessionMachineParams = machine ? [machine, machine] : [];
1095
+ const requestMachineParams = machine ? [machine] : [];
1096
+ const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
1097
+ const sessionOnlyMachineParams = machine ? [machine] : [];
1093
1098
  const sessions = db.prepare(`
1094
1099
  SELECT id, project_path, project_name, total_cost_usd, started_at
1095
1100
  FROM sessions
1096
- WHERE project_path != '' OR project_name != ''
1097
- `).all();
1101
+ WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
1102
+ `).all(...sessionMachineParams);
1098
1103
  const groups = new Map;
1099
1104
  for (const s of sessions) {
1100
1105
  const label = labelForPath(s.project_path, s.project_name);
@@ -1120,7 +1125,8 @@ function queryProjectBreakdown(db, period = "all") {
1120
1125
  FROM requests
1121
1126
  WHERE session_id IN (${placeholders})
1122
1127
  AND ${requestWhere}
1123
- `).get(...g.sessionIds) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
1128
+ ${requestMachineClause}
1129
+ `).get(...g.sessionIds, ...requestMachineParams) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
1124
1130
  const sessionOnlyStats = placeholders.length ? db.prepare(`
1125
1131
  SELECT
1126
1132
  COUNT(*) as sessions,
@@ -1131,8 +1137,9 @@ function queryProjectBreakdown(db, period = "all") {
1131
1137
  FROM sessions
1132
1138
  WHERE id IN (${placeholders})
1133
1139
  AND ${sessionWhere}
1140
+ ${sessionOnlyMachineClause}
1134
1141
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1135
- `).get(...g.sessionIds) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1142
+ `).get(...g.sessionIds, ...sessionOnlyMachineParams) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1136
1143
  const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
1137
1144
  if (totalSessions === 0)
1138
1145
  continue;
@@ -1150,107 +1157,167 @@ function queryProjectBreakdown(db, period = "all") {
1150
1157
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1151
1158
  return result;
1152
1159
  }
1153
- function queryAccountBreakdown(db, period = "all") {
1160
+ function normalizeAccountEmail(email) {
1161
+ return (email ?? "").trim().toLowerCase();
1162
+ }
1163
+ function accountIdentityKey(agent, accountKey, accountName, accountEmail) {
1164
+ const identityAgent = (agent || "").trim();
1165
+ const normalizedEmail = normalizeAccountEmail(accountEmail);
1166
+ if (identityAgent && normalizedEmail)
1167
+ return `${identityAgent}:${normalizedEmail}`;
1168
+ if (identityAgent && accountName)
1169
+ return `${identityAgent}:${accountName}`;
1170
+ if (accountKey)
1171
+ return accountKey;
1172
+ return identityAgent ? `${identityAgent}:unknown` : "";
1173
+ }
1174
+ function addAccountBreakdownRow(groups, row, sessionOnly) {
1175
+ const agent = row.agent || row.account_tool;
1176
+ const email = normalizeAccountEmail(row.account_email);
1177
+ const accountName = row.account_name || email || row.account_key;
1178
+ const key = accountIdentityKey(agent, row.account_key, accountName, email);
1179
+ if (!key)
1180
+ return;
1181
+ const group = groups.get(key) ?? {
1182
+ account_key: key,
1183
+ account_tool: agent,
1184
+ account_name: accountName,
1185
+ account_email: email || null,
1186
+ account_source: row.account_source || "unknown",
1187
+ sessionIds: new Set,
1188
+ requests: 0,
1189
+ total_tokens: 0,
1190
+ api_equivalent_usd: 0,
1191
+ metered_api_usd: 0,
1192
+ subscription_included_usd: 0,
1193
+ estimated_usd: 0,
1194
+ unknown_usd: 0,
1195
+ last_active: ""
1196
+ };
1197
+ if (!group.account_email && email)
1198
+ group.account_email = email;
1199
+ if (!group.account_name && accountName)
1200
+ group.account_name = accountName;
1201
+ if ((!group.account_source || group.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
1202
+ group.account_source = row.account_source;
1203
+ }
1204
+ if (row.session_id)
1205
+ group.sessionIds.add(row.session_id);
1206
+ group.requests += row.requests;
1207
+ group.total_tokens += row.total_tokens;
1208
+ group.api_equivalent_usd += row.cost_usd;
1209
+ if (sessionOnly) {
1210
+ group.estimated_usd += row.cost_usd;
1211
+ } else if (row.cost_basis === "metered_api") {
1212
+ group.metered_api_usd += row.cost_usd;
1213
+ } else if (row.cost_basis === "subscription_included") {
1214
+ group.subscription_included_usd += row.cost_usd;
1215
+ } else if (row.cost_basis === "unknown") {
1216
+ group.unknown_usd += row.cost_usd;
1217
+ } else {
1218
+ group.estimated_usd += row.cost_usd;
1219
+ }
1220
+ if (!group.last_active || row.last_active > group.last_active)
1221
+ group.last_active = row.last_active;
1222
+ groups.set(key, group);
1223
+ }
1224
+ function queryAccountBreakdown(db, period = "all", machine) {
1154
1225
  const requestWhere = requestPeriodWhere(period);
1155
1226
  const sessionWhere = sessionPeriodWhere(period);
1156
- const sessions = db.prepare(`
1157
- SELECT id, account_key, account_tool, account_name, account_email, account_source,
1158
- total_cost_usd, total_tokens, request_count, started_at
1159
- FROM sessions
1160
- WHERE account_key != '' OR account_tool != '' OR account_name != '' OR account_email != ''
1161
- `).all();
1227
+ const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
1228
+ const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
1229
+ const requestMachineParams = machine ? [machine] : [];
1230
+ const sessionMachineParams = machine ? [machine] : [];
1162
1231
  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
- };
1174
- group.sessionIds.push(session.id);
1175
- groups.set(key, group);
1176
- }
1177
- const result = [];
1178
- for (const [key, group] of groups.entries()) {
1179
- const placeholders = group.sessionIds.map(() => "?").join(",");
1180
- const reqStats = placeholders ? db.prepare(`
1181
- SELECT
1182
- COUNT(DISTINCT session_id) as sessions,
1183
- COUNT(*) as requests,
1184
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1185
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1186
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1187
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1188
- COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1189
- COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
1190
- MAX(timestamp) as last_active
1191
- FROM requests
1192
- WHERE session_id IN (${placeholders})
1193
- AND ${requestWhere}
1194
- `).get(...group.sessionIds) : {
1195
- sessions: 0,
1196
- requests: 0,
1197
- cost_usd: 0,
1198
- total_tokens: 0,
1199
- metered_api_usd: 0,
1200
- subscription_included_usd: 0,
1201
- estimated_usd: 0,
1202
- unknown_usd: 0,
1203
- last_active: null
1204
- };
1205
- const sessionOnlyStats = placeholders ? db.prepare(`
1206
- SELECT
1207
- COUNT(*) as sessions,
1208
- COALESCE(SUM(request_count), 0) as requests,
1209
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1210
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1211
- MAX(started_at) as last_active
1212
- FROM sessions
1213
- WHERE id IN (${placeholders})
1214
- AND ${sessionWhere}
1215
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1216
- `).get(...group.sessionIds) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1217
- const sessionsTotal = reqStats.sessions + sessionOnlyStats.sessions;
1218
- if (sessionsTotal === 0)
1219
- continue;
1220
- const apiEquivalentUsd = reqStats.cost_usd + sessionOnlyStats.cost_usd;
1221
- const estimatedUsd = reqStats.estimated_usd + sessionOnlyStats.cost_usd;
1222
- const billableUsd = reqStats.metered_api_usd;
1223
- const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
1224
- result.push({
1225
- account_key: key,
1226
- account_tool: group.account_tool,
1227
- account_name: group.account_name,
1228
- account_email: group.account_email,
1229
- account_source: group.account_source,
1230
- sessions: sessionsTotal,
1231
- requests: reqStats.requests + sessionOnlyStats.requests,
1232
- total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
1233
- api_equivalent_usd: apiEquivalentUsd,
1234
- billable_usd: billableUsd,
1235
- metered_api_usd: reqStats.metered_api_usd,
1236
- subscription_included_usd: reqStats.subscription_included_usd,
1237
- estimated_usd: estimatedUsd,
1238
- unknown_usd: reqStats.unknown_usd,
1239
- cost_usd: apiEquivalentUsd,
1240
- last_active: lastActive
1241
- });
1242
- }
1232
+ const requestRows = db.prepare(`
1233
+ SELECT
1234
+ r.session_id as session_id,
1235
+ COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
1236
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
1237
+ COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
1238
+ COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
1239
+ COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
1240
+ COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
1241
+ 1 as requests,
1242
+ COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1243
+ COALESCE(r.cost_usd, 0) as cost_usd,
1244
+ COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1245
+ r.timestamp as last_active
1246
+ FROM requests r
1247
+ LEFT JOIN sessions s ON s.id = r.session_id
1248
+ WHERE ${requestWhere}${requestMachineClause}
1249
+ AND (
1250
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
1251
+ OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
1252
+ OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
1253
+ OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
1254
+ )
1255
+ `).all(...requestMachineParams);
1256
+ for (const row of requestRows)
1257
+ addAccountBreakdownRow(groups, row, false);
1258
+ const sessionOnlyRows = db.prepare(`
1259
+ SELECT
1260
+ s.id as session_id,
1261
+ s.agent as agent,
1262
+ s.account_key as account_key,
1263
+ s.account_tool as account_tool,
1264
+ s.account_name as account_name,
1265
+ s.account_email as account_email,
1266
+ COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
1267
+ COALESCE(s.request_count, 0) as requests,
1268
+ COALESCE(s.total_tokens, 0) as total_tokens,
1269
+ COALESCE(s.total_cost_usd, 0) as cost_usd,
1270
+ 'estimated' as cost_basis,
1271
+ s.started_at as last_active
1272
+ FROM sessions s
1273
+ WHERE ${sessionWhere}${sessionMachineClause}
1274
+ AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1275
+ AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
1276
+ `).all(...sessionMachineParams);
1277
+ for (const row of sessionOnlyRows)
1278
+ addAccountBreakdownRow(groups, row, true);
1279
+ const result = [...groups.values()].map((group) => ({
1280
+ account_key: group.account_key,
1281
+ account_tool: group.account_tool,
1282
+ account_name: group.account_name,
1283
+ account_email: group.account_email,
1284
+ account_source: group.account_source,
1285
+ sessions: group.sessionIds.size,
1286
+ requests: group.requests,
1287
+ total_tokens: group.total_tokens,
1288
+ api_equivalent_usd: group.api_equivalent_usd,
1289
+ billable_usd: group.metered_api_usd,
1290
+ metered_api_usd: group.metered_api_usd,
1291
+ subscription_included_usd: group.subscription_included_usd,
1292
+ estimated_usd: group.estimated_usd,
1293
+ unknown_usd: group.unknown_usd,
1294
+ cost_usd: group.api_equivalent_usd,
1295
+ last_active: group.last_active
1296
+ }));
1243
1297
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1244
1298
  return result;
1245
1299
  }
1246
- function queryDailyBreakdown(db, days = 30) {
1300
+ function queryDailyBreakdown(db, days = 30, machine) {
1301
+ const machineClause = machine ? " AND machine_id = ?" : "";
1302
+ const params = machine ? [`-${days}`, machine] : [`-${days}`];
1247
1303
  return db.prepare(`
1248
1304
  SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
1249
1305
  FROM requests
1250
- WHERE timestamp >= DATE('now', ? || ' days')
1306
+ WHERE timestamp >= DATE('now', ? || ' days')${machineClause}
1251
1307
  GROUP BY DATE(timestamp), agent
1252
1308
  ORDER BY date ASC
1253
- `).all(`-${days}`);
1309
+ `).all(...params);
1310
+ }
1311
+ function queryHourlyBreakdown(db, machine) {
1312
+ const machineClause = machine ? " AND machine_id = ?" : "";
1313
+ const params = machine ? [machine] : [];
1314
+ return db.prepare(`
1315
+ SELECT STRFTIME('%H', timestamp) as hour, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
1316
+ FROM requests
1317
+ WHERE DATE(timestamp) = DATE('now')${machineClause}
1318
+ GROUP BY STRFTIME('%H', timestamp), agent
1319
+ ORDER BY hour ASC
1320
+ `).all(...params);
1254
1321
  }
1255
1322
  function upsertProject(db, project) {
1256
1323
  db.prepare(`
@@ -1370,17 +1437,48 @@ function queryBillingSummary(db, period) {
1370
1437
  }
1371
1438
  return { total_usd: total, by_provider };
1372
1439
  }
1373
- function listMachines(db) {
1440
+ function listMachines(db, period = "all") {
1441
+ const rWhere = requestPeriodWhere(period);
1442
+ const sWhere = sessionPeriodWhere(period);
1374
1443
  return db.prepare(`
1444
+ WITH request_stats AS (
1445
+ SELECT
1446
+ machine_id,
1447
+ COUNT(DISTINCT session_id) as sessions,
1448
+ COUNT(*) as requests,
1449
+ COALESCE(SUM(cost_usd), 0) as total_cost_usd,
1450
+ MAX(timestamp) as last_active
1451
+ FROM requests
1452
+ WHERE machine_id != ''
1453
+ AND ${rWhere}
1454
+ GROUP BY machine_id
1455
+ ),
1456
+ session_only_stats AS (
1457
+ SELECT
1458
+ machine_id,
1459
+ COUNT(*) as sessions,
1460
+ COALESCE(SUM(request_count), 0) as requests,
1461
+ COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
1462
+ MAX(started_at) as last_active
1463
+ FROM sessions
1464
+ WHERE machine_id != ''
1465
+ AND ${sWhere}
1466
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1467
+ GROUP BY machine_id
1468
+ ),
1469
+ combined AS (
1470
+ SELECT * FROM request_stats
1471
+ UNION ALL
1472
+ SELECT * FROM session_only_stats
1473
+ )
1375
1474
  SELECT
1376
- s.machine_id,
1377
- COUNT(DISTINCT s.id) as sessions,
1378
- COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
1379
- COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
1380
- MAX(s.started_at) as last_active
1381
- FROM sessions s
1382
- WHERE s.machine_id != ''
1383
- GROUP BY s.machine_id
1475
+ machine_id,
1476
+ COALESCE(SUM(sessions), 0) as sessions,
1477
+ COALESCE(SUM(requests), 0) as requests,
1478
+ COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
1479
+ MAX(last_active) as last_active
1480
+ FROM combined
1481
+ GROUP BY machine_id
1384
1482
  ORDER BY total_cost_usd DESC
1385
1483
  `).all();
1386
1484
  }
@@ -2255,18 +2353,22 @@ var AGENT_ACCOUNT_TOOLS = {
2255
2353
  pi: ["pi"],
2256
2354
  hermes: ["hermes"]
2257
2355
  };
2258
- function accountKey(tool, name) {
2259
- return `${tool}:${name}`;
2356
+ function normalizeEmail(email) {
2357
+ return (email ?? "").trim().toLowerCase();
2358
+ }
2359
+ function accountKey(tool, name, email) {
2360
+ const normalizedEmail = normalizeEmail(email);
2361
+ return `${tool}:${normalizedEmail || name}`;
2260
2362
  }
2261
2363
  function normalizeDir(value) {
2262
2364
  return value.replace(/\/+$/, "");
2263
2365
  }
2264
2366
  function fromProfile(profile, source) {
2265
2367
  return {
2266
- account_key: accountKey(profile.tool, profile.name),
2368
+ account_key: accountKey(profile.tool, profile.name, profile.email),
2267
2369
  account_tool: profile.tool,
2268
2370
  account_name: profile.name,
2269
- ...profile.email ? { account_email: profile.email } : {},
2371
+ ...profile.email ? { account_email: normalizeEmail(profile.email) } : {},
2270
2372
  account_source: source
2271
2373
  };
2272
2374
  }
@@ -2278,10 +2380,12 @@ function fromOverride(raw, agent) {
2278
2380
  const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
2279
2381
  if (!tool || !name)
2280
2382
  return null;
2383
+ const email = name.includes("@") ? normalizeEmail(name) : undefined;
2281
2384
  return {
2282
- account_key: accountKey(tool, name),
2385
+ account_key: accountKey(tool, name, email),
2283
2386
  account_tool: tool,
2284
2387
  account_name: name,
2388
+ ...email ? { account_email: email } : {},
2285
2389
  account_source: "override"
2286
2390
  };
2287
2391
  }
@@ -2294,11 +2398,12 @@ function envOverride(agent, env) {
2294
2398
  const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
2295
2399
  if (!tool || !name)
2296
2400
  return null;
2401
+ const email = normalizeEmail(env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"]);
2297
2402
  return {
2298
- account_key: accountKey(tool, name),
2403
+ account_key: accountKey(tool, name, email),
2299
2404
  account_tool: tool,
2300
2405
  account_name: name,
2301
- account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
2406
+ ...email ? { account_email: email } : {},
2302
2407
  account_source: "override"
2303
2408
  };
2304
2409
  }
@@ -4057,16 +4162,22 @@ function createHandler(db) {
4057
4162
  }
4058
4163
  if (path === "/api/fleet" && method === "GET") {
4059
4164
  const period = url.searchParams.get("period") ?? "month";
4165
+ const machine = url.searchParams.get("machine") ?? undefined;
4060
4166
  return ok({
4061
- summary: querySummary(db, period, undefined, true),
4062
- machines: listMachines(db),
4167
+ summary: querySummary(db, period, machine),
4168
+ machines: listMachines(db, period),
4063
4169
  registry: listMachineRegistry(db),
4064
4170
  current_machine: getMachineId()
4065
4171
  });
4066
4172
  }
4067
4173
  if (path === "/api/daily" && method === "GET") {
4068
4174
  const days = Number(url.searchParams.get("days") ?? 30);
4069
- return ok(queryDailyBreakdown(db, days));
4175
+ const machine = url.searchParams.get("machine") ?? undefined;
4176
+ return ok(queryDailyBreakdown(db, days, machine));
4177
+ }
4178
+ if (path === "/api/hourly" && method === "GET") {
4179
+ const machine = url.searchParams.get("machine") ?? undefined;
4180
+ return ok(queryHourlyBreakdown(db, machine));
4070
4181
  }
4071
4182
  if (path === "/api/sessions" && method === "GET") {
4072
4183
  const agent = url.searchParams.get("agent");
@@ -4136,21 +4247,24 @@ function createHandler(db) {
4136
4247
  }
4137
4248
  if (path === "/api/projects" && method === "GET") {
4138
4249
  const period = url.searchParams.get("period") ?? "all";
4139
- return ok(queryProjectBreakdown(db, period));
4250
+ const machine = url.searchParams.get("machine") ?? undefined;
4251
+ return ok(queryProjectBreakdown(db, period, machine));
4140
4252
  }
4141
4253
  if (path === "/api/accounts" && method === "GET") {
4142
4254
  const period = url.searchParams.get("period") ?? "all";
4143
- return ok(queryAccountBreakdown(db, period));
4255
+ const machine = url.searchParams.get("machine") ?? undefined;
4256
+ return ok(queryAccountBreakdown(db, period, machine));
4144
4257
  }
4145
4258
  if (path === "/api/breakdown" && method === "GET") {
4146
4259
  const by = url.searchParams.get("by") ?? "model";
4147
4260
  const period = url.searchParams.get("period") ?? "all";
4261
+ const machine = url.searchParams.get("machine") ?? undefined;
4148
4262
  if (by === "project")
4149
- return ok(queryProjectBreakdown(db, period));
4263
+ return ok(queryProjectBreakdown(db, period, machine));
4150
4264
  if (by === "agent")
4151
- return ok(queryAgentBreakdown(db, period));
4265
+ return ok(queryAgentBreakdown(db, period, machine));
4152
4266
  if (by === "account")
4153
- return ok(queryAccountBreakdown(db, period));
4267
+ return ok(queryAccountBreakdown(db, period, machine));
4154
4268
  return ok(queryModelBreakdown(db));
4155
4269
  }
4156
4270
  if (path === "/api/budgets" && method === "GET") {
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAuC7D,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAqED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAkW/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAcvG"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAuC7D,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAqED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA4W/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAcvG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.2.27",
3
+ "version": "0.2.29",
4
4
  "description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex, Gemini, OpenCode, Cursor, Pi, and Hermes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",