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