@hasna/economy 0.2.21 → 0.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
@@ -1136,6 +1343,12 @@ function upsertSubscription(db, sub) {
1136
1343
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1137
1344
  `).run(sub.id, sub.agent, sub.provider, sub.plan, sub.monthly_fee_usd, sub.included_usage_usd, sub.billing_cycle_start, sub.reset_policy, sub.active, sub.created_at, sub.updated_at);
1138
1345
  }
1346
+ function listSubscriptions(db) {
1347
+ return db.prepare(`SELECT * FROM subscriptions ORDER BY provider, plan`).all();
1348
+ }
1349
+ function deleteSubscription(db, id) {
1350
+ db.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(id);
1351
+ }
1139
1352
  function upsertUsageSnapshot(db, snap) {
1140
1353
  const now = snap.updated_at ?? new Date().toISOString();
1141
1354
  const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
@@ -1204,7 +1417,12 @@ var init_pg_migrations = __esm(() => {
1204
1417
  duration_ms INTEGER DEFAULT 0,
1205
1418
  timestamp TEXT NOT NULL,
1206
1419
  source_request_id TEXT,
1207
- machine_id TEXT DEFAULT ''
1420
+ machine_id TEXT DEFAULT '',
1421
+ account_key TEXT DEFAULT '',
1422
+ account_tool TEXT DEFAULT '',
1423
+ account_name TEXT DEFAULT '',
1424
+ account_email TEXT DEFAULT '',
1425
+ account_source TEXT DEFAULT ''
1208
1426
  )`,
1209
1427
  `CREATE TABLE IF NOT EXISTS sessions (
1210
1428
  id TEXT PRIMARY KEY,
@@ -1216,7 +1434,12 @@ var init_pg_migrations = __esm(() => {
1216
1434
  total_cost_usd REAL DEFAULT 0,
1217
1435
  total_tokens INTEGER DEFAULT 0,
1218
1436
  request_count INTEGER DEFAULT 0,
1219
- machine_id TEXT DEFAULT ''
1437
+ machine_id TEXT DEFAULT '',
1438
+ account_key TEXT DEFAULT '',
1439
+ account_tool TEXT DEFAULT '',
1440
+ account_name TEXT DEFAULT '',
1441
+ account_email TEXT DEFAULT '',
1442
+ account_source TEXT DEFAULT ''
1220
1443
  )`,
1221
1444
  `CREATE TABLE IF NOT EXISTS projects (
1222
1445
  id TEXT PRIMARY KEY,
@@ -1337,47 +1560,41 @@ var init_pg_migrations = __esm(() => {
1337
1560
  )`,
1338
1561
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
1339
1562
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1563
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1564
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1565
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1566
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1567
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1340
1568
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1341
1569
  `ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1342
1570
  `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1571
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1572
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1573
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1574
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1575
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1343
1576
  `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1344
1577
  `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1345
1578
  `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)`
1579
+ `CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
1580
+ `CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
1581
+ `CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`
1347
1582
  ];
1348
1583
  });
1349
1584
 
1350
1585
  // 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
1586
  init_database();
1371
1587
  init_pg_migrations();
1372
1588
  import { randomUUID } from "crypto";
1373
1589
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1590
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1374
1591
  import { registerCloudTools } from "@hasna/cloud";
1375
1592
  import { z } from "zod";
1376
1593
 
1377
1594
  // src/ingest/claude.ts
1378
1595
  init_database();
1379
1596
  init_pricing();
1380
- import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync2, statSync as statSync2 } from "fs";
1597
+ import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
1381
1598
  import { homedir as homedir2 } from "os";
1382
1599
  import { join as join2, basename } from "path";
1383
1600
 
@@ -1400,7 +1617,6 @@ function periodWhere2(period, column) {
1400
1617
  }
