@hasna/economy 0.2.4 → 0.2.6

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
@@ -15,6 +15,7 @@ var __export = (target, all) => {
15
15
  });
16
16
  };
17
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+ var __require = import.meta.require;
18
19
 
19
20
  // src/lib/pricing.ts
20
21
  var exports_pricing = {};
@@ -88,6 +89,10 @@ var init_pricing = __esm(() => {
88
89
  "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
89
90
  "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
90
91
  "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
92
+ "gemini-2.0-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
93
+ "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10, cacheReadPer1M: 0, cacheWritePer1M: 0 },
94
+ "gemini-1.5-pro": { inputPer1M: 1.25, outputPer1M: 5, cacheReadPer1M: 0, cacheWritePer1M: 0 },
95
+ "gemini-1.5-flash": { inputPer1M: 0.075, outputPer1M: 0.3, cacheReadPer1M: 0, cacheWritePer1M: 0 },
91
96
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
92
97
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
93
98
  "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
@@ -174,6 +179,16 @@ function initSchema(db) {
174
179
  updated_at TEXT NOT NULL
175
180
  );
176
181
 
182
+ CREATE TABLE IF NOT EXISTS goals (
183
+ id TEXT PRIMARY KEY,
184
+ period TEXT NOT NULL,
185
+ project_path TEXT,
186
+ agent TEXT,
187
+ limit_usd REAL NOT NULL,
188
+ created_at TEXT NOT NULL,
189
+ updated_at TEXT NOT NULL
190
+ );
191
+
177
192
  CREATE TABLE IF NOT EXISTS ingest_state (
178
193
  source TEXT NOT NULL,
179
194
  key TEXT NOT NULL,
@@ -206,6 +221,8 @@ function periodWhere(period) {
206
221
  return `timestamp >= DATE('now', '-7 days')`;
207
222
  case "month":
208
223
  return `timestamp >= DATE('now', '-30 days')`;
224
+ case "year":
225
+ return `timestamp >= DATE('now', '-365 days')`;
209
226
  case "all":
210
227
  return "1=1";
211
228
  }
@@ -218,6 +235,8 @@ function sessionPeriodWhere(period) {
218
235
  return `started_at >= DATE('now', '-7 days')`;
219
236
  case "month":
220
237
  return `started_at >= DATE('now', '-30 days')`;
238
+ case "year":
239
+ return `started_at >= DATE('now', '-365 days')`;
221
240
  case "all":
222
241
  return "1=1";
223
242
  }
@@ -267,6 +286,11 @@ function querySessions(db, filter = {}) {
267
286
  conditions.push("started_at >= ?");
268
287
  params.push(filter.since);
269
288
  }
289
+ if (filter.search) {
290
+ const q = `%${filter.search}%`;
291
+ conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
292
+ params.push(q, q, `${filter.search}%`);
293
+ }
270
294
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
271
295
  const limit = filter.limit ?? 50;
272
296
  const offset = filter.offset ?? 0;
@@ -319,16 +343,31 @@ function queryModelBreakdown(db) {
319
343
  }
320
344
  function queryProjectBreakdown(db) {
321
345
  return db.prepare(`
322
- SELECT project_path, project_name,
323
- COUNT(*) as sessions,
324
- COALESCE(SUM(total_tokens), 0) as total_tokens,
325
- COALESCE(SUM(request_count), 0) as requests,
326
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
327
- MAX(started_at) as last_active
328
- FROM sessions
329
- GROUP BY project_path ORDER BY cost_usd DESC
346
+ SELECT
347
+ s.project_path,
348
+ COALESCE(p.name, s.project_name) as project_name,
349
+ COUNT(DISTINCT s.id) as sessions,
350
+ COUNT(r.id) as requests,
351
+ COALESCE(SUM(r.cost_usd), COALESCE(SUM(s.total_cost_usd), 0)) as cost_usd,
352
+ COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
353
+ MAX(s.started_at) as last_active
354
+ FROM sessions s
355
+ LEFT JOIN projects p ON p.path = s.project_path OR p.name = s.project_name
356
+ LEFT JOIN requests r ON r.session_id = s.id
357
+ WHERE s.project_path != '' OR s.project_name != ''
358
+ GROUP BY s.project_path
359
+ ORDER BY cost_usd DESC
330
360
  `).all();
331
361
  }
362
+ function queryDailyBreakdown(db, days = 30) {
363
+ return db.prepare(`
364
+ SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
365
+ FROM requests
366
+ WHERE timestamp >= DATE('now', ? || ' days')
367
+ GROUP BY DATE(timestamp), agent
368
+ ORDER BY date ASC
369
+ `).all(`-${days}`);
370
+ }
332
371
  function listBudgets(db) {
333
372
  return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
334
373
  }
@@ -358,6 +397,46 @@ function getBudgetStatuses(db) {
358
397
  };
359
398
  });
360
399
  }
