From 8d7f4d28ce0d897d4549bcbb9f707351ca514552 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 02:05:51 +0100 Subject: [PATCH] fix: load source bundled plugins from pnpm workspaces --- CHANGELOG.md | 1 + README.md | 5 +++- docs/plugins/building-plugins.md | 4 ++- docs/plugins/dependency-resolution.md | 6 ++++ docs/start/setup.md | 17 ++++------- docs/tools/plugin.md | 6 ++++ src/plugins/bundled-dir.test.ts | 41 +++++++++++++++++++++++---- src/plugins/bundled-dir.ts | 14 +++++---- 8 files changed, 69 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd0da3ffa09..a58164cb2b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Plugins/source checkout: load bundled plugins from the `extensions/*` pnpm workspace tree in source checkouts, so plugin-local dependencies and edits are used directly while packaged installs keep using the built runtime tree. Thanks @vincentkoc. - Plugins/ClawHub: prefer versioned ClawPack artifacts when ClawHub publishes digest metadata, verifying the ClawPack response header and downloaded bytes before installing. Thanks @vincentkoc. - Plugins/ClawHub: persist ClawPack digest metadata on ClawHub plugin install and update records so registry refreshes and download verification can reuse stored artifact facts. Thanks @vincentkoc. - Providers/OpenAI: add `extraBody`/`extra_body` passthrough for OpenAI-compatible TTS endpoints, so custom speech servers can receive fields such as `lang` in `/audio/speech` requests. Fixes #39900. Thanks @R3NK0R. diff --git a/README.md b/README.md index 186952c852b..3bee5fbba4d 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,10 @@ Runbook: [iOS connect](https://docs.openclaw.ai/platforms/ios). ## From source (development) -Prefer `pnpm` for builds from source. Bun is optional for running TypeScript directly. +Use `pnpm` for source checkouts. The repository is a pnpm workspace, and bundled +plugins load from `extensions/*` during development so their package-local +dependencies and your edits are used directly. Plain `npm install` at the repo +root is not a supported source setup. For the dev loop: diff --git a/docs/plugins/building-plugins.md b/docs/plugins/building-plugins.md index 06436557a1b..b3b7bd197d5 100644 --- a/docs/plugins/building-plugins.md +++ b/docs/plugins/building-plugins.md @@ -22,7 +22,9 @@ falls back to npm automatically for packages that still use npm distribution. - Node >= 22 and a package manager (npm or pnpm) - Familiarity with TypeScript (ESM) -- For in-repo plugins: repository cloned and `pnpm install` done +- For in-repo plugins: repository cloned and `pnpm install` done. Source + checkout plugin development is pnpm-only because OpenClaw loads bundled + plugins from the `extensions/*` workspace packages. ## What kind of plugin? diff --git a/docs/plugins/dependency-resolution.md b/docs/plugins/dependency-resolution.md index 3bbb127a40c..5d095641ebb 100644 --- a/docs/plugins/dependency-resolution.md +++ b/docs/plugins/dependency-resolution.md @@ -93,6 +93,12 @@ Bundled plugin manifests must not request dependency staging. Large or optional plugin functionality should be packaged as a normal plugin and installed through the same npm/git/ClawHub path as third-party plugins. +In source checkouts, OpenClaw treats the repository as a pnpm monorepo. After +`pnpm install`, bundled plugins load from `extensions/` so package-local +workspace dependencies are available and edits are picked up directly. Source +checkout development is pnpm-only; plain `npm install` at the repository root is +not a supported way to prepare bundled plugin dependencies. + ## Legacy cleanup Older OpenClaw versions generated bundled-plugin dependency roots at startup or diff --git a/docs/start/setup.md b/docs/start/setup.md index fa70dbb18d7..cd23176eb2b 100644 --- a/docs/start/setup.md +++ b/docs/start/setup.md @@ -22,7 +22,9 @@ Pick a setup workflow based on how often you want updates and whether you want t ## Prereqs (from source) - Node 24 recommended (Node 22 LTS, currently `22.14+`, still supported) -- `pnpm` preferred (or Bun if you intentionally use the [Bun workflow](/install/bun)) +- `pnpm` required for source checkouts. OpenClaw loads bundled plugins from the + `extensions/*` pnpm workspace packages in dev mode, so root `npm install` does + not prepare the full source tree. - Docker (optional; only for containerized setup/e2e — see [Docker](/install/docker)) ## Tailoring strategy (so updates do not hurt) @@ -44,7 +46,7 @@ From inside this repo, use the local CLI entry: openclaw setup ``` -If you don’t have a global install yet, run it via `pnpm openclaw setup` (or `bun run openclaw setup` if you are using the Bun workflow). +If you don’t have a global install yet, run it via `pnpm openclaw setup`. ## Run the Gateway from this repo @@ -105,15 +107,6 @@ reloads on relevant source, config, and bundled-plugin metadata changes. `pnpm openclaw setup` is the one-time local config/workspace initialization step for a fresh checkout. `pnpm gateway:watch` does not rebuild `dist/control-ui`, so rerun `pnpm ui:build` after `ui/` changes or use `pnpm ui:dev` while developing the Control UI. -If you are intentionally using the Bun workflow, the equivalent commands are: - -```bash -bun install -# First run only (or after resetting local OpenClaw config/workspace) -bun run openclaw setup -bun run gateway:watch -``` - ### 2) Point the macOS app at your running Gateway In **OpenClaw.app**: @@ -158,7 +151,7 @@ Use this when debugging auth or deciding what to back up: ## Updating (without wrecking your setup) - Keep `~/.openclaw/workspace` and `~/.openclaw/` as “your stuff”; don’t put personal prompts/config into the `openclaw` repo. -- Updating source: `git pull` + your chosen package-manager install step (`pnpm install` by default; `bun install` for Bun workflow) + keep using the matching `gateway:watch` command. +- Updating source: `git pull` + `pnpm install` + keep using `pnpm gateway:watch`. ## Linux (systemd user service) diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index 40257858886..6e4d3782c40 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -106,6 +106,12 @@ through `openclaw plugins install`. See [Plugin dependency resolution](/plugins/dependency-resolution) for the install-time lifecycle. +Source checkouts are pnpm workspaces. If you clone OpenClaw to hack on bundled +plugins, run `pnpm install`; OpenClaw then loads bundled plugins from +`extensions/` so edits and package-local dependencies are used directly. +Plain npm root installs are for packaged OpenClaw, not source checkout +development. + ## Plugin types OpenClaw recognizes two plugin formats: diff --git a/src/plugins/bundled-dir.test.ts b/src/plugins/bundled-dir.test.ts index a7c9fcf4953..6ac8a3ee597 100644 --- a/src/plugins/bundled-dir.test.ts +++ b/src/plugins/bundled-dir.test.ts @@ -22,6 +22,7 @@ function createOpenClawRoot(params: { hasDistRuntimeExtensions?: boolean; hasDistExtensions?: boolean; hasGitCheckout?: boolean; + hasPnpmWorkspace?: boolean; }) { const repoRoot = makeRepoRoot(params.prefix); if (params.hasExtensions) { @@ -39,6 +40,13 @@ function createOpenClawRoot(params: { if (params.hasGitCheckout) { fs.writeFileSync(path.join(repoRoot, ".git"), "gitdir: /tmp/fake.git\n", "utf8"); } + if (params.hasPnpmWorkspace) { + fs.writeFileSync( + path.join(repoRoot, "pnpm-workspace.yaml"), + "packages:\n - .\n - extensions/*\n", + "utf8", + ); + } fs.writeFileSync( path.join(repoRoot, "package.json"), `${JSON.stringify({ name: "openclaw" }, null, 2)}\n`, @@ -188,7 +196,7 @@ describe("resolveBundledPluginsDir", () => { }, ], [ - "prefers built dist/extensions in a git checkout outside vitest", + "prefers source extensions in a pnpm git checkout outside vitest", { prefix: "openclaw-bundled-dir-git-built-", hasExtensions: true, @@ -196,9 +204,10 @@ describe("resolveBundledPluginsDir", () => { hasDistRuntimeExtensions: true, hasDistExtensions: true, hasGitCheckout: true, + hasPnpmWorkspace: true, }, { - expectedRelativeDir: path.join("dist-runtime", "extensions"), + expectedRelativeDir: "extensions", }, ], [ @@ -215,7 +224,7 @@ describe("resolveBundledPluginsDir", () => { }, ], [ - "still prefers built bundled plugins during tsx-driven source execution", + "still prefers source extensions during tsx-driven pnpm source execution", { prefix: "openclaw-bundled-dir-tsx-built-", hasExtensions: true, @@ -223,19 +232,21 @@ describe("resolveBundledPluginsDir", () => { hasDistRuntimeExtensions: true, hasDistExtensions: true, hasGitCheckout: true, + hasPnpmWorkspace: true, }, { - expectedRelativeDir: path.join("dist-runtime", "extensions"), + expectedRelativeDir: "extensions", execArgv: ["--import", "tsx"], }, ], [ - "falls back to source extensions in a git checkout when built trees are missing", + "uses source extensions in a pnpm git checkout when built trees are missing", { prefix: "openclaw-bundled-dir-git-", hasExtensions: true, hasSrc: true, hasGitCheckout: true, + hasPnpmWorkspace: true, }, { expectedRelativeDir: "extensions", @@ -267,6 +278,7 @@ describe("resolveBundledPluginsDir", () => { hasDistRuntimeExtensions: true, hasDistExtensions: true, hasGitCheckout: true, + hasPnpmWorkspace: true, }); fs.mkdirSync(path.join(repoRoot, "dist", "extensions", "discord"), { recursive: true }); fs.mkdirSync(path.join(repoRoot, "dist-runtime", "extensions", "discord"), { @@ -280,6 +292,25 @@ describe("resolveBundledPluginsDir", () => { }); }); + it("keeps built bundled plugins for git-looking trees without pnpm workspace metadata", () => { + const repoRoot = createOpenClawRoot({ + prefix: "openclaw-bundled-dir-git-no-pnpm-", + hasExtensions: true, + hasSrc: true, + hasDistRuntimeExtensions: true, + hasDistExtensions: true, + hasGitCheckout: true, + }); + seedBundledPluginTree(repoRoot, "extensions"); + seedBundledPluginTree(repoRoot, path.join("dist", "extensions")); + seedBundledPluginTree(repoRoot, path.join("dist-runtime", "extensions")); + + expectResolvedBundledDirFromRoot({ + repoRoot, + expectedRelativeDir: path.join("dist-runtime", "extensions"), + }); + }); + it("returns a stable empty bundled plugin directory when bundled plugins are disabled", () => { const repoRoot = createOpenClawRoot({ prefix: "openclaw-bundled-dir-disabled-", diff --git a/src/plugins/bundled-dir.ts b/src/plugins/bundled-dir.ts index c9789cef054..e28641e2f04 100644 --- a/src/plugins/bundled-dir.ts +++ b/src/plugins/bundled-dir.ts @@ -23,6 +23,7 @@ function resolveDisabledBundledPluginsDir(): string { function isSourceCheckoutRoot(packageRoot: string): boolean { return ( fs.existsSync(path.join(packageRoot, ".git")) && + fs.existsSync(path.join(packageRoot, "pnpm-workspace.yaml")) && fs.existsSync(path.join(packageRoot, "src")) && fs.existsSync(path.join(packageRoot, "extensions")) ); @@ -126,12 +127,13 @@ function resolveBundledDirFromPackageRoot(packageRoot: string): string | undefin const builtExtensionsDir = path.join(packageRoot, "dist", "extensions"); const sourceCheckout = isSourceCheckoutRoot(packageRoot); const hasUsableSourceTree = sourceCheckout && hasUsableBundledPluginTree(sourceExtensionsDir); - // Local source checkouts stage a runtime-complete bundled plugin tree under - // dist-runtime/. Prefer that over source extensions only when the paired - // dist/ tree exists; otherwise wrappers can drift ahead of the last build. - // Even when OpenClaw itself runs from TypeScript, bundled plugins should use - // compiled JavaScript whenever it is available. Source plugin entries force - // jiti onto hot runtime paths such as per-run tool construction. + // In pnpm source checkouts, extensions/* is a workspace package tree with its + // own package.json dependencies. Prefer it so git checkouts remain editable + // and dependency-complete without moving optional plugin deps back into root. + if (hasUsableSourceTree) { + return sourceExtensionsDir; + } + const runtimeExtensionsDir = path.join(packageRoot, "dist-runtime", "extensions"); const hasUsableRuntimeTree = sourceCheckout ? hasUsableBundledPluginTree(runtimeExtensionsDir)