diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256
index 47d8446206974..3fbaf50ee95cd 100644
--- a/docs/.generated/config-baseline.sha256
+++ b/docs/.generated/config-baseline.sha256
@@ -1,4 +1,4 @@
-900c26a9b060f1dfa712abfba877bd3bf9c7b0c9f2294faf9834038283ec24b6 config-baseline.json
-d956a1d60f776bba712cb04374a4f5657cad95bb088b536c5e3e4e29d4a21328 config-baseline.core.json
-ef83a06633fc001b5b2535566939186ecb49d05cd1a90b40e54cc58d3e6e44e3 config-baseline.channel.json
-5f5d4e850df6e9854a85b5d008236854ce185c707fdbb566efcf00f8c08b36e3 config-baseline.plugin.json
+a877055a70d2089a7c823caf909b51e3c1e589dc62f4a5f82ff0457caed9c916 config-baseline.json
+a3f1abce6515f78ae7f4100bb1c9d32c85729687bf7cb0f4142bfe54c8d5d167 config-baseline.core.json
+5a9d96d617732a6e93ef22259d0359c1e14c9b2f2624a6880639f7012fbedfcd config-baseline.channel.json
+77639c2fa47bfa62452779cd9286a1ec6fa95ef4f975b71abce3e4aac34005f4 config-baseline.plugin.json
diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256
index 489e0cba74aa2..4e914269e67f1 100644
--- a/docs/.generated/plugin-sdk-api-baseline.sha256
+++ b/docs/.generated/plugin-sdk-api-baseline.sha256
@@ -1,2 +1,2 @@
-73091009a0a45c72eded8003fdf9cf4c10e9470c4a055592a98ea00d55cd45d1 plugin-sdk-api-baseline.json
-9c9d59ffc0b3b6677794cb8fd5afd0208dbc9f3cd1ad59b30ee627f6f6352929 plugin-sdk-api-baseline.jsonl
+9e1ab526b7683e8790ce6edc3ea4d90b4b73e30505849f73da63c7dd7804eaa3 plugin-sdk-api-baseline.json
+3d02b27553993312e83747bfe862db1d597aff8efea59352f5e5f84498c5d843 plugin-sdk-api-baseline.jsonl
diff --git a/extensions/googlechat/src/config-schema.ts b/extensions/googlechat/src/config-schema.ts
index 686d4a0b99c4c..21249d67e7344 100644
--- a/extensions/googlechat/src/config-schema.ts
+++ b/extensions/googlechat/src/config-schema.ts
@@ -1,3 +1,6 @@
-import { buildChannelConfigSchema, GoogleChatConfigSchema } from "openclaw/plugin-sdk/googlechat";
+import {
+ buildChannelConfigSchema,
+ GoogleChatConfigSchema,
+} from "openclaw/plugin-sdk/channel-config-schema";
export const GoogleChatChannelConfigSchema = buildChannelConfigSchema(GoogleChatConfigSchema);
diff --git a/extensions/telegram/src/config-schema.ts b/extensions/telegram/src/config-schema.ts
index a4f3b2ad043dd..16849c4ee571a 100644
--- a/extensions/telegram/src/config-schema.ts
+++ b/extensions/telegram/src/config-schema.ts
@@ -1,4 +1,7 @@
-import { buildChannelConfigSchema, TelegramConfigSchema } from "../config-api.js";
+import {
+ buildChannelConfigSchema,
+ TelegramConfigSchema,
+} from "openclaw/plugin-sdk/channel-config-schema";
import { telegramChannelConfigUiHints } from "./config-ui-hints.js";
export const TelegramChannelConfigSchema = buildChannelConfigSchema(TelegramConfigSchema, {
diff --git a/extensions/x/channel-plugin-api.ts b/extensions/x/channel-plugin-api.ts
new file mode 100644
index 0000000000000..ccd5e594d50df
--- /dev/null
+++ b/extensions/x/channel-plugin-api.ts
@@ -0,0 +1,3 @@
+// Keep bundled channel entry imports narrow so package discovery does not load
+// the wider X runtime and onboarding graph just to read the entry contract.
+export { xPlugin } from "./src/plugin.js";
diff --git a/extensions/x/index.test.ts b/extensions/x/index.test.ts
new file mode 100644
index 0000000000000..14ed152609805
--- /dev/null
+++ b/extensions/x/index.test.ts
@@ -0,0 +1,11 @@
+import { describe, expect, it } from "vitest";
+import entry from "./index.js";
+
+describe("x bundled entries", () => {
+ it("declares the channel entry without importing the broad api barrel", () => {
+ expect(entry.kind).toBe("bundled-channel-entry");
+ expect(entry.id).toBe("x");
+ expect(entry.name).toBe("X (Twitter)");
+ expect(typeof entry.loadChannelPlugin).toBe("function");
+ });
+});
diff --git a/extensions/x/index.ts b/extensions/x/index.ts
index 11eedb63b7812..d68acbe620730 100644
--- a/extensions/x/index.ts
+++ b/extensions/x/index.ts
@@ -1,17 +1,19 @@
-import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
-import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core";
-import { xPlugin } from "./src/plugin.js";
-import { setXRuntime } from "./src/runtime.js";
+import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
+import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
+import { XConfigSchema } from "./src/config-schema.js";
-const plugin = {
+export default defineBundledChannelEntry({
id: "x",
name: "X (Twitter)",
- description: "X (Twitter) channel plugin - monitor mentions and reply to tweets",
- configSchema: emptyPluginConfigSchema(),
- register(api: OpenClawPluginApi) {
- setXRuntime(api.runtime);
- api.registerChannel({ plugin: xPlugin });
+ description: "X (Twitter) channel plugin",
+ importMetaUrl: import.meta.url,
+ configSchema: buildChannelConfigSchema(XConfigSchema),
+ plugin: {
+ specifier: "./channel-plugin-api.js",
+ exportName: "xPlugin",
},
-};
-
-export default plugin;
+ runtime: {
+ specifier: "./runtime-api.js",
+ exportName: "setXRuntime",
+ },
+});
diff --git a/extensions/x/runtime-api.ts b/extensions/x/runtime-api.ts
new file mode 100644
index 0000000000000..aeaad004c2dc1
--- /dev/null
+++ b/extensions/x/runtime-api.ts
@@ -0,0 +1,3 @@
+// Keep runtime activation narrow so the bundled channel entry only imports the
+// X runtime setter when the host actually initializes the plugin.
+export { setXRuntime } from "./src/runtime.js";
diff --git a/package.json b/package.json
index 2ad28cc9c5da5..e054643b16146 100644
--- a/package.json
+++ b/package.json
@@ -1261,7 +1261,7 @@
"qa:lab:up:fast": "node --import tsx scripts/qa-lab-up.ts --use-prebuilt-image --bind-ui-dist --skip-ui-build",
"qa:lab:watch": "vite build --watch --config extensions/qa-lab/web/vite.config.ts",
"qverisbot": "node scripts/run-node.mjs",
- "release:check": "pnpm check:base-config-schema && pnpm check:bundled-channel-config-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm config:docs:check && pnpm plugin-sdk:check-exports && pnpm plugin-sdk:api:check && node scripts/stage-bundled-plugin-runtime-deps.mjs && pnpm ui:build && node --import tsx scripts/release-check.ts",
+ "release:check": "pnpm check:base-config-schema && pnpm check:bundled-channel-config-metadata && pnpm config:docs:check && pnpm plugin-sdk:check-exports && pnpm plugin-sdk:api:check && node scripts/stage-bundled-plugin-runtime-deps.mjs && pnpm ui:build && node --import tsx scripts/release-check.ts",
"release:npm": "bash scripts/release-npm.sh",
"release:openclaw:npm:check": "node --import tsx scripts/openclaw-npm-release-check.ts",
"release:openclaw:npm:verify-published": "node --import tsx scripts/openclaw-npm-postpublish-verify.ts",
diff --git a/scripts/release-check.ts b/scripts/release-check.ts
index 35db18baa91c0..4a01fb0255f2c 100755
--- a/scripts/release-check.ts
+++ b/scripts/release-check.ts
@@ -36,6 +36,7 @@ export {
type PackFile = { path: string };
type PackResult = { files?: PackFile[]; filename?: string; unpackedSize?: number };
+type RootPackageName = { name?: string };
const requiredPathGroups = [
["dist/index.js", "dist/index.mjs"],
@@ -72,6 +73,25 @@ export function listRequiredQaScenarioPackPaths(): string[] {
.toSorted((left, right) => left.localeCompare(right));
}
+export function createReleaseCheckNpmEnv(params?: {
+ env?: NodeJS.ProcessEnv;
+ scratchDir?: string;
+}): NodeJS.ProcessEnv {
+ const env = { ...(params?.env ?? process.env) };
+ const scratchDir = resolve(
+ params?.scratchDir ?? join(tmpdir(), "openclaw-release-check-npm-runtime"),
+ );
+ const cacheDir = env.npm_config_cache?.trim() || join(scratchDir, "cache");
+ const logsDir = env.npm_config_logs_dir?.trim() || join(scratchDir, "logs");
+
+ mkdirSync(cacheDir, { recursive: true });
+ mkdirSync(logsDir, { recursive: true });
+
+ env.npm_config_cache = cacheDir;
+ env.npm_config_logs_dir = logsDir;
+ return env;
+}
+
function collectBundledExtensions(): BundledExtension[] {
const extensionsDir = resolve("extensions");
const entries = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) =>
@@ -93,6 +113,22 @@ function collectBundledExtensions(): BundledExtension[] {
});
}
+export function packageNameToNodeModulesSegments(packageName: string): string[] {
+ return packageName
+ .split("/")
+ .map((segment) => segment.trim())
+ .filter((segment) => segment.length > 0);
+}
+
+function readRootPackageName(): string {
+ const rootPackage = JSON.parse(readFileSync(resolve("package.json"), "utf8")) as RootPackageName;
+ const packageName = rootPackage.name?.trim();
+ if (!packageName) {
+ throw new Error("release-check: root package.json is missing a valid name.");
+ }
+ return packageName;
+}
+
function checkBundledExtensionMetadata() {
const extensions = collectBundledExtensions();
const manifestErrors = collectBundledExtensionManifestErrors(extensions);
@@ -128,6 +164,7 @@ function checkBundledExtensionMetadata() {
function runPackDry(): PackResult[] {
const raw = execSync("npm pack --dry-run --json --ignore-scripts", {
encoding: "utf8",
+ env: createReleaseCheckNpmEnv(),
stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 1024 * 1024 * 100,
});
@@ -140,6 +177,7 @@ function runPack(packDestination: string): PackResult[] {
["pack", "--json", "--ignore-scripts", "--pack-destination", packDestination],
{
encoding: "utf8",
+ env: createReleaseCheckNpmEnv(),
stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 1024 * 1024 * 100,
},
@@ -175,6 +213,7 @@ function installPackedTarball(prefixDir: string, tarballPath: string, cwd: strin
{
cwd,
encoding: "utf8",
+ env: createReleaseCheckNpmEnv(),
stdio: "inherit",
},
);
@@ -184,6 +223,7 @@ function resolveGlobalRoot(prefixDir: string, cwd: string): string {
return execFileSync("npm", ["root", "-g", "--prefix", prefixDir], {
cwd,
encoding: "utf8",
+ env: createReleaseCheckNpmEnv(),
stdio: ["ignore", "pipe", "pipe"],
}).trim();
}
@@ -199,7 +239,10 @@ function runPackedBundledChannelEntrySmoke(): void {
const prefixDir = join(tmpRoot, "prefix");
installPackedTarball(prefixDir, tarballPath, tmpRoot);
- const packageRoot = join(resolveGlobalRoot(prefixDir, tmpRoot), "openclaw");
+ const packageRoot = join(
+ resolveGlobalRoot(prefixDir, tmpRoot),
+ ...packageNameToNodeModulesSegments(readRootPackageName()),
+ );
execFileSync(
process.execPath,
[
diff --git a/scripts/runtime-postbuild.mjs b/scripts/runtime-postbuild.mjs
index 238f1f8360e3e..604d4f7378ca9 100644
--- a/scripts/runtime-postbuild.mjs
+++ b/scripts/runtime-postbuild.mjs
@@ -10,6 +10,12 @@ import { writeOfficialChannelCatalog } from "./write-official-channel-catalog.mj
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const ROOT_RUNTIME_ALIAS_PATTERN = /^(?.+\.(?:runtime|contract))-[A-Za-z0-9_-]+\.js$/u;
+const PACKAGE_SELF_IMPORT_PATTERNS = [
+ /(from\s*["'])openclaw(?=(?:\/|["']))/gu,
+ /(import\s*["'])openclaw(?=(?:\/|["']))/gu,
+ /(import\s*\(\s*["'])openclaw(?=(?:\/|["']))/gu,
+ /(require\s*\(\s*["'])openclaw(?=(?:\/|["']))/gu,
+];
/**
* Copy static (non-transpiled) runtime assets that are referenced by their
@@ -57,6 +63,58 @@ export function copyStaticExtensionAssets(params = {}) {
}
}
+function listFilesRecursive(dirPath, fsImpl) {
+ let entries = [];
+ try {
+ entries = fsImpl.readdirSync(dirPath, { withFileTypes: true });
+ } catch {
+ return [];
+ }
+
+ return entries.flatMap((entry) => {
+ const absolutePath = path.join(dirPath, entry.name);
+ if (entry.isDirectory()) {
+ return listFilesRecursive(absolutePath, fsImpl);
+ }
+ return [absolutePath];
+ });
+}
+
+export function rewritePackageSelfImportsInSource(sourceText, packageName) {
+ if (!packageName || packageName === "openclaw") {
+ return sourceText;
+ }
+ return PACKAGE_SELF_IMPORT_PATTERNS.reduce(
+ (current, pattern) => current.replace(pattern, `$1${packageName}`),
+ sourceText,
+ );
+}
+
+export function rewriteBundledExtensionPackageSelfImports(params = {}) {
+ const rootDir = params.rootDir ?? ROOT;
+ const fsImpl = params.fs ?? fs;
+ const packageName =
+ params.packageName ??
+ JSON.parse(fsImpl.readFileSync(path.join(rootDir, "package.json"), "utf8")).name;
+ const extensionsDir = path.join(rootDir, "dist", "extensions");
+ let rewrittenFiles = 0;
+
+ for (const filePath of listFilesRecursive(extensionsDir, fsImpl)) {
+ if (!filePath.endsWith(".js")) {
+ continue;
+ }
+ const current = fsImpl.readFileSync(filePath, "utf8");
+ const next = rewritePackageSelfImportsInSource(current, packageName);
+ if (next === current) {
+ continue;
+ }
+ fsImpl.writeFileSync(filePath, next, "utf8");
+ rewrittenFiles += 1;
+ }
+
+ return rewrittenFiles;
+}
+
export function writeStableRootRuntimeAliases(params = {}) {
const rootDir = params.rootDir ?? ROOT;
const distDir = path.join(rootDir, "dist");
@@ -89,6 +147,7 @@ export function runRuntimePostBuild(params = {}) {
stageBundledPluginRuntime(params);
writeStableRootRuntimeAliases(params);
copyStaticExtensionAssets(params);
+ rewriteBundledExtensionPackageSelfImports(params);
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
diff --git a/src/plugin-sdk/root-alias.cjs b/src/plugin-sdk/root-alias.cjs
index 2c91cbdb61c48..b20a4a5e45a0a 100644
--- a/src/plugin-sdk/root-alias.cjs
+++ b/src/plugin-sdk/root-alias.cjs
@@ -7,7 +7,6 @@ let monolithicSdk = null;
let diagnosticEventsModule = null;
const jitiLoaders = new Map();
const pluginSdkSubpathsCache = new Map();
-const pluginSdkPackageNames = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"];
const pluginSdkSourceExtensions = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"];
const isDistRootAlias = __filename.includes(
`${path.sep}dist${path.sep}plugin-sdk${path.sep}root-alias.cjs`,
@@ -88,6 +87,22 @@ function getPackageRoot() {
return path.resolve(__dirname, "..", "..");
}
+function listPluginSdkPackageNames() {
+ const packageNames = new Set(["openclaw/plugin-sdk", "@openclaw/plugin-sdk"]);
+ try {
+ const packageJson = JSON.parse(
+ fs.readFileSync(path.join(getPackageRoot(), "package.json"), "utf8"),
+ );
+ const rootPackageName = typeof packageJson?.name === "string" ? packageJson.name.trim() : "";
+ if (rootPackageName) {
+ packageNames.add(`${rootPackageName}/plugin-sdk`);
+ }
+ } catch {
+ // Keep the canonical aliases even if package.json is unavailable.
+ }
+ return [...packageNames];
+}
+
function findDistChunkByPrefix(prefix) {
const distRoot = path.join(getPackageRoot(), "dist");
try {
@@ -133,14 +148,14 @@ function buildPluginSdkAliasMap(useDist) {
const normalizeTarget = (target) =>
process.platform === "win32" ? target.replace(/\\/g, "/") : target;
const aliasMap = Object.fromEntries(
- pluginSdkPackageNames.map((packageName) => [packageName, normalizeTarget(__filename)]),
+ listPluginSdkPackageNames().map((packageName) => [packageName, normalizeTarget(__filename)]),
);
for (const subpath of listPluginSdkExportedSubpaths()) {
if (useDist) {
const candidate = path.join(pluginSdkDir, `${subpath}.js`);
if (fs.existsSync(candidate)) {
- for (const packageName of pluginSdkPackageNames) {
+ for (const packageName of listPluginSdkPackageNames()) {
aliasMap[`${packageName}/${subpath}`] = normalizeTarget(candidate);
}
}
@@ -151,7 +166,7 @@ function buildPluginSdkAliasMap(useDist) {
if (!fs.existsSync(candidate)) {
continue;
}
- for (const packageName of pluginSdkPackageNames) {
+ for (const packageName of listPluginSdkPackageNames()) {
aliasMap[`${packageName}/${subpath}`] = normalizeTarget(candidate);
}
break;
diff --git a/src/plugins/contracts/plugin-sdk-root-alias.test.ts b/src/plugins/contracts/plugin-sdk-root-alias.test.ts
index 599c373edf7d6..29237262834f5 100644
--- a/src/plugins/contracts/plugin-sdk-root-alias.test.ts
+++ b/src/plugins/contracts/plugin-sdk-root-alias.test.ts
@@ -26,6 +26,7 @@ function loadRootAliasWithStubs(options?: {
env?: Record;
monolithicExports?: Record;
aliasPath?: string;
+ packageName?: string;
packageExports?: Record;
platform?: string;
existingPaths?: string[];
@@ -63,6 +64,7 @@ function loadRootAliasWithStubs(options?: {
return {
readFileSync: () =>
JSON.stringify({
+ name: options?.packageName,
exports: {
"./plugin-sdk/group-access": { default: "./dist/plugin-sdk/group-access.js" },
...options?.packageExports,
@@ -280,6 +282,24 @@ describe("plugin-sdk root alias", () => {
});
});
+ it("adds the current package name as a plugin-sdk alias when it differs from openclaw", () => {
+ const lazyModule = loadRootAliasWithStubs({
+ distExists: true,
+ packageName: "@qverisai/qverisbot",
+ monolithicExports: {
+ slowHelper: (): string => "loaded",
+ },
+ });
+
+ expect((lazyModule.moduleExports.slowHelper as () => string)()).toBe("loaded");
+ expect(lazyModule.createJitiOptions.at(-1)?.alias).toMatchObject({
+ "@qverisai/qverisbot/plugin-sdk": rootAliasPath,
+ "@qverisai/qverisbot/plugin-sdk/group-access": expect.stringContaining(
+ path.join("src", "plugin-sdk", "group-access.ts"),
+ ),
+ });
+ });
+
it("keeps bootstrap plugin-sdk aliases deterministic and ignores unsafe subpaths", () => {
const lazyModule = loadRootAliasWithStubs({
distExists: true,
diff --git a/src/plugins/sdk-alias.test.ts b/src/plugins/sdk-alias.test.ts
index cba0071797cf7..f48503e6c7b81 100644
--- a/src/plugins/sdk-alias.test.ts
+++ b/src/plugins/sdk-alias.test.ts
@@ -58,6 +58,7 @@ function withCwd(cwd: string, run: () => T): T {
}
function createPluginSdkAliasFixture(params?: {
+ packageName?: string;
srcFile?: string;
distFile?: string;
srcBody?: string;
@@ -75,7 +76,7 @@ function createPluginSdkAliasFixture(params?: {
params?.trustedRootIndicatorMode ??
(params?.trustedRootIndicators === false ? "none" : "bin+marker");
const packageJson: Record = {
- name: "openclaw",
+ name: params?.packageName ?? "openclaw",
type: "module",
};
if (trustedRootIndicatorMode === "bin+marker") {
@@ -869,6 +870,45 @@ describe("plugin sdk alias helpers", () => {
).toBe(false);
});
+ it("adds the current package name to plugin-sdk loader aliases", () => {
+ const { fixture, sourceRootAlias, sourceChannelRuntimePath } =
+ createPluginSdkAliasTargetFixture();
+ fs.writeFileSync(
+ path.join(fixture.root, "package.json"),
+ JSON.stringify(
+ {
+ name: "@qverisai/qverisbot",
+ type: "module",
+ bin: {
+ openclaw: "openclaw.mjs",
+ },
+ exports: {
+ "./plugin-sdk": { default: "./dist/plugin-sdk/index.js" },
+ "./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" },
+ },
+ },
+ null,
+ 2,
+ ),
+ "utf-8",
+ );
+ const sourcePluginEntry = writePluginEntry(
+ fixture.root,
+ bundledPluginFile("discord", "index.ts"),
+ );
+
+ const aliases = withEnv({ NODE_ENV: undefined }, () =>
+ buildPluginLoaderAliasMap(sourcePluginEntry),
+ );
+
+ expect(fs.realpathSync(aliases["@qverisai/qverisbot/plugin-sdk"] ?? "")).toBe(
+ fs.realpathSync(sourceRootAlias),
+ );
+ expect(fs.realpathSync(aliases["@qverisai/qverisbot/plugin-sdk/channel-runtime"] ?? "")).toBe(
+ fs.realpathSync(sourceChannelRuntimePath),
+ );
+ });
+
it("normalizes Windows alias targets before handing them to Jiti", () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", {
diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts
index 20eefa05661ec..4b7e5feea7459 100644
--- a/src/plugins/sdk-alias.ts
+++ b/src/plugins/sdk-alias.ts
@@ -18,6 +18,7 @@ export type LoaderModuleResolveParams = {
type PluginSdkPackageJson = {
exports?: Record;
bin?: string | Record;
+ name?: string;
};
const STARTUP_ARGV1 = process.argv[1];
@@ -251,7 +252,7 @@ export function resolvePluginSdkAliasFile(params: {
const cachedPluginSdkExportedSubpaths = new Map();
const cachedPluginSdkScopedAliasMaps = new Map>();
-const PLUGIN_SDK_PACKAGE_NAMES = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"] as const;
+const cachedPluginSdkPackageNames = new Map();
const PLUGIN_SDK_SOURCE_CANDIDATE_EXTENSIONS = [
".ts",
".mts",
@@ -261,6 +262,24 @@ const PLUGIN_SDK_SOURCE_CANDIDATE_EXTENSIONS = [
".cjs",
] as const;
+function listPluginSdkPackageNames(packageRoot: string): string[] {
+ const cached = cachedPluginSdkPackageNames.get(packageRoot);
+ if (cached) {
+ return cached;
+ }
+
+ const packageJson = readPluginSdkPackageJson(packageRoot);
+ const packageNames = new Set(["openclaw/plugin-sdk", "@openclaw/plugin-sdk"]);
+ const rootPackageName = typeof packageJson?.name === "string" ? packageJson.name.trim() : "";
+ if (rootPackageName) {
+ packageNames.add(`${rootPackageName}/plugin-sdk`);
+ }
+
+ const resolved = [...packageNames];
+ cachedPluginSdkPackageNames.set(packageRoot, resolved);
+ return resolved;
+}
+
export function listPluginSdkExportedSubpaths(
params: {
modulePath?: string;
@@ -325,7 +344,7 @@ export function resolvePluginSdkScopedAliasMap(
if (kind === "dist") {
const candidate = path.join(packageRoot, "dist", "plugin-sdk", `${subpath}.js`);
if (fs.existsSync(candidate)) {
- for (const packageName of PLUGIN_SDK_PACKAGE_NAMES) {
+ for (const packageName of listPluginSdkPackageNames(packageRoot)) {
aliasMap[`${packageName}/${subpath}`] = candidate;
}
break;
@@ -337,7 +356,7 @@ export function resolvePluginSdkScopedAliasMap(
if (!fs.existsSync(candidate)) {
continue;
}
- for (const packageName of PLUGIN_SDK_PACKAGE_NAMES) {
+ for (const packageName of listPluginSdkPackageNames(packageRoot)) {
aliasMap[`${packageName}/${subpath}`] = candidate;
}
break;
@@ -391,6 +410,9 @@ export function buildPluginLoaderAliasMap(
moduleUrl?: string,
pluginSdkResolution: PluginSdkResolutionPreference = "auto",
): Record {
+ const packageRoot =
+ resolveLoaderPluginSdkPackageRoot({ modulePath, argv1, moduleUrl, pluginSdkResolution }) ??
+ path.dirname(modulePath);
const pluginSdkAlias = resolvePluginSdkAliasFile({
srcFile: "root-alias.cjs",
distFile: "root-alias.cjs",
@@ -406,7 +428,7 @@ export function buildPluginLoaderAliasMap(
: {}),
...(pluginSdkAlias
? Object.fromEntries(
- PLUGIN_SDK_PACKAGE_NAMES.map((packageName) => [
+ listPluginSdkPackageNames(packageRoot).map((packageName) => [
packageName,
normalizeJitiAliasTargetPath(pluginSdkAlias),
]),
diff --git a/test/release-check.test.ts b/test/release-check.test.ts
index 99ff7bf273517..cc6d031b97c2f 100644
--- a/test/release-check.test.ts
+++ b/test/release-check.test.ts
@@ -13,8 +13,10 @@ import {
collectForbiddenPackPaths,
collectMissingPackPaths,
collectPackUnpackedSizeErrors,
+ createReleaseCheckNpmEnv,
listRequiredQaScenarioPackPaths,
packageNameFromSpecifier,
+ packageNameToNodeModulesSegments,
} from "../scripts/release-check.ts";
import { bundledDistPluginFile, bundledPluginFile } from "./helpers/bundled-plugin-paths.js";
@@ -275,6 +277,19 @@ describe("bundled plugin root runtime mirrors", () => {
});
});
+describe("packageNameToNodeModulesSegments", () => {
+ it("keeps unscoped package names as one path segment", () => {
+ expect(packageNameToNodeModulesSegments("openclaw")).toEqual(["openclaw"]);
+ });
+
+ it("splits scoped package names into node_modules path segments", () => {
+ expect(packageNameToNodeModulesSegments("@qverisai/qverisbot")).toEqual([
+ "@qverisai",
+ "qverisbot",
+ ]);
+ });
+});
+
describe("collectForbiddenPackPaths", () => {
it("blocks all packaged node_modules payloads", () => {
expect(
@@ -418,3 +433,44 @@ describe("collectPackUnpackedSizeErrors", () => {
]);
});
});
+
+describe("createReleaseCheckNpmEnv", () => {
+ it("routes npm cache and logs to a writable scratch directory by default", () => {
+ const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-release-check-env-"));
+
+ try {
+ const env = createReleaseCheckNpmEnv({
+ env: { PATH: process.env.PATH },
+ scratchDir: tempRoot,
+ });
+
+ expect(env.PATH).toBe(process.env.PATH);
+ expect(env.npm_config_cache).toBe(join(tempRoot, "cache"));
+ expect(env.npm_config_logs_dir).toBe(join(tempRoot, "logs"));
+ } finally {
+ rmSync(tempRoot, { recursive: true, force: true });
+ }
+ });
+
+ it("preserves explicit npm cache and log directories", () => {
+ const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-release-check-explicit-"));
+ const explicitCache = join(tempRoot, "custom-cache");
+ const explicitLogs = join(tempRoot, "custom-logs");
+
+ try {
+ const env = createReleaseCheckNpmEnv({
+ env: {
+ PATH: process.env.PATH,
+ npm_config_cache: explicitCache,
+ npm_config_logs_dir: explicitLogs,
+ },
+ scratchDir: join(tempRoot, "scratch"),
+ });
+
+ expect(env.npm_config_cache).toBe(explicitCache);
+ expect(env.npm_config_logs_dir).toBe(explicitLogs);
+ } finally {
+ rmSync(tempRoot, { recursive: true, force: true });
+ }
+ });
+});
diff --git a/test/scripts/runtime-postbuild.test.ts b/test/scripts/runtime-postbuild.test.ts
index 7c10c779927bf..0774207218c74 100644
--- a/test/scripts/runtime-postbuild.test.ts
+++ b/test/scripts/runtime-postbuild.test.ts
@@ -4,6 +4,8 @@ import { describe, expect, it, vi } from "vitest";
import {
copyStaticExtensionAssets,
listStaticExtensionAssetOutputs,
+ rewriteBundledExtensionPackageSelfImports,
+ rewritePackageSelfImportsInSource,
writeStableRootRuntimeAliases,
} from "../../scripts/runtime-postbuild.mjs";
import { createScriptTestHarness } from "./test-helpers.js";
@@ -80,3 +82,46 @@ describe("runtime postbuild static assets", () => {
await expect(fs.stat(path.join(distDir, "library.js"))).rejects.toThrow();
});
});
+
+describe("runtime postbuild package self imports", () => {
+ it("rewrites bundled extension self imports to the published package name", () => {
+ const source = [
+ 'import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";',
+ 'import "openclaw/plugin-sdk/runtime";',
+ 'const loadSetup = () => import("openclaw/plugin-sdk/setup-runtime");',
+ 'const compat = require("openclaw/plugin-sdk/compat");',
+ 'const docs = "https://github.com/openclaw/openclaw";',
+ ].join("\n");
+
+ expect(rewritePackageSelfImportsInSource(source, "@qverisai/qverisbot")).toBe(
+ [
+ 'import { defineBundledChannelEntry } from "@qverisai/qverisbot/plugin-sdk/channel-entry-contract";',
+ 'import "@qverisai/qverisbot/plugin-sdk/runtime";',
+ 'const loadSetup = () => import("@qverisai/qverisbot/plugin-sdk/setup-runtime");',
+ 'const compat = require("@qverisai/qverisbot/plugin-sdk/compat");',
+ 'const docs = "https://github.com/openclaw/openclaw";',
+ ].join("\n"),
+ );
+ });
+
+ it("rewrites dist extension entry files in place", async () => {
+ const rootDir = createTempDir("openclaw-runtime-postbuild-");
+ const entryPath = path.join(rootDir, "dist", "extensions", "discord", "index.js");
+ await fs.mkdir(path.dirname(entryPath), { recursive: true });
+ await fs.writeFile(
+ path.join(rootDir, "package.json"),
+ JSON.stringify({ name: "@qverisai/qverisbot" }),
+ "utf8",
+ );
+ await fs.writeFile(
+ entryPath,
+ 'import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";\n',
+ "utf8",
+ );
+
+ expect(rewriteBundledExtensionPackageSelfImports({ rootDir })).toBe(1);
+ expect(await fs.readFile(entryPath, "utf8")).toBe(
+ 'import { defineBundledChannelEntry } from "@qverisai/qverisbot/plugin-sdk/channel-entry-contract";\n',
+ );
+ });
+});