@rely-ai/caliber 1.4.2 → 1.5.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/bin.js +1339 -1303
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -56,12 +56,12 @@ import path22 from "path";
56
56
  import { fileURLToPath } from "url";
57
57
 
58
58
  // src/commands/onboard.ts
59
- import chalk6 from "chalk";
60
- import ora2 from "ora";
59
+ import chalk7 from "chalk";
60
+ import ora3 from "ora";
61
61
  import readline4 from "readline";
62
- import select3 from "@inquirer/select";
62
+ import select4 from "@inquirer/select";
63
63
  import checkbox from "@inquirer/checkbox";
64
- import fs21 from "fs";
64
+ import fs22 from "fs";
65
65
 
66
66
  // src/fingerprint/index.ts
67
67
  import fs6 from "fs";
@@ -1242,11 +1242,23 @@ AgentSetup schema:
1242
1242
 
1243
1243
  Do NOT generate mcpServers \u2014 MCP configuration is managed separately.
1244
1244
 
1245
- All skills follow the OpenSkills standard (agentskills.io):
1246
- - The "name" field must be kebab-case (lowercase letters, numbers, hyphens only). It becomes the directory name.
1247
- - The "description" field should describe what the skill does AND when to use it \u2014 this drives automatic skill discovery by agents.
1248
- - The "content" field is the markdown body only \u2014 do NOT include YAML frontmatter in the content, it will be generated from the name and description fields.
1249
- - Keep skill content under 500 lines. Move detailed references to separate files if needed.
1245
+ All skills follow the OpenSkills standard (agentskills.io). Anthropic's official skill guide defines three levels of progressive disclosure:
1246
+ - Level 1 (YAML frontmatter): Always loaded. Must have enough info for the agent to decide when to activate the skill.
1247
+ - Level 2 (SKILL.md body): Loaded when the skill is relevant. Contains full instructions.
1248
+ - Level 3 (references/): Only loaded on demand for deep detail.
1249
+
1250
+ Skill field requirements:
1251
+ - "name": kebab-case (lowercase letters, numbers, hyphens only). Becomes the directory name.
1252
+ - "description": MUST include WHAT it does + WHEN to use it with specific trigger phrases. Example: "Manages database migrations. Use when user says 'run migration', 'create migration', 'db schema change', or modifies files in db/migrations/."
1253
+ - "content": markdown body only \u2014 do NOT include YAML frontmatter, it is generated from name+description.
1254
+
1255
+ Skill content structure \u2014 follow this template:
1256
+ 1. A heading with the skill name
1257
+ 2. "## Instructions" \u2014 clear, numbered steps. Be specific: include exact commands, file paths, parameter names.
1258
+ 3. "## Examples" \u2014 at least one example showing: User says \u2192 Actions taken \u2192 Result
1259
+ 4. "## Troubleshooting" (optional) \u2014 common errors and how to fix them
1260
+
1261
+ Keep skill content under 200 lines. Focus on actionable instructions, not documentation prose.
1250
1262
 
1251
1263
  The "fileDescriptions" object MUST include a one-liner for every file that will be created or modified. Use actual file paths as keys (e.g. "CLAUDE.md", "AGENTS.md", ".claude/skills/my-skill/SKILL.md", ".agents/skills/my-skill/SKILL.md", ".cursor/skills/my-skill/SKILL.md", ".cursor/rules/my-rule.mdc"). Each description should explain why the change is needed, be concise and lowercase.
1252
1264
 
@@ -1457,13 +1469,13 @@ async function detectProjectStack(fileTree, fileContents) {
1457
1469
  }
1458
1470
 
1459
1471
  // src/fingerprint/index.ts
