diff --git a/CHANGELOG.md b/CHANGELOG.md index cdcf5b03178..899824aad19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai - Web search/Exa: accept `plugins.entries.exa.config.webSearch.baseUrl`, normalize it to the Exa `/search` endpoint, and partition cached results by endpoint. Fixes #54928 and supersedes #54939. Thanks @mrpl327 and @lyfuci. - Web search/MiniMax: include MiniMax Search in the web-search setup flow and let `MINIMAX_API_KEY` participate in MiniMax Search auto-detection. Supersedes #65828. Thanks @Jah-yee. - Plugins/ClawHub: preserve official source-linked trust through archive installs, so OpenClaw can install trusted ClawHub plugin packages that trigger the built-in dangerous-pattern scanner. Thanks @vincentkoc. +- Plugins/ClawHub: install package runtime dependencies for archive-backed plugin installs, so ClawHub packages such as WhatsApp load declared dependencies after download. Thanks @vincentkoc. - Providers/LM Studio: allow `models.providers.lmstudio.params.preload: false` to skip OpenClaw's native model-load call so LM Studio JIT loading, idle TTL, and auto-evict can own model lifecycle. Fixes #75921. Thanks @garyd9. - Telegram: inherit the process DNS result order for Bot API transport and downgrade recovered sticky IPv4 fallback promotions to debug logs, while keeping pinned-IP escalation warnings visible. Fixes #75904. Thanks @highfly-hi and @neeravmakwana. - Sessions: keep durable external conversation pointers, including group and thread-scoped chat sessions, out of age, count, and disk-budget maintenance eviction while still allowing synthetic runtime entries to age out. Fixes #58088. Thanks @drinkflav. diff --git a/src/plugins/install.test.ts b/src/plugins/install.test.ts index 22f97dcfc62..ee8cf313483 100644 --- a/src/plugins/install.test.ts +++ b/src/plugins/install.test.ts @@ -622,7 +622,7 @@ beforeEach(() => { }); describe("installPluginFromArchive", () => { - it("does not run npm for package archive runtime dependencies", async () => { + it("installs package archive runtime dependencies", async () => { const result = await installArchivePackageAndReturnResult({ packageJson: { name: "archive-with-deps", @@ -635,7 +635,12 @@ describe("installPluginFromArchive", () => { }); expect(result.ok).toBe(true); - expect(vi.mocked(runCommandWithTimeout)).not.toHaveBeenCalled(); + expect(vi.mocked(runCommandWithTimeout)).toHaveBeenCalledWith( + expect.arrayContaining(["npm", "install"]), + expect.objectContaining({ + cwd: expect.stringContaining(".openclaw-install-stage-"), + }), + ); }); it("installs scoped archives, rejects duplicate installs, and allows updates", async () => { diff --git a/src/plugins/install.ts b/src/plugins/install.ts index d45a551a11a..4bf4b06bc3a 100644 --- a/src/plugins/install.ts +++ b/src/plugins/install.ts @@ -178,6 +178,13 @@ function buildDirectoryInstallResult(params: { }; } +function hasPackageRuntimeDependencies(manifest: PackageManifest): boolean { + return ( + Object.keys(manifest.dependencies ?? {}).length > 0 || + Object.keys(manifest.optionalDependencies ?? {}).length > 0 + ); +} + function buildBlockedInstallResult(params: { blocked: NonNullable["blocked"]>; }): Extract { @@ -560,6 +567,7 @@ type ValidatedPackagePlugin = { manifestName?: string; version?: string; extensions: string[]; + hasRuntimeDependencies: boolean; peerDependencies: Record; }; @@ -719,6 +727,7 @@ async function validatePackagePluginInstallSource(params: { manifestName: pkgName || undefined, version: typeof manifest.version === "string" ? manifest.version : undefined, extensions, + hasRuntimeDependencies: hasPackageRuntimeDependencies(manifest), peerDependencies: manifest.peerDependencies ?? {}, }, }; @@ -855,8 +864,9 @@ async function installPluginFromPackageDir( mode: preparedTarget.effectiveMode, dryRun, copyErrorPrefix: "failed to copy plugin", - hasDeps: false, - depsLogMessage: "", + hasDeps: + plugin.hasRuntimeDependencies && params.installPolicyRequest?.kind === "plugin-archive", + depsLogMessage: "Installing plugin dependencies…", nameEncoder: encodePluginInstallDirName, afterInstall: async (installedDir) => { return await scanAndLinkInstalledPackage({