160 lines
4.9 KiB
TypeScript
160 lines
4.9 KiB
TypeScript
/**
|
|
* Built-in Plugin Registry
|
|
*
|
|
* Manages built-in plugins that ship with the CLI and can be enabled/disabled
|
|
* by users via the /plugin UI.
|
|
*
|
|
* Built-in plugins differ from bundled skills (src/skills/bundled/) in that:
|
|
* - They appear in the /plugin UI under a "Built-in" section
|
|
* - Users can enable/disable them (persisted to user settings)
|
|
* - They can provide multiple components (skills, hooks, MCP servers)
|
|
*
|
|
* Plugin IDs use the format `{name}@builtin` to distinguish them from
|
|
* marketplace plugins (`{name}@{marketplace}`).
|
|
*/
|
|
|
|
import type { Command } from '../commands.js'
|
|
import type { BundledSkillDefinition } from '../skills/bundledSkills.js'
|
|
import type { BuiltinPluginDefinition, LoadedPlugin } from '../types/plugin.js'
|
|
import { getSettings_DEPRECATED } from '../utils/settings/settings.js'
|
|
|
|
const BUILTIN_PLUGINS: Map<string, BuiltinPluginDefinition> = new Map()
|
|
|
|
export const BUILTIN_MARKETPLACE_NAME = 'builtin'
|
|
|
|
/**
|
|
* Register a built-in plugin. Call this from initBuiltinPlugins() at startup.
|
|
*/
|
|
export function registerBuiltinPlugin(
|
|
definition: BuiltinPluginDefinition,
|
|
): void {
|
|
BUILTIN_PLUGINS.set(definition.name, definition)
|
|
}
|
|
|
|
/**
|
|
* Check if a plugin ID represents a built-in plugin (ends with @builtin).
|
|
*/
|
|
export function isBuiltinPluginId(pluginId: string): boolean {
|
|
return pluginId.endsWith(`@${BUILTIN_MARKETPLACE_NAME}`)
|
|
}
|
|
|
|
/**
|
|
* Get a specific built-in plugin definition by name.
|
|
* Useful for the /plugin UI to show the skills/hooks/MCP list without
|
|
* a marketplace lookup.
|
|
*/
|
|
export function getBuiltinPluginDefinition(
|
|
name: string,
|
|
): BuiltinPluginDefinition | undefined {
|
|
return BUILTIN_PLUGINS.get(name)
|
|
}
|
|
|
|
/**
|
|
* Get all registered built-in plugins as LoadedPlugin objects, split into
|
|
* enabled/disabled based on user settings (with defaultEnabled as fallback).
|
|
* Plugins whose isAvailable() returns false are omitted entirely.
|
|
*/
|
|
export function getBuiltinPlugins(): {
|
|
enabled: LoadedPlugin[]
|
|
disabled: LoadedPlugin[]
|
|
} {
|
|
const settings = getSettings_DEPRECATED()
|
|
const enabled: LoadedPlugin[] = []
|
|
const disabled: LoadedPlugin[] = []
|
|
|
|
for (const [name, definition] of BUILTIN_PLUGINS) {
|
|
if (definition.isAvailable && !definition.isAvailable()) {
|
|
continue
|
|
}
|
|
|
|
const pluginId = `${name}@${BUILTIN_MARKETPLACE_NAME}`
|
|
const userSetting = settings?.enabledPlugins?.[pluginId]
|
|
// Enabled state: user preference > plugin default > true
|
|
const isEnabled =
|
|
userSetting !== undefined
|
|
? userSetting === true
|
|
: (definition.defaultEnabled ?? true)
|
|
|
|
const plugin: LoadedPlugin = {
|
|
name,
|
|
manifest: {
|
|
name,
|
|
description: definition.description,
|
|
version: definition.version,
|
|
},
|
|
path: BUILTIN_MARKETPLACE_NAME, // sentinel — no filesystem path
|
|
source: pluginId,
|
|
repository: pluginId,
|
|
enabled: isEnabled,
|
|
isBuiltin: true,
|
|
hooksConfig: definition.hooks,
|
|
mcpServers: definition.mcpServers,
|
|
}
|
|
|
|
if (isEnabled) {
|
|
enabled.push(plugin)
|
|
} else {
|
|
disabled.push(plugin)
|
|
}
|
|
}
|
|
|
|
return { enabled, disabled }
|
|
}
|
|
|
|
/**
|
|
* Get skills from enabled built-in plugins as Command objects.
|
|
* Skills from disabled plugins are not returned.
|
|
*/
|
|
export function getBuiltinPluginSkillCommands(): Command[] {
|
|
const { enabled } = getBuiltinPlugins()
|
|
const commands: Command[] = []
|
|
|
|
for (const plugin of enabled) {
|
|
const definition = BUILTIN_PLUGINS.get(plugin.name)
|
|
if (!definition?.skills) continue
|
|
for (const skill of definition.skills) {
|
|
commands.push(skillDefinitionToCommand(skill))
|
|
}
|
|
}
|
|
|
|
return commands
|
|
}
|
|
|
|
/**
|
|
* Clear built-in plugins registry (for testing).
|
|
*/
|
|
export function clearBuiltinPlugins(): void {
|
|
BUILTIN_PLUGINS.clear()
|
|
}
|
|
|
|
// --
|
|
|
|
function skillDefinitionToCommand(definition: BundledSkillDefinition): Command {
|
|
return {
|
|
type: 'prompt',
|
|
name: definition.name,
|
|
description: definition.description,
|
|
hasUserSpecifiedDescription: true,
|
|
allowedTools: definition.allowedTools ?? [],
|
|
argumentHint: definition.argumentHint,
|
|
whenToUse: definition.whenToUse,
|
|
model: definition.model,
|
|
disableModelInvocation: definition.disableModelInvocation ?? false,
|
|
userInvocable: definition.userInvocable ?? true,
|
|
contentLength: 0,
|
|
// 'bundled' not 'builtin' — 'builtin' in Command.source means hardcoded
|
|
// slash commands (/help, /clear). Using 'bundled' keeps these skills in
|
|
// the Skill tool's listing, analytics name logging, and prompt-truncation
|
|
// exemption. The user-toggleable aspect is tracked on LoadedPlugin.isBuiltin.
|
|
source: 'bundled',
|
|
loadedFrom: 'bundled',
|
|
hooks: definition.hooks,
|
|
context: definition.context,
|
|
agent: definition.agent,
|
|
isEnabled: definition.isEnabled ?? (() => true),
|
|
isHidden: !(definition.userInvocable ?? true),
|
|
progressMessage: 'running',
|
|
getPromptForCommand: definition.getPromptForCommand,
|
|
}
|
|
}
|