UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,098 lines (1,097 loc) 45.6 kB
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { r as stripAnsi, t as sanitizeForLog } from "./ansi-BI9w76Cm.js"; import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js"; import { t as formatDocsLink } from "./links-CsLBrRff.js"; import { r as theme } from "./theme-vjDs9tao.js"; import { s as sanitizeHostExecEnv } from "./host-env-security-RnaFYKhk.js"; import { g as shortenHomePath, p as resolveUserPath, t as CONFIG_DIR } from "./utils-CCC-BEJH.js"; import { n as defaultRuntime } from "./runtime-B4lgFmsS.js"; import { l as fetchClawHubSkillCard, p as fetchClawHubSkillVerification } from "./clawhub-CzBrlwb2.js"; import { m as writeJson } from "./json-files-2umMHm0W.js"; import { n as isImmutableGitCommitRef, r as parseGitPluginSpec } from "./git-install-BdZ_4rl0.js"; import { l as resolveAgentIdByWorkspacePath } from "./agent-scope-MrLta7Pq.js"; import { c as resolveDefaultAgentId, o as resolveAgentWorkspaceDir } from "./agent-scope-config-CgCYpZfK.js"; import { r as runCommandWithTimeout } from "./exec-DubsSJS2.js"; import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js"; import { s as withTempDir } from "./install-source-utils-Zb798u54.js"; import "./config-C9RxTsn1.js"; import { o as redactSensitiveUrlLikeString } from "./redact-sensitive-url-Cf2Fdzd5.js"; import { s as parseFrontmatter } from "./config-CEJFxFUy.js"; import { a as readTrackedClawHubSkillSlugs, c as untrackClawHubSkill, f as validateRequestedSkillSlug, l as updateSkillsFromClawHub, n as resolveSkillStatusEntry, o as resolveClawHubSkillVerificationTarget, r as installSkillFromClawHub, s as searchSkillsFromClawHub, u as installExtractedSkillRoot } from "./status-Dlj2eT9o.js"; import { a as proposeUpdateSkill, c as readSkillProposalDraftFile, d as reviseSkillProposal, i as proposeCreateSkill, l as rejectSkillProposal, n as inspectSkillProposal, o as quarantineSkillProposal, r as listSkillProposals, s as readSkillProposalDraftDirectory, t as applySkillProposal } from "./service-BlZrnDY7.js"; import { n as decorativePrefix, t as decorativeEmoji } from "./decorative-emoji-C7-_uAV4.js"; import { n as renderTable, t as getTerminalTableWidth } from "./table-CpFtGZut.js"; import { t as resolveOptionFromCommand } from "./cli-utils-Cuzzfy1Q.js"; import { r as parseStrictPositiveIntOption } from "./helpers-gBVG4H2O.js"; import path from "node:path"; import fs from "node:fs/promises"; //#region src/skills/lifecycle/source-install.ts const SKILL_SOURCE_ORIGIN_RELATIVE_PATH = path.join(".openclaw", "source-origin.json"); const DEFAULT_GIT_TIMEOUT_MS = 12e4; function createGitCommandEnv() { return sanitizeHostExecEnv({ baseEnv: { ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_TERMINAL_PROMPT: "0" }, blockPathOverrides: false }); } function formatGitCommandFailure(params) { const detail = sanitizeForLog(redactSensitiveUrlLikeString(params.stderr.trim() || params.stdout.trim() || "git failed")); return `failed to ${params.action} ${sanitizeForLog(redactSensitiveUrlLikeString(params.label))}: ${detail}`; } async function runGitCommand(params) { const result = await runCommandWithTimeout(params.argv, { baseEnv: {}, cwd: params.cwd, timeoutMs: params.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS, env: createGitCommandEnv() }); if (result.code !== 0) return { ok: false, error: formatGitCommandFailure({ action: params.action, label: params.label, stdout: result.stdout, stderr: result.stderr }) }; return { ok: true, stdout: result.stdout }; } async function resolveGitCommitish(params) { const candidates = params.ref.startsWith("origin/") ? [params.ref] : [params.ref, `origin/${params.ref}`]; for (const candidate of candidates) { const resolved = await runCommandWithTimeout([ "git", "rev-parse", "--verify", "--quiet", `${candidate}^{commit}` ], { baseEnv: {}, cwd: params.repoDir, timeoutMs: params.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS, env: createGitCommandEnv() }); const commit = normalizeOptionalString(resolved.stdout); if (resolved.code === 0 && commit) return { ok: true, commitish: commit }; } return { ok: false, error: `failed to resolve ref ${sanitizeForLog(redactSensitiveUrlLikeString(params.ref))} in ${sanitizeForLog(redactSensitiveUrlLikeString(params.label))}` }; } async function readSkillNameFromFrontmatter(skillDir) { try { return normalizeOptionalString(parseFrontmatter(await fs.readFile(path.join(skillDir, "SKILL.md"), "utf8")).name) ?? null; } catch { return null; } } function resolveFallbackSlugFromPath(sourcePath) { return path.basename(path.resolve(sourcePath)).trim(); } async function resolveSkillInstallSlug(params) { const explicit = normalizeOptionalString(params.slug); if (explicit) return validateRequestedSkillSlug(explicit); const frontmatterName = await readSkillNameFromFrontmatter(params.sourceDir); if (frontmatterName) try { return validateRequestedSkillSlug(frontmatterName); } catch {} return validateRequestedSkillSlug(params.fallbackLabel); } async function writeSkillSourceOrigin(targetDir, origin) { await writeJson(path.join(targetDir, SKILL_SOURCE_ORIGIN_RELATIVE_PATH), origin, { trailingNewline: true }); } async function removeClawHubInstallMetadata(targetDir) { await Promise.all([fs.rm(path.join(targetDir, ".clawhub"), { recursive: true, force: true }), fs.rm(path.join(targetDir, ".clawdhub"), { recursive: true, force: true })]); } async function copyGitWorktreeExport(params) { try { await fs.cp(params.repoDir, params.exportDir, { recursive: true, filter: (source) => !path.relative(params.repoDir, source).split(path.sep).includes(".git") }); return { ok: true }; } catch (err) { return { ok: false, error: `failed to prepare git skill source: ${String(err)}` }; } } async function installLocalSkillDir(params) { const slug = await resolveSkillInstallSlug({ sourceDir: params.sourceDir, fallbackLabel: params.fallbackLabel, slug: params.slug }); const install = await installExtractedSkillRoot({ workspaceDir: params.workspaceDir, slug, extractedRoot: params.sourceDir, mode: params.force ? "update" : "install", timeoutMs: params.timeoutMs, logger: params.logger, policy: { config: params.config, installId: params.source, origin: { type: params.source, spec: params.sourceSpec, ...params.git?.commit ? { commit: params.git.commit } : {}, ...params.git?.ref ? { ref: params.git.ref } : {} }, source: params.source === "git" ? { kind: "git", authority: "third-party", mutable: !isImmutableGitCommitRef(params.git?.ref), network: true } : { kind: "local-path", authority: "user", mutable: true, network: false }, requestedSpecifier: params.sourceSpec } }); if (!install.ok) return { ok: false, error: install.error }; await removeClawHubInstallMetadata(install.targetDir); await writeSkillSourceOrigin(install.targetDir, { version: 1, source: params.source, spec: params.sourceSpec, slug, installedAt: Date.now(), ...params.git ? { git: params.git } : {} }); await untrackClawHubSkill(params.workspaceDir, slug); return { ok: true, slug, targetDir: install.targetDir, source: params.source, ...params.git ? { git: params.git } : {} }; } async function installGitSkill(params) { const parsed = parseGitPluginSpec(params.spec); if (!parsed) return { ok: false, error: `Unsupported git skill spec: ${params.spec}` }; return await withTempDir("openclaw-git-skill-", async (tmpDir) => { const repoDir = path.join(tmpDir, "repo"); const exportDir = path.join(tmpDir, "export"); params.logger?.info?.(`Cloning ${sanitizeForLog(redactSensitiveUrlLikeString(parsed.label))}...`); const clone = await runGitCommand({ argv: parsed.ref ? [ "git", "clone", parsed.url, repoDir ] : [ "git", "clone", "--depth", "1", parsed.url, repoDir ], action: "clone", label: parsed.label, timeoutMs: params.timeoutMs }); if (!clone.ok) return clone; if (parsed.ref) { const commitish = await resolveGitCommitish({ repoDir, ref: parsed.ref, label: parsed.label, timeoutMs: params.timeoutMs }); if (!commitish.ok) return commitish; const checkout = await runGitCommand({ argv: [ "git", "switch", "--detach", "--", commitish.commitish ], action: `checkout ${parsed.ref}`, label: parsed.label, cwd: repoDir, timeoutMs: params.timeoutMs }); if (!checkout.ok) return checkout; } const rev = await runGitCommand({ argv: [ "git", "rev-parse", "HEAD" ], action: "resolve commit for", label: parsed.label, cwd: repoDir, timeoutMs: params.timeoutMs }); if (!rev.ok) return rev; const git = { url: redactSensitiveUrlLikeString(parsed.url), ...parsed.ref ? { ref: parsed.ref } : {}, commit: normalizeOptionalString(rev.stdout), resolvedAt: (/* @__PURE__ */ new Date()).toISOString() }; const exported = await copyGitWorktreeExport({ repoDir, exportDir }); if (!exported.ok) return exported; return await installLocalSkillDir({ workspaceDir: params.workspaceDir, sourceDir: exportDir, sourceSpec: redactSensitiveUrlLikeString(parsed.normalizedSpec), source: "git", fallbackLabel: path.basename(parsed.label), slug: params.slug, force: params.force, timeoutMs: params.timeoutMs, logger: params.logger, config: params.config, git }); }); } async function installPathSkill(params) { const sourceDir = resolveUserPath(params.spec); let stat; try { stat = await fs.stat(sourceDir); } catch { return { ok: false, error: `Skill path not found: ${sourceDir}` }; } if (!stat.isDirectory()) return { ok: false, error: `Skill path is not a directory: ${sourceDir}` }; return await installLocalSkillDir({ workspaceDir: params.workspaceDir, sourceDir, sourceSpec: params.spec, source: "path", fallbackLabel: resolveFallbackSlugFromPath(sourceDir), slug: params.slug, force: params.force, timeoutMs: params.timeoutMs, logger: params.logger, config: params.config }); } function isSkillSourceInstallSpec(raw) { const trimmed = raw.trim(); return trimmed.toLowerCase().startsWith("git:") || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("~/") || path.isAbsolute(trimmed); } async function installSkillFromSource(params) { const spec = params.spec.trim(); if (spec.toLowerCase().startsWith("git:")) return await installGitSkill({ ...params, spec }); return await installPathSkill({ ...params, spec }); } //#endregion //#region src/cli/skills-cli.format.ts function appendClawHubHint(output, json) { if (json) return output; return `${output}\n\nTip: use \`openclaw skills search\`, \`openclaw skills install\`, and \`openclaw skills update\` for ClawHub-backed skills.`; } function formatSkillStatus(skill) { if (skill.disabled) return theme.warn(decorativePrefix("⏸", "disabled")); if (skill.blockedByAllowlist) return theme.warn(decorativePrefix("🚫", "blocked")); if (skill.blockedByAgentFilter) return theme.warn(decorativePrefix("🚫", "excluded")); if (skill.eligible) return theme.success("✓ ready"); return theme.warn("△ needs setup"); } function normalizeSkillEmoji(emoji) { if (emoji) return emoji.replaceAll("︎", "️"); return decorativeEmoji("📦"); } const REMAINING_ESC_SEQUENCE_REGEX = new RegExp(String.raw`\u001b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])`, "g"); const JSON_CONTROL_CHAR_REGEX = new RegExp(String.raw`[\u0000-\u001f\u007f-\u009f]`, "g"); function sanitizeJsonString(value) { return stripAnsi(value).replace(REMAINING_ESC_SEQUENCE_REGEX, "").replace(JSON_CONTROL_CHAR_REGEX, ""); } function sanitizeJsonValue(value) { if (typeof value === "string") return sanitizeJsonString(value); if (Array.isArray(value)) return value.map((item) => sanitizeJsonValue(item)); if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, sanitizeJsonValue(entryValue)])); return value; } function formatSkillName(skill) { const emoji = normalizeSkillEmoji(skill.emoji); const name = theme.command(sanitizeForLog(skill.name)); return emoji ? `${emoji} ${name}` : name; } function formatSkillMissingSummary(skill) { const missing = []; 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(", ")}`); return missing.join("; "); } /** Render skill discovery status as sanitized JSON or a terminal table. */ function formatSkillsList(report, opts) { const isReadyForAgent = (skill) => skill.eligible && !skill.blockedByAgentFilter; const skills = opts.eligible ? report.skills.filter(isReadyForAgent) : report.skills; if (opts.json) { const jsonReport = sanitizeJsonValue({ 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, blockedByAgentFilter: s.blockedByAgentFilter, modelVisible: s.modelVisible, userInvocable: s.userInvocable, commandVisible: s.commandVisible, source: s.source, bundled: s.bundled, primaryEnv: s.primaryEnv, homepage: s.homepage, missing: s.missing })) }); return JSON.stringify(jsonReport, null, 2); } if (skills.length === 0) return appendClawHubHint(opts.eligible ? `No eligible skills found. Run \`${formatCliCommand("openclaw skills list")}\` to see all skills.` : "No skills found.", opts.json); const ready = skills.filter(isReadyForAgent); const tableWidth = getTerminalTableWidth(); const rows = skills.map((skill) => { const missing = formatSkillMissingSummary(skill); return { Status: formatSkillStatus(skill), Skill: formatSkillName(skill), Description: theme.muted(skill.description), Source: skill.source, Missing: missing ? theme.warn(missing) : "" }; }); const columns = [ { key: "Status", header: "Status", minWidth: 10 }, { key: "Skill", header: "Skill", minWidth: 22 }, { key: "Description", header: "Description", minWidth: 24, flex: true }, { key: "Source", header: "Source", minWidth: 10 } ]; if (opts.verbose) columns.push({ key: "Missing", header: "Missing", minWidth: 18, flex: true }); const lines = []; lines.push(`${theme.heading("Skills")} ${theme.muted(`(${ready.length}/${skills.length} ready)`)}`); lines.push(renderTable({ width: tableWidth, columns, rows }).trimEnd()); return appendClawHubHint(lines.join("\n"), opts.json); } /** Render one skill's status, requirements, install hints, and API-key setup details. */ function formatSkillInfo(report, skillName, opts) { const requestedName = skillName.trim(); const safeRequestedName = sanitizeJsonString(sanitizeForLog(requestedName)); const skill = resolveSkillStatusEntry(report.skills, requestedName); if (!skill) { if (opts.json) return JSON.stringify(sanitizeJsonValue({ error: "not found", skill: requestedName }), null, 2); return appendClawHubHint(`Skill "${safeRequestedName}" not found. Run \`${formatCliCommand("openclaw skills list")}\` to see available skills.`, opts.json); } if (opts.json) return JSON.stringify(sanitizeJsonValue(skill), null, 2); const lines = []; const emoji = normalizeSkillEmoji(skill.emoji); const status = skill.disabled ? theme.warn(decorativePrefix("⏸", "Disabled")) : skill.blockedByAllowlist ? theme.warn(decorativePrefix("🚫", "Blocked by allowlist")) : skill.blockedByAgentFilter ? theme.warn(decorativePrefix("🚫", "Excluded by agent allowlist")) : skill.eligible ? theme.success("✓ Ready") : theme.warn("△ Needs setup"); const safeName = sanitizeForLog(skill.name); const safeHomepage = skill.homepage ? sanitizeForLog(skill.homepage) : void 0; const safeSkillKey = sanitizeForLog(skill.skillKey); lines.push(`${emoji ? `${emoji} ` : ""}${theme.heading(safeName)} ${status}`); lines.push(""); lines.push(sanitizeForLog(skill.description)); lines.push(""); lines.push(theme.heading("Details:")); lines.push(`${theme.muted(" Source:")} ${sanitizeForLog(skill.source)}`); lines.push(`${theme.muted(" Path:")} ${shortenHomePath(skill.filePath)}`); if (safeHomepage) lines.push(`${theme.muted(" Homepage:")} ${safeHomepage}`); lines.push(`${theme.muted(" Visible to model:")} ${skill.modelVisible ? theme.success("yes") : theme.warn("no")}`); lines.push(`${theme.muted(" Available as command:")} ${skill.commandVisible ? theme.success("yes") : theme.warn("no")}`); if (skill.blockedByAgentFilter) lines.push(`${theme.muted(" Agent allowlist:")} excludes this skill`); if (skill.primaryEnv) lines.push(`${theme.muted(" Primary env:")} ${skill.primaryEnv}`); if (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) { lines.push(""); lines.push(theme.heading("Requirements:")); if (skill.requirements.bins.length > 0) { const binsStatus = skill.requirements.bins.map((bin) => { return skill.missing.bins.includes(bin) ? theme.error(`✗ ${bin}`) : theme.success(`✓ ${bin}`); }); lines.push(`${theme.muted(" Binaries:")} ${binsStatus.join(", ")}`); } if (skill.requirements.anyBins.length > 0) { const anyBinsMissing = skill.missing.anyBins.length > 0; const anyBinsStatus = skill.requirements.anyBins.map((bin) => { return anyBinsMissing ? theme.error(`✗ ${bin}`) : theme.success(`✓ ${bin}`); }); lines.push(`${theme.muted(" Any binaries:")} ${anyBinsStatus.join(", ")}`); } if (skill.requirements.env.length > 0) { const envStatus = skill.requirements.env.map((env) => { return skill.missing.env.includes(env) ? theme.error(`✗ ${env}`) : theme.success(`✓ ${env}`); }); lines.push(`${theme.muted(" Environment:")} ${envStatus.join(", ")}`); } if (skill.requirements.config.length > 0) { const configStatus = skill.requirements.config.map((cfg) => { return skill.missing.config.includes(cfg) ? theme.error(`✗ ${cfg}`) : theme.success(`✓ ${cfg}`); }); lines.push(`${theme.muted(" Config:")} ${configStatus.join(", ")}`); } if (skill.requirements.os.length > 0) { const osStatus = skill.requirements.os.map((osName) => { return skill.missing.os.includes(osName) ? theme.error(`✗ ${osName}`) : theme.success(`✓ ${osName}`); }); lines.push(`${theme.muted(" OS:")} ${osStatus.join(", ")}`); } } if (skill.install.length > 0 && !skill.eligible) { lines.push(""); lines.push(theme.heading("Install options:")); for (const inst of skill.install) lines.push(` ${theme.warn("→")} ${inst.label}`); } if (skill.primaryEnv && skill.missing.env.includes(skill.primaryEnv)) { lines.push(""); lines.push(theme.heading("API key setup:")); if (safeHomepage) lines.push(` Get your key: ${safeHomepage}`); lines.push(` Save via UI: ${theme.muted("Control UI → Skills → ")}${safeName}${theme.muted(" → Save key")}`); lines.push(` Save via CLI: ${formatCliCommand(`openclaw config set skills.entries.${safeSkillKey}.apiKey YOUR_KEY`)}`); lines.push(` Stored in: ${theme.muted("$OPENCLAW_CONFIG_PATH")} ${theme.muted("(default: ~/.openclaw/openclaw.json)")}`); } return appendClawHubHint(lines.join("\n"), opts.json); } /** Render aggregate setup health for all discovered skills. */ function formatSkillsCheck(report, opts) { const eligible = report.skills.filter((s) => s.eligible); const modelVisible = report.skills.filter((s) => s.modelVisible); const commandVisible = report.skills.filter((s) => s.commandVisible); const disabled = report.skills.filter((s) => s.disabled); const blocked = report.skills.filter((s) => s.blockedByAllowlist && !s.disabled); const agentFiltered = report.skills.filter((s) => s.eligible && s.blockedByAgentFilter); const promptHidden = report.skills.filter((s) => s.eligible && !s.blockedByAgentFilter && !s.modelVisible); const missingReqs = report.skills.filter((s) => !s.eligible && !s.disabled && !s.blockedByAllowlist && !s.blockedByAgentFilter); const agentId = report.agentId ?? opts.agent; if (opts.json) return JSON.stringify(sanitizeJsonValue({ agentId, agentSkillFilter: report.agentSkillFilter, workspaceDir: report.workspaceDir, managedSkillsDir: report.managedSkillsDir, summary: { total: report.skills.length, eligible: eligible.length, modelVisible: modelVisible.length, commandVisible: commandVisible.length, disabled: disabled.length, blocked: blocked.length, agentFiltered: agentFiltered.length, notInjected: promptHidden.length, missingRequirements: missingReqs.length }, eligible: eligible.map((s) => s.name), modelVisible: modelVisible.map((s) => s.name), commandVisible: commandVisible.map((s) => s.name), disabled: disabled.map((s) => s.name), blocked: blocked.map((s) => s.name), agentFiltered: agentFiltered.map((s) => s.name), notInjected: promptHidden.map((s) => ({ name: s.name, reason: "disable-model-invocation" })), missingRequirements: missingReqs.map((s) => ({ name: s.name, missing: s.missing, install: s.install })) }), null, 2); const lines = []; lines.push(theme.heading("Skills Status Check")); if (agentId) lines.push(`${theme.muted("Agent:")} ${sanitizeForLog(agentId)}`); lines.push(""); lines.push(`${theme.muted("Total:")} ${report.skills.length}`); lines.push(`${theme.success("✓")} ${theme.muted("Eligible:")} ${eligible.length}`); lines.push(`${theme.success("✓")} ${theme.muted("Visible to model:")} ${modelVisible.length}`); lines.push(`${theme.success("✓")} ${theme.muted("Available as command:")} ${commandVisible.length}`); lines.push(`${theme.warn(decorativePrefix("⏸", "Disabled:"))} ${theme.muted(String(disabled.length))}`); lines.push(`${theme.warn(decorativePrefix("🚫", "Blocked by allowlist:"))} ${theme.muted(String(blocked.length))}`); if (agentId || agentFiltered.length > 0) lines.push(`${theme.warn(decorativePrefix("🚫", "Excluded by agent allowlist:"))} ${theme.muted(String(agentFiltered.length))}`); if (promptHidden.length > 0) lines.push(`${theme.warn("△")} ${theme.muted("Ready but hidden from model prompt:")} ${promptHidden.length}`); lines.push(`${theme.error("✗")} ${theme.muted("Missing requirements:")} ${missingReqs.length}`); if (modelVisible.length > 0 || commandVisible.length > 0 || promptHidden.length > 0) { lines.push(""); lines.push(theme.heading("What this means:")); lines.push(` ${theme.muted("Eligible:")} installed and requirements pass; the agent may still exclude it.`); if (modelVisible.length > 0) lines.push(` ${theme.muted("Visible to model:")} the agent can see the skill instructions during normal chat.`); if (commandVisible.length > 0) lines.push(` ${theme.muted("Available as command:")} people, scripts, or cron jobs can call the skill explicitly.`); if (promptHidden.length > 0) lines.push(` ${theme.muted("Hidden from model prompt:")} installed and ready, but kept out of normal chat.`); } if (modelVisible.length > 0) { lines.push(""); lines.push(theme.heading("Ready and visible to model:")); for (const skill of modelVisible) { const emoji = normalizeSkillEmoji(skill.emoji); lines.push(` ${emoji ? `${emoji} ` : ""}${sanitizeForLog(skill.name)}`); } } if (promptHidden.length > 0) { lines.push(""); lines.push(theme.heading("Ready but hidden from model prompt:")); for (const skill of promptHidden) { const emoji = normalizeSkillEmoji(skill.emoji); const reason = skill.commandVisible ? "skill hides its instructions from the model; commands/cron may still use it" : "skill hides its instructions from the model and is not exposed as a command"; lines.push(` ${emoji ? `${emoji} ` : ""}${sanitizeForLog(skill.name)} ${theme.muted(`(${reason})`)}`); } } if (agentFiltered.length > 0) { lines.push(""); lines.push(theme.heading("Excluded by agent allowlist:")); for (const skill of agentFiltered) { const emoji = normalizeSkillEmoji(skill.emoji); lines.push(` ${emoji ? `${emoji} ` : ""}${sanitizeForLog(skill.name)} ${theme.muted("(loaded, but this agent is not allowed to see/use it)")}`); } } if (missingReqs.length > 0) { lines.push(""); lines.push(theme.heading("Missing requirements:")); for (const skill of missingReqs) { const emoji = normalizeSkillEmoji(skill.emoji); const missing = formatSkillMissingSummary(skill); lines.push(` ${emoji ? `${emoji} ` : ""}${sanitizeForLog(skill.name)} ${theme.muted(`(${missing})`)}`); } } return appendClawHubHint(lines.join("\n"), opts.json); } //#endregion //#region src/cli/skills-cli.ts function resolveSkillsWorkspace(options) { const config = getRuntimeConfig(); const explicitAgentId = normalizeOptionalString(options?.agentId); const inferredAgentId = explicitAgentId ? void 0 : resolveAgentIdByWorkspacePath(config, options?.cwd ?? process.cwd()); const agentId = explicitAgentId ?? inferredAgentId ?? resolveDefaultAgentId(config); return { config, agentId, workspaceDir: resolveAgentWorkspaceDir(config, agentId) }; } function resolveAgentOption(command, opts) { return resolveOptionFromCommand(command, "agent") ?? opts?.agent; } async function loadSkillsStatusReport(options) { const { config, workspaceDir, agentId } = resolveSkillsWorkspace(options); const { buildWorkspaceSkillStatus } = await import("./status-Bu4TX6f5.js"); return buildWorkspaceSkillStatus(workspaceDir, { config, agentId }); } async function runSkillsAction(render, options) { try { const report = await loadSkillsStatusReport(options); defaultRuntime.writeStdout(render(report)); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } } function resolveActiveWorkspaceDir(options) { return resolveSkillsWorkspace(options).workspaceDir; } function resolveSkillsWorkspaceForCommand(command, opts) { return resolveSkillsWorkspace({ agentId: resolveAgentOption(command ?? void 0, opts) }); } function resolveClawHubTargetWorkspaceDir(command, opts) { const agentId = resolveAgentOption(command, opts); if (opts.global && normalizeOptionalString(agentId)) { defaultRuntime.error("Use either --global or --agent, not both."); defaultRuntime.exit(1); return; } if (opts.global) return CONFIG_DIR; return resolveActiveWorkspaceDir({ agentId }); } function shouldFailSkillVerification(result) { const envelope = result; return envelope.ok !== true || envelope.decision !== "pass"; } function buildSkillVerificationOutput(result, target) { return { ...result, openclaw: { resolution: { source: target.resolution.source, selector: target.resolution.selector, registry: target.resolution.registry, installedVersion: target.resolution.installedVersion } } }; } function readVerifiedSkillCardUrl(result) { if (!result.card || typeof result.card !== "object" || Array.isArray(result.card)) return { ok: false, error: "ClawHub verification response did not include a Skill Card URL." }; const card = result.card; if (card.available === false) return { ok: false, error: "Skill Card is not available." }; const url = normalizeOptionalString(card.url); if (!url) return { ok: false, error: "ClawHub verification response did not include a Skill Card URL." }; return { ok: true, url }; } function formatSkillProposalList(manifest) { if (manifest.proposals.length === 0) return "No skill proposals.\n"; return `${manifest.proposals.map((entry) => `${entry.id} ${entry.status} ${entry.kind} ${entry.skillKey} ${entry.title}`).join("\n")}\n`; } function formatSkillProposalInspect(read) { const { record } = read; const supportFiles = read.supportFiles && read.supportFiles.length > 0 ? [ "", "Support files:", ...read.supportFiles.flatMap((file) => [ "", `--- ${file.path} ---`, file.content ]) ] : []; return [ `ID: ${record.id}`, `Status: ${record.status}`, `Kind: ${record.kind}`, `Skill: ${record.target.skillName}`, `Target: ${record.target.skillFile}`, `Scanner: ${record.scan.state}`, record.statusReason ? `Reason: ${record.statusReason}` : void 0, "", read.content, ...supportFiles ].filter((line) => line !== void 0).join("\n"); } async function readSkillProposalInput(options) { const proposal = normalizeOptionalString(options.proposal); const proposalDir = normalizeOptionalString(options.proposalDir); if (proposal && proposalDir) throw new Error("Use either --proposal or --proposal-dir, not both."); if (!proposal && !proposalDir) throw new Error("Provide --proposal or --proposal-dir."); if (proposalDir) return await readSkillProposalDraftDirectory(proposalDir); return { content: await readSkillProposalDraftFile(proposal) }; } /** * Register the skills CLI commands */ function registerSkillsCli(program) { const skills = program.command("skills").description("List and inspect available skills").option("--agent <id>", "Target agent workspace (defaults to cwd-inferred, then default agent)").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/skills", "docs.openclaw.ai/cli/skills")}\n`); skills.command("search").description("Search ClawHub skills").argument("[query...]", "Optional search query").option("--limit <n>", "Max results", (value) => parseStrictPositiveIntOption(value, "--limit")).option("--json", "Output as JSON", false).action(async (queryParts, opts) => { try { const results = await searchSkillsFromClawHub({ query: normalizeOptionalString(queryParts.join(" ")), limit: opts.limit }); if (opts.json) { defaultRuntime.writeJson({ results }); return; } if (results.length === 0) { defaultRuntime.log("No ClawHub skills found."); return; } for (const entry of results) { const version = entry.version ? ` v${entry.version}` : ""; const summary = entry.summary ? ` ${entry.summary}` : ""; defaultRuntime.log(`${entry.slug}${version} ${entry.displayName}${summary}`); } } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); skills.command("install").description("Install a skill from ClawHub, git, or a local directory").argument("<slug>", "ClawHub skill slug, git:<repo>, or local skill directory").option("--version <version>", "Install a specific version").option("--force", "Overwrite an existing workspace skill", false).option("--force-install", "Install a pending GitHub-backed skill before ClawHub scan completes", false).option("--global", "Install into the shared managed skills directory", false).option("--agent <id>", "Target agent workspace (defaults to cwd-inferred, then default agent)").option("--as <slug>", "Install a git/local skill under this slug").action(async (slug, opts, command) => { try { const workspaceDir = resolveClawHubTargetWorkspaceDir(command, opts); if (!workspaceDir) return; if (isSkillSourceInstallSpec(slug)) { if (opts.version) { defaultRuntime.error("--version is only supported for ClawHub skill installs."); defaultRuntime.exit(1); return; } const result = await installSkillFromSource({ workspaceDir, spec: slug, slug: opts.as, force: Boolean(opts.force), logger: { info: (message) => defaultRuntime.log(message), warn: (message) => defaultRuntime.log(theme.warn(message)) } }); if (!result.ok) { defaultRuntime.error(result.error); defaultRuntime.exit(1); return; } defaultRuntime.log(`Installed ${result.slug} from ${result.source} -> ${result.targetDir}`); return; } if (opts.as) { defaultRuntime.error("--as is only supported for git and local directory skill installs."); defaultRuntime.exit(1); return; } const result = await installSkillFromClawHub({ workspaceDir, slug, version: opts.version, force: Boolean(opts.force), ...opts.forceInstall ? { forceInstall: true } : {}, logger: { info: (message) => defaultRuntime.log(message) } }); if (!result.ok) { defaultRuntime.error(result.error); defaultRuntime.exit(1); return; } defaultRuntime.log(`Installed ${result.slug}@${result.version} -> ${result.targetDir}`); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); skills.command("update").description("Update ClawHub-installed skills in the active or shared managed directory").argument("[slug]", "Single skill slug").option("--all", "Update all tracked ClawHub skills", false).option("--force-install", "Install a pending GitHub-backed skill before ClawHub scan completes", false).option("--global", "Update skills in the shared managed skills directory", false).option("--agent <id>", "Target agent workspace (defaults to cwd-inferred, then default agent)").action(async (slug, opts, command) => { try { if (!slug && !opts.all) { defaultRuntime.error("Provide a skill slug or use --all."); defaultRuntime.exit(1); return; } if (slug && opts.all) { defaultRuntime.error("Use either a skill slug or --all."); defaultRuntime.exit(1); return; } const workspaceDir = resolveClawHubTargetWorkspaceDir(command, opts); if (!workspaceDir) return; const tracked = await readTrackedClawHubSkillSlugs(workspaceDir); if (opts.all && tracked.length === 0) { defaultRuntime.log("No tracked ClawHub skills to update."); return; } const results = await updateSkillsFromClawHub({ workspaceDir, slug, ...opts.forceInstall ? { forceInstall: true } : {}, logger: { info: (message) => defaultRuntime.log(message) } }); let failed = false; for (const result of results) { if (!result.ok) { failed = true; defaultRuntime.error(result.error); continue; } if (result.changed) { defaultRuntime.log(`Updated ${result.slug}: ${result.previousVersion ?? "unknown"} -> ${result.version}`); continue; } defaultRuntime.log(`${result.slug} already at ${result.version}`); } if (failed) defaultRuntime.exit(1); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); skills.command("verify").description("Verify a ClawHub skill with ClawHub").argument("<slug>", "ClawHub skill slug").option("--version <version>", "Verify a specific version").option("--tag <tag>", "Verify a dist tag").option("--card", "Print the generated Skill Card Markdown", false).option("--global", "Resolve installed skill metadata from the shared managed skills directory", false).option("--agent <id>", "Target agent workspace (defaults to cwd-inferred, then default agent)").action(async (slug, opts, command) => { let exitCode; try { const workspaceDir = resolveClawHubTargetWorkspaceDir(command, opts); if (!workspaceDir) return; const target = await resolveClawHubSkillVerificationTarget({ workspaceDir, slug, version: opts.version, tag: opts.tag }); if (!target.ok) { defaultRuntime.error(target.error); exitCode = 1; } else { const verification = await fetchClawHubSkillVerification({ slug: target.slug, version: target.version, tag: target.tag, baseUrl: target.baseUrl }); if (opts.card) { const cardUrl = readVerifiedSkillCardUrl(verification); if (!cardUrl.ok) { defaultRuntime.error(cardUrl.error); exitCode = 1; } else { const card = await fetchClawHubSkillCard({ url: cardUrl.url, baseUrl: target.baseUrl }); defaultRuntime.writeStdout(card.endsWith("\n") ? card : `${card}\n`); exitCode = shouldFailSkillVerification(verification) ? 1 : void 0; } } else { defaultRuntime.writeJson(buildSkillVerificationOutput(verification, target)); exitCode = shouldFailSkillVerification(verification) ? 1 : void 0; } } } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); return; } if (exitCode) defaultRuntime.exit(exitCode); }); const workshop = skills.command("workshop").description("Manage pending skill proposals").option("--agent <id>", "Target agent workspace (defaults to cwd-inferred, then default agent)"); workshop.command("list").description("List pending and completed skill proposals").option("--json", "Output as JSON", false).action(async (opts) => { try { const { workspaceDir } = resolveSkillsWorkspaceForCommand(workshop, opts); const manifest = await listSkillProposals({ workspaceDir }); if (opts.json) { defaultRuntime.writeJson(manifest); return; } defaultRuntime.writeStdout(formatSkillProposalList(manifest)); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); workshop.command("inspect").description("Inspect a skill proposal").argument("<proposal-id>", "Skill proposal id").option("--json", "Output as JSON", false).action(async (proposalId, opts) => { try { const { workspaceDir } = resolveSkillsWorkspaceForCommand(workshop, opts); const proposal = await inspectSkillProposal(proposalId, { workspaceDir }); if (!proposal) { defaultRuntime.error(`Skill proposal not found: ${proposalId}`); defaultRuntime.exit(1); return; } if (opts.json) { defaultRuntime.writeJson(proposal); return; } defaultRuntime.writeStdout(formatSkillProposalInspect(proposal)); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); workshop.command("propose-create").description("Create a pending proposal for a new workspace skill").requiredOption("--name <name>", "Skill name").requiredOption("--description <description>", "Skill description").option("--proposal <path>", "Path to PROPOSAL.md draft content").option("--proposal-dir <path>", "Path to proposal directory with PROPOSAL.md and UTF-8 text support files").option("--goal <text>", "Proposal or improvement goal").option("--evidence <text>", "Evidence or notes for the proposal").option("--json", "Output as JSON", false).action(async (opts, command) => { try { const { config, workspaceDir } = resolveSkillsWorkspaceForCommand(command.parent, opts); const draft = await readSkillProposalInput(opts); const proposal = await proposeCreateSkill({ workspaceDir, config, name: opts.name, description: opts.description, content: draft.content, supportFiles: draft.supportFiles, createdBy: "cli", goal: opts.goal, evidence: opts.evidence }); if (opts.json) { defaultRuntime.writeJson(proposal); return; } defaultRuntime.writeStdout(`${proposal.record.id}\n`); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); workshop.command("propose-update").description("Create a pending proposal for an existing workspace skill").argument("<skill>", "Skill name or key").option("--proposal <path>", "Path to PROPOSAL.md draft content").option("--proposal-dir <path>", "Path to proposal directory with PROPOSAL.md and UTF-8 text support files").option("--description <text>", "Concise proposal description").option("--goal <text>", "Proposal or improvement goal").option("--evidence <text>", "Evidence or notes for the proposal").option("--json", "Output as JSON", false).action(async (skill, opts, command) => { try { const { config, workspaceDir, agentId } = resolveSkillsWorkspaceForCommand(command.parent, opts); const draft = await readSkillProposalInput(opts); const proposal = await proposeUpdateSkill({ workspaceDir, config, agentId, skillName: skill, description: opts.description, content: draft.content, supportFiles: draft.supportFiles, createdBy: "cli", goal: opts.goal, evidence: opts.evidence }); if (opts.json) { defaultRuntime.writeJson(proposal); return; } defaultRuntime.writeStdout(`${proposal.record.id}\n`); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); workshop.command("revise").description("Revise a pending skill proposal").argument("<proposal-id>", "Skill proposal id").option("--proposal <path>", "Path to revised PROPOSAL.md draft content").option("--proposal-dir <path>", "Path to revised proposal directory with PROPOSAL.md and UTF-8 text support files").option("--description <description>", "Replacement proposal description").option("--goal <text>", "Replacement research or improvement goal").option("--evidence <text>", "Replacement evidence or notes for the proposal").option("--json", "Output as JSON", false).action(async (proposalId, opts, command) => { try { const { config, workspaceDir } = resolveSkillsWorkspaceForCommand(command.parent, opts); const draft = await readSkillProposalInput(opts); const proposal = await reviseSkillProposal({ workspaceDir, config, proposalId, content: draft.content, supportFiles: draft.supportFiles, description: opts.description, goal: opts.goal, evidence: opts.evidence }); if (opts.json) { defaultRuntime.writeJson(proposal); return; } defaultRuntime.writeStdout(`Revised ${proposal.record.id} ${proposal.record.proposedVersion}\n`); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); workshop.command("apply").description("Apply a pending skill proposal").argument("<proposal-id>", "Skill proposal id").option("--json", "Output as JSON", false).action(async (proposalId, opts, command) => { try { const { workspaceDir } = resolveSkillsWorkspaceForCommand(command.parent, opts); const applied = await applySkillProposal({ workspaceDir, proposalId }); if (opts.json) { defaultRuntime.writeJson(applied); return; } defaultRuntime.writeStdout(`Applied ${applied.record.id} -> ${applied.targetSkillFile}\n`); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); workshop.command("reject").description("Reject a pending skill proposal").argument("<proposal-id>", "Skill proposal id").option("--reason <text>", "Reason for rejection").option("--json", "Output as JSON", false).action(async (proposalId, opts, command) => { try { const { workspaceDir } = resolveSkillsWorkspaceForCommand(command.parent, opts); const record = await rejectSkillProposal({ workspaceDir, proposalId, reason: opts.reason }); if (opts.json) { defaultRuntime.writeJson(record); return; } defaultRuntime.writeStdout(`Rejected ${record.id}\n`); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); workshop.command("quarantine").description("Quarantine a skill proposal").argument("<proposal-id>", "Skill proposal id").option("--reason <text>", "Reason for quarantine").option("--json", "Output as JSON", false).action(async (proposalId, opts, command) => { try { const { workspaceDir } = resolveSkillsWorkspaceForCommand(command.parent, opts); const record = await quarantineSkillProposal({ workspaceDir, proposalId, reason: opts.reason }); if (opts.json) { defaultRuntime.writeJson(record); return; } defaultRuntime.writeStdout(`Quarantined ${record.id}\n`); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); } }); 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).option("--agent <id>", "Target agent workspace (defaults to cwd-inferred, then default agent)").action(async (opts, command) => { await runSkillsAction((report) => formatSkillsList(report, opts), { agentId: resolveAgentOption(command, opts) }); }); skills.command("info").description("Show detailed information about a skill").argument("<name>", "Skill name").option("--json", "Output as JSON", false).option("--agent <id>", "Target agent workspace (defaults to cwd-inferred, then default agent)").action(async (name, opts, command) => { await runSkillsAction((report) => formatSkillInfo(report, name, opts), { agentId: resolveAgentOption(command, opts) }); }); skills.command("check").description("Check which skills are ready, visible, or missing requirements").option("--agent <id>", "Target agent workspace (defaults to cwd-inferred, then default agent)").option("--json", "Output as JSON", false).action(async (opts, command) => { await runSkillsAction((report) => formatSkillsCheck(report, opts), { agentId: resolveAgentOption(command, opts) }); }); skills.action(async (opts, command) => { await runSkillsAction((report) => formatSkillsList(report, {}), { agentId: resolveAgentOption(command, opts) }); }); } //#endregion export { formatSkillInfo, formatSkillsCheck, formatSkillsList, registerSkillsCli };