refactor: dedupe bundled plugin entrypoints

This commit is contained in:
Peter Steinberger
2026-03-17 00:14:01 -07:00
parent be4fdb9222
commit 6f795fd60e
69 changed files with 814 additions and 850 deletions

View File

@@ -1,14 +1,13 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
const PROVIDER_ID = "amazon-bedrock";
const CLAUDE_46_MODEL_RE = /claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i;
const amazonBedrockPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Amazon Bedrock Provider",
description: "Bundled Amazon Bedrock provider policy plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Amazon Bedrock",
@@ -18,6 +17,4 @@ const amazonBedrockPlugin = {
CLAUDE_46_MODEL_RE.test(modelId.trim()) ? "adaptive" : undefined,
});
},
};
export default amazonBedrockPlugin;
});

View File

@@ -1,8 +1,7 @@
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
import { parseDurationMs } from "openclaw/plugin-sdk/cli-runtime";
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
definePluginEntry,
type ProviderAuthContext,
type ProviderResolveDynamicModelContext,
type ProviderRuntimeModel,
@@ -312,12 +311,11 @@ async function runAnthropicSetupTokenNonInteractive(ctx: {
});
}
const anthropicPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Anthropic Provider",
description: "Bundled Anthropic provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Anthropic",
@@ -399,6 +397,4 @@ const anthropicPlugin = {
});
api.registerMediaUnderstandingProvider(anthropicMediaUnderstandingProvider);
},
};
export default anthropicPlugin;
});

View File

@@ -1,16 +1,15 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import {
createPluginBackedWebSearchProvider,
getTopLevelCredentialValue,
setTopLevelCredentialValue,
} from "openclaw/plugin-sdk/provider-web-search";
const bravePlugin = {
export default definePluginEntry({
id: "brave",
name: "Brave Plugin",
description: "Bundled Brave plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerWebSearchProvider(
createPluginBackedWebSearchProvider({
id: "brave",
@@ -26,6 +25,4 @@ const bravePlugin = {
}),
);
},
};
export default bravePlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { ensureModelAllowlistEntry } from "openclaw/plugin-sdk/provider-onboard";
import { buildBytePlusCodingProvider, buildBytePlusProvider } from "./provider-catalog.js";
@@ -6,12 +6,11 @@ import { buildBytePlusCodingProvider, buildBytePlusProvider } from "./provider-c
const PROVIDER_ID = "byteplus";
const BYTEPLUS_DEFAULT_MODEL_REF = "byteplus-plan/ark-code-latest";
const byteplusPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "BytePlus Provider",
description: "Bundled BytePlus provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "BytePlus",
@@ -60,6 +59,4 @@ const byteplusPlugin = {
},
});
},
};
export default byteplusPlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import {
applyAuthProfileConfig,
buildApiKeyCredential,
@@ -84,12 +84,11 @@ async function resolveCloudflareGatewayMetadataInteractive(ctx: {
return { accountId, gatewayId };
}
const cloudflareAiGatewayPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Cloudflare AI Gateway Provider",
description: "Bundled Cloudflare AI Gateway provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Cloudflare AI Gateway",
@@ -252,6 +251,4 @@ const cloudflareAiGatewayPlugin = {
},
});
},
};
export default cloudflareAiGatewayPlugin;
});

View File

@@ -1,6 +1,5 @@
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
definePluginEntry,
type ProviderAuthContext,
type ProviderAuthResult,
} from "openclaw/plugin-sdk/copilot-proxy";
@@ -71,12 +70,11 @@ function buildModelDefinition(modelId: string) {
};
}
const copilotProxyPlugin = {
export default definePluginEntry({
id: "copilot-proxy",
name: "Copilot Proxy",
description: "Local Copilot Proxy (VS Code LM) provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: "copilot-proxy",
label: "Copilot Proxy",
@@ -157,6 +155,4 @@ const copilotProxyPlugin = {
},
});
},
};
export default copilotProxyPlugin;
});

View File

