@caliber-ai/cli 0.13.0 → 0.14.1

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/bin.js CHANGED
@@ -1185,20 +1185,8 @@ function writeClaudeConfig(config) {
1185
1185
  const written = [];
1186
1186
  fs9.writeFileSync("CLAUDE.md", config.claudeMd);
1187
1187
  written.push("CLAUDE.md");
1188
- const claudeDir = ".claude";
1189
- if (!fs9.existsSync(claudeDir)) fs9.mkdirSync(claudeDir, { recursive: true });
1190
- fs9.writeFileSync(
1191
- path10.join(claudeDir, "settings.json"),
1192
- JSON.stringify(config.settings, null, 2)
1193
- );
1194
- written.push(path10.join(claudeDir, "settings.json"));
1195
- fs9.writeFileSync(
1196
- path10.join(claudeDir, "settings.local.json"),
1197
- JSON.stringify(config.settingsLocal, null, 2)
1198
- );
1199
- written.push(path10.join(claudeDir, "settings.local.json"));
1200
1188
  if (config.skills?.length) {
1201
- const skillsDir = path10.join(claudeDir, "skills");
1189
+ const skillsDir = path10.join(".claude", "skills");
1202
1190
  if (!fs9.existsSync(skillsDir)) fs9.mkdirSync(skillsDir, { recursive: true });
1203
1191
  for (const skill of config.skills) {
1204
1192
  const filename = `${skill.name.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}.md`;
@@ -1212,9 +1200,7 @@ function writeClaudeConfig(config) {
1212
1200
  try {
1213
1201
  if (fs9.existsSync(".mcp.json")) {
1214
1202
  const existing = JSON.parse(fs9.readFileSync(".mcp.json", "utf-8"));
1215
- if (existing.mcpServers) {
1216
- existingServers = existing.mcpServers;
1217
- }
1203
+ if (existing.mcpServers) existingServers = existing.mcpServers;
1218
1204
  }
1219
1205
  } catch {
1220
1206
  }
@@ -1319,7 +1305,11 @@ function fileChecksum(filePath) {
1319
1305
  // src/writers/index.ts
1320
1306
  function writeSetup(setup) {
1321
1307
  const filesToWrite = getFilesToWrite(setup);
1322
- const existingFiles = filesToWrite.filter((f) => fs13.existsSync(f));
1308
+ const filesToDelete = (setup.deletions || []).map((d) => d.filePath).filter((f) => fs13.existsSync(f));
1309
+ const existingFiles = [
1310
+ ...filesToWrite.filter((f) => fs13.existsSync(f)),
1311
+ ...filesToDelete
1312
+ ];
1323
1313
  const backupDir = existingFiles.length > 0 ? createBackup(existingFiles) : void 0;
1324
1314
  const written = [];
1325
1315
  if ((setup.targetAgent === "claude" || setup.targetAgent === "both") && setup.claude) {
@@ -1328,15 +1318,28 @@ function writeSetup(setup) {
1328
1318
  if ((setup.targetAgent === "cursor" || setup.targetAgent === "both") && setup.cursor) {
1329
1319
  written.push(...writeCursorConfig(setup.cursor));
1330
1320
  }
1321
+ const deleted = [];
1322
+ for (const filePath of filesToDelete) {
1323
+ fs13.unlinkSync(filePath);
1324
+ deleted.push(filePath);
1325
+ }
1331
1326
  ensureGitignore();
1332
- const entries = written.map((file) => ({
1333
- path: file,
1334
- action: existingFiles.includes(file) ? "modified" : "created",
1335
- checksum: fileChecksum(file),
1336
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1337
- }));
1327
+ const entries = [
1328
+ ...written.map((file) => ({
1329
+ path: file,
1330
+ action: existingFiles.includes(file) ? "modified" : "created",
1331
+ checksum: fileChecksum(file),
1332
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1333
+ })),
1334
+ ...deleted.map((file) => ({
1335
+ path: file,
1336
+ action: "deleted",
1337
+ checksum: "",
1338
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1339
+ }))
1340
+ ];
1338
1341
  writeManifest({ version: 1, backupDir, entries });
1339
- return { written, backupDir };
1342
+ return { written, deleted, backupDir };
1340
1343
  }
1341
1344
  function undoSetup() {
1342
1345
  const manifest = readManifest();
@@ -1351,7 +1354,7 @@ function undoSetup() {
1351
1354
  fs13.unlinkSync(entry.path);
1352
1355
  removed.push(entry.path);
1353
1356
  }
1354
- } else if (entry.action === "modified" && manifest.backupDir) {
1357
+ } else if ((entry.action === "modified" || entry.action === "deleted") && manifest.backupDir) {
1355
1358
  if (restoreBackup(manifest.backupDir, entry.path)) {
1356
1359
  restored.push(entry.path);
1357
1360
  }
@@ -1366,7 +1369,7 @@ function undoSetup() {
1366
1369
  function getFilesToWrite(setup) {
1367
1370
  const files = [];
1368
1371
  if ((setup.targetAgent === "claude" || setup.targetAgent === "both") && setup.claude) {
1369
- files.push("CLAUDE.md", ".claude/settings.json", ".claude/settings.local.json");
1372
+ files.push("CLAUDE.md");
1370
1373
  if (setup.claude.mcpServers) files.push(".mcp.json");
1371
1374
  if (setup.claude.skills) {
1372
1375
  for (const s of setup.claude.skills) {
@@ -1582,10 +1585,10 @@ async function initCommand(options) {
1582
1585
  `));
1583
1586
  console.log(chalk3.dim(" Configure your coding agent environment\n"));
1584
1587
  console.log(chalk3.bold(" What is Caliber?\n"));
1585
- console.log(chalk3.dim(" Caliber analyzes your project and generates optimized configurations"));
1586
- console.log(chalk3.dim(" for AI coding agents (Claude Code, Cursor). It creates CLAUDE.md,"));
1587
- console.log(chalk3.dim(" .cursorrules, skills, permissions, and MCP servers tailored to your"));
1588
- console.log(chalk3.dim(" codebase \u2014 so your AI agent understands your project from day one.\n"));
1588
+ console.log(chalk3.dim(" Caliber audits your AI agent configurations and suggests targeted"));
1589
+ console.log(chalk3.dim(" improvements. It analyzes CLAUDE.md, .cursorrules, skills, and MCP"));
1590
+ console.log(chalk3.dim(" servers against your actual codebase \u2014 keeping what works, fixing"));
1591
+ console.log(chalk3.dim(" what's stale, and adding what's missing.\n"));
1589
1592
  console.log(chalk3.bold(" How it works:\n"));
1590
1593
  console.log(chalk3.dim(" 1. Scan Analyze your code, dependencies, and file structure"));
1591
1594
  console.log(chalk3.dim(" 2. Match Detect existing configs and check for teammate setups"));
@@ -1704,15 +1707,15 @@ async function initCommand(options) {
1704
1707
  if (isEmpty) {
1705
1708
  fingerprint.description = await promptInput("What will you build in this project?");
1706
1709
  }
1707
- console.log(chalk3.hex("#6366f1").bold(" Step 4/6 \u2014 Planning a better setup\n"));
1708
- console.log(chalk3.dim(" AI is building CLAUDE.md, permissions, skills, and rules based on"));
1709
- console.log(chalk3.dim(" your project's stack and conventions.\n"));
1710
+ console.log(chalk3.hex("#6366f1").bold(" Step 4/6 \u2014 Auditing your configs\n"));
1711
+ console.log(chalk3.dim(" AI is auditing your CLAUDE.md, skills, and rules against your"));
1712
+ console.log(chalk3.dim(" project's actual codebase and conventions.\n"));
1710
1713
  console.log(chalk3.dim(" This usually takes 1\u20133 minutes on first run.\n"));
1711
1714
  let generatedSetup = null;
1712
- let setupExplanation;
1713
1715
  let rawOutput;
1714
1716
  trackEvent("generation_started", { target_agent: targetAgent });
1715
1717
  const hasExistingConfig = !!(ec.claudeMd || ec.claudeSettings || ec.claudeSkills?.length || ec.cursorrules || ec.cursorRules?.length || ec.claudeMcpServers || ec.cursorMcpServers);
1718
+ const genStartTime = Date.now();
1716
1719
  const genSpinner = ora2("Generating setup...").start();
1717
1720
  const genMessages = new SpinnerMessages(genSpinner, GENERATION_MESSAGES, { showElapsedTime: true });
1718
1721
  genMessages.start();
@@ -1729,7 +1732,6 @@ async function initCommand(options) {
1729
1732
  },
1730
1733
  (payload) => {
1731
1734
  generatedSetup = payload.setup;
1732
- setupExplanation = payload.explanation;
1733
1735
  rawOutput = payload.raw;
1734
1736
  },
1735
1737
  (error) => {
@@ -1757,21 +1759,19 @@ async function initCommand(options) {
1757
1759
  }
1758
1760
  throw new Error("__exit__");
1759
1761
  }
1760
- genSpinner.succeed("Setup generated");
1762
+ const elapsedMs = Date.now() - genStartTime;
1763
+ const mins = Math.floor(elapsedMs / 6e4);
1764
+ const secs = Math.floor(elapsedMs % 6e4 / 1e3);
1765
+ const timeStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
1766
+ genSpinner.succeed(`Setup generated ${chalk3.dim(`in ${timeStr}`)}`);
1761
1767
  printSetupSummary(generatedSetup);
1762
1768
  console.log(chalk3.hex("#6366f1").bold(" Step 5/6 \u2014 Review\n"));
1763
- console.log(chalk3.dim(" Review the proposed files below. You can accept, refine via chat,"));
1764
- console.log(chalk3.dim(" or see an explanation of why each item was recommended.\n"));
1765
- let explained = false;
1766
- let action = await promptAction(explained);
1767
- while (action === "explain") {
1768
- if (setupExplanation) {
1769
- printExplanation(setupExplanation);
1770
- } else {
1771
- console.log(chalk3.dim("\nNo explanation available for this setup.\n"));
1772
- }
1773
- explained = true;
1774
- action = await promptAction(explained);
1769
+ console.log(chalk3.dim(" Review the proposed files. You can accept, refine via chat,"));
1770
+ console.log(chalk3.dim(" or preview any file to see exactly what will be written.\n"));
1771
+ let action = await promptAction();
1772
+ while (action === "preview") {
1773
+ await promptFilePreview(generatedSetup);
1774
+ action = await promptAction();
1775
1775
  }
1776
1776
  if (action === "decline") {
1777
1777
  trackEvent("setup_declined");
@@ -1798,11 +1798,21 @@ async function initCommand(options) {
1798
1798
  try {
1799
1799
  const result = writeSetup(generatedSetup);
1800
1800
  writeSpinner.succeed("Config files written");
1801
- trackEvent("setup_applied", { files_written: result.written.length, target_agent: targetAgent });
1801
+ trackEvent("setup_applied", {
1802
+ files_written: result.written.length,
1803
+ files_deleted: result.deleted.length,
1804
+ target_agent: targetAgent
1805
+ });
1802
1806
  console.log(chalk3.bold("\nFiles created/updated:"));
1803
1807
  for (const file of result.written) {
1804
1808
  console.log(` ${chalk3.green("\u2713")} ${file}`);
1805
1809
  }
1810
+ if (result.deleted.length > 0) {
1811
+ console.log(chalk3.bold("\nFiles removed:"));
1812
+ for (const file of result.deleted) {
1813
+ console.log(` ${chalk3.red("\u2717")} ${file}`);
1814
+ }
1815
+ }
1806
1816
  if (result.backupDir) {
1807
1817
  console.log(chalk3.dim(`
1808
1818
  Backups saved to ${result.backupDir}`));
@@ -1856,6 +1866,11 @@ async function initCommand(options) {
1856
1866
  }
1857
1867
  console.log(chalk3.bold.green("\nSetup complete! Your coding agent is now configured."));
1858
1868
  console.log(chalk3.dim("Run `caliber undo` to revert changes.\n"));
1869
+ console.log(chalk3.bold(" Next steps:\n"));
1870
+ console.log(` ${chalk3.hex("#6366f1")("caliber health")} Check your config quality and fix issues`);
1871
+ console.log(` ${chalk3.hex("#6366f1")("caliber recommend")} Discover additional skills for your stack`);
1872
+ console.log(` ${chalk3.hex("#6366f1")("caliber diff")} See what changed since last setup`);
1873
+ console.log("");
1859
1874
  }
1860
1875
  async function refineLoop(currentSetup, _targetAgent) {
1861
1876
  const history = [];
@@ -1920,93 +1935,170 @@ async function promptAgent() {
1920
1935
  ]
1921
1936
  });
1922
1937
  }
1923
- async function promptAction(explained) {
1924
- const choices = [
1925
- { name: "Accept and apply", value: "accept" },
1926
- { name: "Refine via chat", value: "refine" },
1927
- ...!explained ? [{ name: "Explain recommendations", value: "explain" }] : [],
1928
- { name: "Decline", value: "decline" }
1929
- ];
1930
- return select({ message: "What would you like to do?", choices });
1931
- }
1932
- function fileEntry(filePath, desc) {
1933
- const icon = fs16.existsSync(filePath) ? chalk3.yellow("~") : chalk3.green("+");
1934
- const description = desc ? chalk3.dim(`\u2014 ${desc}`) : "";
1935
- return ` ${icon} ${filePath} ${description}`;
1938
+ async function promptAction() {
1939
+ return select({
1940
+ message: "What would you like to do?",
1941
+ choices: [
1942
+ { name: "Accept and apply", value: "accept" },
1943
+ { name: "Refine via chat", value: "refine" },
1944
+ { name: "Preview a file", value: "preview" },
1945
+ { name: "Decline", value: "decline" }
1946
+ ]
1947
+ });
1936
1948
  }
1937
1949
  function printSetupSummary(setup) {
1938
1950
  const claude = setup.claude;
1939
1951
  const cursor = setup.cursor;
1940
- const descriptions = setup.fileDescriptions || {};
1952
+ const fileDescriptions = setup.fileDescriptions;
1953
+ const deletions = setup.deletions;
1941
1954
  console.log("");
1955
+ console.log(chalk3.bold(" Proposed changes:\n"));
1956
+ const getDescription = (filePath) => {
1957
+ return fileDescriptions?.[filePath];
1958
+ };
1942
1959
  if (claude) {
1943
1960
  if (claude.claudeMd) {
1944
- console.log(fileEntry("CLAUDE.md", descriptions["CLAUDE.md"]));
1945
- }
1946
- if (claude.settings) {
1947
- console.log(fileEntry(".claude/settings.json", descriptions[".claude/settings.json"]));
1948
- }
1949
- if (claude.settingsLocal) {
1950
- console.log(fileEntry(".claude/settings.local.json", descriptions[".claude/settings.local.json"]));
1961
+ const icon = fs16.existsSync("CLAUDE.md") ? chalk3.yellow("~") : chalk3.green("+");
1962
+ const desc = getDescription("CLAUDE.md");
1963
+ console.log(` ${icon} ${chalk3.bold("CLAUDE.md")}`);
1964
+ if (desc) {
1965
+ console.log(chalk3.dim(` ${desc}`));
1966
+ }
1967
+ console.log("");
1951
1968
  }
1952
1969
  const skills = claude.skills;
1953
1970
  if (Array.isArray(skills) && skills.length > 0) {
1954
1971
  for (const skill of skills) {
1955
1972
  const skillPath = `.claude/skills/${skill.name.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}.md`;
1956
- console.log(fileEntry(skillPath, descriptions[skillPath] || skill.name));
1973
+ const icon = fs16.existsSync(skillPath) ? chalk3.yellow("~") : chalk3.green("+");
1974
+ const desc = getDescription(skillPath);
1975
+ console.log(` ${icon} ${chalk3.bold(skillPath)}`);
1976
+ console.log(chalk3.dim(` ${desc || summarizeSkill(skill)}`));
1977
+ console.log("");
1957
1978
  }
1958
1979
  }
1959
1980
  const mcpServers = claude.mcpServers;
1960
1981
  if (mcpServers && Object.keys(mcpServers).length > 0) {
1961
- console.log(fileEntry(".mcp.json", descriptions[".mcp.json"] || Object.keys(mcpServers).join(", ")));
1982
+ const icon = fs16.existsSync(".mcp.json") ? chalk3.yellow("~") : chalk3.green("+");
1983
+ const serverNames = Object.keys(mcpServers);
1984
+ const desc = getDescription(".mcp.json");
1985
+ console.log(` ${icon} ${chalk3.bold(".mcp.json")}`);
1986
+ console.log(chalk3.dim(` ${desc || `servers: ${serverNames.join(", ")}`}`));
1987
+ console.log("");
1962
1988
  }
1963
1989
  }
1964
1990
  if (cursor) {
1965
1991
  if (cursor.cursorrules) {
1966
- console.log(fileEntry(".cursorrules", descriptions[".cursorrules"]));
1992
+ const icon = fs16.existsSync(".cursorrules") ? chalk3.yellow("~") : chalk3.green("+");
1993
+ const desc = getDescription(".cursorrules");
1994
+ console.log(` ${icon} ${chalk3.bold(".cursorrules")}`);
1995
+ if (desc) console.log(chalk3.dim(` ${desc}`));
1996
+ console.log("");
1967
1997
  }
1968
1998
  const rules = cursor.rules;
1969
1999
  if (Array.isArray(rules) && rules.length > 0) {
1970
2000
  for (const rule of rules) {
1971
2001
  const rulePath = `.cursor/rules/${rule.filename}`;
1972
- console.log(fileEntry(rulePath, descriptions[rulePath]));
2002
+ const icon = fs16.existsSync(rulePath) ? chalk3.yellow("~") : chalk3.green("+");
2003
+ const desc = getDescription(rulePath);
2004
+ console.log(` ${icon} ${chalk3.bold(rulePath)}`);
2005
+ if (desc) {
2006
+ console.log(chalk3.dim(` ${desc}`));
2007
+ } else {
2008
+ const firstLine = rule.content.split("\n").filter((l) => l.trim() && !l.trim().startsWith("#"))[0];
2009
+ if (firstLine) console.log(chalk3.dim(` ${firstLine.trim().slice(0, 80)}`));
2010
+ }
2011
+ console.log("");
1973
2012
  }
1974
2013
  }
1975
2014
  const mcpServers = cursor.mcpServers;
1976
2015
  if (mcpServers && Object.keys(mcpServers).length > 0) {
1977
- console.log(fileEntry(".cursor/mcp.json", descriptions[".cursor/mcp.json"] || Object.keys(mcpServers).join(", ")));
2016
+ const icon = fs16.existsSync(".cursor/mcp.json") ? chalk3.yellow("~") : chalk3.green("+");
2017
+ const serverNames = Object.keys(mcpServers);
2018
+ const desc = getDescription(".cursor/mcp.json");
2019
+ console.log(` ${icon} ${chalk3.bold(".cursor/mcp.json")}`);
2020
+ console.log(chalk3.dim(` ${desc || `servers: ${serverNames.join(", ")}`}`));
2021
+ console.log("");
1978
2022
  }
1979
2023
  }
1980
- console.log("");
1981
- console.log(` ${chalk3.green("+")} ${chalk3.dim("new")} ${chalk3.yellow("~")} ${chalk3.dim("modified")}`);
2024
+ if (Array.isArray(deletions) && deletions.length > 0) {
2025
+ for (const del of deletions) {
2026
+ console.log(` ${chalk3.red("-")} ${chalk3.bold(del.filePath)}`);
2027
+ console.log(chalk3.dim(` ${del.reason}`));
2028
+ console.log("");
2029
+ }
2030
+ }
2031
+ console.log(` ${chalk3.green("+")} ${chalk3.dim("new")} ${chalk3.yellow("~")} ${chalk3.dim("modified")} ${chalk3.red("-")} ${chalk3.dim("removed")}`);
1982
2032
  console.log("");
1983
2033
  }
1984
- function printExplanation(explanation) {
1985
- console.log(chalk3.bold("\n Why this setup?\n"));
1986
- const lines = explanation.split("\n");
1987
- for (const line of lines) {
1988
- const trimmed = line.trim();
1989
- if (!trimmed) continue;
1990
- const headerMatch = trimmed.match(/^\[(.+)\]$/);
1991
- if (headerMatch) {
1992
- console.log(` ${chalk3.bold.hex("#6366f1")(headerMatch[1])}`);
1993
- continue;
2034
+ function summarizeSkill(skill) {
2035
+ const lines = skill.content.split("\n").filter((l) => l.trim() && !l.trim().startsWith("#"));
2036
+ return lines[0]?.trim().slice(0, 80) || skill.name;
2037
+ }
2038
+ function collectSetupFiles(setup) {
2039
+ const files = [];
2040
+ const claude = setup.claude;
2041
+ const cursor = setup.cursor;
2042
+ if (claude) {
2043
+ if (claude.claudeMd) files.push({ path: "CLAUDE.md", content: claude.claudeMd });
2044
+ const skills = claude.skills;
2045
+ if (Array.isArray(skills)) {
2046
+ for (const skill of skills) {
2047
+ const skillPath = `.claude/skills/${skill.name.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}.md`;
2048
+ files.push({ path: skillPath, content: skill.content });
2049
+ }
1994
2050
  }
1995
- const itemMatch = trimmed.match(/^-\s+\*\*(.+?)\*\*[:\s]*(.*)$/);
1996
- if (itemMatch) {
1997
- const name = itemMatch[1];
1998
- const desc = itemMatch[2].replace(/^\s*[-—:]\s*/, "");
1999
- console.log(` ${chalk3.dim("\u25B8")} ${chalk3.white(name)} ${chalk3.dim(desc)}`);
2000
- continue;
2051
+ if (claude.mcpServers && Object.keys(claude.mcpServers).length > 0) {
2052
+ files.push({ path: ".mcp.json", content: JSON.stringify({ mcpServers: claude.mcpServers }, null, 2) });
2001
2053
  }
2002
- const plainMatch = trimmed.match(/^-\s+(.*)$/);
2003
- if (plainMatch) {
2004
- console.log(` ${chalk3.dim("\u25B8")} ${chalk3.dim(plainMatch[1])}`);
2005
- continue;
2054
+ }
2055
+ if (cursor) {
2056
+ if (cursor.cursorrules) files.push({ path: ".cursorrules", content: cursor.cursorrules });
2057
+ const rules = cursor.rules;
2058
+ if (Array.isArray(rules)) {
2059
+ for (const rule of rules) {
2060
+ files.push({ path: `.cursor/rules/${rule.filename}`, content: rule.content });
2061
+ }
2062
+ }
2063
+ if (cursor.mcpServers && Object.keys(cursor.mcpServers).length > 0) {
2064
+ files.push({ path: ".cursor/mcp.json", content: JSON.stringify({ mcpServers: cursor.mcpServers }, null, 2) });
2006
2065
  }
2007
- console.log(` ${chalk3.dim(trimmed)}`);
2008
2066
  }
2067
+ return files;
2068
+ }
2069
+ function previewFileContent(filePath, content, maxLines = 25) {
2070
+ const lines = content.split("\n");
2071
+ const displayLines = lines.slice(0, maxLines);
2072
+ const maxWidth = Math.max(60, ...displayLines.map((l) => l.length + 4));
2073
+ const width = Math.min(maxWidth, 80);
2009
2074
  console.log("");
2075
+ console.log(` ${chalk3.dim("\u250C\u2500")} ${chalk3.bold(filePath)} ${chalk3.dim("\u2500".repeat(Math.max(0, width - filePath.length - 5)) + "\u2510")}`);
2076
+ console.log(` ${chalk3.dim("\u2502")}${" ".repeat(width - 1)}${chalk3.dim("\u2502")}`);
2077
+ for (const line of displayLines) {
2078
+ const truncated = line.length > width - 5 ? line.slice(0, width - 8) + "..." : line;
2079
+ const padding = " ".repeat(Math.max(0, width - truncated.length - 3));
2080
+ console.log(` ${chalk3.dim("\u2502")} ${truncated}${padding}${chalk3.dim("\u2502")}`);
2081
+ }
2082
+ if (lines.length > maxLines) {
2083
+ console.log(` ${chalk3.dim("\u2502")}${" ".repeat(width - 1)}${chalk3.dim("\u2502")}`);
2084
+ const note = `(showing first ${maxLines} lines, full file is ${lines.length} lines)`;
2085
+ const notePadding = " ".repeat(Math.max(0, width - note.length - 3));
2086
+ console.log(` ${chalk3.dim("\u2502")} ${chalk3.dim(note)}${notePadding}${chalk3.dim("\u2502")}`);
2087
+ }
2088
+ console.log(` ${chalk3.dim("\u2514" + "\u2500".repeat(width - 1) + "\u2518")}`);
2089
+ console.log("");
2090
+ }
2091
+ async function promptFilePreview(setup) {
2092
+ const files = collectSetupFiles(setup);
2093
+ if (files.length === 0) {
2094
+ console.log(chalk3.dim("\n No files to preview.\n"));
2095
+ return;
2096
+ }
2097
+ const choice = await select({
2098
+ message: "Which file to preview?",
2099
+ choices: files.map((f, i) => ({ name: f.path, value: i }))
2100
+ });
2101
+ previewFileContent(files[choice].path, files[choice].content);
2010
2102
  }
2011
2103
 
2012
2104
  // src/commands/undo.ts
@@ -2416,6 +2508,7 @@ function printRecommendations(recs) {
2416
2508
  // src/commands/health.ts
2417
2509
  import chalk9 from "chalk";
2418
2510
  import ora6 from "ora";
2511
+ import confirm2 from "@inquirer/confirm";
2419
2512
  async function healthCommand(options) {
2420
2513
  const auth2 = getStoredAuth();
2421
2514
  if (!auth2) {
@@ -2450,30 +2543,44 @@ async function healthCommand(options) {
2450
2543
  return;
2451
2544
  }
2452
2545
  printReport(report);
2453
- if (options.fix) {
2454
- console.log(chalk9.bold("\nGenerating fix plan..."));
2546
+ const shouldFix = options.fix || report.recommendations.length > 0 && !options.json && await confirm2({ message: "Would you like to fix these issues?", default: true });
2547
+ if (shouldFix) {
2548
+ const planSpinner = ora6("Generating fix plan...").start();
2455
2549
  const plan = await apiRequest(
2456
2550
  `/api/context/reports/${report.id}/fix-plan`,
2457
2551
  { method: "POST", body: { projectId } }
2458
2552
  );
2553
+ planSpinner.succeed("Fix plan ready");
2459
2554
  if (!plan?.actions.length) {
2460
- console.log(chalk9.dim("No fixes needed."));
2555
+ console.log(chalk9.dim(" No fixes needed."));
2461
2556
  return;
2462
2557
  }
2463
- console.log(chalk9.bold("\nProposed actions:"));
2558
+ console.log(chalk9.bold("\n Fix Plan:\n"));
2464
2559
  for (const action of plan.actions) {
2465
- const icon = action.type === "remove" ? chalk9.red("- ") : action.type === "add" ? chalk9.green("+ ") : chalk9.yellow("~ ");
2466
- console.log(` ${icon}${action.type}: ${action.items.join(", ")}`);
2560
+ if (action.type === "remove") {
2561
+ console.log(` ${chalk9.red("\u2717 Remove")} ${action.items.join(", ")}`);
2562
+ if (action.reason) console.log(chalk9.dim(` ${action.reason}`));
2563
+ } else if (action.type === "add") {
2564
+ console.log(` ${chalk9.green("+ Add")} ${action.items.join(", ")}`);
2565
+ if (action.reason) console.log(chalk9.dim(` ${action.reason}`));
2566
+ }
2567
+ console.log("");
2568
+ }
2569
+ const scoreAfterStr = plan.estimatedScoreAfter != null ? String(plan.estimatedScoreAfter) : `${report.score + plan.estimatedScoreImprovement}`;
2570
+ console.log(` Expected: ${report.score} \u2192 ${chalk9.green(scoreAfterStr)}/100
2571
+ `);
2572
+ const shouldExecute = options.fix || await confirm2({ message: "Apply this fix plan?", default: true });
2573
+ if (!shouldExecute) {
2574
+ console.log(chalk9.dim(" Fix cancelled."));
2575
+ return;
2467
2576
  }
2468
- console.log(chalk9.dim(`
2469
- Estimated improvement: +${plan.estimatedScoreImprovement} points`));
2470
- const fixSpinner = ora6("Executing fix plan...").start();
2577
+ const fixSpinner = ora6("Applying fixes...").start();
2471
2578
  try {
2472
2579
  const result = await apiRequest(
2473
2580
  `/api/context/reports/${report.id}/fix-execute`,
2474
2581
  { method: "POST", body: { projectId } }
2475
2582
  );
2476
- fixSpinner.succeed("Fix applied");
2583
+ fixSpinner.succeed("Fixes applied");
2477
2584
  console.log("");
2478
2585
  console.log(` Score: ${result.scoreBefore} \u2192 ${chalk9.green(String(result.scoreAfter))} (${chalk9.green(`+${result.improvement}`)})`);
2479
2586
  if (result.itemsRemoved) console.log(` Removed: ${result.itemsRemoved} items`);
@@ -2486,6 +2593,13 @@ Estimated improvement: +${plan.estimatedScoreImprovement} points`));
2486
2593
  }
2487
2594
  }
2488
2595
  }
2596
+ var CATEGORY_DESCRIPTIONS = {
2597
+ tooling: "build/test/lint commands (helps agents the most)",
2598
+ essential: "constraints agents can't discover from code",
2599
+ convention: "style/naming rules (linters handle this; low value)",
2600
+ overview: "project summaries (agents discover this by reading code)",
2601
+ workflow: "development workflows and processes"
2602
+ };
2489
2603
  function printReport(report) {
2490
2604
  const gradeColors = {
2491
2605
  A: chalk9.green,
@@ -2498,24 +2612,43 @@ function printReport(report) {
2498
2612
  console.log(chalk9.bold("\n Context Health Report\n"));
2499
2613
  console.log(` Grade: ${gradeColor(report.grade)} Score: ${gradeColor(String(report.score))}/100
2500
2614
  `);
2501
- if (Object.keys(report.category_breakdown).length) {
2502
- console.log(chalk9.dim(" Category Breakdown:"));
2503
- const categories = Object.entries(report.category_breakdown);
2504
- const maxCount = Math.max(...categories.map(([, v]) => v.count));
2615
+ const categories = Object.entries(report.category_breakdown);
2616
+ if (categories.length) {
2617
+ console.log(chalk9.bold(" Category Breakdown:\n"));
2618
+ const itemsByCategory = /* @__PURE__ */ new Map();
2619
+ if (report.item_classifications) {
2620
+ for (const item of report.item_classifications) {
2621
+ const cat = item.category.toLowerCase();
2622
+ if (!itemsByCategory.has(cat)) itemsByCategory.set(cat, []);
2623
+ itemsByCategory.get(cat).push(item);
2624
+ }
2625
+ }
2505
2626
  for (const [name, data] of categories) {
2506
- const barLen = maxCount > 0 ? Math.round(data.count / maxCount * 20) : 0;
2507
- const bar = "\u2588".repeat(barLen) + "\u2591".repeat(20 - barLen);
2508
- console.log(` ${name.padEnd(12)} ${bar} ${data.count} items (${data.tokens} tokens)`);
2627
+ const desc = CATEGORY_DESCRIPTIONS[name] || "";
2628
+ const header = desc ? `${chalk9.bold(name)} ${chalk9.dim(`\u2014 ${desc}`)}` : chalk9.bold(name);
2629
+ console.log(` ${header}`);
2630
+ const items = itemsByCategory.get(name);
2631
+ if (items?.length) {
2632
+ for (const item of items) {
2633
+ const icon = item.impactScore >= 0 ? chalk9.green("\u2713") : chalk9.red("\u2717");
2634
+ const snippet = item.recommendation.length > 70 ? item.recommendation.slice(0, 67) + "..." : item.recommendation;
2635
+ console.log(` ${icon} ${item.itemName.padEnd(22)} ${chalk9.dim(`"${snippet}"`)}`);
2636
+ }
2637
+ }
2638
+ console.log(chalk9.dim(` ${data.count} items \xB7 ${data.tokens} tokens
2639
+ `));
2509
2640
  }
2510
- console.log("");
2511
2641
  }
2512
2642
  if (report.recommendations.length) {
2513
- console.log(chalk9.dim(" Recommendations:"));
2643
+ console.log(chalk9.bold(" Recommendations:\n"));
2514
2644
  for (const rec of report.recommendations) {
2515
2645
  const icon = rec.priority === "high" ? chalk9.red("!") : rec.priority === "medium" ? chalk9.yellow("~") : chalk9.dim("-");
2516
2646
  console.log(` ${icon} ${rec.description}`);
2647
+ if (rec.reasoning) {
2648
+ console.log(chalk9.dim(` ${rec.reasoning}`));
2649
+ }
2650
+ console.log("");
2517
2651
  }
2518
- console.log("");
2519
2652
  }
2520
2653
  }
2521
2654
 
@@ -3041,7 +3174,7 @@ import chalk14 from "chalk";
3041
3174
  import readline2 from "readline";
3042
3175
  import ora10 from "ora";
3043
3176
  import select2 from "@inquirer/select";
3044
- import confirm2 from "@inquirer/confirm";
3177
+ import confirm3 from "@inquirer/confirm";
3045
3178
  function prompt(question) {
3046
3179
  const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
3047
3180
  return new Promise((resolve2) => {
@@ -3121,7 +3254,7 @@ async function reviewCommand(message, options) {
3121
3254
  console.log(` Most useful: ${bestPart || chalk14.dim("(skipped)")}`);
3122
3255
  console.log(` Could be better: ${biggestGap || chalk14.dim("(skipped)")}`);
3123
3256
  console.log(` Would recommend: ${wouldRecommend}`);
3124
- const shouldSubmit = await confirm2({ message: "Submit this review?", default: true });
3257
+ const shouldSubmit = await confirm3({ message: "Submit this review?", default: true });
3125
3258
  if (!shouldSubmit) {
3126
3259
  console.log(chalk14.dim("\n Review cancelled.\n"));
3127
3260
  return;
@@ -3135,7 +3268,8 @@ var pkg3 = JSON.parse(
3135
3268
  fs20.readFileSync(path17.resolve(__dirname2, "..", "package.json"), "utf-8")
3136
3269
  );
3137
3270
  var program = new Command();
3138
- program.name("caliber").description("Configure your coding agent environment").version(pkg3.version);
3271
+ var displayVersion = process.env.CALIBER_LOCAL ? `${pkg3.version}-local` : pkg3.version;
3272
+ program.name(process.env.CALIBER_LOCAL ? "caloc" : "caliber").description("Configure your coding agent environment").version(displayVersion);
3139
3273
  program.command("init").description("Initialize coding agent setup for this project").option("--agent <type>", "Target agent: claude, cursor, or both").option("--dry-run", "Preview changes without writing files").option("--force", "Overwrite existing setup without prompting").action(initCommand);
3140
3274
  program.command("undo").description("Revert all config changes made by Caliber").action(undoCommand);
3141
3275
  program.command("status").description("Show current Caliber setup status").option("--json", "Output as JSON").action(statusCommand);
@@ -3160,13 +3294,13 @@ import { fileURLToPath as fileURLToPath4 } from "url";
3160
3294
  import { execSync as execSync4 } from "child_process";
3161
3295
  import chalk15 from "chalk";
3162
3296
  import ora11 from "ora";
3163
- import confirm3 from "@inquirer/confirm";
3297
+ import confirm4 from "@inquirer/confirm";
3164
3298
  var __dirname_vc = path18.dirname(fileURLToPath4(import.meta.url));
3165
3299
  var pkg4 = JSON.parse(
3166
3300
  fs21.readFileSync(path18.resolve(__dirname_vc, "..", "package.json"), "utf-8")
3167
3301
  );
3168
3302
  async function promptYesNo(question) {
3169
- return confirm3({ message: question, default: true });
3303
+ return confirm4({ message: question, default: true });
3170
3304
  }
3171
3305
  async function checkForUpdates() {
3172
3306
  if (process.env.CALIBER_SKIP_UPDATE_CHECK) return;
@@ -3231,6 +3365,9 @@ Restarting: caliber ${args.join(" ")}
3231
3365
  }
3232
3366
 
3233
3367
  // src/bin.ts
3368
+ if (process.env.CALIBER_LOCAL) {
3369
+ process.env.CALIBER_SKIP_UPDATE_CHECK = "1";
3370
+ }
3234
3371
  var firstRun = isFirstRun();
3235
3372
  getDeviceId();
3236
3373
  if (firstRun) {