@rely-ai/caliber 1.4.2 → 1.5.0

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