@@ -1,12 +1,13 @@
import os from "node:os";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/device-pair";
import {
approveDevicePairing,
definePluginEntry,
issueDeviceBootstrapToken,
listDevicePairing,
resolveGatewayBindUrl,
runPluginCommandWithTimeout,
resolveTailnetHostWithRunner,
type OpenClawPluginApi,
} from "openclaw/plugin-sdk/device-pair";
import qrcode from "qrcode-terminal";
import {
@@ -325,226 +326,233 @@ function formatSetupInstructions(): string {
].join("\n");
}
export default function register(api: OpenClawPluginApi) {
registerPairingNotifierService(api);
export default definePluginEntry({
id: "device-pair",
name: "Device Pair",
description: "QR/bootstrap pairing helpers for OpenClaw devices",
register(api: OpenClawPluginApi) {
registerPairingNotifierService(api);
api.registerCommand({
name: "pair",
description: "Generate setup codes and approve device pairing requests.",
acceptsArgs: true,
handler: async (ctx) => {
const args = ctx.args?.trim() ?? "";
const tokens = args.split(/\s+/).filter(Boolean);
const action = tokens[0]?.toLowerCase() ?? "";
api.logger.info?.(
`device-pair: /pair invoked channel=${ctx.channel} sender=${ctx.senderId ?? "unknown"} action=${
action || "new"
}`,
);
api.registerCommand({
name: "pair",
description: "Generate setup codes and approve device pairing requests.",
acceptsArgs: true,
handler: async (ctx) => {
const args = ctx.args?.trim() ?? "";
const tokens = args.split(/\s+/).filter(Boolean);
const action = tokens[0]?.toLowerCase() ?? "";
api.logger.info?.(
`device-pair: /pair invoked channel=${ctx.channel} sender=${ctx.senderId ?? "unknown"} action=${
action || "new"
}`,
);
if (action === "status" || action === "pending") {
const list = await listDevicePairing();
return { text: formatPendingRequests(list.pending) };
}
if (action === "notify") {
const notifyAction = tokens[1]?.trim().toLowerCase() ?? "status";
return await handleNotifyCommand({
api,
ctx,
action: notifyAction,
});
}
if (action === "approve") {
const requested = tokens[1]?.trim();
const list = await listDevicePairing();
if (list.pending.length === 0) {
return { text: "No pending device pairing requests." };
if (action === "status" || action === "pending") {
const list = await listDevicePairing();
return { text: formatPendingRequests(list.pending) };
}
let pending: (typeof list.pending)[number] | undefined;
if (requested) {
if (requested.toLowerCase() === "latest") {
pending = [...list.pending].toSorted((a, b) => (b.ts ?? 0) - (a.ts ?? 0))[0];
} else {
pending = list.pending.find((entry) => entry.requestId === requested);
if (action === "notify") {
const notifyAction = tokens[1]?.trim().toLowerCase() ?? "status";
return await handleNotifyCommand({
api,
ctx,
action: notifyAction,
});
}
if (action === "approve") {
const requested = tokens[1]?.trim();
const list = await listDevicePairing();
if (list.pending.length === 0) {
return { text: "No pending device pairing requests." };
}
} else if (list.pending.length === 1) {
pending = list.pending[0];
} else {
let pending: (typeof list.pending)[number] | undefined;
if (requested) {
if (requested.toLowerCase() === "latest") {
pending = [...list.pending].toSorted((a, b) => (b.ts ?? 0) - (a.ts ?? 0))[0];
} else {
pending = list.pending.find((entry) => entry.requestId === requested);
}
} else if (list.pending.length === 1) {
pending = list.pending[0];
} else {
return {
text:
`${formatPendingRequests(list.pending)}\n\n` +
"Multiple pending requests found. Approve one explicitly:\n" +
"/pair approve <requestId>\n" +
"Or approve the most recent:\n" +
"/pair approve latest",
};
}
if (!pending) {
return { text: "Pairing request not found." };
}
const approved = await approveDevicePairing(pending.requestId);
if (!approved) {
return { text: "Pairing request not found." };
}
const label = approved.device.displayName?.trim() || approved.device.deviceId;
const platform = approved.device.platform?.trim();
const platformLabel = platform ? ` (${platform})` : "";
return { text: `✅ Paired ${label}${platformLabel}.` };
}
const authLabelResult = resolveAuthLabel(api.config);
if (authLabelResult.error) {
return { text: `Error: ${authLabelResult.error}` };
}
const urlResult = await resolveGatewayUrl(api);
if (!urlResult.url) {
return { text: `Error: ${urlResult.error ?? "Gateway URL unavailable."}` };
}
const payload: SetupPayload = {
url: urlResult.url,
bootstrapToken: (await issueDeviceBootstrapToken()).token,
};
if (action === "qr") {
const setupCode = encodeSetupCode(payload);
const qrAscii = await renderQrAscii(setupCode);
const authLabel = authLabelResult.label ?? "auth";
const channel = ctx.channel;
const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
let autoNotifyArmed = false;
if (channel === "telegram" && target) {
try {
autoNotifyArmed = await armPairNotifyOnce({ api, ctx });
} catch (err) {
api.logger.warn?.(
`device-pair: failed to arm one-shot pairing notify (${String(
(err as Error)?.message ?? err,
)})`,
);
}
}
if (channel === "telegram" && target) {
try {
const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
if (send) {
await send(
target,
["Scan this QR code with the OpenClaw iOS app:", "", "```", qrAscii, "```"].join(
"\n",
),
{
...(ctx.messageThreadId != null
? { messageThreadId: ctx.messageThreadId }
: {}),
...(ctx.accountId ? { accountId: ctx.accountId } : {}),
},
);
return {
text: [
`Gateway: ${payload.url}`,
`Auth: ${authLabel}`,
"",
autoNotifyArmed
? "After scanning, wait here for the pairing request ping."
: "After scanning, come back here and run `/pair approve` to complete pairing.",
...(autoNotifyArmed
? [
"Ill auto-ping here when the pairing request arrives, then auto-disable.",
"If the ping does not arrive, run `/pair approve latest` manually.",
]
: []),
].join("\n"),
};
}
} catch (err) {
api.logger.warn?.(
`device-pair: telegram QR send failed, falling back (${String(
(err as Error)?.message ?? err,
)})`,
);
}
}
// Render based on channel capability
api.logger.info?.(`device-pair: QR fallback channel=${channel} target=${target}`);
const infoLines = [
`Gateway: ${payload.url}`,
`Auth: ${authLabel}`,
"",
autoNotifyArmed
? "After scanning, wait here for the pairing request ping."
: "After scanning, run `/pair approve` to complete pairing.",
...(autoNotifyArmed
? [
"Ill auto-ping here when the pairing request arrives, then auto-disable.",
"If the ping does not arrive, run `/pair approve latest` manually.",
]
: []),
];
// WebUI + CLI/TUI: ASCII QR
return {
text:
`${formatPendingRequests(list.pending)}\n\n` +
"Multiple pending requests found. Approve one explicitly:\n" +
"/pair approve <requestId>\n" +
"Or approve the most recent:\n" +
"/pair approve latest",
text: [
"Scan this QR code with the OpenClaw iOS app:",
"",
"```",
qrAscii,
"```",
"",
...infoLines,
].join("\n"),
};
}
if (!pending) {
return { text: "Pairing request not found." };
}
const approved = await approveDevicePairing(pending.requestId);
if (!approved) {
return { text: "Pairing request not found." };
}
const label = approved.device.displayName?.trim() || approved.device.deviceId;
const platform = approved.device.platform?.trim();
const platformLabel = platform ? ` (${platform})` : "";
return { text: `✅ Paired ${label}${platformLabel}.` };
}
const authLabelResult = resolveAuthLabel(api.config);
if (authLabelResult.error) {
return { text: `Error: ${authLabelResult.error}` };
}
const urlResult = await resolveGatewayUrl(api);
if (!urlResult.url) {
return { text: `Error: ${urlResult.error ?? "Gateway URL unavailable."}` };
}
const payload: SetupPayload = {
url: urlResult.url,
bootstrapToken: (await issueDeviceBootstrapToken()).token,
};
if (action === "qr") {
const setupCode = encodeSetupCode(payload);
const qrAscii = await renderQrAscii(setupCode);
const authLabel = authLabelResult.label ?? "auth";
const channel = ctx.channel;
const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
let autoNotifyArmed = false;
const authLabel = authLabelResult.label ?? "auth";
if (channel === "telegram" && target) {
try {
autoNotifyArmed = await armPairNotifyOnce({ api, ctx });
} catch (err) {
api.logger.warn?.(
`device-pair: failed to arm one-shot pairing notify (${String(
(err as Error)?.message ?? err,
)})`,
const runtimeKeys = Object.keys(api.runtime ?? {});
const channelKeys = Object.keys(api.runtime?.channel ?? {});
api.logger.debug?.(
`device-pair: runtime keys=${runtimeKeys.join(",") || "none"} channel keys=${
channelKeys.join(",") || "none"
}`,
);
}
}
if (channel === "telegram" && target) {
try {
const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
if (send) {
await send(
target,
["Scan this QR code with the OpenClaw iOS app:", "", "```", qrAscii, "```"].join(
"\n",
),
{
...(ctx.messageThreadId != null ? { messageThreadId: ctx.messageThreadId } : {}),
...(ctx.accountId ? { accountId: ctx.accountId } : {}),
},
if (!send) {
throw new Error(
`telegram runtime unavailable (runtime keys: ${runtimeKeys.join(",")}; channel keys: ${channelKeys.join(
",",
)})`,
);
return {
text: [
`Gateway: ${payload.url}`,
`Auth: ${authLabel}`,
"",
autoNotifyArmed
? "After scanning, wait here for the pairing request ping."
: "After scanning, come back here and run `/pair approve` to complete pairing.",
...(autoNotifyArmed
? [
"Ill auto-ping here when the pairing request arrives, then auto-disable.",
"If the ping does not arrive, run `/pair approve latest` manually.",
]
: []),
].join("\n"),
};
}
await send(target, formatSetupInstructions(), {
...(ctx.messageThreadId != null ? { messageThreadId: ctx.messageThreadId } : {}),
...(ctx.accountId ? { accountId: ctx.accountId } : {}),
});
api.logger.info?.(
`device-pair: telegram split send ok target=${target} account=${ctx.accountId ?? "none"} thread=${
ctx.messageThreadId ?? "none"
}`,
);
return { text: encodeSetupCode(payload) };
} catch (err) {
api.logger.warn?.(
`device-pair: telegram QR send failed, falling back (${String(
`device-pair: telegram split send failed, falling back to single message (${String(
(err as Error)?.message ?? err,
)})`,
);
}
}
// Render based on channel capability
api.logger.info?.(`device-pair: QR fallback channel=${channel} target=${target}`);
const infoLines = [
`Gateway: ${payload.url}`,
`Auth: ${authLabel}`,
"",
autoNotifyArmed
? "After scanning, wait here for the pairing request ping."
: "After scanning, run `/pair approve` to complete pairing.",
...(autoNotifyArmed
? [
"Ill auto-ping here when the pairing request arrives, then auto-disable.",
"If the ping does not arrive, run `/pair approve latest` manually.",
]
: []),
];
// WebUI + CLI/TUI: ASCII QR
return {
text: [
"Scan this QR code with the OpenClaw iOS app:",
"",
"```",
qrAscii,
"```",
"",
...infoLines,
].join("\n"),
text: formatSetupReply(payload, authLabel),
};
}
const channel = ctx.channel;
const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
const authLabel = authLabelResult.label ?? "auth";
if (channel === "telegram" && target) {
try {
const runtimeKeys = Object.keys(api.runtime ?? {});
const channelKeys = Object.keys(api.runtime?.channel ?? {});
api.logger.debug?.(
`device-pair: runtime keys=${runtimeKeys.join(",") || "none"} channel keys=${
channelKeys.join(",") || "none"
}`,
);
const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
if (!send) {
throw new Error(
`telegram runtime unavailable (runtime keys: ${runtimeKeys.join(",")}; channel keys: ${channelKeys.join(
",",
)})`,
);
}
await send(target, formatSetupInstructions(), {
...(ctx.messageThreadId != null ? { messageThreadId: ctx.messageThreadId } : {}),
...(ctx.accountId ? { accountId: ctx.accountId } : {}),
});
api.logger.info?.(
`device-pair: telegram split send ok target=${target} account=${ctx.accountId ?? "none"} thread=${
ctx.messageThreadId ?? "none"
}`,
);
return { text: encodeSetupCode(payload) };
} catch (err) {
api.logger.warn?.(
`device-pair: telegram split send failed, falling back to single message (${String(
(err as Error)?.message ?? err,
)})`,
);
}
}
return {
text: formatSetupReply(payload, authLabel),
};
},
});
}
},
});
},
});

View File

@@ -1,15 +1,11 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diagnostics-otel";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/diagnostics-otel";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createDiagnosticsOtelService } from "./src/service.js";
const plugin = {
export default definePluginEntry({
id: "diagnostics-otel",
name: "Diagnostics OpenTelemetry",
description: "Export diagnostics events to OpenTelemetry",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerService(createDiagnosticsOtelService());
},
};
export default plugin;
});

View File

@@ -1,14 +1,11 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { buildElevenLabsSpeechProvider } from "openclaw/plugin-sdk/speech";
const elevenLabsPlugin = {
export default definePluginEntry({
id: "elevenlabs",
name: "ElevenLabs Speech",
description: "Bundled ElevenLabs speech provider",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerSpeechProvider(buildElevenLabsSpeechProvider());
},
};
export default elevenLabsPlugin;
});

View File

@@ -1,22 +1,15 @@
import {
emptyPluginConfigSchema,
type AnyAgentTool,
type OpenClawPluginApi,
} from "openclaw/plugin-sdk/core";
import { definePluginEntry, type AnyAgentTool } from "openclaw/plugin-sdk/core";
import { createFirecrawlScrapeTool } from "./src/firecrawl-scrape-tool.js";
import { createFirecrawlWebSearchProvider } from "./src/firecrawl-search-provider.js";
import { createFirecrawlSearchTool } from "./src/firecrawl-search-tool.js";
const firecrawlPlugin = {
export default definePluginEntry({
id: "firecrawl",
name: "Firecrawl Plugin",
description: "Bundled Firecrawl search and scrape plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerWebSearchProvider(createFirecrawlWebSearchProvider());
api.registerTool(createFirecrawlSearchTool(api) as AnyAgentTool);
api.registerTool(createFirecrawlScrapeTool(api) as AnyAgentTool);
},
};
export default firecrawlPlugin;
});

View File

@@ -1,6 +1,5 @@
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
definePluginEntry,
type ProviderAuthContext,
type ProviderResolveDynamicModelContext,
type ProviderRuntimeModel,
@@ -116,12 +115,11 @@ async function runGitHubCopilotAuth(ctx: ProviderAuthContext) {
};
}
const githubCopilotPlugin = {
export default definePluginEntry({
id: "github-copilot",
name: "GitHub Copilot Provider",
description: "Bundled GitHub Copilot provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "GitHub Copilot",
@@ -196,6 +194,4 @@ const githubCopilotPlugin = {
await fetchCopilotUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn),
});
},
};
export default githubCopilotPlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { buildGoogleImageGenerationProvider } from "openclaw/plugin-sdk/image-generation";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import {
@@ -14,12 +14,11 @@ import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js";
import { googleMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { isModernGoogleModel, resolveGoogle31ForwardCompatModel } from "./provider-models.js";
const googlePlugin = {
export default definePluginEntry({
id: "google",
name: "Google Plugin",
description: "Bundled Google plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: "google",
label: "Google AI Studio",
@@ -70,6 +69,4 @@ const googlePlugin = {
}),
);
},
};
export default googlePlugin;
});

