@hasna/economy 0.1.1 → 0.2.0
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 +452 -10
- package/dist/mcp/index.js +4 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -639,6 +639,115 @@ var init_codex = __esm(() => {
|
|
|
639
639
|
CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
640
640
|
});
|
|
641
641
|
|
|
642
|
+
// src/lib/config.ts
|
|
643
|
+
var exports_config = {};
|
|
644
|
+
__export(exports_config, {
|
|
645
|
+
setConfigValue: () => setConfigValue,
|
|
646
|
+
saveConfig: () => saveConfig,
|
|
647
|
+
loadConfig: () => loadConfig,
|
|
648
|
+
getConfigValue: () => getConfigValue
|
|
649
|
+
});
|
|
650
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
651
|
+
import { homedir as homedir4 } from "os";
|
|
652
|
+
import { join as join4 } from "path";
|
|
653
|
+
function loadConfig() {
|
|
654
|
+
try {
|
|
655
|
+
if (existsSync4(CONFIG_PATH)) {
|
|
656
|
+
const raw = readFileSync3(CONFIG_PATH, "utf-8");
|
|
657
|
+
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
658
|
+
}
|
|
659
|
+
} catch {}
|
|
660
|
+
return { ...DEFAULTS };
|
|
661
|
+
}
|
|
662
|
+
function saveConfig(config) {
|
|
663
|
+
const dir = CONFIG_PATH.substring(0, CONFIG_PATH.lastIndexOf("/"));
|
|
664
|
+
if (!existsSync4(dir))
|
|
665
|
+
mkdirSync2(dir, { recursive: true });
|
|
666
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + `
|
|
667
|
+
`);
|
|
668
|
+
}
|
|
669
|
+
function getConfigValue(key) {
|
|
670
|
+
const config = loadConfig();
|
|
671
|
+
return config[key] ?? null;
|
|
672
|
+
}
|
|
673
|
+
function setConfigValue(key, value) {
|
|
674
|
+
const config = loadConfig();
|
|
675
|
+
let parsed = value;
|
|
676
|
+
if (value === "true")
|
|
677
|
+
parsed = true;
|
|
678
|
+
else if (value === "false")
|
|
679
|
+
parsed = false;
|
|
680
|
+
else if (value === "null")
|
|
681
|
+
parsed = null;
|
|
682
|
+
else if (!isNaN(Number(value)))
|
|
683
|
+
parsed = Number(value);
|
|
684
|
+
else if (value.startsWith("[")) {
|
|
685
|
+
try {
|
|
686
|
+
parsed = JSON.parse(value);
|
|
687
|
+
} catch {}
|
|
688
|
+
}
|
|
689
|
+
config[key] = parsed;
|
|
690
|
+
saveConfig(config);
|
|
691
|
+
}
|
|
692
|
+
var CONFIG_PATH, DEFAULTS;
|
|
693
|
+
var init_config = __esm(() => {
|
|
694
|
+
CONFIG_PATH = join4(homedir4(), ".economy", "config.json");
|
|
695
|
+
DEFAULTS = {
|
|
696
|
+
port: 3456,
|
|
697
|
+
"default-period": "today",
|
|
698
|
+
"auto-sync": true,
|
|
699
|
+
"sync-interval": 30,
|
|
700
|
+
"alert-thresholds": [5, 10, 25, 50, 100],
|
|
701
|
+
"webhook-url": null
|
|
702
|
+
};
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// src/lib/webhooks.ts
|
|
706
|
+
var exports_webhooks = {};
|
|
707
|
+
__export(exports_webhooks, {
|
|
708
|
+
checkAndFireWebhooks: () => checkAndFireWebhooks
|
|
709
|
+
});
|
|
710
|
+
async function checkAndFireWebhooks(db) {
|
|
711
|
+
const config = loadConfig();
|
|
712
|
+
const url = config["webhook-url"];
|
|
713
|
+
if (!url)
|
|
714
|
+
return;
|
|
715
|
+
const statuses = getBudgetStatuses(db);
|
|
716
|
+
for (const b of statuses) {
|
|
717
|
+
if (!b.is_over_alert)
|
|
718
|
+
continue;
|
|
719
|
+
const key = `webhook-budget-${b.id}-${b.period}`;
|
|
720
|
+
const lastFired = getIngestState(db, "webhook", key);
|
|
721
|
+
const pctBucket = Math.floor(b.percent_used / 10) * 10;
|
|
722
|
+
if (lastFired === String(pctBucket))
|
|
723
|
+
continue;
|
|
724
|
+
await fireWebhook(url, {
|
|
725
|
+
event: "budget_alert",
|
|
726
|
+
budget_id: b.id,
|
|
727
|
+
project: b.project_path ?? "global",
|
|
728
|
+
period: b.period,
|
|
729
|
+
spend: b.current_spend_usd,
|
|
730
|
+
limit: b.limit_usd,
|
|
731
|
+
percent: Math.round(b.percent_used * 10) / 10
|
|
732
|
+
});
|
|
733
|
+
setIngestState(db, "webhook", key, String(pctBucket));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async function fireWebhook(url, payload) {
|
|
737
|
+
try {
|
|
738
|
+
await fetch(url, {
|
|
739
|
+
method: "POST",
|
|
740
|
+
headers: { "Content-Type": "application/json" },
|
|
741
|
+
body: JSON.stringify(payload),
|
|
742
|
+
signal: AbortSignal.timeout(5000)
|
|
743
|
+
});
|
|
744
|
+
} catch {}
|
|
745
|
+
}
|
|
746
|
+
var init_webhooks = __esm(() => {
|
|
747
|
+
init_config();
|
|
748
|
+
init_database();
|
|
749
|
+
});
|
|
750
|
+
|
|
642
751
|
// src/cli/commands/watch.ts
|
|
643
752
|
var exports_watch = {};
|
|
644
753
|
__export(exports_watch, {
|
|
@@ -867,15 +976,15 @@ function startServer(port = 3456) {
|
|
|
867
976
|
return apiHandler(req);
|
|
868
977
|
}
|
|
869
978
|
try {
|
|
870
|
-
const { existsSync:
|
|
871
|
-
if (
|
|
979
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
980
|
+
if (existsSync5(dashboardDir)) {
|
|
872
981
|
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
873
982
|
const fullPath = dashboardDir + filePath;
|
|
874
|
-
if (
|
|
983
|
+
if (existsSync5(fullPath)) {
|
|
875
984
|
return new Response(Bun.file(fullPath));
|
|
876
985
|
}
|
|
877
986
|
const indexPath = dashboardDir + "/index.html";
|
|
878
|
-
if (
|
|
987
|
+
if (existsSync5(indexPath)) {
|
|
879
988
|
return new Response(Bun.file(indexPath));
|
|
880
989
|
}
|
|
881
990
|
}
|
|
@@ -917,7 +1026,22 @@ import chalk2 from "chalk";
|
|
|
917
1026
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
918
1027
|
import { execSync } from "child_process";
|
|
919
1028
|
var program = new Command;
|
|
920
|
-
program.name("economy").description("AI coding cost tracker \u2014 Claude Code, Codex, and Gemini").version("0.1.
|
|
1029
|
+
program.name("economy").description("AI coding cost tracker \u2014 Claude Code, Codex, and Gemini").version("0.1.1");
|
|
1030
|
+
async function autoSync() {
|
|
1031
|
+
const db = openDatabase();
|
|
1032
|
+
ensurePricingSeeded(db);
|
|
1033
|
+
await ingestClaude(db);
|
|
1034
|
+
await ingestCodex(db);
|
|
1035
|
+
}
|
|
1036
|
+
function sparkline(values) {
|
|
1037
|
+
const chars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
1038
|
+
if (values.length === 0)
|
|
1039
|
+
return "";
|
|
1040
|
+
const max = Math.max(...values);
|
|
1041
|
+
if (max === 0)
|
|
1042
|
+
return chars[0].repeat(values.length);
|
|
1043
|
+
return values.map((v) => chars[Math.min(Math.round(v / max * 7), 7)]).join("");
|
|
1044
|
+
}
|
|
921
1045
|
function fmt2(usd) {
|
|
922
1046
|
let formatted;
|
|
923
1047
|
if (usd >= 0.01) {
|
|
@@ -970,9 +1094,47 @@ function printSummary(label, period) {
|
|
|
970
1094
|
]);
|
|
971
1095
|
console.log();
|
|
972
1096
|
}
|
|
973
|
-
program.
|
|
1097
|
+
program.action(async () => {
|
|
1098
|
+
await autoSync();
|
|
1099
|
+
const db = openDatabase();
|
|
1100
|
+
const t = querySummary(db, "today");
|
|
1101
|
+
const w = querySummary(db, "week");
|
|
1102
|
+
const m = querySummary(db, "month");
|
|
1103
|
+
const projects = queryProjectBreakdown(db).slice(0, 3);
|
|
1104
|
+
const daily = queryDailyBreakdown(db, 14).reduce((acc, d) => {
|
|
1105
|
+
acc[d.date] = (acc[d.date] ?? 0) + d.cost_usd;
|
|
1106
|
+
return acc;
|
|
1107
|
+
}, {});
|
|
1108
|
+
const dailyValues = Object.values(daily);
|
|
1109
|
+
console.log();
|
|
1110
|
+
console.log(chalk2.bold.cyan(" Economy"));
|
|
1111
|
+
console.log();
|
|
1112
|
+
printTable(["Period", "Cost", "Sessions", "Requests", "Tokens"], [
|
|
1113
|
+
["Today", fmt2(t.total_usd), fmtCount(t.sessions), fmtCount(t.requests), fmtTokens(t.tokens)],
|
|
1114
|
+
["This Week", fmt2(w.total_usd), fmtCount(w.sessions), fmtCount(w.requests), fmtTokens(w.tokens)],
|
|
1115
|
+
["This Month", fmt2(m.total_usd), fmtCount(m.sessions), fmtCount(m.requests), fmtTokens(m.tokens)]
|
|
1116
|
+
]);
|
|
1117
|
+
if (dailyValues.length > 0) {
|
|
1118
|
+
console.log(`
|
|
1119
|
+
${chalk2.dim("14-day trend:")} ${sparkline(dailyValues)}`);
|
|
1120
|
+
}
|
|
1121
|
+
if (projects.length > 0) {
|
|
1122
|
+
console.log(`
|
|
1123
|
+
${chalk2.dim("Top projects:")}`);
|
|
1124
|
+
for (const p of projects) {
|
|
1125
|
+
console.log(` ${chalk2.white(p.project_name.padEnd(25))} ${fmt2(p.cost_usd)}`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
console.log();
|
|
1129
|
+
});
|
|
1130
|
+
program.command("sync").description("Ingest cost data from Claude Code and Codex").option("--claude", "Only ingest Claude Code telemetry").option("--codex", "Only ingest Codex sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").action(async (opts) => {
|
|
974
1131
|
const db = openDatabase();
|
|
975
1132
|
ensurePricingSeeded(db);
|
|
1133
|
+
if (opts.force) {
|
|
1134
|
+
db.exec(`DELETE FROM ingest_state WHERE source = 'claude'`);
|
|
1135
|
+
if (opts.verbose)
|
|
1136
|
+
console.log(chalk2.dim("Cleared ingest cache"));
|
|
1137
|
+
}
|
|
976
1138
|
const doClaude = opts.claude || !opts.claude && !opts.codex;
|
|
977
1139
|
const doCodex = opts.codex || !opts.claude && !opts.codex;
|
|
978
1140
|
if (doClaude) {
|
|
@@ -985,13 +1147,27 @@ program.command("sync").description("Ingest cost data from Claude Code and Codex
|
|
|
985
1147
|
const r = await ingestCodex(db, opts.verbose);
|
|
986
1148
|
console.log(chalk2.green(`\u2713 ${r.sessions} sessions`));
|
|
987
1149
|
}
|
|
1150
|
+
try {
|
|
1151
|
+
const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
1152
|
+
await checkAndFireWebhooks2(db);
|
|
1153
|
+
} catch {}
|
|
988
1154
|
console.log(chalk2.bold.green(`
|
|
989
1155
|
\u2713 Sync complete`));
|
|
990
1156
|
});
|
|
991
|
-
program.command("today").description("Cost summary for today").action(() =>
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1157
|
+
program.command("today").description("Cost summary for today").action(async () => {
|
|
1158
|
+
await autoSync();
|
|
1159
|
+
printSummary("Today", "today");
|
|
1160
|
+
});
|
|
1161
|
+
program.command("week").description("Cost summary for this week").action(async () => {
|
|
1162
|
+
await autoSync();
|
|
1163
|
+
printSummary("This Week", "week");
|
|
1164
|
+
});
|
|
1165
|
+
program.command("month").description("Cost summary for this month").action(async () => {
|
|
1166
|
+
await autoSync();
|
|
1167
|
+
printSummary("This Month", "month");
|
|
1168
|
+
});
|
|
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) => {
|
|
1170
|
+
await autoSync();
|
|
995
1171
|
const db = openDatabase();
|
|
996
1172
|
const sessions = querySessions(db, {
|
|
997
1173
|
agent: opts.agent,
|
|
@@ -1154,6 +1330,79 @@ projectCmd.command("rename <path> <name>").description("Rename a project").actio
|
|
|
1154
1330
|
upsertProject(db, { ...existing, name });
|
|
1155
1331
|
console.log(chalk2.green(`\u2713 Renamed to: ${name}`));
|
|
1156
1332
|
});
|
|
1333
|
+
projectCmd.command("show <nameOrPath>").description("Detailed project breakdown with sparkline").action(async (nameOrPath) => {
|
|
1334
|
+
await autoSync();
|
|
1335
|
+
const db = openDatabase();
|
|
1336
|
+
const sessions = db.prepare(`SELECT * FROM sessions WHERE project_name LIKE ? OR project_path LIKE ? ORDER BY started_at DESC`).all(`%${nameOrPath}%`, `%${nameOrPath}%`);
|
|
1337
|
+
if (sessions.length === 0) {
|
|
1338
|
+
console.log(chalk2.yellow(`No sessions found for: ${nameOrPath}`));
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
const projectName = sessions[0]["project_name"] || nameOrPath;
|
|
1342
|
+
const projectPath = sessions[0]["project_path"] || "";
|
|
1343
|
+
const totalCost = sessions.reduce((s, r) => s + r["total_cost_usd"], 0);
|
|
1344
|
+
const totalTokens = sessions.reduce((s, r) => s + r["total_tokens"], 0);
|
|
1345
|
+
const daily = db.prepare(`
|
|
1346
|
+
SELECT DATE(r.timestamp) as d, SUM(r.cost_usd) as cost
|
|
1347
|
+
FROM requests r JOIN sessions s ON r.session_id = s.id
|
|
1348
|
+
WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
|
|
1349
|
+
AND r.timestamp >= DATE('now', '-14 days')
|
|
1350
|
+
GROUP BY d ORDER BY d ASC
|
|
1351
|
+
`).all(`%${nameOrPath}%`, `%${nameOrPath}%`);
|
|
1352
|
+
const dailyValues = daily.map((d) => d.cost);
|
|
1353
|
+
const models = db.prepare(`
|
|
1354
|
+
SELECT r.model, COUNT(*) as reqs, SUM(r.cost_usd) as cost
|
|
1355
|
+
FROM requests r JOIN sessions s ON r.session_id = s.id
|
|
1356
|
+
WHERE s.project_name LIKE ? OR s.project_path LIKE ?
|
|
1357
|
+
GROUP BY r.model ORDER BY cost DESC LIMIT 5
|
|
1358
|
+
`).all(`%${nameOrPath}%`, `%${nameOrPath}%`);
|
|
1359
|
+
console.log();
|
|
1360
|
+
console.log(chalk2.bold.cyan(` ${projectName}`));
|
|
1361
|
+
console.log(chalk2.dim(` ${projectPath}`));
|
|
1362
|
+
console.log();
|
|
1363
|
+
printTable(["Metric", "Value"], [
|
|
1364
|
+
["Total cost", fmt2(totalCost)],
|
|
1365
|
+
["Sessions", fmtCount(sessions.length)],
|
|
1366
|
+
["Total tokens", fmtTokens(totalTokens)]
|
|
1367
|
+
]);
|
|
1368
|
+
if (dailyValues.length > 0) {
|
|
1369
|
+
console.log(`
|
|
1370
|
+
${chalk2.dim("14-day trend:")} ${sparkline(dailyValues)}`);
|
|
1371
|
+
}
|
|
1372
|
+
if (models.length > 0) {
|
|
1373
|
+
console.log(`
|
|
1374
|
+
${chalk2.dim("Model breakdown:")}`);
|
|
1375
|
+
for (const m of models) {
|
|
1376
|
+
console.log(` ${chalk2.white(m.model.padEnd(30))} ${fmt2(m.cost)} (${fmtCount(m.reqs)} reqs)`);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
const topSessions = sessions.sort((a, b) => b["total_cost_usd"] - a["total_cost_usd"]).slice(0, 5);
|
|
1380
|
+
if (topSessions.length > 0) {
|
|
1381
|
+
console.log(`
|
|
1382
|
+
${chalk2.dim("Top sessions:")}`);
|
|
1383
|
+
for (const s of topSessions) {
|
|
1384
|
+
console.log(` ${chalk2.dim(s["id"].substring(0, 12))} ${fmt2(s["total_cost_usd"])} ${chalk2.dim(String(s["started_at"]).substring(0, 16))}`);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
console.log();
|
|
1388
|
+
});
|
|
1389
|
+
var configCmd = program.command("config").description("Manage economy configuration");
|
|
1390
|
+
configCmd.command("set <key> <value>").description("Set a config value").action(async (_key, _value) => {
|
|
1391
|
+
const { setConfigValue: setConfigValue2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
1392
|
+
setConfigValue2(_key, _value);
|
|
1393
|
+
console.log(chalk2.green(`\u2713 ${_key} = ${_value}`));
|
|
1394
|
+
});
|
|
1395
|
+
configCmd.command("get <key>").description("Get a config value").action(async (key) => {
|
|
1396
|
+
const { getConfigValue: getConfigValue2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
1397
|
+
console.log(getConfigValue2(key) ?? chalk2.dim("(not set)"));
|
|
1398
|
+
});
|
|
1399
|
+
configCmd.action(async () => {
|
|
1400
|
+
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
1401
|
+
const config = loadConfig2();
|
|
1402
|
+
console.log();
|
|
1403
|
+
printTable(["Key", "Value"], Object.entries(config).map(([k, v]) => [k, String(v)]));
|
|
1404
|
+
console.log();
|
|
1405
|
+
});
|
|
1157
1406
|
var pricingCmd = program.command("pricing").description("Manage model pricing rates");
|
|
1158
1407
|
pricingCmd.command("list").description("List all model prices").action(() => {
|
|
1159
1408
|
const db = openDatabase();
|
|
@@ -1256,4 +1505,197 @@ Codex (~/.codex/config.toml):`));
|
|
|
1256
1505
|
}
|
|
1257
1506
|
console.log();
|
|
1258
1507
|
});
|
|
1508
|
+
program.command("session <id>").description("Show detailed breakdown of a single session").action(async (id) => {
|
|
1509
|
+
await autoSync();
|
|
1510
|
+
const db = openDatabase();
|
|
1511
|
+
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `%${id}%`);
|
|
1512
|
+
if (!session) {
|
|
1513
|
+
console.log(chalk2.red(`Session not found: ${id}`));
|
|
1514
|
+
process.exit(1);
|
|
1515
|
+
}
|
|
1516
|
+
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC`).all(session["id"]);
|
|
1517
|
+
console.log();
|
|
1518
|
+
console.log(chalk2.bold.cyan(` Session: ${session["id"].substring(0, 16)}...`));
|
|
1519
|
+
console.log();
|
|
1520
|
+
printTable(["Field", "Value"], [
|
|
1521
|
+
["Agent", String(session["agent"])],
|
|
1522
|
+
["Project", String(session["project_name"] || session["project_path"] || "\u2014")],
|
|
1523
|
+
["Started", String(session["started_at"]).substring(0, 19)],
|
|
1524
|
+
["Ended", session["ended_at"] ? String(session["ended_at"]).substring(0, 19) : "\u2014"],
|
|
1525
|
+
["Total cost", fmt2(session["total_cost_usd"])],
|
|
1526
|
+
["Total tokens", fmtTokens(session["total_tokens"])],
|
|
1527
|
+
["Requests", fmtCount(session["request_count"])]
|
|
1528
|
+
]);
|
|
1529
|
+
if (requests.length > 0) {
|
|
1530
|
+
console.log(chalk2.dim(`
|
|
1531
|
+
Requests (${requests.length}):
|
|
1532
|
+
`));
|
|
1533
|
+
printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], requests.slice(0, 50).map((r) => [
|
|
1534
|
+
chalk2.dim(String(r["timestamp"]).substring(11, 19)),
|
|
1535
|
+
chalk2.white(String(r["model"]).substring(0, 22)),
|
|
1536
|
+
fmtTokens(r["input_tokens"]),
|
|
1537
|
+
fmtTokens(r["output_tokens"]),
|
|
1538
|
+
fmtTokens(r["cache_read_tokens"]),
|
|
1539
|
+
fmtTokens(r["cache_create_tokens"]),
|
|
1540
|
+
fmt2(r["cost_usd"])
|
|
1541
|
+
]));
|
|
1542
|
+
if (requests.length > 50)
|
|
1543
|
+
console.log(chalk2.dim(` ... and ${requests.length - 50} more requests`));
|
|
1544
|
+
}
|
|
1545
|
+
console.log();
|
|
1546
|
+
});
|
|
1547
|
+
program.command("export").description("Export data as CSV").option("--type <type>", "Data type: sessions or requests", "sessions").option("--period <period>", "Period: today|week|month|all", "month").option("--output <file>", "Output file path (default: stdout)").action(async (opts) => {
|
|
1548
|
+
await autoSync();
|
|
1549
|
+
const db = openDatabase();
|
|
1550
|
+
let csv;
|
|
1551
|
+
if (opts.type === "requests") {
|
|
1552
|
+
const where = opts.period === "today" ? `DATE(timestamp) = DATE('now')` : opts.period === "week" ? `timestamp >= DATE('now', '-7 days')` : opts.period === "all" ? "1=1" : `timestamp >= DATE('now', '-30 days')`;
|
|
1553
|
+
const rows = db.prepare(`SELECT * FROM requests WHERE ${where} ORDER BY timestamp ASC`).all();
|
|
1554
|
+
csv = `id,agent,session_id,model,input_tokens,output_tokens,cache_read_tokens,cache_create_tokens,cost_usd,duration_ms,timestamp
|
|
1555
|
+
`;
|
|
1556
|
+
for (const r of rows) {
|
|
1557
|
+
csv += `${r["id"]},${r["agent"]},${r["session_id"]},${r["model"]},${r["input_tokens"]},${r["output_tokens"]},${r["cache_read_tokens"]},${r["cache_create_tokens"]},${r["cost_usd"]},${r["duration_ms"]},${r["timestamp"]}
|
|
1558
|
+
`;
|
|
1559
|
+
}
|
|
1560
|
+
} else {
|
|
1561
|
+
const where = opts.period === "today" ? `DATE(started_at) = DATE('now')` : opts.period === "week" ? `started_at >= DATE('now', '-7 days')` : opts.period === "all" ? "1=1" : `started_at >= DATE('now', '-30 days')`;
|
|
1562
|
+
const rows = db.prepare(`SELECT * FROM sessions WHERE ${where} ORDER BY started_at DESC`).all();
|
|
1563
|
+
csv = `id,agent,project_path,project_name,started_at,ended_at,total_cost_usd,total_tokens,request_count
|
|
1564
|
+
`;
|
|
1565
|
+
for (const r of rows) {
|
|
1566
|
+
csv += `${r["id"]},${r["agent"]},"${r["project_path"]}","${r["project_name"]}",${r["started_at"]},${r["ended_at"] ?? ""},${r["total_cost_usd"]},${r["total_tokens"]},${r["request_count"]}
|
|
1567
|
+
`;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (opts.output) {
|
|
1571
|
+
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
1572
|
+
writeFileSync2(opts.output, csv);
|
|
1573
|
+
console.log(chalk2.green(`\u2713 Exported to ${opts.output}`));
|
|
1574
|
+
} else {
|
|
1575
|
+
process.stdout.write(csv);
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
program.command("compare <period1> <period2>").description("Compare two periods (today/yesterday/week/lastweek/month/lastmonth)").action(async (p1, p2) => {
|
|
1579
|
+
await autoSync();
|
|
1580
|
+
const db = openDatabase();
|
|
1581
|
+
function dateRange(period) {
|
|
1582
|
+
const now = new Date;
|
|
1583
|
+
const today = now.toISOString().substring(0, 10);
|
|
1584
|
+
switch (period) {
|
|
1585
|
+
case "today":
|
|
1586
|
+
return [today, today];
|
|
1587
|
+
case "yesterday": {
|
|
1588
|
+
const d = new Date(now);
|
|
1589
|
+
d.setDate(d.getDate() - 1);
|
|
1590
|
+
const s = d.toISOString().substring(0, 10);
|
|
1591
|
+
return [s, s];
|
|
1592
|
+
}
|
|
1593
|
+
case "week": {
|
|
1594
|
+
const d = new Date(now);
|
|
1595
|
+
d.setDate(d.getDate() - 7);
|
|
1596
|
+
return [d.toISOString().substring(0, 10), today];
|
|
1597
|
+
}
|
|
1598
|
+
case "lastweek": {
|
|
1599
|
+
const d1 = new Date(now);
|
|
1600
|
+
d1.setDate(d1.getDate() - 14);
|
|
1601
|
+
const d2 = new Date(now);
|
|
1602
|
+
d2.setDate(d2.getDate() - 7);
|
|
1603
|
+
return [d1.toISOString().substring(0, 10), d2.toISOString().substring(0, 10)];
|
|
1604
|
+
}
|
|
1605
|
+
case "month": {
|
|
1606
|
+
const d = new Date(now);
|
|
1607
|
+
d.setDate(d.getDate() - 30);
|
|
1608
|
+
return [d.toISOString().substring(0, 10), today];
|
|
1609
|
+
}
|
|
1610
|
+
case "lastmonth": {
|
|
1611
|
+
const d1 = new Date(now);
|
|
1612
|
+
d1.setDate(d1.getDate() - 60);
|
|
1613
|
+
const d2 = new Date(now);
|
|
1614
|
+
d2.setDate(d2.getDate() - 30);
|
|
1615
|
+
return [d1.toISOString().substring(0, 10), d2.toISOString().substring(0, 10)];
|
|
1616
|
+
}
|
|
1617
|
+
default:
|
|
1618
|
+
return [today, today];
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
function queryRange(from, to) {
|
|
1622
|
+
const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests, COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens FROM requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
|
|
1623
|
+
const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
|
|
1624
|
+
return { ...r, sessions: s.sessions };
|
|
1625
|
+
}
|
|
1626
|
+
const [f1, t1] = dateRange(p1);
|
|
1627
|
+
const [f2, t2] = dateRange(p2);
|
|
1628
|
+
const a = queryRange(f1, t1);
|
|
1629
|
+
const b = queryRange(f2, t2);
|
|
1630
|
+
function delta(v1, v2) {
|
|
1631
|
+
const d = v1 - v2;
|
|
1632
|
+
const pct = v2 > 0 ? (d / v2 * 100).toFixed(1) : "\u2014";
|
|
1633
|
+
const sign = d >= 0 ? "+" : "";
|
|
1634
|
+
const color = d > 0 ? chalk2.red : d < 0 ? chalk2.green : chalk2.dim;
|
|
1635
|
+
return color(`${sign}${pct}%`);
|
|
1636
|
+
}
|
|
1637
|
+
console.log();
|
|
1638
|
+
console.log(chalk2.bold.cyan(` ${p1} vs ${p2}`));
|
|
1639
|
+
console.log();
|
|
1640
|
+
printTable(["Metric", p1, p2, "Change"], [
|
|
1641
|
+
["Cost", fmt2(a.cost), fmt2(b.cost), delta(a.cost, b.cost)],
|
|
1642
|
+
["Sessions", fmtCount(a.sessions), fmtCount(b.sessions), delta(a.sessions, b.sessions)],
|
|
1643
|
+
["Requests", fmtCount(a.requests), fmtCount(b.requests), delta(a.requests, b.requests)],
|
|
1644
|
+
["Tokens", fmtTokens(a.tokens), fmtTokens(b.tokens), delta(a.tokens, b.tokens)]
|
|
1645
|
+
]);
|
|
1646
|
+
console.log();
|
|
1647
|
+
});
|
|
1648
|
+
program.command("forecast").description("Project end-of-month cost based on current burn rate").action(async () => {
|
|
1649
|
+
await autoSync();
|
|
1650
|
+
const db = openDatabase();
|
|
1651
|
+
const now = new Date;
|
|
1652
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1653
|
+
const dayOfMonth = now.getDate();
|
|
1654
|
+
const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
|
|
1655
|
+
const today = now.toISOString().substring(0, 10);
|
|
1656
|
+
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(monthStart);
|
|
1657
|
+
const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
|
|
1658
|
+
const projected = dailyAvg * daysInMonth;
|
|
1659
|
+
const sevenDaysAgo = new Date(now);
|
|
1660
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
1661
|
+
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
|
|
1662
|
+
const last7DailyAvg = last7.cost / 7;
|
|
1663
|
+
const last7Projected = last7DailyAvg * daysInMonth;
|
|
1664
|
+
const dailyCosts = db.prepare(`SELECT DATE(timestamp) as d, SUM(cost_usd) as cost FROM requests WHERE DATE(timestamp) >= ? GROUP BY d ORDER BY cost ASC`).all(monthStart);
|
|
1665
|
+
const cheapest = dailyCosts[0];
|
|
1666
|
+
const mostExpensive = dailyCosts[dailyCosts.length - 1];
|
|
1667
|
+
console.log();
|
|
1668
|
+
console.log(chalk2.bold.cyan(` Forecast (${dayOfMonth} of ${daysInMonth} days)`));
|
|
1669
|
+
console.log();
|
|
1670
|
+
printTable(["Metric", "Value"], [
|
|
1671
|
+
["Spent so far", fmt2(monthSoFar.cost)],
|
|
1672
|
+
["Daily average", fmt2(dailyAvg)],
|
|
1673
|
+
[chalk2.bold("Projected total"), chalk2.bold(fmt2(projected).replace(chalk2.green(""), ""))],
|
|
1674
|
+
["Last 7-day rate", `${fmt2(last7DailyAvg)}/day \u2192 ${fmt2(last7Projected)}`],
|
|
1675
|
+
["Cheapest day", cheapest ? `${fmt2(cheapest.cost)} (${cheapest.d})` : "\u2014"],
|
|
1676
|
+
["Most expensive", mostExpensive ? `${fmt2(mostExpensive.cost)} (${mostExpensive.d})` : "\u2014"]
|
|
1677
|
+
]);
|
|
1678
|
+
console.log();
|
|
1679
|
+
});
|
|
1680
|
+
program.command("efficiency").description("Show output/input token ratio per model").action(async () => {
|
|
1681
|
+
await autoSync();
|
|
1682
|
+
const db = openDatabase();
|
|
1683
|
+
const models = db.prepare(`
|
|
1684
|
+
SELECT model, SUM(input_tokens) as input, SUM(output_tokens) as output,
|
|
1685
|
+
SUM(cache_read_tokens) as cache_read, SUM(cache_create_tokens) as cache_write,
|
|
1686
|
+
COUNT(*) as requests, SUM(cost_usd) as cost
|
|
1687
|
+
FROM requests GROUP BY model ORDER BY cost DESC
|
|
1688
|
+
`).all();
|
|
1689
|
+
console.log();
|
|
1690
|
+
console.log(chalk2.bold.cyan(" Token Efficiency"));
|
|
1691
|
+
console.log();
|
|
1692
|
+
printTable(["Model", "Output/Input", "Cache Hit%", "Cost/1k Output", "Requests"], models.map((m) => {
|
|
1693
|
+
const ratio = m.input > 0 ? (m.output / m.input).toFixed(2) : "\u2014";
|
|
1694
|
+
const totalInput = m.input + m.cache_read + m.cache_write;
|
|
1695
|
+
const cacheHit = totalInput > 0 ? (m.cache_read / totalInput * 100).toFixed(1) + "%" : "\u2014";
|
|
1696
|
+
const costPer1kOutput = m.output > 0 ? fmt2(m.cost / m.output * 1000) : "\u2014";
|
|
1697
|
+
return [chalk2.white(m.model), ratio, cacheHit, costPer1kOutput, fmtCount(m.requests)];
|
|
1698
|
+
}));
|
|
1699
|
+
console.log();
|
|
1700
|
+
});
|
|
1259
1701
|
program.parse();
|
package/dist/mcp/index.js
CHANGED
|
@@ -666,7 +666,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
666
666
|
case "get_cost_summary": {
|
|
667
667
|
const period = a["period"] ?? "today";
|
|
668
668
|
const summary = querySummary(db, period);
|
|
669
|
-
|
|
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) }] };
|
|
670
673
|
}
|
|
671
674
|
case "get_sessions": {
|
|
672
675
|
const sessions = querySessions(db, {
|
package/package.json
CHANGED