import chalk from "chalk"; import type { Command } from "commander"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { buildWorkspaceSkillStatus, type SkillStatusEntry, type SkillStatusReport, } from "../agents/skills-status.js"; import { loadConfig } from "../config/config.js"; import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; export type SkillsListOptions = { json?: boolean; eligible?: boolean; verbose?: boolean; }; export type SkillInfoOptions = { json?: boolean; }; export type SkillsCheckOptions = { json?: boolean; }; function appendClawdHubHint(output: string, json?: boolean): string { if (json) return output; return `${output}\n\nTip: use \`npx clawdhub\` to search, install, and sync skills.`; } /** * Format a single skill for display in the list */ function formatSkillLine(skill: SkillStatusEntry, verbose = false): string { const emoji = skill.emoji ?? "📦"; const status = skill.eligible ? chalk.green("✓") : skill.disabled ? chalk.yellow("disabled") : skill.blockedByAllowlist ? chalk.yellow("blocked") : chalk.red("missing reqs"); const name = skill.eligible ? chalk.white(skill.name) : chalk.gray(skill.name); const desc = chalk.gray( skill.description.length > 50 ? `${skill.description.slice(0, 47)}...` : skill.description, ); if (verbose) { const missing: string[] = []; if (skill.missing.bins.length > 0) { missing.push(`bins: ${skill.missing.bins.join(", ")}`); } if (skill.missing.anyBins.length > 0) { missing.push(`anyBins: ${skill.missing.anyBins.join(", ")}`); } if (skill.missing.env.length > 0) { missing.push(`env: ${skill.missing.env.join(", ")}`); } if (skill.missing.config.length > 0) { missing.push(`config: ${skill.missing.config.join(", ")}`); } if (skill.missing.os.length > 0) { missing.push(`os: ${skill.missing.os.join(", ")}`); } const missingStr = missing.length > 0 ? chalk.red(` [${missing.join("; ")}]`) : ""; return `${emoji} ${name} ${status}${missingStr}\n ${desc}`; } return `${emoji} ${name} ${status} - ${desc}`; } /** * Format the skills list output */ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOptions): string { const skills = opts.eligible ? report.skills.filter((s) => s.eligible) : report.skills; if (opts.json) { const jsonReport = { workspaceDir: report.workspaceDir, managedSkillsDir: report.managedSkillsDir, skills: skills.map((s) => ({ name: s.name, description: s.description, emoji: s.emoji, eligible: s.eligible, disabled: s.disabled, blockedByAllowlist: s.blockedByAllowlist, source: s.source, primaryEnv: s.primaryEnv, homepage: s.homepage, missing: s.missing, })), }; return JSON.stringify(jsonReport, null, 2); } if (skills.length === 0) { const message = opts.eligible ? "No eligible skills found. Run `clawdbot skills list` to see all skills." : "No skills found."; return appendClawdHubHint(message, opts.json); } const eligible = skills.filter((s) => s.eligible); const notEligible = skills.filter((s) => !s.eligible); const lines: string[] = []; lines.push( chalk.bold.cyan("Skills") + chalk.gray(` (${eligible.length}/${skills.length} ready)`), ); lines.push(""); if (eligible.length > 0) { lines.push(chalk.bold.green("Ready:")); for (const skill of eligible) { lines.push(` ${formatSkillLine(skill, opts.verbose)}`); } } if (notEligible.length > 0 && !opts.eligible) { if (eligible.length > 0) lines.push(""); lines.push(chalk.bold.yellow("Not ready:")); for (const skill of notEligible) { lines.push(` ${formatSkillLine(skill, opts.verbose)}`); } } return appendClawdHubHint(lines.join("\n"), opts.json); } /** * Format detailed info for a single skill */ export function formatSkillInfo( report: SkillStatusReport, skillName: string, opts: SkillInfoOptions, ): string { const skill = report.skills.find((s) => s.name === skillName || s.skillKey === skillName); if (!skill) { if (opts.json) { return JSON.stringify({ error: "not found", skill: skillName }, null, 2); } return appendClawdHubHint( `Skill "${skillName}" not found. Run \`clawdbot skills list\` to see available skills.`, opts.json, ); } if (opts.json) { return JSON.stringify(skill, null, 2); } const lines: string[] = []; const emoji = skill.emoji ?? "📦"; const status = skill.eligible ? chalk.green("✓ Ready") : skill.disabled ? chalk.yellow("⏸ Disabled") : skill.blockedByAllowlist ? chalk.yellow("🚫 Blocked by allowlist") : chalk.red("✗ Missing requirements"); lines.push(`${emoji} ${chalk.bold.cyan(skill.name)} ${status}`); lines.push(""); lines.push(chalk.white(skill.description)); lines.push(""); // Details lines.push(chalk.bold("Details:")); lines.push(` Source: ${skill.source}`); lines.push(` Path: ${chalk.gray(skill.filePath)}`); if (skill.homepage) { lines.push(` Homepage: ${chalk.blue(skill.homepage)}`); } if (skill.primaryEnv) { lines.push(` Primary env: ${skill.primaryEnv}`); } // Requirements const hasRequirements = skill.requirements.bins.length > 0 || skill.requirements.anyBins.length > 0 || skill.requirements.env.length > 0 || skill.requirements.config.length > 0 || skill.requirements.os.length > 0; if (hasRequirements) { lines.push(""); lines.push(chalk.bold("Requirements:")); if (skill.requirements.bins.length > 0) { const binsStatus = skill.requirements.bins.map((bin) => { const missing = skill.missing.bins.includes(bin); return missing ? chalk.red(`✗ ${bin}`) : chalk.green(`✓ ${bin}`); }); lines.push(` Binaries: ${binsStatus.join(", ")}`); } if (skill.requirements.anyBins.length > 0) { const anyBinsMissing = skill.missing.anyBins.length > 0; const anyBinsStatus = skill.requirements.anyBins.map((bin) => { const missing = anyBinsMissing; return missing ? chalk.red(`✗ ${bin}`) : chalk.green(`✓ ${bin}`); }); lines.push(` Any binaries: ${anyBinsStatus.join(", ")}`); } if (skill.requirements.env.length > 0) { const envStatus = skill.requirements.env.map((env) => { const missing = skill.missing.env.includes(env); return missing ? chalk.red(`✗ ${env}`) : chalk.green(`✓ ${env}`); }); lines.push(` Environment: ${envStatus.join(", ")}`); } if (skill.requirements.config.length > 0) { const configStatus = skill.requirements.config.map((cfg) => { const missing = skill.missing.config.includes(cfg); return missing ? chalk.red(`✗ ${cfg}`) : chalk.green(`✓ ${cfg}`); }); lines.push(` Config: ${configStatus.join(", ")}`); } if (skill.requirements.os.length > 0) { const osStatus = skill.requirements.os.map((osName) => { const missing = skill.missing.os.includes(osName); return missing ? chalk.red(`✗ ${osName}`) : chalk.green(`✓ ${osName}`); }); lines.push(` OS: ${osStatus.join(", ")}`); } } // Install options if (skill.install.length > 0 && !skill.eligible) { lines.push(""); lines.push(chalk.bold("Install options:")); for (const inst of skill.install) { lines.push(` ${chalk.yellow("→")} ${inst.label}`); } } return appendClawdHubHint(lines.join("\n"), opts.json); } /** * Format a check/summary of all skills status */ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOptions): string { const eligible = report.skills.filter((s) => s.eligible); const disabled = report.skills.filter((s) => s.disabled); const blocked = report.skills.filter((s) => s.blockedByAllowlist && !s.disabled); const missingReqs = report.skills.filter( (s) => !s.eligible && !s.disabled && !s.blockedByAllowlist, ); if (opts.json) { return JSON.stringify( { summary: { total: report.skills.length, eligible: eligible.length, disabled: disabled.length, blocked: blocked.length, missingRequirements: missingReqs.length, }, eligible: eligible.map((s) => s.name), disabled: disabled.map((s) => s.name), blocked: blocked.map((s) => s.name), missingRequirements: missingReqs.map((s) => ({ name: s.name, missing: s.missing, install: s.install, })), }, null, 2, ); } const lines: string[] = []; lines.push(chalk.bold.cyan("Skills Status Check")); lines.push(""); lines.push(`Total: ${report.skills.length}`); lines.push(`${chalk.green("✓")} Eligible: ${eligible.length}`); lines.push(`${chalk.yellow("⏸")} Disabled: ${disabled.length}`); lines.push(`${chalk.yellow("🚫")} Blocked by allowlist: ${blocked.length}`); lines.push(`${chalk.red("✗")} Missing requirements: ${missingReqs.length}`); if (eligible.length > 0) { lines.push(""); lines.push(chalk.bold.green("Ready to use:")); for (const skill of eligible) { const emoji = skill.emoji ?? "📦"; lines.push(` ${emoji} ${skill.name}`); } } if (missingReqs.length > 0) { lines.push(""); lines.push(chalk.bold.red("Missing requirements:")); for (const skill of missingReqs) { const emoji = skill.emoji ?? "📦"; const missing: string[] = []; if (skill.missing.bins.length > 0) { missing.push(`bins: ${skill.missing.bins.join(", ")}`); } if (skill.missing.anyBins.length > 0) { missing.push(`anyBins: ${skill.missing.anyBins.join(", ")}`); } if (skill.missing.env.length > 0) { missing.push(`env: ${skill.missing.env.join(", ")}`); } if (skill.missing.config.length > 0) { missing.push(`config: ${skill.missing.config.join(", ")}`); } if (skill.missing.os.length > 0) { missing.push(`os: ${skill.missing.os.join(", ")}`); } lines.push(` ${emoji} ${skill.name} ${chalk.gray(`(${missing.join("; ")})`)}`); } } return appendClawdHubHint(lines.join("\n"), opts.json); } /** * Register the skills CLI commands */ export function registerSkillsCli(program: Command) { const skills = program .command("skills") .description("List and inspect available skills") .addHelpText( "after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/skills", "docs.clawd.bot/cli/skills")}\n`, ); skills .command("list") .description("List all available skills") .option("--json", "Output as JSON", false) .option("--eligible", "Show only eligible (ready to use) skills", false) .option("-v, --verbose", "Show more details including missing requirements", false) .action(async (opts) => { try { const config = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const report = buildWorkspaceSkillStatus(workspaceDir, { config }); console.log(formatSkillsList(report, opts)); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); skills .command("info") .description("Show detailed information about a skill") .argument("", "Skill name") .option("--json", "Output as JSON", false) .action(async (name, opts) => { try { const config = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const report = buildWorkspaceSkillStatus(workspaceDir, { config }); console.log(formatSkillInfo(report, name, opts)); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); skills .command("check") .description("Check which skills are ready vs missing requirements") .option("--json", "Output as JSON", false) .action(async (opts) => { try { const config = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const report = buildWorkspaceSkillStatus(workspaceDir, { config }); console.log(formatSkillsCheck(report, opts)); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); // Default action (no subcommand) - show list skills.action(async () => { try { const config = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const report = buildWorkspaceSkillStatus(workspaceDir, { config }); console.log(formatSkillsList(report, {})); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); }