@hasna/economy 0.2.17 → 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 +134 -783
- 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 +54 -206
- 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 +384 -600
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.js +63 -357
- 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/cli/index.js
CHANGED
|
@@ -79,7 +79,6 @@ var DEFAULT_PRICING;
|
|
|
79
79
|
var init_pricing = __esm(() => {
|
|
80
80
|
init_database();
|
|
81
81
|
DEFAULT_PRICING = {
|
|
82
|
-
"claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
83
82
|
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
84
83
|
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
85
84
|
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
@@ -90,95 +89,28 @@ var init_pricing = __esm(() => {
|
|
|
90
89
|
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
|
|
91
90
|
"claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
92
91
|
"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
92
|
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
93
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
97
94
|
"gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
98
95
|
"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
96
|
"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
97
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
105
98
|
"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
99
|
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
109
100
|
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
110
101
|
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
111
102
|
"o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
|
|
112
103
|
o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
|
|
113
104
|
"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 }
|
|
105
|
+
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
|
|
125
106
|
};
|
|
126
107
|
});
|
|
127
108
|
|
|
128
109
|
// src/db/database.ts
|
|
129
|
-
var exports_database = {};
|
|
130
|
-
__export(exports_database, {
|
|
131
|
-
upsertSession: () => upsertSession,
|
|
132
|
-
upsertRequest: () => upsertRequest,
|
|
133
|
-
upsertProject: () => upsertProject,
|
|
134
|
-
upsertModelPricing: () => upsertModelPricing,
|
|
135
|
-
upsertGoal: () => upsertGoal,
|
|
136
|
-
upsertBudget: () => upsertBudget,
|
|
137
|
-
upsertBillingDaily: () => upsertBillingDaily,
|
|
138
|
-
setIngestState: () => setIngestState,
|
|
139
|
-
seedModelPricing: () => seedModelPricing,
|
|
140
|
-
rollupSession: () => rollupSession,
|
|
141
|
-
queryTopSessions: () => queryTopSessions,
|
|
142
|
-
querySummary: () => querySummary,
|
|
143
|
-
querySessions: () => querySessions,
|
|
144
|
-
queryRequestsSince: () => queryRequestsSince,
|
|
145
|
-
queryProjectBreakdown: () => queryProjectBreakdown,
|
|
146
|
-
queryModelBreakdown: () => queryModelBreakdown,
|
|
147
|
-
queryDailyBreakdown: () => queryDailyBreakdown,
|
|
148
|
-
queryBillingSummary: () => queryBillingSummary,
|
|
149
|
-
openDatabase: () => openDatabase,
|
|
150
|
-
listProjects: () => listProjects,
|
|
151
|
-
listModelPricing: () => listModelPricing,
|
|
152
|
-
listMachines: () => listMachines,
|
|
153
|
-
listGoals: () => listGoals,
|
|
154
|
-
listBudgets: () => listBudgets,
|
|
155
|
-
getProject: () => getProject,
|
|
156
|
-
getModelPricing: () => getModelPricing,
|
|
157
|
-
getMachineId: () => getMachineId,
|
|
158
|
-
getIngestState: () => getIngestState,
|
|
159
|
-
getGoalStatuses: () => getGoalStatuses,
|
|
160
|
-
getDbPath: () => getDbPath,
|
|
161
|
-
getDataDir: () => getDataDir,
|
|
162
|
-
getBudgetStatuses: () => getBudgetStatuses,
|
|
163
|
-
deleteProject: () => deleteProject,
|
|
164
|
-
deleteModelPricing: () => deleteModelPricing,
|
|
165
|
-
deleteGoal: () => deleteGoal,
|
|
166
|
-
deleteBudget: () => deleteBudget,
|
|
167
|
-
clearBillingRange: () => clearBillingRange
|
|
168
|
-
});
|
|
169
110
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
170
111
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
171
|
-
import { hostname } from "os";
|
|
172
112
|
import { homedir } from "os";
|
|
173
113
|
import { join } from "path";
|
|
174
|
-
function getMachineId() {
|
|
175
|
-
if (process.env["ECONOMY_MACHINE_ID"])
|
|
176
|
-
return process.env["ECONOMY_MACHINE_ID"];
|
|
177
|
-
const h = hostname().toLowerCase();
|
|
178
|
-
if (h.startsWith("spark") || h.startsWith("apple"))
|
|
179
|
-
return h.split(".")[0];
|
|
180
|
-
return h.split(".")[0];
|
|
181
|
-
}
|
|
182
114
|
function getDataDir() {
|
|
183
115
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
184
116
|
const newDir = join(home, ".hasna", "economy");
|
|
@@ -211,7 +143,6 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
211
143
|
}
|
|
212
144
|
const db = new Database(path);
|
|
213
145
|
db.exec("PRAGMA journal_mode = WAL");
|
|
214
|
-
db.exec("PRAGMA busy_timeout = 5000");
|
|
215
146
|
db.exec("PRAGMA foreign_keys = ON");
|
|
216
147
|
initSchema(db);
|
|
217
148
|
if (!skipSeed) {
|
|
@@ -233,8 +164,7 @@ function initSchema(db) {
|
|
|
233
164
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
234
165
|
duration_ms INTEGER DEFAULT 0,
|
|
235
166
|
timestamp TEXT NOT NULL,
|
|
236
|
-
source_request_id TEXT
|
|
237
|
-
machine_id TEXT DEFAULT ''
|
|
167
|
+
source_request_id TEXT
|
|
238
168
|
);
|
|
239
169
|
|
|
240
170
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -246,8 +176,7 @@ function initSchema(db) {
|
|
|
246
176
|
ended_at TEXT,
|
|
247
177
|
total_cost_usd REAL DEFAULT 0,
|
|
248
178
|
total_tokens INTEGER DEFAULT 0,
|
|
249
|
-
request_count INTEGER DEFAULT 0
|
|
250
|
-
machine_id TEXT DEFAULT ''
|
|
179
|
+
request_count INTEGER DEFAULT 0
|
|
251
180
|
);
|
|
252
181
|
|
|
253
182
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -312,27 +241,6 @@ function initSchema(db) {
|
|
|
312
241
|
machine_id TEXT,
|
|
313
242
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
314
243
|
);
|
|
315
|
-
|
|
316
|
-
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
317
|
-
date TEXT NOT NULL,
|
|
318
|
-
provider TEXT NOT NULL,
|
|
319
|
-
description TEXT DEFAULT '',
|
|
320
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
321
|
-
updated_at TEXT NOT NULL,
|
|
322
|
-
PRIMARY KEY (date, provider, description)
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
326
|
-
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
327
|
-
`);
|
|
328
|
-
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
329
|
-
if (!cols.some((c) => c.name === "machine_id")) {
|
|
330
|
-
db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
331
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
332
|
-
}
|
|
333
|
-
db.exec(`
|
|
334
|
-
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
335
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
336
244
|
`);
|
|
337
245
|
}
|
|
338
246
|
function periodWhere(period) {
|
|
@@ -342,11 +250,11 @@ function periodWhere(period) {
|
|
|
342
250
|
case "yesterday":
|
|
343
251
|
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
344
252
|
case "week":
|
|
345
|
-
return `timestamp >= DATE('now', '
|
|
253
|
+
return `timestamp >= DATE('now', '-7 days')`;
|
|
346
254
|
case "month":
|
|
347
|
-
return `timestamp >= DATE('now', '
|
|
255
|
+
return `timestamp >= DATE('now', '-30 days')`;
|
|
348
256
|
case "year":
|
|
349
|
-
return `timestamp >= DATE('now', '
|
|
257
|
+
return `timestamp >= DATE('now', '-365 days')`;
|
|
350
258
|
case "all":
|
|
351
259
|
return "1=1";
|
|
352
260
|
}
|
|
@@ -358,11 +266,11 @@ function sessionPeriodWhere(period) {
|
|
|
358
266
|
case "yesterday":
|
|
359
267
|
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
360
268
|
case "week":
|
|
361
|
-
return `started_at >= DATE('now', '
|
|
269
|
+
return `started_at >= DATE('now', '-7 days')`;
|
|
362
270
|
case "month":
|
|
363
|
-
return `started_at >= DATE('now', '
|
|
271
|
+
return `started_at >= DATE('now', '-30 days')`;
|
|
364
272
|
case "year":
|
|
365
|
-
return `started_at >= DATE('now', '
|
|
273
|
+
return `started_at >= DATE('now', '-365 days')`;
|
|
366
274
|
case "all":
|
|
367
275
|
return "1=1";
|
|
368
276
|
}
|
|
@@ -372,17 +280,17 @@ function upsertRequest(db, req) {
|
|
|
372
280
|
INSERT OR REPLACE INTO requests
|
|
373
281
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
374
282
|
cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
|
|
375
|
-
timestamp, source_request_id
|
|
376
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
377
|
-
`).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
|
|
283
|
+
timestamp, source_request_id)
|
|
284
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
285
|
+
`).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);
|
|
378
286
|
}
|
|
379
287
|
function upsertSession(db, session) {
|
|
380
288
|
db.prepare(`
|
|
381
289
|
INSERT OR REPLACE INTO sessions
|
|
382
290
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
383
|
-
total_cost_usd, total_tokens, request_count
|
|
384
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
385
|
-
`).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
|
|
291
|
+
total_cost_usd, total_tokens, request_count)
|
|
292
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
293
|
+
`).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);
|
|
386
294
|
}
|
|
387
295
|
function rollupSession(db, sessionId) {
|
|
388
296
|
db.prepare(`
|
|
@@ -412,10 +320,6 @@ function querySessions(db, filter = {}) {
|
|
|
412
320
|
conditions.push("started_at >= ?");
|
|
413
321
|
params.push(filter.since);
|
|
414
322
|
}
|
|
415
|
-
if (filter.machine) {
|
|
416
|
-
conditions.push("machine_id = ?");
|
|
417
|
-
params.push(filter.machine);
|
|
418
|
-
}
|
|
419
323
|
if (filter.search) {
|
|
420
324
|
const q = `%${filter.search}%`;
|
|
421
325
|
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
@@ -434,25 +338,24 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
434
338
|
}
|
|
435
339
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
436
340
|
}
|
|
437
|
-
function querySummary(db, period
|
|
341
|
+
function querySummary(db, period) {
|
|
438
342
|
const rWhere = periodWhere(period);
|
|
439
343
|
const sWhere = sessionPeriodWhere(period);
|
|
440
|
-
const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
441
344
|
const r = db.prepare(`
|
|
442
345
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
443
346
|
COUNT(*) as requests,
|
|
444
347
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
445
|
-
FROM requests WHERE ${rWhere}
|
|
348
|
+
FROM requests WHERE ${rWhere}
|
|
446
349
|
`).get();
|
|
447
350
|
const codexTotals = db.prepare(`
|
|
448
351
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
449
352
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
450
353
|
COUNT(*) as sessions
|
|
451
354
|
FROM sessions
|
|
452
|
-
WHERE ${sWhere}
|
|
355
|
+
WHERE ${sWhere}
|
|
453
356
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
454
357
|
`).get();
|
|
455
|
-
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}
|
|
358
|
+
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
|
|
456
359
|
return {
|
|
457
360
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
458
361
|
requests: r.requests,
|
|
@@ -472,66 +375,23 @@ function queryModelBreakdown(db) {
|
|
|
472
375
|
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
473
376
|
`).all();
|
|
474
377
|
}
|
|
475
|
-
function labelForPath(projectPath, projectName) {
|
|
476
|
-
if (projectName && projectName.trim() !== "")
|
|
477
|
-
return projectName;
|
|
478
|
-
if (!projectPath)
|
|
479
|
-
return "";
|
|
480
|
-
const segments = projectPath.split("/").filter(Boolean);
|
|
481
|
-
const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
|
|
482
|
-
for (const seg of segments) {
|
|
483
|
-
if (projectPrefix.test(seg))
|
|
484
|
-
return seg;
|
|
485
|
-
}
|
|
486
|
-
const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
|
|
487
|
-
for (let i = segments.length - 1;i >= 0; i--) {
|
|
488
|
-
if (!generic.has(segments[i].toLowerCase()))
|
|
489
|
-
return segments[i];
|
|
490
|
-
}
|
|
491
|
-
return segments[segments.length - 1] ?? projectPath;
|
|
492
|
-
}
|
|
493
378
|
function queryProjectBreakdown(db) {
|
|
494
|
-
|
|
495
|
-
SELECT
|
|
496
|
-
|
|
497
|
-
|
|
379
|
+
return db.prepare(`
|
|
380
|
+
SELECT
|
|
381
|
+
s.project_path,
|
|
382
|
+
COALESCE(p.name, s.project_name) as project_name,
|
|
383
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
384
|
+
COUNT(r.id) as requests,
|
|
385
|
+
COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
|
|
386
|
+
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
387
|
+
MAX(s.started_at) as last_active
|
|
388
|
+
FROM sessions s
|
|
389
|
+
LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
|
|
390
|
+
LEFT JOIN requests r ON r.session_id = s.id
|
|
391
|
+
WHERE s.project_path != '' OR s.project_name != ''
|
|
392
|
+
GROUP BY s.project_path
|
|
393
|
+
ORDER BY cost_usd DESC
|
|
498
394
|
`).all();
|
|
499
|
-
const groups = new Map;
|
|
500
|
-
for (const s of sessions) {
|
|
501
|
-
const label = labelForPath(s.project_path, s.project_name);
|
|
502
|
-
if (!label)
|
|
503
|
-
continue;
|
|
504
|
-
const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
|
|
505
|
-
g.sessionIds.push(s.id);
|
|
506
|
-
g.totalCost += s.total_cost_usd || 0;
|
|
507
|
-
if (!g.lastActive || s.started_at > g.lastActive)
|
|
508
|
-
g.lastActive = s.started_at;
|
|
509
|
-
if (!g.samplePath)
|
|
510
|
-
g.samplePath = s.project_path;
|
|
511
|
-
groups.set(label, g);
|
|
512
|
-
}
|
|
513
|
-
const result = [];
|
|
514
|
-
for (const [label, g] of groups.entries()) {
|
|
515
|
-
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
516
|
-
const reqStats = placeholders.length ? db.prepare(`
|
|
517
|
-
SELECT
|
|
518
|
-
COUNT(*) as requests,
|
|
519
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
520
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
|
|
521
|
-
FROM requests WHERE session_id IN (${placeholders})
|
|
522
|
-
`).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
|
|
523
|
-
result.push({
|
|
524
|
-
project_path: g.samplePath,
|
|
525
|
-
project_name: label,
|
|
526
|
-
sessions: g.sessionIds.length,
|
|
527
|
-
requests: reqStats.requests,
|
|
528
|
-
total_tokens: reqStats.total_tokens,
|
|
529
|
-
cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
|
|
530
|
-
last_active: g.lastActive
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
534
|
-
return result;
|
|
535
395
|
}
|
|
536
396
|
function queryDailyBreakdown(db, days = 30) {
|
|
537
397
|
return db.prepare(`
|
|
@@ -649,40 +509,6 @@ function setIngestState(db, source, key, value) {
|
|
|
649
509
|
function queryRequestsSince(db, since) {
|
|
650
510
|
return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
|
|
651
511
|
}
|
|
652
|
-
function upsertBillingDaily(db, row) {
|
|
653
|
-
db.prepare(`
|
|
654
|
-
INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
|
|
655
|
-
VALUES (?, ?, ?, ?, ?)
|
|
656
|
-
`).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
|
|
657
|
-
}
|
|
658
|
-
function clearBillingRange(db, provider, fromDate, toDate) {
|
|
659
|
-
db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
|
|
660
|
-
}
|
|
661
|
-
function queryBillingSummary(db, period) {
|
|
662
|
-
const where = period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
|
|
663
|
-
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
|
|
664
|
-
const by_provider = {};
|
|
665
|
-
let total = 0;
|
|
666
|
-
for (const r of rows) {
|
|
667
|
-
by_provider[r.provider] = r.cost;
|
|
668
|
-
total += r.cost;
|
|
669
|
-
}
|
|
670
|
-
return { total_usd: total, by_provider };
|
|
671
|
-
}
|
|
672
|
-
function listMachines(db) {
|
|
673
|
-
return db.prepare(`
|
|
674
|
-
SELECT
|
|
675
|
-
s.machine_id,
|
|
676
|
-
COUNT(DISTINCT s.id) as sessions,
|
|
677
|
-
COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
|
|
678
|
-
COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
|
|
679
|
-
MAX(s.started_at) as last_active
|
|
680
|
-
FROM sessions s
|
|
681
|
-
WHERE s.machine_id != ''
|
|
682
|
-
GROUP BY s.machine_id
|
|
683
|
-
ORDER BY total_cost_usd DESC
|
|
684
|
-
`).all();
|
|
685
|
-
}
|
|
686
512
|
function upsertModelPricing(db, p) {
|
|
687
513
|
db.prepare(`
|
|
688
514
|
INSERT OR REPLACE INTO model_pricing
|
|
@@ -700,11 +526,11 @@ function deleteModelPricing(db, model) {
|
|
|
700
526
|
db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
|
|
701
527
|
}
|
|
702
528
|
function seedModelPricing(db, defaults) {
|
|
703
|
-
const existing =
|
|
529
|
+
const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
|
|
530
|
+
if (existing.count > 0)
|
|
531
|
+
return;
|
|
704
532
|
const now = new Date().toISOString();
|
|
705
533
|
for (const [model, p] of Object.entries(defaults)) {
|
|
706
|
-
if (existing.has(model))
|
|
707
|
-
continue;
|
|
708
534
|
upsertModelPricing(db, {
|
|
709
535
|
model,
|
|
710
536
|
input_per_1m: p.inputPer1M,
|
|
@@ -743,36 +569,29 @@ function collectJsonlFiles(projectDir) {
|
|
|
743
569
|
return files;
|
|
744
570
|
}
|
|
745
571
|
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
746
|
-
|
|
747
|
-
}
|
|
748
|
-
async function ingestTakumi(db, verbose = false) {
|
|
749
|
-
return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
|
|
750
|
-
}
|
|
751
|
-
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
752
|
-
if (!existsSync3(projectsDir)) {
|
|
572
|
+
if (!existsSync3(PROJECTS_DIR)) {
|
|
753
573
|
if (verbose)
|
|
754
|
-
console.log(
|
|
574
|
+
console.log("Claude projects dir not found:", PROJECTS_DIR);
|
|
755
575
|
return { files: 0, requests: 0, sessions: 0 };
|
|
756
576
|
}
|
|
757
|
-
const machineId = getMachineId();
|
|
758
577
|
let totalFiles = 0;
|
|
759
578
|
let totalRequests = 0;
|
|
760
579
|
const touchedSessions = new Set;
|
|
761
580
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
762
|
-
const projectDirs = readdirSync2(
|
|
581
|
+
const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
763
582
|
for (const projectDirEntry of projectDirs) {
|
|
764
|
-
const projectDirPath = join4(
|
|
583
|
+
const projectDirPath = join4(PROJECTS_DIR, projectDirEntry.name);
|
|
765
584
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
766
585
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
767
586
|
for (const filePath of jsonlFiles) {
|
|
768
|
-
const stateKey = filePath.replace(
|
|
587
|
+
const stateKey = filePath.replace(PROJECTS_DIR, "");
|
|
769
588
|
let fileMtime = "0";
|
|
770
589
|
try {
|
|
771
590
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
772
591
|
} catch {
|
|
773
592
|
continue;
|
|
774
593
|
}
|
|
775
|
-
const processed = getIngestState(db,
|
|
594
|
+
const processed = getIngestState(db, "claude", stateKey);
|
|
776
595
|
if (processed === fileMtime)
|
|
777
596
|
continue;
|
|
778
597
|
let lines;
|
|
@@ -813,10 +632,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
813
632
|
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
814
633
|
continue;
|
|
815
634
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
816
|
-
const reqId =
|
|
635
|
+
const reqId = `claude-${sessionId}-${timestamp}`;
|
|
817
636
|
upsertRequest(db, {
|
|
818
637
|
id: reqId,
|
|
819
|
-
agent:
|
|
638
|
+
agent: "claude",
|
|
820
639
|
session_id: sessionId,
|
|
821
640
|
model,
|
|
822
641
|
input_tokens: inputTokens,
|
|
@@ -826,8 +645,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
826
645
|
cost_usd: costUsd,
|
|
827
646
|
duration_ms: 0,
|
|
828
647
|
timestamp,
|
|
829
|
-
source_request_id: reqId
|
|
830
|
-
machine_id: machineId
|
|
648
|
+
source_request_id: reqId
|
|
831
649
|
});
|
|
832
650
|
if (!touchedSessions.has(sessionId)) {
|
|
833
651
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
@@ -836,15 +654,14 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
836
654
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
837
655
|
const session = {
|
|
838
656
|
id: sessionId,
|
|
839
|
-
agent:
|
|
657
|
+
agent: "claude",
|
|
840
658
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
841
659
|
project_name: detectedProject ? detectedProject.name : "",
|
|
842
660
|
started_at: timestamp,
|
|
843
661
|
ended_at: null,
|
|
844
662
|
total_cost_usd: 0,
|
|
845
663
|
total_tokens: 0,
|
|
846
|
-
request_count: 0
|
|
847
|
-
machine_id: machineId
|
|
664
|
+
request_count: 0
|
|
848
665
|
};
|
|
849
666
|
upsertSession(db, session);
|
|
850
667
|
}
|
|
@@ -852,7 +669,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
852
669
|
}
|
|
853
670
|
totalRequests++;
|
|
854
671
|
}
|
|
855
|
-
setIngestState(db,
|
|
672
|
+
setIngestState(db, "claude", stateKey, fileMtime);
|
|
856
673
|
totalFiles++;
|
|
857
674
|
}
|
|
858
675
|
}
|
|
@@ -861,30 +678,28 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
861
678
|
}
|
|
862
679
|
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
863
680
|
}
|
|
864
|
-
var
|
|
681
|
+
var PROJECTS_DIR;
|
|
865
682
|
var init_claude = __esm(() => {
|
|
866
683
|
init_database();
|
|
867
684
|
init_pricing();
|
|
868
|
-
|
|
869
|
-
TAKUMI_PROJECTS_DIR = join4(homedir2(), ".takumi", "projects");
|
|
685
|
+
PROJECTS_DIR = join4(homedir2(), ".claude", "projects");
|
|
870
686
|
});
|
|
871
687
|
|
|
872
688
|
// src/ingest/codex.ts
|
|
873
689
|
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
874
690
|
import { homedir as homedir3 } from "os";
|
|
875
691
|
import { join as join5, basename as basename2 } from "path";
|
|
876
|
-
import { Database as
|
|
692
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
877
693
|
async function ingestCodex(db, verbose = false) {
|
|
878
694
|
if (!existsSync4(CODEX_DB_PATH)) {
|
|
879
695
|
if (verbose)
|
|
880
696
|
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
881
697
|
return { sessions: 0 };
|
|
882
698
|
}
|
|
883
|
-
const machineId = getMachineId();
|
|
884
699
|
let codexDb = null;
|
|
885
700
|
let ingested = 0;
|
|
886
701
|
try {
|
|
887
|
-
codexDb = new
|
|
702
|
+
codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
|
|
888
703
|
const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
|
|
889
704
|
for (const thread of threads) {
|
|
890
705
|
const stateKey = thread.id;
|
|
@@ -905,8 +720,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
905
720
|
ended_at: endedAt,
|
|
906
721
|
total_cost_usd: costUsd,
|
|
907
722
|
total_tokens: thread.tokens_used,
|
|
908
|
-
request_count: 1
|
|
909
|
-
machine_id: machineId
|
|
723
|
+
request_count: 1
|
|
910
724
|
});
|
|
911
725
|
setIngestState(db, "codex", stateKey, "done");
|
|
912
726
|
ingested++;
|
|
@@ -925,88 +739,6 @@ var init_codex = __esm(() => {
|
|
|
925
739
|
CODEX_CONFIG_PATH = join5(homedir3(), ".codex", "config.toml");
|
|
926
740
|
});
|
|
927
741
|
|
|
928
|
-
// src/ingest/gemini.ts
|
|
929
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
|
|
930
|
-
import { homedir as homedir4 } from "os";
|
|
931
|
-
import { join as join6 } from "path";
|
|
932
|
-
async function ingestGemini(db, verbose) {
|
|
933
|
-
if (!existsSync5(GEMINI_TMP_DIR)) {
|
|
934
|
-
if (verbose)
|
|
935
|
-
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
936
|
-
return { sessions: 0 };
|
|
937
|
-
}
|
|
938
|
-
const machineId = getMachineId();
|
|
939
|
-
let totalSessions = 0;
|
|
940
|
-
const touchedSessions = new Set;
|
|
941
|
-
let projectHashDirs = [];
|
|
942
|
-
try {
|
|
943
|
-
projectHashDirs = readdirSync3(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join6(GEMINI_TMP_DIR, d.name));
|
|
944
|
-
} catch {
|
|
945
|
-
return { sessions: 0 };
|
|
946
|
-
}
|
|
947
|
-
for (const projectDir of projectHashDirs) {
|
|
948
|
-
const chatsDir = join6(projectDir, "chats");
|
|
949
|
-
if (!existsSync5(chatsDir))
|
|
950
|
-
continue;
|
|
951
|
-
let chatFiles = [];
|
|
952
|
-
try {
|
|
953
|
-
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join6(chatsDir, f));
|
|
954
|
-
} catch {
|
|
955
|
-
continue;
|
|
956
|
-
}
|
|
957
|
-
for (const filePath of chatFiles) {
|
|
958
|
-
const stateKey = filePath.replace(homedir4(), "~");
|
|
959
|
-
let fileMtime = "0";
|
|
960
|
-
try {
|
|
961
|
-
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
962
|
-
} catch {
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
const processed = getIngestState(db, "gemini", stateKey);
|
|
966
|
-
if (processed === fileMtime)
|
|
967
|
-
continue;
|
|
968
|
-
let chatData;
|
|
969
|
-
try {
|
|
970
|
-
chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
971
|
-
} catch {
|
|
972
|
-
continue;
|
|
973
|
-
}
|
|
974
|
-
const sessionId = chatData.sessionId;
|
|
975
|
-
if (!sessionId)
|
|
976
|
-
continue;
|
|
977
|
-
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
978
|
-
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
979
|
-
if (!existing) {
|
|
980
|
-
const session = {
|
|
981
|
-
id: sessionId,
|
|
982
|
-
agent: "gemini",
|
|
983
|
-
project_path: "",
|
|
984
|
-
project_name: "",
|
|
985
|
-
started_at: startTime,
|
|
986
|
-
ended_at: chatData.lastUpdated ?? null,
|
|
987
|
-
total_cost_usd: 0,
|
|
988
|
-
total_tokens: 0,
|
|
989
|
-
request_count: 0,
|
|
990
|
-
machine_id: machineId
|
|
991
|
-
};
|
|
992
|
-
upsertSession(db, session);
|
|
993
|
-
touchedSessions.add(sessionId);
|
|
994
|
-
totalSessions++;
|
|
995
|
-
}
|
|
996
|
-
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
for (const sessionId of touchedSessions) {
|
|
1000
|
-
rollupSession(db, sessionId);
|
|
1001
|
-
}
|
|
1002
|
-
return { sessions: totalSessions };
|
|
1003
|
-
}
|
|
1004
|
-
var GEMINI_TMP_DIR;
|
|
1005
|
-
var init_gemini = __esm(() => {
|
|
1006
|
-
init_database();
|
|
1007
|
-
GEMINI_TMP_DIR = join6(homedir4(), ".gemini", "tmp");
|
|
1008
|
-
});
|
|
1009
|
-
|
|
1010
742
|
// src/lib/config.ts
|
|
1011
743
|
var exports_config = {};
|
|
1012
744
|
__export(exports_config, {
|
|
@@ -1015,12 +747,12 @@ __export(exports_config, {
|
|
|
1015
747
|
loadConfig: () => loadConfig2,
|
|
1016
748
|
getConfigValue: () => getConfigValue
|
|
1017
749
|
});
|
|
1018
|
-
import { existsSync as existsSync6, readFileSync as
|
|
750
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
1019
751
|
import { join as join7 } from "path";
|
|
1020
752
|
function loadConfig2() {
|
|
1021
753
|
try {
|
|
1022
754
|
if (existsSync6(CONFIG_PATH2)) {
|
|
1023
|
-
const raw =
|
|
755
|
+
const raw = readFileSync5(CONFIG_PATH2, "utf-8");
|
|
1024
756
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
1025
757
|
}
|
|
1026
758
|
} catch {}
|
|
@@ -1230,20 +962,6 @@ function ok(data, meta) {
|
|
|
1230
962
|
function err(message, status = 400) {
|
|
1231
963
|
return json({ error: message }, status);
|
|
1232
964
|
}
|
|
1233
|
-
function normalizeBudgetPeriod(value) {
|
|
1234
|
-
switch (value) {
|
|
1235
|
-
case "day":
|
|
1236
|
-
case "daily":
|
|
1237
|
-
return "daily";
|
|
1238
|
-
case "week":
|
|
1239
|
-
case "weekly":
|
|
1240
|
-
return "weekly";
|
|
1241
|
-
case "month":
|
|
1242
|
-
case "monthly":
|
|
1243
|
-
default:
|
|
1244
|
-
return "monthly";
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
965
|
function applyFields(obj, fields) {
|
|
1248
966
|
if (!fields || fields.length === 0)
|
|
1249
967
|
return obj;
|
|
@@ -1260,11 +978,7 @@ function createHandler(db) {
|
|
|
1260
978
|
return ok({ status: "ok", ts: new Date().toISOString() });
|
|
1261
979
|
if (path === "/api/summary" && method === "GET") {
|
|
1262
980
|
const period = url.searchParams.get("period") ?? "today";
|
|
1263
|
-
|
|
1264
|
-
return ok(querySummary(db, period, machine));
|
|
1265
|
-
}
|
|
1266
|
-
if (path === "/api/machines" && method === "GET") {
|
|
1267
|
-
return ok(listMachines(db), { current_machine: getMachineId() });
|
|
981
|
+
return ok(querySummary(db, period));
|
|
1268
982
|
}
|
|
1269
983
|
if (path === "/api/daily" && method === "GET") {
|
|
1270
984
|
const days = Number(url.searchParams.get("days") ?? 30);
|
|
@@ -1273,22 +987,12 @@ function createHandler(db) {
|
|
|
1273
987
|
if (path === "/api/sessions" && method === "GET") {
|
|
1274
988
|
const agent = url.searchParams.get("agent");
|
|
1275
989
|
const project = url.searchParams.get("project") ?? undefined;
|
|
1276
|
-
const search = url.searchParams.get("search") ?? undefined;
|
|
1277
|
-
const machine = url.searchParams.get("machine") ?? undefined;
|
|
1278
990
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
1279
991
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
1280
992
|
const since = url.searchParams.get("since") ?? undefined;
|
|
1281
993
|
const fieldsParam = url.searchParams.get("fields");
|
|
1282
994
|
const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
1283
|
-
const sessions = querySessions(db, {
|
|
1284
|
-
agent: agent ?? undefined,
|
|
1285
|
-
project,
|
|
1286
|
-
search,
|
|
1287
|
-
machine,
|
|
1288
|
-
limit,
|
|
1289
|
-
offset,
|
|
1290
|
-
since
|
|
1291
|
-
});
|
|
995
|
+
const sessions = querySessions(db, { agent: agent ?? undefined, project, limit, offset, since });
|
|
1292
996
|
return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
|
|
1293
997
|
}
|
|
1294
998
|
if (path === "/api/top" && method === "GET") {
|
|
@@ -1316,7 +1020,7 @@ function createHandler(db) {
|
|
|
1316
1020
|
id: randomUUID(),
|
|
1317
1021
|
project_path: body["project_path"] ?? null,
|
|
1318
1022
|
agent: body["agent"] ?? null,
|
|
1319
|
-
period:
|
|
1023
|
+
period: body["period"] ?? "monthly",
|
|
1320
1024
|
limit_usd: Number(body["limit_usd"]),
|
|
1321
1025
|
alert_at_percent: Number(body["alert_at_percent"] ?? 80),
|
|
1322
1026
|
created_at: now,
|
|
@@ -1377,12 +1081,8 @@ function createHandler(db) {
|
|
|
1377
1081
|
const results = {};
|
|
1378
1082
|
if (sources === "all" || sources === "claude")
|
|
1379
1083
|
results["claude"] = await ingestClaude(db);
|
|
1380
|
-
if (sources === "all" || sources === "takumi")
|
|
1381
|
-
results["takumi"] = await ingestTakumi(db);
|
|
1382
1084
|
if (sources === "all" || sources === "codex")
|
|
1383
1085
|
results["codex"] = await ingestCodex(db);
|
|
1384
|
-
if (sources === "all" || sources === "gemini")
|
|
1385
|
-
results["gemini"] = await ingestGemini(db);
|
|
1386
1086
|
return ok(results);
|
|
1387
1087
|
}
|
|
1388
1088
|
const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
|
|
@@ -1455,7 +1155,6 @@ var init_serve = __esm(() => {
|
|
|
1455
1155
|
init_database();
|
|
1456
1156
|
init_claude();
|
|
1457
1157
|
init_codex();
|
|
1458
|
-
init_gemini();
|
|
1459
1158
|
init_pricing();
|
|
1460
1159
|
CORS = {
|
|
1461
1160
|
"Access-Control-Allow-Origin": "*",
|
|
@@ -1593,102 +1292,6 @@ function menubarStop() {
|
|
|
1593
1292
|
var APP_PATH = "/Applications/Economy Bar.app", REPO = "hasna/open-economy";
|
|
1594
1293
|
var init_menubar = () => {};
|
|
1595
1294
|
|
|
1596
|
-
// src/db/pg-migrations.ts
|
|
1597
|
-
var exports_pg_migrations = {};
|
|
1598
|
-
__export(exports_pg_migrations, {
|
|
1599
|
-
PG_MIGRATIONS: () => PG_MIGRATIONS
|
|
1600
|
-
});
|
|
1601
|
-
var PG_MIGRATIONS;
|
|
1602
|
-
var init_pg_migrations = __esm(() => {
|
|
1603
|
-
PG_MIGRATIONS = [
|
|
1604
|
-
`CREATE TABLE IF NOT EXISTS requests (
|
|
1605
|
-
id TEXT PRIMARY KEY,
|
|
1606
|
-
agent TEXT NOT NULL,
|
|
1607
|
-
session_id TEXT NOT NULL,
|
|
1608
|
-
model TEXT NOT NULL,
|
|
1609
|
-
input_tokens INTEGER DEFAULT 0,
|
|
1610
|
-
output_tokens INTEGER DEFAULT 0,
|
|
1611
|
-
cache_read_tokens INTEGER DEFAULT 0,
|
|
1612
|
-
cache_create_tokens INTEGER DEFAULT 0,
|
|
1613
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
1614
|
-
duration_ms INTEGER DEFAULT 0,
|
|
1615
|
-
timestamp TEXT NOT NULL,
|
|
1616
|
-
source_request_id TEXT,
|
|
1617
|
-
machine_id TEXT DEFAULT ''
|
|
1618
|
-
)`,
|
|
1619
|
-
`CREATE TABLE IF NOT EXISTS sessions (
|
|
1620
|
-
id TEXT PRIMARY KEY,
|
|
1621
|
-
agent TEXT NOT NULL,
|
|
1622
|
-
project_path TEXT DEFAULT '',
|
|
1623
|
-
project_name TEXT DEFAULT '',
|
|
1624
|
-
started_at TEXT NOT NULL,
|
|
1625
|
-
ended_at TEXT,
|
|
1626
|
-
total_cost_usd REAL DEFAULT 0,
|
|
1627
|
-
total_tokens INTEGER DEFAULT 0,
|
|
1628
|
-
request_count INTEGER DEFAULT 0,
|
|
1629
|
-
machine_id TEXT DEFAULT ''
|
|
1630
|
-
)`,
|
|
1631
|
-
`CREATE TABLE IF NOT EXISTS projects (
|
|
1632
|
-
id TEXT PRIMARY KEY,
|
|
1633
|
-
path TEXT UNIQUE NOT NULL,
|
|
1634
|
-
name TEXT NOT NULL,
|
|
1635
|
-
description TEXT,
|
|
1636
|
-
tags TEXT DEFAULT '[]',
|
|
1637
|
-
created_at TEXT NOT NULL
|
|
1638
|
-
)`,
|
|
1639
|
-
`CREATE TABLE IF NOT EXISTS budgets (
|
|
1640
|
-
id TEXT PRIMARY KEY,
|
|
1641
|
-
project_path TEXT,
|
|
1642
|
-
agent TEXT,
|
|
1643
|
-
period TEXT NOT NULL,
|
|
1644
|
-
limit_usd REAL NOT NULL,
|
|
1645
|
-
alert_at_percent INTEGER DEFAULT 80,
|
|
1646
|
-
created_at TEXT NOT NULL,
|
|
1647
|
-
updated_at TEXT NOT NULL
|
|
1648
|
-
)`,
|
|
1649
|
-
`CREATE TABLE IF NOT EXISTS goals (
|
|
1650
|
-
id TEXT PRIMARY KEY,
|
|
1651
|
-
period TEXT NOT NULL,
|
|
1652
|
-
project_path TEXT,
|
|
1653
|
-
agent TEXT,
|
|
1654
|
-
limit_usd REAL NOT NULL,
|
|
1655
|
-
created_at TEXT NOT NULL,
|
|
1656
|
-
updated_at TEXT NOT NULL
|
|
1657
|
-
)`,
|
|
1658
|
-
`CREATE TABLE IF NOT EXISTS ingest_state (
|
|
1659
|
-
source TEXT NOT NULL,
|
|
1660
|
-
key TEXT NOT NULL,
|
|
1661
|
-
value TEXT NOT NULL,
|
|
1662
|
-
PRIMARY KEY (source, key)
|
|
1663
|
-
)`,
|
|
1664
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
1665
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
1666
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
1667
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
1668
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
1669
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
1670
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
1671
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
1672
|
-
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
1673
|
-
model TEXT PRIMARY KEY,
|
|
1674
|
-
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
1675
|
-
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
1676
|
-
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
1677
|
-
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
1678
|
-
updated_at TEXT NOT NULL
|
|
1679
|
-
)`,
|
|
1680
|
-
`CREATE TABLE IF NOT EXISTS feedback (
|
|
1681
|
-
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
1682
|
-
message TEXT NOT NULL,
|
|
1683
|
-
email TEXT,
|
|
1684
|
-
category TEXT DEFAULT 'general',
|
|
1685
|
-
version TEXT,
|
|
1686
|
-
machine_id TEXT,
|
|
1687
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
1688
|
-
)`
|
|
1689
|
-
];
|
|
1690
|
-
});
|
|
1691
|
-
|
|
1692
1295
|
// src/cli/index.ts
|
|
1693
1296
|
import { Command } from "commander";
|
|
1694
1297
|
import chalk4 from "chalk";
|
|
@@ -2047,148 +1650,94 @@ ${chalk.dim("Set it active:")} economy brains model set ${String(status["fine_tu
|
|
|
2047
1650
|
init_database();
|
|
2048
1651
|
init_claude();
|
|
2049
1652
|
init_codex();
|
|
2050
|
-
init_gemini();
|
|
2051
1653
|
|
|
2052
|
-
// src/ingest/
|
|
1654
|
+
// src/ingest/gemini.ts
|
|
2053
1655
|
init_database();
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
}
|
|
2063
|
-
async function syncAnthropicBilling(db, opts = {}) {
|
|
2064
|
-
const key = getAnthropicAdminKey();
|
|
2065
|
-
if (!key)
|
|
2066
|
-
throw new Error("Missing Anthropic admin key (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY)");
|
|
2067
|
-
const now = new Date;
|
|
2068
|
-
const end = opts.toDate ? new Date(opts.toDate) : new Date(now.getTime() + 24 * 3600000);
|
|
2069
|
-
const days = opts.days ?? 31;
|
|
2070
|
-
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
2071
|
-
const startIso = start.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
|
|
2072
|
-
const endIso = end.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
|
|
2073
|
-
let totalUsd = 0;
|
|
2074
|
-
const buckets = [];
|
|
2075
|
-
let nextPage;
|
|
2076
|
-
do {
|
|
2077
|
-
const url = new URL("https://api.anthropic.com/v1/organizations/cost_report");
|
|
2078
|
-
url.searchParams.set("starting_at", startIso);
|
|
2079
|
-
url.searchParams.set("ending_at", endIso);
|
|
2080
|
-
url.searchParams.set("bucket_width", "1d");
|
|
2081
|
-
url.searchParams.set("limit", "31");
|
|
2082
|
-
url.searchParams.append("group_by[]", "description");
|
|
2083
|
-
if (nextPage)
|
|
2084
|
-
url.searchParams.set("page", nextPage);
|
|
2085
|
-
const res = await fetch(url.toString(), {
|
|
2086
|
-
headers: { "anthropic-version": "2023-06-01", "x-api-key": key }
|
|
2087
|
-
});
|
|
2088
|
-
const data = await res.json();
|
|
2089
|
-
if (data.error)
|
|
2090
|
-
throw new Error(`Anthropic API: ${data.error.message}`);
|
|
2091
|
-
if (data.data)
|
|
2092
|
-
buckets.push(...data.data);
|
|
2093
|
-
nextPage = data.has_more ? data.next_page : undefined;
|
|
2094
|
-
} while (nextPage);
|
|
2095
|
-
const fromDateStr = toISODate(start);
|
|
2096
|
-
const toDateStr = toISODate(new Date(end.getTime() - 1000));
|
|
2097
|
-
clearBillingRange(db, "anthropic", fromDateStr, toDateStr);
|
|
2098
|
-
const updatedAt = new Date().toISOString();
|
|
2099
|
-
for (const bucket of buckets) {
|
|
2100
|
-
const date = bucket.starting_at.substring(0, 10);
|
|
2101
|
-
for (const r of bucket.results) {
|
|
2102
|
-
const usd = Number(r.amount) / 100;
|
|
2103
|
-
if (usd === 0)
|
|
2104
|
-
continue;
|
|
2105
|
-
const desc = (r.description ?? "unknown").substring(0, 200);
|
|
2106
|
-
upsertBillingDaily(db, { date, provider: "anthropic", description: desc, cost_usd: usd, updated_at: updatedAt });
|
|
2107
|
-
totalUsd += usd;
|
|
2108
|
-
}
|
|
1656
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
|
|
1657
|
+
import { homedir as homedir4 } from "os";
|
|
1658
|
+
import { join as join6 } from "path";
|
|
1659
|
+
var GEMINI_TMP_DIR = join6(homedir4(), ".gemini", "tmp");
|
|
1660
|
+
async function ingestGemini(db, verbose) {
|
|
1661
|
+
if (!existsSync5(GEMINI_TMP_DIR)) {
|
|
1662
|
+
if (verbose)
|
|
1663
|
+
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
1664
|
+
return { sessions: 0 };
|
|
2109
1665
|
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
const
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
const updatedAt = new Date().toISOString();
|
|
2148
|
-
for (const bucket of buckets) {
|
|
2149
|
-
const date = new Date(bucket.start_time * 1000).toISOString().substring(0, 10);
|
|
2150
|
-
for (const r of bucket.results) {
|
|
2151
|
-
const usd = Number(r.amount?.value ?? 0);
|
|
2152
|
-
if (usd === 0)
|
|
1666
|
+
let totalSessions = 0;
|
|
1667
|
+
const touchedSessions = new Set;
|
|
1668
|
+
let projectHashDirs = [];
|
|
1669
|
+
try {
|
|
1670
|
+
projectHashDirs = readdirSync3(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join6(GEMINI_TMP_DIR, d.name));
|
|
1671
|
+
} catch {
|
|
1672
|
+
return { sessions: 0 };
|
|
1673
|
+
}
|
|
1674
|
+
for (const projectDir of projectHashDirs) {
|
|
1675
|
+
const chatsDir = join6(projectDir, "chats");
|
|
1676
|
+
if (!existsSync5(chatsDir))
|
|
1677
|
+
continue;
|
|
1678
|
+
let chatFiles = [];
|
|
1679
|
+
try {
|
|
1680
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join6(chatsDir, f));
|
|
1681
|
+
} catch {
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
for (const filePath of chatFiles) {
|
|
1685
|
+
const stateKey = filePath.replace(homedir4(), "~");
|
|
1686
|
+
let fileMtime = "0";
|
|
1687
|
+
try {
|
|
1688
|
+
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
1689
|
+
} catch {
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
const processed = getIngestState(db, "gemini", stateKey);
|
|
1693
|
+
if (processed === fileMtime)
|
|
1694
|
+
continue;
|
|
1695
|
+
let chatData;
|
|
1696
|
+
try {
|
|
1697
|
+
chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
1698
|
+
} catch {
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
const sessionId = chatData.sessionId;
|
|
1702
|
+
if (!sessionId)
|
|
2153
1703
|
continue;
|
|
2154
|
-
const
|
|
2155
|
-
|
|
2156
|
-
|
|
1704
|
+
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
1705
|
+
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
1706
|
+
if (!existing) {
|
|
1707
|
+
const session = {
|
|
1708
|
+
id: sessionId,
|
|
1709
|
+
agent: "gemini",
|
|
1710
|
+
project_path: "",
|
|
1711
|
+
project_name: "",
|
|
1712
|
+
started_at: startTime,
|
|
1713
|
+
ended_at: chatData.lastUpdated ?? null,
|
|
1714
|
+
total_cost_usd: 0,
|
|
1715
|
+
total_tokens: 0,
|
|
1716
|
+
request_count: 0
|
|
1717
|
+
};
|
|
1718
|
+
upsertSession(db, session);
|
|
1719
|
+
touchedSessions.add(sessionId);
|
|
1720
|
+
totalSessions++;
|
|
1721
|
+
}
|
|
1722
|
+
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
2157
1723
|
}
|
|
2158
1724
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
init_database();
|
|
2164
|
-
|
|
2165
|
-
// src/lib/package-metadata.ts
|
|
2166
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
2167
|
-
var cachedMetadata = null;
|
|
2168
|
-
function getPackageMetadata() {
|
|
2169
|
-
if (cachedMetadata)
|
|
2170
|
-
return cachedMetadata;
|
|
2171
|
-
const raw = readFileSync5(new URL("../../package.json", import.meta.url), "utf8");
|
|
2172
|
-
const parsed = JSON.parse(raw);
|
|
2173
|
-
cachedMetadata = {
|
|
2174
|
-
name: parsed.name ?? "@hasna/economy",
|
|
2175
|
-
version: parsed.version ?? "0.0.0"
|
|
2176
|
-
};
|
|
2177
|
-
return cachedMetadata;
|
|
1725
|
+
for (const sessionId of touchedSessions) {
|
|
1726
|
+
rollupSession(db, sessionId);
|
|
1727
|
+
}
|
|
1728
|
+
return { sessions: totalSessions };
|
|
2178
1729
|
}
|
|
2179
|
-
var packageMetadata = getPackageMetadata();
|
|
2180
1730
|
|
|
2181
1731
|
// src/cli/index.ts
|
|
2182
1732
|
init_pricing();
|
|
2183
1733
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2184
1734
|
import { execSync as execSync2 } from "child_process";
|
|
2185
1735
|
var program = new Command;
|
|
2186
|
-
program.name("economy").description("AI coding cost tracker \u2014 Claude Code, Codex, and Gemini").version(
|
|
1736
|
+
program.name("economy").description("AI coding cost tracker \u2014 Claude Code, Codex, and Gemini").version("0.2.2");
|
|
2187
1737
|
async function autoSync() {
|
|
2188
1738
|
const db = openDatabase();
|
|
2189
1739
|
ensurePricingSeeded(db);
|
|
2190
1740
|
await ingestClaude(db);
|
|
2191
|
-
await ingestTakumi(db);
|
|
2192
1741
|
await ingestCodex(db);
|
|
2193
1742
|
await ingestGemini(db);
|
|
2194
1743
|
}
|
|
@@ -2301,7 +1850,7 @@ program.action(async () => {
|
|
|
2301
1850
|
}
|
|
2302
1851
|
console.log();
|
|
2303
1852
|
});
|
|
2304
|
-
program.command("sync").description("Ingest cost data from Claude Code, Codex, and Gemini").option("--claude", "Only ingest Claude Code telemetry").option("--
|
|
1853
|
+
program.command("sync").description("Ingest cost data from Claude Code, Codex, and Gemini").option("--claude", "Only ingest Claude Code telemetry").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").action(async (opts) => {
|
|
2305
1854
|
const db = openDatabase();
|
|
2306
1855
|
ensurePricingSeeded(db);
|
|
2307
1856
|
if (opts.force) {
|
|
@@ -2309,9 +1858,8 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
|
|
|
2309
1858
|
if (opts.verbose)
|
|
2310
1859
|
console.log(chalk4.dim("Cleared ingest cache"));
|
|
2311
1860
|
}
|
|
2312
|
-
const anySpecific = opts.claude || opts.
|
|
1861
|
+
const anySpecific = opts.claude || opts.codex || opts.gemini;
|
|
2313
1862
|
const doClaude = opts.claude || !anySpecific;
|
|
2314
|
-
const doTakumi = opts.takumi || !anySpecific;
|
|
2315
1863
|
const doCodex = opts.codex || !anySpecific;
|
|
2316
1864
|
const doGemini = opts.gemini || !anySpecific;
|
|
2317
1865
|
if (doClaude) {
|
|
@@ -2319,11 +1867,6 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
|
|
|
2319
1867
|
const r = await ingestClaude(db, opts.verbose);
|
|
2320
1868
|
console.log(chalk4.green(`\u2713 ${r.files} files, ${r.requests} requests, ${r.sessions} sessions`));
|
|
2321
1869
|
}
|
|
2322
|
-
if (doTakumi) {
|
|
2323
|
-
process.stdout.write(chalk4.cyan("\u2192 Ingesting Takumi sessions... "));
|
|
2324
|
-
const r = await ingestTakumi(db, opts.verbose);
|
|
2325
|
-
console.log(chalk4.green(`\u2713 ${r.files} files, ${r.requests} requests, ${r.sessions} sessions`));
|
|
2326
|
-
}
|
|
2327
1870
|
if (doCodex) {
|
|
2328
1871
|
process.stdout.write(chalk4.cyan("\u2192 Ingesting Codex sessions... "));
|
|
2329
1872
|
const r = await ingestCodex(db, opts.verbose);
|
|
@@ -2334,35 +1877,6 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
|
|
|
2334
1877
|
const r = await ingestGemini(db, opts.verbose);
|
|
2335
1878
|
console.log(chalk4.green(`\u2713 ${r.sessions} sessions`));
|
|
2336
1879
|
}
|
|
2337
|
-
if (opts.backfillMachine) {
|
|
2338
|
-
const machine = getMachineId();
|
|
2339
|
-
const reqCount = db.prepare(`UPDATE requests SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
|
|
2340
|
-
const sessCount = db.prepare(`UPDATE sessions SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
|
|
2341
|
-
console.log(chalk4.cyan(`\u2192 Backfilled machine_id='${machine}': ${reqCount.changes} requests, ${sessCount.changes} sessions`));
|
|
2342
|
-
}
|
|
2343
|
-
if (opts.recalculate) {
|
|
2344
|
-
const { computeCostFromDb: computeCostFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
|
|
2345
|
-
const zeroRows = db.prepare(`SELECT id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens FROM requests WHERE cost_usd = 0 AND (input_tokens > 0 OR output_tokens > 0)`).all();
|
|
2346
|
-
let fixed = 0;
|
|
2347
|
-
for (const r of zeroRows) {
|
|
2348
|
-
const cost = computeCostFromDb2(db, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, r.cache_create_tokens);
|
|
2349
|
-
if (cost > 0) {
|
|
2350
|
-
db.prepare(`UPDATE requests SET cost_usd = ? WHERE id = ?`).run(cost, r.id);
|
|
2351
|
-
fixed++;
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
if (fixed > 0) {
|
|
2355
|
-
const touchedSessions = new Set(zeroRows.map((r) => {
|
|
2356
|
-
const row = db.prepare(`SELECT session_id FROM requests WHERE id = ?`).get(r.id);
|
|
2357
|
-
return row?.session_id;
|
|
2358
|
-
}).filter(Boolean));
|
|
2359
|
-
const { rollupSession: rollupSession2 } = await Promise.resolve().then(() => (init_database(), exports_database));
|
|
2360
|
-
for (const sid of touchedSessions) {
|
|
2361
|
-
rollupSession2(db, sid);
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
console.log(chalk4.cyan(`\u2192 Recalculated: ${fixed}/${zeroRows.length} zero-cost requests now have pricing`));
|
|
2365
|
-
}
|
|
2366
1880
|
try {
|
|
2367
1881
|
const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
2368
1882
|
await checkAndFireWebhooks2(db);
|
|
@@ -2382,14 +1896,13 @@ program.command("month").description("Cost summary for this month").action(async
|
|
|
2382
1896
|
await autoSync();
|
|
2383
1897
|
printSummary("This Month", "month");
|
|
2384
1898
|
});
|
|
2385
|
-
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("--
|
|
1899
|
+
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").option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--search <query>", "Search by project name, session id prefix, or agent").action(async (opts) => {
|
|
2386
1900
|
await autoSync();
|
|
2387
1901
|
const db = openDatabase();
|
|
2388
1902
|
const sinceDate = opts.since ? parseSinceDate(opts.since) : undefined;
|
|
2389
1903
|
let sessions = querySessions(db, {
|
|
2390
1904
|
agent: opts.agent,
|
|
2391
1905
|
project: opts.project,
|
|
2392
|
-
machine: opts.machine,
|
|
2393
1906
|
limit: Number(opts.limit ?? 20),
|
|
2394
1907
|
since: sinceDate,
|
|
2395
1908
|
search: opts.search
|
|
@@ -2838,29 +2351,6 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
2838
2351
|
}
|
|
2839
2352
|
console.log();
|
|
2840
2353
|
});
|
|
2841
|
-
program.command("machines").description("List all machines that have synced data").action(async () => {
|
|
2842
|
-
await autoSync();
|
|
2843
|
-
const db = openDatabase();
|
|
2844
|
-
const machines = listMachines(db);
|
|
2845
|
-
const current = getMachineId();
|
|
2846
|
-
if (machines.length === 0) {
|
|
2847
|
-
console.log(chalk4.yellow(`No machine data yet. Current machine: ${current}`));
|
|
2848
|
-
return;
|
|
2849
|
-
}
|
|
2850
|
-
console.log();
|
|
2851
|
-
console.log(chalk4.bold.cyan(" Machines"));
|
|
2852
|
-
console.log();
|
|
2853
|
-
printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"], machines.map((m) => [
|
|
2854
|
-
m.machine_id === current ? chalk4.green(`${m.machine_id} (this)`) : chalk4.white(m.machine_id),
|
|
2855
|
-
fmtCount(m.sessions),
|
|
2856
|
-
fmtCount(m.requests),
|
|
2857
|
-
fmt2(m.total_cost_usd),
|
|
2858
|
-
chalk4.dim(m.last_active?.substring(0, 16) ?? "\u2014")
|
|
2859
|
-
]));
|
|
2860
|
-
console.log(`
|
|
2861
|
-
${chalk4.dim("Current machine:")} ${chalk4.bold(current)}`);
|
|
2862
|
-
console.log();
|
|
2863
|
-
});
|
|
2864
2354
|
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) => {
|
|
2865
2355
|
await autoSync();
|
|
2866
2356
|
const db = openDatabase();
|
|
@@ -3128,144 +2618,5 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
|
|
|
3128
2618
|
process.exit(1);
|
|
3129
2619
|
}
|
|
3130
2620
|
});
|
|
3131
|
-
var CLOUD_RDS_HOST = "hasnaxyz-prod-opensource.c4limg0qgqvk.us-east-1.rds.amazonaws.com";
|
|
3132
|
-
var CLOUD_RDS_USER = "hasna_admin";
|
|
3133
|
-
var CLOUD_RDS_DB = "economy";
|
|
3134
|
-
var CLOUD_TABLES = ["requests", "sessions", "projects", "budgets", "goals", "model_pricing"];
|
|
3135
|
-
async function getCloudPassword() {
|
|
3136
|
-
if (process.env["ECONOMY_PG_PASSWORD"])
|
|
3137
|
-
return process.env["ECONOMY_PG_PASSWORD"];
|
|
3138
|
-
const { execSync: exec } = await import("child_process");
|
|
3139
|
-
const secretJson = exec(`aws --profile hasna-xyz-hq secretsmanager get-secret-value --secret-id 'rds!db-7a451ce6-83a9-40fa-b24a-81e5d5943511' --query SecretString --output text`, { timeout: 1e4, encoding: "utf-8" });
|
|
3140
|
-
return JSON.parse(secretJson).password;
|
|
3141
|
-
}
|
|
3142
|
-
async function getCloudPg() {
|
|
3143
|
-
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
3144
|
-
const pw = encodeURIComponent(await getCloudPassword());
|
|
3145
|
-
return new PgAdapterAsync(`postgresql://${CLOUD_RDS_USER}:${pw}@${CLOUD_RDS_HOST}:5432/${CLOUD_RDS_DB}?sslmode=require`);
|
|
3146
|
-
}
|
|
3147
|
-
var cloudCmd = program.command("cloud").description("Cross-machine sync via cloud PostgreSQL");
|
|
3148
|
-
cloudCmd.command("push").description("Push local economy data to cloud PostgreSQL").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
|
|
3149
|
-
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
3150
|
-
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
3151
|
-
const cloud = await getCloudPg();
|
|
3152
|
-
const local = new SqliteAdapter(getDbPath());
|
|
3153
|
-
process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
|
|
3154
|
-
for (const sql of PG_MIGRATIONS2) {
|
|
3155
|
-
await cloud.run(sql);
|
|
3156
|
-
}
|
|
3157
|
-
console.log(chalk4.green("\u2713"));
|
|
3158
|
-
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : CLOUD_TABLES;
|
|
3159
|
-
process.stdout.write(chalk4.cyan(`\u2192 Pushing ${tableList.join(", ")}... `));
|
|
3160
|
-
const results = await syncPush(local, cloud, { tables: tableList });
|
|
3161
|
-
const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3162
|
-
console.log(chalk4.green(`\u2713 ${totalRows} rows across ${tableList.length} tables`));
|
|
3163
|
-
local.close();
|
|
3164
|
-
await cloud.close();
|
|
3165
|
-
console.log(chalk4.bold.green(`
|
|
3166
|
-
\u2713 Push complete from ${getMachineId()}`));
|
|
3167
|
-
});
|
|
3168
|
-
cloudCmd.command("pull").description("Pull cloud PostgreSQL data to local").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
|
|
3169
|
-
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
3170
|
-
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
3171
|
-
const cloud = await getCloudPg();
|
|
3172
|
-
const local = new SqliteAdapter(getDbPath());
|
|
3173
|
-
process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
|
|
3174
|
-
for (const sql of PG_MIGRATIONS2) {
|
|
3175
|
-
await cloud.run(sql);
|
|
3176
|
-
}
|
|
3177
|
-
console.log(chalk4.green("\u2713"));
|
|
3178
|
-
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : CLOUD_TABLES;
|
|
3179
|
-
process.stdout.write(chalk4.cyan(`\u2192 Pulling ${tableList.join(", ")}... `));
|
|
3180
|
-
const results = await syncPull(cloud, local, { tables: tableList });
|
|
3181
|
-
const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3182
|
-
console.log(chalk4.green(`\u2713 ${totalRows} rows across ${tableList.length} tables`));
|
|
3183
|
-
local.close();
|
|
3184
|
-
await cloud.close();
|
|
3185
|
-
console.log(chalk4.bold.green(`
|
|
3186
|
-
\u2713 Pull complete to ${getMachineId()}`));
|
|
3187
|
-
});
|
|
3188
|
-
cloudCmd.command("sync").description("Full sync: ingest local \u2192 push to cloud \u2192 pull from cloud").action(async () => {
|
|
3189
|
-
console.log(chalk4.bold.cyan(` Cloud Sync \u2014 ${getMachineId()}
|
|
3190
|
-
`));
|
|
3191
|
-
process.stdout.write(chalk4.cyan("\u2192 Ingesting local data... "));
|
|
3192
|
-
await autoSync();
|
|
3193
|
-
console.log(chalk4.green("\u2713"));
|
|
3194
|
-
const { syncPush, syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
3195
|
-
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
3196
|
-
const cloud = await getCloudPg();
|
|
3197
|
-
const local = new SqliteAdapter(getDbPath());
|
|
3198
|
-
for (const sql of PG_MIGRATIONS2) {
|
|
3199
|
-
await cloud.run(sql);
|
|
3200
|
-
}
|
|
3201
|
-
process.stdout.write(chalk4.cyan("\u2192 Pushing local \u2192 cloud... "));
|
|
3202
|
-
const pushResults = await syncPush(local, cloud, { tables: CLOUD_TABLES });
|
|
3203
|
-
console.log(chalk4.green(`\u2713 ${pushResults.reduce((s, r) => s + r.rowsWritten, 0)} rows`));
|
|
3204
|
-
process.stdout.write(chalk4.cyan("\u2192 Pulling cloud \u2192 local... "));
|
|
3205
|
-
const pullResults = await syncPull(cloud, local, { tables: CLOUD_TABLES });
|
|
3206
|
-
console.log(chalk4.green(`\u2713 ${pullResults.reduce((s, r) => s + r.rowsWritten, 0)} rows`));
|
|
3207
|
-
local.close();
|
|
3208
|
-
await cloud.close();
|
|
3209
|
-
console.log(chalk4.bold.green(`
|
|
3210
|
-
\u2713 Cloud sync complete`));
|
|
3211
|
-
});
|
|
3212
|
-
cloudCmd.command("status").description("Check cloud connection status").action(async () => {
|
|
3213
|
-
console.log();
|
|
3214
|
-
console.log(` Machine: ${chalk4.white(getMachineId())}`);
|
|
3215
|
-
console.log(` RDS Host: ${chalk4.white(CLOUD_RDS_HOST)}`);
|
|
3216
|
-
console.log(` Database: ${chalk4.white(CLOUD_RDS_DB)}`);
|
|
3217
|
-
try {
|
|
3218
|
-
const cloud = await getCloudPg();
|
|
3219
|
-
await cloud.get("SELECT 1 as ok");
|
|
3220
|
-
const tables = await cloud.all("SELECT tablename FROM pg_tables WHERE schemaname = 'public'");
|
|
3221
|
-
console.log(` PostgreSQL: ${chalk4.green("connected")}`);
|
|
3222
|
-
console.log(` Tables: ${chalk4.white(tables.map((t) => t.tablename).join(", ") || "(none)")}`);
|
|
3223
|
-
await cloud.close();
|
|
3224
|
-
} catch (err2) {
|
|
3225
|
-
console.log(` PostgreSQL: ${chalk4.red(`failed \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`)}`);
|
|
3226
|
-
}
|
|
3227
|
-
console.log();
|
|
3228
|
-
});
|
|
3229
|
-
var billingCmd = program.command("billing").description("Pull actual billing from provider admin APIs (ground truth)");
|
|
3230
|
-
billingCmd.command("sync").description("Sync actual billing from Anthropic and OpenAI admin APIs").option("--days <n>", "Days of history to fetch", "31").option("--anthropic", "Only sync Anthropic").option("--openai", "Only sync OpenAI").action(async (opts) => {
|
|
3231
|
-
const db = openDatabase();
|
|
3232
|
-
const days = Number(opts.days ?? 31);
|
|
3233
|
-
const doBoth = !opts.anthropic && !opts.openai;
|
|
3234
|
-
if (opts.anthropic || doBoth) {
|
|
3235
|
-
process.stdout.write(chalk4.cyan("\u2192 Syncing Anthropic billing... "));
|
|
3236
|
-
try {
|
|
3237
|
-
const r = await syncAnthropicBilling(db, { days });
|
|
3238
|
-
console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
|
|
3239
|
-
} catch (e) {
|
|
3240
|
-
console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
|
|
3241
|
-
}
|
|
3242
|
-
}
|
|
3243
|
-
if (opts.openai || doBoth) {
|
|
3244
|
-
process.stdout.write(chalk4.cyan("\u2192 Syncing OpenAI billing... "));
|
|
3245
|
-
try {
|
|
3246
|
-
const r = await syncOpenAIBilling(db, { days });
|
|
3247
|
-
console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
|
|
3248
|
-
} catch (e) {
|
|
3249
|
-
console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
|
|
3250
|
-
}
|
|
3251
|
-
}
|
|
3252
|
-
});
|
|
3253
|
-
billingCmd.command("show").description("Show actual billing totals vs our estimated costs").option("--period <p>", "Period: today|yesterday|week|month|year|all", "month").action((opts) => {
|
|
3254
|
-
const db = openDatabase();
|
|
3255
|
-
const period = opts.period ?? "month";
|
|
3256
|
-
const actual = queryBillingSummary(db, period);
|
|
3257
|
-
const estimated = querySummary(db, period);
|
|
3258
|
-
console.log();
|
|
3259
|
-
console.log(chalk4.bold.cyan(` Billing ${period} (actual from admin APIs)
|
|
3260
|
-
`));
|
|
3261
|
-
printTable(["Provider", "Actual (billed)"], Object.entries(actual.by_provider).map(([p, c]) => [chalk4.white(p), fmt2(c)]));
|
|
3262
|
-
console.log();
|
|
3263
|
-
console.log(` ${chalk4.bold("Actual total:")} ${fmt2(actual.total_usd)}`);
|
|
3264
|
-
console.log(` ${chalk4.dim("Our estimate:")} ${fmt2(estimated.total_usd)}`);
|
|
3265
|
-
const diff = estimated.total_usd - actual.total_usd;
|
|
3266
|
-
const pct = actual.total_usd > 0 ? diff / actual.total_usd * 100 : 0;
|
|
3267
|
-
console.log(` ${chalk4.dim("Difference:")} ${fmt2(Math.abs(diff))} (${diff >= 0 ? "+" : ""}${pct.toFixed(1)}%)`);
|
|
3268
|
-
console.log();
|
|
3269
|
-
});
|
|
3270
2621
|
registerBrainsCommand(program);
|
|
3271
2622
|
program.parse();
|