diff --git a/src/agents/skills/frontmatter.ts b/src/agents/skills/frontmatter.ts index a0775ba98545..efae3ba74a4b 100644 --- a/src/agents/skills/frontmatter.ts +++ b/src/agents/skills/frontmatter.ts @@ -104,14 +104,137 @@ export function resolveOpenClawMetadata( } function parseCapabilities(raw: unknown): SkillCapability[] | undefined { - const list = normalizeStringList(raw); - if (list.length === 0) { + const canonical = new Set(); + const names = extractCapabilityNames(raw); + for (const name of names) { + const normalized = normalizeCapabilityName(name); + if (normalized) { + canonical.add(normalized); + } + } + return canonical.size > 0 ? [...canonical] : undefined; +} + +const CAPABILITY_SET = new Set(SKILL_CAPABILITIES as readonly string[]); + +// Accept common naming used across Codex/Claude/Cursor and map to canonical OpenClaw capabilities. +const CAPABILITY_ALIASES: Record = { + // shell + bash: "shell", + command: "shell", + commands: "shell", + exec: "shell", + process: "shell", + shell: "shell", + terminal: "shell", + "shell.exec": "shell", + "shell.execute": "shell", + shell_exec: "shell", + + // filesystem + "apply-patch": "filesystem", + apply_patch: "filesystem", + edit: "filesystem", + file: "filesystem", + files: "filesystem", + filesystem: "filesystem", + fs: "filesystem", + write: "filesystem", + + // network + fetch: "network", + http: "network", + mcp: "network", + network: "network", + web: "network", + webfetch: "network", + "web-fetch": "network", + web_fetch: "network", + web_search: "network", + "web.search": "network", + "network.fetch": "network", + "network.search": "network", + + // browser / computer-use style + browser: "browser", + "computer-use": "browser", + computer_use: "browser", + gui: "browser", + screen: "browser", + ui: "browser", + + // sessions / orchestration + delegate: "sessions", + orchestration: "sessions", + sessions: "sessions", + sessions_send: "sessions", + sessions_spawn: "sessions", + subagent: "sessions", + subagents: "sessions", + + // messaging + chat: "messaging", + message: "messaging", + messages: "messaging", + messaging: "messaging", + + // scheduling + cron: "scheduling", + schedule: "scheduling", + scheduler: "scheduling", + scheduling: "scheduling", + timer: "scheduling", +}; + +function normalizeCapabilityName(raw: string): SkillCapability | undefined { + const key = raw.trim().toLowerCase(); + if (!key) { return undefined; } - const valid = list.filter((v): v is SkillCapability => - (SKILL_CAPABILITIES as readonly string[]).includes(v), - ); - return valid.length > 0 ? valid : undefined; + if (CAPABILITY_SET.has(key)) { + return key as SkillCapability; + } + const alias = CAPABILITY_ALIASES[key]; + if (alias) { + return alias; + } + const firstSegment = key.split(/[._:-]/)[0]; + if (CAPABILITY_SET.has(firstSegment)) { + return firstSegment as SkillCapability; + } + return undefined; +} + +function extractCapabilityNames(raw: unknown): string[] { + if (!raw) { + return []; + } + if (typeof raw === "string") { + return normalizeStringList(raw); + } + if (Array.isArray(raw)) { + const names: string[] = []; + for (const entry of raw) { + if (typeof entry === "string") { + names.push(entry); + continue; + } + if (entry && typeof entry === "object" && !Array.isArray(entry)) { + const obj = entry as Record; + const candidate = [obj.name, obj.type, obj.id, obj.capability].find( + (value) => typeof value === "string", + ); + if (typeof candidate === "string") { + names.push(candidate); + } + } + } + return names; + } + if (typeof raw === "object") { + return Object.keys(raw as Record); + } + return []; } export function resolveSkillInvocationPolicy(