@hasna/economy 0.2.1 → 0.2.3

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);
@@ -1026,7 +1033,7 @@ import chalk2 from "chalk";
1026
1033
  import { randomUUID as randomUUID2 } from "crypto";
1027
1034
  import { execSync } from "child_process";
1028
1035
  var program = new Command;
1029
- program.name("economy").description("AI coding cost tracker \u2014 Claude Code, Codex, and Gemini").version("0.1.1");
1036
+ program.name("economy").description("AI coding cost tracker \u2014 Claude Code, Codex, and Gemini").version("0.2.2");
1030
1037
  async function autoSync() {
1031
1038
  const db = openDatabase();
1032
1039
  ensurePricingSeeded(db);
@@ -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();
@@ -0,0 +1,13 @@
1
+ export interface EconomyConfig {
2
+ port: number;
3
+ 'default-period': string;
4
+ 'auto-sync': boolean;
5
+ 'sync-interval': number;
6
+ 'alert-thresholds': number[];
7
+ 'webhook-url': string | null;
8
+ }
9
+ export declare function loadConfig(): EconomyConfig;
10
+ export declare function saveConfig(config: EconomyConfig): void;
11
+ export declare function getConfigValue(key: string): unknown;
12
+ export declare function setConfigValue(key: string, value: string): void;
13
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,gBAAgB,EAAE,MAAM,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;IACpB,eAAe,EAAE,MAAM,CAAA;IACvB,kBAAkB,EAAE,MAAM,EAAE,CAAA;IAC5B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAWD,wBAAgB,UAAU,IAAI,aAAa,CAQ1C;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAItD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGnD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAW/D"}
@@ -0,0 +1,3 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ export declare function checkAndFireWebhooks(db: Database): Promise<void>;
3
+ //# sourceMappingURL=webhooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../src/lib/webhooks.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAE1C,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBtE"}
package/dist/mcp/index.js CHANGED
@@ -596,80 +596,68 @@ async function ingestCodex(db, verbose = false) {
596
596
  init_pricing();
597
597
  var db = openDatabase();
598
598
  ensurePricingSeeded(db);
599
- var server = new Server({ name: "economy", version: "0.1.0" }, { capabilities: { tools: {} } });
599
+ var server = new Server({ name: "economy", version: "0.2.2" }, { capabilities: { tools: {} } });
600
+ var fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
601
+ 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);
602
+ function fmtSession(s) {
603
+ const id = String(s["id"] ?? "").slice(0, 8);
604
+ const agent = String(s["agent"] ?? "");
605
+ const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
606
+ const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
607
+ const tok = fmtTok(Number(s["total_tokens"] ?? 0));
608
+ return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
609
+ }
600
610
  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
- }
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"] } } } },
612
+ { 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
+ { name: "get_top_sessions", description: "Top sessions by cost. Params: n(10), agent", inputSchema: { type: "object", properties: { n: { type: "number" }, agent: { type: "string" } } } },
614
+ { name: "get_model_breakdown", description: "Cost per model. No params.", inputSchema: { type: "object", properties: {} } },
615
+ { name: "get_project_breakdown", description: "Cost per project. No params.", inputSchema: { type: "object", properties: {} } },
616
+ { 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"] } } } },
618
+ { 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"] } }
659
620
  ];
621
+ var TOOL_DESCRIPTIONS = {
622
+ get_cost_summary: "period(today|week|month|all) \u2192 {total_usd, sessions, requests, tokens, summary}",
623
+ get_sessions: "agent(claude|codex), project(partial), limit(20) \u2192 compact session table",
624
+ get_top_sessions: "n(10), agent(claude|codex) \u2192 top sessions by cost",
625
+ get_model_breakdown: "no params \u2192 model, requests, tokens, cost",
626
+ get_project_breakdown: "no params \u2192 project_name, sessions, cost",
627
+ 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"
629
+ };
660
630
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
661
631
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
662
632
  const { name, arguments: args } = req.params;
663
633
  const a = args ?? {};
664
634
  try {
665
635
  switch (name) {
636
+ case "search_tools": {
637
+ const q = a["query"]?.toLowerCase();
638
+ const names = TOOLS.map((t) => t.name);
639
+ const matches = q ? names.filter((n) => n.includes(q)) : names;
640
+ return { content: [{ type: "text", text: matches.join(", ") }] };
641
+ }
642
+ case "describe_tools": {
643
+ const names = a["names"] ?? [];
644
+ const result = names.map((n) => `${n}: ${TOOL_DESCRIPTIONS[n] ?? "see tool schema"}`).join(`
645
+ `);
646
+ return { content: [{ type: "text", text: result }] };
647
+ }
666
648
  case "get_cost_summary": {
667
649
  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) }] };
650
+ const s = querySummary(db, period);
651
+ const text = [
652
+ `period: ${period}`,
653
+ `cost: ${fmtUsd(s.total_usd)}`,
654
+ `sessions: ${s.sessions}`,
655
+ `requests: ${s.requests.toLocaleString()}`,
656
+ `tokens: ${fmtTok(s.tokens)}`,
657
+ `summary: You've spent ${fmtUsd(s.total_usd)} ${period === "all" ? "total" : period} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
658
+ ].join(`
659
+ `);
660
+ return { content: [{ type: "text", text }] };
673
661
  }
674
662
  case "get_sessions": {
675
663
  const sessions = querySessions(db, {
@@ -677,29 +665,65 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
677
665
  project: a["project"],
678
666
  limit: Number(a["limit"] ?? 20)
679
667
  });
680
- return { content: [{ type: "text", text: JSON.stringify(sessions, null, 2) }] };
668
+ const lines = ["id agent cost tokens project"];
669
+ for (const s of sessions)
670
+ lines.push(fmtSession(s));
671
+ return { content: [{ type: "text", text: lines.join(`
672
+ `) }] };
681
673
  }
682
674
  case "get_top_sessions": {
683
675
  const sessions = queryTopSessions(db, Number(a["n"] ?? 10), a["agent"]);
684
- return { content: [{ type: "text", text: JSON.stringify(sessions, null, 2) }] };
676
+ const lines = ["rank id agent cost tokens project"];
677
+ sessions.forEach((s, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(s)}`));
678
+ return { content: [{ type: "text", text: lines.join(`
679
+ `) }] };
685
680
  }
