@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.
- package/README.md +37 -45
- package/dist/cli/brains.d.ts +3 -0
- package/dist/cli/brains.d.ts.map +1 -0
- package/dist/cli/commands/menubar.d.ts +7 -0
- package/dist/cli/commands/menubar.d.ts.map +1 -0
- package/dist/cli/commands/watch.d.ts +9 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +127 -82
- package/dist/db/database.d.ts +68 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/pg-migrations.d.ts +7 -0
- package/dist/db/pg-migrations.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/ingest/claude.d.ts +7 -0
- package/dist/ingest/claude.d.ts.map +1 -0
- package/dist/ingest/codex.d.ts +7 -0
- package/dist/ingest/codex.d.ts.map +1 -0
- package/dist/ingest/gemini.d.ts +5 -0
- package/dist/ingest/gemini.d.ts.map +1 -0
- package/dist/lib/config.d.ts +13 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/gatherer.d.ts +21 -0
- package/dist/lib/gatherer.d.ts.map +1 -0
- package/dist/lib/model-config.d.ts +8 -0
- package/dist/lib/model-config.d.ts.map +1 -0
- package/dist/lib/package-metadata.d.ts +8 -0
- package/dist/lib/package-metadata.d.ts.map +1 -0
- package/dist/lib/pricing.d.ts +10 -0
- package/dist/lib/pricing.d.ts.map +1 -0
- package/dist/lib/webhooks.d.ts +3 -0
- package/dist/lib/webhooks.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +305 -326
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +168 -10
- package/dist/server/serve.d.ts +4 -0
- package/dist/server/serve.d.ts.map +1 -0
- package/dist/types/index.d.ts +101 -0
- package/dist/types/index.d.ts.map +1 -0
- 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 {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
{
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
`
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
`
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
`
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
`)
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
`)
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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);
|