1460
- function collectFingerprint(dir) {
1472
+ async function collectFingerprint(dir) {
1461
1473
  const gitRemoteUrl = getGitRemoteUrl();
1462
1474
  const fileTree = getFileTree(dir);
1463
1475
  const existingConfigs = readExistingConfigs(dir);
1464
1476
  const codeAnalysis = analyzeCode(dir);
1465
1477
  const packageName = readPackageName(dir);
1466
- return {
1478
+ const fingerprint = {
1467
1479
  gitRemoteUrl,
1468
1480
  packageName,
1469
1481
  languages: [],
@@ -1473,6 +1485,8 @@ function collectFingerprint(dir) {
1473
1485
  existingConfigs,
1474
1486
  codeAnalysis
1475
1487
  };
1488
+ await enrichWithLLM(fingerprint, dir);
1489
+ return fingerprint;
1476
1490
  }
1477
1491
  function readPackageName(dir) {
1478
1492
  try {
@@ -1498,7 +1512,7 @@ var DEP_FILE_PATTERNS = [
1498
1512
  "composer.json"
1499
1513
  ];
1500
1514
  var MAX_CONTENT_SIZE = 50 * 1024;
1501
- async function enrichFingerprintWithLLM(fingerprint, dir) {
1515
+ async function enrichWithLLM(fingerprint, dir) {
1502
1516
  try {
1503
1517
  const config = loadConfig();
1504
1518
  if (!config) return;
@@ -1520,21 +1534,9 @@ async function enrichFingerprintWithLLM(fingerprint, dir) {
1520
1534
  }
1521
1535
  if (Object.keys(fileContents).length === 0 && fingerprint.fileTree.length === 0) return;
1522
1536
  const result = await detectProjectStack(fingerprint.fileTree, fileContents);
1523
- if (result.languages?.length) {
1524
- const langSet = new Set(fingerprint.languages);
1525
- for (const lang of result.languages) langSet.add(lang);
1526
- fingerprint.languages = [...langSet];
1527
- }
1528
- if (result.frameworks?.length) {
1529
- const fwSet = new Set(fingerprint.frameworks);
1530
- for (const fw of result.frameworks) fwSet.add(fw);
1531
- fingerprint.frameworks = [...fwSet];
1532
- }
1533
- if (result.tools?.length) {
1534
- const toolSet = new Set(fingerprint.tools);
1535
- for (const tool of result.tools) toolSet.add(tool);
1536
- fingerprint.tools = [...toolSet];
1537
- }
1537
+ if (result.languages?.length) fingerprint.languages = result.languages;
1538
+ if (result.frameworks?.length) fingerprint.frameworks = result.frameworks;
1539
+ if (result.tools?.length) fingerprint.tools = result.tools;
1538
1540
  } catch {
1539
1541
  }
1540
1542
  }
@@ -4632,702 +4634,846 @@ async function interactiveSelect(candidates) {
4632
4634
  });
4633
4635
  }
4634
4636
 
4635
- // src/commands/onboard.ts
4636
- async function initCommand(options) {
4637
- const brand = chalk6.hex("#EB9D83");
4638
- const title = chalk6.hex("#83D1EB");
4639
- console.log(brand.bold(`
4640
- \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557
4641
- \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
4642
- \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
4643
- \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
4644
- \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551
4645
- \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D
4646
- `));
4647
- console.log(chalk6.dim(" Onboard your project for AI-assisted development\n"));
4648
- console.log(title.bold(" Welcome to Caliber\n"));
4649
- console.log(chalk6.dim(" Caliber analyzes your codebase and creates tailored config files"));
4650
- console.log(chalk6.dim(" so your AI coding agents understand your project from day one.\n"));
4651
- console.log(title.bold(" How onboarding works:\n"));
4652
- console.log(chalk6.dim(" 1. Connect Set up your LLM provider"));
4653
- console.log(chalk6.dim(" 2. Discover Analyze your code, dependencies, and structure"));
4654
- console.log(chalk6.dim(" 3. Generate Create config files tailored to your project"));
4655
- console.log(chalk6.dim(" 4. Review Preview, refine, and apply the changes"));
4656
- console.log(chalk6.dim(" 5. Enhance Discover MCP servers for your tools\n"));
4657
- console.log(title.bold(" Step 1/5 \u2014 Connect your LLM\n"));
4658
- let config = loadConfig();
4659
- if (!config) {
4660
- console.log(chalk6.dim(" No LLM provider set yet. Choose how to run Caliber:\n"));
4661
- try {
4662
- await runInteractiveProviderSetup({
4663
- selectMessage: "How do you want to use Caliber? (choose LLM provider)"
4637
+ // src/commands/recommend.ts
4638
+ import chalk6 from "chalk";
4639
+ import ora2 from "ora";
4640
+ import select3 from "@inquirer/select";
4641
+ import { mkdirSync, readFileSync as readFileSync7, readdirSync as readdirSync5, existsSync as existsSync9, writeFileSync } from "fs";
4642
+ import { join as join8, dirname as dirname2 } from "path";
4643
+
4644
+ // src/scanner/index.ts
4645
+ import fs21 from "fs";
4646
+ import path17 from "path";
4647
+ import crypto2 from "crypto";
4648
+ function scanLocalState(dir) {
4649
+ const items = [];
4650
+ const claudeMdPath = path17.join(dir, "CLAUDE.md");
4651
+ if (fs21.existsSync(claudeMdPath)) {
4652
+ items.push({
4653
+ type: "rule",
4654
+ platform: "claude",
4655
+ name: "CLAUDE.md",
4656
+ contentHash: hashFile(claudeMdPath),
4657
+ path: claudeMdPath
4658
+ });
4659
+ }
4660
+ const skillsDir = path17.join(dir, ".claude", "skills");
4661
+ if (fs21.existsSync(skillsDir)) {
4662
+ for (const file of fs21.readdirSync(skillsDir).filter((f) => f.endsWith(".md"))) {
4663
+ const filePath = path17.join(skillsDir, file);
4664
+ items.push({
4665
+ type: "skill",
4666
+ platform: "claude",
4667
+ name: file,
4668
+ contentHash: hashFile(filePath),
4669
+ path: filePath
4664
4670
  });
4665
- } catch (err) {
4666
- if (err.message === "__exit__") throw err;
4667
- throw err;
4668
- }
4669
- config = loadConfig();
4670
- if (!config) {
4671
- console.log(chalk6.red(" Setup was cancelled or failed.\n"));
4672
- throw new Error("__exit__");
4673
4671
  }
4674
- console.log(chalk6.green(" \u2713 Provider saved. Let's continue.\n"));
4675
4672
  }
4676
- const displayModel = config.model === "default" && config.provider === "claude-cli" ? process.env.ANTHROPIC_MODEL || "default (inherited from Claude Code)" : config.model;
4677
- const fastModel = getFastModel();
4678
- const modelLine = fastModel ? ` Provider: ${config.provider} | Model: ${displayModel} | Scan: ${fastModel}` : ` Provider: ${config.provider} | Model: ${displayModel}`;
4679
- console.log(chalk6.dim(modelLine + "\n"));
4680
- console.log(title.bold(" Step 2/5 \u2014 Discover your project\n"));
4681
- console.log(chalk6.dim(" Learning about your languages, dependencies, structure, and existing configs.\n"));
4682
- const spinner = ora2("Analyzing project...").start();
4683
- const fingerprint = collectFingerprint(process.cwd());
4684
- await enrichFingerprintWithLLM(fingerprint, process.cwd());
4685
- spinner.succeed("Project analyzed");
4686
- console.log(chalk6.dim(` Languages: ${fingerprint.languages.join(", ") || "none detected"}`));
4687
- console.log(chalk6.dim(` Files: ${fingerprint.fileTree.length} found
4688
- `));
4689
- const targetAgent = options.agent || await promptAgent();
4690
- const preScore = computeLocalScore(process.cwd(), targetAgent);
4691
- const failingForDismissal = preScore.checks.filter((c) => !c.passed && c.maxPoints > 0);
4692
- if (failingForDismissal.length > 0) {
4693
- const newDismissals = await evaluateDismissals(failingForDismissal, fingerprint);
4694
- if (newDismissals.length > 0) {
4695
- const existing = readDismissedChecks();
4696
- const existingIds = new Set(existing.map((d) => d.id));
4697
- const merged = [...existing, ...newDismissals.filter((d) => !existingIds.has(d.id))];
4698
- writeDismissedChecks(merged);
4673
+ const mcpJsonPath = path17.join(dir, ".mcp.json");
4674
+ if (fs21.existsSync(mcpJsonPath)) {
4675
+ try {
4676
+ const mcpJson = JSON.parse(fs21.readFileSync(mcpJsonPath, "utf-8"));
4677
+ if (mcpJson.mcpServers) {
4678
+ for (const name of Object.keys(mcpJson.mcpServers)) {
4679
+ items.push({
4680
+ type: "mcp",
4681
+ platform: "claude",
4682
+ name,
4683
+ contentHash: hashJson(mcpJson.mcpServers[name]),
4684
+ path: mcpJsonPath
4685
+ });
4686
+ }
4687
+ }
4688
+ } catch {
4699
4689
  }
4700
4690
  }
4701
- const baselineScore = computeLocalScore(process.cwd(), targetAgent);
4702
- displayScoreSummary(baselineScore);
4703
- const hasExistingConfig = !!(fingerprint.existingConfigs.claudeMd || fingerprint.existingConfigs.claudeSettings || fingerprint.existingConfigs.claudeSkills?.length || fingerprint.existingConfigs.cursorrules || fingerprint.existingConfigs.cursorRules?.length || fingerprint.existingConfigs.agentsMd);
4704
- const NON_LLM_CHECKS = /* @__PURE__ */ new Set(["hooks_configured", "agents_md_exists", "permissions_configured", "mcp_servers"]);
4705
- if (hasExistingConfig && baselineScore.score === 100) {
4706
- console.log(chalk6.bold.green(" Your setup is already optimal \u2014 nothing to change.\n"));
4707
- console.log(chalk6.dim(" Run ") + chalk6.hex("#83D1EB")("caliber onboard --force") + chalk6.dim(" to regenerate anyway.\n"));
4708
- if (!options.force) return;
4691
+ const agentsMdPath = path17.join(dir, "AGENTS.md");
4692
+ if (fs21.existsSync(agentsMdPath)) {
4693
+ items.push({
4694
+ type: "rule",
4695
+ platform: "codex",
4696
+ name: "AGENTS.md",
4697
+ contentHash: hashFile(agentsMdPath),
4698
+ path: agentsMdPath
4699
+ });
4709
4700
  }
4710
- const allFailingChecks = baselineScore.checks.filter((c) => !c.passed && c.maxPoints > 0);
4711
- const llmFixableChecks = allFailingChecks.filter((c) => !NON_LLM_CHECKS.has(c.id));
4712
- if (hasExistingConfig && llmFixableChecks.length === 0 && allFailingChecks.length > 0 && !options.force) {
4713
- console.log(chalk6.bold.green("\n Your config is fully optimized for LLM generation.\n"));
4714
- console.log(chalk6.dim(" Remaining items need CLI actions:\n"));
4715
- for (const check of allFailingChecks) {
4716
- console.log(chalk6.dim(` \u2022 ${check.name}`));
4717
- if (check.suggestion) {
4718
- console.log(` ${chalk6.hex("#83D1EB")(check.suggestion)}`);
4701
+ const codexSkillsDir = path17.join(dir, ".agents", "skills");
4702
+ if (fs21.existsSync(codexSkillsDir)) {
4703
+ try {
4704
+ for (const name of fs21.readdirSync(codexSkillsDir)) {
4705
+ const skillFile = path17.join(codexSkillsDir, name, "SKILL.md");
4706
+ if (fs21.existsSync(skillFile)) {
4707
+ items.push({
4708
+ type: "skill",
4709
+ platform: "codex",
4710
+ name: `${name}/SKILL.md`,
4711
+ contentHash: hashFile(skillFile),
4712
+ path: skillFile
4713
+ });
4714
+ }
4719
4715
  }
4716
+ } catch {
4720
4717
  }
4721
- console.log("");
4722
- console.log(chalk6.dim(" Run ") + chalk6.hex("#83D1EB")("caliber onboard --force") + chalk6.dim(" to regenerate anyway.\n"));
4723
- return;
4724
4718
  }
4725
- const isEmpty = fingerprint.fileTree.length < 3;
4726
- if (isEmpty) {
4727
- fingerprint.description = await promptInput3("What will you build in this project?");
4719
+ const cursorrulesPath = path17.join(dir, ".cursorrules");
4720
+ if (fs21.existsSync(cursorrulesPath)) {
4721
+ items.push({
4722
+ type: "rule",
4723
+ platform: "cursor",
4724
+ name: ".cursorrules",
4725
+ contentHash: hashFile(cursorrulesPath),
4726
+ path: cursorrulesPath
4727
+ });
4728
4728
  }
4729
- let failingChecks;
4730
- let passingChecks;
4731
- let currentScore;
4732
- if (hasExistingConfig && baselineScore.score >= 95 && !options.force) {
4733
- failingChecks = llmFixableChecks.map((c) => ({ name: c.name, suggestion: c.suggestion }));
4734
- passingChecks = baselineScore.checks.filter((c) => c.passed).map((c) => ({ name: c.name }));
4735
- currentScore = baselineScore.score;
4736
- if (failingChecks.length > 0) {
4737
- console.log(title.bold(" Step 3/5 \u2014 Fine-tuning\n"));
4738
- console.log(chalk6.dim(` Your setup scores ${baselineScore.score}/100 \u2014 fixing ${failingChecks.length} remaining issue${failingChecks.length === 1 ? "" : "s"}:
4739
- `));
4740
- for (const check of failingChecks) {
4741
- console.log(chalk6.dim(` \u2022 ${check.name}`));
4742
- }
4743
- console.log("");
4729
+ const cursorRulesDir = path17.join(dir, ".cursor", "rules");
4730
+ if (fs21.existsSync(cursorRulesDir)) {
4731
+ for (const file of fs21.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"))) {
4732
+ const filePath = path17.join(cursorRulesDir, file);
4733
+ items.push({
4734
+ type: "rule",
4735
+ platform: "cursor",
4736
+ name: file,
4737
+ contentHash: hashFile(filePath),
4738
+ path: filePath
4739
+ });
4744
4740
  }
4745
- } else if (hasExistingConfig) {
4746
- console.log(title.bold(" Step 3/5 \u2014 Improve your setup\n"));
4747
- console.log(chalk6.dim(" Reviewing your existing configs against your codebase"));
4748
- console.log(chalk6.dim(" and preparing improvements.\n"));
4749
- } else {
4750
- console.log(title.bold(" Step 3/5 \u2014 Build your agent setup\n"));
4751
- console.log(chalk6.dim(" Creating config files tailored to your project.\n"));
4752
4741
  }
4753
- console.log(chalk6.dim(" This can take a couple of minutes depending on your model and provider.\n"));
4754
- const genStartTime = Date.now();
4755
- const genSpinner = ora2("Generating setup...").start();
4756
- const genMessages = new SpinnerMessages(genSpinner, GENERATION_MESSAGES, { showElapsedTime: true });
4757
- genMessages.start();
4758
- let generatedSetup = null;
4759
- let rawOutput;
4760
- try {
4761
- const result = await generateSetup(
4762
- fingerprint,
4763
- targetAgent,
4764
- fingerprint.description,
4765
- {
4766
- onStatus: (status) => {
4767
- genMessages.handleServerStatus(status);
4768
- },
4769
- onComplete: (setup) => {
4770
- generatedSetup = setup;
4771
- },
4772
- onError: (error) => {
4773
- genMessages.stop();
4774
- genSpinner.fail(`Generation error: ${error}`);
4742
+ const cursorSkillsDir = path17.join(dir, ".cursor", "skills");
4743
+ if (fs21.existsSync(cursorSkillsDir)) {
4744
+ try {
4745
+ for (const name of fs21.readdirSync(cursorSkillsDir)) {
4746
+ const skillFile = path17.join(cursorSkillsDir, name, "SKILL.md");
4747
+ if (fs21.existsSync(skillFile)) {
4748
+ items.push({
4749
+ type: "skill",
4750
+ platform: "cursor",
4751
+ name: `${name}/SKILL.md`,
4752
+ contentHash: hashFile(skillFile),
4753
+ path: skillFile
4754
+ });
4775
4755
  }
4776
- },
4777
- failingChecks,
4778
- currentScore,
4779
- passingChecks
4780
- );
4781
- if (!generatedSetup) {
4782
- generatedSetup = result.setup;
4783
- rawOutput = result.raw;
4784
- }
4785
- } catch (err) {
4786
- genMessages.stop();
4787
- const msg = err instanceof Error ? err.message : "Unknown error";
4788
- genSpinner.fail(`Generation failed: ${msg}`);
4789
- throw new Error("__exit__");
4790
- }
4791
- genMessages.stop();
4792
- if (!generatedSetup) {
4793
- genSpinner.fail("Failed to generate setup.");
4794
- if (rawOutput) {
4795
- console.log(chalk6.dim("\nRaw LLM output (JSON parse failed):"));
4796
- console.log(chalk6.dim(rawOutput.slice(0, 500)));
4756
+ }
4757
+ } catch {
4797
4758
  }
4798
- throw new Error("__exit__");
4799
4759
  }
4800
- const elapsedMs = Date.now() - genStartTime;
4801
- const mins = Math.floor(elapsedMs / 6e4);
4802
- const secs = Math.floor(elapsedMs % 6e4 / 1e3);
4803
- const timeStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
4804
- genSpinner.succeed(`Setup generated ${chalk6.dim(`in ${timeStr}`)}`);
4805
- printSetupSummary(generatedSetup);
4806
- const sessionHistory = [];
4807
- sessionHistory.push({
4808
- role: "assistant",
4809
- content: summarizeSetup("Initial generation", generatedSetup)
4810
- });
4811
- console.log(title.bold(" Step 4/5 \u2014 Review and apply\n"));
4812
- const setupFiles = collectSetupFiles(generatedSetup);
4813
- const staged = stageFiles(setupFiles, process.cwd());
4814
- const totalChanges = staged.newFiles + staged.modifiedFiles;
4815
- console.log(chalk6.dim(` ${chalk6.green(`${staged.newFiles} new`)} / ${chalk6.yellow(`${staged.modifiedFiles} modified`)} file${totalChanges !== 1 ? "s" : ""}
4816
- `));
4817
- let action;
4818
- if (totalChanges === 0) {
4819
- console.log(chalk6.dim(" No changes needed \u2014 your configs are already up to date.\n"));
4820
- cleanupStaging();
4821
- action = "accept";
4822
- } else {
4823
- const wantsReview = await promptWantsReview();
4824
- if (wantsReview) {
4825
- const reviewMethod = await promptReviewMethod();
4826
- await openReview(reviewMethod, staged.stagedFiles);
4760
+ const cursorMcpPath = path17.join(dir, ".cursor", "mcp.json");
4761
+ if (fs21.existsSync(cursorMcpPath)) {
4762
+ try {
4763
+ const mcpJson = JSON.parse(fs21.readFileSync(cursorMcpPath, "utf-8"));
4764
+ if (mcpJson.mcpServers) {
4765
+ for (const name of Object.keys(mcpJson.mcpServers)) {
4766
+ items.push({
4767
+ type: "mcp",
4768
+ platform: "cursor",
4769
+ name,
4770
+ contentHash: hashJson(mcpJson.mcpServers[name]),
4771
+ path: cursorMcpPath
4772
+ });
4773
+ }
4774
+ }
4775
+ } catch {
4827
4776
  }
4828
- action = await promptReviewAction();
4829
4777
  }
4830
- while (action === "refine") {
4831
- generatedSetup = await refineLoop(generatedSetup, targetAgent, sessionHistory);
4832
- if (!generatedSetup) {
4833
- cleanupStaging();
4834
- console.log(chalk6.dim("Refinement cancelled. No files were modified."));
4835
- return;
4836
- }
4837
- const updatedFiles = collectSetupFiles(generatedSetup);
4838
- const restaged = stageFiles(updatedFiles, process.cwd());
4839
- console.log(chalk6.dim(` ${chalk6.green(`${restaged.newFiles} new`)} / ${chalk6.yellow(`${restaged.modifiedFiles} modified`)} file${restaged.newFiles + restaged.modifiedFiles !== 1 ? "s" : ""}
4840
- `));
4841
- printSetupSummary(generatedSetup);
4842
- await openReview("terminal", restaged.stagedFiles);
4843
- action = await promptReviewAction();
4844
- }
4845
- cleanupStaging();
4846
- if (action === "decline") {
4847
- console.log(chalk6.dim("Setup declined. No files were modified."));
4848
- return;
4778
+ return items;
4779
+ }
4780
+ function hashFile(filePath) {
4781
+ const text = fs21.readFileSync(filePath, "utf-8");
4782
+ return crypto2.createHash("sha256").update(JSON.stringify({ text })).digest("hex");
4783
+ }
4784
+ function hashJson(obj) {
4785
+ return crypto2.createHash("sha256").update(JSON.stringify(obj)).digest("hex");
4786
+ }
4787
+
4788
+ // src/commands/recommend.ts
4789
+ function detectLocalPlatforms() {
4790
+ const items = scanLocalState(process.cwd());
4791
+ const platforms = /* @__PURE__ */ new Set();
4792
+ for (const item of items) {
4793
+ platforms.add(item.platform);
4849
4794
  }
4850
- if (options.dryRun) {
4851
- console.log(chalk6.yellow("\n[Dry run] Would write the following files:"));
4852
- console.log(JSON.stringify(generatedSetup, null, 2));
4853
- return;
4795
+ return platforms.size > 0 ? Array.from(platforms) : ["claude"];
4796
+ }
4797
+ function getSkillPath(platform, slug) {
4798
+ if (platform === "cursor") {
4799
+ return join8(".cursor", "skills", slug, "SKILL.md");
4854
4800
  }
4855
- const writeSpinner = ora2("Writing config files...").start();
4856
- try {
4857
- const result = writeSetup(generatedSetup);
4858
- writeSpinner.succeed("Config files written");
4859
- console.log(chalk6.bold("\nFiles created/updated:"));
4860
- for (const file of result.written) {
4861
- console.log(` ${chalk6.green("\u2713")} ${file}`);
4862
- }
4863
- if (result.deleted.length > 0) {
4864
- console.log(chalk6.bold("\nFiles removed:"));
4865
- for (const file of result.deleted) {
4866
- console.log(` ${chalk6.red("\u2717")} ${file}`);
4867
- }
4868
- }
4869
- if (result.backupDir) {
4870
- console.log(chalk6.dim(`
4871
- Backups saved to ${result.backupDir}`));
4872
- }
4873
- } catch (err) {
4874
- writeSpinner.fail("Failed to write files");
4875
- console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
4876
- throw new Error("__exit__");
4801
+ if (platform === "codex") {
4802
+ return join8(".agents", "skills", slug, "SKILL.md");
4877
4803
  }
4878
- console.log(title.bold("\n Step 5/5 \u2014 Enhance with MCP servers\n"));
4879
- console.log(chalk6.dim(" MCP servers connect your AI agents to external tools and services"));
4880
- console.log(chalk6.dim(" like databases, APIs, and platforms your project depends on.\n"));
4881
- if (fingerprint.tools.length > 0) {
4804
+ return join8(".claude", "skills", slug, "SKILL.md");
4805
+ }
4806
+ function getInstalledSkills() {
4807
+ const installed = /* @__PURE__ */ new Set();
4808
+ const dirs = [
4809
+ join8(process.cwd(), ".claude", "skills"),
4810
+ join8(process.cwd(), ".cursor", "skills"),
4811
+ join8(process.cwd(), ".agents", "skills")
4812
+ ];
4813
+ for (const dir of dirs) {
4882
4814
  try {
4883
- const mcpResult = await discoverAndInstallMcps(targetAgent, fingerprint, process.cwd());
4884
- if (mcpResult.installed > 0) {
4885
- console.log(chalk6.bold(`
4886
- ${mcpResult.installed} MCP server${mcpResult.installed > 1 ? "s" : ""} configured`));
4887
- for (const name of mcpResult.names) {
4888
- console.log(` ${chalk6.green("\u2713")} ${name}`);
4815
+ const entries = readdirSync5(dir, { withFileTypes: true });
4816
+ for (const entry of entries) {
4817
+ if (entry.isDirectory()) {
4818
+ installed.add(entry.name.toLowerCase());
4889
4819
  }
4890
4820
  }
4891
- } catch (err) {
4892
- console.log(chalk6.dim(" MCP discovery skipped: " + (err instanceof Error ? err.message : "unknown error")));
4893
- }
4894
- } else {
4895
- console.log(chalk6.dim(" No external tools or services detected \u2014 skipping MCP discovery.\n"));
4896
- }
4897
- ensurePermissions();
4898
- const sha = getCurrentHeadSha();
4899
- writeState({
4900
- lastRefreshSha: sha ?? "",
4901
- lastRefreshTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
4902
- targetAgent
4903
- });
4904
- console.log("");
4905
- console.log(title.bold(" Keep your configs fresh\n"));
4906
- console.log(chalk6.dim(" Caliber can automatically update your agent configs when your code changes.\n"));
4907
- const hookChoice = await promptHookType(targetAgent);
4908
- if (hookChoice === "claude" || hookChoice === "both") {
4909
- const hookResult = installHook();
4910
- if (hookResult.installed) {
4911
- console.log(` ${chalk6.green("\u2713")} Claude Code hook installed \u2014 docs update on session end`);
4912
- console.log(chalk6.dim(" Run ") + chalk6.hex("#83D1EB")("caliber hooks --remove") + chalk6.dim(" to disable"));
4913
- } else if (hookResult.alreadyInstalled) {
4914
- console.log(chalk6.dim(" Claude Code hook already installed"));
4915
- }
4916
- const learnResult = installLearningHooks();
4917
- if (learnResult.installed) {
4918
- console.log(` ${chalk6.green("\u2713")} Learning hooks installed \u2014 session insights captured automatically`);
4919
- console.log(chalk6.dim(" Run ") + chalk6.hex("#83D1EB")("caliber learn remove") + chalk6.dim(" to disable"));
4920
- } else if (learnResult.alreadyInstalled) {
4921
- console.log(chalk6.dim(" Learning hooks already installed"));
4922
- }
4923
- }
4924
- if (hookChoice === "precommit" || hookChoice === "both") {
4925
- const precommitResult = installPreCommitHook();
4926
- if (precommitResult.installed) {
4927
- console.log(` ${chalk6.green("\u2713")} Pre-commit hook installed \u2014 docs refresh before each commit`);
4928
- console.log(chalk6.dim(" Run ") + chalk6.hex("#83D1EB")("caliber hooks --remove") + chalk6.dim(" to disable"));
4929
- } else if (precommitResult.alreadyInstalled) {
4930
- console.log(chalk6.dim(" Pre-commit hook already installed"));
4931
- } else {
4932
- console.log(chalk6.yellow(" Could not install pre-commit hook (not a git repository?)"));
4821
+ } catch {
4933
4822
  }
4934
4823
  }
4935
- if (hookChoice === "skip") {
4936
- console.log(chalk6.dim(" Skipped auto-refresh hooks. Run ") + chalk6.hex("#83D1EB")("caliber hooks --install") + chalk6.dim(" later to enable."));
4937
- }
4938
- const afterScore = computeLocalScore(process.cwd(), targetAgent);
4939
- if (afterScore.score < baselineScore.score) {
4940
- console.log("");
4941
- console.log(chalk6.yellow(` Score would drop from ${baselineScore.score} to ${afterScore.score} \u2014 reverting changes.`));
4824
+ return installed;
4825
+ }
4826
+ async function searchSkillsSh(technologies) {
4827
+ const bestBySlug = /* @__PURE__ */ new Map();
4828
+ for (const tech of technologies) {
4942
4829
  try {
4943
- const { restored, removed } = undoSetup();
4944
- if (restored.length > 0 || removed.length > 0) {
4945
- console.log(chalk6.dim(` Reverted ${restored.length + removed.length} file${restored.length + removed.length === 1 ? "" : "s"} from backup.`));
4830
+ const resp = await fetch(`https://skills.sh/api/search?q=${encodeURIComponent(tech)}&limit=10`, {
4831
+ signal: AbortSignal.timeout(1e4)
4832
+ });
4833
+ if (!resp.ok) continue;
4834
+ const data = await resp.json();
4835
+ if (!data.skills?.length) continue;
4836
+ for (const skill of data.skills) {
4837
+ const existing = bestBySlug.get(skill.skillId);
4838
+ if (existing && existing.installs >= (skill.installs ?? 0)) continue;
4839
+ bestBySlug.set(skill.skillId, {
4840
+ name: skill.name,
4841
+ slug: skill.skillId,
4842
+ source_url: skill.source ? `https://github.com/${skill.source}` : "",
4843
+ score: 0,
4844
+ reason: skill.description || "",
4845
+ detected_technology: tech,
4846
+ item_type: "skill",
4847
+ installs: skill.installs ?? 0
4848
+ });
4946
4849
  }
4947
4850
  } catch {
4851
+ continue;
4948
4852
  }
4949
- console.log(chalk6.dim(" Run ") + chalk6.hex("#83D1EB")("caliber onboard --force") + chalk6.dim(" to override.\n"));
4950
- return;
4951
4853
  }
4952
- displayScoreDelta(baselineScore, afterScore);
4953
- console.log(chalk6.bold.green(" Onboarding complete! Your project is ready for AI-assisted development."));
4954
- console.log(chalk6.dim(" Run ") + chalk6.hex("#83D1EB")("caliber undo") + chalk6.dim(" to revert changes.\n"));
4955
- console.log(chalk6.bold(" Next steps:\n"));
4956
- console.log(` ${title("caliber score")} See your full config breakdown`);
4957
- console.log(` ${title("caliber recommend")} Discover community skills for your stack`);
4958
- console.log(` ${title("caliber undo")} Revert all changes from this run`);
4959
- console.log("");
4854
+ return Array.from(bestBySlug.values());
4960
4855
  }
4961
- async function refineLoop(currentSetup, _targetAgent, sessionHistory) {
4962
- while (true) {
4963
- const message = await promptInput3("\nWhat would you like to change?");
4964
- if (!message || message.toLowerCase() === "done" || message.toLowerCase() === "accept") {
4965
- return currentSetup;
4966
- }
4967
- if (message.toLowerCase() === "cancel") {
4968
- return null;
4969
- }
4970
- const isValid = await classifyRefineIntent(message);
4971
- if (!isValid) {
4972
- console.log(chalk6.dim(" This doesn't look like a config change request."));
4973
- console.log(chalk6.dim(" Describe what to add, remove, or modify in your configs."));
4974
- console.log(chalk6.dim(' Type "done" to accept the current setup.\n'));
4975
- continue;
4976
- }
4977
- const refineSpinner = ora2("Refining setup...").start();
4978
- const refineMessages = new SpinnerMessages(refineSpinner, REFINE_MESSAGES);
4979
- refineMessages.start();
4980
- const refined = await refineSetup(
4981
- currentSetup,
4982
- message,
4983
- sessionHistory
4984
- );
4985
- refineMessages.stop();
4986
- if (refined) {
4987
- currentSetup = refined;
4988
- sessionHistory.push({ role: "user", content: message });
4989
- sessionHistory.push({
4990
- role: "assistant",
4991
- content: summarizeSetup("Applied changes", refined)
4856
+ async function searchTessl(technologies) {
4857
+ const results = [];
4858
+ const seen = /* @__PURE__ */ new Set();
4859
+ for (const tech of technologies) {
4860
+ try {
4861
+ const resp = await fetch(`https://tessl.io/registry?q=${encodeURIComponent(tech)}`, {
4862
+ signal: AbortSignal.timeout(1e4)
4992
4863
  });
4993
- refineSpinner.succeed("Setup updated");
4994
- printSetupSummary(refined);
4995
- console.log(chalk6.dim('Type "done" to accept, or describe more changes.'));
4996
- } else {
4997
- refineSpinner.fail("Refinement failed \u2014 could not parse AI response.");
4998
- console.log(chalk6.dim('Try rephrasing your request, or type "done" to keep the current setup.'));
4864
+ if (!resp.ok) continue;
4865
+ const html = await resp.text();
4866
+ const linkMatches = html.matchAll(/\/registry\/skills\/github\/([^/]+)\/([^/]+)\/([^/"]+)/g);
4867
+ for (const match of linkMatches) {
4868
+ const [, org, repo, skillName] = match;
4869
+ const slug = `${org}-${repo}-${skillName}`.toLowerCase();
4870
+ if (seen.has(slug)) continue;
4871
+ seen.add(slug);
4872
+ results.push({
4873
+ name: skillName,
4874
+ slug,
4875
+ source_url: `https://github.com/${org}/${repo}`,
4876
+ score: 0,
4877
+ reason: `Skill from ${org}/${repo}`,
4878
+ detected_technology: tech,
4879
+ item_type: "skill"
4880
+ });
4881
+ }
4882
+ } catch {
4883
+ continue;
4999
4884
  }
5000
4885
  }
4886
+ return results;
5001
4887
  }
5002
- function summarizeSetup(action, setup) {
5003
- const descriptions = setup.fileDescriptions;
5004
- const files = descriptions ? Object.entries(descriptions).map(([path24, desc]) => ` ${path24}: ${desc}`).join("\n") : Object.keys(setup).filter((k) => k !== "targetAgent" && k !== "fileDescriptions").join(", ");
5005
- return `${action}. Files:
5006
- ${files}`;
5007
- }
5008
- async function classifyRefineIntent(message) {
5009
- const fastModel = getFastModel();
4888
+ var AWESOME_CLAUDE_CODE_URL = "https://raw.githubusercontent.com/hesreallyhim/awesome-claude-code/main/README.md";
4889
+ async function searchAwesomeClaudeCode(technologies) {
5010
4890
  try {
5011
- const result = await llmJsonCall({
5012
- system: `You classify whether a user message is a valid request to modify AI agent config files (CLAUDE.md, .cursorrules, skills).
5013
- Valid: requests to add, remove, change, or restructure config content. Examples: "add testing commands", "remove the terraform section", "make CLAUDE.md shorter".
5014
- Invalid: questions, requests to show/display something, general chat, or anything that isn't a concrete config change.
5015
- Return {"valid": true} or {"valid": false}. Nothing else.`,
5016
- prompt: message,
5017
- maxTokens: 20,
5018
- ...fastModel ? { model: fastModel } : {}
4891
+ const resp = await fetch(AWESOME_CLAUDE_CODE_URL, {
4892
+ signal: AbortSignal.timeout(1e4)
4893
+ });
4894
+ if (!resp.ok) return [];
4895
+ const markdown = await resp.text();
4896
+ const items = [];
4897
+ const itemPattern = /^[-*]\s+\[([^\]]+)\]\(([^)]+)\)(?:\s+by\s+\[[^\]]*\]\([^)]*\))?\s*[-–—:]\s*(.*)/gm;
4898
+ let match;
4899
+ while ((match = itemPattern.exec(markdown)) !== null) {
4900
+ const [, name, url, description] = match;
4901
+ if (url.startsWith("#")) continue;
4902
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4903
+ items.push({
4904
+ name: name.trim(),
4905
+ slug,
4906
+ source_url: url.trim(),
4907
+ score: 0,
4908
+ reason: description.trim().slice(0, 150),
4909
+ detected_technology: "claude-code",
4910
+ item_type: "skill"
4911
+ });
4912
+ }
4913
+ const techLower = technologies.map((t) => t.toLowerCase());
4914
+ return items.filter((item) => {
4915
+ const text = `${item.name} ${item.reason}`.toLowerCase();
4916
+ return techLower.some((t) => text.includes(t));
5019
4917
  });
5020
- return result.valid === true;
5021
4918
  } catch {
5022
- return true;
4919
+ return [];
5023
4920
  }
5024
4921
  }
5025
- async function evaluateDismissals(failingChecks, fingerprint) {
5026
- const fastModel = getFastModel();
5027
- const checkList = failingChecks.map((c) => ({
5028
- id: c.id,
5029
- name: c.name,
5030
- suggestion: c.suggestion
5031
- }));
5032
- try {
5033
- const result = await llmJsonCall({
5034
- system: `You evaluate whether scoring checks are applicable to a project.
5035
- Given the project's languages/frameworks and a list of failing checks, return which checks are NOT applicable.
4922
+ async function searchAllProviders(technologies, platform) {
4923
+ const searches = [
4924
+ searchSkillsSh(technologies),
4925
+ searchTessl(technologies)
4926
+ ];
4927
+ if (platform === "claude" || !platform) {
4928
+ searches.push(searchAwesomeClaudeCode(technologies));
4929
+ }
4930
+ const results = await Promise.all(searches);
4931
+ const seen = /* @__PURE__ */ new Set();
4932
+ const combined = [];
4933
+ for (const batch of results) {
4934
+ for (const result of batch) {
4935
+ const key = result.name.toLowerCase().replace(/[-_]/g, "");
4936
+ if (seen.has(key)) continue;
4937
+ seen.add(key);
4938
+ combined.push(result);
4939
+ }
4940
+ }
4941
+ return combined;
4942
+ }
4943
+ async function scoreWithLLM2(candidates, projectContext, technologies) {
4944
+ const candidateList = candidates.map((c, i) => `${i}. "${c.name}" \u2014 ${c.reason || "no description"}`).join("\n");
4945
+ const scored = await llmJsonCall({
4946
+ system: `You evaluate whether AI agent skills and tools are relevant to a specific software project.
4947
+ Given a project context and a list of candidates, score each one's relevance from 0-100 and provide a brief reason (max 80 chars).
5036
4948
 
5037
- Only dismiss checks that truly don't apply \u2014 e.g. "Build/test/lint commands" for a pure Terraform/HCL repo with no build system.
5038
- Do NOT dismiss checks that could reasonably apply even if the project doesn't use them yet.
4949
+ Return a JSON array where each element has:
4950
+ - "index": the candidate's index number
4951
+ - "score": relevance score 0-100
4952
+ - "reason": one-liner explaining why it fits or doesn't
5039
4953
 
5040
- Return {"dismissed": [{"id": "check_id", "reason": "brief reason"}]} or {"dismissed": []} if all apply.`,
5041
- prompt: `Languages: ${fingerprint.languages.join(", ") || "none"}
5042
- Frameworks: ${fingerprint.frameworks.join(", ") || "none"}
4954
+ Scoring guidelines:
4955
+ - 90-100: Directly matches a core technology or workflow in the project
4956
+ - 70-89: Relevant to the project's stack, patterns, or development workflow
4957
+ - 50-69: Tangentially related or generic but useful
4958
+ - 0-49: Not relevant to this project
5043
4959
 
5044
- Failing checks:
5045
- ${JSON.stringify(checkList, null, 2)}`,
5046
- maxTokens: 200,
5047
- ...fastModel ? { model: fastModel } : {}
5048
- });
5049
- if (!Array.isArray(result.dismissed)) return [];
5050
- return result.dismissed.filter((d) => d.id && d.reason && failingChecks.some((c) => c.id === d.id)).map((d) => ({ id: d.id, reason: d.reason, dismissedAt: (/* @__PURE__ */ new Date()).toISOString() }));
5051
- } catch {
5052
- return [];
5053
- }
5054
- }
5055
- function promptInput3(question) {
5056
- const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
5057
- return new Promise((resolve2) => {
5058
- rl.question(chalk6.cyan(`${question} `), (answer) => {
5059
- rl.close();
5060
- resolve2(answer.trim());
5061
- });
5062
- });
5063
- }
5064
- async function promptAgent() {
5065
- const selected = await checkbox({
5066
- message: "Which coding agents do you use? (toggle with space)",
5067
- choices: [
5068
- { name: "Claude Code", value: "claude" },
5069
- { name: "Cursor", value: "cursor" },
5070
- { name: "Codex (OpenAI)", value: "codex" }
5071
- ],
5072
- validate: (items) => {
5073
- if (items.length === 0) return "At least one agent must be selected";
5074
- return true;
5075
- }
4960
+ Be selective. Prefer specific, high-quality matches over generic ones.
4961
+ A skill for "React testing" is only relevant if the project uses React.
4962
+ A generic "TypeScript best practices" skill is less valuable than one targeting the project's actual framework.
4963
+ Return ONLY the JSON array.`,
4964
+ prompt: `PROJECT CONTEXT:
4965
+ ${projectContext}
4966
+
4967
+ DETECTED TECHNOLOGIES:
4968
+ ${technologies.join(", ")}
4969
+
4970
+ CANDIDATES:
4971
+ ${candidateList}`,
4972
+ maxTokens: 8e3
5076
4973
  });
5077
- return selected;
4974
+ if (!Array.isArray(scored)) return [];
4975
+ return scored.filter((s) => s.score >= 60 && s.index >= 0 && s.index < candidates.length).sort((a, b) => b.score - a.score).slice(0, 20).map((s) => ({
4976
+ ...candidates[s.index],
4977
+ score: s.score,
4978
+ reason: s.reason || candidates[s.index].reason
4979
+ }));
5078
4980
  }
5079
- async function promptHookType(targetAgent) {
5080
- const choices = [];
5081
- const hasClaude = targetAgent.includes("claude");
5082
- if (hasClaude) {
5083
- choices.push({ name: "Claude Code hook (auto-refresh on session end)", value: "claude" });
4981
+ function buildProjectContext(fingerprint) {
4982
+ const parts = [];
4983
+ if (fingerprint.packageName) parts.push(`Package: ${fingerprint.packageName}`);
4984
+ if (fingerprint.languages.length > 0) parts.push(`Languages: ${fingerprint.languages.join(", ")}`);
4985
+ if (fingerprint.frameworks.length > 0) parts.push(`Frameworks: ${fingerprint.frameworks.join(", ")}`);
4986
+ if (fingerprint.description) parts.push(`Description: ${fingerprint.description}`);
4987
+ if (fingerprint.fileTree.length > 0) {
4988
+ parts.push(`
4989
+ File tree (${fingerprint.fileTree.length} files):
4990
+ ${fingerprint.fileTree.slice(0, 50).join("\n")}`);
5084
4991
  }
5085
- choices.push({ name: "Git pre-commit hook (refresh before each commit)", value: "precommit" });
5086
- if (hasClaude) {
5087
- choices.push({ name: "Both (Claude Code + pre-commit)", value: "both" });
4992
+ if (fingerprint.existingConfigs.claudeMd) {
4993
+ parts.push(`
4994
+ Existing CLAUDE.md (first 500 chars):
4995
+ ${fingerprint.existingConfigs.claudeMd.slice(0, 500)}`);
5088
4996
  }
5089
- choices.push({ name: "Skip for now", value: "skip" });
5090
- return select3({
5091
- message: "How would you like to auto-refresh your docs?",
5092
- choices
5093
- });
4997
+ const deps = extractTopDeps();
4998
+ if (deps.length > 0) {
4999
+ parts.push(`
5000
+ Dependencies: ${deps.slice(0, 30).join(", ")}`);
5001
+ }
5002
+ const installed = getInstalledSkills();
5003
+ if (installed.size > 0) {
5004
+ parts.push(`
5005
+ Already installed skills: ${Array.from(installed).join(", ")}`);
5006
+ }
5007
+ return parts.join("\n");
5094
5008
  }
5095
- async function promptReviewAction() {
5096
- return select3({
5097
- message: "What would you like to do?",
5009
+ function extractTopDeps() {
5010
+ const pkgPath = join8(process.cwd(), "package.json");
5011
+ if (!existsSync9(pkgPath)) return [];
5012
+ try {
5013
+ const pkg3 = JSON.parse(readFileSync7(pkgPath, "utf-8"));
5014
+ const deps = Object.keys(pkg3.dependencies ?? {});
5015
+ const trivial = /* @__PURE__ */ new Set([
5016
+ "typescript",
5017
+ "tslib",
5018
+ "ts-node",
5019
+ "tsx",
5020
+ "prettier",
5021
+ "eslint",
5022
+ "@eslint/js",
5023
+ "rimraf",
5024
+ "cross-env",
5025
+ "dotenv",
5026
+ "nodemon",
5027
+ "husky",
5028
+ "lint-staged",
5029
+ "commitlint",
5030
+ "chalk",
5031
+ "ora",
5032
+ "commander",
5033
+ "yargs",
5034
+ "meow",
5035
+ "inquirer",
5036
+ "@inquirer/confirm",
5037
+ "@inquirer/select",
5038
+ "@inquirer/prompts",
5039
+ "glob",
5040
+ "minimatch",
5041
+ "micromatch",
5042
+ "diff",
5043
+ "semver",
5044
+ "uuid",
5045
+ "nanoid",
5046
+ "debug",
5047
+ "ms",
5048
+ "lodash",
5049
+ "underscore",
5050
+ "tsup",
5051
+ "esbuild",
5052
+ "rollup",
5053
+ "webpack",
5054
+ "vite",
5055
+ "vitest",
5056
+ "jest",
5057
+ "mocha",
5058
+ "chai",
5059
+ "ava",
5060
+ "fs-extra",
5061
+ "mkdirp",
5062
+ "del",
5063
+ "rimraf",
5064
+ "path-to-regexp",
5065
+ "strip-ansi",
5066
+ "ansi-colors"
5067
+ ]);
5068
+ const trivialPatterns = [
5069
+ /^@types\//,
5070
+ /^@rely-ai\//,
5071
+ /^@caliber-ai\//,
5072
+ /^eslint-/,
5073
+ /^@eslint\//,
5074
+ /^prettier-/,
5075
+ /^@typescript-eslint\//,
5076
+ /^@commitlint\//
5077
+ ];
5078
+ return deps.filter(
5079
+ (d) => !trivial.has(d) && !trivialPatterns.some((p) => p.test(d))
5080
+ );
5081
+ } catch {
5082
+ return [];
5083
+ }
5084
+ }
5085
+ async function recommendCommand() {
5086
+ const proceed = await select3({
5087
+ message: "Search public repos for relevant skills to add to this project?",
5098
5088
  choices: [
5099
- { name: "Accept and apply", value: "accept" },
5100
- { name: "Refine via chat", value: "refine" },
5101
- { name: "Decline", value: "decline" }
5089
+ { name: "Yes, find skills for my project", value: true },
5090
+ { name: "No, cancel", value: false }
5102
5091
  ]
5103
5092
  });
5093
+ if (!proceed) {
5094
+ console.log(chalk6.dim(" Cancelled.\n"));
5095
+ return;
5096
+ }
5097
+ await searchAndInstallSkills();
5104
5098
  }
5105
- function printSetupSummary(setup) {
5106
- const claude = setup.claude;
5107
- const cursor = setup.cursor;
5108
- const fileDescriptions = setup.fileDescriptions;
5109
- const deletions = setup.deletions;
5110
- console.log("");
5111
- console.log(chalk6.bold(" Proposed changes:\n"));
5112
- const getDescription = (filePath) => {
5113
- return fileDescriptions?.[filePath];
5114
- };
5115
- if (claude) {
5116
- if (claude.claudeMd) {
5117
- const icon = fs21.existsSync("CLAUDE.md") ? chalk6.yellow("~") : chalk6.green("+");
5118
- const desc = getDescription("CLAUDE.md");
5119
- console.log(` ${icon} ${chalk6.bold("CLAUDE.md")}`);
5120
- if (desc) console.log(chalk6.dim(` ${desc}`));
5121
- console.log("");
5122
- }
5123
- const skills = claude.skills;
5124
- if (Array.isArray(skills) && skills.length > 0) {
5125
- for (const skill of skills) {
5126
- const skillPath = `.claude/skills/${skill.name}/SKILL.md`;
5127
- const icon = fs21.existsSync(skillPath) ? chalk6.yellow("~") : chalk6.green("+");
5128
- const desc = getDescription(skillPath);
5129
- console.log(` ${icon} ${chalk6.bold(skillPath)}`);
5130
- console.log(chalk6.dim(` ${desc || skill.description || skill.name}`));
5131
- console.log("");
5099
+ async function searchAndInstallSkills() {
5100
+ const fingerprint = await collectFingerprint(process.cwd());
5101
+ const platforms = detectLocalPlatforms();
5102
+ const installedSkills = getInstalledSkills();
5103
+ const technologies = [...new Set([
5104
+ ...fingerprint.languages,
5105
+ ...fingerprint.frameworks,
5106
+ ...extractTopDeps()
5107
+ ].filter(Boolean))];
5108
+ if (technologies.length === 0) {
5109
+ console.log(chalk6.yellow("Could not detect any languages or dependencies. Try running from a project root."));
5110
+ throw new Error("__exit__");
5111
+ }
5112
+ const primaryPlatform = platforms.includes("claude") ? "claude" : platforms[0];
5113
+ const searchSpinner = ora2("Searching skill registries...").start();
5114
+ const allCandidates = await searchAllProviders(technologies, primaryPlatform);
5115
+ if (!allCandidates.length) {
5116
+ searchSpinner.succeed("No skills found matching your tech stack.");
5117
+ return;
5118
+ }
5119
+ const newCandidates = allCandidates.filter((c) => !installedSkills.has(c.slug.toLowerCase()));
5120
+ const filteredCount = allCandidates.length - newCandidates.length;
5121
+ if (!newCandidates.length) {
5122
+ searchSpinner.succeed(`Found ${allCandidates.length} skills \u2014 all already installed.`);
5123
+ return;
5124
+ }
5125
+ searchSpinner.succeed(
5126
+ `Found ${allCandidates.length} skills` + (filteredCount > 0 ? chalk6.dim(` (${filteredCount} already installed)`) : "")
5127
+ );
5128
+ let results;
5129
+ const config = loadConfig();
5130
+ if (config) {
5131
+ const scoreSpinner = ora2("Scoring relevance for your project...").start();
5132
+ try {
5133
+ const projectContext = buildProjectContext(fingerprint);
5134
+ results = await scoreWithLLM2(newCandidates, projectContext, technologies);
5135
+ if (results.length === 0) {
5136
+ scoreSpinner.succeed("No highly relevant skills found for your specific project.");
5137
+ return;
5132
5138
  }
5139
+ scoreSpinner.succeed(`${results.length} relevant skill${results.length > 1 ? "s" : ""} for your project`);
5140
+ } catch {
5141
+ scoreSpinner.warn("Could not score relevance \u2014 showing top results");
5142
+ results = newCandidates.slice(0, 20);
5133
5143
  }
5144
+ } else {
5145
+ results = newCandidates.slice(0, 20);
5134
5146
  }
5135
- const codex = setup.codex;
5136
- if (codex) {
5137
- if (codex.agentsMd) {
5138
- const icon = fs21.existsSync("AGENTS.md") ? chalk6.yellow("~") : chalk6.green("+");
5139
- const desc = getDescription("AGENTS.md");
5140
- console.log(` ${icon} ${chalk6.bold("AGENTS.md")}`);
5141
- if (desc) console.log(chalk6.dim(` ${desc}`));
5142
- console.log("");
5147
+ const fetchSpinner = ora2("Verifying skill availability...").start();
5148
+ const contentMap = /* @__PURE__ */ new Map();
5149
+ await Promise.all(results.map(async (rec) => {
5150
+ const content = await fetchSkillContent(rec);
5151
+ if (content) contentMap.set(rec.slug, content);
5152
+ }));
5153
+ const available = results.filter((r) => contentMap.has(r.slug));
5154
+ if (!available.length) {
5155
+ fetchSpinner.fail("No installable skills found \u2014 content could not be fetched.");
5156
+ return;
5157
+ }
5158
+ const unavailableCount = results.length - available.length;
5159
+ fetchSpinner.succeed(
5160
+ `${available.length} installable skill${available.length > 1 ? "s" : ""}` + (unavailableCount > 0 ? chalk6.dim(` (${unavailableCount} unavailable)`) : "")
5161
+ );
5162
+ const selected = await interactiveSelect2(available);
5163
+ if (selected?.length) {
5164
+ await installSkills(selected, platforms, contentMap);
5165
+ }
5166
+ }
5167
+ async function interactiveSelect2(recs) {
5168
+ if (!process.stdin.isTTY) {
5169
+ printSkills(recs);
5170
+ return null;
5171
+ }
5172
+ const selected = /* @__PURE__ */ new Set();
5173
+ let cursor = 0;
5174
+ const { stdin, stdout } = process;
5175
+ let lineCount = 0;
5176
+ const hasScores = recs.some((r) => r.score > 0);
5177
+ function render() {
5178
+ const lines = [];
5179
+ lines.push(chalk6.bold(" Skills"));
5180
+ lines.push("");
5181
+ if (hasScores) {
5182
+ lines.push(` ${chalk6.dim("Score".padEnd(7))} ${chalk6.dim("Name".padEnd(28))} ${chalk6.dim("Why")}`);
5183
+ } else {
5184
+ lines.push(` ${chalk6.dim("Name".padEnd(30))} ${chalk6.dim("Technology".padEnd(18))} ${chalk6.dim("Source")}`);
5143
5185
  }
5144
- const codexSkills = codex.skills;
5145
- if (Array.isArray(codexSkills) && codexSkills.length > 0) {
5146
- for (const skill of codexSkills) {
5147
- const skillPath = `.agents/skills/${skill.name}/SKILL.md`;
5148
- const icon = fs21.existsSync(skillPath) ? chalk6.yellow("~") : chalk6.green("+");
5149
- const desc = getDescription(skillPath);
5150
- console.log(` ${icon} ${chalk6.bold(skillPath)}`);
5151
- console.log(chalk6.dim(` ${desc || skill.description || skill.name}`));
5152
- console.log("");
5186
+ lines.push(chalk6.dim(" " + "\u2500".repeat(70)));
5187
+ for (let i = 0; i < recs.length; i++) {
5188
+ const rec = recs[i];
5189
+ const check = selected.has(i) ? chalk6.green("[x]") : "[ ]";
5190
+ const ptr = i === cursor ? chalk6.cyan(">") : " ";
5191
+ if (hasScores) {
5192
+ const scoreColor = rec.score >= 90 ? chalk6.green : rec.score >= 70 ? chalk6.yellow : chalk6.dim;
5193
+ lines.push(` ${ptr} ${check} ${scoreColor(String(rec.score).padStart(3))} ${rec.name.padEnd(26)} ${chalk6.dim(rec.reason.slice(0, 40))}`);
5194
+ } else {
5195
+ lines.push(` ${ptr} ${check} ${rec.name.padEnd(28)} ${rec.detected_technology.padEnd(16)} ${chalk6.dim(rec.source_url || "")}`);
5153
5196
  }
5154
5197
  }
5198
+ lines.push("");
5199
+ lines.push(chalk6.dim(" \u2191\u2193 navigate \u23B5 toggle a all n none \u23CE install q cancel"));
5200
+ return lines.join("\n");
5155
5201
  }
5156
- if (cursor) {
5157
- if (cursor.cursorrules) {
5158
- const icon = fs21.existsSync(".cursorrules") ? chalk6.yellow("~") : chalk6.green("+");
5159
- const desc = getDescription(".cursorrules");
5160
- console.log(` ${icon} ${chalk6.bold(".cursorrules")}`);
5161
- if (desc) console.log(chalk6.dim(` ${desc}`));
5162
- console.log("");
5202
+ function draw(initial) {
5203
+ if (!initial && lineCount > 0) {
5204
+ stdout.write(`\x1B[${lineCount}A`);
5163
5205
  }
5164
- const cursorSkills = cursor.skills;
5165
- if (Array.isArray(cursorSkills) && cursorSkills.length > 0) {
5166
- for (const skill of cursorSkills) {
5167
- const skillPath = `.cursor/skills/${skill.name}/SKILL.md`;
5168
- const icon = fs21.existsSync(skillPath) ? chalk6.yellow("~") : chalk6.green("+");
5169
- const desc = getDescription(skillPath);
5170
- console.log(` ${icon} ${chalk6.bold(skillPath)}`);
5171
- console.log(chalk6.dim(` ${desc || skill.description || skill.name}`));
5172
- console.log("");
5206
+ stdout.write("\x1B[0J");
5207
+ const output = render();
5208
+ stdout.write(output + "\n");
5209
+ lineCount = output.split("\n").length;
5210
+ }
5211
+ return new Promise((resolve2) => {
5212
+ console.log("");
5213
+ draw(true);
5214
+ stdin.setRawMode(true);
5215
+ stdin.resume();
5216
+ stdin.setEncoding("utf8");
5217
+ function cleanup() {
5218
+ stdin.removeListener("data", onData);
5219
+ stdin.setRawMode(false);
5220
+ stdin.pause();
5221
+ }
5222
+ function onData(key) {
5223
+ switch (key) {
5224
+ case "\x1B[A":
5225
+ cursor = (cursor - 1 + recs.length) % recs.length;
5226
+ draw(false);
5227
+ break;
5228
+ case "\x1B[B":
5229
+ cursor = (cursor + 1) % recs.length;
5230
+ draw(false);
5231
+ break;
5232
+ case " ":
5233
+ selected.has(cursor) ? selected.delete(cursor) : selected.add(cursor);
5234
+ draw(false);
5235
+ break;
5236
+ case "a":
5237
+ recs.forEach((_, i) => selected.add(i));
5238
+ draw(false);
5239
+ break;
5240
+ case "n":
5241
+ selected.clear();
5242
+ draw(false);
5243
+ break;
5244
+ case "\r":
5245
+ case "\n":
5246
+ cleanup();
5247
+ if (selected.size === 0) {
5248
+ console.log(chalk6.dim("\n No skills selected.\n"));
5249
+ resolve2(null);
5250
+ } else {
5251
+ resolve2(Array.from(selected).sort().map((i) => recs[i]));
5252
+ }
5253
+ break;
5254
+ case "q":
5255
+ case "\x1B":
5256
+ case "":
5257
+ cleanup();
5258
+ console.log(chalk6.dim("\n Cancelled.\n"));
5259
+ resolve2(null);
5260
+ break;
5173
5261
  }
5174
5262
  }
5175
- const rules = cursor.rules;
5176
- if (Array.isArray(rules) && rules.length > 0) {
5177
- for (const rule of rules) {
5178
- const rulePath = `.cursor/rules/${rule.filename}`;
5179
- const icon = fs21.existsSync(rulePath) ? chalk6.yellow("~") : chalk6.green("+");
5180
- const desc = getDescription(rulePath);
5181
- console.log(` ${icon} ${chalk6.bold(rulePath)}`);
5182
- if (desc) {
5183
- console.log(chalk6.dim(` ${desc}`));
5184
- } else {
5185
- const firstLine = rule.content.split("\n").filter((l) => l.trim() && !l.trim().startsWith("#"))[0];
5186
- if (firstLine) console.log(chalk6.dim(` ${firstLine.trim().slice(0, 80)}`));
5187
- }
5188
- console.log("");
5263
+ stdin.on("data", onData);
5264
+ });
5265
+ }
5266
+ async function fetchSkillContent(rec) {
5267
+ if (!rec.source_url) return null;
5268
+ const repoPath = rec.source_url.replace("https://github.com/", "");
5269
+ const candidates = [
5270
+ `https://raw.githubusercontent.com/${repoPath}/HEAD/skills/${rec.slug}/SKILL.md`,
5271
+ `https://raw.githubusercontent.com/${repoPath}/HEAD/${rec.slug}/SKILL.md`,
5272
+ `https://raw.githubusercontent.com/${repoPath}/HEAD/.claude/skills/${rec.slug}/SKILL.md`,
5273
+ `https://raw.githubusercontent.com/${repoPath}/HEAD/.agents/skills/${rec.slug}/SKILL.md`
5274
+ ];
5275
+ for (const url of candidates) {
5276
+ try {
5277
+ const resp = await fetch(url, { signal: AbortSignal.timeout(1e4) });
5278
+ if (resp.ok) {
5279
+ const text = await resp.text();
5280
+ if (text.length > 20) return text;
5189
5281
  }
5282
+ } catch {
5190
5283
  }
5191
5284
  }
5192
- if (!codex && !fs21.existsSync("AGENTS.md")) {
5193
- console.log(` ${chalk6.green("+")} ${chalk6.bold("AGENTS.md")}`);
5194
- console.log(chalk6.dim(" Cross-agent coordination file"));
5195
- console.log("");
5196
- }
5197
- if (Array.isArray(deletions) && deletions.length > 0) {
5198
- for (const del of deletions) {
5199
- console.log(` ${chalk6.red("-")} ${chalk6.bold(del.filePath)}`);
5200
- console.log(chalk6.dim(` ${del.reason}`));
5201
- console.log("");
5202
- }
5203
- }
5204
- console.log(` ${chalk6.green("+")} ${chalk6.dim("new")} ${chalk6.yellow("~")} ${chalk6.dim("modified")} ${chalk6.red("-")} ${chalk6.dim("removed")}`);
5205
- console.log("");
5206
- }
5207
- function ensurePermissions() {
5208
- const settingsPath = ".claude/settings.json";
5209
- let settings = {};
5210
5285
  try {
5211
- if (fs21.existsSync(settingsPath)) {
5212
- settings = JSON.parse(fs21.readFileSync(settingsPath, "utf-8"));
5286
+ const resp = await fetch(
5287
+ `https://api.github.com/repos/${repoPath}/git/trees/HEAD?recursive=1`,
5288
+ { signal: AbortSignal.timeout(1e4) }
5289
+ );
5290
+ if (resp.ok) {
5291
+ const tree = await resp.json();
5292
+ const needle = `${rec.slug}/SKILL.md`;
5293
+ const match = tree.tree?.find((f) => f.path.endsWith(needle));
5294
+ if (match) {
5295
+ const rawUrl = `https://raw.githubusercontent.com/${repoPath}/HEAD/${match.path}`;
5296
+ const contentResp = await fetch(rawUrl, { signal: AbortSignal.timeout(1e4) });
5297
+ if (contentResp.ok) return await contentResp.text();
5298
+ }
5213
5299
  }
5214
5300
  } catch {
5215
5301
  }
5216
- const permissions = settings.permissions ?? {};
5217
- const allow = permissions.allow;
5218
- if (Array.isArray(allow) && allow.length > 0) return;
5219
- permissions.allow = [
5220
- "Bash(npm run *)",
5221
- "Bash(npx vitest *)",
5222
- "Bash(npx tsc *)",
5223
- "Bash(git *)"
5224
- ];
5225
- settings.permissions = permissions;
5226
- if (!fs21.existsSync(".claude")) fs21.mkdirSync(".claude", { recursive: true });
5227
- fs21.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5302
+ return null;
5228
5303
  }
5229
-
5230
- // src/commands/undo.ts
5231
- import chalk7 from "chalk";
5232
- import ora3 from "ora";
5233
- function undoCommand() {
5234
- const spinner = ora3("Reverting setup...").start();
5235
- try {
5236
- const { restored, removed } = undoSetup();
5237
- if (restored.length === 0 && removed.length === 0) {
5238
- spinner.info("Nothing to undo.");
5239
- return;
5240
- }
5241
- spinner.succeed("Setup reverted successfully.\n");
5242
- if (restored.length > 0) {
5243
- console.log(chalk7.cyan(" Restored from backup:"));
5244
- for (const file of restored) {
5245
- console.log(` ${chalk7.green("\u21A9")} ${file}`);
5246
- }
5304
+ async function installSkills(recs, platforms, contentMap) {
5305
+ const spinner = ora2(`Installing ${recs.length} skill${recs.length > 1 ? "s" : ""}...`).start();
5306
+ const installed = [];
5307
+ for (const rec of recs) {
5308
+ const content = contentMap.get(rec.slug);
5309
+ if (!content) continue;
5310
+ for (const platform of platforms) {
5311
+ const skillPath = getSkillPath(platform, rec.slug);
5312
+ const fullPath = join8(process.cwd(), skillPath);
5313
+ mkdirSync(dirname2(fullPath), { recursive: true });
5314
+ writeFileSync(fullPath, content, "utf-8");
5315
+ installed.push(`[${platform}] ${skillPath}`);
5247
5316
  }
5248
- if (removed.length > 0) {
5249
- console.log(chalk7.cyan(" Removed:"));
5250
- for (const file of removed) {
5251
- console.log(` ${chalk7.red("\u2717")} ${file}`);
5252
- }
5317
+ }
5318
+ if (installed.length > 0) {
5319
+ spinner.succeed(`Installed ${installed.length} file${installed.length > 1 ? "s" : ""}`);
5320
+ for (const p of installed) {
5321
+ console.log(chalk6.green(` \u2713 ${p}`));
5253
5322
  }
5254
- console.log("");
5255
- } catch (err) {
5256
- spinner.fail(chalk7.red(err instanceof Error ? err.message : "Undo failed"));
5257
- throw new Error("__exit__");
5323
+ } else {
5324
+ spinner.fail("No skills were installed");
5258
5325
  }
5326
+ console.log("");
5259
5327
  }
5260
-
5261
- // src/commands/status.ts
5262
- import chalk8 from "chalk";
5263
- import fs22 from "fs";
5264
- async function statusCommand(options) {
5265
- const config = loadConfig();
5266
- const manifest = readManifest();
5267
- if (options.json) {
5268
- console.log(JSON.stringify({
5269
- configured: !!config,
5270
- provider: config?.provider,
5271
- model: config?.model,
5272
- manifest
5273
- }, null, 2));
5274
- return;
5275
- }
5276
- console.log(chalk8.bold("\nCaliber Status\n"));
5277
- if (config) {
5278
- console.log(` LLM: ${chalk8.green(config.provider)} (${config.model})`);
5328
+ function printSkills(recs) {
5329
+ const hasScores = recs.some((r) => r.score > 0);
5330
+ console.log(chalk6.bold("\n Skills\n"));
5331
+ if (hasScores) {
5332
+ console.log(` ${chalk6.dim("Score".padEnd(7))} ${chalk6.dim("Name".padEnd(28))} ${chalk6.dim("Why")}`);
5279
5333
  } else {
5280
- console.log(` LLM: ${chalk8.yellow("Not configured")} \u2014 run ${chalk8.hex("#83D1EB")("caliber config")}`);
5281
- }
5282
- if (!manifest) {
5283
- console.log(` Setup: ${chalk8.dim("No setup applied")}`);
5284
- console.log(chalk8.dim("\n Run ") + chalk8.hex("#83D1EB")("caliber onboard") + chalk8.dim(" to get started.\n"));
5285
- return;
5334
+ console.log(` ${chalk6.dim("Name".padEnd(30))} ${chalk6.dim("Technology".padEnd(18))} ${chalk6.dim("Source")}`);
5286
5335
  }
5287
- console.log(` Files managed: ${chalk8.cyan(manifest.entries.length.toString())}`);
5288
- for (const entry of manifest.entries) {
5289
- const exists = fs22.existsSync(entry.path);
5290
- const icon = exists ? chalk8.green("\u2713") : chalk8.red("\u2717");
5291
- console.log(` ${icon} ${entry.path} (${entry.action})`);
5336
+ console.log(chalk6.dim(" " + "\u2500".repeat(70)));
5337
+ for (const rec of recs) {
5338
+ if (hasScores) {
5339
+ console.log(` ${String(rec.score).padStart(3)} ${rec.name.padEnd(26)} ${chalk6.dim(rec.reason.slice(0, 50))}`);
5340
+ } else {
5341
+ console.log(` ${rec.name.padEnd(28)} ${rec.detected_technology.padEnd(16)} ${chalk6.dim(rec.source_url || "")}`);
5342
+ }
5292
5343
  }
5293
5344
  console.log("");
5294
5345
  }
5295
5346
 
5296
- // src/commands/regenerate.ts
5297
- import chalk9 from "chalk";
5298
- import ora4 from "ora";
5299
- import select4 from "@inquirer/select";
5300
- async function regenerateCommand(options) {
5301
- const config = loadConfig();
5347
+ // src/commands/onboard.ts
5348
+ async function initCommand(options) {
5349
+ const brand = chalk7.hex("#EB9D83");
5350
+ const title = chalk7.hex("#83D1EB");
5351
+ console.log(brand.bold(`
5352
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557
5353
+ \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
5354
+ \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
5355
+ \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
5356
+ \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551
5357
+ \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D
5358
+ `));
5359
+ console.log(chalk7.dim(" Onboard your project for AI-assisted development\n"));
5360
+ console.log(title.bold(" Welcome to Caliber\n"));
5361
+ console.log(chalk7.dim(" Caliber analyzes your codebase and creates tailored config files"));
5362
+ console.log(chalk7.dim(" so your AI coding agents understand your project from day one.\n"));
5363
+ console.log(title.bold(" How onboarding works:\n"));
5364
+ console.log(chalk7.dim(" 1. Connect Set up your LLM provider"));
5365
+ console.log(chalk7.dim(" 2. Discover Analyze your code, dependencies, and structure"));
5366
+ console.log(chalk7.dim(" 3. Generate Create config files tailored to your project"));
5367
+ console.log(chalk7.dim(" 4. Review Preview, refine, and apply the changes"));
5368
+ console.log(chalk7.dim(" 5. Enhance Discover MCP servers for your tools"));
5369
+ console.log(chalk7.dim(" 6. Skills Browse community skills for your stack\n"));
5370
+ console.log(title.bold(" Step 1/6 \u2014 Connect your LLM\n"));
5371
+ let config = loadConfig();
5302
5372
  if (!config) {
5303
- console.log(chalk9.red("No LLM provider configured. Run ") + chalk9.hex("#83D1EB")("caliber config") + chalk9.red(" first."));
5304
- throw new Error("__exit__");
5305
- }
5306
- const manifest = readManifest();
5307
- if (!manifest) {
5308
- console.log(chalk9.yellow("No existing setup found. Run ") + chalk9.hex("#83D1EB")("caliber onboard") + chalk9.yellow(" first."));
5309
- throw new Error("__exit__");
5373
+ console.log(chalk7.dim(" No LLM provider set yet. Choose how to run Caliber:\n"));
5374
+ try {
5375
+ await runInteractiveProviderSetup({
5376
+ selectMessage: "How do you want to use Caliber? (choose LLM provider)"
5377
+ });
5378
+ } catch (err) {
5379
+ if (err.message === "__exit__") throw err;
5380
+ throw err;
5381
+ }
5382
+ config = loadConfig();
5383
+ if (!config) {
5384
+ console.log(chalk7.red(" Setup was cancelled or failed.\n"));
5385
+ throw new Error("__exit__");
5386
+ }
5387
+ console.log(chalk7.green(" \u2713 Provider saved. Let's continue.\n"));
5310
5388
  }
5311
- const targetAgent = readState()?.targetAgent ?? ["claude", "cursor"];
5312
- const spinner = ora4("Analyzing project...").start();
5313
- const fingerprint = collectFingerprint(process.cwd());
5314
- await enrichFingerprintWithLLM(fingerprint, process.cwd());
5389
+ const displayModel = config.model === "default" && config.provider === "claude-cli" ? process.env.ANTHROPIC_MODEL || "default (inherited from Claude Code)" : config.model;
5390
+ const fastModel = getFastModel();
5391
+ const modelLine = fastModel ? ` Provider: ${config.provider} | Model: ${displayModel} | Scan: ${fastModel}` : ` Provider: ${config.provider} | Model: ${displayModel}`;
5392
+ console.log(chalk7.dim(modelLine + "\n"));
5393
+ console.log(title.bold(" Step 2/6 \u2014 Discover your project\n"));
5394
+ console.log(chalk7.dim(" Learning about your languages, dependencies, structure, and existing configs.\n"));
5395
+ const spinner = ora3("Analyzing project...").start();
5396
+ const fingerprint = await collectFingerprint(process.cwd());
5315
5397
  spinner.succeed("Project analyzed");
5398
+ console.log(chalk7.dim(` Languages: ${fingerprint.languages.join(", ") || "none detected"}`));
5399
+ console.log(chalk7.dim(` Files: ${fingerprint.fileTree.length} found
5400
+ `));
5401
+ const targetAgent = options.agent || await promptAgent();
5402
+ const preScore = computeLocalScore(process.cwd(), targetAgent);
5403
+ const failingForDismissal = preScore.checks.filter((c) => !c.passed && c.maxPoints > 0);
5404
+ if (failingForDismissal.length > 0) {
5405
+ const newDismissals = await evaluateDismissals(failingForDismissal, fingerprint);
5406
+ if (newDismissals.length > 0) {
5407
+ const existing = readDismissedChecks();
5408
+ const existingIds = new Set(existing.map((d) => d.id));
5409
+ const merged = [...existing, ...newDismissals.filter((d) => !existingIds.has(d.id))];
5410
+ writeDismissedChecks(merged);
5411
+ }
5412
+ }
5316
5413
  const baselineScore = computeLocalScore(process.cwd(), targetAgent);
5317
5414
  displayScoreSummary(baselineScore);
5318
- if (baselineScore.score === 100) {
5319
- console.log(chalk9.green(" Your setup is already at 100/100 \u2014 nothing to regenerate.\n"));
5415
+ const hasExistingConfig = !!(fingerprint.existingConfigs.claudeMd || fingerprint.existingConfigs.claudeSettings || fingerprint.existingConfigs.claudeSkills?.length || fingerprint.existingConfigs.cursorrules || fingerprint.existingConfigs.cursorRules?.length || fingerprint.existingConfigs.agentsMd);
5416
+ const NON_LLM_CHECKS = /* @__PURE__ */ new Set(["hooks_configured", "agents_md_exists", "permissions_configured", "mcp_servers"]);
5417
+ if (hasExistingConfig && baselineScore.score === 100) {
5418
+ console.log(chalk7.bold.green(" Your setup is already optimal \u2014 nothing to change.\n"));
5419
+ console.log(chalk7.dim(" Run ") + chalk7.hex("#83D1EB")("caliber onboard --force") + chalk7.dim(" to regenerate anyway.\n"));
5420
+ if (!options.force) return;
5421
+ }
5422
+ const allFailingChecks = baselineScore.checks.filter((c) => !c.passed && c.maxPoints > 0);
5423
+ const llmFixableChecks = allFailingChecks.filter((c) => !NON_LLM_CHECKS.has(c.id));
5424
+ if (hasExistingConfig && llmFixableChecks.length === 0 && allFailingChecks.length > 0 && !options.force) {
5425
+ console.log(chalk7.bold.green("\n Your config is fully optimized for LLM generation.\n"));
5426
+ console.log(chalk7.dim(" Remaining items need CLI actions:\n"));
5427
+ for (const check of allFailingChecks) {
5428
+ console.log(chalk7.dim(` \u2022 ${check.name}`));
5429
+ if (check.suggestion) {
5430
+ console.log(` ${chalk7.hex("#83D1EB")(check.suggestion)}`);
5431
+ }
5432
+ }
5433
+ console.log("");
5434
+ console.log(chalk7.dim(" Run ") + chalk7.hex("#83D1EB")("caliber onboard --force") + chalk7.dim(" to regenerate anyway.\n"));
5320
5435
  return;
5321
5436
  }
5322
- const genSpinner = ora4("Regenerating setup...").start();
5437
+ const isEmpty = fingerprint.fileTree.length < 3;
5438
+ if (isEmpty) {
5439
+ fingerprint.description = await promptInput3("What will you build in this project?");
5440
+ }
5441
+ let failingChecks;
5442
+ let passingChecks;
5443
+ let currentScore;
5444
+ if (hasExistingConfig && baselineScore.score >= 95 && !options.force) {
5445
+ failingChecks = llmFixableChecks.map((c) => ({ name: c.name, suggestion: c.suggestion }));
5446
+ passingChecks = baselineScore.checks.filter((c) => c.passed).map((c) => ({ name: c.name }));
5447
+ currentScore = baselineScore.score;
5448
+ if (failingChecks.length > 0) {
5449
+ console.log(title.bold(" Step 3/6 \u2014 Fine-tuning\n"));
5450
+ console.log(chalk7.dim(` Your setup scores ${baselineScore.score}/100 \u2014 fixing ${failingChecks.length} remaining issue${failingChecks.length === 1 ? "" : "s"}:
5451
+ `));
5452
+ for (const check of failingChecks) {
5453
+ console.log(chalk7.dim(` \u2022 ${check.name}`));
5454
+ }
5455
+ console.log("");
5456
+ }
5457
+ } else if (hasExistingConfig) {
5458
+ console.log(title.bold(" Step 3/6 \u2014 Improve your setup\n"));
5459
+ console.log(chalk7.dim(" Reviewing your existing configs against your codebase"));
5460
+ console.log(chalk7.dim(" and preparing improvements.\n"));
5461
+ } else {
5462
+ console.log(title.bold(" Step 3/6 \u2014 Build your agent setup\n"));
5463
+ console.log(chalk7.dim(" Creating config files tailored to your project.\n"));
5464
+ }
5465
+ console.log(chalk7.dim(" This can take a couple of minutes depending on your model and provider.\n"));
5466
+ const genStartTime = Date.now();
5467
+ const genSpinner = ora3("Generating setup...").start();
5323
5468
  const genMessages = new SpinnerMessages(genSpinner, GENERATION_MESSAGES, { showElapsedTime: true });
5324
5469
  genMessages.start();
5325
5470
  let generatedSetup = null;
5471
+ let rawOutput;
5326
5472
  try {
5327
5473
  const result = await generateSetup(
5328
5474
  fingerprint,
5329
5475
  targetAgent,
5330
- void 0,
5476
+ fingerprint.description,
5331
5477
  {
5332
5478
  onStatus: (status) => {
5333
5479
  genMessages.handleServerStatus(status);
@@ -5339,797 +5485,687 @@ async function regenerateCommand(options) {
5339
5485
  genMessages.stop();
5340
5486
  genSpinner.fail(`Generation error: ${error}`);
5341
5487
  }
5342
- }
5488
+ },
5489
+ failingChecks,
5490
+ currentScore,
5491
+ passingChecks
5343
5492
  );
5344
- if (!generatedSetup) generatedSetup = result.setup;
5493
+ if (!generatedSetup) {
5494
+ generatedSetup = result.setup;
5495
+ rawOutput = result.raw;
5496
+ }
5345
5497
  } catch (err) {
5346
5498
  genMessages.stop();
5347
5499
  const msg = err instanceof Error ? err.message : "Unknown error";
5348
- genSpinner.fail(`Regeneration failed: ${msg}`);
5500
+ genSpinner.fail(`Generation failed: ${msg}`);
5349
5501
  throw new Error("__exit__");
5350
5502
  }
5351
5503
  genMessages.stop();
5352
5504
  if (!generatedSetup) {
5353
- genSpinner.fail("Failed to regenerate setup.");
5505
+ genSpinner.fail("Failed to generate setup.");
5506
+ if (rawOutput) {
5507
+ console.log(chalk7.dim("\nRaw LLM output (JSON parse failed):"));
5508
+ console.log(chalk7.dim(rawOutput.slice(0, 500)));
5509
+ }
5354
5510
  throw new Error("__exit__");
5355
5511
  }
5356
- genSpinner.succeed("Setup regenerated");
5512
+ const elapsedMs = Date.now() - genStartTime;
5513
+ const mins = Math.floor(elapsedMs / 6e4);
5514
+ const secs = Math.floor(elapsedMs % 6e4 / 1e3);
5515
+ const timeStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
5516
+ genSpinner.succeed(`Setup generated ${chalk7.dim(`in ${timeStr}`)}`);
5517
+ printSetupSummary(generatedSetup);
5518
+ const sessionHistory = [];
5519
+ sessionHistory.push({
5520
+ role: "assistant",
5521
+ content: summarizeSetup("Initial generation", generatedSetup)
5522
+ });
5523
+ console.log(title.bold(" Step 4/6 \u2014 Review and apply\n"));
5357
5524
  const setupFiles = collectSetupFiles(generatedSetup);
5358
5525
  const staged = stageFiles(setupFiles, process.cwd());
5359
5526
  const totalChanges = staged.newFiles + staged.modifiedFiles;
5360
- console.log(chalk9.dim(`
5361
- ${chalk9.green(`${staged.newFiles} new`)} / ${chalk9.yellow(`${staged.modifiedFiles} modified`)} file${totalChanges !== 1 ? "s" : ""}
5527
+ console.log(chalk7.dim(` ${chalk7.green(`${staged.newFiles} new`)} / ${chalk7.yellow(`${staged.modifiedFiles} modified`)} file${totalChanges !== 1 ? "s" : ""}
5362
5528
  `));
5529
+ let action;
5363
5530
  if (totalChanges === 0) {
5364
- console.log(chalk9.dim(" No changes needed \u2014 your configs are already up to date.\n"));
5531
+ console.log(chalk7.dim(" No changes needed \u2014 your configs are already up to date.\n"));
5365
5532
  cleanupStaging();
5366
- return;
5367
- }
5368
- if (options.dryRun) {
5369
- console.log(chalk9.yellow("[Dry run] Would write:"));
5370
- for (const f of staged.stagedFiles) {
5371
- console.log(` ${f.isNew ? chalk9.green("+") : chalk9.yellow("~")} ${f.relativePath}`);
5533
+ action = "accept";
5534
+ } else {
5535
+ const wantsReview = await promptWantsReview();
5536
+ if (wantsReview) {
5537
+ const reviewMethod = await promptReviewMethod();
5538
+ await openReview(reviewMethod, staged.stagedFiles);
5372
5539
  }
5373
- cleanupStaging();
5374
- return;
5540
+ action = await promptReviewAction();
5375
5541
  }
5376
- const wantsReview = await promptWantsReview();
5377
- if (wantsReview) {
5378
- const reviewMethod = await promptReviewMethod();
5379
- await openReview(reviewMethod, staged.stagedFiles);
5542
+ while (action === "refine") {
5543
+ generatedSetup = await refineLoop(generatedSetup, targetAgent, sessionHistory);
5544
+ if (!generatedSetup) {
5545
+ cleanupStaging();
5546
+ console.log(chalk7.dim("Refinement cancelled. No files were modified."));
5547
+ return;
5548
+ }
5549
+ const updatedFiles = collectSetupFiles(generatedSetup);
5550
+ const restaged = stageFiles(updatedFiles, process.cwd());
5551
+ console.log(chalk7.dim(` ${chalk7.green(`${restaged.newFiles} new`)} / ${chalk7.yellow(`${restaged.modifiedFiles} modified`)} file${restaged.newFiles + restaged.modifiedFiles !== 1 ? "s" : ""}
5552
+ `));
5553
+ printSetupSummary(generatedSetup);
5554
+ await openReview("terminal", restaged.stagedFiles);
5555
+ action = await promptReviewAction();
5380
5556
  }
5381
- const action = await select4({
5382
- message: "Apply regenerated setup?",
5383
- choices: [
5384
- { name: "Accept and apply", value: "accept" },
5385
- { name: "Decline", value: "decline" }
5386
- ]
5387
- });
5388
5557
  cleanupStaging();
5389
5558
  if (action === "decline") {
5390
- console.log(chalk9.dim("Regeneration cancelled. No files were modified."));
5559
+ console.log(chalk7.dim("Setup declined. No files were modified."));
5391
5560
  return;
5392
5561
  }
5393
- const writeSpinner = ora4("Writing config files...").start();
5562
+ if (options.dryRun) {
5563
+ console.log(chalk7.yellow("\n[Dry run] Would write the following files:"));
5564
+ console.log(JSON.stringify(generatedSetup, null, 2));
5565
+ return;
5566
+ }
5567
+ const writeSpinner = ora3("Writing config files...").start();
5394
5568
  try {
5395
5569
  const result = writeSetup(generatedSetup);
5396
5570
  writeSpinner.succeed("Config files written");
5571
+ console.log(chalk7.bold("\nFiles created/updated:"));
5397
5572
  for (const file of result.written) {
5398
- console.log(` ${chalk9.green("\u2713")} ${file}`);
5573
+ console.log(` ${chalk7.green("\u2713")} ${file}`);
5399
5574
  }
5400
5575
  if (result.deleted.length > 0) {
5576
+ console.log(chalk7.bold("\nFiles removed:"));
5401
5577
  for (const file of result.deleted) {
5402
- console.log(` ${chalk9.red("\u2717")} ${file}`);
5578
+ console.log(` ${chalk7.red("\u2717")} ${file}`);
5403
5579
  }
5404
5580
  }
5405
5581
  if (result.backupDir) {
5406
- console.log(chalk9.dim(`
5582
+ console.log(chalk7.dim(`
5407
5583
  Backups saved to ${result.backupDir}`));
5408
5584
  }
5409
5585
  } catch (err) {
5410
5586
  writeSpinner.fail("Failed to write files");
5411
- console.error(chalk9.red(err instanceof Error ? err.message : "Unknown error"));
5587
+ console.error(chalk7.red(err instanceof Error ? err.message : "Unknown error"));
5412
5588
  throw new Error("__exit__");
5413
5589
  }
5590
+ console.log(title.bold("\n Step 5/6 \u2014 Enhance with MCP servers\n"));
5591
+ console.log(chalk7.dim(" MCP servers connect your AI agents to external tools and services"));
5592
+ console.log(chalk7.dim(" like databases, APIs, and platforms your project depends on.\n"));
5593
+ if (fingerprint.tools.length > 0) {
5594
+ try {
5595
+ const mcpResult = await discoverAndInstallMcps(targetAgent, fingerprint, process.cwd());
5596
+ if (mcpResult.installed > 0) {
5597
+ console.log(chalk7.bold(`
5598
+ ${mcpResult.installed} MCP server${mcpResult.installed > 1 ? "s" : ""} configured`));
5599
+ for (const name of mcpResult.names) {
5600
+ console.log(` ${chalk7.green("\u2713")} ${name}`);
5601
+ }
5602
+ }
5603
+ } catch (err) {
5604
+ console.log(chalk7.dim(" MCP discovery skipped: " + (err instanceof Error ? err.message : "unknown error")));
5605
+ }
5606
+ } else {
5607
+ console.log(chalk7.dim(" No external tools or services detected \u2014 skipping MCP discovery.\n"));
5608
+ }
5609
+ ensurePermissions();
5414
5610
  const sha = getCurrentHeadSha();
5415
5611
  writeState({
5416
5612
  lastRefreshSha: sha ?? "",
5417
5613
  lastRefreshTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
5418
5614
  targetAgent
5419
5615
  });
5616
+ console.log("");
5617
+ console.log(title.bold(" Keep your configs fresh\n"));
5618
+ console.log(chalk7.dim(" Caliber can automatically update your agent configs when your code changes.\n"));
5619
+ const hookChoice = await promptHookType(targetAgent);
5620
+ if (hookChoice === "claude" || hookChoice === "both") {
5621
+ const hookResult = installHook();
5622
+ if (hookResult.installed) {
5623
+ console.log(` ${chalk7.green("\u2713")} Claude Code hook installed \u2014 docs update on session end`);
5624
+ console.log(chalk7.dim(" Run ") + chalk7.hex("#83D1EB")("caliber hooks --remove") + chalk7.dim(" to disable"));
5625
+ } else if (hookResult.alreadyInstalled) {
5626
+ console.log(chalk7.dim(" Claude Code hook already installed"));
5627
+ }
5628
+ const learnResult = installLearningHooks();
5629
+ if (learnResult.installed) {
5630
+ console.log(` ${chalk7.green("\u2713")} Learning hooks installed \u2014 session insights captured automatically`);
5631
+ console.log(chalk7.dim(" Run ") + chalk7.hex("#83D1EB")("caliber learn remove") + chalk7.dim(" to disable"));
5632
+ } else if (learnResult.alreadyInstalled) {
5633
+ console.log(chalk7.dim(" Learning hooks already installed"));
5634
+ }
5635
+ }
5636
+ if (hookChoice === "precommit" || hookChoice === "both") {
5637
+ const precommitResult = installPreCommitHook();
5638
+ if (precommitResult.installed) {
5639
+ console.log(` ${chalk7.green("\u2713")} Pre-commit hook installed \u2014 docs refresh before each commit`);
5640
+ console.log(chalk7.dim(" Run ") + chalk7.hex("#83D1EB")("caliber hooks --remove") + chalk7.dim(" to disable"));
5641
+ } else if (precommitResult.alreadyInstalled) {
5642
+ console.log(chalk7.dim(" Pre-commit hook already installed"));
5643
+ } else {
5644
+ console.log(chalk7.yellow(" Could not install pre-commit hook (not a git repository?)"));
5645
+ }
5646
+ }
5647
+ if (hookChoice === "skip") {
5648
+ console.log(chalk7.dim(" Skipped auto-refresh hooks. Run ") + chalk7.hex("#83D1EB")("caliber hooks --install") + chalk7.dim(" later to enable."));
5649
+ }
5420
5650
  const afterScore = computeLocalScore(process.cwd(), targetAgent);
5421
5651
  if (afterScore.score < baselineScore.score) {
5422
5652
  console.log("");
5423
- console.log(chalk9.yellow(` Score would drop from ${baselineScore.score} to ${afterScore.score} \u2014 reverting changes.`));
5653
+ console.log(chalk7.yellow(` Score would drop from ${baselineScore.score} to ${afterScore.score} \u2014 reverting changes.`));
5424
5654
  try {
5425
5655
  const { restored, removed } = undoSetup();
5426
5656
  if (restored.length > 0 || removed.length > 0) {
5427
- console.log(chalk9.dim(` Reverted ${restored.length + removed.length} file${restored.length + removed.length === 1 ? "" : "s"} from backup.`));
5657
+ console.log(chalk7.dim(` Reverted ${restored.length + removed.length} file${restored.length + removed.length === 1 ? "" : "s"} from backup.`));
5428
5658
  }
5429
5659
  } catch {
5430
5660
  }
5431
- console.log(chalk9.dim(" Run ") + chalk9.hex("#83D1EB")("caliber onboard --force") + chalk9.dim(" to override.\n"));
5661
+ console.log(chalk7.dim(" Run ") + chalk7.hex("#83D1EB")("caliber onboard --force") + chalk7.dim(" to override.\n"));
5432
5662
  return;
5433
5663
  }
5434
5664
  displayScoreDelta(baselineScore, afterScore);
5435
- console.log(chalk9.bold.green(" Regeneration complete!"));
5436
- console.log(chalk9.dim(" Run ") + chalk9.hex("#83D1EB")("caliber undo") + chalk9.dim(" to revert changes.\n"));
5437
- }
5438
-
5439
- // src/commands/recommend.ts
5440
- import chalk10 from "chalk";
5441
- import ora5 from "ora";
5442
- import { mkdirSync, readFileSync as readFileSync7, readdirSync as readdirSync5, existsSync as existsSync9, writeFileSync } from "fs";
5443
- import { join as join8, dirname as dirname2 } from "path";
5444
-
5445
- // src/scanner/index.ts
5446
- import fs23 from "fs";
5447
- import path17 from "path";
5448
- import crypto2 from "crypto";
5449
- function scanLocalState(dir) {
5450
- const items = [];
5451
- const claudeMdPath = path17.join(dir, "CLAUDE.md");
5452
- if (fs23.existsSync(claudeMdPath)) {
5453
- items.push({
5454
- type: "rule",
5455
- platform: "claude",
5456
- name: "CLAUDE.md",
5457
- contentHash: hashFile(claudeMdPath),
5458
- path: claudeMdPath
5459
- });
5460
- }
5461
- const skillsDir = path17.join(dir, ".claude", "skills");
5462
- if (fs23.existsSync(skillsDir)) {
5463
- for (const file of fs23.readdirSync(skillsDir).filter((f) => f.endsWith(".md"))) {
5464
- const filePath = path17.join(skillsDir, file);
5465
- items.push({
5466
- type: "skill",
5467
- platform: "claude",
5468
- name: file,
5469
- contentHash: hashFile(filePath),
5470
- path: filePath
5471
- });
5472
- }
5473
- }
5474
- const mcpJsonPath = path17.join(dir, ".mcp.json");
5475
- if (fs23.existsSync(mcpJsonPath)) {
5665
+ console.log(title.bold("\n Step 6/6 \u2014 Community skills\n"));
5666
+ console.log(chalk7.dim(" Search public skill registries for skills that match your tech stack.\n"));
5667
+ const wantsSkills = await select4({
5668
+ message: "Search public repos for relevant skills to add to this project?",
5669
+ choices: [
5670
+ { name: "Yes, find skills for my project", value: true },
5671
+ { name: "Skip for now", value: false }
5672
+ ]
5673
+ });
5674
+ if (wantsSkills) {
5476
5675
  try {
5477
- const mcpJson = JSON.parse(fs23.readFileSync(mcpJsonPath, "utf-8"));
5478
- if (mcpJson.mcpServers) {
5479
- for (const name of Object.keys(mcpJson.mcpServers)) {
5480
- items.push({
5481
- type: "mcp",
5482
- platform: "claude",
5483
- name,
5484
- contentHash: hashJson(mcpJson.mcpServers[name]),
5485
- path: mcpJsonPath
5486
- });
5487
- }
5676
+ await searchAndInstallSkills();
5677
+ } catch (err) {
5678
+ if (err.message !== "__exit__") {
5679
+ console.log(chalk7.dim(" Skills search failed: " + (err.message || "unknown error")));
5488
5680
  }
5489
- } catch {
5681
+ console.log(chalk7.dim(" Run ") + chalk7.hex("#83D1EB")("caliber skills") + chalk7.dim(" later to try again.\n"));
5490
5682
  }
5683
+ } else {
5684
+ console.log(chalk7.dim(" Skipped. Run ") + chalk7.hex("#83D1EB")("caliber skills") + chalk7.dim(" later to browse.\n"));
5491
5685
  }
5492
- const agentsMdPath = path17.join(dir, "AGENTS.md");
5493
- if (fs23.existsSync(agentsMdPath)) {
5494
- items.push({
5495
- type: "rule",
5496
- platform: "codex",
5497
- name: "AGENTS.md",
5498
- contentHash: hashFile(agentsMdPath),
5499
- path: agentsMdPath
5500
- });
5501
- }
5502
- const codexSkillsDir = path17.join(dir, ".agents", "skills");
5503
- if (fs23.existsSync(codexSkillsDir)) {
5504
- try {
5505
- for (const name of fs23.readdirSync(codexSkillsDir)) {
5506
- const skillFile = path17.join(codexSkillsDir, name, "SKILL.md");
5507
- if (fs23.existsSync(skillFile)) {
5508
- items.push({
5509
- type: "skill",
5510
- platform: "codex",
5511
- name: `${name}/SKILL.md`,
5512
- contentHash: hashFile(skillFile),
5513
- path: skillFile
5514
- });
5515
- }
5516
- }
5517
- } catch {
5686
+ console.log(chalk7.bold.green(" Onboarding complete! Your project is ready for AI-assisted development."));
5687
+ console.log(chalk7.dim(" Run ") + chalk7.hex("#83D1EB")("caliber undo") + chalk7.dim(" to revert changes.\n"));
5688
+ console.log(chalk7.bold(" Next steps:\n"));
5689
+ console.log(` ${title("caliber score")} See your full config breakdown`);
5690
+ console.log(` ${title("caliber skills")} Discover community skills for your stack`);
5691
+ console.log(` ${title("caliber undo")} Revert all changes from this run`);
5692
+ console.log("");
5693
+ }
5694
+ async function refineLoop(currentSetup, _targetAgent, sessionHistory) {
5695
+ while (true) {
5696
+ const message = await promptInput3("\nWhat would you like to change?");
5697
+ if (!message || message.toLowerCase() === "done" || message.toLowerCase() === "accept") {
5698
+ return currentSetup;
5518
5699
  }
5519
- }
5520
- const cursorrulesPath = path17.join(dir, ".cursorrules");
5521
- if (fs23.existsSync(cursorrulesPath)) {
5522
- items.push({
5523
- type: "rule",
5524
- platform: "cursor",
5525
- name: ".cursorrules",
5526
- contentHash: hashFile(cursorrulesPath),
5527
- path: cursorrulesPath
5528
- });
5529
- }
5530
- const cursorRulesDir = path17.join(dir, ".cursor", "rules");
5531
- if (fs23.existsSync(cursorRulesDir)) {
5532
- for (const file of fs23.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"))) {
5533
- const filePath = path17.join(cursorRulesDir, file);
5534
- items.push({
5535
- type: "rule",
5536
- platform: "cursor",
5537
- name: file,
5538
- contentHash: hashFile(filePath),
5539
- path: filePath
5540
- });
5700
+ if (message.toLowerCase() === "cancel") {
5701
+ return null;
5541
5702
  }
5542
- }
5543
- const cursorSkillsDir = path17.join(dir, ".cursor", "skills");
5544
- if (fs23.existsSync(cursorSkillsDir)) {
5545
- try {
5546
- for (const name of fs23.readdirSync(cursorSkillsDir)) {
5547
- const skillFile = path17.join(cursorSkillsDir, name, "SKILL.md");
5548
- if (fs23.existsSync(skillFile)) {
5549
- items.push({
5550
- type: "skill",
5551
- platform: "cursor",
5552
- name: `${name}/SKILL.md`,
5553
- contentHash: hashFile(skillFile),
5554
- path: skillFile
5555
- });
5556
- }
5557
- }
5558
- } catch {
5703
+ const isValid = await classifyRefineIntent(message);
5704
+ if (!isValid) {
5705
+ console.log(chalk7.dim(" This doesn't look like a config change request."));
5706
+ console.log(chalk7.dim(" Describe what to add, remove, or modify in your configs."));
5707
+ console.log(chalk7.dim(' Type "done" to accept the current setup.\n'));
5708
+ continue;
5559
5709
  }
5560
- }
5561
- const cursorMcpPath = path17.join(dir, ".cursor", "mcp.json");
5562
- if (fs23.existsSync(cursorMcpPath)) {
5563
- try {
5564
- const mcpJson = JSON.parse(fs23.readFileSync(cursorMcpPath, "utf-8"));
5565
- if (mcpJson.mcpServers) {
5566
- for (const name of Object.keys(mcpJson.mcpServers)) {
5567
- items.push({
5568
- type: "mcp",
5569
- platform: "cursor",
5570
- name,
5571
- contentHash: hashJson(mcpJson.mcpServers[name]),
5572
- path: cursorMcpPath
5573
- });
5574
- }
5575
- }
5576
- } catch {
5710
+ const refineSpinner = ora3("Refining setup...").start();
5711
+ const refineMessages = new SpinnerMessages(refineSpinner, REFINE_MESSAGES);
5712
+ refineMessages.start();
5713
+ const refined = await refineSetup(
5714
+ currentSetup,
5715
+ message,
5716
+ sessionHistory
5717
+ );
5718
+ refineMessages.stop();
5719
+ if (refined) {
5720
+ currentSetup = refined;
5721
+ sessionHistory.push({ role: "user", content: message });
5722
+ sessionHistory.push({
5723
+ role: "assistant",
5724
+ content: summarizeSetup("Applied changes", refined)
5725
+ });
5726
+ refineSpinner.succeed("Setup updated");
5727
+ printSetupSummary(refined);
5728
+ console.log(chalk7.dim('Type "done" to accept, or describe more changes.'));
5729
+ } else {
5730
+ refineSpinner.fail("Refinement failed \u2014 could not parse AI response.");
5731
+ console.log(chalk7.dim('Try rephrasing your request, or type "done" to keep the current setup.'));
5577
5732
  }
5578
5733
  }
5579
- return items;
5580
5734
  }
5581
- function hashFile(filePath) {
5582
- const text = fs23.readFileSync(filePath, "utf-8");
5583
- return crypto2.createHash("sha256").update(JSON.stringify({ text })).digest("hex");
5735
+ function summarizeSetup(action, setup) {
5736
+ const descriptions = setup.fileDescriptions;
5737
+ const files = descriptions ? Object.entries(descriptions).map(([path24, desc]) => ` ${path24}: ${desc}`).join("\n") : Object.keys(setup).filter((k) => k !== "targetAgent" && k !== "fileDescriptions").join(", ");
5738
+ return `${action}. Files:
5739
+ ${files}`;
5584
5740
  }
5585
- function hashJson(obj) {
5586
- return crypto2.createHash("sha256").update(JSON.stringify(obj)).digest("hex");
5741
+ async function classifyRefineIntent(message) {
5742
+ const fastModel = getFastModel();
5743
+ try {
5744
+ const result = await llmJsonCall({
5745
+ system: `You classify whether a user message is a valid request to modify AI agent config files (CLAUDE.md, .cursorrules, skills).
5746
+ Valid: requests to add, remove, change, or restructure config content. Examples: "add testing commands", "remove the terraform section", "make CLAUDE.md shorter".
5747
+ Invalid: questions, requests to show/display something, general chat, or anything that isn't a concrete config change.
5748
+ Return {"valid": true} or {"valid": false}. Nothing else.`,
5749
+ prompt: message,
5750
+ maxTokens: 20,
5751
+ ...fastModel ? { model: fastModel } : {}
5752
+ });
5753
+ return result.valid === true;
5754
+ } catch {
5755
+ return true;
5756
+ }
5587
5757
  }
5758
+ async function evaluateDismissals(failingChecks, fingerprint) {
5759
+ const fastModel = getFastModel();
5760
+ const checkList = failingChecks.map((c) => ({
5761
+ id: c.id,
5762
+ name: c.name,
5763
+ suggestion: c.suggestion
5764
+ }));
5765
+ try {
5766
+ const result = await llmJsonCall({
5767
+ system: `You evaluate whether scoring checks are applicable to a project.
5768
+ Given the project's languages/frameworks and a list of failing checks, return which checks are NOT applicable.
5588
5769
 
5589
- // src/commands/recommend.ts
5590
- function detectLocalPlatforms() {
5591
- const items = scanLocalState(process.cwd());
5592
- const platforms = /* @__PURE__ */ new Set();
5593
- for (const item of items) {
5594
- platforms.add(item.platform);
5770
+ Only dismiss checks that truly don't apply \u2014 e.g. "Build/test/lint commands" for a pure Terraform/HCL repo with no build system.
5771
+ Do NOT dismiss checks that could reasonably apply even if the project doesn't use them yet.
5772
+
5773
+ Return {"dismissed": [{"id": "check_id", "reason": "brief reason"}]} or {"dismissed": []} if all apply.`,
5774
+ prompt: `Languages: ${fingerprint.languages.join(", ") || "none"}
5775
+ Frameworks: ${fingerprint.frameworks.join(", ") || "none"}
5776
+
5777
+ Failing checks:
5778
+ ${JSON.stringify(checkList, null, 2)}`,
5779
+ maxTokens: 200,
5780
+ ...fastModel ? { model: fastModel } : {}
5781
+ });
5782
+ if (!Array.isArray(result.dismissed)) return [];
5783
+ return result.dismissed.filter((d) => d.id && d.reason && failingChecks.some((c) => c.id === d.id)).map((d) => ({ id: d.id, reason: d.reason, dismissedAt: (/* @__PURE__ */ new Date()).toISOString() }));
5784
+ } catch {
5785
+ return [];
5595
5786
  }
5596
- return platforms.size > 0 ? Array.from(platforms) : ["claude"];
5597
5787
  }
5598
- function getSkillPath(platform, slug) {
5599
- if (platform === "cursor") {
5600
- return join8(".cursor", "skills", slug, "SKILL.md");
5788
+ function promptInput3(question) {
5789
+ const rl = readline4.createInterface({ input: process.stdin, output: process.stdout });
5790
+ return new Promise((resolve2) => {
5791
+ rl.question(chalk7.cyan(`${question} `), (answer) => {
5792
+ rl.close();
5793
+ resolve2(answer.trim());
5794
+ });
5795
+ });
5796
+ }
5797
+ async function promptAgent() {
5798
+ const selected = await checkbox({
5799
+ message: "Which coding agents do you use? (toggle with space)",
5800
+ choices: [
5801
+ { name: "Claude Code", value: "claude" },
5802
+ { name: "Cursor", value: "cursor" },
5803
+ { name: "Codex (OpenAI)", value: "codex" }
5804
+ ],
5805
+ validate: (items) => {
5806
+ if (items.length === 0) return "At least one agent must be selected";
5807
+ return true;
5808
+ }
5809
+ });
5810
+ return selected;
5811
+ }
5812
+ async function promptHookType(targetAgent) {
5813
+ const choices = [];
5814
+ const hasClaude = targetAgent.includes("claude");
5815
+ if (hasClaude) {
5816
+ choices.push({ name: "Claude Code hook (auto-refresh on session end)", value: "claude" });
5601
5817
  }
5602
- if (platform === "codex") {
5603
- return join8(".agents", "skills", slug, "SKILL.md");
5818
+ choices.push({ name: "Git pre-commit hook (refresh before each commit)", value: "precommit" });
5819
+ if (hasClaude) {
5820
+ choices.push({ name: "Both (Claude Code + pre-commit)", value: "both" });
5604
5821
  }
5605
- return join8(".claude", "skills", slug, "SKILL.md");
5822
+ choices.push({ name: "Skip for now", value: "skip" });
5823
+ return select4({
5824
+ message: "How would you like to auto-refresh your docs?",
5825
+ choices
5826
+ });
5606
5827
  }
5607
- function getInstalledSkills() {
5608
- const installed = /* @__PURE__ */ new Set();
5609
- const dirs = [
5610
- join8(process.cwd(), ".claude", "skills"),
5611
- join8(process.cwd(), ".cursor", "skills"),
5612
- join8(process.cwd(), ".agents", "skills")
5613
- ];
5614
- for (const dir of dirs) {
5615
- try {
5616
- const entries = readdirSync5(dir, { withFileTypes: true });
5617
- for (const entry of entries) {
5618
- if (entry.isDirectory()) {
5619
- installed.add(entry.name.toLowerCase());
5620
- }
5828
+ async function promptReviewAction() {
5829
+ return select4({
5830
+ message: "What would you like to do?",
5831
+ choices: [
5832
+ { name: "Accept and apply", value: "accept" },
5833
+ { name: "Refine via chat", value: "refine" },
5834
+ { name: "Decline", value: "decline" }
5835
+ ]
5836
+ });
5837
+ }
5838
+ function printSetupSummary(setup) {
5839
+ const claude = setup.claude;
5840
+ const cursor = setup.cursor;
5841
+ const fileDescriptions = setup.fileDescriptions;
5842
+ const deletions = setup.deletions;
5843
+ console.log("");
5844
+ console.log(chalk7.bold(" Proposed changes:\n"));
5845
+ const getDescription = (filePath) => {
5846
+ return fileDescriptions?.[filePath];
5847
+ };
5848
+ if (claude) {
5849
+ if (claude.claudeMd) {
5850
+ const icon = fs22.existsSync("CLAUDE.md") ? chalk7.yellow("~") : chalk7.green("+");
5851
+ const desc = getDescription("CLAUDE.md");
5852
+ console.log(` ${icon} ${chalk7.bold("CLAUDE.md")}`);
5853
+ if (desc) console.log(chalk7.dim(` ${desc}`));
5854
+ console.log("");
5855
+ }
5856
+ const skills = claude.skills;
5857
+ if (Array.isArray(skills) && skills.length > 0) {
5858
+ for (const skill of skills) {
5859
+ const skillPath = `.claude/skills/${skill.name}/SKILL.md`;
5860
+ const icon = fs22.existsSync(skillPath) ? chalk7.yellow("~") : chalk7.green("+");
5861
+ const desc = getDescription(skillPath);
5862
+ console.log(` ${icon} ${chalk7.bold(skillPath)}`);
5863
+ console.log(chalk7.dim(` ${desc || skill.description || skill.name}`));
5864
+ console.log("");
5621
5865
  }
5622
- } catch {
5623
5866
  }
5624
5867
  }
5625
- return installed;
5626
- }
5627
- async function searchSkillsSh(technologies) {
5628
- const bestBySlug = /* @__PURE__ */ new Map();
5629
- for (const tech of technologies) {
5630
- try {
5631
- const resp = await fetch(`https://skills.sh/api/search?q=${encodeURIComponent(tech)}&limit=10`, {
5632
- signal: AbortSignal.timeout(1e4)
5633
- });
5634
- if (!resp.ok) continue;
5635
- const data = await resp.json();
5636
- if (!data.skills?.length) continue;
5637
- for (const skill of data.skills) {
5638
- const existing = bestBySlug.get(skill.skillId);
5639
- if (existing && existing.installs >= (skill.installs ?? 0)) continue;
5640
- bestBySlug.set(skill.skillId, {
5641
- name: skill.name,
5642
- slug: skill.skillId,
5643
- source_url: skill.source ? `https://github.com/${skill.source}` : "",
5644
- score: 0,
5645
- reason: skill.description || "",
5646
- detected_technology: tech,
5647
- item_type: "skill",
5648
- installs: skill.installs ?? 0
5649
- });
5868
+ const codex = setup.codex;
5869
+ if (codex) {
5870
+ if (codex.agentsMd) {
5871
+ const icon = fs22.existsSync("AGENTS.md") ? chalk7.yellow("~") : chalk7.green("+");
5872
+ const desc = getDescription("AGENTS.md");
5873
+ console.log(` ${icon} ${chalk7.bold("AGENTS.md")}`);
5874
+ if (desc) console.log(chalk7.dim(` ${desc}`));
5875
+ console.log("");
5876
+ }
5877
+ const codexSkills = codex.skills;
5878
+ if (Array.isArray(codexSkills) && codexSkills.length > 0) {
5879
+ for (const skill of codexSkills) {
5880
+ const skillPath = `.agents/skills/${skill.name}/SKILL.md`;
5881
+ const icon = fs22.existsSync(skillPath) ? chalk7.yellow("~") : chalk7.green("+");
5882
+ const desc = getDescription(skillPath);
5883
+ console.log(` ${icon} ${chalk7.bold(skillPath)}`);
5884
+ console.log(chalk7.dim(` ${desc || skill.description || skill.name}`));
5885
+ console.log("");
5650
5886
  }
5651
- } catch {
5652
- continue;
5653
5887
  }
5654
5888
  }
5655
- return Array.from(bestBySlug.values());
5656
- }
5657
- async function searchTessl(technologies) {
5658
- const results = [];
5659
- const seen = /* @__PURE__ */ new Set();
5660
- for (const tech of technologies) {
5661
- try {
5662
- const resp = await fetch(`https://tessl.io/registry?q=${encodeURIComponent(tech)}`, {
5663
- signal: AbortSignal.timeout(1e4)
5664
- });
5665
- if (!resp.ok) continue;
5666
- const html = await resp.text();
5667
- const linkMatches = html.matchAll(/\/registry\/skills\/github\/([^/]+)\/([^/]+)\/([^/"]+)/g);
5668
- for (const match of linkMatches) {
5669
- const [, org, repo, skillName] = match;
5670
- const slug = `${org}-${repo}-${skillName}`.toLowerCase();
5671
- if (seen.has(slug)) continue;
5672
- seen.add(slug);
5673
- results.push({
5674
- name: skillName,
5675
- slug,
5676
- source_url: `https://github.com/${org}/${repo}`,
5677
- score: 0,
5678
- reason: `Skill from ${org}/${repo}`,
5679
- detected_technology: tech,
5680
- item_type: "skill"
5681
- });
5889
+ if (cursor) {
5890
+ if (cursor.cursorrules) {
5891
+ const icon = fs22.existsSync(".cursorrules") ? chalk7.yellow("~") : chalk7.green("+");
5892
+ const desc = getDescription(".cursorrules");
5893
+ console.log(` ${icon} ${chalk7.bold(".cursorrules")}`);
5894
+ if (desc) console.log(chalk7.dim(` ${desc}`));
5895
+ console.log("");
5896
+ }
5897
+ const cursorSkills = cursor.skills;
5898
+ if (Array.isArray(cursorSkills) && cursorSkills.length > 0) {
5899
+ for (const skill of cursorSkills) {
5900
+ const skillPath = `.cursor/skills/${skill.name}/SKILL.md`;
5901
+ const icon = fs22.existsSync(skillPath) ? chalk7.yellow("~") : chalk7.green("+");
5902
+ const desc = getDescription(skillPath);
5903
+ console.log(` ${icon} ${chalk7.bold(skillPath)}`);
5904
+ console.log(chalk7.dim(` ${desc || skill.description || skill.name}`));
5905
+ console.log("");
5682
5906
  }
5683
- } catch {
5684
- continue;
5685
5907
  }
5686
- }
5687
- return results;
5688
- }
5689
- var AWESOME_CLAUDE_CODE_URL = "https://raw.githubusercontent.com/hesreallyhim/awesome-claude-code/main/README.md";
5690
- async function searchAwesomeClaudeCode(technologies) {
5691
- try {
5692
- const resp = await fetch(AWESOME_CLAUDE_CODE_URL, {
5693
- signal: AbortSignal.timeout(1e4)
5694
- });
5695
- if (!resp.ok) return [];
5696
- const markdown = await resp.text();
5697
- const items = [];
5698
- const itemPattern = /^[-*]\s+\[([^\]]+)\]\(([^)]+)\)(?:\s+by\s+\[[^\]]*\]\([^)]*\))?\s*[-–—:]\s*(.*)/gm;
5699
- let match;
5700
- while ((match = itemPattern.exec(markdown)) !== null) {
5701
- const [, name, url, description] = match;
5702
- if (url.startsWith("#")) continue;
5703
- const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
5704
- items.push({
5705
- name: name.trim(),
5706
- slug,
5707
- source_url: url.trim(),
5708
- score: 0,
5709
- reason: description.trim().slice(0, 150),
5710
- detected_technology: "claude-code",
5711
- item_type: "skill"
5712
- });
5908
+ const rules = cursor.rules;
5909
+ if (Array.isArray(rules) && rules.length > 0) {
5910
+ for (const rule of rules) {
5911
+ const rulePath = `.cursor/rules/${rule.filename}`;
5912
+ const icon = fs22.existsSync(rulePath) ? chalk7.yellow("~") : chalk7.green("+");
5913
+ const desc = getDescription(rulePath);
5914
+ console.log(` ${icon} ${chalk7.bold(rulePath)}`);
5915
+ if (desc) {
5916
+ console.log(chalk7.dim(` ${desc}`));
5917
+ } else {
5918
+ const firstLine = rule.content.split("\n").filter((l) => l.trim() && !l.trim().startsWith("#"))[0];
5919
+ if (firstLine) console.log(chalk7.dim(` ${firstLine.trim().slice(0, 80)}`));
5920
+ }
5921
+ console.log("");
5922
+ }
5923
+ }
5924
+ }
5925
+ if (!codex && !fs22.existsSync("AGENTS.md")) {
5926
+ console.log(` ${chalk7.green("+")} ${chalk7.bold("AGENTS.md")}`);
5927
+ console.log(chalk7.dim(" Cross-agent coordination file"));
5928
+ console.log("");
5929
+ }
5930
+ if (Array.isArray(deletions) && deletions.length > 0) {
5931
+ for (const del of deletions) {
5932
+ console.log(` ${chalk7.red("-")} ${chalk7.bold(del.filePath)}`);
5933
+ console.log(chalk7.dim(` ${del.reason}`));
5934
+ console.log("");
5713
5935
  }
5714
- const techLower = technologies.map((t) => t.toLowerCase());
5715
- return items.filter((item) => {
5716
- const text = `${item.name} ${item.reason}`.toLowerCase();
5717
- return techLower.some((t) => text.includes(t));
5718
- });
5719
- } catch {
5720
- return [];
5721
5936
  }
5937
+ console.log(` ${chalk7.green("+")} ${chalk7.dim("new")} ${chalk7.yellow("~")} ${chalk7.dim("modified")} ${chalk7.red("-")} ${chalk7.dim("removed")}`);
5938
+ console.log("");
5722
5939
  }
5723
- async function searchAllProviders(technologies, platform) {
5724
- const searches = [
5725
- searchSkillsSh(technologies),
5726
- searchTessl(technologies)
5727
- ];
5728
- if (platform === "claude" || !platform) {
5729
- searches.push(searchAwesomeClaudeCode(technologies));
5730
- }
5731
- const results = await Promise.all(searches);
5732
- const seen = /* @__PURE__ */ new Set();
5733
- const combined = [];
5734
- for (const batch of results) {
5735
- for (const result of batch) {
5736
- const key = result.name.toLowerCase().replace(/[-_]/g, "");
5737
- if (seen.has(key)) continue;
5738
- seen.add(key);
5739
- combined.push(result);
5940
+ function ensurePermissions() {
5941
+ const settingsPath = ".claude/settings.json";
5942
+ let settings = {};
5943
+ try {
5944
+ if (fs22.existsSync(settingsPath)) {
5945
+ settings = JSON.parse(fs22.readFileSync(settingsPath, "utf-8"));
5740
5946
  }
5947
+ } catch {
5741
5948
  }
5742
- return combined;
5949
+ const permissions = settings.permissions ?? {};
5950
+ const allow = permissions.allow;
5951
+ if (Array.isArray(allow) && allow.length > 0) return;
5952
+ permissions.allow = [
5953
+ "Bash(npm run *)",
5954
+ "Bash(npx vitest *)",
5955
+ "Bash(npx tsc *)",
5956
+ "Bash(git *)"
5957
+ ];
5958
+ settings.permissions = permissions;
5959
+ if (!fs22.existsSync(".claude")) fs22.mkdirSync(".claude", { recursive: true });
5960
+ fs22.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5743
5961
  }
5744
- async function scoreWithLLM2(candidates, projectContext, technologies) {
5745
- const candidateList = candidates.map((c, i) => `${i}. "${c.name}" \u2014 ${c.reason || "no description"}`).join("\n");
5746
- const scored = await llmJsonCall({
5747
- system: `You evaluate whether AI agent skills and tools are relevant to a specific software project.
5748
- Given a project context and a list of candidates, score each one's relevance from 0-100 and provide a brief reason (max 80 chars).
5749
-
5750
- Return a JSON array where each element has:
5751
- - "index": the candidate's index number
5752
- - "score": relevance score 0-100
5753
- - "reason": one-liner explaining why it fits or doesn't
5754
-
5755
- Scoring guidelines:
5756
- - 90-100: Directly matches a core technology or workflow in the project
5757
- - 70-89: Relevant to the project's stack, patterns, or development workflow
5758
- - 50-69: Tangentially related or generic but useful
5759
- - 0-49: Not relevant to this project
5760
-
5761
- Be selective. Prefer specific, high-quality matches over generic ones.
5762
- A skill for "React testing" is only relevant if the project uses React.
5763
- A generic "TypeScript best practices" skill is less valuable than one targeting the project's actual framework.
5764
- Return ONLY the JSON array.`,
5765
- prompt: `PROJECT CONTEXT:
5766
- ${projectContext}
5767
-
5768
- DETECTED TECHNOLOGIES:
5769
- ${technologies.join(", ")}
5770
5962
 
5771
- CANDIDATES:
5772
- ${candidateList}`,
5773
- maxTokens: 8e3
5774
- });
5775
- if (!Array.isArray(scored)) return [];
5776
- return scored.filter((s) => s.score >= 60 && s.index >= 0 && s.index < candidates.length).sort((a, b) => b.score - a.score).slice(0, 20).map((s) => ({
5777
- ...candidates[s.index],
5778
- score: s.score,
5779
- reason: s.reason || candidates[s.index].reason
5780
- }));
5963
+ // src/commands/undo.ts
5964
+ import chalk8 from "chalk";
5965
+ import ora4 from "ora";
5966
+ function undoCommand() {
5967
+ const spinner = ora4("Reverting setup...").start();
5968
+ try {
5969
+ const { restored, removed } = undoSetup();
5970
+ if (restored.length === 0 && removed.length === 0) {
5971
+ spinner.info("Nothing to undo.");
5972
+ return;
5973
+ }
5974
+ spinner.succeed("Setup reverted successfully.\n");
5975
+ if (restored.length > 0) {
5976
+ console.log(chalk8.cyan(" Restored from backup:"));
5977
+ for (const file of restored) {
5978
+ console.log(` ${chalk8.green("\u21A9")} ${file}`);
5979
+ }
5980
+ }
5981
+ if (removed.length > 0) {
5982
+ console.log(chalk8.cyan(" Removed:"));
5983
+ for (const file of removed) {
5984
+ console.log(` ${chalk8.red("\u2717")} ${file}`);
5985
+ }
5986
+ }
5987
+ console.log("");
5988
+ } catch (err) {
5989
+ spinner.fail(chalk8.red(err instanceof Error ? err.message : "Undo failed"));
5990
+ throw new Error("__exit__");
5991
+ }
5781
5992
  }
5782
- function buildProjectContext(dir) {
5783
- const parts = [];
5784
- const fingerprint = collectFingerprint(dir);
5785
- if (fingerprint.packageName) parts.push(`Package: ${fingerprint.packageName}`);
5786
- if (fingerprint.languages.length > 0) parts.push(`Languages: ${fingerprint.languages.join(", ")}`);
5787
- if (fingerprint.frameworks.length > 0) parts.push(`Frameworks: ${fingerprint.frameworks.join(", ")}`);
5788
- if (fingerprint.description) parts.push(`Description: ${fingerprint.description}`);
5789
- if (fingerprint.fileTree.length > 0) {
5790
- parts.push(`
5791
- File tree (${fingerprint.fileTree.length} files):
5792
- ${fingerprint.fileTree.slice(0, 50).join("\n")}`);
5993
+
5994
+ // src/commands/status.ts
5995
+ import chalk9 from "chalk";
5996
+ import fs23 from "fs";
5997
+ async function statusCommand(options) {
5998
+ const config = loadConfig();
5999
+ const manifest = readManifest();
6000
+ if (options.json) {
6001
+ console.log(JSON.stringify({
6002
+ configured: !!config,
6003
+ provider: config?.provider,
6004
+ model: config?.model,
6005
+ manifest
6006
+ }, null, 2));
6007
+ return;
5793
6008
  }
5794
- if (fingerprint.existingConfigs.claudeMd) {
5795
- parts.push(`
5796
- Existing CLAUDE.md (first 500 chars):
5797
- ${fingerprint.existingConfigs.claudeMd.slice(0, 500)}`);
6009
+ console.log(chalk9.bold("\nCaliber Status\n"));
6010
+ if (config) {
6011
+ console.log(` LLM: ${chalk9.green(config.provider)} (${config.model})`);
6012
+ } else {
6013
+ console.log(` LLM: ${chalk9.yellow("Not configured")} \u2014 run ${chalk9.hex("#83D1EB")("caliber config")}`);
5798
6014
  }
5799
- const deps = extractTopDeps();
5800
- if (deps.length > 0) {
5801
- parts.push(`
5802
- Dependencies: ${deps.slice(0, 30).join(", ")}`);
6015
+ if (!manifest) {
6016
+ console.log(` Setup: ${chalk9.dim("No setup applied")}`);
6017
+ console.log(chalk9.dim("\n Run ") + chalk9.hex("#83D1EB")("caliber onboard") + chalk9.dim(" to get started.\n"));
6018
+ return;
5803
6019
  }
5804
- const installed = getInstalledSkills();
5805
- if (installed.size > 0) {
5806
- parts.push(`
5807
- Already installed skills: ${Array.from(installed).join(", ")}`);
6020
+ console.log(` Files managed: ${chalk9.cyan(manifest.entries.length.toString())}`);
6021
+ for (const entry of manifest.entries) {
6022
+ const exists = fs23.existsSync(entry.path);
6023
+ const icon = exists ? chalk9.green("\u2713") : chalk9.red("\u2717");
6024
+ console.log(` ${icon} ${entry.path} (${entry.action})`);
5808
6025
  }
5809
- return parts.join("\n");
6026
+ console.log("");
5810
6027
  }
5811
- function extractTopDeps() {
5812
- const pkgPath = join8(process.cwd(), "package.json");
5813
- if (!existsSync9(pkgPath)) return [];
6028
+
6029
+ // src/commands/regenerate.ts
6030
+ import chalk10 from "chalk";
6031
+ import ora5 from "ora";
6032
+ import select5 from "@inquirer/select";
6033
+ async function regenerateCommand(options) {
6034
+ const config = loadConfig();
6035
+ if (!config) {
6036
+ console.log(chalk10.red("No LLM provider configured. Run ") + chalk10.hex("#83D1EB")("caliber config") + chalk10.red(" first."));
6037
+ throw new Error("__exit__");
6038
+ }
6039
+ const manifest = readManifest();
6040
+ if (!manifest) {
6041
+ console.log(chalk10.yellow("No existing setup found. Run ") + chalk10.hex("#83D1EB")("caliber onboard") + chalk10.yellow(" first."));
6042
+ throw new Error("__exit__");
6043
+ }
6044
+ const targetAgent = readState()?.targetAgent ?? ["claude", "cursor"];
6045
+ const spinner = ora5("Analyzing project...").start();
6046
+ const fingerprint = await collectFingerprint(process.cwd());
6047
+ spinner.succeed("Project analyzed");
6048
+ const baselineScore = computeLocalScore(process.cwd(), targetAgent);
6049
+ displayScoreSummary(baselineScore);
6050
+ if (baselineScore.score === 100) {
6051
+ console.log(chalk10.green(" Your setup is already at 100/100 \u2014 nothing to regenerate.\n"));
6052
+ return;
6053
+ }
6054
+ const genSpinner = ora5("Regenerating setup...").start();
6055
+ const genMessages = new SpinnerMessages(genSpinner, GENERATION_MESSAGES, { showElapsedTime: true });
6056
+ genMessages.start();
6057
+ let generatedSetup = null;
5814
6058
  try {
5815
- const pkg3 = JSON.parse(readFileSync7(pkgPath, "utf-8"));
5816
- const deps = Object.keys(pkg3.dependencies ?? {});
5817
- const trivial = /* @__PURE__ */ new Set([
5818
- "typescript",
5819
- "tslib",
5820
- "ts-node",
5821
- "tsx",
5822
- "prettier",
5823
- "eslint",
5824
- "@eslint/js",
5825
- "rimraf",
5826
- "cross-env",
5827
- "dotenv",
5828
- "nodemon",
5829
- "husky",
5830
- "lint-staged",
5831
- "commitlint",
5832
- "chalk",
5833
- "ora",
5834
- "commander",
5835
- "yargs",
5836
- "meow",
5837
- "inquirer",
5838
- "@inquirer/confirm",
5839
- "@inquirer/select",
5840
- "@inquirer/prompts",
5841
- "glob",
5842
- "minimatch",
5843
- "micromatch",
5844
- "diff",
5845
- "semver",
5846
- "uuid",
5847
- "nanoid",
5848
- "debug",
5849
- "ms",
5850
- "lodash",
5851
- "underscore",
5852
- "tsup",
5853
- "esbuild",
5854
- "rollup",
5855
- "webpack",
5856
- "vite",
5857
- "vitest",
5858
- "jest",
5859
- "mocha",
5860
- "chai",
5861
- "ava",
5862
- "fs-extra",
5863
- "mkdirp",
5864
- "del",
5865
- "rimraf",
5866
- "path-to-regexp",
5867
- "strip-ansi",
5868
- "ansi-colors"
5869
- ]);
5870
- const trivialPatterns = [
5871
- /^@types\//,
5872
- /^@rely-ai\//,
5873
- /^@caliber-ai\//,
5874
- /^eslint-/,
5875
- /^@eslint\//,
5876
- /^prettier-/,
5877
- /^@typescript-eslint\//,
5878
- /^@commitlint\//
5879
- ];
5880
- return deps.filter(
5881
- (d) => !trivial.has(d) && !trivialPatterns.some((p) => p.test(d))
6059
+ const result = await generateSetup(
6060
+ fingerprint,
6061
+ targetAgent,
6062
+ void 0,
6063
+ {
6064
+ onStatus: (status) => {
6065
+ genMessages.handleServerStatus(status);
6066
+ },
6067
+ onComplete: (setup) => {
6068
+ generatedSetup = setup;
6069
+ },
6070
+ onError: (error) => {
6071
+ genMessages.stop();
6072
+ genSpinner.fail(`Generation error: ${error}`);
6073
+ }
6074
+ }
5882
6075
  );
5883
- } catch {
5884
- return [];
5885
- }
5886
- }
5887
- async function recommendCommand(options) {
5888
- const fingerprint = collectFingerprint(process.cwd());
5889
- const platforms = detectLocalPlatforms();
5890
- const installedSkills = getInstalledSkills();
5891
- const technologies = [...new Set([
5892
- ...fingerprint.languages,
5893
- ...fingerprint.frameworks,
5894
- ...extractTopDeps()
5895
- ].filter(Boolean))];
5896
- if (technologies.length === 0) {
5897
- console.log(chalk10.yellow("Could not detect any languages or dependencies. Try running from a project root."));
6076
+ if (!generatedSetup) generatedSetup = result.setup;
6077
+ } catch (err) {
6078
+ genMessages.stop();
6079
+ const msg = err instanceof Error ? err.message : "Unknown error";
6080
+ genSpinner.fail(`Regeneration failed: ${msg}`);
5898
6081
  throw new Error("__exit__");
5899
6082
  }
5900
- const primaryPlatform = platforms.includes("claude") ? "claude" : platforms[0];
5901
- const searchSpinner = ora5("Searching skill registries...").start();
5902
- const allCandidates = await searchAllProviders(technologies, primaryPlatform);
5903
- if (!allCandidates.length) {
5904
- searchSpinner.succeed("No skills found matching your tech stack.");
5905
- return;
6083
+ genMessages.stop();
6084
+ if (!generatedSetup) {
6085
+ genSpinner.fail("Failed to regenerate setup.");
6086
+ throw new Error("__exit__");
5906
6087
  }
5907
- const newCandidates = allCandidates.filter((c) => !installedSkills.has(c.slug.toLowerCase()));
5908
- const filteredCount = allCandidates.length - newCandidates.length;
5909
- if (!newCandidates.length) {
5910
- searchSpinner.succeed(`Found ${allCandidates.length} skills \u2014 all already installed.`);
6088
+ genSpinner.succeed("Setup regenerated");
6089
+ const setupFiles = collectSetupFiles(generatedSetup);
6090
+ const staged = stageFiles(setupFiles, process.cwd());
6091
+ const totalChanges = staged.newFiles + staged.modifiedFiles;
6092
+ console.log(chalk10.dim(`
6093
+ ${chalk10.green(`${staged.newFiles} new`)} / ${chalk10.yellow(`${staged.modifiedFiles} modified`)} file${totalChanges !== 1 ? "s" : ""}
6094
+ `));
6095
+ if (totalChanges === 0) {
6096
+ console.log(chalk10.dim(" No changes needed \u2014 your configs are already up to date.\n"));
6097
+ cleanupStaging();
5911
6098
  return;
5912
6099
  }
5913
- searchSpinner.succeed(
5914
- `Found ${allCandidates.length} skills` + (filteredCount > 0 ? chalk10.dim(` (${filteredCount} already installed)`) : "")
5915
- );
5916
- let results;
5917
- const config = loadConfig();
5918
- if (config) {
5919
- const scoreSpinner = ora5("Scoring relevance for your project...").start();
5920
- try {
5921
- const projectContext = buildProjectContext(process.cwd());
5922
- results = await scoreWithLLM2(newCandidates, projectContext, technologies);
5923
- if (results.length === 0) {
5924
- scoreSpinner.succeed("No highly relevant skills found for your specific project.");
5925
- return;
5926
- }
5927
- scoreSpinner.succeed(`${results.length} relevant skill${results.length > 1 ? "s" : ""} for your project`);
5928
- } catch {
5929
- scoreSpinner.warn("Could not score relevance \u2014 showing top results");
5930
- results = newCandidates.slice(0, 20);
6100
+ if (options.dryRun) {
6101
+ console.log(chalk10.yellow("[Dry run] Would write:"));
6102
+ for (const f of staged.stagedFiles) {
6103
+ console.log(` ${f.isNew ? chalk10.green("+") : chalk10.yellow("~")} ${f.relativePath}`);
5931
6104
  }
5932
- } else {
5933
- results = newCandidates.slice(0, 20);
5934
- }
5935
- const fetchSpinner = ora5("Verifying skill availability...").start();
5936
- const contentMap = /* @__PURE__ */ new Map();
5937
- await Promise.all(results.map(async (rec) => {
5938
- const content = await fetchSkillContent(rec);
5939
- if (content) contentMap.set(rec.slug, content);
5940
- }));
5941
- const available = results.filter((r) => contentMap.has(r.slug));
5942
- if (!available.length) {
5943
- fetchSpinner.fail("No installable skills found \u2014 content could not be fetched.");
6105
+ cleanupStaging();
5944
6106
  return;
5945
6107
  }
5946
- const unavailableCount = results.length - available.length;
5947
- fetchSpinner.succeed(
5948
- `${available.length} installable skill${available.length > 1 ? "s" : ""}` + (unavailableCount > 0 ? chalk10.dim(` (${unavailableCount} unavailable)`) : "")
5949
- );
5950
- const selected = await interactiveSelect2(available);
5951
- if (selected?.length) {
5952
- await installSkills(selected, platforms, contentMap);
6108
+ const wantsReview = await promptWantsReview();
6109
+ if (wantsReview) {
6110
+ const reviewMethod = await promptReviewMethod();
6111
+ await openReview(reviewMethod, staged.stagedFiles);
5953
6112
  }
5954
- }
5955
- async function interactiveSelect2(recs) {
5956
- if (!process.stdin.isTTY) {
5957
- printRecommendations(recs);
5958
- return null;
6113
+ const action = await select5({
6114
+ message: "Apply regenerated setup?",
6115
+ choices: [
6116
+ { name: "Accept and apply", value: "accept" },
6117
+ { name: "Decline", value: "decline" }
6118
+ ]
6119
+ });
6120
+ cleanupStaging();
6121
+ if (action === "decline") {
6122
+ console.log(chalk10.dim("Regeneration cancelled. No files were modified."));
6123
+ return;
5959
6124
  }
5960
- const selected = /* @__PURE__ */ new Set();
5961
- let cursor = 0;
5962
- const { stdin, stdout } = process;
5963
- let lineCount = 0;
5964
- const hasScores = recs.some((r) => r.score > 0);
5965
- function render() {
5966
- const lines = [];
5967
- lines.push(chalk10.bold(" Recommendations"));
5968
- lines.push("");
5969
- if (hasScores) {
5970
- lines.push(` ${chalk10.dim("Score".padEnd(7))} ${chalk10.dim("Name".padEnd(28))} ${chalk10.dim("Why")}`);
5971
- } else {
5972
- lines.push(` ${chalk10.dim("Name".padEnd(30))} ${chalk10.dim("Technology".padEnd(18))} ${chalk10.dim("Source")}`);
6125
+ const writeSpinner = ora5("Writing config files...").start();
6126
+ try {
6127
+ const result = writeSetup(generatedSetup);
6128
+ writeSpinner.succeed("Config files written");
6129
+ for (const file of result.written) {
6130
+ console.log(` ${chalk10.green("\u2713")} ${file}`);
5973
6131
  }
5974
- lines.push(chalk10.dim(" " + "\u2500".repeat(70)));
5975
- for (let i = 0; i < recs.length; i++) {
5976
- const rec = recs[i];
5977
- const check = selected.has(i) ? chalk10.green("[x]") : "[ ]";
5978
- const ptr = i === cursor ? chalk10.cyan(">") : " ";
5979
- if (hasScores) {
5980
- const scoreColor = rec.score >= 90 ? chalk10.green : rec.score >= 70 ? chalk10.yellow : chalk10.dim;
5981
- lines.push(` ${ptr} ${check} ${scoreColor(String(rec.score).padStart(3))} ${rec.name.padEnd(26)} ${chalk10.dim(rec.reason.slice(0, 40))}`);
5982
- } else {
5983
- lines.push(` ${ptr} ${check} ${rec.name.padEnd(28)} ${rec.detected_technology.padEnd(16)} ${chalk10.dim(rec.source_url || "")}`);
6132
+ if (result.deleted.length > 0) {
6133
+ for (const file of result.deleted) {
6134
+ console.log(` ${chalk10.red("\u2717")} ${file}`);
5984
6135
  }
5985
6136
  }
5986
- lines.push("");
5987
- lines.push(chalk10.dim(" \u2191\u2193 navigate \u23B5 toggle a all n none \u23CE install q cancel"));
5988
- return lines.join("\n");
5989
- }
5990
- function draw(initial) {
5991
- if (!initial && lineCount > 0) {
5992
- stdout.write(`\x1B[${lineCount}A`);
6137
+ if (result.backupDir) {
6138
+ console.log(chalk10.dim(`
6139
+ Backups saved to ${result.backupDir}`));
5993
6140
  }
5994
- stdout.write("\x1B[0J");
5995
- const output = render();
5996
- stdout.write(output + "\n");
5997
- lineCount = output.split("\n").length;
6141
+ } catch (err) {
6142
+ writeSpinner.fail("Failed to write files");
6143
+ console.error(chalk10.red(err instanceof Error ? err.message : "Unknown error"));
6144
+ throw new Error("__exit__");
5998
6145
  }
5999
- return new Promise((resolve2) => {
6000
- console.log("");
6001
- draw(true);
6002
- stdin.setRawMode(true);
6003
- stdin.resume();
6004
- stdin.setEncoding("utf8");
6005
- function cleanup() {
6006
- stdin.removeListener("data", onData);
6007
- stdin.setRawMode(false);
6008
- stdin.pause();
6009
- }
6010
- function onData(key) {
6011
- switch (key) {
6012
- case "\x1B[A":
6013
- cursor = (cursor - 1 + recs.length) % recs.length;
6014
- draw(false);
6015
- break;
6016
- case "\x1B[B":
6017
- cursor = (cursor + 1) % recs.length;
6018
- draw(false);
6019
- break;
6020
- case " ":
6021
- selected.has(cursor) ? selected.delete(cursor) : selected.add(cursor);
6022
- draw(false);
6023
- break;
6024
- case "a":
6025
- recs.forEach((_, i) => selected.add(i));
6026
- draw(false);
6027
- break;
6028
- case "n":
6029
- selected.clear();
6030
- draw(false);
6031
- break;
6032
- case "\r":
6033
- case "\n":
6034
- cleanup();
6035
- if (selected.size === 0) {
6036
- console.log(chalk10.dim("\n No skills selected.\n"));
6037
- resolve2(null);
6038
- } else {
6039
- resolve2(Array.from(selected).sort().map((i) => recs[i]));
6040
- }
6041
- break;
6042
- case "q":
6043
- case "\x1B":
6044
- case "":
6045
- cleanup();
6046
- console.log(chalk10.dim("\n Cancelled.\n"));
6047
- resolve2(null);
6048
- break;
6049
- }
6050
- }
6051
- stdin.on("data", onData);
6146
+ const sha = getCurrentHeadSha();
6147
+ writeState({
6148
+ lastRefreshSha: sha ?? "",
6149
+ lastRefreshTimestamp: (/* @__PURE__ */ new Date()).toISOString(),
6150
+ targetAgent
6052
6151
  });
6053
- }
6054
- async function fetchSkillContent(rec) {
6055
- if (!rec.source_url) return null;
6056
- const repoPath = rec.source_url.replace("https://github.com/", "");
6057
- const candidates = [
6058
- `https://raw.githubusercontent.com/${repoPath}/HEAD/skills/${rec.slug}/SKILL.md`,
6059
- `https://raw.githubusercontent.com/${repoPath}/HEAD/${rec.slug}/SKILL.md`,
6060
- `https://raw.githubusercontent.com/${repoPath}/HEAD/.claude/skills/${rec.slug}/SKILL.md`,
6061
- `https://raw.githubusercontent.com/${repoPath}/HEAD/.agents/skills/${rec.slug}/SKILL.md`
6062
- ];
6063
- for (const url of candidates) {
6152
+ const afterScore = computeLocalScore(process.cwd(), targetAgent);
6153
+ if (afterScore.score < baselineScore.score) {
6154
+ console.log("");
6155
+ console.log(chalk10.yellow(` Score would drop from ${baselineScore.score} to ${afterScore.score} \u2014 reverting changes.`));
6064
6156
  try {
6065
- const resp = await fetch(url, { signal: AbortSignal.timeout(1e4) });
6066
- if (resp.ok) {
6067
- const text = await resp.text();
6068
- if (text.length > 20) return text;
6157
+ const { restored, removed } = undoSetup();
6158
+ if (restored.length > 0 || removed.length > 0) {
6159
+ console.log(chalk10.dim(` Reverted ${restored.length + removed.length} file${restored.length + removed.length === 1 ? "" : "s"} from backup.`));
6069
6160
  }
6070
6161
  } catch {
6071
6162
  }
6163
+ console.log(chalk10.dim(" Run ") + chalk10.hex("#83D1EB")("caliber onboard --force") + chalk10.dim(" to override.\n"));
6164
+ return;
6072
6165
  }
6073
- try {
6074
- const resp = await fetch(
6075
- `https://api.github.com/repos/${repoPath}/git/trees/HEAD?recursive=1`,
6076
- { signal: AbortSignal.timeout(1e4) }
6077
- );
6078
- if (resp.ok) {
6079
- const tree = await resp.json();
6080
- const needle = `${rec.slug}/SKILL.md`;
6081
- const match = tree.tree?.find((f) => f.path.endsWith(needle));
6082
- if (match) {
6083
- const rawUrl = `https://raw.githubusercontent.com/${repoPath}/HEAD/${match.path}`;
6084
- const contentResp = await fetch(rawUrl, { signal: AbortSignal.timeout(1e4) });
6085
- if (contentResp.ok) return await contentResp.text();
6086
- }
6087
- }
6088
- } catch {
6089
- }
6090
- return null;
6091
- }
6092
- async function installSkills(recs, platforms, contentMap) {
6093
- const spinner = ora5(`Installing ${recs.length} skill${recs.length > 1 ? "s" : ""}...`).start();
6094
- const installed = [];
6095
- for (const rec of recs) {
6096
- const content = contentMap.get(rec.slug);
6097
- if (!content) continue;
6098
- for (const platform of platforms) {
6099
- const skillPath = getSkillPath(platform, rec.slug);
6100
- const fullPath = join8(process.cwd(), skillPath);
6101
- mkdirSync(dirname2(fullPath), { recursive: true });
6102
- writeFileSync(fullPath, content, "utf-8");
6103
- installed.push(`[${platform}] ${skillPath}`);
6104
- }
6105
- }
6106
- if (installed.length > 0) {
6107
- spinner.succeed(`Installed ${installed.length} file${installed.length > 1 ? "s" : ""}`);
6108
- for (const p of installed) {
6109
- console.log(chalk10.green(` \u2713 ${p}`));
6110
- }
6111
- } else {
6112
- spinner.fail("No skills were installed");
6113
- }
6114
- console.log("");
6115
- }
6116
- function printRecommendations(recs) {
6117
- const hasScores = recs.some((r) => r.score > 0);
6118
- console.log(chalk10.bold("\n Recommendations\n"));
6119
- if (hasScores) {
6120
- console.log(` ${chalk10.dim("Score".padEnd(7))} ${chalk10.dim("Name".padEnd(28))} ${chalk10.dim("Why")}`);
6121
- } else {
6122
- console.log(` ${chalk10.dim("Name".padEnd(30))} ${chalk10.dim("Technology".padEnd(18))} ${chalk10.dim("Source")}`);
6123
- }
6124
- console.log(chalk10.dim(" " + "\u2500".repeat(70)));
6125
- for (const rec of recs) {
6126
- if (hasScores) {
6127
- console.log(` ${String(rec.score).padStart(3)} ${rec.name.padEnd(26)} ${chalk10.dim(rec.reason.slice(0, 50))}`);
6128
- } else {
6129
- console.log(` ${rec.name.padEnd(28)} ${rec.detected_technology.padEnd(16)} ${chalk10.dim(rec.source_url || "")}`);
6130
- }
6131
- }
6132
- console.log("");
6166
+ displayScoreDelta(baselineScore, afterScore);
6167
+ console.log(chalk10.bold.green(" Regeneration complete!"));
6168
+ console.log(chalk10.dim(" Run ") + chalk10.hex("#83D1EB")("caliber undo") + chalk10.dim(" to revert changes.\n"));
6133
6169
  }
6134
6170
 
6135
6171
  // src/commands/score.ts
@@ -6373,7 +6409,7 @@ async function refreshSingleRepo(repoDir, options) {
6373
6409
  }
6374
6410
  const spinner = quiet ? null : ora6(`${prefix}Analyzing changes...`).start();
6375
6411
  const existingDocs = readExistingConfigs(repoDir);
6376
- const fingerprint = collectFingerprint(repoDir);
6412
+ const fingerprint = await collectFingerprint(repoDir);
6377
6413
  const projectContext = {
6378
6414
  languages: fingerprint.languages,
6379
6415
  frameworks: fingerprint.frameworks,
@@ -7033,7 +7069,7 @@ program.command("undo").description("Revert all config changes made by Caliber")
7033
7069
  program.command("status").description("Show current Caliber setup status").option("--json", "Output as JSON").action(statusCommand);
7034
7070
  program.command("regenerate").alias("regen").alias("re").description("Re-analyze project and regenerate setup").option("--dry-run", "Preview changes without writing files").action(regenerateCommand);
7035
7071
  program.command("config").description("Configure LLM provider, API key, and model").action(configCommand);
7036
- program.command("recommend").description("Discover and install skill recommendations").option("--generate", "Force fresh recommendation search").action(recommendCommand);
7072
+ program.command("skills").description("Discover and install community skills for your project").action(recommendCommand);
7037
7073
  program.command("score").description("Score your current agent config setup (deterministic, no network)").option("--json", "Output as JSON").option("--quiet", "One-line output for scripts/hooks").option("--agent <type>", "Target agents (comma-separated): claude, cursor, codex", parseAgentOption).action(scoreCommand);
7038
7074
  program.command("refresh").description("Update docs based on recent code changes").option("--quiet", "Suppress output (for use in hooks)").option("--dry-run", "Preview changes without writing files").action(refreshCommand);
7039
7075
  program.command("hooks").description("Manage auto-refresh hooks (toggle interactively)").option("--install", "Enable all hooks non-interactively").option("--remove", "Disable all hooks non-interactively").action(hooksCommand);