View File

@@ -1,16 +1,15 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { applyHuggingfaceConfig, HUGGINGFACE_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildHuggingfaceProvider } from "./provider-catalog.js";
const PROVIDER_ID = "huggingface";
const huggingfacePlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Hugging Face Provider",
description: "Bundled Hugging Face provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Hugging Face",
@@ -56,6 +55,4 @@ const huggingfacePlugin = {
},
});
},
};
export default huggingfacePlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import {
@@ -10,12 +10,11 @@ import { buildKilocodeProviderWithDiscovery } from "./provider-catalog.js";
const PROVIDER_ID = "kilocode";
const kilocodePlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Kilo Gateway Provider",
description: "Bundled Kilo Gateway provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Kilo Gateway",
@@ -66,6 +65,4 @@ const kilocodePlugin = {
isCacheTtlEligible: (ctx) => ctx.modelId.startsWith("anthropic/"),
});
},
};
export default kilocodePlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { isRecord } from "openclaw/plugin-sdk/text-runtime";
import { applyKimiCodeConfig, KIMI_CODING_MODEL_REF } from "./onboard.js";
@@ -7,12 +7,11 @@ import { buildKimiCodingProvider } from "./provider-catalog.js";
const PLUGIN_ID = "kimi";
const PROVIDER_ID = "kimi";
const kimiCodingPlugin = {
export default definePluginEntry({
id: PLUGIN_ID,
name: "Kimi Provider",
description: "Bundled Kimi provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Kimi",
@@ -82,6 +81,4 @@ const kimiCodingPlugin = {
},
});
},
};
export default kimiCodingPlugin;
});

View File

@@ -1,6 +1,15 @@
import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/llm-task";
import {
definePluginEntry,
type AnyAgentTool,
type OpenClawPluginApi,
} from "openclaw/plugin-sdk/llm-task";
import { createLlmTaskTool } from "./src/llm-task-tool.js";
export default function register(api: OpenClawPluginApi) {
api.registerTool(createLlmTaskTool(api) as unknown as AnyAgentTool, { optional: true });
}
export default definePluginEntry({
id: "llm-task",
name: "LLM Task",
description: "Optional tool for structured subtask execution",
register(api: OpenClawPluginApi) {
api.registerTool(createLlmTaskTool(api) as unknown as AnyAgentTool, { optional: true });
},
});

View File

@@ -1,18 +1,24 @@
import type {
AnyAgentTool,
OpenClawPluginApi,
OpenClawPluginToolFactory,
import {
definePluginEntry,
type AnyAgentTool,
type OpenClawPluginApi,
type OpenClawPluginToolFactory,
} from "openclaw/plugin-sdk/lobster";
import { createLobsterTool } from "./src/lobster-tool.js";
export default function register(api: OpenClawPluginApi) {
api.registerTool(
((ctx) => {
if (ctx.sandboxed) {
return null;
}
return createLobsterTool(api) as AnyAgentTool;
}) as OpenClawPluginToolFactory,
{ optional: true },
);
}
export default definePluginEntry({
id: "lobster",
name: "Lobster",
description: "Optional local shell helper tools",
register(api: OpenClawPluginApi) {
api.registerTool(
((ctx) => {
if (ctx.sandboxed) {
return null;
}
return createLobsterTool(api) as AnyAgentTool;
}) as OpenClawPluginToolFactory,
{ optional: true },
);
},
});

View File

@@ -1,13 +1,11 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/memory-core";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/memory-core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
const memoryCorePlugin = {
export default definePluginEntry({
id: "memory-core",
name: "Memory (Core)",
description: "File-backed memory search tools and CLI",
kind: "memory",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerTool(
(ctx) => {
const memorySearchTool = api.runtime.tools.createMemorySearchTool({
@@ -33,6 +31,4 @@ const memoryCorePlugin = {
{ commands: ["memory"] },
);
},
};
export default memoryCorePlugin;
});

View File

@@ -18,6 +18,18 @@ const HAS_OPENAI_KEY = Boolean(process.env.OPENAI_API_KEY);
const liveEnabled = HAS_OPENAI_KEY && process.env.OPENCLAW_LIVE_TEST === "1";
const describeLive = liveEnabled ? describe : describe.skip;
type MemoryPluginTestConfig = {
embedding?: {
apiKey?: string;
model?: string;
dimensions?: number;
};
dbPath?: string;
captureMaxChars?: number;
autoCapture?: boolean;
autoRecall?: boolean;
};
function installTmpDirHarness(params: { prefix: string }) {
let tmpDir = "";
let dbPath = "";
@@ -51,7 +63,7 @@ describe("memory plugin e2e", () => {
},
dbPath: getDbPath(),
...overrides,
});
}) as MemoryPluginTestConfig | undefined;
}
test("memory plugin registers and initializes correctly", async () => {
@@ -89,7 +101,7 @@ describe("memory plugin e2e", () => {
apiKey: "${TEST_MEMORY_API_KEY}",
},
dbPath: getDbPath(),
});
}) as MemoryPluginTestConfig | undefined;
expect(config?.embedding?.apiKey).toBe("test-key-123");

View File

@@ -10,7 +10,7 @@ import { randomUUID } from "node:crypto";
import type * as LanceDB from "@lancedb/lancedb";
import { Type } from "@sinclair/typebox";
import OpenAI from "openai";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/memory-lancedb";
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/memory-lancedb";
import {
DEFAULT_CAPTURE_MAX_CHARS,
MEMORY_CATEGORIES,
@@ -289,7 +289,7 @@ export function detectCategory(text: string): MemoryCategory {
// Plugin Definition
// ============================================================================
const memoryPlugin = {
export default definePluginEntry({
id: "memory-lancedb",
name: "Memory (LanceDB)",
description: "LanceDB-backed long-term memory with auto-recall/capture",
@@ -673,6 +673,4 @@ const memoryPlugin = {
},
});
},
};
export default memoryPlugin;
});

View File

@@ -1,14 +1,11 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { buildMicrosoftSpeechProvider } from "openclaw/plugin-sdk/speech";
const microsoftPlugin = {
export default definePluginEntry({
id: "microsoft",
name: "Microsoft Speech",
description: "Bundled Microsoft speech provider",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerSpeechProvider(buildMicrosoftSpeechProvider());
},
};
export default microsoftPlugin;
});

View File

@@ -1,7 +1,6 @@
import {
buildOauthProviderAuthResult,
emptyPluginConfigSchema,
type OpenClawPluginApi,
definePluginEntry,
type ProviderAuthContext,
type ProviderAuthResult,
type ProviderCatalogContext,
@@ -159,12 +158,11 @@ function createOAuthHandler(region: MiniMaxRegion) {
};
}
const minimaxPlugin = {
export default definePluginEntry({
id: API_PROVIDER_ID,
name: "MiniMax",
description: "Bundled MiniMax API-key and OAuth provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: API_PROVIDER_ID,
label: PROVIDER_LABEL,
@@ -280,6 +278,4 @@ const minimaxPlugin = {
api.registerMediaUnderstandingProvider(minimaxMediaUnderstandingProvider);
api.registerMediaUnderstandingProvider(minimaxPortalMediaUnderstandingProvider);
},
};
export default minimaxPlugin;
});

View File

