diff --git a/src/agents/sessions/extensions/loader.test.ts b/src/agents/sessions/extensions/loader.test.ts index d428ac26f34..2ad8527a402 100644 --- a/src/agents/sessions/extensions/loader.test.ts +++ b/src/agents/sessions/extensions/loader.test.ts @@ -50,4 +50,38 @@ export default async function(api) { expect(result.extensions).toHaveLength(1); expect(result.extensions[0]?.commands.has("sdk-subpath-probe")).toBe(true); }); + + it("resolves generic plugin SDK subpaths through the shared plugin loader aliases", async () => { + const dir = await mkdtemp(join(tmpdir(), "openclaw-extension-sdk-")); + tempDirs.push(dir); + const extensionPath = join(dir, "extension.ts"); + await writeFile( + extensionPath, + ` +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { defineTool } from "@openclaw/plugin-sdk/agent-sessions"; + +export default async function(api) { + if (normalizeLowercaseStringOrEmpty(" MIXED ") !== "mixed") { + throw new Error("generic sdk subpath unavailable"); + } + const tool = defineTool({ + name: "shared-sdk-probe", + description: "probe", + parameters: { type: "object", properties: {}, additionalProperties: false }, + handler() { + return { content: [{ type: "text", text: "ok" }] }; + }, + }); + api.registerTool(tool); +} +`, + ); + + const result = await loadExtensions([extensionPath], dir); + + expect(result.errors).toEqual([]); + expect(result.extensions).toHaveLength(1); + expect(result.extensions[0]?.tools.has("shared-sdk-probe")).toBe(true); + }); }); diff --git a/src/agents/sessions/extensions/loader.ts b/src/agents/sessions/extensions/loader.ts index f3239b56fcd..6099bf43f1b 100644 --- a/src/agents/sessions/extensions/loader.ts +++ b/src/agents/sessions/extensions/loader.ts @@ -8,8 +8,6 @@ import { createRequire } from "node:module"; import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; -import type { KeyId } from "@earendil-works/pi-tui"; -import * as bundledTui from "@earendil-works/pi-tui"; import { createJiti } from "jiti/static"; import * as bundledLlm from "openclaw/plugin-sdk/llm"; import * as bundledLlmAnthropic from "openclaw/plugin-sdk/llm-anthropic"; @@ -26,6 +24,10 @@ import * as bundledLlmProviderRuntime from "openclaw/plugin-sdk/llm-provider-run import * as bundledTypebox from "typebox"; import * as bundledTypeboxCompile from "typebox/compile"; import * as bundledTypeboxValue from "typebox/value"; +import { + buildPluginLoaderAliasMap, + buildPluginLoaderJitiOptions, +} from "../../../plugins/sdk-alias.js"; import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.js"; import * as bundledAgentCore from "../../runtime/index.js"; import { createEventBus, type EventBus } from "../event-bus.js"; @@ -38,6 +40,7 @@ import type { ExtensionAPI, ExtensionFactory, ExtensionRuntime, + ExtensionShortcut, LoadExtensionsResult, MessageRenderer, ProviderConfig, @@ -54,7 +57,6 @@ const VIRTUAL_MODULES: Record = { "@sinclair/typebox/compile": bundledTypeboxCompile, "@sinclair/typebox/value": bundledTypeboxValue, "openclaw/plugin-sdk/agent-core": bundledAgentCore, - "@earendil-works/pi-tui": bundledTui, "openclaw/plugin-sdk/llm": bundledLlm, "openclaw/plugin-sdk/llm-anthropic": bundledLlmAnthropic, "openclaw/plugin-sdk/llm-bedrock": bundledLlmBedrock, @@ -65,93 +67,36 @@ const VIRTUAL_MODULES: Record = { "openclaw/plugin-sdk/llm-openai-responses": bundledLlmOpenAiResponses, "openclaw/plugin-sdk/llm-provider-runtime": bundledLlmProviderRuntime, "openclaw/plugin-sdk/agent-sessions": bundledAgentSessions, + "@openclaw/plugin-sdk/agent-sessions": bundledAgentSessions, }; const require = createRequire(import.meta.url); -/** - * Get aliases for jiti (used in Node.js/development mode). - * In Bun binary mode, virtualModules is used instead. - */ let aliases: Record | null = null; -const PLUGIN_SDK_SOURCE_ALIASES = { - "openclaw/plugin-sdk/agent-core": "src/plugin-sdk/agent-core.ts", - "openclaw/plugin-sdk/llm": "src/plugin-sdk/llm.ts", - "openclaw/plugin-sdk/llm-anthropic": "src/plugin-sdk/llm-anthropic.ts", - "openclaw/plugin-sdk/llm-bedrock": "src/plugin-sdk/llm-bedrock.ts", - "openclaw/plugin-sdk/llm-google-shared": "src/plugin-sdk/llm-google-shared.ts", - "openclaw/plugin-sdk/llm-oauth": "src/plugin-sdk/llm-oauth.ts", - "openclaw/plugin-sdk/llm-openai-codex-responses": "src/plugin-sdk/llm-openai-codex-responses.ts", - "openclaw/plugin-sdk/llm-openai-completions": "src/plugin-sdk/llm-openai-completions.ts", - "openclaw/plugin-sdk/llm-openai-responses": "src/plugin-sdk/llm-openai-responses.ts", - "openclaw/plugin-sdk/llm-provider-runtime": "src/plugin-sdk/llm-provider-runtime.ts", -} as const; - -function findPackageRoot(startDir: string): string { - let current = startDir; - while (true) { - if (fs.existsSync(path.join(current, "package.json"))) { - return current; - } - const parent = path.dirname(current); - if (parent === current) { - return startDir; - } - current = parent; - } +function resolveExtensionSafeAgentSessionsEntry(): string { + const currentDirname = path.dirname(fileURLToPath(import.meta.url)); + const jsEntry = path.resolve(currentDirname, "..", "extension-sdk.js"); + return fs.existsSync(jsEntry) ? jsEntry : path.resolve(currentDirname, "..", "extension-sdk.ts"); } -function resolveModuleEntryForJiti(params: { moduleId: string; sourcePath?: string }): string { - const currentModuleDir = path.dirname(fileURLToPath(import.meta.url)); - const sourceEntry = params.sourcePath - ? path.join(findPackageRoot(currentModuleDir), params.sourcePath) - : undefined; - if ( - sourceEntry && - currentModuleDir.split(path.sep).includes("src") && - fs.existsSync(sourceEntry) - ) { - return sourceEntry; - } - - const resolved = fileURLToPath(import.meta.resolve(params.moduleId)); - if (fs.existsSync(resolved) || !params.sourcePath) { - return resolved; - } - - return sourceEntry && fs.existsSync(sourceEntry) ? sourceEntry : resolved; -} - -function resolvePluginSdkAliasesForJiti(): Record { - return Object.fromEntries( - Object.entries(PLUGIN_SDK_SOURCE_ALIASES).map(([moduleId, sourcePath]) => [ - moduleId, - resolveModuleEntryForJiti({ moduleId, sourcePath }), - ]), - ); -} - -function getAliases(): Record { +function getExtensionLoaderAliases(): Record { if (aliases) { return aliases; } - const currentDirname = path.dirname(fileURLToPath(import.meta.url)); - const agentSessionsEntry = fs.existsSync(path.resolve(currentDirname, "..", "extension-sdk.js")) - ? path.resolve(currentDirname, "..", "extension-sdk.js") - : path.resolve(currentDirname, "..", "extension-sdk.ts"); - + const agentSessionsEntry = resolveExtensionSafeAgentSessionsEntry(); const typeboxEntry = require.resolve("typebox"); const typeboxCompileEntry = require.resolve("typebox/compile"); const typeboxValueEntry = require.resolve("typebox/value"); - - const tuiEntry = fileURLToPath(import.meta.resolve("@earendil-works/pi-tui")); + const loaderModulePath = fileURLToPath(import.meta.url); aliases = { + ...buildPluginLoaderAliasMap(loaderModulePath, process.argv[1], import.meta.url), + // The public agent-sessions export includes the resource loader. Extensions + // load through the resource loader, so use the cycle-safe SDK barrel here. "openclaw/plugin-sdk/agent-sessions": agentSessionsEntry, - ...resolvePluginSdkAliasesForJiti(), - "@earendil-works/pi-tui": tuiEntry, + "@openclaw/plugin-sdk/agent-sessions": agentSessionsEntry, typebox: typeboxEntry, "typebox/compile": typeboxCompileEntry, "typebox/value": typeboxValueEntry, @@ -285,7 +230,7 @@ function createExtensionAPI( }, registerShortcut( - shortcut: KeyId, + shortcut: ExtensionShortcut["shortcut"], options: { description?: string; handler: (ctx: import("./types.js").ExtensionContext) => Promise | void; @@ -409,13 +354,16 @@ function createExtensionAPI( async function loadExtensionModule(extensionPath: string) { const jiti = createJiti(import.meta.url, { - moduleCache: false, - // In Bun binary: use virtualModules for bundled packages (no filesystem resolution) - // Also disable tryNative so jiti handles ALL imports (not just the entry point) - // In Node.js/dev: use aliases to resolve to node_modules paths ...(isBunBinary - ? { virtualModules: VIRTUAL_MODULES, tryNative: false } - : { alias: getAliases() }), + ? { + ...buildPluginLoaderJitiOptions({}), + // Bun binaries need virtual modules because extension SDK files are + // bundled into the executable rather than present on disk. + tryNative: false, + virtualModules: VIRTUAL_MODULES, + } + : buildPluginLoaderJitiOptions(getExtensionLoaderAliases())), + moduleCache: false, }); const module = await jiti.import(extensionPath, { default: true });