@hasna/economy 0.2.3 → 0.2.5

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,16 +1,21 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
  var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
4
8
  var __export = (target, all) => {
5
9
  for (var name in all)
6
10
  __defProp(target, name, {
7
11
  get: all[name],
8
12
  enumerable: true,
9
13
  configurable: true,
10
- set: (newValue) => all[name] = () => newValue
14
+ set: __exportSetter.bind(all, name)
11
15
  });
12
16
  };
13
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+ var __require = import.meta.require;
14
19
 
15
20
  // src/lib/pricing.ts
16
21
  var exports_pricing = {};
@@ -84,6 +89,10 @@ var init_pricing = __esm(() => {
84
89
  "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
85
90
  "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
86
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 },
87
96
  "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
88
97
  "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
89
98
  "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
@@ -170,6 +179,16 @@ function initSchema(db) {
170
179
  updated_at TEXT NOT NULL
171
180
  );
172
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
+
173
192
  CREATE TABLE IF NOT EXISTS ingest_state (
174
193
  source TEXT NOT NULL,
175
194
  key TEXT NOT NULL,
@@ -202,6 +221,8 @@ function periodWhere(period) {
202
221
  return `timestamp >= DATE('now', '-7 days')`;
203
222
  case "month":
204
223
  return `timestamp >= DATE('now', '-30 days')`;
224
+ case "year":
225
+ return `timestamp >= DATE('now', '-365 days')`;
205
226
  case "all":
206
227
  return "1=1";
207
228
  }
@@ -214,6 +235,8 @@ function sessionPeriodWhere(period) {
214
235
  return `started_at >= DATE('now', '-7 days')`;
215
236
  case "month":
216
237
  return `started_at >= DATE('now', '-30 days')`;
238
+ case "year":
239
+ return `started_at >= DATE('now', '-365 days')`;
217
240
  case "all":
218
241
  return "1=1";
219
242
  }
@@ -263,6 +286,11 @@ function querySessions(db, filter = {}) {
263
286
  conditions.push("started_at >= ?");
264
287
  params.push(filter.since);
265
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
+ }
266
294
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
267
295
  const limit = filter.limit ?? 50;
268
296
  const offset = filter.offset ?? 0;
@@ -315,16 +343,31 @@ function queryModelBreakdown(db) {
315
343
  }
316
344
  function queryProjectBreakdown(db) {
317
345
  return db.prepare(`
318
- SELECT project_path, project_name,
319
- COUNT(*) as sessions,
320
- COALESCE(SUM(total_tokens), 0) as total_tokens,
321
- COALESCE(SUM(request_count), 0) as requests,
322
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
323
- MAX(started_at) as last_active
324
- FROM sessions
325
- 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
326
360
  `).all();
327
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
+ }
328
371
  function listBudgets(db) {
329
372
  return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
330
373
  }
@@ -354,6 +397,46 @@ function getBudgetStatuses(db) {
354
397
  };
355
398
  });
356
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
+ }
357
440
  function getIngestState(db, source, key) {
358
441
  const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
359
442
  return row?.value ?? null;
@@ -401,6 +484,9 @@ init_pricing();
401
484
  import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
402
485
  import { homedir as homedir2 } from "os";
403
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
+ }
404
490
  var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
