@hasna/economy 0.2.16 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +186 -13
- package/README.md +186 -13
- package/dashboard/dist/assets/index-5mUN0CPj.css +1 -0
- package/dashboard/dist/assets/index-L1FgNQ4t.js +93 -0
- package/dashboard/dist/index.html +14 -0
- package/dashboard/dist/logo.jpg +0 -0
- package/dashboard/dist/vite.svg +1 -0
- package/dist/cli/index.js +131 -757
- package/dist/db/database.d.ts +1 -23
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/index.js +51 -180
- package/dist/ingest/claude.d.ts +1 -6
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +1 -1
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/webhooks.d.ts +1 -1
- package/dist/lib/webhooks.d.ts.map +1 -1
- package/dist/mcp/index.js +381 -574
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.js +60 -331
- package/dist/server/serve.d.ts +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -4
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -3
- package/dist/ingest/billing.d.ts +0 -18
- package/dist/ingest/billing.d.ts.map +0 -1
- package/dist/lib/package-metadata.d.ts +0 -8
- package/dist/lib/package-metadata.d.ts.map +0 -1
package/dist/server/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
1
|
// @bun
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __returnValue = (v) => v;
|
|
@@ -79,7 +78,6 @@ var DEFAULT_PRICING;
|
|
|
79
78
|
var init_pricing = __esm(() => {
|
|
80
79
|
init_database();
|
|
81
80
|
DEFAULT_PRICING = {
|
|
82
|
-
"claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
83
81
|
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
84
82
|
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
85
83
|
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
@@ -90,55 +88,28 @@ var init_pricing = __esm(() => {
|
|
|
90
88
|
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
|
|
91
89
|
"claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
92
90
|
"claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
|
|
93
|
-
"gemini-3.1-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
|
|
94
|
-
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
95
|
-
"gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
96
91
|
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
92
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
97
93
|
"gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
98
94
|
"gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
99
|
-
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
100
|
-
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
101
|
-
"gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
102
95
|
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
103
|
-
"gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
104
96
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
105
97
|
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
106
|
-
"gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
107
|
-
"gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
108
98
|
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
109
99
|
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
110
100
|
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
111
101
|
"o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
|
|
112
102
|
o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
|
|
113
103
|
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
114
|
-
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
|
|
115
|
-
"qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
116
|
-
"qwen3.6": { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
117
|
-
"minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
118
|
-
"minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
119
|
-
"minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
120
|
-
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
121
|
-
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
122
|
-
"glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
123
|
-
"glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
124
|
-
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
104
|
+
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
|
|
125
105
|
};
|
|
126
106
|
});
|
|
127
107
|
|
|
128
108
|
// src/db/database.ts
|
|
129
109
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
130
110
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
131
|
-
import { hostname } from "os";
|
|
132
111
|
import { homedir } from "os";
|
|
133
112
|
import { join } from "path";
|
|
134
|
-
function getMachineId() {
|
|
135
|
-
if (process.env["ECONOMY_MACHINE_ID"])
|
|
136
|
-
return process.env["ECONOMY_MACHINE_ID"];
|
|
137
|
-
const h = hostname().toLowerCase();
|
|
138
|
-
if (h.startsWith("spark") || h.startsWith("apple"))
|
|
139
|
-
return h.split(".")[0];
|
|
140
|
-
return h.split(".")[0];
|
|
141
|
-
}
|
|
142
113
|
function getDataDir() {
|
|
143
114
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
144
115
|
const newDir = join(home, ".hasna", "economy");
|
|
@@ -171,7 +142,6 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
171
142
|
}
|
|
172
143
|
const db = new Database(path);
|
|
173
144
|
db.exec("PRAGMA journal_mode = WAL");
|
|
174
|
-
db.exec("PRAGMA busy_timeout = 5000");
|
|
175
145
|
db.exec("PRAGMA foreign_keys = ON");
|
|
176
146
|
initSchema(db);
|
|
177
147
|
if (!skipSeed) {
|
|
@@ -193,8 +163,7 @@ function initSchema(db) {
|
|
|
193
163
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
194
164
|
duration_ms INTEGER DEFAULT 0,
|
|
195
165
|
timestamp TEXT NOT NULL,
|
|
196
|
-
source_request_id TEXT
|
|
197
|
-
machine_id TEXT DEFAULT ''
|
|
166
|
+
source_request_id TEXT
|
|
198
167
|
);
|
|
199
168
|
|
|
200
169
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -206,8 +175,7 @@ function initSchema(db) {
|
|
|
206
175
|
ended_at TEXT,
|
|
207
176
|
total_cost_usd REAL DEFAULT 0,
|
|
208
177
|
total_tokens INTEGER DEFAULT 0,
|
|
209
|
-
request_count INTEGER DEFAULT 0
|
|
210
|
-
machine_id TEXT DEFAULT ''
|
|
178
|
+
request_count INTEGER DEFAULT 0
|
|
211
179
|
);
|
|
212
180
|
|
|
213
181
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -272,27 +240,6 @@ function initSchema(db) {
|
|
|
272
240
|
machine_id TEXT,
|
|
273
241
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
274
242
|
);
|
|
275
|
-
|
|
276
|
-
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
277
|
-
date TEXT NOT NULL,
|
|
278
|
-
provider TEXT NOT NULL,
|
|
279
|
-
description TEXT DEFAULT '',
|
|
280
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
281
|
-
updated_at TEXT NOT NULL,
|
|
282
|
-
PRIMARY KEY (date, provider, description)
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
286
|
-
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
287
|
-
`);
|
|
288
|
-
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
289
|
-
if (!cols.some((c) => c.name === "machine_id")) {
|
|
290
|
-
db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
291
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
292
|
-
}
|
|
293
|
-
db.exec(`
|
|
294
|
-
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
295
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
296
243
|
`);
|
|
297
244
|
}
|
|
298
245
|
function periodWhere(period) {
|
|
@@ -302,11 +249,11 @@ function periodWhere(period) {
|
|
|
302
249
|
case "yesterday":
|
|
303
250
|
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
304
251
|
case "week":
|
|
305
|
-
return `timestamp >= DATE('now', '
|
|
252
|
+
return `timestamp >= DATE('now', '-7 days')`;
|
|
306
253
|
case "month":
|
|
307
|
-
return `timestamp >= DATE('now', '
|
|
254
|
+
return `timestamp >= DATE('now', '-30 days')`;
|
|
308
255
|
case "year":
|
|
309
|
-
return `timestamp >= DATE('now', '
|
|
256
|
+
return `timestamp >= DATE('now', '-365 days')`;
|
|
310
257
|
case "all":
|
|
311
258
|
return "1=1";
|
|
312
259
|
}
|
|
@@ -318,11 +265,11 @@ function sessionPeriodWhere(period) {
|
|
|
318
265
|
case "yesterday":
|
|
319
266
|
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
320
267
|
case "week":
|
|
321
|
-
return `started_at >= DATE('now', '
|
|
268
|
+
return `started_at >= DATE('now', '-7 days')`;
|
|
322
269
|
case "month":
|
|
323
|
-
return `started_at >= DATE('now', '
|
|
270
|
+
return `started_at >= DATE('now', '-30 days')`;
|
|
324
271
|
case "year":
|
|
325
|
-
return `started_at >= DATE('now', '
|
|
272
|
+
return `started_at >= DATE('now', '-365 days')`;
|
|
326
273
|
case "all":
|
|
327
274
|
return "1=1";
|
|
328
275
|
}
|
|
@@ -332,17 +279,17 @@ function upsertRequest(db, req) {
|
|
|
332
279
|
INSERT OR REPLACE INTO requests
|
|
333
280
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
334
281
|
cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
|
|
335
|
-
timestamp, source_request_id
|
|
336
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
337
|
-
`).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
|
|
282
|
+
timestamp, source_request_id)
|
|
283
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
284
|
+
`).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);
|
|
338
285
|
}
|
|
339
286
|
function upsertSession(db, session) {
|
|
340
287
|
db.prepare(`
|
|
341
288
|
INSERT OR REPLACE INTO sessions
|
|
342
289
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
343
|
-
total_cost_usd, total_tokens, request_count
|
|
344
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
345
|
-
`).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
|
|
290
|
+
total_cost_usd, total_tokens, request_count)
|
|
291
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
292
|
+
`).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);
|
|
346
293
|
}
|
|
347
294
|
function rollupSession(db, sessionId) {
|
|
348
295
|
db.prepare(`
|
|
@@ -372,10 +319,6 @@ function querySessions(db, filter = {}) {
|
|
|
372
319
|
conditions.push("started_at >= ?");
|
|
373
320
|
params.push(filter.since);
|
|
374
321
|
}
|
|
375
|
-
if (filter.machine) {
|
|
376
|
-
conditions.push("machine_id = ?");
|
|
377
|
-
params.push(filter.machine);
|
|
378
|
-
}
|
|
379
322
|
if (filter.search) {
|
|
380
323
|
const q = `%${filter.search}%`;
|
|
381
324
|
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
@@ -394,25 +337,24 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
394
337
|
}
|
|
395
338
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
396
339
|
}
|
|
397
|
-
function querySummary(db, period
|
|
340
|
+
function querySummary(db, period) {
|
|
398
341
|
const rWhere = periodWhere(period);
|
|
399
342
|
const sWhere = sessionPeriodWhere(period);
|
|
400
|
-
const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
401
343
|
const r = db.prepare(`
|
|
402
344
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
403
345
|
COUNT(*) as requests,
|
|
404
346
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
405
|
-
FROM requests WHERE ${rWhere}
|
|
347
|
+
FROM requests WHERE ${rWhere}
|
|
406
348
|
`).get();
|
|
407
349
|
const codexTotals = db.prepare(`
|
|
408
350
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
409
351
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
410
352
|
COUNT(*) as sessions
|
|
411
353
|
FROM sessions
|
|
412
|
-
WHERE ${sWhere}
|
|
354
|
+
WHERE ${sWhere}
|
|
413
355
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
414
356
|
`).get();
|
|
415
|
-
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}
|
|
357
|
+
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
|
|
416
358
|
return {
|
|
417
359
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
418
360
|
requests: r.requests,
|
|
@@ -434,39 +376,19 @@ function queryModelBreakdown(db) {
|
|
|
434
376
|
}
|
|
435
377
|
function queryProjectBreakdown(db) {
|
|
436
378
|
return db.prepare(`
|
|
437
|
-
WITH labeled AS (
|
|
438
|
-
SELECT
|
|
439
|
-
s.id,
|
|
440
|
-
s.project_path,
|
|
441
|
-
s.total_cost_usd,
|
|
442
|
-
s.started_at,
|
|
443
|
-
COALESCE(
|
|
444
|
-
NULLIF(s.project_name, ''),
|
|
445
|
-
CASE
|
|
446
|
-
WHEN s.project_path LIKE '%/%'
|
|
447
|
-
THEN substr(s.project_path, length(rtrim(s.project_path, replace(s.project_path, '/', ''))) + 1)
|
|
448
|
-
ELSE s.project_path
|
|
449
|
-
END
|
|
450
|
-
) as label
|
|
451
|
-
FROM sessions s
|
|
452
|
-
WHERE s.project_path != '' OR s.project_name != ''
|
|
453
|
-
)
|
|
454
379
|
SELECT
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
COUNT(DISTINCT
|
|
458
|
-
|
|
459
|
-
COALESCE(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
MAX(l.started_at) as last_active
|
|
468
|
-
FROM labeled l
|
|
469
|
-
GROUP BY l.label
|
|
380
|
+
s.project_path,
|
|
381
|
+
COALESCE(p.name, s.project_name) as project_name,
|
|
382
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
383
|
+
COUNT(r.id) as requests,
|
|
384
|
+
COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
|
|
385
|
+
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
386
|
+
MAX(s.started_at) as last_active
|
|
387
|
+
FROM sessions s
|
|
388
|
+
LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
|
|
389
|
+
LEFT JOIN requests r ON r.session_id = s.id
|
|
390
|
+
WHERE s.project_path != '' OR s.project_name != ''
|
|
391
|
+
GROUP BY s.project_path
|
|
470
392
|
ORDER BY cost_usd DESC
|
|
471
393
|
`).all();
|
|
472
394
|
}
|
|
@@ -577,20 +499,6 @@ function getIngestState(db, source, key) {
|
|
|
577
499
|
function setIngestState(db, source, key, value) {
|
|
578
500
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
579
501
|
}
|
|
580
|
-
function listMachines(db) {
|
|
581
|
-
return db.prepare(`
|
|
582
|
-
SELECT
|
|
583
|
-
s.machine_id,
|
|
584
|
-
COUNT(DISTINCT s.id) as sessions,
|
|
585
|
-
COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
|
|
586
|
-
COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
|
|
587
|
-
MAX(s.started_at) as last_active
|
|
588
|
-
FROM sessions s
|
|
589
|
-
WHERE s.machine_id != ''
|
|
590
|
-
GROUP BY s.machine_id
|
|
591
|
-
ORDER BY total_cost_usd DESC
|
|
592
|
-
`).all();
|
|
593
|
-
}
|
|
594
502
|
function upsertModelPricing(db, p) {
|
|
595
503
|
db.prepare(`
|
|
596
504
|
INSERT OR REPLACE INTO model_pricing
|
|
@@ -608,11 +516,11 @@ function deleteModelPricing(db, model) {
|
|
|
608
516
|
db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
|
|
609
517
|
}
|
|
610
518
|
function seedModelPricing(db, defaults) {
|
|
611
|
-
const existing =
|
|
519
|
+
const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
|
|
520
|
+
if (existing.count > 0)
|
|
521
|
+
return;
|
|
612
522
|
const now = new Date().toISOString();
|
|
613
523
|
for (const [model, p] of Object.entries(defaults)) {
|
|
614
|
-
if (existing.has(model))
|
|
615
|
-
continue;
|
|
616
524
|
upsertModelPricing(db, {
|
|
617
525
|
model,
|
|
618
526
|
input_per_1m: p.inputPer1M,
|
|
@@ -637,8 +545,7 @@ import { join as join2, basename } from "path";
|
|
|
637
545
|
function autoDetectProject(cwd, projects) {
|
|
638
546
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
639
547
|
}
|
|
640
|
-
var
|
|
641
|
-
var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
|
|
548
|
+
var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
|
|
642
549
|
function dirNameToPath(dirName) {
|
|
643
550
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
644
551
|
}
|
|
@@ -658,36 +565,29 @@ function collectJsonlFiles(projectDir) {
|
|
|
658
565
|
return files;
|
|
659
566
|
}
|
|
660
567
|
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
661
|
-
|
|
662
|
-
}
|
|
663
|
-
async function ingestTakumi(db, verbose = false) {
|
|
664
|
-
return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
|
|
665
|
-
}
|
|
666
|
-
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
667
|
-
if (!existsSync2(projectsDir)) {
|
|
568
|
+
if (!existsSync2(PROJECTS_DIR)) {
|
|
668
569
|
if (verbose)
|
|
669
|
-
console.log(
|
|
570
|
+
console.log("Claude projects dir not found:", PROJECTS_DIR);
|
|
670
571
|
return { files: 0, requests: 0, sessions: 0 };
|
|
671
572
|
}
|
|
672
|
-
const machineId = getMachineId();
|
|
673
573
|
let totalFiles = 0;
|
|
674
574
|
let totalRequests = 0;
|
|
675
575
|
const touchedSessions = new Set;
|
|
676
576
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
677
|
-
const projectDirs = readdirSync2(
|
|
577
|
+
const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
678
578
|
for (const projectDirEntry of projectDirs) {
|
|
679
|
-
const projectDirPath = join2(
|
|
579
|
+
const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
|
|
680
580
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
681
581
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
682
582
|
for (const filePath of jsonlFiles) {
|
|
683
|
-
const stateKey = filePath.replace(
|
|
583
|
+
const stateKey = filePath.replace(PROJECTS_DIR, "");
|
|
684
584
|
let fileMtime = "0";
|
|
685
585
|
try {
|
|
686
586
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
687
587
|
} catch {
|
|
688
588
|
continue;
|
|
689
589
|
}
|
|
690
|
-
const processed = getIngestState(db,
|
|
590
|
+
const processed = getIngestState(db, "claude", stateKey);
|
|
691
591
|
if (processed === fileMtime)
|
|
692
592
|
continue;
|
|
693
593
|
let lines;
|
|
@@ -728,10 +628,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
728
628
|
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
729
629
|
continue;
|
|
730
630
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
731
|
-
const reqId =
|
|
631
|
+
const reqId = `claude-${sessionId}-${timestamp}`;
|
|
732
632
|
upsertRequest(db, {
|
|
733
633
|
id: reqId,
|
|
734
|
-
agent:
|
|
634
|
+
agent: "claude",
|
|
735
635
|
session_id: sessionId,
|
|
736
636
|
model,
|
|
737
637
|
input_tokens: inputTokens,
|
|
@@ -741,8 +641,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
741
641
|
cost_usd: costUsd,
|
|
742
642
|
duration_ms: 0,
|
|
743
643
|
timestamp,
|
|
744
|
-
source_request_id: reqId
|
|
745
|
-
machine_id: machineId
|
|
644
|
+
source_request_id: reqId
|
|
746
645
|
});
|
|
747
646
|
if (!touchedSessions.has(sessionId)) {
|
|
748
647
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
@@ -751,15 +650,14 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
751
650
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
752
651
|
const session = {
|
|
753
652
|
id: sessionId,
|
|
754
|
-
agent:
|
|
653
|
+
agent: "claude",
|
|
755
654
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
756
655
|
project_name: detectedProject ? detectedProject.name : "",
|
|
757
656
|
started_at: timestamp,
|
|
758
657
|
ended_at: null,
|
|
759
658
|
total_cost_usd: 0,
|
|
760
659
|
total_tokens: 0,
|
|
761
|
-
request_count: 0
|
|
762
|
-
machine_id: machineId
|
|
660
|
+
request_count: 0
|
|
763
661
|
};
|
|
764
662
|
upsertSession(db, session);
|
|
765
663
|
}
|
|
@@ -767,7 +665,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
767
665
|
}
|
|
768
666
|
totalRequests++;
|
|
769
667
|
}
|
|
770
|
-
setIngestState(db,
|
|
668
|
+
setIngestState(db, "claude", stateKey, fileMtime);
|
|
771
669
|
totalFiles++;
|
|
772
670
|
}
|
|
773
671
|
}
|
|
@@ -782,7 +680,7 @@ init_database();
|
|
|
782
680
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
783
681
|
import { homedir as homedir3 } from "os";
|
|
784
682
|
import { join as join3, basename as basename2 } from "path";
|
|
785
|
-
import { Database as
|
|
683
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
786
684
|
var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
787
685
|
var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
788
686
|
async function ingestCodex(db, verbose = false) {
|
|
@@ -791,11 +689,10 @@ async function ingestCodex(db, verbose = false) {
|
|
|
791
689
|
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
792
690
|
return { sessions: 0 };
|
|
793
691
|
}
|
|
794
|
-
const machineId = getMachineId();
|
|
795
692
|
let codexDb = null;
|
|
796
693
|
let ingested = 0;
|
|
797
694
|
try {
|
|
798
|
-
codexDb = new
|
|
695
|
+
codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
|
|
799
696
|
const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
|
|
800
697
|
for (const thread of threads) {
|
|
801
698
|
const stateKey = thread.id;
|
|
@@ -816,8 +713,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
816
713
|
ended_at: endedAt,
|
|
817
714
|
total_cost_usd: costUsd,
|
|
818
715
|
total_tokens: thread.tokens_used,
|
|
819
|
-
request_count: 1
|
|
820
|
-
machine_id: machineId
|
|
716
|
+
request_count: 1
|
|
821
717
|
});
|
|
822
718
|
setIngestState(db, "codex", stateKey, "done");
|
|
823
719
|
ingested++;
|
|
@@ -830,85 +726,6 @@ async function ingestCodex(db, verbose = false) {
|
|
|
830
726
|
return { sessions: ingested };
|
|
831
727
|
}
|
|
832
728
|
|
|
833
|
-
// src/ingest/gemini.ts
|
|
834
|
-
init_database();
|
|
835
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
|
|
836
|
-
import { homedir as homedir4 } from "os";
|
|
837
|
-
import { join as join4 } from "path";
|
|
838
|
-
var GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
|
|
839
|
-
async function ingestGemini(db, verbose) {
|
|
840
|
-
if (!existsSync4(GEMINI_TMP_DIR)) {
|
|
841
|
-
if (verbose)
|
|
842
|
-
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
843
|
-
return { sessions: 0 };
|
|
844
|
-
}
|
|
845
|
-
const machineId = getMachineId();
|
|
846
|
-
let totalSessions = 0;
|
|
847
|
-
const touchedSessions = new Set;
|
|
848
|
-
let projectHashDirs = [];
|
|
849
|
-
try {
|
|
850
|
-
projectHashDirs = readdirSync3(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join4(GEMINI_TMP_DIR, d.name));
|
|
851
|
-
} catch {
|
|
852
|
-
return { sessions: 0 };
|
|
853
|
-
}
|
|
854
|
-
for (const projectDir of projectHashDirs) {
|
|
855
|
-
const chatsDir = join4(projectDir, "chats");
|
|
856
|
-
if (!existsSync4(chatsDir))
|
|
857
|
-
continue;
|
|
858
|
-
let chatFiles = [];
|
|
859
|
-
try {
|
|
860
|
-
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join4(chatsDir, f));
|
|
861
|
-
} catch {
|
|
862
|
-
continue;
|
|
863
|
-
}
|
|
864
|
-
for (const filePath of chatFiles) {
|
|
865
|
-
const stateKey = filePath.replace(homedir4(), "~");
|
|
866
|
-
let fileMtime = "0";
|
|
867
|
-
try {
|
|
868
|
-
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
869
|
-
} catch {
|
|
870
|
-
continue;
|
|
871
|
-
}
|
|
872
|
-
const processed = getIngestState(db, "gemini", stateKey);
|
|
873
|
-
if (processed === fileMtime)
|
|
874
|
-
continue;
|
|
875
|
-
let chatData;
|
|
876
|
-
try {
|
|
877
|
-
chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
878
|
-
} catch {
|
|
879
|
-
continue;
|
|
880
|
-
}
|
|
881
|
-
const sessionId = chatData.sessionId;
|
|
882
|
-
if (!sessionId)
|
|
883
|
-
continue;
|
|
884
|
-
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
885
|
-
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
886
|
-
if (!existing) {
|
|
887
|
-
const session = {
|
|
888
|
-
id: sessionId,
|
|
889
|
-
agent: "gemini",
|
|
890
|
-
project_path: "",
|
|
891
|
-
project_name: "",
|
|
892
|
-
started_at: startTime,
|
|
893
|
-
ended_at: chatData.lastUpdated ?? null,
|
|
894
|
-
total_cost_usd: 0,
|
|
895
|
-
total_tokens: 0,
|
|
896
|
-
request_count: 0,
|
|
897
|
-
machine_id: machineId
|
|
898
|
-
};
|
|
899
|
-
upsertSession(db, session);
|
|
900
|
-
touchedSessions.add(sessionId);
|
|
901
|
-
totalSessions++;
|
|
902
|
-
}
|
|
903
|
-
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
for (const sessionId of touchedSessions) {
|
|
907
|
-
rollupSession(db, sessionId);
|
|
908
|
-
}
|
|
909
|
-
return { sessions: totalSessions };
|
|
910
|
-
}
|
|
911
|
-
|
|
912
729
|
// src/server/serve.ts
|
|
913
730
|
init_pricing();
|
|
914
731
|
import { randomUUID } from "crypto";
|
|
@@ -929,20 +746,6 @@ function ok(data, meta) {
|
|
|
929
746
|
function err(message, status = 400) {
|
|
930
747
|
return json({ error: message }, status);
|
|
931
748
|
}
|
|
932
|
-
function normalizeBudgetPeriod(value) {
|
|
933
|
-
switch (value) {
|
|
934
|
-
case "day":
|
|
935
|
-
case "daily":
|
|
936
|
-
return "daily";
|
|
937
|
-
case "week":
|
|
938
|
-
case "weekly":
|
|
939
|
-
return "weekly";
|
|
940
|
-
case "month":
|
|
941
|
-
case "monthly":
|
|
942
|
-
default:
|
|
943
|
-
return "monthly";
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
749
|
function applyFields(obj, fields) {
|
|
947
750
|
if (!fields || fields.length === 0)
|
|
948
751
|
return obj;
|
|
@@ -959,11 +762,7 @@ function createHandler(db) {
|
|
|
959
762
|
return ok({ status: "ok", ts: new Date().toISOString() });
|
|
960
763
|
if (path === "/api/summary" && method === "GET") {
|
|
961
764
|
const period = url.searchParams.get("period") ?? "today";
|
|
962
|
-
|
|
963
|
-
return ok(querySummary(db, period, machine));
|
|
964
|
-
}
|
|
965
|
-
if (path === "/api/machines" && method === "GET") {
|
|
966
|
-
return ok(listMachines(db), { current_machine: getMachineId() });
|
|
765
|
+
return ok(querySummary(db, period));
|
|
967
766
|
}
|
|
968
767
|
if (path === "/api/daily" && method === "GET") {
|
|
969
768
|
const days = Number(url.searchParams.get("days") ?? 30);
|
|
@@ -972,22 +771,12 @@ function createHandler(db) {
|
|
|
972
771
|
if (path === "/api/sessions" && method === "GET") {
|
|
973
772
|
const agent = url.searchParams.get("agent");
|
|
974
773
|
const project = url.searchParams.get("project") ?? undefined;
|
|
975
|
-
const search = url.searchParams.get("search") ?? undefined;
|
|
976
|
-
const machine = url.searchParams.get("machine") ?? undefined;
|
|
977
774
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
978
775
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
979
776
|
const since = url.searchParams.get("since") ?? undefined;
|
|
980
777
|
const fieldsParam = url.searchParams.get("fields");
|
|
981
778
|
const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
982
|
-
const sessions = querySessions(db, {
|
|
983
|
-
agent: agent ?? undefined,
|
|
984
|
-
project,
|
|
985
|
-
search,
|
|
986
|
-
machine,
|
|
987
|
-
limit,
|
|
988
|
-
offset,
|
|
989
|
-
since
|
|
990
|
-
});
|
|
779
|
+
const sessions = querySessions(db, { agent: agent ?? undefined, project, limit, offset, since });
|
|
991
780
|
return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
|
|
992
781
|
}
|
|
993
782
|
if (path === "/api/top" && method === "GET") {
|
|
@@ -1015,7 +804,7 @@ function createHandler(db) {
|
|
|
1015
804
|
id: randomUUID(),
|
|
1016
805
|
project_path: body["project_path"] ?? null,
|
|
1017
806
|
agent: body["agent"] ?? null,
|
|
1018
|
-
period:
|
|
807
|
+
period: body["period"] ?? "monthly",
|
|
1019
808
|
limit_usd: Number(body["limit_usd"]),
|
|
1020
809
|
alert_at_percent: Number(body["alert_at_percent"] ?? 80),
|
|
1021
810
|
created_at: now,
|
|
@@ -1076,12 +865,8 @@ function createHandler(db) {
|
|
|
1076
865
|
const results = {};
|
|
1077
866
|
if (sources === "all" || sources === "claude")
|
|
1078
867
|
results["claude"] = await ingestClaude(db);
|
|
1079
|
-
if (sources === "all" || sources === "takumi")
|
|
1080
|
-
results["takumi"] = await ingestTakumi(db);
|
|
1081
868
|
if (sources === "all" || sources === "codex")
|
|
1082
869
|
results["codex"] = await ingestCodex(db);
|
|
1083
|
-
if (sources === "all" || sources === "gemini")
|
|
1084
|
-
results["gemini"] = await ingestGemini(db);
|
|
1085
870
|
return ok(results);
|
|
1086
871
|
}
|
|
1087
872
|
const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
|
|
@@ -1131,15 +916,15 @@ function startServer(port = 3456) {
|
|
|
1131
916
|
return apiHandler(req);
|
|
1132
917
|
}
|
|
1133
918
|
try {
|
|
1134
|
-
const { existsSync:
|
|
1135
|
-
if (
|
|
919
|
+
const { existsSync: existsSync4 } = await import("fs");
|
|
920
|
+
if (existsSync4(dashboardDir)) {
|
|
1136
921
|
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
1137
922
|
const fullPath = dashboardDir + filePath;
|
|
1138
|
-
if (
|
|
923
|
+
if (existsSync4(fullPath)) {
|
|
1139
924
|
return new Response(Bun.file(fullPath));
|
|
1140
925
|
}
|
|
1141
926
|
const indexPath = dashboardDir + "/index.html";
|
|
1142
|
-
if (
|
|
927
|
+
if (existsSync4(indexPath)) {
|
|
1143
928
|
return new Response(Bun.file(indexPath));
|
|
1144
929
|
}
|
|
1145
930
|
}
|
|
@@ -1150,62 +935,6 @@ function startServer(port = 3456) {
|
|
|
1150
935
|
console.log(`economy-serve listening on http://localhost:${port}`);
|
|
1151
936
|
}
|
|
1152
937
|
|
|
1153
|
-
// src/lib/package-metadata.ts
|
|
1154
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
1155
|
-
var cachedMetadata = null;
|
|
1156
|
-
function getPackageMetadata() {
|
|
1157
|
-
if (cachedMetadata)
|
|
1158
|
-
return cachedMetadata;
|
|
1159
|
-
const raw = readFileSync4(new URL("../../package.json", import.meta.url), "utf8");
|
|
1160
|
-
const parsed = JSON.parse(raw);
|
|
1161
|
-
cachedMetadata = {
|
|
1162
|
-
name: parsed.name ?? "@hasna/economy",
|
|
1163
|
-
version: parsed.version ?? "0.0.0"
|
|
1164
|
-
};
|
|
1165
|
-
return cachedMetadata;
|
|
1166
|
-
}
|
|
1167
|
-
var packageMetadata = getPackageMetadata();
|
|
1168
|
-
|
|
1169
938
|
// src/server/index.ts
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
REST API server for ${packageMetadata.name}
|
|
1174
|
-
|
|
1175
|
-
Options:
|
|
1176
|
-
-p, --port <port> Port to bind (default: ECONOMY_PORT or 3456)
|
|
1177
|
-
-V, --version output the version number
|
|
1178
|
-
-h, --help display help for command`);
|
|
1179
|
-
}
|
|
1180
|
-
function resolvePort(argv) {
|
|
1181
|
-
for (let i = 0;i < argv.length; i++) {
|
|
1182
|
-
const arg = argv[i];
|
|
1183
|
-
if ((arg === "--port" || arg === "-p") && argv[i + 1]) {
|
|
1184
|
-
const value2 = Number(argv[i + 1]);
|
|
1185
|
-
if (!Number.isFinite(value2) || value2 <= 0) {
|
|
1186
|
-
throw new Error(`Invalid port: ${argv[i + 1]}`);
|
|
1187
|
-
}
|
|
1188
|
-
return value2;
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
const value = Number(process.env["ECONOMY_PORT"] ?? 3456);
|
|
1192
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
1193
|
-
throw new Error(`Invalid ECONOMY_PORT: ${process.env["ECONOMY_PORT"]}`);
|
|
1194
|
-
}
|
|
1195
|
-
return value;
|
|
1196
|
-
}
|
|
1197
|
-
var args = process.argv.slice(2);
|
|
1198
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
1199
|
-
printHelp();
|
|
1200
|
-
process.exit(0);
|
|
1201
|
-
}
|
|
1202
|
-
if (args.includes("--version") || args.includes("-V")) {
|
|
1203
|
-
console.log(packageMetadata.version);
|
|
1204
|
-
process.exit(0);
|
|
1205
|
-
}
|
|
1206
|
-
try {
|
|
1207
|
-
startServer(resolvePort(args));
|
|
1208
|
-
} catch (error) {
|
|
1209
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
1210
|
-
process.exit(1);
|
|
1211
|
-
}
|
|
939
|
+
var port = Number(process.env["ECONOMY_PORT"] ?? 3456);
|
|
940
|
+
startServer(port);
|