@hasna/economy 0.2.27 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -566,6 +566,26 @@ function openDatabase(dbPath, skipSeed = false) {
566
566
  }
567
567
  return db;
568
568
  }
569
+ function quoteSqlIdent(identifier) {
570
+ return `"${identifier.replace(/"/g, '""')}"`;
571
+ }
572
+ function hasColumn(db, table, column) {
573
+ const columns = db.prepare(`PRAGMA table_info(${quoteSqlIdent(table)})`).all();
574
+ return columns.some((c) => c.name === column);
575
+ }
576
+ function addColumnIfMissing(db, table, column, definition) {
577
+ if (hasColumn(db, table, column))
578
+ return false;
579
+ try {
580
+ db.exec(`ALTER TABLE ${quoteSqlIdent(table)} ADD COLUMN ${quoteSqlIdent(column)} ${definition}`);
581
+ return true;
582
+ } catch (error) {
583
+ const message = error instanceof Error ? error.message : String(error);
584
+ if (/duplicate column name/i.test(message))
585
+ return true;
586
+ throw error;
587
+ }
588
+ }
569
589
  function initSchema(db) {
570
590
  db.exec(`
571
591
  CREATE TABLE IF NOT EXISTS requests (
@@ -736,59 +756,31 @@ function initSchema(db) {
736
756
  CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
737
757
  CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
738
758
  `);
739
- const cols = db.prepare(`PRAGMA table_info(requests)`).all();
740
- if (!cols.some((c) => c.name === "machine_id")) {
741
- db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
742
- db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
743
- }
744
- if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
745
- db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
759
+ addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
760
+ addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
761
+ if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
746
762
  db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
747
763
  }
748
- if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
749
- db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
750
- }
751
- if (!cols.some((c) => c.name === "cost_basis")) {
752
- db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
753
- }
754
- if (!cols.some((c) => c.name === "attribution_tag")) {
755
- db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
756
- }
757
- if (!cols.some((c) => c.name === "updated_at")) {
758
- db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
764
+ addColumnIfMissing(db, "requests", "cache_create_1h_tokens", "INTEGER DEFAULT 0");
765
+ addColumnIfMissing(db, "requests", "cost_basis", `TEXT DEFAULT 'estimated'`);
766
+ addColumnIfMissing(db, "requests", "attribution_tag", `TEXT DEFAULT ''`);
767
+ if (addColumnIfMissing(db, "requests", "updated_at", `TEXT DEFAULT ''`)) {
759
768
  db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
760
769
  }
761
- if (!cols.some((c) => c.name === "synced_at")) {
762
- db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
763
- }
770
+ addColumnIfMissing(db, "requests", "synced_at", `TEXT DEFAULT ''`);
764
771
  for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
765
- if (!cols.some((c) => c.name === column)) {
766
- db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
767
- }
768
- }
769
- const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
770
- if (!sessionCols.some((c) => c.name === "attribution_tag")) {
771
- db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
772
+ addColumnIfMissing(db, "requests", column, `TEXT DEFAULT ''`);
772
773
  }
773
- if (!sessionCols.some((c) => c.name === "updated_at")) {
774
- db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
774
+ addColumnIfMissing(db, "sessions", "attribution_tag", `TEXT DEFAULT ''`);
775
+ if (addColumnIfMissing(db, "sessions", "updated_at", `TEXT DEFAULT ''`)) {
775
776
  db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
776
777
  }
777
- if (!sessionCols.some((c) => c.name === "synced_at")) {
778
- db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
779
- }
778
+ addColumnIfMissing(db, "sessions", "synced_at", `TEXT DEFAULT ''`);
780
779
  for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
781
- if (!sessionCols.some((c) => c.name === column)) {
782
- db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
783
- }
784
- }
785
- const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
786
- if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
787
- db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
788
- }
789
- if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
790
- db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
780
+ addColumnIfMissing(db, "sessions", column, `TEXT DEFAULT ''`);
791
781
  }
782
+ addColumnIfMissing(db, "model_pricing", "cache_write_1h_per_1m", "REAL NOT NULL DEFAULT 0");
783
+ addColumnIfMissing(db, "model_pricing", "cache_storage_per_1m_hour", "REAL NOT NULL DEFAULT 0");
792
784
  db.exec(`
793
785
  CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
794
786
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
@@ -949,17 +941,22 @@ function querySummary(db, period, machine, allMachines = false) {
949
941
  const codexTotals = db.prepare(`
950
942
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
951
943
  COALESCE(SUM(total_tokens), 0) as tokens,
944
+ COALESCE(SUM(request_count), 0) as requests,
952
945
  COUNT(*) as sessions
953
946
  FROM sessions
954
947
  WHERE ${sWhere}${machineClause}
955
948
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
956
949
  `).get();
957
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
950
+ const requestSessionCount = db.prepare(`
951
+ SELECT COUNT(DISTINCT session_id) as sessions
952
+ FROM requests
953
+ WHERE ${rWhere}${machineClause}
954
+ `).get();
958
955
  return {
959
956
  total_usd: r.total_usd + codexTotals.cost_usd,
960
- requests: r.requests,
957
+ requests: r.requests + codexTotals.requests,
961
958
  tokens: r.tokens + codexTotals.tokens,
962
- sessions: sessionCount.sessions,
959
+ sessions: requestSessionCount.sessions + codexTotals.sessions,
963
960
  period
964
961
  };
965
962
  }
@@ -974,8 +971,10 @@ function queryModelBreakdown(db) {
974
971
  FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
975
972
  `).all();
976
973
  }
977
- function queryAgentBreakdown(db, period = "all") {
974
+ function queryAgentBreakdown(db, period = "all", machine) {
978
975
  const requestWhere = requestPeriodWhere(period);
976
+ const machineClause = machine ? " AND machine_id = ?" : "";
977
+ const machineParams = machine ? [machine] : [];
979
978
  const groups = new Map;
980
979
  const requestRows = db.prepare(`
981
980
  SELECT agent,
@@ -991,10 +990,10 @@ function queryAgentBreakdown(db, period = "all") {
991
990
  COALESCE(SUM(cost_usd), 0) as cost_usd,
992
991
  MAX(timestamp) as last_active
993
992
  FROM requests
994
- WHERE ${requestWhere}
993
+ WHERE ${requestWhere}${machineClause}
995
994
  GROUP BY agent
996
995
  ORDER BY api_equivalent_usd DESC
997
- `).all();
996
+ `).all(...machineParams);
998
997
  for (const row of requestRows) {
999
998
  groups.set(row.agent, row);
1000
999
  }
@@ -1007,10 +1006,10 @@ function queryAgentBreakdown(db, period = "all") {
1007
1006
  COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1008
1007
  MAX(started_at) as last_active
1009
1008
  FROM sessions
1010
- WHERE ${sessionWhere}
1009
+ WHERE ${sessionWhere}${machineClause}
1011
1010
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1012
1011
  GROUP BY agent
1013
- `).all();
1012
+ `).all(...machineParams);
1014
1013
  for (const row of sessionOnlyRows) {
1015
1014
  const existing = groups.get(row.agent) ?? {
1016
1015
  agent: row.agent,
@@ -1087,14 +1086,20 @@ function labelForPath(projectPath, projectName) {
1087
1086
  function groupKeyForPath(projectPath, projectName) {
1088
1087
  return labelForPath(projectPath, projectName).trim().toLowerCase();
1089
1088
  }
1090
- function queryProjectBreakdown(db, period = "all") {
1089
+ function queryProjectBreakdown(db, period = "all", machine) {
1091
1090
  const requestWhere = requestPeriodWhere(period);
1092
1091
  const sessionWhere = sessionPeriodWhere(period);
1092
+ const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
1093
+ const requestMachineClause = machine ? " AND machine_id = ?" : "";
1094
+ const sessionMachineParams = machine ? [machine, machine] : [];
1095
+ const requestMachineParams = machine ? [machine] : [];
1096
+ const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
1097
+ const sessionOnlyMachineParams = machine ? [machine] : [];
1093
1098
  const sessions = db.prepare(`
1094
1099
  SELECT id, project_path, project_name, total_cost_usd, started_at
1095
1100
  FROM sessions
1096
- WHERE project_path != '' OR project_name != ''
1097
- `).all();
1101
+ WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
1102
+ `).all(...sessionMachineParams);
1098
1103
  const groups = new Map;
