405 lines
13 KiB
TypeScript
405 lines
13 KiB
TypeScript
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("<name>", "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);
|
|
}
|
|
});
|
|
}
|