@hasna/economy 0.2.27 → 0.2.29
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 +2 -1
- package/README.md +16 -1
- package/dist/cli/index.js +417 -189
- package/dist/db/database.d.ts +10 -5
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +268 -162
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/mcp/http.d.ts +13 -0
- package/dist/mcp/http.d.ts.map +1 -0
- package/dist/mcp/index.js +846 -670
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/otel/index.js +35 -43
- package/dist/server/index.js +284 -170
- package/dist/server/serve.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -566,6 +566,26 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
566
566
|
}
|
|
567
567
|
return db;
|
|
568
568
|
}
|
|
569
|
+
function quoteSqlIdent(identifier) {
|
|
570
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
571
|
+
}
|
|
572
|
+
function hasColumn(db, table, column) {
|
|
573
|
+
const columns = db.prepare(`PRAGMA table_info(${quoteSqlIdent(table)})`).all();
|
|
574
|
+
return columns.some((c) => c.name === column);
|
|
575
|
+
}
|
|
576
|
+
function addColumnIfMissing(db, table, column, definition) {
|
|
577
|
+
if (hasColumn(db, table, column))
|
|
578
|
+
return false;
|
|
579
|
+
try {
|
|
580
|
+
db.exec(`ALTER TABLE ${quoteSqlIdent(table)} ADD COLUMN ${quoteSqlIdent(column)} ${definition}`);
|
|
581
|
+
return true;
|
|
582
|
+
} catch (error) {
|
|
583
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
584
|
+
if (/duplicate column name/i.test(message))
|
|
585
|
+
return true;
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
569
589
|
function initSchema(db) {
|
|
570
590
|
db.exec(`
|
|
571
591
|
CREATE TABLE IF NOT EXISTS requests (
|
|
@@ -736,59 +756,31 @@ function initSchema(db) {
|
|
|
736
756
|
CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
|
|
737
757
|
CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
|
|
738
758
|
`);
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
743
|
-
}
|
|
744
|
-
if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
|
|
745
|
-
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
|
|
759
|
+
addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
|
|
760
|
+
addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
|
|
761
|
+
if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
|
|
746
762
|
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
747
763
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
if (
|
|
752
|
-
db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
|
|
753
|
-
}
|
|
754
|
-
if (!cols.some((c) => c.name === "attribution_tag")) {
|
|
755
|
-
db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
756
|
-
}
|
|
757
|
-
if (!cols.some((c) => c.name === "updated_at")) {
|
|
758
|
-
db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
764
|
+
addColumnIfMissing(db, "requests", "cache_create_1h_tokens", "INTEGER DEFAULT 0");
|
|
765
|
+
addColumnIfMissing(db, "requests", "cost_basis", `TEXT DEFAULT 'estimated'`);
|
|
766
|
+
addColumnIfMissing(db, "requests", "attribution_tag", `TEXT DEFAULT ''`);
|
|
767
|
+
if (addColumnIfMissing(db, "requests", "updated_at", `TEXT DEFAULT ''`)) {
|
|
759
768
|
db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
|
|
760
769
|
}
|
|
761
|
-
|
|
762
|
-
db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
763
|
-
}
|
|
770
|
+
addColumnIfMissing(db, "requests", "synced_at", `TEXT DEFAULT ''`);
|
|
764
771
|
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
765
|
-
|
|
766
|
-
db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
770
|
-
if (!sessionCols.some((c) => c.name === "attribution_tag")) {
|
|
771
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
772
|
+
addColumnIfMissing(db, "requests", column, `TEXT DEFAULT ''`);
|
|
772
773
|
}
|
|
773
|
-
|
|
774
|
-
|
|
774
|
+
addColumnIfMissing(db, "sessions", "attribution_tag", `TEXT DEFAULT ''`);
|
|
775
|
+
if (addColumnIfMissing(db, "sessions", "updated_at", `TEXT DEFAULT ''`)) {
|
|
775
776
|
db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
776
777
|
}
|
|
777
|
-
|
|
778
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
779
|
-
}
|
|
778
|
+
addColumnIfMissing(db, "sessions", "synced_at", `TEXT DEFAULT ''`);
|
|
780
779
|
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
781
|
-
|
|
782
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
|
|
786
|
-
if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
|
|
787
|
-
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
|
|
788
|
-
}
|
|
789
|
-
if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
|
|
790
|
-
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
|
|
780
|
+
addColumnIfMissing(db, "sessions", column, `TEXT DEFAULT ''`);
|
|
791
781
|
}
|
|
782
|
+
addColumnIfMissing(db, "model_pricing", "cache_write_1h_per_1m", "REAL NOT NULL DEFAULT 0");
|
|
783
|
+
addColumnIfMissing(db, "model_pricing", "cache_storage_per_1m_hour", "REAL NOT NULL DEFAULT 0");
|
|
792
784
|
db.exec(`
|
|
793
785
|
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
794
786
|
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
@@ -949,17 +941,22 @@ function querySummary(db, period, machine, allMachines = false) {
|
|
|
949
941
|
const codexTotals = db.prepare(`
|
|
950
942
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
951
943
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
944
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
952
945
|
COUNT(*) as sessions
|
|
953
946
|
FROM sessions
|
|
954
947
|
WHERE ${sWhere}${machineClause}
|
|
955
948
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
956
949
|
`).get();
|
|
957
|
-
const
|
|
950
|
+
const requestSessionCount = db.prepare(`
|
|
951
|
+
SELECT COUNT(DISTINCT session_id) as sessions
|
|
952
|
+
FROM requests
|
|
953
|
+
WHERE ${rWhere}${machineClause}
|
|
954
|
+
`).get();
|
|
958
955
|
return {
|
|
959
956
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
960
|
-
requests: r.requests,
|
|
957
|
+
requests: r.requests + codexTotals.requests,
|
|
961
958
|
tokens: r.tokens + codexTotals.tokens,
|
|
962
|
-
sessions:
|
|
959
|
+
sessions: requestSessionCount.sessions + codexTotals.sessions,
|
|
963
960
|
period
|
|
964
961
|
};
|
|
965
962
|
}
|
|
@@ -974,8 +971,10 @@ function queryModelBreakdown(db) {
|
|
|
974
971
|
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
975
972
|
`).all();
|
|
976
973
|
}
|
|
977
|
-
function queryAgentBreakdown(db, period = "all") {
|
|
974
|
+
function queryAgentBreakdown(db, period = "all", machine) {
|
|
978
975
|
const requestWhere = requestPeriodWhere(period);
|
|
976
|
+
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
977
|
+
const machineParams = machine ? [machine] : [];
|
|
979
978
|
const groups = new Map;
|
|
980
979
|
const requestRows = db.prepare(`
|
|
981
980
|
SELECT agent,
|
|
@@ -991,10 +990,10 @@ function queryAgentBreakdown(db, period = "all") {
|
|
|
991
990
|
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
992
991
|
MAX(timestamp) as last_active
|
|
993
992
|
FROM requests
|
|
994
|
-
WHERE ${requestWhere}
|
|
993
|
+
WHERE ${requestWhere}${machineClause}
|
|
995
994
|
GROUP BY agent
|
|
996
995
|
ORDER BY api_equivalent_usd DESC
|
|
997
|
-
`).all();
|
|
996
|
+
`).all(...machineParams);
|
|
998
997
|
for (const row of requestRows) {
|
|
999
998
|
groups.set(row.agent, row);
|
|
1000
999
|
}
|
|
@@ -1007,10 +1006,10 @@ function queryAgentBreakdown(db, period = "all") {
|
|
|
1007
1006
|
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1008
1007
|
MAX(started_at) as last_active
|
|
1009
1008
|
FROM sessions
|
|
1010
|
-
WHERE ${sessionWhere}
|
|
1009
|
+
WHERE ${sessionWhere}${machineClause}
|
|
1011
1010
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1012
1011
|
GROUP BY agent
|
|
1013
|
-
`).all();
|
|
1012
|
+
`).all(...machineParams);
|
|
1014
1013
|
for (const row of sessionOnlyRows) {
|
|
1015
1014
|
const existing = groups.get(row.agent) ?? {
|
|
1016
1015
|
agent: row.agent,
|
|
@@ -1087,14 +1086,20 @@ function labelForPath(projectPath, projectName) {
|
|
|
1087
1086
|
function groupKeyForPath(projectPath, projectName) {
|
|
1088
1087
|
return labelForPath(projectPath, projectName).trim().toLowerCase();
|
|
1089
1088
|
}
|
|
1090
|
-
function queryProjectBreakdown(db, period = "all") {
|
|
1089
|
+
function queryProjectBreakdown(db, period = "all", machine) {
|
|
1091
1090
|
const requestWhere = requestPeriodWhere(period);
|
|
1092
1091
|
const sessionWhere = sessionPeriodWhere(period);
|
|
1092
|
+
const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
|
|
1093
|
+
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
1094
|
+
const sessionMachineParams = machine ? [machine, machine] : [];
|
|
1095
|
+
const requestMachineParams = machine ? [machine] : [];
|
|
1096
|
+
const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
|
|
1097
|
+
const sessionOnlyMachineParams = machine ? [machine] : [];
|
|
1093
1098
|
const sessions = db.prepare(`
|
|
1094
1099
|
SELECT id, project_path, project_name, total_cost_usd, started_at
|
|
1095
1100
|
FROM sessions
|
|
1096
|
-
WHERE project_path != '' OR project_name != ''
|
|
1097
|
-
`).all();
|
|
1101
|
+
WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
|
|
1102
|
+
`).all(...sessionMachineParams);
|
|
1098
1103
|
const groups = new Map;
|
|
1099
1104
|
for (const s of sessions) {
|
|
1100
1105
|
const label = labelForPath(s.project_path, s.project_name);
|
|
@@ -1120,7 +1125,8 @@ function queryProjectBreakdown(db, period = "all") {
|
|
|
1120
1125
|
FROM requests
|
|
1121
1126
|
WHERE session_id IN (${placeholders})
|
|
1122
1127
|
AND ${requestWhere}
|
|
1123
|
-
|
|
1128
|
+
${requestMachineClause}
|
|
1129
|
+
`).get(...g.sessionIds, ...requestMachineParams) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
|
|
1124
1130
|
const sessionOnlyStats = placeholders.length ? db.prepare(`
|
|
1125
1131
|
SELECT
|
|
1126
1132
|
COUNT(*) as sessions,
|
|
@@ -1131,8 +1137,9 @@ function queryProjectBreakdown(db, period = "all") {
|
|
|
1131
1137
|
FROM sessions
|
|
1132
1138
|
WHERE id IN (${placeholders})
|
|
1133
1139
|
AND ${sessionWhere}
|
|
1140
|
+
${sessionOnlyMachineClause}
|
|
1134
1141
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1135
|
-
`).get(...g.sessionIds) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
|
|
1142
|
+
`).get(...g.sessionIds, ...sessionOnlyMachineParams) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
|
|
1136
1143
|
const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
|
|
1137
1144
|
if (totalSessions === 0)
|
|
1138
1145
|
continue;
|
|
@@ -1150,107 +1157,167 @@ function queryProjectBreakdown(db, period = "all") {
|
|
|
1150
1157
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1151
1158
|
return result;
|
|
1152
1159
|
}
|
|
1153
|
-
function
|
|
1160
|
+
function normalizeAccountEmail(email) {
|
|
1161
|
+
return (email ?? "").trim().toLowerCase();
|
|
1162
|
+
}
|
|
1163
|
+
function accountIdentityKey(agent, accountKey, accountName, accountEmail) {
|
|
1164
|
+
const identityAgent = (agent || "").trim();
|
|
1165
|
+
const normalizedEmail = normalizeAccountEmail(accountEmail);
|
|
1166
|
+
if (identityAgent && normalizedEmail)
|
|
1167
|
+
return `${identityAgent}:${normalizedEmail}`;
|
|
1168
|
+
if (identityAgent && accountName)
|
|
1169
|
+
return `${identityAgent}:${accountName}`;
|
|
1170
|
+
if (accountKey)
|
|
1171
|
+
return accountKey;
|
|
1172
|
+
return identityAgent ? `${identityAgent}:unknown` : "";
|
|
1173
|
+
}
|
|
1174
|
+
function addAccountBreakdownRow(groups, row, sessionOnly) {
|
|
1175
|
+
const agent = row.agent || row.account_tool;
|
|
1176
|
+
const email = normalizeAccountEmail(row.account_email);
|
|
1177
|
+
const accountName = row.account_name || email || row.account_key;
|
|
1178
|
+
const key = accountIdentityKey(agent, row.account_key, accountName, email);
|
|
1179
|
+
if (!key)
|
|
1180
|
+
return;
|
|
1181
|
+
const group = groups.get(key) ?? {
|
|
1182
|
+
account_key: key,
|
|
1183
|
+
account_tool: agent,
|
|
1184
|
+
account_name: accountName,
|
|
1185
|
+
account_email: email || null,
|
|
1186
|
+
account_source: row.account_source || "unknown",
|
|
1187
|
+
sessionIds: new Set,
|
|
1188
|
+
requests: 0,
|
|
1189
|
+
total_tokens: 0,
|
|
1190
|
+
api_equivalent_usd: 0,
|
|
1191
|
+
metered_api_usd: 0,
|
|
1192
|
+
subscription_included_usd: 0,
|
|
1193
|
+
estimated_usd: 0,
|
|
1194
|
+
unknown_usd: 0,
|
|
1195
|
+
last_active: ""
|
|
1196
|
+
};
|
|
1197
|
+
if (!group.account_email && email)
|
|
1198
|
+
group.account_email = email;
|
|
1199
|
+
if (!group.account_name && accountName)
|
|
1200
|
+
group.account_name = accountName;
|
|
1201
|
+
if ((!group.account_source || group.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
|
|
1202
|
+
group.account_source = row.account_source;
|
|
1203
|
+
}
|
|
1204
|
+
if (row.session_id)
|
|
1205
|
+
group.sessionIds.add(row.session_id);
|
|
1206
|
+
group.requests += row.requests;
|
|
1207
|
+
group.total_tokens += row.total_tokens;
|
|
1208
|
+
group.api_equivalent_usd += row.cost_usd;
|
|
1209
|
+
if (sessionOnly) {
|
|
1210
|
+
group.estimated_usd += row.cost_usd;
|
|
1211
|
+
} else if (row.cost_basis === "metered_api") {
|
|
1212
|
+
group.metered_api_usd += row.cost_usd;
|
|
1213
|
+
} else if (row.cost_basis === "subscription_included") {
|
|
1214
|
+
group.subscription_included_usd += row.cost_usd;
|
|
1215
|
+
} else if (row.cost_basis === "unknown") {
|
|
1216
|
+
group.unknown_usd += row.cost_usd;
|
|
1217
|
+
} else {
|
|
1218
|
+
group.estimated_usd += row.cost_usd;
|
|
1219
|
+
}
|
|
1220
|
+
if (!group.last_active || row.last_active > group.last_active)
|
|
1221
|
+
group.last_active = row.last_active;
|
|
1222
|
+
groups.set(key, group);
|
|
1223
|
+
}
|
|
1224
|
+
function queryAccountBreakdown(db, period = "all", machine) {
|
|
1154
1225
|
const requestWhere = requestPeriodWhere(period);
|
|
1155
1226
|
const sessionWhere = sessionPeriodWhere(period);
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
WHERE account_key != '' OR account_tool != '' OR account_name != '' OR account_email != ''
|
|
1161
|
-
`).all();
|
|
1227
|
+
const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
|
|
1228
|
+
const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
|
|
1229
|
+
const requestMachineParams = machine ? [machine] : [];
|
|
1230
|
+
const sessionMachineParams = machine ? [machine] : [];
|
|
1162
1231
|
const groups = new Map;
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
account_email: group.account_email,
|
|
1229
|
-
account_source: group.account_source,
|
|
1230
|
-
sessions: sessionsTotal,
|
|
1231
|
-
requests: reqStats.requests + sessionOnlyStats.requests,
|
|
1232
|
-
total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
|
|
1233
|
-
api_equivalent_usd: apiEquivalentUsd,
|
|
1234
|
-
billable_usd: billableUsd,
|
|
1235
|
-
metered_api_usd: reqStats.metered_api_usd,
|
|
1236
|
-
subscription_included_usd: reqStats.subscription_included_usd,
|
|
1237
|
-
estimated_usd: estimatedUsd,
|
|
1238
|
-
unknown_usd: reqStats.unknown_usd,
|
|
1239
|
-
cost_usd: apiEquivalentUsd,
|
|
1240
|
-
last_active: lastActive
|
|
1241
|
-
});
|
|
1242
|
-
}
|
|
1232
|
+
const requestRows = db.prepare(`
|
|
1233
|
+
SELECT
|
|
1234
|
+
r.session_id as session_id,
|
|
1235
|
+
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
|
|
1236
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
|
|
1237
|
+
COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
|
|
1238
|
+
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
|
|
1239
|
+
COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
|
|
1240
|
+
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1241
|
+
1 as requests,
|
|
1242
|
+
COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
|
|
1243
|
+
COALESCE(r.cost_usd, 0) as cost_usd,
|
|
1244
|
+
COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
|
|
1245
|
+
r.timestamp as last_active
|
|
1246
|
+
FROM requests r
|
|
1247
|
+
LEFT JOIN sessions s ON s.id = r.session_id
|
|
1248
|
+
WHERE ${requestWhere}${requestMachineClause}
|
|
1249
|
+
AND (
|
|
1250
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
|
|
1251
|
+
OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
|
|
1252
|
+
OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
|
|
1253
|
+
OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
|
|
1254
|
+
)
|
|
1255
|
+
`).all(...requestMachineParams);
|
|
1256
|
+
for (const row of requestRows)
|
|
1257
|
+
addAccountBreakdownRow(groups, row, false);
|
|
1258
|
+
const sessionOnlyRows = db.prepare(`
|
|
1259
|
+
SELECT
|
|
1260
|
+
s.id as session_id,
|
|
1261
|
+
s.agent as agent,
|
|
1262
|
+
s.account_key as account_key,
|
|
1263
|
+
s.account_tool as account_tool,
|
|
1264
|
+
s.account_name as account_name,
|
|
1265
|
+
s.account_email as account_email,
|
|
1266
|
+
COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1267
|
+
COALESCE(s.request_count, 0) as requests,
|
|
1268
|
+
COALESCE(s.total_tokens, 0) as total_tokens,
|
|
1269
|
+
COALESCE(s.total_cost_usd, 0) as cost_usd,
|
|
1270
|
+
'estimated' as cost_basis,
|
|
1271
|
+
s.started_at as last_active
|
|
1272
|
+
FROM sessions s
|
|
1273
|
+
WHERE ${sessionWhere}${sessionMachineClause}
|
|
1274
|
+
AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1275
|
+
AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
|
|
1276
|
+
`).all(...sessionMachineParams);
|
|
1277
|
+
for (const row of sessionOnlyRows)
|
|
1278
|
+
addAccountBreakdownRow(groups, row, true);
|
|
1279
|
+
const result = [...groups.values()].map((group) => ({
|
|
1280
|
+
account_key: group.account_key,
|
|
1281
|
+
account_tool: group.account_tool,
|
|
1282
|
+
account_name: group.account_name,
|
|
1283
|
+
account_email: group.account_email,
|
|
1284
|
+
account_source: group.account_source,
|
|
1285
|
+
sessions: group.sessionIds.size,
|
|
1286
|
+
requests: group.requests,
|
|
1287
|
+
total_tokens: group.total_tokens,
|
|
1288
|
+
api_equivalent_usd: group.api_equivalent_usd,
|
|
1289
|
+
billable_usd: group.metered_api_usd,
|
|
1290
|
+
metered_api_usd: group.metered_api_usd,
|
|
1291
|
+
subscription_included_usd: group.subscription_included_usd,
|
|
1292
|
+
estimated_usd: group.estimated_usd,
|
|
1293
|
+
unknown_usd: group.unknown_usd,
|
|
1294
|
+
cost_usd: group.api_equivalent_usd,
|
|
1295
|
+
last_active: group.last_active
|
|
1296
|
+
}));
|
|
1243
1297
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1244
1298
|
return result;
|
|
1245
1299
|
}
|
|
1246
|
-
function queryDailyBreakdown(db, days = 30) {
|
|
1300
|
+
function queryDailyBreakdown(db, days = 30, machine) {
|
|
1301
|
+
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
1302
|
+
const params = machine ? [`-${days}`, machine] : [`-${days}`];
|
|
1247
1303
|
return db.prepare(`
|
|
1248
1304
|
SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1249
1305
|
FROM requests
|
|
1250
|
-
WHERE timestamp >= DATE('now', ? || ' days')
|
|
1306
|
+
WHERE timestamp >= DATE('now', ? || ' days')${machineClause}
|
|
1251
1307
|
GROUP BY DATE(timestamp), agent
|
|
1252
1308
|
ORDER BY date ASC
|
|
1253
|
-
`).all(
|
|
1309
|
+
`).all(...params);
|
|
1310
|
+
}
|
|
1311
|
+
function queryHourlyBreakdown(db, machine) {
|
|
1312
|
+
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
1313
|
+
const params = machine ? [machine] : [];
|
|
1314
|
+
return db.prepare(`
|
|
1315
|
+
SELECT STRFTIME('%H', timestamp) as hour, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1316
|
+
FROM requests
|
|
1317
|
+
WHERE DATE(timestamp) = DATE('now')${machineClause}
|
|
1318
|
+
GROUP BY STRFTIME('%H', timestamp), agent
|
|
1319
|
+
ORDER BY hour ASC
|
|
1320
|
+
`).all(...params);
|
|
1254
1321
|
}
|
|
1255
1322
|
function upsertProject(db, project) {
|
|
1256
1323
|
db.prepare(`
|
|
@@ -1370,17 +1437,48 @@ function queryBillingSummary(db, period) {
|
|
|
1370
1437
|
}
|
|
1371
1438
|
return { total_usd: total, by_provider };
|
|
1372
1439
|
}
|
|
1373
|
-
function listMachines(db) {
|
|
1440
|
+
function listMachines(db, period = "all") {
|
|
1441
|
+
const rWhere = requestPeriodWhere(period);
|
|
1442
|
+
const sWhere = sessionPeriodWhere(period);
|
|
1374
1443
|
return db.prepare(`
|
|
1444
|
+
WITH request_stats AS (
|
|
1445
|
+
SELECT
|
|
1446
|
+
machine_id,
|
|
1447
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
1448
|
+
COUNT(*) as requests,
|
|
1449
|
+
COALESCE(SUM(cost_usd), 0) as total_cost_usd,
|
|
1450
|
+
MAX(timestamp) as last_active
|
|
1451
|
+
FROM requests
|
|
1452
|
+
WHERE machine_id != ''
|
|
1453
|
+
AND ${rWhere}
|
|
1454
|
+
GROUP BY machine_id
|
|
1455
|
+
),
|
|
1456
|
+
session_only_stats AS (
|
|
1457
|
+
SELECT
|
|
1458
|
+
machine_id,
|
|
1459
|
+
COUNT(*) as sessions,
|
|
1460
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
1461
|
+
COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
|
|
1462
|
+
MAX(started_at) as last_active
|
|
1463
|
+
FROM sessions
|
|
1464
|
+
WHERE machine_id != ''
|
|
1465
|
+
AND ${sWhere}
|
|
1466
|
+
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1467
|
+
GROUP BY machine_id
|
|
1468
|
+
),
|
|
1469
|
+
combined AS (
|
|
1470
|
+
SELECT * FROM request_stats
|
|
1471
|
+
UNION ALL
|
|
1472
|
+
SELECT * FROM session_only_stats
|
|
1473
|
+
)
|
|
1375
1474
|
SELECT
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
COALESCE((
|
|
1379
|
-
COALESCE(SUM(
|
|
1380
|
-
MAX(
|
|
1381
|
-
FROM
|
|
1382
|
-
|
|
1383
|
-
GROUP BY s.machine_id
|
|
1475
|
+
machine_id,
|
|
1476
|
+
COALESCE(SUM(sessions), 0) as sessions,
|
|
1477
|
+
COALESCE(SUM(requests), 0) as requests,
|
|
1478
|
+
COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
|
|
1479
|
+
MAX(last_active) as last_active
|
|
1480
|
+
FROM combined
|
|
1481
|
+
GROUP BY machine_id
|
|
1384
1482
|
ORDER BY total_cost_usd DESC
|
|
1385
1483
|
`).all();
|
|
1386
1484
|
}
|
|
@@ -2255,18 +2353,22 @@ var AGENT_ACCOUNT_TOOLS = {
|
|
|
2255
2353
|
pi: ["pi"],
|
|
2256
2354
|
hermes: ["hermes"]
|
|
2257
2355
|
};
|
|
2258
|
-
function
|
|
2259
|
-
return
|
|
2356
|
+
function normalizeEmail(email) {
|
|
2357
|
+
return (email ?? "").trim().toLowerCase();
|
|
2358
|
+
}
|
|
2359
|
+
function accountKey(tool, name, email) {
|
|
2360
|
+
const normalizedEmail = normalizeEmail(email);
|
|
2361
|
+
return `${tool}:${normalizedEmail || name}`;
|
|
2260
2362
|
}
|
|
2261
2363
|
function normalizeDir(value) {
|
|
2262
2364
|
return value.replace(/\/+$/, "");
|
|
2263
2365
|
}
|
|
2264
2366
|
function fromProfile(profile, source) {
|
|
2265
2367
|
return {
|
|
2266
|
-
account_key: accountKey(profile.tool, profile.name),
|
|
2368
|
+
account_key: accountKey(profile.tool, profile.name, profile.email),
|
|
2267
2369
|
account_tool: profile.tool,
|
|
2268
2370
|
account_name: profile.name,
|
|
2269
|
-
...profile.email ? { account_email: profile.email } : {},
|
|
2371
|
+
...profile.email ? { account_email: normalizeEmail(profile.email) } : {},
|
|
2270
2372
|
account_source: source
|
|
2271
2373
|
};
|
|
2272
2374
|
}
|
|
@@ -2278,10 +2380,12 @@ function fromOverride(raw, agent) {
|
|
|
2278
2380
|
const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
|
|
2279
2381
|
if (!tool || !name)
|
|
2280
2382
|
return null;
|
|
2383
|
+
const email = name.includes("@") ? normalizeEmail(name) : undefined;
|
|
2281
2384
|
return {
|
|
2282
|
-
account_key: accountKey(tool, name),
|
|
2385
|
+
account_key: accountKey(tool, name, email),
|
|
2283
2386
|
account_tool: tool,
|
|
2284
2387
|
account_name: name,
|
|
2388
|
+
...email ? { account_email: email } : {},
|
|
2285
2389
|
account_source: "override"
|
|
2286
2390
|
};
|
|
2287
2391
|
}
|
|
@@ -2294,11 +2398,12 @@ function envOverride(agent, env) {
|
|
|
2294
2398
|
const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
|
|
2295
2399
|
if (!tool || !name)
|
|
2296
2400
|
return null;
|
|
2401
|
+
const email = normalizeEmail(env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"]);
|
|
2297
2402
|
return {
|
|
2298
|
-
account_key: accountKey(tool, name),
|
|
2403
|
+
account_key: accountKey(tool, name, email),
|
|
2299
2404
|
account_tool: tool,
|
|
2300
2405
|
account_name: name,
|
|
2301
|
-
account_email:
|
|
2406
|
+
...email ? { account_email: email } : {},
|
|
2302
2407
|
account_source: "override"
|
|
2303
2408
|
};
|
|
2304
2409
|
}
|
|
@@ -4057,16 +4162,22 @@ function createHandler(db) {
|
|
|
4057
4162
|
}
|
|
4058
4163
|
if (path === "/api/fleet" && method === "GET") {
|
|
4059
4164
|
const period = url.searchParams.get("period") ?? "month";
|
|
4165
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
4060
4166
|
return ok({
|
|
4061
|
-
summary: querySummary(db, period,
|
|
4062
|
-
machines: listMachines(db),
|
|
4167
|
+
summary: querySummary(db, period, machine),
|
|
4168
|
+
machines: listMachines(db, period),
|
|
4063
4169
|
registry: listMachineRegistry(db),
|
|
4064
4170
|
current_machine: getMachineId()
|
|
4065
4171
|
});
|
|
4066
4172
|
}
|
|
4067
4173
|
if (path === "/api/daily" && method === "GET") {
|
|
4068
4174
|
const days = Number(url.searchParams.get("days") ?? 30);
|
|
4069
|
-
|
|
4175
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
4176
|
+
return ok(queryDailyBreakdown(db, days, machine));
|
|
4177
|
+
}
|
|
4178
|
+
if (path === "/api/hourly" && method === "GET") {
|
|
4179
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
4180
|
+
return ok(queryHourlyBreakdown(db, machine));
|
|
4070
4181
|
}
|
|
4071
4182
|
if (path === "/api/sessions" && method === "GET") {
|
|
4072
4183
|
const agent = url.searchParams.get("agent");
|
|
@@ -4136,21 +4247,24 @@ function createHandler(db) {
|
|
|
4136
4247
|
}
|
|
4137
4248
|
if (path === "/api/projects" && method === "GET") {
|
|
4138
4249
|
const period = url.searchParams.get("period") ?? "all";
|
|
4139
|
-
|
|
4250
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
4251
|
+
return ok(queryProjectBreakdown(db, period, machine));
|
|
4140
4252
|
}
|
|
4141
4253
|
if (path === "/api/accounts" && method === "GET") {
|
|
4142
4254
|
const period = url.searchParams.get("period") ?? "all";
|
|
4143
|
-
|
|
4255
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
4256
|
+
return ok(queryAccountBreakdown(db, period, machine));
|
|
4144
4257
|
}
|
|
4145
4258
|
if (path === "/api/breakdown" && method === "GET") {
|
|
4146
4259
|
const by = url.searchParams.get("by") ?? "model";
|
|
4147
4260
|
const period = url.searchParams.get("period") ?? "all";
|
|
4261
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
4148
4262
|
if (by === "project")
|
|
4149
|
-
return ok(queryProjectBreakdown(db, period));
|
|
4263
|
+
return ok(queryProjectBreakdown(db, period, machine));
|
|
4150
4264
|
if (by === "agent")
|
|
4151
|
-
return ok(queryAgentBreakdown(db, period));
|
|
4265
|
+
return ok(queryAgentBreakdown(db, period, machine));
|
|
4152
4266
|
if (by === "account")
|
|
4153
|
-
return ok(queryAccountBreakdown(db, period));
|
|
4267
|
+
return ok(queryAccountBreakdown(db, period, machine));
|
|
4154
4268
|
return ok(queryModelBreakdown(db));
|
|
4155
4269
|
}
|
|
4156
4270
|
if (path === "/api/budgets" && method === "GET") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAuC7D,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAqED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAuC7D,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAqED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA4W/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAcvG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/economy",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.29",
|
|
4
4
|
"description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex, Gemini, OpenCode, Cursor, Pi, and Hermes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|