1099
1104
  for (const s of sessions) {
1100
1105
  const label = labelForPath(s.project_path, s.project_name);
@@ -1120,7 +1125,8 @@ function queryProjectBreakdown(db, period = "all") {
1120
1125
  FROM requests
1121
1126
  WHERE session_id IN (${placeholders})
1122
1127
  AND ${requestWhere}
1123
- `).get(...g.sessionIds) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
1128
+ ${requestMachineClause}
1129
+ `).get(...g.sessionIds, ...requestMachineParams) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
1124
1130
  const sessionOnlyStats = placeholders.length ? db.prepare(`
1125
1131
  SELECT
1126
1132
  COUNT(*) as sessions,
@@ -1131,8 +1137,9 @@ function queryProjectBreakdown(db, period = "all") {
1131
1137
  FROM sessions
1132
1138
  WHERE id IN (${placeholders})
1133
1139
  AND ${sessionWhere}
1140
+ ${sessionOnlyMachineClause}
1134
1141
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1135
- `).get(...g.sessionIds) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1142
+ `).get(...g.sessionIds, ...sessionOnlyMachineParams) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1136
1143
  const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
1137
1144
  if (totalSessions === 0)
1138
1145
  continue;
@@ -1150,107 +1157,156 @@ function queryProjectBreakdown(db, period = "all") {
1150
1157
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1151
1158
  return result;
1152
1159
  }
1153
- function queryAccountBreakdown(db, period = "all") {
1160
+ function normalizeAccountEmail(email) {
1161
+ return (email ?? "").trim().toLowerCase();
1162
+ }
1163
+ function accountIdentityKey(agent, accountKey, accountName, accountEmail) {
1164
+ const identityAgent = (agent || "").trim();
1165
+ const normalizedEmail = normalizeAccountEmail(accountEmail);
1166
+ if (identityAgent && normalizedEmail)
1167
+ return `${identityAgent}:${normalizedEmail}`;
1168
+ if (identityAgent && accountName)
1169
+ return `${identityAgent}:${accountName}`;
1170
+ if (accountKey)
1171
+ return accountKey;
1172
+ return identityAgent ? `${identityAgent}:unknown` : "";
1173
+ }
1174
+ function addAccountBreakdownRow(groups, row, sessionOnly) {
1175
+ const agent = row.agent || row.account_tool;
1176
+ const email = normalizeAccountEmail(row.account_email);
1177
+ const accountName = row.account_name || email || row.account_key;
1178
+ const key = accountIdentityKey(agent, row.account_key, accountName, email);
1179
+ if (!key)
1180
+ return;
1181
+ const group = groups.get(key) ?? {
1182
+ account_key: key,
1183
+ account_tool: agent,
1184
+ account_name: accountName,
1185
+ account_email: email || null,
1186
+ account_source: row.account_source || "unknown",
1187
+ sessionIds: new Set,
1188
+ requests: 0,
1189
+ total_tokens: 0,
1190
+ api_equivalent_usd: 0,
1191
+ metered_api_usd: 0,
1192
+ subscription_included_usd: 0,
1193
+ estimated_usd: 0,
1194
+ unknown_usd: 0,
1195
+ last_active: ""
1196
+ };
1197
+ if (!group.account_email && email)
1198
+ group.account_email = email;
1199
+ if (!group.account_name && accountName)
1200
+ group.account_name = accountName;
1201
+ if ((!group.account_source || group.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
1202
+ group.account_source = row.account_source;
1203
+ }
1204
+ if (row.session_id)
1205
+ group.sessionIds.add(row.session_id);
1206
+ group.requests += row.requests;
1207
+ group.total_tokens += row.total_tokens;
1208
+ group.api_equivalent_usd += row.cost_usd;
1209
+ if (sessionOnly) {
1210
+ group.estimated_usd += row.cost_usd;
1211
+ } else if (row.cost_basis === "metered_api") {
1212
+ group.metered_api_usd += row.cost_usd;
1213
+ } else if (row.cost_basis === "subscription_included") {
1214
+ group.subscription_included_usd += row.cost_usd;
1215
+ } else if (row.cost_basis === "unknown") {
1216
+ group.unknown_usd += row.cost_usd;
1217
+ } else {
1218
+ group.estimated_usd += row.cost_usd;
1219
+ }
1220
+ if (!group.last_active || row.last_active > group.last_active)
1221
+ group.last_active = row.last_active;
1222
+ groups.set(key, group);
1223
+ }
1224
+ function queryAccountBreakdown(db, period = "all", machine) {
1154
1225
  const requestWhere = requestPeriodWhere(period);
1155
1226
  const sessionWhere = sessionPeriodWhere(period);
1156
- const sessions = db.prepare(`
1157
- SELECT id, account_key, account_tool, account_name, account_email, account_source,
1158
- total_cost_usd, total_tokens, request_count, started_at
1159
- FROM sessions
1160
- WHERE account_key != '' OR account_tool != '' OR account_name != '' OR account_email != ''
1161
- `).all();
1227
+ const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
1228
+ const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
1229
+ const requestMachineParams = machine ? [machine] : [];
1230
+ const sessionMachineParams = machine ? [machine] : [];
1162
1231
  const groups = new Map;
1163
- for (const session of sessions) {
1164
- const key = session.account_key || `${session.account_tool}:${session.account_name}`;
1165
- if (!key || key === ":")
1166
- continue;
1167
- const group = groups.get(key) ?? {
1168
- sessionIds: [],
1169
- account_tool: session.account_tool,
1170
- account_name: session.account_name,
1171
- account_email: session.account_email || null,
1172
- account_source: session.account_source || "unknown"
1173
- };
1174
- group.sessionIds.push(session.id);
1175
- groups.set(key, group);
1176
- }
1177
- const result = [];
1178
- for (const [key, group] of groups.entries()) {
1179
- const placeholders = group.sessionIds.map(() => "?").join(",");
1180
- const reqStats = placeholders ? db.prepare(`
1181
- SELECT
1182
- COUNT(DISTINCT session_id) as sessions,
1183
- COUNT(*) as requests,
1184
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1185
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1186
- COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
1187
- COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
1188
- COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
1189
- COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
1190
- MAX(timestamp) as last_active
1191
- FROM requests
1192
- WHERE session_id IN (${placeholders})
1193
- AND ${requestWhere}
1194
- `).get(...group.sessionIds) : {
1195
- sessions: 0,
1196
- requests: 0,
1197
- cost_usd: 0,
1198
- total_tokens: 0,
1199
- metered_api_usd: 0,
1200
- subscription_included_usd: 0,
1201
- estimated_usd: 0,
1202
- unknown_usd: 0,
1203
- last_active: null
1204
- };
1205
- const sessionOnlyStats = placeholders ? db.prepare(`
1206
- SELECT
1207
- COUNT(*) as sessions,
1208
- COALESCE(SUM(request_count), 0) as requests,
1209
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1210
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1211
- MAX(started_at) as last_active
1212
- FROM sessions
1213
- WHERE id IN (${placeholders})
1214
- AND ${sessionWhere}
1215
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1216
- `).get(...group.sessionIds) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1217
- const sessionsTotal = reqStats.sessions + sessionOnlyStats.sessions;
1218
- if (sessionsTotal === 0)
1219
- continue;
1220
- const apiEquivalentUsd = reqStats.cost_usd + sessionOnlyStats.cost_usd;
1221
- const estimatedUsd = reqStats.estimated_usd + sessionOnlyStats.cost_usd;
1222
- const billableUsd = reqStats.metered_api_usd;
1223
- const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
1224
- result.push({
1225
- account_key: key,
1226
- account_tool: group.account_tool,
1227
- account_name: group.account_name,
1228
- account_email: group.account_email,
1229
- account_source: group.account_source,
1230
- sessions: sessionsTotal,
1231
- requests: reqStats.requests + sessionOnlyStats.requests,
1232
- total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
1233
- api_equivalent_usd: apiEquivalentUsd,
1234
- billable_usd: billableUsd,
1235
- metered_api_usd: reqStats.metered_api_usd,
1236
- subscription_included_usd: reqStats.subscription_included_usd,
1237
- estimated_usd: estimatedUsd,
1238
- unknown_usd: reqStats.unknown_usd,
1239
- cost_usd: apiEquivalentUsd,
1240
- last_active: lastActive
1241
- });
1242
- }
1232
+ const requestRows = db.prepare(`
1233
+ SELECT
1234
+ r.session_id as session_id,
1235
+ COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
1236
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
1237
+ COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
1238
+ COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
1239
+ COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
1240
+ COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
1241
+ 1 as requests,
1242
+ COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
1243
+ COALESCE(r.cost_usd, 0) as cost_usd,
1244
+ COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
1245
+ r.timestamp as last_active
1246
+ FROM requests r
1247
+ LEFT JOIN sessions s ON s.id = r.session_id
1248
+ WHERE ${requestWhere}${requestMachineClause}
1249
+ AND (
1250
+ COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
1251
+ OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
1252
+ OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
1253
+ OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
1254
+ )
1255
+ `).all(...requestMachineParams);
1256
+ for (const row of requestRows)
1257
+ addAccountBreakdownRow(groups, row, false);
1258
+ const sessionOnlyRows = db.prepare(`
1259
+ SELECT
1260
+ s.id as session_id,
1261
+ s.agent as agent,
1262
+ s.account_key as account_key,
1263
+ s.account_tool as account_tool,
1264
+ s.account_name as account_name,
1265
+ s.account_email as account_email,
1266
+ COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
1267
+ COALESCE(s.request_count, 0) as requests,
1268
+ COALESCE(s.total_tokens, 0) as total_tokens,
1269
+ COALESCE(s.total_cost_usd, 0) as cost_usd,
1270
+ 'estimated' as cost_basis,
1271
+ s.started_at as last_active
1272
+ FROM sessions s
1273
+ WHERE ${sessionWhere}${sessionMachineClause}
1274
+ AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
1275
+ AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
1276
+ `).all(...sessionMachineParams);
1277
+ for (const row of sessionOnlyRows)
1278
+ addAccountBreakdownRow(groups, row, true);
1279
+ const result = [...groups.values()].map((group) => ({
1280
+ account_key: group.account_key,
1281
+ account_tool: group.account_tool,
1282
+ account_name: group.account_name,
1283
+ account_email: group.account_email,
1284
+ account_source: group.account_source,
1285
+ sessions: group.sessionIds.size,
1286
+ requests: group.requests,
1287
+ total_tokens: group.total_tokens,
1288
+ api_equivalent_usd: group.api_equivalent_usd,
1289
+ billable_usd: group.metered_api_usd,
1290
+ metered_api_usd: group.metered_api_usd,
1291
+ subscription_included_usd: group.subscription_included_usd,
1292
+ estimated_usd: group.estimated_usd,
1293
+ unknown_usd: group.unknown_usd,
1294
+ cost_usd: group.api_equivalent_usd,
1295
+ last_active: group.last_active
1296
+ }));
1243
1297
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1244
1298
  return result;
1245
1299
  }
1246
- function queryDailyBreakdown(db, days = 30) {
1300
+ function queryDailyBreakdown(db, days = 30, machine) {
1301
+ const machineClause = machine ? " AND machine_id = ?" : "";
1302
+ const params = machine ? [`-${days}`, machine] : [`-${days}`];
1247
1303
  return db.prepare(`
1248
1304
  SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
1249
1305
  FROM requests
1250
- WHERE timestamp >= DATE('now', ? || ' days')
1306
+ WHERE timestamp >= DATE('now', ? || ' days')${machineClause}
1251
1307
  GROUP BY DATE(timestamp), agent
1252
1308
  ORDER BY date ASC
1253
- `).all(`-${days}`);
1309
+ `).all(...params);
1254
1310
  }
