@hasna/economy 0.2.0 → 0.2.2

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/cli/index.js CHANGED
@@ -844,6 +844,11 @@ function ok(data, meta) {
844
844
  function err(message, status = 400) {
845
845
  return json({ error: message }, status);
846
846
  }
847
+ function applyFields(obj, fields) {
848
+ if (!fields || fields.length === 0)
849
+ return obj;
850
+ return Object.fromEntries(fields.map((f) => [f, obj[f] ?? null]));
851
+ }
847
852
  function createHandler(db) {
848
853
  return async function handler(req) {
849
854
  const url = new URL(req.url);
@@ -867,8 +872,10 @@ function createHandler(db) {
867
872
  const limit = Number(url.searchParams.get("limit") ?? 50);
868
873
  const offset = Number(url.searchParams.get("offset") ?? 0);
869
874
  const since = url.searchParams.get("since") ?? undefined;
875
+ const fieldsParam = url.searchParams.get("fields");
876
+ const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
870
877
  const sessions = querySessions(db, { agent: agent ?? undefined, project, limit, offset, since });
871
- return ok(sessions, { limit, offset });
878
+ return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
872
879
  }
873
880
  if (path === "/api/top" && method === "GET") {
874
881
  const n = Number(url.searchParams.get("n") ?? 10);
@@ -1166,7 +1173,7 @@ program.command("month").description("Cost summary for this month").action(async
1166
1173
  await autoSync();
1167
1174
  printSummary("This Month", "month");
1168
1175
  });
1169
- program.command("sessions").description("List coding sessions with costs").option("--agent <agent>", "Filter by agent (claude|codex)").option("--project <path>", "Filter by project path").option("--limit <n>", "Number of sessions", "20").action(async (opts) => {
1176
+ program.command("sessions").description("List coding sessions with costs").option("--agent <agent>", "Filter by agent (claude|codex)").option("--project <path>", "Filter by project path").option("--limit <n>", "Number of sessions", "20").option("--format <fmt>", "Output format: table|compact|csv|json", "table").action(async (opts) => {
1170
1177
  await autoSync();
1171
1178
  const db = openDatabase();
1172
1179
  const sessions = querySessions(db, {
@@ -1175,7 +1182,24 @@ program.command("sessions").description("List coding sessions with costs").optio
1175
1182
  limit: Number(opts.limit ?? 20)
1176
1183
  });
1177
1184
  if (sessions.length === 0) {
1178
- console.log(chalk2.yellow("No sessions found. Run `economy sync` first."));
1185
+ console.log(chalk2.yellow("No sessions found."));
1186
+ return;
1187
+ }
1188
+ const f = opts.format ?? "table";
1189
+ if (f === "compact") {
1190
+ for (const s of sessions)
1191
+ process.stdout.write(`${s.id.slice(0, 8)} ${s.agent} ${fmt2(s.total_cost_usd)} ${fmtTokens(s.total_tokens)} ${s.project_name || "\u2014"}
1192
+ `);
1193
+ return;
1194
+ }
1195
+ if (f === "json") {
1196
+ console.log(JSON.stringify(sessions, null, 2));
1197
+ return;
1198
+ }
1199
+ if (f === "csv") {
1200
+ console.log("id,agent,project_name,total_cost_usd,total_tokens,request_count,started_at");
1201
+ for (const s of sessions)
1202
+ console.log(`${s.id},${s.agent},"${s.project_name}",${s.total_cost_usd},${s.total_tokens},${s.request_count},${s.started_at}`);
1179
1203
  return;
1180
1204
  }
1181
1205
  console.log();
package/dist/mcp/index.js CHANGED
@@ -391,7 +391,6 @@ var init_database = () => {};
391
391
 
392
392
  // src/mcp/index.ts
393
393
  init_database();
394
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
395
394
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
396
395
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
397
396
 
@@ -596,80 +595,67 @@ async function ingestCodex(db, verbose = false) {
596
595
  init_pricing();
597
596
  var db = openDatabase();
598
597
  ensurePricingSeeded(db);
599
- var server = new Server({ name: "economy", version: "0.1.0" }, { capabilities: { tools: {} } });
598
+ var fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
599
+ 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);
600
+ function fmtSession(s) {
601
+ const id = String(s["id"] ?? "").slice(0, 8);
602
+ const agent = String(s["agent"] ?? "");
603
+ const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
604
+ const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
605
+ const tok = fmtTok(Number(s["total_tokens"] ?? 0));
606
+ return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
607
+ }
600
608
  var TOOLS = [
601
- {
602
- name: "get_cost_summary",
603
- description: "Get total cost summary for a time period",
604
- inputSchema: {
605
- type: "object",
606
- properties: {
607
- period: { type: "string", enum: ["today", "week", "month", "all"], description: "Time period", default: "today" }
608
- }
609
- }
610
- },
611
- {
612
- name: "get_sessions",
613
- description: "List coding sessions with cost data",
614
- inputSchema: {
615
- type: "object",
616
- properties: {
617
- agent: { type: "string", enum: ["claude", "codex"], description: "Filter by agent" },
618
- project: { type: "string", description: "Filter by project path (partial match)" },
619
- limit: { type: "number", description: "Max results", default: 20 }
620
- }
621
- }
622
- },
623
- {
624
- name: "get_top_sessions",
625
- description: "Get the most expensive coding sessions",
626
- inputSchema: {
627
- type: "object",
628
- properties: {
629
- n: { type: "number", description: "Number of sessions to return", default: 10 },
630
- agent: { type: "string", enum: ["claude", "codex"], description: "Filter by agent" }
631
- }
632
- }
633
- },
634
- {
635
- name: "get_model_breakdown",
636
- description: "Get cost breakdown by AI model",
637
- inputSchema: { type: "object", properties: {} }
638
- },
639
- {
640
- name: "get_project_breakdown",
641
- description: "Get cost breakdown by project",
642
- inputSchema: { type: "object", properties: {} }
643
- },
644
- {
645
- name: "get_budget_status",
646
- description: "Get current budget status and spending vs limits",
647
- inputSchema: { type: "object", properties: {} }
648
- },
649
- {
650
- name: "sync",
651
- description: "Trigger cost data ingestion from Claude Code and/or Codex",
652
- inputSchema: {
653
- type: "object",
654
- properties: {
655
- sources: { type: "string", enum: ["all", "claude", "codex"], default: "all" }
656
- }
657
- }
658
- }
609
+ { 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"] } } } },
610
+ { 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" } } } },
611
+ { name: "get_top_sessions", description: "Top sessions by cost. Params: n(10), agent", inputSchema: { type: "object", properties: { n: { type: "number" }, agent: { type: "string" } } } },
612
+ { name: "get_model_breakdown", description: "Cost per model. No params.", inputSchema: { type: "object", properties: {} } },
613
+ { name: "get_project_breakdown", description: "Cost per project. No params.", inputSchema: { type: "object", properties: {} } },
614
+ { name: "get_budget_status", description: "Budget limits vs spend, percent used, alert flags. No params.", inputSchema: { type: "object", properties: {} } },
615
+ { name: "sync", description: "Ingest new cost data. sources: all|claude|codex", inputSchema: { type: "object", properties: { sources: { type: "string", enum: ["all", "claude", "codex"] } } } },
616
+ { name: "search_tools", description: "List tool names matching query. Use first to find relevant tools.", inputSchema: { type: "object", properties: { query: { type: "string" } } } },
617
+ { name: "describe_tools", description: "Get param hints for specific tools by name.", inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" } } }, required: ["names"] } }
659
618
  ];
619
+ var TOOL_DESCRIPTIONS = {
620
+ get_cost_summary: "period(today|week|month|all) \u2192 {total_usd, sessions, requests, tokens, summary}",
621
+ get_sessions: "agent(claude|codex), project(partial), limit(20) \u2192 compact session table",
622
+ get_top_sessions: "n(10), agent(claude|codex) \u2192 top sessions by cost",
623
+ get_model_breakdown: "no params \u2192 model, requests, tokens, cost",
624
+ get_project_breakdown: "no params \u2192 project_name, sessions, cost",
625
+ get_budget_status: "no params \u2192 budget limits, current spend, percent_used, is_over_alert",
626
+ sync: "sources(all|claude|codex) \u2192 {files, requests, sessions} ingested"
627
+ };
660
628
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
661
629
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
662
630
  const { name, arguments: args } = req.params;
663
631
  const a = args ?? {};
664
632
  try {
665
633
  switch (name) {
634
+ case "search_tools": {
635
+ const q = a["query"]?.toLowerCase();
636
+ const names = TOOLS.map((t) => t.name);
637
+ const matches = q ? names.filter((n) => n.includes(q)) : names;
638
+ return { content: [{ type: "text", text: matches.join(", ") }] };
639
+ }
640
+ case "describe_tools": {
641
+ const names = a["names"] ?? [];
642
+ const result = names.map((n) => `${n}: ${TOOL_DESCRIPTIONS[n] ?? "see tool schema"}`).join(`
643
+ `);
644
+ return { content: [{ type: "text", text: result }] };
645
+ }
666
646
  case "get_cost_summary": {
667
647
  const period = a["period"] ?? "today";
668
- const summary = querySummary(db, period);
669
- const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
670
- const 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);
671
- const result = { ...summary, summary: `You've spent ${fmtUsd(summary.total_usd)} ${period === "all" ? "total" : period} across ${summary.sessions} sessions (${summary.requests.toLocaleString()} requests, ${fmtTok(summary.tokens)} tokens)` };
672
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
648
+ const s = querySummary(db, period);
649
+ const text = [
650
+ `period: ${period}`,
651
+ `cost: ${fmtUsd(s.total_usd)}`,
652
+ `sessions: ${s.sessions}`,
653
+ `requests: ${s.requests.toLocaleString()}`,
654
+ `tokens: ${fmtTok(s.tokens)}`,
655
+ `summary: You've spent ${fmtUsd(s.total_usd)} ${period === "all" ? "total" : period} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
656
+ ].join(`
657
+ `);
658
+ return { content: [{ type: "text", text }] };
673
659
  }
674
660
  case "get_sessions": {
675
661
  const sessions = querySessions(db, {
@@ -677,29 +663,65 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
677
663
  project: a["project"],
678
664
  limit: Number(a["limit"] ?? 20)
679
665
  });
680
- return { content: [{ type: "text", text: JSON.stringify(sessions, null, 2) }] };
666
+ const lines = ["id agent cost tokens project"];
667
+ for (const s of sessions)
668
+ lines.push(fmtSession(s));
669
+ return { content: [{ type: "text", text: lines.join(`
670
+ `) }] };
681
671
  }
682
672
  case "get_top_sessions": {
683
673
  const sessions = queryTopSessions(db, Number(a["n"] ?? 10), a["agent"]);
684
- return { content: [{ type: "text", text: JSON.stringify(sessions, null, 2) }] };
674
+ const lines = ["rank id agent cost tokens project"];
675
+ sessions.forEach((s, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(s)}`));
676
+ return { content: [{ type: "text", text: lines.join(`
677
+ `) }] };
685
678
  }
686
679
  case "get_model_breakdown": {
687
- return { content: [{ type: "text", text: JSON.stringify(queryModelBreakdown(db), null, 2) }] };
680
+ const rows = queryModelBreakdown(db);
681
+ const lines = ["model reqs tokens cost"];
682
+ for (const r of rows) {
683
+ 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"]))}`);
684
+ }
685
+ return { content: [{ type: "text", text: lines.join(`
686
+ `) }] };
688
687
  }
689
688
  case "get_project_breakdown": {
690
- return { content: [{ type: "text", text: JSON.stringify(queryProjectBreakdown(db), null, 2) }] };
689
+ const rows = queryProjectBreakdown(db);
690
+ const lines = ["project sessions tokens cost"];
691
+ for (const r of rows) {
692
+ const name2 = String(r["project_name"] || r["project_path"] || "\u2014").slice(0, 20);
693
+ lines.push(`${name2.padEnd(21)}${String(r["sessions"]).padEnd(9)}${fmtTok(Number(r["total_tokens"])).padEnd(9)}${fmtUsd(Number(r["cost_usd"]))}`);
694
+ }
695
+ return { content: [{ type: "text", text: lines.join(`
696
+ `) }] };
691
697
  }
692
698
  case "get_budget_status": {
693
- return { content: [{ type: "text", text: JSON.stringify(getBudgetStatuses(db), null, 2) }] };
699
+ const budgets = getBudgetStatuses(db);
700
+ if (budgets.length === 0)
701
+ return { content: [{ type: "text", text: "No budgets set." }] };
702
+ const lines = ["scope period spent limit used% status"];
703
+ for (const b of budgets) {
704
+ const scope = String(b["project_path"] ?? "global").slice(0, 20);
705
+ const pct = Number(b["percent_used"]).toFixed(1);
706
+ const status = b["is_over_limit"] ? "OVER" : b["is_over_alert"] ? "ALERT" : "OK";
707
+ 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}`);
708
+ }
709
+ return { content: [{ type: "text", text: lines.join(`
710
+ `) }] };
694
711
  }
695
712
  case "sync": {
696
713
  const sources = a["sources"] ?? "all";
697
- const results = {};
698
- if (sources === "all" || sources === "claude")
699
- results["claude"] = await ingestClaude(db);
700
- if (sources === "all" || sources === "codex")
701
- results["codex"] = await ingestCodex(db);
702
- return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
714
+ const parts = [];
715
+ if (sources === "all" || sources === "claude") {
716
+ const r = await ingestClaude(db);
717
+ parts.push(`claude: ${r["files"]} files, ${r["requests"]} requests, ${r["sessions"]} sessions`);
718
+ }
719
+ if (sources === "all" || sources === "codex") {
720
+ const r = await ingestCodex(db);
721
+ parts.push(`codex: ${r["sessions"]} sessions`);
722
+ }
723
+ return { content: [{ type: "text", text: parts.join(`
724
+ `) || "done" }] };
703
725
  }
704
726
  default:
705
727
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
@@ -646,6 +646,11 @@ function ok(data, meta) {
646
646
  function err(message, status = 400) {
647
647
  return json({ error: message }, status);
648
648
  }
649
+ function applyFields(obj, fields) {
650
+ if (!fields || fields.length === 0)
651
+ return obj;
652
+ return Object.fromEntries(fields.map((f) => [f, obj[f] ?? null]));
653
+ }
649
654
  function createHandler(db) {
650
655
  return async function handler(req) {
651
656
  const url = new URL(req.url);
@@ -669,8 +674,10 @@ function createHandler(db) {
669
674
  const limit = Number(url.searchParams.get("limit") ?? 50);
670
675
  const offset = Number(url.searchParams.get("offset") ?? 0);
671
676
  const since = url.searchParams.get("since") ?? undefined;
677
+ const fieldsParam = url.searchParams.get("fields");
678
+ const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
672
679
  const sessions = querySessions(db, { agent: agent ?? undefined, project, limit, offset, since });
673
- return ok(sessions, { limit, offset });
680
+ return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
674
681
  }
675
682
  if (path === "/api/top" && method === "GET") {
676
683
  const n = Number(url.searchParams.get("n") ?? 10);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",
@@ -48,7 +48,7 @@
48
48
  "author": "hasna",
49
49
  "license": "Apache-2.0",
50
50
  "publishConfig": {
51
- "access": "restricted",
51
+ "access": "public",
52
52
  "registry": "https://registry.npmjs.org/"
53
53
  },
54
54
  "dependencies": {