@hasna/economy 0.2.10 → 0.2.12
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/README.md +37 -45
- package/dist/cli/index.js +228 -100
- package/dist/db/database.d.ts +10 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/index.js +64 -17
- package/dist/ingest/claude.d.ts +1 -1
- 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/package-metadata.d.ts +8 -0
- package/dist/lib/package-metadata.d.ts.map +1 -0
- 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 +386 -347
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +237 -26
- package/dist/server/serve.d.ts +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +6 -5
package/dist/mcp/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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);
|
|
5
4
|
var __returnValue = (v) => v;
|
|
6
5
|
function __exportSetter(name, newValue) {
|
|
7
6
|
this[name] = __returnValue.bind(null, newValue);
|
|
@@ -16,7 +15,6 @@ var __export = (target, all) => {
|
|
|
16
15
|
});
|
|
17
16
|
};
|
|
18
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
19
|
-
var __require = import.meta.require;
|
|
20
18
|
|
|
21
19
|
// src/lib/pricing.ts
|
|
22
20
|
var exports_pricing = {};
|
|
@@ -110,8 +108,17 @@ var init_pricing = __esm(() => {
|
|
|
110
108
|
// src/db/database.ts
|
|
111
109
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
112
110
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
111
|
+
import { hostname } from "os";
|
|
113
112
|
import { homedir } from "os";
|
|
114
113
|
import { join } from "path";
|
|
114
|
+
function getMachineId() {
|
|
115
|
+
if (process.env["ECONOMY_MACHINE_ID"])
|
|
116
|
+
return process.env["ECONOMY_MACHINE_ID"];
|
|
117
|
+
const h = hostname().toLowerCase();
|
|
118
|
+
if (h.startsWith("spark") || h.startsWith("apple"))
|
|
119
|
+
return h.split(".")[0];
|
|
120
|
+
return h.split(".")[0];
|
|
121
|
+
}
|
|
115
122
|
function getDataDir() {
|
|
116
123
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
117
124
|
const newDir = join(home, ".hasna", "economy");
|
|
@@ -144,6 +151,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
144
151
|
}
|
|
145
152
|
const db = new Database(path);
|
|
146
153
|
db.exec("PRAGMA journal_mode = WAL");
|
|
154
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
147
155
|
db.exec("PRAGMA foreign_keys = ON");
|
|
148
156
|
initSchema(db);
|
|
149
157
|
if (!skipSeed) {
|
|
@@ -165,7 +173,8 @@ function initSchema(db) {
|
|
|
165
173
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
166
174
|
duration_ms INTEGER DEFAULT 0,
|
|
167
175
|
timestamp TEXT NOT NULL,
|
|
168
|
-
source_request_id TEXT
|
|
176
|
+
source_request_id TEXT,
|
|
177
|
+
machine_id TEXT DEFAULT ''
|
|
169
178
|
);
|
|
170
179
|
|
|
171
180
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -177,7 +186,8 @@ function initSchema(db) {
|
|
|
177
186
|
ended_at TEXT,
|
|
178
187
|
total_cost_usd REAL DEFAULT 0,
|
|
179
188
|
total_tokens INTEGER DEFAULT 0,
|
|
180
|
-
request_count INTEGER DEFAULT 0
|
|
189
|
+
request_count INTEGER DEFAULT 0,
|
|
190
|
+
machine_id TEXT DEFAULT ''
|
|
181
191
|
);
|
|
182
192
|
|
|
183
193
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -243,6 +253,15 @@ function initSchema(db) {
|
|
|
243
253
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
244
254
|
);
|
|
245
255
|
`);
|
|
256
|
+
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
257
|
+
if (!cols.some((c) => c.name === "machine_id")) {
|
|
258
|
+
db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
259
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
260
|
+
}
|
|
261
|
+
db.exec(`
|
|
262
|
+
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
263
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
264
|
+
`);
|
|
246
265
|
}
|
|
247
266
|
function periodWhere(period) {
|
|
248
267
|
switch (period) {
|
|
@@ -281,17 +300,17 @@ function upsertRequest(db, req) {
|
|
|
281
300
|
INSERT OR REPLACE INTO requests
|
|
282
301
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
283
302
|
cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
|
|
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);
|
|
303
|
+
timestamp, source_request_id, machine_id)
|
|
304
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
305
|
+
`).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, req.machine_id ?? "");
|
|
287
306
|
}
|
|
288
307
|
function upsertSession(db, session) {
|
|
289
308
|
db.prepare(`
|
|
290
309
|
INSERT OR REPLACE INTO sessions
|
|
291
310
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
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);
|
|
311
|
+
total_cost_usd, total_tokens, request_count, machine_id)
|
|
312
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
313
|
+
`).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, session.machine_id ?? "");
|
|
295
314
|
}
|
|
296
315
|
function rollupSession(db, sessionId) {
|
|
297
316
|
db.prepare(`
|
|
@@ -321,6 +340,10 @@ function querySessions(db, filter = {}) {
|
|
|
321
340
|
conditions.push("started_at >= ?");
|
|
322
341
|
params.push(filter.since);
|
|
323
342
|
}
|
|
343
|
+
if (filter.machine) {
|
|
344
|
+
conditions.push("machine_id = ?");
|
|
345
|
+
params.push(filter.machine);
|
|
346
|
+
}
|
|
324
347
|
if (filter.search) {
|
|
325
348
|
const q = `%${filter.search}%`;
|
|
326
349
|
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
@@ -339,24 +362,25 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
339
362
|
}
|
|
340
363
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
341
364
|
}
|
|
342
|
-
function querySummary(db, period) {
|
|
365
|
+
function querySummary(db, period, machine) {
|
|
343
366
|
const rWhere = periodWhere(period);
|
|
344
367
|
const sWhere = sessionPeriodWhere(period);
|
|
368
|
+
const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
345
369
|
const r = db.prepare(`
|
|
346
370
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
347
371
|
COUNT(*) as requests,
|
|
348
372
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
349
|
-
FROM requests WHERE ${rWhere}
|
|
373
|
+
FROM requests WHERE ${rWhere}${machineClause}
|
|
350
374
|
`).get();
|
|
351
375
|
const codexTotals = db.prepare(`
|
|
352
376
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
353
377
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
354
378
|
COUNT(*) as sessions
|
|
355
379
|
FROM sessions
|
|
356
|
-
WHERE ${sWhere}
|
|
380
|
+
WHERE ${sWhere}${machineClause}
|
|
357
381
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
358
382
|
`).get();
|
|
359
|
-
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
|
|
383
|
+
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
|
|
360
384
|
return {
|
|
361
385
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
362
386
|
requests: r.requests,
|
|
@@ -479,6 +503,20 @@ function getIngestState(db, source, key) {
|
|
|
479
503
|
function setIngestState(db, source, key, value) {
|
|
480
504
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
481
505
|
}
|
|
506
|
+
function listMachines(db) {
|
|
507
|
+
return db.prepare(`
|
|
508
|
+
SELECT
|
|
509
|
+
s.machine_id,
|
|
510
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
511
|
+
COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
|
|
512
|
+
COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
|
|
513
|
+
MAX(s.started_at) as last_active
|
|
514
|
+
FROM sessions s
|
|
515
|
+
WHERE s.machine_id != ''
|
|
516
|
+
GROUP BY s.machine_id
|
|
517
|
+
ORDER BY total_cost_usd DESC
|
|
518
|
+
`).all();
|
|
519
|
+
}
|
|
482
520
|
function upsertModelPricing(db, p) {
|
|
483
521
|
db.prepare(`
|
|
484
522
|
INSERT OR REPLACE INTO model_pricing
|
|
@@ -507,81 +545,13 @@ function seedModelPricing(db, defaults) {
|
|
|
507
545
|
}
|
|
508
546
|
var init_database = () => {};
|
|
509
547
|
|
|
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
|
-
|
|
580
548
|
// src/mcp/index.ts
|
|
581
549
|
init_database();
|
|
582
|
-
import {
|
|
550
|
+
import { randomUUID } from "crypto";
|
|
551
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
583
552
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
584
|
-
import {
|
|
553
|
+
import { registerCloudTools } from "@hasna/cloud";
|
|
554
|
+
import { z } from "zod";
|
|
585
555
|
|
|
586
556
|
// src/ingest/claude.ts
|
|
587
557
|
init_database();
|
|
@@ -617,6 +587,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
617
587
|
console.log("Claude projects dir not found:", PROJECTS_DIR);
|
|
618
588
|
return { files: 0, requests: 0, sessions: 0 };
|
|
619
589
|
}
|
|
590
|
+
const machineId = getMachineId();
|
|
620
591
|
let totalFiles = 0;
|
|
621
592
|
let totalRequests = 0;
|
|
622
593
|
const touchedSessions = new Set;
|
|
@@ -688,7 +659,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
688
659
|
cost_usd: costUsd,
|
|
689
660
|
duration_ms: 0,
|
|
690
661
|
timestamp,
|
|
691
|
-
source_request_id: reqId
|
|
662
|
+
source_request_id: reqId,
|
|
663
|
+
machine_id: machineId
|
|
692
664
|
});
|
|
693
665
|
if (!touchedSessions.has(sessionId)) {
|
|
694
666
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
@@ -704,7 +676,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
704
676
|
ended_at: null,
|
|
705
677
|
total_cost_usd: 0,
|
|
706
678
|
total_tokens: 0,
|
|
707
|
-
request_count: 0
|
|
679
|
+
request_count: 0,
|
|
680
|
+
machine_id: machineId
|
|
708
681
|
};
|
|
709
682
|
upsertSession(db, session);
|
|
710
683
|
}
|
|
@@ -727,7 +700,7 @@ init_database();
|
|
|
727
700
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
728
701
|
import { homedir as homedir3 } from "os";
|
|
729
702
|
import { join as join3, basename as basename2 } from "path";
|
|
730
|
-
import { Database as
|
|
703
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
731
704
|
var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
732
705
|
var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
733
706
|
async function ingestCodex(db, verbose = false) {
|
|
@@ -736,10 +709,11 @@ async function ingestCodex(db, verbose = false) {
|
|
|
736
709
|
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
737
710
|
return { sessions: 0 };
|
|
738
711
|
}
|
|
712
|
+
const machineId = getMachineId();
|
|
739
713
|
let codexDb = null;
|
|
740
714
|
let ingested = 0;
|
|
741
715
|
try {
|
|
742
|
-
codexDb = new
|
|
716
|
+
codexDb = new BunDatabase(CODEX_DB_PATH, { readonly: true });
|
|
743
717
|
const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
|
|
744
718
|
for (const thread of threads) {
|
|
745
719
|
const stateKey = thread.id;
|
|
@@ -760,7 +734,8 @@ async function ingestCodex(db, verbose = false) {
|
|
|
760
734
|
ended_at: endedAt,
|
|
761
735
|
total_cost_usd: costUsd,
|
|
762
736
|
total_tokens: thread.tokens_used,
|
|
763
|
-
request_count: 1
|
|
737
|
+
request_count: 1,
|
|
738
|
+
machine_id: machineId
|
|
764
739
|
});
|
|
765
740
|
setIngestState(db, "codex", stateKey, "done");
|
|
766
741
|
ingested++;
|
|
@@ -785,6 +760,7 @@ async function ingestGemini(db, verbose) {
|
|
|
785
760
|
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
786
761
|
return { sessions: 0 };
|
|
787
762
|
}
|
|
763
|
+
const machineId = getMachineId();
|
|
788
764
|
let totalSessions = 0;
|
|
789
765
|
const touchedSessions = new Set;
|
|
790
766
|
let projectHashDirs = [];
|
|
@@ -835,7 +811,8 @@ async function ingestGemini(db, verbose) {
|
|
|
835
811
|
ended_at: chatData.lastUpdated ?? null,
|
|
836
812
|
total_cost_usd: 0,
|
|
837
813
|
total_tokens: 0,
|
|
838
|
-
request_count: 0
|
|
814
|
+
request_count: 0,
|
|
815
|
+
machine_id: machineId
|
|
839
816
|
};
|
|
840
817
|
upsertSession(db, session);
|
|
841
818
|
touchedSessions.add(sessionId);
|
|
@@ -850,11 +827,93 @@ async function ingestGemini(db, verbose) {
|
|
|
850
827
|
return { sessions: totalSessions };
|
|
851
828
|
}
|
|
852
829
|
|
|
830
|
+
// src/lib/package-metadata.ts
|
|
831
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
832
|
+
var cachedMetadata = null;
|
|
833
|
+
function getPackageMetadata() {
|
|
834
|
+
if (cachedMetadata)
|
|
835
|
+
return cachedMetadata;
|
|
836
|
+
const raw = readFileSync4(new URL("../../package.json", import.meta.url), "utf8");
|
|
837
|
+
const parsed = JSON.parse(raw);
|
|
838
|
+
cachedMetadata = {
|
|
839
|
+
name: parsed.name ?? "@hasna/economy",
|
|
840
|
+
version: parsed.version ?? "0.0.0"
|
|
841
|
+
};
|
|
842
|
+
return cachedMetadata;
|
|
843
|
+
}
|
|
844
|
+
var packageMetadata = getPackageMetadata();
|
|
845
|
+
|
|
853
846
|
// src/mcp/index.ts
|
|
854
847
|
init_pricing();
|
|
848
|
+
function printHelp() {
|
|
849
|
+
console.log(`Usage: economy-mcp [options]
|
|
850
|
+
|
|
851
|
+
Runs the ${packageMetadata.name} MCP stdio server.
|
|
852
|
+
|
|
853
|
+
Options:
|
|
854
|
+
-V, --version output the version number
|
|
855
|
+
-h, --help display help for command`);
|
|
856
|
+
}
|
|
857
|
+
var args = process.argv.slice(2);
|
|
858
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
859
|
+
printHelp();
|
|
860
|
+
process.exit(0);
|
|
861
|
+
}
|
|
862
|
+
if (args.includes("--version") || args.includes("-V")) {
|
|
863
|
+
console.log(packageMetadata.version);
|
|
864
|
+
process.exit(0);
|
|
865
|
+
}
|
|
855
866
|
var db = openDatabase();
|
|
856
867
|
ensurePricingSeeded(db);
|
|
857
|
-
var server = new
|
|
868
|
+
var server = new McpServer({
|
|
869
|
+
name: "economy",
|
|
870
|
+
version: packageMetadata.version
|
|
871
|
+
});
|
|
872
|
+
var _econAgents = new Map;
|
|
873
|
+
var TOOL_NAMES = [
|
|
874
|
+
"get_cost_summary",
|
|
875
|
+
"get_sessions",
|
|
876
|
+
"get_top_sessions",
|
|
877
|
+
"get_model_breakdown",
|
|
878
|
+
"get_project_breakdown",
|
|
879
|
+
"get_budget_status",
|
|
880
|
+
"get_daily",
|
|
881
|
+
"get_session_detail",
|
|
882
|
+
"sync",
|
|
883
|
+
"search_tools",
|
|
884
|
+
"describe_tools",
|
|
885
|
+
"get_goals",
|
|
886
|
+
"set_goal",
|
|
887
|
+
"remove_goal",
|
|
888
|
+
"list_machines",
|
|
889
|
+
"register_agent",
|
|
890
|
+
"heartbeat",
|
|
891
|
+
"set_focus",
|
|
892
|
+
"list_agents",
|
|
893
|
+
"send_feedback"
|
|
894
|
+
];
|
|
895
|
+
var TOOL_DESCRIPTIONS = {
|
|
896
|
+
get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
|
|
897
|
+
get_sessions: "agent(claude|codex|gemini), project(partial), machine?(hostname), limit(20) -> compact session table",
|
|
898
|
+
get_top_sessions: "n(10), agent(claude|codex|gemini) -> top sessions by cost",
|
|
899
|
+
list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
|
|
900
|
+
get_model_breakdown: "no params -> model, requests, tokens, cost",
|
|
901
|
+
get_project_breakdown: "no params -> project_name, sessions, cost",
|
|
902
|
+
get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
|
|
903
|
+
get_daily: "days(30) -> daily cost table grouped by date and agent",
|
|
904
|
+
get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
|
|
905
|
+
sync: "sources(all|claude|codex|gemini) -> ingest latest cost data",
|
|
906
|
+
search_tools: "query substring -> tool name list",
|
|
907
|
+
describe_tools: "names[] -> one-line parameter hints",
|
|
908
|
+
get_goals: "no params -> goal progress summary",
|
|
909
|
+
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
|
|
910
|
+
remove_goal: "id -> delete goal",
|
|
911
|
+
register_agent: "name, session_id? -> register agent session",
|
|
912
|
+
heartbeat: "agent_id -> update last_seen_at",
|
|
913
|
+
set_focus: "agent_id, project_id? -> set active project context",
|
|
914
|
+
list_agents: "no params -> registered agent list",
|
|
915
|
+
send_feedback: "message, email?, category? -> save feedback locally"
|
|
916
|
+
};
|
|
858
917
|
var fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
859
918
|
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);
|
|
860
919
|
function fmtSession(s) {
|
|
@@ -865,262 +924,242 @@ function fmtSession(s) {
|
|
|
865
924
|
const tok = fmtTok(Number(s["total_tokens"] ?? 0));
|
|
866
925
|
return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
|
|
867
926
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
{
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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 ?? {};
|
|
907
|
-
try {
|
|
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(`
|
|
927
|
+
function text(text2) {
|
|
928
|
+
return { content: [{ type: "text", text: text2 }] };
|
|
929
|
+
}
|
|
930
|
+
function textError(message) {
|
|
931
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
932
|
+
}
|
|
933
|
+
server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z.string().optional() }, async ({ query }) => {
|
|
934
|
+
const q = query?.toLowerCase();
|
|
935
|
+
const matches = q ? TOOL_NAMES.filter((name) => name.includes(q)) : [...TOOL_NAMES];
|
|
936
|
+
return text(matches.join(", "));
|
|
937
|
+
});
|
|
938
|
+
server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z.array(z.string()) }, async ({ names }) => {
|
|
939
|
+
const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
|
|
932
940
|
`);
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
`) }
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
`)
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
`
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
return
|
|
941
|
+
return text(result);
|
|
942
|
+
});
|
|
943
|
+
server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all. machine: filter by hostname.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), machine: z.string().optional() }, async ({ period, machine }) => {
|
|
944
|
+
const resolved = period ?? "today";
|
|
945
|
+
const s = querySummary(db, resolved, machine);
|
|
946
|
+
const machineLabel = machine ? ` on ${machine}` : "";
|
|
947
|
+
return text([
|
|
948
|
+
`period: ${resolved}${machineLabel}`,
|
|
949
|
+
`cost: ${fmtUsd(s.total_usd)}`,
|
|
950
|
+
`sessions: ${s.sessions}`,
|
|
951
|
+
`requests: ${s.requests.toLocaleString()}`,
|
|
952
|
+
`tokens: ${fmtTok(s.tokens)}`,
|
|
953
|
+
`summary: You've spent ${fmtUsd(s.total_usd)} ${resolved === "all" ? "total" : resolved}${machineLabel} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
|
|
954
|
+
].join(`
|
|
955
|
+
`));
|
|
956
|
+
});
|
|
957
|
+
server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, machine, limit(20)", {
|
|
958
|
+
agent: z.enum(["claude", "codex", "gemini"]).optional(),
|
|
959
|
+
project: z.string().optional(),
|
|
960
|
+
machine: z.string().optional(),
|
|
961
|
+
limit: z.number().int().positive().max(100).optional()
|
|
962
|
+
}, async ({ agent, project, machine, limit }) => {
|
|
963
|
+
const sessions = querySessions(db, {
|
|
964
|
+
agent,
|
|
965
|
+
project,
|
|
966
|
+
machine,
|
|
967
|
+
limit: limit ?? 20
|
|
968
|
+
});
|
|
969
|
+
const lines = ["id agent cost tokens project"];
|
|
970
|
+
for (const session of sessions)
|
|
971
|
+
lines.push(fmtSession(session));
|
|
972
|
+
return text(lines.join(`
|
|
973
|
+
`));
|
|
974
|
+
});
|
|
975
|
+
server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
|
|
976
|
+
n: z.number().int().positive().max(100).optional(),
|
|
977
|
+
agent: z.enum(["claude", "codex", "gemini"]).optional()
|
|
978
|
+
}, async ({ n, agent }) => {
|
|
979
|
+
const sessions = queryTopSessions(db, n ?? 10, agent);
|
|
980
|
+
const lines = ["rank id agent cost tokens project"];
|
|
981
|
+
sessions.forEach((session, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(session)}`));
|
|
982
|
+
return text(lines.join(`
|
|
983
|
+
`));
|
|
984
|
+
});
|
|
985
|
+
server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
|
|
986
|
+
const rows = queryModelBreakdown(db);
|
|
987
|
+
const lines = ["model reqs tokens cost"];
|
|
988
|
+
for (const row of rows) {
|
|
989
|
+
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"]))}`);
|
|
990
|
+
}
|
|
991
|
+
return text(lines.join(`
|
|
992
|
+
`));
|
|
993
|
+
});
|
|
994
|
+
server.tool("get_project_breakdown", "Cost per project. No params.", {}, async () => {
|
|
995
|
+
const rows = queryProjectBreakdown(db);
|
|
996
|
+
const lines = ["project sessions tokens cost"];
|
|
997
|
+
for (const row of rows) {
|
|
998
|
+
const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
|
|
999
|
+
lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
|
|
1000
|
+
}
|
|
1001
|
+
return text(lines.join(`
|
|
1002
|
+
`));
|
|
1003
|
+
});
|
|
1004
|
+
server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
|
|
1005
|
+
const budgets = getBudgetStatuses(db);
|
|
1006
|
+
if (budgets.length === 0)
|
|
1007
|
+
return text("No budgets set.");
|
|
1008
|
+
const lines = ["scope period spent limit used% status"];
|
|
1009
|
+
for (const budget of budgets) {
|
|
1010
|
+
const scope = String(budget["project_path"] ?? "global").slice(0, 20);
|
|
1011
|
+
const pct = Number(budget["percent_used"]).toFixed(1);
|
|
1012
|
+
const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
|
|
1013
|
+
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}`);
|
|
1014
|
+
}
|
|
1015
|
+
return text(lines.join(`
|
|
1016
|
+
`));
|
|
1017
|
+
});
|
|
1018
|
+
server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
|
|
1019
|
+
const rows = queryDailyBreakdown(db, days ?? 30);
|
|
1020
|
+
const byDate = new Map;
|
|
1021
|
+
for (const row of rows) {
|
|
1022
|
+
const date = String(row["date"]);
|
|
1023
|
+
const entry = byDate.get(date) ?? { claude: 0, codex: 0, gemini: 0 };
|
|
1024
|
+
if (row["agent"] === "claude")
|
|
1025
|
+
entry.claude += Number(row["cost_usd"]);
|
|
1026
|
+
else if (row["agent"] === "codex")
|
|
1027
|
+
entry.codex += Number(row["cost_usd"]);
|
|
1028
|
+
else if (row["agent"] === "gemini")
|
|
1029
|
+
entry.gemini += Number(row["cost_usd"]);
|
|
1030
|
+
byDate.set(date, entry);
|
|
1031
|
+
}
|
|
1032
|
+
const lines = ["date claude codex gemini total"];
|
|
1033
|
+
for (const [date, costs] of [...byDate.entries()].sort()) {
|
|
1034
|
+
const total = costs.claude + costs.codex + costs.gemini;
|
|
1035
|
+
lines.push(`${date} ${fmtUsd(costs.claude).padEnd(11)}${fmtUsd(costs.codex).padEnd(11)}${fmtUsd(costs.gemini).padEnd(11)}${fmtUsd(total)}`);
|
|
1036
|
+
}
|
|
1037
|
+
return text(lines.join(`
|
|
1038
|
+
`));
|
|
1039
|
+
});
|
|
1040
|
+
server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
|
|
1041
|
+
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(session_id, `${session_id}%`);
|
|
1042
|
+
if (!session)
|
|
1043
|
+
return textError(`Session not found: ${session_id}`);
|
|
1044
|
+
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
|
|
1045
|
+
const lines = [
|
|
1046
|
+
`session: ${String(session["id"]).slice(0, 16)}`,
|
|
1047
|
+
`agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
|
|
1048
|
+
`cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
|
|
1049
|
+
"",
|
|
1050
|
+
"time model input output cost"
|
|
1051
|
+
];
|
|
1052
|
+
for (const request of requests) {
|
|
1053
|
+
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"]))}`);
|
|
1054
|
+
}
|
|
1055
|
+
return text(lines.join(`
|
|
1056
|
+
`));
|
|
1057
|
+
});
|
|
1058
|
+
server.tool("sync", "Ingest new cost data. sources: all|claude|codex|gemini", { sources: z.enum(["all", "claude", "codex", "gemini"]).optional() }, async ({ sources }) => {
|
|
1059
|
+
const selected = sources ?? "all";
|
|
1060
|
+
const parts = [];
|
|
1061
|
+
if (selected === "all" || selected === "claude") {
|
|
1062
|
+
const result = await ingestClaude(db);
|
|
1063
|
+
parts.push(`claude: ${result["files"]} files, ${result["requests"]} requests, ${result["sessions"]} sessions`);
|
|
1064
|
+
}
|
|
1065
|
+
if (selected === "all" || selected === "codex") {
|
|
1066
|
+
const result = await ingestCodex(db);
|
|
1067
|
+
parts.push(`codex: ${result["sessions"]} sessions`);
|
|
1068
|
+
}
|
|
1069
|
+
if (selected === "all" || selected === "gemini") {
|
|
1070
|
+
const result = await ingestGemini(db);
|
|
1071
|
+
parts.push(`gemini: ${result["sessions"]} sessions`);
|
|
1072
|
+
}
|
|
1073
|
+
return text(parts.join(`
|
|
1074
|
+
`) || "done");
|
|
1075
|
+
});
|
|
1076
|
+
server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
|
|
1077
|
+
const goals = getGoalStatuses(db);
|
|
1078
|
+
if (goals.length === 0)
|
|
1079
|
+
return text("No goals set.");
|
|
1080
|
+
const lines = ["period scope limit spent used% status"];
|
|
1081
|
+
for (const goal of goals) {
|
|
1082
|
+
const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
|
|
1083
|
+
const pct = Number(goal["percent_used"]).toFixed(1);
|
|
1084
|
+
const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
|
|
1085
|
+
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}`);
|
|
1086
|
+
}
|
|
1087
|
+
return text(lines.join(`
|
|
1088
|
+
`));
|
|
1089
|
+
});
|
|
1090
|
+
server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
|
|
1091
|
+
period: z.enum(["day", "week", "month", "year"]),
|
|
1092
|
+
limit_usd: z.number().nonnegative(),
|
|
1093
|
+
project_path: z.string().optional(),
|
|
1094
|
+
agent: z.string().optional()
|
|
1095
|
+
}, async ({ period, limit_usd, project_path, agent }) => {
|
|
1096
|
+
const now = new Date().toISOString();
|
|
1097
|
+
upsertGoal(db, {
|
|
1098
|
+
id: randomUUID(),
|
|
1099
|
+
period,
|
|
1100
|
+
project_path: project_path ?? null,
|
|
1101
|
+
agent: agent ?? null,
|
|
1102
|
+
limit_usd,
|
|
1103
|
+
created_at: now,
|
|
1104
|
+
updated_at: now
|
|
1105
|
+
});
|
|
1106
|
+
return text(`Goal set: ${period} $${limit_usd}`);
|
|
1107
|
+
});
|
|
1108
|
+
server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
|
|
1109
|
+
deleteGoal(db, id);
|
|
1110
|
+
return text("Goal removed.");
|
|
1111
|
+
});
|
|
1112
|
+
server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
|
|
1113
|
+
const machines = listMachines(db);
|
|
1114
|
+
if (machines.length === 0)
|
|
1115
|
+
return text(`No machine data yet. Current machine: ${getMachineId()}`);
|
|
1116
|
+
const lines = ["machine sessions requests cost last_active"];
|
|
1117
|
+
for (const m of machines) {
|
|
1118
|
+
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"}`);
|
|
1119
|
+
}
|
|
1120
|
+
lines.push(`
|
|
1121
|
+
current machine: ${getMachineId()}`);
|
|
1122
|
+
return text(lines.join(`
|
|
1123
|
+
`));
|
|
1124
|
+
});
|
|
1125
|
+
server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
|
|
1126
|
+
const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
|
|
1127
|
+
if (existing) {
|
|
1128
|
+
existing.last_seen_at = new Date().toISOString();
|
|
1129
|
+
return text(JSON.stringify(existing));
|
|
1130
|
+
}
|
|
1131
|
+
const id = Math.random().toString(36).slice(2, 10);
|
|
1132
|
+
const agent = { id, name, last_seen_at: new Date().toISOString() };
|
|
1133
|
+
_econAgents.set(id, agent);
|
|
1134
|
+
return text(JSON.stringify(agent));
|
|
1135
|
+
});
|
|
1136
|
+
server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
|
|
1137
|
+
const agent = _econAgents.get(agent_id);
|
|
1138
|
+
if (!agent)
|
|
1139
|
+
return textError("Agent not found");
|
|
1140
|
+
agent.last_seen_at = new Date().toISOString();
|
|
1141
|
+
return text(`\u2665 ${agent.name}`);
|
|
1142
|
+
});
|
|
1143
|
+
server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
|
|
1144
|
+
const agent = _econAgents.get(agent_id);
|
|
1145
|
+
if (!agent)
|
|
1146
|
+
return textError("Agent not found");
|
|
1147
|
+
agent.project_id = project_id ?? undefined;
|
|
1148
|
+
return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
|
|
1149
|
+
});
|
|
1150
|
+
server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
|
|
1151
|
+
server.tool("send_feedback", "Send feedback about this service.", {
|
|
1152
|
+
message: z.string(),
|
|
1153
|
+
email: z.string().optional(),
|
|
1154
|
+
category: z.enum(["bug", "feature", "general"]).optional()
|
|
1155
|
+
}, async ({ message, email, category }) => {
|
|
1156
|
+
try {
|
|
1157
|
+
db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(message, email ?? null, category ?? "general", packageMetadata.version);
|
|
1158
|
+
return text("Feedback saved. Thank you!");
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
return textError(String(error));
|
|
1122
1161
|
}
|
|
1123
1162
|
});
|
|
1124
|
-
var _econAgents = new Map;
|
|
1125
1163
|
var transport = new StdioServerTransport;
|
|
1164
|
+
registerCloudTools(server, "economy");
|
|
1126
1165
|
await server.connect(transport);
|