@hasna/economy 0.2.11 → 0.2.13

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
@@ -108,8 +108,17 @@ var init_pricing = __esm(() => {
108
108
  // src/db/database.ts
109
109
  import { SqliteAdapter as Database } from "@hasna/cloud";
110
110
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
111
+ import { hostname } from "os";
111
112
  import { homedir } from "os";
112
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
+ }
113
122
  function getDataDir() {
114
123
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
115
124
  const newDir = join(home, ".hasna", "economy");
@@ -142,6 +151,7 @@ function openDatabase(dbPath, skipSeed = false) {
142
151
  }
143
152
  const db = new Database(path);
144
153
  db.exec("PRAGMA journal_mode = WAL");
154
+ db.exec("PRAGMA busy_timeout = 5000");
145
155
  db.exec("PRAGMA foreign_keys = ON");
146
156
  initSchema(db);
147
157
  if (!skipSeed) {
@@ -163,7 +173,8 @@ function initSchema(db) {
163
173
  cost_usd REAL NOT NULL DEFAULT 0,
164
174
  duration_ms INTEGER DEFAULT 0,
165
175
  timestamp TEXT NOT NULL,
166
- source_request_id TEXT
176
+ source_request_id TEXT,
177
+ machine_id TEXT DEFAULT ''
167
178
  );
168
179
 
169
180
  CREATE TABLE IF NOT EXISTS sessions (
@@ -175,7 +186,8 @@ function initSchema(db) {
175
186
  ended_at TEXT,
176
187
  total_cost_usd REAL DEFAULT 0,
177
188
  total_tokens INTEGER DEFAULT 0,
178
- request_count INTEGER DEFAULT 0
189
+ request_count INTEGER DEFAULT 0,
190
+ machine_id TEXT DEFAULT ''
179
191
  );
180
192
 
181
193
  CREATE TABLE IF NOT EXISTS projects (
@@ -241,6 +253,15 @@ function initSchema(db) {
241
253
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
242
254
  );
243
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
+ `);
244
265
  }
245
266
  function periodWhere(period) {
246
267
  switch (period) {
@@ -279,17 +300,17 @@ function upsertRequest(db, req) {
279
300
  INSERT OR REPLACE INTO requests
280
301
  (id, agent, session_id, model, input_tokens, output_tokens,
281
302
  cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
282
- timestamp, source_request_id)
283
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
284
- `).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 ?? "");
285
306
  }
286
307
  function upsertSession(db, session) {
287
308
  db.prepare(`
288
309
  INSERT OR REPLACE INTO sessions
289
310
  (id, agent, project_path, project_name, started_at, ended_at,
290
- total_cost_usd, total_tokens, request_count)
291
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
292
- `).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 ?? "");
293
314
  }
294
315
  function rollupSession(db, sessionId) {
295
316
  db.prepare(`
@@ -319,6 +340,10 @@ function querySessions(db, filter = {}) {
319
340
  conditions.push("started_at >= ?");
320
341
  params.push(filter.since);
321
342
  }
343
+ if (filter.machine) {
344
+ conditions.push("machine_id = ?");
345
+ params.push(filter.machine);
346
+ }
322
347
  if (filter.search) {
323
348
  const q = `%${filter.search}%`;
324
349
  conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
@@ -337,24 +362,25 @@ function queryTopSessions(db, n = 10, agent) {
337
362
  }
338
363
  return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
339
364
  }
340
- function querySummary(db, period) {
365
+ function querySummary(db, period, machine) {
341
366
  const rWhere = periodWhere(period);
342
367
  const sWhere = sessionPeriodWhere(period);
368
+ const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
343
369
  const r = db.prepare(`
344
370
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
345
371
  COUNT(*) as requests,
346
372
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
347
- FROM requests WHERE ${rWhere}
373
+ FROM requests WHERE ${rWhere}${machineClause}
348
374
  `).get();
349
375
  const codexTotals = db.prepare(`
350
376
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
351
377
  COALESCE(SUM(total_tokens), 0) as tokens,
352
378
  COUNT(*) as sessions
353
379
  FROM sessions
354
- WHERE ${sWhere}
380
+ WHERE ${sWhere}${machineClause}
355
381
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
356
382
  `).get();
357
- 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();
358
384
  return {
359
385
  total_usd: r.total_usd + codexTotals.cost_usd,
360
386
  requests: r.requests,
@@ -477,6 +503,20 @@ function getIngestState(db, source, key) {
477
503
  function setIngestState(db, source, key, value) {
478
504
  db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
479
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
+ }
480
520
  function upsertModelPricing(db, p) {
481
521
  db.prepare(`
482
522
  INSERT OR REPLACE INTO model_pricing
@@ -513,6 +553,95 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
513
553
  import { registerCloudTools } from "@hasna/cloud";
514
554
  import { z } from "zod";
515
555
 
556
+ // src/db/pg-migrations.ts
557
+ var PG_MIGRATIONS = [
558
+ `CREATE TABLE IF NOT EXISTS requests (
559
+ id TEXT PRIMARY KEY,
560
+ agent TEXT NOT NULL,
561
+ session_id TEXT NOT NULL,
562
+ model TEXT NOT NULL,
563
+ input_tokens INTEGER DEFAULT 0,
564
+ output_tokens INTEGER DEFAULT 0,
565
+ cache_read_tokens INTEGER DEFAULT 0,
566
+ cache_create_tokens INTEGER DEFAULT 0,
567
+ cost_usd REAL NOT NULL DEFAULT 0,
568
+ duration_ms INTEGER DEFAULT 0,
569
+ timestamp TEXT NOT NULL,
570
+ source_request_id TEXT,
571
+ machine_id TEXT DEFAULT ''
572
+ )`,
573
+ `CREATE TABLE IF NOT EXISTS sessions (
574
+ id TEXT PRIMARY KEY,
575
+ agent TEXT NOT NULL,
576
+ project_path TEXT DEFAULT '',
577
+ project_name TEXT DEFAULT '',
578
+ started_at TEXT NOT NULL,
579
+ ended_at TEXT,
580
+ total_cost_usd REAL DEFAULT 0,
581
+ total_tokens INTEGER DEFAULT 0,
582
+ request_count INTEGER DEFAULT 0,
583
+ machine_id TEXT DEFAULT ''
584
+ )`,
585
+ `CREATE TABLE IF NOT EXISTS projects (
586
+ id TEXT PRIMARY KEY,
587
+ path TEXT UNIQUE NOT NULL,
588
+ name TEXT NOT NULL,
589
+ description TEXT,
590
+ tags TEXT DEFAULT '[]',
591
+ created_at TEXT NOT NULL
592
+ )`,
593
+ `CREATE TABLE IF NOT EXISTS budgets (
594
+ id TEXT PRIMARY KEY,
595
+ project_path TEXT,
596
+ agent TEXT,
597
+ period TEXT NOT NULL,
598
+ limit_usd REAL NOT NULL,
599
+ alert_at_percent INTEGER DEFAULT 80,
600
+ created_at TEXT NOT NULL,
601
+ updated_at TEXT NOT NULL
602
+ )`,
603
+ `CREATE TABLE IF NOT EXISTS goals (
604
+ id TEXT PRIMARY KEY,
605
+ period TEXT NOT NULL,
606
+ project_path TEXT,
607
+ agent TEXT,
608
+ limit_usd REAL NOT NULL,
609
+ created_at TEXT NOT NULL,
610
+ updated_at TEXT NOT NULL
611
+ )`,
612
+ `CREATE TABLE IF NOT EXISTS ingest_state (
613
+ source TEXT NOT NULL,
614
+ key TEXT NOT NULL,
615
+ value TEXT NOT NULL,
616
+ PRIMARY KEY (source, key)
617
+ )`,
618
+ `CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
619
+ `CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
620
+ `CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
621
+ `CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
622
+ `CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
623
+ `CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
624
+ `CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
625
+ `CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
626
+ `CREATE TABLE IF NOT EXISTS model_pricing (
627
+ model TEXT PRIMARY KEY,
628
+ input_per_1m REAL NOT NULL DEFAULT 0,
629
+ output_per_1m REAL NOT NULL DEFAULT 0,
630
+ cache_read_per_1m REAL NOT NULL DEFAULT 0,
631
+ cache_write_per_1m REAL NOT NULL DEFAULT 0,
632
+ updated_at TEXT NOT NULL
633
+ )`,
634
+ `CREATE TABLE IF NOT EXISTS feedback (
635
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
636
+ message TEXT NOT NULL,
637
+ email TEXT,
638
+ category TEXT DEFAULT 'general',
639
+ version TEXT,
640
+ machine_id TEXT,
641
+ created_at TEXT NOT NULL DEFAULT NOW()::text
642
+ )`
643
+ ];
644
+
516
645
  // src/ingest/claude.ts
517
646
  init_database();
518
647
  init_pricing();
@@ -522,7 +651,8 @@ import { join as join2, basename } from "path";
522
651
  function autoDetectProject(cwd, projects) {
523
652
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
524
653
  }
525
- var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
654
+ var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
655
+ var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
526
656
  function dirNameToPath(dirName) {
527
657
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
528
658
  }
@@ -542,29 +672,36 @@ function collectJsonlFiles(projectDir) {
542
672
  return files;
543
673
  }
544
674
  async function ingestClaude(db, verbose = false, _telemetryDir) {
545
- if (!existsSync2(PROJECTS_DIR)) {
675
+ return ingestJsonlProjects(db, CLAUDE_PROJECTS_DIR, "claude", verbose);
676
+ }
677
+ async function ingestTakumi(db, verbose = false) {
678
+ return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
679
+ }
680
+ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
681
+ if (!existsSync2(projectsDir)) {
546
682
  if (verbose)
547
- console.log("Claude projects dir not found:", PROJECTS_DIR);
683
+ console.log(`${agentName} projects dir not found:`, projectsDir);
548
684
  return { files: 0, requests: 0, sessions: 0 };
549
685
  }
686
+ const machineId = getMachineId();
550
687
  let totalFiles = 0;
551
688
  let totalRequests = 0;
552
689
  const touchedSessions = new Set;
553
690
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
554
- const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
691
+ const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
555
692
  for (const projectDirEntry of projectDirs) {
556
- const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
693
+ const projectDirPath = join2(projectsDir, projectDirEntry.name);
557
694
  const projectPath = dirNameToPath(projectDirEntry.name);
558
695
  const jsonlFiles = collectJsonlFiles(projectDirPath);
559
696
  for (const filePath of jsonlFiles) {
560
- const stateKey = filePath.replace(PROJECTS_DIR, "");
697
+ const stateKey = filePath.replace(projectsDir, "");
561
698
  let fileMtime = "0";
562
699
  try {
563
700
  fileMtime = statSync2(filePath).mtimeMs.toString();
564
701
  } catch {
565
702
  continue;
566
703
  }
567
- const processed = getIngestState(db, "claude", stateKey);
704
+ const processed = getIngestState(db, agentName, stateKey);
568
705
  if (processed === fileMtime)
569
706
  continue;
570
707
  let lines;
@@ -605,10 +742,10 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
605
742
  if (inputTokens + outputTokens + cacheWriteTokens === 0)
606
743
  continue;
607
744
  const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
608
- const reqId = `claude-${sessionId}-${timestamp}`;
745
+ const reqId = `${agentName}-${sessionId}-${timestamp}`;
609
746
  upsertRequest(db, {
610
747
  id: reqId,
611
- agent: "claude",
748
+ agent: agentName,
612
749
  session_id: sessionId,
613
750
  model,
614
751
  input_tokens: inputTokens,
@@ -618,7 +755,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
618
755
  cost_usd: costUsd,
619
756
  duration_ms: 0,
620
757
  timestamp,
621
- source_request_id: reqId
758
+ source_request_id: reqId,
759
+ machine_id: machineId
622
760
  });
623
761
  if (!touchedSessions.has(sessionId)) {
624
762
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
@@ -627,14 +765,15 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
627
765
  const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
628
766
  const session = {
629
767
  id: sessionId,
630
- agent: "claude",
768
+ agent: agentName,
631
769
  project_path: detectedProject ? detectedProject.path : effectiveCwd,
632
770
  project_name: detectedProject ? detectedProject.name : "",
633
771
  started_at: timestamp,
634
772
  ended_at: null,
635
773
  total_cost_usd: 0,
636
774
  total_tokens: 0,
637
- request_count: 0
775
+ request_count: 0,
776
+ machine_id: machineId
638
777
  };
639
778
  upsertSession(db, session);
640
779
  }
@@ -642,7 +781,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
642
781
  }
643
782
  totalRequests++;
644
783
  }
645
- setIngestState(db, "claude", stateKey, fileMtime);
784
+ setIngestState(db, agentName, stateKey, fileMtime);
646
785
  totalFiles++;
647
786
  }
648
787
  }
@@ -666,6 +805,7 @@ async function ingestCodex(db, verbose = false) {
666
805
  console.log("Codex DB not found:", CODEX_DB_PATH);
667
806
  return { sessions: 0 };
668
807
  }
808
+ const machineId = getMachineId();
669
809
  let codexDb = null;
670
810
  let ingested = 0;
671
811
  try {
@@ -690,7 +830,8 @@ async function ingestCodex(db, verbose = false) {
690
830
  ended_at: endedAt,
691
831
  total_cost_usd: costUsd,
692
832
  total_tokens: thread.tokens_used,
693
- request_count: 1
833
+ request_count: 1,
834
+ machine_id: machineId
694
835
  });
695
836
  setIngestState(db, "codex", stateKey, "done");
696
837
  ingested++;
@@ -715,6 +856,7 @@ async function ingestGemini(db, verbose) {
715
856
  console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
716
857
  return { sessions: 0 };
717
858
  }
859
+ const machineId = getMachineId();
718
860
  let totalSessions = 0;
719
861
  const touchedSessions = new Set;
720
862
  let projectHashDirs = [];
@@ -765,7 +907,8 @@ async function ingestGemini(db, verbose) {
765
907
  ended_at: chatData.lastUpdated ?? null,
766
908
  total_cost_usd: 0,
767
909
  total_tokens: 0,
768
- request_count: 0
910
+ request_count: 0,
911
+ machine_id: machineId
769
912
  };
770
913
  upsertSession(db, session);
771
914
  touchedSessions.add(sessionId);
@@ -838,6 +981,7 @@ var TOOL_NAMES = [
838
981
  "get_goals",
839
982
  "set_goal",
840
983
  "remove_goal",
984
+ "list_machines",
841
985
  "register_agent",
842
986
  "heartbeat",
843
987
  "set_focus",
@@ -845,9 +989,10 @@ var TOOL_NAMES = [
845
989
  "send_feedback"
846
990
  ];
847
991
  var TOOL_DESCRIPTIONS = {
848
- get_cost_summary: "period(today|week|month|year|all) -> {total_usd, sessions, requests, tokens, summary}",
849
- get_sessions: "agent(claude|codex|gemini), project(partial), limit(20) -> compact session table",
992
+ get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
993
+ get_sessions: "agent(claude|codex|gemini), project(partial), machine?(hostname), limit(20) -> compact session table",
850
994
  get_top_sessions: "n(10), agent(claude|codex|gemini) -> top sessions by cost",
995
+ list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
851
996
  get_model_breakdown: "no params -> model, requests, tokens, cost",
852
997
  get_project_breakdown: "no params -> project_name, sessions, cost",
853
998
  get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
@@ -891,27 +1036,30 @@ server.tool("describe_tools", "Get param hints for specific tools by name.", { n
891
1036
  `);
892
1037
  return text(result);
893
1038
  });
894
- server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
1039
+ 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 }) => {
895
1040
  const resolved = period ?? "today";
896
- const s = querySummary(db, resolved);
1041
+ const s = querySummary(db, resolved, machine);
1042
+ const machineLabel = machine ? ` on ${machine}` : "";
897
1043
  return text([
898
- `period: ${resolved}`,
1044
+ `period: ${resolved}${machineLabel}`,
899
1045
  `cost: ${fmtUsd(s.total_usd)}`,
900
1046
  `sessions: ${s.sessions}`,
901
1047
  `requests: ${s.requests.toLocaleString()}`,
902
1048
  `tokens: ${fmtTok(s.tokens)}`,
903
- `summary: You've spent ${fmtUsd(s.total_usd)} ${resolved === "all" ? "total" : resolved} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
1049
+ `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)`
904
1050
  ].join(`
905
1051
  `));
906
1052
  });
907
- server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, limit(20)", {
908
- agent: z.enum(["claude", "codex", "gemini"]).optional(),
1053
+ server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, machine, limit(20)", {
1054
+ agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional(),
909
1055
  project: z.string().optional(),
1056
+ machine: z.string().optional(),
910
1057
  limit: z.number().int().positive().max(100).optional()
911
- }, async ({ agent, project, limit }) => {
1058
+ }, async ({ agent, project, machine, limit }) => {
912
1059
  const sessions = querySessions(db, {
913
1060
  agent,
914
1061
  project,
1062
+ machine,
915
1063
  limit: limit ?? 20
916
1064
  });
917
1065
  const lines = ["id agent cost tokens project"];
@@ -922,7 +1070,7 @@ server.tool("get_sessions", "List sessions. Returns compact table. Params: agent
922
1070
  });
923
1071
  server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
924
1072
  n: z.number().int().positive().max(100).optional(),
925
- agent: z.enum(["claude", "codex", "gemini"]).optional()
1073
+ agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional()
926
1074
  }, async ({ n, agent }) => {
927
1075
  const sessions = queryTopSessions(db, n ?? 10, agent);
928
1076
  const lines = ["rank id agent cost tokens project"];
@@ -1003,13 +1151,17 @@ server.tool("get_session_detail", "Per-request breakdown of a single session. Pa
1003
1151
  return text(lines.join(`
1004
1152
  `));
1005
1153
  });
