{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGlE,OAAO,EAAE,KAAK,gBAAgB,EAAkB,MAAM,eAAe,CAAC;AAEtE,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,uGAAuG;AACvG,wBAAgB,yBAAyB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGnF;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUrF;AA0CD,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAe/C,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,gBAAgB,EAAE,CAwBxE;AAED,MAAM,WAAW,oBAAoB;IACpC;;;;;;;;;;;;;;;OAeG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAEvE;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,gBAAgB,CA6C7G","sourcesContent":["/**\n * Plugin discovery and wiring.\n *\n * Discovers plugin directories under the `plugins/` folders, parses their\n * manifests (see {@link parsePluginDir}), and turns each into a synthetic\n * {@link ExtensionFactory} that registers the plugin's capabilities through the\n * existing ExtensionAPI. The factory is loaded by the standard extension loader,\n * so plugins are just extensions assembled from a manifest instead of code.\n *\n * Capability wiring (minimum):\n *  - skills / themes         → `resources_discover` skill/theme paths\n *  - commands                → `resources_discover` slash-command paths (`.agents/commands`)\n *  - agents                  → `resources_discover` agent paths (`.agents/agents` subagents)\n *  - providers (native only) → `registerProvider`\n *  - hooks                   → shell-protocol bridge (see hooks-bridge.ts)\n *  - mcpServers              → parsed; wiring deferred (see design doc)\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { type ExtensionMcpServerConfig, registerExtensionMcpServers } from \"../../extension-mcp-servers.js\";\nimport type { ExtensionAPI, ExtensionFactory } from \"../types.js\";\nimport { installPluginHooks } from \"./hooks-bridge.js\";\nimport { ensurePluginDataDir, pluginDataDir } from \"./locations.js\";\nimport { type NormalizedPlugin, parsePluginDir } from \"./manifest.js\";\n\n/** Extension path a plugin is loaded under: `<plugin:my-tool>`. */\nexport function pluginExtensionPath(id: string): string {\n\treturn `<plugin:${id}>`;\n}\n\n/** Recover the plugin id from {@link pluginExtensionPath}, or undefined for a non-plugin extension. */\nexport function pluginIdFromExtensionPath(extensionPath: string): string | undefined {\n\tconst match = /^<plugin:(.+)>$/.exec(extensionPath);\n\treturn match?.[1];\n}\n\n/**\n * The template variables a plugin may use, mapped to their values.\n *\n * All vendor spellings are honored rather than only the ones hoocode invented.\n * A plugin does not know which agent is loading it: a Copilot plugin writes\n * `${PLUGIN_ROOT}`, a Claude one `${CLAUDE_PLUGIN_ROOT}`, and either may use\n * `${*_PLUGIN_DATA}` for runtime state. Supporting one spelling means the others\n * reach the shell or the MCP client as literal text.\n */\nexport function pluginVariables(root: string, dataDir: string): Record<string, string> {\n\treturn {\n\t\tPLUGIN_ROOT: root,\n\t\tCLAUDE_PLUGIN_ROOT: root,\n\t\tAGENTS_PLUGIN_ROOT: root,\n\t\tCOPILOT_PLUGIN_ROOT: root,\n\t\tCLAUDE_PLUGIN_DATA: dataDir,\n\t\tCOPILOT_PLUGIN_DATA: dataDir,\n\t\tAGENTS_PLUGIN_DATA: dataDir,\n\t};\n}\n\n/** Expand every `${VAR}` in {@link pluginVariables}. Unknown `${...}` is left alone. */\nfunction substituteVars(value: string, vars: Record<string, string>): string {\n\treturn value.replace(/\\$\\{([A-Z_]+)\\}/g, (whole, name: string) => vars[name] ?? whole);\n}\n\n/** Coerce parsed mcpServers into the standard config shape, substituting root vars. */\nfunction resolveMcpServers(\n\tmcpServers: Record<string, unknown>,\n\tvars: Record<string, string>,\n): Record<string, ExtensionMcpServerConfig> {\n\tconst out: Record<string, ExtensionMcpServerConfig> = {};\n\tfor (const [name, raw] of Object.entries(mcpServers)) {\n\t\tif (!raw || typeof raw !== \"object\") continue;\n\t\tconst cfg = raw as ExtensionMcpServerConfig;\n\t\t// Remote servers ({ type: \"http\" | \"sse\", url }): pass through for the\n\t\t// MCP loader's HTTP transports.\n\t\tif (typeof cfg.url === \"string\" && typeof cfg.command !== \"string\") {\n\t\t\tout[name] = {\n\t\t\t\ttype: cfg.type === \"sse\" ? \"sse\" : \"http\",\n\t\t\t\turl: cfg.url,\n\t\t\t\theaders: cfg.headers\n\t\t\t\t\t? Object.fromEntries(Object.entries(cfg.headers).map(([k, v]) => [k, substituteVars(String(v), vars)]))\n\t\t\t\t\t: undefined,\n\t\t\t\tbackground: cfg.background,\n\t\t\t};\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof cfg.command !== \"string\") continue;\n\t\tout[name] = {\n\t\t\tcommand: substituteVars(cfg.command, vars),\n\t\t\targs: cfg.args?.map((a) => substituteVars(String(a), vars)),\n\t\t\tenv: cfg.env\n\t\t\t\t? Object.fromEntries(Object.entries(cfg.env).map(([k, v]) => [k, substituteVars(String(v), vars)]))\n\t\t\t\t: undefined,\n\t\t\tbackground: cfg.background,\n\t\t};\n\t}\n\treturn out;\n}\n\nexport type { NormalizedPlugin } from \"./manifest.js\";\nexport { parsePluginDir } from \"./manifest.js\";\n\n/**\n * Discover plugins across the given `plugins/` directories.\n * First-wins on duplicate ids (project dirs should be listed before global).\n */\n/**\n * A skills directory holds plain skills and plugins side by side, and the\n * manifest is the only thing separating them. Everywhere else the manifest is\n * optional and components in their default locations are enough.\n */\nfunction isSkillsDirectory(dir: string): boolean {\n\treturn path.basename(dir) === \"skills\";\n}\n\nexport function discoverPlugins(pluginDirs: string[]): NormalizedPlugin[] {\n\tconst plugins: NormalizedPlugin[] = [];\n\tconst seen = new Set<string>();\n\n\tfor (const dir of pluginDirs) {\n\t\tif (!fs.existsSync(dir)) continue;\n\t\tconst requireManifest = isSkillsDirectory(dir);\n\t\tlet entries: fs.Dirent[];\n\t\ttry {\n\t\t\tentries = fs.readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.isDirectory() && !entry.isSymbolicLink()) continue;\n\t\t\tconst plugin = parsePluginDir(path.join(dir, entry.name), { requireManifest });\n\t\t\tif (plugin && !seen.has(plugin.id)) {\n\t\t\t\tseen.add(plugin.id);\n\t\t\t\tplugins.push(plugin);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn plugins;\n}\n\nexport interface PluginFactoryOptions {\n\t/**\n\t * Load only capabilities that cannot execute: skills, commands, subagents,\n\t * themes. Hooks and MCP servers are skipped and reported.\n\t *\n\t * Set for **project-scoped** plugins — those discovered under the workspace,\n\t * chiefly `<cwd>/.claude/skills/`. That content arrives with a cloned\n\t * repository rather than from the user, and registering shell hooks or\n\t * spawning MCP servers from it with no confirmation is a real escalation over\n\t * reading skill text, which is all a project skills directory gets today.\n\t *\n\t * Claude Code gates the same content behind a workspace trust dialog and\n\t * per-server MCP approval. hoocode has no trust mechanism at all, so it\n\t * withholds the executable half instead of pretending to gate it. If a trust\n\t * gate is ever added, this is the flag it replaces.\n\t * See docs/plugin-system-architecture.md §5.9.\n\t */\n\tpassiveOnly?: boolean;\n}\n\n/** Capabilities withheld from a passive-only plugin, for reporting. */\nexport function withheldCapabilities(plugin: NormalizedPlugin): string[] {\n\treturn [plugin.hooks && \"hooks\", plugin.mcpServers && \"mcp servers\"].filter((x): x is string => !!x);\n}\n\n/** Build a synthetic extension factory that wires one normalized plugin. */\nexport function buildPluginFactory(plugin: NormalizedPlugin, options?: PluginFactoryOptions): ExtensionFactory {\n\tconst passiveOnly = options?.passiveOnly === true;\n\t// Only the executable capabilities can reference the data dir, so it is\n\t// created only when one of them is actually wired — no empty directory per\n\t// installed plugin.\n\tconst wiresExecutables = !passiveOnly && !!(plugin.hooks || plugin.mcpServers);\n\tconst vars = pluginVariables(\n\t\tplugin.root,\n\t\twiresExecutables ? ensurePluginDataDir(plugin.id) : pluginDataDir(plugin.id),\n\t);\n\tconst factory: ExtensionFactory = (hoo: ExtensionAPI) => {\n\t\t// Resources: contribute the plugin's capability directories. Commands map to\n\t\t// the slash-command surface (`.agents/commands`) and agents to subagent\n\t\t// definitions (`.agents/agents`), matching hoocode's native conventions.\n\t\tif (plugin.skillsDir || plugin.commandsDir || plugin.themesDir || plugin.agentsDir) {\n\t\t\thoo.on(\"resources_discover\", () => ({\n\t\t\t\tskillPaths: plugin.skillsDir ? [plugin.skillsDir] : undefined,\n\t\t\t\tthemePaths: plugin.themesDir ? [plugin.themesDir] : undefined,\n\t\t\t\tslashCommandPaths: plugin.commandsDir ? [plugin.commandsDir] : undefined,\n\t\t\t\tagentPaths: plugin.agentsDir ? [plugin.agentsDir] : undefined,\n\t\t\t}));\n\t\t}\n\n\t\t// Providers (native plugins only).\n\t\tfor (const provider of plugin.providers ?? []) {\n\t\t\thoo.registerProvider(provider.name, provider.config);\n\t\t}\n\n\t\t// Hooks: true-parity shell bridge. Executable, so withheld from a\n\t\t// project-scoped plugin (see PluginFactoryOptions.passiveOnly).\n\t\tif (plugin.hooks && !passiveOnly) {\n\t\t\tinstallPluginHooks(hoo, plugin.hooks, plugin.root, vars, () => {\n\t\t\t\t// Non-blocking hook failures are intentionally quiet (Claude Code parity).\n\t\t\t});\n\t\t}\n\n\t\t// MCP servers: register for the hoo-core mcp-loader to connect on session_start.\n\t\t// Executable, so withheld from a project-scoped plugin.\n\t\tif (plugin.mcpServers && !passiveOnly) {\n\t\t\tregisterExtensionMcpServers(plugin.id, resolveMcpServers(plugin.mcpServers, vars));\n\t\t}\n\t};\n\n\tfactory.displayName = `plugin:${plugin.id}`;\n\treturn factory;\n}\n"]}