@caliber-ai/cli 0.9.1 → 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 +150 -6
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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"));
|