@hasna/economy 0.2.21 → 0.2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -2
- package/README.md +33 -20
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/index.js +580 -88
- package/dist/db/database.d.ts +4 -2
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/index.js +401 -34
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/cursor.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts.map +1 -1
- package/dist/ingest/opencode.d.ts.map +1 -1
- package/dist/ingest/pi.d.ts.map +1 -1
- package/dist/lib/accounts.d.ts +11 -0
- package/dist/lib/accounts.d.ts.map +1 -0
- package/dist/lib/cloud-sync.d.ts.map +1 -1
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/mcp/index.js +964 -582
- package/dist/otel/index.js +77 -26
- package/dist/server/index.js +526 -89
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +43 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +9 -4
- package/dist/mcp/http.d.ts +0 -13
- package/dist/mcp/http.d.ts.map +0 -1
- package/dist/mcp/server.d.ts +0 -4
- package/dist/mcp/server.d.ts.map +0 -1
package/dist/server/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
|
|
@@ -1157,6 +1364,12 @@ function upsertSubscription(db, sub) {
|
|
|
1157
1364
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1158
1365
|
`).run(sub.id, sub.agent, sub.provider, sub.plan, sub.monthly_fee_usd, sub.included_usage_usd, sub.billing_cycle_start, sub.reset_policy, sub.active, sub.created_at, sub.updated_at);
|
|
1159
1366
|
}
|
|
1367
|
+
function listSubscriptions(db) {
|
|
1368
|
+
return db.prepare(`SELECT * FROM subscriptions ORDER BY provider, plan`).all();
|
|
1369
|
+
}
|
|
1370
|
+
function deleteSubscription(db, id) {
|
|
1371
|
+
db.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(id);
|
|
1372
|
+
}
|
|
1160
1373
|
function upsertUsageSnapshot(db, snap) {
|
|
1161
1374
|
const now = snap.updated_at ?? new Date().toISOString();
|
|
1162
1375
|
const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
|
|
@@ -1228,7 +1441,12 @@ var init_pg_migrations = __esm(() => {
|
|
|
1228
1441
|
duration_ms INTEGER DEFAULT 0,
|
|
1229
1442
|
timestamp TEXT NOT NULL,
|
|
1230
1443
|
source_request_id TEXT,
|
|
1231
|
-
machine_id TEXT DEFAULT ''
|
|
1444
|
+
machine_id TEXT DEFAULT '',
|
|
1445
|
+
account_key TEXT DEFAULT '',
|
|
1446
|
+
account_tool TEXT DEFAULT '',
|
|
1447
|
+
account_name TEXT DEFAULT '',
|
|
1448
|
+
account_email TEXT DEFAULT '',
|
|
1449
|
+
account_source TEXT DEFAULT ''
|
|
1232
1450
|
)`,
|
|
1233
1451
|
`CREATE TABLE IF NOT EXISTS sessions (
|
|
1234
1452
|
id TEXT PRIMARY KEY,
|
|
@@ -1240,7 +1458,12 @@ var init_pg_migrations = __esm(() => {
|
|
|
1240
1458
|
total_cost_usd REAL DEFAULT 0,
|
|
1241
1459
|
total_tokens INTEGER DEFAULT 0,
|
|
1242
1460
|
request_count INTEGER DEFAULT 0,
|
|
1243
|
-
machine_id TEXT DEFAULT ''
|
|
1461
|
+
machine_id TEXT DEFAULT '',
|
|
1462
|
+
account_key TEXT DEFAULT '',
|
|
1463
|
+
account_tool TEXT DEFAULT '',
|
|
1464
|
+
account_name TEXT DEFAULT '',
|
|
1465
|
+
account_email TEXT DEFAULT '',
|
|
1466
|
+
account_source TEXT DEFAULT ''
|
|
1244
1467
|
)`,
|
|
1245
1468
|
`CREATE TABLE IF NOT EXISTS projects (
|
|
1246
1469
|
id TEXT PRIMARY KEY,
|
|
@@ -1361,13 +1584,25 @@ var init_pg_migrations = __esm(() => {
|
|
|
1361
1584
|
)`,
|
|
1362
1585
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
|
|
1363
1586
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1587
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
|
|
1588
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
|
|
1589
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
|
|
1590
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
|
|
1591
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
1364
1592
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1365
1593
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1366
1594
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1595
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
|
|
1596
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
|
|
1597
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
|
|
1598
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
|
|
1599
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
1367
1600
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1368
1601
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1369
1602
|
`CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
|
|
1370
|
-
`CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)
|
|
1603
|
+
`CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
|
|
1604
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
|
|
1605
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`
|
|
1371
1606
|
];
|
|
1372
1607
|
});
|
|
1373
1608
|
|
|
@@ -1378,7 +1613,7 @@ __export(exports_billing, {
|
|
|
1378
1613
|
syncGeminiBilling: () => syncGeminiBilling,
|
|
1379
1614
|
syncAnthropicBilling: () => syncAnthropicBilling
|
|
1380
1615
|
});
|
|
1381
|
-
import { readFileSync as
|
|
1616
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
1382
1617
|
function getAnthropicAdminKey() {
|
|
1383
1618
|
return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
|
|
1384
1619
|
}
|
|
@@ -1572,7 +1807,7 @@ async function syncGeminiBilling(db, opts = {}) {
|
|
|
1572
1807
|
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
1573
1808
|
const fromDateStr = toISODate(start);
|
|
1574
1809
|
const toDateStr = toISODate(end);
|
|
1575
|
-
const rows = parseBillingRows(
|
|
1810
|
+
const rows = parseBillingRows(readFileSync9(exportPath, "utf-8"));
|
|
1576
1811
|
clearBillingRange(db, "gemini", fromDateStr, toDateStr);
|
|
1577
1812
|
const updatedAt = new Date().toISOString();
|
|
1578
1813
|
let totalUsd = 0;
|
|
@@ -1634,7 +1869,7 @@ var init_open_projects = __esm(() => {
|
|
|
1634
1869
|
});
|
|
1635
1870
|
|
|
1636
1871
|
// src/lib/config.ts
|
|
1637
|
-
import { existsSync as existsSync10, readFileSync as
|
|
1872
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
1638
1873
|
import { dirname, join as join9 } from "path";
|
|
1639
1874
|
function getConfigPath() {
|
|
1640
1875
|
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join9(getDataDir(), "config.json");
|
|
@@ -1643,7 +1878,7 @@ function loadConfig() {
|
|
|
1643
1878
|
try {
|
|
1644
1879
|
const configPath = getConfigPath();
|
|
1645
1880
|
if (existsSync10(configPath)) {
|
|
1646
|
-
const raw =
|
|
1881
|
+
const raw = readFileSync10(configPath, "utf-8");
|
|
1647
1882
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
1648
1883
|
}
|
|
1649
1884
|
} catch {}
|
|
@@ -1809,7 +2044,6 @@ function periodWhere2(period, column) {
|
|
|
1809
2044
|
}
|
|
1810
2045
|
function prorateMonthlyFee(monthlyFee, period) {
|
|
1811
2046
|
const now = new Date;
|
|
1812
|
-
const dayOfMonth = now.getDate();
|
|
1813
2047
|
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1814
2048
|
switch (period) {
|
|
1815
2049
|
case "today":
|
|
@@ -1891,6 +2125,131 @@ function defaultCostBasisForAgent(agent) {
|
|
|
1891
2125
|
return "estimated";
|
|
1892
2126
|
}
|
|
1893
2127
|
|
|
2128
|
+
// src/lib/accounts.ts
|
|
2129
|
+
var AGENT_ACCOUNT_TOOLS = {
|
|
2130
|
+
claude: ["claude"],
|
|
2131
|
+
takumi: ["takumi", "claude"],
|
|
2132
|
+
codex: ["codex"],
|
|
2133
|
+
gemini: ["gemini"],
|
|
2134
|
+
opencode: ["opencode"],
|
|
2135
|
+
cursor: ["cursor"],
|
|
2136
|
+
pi: ["pi"],
|
|
2137
|
+
hermes: ["hermes"]
|
|
2138
|
+
};
|
|
2139
|
+
function accountKey(tool, name) {
|
|
2140
|
+
return `${tool}:${name}`;
|
|
2141
|
+
}
|
|
2142
|
+
function normalizeDir(value) {
|
|
2143
|
+
return value.replace(/\/+$/, "");
|
|
2144
|
+
}
|
|
2145
|
+
function fromProfile(profile, source) {
|
|
2146
|
+
return {
|
|
2147
|
+
account_key: accountKey(profile.tool, profile.name),
|
|
2148
|
+
account_tool: profile.tool,
|
|
2149
|
+
account_name: profile.name,
|
|
2150
|
+
...profile.email ? { account_email: profile.email } : {},
|
|
2151
|
+
account_source: source
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
function fromOverride(raw, agent) {
|
|
2155
|
+
const value = raw.trim();
|
|
2156
|
+
if (!value)
|
|
2157
|
+
return null;
|
|
2158
|
+
const candidateTool = AGENT_ACCOUNT_TOOLS[agent][0] ?? agent;
|
|
2159
|
+
const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
|
|
2160
|
+
if (!tool || !name)
|
|
2161
|
+
return null;
|
|
2162
|
+
return {
|
|
2163
|
+
account_key: accountKey(tool, name),
|
|
2164
|
+
account_tool: tool,
|
|
2165
|
+
account_name: name,
|
|
2166
|
+
account_source: "override"
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
function envOverride(agent, env) {
|
|
2170
|
+
const agentPrefix = agent.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
2171
|
+
const raw = env[`ECONOMY_${agentPrefix}_ACCOUNT_KEY`] ?? env[`ECONOMY_${agentPrefix}_ACCOUNT`] ?? env["ECONOMY_ACCOUNT_KEY"] ?? env["ECONOMY_ACCOUNT"];
|
|
2172
|
+
if (raw)
|
|
2173
|
+
return fromOverride(raw, agent);
|
|
2174
|
+
const tool = env[`ECONOMY_${agentPrefix}_ACCOUNT_TOOL`] ?? env["ECONOMY_ACCOUNT_TOOL"];
|
|
2175
|
+
const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
|
|
2176
|
+
if (!tool || !name)
|
|
2177
|
+
return null;
|
|
2178
|
+
return {
|
|
2179
|
+
account_key: accountKey(tool, name),
|
|
2180
|
+
account_tool: tool,
|
|
2181
|
+
account_name: name,
|
|
2182
|
+
account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
|
|
2183
|
+
account_source: "override"
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
function knownToolIds(api) {
|
|
2187
|
+
try {
|
|
2188
|
+
return new Set(api.listTools().map((tool) => tool.id));
|
|
2189
|
+
} catch {
|
|
2190
|
+
return new Set;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
function profileForEnvDir(api, tool, env) {
|
|
2194
|
+
const configuredDir = env[tool.envVar];
|
|
2195
|
+
if (!configuredDir)
|
|
2196
|
+
return null;
|
|
2197
|
+
const normalized = normalizeDir(configuredDir);
|
|
2198
|
+
try {
|
|
2199
|
+
return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
2200
|
+
} catch {
|
|
2201
|
+
return null;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
async function resolveAccountForAgent(agent, env = process.env) {
|
|
2205
|
+
const override = envOverride(agent, env);
|
|
2206
|
+
if (override)
|
|
2207
|
+
return override;
|
|
2208
|
+
let api;
|
|
2209
|
+
try {
|
|
2210
|
+
api = await import("@hasna/accounts");
|
|
2211
|
+
} catch {
|
|
2212
|
+
return null;
|
|
2213
|
+
}
|
|
2214
|
+
const toolIds = knownToolIds(api);
|
|
2215
|
+
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
2216
|
+
if (!toolIds.has(toolId))
|
|
2217
|
+
continue;
|
|
2218
|
+
let tool;
|
|
2219
|
+
try {
|
|
2220
|
+
tool = api.getTool(toolId);
|
|
2221
|
+
} catch {
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
const envProfile = profileForEnvDir(api, tool, env);
|
|
2225
|
+
if (envProfile)
|
|
2226
|
+
return fromProfile(envProfile, "env");
|
|
2227
|
+
try {
|
|
2228
|
+
const applied = api.appliedProfile(toolId);
|
|
2229
|
+
if (applied)
|
|
2230
|
+
return fromProfile(applied, "applied");
|
|
2231
|
+
} catch {}
|
|
2232
|
+
try {
|
|
2233
|
+
const current = api.currentProfile(toolId);
|
|
2234
|
+
if (current)
|
|
2235
|
+
return fromProfile(current, "current");
|
|
2236
|
+
} catch {}
|
|
2237
|
+
}
|
|
2238
|
+
return null;
|
|
2239
|
+
}
|
|
2240
|
+
function withAccount(record, account) {
|
|
2241
|
+
if (!account)
|
|
2242
|
+
return record;
|
|
2243
|
+
return {
|
|
2244
|
+
...record,
|
|
2245
|
+
account_key: account.account_key,
|
|
2246
|
+
account_tool: account.account_tool,
|
|
2247
|
+
account_name: account.account_name,
|
|
2248
|
+
account_email: account.account_email ?? "",
|
|
2249
|
+
account_source: account.account_source
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
|
|
1894
2253
|
// src/ingest/claude.ts
|
|
1895
2254
|
function autoDetectProject(cwd, projects) {
|
|
1896
2255
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
@@ -1932,6 +2291,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
1932
2291
|
let totalRequests = 0;
|
|
1933
2292
|
const touchedSessions = new Set;
|
|
1934
2293
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
2294
|
+
const account = await resolveAccountForAgent(agentName);
|
|
1935
2295
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
1936
2296
|
for (const projectDirEntry of projectDirs) {
|
|
1937
2297
|
const projectDirPath = join2(projectsDir, projectDirEntry.name);
|
|
@@ -1995,7 +2355,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
1995
2355
|
}
|
|
1996
2356
|
const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
|
|
1997
2357
|
const reqId = `${agentName}-${sourceRequestId}`;
|
|
1998
|
-
upsertRequest(db, {
|
|
2358
|
+
upsertRequest(db, withAccount({
|
|
1999
2359
|
id: reqId,
|
|
2000
2360
|
agent: agentName,
|
|
2001
2361
|
session_id: sessionId,
|
|
@@ -2012,7 +2372,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
2012
2372
|
timestamp,
|
|
2013
2373
|
source_request_id: sourceRequestId,
|
|
2014
2374
|
machine_id: machineId
|
|
2015
|
-
});
|
|
2375
|
+
}, account));
|
|
2016
2376
|
if (!touchedSessions.has(sessionId)) {
|
|
2017
2377
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
2018
2378
|
if (!existing) {
|
|
@@ -2030,7 +2390,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
2030
2390
|
request_count: 0,
|
|
2031
2391
|
machine_id: machineId
|
|
2032
2392
|
};
|
|
2033
|
-
upsertSession(db, session);
|
|
2393
|
+
upsertSession(db, withAccount(session, account));
|
|
2034
2394
|
}
|
|
2035
2395
|
touchedSessions.add(sessionId);
|
|
2036
2396
|
}
|
|
@@ -2076,7 +2436,7 @@ import { join as join3, basename as basename2 } from "path";
|
|
|
2076
2436
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
2077
2437
|
var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
2078
2438
|
var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
2079
|
-
var CODEX_INGEST_VERSION = "rollout-
|
|
2439
|
+
var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
2080
2440
|
function codexDbPath() {
|
|
2081
2441
|
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
2082
2442
|
}
|
|
@@ -2109,8 +2469,9 @@ function buildThreadQuery(codexDb) {
|
|
|
2109
2469
|
function readTokenEvents(rolloutPath) {
|
|
2110
2470
|
if (!rolloutPath || !existsSync3(rolloutPath))
|
|
2111
2471
|
return [];
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2472
|
+
const fallbackUsages = new Map;
|
|
2473
|
+
let fallbackTimestamp;
|
|
2474
|
+
let aggregate = null;
|
|
2114
2475
|
for (const line of readFileSync2(rolloutPath, "utf-8").split(`
|
|
2115
2476
|
`)) {
|
|
2116
2477
|
if (!line.trim())
|
|
@@ -2127,20 +2488,48 @@ function readTokenEvents(rolloutPath) {
|
|
|
2127
2488
|
if (!payload || payload["type"] !== "token_count")
|
|
2128
2489
|
continue;
|
|
2129
2490
|
const info = payload["info"];
|
|
2491
|
+
const timestamp = entry["timestamp"];
|
|
2492
|
+
const entryTimestamp = typeof timestamp === "string" ? timestamp : undefined;
|
|
2493
|
+
const totalUsage = info?.["total_token_usage"];
|
|
2494
|
+
if (totalUsage && tokenTotal(totalUsage) > 0) {
|
|
2495
|
+
aggregate = { usage: totalUsage, timestamp: entryTimestamp };
|
|
2496
|
+
continue;
|
|
2497
|
+
}
|
|
2130
2498
|
const usage = info?.["last_token_usage"];
|
|
2131
2499
|
if (!usage)
|
|
2132
2500
|
continue;
|
|
2133
|
-
|
|
2134
|
-
if (total <= 0)
|
|
2501
|
+
if (tokenTotal(usage) <= 0)
|
|
2135
2502
|
continue;
|
|
2136
2503
|
const key = JSON.stringify(usage);
|
|
2137
|
-
if (
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
const timestamp = entry["timestamp"];
|
|
2141
|
-
events.push({ usage, timestamp: typeof timestamp === "string" ? timestamp : undefined });
|
|
2504
|
+
if (!fallbackUsages.has(key))
|
|
2505
|
+
fallbackUsages.set(key, usage);
|
|
2506
|
+
fallbackTimestamp = entryTimestamp ?? fallbackTimestamp;
|
|
2142
2507
|
}
|
|
2143
|
-
|
|
2508
|
+
if (aggregate)
|
|
2509
|
+
return [aggregate];
|
|
2510
|
+
if (fallbackUsages.size === 0)
|
|
2511
|
+
return [];
|
|
2512
|
+
return [{ usage: sumTokenUsages([...fallbackUsages.values()]), timestamp: fallbackTimestamp }];
|
|
2513
|
+
}
|
|
2514
|
+
function tokenTotal(usage) {
|
|
2515
|
+
return usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
2516
|
+
}
|
|
2517
|
+
function sumTokenUsages(usages) {
|
|
2518
|
+
const result = {
|
|
2519
|
+
input_tokens: 0,
|
|
2520
|
+
cached_input_tokens: 0,
|
|
2521
|
+
output_tokens: 0,
|
|
2522
|
+
reasoning_output_tokens: 0,
|
|
2523
|
+
total_tokens: 0
|
|
2524
|
+
};
|
|
2525
|
+
for (const usage of usages) {
|
|
2526
|
+
result.input_tokens = (result.input_tokens ?? 0) + (usage.input_tokens ?? 0);
|
|
2527
|
+
result.cached_input_tokens = (result.cached_input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
|
|
2528
|
+
result.output_tokens = (result.output_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
2529
|
+
result.reasoning_output_tokens = (result.reasoning_output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0);
|
|
2530
|
+
result.total_tokens = (result.total_tokens ?? 0) + tokenTotal(usage);
|
|
2531
|
+
}
|
|
2532
|
+
return result;
|
|
2144
2533
|
}
|
|
2145
2534
|
function fallbackEvents(totalTokens) {
|
|
2146
2535
|
const inputTokens = Math.floor(totalTokens * 0.6);
|
|
@@ -2164,6 +2553,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2164
2553
|
let codexDb = null;
|
|
2165
2554
|
let ingested = 0;
|
|
2166
2555
|
let requests = 0;
|
|
2556
|
+
const account = await resolveAccountForAgent("codex");
|
|
2167
2557
|
try {
|
|
2168
2558
|
codexDb = new BunDatabase(dbPath, { readonly: true });
|
|
2169
2559
|
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
@@ -2178,7 +2568,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2178
2568
|
const sessionId = `codex-${thread.id}`;
|
|
2179
2569
|
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
2180
2570
|
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
2181
|
-
upsertSession(db, {
|
|
2571
|
+
upsertSession(db, withAccount({
|
|
2182
2572
|
id: sessionId,
|
|
2183
2573
|
agent: "codex",
|
|
2184
2574
|
project_path: projectPath,
|
|
@@ -2189,9 +2579,10 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2189
2579
|
total_tokens: 0,
|
|
2190
2580
|
request_count: 0,
|
|
2191
2581
|
machine_id: machineId
|
|
2192
|
-
});
|
|
2582
|
+
}, account));
|
|
2193
2583
|
const events = readTokenEvents(thread.rollout_path);
|
|
2194
2584
|
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2585
|
+
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2195
2586
|
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2196
2587
|
tokenEvents.forEach((event, index) => {
|
|
2197
2588
|
const usage = event.usage;
|
|
@@ -2202,7 +2593,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2202
2593
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2203
2594
|
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2204
2595
|
const requestId = `${sessionId}-${index}`;
|
|
2205
|
-
upsertRequest(db, {
|
|
2596
|
+
upsertRequest(db, withAccount({
|
|
2206
2597
|
id: requestId,
|
|
2207
2598
|
agent: "codex",
|
|
2208
2599
|
session_id: sessionId,
|
|
@@ -2217,14 +2608,14 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2217
2608
|
timestamp,
|
|
2218
2609
|
source_request_id: requestId,
|
|
2219
2610
|
machine_id: machineId
|
|
2220
|
-
});
|
|
2611
|
+
}, account));
|
|
2221
2612
|
requests++;
|
|
2222
2613
|
});
|
|
2223
2614
|
rollupSession(db, sessionId);
|
|
2224
2615
|
setIngestState(db, "codex", thread.id, stateValue);
|
|
2225
2616
|
ingested++;
|
|
2226
2617
|
if (verbose)
|
|
2227
|
-
console.log(`Codex session ${thread.id}: ${
|
|
2618
|
+
console.log(`Codex session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
|
|
2228
2619
|
}
|
|
2229
2620
|
} finally {
|
|
2230
2621
|
codexDb?.close();
|
|
@@ -2291,6 +2682,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2291
2682
|
let totalSessions = 0;
|
|
2292
2683
|
let totalRequests = 0;
|
|
2293
2684
|
const touchedSessions = new Set;
|
|
2685
|
+
const account = await resolveAccountForAgent("gemini");
|
|
2294
2686
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
2295
2687
|
for (const projectDir of projectDirs) {
|
|
2296
2688
|
const chatsDir = join4(projectDir, "chats");
|
|
@@ -2339,7 +2731,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2339
2731
|
request_count: 0,
|
|
2340
2732
|
machine_id: machineId
|
|
2341
2733
|
};
|
|
2342
|
-
upsertSession(db, session);
|
|
2734
|
+
upsertSession(db, withAccount(session, account));
|
|
2343
2735
|
totalSessions++;
|
|
2344
2736
|
}
|
|
2345
2737
|
touchedSessions.add(sessionId);
|
|
@@ -2363,7 +2755,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2363
2755
|
const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
|
|
2364
2756
|
const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
|
|
2365
2757
|
const requestId = `gemini-${sessionId}-${message.id ?? index}`;
|
|
2366
|
-
upsertRequest(db, {
|
|
2758
|
+
upsertRequest(db, withAccount({
|
|
2367
2759
|
id: requestId,
|
|
2368
2760
|
agent: "gemini",
|
|
2369
2761
|
session_id: sessionId,
|
|
@@ -2378,7 +2770,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2378
2770
|
timestamp,
|
|
2379
2771
|
source_request_id: message.id ?? requestId,
|
|
2380
2772
|
machine_id: machineId
|
|
2381
|
-
});
|
|
2773
|
+
}, account));
|
|
2382
2774
|
totalRequests++;
|
|
2383
2775
|
}
|
|
2384
2776
|
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
@@ -2427,6 +2819,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
2427
2819
|
const touched = new Set;
|
|
2428
2820
|
const machineId = getMachineId();
|
|
2429
2821
|
const now = new Date().toISOString();
|
|
2822
|
+
const account = await resolveAccountForAgent("opencode");
|
|
2430
2823
|
for (const file of files) {
|
|
2431
2824
|
const mtime = statSync4(file).mtimeMs;
|
|
2432
2825
|
const stateKey = file;
|
|
@@ -2456,7 +2849,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
2456
2849
|
const sourceId = file.replace(OPENCODE_STORAGE, "");
|
|
2457
2850
|
const reqId = `opencode-${sourceId}`;
|
|
2458
2851
|
const costUsd = usage.cost ?? computeCostFromDb(db, model, input, output, cacheRead, cacheWrite, 0);
|
|
2459
|
-
upsertRequest(db, {
|
|
2852
|
+
upsertRequest(db, withAccount({
|
|
2460
2853
|
id: reqId,
|
|
2461
2854
|
agent: "opencode",
|
|
2462
2855
|
session_id: sessionId,
|
|
@@ -2472,10 +2865,10 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
2472
2865
|
source_request_id: sourceId,
|
|
2473
2866
|
machine_id: machineId,
|
|
2474
2867
|
updated_at: now
|
|
2475
|
-
});
|
|
2868
|
+
}, account));
|
|
2476
2869
|
requests++;
|
|
2477
2870
|
if (!touched.has(sessionId)) {
|
|
2478
|
-
upsertSession(db, {
|
|
2871
|
+
upsertSession(db, withAccount({
|
|
2479
2872
|
id: sessionId,
|
|
2480
2873
|
agent: "opencode",
|
|
2481
2874
|
project_path: "",
|
|
@@ -2487,7 +2880,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
2487
2880
|
request_count: 0,
|
|
2488
2881
|
machine_id: machineId,
|
|
2489
2882
|
updated_at: now
|
|
2490
|
-
});
|
|
2883
|
+
}, account));
|
|
2491
2884
|
touched.add(sessionId);
|
|
2492
2885
|
}
|
|
2493
2886
|
setIngestState(db, "opencode", stateKey, String(mtime));
|
|
@@ -2534,6 +2927,7 @@ async function ingestCursor(db, verbose = false) {
|
|
|
2534
2927
|
const machineId = getMachineId();
|
|
2535
2928
|
const now = new Date().toISOString();
|
|
2536
2929
|
let snapshots = 0;
|
|
2930
|
+
const account = await resolveAccountForAgent("cursor");
|
|
2537
2931
|
const usage = await cursorFetch("/api/usage", token);
|
|
2538
2932
|
if (usage?.premiumRequests != null && usage.maxPremiumRequests) {
|
|
2539
2933
|
upsertUsageSnapshot(db, {
|
|
@@ -2581,7 +2975,7 @@ async function ingestCursor(db, verbose = false) {
|
|
|
2581
2975
|
}
|
|
2582
2976
|
const sessionId = `cursor-${today}-${machineId}`;
|
|
2583
2977
|
if (onDemand + included > 0) {
|
|
2584
|
-
upsertSession(db, {
|
|
2978
|
+
upsertSession(db, withAccount({
|
|
2585
2979
|
id: sessionId,
|
|
2586
2980
|
agent: "cursor",
|
|
2587
2981
|
project_path: "",
|
|
@@ -2593,8 +2987,8 @@ async function ingestCursor(db, verbose = false) {
|
|
|
2593
2987
|
request_count: 1,
|
|
2594
2988
|
machine_id: machineId,
|
|
2595
2989
|
updated_at: now
|
|
2596
|
-
});
|
|
2597
|
-
upsertRequest(db, {
|
|
2990
|
+
}, account));
|
|
2991
|
+
upsertRequest(db, withAccount({
|
|
2598
2992
|
id: `cursor-${today}-${machineId}-usage`,
|
|
2599
2993
|
agent: "cursor",
|
|
2600
2994
|
session_id: sessionId,
|
|
@@ -2610,7 +3004,7 @@ async function ingestCursor(db, verbose = false) {
|
|
|
2610
3004
|
source_request_id: today,
|
|
2611
3005
|
machine_id: machineId,
|
|
2612
3006
|
updated_at: now
|
|
2613
|
-
});
|
|
3007
|
+
}, account));
|
|
2614
3008
|
rollupSession(db, sessionId);
|
|
2615
3009
|
}
|
|
2616
3010
|
setIngestState(db, "cursor", `sync-${today}`, now);
|
|
@@ -2643,6 +3037,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
2643
3037
|
const touched = new Set;
|
|
2644
3038
|
const machineId = getMachineId();
|
|
2645
3039
|
const now = new Date().toISOString();
|
|
3040
|
+
const account = await resolveAccountForAgent("pi");
|
|
2646
3041
|
for (const file of files) {
|
|
2647
3042
|
const mtime = statSync5(file).mtimeMs;
|
|
2648
3043
|
const prev = getIngestState(db, "pi", file);
|
|
@@ -2668,7 +3063,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
2668
3063
|
const model = turn.model ?? turn.provider ?? "unknown";
|
|
2669
3064
|
const timestamp = turn.timestamp ?? new Date(statSync5(file).mtime).toISOString();
|
|
2670
3065
|
const reqId = `pi-${sessionId}-${i}`;
|
|
2671
|
-
upsertRequest(db, {
|
|
3066
|
+
upsertRequest(db, withAccount({
|
|
2672
3067
|
id: reqId,
|
|
2673
3068
|
agent: "pi",
|
|
2674
3069
|
session_id: sessionId,
|
|
@@ -2684,11 +3079,11 @@ async function ingestPi(db, verbose = false) {
|
|
|
2684
3079
|
source_request_id: `${sessionId}-${i}`,
|
|
2685
3080
|
machine_id: machineId,
|
|
2686
3081
|
updated_at: now
|
|
2687
|
-
});
|
|
3082
|
+
}, account));
|
|
2688
3083
|
requests++;
|
|
2689
3084
|
}
|
|
2690
3085
|
if (turns.length > 0) {
|
|
2691
|
-
upsertSession(db, {
|
|
3086
|
+
upsertSession(db, withAccount({
|
|
2692
3087
|
id: sessionId,
|
|
2693
3088
|
agent: "pi",
|
|
2694
3089
|
project_path: "",
|
|
@@ -2700,7 +3095,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
2700
3095
|
request_count: 0,
|
|
2701
3096
|
machine_id: machineId,
|
|
2702
3097
|
updated_at: now
|
|
2703
|
-
});
|
|
3098
|
+
}, account));
|
|
2704
3099
|
touched.add(sessionId);
|
|
2705
3100
|
}
|
|
2706
3101
|
setIngestState(db, "pi", file, String(mtime));
|
|
@@ -2748,13 +3143,14 @@ async function ingestHermes(db, verbose = false) {
|
|
|
2748
3143
|
const machineId = getMachineId();
|
|
2749
3144
|
const now = new Date().toISOString();
|
|
2750
3145
|
let requests = 0;
|
|
3146
|
+
const account = await resolveAccountForAgent("hermes");
|
|
2751
3147
|
for (const row of rows) {
|
|
2752
3148
|
const sessionId = `hermes-${row.id}`;
|
|
2753
3149
|
const startedAt = new Date(row.started_at * 1000).toISOString();
|
|
2754
3150
|
const endedAt = row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null;
|
|
2755
3151
|
const cost = row.actual_cost_usd ?? row.estimated_cost_usd ?? 0;
|
|
2756
3152
|
const tokens = row.input_tokens + row.output_tokens + row.cache_read_tokens + row.cache_write_tokens + row.reasoning_tokens;
|
|
2757
|
-
upsertSession(db, {
|
|
3153
|
+
upsertSession(db, withAccount({
|
|
2758
3154
|
id: sessionId,
|
|
2759
3155
|
agent: "hermes",
|
|
2760
3156
|
project_path: row.source ?? "",
|
|
@@ -2766,9 +3162,9 @@ async function ingestHermes(db, verbose = false) {
|
|
|
2766
3162
|
request_count: 1,
|
|
2767
3163
|
machine_id: machineId,
|
|
2768
3164
|
updated_at: now
|
|
2769
|
-
});
|
|
3165
|
+
}, account));
|
|
2770
3166
|
const reqId = `hermes-${row.id}-rollup`;
|
|
2771
|
-
upsertRequest(db, {
|
|
3167
|
+
upsertRequest(db, withAccount({
|
|
2772
3168
|
id: reqId,
|
|
2773
3169
|
agent: "hermes",
|
|
2774
3170
|
session_id: sessionId,
|
|
@@ -2784,7 +3180,7 @@ async function ingestHermes(db, verbose = false) {
|
|
|
2784
3180
|
source_request_id: row.id,
|
|
2785
3181
|
machine_id: machineId,
|
|
2786
3182
|
updated_at: now
|
|
2787
|
-
});
|
|
3183
|
+
}, account));
|
|
2788
3184
|
requests++;
|
|
2789
3185
|
rollupSession(db, sessionId);
|
|
2790
3186
|
if (verbose)
|
|
@@ -2804,7 +3200,7 @@ function statSyncSafe(path) {
|
|
|
2804
3200
|
|
|
2805
3201
|
// src/ingest/claude-quota.ts
|
|
2806
3202
|
init_database();
|
|
2807
|
-
import { existsSync as existsSync8, readFileSync as
|
|
3203
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
2808
3204
|
|
|
2809
3205
|
// src/lib/paths.ts
|
|
2810
3206
|
import { homedir as homedir8 } from "os";
|
|
@@ -2842,7 +3238,7 @@ function readClaudeToken() {
|
|
|
2842
3238
|
if (!existsSync8(CREDENTIALS_PATH))
|
|
2843
3239
|
return null;
|
|
2844
3240
|
try {
|
|
2845
|
-
const creds = JSON.parse(
|
|
3241
|
+
const creds = JSON.parse(readFileSync6(CREDENTIALS_PATH, "utf-8"));
|
|
2846
3242
|
const oauth = creds.claudeAiOauth;
|
|
2847
3243
|
if (!oauth?.accessToken)
|
|
2848
3244
|
return null;
|
|
@@ -2978,7 +3374,7 @@ async function ingestClaudeQuota(db, verbose = false) {
|
|
|
2978
3374
|
|
|
2979
3375
|
// src/ingest/codex-quota.ts
|
|
2980
3376
|
init_database();
|
|
2981
|
-
import { existsSync as existsSync9, readFileSync as
|
|
3377
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
2982
3378
|
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
2983
3379
|
function readCodexAuth() {
|
|
2984
3380
|
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
@@ -2988,7 +3384,7 @@ function readCodexAuth() {
|
|
|
2988
3384
|
if (!existsSync9(authPath))
|
|
2989
3385
|
return null;
|
|
2990
3386
|
try {
|
|
2991
|
-
const auth = JSON.parse(
|
|
3387
|
+
const auth = JSON.parse(readFileSync7(authPath, "utf-8"));
|
|
2992
3388
|
const token = auth.tokens?.access_token;
|
|
2993
3389
|
if (!token)
|
|
2994
3390
|
return null;
|
|
@@ -3122,12 +3518,12 @@ init_database();
|
|
|
3122
3518
|
init_database();
|
|
3123
3519
|
|
|
3124
3520
|
// src/lib/package-metadata.ts
|
|
3125
|
-
import { readFileSync as
|
|
3521
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
3126
3522
|
var cachedMetadata = null;
|
|
3127
3523
|
function getPackageMetadata() {
|
|
3128
3524
|
if (cachedMetadata)
|
|
3129
3525
|
return cachedMetadata;
|
|
3130
|
-
const raw =
|
|
3526
|
+
const raw = readFileSync8(new URL("../../package.json", import.meta.url), "utf8");
|
|
3131
3527
|
const parsed = JSON.parse(raw);
|
|
3132
3528
|
cachedMetadata = {
|
|
3133
3529
|
name: parsed.name ?? "@hasna/economy",
|
|
@@ -3179,44 +3575,27 @@ async function runCloudMigrations(cloud) {
|
|
|
3179
3575
|
await cloud.run(sql);
|
|
3180
3576
|
}
|
|
3181
3577
|
}
|
|
3182
|
-
function isCloudIncrementalEnabled() {
|
|
3183
|
-
return process.env["ECONOMY_CLOUD_INCREMENTAL"] === "1" || process.env["ECONOMY_CLOUD_INCREMENTAL"] === "true";
|
|
3184
|
-
}
|
|
3185
3578
|
async function cloudPush(opts) {
|
|
3186
|
-
const { syncPush,
|
|
3579
|
+
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
3187
3580
|
const cloud = await getCloudPg();
|
|
3188
3581
|
const local = new SqliteAdapter(getDbPath());
|
|
3189
3582
|
await runCloudMigrations(cloud);
|
|
3190
3583
|
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
ensureSyncMetaTable(local);
|
|
3194
|
-
const results = incrementalSyncPush(local, cloud, tables, { conflictColumn: "updated_at" });
|
|
3195
|
-
rows = results.reduce((s, r) => s + r.synced_rows, 0);
|
|
3196
|
-
} else {
|
|
3197
|
-
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
3198
|
-
rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3199
|
-
}
|
|
3584
|
+
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
3585
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3200
3586
|
touchMachineRegistry(local, "push");
|
|
3201
3587
|
local.close();
|
|
3202
3588
|
await cloud.close();
|
|
3203
3589
|
return { rows, machine: getMachineId() };
|
|
3204
3590
|
}
|
|
3205
3591
|
async function cloudPull(opts) {
|
|
3206
|
-
const { syncPull,
|
|
3592
|
+
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
3207
3593
|
const cloud = await getCloudPg();
|
|
3208
3594
|
const local = new SqliteAdapter(getDbPath());
|
|
3209
3595
|
await runCloudMigrations(cloud);
|
|
3210
3596
|
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
ensureSyncMetaTable(local);
|
|
3214
|
-
const results = incrementalSyncPull(cloud, local, tables, { conflictColumn: "updated_at" });
|
|
3215
|
-
rows = results.reduce((s, r) => s + r.synced_rows, 0);
|
|
3216
|
-
} else {
|
|
3217
|
-
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
3218
|
-
rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3219
|
-
}
|
|
3597
|
+
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
3598
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3220
3599
|
touchMachineRegistry(local, "pull");
|
|
3221
3600
|
local.close();
|
|
3222
3601
|
await cloud.close();
|
|
@@ -3526,6 +3905,7 @@ function createHandler(db) {
|
|
|
3526
3905
|
const project = url.searchParams.get("project") ?? undefined;
|
|
3527
3906
|
const search = url.searchParams.get("search") ?? undefined;
|
|
3528
3907
|
const machine = url.searchParams.get("machine") ?? undefined;
|
|
3908
|
+
const account = url.searchParams.get("account") ?? undefined;
|
|
3529
3909
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
3530
3910
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
3531
3911
|
const since = url.searchParams.get("since") ?? undefined;
|
|
@@ -3536,6 +3916,7 @@ function createHandler(db) {
|
|
|
3536
3916
|
project,
|
|
3537
3917
|
search,
|
|
3538
3918
|
machine,
|
|
3919
|
+
account,
|
|
3539
3920
|
limit,
|
|
3540
3921
|
offset,
|
|
3541
3922
|
since
|
|
@@ -3586,11 +3967,23 @@ function createHandler(db) {
|
|
|
3586
3967
|
return ok(results);
|
|
3587
3968
|
}
|
|
3588
3969
|
if (path === "/api/projects" && method === "GET") {
|
|
3589
|
-
|
|
3970
|
+
const period = url.searchParams.get("period") ?? "all";
|
|
3971
|
+
return ok(queryProjectBreakdown(db, period));
|
|
3972
|
+
}
|
|
3973
|
+
if (path === "/api/accounts" && method === "GET") {
|
|
3974
|
+
const period = url.searchParams.get("period") ?? "all";
|
|
3975
|
+
return ok(queryAccountBreakdown(db, period));
|
|
3590
3976
|
}
|
|
3591
3977
|
if (path === "/api/breakdown" && method === "GET") {
|
|
3592
3978
|
const by = url.searchParams.get("by") ?? "model";
|
|
3593
|
-
|
|
3979
|
+
const period = url.searchParams.get("period") ?? "all";
|
|
3980
|
+
if (by === "project")
|
|
3981
|
+
return ok(queryProjectBreakdown(db, period));
|
|
3982
|
+
if (by === "agent")
|
|
3983
|
+
return ok(queryAgentBreakdown(db, period));
|
|
3984
|
+
if (by === "account")
|
|
3985
|
+
return ok(queryAccountBreakdown(db, period));
|
|
3986
|
+
return ok(queryModelBreakdown(db));
|
|
3594
3987
|
}
|
|
3595
3988
|
if (path === "/api/budgets" && method === "GET") {
|
|
3596
3989
|
return ok(getBudgetStatuses(db));
|
|
@@ -3725,6 +4118,50 @@ function createHandler(db) {
|
|
|
3725
4118
|
const agent = url.searchParams.get("agent") ?? undefined;
|
|
3726
4119
|
return ok(querySavingsSummary(db, period, agent && isAgent(agent) ? agent : undefined));
|
|
3727
4120
|
}
|
|
4121
|
+
if (path === "/api/subscriptions" && method === "GET") {
|
|
4122
|
+
return ok(listSubscriptions(db));
|
|
4123
|
+
}
|
|
4124
|
+
if (path === "/api/subscriptions" && method === "POST") {
|
|
4125
|
+
const body = await jsonBody(req);
|
|
4126
|
+
if (!body)
|
|
4127
|
+
return err("invalid JSON body");
|
|
4128
|
+
const provider = optionalString(body["provider"])?.trim();
|
|
4129
|
+
const plan = optionalString(body["plan"])?.trim();
|
|
4130
|
+
if (!provider)
|
|
4131
|
+
return err("provider is required");
|
|
4132
|
+
if (!plan)
|
|
4133
|
+
return err("plan is required");
|
|
4134
|
+
const monthlyFee = finiteNumber(body["monthly_fee_usd"] ?? body["fee_usd"] ?? 0);
|
|
4135
|
+
const includedUsage = finiteNumber(body["included_usage_usd"] ?? 0);
|
|
4136
|
+
if (monthlyFee == null || monthlyFee < 0)
|
|
4137
|
+
return err("monthly_fee_usd must be a non-negative number");
|
|
4138
|
+
if (includedUsage == null || includedUsage < 0)
|
|
4139
|
+
return err("included_usage_usd must be a non-negative number");
|
|
4140
|
+
const agent = optionalAgent(body["agent"]);
|
|
4141
|
+
if (agent === undefined)
|
|
4142
|
+
return err(AGENT_ERROR);
|
|
4143
|
+
const now = new Date().toISOString();
|
|
4144
|
+
const subscription = {
|
|
4145
|
+
id: optionalString(body["id"])?.trim() || randomUUID(),
|
|
4146
|
+
agent,
|
|
4147
|
+
provider,
|
|
4148
|
+
plan,
|
|
4149
|
+
monthly_fee_usd: monthlyFee,
|
|
4150
|
+
included_usage_usd: includedUsage,
|
|
4151
|
+
billing_cycle_start: optionalString(body["billing_cycle_start"]),
|
|
4152
|
+
reset_policy: optionalString(body["reset_policy"]) ?? "monthly",
|
|
4153
|
+
active: body["active"] === false || body["active"] === 0 ? 0 : 1,
|
|
4154
|
+
created_at: optionalString(body["created_at"]) ?? now,
|
|
4155
|
+
updated_at: now
|
|
4156
|
+
};
|
|
4157
|
+
upsertSubscription(db, subscription);
|
|
4158
|
+
return ok(subscription);
|
|
4159
|
+
}
|
|
4160
|
+
const subscriptionMatch = path.match(/^\/api\/subscriptions\/(.+)$/);
|
|
4161
|
+
if (subscriptionMatch && method === "DELETE") {
|
|
4162
|
+
deleteSubscription(db, decodeURIComponent(subscriptionMatch[1]));
|
|
4163
|
+
return ok({ ok: true });
|
|
4164
|
+
}
|
|
3728
4165
|
const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
|
|
3729
4166
|
if (sessionRequestsMatch && method === "GET") {
|
|
3730
4167
|
const sessionId = decodeURIComponent(sessionRequestsMatch[1]);
|