1255
1311
  function upsertBudget(db, budget) {
1256
1312
  db.prepare(`
@@ -1349,17 +1405,48 @@ function queryBillingSummary(db, period) {
1349
1405
  }
1350
1406
  return { total_usd: total, by_provider };
1351
1407
  }
1352
- function listMachines(db) {
1408
+ function listMachines(db, period = "all") {
1409
+ const rWhere = requestPeriodWhere(period);
1410
+ const sWhere = sessionPeriodWhere(period);
1353
1411
  return db.prepare(`
1412
+ WITH request_stats AS (
1413
+ SELECT
1414
+ machine_id,
1415
+ COUNT(DISTINCT session_id) as sessions,
1416
+ COUNT(*) as requests,
1417
+ COALESCE(SUM(cost_usd), 0) as total_cost_usd,
1418
+ MAX(timestamp) as last_active
1419
+ FROM requests
1420
+ WHERE machine_id != ''
1421
+ AND ${rWhere}
1422
+ GROUP BY machine_id
1423
+ ),
1424
+ session_only_stats AS (
1425
+ SELECT
1426
+ machine_id,
1427
+ COUNT(*) as sessions,
1428
+ COALESCE(SUM(request_count), 0) as requests,
1429
+ COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
1430
+ MAX(started_at) as last_active
1431
+ FROM sessions
1432
+ WHERE machine_id != ''
1433
+ AND ${sWhere}
1434
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1435
+ GROUP BY machine_id
1436
+ ),
1437
+ combined AS (
1438
+ SELECT * FROM request_stats
1439
+ UNION ALL
1440
+ SELECT * FROM session_only_stats
1441
+ )
1354
1442
  SELECT
1355
- s.machine_id,
1356
- COUNT(DISTINCT s.id) as sessions,
1357
- COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
1358
- COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
1359
- MAX(s.started_at) as last_active
1360
- FROM sessions s
1361
- WHERE s.machine_id != ''
1362
- GROUP BY s.machine_id
1443
+ machine_id,
1444
+ COALESCE(SUM(sessions), 0) as sessions,
1445
+ COALESCE(SUM(requests), 0) as requests,
1446
+ COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
1447
+ MAX(last_active) as last_active
1448
+ FROM combined
1449
+ GROUP BY machine_id
1363
1450
  ORDER BY total_cost_usd DESC
1364
1451
  `).all();
1365
1452
  }
@@ -1648,18 +1735,36 @@ var init_pg_migrations = __esm(() => {
1648
1735
  });
1649
1736
 
1650
1737
  // src/mcp/index.ts
1738
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1739
+
1740
+ // src/lib/package-metadata.ts
1741
+ import { readFileSync } from "fs";
1742
+ var cachedMetadata = null;
1743
+ function getPackageMetadata() {
1744
+ if (cachedMetadata)
1745
+ return cachedMetadata;
1746
+ const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf8");
1747
+ const parsed = JSON.parse(raw);
1748
+ cachedMetadata = {
1749
+ name: parsed.name ?? "@hasna/economy",
1750
+ version: parsed.version ?? "0.0.0"
1751
+ };
1752
+ return cachedMetadata;
1753
+ }
1754
+ var packageMetadata = getPackageMetadata();
1755
+
1756
+ // src/mcp/server.ts
1651
1757
  init_database();
1652
1758
  init_pg_migrations();
1653
1759
  import { randomUUID } from "crypto";
1654
1760
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1655
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1656
1761
  import { registerCloudTools } from "@hasna/cloud";
1657
1762
  import { z } from "zod";
1658
1763
 
1659
1764
  // src/ingest/claude.ts
1660
1765
  init_database();
1661
1766
  init_pricing();
1662
- import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
1767
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync2, statSync as statSync2 } from "fs";
1663
1768
  import { homedir as homedir2 } from "os";
1664
1769
  import { join as join2, basename } from "path";
1665
1770
 
@@ -1828,18 +1933,22 @@ var AGENT_ACCOUNT_TOOLS = {
1828
1933
  pi: ["pi"],
1829
1934
  hermes: ["hermes"]
1830
1935
  };
1831
- function accountKey(tool, name) {
1832
- return `${tool}:${name}`;
1936
+ function normalizeEmail(email) {
1937
+ return (email ?? "").trim().toLowerCase();
1938
+ }
1939
+ function accountKey(tool, name, email) {
1940
+ const normalizedEmail = normalizeEmail(email);
1941
+ return `${tool}:${normalizedEmail || name}`;
1833
1942
  }
1834
1943
  function normalizeDir(value) {
1835
1944
  return value.replace(/\/+$/, "");
1836
1945
  }
1837
1946
  function fromProfile(profile, source) {
1838
1947
  return {
1839
- account_key: accountKey(profile.tool, profile.name),
1948
+ account_key: accountKey(profile.tool, profile.name, profile.email),
1840
1949
  account_tool: profile.tool,
1841
1950
  account_name: profile.name,
1842
- ...profile.email ? { account_email: profile.email } : {},
1951
+ ...profile.email ? { account_email: normalizeEmail(profile.email) } : {},
1843
1952
  account_source: source
1844
1953
  };
1845
1954
  }
@@ -1851,10 +1960,12 @@ function fromOverride(raw, agent) {
1851
1960
  const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
1852
1961
  if (!tool || !name)
1853
1962
  return null;
1963
+ const email = name.includes("@") ? normalizeEmail(name) : undefined;
1854
1964
  return {
1855
- account_key: accountKey(tool, name),
1965
+ account_key: accountKey(tool, name, email),
1856
1966
  account_tool: tool,
1857
1967
  account_name: name,
1968
+ ...email ? { account_email: email } : {},
1858
1969
  account_source: "override"
1859
1970
  };
1860
1971
  }
@@ -1867,11 +1978,12 @@ function envOverride(agent, env) {
1867
1978
  const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
1868
1979
  if (!tool || !name)
1869
1980
  return null;
1981
+ const email = normalizeEmail(env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"]);
1870
1982
  return {
1871
- account_key: accountKey(tool, name),
1983
+ account_key: accountKey(tool, name, email),
1872
1984
  account_tool: tool,
1873
1985
  account_name: name,
1874
- account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
1986
+ ...email ? { account_email: email } : {},
1875
1987
  account_source: "override"
1876
1988
  };
1877
1989
  }
@@ -2002,7 +2114,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2002
2114
  continue;
2003
2115
  let lines;
2004
2116
  try {
2005
- lines = readFileSync(filePath, "utf-8").split(`
2117
+ lines = readFileSync2(filePath, "utf-8").split(`
2006
2118
  `).filter((l) => l.trim());
2007
2119
  } catch {
2008
2120
  continue;
@@ -2122,7 +2234,7 @@ function supportsClaudeDataResidencyPricing(model) {
2122
2234
  // src/ingest/codex.ts
2123
2235
  init_database();
2124
2236
  init_pricing();
2125
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2237
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2126
2238
  import { homedir as homedir3 } from "os";
2127
2239
  import { join as join3, basename as basename2 } from "path";
2128
2240
  import { Database as BunDatabase } from "bun:sqlite";
@@ -2140,7 +2252,7 @@ function readCodexModel() {
2140
2252
  if (!existsSync3(configPath))
2141
2253
  return "gpt-5-codex";
2142
2254
  try {
2143
- const content = readFileSync2(configPath, "utf-8");
2255
+ const content = readFileSync3(configPath, "utf-8");
2144
2256
  const match = content.match(/^model\s*=\s*"([^"]+)"/m);
2145
2257
  return match?.[1] ?? "gpt-5-codex";
2146
2258
  } catch {
@@ -2183,7 +2295,7 @@ function readTokenEvents(rolloutPath) {
2183
2295
  const fallbackUsages = new Map;
2184
2296
  let fallbackTimestamp;
2185
2297
  let aggregate = null;
2186
- for (const line of readFileSync2(rolloutPath, "utf-8").split(`
2298
+ for (const line of readFileSync3(rolloutPath, "utf-8").split(`
2187
2299
  `)) {
2188
2300
  if (!line.trim())
2189
2301
  continue;
@@ -2339,7 +2451,7 @@ async function ingestCodex(db, verbose = false) {
2339
2451
  // src/ingest/gemini.ts
2340
2452
  init_database();
2341
2453
  init_pricing();
2342
- import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
2454
+ import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync4, statSync as statSync3 } from "fs";
2343
2455
  import { homedir as homedir4 } from "os";
2344
2456
  import { join as join4, basename as basename3 } from "path";
2345
2457
  var DEFAULT_GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
@@ -2379,7 +2491,7 @@ function projectRoot(projectDir, chatData) {
2379
2491
  const rootFile = join4(projectDir, ".project_root");
2380
2492
  try {
2381
2493
  if (existsSync4(rootFile))
2382
- return readFileSync3(rootFile, "utf-8").trim();
2494
+ return readFileSync4(rootFile, "utf-8").trim();
2383
2495
  } catch {}
2384
2496
  return "";
2385
2497
  }
@@ -2420,7 +2532,7 @@ async function ingestGemini(db, verbose) {
2420
2532
  continue;
2421
2533
  let chatData;
2422
2534
  try {
2423
- chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
2535
+ chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
2424
2536
  } catch {
2425
2537
  continue;
2426
2538
  }
@@ -2498,7 +2610,7 @@ async function ingestGemini(db, verbose) {
2498
2610
  // src/ingest/opencode.ts
2499
2611
  init_database();
2500
2612
  init_pricing();
2501
- import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
2613
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
2502
2614
  import { homedir as homedir5 } from "os";
2503
2615
  import { join as join5 } from "path";
2504
2616
  var OPENCODE_STORAGE = join5(homedir5(), ".local", "share", "opencode", "storage");
@@ -2541,7 +2653,7 @@ async function ingestOpenCode(db, verbose = false) {
2541
2653
  continue;
2542
2654
  let parsed;
2543
2655
  try {
2544
- parsed = JSON.parse(readFileSync4(file, "utf-8"));
2656
+ parsed = JSON.parse(readFileSync5(file, "utf-8"));
2545
2657
  } catch {
2546
2658
  continue;
2547
2659
  }
@@ -2728,7 +2840,7 @@ async function ingestCursor(db, verbose = false) {
2728
2840
 
2729
2841
  // src/ingest/pi.ts
2730
2842
  init_database();
2731
- import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
2843
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
2732
2844
  import { homedir as homedir6 } from "os";
2733
2845
  import { join as join6 } from "path";
2734
2846
  var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join6(homedir6(), ".pi", "agent", "sessions");
@@ -2758,7 +2870,7 @@ async function ingestPi(db, verbose = false) {
2758
2870
  continue;
2759
2871
  let data;
2760
2872
  try {
2761
- data = JSON.parse(readFileSync5(file, "utf-8"));
2873
+ data = JSON.parse(readFileSync6(file, "utf-8"));
2762
2874
  } catch {
2763
2875
  continue;
2764
2876
  }
@@ -2913,7 +3025,7 @@ function statSyncSafe(path) {
2913
3025
 
2914
3026
  // src/ingest/claude-quota.ts
2915
3027
  init_database();
2916
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
3028
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
2917
3029
 
2918
3030
  // src/lib/paths.ts
2919
3031
  import { homedir as homedir8 } from "os";
@@ -2951,7 +3063,7 @@ function readClaudeToken() {
2951
3063
  if (!existsSync8(CREDENTIALS_PATH))
2952
3064
  return null;
2953
3065
  try {
2954
- const creds = JSON.parse(readFileSync6(CREDENTIALS_PATH, "utf-8"));
3066
+ const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
2955
3067
  const oauth = creds.claudeAiOauth;
2956
3068
  if (!oauth?.accessToken)
2957
3069
  return null;
@@ -3087,7 +3199,7 @@ async function ingestClaudeQuota(db, verbose = false) {
3087
3199
 
3088
3200
  // src/ingest/codex-quota.ts
3089
3201
  init_database();
3090
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
3202
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
3091
3203
  var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
3092
3204
  function readCodexAuth() {
3093
3205
  const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
@@ -3097,7 +3209,7 @@ function readCodexAuth() {
3097
3209
  if (!existsSync9(authPath))
3098
3210
  return null;
3099
3211
  try {
3100
- const auth = JSON.parse(readFileSync7(authPath, "utf-8"));
3212
+ const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
3101
3213
  const token = auth.tokens?.access_token;
3102
3214
  if (!token)
3103
3215
  return null;
@@ -3229,24 +3341,6 @@ init_database();
3229
3341
 
3230
3342
  // src/lib/cloud-sync.ts
3231
3343
  init_database();
3232
-
3233
- // src/lib/package-metadata.ts
3234
- import { readFileSync as readFileSync8 } from "fs";
3235
- var cachedMetadata = null;
3236
- function getPackageMetadata() {
3237
- if (cachedMetadata)
3238
- return cachedMetadata;
3239
- const raw = readFileSync8(new URL("../../package.json", import.meta.url), "utf8");
3240
- const parsed = JSON.parse(raw);
3241
- cachedMetadata = {
3242
- name: parsed.name ?? "@hasna/economy",
3243
- version: parsed.version ?? "0.0.0"
3244
- };
3245
- return cachedMetadata;
3246
- }
3247
- var packageMetadata = getPackageMetadata();
3248
-
3249
- // src/lib/cloud-sync.ts
3250
3344
  var CLOUD_TABLES = [
3251
3345
  "requests",
3252
3346
  "sessions",
@@ -3412,6 +3506,9 @@ var AGENTS = [
3412
3506
  "hermes"
3413
3507
  ];
3414
3508
 
3509
+ // src/mcp/server.ts
3510
+ init_database();
3511
+
3415
3512
  // src/lib/periods.ts
3416
3513
  function ymd(date) {
3417
3514
  return date.toISOString().substring(0, 10);
@@ -3440,501 +3537,580 @@ function usageSnapshotFilterForPeriod(period) {
3440
3537
  }
3441
3538
  }
3442
3539
 
3443
- // src/mcp/index.ts
3444
- init_database();
3540
+ // src/mcp/server.ts
3445
3541
  init_pricing();
3446
3542
  init_pricing();
3447
- function printHelp() {
3448
- console.log(`Usage: economy-mcp [options]
3449
-
3450
- Runs the ${packageMetadata.name} MCP stdio server.
3451
-
3452
- Options:
3453
- -V, --version output the version number
3454
- -h, --help display help for command`);
3455
- }
3456
- var args = process.argv.slice(2);
3457
- if (args.includes("--help") || args.includes("-h")) {
3458
- printHelp();
3459
- process.exit(0);
3460
- }
3461
- if (args.includes("--version") || args.includes("-V")) {
3462
- console.log(packageMetadata.version);
3463
- process.exit(0);
3464
- }
3465
- var db = openDatabase();
3466
- ensurePricingSeeded(db);
3467
- var server = new McpServer({
3468
- name: "economy",
3469
- version: packageMetadata.version
3470
- });
3471
- var _econAgents = new Map;
3472
- var TOOL_NAMES = [
3473
- "get_cost_summary",
3474
- "get_sessions",
3475
- "get_top_sessions",
3476
- "get_model_breakdown",
3477
- "get_project_breakdown",
3478
- "get_agent_breakdown",
3479
- "get_account_breakdown",
3480
- "get_budget_status",
3481
- "set_budget",
3482
- "remove_budget",
3483
- "get_pricing",
3484
- "set_pricing",
3485
- "remove_pricing",
3486
- "get_daily",
3487
- "get_billing_summary",
3488
- "get_session_detail",
3489
- "get_usage",
3490
- "get_savings",
3491
- "list_subscriptions",
3492
- "set_subscription",
3493
- "remove_subscription",
3494
- "estimate_cost",
3495
- "sync",
3496
- "search_tools",
3497
- "describe_tools",
3498
- "get_goals",
3499
- "set_goal",
3500
- "remove_goal",
3501
- "list_machines",
3502
- "register_agent",
3503
- "heartbeat",
3504
- "set_focus",
3505
- "list_agents",
3506
- "send_feedback"
3507
- ];
3508
- var TOOL_DESCRIPTIONS = {
3509
- get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
3510
- get_sessions: `agent(${AGENTS.join("|")}), project(partial), account?(key/name/email), machine?(hostname), limit(20) -> compact session table`,
3511
- get_top_sessions: `n(10), agent(${AGENTS.join("|")}) -> top sessions by cost`,
3512
- list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
3513
- get_model_breakdown: "no params -> model, requests, tokens, cost",
3514
- get_project_breakdown: "period?(today|week|month|year|all) -> project_name, sessions, tokens, cost",
3515
- get_agent_breakdown: "period?(today|week|month|year|all) -> agent, sessions, requests, tokens, api-equivalent, billable, included",
3516
- get_account_breakdown: "period?(today|week|month|year|all) -> account profile, sessions, requests, tokens, api-equivalent, billable, included",
3517
- get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
3518
- set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, alert_at_percent? -> create budget",
3519
- remove_budget: "id -> delete budget",
3520
- get_pricing: "no params -> model pricing rows with input, output, cache read/write, and cache storage rates",
3521
- 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",
3522
- remove_pricing: "model -> delete pricing row",
3523
- get_daily: "days(30) -> daily cost table grouped by date and agent",
3524
- get_billing_summary: "period(today|yesterday|week|month|year|all) -> actual provider billing totals",
3525
- get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
3526
- get_usage: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> usage snapshots and all-machine summary`,
3527
- get_savings: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> subscription/API-equivalent savings`,
3528
- list_subscriptions: "no params -> configured subscription plans and included usage",
3529
- set_subscription: `provider, plan, monthly_fee_usd?, included_usage_usd?, agent?(${AGENTS.join("|")}) -> create/update subscription plan`,
3530
- remove_subscription: "id -> delete subscription plan",
3531
- estimate_cost: "model, input_tokens?, output_tokens? -> pre-flight token cost estimate",
3532
- sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
3533
- search_tools: "query substring -> tool name list",
3534
- describe_tools: "names[] -> one-line parameter hints",
3535
- get_goals: "no params -> goal progress summary",
3536
- set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
3537
- remove_goal: "id -> delete goal",
3538
- register_agent: "name, session_id? -> register agent session",
3539
- heartbeat: "agent_id -> update last_seen_at",
3540
- set_focus: "agent_id, project_id? -> set active project context",
3541
- list_agents: "no params -> registered agent list",
3542
- send_feedback: "message, email?, category? -> save feedback locally"
3543
- };
3544
- var fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
3545
- 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);
3546
- function fmtSession(s) {
3547
- const id = String(s["id"] ?? "").slice(0, 8);
3548
- const agent = String(s["agent"] ?? "");
3549
- const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
3550
- const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
3551
- const tok = fmtTok(Number(s["total_tokens"] ?? 0));
3552
- return `${id} ${agent.padEnd(9)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
3553
- }
3554
- function text(text2) {
3555
- return { content: [{ type: "text", text: text2 }] };
3556
- }
3557
- function textError(message) {
3558
- return { content: [{ type: "text", text: message }], isError: true };
3559
- }
3560
- server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z.string().optional() }, async ({ query }) => {
3561
- const q = query?.toLowerCase();
3562
- const matches = q ? TOOL_NAMES.filter((name) => name.includes(q)) : [...TOOL_NAMES];
3563
- return text(matches.join(", "));
3564
- });
3565
- server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z.array(z.string()) }, async ({ names }) => {
3566
- const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
3543
+ var MCP_NAME = "economy";
3544
+ var DEFAULT_MCP_HTTP_PORT = 8860;
3545
+ function buildServer() {
3546
+ const db = openDatabase();
3547
+ ensurePricingSeeded(db);
3548
+ const server = new McpServer({
3549
+ name: MCP_NAME,
3550
+ version: packageMetadata.version
3551
+ });
3552
+ const _econAgents = new Map;
3553
+ const TOOL_NAMES = [
3554
+ "get_cost_summary",
3555
+ "get_sessions",
3556
+ "get_top_sessions",
3557
+ "get_model_breakdown",
3558
+ "get_project_breakdown",
3559
+ "get_agent_breakdown",
3560
+ "get_account_breakdown",
3561
+ "get_budget_status",
3562
+ "set_budget",
3563
+ "remove_budget",
3564
+ "get_pricing",
3565
+ "set_pricing",
3566
+ "remove_pricing",
3567
+ "get_daily",
3568
+ "get_billing_summary",
3569
+ "get_session_detail",
3570
+ "get_usage",
3571
+ "get_savings",
3572
+ "list_subscriptions",
3573
+ "set_subscription",
3574
+ "remove_subscription",
3575
+ "sync",
3576
+ "search_tools",
3577
+ "describe_tools",
3578
+ "get_goals",
3579
+ "set_goal",
3580
+ "remove_goal",
3581
+ "list_machines",
3582
+ "register_agent",
3583
+ "heartbeat",
3584
+ "set_focus",
3585
+ "list_agents",
3586
+ "send_feedback"
3587
+ ];
3588
+ const TOOL_DESCRIPTIONS = {
3589
+ get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
3590
+ get_sessions: `agent(${AGENTS.join("|")}), project(partial), account?(key/name/email), machine?(hostname), limit(20) -> compact session table`,
3591
+ get_top_sessions: `n(10), agent(${AGENTS.join("|")}) -> top sessions by cost`,
3592
+ list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
3593
+ get_model_breakdown: "no params -> model, requests, tokens, cost",
3594
+ get_project_breakdown: "period?(today|week|month|year|all) -> project_name, sessions, tokens, cost",
3595
+ get_agent_breakdown: "period?(today|week|month|year|all) -> agent, sessions, requests, tokens, api-equivalent, billable, included",
3596
+ get_account_breakdown: "period?(today|week|month|year|all) -> account profile, sessions, requests, tokens, api-equivalent, billable, included",
3597
+ get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
3598
+ set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, alert_at_percent? -> create budget",
3599
+ remove_budget: "id -> delete budget",
3600
+ get_pricing: "no params -> model pricing rows with input, output, cache read/write, and cache storage rates",
3601
+ 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",
3602
+ remove_pricing: "model -> delete pricing row",
3603
+ get_daily: "days(30) -> daily cost table grouped by date and agent",
3604
+ get_billing_summary: "period(today|yesterday|week|month|year|all) -> actual provider billing totals",
3605
+ get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
3606
+ get_usage: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> usage snapshots and all-machine summary`,
3607
+ get_savings: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> subscription/API-equivalent savings`,
3608
+ list_subscriptions: "no params -> configured subscription plans and included usage",
3609
+ set_subscription: `provider, plan, monthly_fee_usd?, included_usage_usd?, agent?(${AGENTS.join("|")}) -> create/update subscription plan`,
3610
+ remove_subscription: "id -> delete subscription plan",
3611
+ sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
3612
+ search_tools: "query substring -> tool name list",
3613
+ describe_tools: "names[] -> one-line parameter hints",
3614
+ get_goals: "no params -> goal progress summary",
3615
+ set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
3616
+ remove_goal: "id -> delete goal",
3617
+ register_agent: "name, session_id? -> register agent session",
3618
+ heartbeat: "agent_id -> update last_seen_at",
3619
+ set_focus: "agent_id, project_id? -> set active project context",
3620
+ list_agents: "no params -> registered agent list",
3621
+ send_feedback: "message, email?, category? -> save feedback locally"
3622
+ };
3623
+ const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
3624
+ 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);
3625
+ function fmtSession(s) {
3626
+ const id = String(s["id"] ?? "").slice(0, 8);
3627
+ const agent = String(s["agent"] ?? "");
3628
+ const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
3629
+ const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
3630
+ const tok = fmtTok(Number(s["total_tokens"] ?? 0));
3631
+ return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
3632
+ }
3633
+ function text(text2) {
3634
+ return { content: [{ type: "text", text: text2 }] };
3635
+ }
3636
+ function textError(message) {
3637
+ return { content: [{ type: "text", text: message }], isError: true };
3638
+ }
3639
+ server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z.string().optional() }, async ({ query }) => {
3640
+ const q = query?.toLowerCase();
3641
+ const matches = q ? TOOL_NAMES.filter((name) => name.includes(q)) : [...TOOL_NAMES];
3642
+ return text(matches.join(", "));
3643
+ });
3644
+ server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z.array(z.string()) }, async ({ names }) => {
3645
+ const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
3567
3646
  `);
3568
- return text(result);
3569
- });
3570
- 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 }) => {
3571
- const resolved = period ?? "today";
3572
- const s = querySummary(db, resolved, machine);
3573
- const machineLabel = machine ? ` on ${machine}` : "";
3574
- return text([
3575
- `period: ${resolved}${machineLabel}`,
3576
- `cost: ${fmtUsd(s.total_usd)}`,
3577
- `sessions: ${s.sessions}`,
3578
- `requests: ${s.requests.toLocaleString()}`,
3579
- `tokens: ${fmtTok(s.tokens)}`,
3580
- `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)`
3581
- ].join(`
3647
+ return text(result);
3648
+ });
3649
+ 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 }) => {
3650
+ const resolved = period ?? "today";
3651
+ const s = querySummary(db, resolved, machine);
3652
+ const machineLabel = machine ? ` on ${machine}` : "";
3653
+ return text([
3654
+ `period: ${resolved}${machineLabel}`,
3655
+ `cost: ${fmtUsd(s.total_usd)}`,
3656
+ `sessions: ${s.sessions}`,
3657
+ `requests: ${s.requests.toLocaleString()}`,
3658
+ `tokens: ${fmtTok(s.tokens)}`,
3659
+ `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)`
3660
+ ].join(`
3582
3661
  `));
3583
- });
3584
- server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, account, machine, limit(20)", {
3585
- agent: z.enum(AGENTS).optional(),
3586
- project: z.string().optional(),
3587
- account: z.string().optional(),
3588
- machine: z.string().optional(),
3589
- limit: z.number().int().positive().max(100).optional()
3590
- }, async ({ agent, project, account, machine, limit }) => {
3591
- const sessions = querySessions(db, {
3592
- agent,
3593
- project,
3594
- account,
3595
- machine,
3596
- limit: limit ?? 20
3597
3662
  });
3598
- const lines = ["id agent cost tokens project"];
3599
- for (const session of sessions)
3600
- lines.push(fmtSession(session));
3601
- return text(lines.join(`
3663
+ server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, account, machine, limit(20)", {
3664
+ agent: z.enum(AGENTS).optional(),
3665
+ project: z.string().optional(),
3666
+ account: z.string().optional(),
3667
+ machine: z.string().optional(),
3668
+ limit: z.number().int().positive().max(100).optional()
3669
+ }, async ({ agent, project, account, machine, limit }) => {
3670
+ const sessions = querySessions(db, {
3671
+ agent,
3672
+ project,
3673
+ account,
3674
+ machine,
3675
+ limit: limit ?? 20
3676
+ });
3677
+ const lines = ["id agent cost tokens project"];
3678
+ for (const session of sessions)
3679
+ lines.push(fmtSession(session));
3680
+ return text(lines.join(`
3602
3681
  `));
3603
- });
3604
- server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
3605
- n: z.number().int().positive().max(100).optional(),
3606
- agent: z.enum(AGENTS).optional()
3607
- }, async ({ n, agent }) => {
3608
- const sessions = queryTopSessions(db, n ?? 10, agent);
3609
- const lines = ["rank id agent cost tokens project"];
3610
- sessions.forEach((session, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(session)}`));
3611
- return text(lines.join(`
3682
+ });
3683
+ server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
3684
+ n: z.number().int().positive().max(100).optional(),
3685
+ agent: z.enum(AGENTS).optional()
3686
+ }, async ({ n, agent }) => {
3687
+ const sessions = queryTopSessions(db, n ?? 10, agent);
3688
+ const lines = ["rank id agent cost tokens project"];
3689
+ sessions.forEach((session, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(session)}`));
3690
+ return text(lines.join(`
3612
3691
  `));
3613
- });
3614
- server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
3615
- const rows = queryModelBreakdown(db);
3616
- const lines = ["model agent reqs tokens cost"];
3617
- for (const row of rows) {
3618
- 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"]))}`);
3619
- }
3620
- return text(lines.join(`
3692
+ });
3693
+ server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
3694
+ const rows = queryModelBreakdown(db);
3695
+ const lines = ["model agent reqs tokens cost"];
3696
+ for (const row of rows) {
3697
+ 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"]))}`);
3698
+ }
3699
+ return text(lines.join(`
3621
3700
  `));
