@hasna/economy 0.2.17 → 0.2.18

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