@hasna/economy 0.2.17 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +186 -13
- package/README.md +186 -13
- package/dashboard/dist/assets/index-5mUN0CPj.css +1 -0
- package/dashboard/dist/assets/index-L1FgNQ4t.js +93 -0
- package/dashboard/dist/index.html +14 -0
- package/dashboard/dist/logo.jpg +0 -0
- package/dashboard/dist/vite.svg +1 -0
- package/dist/cli/index.js +134 -783
- package/dist/db/database.d.ts +1 -23
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/index.js +54 -206
- package/dist/ingest/claude.d.ts +1 -6
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +1 -1
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/webhooks.d.ts +1 -1
- package/dist/lib/webhooks.d.ts.map +1 -1
- package/dist/mcp/index.js +384 -600
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.js +63 -357
- package/dist/server/serve.d.ts +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -4
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -3
- package/dist/ingest/billing.d.ts +0 -18
- package/dist/ingest/billing.d.ts.map +0 -1
- package/dist/lib/package-metadata.d.ts +0 -8
- package/dist/lib/package-metadata.d.ts.map +0 -1
package/dist/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,
|
|
@@ -431,66 +376,23 @@ function queryModelBreakdown(db) {
|
|
|
431
376
|
FROM requests GROUP BY model, agent ORDER BY cost_usd DESC
|
|
432
377
|
`).all();
|
|
433
378
|
}
|
|
434
|
-
function labelForPath(projectPath, projectName) {
|
|
435
|
-
if (projectName && projectName.trim() !== "")
|
|
436
|
-
return projectName;
|
|
437
|
-
if (!projectPath)
|
|
438
|
-
return "";
|
|
439
|
-
const segments = projectPath.split("/").filter(Boolean);
|
|
440
|
-
const projectPrefix = /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/;
|
|
441
|
-
for (const seg of segments) {
|
|
442
|
-
if (projectPrefix.test(seg))
|
|
443
|
-
return seg;
|
|
444
|
-
}
|
|
445
|
-
const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
|
|
446
|
-
for (let i = segments.length - 1;i >= 0; i--) {
|
|
447
|
-
if (!generic.has(segments[i].toLowerCase()))
|
|
448
|
-
return segments[i];
|
|
449
|
-
}
|
|
450
|
-
return segments[segments.length - 1] ?? projectPath;
|
|
451
|
-
}
|
|
452
379
|
function queryProjectBreakdown(db) {
|
|
453
|
-
|
|
454
|
-
SELECT
|
|
455
|
-
|
|
456
|
-
|
|
380
|
+
return db.prepare(`
|
|
381
|
+
SELECT
|
|
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
|
|
394
|
+
ORDER BY cost_usd DESC
|
|
457
395
|
`).all();
|
|
458
|
-
const groups = new Map;
|
|
459
|
-
for (const s of sessions) {
|
|
460
|
-
const label = labelForPath(s.project_path, s.project_name);
|
|
461
|
-
if (!label)
|
|
462
|
-
continue;
|
|
463
|
-
const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path, totalCost: 0, lastActive: "" };
|
|
464
|
-
g.sessionIds.push(s.id);
|
|
465
|
-
g.totalCost += s.total_cost_usd || 0;
|
|
466
|
-
if (!g.lastActive || s.started_at > g.lastActive)
|
|
467
|
-
g.lastActive = s.started_at;
|
|
468
|
-
if (!g.samplePath)
|
|
469
|
-
g.samplePath = s.project_path;
|
|
470
|
-
groups.set(label, g);
|
|
471
|
-
}
|
|
472
|
-
const result = [];
|
|
473
|
-
for (const [label, g] of groups.entries()) {
|
|
474
|
-
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
475
|
-
const reqStats = placeholders.length ? db.prepare(`
|
|
476
|
-
SELECT
|
|
477
|
-
COUNT(*) as requests,
|
|
478
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
479
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens
|
|
480
|
-
FROM requests WHERE session_id IN (${placeholders})
|
|
481
|
-
`).get(...g.sessionIds) : { requests: 0, cost_usd: 0, total_tokens: 0 };
|
|
482
|
-
result.push({
|
|
483
|
-
project_path: g.samplePath,
|
|
484
|
-
project_name: label,
|
|
485
|
-
sessions: g.sessionIds.length,
|
|
486
|
-
requests: reqStats.requests,
|
|
487
|
-
total_tokens: reqStats.total_tokens,
|
|
488
|
-
cost_usd: reqStats.cost_usd > 0 ? reqStats.cost_usd : g.totalCost,
|
|
489
|
-
last_active: g.lastActive
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
493
|
-
return result;
|
|
494
396
|
}
|
|
495
397
|
function queryDailyBreakdown(db, days = 30) {
|
|
496
398
|
return db.prepare(`
|
|
@@ -577,20 +479,6 @@ function getIngestState(db, source, key) {
|
|
|
577
479
|
function setIngestState(db, source, key, value) {
|
|
578
480
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
579
481
|
}
|
|
580
|
-
function listMachines(db) {
|
|
581
|
-
return db.prepare(`
|
|
582
|
-
SELECT
|
|
583
|
-
s.machine_id,
|
|
584
|
-
COUNT(DISTINCT s.id) as sessions,
|
|
585
|
-
COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
|
|
586
|
-
COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
|
|
587
|
-
MAX(s.started_at) as last_active
|
|
588
|
-
FROM sessions s
|
|
589
|
-
WHERE s.machine_id != ''
|
|
590
|
-
GROUP BY s.machine_id
|
|
591
|
-
ORDER BY total_cost_usd DESC
|
|
592
|
-
`).all();
|
|
593
|
-
}
|
|
594
482
|
function upsertModelPricing(db, p) {
|
|
595
483
|
db.prepare(`
|
|
596
484
|
INSERT OR REPLACE INTO model_pricing
|
|
@@ -602,11 +490,11 @@ function getModelPricing(db, model) {
|
|
|
602
490
|
return db.prepare(`SELECT * FROM model_pricing WHERE model = ?`).get(model);
|
|
603
491
|
}
|
|
604
492
|
function seedModelPricing(db, defaults) {
|
|
605
|
-
const existing =
|
|
493
|
+
const existing = db.prepare(`SELECT COUNT(*) as count FROM model_pricing`).get();
|
|
494
|
+
if (existing.count > 0)
|
|
495
|
+
return;
|
|
606
496
|
const now = new Date().toISOString();
|
|
607
497
|
for (const [model, p] of Object.entries(defaults)) {
|
|
608
|
-
if (existing.has(model))
|
|
609
|
-
continue;
|
|
610
498
|
upsertModelPricing(db, {
|
|
611
499
|
model,
|
|
612
500
|
input_per_1m: p.inputPer1M,
|
|
@@ -619,102 +507,81 @@ function seedModelPricing(db, defaults) {
|
|
|
619
507
|
}
|
|
620
508
|
var init_database = () => {};
|
|
621
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
|
+
|
|
622
580
|
// src/mcp/index.ts
|
|
623
581
|
init_database();
|
|
624
|
-
import {
|
|
625
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
582
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
626
583
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
627
|
-
import {
|
|
628
|
-
import { z } from "zod";
|
|
629
|
-
|
|
630
|
-
// src/db/pg-migrations.ts
|
|
631
|
-
var PG_MIGRATIONS = [
|
|
632
|
-
`CREATE TABLE IF NOT EXISTS requests (
|
|
633
|
-
id TEXT PRIMARY KEY,
|
|
634
|
-
agent TEXT NOT NULL,
|
|
635
|
-
session_id TEXT NOT NULL,
|
|
636
|
-
model TEXT NOT NULL,
|
|
637
|
-
input_tokens INTEGER DEFAULT 0,
|
|
638
|
-
output_tokens INTEGER DEFAULT 0,
|
|
639
|
-
cache_read_tokens INTEGER DEFAULT 0,
|
|
640
|
-
cache_create_tokens INTEGER DEFAULT 0,
|
|
641
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
642
|
-
duration_ms INTEGER DEFAULT 0,
|
|
643
|
-
timestamp TEXT NOT NULL,
|
|
644
|
-
source_request_id TEXT,
|
|
645
|
-
machine_id TEXT DEFAULT ''
|
|
646
|
-
)`,
|
|
647
|
-
`CREATE TABLE IF NOT EXISTS sessions (
|
|
648
|
-
id TEXT PRIMARY KEY,
|
|
649
|
-
agent TEXT NOT NULL,
|
|
650
|
-
project_path TEXT DEFAULT '',
|
|
651
|
-
project_name TEXT DEFAULT '',
|
|
652
|
-
started_at TEXT NOT NULL,
|
|
653
|
-
ended_at TEXT,
|
|
654
|
-
total_cost_usd REAL DEFAULT 0,
|
|
655
|
-
total_tokens INTEGER DEFAULT 0,
|
|
656
|
-
request_count INTEGER DEFAULT 0,
|
|
657
|
-
machine_id TEXT DEFAULT ''
|
|
658
|
-
)`,
|
|
659
|
-
`CREATE TABLE IF NOT EXISTS projects (
|
|
660
|
-
id TEXT PRIMARY KEY,
|
|
661
|
-
path TEXT UNIQUE NOT NULL,
|
|
662
|
-
name TEXT NOT NULL,
|
|
663
|
-
description TEXT,
|
|
664
|
-
tags TEXT DEFAULT '[]',
|
|
665
|
-
created_at TEXT NOT NULL
|
|
666
|
-
)`,
|
|
667
|
-
`CREATE TABLE IF NOT EXISTS budgets (
|
|
668
|
-
id TEXT PRIMARY KEY,
|
|
669
|
-
project_path TEXT,
|
|
670
|
-
agent TEXT,
|
|
671
|
-
period TEXT NOT NULL,
|
|
672
|
-
limit_usd REAL NOT NULL,
|
|
673
|
-
alert_at_percent INTEGER DEFAULT 80,
|
|
674
|
-
created_at TEXT NOT NULL,
|
|
675
|
-
updated_at TEXT NOT NULL
|
|
676
|
-
)`,
|
|
677
|
-
`CREATE TABLE IF NOT EXISTS goals (
|
|
678
|
-
id TEXT PRIMARY KEY,
|
|
679
|
-
period TEXT NOT NULL,
|
|
680
|
-
project_path TEXT,
|
|
681
|
-
agent TEXT,
|
|
682
|
-
limit_usd REAL NOT NULL,
|
|
683
|
-
created_at TEXT NOT NULL,
|
|
684
|
-
updated_at TEXT NOT NULL
|
|
685
|
-
)`,
|
|
686
|
-
`CREATE TABLE IF NOT EXISTS ingest_state (
|
|
687
|
-
source TEXT NOT NULL,
|
|
688
|
-
key TEXT NOT NULL,
|
|
689
|
-
value TEXT NOT NULL,
|
|
690
|
-
PRIMARY KEY (source, key)
|
|
691
|
-
)`,
|
|
692
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
693
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
694
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
695
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
696
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
697
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
698
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
699
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
700
|
-
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
701
|
-
model TEXT PRIMARY KEY,
|
|
702
|
-
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
703
|
-
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
704
|
-
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
705
|
-
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
706
|
-
updated_at TEXT NOT NULL
|
|
707
|
-
)`,
|
|
708
|
-
`CREATE TABLE IF NOT EXISTS feedback (
|
|
709
|
-
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
710
|
-
message TEXT NOT NULL,
|
|
711
|
-
email TEXT,
|
|
712
|
-
category TEXT DEFAULT 'general',
|
|
713
|
-
version TEXT,
|
|
714
|
-
machine_id TEXT,
|
|
715
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
716
|
-
)`
|
|
717
|
-
];
|
|
584
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
718
585
|
|
|
719
586
|
// src/ingest/claude.ts
|
|
720
587
|
init_database();
|
|
@@ -725,8 +592,7 @@ import { join as join2, basename } from "path";
|
|
|
725
592
|
function autoDetectProject(cwd, projects) {
|
|
726
593
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
727
594
|
}
|
|
728
|
-
var
|
|
729
|
-
var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
|
|
595
|
+
var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
|
|
730
596
|
function dirNameToPath(dirName) {
|
|
731
597
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
732
598
|
}
|
|
@@ -746,36 +612,29 @@ function collectJsonlFiles(projectDir) {
|
|
|
746
612
|
return files;
|
|
747
613
|
}
|
|
748
614
|
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
749
|
-
|
|
750
|
-
}
|
|
751
|
-
async function ingestTakumi(db, verbose = false) {
|
|
752
|
-
return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
|
|
753
|
-
}
|
|
754
|
-
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
755
|
-
if (!existsSync2(projectsDir)) {
|
|
615
|
+
if (!existsSync2(PROJECTS_DIR)) {
|
|
756
616
|
if (verbose)
|
|
757
|
-
console.log(
|
|
617
|
+
console.log("Claude projects dir not found:", PROJECTS_DIR);
|
|
758
618
|
return { files: 0, requests: 0, sessions: 0 };
|
|
759
619
|
}
|
|
760
|
-
const machineId = getMachineId();
|
|
761
620
|
let totalFiles = 0;
|
|
762
621
|
let totalRequests = 0;
|
|
763
622
|
const touchedSessions = new Set;
|
|
764
623
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
765
|
-
const projectDirs = readdirSync2(
|
|
624
|
+
const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
766
625
|
for (const projectDirEntry of projectDirs) {
|
|
767
|
-
const projectDirPath = join2(
|
|
626
|
+
const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
|
|
768
627
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
769
628
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
770
629
|
for (const filePath of jsonlFiles) {
|
|
771
|
-
const stateKey = filePath.replace(
|
|
630
|
+
const stateKey = filePath.replace(PROJECTS_DIR, "");
|
|
772
631
|
let fileMtime = "0";
|
|
773
632
|
try {
|
|
774
633
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
775
634
|
} catch {
|
|
776
635
|
continue;
|
|
777
636
|
}
|
|
778
|
-
const processed = getIngestState(db,
|
|
637
|
+
const processed = getIngestState(db, "claude", stateKey);
|
|
779
638
|
if (processed === fileMtime)
|
|
780
639
|
continue;
|
|
781
640
|
let lines;
|
|
@@ -816,10 +675,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
816
675
|
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
817
676
|
continue;
|
|
818
677
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
819
|
-
const reqId =
|
|
678
|
+
const reqId = `claude-${sessionId}-${timestamp}`;
|
|
820
679
|
upsertRequest(db, {
|
|
821
680
|
id: reqId,
|
|
822
|
-
agent:
|
|
681
|
+
agent: "claude",
|
|
823
682
|
session_id: sessionId,
|
|
824
683
|
model,
|
|
825
684
|
input_tokens: inputTokens,
|
|
@@ -829,8 +688,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
829
688
|
cost_usd: costUsd,
|
|
830
689
|
duration_ms: 0,
|
|
831
690
|
timestamp,
|
|
832
|
-
source_request_id: reqId
|
|
833
|
-
machine_id: machineId
|
|
691
|
+
source_request_id: reqId
|
|
834
692
|
});
|
|
835
693
|
if (!touchedSessions.has(sessionId)) {
|
|
836
694
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
@@ -839,15 +697,14 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
839
697
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
840
698
|
const session = {
|
|
841
699
|
id: sessionId,
|
|
842
|
-
agent:
|
|
700
|
+
agent: "claude",
|
|
843
701
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
844
702
|
project_name: detectedProject ? detectedProject.name : "",
|
|
845
703
|
started_at: timestamp,
|
|
846
704
|
ended_at: null,
|
|
847
705
|
total_cost_usd: 0,
|
|
848
706
|
total_tokens: 0,
|
|
849
|
-
request_count: 0
|
|
850
|
-
machine_id: machineId
|
|
707
|
+
request_count: 0
|
|
851
708
|
};
|
|
852
709
|
upsertSession(db, session);
|
|
853
710
|
}
|
|
@@ -855,7 +712,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
855
712
|
}
|
|
856
713
|
totalRequests++;
|
|
857
714
|
}
|
|
858
|
-
setIngestState(db,
|
|
715
|
+
setIngestState(db, "claude", stateKey, fileMtime);
|
|
859
716
|
totalFiles++;
|
|
860
717
|
}
|
|
861
718
|
}
|
|
@@ -870,7 +727,7 @@ init_database();
|
|
|
870
727
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
871
728
|
import { homedir as homedir3 } from "os";
|
|
872
729
|
import { join as join3, basename as basename2 } from "path";
|
|
873
|
-
import { Database as
|
|
730
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
874
731
|
var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
875
732
|
var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
876
733
|
async function ingestCodex(db, verbose = false) {
|
|
@@ -879,11 +736,10 @@ async function ingestCodex(db, verbose = false) {
|
|
|
879
736
|
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
880
737
|
return { sessions: 0 };
|
|
881
738
|
}
|
|
882
|
-
const machineId = getMachineId();
|
|
883
739
|
let codexDb = null;
|
|
884
740
|
let ingested = 0;
|
|
885
741
|
try {
|
|
886
|
-
codexDb = new
|
|
742
|
+
codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
|
|
887
743
|
const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
|
|
888
744
|
for (const thread of threads) {
|
|
889
745
|
const stateKey = thread.id;
|
|
@@ -904,8 +760,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
904
760
|
ended_at: endedAt,
|
|
905
761
|
total_cost_usd: costUsd,
|
|
906
762
|
total_tokens: thread.tokens_used,
|
|
907
|
-
request_count: 1
|
|
908
|
-
machine_id: machineId
|
|
763
|
+
request_count: 1
|
|
909
764
|
});
|
|
910
765
|
setIngestState(db, "codex", stateKey, "done");
|
|
911
766
|
ingested++;
|
|
@@ -930,7 +785,6 @@ async function ingestGemini(db, verbose) {
|
|
|
930
785
|
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
931
786
|
return { sessions: 0 };
|
|
932
787
|
}
|
|
933
|
-
const machineId = getMachineId();
|
|
934
788
|
let totalSessions = 0;
|
|
935
789
|
const touchedSessions = new Set;
|
|
936
790
|
let projectHashDirs = [];
|
|
@@ -981,8 +835,7 @@ async function ingestGemini(db, verbose) {
|
|
|
981
835
|
ended_at: chatData.lastUpdated ?? null,
|
|
982
836
|
total_cost_usd: 0,
|
|
983
837
|
total_tokens: 0,
|
|
984
|
-
request_count: 0
|
|
985
|
-
machine_id: machineId
|
|
838
|
+
request_count: 0
|
|
986
839
|
};
|
|
987
840
|
upsertSession(db, session);
|
|
988
841
|
touchedSessions.add(sessionId);
|
|
@@ -997,93 +850,11 @@ async function ingestGemini(db, verbose) {
|
|
|
997
850
|
return { sessions: totalSessions };
|
|
998
851
|
}
|
|
999
852
|
|
|
1000
|
-
// src/lib/package-metadata.ts
|
|
1001
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
1002
|
-
var cachedMetadata = null;
|
|
1003
|
-
function getPackageMetadata() {
|
|
1004
|
-
if (cachedMetadata)
|
|
1005
|
-
return cachedMetadata;
|
|
1006
|
-
const raw = readFileSync4(new URL("../../package.json", import.meta.url), "utf8");
|
|
1007
|
-
const parsed = JSON.parse(raw);
|
|
1008
|
-
cachedMetadata = {
|
|
1009
|
-
name: parsed.name ?? "@hasna/economy",
|
|
1010
|
-
version: parsed.version ?? "0.0.0"
|
|
1011
|
-
};
|
|
1012
|
-
return cachedMetadata;
|
|
1013
|
-
}
|
|
1014
|
-
var packageMetadata = getPackageMetadata();
|
|
1015
|
-
|
|
1016
853
|
// src/mcp/index.ts
|
|
1017
854
|
init_pricing();
|
|
1018
|
-
function printHelp() {
|
|
1019
|
-
console.log(`Usage: economy-mcp [options]
|
|
1020
|
-
|
|
1021
|
-
Runs the ${packageMetadata.name} MCP stdio server.
|
|
1022
|
-
|
|
1023
|
-
Options:
|
|
1024
|
-
-V, --version output the version number
|
|
1025
|
-
-h, --help display help for command`);
|
|
1026
|
-
}
|
|
1027
|
-
var args = process.argv.slice(2);
|
|
1028
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
1029
|
-
printHelp();
|
|
1030
|
-
process.exit(0);
|
|
1031
|
-
}
|
|
1032
|
-
if (args.includes("--version") || args.includes("-V")) {
|
|
1033
|
-
console.log(packageMetadata.version);
|
|
1034
|
-
process.exit(0);
|
|
1035
|
-
}
|
|
1036
855
|
var db = openDatabase();
|
|
1037
856
|
ensurePricingSeeded(db);
|
|
1038
|
-
var server = new
|
|
1039
|
-
name: "economy",
|
|
1040
|
-
version: packageMetadata.version
|
|
1041
|
-
});
|
|
1042
|
-
var _econAgents = new Map;
|
|
1043
|
-
var TOOL_NAMES = [
|
|
1044
|
-
"get_cost_summary",
|
|
1045
|
-
"get_sessions",
|
|
1046
|
-
"get_top_sessions",
|
|
1047
|
-
"get_model_breakdown",
|
|
1048
|
-
"get_project_breakdown",
|
|
1049
|
-
"get_budget_status",
|
|
1050
|
-
"get_daily",
|
|
1051
|
-
"get_session_detail",
|
|
1052
|
-
"sync",
|
|
1053
|
-
"search_tools",
|
|
1054
|
-
"describe_tools",
|
|
1055
|
-
"get_goals",
|
|
1056
|
-
"set_goal",
|
|
1057
|
-
"remove_goal",
|
|
1058
|
-
"list_machines",
|
|
1059
|
-
"register_agent",
|
|
1060
|
-
"heartbeat",
|
|
1061
|
-
"set_focus",
|
|
1062
|
-
"list_agents",
|
|
1063
|
-
"send_feedback"
|
|
1064
|
-
];
|
|
1065
|
-
var TOOL_DESCRIPTIONS = {
|
|
1066
|
-
get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
|
|
1067
|
-
get_sessions: "agent(claude|codex|gemini), project(partial), machine?(hostname), limit(20) -> compact session table",
|
|
1068
|
-
get_top_sessions: "n(10), agent(claude|codex|gemini) -> top sessions by cost",
|
|
1069
|
-
list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
|
|
1070
|
-
get_model_breakdown: "no params -> model, requests, tokens, cost",
|
|
1071
|
-
get_project_breakdown: "no params -> project_name, sessions, cost",
|
|
1072
|
-
get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
|
|
1073
|
-
get_daily: "days(30) -> daily cost table grouped by date and agent",
|
|
1074
|
-
get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
|
|
1075
|
-
sync: "sources(all|claude|codex|gemini) -> ingest latest cost data",
|
|
1076
|
-
search_tools: "query substring -> tool name list",
|
|
1077
|
-
describe_tools: "names[] -> one-line parameter hints",
|
|
1078
|
-
get_goals: "no params -> goal progress summary",
|
|
1079
|
-
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
|
|
1080
|
-
remove_goal: "id -> delete goal",
|
|
1081
|
-
register_agent: "name, session_id? -> register agent session",
|
|
1082
|
-
heartbeat: "agent_id -> update last_seen_at",
|
|
1083
|
-
set_focus: "agent_id, project_id? -> set active project context",
|
|
1084
|
-
list_agents: "no params -> registered agent list",
|
|
1085
|
-
send_feedback: "message, email?, category? -> save feedback locally"
|
|
1086
|
-
};
|
|
857
|
+
var server = new Server({ name: "economy", version: "0.2.2" }, { capabilities: { tools: {} } });
|
|
1087
858
|
var fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
1088
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);
|
|
1089
860
|
function fmtSession(s) {
|
|
@@ -1094,249 +865,262 @@ function fmtSession(s) {
|
|
|
1094
865
|
const tok = fmtTok(Number(s["total_tokens"] ?? 0));
|
|
1095
866
|
return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
|
|
1096
867
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
machine,
|
|
1137
|
-
limit: limit ?? 20
|
|
1138
|
-
});
|
|
1139
|
-
const lines = ["id agent cost tokens project"];
|
|
1140
|
-
for (const session of sessions)
|
|
1141
|
-
lines.push(fmtSession(session));
|
|
1142
|
-
return text(lines.join(`
|
|
1143
|
-
`));
|
|
1144
|
-
});
|
|
1145
|
-
server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
|
|
1146
|
-
n: z.number().int().positive().max(100).optional(),
|
|
1147
|
-
agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional()
|
|
1148
|
-
}, async ({ n, agent }) => {
|
|
1149
|
-
const sessions = queryTopSessions(db, n ?? 10, agent);
|
|
1150
|
-
const lines = ["rank id agent cost tokens project"];
|
|
1151
|
-
sessions.forEach((session, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(session)}`));
|
|
1152
|
-
return text(lines.join(`
|
|
1153
|
-
`));
|
|
1154
|
-
});
|
|
1155
|
-
server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
|
|
1156
|
-
const rows = queryModelBreakdown(db);
|
|
1157
|
-
const lines = ["model reqs tokens cost"];
|
|
1158
|
-
for (const row of rows) {
|
|
1159
|
-
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"]))}`);
|
|
1160
|
-
}
|
|
1161
|
-
return text(lines.join(`
|
|
1162
|
-
`));
|
|
1163
|
-
});
|
|
1164
|
-
server.tool("get_project_breakdown", "Cost per project. No params.", {}, async () => {
|
|
1165
|
-
const rows = queryProjectBreakdown(db);
|
|
1166
|
-
const lines = ["project sessions tokens cost"];
|
|
1167
|
-
for (const row of rows) {
|
|
1168
|
-
const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
|
|
1169
|
-
lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
|
|
1170
|
-
}
|
|
1171
|
-
return text(lines.join(`
|
|
1172
|
-
`));
|
|
1173
|
-
});
|
|
1174
|
-
server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
|
|
1175
|
-
const budgets = getBudgetStatuses(db);
|
|
1176
|
-
if (budgets.length === 0)
|
|
1177
|
-
return text("No budgets set.");
|
|
1178
|
-
const lines = ["scope period spent limit used% status"];
|
|
1179
|
-
for (const budget of budgets) {
|
|
1180
|
-
const scope = String(budget["project_path"] ?? "global").slice(0, 20);
|
|
1181
|
-
const pct = Number(budget["percent_used"]).toFixed(1);
|
|
1182
|
-
const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
|
|
1183
|
-
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}`);
|
|
1184
|
-
}
|
|
1185
|
-
return text(lines.join(`
|
|
1186
|
-
`));
|
|
1187
|
-
});
|
|
1188
|
-
server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
|
|
1189
|
-
const rows = queryDailyBreakdown(db, days ?? 30);
|
|
1190
|
-
const byDate = new Map;
|
|
1191
|
-
for (const row of rows) {
|
|
1192
|
-
const date = String(row["date"]);
|
|
1193
|
-
const entry = byDate.get(date) ?? { claude: 0, codex: 0, gemini: 0 };
|
|
1194
|
-
if (row["agent"] === "claude")
|
|
1195
|
-
entry.claude += Number(row["cost_usd"]);
|
|
1196
|
-
else if (row["agent"] === "codex")
|
|
1197
|
-
entry.codex += Number(row["cost_usd"]);
|
|
1198
|
-
else if (row["agent"] === "gemini")
|
|
1199
|
-
entry.gemini += Number(row["cost_usd"]);
|
|
1200
|
-
byDate.set(date, entry);
|
|
1201
|
-
}
|
|
1202
|
-
const lines = ["date claude codex gemini total"];
|
|
1203
|
-
for (const [date, costs] of [...byDate.entries()].sort()) {
|
|
1204
|
-
const total = costs.claude + costs.codex + costs.gemini;
|
|
1205
|
-
lines.push(`${date} ${fmtUsd(costs.claude).padEnd(11)}${fmtUsd(costs.codex).padEnd(11)}${fmtUsd(costs.gemini).padEnd(11)}${fmtUsd(total)}`);
|
|
1206
|
-
}
|
|
1207
|
-
return text(lines.join(`
|
|
1208
|
-
`));
|
|
1209
|
-
});
|
|
1210
|
-
server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
|
|
1211
|
-
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(session_id, `${session_id}%`);
|
|
1212
|
-
if (!session)
|
|
1213
|
-
return textError(`Session not found: ${session_id}`);
|
|
1214
|
-
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
|
|
1215
|
-
const lines = [
|
|
1216
|
-
`session: ${String(session["id"]).slice(0, 16)}`,
|
|
1217
|
-
`agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
|
|
1218
|
-
`cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
|
|
1219
|
-
"",
|
|
1220
|
-
"time model input output cost"
|
|
1221
|
-
];
|
|
1222
|
-
for (const request of requests) {
|
|
1223
|
-
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"]))}`);
|
|
1224
|
-
}
|
|
1225
|
-
return text(lines.join(`
|
|
1226
|
-
`));
|
|
1227
|
-
});
|
|
1228
|
-
server.tool("sync", "Ingest new cost data. sources: all|claude|takumi|codex|gemini", { sources: z.enum(["all", "claude", "takumi", "codex", "gemini"]).optional() }, async ({ sources }) => {
|
|
1229
|
-
const selected = sources ?? "all";
|
|
1230
|
-
const parts = [];
|
|
1231
|
-
if (selected === "all" || selected === "claude") {
|
|
1232
|
-
const result = await ingestClaude(db);
|
|
1233
|
-
parts.push(`claude: ${result["files"]} files, ${result["requests"]} requests, ${result["sessions"]} sessions`);
|
|
1234
|
-
}
|
|
1235
|
-
if (selected === "all" || selected === "takumi") {
|
|
1236
|
-
const result = await ingestTakumi(db);
|
|
1237
|
-
parts.push(`takumi: ${result["files"]} files, ${result["requests"]} requests, ${result["sessions"]} sessions`);
|
|
1238
|
-
}
|
|
1239
|
-
if (selected === "all" || selected === "codex") {
|
|
1240
|
-
const result = await ingestCodex(db);
|
|
1241
|
-
parts.push(`codex: ${result["sessions"]} sessions`);
|
|
1242
|
-
}
|
|
1243
|
-
if (selected === "all" || selected === "gemini") {
|
|
1244
|
-
const result = await ingestGemini(db);
|
|
1245
|
-
parts.push(`gemini: ${result["sessions"]} sessions`);
|
|
1246
|
-
}
|
|
1247
|
-
return text(parts.join(`
|
|
1248
|
-
`) || "done");
|
|
1249
|
-
});
|
|
1250
|
-
server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
|
|
1251
|
-
const goals = getGoalStatuses(db);
|
|
1252
|
-
if (goals.length === 0)
|
|
1253
|
-
return text("No goals set.");
|
|
1254
|
-
const lines = ["period scope limit spent used% status"];
|
|
1255
|
-
for (const goal of goals) {
|
|
1256
|
-
const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
|
|
1257
|
-
const pct = Number(goal["percent_used"]).toFixed(1);
|
|
1258
|
-
const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
|
|
1259
|
-
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}`);
|
|
1260
|
-
}
|
|
1261
|
-
return text(lines.join(`
|
|
1262
|
-
`));
|
|
1263
|
-
});
|
|
1264
|
-
server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
|
|
1265
|
-
period: z.enum(["day", "week", "month", "year"]),
|
|
1266
|
-
limit_usd: z.number().nonnegative(),
|
|
1267
|
-
project_path: z.string().optional(),
|
|
1268
|
-
agent: z.string().optional()
|
|
1269
|
-
}, async ({ period, limit_usd, project_path, agent }) => {
|
|
1270
|
-
const now = new Date().toISOString();
|
|
1271
|
-
upsertGoal(db, {
|
|
1272
|
-
id: randomUUID(),
|
|
1273
|
-
period,
|
|
1274
|
-
project_path: project_path ?? null,
|
|
1275
|
-
agent: agent ?? null,
|
|
1276
|
-
limit_usd,
|
|
1277
|
-
created_at: now,
|
|
1278
|
-
updated_at: now
|
|
1279
|
-
});
|
|
1280
|
-
return text(`Goal set: ${period} $${limit_usd}`);
|
|
1281
|
-
});
|
|
1282
|
-
server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
|
|
1283
|
-
deleteGoal(db, id);
|
|
1284
|
-
return text("Goal removed.");
|
|
1285
|
-
});
|
|
1286
|
-
server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
|
|
1287
|
-
const machines = listMachines(db);
|
|
1288
|
-
if (machines.length === 0)
|
|
1289
|
-
return text(`No machine data yet. Current machine: ${getMachineId()}`);
|
|
1290
|
-
const lines = ["machine sessions requests cost last_active"];
|
|
1291
|
-
for (const m of machines) {
|
|
1292
|
-
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"}`);
|
|
1293
|
-
}
|
|
1294
|
-
lines.push(`
|
|
1295
|
-
current machine: ${getMachineId()}`);
|
|
1296
|
-
return text(lines.join(`
|
|
1297
|
-
`));
|
|
1298
|
-
});
|
|
1299
|
-
server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
|
|
1300
|
-
const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
|
|
1301
|
-
if (existing) {
|
|
1302
|
-
existing.last_seen_at = new Date().toISOString();
|
|
1303
|
-
return text(JSON.stringify(existing));
|
|
1304
|
-
}
|
|
1305
|
-
const id = Math.random().toString(36).slice(2, 10);
|
|
1306
|
-
const agent = { id, name, last_seen_at: new Date().toISOString() };
|
|
1307
|
-
_econAgents.set(id, agent);
|
|
1308
|
-
return text(JSON.stringify(agent));
|
|
1309
|
-
});
|
|
1310
|
-
server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
|
|
1311
|
-
const agent = _econAgents.get(agent_id);
|
|
1312
|
-
if (!agent)
|
|
1313
|
-
return textError("Agent not found");
|
|
1314
|
-
agent.last_seen_at = new Date().toISOString();
|
|
1315
|
-
return text(`\u2665 ${agent.name}`);
|
|
1316
|
-
});
|
|
1317
|
-
server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
|
|
1318
|
-
const agent = _econAgents.get(agent_id);
|
|
1319
|
-
if (!agent)
|
|
1320
|
-
return textError("Agent not found");
|
|
1321
|
-
agent.project_id = project_id ?? undefined;
|
|
1322
|
-
return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
|
|
1323
|
-
});
|
|
1324
|
-
server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
|
|
1325
|
-
server.tool("send_feedback", "Send feedback about this service.", {
|
|
1326
|
-
message: z.string(),
|
|
1327
|
-
email: z.string().optional(),
|
|
1328
|
-
category: z.enum(["bug", "feature", "general"]).optional()
|
|
1329
|
-
}, 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 ?? {};
|
|
1330
907
|
try {
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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 };
|
|
1335
1122
|
}
|
|
1336
1123
|
});
|
|
1124
|
+
var _econAgents = new Map;
|
|
1337
1125
|
var transport = new StdioServerTransport;
|
|
1338
|
-
registerCloudTools(server, "economy", {
|
|
1339
|
-
dbPath: getDbPath(),
|
|
1340
|
-
migrations: PG_MIGRATIONS
|
|
1341
|
-
});
|
|
1342
1126
|
await server.connect(transport);
|