405
491
  function dirNameToPath(dirName) {
406
492
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
@@ -429,11 +515,11 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
429
515
  let totalFiles = 0;
430
516
  let totalRequests = 0;
431
517
  const touchedSessions = new Set;
518
+ const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
432
519
  const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
433
520
  for (const projectDirEntry of projectDirs) {
434
521
  const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
435
522
  const projectPath = dirNameToPath(projectDirEntry.name);
436
- const projectName = basename(projectPath);
437
523
  const jsonlFiles = collectJsonlFiles(projectDirPath);
438
524
  for (const filePath of jsonlFiles) {
439
525
  const stateKey = filePath.replace(PROJECTS_DIR, "");
@@ -502,11 +588,13 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
502
588
  if (!touchedSessions.has(sessionId)) {
503
589
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
504
590
  if (!existing) {
591
+ const effectiveCwd = sessionCwd || projectPath;
592
+ const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
505
593
  const session = {
506
594
  id: sessionId,
507
595
  agent: "claude",
508
- project_path: sessionCwd || projectPath,
509
- project_name: basename(sessionCwd || projectPath),
596
+ project_path: detectedProject ? detectedProject.path : effectiveCwd,
597
+ project_name: detectedProject ? detectedProject.name : "",
510
598
  started_at: timestamp,
511
599
  ended_at: null,
512
600
  total_cost_usd: 0,
@@ -537,24 +625,12 @@ import { join as join3, basename as basename2 } from "path";
537
625
  import { Database as Database2 } from "bun:sqlite";
538
626
  var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
539
627
  var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
540
- function readCodexModel() {
541
- if (!existsSync3(CODEX_CONFIG_PATH))
542
- return "gpt-5.3-codex";
543
- try {
544
- const content = readFileSync2(CODEX_CONFIG_PATH, "utf-8");
545
- const match = content.match(/^model\s*=\s*"([^"]+)"/m);
546
- return match?.[1] ?? "gpt-5.3-codex";
547
- } catch {
548
- return "gpt-5.3-codex";
549
- }
550
- }
551
628
  async function ingestCodex(db, verbose = false) {
552
629
  if (!existsSync3(CODEX_DB_PATH)) {
553
630
  if (verbose)
554
631
  console.log("Codex DB not found:", CODEX_DB_PATH);
555
632
  return { sessions: 0 };
556
633
  }
557
- const model = readCodexModel();
558
634
  let codexDb = null;
559
635
  let ingested = 0;
560
636
  try {
@@ -592,6 +668,83 @@ async function ingestCodex(db, verbose = false) {
592
668
  return { sessions: ingested };
593
669
  }
594
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
+
595
748
  // src/mcp/index.ts
596
749
  init_pricing();
597
750
  var db = openDatabase();
@@ -608,24 +761,37 @@ function fmtSession(s) {
608
761
  return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
609
762
  }
610
763
  var TOOLS = [
611
- { 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"] } } } },
612
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" } } } },
613
766
  { name: "get_top_sessions", description: "Top sessions by cost. Params: n(10), agent", inputSchema: { type: "object", properties: { n: { type: "number" }, agent: { type: "string" } } } },
614
767
  { name: "get_model_breakdown", description: "Cost per model. No params.", inputSchema: { type: "object", properties: {} } },
615
768
  { name: "get_project_breakdown", description: "Cost per project. No params.", inputSchema: { type: "object", properties: {} } },
616
769
  { name: "get_budget_status", description: "Budget limits vs spend, percent used, alert flags. No params.", inputSchema: { type: "object", properties: {} } },
617
- { 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"] } } } },
618
773
  { name: "search_tools", description: "List tool names matching query. Use first to find relevant tools.", inputSchema: { type: "object", properties: { query: { type: "string" } } } },
619
- { 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"] } }
620
781
  ];
621
782
  var TOOL_DESCRIPTIONS = {
622
- 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}",
623
784
  get_sessions: "agent(claude|codex), project(partial), limit(20) \u2192 compact session table",
624
785
  get_top_sessions: "n(10), agent(claude|codex) \u2192 top sessions by cost",
625
786
  get_model_breakdown: "no params \u2192 model, requests, tokens, cost",
626
787
  get_project_breakdown: "no params \u2192 project_name, sessions, cost",
627
788
  get_budget_status: "no params \u2192 budget limits, current spend, percent_used, is_over_alert",
628
- 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"
629
795
  };
630
796
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
631
797
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
@@ -709,6 +875,48 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
709
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}`);
710
876
  }
711
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(`
712
920
  `) }] };
713
921
  }
714
922
  case "sync": {
@@ -722,9 +930,71 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
722
930
  const r = await ingestCodex(db);
723
931
  parts.push(`codex: ${r["sessions"]} sessions`);
724
932
  }
933
+ if (sources === "all" || sources === "gemini") {
934
+ const r = await ingestGemini(db);
935
+ parts.push(`gemini: ${r["sessions"]} sessions`);
936
+ }
725
937
  return { content: [{ type: "text", text: parts.join(`
726
938
  `) || "done" }] };
727
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
+ }
728
998
  default:
729
999
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
730
1000
  }
@@ -732,5 +1002,6 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
732
1002
  return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }], isError: true };
733
1003
  }
734
1004
  });
1005
+ var _econAgents = new Map;
735
1006
  var transport = new StdioServerTransport;
736
1007
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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",
@@ -1,8 +0,0 @@
1
- import type { Agent } from '../../types/index.js';
2
- interface WatchOptions {
3
- interval: number;
4
- agent?: Agent;
5
- }
6
- export declare function watchCosts(opts: WatchOptions): Promise<void>;
7
- export {};
8
- //# sourceMappingURL=watch.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/watch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAEjD,UAAU,YAAY;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,KAAK,CAAA;CACd;AAuBD,wBAAsB,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiElE"}
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env bun
2
- export {};
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":""}