@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/mcp/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
4
5
|
var __returnValue = (v) => v;
|
|
5
6
|
function __exportSetter(name, newValue) {
|
|
6
7
|
this[name] = __returnValue.bind(null, newValue);
|
|
@@ -15,6 +16,7 @@ var __export = (target, all) => {
|
|
|
15
16
|
});
|
|
16
17
|
};
|
|
17
18
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
19
|
+
var __require = import.meta.require;
|
|
18
20
|
|
|
19
21
|
// src/lib/pricing.ts
|
|
20
22
|
var exports_pricing = {};
|
|
@@ -78,7 +80,6 @@ var DEFAULT_PRICING;
|
|
|
78
80
|
var init_pricing = __esm(() => {
|
|
79
81
|
init_database();
|
|
80
82
|
DEFAULT_PRICING = {
|
|
81
|
-
"claude-opus-4-7": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
82
83
|
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
83
84
|
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
84
85
|
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
@@ -89,55 +90,28 @@ var init_pricing = __esm(() => {
|
|
|
89
90
|
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
|
|
90
91
|
"claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
91
92
|
"claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
|
|
92
|
-
"gemini-3.1-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0.31, cacheWritePer1M: 0 },
|
|
93
|
-
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
94
|
-
"gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
95
93
|
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
94
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
96
95
|
"gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
97
96
|
"gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
98
|
-
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15, cacheReadPer1M: 0.25, cacheWritePer1M: 0 },
|
|
99
|
-
"gpt-5.4-pro": { inputPer1M: 30, outputPer1M: 180, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
100
|
-
"gpt-5.4-mini": { inputPer1M: 0.75, outputPer1M: 4.5, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
101
97
|
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
102
|
-
"gpt-5.3-chat": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
103
98
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
104
99
|
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
105
|
-
"gpt-5-mini": { inputPer1M: 0.3, outputPer1M: 1.2, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
106
|
-
"gpt-5.2": { inputPer1M: 2, outputPer1M: 8, cacheReadPer1M: 0.5, cacheWritePer1M: 0 },
|
|
107
100
|
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
108
101
|
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
109
102
|
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
110
103
|
"o1-mini": { inputPer1M: 3, outputPer1M: 12, cacheReadPer1M: 1.5, cacheWritePer1M: 0 },
|
|
111
104
|
o3: { inputPer1M: 10, outputPer1M: 40, cacheReadPer1M: 2.5, cacheWritePer1M: 0 },
|
|
112
105
|
"o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.55, cacheWritePer1M: 0 },
|
|
113
|
-
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
|
|
114
|
-
"qwen3.6-plus": { inputPer1M: 0.8, outputPer1M: 2, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
115
|
-
"qwen3.6": { inputPer1M: 0.3, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
116
|
-
"minimax-m2.7": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
117
|
-
"minimax-m2.7-highspeed": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
118
|
-
"minimax-m1": { inputPer1M: 0.2, outputPer1M: 1.1, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
119
|
-
"grok-3": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
120
|
-
"grok-3-mini": { inputPer1M: 0.3, outputPer1M: 0.5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
121
|
-
"glm-5.1": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
122
|
-
"glm-5": { inputPer1M: 0.7, outputPer1M: 0.7, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
123
|
-
"kimi-k2": { inputPer1M: 0.6, outputPer1M: 0.6, cacheReadPer1M: 0, cacheWritePer1M: 0 }
|
|
106
|
+
"o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4, cacheReadPer1M: 0.275, cacheWritePer1M: 0 }
|
|
124
107
|
};
|
|
125
108
|
});
|
|
126
109
|
|
|
127
110
|
// src/db/database.ts
|
|
128
111
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
129
112
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
130
|
-
import { hostname } from "os";
|
|
131
113
|
import { homedir } from "os";
|
|
132
114
|
import { join } from "path";
|
|
133
|
-
function getMachineId() {
|
|
134
|
-
if (process.env["ECONOMY_MACHINE_ID"])
|
|
135
|
-
return process.env["ECONOMY_MACHINE_ID"];
|
|
136
|
-
const h = hostname().toLowerCase();
|
|
137
|
-
if (h.startsWith("spark") || h.startsWith("apple"))
|
|
138
|
-
return h.split(".")[0];
|
|
139
|
-
return h.split(".")[0];
|
|
140
|
-
}
|
|
141
115
|
function getDataDir() {
|
|
142
116
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
143
117
|
const newDir = join(home, ".hasna", "economy");
|
|
@@ -170,7 +144,6 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
170
144
|
}
|
|
171
145
|
const db = new Database(path);
|
|
172
146
|
db.exec("PRAGMA journal_mode = WAL");
|
|
173
|
-
db.exec("PRAGMA busy_timeout = 5000");
|
|
174
147
|
db.exec("PRAGMA foreign_keys = ON");
|
|
175
148
|
initSchema(db);
|
|
176
149
|
if (!skipSeed) {
|
|
@@ -192,8 +165,7 @@ function initSchema(db) {
|
|
|
192
165
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
193
166
|
duration_ms INTEGER DEFAULT 0,
|
|
194
167
|
timestamp TEXT NOT NULL,
|
|
195
|
-
source_request_id TEXT
|
|
196
|
-
machine_id TEXT DEFAULT ''
|
|
168
|
+
source_request_id TEXT
|
|
197
169
|
);
|
|
198
170
|
|
|
199
171
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -205,8 +177,7 @@ function initSchema(db) {
|
|
|
205
177
|
ended_at TEXT,
|
|
206
178
|
total_cost_usd REAL DEFAULT 0,
|
|
207
179
|
total_tokens INTEGER DEFAULT 0,
|
|
208
|
-
request_count INTEGER DEFAULT 0
|
|
209
|
-
machine_id TEXT DEFAULT ''
|
|
180
|
+
request_count INTEGER DEFAULT 0
|
|
210
181
|
);
|
|
211
182
|
|
|
212
183
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -271,27 +242,6 @@ function initSchema(db) {
|
|
|
271
242
|
machine_id TEXT,
|
|
272
243
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
273
244
|
);
|
|
274
|
-
|
|
275
|
-
CREATE TABLE IF NOT EXISTS billing_daily (
|
|
276
|
-
date TEXT NOT NULL,
|
|
277
|
-
provider TEXT NOT NULL,
|
|
278
|
-
description TEXT DEFAULT '',
|
|
279
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
280
|
-
updated_at TEXT NOT NULL,
|
|
281
|
-
PRIMARY KEY (date, provider, description)
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date);
|
|
285
|
-
CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider);
|
|
286
|
-
`);
|
|
287
|
-
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
288
|
-
if (!cols.some((c) => c.name === "machine_id")) {
|
|
289
|
-
db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
290
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
291
|
-
}
|
|
292
|
-
db.exec(`
|
|
293
|
-
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
294
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
295
245
|
`);
|
|
296
246
|
}
|
|
297
247
|
function periodWhere(period) {
|
|
@@ -301,11 +251,11 @@ function periodWhere(period) {
|
|
|
301
251
|
case "yesterday":
|
|
302
252
|
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
303
253
|
case "week":
|
|
304
|
-
return `timestamp >= DATE('now', '
|
|
254
|
+
return `timestamp >= DATE('now', '-7 days')`;
|
|
305
255
|
case "month":
|
|
306
|
-
return `timestamp >= DATE('now', '
|
|
256
|
+
return `timestamp >= DATE('now', '-30 days')`;
|
|
307
257
|
case "year":
|
|
308
|
-
return `timestamp >= DATE('now', '
|
|
258
|
+
return `timestamp >= DATE('now', '-365 days')`;
|
|
309
259
|
case "all":
|
|
310
260
|
return "1=1";
|
|
311
261
|
}
|
|
@@ -317,11 +267,11 @@ function sessionPeriodWhere(period) {
|
|
|
317
267
|
case "yesterday":
|
|
318
268
|
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
319
269
|
case "week":
|
|
320
|
-
return `started_at >= DATE('now', '
|
|
270
|
+
return `started_at >= DATE('now', '-7 days')`;
|
|
321
271
|
case "month":
|
|
322
|
-
return `started_at >= DATE('now', '
|
|
272
|
+
return `started_at >= DATE('now', '-30 days')`;
|
|
323
273
|
case "year":
|
|
324
|
-
return `started_at >= DATE('now', '
|
|
274
|
+
return `started_at >= DATE('now', '-365 days')`;
|
|
325
275
|
case "all":
|
|
326
276
|
return "1=1";
|
|
327
277
|
}
|
|
@@ -331,17 +281,17 @@ function upsertRequest(db, req) {
|
|
|
331
281
|
INSERT OR REPLACE INTO requests
|
|
332
282
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
333
283
|
cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
|
|
334
|
-
timestamp, source_request_id
|
|
335
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
336
|
-
`).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
|
|
284
|
+
timestamp, source_request_id)
|
|
285
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
286
|
+
`).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);
|
|
337
287
|
}
|
|
338
288
|
function upsertSession(db, session) {
|
|
339
289
|
db.prepare(`
|
|
340
290
|
INSERT OR REPLACE INTO sessions
|
|
341
291
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
342
|
-
total_cost_usd, total_tokens, request_count
|
|
343
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
344
|
-
`).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
|
|
292
|
+
total_cost_usd, total_tokens, request_count)
|
|
293
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
294
|
+
`).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);
|
|
345
295
|
}
|
|
346
296
|
function rollupSession(db, sessionId) {
|
|
347
297
|
db.prepare(`
|
|
@@ -371,10 +321,6 @@ function querySessions(db, filter = {}) {
|
|
|
371
321
|
conditions.push("started_at >= ?");
|
|
372
322
|
params.push(filter.since);
|
|
373
323
|
}
|
|
374
|
-
if (filter.machine) {
|
|
375
|
-
conditions.push("machine_id = ?");
|
|
376
|
-
params.push(filter.machine);
|
|
377
|
-
}
|
|
378
324
|
if (filter.search) {
|
|
379
325
|
const q = `%${filter.search}%`;
|
|
380
326
|
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
@@ -393,25 +339,24 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
393
339
|
}
|
|
394
340
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
395
341
|
}
|
|
396
|
-
function querySummary(db, period
|
|
342
|
+
function querySummary(db, period) {
|
|
397
343
|
const rWhere = periodWhere(period);
|
|
398
344
|
const sWhere = sessionPeriodWhere(period);
|
|
399
|
-
const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
400
345
|
const r = db.prepare(`
|
|
401
346
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
402
347
|
COUNT(*) as requests,
|
|
403
348
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
404
|
-
FROM requests WHERE ${rWhere}
|
|
349
|
+
FROM requests WHERE ${rWhere}
|
|
405
350
|
`).get();
|
|
406
351
|
const codexTotals = db.prepare(`
|
|
407
352
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
408
353
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
409
354
|
COUNT(*) as sessions
|
|
410
355
|
FROM sessions
|
|
411
|
-
WHERE ${sWhere}
|
|
356
|
+
WHERE ${sWhere}
|
|
412
357
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
413
358
|
`).get();
|
|
414
|
-
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}
|
|
359
|
+
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
|
|
415
360
|
return {
|
|
416
361
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
417
362
|
requests: r.requests,
|
|
@@ -433,39 +378,19 @@ function queryModelBreakdown(db) {
|
|
|
433
378
|
}
|
|
434
379
|
function queryProjectBreakdown(db) {
|
|
435
380
|
return db.prepare(`
|
|
436
|
-
WITH labeled AS (
|
|
437
|
-
SELECT
|
|
438
|
-
s.id,
|
|
439
|
-
s.project_path,
|
|
440
|
-
s.total_cost_usd,
|
|
441
|
-
s.started_at,
|
|
442
|
-
COALESCE(
|
|
443
|
-
NULLIF(s.project_name, ''),
|
|
444
|
-
CASE
|
|
445
|
-
WHEN s.project_path LIKE '%/%'
|
|
446
|
-
THEN substr(s.project_path, length(rtrim(s.project_path, replace(s.project_path, '/', ''))) + 1)
|
|
447
|
-
ELSE s.project_path
|
|
448
|
-
END
|
|
449
|
-
) as label
|
|
450
|
-
FROM sessions s
|
|
451
|
-
WHERE s.project_path != '' OR s.project_name != ''
|
|
452
|
-
)
|
|
453
381
|
SELECT
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
COUNT(DISTINCT
|
|
457
|
-
|
|
458
|
-
COALESCE(
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
MAX(l.started_at) as last_active
|
|
467
|
-
FROM labeled l
|
|
468
|
-
GROUP BY l.label
|
|
382
|
+
s.project_path,
|
|
383
|
+
COALESCE(p.name, s.project_name) as project_name,
|
|
384
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
385
|
+
COUNT(r.id) as requests,
|
|
386
|
+
COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
|
|
387
|
+
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
388
|
+
MAX(s.started_at) as last_active
|
|
389
|
+
FROM sessions s
|
|
390
|
+
LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
|
|
391
|
+
LEFT JOIN requests r ON r.session_id = s.id
|
|
392
|
+
WHERE s.project_path != '' OR s.project_name != ''
|
|
393
|
+
GROUP BY s.project_path
|
|
469
394
|
ORDER BY cost_usd DESC
|
|
470
395
|
`).all();
|
|
471
396
|
}
|
|
@@ -554,20 +479,6 @@ function getIngestState(db, source, key) {
|
|
|
554
479
|
function setIngestState(db, source, key, value) {
|
|
555
480
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
556
481
|
}
|
|
557
|
-
function listMachines(db) {
|
|
558
|
-
return db.prepare(`
|
|
559
|
-
SELECT
|
|
560
|
-
s.machine_id,
|
|
561
|
-
COUNT(DISTINCT s.id) as sessions,
|
|
562
|
-
COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
|
|
563
|
-
COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
|
|
564
|
-
MAX(s.started_at) as last_active
|
|
565
|
-
FROM sessions s
|
|
566
|
-
WHERE s.machine_id != ''
|
|
567
|
-
GROUP BY s.machine_id
|
|
568
|
-
ORDER BY total_cost_usd DESC
|
|
569
|
-
`).all();
|
|
570
|
-
}
|
|
571
482
|
function upsertModelPricing(db, p) {
|
|
572
483
|
db.prepare(`
|
|
573
484
|
INSERT OR REPLACE INTO model_pricing
|
|
@@ -579,11 +490,11 @@ function getModelPricing(db, model) {
|
|
|
579
490
|
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
580
491
|
}
|
|
581
492
|
function seedModelPricing(db, defaults) {
|
|
582
|
-
const existing =
|
|
493
|
+
const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
|
|
494
|
+
if (existing.count > 0)
|
|
495
|
+
return;
|
|
583
496
|
const now = new Date().toISOString();
|
|
584
497
|
for (const [model, p] of Object.entries(defaults)) {
|
|
585
|
-
if (existing.has(model))
|
|
586
|
-
continue;
|
|
587
498
|
upsertModelPricing(db, {
|
|
588
499
|
model,
|
|
589
500
|
input_per_1m: p.inputPer1M,
|
|
@@ -596,102 +507,81 @@ function seedModelPricing(db, defaults) {
|
|
|
596
507
|
}
|
|
597
508
|
var init_database = () => {};
|
|
598
509
|
|
|
510
|
+
// package.json
|
|
511
|
+
var require_package = __commonJS((exports, module) => {
|
|
512
|
+
module.exports = {
|
|
513
|
+
name: "@hasna/economy",
|
|
514
|
+
version: "0.2.10",
|
|
515
|
+
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API + web dashboard for Claude Code, Codex, and Gemini",
|
|
516
|
+
type: "module",
|
|
517
|
+
main: "dist/index.js",
|
|
518
|
+
types: "dist/index.d.ts",
|
|
519
|
+
bin: {
|
|
520
|
+
economy: "dist/cli/index.js",
|
|
521
|
+
"economy-mcp": "dist/mcp/index.js",
|
|
522
|
+
"economy-serve": "dist/server/index.js"
|
|
523
|
+
},
|
|
524
|
+
exports: {
|
|
525
|
+
".": {
|
|
526
|
+
types: "./dist/index.d.ts",
|
|
527
|
+
import: "./dist/index.js"
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
files: [
|
|
531
|
+
"dist",
|
|
532
|
+
"LICENSE"
|
|
533
|
+
],
|
|
534
|
+
scripts: {
|
|
535
|
+
build: "cd dashboard && bun run build && cd .. && bun build src/cli/index.ts --outdir dist/cli --target bun --packages external && bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/index.ts --outdir dist --target bun --packages external && tsc --emitDeclarationOnly --outDir dist",
|
|
536
|
+
"build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
|
|
537
|
+
"build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
|
|
538
|
+
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
|
|
539
|
+
"build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
|
|
540
|
+
"build:dashboard": "cd dashboard && bun run build",
|
|
541
|
+
typecheck: "tsc --noEmit",
|
|
542
|
+
test: "bun test",
|
|
543
|
+
"dev:cli": "bun run src/cli/index.ts",
|
|
544
|
+
"dev:mcp": "bun run src/mcp/index.ts",
|
|
545
|
+
"dev:serve": "bun run src/server/index.ts",
|
|
546
|
+
postinstall: "mkdir -p $HOME/.hasna/economy/training 2>/dev/null || true"
|
|
547
|
+
},
|
|
548
|
+
keywords: [
|
|
549
|
+
"economy",
|
|
550
|
+
"cost",
|
|
551
|
+
"ai",
|
|
552
|
+
"claude",
|
|
553
|
+
"codex",
|
|
554
|
+
"gemini",
|
|
555
|
+
"mcp",
|
|
556
|
+
"cli",
|
|
557
|
+
"budget",
|
|
558
|
+
"tracking"
|
|
559
|
+
],
|
|
560
|
+
author: "hasna",
|
|
561
|
+
license: "Apache-2.0",
|
|
562
|
+
publishConfig: {
|
|
563
|
+
registry: "https://registry.npmjs.org",
|
|
564
|
+
access: "public"
|
|
565
|
+
},
|
|
566
|
+
dependencies: {
|
|
567
|
+
"@hasna/cloud": "^0.1.0",
|
|
568
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
569
|
+
chalk: "^5.4.1",
|
|
570
|
+
commander: "^13.1.0"
|
|
571
|
+
},
|
|
572
|
+
devDependencies: {
|
|
573
|
+
"@types/bun": "latest",
|
|
574
|
+
"bun-types": "latest",
|
|
575
|
+
typescript: "^5.7.2"
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
});
|
|
579
|
+
|
|
599
580
|
// src/mcp/index.ts
|
|
600
581
|
init_database();
|
|
601
|
-
import {
|
|
602
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
582
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
603
583
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
604
|
-
import {
|
|
605
|
-
import { z } from "zod";
|
|
606
|
-
|
|
607
|
-
// src/db/pg-migrations.ts
|
|
608
|
-
var PG_MIGRATIONS = [
|
|
609
|
-
`CREATE TABLE IF NOT EXISTS requests (
|
|
610
|
-
id TEXT PRIMARY KEY,
|
|
611
|
-
agent TEXT NOT NULL,
|
|
612
|
-
session_id TEXT NOT NULL,
|
|
613
|
-
model TEXT NOT NULL,
|
|
614
|
-
input_tokens INTEGER DEFAULT 0,
|
|
615
|
-
output_tokens INTEGER DEFAULT 0,
|
|
616
|
-
cache_read_tokens INTEGER DEFAULT 0,
|
|
617
|
-
cache_create_tokens INTEGER DEFAULT 0,
|
|
618
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
619
|
-
duration_ms INTEGER DEFAULT 0,
|
|
620
|
-
timestamp TEXT NOT NULL,
|
|
621
|
-
source_request_id TEXT,
|
|
622
|
-
machine_id TEXT DEFAULT ''
|
|
623
|
-
)`,
|
|
624
|
-
`CREATE TABLE IF NOT EXISTS sessions (
|
|
625
|
-
id TEXT PRIMARY KEY,
|
|
626
|
-
agent TEXT NOT NULL,
|
|
627
|
-
project_path TEXT DEFAULT '',
|
|
628
|
-
project_name TEXT DEFAULT '',
|
|
629
|
-
started_at TEXT NOT NULL,
|
|
630
|
-
ended_at TEXT,
|
|
631
|
-
total_cost_usd REAL DEFAULT 0,
|
|
632
|
-
total_tokens INTEGER DEFAULT 0,
|
|
633
|
-
request_count INTEGER DEFAULT 0,
|
|
634
|
-
machine_id TEXT DEFAULT ''
|
|
635
|
-
)`,
|
|
636
|
-
`CREATE TABLE IF NOT EXISTS projects (
|
|
637
|
-
id TEXT PRIMARY KEY,
|
|
638
|
-
path TEXT UNIQUE NOT NULL,
|
|
639
|
-
name TEXT NOT NULL,
|
|
640
|
-
description TEXT,
|
|
641
|
-
tags TEXT DEFAULT '[]',
|
|
642
|
-
created_at TEXT NOT NULL
|
|
643
|
-
)`,
|
|
644
|
-
`CREATE TABLE IF NOT EXISTS budgets (
|
|
645
|
-
id TEXT PRIMARY KEY,
|
|
646
|
-
project_path TEXT,
|
|
647
|
-
agent TEXT,
|
|
648
|
-
period TEXT NOT NULL,
|
|
649
|
-
limit_usd REAL NOT NULL,
|
|
650
|
-
alert_at_percent INTEGER DEFAULT 80,
|
|
651
|
-
created_at TEXT NOT NULL,
|
|
652
|
-
updated_at TEXT NOT NULL
|
|
653
|
-
)`,
|
|
654
|
-
`CREATE TABLE IF NOT EXISTS goals (
|
|
655
|
-
id TEXT PRIMARY KEY,
|
|
656
|
-
period TEXT NOT NULL,
|
|
657
|
-
project_path TEXT,
|
|
658
|
-
agent TEXT,
|
|
659
|
-
limit_usd REAL NOT NULL,
|
|
660
|
-
created_at TEXT NOT NULL,
|
|
661
|
-
updated_at TEXT NOT NULL
|
|
662
|
-
)`,
|
|
663
|
-
`CREATE TABLE IF NOT EXISTS ingest_state (
|
|
664
|
-
source TEXT NOT NULL,
|
|
665
|
-
key TEXT NOT NULL,
|
|
666
|
-
value TEXT NOT NULL,
|
|
667
|
-
PRIMARY KEY (source, key)
|
|
668
|
-
)`,
|
|
669
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
670
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
671
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
672
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
673
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
674
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
675
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
676
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
677
|
-
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
678
|
-
model TEXT PRIMARY KEY,
|
|
679
|
-
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
680
|
-
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
681
|
-
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
682
|
-
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
683
|
-
updated_at TEXT NOT NULL
|
|
684
|
-
)`,
|
|
685
|
-
`CREATE TABLE IF NOT EXISTS feedback (
|
|
686
|
-
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
687
|
-
message TEXT NOT NULL,
|
|
688
|
-
email TEXT,
|
|
689
|
-
category TEXT DEFAULT 'general',
|
|
690
|
-
version TEXT,
|
|
691
|
-
machine_id TEXT,
|
|
692
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
693
|
-
)`
|
|
694
|
-
];
|
|
584
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
695
585
|
|
|
696
586
|
// src/ingest/claude.ts
|
|
697
587
|
init_database();
|
|
@@ -702,8 +592,7 @@ import { join as join2, basename } from "path";
|
|
|
702
592
|
function autoDetectProject(cwd, projects) {
|
|
703
593
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
704
594
|
}
|
|
705
|
-
var
|
|
706
|
-
var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
|
|
595
|
+
var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
|
|
707
596
|
function dirNameToPath(dirName) {
|
|
708
597
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
709
598
|
}
|
|
@@ -723,36 +612,29 @@ function collectJsonlFiles(projectDir) {
|
|
|
723
612
|
return files;
|
|
724
613
|
}
|
|
725
614
|
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
async function ingestTakumi(db, verbose = false) {
|
|
729
|
-
return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
|
|
730
|
-
}
|
|
731
|
-
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
732
|
-
if (!existsSync2(projectsDir)) {
|
|
615
|
+
if (!existsSync2(PROJECTS_DIR)) {
|
|
733
616
|
if (verbose)
|
|
734
|
-
console.log(
|
|
617
|
+
console.log("Claude projects dir not found:", PROJECTS_DIR);
|
|
735
618
|
return { files: 0, requests: 0, sessions: 0 };
|
|
736
619
|
}
|
|
737
|
-
const machineId = getMachineId();
|
|
738
620
|
let totalFiles = 0;
|
|
739
621
|
let totalRequests = 0;
|
|
740
622
|
const touchedSessions = new Set;
|
|
741
623
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
742
|
-
const projectDirs = readdirSync2(
|
|
624
|
+
const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
743
625
|
for (const projectDirEntry of projectDirs) {
|
|
744
|
-
const projectDirPath = join2(
|
|
626
|
+
const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
|
|
745
627
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
746
628
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
747
629
|
for (const filePath of jsonlFiles) {
|
|
748
|
-
const stateKey = filePath.replace(
|
|
630
|
+
const stateKey = filePath.replace(PROJECTS_DIR, "");
|
|
749
631
|
let fileMtime = "0";
|
|
750
632
|
try {
|
|
751
633
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
752
634
|
} catch {
|
|
753
635
|
continue;
|
|
754
636
|
}
|
|
755
|
-
const processed = getIngestState(db,
|
|
637
|
+
const processed = getIngestState(db, "claude", stateKey);
|
|
756
638
|
if (processed === fileMtime)
|
|
757
639
|
continue;
|
|
758
640
|
let lines;
|
|
@@ -793,10 +675,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
793
675
|
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
794
676
|
continue;
|
|
795
677
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
796
|
-
const reqId =
|
|
678
|
+
const reqId = `claude-${sessionId}-${timestamp}`;
|
|
797
679
|
upsertRequest(db, {
|
|
798
680
|
id: reqId,
|
|
799
|
-
agent:
|
|
681
|
+
agent: "claude",
|
|
800
682
|
session_id: sessionId,
|
|
801
683
|
model,
|
|
802
684
|
input_tokens: inputTokens,
|
|
@@ -806,8 +688,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
806
688
|
cost_usd: costUsd,
|
|
807
689
|
duration_ms: 0,
|
|
808
690
|
timestamp,
|
|
809
|
-
source_request_id: reqId
|
|
810
|
-
machine_id: machineId
|
|
691
|
+
source_request_id: reqId
|
|
811
692
|
});
|
|
812
693
|
if (!touchedSessions.has(sessionId)) {
|
|
813
694
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
@@ -816,15 +697,14 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
816
697
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
817
698
|
const session = {
|
|
818
699
|
id: sessionId,
|
|
819
|
-
agent:
|
|
700
|
+
agent: "claude",
|
|
820
701
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
821
702
|
project_name: detectedProject ? detectedProject.name : "",
|
|
822
703
|
started_at: timestamp,
|
|
823
704
|
ended_at: null,
|
|
824
705
|
total_cost_usd: 0,
|
|
825
706
|
total_tokens: 0,
|
|
826
|
-
request_count: 0
|
|
827
|
-
machine_id: machineId
|
|
707
|
+
request_count: 0
|
|
828
708
|
};
|
|
829
709
|
upsertSession(db, session);
|
|
830
710
|
}
|
|
@@ -832,7 +712,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
832
712
|
}
|
|
833
713
|
totalRequests++;
|
|
834
714
|
}
|
|
835
|
-
setIngestState(db,
|
|
715
|
+
setIngestState(db, "claude", stateKey, fileMtime);
|
|
836
716
|
totalFiles++;
|
|
837
717
|
}
|
|
838
718
|
}
|
|
@@ -847,7 +727,7 @@ init_database();
|
|
|
847
727
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
848
728
|
import { homedir as homedir3 } from "os";
|
|
849
729
|
import { join as join3, basename as basename2 } from "path";
|
|
850
|
-
import { Database as
|
|
730
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
851
731
|
var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
852
732
|
var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
853
733
|
async function ingestCodex(db, verbose = false) {
|
|
@@ -856,11 +736,10 @@ async function ingestCodex(db, verbose = false) {
|
|
|
856
736
|
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
857
737
|
return { sessions: 0 };
|
|
858
738
|
}
|
|
859
|
-
const machineId = getMachineId();
|
|
860
739
|
let codexDb = null;
|
|
861
740
|
let ingested = 0;
|
|
862
741
|
try {
|
|
863
|
-
codexDb = new
|
|
742
|
+
codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
|
|
864
743
|
const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
|
|
865
744
|
for (const thread of threads) {
|
|
866
745
|
const stateKey = thread.id;
|
|
@@ -881,8 +760,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
881
760
|
ended_at: endedAt,
|
|
882
761
|
total_cost_usd: costUsd,
|
|
883
762
|
total_tokens: thread.tokens_used,
|
|
884
|
-
request_count: 1
|
|
885
|
-
machine_id: machineId
|
|
763
|
+
request_count: 1
|
|
886
764
|
});
|
|
887
765
|
setIngestState(db, "codex", stateKey, "done");
|
|
888
766
|
ingested++;
|
|
@@ -907,7 +785,6 @@ async function ingestGemini(db, verbose) {
|
|
|
907
785
|
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
908
786
|
return { sessions: 0 };
|
|
909
787
|
}
|
|
910
|
-
const machineId = getMachineId();
|
|
911
788
|
let totalSessions = 0;
|
|
912
789
|
const touchedSessions = new Set;
|
|
913
790
|
let projectHashDirs = [];
|
|
@@ -958,8 +835,7 @@ async function ingestGemini(db, verbose) {
|
|
|
958
835
|
ended_at: chatData.lastUpdated ?? null,
|
|
959
836
|
total_cost_usd: 0,
|
|
960
837
|
total_tokens: 0,
|
|
961
|
-
request_count: 0
|
|
962
|
-
machine_id: machineId
|
|
838
|
+
request_count: 0
|
|
963
839
|
};
|
|
964
840
|
upsertSession(db, session);
|
|
965
841
|
touchedSessions.add(sessionId);
|
|
@@ -974,93 +850,11 @@ async function ingestGemini(db, verbose) {
|
|
|
974
850
|
return { sessions: totalSessions };
|
|
975
851
|
}
|
|
976
852
|
|
|
977
|
-
// src/lib/package-metadata.ts
|
|
978
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
979
|
-
var cachedMetadata = null;
|
|
980
|
-
function getPackageMetadata() {
|
|
981
|
-
if (cachedMetadata)
|
|
982
|
-
return cachedMetadata;
|
|
983
|
-
const raw = readFileSync4(new URL("../../package.json", import.meta.url), "utf8");
|
|
984
|
-
const parsed = JSON.parse(raw);
|
|
985
|
-
cachedMetadata = {
|
|
986
|
-
name: parsed.name ?? "@hasna/economy",
|
|
987
|
-
version: parsed.version ?? "0.0.0"
|
|
988
|
-
};
|
|
989
|
-
return cachedMetadata;
|
|
990
|
-
}
|
|
991
|
-
var packageMetadata = getPackageMetadata();
|
|
992
|
-
|
|
993
853
|
// src/mcp/index.ts
|
|
994
854
|
init_pricing();
|
|
995
|
-
function printHelp() {
|
|
996
|
-
console.log(`Usage: economy-mcp [options]
|
|
997
|
-
|
|
998
|
-
Runs the ${packageMetadata.name} MCP stdio server.
|
|
999
|
-
|
|
1000
|
-
Options:
|
|
1001
|
-
-V, --version output the version number
|
|
1002
|
-
-h, --help display help for command`);
|
|
1003
|
-
}
|
|
1004
|
-
var args = process.argv.slice(2);
|
|
1005
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
1006
|
-
printHelp();
|
|
1007
|
-
process.exit(0);
|
|
1008
|
-
}
|
|
1009
|
-
if (args.includes("--version") || args.includes("-V")) {
|
|
1010
|
-
console.log(packageMetadata.version);
|
|
1011
|
-
process.exit(0);
|
|
1012
|
-
}
|
|
1013
855
|
var db = openDatabase();
|
|
1014
856
|
ensurePricingSeeded(db);
|
|
1015
|
-
var server = new
|
|
1016
|
-
name: "economy",
|
|
1017
|
-
version: packageMetadata.version
|
|
1018
|
-
});
|
|
1019
|
-
var _econAgents = new Map;
|
|
1020
|
-
var TOOL_NAMES = [
|
|
1021
|
-
"get_cost_summary",
|
|
1022
|
-
"get_sessions",
|
|
1023
|
-
"get_top_sessions",
|
|
1024
|
-
"get_model_breakdown",
|
|
1025
|
-
"get_project_breakdown",
|
|
1026
|
-
"get_budget_status",
|
|
1027
|
-
"get_daily",
|
|
1028
|
-
"get_session_detail",
|
|
1029
|
-
"sync",
|
|
1030
|
-
"search_tools",
|
|
1031
|
-
"describe_tools",
|
|
1032
|
-
"get_goals",
|
|
1033
|
-
"set_goal",
|
|
1034
|
-
"remove_goal",
|
|
1035
|
-
"list_machines",
|
|
1036
|
-
"register_agent",
|
|
1037
|
-
"heartbeat",
|
|
1038
|
-
"set_focus",
|
|
1039
|
-
"list_agents",
|
|
1040
|
-
"send_feedback"
|
|
1041
|
-
];
|
|
1042
|
-
var TOOL_DESCRIPTIONS = {
|
|
1043
|
-
get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
|
|
1044
|
-
get_sessions: "agent(claude|codex|gemini), project(partial), machine?(hostname), limit(20) -> compact session table",
|
|
1045
|
-
get_top_sessions: "n(10), agent(claude|codex|gemini) -> top sessions by cost",
|
|
1046
|
-
list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
|
|
1047
|
-
get_model_breakdown: "no params -> model, requests, tokens, cost",
|
|
1048
|
-
get_project_breakdown: "no params -> project_name, sessions, cost",
|
|
1049
|
-
get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
|
|
1050
|
-
get_daily: "days(30) -> daily cost table grouped by date and agent",
|
|
1051
|
-
get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
|
|
1052
|
-
sync: "sources(all|claude|codex|gemini) -> ingest latest cost data",
|
|
1053
|
-
search_tools: "query substring -> tool name list",
|
|
1054
|
-
describe_tools: "names[] -> one-line parameter hints",
|
|
1055
|
-
get_goals: "no params -> goal progress summary",
|
|
1056
|
-
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
|
|
1057
|
-
remove_goal: "id -> delete goal",
|
|
1058
|
-
register_agent: "name, session_id? -> register agent session",
|
|
1059
|
-
heartbeat: "agent_id -> update last_seen_at",
|
|
1060
|
-
set_focus: "agent_id, project_id? -> set active project context",
|
|
1061
|
-
list_agents: "no params -> registered agent list",
|
|
1062
|
-
send_feedback: "message, email?, category? -> save feedback locally"
|
|
1063
|
-
};
|
|
857
|
+
var server = new Server({ name: "economy", version: "0.2.2" }, { capabilities: { tools: {} } });
|
|
1064
858
|
var fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
1065
859
|
var fmtTok = (n) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
1066
860
|
function fmtSession(s) {
|
|
@@ -1071,249 +865,262 @@ function fmtSession(s) {
|
|
|
1071
865
|
const tok = fmtTok(Number(s["total_tokens"] ?? 0));
|
|
1072
866
|
return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
|
|
1073
867
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
machine,
|
|
1114
|
-
limit: limit ?? 20
|
|
1115
|
-
});
|
|
1116
|
-
const lines = ["id agent cost tokens project"];
|
|
1117
|
-
for (const session of sessions)
|
|
1118
|
-
lines.push(fmtSession(session));
|
|
1119
|
-
return text(lines.join(`
|
|
1120
|
-
`));
|
|
1121
|
-
});
|
|
1122
|
-
server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
|
|
1123
|
-
n: z.number().int().positive().max(100).optional(),
|
|
1124
|
-
agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional()
|
|
1125
|
-
}, async ({ n, agent }) => {
|
|
1126
|
-
const sessions = queryTopSessions(db, n ?? 10, agent);
|
|
1127
|
-
const lines = ["rank id agent cost tokens project"];
|
|
1128
|
-
sessions.forEach((session, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(session)}`));
|
|
1129
|
-
return text(lines.join(`
|
|
1130
|
-
`));
|
|
1131
|
-
});
|
|
1132
|
-
server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
|
|
1133
|
-
const rows = queryModelBreakdown(db);
|
|
1134
|
-
const lines = ["model reqs tokens cost"];
|
|
1135
|
-
for (const row of rows) {
|
|
1136
|
-
lines.push(`${String(row["model"]).slice(0, 30).padEnd(31)}${String(row["requests"]).padEnd(8)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
|
|
1137
|
-
}
|
|
1138
|
-
return text(lines.join(`
|
|
1139
|
-
`));
|
|
1140
|
-
});
|
|
1141
|
-
server.tool("get_project_breakdown", "Cost per project. No params.", {}, async () => {
|
|
1142
|
-
const rows = queryProjectBreakdown(db);
|
|
1143
|
-
const lines = ["project sessions tokens cost"];
|
|
1144
|
-
for (const row of rows) {
|
|
1145
|
-
const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
|
|
1146
|
-
lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
|
|
1147
|
-
}
|
|
1148
|
-
return text(lines.join(`
|
|
1149
|
-
`));
|
|
1150
|
-
});
|
|
1151
|
-
server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
|
|
1152
|
-
const budgets = getBudgetStatuses(db);
|
|
1153
|
-
if (budgets.length === 0)
|
|
1154
|
-
return text("No budgets set.");
|
|
1155
|
-
const lines = ["scope period spent limit used% status"];
|
|
1156
|
-
for (const budget of budgets) {
|
|
1157
|
-
const scope = String(budget["project_path"] ?? "global").slice(0, 20);
|
|
1158
|
-
const pct = Number(budget["percent_used"]).toFixed(1);
|
|
1159
|
-
const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
|
|
1160
|
-
lines.push(`${scope.padEnd(21)}${String(budget["period"]).padEnd(9)}${fmtUsd(Number(budget["current_spend_usd"])).padEnd(11)}${fmtUsd(Number(budget["limit_usd"])).padEnd(11)}${pct}%`.padEnd(49) + ` ${status}`);
|
|
1161
|
-
}
|
|
1162
|
-
return text(lines.join(`
|
|
1163
|
-
`));
|
|
1164
|
-
});
|
|
1165
|
-
server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
|
|
1166
|
-
const rows = queryDailyBreakdown(db, days ?? 30);
|
|
1167
|
-
const byDate = new Map;
|
|
1168
|
-
for (const row of rows) {
|
|
1169
|
-
const date = String(row["date"]);
|
|
1170
|
-
const entry = byDate.get(date) ?? { claude: 0, codex: 0, gemini: 0 };
|
|
1171
|
-
if (row["agent"] === "claude")
|
|
1172
|
-
entry.claude += Number(row["cost_usd"]);
|
|
1173
|
-
else if (row["agent"] === "codex")
|
|
1174
|
-
entry.codex += Number(row["cost_usd"]);
|
|
1175
|
-
else if (row["agent"] === "gemini")
|
|
1176
|
-
entry.gemini += Number(row["cost_usd"]);
|
|
1177
|
-
byDate.set(date, entry);
|
|
1178
|
-
}
|
|
1179
|
-
const lines = ["date claude codex gemini total"];
|
|
1180
|
-
for (const [date, costs] of [...byDate.entries()].sort()) {
|
|
1181
|
-
const total = costs.claude + costs.codex + costs.gemini;
|
|
1182
|
-
lines.push(`${date} ${fmtUsd(costs.claude).padEnd(11)}${fmtUsd(costs.codex).padEnd(11)}${fmtUsd(costs.gemini).padEnd(11)}${fmtUsd(total)}`);
|
|
1183
|
-
}
|
|
1184
|
-
return text(lines.join(`
|
|
1185
|
-
`));
|
|
1186
|
-
});
|
|
1187
|
-
server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
|
|
1188
|
-
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(session_id, `${session_id}%`);
|
|
1189
|
-
if (!session)
|
|
1190
|
-
return textError(`Session not found: ${session_id}`);
|
|
1191
|
-
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
|
|
1192
|
-
const lines = [
|
|
1193
|
-
`session: ${String(session["id"]).slice(0, 16)}`,
|
|
1194
|
-
`agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
|
|
1195
|
-
`cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
|
|
1196
|
-
"",
|
|
1197
|
-
"time model input output cost"
|
|
1198
|
-
];
|
|
1199
|
-
for (const request of requests) {
|
|
1200
|
-
lines.push(`${String(request["timestamp"]).slice(11, 19)} ${String(request["model"]).slice(0, 22).padEnd(23)}${fmtTok(Number(request["input_tokens"])).padEnd(9)}${fmtTok(Number(request["output_tokens"])).padEnd(9)}${fmtUsd(Number(request["cost_usd"]))}`);
|
|
1201
|
-
}
|
|
1202
|
-
return text(lines.join(`
|
|
1203
|
-
`));
|
|
1204
|
-
});
|
|
1205
|
-
server.tool("sync", "Ingest new cost data. sources: all|claude|takumi|codex|gemini", { sources: z.enum(["all", "claude", "takumi", "codex", "gemini"]).optional() }, async ({ sources }) => {
|
|
1206
|
-
const selected = sources ?? "all";
|
|
1207
|
-
const parts = [];
|
|
1208
|
-
if (selected === "all" || selected === "claude") {
|
|
1209
|
-
const result = await ingestClaude(db);
|
|
1210
|
-
parts.push(`claude: ${result["files"]} files, ${result["requests"]} requests, ${result["sessions"]} sessions`);
|
|
1211
|
-
}
|
|
1212
|
-
if (selected === "all" || selected === "takumi") {
|
|
1213
|
-
const result = await ingestTakumi(db);
|
|
1214
|
-
parts.push(`takumi: ${result["files"]} files, ${result["requests"]} requests, ${result["sessions"]} sessions`);
|
|
1215
|
-
}
|
|
1216
|
-
if (selected === "all" || selected === "codex") {
|
|
1217
|
-
const result = await ingestCodex(db);
|
|
1218
|
-
parts.push(`codex: ${result["sessions"]} sessions`);
|
|
1219
|
-
}
|
|
1220
|
-
if (selected === "all" || selected === "gemini") {
|
|
1221
|
-
const result = await ingestGemini(db);
|
|
1222
|
-
parts.push(`gemini: ${result["sessions"]} sessions`);
|
|
1223
|
-
}
|
|
1224
|
-
return text(parts.join(`
|
|
1225
|
-
`) || "done");
|
|
1226
|
-
});
|
|
1227
|
-
server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
|
|
1228
|
-
const goals = getGoalStatuses(db);
|
|
1229
|
-
if (goals.length === 0)
|
|
1230
|
-
return text("No goals set.");
|
|
1231
|
-
const lines = ["period scope limit spent used% status"];
|
|
1232
|
-
for (const goal of goals) {
|
|
1233
|
-
const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
|
|
1234
|
-
const pct = Number(goal["percent_used"]).toFixed(1);
|
|
1235
|
-
const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
|
|
1236
|
-
lines.push(`${String(goal["period"]).padEnd(9)}${scope.padEnd(21)}${fmtUsd(Number(goal["limit_usd"])).padEnd(11)}${fmtUsd(Number(goal["current_spend_usd"])).padEnd(11)}${pct}% ${status}`);
|
|
1237
|
-
}
|
|
1238
|
-
return text(lines.join(`
|
|
1239
|
-
`));
|
|
1240
|
-
});
|
|
1241
|
-
server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
|
|
1242
|
-
period: z.enum(["day", "week", "month", "year"]),
|
|
1243
|
-
limit_usd: z.number().nonnegative(),
|
|
1244
|
-
project_path: z.string().optional(),
|
|
1245
|
-
agent: z.string().optional()
|
|
1246
|
-
}, async ({ period, limit_usd, project_path, agent }) => {
|
|
1247
|
-
const now = new Date().toISOString();
|
|
1248
|
-
upsertGoal(db, {
|
|
1249
|
-
id: randomUUID(),
|
|
1250
|
-
period,
|
|
1251
|
-
project_path: project_path ?? null,
|
|
1252
|
-
agent: agent ?? null,
|
|
1253
|
-
limit_usd,
|
|
1254
|
-
created_at: now,
|
|
1255
|
-
updated_at: now
|
|
1256
|
-
});
|
|
1257
|
-
return text(`Goal set: ${period} $${limit_usd}`);
|
|
1258
|
-
});
|
|
1259
|
-
server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
|
|
1260
|
-
deleteGoal(db, id);
|
|
1261
|
-
return text("Goal removed.");
|
|
1262
|
-
});
|
|
1263
|
-
server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
|
|
1264
|
-
const machines = listMachines(db);
|
|
1265
|
-
if (machines.length === 0)
|
|
1266
|
-
return text(`No machine data yet. Current machine: ${getMachineId()}`);
|
|
1267
|
-
const lines = ["machine sessions requests cost last_active"];
|
|
1268
|
-
for (const m of machines) {
|
|
1269
|
-
lines.push(`${m.machine_id.padEnd(17)}${String(m.sessions).padEnd(10)}${String(m.requests).padEnd(10)}${fmtUsd(m.total_cost_usd).padEnd(12)}${m.last_active?.substring(0, 16) ?? "\u2014"}`);
|
|
1270
|
-
}
|
|
1271
|
-
lines.push(`
|
|
1272
|
-
current machine: ${getMachineId()}`);
|
|
1273
|
-
return text(lines.join(`
|
|
1274
|
-
`));
|
|
1275
|
-
});
|
|
1276
|
-
server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
|
|
1277
|
-
const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
|
|
1278
|
-
if (existing) {
|
|
1279
|
-
existing.last_seen_at = new Date().toISOString();
|
|
1280
|
-
return text(JSON.stringify(existing));
|
|
1281
|
-
}
|
|
1282
|
-
const id = Math.random().toString(36).slice(2, 10);
|
|
1283
|
-
const agent = { id, name, last_seen_at: new Date().toISOString() };
|
|
1284
|
-
_econAgents.set(id, agent);
|
|
1285
|
-
return text(JSON.stringify(agent));
|
|
1286
|
-
});
|
|
1287
|
-
server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
|
|
1288
|
-
const agent = _econAgents.get(agent_id);
|
|
1289
|
-
if (!agent)
|
|
1290
|
-
return textError("Agent not found");
|
|
1291
|
-
agent.last_seen_at = new Date().toISOString();
|
|
1292
|
-
return text(`\u2665 ${agent.name}`);
|
|
1293
|
-
});
|
|
1294
|
-
server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
|
|
1295
|
-
const agent = _econAgents.get(agent_id);
|
|
1296
|
-
if (!agent)
|
|
1297
|
-
return textError("Agent not found");
|
|
1298
|
-
agent.project_id = project_id ?? undefined;
|
|
1299
|
-
return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
|
|
1300
|
-
});
|
|
1301
|
-
server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
|
|
1302
|
-
server.tool("send_feedback", "Send feedback about this service.", {
|
|
1303
|
-
message: z.string(),
|
|
1304
|
-
email: z.string().optional(),
|
|
1305
|
-
category: z.enum(["bug", "feature", "general"]).optional()
|
|
1306
|
-
}, async ({ message, email, category }) => {
|
|
868
|
+
var TOOLS = [
|
|
869
|
+
{ name: "get_cost_summary", description: "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all", inputSchema: { type: "object", properties: { period: { type: "string", enum: ["today", "week", "month", "year", "all"] } } } },
|
|
870
|
+
{ name: "get_sessions", description: "List sessions. Returns compact table. Params: agent, project, limit(20)", inputSchema: { type: "object", properties: { agent: { type: "string" }, project: { type: "string" }, limit: { type: "number" } } } },
|
|
871
|
+
{ name: "get_top_sessions", description: "Top sessions by cost. Params: n(10), agent", inputSchema: { type: "object", properties: { n: { type: "number" }, agent: { type: "string" } } } },
|
|
872
|
+
{ name: "get_model_breakdown", description: "Cost per model. No params.", inputSchema: { type: "object", properties: {} } },
|
|
873
|
+
{ name: "get_project_breakdown", description: "Cost per project. No params.", inputSchema: { type: "object", properties: {} } },
|
|
874
|
+
{ name: "get_budget_status", description: "Budget limits vs spend, percent used, alert flags. No params.", inputSchema: { type: "object", properties: {} } },
|
|
875
|
+
{ name: "get_daily", description: "Daily cost table by agent. Params: days(30)", inputSchema: { type: "object", properties: { days: { type: "number" } } } },
|
|
876
|
+
{ name: "get_session_detail", description: "Per-request breakdown of a single session. Params: session_id (prefix ok)", inputSchema: { type: "object", properties: { session_id: { type: "string" } }, required: ["session_id"] } },
|
|
877
|
+
{ name: "sync", description: "Ingest new cost data. sources: all|claude|codex|gemini", inputSchema: { type: "object", properties: { sources: { type: "string", enum: ["all", "claude", "codex", "gemini"] } } } },
|
|
878
|
+
{ name: "search_tools", description: "List tool names matching query. Use first to find relevant tools.", inputSchema: { type: "object", properties: { query: { type: "string" } } } },
|
|
879
|
+
{ name: "describe_tools", description: "Get param hints for specific tools by name.", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" } } }, required: ["names"] } },
|
|
880
|
+
{ name: "get_goals", description: "All spending goals with current progress. No params.", inputSchema: { type: "object", properties: {} } },
|
|
881
|
+
{ name: "set_goal", description: "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", inputSchema: { type: "object", properties: { period: { type: "string" }, limit_usd: { type: "number" }, project_path: { type: "string" }, agent: { type: "string" } }, required: ["period", "limit_usd"] } },
|
|
882
|
+
{ name: "remove_goal", description: "Delete a goal by id.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
883
|
+
{ name: "register_agent", description: "Register agent session.", inputSchema: { type: "object", properties: { name: { type: "string" }, session_id: { type: "string" } }, required: ["name"] } },
|
|
884
|
+
{ name: "heartbeat", description: "Update last_seen_at.", inputSchema: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] } },
|
|
885
|
+
{ name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } },
|
|
886
|
+
{ name: "list_agents", description: "List all registered agents.", inputSchema: { type: "object", properties: {} } },
|
|
887
|
+
{ name: "send_feedback", description: "Send feedback about this service.", inputSchema: { type: "object", properties: { message: { type: "string" }, email: { type: "string" }, category: { type: "string", enum: ["bug", "feature", "general"] } }, required: ["message"] } }
|
|
888
|
+
];
|
|
889
|
+
var TOOL_DESCRIPTIONS = {
|
|
890
|
+
get_cost_summary: "period(today|week|month|year|all) \u2192 {total_usd, sessions, requests, tokens, summary}",
|
|
891
|
+
get_sessions: "agent(claude|codex), project(partial), limit(20) \u2192 compact session table",
|
|
892
|
+
get_top_sessions: "n(10), agent(claude|codex) \u2192 top sessions by cost",
|
|
893
|
+
get_model_breakdown: "no params \u2192 model, requests, tokens, cost",
|
|
894
|
+
get_project_breakdown: "no params \u2192 project_name, sessions, cost",
|
|
895
|
+
get_budget_status: "no params \u2192 budget limits, current spend, percent_used, is_over_alert",
|
|
896
|
+
get_daily: "days(30) \u2192 daily cost table grouped by date and agent",
|
|
897
|
+
get_session_detail: "session_id(prefix ok) \u2192 per-request breakdown with model, tokens, cost",
|
|
898
|
+
sync: "sources(all|claude|codex|gemini) \u2192 {files, requests, sessions} ingested",
|
|
899
|
+
get_goals: "no params \u2192 period, scope, limit, spent, percent, status(ON TRACK/AT RISK/OVER)",
|
|
900
|
+
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? \u2192 creates/updates goal",
|
|
901
|
+
remove_goal: "id \u2192 deletes goal"
|
|
902
|
+
};
|
|
903
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
904
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
905
|
+
const { name, arguments: args } = req.params;
|
|
906
|
+
const a = args ?? {};
|
|
1307
907
|
try {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
908
|
+
switch (name) {
|
|
909
|
+
case "search_tools": {
|
|
910
|
+
const q = a["query"]?.toLowerCase();
|
|
911
|
+
const names = TOOLS.map((t) => t.name);
|
|
912
|
+
const matches = q ? names.filter((n) => n.includes(q)) : names;
|
|
913
|
+
return { content: [{ type: "text", text: matches.join(", ") }] };
|
|
914
|
+
}
|
|
915
|
+
case "describe_tools": {
|
|
916
|
+
const names = a["names"] ?? [];
|
|
917
|
+
const result = names.map((n) => `${n}: ${TOOL_DESCRIPTIONS[n] ?? "see tool schema"}`).join(`
|
|
918
|
+
`);
|
|
919
|
+
return { content: [{ type: "text", text: result }] };
|
|
920
|
+
}
|
|
921
|
+
case "get_cost_summary": {
|
|
922
|
+
const period = a["period"] ?? "today";
|
|
923
|
+
const s = querySummary(db, period);
|
|
924
|
+
const text = [
|
|
925
|
+
`period: ${period}`,
|
|
926
|
+
`cost: ${fmtUsd(s.total_usd)}`,
|
|
927
|
+
`sessions: ${s.sessions}`,
|
|
928
|
+
`requests: ${s.requests.toLocaleString()}`,
|
|
929
|
+
`tokens: ${fmtTok(s.tokens)}`,
|
|
930
|
+
`summary: You've spent ${fmtUsd(s.total_usd)} ${period === "all" ? "total" : period} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
|
|
931
|
+
].join(`
|
|
932
|
+
`);
|
|
933
|
+
return { content: [{ type: "text", text }] };
|
|
934
|
+
}
|
|
935
|
+
case "get_sessions": {
|
|
936
|
+
const sessions = querySessions(db, {
|
|
937
|
+
agent: a["agent"],
|
|
938
|
+
project: a["project"],
|
|
939
|
+
limit: Number(a["limit"] ?? 20)
|
|
940
|
+
});
|
|
941
|
+
const lines = ["id agent cost tokens project"];
|
|
942
|
+
for (const s of sessions)
|
|
943
|
+
lines.push(fmtSession(s));
|
|
944
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
945
|
+
`) }] };
|
|
946
|
+
}
|
|
947
|
+
case "get_top_sessions": {
|
|
948
|
+
const sessions = queryTopSessions(db, Number(a["n"] ?? 10), a["agent"]);
|
|
949
|
+
const lines = ["rank id agent cost tokens project"];
|
|
950
|
+
sessions.forEach((s, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(s)}`));
|
|
951
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
952
|
+
`) }] };
|
|
953
|
+
}
|
|
954
|
+
case "get_model_breakdown": {
|
|
955
|
+
const rows = queryModelBreakdown(db);
|
|
956
|
+
const lines = ["model reqs tokens cost"];
|
|
957
|
+
for (const r of rows) {
|
|
958
|
+
lines.push(`${String(r["model"]).slice(0, 30).padEnd(31)}${String(r["requests"]).padEnd(8)}${fmtTok(Number(r["total_tokens"])).padEnd(9)}${fmtUsd(Number(r["cost_usd"]))}`);
|
|
959
|
+
}
|
|
960
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
961
|
+
`) }] };
|
|
962
|
+
}
|
|
963
|
+
case "get_project_breakdown": {
|
|
964
|
+
const rows = queryProjectBreakdown(db);
|
|
965
|
+
const lines = ["project sessions tokens cost"];
|
|
966
|
+
for (const r of rows) {
|
|
967
|
+
const name2 = String(r["project_name"] || r["project_path"] || "\u2014").slice(0, 20);
|
|
968
|
+
lines.push(`${name2.padEnd(21)}${String(r["sessions"]).padEnd(9)}${fmtTok(Number(r["total_tokens"])).padEnd(9)}${fmtUsd(Number(r["cost_usd"]))}`);
|
|
969
|
+
}
|
|
970
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
971
|
+
`) }] };
|
|
972
|
+
}
|
|
973
|
+
case "get_budget_status": {
|
|
974
|
+
const budgets = getBudgetStatuses(db);
|
|
975
|
+
if (budgets.length === 0)
|
|
976
|
+
return { content: [{ type: "text", text: "No budgets set." }] };
|
|
977
|
+
const lines = ["scope period spent limit used% status"];
|
|
978
|
+
for (const b of budgets) {
|
|
979
|
+
const scope = String(b["project_path"] ?? "global").slice(0, 20);
|
|
980
|
+
const pct = Number(b["percent_used"]).toFixed(1);
|
|
981
|
+
const status = b["is_over_limit"] ? "OVER" : b["is_over_alert"] ? "ALERT" : "OK";
|
|
982
|
+
lines.push(`${scope.padEnd(21)}${String(b["period"]).padEnd(9)}${fmtUsd(Number(b["current_spend_usd"])).padEnd(11)}${fmtUsd(Number(b["limit_usd"])).padEnd(11)}${pct}%`.padEnd(49) + ` ${status}`);
|
|
983
|
+
}
|
|
984
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
985
|
+
`) }] };
|
|
986
|
+
}
|
|
987
|
+
case "get_daily": {
|
|
988
|
+
const days = Number(a["days"] ?? 30);
|
|
989
|
+
const rows = queryDailyBreakdown(db, days);
|
|
990
|
+
const lines = ["date claude codex gemini total"];
|
|
991
|
+
const byDate = new Map;
|
|
992
|
+
for (const r of rows) {
|
|
993
|
+
const d = String(r["date"]);
|
|
994
|
+
const entry = byDate.get(d) ?? { claude: 0, codex: 0, gemini: 0 };
|
|
995
|
+
if (r["agent"] === "claude")
|
|
996
|
+
entry.claude += Number(r["cost_usd"]);
|
|
997
|
+
else if (r["agent"] === "codex")
|
|
998
|
+
entry.codex += Number(r["cost_usd"]);
|
|
999
|
+
else if (r["agent"] === "gemini")
|
|
1000
|
+
entry.gemini += Number(r["cost_usd"]);
|
|
1001
|
+
byDate.set(d, entry);
|
|
1002
|
+
}
|
|
1003
|
+
for (const [date, costs] of [...byDate.entries()].sort()) {
|
|
1004
|
+
const total = costs.claude + costs.codex + costs.gemini;
|
|
1005
|
+
lines.push(`${date} ${fmtUsd(costs.claude).padEnd(11)}${fmtUsd(costs.codex).padEnd(11)}${fmtUsd(costs.gemini).padEnd(11)}${fmtUsd(total)}`);
|
|
1006
|
+
}
|
|
1007
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
1008
|
+
`) }] };
|
|
1009
|
+
}
|
|
1010
|
+
case "get_session_detail": {
|
|
1011
|
+
const sid = String(a["session_id"] ?? "");
|
|
1012
|
+
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(sid, `${sid}%`);
|
|
1013
|
+
if (!session)
|
|
1014
|
+
return { content: [{ type: "text", text: `Session not found: ${sid}` }], isError: true };
|
|
1015
|
+
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
|
|
1016
|
+
const lines = [
|
|
1017
|
+
`session: ${String(session["id"]).slice(0, 16)}`,
|
|
1018
|
+
`agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
|
|
1019
|
+
`cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
|
|
1020
|
+
"",
|
|
1021
|
+
"time model input output cost"
|
|
1022
|
+
];
|
|
1023
|
+
for (const r of requests) {
|
|
1024
|
+
lines.push(`${String(r["timestamp"]).slice(11, 19)} ${String(r["model"]).slice(0, 22).padEnd(23)}${fmtTok(Number(r["input_tokens"])).padEnd(9)}${fmtTok(Number(r["output_tokens"])).padEnd(9)}${fmtUsd(Number(r["cost_usd"]))}`);
|
|
1025
|
+
}
|
|
1026
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
1027
|
+
`) }] };
|
|
1028
|
+
}
|
|
1029
|
+
case "sync": {
|
|
1030
|
+
const sources = a["sources"] ?? "all";
|
|
1031
|
+
const parts = [];
|
|
1032
|
+
if (sources === "all" || sources === "claude") {
|
|
1033
|
+
const r = await ingestClaude(db);
|
|
1034
|
+
parts.push(`claude: ${r["files"]} files, ${r["requests"]} requests, ${r["sessions"]} sessions`);
|
|
1035
|
+
}
|
|
1036
|
+
if (sources === "all" || sources === "codex") {
|
|
1037
|
+
const r = await ingestCodex(db);
|
|
1038
|
+
parts.push(`codex: ${r["sessions"]} sessions`);
|
|
1039
|
+
}
|
|
1040
|
+
if (sources === "all" || sources === "gemini") {
|
|
1041
|
+
const r = await ingestGemini(db);
|
|
1042
|
+
parts.push(`gemini: ${r["sessions"]} sessions`);
|
|
1043
|
+
}
|
|
1044
|
+
return { content: [{ type: "text", text: parts.join(`
|
|
1045
|
+
`) || "done" }] };
|
|
1046
|
+
}
|
|
1047
|
+
case "get_goals": {
|
|
1048
|
+
const goals = getGoalStatuses(db);
|
|
1049
|
+
if (goals.length === 0)
|
|
1050
|
+
return { content: [{ type: "text", text: "No goals set." }] };
|
|
1051
|
+
const lines = ["period scope limit spent used% status"];
|
|
1052
|
+
for (const g of goals) {
|
|
1053
|
+
const scope = String(g["project_path"] ?? g["agent"] ?? "global").slice(0, 20);
|
|
1054
|
+
const pct = Number(g["percent_used"]).toFixed(1);
|
|
1055
|
+
const status = g["is_over"] ? "OVER" : g["is_at_risk"] ? "AT RISK" : "ON TRACK";
|
|
1056
|
+
lines.push(`${String(g["period"]).padEnd(9)}${scope.padEnd(21)}${fmtUsd(Number(g["limit_usd"])).padEnd(11)}${fmtUsd(Number(g["current_spend_usd"])).padEnd(11)}${pct}% ${status}`);
|
|
1057
|
+
}
|
|
1058
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
1059
|
+
`) }] };
|
|
1060
|
+
}
|
|
1061
|
+
case "set_goal": {
|
|
1062
|
+
const { randomUUID } = await import("crypto");
|
|
1063
|
+
const now = new Date().toISOString();
|
|
1064
|
+
upsertGoal(db, {
|
|
1065
|
+
id: randomUUID(),
|
|
1066
|
+
period: String(a["period"] ?? "month"),
|
|
1067
|
+
project_path: a["project_path"] ?? null,
|
|
1068
|
+
agent: a["agent"] ?? null,
|
|
1069
|
+
limit_usd: Number(a["limit_usd"]),
|
|
1070
|
+
created_at: now,
|
|
1071
|
+
updated_at: now
|
|
1072
|
+
});
|
|
1073
|
+
return { content: [{ type: "text", text: `Goal set: ${a["period"]} $${a["limit_usd"]}` }] };
|
|
1074
|
+
}
|
|
1075
|
+
case "remove_goal": {
|
|
1076
|
+
deleteGoal(db, String(a["id"] ?? ""));
|
|
1077
|
+
return { content: [{ type: "text", text: "Goal removed." }] };
|
|
1078
|
+
}
|
|
1079
|
+
case "register_agent": {
|
|
1080
|
+
const n = String(args["name"] ?? "");
|
|
1081
|
+
const ex = [..._econAgents.values()].find((x) => x.name === n);
|
|
1082
|
+
if (ex) {
|
|
1083
|
+
ex.last_seen_at = new Date().toISOString();
|
|
1084
|
+
return { content: [{ type: "text", text: JSON.stringify(ex) }] };
|
|
1085
|
+
}
|
|
1086
|
+
const id = Math.random().toString(36).slice(2, 10);
|
|
1087
|
+
const ag = { id, name: n, last_seen_at: new Date().toISOString() };
|
|
1088
|
+
_econAgents.set(id, ag);
|
|
1089
|
+
return { content: [{ type: "text", text: JSON.stringify(ag) }] };
|
|
1090
|
+
}
|
|
1091
|
+
case "heartbeat": {
|
|
1092
|
+
const ag = _econAgents.get(String(args["agent_id"] ?? ""));
|
|
1093
|
+
if (!ag)
|
|
1094
|
+
return { content: [{ type: "text", text: `Agent not found` }], isError: true };
|
|
1095
|
+
ag.last_seen_at = new Date().toISOString();
|
|
1096
|
+
return { content: [{ type: "text", text: `\u2665 ${ag.name}` }] };
|
|
1097
|
+
}
|
|
1098
|
+
case "set_focus": {
|
|
1099
|
+
const ag = _econAgents.get(String(args["agent_id"] ?? ""));
|
|
1100
|
+
if (!ag)
|
|
1101
|
+
return { content: [{ type: "text", text: `Agent not found` }], isError: true };
|
|
1102
|
+
ag["project_id"] = args["project_id"];
|
|
1103
|
+
return { content: [{ type: "text", text: String(args["project_id"] ? `Focus: ${args["project_id"]}` : "Focus cleared") }] };
|
|
1104
|
+
}
|
|
1105
|
+
case "list_agents": {
|
|
1106
|
+
return { content: [{ type: "text", text: JSON.stringify([..._econAgents.values()]) }] };
|
|
1107
|
+
}
|
|
1108
|
+
case "send_feedback": {
|
|
1109
|
+
try {
|
|
1110
|
+
const pkg = require_package();
|
|
1111
|
+
db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(String(a["message"]), a["email"] || null, a["category"] || "general", pkg.version);
|
|
1112
|
+
return { content: [{ type: "text", text: "Feedback saved. Thank you!" }] };
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
return { content: [{ type: "text", text: String(e) }], isError: true };
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
default:
|
|
1118
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
1119
|
+
}
|
|
1120
|
+
} catch (e) {
|
|
1121
|
+
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
|
|
1312
1122
|
}
|
|
1313
1123
|
});
|
|
1124
|
+
var _econAgents = new Map;
|
|
1314
1125
|
var transport = new StdioServerTransport;
|
|
1315
|
-
registerCloudTools(server, "economy", {
|
|
1316
|
-
dbPath: getDbPath(),
|
|
1317
|
-
migrations: PG_MIGRATIONS
|
|
1318
|
-
});
|
|
1319
1126
|
await server.connect(transport);
|