3622
- });
3623
- 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 }) => {
3624
- const rows = queryProjectBreakdown(db, period ?? "all");
3625
- const lines = ["project sessions tokens cost"];
3626
- for (const row of rows) {
3627
- const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
3628
- lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
3629
- }
3630
- return text(lines.join(`
3701
+ });
3702
+ 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 }) => {
3703
+ const rows = queryProjectBreakdown(db, period ?? "all");
3704
+ const lines = ["project sessions tokens cost"];
3705
+ for (const row of rows) {
3706
+ const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
3707
+ lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
3708
+ }
3709
+ return text(lines.join(`
3631
3710
  `));
3632
- });
3633
- 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 }) => {
3634
- const rows = queryAgentBreakdown(db, period ?? "all");
3635
- if (rows.length === 0)
3636
- return text("No agent usage yet.");
3637
- const lines = ["agent sessions requests tokens api_eq billable included"];
3638
- for (const row of rows) {
3639
- 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))}`);
3640
- }
3641
- return text(lines.join(`
3711
+ });
3712
+ 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 }) => {
3713
+ const rows = queryAgentBreakdown(db, period ?? "all");
3714
+ if (rows.length === 0)
3715
+ return text("No agent usage yet.");
3716
+ const lines = ["agent sessions requests tokens api_eq billable included"];
3717
+ for (const row of rows) {
3718
+ 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))}`);
3719
+ }
3720
+ return text(lines.join(`
3642
3721
  `));
3643
- });
3644
- 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 }) => {
3645
- const rows = queryAccountBreakdown(db, period ?? "all");
3646
- if (rows.length === 0)
3647
- return text("No account-attributed sessions yet.");
3648
- const lines = ["account sessions requests tokens api_eq billable included"];
3649
- for (const row of rows) {
3650
- const label = String(row["account_key"] || row["account_name"] || "\u2014").slice(0, 20);
3651
- 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))}`);
3652
- }
3653
- return text(lines.join(`
3722
+ });
3723
+ 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 }) => {
3724
+ const rows = queryAccountBreakdown(db, period ?? "all");
3725
+ if (rows.length === 0)
3726
+ return text("No account-attributed sessions yet.");
3727
+ const lines = ["account agent sessions requests tokens api_eq billable included"];
3728
+ for (const row of rows) {
3729
+ const label = String(row["account_email"] || row["account_name"] || row["account_key"] || "\u2014").slice(0, 20);
3730
+ lines.push(`${label.padEnd(21)}` + `${String(row["account_tool"] ?? "").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))}`);
3731
+ }
3732
+ return text(lines.join(`
3654
3733
  `));
3655
- });
3656
- server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
3657
- const budgets = getBudgetStatuses(db);
3658
- if (budgets.length === 0)
3659
- return text("No budgets set.");
3660
- const lines = ["scope period spent limit used% status"];
3661
- for (const budget of budgets) {
3662
- const scope = String(budget["project_path"] ?? "global").slice(0, 20);
3663
- const pct = Number(budget["percent_used"]).toFixed(1);
3664
- const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
3665
- 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}`);
3666
- }
3667
- return text(lines.join(`
3734
+ });
3735
+ server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
3736
+ const budgets = getBudgetStatuses(db);
3737
+ if (budgets.length === 0)
3738
+ return text("No budgets set.");
3739
+ const lines = ["scope period spent limit used% status"];
3740
+ for (const budget of budgets) {
3741
+ const scope = String(budget["project_path"] ?? "global").slice(0, 20);
3742
+ const pct = Number(budget["percent_used"]).toFixed(1);
3743
+ const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
3744
+ 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}`);
3745
+ }
3746
+ return text(lines.join(`
3668
3747
  `));
3669
- });
3670
- server.tool("set_budget", "Create a spending budget. period: daily|weekly|monthly. limit_usd must be positive. alert_at_percent defaults to 80.", {
3671
- period: z.enum(["daily", "weekly", "monthly"]),
3672
- limit_usd: z.number().positive(),
3673
- project_path: z.string().optional(),
3674
- agent: z.enum(AGENTS).optional(),
3675
- alert_at_percent: z.number().positive().max(100).optional()
3676
- }, async ({ period, limit_usd, project_path, agent, alert_at_percent }) => {
3677
- const now = new Date().toISOString();
3678
- const id = randomUUID();
3679
- upsertBudget(db, {
3680
- id,
3681
- project_path: project_path ?? null,
3682
- agent: agent ?? null,
3683
- period,
3684
- limit_usd,
3685
- alert_at_percent: alert_at_percent ?? 80,
3686
- created_at: now,
3687
- updated_at: now
3688
3748
  });
3689
- return text(`Budget set: ${id}`);
3690
- });
3691
- server.tool("remove_budget", "Delete a budget by id.", { id: z.string() }, async ({ id }) => {
3692
- deleteBudget(db, id);
3693
- return text("Budget removed.");
3694
- });
3695
- server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage.", {}, async () => {
3696
- const rows = listModelPricing(db);
3697
- const lines = ["model input output cache-r cache-w cache-1h storage-h"];
3698
- for (const row of rows) {
3699
- 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)}`);
3700
- }
3701
- return text(lines.join(`
3749
+ server.tool("set_budget", "Create a spending budget. period: daily|weekly|monthly. limit_usd must be positive. alert_at_percent defaults to 80.", {
3750
+ period: z.enum(["daily", "weekly", "monthly"]),
3751
+ limit_usd: z.number().positive(),
3752
+ project_path: z.string().optional(),
3753
+ agent: z.enum(AGENTS).optional(),
3754
+ alert_at_percent: z.number().positive().max(100).optional()
3755
+ }, async ({ period, limit_usd, project_path, agent, alert_at_percent }) => {
3756
+ const now = new Date().toISOString();
3757
+ const id = randomUUID();
3758
+ upsertBudget(db, {
3759
+ id,
3760
+ project_path: project_path ?? null,
3761
+ agent: agent ?? null,
3762
+ period,
3763
+ limit_usd,
3764
+ alert_at_percent: alert_at_percent ?? 80,
3765
+ created_at: now,
3766
+ updated_at: now
3767
+ });
3768
+ return text(`Budget set: ${id}`);
3769
+ });
3770
+ server.tool("remove_budget", "Delete a budget by id.", { id: z.string() }, async ({ id }) => {
3771
+ deleteBudget(db, id);
3772
+ return text("Budget removed.");
3773
+ });
3774
+ server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage.", {}, async () => {
3775
+ const rows = listModelPricing(db);
3776
+ const lines = ["model input output cache-r cache-w cache-1h storage-h"];
3777
+ for (const row of rows) {
3778
+ 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)}`);
3779
+ }
3780
+ return text(lines.join(`
3702
3781
  `));
