diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 3c2e65617df..5ebe3c4bfa5 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -25,6 +25,7 @@ import { resolveDefaultGroupPolicy, resolveChannelMediaMaxBytes, warnMissingProviderGroupPolicyFallbackOnce, + listSkillCommandsForAgents, type HistoryEntry, } from "openclaw/plugin-sdk"; import { getMattermostRuntime } from "../runtime.js"; @@ -258,8 +259,6 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} if (slashConfig.nativeSkills === true) { try { - const { listSkillCommandsForAgents } = - await import("../../../../src/auto-reply/skill-commands.js"); const skillCommands = listSkillCommandsForAgents({ cfg: cfg as any }); for (const spec of skillCommands) { const name = typeof spec.name === "string" ? spec.name.trim() : ""; @@ -270,6 +269,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} description: spec.description || `Run skill ${name}`, autoComplete: true, autoCompleteHint: "[args]", + originalName: name, }); } } catch (err) { @@ -300,10 +300,19 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} allRegistered.push(...registered); } + // Build trigger→originalName map for accurate command name resolution + const triggerMap = new Map(); + for (const cmd of dedupedCommands) { + if (cmd.originalName) { + triggerMap.set(cmd.trigger, cmd.originalName); + } + } + activateSlashCommands({ account, commandTokens: allRegistered.map((cmd) => cmd.token).filter(Boolean), registeredCommands: allRegistered, + triggerMap, api: { cfg, runtime }, log: (msg) => runtime.log?.(msg), }); diff --git a/extensions/mattermost/src/mattermost/slash-commands.ts b/extensions/mattermost/src/mattermost/slash-commands.ts index 1882893376a..69e29f8fa6c 100644 --- a/extensions/mattermost/src/mattermost/slash-commands.ts +++ b/extensions/mattermost/src/mattermost/slash-commands.ts @@ -35,6 +35,8 @@ export type MattermostCommandSpec = { description: string; autoComplete: boolean; autoCompleteHint?: string; + /** Original command name (for skill commands that start with oc_) */ + originalName?: string; }; export type MattermostRegisteredCommand = { @@ -128,39 +130,46 @@ type MattermostCommandResponse = { export const DEFAULT_COMMAND_SPECS: MattermostCommandSpec[] = [ { trigger: "oc_status", + originalName: "status", description: "Show session status (model, usage, uptime)", autoComplete: true, }, { trigger: "oc_model", + originalName: "model", description: "View or change the current model", autoComplete: true, autoCompleteHint: "[model-name]", }, { trigger: "oc_new", + originalName: "new", description: "Start a new conversation session", autoComplete: true, }, { trigger: "oc_help", + originalName: "help", description: "Show available commands", autoComplete: true, }, { trigger: "oc_think", + originalName: "think", description: "Set thinking/reasoning level", autoComplete: true, autoCompleteHint: "[off|low|medium|high]", }, { trigger: "oc_reasoning", + originalName: "reasoning", description: "Toggle reasoning mode", autoComplete: true, autoCompleteHint: "[on|off]", }, { trigger: "oc_verbose", + originalName: "verbose", description: "Toggle verbose mode", autoComplete: true, autoCompleteHint: "[on|off]", @@ -435,9 +444,14 @@ export function parseSlashCommandPayload( * Map the trigger word back to the original OpenClaw command name. * e.g. "oc_status" -> "/status", "oc_model" -> "/model" */ -export function resolveCommandText(trigger: string, text: string): string { - // Strip the "oc_" prefix to get the original command name - const commandName = trigger.startsWith("oc_") ? trigger.slice(3) : trigger; +export function resolveCommandText( + trigger: string, + text: string, + triggerMap?: ReadonlyMap, +): string { + // Use the trigger map if available for accurate name resolution + const commandName = + triggerMap?.get(trigger) ?? (trigger.startsWith("oc_") ? trigger.slice(3) : trigger); const args = text.trim(); return args ? `/${commandName} ${args}` : `/${commandName}`; } diff --git a/extensions/mattermost/src/mattermost/slash-http.ts b/extensions/mattermost/src/mattermost/slash-http.ts index cdb77314074..3b784409378 100644 --- a/extensions/mattermost/src/mattermost/slash-http.ts +++ b/extensions/mattermost/src/mattermost/slash-http.ts @@ -36,6 +36,8 @@ type SlashHttpHandlerParams = { runtime: RuntimeEnv; /** Expected token from registered commands (for validation). */ commandTokens: Set; + /** Map from trigger to original command name (for skill commands that start with oc_). */ + triggerMap?: ReadonlyMap; log?: (msg: string) => void; }; @@ -361,7 +363,7 @@ async function authorizeSlashInvocation(params: { * from the Mattermost server when a user invokes a registered slash command. */ export function createSlashCommandHttpHandler(params: SlashHttpHandlerParams) { - const { account, cfg, runtime, commandTokens, log } = params; + const { account, cfg, runtime, commandTokens, triggerMap, log } = params; const MAX_BODY_BYTES = 64 * 1024; // 64KB @@ -404,7 +406,7 @@ export function createSlashCommandHttpHandler(params: SlashHttpHandlerParams) { // Extract command info const trigger = payload.command.replace(/^\//, "").trim(); - const commandText = resolveCommandText(trigger, payload.text); + const commandText = resolveCommandText(trigger, payload.text, triggerMap); const channelId = payload.channel_id; const senderId = payload.user_id; const senderName = payload.user_name ?? senderId; diff --git a/extensions/mattermost/src/mattermost/slash-state.ts b/extensions/mattermost/src/mattermost/slash-state.ts index 2baf7669741..6c75a8ab4c7 100644 --- a/extensions/mattermost/src/mattermost/slash-state.ts +++ b/extensions/mattermost/src/mattermost/slash-state.ts @@ -26,6 +26,8 @@ type SlashCommandAccountState = { handler: ((req: IncomingMessage, res: ServerResponse) => Promise) | null; /** The account that activated slash commands. */ account: ResolvedMattermostAccount; + /** Map from trigger to original command name (for skill commands that start with oc_). */ + triggerMap: Map; }; /** Map from accountId → per-account slash command state. */ @@ -53,13 +55,14 @@ export function activateSlashCommands(params: { account: ResolvedMattermostAccount; commandTokens: string[]; registeredCommands: MattermostRegisteredCommand[]; + triggerMap?: Map; api: { cfg: import("openclaw/plugin-sdk").OpenClawConfig; runtime: import("openclaw/plugin-sdk").RuntimeEnv; }; log?: (msg: string) => void; }) { - const { account, commandTokens, registeredCommands, api, log } = params; + const { account, commandTokens, registeredCommands, triggerMap, api, log } = params; const accountId = account.accountId; const tokenSet = new Set(commandTokens); @@ -69,6 +72,7 @@ export function activateSlashCommands(params: { cfg: api.cfg, runtime: api.runtime, commandTokens: tokenSet, + triggerMap, log, }); @@ -77,6 +81,7 @@ export function activateSlashCommands(params: { registeredCommands, handler, account, + triggerMap: triggerMap ?? new Map(), }); log?.( diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 3a1e547548c..32d0f3cfd79 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -543,6 +543,8 @@ export type { } from "../infra/diagnostic-events.js"; export { detectMime, extensionForMime, getFileExtension } from "../media/mime.js"; export { extractOriginalFilename } from "../media/store.js"; +export { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js"; +export type { SkillCommandSpec } from "../agents/skills.js"; // Channel: Discord export {