refactor(plugin-sdk): centralize entrypoint manifest

This commit is contained in:
Peter Steinberger
2026-03-16 01:32:47 +00:00
parent 92e765cdee
commit 59940cb3ee
11 changed files with 150 additions and 414 deletions

View File

@@ -292,6 +292,7 @@
"moltbot:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
"openclaw": "node scripts/run-node.mjs",
"openclaw:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
"plugin-sdk:sync-exports": "node scripts/sync-plugin-sdk-exports.mjs",
"plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
"prepack": "pnpm build && pnpm ui:build",
"prepare": "command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git config core.hooksPath git-hooks || exit 0",

View File

@@ -11,6 +11,7 @@
import { readFileSync, existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { pluginSdkSubpaths } from "./lib/plugin-sdk-entries.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const distFile = resolve(__dirname, "..", "dist", "plugin-sdk", "index.js");
@@ -41,51 +42,6 @@ const exportedNames = exportMatch[1]
const exportSet = new Set(exportedNames);
const requiredSubpathEntries = [
"core",
"compat",
"telegram",
"discord",
"slack",
"signal",
"imessage",
"whatsapp",
"line",
"msteams",
"acpx",
"bluebubbles",
"copilot-proxy",
"device-pair",
"diagnostics-otel",
"diffs",
"feishu",
"googlechat",
"irc",
"llm-task",
"lobster",
"matrix",
"mattermost",
"memory-core",
"memory-lancedb",
"minimax-portal-auth",
"nextcloud-talk",
"nostr",
"open-prose",
"phone-control",
"qwen-portal-auth",
"synology-chat",
"talk-voice",
"test-utils",
"thread-ownership",
"tlon",
"twitch",
"voice-call",
"zalo",
"zalouser",
"account-id",
"keyed-async-queue",
];
const requiredRuntimeShimEntries = ["root-alias.cjs"];
// Critical functions that channel extension plugins import from openclaw/plugin-sdk.
@@ -123,7 +79,7 @@ for (const name of requiredExports) {
}
}
for (const entry of requiredSubpathEntries) {
for (const entry of pluginSdkSubpaths) {
const jsPath = resolve(__dirname, "..", "dist", "plugin-sdk", `${entry}.js`);
const dtsPath = resolve(__dirname, "..", "dist", "plugin-sdk", `${entry}.d.ts`);
if (!existsSync(jsPath)) {

View File

@@ -0,0 +1,78 @@
export const pluginSdkEntrypoints = [
"index",
"core",
"compat",
"telegram",
"discord",
"slack",
"signal",
"imessage",
"whatsapp",
"line",
"msteams",
"acpx",
"bluebubbles",
"copilot-proxy",
"device-pair",
"diagnostics-otel",
"diffs",
"feishu",
"googlechat",
"irc",
"llm-task",
"lobster",
"matrix",
"mattermost",
"memory-core",
"memory-lancedb",
"minimax-portal-auth",
"nextcloud-talk",
"nostr",
"open-prose",
"phone-control",
"qwen-portal-auth",
"synology-chat",
"talk-voice",
"test-utils",
"thread-ownership",
"tlon",
"twitch",
"voice-call",
"zalo",
"zalouser",
"account-id",
"keyed-async-queue",
];
export const pluginSdkSubpaths = pluginSdkEntrypoints.filter((entry) => entry !== "index");
export function buildPluginSdkEntrySources() {
return Object.fromEntries(
pluginSdkEntrypoints.map((entry) => [entry, `src/plugin-sdk/${entry}.ts`]),
);
}
export function buildPluginSdkSpecifiers() {
return pluginSdkEntrypoints.map((entry) =>
entry === "index" ? "openclaw/plugin-sdk" : `openclaw/plugin-sdk/${entry}`,
);
}
export function buildPluginSdkPackageExports() {
return Object.fromEntries(
pluginSdkEntrypoints.map((entry) => [
entry === "index" ? "./plugin-sdk" : `./plugin-sdk/${entry}`,
{
types: `./dist/plugin-sdk/${entry}.d.ts`,
default: `./dist/plugin-sdk/${entry}.js`,
},
]),
);
}
export function listPluginSdkDistArtifacts() {
return pluginSdkEntrypoints.flatMap((entry) => [
`dist/plugin-sdk/${entry}.js`,
`dist/plugin-sdk/${entry}.d.ts`,
]);
}

View File

@@ -10,6 +10,7 @@ import {
type BundledExtension,
type ExtensionPackageJson as PackageJson,
} from "./lib/bundled-extension-manifest.ts";
import { listPluginSdkDistArtifacts } from "./lib/plugin-sdk-entries.mjs";
import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts";
export { collectBundledExtensionManifestErrors } from "./lib/bundled-extension-manifest.ts";
@@ -20,93 +21,8 @@ type PackResult = { files?: PackFile[]; filename?: string; unpackedSize?: number
const requiredPathGroups = [
["dist/index.js", "dist/index.mjs"],
["dist/entry.js", "dist/entry.mjs"],
"dist/plugin-sdk/index.js",
"dist/plugin-sdk/index.d.ts",
"dist/plugin-sdk/core.js",
"dist/plugin-sdk/core.d.ts",
...listPluginSdkDistArtifacts(),
"dist/plugin-sdk/root-alias.cjs",
"dist/plugin-sdk/compat.js",
"dist/plugin-sdk/compat.d.ts",
"dist/plugin-sdk/telegram.js",
"dist/plugin-sdk/telegram.d.ts",
"dist/plugin-sdk/discord.js",
"dist/plugin-sdk/discord.d.ts",
"dist/plugin-sdk/slack.js",
"dist/plugin-sdk/slack.d.ts",
"dist/plugin-sdk/signal.js",
"dist/plugin-sdk/signal.d.ts",
"dist/plugin-sdk/imessage.js",
"dist/plugin-sdk/imessage.d.ts",
"dist/plugin-sdk/whatsapp.js",
"dist/plugin-sdk/whatsapp.d.ts",
"dist/plugin-sdk/line.js",
"dist/plugin-sdk/line.d.ts",
"dist/plugin-sdk/msteams.js",
"dist/plugin-sdk/msteams.d.ts",
"dist/plugin-sdk/acpx.js",
"dist/plugin-sdk/acpx.d.ts",
"dist/plugin-sdk/bluebubbles.js",
"dist/plugin-sdk/bluebubbles.d.ts",
"dist/plugin-sdk/copilot-proxy.js",
"dist/plugin-sdk/copilot-proxy.d.ts",
"dist/plugin-sdk/device-pair.js",
"dist/plugin-sdk/device-pair.d.ts",
"dist/plugin-sdk/diagnostics-otel.js",
"dist/plugin-sdk/diagnostics-otel.d.ts",
"dist/plugin-sdk/diffs.js",
"dist/plugin-sdk/diffs.d.ts",
"dist/plugin-sdk/feishu.js",
"dist/plugin-sdk/feishu.d.ts",
"dist/plugin-sdk/googlechat.js",
"dist/plugin-sdk/googlechat.d.ts",
"dist/plugin-sdk/irc.js",
"dist/plugin-sdk/irc.d.ts",
"dist/plugin-sdk/llm-task.js",
"dist/plugin-sdk/llm-task.d.ts",
"dist/plugin-sdk/lobster.js",
"dist/plugin-sdk/lobster.d.ts",
"dist/plugin-sdk/matrix.js",
"dist/plugin-sdk/matrix.d.ts",
"dist/plugin-sdk/mattermost.js",
"dist/plugin-sdk/mattermost.d.ts",
"dist/plugin-sdk/memory-core.js",
"dist/plugin-sdk/memory-core.d.ts",
"dist/plugin-sdk/memory-lancedb.js",
"dist/plugin-sdk/memory-lancedb.d.ts",
"dist/plugin-sdk/minimax-portal-auth.js",
"dist/plugin-sdk/minimax-portal-auth.d.ts",
"dist/plugin-sdk/nextcloud-talk.js",
"dist/plugin-sdk/nextcloud-talk.d.ts",
"dist/plugin-sdk/nostr.js",
"dist/plugin-sdk/nostr.d.ts",
"dist/plugin-sdk/open-prose.js",
"dist/plugin-sdk/open-prose.d.ts",
"dist/plugin-sdk/phone-control.js",
"dist/plugin-sdk/phone-control.d.ts",
"dist/plugin-sdk/qwen-portal-auth.js",
"dist/plugin-sdk/qwen-portal-auth.d.ts",
"dist/plugin-sdk/synology-chat.js",
"dist/plugin-sdk/synology-chat.d.ts",
"dist/plugin-sdk/talk-voice.js",
"dist/plugin-sdk/talk-voice.d.ts",
"dist/plugin-sdk/test-utils.js",
"dist/plugin-sdk/test-utils.d.ts",
"dist/plugin-sdk/thread-ownership.js",
"dist/plugin-sdk/thread-ownership.d.ts",
"dist/plugin-sdk/tlon.js",
"dist/plugin-sdk/tlon.d.ts",
"dist/plugin-sdk/twitch.js",
"dist/plugin-sdk/twitch.d.ts",
"dist/plugin-sdk/voice-call.js",
"dist/plugin-sdk/voice-call.d.ts",
"dist/plugin-sdk/zalo.js",
"dist/plugin-sdk/zalo.d.ts",
"dist/plugin-sdk/zalouser.js",
"dist/plugin-sdk/zalouser.d.ts",
"dist/plugin-sdk/account-id.js",
"dist/plugin-sdk/account-id.d.ts",
"dist/plugin-sdk/keyed-async-queue.js",
"dist/plugin-sdk/keyed-async-queue.d.ts",
"dist/build-info.json",
];
const forbiddenPrefixes = ["dist/OpenClaw.app/"];

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { buildPluginSdkPackageExports } from "./lib/plugin-sdk-entries.mjs";
const packageJsonPath = path.join(process.cwd(), "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
const currentExports = packageJson.exports ?? {};
const syncedPluginSdkExports = buildPluginSdkPackageExports();
const nextExports = {};
let insertedPluginSdkExports = false;
for (const [key, value] of Object.entries(currentExports)) {
if (key.startsWith("./plugin-sdk")) {
if (!insertedPluginSdkExports) {
Object.assign(nextExports, syncedPluginSdkExports);
insertedPluginSdkExports = true;
}
continue;
}
nextExports[key] = value;
if (key === "." && !insertedPluginSdkExports) {
Object.assign(nextExports, syncedPluginSdkExports);
insertedPluginSdkExports = true;
}
}
if (!insertedPluginSdkExports) {
Object.assign(nextExports, syncedPluginSdkExports);
}
packageJson.exports = nextExports;
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");

View File

@@ -1,57 +1,13 @@
import fs from "node:fs";
import path from "node:path";
import { pluginSdkEntrypoints } from "./lib/plugin-sdk-entries.mjs";
// `tsc` emits declarations under `dist/plugin-sdk/src/plugin-sdk/*` because the source lives
// at `src/plugin-sdk/*` and `rootDir` is `.` (repo root, to support cross-src/extensions refs).
//
// Our package export map points subpath `types` at `dist/plugin-sdk/<entry>.d.ts`, so we
// generate stable entry d.ts files that re-export the real declarations.
const entrypoints = [
"index",
"core",
"compat",
"telegram",
"discord",
"slack",
"signal",
"imessage",
"whatsapp",
"line",
"msteams",
"acpx",
"bluebubbles",
"copilot-proxy",
"device-pair",
"diagnostics-otel",
"diffs",
"feishu",
"googlechat",
"irc",
"llm-task",
"lobster",
"matrix",
"mattermost",
"memory-core",
"memory-lancedb",
"minimax-portal-auth",
"nextcloud-talk",
"nostr",
"open-prose",
"phone-control",
"qwen-portal-auth",
"synology-chat",
"talk-voice",
"test-utils",
"thread-ownership",
"tlon",
"twitch",
"voice-call",
"zalo",
"zalouser",
"account-id",
"keyed-async-queue",
] as const;
for (const entry of entrypoints) {
for (const entry of pluginSdkEntrypoints) {
const out = path.join(process.cwd(), `dist/plugin-sdk/${entry}.d.ts`);
fs.mkdirSync(path.dirname(out), { recursive: true });
// NodeNext: reference the runtime specifier with `.js`, TS will map it to `.d.ts`.

View File

@@ -4,68 +4,15 @@ import path from "node:path";
import { pathToFileURL } from "node:url";
import { build } from "tsdown";
import { describe, expect, it } from "vitest";
import {
buildPluginSdkEntrySources,
buildPluginSdkPackageExports,
buildPluginSdkSpecifiers,
pluginSdkEntrypoints,
} from "../../scripts/lib/plugin-sdk-entries.mjs";
import * as sdk from "./index.js";
const pluginSdkEntrypoints = [
"index",
"core",
"compat",
"telegram",
"discord",
"slack",
"signal",
"imessage",
"whatsapp",
"line",
"msteams",
"acpx",
"bluebubbles",
"copilot-proxy",
"device-pair",
"diagnostics-otel",
"diffs",
"feishu",
"googlechat",
"irc",
"llm-task",
"lobster",
"matrix",
"mattermost",
"memory-core",
"memory-lancedb",
"minimax-portal-auth",
"nextcloud-talk",
"nostr",
"open-prose",
"phone-control",
"qwen-portal-auth",
"synology-chat",
"talk-voice",
"test-utils",
"thread-ownership",
"tlon",
"twitch",
"voice-call",
"zalo",
"zalouser",
"account-id",
"keyed-async-queue",
] as const;
const pluginSdkSpecifiers = pluginSdkEntrypoints.map((entry) =>
entry === "index" ? "openclaw/plugin-sdk" : `openclaw/plugin-sdk/${entry}`,
);
function buildPluginSdkPackageExports() {
return Object.fromEntries(
pluginSdkEntrypoints.map((entry) => [
entry === "index" ? "./plugin-sdk" : `./plugin-sdk/${entry}`,
{
default: `./dist/plugin-sdk/${entry}.js`,
},
]),
);
}
const pluginSdkSpecifiers = buildPluginSdkSpecifiers();
describe("plugin-sdk exports", () => {
it("does not expose runtime modules", () => {
@@ -180,9 +127,7 @@ describe("plugin-sdk exports", () => {
clean: true,
config: false,
dts: false,
entry: Object.fromEntries(
pluginSdkEntrypoints.map((entry) => [entry, `src/plugin-sdk/${entry}.ts`]),
),
entry: buildPluginSdkEntrySources(),
env: { NODE_ENV: "production" },
fixedExtension: false,
logLevel: "error",
@@ -237,4 +182,16 @@ describe("plugin-sdk exports", () => {
await fs.rm(fixtureDir, { recursive: true, force: true });
}
});
it("keeps package.json plugin-sdk exports synced with the manifest", async () => {
const packageJsonPath = path.join(process.cwd(), "package.json");
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as {
exports?: Record<string, unknown>;
};
const currentPluginSdkExports = Object.fromEntries(
Object.entries(packageJson.exports ?? {}).filter(([key]) => key.startsWith("./plugin-sdk")),
);
expect(currentPluginSdkExports).toEqual(buildPluginSdkPackageExports());
});
});

View File

@@ -9,42 +9,14 @@ import * as slackSdk from "openclaw/plugin-sdk/slack";
import * as telegramSdk from "openclaw/plugin-sdk/telegram";
import * as whatsappSdk from "openclaw/plugin-sdk/whatsapp";
import { describe, expect, it } from "vitest";
import { pluginSdkSubpaths } from "../../scripts/lib/plugin-sdk-entries.mjs";
const bundledExtensionSubpathLoaders = [
{ id: "acpx", load: () => import("openclaw/plugin-sdk/acpx") },
{ id: "bluebubbles", load: () => import("openclaw/plugin-sdk/bluebubbles") },
{ id: "copilot-proxy", load: () => import("openclaw/plugin-sdk/copilot-proxy") },
{ id: "device-pair", load: () => import("openclaw/plugin-sdk/device-pair") },
{ id: "diagnostics-otel", load: () => import("openclaw/plugin-sdk/diagnostics-otel") },
{ id: "diffs", load: () => import("openclaw/plugin-sdk/diffs") },
{ id: "feishu", load: () => import("openclaw/plugin-sdk/feishu") },
{ id: "googlechat", load: () => import("openclaw/plugin-sdk/googlechat") },
{ id: "irc", load: () => import("openclaw/plugin-sdk/irc") },
{ id: "llm-task", load: () => import("openclaw/plugin-sdk/llm-task") },
{ id: "lobster", load: () => import("openclaw/plugin-sdk/lobster") },
{ id: "matrix", load: () => import("openclaw/plugin-sdk/matrix") },
{ id: "mattermost", load: () => import("openclaw/plugin-sdk/mattermost") },
{ id: "memory-core", load: () => import("openclaw/plugin-sdk/memory-core") },
{ id: "memory-lancedb", load: () => import("openclaw/plugin-sdk/memory-lancedb") },
{
id: "minimax-portal-auth",
load: () => import("openclaw/plugin-sdk/minimax-portal-auth"),
},
{ id: "nextcloud-talk", load: () => import("openclaw/plugin-sdk/nextcloud-talk") },
{ id: "nostr", load: () => import("openclaw/plugin-sdk/nostr") },
{ id: "open-prose", load: () => import("openclaw/plugin-sdk/open-prose") },
{ id: "phone-control", load: () => import("openclaw/plugin-sdk/phone-control") },
{ id: "qwen-portal-auth", load: () => import("openclaw/plugin-sdk/qwen-portal-auth") },
{ id: "synology-chat", load: () => import("openclaw/plugin-sdk/synology-chat") },
{ id: "talk-voice", load: () => import("openclaw/plugin-sdk/talk-voice") },
{ id: "test-utils", load: () => import("openclaw/plugin-sdk/test-utils") },
{ id: "thread-ownership", load: () => import("openclaw/plugin-sdk/thread-ownership") },
{ id: "tlon", load: () => import("openclaw/plugin-sdk/tlon") },
{ id: "twitch", load: () => import("openclaw/plugin-sdk/twitch") },
{ id: "voice-call", load: () => import("openclaw/plugin-sdk/voice-call") },
{ id: "zalo", load: () => import("openclaw/plugin-sdk/zalo") },
{ id: "zalouser", load: () => import("openclaw/plugin-sdk/zalouser") },
] as const;
const importPluginSdkSubpath = (specifier: string) => import(/* @vite-ignore */ specifier);
const bundledExtensionSubpathLoaders = pluginSdkSubpaths.map((id) => ({
id,
load: () => importPluginSdkSubpath(`openclaw/plugin-sdk/${id}`),
}));
describe("plugin-sdk subpath exports", () => {
it("exports compat helpers", () => {

View File

@@ -10,51 +10,6 @@
"rootDir": ".",
"tsBuildInfoFile": "dist/plugin-sdk/.tsbuildinfo"
},
"include": [
"src/plugin-sdk/index.ts",
"src/plugin-sdk/core.ts",
"src/plugin-sdk/compat.ts",
"src/plugin-sdk/telegram.ts",
"src/plugin-sdk/discord.ts",
"src/plugin-sdk/slack.ts",
"src/plugin-sdk/signal.ts",
"src/plugin-sdk/imessage.ts",
"src/plugin-sdk/whatsapp.ts",
"src/plugin-sdk/line.ts",
"src/plugin-sdk/msteams.ts",
"src/plugin-sdk/account-id.ts",
"src/plugin-sdk/keyed-async-queue.ts",
"src/plugin-sdk/acpx.ts",
"src/plugin-sdk/bluebubbles.ts",
"src/plugin-sdk/copilot-proxy.ts",
"src/plugin-sdk/device-pair.ts",
"src/plugin-sdk/diagnostics-otel.ts",
"src/plugin-sdk/diffs.ts",
"src/plugin-sdk/feishu.ts",
"src/plugin-sdk/googlechat.ts",
"src/plugin-sdk/irc.ts",
"src/plugin-sdk/llm-task.ts",
"src/plugin-sdk/lobster.ts",
"src/plugin-sdk/matrix.ts",
"src/plugin-sdk/mattermost.ts",
"src/plugin-sdk/memory-core.ts",
"src/plugin-sdk/memory-lancedb.ts",
"src/plugin-sdk/minimax-portal-auth.ts",
"src/plugin-sdk/nextcloud-talk.ts",
"src/plugin-sdk/nostr.ts",
"src/plugin-sdk/open-prose.ts",
"src/plugin-sdk/phone-control.ts",
"src/plugin-sdk/qwen-portal-auth.ts",
"src/plugin-sdk/synology-chat.ts",
"src/plugin-sdk/talk-voice.ts",
"src/plugin-sdk/test-utils.ts",
"src/plugin-sdk/thread-ownership.ts",
"src/plugin-sdk/tlon.ts",
"src/plugin-sdk/twitch.ts",
"src/plugin-sdk/voice-call.ts",
"src/plugin-sdk/zalo.ts",
"src/plugin-sdk/zalouser.ts",
"src/types/**/*.d.ts"
],
"include": ["src/plugin-sdk/**/*.ts", "src/types/**/*.d.ts"],
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
}

View File

@@ -1,6 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { defineConfig } from "tsdown";
import { buildPluginSdkEntrySources } from "./scripts/lib/plugin-sdk-entries.mjs";
const env = {
NODE_ENV: "production",
@@ -58,52 +59,6 @@ function nodeBuildConfig(config: Record<string, unknown>) {
};
}
const pluginSdkEntrypoints = [
"index",
"core",
"compat",
"telegram",
"discord",
"slack",
"signal",
"imessage",
"whatsapp",
"line",
"msteams",
"acpx",
"bluebubbles",
"copilot-proxy",
"device-pair",
"diagnostics-otel",
"diffs",
"feishu",
"googlechat",
"irc",
"llm-task",
"lobster",
"matrix",
"mattermost",
"memory-core",
"memory-lancedb",
"minimax-portal-auth",
"nextcloud-talk",
"nostr",
"open-prose",
"phone-control",
"qwen-portal-auth",
"synology-chat",
"talk-voice",
"test-utils",
"thread-ownership",
"tlon",
"twitch",
"voice-call",
"zalo",
"zalouser",
"account-id",
"keyed-async-queue",
] as const;
function listBundledPluginBuildEntries(): Record<string, string> {
const extensionsRoot = path.join(process.cwd(), "extensions");
const entries: Record<string, string> = {};
@@ -189,7 +144,7 @@ export default defineConfig([
nodeBuildConfig({
// Bundle all plugin-sdk entries in a single build so the bundler can share
// common chunks instead of duplicating them per entry (~712MB heap saved).
entry: Object.fromEntries(pluginSdkEntrypoints.map((e) => [e, `src/plugin-sdk/${e}.ts`])),
entry: buildPluginSdkEntrySources(),
outDir: "dist/plugin-sdk",
}),
nodeBuildConfig({

View File

@@ -2,57 +2,13 @@ import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";
import { pluginSdkSubpaths } from "./scripts/lib/plugin-sdk-entries.mjs";
const repoRoot = path.dirname(fileURLToPath(import.meta.url));
const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
const isWindows = process.platform === "win32";
const localWorkers = Math.max(4, Math.min(16, os.cpus().length));
const ciWorkers = isWindows ? 2 : 3;
const pluginSdkSubpaths = [
"account-id",
"core",
"compat",
"telegram",
"discord",
"slack",
"signal",
"imessage",
"whatsapp",
"line",
"msteams",
"acpx",
"bluebubbles",
"copilot-proxy",
"device-pair",
"diagnostics-otel",
"diffs",
"feishu",
"googlechat",
"irc",
"llm-task",
"lobster",
"matrix",
"mattermost",
"memory-core",
"memory-lancedb",
"minimax-portal-auth",
"nextcloud-talk",
"nostr",
"open-prose",
"phone-control",
"qwen-portal-auth",
"synology-chat",
"talk-voice",
"test-utils",
"thread-ownership",
"tlon",
"twitch",
"voice-call",
"zalo",
"zalouser",
"keyed-async-queue",
] as const;
export default defineConfig({
resolve: {
// Keep this ordered: the base `openclaw/plugin-sdk` alias is a prefix match.