@hasna/economy 0.2.4 → 0.2.5
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/dist/mcp/index.js +295 -54
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -15,6 +15,7 @@ var __export = (target, all) => {
|
|
|
15
15
|
});
|
|
16
16
|
};
|
|
17
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
+
var __require = import.meta.require;
|
|
18
19
|
|
|
19
20
|
// src/lib/pricing.ts
|
|
20
21
|
var exports_pricing = {};
|
|
@@ -88,6 +89,10 @@ var init_pricing = __esm(() => {
|
|
|
88
89
|
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
|
|
89
90
|
"claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
90
91
|
"claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
|
|
92
|
+
"gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
93
|
+
"gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
94
|
+
"gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
95
|
+
"gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
|
|
91
96
|
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
92
97
|
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
93
98
|
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
@@ -174,6 +179,16 @@ function initSchema(db) {
|
|
|
174
179
|
updated_at TEXT NOT NULL
|
|
175
180
|
);
|
|
176
181
|
|
|
182
|
+
CREATE TABLE IF NOT EXISTS goals (
|
|
183
|
+
id TEXT PRIMARY KEY,
|
|
184
|
+
period TEXT NOT NULL,
|
|
185
|
+
project_path TEXT,
|
|
186
|
+
agent TEXT,
|
|
187
|
+
limit_usd REAL NOT NULL,
|
|
188
|
+
created_at TEXT NOT NULL,
|
|
189
|
+
updated_at TEXT NOT NULL
|
|
190
|
+
);
|
|
191
|
+
|
|
177
192
|
CREATE TABLE IF NOT EXISTS ingest_state (
|
|
178
193
|
source TEXT NOT NULL,
|
|
179
194
|
key TEXT NOT NULL,
|
|
@@ -206,6 +221,8 @@ function periodWhere(period) {
|
|
|
206
221
|
return `timestamp >= DATE('now', '-7 days')`;
|
|
207
222
|
case "month":
|
|
208
223
|
return `timestamp >= DATE('now', '-30 days')`;
|
|
224
|
+
case "year":
|
|
225
|
+
return `timestamp >= DATE('now', '-365 days')`;
|
|
209
226
|
case "all":
|
|
210
227
|
return "1=1";
|
|
211
228
|
}
|
|
@@ -218,6 +235,8 @@ function sessionPeriodWhere(period) {
|
|
|
218
235
|
return `started_at >= DATE('now', '-7 days')`;
|
|
219
236
|
case "month":
|
|
220
237
|
return `started_at >= DATE('now', '-30 days')`;
|
|
238
|
+
case "year":
|
|
239
|
+
return `started_at >= DATE('now', '-365 days')`;
|
|
221
240
|
case "all":
|
|
222
241
|
return "1=1";
|
|
223
242
|
}
|
|
@@ -267,6 +286,11 @@ function querySessions(db, filter = {}) {
|
|
|
267
286
|
conditions.push("started_at >= ?");
|
|
268
287
|
params.push(filter.since);
|
|
269
288
|
}
|
|
289
|
+
if (filter.search) {
|
|
290
|
+
const q = `%${filter.search}%`;
|
|
291
|
+
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
292
|
+
params.push(q, q, `${filter.search}%`);
|
|
293
|
+
}
|
|
270
294
|
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
271
295
|
const limit = filter.limit ?? 50;
|
|
272
296
|
const offset = filter.offset ?? 0;
|
|
@@ -319,16 +343,31 @@ function queryModelBreakdown(db) {
|
|
|
319
343
|
}
|
|
320
344
|
function queryProjectBreakdown(db) {
|
|
321
345
|
return db.prepare(`
|
|
322
|
-
SELECT
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
346
|
+
SELECT
|
|
347
|
+
s.project_path,
|
|
348
|
+
COALESCE(p.name, s.project_name) as project_name,
|
|
349
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
350
|
+
COUNT(r.id) as requests,
|
|
351
|
+
COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
|
|
352
|
+
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
353
|
+
MAX(s.started_at) as last_active
|
|
354
|
+
FROM sessions s
|
|
355
|
+
LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
|
|
356
|
+
LEFT JOIN requests r ON r.session_id = s.id
|
|
357
|
+
WHERE s.project_path != '' OR s.project_name != ''
|
|
358
|
+
GROUP BY s.project_path
|
|
359
|
+
ORDER BY cost_usd DESC
|
|
330
360
|
`).all();
|
|
331
361
|
}
|
|
362
|
+
function queryDailyBreakdown(db, days = 30) {
|
|
363
|
+
return db.prepare(`
|
|
364
|
+
SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
365
|
+
FROM requests
|
|
366
|
+
WHERE timestamp >= DATE('now', ? || ' days')
|
|
367
|
+
GROUP BY DATE(timestamp), agent
|
|
368
|
+
ORDER BY date ASC
|
|
369
|
+
`).all(`-${days}`);
|
|
370
|
+
}
|
|
332
371
|
function listBudgets(db) {
|
|
333
372
|
return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
|
|
334
373
|
}
|
|
@@ -358,6 +397,46 @@ function getBudgetStatuses(db) {
|
|
|
358
397
|
};
|
|
359
398
|
});
|
|
360
399
|
}
|
|
400
|
+
function upsertGoal(db, goal) {
|
|
401
|
+
db.prepare(`
|
|
402
|
+
INSERT OR REPLACE INTO goals
|
|
403
|
+
(id, period, project_path, agent, limit_usd, created_at, updated_at)
|
|
404
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
405
|
+
`).run(goal.id, goal.period, goal.project_path ?? null, goal.agent ?? null, goal.limit_usd, goal.created_at, goal.updated_at);
|
|
406
|
+
}
|
|
407
|
+
function deleteGoal(db, id) {
|
|
408
|
+
db.prepare(`DELETE FROM goals WHERE id = ?`).run(id);
|
|
409
|
+
}
|
|
410
|
+
function listGoals(db) {
|
|
411
|
+
return db.prepare(`SELECT * FROM goals ORDER BY created_at DESC`).all();
|
|
412
|
+
}
|
|
413
|
+
function getGoalStatuses(db) {
|
|
414
|
+
const goals = listGoals(db);
|
|
415
|
+
return goals.map((g) => {
|
|
416
|
+
const periodStart = g.period === "day" ? "DATE('now')" : g.period === "week" ? "DATE('now', '-7 days')" : g.period === "month" ? "DATE('now', '-30 days')" : "DATE('now', '-365 days')";
|
|
417
|
+
let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
|
|
418
|
+
const params = [];
|
|
419
|
+
if (g.project_path) {
|
|
420
|
+
spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
|
|
421
|
+
params.push(g.project_path);
|
|
422
|
+
}
|
|
423
|
+
if (g.agent) {
|
|
424
|
+
spendQuery += ` AND agent = ?`;
|
|
425
|
+
params.push(g.agent);
|
|
426
|
+
}
|
|
427
|
+
const row = db.prepare(spendQuery).get(...params);
|
|
428
|
+
const spend = row.spend;
|
|
429
|
+
const percent = g.limit_usd > 0 ? spend / g.limit_usd * 100 : 0;
|
|
430
|
+
return {
|
|
431
|
+
...g,
|
|
432
|
+
current_spend_usd: spend,
|
|
433
|
+
percent_used: percent,
|
|
434
|
+
is_on_track: percent < 70,
|
|
435
|
+
is_at_risk: percent >= 70 && percent <= 100,
|
|
436
|
+
is_over: percent > 100
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
}
|
|
361
440
|
function getIngestState(db, source, key) {
|
|
362
441
|
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
|
|
363
442
|
return row?.value ?? null;
|
|
@@ -405,6 +484,9 @@ init_pricing();
|
|
|
405
484
|
import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
|
|
406
485
|
import { homedir as homedir2 } from "os";
|
|
407
486
|
import { join as join2, basename } from "path";
|
|
487
|
+
function autoDetectProject(cwd, projects) {
|
|
488
|
+
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
489
|
+
}
|
|
408
490
|
var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
|
|
409
491
|
function dirNameToPath(dirName) {
|
|
410
492
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
@@ -433,11 +515,11 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
433
515
|
let totalFiles = 0;
|
|
434
516
|
let totalRequests = 0;
|
|
435
517
|
const touchedSessions = new Set;
|
|
518
|
+
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
436
519
|
const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
437
520
|
for (const projectDirEntry of projectDirs) {
|
|
438
521
|
const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
|
|
439
522
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
440
|
-
const projectName = basename(projectPath);
|
|
441
523
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
442
524
|
for (const filePath of jsonlFiles) {
|
|
443
525
|
const stateKey = filePath.replace(PROJECTS_DIR, "");
|
|
@@ -506,11 +588,13 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
506
588
|
if (!touchedSessions.has(sessionId)) {
|
|
507
589
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
508
590
|
if (!existing) {
|
|
591
|
+
const effectiveCwd = sessionCwd || projectPath;
|
|
592
|
+
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
509
593
|
const session = {
|
|
510
594
|
id: sessionId,
|
|
511
595
|
agent: "claude",
|
|
512
|
-
project_path:
|
|
513
|
-
project_name:
|
|
596
|
+
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
597
|
+
project_name: detectedProject ? detectedProject.name : "",
|
|
514
598
|
started_at: timestamp,
|
|
515
599
|
ended_at: null,
|
|
516
600
|
total_cost_usd: 0,
|
|
@@ -541,24 +625,12 @@ import { join as join3, basename as basename2 } from "path";
|
|
|
541
625
|
import { Database as Database2 } from "bun:sqlite";
|
|
542
626
|
var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
543
627
|
var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
544
|
-
function readCodexModel() {
|
|
545
|
-
if (!existsSync3(CODEX_CONFIG_PATH))
|
|
546
|
-
return "gpt-5.3-codex";
|
|
547
|
-
try {
|
|
548
|
-
const content = readFileSync2(CODEX_CONFIG_PATH, "utf-8");
|
|
549
|
-
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
550
|
-
return match?.[1] ?? "gpt-5.3-codex";
|
|
551
|
-
} catch {
|
|
552
|
-
return "gpt-5.3-codex";
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
628
|
async function ingestCodex(db, verbose = false) {
|
|
556
629
|
if (!existsSync3(CODEX_DB_PATH)) {
|
|
557
630
|
if (verbose)
|
|
558
631
|
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
559
632
|
return { sessions: 0 };
|
|
560
633
|
}
|
|
561
|
-
const model = readCodexModel();
|
|
562
634
|
let codexDb = null;
|
|
563
635
|
let ingested = 0;
|
|
564
636
|
try {
|
|
@@ -596,6 +668,83 @@ async function ingestCodex(db, verbose = false) {
|
|
|
596
668
|
return { sessions: ingested };
|
|
597
669
|
}
|
|
598
670
|
|
|
671
|
+
// src/ingest/gemini.ts
|
|
672
|
+
init_database();
|
|
673
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
674
|
+
import { homedir as homedir4 } from "os";
|
|
675
|
+
import { join as join4 } from "path";
|
|
676
|
+
var GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
|
|
677
|
+
async function ingestGemini(db, verbose) {
|
|
678
|
+
if (!existsSync4(GEMINI_TMP_DIR)) {
|
|
679
|
+
if (verbose)
|
|
680
|
+
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
681
|
+
return { sessions: 0 };
|
|
682
|
+
}
|
|
683
|
+
let totalSessions = 0;
|
|
684
|
+
const touchedSessions = new Set;
|
|
685
|
+
let projectHashDirs = [];
|
|
686
|
+
try {
|
|
687
|
+
projectHashDirs = readdirSync2(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join4(GEMINI_TMP_DIR, d.name));
|
|
688
|
+
} catch {
|
|
689
|
+
return { sessions: 0 };
|
|
690
|
+
}
|
|
691
|
+
for (const projectDir of projectHashDirs) {
|
|
692
|
+
const chatsDir = join4(projectDir, "chats");
|
|
693
|
+
if (!existsSync4(chatsDir))
|
|
694
|
+
continue;
|
|
695
|
+
let chatFiles = [];
|
|
696
|
+
try {
|
|
697
|
+
chatFiles = readdirSync2(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join4(chatsDir, f));
|
|
698
|
+
} catch {
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
for (const filePath of chatFiles) {
|
|
702
|
+
const stateKey = filePath.replace(homedir4(), "~");
|
|
703
|
+
let fileMtime = "0";
|
|
704
|
+
try {
|
|
705
|
+
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
706
|
+
} catch {
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
const processed = getIngestState(db, "gemini", stateKey);
|
|
710
|
+
if (processed === fileMtime)
|
|
711
|
+
continue;
|
|
712
|
+
let chatData;
|
|
713
|
+
try {
|
|
714
|
+
chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
715
|
+
} catch {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
const sessionId = chatData.sessionId;
|
|
719
|
+
if (!sessionId)
|
|
720
|
+
continue;
|
|
721
|
+
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
722
|
+
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
723
|
+
if (!existing) {
|
|
724
|
+
const session = {
|
|
725
|
+
id: sessionId,
|
|
726
|
+
agent: "gemini",
|
|
727
|
+
project_path: "",
|
|
728
|
+
project_name: "",
|
|
729
|
+
started_at: startTime,
|
|
730
|
+
ended_at: chatData.lastUpdated ?? null,
|
|
731
|
+
total_cost_usd: 0,
|
|
732
|
+
total_tokens: 0,
|
|
733
|
+
request_count: 0
|
|
734
|
+
};
|
|
735
|
+
upsertSession(db, session);
|
|
736
|
+
touchedSessions.add(sessionId);
|
|
737
|
+
totalSessions++;
|
|
738
|
+
}
|
|
739
|
+
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
for (const sessionId of touchedSessions) {
|
|
743
|
+
rollupSession(db, sessionId);
|
|
744
|
+
}
|
|
745
|
+
return { sessions: totalSessions };
|
|
746
|
+
}
|
|
747
|
+
|
|
599
748
|
// src/mcp/index.ts
|
|
600
749
|
init_pricing();
|
|
601
750
|
var db = openDatabase();
|
|
@@ -612,24 +761,37 @@ function fmtSession(s) {
|
|
|
612
761
|
return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
|
|
613
762
|
}
|
|
614
763
|
var TOOLS = [
|
|
615
|
-
{ name: "get_cost_summary", description: "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|all", inputSchema: { type: "object", properties: { period: { type: "string", enum: ["today", "week", "month", "all"] } } } },
|
|
764
|
+
{ 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"] } } } },
|
|
616
765
|
{ 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" } } } },
|
|
617
766
|
{ name: "get_top_sessions", description: "Top sessions by cost. Params: n(10), agent", inputSchema: { type: "object", properties: { n: { type: "number" }, agent: { type: "string" } } } },
|
|
618
767
|
{ name: "get_model_breakdown", description: "Cost per model. No params.", inputSchema: { type: "object", properties: {} } },
|
|
619
768
|
{ name: "get_project_breakdown", description: "Cost per project. No params.", inputSchema: { type: "object", properties: {} } },
|
|
620
769
|
{ name: "get_budget_status", description: "Budget limits vs spend, percent used, alert flags. No params.", inputSchema: { type: "object", properties: {} } },
|
|
621
|
-
{ name: "
|
|
770
|
+
{ name: "get_daily", description: "Daily cost table by agent. Params: days(30)", inputSchema: { type: "object", properties: { days: { type: "number" } } } },
|
|
771
|
+
{ 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"] } },
|
|
772
|
+
{ name: "sync", description: "Ingest new cost data. sources: all|claude|codex|gemini", inputSchema: { type: "object", properties: { sources: { type: "string", enum: ["all", "claude", "codex", "gemini"] } } } },
|
|
622
773
|
{ name: "search_tools", description: "List tool names matching query. Use first to find relevant tools.", inputSchema: { type: "object", properties: { query: { type: "string" } } } },
|
|
623
|
-
{ name: "describe_tools", description: "Get param hints for specific tools by name.", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" } } }, required: ["names"] } }
|
|
774
|
+
{ name: "describe_tools", description: "Get param hints for specific tools by name.", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" } } }, required: ["names"] } },
|
|
775
|
+
{ name: "get_goals", description: "All spending goals with current progress. No params.", inputSchema: { type: "object", properties: {} } },
|
|
776
|
+
{ 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"] } },
|
|
777
|
+
{ name: "remove_goal", description: "Delete a goal by id.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
778
|
+
{ name: "register_agent", description: "Register agent session.", inputSchema: { type: "object", properties: { name: { type: "string" }, session_id: { type: "string" } }, required: ["name"] } },
|
|
779
|
+
{ name: "heartbeat", description: "Update last_seen_at.", inputSchema: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] } },
|
|
780
|
+
{ name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } }
|
|
624
781
|
];
|
|
625
782
|
var TOOL_DESCRIPTIONS = {
|
|
626
|
-
get_cost_summary: "period(today|week|month|all) \u2192 {total_usd, sessions, requests, tokens, summary}",
|
|
783
|
+
get_cost_summary: "period(today|week|month|year|all) \u2192 {total_usd, sessions, requests, tokens, summary}",
|
|
627
784
|
get_sessions: "agent(claude|codex), project(partial), limit(20) \u2192 compact session table",
|
|
628
785
|
get_top_sessions: "n(10), agent(claude|codex) \u2192 top sessions by cost",
|
|
629
786
|
get_model_breakdown: "no params \u2192 model, requests, tokens, cost",
|
|
630
787
|
get_project_breakdown: "no params \u2192 project_name, sessions, cost",
|
|
631
788
|
get_budget_status: "no params \u2192 budget limits, current spend, percent_used, is_over_alert",
|
|
632
|
-
|
|
789
|
+
get_daily: "days(30) \u2192 daily cost table grouped by date and agent",
|
|
790
|
+
get_session_detail: "session_id(prefix ok) \u2192 per-request breakdown with model, tokens, cost",
|
|
791
|
+
sync: "sources(all|claude|codex|gemini) \u2192 {files, requests, sessions} ingested",
|
|
792
|
+
get_goals: "no params \u2192 period, scope, limit, spent, percent, status(ON TRACK/AT RISK/OVER)",
|
|
793
|
+
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? \u2192 creates/updates goal",
|
|
794
|
+
remove_goal: "id \u2192 deletes goal"
|
|
633
795
|
};
|
|
634
796
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
635
797
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
@@ -713,6 +875,48 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
713
875
|
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}`);
|
|
714
876
|
}
|
|
715
877
|
return { content: [{ type: "text", text: lines.join(`
|
|
878
|
+
`) }] };
|
|
879
|
+
}
|
|
880
|
+
case "get_daily": {
|
|
881
|
+
const days = Number(a["days"] ?? 30);
|
|
882
|
+
const rows = queryDailyBreakdown(db, days);
|
|
883
|
+
const lines = ["date claude codex gemini total"];
|
|
884
|
+
const byDate = new Map;
|
|
885
|
+
for (const r of rows) {
|
|
886
|
+
const d = String(r["date"]);
|
|
887
|
+
const entry = byDate.get(d) ?? { claude: 0, codex: 0, gemini: 0 };
|
|
888
|
+
if (r["agent"] === "claude")
|
|
889
|
+
entry.claude += Number(r["cost_usd"]);
|
|
890
|
+
else if (r["agent"] === "codex")
|
|
891
|
+
entry.codex += Number(r["cost_usd"]);
|
|
892
|
+
else if (r["agent"] === "gemini")
|
|
893
|
+
entry.gemini += Number(r["cost_usd"]);
|
|
894
|
+
byDate.set(d, entry);
|
|
895
|
+
}
|
|
896
|
+
for (const [date, costs] of [...byDate.entries()].sort()) {
|
|
897
|
+
const total = costs.claude + costs.codex + costs.gemini;
|
|
898
|
+
lines.push(`${date} ${fmtUsd(costs.claude).padEnd(11)}${fmtUsd(costs.codex).padEnd(11)}${fmtUsd(costs.gemini).padEnd(11)}${fmtUsd(total)}`);
|
|
899
|
+
}
|
|
900
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
901
|
+
`) }] };
|
|
902
|
+
}
|
|
903
|
+
case "get_session_detail": {
|
|
904
|
+
const sid = String(a["session_id"] ?? "");
|
|
905
|
+
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(sid, `${sid}%`);
|
|
906
|
+
if (!session)
|
|
907
|
+
return { content: [{ type: "text", text: `Session not found: ${sid}` }], isError: true };
|
|
908
|
+
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
|
|
909
|
+
const lines = [
|
|
910
|
+
`session: ${String(session["id"]).slice(0, 16)}`,
|
|
911
|
+
`agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
|
|
912
|
+
`cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
|
|
913
|
+
"",
|
|
914
|
+
"time model input output cost"
|
|
915
|
+
];
|
|
916
|
+
for (const r of requests) {
|
|
917
|
+
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"]))}`);
|
|
918
|
+
}
|
|
919
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
716
920
|
`) }] };
|
|
717
921
|
}
|
|
718
922
|
case "sync": {
|
|
@@ -726,9 +930,71 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
726
930
|
const r = await ingestCodex(db);
|
|
727
931
|
parts.push(`codex: ${r["sessions"]} sessions`);
|
|
728
932
|
}
|
|
933
|
+
if (sources === "all" || sources === "gemini") {
|
|
934
|
+
const r = await ingestGemini(db);
|
|
935
|
+
parts.push(`gemini: ${r["sessions"]} sessions`);
|
|
936
|
+
}
|
|
729
937
|
return { content: [{ type: "text", text: parts.join(`
|
|
730
938
|
`) || "done" }] };
|
|
731
939
|
}
|
|
940
|
+
case "get_goals": {
|
|
941
|
+
const goals = getGoalStatuses(db);
|
|
942
|
+
if (goals.length === 0)
|
|
943
|
+
return { content: [{ type: "text", text: "No goals set." }] };
|
|
944
|
+
const lines = ["period scope limit spent used% status"];
|
|
945
|
+
for (const g of goals) {
|
|
946
|
+
const scope = String(g["project_path"] ?? g["agent"] ?? "global").slice(0, 20);
|
|
947
|
+
const pct = Number(g["percent_used"]).toFixed(1);
|
|
948
|
+
const status = g["is_over"] ? "OVER" : g["is_at_risk"] ? "AT RISK" : "ON TRACK";
|
|
949
|
+
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}`);
|
|
950
|
+
}
|
|
951
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
952
|
+
`) }] };
|
|
953
|
+
}
|
|
954
|
+
case "set_goal": {
|
|
955
|
+
const { randomUUID } = await import("crypto");
|
|
956
|
+
const now = new Date().toISOString();
|
|
957
|
+
upsertGoal(db, {
|
|
958
|
+
id: randomUUID(),
|
|
959
|
+
period: String(a["period"] ?? "month"),
|
|
960
|
+
project_path: a["project_path"] ?? null,
|
|
961
|
+
agent: a["agent"] ?? null,
|
|
962
|
+
limit_usd: Number(a["limit_usd"]),
|
|
963
|
+
created_at: now,
|
|
964
|
+
updated_at: now
|
|
965
|
+
});
|
|
966
|
+
return { content: [{ type: "text", text: `Goal set: ${a["period"]} $${a["limit_usd"]}` }] };
|
|
967
|
+
}
|
|
968
|
+
case "remove_goal": {
|
|
969
|
+
deleteGoal(db, String(a["id"] ?? ""));
|
|
970
|
+
return { content: [{ type: "text", text: "Goal removed." }] };
|
|
971
|
+
}
|
|
972
|
+
case "register_agent": {
|
|
973
|
+
const n = String(args["name"] ?? "");
|
|
974
|
+
const ex = [..._econAgents.values()].find((x) => x.name === n);
|
|
975
|
+
if (ex) {
|
|
976
|
+
ex.last_seen_at = new Date().toISOString();
|
|
977
|
+
return { content: [{ type: "text", text: JSON.stringify(ex) }] };
|
|
978
|
+
}
|
|
979
|
+
const id = Math.random().toString(36).slice(2, 10);
|
|
980
|
+
const ag = { id, name: n, last_seen_at: new Date().toISOString() };
|
|
981
|
+
_econAgents.set(id, ag);
|
|
982
|
+
return { content: [{ type: "text", text: JSON.stringify(ag) }] };
|
|
983
|
+
}
|
|
984
|
+
case "heartbeat": {
|
|
985
|
+
const ag = _econAgents.get(String(args["agent_id"] ?? ""));
|
|
986
|
+
if (!ag)
|
|
987
|
+
return { content: [{ type: "text", text: `Agent not found` }], isError: true };
|
|
988
|
+
ag.last_seen_at = new Date().toISOString();
|
|
989
|
+
return { content: [{ type: "text", text: `\u2665 ${ag.name}` }] };
|
|
990
|
+
}
|
|
991
|
+
case "set_focus": {
|
|
992
|
+
const ag = _econAgents.get(String(args["agent_id"] ?? ""));
|
|
993
|
+
if (!ag)
|
|
994
|
+
return { content: [{ type: "text", text: `Agent not found` }], isError: true };
|
|
995
|
+
ag["project_id"] = args["project_id"];
|
|
996
|
+
return { content: [{ type: "text", text: String(args["project_id"] ? `Focus: ${args["project_id"]}` : "Focus cleared") }] };
|
|
997
|
+
}
|
|
732
998
|
default:
|
|
733
999
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
734
1000
|
}
|
|
@@ -736,31 +1002,6 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
736
1002
|
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
|
|
737
1003
|
}
|
|
738
1004
|
});
|
|
739
|
-
var
|
|
740
|
-
server.tool("register_agent", "Register this agent session. Returns agent_id for use in heartbeat/set_focus.", { name: z.string(), session_id: z.string().optional() }, async (a) => {
|
|
741
|
-
const existing = [..._agentReg.values()].find((x) => x.name === a.name);
|
|
742
|
-
if (existing) {
|
|
743
|
-
existing.last_seen_at = new Date().toISOString();
|
|
744
|
-
return { content: [{ type: "text", text: JSON.stringify(existing) }] };
|
|
745
|
-
}
|
|
746
|
-
const id = Math.random().toString(36).slice(2, 10);
|
|
747
|
-
const ag = { id, name: a.name, last_seen_at: new Date().toISOString() };
|
|
748
|
-
_agentReg.set(id, ag);
|
|
749
|
-
return { content: [{ type: "text", text: JSON.stringify(ag) }] };
|
|
750
|
-
});
|
|
751
|
-
server.tool("heartbeat", "Update last_seen_at to signal agent is active.", { agent_id: z.string() }, async (a) => {
|
|
752
|
-
const ag = _agentReg.get(a.agent_id);
|
|
753
|
-
if (!ag)
|
|
754
|
-
return { content: [{ type: "text", text: `Agent not found: ${a.agent_id}` }], isError: true };
|
|
755
|
-
ag.last_seen_at = new Date().toISOString();
|
|
756
|
-
return { content: [{ type: "text", text: `\u2665 ${ag.name} \u2014 active` }] };
|
|
757
|
-
});
|
|
758
|
-
server.tool("set_focus", "Set active project context for this agent session.", { agent_id: z.string(), project_id: z.string().optional() }, async (a) => {
|
|
759
|
-
const ag = _agentReg.get(a.agent_id);
|
|
760
|
-
if (!ag)
|
|
761
|
-
return { content: [{ type: "text", text: `Agent not found: ${a.agent_id}` }], isError: true };
|
|
762
|
-
ag.project_id = a.project_id;
|
|
763
|
-
return { content: [{ type: "text", text: a.project_id ? `Focus: ${a.project_id}` : "Focus cleared" }] };
|
|
764
|
-
});
|
|
1005
|
+
var _econAgents = new Map;
|
|
765
1006
|
var transport = new StdioServerTransport;
|
|
766
1007
|
await server.connect(transport);
|
package/package.json
CHANGED