@hasna/economy 0.2.21 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -583,7 +583,12 @@ function initSchema(db) {
583
583
  duration_ms INTEGER DEFAULT 0,
584
584
  timestamp TEXT NOT NULL,
585
585
  source_request_id TEXT,
586
- machine_id TEXT DEFAULT ''
586
+ machine_id TEXT DEFAULT '',
587
+ account_key TEXT DEFAULT '',
588
+ account_tool TEXT DEFAULT '',
589
+ account_name TEXT DEFAULT '',
590
+ account_email TEXT DEFAULT '',
591
+ account_source TEXT DEFAULT ''
587
592
  );
588
593
 
589
594
  CREATE TABLE IF NOT EXISTS sessions (
@@ -596,7 +601,12 @@ function initSchema(db) {
596
601
  total_cost_usd REAL DEFAULT 0,
597
602
  total_tokens INTEGER DEFAULT 0,
598
603
  request_count INTEGER DEFAULT 0,
599
- machine_id TEXT DEFAULT ''
604
+ machine_id TEXT DEFAULT '',
605
+ account_key TEXT DEFAULT '',
606
+ account_tool TEXT DEFAULT '',
607
+ account_name TEXT DEFAULT '',
608
+ account_email TEXT DEFAULT '',
609
+ account_source TEXT DEFAULT ''
600
610
  );
601
611
 
602
612
  CREATE TABLE IF NOT EXISTS projects (
@@ -751,6 +761,11 @@ function initSchema(db) {
751
761
  if (!cols.some((c) => c.name === "synced_at")) {
752
762
  db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
753
763
  }
764
+ for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
765
+ if (!cols.some((c) => c.name === column)) {
766
+ db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
767
+ }
768
+ }
754
769
  const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
755
770
  if (!sessionCols.some((c) => c.name === "attribution_tag")) {
756
771
  db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
@@ -762,6 +777,11 @@ function initSchema(db) {
762
777
  if (!sessionCols.some((c) => c.name === "synced_at")) {
763
778
  db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
764
779
  }
780
+ for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
781
+ if (!sessionCols.some((c) => c.name === column)) {
782
+ db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
783
+ }
784
+ }
765
785
  const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
766
786
  if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
767
787
  db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
@@ -772,6 +792,8 @@ function initSchema(db) {
772
792
  db.exec(`
773
793
  CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
774
794
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
795
+ CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
796
+ CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
775
797
  `);
776
798
  }
777
799
  function periodWhere(period) {
@@ -806,6 +828,22 @@ function sessionPeriodWhere(period) {
806
828
  return "1=1";
807
829
  }
808
830
  }
831
+ function requestPeriodWhere(period) {
832
+ switch (period) {
833
+ case "today":
834
+ return `DATE(timestamp) = DATE('now')`;
835
+ case "yesterday":
836
+ return `DATE(timestamp) = DATE('now', '-1 day')`;
837
+ case "week":
838
+ return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
839
+ case "month":
840
+ return `timestamp >= DATE('now', 'start of month')`;
841
+ case "year":
842
+ return `timestamp >= DATE('now', 'start of year')`;
843
+ case "all":
844
+ return "1=1";
845
+ }
846
+ }
809
847
  function upsertRequest(db, req) {
810
848
  const now = req.updated_at ?? new Date().toISOString();
811
849
  db.prepare(`
@@ -813,18 +851,20 @@ function upsertRequest(db, req) {
813
851
  (id, agent, session_id, model, input_tokens, output_tokens,
814
852
  cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
815
853
  cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
816
- source_request_id, machine_id, attribution_tag, updated_at, synced_at)
817
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
818
- `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", now, req.synced_at ?? "");
854
+ source_request_id, machine_id, attribution_tag, account_key, account_tool,
855
+ account_name, account_email, account_source, updated_at, synced_at)
856
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
857
+ `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
819
858
  }
820
859
  function upsertSession(db, session) {
821
860
  const now = session.updated_at ?? new Date().toISOString();
822
861
  db.prepare(`
823
862
  INSERT OR REPLACE INTO sessions
824
863
  (id, agent, project_path, project_name, started_at, ended_at,
825
- total_cost_usd, total_tokens, request_count, machine_id, attribution_tag, updated_at, synced_at)
826
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
827
- `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", now, session.synced_at ?? "");
864
+ total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
865
+ account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
866
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
867
+ `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
828
868
  }
829
869
  function rollupSession(db, sessionId) {
830
870
  db.prepare(`
@@ -835,9 +875,24 @@ function rollupSession(db, sessionId) {
835
875
  ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
836
876
  started_at = CASE WHEN started_at = '' OR started_at IS NULL
837
877
  THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
838
- ELSE started_at END
878
+ ELSE started_at END,
879
+ account_key = CASE WHEN account_key = '' OR account_key IS NULL
880
+ THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
881
+ ELSE account_key END,
882
+ account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
883
+ THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
884
+ ELSE account_tool END,
885
+ account_name = CASE WHEN account_name = '' OR account_name IS NULL
886
+ THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
887
+ ELSE account_name END,
888
+ account_email = CASE WHEN account_email = '' OR account_email IS NULL
889
+ THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
890
+ ELSE account_email END,
891
+ account_source = CASE WHEN account_source = '' OR account_source IS NULL
892
+ THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
893
+ ELSE account_source END
839
894
  WHERE id = ?
840
- `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
895
+ `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
841
896
  }
842
897
  function querySessions(db, filter = {}) {
843
898
  const conditions = [];
@@ -850,6 +905,11 @@ function querySessions(db, filter = {}) {
850
905
  conditions.push("project_path LIKE ?");
851
906
  params.push(`%${filter.project}%`);
852
907
  }
908
+ if (filter.account) {
909
+ const q = `%${filter.account}%`;
910
+ conditions.push("(account_key LIKE ? OR account_name LIKE ? OR account_email LIKE ?)");
911
+ params.push(q, q, q);
912
+ }
853
913
  if (filter.since) {
854
914
  conditions.push("started_at >= ?");
855
915
  params.push(filter.since);
@@ -914,6 +974,70 @@ function queryModelBreakdown(db) {
914
974
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
915
975
  `).all();
916
976
  }
977
+ function queryAgentBreakdown(db, period = "all") {
978
+ const requestWhere = requestPeriodWhere(period);
979
+ const groups = new Map;
980
+ const requestRows = db.prepare(`
981
+ SELECT agent,
982
+ COUNT(DISTINCT session_id) as sessions,
983
+ COUNT(*) as requests,
984
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
985
+ COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
986
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
987
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
988
+ COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
989
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
990
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
991
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
992
+ MAX(timestamp) as last_active
993
+ FROM requests
994
+ WHERE ${requestWhere}
995
+ GROUP BY agent
996
+ ORDER BY api_equivalent_usd DESC
997
+ `).all();
998
+ for (const row of requestRows) {
999
+ groups.set(row.agent, row);
1000
+ }
1001
+ const sessionWhere = sessionPeriodWhere(period);
1002
+ const sessionOnlyRows = db.prepare(`
1003
+ SELECT agent,
1004
+ COUNT(*) as sessions,
1005
+ COALESCE(SUM(request_count), 0) as requests,
1006
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
1007
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1008
+ MAX(started_at) as last_active
1009
+ FROM sessions
1010
+ WHERE ${sessionWhere}
1011
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1012
+ GROUP BY agent
1013
+ `).all();
1014
+ for (const row of sessionOnlyRows) {
1015
+ const existing = groups.get(row.agent) ?? {
1016
+ agent: row.agent,
1017
+ sessions: 0,
1018
+ requests: 0,
1019
+ total_tokens: 0,
1020
+ api_equivalent_usd: 0,
1021
+ billable_usd: 0,
1022
+ metered_api_usd: 0,
1023
+ subscription_included_usd: 0,
1024
+ estimated_usd: 0,
1025
+ unknown_usd: 0,
1026
+ cost_usd: 0,
1027
+ last_active: ""
1028
+ };
1029
+ existing.sessions += row.sessions;
1030
+ existing.requests += row.requests;
1031
+ existing.total_tokens += row.total_tokens;
1032
+ existing.api_equivalent_usd += row.cost_usd;
1033
+ existing.estimated_usd += row.cost_usd;
1034
+ existing.cost_usd += row.cost_usd;
1035
+ if (!existing.last_active || row.last_active > existing.last_active)
1036
+ existing.last_active = row.last_active;
1037
+ groups.set(row.agent, existing);
1038
+ }
1039
+ return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
1040
+ }
917
1041
  function labelForPath(projectPath, projectName) {
918
1042
  if (projectName && projectName.trim() !== "")
919
1043
  return projectName;
@@ -932,11 +1056,13 @@ function labelForPath(projectPath, projectName) {
932
1056
  }
933
1057
  return segments[segments.length - 1] ?? projectPath;
934
1058
  }
935
- function queryProjectBreakdown(db) {
1059
+ function queryProjectBreakdown(db, period = "all") {
1060
+ const where = sessionPeriodWhere(period);
936
1061
  const sessions = db.prepare(`
937
1062
  SELECT id, project_path, project_name, total_cost_usd, started_at
938
1063
  FROM sessions
939
- WHERE project_path != '' OR project_name != ''
1064
+ WHERE ${where}
1065
+ AND (project_path != '' OR project_name != '')
940
1066
  `).all();
941
1067
  const groups = new Map;
942
1068
  for (const s of sessions) {
@@ -975,6 +1101,87 @@ function queryProjectBreakdown(db) {
975
1101
  result.sort((a, b) => b.cost_usd - a.cost_usd);
976
1102
  return result;
977
1103
  }
1104
+ function queryAccountBreakdown(db, period = "all") {
1105
+ const sWhere = sessionPeriodWhere(period);
1106
+ const sessions = db.prepare(`
1107
+ SELECT id, account_key, account_tool, account_name, account_email, account_source,
1108
+ total_cost_usd, total_tokens, request_count, started_at
1109
+ FROM sessions
1110
+ WHERE ${sWhere}
1111
+ AND (account_key != '' OR account_tool != '' OR account_name != '' OR account_email != '')
1112
+ `).all();
1113
+ const groups = new Map;
1114
+ for (const session of sessions) {
1115
+ const key = session.account_key || `${session.account_tool}:${session.account_name}`;
1116
+ if (!key || key === ":")
1117
+ continue;
1118
+ const group = groups.get(key) ?? {
1119
+ sessionIds: [],
1120
+ account_tool: session.account_tool,
1121
+ account_name: session.account_name,
1122
+ account_email: session.account_email || null,
1123
+ account_source: session.account_source || "unknown",
1124
+ totalCost: 0,
1125
+ totalTokens: 0,
1126
+ requests: 0,
1127
+ lastActive: ""
1128
+ };
1129
+ group.sessionIds.push(session.id);
1130
+ group.totalCost += session.total_cost_usd || 0;
1131
+ group.totalTokens += session.total_tokens || 0;
1132
+ group.requests += session.request_count || 0;
1133
+ if (!group.lastActive || session.started_at > group.lastActive)
1134
+ group.lastActive = session.started_at;
1135
+ groups.set(key, group);
1136
+ }
1137
+ const result = [];
1138
+ for (const [key, group] of groups.entries()) {
1139
+ const placeholders = group.sessionIds.map(() => "?").join(",");
1140
+ const reqStats = placeholders ? db.prepare(`
1141
+ SELECT
1142
+ COUNT(*) as requests,
1143
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
1144
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1145
+ COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1146
+ COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1147
+ COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1148
+ COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd
1149
+ FROM requests WHERE session_id IN (${placeholders})
1150
+ `).get(...group.sessionIds) : {
1151
+ requests: 0,
1152
+ cost_usd: 0,
1153
+ total_tokens: 0,
1154
+ metered_api_usd: 0,
1155
+ subscription_included_usd: 0,
1156
+ estimated_usd: 0,
1157
+ unknown_usd: 0
1158
+ };
1159
+ const hasRequestCosts = reqStats.requests > 0;
1160
+ const apiEquivalentUsd = hasRequestCosts ? reqStats.cost_usd : group.totalCost;
1161
+ const estimatedUsd = hasRequestCosts ? reqStats.estimated_usd : group.totalCost;
1162
+ const billableUsd = reqStats.metered_api_usd;
1163
+ result.push({
1164
+ account_key: key,
1165
+ account_tool: group.account_tool,
1166
+ account_name: group.account_name,
1167
+ account_email: group.account_email,
1168
+ account_source: group.account_source,
1169
+ sessions: group.sessionIds.length,
1170
+ requests: reqStats.requests || group.requests,
1171
+ total_tokens: reqStats.total_tokens || group.totalTokens,
1172
+ api_equivalent_usd: apiEquivalentUsd,
1173
+ billable_usd: billableUsd,
1174
+ metered_api_usd: reqStats.metered_api_usd,
1175
+ subscription_included_usd: reqStats.subscription_included_usd,
1176
+ estimated_usd: estimatedUsd,
1177
+ unknown_usd: reqStats.unknown_usd,
1178
+ cost_usd: apiEquivalentUsd,
1179
+ last_active: group.lastActive
1180
+ });
1181
+ }
1182
+ result.sort((a, b) => b.cost_usd - a.cost_usd);
1183
+ return result;
1184
+ }
978
1185
  function queryDailyBreakdown(db, days = 30) {
979
1186
  return db.prepare(`
980
1187
  SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
@@ -1204,7 +1411,12 @@ var init_pg_migrations = __esm(() => {
1204
1411
  duration_ms INTEGER DEFAULT 0,
1205
1412
  timestamp TEXT NOT NULL,
1206
1413
  source_request_id TEXT,
1207
- machine_id TEXT DEFAULT ''
1414
+ machine_id TEXT DEFAULT '',
1415
+ account_key TEXT DEFAULT '',
1416
+ account_tool TEXT DEFAULT '',
1417
+ account_name TEXT DEFAULT '',
1418
+ account_email TEXT DEFAULT '',
1419
+ account_source TEXT DEFAULT ''
1208
1420
  )`,
1209
1421
  `CREATE TABLE IF NOT EXISTS sessions (
1210
1422
  id TEXT PRIMARY KEY,
@@ -1216,7 +1428,12 @@ var init_pg_migrations = __esm(() => {
1216
1428
  total_cost_usd REAL DEFAULT 0,
1217
1429
  total_tokens INTEGER DEFAULT 0,
1218
1430
  request_count INTEGER DEFAULT 0,
1219
- machine_id TEXT DEFAULT ''
1431
+ machine_id TEXT DEFAULT '',
1432
+ account_key TEXT DEFAULT '',
1433
+ account_tool TEXT DEFAULT '',
1434
+ account_name TEXT DEFAULT '',
1435
+ account_email TEXT DEFAULT '',
1436
+ account_source TEXT DEFAULT ''
1220
1437
  )`,
1221
1438
  `CREATE TABLE IF NOT EXISTS projects (
1222
1439
  id TEXT PRIMARY KEY,
@@ -1337,47 +1554,41 @@ var init_pg_migrations = __esm(() => {
1337
1554
  )`,
1338
1555
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
1339
1556
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1557
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1558
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1559
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1560
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1561
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1340
1562
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1341
1563
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1342
1564
  `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1565
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1566
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1567
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1568
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1569
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1343
1570
  `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1344
1571
  `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1345
1572
  `CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
1346
- `CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`
1573
+ `CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
1574
+ `CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
1575
+ `CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`
1347
1576
  ];
1348
1577
  });
1349
1578
 
1350
1579
  // src/mcp/index.ts
1351
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1352
-
1353
- // src/lib/package-metadata.ts
1354
- import { readFileSync } from "fs";
1355
- var cachedMetadata = null;
1356
- function getPackageMetadata() {
1357
- if (cachedMetadata)
1358
- return cachedMetadata;
1359
- const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf8");
1360
- const parsed = JSON.parse(raw);
1361
- cachedMetadata = {
1362
- name: parsed.name ?? "@hasna/economy",
1363
- version: parsed.version ?? "0.0.0"
1364
- };
1365
- return cachedMetadata;
1366
- }
1367
- var packageMetadata = getPackageMetadata();
1368
-
1369
- // src/mcp/server.ts
1370
1580
  init_database();
1371
1581
  init_pg_migrations();
1372
1582
  import { randomUUID } from "crypto";
1373
1583
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1584
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1374
1585
  import { registerCloudTools } from "@hasna/cloud";
1375
1586
  import { z } from "zod";
1376
1587
 
1377
1588
  // src/ingest/claude.ts
1378
1589
  init_database();
1379
1590
  init_pricing();
1380
- import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync2, statSync as statSync2 } from "fs";
1591
+ import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
1381
1592
  import { homedir as homedir2 } from "os";
1382
1593
  import { join as join2, basename } from "path";
1383
1594
 
@@ -1400,7 +1611,6 @@ function periodWhere2(period, column) {
1400
1611
  }
1401
1612
  function prorateMonthlyFee(monthlyFee, period) {
1402
1613
  const now = new Date;
1403
- const dayOfMonth = now.getDate();
1404
1614
  const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
1405
1615
  switch (period) {
1406
1616
  case "today":
@@ -1482,6 +1692,131 @@ function defaultCostBasisForAgent(agent) {
1482
1692
  return "estimated";
1483
1693
  }
1484
1694
 
1695
+ // src/lib/accounts.ts
1696
+ var AGENT_ACCOUNT_TOOLS = {
1697
+ claude: ["claude"],
1698
+ takumi: ["takumi", "claude"],
1699
+ codex: ["codex"],
1700
+ gemini: ["gemini"],
1701
+ opencode: ["opencode"],
1702
+ cursor: ["cursor"],
1703
+ pi: ["pi"],
1704
+ hermes: ["hermes"]
1705
+ };
1706
+ function accountKey(tool, name) {
1707
+ return `${tool}:${name}`;
1708
+ }
1709
+ function normalizeDir(value) {
1710
+ return value.replace(/\/+$/, "");
1711
+ }
1712
+ function fromProfile(profile, source) {
1713
+ return {
1714
+ account_key: accountKey(profile.tool, profile.name),
1715
+ account_tool: profile.tool,
1716
+ account_name: profile.name,
1717
+ ...profile.email ? { account_email: profile.email } : {},
1718
+ account_source: source
1719
+ };
1720
+ }
1721
+ function fromOverride(raw, agent) {
1722
+ const value = raw.trim();
1723
+ if (!value)
1724
+ return null;
1725
+ const candidateTool = AGENT_ACCOUNT_TOOLS[agent][0] ?? agent;
1726
+ const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
1727
+ if (!tool || !name)
1728
+ return null;
1729
+ return {
1730
+ account_key: accountKey(tool, name),
1731
+ account_tool: tool,
1732
+ account_name: name,
1733
+ account_source: "override"
1734
+ };
1735
+ }
1736
+ function envOverride(agent, env) {
1737
+ const agentPrefix = agent.toUpperCase().replace(/[^A-Z0-9]/g, "_");
1738
+ const raw = env[`ECONOMY_${agentPrefix}_ACCOUNT_KEY`] ?? env[`ECONOMY_${agentPrefix}_ACCOUNT`] ?? env["ECONOMY_ACCOUNT_KEY"] ?? env["ECONOMY_ACCOUNT"];
1739
+ if (raw)
1740
+ return fromOverride(raw, agent);
1741
+ const tool = env[`ECONOMY_${agentPrefix}_ACCOUNT_TOOL`] ?? env["ECONOMY_ACCOUNT_TOOL"];
1742
+ const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
1743
+ if (!tool || !name)
1744
+ return null;
1745
+ return {
1746
+ account_key: accountKey(tool, name),
1747
+ account_tool: tool,
1748
+ account_name: name,
1749
+ account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
1750
+ account_source: "override"
1751
+ };
1752
+ }
1753
+ function knownToolIds(api) {
1754
+ try {
1755
+ return new Set(api.listTools().map((tool) => tool.id));
1756
+ } catch {
1757
+ return new Set;
1758
+ }
1759
+ }
1760
+ function profileForEnvDir(api, tool, env) {
1761
+ const configuredDir = env[tool.envVar];
1762
+ if (!configuredDir)
1763
+ return null;
1764
+ const normalized = normalizeDir(configuredDir);
1765
+ try {
1766
+ return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
1767
+ } catch {
1768
+ return null;
1769
+ }
1770
+ }
1771
+ async function resolveAccountForAgent(agent, env = process.env) {
1772
+ const override = envOverride(agent, env);
1773
+ if (override)
1774
+ return override;
1775
+ let api;
1776
+ try {
1777
+ api = await import("@hasna/accounts");
1778
+ } catch {
1779
+ return null;
1780
+ }
1781
+ const toolIds = knownToolIds(api);
1782
+ for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
1783
+ if (!toolIds.has(toolId))
1784
+ continue;
1785
+ let tool;
1786
+ try {
1787
+ tool = api.getTool(toolId);
1788
+ } catch {
1789
+ continue;
1790
+ }
1791
+ const envProfile = profileForEnvDir(api, tool, env);
1792
+ if (envProfile)
1793
+ return fromProfile(envProfile, "env");
1794
+ try {
1795
+ const applied = api.appliedProfile(toolId);
1796
+ if (applied)
1797
+ return fromProfile(applied, "applied");
1798
+ } catch {}
1799
+ try {
1800
+ const current = api.currentProfile(toolId);
1801
+ if (current)
1802
+ return fromProfile(current, "current");
1803
+ } catch {}
1804
+ }
1805
+ return null;
1806
+ }
1807
+ function withAccount(record, account) {
1808
+ if (!account)
1809
+ return record;
1810
+ return {
1811
+ ...record,
1812
+ account_key: account.account_key,
1813
+ account_tool: account.account_tool,
1814
+ account_name: account.account_name,
1815
+ account_email: account.account_email ?? "",
1816
+ account_source: account.account_source
1817
+ };
1818
+ }
1819
+
1485
1820
  // src/ingest/claude.ts
1486
1821
  function autoDetectProject(cwd, projects) {
1487
1822
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
@@ -1523,6 +1858,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1523
1858
  let totalRequests = 0;
1524
1859
  const touchedSessions = new Set;
1525
1860
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
1861
+ const account = await resolveAccountForAgent(agentName);
1526
1862
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
1527
1863
  for (const projectDirEntry of projectDirs) {
1528
1864
  const projectDirPath = join2(projectsDir, projectDirEntry.name);
@@ -1541,7 +1877,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1541
1877
  continue;
1542
1878
  let lines;
1543
1879
  try {
1544
- lines = readFileSync2(filePath, "utf-8").split(`
1880
+ lines = readFileSync(filePath, "utf-8").split(`
1545
1881
  `).filter((l) => l.trim());
1546
1882
  } catch {
1547
1883
  continue;
@@ -1586,7 +1922,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1586
1922
  }
1587
1923
  const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
1588
1924
  const reqId = `${agentName}-${sourceRequestId}`;
1589
- upsertRequest(db, {
1925
+ upsertRequest(db, withAccount({
1590
1926
  id: reqId,
1591
1927
  agent: agentName,
1592
1928
  session_id: sessionId,
@@ -1603,7 +1939,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1603
1939
  timestamp,
1604
1940
  source_request_id: sourceRequestId,
1605
1941
  machine_id: machineId
1606
- });
1942
+ }, account));
1607
1943
  if (!touchedSessions.has(sessionId)) {
1608
1944
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
1609
1945
  if (!existing) {
@@ -1621,7 +1957,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1621
1957
  request_count: 0,
1622
1958
  machine_id: machineId
1623
1959
  };
1624
- upsertSession(db, session);
1960
+ upsertSession(db, withAccount(session, account));
1625
1961
  }
1626
1962
  touchedSessions.add(sessionId);
1627
1963
  }
@@ -1661,13 +1997,13 @@ function supportsClaudeDataResidencyPricing(model) {
1661
1997
  // src/ingest/codex.ts
1662
1998
  init_database();
1663
1999
  init_pricing();
1664
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2000
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1665
2001
  import { homedir as homedir3 } from "os";
1666
2002
  import { join as join3, basename as basename2 } from "path";
1667
2003
  import { Database as BunDatabase } from "bun:sqlite";
1668
2004
  var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
1669
2005
  var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
1670
- var CODEX_INGEST_VERSION = "rollout-token-dedupe-v2";
2006
+ var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
1671
2007
  function codexDbPath() {
1672
2008
  return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
1673
2009
  }
@@ -1679,7 +2015,7 @@ function readCodexModel() {
1679
2015
  if (!existsSync3(configPath))
1680
2016
  return "gpt-5-codex";
1681
2017
  try {
1682
- const content = readFileSync3(configPath, "utf-8");
2018
+ const content = readFileSync2(configPath, "utf-8");
1683
2019
  const match = content.match(/^model\s*=\s*"([^"]+)"/m);
1684
2020
  return match?.[1] ?? "gpt-5-codex";
1685
2021
  } catch {
@@ -1700,9 +2036,10 @@ function buildThreadQuery(codexDb) {
1700
2036
  function readTokenEvents(rolloutPath) {
1701
2037
  if (!rolloutPath || !existsSync3(rolloutPath))
1702
2038
  return [];
1703
- const events = [];
1704
- const seen = new Set;
1705
- for (const line of readFileSync3(rolloutPath, "utf-8").split(`
2039
+ const fallbackUsages = new Map;
2040
+ let fallbackTimestamp;
2041
+ let aggregate = null;
2042
+ for (const line of readFileSync2(rolloutPath, "utf-8").split(`
1706
2043
  `)) {
1707
2044
  if (!line.trim())
1708
2045
  continue;
@@ -1718,20 +2055,48 @@ function readTokenEvents(rolloutPath) {
1718
2055
  if (!payload || payload["type"] !== "token_count")
1719
2056
  continue;
1720
2057
  const info = payload["info"];
2058
+ const timestamp = entry["timestamp"];
2059
+ const entryTimestamp = typeof timestamp === "string" ? timestamp : undefined;
2060
+ const totalUsage = info?.["total_token_usage"];
2061
+ if (totalUsage && tokenTotal(totalUsage) > 0) {
2062
+ aggregate = { usage: totalUsage, timestamp: entryTimestamp };
2063
+ continue;
2064
+ }
1721
2065
  const usage = info?.["last_token_usage"];
1722
2066
  if (!usage)
1723
2067
  continue;
1724
- const total = usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
1725
- if (total <= 0)
2068
+ if (tokenTotal(usage) <= 0)
1726
2069
  continue;
1727
2070
  const key = JSON.stringify(usage);
1728
- if (seen.has(key))
1729
- continue;
1730
- seen.add(key);
1731
- const timestamp = entry["timestamp"];
1732
- events.push({ usage, timestamp: typeof timestamp === "string" ? timestamp : undefined });
2071
+ if (!fallbackUsages.has(key))
2072
+ fallbackUsages.set(key, usage);
2073
+ fallbackTimestamp = entryTimestamp ?? fallbackTimestamp;
1733
2074
  }
1734
- return events;
2075
+ if (aggregate)
2076
+ return [aggregate];
2077
+ if (fallbackUsages.size === 0)
2078
+ return [];
2079
+ return [{ usage: sumTokenUsages([...fallbackUsages.values()]), timestamp: fallbackTimestamp }];
2080
+ }
2081
+ function tokenTotal(usage) {
2082
+ return usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
2083
+ }
2084
+ function sumTokenUsages(usages) {
2085
+ const result = {
2086
+ input_tokens: 0,
2087
+ cached_input_tokens: 0,
2088
+ output_tokens: 0,
2089
+ reasoning_output_tokens: 0,
2090
+ total_tokens: 0
2091
+ };
2092
+ for (const usage of usages) {
2093
+ result.input_tokens = (result.input_tokens ?? 0) + (usage.input_tokens ?? 0);
2094
+ result.cached_input_tokens = (result.cached_input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
2095
+ result.output_tokens = (result.output_tokens ?? 0) + (usage.output_tokens ?? 0);
2096
+ result.reasoning_output_tokens = (result.reasoning_output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0);
2097
+ result.total_tokens = (result.total_tokens ?? 0) + tokenTotal(usage);
2098
+ }
2099
+ return result;
1735
2100
  }
1736
2101
  function fallbackEvents(totalTokens) {
1737
2102
  const inputTokens = Math.floor(totalTokens * 0.6);
@@ -1755,6 +2120,7 @@ async function ingestCodex(db, verbose = false) {
1755
2120
  let codexDb = null;
1756
2121
  let ingested = 0;
1757
2122
  let requests = 0;
2123
+ const account = await resolveAccountForAgent("codex");
1758
2124
  try {
1759
2125
  codexDb = new BunDatabase(dbPath, { readonly: true });
1760
2126
  const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
@@ -1769,7 +2135,7 @@ async function ingestCodex(db, verbose = false) {
1769
2135
  const sessionId = `codex-${thread.id}`;
1770
2136
  const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
1771
2137
  const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
1772
- upsertSession(db, {
2138
+ upsertSession(db, withAccount({
1773
2139
  id: sessionId,
1774
2140
  agent: "codex",
1775
2141
  project_path: projectPath,
@@ -1780,9 +2146,10 @@ async function ingestCodex(db, verbose = false) {
1780
2146
  total_tokens: 0,
1781
2147
  request_count: 0,
1782
2148
  machine_id: machineId
1783
- });
2149
+ }, account));
1784
2150
  const events = readTokenEvents(thread.rollout_path);
1785
2151
  const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
2152
+ const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
1786
2153
  db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
1787
2154
  tokenEvents.forEach((event, index) => {
1788
2155
  const usage = event.usage;
@@ -1793,7 +2160,7 @@ async function ingestCodex(db, verbose = false) {
1793
2160
  const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
1794
2161
  const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
1795
2162
  const requestId = `${sessionId}-${index}`;
1796
- upsertRequest(db, {
2163
+ upsertRequest(db, withAccount({
1797
2164
  id: requestId,
1798
2165
  agent: "codex",
1799
2166
  session_id: sessionId,
@@ -1808,14 +2175,14 @@ async function ingestCodex(db, verbose = false) {
1808
2175
  timestamp,
1809
2176
  source_request_id: requestId,
1810
2177
  machine_id: machineId
1811
- });
2178
+ }, account));
1812
2179
  requests++;
1813
2180
  });
1814
2181
  rollupSession(db, sessionId);
1815
2182
  setIngestState(db, "codex", thread.id, stateValue);
1816
2183
  ingested++;
1817
2184
  if (verbose)
1818
- console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens on ${model}`);
2185
+ console.log(`Codex session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
1819
2186
  }
1820
2187
  } finally {
1821
2188
  codexDb?.close();
@@ -1826,7 +2193,7 @@ async function ingestCodex(db, verbose = false) {
1826
2193
  // src/ingest/gemini.ts
1827
2194
  init_database();
1828
2195
  init_pricing();
1829
- import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync4, statSync as statSync3 } from "fs";
2196
+ import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
1830
2197
  import { homedir as homedir4 } from "os";
1831
2198
  import { join as join4, basename as basename3 } from "path";
1832
2199
  var DEFAULT_GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
@@ -1866,7 +2233,7 @@ function projectRoot(projectDir, chatData) {
1866
2233
  const rootFile = join4(projectDir, ".project_root");
1867
2234
  try {
1868
2235
  if (existsSync4(rootFile))
1869
- return readFileSync4(rootFile, "utf-8").trim();
2236
+ return readFileSync3(rootFile, "utf-8").trim();
1870
2237
  } catch {}
1871
2238
  return "";
1872
2239
  }
@@ -1882,6 +2249,7 @@ async function ingestGemini(db, verbose) {
1882
2249
  let totalSessions = 0;
1883
2250
  let totalRequests = 0;
1884
2251
  const touchedSessions = new Set;
2252
+ const account = await resolveAccountForAgent("gemini");
1885
2253
  const projectDirs = listProjectDirs(tmpDir, historyDir);
1886
2254
  for (const projectDir of projectDirs) {
1887
2255
  const chatsDir = join4(projectDir, "chats");
@@ -1906,7 +2274,7 @@ async function ingestGemini(db, verbose) {
1906
2274
  continue;
1907
2275
  let chatData;
1908
2276
  try {
1909
- chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
2277
+ chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
1910
2278
  } catch {
1911
2279
  continue;
1912
2280
  }
@@ -1930,7 +2298,7 @@ async function ingestGemini(db, verbose) {
1930
2298
  request_count: 0,
1931
2299
  machine_id: machineId
1932
2300
  };
1933
- upsertSession(db, session);
2301
+ upsertSession(db, withAccount(session, account));
1934
2302
  totalSessions++;
1935
2303
  }
1936
2304
  touchedSessions.add(sessionId);
@@ -1954,7 +2322,7 @@ async function ingestGemini(db, verbose) {
1954
2322
  const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
1955
2323
  const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
1956
2324
  const requestId = `gemini-${sessionId}-${message.id ?? index}`;
1957
- upsertRequest(db, {
2325
+ upsertRequest(db, withAccount({
1958
2326
  id: requestId,
1959
2327
  agent: "gemini",
1960
2328
  session_id: sessionId,
@@ -1969,7 +2337,7 @@ async function ingestGemini(db, verbose) {
1969
2337
  timestamp,
1970
2338
  source_request_id: message.id ?? requestId,
1971
2339
  machine_id: machineId
1972
- });
2340
+ }, account));
1973
2341
  totalRequests++;
1974
2342
  }
1975
2343
  setIngestState(db, "gemini", stateKey, fileMtime);
@@ -1984,7 +2352,7 @@ async function ingestGemini(db, verbose) {
1984
2352
  // src/ingest/opencode.ts
1985
2353
  init_database();
1986
2354
  init_pricing();
1987
- import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
2355
+ import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
1988
2356
  import { homedir as homedir5 } from "os";
1989
2357
  import { join as join5 } from "path";
1990
2358
  var OPENCODE_STORAGE = join5(homedir5(), ".local", "share", "opencode", "storage");
@@ -2018,6 +2386,7 @@ async function ingestOpenCode(db, verbose = false) {
2018
2386
  const touched = new Set;
2019
2387
  const machineId = getMachineId();
2020
2388
  const now = new Date().toISOString();
2389
+ const account = await resolveAccountForAgent("opencode");
2021
2390
  for (const file of files) {
2022
2391
  const mtime = statSync4(file).mtimeMs;
2023
2392
  const stateKey = file;
@@ -2026,7 +2395,7 @@ async function ingestOpenCode(db, verbose = false) {
2026
2395
  continue;
2027
2396
  let parsed;
2028
2397
  try {
2029
- parsed = JSON.parse(readFileSync5(file, "utf-8"));
2398
+ parsed = JSON.parse(readFileSync4(file, "utf-8"));
2030
2399
  } catch {
2031
2400
  continue;
2032
2401
  }
@@ -2047,7 +2416,7 @@ async function ingestOpenCode(db, verbose = false) {
2047
2416
  const sourceId = file.replace(OPENCODE_STORAGE, "");
2048
2417
  const reqId = `opencode-${sourceId}`;
2049
2418
  const costUsd = usage.cost ?? computeCostFromDb(db, model, input, output, cacheRead, cacheWrite, 0);
2050
- upsertRequest(db, {
2419
+ upsertRequest(db, withAccount({
2051
2420
  id: reqId,
2052
2421
  agent: "opencode",
2053
2422
  session_id: sessionId,
@@ -2063,10 +2432,10 @@ async function ingestOpenCode(db, verbose = false) {
2063
2432
  source_request_id: sourceId,
2064
2433
  machine_id: machineId,
2065
2434
  updated_at: now
2066
- });
2435
+ }, account));
2067
2436
  requests++;
2068
2437
  if (!touched.has(sessionId)) {
2069
- upsertSession(db, {
2438
+ upsertSession(db, withAccount({
2070
2439
  id: sessionId,
2071
2440
  agent: "opencode",
2072
2441
  project_path: "",
@@ -2078,7 +2447,7 @@ async function ingestOpenCode(db, verbose = false) {
2078
2447
  request_count: 0,
2079
2448
  machine_id: machineId,
2080
2449
  updated_at: now
2081
- });
2450
+ }, account));
2082
2451
  touched.add(sessionId);
2083
2452
  }
2084
2453
  setIngestState(db, "opencode", stateKey, String(mtime));
@@ -2125,6 +2494,7 @@ async function ingestCursor(db, verbose = false) {
2125
2494
  const machineId = getMachineId();
2126
2495
  const now = new Date().toISOString();
2127
2496
  let snapshots = 0;
2497
+ const account = await resolveAccountForAgent("cursor");
2128
2498
  const usage = await cursorFetch("/api/usage", token);
2129
2499
  if (usage?.premiumRequests != null && usage.maxPremiumRequests) {
2130
2500
  upsertUsageSnapshot(db, {
@@ -2172,7 +2542,7 @@ async function ingestCursor(db, verbose = false) {
2172
2542
  }
2173
2543
  const sessionId = `cursor-${today}-${machineId}`;
2174
2544
  if (onDemand + included > 0) {
2175
- upsertSession(db, {
2545
+ upsertSession(db, withAccount({
2176
2546
  id: sessionId,
2177
2547
  agent: "cursor",
2178
2548
  project_path: "",
@@ -2184,8 +2554,8 @@ async function ingestCursor(db, verbose = false) {
2184
2554
  request_count: 1,
2185
2555
  machine_id: machineId,
2186
2556
  updated_at: now
2187
- });
2188
- upsertRequest(db, {
2557
+ }, account));
2558
+ upsertRequest(db, withAccount({
2189
2559
  id: `cursor-${today}-${machineId}-usage`,
2190
2560
  agent: "cursor",
2191
2561
  session_id: sessionId,
@@ -2201,7 +2571,7 @@ async function ingestCursor(db, verbose = false) {
2201
2571
  source_request_id: today,
2202
2572
  machine_id: machineId,
2203
2573
  updated_at: now
2204
- });
2574
+ }, account));
2205
2575
  rollupSession(db, sessionId);
2206
2576
  }
2207
2577
  setIngestState(db, "cursor", `sync-${today}`, now);
@@ -2212,7 +2582,7 @@ async function ingestCursor(db, verbose = false) {
2212
2582
 
2213
2583
  // src/ingest/pi.ts
2214
2584
  init_database();
2215
- import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
2585
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
2216
2586
  import { homedir as homedir6 } from "os";
2217
2587
  import { join as join6 } from "path";
2218
2588
  var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join6(homedir6(), ".pi", "agent", "sessions");
@@ -2234,6 +2604,7 @@ async function ingestPi(db, verbose = false) {
2234
2604
  const touched = new Set;
2235
2605
  const machineId = getMachineId();
2236
2606
  const now = new Date().toISOString();
2607
+ const account = await resolveAccountForAgent("pi");
2237
2608
  for (const file of files) {
2238
2609
  const mtime = statSync5(file).mtimeMs;
2239
2610
  const prev = getIngestState(db, "pi", file);
@@ -2241,7 +2612,7 @@ async function ingestPi(db, verbose = false) {
2241
2612
  continue;
2242
2613
  let data;
2243
2614
  try {
2244
- data = JSON.parse(readFileSync6(file, "utf-8"));
2615
+ data = JSON.parse(readFileSync5(file, "utf-8"));
2245
2616
  } catch {
2246
2617
  continue;
2247
2618
  }
@@ -2259,7 +2630,7 @@ async function ingestPi(db, verbose = false) {
2259
2630
  const model = turn.model ?? turn.provider ?? "unknown";
2260
2631
  const timestamp = turn.timestamp ?? new Date(statSync5(file).mtime).toISOString();
2261
2632
  const reqId = `pi-${sessionId}-${i}`;
2262
- upsertRequest(db, {
2633
+ upsertRequest(db, withAccount({
2263
2634
  id: reqId,
2264
2635
  agent: "pi",
2265
2636
  session_id: sessionId,
@@ -2275,11 +2646,11 @@ async function ingestPi(db, verbose = false) {
2275
2646
  source_request_id: `${sessionId}-${i}`,
2276
2647
  machine_id: machineId,
2277
2648
  updated_at: now
2278
- });
2649
+ }, account));
2279
2650
  requests++;
2280
2651
  }
2281
2652
  if (turns.length > 0) {
2282
- upsertSession(db, {
2653
+ upsertSession(db, withAccount({
2283
2654
  id: sessionId,
2284
2655
  agent: "pi",
2285
2656
  project_path: "",
@@ -2291,7 +2662,7 @@ async function ingestPi(db, verbose = false) {
2291
2662
  request_count: 0,
2292
2663
  machine_id: machineId,
2293
2664
  updated_at: now
2294
- });
2665
+ }, account));
2295
2666
  touched.add(sessionId);
2296
2667
  }
2297
2668
  setIngestState(db, "pi", file, String(mtime));
@@ -2339,13 +2710,14 @@ async function ingestHermes(db, verbose = false) {
2339
2710
  const machineId = getMachineId();
2340
2711
  const now = new Date().toISOString();
2341
2712
  let requests = 0;
2713
+ const account = await resolveAccountForAgent("hermes");
2342
2714
  for (const row of rows) {
2343
2715
  const sessionId = `hermes-${row.id}`;
2344
2716
  const startedAt = new Date(row.started_at * 1000).toISOString();
2345
2717
  const endedAt = row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null;
2346
2718
  const cost = row.actual_cost_usd ?? row.estimated_cost_usd ?? 0;
2347
2719
  const tokens = row.input_tokens + row.output_tokens + row.cache_read_tokens + row.cache_write_tokens + row.reasoning_tokens;
2348
- upsertSession(db, {
2720
+ upsertSession(db, withAccount({
2349
2721
  id: sessionId,
2350
2722
  agent: "hermes",
2351
2723
  project_path: row.source ?? "",
@@ -2357,9 +2729,9 @@ async function ingestHermes(db, verbose = false) {
2357
2729
  request_count: 1,
2358
2730
  machine_id: machineId,
2359
2731
  updated_at: now
2360
- });
2732
+ }, account));
2361
2733
  const reqId = `hermes-${row.id}-rollup`;
2362
- upsertRequest(db, {
2734
+ upsertRequest(db, withAccount({
2363
2735
  id: reqId,
2364
2736
  agent: "hermes",
2365
2737
  session_id: sessionId,
@@ -2375,7 +2747,7 @@ async function ingestHermes(db, verbose = false) {
2375
2747
  source_request_id: row.id,
2376
2748
  machine_id: machineId,
2377
2749
  updated_at: now
2378
- });
2750
+ }, account));
2379
2751
  requests++;
2380
2752
  rollupSession(db, sessionId);
2381
2753
  if (verbose)
@@ -2395,7 +2767,7 @@ function statSyncSafe(path) {
2395
2767
 
2396
2768
  // src/ingest/claude-quota.ts
2397
2769
  init_database();
2398
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
2770
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
2399
2771
 
2400
2772
  // src/lib/paths.ts
2401
2773
  import { homedir as homedir8 } from "os";
@@ -2433,7 +2805,7 @@ function readClaudeToken() {
2433
2805
  if (!existsSync8(CREDENTIALS_PATH))
2434
2806
  return null;
2435
2807
  try {
2436
- const creds = JSON.parse(readFileSync8(CREDENTIALS_PATH, "utf-8"));
2808
+ const creds = JSON.parse(readFileSync6(CREDENTIALS_PATH, "utf-8"));
2437
2809
  const oauth = creds.claudeAiOauth;
2438
2810
  if (!oauth?.accessToken)
2439
2811
  return null;
@@ -2569,7 +2941,7 @@ async function ingestClaudeQuota(db, verbose = false) {
2569
2941
 
2570
2942
  // src/ingest/codex-quota.ts
2571
2943
  init_database();
2572
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
2944
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
2573
2945
  var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
2574
2946
  function readCodexAuth() {
2575
2947
  const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
@@ -2579,7 +2951,7 @@ function readCodexAuth() {
2579
2951
  if (!existsSync9(authPath))
2580
2952
  return null;
2581
2953
  try {
2582
- const auth = JSON.parse(readFileSync9(authPath, "utf-8"));
2954
+ const auth = JSON.parse(readFileSync7(authPath, "utf-8"));
2583
2955
  const token = auth.tokens?.access_token;
2584
2956
  if (!token)
2585
2957
  return null;
@@ -2711,6 +3083,24 @@ init_database();
2711
3083
 
2712
3084
  // src/lib/cloud-sync.ts
2713
3085
  init_database();
3086
+
3087
+ // src/lib/package-metadata.ts
3088
+ import { readFileSync as readFileSync8 } from "fs";
3089
+ var cachedMetadata = null;
3090
+ function getPackageMetadata() {
3091
+ if (cachedMetadata)
3092
+ return cachedMetadata;
3093
+ const raw = readFileSync8(new URL("../../package.json", import.meta.url), "utf8");
3094
+ const parsed = JSON.parse(raw);
3095
+ cachedMetadata = {
3096
+ name: parsed.name ?? "@hasna/economy",
3097
+ version: parsed.version ?? "0.0.0"
3098
+ };
3099
+ return cachedMetadata;
3100
+ }
3101
+ var packageMetadata = getPackageMetadata();
3102
+
3103
+ // src/lib/cloud-sync.ts
2714
3104
  var CLOUD_TABLES = [
2715
3105
  "requests",
2716
3106
  "sessions",
@@ -2752,44 +3142,27 @@ async function runCloudMigrations(cloud) {
2752
3142
  await cloud.run(sql);
2753
3143
  }
2754
3144
  }
2755
- function isCloudIncrementalEnabled() {
2756
- return process.env["ECONOMY_CLOUD_INCREMENTAL"] === "1" || process.env["ECONOMY_CLOUD_INCREMENTAL"] === "true";
2757
- }
2758
3145
  async function cloudPush(opts) {
2759
- const { syncPush, incrementalSyncPush, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
3146
+ const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
2760
3147
  const cloud = await getCloudPg();
2761
3148
  const local = new SqliteAdapter(getDbPath());
2762
3149
  await runCloudMigrations(cloud);
2763
3150
  const tables = opts?.tables ?? [...CLOUD_TABLES];
2764
- let rows = 0;
2765
- if (isCloudIncrementalEnabled()) {
2766
- ensureSyncMetaTable(local);
2767
- const results = incrementalSyncPush(local, cloud, tables, { conflictColumn: "updated_at" });
2768
- rows = results.reduce((s, r) => s + r.synced_rows, 0);
2769
- } else {
2770
- const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
2771
- rows = results.reduce((s, r) => s + r.rowsWritten, 0);
2772
- }
3151
+ const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
3152
+ const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
2773
3153
  touchMachineRegistry(local, "push");
2774
3154
  local.close();
2775
3155
  await cloud.close();
2776
3156
  return { rows, machine: getMachineId() };
2777
3157
  }
2778
3158
  async function cloudPull(opts) {
2779
- const { syncPull, incrementalSyncPull, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
3159
+ const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
2780
3160
  const cloud = await getCloudPg();
2781
3161
  const local = new SqliteAdapter(getDbPath());
2782
3162
  await runCloudMigrations(cloud);
2783
3163
  const tables = opts?.tables ?? [...CLOUD_TABLES];
2784
- let rows = 0;
2785
- if (isCloudIncrementalEnabled()) {
2786
- ensureSyncMetaTable(local);
2787
- const results = incrementalSyncPull(cloud, local, tables, { conflictColumn: "updated_at" });
2788
- rows = results.reduce((s, r) => s + r.synced_rows, 0);
2789
- } else {
2790
- const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
2791
- rows = results.reduce((s, r) => s + r.rowsWritten, 0);
2792
- }
3164
+ const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
3165
+ const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
2793
3166
  touchMachineRegistry(local, "pull");
2794
3167
  local.close();
2795
3168
  await cloud.close();
@@ -2893,496 +3266,442 @@ var AGENTS = [
2893
3266
  "hermes"
2894
3267
  ];
2895
3268
 
2896
- // src/mcp/server.ts
3269
+ // src/mcp/index.ts
2897
3270
  init_database();
2898
3271
  init_pricing();
2899
3272
  init_pricing();
2900
- var MCP_NAME = "economy";
2901
- var DEFAULT_MCP_HTTP_PORT = 8860;
2902
- function buildServer() {
2903
- const db = openDatabase();
2904
- ensurePricingSeeded(db);
2905
- const server = new McpServer({
2906
- name: MCP_NAME,
2907
- version: packageMetadata.version
2908
- });
2909
- const _econAgents = new Map;
2910
- const TOOL_NAMES = [
2911
- "get_cost_summary",
2912
- "get_sessions",
2913
- "get_top_sessions",
2914
- "get_model_breakdown",
2915
- "get_project_breakdown",
2916
- "get_budget_status",
2917
- "set_budget",
2918
- "remove_budget",
2919
- "get_pricing",
2920
- "set_pricing",
2921
- "remove_pricing",
2922
- "get_daily",
2923
- "get_billing_summary",
2924
- "get_session_detail",
2925
- "sync",
2926
- "search_tools",
2927
- "describe_tools",
2928
- "get_goals",
2929
- "set_goal",
2930
- "remove_goal",
2931
- "list_machines",
2932
- "register_agent",
2933
- "heartbeat",
2934
- "set_focus",
2935
- "list_agents",
2936
- "send_feedback"
2937
- ];
2938
- const TOOL_DESCRIPTIONS = {
2939
- get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
2940
- get_sessions: "agent(claude|takumi|codex|gemini), project(partial), machine?(hostname), limit(20) -> compact session table",
2941
- get_top_sessions: "n(10), agent(claude|takumi|codex|gemini) -> top sessions by cost",
2942
- list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
2943
- get_model_breakdown: "no params -> model, requests, tokens, cost",
2944
- get_project_breakdown: "no params -> project_name, sessions, cost",
2945
- get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
2946
- set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, alert_at_percent? -> create budget",
2947
- remove_budget: "id -> delete budget",
2948
- get_pricing: "no params -> model pricing rows with input, output, cache read/write, and cache storage rates",
2949
- set_pricing: "model, input_per_1m, output_per_1m, cache_read_per_1m?, cache_write_per_1m?, cache_write_1h_per_1m?, cache_storage_per_1m_hour? -> create/update pricing",
2950
- remove_pricing: "model -> delete pricing row",
2951
- get_daily: "days(30) -> daily cost table grouped by date and agent",
2952
- get_billing_summary: "period(today|yesterday|week|month|year|all) -> actual provider billing totals",
2953
- get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
2954
- sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
2955
- search_tools: "query substring -> tool name list",
2956
- describe_tools: "names[] -> one-line parameter hints",
2957
- get_goals: "no params -> goal progress summary",
2958
- set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
2959
- remove_goal: "id -> delete goal",
2960
- register_agent: "name, session_id? -> register agent session",
2961
- heartbeat: "agent_id -> update last_seen_at",
2962
- set_focus: "agent_id, project_id? -> set active project context",
2963
- list_agents: "no params -> registered agent list",
2964
- send_feedback: "message, email?, category? -> save feedback locally"
2965
- };
2966
- const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
2967
- const fmtTok = (n) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
2968
- function fmtSession(s) {
2969
- const id = String(s["id"] ?? "").slice(0, 8);
2970
- const agent = String(s["agent"] ?? "");
2971
- const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
2972
- const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
2973
- const tok = fmtTok(Number(s["total_tokens"] ?? 0));
2974
- return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
2975
- }
2976
- function text(text2) {
2977
- return { content: [{ type: "text", text: text2 }] };
2978
- }
2979
- function textError(message) {
2980
- return { content: [{ type: "text", text: message }], isError: true };
2981
- }
2982
- server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z.string().optional() }, async ({ query }) => {
2983
- const q = query?.toLowerCase();
2984
- const matches = q ? TOOL_NAMES.filter((name) => name.includes(q)) : [...TOOL_NAMES];
2985
- return text(matches.join(", "));
2986
- });
2987
- server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z.array(z.string()) }, async ({ names }) => {
2988
- const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
3273
+ function printHelp() {
3274
+ console.log(`Usage: economy-mcp [options]
3275
+
3276
+ Runs the ${packageMetadata.name} MCP stdio server.
3277
+
3278
+ Options:
3279
+ -V, --version output the version number
3280
+ -h, --help display help for command`);
3281
+ }
3282
+ var args = process.argv.slice(2);
3283
+ if (args.includes("--help") || args.includes("-h")) {
3284
+ printHelp();
3285
+ process.exit(0);
3286
+ }
3287
+ if (args.includes("--version") || args.includes("-V")) {
3288
+ console.log(packageMetadata.version);
3289
+ process.exit(0);
3290
+ }
3291
+ var db = openDatabase();
3292
+ ensurePricingSeeded(db);
3293
+ var server = new McpServer({
3294
+ name: "economy",
3295
+ version: packageMetadata.version
3296
+ });
3297
+ var _econAgents = new Map;
3298
+ var TOOL_NAMES = [
3299
+ "get_cost_summary",
3300
+ "get_sessions",
3301
+ "get_top_sessions",
3302
+ "get_model_breakdown",
3303
+ "get_project_breakdown",
3304
+ "get_agent_breakdown",
3305
+ "get_account_breakdown",
3306
+ "get_budget_status",
3307
+ "set_budget",
3308
+ "remove_budget",
3309
+ "get_pricing",
3310
+ "set_pricing",
3311
+ "remove_pricing",
3312
+ "get_daily",
3313
+ "get_billing_summary",
3314
+ "get_session_detail",
3315
+ "get_usage",
3316
+ "get_savings",
3317
+ "estimate_cost",
3318
+ "sync",
3319
+ "search_tools",
3320
+ "describe_tools",
3321
+ "get_goals",
3322
+ "set_goal",
3323
+ "remove_goal",
3324
+ "list_machines",
3325
+ "register_agent",
3326
+ "heartbeat",
3327
+ "set_focus",
3328
+ "list_agents",
3329
+ "send_feedback"
3330
+ ];
3331
+ var TOOL_DESCRIPTIONS = {
3332
+ get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
3333
+ get_sessions: `agent(${AGENTS.join("|")}), project(partial), machine?(hostname), limit(20) -> compact session table`,
3334
+ get_top_sessions: `n(10), agent(${AGENTS.join("|")}) -> top sessions by cost`,
3335
+ list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
3336
+ get_model_breakdown: "no params -> model, requests, tokens, cost",
3337
+ get_project_breakdown: "period?(today|week|month|year|all) -> project_name, sessions, tokens, cost",
3338
+ get_agent_breakdown: "period?(today|week|month|year|all) -> agent, sessions, requests, tokens, api-equivalent, billable, included",
3339
+ get_account_breakdown: "period?(today|week|month|year|all) -> account profile, sessions, requests, tokens, api-equivalent, billable, included",
3340
+ get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
3341
+ set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, alert_at_percent? -> create budget",
3342
+ remove_budget: "id -> delete budget",
3343
+ get_pricing: "no params -> model pricing rows with input, output, cache read/write, and cache storage rates",
3344
+ set_pricing: "model, input_per_1m, output_per_1m, cache_read_per_1m?, cache_write_per_1m?, cache_write_1h_per_1m?, cache_storage_per_1m_hour? -> create/update pricing",
3345
+ remove_pricing: "model -> delete pricing row",
3346
+ get_daily: "days(30) -> daily cost table grouped by date and agent",
3347
+ get_billing_summary: "period(today|yesterday|week|month|year|all) -> actual provider billing totals",
3348
+ get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
3349
+ get_usage: `period(today|week|month), agent?(${AGENTS.join("|")}) -> usage snapshots and all-machine summary`,
3350
+ get_savings: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> subscription/API-equivalent savings`,
3351
+ estimate_cost: "model, input_tokens?, output_tokens? -> pre-flight token cost estimate",
3352
+ sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
3353
+ search_tools: "query substring -> tool name list",
3354
+ describe_tools: "names[] -> one-line parameter hints",
3355
+ get_goals: "no params -> goal progress summary",
3356
+ set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
3357
+ remove_goal: "id -> delete goal",
3358
+ register_agent: "name, session_id? -> register agent session",
3359
+ heartbeat: "agent_id -> update last_seen_at",
3360
+ set_focus: "agent_id, project_id? -> set active project context",
3361
+ list_agents: "no params -> registered agent list",
3362
+ send_feedback: "message, email?, category? -> save feedback locally"
3363
+ };
3364
+ var fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
3365
+ var fmtTok = (n) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
3366
+ function fmtSession(s) {
3367
+ const id = String(s["id"] ?? "").slice(0, 8);
3368
+ const agent = String(s["agent"] ?? "");
3369
+ const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
3370
+ const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
3371
+ const tok = fmtTok(Number(s["total_tokens"] ?? 0));
3372
+ return `${id} ${agent.padEnd(9)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
3373
+ }
3374
+ function text(text2) {
3375
+ return { content: [{ type: "text", text: text2 }] };
3376
+ }
3377
+ function textError(message) {
3378
+ return { content: [{ type: "text", text: message }], isError: true };
3379
+ }
3380
+ server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z.string().optional() }, async ({ query }) => {
3381
+ const q = query?.toLowerCase();
3382
+ const matches = q ? TOOL_NAMES.filter((name) => name.includes(q)) : [...TOOL_NAMES];
3383
+ return text(matches.join(", "));
3384
+ });
3385
+ server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z.array(z.string()) }, async ({ names }) => {
3386
+ const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
2989
3387
  `);
2990
- return text(result);
2991
- });
2992
- server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all. machine: filter by hostname.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), machine: z.string().optional() }, async ({ period, machine }) => {
2993
- const resolved = period ?? "today";
2994
- const s = querySummary(db, resolved, machine);
2995
- const machineLabel = machine ? ` on ${machine}` : "";
2996
- return text([
2997
- `period: ${resolved}${machineLabel}`,
2998
- `cost: ${fmtUsd(s.total_usd)}`,
2999
- `sessions: ${s.sessions}`,
3000
- `requests: ${s.requests.toLocaleString()}`,
3001
- `tokens: ${fmtTok(s.tokens)}`,
3002
- `summary: You've spent ${fmtUsd(s.total_usd)} ${resolved === "all" ? "total" : resolved}${machineLabel} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
3003
- ].join(`
3388
+ return text(result);
3389
+ });
3390
+ server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all. machine: filter by hostname.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), machine: z.string().optional() }, async ({ period, machine }) => {
3391
+ const resolved = period ?? "today";
3392
+ const s = querySummary(db, resolved, machine);
3393
+ const machineLabel = machine ? ` on ${machine}` : "";
3394
+ return text([
3395
+ `period: ${resolved}${machineLabel}`,
3396
+ `cost: ${fmtUsd(s.total_usd)}`,
3397
+ `sessions: ${s.sessions}`,
3398
+ `requests: ${s.requests.toLocaleString()}`,
3399
+ `tokens: ${fmtTok(s.tokens)}`,
3400
+ `summary: You've spent ${fmtUsd(s.total_usd)} ${resolved === "all" ? "total" : resolved}${machineLabel} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
3401
+ ].join(`
3004
3402
  `));
3403
+ });
3404
+ server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, machine, limit(20)", {
3405
+ agent: z.enum(AGENTS).optional(),
3406
+ project: z.string().optional(),
3407
+ machine: z.string().optional(),
3408
+ limit: z.number().int().positive().max(100).optional()
3409
+ }, async ({ agent, project, machine, limit }) => {
3410
+ const sessions = querySessions(db, {
3411
+ agent,
3412
+ project,
3413
+ machine,
3414
+ limit: limit ?? 20
3005
3415
  });
3006
- server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, machine, limit(20)", {
3007
- agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional(),
3008
- project: z.string().optional(),
3009
- machine: z.string().optional(),
3010
- limit: z.number().int().positive().max(100).optional()
3011
- }, async ({ agent, project, machine, limit }) => {
3012
- const sessions = querySessions(db, {
3013
- agent,
3014
- project,
3015
- machine,
3016
- limit: limit ?? 20
3017
- });
3018
- const lines = ["id agent cost tokens project"];
3019
- for (const session of sessions)
3020
- lines.push(fmtSession(session));
3021
- return text(lines.join(`
3416
+ const lines = ["id agent cost tokens project"];
3417
+ for (const session of sessions)
3418
+ lines.push(fmtSession(session));
3419
+ return text(lines.join(`
3022
3420
  `));
3023
- });
3024
- server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
3025
- n: z.number().int().positive().max(100).optional(),
3026
- agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional()
3027
- }, async ({ n, agent }) => {
3028
- const sessions = queryTopSessions(db, n ?? 10, agent);
3029
- const lines = ["rank id agent cost tokens project"];
3030
- sessions.forEach((session, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(session)}`));
3031
- return text(lines.join(`
3421
+ });
3422
+ server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
3423
+ n: z.number().int().positive().max(100).optional(),
3424
+ agent: z.enum(AGENTS).optional()
3425
+ }, async ({ n, agent }) => {
3426
+ const sessions = queryTopSessions(db, n ?? 10, agent);
3427
+ const lines = ["rank id agent cost tokens project"];
3428
+ sessions.forEach((session, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(session)}`));
3429
+ return text(lines.join(`
3032
3430
  `));
3033
- });
3034
- server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
3035
- const rows = queryModelBreakdown(db);
3036
- const lines = ["model reqs tokens cost"];
3037
- for (const row of rows) {
3038
- lines.push(`${String(row["model"]).slice(0, 30).padEnd(31)}${String(row["requests"]).padEnd(8)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
3039
- }
3040
- return text(lines.join(`
3431
+ });
3432
+ server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
3433
+ const rows = queryModelBreakdown(db);
3434
+ const lines = ["model agent reqs tokens cost"];
3435
+ for (const row of rows) {
3436
+ lines.push(`${String(row["model"]).slice(0, 30).padEnd(31)}${String(row["agent"]).padEnd(10)}${String(row["requests"]).padEnd(8)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
3437
+ }
3438
+ return text(lines.join(`
3041
3439
  `));
3042
- });
3043
- server.tool("get_project_breakdown", "Cost per project. No params.", {}, async () => {
3044
- const rows = queryProjectBreakdown(db);
3045
- const lines = ["project sessions tokens cost"];
3046
- for (const row of rows) {
3047
- const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
3048
- lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
3049
- }
3050
- return text(lines.join(`
3440
+ });
3441
+ server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all).", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
3442
+ const rows = queryProjectBreakdown(db, period ?? "all");
3443
+ const lines = ["project sessions tokens cost"];
3444
+ for (const row of rows) {
3445
+ const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
3446
+ lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
3447
+ }
3448
+ return text(lines.join(`
3051
3449
  `));
3052
- });
3053
- server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
3054
- const budgets = getBudgetStatuses(db);
3055
- if (budgets.length === 0)
3056
- return text("No budgets set.");
3057
- const lines = ["scope period spent limit used% status"];
3058
- for (const budget of budgets) {
3059
- const scope = String(budget["project_path"] ?? "global").slice(0, 20);
3060
- const pct = Number(budget["percent_used"]).toFixed(1);
3061
- const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
3062
- lines.push(`${scope.padEnd(21)}${String(budget["period"]).padEnd(9)}${fmtUsd(Number(budget["current_spend_usd"])).padEnd(11)}${fmtUsd(Number(budget["limit_usd"])).padEnd(11)}${pct}%`.padEnd(49) + ` ${status}`);
3063
- }
3064
- return text(lines.join(`
3450
+ });
3451
+ server.tool("get_agent_breakdown", "Cost per coding agent. Params: period(today|week|month|year|all). Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
3452
+ const rows = queryAgentBreakdown(db, period ?? "all");
3453
+ if (rows.length === 0)
3454
+ return text("No agent usage yet.");
3455
+ const lines = ["agent sessions requests tokens api_eq billable included"];
3456
+ for (const row of rows) {
3457
+ lines.push(`${String(row["agent"]).slice(0, 10).padEnd(11)}` + `${String(row["sessions"]).padEnd(9)}` + `${String(row["requests"]).padEnd(9)}` + `${fmtTok(Number(row["total_tokens"])).padEnd(9)}` + `${fmtUsd(Number(row["api_equivalent_usd"] ?? row["cost_usd"])).padEnd(10)}` + `${fmtUsd(Number(row["billable_usd"] ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row["subscription_included_usd"] ?? 0))}`);
3458
+ }
3459
+ return text(lines.join(`
3065
3460
  `));
3066
- });
3067
- server.tool("set_budget", "Create a spending budget. period: daily|weekly|monthly. limit_usd must be positive. alert_at_percent defaults to 80.", {
3068
- period: z.enum(["daily", "weekly", "monthly"]),
3069
- limit_usd: z.number().positive(),
3070
- project_path: z.string().optional(),
3071
- agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional(),
3072
- alert_at_percent: z.number().positive().max(100).optional()
3073
- }, async ({ period, limit_usd, project_path, agent, alert_at_percent }) => {
3074
- const now = new Date().toISOString();
3075
- const id = randomUUID();
3076
- upsertBudget(db, {
3077
- id,
3078
- project_path: project_path ?? null,
3079
- agent: agent ?? null,
3080
- period,
3081
- limit_usd,
3082
- alert_at_percent: alert_at_percent ?? 80,
3083
- created_at: now,
3084
- updated_at: now
3085
- });
3086
- return text(`Budget set: ${id}`);
3087
- });
3088
- server.tool("remove_budget", "Delete a budget by id.", { id: z.string() }, async ({ id }) => {
3089
- deleteBudget(db, id);
3090
- return text("Budget removed.");
3091
- });
3092
- server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage.", {}, async () => {
3093
- const rows = listModelPricing(db);
3094
- const lines = ["model input output cache-r cache-w cache-1h storage-h"];
3095
- for (const row of rows) {
3096
- lines.push(`${row.model.slice(0, 30).padEnd(31)}` + `${fmtUsd(row.input_per_1m).padEnd(9)}` + `${fmtUsd(row.output_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_read_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_1h_per_1m ?? 0).padEnd(9)}` + `${fmtUsd(row.cache_storage_per_1m_hour ?? 0)}`);
3097
- }
3098
- return text(lines.join(`
3461
+ });
3462
+ server.tool("get_account_breakdown", "Cost per account/profile. Params: period(today|week|month|year|all). Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
3463
+ const rows = queryAccountBreakdown(db, period ?? "all");
3464
+ if (rows.length === 0)
3465
+ return text("No account-attributed sessions yet.");
3466
+ const lines = ["account sessions requests tokens api_eq billable included"];
3467
+ for (const row of rows) {
3468
+ const label = String(row["account_key"] || row["account_name"] || "\u2014").slice(0, 20);
3469
+ lines.push(`${label.padEnd(21)}` + `${String(row["sessions"]).padEnd(9)}` + `${String(row["requests"]).padEnd(9)}` + `${fmtTok(Number(row["total_tokens"])).padEnd(9)}` + `${fmtUsd(Number(row["api_equivalent_usd"] ?? row["cost_usd"])).padEnd(10)}` + `${fmtUsd(Number(row["billable_usd"] ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row["subscription_included_usd"] ?? 0))}`);
3470
+ }
3471
+ return text(lines.join(`
3099
3472
  `));
3100
- });
3101
- server.tool("set_pricing", "Create or update a model pricing row. Values are USD per 1M tokens except cache_storage_per_1m_hour.", {
3102
- model: z.string().min(1),
3103
- input_per_1m: z.number().nonnegative(),
3104
- output_per_1m: z.number().nonnegative(),
3105
- cache_read_per_1m: z.number().nonnegative().optional(),
3106
- cache_write_per_1m: z.number().nonnegative().optional(),
3107
- cache_write_1h_per_1m: z.number().nonnegative().optional(),
3108
- cache_storage_per_1m_hour: z.number().nonnegative().optional()
3109
- }, async (input) => {
3110
- const model = input.model.trim();
3111
- if (!model)
3112
- return textError("model is required");
3113
- upsertModelPricing(db, {
3114
- model,
3115
- input_per_1m: input.input_per_1m,
3116
- output_per_1m: input.output_per_1m,
3117
- cache_read_per_1m: input.cache_read_per_1m ?? 0,
3118
- cache_write_per_1m: input.cache_write_per_1m ?? 0,
3119
- cache_write_1h_per_1m: input.cache_write_1h_per_1m ?? 0,
3120
- cache_storage_per_1m_hour: input.cache_storage_per_1m_hour ?? 0,
3121
- updated_at: new Date().toISOString()
3122
- });
3123
- return text(`Pricing set: ${model}`);
3124
- });
3125
- server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z.string() }, async ({ model }) => {
3126
- deleteModelPricing(db, model);
3127
- return text("Pricing removed.");
3128
- });
3129
- server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
3130
- const rows = queryDailyBreakdown(db, days ?? 30);
3131
- const byDate = new Map;
3132
- for (const row of rows) {
3133
- const date = String(row["date"]);
3134
- const entry = byDate.get(date) ?? { claude: 0, takumi: 0, codex: 0, gemini: 0 };
3135
- if (row["agent"] === "claude")
3136
- entry.claude += Number(row["cost_usd"]);
3137
- else if (row["agent"] === "takumi")
3138
- entry.takumi += Number(row["cost_usd"]);
3139
- else if (row["agent"] === "codex")
3140
- entry.codex += Number(row["cost_usd"]);
3141
- else if (row["agent"] === "gemini")
3142
- entry.gemini += Number(row["cost_usd"]);
3143
- byDate.set(date, entry);
3144
- }
3145
- const lines = ["date claude takumi codex gemini total"];
3146
- for (const [date, costs] of [...byDate.entries()].sort()) {
3147
- const total = costs.claude + costs.takumi + costs.codex + costs.gemini;
3148
- lines.push(`${date} ${fmtUsd(costs.claude).padEnd(11)}${fmtUsd(costs.takumi).padEnd(11)}${fmtUsd(costs.codex).padEnd(11)}${fmtUsd(costs.gemini).padEnd(11)}${fmtUsd(total)}`);
3149
- }
3150
- return text(lines.join(`
3473
+ });
3474
+ server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
3475
+ const budgets = getBudgetStatuses(db);
3476
+ if (budgets.length === 0)
3477
+ return text("No budgets set.");
3478
+ const lines = ["scope period spent limit used% status"];
3479
+ for (const budget of budgets) {
3480
+ const scope = String(budget["project_path"] ?? "global").slice(0, 20);
3481
+ const pct = Number(budget["percent_used"]).toFixed(1);
3482
+ const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
3483
+ lines.push(`${scope.padEnd(21)}${String(budget["period"]).padEnd(9)}${fmtUsd(Number(budget["current_spend_usd"])).padEnd(11)}${fmtUsd(Number(budget["limit_usd"])).padEnd(11)}${pct}%`.padEnd(49) + ` ${status}`);
3484
+ }
3485
+ return text(lines.join(`
3151
3486
  `));
3487
+ });
3488
+ server.tool("set_budget", "Create a spending budget. period: daily|weekly|monthly. limit_usd must be positive. alert_at_percent defaults to 80.", {
3489
+ period: z.enum(["daily", "weekly", "monthly"]),
3490
+ limit_usd: z.number().positive(),
3491
+ project_path: z.string().optional(),
3492
+ agent: z.enum(AGENTS).optional(),
3493
+ alert_at_percent: z.number().positive().max(100).optional()
3494
+ }, async ({ period, limit_usd, project_path, agent, alert_at_percent }) => {
3495
+ const now = new Date().toISOString();
3496
+ const id = randomUUID();
3497
+ upsertBudget(db, {
3498
+ id,
3499
+ project_path: project_path ?? null,
3500
+ agent: agent ?? null,
3501
+ period,
3502
+ limit_usd,
3503
+ alert_at_percent: alert_at_percent ?? 80,
3504
+ created_at: now,
3505
+ updated_at: now
3152
3506
  });
3153
- server.tool("get_billing_summary", "Actual provider billing totals from admin API sync. Params: period(today|yesterday|week|month|year|all)", { period: z.enum(["today", "yesterday", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
3154
- const summary = queryBillingSummary(db, period ?? "month");
3155
- const lines = ["provider billed"];
3156
- for (const [provider, cost] of Object.entries(summary.by_provider)) {
3157
- lines.push(`${provider.padEnd(11)}${fmtUsd(cost)}`);
3158
- }
3159
- lines.push(`total ${fmtUsd(summary.total_usd)}`);
3160
- return text(lines.join(`
3507
+ return text(`Budget set: ${id}`);
3508
+ });
3509
+ server.tool("remove_budget", "Delete a budget by id.", { id: z.string() }, async ({ id }) => {
3510
+ deleteBudget(db, id);
3511
+ return text("Budget removed.");
3512
+ });
3513
+ server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage.", {}, async () => {
3514
+ const rows = listModelPricing(db);
3515
+ const lines = ["model input output cache-r cache-w cache-1h storage-h"];
3516
+ for (const row of rows) {
3517
+ lines.push(`${row.model.slice(0, 30).padEnd(31)}` + `${fmtUsd(row.input_per_1m).padEnd(9)}` + `${fmtUsd(row.output_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_read_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_1h_per_1m ?? 0).padEnd(9)}` + `${fmtUsd(row.cache_storage_per_1m_hour ?? 0)}`);
3518
+ }
3519
+ return text(lines.join(`
3161
3520
  `));
3521
+ });
3522
+ server.tool("set_pricing", "Create or update a model pricing row. Values are USD per 1M tokens except cache_storage_per_1m_hour.", {
3523
+ model: z.string().min(1),
3524
+ input_per_1m: z.number().nonnegative(),
3525
+ output_per_1m: z.number().nonnegative(),
3526
+ cache_read_per_1m: z.number().nonnegative().optional(),
3527
+ cache_write_per_1m: z.number().nonnegative().optional(),
3528
+ cache_write_1h_per_1m: z.number().nonnegative().optional(),
3529
+ cache_storage_per_1m_hour: z.number().nonnegative().optional()
3530
+ }, async (input) => {
3531
+ const model = input.model.trim();
3532
+ if (!model)
3533
+ return textError("model is required");
3534
+ upsertModelPricing(db, {
3535
+ model,
3536
+ input_per_1m: input.input_per_1m,
3537
+ output_per_1m: input.output_per_1m,
3538
+ cache_read_per_1m: input.cache_read_per_1m ?? 0,
3539
+ cache_write_per_1m: input.cache_write_per_1m ?? 0,
3540
+ cache_write_1h_per_1m: input.cache_write_1h_per_1m ?? 0,
3541
+ cache_storage_per_1m_hour: input.cache_storage_per_1m_hour ?? 0,
3542
+ updated_at: new Date().toISOString()
3162
3543
  });
3163
- server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
3164
- const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(session_id, `${session_id}%`);
3165
- if (!session)
3166
- return textError(`Session not found: ${session_id}`);
3167
- const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
3168
- const lines = [
3169
- `session: ${String(session["id"]).slice(0, 16)}`,
3170
- `agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
3171
- `cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
3172
- "",
3173
- "time model input output cache-r cache-5m cache-1h cost"
3174
- ];
3175
- for (const request of requests) {
3176
- lines.push(`${String(request["timestamp"]).slice(11, 19)} ` + `${String(request["model"]).slice(0, 22).padEnd(23)}` + `${fmtTok(Number(request["input_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["output_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_read_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_create_5m_tokens"] ?? request["cache_create_tokens"] ?? 0)).padEnd(9)}` + `${fmtTok(Number(request["cache_create_1h_tokens"] ?? 0)).padEnd(9)}` + `${fmtUsd(Number(request["cost_usd"]))}`);
3177
- }
3178
- return text(lines.join(`
3544
+ return text(`Pricing set: ${model}`);
3545
+ });
3546
+ server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z.string() }, async ({ model }) => {
3547
+ deleteModelPricing(db, model);
3548
+ return text("Pricing removed.");
3549
+ });
3550
+ server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
3551
+ const rows = queryDailyBreakdown(db, days ?? 30);
3552
+ const byDate = new Map;
3553
+ for (const row of rows) {
3554
+ const date = String(row["date"]);
3555
+ const agent = String(row["agent"]);
3556
+ const entry = byDate.get(date) ?? Object.fromEntries(AGENTS.map((name) => [name, 0]));
3557
+ entry[agent] = (entry[agent] ?? 0) + Number(row["cost_usd"]);
3558
+ byDate.set(date, entry);
3559
+ }
3560
+ const lines = [`date ${AGENTS.map((agent) => agent.slice(0, 8).padEnd(10)).join("")}total`];
3561
+ for (const [date, costs] of [...byDate.entries()].sort()) {
3562
+ const total = Object.values(costs).reduce((sum, value) => sum + value, 0);
3563
+ lines.push(`${date} ${AGENTS.map((agent) => fmtUsd(costs[agent] ?? 0).padEnd(10)).join("")}${fmtUsd(total)}`);
3564
+ }
3565
+ return text(lines.join(`
3179
3566
  `));
3180
- });
3181
- server.tool("sync", `Ingest new cost data. sources: all|${AGENTS.join("|")}`, { sources: z.enum(["all", ...AGENTS]).optional() }, async ({ sources }) => {
3182
- const selected = sources ?? "all";
3183
- const opts = selected === "all" ? {} : { [selected]: true };
3184
- const result = await syncAll(db, opts);
3185
- return text(JSON.stringify(result, null, 2));
3186
- });
3187
- server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month", { period: z.enum(["today", "week", "month"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
3188
- const p = period ?? "month";
3189
- const snaps = queryUsageSnapshots(db, { agent });
3190
- const summary = querySummary(db, p, undefined, true);
3191
- return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
3192
- });
3193
- server.tool("get_savings", "Subscription vs API savings summary", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
3194
- return text(JSON.stringify(querySavingsSummary(db, period ?? "month", agent), null, 2));
3195
- });
3196
- server.tool("estimate_cost", "Pre-flight cost estimate for token counts", { model: z.string(), input_tokens: z.number().optional(), output_tokens: z.number().optional() }, async ({ model, input_tokens, output_tokens }) => {
3197
- const cost = computeCostFromDb(db, model, input_tokens ?? 0, output_tokens ?? 0, 0, 0, 0);
3198
- return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
3199
- });
3200
- server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
3201
- const goals = getGoalStatuses(db);
3202
- if (goals.length === 0)
3203
- return text("No goals set.");
3204
- const lines = ["period scope limit spent used% status"];
3205
- for (const goal of goals) {
3206
- const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
3207
- const pct = Number(goal["percent_used"]).toFixed(1);
3208
- const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
3209
- lines.push(`${String(goal["period"]).padEnd(9)}${scope.padEnd(21)}${fmtUsd(Number(goal["limit_usd"])).padEnd(11)}${fmtUsd(Number(goal["current_spend_usd"])).padEnd(11)}${pct}% ${status}`);
3210
- }
3211
- return text(lines.join(`
3567
+ });
3568
+ server.tool("get_billing_summary", "Actual provider billing totals from admin API sync. Params: period(today|yesterday|week|month|year|all)", { period: z.enum(["today", "yesterday", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
3569
+ const summary = queryBillingSummary(db, period ?? "month");
3570
+ const lines = ["provider billed"];
3571
+ for (const [provider, cost] of Object.entries(summary.by_provider)) {
3572
+ lines.push(`${provider.padEnd(11)}${fmtUsd(cost)}`);
3573
+ }
3574
+ lines.push(`total ${fmtUsd(summary.total_usd)}`);
3575
+ return text(lines.join(`
3212
3576
  `));
3577
+ });
3578
+ server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
3579
+ const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(session_id, `${session_id}%`);
3580
+ if (!session)
3581
+ return textError(`Session not found: ${session_id}`);
3582
+ const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
3583
+ const lines = [
3584
+ `session: ${String(session["id"]).slice(0, 16)}`,
3585
+ `agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
3586
+ `cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
3587
+ "",
3588
+ "time model input output cache-r cache-5m cache-1h cost"
3589
+ ];
3590
+ for (const request of requests) {
3591
+ lines.push(`${String(request["timestamp"]).slice(11, 19)} ` + `${String(request["model"]).slice(0, 22).padEnd(23)}` + `${fmtTok(Number(request["input_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["output_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_read_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_create_5m_tokens"] ?? request["cache_create_tokens"] ?? 0)).padEnd(9)}` + `${fmtTok(Number(request["cache_create_1h_tokens"] ?? 0)).padEnd(9)}` + `${fmtUsd(Number(request["cost_usd"]))}`);
3592
+ }
3593
+ return text(lines.join(`
3594
+ `));
3595
+ });
3596
+ server.tool("sync", `Ingest new cost data. sources: all|${AGENTS.join("|")}`, { sources: z.enum(["all", ...AGENTS]).optional() }, async ({ sources }) => {
3597
+ const selected = sources ?? "all";
3598
+ const opts = selected === "all" ? {} : { [selected]: true };
3599
+ const result = await syncAll(db, opts);
3600
+ return text(JSON.stringify(result, null, 2));
3601
+ });
3602
+ server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month", { period: z.enum(["today", "week", "month"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
3603
+ const p = period ?? "month";
3604
+ const snaps = queryUsageSnapshots(db, { agent });
3605
+ const summary = querySummary(db, p, undefined, true);
3606
+ return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
3607
+ });
3608
+ server.tool("get_savings", "Subscription vs API savings summary", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
3609
+ return text(JSON.stringify(querySavingsSummary(db, period ?? "month", agent), null, 2));
3610
+ });
3611
+ server.tool("estimate_cost", "Pre-flight cost estimate for token counts", { model: z.string(), input_tokens: z.number().optional(), output_tokens: z.number().optional() }, async ({ model, input_tokens, output_tokens }) => {
3612
+ const cost = computeCostFromDb(db, model, input_tokens ?? 0, output_tokens ?? 0, 0, 0, 0);
3613
+ return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
3614
+ });
3615
+ server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
3616
+ const goals = getGoalStatuses(db);
3617
+ if (goals.length === 0)
3618
+ return text("No goals set.");
3619
+ const lines = ["period scope limit spent used% status"];
3620
+ for (const goal of goals) {
3621
+ const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
3622
+ const pct = Number(goal["percent_used"]).toFixed(1);
3623
+ const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
3624
+ lines.push(`${String(goal["period"]).padEnd(9)}${scope.padEnd(21)}${fmtUsd(Number(goal["limit_usd"])).padEnd(11)}${fmtUsd(Number(goal["current_spend_usd"])).padEnd(11)}${pct}% ${status}`);
3625
+ }
3626
+ return text(lines.join(`
3627
+ `));
3628
+ });
3629
+ server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
3630
+ period: z.enum(["day", "week", "month", "year"]),
3631
+ limit_usd: z.number().positive(),
3632
+ project_path: z.string().optional(),
3633
+ agent: z.enum(AGENTS).optional()
3634
+ }, async ({ period, limit_usd, project_path, agent }) => {
3635
+ const now = new Date().toISOString();
3636
+ upsertGoal(db, {
3637
+ id: randomUUID(),
3638
+ period,
3639
+ project_path: project_path ?? null,
3640
+ agent: agent ?? null,
3641
+ limit_usd,
3642
+ created_at: now,
3643
+ updated_at: now
3213
3644
  });
3214
- server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
3215
- period: z.enum(["day", "week", "month", "year"]),
3216
- limit_usd: z.number().positive(),
3217
- project_path: z.string().optional(),
3218
- agent: z.string().optional()
3219
- }, async ({ period, limit_usd, project_path, agent }) => {
3220
- const now = new Date().toISOString();
3221
- upsertGoal(db, {
3222
- id: randomUUID(),
3223
- period,
3224
- project_path: project_path ?? null,
3225
- agent: agent ?? null,
3226
- limit_usd,
3227
- created_at: now,
3228
- updated_at: now
3229
- });
3230
- return text(`Goal set: ${period} $${limit_usd}`);
3231
- });
3232
- server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
3233
- deleteGoal(db, id);
3234
- return text("Goal removed.");
3235
- });
3236
- server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
3237
- const machines = listMachines(db);
3238
- if (machines.length === 0)
3239
- return text(`No machine data yet. Current machine: ${getMachineId()}`);
3240
- const lines = ["machine sessions requests cost last_active"];
3241
- for (const m of machines) {
3242
- lines.push(`${m.machine_id.padEnd(17)}${String(m.sessions).padEnd(10)}${String(m.requests).padEnd(10)}${fmtUsd(m.total_cost_usd).padEnd(12)}${m.last_active?.substring(0, 16) ?? "\u2014"}`);
3243
- }
3244
- lines.push(`
3645
+ return text(`Goal set: ${period} $${limit_usd}`);
3646
+ });
3647
+ server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
3648
+ deleteGoal(db, id);
3649
+ return text("Goal removed.");
3650
+ });
3651
+ server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
3652
+ const machines = listMachines(db);
3653
+ if (machines.length === 0)
3654
+ return text(`No machine data yet. Current machine: ${getMachineId()}`);
3655
+ const lines = ["machine sessions requests cost last_active"];
3656
+ for (const m of machines) {
3657
+ lines.push(`${m.machine_id.padEnd(17)}${String(m.sessions).padEnd(10)}${String(m.requests).padEnd(10)}${fmtUsd(m.total_cost_usd).padEnd(12)}${m.last_active?.substring(0, 16) ?? "\u2014"}`);
3658
+ }
3659
+ lines.push(`
3245
3660
  current machine: ${getMachineId()}`);
3246
- return text(lines.join(`
3661
+ return text(lines.join(`
3247
3662
  `));
3248
- });
3249
- server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
3250
- const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
3251
- if (existing) {
3252
- existing.last_seen_at = new Date().toISOString();
3253
- return text(JSON.stringify(existing));
3254
- }
3255
- const id = Math.random().toString(36).slice(2, 10);
3256
- const agent = { id, name, last_seen_at: new Date().toISOString() };
3257
- _econAgents.set(id, agent);
3258
- return text(JSON.stringify(agent));
3259
- });
3260
- server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
3261
- const agent = _econAgents.get(agent_id);
3262
- if (!agent)
3263
- return textError("Agent not found");
3264
- agent.last_seen_at = new Date().toISOString();
3265
- return text(`\u2665 ${agent.name}`);
3266
- });
3267
- server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
3268
- const agent = _econAgents.get(agent_id);
3269
- if (!agent)
3270
- return textError("Agent not found");
3271
- agent.project_id = project_id ?? undefined;
3272
- return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
3273
- });
3274
- server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
3275
- server.tool("send_feedback", "Send feedback about this service.", {
3276
- message: z.string(),
3277
- email: z.string().optional(),
3278
- category: z.enum(["bug", "feature", "general"]).optional()
3279
- }, async ({ message, email, category }) => {
3280
- try {
3281
- db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(message, email ?? null, category ?? "general", packageMetadata.version);
3282
- return text("Feedback saved. Thank you!");
3283
- } catch (error) {
3284
- return textError(String(error));
3285
- }
3286
- });
3287
- registerCloudTools(server, MCP_NAME, {
3288
- dbPath: getDbPath(),
3289
- migrations: PG_MIGRATIONS
3290
- });
3291
- return server;
3292
- }
3293
-
3294
- // src/mcp/http.ts
3295
- import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3296
- function isStdioMode(argv = process.argv.slice(2)) {
3297
- return argv.includes("--stdio") || process.env["MCP_STDIO"] === "1";
3298
- }
3299
- function resolveHttpPort(argv = process.argv.slice(2)) {
3300
- for (let i = 0;i < argv.length; i++) {
3301
- const arg = argv[i];
3302
- if (arg === "--port" || arg === "-p") {
3303
- const raw = argv[i + 1];
3304
- if (!raw)
3305
- throw new Error(`Invalid port: ${raw ?? ""}`);
3306
- return parsePort(raw, "port");
3307
- }
3308
- }
3309
- const fromEnv = process.env["MCP_HTTP_PORT"];
3310
- if (fromEnv)
3311
- return parsePort(fromEnv, "MCP_HTTP_PORT");
3312
- return DEFAULT_MCP_HTTP_PORT;
3313
- }
3314
- function parsePort(raw, label) {
3315
- const value = Number(raw);
3316
- if (!Number.isInteger(value) || value < 1 || value > 65535) {
3317
- throw new Error(`Invalid ${label}: ${raw}`);
3318
- }
3319
- return value;
3320
- }
3321
- async function handleMcpHttpRequest(req) {
3322
- const url = new URL(req.url);
3323
- if (url.pathname === "/health" && req.method === "GET") {
3324
- return Response.json({ status: "ok", name: MCP_NAME });
3325
- }
3326
- if (url.pathname === "/mcp") {
3327
- const transport = new WebStandardStreamableHTTPServerTransport({
3328
- sessionIdGenerator: undefined
3329
- });
3330
- const server = buildServer();
3331
- await server.connect(transport);
3332
- return transport.handleRequest(req);
3333
- }
3334
- return new Response("Not Found", { status: 404 });
3335
- }
3336
- function startHttpServer(options = {}) {
3337
- const port = options.port ?? DEFAULT_MCP_HTTP_PORT;
3338
- const hostname2 = options.hostname ?? "127.0.0.1";
3339
- const log = options.log ?? console.error;
3340
- const server = Bun.serve({
3341
- port,
3342
- hostname: hostname2,
3343
- fetch: handleMcpHttpRequest
3344
- });
3345
- const address = `http://${hostname2}:${server.port}`;
3346
- log(`${MCP_NAME}-mcp HTTP listening on ${address}/mcp (health: ${address}/health)`);
3347
- return server;
3348
- }
3349
-
3350
- // src/mcp/index.ts
3351
- function printHelp() {
3352
- console.log(`Usage: economy-mcp [options]
3353
-
3354
- Runs the ${packageMetadata.name} MCP server (stdio by default).
3355
-
3356
- Options:
3357
- --http Serve MCP over Streamable HTTP on 127.0.0.1
3358
- -p, --port <port> HTTP port (default: MCP_HTTP_PORT or 8815)
3359
- -V, --version output the version number
3360
- -h, --help display help for command
3361
-
3362
- Environment:
3363
- MCP_HTTP=1 Enable HTTP mode
3364
- MCP_HTTP_PORT Override default HTTP port (8815)`);
3365
- }
3366
- var args = process.argv.slice(2);
3367
- if (args.includes("--help") || args.includes("-h")) {
3368
- printHelp();
3369
- process.exit(0);
3370
- }
3371
- if (args.includes("--version") || args.includes("-V")) {
3372
- console.log(packageMetadata.version);
3373
- process.exit(0);
3374
- }
3375
- async function main() {
3376
- if (isStdioMode(args)) {
3377
- const server = buildServer();
3378
- const transport = new StdioServerTransport;
3379
- await server.connect(transport);
3380
- return;
3663
+ });
3664
+ server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
3665
+ const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
3666
+ if (existing) {
3667
+ existing.last_seen_at = new Date().toISOString();
3668
+ return text(JSON.stringify(existing));
3669
+ }
3670
+ const id = Math.random().toString(36).slice(2, 10);
3671
+ const agent = { id, name, last_seen_at: new Date().toISOString() };
3672
+ _econAgents.set(id, agent);
3673
+ return text(JSON.stringify(agent));
3674
+ });
3675
+ server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
3676
+ const agent = _econAgents.get(agent_id);
3677
+ if (!agent)
3678
+ return textError("Agent not found");
3679
+ agent.last_seen_at = new Date().toISOString();
3680
+ return text(`\u2665 ${agent.name}`);
3681
+ });
3682
+ server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
3683
+ const agent = _econAgents.get(agent_id);
3684
+ if (!agent)
3685
+ return textError("Agent not found");
3686
+ agent.project_id = project_id ?? undefined;
3687
+ return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
3688
+ });
3689
+ server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
3690
+ server.tool("send_feedback", "Send feedback about this service.", {
3691
+ message: z.string(),
3692
+ email: z.string().optional(),
3693
+ category: z.enum(["bug", "feature", "general"]).optional()
3694
+ }, async ({ message, email, category }) => {
3695
+ try {
3696
+ db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(message, email ?? null, category ?? "general", packageMetadata.version);
3697
+ return text("Feedback saved. Thank you!");
3698
+ } catch (error) {
3699
+ return textError(String(error));
3381
3700
  }
3382
- startHttpServer({ port: resolveHttpPort(args) });
3383
- await new Promise(() => {});
3384
- }
3385
- main().catch((error) => {
3386
- console.error("MCP server error:", error);
3387
- process.exit(1);
3388
3701
  });
3702
+ var transport = new StdioServerTransport;
3703
+ registerCloudTools(server, "economy", {
3704
+ dbPath: getDbPath(),
3705
+ migrations: PG_MIGRATIONS
3706
+ });
3707
+ await server.connect(transport);