3703
- });
3704
- server.tool("set_pricing", "Create or update a model pricing row. Values are USD per 1M tokens except cache_storage_per_1m_hour.", {
3705
- model: z.string().min(1),
3706
- input_per_1m: z.number().nonnegative(),
3707
- output_per_1m: z.number().nonnegative(),
3708
- cache_read_per_1m: z.number().nonnegative().optional(),
3709
- cache_write_per_1m: z.number().nonnegative().optional(),
3710
- cache_write_1h_per_1m: z.number().nonnegative().optional(),
3711
- cache_storage_per_1m_hour: z.number().nonnegative().optional()
3712
- }, async (input) => {
3713
- const model = input.model.trim();
3714
- if (!model)
3715
- return textError("model is required");
3716
- upsertModelPricing(db, {
3717
- model,
3718
- input_per_1m: input.input_per_1m,
3719
- output_per_1m: input.output_per_1m,
3720
- cache_read_per_1m: input.cache_read_per_1m ?? 0,
3721
- cache_write_per_1m: input.cache_write_per_1m ?? 0,
3722
- cache_write_1h_per_1m: input.cache_write_1h_per_1m ?? 0,
3723
- cache_storage_per_1m_hour: input.cache_storage_per_1m_hour ?? 0,
3724
- updated_at: new Date().toISOString()
3725
3782
  });
3726
- return text(`Pricing set: ${model}`);
3727
- });
3728
- server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z.string() }, async ({ model }) => {
3729
- deleteModelPricing(db, model);
3730
- return text("Pricing removed.");
3731
- });
3732
- server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
3733
- const rows = queryDailyBreakdown(db, days ?? 30);
3734
- const byDate = new Map;
3735
- for (const row of rows) {
3736
- const date = String(row["date"]);
3737
- const agent = String(row["agent"]);
3738
- const entry = byDate.get(date) ?? Object.fromEntries(AGENTS.map((name) => [name, 0]));
3739
- entry[agent] = (entry[agent] ?? 0) + Number(row["cost_usd"]);
3740
- byDate.set(date, entry);
3741
- }
3742
- const lines = [`date ${AGENTS.map((agent) => agent.slice(0, 8).padEnd(10)).join("")}total`];
3743
- for (const [date, costs] of [...byDate.entries()].sort()) {
3744
- const total = Object.values(costs).reduce((sum, value) => sum + value, 0);
3745
- lines.push(`${date} ${AGENTS.map((agent) => fmtUsd(costs[agent] ?? 0).padEnd(10)).join("")}${fmtUsd(total)}`);
3746
- }
3747
- return text(lines.join(`
3783
+ server.tool("set_pricing", "Create or update a model pricing row. Values are USD per 1M tokens except cache_storage_per_1m_hour.", {
3784
+ model: z.string().min(1),
3785
+ input_per_1m: z.number().nonnegative(),
3786
+ output_per_1m: z.number().nonnegative(),
3787
+ cache_read_per_1m: z.number().nonnegative().optional(),
3788
+ cache_write_per_1m: z.number().nonnegative().optional(),
3789
+ cache_write_1h_per_1m: z.number().nonnegative().optional(),
3790
+ cache_storage_per_1m_hour: z.number().nonnegative().optional()
3791
+ }, async (input) => {
3792
+ const model = input.model.trim();
3793
+ if (!model)
3794
+ return textError("model is required");
3795
+ upsertModelPricing(db, {
3796
+ model,
3797
+ input_per_1m: input.input_per_1m,
3798
+ output_per_1m: input.output_per_1m,
3799
+ cache_read_per_1m: input.cache_read_per_1m ?? 0,
3800
+ cache_write_per_1m: input.cache_write_per_1m ?? 0,
3801
+ cache_write_1h_per_1m: input.cache_write_1h_per_1m ?? 0,
3802
+ cache_storage_per_1m_hour: input.cache_storage_per_1m_hour ?? 0,
3803
+ updated_at: new Date().toISOString()
3804
+ });
3805
+ return text(`Pricing set: ${model}`);
3806
+ });
3807
+ server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z.string() }, async ({ model }) => {
3808
+ deleteModelPricing(db, model);
3809
+ return text("Pricing removed.");
3810
+ });
3811
+ server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
3812
+ const rows = queryDailyBreakdown(db, days ?? 30);
3813
+ const byDate = new Map;
3814
+ for (const row of rows) {
3815
+ const date = String(row["date"]);
3816
+ const entry = byDate.get(date) ?? { claude: 0, takumi: 0, codex: 0, gemini: 0 };
3817
+ if (row["agent"] === "claude")
3818
+ entry.claude += Number(row["cost_usd"]);
3819
+ else if (row["agent"] === "takumi")
3820
+ entry.takumi += Number(row["cost_usd"]);
3821
+ else if (row["agent"] === "codex")
3822
+ entry.codex += Number(row["cost_usd"]);
3823
+ else if (row["agent"] === "gemini")
3824
+ entry.gemini += Number(row["cost_usd"]);
3825
+ byDate.set(date, entry);
3826
+ }
3827
+ const lines = ["date claude takumi codex gemini total"];
3828
+ for (const [date, costs] of [...byDate.entries()].sort()) {
3829
+ const total = costs.claude + costs.takumi + costs.codex + costs.gemini;
3830
+ 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)}`);
3831
+ }
3832
+ return text(lines.join(`
3748
3833
  `));
