/ src / plugins / builtinPlugins.ts
builtinPlugins.ts
  1  /**
  2   * Built-in Plugin Registry
  3   *
  4   * Manages built-in plugins that ship with the CLI and can be enabled/disabled
  5   * by users via the /plugin UI.
  6   *
  7   * Built-in plugins differ from bundled skills (src/skills/bundled/) in that:
  8   * - They appear in the /plugin UI under a "Built-in" section
  9   * - Users can enable/disable them (persisted to user settings)
 10   * - They can provide multiple components (skills, hooks, MCP servers)
 11   *
 12   * Plugin IDs use the format `{name}@builtin` to distinguish them from
 13   * marketplace plugins (`{name}@{marketplace}`).
 14   */
 15  
 16  import type { Command } from '../commands.js'
 17  import type { BundledSkillDefinition } from '../skills/bundledSkills.js'
 18  import type { BuiltinPluginDefinition, LoadedPlugin } from '../types/plugin.js'
 19  import { getSettings_DEPRECATED } from '../utils/settings/settings.js'
 20  
 21  const BUILTIN_PLUGINS: Map<string, BuiltinPluginDefinition> = new Map()
 22  
 23  export const BUILTIN_MARKETPLACE_NAME = 'builtin'
 24  
 25  /**
 26   * Register a built-in plugin. Call this from initBuiltinPlugins() at startup.
 27   */
 28  export function registerBuiltinPlugin(
 29    definition: BuiltinPluginDefinition,
 30  ): void {
 31    BUILTIN_PLUGINS.set(definition.name, definition)
 32  }
 33  
 34  /**
 35   * Check if a plugin ID represents a built-in plugin (ends with @builtin).
 36   */
 37  export function isBuiltinPluginId(pluginId: string): boolean {
 38    return pluginId.endsWith(`@${BUILTIN_MARKETPLACE_NAME}`)
 39  }
 40  
 41  /**
 42   * Get a specific built-in plugin definition by name.
 43   * Useful for the /plugin UI to show the skills/hooks/MCP list without
 44   * a marketplace lookup.
 45   */
 46  export function getBuiltinPluginDefinition(
 47    name: string,
 48  ): BuiltinPluginDefinition | undefined {
 49    return BUILTIN_PLUGINS.get(name)
 50  }
 51  
 52  /**
 53   * Get all registered built-in plugins as LoadedPlugin objects, split into
 54   * enabled/disabled based on user settings (with defaultEnabled as fallback).
 55   * Plugins whose isAvailable() returns false are omitted entirely.
 56   */
 57  export function getBuiltinPlugins(): {
 58    enabled: LoadedPlugin[]
 59    disabled: LoadedPlugin[]
 60  } {
 61    const settings = getSettings_DEPRECATED()
 62    const enabled: LoadedPlugin[] = []
 63    const disabled: LoadedPlugin[] = []
 64  
 65    for (const [name, definition] of BUILTIN_PLUGINS) {
 66      if (definition.isAvailable && !definition.isAvailable()) {
 67        continue
 68      }
 69  
 70      const pluginId = `${name}@${BUILTIN_MARKETPLACE_NAME}`
 71      const userSetting = settings?.enabledPlugins?.[pluginId]
 72      // Enabled state: user preference > plugin default > true
 73      const isEnabled =
 74        userSetting !== undefined
 75          ? userSetting === true
 76          : (definition.defaultEnabled ?? true)
 77  
 78      const plugin: LoadedPlugin = {
 79        name,
 80        manifest: {
 81          name,
 82          description: definition.description,
 83          version: definition.version,
 84        },
 85        path: BUILTIN_MARKETPLACE_NAME, // sentinel — no filesystem path
 86        source: pluginId,
 87        repository: pluginId,
 88        enabled: isEnabled,
 89        isBuiltin: true,
 90        hooksConfig: definition.hooks,
 91        mcpServers: definition.mcpServers,
 92      }
 93  
 94      if (isEnabled) {
 95        enabled.push(plugin)
 96      } else {
 97        disabled.push(plugin)
 98      }
 99    }
100  
101    return { enabled, disabled }
102  }
103  
104  /**
105   * Get skills from enabled built-in plugins as Command objects.
106   * Skills from disabled plugins are not returned.
107   */
108  export function getBuiltinPluginSkillCommands(): Command[] {
109    const { enabled } = getBuiltinPlugins()
110    const commands: Command[] = []
111  
112    for (const plugin of enabled) {
113      const definition = BUILTIN_PLUGINS.get(plugin.name)
114      if (!definition?.skills) continue
115      for (const skill of definition.skills) {
116        commands.push(skillDefinitionToCommand(skill))
117      }
118    }
119  
120    return commands
121  }
122  
123  /**
124   * Clear built-in plugins registry (for testing).
125   */
126  export function clearBuiltinPlugins(): void {
127    BUILTIN_PLUGINS.clear()
128  }
129  
130  // --
131  
132  function skillDefinitionToCommand(definition: BundledSkillDefinition): Command {
133    return {
134      type: 'prompt',
135      name: definition.name,
136      description: definition.description,
137      hasUserSpecifiedDescription: true,
138      allowedTools: definition.allowedTools ?? [],
139      argumentHint: definition.argumentHint,
140      whenToUse: definition.whenToUse,
141      model: definition.model,
142      disableModelInvocation: definition.disableModelInvocation ?? false,
143      userInvocable: definition.userInvocable ?? true,
144      contentLength: 0,
145      // 'bundled' not 'builtin' — 'builtin' in Command.source means hardcoded
146      // slash commands (/help, /clear). Using 'bundled' keeps these skills in
147      // the Skill tool's listing, analytics name logging, and prompt-truncation
148      // exemption. The user-toggleable aspect is tracked on LoadedPlugin.isBuiltin.
149      source: 'bundled',
150      loadedFrom: 'bundled',
151      hooks: definition.hooks,
152      context: definition.context,
153      agent: definition.agent,
154      isEnabled: definition.isEnabled ?? (() => true),
155      isHidden: !(definition.userInvocable ?? true),
156      progressMessage: 'running',
157      getPromptForCommand: definition.getPromptForCommand,
158    }
159  }