@hasna/economy 0.2.22 → 0.2.24
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/README.md +28 -7
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/index.js +204 -48
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +67 -38
- package/dist/lib/periods.d.ts +6 -0
- package/dist/lib/periods.d.ts.map +1 -0
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/mcp/index.js +221 -47
- package/dist/server/index.js +209 -46
- package/dist/server/serve.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# @hasna/economy
|
|
2
2
|
|
|
3
|
-
AI coding cost tracker for Claude Code, Takumi, Codex, and
|
|
3
|
+
AI coding cost tracker for Claude Code, Takumi, Codex, Gemini, OpenCode, Cursor, Pi, and Hermes. It ships as a CLI, MCP server, REST API, web dashboard, and native macOS menu bar app.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@hasna/economy)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
10
|
-
- Ingests local Claude Code, Takumi, Codex, and
|
|
10
|
+
- Ingests local Claude Code, Takumi, Codex, Gemini, OpenCode, Cursor, Pi, and Hermes usage.
|
|
11
11
|
- Tracks sessions, requests, projects, machines, models, cache tokens, budgets, goals, and provider billing.
|
|
12
12
|
- Attributes usage to `@hasna/accounts` profiles when agents run under managed account/profile config dirs.
|
|
13
|
+
- Breaks down API-equivalent, metered API, subscription-included, estimated, and unknown cost by account and coding agent.
|
|
13
14
|
- Seeds editable model pricing with input, output, cache-read, 5-minute cache-write, 1-hour cache-write, and context-cache storage rates.
|
|
14
15
|
- Handles tiered pricing such as Gemini long-prompt rates and OpenAI long-context rates.
|
|
15
16
|
- Reconciles estimates against Anthropic, OpenAI, and Gemini billing sources.
|
|
@@ -70,7 +71,7 @@ Gemini settings:
|
|
|
70
71
|
}
|
|
71
72
|
```
|
|
72
73
|
|
|
73
|
-
The MCP server exposes read tools for summaries, sessions, machines, pricing, daily spend, budgets, goals,
|
|
74
|
+
The MCP server exposes read tools for summaries, sessions, machines, pricing, daily spend, budgets, goals, provider billing, usage snapshots, savings, project/account/agent breakdowns, and subscriptions. It also exposes mutation tools for budgets, pricing rows, goals, and subscriptions so coding agents can manage Economy data through the same validated surface as the CLI and REST API.
|
|
74
75
|
|
|
75
76
|
## Ingest
|
|
76
77
|
|
|
@@ -87,6 +88,10 @@ economy sync --claude
|
|
|
87
88
|
economy sync --codex
|
|
88
89
|
economy sync --gemini
|
|
89
90
|
economy sync --takumi
|
|
91
|
+
economy sync --opencode
|
|
92
|
+
economy sync --cursor
|
|
93
|
+
economy sync --pi
|
|
94
|
+
economy sync --hermes
|
|
90
95
|
```
|
|
91
96
|
|
|
92
97
|
Useful repair options:
|
|
@@ -103,6 +108,15 @@ Account attribution is automatic when `@hasna/accounts` has a matching active, a
|
|
|
103
108
|
|
|
104
109
|
Account breakdowns report `api_equivalent_usd` for the API list-price value of the usage, plus `billable_usd`/`metered_api_usd` for known direct API spend and `subscription_included_usd` for usage covered by a subscription.
|
|
105
110
|
|
|
111
|
+
Subscription plans can be configured locally and are used by savings calculations:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
economy subscriptions set --provider cursor --plan pro --fee 20 --included 20 --agent cursor
|
|
115
|
+
economy subscriptions list
|
|
116
|
+
economy savings month
|
|
117
|
+
economy usage month --agent cursor
|
|
118
|
+
```
|
|
119
|
+
|
|
106
120
|
## Pricing
|
|
107
121
|
|
|
108
122
|
Default pricing is seeded into SQLite and can be edited locally:
|
|
@@ -150,7 +164,7 @@ economy config set webhook-url https://example.com/economy-webhook
|
|
|
150
164
|
economy config webhook-test
|
|
151
165
|
```
|
|
152
166
|
|
|
153
|
-
Budgets and goals can be global, project-scoped with `--project`, agent-scoped with `--agent`, or both. Valid agent scopes are `claude`, `takumi`, `codex`, and `
|
|
167
|
+
Budgets and goals can be global, project-scoped with `--project`, agent-scoped with `--agent`, or both. Valid agent scopes are `claude`, `takumi`, `codex`, `gemini`, `opencode`, `cursor`, `pi`, and `hermes`.
|
|
154
168
|
|
|
155
169
|
Budget webhooks fire after sync when the alert threshold is crossed. Failed webhook deliveries are not marked as fired, so the next sync can retry them.
|
|
156
170
|
|
|
@@ -169,7 +183,14 @@ Common endpoints:
|
|
|
169
183
|
- `GET /api/sessions?agent=codex&limit=20`
|
|
170
184
|
- `GET /api/sessions/:id/requests`
|
|
171
185
|
- `GET /api/models`
|
|
172
|
-
- `GET /api/projects`
|
|
186
|
+
- `GET /api/projects?period=month`
|
|
187
|
+
- `GET /api/breakdown?by=agent&period=month`
|
|
188
|
+
- `GET /api/accounts?period=month`
|
|
189
|
+
- `GET /api/usage?period=month`
|
|
190
|
+
- `GET /api/savings?period=month`
|
|
191
|
+
- `GET /api/subscriptions`
|
|
192
|
+
- `POST /api/subscriptions`
|
|
193
|
+
- `DELETE /api/subscriptions/:id`
|
|
173
194
|
- `GET /api/budgets`
|
|
174
195
|
- `POST /api/budgets`
|
|
175
196
|
- `DELETE /api/budgets/:id`
|
|
@@ -183,13 +204,13 @@ Common endpoints:
|
|
|
183
204
|
- `POST /api/sync`
|
|
184
205
|
- `POST /api/billing/sync`
|
|
185
206
|
|
|
186
|
-
Budget and
|
|
207
|
+
Budget, goal, and subscription mutation endpoints validate agent scopes against `claude`, `takumi`, `codex`, `gemini`, `opencode`, `cursor`, `pi`, and `hermes`.
|
|
187
208
|
|
|
188
209
|
The server also serves the built dashboard when `dashboard/dist` is present.
|
|
189
210
|
|
|
190
211
|
## Native macOS Menubar
|
|
191
212
|
|
|
192
|
-
The `menubar/` app is a native SwiftUI `MenuBarExtra` app, not Electron. It targets Swift 5.9+ and macOS 14+, and talks to the REST API exposed by `economy-serve`. The default server URL is `http://127.0.0.1:3456`.
|
|
213
|
+
The `menubar/` app is a native SwiftUI `MenuBarExtra` app, not Electron. It targets Swift 5.9+ and macOS 14+, and talks to the REST API exposed by `economy-serve`. It shows today/week/month spend, token and request counts, top agents, top accounts, top projects, subscription savings, usage snapshots, recent sessions, and fleet status. The default server URL is `http://127.0.0.1:3456`.
|
|
193
214
|
|
|
194
215
|
Build it on macOS:
|
|
195
216
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extras.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/extras.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"extras.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/extras.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AA8CnC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAsR/D;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkC5D"}
|
package/dist/cli/index.js
CHANGED
|
@@ -1106,23 +1106,20 @@ function labelForPath(projectPath, projectName) {
|
|
|
1106
1106
|
return segments[segments.length - 1] ?? projectPath;
|
|
1107
1107
|
}
|
|
1108
1108
|
function queryProjectBreakdown(db, period = "all") {
|
|
1109
|
-
const
|
|
1109
|
+
const requestWhere = requestPeriodWhere(period);
|
|
1110
|
+
const sessionWhere = sessionPeriodWhere(period);
|
|
1110
1111
|
const sessions = db.prepare(`
|
|
1111
1112
|
SELECT id, project_path, project_name, total_cost_usd, started_at
|
|
1112
1113
|
FROM sessions
|
|
1113
|
-
WHERE
|
|
1114
|
-
AND (project_path != '' OR project_name != '')
|
|
1114
|
+
WHERE project_path != '' OR project_name != ''
|
|
1115
1115
|
`).all();
|
|
1116
1116
|
const groups = new Map;
|
|
1117
1117
|
for (const s of sessions) {
|
|
1118
1118
|
const label = labelForPath(s.project_path, s.project_name);
|
|
1119
1119
|
if (!label)
|
|
1120
1120
|
continue;
|
|
1121
|
-
const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path
|
|
1121
|
+
const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path };
|
|
1122
1122
|
g.sessionIds.push(s.id);
|
|
1123
|
-
g.totalCost += s.total_cost_usd || 0;
|
|
1124
|
-
if (!g.lastActive || s.started_at > g.lastActive)
|
|
1125
|
-
g.lastActive = s.started_at;
|
|
1126
1123
|
if (!g.samplePath)
|
|
1127
1124
|
g.samplePath = s.project_path;
|
|
1128
1125
|
groups.set(label, g);
|
|
@@ -1132,32 +1129,52 @@ function queryProjectBreakdown(db, period = "all") {
|
|
|
1132
1129
|
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
1133
1130
|
const reqStats = placeholders.length ? db.prepare(`
|
|
1134
1131
|
SELECT
|
|
1132
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
1135
1133
|
COUNT(*) as requests,
|
|
1136
1134
|
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1137
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
|
|
1138
|
-
|
|
1139
|
-
|
|
1135
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1136
|
+
MAX(timestamp) as last_active
|
|
1137
|
+
FROM requests
|
|
1138
|
+
WHERE session_id IN (${placeholders})
|
|
1139
|
+
AND ${requestWhere}
|
|
1140
|
+
`).get(...g.sessionIds) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
|
|
1141
|
+
const sessionOnlyStats = placeholders.length ? db.prepare(`
|
|
1142
|
+
SELECT
|
|
1143
|
+
COUNT(*) as sessions,
|
|
1144
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
1145
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1146
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1147
|
+
MAX(started_at) as last_active
|
|
1148
|
+
FROM sessions
|
|
1149
|
+
WHERE id IN (${placeholders})
|
|
1150
|
+
AND ${sessionWhere}
|
|
1151
|
+
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1152
|
+
`).get(...g.sessionIds) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
|
|
1153
|
+
const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
|
|
1154
|
+
if (totalSessions === 0)
|
|
1155
|
+
continue;
|
|
1156
|
+
const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
|
|
1140
1157
|
result.push({
|
|
1141
1158
|
project_path: g.samplePath,
|
|
1142
1159
|
project_name: label,
|
|
1143
|
-
sessions:
|
|
1144
|
-
requests: reqStats.requests,
|
|
1145
|
-
total_tokens: reqStats.total_tokens,
|
|
1146
|
-
cost_usd: reqStats.cost_usd
|
|
1147
|
-
last_active:
|
|
1160
|
+
sessions: totalSessions,
|
|
1161
|
+
requests: reqStats.requests + sessionOnlyStats.requests,
|
|
1162
|
+
total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
|
|
1163
|
+
cost_usd: reqStats.cost_usd + sessionOnlyStats.cost_usd,
|
|
1164
|
+
last_active: lastActive
|
|
1148
1165
|
});
|
|
1149
1166
|
}
|
|
1150
1167
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1151
1168
|
return result;
|
|
1152
1169
|
}
|
|
1153
1170
|
function queryAccountBreakdown(db, period = "all") {
|
|
1154
|
-
const
|
|
1171
|
+
const requestWhere = requestPeriodWhere(period);
|
|
1172
|
+
const sessionWhere = sessionPeriodWhere(period);
|
|
1155
1173
|
const sessions = db.prepare(`
|
|
1156
1174
|
SELECT id, account_key, account_tool, account_name, account_email, account_source,
|
|
1157
1175
|
total_cost_usd, total_tokens, request_count, started_at
|
|
1158
1176
|
FROM sessions
|
|
1159
|
-
WHERE
|
|
1160
|
-
AND (account_key != '' OR account_tool != '' OR account_name != '' OR account_email != '')
|
|
1177
|
+
WHERE account_key != '' OR account_tool != '' OR account_name != '' OR account_email != ''
|
|
1161
1178
|
`).all();
|
|
1162
1179
|
const groups = new Map;
|
|
1163
1180
|
for (const session of sessions) {
|
|
@@ -1169,18 +1186,9 @@ function queryAccountBreakdown(db, period = "all") {
|
|
|
1169
1186
|
account_tool: session.account_tool,
|
|
1170
1187
|
account_name: session.account_name,
|
|
1171
1188
|
account_email: session.account_email || null,
|
|
1172
|
-
account_source: session.account_source || "unknown"
|
|
1173
|
-
totalCost: 0,
|
|
1174
|
-
totalTokens: 0,
|
|
1175
|
-
requests: 0,
|
|
1176
|
-
lastActive: ""
|
|
1189
|
+
account_source: session.account_source || "unknown"
|
|
1177
1190
|
};
|
|
1178
1191
|
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
1192
|
groups.set(key, group);
|
|
1185
1193
|
}
|
|
1186
1194
|
const result = [];
|
|
@@ -1188,36 +1196,57 @@ function queryAccountBreakdown(db, period = "all") {
|
|
|
1188
1196
|
const placeholders = group.sessionIds.map(() => "?").join(",");
|
|
1189
1197
|
const reqStats = placeholders ? db.prepare(`
|
|
1190
1198
|
SELECT
|
|
1199
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
1191
1200
|
COUNT(*) as requests,
|
|
1192
1201
|
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1193
1202
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1194
1203
|
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
1195
1204
|
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1196
1205
|
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
|
-
|
|
1206
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
|
|
1207
|
+
MAX(timestamp) as last_active
|
|
1208
|
+
FROM requests
|
|
1209
|
+
WHERE session_id IN (${placeholders})
|
|
1210
|
+
AND ${requestWhere}
|
|
1199
1211
|
`).get(...group.sessionIds) : {
|
|
1212
|
+
sessions: 0,
|
|
1200
1213
|
requests: 0,
|
|
1201
1214
|
cost_usd: 0,
|
|
1202
1215
|
total_tokens: 0,
|
|
1203
1216
|
metered_api_usd: 0,
|
|
1204
1217
|
subscription_included_usd: 0,
|
|
1205
1218
|
estimated_usd: 0,
|
|
1206
|
-
unknown_usd: 0
|
|
1219
|
+
unknown_usd: 0,
|
|
1220
|
+
last_active: null
|
|
1207
1221
|
};
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1210
|
-
|
|
1222
|
+
const sessionOnlyStats = placeholders ? db.prepare(`
|
|
1223
|
+
SELECT
|
|
1224
|
+
COUNT(*) as sessions,
|
|
1225
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
1226
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1227
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1228
|
+
MAX(started_at) as last_active
|
|
1229
|
+
FROM sessions
|
|
1230
|
+
WHERE id IN (${placeholders})
|
|
1231
|
+
AND ${sessionWhere}
|
|
1232
|
+
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1233
|
+
`).get(...group.sessionIds) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
|
|
1234
|
+
const sessionsTotal = reqStats.sessions + sessionOnlyStats.sessions;
|
|
1235
|
+
if (sessionsTotal === 0)
|
|
1236
|
+
continue;
|
|
1237
|
+
const apiEquivalentUsd = reqStats.cost_usd + sessionOnlyStats.cost_usd;
|
|
1238
|
+
const estimatedUsd = reqStats.estimated_usd + sessionOnlyStats.cost_usd;
|
|
1211
1239
|
const billableUsd = reqStats.metered_api_usd;
|
|
1240
|
+
const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
|
|
1212
1241
|
result.push({
|
|
1213
1242
|
account_key: key,
|
|
1214
1243
|
account_tool: group.account_tool,
|
|
1215
1244
|
account_name: group.account_name,
|
|
1216
1245
|
account_email: group.account_email,
|
|
1217
1246
|
account_source: group.account_source,
|
|
1218
|
-
sessions:
|
|
1219
|
-
requests: reqStats.requests
|
|
1220
|
-
total_tokens: reqStats.total_tokens
|
|
1247
|
+
sessions: sessionsTotal,
|
|
1248
|
+
requests: reqStats.requests + sessionOnlyStats.requests,
|
|
1249
|
+
total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
|
|
1221
1250
|
api_equivalent_usd: apiEquivalentUsd,
|
|
1222
1251
|
billable_usd: billableUsd,
|
|
1223
1252
|
metered_api_usd: reqStats.metered_api_usd,
|
|
@@ -1225,7 +1254,7 @@ function queryAccountBreakdown(db, period = "all") {
|
|
|
1225
1254
|
estimated_usd: estimatedUsd,
|
|
1226
1255
|
unknown_usd: reqStats.unknown_usd,
|
|
1227
1256
|
cost_usd: apiEquivalentUsd,
|
|
1228
|
-
last_active:
|
|
1257
|
+
last_active: lastActive
|
|
1229
1258
|
});
|
|
1230
1259
|
}
|
|
1231
1260
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
@@ -1510,6 +1539,10 @@ function prorateMonthlyFee(monthlyFee, period) {
|
|
|
1510
1539
|
return monthlyFee;
|
|
1511
1540
|
}
|
|
1512
1541
|
}
|
|
1542
|
+
function proratedIncludedConsumed(includedUsage, includedCap, period) {
|
|
1543
|
+
const cap = prorateMonthlyFee(includedCap, period);
|
|
1544
|
+
return cap > 0 ? Math.min(includedUsage, cap) : includedUsage;
|
|
1545
|
+
}
|
|
1513
1546
|
function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
|
|
1514
1547
|
return Math.max(0, apiEquivalent - onDemand - subscriptionFee);
|
|
1515
1548
|
}
|
|
@@ -1537,24 +1570,74 @@ function querySavingsSummary(db, period, agent) {
|
|
|
1537
1570
|
AND metric = 'on_demand_usd'
|
|
1538
1571
|
`).get(...params);
|
|
1539
1572
|
const subs = db.prepare(`
|
|
1540
|
-
SELECT
|
|
1573
|
+
SELECT
|
|
1574
|
+
COALESCE(SUM(monthly_fee_usd), 0) as fee,
|
|
1575
|
+
COALESCE(SUM(included_usage_usd), 0) as included
|
|
1541
1576
|
FROM subscriptions
|
|
1542
|
-
WHERE active = 1${agent ? " AND agent = ?" : ""}
|
|
1577
|
+
WHERE active = 1${agent ? " AND (agent = ? OR agent IS NULL)" : ""}
|
|
1543
1578
|
`).get(...agent ? [agent] : []);
|
|
1544
|
-
const subscriptionFee = prorateMonthlyFee(subs.
|
|
1579
|
+
const subscriptionFee = prorateMonthlyFee(subs.fee, period);
|
|
1545
1580
|
const apiEquivalent = apiRow.total + includedRow.total;
|
|
1581
|
+
const includedConsumed = proratedIncludedConsumed(includedRow.total, subs.included, period);
|
|
1546
1582
|
const onDemand = onDemandRow.total;
|
|
1547
1583
|
const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
|
|
1548
1584
|
const byAgent = {};
|
|
1549
1585
|
if (!agent) {
|
|
1586
|
+
const onDemandByAgent = new Map;
|
|
1587
|
+
for (const row of db.prepare(`
|
|
1588
|
+
SELECT agent, COALESCE(SUM(value), 0) as total
|
|
1589
|
+
FROM usage_snapshots
|
|
1590
|
+
WHERE ${subWhere}
|
|
1591
|
+
AND metric = 'on_demand_usd'
|
|
1592
|
+
GROUP BY agent
|
|
1593
|
+
`).all()) {
|
|
1594
|
+
onDemandByAgent.set(row.agent, row.total);
|
|
1595
|
+
}
|
|
1596
|
+
const subscriptionByAgent = new Map;
|
|
1597
|
+
for (const row of db.prepare(`
|
|
1598
|
+
SELECT agent,
|
|
1599
|
+
COALESCE(SUM(monthly_fee_usd), 0) as fee,
|
|
1600
|
+
COALESCE(SUM(included_usage_usd), 0) as included
|
|
1601
|
+
FROM subscriptions
|
|
1602
|
+
WHERE active = 1 AND agent IS NOT NULL
|
|
1603
|
+
GROUP BY agent
|
|
1604
|
+
`).all()) {
|
|
1605
|
+
subscriptionByAgent.set(row.agent, row);
|
|
1606
|
+
}
|
|
1607
|
+
const globalSubs = db.prepare(`
|
|
1608
|
+
SELECT
|
|
1609
|
+
COALESCE(SUM(monthly_fee_usd), 0) as fee,
|
|
1610
|
+
COALESCE(SUM(included_usage_usd), 0) as included
|
|
1611
|
+
FROM subscriptions
|
|
1612
|
+
WHERE active = 1 AND agent IS NULL
|
|
1613
|
+
`).get();
|
|
1614
|
+
const rows = db.prepare(`
|
|
1615
|
+
SELECT agent,
|
|
1616
|
+
COALESCE(SUM(cost_usd), 0) as api_eq,
|
|
1617
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as included
|
|
1618
|
+
FROM requests WHERE ${where}
|
|
1619
|
+
GROUP BY agent
|
|
1620
|
+
`).all();
|
|
1621
|
+
const totalAgentApiEq = rows.reduce((sum, row) => sum + row.api_eq, 0);
|
|
1550
1622
|
for (const row of db.prepare(`
|
|
1551
|
-
SELECT agent,
|
|
1623
|
+
SELECT agent,
|
|
1624
|
+
COALESCE(SUM(cost_usd), 0) as api_eq,
|
|
1625
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as included
|
|
1552
1626
|
FROM requests WHERE ${where}
|
|
1553
1627
|
GROUP BY agent
|
|
1554
1628
|
`).all()) {
|
|
1629
|
+
const agentSubs = subscriptionByAgent.get(row.agent) ?? { fee: 0, included: 0 };
|
|
1630
|
+
const globalShare = totalAgentApiEq > 0 ? row.api_eq / totalAgentApiEq : 0;
|
|
1631
|
+
const agentFee = prorateMonthlyFee(agentSubs.fee + globalSubs.fee * globalShare, period);
|
|
1632
|
+
const agentIncludedCap = agentSubs.included + globalSubs.included * globalShare;
|
|
1633
|
+
const agentIncludedConsumed = proratedIncludedConsumed(row.included, agentIncludedCap, period);
|
|
1634
|
+
const agentOnDemand = onDemandByAgent.get(row.agent) ?? 0;
|
|
1555
1635
|
byAgent[row.agent] = {
|
|
1556
1636
|
api_equivalent_usd: row.api_eq,
|
|
1557
|
-
|
|
1637
|
+
subscription_fee_usd: agentFee,
|
|
1638
|
+
included_consumed_usd: agentIncludedConsumed,
|
|
1639
|
+
on_demand_usd: agentOnDemand,
|
|
1640
|
+
saved_usd: computeSavedUsd(row.api_eq, agentOnDemand, agentFee)
|
|
1558
1641
|
};
|
|
1559
1642
|
}
|
|
1560
1643
|
}
|
|
@@ -1562,7 +1645,7 @@ function querySavingsSummary(db, period, agent) {
|
|
|
1562
1645
|
period,
|
|
1563
1646
|
api_equivalent_usd: apiEquivalent,
|
|
1564
1647
|
subscription_fee_usd: subscriptionFee,
|
|
1565
|
-
included_consumed_usd:
|
|
1648
|
+
included_consumed_usd: includedConsumed,
|
|
1566
1649
|
on_demand_usd: onDemand,
|
|
1567
1650
|
saved_usd: saved,
|
|
1568
1651
|
by_agent: byAgent
|
|
@@ -1640,6 +1723,34 @@ var init_billing_diff = __esm(() => {
|
|
|
1640
1723
|
};
|
|
1641
1724
|
});
|
|
1642
1725
|
|
|
1726
|
+
// src/lib/periods.ts
|
|
1727
|
+
function ymd(date) {
|
|
1728
|
+
return date.toISOString().substring(0, 10);
|
|
1729
|
+
}
|
|
1730
|
+
function usageSnapshotFilterForPeriod(period) {
|
|
1731
|
+
const now = new Date;
|
|
1732
|
+
switch (period) {
|
|
1733
|
+
case "today":
|
|
1734
|
+
return { date: ymd(now) };
|
|
1735
|
+
case "yesterday": {
|
|
1736
|
+
const yesterday = new Date(now);
|
|
1737
|
+
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
|
1738
|
+
return { date: ymd(yesterday) };
|
|
1739
|
+
}
|
|
1740
|
+
case "week": {
|
|
1741
|
+
const weekAgo = new Date(now);
|
|
1742
|
+
weekAgo.setUTCDate(weekAgo.getUTCDate() - 7);
|
|
1743
|
+
return { since: ymd(weekAgo) };
|
|
1744
|
+
}
|
|
1745
|
+
case "month":
|
|
1746
|
+
return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1))) };
|
|
1747
|
+
case "year":
|
|
1748
|
+
return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), 0, 1))) };
|
|
1749
|
+
case "all":
|
|
1750
|
+
return {};
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1643
1754
|
// src/lib/package-metadata.ts
|
|
1644
1755
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1645
1756
|
function getPackageMetadata() {
|
|
@@ -4563,9 +4674,11 @@ function createHandler(db) {
|
|
|
4563
4674
|
if (path === "/api/usage" && method === "GET") {
|
|
4564
4675
|
const period = url.searchParams.get("period") ?? "month";
|
|
4565
4676
|
const agent = url.searchParams.get("agent") ?? undefined;
|
|
4566
|
-
const since = period === "month" ? new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().substring(0, 10) : undefined;
|
|
4567
4677
|
return ok({
|
|
4568
|
-
snapshots: queryUsageSnapshots(db, {
|
|
4678
|
+
snapshots: queryUsageSnapshots(db, {
|
|
4679
|
+
agent: agent && isAgent(agent) ? agent : undefined,
|
|
4680
|
+
...usageSnapshotFilterForPeriod(period)
|
|
4681
|
+
}),
|
|
4569
4682
|
summary: querySummary(db, period, undefined, true)
|
|
4570
4683
|
});
|
|
4571
4684
|
}
|
|
@@ -4574,6 +4687,50 @@ function createHandler(db) {
|
|
|
4574
4687
|
const agent = url.searchParams.get("agent") ?? undefined;
|
|
4575
4688
|
return ok(querySavingsSummary(db, period, agent && isAgent(agent) ? agent : undefined));
|
|
4576
4689
|
}
|
|
4690
|
+
if (path === "/api/subscriptions" && method === "GET") {
|
|
4691
|
+
return ok(listSubscriptions(db));
|
|
4692
|
+
}
|
|
4693
|
+
if (path === "/api/subscriptions" && method === "POST") {
|
|
4694
|
+
const body = await jsonBody(req);
|
|
4695
|
+
if (!body)
|
|
4696
|
+
return err("invalid JSON body");
|
|
4697
|
+
const provider = optionalString(body["provider"])?.trim();
|
|
4698
|
+
const plan = optionalString(body["plan"])?.trim();
|
|
4699
|
+
if (!provider)
|
|
4700
|
+
return err("provider is required");
|
|
4701
|
+
if (!plan)
|
|
4702
|
+
return err("plan is required");
|
|
4703
|
+
const monthlyFee = finiteNumber(body["monthly_fee_usd"] ?? body["fee_usd"] ?? 0);
|
|
4704
|
+
const includedUsage = finiteNumber(body["included_usage_usd"] ?? 0);
|
|
4705
|
+
if (monthlyFee == null || monthlyFee < 0)
|
|
4706
|
+
return err("monthly_fee_usd must be a non-negative number");
|
|
4707
|
+
if (includedUsage == null || includedUsage < 0)
|
|
4708
|
+
return err("included_usage_usd must be a non-negative number");
|
|
4709
|
+
const agent = optionalAgent(body["agent"]);
|
|
4710
|
+
if (agent === undefined)
|
|
4711
|
+
return err(AGENT_ERROR);
|
|
4712
|
+
const now = new Date().toISOString();
|
|
4713
|
+
const subscription = {
|
|
4714
|
+
id: optionalString(body["id"])?.trim() || randomUUID2(),
|
|
4715
|
+
agent,
|
|
4716
|
+
provider,
|
|
4717
|
+
plan,
|
|
4718
|
+
monthly_fee_usd: monthlyFee,
|
|
4719
|
+
included_usage_usd: includedUsage,
|
|
4720
|
+
billing_cycle_start: optionalString(body["billing_cycle_start"]),
|
|
4721
|
+
reset_policy: optionalString(body["reset_policy"]) ?? "monthly",
|
|
4722
|
+
active: body["active"] === false || body["active"] === 0 ? 0 : 1,
|
|
4723
|
+
created_at: optionalString(body["created_at"]) ?? now,
|
|
4724
|
+
updated_at: now
|
|
4725
|
+
};
|
|
4726
|
+
upsertSubscription(db, subscription);
|
|
4727
|
+
return ok(subscription);
|
|
4728
|
+
}
|
|
4729
|
+
const subscriptionMatch = path.match(/^\/api\/subscriptions\/(.+)$/);
|
|
4730
|
+
if (subscriptionMatch && method === "DELETE") {
|
|
4731
|
+
deleteSubscription(db, decodeURIComponent(subscriptionMatch[1]));
|
|
4732
|
+
return ok({ ok: true });
|
|
4733
|
+
}
|
|
4577
4734
|
const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
|
|
4578
4735
|
if (sessionRequestsMatch && method === "GET") {
|
|
4579
4736
|
const sessionId = decodeURIComponent(sessionRequestsMatch[1]);
|
|
@@ -6135,8 +6292,7 @@ function registerExtendedCommands(program) {
|
|
|
6135
6292
|
const db = openDatabase();
|
|
6136
6293
|
const period = parsePeriod(periodArg, "month");
|
|
6137
6294
|
const agent = parseAgent(opts.agent, "--agent");
|
|
6138
|
-
const
|
|
6139
|
-
const snaps = queryUsageSnapshots(db, { agent, since });
|
|
6295
|
+
const snaps = queryUsageSnapshots(db, { agent, ...usageSnapshotFilterForPeriod(period) });
|
|
6140
6296
|
const summary = querySummary(db, period, undefined, true);
|
|
6141
6297
|
if (opts.json) {
|
|
6142
6298
|
console.log(JSON.stringify({ period, agent: agent ?? "all", snapshots: snaps, summary }, null, 2));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAKxD,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,cAAc,EACd,MAAM,EACN,YAAY,EACZ,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,MAAM,EACN,aAAa,EACd,MAAM,mBAAmB,CAAA;AAE1B,wBAAgB,YAAY,IAAI,MAAM,CAKrC;AAED,wBAAgB,UAAU,IAAI,MAAM,CAkBnC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAIlC;AAED,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,UAAQ,GAAG,QAAQ,CAgBxE;AAkRD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAuBrE;AAID,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAkBzE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CA2BnE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,aAAkB,GAAG,cAAc,EAAE,CAuBxF;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,SAAK,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CAKvF;AAID,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,UAAQ,GAAG,WAAW,CA8B7G;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAUlE;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,MAAc,GAAG,cAAc,EAAE,CA0E1F;AA0BD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,MAAc,GAAG,gBAAgB,EAAE,
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"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"}
|