@caliber-ai/cli 0.9.0 → 0.10.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
@@ -2132,6 +2132,8 @@ function logoutCommand() {
2132
2132
  // src/commands/recommend.ts
2133
2133
  import chalk7 from "chalk";
2134
2134
  import ora5 from "ora";
2135
+ import { mkdirSync, writeFileSync } from "fs";
2136
+ import { join } from "path";
2135
2137
  async function recommendCommand(options) {
2136
2138
  const auth2 = getStoredAuth();
2137
2139
  if (!auth2) {
@@ -2151,20 +2153,23 @@ async function recommendCommand(options) {
2151
2153
  const projectId = match.project.id;
2152
2154
  if (options.generate) {
2153
2155
  const spinner = ora5("Detecting technologies and searching for skills...").start();
2156
+ let recs2;
2154
2157
  try {
2155
- const recs2 = await apiRequest(
2158
+ recs2 = await apiRequest(
2156
2159
  `/api/recommendations/project/${projectId}/generate`,
2157
2160
  { method: "POST" }
2158
2161
  );
2159
2162
  spinner.succeed(`Found ${recs2?.length || 0} recommendations`);
2160
- if (recs2?.length) {
2161
- console.log("");
2162
- printRecommendations(recs2);
2163
- }
2164
2163
  } catch (err) {
2165
2164
  spinner.fail("Failed to generate recommendations");
2166
2165
  throw err;
2167
2166
  }
2167
+ if (recs2?.length) {
2168
+ const selected = await interactiveSelect(recs2);
2169
+ if (selected?.length) {
2170
+ await installSkills(selected);
2171
+ }
2172
+ }
2168
2173
  return;
2169
2174
  }
2170
2175
  const statusFilter = options.status || "pending";
@@ -2177,7 +2182,146 @@ No ${statusFilter} recommendations. Run \`caliber recommend --generate\` to disc
2177
2182
  `));
2178
2183
  return;
2179
2184
  }
2180
- printRecommendations(recs);
2185
+ if (statusFilter === "pending") {
2186
+ const selected = await interactiveSelect(recs);
2187
+ if (selected?.length) {
2188
+ await installSkills(selected);
2189
+ }
2190
+ } else {
2191
+ printRecommendations(recs);
2192
+ }
2193
+ }
2194
+ async function interactiveSelect(recs) {
2195
+ if (!process.stdin.isTTY) {
2196
+ printRecommendations(recs);
2197
+ return null;
2198
+ }
2199
+ const selected = /* @__PURE__ */ new Set();
2200
+ let cursor = 0;
2201
+ const { stdin, stdout } = process;
2202
+ let lineCount = 0;
2203
+ function render() {
2204
+ const lines = [];
2205
+ lines.push(chalk7.bold(" Skill Recommendations"));
2206
+ lines.push("");
2207
+ lines.push(` ${chalk7.dim("Name".padEnd(30))} ${chalk7.dim("Score".padEnd(8))} ${chalk7.dim("Technology")}`);
2208
+ lines.push(chalk7.dim(" " + "\u2500".repeat(55)));
2209
+ for (let i = 0; i < recs.length; i++) {
2210
+ const rec = recs[i];
2211
+ const check = selected.has(i) ? chalk7.green("[x]") : "[ ]";
2212
+ const ptr = i === cursor ? chalk7.cyan("\u276F") : " ";
2213
+ const scoreColor = rec.score >= 90 ? chalk7.green : rec.score >= 70 ? chalk7.blue : chalk7.yellow;
2214
+ lines.push(` ${ptr} ${check} ${rec.skill_name.padEnd(26)} ${scoreColor(`${rec.score}%`.padEnd(8))} ${rec.detected_technology}`);
2215
+ }
2216
+ lines.push("");
2217
+ lines.push(chalk7.dim(" \u2191\u2193 navigate \u23B5 toggle a all n none \u23CE install q cancel"));
2218
+ return lines.join("\n");
2219
+ }
2220
+ function draw(initial) {
2221
+ if (!initial && lineCount > 0) {
2222
+ stdout.write(`\x1B[${lineCount}A`);
2223
+ }
2224
+ stdout.write("\x1B[0J");
2225
+ const output = render();
2226
+ stdout.write(output + "\n");
2227
+ lineCount = output.split("\n").length;
2228
+ }
2229
+ return new Promise((resolve2) => {
2230
+ console.log("");
2231
+ draw(true);
2232
+ stdin.setRawMode(true);
2233
+ stdin.resume();
2234
+ stdin.setEncoding("utf8");
2235
+ function cleanup() {
2236
+ stdin.removeListener("data", onData);
2237
+ stdin.setRawMode(false);
2238
+ stdin.pause();
2239
+ }
2240
+ function onData(key) {
2241
+ switch (key) {
2242
+ case "\x1B[A":
2243
+ cursor = (cursor - 1 + recs.length) % recs.length;
2244
+ draw(false);
2245
+ break;
2246
+ case "\x1B[B":
2247
+ cursor = (cursor + 1) % recs.length;
2248
+ draw(false);
2249
+ break;
2250
+ case " ":
2251
+ selected.has(cursor) ? selected.delete(cursor) : selected.add(cursor);
2252
+ draw(false);
2253
+ break;
2254
+ case "a":
2255
+ recs.forEach((_, i) => selected.add(i));
2256
+ draw(false);
2257
+ break;
2258
+ case "n":
2259
+ selected.clear();
2260
+ draw(false);
2261
+ break;
2262
+ case "\r":
2263
+ case "\n":
2264
+ cleanup();
2265
+ if (selected.size === 0) {
2266
+ console.log(chalk7.dim("\n No skills selected.\n"));
2267
+ resolve2(null);
2268
+ } else {
2269
+ resolve2(Array.from(selected).sort().map((i) => recs[i]));
2270
+ }
2271
+ break;
2272
+ case "q":
2273
+ case "\x1B":
2274
+ case "":
2275
+ cleanup();
2276
+ console.log(chalk7.dim("\n Cancelled.\n"));
2277
+ resolve2(null);
2278
+ break;
2279
+ }
2280
+ }
2281
+ stdin.on("data", onData);
2282
+ });
2283
+ }
2284
+ async function installSkills(recs) {
2285
+ const spinner = ora5(`Installing ${recs.length} skill${recs.length > 1 ? "s" : ""}...`).start();
2286
+ const skillsDir = join(process.cwd(), ".claude", "skills");
2287
+ mkdirSync(skillsDir, { recursive: true });
2288
+ const installed = [];
2289
+ const warnings = [];
2290
+ for (const rec of recs) {
2291
+ try {
2292
+ const result = await apiRequest(
2293
+ `/api/recommendations/${rec.id}/content`
2294
+ );
2295
+ if (!result?.content) {
2296
+ warnings.push(`No content available for ${rec.skill_name}`);
2297
+ continue;
2298
+ }
2299
+ const filename = `${rec.skill_slug}.md`;
2300
+ writeFileSync(join(skillsDir, filename), result.content, "utf-8");
2301
+ installed.push(join(".claude", "skills", filename));
2302
+ try {
2303
+ await apiRequest(`/api/recommendations/${rec.id}/status`, {
2304
+ method: "PUT",
2305
+ body: { status: "accepted" }
2306
+ });
2307
+ } catch {
2308
+ }
2309
+ } catch {
2310
+ warnings.push(`Failed to install ${rec.skill_name}`);
2311
+ }
2312
+ }
2313
+ if (installed.length > 0) {
2314
+ spinner.succeed(`Installed ${installed.length} skill${installed.length > 1 ? "s" : ""}`);
2315
+ for (const p of installed) {
2316
+ console.log(chalk7.green(` \u2713 ${p}`));
2317
+ }
2318
+ } else {
2319
+ spinner.fail("No skills were installed");
2320
+ }
2321
+ for (const w of warnings) {
2322
+ console.log(chalk7.yellow(` \u26A0 ${w}`));
2323
+ }
2324
+ console.log("");
2181
2325
  }
2182
2326
  function printRecommendations(recs) {
2183
2327
  console.log(chalk7.bold("\n Skill Recommendations\n"));
@@ -2961,6 +3105,7 @@ function promptYesNo(question) {
2961
3105
  });
2962
3106
  }
2963
3107
  async function checkForUpdates() {
3108
+ if (process.env.CALIBER_SKIP_UPDATE_CHECK) return;
2964
3109
  try {
2965
3110
  const controller = new AbortController();
2966
3111
  const timeout = setTimeout(() => controller.abort(), 2e3);
@@ -3004,7 +3149,8 @@ Update available: ${current} -> ${latest}`)
3004
3149
  Restarting: caliber ${args.join(" ")}
3005
3150
  `));
3006
3151
  execSync4(`caliber ${args.map((a) => JSON.stringify(a)).join(" ")}`, {
3007
- stdio: "inherit"
3152
+ stdio: "inherit",
3153
+ env: { ...process.env, CALIBER_SKIP_UPDATE_CHECK: "1" }
3008
3154
  });
3009
3155
  process.exit(0);
3010
3156
  } catch {