@hasna/economy 0.2.3 → 0.2.5
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 +300 -29
- 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/server/index.js
DELETED
|
@@ -1,809 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __export = (target, all) => {
|
|
4
|
-
for (var name in all)
|
|
5
|
-
__defProp(target, name, {
|
|
6
|
-
get: all[name],
|
|
7
|
-
enumerable: true,
|
|
8
|
-
configurable: true,
|
|
9
|
-
set: (newValue) => all[name] = () => newValue
|
|
10
|
-
});
|
|
11
|
-
};
|
|
12
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
13
|
-
var __require = import.meta.require;
|
|
14
|
-
|
|
15
|
-
// src/lib/pricing.ts
|
|
16
|
-
var exports_pricing = {};
|
|
17
|
-
__export(exports_pricing, {
|
|
18
|
-
normalizeModelName: () => normalizeModelName,
|
|
19
|
-
getPricingFromDb: () => getPricingFromDb,
|
|
20
|
-
getPricing: () => getPricing,
|
|
21
|
-
ensurePricingSeeded: () => ensurePricingSeeded,
|
|
22
|
-
computeCostFromDb: () => computeCostFromDb,
|
|
23
|
-
computeCost: () => computeCost,
|
|
24
|
-
DEFAULT_PRICING: () => DEFAULT_PRICING
|
|
25
|
-
});
|
|
26
|
-
function normalizeModelName(raw) {
|
|
27
|
-
return raw.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "").toLowerCase();
|
|
28
|
-
}
|
|
29
|
-
function ensurePricingSeeded(db) {
|
|
30
|
-
seedModelPricing(db, DEFAULT_PRICING);
|
|
31
|
-
}
|
|
32
|
-
function getPricingFromDb(db, model) {
|
|
33
|
-
const normalized = normalizeModelName(model);
|
|
34
|
-
const row = getModelPricing(db, normalized);
|
|
35
|
-
if (row) {
|
|
36
|
-
return {
|
|
37
|
-
inputPer1M: row.input_per_1m,
|
|
38
|
-
outputPer1M: row.output_per_1m,
|
|
39
|
-
cacheReadPer1M: row.cache_read_per_1m,
|
|
40
|
-
cacheWritePer1M: row.cache_write_per_1m
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
const allRows = db.prepare(`SELECT * FROM model_pricing`).all();
|
|
44
|
-
for (const r of allRows) {
|
|
45
|
-
if (normalized.startsWith(r.model)) {
|
|
46
|
-
return { inputPer1M: r.input_per_1m, outputPer1M: r.output_per_1m, cacheReadPer1M: r.cache_read_per_1m, cacheWritePer1M: r.cache_write_per_1m };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
function getPricing(model) {
|
|
52
|
-
const normalized = normalizeModelName(model);
|
|
53
|
-
if (DEFAULT_PRICING[normalized])
|
|
54
|
-
return DEFAULT_PRICING[normalized] ?? null;
|
|
55
|
-
for (const key of Object.keys(DEFAULT_PRICING)) {
|
|
56
|
-
if (normalized.startsWith(key))
|
|
57
|
-
return DEFAULT_PRICING[key] ?? null;
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
|
|
62
|
-
const pricing = getPricing(model);
|
|
63
|
-
if (!pricing)
|
|
64
|
-
return 0;
|
|
65
|
-
return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
|
|
66
|
-
}
|
|
67
|
-
function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
|
|
68
|
-
const pricing = getPricingFromDb(db, model) ?? getPricing(model);
|
|
69
|
-
if (!pricing)
|
|
70
|
-
return 0;
|
|
71
|
-
return (inputTokens * pricing.inputPer1M + outputTokens * pricing.outputPer1M + cacheReadTokens * pricing.cacheReadPer1M + cacheWriteTokens * pricing.cacheWritePer1M) / 1e6;
|
|
72
|
-
}
|
|
73
|
-
var DEFAULT_PRICING;
|
|
74
|
-
var init_pricing = __esm(() => {
|
|
75
|
-
init_database();
|
|
76
|
-
DEFAULT_PRICING = {
|
|
77
|
-
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
78
|
-
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
79
|
-
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
80
|
-
"claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
81
|
-
"claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
82
|
-
"claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
83
|
-
"claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
84
|
-
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
|
|
85
|
-
"claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
86
|
-
"claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
|
|
87
|
-
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
88
|
-
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
89
|
-
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
90
|
-
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
91
|
-
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
92
|
-
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
93
|
-
"o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
|
|
94
|
-
o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
|
|
95
|
-
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
96
|
-
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
|
|
97
|
-
};
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// src/db/database.ts
|
|
101
|
-
import { Database } from "bun:sqlite";
|
|
102
|
-
import { existsSync, mkdirSync } from "fs";
|
|
103
|
-
import { homedir } from "os";
|
|
104
|
-
import { join } from "path";
|
|
105
|
-
function getDbPath() {
|
|
106
|
-
return process.env["ECONOMY_DB"] ?? join(homedir(), ".economy", "economy.db");
|
|
107
|
-
}
|
|
108
|
-
function openDatabase(dbPath, skipSeed = false) {
|
|
109
|
-
const path = dbPath ?? getDbPath();
|
|
110
|
-
if (path !== ":memory:") {
|
|
111
|
-
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
112
|
-
if (dir && !existsSync(dir))
|
|
113
|
-
mkdirSync(dir, { recursive: true });
|
|
114
|
-
}
|
|
115
|
-
const db = new Database(path);
|
|
116
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
117
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
118
|
-
initSchema(db);
|
|
119
|
-
if (!skipSeed) {
|
|
120
|
-
Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
|
|
121
|
-
}
|
|
122
|
-
return db;
|
|
123
|
-
}
|
|
124
|
-
function initSchema(db) {
|
|
125
|
-
db.exec(`
|
|
126
|
-
CREATE TABLE IF NOT EXISTS requests (
|
|
127
|
-
id TEXT PRIMARY KEY,
|
|
128
|
-
agent TEXT NOT NULL,
|
|
129
|
-
session_id TEXT NOT NULL,
|
|
130
|
-
model TEXT NOT NULL,
|
|
131
|
-
input_tokens INTEGER DEFAULT 0,
|
|
132
|
-
output_tokens INTEGER DEFAULT 0,
|
|
133
|
-
cache_read_tokens INTEGER DEFAULT 0,
|
|
134
|
-
cache_create_tokens INTEGER DEFAULT 0,
|
|
135
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
136
|
-
duration_ms INTEGER DEFAULT 0,
|
|
137
|
-
timestamp TEXT NOT NULL,
|
|
138
|
-
source_request_id TEXT
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
142
|
-
id TEXT PRIMARY KEY,
|
|
143
|
-
agent TEXT NOT NULL,
|
|
144
|
-
project_path TEXT DEFAULT '',
|
|
145
|
-
project_name TEXT DEFAULT '',
|
|
146
|
-
started_at TEXT NOT NULL,
|
|
147
|
-
ended_at TEXT,
|
|
148
|
-
total_cost_usd REAL DEFAULT 0,
|
|
149
|
-
total_tokens INTEGER DEFAULT 0,
|
|
150
|
-
request_count INTEGER DEFAULT 0
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
154
|
-
id TEXT PRIMARY KEY,
|
|
155
|
-
path TEXT UNIQUE NOT NULL,
|
|
156
|
-
name TEXT NOT NULL,
|
|
157
|
-
description TEXT,
|
|
158
|
-
tags TEXT DEFAULT '[]',
|
|
159
|
-
created_at TEXT NOT NULL
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
CREATE TABLE IF NOT EXISTS budgets (
|
|
163
|
-
id TEXT PRIMARY KEY,
|
|
164
|
-
project_path TEXT,
|
|
165
|
-
agent TEXT,
|
|
166
|
-
period TEXT NOT NULL,
|
|
167
|
-
limit_usd REAL NOT NULL,
|
|
168
|
-
alert_at_percent INTEGER DEFAULT 80,
|
|
169
|
-
created_at TEXT NOT NULL,
|
|
170
|
-
updated_at TEXT NOT NULL
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
CREATE TABLE IF NOT EXISTS ingest_state (
|
|
174
|
-
source TEXT NOT NULL,
|
|
175
|
-
key TEXT NOT NULL,
|
|
176
|
-
value TEXT NOT NULL,
|
|
177
|
-
PRIMARY KEY (source, key)
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id);
|
|
181
|
-
CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp);
|
|
182
|
-
CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent);
|
|
183
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
|
|
184
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
|
|
185
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
|
|
186
|
-
|
|
187
|
-
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
188
|
-
model TEXT PRIMARY KEY,
|
|
189
|
-
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
190
|
-
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
191
|
-
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
192
|
-
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
193
|
-
updated_at TEXT NOT NULL
|
|
194
|
-
);
|
|
195
|
-
`);
|
|
196
|
-
}
|
|
197
|
-
function periodWhere(period) {
|
|
198
|
-
switch (period) {
|
|
199
|
-
case "today":
|
|
200
|
-
return `DATE(timestamp) = DATE('now')`;
|
|
201
|
-
case "week":
|
|
202
|
-
return `timestamp >= DATE('now', '-7 days')`;
|
|
203
|
-
case "month":
|
|
204
|
-
return `timestamp >= DATE('now', '-30 days')`;
|
|
205
|
-
case "all":
|
|
206
|
-
return "1=1";
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
function sessionPeriodWhere(period) {
|
|
210
|
-
switch (period) {
|
|
211
|
-
case "today":
|
|
212
|
-
return `DATE(started_at) = DATE('now')`;
|
|
213
|
-
case "week":
|
|
214
|
-
return `started_at >= DATE('now', '-7 days')`;
|
|
215
|
-
case "month":
|
|
216
|
-
return `started_at >= DATE('now', '-30 days')`;
|
|
217
|
-
case "all":
|
|
218
|
-
return "1=1";
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
function upsertRequest(db, req) {
|
|
222
|
-
db.prepare(`
|
|
223
|
-
INSERT OR REPLACE INTO requests
|
|
224
|
-
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
225
|
-
cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
|
|
226
|
-
timestamp, source_request_id)
|
|
227
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
228
|
-
`).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);
|
|
229
|
-
}
|
|
230
|
-
function upsertSession(db, session) {
|
|
231
|
-
db.prepare(`
|
|
232
|
-
INSERT OR REPLACE INTO sessions
|
|
233
|
-
(id, agent, project_path, project_name, started_at, ended_at,
|
|
234
|
-
total_cost_usd, total_tokens, request_count)
|
|
235
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
236
|
-
`).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);
|
|
237
|
-
}
|
|
238
|
-
function rollupSession(db, sessionId) {
|
|
239
|
-
db.prepare(`
|
|
240
|
-
UPDATE sessions SET
|
|
241
|
-
total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM requests WHERE session_id = ?),
|
|
242
|
-
total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM requests WHERE session_id = ?),
|
|
243
|
-
request_count = (SELECT COUNT(*) FROM requests WHERE session_id = ?),
|
|
244
|
-
ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
|
|
245
|
-
started_at = CASE WHEN started_at = '' OR started_at IS NULL
|
|
246
|
-
THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
|
|
247
|
-
ELSE started_at END
|
|
248
|
-
WHERE id = ?
|
|
249
|
-
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
250
|
-
}
|
|
251
|
-
function querySessions(db, filter = {}) {
|
|
252
|
-
const conditions = [];
|
|
253
|
-
const params = [];
|
|
254
|
-
if (filter.agent) {
|
|
255
|
-
conditions.push("agent = ?");
|
|
256
|
-
params.push(filter.agent);
|
|
257
|
-
}
|
|
258
|
-
if (filter.project) {
|
|
259
|
-
conditions.push("project_path LIKE ?");
|
|
260
|
-
params.push(`%${filter.project}%`);
|
|
261
|
-
}
|
|
262
|
-
if (filter.since) {
|
|
263
|
-
conditions.push("started_at >= ?");
|
|
264
|
-
params.push(filter.since);
|
|
265
|
-
}
|
|
266
|
-
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
267
|
-
const limit = filter.limit ?? 50;
|
|
268
|
-
const offset = filter.offset ?? 0;
|
|
269
|
-
return db.prepare(`
|
|
270
|
-
SELECT * FROM sessions ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?
|
|
271
|
-
`).all(...params, limit, offset);
|
|
272
|
-
}
|
|
273
|
-
function queryTopSessions(db, n = 10, agent) {
|
|
274
|
-
if (agent) {
|
|
275
|
-
return db.prepare(`SELECT * FROM sessions WHERE agent = ? ORDER BY total_cost_usd DESC LIMIT ?`).all(agent, n);
|
|
276
|
-
}
|
|
277
|
-
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
278
|
-
}
|
|
279
|
-
function querySummary(db, period) {
|
|
280
|
-
const rWhere = periodWhere(period);
|
|
281
|
-
const sWhere = sessionPeriodWhere(period);
|
|
282
|
-
const r = db.prepare(`
|
|
283
|
-
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
284
|
-
COUNT(*) as requests,
|
|
285
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
286
|
-
FROM requests WHERE ${rWhere}
|
|
287
|
-
`).get();
|
|
288
|
-
const codexTotals = db.prepare(`
|
|
289
|
-
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
290
|
-
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
291
|
-
COUNT(*) as sessions
|
|
292
|
-
FROM sessions
|
|
293
|
-
WHERE ${sWhere}
|
|
294
|
-
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
295
|
-
`).get();
|
|
296
|
-
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
|
|
297
|
-
return {
|
|
298
|
-
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
299
|
-
requests: r.requests,
|
|
300
|
-
tokens: r.tokens + codexTotals.tokens,
|
|
301
|
-
sessions: sessionCount.sessions,
|
|
302
|
-
period
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
function queryModelBreakdown(db) {
|
|
306
|
-
return db.prepare(`
|
|
307
|
-
SELECT model, agent,
|
|
308
|
-
COUNT(*) as requests,
|
|
309
|
-
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
310
|
-
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
311
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
312
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
313
|
-
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
314
|
-
`).all();
|
|
315
|
-
}
|
|
316
|
-
function queryProjectBreakdown(db) {
|
|
317
|
-
return db.prepare(`
|
|
318
|
-
SELECT project_path, project_name,
|
|
319
|
-
COUNT(*) as sessions,
|
|
320
|
-
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
321
|
-
COALESCE(SUM(request_count), 0) as requests,
|
|
322
|
-
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
323
|
-
MAX(started_at) as last_active
|
|
324
|
-
FROM sessions
|
|
325
|
-
GROUP BY project_path ORDER BY cost_usd DESC
|
|
326
|
-
`).all();
|
|
327
|
-
}
|
|
328
|
-
function queryDailyBreakdown(db, days = 30) {
|
|
329
|
-
return db.prepare(`
|
|
330
|
-
SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
331
|
-
FROM requests
|
|
332
|
-
WHERE timestamp >= DATE('now', ? || ' days')
|
|
333
|
-
GROUP BY DATE(timestamp), agent
|
|
334
|
-
ORDER BY date ASC
|
|
335
|
-
`).all(`-${days}`);
|
|
336
|
-
}
|
|
337
|
-
function upsertProject(db, project) {
|
|
338
|
-
db.prepare(`
|
|
339
|
-
INSERT OR REPLACE INTO projects (id, path, name, description, tags, created_at)
|
|
340
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
341
|
-
`).run(project.id, project.path, project.name, project.description ?? null, JSON.stringify(project.tags), project.created_at);
|
|
342
|
-
}
|
|
343
|
-
function listProjects(db) {
|
|
344
|
-
return db.prepare(`SELECT * FROM projects ORDER BY created_at DESC`).all().map((row) => ({ ...row, tags: JSON.parse(row["tags"] ?? "[]") }));
|
|
345
|
-
}
|
|
346
|
-
function deleteProject(db, path) {
|
|
347
|
-
db.prepare(`DELETE FROM projects WHERE path = ?`).run(path);
|
|
348
|
-
}
|
|
349
|
-
function upsertBudget(db, budget) {
|
|
350
|
-
db.prepare(`
|
|
351
|
-
INSERT OR REPLACE INTO budgets
|
|
352
|
-
(id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
353
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
354
|
-
`).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);
|
|
355
|
-
}
|
|
356
|
-
function listBudgets(db) {
|
|
357
|
-
return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
|
|
358
|
-
}
|
|
359
|
-
function deleteBudget(db, id) {
|
|
360
|
-
db.prepare(`DELETE FROM budgets WHERE id = ?`).run(id);
|
|
361
|
-
}
|
|
362
|
-
function getBudgetStatuses(db) {
|
|
363
|
-
const budgets = listBudgets(db);
|
|
364
|
-
return budgets.map((b) => {
|
|
365
|
-
const periodStart = b.period === "daily" ? "DATE('now')" : b.period === "weekly" ? "DATE('now', '-7 days')" : "DATE('now', '-30 days')";
|
|
366
|
-
let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
|
|
367
|
-
const params = [];
|
|
368
|
-
if (b.project_path) {
|
|
369
|
-
spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
|
|
370
|
-
params.push(b.project_path);
|
|
371
|
-
}
|
|
372
|
-
if (b.agent) {
|
|
373
|
-
spendQuery += ` AND agent = ?`;
|
|
374
|
-
params.push(b.agent);
|
|
375
|
-
}
|
|
376
|
-
const row = db.prepare(spendQuery).get(...params);
|
|
377
|
-
const spend = row.spend;
|
|
378
|
-
const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
|
|
379
|
-
return {
|
|
380
|
-
...b,
|
|
381
|
-
current_spend_usd: spend,
|
|
382
|
-
percent_used: percent,
|
|
383
|
-
is_over_limit: percent >= 100,
|
|
384
|
-
is_over_alert: percent >= b.alert_at_percent
|
|
385
|
-
};
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
function getIngestState(db, source, key) {
|
|
389
|
-
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
|
|
390
|
-
return row?.value ?? null;
|
|
391
|
-
}
|
|
392
|
-
function setIngestState(db, source, key, value) {
|
|
393
|
-
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
394
|
-
}
|
|
395
|
-
function upsertModelPricing(db, p) {
|
|
396
|
-
db.prepare(`
|
|
397
|
-
INSERT OR REPLACE INTO model_pricing
|
|
398
|
-
(model, input_per_1m, output_per_1m, cache_read_per_1m, cache_write_per_1m, updated_at)
|
|
399
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
400
|
-
`).run(p.model, p.input_per_1m, p.output_per_1m, p.cache_read_per_1m, p.cache_write_per_1m, p.updated_at);
|
|
401
|
-
}
|
|
402
|
-
function getModelPricing(db, model) {
|
|
403
|
-
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
404
|
-
}
|
|
405
|
-
function listModelPricing(db) {
|
|
406
|
-
return db.prepare(`SELECT * FROM model_pricing ORDER BY model ASC`).all();
|
|
407
|
-
}
|
|
408
|
-
function deleteModelPricing(db, model) {
|
|
409
|
-
db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
|
|
410
|
-
}
|
|
411
|
-
function seedModelPricing(db, defaults) {
|
|
412
|
-
const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
|
|
413
|
-
if (existing.count > 0)
|
|
414
|
-
return;
|
|
415
|
-
const now = new Date().toISOString();
|
|
416
|
-
for (const [model, p] of Object.entries(defaults)) {
|
|
417
|
-
upsertModelPricing(db, {
|
|
418
|
-
model,
|
|
419
|
-
input_per_1m: p.inputPer1M,
|
|
420
|
-
output_per_1m: p.outputPer1M,
|
|
421
|
-
cache_read_per_1m: p.cacheReadPer1M,
|
|
422
|
-
cache_write_per_1m: p.cacheWritePer1M,
|
|
423
|
-
updated_at: now
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
var init_database = () => {};
|
|
428
|
-
|
|
429
|
-
// src/server/serve.ts
|
|
430
|
-
init_database();
|
|
431
|
-
|
|
432
|
-
// src/ingest/claude.ts
|
|
433
|
-
init_database();
|
|
434
|
-
init_pricing();
|
|
435
|
-
import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
|
|
436
|
-
import { homedir as homedir2 } from "os";
|
|
437
|
-
import { join as join2, basename } from "path";
|
|
438
|
-
var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
|
|
439
|
-
function dirNameToPath(dirName) {
|
|
440
|
-
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
441
|
-
}
|
|
442
|
-
function collectJsonlFiles(projectDir) {
|
|
443
|
-
const files = [];
|
|
444
|
-
function walk(dir) {
|
|
445
|
-
try {
|
|
446
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
447
|
-
if (entry.isDirectory())
|
|
448
|
-
walk(join2(dir, entry.name));
|
|
449
|
-
else if (entry.name.endsWith(".jsonl"))
|
|
450
|
-
files.push(join2(dir, entry.name));
|
|
451
|
-
}
|
|
452
|
-
} catch {}
|
|
453
|
-
}
|
|
454
|
-
walk(projectDir);
|
|
455
|
-
return files;
|
|
456
|
-
}
|
|
457
|
-
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
458
|
-
if (!existsSync2(PROJECTS_DIR)) {
|
|
459
|
-
if (verbose)
|
|
460
|
-
console.log("Claude projects dir not found:", PROJECTS_DIR);
|
|
461
|
-
return { files: 0, requests: 0, sessions: 0 };
|
|
462
|
-
}
|
|
463
|
-
let totalFiles = 0;
|
|
464
|
-
let totalRequests = 0;
|
|
465
|
-
const touchedSessions = new Set;
|
|
466
|
-
const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
467
|
-
for (const projectDirEntry of projectDirs) {
|
|
468
|
-
const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
|
|
469
|
-
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
470
|
-
const projectName = basename(projectPath);
|
|
471
|
-
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
472
|
-
for (const filePath of jsonlFiles) {
|
|
473
|
-
const stateKey = filePath.replace(PROJECTS_DIR, "");
|
|
474
|
-
let fileMtime = "0";
|
|
475
|
-
try {
|
|
476
|
-
fileMtime = statSync(filePath).mtimeMs.toString();
|
|
477
|
-
} catch {
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
const processed = getIngestState(db, "claude", stateKey);
|
|
481
|
-
if (processed === fileMtime)
|
|
482
|
-
continue;
|
|
483
|
-
let lines;
|
|
484
|
-
try {
|
|
485
|
-
lines = readFileSync(filePath, "utf-8").split(`
|
|
486
|
-
`).filter((l) => l.trim());
|
|
487
|
-
} catch {
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
const fileBasename = basename(filePath, ".jsonl");
|
|
491
|
-
const isUuid = /^[0-9a-f-]{36}$/.test(fileBasename);
|
|
492
|
-
let sessionId = isUuid ? fileBasename : fileBasename.replace(/^agent-/, "");
|
|
493
|
-
let sessionCwd = projectPath;
|
|
494
|
-
for (const line of lines) {
|
|
495
|
-
let entry;
|
|
496
|
-
try {
|
|
497
|
-
entry = JSON.parse(line);
|
|
498
|
-
} catch {
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
if (entry.sessionId)
|
|
502
|
-
sessionId = entry.sessionId;
|
|
503
|
-
if (entry.cwd)
|
|
504
|
-
sessionCwd = entry.cwd;
|
|
505
|
-
if (entry.message?.role !== "assistant")
|
|
506
|
-
continue;
|
|
507
|
-
const usage = entry.message.usage;
|
|
508
|
-
if (!usage)
|
|
509
|
-
continue;
|
|
510
|
-
const model = entry.message.model;
|
|
511
|
-
if (!model)
|
|
512
|
-
continue;
|
|
513
|
-
const inputTokens = usage.input_tokens ?? 0;
|
|
514
|
-
const outputTokens = usage.output_tokens ?? 0;
|
|
515
|
-
const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
|
|
516
|
-
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
517
|
-
const timestamp = entry.timestamp ?? new Date().toISOString();
|
|
518
|
-
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
519
|
-
continue;
|
|
520
|
-
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
521
|
-
const reqId = `claude-${sessionId}-${timestamp}`;
|
|
522
|
-
upsertRequest(db, {
|
|
523
|
-
id: reqId,
|
|
524
|
-
agent: "claude",
|
|
525
|
-
session_id: sessionId,
|
|
526
|
-
model,
|
|
527
|
-
input_tokens: inputTokens,
|
|
528
|
-
output_tokens: outputTokens,
|
|
529
|
-
cache_read_tokens: cacheReadTokens,
|
|
530
|
-
cache_create_tokens: cacheWriteTokens,
|
|
531
|
-
cost_usd: costUsd,
|
|
532
|
-
duration_ms: 0,
|
|
533
|
-
timestamp,
|
|
534
|
-
source_request_id: reqId
|
|
535
|
-
});
|
|
536
|
-
if (!touchedSessions.has(sessionId)) {
|
|
537
|
-
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
538
|
-
if (!existing) {
|
|
539
|
-
const session = {
|
|
540
|
-
id: sessionId,
|
|
541
|
-
agent: "claude",
|
|
542
|
-
project_path: sessionCwd || projectPath,
|
|
543
|
-
project_name: basename(sessionCwd || projectPath),
|
|
544
|
-
started_at: timestamp,
|
|
545
|
-
ended_at: null,
|
|
546
|
-
total_cost_usd: 0,
|
|
547
|
-
total_tokens: 0,
|
|
548
|
-
request_count: 0
|
|
549
|
-
};
|
|
550
|
-
upsertSession(db, session);
|
|
551
|
-
}
|
|
552
|
-
touchedSessions.add(sessionId);
|
|
553
|
-
}
|
|
554
|
-
totalRequests++;
|
|
555
|
-
}
|
|
556
|
-
setIngestState(db, "claude", stateKey, fileMtime);
|
|
557
|
-
totalFiles++;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
for (const sessionId of touchedSessions) {
|
|
561
|
-
rollupSession(db, sessionId);
|
|
562
|
-
}
|
|
563
|
-
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// src/ingest/codex.ts
|
|
567
|
-
init_database();
|
|
568
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
569
|
-
import { homedir as homedir3 } from "os";
|
|
570
|
-
import { join as join3, basename as basename2 } from "path";
|
|
571
|
-
import { Database as Database2 } from "bun:sqlite";
|
|
572
|
-
var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
573
|
-
var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
574
|
-
function readCodexModel() {
|
|
575
|
-
if (!existsSync3(CODEX_CONFIG_PATH))
|
|
576
|
-
return "gpt-5.3-codex";
|
|
577
|
-
try {
|
|
578
|
-
const content = readFileSync2(CODEX_CONFIG_PATH, "utf-8");
|
|
579
|
-
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
580
|
-
return match?.[1] ?? "gpt-5.3-codex";
|
|
581
|
-
} catch {
|
|
582
|
-
return "gpt-5.3-codex";
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
async function ingestCodex(db, verbose = false) {
|
|
586
|
-
if (!existsSync3(CODEX_DB_PATH)) {
|
|
587
|
-
if (verbose)
|
|
588
|
-
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
589
|
-
return { sessions: 0 };
|
|
590
|
-
}
|
|
591
|
-
const model = readCodexModel();
|
|
592
|
-
let codexDb = null;
|
|
593
|
-
let ingested = 0;
|
|
594
|
-
try {
|
|
595
|
-
codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
|
|
596
|
-
const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
|
|
597
|
-
for (const thread of threads) {
|
|
598
|
-
const stateKey = thread.id;
|
|
599
|
-
const processed = getIngestState(db, "codex", stateKey);
|
|
600
|
-
if (processed === "done")
|
|
601
|
-
continue;
|
|
602
|
-
const costUsd = 0;
|
|
603
|
-
const projectPath = thread.cwd ?? "";
|
|
604
|
-
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
605
|
-
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
606
|
-
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
607
|
-
upsertSession(db, {
|
|
608
|
-
id: `codex-${thread.id}`,
|
|
609
|
-
agent: "codex",
|
|
610
|
-
project_path: projectPath,
|
|
611
|
-
project_name: projectName,
|
|
612
|
-
started_at: startedAt,
|
|
613
|
-
ended_at: endedAt,
|
|
614
|
-
total_cost_usd: costUsd,
|
|
615
|
-
total_tokens: thread.tokens_used,
|
|
616
|
-
request_count: 1
|
|
617
|
-
});
|
|
618
|
-
setIngestState(db, "codex", stateKey, "done");
|
|
619
|
-
ingested++;
|
|
620
|
-
if (verbose)
|
|
621
|
-
console.log(`Codex session ${thread.id}: ${thread.tokens_used} tokens \u2192 $${costUsd.toFixed(4)}`);
|
|
622
|
-
}
|
|
623
|
-
} finally {
|
|
624
|
-
codexDb?.close();
|
|
625
|
-
}
|
|
626
|
-
return { sessions: ingested };
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// src/server/serve.ts
|
|
630
|
-
init_pricing();
|
|
631
|
-
import { randomUUID } from "crypto";
|
|
632
|
-
var CORS = {
|
|
633
|
-
"Access-Control-Allow-Origin": "*",
|
|
634
|
-
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
|
635
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
636
|
-
};
|
|
637
|
-
function json(data, status = 200) {
|
|
638
|
-
return new Response(JSON.stringify(data), {
|
|
639
|
-
status,
|
|
640
|
-
headers: { "Content-Type": "application/json", ...CORS }
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
function ok(data, meta) {
|
|
644
|
-
return json({ data, meta: meta ?? {} });
|
|
645
|
-
}
|
|
646
|
-
function err(message, status = 400) {
|
|
647
|
-
return json({ error: message }, status);
|
|
648
|
-
}
|
|
649
|
-
function applyFields(obj, fields) {
|
|
650
|
-
if (!fields || fields.length === 0)
|
|
651
|
-
return obj;
|
|
652
|
-
return Object.fromEntries(fields.map((f) => [f, obj[f] ?? null]));
|
|
653
|
-
}
|
|
654
|
-
function createHandler(db) {
|
|
655
|
-
return async function handler(req) {
|
|
656
|
-
const url = new URL(req.url);
|
|
657
|
-
const path = url.pathname;
|
|
658
|
-
const method = req.method;
|
|
659
|
-
if (method === "OPTIONS")
|
|
660
|
-
return new Response(null, { status: 204, headers: CORS });
|
|
661
|
-
if (path === "/health")
|
|
662
|
-
return ok({ status: "ok", ts: new Date().toISOString() });
|
|
663
|
-
if (path === "/api/summary" && method === "GET") {
|
|
664
|
-
const period = url.searchParams.get("period") ?? "today";
|
|
665
|
-
return ok(querySummary(db, period));
|
|
666
|
-
}
|
|
667
|
-
if (path === "/api/daily" && method === "GET") {
|
|
668
|
-
const days = Number(url.searchParams.get("days") ?? 30);
|
|
669
|
-
return ok(queryDailyBreakdown(db, days));
|
|
670
|
-
}
|
|
671
|
-
if (path === "/api/sessions" && method === "GET") {
|
|
672
|
-
const agent = url.searchParams.get("agent");
|
|
673
|
-
const project = url.searchParams.get("project") ?? undefined;
|
|
674
|
-
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
675
|
-
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
676
|
-
const since = url.searchParams.get("since") ?? undefined;
|
|
677
|
-
const fieldsParam = url.searchParams.get("fields");
|
|
678
|
-
const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
679
|
-
const sessions = querySessions(db, { agent: agent ?? undefined, project, limit, offset, since });
|
|
680
|
-
return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
|
|
681
|
-
}
|
|
682
|
-
if (path === "/api/top" && method === "GET") {
|
|
683
|
-
const n = Number(url.searchParams.get("n") ?? 10);
|
|
684
|
-
const agent = url.searchParams.get("agent") ?? undefined;
|
|
685
|
-
return ok(queryTopSessions(db, n, agent));
|
|
686
|
-
}
|
|
687
|
-
if (path === "/api/models" && method === "GET") {
|
|
688
|
-
return ok(queryModelBreakdown(db));
|
|
689
|
-
}
|
|
690
|
-
if (path === "/api/projects" && method === "GET") {
|
|
691
|
-
return ok(queryProjectBreakdown(db));
|
|
692
|
-
}
|
|
693
|
-
if (path === "/api/breakdown" && method === "GET") {
|
|
694
|
-
const by = url.searchParams.get("by") ?? "model";
|
|
695
|
-
return ok(by === "project" ? queryProjectBreakdown(db) : queryModelBreakdown(db));
|
|
696
|
-
}
|
|
697
|
-
if (path === "/api/budgets" && method === "GET") {
|
|
698
|
-
return ok(getBudgetStatuses(db));
|
|
699
|
-
}
|
|
700
|
-
if (path === "/api/budgets" && method === "POST") {
|
|
701
|
-
const body = await req.json();
|
|
702
|
-
const now = new Date().toISOString();
|
|
703
|
-
upsertBudget(db, {
|
|
704
|
-
id: randomUUID(),
|
|
705
|
-
project_path: body["project_path"] ?? null,
|
|
706
|
-
agent: body["agent"] ?? null,
|
|
707
|
-
period: body["period"] ?? "monthly",
|
|
708
|
-
limit_usd: Number(body["limit_usd"]),
|
|
709
|
-
alert_at_percent: Number(body["alert_at_percent"] ?? 80),
|
|
710
|
-
created_at: now,
|
|
711
|
-
updated_at: now
|
|
712
|
-
});
|
|
713
|
-
return ok({ ok: true });
|
|
714
|
-
}
|
|
715
|
-
const budgetMatch = path.match(/^\/api\/budgets\/(.+)$/);
|
|
716
|
-
if (budgetMatch && method === "DELETE") {
|
|
717
|
-
deleteBudget(db, budgetMatch[1]);
|
|
718
|
-
return ok({ ok: true });
|
|
719
|
-
}
|
|
720
|
-
if (path === "/api/project-registry" && method === "GET") {
|
|
721
|
-
return ok(listProjects(db));
|
|
722
|
-
}
|
|
723
|
-
if (path === "/api/project-registry" && method === "POST") {
|
|
724
|
-
const body = await req.json();
|
|
725
|
-
const { basename: basename3 } = await import("path");
|
|
726
|
-
const projPath = body["path"];
|
|
727
|
-
upsertProject(db, {
|
|
728
|
-
id: randomUUID(),
|
|
729
|
-
path: projPath,
|
|
730
|
-
name: body["name"] ?? basename3(projPath),
|
|
731
|
-
description: body["description"] ?? null,
|
|
732
|
-
tags: body["tags"] ?? [],
|
|
733
|
-
created_at: new Date().toISOString()
|
|
734
|
-
});
|
|
735
|
-
return ok({ ok: true });
|
|
736
|
-
}
|
|
737
|
-
const projMatch = path.match(/^\/api\/project-registry\/(.+)$/);
|
|
738
|
-
if (projMatch && method === "DELETE") {
|
|
739
|
-
deleteProject(db, decodeURIComponent(projMatch[1]));
|
|
740
|
-
return ok({ ok: true });
|
|
741
|
-
}
|
|
742
|
-
if (path === "/api/pricing" && method === "GET") {
|
|
743
|
-
return ok(listModelPricing(db));
|
|
744
|
-
}
|
|
745
|
-
if (path === "/api/pricing" && method === "POST") {
|
|
746
|
-
const body = await req.json();
|
|
747
|
-
upsertModelPricing(db, {
|
|
748
|
-
model: body["model"],
|
|
749
|
-
input_per_1m: Number(body["input_per_1m"]),
|
|
750
|
-
output_per_1m: Number(body["output_per_1m"]),
|
|
751
|
-
cache_read_per_1m: Number(body["cache_read_per_1m"] ?? 0),
|
|
752
|
-
cache_write_per_1m: Number(body["cache_write_per_1m"] ?? 0),
|
|
753
|
-
updated_at: new Date().toISOString()
|
|
754
|
-
});
|
|
755
|
-
return ok({ ok: true });
|
|
756
|
-
}
|
|
757
|
-
const pricingMatch = path.match(/^\/api\/pricing\/(.+)$/);
|
|
758
|
-
if (pricingMatch && method === "DELETE") {
|
|
759
|
-
deleteModelPricing(db, decodeURIComponent(pricingMatch[1]));
|
|
760
|
-
return ok({ ok: true });
|
|
761
|
-
}
|
|
762
|
-
if (path === "/api/sync" && method === "POST") {
|
|
763
|
-
const body = await req.json().catch(() => ({}));
|
|
764
|
-
const sources = body["sources"] ?? "all";
|
|
765
|
-
const results = {};
|
|
766
|
-
if (sources === "all" || sources === "claude")
|
|
767
|
-
results["claude"] = await ingestClaude(db);
|
|
768
|
-
if (sources === "all" || sources === "codex")
|
|
769
|
-
results["codex"] = await ingestCodex(db);
|
|
770
|
-
return ok(results);
|
|
771
|
-
}
|
|
772
|
-
return err("Not found", 404);
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
function startServer(port = 3456) {
|
|
776
|
-
const db = openDatabase();
|
|
777
|
-
ensurePricingSeeded(db);
|
|
778
|
-
const apiHandler = createHandler(db);
|
|
779
|
-
const dashboardDir = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
780
|
-
Bun.serve({
|
|
781
|
-
port,
|
|
782
|
-
async fetch(req) {
|
|
783
|
-
const url = new URL(req.url);
|
|
784
|
-
if (url.pathname.startsWith("/api") || url.pathname === "/health") {
|
|
785
|
-
return apiHandler(req);
|
|
786
|
-
}
|
|
787
|
-
try {
|
|
788
|
-
const { existsSync: existsSync4 } = await import("fs");
|
|
789
|
-
if (existsSync4(dashboardDir)) {
|
|
790
|
-
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
791
|
-
const fullPath = dashboardDir + filePath;
|
|
792
|
-
if (existsSync4(fullPath)) {
|
|
793
|
-
return new Response(Bun.file(fullPath));
|
|
794
|
-
}
|
|
795
|
-
const indexPath = dashboardDir + "/index.html";
|
|
796
|
-
if (existsSync4(indexPath)) {
|
|
797
|
-
return new Response(Bun.file(indexPath));
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
} catch {}
|
|
801
|
-
return apiHandler(req);
|
|
802
|
-
}
|
|
803
|
-
});
|
|
804
|
-
console.log(`economy-serve listening on http://localhost:${port}`);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// src/server/index.ts
|
|
808
|
-
var port = Number(process.env["ECONOMY_PORT"] ?? 3456);
|
|
809
|
-
startServer(port);
|