{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAuBH,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAOhE,OAAO,KAAK,EACX,SAAS,EAET,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EAKpB,MAAM,YAAY,CAAC;AA2GpB;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,gBAAgB,CA+CzD;AA4OD;;GAEG;AACH,wBAAsB,wBAAwB,CAC7C,OAAO,EAAE,gBAAgB,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,gBAAgB,EACzB,aAAa,SAAa,EAC1B,WAAW,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,SAAS,CAAC,CAepB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAwBrH;AA6HD;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAsB,EAChC,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,oBAAoB,CAAC,CAkD/B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAEhF;AAWD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,OAAO,CAEpH;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,EAAE,CAWzF;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAChC,UAAU,EAAE,MAAM,EAAE,EACpB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,gBAAgB,GACvB,OAAO,CAAC;IAAE,UAAU,EAAE,SAAS,EAAE,CAAC;IAAC,MAAM,EAAE,kBAAkB,EAAE,CAAA;CAAE,CAAC,CA8CpE","sourcesContent":["/**\n * Extension loader - loads TypeScript extension modules using jiti.\n *\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport * as _bundledPiAgentCore from \"@kolisachint/hoocode-agent-core\";\nimport * as _bundledPiAi from \"@kolisachint/hoocode-ai\";\nimport * as _bundledPiAiOauth from \"@kolisachint/hoocode-ai/oauth\";\nimport type { KeyId } from \"@kolisachint/hoocode-tui\";\nimport * as _bundledPiTui from \"@kolisachint/hoocode-tui\";\nimport { createJiti } from \"jiti/static\";\n// Static imports of packages that extensions may use.\n// These MUST be static so Bun bundles them into the compiled binary.\n// The virtualModules option then makes them available to extensions.\nimport * as _bundledTypebox from \"typebox\";\nimport * as _bundledTypeboxCompile from \"typebox/compile\";\nimport * as _bundledTypeboxValue from \"typebox/value\";\nimport { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from \"../../config.js\";\n// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,\n// avoiding a circular dependency. Extensions can import from @kolisachint/hoocode-agent.\nimport * as _bundledPiCodingAgent from \"../../index.js\";\nimport { createEventBus, type EventBus } from \"../event-bus.js\";\nimport type { ExecOptions } from \"../exec.js\";\nimport { execCommand } from \"../exec.js\";\nimport { clearExtensionMcpServers } from \"../extension-mcp-servers.js\";\nimport { createSyntheticSourceInfo } from \"../source-info.js\";\nimport { buildPluginFactory, discoverPlugins, pluginExtensionPath, withheldCapabilities } from \"./plugins/index.js\";\nimport { isRepositorySupplied, shouldWithholdRepositorySupplied } from \"./plugins/trust.js\";\nimport type {\n\tExtension,\n\tExtensionAPI,\n\tExtensionFactory,\n\tExtensionLoadIssue,\n\tExtensionRuntime,\n\tLoadExtensionsResult,\n\tMessageRenderer,\n\tProviderConfig,\n\tRegisteredCommand,\n\tToolDefinition,\n} from \"./types.js\";\n\n/** Modules available to extensions via virtualModules (for compiled Bun binary) */\nconst VIRTUAL_MODULES: Record<string, unknown> = {\n\ttypebox: _bundledTypebox,\n\t\"typebox/compile\": _bundledTypeboxCompile,\n\t\"typebox/value\": _bundledTypeboxValue,\n\t\"@sinclair/typebox\": _bundledTypebox,\n\t\"@sinclair/typebox/compile\": _bundledTypeboxCompile,\n\t\"@sinclair/typebox/value\": _bundledTypeboxValue,\n\t\"@kolisachint/hoocode-agent-core\": _bundledPiAgentCore,\n\t\"@kolisachint/hoocode-tui\": _bundledPiTui,\n\t\"@kolisachint/hoocode-ai\": _bundledPiAi,\n\t\"@kolisachint/hoocode-ai/oauth\": _bundledPiAiOauth,\n\t\"@kolisachint/hoocode-agent\": _bundledPiCodingAgent,\n};\n\nconst require = createRequire(import.meta.url);\n\n/**\n * Get aliases for jiti (used in Node.js/development mode).\n * In Bun binary mode, virtualModules is used instead.\n */\nlet _aliases: Record<string, string> | null = null;\n\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\tconst typeboxEntry = require.resolve(\"typebox\");\n\tconst typeboxCompileEntry = require.resolve(\"typebox/compile\");\n\tconst typeboxValueEntry = require.resolve(\"typebox/value\");\n\n\tconst packagesRoot = path.resolve(__dirname, \"../../../../\");\n\t// Prefer the sibling workspace build; fall back to normal package resolution.\n\t// Returns null when neither exists (an unbuilt workspace checkout), so the\n\t// specifier is simply left unaliased instead of breaking every extension.\n\tconst resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string | null => {\n\t\tconst workspacePath = path.join(packagesRoot, workspaceRelativePath);\n\t\tif (fs.existsSync(workspacePath)) {\n\t\t\treturn workspacePath;\n\t\t}\n\t\t// require.resolve, not import.meta.resolve: the latter is undefined once\n\t\t// this module goes through a bundler/test transform, and throwing here\n\t\t// would take down extensions that never import this specifier.\n\t\ttry {\n\t\t\treturn require.resolve(specifier);\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t};\n\n\t_aliases = {\n\t\t\"@kolisachint/hoocode-agent\": packageIndex,\n\t\ttypebox: typeboxEntry,\n\t\t\"typebox/compile\": typeboxCompileEntry,\n\t\t\"typebox/value\": typeboxValueEntry,\n\t\t\"@sinclair/typebox\": typeboxEntry,\n\t\t\"@sinclair/typebox/compile\": typeboxCompileEntry,\n\t\t\"@sinclair/typebox/value\": typeboxValueEntry,\n\t};\n\n\tconst workspaceAliases: Array<[string, string, string]> = [\n\t\t[\"@kolisachint/hoocode-agent-core\", \"agent/dist/index.js\", \"@kolisachint/hoocode-agent-core\"],\n\t\t[\"@kolisachint/hoocode-tui\", \"tui/dist/index.js\", \"@kolisachint/hoocode-tui\"],\n\t\t[\"@kolisachint/hoocode-ai\", \"ai/dist/index.js\", \"@kolisachint/hoocode-ai\"],\n\t\t[\"@kolisachint/hoocode-ai/oauth\", \"ai/dist/oauth.js\", \"@kolisachint/hoocode-ai/oauth\"],\n\t];\n\tfor (const [alias, workspaceRelativePath, specifier] of workspaceAliases) {\n\t\tconst entry = resolveWorkspaceOrImport(workspaceRelativePath, specifier);\n\t\tif (entry) {\n\t\t\t_aliases[alias] = entry;\n\t\t}\n\t}\n\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\nfunction resolvePath(extPath: string, cwd: string): string {\n\tconst expanded = expandPath(extPath);\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\treturn path.resolve(cwd, expanded);\n}\n\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\n/**\n * Create a runtime with throwing stubs for action methods.\n * Runner.bindCore() replaces these with real implementations.\n */\nexport function createExtensionRuntime(): ExtensionRuntime {\n\tconst notInitialized = () => {\n\t\tthrow new Error(\"Extension runtime not initialized. Action methods cannot be called during extension loading.\");\n\t};\n\tconst state: { staleMessage?: string } = {};\n\tconst assertActive = () => {\n\t\tif (state.staleMessage) {\n\t\t\tthrow new Error(state.staleMessage);\n\t\t}\n\t};\n\n\tconst runtime: ExtensionRuntime = {\n\t\tsendMessage: notInitialized,\n\t\tsendUserMessage: notInitialized,\n\t\tappendEntry: notInitialized,\n\t\tsetSessionName: notInitialized,\n\t\tgetSessionName: notInitialized,\n\t\tsetLabel: notInitialized,\n\t\tgetActiveTools: notInitialized,\n\t\tgetAllTools: notInitialized,\n\t\tsetActiveTools: notInitialized,\n\t\t// registerTool() is valid during extension load; refresh is only needed post-bind.\n\t\trefreshTools: () => {},\n\t\tgetCommands: notInitialized,\n\t\tsetModel: () => Promise.reject(new Error(\"Extension runtime not initialized\")),\n\t\tgetThinkingLevel: notInitialized,\n\t\tsetThinkingLevel: notInitialized,\n\t\tflagValues: new Map(),\n\t\tpendingProviderRegistrations: [],\n\t\tmodeSearchPaths: [],\n\t\tassertActive,\n\t\tinvalidate: (message) => {\n\t\t\tstate.staleMessage ??=\n\t\t\t\tmessage ??\n\t\t\t\t\"This extension ctx is stale after session replacement or reload. Do not use a captured hoo or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().\";\n\t\t},\n\t\t// Pre-bind: queue registrations so bindCore() can flush them once the\n\t\t// model registry is available. bindCore() replaces both with direct calls.\n\t\tregisterProvider: (name, config, extensionPath = \"<unknown>\") => {\n\t\t\truntime.pendingProviderRegistrations.push({ name, config, extensionPath });\n\t\t},\n\t\tunregisterProvider: (name) => {\n\t\t\truntime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);\n\t\t},\n\t};\n\n\treturn runtime;\n}\n\n/**\n * Create the ExtensionAPI for an extension.\n * Registration methods write to the extension object.\n * Action methods delegate to the shared runtime.\n */\nfunction createExtensionAPI(\n\textension: Extension,\n\truntime: ExtensionRuntime,\n\tcwd: string,\n\teventBus: EventBus,\n): ExtensionAPI {\n\tconst api = {\n\t\t// Registration methods - write to extension\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\truntime.assertActive();\n\t\t\tconst list = extension.handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\textension.handlers.set(event, list);\n\t\t},\n\n\t\tregisterTool(tool: ToolDefinition): void {\n\t\t\truntime.assertActive();\n\t\t\textension.tools.set(tool.name, {\n\t\t\t\tdefinition: tool,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t});\n\t\t\truntime.refreshTools();\n\t\t},\n\n\t\tregisterCommand(name: string, options: Omit<RegisteredCommand, \"name\" | \"sourceInfo\">): void {\n\t\t\truntime.assertActive();\n\t\t\textension.commands.set(name, {\n\t\t\t\tname,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t\t...options,\n\t\t\t});\n\t\t},\n\n\t\tregisterShortcut(\n\t\t\tshortcut: KeyId,\n\t\t\toptions: {\n\t\t\t\tdescription?: string;\n\t\t\t\thandler: (ctx: import(\"./types.js\").ExtensionContext) => Promise<void> | void;\n\t\t\t},\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });\n\t\t},\n\n\t\tregisterFlag(\n\t\t\tname: string,\n\t\t\toptions: { description?: string; type: \"boolean\" | \"string\"; default?: boolean | string },\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.flags.set(name, { name, extensionPath: extension.path, ...options });\n\t\t\tif (options.default !== undefined && !runtime.flagValues.has(name)) {\n\t\t\t\truntime.flagValues.set(name, options.default);\n\t\t\t}\n\t\t},\n\n\t\tregisterMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {\n\t\t\truntime.assertActive();\n\t\t\textension.messageRenderers.set(customType, renderer as MessageRenderer);\n\t\t},\n\n\t\t// Flag access - checks extension registered it, reads from runtime\n\t\tgetFlag(name: string): boolean | string | undefined {\n\t\t\truntime.assertActive();\n\t\t\tif (!extension.flags.has(name)) return undefined;\n\t\t\treturn runtime.flagValues.get(name);\n\t\t},\n\n\t\taddModeSearchPath(dirPath: string): void {\n\t\t\truntime.assertActive();\n\t\t\tconst resolved = resolvePath(dirPath, cwd);\n\t\t\tif (!runtime.modeSearchPaths.includes(resolved)) {\n\t\t\t\truntime.modeSearchPaths.push(resolved);\n\t\t\t}\n\t\t},\n\n\t\tgetModeSearchPaths(): string[] {\n\t\t\truntime.assertActive();\n\t\t\treturn [...runtime.modeSearchPaths];\n\t\t},\n\n\t\t// Action methods - delegate to shared runtime\n\t\tsendMessage(message, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendMessage(message, options);\n\t\t},\n\n\t\tsendUserMessage(content, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendUserMessage(content, options);\n\t\t},\n\n\t\tappendEntry(customType: string, data?: unknown): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.appendEntry(customType, data);\n\t\t},\n\n\t\tsetSessionName(name: string): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setSessionName(name);\n\t\t},\n\n\t\tgetSessionName(): string | undefined {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getSessionName();\n\t\t},\n\n\t\tsetLabel(entryId: string, label: string | undefined): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setLabel(entryId, label);\n\t\t},\n\n\t\texec(command: string, args: string[], options?: ExecOptions) {\n\t\t\truntime.assertActive();\n\t\t\treturn execCommand(command, args, options?.cwd ?? cwd, options);\n\t\t},\n\n\t\tgetActiveTools(): string[] {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getActiveTools();\n\t\t},\n\n\t\tgetAllTools() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getAllTools();\n\t\t},\n\n\t\tsetActiveTools(toolNames: string[]): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setActiveTools(toolNames);\n\t\t},\n\n\t\tgetCommands() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getCommands();\n\t\t},\n\n\t\tsetModel(model) {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.setModel(model);\n\t\t},\n\n\t\tgetThinkingLevel() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getThinkingLevel();\n\t\t},\n\n\t\tsetThinkingLevel(level) {\n\t\t\truntime.assertActive();\n\t\t\truntime.setThinkingLevel(level);\n\t\t},\n\n\t\tregisterProvider(name: string, config: ProviderConfig) {\n\t\t\truntime.assertActive();\n\t\t\truntime.registerProvider(name, config, extension.path);\n\t\t},\n\n\t\tunregisterProvider(name: string) {\n\t\t\truntime.assertActive();\n\t\t\truntime.unregisterProvider(name, extension.path);\n\t\t},\n\n\t\tevents: eventBus,\n\t} as ExtensionAPI;\n\n\treturn api;\n}\n\nasync function loadExtensionModule(extensionPath: string) {\n\tconst jiti = createJiti(import.meta.url, {\n\t\tmoduleCache: false,\n\t\t// In Bun binary: use virtualModules for bundled packages (no filesystem resolution)\n\t\t// Also disable tryNative so jiti handles ALL imports (not just the entry point)\n\t\t// In Node.js/dev: use aliases to resolve to node_modules paths\n\t\t...(isBunBinary ? { virtualModules: VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() }),\n\t});\n\n\tconst module = await jiti.import(extensionPath, { default: true });\n\tconst factory = module as ExtensionFactory;\n\treturn typeof factory !== \"function\" ? undefined : factory;\n}\n\n/**\n * Create an Extension object with empty collections.\n */\nfunction createExtension(extensionPath: string, resolvedPath: string): Extension {\n\tconst source =\n\t\textensionPath.startsWith(\"<\") && extensionPath.endsWith(\">\")\n\t\t\t? extensionPath.slice(1, -1).split(\":\")[0] || \"temporary\"\n\t\t\t: \"local\";\n\tconst baseDir = extensionPath.startsWith(\"<\") ? undefined : path.dirname(resolvedPath);\n\n\treturn {\n\t\tpath: extensionPath,\n\t\tresolvedPath,\n\t\tsourceInfo: createSyntheticSourceInfo(extensionPath, { source, baseDir }),\n\t\thandlers: new Map(),\n\t\ttools: new Map(),\n\t\tmessageRenderers: new Map(),\n\t\tcommands: new Map(),\n\t\tflags: new Map(),\n\t\tshortcuts: new Map(),\n\t};\n}\n\nasync function loadExtension(\n\textensionPath: string,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n): Promise<{ extension: Extension | null; error: string | null }> {\n\tconst resolvedPath = resolvePath(extensionPath, cwd);\n\n\ttry {\n\t\tconst factory = await loadExtensionModule(resolvedPath);\n\t\tif (!factory) {\n\t\t\treturn { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` };\n\t\t}\n\n\t\tconst extension = createExtension(extensionPath, resolvedPath);\n\t\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\t\tawait factory(api);\n\n\t\treturn { extension, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { extension: null, error: `Failed to load extension: ${message}` };\n\t}\n}\n\n/**\n * Create an Extension from an inline factory function.\n */\nexport async function loadExtensionFromFactory(\n\tfactory: ExtensionFactory,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\textensionPath = \"<inline>\",\n\tdisplayName?: string,\n): Promise<Extension> {\n\tconst effectivePath = displayName ?? extensionPath;\n\tconst extension = createExtension(effectivePath, extensionPath);\n\tif (displayName) {\n\t\textension.sourceInfo = createSyntheticSourceInfo(displayName, {\n\t\t\tsource: \"inline\",\n\t\t\tscope: \"temporary\",\n\t\t\torigin: \"top-level\",\n\t\t});\n\t}\n\textension.displayName = displayName;\n\textension.internal = factory.internal === true;\n\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\tawait factory(api);\n\treturn extension;\n}\n\n/**\n * Load extensions from paths.\n */\nexport async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {\n\tconst extensions: Extension[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst resolvedEventBus = eventBus ?? createEventBus();\n\tconst runtime = createExtensionRuntime();\n\n\tfor (const extPath of paths) {\n\t\tconst { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, runtime);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: extPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (extension) {\n\t\t\textensions.push(extension);\n\t\t}\n\t}\n\n\treturn {\n\t\textensions,\n\t\terrors,\n\t\truntime,\n\t};\n}\n\ninterface HooCodeManifest {\n\textensions?: string[];\n\tthemes?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n}\n\n/**\n * Read a package's resource manifest.\n *\n * `hoocode` is the field. `pi` is a deprecated alias kept for packages authored\n * against the upstream project this one grew out of: dropping it would break\n * third-party packages that are still published and still work, and the cost of\n * keeping it is one branch. It is not a rename in progress -- nothing in this\n * repository writes `pi`, the scaffolders emit `hoocode`, and a package that\n * declares both gets `hoocode`.\n */\nfunction readHooCodeManifest(packageJsonPath: string): HooCodeManifest | null {\n\ttry {\n\t\tconst content = fs.readFileSync(packageJsonPath, \"utf-8\");\n\t\tconst pkg = JSON.parse(content);\n\t\tif (pkg.hoocode && typeof pkg.hoocode === \"object\") {\n\t\t\treturn pkg.hoocode as HooCodeManifest;\n\t\t}\n\t\t// Deprecated alias; see above.\n\t\tif (pkg.pi && typeof pkg.pi === \"object\") {\n\t\t\treturn pkg.pi as HooCodeManifest;\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction isExtensionFile(name: string): boolean {\n\treturn name.endsWith(\".ts\") || name.endsWith(\".js\");\n}\n\n/**\n * Resolve extension entry points from a directory.\n *\n * Checks for:\n * 1. package.json with a \"hoocode.extensions\" field -> returns declared paths\n * 2. index.ts or index.js -> returns the index file\n *\n * Returns resolved paths or null if no entry points found.\n */\nfunction resolveExtensionEntries(dir: string): string[] | null {\n\t// A declared manifest wins over a bare index file.\n\tconst packageJsonPath = path.join(dir, \"package.json\");\n\tif (fs.existsSync(packageJsonPath)) {\n\t\tconst manifest = readHooCodeManifest(packageJsonPath);\n\t\tif (manifest?.extensions?.length) {\n\t\t\tconst entries: string[] = [];\n\t\t\tfor (const extPath of manifest.extensions) {\n\t\t\t\tconst resolvedExtPath = path.resolve(dir, extPath);\n\t\t\t\tif (fs.existsSync(resolvedExtPath)) {\n\t\t\t\t\tentries.push(resolvedExtPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (entries.length > 0) {\n\t\t\t\treturn entries;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for index.ts or index.js\n\tconst indexTs = path.join(dir, \"index.ts\");\n\tconst indexJs = path.join(dir, \"index.js\");\n\tif (fs.existsSync(indexTs)) {\n\t\treturn [indexTs];\n\t}\n\tif (fs.existsSync(indexJs)) {\n\t\treturn [indexJs];\n\t}\n\n\treturn null;\n}\n\n/**\n * Discover extensions in a directory.\n *\n * Discovery rules:\n * 1. Direct files: `extensions/*.ts` or `*.js` → load\n * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load\n * 3. Subdirectory with package.json: `extensions/* /package.json` with a \"hoocode\" field → load what it declares\n *\n * No recursion beyond one level. Complex packages must use package.json manifest.\n */\nfunction discoverExtensionsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst discovered: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst entryPath = path.join(dir, entry.name);\n\n\t\t\t// 1. Direct files: *.ts or *.js\n\t\t\tif ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {\n\t\t\t\tdiscovered.push(entryPath);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 2 & 3. Subdirectories\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\tconst entries = resolveExtensionEntries(entryPath);\n\t\t\t\tif (entries) {\n\t\t\t\t\tdiscovered.push(...entries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn discovered;\n}\n\n/**\n * Discover and load extensions from standard locations.\n */\nexport async function discoverAndLoadExtensions(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n\teventBus?: EventBus,\n): Promise<LoadExtensionsResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/\n\tconst localExtDir = path.join(cwd, CONFIG_DIR_NAME, \"extensions\");\n\taddPaths(discoverExtensionsInDir(localExtDir));\n\n\t// 2. Global extensions: agentDir/extensions/\n\tconst globalExtDir = path.join(agentDir, \"extensions\");\n\taddPaths(discoverExtensionsInDir(globalExtDir));\n\n\t// 3. Explicitly configured paths\n\tfor (const p of configuredPaths) {\n\t\tconst resolved = resolvePath(p, cwd);\n\t\tif (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n\t\t\t// Check for package.json with a hoocode manifest, or an index.ts\n\t\t\tconst entries = resolveExtensionEntries(resolved);\n\t\t\tif (entries) {\n\t\t\t\taddPaths(entries);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// No explicit entries - discover individual files in directory\n\t\t\taddPaths(discoverExtensionsInDir(resolved));\n\t\t\tcontinue;\n\t\t}\n\n\t\taddPaths([resolved]);\n\t}\n\n\tconst resolvedEventBus = eventBus ?? createEventBus();\n\tconst result = await loadExtensions(allPaths, cwd, resolvedEventBus);\n\n\t// Plugins: directories under plugins/ with a recognized manifest.\n\tconst pluginDirs = defaultPluginDirs(cwd, agentDir);\n\tconst pluginResult = await loadPlugins(pluginDirs, cwd, resolvedEventBus, result.runtime);\n\tresult.extensions.push(...pluginResult.extensions);\n\tresult.errors.push(...pluginResult.errors);\n\n\treturn result;\n}\n\n/**\n * Whether a discovered plugin sits in the working tree, and therefore came with\n * the repository as far as anyone but its installer can tell.\n *\n * All three project paths count: `<cwd>/.claude/skills` (the vendor convention\n * for repo-committed plugins) and `<cwd>/.agents/plugins` + `<cwd>/.hoocode/plugins`\n * (hoocode's own project-scope install homes). An earlier version listed only the\n * first, reasoning that the hoocode homes held plugins \"the user installed\n * deliberately\" — true of the person who ran the install, and false for every\n * collaborator who clones the result. Location cannot tell those two apart, so it\n * is the wrong thing to ask; {@link isWorkspaceTrusted} asks the right one.\n */\nexport function isProjectSuppliedPlugin(pluginRoot: string, cwd: string): boolean {\n\treturn isRepositorySupplied(pluginRoot, pluginProjectScopeRoots(cwd));\n}\n\n/** A plugin's project-scope homes — the locations that travel with a clone. */\nfunction pluginProjectScopeRoots(cwd: string): string[] {\n\treturn [\n\t\tpath.join(cwd, \".claude\", \"skills\"),\n\t\tpath.join(cwd, \".agents\", \"plugins\"),\n\t\tpath.join(cwd, CONFIG_DIR_NAME, \"plugins\"),\n\t];\n}\n\n/**\n * Whether a plugin's **executable** capabilities (hooks, MCP servers) should be\n * withheld: it lives in the working tree and this machine has not trusted the\n * workspace.\n *\n * Passive capabilities always load. Reading a repository's skill text is what\n * opening the repository already implies; starting its processes is not.\n */\nexport function shouldWithholdExecutables(pluginRoot: string, cwd: string, agentDir: string = getAgentDir()): boolean {\n\treturn shouldWithholdRepositorySupplied(pluginRoot, cwd, pluginProjectScopeRoots(cwd), agentDir);\n}\n\n/**\n * Standard plugin discovery directories, highest precedence first.\n *\n * `.agents/plugins/` is the cross-vendor, primary home and is listed ahead of the\n * `.hoocode/plugins/` fallback at each scope, so an `.agents`-installed plugin\n * wins over a same-id `.hoocode` one (discoverPlugins is first-wins by id).\n * Project scope beats global. The global surfaces live next to the agent dir\n * (`~/.agents`, `~/.claude` alongside `~/.hoocode`), so they stay parameterized\n * on `agentDir` rather than hardcoding the home directory.\n *\n * Two of these are *production homes* for plugins hoocode authored, and two are\n * skills directories:\n *\n * - `<cwd>/.agents/plugins` is the legacy project-local install home. Nothing\n *   writes there any more; it is read so plugins installed by older versions\n *   keep working.\n * - `.claude/skills` (project and personal) implements Claude Code's\n *   skills-directory plugins: a folder there carrying `.claude-plugin/plugin.json`\n *   is a plugin, and a folder with only a `SKILL.md` stays a plain skill —\n *   `parsePluginDir` returns null for the latter, which is exactly the vendor's\n *   own rule, so no special-casing is needed here.\n *\n * See docs/plugin-system-architecture.md §5.3 and §5.7.\n */\nexport function defaultPluginDirs(cwd: string, agentDir: string = getAgentDir()): string[] {\n\tconst home = path.dirname(agentDir);\n\treturn [\n\t\tpath.join(cwd, \".agents\", \"plugins\"),\n\t\tpath.join(cwd, CONFIG_DIR_NAME, \"plugins\"),\n\t\tpath.join(cwd, \".claude\", \"skills\"),\n\t\tpath.join(home, \".agents\", \"plugins\"),\n\t\tpath.join(home, \".agents\", \"publish\", \"github\"),\n\t\tpath.join(home, \".claude\", \"skills\"),\n\t\tpath.join(agentDir, \"plugins\"),\n\t];\n}\n\n/**\n * Discover plugins under `pluginDirs` and load each as a synthetic extension into\n * the given runtime/event bus. Clears the extension MCP registry first so reloads\n * rebuild the set cleanly.\n */\nexport async function loadPlugins(\n\tpluginDirs: string[],\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n): Promise<{ extensions: Extension[]; errors: ExtensionLoadIssue[] }> {\n\tclearExtensionMcpServers();\n\tconst extensions: Extension[] = [];\n\tconst errors: ExtensionLoadIssue[] = [];\n\n\tfor (const plugin of discoverPlugins(pluginDirs)) {\n\t\ttry {\n\t\t\t// Withhold the executable half of a plugin that lives in the working tree\n\t\t\t// until this machine has trusted the workspace. Skills, commands and\n\t\t\t// subagents still load — reading a repository's text is what opening it\n\t\t\t// already implies. See PluginFactoryOptions.passiveOnly.\n\t\t\tconst passiveOnly = shouldWithholdExecutables(plugin.root, cwd);\n\t\t\tconst withheld = passiveOnly ? withheldCapabilities(plugin) : [];\n\t\t\tconst extension = await loadExtensionFromFactory(\n\t\t\t\tbuildPluginFactory(plugin, { passiveOnly }),\n\t\t\t\tcwd,\n\t\t\t\teventBus,\n\t\t\t\truntime,\n\t\t\t\tpluginExtensionPath(plugin.id),\n\t\t\t\t`plugin:${plugin.id}`,\n\t\t\t);\n\t\t\textensions.push(extension);\n\t\t\tif (withheld.length > 0) {\n\t\t\t\t// A warning, not an error: the plugin *did* load, minus its executable\n\t\t\t\t// half, and the session must reach the prompt for `/plugin trust` to be\n\t\t\t\t// runnable at all. Reporting this as an error aborted startup, which\n\t\t\t\t// left the only remedy behind a door it had just locked.\n\t\t\t\terrors.push({\n\t\t\t\t\tpath: plugin.manifestPath,\n\t\t\t\t\tseverity: \"warning\",\n\t\t\t\t\terror:\n\t\t\t\t\t\t`Plugin \"${plugin.id}\" is in the working tree: ${withheld.join(\" and \")} not loaded. ` +\n\t\t\t\t\t\t\"Code committed to a repository runs for whoever clones it, so hoocode does not start it \" +\n\t\t\t\t\t\t\"until you say this directory is yours to run code from. Run `/plugin trust` to allow it here, \" +\n\t\t\t\t\t\t\"or install the plugin at user scope instead.\",\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (err) {\n\t\t\terrors.push({\n\t\t\t\tpath: plugin.manifestPath,\n\t\t\t\terror: `Failed to load plugin \"${plugin.id}\": ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn { extensions, errors };\n}\n"]}