@hasna/economy 0.2.10 → 0.2.12

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