400
+ function upsertGoal(db, goal) {
401
+ db.prepare(`
402
+ INSERT OR REPLACE INTO goals
403
+ (id, period, project_path, agent, limit_usd, created_at, updated_at)
404
+ VALUES (?, ?, ?, ?, ?, ?, ?)
405
+ `).run(goal.id, goal.period, goal.project_path ?? null, goal.agent ?? null, goal.limit_usd, goal.created_at, goal.updated_at);
406
+ }
407
+ function deleteGoal(db, id) {
408
+ db.prepare(`DELETE FROM goals WHERE id = ?`).run(id);
409
+ }
410
+ function listGoals(db) {
411
+ return db.prepare(`SELECT * FROM goals ORDER BY created_at DESC`).all();
412
+ }
413
+ function getGoalStatuses(db) {
414
+ const goals = listGoals(db);
415
+ return goals.map((g) => {
416
+ const periodStart = g.period === "day" ? "DATE('now')" : g.period === "week" ? "DATE('now', '-7 days')" : g.period === "month" ? "DATE('now', '-30 days')" : "DATE('now', '-365 days')";
417
+ let spendQuery = `SELECT COALESCE(SUM(cost_usd), 0) as spend FROM requests WHERE timestamp >= ${periodStart}`;
418
+ const params = [];
419
+ if (g.project_path) {
420
+ spendQuery += ` AND session_id IN (SELECT id FROM sessions WHERE project_path = ?)`;
421
+ params.push(g.project_path);
422
+ }
423
+ if (g.agent) {
424
+ spendQuery += ` AND agent = ?`;
425
+ params.push(g.agent);
426
+ }
427
+ const row = db.prepare(spendQuery).get(...params);
428
+ const spend = row.spend;
429
+ const percent = g.limit_usd > 0 ? spend / g.limit_usd * 100 : 0;
430
+ return {
431
+ ...g,
432
+ current_spend_usd: spend,
433
+ percent_used: percent,
434
+ is_on_track: percent < 70,
435
+ is_at_risk: percent >= 70 && percent <= 100,
436
+ is_over: percent > 100
437
+ };
438
+ });
439
+ }
361
440
  function getIngestState(db, source, key) {
362
441
  const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
363
442
  return row?.value ?? null;
@@ -405,6 +484,9 @@ init_pricing();
405
484
  import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
406
485
  import { homedir as homedir2 } from "os";
407
486
  import { join as join2, basename } from "path";
487
+ function autoDetectProject(cwd, projects) {
488
+ return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
489
+ }
408
490
  var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
409
491
  function dirNameToPath(dirName) {
410
492
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
@@ -433,11 +515,11 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
433
515
  let totalFiles = 0;
434
516
  let totalRequests = 0;
435
517
  const touchedSessions = new Set;
518
+ const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
436
519
  const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
437
520
  for (const projectDirEntry of projectDirs) {
438
521
  const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
439
522
  const projectPath = dirNameToPath(projectDirEntry.name);
440
- const projectName = basename(projectPath);
441
523
  const jsonlFiles = collectJsonlFiles(projectDirPath);
442
524
  for (const filePath of jsonlFiles) {
443
525
  const stateKey = filePath.replace(PROJECTS_DIR, "");
@@ -506,11 +588,13 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
506
588
  if (!touchedSessions.has(sessionId)) {
507
589
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
508
590
  if (!existing) {
591
+ const effectiveCwd = sessionCwd || projectPath;
592
+ const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
509
593
  const session = {
510
594
  id: sessionId,
511
595
  agent: "claude",
512
- project_path: sessionCwd || projectPath,
513
- project_name: basename(sessionCwd || projectPath),
596
+ project_path: detectedProject ? detectedProject.path : effectiveCwd,
597
+ project_name: detectedProject ? detectedProject.name : "",
514
598
  started_at: timestamp,
515
599
  ended_at: null,
516
600
  total_cost_usd: 0,
@@ -541,24 +625,12 @@ import { join as join3, basename as basename2 } from "path";
541
625
  import { Database as Database2 } from "bun:sqlite";
542
626
  var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
543
627
  var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
544
- function readCodexModel() {
545
- if (!existsSync3(CODEX_CONFIG_PATH))
546
- return "gpt-5.3-codex";
547
- try {
548
- const content = readFileSync2(CODEX_CONFIG_PATH, "utf-8");
549
- const match = content.match(/^model\s*=\s*"([^"]+)"/m);
550
- return match?.[1] ?? "gpt-5.3-codex";
551
- } catch {
552
- return "gpt-5.3-codex";
553
- }
554
- }
555
628
  async function ingestCodex(db, verbose = false) {
556
629
  if (!existsSync3(CODEX_DB_PATH)) {
557
630
  if (verbose)
558
631
  console.log("Codex DB not found:", CODEX_DB_PATH);
559
632
  return { sessions: 0 };
560
633
  }
561
- const model = readCodexModel();
562
634
  let codexDb = null;
563
635
  let ingested = 0;
564
636
  try {
@@ -596,6 +668,83 @@ async function ingestCodex(db, verbose = false) {
596
668
  return { sessions: ingested };
597
669
  }
598
670
 
671
+ // src/ingest/gemini.ts
672
+ init_database();
673
+ import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync2 } from "fs";
674
+ import { homedir as homedir4 } from "os";
675
+ import { join as join4 } from "path";
676
+ var GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
677
+ async function ingestGemini(db, verbose) {
678
+ if (!existsSync4(GEMINI_TMP_DIR)) {
679
+ if (verbose)
680
+ console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
681
+ return { sessions: 0 };
682
+ }
683
+ let totalSessions = 0;
684
+ const touchedSessions = new Set;
685
+ let projectHashDirs = [];
686
+ try {
687
+ projectHashDirs = readdirSync2(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join4(GEMINI_TMP_DIR, d.name));
688
+ } catch {
689
+ return { sessions: 0 };
690
+ }
691
+ for (const projectDir of projectHashDirs) {
692
+ const chatsDir = join4(projectDir, "chats");
693
+ if (!existsSync4(chatsDir))
694
+ continue;
695
+ let chatFiles = [];
696
+ try {
697
+ chatFiles = readdirSync2(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join4(chatsDir, f));
698
+ } catch {
699
+ continue;
700
+ }
701
+ for (const filePath of chatFiles) {
702
+ const stateKey = filePath.replace(homedir4(), "~");
703
+ let fileMtime = "0";
704
+ try {
705
+ fileMtime = statSync2(filePath).mtimeMs.toString();
706
+ } catch {
707
+ continue;
708
+ }
709
+ const processed = getIngestState(db, "gemini", stateKey);
710
+ if (processed === fileMtime)
711
+ continue;
712
+ let chatData;
713
+ try {
714
+ chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
715
+ } catch {
716
+ continue;
717
+ }
718
+ const sessionId = chatData.sessionId;
719
+ if (!sessionId)
720
+ continue;
721
+ const startTime = chatData.startTime ?? new Date().toISOString();
722
+ const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
723
+ if (!existing) {
724
+ const session = {
725
+ id: sessionId,
726
+ agent: "gemini",
727
+ project_path: "",
728
+ project_name: "",
729
+ started_at: startTime,
730
+ ended_at: chatData.lastUpdated ?? null,
731
+ total_cost_usd: 0,
732
+ total_tokens: 0,
733
+ request_count: 0
734
+ };
735
+ upsertSession(db, session);
736
+ touchedSessions.add(sessionId);
737
+ totalSessions++;
738
+ }
739
+ setIngestState(db, "gemini", stateKey, fileMtime);
740
+ }
741
+ }
742
+ for (const sessionId of touchedSessions) {
743
+ rollupSession(db, sessionId);
744
+ }
745
+ return { sessions: totalSessions };
746
+ }
747
+
599
748
  // src/mcp/index.ts
600
749
  init_pricing();
601
750
  var db = openDatabase();
@@ -612,24 +761,37 @@ function fmtSession(s) {
612
761
  return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
613
762
  }
614
763
  var TOOLS = [
615
- { name: "get_cost_summary", description: "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|all", inputSchema: { type: "object", properties: { period: { type: "string", enum: ["today", "week", "month", "all"] } } } },
764
+ { 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"] } } } },
616
765
  { 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" } } } },
617
766
  { name: "get_top_sessions", description: "Top sessions by cost. Params: n(10), agent", inputSchema: { type: "object", properties: { n: { type: "number" }, agent: { type: "string" } } } },
618
767
  { name: "get_model_breakdown", description: "Cost per model. No params.", inputSchema: { type: "object", properties: {} } },
619
768
  { name: "get_project_breakdown", description: "Cost per project. No params.", inputSchema: { type: "object", properties: {} } },
620
769
  { name: "get_budget_status", description: "Budget limits vs spend, percent used, alert flags. No params.", inputSchema: { type: "object", properties: {} } },
621
- { name: "sync", description: "Ingest new cost data. sources: all|claude|codex", inputSchema: { type: "object", properties: { sources: { type: "string", enum: ["all", "claude", "codex"] } } } },
770
+ { name: "get_daily", description: "Daily cost table by agent. Params: days(30)", inputSchema: { type: "object", properties: { days: { type: "number" } } } },
771
+ { 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"] } },
772
+ { name: "sync", description: "Ingest new cost data. sources: all|claude|codex|gemini", inputSchema: { type: "object", properties: { sources: { type: "string", enum: ["all", "claude", "codex", "gemini"] } } } },
622
773
  { name: "search_tools", description: "List tool names matching query. Use first to find relevant tools.", inputSchema: { type: "object", properties: { query: { type: "string" } } } },
623
- { name: "describe_tools", description: "Get param hints for specific tools by name.", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" } } }, required: ["names"] } }
774
+ { name: "describe_tools", description: "Get param hints for specific tools by name.", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" } } }, required: ["names"] } },
775
+ { name: "get_goals", description: "All spending goals with current progress. No params.", inputSchema: { type: "object", properties: {} } },
776
+ { 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"] } },
777
+ { name: "remove_goal", description: "Delete a goal by id.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
778
+ { name: "register_agent", description: "Register agent session.", inputSchema: { type: "object", properties: { name: { type: "string" }, session_id: { type: "string" } }, required: ["name"] } },
779
+ { name: "heartbeat", description: "Update last_seen_at.", inputSchema: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] } },
780
+ { name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } }
624
781
  ];
625
782
  var TOOL_DESCRIPTIONS = {
626
- get_cost_summary: "period(today|week|month|all) \u2192 {total_usd, sessions, requests, tokens, summary}",
783
+ get_cost_summary: "period(today|week|month|year|all) \u2192 {total_usd, sessions, requests, tokens, summary}",
627
784
  get_sessions: "agent(claude|codex), project(partial), limit(20) \u2192 compact session table",
628
785
  get_top_sessions: "n(10), agent(claude|codex) \u2192 top sessions by cost",
629
786
  get_model_breakdown: "no params \u2192 model, requests, tokens, cost",
630
787
  get_project_breakdown: "no params \u2192 project_name, sessions, cost",
631
788
  get_budget_status: "no params \u2192 budget limits, current spend, percent_used, is_over_alert",
632
- sync: "sources(all|claude|codex) \u2192 {files, requests, sessions} ingested"
789
+ get_daily: "days(30) \u2192 daily cost table grouped by date and agent",
790
+ get_session_detail: "session_id(prefix ok) \u2192 per-request breakdown with model, tokens, cost",
791
+ sync: "sources(all|claude|codex|gemini) \u2192 {files, requests, sessions} ingested",
792
+ get_goals: "no params \u2192 period, scope, limit, spent, percent, status(ON TRACK/AT RISK/OVER)",
793
+ set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? \u2192 creates/updates goal",
794
+ remove_goal: "id \u2192 deletes goal"
633
795
  };
634
796
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
635
797
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
@@ -713,6 +875,48 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
713
875
  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}`);
714
876
  }
715
877
  return { content: [{ type: "text", text: lines.join(`
878
+ `) }] };
879
+ }
880
+ case "get_daily": {
881
+ const days = Number(a["days"] ?? 30);
882
+ const rows = queryDailyBreakdown(db, days);
883
+ const lines = ["date claude codex gemini total"];
884
+ const byDate = new Map;
885
+ for (const r of rows) {
886
+ const d = String(r["date"]);
887
+ const entry = byDate.get(d) ?? { claude: 0, codex: 0, gemini: 0 };
888
+ if (r["agent"] === "claude")
889
+ entry.claude += Number(r["cost_usd"]);
890
+ else if (r["agent"] === "codex")
891
+ entry.codex += Number(r["cost_usd"]);
892
+ else if (r["agent"] === "gemini")
893
+ entry.gemini += Number(r["cost_usd"]);
894
+ byDate.set(d, entry);
895
+ }
896
+ for (const [date, costs] of [...byDate.entries()].sort()) {
897
+ const total = costs.claude + costs.codex + costs.gemini;
898
+ lines.push(`${date} ${fmtUsd(costs.claude).padEnd(11)}${fmtUsd(costs.codex).padEnd(11)}${fmtUsd(costs.gemini).padEnd(11)}${fmtUsd(total)}`);
899
+ }
900
+ return { content: [{ type: "text", text: lines.join(`
901
+ `) }] };
902
+ }
903
+ case "get_session_detail": {
904
+ const sid = String(a["session_id"] ?? "");
905
+ const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(sid, `${sid}%`);
906
+ if (!session)
907
+ return { content: [{ type: "text", text: `Session not found: ${sid}` }], isError: true };
908
+ const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
909
+ const lines = [
910
+ `session: ${String(session["id"]).slice(0, 16)}`,
911
+ `agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
912
+ `cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
913
+ "",
914
+ "time model input output cost"
915
+ ];
916
+ for (const r of requests) {
917
+ 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"]))}`);
918
+ }
919
+ return { content: [{ type: "text", text: lines.join(`
716
920
  `) }] };
717
921
  }
718
922
  case "sync": {
@@ -726,9 +930,71 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
726
930
  const r = await ingestCodex(db);
727
931
  parts.push(`codex: ${r["sessions"]} sessions`);
728
932
  }
933
+ if (sources === "all" || sources === "gemini") {
934
+ const r = await ingestGemini(db);
935
+ parts.push(`gemini: ${r["sessions"]} sessions`);
936
+ }
729
937
  return { content: [{ type: "text", text: parts.join(`
730
938
  `) || "done" }] };
731
939
  }
940
+ case "get_goals": {
941
+ const goals = getGoalStatuses(db);
942
+ if (goals.length === 0)
943
+ return { content: [{ type: "text", text: "No goals set." }] };
944
+ const lines = ["period scope limit spent used% status"];
945
+ for (const g of goals) {
946
+ const scope = String(g["project_path"] ?? g["agent"] ?? "global").slice(0, 20);
947
+ const pct = Number(g["percent_used"]).toFixed(1);
948
+ const status = g["is_over"] ? "OVER" : g["is_at_risk"] ? "AT RISK" : "ON TRACK";
949
+ 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}`);
950
+ }
951
+ return { content: [{ type: "text", text: lines.join(`
952
+ `) }] };
953
+ }
954
+ case "set_goal": {
955
+ const { randomUUID } = await import("crypto");
956
+ const now = new Date().toISOString();
957
+ upsertGoal(db, {
958
+ id: randomUUID(),
959
+ period: String(a["period"] ?? "month"),
960
+ project_path: a["project_path"] ?? null,
961
+ agent: a["agent"] ?? null,
962
+ limit_usd: Number(a["limit_usd"]),
963
+ created_at: now,
964
+ updated_at: now
965
+ });
966
+ return { content: [{ type: "text", text: `Goal set: ${a["period"]} $${a["limit_usd"]}` }] };
967
+ }
968
+ case "remove_goal": {
969
+ deleteGoal(db, String(a["id"] ?? ""));
970
+ return { content: [{ type: "text", text: "Goal removed." }] };
971
+ }
972
+ case "register_agent": {
973
+ const n = String(args["name"] ?? "");
974
+ const ex = [..._econAgents.values()].find((x) => x.name === n);
975
+ if (ex) {
976
+ ex.last_seen_at = new Date().toISOString();
977
+ return { content: [{ type: "text", text: JSON.stringify(ex) }] };
978
+ }
979
+ const id = Math.random().toString(36).slice(2, 10);
980
+ const ag = { id, name: n, last_seen_at: new Date().toISOString() };
981
+ _econAgents.set(id, ag);
982
+ return { content: [{ type: "text", text: JSON.stringify(ag) }] };
983
+ }
984
+ case "heartbeat": {
985
+ const ag = _econAgents.get(String(args["agent_id"] ?? ""));
986
+ if (!ag)
987
+ return { content: [{ type: "text", text: `Agent not found` }], isError: true };
988
+ ag.last_seen_at = new Date().toISOString();
989
+ return { content: [{ type: "text", text: `\u2665 ${ag.name}` }] };
990
+ }
991
+ case "set_focus": {
992
+ const ag = _econAgents.get(String(args["agent_id"] ?? ""));
993
+ if (!ag)
994
+ return { content: [{ type: "text", text: `Agent not found` }], isError: true };
995
+ ag["project_id"] = args["project_id"];
996
+ return { content: [{ type: "text", text: String(args["project_id"] ? `Focus: ${args["project_id"]}` : "Focus cleared") }] };
997
+ }
732
998
  default:
733
999
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
734
1000
  }
@@ -736,31 +1002,6 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
736
1002
  return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
737
1003
  }
738
1004
  });
739
- var _agentReg = new Map;
740
- server.tool("register_agent", "Register this agent session. Returns agent_id for use in heartbeat/set_focus.", { name: z.string(), session_id: z.string().optional() }, async (a) => {
741
- const existing = [..._agentReg.values()].find((x) => x.name === a.name);
742
- if (existing) {
743
- existing.last_seen_at = new Date().toISOString();
744
- return { content: [{ type: "text", text: JSON.stringify(existing) }] };
745
- }
746
- const id = Math.random().toString(36).slice(2, 10);
747
- const ag = { id, name: a.name, last_seen_at: new Date().toISOString() };
748
- _agentReg.set(id, ag);
749
- return { content: [{ type: "text", text: JSON.stringify(ag) }] };
750
- });
751
- server.tool("heartbeat", "Update last_seen_at to signal agent is active.", { agent_id: z.string() }, async (a) => {
752
- const ag = _agentReg.get(a.agent_id);
753
- if (!ag)
754
- return { content: [{ type: "text", text: `Agent not found: ${a.agent_id}` }], isError: true };
755
- ag.last_seen_at = new Date().toISOString();
756
- return { content: [{ type: "text", text: `\u2665 ${ag.name} \u2014 active` }] };
757
- });
758
- server.tool("set_focus", "Set active project context for this agent session.", { agent_id: z.string(), project_id: z.string().optional() }, async (a) => {
759
- const ag = _agentReg.get(a.agent_id);
760
- if (!ag)
761
- return { content: [{ type: "text", text: `Agent not found: ${a.agent_id}` }], isError: true };
762
- ag.project_id = a.project_id;
763
- return { content: [{ type: "text", text: a.project_id ? `Focus: ${a.project_id}` : "Focus cleared" }] };
764
- });
1005
+ var _econAgents = new Map;
765
1006
  var transport = new StdioServerTransport;
766
1007
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex, and Gemini",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",