@@ -1,16 +1,15 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { mistralMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { applyMistralConfig, MISTRAL_DEFAULT_MODEL_REF } from "./onboard.js";
const PROVIDER_ID = "mistral";
const mistralPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Mistral Provider",
description: "Bundled Mistral provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Mistral",
@@ -53,6 +52,4 @@ const mistralPlugin = {
});
api.registerMediaUnderstandingProvider(mistralMediaUnderstandingProvider);
},
};
export default mistralPlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import {
@@ -10,12 +10,11 @@ import { buildModelStudioProvider } from "./provider-catalog.js";
const PROVIDER_ID = "modelstudio";
const modelStudioPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Model Studio Provider",
description: "Bundled Model Studio provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Model Studio",
@@ -89,6 +88,4 @@ const modelStudioPlugin = {
},
});
},
};
export default modelStudioPlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import {
@@ -20,12 +20,11 @@ import { buildMoonshotProvider } from "./provider-catalog.js";
const PROVIDER_ID = "moonshot";
const moonshotPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Moonshot Provider",
description: "Bundled Moonshot provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Moonshot",
@@ -108,6 +107,4 @@ const moonshotPlugin = {
}),
);
},
};
export default moonshotPlugin;
});

View File

@@ -1,15 +1,14 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { buildNvidiaProvider } from "./provider-catalog.js";
const PROVIDER_ID = "nvidia";
const nvidiaPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "NVIDIA Provider",
description: "Bundled NVIDIA provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "NVIDIA",
@@ -27,6 +26,4 @@ const nvidiaPlugin = {
},
});
},
};
export default nvidiaPlugin;
});

View File

@@ -1,5 +1,5 @@
import {
emptyPluginConfigSchema,
definePluginEntry,
type OpenClawPluginApi,
type ProviderAuthContext,
type ProviderAuthMethodNonInteractiveContext,
@@ -15,11 +15,10 @@ async function loadProviderSetup() {
return await import("openclaw/plugin-sdk/ollama-setup");
}
const ollamaPlugin = {
export default definePluginEntry({
id: "ollama",
name: "Ollama Provider",
description: "Bundled Ollama provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
api.registerProvider({
id: PROVIDER_ID,
@@ -123,6 +122,4 @@ const ollamaPlugin = {
},
});
},
};
export default ollamaPlugin;
});

View File

@@ -1,5 +1,10 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/open-prose";
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/open-prose";
export default function register(_api: OpenClawPluginApi) {
// OpenProse is delivered via plugin-shipped skills.
}
export default definePluginEntry({
id: "open-prose",
name: "OpenProse",
description: "Plugin-shipped prose skills bundle",
register(_api: OpenClawPluginApi) {
// OpenProse is delivered via plugin-shipped skills.
},
});

View File

@@ -1,22 +1,19 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { buildOpenAIImageGenerationProvider } from "openclaw/plugin-sdk/image-generation";
import { buildOpenAISpeechProvider } from "openclaw/plugin-sdk/speech";
import { openaiMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { buildOpenAICodexProviderPlugin } from "./openai-codex-provider.js";
import { buildOpenAIProvider } from "./openai-provider.js";
const openAIPlugin = {
export default definePluginEntry({
id: "openai",
name: "OpenAI Provider",
description: "Bundled OpenAI provider plugins",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider(buildOpenAIProvider());
api.registerProvider(buildOpenAICodexProviderPlugin());
api.registerSpeechProvider(buildOpenAISpeechProvider());
api.registerMediaUnderstandingProvider(openaiMediaUnderstandingProvider);
api.registerImageGenerationProvider(buildOpenAIImageGenerationProvider());
},
};
export default openAIPlugin;
});

View File

@@ -1,16 +1,15 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { OPENCODE_GO_DEFAULT_MODEL_REF } from "openclaw/plugin-sdk/provider-models";
import { applyOpencodeGoConfig } from "./onboard.js";
const PROVIDER_ID = "opencode-go";
const opencodeGoPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "OpenCode Go Provider",
description: "Bundled OpenCode Go provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "OpenCode Go",
@@ -53,6 +52,4 @@ const opencodeGoPlugin = {
isModernModelRef: () => true,
});
},
};
export default opencodeGoPlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { OPENCODE_ZEN_DEFAULT_MODEL } from "openclaw/plugin-sdk/provider-models";
import { applyOpencodeZenConfig } from "./onboard.js";
@@ -14,12 +14,11 @@ function isModernOpencodeModel(modelId: string): boolean {
return !lower.startsWith(MINIMAX_PREFIX);
}
const opencodePlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "OpenCode Zen Provider",
description: "Bundled OpenCode Zen provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "OpenCode Zen",
@@ -63,6 +62,4 @@ const opencodePlugin = {
isModernModelRef: ({ modelId }) => isModernOpencodeModel(modelId),
});
},
};
export default opencodePlugin;
});

View File

@@ -1,7 +1,6 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
definePluginEntry,
type ProviderResolveDynamicModelContext,
type ProviderRuntimeModel,
} from "openclaw/plugin-sdk/core";
@@ -74,12 +73,11 @@ function isOpenRouterCacheTtlModel(modelId: string): boolean {
return OPENROUTER_CACHE_TTL_MODEL_PREFIXES.some((prefix) => modelId.startsWith(prefix));
}
const openRouterPlugin = {
export default definePluginEntry({
id: "openrouter",
name: "OpenRouter Provider",
description: "Bundled OpenRouter provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "OpenRouter",
@@ -151,6 +149,4 @@ const openRouterPlugin = {
isCacheTtlEligible: (ctx) => isOpenRouterCacheTtlModel(ctx.modelId),
});
},
};
export default openRouterPlugin;
});

View File

@@ -1,16 +1,15 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import {
createPluginBackedWebSearchProvider,
getScopedCredentialValue,
setScopedCredentialValue,
} from "openclaw/plugin-sdk/provider-web-search";
const perplexityPlugin = {
export default definePluginEntry({
id: "perplexity",
name: "Perplexity Plugin",
description: "Bundled Perplexity plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerWebSearchProvider(
createPluginBackedWebSearchProvider({
id: "perplexity",
@@ -27,6 +26,4 @@ const perplexityPlugin = {
}),
);
},
};
export default perplexityPlugin;
});

View File