1401
1618
  function prorateMonthlyFee(monthlyFee, period) {
1402
1619
  const now = new Date;
1403
- const dayOfMonth = now.getDate();
1404
1620
  const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
1405
1621
  switch (period) {
1406
1622
  case "today":
@@ -1482,6 +1698,131 @@ function defaultCostBasisForAgent(agent) {
1482
1698
  return "estimated";
1483
1699
  }
1484
1700
 
1701
+ // src/lib/accounts.ts
1702
+ var AGENT_ACCOUNT_TOOLS = {
1703
+ claude: ["claude"],
1704
+ takumi: ["takumi", "claude"],
1705
+ codex: ["codex"],
1706
+ gemini: ["gemini"],
1707
+ opencode: ["opencode"],
1708
+ cursor: ["cursor"],
1709
+ pi: ["pi"],
1710
+ hermes: ["hermes"]
1711
+ };
1712
+ function accountKey(tool, name) {
1713
+ return `${tool}:${name}`;
1714
+ }
1715
+ function normalizeDir(value) {
1716
+ return value.replace(/\/+$/, "");
1717
+ }
1718
+ function fromProfile(profile, source) {
1719
+ return {
1720
+ account_key: accountKey(profile.tool, profile.name),
1721
+ account_tool: profile.tool,
1722
+ account_name: profile.name,
1723
+ ...profile.email ? { account_email: profile.email } : {},
1724
+ account_source: source
1725
+ };
1726
+ }
1727
+ function fromOverride(raw, agent) {
1728
+ const value = raw.trim();
1729
+ if (!value)
1730
+ return null;
1731
+ const candidateTool = AGENT_ACCOUNT_TOOLS[agent][0] ?? agent;
1732
+ const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
1733
+ if (!tool || !name)
1734
+ return null;
1735
+ return {
1736
+ account_key: accountKey(tool, name),
1737
+ account_tool: tool,
1738
+ account_name: name,
1739
+ account_source: "override"
1740
+ };
1741
+ }
1742
+ function envOverride(agent, env) {
1743
+ const agentPrefix = agent.toUpperCase().replace(/[^A-Z0-9]/g, "_");
1744
+ const raw = env[`ECONOMY_${agentPrefix}_ACCOUNT_KEY`] ?? env[`ECONOMY_${agentPrefix}_ACCOUNT`] ?? env["ECONOMY_ACCOUNT_KEY"] ?? env["ECONOMY_ACCOUNT"];
1745
+ if (raw)
1746
+ return fromOverride(raw, agent);
1747
+ const tool = env[`ECONOMY_${agentPrefix}_ACCOUNT_TOOL`] ?? env["ECONOMY_ACCOUNT_TOOL"];
1748
+ const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
1749
+ if (!tool || !name)
1750
+ return null;
1751
+ return {
1752
+ account_key: accountKey(tool, name),
1753
+ account_tool: tool,
1754
+ account_name: name,
1755
+ account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
1756
+ account_source: "override"
1757
+ };
1758
+ }
1759
+ function knownToolIds(api) {
1760
+ try {
1761
+ return new Set(api.listTools().map((tool) => tool.id));
1762
+ } catch {
1763
+ return new Set;
1764
+ }
1765
+ }
1766
+ function profileForEnvDir(api, tool, env) {
1767
+ const configuredDir = env[tool.envVar];
1768
+ if (!configuredDir)
1769
+ return null;
1770
+ const normalized = normalizeDir(configuredDir);
1771
+ try {
1772
+ return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
1773
+ } catch {
1774
+ return null;
1775
+ }
1776
+ }
1777
+ async function resolveAccountForAgent(agent, env = process.env) {
1778
+ const override = envOverride(agent, env);
1779
+ if (override)
1780
+ return override;
1781
+ let api;
1782
+ try {
1783
+ api = await import("@hasna/accounts");
1784
+ } catch {
1785
+ return null;
1786
+ }
1787
+ const toolIds = knownToolIds(api);
1788
+ for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
1789
+ if (!toolIds.has(toolId))
1790
+ continue;
1791
+ let tool;
1792
+ try {
1793
+ tool = api.getTool(toolId);
1794
+ } catch {
1795
+ continue;
1796
+ }
1797
+ const envProfile = profileForEnvDir(api, tool, env);
1798
+ if (envProfile)
1799
+ return fromProfile(envProfile, "env");
1800
+ try {
1801
+ const applied = api.appliedProfile(toolId);
1802
+ if (applied)
1803
+ return fromProfile(applied, "applied");
1804
+ } catch {}
1805
+ try {
1806
+ const current = api.currentProfile(toolId);
1807
+ if (current)
1808
+ return fromProfile(current, "current");
1809
+ } catch {}
1810
+ }
1811
+ return null;
1812
+ }
1813
+ function withAccount(record, account) {
1814
+ if (!account)
1815
+ return record;
1816
+ return {
1817
+ ...record,
1818
+ account_key: account.account_key,
1819
+ account_tool: account.account_tool,
1820
+ account_name: account.account_name,
1821
+ account_email: account.account_email ?? "",
1822
+ account_source: account.account_source
1823
+ };
1824
+ }
1825
+
1485
1826
  // src/ingest/claude.ts
1486
1827
  function autoDetectProject(cwd, projects) {
1487
1828
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
@@ -1523,6 +1864,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1523
1864
  let totalRequests = 0;
1524
1865
  const touchedSessions = new Set;
1525
1866
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
1867
+ const account = await resolveAccountForAgent(agentName);
1526
1868
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
1527
1869
  for (const projectDirEntry of projectDirs) {
1528
1870
  const projectDirPath = join2(projectsDir, projectDirEntry.name);
@@ -1541,7 +1883,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1541
1883
  continue;
1542
1884
  let lines;
1543
1885
  try {
1544
- lines = readFileSync2(filePath, "utf-8").split(`
1886
+ lines = readFileSync(filePath, "utf-8").split(`
1545
1887
  `).filter((l) => l.trim());
1546
1888
  } catch {
1547
1889
  continue;
@@ -1586,7 +1928,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1586
1928
  }
1587
1929
  const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
1588
1930
  const reqId = `${agentName}-${sourceRequestId}`;
1589
- upsertRequest(db, {
1931
+ upsertRequest(db, withAccount({
1590
1932
  id: reqId,
1591
1933
  agent: agentName,
1592
1934
  session_id: sessionId,
@@ -1603,7 +1945,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1603
1945
  timestamp,
1604
1946
  source_request_id: sourceRequestId,
1605
1947
  machine_id: machineId
1606
- });
1948
+ }, account));
1607
1949
  if (!touchedSessions.has(sessionId)) {
1608
1950
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
1609
1951
  if (!existing) {
@@ -1621,7 +1963,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
1621
1963
  request_count: 0,
1622
1964
  machine_id: machineId
1623
1965
  };
1624
- upsertSession(db, session);
1966
+ upsertSession(db, withAccount(session, account));
1625
1967
  }
1626
1968
  touchedSessions.add(sessionId);
1627
1969
  }
@@ -1661,13 +2003,13 @@ function supportsClaudeDataResidencyPricing(model) {
1661
2003
  // src/ingest/codex.ts
1662
2004
  init_database();
1663
2005
  init_pricing();
1664
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2006
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1665
2007
  import { homedir as homedir3 } from "os";
1666
2008
  import { join as join3, basename as basename2 } from "path";
1667
2009
  import { Database as BunDatabase } from "bun:sqlite";
1668
2010
  var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
1669
2011
  var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
1670
- var CODEX_INGEST_VERSION = "rollout-token-dedupe-v2";
2012
+ var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
1671
2013
  function codexDbPath() {
1672
2014
  return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
1673
2015
  }
@@ -1679,7 +2021,7 @@ function readCodexModel() {
1679
2021
  if (!existsSync3(configPath))
1680
2022
  return "gpt-5-codex";
1681
2023
  try {
1682
- const content = readFileSync3(configPath, "utf-8");
2024
+ const content = readFileSync2(configPath, "utf-8");
1683
2025
  const match = content.match(/^model\s*=\s*"([^"]+)"/m);
1684
2026
  return match?.[1] ?? "gpt-5-codex";
1685
2027
  } catch {
@@ -1700,9 +2042,10 @@ function buildThreadQuery(codexDb) {
1700
2042
  function readTokenEvents(rolloutPath) {
1701
2043
  if (!rolloutPath || !existsSync3(rolloutPath))
1702
2044
  return [];
1703
- const events = [];
1704
- const seen = new Set;
1705
- for (const line of readFileSync3(rolloutPath, "utf-8").split(`
2045
+ const fallbackUsages = new Map;
2046
+ let fallbackTimestamp;
2047
+ let aggregate = null;
2048
+ for (const line of readFileSync2(rolloutPath, "utf-8").split(`
1706
2049
  `)) {
1707
2050
  if (!line.trim())
1708
2051
  continue;
@@ -1718,20 +2061,48 @@ function readTokenEvents(rolloutPath) {
1718
2061
  if (!payload || payload["type"] !== "token_count")
1719
2062
  continue;
1720
2063
  const info = payload["info"];
2064
+ const timestamp = entry["timestamp"];
2065
+ const entryTimestamp = typeof timestamp === "string" ? timestamp : undefined;
2066
+ const totalUsage = info?.["total_token_usage"];
2067
+ if (totalUsage && tokenTotal(totalUsage) > 0) {
2068
+ aggregate = { usage: totalUsage, timestamp: entryTimestamp };
2069
+ continue;
2070
+ }
1721
2071
  const usage = info?.["last_token_usage"];
1722
2072
  if (!usage)
1723
2073
  continue;
1724
- const total = usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
1725
- if (total <= 0)
2074
+ if (tokenTotal(usage) <= 0)
1726
2075
  continue;
1727
2076
  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 });
2077
+ if (!fallbackUsages.has(key))
2078
+ fallbackUsages.set(key, usage);
2079
+ fallbackTimestamp = entryTimestamp ?? fallbackTimestamp;
2080
+ }
2081
+ if (aggregate)
2082
+ return [aggregate];
2083
+ if (fallbackUsages.size === 0)
2084
+ return [];
2085
+ return [{ usage: sumTokenUsages([...fallbackUsages.values()]), timestamp: fallbackTimestamp }];
2086
+ }
2087
+ function tokenTotal(usage) {
2088
+ return usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
2089
+ }
2090
+ function sumTokenUsages(usages) {
2091
+ const result = {
2092
+ input_tokens: 0,
2093
+ cached_input_tokens: 0,
2094
+ output_tokens: 0,
2095
+ reasoning_output_tokens: 0,
2096
+ total_tokens: 0
2097
+ };
2098
+ for (const usage of usages) {
2099
+ result.input_tokens = (result.input_tokens ?? 0) + (usage.input_tokens ?? 0);
2100
+ result.cached_input_tokens = (result.cached_input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
2101
+ result.output_tokens = (result.output_tokens ?? 0) + (usage.output_tokens ?? 0);
2102
+ result.reasoning_output_tokens = (result.reasoning_output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0);
2103
+ result.total_tokens = (result.total_tokens ?? 0) + tokenTotal(usage);
1733
2104
  }
1734
- return events;
2105
+ return result;
1735
2106
  }
1736
2107
  function fallbackEvents(totalTokens) {
1737
2108
  const inputTokens = Math.floor(totalTokens * 0.6);
@@ -1755,6 +2126,7 @@ async function ingestCodex(db, verbose = false) {
1755
2126
  let codexDb = null;
1756
2127
  let ingested = 0;
1757
2128
  let requests = 0;
2129
+ const account = await resolveAccountForAgent("codex");
1758
2130
  try {
1759
2131
  codexDb = new BunDatabase(dbPath, { readonly: true });
1760
2132
  const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
@@ -1769,7 +2141,7 @@ async function ingestCodex(db, verbose = false) {
1769
2141
  const sessionId = `codex-${thread.id}`;
1770
2142
  const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
1771
2143
  const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
1772
- upsertSession(db, {
2144
+ upsertSession(db, withAccount({
1773
2145
  id: sessionId,
1774
2146
  agent: "codex",
1775
2147
  project_path: projectPath,
@@ -1780,9 +2152,10 @@ async function ingestCodex(db, verbose = false) {
1780
2152
  total_tokens: 0,
1781
2153
  request_count: 0,
1782
2154
  machine_id: machineId
1783
- });
2155
+ }, account));
1784
2156
  const events = readTokenEvents(thread.rollout_path);
1785
2157
  const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
2158
+ const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
1786
2159
  db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
1787
2160
  tokenEvents.forEach((event, index) => {
1788
2161
  const usage = event.usage;
@@ -1793,7 +2166,7 @@ async function ingestCodex(db, verbose = false) {
1793
2166
  const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
1794
2167
  const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
1795
2168
  const requestId = `${sessionId}-${index}`;
1796
- upsertRequest(db, {
2169
+ upsertRequest(db, withAccount({
1797
2170
  id: requestId,
1798
2171
  agent: "codex",
1799
2172
  session_id: sessionId,
@@ -1808,14 +2181,14 @@ async function ingestCodex(db, verbose = false) {
1808
2181
  timestamp,
1809
2182
  source_request_id: requestId,
1810
2183
  machine_id: machineId
1811
- });
2184
+ }, account));
1812
2185
  requests++;
1813
2186
  });
1814
2187
  rollupSession(db, sessionId);
1815
2188
  setIngestState(db, "codex", thread.id, stateValue);
1816
2189
  ingested++;
1817
2190
  if (verbose)
1818
- console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens on ${model}`);
2191
+ console.log(`Codex session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
1819
2192
  }
1820
2193
  } finally {
1821
2194
  codexDb?.close();
@@ -1826,7 +2199,7 @@ async function ingestCodex(db, verbose = false) {
1826
2199
  // src/ingest/gemini.ts
1827
2200
  init_database();
1828
2201
  init_pricing();
1829
- import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync4, statSync as statSync3 } from "fs";
2202
+ import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
1830
2203
  import { homedir as homedir4 } from "os";
1831
2204
  import { join as join4, basename as basename3 } from "path";
1832
2205
  var DEFAULT_GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
@@ -1866,7 +2239,7 @@ function projectRoot(projectDir, chatData) {
1866
2239
  const rootFile = join4(projectDir, ".project_root");
1867
2240
  try {
1868
2241
  if (existsSync4(rootFile))
1869
- return readFileSync4(rootFile, "utf-8").trim();
2242
+ return readFileSync3(rootFile, "utf-8").trim();
1870
2243
  } catch {}
1871
2244
  return "";
1872
2245
  }
@@ -1882,6 +2255,7 @@ async function ingestGemini(db, verbose) {
1882
2255
  let totalSessions = 0;
1883
2256
  let totalRequests = 0;
1884
2257
  const touchedSessions = new Set;
2258
+ const account = await resolveAccountForAgent("gemini");
1885
2259
  const projectDirs = listProjectDirs(tmpDir, historyDir);
1886
2260
  for (const projectDir of projectDirs) {
1887
2261
  const chatsDir = join4(projectDir, "chats");
@@ -1906,7 +2280,7 @@ async function ingestGemini(db, verbose) {
1906
2280
  continue;
1907
2281
  let chatData;
1908
2282
  try {
1909
- chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
2283
+ chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
1910
2284
  } catch {
1911
2285
  continue;
1912
2286
  }
@@ -1930,7 +2304,7 @@ async function ingestGemini(db, verbose) {
1930
2304
  request_count: 0,
1931
2305
  machine_id: machineId
1932
2306
  };
1933
- upsertSession(db, session);
2307
+ upsertSession(db, withAccount(session, account));
1934
2308
  totalSessions++;
1935
2309
  }
1936
2310
  touchedSessions.add(sessionId);
@@ -1954,7 +2328,7 @@ async function ingestGemini(db, verbose) {
1954
2328
  const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
1955
2329
  const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
1956
2330
  const requestId = `gemini-${sessionId}-${message.id ?? index}`;
1957
- upsertRequest(db, {
2331
+ upsertRequest(db, withAccount({
1958
2332
  id: requestId,
1959
2333
  agent: "gemini",
1960
2334
  session_id: sessionId,
@@ -1969,7 +2343,7 @@ async function ingestGemini(db, verbose) {
1969
2343
  timestamp,
1970
2344
  source_request_id: message.id ?? requestId,
1971
2345
  machine_id: machineId
1972
- });
2346
+ }, account));
1973
2347
  totalRequests++;
1974
2348
  }
1975
2349
  setIngestState(db, "gemini", stateKey, fileMtime);
@@ -1984,7 +2358,7 @@ async function ingestGemini(db, verbose) {
1984
2358
  // src/ingest/opencode.ts
1985
2359
  init_database();
1986
2360
  init_pricing();
1987
- import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
2361
+ import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
1988
2362
  import { homedir as homedir5 } from "os";
1989
2363
  import { join as join5 } from "path";
1990
2364
  var OPENCODE_STORAGE = join5(homedir5(), ".local", "share", "opencode", "storage");
@@ -2018,6 +2392,7 @@ async function ingestOpenCode(db, verbose = false) {
2018
2392
  const touched = new Set;
2019
2393
  const machineId = getMachineId();
2020
2394
  const now = new Date().toISOString();
2395
+ const account = await resolveAccountForAgent("opencode");
2021
2396
  for (const file of files) {
2022
2397
  const mtime = statSync4(file).mtimeMs;
2023
2398
  const stateKey = file;
@@ -2026,7 +2401,7 @@ async function ingestOpenCode(db, verbose = false) {
2026
2401
  continue;
2027
2402
  let parsed;
2028
2403
  try {
2029
- parsed = JSON.parse(readFileSync5(file, "utf-8"));
2404
+ parsed = JSON.parse(readFileSync4(file, "utf-8"));
2030
2405
  } catch {
2031
2406
  continue;
2032
2407
  }
@@ -2047,7 +2422,7 @@ async function ingestOpenCode(db, verbose = false) {
2047
2422
  const sourceId = file.replace(OPENCODE_STORAGE, "");
2048
2423
  const reqId = `opencode-${sourceId}`;
2049
2424
  const costUsd = usage.cost ?? computeCostFromDb(db, model, input, output, cacheRead, cacheWrite, 0);
2050
- upsertRequest(db, {
2425
+ upsertRequest(db, withAccount({
2051
2426
  id: reqId,
2052
2427
  agent: "opencode",
2053
2428
  session_id: sessionId,
@@ -2063,10 +2438,10 @@ async function ingestOpenCode(db, verbose = false) {
2063
2438
  source_request_id: sourceId,
2064
2439
  machine_id: machineId,
2065
2440
  updated_at: now
2066
- });
2441
+ }, account));
2067
2442
  requests++;
2068
2443
  if (!touched.has(sessionId)) {
2069
- upsertSession(db, {
2444
+ upsertSession(db, withAccount({
2070
2445
  id: sessionId,
2071
2446
  agent: "opencode",
2072
2447
  project_path: "",
@@ -2078,7 +2453,7 @@ async function ingestOpenCode(db, verbose = false) {
2078
2453
  request_count: 0,
2079
2454
  machine_id: machineId,
2080
2455
  updated_at: now
2081
- });
2456
+ }, account));
2082
2457
  touched.add(sessionId);
2083
2458
  }
2084
2459
  setIngestState(db, "opencode", stateKey, String(mtime));
@@ -2125,6 +2500,7 @@ async function ingestCursor(db, verbose = false) {
2125
2500
  const machineId = getMachineId();
2126
2501
  const now = new Date().toISOString();
2127
2502
  let snapshots = 0;
2503
+ const account = await resolveAccountForAgent("cursor");
2128
2504
  const usage = await cursorFetch("/api/usage", token);
2129
2505
  if (usage?.premiumRequests != null && usage.maxPremiumRequests) {
2130
2506
  upsertUsageSnapshot(db, {
@@ -2172,7 +2548,7 @@ async function ingestCursor(db, verbose = false) {
2172
2548
  }
2173
2549
  const sessionId = `cursor-${today}-${machineId}`;
2174
2550
  if (onDemand + included > 0) {
2175
- upsertSession(db, {
2551
+ upsertSession(db, withAccount({
2176
2552
  id: sessionId,
2177
2553
  agent: "cursor",
2178
2554
  project_path: "",
@@ -2184,8 +2560,8 @@ async function ingestCursor(db, verbose = false) {
2184
2560
  request_count: 1,
2185
2561
  machine_id: machineId,
2186
2562
  updated_at: now
2187
- });
2188
- upsertRequest(db, {
2563
+ }, account));
2564
+ upsertRequest(db, withAccount({
2189
2565
  id: `cursor-${today}-${machineId}-usage`,
2190
2566
  agent: "cursor",
2191
2567
  session_id: sessionId,
@@ -2201,7 +2577,7 @@ async function ingestCursor(db, verbose = false) {
2201
2577
  source_request_id: today,
2202
2578
  machine_id: machineId,
2203
2579
  updated_at: now
2204
- });
2580
+ }, account));
2205
2581
  rollupSession(db, sessionId);
2206
2582
  }
2207
2583
  setIngestState(db, "cursor", `sync-${today}`, now);
@@ -2212,7 +2588,7 @@ async function ingestCursor(db, verbose = false) {
2212
2588
 
2213
2589
  // src/ingest/pi.ts
2214
2590
  init_database();
2215
- import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
2591
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
2216
2592
  import { homedir as homedir6 } from "os";
2217
2593
  import { join as join6 } from "path";
2218
2594
  var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join6(homedir6(), ".pi", "agent", "sessions");
@@ -2234,6 +2610,7 @@ async function ingestPi(db, verbose = false) {
2234
2610
  const touched = new Set;
2235
2611
  const machineId = getMachineId();
2236
2612
  const now = new Date().toISOString();
2613
+ const account = await resolveAccountForAgent("pi");
2237
2614
  for (const file of files) {
2238
2615
  const mtime = statSync5(file).mtimeMs;
2239
2616
  const prev = getIngestState(db, "pi", file);
@@ -2241,7 +2618,7 @@ async function ingestPi(db, verbose = false) {
2241
2618
  continue;
2242
2619
  let data;
2243
2620
  try {
2244
- data = JSON.parse(readFileSync6(file, "utf-8"));
2621
+ data = JSON.parse(readFileSync5(file, "utf-8"));
2245
2622
  } catch {
2246
2623
  continue;
2247
2624
  }
@@ -2259,7 +2636,7 @@ async function ingestPi(db, verbose = false) {
2259
2636
  const model = turn.model ?? turn.provider ?? "unknown";
2260
2637
  const timestamp = turn.timestamp ?? new Date(statSync5(file).mtime).toISOString();
2261
2638
  const reqId = `pi-${sessionId}-${i}`;
2262
- upsertRequest(db, {
2639
+ upsertRequest(db, withAccount({
2263
2640
  id: reqId,
2264
2641
  agent: "pi",
2265
2642
  session_id: sessionId,
@@ -2275,11 +2652,11 @@ async function ingestPi(db, verbose = false) {
2275
2652
  source_request_id: `${sessionId}-${i}`,
2276
2653
  machine_id: machineId,
2277
2654
  updated_at: now
2278
- });
2655
+ }, account));
2279
2656
  requests++;
2280
2657
  }
2281
2658
  if (turns.length > 0) {
2282
- upsertSession(db, {
2659
+ upsertSession(db, withAccount({
2283
2660
  id: sessionId,
2284
2661
  agent: "pi",
2285
2662
  project_path: "",
@@ -2291,7 +2668,7 @@ async function ingestPi(db, verbose = false) {
2291
2668
  request_count: 0,
2292
2669
  machine_id: machineId,
2293
2670
  updated_at: now
2294
- });
2671
+ }, account));
2295
2672
  touched.add(sessionId);
2296
2673
  }
2297
2674
  setIngestState(db, "pi", file, String(mtime));
@@ -2339,13 +2716,14 @@ async function ingestHermes(db, verbose = false) {
2339
2716
  const machineId = getMachineId();
2340
2717
  const now = new Date().toISOString();
2341
2718
  let requests = 0;
2719
+ const account = await resolveAccountForAgent("hermes");
2342
2720
  for (const row of rows) {
2343
2721
  const sessionId = `hermes-${row.id}`;
2344
2722
  const startedAt = new Date(row.started_at * 1000).toISOString();
2345
2723
  const endedAt = row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null;
2346
2724
  const cost = row.actual_cost_usd ?? row.estimated_cost_usd ?? 0;
2347
2725
  const tokens = row.input_tokens + row.output_tokens + row.cache_read_tokens + row.cache_write_tokens + row.reasoning_tokens;
2348
- upsertSession(db, {
2726
+ upsertSession(db, withAccount({
2349
2727
  id: sessionId,
2350
2728
  agent: "hermes",
2351
2729
  project_path: row.source ?? "",
@@ -2357,9 +2735,9 @@ async function ingestHermes(db, verbose = false) {
2357
2735
  request_count: 1,
2358
2736
  machine_id: machineId,
2359
2737
  updated_at: now
2360
- });
2738
+ }, account));
2361
2739
  const reqId = `hermes-${row.id}-rollup`;
2362
- upsertRequest(db, {
2740
+ upsertRequest(db, withAccount({
2363
2741
  id: reqId,
2364
2742
  agent: "hermes",
2365
2743
  session_id: sessionId,
@@ -2375,7 +2753,7 @@ async function ingestHermes(db, verbose = false) {
2375
2753
  source_request_id: row.id,
2376
2754
  machine_id: machineId,
2377
2755
  updated_at: now
2378
- });
2756
+ }, account));
2379
2757
  requests++;
2380
2758
  rollupSession(db, sessionId);
2381
2759
  if (verbose)
@@ -2395,7 +2773,7 @@ function statSyncSafe(path) {
2395
2773
 
2396
2774
  // src/ingest/claude-quota.ts
2397
2775
  init_database();
2398
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
2776
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
2399
2777
 
2400
2778
  // src/lib/paths.ts
2401
2779
  import { homedir as homedir8 } from "os";
@@ -2433,7 +2811,7 @@ function readClaudeToken() {
2433
2811
  if (!existsSync8(CREDENTIALS_PATH))
2434
2812
  return null;
2435
2813
  try {
2436
- const creds = JSON.parse(readFileSync8(CREDENTIALS_PATH, "utf-8"));
2814
+ const creds = JSON.parse(readFileSync6(CREDENTIALS_PATH, "utf-8"));
2437
2815
  const oauth = creds.claudeAiOauth;
2438
2816
  if (!oauth?.accessToken)
2439
2817
  return null;
@@ -2569,7 +2947,7 @@ async function ingestClaudeQuota(db, verbose = false) {
2569
2947
 
2570
2948
  // src/ingest/codex-quota.ts
2571
2949
  init_database();
2572
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
2950
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
2573
2951
  var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
2574
2952
  function readCodexAuth() {
2575
2953
  const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
@@ -2579,7 +2957,7 @@ function readCodexAuth() {
2579
2957
  if (!existsSync9(authPath))
2580
2958
  return null;
2581
2959
  try {
2582
- const auth = JSON.parse(readFileSync9(authPath, "utf-8"));
2960
+ const auth = JSON.parse(readFileSync7(authPath, "utf-8"));
2583
2961
  const token = auth.tokens?.access_token;
2584
2962
  if (!token)
2585
2963
  return null;
@@ -2711,6 +3089,24 @@ init_database();
2711
3089
 
2712
3090
  // src/lib/cloud-sync.ts
2713
3091
  init_database();
3092
+
3093
+ // src/lib/package-metadata.ts
3094
+ import { readFileSync as readFileSync8 } from "fs";
3095
+ var cachedMetadata = null;
3096
+ function getPackageMetadata() {
3097
+ if (cachedMetadata)
3098
+ return cachedMetadata;
3099
+ const raw = readFileSync8(new URL("../../package.json", import.meta.url), "utf8");
3100
+ const parsed = JSON.parse(raw);
3101
+ cachedMetadata = {
3102
+ name: parsed.name ?? "@hasna/economy",
3103
+ version: parsed.version ?? "0.0.0"
3104
+ };
3105
+ return cachedMetadata;
3106
+ }
3107
+ var packageMetadata = getPackageMetadata();
3108
+
3109
+ // src/lib/cloud-sync.ts
2714
3110
  var CLOUD_TABLES = [
2715
3111
  "requests",
2716
3112
  "sessions",
@@ -2752,44 +3148,27 @@ async function runCloudMigrations(cloud) {
2752
3148
  await cloud.run(sql);
2753
3149
  }
2754
3150
  }
2755
- function isCloudIncrementalEnabled() {
2756
- return process.env["ECONOMY_CLOUD_INCREMENTAL"] === "1" || process.env["ECONOMY_CLOUD_INCREMENTAL"] === "true";
2757
- }
2758
3151
  async function cloudPush(opts) {
2759
- const { syncPush, incrementalSyncPush, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
3152
+ const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
2760
3153
  const cloud = await getCloudPg();
2761
3154
  const local = new SqliteAdapter(getDbPath());
2762
3155
  await runCloudMigrations(cloud);
2763
3156
  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
- }
3157
+ const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
3158
+ const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
2773
3159
  touchMachineRegistry(local, "push");
2774
3160
  local.close();
2775
3161
  await cloud.close();
2776
3162
  return { rows, machine: getMachineId() };
2777
3163
  }
2778
3164
  async function cloudPull(opts) {
2779
- const { syncPull, incrementalSyncPull, ensureSyncMetaTable, SqliteAdapter } = await import("@hasna/cloud");
3165
+ const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
2780
3166
  const cloud = await getCloudPg();
2781
3167
  const local = new SqliteAdapter(getDbPath());
2782
3168
  await runCloudMigrations(cloud);
2783
3169
  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
- }
3170
+ const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
3171
+ const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
2793
3172
  touchMachineRegistry(local, "pull");
2794
3173
  local.close();
2795
3174
  await cloud.close();
@@ -2893,496 +3272,499 @@ var AGENTS = [
2893
3272
  "hermes"
2894
3273
  ];
2895
3274
 
2896
- // src/mcp/server.ts
3275
+ // src/mcp/index.ts
2897
3276
  init_database();
2898
3277
  init_pricing();
2899
3278
  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(`
3279
+ function printHelp() {
3280
+ console.log(`Usage: economy-mcp [options]
3281
+
3282
+ Runs the ${packageMetadata.name} MCP stdio server.
3283
+
3284
+ Options:
3285
+ -V, --version output the version number
3286
+ -h, --help display help for command`);
3287
+ }
3288
+ var args = process.argv.slice(2);
3289
+ if (args.includes("--help") || args.includes("-h")) {
3290
+ printHelp();
3291
+ process.exit(0);
3292
+ }
3293
+ if (args.includes("--version") || args.includes("-V")) {
3294
+ console.log(packageMetadata.version);
3295
+ process.exit(0);
3296
+ }
3297
+ var db = openDatabase();
3298
+ ensurePricingSeeded(db);
3299
+ var server = new McpServer({
3300
+ name: "economy",
3301
+ version: packageMetadata.version
3302
+ });
3303
+ var _econAgents = new Map;
3304
+ var TOOL_NAMES = [
3305
+ "get_cost_summary",
3306
+ "get_sessions",
3307
+ "get_top_sessions",
3308
+ "get_model_breakdown",
3309
+ "get_project_breakdown",
3310
+ "get_agent_breakdown",
3311
+ "get_account_breakdown",
3312
+ "get_budget_status",
3313
+ "set_budget",
3314
+ "remove_budget",
3315
+ "get_pricing",
3316
+ "set_pricing",
3317
+ "remove_pricing",
3318
+ "get_daily",
3319
+ "get_billing_summary",
3320
+ "get_session_detail",
3321
+ "get_usage",
3322
+ "get_savings",
3323
+ "list_subscriptions",
3324
+ "set_subscription",
3325
+ "remove_subscription",
3326
+ "estimate_cost",
3327
+ "sync",
3328
+ "search_tools",
3329
+ "describe_tools",
3330
+ "get_goals",
3331
+ "set_goal",
3332
+ "remove_goal",
3333
+ "list_machines",
3334
+ "register_agent",
3335
+ "heartbeat",
3336
+ "set_focus",
3337
+ "list_agents",
3338
+ "send_feedback"
3339
+ ];
3340
+ var TOOL_DESCRIPTIONS = {
3341
+ get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
3342
+ get_sessions: `agent(${AGENTS.join("|")}), project(partial), machine?(hostname), limit(20) -> compact session table`,
3343
+ get_top_sessions: `n(10), agent(${AGENTS.join("|")}) -> top sessions by cost`,
3344
+ list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
3345
+ get_model_breakdown: "no params -> model, requests, tokens, cost",
3346
+ get_project_breakdown: "period?(today|week|month|year|all) -> project_name, sessions, tokens, cost",
3347
+ get_agent_breakdown: "period?(today|week|month|year|all) -> agent, sessions, requests, tokens, api-equivalent, billable, included",
3348
+ get_account_breakdown: "period?(today|week|month|year|all) -> account profile, sessions, requests, tokens, api-equivalent, billable, included",
3349
+ get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
3350
+ set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, alert_at_percent? -> create budget",
3351
+ remove_budget: "id -> delete budget",
3352
+ get_pricing: "no params -> model pricing rows with input, output, cache read/write, and cache storage rates",
3353
+ 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",
3354
+ remove_pricing: "model -> delete pricing row",
3355
+ get_daily: "days(30) -> daily cost table grouped by date and agent",
3356
+ get_billing_summary: "period(today|yesterday|week|month|year|all) -> actual provider billing totals",
3357
+ get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
3358
+ get_usage: `period(today|week|month), agent?(${AGENTS.join("|")}) -> usage snapshots and all-machine summary`,
3359
+ get_savings: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> subscription/API-equivalent savings`,
3360
+ list_subscriptions: "no params -> configured subscription plans and included usage",
3361
+ set_subscription: `provider, plan, monthly_fee_usd?, included_usage_usd?, agent?(${AGENTS.join("|")}) -> create/update subscription plan`,
3362
+ remove_subscription: "id -> delete subscription plan",
3363
+ estimate_cost: "model, input_tokens?, output_tokens? -> pre-flight token cost estimate",
3364
+ sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
3365
+ search_tools: "query substring -> tool name list",
3366
+ describe_tools: "names[] -> one-line parameter hints",
3367
+ get_goals: "no params -> goal progress summary",
3368
+ set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
3369
+ remove_goal: "id -> delete goal",
3370
+ register_agent: "name, session_id? -> register agent session",
3371
+ heartbeat: "agent_id -> update last_seen_at",
3372
+ set_focus: "agent_id, project_id? -> set active project context",
3373
+ list_agents: "no params -> registered agent list",
3374
+ send_feedback: "message, email?, category? -> save feedback locally"
3375
+ };
3376
+ var fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
3377
+ 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);
3378
+ function fmtSession(s) {
3379
+ const id = String(s["id"] ?? "").slice(0, 8);
3380
+ const agent = String(s["agent"] ?? "");
3381
+ const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
3382
+ const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
3383
+ const tok = fmtTok(Number(s["total_tokens"] ?? 0));
3384
+ return `${id} ${agent.padEnd(9)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
3385
+ }
3386
+ function text(text2) {
3387
+ return { content: [{ type: "text", text: text2 }] };
3388
+ }
3389
+ function textError(message) {
3390
+ return { content: [{ type: "text", text: message }], isError: true };
3391
+ }
3392
+ server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z.string().optional() }, async ({ query }) => {
3393
+ const q = query?.toLowerCase();
3394
+ const matches = q ? TOOL_NAMES.filter((name) => name.includes(q)) : [...TOOL_NAMES];
3395
+ return text(matches.join(", "));
3396
+ });
3397
+ server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z.array(z.string()) }, async ({ names }) => {
3398
+ const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
2989
3399
  `);
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(`
3004
- `));
3005
- });
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(`
3400
+ return text(result);
3401
+ });
3402
+ 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 }) => {
3403
+ const resolved = period ?? "today";
3404
+ const s = querySummary(db, resolved, machine);
3405
+ const machineLabel = machine ? ` on ${machine}` : "";
3406
+ return text([
3407
+ `period: ${resolved}${machineLabel}`,
3408
+ `cost: ${fmtUsd(s.total_usd)}`,
3409
+ `sessions: ${s.sessions}`,
3410
+ `requests: ${s.requests.toLocaleString()}`,
3411
+ `tokens: ${fmtTok(s.tokens)}`,
3412
+ `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)`
3413
+ ].join(`
3022
3414
  `));
3415
+ });
3416
+ server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, machine, limit(20)", {
3417
+ agent: z.enum(AGENTS).optional(),
3418
+ project: z.string().optional(),
3419
+ machine: z.string().optional(),
3420
+ limit: z.number().int().positive().max(100).optional()
3421
+ }, async ({ agent, project, machine, limit }) => {
3422
+ const sessions = querySessions(db, {
3423
+ agent,
3424
+ project,
3425
+ machine,
3426
+ limit: limit ?? 20
3023
3427
  });
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(`
3428
+ const lines = ["id agent cost tokens project"];
3429
+ for (const session of sessions)
3430
+ lines.push(fmtSession(session));
3431
+ return text(lines.join(`
3032
3432
  `));
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(`
3433
+ });
3434
+ server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
3435
+ n: z.number().int().positive().max(100).optional(),
3436
+ agent: z.enum(AGENTS).optional()
3437
+ }, async ({ n, agent }) => {
3438
+ const sessions = queryTopSessions(db, n ?? 10, agent);
3439
+ const lines = ["rank id agent cost tokens project"];
3440
+ sessions.forEach((session, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(session)}`));
3441
+ return text(lines.join(`
3041
3442
  `));
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(`
3443
+ });
3444
+ server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
3445
+ const rows = queryModelBreakdown(db);
3446
+ const lines = ["model agent reqs tokens cost"];
3447
+ for (const row of rows) {
3448
+ 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"]))}`);
3449
+ }
3450
+ return text(lines.join(`
3051
3451
  `));
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(`
3452
+ });
3453
+ 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 }) => {
3454
+ const rows = queryProjectBreakdown(db, period ?? "all");
3455
+ const lines = ["project sessions tokens cost"];
3456
+ for (const row of rows) {
3457
+ const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
3458
+ lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
3459
+ }
3460
+ return text(lines.join(`
3065
3461
  `));
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(`
3462
+ });
3463
+ 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 }) => {
3464
+ const rows = queryAgentBreakdown(db, period ?? "all");
3465
+ if (rows.length === 0)
3466
+ return text("No agent usage yet.");
3467
+ const lines = ["agent sessions requests tokens api_eq billable included"];
3468
+ for (const row of rows) {
3469
+ 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))}`);
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_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 }) => {
3475
+ const rows = queryAccountBreakdown(db, period ?? "all");
3476
+ if (rows.length === 0)
3477
+ return text("No account-attributed sessions yet.");
3478
+ const lines = ["account sessions requests tokens api_eq billable included"];
3479
+ for (const row of rows) {
3480
+ const label = String(row["account_key"] || row["account_name"] || "\u2014").slice(0, 20);
3481
+ 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))}`);
3482
+ }
3483
+ return text(lines.join(`
3151
3484
  `));
3152
- });
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(`
3485
+ });
3486
+ server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
3487
+ const budgets = getBudgetStatuses(db);
3488
+ if (budgets.length === 0)
3489
+ return text("No budgets set.");
3490
+ const lines = ["scope period spent limit used% status"];
3491
+ for (const budget of budgets) {
3492
+ const scope = String(budget["project_path"] ?? "global").slice(0, 20);
3493
+ const pct = Number(budget["percent_used"]).toFixed(1);
3494
+ const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
3495
+ 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}`);
3496
+ }
3497
+ return text(lines.join(`
3161
3498
  `));
3499
+ });
3500
+ server.tool("set_budget", "Create a spending budget. period: daily|weekly|monthly. limit_usd must be positive. alert_at_percent defaults to 80.", {
3501
+ period: z.enum(["daily", "weekly", "monthly"]),
3502
+ limit_usd: z.number().positive(),
3503
+ project_path: z.string().optional(),
3504
+ agent: z.enum(AGENTS).optional(),
3505
+ alert_at_percent: z.number().positive().max(100).optional()
3506
+ }, async ({ period, limit_usd, project_path, agent, alert_at_percent }) => {
3507
+ const now = new Date().toISOString();
3508
+ const id = randomUUID();
3509
+ upsertBudget(db, {
3510
+ id,
3511
+ project_path: project_path ?? null,
3512
+ agent: agent ?? null,
3513
+ period,
3514
+ limit_usd,
3515
+ alert_at_percent: alert_at_percent ?? 80,
3516
+ created_at: now,
3517
+ updated_at: now
3162
3518
  });
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(`
3519
+ return text(`Budget set: ${id}`);
3520
+ });
3521
+ server.tool("remove_budget", "Delete a budget by id.", { id: z.string() }, async ({ id }) => {
3522
+ deleteBudget(db, id);
3523
+ return text("Budget removed.");
3524
+ });
3525
+ server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage.", {}, async () => {
3526
+ const rows = listModelPricing(db);
3527
+ const lines = ["model input output cache-r cache-w cache-1h storage-h"];
3528
+ for (const row of rows) {
3529
+ 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)}`);
3530
+ }
3531
+ return text(lines.join(`
3179
3532
  `));
3533
+ });
3534
+ server.tool("set_pricing", "Create or update a model pricing row. Values are USD per 1M tokens except cache_storage_per_1m_hour.", {
3535
+ model: z.string().min(1),
3536
+ input_per_1m: z.number().nonnegative(),
3537
+ output_per_1m: z.number().nonnegative(),
3538
+ cache_read_per_1m: z.number().nonnegative().optional(),
3539
+ cache_write_per_1m: z.number().nonnegative().optional(),
3540
+ cache_write_1h_per_1m: z.number().nonnegative().optional(),
3541
+ cache_storage_per_1m_hour: z.number().nonnegative().optional()
3542
+ }, async (input) => {
3543
+ const model = input.model.trim();
3544
+ if (!model)
3545
+ return textError("model is required");
3546
+ upsertModelPricing(db, {
3547
+ model,
3548
+ input_per_1m: input.input_per_1m,
3549
+ output_per_1m: input.output_per_1m,
3550
+ cache_read_per_1m: input.cache_read_per_1m ?? 0,
3551
+ cache_write_per_1m: input.cache_write_per_1m ?? 0,
3552
+ cache_write_1h_per_1m: input.cache_write_1h_per_1m ?? 0,
3553
+ cache_storage_per_1m_hour: input.cache_storage_per_1m_hour ?? 0,
3554
+ updated_at: new Date().toISOString()
3180
3555
  });
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(`
3556
+ return text(`Pricing set: ${model}`);
3557
+ });
3558
+ server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z.string() }, async ({ model }) => {
3559
+ deleteModelPricing(db, model);
3560
+ return text("Pricing removed.");
3561
+ });
3562
+ server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
3563
+ const rows = queryDailyBreakdown(db, days ?? 30);
3564
+ const byDate = new Map;
3565
+ for (const row of rows) {
3566
+ const date = String(row["date"]);
3567
+ const agent = String(row["agent"]);
3568
+ const entry = byDate.get(date) ?? Object.fromEntries(AGENTS.map((name) => [name, 0]));
3569
+ entry[agent] = (entry[agent] ?? 0) + Number(row["cost_usd"]);
3570
+ byDate.set(date, entry);
3571
+ }
3572
+ const lines = [`date ${AGENTS.map((agent) => agent.slice(0, 8).padEnd(10)).join("")}total`];
3573
+ for (const [date, costs] of [...byDate.entries()].sort()) {
3574
+ const total = Object.values(costs).reduce((sum, value) => sum + value, 0);
3575
+ lines.push(`${date} ${AGENTS.map((agent) => fmtUsd(costs[agent] ?? 0).padEnd(10)).join("")}${fmtUsd(total)}`);
3576
+ }
3577
+ return text(lines.join(`
3212
3578
  `));
3213
- });
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(`
3245
- current machine: ${getMachineId()}`);
3246
- return text(lines.join(`
3579
+ });
3580
+ 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 }) => {
3581
+ const summary = queryBillingSummary(db, period ?? "month");
3582
+ const lines = ["provider billed"];
3583
+ for (const [provider, cost] of Object.entries(summary.by_provider)) {
3584
+ lines.push(`${provider.padEnd(11)}${fmtUsd(cost)}`);
3585
+ }
3586
+ lines.push(`total ${fmtUsd(summary.total_usd)}`);
3587
+ return text(lines.join(`
3247
3588
  `));
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}`);
3589
+ });
3590
+ server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
3591
+ const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(session_id, `${session_id}%`);
3592
+ if (!session)
3593
+ return textError(`Session not found: ${session_id}`);
3594
+ const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
3595
+ const lines = [
3596
+ `session: ${String(session["id"]).slice(0, 16)}`,
3597
+ `agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
3598
+ `cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
3599
+ "",
3600
+ "time model input output cache-r cache-5m cache-1h cost"
3601
+ ];
3602
+ for (const request of requests) {
3603
+ 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"]))}`);
3318
3604
  }
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 });
3605
+ return text(lines.join(`
3606
+ `));
3607
+ });
3608
+ server.tool("sync", `Ingest new cost data. sources: all|${AGENTS.join("|")}`, { sources: z.enum(["all", ...AGENTS]).optional() }, async ({ sources }) => {
3609
+ const selected = sources ?? "all";
3610
+ const opts = selected === "all" ? {} : { [selected]: true };
3611
+ const result = await syncAll(db, opts);
3612
+ return text(JSON.stringify(result, null, 2));
3613
+ });
3614
+ 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 }) => {
3615
+ const p = period ?? "month";
3616
+ const snaps = queryUsageSnapshots(db, { agent });
3617
+ const summary = querySummary(db, p, undefined, true);
3618
+ return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
3619
+ });
3620
+ 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 }) => {
3621
+ return text(JSON.stringify(querySavingsSummary(db, period ?? "month", agent), null, 2));
3622
+ });
3623
+ server.tool("list_subscriptions", "List configured subscription plans and included usage caps. No params.", {}, async () => {
3624
+ const rows = listSubscriptions(db);
3625
+ if (rows.length === 0)
3626
+ return text("No subscriptions configured.");
3627
+ const lines = ["id provider plan agent fee included active"];
3628
+ for (const row of rows) {
3629
+ lines.push(`${String(row["id"]).slice(0, 8).padEnd(9)}` + `${String(row["provider"]).slice(0, 12).padEnd(13)}` + `${String(row["plan"]).slice(0, 10).padEnd(11)}` + `${String(row["agent"] ?? "all").slice(0, 10).padEnd(11)}` + `${fmtUsd(Number(row["monthly_fee_usd"] ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row["included_usage_usd"] ?? 0)).padEnd(10)}` + `${Number(row["active"] ?? 0) ? "yes" : "no"}`);
3325
3630
  }
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
3631
+ return text(lines.join(`
3632
+ `));
3633
+ });
3634
+ server.tool("set_subscription", `Create or update a subscription plan. agent may be ${AGENTS.join("|")}.`, {
3635
+ id: z.string().optional(),
3636
+ provider: z.string(),
3637
+ plan: z.string(),
3638
+ agent: z.enum(AGENTS).optional(),
3639
+ monthly_fee_usd: z.number().optional(),
3640
+ included_usage_usd: z.number().optional(),
3641
+ billing_cycle_start: z.string().optional(),
3642
+ reset_policy: z.string().optional(),
3643
+ active: z.boolean().optional()
3644
+ }, async (input) => {
3645
+ if (input.monthly_fee_usd != null && input.monthly_fee_usd < 0)
3646
+ return text("monthly_fee_usd must be non-negative");
3647
+ if (input.included_usage_usd != null && input.included_usage_usd < 0)
3648
+ return text("included_usage_usd must be non-negative");
3649
+ const now = new Date().toISOString();
3650
+ const subscription = {
3651
+ id: input.id?.trim() || randomUUID(),
3652
+ agent: input.agent ?? null,
3653
+ provider: input.provider.trim(),
3654
+ plan: input.plan.trim(),
3655
+ monthly_fee_usd: input.monthly_fee_usd ?? 0,
3656
+ included_usage_usd: input.included_usage_usd ?? 0,
3657
+ billing_cycle_start: input.billing_cycle_start ?? null,
3658
+ reset_policy: input.reset_policy ?? "monthly",
3659
+ active: input.active === false ? 0 : 1,
3660
+ created_at: now,
3661
+ updated_at: now
3662
+ };
3663
+ if (!subscription.provider)
3664
+ return text("provider is required");
3665
+ if (!subscription.plan)
3666
+ return text("plan is required");
3667
+ upsertSubscription(db, subscription);
3668
+ return text(JSON.stringify(subscription, null, 2));
3669
+ });
3670
+ server.tool("remove_subscription", "Remove a subscription plan by id.", { id: z.string() }, async ({ id }) => {
3671
+ deleteSubscription(db, id);
3672
+ return text(`Removed subscription ${id}`);
3673
+ });
3674
+ 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 }) => {
3675
+ const cost = computeCostFromDb(db, model, input_tokens ?? 0, output_tokens ?? 0, 0, 0, 0);
3676
+ return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
3677
+ });
3678
+ server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
3679
+ const goals = getGoalStatuses(db);
3680
+ if (goals.length === 0)
3681
+ return text("No goals set.");
3682
+ const lines = ["period scope limit spent used% status"];
3683
+ for (const goal of goals) {
3684
+ const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
3685
+ const pct = Number(goal["percent_used"]).toFixed(1);
3686
+ const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
3687
+ 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}`);
3688
+ }
3689
+ return text(lines.join(`
3690
+ `));
3691
+ });
3692
+ server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
3693
+ period: z.enum(["day", "week", "month", "year"]),
3694
+ limit_usd: z.number().positive(),
3695
+ project_path: z.string().optional(),
3696
+ agent: z.enum(AGENTS).optional()
3697
+ }, async ({ period, limit_usd, project_path, agent }) => {
3698
+ const now = new Date().toISOString();
3699
+ upsertGoal(db, {
3700
+ id: randomUUID(),
3701
+ period,
3702
+ project_path: project_path ?? null,
3703
+ agent: agent ?? null,
3704
+ limit_usd,
3705
+ created_at: now,
3706
+ updated_at: now
3344
3707
  });
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;
3708
+ return text(`Goal set: ${period} $${limit_usd}`);
3709
+ });
3710
+ server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
3711
+ deleteGoal(db, id);
3712
+ return text("Goal removed.");
3713
+ });
3714
+ server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
3715
+ const machines = listMachines(db);
3716
+ if (machines.length === 0)
3717
+ return text(`No machine data yet. Current machine: ${getMachineId()}`);
3718
+ const lines = ["machine sessions requests cost last_active"];
3719
+ for (const m of machines) {
3720
+ 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"}`);
3721
+ }
3722
+ lines.push(`
3723
+ current machine: ${getMachineId()}`);
3724
+ return text(lines.join(`
3725
+ `));
3726
+ });
3727
+ server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
3728
+ const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
3729
+ if (existing) {
3730
+ existing.last_seen_at = new Date().toISOString();
3731
+ return text(JSON.stringify(existing));
3732
+ }
3733
+ const id = Math.random().toString(36).slice(2, 10);
3734
+ const agent = { id, name, last_seen_at: new Date().toISOString() };
3735
+ _econAgents.set(id, agent);
3736
+ return text(JSON.stringify(agent));
3737
+ });
3738
+ server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
3739
+ const agent = _econAgents.get(agent_id);
3740
+ if (!agent)
3741
+ return textError("Agent not found");
3742
+ agent.last_seen_at = new Date().toISOString();
3743
+ return text(`\u2665 ${agent.name}`);
3744
+ });
3745
+ server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
3746
+ const agent = _econAgents.get(agent_id);
3747
+ if (!agent)
3748
+ return textError("Agent not found");
3749
+ agent.project_id = project_id ?? undefined;
3750
+ return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
3751
+ });
3752
+ server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
3753
+ server.tool("send_feedback", "Send feedback about this service.", {
3754
+ message: z.string(),
3755
+ email: z.string().optional(),
3756
+ category: z.enum(["bug", "feature", "general"]).optional()
3757
+ }, async ({ message, email, category }) => {
3758
+ try {
3759
+ db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(message, email ?? null, category ?? "general", packageMetadata.version);
3760
+ return text("Feedback saved. Thank you!");
3761
+ } catch (error) {
3762
+ return textError(String(error));
3381
3763
  }
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
3764
  });
3765
+ var transport = new StdioServerTransport;
3766
+ registerCloudTools(server, "economy", {
3767
+ dbPath: getDbPath(),
3768
+ migrations: PG_MIGRATIONS
3769
+ });
3770
+ await server.connect(transport);