@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/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,
|
|
@@ -474,39 +377,19 @@ function queryModelBreakdown(db) {
|
|
|
474
377
|
}
|
|
475
378
|
function queryProjectBreakdown(db) {
|
|
476
379
|
return db.prepare(`
|
|
477
|
-
WITH labeled AS (
|
|
478
|
-
SELECT
|
|
479
|
-
s.id,
|
|
480
|
-
s.project_path,
|
|
481
|
-
s.total_cost_usd,
|
|
482
|
-
s.started_at,
|
|
483
|
-
COALESCE(
|
|
484
|
-
NULLIF(s.project_name, ''),
|
|
485
|
-
CASE
|
|
486
|
-
WHEN s.project_path LIKE '%/%'
|
|
487
|
-
THEN substr(s.project_path, length(rtrim(s.project_path, replace(s.project_path, '/', ''))) + 1)
|
|
488
|
-
ELSE s.project_path
|
|
489
|
-
END
|
|
490
|
-
) as label
|
|
491
|
-
FROM sessions s
|
|
492
|
-
WHERE s.project_path != '' OR s.project_name != ''
|
|
493
|
-
)
|
|
494
380
|
SELECT
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
COUNT(DISTINCT
|
|
498
|
-
|
|
499
|
-
COALESCE(
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
MAX(l.started_at) as last_active
|
|
508
|
-
FROM labeled l
|
|
509
|
-
GROUP BY l.label
|
|
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
|
|
510
393
|
ORDER BY cost_usd DESC
|
|
511
394
|
`).all();
|
|
512
395
|
}
|
|
@@ -626,40 +509,6 @@ function setIngestState(db, source, key, value) {
|
|
|
626
509
|
function queryRequestsSince(db, since) {
|
|
627
510
|
return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
|
|
628
511
|
}
|
|
629
|
-
function upsertBillingDaily(db, row) {
|
|
630
|
-
db.prepare(`
|
|
631
|
-
INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
|
|
632
|
-
VALUES (?, ?, ?, ?, ?)
|
|
633
|
-
`).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
|
|
634
|
-
}
|
|
635
|
-
function clearBillingRange(db, provider, fromDate, toDate) {
|
|
636
|
-
db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
|
|
637
|
-
}
|
|
638
|
-
function queryBillingSummary(db, period) {
|
|
639
|
-
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";
|
|
640
|
-
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
|
|
641
|
-
const by_provider = {};
|
|
642
|
-
let total = 0;
|
|
643
|
-
for (const r of rows) {
|
|
644
|
-
by_provider[r.provider] = r.cost;
|
|
645
|
-
total += r.cost;
|
|
646
|
-
}
|
|
647
|
-
return { total_usd: total, by_provider };
|
|
648
|
-
}
|
|
649
|
-
function listMachines(db) {
|
|
650
|
-
return db.prepare(`
|
|
651
|
-
SELECT
|
|
652
|
-
s.machine_id,
|
|
653
|
-
COUNT(DISTINCT s.id) as sessions,
|
|
654
|
-
COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
|
|
655
|
-
COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
|
|
656
|
-
MAX(s.started_at) as last_active
|
|
657
|
-
FROM sessions s
|
|
658
|
-
WHERE s.machine_id != ''
|
|
659
|
-
GROUP BY s.machine_id
|
|
660
|
-
ORDER BY total_cost_usd DESC
|
|
661
|
-
`).all();
|
|
662
|
-
}
|
|
663
512
|
function upsertModelPricing(db, p) {
|
|
664
513
|
db.prepare(`
|
|
665
514
|
INSERT OR REPLACE INTO model_pricing
|
|
@@ -677,11 +526,11 @@ function deleteModelPricing(db, model) {
|
|
|
677
526
|
db.prepare(`DELETE FROM model_pricing WHERE model = ?`).run(model);
|
|
678
527
|
}
|
|
679
528
|
function seedModelPricing(db, defaults) {
|
|
680
|
-
const existing =
|
|
529
|
+
const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
|
|
530
|
+
if (existing.count > 0)
|
|
531
|
+
return;
|
|
681
532
|
const now = new Date().toISOString();
|
|
682
533
|
for (const [model, p] of Object.entries(defaults)) {
|
|
683
|
-
if (existing.has(model))
|
|
684
|
-
continue;
|
|
685
534
|
upsertModelPricing(db, {
|
|
686
535
|
model,
|
|
687
536
|
input_per_1m: p.inputPer1M,
|
|
@@ -720,36 +569,29 @@ function collectJsonlFiles(projectDir) {
|
|
|
720
569
|
return files;
|
|
721
570
|
}
|
|
722
571
|
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
async function ingestTakumi(db, verbose = false) {
|
|
726
|
-
return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
|
|
727
|
-
}
|
|
728
|
-
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
729
|
-
if (!existsSync3(projectsDir)) {
|
|
572
|
+
if (!existsSync3(PROJECTS_DIR)) {
|
|
730
573
|
if (verbose)
|
|
731
|
-
console.log(
|
|
574
|
+
console.log("Claude projects dir not found:", PROJECTS_DIR);
|
|
732
575
|
return { files: 0, requests: 0, sessions: 0 };
|
|
733
576
|
}
|
|
734
|
-
const machineId = getMachineId();
|
|
735
577
|
let totalFiles = 0;
|
|
736
578
|
let totalRequests = 0;
|
|
737
579
|
const touchedSessions = new Set;
|
|
738
580
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
739
|
-
const projectDirs = readdirSync2(
|
|
581
|
+
const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
740
582
|
for (const projectDirEntry of projectDirs) {
|
|
741
|
-
const projectDirPath = join4(
|
|
583
|
+
const projectDirPath = join4(PROJECTS_DIR, projectDirEntry.name);
|
|
742
584
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
743
585
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
744
586
|
for (const filePath of jsonlFiles) {
|
|
745
|
-
const stateKey = filePath.replace(
|
|
587
|
+
const stateKey = filePath.replace(PROJECTS_DIR, "");
|
|
746
588
|
let fileMtime = "0";
|
|
747
589
|
try {
|
|
748
590
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
749
591
|
} catch {
|
|
750
592
|
continue;
|
|
751
593
|
}
|
|
752
|
-
const processed = getIngestState(db,
|
|
594
|
+
const processed = getIngestState(db, "claude", stateKey);
|
|
753
595
|
if (processed === fileMtime)
|
|
754
596
|
continue;
|
|
755
597
|
let lines;
|
|
@@ -790,10 +632,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
790
632
|
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
791
633
|
continue;
|
|
792
634
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
793
|
-
const reqId =
|
|
635
|
+
const reqId = `claude-${sessionId}-${timestamp}`;
|
|
794
636
|
upsertRequest(db, {
|
|
795
637
|
id: reqId,
|
|
796
|
-
agent:
|
|
638
|
+
agent: "claude",
|
|
797
639
|
session_id: sessionId,
|
|
798
640
|
model,
|
|
799
641
|
input_tokens: inputTokens,
|
|
@@ -803,8 +645,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
803
645
|
cost_usd: costUsd,
|
|
804
646
|
duration_ms: 0,
|
|
805
647
|
timestamp,
|
|
806
|
-
source_request_id: reqId
|
|
807
|
-
machine_id: machineId
|
|
648
|
+
source_request_id: reqId
|
|
808
649
|
});
|
|
809
650
|
if (!touchedSessions.has(sessionId)) {
|
|
810
651
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
@@ -813,15 +654,14 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
813
654
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
814
655
|
const session = {
|
|
815
656
|
id: sessionId,
|
|
816
|
-
agent:
|
|
657
|
+
agent: "claude",
|
|
817
658
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
818
659
|
project_name: detectedProject ? detectedProject.name : "",
|
|
819
660
|
started_at: timestamp,
|
|
820
661
|
ended_at: null,
|
|
821
662
|
total_cost_usd: 0,
|
|
822
663
|
total_tokens: 0,
|
|
823
|
-
request_count: 0
|
|
824
|
-
machine_id: machineId
|
|
664
|
+
request_count: 0
|
|
825
665
|
};
|
|
826
666
|
upsertSession(db, session);
|
|
827
667
|
}
|
|
@@ -829,7 +669,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
829
669
|
}
|
|
830
670
|
totalRequests++;
|
|
831
671
|
}
|
|
832
|
-
setIngestState(db,
|
|
672
|
+
setIngestState(db, "claude", stateKey, fileMtime);
|
|
833
673
|
totalFiles++;
|
|
834
674
|
}
|
|
835
675
|
}
|
|
@@ -838,30 +678,28 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
838
678
|
}
|
|
839
679
|
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
840
680
|
}
|
|
841
|
-
var
|
|
681
|
+
var PROJECTS_DIR;
|
|
842
682
|
var init_claude = __esm(() => {
|
|
843
683
|
init_database();
|
|
844
684
|
init_pricing();
|
|
845
|
-
|
|
846
|
-
TAKUMI_PROJECTS_DIR = join4(homedir2(), ".takumi", "projects");
|
|
685
|
+
PROJECTS_DIR = join4(homedir2(), ".claude", "projects");
|
|
847
686
|
});
|
|
848
687
|
|
|
849
688
|
// src/ingest/codex.ts
|
|
850
689
|
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
851
690
|
import { homedir as homedir3 } from "os";
|
|
852
691
|
import { join as join5, basename as basename2 } from "path";
|
|
853
|
-
import { Database as
|
|
692
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
854
693
|
async function ingestCodex(db, verbose = false) {
|
|
855
694
|
if (!existsSync4(CODEX_DB_PATH)) {
|
|
856
695
|
if (verbose)
|
|
857
696
|
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
858
697
|
return { sessions: 0 };
|
|
859
698
|
}
|
|
860
|
-
const machineId = getMachineId();
|
|
861
699
|
let codexDb = null;
|
|
862
700
|
let ingested = 0;
|
|
863
701
|
try {
|
|
864
|
-
codexDb = new
|
|
702
|
+
codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
|
|
865
703
|
const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
|
|
866
704
|
for (const thread of threads) {
|
|
867
705
|
const stateKey = thread.id;
|
|
@@ -882,8 +720,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
882
720
|
ended_at: endedAt,
|
|
883
721
|
total_cost_usd: costUsd,
|
|
884
722
|
total_tokens: thread.tokens_used,
|
|
885
|
-
request_count: 1
|
|
886
|
-
machine_id: machineId
|
|
723
|
+
request_count: 1
|
|
887
724
|
});
|
|
888
725
|
setIngestState(db, "codex", stateKey, "done");
|
|
889
726
|
ingested++;
|
|
@@ -902,88 +739,6 @@ var init_codex = __esm(() => {
|
|
|
902
739
|
CODEX_CONFIG_PATH = join5(homedir3(), ".codex", "config.toml");
|
|
903
740
|
});
|
|
904
741
|
|
|
905
|
-
// src/ingest/gemini.ts
|
|
906
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
|
|
907
|
-
import { homedir as homedir4 } from "os";
|
|
908
|
-
import { join as join6 } from "path";
|
|
909
|
-
async function ingestGemini(db, verbose) {
|
|
910
|
-
if (!existsSync5(GEMINI_TMP_DIR)) {
|
|
911
|
-
if (verbose)
|
|
912
|
-
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
913
|
-
return { sessions: 0 };
|
|
914
|
-
}
|
|
915
|
-
const machineId = getMachineId();
|
|
916
|
-
let totalSessions = 0;
|
|
917
|
-
const touchedSessions = new Set;
|
|
918
|
-
let projectHashDirs = [];
|
|
919
|
-
try {
|
|
920
|
-
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));
|
|
921
|
-
} catch {
|
|
922
|
-
return { sessions: 0 };
|
|
923
|
-
}
|
|
924
|
-
for (const projectDir of projectHashDirs) {
|
|
925
|
-
const chatsDir = join6(projectDir, "chats");
|
|
926
|
-
if (!existsSync5(chatsDir))
|
|
927
|
-
continue;
|
|
928
|
-
let chatFiles = [];
|
|
929
|
-
try {
|
|
930
|
-
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join6(chatsDir, f));
|
|
931
|
-
} catch {
|
|
932
|
-
continue;
|
|
933
|
-
}
|
|
934
|
-
for (const filePath of chatFiles) {
|
|
935
|
-
const stateKey = filePath.replace(homedir4(), "~");
|
|
936
|
-
let fileMtime = "0";
|
|
937
|
-
try {
|
|
938
|
-
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
939
|
-
} catch {
|
|
940
|
-
continue;
|
|
941
|
-
}
|
|
942
|
-
const processed = getIngestState(db, "gemini", stateKey);
|
|
943
|
-
if (processed === fileMtime)
|
|
944
|
-
continue;
|
|
945
|
-
let chatData;
|
|
946
|
-
try {
|
|
947
|
-
chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
948
|
-
} catch {
|
|
949
|
-
continue;
|
|
950
|
-
}
|
|
951
|
-
const sessionId = chatData.sessionId;
|
|
952
|
-
if (!sessionId)
|
|
953
|
-
continue;
|
|
954
|
-
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
955
|
-
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
956
|
-
if (!existing) {
|
|
957
|
-
const session = {
|
|
958
|
-
id: sessionId,
|
|
959
|
-
agent: "gemini",
|
|
960
|
-
project_path: "",
|
|
961
|
-
project_name: "",
|
|
962
|
-
started_at: startTime,
|
|
963
|
-
ended_at: chatData.lastUpdated ?? null,
|
|
964
|
-
total_cost_usd: 0,
|
|
965
|
-
total_tokens: 0,
|
|
966
|
-
request_count: 0,
|
|
967
|
-
machine_id: machineId
|
|
968
|
-
};
|
|
969
|
-
upsertSession(db, session);
|
|
970
|
-
touchedSessions.add(sessionId);
|
|
971
|
-
totalSessions++;
|
|
972
|
-
}
|
|
973
|
-
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
for (const sessionId of touchedSessions) {
|
|
977
|
-
rollupSession(db, sessionId);
|
|
978
|
-
}
|
|
979
|
-
return { sessions: totalSessions };
|
|
980
|
-
}
|
|
981
|
-
var GEMINI_TMP_DIR;
|
|
982
|
-
var init_gemini = __esm(() => {
|
|
983
|
-
init_database();
|
|
984
|
-
GEMINI_TMP_DIR = join6(homedir4(), ".gemini", "tmp");
|
|
985
|
-
});
|
|
986
|
-
|
|
987
742
|
// src/lib/config.ts
|
|
988
743
|
var exports_config = {};
|
|
989
744
|
__export(exports_config, {
|
|
@@ -992,12 +747,12 @@ __export(exports_config, {
|
|
|
992
747
|
loadConfig: () => loadConfig2,
|
|
993
748
|
getConfigValue: () => getConfigValue
|
|
994
749
|
});
|
|
995
|
-
import { existsSync as existsSync6, readFileSync as
|
|
750
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
996
751
|
import { join as join7 } from "path";
|
|
997
752
|
function loadConfig2() {
|
|
998
753
|
try {
|
|
999
754
|
if (existsSync6(CONFIG_PATH2)) {
|
|
1000
|
-
const raw =
|
|
755
|
+
const raw = readFileSync5(CONFIG_PATH2, "utf-8");
|
|
1001
756
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
1002
757
|
}
|
|
1003
758
|
} catch {}
|
|
@@ -1207,20 +962,6 @@ function ok(data, meta) {
|
|
|
1207
962
|
function err(message, status = 400) {
|
|
1208
963
|
return json({ error: message }, status);
|
|
1209
964
|
}
|
|
1210
|
-
function normalizeBudgetPeriod(value) {
|
|
1211
|
-
switch (value) {
|
|
1212
|
-
case "day":
|
|
1213
|
-
case "daily":
|
|
1214
|
-
return "daily";
|
|
1215
|
-
case "week":
|
|
1216
|
-
case "weekly":
|
|
1217
|
-
return "weekly";
|
|
1218
|
-
case "month":
|
|
1219
|
-
case "monthly":
|
|
1220
|
-
default:
|
|
1221
|
-
return "monthly";
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
965
|
function applyFields(obj, fields) {
|
|
1225
966
|
if (!fields || fields.length === 0)
|
|
1226
967
|
return obj;
|
|
@@ -1237,11 +978,7 @@ function createHandler(db) {
|
|
|
1237
978
|
return ok({ status: "ok", ts: new Date().toISOString() });
|
|
1238
979
|
if (path === "/api/summary" && method === "GET") {
|
|
1239
980
|
const period = url.searchParams.get("period") ?? "today";
|
|
1240
|
-
|
|
1241
|
-
return ok(querySummary(db, period, machine));
|
|
1242
|
-
}
|
|
1243
|
-
if (path === "/api/machines" && method === "GET") {
|
|
1244
|
-
return ok(listMachines(db), { current_machine: getMachineId() });
|
|
981
|
+
return ok(querySummary(db, period));
|
|
1245
982
|
}
|
|
1246
983
|
if (path === "/api/daily" && method === "GET") {
|
|
1247
984
|
const days = Number(url.searchParams.get("days") ?? 30);
|
|
@@ -1250,22 +987,12 @@ function createHandler(db) {
|
|
|
1250
987
|
if (path === "/api/sessions" && method === "GET") {
|
|
1251
988
|
const agent = url.searchParams.get("agent");
|
|
1252
989
|
const project = url.searchParams.get("project") ?? undefined;
|
|
1253
|
-
const search = url.searchParams.get("search") ?? undefined;
|
|
1254
|
-
const machine = url.searchParams.get("machine") ?? undefined;
|
|
1255
990
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
1256
991
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
1257
992
|
const since = url.searchParams.get("since") ?? undefined;
|
|
1258
993
|
const fieldsParam = url.searchParams.get("fields");
|
|
1259
994
|
const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
1260
|
-
const sessions = querySessions(db, {
|
|
1261
|
-
agent: agent ?? undefined,
|
|
1262
|
-
project,
|
|
1263
|
-
search,
|
|
1264
|
-
machine,
|
|
1265
|
-
limit,
|
|
1266
|
-
offset,
|
|
1267
|
-
since
|
|
1268
|
-
});
|
|
995
|
+
const sessions = querySessions(db, { agent: agent ?? undefined, project, limit, offset, since });
|
|
1269
996
|
return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
|
|
1270
997
|
}
|
|
1271
998
|
if (path === "/api/top" && method === "GET") {
|
|
@@ -1293,7 +1020,7 @@ function createHandler(db) {
|
|
|
1293
1020
|
id: randomUUID(),
|
|
1294
1021
|
project_path: body["project_path"] ?? null,
|
|
1295
1022
|
agent: body["agent"] ?? null,
|
|
1296
|
-
period:
|
|
1023
|
+
period: body["period"] ?? "monthly",
|
|
1297
1024
|
limit_usd: Number(body["limit_usd"]),
|
|
1298
1025
|
alert_at_percent: Number(body["alert_at_percent"] ?? 80),
|
|
1299
1026
|
created_at: now,
|
|
@@ -1354,12 +1081,8 @@ function createHandler(db) {
|
|
|
1354
1081
|
const results = {};
|
|
1355
1082
|
if (sources === "all" || sources === "claude")
|
|
1356
1083
|
results["claude"] = await ingestClaude(db);
|
|
1357
|
-
if (sources === "all" || sources === "takumi")
|
|
1358
|
-
results["takumi"] = await ingestTakumi(db);
|
|
1359
1084
|
if (sources === "all" || sources === "codex")
|
|
1360
1085
|
results["codex"] = await ingestCodex(db);
|
|
1361
|
-
if (sources === "all" || sources === "gemini")
|
|
1362
|
-
results["gemini"] = await ingestGemini(db);
|
|
1363
1086
|
return ok(results);
|
|
1364
1087
|
}
|
|
1365
1088
|
const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
|
|
@@ -1432,7 +1155,6 @@ var init_serve = __esm(() => {
|
|
|
1432
1155
|
init_database();
|
|
1433
1156
|
init_claude();
|
|
1434
1157
|
init_codex();
|
|
1435
|
-
init_gemini();
|
|
1436
1158
|
init_pricing();
|
|
1437
1159
|
CORS = {
|
|
1438
1160
|
"Access-Control-Allow-Origin": "*",
|
|
@@ -1570,102 +1292,6 @@ function menubarStop() {
|
|
|
1570
1292
|
var APP_PATH = "/Applications/Economy Bar.app", REPO = "hasna/open-economy";
|
|
1571
1293
|
var init_menubar = () => {};
|
|
1572
1294
|
|
|
1573
|
-
// src/db/pg-migrations.ts
|
|
1574
|
-
var exports_pg_migrations = {};
|
|
1575
|
-
__export(exports_pg_migrations, {
|
|
1576
|
-
PG_MIGRATIONS: () => PG_MIGRATIONS
|
|
1577
|
-
});
|
|
1578
|
-
var PG_MIGRATIONS;
|
|
1579
|
-
var init_pg_migrations = __esm(() => {
|
|
1580
|
-
PG_MIGRATIONS = [
|
|
1581
|
-
`CREATE TABLE IF NOT EXISTS requests (
|
|
1582
|
-
id TEXT PRIMARY KEY,
|
|
1583
|
-
agent TEXT NOT NULL,
|
|
1584
|
-
session_id TEXT NOT NULL,
|
|
1585
|
-
model TEXT NOT NULL,
|
|
1586
|
-
input_tokens INTEGER DEFAULT 0,
|
|
1587
|
-
output_tokens INTEGER DEFAULT 0,
|
|
1588
|
-
cache_read_tokens INTEGER DEFAULT 0,
|
|
1589
|
-
cache_create_tokens INTEGER DEFAULT 0,
|
|
1590
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
1591
|
-
duration_ms INTEGER DEFAULT 0,
|
|
1592
|
-
timestamp TEXT NOT NULL,
|
|
1593
|
-
source_request_id TEXT,
|
|
1594
|
-
machine_id TEXT DEFAULT ''
|
|
1595
|
-
)`,
|
|
1596
|
-
`CREATE TABLE IF NOT EXISTS sessions (
|
|
1597
|
-
id TEXT PRIMARY KEY,
|
|
1598
|
-
agent TEXT NOT NULL,
|
|
1599
|
-
project_path TEXT DEFAULT '',
|
|
1600
|
-
project_name TEXT DEFAULT '',
|
|
1601
|
-
started_at TEXT NOT NULL,
|
|
1602
|
-
ended_at TEXT,
|
|
1603
|
-
total_cost_usd REAL DEFAULT 0,
|
|
1604
|
-
total_tokens INTEGER DEFAULT 0,
|
|
1605
|
-
request_count INTEGER DEFAULT 0,
|
|
1606
|
-
machine_id TEXT DEFAULT ''
|
|
1607
|
-
)`,
|
|
1608
|
-
`CREATE TABLE IF NOT EXISTS projects (
|
|
1609
|
-
id TEXT PRIMARY KEY,
|
|
1610
|
-
path TEXT UNIQUE NOT NULL,
|
|
1611
|
-
name TEXT NOT NULL,
|
|
1612
|
-
description TEXT,
|
|
1613
|
-
tags TEXT DEFAULT '[]',
|
|
1614
|
-
created_at TEXT NOT NULL
|
|
1615
|
-
)`,
|
|
1616
|
-
`CREATE TABLE IF NOT EXISTS budgets (
|
|
1617
|
-
id TEXT PRIMARY KEY,
|
|
1618
|
-
project_path TEXT,
|
|
1619
|
-
agent TEXT,
|
|
1620
|
-
period TEXT NOT NULL,
|
|
1621
|
-
limit_usd REAL NOT NULL,
|
|
1622
|
-
alert_at_percent INTEGER DEFAULT 80,
|
|
1623
|
-
created_at TEXT NOT NULL,
|
|
1624
|
-
updated_at TEXT NOT NULL
|
|
1625
|
-
)`,
|
|
1626
|
-
`CREATE TABLE IF NOT EXISTS goals (
|
|
1627
|
-
id TEXT PRIMARY KEY,
|
|
1628
|
-
period TEXT NOT NULL,
|
|
1629
|
-
project_path TEXT,
|
|
1630
|
-
agent TEXT,
|
|
1631
|
-
limit_usd REAL NOT NULL,
|
|
1632
|
-
created_at TEXT NOT NULL,
|
|
1633
|
-
updated_at TEXT NOT NULL
|
|
1634
|
-
)`,
|
|
1635
|
-
`CREATE TABLE IF NOT EXISTS ingest_state (
|
|
1636
|
-
source TEXT NOT NULL,
|
|
1637
|
-
key TEXT NOT NULL,
|
|
1638
|
-
value TEXT NOT NULL,
|
|
1639
|
-
PRIMARY KEY (source, key)
|
|
1640
|
-
)`,
|
|
1641
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
1642
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
1643
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
1644
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
1645
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
1646
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
1647
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
1648
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
1649
|
-
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
1650
|
-
model TEXT PRIMARY KEY,
|
|
1651
|
-
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
1652
|
-
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
1653
|
-
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
1654
|
-
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
1655
|
-
updated_at TEXT NOT NULL
|
|
1656
|
-
)`,
|
|
1657
|
-
`CREATE TABLE IF NOT EXISTS feedback (
|
|
1658
|
-
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
1659
|
-
message TEXT NOT NULL,
|
|
1660
|
-
email TEXT,
|
|
1661
|
-
category TEXT DEFAULT 'general',
|
|
1662
|
-
version TEXT,
|
|
1663
|
-
machine_id TEXT,
|
|
1664
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
1665
|
-
)`
|
|
1666
|
-
];
|
|
1667
|
-
});
|
|
1668
|
-
|
|
1669
1295
|
// src/cli/index.ts
|
|
1670
1296
|
import { Command } from "commander";
|
|
1671
1297
|
import chalk4 from "chalk";
|
|
@@ -2024,148 +1650,94 @@ ${chalk.dim("Set it active:")} economy brains model set ${String(status["fine_tu
|
|
|
2024
1650
|
init_database();
|
|
2025
1651
|
init_claude();
|
|
2026
1652
|
init_codex();
|
|
2027
|
-
init_gemini();
|
|
2028
1653
|
|
|
2029
|
-
// src/ingest/
|
|
1654
|
+
// src/ingest/gemini.ts
|
|
2030
1655
|
init_database();
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
}
|
|
2040
|
-
async function syncAnthropicBilling(db, opts = {}) {
|
|
2041
|
-
const key = getAnthropicAdminKey();
|
|
2042
|
-
if (!key)
|
|
2043
|
-
throw new Error("Missing Anthropic admin key (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY)");
|
|
2044
|
-
const now = new Date;
|
|
2045
|
-
const end = opts.toDate ? new Date(opts.toDate) : new Date(now.getTime() + 24 * 3600000);
|
|
2046
|
-
const days = opts.days ?? 31;
|
|
2047
|
-
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
2048
|
-
const startIso = start.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
|
|
2049
|
-
const endIso = end.toISOString().replace(/\.\d+/, "").replace(/:\d{2}Z$/, ":00Z");
|
|
2050
|
-
let totalUsd = 0;
|
|
2051
|
-
const buckets = [];
|
|
2052
|
-
let nextPage;
|
|
2053
|
-
do {
|
|
2054
|
-
const url = new URL("https://api.anthropic.com/v1/organizations/cost_report");
|
|
2055
|
-
url.searchParams.set("starting_at", startIso);
|
|
2056
|
-
url.searchParams.set("ending_at", endIso);
|
|
2057
|
-
url.searchParams.set("bucket_width", "1d");
|
|
2058
|
-
url.searchParams.set("limit", "31");
|
|
2059
|
-
url.searchParams.append("group_by[]", "description");
|
|
2060
|
-
if (nextPage)
|
|
2061
|
-
url.searchParams.set("page", nextPage);
|
|
2062
|
-
const res = await fetch(url.toString(), {
|
|
2063
|
-
headers: { "anthropic-version": "2023-06-01", "x-api-key": key }
|
|
2064
|
-
});
|
|
2065
|
-
const data = await res.json();
|
|
2066
|
-
if (data.error)
|
|
2067
|
-
throw new Error(`Anthropic API: ${data.error.message}`);
|
|
2068
|
-
if (data.data)
|
|
2069
|
-
buckets.push(...data.data);
|
|
2070
|
-
nextPage = data.has_more ? data.next_page : undefined;
|
|
2071
|
-
} while (nextPage);
|
|
2072
|
-
const fromDateStr = toISODate(start);
|
|
2073
|
-
const toDateStr = toISODate(new Date(end.getTime() - 1000));
|
|
2074
|
-
clearBillingRange(db, "anthropic", fromDateStr, toDateStr);
|
|
2075
|
-
const updatedAt = new Date().toISOString();
|
|
2076
|
-
for (const bucket of buckets) {
|
|
2077
|
-
const date = bucket.starting_at.substring(0, 10);
|
|
2078
|
-
for (const r of bucket.results) {
|
|
2079
|
-
const usd = Number(r.amount) / 100;
|
|
2080
|
-
if (usd === 0)
|
|
2081
|
-
continue;
|
|
2082
|
-
const desc = (r.description ?? "unknown").substring(0, 200);
|
|
2083
|
-
upsertBillingDaily(db, { date, provider: "anthropic", description: desc, cost_usd: usd, updated_at: updatedAt });
|
|
2084
|
-
totalUsd += usd;
|
|
2085
|
-
}
|
|
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 };
|
|
2086
1665
|
}
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
const
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
const updatedAt = new Date().toISOString();
|
|
2125
|
-
for (const bucket of buckets) {
|
|
2126
|
-
const date = new Date(bucket.start_time * 1000).toISOString().substring(0, 10);
|
|
2127
|
-
for (const r of bucket.results) {
|
|
2128
|
-
const usd = Number(r.amount?.value ?? 0);
|
|
2129
|
-
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)
|
|
2130
1703
|
continue;
|
|
2131
|
-
const
|
|
2132
|
-
|
|
2133
|
-
|
|
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);
|
|
2134
1723
|
}
|
|
2135
1724
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
init_database();
|
|
2141
|
-
|
|
2142
|
-
// src/lib/package-metadata.ts
|
|
2143
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
2144
|
-
var cachedMetadata = null;
|
|
2145
|
-
function getPackageMetadata() {
|
|
2146
|
-
if (cachedMetadata)
|
|
2147
|
-
return cachedMetadata;
|
|
2148
|
-
const raw = readFileSync5(new URL("../../package.json", import.meta.url), "utf8");
|
|
2149
|
-
const parsed = JSON.parse(raw);
|
|
2150
|
-
cachedMetadata = {
|
|
2151
|
-
name: parsed.name ?? "@hasna/economy",
|
|
2152
|
-
version: parsed.version ?? "0.0.0"
|
|
2153
|
-
};
|
|
2154
|
-
return cachedMetadata;
|
|
1725
|
+
for (const sessionId of touchedSessions) {
|
|
1726
|
+
rollupSession(db, sessionId);
|
|
1727
|
+
}
|
|
1728
|
+
return { sessions: totalSessions };
|
|
2155
1729
|
}
|
|
2156
|
-
var packageMetadata = getPackageMetadata();
|
|
2157
1730
|
|
|
2158
1731
|
// src/cli/index.ts
|
|
2159
1732
|
init_pricing();
|
|
2160
1733
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2161
1734
|
import { execSync as execSync2 } from "child_process";
|
|
2162
1735
|
var program = new Command;
|
|
2163
|
-
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");
|
|
2164
1737
|
async function autoSync() {
|
|
2165
1738
|
const db = openDatabase();
|
|
2166
1739
|
ensurePricingSeeded(db);
|
|
2167
1740
|
await ingestClaude(db);
|
|
2168
|
-
await ingestTakumi(db);
|
|
2169
1741
|
await ingestCodex(db);
|
|
2170
1742
|
await ingestGemini(db);
|
|
2171
1743
|
}
|
|
@@ -2278,7 +1850,7 @@ program.action(async () => {
|
|
|
2278
1850
|
}
|
|
2279
1851
|
console.log();
|
|
2280
1852
|
});
|
|
2281
|
-
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) => {
|
|
2282
1854
|
const db = openDatabase();
|
|
2283
1855
|
ensurePricingSeeded(db);
|
|
2284
1856
|
if (opts.force) {
|
|
@@ -2286,9 +1858,8 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
|
|
|
2286
1858
|
if (opts.verbose)
|
|
2287
1859
|
console.log(chalk4.dim("Cleared ingest cache"));
|
|
2288
1860
|
}
|
|
2289
|
-
const anySpecific = opts.claude || opts.
|
|
1861
|
+
const anySpecific = opts.claude || opts.codex || opts.gemini;
|
|
2290
1862
|
const doClaude = opts.claude || !anySpecific;
|
|
2291
|
-
const doTakumi = opts.takumi || !anySpecific;
|
|
2292
1863
|
const doCodex = opts.codex || !anySpecific;
|
|
2293
1864
|
const doGemini = opts.gemini || !anySpecific;
|
|
2294
1865
|
if (doClaude) {
|
|
@@ -2296,11 +1867,6 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
|
|
|
2296
1867
|
const r = await ingestClaude(db, opts.verbose);
|
|
2297
1868
|
console.log(chalk4.green(`\u2713 ${r.files} files, ${r.requests} requests, ${r.sessions} sessions`));
|
|
2298
1869
|
}
|
|
2299
|
-
if (doTakumi) {
|
|
2300
|
-
process.stdout.write(chalk4.cyan("\u2192 Ingesting Takumi sessions... "));
|
|
2301
|
-
const r = await ingestTakumi(db, opts.verbose);
|
|
2302
|
-
console.log(chalk4.green(`\u2713 ${r.files} files, ${r.requests} requests, ${r.sessions} sessions`));
|
|
2303
|
-
}
|
|
2304
1870
|
if (doCodex) {
|
|
2305
1871
|
process.stdout.write(chalk4.cyan("\u2192 Ingesting Codex sessions... "));
|
|
2306
1872
|
const r = await ingestCodex(db, opts.verbose);
|
|
@@ -2311,35 +1877,6 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
|
|
|
2311
1877
|
const r = await ingestGemini(db, opts.verbose);
|
|
2312
1878
|
console.log(chalk4.green(`\u2713 ${r.sessions} sessions`));
|
|
2313
1879
|
}
|
|
2314
|
-
if (opts.backfillMachine) {
|
|
2315
|
-
const machine = getMachineId();
|
|
2316
|
-
const reqCount = db.prepare(`UPDATE requests SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
|
|
2317
|
-
const sessCount = db.prepare(`UPDATE sessions SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
|
|
2318
|
-
console.log(chalk4.cyan(`\u2192 Backfilled machine_id='${machine}': ${reqCount.changes} requests, ${sessCount.changes} sessions`));
|
|
2319
|
-
}
|
|
2320
|
-
if (opts.recalculate) {
|
|
2321
|
-
const { computeCostFromDb: computeCostFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
|
|
2322
|
-
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();
|
|
2323
|
-
let fixed = 0;
|
|
2324
|
-
for (const r of zeroRows) {
|
|
2325
|
-
const cost = computeCostFromDb2(db, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, r.cache_create_tokens);
|
|
2326
|
-
if (cost > 0) {
|
|
2327
|
-
db.prepare(`UPDATE requests SET cost_usd = ? WHERE id = ?`).run(cost, r.id);
|
|
2328
|
-
fixed++;
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
if (fixed > 0) {
|
|
2332
|
-
const touchedSessions = new Set(zeroRows.map((r) => {
|
|
2333
|
-
const row = db.prepare(`SELECT session_id FROM requests WHERE id = ?`).get(r.id);
|
|
2334
|
-
return row?.session_id;
|
|
2335
|
-
}).filter(Boolean));
|
|
2336
|
-
const { rollupSession: rollupSession2 } = await Promise.resolve().then(() => (init_database(), exports_database));
|
|
2337
|
-
for (const sid of touchedSessions) {
|
|
2338
|
-
rollupSession2(db, sid);
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
console.log(chalk4.cyan(`\u2192 Recalculated: ${fixed}/${zeroRows.length} zero-cost requests now have pricing`));
|
|
2342
|
-
}
|
|
2343
1880
|
try {
|
|
2344
1881
|
const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
2345
1882
|
await checkAndFireWebhooks2(db);
|
|
@@ -2359,14 +1896,13 @@ program.command("month").description("Cost summary for this month").action(async
|
|
|
2359
1896
|
await autoSync();
|
|
2360
1897
|
printSummary("This Month", "month");
|
|
2361
1898
|
});
|
|
2362
|
-
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) => {
|
|
2363
1900
|
await autoSync();
|
|
2364
1901
|
const db = openDatabase();
|
|
2365
1902
|
const sinceDate = opts.since ? parseSinceDate(opts.since) : undefined;
|
|
2366
1903
|
let sessions = querySessions(db, {
|
|
2367
1904
|
agent: opts.agent,
|
|
2368
1905
|
project: opts.project,
|
|
2369
|
-
machine: opts.machine,
|
|
2370
1906
|
limit: Number(opts.limit ?? 20),
|
|
2371
1907
|
since: sinceDate,
|
|
2372
1908
|
search: opts.search
|
|
@@ -2815,29 +2351,6 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
2815
2351
|
}
|
|
2816
2352
|
console.log();
|
|
2817
2353
|
});
|
|
2818
|
-
program.command("machines").description("List all machines that have synced data").action(async () => {
|
|
2819
|
-
await autoSync();
|
|
2820
|
-
const db = openDatabase();
|
|
2821
|
-
const machines = listMachines(db);
|
|
2822
|
-
const current = getMachineId();
|
|
2823
|
-
if (machines.length === 0) {
|
|
2824
|
-
console.log(chalk4.yellow(`No machine data yet. Current machine: ${current}`));
|
|
2825
|
-
return;
|
|
2826
|
-
}
|
|
2827
|
-
console.log();
|
|
2828
|
-
console.log(chalk4.bold.cyan(" Machines"));
|
|
2829
|
-
console.log();
|
|
2830
|
-
printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"], machines.map((m) => [
|
|
2831
|
-
m.machine_id === current ? chalk4.green(`${m.machine_id} (this)`) : chalk4.white(m.machine_id),
|
|
2832
|
-
fmtCount(m.sessions),
|
|
2833
|
-
fmtCount(m.requests),
|
|
2834
|
-
fmt2(m.total_cost_usd),
|
|
2835
|
-
chalk4.dim(m.last_active?.substring(0, 16) ?? "\u2014")
|
|
2836
|
-
]));
|
|
2837
|
-
console.log(`
|
|
2838
|
-
${chalk4.dim("Current machine:")} ${chalk4.bold(current)}`);
|
|
2839
|
-
console.log();
|
|
2840
|
-
});
|
|
2841
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) => {
|
|
2842
2355
|
await autoSync();
|
|
2843
2356
|
const db = openDatabase();
|
|
@@ -3105,144 +2618,5 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
|
|
|
3105
2618
|
process.exit(1);
|
|
3106
2619
|
}
|
|
3107
2620
|
});
|
|
3108
|
-
var CLOUD_RDS_HOST = "hasnaxyz-prod-opensource.c4limg0qgqvk.us-east-1.rds.amazonaws.com";
|
|
3109
|
-
var CLOUD_RDS_USER = "hasna_admin";
|
|
3110
|
-
var CLOUD_RDS_DB = "economy";
|
|
3111
|
-
var CLOUD_TABLES = ["requests", "sessions", "projects", "budgets", "goals", "model_pricing"];
|
|
3112
|
-
async function getCloudPassword() {
|
|
3113
|
-
if (process.env["ECONOMY_PG_PASSWORD"])
|
|
3114
|
-
return process.env["ECONOMY_PG_PASSWORD"];
|
|
3115
|
-
const { execSync: exec } = await import("child_process");
|
|
3116
|
-
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" });
|
|
3117
|
-
return JSON.parse(secretJson).password;
|
|
3118
|
-
}
|
|
3119
|
-
async function getCloudPg() {
|
|
3120
|
-
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
3121
|
-
const pw = encodeURIComponent(await getCloudPassword());
|
|
3122
|
-
return new PgAdapterAsync(`postgresql://${CLOUD_RDS_USER}:${pw}@${CLOUD_RDS_HOST}:5432/${CLOUD_RDS_DB}?sslmode=require`);
|
|
3123
|
-
}
|
|
3124
|
-
var cloudCmd = program.command("cloud").description("Cross-machine sync via cloud PostgreSQL");
|
|
3125
|
-
cloudCmd.command("push").description("Push local economy data to cloud PostgreSQL").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
|
|
3126
|
-
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
3127
|
-
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
3128
|
-
const cloud = await getCloudPg();
|
|
3129
|
-
const local = new SqliteAdapter(getDbPath());
|
|
3130
|
-
process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
|
|
3131
|
-
for (const sql of PG_MIGRATIONS2) {
|
|
3132
|
-
await cloud.run(sql);
|
|
3133
|
-
}
|
|
3134
|
-
console.log(chalk4.green("\u2713"));
|
|
3135
|
-
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : CLOUD_TABLES;
|
|
3136
|
-
process.stdout.write(chalk4.cyan(`\u2192 Pushing ${tableList.join(", ")}... `));
|
|
3137
|
-
const results = await syncPush(local, cloud, { tables: tableList });
|
|
3138
|
-
const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3139
|
-
console.log(chalk4.green(`\u2713 ${totalRows} rows across ${tableList.length} tables`));
|
|
3140
|
-
local.close();
|
|
3141
|
-
await cloud.close();
|
|
3142
|
-
console.log(chalk4.bold.green(`
|
|
3143
|
-
\u2713 Push complete from ${getMachineId()}`));
|
|
3144
|
-
});
|
|
3145
|
-
cloudCmd.command("pull").description("Pull cloud PostgreSQL data to local").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
|
|
3146
|
-
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
3147
|
-
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
3148
|
-
const cloud = await getCloudPg();
|
|
3149
|
-
const local = new SqliteAdapter(getDbPath());
|
|
3150
|
-
process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
|
|
3151
|
-
for (const sql of PG_MIGRATIONS2) {
|
|
3152
|
-
await cloud.run(sql);
|
|
3153
|
-
}
|
|
3154
|
-
console.log(chalk4.green("\u2713"));
|
|
3155
|
-
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : CLOUD_TABLES;
|
|
3156
|
-
process.stdout.write(chalk4.cyan(`\u2192 Pulling ${tableList.join(", ")}... `));
|
|
3157
|
-
const results = await syncPull(cloud, local, { tables: tableList });
|
|
3158
|
-
const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3159
|
-
console.log(chalk4.green(`\u2713 ${totalRows} rows across ${tableList.length} tables`));
|
|
3160
|
-
local.close();
|
|
3161
|
-
await cloud.close();
|
|
3162
|
-
console.log(chalk4.bold.green(`
|
|
3163
|
-
\u2713 Pull complete to ${getMachineId()}`));
|
|
3164
|
-
});
|
|
3165
|
-
cloudCmd.command("sync").description("Full sync: ingest local \u2192 push to cloud \u2192 pull from cloud").action(async () => {
|
|
3166
|
-
console.log(chalk4.bold.cyan(` Cloud Sync \u2014 ${getMachineId()}
|
|
3167
|
-
`));
|
|
3168
|
-
process.stdout.write(chalk4.cyan("\u2192 Ingesting local data... "));
|
|
3169
|
-
await autoSync();
|
|
3170
|
-
console.log(chalk4.green("\u2713"));
|
|
3171
|
-
const { syncPush, syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
3172
|
-
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
3173
|
-
const cloud = await getCloudPg();
|
|
3174
|
-
const local = new SqliteAdapter(getDbPath());
|
|
3175
|
-
for (const sql of PG_MIGRATIONS2) {
|
|
3176
|
-
await cloud.run(sql);
|
|
3177
|
-
}
|
|
3178
|
-
process.stdout.write(chalk4.cyan("\u2192 Pushing local \u2192 cloud... "));
|
|
3179
|
-
const pushResults = await syncPush(local, cloud, { tables: CLOUD_TABLES });
|
|
3180
|
-
console.log(chalk4.green(`\u2713 ${pushResults.reduce((s, r) => s + r.rowsWritten, 0)} rows`));
|
|
3181
|
-
process.stdout.write(chalk4.cyan("\u2192 Pulling cloud \u2192 local... "));
|
|
3182
|
-
const pullResults = await syncPull(cloud, local, { tables: CLOUD_TABLES });
|
|
3183
|
-
console.log(chalk4.green(`\u2713 ${pullResults.reduce((s, r) => s + r.rowsWritten, 0)} rows`));
|
|
3184
|
-
local.close();
|
|
3185
|
-
await cloud.close();
|
|
3186
|
-
console.log(chalk4.bold.green(`
|
|
3187
|
-
\u2713 Cloud sync complete`));
|
|
3188
|
-
});
|
|
3189
|
-
cloudCmd.command("status").description("Check cloud connection status").action(async () => {
|
|
3190
|
-
console.log();
|
|
3191
|
-
console.log(` Machine: ${chalk4.white(getMachineId())}`);
|
|
3192
|
-
console.log(` RDS Host: ${chalk4.white(CLOUD_RDS_HOST)}`);
|
|
3193
|
-
console.log(` Database: ${chalk4.white(CLOUD_RDS_DB)}`);
|
|
3194
|
-
try {
|
|
3195
|
-
const cloud = await getCloudPg();
|
|
3196
|
-
await cloud.get("SELECT 1 as ok");
|
|
3197
|
-
const tables = await cloud.all("SELECT tablename FROM pg_tables WHERE schemaname = 'public'");
|
|
3198
|
-
console.log(` PostgreSQL: ${chalk4.green("connected")}`);
|
|
3199
|
-
console.log(` Tables: ${chalk4.white(tables.map((t) => t.tablename).join(", ") || "(none)")}`);
|
|
3200
|
-
await cloud.close();
|
|
3201
|
-
} catch (err2) {
|
|
3202
|
-
console.log(` PostgreSQL: ${chalk4.red(`failed \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`)}`);
|
|
3203
|
-
}
|
|
3204
|
-
console.log();
|
|
3205
|
-
});
|
|
3206
|
-
var billingCmd = program.command("billing").description("Pull actual billing from provider admin APIs (ground truth)");
|
|
3207
|
-
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) => {
|
|
3208
|
-
const db = openDatabase();
|
|
3209
|
-
const days = Number(opts.days ?? 31);
|
|
3210
|
-
const doBoth = !opts.anthropic && !opts.openai;
|
|
3211
|
-
if (opts.anthropic || doBoth) {
|
|
3212
|
-
process.stdout.write(chalk4.cyan("\u2192 Syncing Anthropic billing... "));
|
|
3213
|
-
try {
|
|
3214
|
-
const r = await syncAnthropicBilling(db, { days });
|
|
3215
|
-
console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
|
|
3216
|
-
} catch (e) {
|
|
3217
|
-
console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3220
|
-
if (opts.openai || doBoth) {
|
|
3221
|
-
process.stdout.write(chalk4.cyan("\u2192 Syncing OpenAI billing... "));
|
|
3222
|
-
try {
|
|
3223
|
-
const r = await syncOpenAIBilling(db, { days });
|
|
3224
|
-
console.log(chalk4.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
|
|
3225
|
-
} catch (e) {
|
|
3226
|
-
console.log(chalk4.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
});
|
|
3230
|
-
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) => {
|
|
3231
|
-
const db = openDatabase();
|
|
3232
|
-
const period = opts.period ?? "month";
|
|
3233
|
-
const actual = queryBillingSummary(db, period);
|
|
3234
|
-
const estimated = querySummary(db, period);
|
|
3235
|
-
console.log();
|
|
3236
|
-
console.log(chalk4.bold.cyan(` Billing ${period} (actual from admin APIs)
|
|
3237
|
-
`));
|
|
3238
|
-
printTable(["Provider", "Actual (billed)"], Object.entries(actual.by_provider).map(([p, c]) => [chalk4.white(p), fmt2(c)]));
|
|
3239
|
-
console.log();
|
|
3240
|
-
console.log(` ${chalk4.bold("Actual total:")} ${fmt2(actual.total_usd)}`);
|
|
3241
|
-
console.log(` ${chalk4.dim("Our estimate:")} ${fmt2(estimated.total_usd)}`);
|
|
3242
|
-
const diff = estimated.total_usd - actual.total_usd;
|
|
3243
|
-
const pct = actual.total_usd > 0 ? diff / actual.total_usd * 100 : 0;
|
|
3244
|
-
console.log(` ${chalk4.dim("Difference:")} ${fmt2(Math.abs(diff))} (${diff >= 0 ? "+" : ""}${pct.toFixed(1)}%)`);
|
|
3245
|
-
console.log();
|
|
3246
|
-
});
|
|
3247
2621
|
registerBrainsCommand(program);
|
|
3248
2622
|
program.parse();
|