1006
- server.tool("sync", "Ingest new cost data. sources: all|claude|codex|gemini", { sources: z.enum(["all", "claude", "codex", "gemini"]).optional() }, async ({ sources }) => {
1154
+ server.tool("sync", "Ingest new cost data. sources: all|claude|takumi|codex|gemini", { sources: z.enum(["all", "claude", "takumi", "codex", "gemini"]).optional() }, async ({ sources }) => {
1007
1155
  const selected = sources ?? "all";
1008
1156
  const parts = [];
1009
1157
  if (selected === "all" || selected === "claude") {
1010
1158
  const result = await ingestClaude(db);
1011
1159
  parts.push(`claude: ${result["files"]} files, ${result["requests"]} requests, ${result["sessions"]} sessions`);
1012
1160
  }
1161
+ if (selected === "all" || selected === "takumi") {
1162
+ const result = await ingestTakumi(db);
1163
+ parts.push(`takumi: ${result["files"]} files, ${result["requests"]} requests, ${result["sessions"]} sessions`);
1164
+ }
1013
1165
  if (selected === "all" || selected === "codex") {
1014
1166
  const result = await ingestCodex(db);
1015
1167
  parts.push(`codex: ${result["sessions"]} sessions`);
@@ -1057,6 +1209,19 @@ server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({
1057
1209
  deleteGoal(db, id);
1058
1210
  return text("Goal removed.");
1059
1211
  });
1212
+ server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
1213
+ const machines = listMachines(db);
1214
+ if (machines.length === 0)
1215
+ return text(`No machine data yet. Current machine: ${getMachineId()}`);
1216
+ const lines = ["machine sessions requests cost last_active"];
1217
+ for (const m of machines) {
1218
+ 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"}`);
1219
+ }
1220
+ lines.push(`
1221
+ current machine: ${getMachineId()}`);
1222
+ return text(lines.join(`
1223
+ `));
1224
+ });
1060
1225
  server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
1061
1226
  const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
1062
1227
  if (existing) {
@@ -1096,5 +1261,8 @@ server.tool("send_feedback", "Send feedback about this service.", {
1096
1261
  }
1097
1262
  });
1098
1263
  var transport = new StdioServerTransport;
1099
- registerCloudTools(server, "economy");
1264
+ registerCloudTools(server, "economy", {
1265
+ dbPath: getDbPath(),
1266
+ migrations: PG_MIGRATIONS
1267
+ });
1100
1268
  await server.connect(transport);