@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/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
|
|
@@ -1228,7 +1435,12 @@ var init_pg_migrations = __esm(() => {
|
|
|
1228
1435
|
duration_ms INTEGER DEFAULT 0,
|
|
1229
1436
|
timestamp TEXT NOT NULL,
|
|
1230
1437
|
source_request_id TEXT,
|
|
1231
|
-
machine_id TEXT DEFAULT ''
|
|
1438
|
+
machine_id TEXT DEFAULT '',
|
|
1439
|
+
account_key TEXT DEFAULT '',
|
|
1440
|
+
account_tool TEXT DEFAULT '',
|
|
1441
|
+
account_name TEXT DEFAULT '',
|
|
1442
|
+
account_email TEXT DEFAULT '',
|
|
1443
|
+
account_source TEXT DEFAULT ''
|
|
1232
1444
|
)`,
|
|
1233
1445
|
`CREATE TABLE IF NOT EXISTS sessions (
|
|
1234
1446
|
id TEXT PRIMARY KEY,
|
|
@@ -1240,7 +1452,12 @@ var init_pg_migrations = __esm(() => {
|
|
|
1240
1452
|
total_cost_usd REAL DEFAULT 0,
|
|
1241
1453
|
total_tokens INTEGER DEFAULT 0,
|
|
1242
1454
|
request_count INTEGER DEFAULT 0,
|
|
1243
|
-
machine_id TEXT DEFAULT ''
|
|
1455
|
+
machine_id TEXT DEFAULT '',
|
|
1456
|
+
account_key TEXT DEFAULT '',
|
|
1457
|
+
account_tool TEXT DEFAULT '',
|
|
1458
|
+
account_name TEXT DEFAULT '',
|
|
1459
|
+
account_email TEXT DEFAULT '',
|
|
1460
|
+
account_source TEXT DEFAULT ''
|
|
1244
1461
|
)`,
|
|
1245
1462
|
`CREATE TABLE IF NOT EXISTS projects (
|
|
1246
1463
|
id TEXT PRIMARY KEY,
|
|
@@ -1361,13 +1578,25 @@ var init_pg_migrations = __esm(() => {
|
|
|
1361
1578
|
)`,
|
|
1362
1579
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
|
|
1363
1580
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1581
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
|
|
1582
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
|
|
1583
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
|
|
1584
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
|
|
1585
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
1364
1586
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1365
1587
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1366
1588
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1589
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
|
|
1590
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
|
|
1591
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
|
|
1592
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
|
|
1593
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
1367
1594
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1368
1595
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1369
1596
|
`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)
|
|
1597
|
+
`CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
|
|
1598
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
|
|
1599
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`
|
|
1371
1600
|
];
|
|
1372
1601
|
});
|
|
1373
1602
|
|
|
@@ -1378,7 +1607,7 @@ __export(exports_billing, {
|
|
|
1378
1607
|
syncGeminiBilling: () => syncGeminiBilling,
|
|
1379
1608
|
syncAnthropicBilling: () => syncAnthropicBilling
|
|
1380
1609
|
});
|
|
1381
|
-
import { readFileSync as
|
|
1610
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
1382
1611
|
function getAnthropicAdminKey() {
|
|
1383
1612
|
return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
|
|
1384
1613
|
}
|
|
@@ -1572,7 +1801,7 @@ async function syncGeminiBilling(db, opts = {}) {
|
|
|
1572
1801
|
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
1573
1802
|
const fromDateStr = toISODate(start);
|
|
1574
1803
|
const toDateStr = toISODate(end);
|
|
1575
|
-
const rows = parseBillingRows(
|
|
1804
|
+
const rows = parseBillingRows(readFileSync9(exportPath, "utf-8"));
|
|
1576
1805
|
clearBillingRange(db, "gemini", fromDateStr, toDateStr);
|
|
1577
1806
|
const updatedAt = new Date().toISOString();
|
|
1578
1807
|
let totalUsd = 0;
|
|
@@ -1634,7 +1863,7 @@ var init_open_projects = __esm(() => {
|
|
|
1634
1863
|
});
|
|
1635
1864
|
|
|
1636
1865
|
// src/lib/config.ts
|
|
1637
|
-
import { existsSync as existsSync10, readFileSync as
|
|
1866
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
1638
1867
|
import { dirname, join as join9 } from "path";
|
|
1639
1868
|
function getConfigPath() {
|
|
1640
1869
|
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join9(getDataDir(), "config.json");
|
|
@@ -1643,7 +1872,7 @@ function loadConfig() {
|
|
|
1643
1872
|
try {
|
|
1644
1873
|
const configPath = getConfigPath();
|
|
1645
1874
|
if (existsSync10(configPath)) {
|
|
1646
|
-
const raw =
|
|
1875
|
+
const raw = readFileSync10(configPath, "utf-8");
|
|
1647
1876
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
1648
1877
|
}
|
|
1649
1878
|
} catch {}
|
|
@@ -1809,7 +2038,6 @@ function periodWhere2(period, column) {
|
|
|
1809
2038
|
}
|
|
1810
2039
|
function prorateMonthlyFee(monthlyFee, period) {
|
|
1811
2040
|
const now = new Date;
|
|
1812
|
-
const dayOfMonth = now.getDate();
|
|
1813
2041
|
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1814
2042
|
switch (period) {
|
|
1815
2043
|
case "today":
|
|
@@ -1891,6 +2119,131 @@ function defaultCostBasisForAgent(agent) {
|
|
|
1891
2119
|
return "estimated";
|
|
1892
2120
|
}
|
|
1893
2121
|
|
|
2122
|
+
// src/lib/accounts.ts
|
|
2123
|
+
var AGENT_ACCOUNT_TOOLS = {
|
|
2124
|
+
claude: ["claude"],
|
|
2125
|
+
takumi: ["takumi", "claude"],
|
|
2126
|
+
codex: ["codex"],
|
|
2127
|
+
gemini: ["gemini"],
|
|
2128
|
+
opencode: ["opencode"],
|
|
2129
|
+
cursor: ["cursor"],
|
|
2130
|
+
pi: ["pi"],
|
|
2131
|
+
hermes: ["hermes"]
|
|
2132
|
+
};
|
|
2133
|
+
function accountKey(tool, name) {
|
|
2134
|
+
return `${tool}:${name}`;
|
|
2135
|
+
}
|
|
2136
|
+
function normalizeDir(value) {
|
|
2137
|
+
return value.replace(/\/+$/, "");
|
|
2138
|
+
}
|
|
2139
|
+
function fromProfile(profile, source) {
|
|
2140
|
+
return {
|
|
2141
|
+
account_key: accountKey(profile.tool, profile.name),
|
|
2142
|
+
account_tool: profile.tool,
|
|
2143
|
+
account_name: profile.name,
|
|
2144
|
+
...profile.email ? { account_email: profile.email } : {},
|
|
2145
|
+
account_source: source
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
function fromOverride(raw, agent) {
|
|
2149
|
+
const value = raw.trim();
|
|
2150
|
+
if (!value)
|
|
2151
|
+
return null;
|
|
2152
|
+
const candidateTool = AGENT_ACCOUNT_TOOLS[agent][0] ?? agent;
|
|
2153
|
+
const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
|
|
2154
|
+
if (!tool || !name)
|
|
2155
|
+
return null;
|
|
2156
|
+
return {
|
|
2157
|
+
account_key: accountKey(tool, name),
|
|
2158
|
+
account_tool: tool,
|
|
2159
|
+
account_name: name,
|
|
2160
|
+
account_source: "override"
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
function envOverride(agent, env) {
|
|
2164
|
+
const agentPrefix = agent.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
2165
|
+
const raw = env[`ECONOMY_${agentPrefix}_ACCOUNT_KEY`] ?? env[`ECONOMY_${agentPrefix}_ACCOUNT`] ?? env["ECONOMY_ACCOUNT_KEY"] ?? env["ECONOMY_ACCOUNT"];
|
|
2166
|
+
if (raw)
|
|
2167
|
+
return fromOverride(raw, agent);
|
|
2168
|
+
const tool = env[`ECONOMY_${agentPrefix}_ACCOUNT_TOOL`] ?? env["ECONOMY_ACCOUNT_TOOL"];
|
|
2169
|
+
const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
|
|
2170
|
+
if (!tool || !name)
|
|
2171
|
+
return null;
|
|
2172
|
+
return {
|
|
2173
|
+
account_key: accountKey(tool, name),
|
|
2174
|
+
account_tool: tool,
|
|
2175
|
+
account_name: name,
|
|
2176
|
+
account_email: env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"],
|
|
2177
|
+
account_source: "override"
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
function knownToolIds(api) {
|
|
2181
|
+
try {
|
|
2182
|
+
return new Set(api.listTools().map((tool) => tool.id));
|
|
2183
|
+
} catch {
|
|
2184
|
+
return new Set;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
function profileForEnvDir(api, tool, env) {
|
|
2188
|
+
const configuredDir = env[tool.envVar];
|
|
2189
|
+
if (!configuredDir)
|
|
2190
|
+
return null;
|
|
2191
|
+
const normalized = normalizeDir(configuredDir);
|
|
2192
|
+
try {
|
|
2193
|
+
return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
2194
|
+
} catch {
|
|
2195
|
+
return null;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
async function resolveAccountForAgent(agent, env = process.env) {
|
|
2199
|
+
const override = envOverride(agent, env);
|
|
2200
|
+
if (override)
|
|
2201
|
+
return override;
|
|
2202
|
+
let api;
|
|
2203
|
+
try {
|
|
2204
|
+
api = await import("@hasna/accounts");
|
|
2205
|
+
} catch {
|
|
2206
|
+
return null;
|
|
2207
|
+
}
|
|
2208
|
+
const toolIds = knownToolIds(api);
|
|
2209
|
+
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
2210
|
+
if (!toolIds.has(toolId))
|
|
2211
|
+
continue;
|
|
2212
|
+
let tool;
|
|
2213
|
+
try {
|
|
2214
|
+
tool = api.getTool(toolId);
|
|
2215
|
+
} catch {
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
const envProfile = profileForEnvDir(api, tool, env);
|
|
2219
|
+
if (envProfile)
|
|
2220
|
+
return fromProfile(envProfile, "env");
|
|
2221
|
+
try {
|
|
2222
|
+
const applied = api.appliedProfile(toolId);
|
|
2223
|
+
if (applied)
|
|
2224
|
+
return fromProfile(applied, "applied");
|
|
2225
|
+
} catch {}
|
|
2226
|
+
try {
|
|
2227
|
+
const current = api.currentProfile(toolId);
|
|
2228
|
+
if (current)
|
|
2229
|
+
return fromProfile(current, "current");
|
|
2230
|
+
} catch {}
|
|
2231
|
+
}
|
|
2232
|
+
return null;
|
|
2233
|
+
}
|
|
2234
|
+
function withAccount(record, account) {
|
|
2235
|
+
if (!account)
|
|
2236
|
+
return record;
|
|
2237
|
+
return {
|
|
2238
|
+
...record,
|
|
2239
|
+
account_key: account.account_key,
|
|
2240
|
+
account_tool: account.account_tool,
|
|
2241
|
+
account_name: account.account_name,
|
|
2242
|
+
account_email: account.account_email ?? "",
|
|
2243
|
+
account_source: account.account_source
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
|
|
1894
2247
|
// src/ingest/claude.ts
|
|
1895
2248
|
function autoDetectProject(cwd, projects) {
|
|
1896
2249
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
@@ -1932,6 +2285,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
1932
2285
|
let totalRequests = 0;
|
|
1933
2286
|
const touchedSessions = new Set;
|
|
1934
2287
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
2288
|
+
const account = await resolveAccountForAgent(agentName);
|
|
1935
2289
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
1936
2290
|
for (const projectDirEntry of projectDirs) {
|
|
1937
2291
|
const projectDirPath = join2(projectsDir, projectDirEntry.name);
|
|
@@ -1995,7 +2349,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
1995
2349
|
}
|
|
1996
2350
|
const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
|
|
1997
2351
|
const reqId = `${agentName}-${sourceRequestId}`;
|
|
1998
|
-
upsertRequest(db, {
|
|
2352
|
+
upsertRequest(db, withAccount({
|
|
1999
2353
|
id: reqId,
|
|
2000
2354
|
agent: agentName,
|
|
2001
2355
|
session_id: sessionId,
|
|
@@ -2012,7 +2366,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
2012
2366
|
timestamp,
|
|
2013
2367
|
source_request_id: sourceRequestId,
|
|
2014
2368
|
machine_id: machineId
|
|
2015
|
-
});
|
|
2369
|
+
}, account));
|
|
2016
2370
|
if (!touchedSessions.has(sessionId)) {
|
|
2017
2371
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
2018
2372
|
if (!existing) {
|
|
@@ -2030,7 +2384,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
2030
2384
|
request_count: 0,
|
|
2031
2385
|
machine_id: machineId
|
|
2032
2386
|
};
|
|
2033
|
-
upsertSession(db, session);
|
|
2387
|
+
upsertSession(db, withAccount(session, account));
|
|
2034
2388
|
}
|
|
2035
2389
|
touchedSessions.add(sessionId);
|
|
2036
2390
|
}
|
|
@@ -2076,7 +2430,7 @@ import { join as join3, basename as basename2 } from "path";
|
|
|
2076
2430
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
2077
2431
|
var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
2078
2432
|
var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
2079
|
-
var CODEX_INGEST_VERSION = "rollout-
|
|
2433
|
+
var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
2080
2434
|
function codexDbPath() {
|
|
2081
2435
|
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
2082
2436
|
}
|
|
@@ -2109,8 +2463,9 @@ function buildThreadQuery(codexDb) {
|
|
|
2109
2463
|
function readTokenEvents(rolloutPath) {
|
|
2110
2464
|
if (!rolloutPath || !existsSync3(rolloutPath))
|
|
2111
2465
|
return [];
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2466
|
+
const fallbackUsages = new Map;
|
|
2467
|
+
let fallbackTimestamp;
|
|
2468
|
+
let aggregate = null;
|
|
2114
2469
|
for (const line of readFileSync2(rolloutPath, "utf-8").split(`
|
|
2115
2470
|
`)) {
|
|
2116
2471
|
if (!line.trim())
|
|
@@ -2127,20 +2482,48 @@ function readTokenEvents(rolloutPath) {
|
|
|
2127
2482
|
if (!payload || payload["type"] !== "token_count")
|
|
2128
2483
|
continue;
|
|
2129
2484
|
const info = payload["info"];
|
|
2485
|
+
const timestamp = entry["timestamp"];
|
|
2486
|
+
const entryTimestamp = typeof timestamp === "string" ? timestamp : undefined;
|
|
2487
|
+
const totalUsage = info?.["total_token_usage"];
|
|
2488
|
+
if (totalUsage && tokenTotal(totalUsage) > 0) {
|
|
2489
|
+
aggregate = { usage: totalUsage, timestamp: entryTimestamp };
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2130
2492
|
const usage = info?.["last_token_usage"];
|
|
2131
2493
|
if (!usage)
|
|
2132
2494
|
continue;
|
|
2133
|
-
|
|
2134
|
-
if (total <= 0)
|
|
2495
|
+
if (tokenTotal(usage) <= 0)
|
|
2135
2496
|
continue;
|
|
2136
2497
|
const key = JSON.stringify(usage);
|
|
2137
|
-
if (
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2498
|
+
if (!fallbackUsages.has(key))
|
|
2499
|
+
fallbackUsages.set(key, usage);
|
|
2500
|
+
fallbackTimestamp = entryTimestamp ?? fallbackTimestamp;
|
|
2501
|
+
}
|
|
2502
|
+
if (aggregate)
|
|
2503
|
+
return [aggregate];
|
|
2504
|
+
if (fallbackUsages.size === 0)
|
|
2505
|
+
return [];
|
|
2506
|
+
return [{ usage: sumTokenUsages([...fallbackUsages.values()]), timestamp: fallbackTimestamp }];
|
|
2507
|
+
}
|
|
2508
|
+
function tokenTotal(usage) {
|
|
2509
|
+
return usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
2510
|
+
}
|
|
2511
|
+
function sumTokenUsages(usages) {
|
|
2512
|
+
const result = {
|
|
2513
|
+
input_tokens: 0,
|
|
2514
|
+
cached_input_tokens: 0,
|
|
2515
|
+
output_tokens: 0,
|
|
2516
|
+
reasoning_output_tokens: 0,
|
|
2517
|
+
total_tokens: 0
|
|
2518
|
+
};
|
|
2519
|
+
for (const usage of usages) {
|
|
2520
|
+
result.input_tokens = (result.input_tokens ?? 0) + (usage.input_tokens ?? 0);
|
|
2521
|
+
result.cached_input_tokens = (result.cached_input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
|
|
2522
|
+
result.output_tokens = (result.output_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
2523
|
+
result.reasoning_output_tokens = (result.reasoning_output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0);
|
|
2524
|
+
result.total_tokens = (result.total_tokens ?? 0) + tokenTotal(usage);
|
|
2142
2525
|
}
|
|
2143
|
-
return
|
|
2526
|
+
return result;
|
|
2144
2527
|
}
|
|
2145
2528
|
function fallbackEvents(totalTokens) {
|
|
2146
2529
|
const inputTokens = Math.floor(totalTokens * 0.6);
|
|
@@ -2164,6 +2547,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2164
2547
|
let codexDb = null;
|
|
2165
2548
|
let ingested = 0;
|
|
2166
2549
|
let requests = 0;
|
|
2550
|
+
const account = await resolveAccountForAgent("codex");
|
|
2167
2551
|
try {
|
|
2168
2552
|
codexDb = new BunDatabase(dbPath, { readonly: true });
|
|
2169
2553
|
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
@@ -2178,7 +2562,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2178
2562
|
const sessionId = `codex-${thread.id}`;
|
|
2179
2563
|
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
2180
2564
|
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
2181
|
-
upsertSession(db, {
|
|
2565
|
+
upsertSession(db, withAccount({
|
|
2182
2566
|
id: sessionId,
|
|
2183
2567
|
agent: "codex",
|
|
2184
2568
|
project_path: projectPath,
|
|
@@ -2189,9 +2573,10 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2189
2573
|
total_tokens: 0,
|
|
2190
2574
|
request_count: 0,
|
|
2191
2575
|
machine_id: machineId
|
|
2192
|
-
});
|
|
2576
|
+
}, account));
|
|
2193
2577
|
const events = readTokenEvents(thread.rollout_path);
|
|
2194
2578
|
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2579
|
+
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2195
2580
|
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2196
2581
|
tokenEvents.forEach((event, index) => {
|
|
2197
2582
|
const usage = event.usage;
|
|
@@ -2202,7 +2587,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2202
2587
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2203
2588
|
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2204
2589
|
const requestId = `${sessionId}-${index}`;
|
|
2205
|
-
upsertRequest(db, {
|
|
2590
|
+
upsertRequest(db, withAccount({
|
|
2206
2591
|
id: requestId,
|
|
2207
2592
|
agent: "codex",
|
|
2208
2593
|
session_id: sessionId,
|
|
@@ -2217,14 +2602,14 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2217
2602
|
timestamp,
|
|
2218
2603
|
source_request_id: requestId,
|
|
2219
2604
|
machine_id: machineId
|
|
2220
|
-
});
|
|
2605
|
+
}, account));
|
|
2221
2606
|
requests++;
|
|
2222
2607
|
});
|
|
2223
2608
|
rollupSession(db, sessionId);
|
|
2224
2609
|
setIngestState(db, "codex", thread.id, stateValue);
|
|
2225
2610
|
ingested++;
|
|
2226
2611
|
if (verbose)
|
|
2227
|
-
console.log(`Codex session ${thread.id}: ${
|
|
2612
|
+
console.log(`Codex session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
|
|
2228
2613
|
}
|
|
2229
2614
|
} finally {
|
|
2230
2615
|
codexDb?.close();
|
|
@@ -2291,6 +2676,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2291
2676
|
let totalSessions = 0;
|
|
2292
2677
|
let totalRequests = 0;
|
|
2293
2678
|
const touchedSessions = new Set;
|
|
2679
|
+
const account = await resolveAccountForAgent("gemini");
|
|
2294
2680
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
2295
2681
|
for (const projectDir of projectDirs) {
|
|
2296
2682
|
const chatsDir = join4(projectDir, "chats");
|
|
@@ -2339,7 +2725,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2339
2725
|
request_count: 0,
|
|
2340
2726
|
machine_id: machineId
|
|
2341
2727
|
};
|
|
2342
|
-
upsertSession(db, session);
|
|
2728
|
+
upsertSession(db, withAccount(session, account));
|
|
2343
2729
|
totalSessions++;
|
|
2344
2730
|
}
|
|
2345
2731
|
touchedSessions.add(sessionId);
|
|
@@ -2363,7 +2749,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2363
2749
|
const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
|
|
2364
2750
|
const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
|
|
2365
2751
|
const requestId = `gemini-${sessionId}-${message.id ?? index}`;
|
|
2366
|
-
upsertRequest(db, {
|
|
2752
|
+
upsertRequest(db, withAccount({
|
|
2367
2753
|
id: requestId,
|
|
2368
2754
|
agent: "gemini",
|
|
2369
2755
|
session_id: sessionId,
|
|
@@ -2378,7 +2764,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2378
2764
|
timestamp,
|
|
2379
2765
|
source_request_id: message.id ?? requestId,
|
|
2380
2766
|
machine_id: machineId
|
|
2381
|
-
});
|
|
2767
|
+
}, account));
|
|
2382
2768
|
totalRequests++;
|
|
2383
2769
|
}
|
|
2384
2770
|
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
@@ -2427,6 +2813,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
2427
2813
|
const touched = new Set;
|
|
2428
2814
|
const machineId = getMachineId();
|
|
2429
2815
|
const now = new Date().toISOString();
|
|
2816
|
+
const account = await resolveAccountForAgent("opencode");
|
|
2430
2817
|
for (const file of files) {
|
|
2431
2818
|
const mtime = statSync4(file).mtimeMs;
|
|
2432
2819
|
const stateKey = file;
|
|
@@ -2456,7 +2843,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
2456
2843
|
const sourceId = file.replace(OPENCODE_STORAGE, "");
|
|
2457
2844
|
const reqId = `opencode-${sourceId}`;
|
|
2458
2845
|
const costUsd = usage.cost ?? computeCostFromDb(db, model, input, output, cacheRead, cacheWrite, 0);
|
|
2459
|
-
upsertRequest(db, {
|
|
2846
|
+
upsertRequest(db, withAccount({
|
|
2460
2847
|
id: reqId,
|
|
2461
2848
|
agent: "opencode",
|
|
2462
2849
|
session_id: sessionId,
|
|
@@ -2472,10 +2859,10 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
2472
2859
|
source_request_id: sourceId,
|
|
2473
2860
|
machine_id: machineId,
|
|
2474
2861
|
updated_at: now
|
|
2475
|
-
});
|
|
2862
|
+
}, account));
|
|
2476
2863
|
requests++;
|
|
2477
2864
|
if (!touched.has(sessionId)) {
|
|
2478
|
-
upsertSession(db, {
|
|
2865
|
+
upsertSession(db, withAccount({
|
|
2479
2866
|
id: sessionId,
|
|
2480
2867
|
agent: "opencode",
|
|
2481
2868
|
project_path: "",
|
|
@@ -2487,7 +2874,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
2487
2874
|
request_count: 0,
|
|
2488
2875
|
machine_id: machineId,
|
|
2489
2876
|
updated_at: now
|
|
2490
|
-
});
|
|
2877
|
+
}, account));
|
|
2491
2878
|
touched.add(sessionId);
|
|
2492
2879
|
}
|
|
2493
2880
|
setIngestState(db, "opencode", stateKey, String(mtime));
|
|
@@ -2534,6 +2921,7 @@ async function ingestCursor(db, verbose = false) {
|
|
|
2534
2921
|
const machineId = getMachineId();
|
|
2535
2922
|
const now = new Date().toISOString();
|
|
2536
2923
|
let snapshots = 0;
|
|
2924
|
+
const account = await resolveAccountForAgent("cursor");
|
|
2537
2925
|
const usage = await cursorFetch("/api/usage", token);
|
|
2538
2926
|
if (usage?.premiumRequests != null && usage.maxPremiumRequests) {
|
|
2539
2927
|
upsertUsageSnapshot(db, {
|
|
@@ -2581,7 +2969,7 @@ async function ingestCursor(db, verbose = false) {
|
|
|
2581
2969
|
}
|
|
2582
2970
|
const sessionId = `cursor-${today}-${machineId}`;
|
|
2583
2971
|
if (onDemand + included > 0) {
|
|
2584
|
-
upsertSession(db, {
|
|
2972
|
+
upsertSession(db, withAccount({
|
|
2585
2973
|
id: sessionId,
|
|
2586
2974
|
agent: "cursor",
|
|
2587
2975
|
project_path: "",
|
|
@@ -2593,8 +2981,8 @@ async function ingestCursor(db, verbose = false) {
|
|
|
2593
2981
|
request_count: 1,
|
|
2594
2982
|
machine_id: machineId,
|
|
2595
2983
|
updated_at: now
|
|
2596
|
-
});
|
|
2597
|
-
upsertRequest(db, {
|
|
2984
|
+
}, account));
|
|
2985
|
+
upsertRequest(db, withAccount({
|
|
2598
2986
|
id: `cursor-${today}-${machineId}-usage`,
|
|
2599
2987
|
agent: "cursor",
|
|
2600
2988
|
session_id: sessionId,
|
|
@@ -2610,7 +2998,7 @@ async function ingestCursor(db, verbose = false) {
|
|
|
2610
2998
|
source_request_id: today,
|
|
2611
2999
|
machine_id: machineId,
|
|
2612
3000
|
updated_at: now
|
|
2613
|
-
});
|
|
3001
|
+
}, account));
|
|
2614
3002
|
rollupSession(db, sessionId);
|
|
2615
3003
|
}
|
|
2616
3004
|
setIngestState(db, "cursor", `sync-${today}`, now);
|
|
@@ -2643,6 +3031,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
2643
3031
|
const touched = new Set;
|
|
2644
3032
|
const machineId = getMachineId();
|
|
2645
3033
|
const now = new Date().toISOString();
|
|
3034
|
+
const account = await resolveAccountForAgent("pi");
|
|
2646
3035
|
for (const file of files) {
|
|
2647
3036
|
const mtime = statSync5(file).mtimeMs;
|
|
2648
3037
|
const prev = getIngestState(db, "pi", file);
|
|
@@ -2668,7 +3057,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
2668
3057
|
const model = turn.model ?? turn.provider ?? "unknown";
|
|
2669
3058
|
const timestamp = turn.timestamp ?? new Date(statSync5(file).mtime).toISOString();
|
|
2670
3059
|
const reqId = `pi-${sessionId}-${i}`;
|
|
2671
|
-
upsertRequest(db, {
|
|
3060
|
+
upsertRequest(db, withAccount({
|
|
2672
3061
|
id: reqId,
|
|
2673
3062
|
agent: "pi",
|
|
2674
3063
|
session_id: sessionId,
|
|
@@ -2684,11 +3073,11 @@ async function ingestPi(db, verbose = false) {
|
|
|
2684
3073
|
source_request_id: `${sessionId}-${i}`,
|
|
2685
3074
|
machine_id: machineId,
|
|
2686
3075
|
updated_at: now
|
|
2687
|
-
});
|
|
3076
|
+
}, account));
|
|
2688
3077
|
requests++;
|
|
2689
3078
|
}
|
|
2690
3079
|
if (turns.length > 0) {
|
|
2691
|
-
upsertSession(db, {
|
|
3080
|
+
upsertSession(db, withAccount({
|
|
2692
3081
|
id: sessionId,
|
|
2693
3082
|
agent: "pi",
|
|
2694
3083
|
project_path: "",
|
|
@@ -2700,7 +3089,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
2700
3089
|
request_count: 0,
|
|
2701
3090
|
machine_id: machineId,
|
|
2702
3091
|
updated_at: now
|
|
2703
|
-
});
|
|
3092
|
+
}, account));
|
|
2704
3093
|
touched.add(sessionId);
|
|
2705
3094
|
}
|
|
2706
3095
|
setIngestState(db, "pi", file, String(mtime));
|
|
@@ -2748,13 +3137,14 @@ async function ingestHermes(db, verbose = false) {
|
|
|
2748
3137
|
const machineId = getMachineId();
|
|
2749
3138
|
const now = new Date().toISOString();
|
|
2750
3139
|
let requests = 0;
|
|
3140
|
+
const account = await resolveAccountForAgent("hermes");
|
|
2751
3141
|
for (const row of rows) {
|
|
2752
3142
|
const sessionId = `hermes-${row.id}`;
|
|
2753
3143
|
const startedAt = new Date(row.started_at * 1000).toISOString();
|
|
2754
3144
|
const endedAt = row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null;
|
|
2755
3145
|
const cost = row.actual_cost_usd ?? row.estimated_cost_usd ?? 0;
|
|
2756
3146
|
const tokens = row.input_tokens + row.output_tokens + row.cache_read_tokens + row.cache_write_tokens + row.reasoning_tokens;
|
|
2757
|
-
upsertSession(db, {
|
|
3147
|
+
upsertSession(db, withAccount({
|
|
2758
3148
|
id: sessionId,
|
|
2759
3149
|
agent: "hermes",
|
|
2760
3150
|
project_path: row.source ?? "",
|
|
@@ -2766,9 +3156,9 @@ async function ingestHermes(db, verbose = false) {
|
|
|
2766
3156
|
request_count: 1,
|
|
2767
3157
|
machine_id: machineId,
|
|
2768
3158
|
updated_at: now
|
|
2769
|
-
});
|
|
3159
|
+
}, account));
|
|
2770
3160
|
const reqId = `hermes-${row.id}-rollup`;
|
|
2771
|
-
upsertRequest(db, {
|
|
3161
|
+
upsertRequest(db, withAccount({
|
|
2772
3162
|
id: reqId,
|
|
2773
3163
|
agent: "hermes",
|
|
2774
3164
|
session_id: sessionId,
|
|
@@ -2784,7 +3174,7 @@ async function ingestHermes(db, verbose = false) {
|
|
|
2784
3174
|
source_request_id: row.id,
|
|
2785
3175
|
machine_id: machineId,
|
|
2786
3176
|
updated_at: now
|
|
2787
|
-
});
|
|
3177
|
+
}, account));
|
|
2788
3178
|
requests++;
|
|
2789
3179
|
rollupSession(db, sessionId);
|
|
2790
3180
|
if (verbose)
|
|
@@ -2804,7 +3194,7 @@ function statSyncSafe(path) {
|
|
|
2804
3194
|
|
|
2805
3195
|
// src/ingest/claude-quota.ts
|
|
2806
3196
|
init_database();
|
|
2807
|
-
import { existsSync as existsSync8, readFileSync as
|
|
3197
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
2808
3198
|
|
|
2809
3199
|
// src/lib/paths.ts
|
|
2810
3200
|
import { homedir as homedir8 } from "os";
|
|
@@ -2842,7 +3232,7 @@ function readClaudeToken() {
|
|
|
2842
3232
|
if (!existsSync8(CREDENTIALS_PATH))
|
|
2843
3233
|
return null;
|
|
2844
3234
|
try {
|
|
2845
|
-
const creds = JSON.parse(
|
|
3235
|
+
const creds = JSON.parse(readFileSync6(CREDENTIALS_PATH, "utf-8"));
|
|
2846
3236
|
const oauth = creds.claudeAiOauth;
|
|
2847
3237
|
if (!oauth?.accessToken)
|
|
2848
3238
|
return null;
|
|
@@ -2978,7 +3368,7 @@ async function ingestClaudeQuota(db, verbose = false) {
|
|
|
2978
3368
|
|
|
2979
3369
|
// src/ingest/codex-quota.ts
|
|
2980
3370
|
init_database();
|
|
2981
|
-
import { existsSync as existsSync9, readFileSync as
|
|
3371
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
2982
3372
|
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
2983
3373
|
function readCodexAuth() {
|
|
2984
3374
|
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
@@ -2988,7 +3378,7 @@ function readCodexAuth() {
|
|
|
2988
3378
|
if (!existsSync9(authPath))
|
|
2989
3379
|
return null;
|
|
2990
3380
|
try {
|
|
2991
|
-
const auth = JSON.parse(
|
|
3381
|
+
const auth = JSON.parse(readFileSync7(authPath, "utf-8"));
|
|
2992
3382
|
const token = auth.tokens?.access_token;
|
|
2993
3383
|
if (!token)
|
|
2994
3384
|
return null;
|
|
@@ -3122,12 +3512,12 @@ init_database();
|
|
|
3122
3512
|
init_database();
|
|
3123
3513
|
|
|
3124
3514
|
// src/lib/package-metadata.ts
|
|
3125
|
-
import { readFileSync as
|
|
3515
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
3126
3516
|
var cachedMetadata = null;
|
|
3127
3517
|
function getPackageMetadata() {
|
|
3128
3518
|
if (cachedMetadata)
|
|
3129
3519
|
return cachedMetadata;
|
|
3130
|
-
const raw =
|
|
3520
|
+
const raw = readFileSync8(new URL("../../package.json", import.meta.url), "utf8");
|
|
3131
3521
|
const parsed = JSON.parse(raw);
|
|
3132
3522
|
cachedMetadata = {
|
|
3133
3523
|
name: parsed.name ?? "@hasna/economy",
|
|
@@ -3179,44 +3569,27 @@ async function runCloudMigrations(cloud) {
|
|
|
3179
3569
|
await cloud.run(sql);
|
|
3180
3570
|
}
|
|
3181
3571
|
}
|
|
3182
|
-
function isCloudIncrementalEnabled() {
|
|
3183
|
-
return process.env["ECONOMY_CLOUD_INCREMENTAL"] === "1" || process.env["ECONOMY_CLOUD_INCREMENTAL"] === "true";
|
|
3184
|
-
}
|
|
3185
3572
|
async function cloudPush(opts) {
|
|
3186
|
-
const { syncPush,
|
|
3573
|
+
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
3187
3574
|
const cloud = await getCloudPg();
|
|
3188
3575
|
const local = new SqliteAdapter(getDbPath());
|
|
3189
3576
|
await runCloudMigrations(cloud);
|
|
3190
3577
|
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
|
-
}
|
|
3578
|
+
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
3579
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3200
3580
|
touchMachineRegistry(local, "push");
|
|
3201
3581
|
local.close();
|
|
3202
3582
|
await cloud.close();
|
|
3203
3583
|
return { rows, machine: getMachineId() };
|
|
3204
3584
|
}
|
|
3205
3585
|
async function cloudPull(opts) {
|
|
3206
|
-
const { syncPull,
|
|
3586
|
+
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
3207
3587
|
const cloud = await getCloudPg();
|
|
3208
3588
|
const local = new SqliteAdapter(getDbPath());
|
|
3209
3589
|
await runCloudMigrations(cloud);
|
|
3210
3590
|
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
|
-
}
|
|
3591
|
+
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
3592
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3220
3593
|
touchMachineRegistry(local, "pull");
|
|
3221
3594
|
local.close();
|
|
3222
3595
|
await cloud.close();
|
|
@@ -3526,6 +3899,7 @@ function createHandler(db) {
|
|
|
3526
3899
|
const project = url.searchParams.get("project") ?? undefined;
|
|
3527
3900
|
const search = url.searchParams.get("search") ?? undefined;
|
|
3528
3901
|
const machine = url.searchParams.get("machine") ?? undefined;
|
|
3902
|
+
const account = url.searchParams.get("account") ?? undefined;
|
|
3529
3903
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
3530
3904
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
3531
3905
|
const since = url.searchParams.get("since") ?? undefined;
|
|
@@ -3536,6 +3910,7 @@ function createHandler(db) {
|
|
|
3536
3910
|
project,
|
|
3537
3911
|
search,
|
|
3538
3912
|
machine,
|
|
3913
|
+
account,
|
|
3539
3914
|
limit,
|
|
3540
3915
|
offset,
|
|
3541
3916
|
since
|
|
@@ -3586,11 +3961,23 @@ function createHandler(db) {
|
|
|
3586
3961
|
return ok(results);
|
|
3587
3962
|
}
|
|
3588
3963
|
if (path === "/api/projects" && method === "GET") {
|
|
3589
|
-
|
|
3964
|
+
const period = url.searchParams.get("period") ?? "all";
|
|
3965
|
+
return ok(queryProjectBreakdown(db, period));
|
|
3966
|
+
}
|
|
3967
|
+
if (path === "/api/accounts" && method === "GET") {
|
|
3968
|
+
const period = url.searchParams.get("period") ?? "all";
|
|
3969
|
+
return ok(queryAccountBreakdown(db, period));
|
|
3590
3970
|
}
|
|
3591
3971
|
if (path === "/api/breakdown" && method === "GET") {
|
|
3592
3972
|
const by = url.searchParams.get("by") ?? "model";
|
|
3593
|
-
|
|
3973
|
+
const period = url.searchParams.get("period") ?? "all";
|
|
3974
|
+
if (by === "project")
|
|
3975
|
+
return ok(queryProjectBreakdown(db, period));
|
|
3976
|
+
if (by === "agent")
|
|
3977
|
+
return ok(queryAgentBreakdown(db, period));
|
|
3978
|
+
if (by === "account")
|
|
3979
|
+
return ok(queryAccountBreakdown(db, period));
|
|
3980
|
+
return ok(queryModelBreakdown(db));
|
|
3594
3981
|
}
|
|
3595
3982
|
if (path === "/api/budgets" && method === "GET") {
|
|
3596
3983
|
return ok(getBudgetStatuses(db));
|