@@ -68,7 +68,7 @@ describe("phone-control plugin", () => {
});
let command: OpenClawPluginCommandDefinition | undefined;
registerPhoneControl(
registerPhoneControl.register(
createApi({
stateDir,
getConfig: () => config,

View File

@@ -1,6 +1,10 @@
import fs from "node:fs/promises";
import path from "node:path";
import type { OpenClawPluginApi, OpenClawPluginService } from "openclaw/plugin-sdk/phone-control";
import {
definePluginEntry,
type OpenClawPluginApi,
type OpenClawPluginService,
} from "openclaw/plugin-sdk/phone-control";
type ArmGroup = "camera" | "screen" | "writes" | "all";
@@ -283,139 +287,144 @@ function formatStatus(state: ArmStateFile | null): string {
return `Phone control: armed (${until}).\nTemporarily allowed: ${cmdLabel}`;
}
export default function register(api: OpenClawPluginApi) {
let expiryInterval: ReturnType<typeof setInterval> | null = null;
export default definePluginEntry({
id: "phone-control",
name: "Phone Control",
description: "Temporary allowlist control for phone automation commands",
register(api: OpenClawPluginApi) {
let expiryInterval: ReturnType<typeof setInterval> | null = null;
const timerService: OpenClawPluginService = {
id: "phone-control-expiry",
start: async (ctx) => {
const statePath = resolveStatePath(ctx.stateDir);
const tick = async () => {
const state = await readArmState(statePath);
if (!state || state.expiresAtMs == null) {
return;
}
if (Date.now() < state.expiresAtMs) {
return;
}
await disarmNow({
api,
stateDir: ctx.stateDir,
statePath,
reason: "expired",
});
};
// Best effort; don't crash the gateway if state is corrupt.
await tick().catch(() => {});
expiryInterval = setInterval(() => {
tick().catch(() => {});
}, 15_000);
expiryInterval.unref?.();
return;
},
stop: async () => {
if (expiryInterval) {
clearInterval(expiryInterval);
expiryInterval = null;
}
return;
},
};
api.registerService(timerService);
api.registerCommand({
name: "phone",
description: "Arm/disarm high-risk phone node commands (camera/screen/writes).",
acceptsArgs: true,
handler: async (ctx) => {
const args = ctx.args?.trim() ?? "";
const tokens = args.split(/\s+/).filter(Boolean);
const action = tokens[0]?.toLowerCase() ?? "";
const stateDir = api.runtime.state.resolveStateDir();
const statePath = resolveStatePath(stateDir);
if (!action || action === "help") {
const state = await readArmState(statePath);
return { text: `${formatStatus(state)}\n\n${formatHelp()}` };
}
if (action === "status") {
const state = await readArmState(statePath);
return { text: formatStatus(state) };
}
if (action === "disarm") {
const res = await disarmNow({
api,
stateDir,
statePath,
reason: "manual",
});
if (!res.changed) {
return { text: "Phone control: disarmed." };
}
const restoredLabel = res.restored.length > 0 ? res.restored.join(", ") : "none";
const removedLabel = res.removed.length > 0 ? res.removed.join(", ") : "none";
return {
text: `Phone control: disarmed.\nRemoved allowlist: ${removedLabel}\nRestored denylist: ${restoredLabel}`,
};
}
if (action === "arm") {
const group = parseGroup(tokens[1]);
if (!group) {
return { text: `Usage: /phone arm <group> [duration]\nGroups: ${formatGroupList()}` };
}
const durationMs = parseDurationMs(tokens[2]) ?? 10 * 60_000;
const expiresAtMs = Date.now() + durationMs;
const commands = resolveCommandsForGroup(group);
const cfg = api.runtime.config.loadConfig();
const allowSet = new Set(normalizeAllowList(cfg));
const denySet = new Set(normalizeDenyList(cfg));
const addedToAllow: string[] = [];
const removedFromDeny: string[] = [];
for (const cmd of commands) {
if (!allowSet.has(cmd)) {
allowSet.add(cmd);
addedToAllow.push(cmd);
const timerService: OpenClawPluginService = {
id: "phone-control-expiry",
start: async (ctx) => {
const statePath = resolveStatePath(ctx.stateDir);
const tick = async () => {
const state = await readArmState(statePath);
if (!state || state.expiresAtMs == null) {
return;
}
if (denySet.delete(cmd)) {
removedFromDeny.push(cmd);
if (Date.now() < state.expiresAtMs) {
return;
}
}
const next = patchConfigNodeLists(cfg, {
allowCommands: uniqSorted([...allowSet]),
denyCommands: uniqSorted([...denySet]),
});
await api.runtime.config.writeConfigFile(next);
await writeArmState(statePath, {
version: STATE_VERSION,
armedAtMs: Date.now(),
expiresAtMs,
group,
armedCommands: uniqSorted(commands),
addedToAllow: uniqSorted(addedToAllow),
removedFromDeny: uniqSorted(removedFromDeny),
});
const allowedLabel = uniqSorted(commands).join(", ");
return {
text:
`Phone control: armed for ${formatDuration(durationMs)}.\n` +
`Temporarily allowed: ${allowedLabel}\n` +
`To disarm early: /phone disarm`,
await disarmNow({
api,
stateDir: ctx.stateDir,
statePath,
reason: "expired",
});
};
}
return { text: formatHelp() };
},
});
}
// Best effort; don't crash the gateway if state is corrupt.
await tick().catch(() => {});
expiryInterval = setInterval(() => {
tick().catch(() => {});
}, 15_000);
expiryInterval.unref?.();
return;
},
stop: async () => {
if (expiryInterval) {
clearInterval(expiryInterval);
expiryInterval = null;
}
return;
},
};
api.registerService(timerService);
api.registerCommand({
name: "phone",
description: "Arm/disarm high-risk phone node commands (camera/screen/writes).",
acceptsArgs: true,
handler: async (ctx) => {
const args = ctx.args?.trim() ?? "";
const tokens = args.split(/\s+/).filter(Boolean);
const action = tokens[0]?.toLowerCase() ?? "";
const stateDir = api.runtime.state.resolveStateDir();
const statePath = resolveStatePath(stateDir);
if (!action || action === "help") {
const state = await readArmState(statePath);
return { text: `${formatStatus(state)}\n\n${formatHelp()}` };
}
if (action === "status") {
const state = await readArmState(statePath);
return { text: formatStatus(state) };
}
if (action === "disarm") {
const res = await disarmNow({
api,
stateDir,
statePath,
reason: "manual",
});
if (!res.changed) {
return { text: "Phone control: disarmed." };
}
const restoredLabel = res.restored.length > 0 ? res.restored.join(", ") : "none";
const removedLabel = res.removed.length > 0 ? res.removed.join(", ") : "none";
return {
text: `Phone control: disarmed.\nRemoved allowlist: ${removedLabel}\nRestored denylist: ${restoredLabel}`,
};
}
if (action === "arm") {
const group = parseGroup(tokens[1]);
if (!group) {
return { text: `Usage: /phone arm <group> [duration]\nGroups: ${formatGroupList()}` };
}
const durationMs = parseDurationMs(tokens[2]) ?? 10 * 60_000;
const expiresAtMs = Date.now() + durationMs;
const commands = resolveCommandsForGroup(group);
const cfg = api.runtime.config.loadConfig();
const allowSet = new Set(normalizeAllowList(cfg));
const denySet = new Set(normalizeDenyList(cfg));
const addedToAllow: string[] = [];
const removedFromDeny: string[] = [];
for (const cmd of commands) {
if (!allowSet.has(cmd)) {
allowSet.add(cmd);
addedToAllow.push(cmd);
}
if (denySet.delete(cmd)) {
removedFromDeny.push(cmd);
}
}
const next = patchConfigNodeLists(cfg, {
allowCommands: uniqSorted([...allowSet]),
denyCommands: uniqSorted([...denySet]),
});
await api.runtime.config.writeConfigFile(next);
await writeArmState(statePath, {
version: STATE_VERSION,
armedAtMs: Date.now(),
expiresAtMs,
group,
armedCommands: uniqSorted(commands),
addedToAllow: uniqSorted(addedToAllow),
removedFromDeny: uniqSorted(removedFromDeny),
});
const allowedLabel = uniqSorted(commands).join(", ");
return {
text:
`Phone control: armed for ${formatDuration(durationMs)}.\n` +
`Temporarily allowed: ${allowedLabel}\n` +
`To disarm early: /phone disarm`,
};
}
return { text: formatHelp() };
},
});
},
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { applyQianfanConfig, QIANFAN_DEFAULT_MODEL_REF } from "./onboard.js";
@@ -6,12 +6,11 @@ import { buildQianfanProvider } from "./provider-catalog.js";
const PROVIDER_ID = "qianfan";
const qianfanPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Qianfan Provider",
description: "Bundled Qianfan provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Qianfan",
@@ -50,6 +49,4 @@ const qianfanPlugin = {
},
});
},
};
export default qianfanPlugin;
});

View File

@@ -2,8 +2,7 @@ import { ensureAuthProfileStore, listProfilesForProvider } from "openclaw/plugin
import { QWEN_OAUTH_MARKER } from "openclaw/plugin-sdk/agent-runtime";
import {
buildOauthProviderAuthResult,
emptyPluginConfigSchema,
type OpenClawPluginApi,
definePluginEntry,
type ProviderAuthContext,
type ProviderCatalogContext,
} from "openclaw/plugin-sdk/qwen-portal-auth";
@@ -55,12 +54,11 @@ function resolveCatalog(ctx: ProviderCatalogContext) {
};
}
const qwenPortalPlugin = {
export default definePluginEntry({
id: "qwen-portal-auth",
name: "Qwen OAuth",
description: "OAuth flow for Qwen (free-tier) models",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: PROVIDER_LABEL,
@@ -146,6 +144,4 @@ const qwenPortalPlugin = {
}),
});
},
};
export default qwenPortalPlugin;
});

View File

@@ -5,7 +5,7 @@ import {
SGLANG_PROVIDER_LABEL,
} from "openclaw/plugin-sdk/agent-runtime";
import {
emptyPluginConfigSchema,
definePluginEntry,
type OpenClawPluginApi,
type ProviderAuthMethodNonInteractiveContext,
} from "openclaw/plugin-sdk/core";
@@ -16,11 +16,10 @@ async function loadProviderSetup() {
return await import("openclaw/plugin-sdk/self-hosted-provider-setup");
}
const sglangPlugin = {
export default definePluginEntry({
id: "sglang",
name: "SGLang Provider",
description: "Bundled SGLang provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
api.registerProvider({
id: PROVIDER_ID,
@@ -87,6 +86,4 @@ const sglangPlugin = {
},
});
},
};
export default sglangPlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { applySyntheticConfig, SYNTHETIC_DEFAULT_MODEL_REF } from "./onboard.js";
@@ -6,12 +6,11 @@ import { buildSyntheticProvider } from "./provider-catalog.js";
const PROVIDER_ID = "synthetic";
const syntheticPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Synthetic Provider",
description: "Bundled Synthetic provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Synthetic",
@@ -50,6 +49,4 @@ const syntheticPlugin = {
},
});
},
};
export default syntheticPlugin;
});

View File

@@ -20,7 +20,7 @@ function createHarness(config: Record<string, unknown>) {
command = definition;
}),
};
register(api as never);
register.register(api as never);
if (!command) {
throw new Error("talk-voice command not registered");
}

View File

