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