@hasna/economy 0.2.28 → 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.
package/dist/cli/index.js CHANGED
@@ -534,6 +534,7 @@ __export(exports_database, {
534
534
  queryRequestsSince: () => queryRequestsSince,
535
535
  queryProjectBreakdown: () => queryProjectBreakdown,
536
536
  queryModelBreakdown: () => queryModelBreakdown,
537
+ queryHourlyBreakdown: () => queryHourlyBreakdown,
537
538
  queryDailyBreakdown: () => queryDailyBreakdown,
538
539
  queryBillingSummary: () => queryBillingSummary,
539
540
  queryAgentBreakdown: () => queryAgentBreakdown,
@@ -1020,8 +1021,10 @@ function queryModelBreakdown(db) {
1020
1021
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
1021
1022
  `).all();
1022
1023
  }
1023
- function queryAgentBreakdown(db, period = "all") {
1024
+ function queryAgentBreakdown(db, period = "all", machine) {
1024
1025
  const requestWhere = requestPeriodWhere(period);
1026
+ const machineClause = machine ? " AND machine_id = ?" : "";
1027
+ const machineParams = machine ? [machine] : [];
1025
1028
  const groups = new Map;
1026
1029
  const requestRows = db.prepare(`
1027
1030
  SELECT agent,
@@ -1037,10 +1040,10 @@ function queryAgentBreakdown(db, period = "all") {
1037
1040
  COALESCE(SUM(cost_usd), 0) as cost_usd,
1038
1041
  MAX(timestamp) as last_active
1039
1042
  FROM requests
1040
- WHERE ${requestWhere}
1043
+ WHERE ${requestWhere}${machineClause}
1041
1044
  GROUP BY agent
1042
1045
  ORDER BY api_equivalent_usd DESC
1043
- `).all();
1046
+ `).all(...machineParams);
1044
1047
  for (const row of requestRows) {
1045
1048
  groups.set(row.agent, row);
1046
1049
  }
@@ -1053,10 +1056,10 @@ function queryAgentBreakdown(db, period = "all") {
1053
1056
  COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1054
1057
  MAX(started_at) as last_active
1055
1058
  FROM sessions
1056
- WHERE ${sessionWhere}
1059
+ WHERE ${sessionWhere}${machineClause}
1057
1060
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1058
1061
  GROUP BY agent
1059
- `).all();
1062
+ `).all(...machineParams);
1060
1063
  for (const row of sessionOnlyRows) {
1061
1064
  const existing = groups.get(row.agent) ?? {
1062
1065
  agent: row.agent,
@@ -1133,14 +1136,20 @@ function labelForPath(projectPath, projectName) {
1133
1136
  function groupKeyForPath(projectPath, projectName) {
1134
1137
  return labelForPath(projectPath, projectName).trim().toLowerCase();
1135
1138
  }
1136
- function queryProjectBreakdown(db, period = "all") {
1139
+ function queryProjectBreakdown(db, period = "all", machine) {
1137
1140
  const requestWhere = requestPeriodWhere(period);
1138
1141
  const sessionWhere = sessionPeriodWhere(period);
1142
+ const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
1143
+ const requestMachineClause = machine ? " AND machine_id = ?" : "";
1144
+ const sessionMachineParams = machine ? [machine, machine] : [];
1145
+ const requestMachineParams = machine ? [machine] : [];
1146
+ const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
1147
+ const sessionOnlyMachineParams = machine ? [machine] : [];
1139
1148
  const sessions = db.prepare(`
1140
1149
  SELECT id, project_path, project_name, total_cost_usd, started_at
1141
1150
  FROM sessions
1142
- WHERE project_path != '' OR project_name != ''
1143
- `).all();
1151
+ WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
1152
+ `).all(...sessionMachineParams);
1144
1153
  const groups = new Map;
1145
1154
  for (const s of sessions) {
1146
1155
  const label = labelForPath(s.project_path, s.project_name);
@@ -1166,7 +1175,8 @@ function queryProjectBreakdown(db, period = "all") {
1166
1175
  FROM requests
1167
1176
  WHERE session_id IN (${placeholders})
1168
1177
  AND ${requestWhere}
1169
- `).get(...g.sessionIds) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
1178
+ ${requestMachineClause}
1179
+ `).get(...g.sessionIds, ...requestMachineParams) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
1170
1180
  const sessionOnlyStats = placeholders.length ? db.prepare(`
1171
1181
  SELECT
1172
1182
  COUNT(*) as sessions,
@@ -1177,8 +1187,9 @@ function queryProjectBreakdown(db, period = "all") {
1177
1187
  FROM sessions
1178
1188
  WHERE id IN (${placeholders})
1179
1189
  AND ${sessionWhere}
1190
+ ${sessionOnlyMachineClause}
1180
1191
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1181
- `).get(...g.sessionIds) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1192
+ `).get(...g.sessionIds, ...sessionOnlyMachineParams) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1182
1193
  const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
1183
1194
  if (totalSessions === 0)
1184
1195
  continue;
@@ -1196,107 +1207,167 @@ function queryProjectBreakdown(db, period = "all") {
1196
1207
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1197
1208
  return result;
1198
1209
  }
1199
- function queryAccountBreakdown(db, period = "all") {
1210
+ function normalizeAccountEmail(email) {
1211
+ return (email ?? "").trim().toLowerCase();
1212
+ }
1213
+ function accountIdentityKey(agent, accountKey, accountName, accountEmail) {
1214
+ const identityAgent = (agent || "").trim();
1215
+ const normalizedEmail = normalizeAccountEmail(accountEmail);
1216
+ if (identityAgent && normalizedEmail)
1217
+ return `${identityAgent}:${normalizedEmail}`;
1218
+ if (identityAgent && accountName)
1219
+ return `${identityAgent}:${accountName}`;
1220
+ if (accountKey)
1221
+ return accountKey;
1222
+ return identityAgent ? `${identityAgent}:unknown` : "";
1223
+ }
1224
+ function addAccountBreakdownRow(groups, row, sessionOnly) {
1225
+ const agent = row.agent || row.account_tool;
1226
+ const email = normalizeAccountEmail(row.account_email);
1227
+ const accountName = row.account_name || email || row.account_key;
1228
+ const key = accountIdentityKey(agent, row.account_key, accountName, email);
1229
+ if (!key)
1230
+ return;
1231
+ const group = groups.get(key) ?? {
1232
+ account_key: key,
1233
+ account_tool: agent,
1234
+ account_name: accountName,
1235
+ account_email: email || null,
1236
+ account_source: row.account_source || "unknown",
1237
+ sessionIds: new Set,
1238
+ requests: 0,
1239
+ total_tokens: 0,
1240
+ api_equivalent_usd: 0,
1241
+ metered_api_usd: 0,
1242
+ subscription_included_usd: 0,
1243
+ estimated_usd: 0,
1244
+ unknown_usd: 0,
1245
+ last_active: ""
1246
+ };
1247
+ if (!group.account_email && email)
1248
+ group.account_email = email;
1249
+ if (!group.account_name && accountName)
1250
+ group.account_name = accountName;
1251
+ if ((!group.account_source || group.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
1252
+ group.account_source = row.account_source;
1253
+ }
1254
+ if (row.session_id)
1255
+ group.sessionIds.add(row.session_id);
1256
+ group.requests += row.requests;
1257
+ group.total_tokens += row.total_tokens;
1258
+ group.api_equivalent_usd += row.cost_usd;
1259
+ if (sessionOnly) {
1260
+ group.estimated_usd += row.cost_usd;
1261
+ } else if (row.cost_basis === "metered_api") {
1262
+ group.metered_api_usd += row.cost_usd;
1263
+ } else if (row.cost_basis === "subscription_included") {
1264
+ group.subscription_included_usd += row.cost_usd;
1265
+ } else if (row.cost_basis === "unknown") {
1266
+ group.unknown_usd += row.cost_usd;
1267
+ } else {
1268
+ group.estimated_usd += row.cost_usd;
1269
+ }
1270
+ if (!group.last_active || row.last_active > group.last_active)
1271
+ group.last_active = row.last_active;
1272
+ groups.set(key, group);
1273
+ }
1274
+ function queryAccountBreakdown(db, period = "all", machine) {
1200
1275
  const requestWhere = requestPeriodWhere(period);
1201
1276
  const sessionWhere = sessionPeriodWhere(period);
1202
- const sessions = db.prepare(`
1203
- SELECT id, account_key, account_tool, account_name, account_email, account_source,
1204
- total_cost_usd, total_tokens, request_count, started_at
1205
- FROM sessions
1206
- WHERE account_key != '' OR account_tool != '' OR account_name != '' OR account_email != ''
1207
- `).all();
1277
+ const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
1278
+ const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
1279
+ const requestMachineParams = machine ? [machine] : [];
1280
+ const sessionMachineParams = machine ? [machine] : [];
1208
1281
  const groups = new Map;
1209
- for (const session of sessions) {
1210
- const key = session.account_key || `${session.account_tool}:${session.account_name}`;
1211
- if (!key || key === ":")
1212
- continue;
1213
- const group = groups.get(key) ?? {
1214
- sessionIds: [],
1215
- account_tool: session.account_tool,
1216
- account_name: session.account_name,
1217
- account_email: session.account_email || null,
1218
- account_source: session.account_source || "unknown"
1219
- };
1220
- group.sessionIds.push(session.id);
1221
- groups.set(key, group);
1222
- }
1223
- const result = [];
1224
- for (const [key, group] of groups.entries()) {
1225
- const placeholders = group.sessionIds.map(() => "?").join(",");
1226
- const reqStats = placeholders ? db.prepare(`
1227
- SELECT
1228
- COUNT(DISTINCT session_id) as sessions,
1229
- COUNT(*) as requests,
1230
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1231
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1232
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1233
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1234
- COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1235
- COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
1236
- MAX(timestamp) as last_active
1237
- FROM requests
1238
- WHERE session_id IN (${placeholders})
1239
- AND ${requestWhere}
1240
- `).get(...group.sessionIds) : {
1241
- sessions: 0,
1242
- requests: 0,
1243
- cost_usd: 0,
1244
- total_tokens: 0,
1245
- metered_api_usd: 0,
1246
- subscription_included_usd: 0,
1247
- estimated_usd: 0,
1248
- unknown_usd: 0,
1249
- last_active: null
1250
- };
1251
- const sessionOnlyStats = placeholders ? db.prepare(`
1252
- SELECT
1253
- COUNT(*) as sessions,
1254
- COALESCE(SUM(request_count), 0) as requests,
1255
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1256
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1257
- MAX(started_at) as last_active
1258
- FROM sessions
1259
- WHERE id IN (${placeholders})
1260
- AND ${sessionWhere}
1261
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1262
- `).get(...group.sessionIds) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1263
- const sessionsTotal = reqStats.sessions + sessionOnlyStats.sessions;
1264
- if (sessionsTotal === 0)
1265
- continue;
1266
- const apiEquivalentUsd = reqStats.cost_usd + sessionOnlyStats.cost_usd;
1267
- const estimatedUsd = reqStats.estimated_usd + sessionOnlyStats.cost_usd;
1268
- const billableUsd = reqStats.metered_api_usd;
1269
- const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
1270
- result.push({
1271
- account_key: key,
1272
- account_tool: group.account_tool,
1273
- account_name: group.account_name,
1274
- account_email: group.account_email,
1275
- account_source: group.account_source,
1276
- sessions: sessionsTotal,
1277
- requests: reqStats.requests + sessionOnlyStats.requests,
1278
- total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
1279
- api_equivalent_usd: apiEquivalentUsd,
1280
- billable_usd: billableUsd,
1281
- metered_api_usd: reqStats.metered_api_usd,
1282
- subscription_included_usd: reqStats.subscription_included_usd,
1283
- estimated_usd: estimatedUsd,
1284
- unknown_usd: reqStats.unknown_usd,
1285
- cost_usd: apiEquivalentUsd,
1286
- last_active: lastActive
1287
- });
1288
- }
1282
+ const requestRows = db.prepare(`
1283
+ SELECT
1284
+ r.session_id as session_id,
1285
+ COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
1286
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
1287
+ COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
1288
+ COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
1289
+ COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
1290
+ COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
1291
+ 1 as requests,
1292
+ COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1293
+ COALESCE(r.cost_usd, 0) as cost_usd,
1294
+ COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1295
+ r.timestamp as last_active
1296
+ FROM requests r
1297
+ LEFT JOIN sessions s ON s.id = r.session_id
1298
+ WHERE ${requestWhere}${requestMachineClause}
1299
+ AND (
1300
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
1301
+ OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
1302
+ OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
1303
+ OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
1304
+ )
1305
+ `).all(...requestMachineParams);
1306
+ for (const row of requestRows)
1307
+ addAccountBreakdownRow(groups, row, false);
1308
+ const sessionOnlyRows = db.prepare(`
1309
+ SELECT
1310
+ s.id as session_id,
1311
+ s.agent as agent,
1312
+ s.account_key as account_key,
1313
+ s.account_tool as account_tool,
1314
+ s.account_name as account_name,
1315
+ s.account_email as account_email,
1316
+ COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
1317
+ COALESCE(s.request_count, 0) as requests,
1318
+ COALESCE(s.total_tokens, 0) as total_tokens,
1319
+ COALESCE(s.total_cost_usd, 0) as cost_usd,
1320
+ 'estimated' as cost_basis,
1321
+ s.started_at as last_active
1322
+ FROM sessions s
1323
+ WHERE ${sessionWhere}${sessionMachineClause}
1324
+ AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1325
+ AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
1326
+ `).all(...sessionMachineParams);
1327
+ for (const row of sessionOnlyRows)
1328
+ addAccountBreakdownRow(groups, row, true);
1329
+ const result = [...groups.values()].map((group) => ({
1330
+ account_key: group.account_key,
1331
+ account_tool: group.account_tool,
1332
+ account_name: group.account_name,
1333
+ account_email: group.account_email,
1334
+ account_source: group.account_source,
1335
+ sessions: group.sessionIds.size,
1336
+ requests: group.requests,
1337
+ total_tokens: group.total_tokens,
1338
+ api_equivalent_usd: group.api_equivalent_usd,
1339
+ billable_usd: group.metered_api_usd,
1340
+ metered_api_usd: group.metered_api_usd,
1341
+ subscription_included_usd: group.subscription_included_usd,
1342
+ estimated_usd: group.estimated_usd,
1343
+ unknown_usd: group.unknown_usd,
1344
+ cost_usd: group.api_equivalent_usd,
1345
+ last_active: group.last_active
1346
+ }));
1289
1347
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1290
1348
  return result;
1291
1349
  }
1292
- function queryDailyBreakdown(db, days = 30) {
1350
+ function queryDailyBreakdown(db, days = 30, machine) {
1351
+ const machineClause = machine ? " AND machine_id = ?" : "";
1352
+ const params = machine ? [`-${days}`, machine] : [`-${days}`];
1293
1353
  return db.prepare(`
1294
1354
  SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
1295
1355
  FROM requests
1296
- WHERE timestamp >= DATE('now', ? || ' days')
1356
+ WHERE timestamp >= DATE('now', ? || ' days')${machineClause}
1297
1357
  GROUP BY DATE(timestamp), agent
1298
1358
  ORDER BY date ASC
1299
- `).all(`-${days}`);
1359
+ `).all(...params);
1360
+ }
1361
+ function queryHourlyBreakdown(db, machine) {
1362
+ const machineClause = machine ? " AND machine_id = ?" : "";
1363
+ const params = machine ? [machine] : [];
1364
+ return db.prepare(`
1365
+ SELECT STRFTIME('%H', timestamp) as hour, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
1366
+ FROM requests
1367
+ WHERE DATE(timestamp) = DATE('now')${machineClause}
1368
+ GROUP BY STRFTIME('%H', timestamp), agent
1369
+ ORDER BY hour ASC
1370
+ `).all(...params);
1300
1371
  }
1301
1372
  function upsertProject(db, project) {
1302
1373
  db.prepare(`
@@ -2359,18 +2430,22 @@ function agentPaths() {
2359
2430
  var init_paths = () => {};
2360
2431
 
2361
2432
  // src/lib/accounts.ts
2362
- function accountKey(tool, name) {
2363
- return `${tool}:${name}`;
2433
+ function normalizeEmail(email) {
2434
+ return (email ?? "").trim().toLowerCase();
2435
+ }
2436
+ function accountKey(tool, name, email) {
2437
+ const normalizedEmail = normalizeEmail(email);
2438
+ return `${tool}:${normalizedEmail || name}`;
2364
2439
  }
2365
2440
  function normalizeDir(value) {
2366
2441
  return value.replace(/\/+$/, "");
2367
2442
  }
2368
2443
  function fromProfile(profile, source) {
2369
2444
  return {
2370
- account_key: accountKey(profile.tool, profile.name),
2445
+ account_key: accountKey(profile.tool, profile.name, profile.email),
2371
2446
  account_tool: profile.tool,
2372
2447
  account_name: profile.name,
2373
- ...profile.email ? { account_email: profile.email } : {},
2448
+ ...profile.email ? { account_email: normalizeEmail(profile.email) } : {},
2374
2449
  account_source: source
2375
2450
  };
2376
2451
  }
@@ -2382,10 +2457,12 @@ function fromOverride(raw, agent) {
2382
2457
  const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
2383
2458
  if (!tool || !name)
2384
2459
  return null;
2460
+ const email = name.includes("@") ? normalizeEmail(name) : undefined;
2385
2461
  return {
2386
- account_key: accountKey(tool, name),
2462
+ account_key: accountKey(tool, name, email),
2387
2463
  account_tool: tool,
2388
2464
  account_name: name,
2465
+ ...email ? { account_email: email } : {},
2389
2466
  account_source: "override"
2390
2467
  };
2391
2468
  }
@@ -2398,11 +2475,12 @@ function envOverride(agent, env) {
2398
2475
  const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
2399
2476
  if (!tool || !name)
2400
2477
  return null;
2478
+ const email = normalizeEmail(env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"]);
2401
2479
  return {
2402
- account_key: accountKey(tool, name),
2480
+ account_key: accountKey(tool, name, email),
2403
2481
  account_tool: tool,
2404
2482
  account_name: name,
2405
- account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
2483
+ ...email ? { account_email: email } : {},
2406
2484
  account_source: "override"
2407
2485
  };
2408
2486
  }
@@ -4541,8 +4619,9 @@ function createHandler(db) {
4541
4619
  }
4542
4620
  if (path === "/api/fleet" && method === "GET") {
4543
4621
  const period = url.searchParams.get("period") ?? "month";
4622
+ const machine = url.searchParams.get("machine") ?? undefined;
4544
4623
  return ok({
4545
- summary: querySummary(db, period, undefined, true),
4624
+ summary: querySummary(db, period, machine),
4546
4625
  machines: listMachines(db, period),
4547
4626
  registry: listMachineRegistry(db),
4548
4627
  current_machine: getMachineId()
@@ -4550,7 +4629,12 @@ function createHandler(db) {
4550
4629
  }
4551
4630
  if (path === "/api/daily" && method === "GET") {
4552
4631
  const days = Number(url.searchParams.get("days") ?? 30);
4553
- return ok(queryDailyBreakdown(db, days));
4632
+ const machine = url.searchParams.get("machine") ?? undefined;
4633
+ return ok(queryDailyBreakdown(db, days, machine));
4634
+ }
4635
+ if (path === "/api/hourly" && method === "GET") {
4636
+ const machine = url.searchParams.get("machine") ?? undefined;
4637
+ return ok(queryHourlyBreakdown(db, machine));
4554
4638
  }
4555
4639
  if (path === "/api/sessions" && method === "GET") {
4556
4640
  const agent = url.searchParams.get("agent");
@@ -4620,21 +4704,24 @@ function createHandler(db) {
4620
4704
  }
4621
4705
  if (path === "/api/projects" && method === "GET") {
4622
4706
  const period = url.searchParams.get("period") ?? "all";
4623
- return ok(queryProjectBreakdown(db, period));
4707
+ const machine = url.searchParams.get("machine") ?? undefined;
4708
+ return ok(queryProjectBreakdown(db, period, machine));
4624
4709
  }
4625
4710
  if (path === "/api/accounts" && method === "GET") {
4626
4711
  const period = url.searchParams.get("period") ?? "all";
4627
- return ok(queryAccountBreakdown(db, period));
4712
+ const machine = url.searchParams.get("machine") ?? undefined;
4713
+ return ok(queryAccountBreakdown(db, period, machine));
4628
4714
  }
4629
4715
  if (path === "/api/breakdown" && method === "GET") {
4630
4716
  const by = url.searchParams.get("by") ?? "model";
4631
4717
  const period = url.searchParams.get("period") ?? "all";
4718
+ const machine = url.searchParams.get("machine") ?? undefined;
4632
4719
  if (by === "project")
4633
- return ok(queryProjectBreakdown(db, period));
4720
+ return ok(queryProjectBreakdown(db, period, machine));
4634
4721
  if (by === "agent")
4635
- return ok(queryAgentBreakdown(db, period));
4722
+ return ok(queryAgentBreakdown(db, period, machine));
4636
4723
  if (by === "account")
4637
- return ok(queryAccountBreakdown(db, period));
4724
+ return ok(queryAccountBreakdown(db, period, machine));
4638
4725
  return ok(queryModelBreakdown(db));
4639
4726
  }
4640
4727
  if (path === "/api/budgets" && method === "GET") {
@@ -6245,6 +6332,8 @@ var TOP_LEVEL = [
6245
6332
  "doctor",
6246
6333
  "init",
6247
6334
  "estimate",
6335
+ "accounts",
6336
+ "breakdown",
6248
6337
  "fleet",
6249
6338
  "merge-db",
6250
6339
  "todos",
@@ -7015,6 +7104,22 @@ function printTable(headers, rows) {
7015
7104
  }
7016
7105
  console.log(`\u2514${sep2.replace(/\u253C/g, "\u2534")}\u2518`);
7017
7106
  }
7107
+ function accountDisplayName(row) {
7108
+ return row.account_email || row.account_name || row.account_key || "unknown";
7109
+ }
7110
+ function printAccountBreakdown(rows) {
7111
+ printTable(["Account", "Agent", "Source", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], rows.map((r) => [
7112
+ chalk7.white(accountDisplayName(r)),
7113
+ fmtAgent(r.account_tool),
7114
+ chalk7.dim(r.account_source || "unknown"),
7115
+ String(r.sessions),
7116
+ String(r.requests),
7117
+ chalk7.cyan(fmtTokens(r.total_tokens)),
7118
+ fmt4(r.api_equivalent_usd),
7119
+ fmt4(r.billable_usd),
7120
+ fmt4(r.subscription_included_usd)
7121
+ ]));
7122
+ }
7018
7123
  function parseSinceDate(since) {
7019
7124
  const relMatch = since.match(/^(\d+)d$/);
7020
7125
  if (relMatch) {
@@ -7274,29 +7379,106 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
7274
7379
  ]));
7275
7380
  } else if (opts.by === "account") {
7276
7381
  const rows = sinceDate ? db.prepare(`
7277
- SELECT account_key, account_tool, account_name, account_email, account_source,
7382
+ WITH request_rows AS (
7383
+ SELECT
7384
+ r.session_id as session_id,
7385
+ COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
7386
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as raw_account_key,
7387
+ COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as raw_account_name,
7388
+ LOWER(TRIM(COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''))) as raw_account_email,
7389
+ COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
7390
+ 1 as requests,
7391
+ COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
7392
+ COALESCE(r.cost_usd, 0) as cost_usd,
7393
+ COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
7394
+ r.timestamp as last_active
7395
+ FROM requests r
7396
+ LEFT JOIN sessions s ON s.id = r.session_id
7397
+ WHERE r.timestamp >= ?
7398
+ AND (
7399
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
7400
+ OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
7401
+ OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
7402
+ OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
7403
+ )
7404
+ ),
7405
+ session_only_rows AS (
7406
+ SELECT
7407
+ s.id as session_id,
7408
+ COALESCE(NULLIF(s.agent, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
7409
+ s.account_key as raw_account_key,
7410
+ s.account_name as raw_account_name,
7411
+ LOWER(TRIM(COALESCE(s.account_email, ''))) as raw_account_email,
7412
+ COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
7413
+ COALESCE(s.request_count, 0) as requests,
7414
+ COALESCE(s.total_tokens, 0) as total_tokens,
7415
+ COALESCE(s.total_cost_usd, 0) as cost_usd,
7416
+ 'estimated' as cost_basis,
7417
+ s.started_at as last_active
7418
+ FROM sessions s
7419
+ WHERE s.started_at >= ?
7420
+ AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
7421
+ AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
7422
+ ),
7423
+ normalized AS (
7424
+ SELECT
7425
+ CASE
7426
+ WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
7427
+ WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
7428
+ ELSE raw_account_key
7429
+ END as account_key,
7430
+ account_agent as account_tool,
7431
+ raw_account_name as account_name,
7432
+ raw_account_email as account_email,
7433
+ account_source,
7434
+ session_id,
7435
+ requests,
7436
+ total_tokens,
7437
+ cost_usd,
7438
+ cost_basis,
7439
+ last_active
7440
+ FROM request_rows
7441
+ UNION ALL
7442
+ SELECT
7443
+ CASE
7444
+ WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
7445
+ WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
7446
+ ELSE raw_account_key
7447
+ END as account_key,
7448
+ account_agent as account_tool,
7449
+ raw_account_name as account_name,
7450
+ raw_account_email as account_email,
7451
+ account_source,
7452
+ session_id,
7453
+ requests,
7454
+ total_tokens,
7455
+ cost_usd,
7456
+ cost_basis,
7457
+ last_active
7458
+ FROM session_only_rows
7459
+ )
7460
+ SELECT account_key,
7461
+ account_tool,
7462
+ COALESCE(MAX(NULLIF(account_name, '')), MAX(NULLIF(account_email, '')), account_key) as account_name,
7463
+ NULLIF(account_email, '') as account_email,
7464
+ COALESCE(MAX(NULLIF(account_source, 'unknown')), 'unknown') as account_source,
7278
7465
  COUNT(DISTINCT session_id) as sessions,
7279
- COUNT(*) as requests,
7280
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
7466
+ COALESCE(SUM(requests), 0) as requests,
7467
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
7281
7468
  COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
7469
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
7282
7470
  COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
7283
7471
  COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
7284
- MAX(timestamp) as last_active
7285
- FROM requests
7286
- WHERE timestamp >= ?
7287
- AND (account_key != '' OR account_tool != '' OR account_name != '' OR account_email != '')
7288
- GROUP BY account_key, account_tool, account_name, account_email, account_source
7472
+ COALESCE(SUM(CASE WHEN cost_basis NOT IN ('metered_api', 'subscription_included', 'unknown') THEN cost_usd ELSE 0 END), 0) as estimated_usd,
7473
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
7474
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
7475
+ MAX(last_active) as last_active
7476
+ FROM normalized
7477
+ WHERE account_key != ''
7478
+ GROUP BY account_key, account_tool, account_email
7289
7479
  ORDER BY api_equivalent_usd DESC
7290
- `).all(sinceDate) : queryAccountBreakdown(db);
7291
- printTable(["Account", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], rows.map((r) => [
7292
- chalk7.white(r.account_key || r.account_name || chalk7.dim("unknown")),
7293
- String(r.sessions),
7294
- String(r.requests),
7295
- chalk7.cyan(fmtTokens(r.total_tokens)),
7296
- fmt4(r.api_equivalent_usd),
7297
- fmt4("billable_usd" in r ? Number(r.billable_usd) : r.metered_api_usd),
7298
- fmt4(r.subscription_included_usd)
7299
- ]));
7480
+ `).all(sinceDate, sinceDate) : queryAccountBreakdown(db);
7481
+ printAccountBreakdown(rows);
7300
7482
  } else {
7301
7483
  const rows = sinceDate ? db.prepare(`
7302
7484
  SELECT model, agent,
@@ -7318,6 +7500,24 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
7318
7500
  }
7319
7501
  console.log();
7320
7502
  });
7503
+ var ACCOUNT_PERIODS = ["today", "week", "month", "year", "all"];
7504
+ program.command("accounts [period]").description("List account usage by email address and coding agent").option("--json", "Output JSON").action((periodArg, opts) => {
7505
+ const period = requireCliChoice(periodArg, "period", ACCOUNT_PERIODS);
7506
+ const rows = queryAccountBreakdown(openDatabase(), period);
7507
+ if (opts.json) {
7508
+ console.log(JSON.stringify(rows, null, 2));
7509
+ return;
7510
+ }
7511
+ if (rows.length === 0) {
7512
+ console.log(chalk7.yellow("No account-attributed sessions yet. Run `economy sync` first."));
7513
+ return;
7514
+ }
7515
+ console.log();
7516
+ console.log(chalk7.bold.cyan(` Accounts \u2014 ${period}`));
7517
+ console.log();
7518
+ printAccountBreakdown(rows);
7519
+ console.log();
7520
+ });
7321
7521
  program.command("watch").description("Live stream of incoming costs").option("--interval <seconds>", "Poll interval in seconds", "10").option("--daemon", "Watch agent data directories and sync on change").option("--agent <agent>", "Filter by agent").option("--notify <amount>", "Fire macOS notification when cumulative cost crosses this USD threshold").action(async (opts) => {
7322
7522
  const { watchCosts: watchCosts2 } = await Promise.resolve().then(() => (init_watch(), exports_watch));
7323
7523
  await watchCosts2({