@@ -1,6 +1,6 @@
import { resolveActiveTalkProviderConfig } from "openclaw/plugin-sdk/config-runtime";
import type { SpeechVoiceOption } from "openclaw/plugin-sdk/speech";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/talk-voice";
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/talk-voice";
function mask(s: string, keep: number = 6): string {
const trimmed = s.trim();
@@ -99,119 +99,124 @@ function asProviderBaseUrl(value: unknown): string | undefined {
return trimmed || undefined;
}
export default function register(api: OpenClawPluginApi) {
api.registerCommand({
name: "voice",
nativeNames: {
discord: "talkvoice",
},
description: "List/set Talk provider voices (affects iOS Talk playback).",
acceptsArgs: true,
handler: async (ctx) => {
const commandLabel = resolveCommandLabel(ctx.channel);
const args = ctx.args?.trim() ?? "";
const tokens = args.split(/\s+/).filter(Boolean);
const action = (tokens[0] ?? "status").toLowerCase();
export default definePluginEntry({
id: "talk-voice",
name: "Talk Voice",
description: "Command helpers for managing Talk voice configuration",
register(api: OpenClawPluginApi) {
api.registerCommand({
name: "voice",
nativeNames: {
discord: "talkvoice",
},
description: "List/set Talk provider voices (affects iOS Talk playback).",
acceptsArgs: true,
handler: async (ctx) => {
const commandLabel = resolveCommandLabel(ctx.channel);
const args = ctx.args?.trim() ?? "";
const tokens = args.split(/\s+/).filter(Boolean);
const action = (tokens[0] ?? "status").toLowerCase();
const cfg = api.runtime.config.loadConfig();
const active = resolveActiveTalkProviderConfig(cfg.talk);
if (!active) {
return {
text:
"Talk voice is not configured.\n\n" +
"Missing: talk.provider and talk.providers.<provider>.\n" +
"Set it on the gateway, then retry.",
};
}
const providerId = active.provider;
const providerLabel = resolveProviderLabel(providerId);
const apiKey = asTrimmedString(active.config.apiKey);
const baseUrl = asProviderBaseUrl(active.config.baseUrl);
const currentVoiceId =
asTrimmedString(active.config.voiceId) || asTrimmedString(cfg.talk?.voiceId);
if (action === "status") {
return {
text:
"Talk voice status:\n" +
`- provider: ${providerId}\n` +
`- talk.voiceId: ${currentVoiceId ? currentVoiceId : "(unset)"}\n` +
`- ${providerId}.apiKey: ${apiKey ? mask(apiKey) : "(unset)"}`,
};
}
if (action === "list") {
const limit = Number.parseInt(tokens[1] ?? "12", 10);
try {
const voices = await api.runtime.tts.listVoices({
provider: providerId,
cfg,
apiKey: apiKey || undefined,
baseUrl,
});
const cfg = api.runtime.config.loadConfig();
const active = resolveActiveTalkProviderConfig(cfg.talk);
if (!active) {
return {
text: formatVoiceList(voices, Number.isFinite(limit) ? limit : 12, providerId),
text:
"Talk voice is not configured.\n\n" +
"Missing: talk.provider and talk.providers.<provider>.\n" +
"Set it on the gateway, then retry.",
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { text: `${providerLabel} voice list failed: ${message}` };
}
}
const providerId = active.provider;
const providerLabel = resolveProviderLabel(providerId);
const apiKey = asTrimmedString(active.config.apiKey);
const baseUrl = asProviderBaseUrl(active.config.baseUrl);
if (action === "set") {
const query = tokens.slice(1).join(" ").trim();
if (!query) {
return { text: `Usage: ${commandLabel} set <voiceId|name>` };
}
let voices: SpeechVoiceOption[];
try {
voices = await api.runtime.tts.listVoices({
provider: providerId,
cfg,
apiKey: apiKey || undefined,
baseUrl,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { text: `${providerLabel} voice lookup failed: ${message}` };
}
const chosen = findVoice(voices, query);
if (!chosen) {
const hint = isLikelyVoiceId(query) ? query : `"${query}"`;
return { text: `No voice found for ${hint}. Try: ${commandLabel} list` };
const currentVoiceId =
asTrimmedString(active.config.voiceId) || asTrimmedString(cfg.talk?.voiceId);
if (action === "status") {
return {
text:
"Talk voice status:\n" +
`- provider: ${providerId}\n` +
`- talk.voiceId: ${currentVoiceId ? currentVoiceId : "(unset)"}\n` +
`- ${providerId}.apiKey: ${apiKey ? mask(apiKey) : "(unset)"}`,
};
}
const nextConfig = {
...cfg,
talk: {
...cfg.talk,
provider: providerId,
providers: {
...(cfg.talk?.providers ?? {}),
[providerId]: {
...(cfg.talk?.providers?.[providerId] ?? {}),
voiceId: chosen.id,
if (action === "list") {
const limit = Number.parseInt(tokens[1] ?? "12", 10);
try {
const voices = await api.runtime.tts.listVoices({
provider: providerId,
cfg,
apiKey: apiKey || undefined,
baseUrl,
});
return {
text: formatVoiceList(voices, Number.isFinite(limit) ? limit : 12, providerId),
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { text: `${providerLabel} voice list failed: ${message}` };
}
}
if (action === "set") {
const query = tokens.slice(1).join(" ").trim();
if (!query) {
return { text: `Usage: ${commandLabel} set <voiceId|name>` };
}
let voices: SpeechVoiceOption[];
try {
voices = await api.runtime.tts.listVoices({
provider: providerId,
cfg,
apiKey: apiKey || undefined,
baseUrl,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { text: `${providerLabel} voice lookup failed: ${message}` };
}
const chosen = findVoice(voices, query);
if (!chosen) {
const hint = isLikelyVoiceId(query) ? query : `"${query}"`;
return { text: `No voice found for ${hint}. Try: ${commandLabel} list` };
}
const nextConfig = {
...cfg,
talk: {
...cfg.talk,
provider: providerId,
providers: {
...(cfg.talk?.providers ?? {}),
[providerId]: {
...(cfg.talk?.providers?.[providerId] ?? {}),
voiceId: chosen.id,
},
},
...(providerId === "elevenlabs" ? { voiceId: chosen.id } : {}),
},
...(providerId === "elevenlabs" ? { voiceId: chosen.id } : {}),
},
};
await api.runtime.config.writeConfigFile(nextConfig);
const name = (chosen.name ?? "").trim() || "(unnamed)";
return { text: `${providerLabel} Talk voice set to ${name}\n${chosen.id}` };
}
return {
text: [
"Voice commands:",
"",
`${commandLabel} status`,
`${commandLabel} list [limit]`,
`${commandLabel} set <voiceId|name>`,
].join("\n"),
};
await api.runtime.config.writeConfigFile(nextConfig);
const name = (chosen.name ?? "").trim() || "(unnamed)";
return { text: `${providerLabel} Talk voice set to ${name}\n${chosen.id}` };
}
return {
text: [
"Voice commands:",
"",
`${commandLabel} status`,
`${commandLabel} list [limit]`,
`${commandLabel} set <voiceId|name>`,
].join("\n"),
};
},
});
}
},
});
},
});

View File

@@ -39,7 +39,7 @@ describe("thread-ownership plugin", () => {
});
it("registers message_received and message_sending hooks", () => {
register(api as any);
register.register(api as any);
expect(api.on).toHaveBeenCalledTimes(2);
expect(api.on).toHaveBeenCalledWith("message_received", expect.any(Function));
@@ -48,7 +48,7 @@ describe("thread-ownership plugin", () => {
describe("message_sending", () => {
beforeEach(() => {
register(api as any);
register.register(api as any);
});
async function sendSlackThreadMessage() {
@@ -120,7 +120,7 @@ describe("thread-ownership plugin", () => {
describe("message_received @-mention tracking", () => {
beforeEach(() => {
register(api as any);
register.register(api as any);
});
it("tracks @-mentions and skips ownership check for mentioned threads", async () => {

View File

@@ -1,4 +1,8 @@
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/thread-ownership";
import {
definePluginEntry,
type OpenClawConfig,
type OpenClawPluginApi,
} from "openclaw/plugin-sdk/thread-ownership";
type ThreadOwnershipConfig = {
forwarderUrl?: string;
@@ -39,95 +43,79 @@ function resolveOwnershipAgent(config: OpenClawConfig): { id: string; name: stri
return { id, name };
}
export default function register(api: OpenClawPluginApi) {
const pluginCfg = (api.pluginConfig ?? {}) as ThreadOwnershipConfig;
const forwarderUrl = (
pluginCfg.forwarderUrl ??
process.env.SLACK_FORWARDER_URL ??
"http://slack-forwarder:8750"
).replace(/\/$/, "");
export default definePluginEntry({
id: "thread-ownership",
name: "Thread Ownership",
description: "Slack thread claim coordination for multi-agent setups",
register(api: OpenClawPluginApi) {
const pluginCfg = (api.pluginConfig ?? {}) as ThreadOwnershipConfig;
const forwarderUrl = (
pluginCfg.forwarderUrl ??
process.env.SLACK_FORWARDER_URL ??
"http://slack-forwarder:8750"
).replace(/\/$/, "");
const abTestChannels = new Set(
pluginCfg.abTestChannels ??
process.env.THREAD_OWNERSHIP_CHANNELS?.split(",").filter(Boolean) ??
[],
);
const abTestChannels = new Set(
pluginCfg.abTestChannels ??
process.env.THREAD_OWNERSHIP_CHANNELS?.split(",").filter(Boolean) ??
[],
);
const { id: agentId, name: agentName } = resolveOwnershipAgent(api.config);
const botUserId = process.env.SLACK_BOT_USER_ID ?? "";
const { id: agentId, name: agentName } = resolveOwnershipAgent(api.config);
const botUserId = process.env.SLACK_BOT_USER_ID ?? "";
// ---------------------------------------------------------------------------
// message_received: track @-mentions so the agent can reply even if it
// doesn't own the thread.
// ---------------------------------------------------------------------------
api.on("message_received", async (event, ctx) => {
if (ctx.channelId !== "slack") return;
api.on("message_received", async (event, ctx) => {
if (ctx.channelId !== "slack") return;
const text = event.content ?? "";
const threadTs = (event.metadata?.threadTs as string) ?? "";
const channelId = (event.metadata?.channelId as string) ?? ctx.conversationId ?? "";
const text = event.content ?? "";
const threadTs = (event.metadata?.threadTs as string) ?? "";
const channelId = (event.metadata?.channelId as string) ?? ctx.conversationId ?? "";
if (!threadTs || !channelId) return;
if (!threadTs || !channelId) return;
const mentioned =
(agentName && text.includes(`@${agentName}`)) ||
(botUserId && text.includes(`<@${botUserId}>`));
if (mentioned) {
cleanExpiredMentions();
mentionedThreads.set(`${channelId}:${threadTs}`, Date.now());
}
});
// Check if this agent was @-mentioned.
const mentioned =
(agentName && text.includes(`@${agentName}`)) ||
(botUserId && text.includes(`<@${botUserId}>`));
api.on("message_sending", async (event, ctx) => {
if (ctx.channelId !== "slack") return;
const threadTs = (event.metadata?.threadTs as string) ?? "";
const channelId = (event.metadata?.channelId as string) ?? event.to;
if (!threadTs) return;
if (abTestChannels.size > 0 && !abTestChannels.has(channelId)) return;
if (mentioned) {
cleanExpiredMentions();
mentionedThreads.set(`${channelId}:${threadTs}`, Date.now());
}
});
if (mentionedThreads.has(`${channelId}:${threadTs}`)) return;
// ---------------------------------------------------------------------------
// message_sending: check thread ownership before sending to Slack.
// Returns { cancel: true } if another agent owns the thread.
// ---------------------------------------------------------------------------
api.on("message_sending", async (event, ctx) => {
if (ctx.channelId !== "slack") return;
try {
const resp = await fetch(`${forwarderUrl}/api/v1/ownership/${channelId}/${threadTs}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent_id: agentId }),
signal: AbortSignal.timeout(3000),
});
const threadTs = (event.metadata?.threadTs as string) ?? "";
const channelId = (event.metadata?.channelId as string) ?? event.to;
// Top-level messages (no thread) are always allowed.
if (!threadTs) return;
// Only enforce in A/B test channels (if set is empty, skip entirely).
if (abTestChannels.size > 0 && !abTestChannels.has(channelId)) return;
// If this agent was @-mentioned in this thread recently, skip ownership check.
cleanExpiredMentions();
if (mentionedThreads.has(`${channelId}:${threadTs}`)) return;
// Try to claim ownership via the forwarder HTTP API.
try {
const resp = await fetch(`${forwarderUrl}/api/v1/ownership/${channelId}/${threadTs}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent_id: agentId }),
signal: AbortSignal.timeout(3000),
});
if (resp.ok) {
// We own it (or just claimed it), proceed.
return;
}
if (resp.status === 409) {
// Another agent owns this thread — cancel the send.
const body = (await resp.json()) as { owner?: string };
api.logger.info?.(
`thread-ownership: cancelled send to ${channelId}:${threadTs} — owned by ${body.owner}`,
if (resp.ok) {
return;
}
if (resp.status === 409) {
const body = (await resp.json()) as { owner?: string };
api.logger.info?.(
`thread-ownership: cancelled send to ${channelId}:${threadTs} — owned by ${body.owner}`,
);
return { cancel: true };
}
api.logger.warn?.(`thread-ownership: unexpected status ${resp.status}, allowing send`);
} catch (err) {
api.logger.warn?.(
`thread-ownership: ownership check failed (${String(err)}), allowing send`,
);
return { cancel: true };
}
// Unexpected status — fail open.
api.logger.warn?.(`thread-ownership: unexpected status ${resp.status}, allowing send`);
} catch (err) {
// Network error — fail open.
api.logger.warn?.(`thread-ownership: ownership check failed (${String(err)}), allowing send`);
}
});
}
});
},
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { applyTogetherConfig, TOGETHER_DEFAULT_MODEL_REF } from "./onboard.js";
@@ -6,12 +6,11 @@ import { buildTogetherProvider } from "./provider-catalog.js";
const PROVIDER_ID = "together";
const togetherPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Together Provider",
description: "Bundled Together provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Together",
@@ -50,6 +49,4 @@ const togetherPlugin = {
},
});
},
};
export default togetherPlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { applyVeniceConfig, VENICE_DEFAULT_MODEL_REF } from "./onboard.js";
@@ -6,12 +6,11 @@ import { buildVeniceProvider } from "./provider-catalog.js";
const PROVIDER_ID = "venice";
const venicePlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Venice Provider",
description: "Bundled Venice provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Venice",
@@ -56,6 +55,4 @@ const venicePlugin = {
},
});
},
};
export default venicePlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { applyVercelAiGatewayConfig, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF } from "./onboard.js";
@@ -6,12 +6,11 @@ import { buildVercelAiGatewayProvider } from "./provider-catalog.js";
const PROVIDER_ID = "vercel-ai-gateway";
const vercelAiGatewayPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Vercel AI Gateway Provider",
description: "Bundled Vercel AI Gateway provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Vercel AI Gateway",
@@ -50,6 +49,4 @@ const vercelAiGatewayPlugin = {
},
});
},
};
export default vercelAiGatewayPlugin;
});

View File

@@ -5,7 +5,7 @@ import {
VLLM_PROVIDER_LABEL,
} from "openclaw/plugin-sdk/agent-runtime";
import {
emptyPluginConfigSchema,
definePluginEntry,
type OpenClawPluginApi,
type ProviderAuthMethodNonInteractiveContext,
} from "openclaw/plugin-sdk/core";
@@ -16,11 +16,10 @@ async function loadProviderSetup() {
return await import("openclaw/plugin-sdk/self-hosted-provider-setup");
}
const vllmPlugin = {
export default definePluginEntry({
id: "vllm",
name: "vLLM Provider",
description: "Bundled vLLM provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
api.registerProvider({
id: PROVIDER_ID,
@@ -87,6 +86,4 @@ const vllmPlugin = {
},
});
},
};
export default vllmPlugin;
});

View File

@@ -1,7 +1,8 @@
import { Type } from "@sinclair/typebox";
import type {
GatewayRequestHandlerOptions,
OpenClawPluginApi,
import {
definePluginEntry,
type GatewayRequestHandlerOptions,
type OpenClawPluginApi,
} from "openclaw/plugin-sdk/voice-call";
import { registerVoiceCallCli } from "./src/cli.js";
import {
@@ -143,7 +144,7 @@ const VoiceCallToolSchema = Type.Union([
}),
]);
const voiceCallPlugin = {
export default definePluginEntry({
id: "voice-call",
name: "Voice Call",
description: "Voice-call plugin with Telnyx/Twilio/Plivo providers",
@@ -560,6 +561,4 @@ const voiceCallPlugin = {
},
});
},
};
export default voiceCallPlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { ensureModelAllowlistEntry } from "openclaw/plugin-sdk/provider-onboard";
import { buildDoubaoCodingProvider, buildDoubaoProvider } from "./provider-catalog.js";
@@ -6,12 +6,11 @@ import { buildDoubaoCodingProvider, buildDoubaoProvider } from "./provider-catal
const PROVIDER_ID = "volcengine";
const VOLCENGINE_DEFAULT_MODEL_REF = "volcengine-plan/ark-code-latest";
const volcenginePlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Volcengine Provider",
description: "Bundled Volcengine provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Volcengine",
@@ -60,6 +59,4 @@ const volcenginePlugin = {
},
});
},
};
export default volcenginePlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-models";
import {
@@ -16,12 +16,11 @@ function matchesModernXaiModel(modelId: string): boolean {
return XAI_MODERN_MODEL_PREFIXES.some((prefix) => normalized.startsWith(prefix));
}
const xaiPlugin = {
export default definePluginEntry({
id: "xai",
name: "xAI Plugin",
description: "Bundled xAI plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "xAI",
@@ -68,6 +67,4 @@ const xaiPlugin = {
}),
);
},
};
export default xaiPlugin;
});

View File

@@ -1,4 +1,4 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth";
import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog";
import { PROVIDER_LABELS } from "openclaw/plugin-sdk/provider-usage";
@@ -7,12 +7,11 @@ import { buildXiaomiProvider } from "./provider-catalog.js";
const PROVIDER_ID = "xiaomi";
const xiaomiPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Xiaomi Provider",
description: "Bundled Xiaomi provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Xiaomi",
@@ -62,6 +61,4 @@ const xiaomiPlugin = {
}),
});
},
};
export default xiaomiPlugin;
});

