diff --git a/src/agent/skills/parser.ts b/src/agent/skills/parser.ts index 3205882e..78d4b57d 100644 --- a/src/agent/skills/parser.ts +++ b/src/agent/skills/parser.ts @@ -89,9 +89,61 @@ function validateFrontmatter(raw: Record): SkillFrontmatter | n }; } + // Parse invocation control fields + // Support both kebab-case and camelCase for compatibility + const userInvocableRaw = + raw["user-invocable"] ?? raw["userInvocable"] ?? raw["user_invocable"]; + if (typeof userInvocableRaw === "boolean") { + frontmatter.userInvocable = userInvocableRaw; + } else if (typeof userInvocableRaw === "string") { + frontmatter.userInvocable = parseBooleanString(userInvocableRaw); + } + + const disableModelRaw = + raw["disable-model-invocation"] ?? + raw["disableModelInvocation"] ?? + raw["disable_model_invocation"]; + if (typeof disableModelRaw === "boolean") { + frontmatter.disableModelInvocation = disableModelRaw; + } else if (typeof disableModelRaw === "string") { + frontmatter.disableModelInvocation = parseBooleanString(disableModelRaw); + } + + // Parse command dispatch fields + const dispatchRaw = + raw["command-dispatch"] ?? raw["commandDispatch"] ?? raw["command_dispatch"]; + if (typeof dispatchRaw === "string") { + frontmatter.commandDispatch = dispatchRaw.trim().toLowerCase(); + } + + const toolRaw = raw["command-tool"] ?? raw["commandTool"] ?? raw["command_tool"]; + if (typeof toolRaw === "string") { + frontmatter.commandTool = toolRaw.trim(); + } + + const argModeRaw = + raw["command-arg-mode"] ?? raw["commandArgMode"] ?? raw["command_arg_mode"]; + if (typeof argModeRaw === "string") { + frontmatter.commandArgMode = argModeRaw.trim().toLowerCase(); + } + return frontmatter; } +/** + * Parse boolean from string value + */ +function parseBooleanString(value: string): boolean | undefined { + const normalized = value.trim().toLowerCase(); + if (normalized === "true" || normalized === "yes" || normalized === "1") { + return true; + } + if (normalized === "false" || normalized === "no" || normalized === "0") { + return false; + } + return undefined; +} + /** * Parse a SKILL.md file into a Skill object * diff --git a/src/agent/skills/types.ts b/src/agent/skills/types.ts index 256ce282..0534a3d2 100644 --- a/src/agent/skills/types.ts +++ b/src/agent/skills/types.ts @@ -106,6 +106,20 @@ export interface SkillFrontmatter { homepage?: string | undefined; /** Skill-specific metadata */ metadata?: SkillMetadata | undefined; + + // Invocation control fields + /** Whether users can invoke via /command (default: true) */ + userInvocable?: boolean | undefined; + /** Whether to exclude from AI system prompt (default: false) */ + disableModelInvocation?: boolean | undefined; + + // Command dispatch fields + /** Command dispatch mode (e.g., "tool") */ + commandDispatch?: string | undefined; + /** Tool name for dispatch (when commandDispatch: "tool") */ + commandTool?: string | undefined; + /** Argument mode for dispatch (default: "raw") */ + commandArgMode?: string | undefined; } /** @@ -222,6 +236,61 @@ export interface EligibilityResult { reasons?: string[] | undefined; } +// ============================================================================ +// Invocation Types +// ============================================================================ + +/** + * Skill invocation policy + * Controls how a skill can be invoked + */ +export interface SkillInvocationPolicy { + /** Whether users can invoke this skill via /command (default: true) */ + userInvocable: boolean; + /** Whether to exclude from AI's system prompt (default: false) */ + disableModelInvocation: boolean; +} + +/** + * Command dispatch specification + * For skills that dispatch directly to a tool + */ +export interface SkillCommandDispatch { + /** Dispatch type */ + kind: "tool"; + /** Tool name to invoke */ + toolName: string; + /** How to pass arguments (default: "raw") */ + argMode?: "raw" | undefined; +} + +/** + * Skill command specification + * Represents a user-invocable skill command + */ +export interface SkillCommandSpec { + /** Normalized command name (e.g., "pdf" for /pdf) */ + name: string; + /** Original skill name/ID */ + skillId: string; + /** Command description */ + description: string; + /** Optional dispatch behavior */ + dispatch?: SkillCommandDispatch | undefined; +} + +/** + * Skill invocation result + */ +export interface SkillInvocationResult { + /** The matched command */ + command: SkillCommandSpec; + /** Arguments passed to the command */ + args?: string | undefined; + /** The skill instructions to inject */ + instructions: string; +} + /** * Filename constant for skill definition file */