@hasna/economy 0.2.16 → 0.2.18

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