3749
- });
3750
- 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 }) => {
3751
- const summary = queryBillingSummary(db, period ?? "month");
3752
- const lines = ["provider billed"];
3753
- for (const [provider, cost] of Object.entries(summary.by_provider)) {
3754
- lines.push(`${provider.padEnd(11)}${fmtUsd(cost)}`);
3755
- }
3756
- lines.push(`total ${fmtUsd(summary.total_usd)}`);
3757
- return text(lines.join(`
3834
+ });
3835
+ 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 }) => {
3836
+ const summary = queryBillingSummary(db, period ?? "month");
3837
+ const lines = ["provider billed"];
3838
+ for (const [provider, cost] of Object.entries(summary.by_provider)) {
3839
+ lines.push(`${provider.padEnd(11)}${fmtUsd(cost)}`);
3840
+ }
3841
+ lines.push(`total ${fmtUsd(summary.total_usd)}`);
3842
+ return text(lines.join(`
3758
3843
  `));
3759
- });
3760
- server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
3761
- const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(session_id, `${session_id}%`);
3762
- if (!session)
3763
- return textError(`Session not found: ${session_id}`);
3764
- const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
3765
- const lines = [
3766
- `session: ${String(session["id"]).slice(0, 16)}`,
3767
- `agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
3768
- `cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
3769
- "",
3770
- "time model input output cache-r cache-5m cache-1h cost"
3771
- ];
3772
- for (const request of requests) {
3773
- 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"]))}`);
3774
- }
3775
- return text(lines.join(`
3844
+ });
3845
+ server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
3846
+ const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(session_id, `${session_id}%`);
3847
+ if (!session)
3848
+ return textError(`Session not found: ${session_id}`);
3849
+ const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
3850
+ const lines = [
3851
+ `session: ${String(session["id"]).slice(0, 16)}`,
3852
+ `agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
3853
+ `cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
3854
+ "",
3855
+ "time model input output cache-r cache-5m cache-1h cost"
3856
+ ];
3857
+ for (const request of requests) {
3858
+ 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"]))}`);
3859
+ }
3860
+ return text(lines.join(`
3776
3861
  `));
3777
- });
3778
- server.tool("sync", `Ingest new cost data. sources: all|${AGENTS.join("|")}`, { sources: z.enum(["all", ...AGENTS]).optional() }, async ({ sources }) => {
3779
- const selected = sources ?? "all";
3780
- const opts = selected === "all" ? {} : { [selected]: true };
3781
- const result = await syncAll(db, opts);
3782
- return text(JSON.stringify(result, null, 2));
3783
- });
3784
- server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month|year|all", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
3785
- const p = period ?? "month";
3786
- const snaps = queryUsageSnapshots(db, { agent, ...usageSnapshotFilterForPeriod(p) });
3787
- const summary = querySummary(db, p, undefined, true);
3788
- return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
3789
- });
3790
- 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 }) => {
3791
- return text(JSON.stringify(querySavingsSummary(db, period ?? "month", agent), null, 2));
3792
- });
3793
- server.tool("list_subscriptions", "List configured subscription plans and included usage caps. No params.", {}, async () => {
3794
- const rows = listSubscriptions(db);
3795
- if (rows.length === 0)
3796
- return text("No subscriptions configured.");
3797
- const lines = ["id provider plan agent fee included active"];
3798
- for (const row of rows) {
3799
- 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"}`);
3800
- }
3801
- return text(lines.join(`
3862
+ });
3863
+ server.tool("sync", `Ingest new cost data. sources: all|${AGENTS.join("|")}`, { sources: z.enum(["all", ...AGENTS]).optional() }, async ({ sources }) => {
3864
+ const selected = sources ?? "all";
3865
+ const opts = selected === "all" ? {} : { [selected]: true };
3866
+ const result = await syncAll(db, opts);
3867
+ return text(JSON.stringify(result, null, 2));
3868
+ });
3869
+ server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month|year|all", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
3870
+ const p = period ?? "month";
3871
+ const snaps = queryUsageSnapshots(db, { agent, ...usageSnapshotFilterForPeriod(p) });
3872
+ const summary = querySummary(db, p, undefined, true);
3873
+ return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
3874
+ });
3875
+ 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 }) => {
3876
+ return text(JSON.stringify(querySavingsSummary(db, period ?? "month", agent), null, 2));
3877
+ });
3878
+ server.tool("list_subscriptions", "List configured subscription plans used by savings calculations.", {}, async () => {
3879
+ const rows = listSubscriptions(db);
3880
+ if (rows.length === 0)
3881
+ return text("No subscriptions configured.");
3882
+ const lines = ["id provider plan agent fee included active"];
3883
+ for (const row of rows) {
3884
+ lines.push(`${row.id.slice(0, 18).padEnd(19)}` + `${row.provider.slice(0, 10).padEnd(11)}` + `${row.plan.slice(0, 10).padEnd(11)}` + `${(row.agent ?? "all").slice(0, 10).padEnd(11)}` + `${fmtUsd(row.monthly_fee_usd).padEnd(10)}` + `${fmtUsd(row.included_usage_usd).padEnd(10)}` + `${row.active ? "yes" : "no"}`);
3885
+ }
3886
+ return text(lines.join(`
3802
3887
  `));
3803
- });
3804
- server.tool("set_subscription", `Create or update a subscription plan. agent may be ${AGENTS.join("|")}.`, {
3805
- id: z.string().optional(),
3806
- provider: z.string(),
3807
- plan: z.string(),
3808
- agent: z.enum(AGENTS).optional(),
3809
- monthly_fee_usd: z.number().optional(),
3810
- included_usage_usd: z.number().optional(),
3811
- billing_cycle_start: z.string().optional(),
3812
- reset_policy: z.string().optional(),
3813
- active: z.boolean().optional()
3814
- }, async (input) => {
3815
- if (input.monthly_fee_usd != null && input.monthly_fee_usd < 0)
3816
- return text("monthly_fee_usd must be non-negative");
3817
- if (input.included_usage_usd != null && input.included_usage_usd < 0)
3818
- return text("included_usage_usd must be non-negative");
3819
- const now = new Date().toISOString();
3820
- const subscription = {
3821
- id: input.id?.trim() || randomUUID(),
3822
- agent: input.agent ?? null,
3823
- provider: input.provider.trim(),
3824
- plan: input.plan.trim(),
3825
- monthly_fee_usd: input.monthly_fee_usd ?? 0,
3826
- included_usage_usd: input.included_usage_usd ?? 0,
3827
- billing_cycle_start: input.billing_cycle_start ?? null,
3828
- reset_policy: input.reset_policy ?? "monthly",
3829
- active: input.active === false ? 0 : 1,
3830
- created_at: now,
3831
- updated_at: now
3832
- };
3833
- if (!subscription.provider)
3834
- return text("provider is required");
3835
- if (!subscription.plan)
3836
- return text("plan is required");
3837
- upsertSubscription(db, subscription);
3838
- return text(JSON.stringify(subscription, null, 2));
3839
- });
3840
- server.tool("remove_subscription", "Remove a subscription plan by id.", { id: z.string() }, async ({ id }) => {
3841
- deleteSubscription(db, id);
3842
- return text(`Removed subscription ${id}`);
3843
- });
3844
- 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 }) => {
3845
- const cost = computeCostFromDb(db, model, input_tokens ?? 0, output_tokens ?? 0, 0, 0, 0);
3846
- return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
3847
- });
3848
- server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
3849
- const goals = getGoalStatuses(db);
3850
- if (goals.length === 0)
3851
- return text("No goals set.");
3852
- const lines = ["period scope limit spent used% status"];
3853
- for (const goal of goals) {
3854
- const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
3855
- const pct = Number(goal["percent_used"]).toFixed(1);
3856
- const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
3857
- 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}`);
3858
- }
3859
- return text(lines.join(`
3888
+ });
3889
+ server.tool("set_subscription", "Create or update a subscription plan used by subscription-vs-API savings calculations.", {
3890
+ id: z.string().optional(),
3891
+ provider: z.string().min(1),
3892
+ plan: z.string().min(1),
3893
+ agent: z.enum(AGENTS).optional(),
3894
+ monthly_fee_usd: z.number().nonnegative().optional(),
3895
+ included_usage_usd: z.number().nonnegative().optional(),
3896
+ billing_cycle_start: z.string().optional(),
3897
+ reset_policy: z.string().optional(),
3898
+ active: z.boolean().optional()
3899
+ }, async (input) => {
3900
+ const now = new Date().toISOString();
3901
+ const row = {
3902
+ id: input.id ?? randomUUID(),
3903
+ provider: input.provider,
3904
+ plan: input.plan,
3905
+ agent: input.agent ?? null,
3906
+ monthly_fee_usd: input.monthly_fee_usd ?? 0,
3907
+ included_usage_usd: input.included_usage_usd ?? 0,
3908
+ billing_cycle_start: input.billing_cycle_start ?? null,
3909
+ reset_policy: input.reset_policy ?? "monthly",
3910
+ active: input.active === false ? 0 : 1,
3911
+ created_at: now,
3912
+ updated_at: now
3913
+ };
3914
+ upsertSubscription(db, row);
3915
+ return text(JSON.stringify(row, null, 2));
3916
+ });
3917
+ server.tool("remove_subscription", "Delete a subscription plan by id.", { id: z.string() }, async ({ id }) => {
3918
+ deleteSubscription(db, id);
3919
+ return text("Subscription removed.");
3920
+ });
3921
+ 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 }) => {
3922
+ const cost = computeCostFromDb(db, model, input_tokens ?? 0, output_tokens ?? 0, 0, 0, 0);
3923
+ return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
3924
+ });
3925
+ server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
3926
+ const goals = getGoalStatuses(db);
3927
+ if (goals.length === 0)
3928
+ return text("No goals set.");
3929
+ const lines = ["period scope limit spent used% status"];
3930
+ for (const goal of goals) {
3931
+ const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
3932
+ const pct = Number(goal["percent_used"]).toFixed(1);
3933
+ const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
3934
+ 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}`);
3935
+ }
3936
+ return text(lines.join(`
3860
3937
  `));
3861
- });
3862
- server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
3863
- period: z.enum(["day", "week", "month", "year"]),
3864
- limit_usd: z.number().positive(),
3865
- project_path: z.string().optional(),
3866
- agent: z.enum(AGENTS).optional()
3867
- }, async ({ period, limit_usd, project_path, agent }) => {
3868
- const now = new Date().toISOString();
3869
- upsertGoal(db, {
3870
- id: randomUUID(),
3871
- period,
3872
- project_path: project_path ?? null,
3873
- agent: agent ?? null,
3874
- limit_usd,
3875
- created_at: now,
3876
- updated_at: now
3877
3938
  });
3878
- return text(`Goal set: ${period} $${limit_usd}`);
3879
- });
3880
- server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
3881
- deleteGoal(db, id);
3882
- return text("Goal removed.");
3883
- });
3884
- server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
3885
- const machines = listMachines(db);
3886
- if (machines.length === 0)
3887
- return text(`No machine data yet. Current machine: ${getMachineId()}`);
3888
- const lines = ["machine sessions requests cost last_active"];
3889
- for (const m of machines) {
3890
- 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"}`);
3891
- }
3892
- lines.push(`
3939
+ server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
3940
+ period: z.enum(["day", "week", "month", "year"]),
3941
+ limit_usd: z.number().positive(),
3942
+ project_path: z.string().optional(),
3943
+ agent: z.string().optional()
3944
+ }, async ({ period, limit_usd, project_path, agent }) => {
3945
+ const now = new Date().toISOString();
3946
+ upsertGoal(db, {
3947
+ id: randomUUID(),
3948
+ period,
3949
+ project_path: project_path ?? null,
3950
+ agent: agent ?? null,
3951
+ limit_usd,
3952
+ created_at: now,
3953
+ updated_at: now
3954
+ });
3955
+ return text(`Goal set: ${period} $${limit_usd}`);
3956
+ });
3957
+ server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
3958
+ deleteGoal(db, id);
3959
+ return text("Goal removed.");
3960
+ });
3961
+ server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
3962
+ const machines = listMachines(db);
3963
+ if (machines.length === 0)
3964
+ return text(`No machine data yet. Current machine: ${getMachineId()}`);
3965
+ const lines = ["machine sessions requests cost last_active"];
3966
+ for (const m of machines) {
3967
+ 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"}`);
3968
+ }
3969
+ lines.push(`
3893
3970
  current machine: ${getMachineId()}`);