686
681
  case "get_model_breakdown": {
687
- return { content: [{ type: "text", text: JSON.stringify(queryModelBreakdown(db), null, 2) }] };
682
+ const rows = queryModelBreakdown(db);
683
+ const lines = ["model reqs tokens cost"];
684
+ for (const r of rows) {
685
+ 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"]))}`);
686
+ }
687
+ return { content: [{ type: "text", text: lines.join(`
688
+ `) }] };
688
689
  }
689
690
  case "get_project_breakdown": {
690
- return { content: [{ type: "text", text: JSON.stringify(queryProjectBreakdown(db), null, 2) }] };
691
+ const rows = queryProjectBreakdown(db);
692
+ const lines = ["project sessions tokens cost"];
693
+ for (const r of rows) {
694
+ const name2 = String(r["project_name"] || r["project_path"] || "\u2014").slice(0, 20);
695
+ lines.push(`${name2.padEnd(21)}${String(r["sessions"]).padEnd(9)}${fmtTok(Number(r["total_tokens"])).padEnd(9)}${fmtUsd(Number(r["cost_usd"]))}`);
696
+ }
697
+ return { content: [{ type: "text", text: lines.join(`
698
+ `) }] };
691
699
  }
692
700
  case "get_budget_status": {
693
- return { content: [{ type: "text", text: JSON.stringify(getBudgetStatuses(db), null, 2) }] };
701
+ const budgets = getBudgetStatuses(db);
702
+ if (budgets.length === 0)
703
+ return { content: [{ type: "text", text: "No budgets set." }] };
704
+ const lines = ["scope period spent limit used% status"];
705
+ for (const b of budgets) {
706
+ const scope = String(b["project_path"] ?? "global").slice(0, 20);
707
+ const pct = Number(b["percent_used"]).toFixed(1);
708
+ const status = b["is_over_limit"] ? "OVER" : b["is_over_alert"] ? "ALERT" : "OK";
709
+ 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
+ }
711
+ return { content: [{ type: "text", text: lines.join(`
712
+ `) }] };
694
713
  }
695
714
  case "sync": {
696
715
  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) }] };
716
+ const parts = [];
717
+ if (sources === "all" || sources === "claude") {
718
+ const r = await ingestClaude(db);
719
+ parts.push(`claude: ${r["files"]} files, ${r["requests"]} requests, ${r["sessions"]} sessions`);
720
+ }
721
+ if (sources === "all" || sources === "codex") {
722
+ const r = await ingestCodex(db);
723
+ parts.push(`codex: ${r["sessions"]} sessions`);
724
+ }
725
+ return { content: [{ type: "text", text: parts.join(`
726
+ `) || "done" }] };
703
727
  }
704
728
  default:
705
729
  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);
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAoC1C,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA2I/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,GAAG,IAAI,CAuC7C"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AA0C1C,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA6I/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,GAAG,IAAI,CAuC7C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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": {