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 7c8733256a9..aef23711058 100644 --- a/extensions/slack/src/monitor/slash.ts +++ b/extensions/slack/src/monitor/slash.ts @@ -3,6 +3,7 @@ import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pi import { resolveCommandAuthorizedFromAuthorizers, resolveNativeCommandSessionTargets, + listProviderPluginCommandSpecs, } from "openclaw/plugin-sdk/command-auth"; import { type ChatCommandDefinition, type CommandArgs } from "openclaw/plugin-sdk/command-auth"; import { @@ -670,6 +671,17 @@ export async function registerSlackMonitorSlashCommands(params: { skillCommands, provider: "slack", }); + const existingNativeNames = new Set( + nativeCommands.map((c) => normalizeLowercaseStringOrEmpty(c.name)).filter(Boolean), + ); + for (const pluginCommand of listProviderPluginCommandSpecs("slack")) { + const normalizedName = normalizeLowercaseStringOrEmpty(pluginCommand.name); + if (!normalizedName || existingNativeNames.has(normalizedName)) { + continue; + } + existingNativeNames.add(normalizedName); + nativeCommands.push(pluginCommand); + } } if (nativeCommands.length > 0) { diff --git a/src/plugin-sdk/command-auth.ts b/src/plugin-sdk/command-auth.ts index 6c50e38d5a1..2b5ef25a8d8 100644 --- a/src/plugin-sdk/command-auth.ts +++ b/src/plugin-sdk/command-auth.ts @@ -76,6 +76,10 @@ export { listSkillCommandsForWorkspace, resolveSkillCommandInvocation, } from "../auto-reply/skill-commands.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 2b2637ba867..5e2cc358838 100644 --- a/src/plugins/command-registry-state.ts +++ b/src/plugins/command-registry-state.ts @@ -79,6 +79,15 @@ export function getPluginCommandSpecs(provider?: string): Array<{ ) { 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 f64fedbacef..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, @@ -202,6 +203,17 @@ beforeEach(() => { }, }, }, + { + pluginId: "slack", + source: "test", + plugin: { + ...createChannelTestPluginBase({ + id: "slack", + label: "Slack", + capabilities: { nativeCommands: true, chatTypes: ["direct", "group"] }, + }), + }, + }, ]), ); }); @@ -300,6 +312,25 @@ describe("registerPluginCommand", () => { ]); }); + 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, + }, + ]); + }); + it("accepts native progress metadata on plugin commands", () => { const result = registerVoiceCommandForTest({ nativeProgressMessages: { telegram: "Running voice command..." }, 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,