@hasna/economy 0.2.10 → 0.2.11

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 = {};
@@ -507,81 +505,13 @@ function seedModelPricing(db, defaults) {
507
505
  }
508
506
  var init_database = () => {};
509
507
 
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
508
  // src/mcp/index.ts
581
509
  init_database();
582
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
510
+ import { randomUUID } from "crypto";
511
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
583
512
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
584
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
513
+ import { registerCloudTools } from "@hasna/cloud";
514
+ import { z } from "zod";
585
515
 
586
516
  // src/ingest/claude.ts
587
517
  init_database();
@@ -727,7 +657,7 @@ init_database();
727
657
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
728
658
  import { homedir as homedir3 } from "os";
729
659
  import { join as join3, basename as basename2 } from "path";
730
- import { Database as Database2 } from "bun:sqlite";
660
+ import { Database as BunDatabase } from "bun:sqlite";
731
661
  var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
732
662
  var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
733
663
  async function ingestCodex(db, verbose = false) {
@@ -739,7 +669,7 @@ async function ingestCodex(db, verbose = false) {
739
669
  let codexDb = null;
740
670
  let ingested = 0;
741
671
  try {
742
- codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
672
+ codexDb = new BunDatabase(CODEX_DB_PATH, { readonly: true });
743
673
  const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
744
674
  for (const thread of threads) {
745
675
  const stateKey = thread.id;
@@ -850,11 +780,91 @@ async function ingestGemini(db, verbose) {
850
780
  return { sessions: totalSessions };
851
781
  }
852
782
 
783
+ // src/lib/package-metadata.ts
784
+ import { readFileSync as readFileSync4 } from "fs";
785
+ var cachedMetadata = null;
786
+ function getPackageMetadata() {
787
+ if (cachedMetadata)
788
+ return cachedMetadata;
789
+ const raw = readFileSync4(new URL("../../package.json", import.meta.url), "utf8");
790
+ const parsed = JSON.parse(raw);
791
+ cachedMetadata = {
792
+ name: parsed.name ?? "@hasna/economy",
793
+ version: parsed.version ?? "0.0.0"
794
+ };
795
+ return cachedMetadata;
796
+ }
797
+ var packageMetadata = getPackageMetadata();
798
+
853
799
  // src/mcp/index.ts
854
800
  init_pricing();
801
+ function printHelp() {
802
+ console.log(`Usage: economy-mcp [options]
803
+
804
+ Runs the ${packageMetadata.name} MCP stdio server.
805
+
806
+ Options:
807
+ -V, --version output the version number
808
+ -h, --help display help for command`);
809
+ }
810
+ var args = process.argv.slice(2);
811
+ if (args.includes("--help") || args.includes("-h")) {
812
+ printHelp();
813
+ process.exit(0);
814
+ }
815
+ if (args.includes("--version") || args.includes("-V")) {
816
+ console.log(packageMetadata.version);
817
+ process.exit(0);
818
+ }
855
819
  var db = openDatabase();
856
820
  ensurePricingSeeded(db);
857
- var server = new Server({ name: "economy", version: "0.2.2" }, { capabilities: { tools: {} } });
821
+ var server = new McpServer({
822
+ name: "economy",
823
+ version: packageMetadata.version
824
+ });
825
+ var _econAgents = new Map;
826
+ var TOOL_NAMES = [
827
+ "get_cost_summary",
828
+ "get_sessions",
829
+ "get_top_sessions",
830
+ "get_model_breakdown",
831
+ "get_project_breakdown",
832
+ "get_budget_status",
833
+ "get_daily",
834
+ "get_session_detail",
835
+ "sync",
836
+ "search_tools",
837
+ "describe_tools",
838
+ "get_goals",
839
+ "set_goal",
840
+ "remove_goal",
841
+ "register_agent",
842
+ "heartbeat",
843
+ "set_focus",
844
+ "list_agents",
845
+ "send_feedback"
846
+ ];
847
+ 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",
850
+ get_top_sessions: "n(10), agent(claude|codex|gemini) -> top sessions by cost",
851
+ get_model_breakdown: "no params -> model, requests, tokens, cost",
852
+ get_project_breakdown: "no params -> project_name, sessions, cost",
853
+ get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
854
+ get_daily: "days(30) -> daily cost table grouped by date and agent",
855
+ get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
856
+ sync: "sources(all|claude|codex|gemini) -> ingest latest cost data",
857
+ search_tools: "query substring -> tool name list",
858
+ describe_tools: "names[] -> one-line parameter hints",
859
+ get_goals: "no params -> goal progress summary",
860
+ set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
861
+ remove_goal: "id -> delete goal",
862
+ register_agent: "name, session_id? -> register agent session",
863
+ heartbeat: "agent_id -> update last_seen_at",
864
+ set_focus: "agent_id, project_id? -> set active project context",
865
+ list_agents: "no params -> registered agent list",
866
+ send_feedback: "message, email?, category? -> save feedback locally"
867
+ };
858
868
  var fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
859
869
  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
870
  function fmtSession(s) {
@@ -865,262 +875,226 @@ function fmtSession(s) {
865
875
  const tok = fmtTok(Number(s["total_tokens"] ?? 0));
866
876
  return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
867
877
  }
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(`
878
+ function text(text2) {
879
+ return { content: [{ type: "text", text: text2 }] };
880
+ }
881
+ function textError(message) {
882
+ return { content: [{ type: "text", text: message }], isError: true };
883
+ }
884
+ server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z.string().optional() }, async ({ query }) => {
885
+ const q = query?.toLowerCase();
886
+ const matches = q ? TOOL_NAMES.filter((name) => name.includes(q)) : [...TOOL_NAMES];
887
+ return text(matches.join(", "));
888
+ });
889
+ server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z.array(z.string()) }, async ({ names }) => {
890
+ const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
932
891
  `);
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 };
892
+ return text(result);
893
+ });
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 }) => {
895
+ const resolved = period ?? "today";
896
+ const s = querySummary(db, resolved);
897
+ return text([
898
+ `period: ${resolved}`,
899
+ `cost: ${fmtUsd(s.total_usd)}`,
900
+ `sessions: ${s.sessions}`,
901
+ `requests: ${s.requests.toLocaleString()}`,
902
+ `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)`
904
+ ].join(`
905
+ `));
906
+ });
907
+ server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, limit(20)", {
908
+ agent: z.enum(["claude", "codex", "gemini"]).optional(),
909
+ project: z.string().optional(),
910
+ limit: z.number().int().positive().max(100).optional()
911
+ }, async ({ agent, project, limit }) => {
912
+ const sessions = querySessions(db, {
913
+ agent,
914
+ project,
915
+ limit: limit ?? 20
916
+ });
917
+ const lines = ["id agent cost tokens project"];
918
+ for (const session of sessions)
919
+ lines.push(fmtSession(session));
920
+ return text(lines.join(`
921
+ `));
922
+ });
923
+ server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
924
+ n: z.number().int().positive().max(100).optional(),
925
+ agent: z.enum(["claude", "codex", "gemini"]).optional()
926
+ }, async ({ n, agent }) => {
927
+ const sessions = queryTopSessions(db, n ?? 10, agent);
928
+ const lines = ["rank id agent cost tokens project"];
929
+ sessions.forEach((session, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(session)}`));
930
+ return text(lines.join(`
931
+ `));
932
+ });
933
+ server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
934
+ const rows = queryModelBreakdown(db);
935
+ const lines = ["model reqs tokens cost"];
936
+ for (const row of rows) {
937
+ 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"]))}`);
938
+ }
939
+ return text(lines.join(`
940
+ `));
941
+ });
942
+ server.tool("get_project_breakdown", "Cost per project. No params.", {}, async () => {
943
+ const rows = queryProjectBreakdown(db);
944
+ const lines = ["project sessions tokens cost"];
945
+ for (const row of rows) {
946
+ const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
947
+ lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
948
+ }
949
+ return text(lines.join(`
950
+ `));
951
+ });
952
+ server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
953
+ const budgets = getBudgetStatuses(db);
954
+ if (budgets.length === 0)
955
+ return text("No budgets set.");
956
+ const lines = ["scope period spent limit used% status"];
957
+ for (const budget of budgets) {
958
+ const scope = String(budget["project_path"] ?? "global").slice(0, 20);
959
+ const pct = Number(budget["percent_used"]).toFixed(1);
960
+ const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
961
+ 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}`);
962
+ }
963
+ return text(lines.join(`
964
+ `));
965
+ });
966
+ server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
967
+ const rows = queryDailyBreakdown(db, days ?? 30);
968
+ const byDate = new Map;
969
+ for (const row of rows) {
970
+ const date = String(row["date"]);
971
+ const entry = byDate.get(date) ?? { claude: 0, codex: 0, gemini: 0 };
972
+ if (row["agent"] === "claude")
973
+ entry.claude += Number(row["cost_usd"]);
974
+ else if (row["agent"] === "codex")
975
+ entry.codex += Number(row["cost_usd"]);
976
+ else if (row["agent"] === "gemini")
977
+ entry.gemini += Number(row["cost_usd"]);
978
+ byDate.set(date, entry);
979
+ }
980
+ const lines = ["date claude codex gemini total"];
981
+ for (const [date, costs] of [...byDate.entries()].sort()) {
982
+ const total = costs.claude + costs.codex + costs.gemini;
983
+ lines.push(`${date} ${fmtUsd(costs.claude).padEnd(11)}${fmtUsd(costs.codex).padEnd(11)}${fmtUsd(costs.gemini).padEnd(11)}${fmtUsd(total)}`);
984
+ }
985
+ return text(lines.join(`
986
+ `));
987
+ });
988
+ server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
989
+ const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(session_id, `${session_id}%`);
990
+ if (!session)
991
+ return textError(`Session not found: ${session_id}`);
992
+ const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
993
+ const lines = [
994
+ `session: ${String(session["id"]).slice(0, 16)}`,
995
+ `agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
996
+ `cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
997
+ "",
998
+ "time model input output cost"
999
+ ];
1000
+ for (const request of requests) {
1001
+ 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"]))}`);
1002
+ }
1003
+ return text(lines.join(`
1004
+ `));
1005
+ });
1006
+ server.tool("sync", "Ingest new cost data. sources: all|claude|codex|gemini", { sources: z.enum(["all", "claude", "codex", "gemini"]).optional() }, async ({ sources }) => {
1007
+ const selected = sources ?? "all";
1008
+ const parts = [];
1009
+ if (selected === "all" || selected === "claude") {
1010
+ const result = await ingestClaude(db);
1011
+ parts.push(`claude: ${result["files"]} files, ${result["requests"]} requests, ${result["sessions"]} sessions`);
1012
+ }
1013
+ if (selected === "all" || selected === "codex") {
1014
+ const result = await ingestCodex(db);
1015
+ parts.push(`codex: ${result["sessions"]} sessions`);
1016
+ }
1017
+ if (selected === "all" || selected === "gemini") {
1018
+ const result = await ingestGemini(db);
1019
+ parts.push(`gemini: ${result["sessions"]} sessions`);
1020
+ }
1021
+ return text(parts.join(`
1022
+ `) || "done");
1023
+ });
1024
+ server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
1025
+ const goals = getGoalStatuses(db);
1026
+ if (goals.length === 0)
1027
+ return text("No goals set.");
1028
+ const lines = ["period scope limit spent used% status"];
1029
+ for (const goal of goals) {
1030
+ const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
1031
+ const pct = Number(goal["percent_used"]).toFixed(1);
1032
+ const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
1033
+ 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}`);
1034
+ }
1035
+ return text(lines.join(`
1036
+ `));
1037
+ });
1038
+ server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
1039
+ period: z.enum(["day", "week", "month", "year"]),
1040
+ limit_usd: z.number().nonnegative(),
1041
+ project_path: z.string().optional(),
1042
+ agent: z.string().optional()
1043
+ }, async ({ period, limit_usd, project_path, agent }) => {
1044
+ const now = new Date().toISOString();
1045
+ upsertGoal(db, {
1046
+ id: randomUUID(),
1047
+ period,
1048
+ project_path: project_path ?? null,
1049
+ agent: agent ?? null,
1050
+ limit_usd,
1051
+ created_at: now,
1052
+ updated_at: now
1053
+ });
1054
+ return text(`Goal set: ${period} $${limit_usd}`);
1055
+ });
1056
+ server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
1057
+ deleteGoal(db, id);
1058
+ return text("Goal removed.");
1059
+ });
1060
+ server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
1061
+ const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
1062
+ if (existing) {
1063
+ existing.last_seen_at = new Date().toISOString();
1064
+ return text(JSON.stringify(existing));
1065
+ }
1066
+ const id = Math.random().toString(36).slice(2, 10);
1067
+ const agent = { id, name, last_seen_at: new Date().toISOString() };
1068
+ _econAgents.set(id, agent);
1069
+ return text(JSON.stringify(agent));
1070
+ });
1071
+ server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
1072
+ const agent = _econAgents.get(agent_id);
1073
+ if (!agent)
1074
+ return textError("Agent not found");
1075
+ agent.last_seen_at = new Date().toISOString();
1076
+ return text(`\u2665 ${agent.name}`);
1077
+ });
1078
+ server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
1079
+ const agent = _econAgents.get(agent_id);
1080
+ if (!agent)
1081
+ return textError("Agent not found");
1082
+ agent.project_id = project_id ?? undefined;
1083
+ return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
1084
+ });
1085
+ server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
1086
+ server.tool("send_feedback", "Send feedback about this service.", {
1087
+ message: z.string(),
1088
+ email: z.string().optional(),
1089
+ category: z.enum(["bug", "feature", "general"]).optional()
1090
+ }, async ({ message, email, category }) => {
1091
+ try {
1092
+ db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(message, email ?? null, category ?? "general", packageMetadata.version);
1093
+ return text("Feedback saved. Thank you!");
1094
+ } catch (error) {
1095
+ return textError(String(error));
1122
1096
  }
1123
1097
  });
1124
- var _econAgents = new Map;
1125
1098
  var transport = new StdioServerTransport;
1099
+ registerCloudTools(server, "economy");
1126
1100
  await server.connect(transport);