@caliber-ai/cli 0.14.1 → 0.16.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.
package/dist/bin.js CHANGED
@@ -85,6 +85,7 @@ import ora2 from "ora";
85
85
  import readline from "readline";
86
86
  import select from "@inquirer/select";
87
87
  import fs16 from "fs";
88
+ import { createTwoFilesPatch } from "diff";
88
89
 
89
90
  // src/auth/token-store.ts
90
91
  init_constants();
@@ -2066,26 +2067,38 @@ function collectSetupFiles(setup) {
2066
2067
  }
2067
2068
  return files;
2068
2069
  }
2069
- function previewFileContent(filePath, content, maxLines = 25) {
2070
+ function previewNewFile(filePath, content, maxLines = 40) {
2070
2071
  const lines = content.split("\n");
2071
2072
  const displayLines = lines.slice(0, maxLines);
2072
- const maxWidth = Math.max(60, ...displayLines.map((l) => l.length + 4));
2073
- const width = Math.min(maxWidth, 80);
2074
2073
  console.log("");
2075
- console.log(` ${chalk3.dim("\u250C\u2500")} ${chalk3.bold(filePath)} ${chalk3.dim("\u2500".repeat(Math.max(0, width - filePath.length - 5)) + "\u2510")}`);
2076
- console.log(` ${chalk3.dim("\u2502")}${" ".repeat(width - 1)}${chalk3.dim("\u2502")}`);
2074
+ console.log(` ${chalk3.green("+ new")} ${chalk3.bold(filePath)}`);
2075
+ console.log(chalk3.dim(" \u2500".repeat(30)));
2077
2076
  for (const line of displayLines) {
2078
- const truncated = line.length > width - 5 ? line.slice(0, width - 8) + "..." : line;
2079
- const padding = " ".repeat(Math.max(0, width - truncated.length - 3));
2080
- console.log(` ${chalk3.dim("\u2502")} ${truncated}${padding}${chalk3.dim("\u2502")}`);
2077
+ console.log(` ${chalk3.green("+")} ${line}`);
2081
2078
  }
2082
2079
  if (lines.length > maxLines) {
2083
- console.log(` ${chalk3.dim("\u2502")}${" ".repeat(width - 1)}${chalk3.dim("\u2502")}`);
2084
- const note = `(showing first ${maxLines} lines, full file is ${lines.length} lines)`;
2085
- const notePadding = " ".repeat(Math.max(0, width - note.length - 3));
2086
- console.log(` ${chalk3.dim("\u2502")} ${chalk3.dim(note)}${notePadding}${chalk3.dim("\u2502")}`);
2080
+ console.log("");
2081
+ console.log(chalk3.dim(` ... ${lines.length - maxLines} more lines (${lines.length} total)`));
2082
+ }
2083
+ console.log("");
2084
+ }
2085
+ function previewDiff(filePath, oldContent, newContent) {
2086
+ const patch = createTwoFilesPatch(filePath, filePath, oldContent, newContent, "current", "proposed", { context: 3 });
2087
+ const patchLines = patch.split("\n").slice(2);
2088
+ console.log("");
2089
+ console.log(` ${chalk3.yellow("~ modified")} ${chalk3.bold(filePath)}`);
2090
+ console.log(chalk3.dim(" \u2500".repeat(30)));
2091
+ for (const line of patchLines) {
2092
+ if (line.startsWith("@@")) {
2093
+ console.log(` ${chalk3.cyan(line)}`);
2094
+ } else if (line.startsWith("+")) {
2095
+ console.log(` ${chalk3.green(line)}`);
2096
+ } else if (line.startsWith("-")) {
2097
+ console.log(` ${chalk3.red(line)}`);
2098
+ } else {
2099
+ console.log(` ${chalk3.dim(line)}`);
2100
+ }
2087
2101
  }
2088
- console.log(` ${chalk3.dim("\u2514" + "\u2500".repeat(width - 1) + "\u2518")}`);
2089
2102
  console.log("");
2090
2103
  }
2091
2104
  async function promptFilePreview(setup) {
@@ -2094,11 +2107,19 @@ async function promptFilePreview(setup) {
2094
2107
  console.log(chalk3.dim("\n No files to preview.\n"));
2095
2108
  return;
2096
2109
  }
2097
- const choice = await select({
2098
- message: "Which file to preview?",
2099
- choices: files.map((f, i) => ({ name: f.path, value: i }))
2110
+ const choices = files.map((f, i) => {
2111
+ const exists = fs16.existsSync(f.path);
2112
+ const icon = exists ? chalk3.yellow("~") : chalk3.green("+");
2113
+ return { name: `${icon} ${f.path}`, value: i };
2100
2114
  });
2101
- previewFileContent(files[choice].path, files[choice].content);
2115
+ const choice = await select({ message: "Which file to preview?", choices });
2116
+ const file = files[choice];
2117
+ if (fs16.existsSync(file.path)) {
2118
+ const existing = fs16.readFileSync(file.path, "utf-8");
2119
+ previewDiff(file.path, existing, file.content);
2120
+ } else {
2121
+ previewNewFile(file.path, file.content);
2122
+ }
2102
2123
  }
2103
2124
 
2104
2125
  // src/commands/undo.ts
@@ -2297,7 +2318,21 @@ function logoutCommand() {
2297
2318
  import chalk8 from "chalk";
2298
2319
  import ora5 from "ora";
2299
2320
  import { mkdirSync, writeFileSync } from "fs";
2300
- import { join } from "path";
2321
+ import { join, dirname as dirname2 } from "path";
2322
+ function detectLocalPlatforms() {
2323
+ const items = scanLocalState(process.cwd());
2324
+ const platforms = /* @__PURE__ */ new Set();
2325
+ for (const item of items) {
2326
+ platforms.add(item.platform);
2327
+ }
2328
+ return platforms.size > 0 ? Array.from(platforms) : ["claude"];
2329
+ }
2330
+ function getSkillPath(platform, slug) {
2331
+ if (platform === "cursor") {
2332
+ return join(".cursor", "rules", `${slug}.mdc`);
2333
+ }
2334
+ return join(".claude", "skills", `${slug}.md`);
2335
+ }
2301
2336
  async function recommendCommand(options) {
2302
2337
  const auth2 = getStoredAuth();
2303
2338
  if (!auth2) {
@@ -2315,44 +2350,57 @@ async function recommendCommand(options) {
2315
2350
  throw new Error("__exit__");
2316
2351
  }
2317
2352
  const projectId = match.project.id;
2318
- if (options.generate) {
2319
- const spinner = ora5("Detecting technologies and searching for skills...").start();
2320
- let recs2;
2321
- try {
2322
- recs2 = await apiRequest(
2323
- `/api/recommendations/project/${projectId}/generate`,
2324
- { method: "POST" }
2325
- );
2326
- spinner.succeed(`Found ${recs2?.length || 0} recommendations`);
2327
- } catch (err) {
2328
- spinner.fail("Failed to generate recommendations");
2329
- throw err;
2353
+ const platforms = detectLocalPlatforms();
2354
+ if (options.status) {
2355
+ const recs2 = await apiRequest(
2356
+ `/api/recommendations/project/${projectId}?status=${options.status}`
2357
+ );
2358
+ if (!recs2?.length) {
2359
+ console.log(chalk8.dim(`
2360
+ No ${options.status} recommendations.
2361
+ `));
2362
+ return;
2330
2363
  }
2364
+ printRecommendations(recs2);
2365
+ return;
2366
+ }
2367
+ if (options.generate) {
2368
+ const recs2 = await generateRecommendations(projectId);
2331
2369
  if (recs2?.length) {
2332
- const selected = await interactiveSelect(recs2);
2333
- if (selected?.length) {
2334
- await installSkills(selected);
2370
+ const selected2 = await interactiveSelect(recs2);
2371
+ if (selected2?.length) {
2372
+ await installSkills(selected2, platforms);
2335
2373
  }
2336
2374
  }
2337
2375
  return;
2338
2376
  }
2339
- const statusFilter = options.status || "pending";
2340
- const recs = await apiRequest(
2341
- `/api/recommendations/project/${projectId}?status=${statusFilter}`
2377
+ let recs = await apiRequest(
2378
+ `/api/recommendations/project/${projectId}?status=pending`
2342
2379
  );
2343
2380
  if (!recs?.length) {
2344
- console.log(chalk8.dim(`
2345
- No ${statusFilter} recommendations. Run \`caliber recommend --generate\` to discover skills.
2346
- `));
2381
+ recs = await generateRecommendations(projectId);
2382
+ }
2383
+ if (!recs?.length) {
2384
+ console.log(chalk8.dim("\nNo recommendations found for this project.\n"));
2347
2385
  return;
2348
2386
  }
2349
- if (statusFilter === "pending") {
2350
- const selected = await interactiveSelect(recs);
2351
- if (selected?.length) {
2352
- await installSkills(selected);
2353
- }
2354
- } else {
2355
- printRecommendations(recs);
2387
+ const selected = await interactiveSelect(recs);
2388
+ if (selected?.length) {
2389
+ await installSkills(selected, platforms);
2390
+ }
2391
+ }
2392
+ async function generateRecommendations(projectId) {
2393
+ const spinner = ora5("Detecting technologies and searching for skills...").start();
2394
+ try {
2395
+ const recs = await apiRequest(
2396
+ `/api/recommendations/project/${projectId}/generate`,
2397
+ { method: "POST" }
2398
+ );
2399
+ spinner.succeed(`Found ${recs?.length || 0} recommendations`);
2400
+ return recs;
2401
+ } catch (err) {
2402
+ spinner.fail("Failed to generate recommendations");
2403
+ throw err;
2356
2404
  }
2357
2405
  }
2358
2406
  async function interactiveSelect(recs) {
@@ -2445,24 +2493,32 @@ async function interactiveSelect(recs) {
2445
2493
  stdin.on("data", onData);
2446
2494
  });
2447
2495
  }
2448
- async function installSkills(recs) {
2496
+ async function installSkills(recs, platforms) {
2449
2497
  const spinner = ora5(`Installing ${recs.length} skill${recs.length > 1 ? "s" : ""}...`).start();
2450
- const skillsDir = join(process.cwd(), ".claude", "skills");
2451
- mkdirSync(skillsDir, { recursive: true });
2452
2498
  const installed = [];
2453
2499
  const warnings = [];
2454
2500
  for (const rec of recs) {
2455
- try {
2456
- const result = await apiRequest(
2457
- `/api/recommendations/${rec.id}/content`
2458
- );
2459
- if (!result?.content) {
2460
- warnings.push(`No content available for ${rec.skill_name}`);
2461
- continue;
2501
+ let accepted = false;
2502
+ for (const platform of platforms) {
2503
+ try {
2504
+ const result = await apiRequest(
2505
+ `/api/recommendations/${rec.id}/content?platform=${platform}`
2506
+ );
2507
+ if (!result?.content) {
2508
+ warnings.push(`[${platform}] No content available for ${rec.skill_name}`);
2509
+ continue;
2510
+ }
2511
+ const skillPath = getSkillPath(platform, rec.skill_slug);
2512
+ const fullPath = join(process.cwd(), skillPath);
2513
+ mkdirSync(dirname2(fullPath), { recursive: true });
2514
+ writeFileSync(fullPath, result.content, "utf-8");
2515
+ installed.push(`[${platform}] ${skillPath}`);
2516
+ accepted = true;
2517
+ } catch {
2518
+ warnings.push(`[${platform}] Failed to fetch ${rec.skill_name}`);
2462
2519
  }
2463
- const filename = `${rec.skill_slug}.md`;
2464
- writeFileSync(join(skillsDir, filename), result.content, "utf-8");
2465
- installed.push(join(".claude", "skills", filename));
2520
+ }
2521
+ if (accepted) {
2466
2522
  try {
2467
2523
  await apiRequest(`/api/recommendations/${rec.id}/status`, {
2468
2524
  method: "PUT",
@@ -2470,12 +2526,10 @@ async function installSkills(recs) {
2470
2526
  });
2471
2527
  } catch {
2472
2528
  }
2473
- } catch {
2474
- warnings.push(`Failed to install ${rec.skill_name}`);
2475
2529
  }
2476
2530
  }
2477
2531
  if (installed.length > 0) {
2478
- spinner.succeed(`Installed ${installed.length} skill${installed.length > 1 ? "s" : ""}`);
2532
+ spinner.succeed(`Installed ${installed.length} file${installed.length > 1 ? "s" : ""}`);
2479
2533
  for (const p of installed) {
2480
2534
  console.log(chalk8.green(` \u2713 ${p}`));
2481
2535
  }
@@ -3276,7 +3330,7 @@ program.command("status").description("Show current Caliber setup status").optio
3276
3330
  program.command("regenerate").alias("regen").alias("re").alias("update").description("Re-analyze project and regenerate setup").option("--dry-run", "Preview changes without writing files").action(regenerateCommand);
3277
3331
  program.command("login").description("Authenticate with Caliber").action(loginCommand);
3278
3332
  program.command("logout").description("Clear stored credentials").action(logoutCommand);
3279
- program.command("recommend").description("Discover and manage skill recommendations").option("--generate", "Generate new recommendations").option("--status <status>", "Filter by status: pending, accepted, dismissed").action(recommendCommand);
3333
+ program.command("recommend").description("Discover and manage skill recommendations").option("--generate", "Force fresh recommendation generation").option("--status <status>", "View recommendations by status: pending, accepted, dismissed").action(recommendCommand);
3280
3334
  program.command("health").description("Analyze context health and quality").option("--fix", "Generate and execute a fix plan").option("--json", "Output as JSON").action(healthCommand);
3281
3335
  program.command("sync").description("Sync local config with server state").option("--platform <platform>", "Target platform: claude, cursor, or both").option("--dry-run", "Preview changes without writing files").action(syncCommand);
3282
3336
  program.command("diff").description("Compare local config with server state").option("--platform <platform>", "Target platform: claude, cursor, or both").action(diffCommand);