Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/.generated/config-baseline.sha256
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions docs/.generated/plugin-sdk-api-baseline.sha256
Original file line number Diff line number Diff line change
@@ -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
5 changes: 4 additions & 1 deletion extensions/googlechat/src/config-schema.ts
Original file line number Diff line number Diff line change
@@ -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);
5 changes: 4 additions & 1 deletion extensions/telegram/src/config-schema.ts
Original file line number Diff line number Diff line change
@@ -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, {
Expand Down
3 changes: 3 additions & 0 deletions extensions/x/channel-plugin-api.ts
Original file line number Diff line number Diff line change
@@ -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";
11 changes: 11 additions & 0 deletions extensions/x/index.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
28 changes: 15 additions & 13 deletions extensions/x/index.ts
Original file line number Diff line number Diff line change
@@ -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",
},
});
3 changes: 3 additions & 0 deletions extensions/x/runtime-api.ts
Original file line number Diff line number Diff line change
@@ -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";
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
45 changes: 44 additions & 1 deletion scripts/release-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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) =>
Expand All @@ -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);
Expand Down Expand Up @@ -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,
});
Expand All @@ -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,
},
Expand Down Expand Up @@ -175,6 +213,7 @@ function installPackedTarball(prefixDir: string, tarballPath: string, cwd: strin
{
cwd,
encoding: "utf8",
env: createReleaseCheckNpmEnv(),
stdio: "inherit",
},
);
Expand All @@ -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();
}
Expand All @@ -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,
[
Expand Down
59 changes: 59 additions & 0 deletions scripts/runtime-postbuild.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /^(?<base>.+\.(?: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
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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) {
Expand Down
23 changes: 19 additions & 4 deletions src/plugin-sdk/root-alias.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The hardcoded pluginSdkPackageNames constant was removed in favor of a dynamic lookup, but the new lookup function listPluginSdkPackageNames is called repeatedly within loops in buildPluginSdkAliasMap. Since it performs synchronous disk I/O to read package.json, this can significantly impact performance during plugin loading.

I suggest adding a cache variable at the top level to store the resolved package names.

let pluginSdkPackageNamesCache = null;
const pluginSdkSourceExtensions = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"];

const isDistRootAlias = __filename.includes(
`${path.sep}dist${path.sep}plugin-sdk${path.sep}root-alias.cjs`,
Expand Down Expand Up @@ -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];
}
Comment on lines +90 to +104

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Implement caching in listPluginSdkPackageNames to avoid redundant synchronous disk I/O when resolving package names multiple times. This is particularly important because this function is called inside a loop over all exported subpaths in buildPluginSdkAliasMap.

function listPluginSdkPackageNames() {
  if (pluginSdkPackageNamesCache) {
    return pluginSdkPackageNamesCache;
  }
  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.
  }
  pluginSdkPackageNamesCache = [...packageNames];
  return pluginSdkPackageNamesCache;
}


function findDistChunkByPrefix(prefix) {
const distRoot = path.join(getPackageRoot(), "dist");
try {
Expand Down Expand Up @@ -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);
}
}
Expand All @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions src/plugins/contracts/plugin-sdk-root-alias.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function loadRootAliasWithStubs(options?: {
env?: Record<string, string | undefined>;
monolithicExports?: Record<string | symbol, unknown>;
aliasPath?: string;
packageName?: string;
packageExports?: Record<string, unknown>;
platform?: string;
existingPaths?: string[];
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading