@hasna/economy 0.2.9 → 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.
Files changed (46) hide show
  1. package/README.md +37 -45
  2. package/dist/cli/brains.d.ts +3 -0
  3. package/dist/cli/brains.d.ts.map +1 -0
  4. package/dist/cli/commands/menubar.d.ts +7 -0
  5. package/dist/cli/commands/menubar.d.ts.map +1 -0
  6. package/dist/cli/commands/watch.d.ts +9 -0
  7. package/dist/cli/commands/watch.d.ts.map +1 -0
  8. package/dist/cli/index.d.ts +3 -0
  9. package/dist/cli/index.d.ts.map +1 -0
  10. package/dist/cli/index.js +127 -82
  11. package/dist/db/database.d.ts +68 -0
  12. package/dist/db/database.d.ts.map +1 -0
  13. package/dist/db/pg-migrations.d.ts +7 -0
  14. package/dist/db/pg-migrations.d.ts.map +1 -0
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +2 -2
  18. package/dist/ingest/claude.d.ts +7 -0
  19. package/dist/ingest/claude.d.ts.map +1 -0
  20. package/dist/ingest/codex.d.ts +7 -0
  21. package/dist/ingest/codex.d.ts.map +1 -0
  22. package/dist/ingest/gemini.d.ts +5 -0
  23. package/dist/ingest/gemini.d.ts.map +1 -0
  24. package/dist/lib/config.d.ts +13 -0
  25. package/dist/lib/config.d.ts.map +1 -0
  26. package/dist/lib/gatherer.d.ts +21 -0
  27. package/dist/lib/gatherer.d.ts.map +1 -0
  28. package/dist/lib/model-config.d.ts +8 -0
  29. package/dist/lib/model-config.d.ts.map +1 -0
  30. package/dist/lib/package-metadata.d.ts +8 -0
  31. package/dist/lib/package-metadata.d.ts.map +1 -0
  32. package/dist/lib/pricing.d.ts +10 -0
  33. package/dist/lib/pricing.d.ts.map +1 -0
  34. package/dist/lib/webhooks.d.ts +3 -0
  35. package/dist/lib/webhooks.d.ts.map +1 -0
  36. package/dist/mcp/index.d.ts +3 -0
  37. package/dist/mcp/index.d.ts.map +1 -0
  38. package/dist/mcp/index.js +305 -326
  39. package/dist/server/index.d.ts +3 -0
  40. package/dist/server/index.d.ts.map +1 -0
  41. package/dist/server/index.js +168 -10
  42. package/dist/server/serve.d.ts +4 -0
  43. package/dist/server/serve.d.ts.map +1 -0
  44. package/dist/types/index.d.ts +101 -0
  45. package/dist/types/index.d.ts.map +1 -0
  46. package/package.json +6 -4
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,80 +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.8",
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
- },
547
- keywords: [
548
- "economy",
549
- "cost",
550
- "ai",
551
- "claude",
552
- "codex",
553
- "gemini",
554
- "mcp",
555
- "cli",
556
- "budget",
557
- "tracking"
558
- ],
559
- author: "hasna",
560
- license: "Apache-2.0",
561
- publishConfig: {
562
- registry: "https://registry.npmjs.org",
563
- access: "public"
564
- },
565
- dependencies: {
566
- "@hasna/cloud": "^0.1.0",
567
- "@modelcontextprotocol/sdk": "^1.12.1",
568
- chalk: "^5.4.1",
569
- commander: "^13.1.0"
570
- },
571
- devDependencies: {
572
- "@types/bun": "latest",
573
- "bun-types": "latest",
574
- typescript: "^5.7.2"
575
- }
576
- };
577
- });
578
-
579
508
  // src/mcp/index.ts
580
509
  init_database();
581
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
510
+ import { randomUUID } from "crypto";
511
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
582
512
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
583
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
513
+ import { registerCloudTools } from "@hasna/cloud";
514
+ import { z } from "zod";
584
515
 
585
516
  // src/ingest/claude.ts
586
517
  init_database();
@@ -726,7 +657,7 @@ init_database();
726
657
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
727
658
  import { homedir as homedir3 } from "os";
728
659
  import { join as join3, basename as basename2 } from "path";
729
- import { Database as Database2 } from "bun:sqlite";
660
+ import { Database as BunDatabase } from "bun:sqlite";
730
661
  var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
731
662
  var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
