@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/cli/index.js
CHANGED
|
@@ -536,6 +536,8 @@ __export(exports_database, {
|
|
|
536
536
|
queryModelBreakdown: () => queryModelBreakdown,
|
|
537
537
|
queryDailyBreakdown: () => queryDailyBreakdown,
|
|
538
538
|
queryBillingSummary: () => queryBillingSummary,
|
|
539
|
+
queryAgentBreakdown: () => queryAgentBreakdown,
|
|
540
|
+
queryAccountBreakdown: () => queryAccountBreakdown,
|
|
539
541
|
openDatabase: () => openDatabase,
|
|
540
542
|
listSubscriptions: () => listSubscriptions,
|
|
541
543
|
listProjects: () => listProjects,
|
|
@@ -630,7 +632,12 @@ function initSchema(db) {
|
|
|
630
632
|
duration_ms INTEGER DEFAULT 0,
|
|
631
633
|
timestamp TEXT NOT NULL,
|
|
632
634
|
source_request_id TEXT,
|
|
633
|
-
machine_id TEXT DEFAULT ''
|
|
635
|
+
machine_id TEXT DEFAULT '',
|
|
636
|
+
account_key TEXT DEFAULT '',
|
|
637
|
+
account_tool TEXT DEFAULT '',
|
|
638
|
+
account_name TEXT DEFAULT '',
|
|
639
|
+
account_email TEXT DEFAULT '',
|
|
640
|
+
account_source TEXT DEFAULT ''
|
|
634
641
|
);
|
|
635
642
|
|
|
636
643
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -643,7 +650,12 @@ function initSchema(db) {
|
|
|
643
650
|
total_cost_usd REAL DEFAULT 0,
|
|
644
651
|
total_tokens INTEGER DEFAULT 0,
|
|
645
652
|
request_count INTEGER DEFAULT 0,
|
|
646
|
-
machine_id TEXT DEFAULT ''
|
|
653
|
+
machine_id TEXT DEFAULT '',
|
|
654
|
+
account_key TEXT DEFAULT '',
|
|
655
|
+
account_tool TEXT DEFAULT '',
|
|
656
|
+
account_name TEXT DEFAULT '',
|
|
657
|
+
account_email TEXT DEFAULT '',
|
|
658
|
+
account_source TEXT DEFAULT ''
|
|
647
659
|
);
|
|
648
660
|
|
|
649
661
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -798,6 +810,11 @@ function initSchema(db) {
|
|
|
798
810
|
if (!cols.some((c) => c.name === "synced_at")) {
|
|
799
811
|
db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
800
812
|
}
|
|
813
|
+
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
814
|
+
if (!cols.some((c) => c.name === column)) {
|
|
815
|
+
db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
801
818
|
const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
802
819
|
if (!sessionCols.some((c) => c.name === "attribution_tag")) {
|
|
803
820
|
db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
@@ -809,6 +826,11 @@ function initSchema(db) {
|
|
|
809
826
|
if (!sessionCols.some((c) => c.name === "synced_at")) {
|
|
810
827
|
db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
811
828
|
}
|
|
829
|
+
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
830
|
+
if (!sessionCols.some((c) => c.name === column)) {
|
|
831
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
812
834
|
const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
|
|
813
835
|
if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
|
|
814
836
|
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
|
|
@@ -819,6 +841,8 @@ function initSchema(db) {
|
|
|
819
841
|
db.exec(`
|
|
820
842
|
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
821
843
|
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
844
|
+
CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
|
|
845
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
|
|
822
846
|
`);
|
|
823
847
|
}
|
|
824
848
|
function periodWhere(period) {
|
|
@@ -853,6 +877,22 @@ function sessionPeriodWhere(period) {
|
|
|
853
877
|
return "1=1";
|
|
854
878
|
}
|
|
855
879
|
}
|
|
880
|
+
function requestPeriodWhere(period) {
|
|
881
|
+
switch (period) {
|
|
882
|
+
case "today":
|
|
883
|
+
return `DATE(timestamp) = DATE('now')`;
|
|
884
|
+
case "yesterday":
|
|
885
|
+
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
886
|
+
case "week":
|
|
887
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
888
|
+
case "month":
|
|
889
|
+
return `timestamp >= DATE('now', 'start of month')`;
|
|
890
|
+
case "year":
|
|
891
|
+
return `timestamp >= DATE('now', 'start of year')`;
|
|
892
|
+
case "all":
|
|
893
|
+
return "1=1";
|
|
894
|
+
}
|
|
895
|
+
}
|
|
856
896
|
function upsertRequest(db, req) {
|
|
857
897
|
const now = req.updated_at ?? new Date().toISOString();
|
|
858
898
|
db.prepare(`
|
|
@@ -860,18 +900,20 @@ function upsertRequest(db, req) {
|
|
|
860
900
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
861
901
|
cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
|
|
862
902
|
cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
|
|
863
|
-
source_request_id, machine_id, attribution_tag,
|
|
864
|
-
|
|
865
|
-
|
|
903
|
+
source_request_id, machine_id, attribution_tag, account_key, account_tool,
|
|
904
|
+
account_name, account_email, account_source, updated_at, synced_at)
|
|
905
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
906
|
+
`).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 ?? "");
|
|
866
907
|
}
|
|
867
908
|
function upsertSession(db, session) {
|
|
868
909
|
const now = session.updated_at ?? new Date().toISOString();
|
|
869
910
|
db.prepare(`
|
|
870
911
|
INSERT OR REPLACE INTO sessions
|
|
871
912
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
872
|
-
total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
|
|
873
|
-
|
|
874
|
-
|
|
913
|
+
total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
|
|
914
|
+
account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
915
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
916
|
+
`).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 ?? "");
|
|
875
917
|
}
|
|
876
918
|
function rollupSession(db, sessionId) {
|
|
877
919
|
db.prepare(`
|
|
@@ -882,9 +924,24 @@ function rollupSession(db, sessionId) {
|
|
|
882
924
|
ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
|
|
883
925
|
started_at = CASE WHEN started_at = '' OR started_at IS NULL
|
|
884
926
|
THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
|
|
885
|
-
ELSE started_at END
|
|
927
|
+
ELSE started_at END,
|
|
928
|
+
account_key = CASE WHEN account_key = '' OR account_key IS NULL
|
|
929
|
+
THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
930
|
+
ELSE account_key END,
|
|
931
|
+
account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
|
|
932
|
+
THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
933
|
+
ELSE account_tool END,
|
|
934
|
+
account_name = CASE WHEN account_name = '' OR account_name IS NULL
|
|
935
|
+
THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
936
|
+
ELSE account_name END,
|
|
937
|
+
account_email = CASE WHEN account_email = '' OR account_email IS NULL
|
|
938
|
+
THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
939
|
+
ELSE account_email END,
|
|
940
|
+
account_source = CASE WHEN account_source = '' OR account_source IS NULL
|
|
941
|
+
THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
942
|
+
ELSE account_source END
|
|
886
943
|
WHERE id = ?
|
|
887
|
-
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
944
|
+
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
888
945
|
}
|
|
889
946
|
function querySessions(db, filter = {}) {
|
|
890
947
|
const conditions = [];
|
|
@@ -897,6 +954,11 @@ function querySessions(db, filter = {}) {
|
|
|
897
954
|
conditions.push("project_path LIKE ?");
|
|
898
955
|
params.push(`%${filter.project}%`);
|
|
899
956
|
}
|
|
957
|
+
if (filter.account) {
|
|
958
|
+
const q = `%${filter.account}%`;
|
|
959
|
+
conditions.push("(account_key LIKE ? OR account_name LIKE ? OR account_email LIKE ?)");
|
|
960
|
+
params.push(q, q, q);
|
|
961
|
+
}
|
|
900
962
|
if (filter.since) {
|
|
901
963
|
conditions.push("started_at >= ?");
|
|
902
964
|
params.push(filter.since);
|
|
@@ -961,6 +1023,70 @@ function queryModelBreakdown(db) {
|
|
|
961
1023
|
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
962
1024
|
`).all();
|
|
963
1025
|
}
|
|
1026
|
+
function queryAgentBreakdown(db, period = "all") {
|
|
1027
|
+
const requestWhere = requestPeriodWhere(period);
|
|
1028
|
+
const groups = new Map;
|
|
1029
|
+
const requestRows = db.prepare(`
|
|
1030
|
+
SELECT agent,
|
|
1031
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
1032
|
+
COUNT(*) as requests,
|
|
1033
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1034
|
+
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
1035
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
1036
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1037
|
+
COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
|
|
1038
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
|
|
1039
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
1040
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1041
|
+
MAX(timestamp) as last_active
|
|
1042
|
+
FROM requests
|
|
1043
|
+
WHERE ${requestWhere}
|
|
1044
|
+
GROUP BY agent
|
|
1045
|
+
ORDER BY api_equivalent_usd DESC
|
|
1046
|
+
`).all();
|
|
1047
|
+
for (const row of requestRows) {
|
|
1048
|
+
groups.set(row.agent, row);
|
|
1049
|
+
}
|
|
1050
|
+
const sessionWhere = sessionPeriodWhere(period);
|
|
1051
|
+
const sessionOnlyRows = db.prepare(`
|
|
1052
|
+
SELECT agent,
|
|
1053
|
+
COUNT(*) as sessions,
|
|
1054
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
1055
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1056
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1057
|
+
MAX(started_at) as last_active
|
|
1058
|
+
FROM sessions
|
|
1059
|
+
WHERE ${sessionWhere}
|
|
1060
|
+
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1061
|
+
GROUP BY agent
|
|
1062
|
+
`).all();
|
|
1063
|
+
for (const row of sessionOnlyRows) {
|
|
1064
|
+
const existing = groups.get(row.agent) ?? {
|
|
1065
|
+
agent: row.agent,
|
|
1066
|
+
sessions: 0,
|
|
1067
|
+
requests: 0,
|
|
1068
|
+
total_tokens: 0,
|
|
1069
|
+
api_equivalent_usd: 0,
|
|
1070
|
+
billable_usd: 0,
|
|
1071
|
+
metered_api_usd: 0,
|
|
1072
|
+
subscription_included_usd: 0,
|
|
1073
|
+
estimated_usd: 0,
|
|
1074
|
+
unknown_usd: 0,
|
|
1075
|
+
cost_usd: 0,
|
|
1076
|
+
last_active: ""
|
|
1077
|
+
};
|
|
1078
|
+
existing.sessions += row.sessions;
|
|
1079
|
+
existing.requests += row.requests;
|
|
1080
|
+
existing.total_tokens += row.total_tokens;
|
|
1081
|
+
existing.api_equivalent_usd += row.cost_usd;
|
|
1082
|
+
existing.estimated_usd += row.cost_usd;
|
|
1083
|
+
existing.cost_usd += row.cost_usd;
|
|
1084
|
+
if (!existing.last_active || row.last_active > existing.last_active)
|
|
1085
|
+
existing.last_active = row.last_active;
|
|
1086
|
+
groups.set(row.agent, existing);
|
|
1087
|
+
}
|
|
1088
|
+
return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
|
|
1089
|
+
}
|
|
964
1090
|
function labelForPath(projectPath, projectName) {
|
|
965
1091
|
if (projectName && projectName.trim() !== "")
|
|
966
1092
|
return projectName;
|
|
@@ -979,11 +1105,13 @@ function labelForPath(projectPath, projectName) {
|
|
|
979
1105
|
}
|
|
980
1106
|
return segments[segments.length - 1] ?? projectPath;
|
|
981
1107
|
}
|
|
982
|
-
function queryProjectBreakdown(db) {
|
|
1108
|
+
function queryProjectBreakdown(db, period = "all") {
|
|
1109
|
+
const where = sessionPeriodWhere(period);
|
|
983
1110
|
const sessions = db.prepare(`
|
|
984
1111
|
SELECT id, project_path, project_name, total_cost_usd, started_at
|
|
985
1112
|
FROM sessions
|
|
986
|
-
WHERE
|
|
1113
|
+
WHERE ${where}
|
|
1114
|
+
AND (project_path != '' OR project_name != '')
|
|
987
1115
|
`).all();
|
|
988
1116
|
const groups = new Map;
|
|
989
1117
|
for (const s of sessions) {
|
|
@@ -1022,6 +1150,87 @@ function queryProjectBreakdown(db) {
|
|
|
1022
1150
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1023
1151
|
return result;
|
|
1024
1152
|
}
|
|
1153
|
+
function queryAccountBreakdown(db, period = "all") {
|
|
1154
|
+
const sWhere = sessionPeriodWhere(period);
|
|
1155
|
+
const sessions = db.prepare(`
|
|
1156
|
+
SELECT id, account_key, account_tool, account_name, account_email, account_source,
|
|
1157
|
+
total_cost_usd, total_tokens, request_count, started_at
|
|
1158
|
+
FROM sessions
|
|
1159
|
+
WHERE ${sWhere}
|
|
1160
|
+
AND (account_key != '' OR account_tool != '' OR account_name != '' OR account_email != '')
|
|
1161
|
+
`).all();
|
|
1162
|
+
const groups = new Map;
|
|
1163
|
+
for (const session of sessions) {
|
|
1164
|
+
const key = session.account_key || `${session.account_tool}:${session.account_name}`;
|
|
1165
|
+
if (!key || key === ":")
|
|
1166
|
+
continue;
|
|
1167
|
+
const group = groups.get(key) ?? {
|
|
1168
|
+
sessionIds: [],
|
|
1169
|
+
account_tool: session.account_tool,
|
|
1170
|
+
account_name: session.account_name,
|
|
1171
|
+
account_email: session.account_email || null,
|
|
1172
|
+
account_source: session.account_source || "unknown",
|
|
1173
|
+
totalCost: 0,
|
|
1174
|
+
totalTokens: 0,
|
|
1175
|
+
requests: 0,
|
|
1176
|
+
lastActive: ""
|
|
1177
|
+
};
|
|
1178
|
+
group.sessionIds.push(session.id);
|
|
1179
|
+
group.totalCost += session.total_cost_usd || 0;
|
|
1180
|
+
group.totalTokens += session.total_tokens || 0;
|
|
1181
|
+
group.requests += session.request_count || 0;
|
|
1182
|
+
if (!group.lastActive || session.started_at > group.lastActive)
|
|
1183
|
+
group.lastActive = session.started_at;
|
|
1184
|
+
groups.set(key, group);
|
|
1185
|
+
}
|
|
1186
|
+
const result = [];
|
|
1187
|
+
for (const [key, group] of groups.entries()) {
|
|
1188
|
+
const placeholders = group.sessionIds.map(() => "?").join(",");
|
|
1189
|
+
const reqStats = placeholders ? db.prepare(`
|
|
1190
|
+
SELECT
|
|
1191
|
+
COUNT(*) as requests,
|
|
1192
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1193
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1194
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
1195
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1196
|
+
COALESCE(SUM(CASE WHEN COALESCE(cost_basis, 'estimated') = 'estimated' THEN cost_usd ELSE 0 END), 0) as estimated_usd,
|
|
1197
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd
|
|
1198
|
+
FROM requests WHERE session_id IN (${placeholders})
|
|
1199
|
+
`).get(...group.sessionIds) : {
|
|
1200
|
+
requests: 0,
|
|
1201
|
+
cost_usd: 0,
|
|
1202
|
+
total_tokens: 0,
|
|
1203
|
+
metered_api_usd: 0,
|
|
1204
|
+
subscription_included_usd: 0,
|
|
1205
|
+
estimated_usd: 0,
|
|
1206
|
+
unknown_usd: 0
|
|
1207
|
+
};
|
|
1208
|
+
const hasRequestCosts = reqStats.requests > 0;
|
|
1209
|
+
const apiEquivalentUsd = hasRequestCosts ? reqStats.cost_usd : group.totalCost;
|
|
1210
|
+
const estimatedUsd = hasRequestCosts ? reqStats.estimated_usd : group.totalCost;
|
|
1211
|
+
const billableUsd = reqStats.metered_api_usd;
|
|
1212
|
+
result.push({
|
|
1213
|
+
account_key: key,
|
|
1214
|
+
account_tool: group.account_tool,
|
|
1215
|
+
account_name: group.account_name,
|
|
1216
|
+
account_email: group.account_email,
|
|
1217
|
+
account_source: group.account_source,
|
|
1218
|
+
sessions: group.sessionIds.length,
|
|
1219
|
+
requests: reqStats.requests || group.requests,
|
|
1220
|
+
total_tokens: reqStats.total_tokens || group.totalTokens,
|
|
1221
|
+
api_equivalent_usd: apiEquivalentUsd,
|
|
1222
|
+
billable_usd: billableUsd,
|
|
1223
|
+
metered_api_usd: reqStats.metered_api_usd,
|
|
1224
|
+
subscription_included_usd: reqStats.subscription_included_usd,
|
|
1225
|
+
estimated_usd: estimatedUsd,
|
|
1226
|
+
unknown_usd: reqStats.unknown_usd,
|
|
1227
|
+
cost_usd: apiEquivalentUsd,
|
|
1228
|
+
last_active: group.lastActive
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1232
|
+
return result;
|
|
1233
|
+
}
|
|
1025
1234
|
function queryDailyBreakdown(db, days = 30) {
|
|
1026
1235
|
return db.prepare(`
|
|
1027
1236
|
SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
@@ -1286,7 +1495,6 @@ function periodWhere2(period, column) {
|
|
|
1286
1495
|
}
|
|
1287
1496
|
function prorateMonthlyFee(monthlyFee, period) {
|
|
1288
1497
|
const now = new Date;
|
|
1289
|
-
const dayOfMonth = now.getDate();
|
|
1290
1498
|
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1291
1499
|
switch (period) {
|
|
1292
1500
|
case "today":
|
|
@@ -1473,7 +1681,12 @@ var init_pg_migrations = __esm(() => {
|
|
|
1473
1681
|
duration_ms INTEGER DEFAULT 0,
|
|
1474
1682
|
timestamp TEXT NOT NULL,
|
|
1475
1683
|
source_request_id TEXT,
|
|
1476
|
-
machine_id TEXT DEFAULT ''
|
|
1684
|
+
machine_id TEXT DEFAULT '',
|
|
1685
|
+
account_key TEXT DEFAULT '',
|
|
1686
|
+
account_tool TEXT DEFAULT '',
|
|
1687
|
+
account_name TEXT DEFAULT '',
|
|
1688
|
+
account_email TEXT DEFAULT '',
|
|
1689
|
+
account_source TEXT DEFAULT ''
|
|
1477
1690
|
)`,
|
|
1478
1691
|
`CREATE TABLE IF NOT EXISTS sessions (
|
|
1479
1692
|
id TEXT PRIMARY KEY,
|
|
@@ -1485,7 +1698,12 @@ var init_pg_migrations = __esm(() => {
|
|
|
1485
1698
|
total_cost_usd REAL DEFAULT 0,
|
|
1486
1699
|
total_tokens INTEGER DEFAULT 0,
|
|
1487
1700
|
request_count INTEGER DEFAULT 0,
|
|
1488
|
-
machine_id TEXT DEFAULT ''
|
|
1701
|
+
machine_id TEXT DEFAULT '',
|
|
1702
|
+
account_key TEXT DEFAULT '',
|
|
1703
|
+
account_tool TEXT DEFAULT '',
|
|
1704
|
+
account_name TEXT DEFAULT '',
|
|
1705
|
+
account_email TEXT DEFAULT '',
|
|
1706
|
+
account_source TEXT DEFAULT ''
|
|
1489
1707
|
)`,
|
|
1490
1708
|
`CREATE TABLE IF NOT EXISTS projects (
|
|
1491
1709
|
id TEXT PRIMARY KEY,
|
|
@@ -1606,13 +1824,25 @@ var init_pg_migrations = __esm(() => {
|
|
|
1606
1824
|
)`,
|
|
1607
1825
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
|
|
1608
1826
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1827
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
|
|
1828
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
|
|
1829
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
|
|
1830
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
|
|
1831
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
1609
1832
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1610
1833
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1611
1834
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1835
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
|
|
1836
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
|
|
1837
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
|
|
1838
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
|
|
1839
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
1612
1840
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1613
1841
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1614
1842
|
`CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
|
|
1615
|
-
`CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)
|
|
1843
|
+
`CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
|
|
1844
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
|
|
1845
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`
|
|
1616
1846
|
];
|
|
1617
1847
|
});
|
|
1618
1848
|
|
|
@@ -1644,44 +1874,27 @@ async function runCloudMigrations(cloud) {
|
|
|
1644
1874
|
await cloud.run(sql);
|
|
1645
1875
|
}
|
|
1646
1876
|
}
|
|
1647
|
-
function isCloudIncrementalEnabled() {
|
|
1648
|
-
return process.env["ECONOMY_CLOUD_INCREMENTAL"] === "1" || process.env["ECONOMY_CLOUD_INCREMENTAL"] === "true";
|
|
1649
|
-
}
|
|
1650
1877
|
async function cloudPush(opts) {
|
|
1651
|
-
const { syncPush,
|
|
1878
|
+
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
1652
1879
|
const cloud = await getCloudPg();
|
|
1653
1880
|
const local = new SqliteAdapter(getDbPath());
|
|
1654
1881
|
await runCloudMigrations(cloud);
|
|
1655
1882
|
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
ensureSyncMetaTable(local);
|
|
1659
|
-
const results = incrementalSyncPush(local, cloud, tables, { conflictColumn: "updated_at" });
|
|
1660
|
-
rows = results.reduce((s, r) => s + r.synced_rows, 0);
|
|
1661
|
-
} else {
|
|
1662
|
-
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
1663
|
-
rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
1664
|
-
}
|
|
1883
|
+
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
1884
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
1665
1885
|
touchMachineRegistry(local, "push");
|
|
1666
1886
|
local.close();
|
|
1667
1887
|
await cloud.close();
|
|
1668
1888
|
return { rows, machine: getMachineId() };
|
|
1669
1889
|
}
|
|
1670
1890
|
async function cloudPull(opts) {
|
|
1671
|
-
const { syncPull,
|
|
1891
|
+
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
1672
1892
|
const cloud = await getCloudPg();
|
|
1673
1893
|
const local = new SqliteAdapter(getDbPath());
|
|
1674
1894
|
await runCloudMigrations(cloud);
|
|
1675
1895
|
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
ensureSyncMetaTable(local);
|
|
1679
|
-
const results = incrementalSyncPull(cloud, local, tables, { conflictColumn: "updated_at" });
|
|
1680
|
-
rows = results.reduce((s, r) => s + r.synced_rows, 0);
|
|
1681
|
-
} else {
|
|
1682
|
-
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
1683
|
-
rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
1684
|
-
}
|
|
1896
|
+
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
1897
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
1685
1898
|
touchMachineRegistry(local, "pull");
|
|
1686
1899
|
local.close();
|
|
1687
1900
|
await cloud.close();
|
|
@@ -1970,6 +2183,134 @@ function agentPaths() {
|
|
|
1970
2183
|
}
|
|
1971
2184
|
var init_paths = () => {};
|
|
1972
2185
|
|
|
2186
|
+
// src/lib/accounts.ts
|
|
2187
|
+
function accountKey(tool, name) {
|
|
2188
|
+
return `${tool}:${name}`;
|
|
2189
|
+
}
|
|
2190
|
+
function normalizeDir(value) {
|
|
2191
|
+
return value.replace(/\/+$/, "");
|
|
2192
|
+
}
|
|
2193
|
+
function fromProfile(profile, source) {
|
|
2194
|
+
return {
|
|
2195
|
+
account_key: accountKey(profile.tool, profile.name),
|
|
2196
|
+
account_tool: profile.tool,
|
|
2197
|
+
account_name: profile.name,
|
|
2198
|
+
...profile.email ? { account_email: profile.email } : {},
|
|
2199
|
+
account_source: source
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
function fromOverride(raw, agent) {
|
|
2203
|
+
const value = raw.trim();
|
|
2204
|
+
if (!value)
|
|
2205
|
+
return null;
|
|
2206
|
+
const candidateTool = AGENT_ACCOUNT_TOOLS[agent][0] ?? agent;
|
|
2207
|
+
const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
|
|
2208
|
+
if (!tool || !name)
|
|
2209
|
+
return null;
|
|
2210
|
+
return {
|
|
2211
|
+
account_key: accountKey(tool, name),
|
|
2212
|
+
account_tool: tool,
|
|
2213
|
+
account_name: name,
|
|
2214
|
+
account_source: "override"
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
function envOverride(agent, env) {
|
|
2218
|
+
const agentPrefix = agent.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
2219
|
+
const raw = env[`ECONOMY_${agentPrefix}_ACCOUNT_KEY`] ?? env[`ECONOMY_${agentPrefix}_ACCOUNT`] ?? env["ECONOMY_ACCOUNT_KEY"] ?? env["ECONOMY_ACCOUNT"];
|
|
2220
|
+
if (raw)
|
|
2221
|
+
return fromOverride(raw, agent);
|
|
2222
|
+
const tool = env[`ECONOMY_${agentPrefix}_ACCOUNT_TOOL`] ?? env["ECONOMY_ACCOUNT_TOOL"];
|
|
2223
|
+
const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
|
|
2224
|
+
if (!tool || !name)
|
|
2225
|
+
return null;
|
|
2226
|
+
return {
|
|
2227
|
+
account_key: accountKey(tool, name),
|
|
2228
|
+
account_tool: tool,
|
|
2229
|
+
account_name: name,
|
|
2230
|
+
account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
|
|
2231
|
+
account_source: "override"
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
function knownToolIds(api) {
|
|
2235
|
+
try {
|
|
2236
|
+
return new Set(api.listTools().map((tool) => tool.id));
|
|
2237
|
+
} catch {
|
|
2238
|
+
return new Set;
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
function profileForEnvDir(api, tool, env) {
|
|
2242
|
+
const configuredDir = env[tool.envVar];
|
|
2243
|
+
if (!configuredDir)
|
|
2244
|
+
return null;
|
|
2245
|
+
const normalized = normalizeDir(configuredDir);
|
|
2246
|
+
try {
|
|
2247
|
+
return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
2248
|
+
} catch {
|
|
2249
|
+
return null;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
async function resolveAccountForAgent(agent, env = process.env) {
|
|
2253
|
+
const override = envOverride(agent, env);
|
|
2254
|
+
if (override)
|
|
2255
|
+
return override;
|
|
2256
|
+
let api;
|
|
2257
|
+
try {
|
|
2258
|
+
api = await import("@hasna/accounts");
|
|
2259
|
+
} catch {
|
|
2260
|
+
return null;
|
|
2261
|
+
}
|
|
2262
|
+
const toolIds = knownToolIds(api);
|
|
2263
|
+
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
2264
|
+
if (!toolIds.has(toolId))
|
|
2265
|
+
continue;
|
|
2266
|
+
let tool;
|
|
2267
|
+
try {
|
|
2268
|
+
tool = api.getTool(toolId);
|
|
2269
|
+
} catch {
|
|
2270
|
+
continue;
|
|
2271
|
+
}
|
|
2272
|
+
const envProfile = profileForEnvDir(api, tool, env);
|
|
2273
|
+
if (envProfile)
|
|
2274
|
+
return fromProfile(envProfile, "env");
|
|
2275
|
+
try {
|
|
2276
|
+
const applied = api.appliedProfile(toolId);
|
|
2277
|
+
if (applied)
|
|
2278
|
+
return fromProfile(applied, "applied");
|
|
2279
|
+
} catch {}
|
|
2280
|
+
try {
|
|
2281
|
+
const current = api.currentProfile(toolId);
|
|
2282
|
+
if (current)
|
|
2283
|
+
return fromProfile(current, "current");
|
|
2284
|
+
} catch {}
|
|
2285
|
+
}
|
|
2286
|
+
return null;
|
|
2287
|
+
}
|
|
2288
|
+
function withAccount(record, account) {
|
|
2289
|
+
if (!account)
|
|
2290
|
+
return record;
|
|
2291
|
+
return {
|
|
2292
|
+
...record,
|
|
2293
|
+
account_key: account.account_key,
|
|
2294
|
+
account_tool: account.account_tool,
|
|
2295
|
+
account_name: account.account_name,
|
|
2296
|
+
account_email: account.account_email ?? "",
|
|
2297
|
+
account_source: account.account_source
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
var AGENT_ACCOUNT_TOOLS;
|
|
2301
|
+
var init_accounts = __esm(() => {
|
|
2302
|
+
AGENT_ACCOUNT_TOOLS = {
|
|
2303
|
+
claude: ["claude"],
|
|
2304
|
+
takumi: ["takumi", "claude"],
|
|
2305
|
+
codex: ["codex"],
|
|
2306
|
+
gemini: ["gemini"],
|
|
2307
|
+
opencode: ["opencode"],
|
|
2308
|
+
cursor: ["cursor"],
|
|
2309
|
+
pi: ["pi"],
|
|
2310
|
+
hermes: ["hermes"]
|
|
2311
|
+
};
|
|
2312
|
+
});
|
|
2313
|
+
|
|
1973
2314
|
// src/ingest/claude.ts
|
|
1974
2315
|
import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
1975
2316
|
import { homedir as homedir3 } from "os";
|
|
@@ -2012,6 +2353,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
2012
2353
|
let totalRequests = 0;
|
|
2013
2354
|
const touchedSessions = new Set;
|
|
2014
2355
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
2356
|
+
const account = await resolveAccountForAgent(agentName);
|
|
2015
2357
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
2016
2358
|
for (const projectDirEntry of projectDirs) {
|
|
2017
2359
|
const projectDirPath = join6(projectsDir, projectDirEntry.name);
|
|
@@ -2075,7 +2417,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
2075
2417
|
}
|
|
2076
2418
|
const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
|
|
2077
2419
|
const reqId = `${agentName}-${sourceRequestId}`;
|
|
2078
|
-
upsertRequest(db, {
|
|
2420
|
+
upsertRequest(db, withAccount({
|
|
2079
2421
|
id: reqId,
|
|
2080
2422
|
agent: agentName,
|
|
2081
2423
|
session_id: sessionId,
|
|
@@ -2092,7 +2434,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
2092
2434
|
timestamp,
|
|
2093
2435
|
source_request_id: sourceRequestId,
|
|
2094
2436
|
machine_id: machineId
|
|
2095
|
-
});
|
|
2437
|
+
}, account));
|
|
2096
2438
|
if (!touchedSessions.has(sessionId)) {
|
|
2097
2439
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
2098
2440
|
if (!existing) {
|
|
@@ -2110,7 +2452,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
2110
2452
|
request_count: 0,
|
|
2111
2453
|
machine_id: machineId
|
|
2112
2454
|
};
|
|
2113
|
-
upsertSession(db, session);
|
|
2455
|
+
upsertSession(db, withAccount(session, account));
|
|
2114
2456
|
}
|
|
2115
2457
|
touchedSessions.add(sessionId);
|
|
2116
2458
|
}
|
|
@@ -2150,6 +2492,7 @@ var CLAUDE_PROJECTS_DIR, TAKUMI_PROJECTS_DIR;
|
|
|
2150
2492
|
var init_claude = __esm(() => {
|
|
2151
2493
|
init_database();
|
|
2152
2494
|
init_pricing();
|
|
2495
|
+
init_accounts();
|
|
2153
2496
|
CLAUDE_PROJECTS_DIR = join6(homedir3(), ".claude", "projects");
|
|
2154
2497
|
TAKUMI_PROJECTS_DIR = join6(homedir3(), ".takumi", "projects");
|
|
2155
2498
|
});
|
|
@@ -2191,8 +2534,9 @@ function buildThreadQuery(codexDb) {
|
|
|
2191
2534
|
function readTokenEvents(rolloutPath) {
|
|
2192
2535
|
if (!rolloutPath || !existsSync5(rolloutPath))
|
|
2193
2536
|
return [];
|
|
2194
|
-
const
|
|
2195
|
-
|
|
2537
|
+
const fallbackUsages = new Map;
|
|
2538
|
+
let fallbackTimestamp;
|
|
2539
|
+
let aggregate = null;
|
|
2196
2540
|
for (const line of readFileSync4(rolloutPath, "utf-8").split(`
|
|
2197
2541
|
`)) {
|
|
2198
2542
|
if (!line.trim())
|
|
@@ -2209,20 +2553,48 @@ function readTokenEvents(rolloutPath) {
|
|
|
2209
2553
|
if (!payload || payload["type"] !== "token_count")
|
|
2210
2554
|
continue;
|
|
2211
2555
|
const info = payload["info"];
|
|
2556
|
+
const timestamp = entry["timestamp"];
|
|
2557
|
+
const entryTimestamp = typeof timestamp === "string" ? timestamp : undefined;
|
|
2558
|
+
const totalUsage = info?.["total_token_usage"];
|
|
2559
|
+
if (totalUsage && tokenTotal(totalUsage) > 0) {
|
|
2560
|
+
aggregate = { usage: totalUsage, timestamp: entryTimestamp };
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2212
2563
|
const usage = info?.["last_token_usage"];
|
|
2213
2564
|
if (!usage)
|
|
2214
2565
|
continue;
|
|
2215
|
-
|
|
2216
|
-
if (total <= 0)
|
|
2566
|
+
if (tokenTotal(usage) <= 0)
|
|
2217
2567
|
continue;
|
|
2218
2568
|
const key = JSON.stringify(usage);
|
|
2219
|
-
if (
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
const timestamp = entry["timestamp"];
|
|
2223
|
-
events.push({ usage, timestamp: typeof timestamp === "string" ? timestamp : undefined });
|
|
2569
|
+
if (!fallbackUsages.has(key))
|
|
2570
|
+
fallbackUsages.set(key, usage);
|
|
2571
|
+
fallbackTimestamp = entryTimestamp ?? fallbackTimestamp;
|
|
2224
2572
|
}
|
|
2225
|
-
|
|
2573
|
+
if (aggregate)
|
|
2574
|
+
return [aggregate];
|
|
2575
|
+
if (fallbackUsages.size === 0)
|
|
2576
|
+
return [];
|
|
2577
|
+
return [{ usage: sumTokenUsages([...fallbackUsages.values()]), timestamp: fallbackTimestamp }];
|
|
2578
|
+
}
|
|
2579
|
+
function tokenTotal(usage) {
|
|
2580
|
+
return usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
2581
|
+
}
|
|
2582
|
+
function sumTokenUsages(usages) {
|
|
2583
|
+
const result = {
|
|
2584
|
+
input_tokens: 0,
|
|
2585
|
+
cached_input_tokens: 0,
|
|
2586
|
+
output_tokens: 0,
|
|
2587
|
+
reasoning_output_tokens: 0,
|
|
2588
|
+
total_tokens: 0
|
|
2589
|
+
};
|
|
2590
|
+
for (const usage of usages) {
|
|
2591
|
+
result.input_tokens = (result.input_tokens ?? 0) + (usage.input_tokens ?? 0);
|
|
2592
|
+
result.cached_input_tokens = (result.cached_input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
|
|
2593
|
+
result.output_tokens = (result.output_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
2594
|
+
result.reasoning_output_tokens = (result.reasoning_output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0);
|
|
2595
|
+
result.total_tokens = (result.total_tokens ?? 0) + tokenTotal(usage);
|
|
2596
|
+
}
|
|
2597
|
+
return result;
|
|
2226
2598
|
}
|
|
2227
2599
|
function fallbackEvents(totalTokens) {
|
|
2228
2600
|
const inputTokens = Math.floor(totalTokens * 0.6);
|
|
@@ -2246,6 +2618,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2246
2618
|
let codexDb = null;
|
|
2247
2619
|
let ingested = 0;
|
|
2248
2620
|
let requests = 0;
|
|
2621
|
+
const account = await resolveAccountForAgent("codex");
|
|
2249
2622
|
try {
|
|
2250
2623
|
codexDb = new BunDatabase(dbPath, { readonly: true });
|
|
2251
2624
|
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
@@ -2260,7 +2633,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2260
2633
|
const sessionId = `codex-${thread.id}`;
|
|
2261
2634
|
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
2262
2635
|
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
2263
|
-
upsertSession(db, {
|
|
2636
|
+
upsertSession(db, withAccount({
|
|
2264
2637
|
id: sessionId,
|
|
2265
2638
|
agent: "codex",
|
|
2266
2639
|
project_path: projectPath,
|
|
@@ -2271,9 +2644,10 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2271
2644
|
total_tokens: 0,
|
|
2272
2645
|
request_count: 0,
|
|
2273
2646
|
machine_id: machineId
|
|
2274
|
-
});
|
|
2647
|
+
}, account));
|
|
2275
2648
|
const events = readTokenEvents(thread.rollout_path);
|
|
2276
2649
|
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2650
|
+
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2277
2651
|
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2278
2652
|
tokenEvents.forEach((event, index) => {
|
|
2279
2653
|
const usage = event.usage;
|
|
@@ -2284,7 +2658,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2284
2658
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2285
2659
|
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2286
2660
|
const requestId = `${sessionId}-${index}`;
|
|
2287
|
-
upsertRequest(db, {
|
|
2661
|
+
upsertRequest(db, withAccount({
|
|
2288
2662
|
id: requestId,
|
|
2289
2663
|
agent: "codex",
|
|
2290
2664
|
session_id: sessionId,
|
|
@@ -2299,24 +2673,25 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2299
2673
|
timestamp,
|
|
2300
2674
|
source_request_id: requestId,
|
|
2301
2675
|
machine_id: machineId
|
|
2302
|
-
});
|
|
2676
|
+
}, account));
|
|
2303
2677
|
requests++;
|
|
2304
2678
|
});
|
|
2305
2679
|
rollupSession(db, sessionId);
|
|
2306
2680
|
setIngestState(db, "codex", thread.id, stateValue);
|
|
2307
2681
|
ingested++;
|
|
2308
2682
|
if (verbose)
|
|
2309
|
-
console.log(`Codex session ${thread.id}: ${
|
|
2683
|
+
console.log(`Codex session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
|
|
2310
2684
|
}
|
|
2311
2685
|
} finally {
|
|
2312
2686
|
codexDb?.close();
|
|
2313
2687
|
}
|
|
2314
2688
|
return { sessions: ingested, requests };
|
|
2315
2689
|
}
|
|
2316
|
-
var DEFAULT_CODEX_DB_PATH, DEFAULT_CODEX_CONFIG_PATH, CODEX_INGEST_VERSION = "rollout-
|
|
2690
|
+
var DEFAULT_CODEX_DB_PATH, DEFAULT_CODEX_CONFIG_PATH, CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
2317
2691
|
var init_codex = __esm(() => {
|
|
2318
2692
|
init_database();
|
|
2319
2693
|
init_pricing();
|
|
2694
|
+
init_accounts();
|
|
2320
2695
|
DEFAULT_CODEX_DB_PATH = join7(homedir4(), ".codex", "state_5.sqlite");
|
|
2321
2696
|
DEFAULT_CODEX_CONFIG_PATH = join7(homedir4(), ".codex", "config.toml");
|
|
2322
2697
|
});
|
|
@@ -2376,6 +2751,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2376
2751
|
let totalSessions = 0;
|
|
2377
2752
|
let totalRequests = 0;
|
|
2378
2753
|
const touchedSessions = new Set;
|
|
2754
|
+
const account = await resolveAccountForAgent("gemini");
|
|
2379
2755
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
2380
2756
|
for (const projectDir of projectDirs) {
|
|
2381
2757
|
const chatsDir = join8(projectDir, "chats");
|
|
@@ -2424,7 +2800,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2424
2800
|
request_count: 0,
|
|
2425
2801
|
machine_id: machineId
|
|
2426
2802
|
};
|
|
2427
|
-
upsertSession(db, session);
|
|
2803
|
+
upsertSession(db, withAccount(session, account));
|
|
2428
2804
|
totalSessions++;
|
|
2429
2805
|
}
|
|
2430
2806
|
touchedSessions.add(sessionId);
|
|
@@ -2448,7 +2824,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2448
2824
|
const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
|
|
2449
2825
|
const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
|
|
2450
2826
|
const requestId = `gemini-${sessionId}-${message.id ?? index}`;
|
|
2451
|
-
upsertRequest(db, {
|
|
2827
|
+
upsertRequest(db, withAccount({
|
|
2452
2828
|
id: requestId,
|
|
2453
2829
|
agent: "gemini",
|
|
2454
2830
|
session_id: sessionId,
|
|
@@ -2463,7 +2839,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2463
2839
|
timestamp,
|
|
2464
2840
|
source_request_id: message.id ?? requestId,
|
|
2465
2841
|
machine_id: machineId
|
|
2466
|
-
});
|
|
2842
|
+
}, account));
|
|
2467
2843
|
totalRequests++;
|
|
2468
2844
|
}
|
|
2469
2845
|
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
@@ -2478,6 +2854,7 @@ var DEFAULT_GEMINI_TMP_DIR, DEFAULT_GEMINI_HISTORY_DIR;
|
|
|
2478
2854
|
var init_gemini = __esm(() => {
|
|
2479
2855
|
init_database();
|
|
2480
2856
|
init_pricing();
|
|
2857
|
+
init_accounts();
|
|
2481
2858
|
DEFAULT_GEMINI_TMP_DIR = join8(homedir5(), ".gemini", "tmp");
|
|
2482
2859
|
DEFAULT_GEMINI_HISTORY_DIR = join8(homedir5(), ".gemini", "history");
|
|
2483
2860
|
});
|
|
@@ -2516,6 +2893,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
2516
2893
|
const touched = new Set;
|
|
2517
2894
|
const machineId = getMachineId();
|
|
2518
2895
|
const now = new Date().toISOString();
|
|
2896
|
+
const account = await resolveAccountForAgent("opencode");
|
|
2519
2897
|
for (const file of files) {
|
|
2520
2898
|
const mtime = statSync4(file).mtimeMs;
|
|
2521
2899
|
const stateKey = file;
|
|
@@ -2545,7 +2923,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
2545
2923
|
const sourceId = file.replace(OPENCODE_STORAGE, "");
|
|
2546
2924
|
const reqId = `opencode-${sourceId}`;
|
|
2547
2925
|
const costUsd = usage.cost ?? computeCostFromDb(db, model, input, output, cacheRead, cacheWrite, 0);
|
|
2548
|
-
upsertRequest(db, {
|
|
2926
|
+
upsertRequest(db, withAccount({
|
|
2549
2927
|
id: reqId,
|
|
2550
2928
|
agent: "opencode",
|
|
2551
2929
|
session_id: sessionId,
|
|
@@ -2561,10 +2939,10 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
2561
2939
|
source_request_id: sourceId,
|
|
2562
2940
|
machine_id: machineId,
|
|
2563
2941
|
updated_at: now
|
|
2564
|
-
});
|
|
2942
|
+
}, account));
|
|
2565
2943
|
requests++;
|
|
2566
2944
|
if (!touched.has(sessionId)) {
|
|
2567
|
-
upsertSession(db, {
|
|
2945
|
+
upsertSession(db, withAccount({
|
|
2568
2946
|
id: sessionId,
|
|
2569
2947
|
agent: "opencode",
|
|
2570
2948
|
project_path: "",
|
|
@@ -2576,7 +2954,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
2576
2954
|
request_count: 0,
|
|
2577
2955
|
machine_id: machineId,
|
|
2578
2956
|
updated_at: now
|
|
2579
|
-
});
|
|
2957
|
+
}, account));
|
|
2580
2958
|
touched.add(sessionId);
|
|
2581
2959
|
}
|
|
2582
2960
|
setIngestState(db, "opencode", stateKey, String(mtime));
|
|
@@ -2591,6 +2969,7 @@ var OPENCODE_STORAGE;
|
|
|
2591
2969
|
var init_opencode = __esm(() => {
|
|
2592
2970
|
init_database();
|
|
2593
2971
|
init_pricing();
|
|
2972
|
+
init_accounts();
|
|
2594
2973
|
OPENCODE_STORAGE = join9(homedir6(), ".local", "share", "opencode", "storage");
|
|
2595
2974
|
});
|
|
2596
2975
|
|
|
@@ -2628,6 +3007,7 @@ async function ingestCursor(db, verbose = false) {
|
|
|
2628
3007
|
const machineId = getMachineId();
|
|
2629
3008
|
const now = new Date().toISOString();
|
|
2630
3009
|
let snapshots = 0;
|
|
3010
|
+
const account = await resolveAccountForAgent("cursor");
|
|
2631
3011
|
const usage = await cursorFetch("/api/usage", token);
|
|
2632
3012
|
if (usage?.premiumRequests != null && usage.maxPremiumRequests) {
|
|
2633
3013
|
upsertUsageSnapshot(db, {
|
|
@@ -2675,7 +3055,7 @@ async function ingestCursor(db, verbose = false) {
|
|
|
2675
3055
|
}
|
|
2676
3056
|
const sessionId = `cursor-${today}-${machineId}`;
|
|
2677
3057
|
if (onDemand + included > 0) {
|
|
2678
|
-
upsertSession(db, {
|
|
3058
|
+
upsertSession(db, withAccount({
|
|
2679
3059
|
id: sessionId,
|
|
2680
3060
|
agent: "cursor",
|
|
2681
3061
|
project_path: "",
|
|
@@ -2687,8 +3067,8 @@ async function ingestCursor(db, verbose = false) {
|
|
|
2687
3067
|
request_count: 1,
|
|
2688
3068
|
machine_id: machineId,
|
|
2689
3069
|
updated_at: now
|
|
2690
|
-
});
|
|
2691
|
-
upsertRequest(db, {
|
|
3070
|
+
}, account));
|
|
3071
|
+
upsertRequest(db, withAccount({
|
|
2692
3072
|
id: `cursor-${today}-${machineId}-usage`,
|
|
2693
3073
|
agent: "cursor",
|
|
2694
3074
|
session_id: sessionId,
|
|
@@ -2704,7 +3084,7 @@ async function ingestCursor(db, verbose = false) {
|
|
|
2704
3084
|
source_request_id: today,
|
|
2705
3085
|
machine_id: machineId,
|
|
2706
3086
|
updated_at: now
|
|
2707
|
-
});
|
|
3087
|
+
}, account));
|
|
2708
3088
|
rollupSession(db, sessionId);
|
|
2709
3089
|
}
|
|
2710
3090
|
setIngestState(db, "cursor", `sync-${today}`, now);
|
|
@@ -2714,6 +3094,7 @@ async function ingestCursor(db, verbose = false) {
|
|
|
2714
3094
|
}
|
|
2715
3095
|
var init_cursor = __esm(() => {
|
|
2716
3096
|
init_database();
|
|
3097
|
+
init_accounts();
|
|
2717
3098
|
});
|
|
2718
3099
|
|
|
2719
3100
|
// src/ingest/pi.ts
|
|
@@ -2738,6 +3119,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
2738
3119
|
const touched = new Set;
|
|
2739
3120
|
const machineId = getMachineId();
|
|
2740
3121
|
const now = new Date().toISOString();
|
|
3122
|
+
const account = await resolveAccountForAgent("pi");
|
|
2741
3123
|
for (const file of files) {
|
|
2742
3124
|
const mtime = statSync5(file).mtimeMs;
|
|
2743
3125
|
const prev = getIngestState(db, "pi", file);
|
|
@@ -2763,7 +3145,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
2763
3145
|
const model = turn.model ?? turn.provider ?? "unknown";
|
|
2764
3146
|
const timestamp = turn.timestamp ?? new Date(statSync5(file).mtime).toISOString();
|
|
2765
3147
|
const reqId = `pi-${sessionId}-${i}`;
|
|
2766
|
-
upsertRequest(db, {
|
|
3148
|
+
upsertRequest(db, withAccount({
|
|
2767
3149
|
id: reqId,
|
|
2768
3150
|
agent: "pi",
|
|
2769
3151
|
session_id: sessionId,
|
|
@@ -2779,11 +3161,11 @@ async function ingestPi(db, verbose = false) {
|
|
|
2779
3161
|
source_request_id: `${sessionId}-${i}`,
|
|
2780
3162
|
machine_id: machineId,
|
|
2781
3163
|
updated_at: now
|
|
2782
|
-
});
|
|
3164
|
+
}, account));
|
|
2783
3165
|
requests++;
|
|
2784
3166
|
}
|
|
2785
3167
|
if (turns.length > 0) {
|
|
2786
|
-
upsertSession(db, {
|
|
3168
|
+
upsertSession(db, withAccount({
|
|
2787
3169
|
id: sessionId,
|
|
2788
3170
|
agent: "pi",
|
|
2789
3171
|
project_path: "",
|
|
@@ -2795,7 +3177,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
2795
3177
|
request_count: 0,
|
|
2796
3178
|
machine_id: machineId,
|
|
2797
3179
|
updated_at: now
|
|
2798
|
-
});
|
|
3180
|
+
}, account));
|
|
2799
3181
|
touched.add(sessionId);
|
|
2800
3182
|
}
|
|
2801
3183
|
setIngestState(db, "pi", file, String(mtime));
|
|
@@ -2809,6 +3191,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
2809
3191
|
var PI_SESSION_DIR;
|
|
2810
3192
|
var init_pi = __esm(() => {
|
|
2811
3193
|
init_database();
|
|
3194
|
+
init_accounts();
|
|
2812
3195
|
PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join10(homedir7(), ".pi", "agent", "sessions");
|
|
2813
3196
|
});
|
|
2814
3197
|
|
|
@@ -2846,13 +3229,14 @@ async function ingestHermes(db, verbose = false) {
|
|
|
2846
3229
|
const machineId = getMachineId();
|
|
2847
3230
|
const now = new Date().toISOString();
|
|
2848
3231
|
let requests = 0;
|
|
3232
|
+
const account = await resolveAccountForAgent("hermes");
|
|
2849
3233
|
for (const row of rows) {
|
|
2850
3234
|
const sessionId = `hermes-${row.id}`;
|
|
2851
3235
|
const startedAt = new Date(row.started_at * 1000).toISOString();
|
|
2852
3236
|
const endedAt = row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null;
|
|
2853
3237
|
const cost = row.actual_cost_usd ?? row.estimated_cost_usd ?? 0;
|
|
2854
3238
|
const tokens = row.input_tokens + row.output_tokens + row.cache_read_tokens + row.cache_write_tokens + row.reasoning_tokens;
|
|
2855
|
-
upsertSession(db, {
|
|
3239
|
+
upsertSession(db, withAccount({
|
|
2856
3240
|
id: sessionId,
|
|
2857
3241
|
agent: "hermes",
|
|
2858
3242
|
project_path: row.source ?? "",
|
|
@@ -2864,9 +3248,9 @@ async function ingestHermes(db, verbose = false) {
|
|
|
2864
3248
|
request_count: 1,
|
|
2865
3249
|
machine_id: machineId,
|
|
2866
3250
|
updated_at: now
|
|
2867
|
-
});
|
|
3251
|
+
}, account));
|
|
2868
3252
|
const reqId = `hermes-${row.id}-rollup`;
|
|
2869
|
-
upsertRequest(db, {
|
|
3253
|
+
upsertRequest(db, withAccount({
|
|
2870
3254
|
id: reqId,
|
|
2871
3255
|
agent: "hermes",
|
|
2872
3256
|
session_id: sessionId,
|
|
@@ -2882,7 +3266,7 @@ async function ingestHermes(db, verbose = false) {
|
|
|
2882
3266
|
source_request_id: row.id,
|
|
2883
3267
|
machine_id: machineId,
|
|
2884
3268
|
updated_at: now
|
|
2885
|
-
});
|
|
3269
|
+
}, account));
|
|
2886
3270
|
requests++;
|
|
2887
3271
|
rollupSession(db, sessionId);
|
|
2888
3272
|
if (verbose)
|
|
@@ -2902,11 +3286,12 @@ function statSyncSafe(path) {
|
|
|
2902
3286
|
var HERMES_DB;
|
|
2903
3287
|
var init_hermes = __esm(() => {
|
|
2904
3288
|
init_database();
|
|
3289
|
+
init_accounts();
|
|
2905
3290
|
HERMES_DB = join11(homedir8(), ".hermes", "state.db");
|
|
2906
3291
|
});
|
|
2907
3292
|
|
|
2908
3293
|
// src/ingest/claude-quota.ts
|
|
2909
|
-
import { existsSync as existsSync10, readFileSync as
|
|
3294
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
2910
3295
|
function readClaudeToken() {
|
|
2911
3296
|
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
2912
3297
|
if (fromEnv)
|
|
@@ -2914,7 +3299,7 @@ function readClaudeToken() {
|
|
|
2914
3299
|
if (!existsSync10(CREDENTIALS_PATH))
|
|
2915
3300
|
return null;
|
|
2916
3301
|
try {
|
|
2917
|
-
const creds = JSON.parse(
|
|
3302
|
+
const creds = JSON.parse(readFileSync8(CREDENTIALS_PATH, "utf-8"));
|
|
2918
3303
|
const oauth = creds.claudeAiOauth;
|
|
2919
3304
|
if (!oauth?.accessToken)
|
|
2920
3305
|
return null;
|
|
@@ -3055,7 +3440,7 @@ var init_claude_quota = __esm(() => {
|
|
|
3055
3440
|
});
|
|
3056
3441
|
|
|
3057
3442
|
// src/ingest/codex-quota.ts
|
|
3058
|
-
import { existsSync as existsSync11, readFileSync as
|
|
3443
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
3059
3444
|
function readCodexAuth() {
|
|
3060
3445
|
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
3061
3446
|
if (fromEnv)
|
|
@@ -3064,7 +3449,7 @@ function readCodexAuth() {
|
|
|
3064
3449
|
if (!existsSync11(authPath))
|
|
3065
3450
|
return null;
|
|
3066
3451
|
try {
|
|
3067
|
-
const auth = JSON.parse(
|
|
3452
|
+
const auth = JSON.parse(readFileSync9(authPath, "utf-8"));
|
|
3068
3453
|
const token = auth.tokens?.access_token;
|
|
3069
3454
|
if (!token)
|
|
3070
3455
|
return null;
|
|
@@ -3248,7 +3633,7 @@ __export(exports_billing, {
|
|
|
3248
3633
|
syncGeminiBilling: () => syncGeminiBilling,
|
|
3249
3634
|
syncAnthropicBilling: () => syncAnthropicBilling
|
|
3250
3635
|
});
|
|
3251
|
-
import { readFileSync as
|
|
3636
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
3252
3637
|
function getAnthropicAdminKey() {
|
|
3253
3638
|
return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
|
|
3254
3639
|
}
|
|
@@ -3442,7 +3827,7 @@ async function syncGeminiBilling(db, opts = {}) {
|
|
|
3442
3827
|
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
3443
3828
|
const fromDateStr = toISODate(start);
|
|
3444
3829
|
const toDateStr = toISODate(end);
|
|
3445
|
-
const rows = parseBillingRows(
|
|
3830
|
+
const rows = parseBillingRows(readFileSync10(exportPath, "utf-8"));
|
|
3446
3831
|
clearBillingRange(db, "gemini", fromDateStr, toDateStr);
|
|
3447
3832
|
const updatedAt = new Date().toISOString();
|
|
3448
3833
|
let totalUsd = 0;
|
|
@@ -3511,7 +3896,7 @@ __export(exports_config, {
|
|
|
3511
3896
|
loadConfig: () => loadConfig2,
|
|
3512
3897
|
getConfigValue: () => getConfigValue
|
|
3513
3898
|
});
|
|
3514
|
-
import { existsSync as existsSync12, readFileSync as
|
|
3899
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
3515
3900
|
import { dirname as dirname2, join as join12 } from "path";
|
|
3516
3901
|
function getConfigPath() {
|
|
3517
3902
|
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join12(getDataDir(), "config.json");
|
|
@@ -3520,7 +3905,7 @@ function loadConfig2() {
|
|
|
3520
3905
|
try {
|
|
3521
3906
|
const configPath = getConfigPath();
|
|
3522
3907
|
if (existsSync12(configPath)) {
|
|
3523
|
-
const raw =
|
|
3908
|
+
const raw = readFileSync11(configPath, "utf-8");
|
|
3524
3909
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
3525
3910
|
}
|
|
3526
3911
|
} catch {}
|
|
@@ -3976,6 +4361,7 @@ function createHandler(db) {
|
|
|
3976
4361
|
const project = url.searchParams.get("project") ?? undefined;
|
|
3977
4362
|
const search = url.searchParams.get("search") ?? undefined;
|
|
3978
4363
|
const machine = url.searchParams.get("machine") ?? undefined;
|
|
4364
|
+
const account = url.searchParams.get("account") ?? undefined;
|
|
3979
4365
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
3980
4366
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
3981
4367
|
const since = url.searchParams.get("since") ?? undefined;
|
|
@@ -3986,6 +4372,7 @@ function createHandler(db) {
|
|
|
3986
4372
|
project,
|
|
3987
4373
|
search,
|
|
3988
4374
|
machine,
|
|
4375
|
+
account,
|
|
3989
4376
|
limit,
|
|
3990
4377
|
offset,
|
|
3991
4378
|
since
|
|
@@ -4036,11 +4423,23 @@ function createHandler(db) {
|
|
|
4036
4423
|
return ok(results);
|
|
4037
4424
|
}
|
|
4038
4425
|
if (path === "/api/projects" && method === "GET") {
|
|
4039
|
-
|
|
4426
|
+
const period = url.searchParams.get("period") ?? "all";
|
|
4427
|
+
return ok(queryProjectBreakdown(db, period));
|
|
4428
|
+
}
|
|
4429
|
+
if (path === "/api/accounts" && method === "GET") {
|
|
4430
|
+
const period = url.searchParams.get("period") ?? "all";
|
|
4431
|
+
return ok(queryAccountBreakdown(db, period));
|
|
4040
4432
|
}
|
|
4041
4433
|
if (path === "/api/breakdown" && method === "GET") {
|
|
4042
4434
|
const by = url.searchParams.get("by") ?? "model";
|
|
4043
|
-
|
|
4435
|
+
const period = url.searchParams.get("period") ?? "all";
|
|
4436
|
+
if (by === "project")
|
|
4437
|
+
return ok(queryProjectBreakdown(db, period));
|
|
4438
|
+
if (by === "agent")
|
|
4439
|
+
return ok(queryAgentBreakdown(db, period));
|
|
4440
|
+
if (by === "account")
|
|
4441
|
+
return ok(queryAccountBreakdown(db, period));
|
|
4442
|
+
return ok(queryModelBreakdown(db));
|
|
4044
4443
|
}
|
|
4045
4444
|
if (path === "/api/budgets" && method === "GET") {
|
|
4046
4445
|
return ok(getBudgetStatuses(db));
|
|
@@ -6305,7 +6704,7 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
|
|
|
6305
6704
|
]));
|
|
6306
6705
|
console.log();
|
|
6307
6706
|
});
|
|
6308
|
-
program.command("breakdown").description("Cost breakdown by model, agent, or
|
|
6707
|
+
program.command("breakdown").description("Cost breakdown by model, agent, project, or account").option("--by <dimension>", "Dimension: model|agent|project|account", "model").option("--since <date>", "Filter since date or relative (e.g. 2026-03-01, 7d, 30d)").action((opts) => {
|
|
6309
6708
|
const db = openDatabase();
|
|
6310
6709
|
const sinceDate = opts.since ? parseSinceDate(opts.since) : undefined;
|
|
6311
6710
|
console.log();
|
|
@@ -6327,6 +6726,55 @@ program.command("breakdown").description("Cost breakdown by model, agent, or pro
|
|
|
6327
6726
|
chalk7.cyan(fmtTokens(r.total_tokens)),
|
|
6328
6727
|
fmt4(r.cost_usd)
|
|
6329
6728
|
]));
|
|
6729
|
+
} else if (opts.by === "agent") {
|
|
6730
|
+
const rows = sinceDate ? db.prepare(`
|
|
6731
|
+
SELECT agent,
|
|
6732
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
6733
|
+
COUNT(*) as requests,
|
|
6734
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
6735
|
+
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
6736
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
6737
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
6738
|
+
MAX(timestamp) as last_active
|
|
6739
|
+
FROM requests
|
|
6740
|
+
WHERE timestamp >= ?
|
|
6741
|
+
GROUP BY agent
|
|
6742
|
+
ORDER BY api_equivalent_usd DESC
|
|
6743
|
+
`).all(sinceDate) : queryAgentBreakdown(db);
|
|
6744
|
+
printTable(["Agent", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], rows.map((r) => [
|
|
6745
|
+
fmtAgent(r.agent),
|
|
6746
|
+
String(r.sessions),
|
|
6747
|
+
String(r.requests),
|
|
6748
|
+
chalk7.cyan(fmtTokens(r.total_tokens)),
|
|
6749
|
+
fmt4(r.api_equivalent_usd),
|
|
6750
|
+
fmt4(r.billable_usd),
|
|
6751
|
+
fmt4(r.subscription_included_usd)
|
|
6752
|
+
]));
|
|
6753
|
+
} else if (opts.by === "account") {
|
|
6754
|
+
const rows = sinceDate ? db.prepare(`
|
|
6755
|
+
SELECT account_key, account_tool, account_name, account_email, account_source,
|
|
6756
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
6757
|
+
COUNT(*) as requests,
|
|
6758
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
6759
|
+
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
6760
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
6761
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
6762
|
+
MAX(timestamp) as last_active
|
|
6763
|
+
FROM requests
|
|
6764
|
+
WHERE timestamp >= ?
|
|
6765
|
+
AND (account_key != '' OR account_tool != '' OR account_name != '' OR account_email != '')
|
|
6766
|
+
GROUP BY account_key, account_tool, account_name, account_email, account_source
|
|
6767
|
+
ORDER BY api_equivalent_usd DESC
|
|
6768
|
+
`).all(sinceDate) : queryAccountBreakdown(db);
|
|
6769
|
+
printTable(["Account", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], rows.map((r) => [
|
|
6770
|
+
chalk7.white(r.account_key || r.account_name || chalk7.dim("unknown")),
|
|
6771
|
+
String(r.sessions),
|
|
6772
|
+
String(r.requests),
|
|
6773
|
+
chalk7.cyan(fmtTokens(r.total_tokens)),
|
|
6774
|
+
fmt4(r.api_equivalent_usd),
|
|
6775
|
+
fmt4("billable_usd" in r ? Number(r.billable_usd) : r.metered_api_usd),
|
|
6776
|
+
fmt4(r.subscription_included_usd)
|
|
6777
|
+
]));
|
|
6330
6778
|
} else {
|
|
6331
6779
|
const rows = sinceDate ? db.prepare(`
|
|
6332
6780
|
SELECT model, agent,
|