kerf-cli 0.1.1 → 0.1.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/CHANGELOG.md +11 -6
- package/README.md +37 -32
- package/USAGE.md +644 -0
- package/dist/index.js +87 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -314,8 +314,11 @@ function getPeriodKey(timestamp, period) {
|
|
|
314
314
|
}
|
|
315
315
|
function formatPeriodLabel(key, period) {
|
|
316
316
|
switch (period) {
|
|
317
|
-
case "hour":
|
|
318
|
-
|
|
317
|
+
case "hour": {
|
|
318
|
+
const parts = key.split("-");
|
|
319
|
+
const dateStr = `${parts[0]}-${parts[1]}-${parts[2]}T${parts[3]}:00:00`;
|
|
320
|
+
return dayjs2(dateStr).format("MMM D, h A");
|
|
321
|
+
}
|
|
319
322
|
case "day":
|
|
320
323
|
return dayjs2(key).format("ddd, MMM D");
|
|
321
324
|
case "week":
|
|
@@ -419,8 +422,25 @@ function ContextBar({ used, total, overhead }) {
|
|
|
419
422
|
|
|
420
423
|
// src/core/tokenCounter.ts
|
|
421
424
|
import { readFileSync as readFileSync2, existsSync } from "node:fs";
|
|
425
|
+
import { execSync } from "node:child_process";
|
|
422
426
|
import { join as join3 } from "node:path";
|
|
423
427
|
import { homedir as homedir2 } from "node:os";
|
|
428
|
+
function findGitRootClaudeMd() {
|
|
429
|
+
try {
|
|
430
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
431
|
+
encoding: "utf-8",
|
|
432
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
433
|
+
}).trim();
|
|
434
|
+
if (gitRoot && gitRoot !== process.cwd()) {
|
|
435
|
+
return [
|
|
436
|
+
join3(gitRoot, "CLAUDE.md"),
|
|
437
|
+
join3(gitRoot, ".claude", "CLAUDE.md")
|
|
438
|
+
];
|
|
439
|
+
}
|
|
440
|
+
} catch {
|
|
441
|
+
}
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
424
444
|
function estimateTokens(text) {
|
|
425
445
|
return Math.ceil(text.length / 3.5);
|
|
426
446
|
}
|
|
@@ -473,7 +493,9 @@ function parseClaudeMdSections(content) {
|
|
|
473
493
|
function analyzeClaudeMd(filePath) {
|
|
474
494
|
const paths = filePath ? [filePath] : [
|
|
475
495
|
join3(process.cwd(), "CLAUDE.md"),
|
|
476
|
-
join3(process.cwd(), ".claude", "CLAUDE.md")
|
|
496
|
+
join3(process.cwd(), ".claude", "CLAUDE.md"),
|
|
497
|
+
...findGitRootClaudeMd(),
|
|
498
|
+
join3(homedir2(), ".claude", "CLAUDE.md")
|
|
477
499
|
];
|
|
478
500
|
for (const p of paths) {
|
|
479
501
|
if (existsSync(p)) {
|
|
@@ -573,7 +595,7 @@ function Dashboard({ sessionFilePath, interval }) {
|
|
|
573
595
|
const recentMessages = session.messages.slice(-8);
|
|
574
596
|
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
575
597
|
/* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
576
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "kerf watch" }),
|
|
598
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "kerf-cli watch" }),
|
|
577
599
|
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
578
600
|
" | session: ",
|
|
579
601
|
session.sessionId.slice(0, 8),
|
|
@@ -633,7 +655,7 @@ function registerWatchCommand(program2) {
|
|
|
633
655
|
}
|
|
634
656
|
if (!sessionFilePath) {
|
|
635
657
|
console.log(
|
|
636
|
-
"No active Claude Code session found. Start Claude Code and run 'kerf watch' again."
|
|
658
|
+
"No active Claude Code session found. Start Claude Code and run 'kerf-cli watch' again."
|
|
637
659
|
);
|
|
638
660
|
process.exit(0);
|
|
639
661
|
}
|
|
@@ -674,8 +696,8 @@ async function estimateTaskCost(taskDescription, options = {}) {
|
|
|
674
696
|
let fileList = options.files ?? [];
|
|
675
697
|
if (fileList.length === 0) {
|
|
676
698
|
try {
|
|
677
|
-
const { execSync } = await import("node:child_process");
|
|
678
|
-
const output =
|
|
699
|
+
const { execSync: execSync2 } = await import("node:child_process");
|
|
700
|
+
const output = execSync2("git diff --name-only HEAD 2>/dev/null || git ls-files -m 2>/dev/null", {
|
|
679
701
|
cwd,
|
|
680
702
|
encoding: "utf-8"
|
|
681
703
|
});
|
|
@@ -736,7 +758,7 @@ async function estimateTaskCost(taskDescription, options = {}) {
|
|
|
736
758
|
recommendations.push(`Using Opus would cost ~${formatCost(expectedCost * ratio)} (${ratio.toFixed(0)}x more)`);
|
|
737
759
|
}
|
|
738
760
|
if (overhead.percentUsable < 60) {
|
|
739
|
-
recommendations.push(`High ghost token overhead (${(100 - overhead.percentUsable).toFixed(0)}%). Run 'kerf audit' to optimize.`);
|
|
761
|
+
recommendations.push(`High ghost token overhead (${(100 - overhead.percentUsable).toFixed(0)}%). Run 'kerf-cli audit' to optimize.`);
|
|
740
762
|
}
|
|
741
763
|
if (fileTokens > 5e4) {
|
|
742
764
|
recommendations.push(`Large file context (${(fileTokens / 1e3).toFixed(0)}K tokens). Consider narrowing scope.`);
|
|
@@ -768,7 +790,7 @@ function EstimateCard({ task, estimate }) {
|
|
|
768
790
|
const formatK = (n) => `${(n / 1e3).toFixed(1)}K`;
|
|
769
791
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [
|
|
770
792
|
/* @__PURE__ */ jsxs4(Text4, { bold: true, color: "cyan", children: [
|
|
771
|
-
"kerf estimate: '",
|
|
793
|
+
"kerf-cli estimate: '",
|
|
772
794
|
task,
|
|
773
795
|
"'"
|
|
774
796
|
] }),
|
|
@@ -1087,7 +1109,7 @@ function registerBudgetCommand(program2) {
|
|
|
1087
1109
|
const projectPath = opts.project || process.cwd();
|
|
1088
1110
|
const status = manager.checkBudget(projectPath);
|
|
1089
1111
|
if (!status) {
|
|
1090
|
-
console.log("No budget set for this project. Use 'kerf budget set <amount>' to set one.");
|
|
1112
|
+
console.log("No budget set for this project. Use 'kerf-cli budget set <amount>' to set one.");
|
|
1091
1113
|
manager.close();
|
|
1092
1114
|
return;
|
|
1093
1115
|
}
|
|
@@ -1102,7 +1124,7 @@ function registerBudgetCommand(program2) {
|
|
|
1102
1124
|
const empty = barWidth - filled;
|
|
1103
1125
|
const color = pct < 50 ? "green" : pct < 80 ? "yellow" : "red";
|
|
1104
1126
|
const barColor = color === "green" ? chalk.green : color === "yellow" ? chalk.yellow : chalk.red;
|
|
1105
|
-
console.log(chalk.bold.cyan("\n kerf budget\n"));
|
|
1127
|
+
console.log(chalk.bold.cyan("\n kerf-cli budget\n"));
|
|
1106
1128
|
console.log(` Period: ${status.period} (${status.periodStart.slice(0, 10)} to ${status.periodEnd.slice(0, 10)})`);
|
|
1107
1129
|
console.log(` Budget: ${formatCost(status.budget)}`);
|
|
1108
1130
|
console.log(` Spent: ${barColor(formatCost(status.spent))}`);
|
|
@@ -1118,11 +1140,11 @@ function registerBudgetCommand(program2) {
|
|
|
1118
1140
|
const manager = new BudgetManager();
|
|
1119
1141
|
const projects = manager.listProjects();
|
|
1120
1142
|
if (projects.length === 0) {
|
|
1121
|
-
console.log("No projects with budgets. Use 'kerf budget set <amount>' to set one.");
|
|
1143
|
+
console.log("No projects with budgets. Use 'kerf-cli budget set <amount>' to set one.");
|
|
1122
1144
|
manager.close();
|
|
1123
1145
|
return;
|
|
1124
1146
|
}
|
|
1125
|
-
console.log(chalk.bold.cyan("\n kerf budget list\n"));
|
|
1147
|
+
console.log(chalk.bold.cyan("\n kerf-cli budget list\n"));
|
|
1126
1148
|
for (const p of projects) {
|
|
1127
1149
|
const budgetStr = p.budget ? `${formatCost(p.budget)}/${p.period}` : "no budget";
|
|
1128
1150
|
const spentStr = p.spent > 0 ? ` (spent: ${formatCost(p.spent)})` : "";
|
|
@@ -1410,12 +1432,12 @@ function registerAuditCommand(program2) {
|
|
|
1410
1432
|
return;
|
|
1411
1433
|
}
|
|
1412
1434
|
const gradeColor = result.grade === "A" ? chalk2.green : result.grade === "B" ? chalk2.yellow : chalk2.red;
|
|
1413
|
-
console.log(chalk2.bold.cyan("\n kerf audit report\n"));
|
|
1435
|
+
console.log(chalk2.bold.cyan("\n kerf-cli audit report\n"));
|
|
1414
1436
|
console.log(
|
|
1415
1437
|
` Context Window Health: ${gradeColor.bold(result.grade)} (${result.contextOverhead.percentUsable.toFixed(0)}% usable)
|
|
1416
1438
|
`
|
|
1417
1439
|
);
|
|
1418
|
-
if (!opts.
|
|
1440
|
+
if (!opts.claudeMdOnly) {
|
|
1419
1441
|
console.log(chalk2.bold(" Ghost Token Breakdown:"));
|
|
1420
1442
|
const oh = result.contextOverhead;
|
|
1421
1443
|
const fmt = (label, tokens) => {
|
|
@@ -1444,17 +1466,52 @@ function registerAuditCommand(program2) {
|
|
|
1444
1466
|
console.log(
|
|
1445
1467
|
` Critical rules in dead zone: ${cma.criticalRulesInDeadZone > 0 ? chalk2.red(String(cma.criticalRulesInDeadZone)) : "0"}`
|
|
1446
1468
|
);
|
|
1469
|
+
if (opts.claudeMdOnly) {
|
|
1470
|
+
console.log();
|
|
1471
|
+
console.log(chalk2.bold(" Sections:"));
|
|
1472
|
+
for (const section of cma.sections) {
|
|
1473
|
+
const zone = section.attentionZone === "low-middle" ? chalk2.red(" [dead zone]") : chalk2.green(" [high attention]");
|
|
1474
|
+
const critical = section.hasCriticalRules ? chalk2.yellow(" *critical rules*") : "";
|
|
1475
|
+
console.log(
|
|
1476
|
+
` ${section.title.padEnd(30)} ${String(section.tokens).padStart(5)} tokens L${section.lineStart}-${section.lineEnd}${zone}${critical}`
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
if (cma.suggestedReorder.length > 0) {
|
|
1480
|
+
console.log();
|
|
1481
|
+
console.log(chalk2.bold(" Suggested section order:"));
|
|
1482
|
+
cma.suggestedReorder.forEach((title, i) => {
|
|
1483
|
+
console.log(` ${i + 1}. ${title}`);
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1447
1487
|
console.log();
|
|
1488
|
+
} else if (!opts.mcpOnly && !result.claudeMdAnalysis) {
|
|
1489
|
+
console.log(chalk2.yellow(" No CLAUDE.md found in current directory or git root.\n"));
|
|
1448
1490
|
}
|
|
1449
|
-
if (result.
|
|
1450
|
-
console.log(chalk2.bold("
|
|
1451
|
-
result.
|
|
1452
|
-
const
|
|
1491
|
+
if (opts.mcpOnly && result.mcpServers.length > 0) {
|
|
1492
|
+
console.log(chalk2.bold(" MCP Servers:"));
|
|
1493
|
+
for (const server of result.mcpServers) {
|
|
1494
|
+
const heavy = server.isHeavy ? chalk2.red(" [heavy]") : "";
|
|
1453
1495
|
console.log(
|
|
1454
|
-
` ${
|
|
1496
|
+
` ${server.name.padEnd(20)} ${String(server.toolCount).padStart(3)} tools ${server.estimatedTokens.toLocaleString().padStart(6)} tokens${heavy}`
|
|
1455
1497
|
);
|
|
1456
|
-
|
|
1457
|
-
|
|
1498
|
+
}
|
|
1499
|
+
console.log();
|
|
1500
|
+
} else if (opts.mcpOnly && result.mcpServers.length === 0) {
|
|
1501
|
+
console.log(chalk2.dim(" No MCP servers configured.\n"));
|
|
1502
|
+
}
|
|
1503
|
+
if (result.recommendations.length > 0) {
|
|
1504
|
+
const filteredRecs = opts.claudeMdOnly ? result.recommendations.filter((r) => r.category === "claude-md") : opts.mcpOnly ? result.recommendations.filter((r) => r.category === "mcp") : result.recommendations;
|
|
1505
|
+
if (filteredRecs.length > 0) {
|
|
1506
|
+
console.log(chalk2.bold(" Recommendations:"));
|
|
1507
|
+
filteredRecs.forEach((rec, i) => {
|
|
1508
|
+
const priorityColor = rec.priority === "high" ? chalk2.red : rec.priority === "medium" ? chalk2.yellow : chalk2.dim;
|
|
1509
|
+
console.log(
|
|
1510
|
+
` ${i + 1}. ${priorityColor(`[${rec.priority.toUpperCase()}]`)} ${rec.action}`
|
|
1511
|
+
);
|
|
1512
|
+
console.log(chalk2.dim(` Impact: ${rec.impact}`));
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1458
1515
|
}
|
|
1459
1516
|
console.log();
|
|
1460
1517
|
if (opts.fix) {
|
|
@@ -1557,7 +1614,7 @@ function registerReportCommand(program2) {
|
|
|
1557
1614
|
}
|
|
1558
1615
|
const periodLabel = opts.period === "today" ? now.format("ddd, MMM D, YYYY") : opts.period;
|
|
1559
1616
|
console.log(chalk3.bold.cyan(`
|
|
1560
|
-
kerf report -- ${periodLabel}
|
|
1617
|
+
kerf-cli report -- ${periodLabel}
|
|
1561
1618
|
`));
|
|
1562
1619
|
console.log(` Total Cost: ${chalk3.bold(formatCost(totalCost))}`);
|
|
1563
1620
|
console.log(` Total Tokens: ${formatTokens(totalInput)} in / ${formatTokens(totalOutput)} out`);
|
|
@@ -1609,8 +1666,8 @@ import { mkdirSync as mkdirSync2, existsSync as existsSync5, readFileSync as rea
|
|
|
1609
1666
|
import { join as join6, dirname as dirname2 } from "node:path";
|
|
1610
1667
|
import { homedir as homedir4 } from "node:os";
|
|
1611
1668
|
function registerInitCommand(program2) {
|
|
1612
|
-
program2.command("init").description("Set up kerf for the current project").option("--global", "Install hooks globally").option("--hooks-only", "Only install hooks").option("--no-hooks", "Skip hook installation").option("--force", "Skip confirmation prompts").action(async (opts) => {
|
|
1613
|
-
console.log(chalk4.bold.cyan("\n Welcome to kerf!\n"));
|
|
1669
|
+
program2.command("init").description("Set up kerf-cli for the current project").option("--global", "Install hooks globally").option("--hooks-only", "Only install hooks").option("--no-hooks", "Skip hook installation").option("--force", "Skip confirmation prompts").action(async (opts) => {
|
|
1670
|
+
console.log(chalk4.bold.cyan("\n Welcome to kerf-cli!\n"));
|
|
1614
1671
|
console.log(" Setting up cost intelligence for Claude Code...\n");
|
|
1615
1672
|
const kerfDir = join6(homedir4(), ".kerf");
|
|
1616
1673
|
if (!existsSync5(kerfDir)) {
|
|
@@ -1628,14 +1685,14 @@ function registerInitCommand(program2) {
|
|
|
1628
1685
|
}
|
|
1629
1686
|
}
|
|
1630
1687
|
try {
|
|
1631
|
-
const { execSync } = await import("node:child_process");
|
|
1688
|
+
const { execSync: execSync2 } = await import("node:child_process");
|
|
1632
1689
|
try {
|
|
1633
|
-
|
|
1690
|
+
execSync2("which rtk", { stdio: "ignore" });
|
|
1634
1691
|
console.log(chalk4.green(" Detected RTK (command compression) -- compatible!"));
|
|
1635
1692
|
} catch {
|
|
1636
1693
|
}
|
|
1637
1694
|
try {
|
|
1638
|
-
|
|
1695
|
+
execSync2("which ccusage", { stdio: "ignore" });
|
|
1639
1696
|
console.log(chalk4.green(" Detected ccusage -- will import historical data"));
|
|
1640
1697
|
} catch {
|
|
1641
1698
|
}
|
|
@@ -1664,7 +1721,7 @@ function registerInitCommand(program2) {
|
|
|
1664
1721
|
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "50"
|
|
1665
1722
|
}
|
|
1666
1723
|
}, null, 4).split("\n").map((l) => " " + l).join("\n")));
|
|
1667
|
-
console.log(chalk4.bold.cyan("\n Run 'kerf watch' to start the live dashboard!\n"));
|
|
1724
|
+
console.log(chalk4.bold.cyan("\n Run 'kerf-cli watch' to start the live dashboard!\n"));
|
|
1668
1725
|
});
|
|
1669
1726
|
}
|
|
1670
1727
|
function installHooks(settingsPath) {
|
|
@@ -1691,7 +1748,7 @@ function installHooks(settingsPath) {
|
|
|
1691
1748
|
|
|
1692
1749
|
// src/cli/index.ts
|
|
1693
1750
|
var program = new Command();
|
|
1694
|
-
program.name("kerf").version("0.1.0").description("Cost intelligence for Claude Code. Know before you spend.");
|
|
1751
|
+
program.name("kerf-cli").version("0.1.0").description("Cost intelligence for Claude Code. Know before you spend.");
|
|
1695
1752
|
registerWatchCommand(program);
|
|
1696
1753
|
registerEstimateCommand(program);
|
|
1697
1754
|
registerBudgetCommand(program);
|