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/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
- return dayjs2(key, "YYYY-MM-DD-HH").format("MMM D, h A");
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 = execSync("git diff --name-only HEAD 2>/dev/null || git ls-files -m 2>/dev/null", {
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.mcpOnly) {
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.recommendations.length > 0) {
1450
- console.log(chalk2.bold(" Recommendations:"));
1451
- result.recommendations.forEach((rec, i) => {
1452
- const priorityColor = rec.priority === "high" ? chalk2.red : rec.priority === "medium" ? chalk2.yellow : chalk2.dim;
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
- ` ${i + 1}. ${priorityColor(`[${rec.priority.toUpperCase()}]`)} ${rec.action}`
1496
+ ` ${server.name.padEnd(20)} ${String(server.toolCount).padStart(3)} tools ${server.estimatedTokens.toLocaleString().padStart(6)} tokens${heavy}`
1455
1497
  );
1456
- console.log(chalk2.dim(` Impact: ${rec.impact}`));
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
- execSync("which rtk", { stdio: "ignore" });
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
- execSync("which ccusage", { stdio: "ignore" });
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);