3894
- return text(lines.join(`
3971
+ return text(lines.join(`
3895
3972
  `));
3896
- });
3897
- server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
3898
- const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
3899
- if (existing) {
3900
- existing.last_seen_at = new Date().toISOString();
3901
- return text(JSON.stringify(existing));
3902
- }
3903
- const id = Math.random().toString(36).slice(2, 10);
3904
- const agent = { id, name, last_seen_at: new Date().toISOString() };
3905
- _econAgents.set(id, agent);
3906
- return text(JSON.stringify(agent));
3907
- });
3908
- server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
3909
- const agent = _econAgents.get(agent_id);
3910
- if (!agent)
3911
- return textError("Agent not found");
3912
- agent.last_seen_at = new Date().toISOString();
3913
- return text(`\u2665 ${agent.name}`);
3914
- });
3915
- server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
3916
- const agent = _econAgents.get(agent_id);
3917
- if (!agent)
3918
- return textError("Agent not found");
3919
- agent.project_id = project_id ?? undefined;
3920
- return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
3921
- });
3922
- server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
3923
- server.tool("send_feedback", "Send feedback about this service.", {
3924
- message: z.string(),
3925
- email: z.string().optional(),
3926
- category: z.enum(["bug", "feature", "general"]).optional()
3927
- }, async ({ message, email, category }) => {
3928
- try {
3929
- db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(message, email ?? null, category ?? "general", packageMetadata.version);
3930
- return text("Feedback saved. Thank you!");
3931
- } catch (error) {
3932
- return textError(String(error));
3973
+ });
3974
+ server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
3975
+ const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
3976
+ if (existing) {
3977
+ existing.last_seen_at = new Date().toISOString();
3978
+ return text(JSON.stringify(existing));
3979
+ }
3980
+ const id = Math.random().toString(36).slice(2, 10);
3981
+ const agent = { id, name, last_seen_at: new Date().toISOString() };
3982
+ _econAgents.set(id, agent);
3983
+ return text(JSON.stringify(agent));
3984
+ });
3985
+ server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
3986
+ const agent = _econAgents.get(agent_id);
3987
+ if (!agent)
3988
+ return textError("Agent not found");
3989
+ agent.last_seen_at = new Date().toISOString();
3990
+ return text(`\u2665 ${agent.name}`);
3991
+ });
3992
+ server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
3993
+ const agent = _econAgents.get(agent_id);
3994
+ if (!agent)
3995
+ return textError("Agent not found");
3996
+ agent.project_id = project_id ?? undefined;
3997
+ return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
3998
+ });
3999
+ server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
4000
+ server.tool("send_feedback", "Send feedback about this service.", {
4001
+ message: z.string(),
4002
+ email: z.string().optional(),
4003
+ category: z.enum(["bug", "feature", "general"]).optional()
4004
+ }, async ({ message, email, category }) => {
4005
+ try {
4006
+ db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(message, email ?? null, category ?? "general", packageMetadata.version);
4007
+ return text("Feedback saved. Thank you!");
4008
+ } catch (error) {
4009
+ return textError(String(error));
4010
+ }
4011
+ });
4012
+ registerCloudTools(server, MCP_NAME, {
4013
+ dbPath: getDbPath(),
4014
+ migrations: PG_MIGRATIONS
4015
+ });
4016
+ return server;
4017
+ }
4018
+
4019
+ // src/mcp/http.ts
4020
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
4021
+ function isHttpMode(argv = process.argv.slice(2)) {
4022
+ return argv.includes("--http") || process.env["MCP_HTTP"] === "1";
4023
+ }
4024
+ function isStdioMode(argv = process.argv.slice(2)) {
4025
+ return argv.includes("--stdio") || process.env["MCP_STDIO"] === "1";
4026
+ }
4027
+ function resolveHttpPort(argv = process.argv.slice(2)) {
4028
+ for (let i = 0;i < argv.length; i++) {
4029
+ const arg = argv[i];
4030
+ if (arg === "--port" || arg === "-p") {
4031
+ const raw = argv[i + 1];
4032
+ if (!raw)
4033
+ throw new Error(`Invalid port: ${raw ?? ""}`);
4034
+ return parsePort(raw, "port");
4035
+ }
3933
4036
  }
4037
+ const fromEnv = process.env["MCP_HTTP_PORT"];
4038
+ if (fromEnv)
4039
+ return parsePort(fromEnv, "MCP_HTTP_PORT");
4040
+ return DEFAULT_MCP_HTTP_PORT;
4041
+ }
4042
+ function parsePort(raw, label) {
4043
+ const value = Number(raw);
4044
+ if (!Number.isInteger(value) || value < 1 || value > 65535) {
4045
+ throw new Error(`Invalid ${label}: ${raw}`);
4046
+ }
4047
+ return value;
4048
+ }
4049
+ async function handleMcpHttpRequest(req) {
4050
+ const url = new URL(req.url);
4051
+ if (url.pathname === "/health" && req.method === "GET") {
4052
+ return Response.json({ status: "ok", name: MCP_NAME });
4053
+ }
4054
+ if (url.pathname === "/mcp") {
4055
+ const transport = new WebStandardStreamableHTTPServerTransport({
4056
+ sessionIdGenerator: undefined
4057
+ });
4058
+ const server = buildServer();
4059
+ await server.connect(transport);
4060
+ return transport.handleRequest(req);
4061
+ }
4062
+ return new Response("Not Found", { status: 404 });
4063
+ }
4064
+ function startHttpServer(options = {}) {
4065
+ const port = options.port ?? DEFAULT_MCP_HTTP_PORT;
4066
+ const hostname2 = options.hostname ?? "127.0.0.1";
4067
+ const log = options.log ?? console.error;
4068
+ const server = Bun.serve({
4069
+ port,
4070
+ hostname: hostname2,
4071
+ fetch: handleMcpHttpRequest
4072
+ });
4073
+ const address = `http://${hostname2}:${server.port}`;
4074
+ log(`${MCP_NAME}-mcp HTTP listening on ${address}/mcp (health: ${address}/health)`);
4075
+ return server;
4076
+ }
4077
+
4078
+ // src/mcp/index.ts
4079
+ function printHelp() {
4080
+ console.log(`Usage: economy-mcp [options]
4081
+
4082
+ Runs the ${packageMetadata.name} MCP server (stdio by default).
4083
+
4084
+ Options:
4085
+ --http Serve MCP over Streamable HTTP on 127.0.0.1
4086
+ -p, --port <port> HTTP port (default: MCP_HTTP_PORT or 8815)
4087
+ -V, --version output the version number
4088
+ -h, --help display help for command
4089
+
4090
+ Environment:
4091
+ MCP_HTTP=1 Enable HTTP mode
4092
+ MCP_HTTP_PORT Override default HTTP port (8815)`);
4093
+ }
4094
+ var args = process.argv.slice(2);
4095
+ if (args.includes("--help") || args.includes("-h")) {
4096
+ printHelp();
4097
+ process.exit(0);
4098
+ }
4099
+ if (args.includes("--version") || args.includes("-V")) {
4100
+ console.log(packageMetadata.version);
4101
+ process.exit(0);
4102
+ }
4103
+ async function main() {
4104
+ if (isStdioMode(args) || !isHttpMode(args)) {
4105
+ const server = buildServer();
4106
+ const transport = new StdioServerTransport;
4107
+ await server.connect(transport);
4108
+ return;
4109
+ }
4110
+ startHttpServer({ port: resolveHttpPort(args) });
4111
+ await new Promise(() => {});
4112
+ }
4113
+ main().catch((error) => {
4114
+ console.error("MCP server error:", error);
4115
+ process.exit(1);
3934
4116
  });
3935
- var transport = new StdioServerTransport;
3936
- registerCloudTools(server, "economy", {
3937
- dbPath: getDbPath(),
3938
- migrations: PG_MIGRATIONS
3939
- });
3940
- await server.connect(transport);