@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/LICENSE +2 -1
- package/README.md +16 -1
- package/dist/cli/index.js +417 -189
- package/dist/db/database.d.ts +10 -5
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +268 -162
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/mcp/http.d.ts +13 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/index.js +846 -670
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/otel/index.js +35 -43
- package/dist/server/index.js +284 -170
- package/dist/server/serve.d.ts.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
774
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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(
|
|
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
|
-
|
|
1356
|
-
|
|
1357
|
-
COALESCE((
|
|
1358
|
-
COALESCE(SUM(
|
|
1359
|
-
MAX(
|
|
1360
|
-
FROM
|
|
1361
|
-
|
|
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
|
|
1832
|
-
return
|
|
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:
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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/
|
|
3444
|
-
init_database();
|
|
3540
|
+
// src/mcp/server.ts
|
|
3445
3541
|
init_pricing();
|
|
3446
3542
|
init_pricing();
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
}
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
}
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
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
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
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
|
-
|
|
3606
|
-
|
|
3607
|
-
}, async ({ n, agent }) => {
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
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
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
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
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
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
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
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
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
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
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
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
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
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
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
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
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
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
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
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
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
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
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
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
|
-
|
|
3792
|
-
});
|
|
3793
|
-
server.tool("list_subscriptions", "List configured subscription plans
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
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",
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
}, async (input) => {
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
}
|
|
3833
|
-
|
|
3834
|
-
return text("
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
});
|
|
3840
|
-
server.tool("
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
return text(
|
|
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
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
})
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
});
|
|
3908
|
-
server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
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
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
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
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
}, async ({ message, email, category }) => {
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
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);
|