732
663
  async function ingestCodex(db, verbose = false) {
@@ -738,7 +669,7 @@ async function ingestCodex(db, verbose = false) {
738
669
  let codexDb = null;
739
670
  let ingested = 0;
740
671
  try {
741
- codexDb = new Database2(CODEX_DB_PATH, { readonly: true });
672
+ codexDb = new BunDatabase(CODEX_DB_PATH, { readonly: true });
742
673
  const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
743
674
  for (const thread of threads) {
744
675
  const stateKey = thread.id;
@@ -849,11 +780,91 @@ async function ingestGemini(db, verbose) {
849
780
  return { sessions: totalSessions };
850
781
  }
851
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
+
852
799
  // src/mcp/index.ts
853
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
+ }
854
819
  var db = openDatabase();
855
820
  ensurePricingSeeded(db);
856
- 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
+ };
857
868
  var fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
858
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);
859
870
  function fmtSession(s) {
@@ -864,258 +875,226 @@ function fmtSession(s) {
864
875
  const tok = fmtTok(Number(s["total_tokens"] ?? 0));
865
876
  return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
866
877
  }
867
- var TOOLS = [
868
- { 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"] } } } },
869
- { 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" } } } },
870
- { name: "get_top_sessions", description: "Top sessions by cost. Params: n(10), agent", inputSchema: { type: "object", properties: { n: { type: "number" }, agent: { type: "string" } } } },
871
- { name: "get_model_breakdown", description: "Cost per model. No params.", inputSchema: { type: "object", properties: {} } },
872
- { name: "get_project_breakdown", description: "Cost per project. No params.", inputSchema: { type: "object", properties: {} } },
873
- { name: "get_budget_status", description: "Budget limits vs spend, percent used, alert flags. No params.", inputSchema: { type: "object", properties: {} } },
874
- { name: "get_daily", description: "Daily cost table by agent. Params: days(30)", inputSchema: { type: "object", properties: { days: { type: "number" } } } },
875
- { 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"] } },
876
- { name: "sync", description: "Ingest new cost data. sources: all|claude|codex|gemini", inputSchema: { type: "object", properties: { sources: { type: "string", enum: ["all", "claude", "codex", "gemini"] } } } },
877
- { name: "search_tools", description: "List tool names matching query. Use first to find relevant tools.", inputSchema: { type: "object", properties: { query: { type: "string" } } } },
878
- { name: "describe_tools", description: "Get param hints for specific tools by name.", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" } } }, required: ["names"] } },
879
- { name: "get_goals", description: "All spending goals with current progress. No params.", inputSchema: { type: "object", properties: {} } },
880
- { 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"] } },
881
- { name: "remove_goal", description: "Delete a goal by id.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
882
- { name: "register_agent", description: "Register agent session.", inputSchema: { type: "object", properties: { name: { type: "string" }, session_id: { type: "string" } }, required: ["name"] } },
883
- { name: "heartbeat", description: "Update last_seen_at.", inputSchema: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] } },
884
- { name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } },
885
- { 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"] } }
886
- ];
887
- var TOOL_DESCRIPTIONS = {
888
- get_cost_summary: "period(today|week|month|year|all) \u2192 {total_usd, sessions, requests, tokens, summary}",
889
- get_sessions: "agent(claude|codex), project(partial), limit(20) \u2192 compact session table",
890
- get_top_sessions: "n(10), agent(claude|codex) \u2192 top sessions by cost",
891
- get_model_breakdown: "no params \u2192 model, requests, tokens, cost",
892
- get_project_breakdown: "no params \u2192 project_name, sessions, cost",
893
- get_budget_status: "no params \u2192 budget limits, current spend, percent_used, is_over_alert",
894
- get_daily: "days(30) \u2192 daily cost table grouped by date and agent",
895
- get_session_detail: "session_id(prefix ok) \u2192 per-request breakdown with model, tokens, cost",
896
- sync: "sources(all|claude|codex|gemini) \u2192 {files, requests, sessions} ingested",
897
- get_goals: "no params \u2192 period, scope, limit, spent, percent, status(ON TRACK/AT RISK/OVER)",
898
- set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? \u2192 creates/updates goal",
899
- remove_goal: "id \u2192 deletes goal"
900
- };
901
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
902
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
903
- const { name, arguments: args } = req.params;
904
- const a = args ?? {};
905
- try {
906
- switch (name) {
907
- case "search_tools": {
908
- const q = a["query"]?.toLowerCase();
909
- const names = TOOLS.map((t) => t.name);
910
- const matches = q ? names.filter((n) => n.includes(q)) : names;
911
- return { content: [{ type: "text", text: matches.join(", ") }] };
912
- }
913
- case "describe_tools": {
914
- const names = a["names"] ?? [];
915
- const result = names.map((n) => `${n}: ${TOOL_DESCRIPTIONS[n] ?? "see tool schema"}`).join(`
916
- `);
917
- return { content: [{ type: "text", text: result }] };
918
- }
919
- case "get_cost_summary": {
920
- const period = a["period"] ?? "today";
921
- const s = querySummary(db, period);
922
- const text = [
923
- `period: ${period}`,
924
- `cost: ${fmtUsd(s.total_usd)}`,
925
- `sessions: ${s.sessions}`,
926
- `requests: ${s.requests.toLocaleString()}`,
927
- `tokens: ${fmtTok(s.tokens)}`,
928
- `summary: You've spent ${fmtUsd(s.total_usd)} ${period === "all" ? "total" : period} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
929
- ].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(`
930
891
  `);
931
- return { content: [{ type: "text", text }] };
932
- }
933
- case "get_sessions": {
934
- const sessions = querySessions(db, {
935
- agent: a["agent"],
936
- project: a["project"],
937
- limit: Number(a["limit"] ?? 20)
938
- });
939
- const lines = ["id agent cost tokens project"];
940
- for (const s of sessions)
941
- lines.push(fmtSession(s));
942
- return { content: [{ type: "text", text: lines.join(`
943
- `) }] };
944
- }
945
- case "get_top_sessions": {
946
- const sessions = queryTopSessions(db, Number(a["n"] ?? 10), a["agent"]);
947
- const lines = ["rank id agent cost tokens project"];
948
- sessions.forEach((s, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(s)}`));
949
- return { content: [{ type: "text", text: lines.join(`
950
- `) }] };
951
- }
952
- case "get_model_breakdown": {
953
- const rows = queryModelBreakdown(db);
954
- const lines = ["model reqs tokens cost"];
955
- for (const r of rows) {
956
- 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"]))}`);
957
- }
958
- return { content: [{ type: "text", text: lines.join(`
959
- `) }] };
960
- }
961
- case "get_project_breakdown": {
962
- const rows = queryProjectBreakdown(db);
963
- const lines = ["project sessions tokens cost"];
964
- for (const r of rows) {
965
- const name2 = String(r["project_name"] || r["project_path"] || "\u2014").slice(0, 20);
966
- lines.push(`${name2.padEnd(21)}${String(r["sessions"]).padEnd(9)}${fmtTok(Number(r["total_tokens"])).padEnd(9)}${fmtUsd(Number(r["cost_usd"]))}`);
967
- }
968
- return { content: [{ type: "text", text: lines.join(`
969
- `) }] };
970
- }
971
- case "get_budget_status": {
972
- const budgets = getBudgetStatuses(db);
973
- if (budgets.length === 0)
974
- return { content: [{ type: "text", text: "No budgets set." }] };
975
- const lines = ["scope period spent limit used% status"];
976
- for (const b of budgets) {
977
- const scope = String(b["project_path"] ?? "global").slice(0, 20);
978
- const pct = Number(b["percent_used"]).toFixed(1);
979
- const status = b["is_over_limit"] ? "OVER" : b["is_over_alert"] ? "ALERT" : "OK";
980
- 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}`);
981
- }
982
- return { content: [{ type: "text", text: lines.join(`
983
- `) }] };
984
- }
985
- case "get_daily": {
986
- const days = Number(a["days"] ?? 30);
987
- const rows = queryDailyBreakdown(db, days);
988
- const lines = ["date claude codex gemini total"];
989
- const byDate = new Map;
990
- for (const r of rows) {
991
- const d = String(r["date"]);
992
- const entry = byDate.get(d) ?? { claude: 0, codex: 0, gemini: 0 };
993
- if (r["agent"] === "claude")
994
- entry.claude += Number(r["cost_usd"]);
995
- else if (r["agent"] === "codex")
996
- entry.codex += Number(r["cost_usd"]);
997
- else if (r["agent"] === "gemini")
998
- entry.gemini += Number(r["cost_usd"]);
999
- byDate.set(d, entry);
1000
- }
1001
- for (const [date, costs] of [...byDate.entries()].sort()) {
1002
- const total = costs.claude + costs.codex + costs.gemini;
1003
- lines.push(`${date} ${fmtUsd(costs.claude).padEnd(11)}${fmtUsd(costs.codex).padEnd(11)}${fmtUsd(costs.gemini).padEnd(11)}${fmtUsd(total)}`);
1004
- }
1005
- return { content: [{ type: "text", text: lines.join(`
1006
- `) }] };
1007
- }
1008
- case "get_session_detail": {
1009
- const sid = String(a["session_id"] ?? "");
1010
- const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(sid, `${sid}%`);
1011
- if (!session)
1012
- return { content: [{ type: "text", text: `Session not found: ${sid}` }], isError: true };
1013
- const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC LIMIT 50`).all(session["id"]);
1014
- const lines = [
1015
- `session: ${String(session["id"]).slice(0, 16)}`,
1016
- `agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
1017
- `cost: ${fmtUsd(Number(session["total_cost_usd"]))} tokens: ${fmtTok(Number(session["total_tokens"]))} requests: ${session["request_count"]}`,
1018
- "",
1019
- "time model input output cost"
1020
- ];
1021
- for (const r of requests) {
1022
- 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"]))}`);
1023
- }
1024
- return { content: [{ type: "text", text: lines.join(`
1025
- `) }] };
1026
- }
1027
- case "sync": {
1028
- const sources = a["sources"] ?? "all";
1029
- const parts = [];
1030
- if (sources === "all" || sources === "claude") {
1031
- const r = await ingestClaude(db);
1032
- parts.push(`claude: ${r["files"]} files, ${r["requests"]} requests, ${r["sessions"]} sessions`);
1033
- }
1034
- if (sources === "all" || sources === "codex") {
1035
- const r = await ingestCodex(db);
1036
- parts.push(`codex: ${r["sessions"]} sessions`);
1037
- }
1038
- if (sources === "all" || sources === "gemini") {
1039
- const r = await ingestGemini(db);
1040
- parts.push(`gemini: ${r["sessions"]} sessions`);
1041
- }
1042
- return { content: [{ type: "text", text: parts.join(`
1043
- `) || "done" }] };
1044
- }
1045
- case "get_goals": {
1046
- const goals = getGoalStatuses(db);
1047
- if (goals.length === 0)
1048
- return { content: [{ type: "text", text: "No goals set." }] };
1049
- const lines = ["period scope limit spent used% status"];
1050
- for (const g of goals) {
1051
- const scope = String(g["project_path"] ?? g["agent"] ?? "global").slice(0, 20);
1052
- const pct = Number(g["percent_used"]).toFixed(1);
1053
- const status = g["is_over"] ? "OVER" : g["is_at_risk"] ? "AT RISK" : "ON TRACK";
1054
- 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}`);
1055
- }
1056
- return { content: [{ type: "text", text: lines.join(`
1057
- `) }] };
1058
- }
1059
- case "set_goal": {
1060
- const { randomUUID } = await import("crypto");
1061
- const now = new Date().toISOString();
1062
- upsertGoal(db, {
1063
- id: randomUUID(),
1064
- period: String(a["period"] ?? "month"),
1065
- project_path: a["project_path"] ?? null,
1066
- agent: a["agent"] ?? null,
1067
- limit_usd: Number(a["limit_usd"]),
1068
- created_at: now,
1069
- updated_at: now
1070
- });
1071
- return { content: [{ type: "text", text: `Goal set: ${a["period"]} $${a["limit_usd"]}` }] };
1072
- }
1073
- case "remove_goal": {
1074
- deleteGoal(db, String(a["id"] ?? ""));
1075
- return { content: [{ type: "text", text: "Goal removed." }] };
1076
- }
1077
- case "register_agent": {
1078
- const n = String(args["name"] ?? "");
1079
- const ex = [..._econAgents.values()].find((x) => x.name === n);
1080
- if (ex) {
1081
- ex.last_seen_at = new Date().toISOString();
1082
- return { content: [{ type: "text", text: JSON.stringify(ex) }] };
1083
- }
1084
- const id = Math.random().toString(36).slice(2, 10);
1085
- const ag = { id, name: n, last_seen_at: new Date().toISOString() };
1086
- _econAgents.set(id, ag);
1087
- return { content: [{ type: "text", text: JSON.stringify(ag) }] };
1088
- }
1089
- case "heartbeat": {
1090
- const ag = _econAgents.get(String(args["agent_id"] ?? ""));
1091
- if (!ag)
1092
- return { content: [{ type: "text", text: `Agent not found` }], isError: true };
1093
- ag.last_seen_at = new Date().toISOString();
1094
- return { content: [{ type: "text", text: `\u2665 ${ag.name}` }] };
1095
- }
1096
- case "set_focus": {
1097
- const ag = _econAgents.get(String(args["agent_id"] ?? ""));
1098
- if (!ag)
1099
- return { content: [{ type: "text", text: `Agent not found` }], isError: true };
1100
- ag["project_id"] = args["project_id"];
1101
- return { content: [{ type: "text", text: String(args["project_id"] ? `Focus: ${args["project_id"]}` : "Focus cleared") }] };
1102
- }
1103
- case "send_feedback": {
1104
- try {
1105
- const pkg = require_package();
1106
- db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(String(a["message"]), a["email"] || null, a["category"] || "general", pkg.version);
1107
- return { content: [{ type: "text", text: "Feedback saved. Thank you!" }] };
1108
- } catch (e) {
1109
- return { content: [{ type: "text", text: String(e) }], isError: true };
1110
- }
1111
- }
1112
- default:
1113
- return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
1114
- }
1115
- } catch (e) {
1116
- 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));
1117
1096
  }
1118
1097
  });
1119
- var _econAgents = new Map;
1120
1098
  var transport = new StdioServerTransport;
1099
+ registerCloudTools(server, "economy");
1121
1100
  await server.connect(transport);