View File

@@ -1,6 +1,5 @@
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
definePluginEntry,
type ProviderAuthContext,
type ProviderAuthMethod,
type ProviderAuthMethodNonInteractiveContext,
@@ -226,12 +225,11 @@ function buildZaiApiKeyMethod(params: {
};
}
const zaiPlugin = {
export default definePluginEntry({
id: PROVIDER_ID,
name: "Z.AI Provider",
description: "Bundled Z.AI provider plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Z.AI",
@@ -311,6 +309,4 @@ const zaiPlugin = {
});
api.registerMediaUnderstandingProvider(zaiMediaUnderstandingProvider);
},
};
export default zaiPlugin;
});

View File

@@ -113,7 +113,7 @@ export async function resolveSharedMemoryStatusSnapshot(params: {
purpose: "status";
}) => Promise<{
manager: {
probeVectorAvailability(): Promise<void>;
probeVectorAvailability(): Promise<boolean>;
status(): MemoryProviderStatus;
close?(): Promise<void>;
} | null;

View File

@@ -1,4 +1,5 @@
type JsonSchemaObject = {
type?: string | string[];
properties?: Record<string, JsonSchemaObject>;
additionalProperties?: JsonSchemaObject | boolean;
items?: JsonSchemaObject | JsonSchemaObject[];

View File

@@ -1,7 +1,7 @@
// Narrow plugin-sdk surface for the bundled copilot-proxy plugin.
// Keep this list additive and scoped to symbols used under extensions/copilot-proxy.
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { definePluginEntry } from "./core.js";
export type {
OpenClawPluginApi,
ProviderAuthContext,

View File

@@ -5,6 +5,7 @@ import type {
OpenClawPluginApi,
OpenClawPluginCommandDefinition,
OpenClawPluginConfigSchema,
OpenClawPluginDefinition,
PluginInteractiveTelegramHandlerContext,
} from "../plugins/types.js";
@@ -42,6 +43,7 @@ export type {
ProviderAuthMethod,
ProviderAuthResult,
OpenClawPluginCommandDefinition,
OpenClawPluginDefinition,
PluginInteractiveTelegramHandlerContext,
} from "../plugins/types.js";
export type { OpenClawConfig } from "../config/config.js";
@@ -88,11 +90,53 @@ type DefineChannelPluginEntryOptions<TPlugin extends ChannelPlugin = ChannelPlug
name: string;
description: string;
plugin: TPlugin;
configSchema?: () => OpenClawPluginConfigSchema;
configSchema?: DefinePluginEntryOptions["configSchema"];
setRuntime?: (runtime: PluginRuntime) => void;
registerFull?: (api: OpenClawPluginApi) => void;
};
type DefinePluginEntryOptions = {
id: string;
name: string;
description: string;
kind?: OpenClawPluginDefinition["kind"];
configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema);
register: (api: OpenClawPluginApi) => void;
};
type DefinedPluginEntry = {
id: string;
name: string;
description: string;
configSchema: OpenClawPluginConfigSchema;
register: NonNullable<OpenClawPluginDefinition["register"]>;
} & Pick<OpenClawPluginDefinition, "kind">;
function resolvePluginConfigSchema(
configSchema: DefinePluginEntryOptions["configSchema"] = emptyPluginConfigSchema,
): OpenClawPluginConfigSchema {
return typeof configSchema === "function" ? configSchema() : configSchema;
}
// Shared generic plugin-entry boilerplate for bundled and third-party plugins.
export function definePluginEntry({
id,
name,
description,
kind,
configSchema = emptyPluginConfigSchema,
register,
}: DefinePluginEntryOptions): DefinedPluginEntry {
return {
id,
name,
description,
...(kind ? { kind } : {}),
configSchema: resolvePluginConfigSchema(configSchema),
register,
};
}
// Shared channel-plugin entry boilerplate for bundled and third-party channels.
export function defineChannelPluginEntry<TPlugin extends ChannelPlugin>({
id,
@@ -103,11 +147,11 @@ export function defineChannelPluginEntry<TPlugin extends ChannelPlugin>({
setRuntime,
registerFull,
}: DefineChannelPluginEntryOptions<TPlugin>) {
return {
return definePluginEntry({
id,
name,
description,
configSchema: configSchema(),
configSchema,
register(api: OpenClawPluginApi) {
setRuntime?.(api.runtime);
api.registerChannel({ plugin });
@@ -116,7 +160,7 @@ export function defineChannelPluginEntry<TPlugin extends ChannelPlugin>({
}
registerFull?.(api);
},
};
});
}
// Shared setup-entry shape so bundled channels do not duplicate `{ plugin }`.

View File

@@ -1,6 +1,7 @@
// Narrow plugin-sdk surface for the bundled device-pair plugin.
// Keep this list additive and scoped to symbols used under extensions/device-pair.
export { definePluginEntry } from "./core.js";
export { approveDevicePairing, listDevicePairing } from "../infra/device-pairing.js";
export { issueDeviceBootstrapToken } from "../infra/device-bootstrap.js";
export type { OpenClawPluginApi } from "../plugins/types.js";

View File

@@ -1,6 +1,7 @@
// Narrow plugin-sdk surface for the bundled llm-task plugin.
// Keep this list additive and scoped to symbols used under extensions/llm-task.
export { definePluginEntry } from "./core.js";
export { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
export {
formatThinkingLevels,

View File

@@ -1,6 +1,7 @@
// Narrow plugin-sdk surface for the bundled lobster plugin.
// Keep this list additive and scoped to symbols used under extensions/lobster.
export { definePluginEntry } from "./core.js";
export {
applyWindowsSpawnProgramPolicy,
materializeWindowsSpawnProgram,

View File

@@ -1,4 +1,5 @@
// Narrow plugin-sdk surface for the bundled memory-lancedb plugin.
// Keep this list additive and scoped to symbols used under extensions/memory-lancedb.
export { definePluginEntry } from "./core.js";
export type { OpenClawPluginApi } from "../plugins/types.js";

View File

@@ -1,7 +1,7 @@
// Narrow plugin-sdk surface for MiniMax OAuth helpers used by the bundled minimax plugin.
// Keep this list additive and scoped to MiniMax OAuth support code.
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { definePluginEntry } from "./core.js";
export { buildOauthProviderAuthResult } from "./provider-auth-result.js";
export type {
OpenClawPluginApi,

View File

@@ -1,4 +1,5 @@
// Narrow plugin-sdk surface for the bundled open-prose plugin.
// Keep this list additive and scoped to symbols used under extensions/open-prose.
export { definePluginEntry } from "./core.js";
export type { OpenClawPluginApi } from "../plugins/types.js";

View File

@@ -1,6 +1,7 @@
// Narrow plugin-sdk surface for the bundled phone-control plugin.
// Keep this list additive and scoped to symbols used under extensions/phone-control.
export { definePluginEntry } from "./core.js";
export type {
OpenClawPluginApi,
OpenClawPluginCommandDefinition,

View File

@@ -1,7 +1,7 @@
// Narrow plugin-sdk surface for the bundled qwen-portal-auth plugin.
// Keep this list additive and scoped to symbols used under extensions/qwen-portal-auth.
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { definePluginEntry } from "./core.js";
export { buildOauthProviderAuthResult } from "./provider-auth-result.js";
export type {
OpenClawPluginApi,

View File

@@ -49,6 +49,7 @@ describe("plugin-sdk subpath exports", () => {
it("keeps core focused on generic shared exports", () => {
expect(typeof coreSdk.emptyPluginConfigSchema).toBe("function");
expect(typeof coreSdk.definePluginEntry).toBe("function");
expect(typeof coreSdk.defineChannelPluginEntry).toBe("function");
expect(typeof coreSdk.defineSetupPluginEntry).toBe("function");
expect("runPassiveAccountLifecycle" in asExports(coreSdk)).toBe(false);

View File

@@ -1,4 +1,5 @@
// Narrow plugin-sdk surface for the bundled talk-voice plugin.
// Keep this list additive and scoped to symbols used under extensions/talk-voice.
export { definePluginEntry } from "./core.js";
export type { OpenClawPluginApi } from "../plugins/types.js";

View File

@@ -1,5 +1,6 @@
// Narrow plugin-sdk surface for the bundled thread-ownership plugin.
// Keep this list additive and scoped to symbols used under extensions/thread-ownership.
export { definePluginEntry } from "./core.js";
export type { OpenClawConfig } from "../config/config.js";
export type { OpenClawPluginApi } from "../plugins/types.js";

View File

@@ -1,6 +1,7 @@
// Narrow plugin-sdk surface for the bundled voice-call plugin.
// Keep this list additive and scoped to symbols used under extensions/voice-call.
export { definePluginEntry } from "./core.js";
export {
TtsAutoSchema,
TtsConfigSchema,

View File

@@ -45,7 +45,7 @@ type RegisterCliContext = {
function setup(config: Record<string, unknown>): Registered {
const methods = new Map<string, unknown>();
const tools: unknown[] = [];
plugin.register({
void plugin.register({
id: "voice-call",
name: "Voice Call",
description: "test",