@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 +27 -3
- package/dist/mcp/index.js +98 -76
- package/dist/server/index.js +8 -1
- package/package.json +2 -2
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.
|
|
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
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
698
|
-
if (sources === "all" || sources === "claude")
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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 };
|
package/dist/server/index.js
CHANGED
|
@@ -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.
|
|
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": "
|
|
51
|
+
"access": "public",
|
|
52
52
|
"registry": "https://registry.npmjs.org/"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|