@hasna/economy 0.2.3 → 0.2.4
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/dist/mcp/index.js +31 -1
- package/package.json +1 -1
- package/dist/cli/commands/watch.d.ts +0 -8
- package/dist/cli/commands/watch.d.ts.map +0 -1
- package/dist/cli/index.d.ts +0 -3
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -1725
- package/dist/db/database.d.ts +0 -47
- package/dist/db/database.d.ts.map +0 -1
- package/dist/index.d.ts +0 -6
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -674
- package/dist/ingest/claude.d.ts +0 -7
- package/dist/ingest/claude.d.ts.map +0 -1
- package/dist/ingest/codex.d.ts +0 -7
- package/dist/ingest/codex.d.ts.map +0 -1
- package/dist/lib/config.d.ts +0 -13
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/pricing.d.ts +0 -10
- package/dist/lib/pricing.d.ts.map +0 -1
- package/dist/lib/webhooks.d.ts +0 -3
- package/dist/lib/webhooks.d.ts.map +0 -1
- package/dist/mcp/index.d.ts +0 -3
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/server/index.d.ts +0 -2
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -809
- package/dist/server/serve.d.ts +0 -4
- package/dist/server/serve.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -100
- package/dist/types/index.d.ts.map +0 -1
package/dist/cli/index.js
DELETED
|
@@ -1,1725 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
// @bun
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __export = (target, all) => {
|
|
5
|
-
for (var name in all)
|
|
6
|
-
__defProp(target, name, {
|
|
7
|
-
get: all[name],
|
|
8
|
-
enumerable: true,
|
|
9
|
-
configurable: true,
|
|
10
|
-
set: (newValue) => all[name] = () => newValue
|
|
11
|
-
});
|
|
12
|
-
};
|
|
13
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
14
|
-
var __require = import.meta.require;
|
|
15
|
-
|
|
16
|
-
// src/lib/pricing.ts
|
|
17
|
-
var exports_pricing = {};
|
|
18
|
-
__export(exports_pricing, {
|
|
19
|
-
normalizeModelName: () => normalizeModelName,
|
|
20
|
-
getPricingFromDb: () => getPricingFromDb,
|
|
21
|
-
getPricing: () => getPricing,
|
|
22
|
-
ensurePricingSeeded: () => ensurePricingSeeded,
|
|
23
|
-
computeCostFromDb: () => computeCostFromDb,
|
|
24
|
-
computeCost: () => computeCost,
|
|
25
|
-
DEFAULT_PRICING: () => DEFAULT_PRICING
|
|
26
|
-
});
|
|
27
|
-
function normalizeModelName(raw) {
|
|
28
|
-
return raw.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "").toLowerCase();
|
|
29
|
-
}
|
|
30
|
-
function ensurePricingSeeded(db) {
|
|
31
|
-
seedModelPricing(db, DEFAULT_PRICING);
|
|
32
|
-
}
|
|
33
|
-
function getPricingFromDb(db, model) {
|
|
34
|
-
const normalized = normalizeModelName(model);
|
|
35
|
-
const row = getModelPricing(db, normalized);
|
|
36
|
-
if (row) {
|
|
37
|
-
return {
|
|
38
|
-
inputPer1M: row.input_per_1m,
|
|
39
|
-
outputPer1M: row.output_per_1m,
|
|
40
|
-
cacheReadPer1M: row.cache_read_per_1m,
|
|
41
|
-
cacheWritePer1M: row.cache_write_per_1m
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
|
|
45
|
-
for (const r of allRows) {
|
|
46
|
-
if (normalized.startsWith(r.model)) {
|
|
47
|
-
return { inputPer1M: r.input_per_1m, outputPer1M: r.output_per_1m, cacheReadPer1M: r.cache_read_per_1m, cacheWritePer1M: r.cache_write_per_1m };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
function getPricing(model) {
|
|
53
|
-
const normalized = normalizeModelName(model);
|
|
54
|
-
if (DEFAULT_PRICING[normalized])
|
|
55
|
-
return DEFAULT_PRICING[normalized] ?? null;
|
|
56
|
-
for (const key of Object.keys(DEFAULT_PRICING)) {
|
|
57
|
-
if (normalized.startsWith(key))
|
|
58
|
-
return DEFAULT_PRICING[key] ?? null;
|
|
59
|
-
}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
|
|
63
|
-
const pricing = getPricing(model);
|
|
64
|
-
if (!pricing)
|
|
65
|
-
return 0;
|
|
66
|
-
return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
|
|
67
|
-
}
|
|
68
|
-
function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
|
|
69
|
-
const pricing = getPricingFromDb(db, model) ?? getPricing(model);
|
|
70
|
-
if (!pricing)
|
|
71
|
-
return 0;
|
|
72
|
-
return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
|
|
73
|
-
}
|
|
74
|
-
var DEFAULT_PRICING;
|
|
75
|
-
var init_pricing = __esm(() => {
|
|
76
|
-
init_database();
|
|
77
|
-
DEFAULT_PRICING = {
|
|
78
|
-
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
79
|
-
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
80
|
-
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
81
|
-
"claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
82
|
-
"claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
83
|
-
"claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
84
|
-
"claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
85
|
-
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
|
|
86
|
-
"claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
87
|
-
"claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
|
|
88
|
-
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
89
|
-
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
90
|
-
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
91
|
-
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
92
|
-
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
93
|
-
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
94
|
-
"o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
|
|
95
|
-
o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
|
|
96
|
-
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
97
|
-
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
|
|
98
|
-
};
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// src/db/database.ts
|
|
102
|
-
import { Database } from "bun:sqlite";
|
|
103
|
-
import { existsSync, mkdirSync } from "fs";
|
|
104
|
-
import { homedir } from "os";
|
|
105
|
-
import { join } from "path";
|
|
106
|
-
function getDbPath() {
|
|
107
|
-
return process.env["ECONOMY_DB"] ?? join(homedir(), ".economy", "economy.db");
|
|
108
|
-
}
|
|
109
|
-
function openDatabase(dbPath, skipSeed = false) {
|
|
110
|
-
const path = dbPath ?? getDbPath();
|
|
111
|
-
if (path !== ":memory:") {
|
|
112
|
-
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
113
|
-
if (dir && !existsSync(dir))
|
|
114
|
-
mkdirSync(dir, { recursive: true });
|
|
115
|
-
}
|
|
116
|
-
const db = new Database(path);
|
|
117
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
118
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
119
|
-
initSchema(db);
|
|
120
|
-
if (!skipSeed) {
|
|
121
|
-
Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
|
|
122
|
-
}
|
|
123
|
-
return db;
|
|
124
|
-
}
|
|
125
|
-
function initSchema(db) {
|
|
126
|
-
db.exec(`
|
|
127
|
-
CREATE TABLE IF NOT EXISTS requests (
|
|
128
|
-
id TEXT PRIMARY KEY,
|
|
129
|
-
agent TEXT NOT NULL,
|
|
130
|
-
session_id TEXT NOT NULL,
|
|
131
|
-
model TEXT NOT NULL,
|
|
132
|
-
input_tokens INTEGER DEFAULT 0,
|
|
133
|
-
output_tokens INTEGER DEFAULT 0,
|
|
134
|
-
cache_read_tokens INTEGER DEFAULT 0,
|
|
135
|
-
cache_create_tokens INTEGER DEFAULT 0,
|
|
136
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
137
|
-
duration_ms INTEGER DEFAULT 0,
|
|
138
|
-
timestamp TEXT NOT NULL,
|
|
139
|
-
source_request_id TEXT
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
143
|
-
id TEXT PRIMARY KEY,
|
|
144
|
-
agent TEXT NOT NULL,
|
|
145
|
-
project_path TEXT DEFAULT '',
|
|
146
|
-
project_name TEXT DEFAULT '',
|
|
147
|
-
started_at TEXT NOT NULL,
|
|
148
|
-
ended_at TEXT,
|
|
149
|
-
total_cost_usd REAL DEFAULT 0,
|
|
150
|
-
total_tokens INTEGER DEFAULT 0,
|
|
151
|
-
request_count INTEGER DEFAULT 0
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
155
|
-
id TEXT PRIMARY KEY,
|
|
156
|
-
path TEXT UNIQUE NOT NULL,
|
|
157
|
-
name TEXT NOT NULL,
|
|
158
|
-
description TEXT,
|
|
159
|
-
tags TEXT DEFAULT '[]',
|
|
160
|
-
created_at TEXT NOT NULL
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
CREATE TABLE IF NOT EXISTS budgets (
|
|
164
|
-
id TEXT PRIMARY KEY,
|
|
165
|
-
project_path TEXT,
|
|
166
|
-
agent TEXT,
|
|
167
|
-
period TEXT NOT NULL,
|
|
168
|
-
limit_usd REAL NOT NULL,
|
|
169
|
-
alert_at_percent INTEGER DEFAULT 80,
|
|
170
|
-
created_at TEXT NOT NULL,
|
|
171
|
-
updated_at TEXT NOT NULL
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
CREATE TABLE IF NOT EXISTS ingest_state (
|
|
175
|
-
source TEXT NOT NULL,
|
|
176
|
-
key TEXT NOT NULL,
|
|
177
|
-
value TEXT NOT NULL,
|
|
178
|
-
PRIMARY KEY (source, key)
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
|
|
182
|
-
CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp);
|
|
183
|
-
CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent);
|
|
184
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
|
|
185
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
|
|
186
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
|
|
187
|
-
|
|
188
|
-
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
189
|
-
model TEXT PRIMARY KEY,
|
|
190
|
-
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
191
|
-
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
192
|
-
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
193
|
-
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
194
|
-
updated_at TEXT NOT NULL
|
|
195
|
-
);
|
|
196
|
-
`);
|
|
197
|
-
}
|
|
198
|
-
function periodWhere(period) {
|
|
199
|
-
switch (period) {
|
|
200
|
-
case "today":
|
|
201
|
-
return `DATE(timestamp) = DATE('now')`;
|
|
202
|
-
case "week":
|
|
203
|
-
return `timestamp >= DATE('now', '-7 days')`;
|
|
204
|
-
case "month":
|
|
205
|
-
return `timestamp >= DATE('now', '-30 days')`;
|
|
206
|
-
case "all":
|
|
207
|
-
return "1=1";
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
function sessionPeriodWhere(period) {
|
|
211
|
-
switch (period) {
|
|
212
|
-
case "today":
|
|
213
|
-
return `DATE(started_at) = DATE('now')`;
|
|
214
|
-
case "week":
|
|
215
|
-
return `started_at >= DATE('now', '-7 days')`;
|
|
216
|
-
case "month":
|
|
217
|
-
return `started_at >= DATE('now', '-30 days')`;
|
|
218
|
-
case "all":
|
|
219
|
-
return "1=1";
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
function upsertRequest(db, req) {
|
|
223
|
-
db.prepare(`
|
|
224
|
-
INSERT OR REPLACE INTO requests
|
|
225
|
-
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
226
|
-
cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
|
|
227
|
-
timestamp, source_request_id)
|
|
228
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
229
|
-
`).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.cost_usd, req.duration_ms, req.timestamp, req.source_request_id);
|
|
230
|
-
}
|
|
231
|
-
function upsertSession(db, session) {
|
|
232
|
-
db.prepare(`
|
|
233
|
-
INSERT OR REPLACE INTO sessions
|
|
234
|
-
(id, agent, project_path, project_name, started_at, ended_at,
|
|
235
|
-
total_cost_usd, total_tokens, request_count)
|
|
236
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
237
|
-
`).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);
|
|
238
|
-
}
|
|
239
|
-
function rollupSession(db, sessionId) {
|
|
240
|
-
db.prepare(`
|
|
241
|
-
UPDATE sessions SET
|
|
242
|
-
total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM requests WHERE session_id = ?),
|
|
243
|
-
total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM requests WHERE session_id = ?),
|
|
244
|
-
request_count = (SELECT COUNT(*) FROM requests WHERE session_id = ?),
|
|
245
|
-
ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
|
|
246
|
-
started_at = CASE WHEN started_at = '' OR started_at IS NULL
|
|
247
|
-
THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
|
|
248
|
-
ELSE started_at END
|
|
249
|
-
WHERE id = ?
|
|
250
|
-
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
251
|
-
}
|
|
252
|
-
function querySessions(db, filter = {}) {
|
|
253
|
-
const conditions = [];
|
|
254
|
-
const params = [];
|
|
255
|
-
if (filter.agent) {
|
|
256
|
-
conditions.push("agent = ?");
|
|
257
|
-
params.push(filter.agent);
|
|
258
|
-
}
|
|
259
|
-
if (filter.project) {
|
|
260
|
-
conditions.push("project_path LIKE ?");
|
|
261
|
-
params.push(`%${filter.project}%`);
|
|
262
|
-
}
|
|
263
|
-
if (filter.since) {
|
|
264
|
-
conditions.push("started_at >= ?");
|
|
265
|
-
params.push(filter.since);
|
|
266
|
-
}
|
|
267
|
-
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
268
|
-
const limit = filter.limit ?? 50;
|
|
269
|
-
const offset = filter.offset ?? 0;
|
|
270
|
-
return db.prepare(`
|
|
271
|
-
SELECT * FROM sessions ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?
|
|
272
|
-
`).all(...params, limit, offset);
|
|
273
|
-
}
|
|
274
|
-
function queryTopSessions(db, n = 10, agent) {
|
|
275
|
-
if (agent) {
|
|
276
|
-
return db.prepare(`SELECT * FROM sessions WHERE agent = ? ORDER BY total_cost_usd DESC LIMIT ?`).all(agent, n);
|
|
277
|
-
}
|
|
278
|
-
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
279
|
-
}
|
|
280
|
-
function querySummary(db, period) {
|
|
281
|
-
const rWhere = periodWhere(period);
|
|
282
|
-
const sWhere = sessionPeriodWhere(period);
|
|
283
|
-
const r = db.prepare(`
|
|
284
|
-
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
285
|
-
COUNT(*) as requests,
|
|
286
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
287
|
-
FROM requests WHERE ${rWhere}
|
|
288
|
-
`).get();
|
|
289
|
-
const codexTotals = db.prepare(`
|
|
290
|
-
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
291
|
-
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
292
|
-
COUNT(*) as sessions
|
|
293
|
-
FROM sessions
|
|
294
|
-
WHERE ${sWhere}
|
|
295
|
-
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
296
|
-
`).get();
|
|
297
|
-
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
|
|
298
|
-
return {
|
|
299
|
-
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
300
|
-
requests: r.requests,
|
|
301
|
-
tokens: r.tokens + codexTotals.tokens,
|
|
302
|
-
sessions: sessionCount.sessions,
|
|
303
|
-
period
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
function queryModelBreakdown(db) {
|
|
307
|
-
return db.prepare(`
|
|
308
|
-
SELECT model, agent,
|
|
309
|
-
COUNT(*) as requests,
|
|
310
|
-
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
311
|
-
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
312
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
313
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
314
|
-
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
315
|
-
`).all();
|
|
316
|
-
}
|
|
317
|
-
function queryProjectBreakdown(db) {
|
|
318
|
-
return db.prepare(`
|
|
319
|
-
SELECT project_path, project_name,
|
|
320
|
-
COUNT(*) as sessions,
|
|
321
|
-
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
322
|
-
COALESCE(SUM(request_count), 0) as requests,
|
|
323
|
-
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
324
|
-
MAX(started_at) as last_active
|
|
325
|
-
FROM sessions
|
|
326
|
-
GROUP BY project_path ORDER BY cost_usd DESC
|
|
327
|
-
`).all();
|
|
328
|
-
}
|
|
329
|
-
function queryDailyBreakdown(db, days = 30) {
|
|
330
|
-
return db.prepare(`
|
|
331
|
-
SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
332
|
-
FROM requests
|
|
333
|
-
WHERE timestamp >= DATE('now', ? || ' days')
|
|
334
|
-
GROUP BY DATE(timestamp), agent
|
|
335
|
-
ORDER BY date ASC
|
|
336
|
-
`).all(`-${days}`);
|
|
337
|
-
}
|
|
338
|
-
function upsertProject(db, project) {
|
|
339
|
-
db.prepare(`
|
|
340
|
-
INSERT OR REPLACE INTO projects (id, path, name, description, tags, created_at)
|
|
341
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
342
|
-
`).run(project.id, project.path, project.name, project.description ?? null, JSON.stringify(project.tags), project.created_at);
|
|
343
|
-
}
|
|
344
|
-
function getProject(db, path) {
|
|
345
|
-
const row = db.prepare(`SELECT * FROM projects WHERE path = ?`).get(path);
|
|
346
|
-
if (!row)
|
|
347
|
-
return null;
|
|
348
|
-
return { ...row, tags: JSON.parse(row["tags"] ?? "[]") };
|
|
349
|
-
}
|
|
350
|
-
function listProjects(db) {
|
|
351
|
-
return db.prepare(`SELECT * FROM projects ORDER BY created_at DESC`).all().map((row) => ({ ...row, tags: JSON.parse(row["tags"] ?? "[]") }));
|
|
352
|
-
}
|
|
353
|
-
function deleteProject(db, path) {
|
|
354
|
-
db.prepare(`DELETE FROM projects WHERE path = ?`).run(path);
|
|
355
|
-
}
|
|
356
|
-
function upsertBudget(db, budget) {
|
|
357
|
-
db.prepare(`
|
|
358
|
-
INSERT OR REPLACE INTO budgets
|
|
359
|
-
(id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
360
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
361
|
-
`).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
|
|
362
|
-
}
|
|
363
|
-
function listBudgets(db) {
|
|
364
|
-
return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
|
|
365
|
-
}
|
|
366
|
-
function deleteBudget(db, id) {
|
|
367
|
-
db.prepare(`DELETE FROM budgets WHERE id = ?`).run(id);
|
|
368
|
-
}
|
|
369
|
-
function getBudgetStatuses(db) {
|
|
370
|
-
const budgets = listBudgets(db);
|
|
371
|
-
return budgets.map((b) => {
|
|
372
|
-
const periodStart = b.period === "daily" ? "DATE('now')" : b.period === "weekly" ? "DATE('now', '-7 days')" : "DATE('now', '-30 days')";
|
|
373
|
-
let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
|
|
374
|
-
const params = [];
|
|
375
|
-
if (b.project_path) {
|
|
376
|
-
spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
|
|
377
|
-
params.push(b.project_path);
|
|
378
|
-
}
|
|
379
|
-
if (b.agent) {
|
|
380
|
-
spendQuery += ` AND agent = ?`;
|
|
381
|
-
params.push(b.agent);
|
|
382
|
-
}
|
|
383
|
-
const row = db.prepare(spendQuery).get(...params);
|
|
384
|
-
const spend = row.spend;
|
|
385
|
-
const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
|
|
386
|
-
return {
|
|
387
|
-
...b,
|
|
388
|
-
current_spend_usd: spend,
|
|
389
|
-
percent_used: percent,
|
|
390
|
-
is_over_limit: percent >= 100,
|
|
391
|
-
is_over_alert: percent >= b.alert_at_percent
|
|
392
|
-
};
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
function getIngestState(db, source, key) {
|
|
396
|
-
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
|
|
397
|
-
return row?.value ?? null;
|
|
398
|
-
}
|
|
399
|
-
function setIngestState(db, source, key, value) {
|
|
400
|
-
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
401
|
-
}
|
|
402
|
-
function queryRequestsSince(db, since) {
|
|
403
|
-
return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
|
|
404
|
-
}
|
|
405
|
-
function upsertModelPricing(db, p) {
|
|
406
|
-
db.prepare(`
|
|
407
|
-
INSERT OR REPLACE INTO model_pricing
|
|
408
|
-
(model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
|
|
409
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
410
|
-
`).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.updated_at);
|
|
411
|
-
}
|
|
412
|
-
function getModelPricing(db, model) {
|
|
413
|
-
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
414
|
-
}
|
|
415
|
-
function listModelPricing(db) {
|
|
416
|
-
return db.prepare(`SELECT * FROM model_pricing ORDER BY model ASC`).all();
|
|
417
|
-
}
|
|
418
|
-
function deleteModelPricing(db, model) {
|
|
419
|
-
db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
|
|
420
|
-
}
|
|
421
|
-
function seedModelPricing(db, defaults) {
|
|
422
|
-
const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
|
|
423
|
-
if (existing.count > 0)
|
|
424
|
-
return;
|
|
425
|
-
const now = new Date().toISOString();
|
|
426
|
-
for (const [model, p] of Object.entries(defaults)) {
|
|
427
|
-
upsertModelPricing(db, {
|
|
428
|
-
model,
|
|
429
|
-
input_per_1m: p.inputPer1M,
|
|
430
|
-
output_per_1m: p.outputPer1M,
|
|
431
|
-
cache_read_per_1m: p.cacheReadPer1M,
|
|
432
|
-
cache_write_per_1m: p.cacheWritePer1M,
|
|
433
|
-
updated_at: now
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
var init_database = () => {};
|
|
438
|
-
|
|
439
|
-
// src/ingest/claude.ts
|
|
440
|
-
import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
|
|
441
|
-
import { homedir as homedir2 } from "os";
|
|
442
|
-
import { join as join2, basename } from "path";
|
|
443
|
-
function dirNameToPath(dirName) {
|
|
444
|
-
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
445
|
-
}
|
|
446
|
-
function collectJsonlFiles(projectDir) {
|
|
447
|
-
const files = [];
|
|
448
|
-
function walk(dir) {
|
|
449
|
-
try {
|
|
450
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
451
|
-
if (entry.isDirectory())
|
|
452
|
-
walk(join2(dir, entry.name));
|
|
453
|
-
else if (entry.name.endsWith(".jsonl"))
|
|
454
|
-
files.push(join2(dir, entry.name));
|
|
455
|
-
}
|
|
456
|
-
} catch {}
|
|
457
|
-
}
|
|
458
|
-
walk(projectDir);
|
|
459
|
-
return files;
|
|
460
|
-
}
|
|
461
|
-
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
462
|
-
if (!existsSync2(PROJECTS_DIR)) {
|
|
463
|
-
if (verbose)
|
|
464
|
-
console.log("Claude projects dir not found:", PROJECTS_DIR);
|
|
465
|
-
return { files: 0, requests: 0, sessions: 0 };
|
|
466
|
-
}
|
|
467
|
-
let totalFiles = 0;
|
|
468
|
-
let totalRequests = 0;
|
|
469
|
-
const touchedSessions = new Set;
|
|
470
|
-
const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
471
|
-
for (const projectDirEntry of projectDirs) {
|
|
472
|
-
const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
|
|
473
|
-
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
474
|
-
const projectName = basename(projectPath);
|
|
475
|
-
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
476
|
-
for (const filePath of jsonlFiles) {
|
|
477
|
-
const stateKey = filePath.replace(PROJECTS_DIR, "");
|
|
478
|
-
let fileMtime = "0";
|
|
479
|
-
try {
|
|
480
|
-
fileMtime = statSync(filePath).mtimeMs.toString();
|
|
481
|
-
} catch {
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
const processed = getIngestState(db, "claude", stateKey);
|
|
485
|
-
if (processed === fileMtime)
|
|
486
|
-
continue;
|
|
487
|
-
let lines;
|
|
488
|
-
try {
|
|
489
|
-
lines = readFileSync(filePath, "utf-8").split(`
|
|
490
|
-
`).filter((l) => l.trim());
|
|
491
|
-
} catch {
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
const fileBasename = basename(filePath, ".jsonl");
|
|
495
|
-
const isUuid = /^[0-9a-f-]{36}$/.test(fileBasename);
|
|
496
|
-
let sessionId = isUuid ? fileBasename : fileBasename.replace(/^agent-/, "");
|
|
497
|
-
let sessionCwd = projectPath;
|
|
498
|
-
for (const line of lines) {
|
|
499
|
-
let entry;
|
|
500
|
-
try {
|
|
501
|
-
entry = JSON.parse(line);
|
|
502
|
-
} catch {
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
if (entry.sessionId)
|
|
506
|
-
sessionId = entry.sessionId;
|
|
507
|
-
if (entry.cwd)
|
|
508
|
-
sessionCwd = entry.cwd;
|
|
509
|
-
if (entry.message?.role !== "assistant")
|
|
510
|
-
continue;
|
|
511
|
-
const usage = entry.message.usage;
|
|
512
|
-
if (!usage)
|
|
513
|
-
continue;
|
|
514
|
-
const model = entry.message.model;
|
|
515
|
-
if (!model)
|
|
516
|
-
continue;
|
|
517
|
-
const inputTokens = usage.input_tokens ?? 0;
|
|
518
|
-
const outputTokens = usage.output_tokens ?? 0;
|
|
519
|
-
const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
|
|
520
|
-
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
521
|
-
const timestamp = entry.timestamp ?? new Date().toISOString();
|
|
522
|
-
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
523
|
-
continue;
|
|
524
|
-
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
525
|
-
const reqId = `claude-${sessionId}-${timestamp}`;
|
|
526
|
-
upsertRequest(db, {
|
|
527
|
-
id: reqId,
|
|
528
|
-
agent: "claude",
|
|
529
|
-
session_id: sessionId,
|
|
530
|
-
model,
|
|
531
|
-
input_tokens: inputTokens,
|
|
532
|
-
output_tokens: outputTokens,
|
|
533
|
-
cache_read_tokens: cacheReadTokens,
|
|
534
|
-
cache_create_tokens: cacheWriteTokens,
|
|
535
|
-
cost_usd: costUsd,
|
|
536
|
-
duration_ms: 0,
|
|
537
|
-
timestamp,
|
|
538
|
-
source_request_id: reqId
|
|
539
|
-
});
|
|
540
|
-
if (!touchedSessions.has(sessionId)) {
|
|
541
|
-
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
542
|
-
if (!existing) {
|
|
543
|
-
const session = {
|
|
544
|
-
id: sessionId,
|
|
545
|
-
agent: "claude",
|
|
546
|
-
project_path: sessionCwd || projectPath,
|
|
547
|
-
project_name: basename(sessionCwd || projectPath),
|
|
548
|
-
started_at: timestamp,
|
|
549
|
-
ended_at: null,
|
|
550
|
-
total_cost_usd: 0,
|
|
551
|
-
total_tokens: 0,
|
|
552
|
-
request_count: 0
|
|
553
|
-
};
|
|
554
|
-
upsertSession(db, session);
|
|
555
|
-
}
|
|
556
|
-
touchedSessions.add(sessionId);
|
|
557
|
-
}
|
|
558
|
-
totalRequests++;
|
|
559
|
-
}
|
|
560
|
-
setIngestState(db, "claude", stateKey, fileMtime);
|
|
561
|
-
totalFiles++;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
for (const sessionId of touchedSessions) {
|
|
565
|
-
rollupSession(db, sessionId);
|
|
566
|
-
}
|
|
567
|
-
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
568
|
-
}
|
|
569
|
-
var PROJECTS_DIR;
|
|
570
|
-
var init_claude = __esm(() => {
|
|
571
|
-
init_database();
|
|
572
|
-
init_pricing();
|
|
573
|
-
PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
// src/ingest/codex.ts
|
|
577
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
578
|
-
import { homedir as homedir3 } from "os";
|
|
579
|
-
import { join as join3, basename as basename2 } from "path";
|
|
580
|
-
import { Database as Database2 } from "bun:sqlite";
|
|
581
|
-
function readCodexModel() {
|
|
582
|
-
if (!existsSync3(CODEX_CONFIG_PATH))
|
|
583
|
-
return "gpt-5.3-codex";
|
|
584
|
-
try {
|
|
585
|
-
const content = readFileSync2(CODEX_CONFIG_PATH, "utf-8");
|
|
586
|
-
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
587
|
-
return match?.[1] ?? "gpt-5.3-codex";
|
|
588
|
-
} catch {
|
|
589
|
-
return "gpt-5.3-codex";
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
async function ingestCodex(db, verbose = false) {
|
|
593
|
-
if (!existsSync3(CODEX_DB_PATH)) {
|
|
594
|
-
if (verbose)
|
|
595
|
-
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
596
|
-
return { sessions: 0 };
|
|
597
|
-
}
|
|
598
|
-
const model = readCodexModel();
|
|
599
|
-
let codexDb = null;
|
|
600
|
-
let ingested = 0;
|
|
601
|
-
try {
|
|
602
|
-
codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
|
|
603
|
-
const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
|
|
604
|
-
for (const thread of threads) {
|
|
605
|
-
const stateKey = thread.id;
|
|
606
|
-
const processed = getIngestState(db, "codex", stateKey);
|
|
607
|
-
if (processed === "done")
|
|
608
|
-
continue;
|
|
609
|
-
const costUsd = 0;
|
|
610
|
-
const projectPath = thread.cwd ?? "";
|
|
611
|
-
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
612
|
-
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
613
|
-
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
614
|
-
upsertSession(db, {
|
|
615
|
-
id: `codex-${thread.id}`,
|
|
616
|
-
agent: "codex",
|
|
617
|
-
project_path: projectPath,
|
|
618
|
-
project_name: projectName,
|
|
619
|
-
started_at: startedAt,
|
|
620
|
-
ended_at: endedAt,
|
|
621
|
-
total_cost_usd: costUsd,
|
|
622
|
-
total_tokens: thread.tokens_used,
|
|
623
|
-
request_count: 1
|
|
624
|
-
});
|
|
625
|
-
setIngestState(db, "codex", stateKey, "done");
|
|
626
|
-
ingested++;
|
|
627
|
-
if (verbose)
|
|
628
|
-
console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens \u2192 $${costUsd.toFixed(4)}`);
|
|
629
|
-
}
|
|
630
|
-
} finally {
|
|
631
|
-
codexDb?.close();
|
|
632
|
-
}
|
|
633
|
-
return { sessions: ingested };
|
|
634
|
-
}
|
|
635
|
-
var CODEX_DB_PATH, CODEX_CONFIG_PATH;
|
|
636
|
-
var init_codex = __esm(() => {
|
|
637
|
-
init_database();
|
|
638
|
-
CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
639
|
-
CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
// src/lib/config.ts
|
|
643
|
-
var exports_config = {};
|
|
644
|
-
__export(exports_config, {
|
|
645
|
-
setConfigValue: () => setConfigValue,
|
|
646
|
-
saveConfig: () => saveConfig,
|
|
647
|
-
loadConfig: () => loadConfig,
|
|
648
|
-
getConfigValue: () => getConfigValue
|
|
649
|
-
});
|
|
650
|
-
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
651
|
-
import { homedir as homedir4 } from "os";
|
|
652
|
-
import { join as join4 } from "path";
|
|
653
|
-
function loadConfig() {
|
|
654
|
-
try {
|
|
655
|
-
if (existsSync4(CONFIG_PATH)) {
|
|
656
|
-
const raw = readFileSync3(CONFIG_PATH, "utf-8");
|
|
657
|
-
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
658
|
-
}
|
|
659
|
-
} catch {}
|
|
660
|
-
return { ...DEFAULTS };
|
|
661
|
-
}
|
|
662
|
-
function saveConfig(config) {
|
|
663
|
-
const dir = CONFIG_PATH.substring(0, CONFIG_PATH.lastIndexOf("/"));
|
|
664
|
-
if (!existsSync4(dir))
|
|
665
|
-
mkdirSync2(dir, { recursive: true });
|
|
666
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + `
|
|
667
|
-
`);
|
|
668
|
-
}
|
|
669
|
-
function getConfigValue(key) {
|
|
670
|
-
const config = loadConfig();
|
|
671
|
-
return config[key] ?? null;
|
|
672
|
-
}
|
|
673
|
-
function setConfigValue(key, value) {
|
|
674
|
-
const config = loadConfig();
|
|
675
|
-
let parsed = value;
|
|
676
|
-
if (value === "true")
|
|
677
|
-
parsed = true;
|
|
678
|
-
else if (value === "false")
|
|
679
|
-
parsed = false;
|
|
680
|
-
else if (value === "null")
|
|
681
|
-
parsed = null;
|
|
682
|
-
else if (!isNaN(Number(value)))
|
|
683
|
-
parsed = Number(value);
|
|
684
|
-
else if (value.startsWith("[")) {
|
|
685
|
-
try {
|
|
686
|
-
parsed = JSON.parse(value);
|
|
687
|
-
} catch {}
|
|
688
|
-
}
|
|
689
|
-
config[key] = parsed;
|
|
690
|
-
saveConfig(config);
|
|
691
|
-
}
|
|
692
|
-
var CONFIG_PATH, DEFAULTS;
|
|
693
|
-
var init_config = __esm(() => {
|
|
694
|
-
CONFIG_PATH = join4(homedir4(), ".economy", "config.json");
|
|
695
|
-
DEFAULTS = {
|
|
696
|
-
port: 3456,
|
|
697
|
-
"default-period": "today",
|
|
698
|
-
"auto-sync": true,
|
|
699
|
-
"sync-interval": 30,
|
|
700
|
-
"alert-thresholds": [5, 10, 25, 50, 100],
|
|
701
|
-
"webhook-url": null
|
|
702
|
-
};
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
// src/lib/webhooks.ts
|
|
706
|
-
var exports_webhooks = {};
|
|
707
|
-
__export(exports_webhooks, {
|
|
708
|
-
checkAndFireWebhooks: () => checkAndFireWebhooks
|
|
709
|
-
});
|
|
710
|
-
async function checkAndFireWebhooks(db) {
|
|
711
|
-
const config = loadConfig();
|
|
712
|
-
const url = config["webhook-url"];
|
|
713
|
-
if (!url)
|
|
714
|
-
return;
|
|
715
|
-
const statuses = getBudgetStatuses(db);
|
|
716
|
-
for (const b of statuses) {
|
|
717
|
-
if (!b.is_over_alert)
|
|
718
|
-
continue;
|
|
719
|
-
const key = `webhook-budget-${b.id}-${b.period}`;
|
|
720
|
-
const lastFired = getIngestState(db, "webhook", key);
|
|
721
|
-
const pctBucket = Math.floor(b.percent_used / 10) * 10;
|
|
722
|
-
if (lastFired === String(pctBucket))
|
|
723
|
-
continue;
|
|
724
|
-
await fireWebhook(url, {
|
|
725
|
-
event: "budget_alert",
|
|
726
|
-
budget_id: b.id,
|
|
727
|
-
project: b.project_path ?? "global",
|
|
728
|
-
period: b.period,
|
|
729
|
-
spend: b.current_spend_usd,
|
|
730
|
-
limit: b.limit_usd,
|
|
731
|
-
percent: Math.round(b.percent_used * 10) / 10
|
|
732
|
-
});
|
|
733
|
-
setIngestState(db, "webhook", key, String(pctBucket));
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
async function fireWebhook(url, payload) {
|
|
737
|
-
try {
|
|
738
|
-
await fetch(url, {
|
|
739
|
-
method: "POST",
|
|
740
|
-
headers: { "Content-Type": "application/json" },
|
|
741
|
-
body: JSON.stringify(payload),
|
|
742
|
-
signal: AbortSignal.timeout(5000)
|
|
743
|
-
});
|
|
744
|
-
} catch {}
|
|
745
|
-
}
|
|
746
|
-
var init_webhooks = __esm(() => {
|
|
747
|
-
init_config();
|
|
748
|
-
init_database();
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
// src/cli/commands/watch.ts
|
|
752
|
-
var exports_watch = {};
|
|
753
|
-
__export(exports_watch, {
|
|
754
|
-
watchCosts: () => watchCosts
|
|
755
|
-
});
|
|
756
|
-
import chalk from "chalk";
|
|
757
|
-
function fmt(usd) {
|
|
758
|
-
return chalk.green(`$${usd.toFixed(4)}`);
|
|
759
|
-
}
|
|
760
|
-
function notify(title, body) {
|
|
761
|
-
try {
|
|
762
|
-
const { execSync } = __require("child_process");
|
|
763
|
-
execSync(`osascript -e 'display notification "${body.replace(/'/g, "")}" with title "${title.replace(/'/g, "")}"'`, { stdio: "ignore" });
|
|
764
|
-
} catch {}
|
|
765
|
-
}
|
|
766
|
-
function renderHeader(todayUsd, weekUsd) {
|
|
767
|
-
process.stdout.write("\x1B[H\x1B[2J");
|
|
768
|
-
console.log(chalk.bold.cyan(" economy watch") + chalk.dim(" \u2014 live cost stream"));
|
|
769
|
-
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
770
|
-
console.log(` Today: ${fmt(todayUsd)} Week: ${fmt(weekUsd)}`);
|
|
771
|
-
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
772
|
-
console.log(chalk.dim(" [agent] cost model tokens project"));
|
|
773
|
-
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
774
|
-
}
|
|
775
|
-
async function watchCosts(opts) {
|
|
776
|
-
const db = openDatabase();
|
|
777
|
-
let lastCheck = new Date(Date.now() - opts.interval * 1000).toISOString();
|
|
778
|
-
const lines = [];
|
|
779
|
-
const MAX_LINES = 20;
|
|
780
|
-
const initialSummaryToday = querySummary(db, "today");
|
|
781
|
-
const initialSummaryWeek = querySummary(db, "week");
|
|
782
|
-
renderHeader(initialSummaryToday.total_usd, initialSummaryWeek.total_usd);
|
|
783
|
-
console.log(chalk.dim(`
|
|
784
|
-
Polling every ${opts.interval}s \u2014 Ctrl+C to exit
|
|
785
|
-
`));
|
|
786
|
-
async function poll() {
|
|
787
|
-
const now = new Date().toISOString();
|
|
788
|
-
await ingestClaude(db);
|
|
789
|
-
await ingestCodex(db);
|
|
790
|
-
const newRequests = queryRequestsSince(db, lastCheck);
|
|
791
|
-
lastCheck = now;
|
|
792
|
-
for (const req of newRequests) {
|
|
793
|
-
if (opts.agent && req.agent !== opts.agent)
|
|
794
|
-
continue;
|
|
795
|
-
const agentLabel = req.agent === "claude" ? chalk.blue("[claude]") : chalk.yellow("[codex] ");
|
|
796
|
-
const tokens = req.input_tokens + req.output_tokens;
|
|
797
|
-
const tokStr = tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
|
|
798
|
-
const line = ` ${agentLabel} ${fmt(req.cost_usd).padEnd(14)}${req.model.substring(0, 24).padEnd(26)}${tokStr.padEnd(10)}${req.session_id.substring(0, 12)}`;
|
|
799
|
-
lines.push(line);
|
|
800
|
-
if (lines.length > MAX_LINES)
|
|
801
|
-
lines.shift();
|
|
802
|
-
if (req.cost_usd > 1) {
|
|
803
|
-
notify("economy: high cost", `$${req.cost_usd.toFixed(2)} on ${req.model}`);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
const today = querySummary(db, "today");
|
|
807
|
-
const week = querySummary(db, "week");
|
|
808
|
-
renderHeader(today.total_usd, week.total_usd);
|
|
809
|
-
for (const line of lines)
|
|
810
|
-
console.log(line);
|
|
811
|
-
if (lines.length === 0)
|
|
812
|
-
console.log(chalk.dim(" Waiting for new requests..."));
|
|
813
|
-
console.log(chalk.dim(`
|
|
814
|
-
Last updated: ${new Date().toLocaleTimeString()} \u2014 polling every ${opts.interval}s \u2014 Ctrl+C to exit`));
|
|
815
|
-
}
|
|
816
|
-
await poll();
|
|
817
|
-
const timer = setInterval(poll, opts.interval * 1000);
|
|
818
|
-
process.on("SIGINT", () => {
|
|
819
|
-
clearInterval(timer);
|
|
820
|
-
console.log(chalk.dim(`
|
|
821
|
-
|
|
822
|
-
Stopped watching.`));
|
|
823
|
-
process.exit(0);
|
|
824
|
-
});
|
|
825
|
-
await new Promise(() => {});
|
|
826
|
-
}
|
|
827
|
-
var init_watch = __esm(() => {
|
|
828
|
-
init_database();
|
|
829
|
-
init_claude();
|
|
830
|
-
init_codex();
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
// src/server/serve.ts
|
|
834
|
-
import { randomUUID } from "crypto";
|
|
835
|
-
function json(data, status = 200) {
|
|
836
|
-
return new Response(JSON.stringify(data), {
|
|
837
|
-
status,
|
|
838
|
-
headers: { "Content-Type": "application/json", ...CORS }
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
function ok(data, meta) {
|
|
842
|
-
return json({ data, meta: meta ?? {} });
|
|
843
|
-
}
|
|
844
|
-
function err(message, status = 400) {
|
|
845
|
-
return json({ error: message }, status);
|
|
846
|
-
}
|
|
847
|
-
function applyFields(obj, fields) {
|
|
848
|
-
if (!fields || fields.length === 0)
|
|
849
|
-
return obj;
|
|
850
|
-
return Object.fromEntries(fields.map((f) => [f, obj[f] ?? null]));
|
|
851
|
-
}
|
|
852
|
-
function createHandler(db) {
|
|
853
|
-
return async function handler(req) {
|
|
854
|
-
const url = new URL(req.url);
|
|
855
|
-
const path = url.pathname;
|
|
856
|
-
const method = req.method;
|
|
857
|
-
if (method === "OPTIONS")
|
|
858
|
-
return new Response(null, { status: 204, headers: CORS });
|
|
859
|
-
if (path === "/health")
|
|
860
|
-
return ok({ status: "ok", ts: new Date().toISOString() });
|
|
861
|
-
if (path === "/api/summary" && method === "GET") {
|
|
862
|
-
const period = url.searchParams.get("period") ?? "today";
|
|
863
|
-
return ok(querySummary(db, period));
|
|
864
|
-
}
|
|
865
|
-
if (path === "/api/daily" && method === "GET") {
|
|
866
|
-
const days = Number(url.searchParams.get("days") ?? 30);
|
|
867
|
-
return ok(queryDailyBreakdown(db, days));
|
|
868
|
-
}
|
|
869
|
-
if (path === "/api/sessions" && method === "GET") {
|
|
870
|
-
const agent = url.searchParams.get("agent");
|
|
871
|
-
const project = url.searchParams.get("project") ?? undefined;
|
|
872
|
-
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
873
|
-
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
874
|
-
const since = url.searchParams.get("since") ?? undefined;
|
|
875
|
-
const fieldsParam = url.searchParams.get("fields");
|
|
876
|
-
const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
877
|
-
const sessions = querySessions(db, { agent: agent ?? undefined, project, limit, offset, since });
|
|
878
|
-
return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
|
|
879
|
-
}
|
|
880
|
-
if (path === "/api/top" && method === "GET") {
|
|
881
|
-
const n = Number(url.searchParams.get("n") ?? 10);
|
|
882
|
-
const agent = url.searchParams.get("agent") ?? undefined;
|
|
883
|
-
return ok(queryTopSessions(db, n, agent));
|
|
884
|
-
}
|
|
885
|
-
if (path === "/api/models" && method === "GET") {
|
|
886
|
-
return ok(queryModelBreakdown(db));
|
|
887
|
-
}
|
|
888
|
-
if (path === "/api/projects" && method === "GET") {
|
|
889
|
-
return ok(queryProjectBreakdown(db));
|
|
890
|
-
}
|
|
891
|
-
if (path === "/api/breakdown" && method === "GET") {
|
|
892
|
-
const by = url.searchParams.get("by") ?? "model";
|
|
893
|
-
return ok(by === "project" ? queryProjectBreakdown(db) : queryModelBreakdown(db));
|
|
894
|
-
}
|
|
895
|
-
if (path === "/api/budgets" && method === "GET") {
|
|
896
|
-
return ok(getBudgetStatuses(db));
|
|
897
|
-
}
|
|
898
|
-
if (path === "/api/budgets" && method === "POST") {
|
|
899
|
-
const body = await req.json();
|
|
900
|
-
const now = new Date().toISOString();
|
|
901
|
-
upsertBudget(db, {
|
|
902
|
-
id: randomUUID(),
|
|
903
|
-
project_path: body["project_path"] ?? null,
|
|
904
|
-
agent: body["agent"] ?? null,
|
|
905
|
-
period: body["period"] ?? "monthly",
|
|
906
|
-
limit_usd: Number(body["limit_usd"]),
|
|
907
|
-
alert_at_percent: Number(body["alert_at_percent"] ?? 80),
|
|
908
|
-
created_at: now,
|
|
909
|
-
updated_at: now
|
|
910
|
-
});
|
|
911
|
-
return ok({ ok: true });
|
|
912
|
-
}
|
|
913
|
-
const budgetMatch = path.match(/^\/api\/budgets\/(.+)$/);
|
|
914
|
-
if (budgetMatch && method === "DELETE") {
|
|
915
|
-
deleteBudget(db, budgetMatch[1]);
|
|
916
|
-
return ok({ ok: true });
|
|
917
|
-
}
|
|
918
|
-
if (path === "/api/project-registry" && method === "GET") {
|
|
919
|
-
return ok(listProjects(db));
|
|
920
|
-
}
|
|
921
|
-
if (path === "/api/project-registry" && method === "POST") {
|
|
922
|
-
const body = await req.json();
|
|
923
|
-
const { basename: basename3 } = await import("path");
|
|
924
|
-
const projPath = body["path"];
|
|
925
|
-
upsertProject(db, {
|
|
926
|
-
id: randomUUID(),
|
|
927
|
-
path: projPath,
|
|
928
|
-
name: body["name"] ?? basename3(projPath),
|
|
929
|
-
description: body["description"] ?? null,
|
|
930
|
-
tags: body["tags"] ?? [],
|
|
931
|
-
created_at: new Date().toISOString()
|
|
932
|
-
});
|
|
933
|
-
return ok({ ok: true });
|
|
934
|
-
}
|
|
935
|
-
const projMatch = path.match(/^\/api\/project-registry\/(.+)$/);
|
|
936
|
-
if (projMatch && method === "DELETE") {
|
|
937
|
-
deleteProject(db, decodeURIComponent(projMatch[1]));
|
|
938
|
-
return ok({ ok: true });
|
|
939
|
-
}
|
|
940
|
-
if (path === "/api/pricing" && method === "GET") {
|
|
941
|
-
return ok(listModelPricing(db));
|
|
942
|
-
}
|
|
943
|
-
if (path === "/api/pricing" && method === "POST") {
|
|
944
|
-
const body = await req.json();
|
|
945
|
-
upsertModelPricing(db, {
|
|
946
|
-
model: body["model"],
|
|
947
|
-
input_per_1m: Number(body["input_per_1m"]),
|
|
948
|
-
output_per_1m: Number(body["output_per_1m"]),
|
|
949
|
-
cache_read_per_1m: Number(body["cache_read_per_1m"] ?? 0),
|
|
950
|
-
cache_write_per_1m: Number(body["cache_write_per_1m"] ?? 0),
|
|
951
|
-
updated_at: new Date().toISOString()
|
|
952
|
-
});
|
|
953
|
-
return ok({ ok: true });
|
|
954
|
-
}
|
|
955
|
-
const pricingMatch = path.match(/^\/api\/pricing\/(.+)$/);
|
|
956
|
-
if (pricingMatch && method === "DELETE") {
|
|
957
|
-
deleteModelPricing(db, decodeURIComponent(pricingMatch[1]));
|
|
958
|
-
return ok({ ok: true });
|
|
959
|
-
}
|
|
960
|
-
if (path === "/api/sync" && method === "POST") {
|
|
961
|
-
const body = await req.json().catch(() => ({}));
|
|
962
|
-
const sources = body["sources"] ?? "all";
|
|
963
|
-
const results = {};
|
|
964
|
-
if (sources === "all" || sources === "claude")
|
|
965
|
-
results["claude"] = await ingestClaude(db);
|
|
966
|
-
if (sources === "all" || sources === "codex")
|
|
967
|
-
results["codex"] = await ingestCodex(db);
|
|
968
|
-
return ok(results);
|
|
969
|
-
}
|
|
970
|
-
return err("Not found", 404);
|
|
971
|
-
};
|
|
972
|
-
}
|
|
973
|
-
function startServer(port = 3456) {
|
|
974
|
-
const db = openDatabase();
|
|
975
|
-
ensurePricingSeeded(db);
|
|
976
|
-
const apiHandler = createHandler(db);
|
|
977
|
-
const dashboardDir = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
978
|
-
Bun.serve({
|
|
979
|
-
port,
|
|
980
|
-
async fetch(req) {
|
|
981
|
-
const url = new URL(req.url);
|
|
982
|
-
if (url.pathname.startsWith("/api") || url.pathname === "/health") {
|
|
983
|
-
return apiHandler(req);
|
|
984
|
-
}
|
|
985
|
-
try {
|
|
986
|
-
const { existsSync: existsSync5 } = await import("fs");
|
|
987
|
-
if (existsSync5(dashboardDir)) {
|
|
988
|
-
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
989
|
-
const fullPath = dashboardDir + filePath;
|
|
990
|
-
if (existsSync5(fullPath)) {
|
|
991
|
-
return new Response(Bun.file(fullPath));
|
|
992
|
-
}
|
|
993
|
-
const indexPath = dashboardDir + "/index.html";
|
|
994
|
-
if (existsSync5(indexPath)) {
|
|
995
|
-
return new Response(Bun.file(indexPath));
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
} catch {}
|
|
999
|
-
return apiHandler(req);
|
|
1000
|
-
}
|
|
1001
|
-
});
|
|
1002
|
-
console.log(`economy-serve listening on http://localhost:${port}`);
|
|
1003
|
-
}
|
|
1004
|
-
var CORS;
|
|
1005
|
-
var init_serve = __esm(() => {
|
|
1006
|
-
init_database();
|
|
1007
|
-
init_claude();
|
|
1008
|
-
init_codex();
|
|
1009
|
-
init_pricing();
|
|
1010
|
-
CORS = {
|
|
1011
|
-
"Access-Control-Allow-Origin": "*",
|
|
1012
|
-
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
|
1013
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
1014
|
-
};
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
// src/server/index.ts
|
|
1018
|
-
var exports_server = {};
|
|
1019
|
-
var port;
|
|
1020
|
-
var init_server = __esm(() => {
|
|
1021
|
-
init_serve();
|
|
1022
|
-
port = Number(process.env["ECONOMY_PORT"] ?? 3456);
|
|
1023
|
-
startServer(port);
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
// src/cli/index.ts
|
|
1027
|
-
init_database();
|
|
1028
|
-
init_claude();
|
|
1029
|
-
init_codex();
|
|
1030
|
-
init_pricing();
|
|
1031
|
-
import { Command } from "commander";
|
|
1032
|
-
import chalk2 from "chalk";
|
|
1033
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
1034
|
-
import { execSync } from "child_process";
|
|
1035
|
-
var program = new Command;
|
|
1036
|
-
program.name("economy").description("AI coding cost tracker \u2014 Claude Code, Codex, and Gemini").version("0.2.2");
|
|
1037
|
-
async function autoSync() {
|
|
1038
|
-
const db = openDatabase();
|
|
1039
|
-
ensurePricingSeeded(db);
|
|
1040
|
-
await ingestClaude(db);
|
|
1041
|
-
await ingestCodex(db);
|
|
1042
|
-
}
|
|
1043
|
-
function sparkline(values) {
|
|
1044
|
-
const chars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
1045
|
-
if (values.length === 0)
|
|
1046
|
-
return "";
|
|
1047
|
-
const max = Math.max(...values);
|
|
1048
|
-
if (max === 0)
|
|
1049
|
-
return chars[0].repeat(values.length);
|
|
1050
|
-
return values.map((v) => chars[Math.min(Math.round(v / max * 7), 7)]).join("");
|
|
1051
|
-
}
|
|
1052
|
-
function fmt2(usd) {
|
|
1053
|
-
let formatted;
|
|
1054
|
-
if (usd >= 0.01) {
|
|
1055
|
-
formatted = "$" + usd.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
1056
|
-
} else {
|
|
1057
|
-
formatted = "$" + usd.toFixed(6);
|
|
1058
|
-
}
|
|
1059
|
-
return chalk2.green(formatted);
|
|
1060
|
-
}
|
|
1061
|
-
function fmtTokens(n) {
|
|
1062
|
-
if (n >= 1e9)
|
|
1063
|
-
return `${(n / 1e9).toFixed(1)}B`;
|
|
1064
|
-
if (n >= 1e6)
|
|
1065
|
-
return `${(n / 1e6).toFixed(1)}M`;
|
|
1066
|
-
if (n >= 1000)
|
|
1067
|
-
return `${(n / 1000).toFixed(1)}k`;
|
|
1068
|
-
return n.toLocaleString("en-US");
|
|
1069
|
-
}
|
|
1070
|
-
function fmtCount(n) {
|
|
1071
|
-
return n.toLocaleString("en-US");
|
|
1072
|
-
}
|
|
1073
|
-
function printTable(headers, rows) {
|
|
1074
|
-
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").replace(/\x1b\[[0-9;]*m/g, "").length)));
|
|
1075
|
-
const sep = widths.map((w) => "\u2500".repeat(w + 2)).join("\u253C");
|
|
1076
|
-
const header = headers.map((h, i) => ` ${h.padEnd(widths[i] ?? 0)} `).join("\u2502");
|
|
1077
|
-
console.log(`\u250C${sep.replace(/\u253C/g, "\u252C")}\u2510`);
|
|
1078
|
-
console.log(`\u2502${header}\u2502`);
|
|
1079
|
-
console.log(`\u251C${sep}\u2524`);
|
|
1080
|
-
for (const row of rows) {
|
|
1081
|
-
const line = row.map((cell, i) => {
|
|
1082
|
-
const plain = cell.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1083
|
-
return ` ${cell}${" ".repeat(Math.max(0, (widths[i] ?? 0) - plain.length))} `;
|
|
1084
|
-
}).join("\u2502");
|
|
1085
|
-
console.log(`\u2502${line}\u2502`);
|
|
1086
|
-
}
|
|
1087
|
-
console.log(`\u2514${sep.replace(/\u253C/g, "\u2534")}\u2518`);
|
|
1088
|
-
}
|
|
1089
|
-
function printSummary(label, period) {
|
|
1090
|
-
const db = openDatabase();
|
|
1091
|
-
ensurePricingSeeded(db);
|
|
1092
|
-
const s = querySummary(db, period);
|
|
1093
|
-
console.log();
|
|
1094
|
-
console.log(chalk2.bold.cyan(` ${label}`));
|
|
1095
|
-
console.log();
|
|
1096
|
-
printTable(["Metric", "Value"], [
|
|
1097
|
-
["Total cost", fmt2(s.total_usd)],
|
|
1098
|
-
["Sessions", chalk2.yellow(fmtCount(s.sessions))],
|
|
1099
|
-
["Requests", chalk2.yellow(fmtCount(s.requests))],
|
|
1100
|
-
["Tokens", chalk2.yellow(fmtTokens(s.tokens))]
|
|
1101
|
-
]);
|
|
1102
|
-
console.log();
|
|
1103
|
-
}
|
|
1104
|
-
program.action(async () => {
|
|
1105
|
-
await autoSync();
|
|
1106
|
-
const db = openDatabase();
|
|
1107
|
-
const t = querySummary(db, "today");
|
|
1108
|
-
const w = querySummary(db, "week");
|
|
1109
|
-
const m = querySummary(db, "month");
|
|
1110
|
-
const projects = queryProjectBreakdown(db).slice(0, 3);
|
|
1111
|
-
const daily = queryDailyBreakdown(db, 14).reduce((acc, d) => {
|
|
1112
|
-
acc[d.date] = (acc[d.date] ?? 0) + d.cost_usd;
|
|
1113
|
-
return acc;
|
|
1114
|
-
}, {});
|
|
1115
|
-
const dailyValues = Object.values(daily);
|
|
1116
|
-
console.log();
|
|
1117
|
-
console.log(chalk2.bold.cyan(" Economy"));
|
|
1118
|
-
console.log();
|
|
1119
|
-
printTable(["Period", "Cost", "Sessions", "Requests", "Tokens"], [
|
|
1120
|
-
["Today", fmt2(t.total_usd), fmtCount(t.sessions), fmtCount(t.requests), fmtTokens(t.tokens)],
|
|
1121
|
-
["This Week", fmt2(w.total_usd), fmtCount(w.sessions), fmtCount(w.requests), fmtTokens(w.tokens)],
|
|
1122
|
-
["This Month", fmt2(m.total_usd), fmtCount(m.sessions), fmtCount(m.requests), fmtTokens(m.tokens)]
|
|
1123
|
-
]);
|
|
1124
|
-
if (dailyValues.length > 0) {
|
|
1125
|
-
console.log(`
|
|
1126
|
-
${chalk2.dim("14-day trend:")} ${sparkline(dailyValues)}`);
|
|
1127
|
-
}
|
|
1128
|
-
if (projects.length > 0) {
|
|
1129
|
-
console.log(`
|
|
1130
|
-
${chalk2.dim("Top projects:")}`);
|
|
1131
|
-
for (const p of projects) {
|
|
1132
|
-
console.log(` ${chalk2.white(p.project_name.padEnd(25))} ${fmt2(p.cost_usd)}`);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
console.log();
|
|
1136
|
-
});
|
|
1137
|
-
program.command("sync").description("Ingest cost data from Claude Code and Codex").option("--claude", "Only ingest Claude Code telemetry").option("--codex", "Only ingest Codex sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").action(async (opts) => {
|
|
1138
|
-
const db = openDatabase();
|
|
1139
|
-
ensurePricingSeeded(db);
|
|
1140
|
-
if (opts.force) {
|
|
1141
|
-
db.exec(`DELETE FROM ingest_state WHERE source = 'claude'`);
|
|
1142
|
-
if (opts.verbose)
|
|
1143
|
-
console.log(chalk2.dim("Cleared ingest cache"));
|
|
1144
|
-
}
|
|
1145
|
-
const doClaude = opts.claude || !opts.claude && !opts.codex;
|
|
1146
|
-
const doCodex = opts.codex || !opts.claude && !opts.codex;
|
|
1147
|
-
if (doClaude) {
|
|
1148
|
-
process.stdout.write(chalk2.cyan("\u2192 Ingesting Claude Code telemetry... "));
|
|
1149
|
-
const r = await ingestClaude(db, opts.verbose);
|
|
1150
|
-
console.log(chalk2.green(`\u2713 ${r.files} files, ${r.requests} requests, ${r.sessions} sessions`));
|
|
1151
|
-
}
|
|
1152
|
-
if (doCodex) {
|
|
1153
|
-
process.stdout.write(chalk2.cyan("\u2192 Ingesting Codex sessions... "));
|
|
1154
|
-
const r = await ingestCodex(db, opts.verbose);
|
|
1155
|
-
console.log(chalk2.green(`\u2713 ${r.sessions} sessions`));
|
|
1156
|
-
}
|
|
1157
|
-
try {
|
|
1158
|
-
const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
1159
|
-
await checkAndFireWebhooks2(db);
|
|
1160
|
-
} catch {}
|
|
1161
|
-
console.log(chalk2.bold.green(`
|
|
1162
|
-
\u2713 Sync complete`));
|
|
1163
|
-
});
|
|
1164
|
-
program.command("today").description("Cost summary for today").action(async () => {
|
|
1165
|
-
await autoSync();
|
|
1166
|
-
printSummary("Today", "today");
|
|
1167
|
-
});
|
|
1168
|
-
program.command("week").description("Cost summary for this week").action(async () => {
|
|
1169
|
-
await autoSync();
|
|
1170
|
-
printSummary("This Week", "week");
|
|
1171
|
-
});
|
|
1172
|
-
program.command("month").description("Cost summary for this month").action(async () => {
|
|
1173
|
-
await autoSync();
|
|
1174
|
-
printSummary("This Month", "month");
|
|
1175
|
-
});
|
|
1176
|
-
program.command("sessions").description("List coding sessions with costs").option("--agent <agent>", "Filter by agent (claude|codex)").option("--project <path>", "Filter by project path").option("--limit <n>", "Number of sessions", "20").option("--format <fmt>", "Output format: table|compact|csv|json", "table").action(async (opts) => {
|
|
1177
|
-
await autoSync();
|
|
1178
|
-
const db = openDatabase();
|
|
1179
|
-
const sessions = querySessions(db, {
|
|
1180
|
-
agent: opts.agent,
|
|
1181
|
-
project: opts.project,
|
|
1182
|
-
limit: Number(opts.limit ?? 20)
|
|
1183
|
-
});
|
|
1184
|
-
if (sessions.length === 0) {
|
|
1185
|
-
console.log(chalk2.yellow("No sessions found."));
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
const f = opts.format ?? "table";
|
|
1189
|
-
if (f === "compact") {
|
|
1190
|
-
for (const s of sessions)
|
|
1191
|
-
process.stdout.write(`${s.id.slice(0, 8)} ${s.agent} ${fmt2(s.total_cost_usd)} ${fmtTokens(s.total_tokens)} ${s.project_name || "\u2014"}
|
|
1192
|
-
`);
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
if (f === "json") {
|
|
1196
|
-
console.log(JSON.stringify(sessions, null, 2));
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
if (f === "csv") {
|
|
1200
|
-
console.log("id,agent,project_name,total_cost_usd,total_tokens,request_count,started_at");
|
|
1201
|
-
for (const s of sessions)
|
|
1202
|
-
console.log(`${s.id},${s.agent},"${s.project_name}",${s.total_cost_usd},${s.total_tokens},${s.request_count},${s.started_at}`);
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
console.log();
|
|
1206
|
-
printTable(["Session ID", "Agent", "Project", "Cost", "Tokens", "Requests", "Started"], sessions.map((s) => [
|
|
1207
|
-
chalk2.dim(s.id.substring(0, 12)),
|
|
1208
|
-
s.agent === "claude" ? chalk2.blue("claude") : chalk2.yellow("codex"),
|
|
1209
|
-
chalk2.white(s.project_name || chalk2.dim("unknown")),
|
|
1210
|
-
fmt2(s.total_cost_usd),
|
|
1211
|
-
chalk2.cyan(fmtTokens(s.total_tokens)),
|
|
1212
|
-
fmtCount(s.request_count),
|
|
1213
|
-
chalk2.dim(s.started_at.substring(0, 16))
|
|
1214
|
-
]));
|
|
1215
|
-
console.log();
|
|
1216
|
-
});
|
|
1217
|
-
program.command("top").description("Most expensive sessions").option("-n <n>", "Number of sessions", "10").option("--agent <agent>", "Filter by agent").action((opts) => {
|
|
1218
|
-
const db = openDatabase();
|
|
1219
|
-
const sessions = queryTopSessions(db, Number(opts.n ?? 10), opts.agent);
|
|
1220
|
-
if (sessions.length === 0) {
|
|
1221
|
-
console.log(chalk2.yellow("No sessions found. Run `economy sync` first."));
|
|
1222
|
-
return;
|
|
1223
|
-
}
|
|
1224
|
-
console.log();
|
|
1225
|
-
printTable(["#", "Project", "Agent", "Cost", "Tokens", "Started"], sessions.map((s, i) => [
|
|
1226
|
-
chalk2.dim(String(i + 1)),
|
|
1227
|
-
chalk2.white(s.project_name || chalk2.dim("unknown")),
|
|
1228
|
-
s.agent === "claude" ? chalk2.blue("claude") : chalk2.yellow("codex"),
|
|
1229
|
-
fmt2(s.total_cost_usd),
|
|
1230
|
-
chalk2.cyan(fmtTokens(s.total_tokens)),
|
|
1231
|
-
chalk2.dim(s.started_at.substring(0, 16))
|
|
1232
|
-
]));
|
|
1233
|
-
console.log();
|
|
1234
|
-
});
|
|
1235
|
-
program.command("breakdown").description("Cost breakdown by model, agent, or project").option("--by <dimension>", "Dimension: model|agent|project", "model").action((opts) => {
|
|
1236
|
-
const db = openDatabase();
|
|
1237
|
-
console.log();
|
|
1238
|
-
if (opts.by === "project") {
|
|
1239
|
-
const rows = queryProjectBreakdown(db);
|
|
1240
|
-
printTable(["Project", "Sessions", "Requests", "Tokens", "Cost"], rows.map((r) => [
|
|
1241
|
-
chalk2.white(r.project_name || chalk2.dim("unknown")),
|
|
1242
|
-
String(r.sessions),
|
|
1243
|
-
String(r.requests),
|
|
1244
|
-
chalk2.cyan(fmtTokens(r.total_tokens)),
|
|
1245
|
-
fmt2(r.cost_usd)
|
|
1246
|
-
]));
|
|
1247
|
-
} else {
|
|
1248
|
-
const rows = queryModelBreakdown(db);
|
|
1249
|
-
printTable(["Model", "Agent", "Requests", "Tokens", "Cost"], rows.map((r) => [
|
|
1250
|
-
chalk2.white(r.model),
|
|
1251
|
-
r.agent === "claude" ? chalk2.blue("claude") : chalk2.yellow("codex"),
|
|
1252
|
-
String(r.requests),
|
|
1253
|
-
chalk2.cyan(fmtTokens(r.total_tokens)),
|
|
1254
|
-
fmt2(r.cost_usd)
|
|
1255
|
-
]));
|
|
1256
|
-
}
|
|
1257
|
-
console.log();
|
|
1258
|
-
});
|
|
1259
|
-
program.command("watch").description("Live stream of incoming costs").option("--interval <seconds>", "Poll interval in seconds", "10").option("--agent <agent>", "Filter by agent").action(async (opts) => {
|
|
1260
|
-
const { watchCosts: watchCosts2 } = await Promise.resolve().then(() => (init_watch(), exports_watch));
|
|
1261
|
-
await watchCosts2({ interval: Number(opts.interval ?? 10), agent: opts.agent });
|
|
1262
|
-
});
|
|
1263
|
-
var budgetCmd = program.command("budget").description("Manage spending budgets");
|
|
1264
|
-
budgetCmd.command("set").description("Set a budget").option("--project <path>", "Project path (omit for global)").option("--period <period>", "Period: daily|weekly|monthly", "monthly").option("--limit <usd>", "Budget limit in USD").option("--alert <percent>", "Alert threshold %", "80").option("--agent <agent>", "Limit to agent (claude|codex)").action((opts) => {
|
|
1265
|
-
if (!opts.limit) {
|
|
1266
|
-
console.error(chalk2.red("--limit is required"));
|
|
1267
|
-
process.exit(1);
|
|
1268
|
-
}
|
|
1269
|
-
const db = openDatabase();
|
|
1270
|
-
const now = new Date().toISOString();
|
|
1271
|
-
upsertBudget(db, {
|
|
1272
|
-
id: randomUUID2(),
|
|
1273
|
-
project_path: opts.project ?? null,
|
|
1274
|
-
agent: opts.agent ?? null,
|
|
1275
|
-
period: opts.period ?? "monthly",
|
|
1276
|
-
limit_usd: Number(opts.limit),
|
|
1277
|
-
alert_at_percent: Number(opts.alert ?? 80),
|
|
1278
|
-
created_at: now,
|
|
1279
|
-
updated_at: now
|
|
1280
|
-
});
|
|
1281
|
-
console.log(chalk2.green(`\u2713 Budget set: ${opts.project ?? "global"} \u2014 ${opts.period} $${opts.limit}`));
|
|
1282
|
-
});
|
|
1283
|
-
budgetCmd.command("list").description("List all budgets").action(() => {
|
|
1284
|
-
const db = openDatabase();
|
|
1285
|
-
const statuses = getBudgetStatuses(db);
|
|
1286
|
-
if (statuses.length === 0) {
|
|
1287
|
-
console.log(chalk2.yellow("No budgets set."));
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
console.log();
|
|
1291
|
-
printTable(["Scope", "Period", "Limit", "Spent", "Used%", "Status"], statuses.map((b) => {
|
|
1292
|
-
const pct = b.percent_used.toFixed(1);
|
|
1293
|
-
const status = b.is_over_limit ? chalk2.red("OVER") : b.is_over_alert ? chalk2.yellow("ALERT") : chalk2.green("OK");
|
|
1294
|
-
const pctColor = b.is_over_limit ? chalk2.red(pct + "%") : b.is_over_alert ? chalk2.yellow(pct + "%") : chalk2.green(pct + "%");
|
|
1295
|
-
return [
|
|
1296
|
-
chalk2.white(b.project_path ?? "global"),
|
|
1297
|
-
b.period,
|
|
1298
|
-
fmt2(b.limit_usd),
|
|
1299
|
-
fmt2(b.current_spend_usd),
|
|
1300
|
-
pctColor,
|
|
1301
|
-
status
|
|
1302
|
-
];
|
|
1303
|
-
}));
|
|
1304
|
-
console.log();
|
|
1305
|
-
});
|
|
1306
|
-
budgetCmd.command("remove <id>").description("Remove a budget by ID").action((id) => {
|
|
1307
|
-
const db = openDatabase();
|
|
1308
|
-
deleteBudget(db, id);
|
|
1309
|
-
console.log(chalk2.green(`\u2713 Budget removed`));
|
|
1310
|
-
});
|
|
1311
|
-
var projectCmd = program.command("project").description("Manage tracked projects");
|
|
1312
|
-
projectCmd.command("add <path>").description("Add a project").option("--name <name>", "Human-readable name").action((path, opts) => {
|
|
1313
|
-
const db = openDatabase();
|
|
1314
|
-
const { basename: basename3 } = __require("path");
|
|
1315
|
-
upsertProject(db, {
|
|
1316
|
-
id: randomUUID2(),
|
|
1317
|
-
path,
|
|
1318
|
-
name: opts.name ?? basename3(path),
|
|
1319
|
-
description: null,
|
|
1320
|
-
tags: [],
|
|
1321
|
-
created_at: new Date().toISOString()
|
|
1322
|
-
});
|
|
1323
|
-
console.log(chalk2.green(`\u2713 Project added: ${path}`));
|
|
1324
|
-
});
|
|
1325
|
-
projectCmd.command("list").description("List all projects with costs").action(() => {
|
|
1326
|
-
const db = openDatabase();
|
|
1327
|
-
const projects = queryProjectBreakdown(db);
|
|
1328
|
-
if (projects.length === 0) {
|
|
1329
|
-
console.log(chalk2.yellow("No projects tracked yet."));
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
|
-
console.log();
|
|
1333
|
-
printTable(["Project", "Path", "Sessions", "Cost", "Last Active"], projects.map((p) => [
|
|
1334
|
-
chalk2.white(p.project_name || chalk2.dim("unknown")),
|
|
1335
|
-
chalk2.dim(p.project_path.substring(0, 40)),
|
|
1336
|
-
String(p.sessions),
|
|
1337
|
-
fmt2(p.cost_usd),
|
|
1338
|
-
chalk2.dim(p.last_active?.substring(0, 16) ?? "\u2014")
|
|
1339
|
-
]));
|
|
1340
|
-
console.log();
|
|
1341
|
-
});
|
|
1342
|
-
projectCmd.command("remove <path>").description("Remove a project (keeps historical data)").action((path) => {
|
|
1343
|
-
const db = openDatabase();
|
|
1344
|
-
deleteProject(db, path);
|
|
1345
|
-
console.log(chalk2.green(`\u2713 Project removed`));
|
|
1346
|
-
});
|
|
1347
|
-
projectCmd.command("rename <path> <name>").description("Rename a project").action((path, name) => {
|
|
1348
|
-
const db = openDatabase();
|
|
1349
|
-
const existing = getProject(db, path);
|
|
1350
|
-
if (!existing) {
|
|
1351
|
-
console.error(chalk2.red("Project not found"));
|
|
1352
|
-
process.exit(1);
|
|
1353
|
-
}
|
|
1354
|
-
upsertProject(db, { ...existing, name });
|
|
1355
|
-
console.log(chalk2.green(`\u2713 Renamed to: ${name}`));
|
|
1356
|
-
});
|
|
1357
|
-
projectCmd.command("show <nameOrPath>").description("Detailed project breakdown with sparkline").action(async (nameOrPath) => {
|
|
1358
|
-
await autoSync();
|
|
1359
|
-
const db = openDatabase();
|
|
1360
|
-
const sessions = db.prepare(`SELECT * FROM sessions WHERE project_name LIKE ? OR project_path LIKE ? ORDER BY started_at DESC`).all(`%${nameOrPath}%`, `%${nameOrPath}%`);
|
|
1361
|
-
if (sessions.length === 0) {
|
|
1362
|
-
console.log(chalk2.yellow(`No sessions found for: ${nameOrPath}`));
|
|
1363
|
-
return;
|
|
1364
|
-
}
|
|
1365
|
-
const projectName = sessions[0]["project_name"] || nameOrPath;
|
|
1366
|
-
const projectPath = sessions[0]["project_path"] || "";
|
|
1367
|
-
const totalCost = sessions.reduce((s, r) => s + r["total_cost_usd"], 0);
|
|
1368
|
-
const totalTokens = sessions.reduce((s, r) => s + r["total_tokens"], 0);
|
|
1369
|
-
const daily = db.prepare(`
|
|
1370
|
-
SELECT DATE(r.timestamp) as d, SUM(r.cost_usd) as cost
|
|
1371
|
-
FROM requests r JOIN sessions s ON r.session_id = s.id
|
|
1372
|
-
WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
|
|
1373
|
-
AND r.timestamp >= DATE('now', '-14 days')
|
|
1374
|
-
GROUP BY d ORDER BY d ASC
|
|
1375
|
-
`).all(`%${nameOrPath}%`, `%${nameOrPath}%`);
|
|
1376
|
-
const dailyValues = daily.map((d) => d.cost);
|
|
1377
|
-
const models = db.prepare(`
|
|
1378
|
-
SELECT r.model, COUNT(*) as reqs, SUM(r.cost_usd) as cost
|
|
1379
|
-
FROM requests r JOIN sessions s ON r.session_id = s.id
|
|
1380
|
-
WHERE s.project_name LIKE ? OR s.project_path LIKE ?
|
|
1381
|
-
GROUP BY r.model ORDER BY cost DESC LIMIT 5
|
|
1382
|
-
`).all(`%${nameOrPath}%`, `%${nameOrPath}%`);
|
|
1383
|
-
console.log();
|
|
1384
|
-
console.log(chalk2.bold.cyan(` ${projectName}`));
|
|
1385
|
-
console.log(chalk2.dim(` ${projectPath}`));
|
|
1386
|
-
console.log();
|
|
1387
|
-
printTable(["Metric", "Value"], [
|
|
1388
|
-
["Total cost", fmt2(totalCost)],
|
|
1389
|
-
["Sessions", fmtCount(sessions.length)],
|
|
1390
|
-
["Total tokens", fmtTokens(totalTokens)]
|
|
1391
|
-
]);
|
|
1392
|
-
if (dailyValues.length > 0) {
|
|
1393
|
-
console.log(`
|
|
1394
|
-
${chalk2.dim("14-day trend:")} ${sparkline(dailyValues)}`);
|
|
1395
|
-
}
|
|
1396
|
-
if (models.length > 0) {
|
|
1397
|
-
console.log(`
|
|
1398
|
-
${chalk2.dim("Model breakdown:")}`);
|
|
1399
|
-
for (const m of models) {
|
|
1400
|
-
console.log(` ${chalk2.white(m.model.padEnd(30))} ${fmt2(m.cost)} (${fmtCount(m.reqs)} reqs)`);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
const topSessions = sessions.sort((a, b) => b["total_cost_usd"] - a["total_cost_usd"]).slice(0, 5);
|
|
1404
|
-
if (topSessions.length > 0) {
|
|
1405
|
-
console.log(`
|
|
1406
|
-
${chalk2.dim("Top sessions:")}`);
|
|
1407
|
-
for (const s of topSessions) {
|
|
1408
|
-
console.log(` ${chalk2.dim(s["id"].substring(0, 12))} ${fmt2(s["total_cost_usd"])} ${chalk2.dim(String(s["started_at"]).substring(0, 16))}`);
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
console.log();
|
|
1412
|
-
});
|
|
1413
|
-
var configCmd = program.command("config").description("Manage economy configuration");
|
|
1414
|
-
configCmd.command("set <key> <value>").description("Set a config value").action(async (_key, _value) => {
|
|
1415
|
-
const { setConfigValue: setConfigValue2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
1416
|
-
setConfigValue2(_key, _value);
|
|
1417
|
-
console.log(chalk2.green(`\u2713 ${_key} = ${_value}`));
|
|
1418
|
-
});
|
|
1419
|
-
configCmd.command("get <key>").description("Get a config value").action(async (key) => {
|
|
1420
|
-
const { getConfigValue: getConfigValue2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
1421
|
-
console.log(getConfigValue2(key) ?? chalk2.dim("(not set)"));
|
|
1422
|
-
});
|
|
1423
|
-
configCmd.action(async () => {
|
|
1424
|
-
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
1425
|
-
const config = loadConfig2();
|
|
1426
|
-
console.log();
|
|
1427
|
-
printTable(["Key", "Value"], Object.entries(config).map(([k, v]) => [k, String(v)]));
|
|
1428
|
-
console.log();
|
|
1429
|
-
});
|
|
1430
|
-
var pricingCmd = program.command("pricing").description("Manage model pricing rates");
|
|
1431
|
-
pricingCmd.command("list").description("List all model prices").action(() => {
|
|
1432
|
-
const db = openDatabase();
|
|
1433
|
-
ensurePricingSeeded(db);
|
|
1434
|
-
const rows = listModelPricing(db);
|
|
1435
|
-
console.log();
|
|
1436
|
-
printTable(["Model", "Input/1M", "Output/1M", "CacheRead/1M", "CacheWrite/1M"], rows.map((r) => [
|
|
1437
|
-
chalk2.white(r.model),
|
|
1438
|
-
fmt2(r.input_per_1m),
|
|
1439
|
-
fmt2(r.output_per_1m),
|
|
1440
|
-
fmt2(r.cache_read_per_1m),
|
|
1441
|
-
fmt2(r.cache_write_per_1m)
|
|
1442
|
-
]));
|
|
1443
|
-
console.log();
|
|
1444
|
-
});
|
|
1445
|
-
pricingCmd.command("set <model>").description("Set pricing for a model").option("--input <usd>", "Input price per 1M tokens").option("--output <usd>", "Output price per 1M tokens").option("--cache-read <usd>", "Cache read price per 1M tokens", "0").option("--cache-write <usd>", "Cache write price per 1M tokens", "0").action((model, opts) => {
|
|
1446
|
-
if (!opts.input || !opts.output) {
|
|
1447
|
-
console.error(chalk2.red("--input and --output are required"));
|
|
1448
|
-
process.exit(1);
|
|
1449
|
-
}
|
|
1450
|
-
const db = openDatabase();
|
|
1451
|
-
ensurePricingSeeded(db);
|
|
1452
|
-
upsertModelPricing(db, {
|
|
1453
|
-
model,
|
|
1454
|
-
input_per_1m: Number(opts.input),
|
|
1455
|
-
output_per_1m: Number(opts.output),
|
|
1456
|
-
cache_read_per_1m: Number(opts.cacheRead ?? 0),
|
|
1457
|
-
cache_write_per_1m: Number(opts.cacheWrite ?? 0),
|
|
1458
|
-
updated_at: new Date().toISOString()
|
|
1459
|
-
});
|
|
1460
|
-
console.log(chalk2.green(`\u2713 Pricing updated for ${model}`));
|
|
1461
|
-
});
|
|
1462
|
-
pricingCmd.command("remove <model>").description("Remove pricing for a model").action((model) => {
|
|
1463
|
-
const db = openDatabase();
|
|
1464
|
-
deleteModelPricing(db, model);
|
|
1465
|
-
console.log(chalk2.green(`\u2713 Pricing removed for ${model}`));
|
|
1466
|
-
});
|
|
1467
|
-
program.command("serve").description("Start the REST API server").option("-p, --port <port>", "Port", "3456").action(async (opts) => {
|
|
1468
|
-
const port2 = Number(opts.port ?? 3456);
|
|
1469
|
-
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
1470
|
-
startServer2(port2);
|
|
1471
|
-
});
|
|
1472
|
-
program.command("dashboard").description("Open the web dashboard (auto-starts server if not running)").option("-p, --port <port>", "Server port", "3456").action(async (opts) => {
|
|
1473
|
-
const port2 = Number(opts.port ?? 3456);
|
|
1474
|
-
const url = `http://localhost:${port2}`;
|
|
1475
|
-
let serverRunning = false;
|
|
1476
|
-
try {
|
|
1477
|
-
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(500) });
|
|
1478
|
-
serverRunning = res.ok;
|
|
1479
|
-
} catch {}
|
|
1480
|
-
if (!serverRunning) {
|
|
1481
|
-
console.log(chalk2.cyan(`\u2192 Starting economy server on port ${port2}...`));
|
|
1482
|
-
const { spawn } = await import("child_process");
|
|
1483
|
-
const { resolve, dirname: dirname2 } = await import("path");
|
|
1484
|
-
const serveScript = resolve(dirname2(process.argv[1]), "..", "server", "index.js");
|
|
1485
|
-
const child = spawn(process.execPath, [serveScript], {
|
|
1486
|
-
detached: true,
|
|
1487
|
-
stdio: "ignore",
|
|
1488
|
-
env: { ...process.env, ECONOMY_PORT: String(port2) }
|
|
1489
|
-
});
|
|
1490
|
-
child.unref();
|
|
1491
|
-
let attempts = 0;
|
|
1492
|
-
while (attempts < 20) {
|
|
1493
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
1494
|
-
try {
|
|
1495
|
-
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(300) });
|
|
1496
|
-
if (res.ok) {
|
|
1497
|
-
serverRunning = true;
|
|
1498
|
-
break;
|
|
1499
|
-
}
|
|
1500
|
-
} catch {}
|
|
1501
|
-
attempts++;
|
|
1502
|
-
}
|
|
1503
|
-
if (serverRunning) {
|
|
1504
|
-
console.log(chalk2.green(`\u2713 Server started`));
|
|
1505
|
-
} else {
|
|
1506
|
-
console.log(chalk2.yellow(`\u26A0 Server didn't respond \u2014 open ${url} manually after running \`economy serve\``));
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
console.log(chalk2.cyan(`Opening ${url}`));
|
|
1510
|
-
try {
|
|
1511
|
-
execSync(`open ${url}`);
|
|
1512
|
-
} catch {
|
|
1513
|
-
console.log(chalk2.yellow(`Open your browser at ${url}`));
|
|
1514
|
-
}
|
|
1515
|
-
});
|
|
1516
|
-
program.command("mcp").description("Show MCP server install commands").option("--claude", "Install into Claude Code").option("--codex", "Install into Codex").option("--all", "Install into all agents").action(async (opts) => {
|
|
1517
|
-
const doAll = opts.all || !opts.claude && !opts.codex;
|
|
1518
|
-
if (opts.claude || doAll) {
|
|
1519
|
-
console.log(chalk2.bold.cyan(`
|
|
1520
|
-
Claude Code:`));
|
|
1521
|
-
console.log(chalk2.white(" claude mcp add --transport stdio --scope user economy -- economy-mcp"));
|
|
1522
|
-
}
|
|
1523
|
-
if (opts.codex || doAll) {
|
|
1524
|
-
console.log(chalk2.bold.yellow(`
|
|
1525
|
-
Codex (~/.codex/config.toml):`));
|
|
1526
|
-
console.log(chalk2.white(` [mcp_servers.economy]
|
|
1527
|
-
command = "economy-mcp"
|
|
1528
|
-
args = []`));
|
|
1529
|
-
}
|
|
1530
|
-
console.log();
|
|
1531
|
-
});
|
|
1532
|
-
program.command("session <id>").description("Show detailed breakdown of a single session").action(async (id) => {
|
|
1533
|
-
await autoSync();
|
|
1534
|
-
const db = openDatabase();
|
|
1535
|
-
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `%${id}%`);
|
|
1536
|
-
if (!session) {
|
|
1537
|
-
console.log(chalk2.red(`Session not found: ${id}`));
|
|
1538
|
-
process.exit(1);
|
|
1539
|
-
}
|
|
1540
|
-
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC`).all(session["id"]);
|
|
1541
|
-
console.log();
|
|
1542
|
-
console.log(chalk2.bold.cyan(` Session: ${session["id"].substring(0, 16)}...`));
|
|
1543
|
-
console.log();
|
|
1544
|
-
printTable(["Field", "Value"], [
|
|
1545
|
-
["Agent", String(session["agent"])],
|
|
1546
|
-
["Project", String(session["project_name"] || session["project_path"] || "\u2014")],
|
|
1547
|
-
["Started", String(session["started_at"]).substring(0, 19)],
|
|
1548
|
-
["Ended", session["ended_at"] ? String(session["ended_at"]).substring(0, 19) : "\u2014"],
|
|
1549
|
-
["Total cost", fmt2(session["total_cost_usd"])],
|
|
1550
|
-
["Total tokens", fmtTokens(session["total_tokens"])],
|
|
1551
|
-
["Requests", fmtCount(session["request_count"])]
|
|
1552
|
-
]);
|
|
1553
|
-
if (requests.length > 0) {
|
|
1554
|
-
console.log(chalk2.dim(`
|
|
1555
|
-
Requests (${requests.length}):
|
|
1556
|
-
`));
|
|
1557
|
-
printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], requests.slice(0, 50).map((r) => [
|
|
1558
|
-
chalk2.dim(String(r["timestamp"]).substring(11, 19)),
|
|
1559
|
-
chalk2.white(String(r["model"]).substring(0, 22)),
|
|
1560
|
-
fmtTokens(r["input_tokens"]),
|
|
1561
|
-
fmtTokens(r["output_tokens"]),
|
|
1562
|
-
fmtTokens(r["cache_read_tokens"]),
|
|
1563
|
-
fmtTokens(r["cache_create_tokens"]),
|
|
1564
|
-
fmt2(r["cost_usd"])
|
|
1565
|
-
]));
|
|
1566
|
-
if (requests.length > 50)
|
|
1567
|
-
console.log(chalk2.dim(` ... and ${requests.length - 50} more requests`));
|
|
1568
|
-
}
|
|
1569
|
-
console.log();
|
|
1570
|
-
});
|
|
1571
|
-
program.command("export").description("Export data as CSV").option("--type <type>", "Data type: sessions or requests", "sessions").option("--period <period>", "Period: today|week|month|all", "month").option("--output <file>", "Output file path (default: stdout)").action(async (opts) => {
|
|
1572
|
-
await autoSync();
|
|
1573
|
-
const db = openDatabase();
|
|
1574
|
-
let csv;
|
|
1575
|
-
if (opts.type === "requests") {
|
|
1576
|
-
const where = opts.period === "today" ? `DATE(timestamp) = DATE('now')` : opts.period === "week" ? `timestamp >= DATE('now', '-7 days')` : opts.period === "all" ? "1=1" : `timestamp >= DATE('now', '-30 days')`;
|
|
1577
|
-
const rows = db.prepare(`SELECT * FROM requests WHERE ${where} ORDER BY timestamp ASC`).all();
|
|
1578
|
-
csv = `id,agent,session_id,model,input_tokens,output_tokens,cache_read_tokens,cache_create_tokens,cost_usd,duration_ms,timestamp
|
|
1579
|
-
`;
|
|
1580
|
-
for (const r of rows) {
|
|
1581
|
-
csv += `${r["id"]},${r["agent"]},${r["session_id"]},${r["model"]},${r["input_tokens"]},${r["output_tokens"]},${r["cache_read_tokens"]},${r["cache_create_tokens"]},${r["cost_usd"]},${r["duration_ms"]},${r["timestamp"]}
|
|
1582
|
-
`;
|
|
1583
|
-
}
|
|
1584
|
-
} else {
|
|
1585
|
-
const where = opts.period === "today" ? `DATE(started_at) = DATE('now')` : opts.period === "week" ? `started_at >= DATE('now', '-7 days')` : opts.period === "all" ? "1=1" : `started_at >= DATE('now', '-30 days')`;
|
|
1586
|
-
const rows = db.prepare(`SELECT * FROM sessions WHERE ${where} ORDER BY started_at DESC`).all();
|
|
1587
|
-
csv = `id,agent,project_path,project_name,started_at,ended_at,total_cost_usd,total_tokens,request_count
|
|
1588
|
-
`;
|
|
1589
|
-
for (const r of rows) {
|
|
1590
|
-
csv += `${r["id"]},${r["agent"]},"${r["project_path"]}","${r["project_name"]}",${r["started_at"]},${r["ended_at"] ?? ""},${r["total_cost_usd"]},${r["total_tokens"]},${r["request_count"]}
|
|
1591
|
-
`;
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
if (opts.output) {
|
|
1595
|
-
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
1596
|
-
writeFileSync2(opts.output, csv);
|
|
1597
|
-
console.log(chalk2.green(`\u2713 Exported to ${opts.output}`));
|
|
1598
|
-
} else {
|
|
1599
|
-
process.stdout.write(csv);
|
|
1600
|
-
}
|
|
1601
|
-
});
|
|
1602
|
-
program.command("compare <period1> <period2>").description("Compare two periods (today/yesterday/week/lastweek/month/lastmonth)").action(async (p1, p2) => {
|
|
1603
|
-
await autoSync();
|
|
1604
|
-
const db = openDatabase();
|
|
1605
|
-
function dateRange(period) {
|
|
1606
|
-
const now = new Date;
|
|
1607
|
-
const today = now.toISOString().substring(0, 10);
|
|
1608
|
-
switch (period) {
|
|
1609
|
-
case "today":
|
|
1610
|
-
return [today, today];
|
|
1611
|
-
case "yesterday": {
|
|
1612
|
-
const d = new Date(now);
|
|
1613
|
-
d.setDate(d.getDate() - 1);
|
|
1614
|
-
const s = d.toISOString().substring(0, 10);
|
|
1615
|
-
return [s, s];
|
|
1616
|
-
}
|
|
1617
|
-
case "week": {
|
|
1618
|
-
const d = new Date(now);
|
|
1619
|
-
d.setDate(d.getDate() - 7);
|
|
1620
|
-
return [d.toISOString().substring(0, 10), today];
|
|
1621
|
-
}
|
|
1622
|
-
case "lastweek": {
|
|
1623
|
-
const d1 = new Date(now);
|
|
1624
|
-
d1.setDate(d1.getDate() - 14);
|
|
1625
|
-
const d2 = new Date(now);
|
|
1626
|
-
d2.setDate(d2.getDate() - 7);
|
|
1627
|
-
return [d1.toISOString().substring(0, 10), d2.toISOString().substring(0, 10)];
|
|
1628
|
-
}
|
|
1629
|
-
case "month": {
|
|
1630
|
-
const d = new Date(now);
|
|
1631
|
-
d.setDate(d.getDate() - 30);
|
|
1632
|
-
return [d.toISOString().substring(0, 10), today];
|
|
1633
|
-
}
|
|
1634
|
-
case "lastmonth": {
|
|
1635
|
-
const d1 = new Date(now);
|
|
1636
|
-
d1.setDate(d1.getDate() - 60);
|
|
1637
|
-
const d2 = new Date(now);
|
|
1638
|
-
d2.setDate(d2.getDate() - 30);
|
|
1639
|
-
return [d1.toISOString().substring(0, 10), d2.toISOString().substring(0, 10)];
|
|
1640
|
-
}
|
|
1641
|
-
default:
|
|
1642
|
-
return [today, today];
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
function queryRange(from, to) {
|
|
1646
|
-
const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests, COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens FROM requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
|
|
1647
|
-
const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
|
|
1648
|
-
return { ...r, sessions: s.sessions };
|
|
1649
|
-
}
|
|
1650
|
-
const [f1, t1] = dateRange(p1);
|
|
1651
|
-
const [f2, t2] = dateRange(p2);
|
|
1652
|
-
const a = queryRange(f1, t1);
|
|
1653
|
-
const b = queryRange(f2, t2);
|
|
1654
|
-
function delta(v1, v2) {
|
|
1655
|
-
const d = v1 - v2;
|
|
1656
|
-
const pct = v2 > 0 ? (d / v2 * 100).toFixed(1) : "\u2014";
|
|
1657
|
-
const sign = d >= 0 ? "+" : "";
|
|
1658
|
-
const color = d > 0 ? chalk2.red : d < 0 ? chalk2.green : chalk2.dim;
|
|
1659
|
-
return color(`${sign}${pct}%`);
|
|
1660
|
-
}
|
|
1661
|
-
console.log();
|
|
1662
|
-
console.log(chalk2.bold.cyan(` ${p1} vs ${p2}`));
|
|
1663
|
-
console.log();
|
|
1664
|
-
printTable(["Metric", p1, p2, "Change"], [
|
|
1665
|
-
["Cost", fmt2(a.cost), fmt2(b.cost), delta(a.cost, b.cost)],
|
|
1666
|
-
["Sessions", fmtCount(a.sessions), fmtCount(b.sessions), delta(a.sessions, b.sessions)],
|
|
1667
|
-
["Requests", fmtCount(a.requests), fmtCount(b.requests), delta(a.requests, b.requests)],
|
|
1668
|
-
["Tokens", fmtTokens(a.tokens), fmtTokens(b.tokens), delta(a.tokens, b.tokens)]
|
|
1669
|
-
]);
|
|
1670
|
-
console.log();
|
|
1671
|
-
});
|
|
1672
|
-
program.command("forecast").description("Project end-of-month cost based on current burn rate").action(async () => {
|
|
1673
|
-
await autoSync();
|
|
1674
|
-
const db = openDatabase();
|
|
1675
|
-
const now = new Date;
|
|
1676
|
-
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1677
|
-
const dayOfMonth = now.getDate();
|
|
1678
|
-
const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
|
|
1679
|
-
const today = now.toISOString().substring(0, 10);
|
|
1680
|
-
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(monthStart);
|
|
1681
|
-
const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
|
|
1682
|
-
const projected = dailyAvg * daysInMonth;
|
|
1683
|
-
const sevenDaysAgo = new Date(now);
|
|
1684
|
-
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
1685
|
-
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
|
|
1686
|
-
const last7DailyAvg = last7.cost / 7;
|
|
1687
|
-
const last7Projected = last7DailyAvg * daysInMonth;
|
|
1688
|
-
const dailyCosts = db.prepare(`SELECT DATE(timestamp) as d, SUM(cost_usd) as cost FROM requests WHERE DATE(timestamp) >= ? GROUP BY d ORDER BY cost ASC`).all(monthStart);
|
|
1689
|
-
const cheapest = dailyCosts[0];
|
|
1690
|
-
const mostExpensive = dailyCosts[dailyCosts.length - 1];
|
|
1691
|
-
console.log();
|
|
1692
|
-
console.log(chalk2.bold.cyan(` Forecast (${dayOfMonth} of ${daysInMonth} days)`));
|
|
1693
|
-
console.log();
|
|
1694
|
-
printTable(["Metric", "Value"], [
|
|
1695
|
-
["Spent so far", fmt2(monthSoFar.cost)],
|
|
1696
|
-
["Daily average", fmt2(dailyAvg)],
|
|
1697
|
-
[chalk2.bold("Projected total"), chalk2.bold(fmt2(projected).replace(chalk2.green(""), ""))],
|
|
1698
|
-
["Last 7-day rate", `${fmt2(last7DailyAvg)}/day \u2192 ${fmt2(last7Projected)}`],
|
|
1699
|
-
["Cheapest day", cheapest ? `${fmt2(cheapest.cost)} (${cheapest.d})` : "\u2014"],
|
|
1700
|
-
["Most expensive", mostExpensive ? `${fmt2(mostExpensive.cost)} (${mostExpensive.d})` : "\u2014"]
|
|
1701
|
-
]);
|
|
1702
|
-
console.log();
|
|
1703
|
-
});
|
|
1704
|
-
program.command("efficiency").description("Show output/input token ratio per model").action(async () => {
|
|
1705
|
-
await autoSync();
|
|
1706
|
-
const db = openDatabase();
|
|
1707
|
-
const models = db.prepare(`
|
|
1708
|
-
SELECT model, SUM(input_tokens) as input, SUM(output_tokens) as output,
|
|
1709
|
-
SUM(cache_read_tokens) as cache_read, SUM(cache_create_tokens) as cache_write,
|
|
1710
|
-
COUNT(*) as requests, SUM(cost_usd) as cost
|
|
1711
|
-
FROM requests GROUP BY model ORDER BY cost DESC
|
|
1712
|
-
`).all();
|
|
1713
|
-
console.log();
|
|
1714
|
-
console.log(chalk2.bold.cyan(" Token Efficiency"));
|
|
1715
|
-
console.log();
|
|
1716
|
-
printTable(["Model", "Output/Input", "Cache Hit%", "Cost/1k Output", "Requests"], models.map((m) => {
|
|
1717
|
-
const ratio = m.input > 0 ? (m.output / m.input).toFixed(2) : "\u2014";
|
|
1718
|
-
const totalInput = m.input + m.cache_read + m.cache_write;
|
|
1719
|
-
const cacheHit = totalInput > 0 ? (m.cache_read / totalInput * 100).toFixed(1) + "%" : "\u2014";
|
|
1720
|
-
const costPer1kOutput = m.output > 0 ? fmt2(m.cost / m.output * 1000) : "\u2014";
|
|
1721
|
-
return [chalk2.white(m.model), ratio, cacheHit, costPer1kOutput, fmtCount(m.requests)];
|
|
1722
|
-
}));
|
|
1723
|
-
console.log();
|
|
1724
|
-
});
|
|
1725
|
-
program.parse();
|