diff --git a/CHANGELOG.md b/CHANGELOG.md
index 185ec841f85..6394829760b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai
- Tools: add a platform-level tool descriptor planner for descriptor-first visibility, generic availability checks, and executor references. Thanks @shakkernerd.
- Docs/Codex: clarify that ChatGPT/Codex subscription setups should use `openai/gpt-*` with `agentRuntime.id: "codex"` for native Codex runtime, while `openai-codex/*` remains the PI OAuth route. Thanks @pashpashpash.
- 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/beta: prepare Google Chat, LINE, Matrix, and Mattermost for `2026.5.1-beta.2` npm and ClawHub publishing, and keep publishable plugin dist trees out of the core npm package. Thanks @vincentkoc.
- Plugins/beta: prepare BlueBubbles, diagnostics Prometheus, Google Meet, Nextcloud Talk, Nostr, Zalo, and Zalo Personal for `2026.5.1-beta.2` npm and ClawHub publishing. Thanks @vincentkoc.
- Plugins/beta: prepare diagnostics OpenTelemetry, Discord, Diffs, Lobster, Memory LanceDB, Microsoft Teams, QQ Bot, Voice Call, and WhatsApp for `2026.5.1-beta.1` npm and ClawHub publishing. Thanks @vincentkoc.
- Plugins/beta: prepare Brave, Codex, Feishu, Synology Chat, Tlon, and Twitch for `2026.5.1-beta.1` npm and ClawHub publishing. Thanks @vincentkoc.
diff --git a/docs/channels/googlechat.md b/docs/channels/googlechat.md
index a862ced39de..d3c0ef95421 100644
--- a/docs/channels/googlechat.md
+++ b/docs/channels/googlechat.md
@@ -5,7 +5,21 @@ read_when:
title: "Google Chat"
---
-Status: ready for DMs + spaces via Google Chat API webhooks (HTTP only).
+Status: downloadable plugin for DMs + spaces via Google Chat API webhooks (HTTP only).
+
+## Install
+
+Install Google Chat before configuring the channel:
+
+```bash
+openclaw plugins install @openclaw/googlechat
+```
+
+Local checkout (when running from a git repo):
+
+```bash
+openclaw plugins install ./path/to/local/googlechat-plugin
+```
## Quick setup (beginner)
diff --git a/docs/channels/index.md b/docs/channels/index.md
index 4a9de89f339..0fb5d6fa321 100644
--- a/docs/channels/index.md
+++ b/docs/channels/index.md
@@ -24,12 +24,12 @@ Text is supported everywhere; media and reactions vary by channel.
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (bundled plugin; edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
- [Feishu](/channels/feishu) — Feishu/Lark bot via WebSocket (bundled plugin).
-- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook.
+- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook (downloadable plugin).
- [iMessage (legacy)](/channels/imessage) — Legacy macOS integration via imsg CLI (deprecated, use BlueBubbles for new setups).
- [IRC](/channels/irc) — Classic IRC servers; channels + DMs with pairing/allowlist controls.
-- [LINE](/channels/line) — LINE Messaging API bot (bundled plugin).
-- [Matrix](/channels/matrix) — Matrix protocol (bundled plugin).
-- [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (bundled plugin).
+- [LINE](/channels/line) — LINE Messaging API bot (downloadable plugin).
+- [Matrix](/channels/matrix) — Matrix protocol (downloadable plugin).
+- [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (downloadable plugin).
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (bundled plugin).
- [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (bundled plugin).
- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (bundled plugin).
diff --git a/docs/channels/line.md b/docs/channels/line.md
index 40a59594b34..64f775394d3 100644
--- a/docs/channels/line.md
+++ b/docs/channels/line.md
@@ -11,26 +11,18 @@ LINE connects to OpenClaw via the LINE Messaging API. The plugin runs as a webho
receiver on the gateway and uses your channel access token + channel secret for
authentication.
-Status: bundled plugin. Direct messages, group chats, media, locations, Flex
+Status: downloadable plugin. Direct messages, group chats, media, locations, Flex
messages, template messages, and quick replies are supported. Reactions and threads
are not supported.
-## Bundled plugin
+## Install
-LINE ships as a bundled plugin in current OpenClaw releases, so normal
-packaged builds do not need a separate install.
-
-If you are on an older build or a custom install that excludes LINE, install a
-current npm package when one is published:
+Install LINE before configuring the channel:
```bash
openclaw plugins install @openclaw/line
```
-If npm reports the OpenClaw-owned package as deprecated or missing, use a
-current packaged OpenClaw build or a local checkout until the npm package train
-catches up.
-
Local checkout (when running from a git repo):
```bash
diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md
index 65698fd613d..e366cd0108a 100644
--- a/docs/channels/matrix.md
+++ b/docs/channels/matrix.md
@@ -6,23 +6,17 @@ read_when:
title: "Matrix"
---
-Matrix is a bundled channel plugin for OpenClaw.
+Matrix is a downloadable channel plugin for OpenClaw.
It uses the official `matrix-js-sdk` and supports DMs, rooms, threads, media, reactions, polls, location, and E2EE.
-## Bundled plugin
+## Install
-Current packaged OpenClaw releases ship the Matrix plugin in the box. You do not need to install anything; configuring `channels.matrix.*` (see [Setup](#setup)) is what activates it.
-
-For older builds or custom installs that exclude Matrix, install a current npm
-package when one is published:
+Install Matrix before configuring the channel:
```bash
openclaw plugins install @openclaw/matrix
```
-If npm reports the OpenClaw-owned package as deprecated, use a current packaged
-OpenClaw build or a local checkout until a newer npm package is published.
-
From a local checkout:
```bash
diff --git a/docs/channels/mattermost.md b/docs/channels/mattermost.md
index da7e93c0f2f..5a148b7b7ad 100644
--- a/docs/channels/mattermost.md
+++ b/docs/channels/mattermost.md
@@ -7,15 +7,11 @@ title: "Mattermost"
sidebarTitle: "Mattermost"
---
-Status: bundled plugin (bot token + WebSocket events). Channels, groups, and DMs are supported. Mattermost is a self-hostable team messaging platform; see the official site at [mattermost.com](https://mattermost.com) for product details and downloads.
+Status: downloadable plugin (bot token + WebSocket events). Channels, groups, and DMs are supported. Mattermost is a self-hostable team messaging platform; see the official site at [mattermost.com](https://mattermost.com) for product details and downloads.
-## Bundled plugin
+## Install
-
-Mattermost ships as a bundled plugin in current OpenClaw releases, so normal packaged builds do not need a separate install.
-
-
-If you are on an older build or a custom install that excludes Mattermost, install a current npm package when one is published:
+Install Mattermost before configuring the channel:
@@ -30,10 +26,6 @@ If you are on an older build or a custom install that excludes Mattermost, insta
-If npm reports the OpenClaw-owned package as deprecated, use a current packaged
-OpenClaw build or the local checkout path until a newer npm package is
-published.
-
Details: [Plugins](/tools/plugin)
## Quick setup
diff --git a/extensions/googlechat/package.json b/extensions/googlechat/package.json
index c4cea3c72a7..f6980997cca 100644
--- a/extensions/googlechat/package.json
+++ b/extensions/googlechat/package.json
@@ -1,8 +1,11 @@
{
"name": "@openclaw/googlechat",
- "version": "2026.4.25",
- "private": true,
+ "version": "2026.5.1-beta.2",
"description": "OpenClaw Google Chat channel plugin",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/openclaw/openclaw"
+ },
"type": "module",
"dependencies": {
"gaxios": "7.1.4",
@@ -70,6 +73,16 @@
"npmSpec": "@openclaw/googlechat",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10"
+ },
+ "compat": {
+ "pluginApi": ">=2026.4.25"
+ },
+ "build": {
+ "openclawVersion": "2026.5.1-beta.2"
+ },
+ "release": {
+ "publishToClawHub": true,
+ "publishToNpm": true
}
}
}
diff --git a/extensions/line/package.json b/extensions/line/package.json
index 0a343b67344..5ddecb2857b 100644
--- a/extensions/line/package.json
+++ b/extensions/line/package.json
@@ -1,8 +1,11 @@
{
"name": "@openclaw/line",
- "version": "2026.4.25",
- "private": true,
+ "version": "2026.5.1-beta.2",
"description": "OpenClaw LINE channel plugin",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/openclaw/openclaw"
+ },
"type": "module",
"dependencies": {
"@line/bot-sdk": "^11.0.0"
@@ -40,6 +43,16 @@
"npmSpec": "@openclaw/line",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10"
+ },
+ "compat": {
+ "pluginApi": ">=2026.4.25"
+ },
+ "build": {
+ "openclawVersion": "2026.5.1-beta.2"
+ },
+ "release": {
+ "publishToClawHub": true,
+ "publishToNpm": true
}
}
}
diff --git a/extensions/matrix/package.json b/extensions/matrix/package.json
index 52ff337eec3..51088eb3afb 100644
--- a/extensions/matrix/package.json
+++ b/extensions/matrix/package.json
@@ -1,7 +1,11 @@
{
"name": "@openclaw/matrix",
- "version": "2026.4.25",
+ "version": "2026.5.1-beta.2",
"description": "OpenClaw Matrix channel plugin",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/openclaw/openclaw"
+ },
"type": "module",
"dependencies": {
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.5.1",
@@ -79,6 +83,16 @@
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10",
"allowInvalidConfigRecovery": true
+ },
+ "compat": {
+ "pluginApi": ">=2026.4.25"
+ },
+ "build": {
+ "openclawVersion": "2026.5.1-beta.2"
+ },
+ "release": {
+ "publishToClawHub": true,
+ "publishToNpm": true
}
}
}
diff --git a/extensions/mattermost/package.json b/extensions/mattermost/package.json
index dc9ddbfbdf1..2e166a168ea 100644
--- a/extensions/mattermost/package.json
+++ b/extensions/mattermost/package.json
@@ -1,7 +1,11 @@
{
"name": "@openclaw/mattermost",
- "version": "2026.4.25",
+ "version": "2026.5.1-beta.2",
"description": "OpenClaw Mattermost channel plugin",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/openclaw/openclaw"
+ },
"type": "module",
"dependencies": {
"ws": "^8.20.0"
@@ -36,6 +40,16 @@
"npmSpec": "@openclaw/mattermost",
"defaultChoice": "npm",
"minHostVersion": ">=2026.4.10"
+ },
+ "compat": {
+ "pluginApi": ">=2026.4.25"
+ },
+ "build": {
+ "openclawVersion": "2026.5.1-beta.2"
+ },
+ "release": {
+ "publishToClawHub": true,
+ "publishToNpm": true
}
}
}
diff --git a/package.json b/package.json
index 93dacfeca7a..e85d360837b 100644
--- a/package.json
+++ b/package.json
@@ -33,9 +33,35 @@
"!dist/plugin-sdk/.tsbuildinfo",
"!dist/extensions/node_modules/**",
"!dist/extensions/*/node_modules/**",
+ "!dist/extensions/bluebubbles/**",
+ "!dist/extensions/brave/**",
+ "!dist/extensions/codex/**",
+ "!dist/extensions/diagnostics-otel/**",
+ "!dist/extensions/diagnostics-prometheus/**",
+ "!dist/extensions/diffs/**",
+ "!dist/extensions/discord/**",
+ "!dist/extensions/feishu/**",
+ "!dist/extensions/google-meet/**",
+ "!dist/extensions/googlechat/**",
+ "!dist/extensions/line/**",
+ "!dist/extensions/lobster/**",
+ "!dist/extensions/matrix/**",
+ "!dist/extensions/mattermost/**",
+ "!dist/extensions/memory-lancedb/**",
+ "!dist/extensions/msteams/**",
+ "!dist/extensions/nextcloud-talk/**",
+ "!dist/extensions/nostr/**",
+ "!dist/extensions/qqbot/**",
"!dist/extensions/qa-channel/**",
"!dist/extensions/qa-lab/**",
"!dist/extensions/qa-matrix/**",
+ "!dist/extensions/synology-chat/**",
+ "!dist/extensions/tlon/**",
+ "!dist/extensions/twitch/**",
+ "!dist/extensions/voice-call/**",
+ "!dist/extensions/whatsapp/**",
+ "!dist/extensions/zalo/**",
+ "!dist/extensions/zalouser/**",
"!dist/plugin-sdk/extensions/qa-channel/**",
"!dist/plugin-sdk/extensions/qa-lab/**",
"!dist/plugin-sdk/qa-channel.*",
diff --git a/src/infra/package-dist-inventory.test.ts b/src/infra/package-dist-inventory.test.ts
index 0d3788eb357..e810e35fa34 100644
--- a/src/infra/package-dist-inventory.test.ts
+++ b/src/infra/package-dist-inventory.test.ts
@@ -187,6 +187,69 @@ describe("package dist inventory", () => {
);
});
+ it("keeps publishable externalized bundled plugin dist trees out of the inventory", async () => {
+ await withTempDir({ prefix: "openclaw-dist-inventory-externalized-" }, async (packageRoot) => {
+ const externalizedRuntime = path.join(
+ packageRoot,
+ "dist",
+ "extensions",
+ "external-chat",
+ "index.js",
+ );
+ const bundledRuntime = path.join(
+ packageRoot,
+ "dist",
+ "extensions",
+ "bundled-chat",
+ "index.js",
+ );
+ const externalizedPackageJson = path.join(
+ packageRoot,
+ "extensions",
+ "external-chat",
+ "package.json",
+ );
+ const bundledPackageJson = path.join(
+ packageRoot,
+ "extensions",
+ "bundled-chat",
+ "package.json",
+ );
+
+ await fs.mkdir(path.dirname(externalizedRuntime), { recursive: true });
+ await fs.mkdir(path.dirname(bundledRuntime), { recursive: true });
+ await fs.mkdir(path.dirname(externalizedPackageJson), { recursive: true });
+ await fs.mkdir(path.dirname(bundledPackageJson), { recursive: true });
+ await fs.writeFile(externalizedRuntime, "export {};\n", "utf8");
+ await fs.writeFile(bundledRuntime, "export {};\n", "utf8");
+ await fs.writeFile(
+ externalizedPackageJson,
+ JSON.stringify({
+ name: "@openclaw/external-chat",
+ openclaw: {
+ release: {
+ publishToClawHub: true,
+ publishToNpm: true,
+ },
+ },
+ }),
+ "utf8",
+ );
+ await fs.writeFile(
+ bundledPackageJson,
+ JSON.stringify({
+ name: "@openclaw/bundled-chat",
+ openclaw: {},
+ }),
+ "utf8",
+ );
+
+ await expect(writePackageDistInventory(packageRoot)).resolves.toEqual([
+ "dist/extensions/bundled-chat/index.js",
+ ]);
+ });
+ });
+
it("reports runtime-created install staging dirs during installed dist verification", async () => {
await withTempDir({ prefix: "openclaw-dist-inventory-stage-" }, async (packageRoot) => {
const realFile = path.join(packageRoot, "dist", "real-AbC123.js");
diff --git a/src/infra/package-dist-inventory.ts b/src/infra/package-dist-inventory.ts
index 91130a3e9b3..ef0c0d3b2e9 100644
--- a/src/infra/package-dist-inventory.ts
+++ b/src/infra/package-dist-inventory.ts
@@ -39,6 +39,7 @@ const OMITTED_DIST_SUBTREE_PATTERNS = [
new RegExp(`^dist/plugin-sdk/extensions/${LEGACY_QA_LAB_DIR}(?:/|$)`, "u"),
] as const;
const INSTALL_STAGE_DEBRIS_DIR_PATTERN = /^\.openclaw-install-stage(?:-[^/]+)?$/iu;
+type ExternalizedBundledExtensionIds = ReadonlySet;
function normalizeRelativePath(value: string): string {
return value.replace(/\\/g, "/");
@@ -74,10 +75,86 @@ export function isLegacyPluginDependencyInstallStagePath(relativePath: string):
);
}
-function isPackagedDistPath(relativePath: string): boolean {
+function isExternalizedBundledExtensionDistPath(
+ relativePath: string,
+ externalizedExtensionIds: ExternalizedBundledExtensionIds,
+): boolean {
+ if (externalizedExtensionIds.size === 0) {
+ return false;
+ }
+ const parts = normalizeRelativePath(relativePath).split("/");
+ return (
+ parts.length >= 3 &&
+ parts[0] === "dist" &&
+ parts[1] === "extensions" &&
+ Boolean(parts[2]) &&
+ externalizedExtensionIds.has(parts[2] ?? "")
+ );
+}
+
+function isPublishableExternalizedBundledManifest(value: unknown): boolean {
+ if (!value || typeof value !== "object") {
+ return false;
+ }
+ const openclaw = (value as { openclaw?: unknown }).openclaw;
+ if (!openclaw || typeof openclaw !== "object") {
+ return false;
+ }
+ const release = (openclaw as { release?: unknown }).release;
+ if (!release || typeof release !== "object") {
+ return false;
+ }
+ const typedRelease = release as { publishToClawHub?: unknown; publishToNpm?: unknown };
+ return typedRelease.publishToNpm === true || typedRelease.publishToClawHub === true;
+}
+
+async function collectExternalizedBundledExtensionIds(
+ packageRoot: string,
+): Promise {
+ const extensionsDir = path.join(packageRoot, "extensions");
+ let entries: import("node:fs").Dirent[];
+ try {
+ entries = await fs.readdir(extensionsDir, { withFileTypes: true });
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
+ return new Set();
+ }
+ throw error;
+ }
+
+ const ids = new Set();
+ await Promise.all(
+ entries.map(async (entry) => {
+ if (!entry.isDirectory()) {
+ return;
+ }
+ const packageJsonPath = path.join(extensionsDir, entry.name, "package.json");
+ try {
+ const parsed = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as unknown;
+ if (isPublishableExternalizedBundledManifest(parsed)) {
+ ids.add(entry.name);
+ }
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
+ return;
+ }
+ throw error;
+ }
+ }),
+ );
+ return ids;
+}
+
+function isPackagedDistPath(
+ relativePath: string,
+ externalizedExtensionIds: ExternalizedBundledExtensionIds,
+): boolean {
if (!relativePath.startsWith("dist/")) {
return false;
}
+ if (isExternalizedBundledExtensionDistPath(relativePath, externalizedExtensionIds)) {
+ return false;
+ }
if (isLegacyPluginDependencyDirPath(relativePath)) {
return false;
}
@@ -106,16 +183,24 @@ function isPackagedDistPath(relativePath: string): boolean {
return true;
}
-function isOmittedDistSubtree(relativePath: string): boolean {
+function isOmittedDistSubtree(
+ relativePath: string,
+ externalizedExtensionIds: ExternalizedBundledExtensionIds,
+): boolean {
return (
+ isExternalizedBundledExtensionDistPath(relativePath, externalizedExtensionIds) ||
isLegacyPluginDependencyDirPath(relativePath) ||
OMITTED_DIST_SUBTREE_PATTERNS.some((pattern) => pattern.test(relativePath))
);
}
-async function collectRelativeFiles(rootDir: string, baseDir: string): Promise {
+async function collectRelativeFiles(
+ rootDir: string,
+ baseDir: string,
+ externalizedExtensionIds: ExternalizedBundledExtensionIds,
+): Promise {
const rootRelativePath = normalizeRelativePath(path.relative(baseDir, rootDir));
- if (rootRelativePath && isOmittedDistSubtree(rootRelativePath)) {
+ if (rootRelativePath && isOmittedDistSubtree(rootRelativePath, externalizedExtensionIds)) {
return [];
}
try {
@@ -134,10 +219,10 @@ async function collectRelativeFiles(rootDir: string, baseDir: string): Promise {
- return await collectRelativeFiles(path.join(packageRoot, "dist"), packageRoot);
+ const externalizedExtensionIds = await collectExternalizedBundledExtensionIds(packageRoot);
+ return await collectRelativeFiles(
+ path.join(packageRoot, "dist"),
+ packageRoot,
+ externalizedExtensionIds,
+ );
}
export async function collectLegacyPluginDependencyStagingDebrisPaths(
diff --git a/test/plugin-npm-release.test.ts b/test/plugin-npm-release.test.ts
index f2e49909fab..a2db21542e2 100644
--- a/test/plugin-npm-release.test.ts
+++ b/test/plugin-npm-release.test.ts
@@ -1,7 +1,8 @@
-import { mkdirSync } from "node:fs";
+import { mkdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { bundledPluginFile, bundledPluginRoot } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it } from "vitest";
+import { collectClawHubPublishablePluginPackages } from "../scripts/lib/plugin-clawhub-release.ts";
import {
collectPublishablePluginPackages,
collectChangedExtensionIdsFromPaths,
@@ -155,6 +156,22 @@ describe("collectPublishablePluginPackageErrors", () => {
});
describe("collectPublishablePluginPackages", () => {
+ it("keeps publishable plugin dist trees out of the core npm package files list", () => {
+ const rootPackage = JSON.parse(readFileSync("package.json", "utf8")) as {
+ files?: unknown;
+ };
+ const packageFiles = new Set(Array.isArray(rootPackage.files) ? rootPackage.files : []);
+ const publishablePlugins = [
+ ...collectPublishablePluginPackages(),
+ ...collectClawHubPublishablePluginPackages(),
+ ];
+ const missingExclusions = Array.from(
+ new Set(publishablePlugins.map((plugin) => `!dist/extensions/${plugin.extensionId}/**`)),
+ ).filter((entry) => !packageFiles.has(entry));
+
+ expect(missingExclusions).toEqual([]);
+ });
+
it("collects publishable npm plugins from extension package manifests", () => {
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-release-");
mkdirSync(join(repoDir, "extensions", "demo-plugin"), { recursive: true });