diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bdf183e0c9..9b9038a26d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -271,6 +271,7 @@ Docs: https://docs.openclaw.ai - Daemon/launchd: keep `openclaw gateway stop` persistent without uninstalling the macOS LaunchAgent, re-enable it on explicit restart or repair, and harden launchd label handling. (#64447) Thanks @ngutman. - Plugins/context engines: preserve `plugins.slots.contextEngine` through normalization and keep explicitly selected workspace context-engine plugins enabled, so loader diagnostics and plugin activation stop dropping that slot selection. (#64192) Thanks @hclsys. - Heartbeat: stop top-level `interval:` and `prompt:` fields outside the `tasks:` block from bleeding into the last parsed heartbeat task. (#64488) Thanks @Rahulkumar070. +- Slack/plugin commands: include plugin-registered slash commands in Slack native command registration when Slack native commands are enabled. (#64578) Thanks @rafaelreis-r. - Agents/OpenAI replay: preserve malformed function-call arguments in stored assistant history, avoid double-encoding preserved raw strings on replay, and coerce replayed string args back to objects at Anthropic and Google provider boundaries. (#61956) Thanks @100yenadmin. - Heartbeat/config: accept and honor `agents.defaults.heartbeat.timeoutSeconds` and per-agent heartbeat timeout overrides for heartbeat agent turns. (#64491) Thanks @cedillarack. - CLI/devices: make implicit `openclaw devices approve` selection preview-only and require approving the exact request ID, preventing latest-request races during device pairing. (#64160) Thanks @coygeek. diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 24fb0fd41ee..b4bfcd6c5aa 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -600f05b14825fa01eb9d63ab6cab5f33c74ff44a48cab5c65457ab08e5b0e91a plugin-sdk-api-baseline.json -99d649a86a30756b18b91686f3683e6e829c5e316e1370266ec4fee344bc55cb plugin-sdk-api-baseline.jsonl +42a93d8368fd40f6bbe3045ba89b84a28e1131c700d4e57580febd3e773b23a4 plugin-sdk-api-baseline.json +515333c277b725abaccf4fd5ab8c5e58b2de39b26e1fe4738f31852fcf789c96 plugin-sdk-api-baseline.jsonl diff --git a/extensions/slack/src/monitor/slash.ts b/extensions/slack/src/monitor/slash.ts index 95de43f0e81..aef23711058 100644 --- a/extensions/slack/src/monitor/slash.ts +++ b/extensions/slack/src/monitor/slash.ts @@ -3,7 +3,7 @@ import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pi import { resolveCommandAuthorizedFromAuthorizers, resolveNativeCommandSessionTargets, - getPluginCommandSpecs, + listProviderPluginCommandSpecs, } from "openclaw/plugin-sdk/command-auth"; import { type ChatCommandDefinition, type CommandArgs } from "openclaw/plugin-sdk/command-auth"; import { @@ -674,7 +674,7 @@ export async function registerSlackMonitorSlashCommands(params: { const existingNativeNames = new Set( nativeCommands.map((c) => normalizeLowercaseStringOrEmpty(c.name)).filter(Boolean), ); - for (const pluginCommand of getPluginCommandSpecs("slack")) { + for (const pluginCommand of listProviderPluginCommandSpecs("slack")) { const normalizedName = normalizeLowercaseStringOrEmpty(pluginCommand.name); if (!normalizedName || existingNativeNames.has(normalizedName)) { continue; diff --git a/src/plugin-sdk/command-auth.ts b/src/plugin-sdk/command-auth.ts index 25ff938aee0..2b5ef25a8d8 100644 --- a/src/plugin-sdk/command-auth.ts +++ b/src/plugin-sdk/command-auth.ts @@ -76,7 +76,10 @@ export { listSkillCommandsForWorkspace, resolveSkillCommandInvocation, } from "../auto-reply/skill-commands.js"; -export { getPluginCommandSpecs } from "../plugins/command-registration.js"; +export { + getPluginCommandSpecs, + listProviderPluginCommandSpecs, +} from "../plugins/command-registration.js"; export type { SkillCommandSpec } from "../agents/skills.js"; export { buildModelsProviderData, diff --git a/src/plugins/command-registration.ts b/src/plugins/command-registration.ts index fbeb2202644..45101a5c431 100644 --- a/src/plugins/command-registration.ts +++ b/src/plugins/command-registration.ts @@ -8,6 +8,7 @@ import { clearPluginCommandsForPlugin, getPluginCommandSpecs, isPluginCommandRegistryLocked, + listProviderPluginCommandSpecs, pluginCommands, type RegisteredPluginCommand, } from "./command-registry-state.js"; @@ -196,5 +197,10 @@ export function registerPluginCommand( return { ok: true }; } -export { clearPluginCommands, clearPluginCommandsForPlugin, getPluginCommandSpecs }; +export { + clearPluginCommands, + clearPluginCommandsForPlugin, + getPluginCommandSpecs, + listProviderPluginCommandSpecs, +}; export type { RegisteredPluginCommand }; diff --git a/src/plugins/command-registry-state.ts b/src/plugins/command-registry-state.ts index dcd2f871e90..5e2cc358838 100644 --- a/src/plugins/command-registry-state.ts +++ b/src/plugins/command-registry-state.ts @@ -73,16 +73,21 @@ export function getPluginCommandSpecs(provider?: string): Array<{ acceptsArgs: boolean; }> { const providerName = normalizeOptionalLowercaseString(provider); - if (providerName) { - const channelPlugin = getChannelPlugin(providerName); - if ( - !channelPlugin || - (!channelPlugin.capabilities?.nativeCommands && - !channelPlugin.commands?.nativeCommandsAutoEnabled) - ) { - return []; - } + if ( + providerName && + getChannelPlugin(providerName)?.commands?.nativeCommandsAutoEnabled !== true + ) { + return []; } + return listProviderPluginCommandSpecs(provider); +} + +/** Resolve plugin command specs for a provider's native naming surface without support gating. */ +export function listProviderPluginCommandSpecs(provider?: string): Array<{ + name: string; + description: string; + acceptsArgs: boolean; +}> { return Array.from(pluginCommands.values()).map((cmd) => ({ name: resolvePluginNativeName(cmd, provider), description: cmd.description, diff --git a/src/plugins/commands.test.ts b/src/plugins/commands.test.ts index a2a5e53210d..0fd2d36ad1a 100644 --- a/src/plugins/commands.test.ts +++ b/src/plugins/commands.test.ts @@ -5,6 +5,7 @@ import { clearPluginCommands, executePluginCommand, getPluginCommandSpecs, + listProviderPluginCommandSpecs, listPluginCommands, matchPluginCommand, registerPluginCommand, @@ -307,7 +308,26 @@ describe("registerPluginCommand", () => { { provider: undefined, expectedNames: ["talkvoice"] }, { provider: "discord", expectedNames: ["discordvoice"] }, { provider: "telegram", expectedNames: ["talkvoice"] }, - { provider: "slack", expectedNames: ["talkvoice"] }, + { provider: "slack", expectedNames: [] }, + ]); + }); + + it("allows Slack to resolve provider-native plugin specs without changing shared native gating", () => { + const result = registerVoiceCommandForTest({ + nativeNames: { + default: "talkvoice", + discord: "discordvoice", + }, + description: "Demo command", + }); + + expect(result).toEqual({ ok: true }); + expect(listProviderPluginCommandSpecs("slack")).toEqual([ + { + name: "talkvoice", + description: "Demo command", + acceptsArgs: false, + }, ]); }); diff --git a/src/plugins/commands.ts b/src/plugins/commands.ts index f13e0b0c427..225e1fb5ab0 100644 --- a/src/plugins/commands.ts +++ b/src/plugins/commands.ts @@ -14,6 +14,7 @@ import { clearPluginCommandsForPlugin, getPluginCommandSpecs, listPluginInvocationKeys, + listProviderPluginCommandSpecs, registerPluginCommand, validateCommandName, validatePluginCommandDefinition, @@ -42,6 +43,7 @@ export { clearPluginCommands, clearPluginCommandsForPlugin, getPluginCommandSpecs, + listProviderPluginCommandSpecs, registerPluginCommand, validateCommandName, validatePluginCommandDefinition,