diff --git a/src/slack/monitor/provider.ts b/src/slack/monitor/provider.ts index 0533647abd6..6362cfcc8a0 100644 --- a/src/slack/monitor/provider.ts +++ b/src/slack/monitor/provider.ts @@ -227,7 +227,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { const handleSlackMessage = createSlackMessageHandler({ ctx, account }); registerSlackMonitorEvents({ ctx, account, handleSlackMessage }); - registerSlackMonitorSlashCommands({ ctx, account }); + await registerSlackMonitorSlashCommands({ ctx, account }); if (slackMode === "http" && slackHttpHandler) { unregisterHttpHandler = registerSlackHttpHandler({ path: slackWebhookPath, diff --git a/src/slack/monitor/slash.command-arg-menus.test.ts b/src/slack/monitor/slash.command-arg-menus.test.ts deleted file mode 100644 index 15931947973..00000000000 --- a/src/slack/monitor/slash.command-arg-menus.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { getSlackSlashMocks, resetSlackSlashMocks } from "./slash.test-harness.js"; - -const { dispatchMock } = getSlackSlashMocks(); - -beforeEach(() => { - resetSlackSlashMocks(); -}); - -async function registerCommands(ctx: unknown, account: unknown) { - const { registerSlackMonitorSlashCommands } = await import("./slash.js"); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); -} - -function encodeValue(parts: { command: string; arg: string; value: string; userId: string }) { - return [ - "cmdarg", - encodeURIComponent(parts.command), - encodeURIComponent(parts.arg), - encodeURIComponent(parts.value), - encodeURIComponent(parts.userId), - ].join("|"); -} - -function createHarness() { - const commands = new Map Promise>(); - const actions = new Map Promise>(); - - const postEphemeral = vi.fn().mockResolvedValue({ ok: true }); - const app = { - client: { chat: { postEphemeral } }, - command: (name: string, handler: (args: unknown) => Promise) => { - commands.set(name, handler); - }, - action: (id: string, handler: (args: unknown) => Promise) => { - actions.set(id, handler); - }, - }; - - const ctx = { - cfg: { commands: { native: true } }, - runtime: {}, - botToken: "bot-token", - botUserId: "bot", - teamId: "T1", - allowFrom: ["*"], - dmEnabled: true, - dmPolicy: "open", - groupDmEnabled: false, - groupDmChannels: [], - defaultRequireMention: true, - groupPolicy: "open", - useAccessGroups: false, - channelsConfig: undefined, - slashCommand: { - enabled: true, - name: "openclaw", - ephemeral: true, - sessionPrefix: "slack:slash", - }, - textLimit: 4000, - app, - isChannelAllowed: () => true, - resolveChannelName: async () => ({ name: "dm", type: "im" }), - resolveUserName: async () => ({ name: "Ada" }), - } as unknown; - - const account = { accountId: "acct", config: { commands: { native: true } } } as unknown; - - return { commands, actions, postEphemeral, ctx, account }; -} - -describe("Slack native command argument menus", () => { - it("shows a button menu when required args are omitted", async () => { - const { commands, ctx, account } = createHarness(); - await registerCommands(ctx, account); - - const handler = commands.get("/usage"); - if (!handler) { - throw new Error("Missing /usage handler"); - } - - const respond = vi.fn().mockResolvedValue(undefined); - const ack = vi.fn().mockResolvedValue(undefined); - - await handler({ - command: { - user_id: "U1", - user_name: "Ada", - channel_id: "C1", - channel_name: "directmessage", - text: "", - trigger_id: "t1", - }, - ack, - respond, - }); - - expect(respond).toHaveBeenCalledTimes(1); - const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> }; - expect(payload.blocks?.[0]?.type).toBe("section"); - expect(payload.blocks?.[1]?.type).toBe("actions"); - }); - - it("dispatches the command when a menu button is clicked", async () => { - const { actions, ctx, account } = createHarness(); - await registerCommands(ctx, account); - - const handler = actions.get("openclaw_cmdarg"); - if (!handler) { - throw new Error("Missing arg-menu action handler"); - } - - const respond = vi.fn().mockResolvedValue(undefined); - await handler({ - ack: vi.fn().mockResolvedValue(undefined), - action: { - value: encodeValue({ command: "usage", arg: "mode", value: "tokens", userId: "U1" }), - }, - body: { - user: { id: "U1", name: "Ada" }, - channel: { id: "C1", name: "directmessage" }, - trigger_id: "t1", - }, - respond, - }); - - expect(dispatchMock).toHaveBeenCalledTimes(1); - const call = dispatchMock.mock.calls[0]?.[0] as { ctx?: { Body?: string } }; - expect(call.ctx?.Body).toBe("/usage tokens"); - }); - - it("rejects menu clicks from other users", async () => { - const { actions, ctx, account } = createHarness(); - await registerCommands(ctx, account); - - const handler = actions.get("openclaw_cmdarg"); - if (!handler) { - throw new Error("Missing arg-menu action handler"); - } - - const respond = vi.fn().mockResolvedValue(undefined); - await handler({ - ack: vi.fn().mockResolvedValue(undefined), - action: { - value: encodeValue({ command: "usage", arg: "mode", value: "tokens", userId: "U1" }), - }, - body: { - user: { id: "U2", name: "Eve" }, - channel: { id: "C1", name: "directmessage" }, - trigger_id: "t1", - }, - respond, - }); - - expect(dispatchMock).not.toHaveBeenCalled(); - expect(respond).toHaveBeenCalledWith({ - text: "That menu is for another user.", - response_type: "ephemeral", - }); - }); - - it("falls back to postEphemeral with token when respond is unavailable", async () => { - const { actions, postEphemeral, ctx, account } = createHarness(); - await registerCommands(ctx, account); - - const handler = actions.get("openclaw_cmdarg"); - if (!handler) { - throw new Error("Missing arg-menu action handler"); - } - - await handler({ - ack: vi.fn().mockResolvedValue(undefined), - action: { value: "garbage" }, - body: { user: { id: "U1" }, channel: { id: "C1" } }, - }); - - expect(postEphemeral).toHaveBeenCalledWith( - expect.objectContaining({ - token: "bot-token", - channel: "C1", - user: "U1", - }), - ); - }); - - it("treats malformed percent-encoding as an invalid button (no throw)", async () => { - const { actions, postEphemeral, ctx, account } = createHarness(); - await registerCommands(ctx, account); - - const handler = actions.get("openclaw_cmdarg"); - if (!handler) { - throw new Error("Missing arg-menu action handler"); - } - - await handler({ - ack: vi.fn().mockResolvedValue(undefined), - action: { value: "cmdarg|%E0%A4%A|mode|on|U1" }, - body: { user: { id: "U1" }, channel: { id: "C1" } }, - }); - - expect(postEphemeral).toHaveBeenCalledWith( - expect.objectContaining({ - token: "bot-token", - channel: "C1", - user: "U1", - text: "Sorry, that button is no longer valid.", - }), - ); - }); -}); diff --git a/src/slack/monitor/slash.policy.test.ts b/src/slack/monitor/slash.test.ts similarity index 54% rename from src/slack/monitor/slash.policy.test.ts rename to src/slack/monitor/slash.test.ts index 108ed91f266..352b74d9021 100644 --- a/src/slack/monitor/slash.policy.test.ts +++ b/src/slack/monitor/slash.test.ts @@ -1,18 +1,227 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { getSlackSlashMocks, resetSlackSlashMocks } from "./slash.test-harness.js"; +type RegisterFn = (params: { ctx: unknown; account: unknown }) => Promise; +let registerSlackMonitorSlashCommands: RegisterFn; + const { dispatchMock } = getSlackSlashMocks(); +beforeAll(async () => { + ({ registerSlackMonitorSlashCommands } = (await import("./slash.js")) as unknown as { + registerSlackMonitorSlashCommands: RegisterFn; + }); +}); + beforeEach(() => { resetSlackSlashMocks(); }); async function registerCommands(ctx: unknown, account: unknown) { - const { registerSlackMonitorSlashCommands } = await import("./slash.js"); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + await registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); } -function createHarness(overrides?: { +function encodeValue(parts: { command: string; arg: string; value: string; userId: string }) { + return [ + "cmdarg", + encodeURIComponent(parts.command), + encodeURIComponent(parts.arg), + encodeURIComponent(parts.value), + encodeURIComponent(parts.userId), + ].join("|"); +} + +function createArgMenusHarness() { + const commands = new Map Promise>(); + const actions = new Map Promise>(); + + const postEphemeral = vi.fn().mockResolvedValue({ ok: true }); + const app = { + client: { chat: { postEphemeral } }, + command: (name: string, handler: (args: unknown) => Promise) => { + commands.set(name, handler); + }, + action: (id: string, handler: (args: unknown) => Promise) => { + actions.set(id, handler); + }, + }; + + const ctx = { + cfg: { commands: { native: true, nativeSkills: false } }, + runtime: {}, + botToken: "bot-token", + botUserId: "bot", + teamId: "T1", + allowFrom: ["*"], + dmEnabled: true, + dmPolicy: "open", + groupDmEnabled: false, + groupDmChannels: [], + defaultRequireMention: true, + groupPolicy: "open", + useAccessGroups: false, + channelsConfig: undefined, + slashCommand: { + enabled: true, + name: "openclaw", + ephemeral: true, + sessionPrefix: "slack:slash", + }, + textLimit: 4000, + app, + isChannelAllowed: () => true, + resolveChannelName: async () => ({ name: "dm", type: "im" }), + resolveUserName: async () => ({ name: "Ada" }), + } as unknown; + + const account = { + accountId: "acct", + config: { commands: { native: true, nativeSkills: false } }, + } as unknown; + + return { commands, actions, postEphemeral, ctx, account }; +} + +describe("Slack native command argument menus", () => { + it("shows a button menu when required args are omitted", async () => { + const { commands, ctx, account } = createArgMenusHarness(); + await registerCommands(ctx, account); + + const handler = commands.get("/usage"); + if (!handler) { + throw new Error("Missing /usage handler"); + } + + const respond = vi.fn().mockResolvedValue(undefined); + const ack = vi.fn().mockResolvedValue(undefined); + + await handler({ + command: { + user_id: "U1", + user_name: "Ada", + channel_id: "C1", + channel_name: "directmessage", + text: "", + trigger_id: "t1", + }, + ack, + respond, + }); + + expect(respond).toHaveBeenCalledTimes(1); + const payload = respond.mock.calls[0]?.[0] as { blocks?: Array<{ type: string }> }; + expect(payload.blocks?.[0]?.type).toBe("section"); + expect(payload.blocks?.[1]?.type).toBe("actions"); + }); + + it("dispatches the command when a menu button is clicked", async () => { + const { actions, ctx, account } = createArgMenusHarness(); + await registerCommands(ctx, account); + + const handler = actions.get("openclaw_cmdarg"); + if (!handler) { + throw new Error("Missing arg-menu action handler"); + } + + const respond = vi.fn().mockResolvedValue(undefined); + await handler({ + ack: vi.fn().mockResolvedValue(undefined), + action: { + value: encodeValue({ command: "usage", arg: "mode", value: "tokens", userId: "U1" }), + }, + body: { + user: { id: "U1", name: "Ada" }, + channel: { id: "C1", name: "directmessage" }, + trigger_id: "t1", + }, + respond, + }); + + expect(dispatchMock).toHaveBeenCalledTimes(1); + const call = dispatchMock.mock.calls[0]?.[0] as { ctx?: { Body?: string } }; + expect(call.ctx?.Body).toBe("/usage tokens"); + }); + + it("rejects menu clicks from other users", async () => { + const { actions, ctx, account } = createArgMenusHarness(); + await registerCommands(ctx, account); + + const handler = actions.get("openclaw_cmdarg"); + if (!handler) { + throw new Error("Missing arg-menu action handler"); + } + + const respond = vi.fn().mockResolvedValue(undefined); + await handler({ + ack: vi.fn().mockResolvedValue(undefined), + action: { + value: encodeValue({ command: "usage", arg: "mode", value: "tokens", userId: "U1" }), + }, + body: { + user: { id: "U2", name: "Eve" }, + channel: { id: "C1", name: "directmessage" }, + trigger_id: "t1", + }, + respond, + }); + + expect(dispatchMock).not.toHaveBeenCalled(); + expect(respond).toHaveBeenCalledWith({ + text: "That menu is for another user.", + response_type: "ephemeral", + }); + }); + + it("falls back to postEphemeral with token when respond is unavailable", async () => { + const { actions, postEphemeral, ctx, account } = createArgMenusHarness(); + await registerCommands(ctx, account); + + const handler = actions.get("openclaw_cmdarg"); + if (!handler) { + throw new Error("Missing arg-menu action handler"); + } + + await handler({ + ack: vi.fn().mockResolvedValue(undefined), + action: { value: "garbage" }, + body: { user: { id: "U1" }, channel: { id: "C1" } }, + }); + + expect(postEphemeral).toHaveBeenCalledWith( + expect.objectContaining({ + token: "bot-token", + channel: "C1", + user: "U1", + }), + ); + }); + + it("treats malformed percent-encoding as an invalid button (no throw)", async () => { + const { actions, postEphemeral, ctx, account } = createArgMenusHarness(); + await registerCommands(ctx, account); + + const handler = actions.get("openclaw_cmdarg"); + if (!handler) { + throw new Error("Missing arg-menu action handler"); + } + + await handler({ + ack: vi.fn().mockResolvedValue(undefined), + action: { value: "cmdarg|%E0%A4%A|mode|on|U1" }, + body: { user: { id: "U1" }, channel: { id: "C1" } }, + }); + + expect(postEphemeral).toHaveBeenCalledWith( + expect.objectContaining({ + token: "bot-token", + channel: "C1", + user: "U1", + text: "Sorry, that button is no longer valid.", + }), + ); + }); +}); + +function createPolicyHarness(overrides?: { groupPolicy?: "open" | "allowlist"; channelsConfig?: Record; channelId?: string; @@ -104,7 +313,7 @@ async function runSlashHandler(params: { describe("slack slash commands channel policy", () => { it("allows unlisted channels when groupPolicy is open", async () => { - const { commands, ctx, account, channelId, channelName } = createHarness({ + const { commands, ctx, account, channelId, channelName } = createPolicyHarness({ groupPolicy: "open", channelsConfig: { C_LISTED: { requireMention: true } }, channelId: "C_UNLISTED", @@ -127,7 +336,7 @@ describe("slack slash commands channel policy", () => { }); it("blocks explicitly denied channels when groupPolicy is open", async () => { - const { commands, ctx, account, channelId, channelName } = createHarness({ + const { commands, ctx, account, channelId, channelName } = createPolicyHarness({ groupPolicy: "open", channelsConfig: { C_DENIED: { allow: false } }, channelId: "C_DENIED", @@ -151,7 +360,7 @@ describe("slack slash commands channel policy", () => { }); it("blocks unlisted channels when groupPolicy is allowlist", async () => { - const { commands, ctx, account, channelId, channelName } = createHarness({ + const { commands, ctx, account, channelId, channelName } = createPolicyHarness({ groupPolicy: "allowlist", channelsConfig: { C_LISTED: { requireMention: true } }, channelId: "C_UNLISTED", @@ -177,7 +386,7 @@ describe("slack slash commands channel policy", () => { describe("slack slash commands access groups", () => { it("fails closed when channel type lookup returns empty for channels", async () => { - const { commands, ctx, account, channelId, channelName } = createHarness({ + const { commands, ctx, account, channelId, channelName } = createPolicyHarness({ allowFrom: [], channelId: "C_UNKNOWN", channelName: "unknown", @@ -201,7 +410,7 @@ describe("slack slash commands access groups", () => { }); it("still treats D-prefixed channel ids as DMs when lookup fails", async () => { - const { commands, ctx, account } = createHarness({ + const { commands, ctx, account } = createPolicyHarness({ allowFrom: [], channelId: "D123", channelName: "notdirectmessage", @@ -228,7 +437,7 @@ describe("slack slash commands access groups", () => { }); it("computes CommandAuthorized for DM slash commands when dmPolicy is open", async () => { - const { commands, ctx, account } = createHarness({ + const { commands, ctx, account } = createPolicyHarness({ allowFrom: ["U_OWNER"], channelId: "D999", channelName: "directmessage", @@ -254,7 +463,7 @@ describe("slack slash commands access groups", () => { }); it("enforces access-group gating when lookup fails for private channels", async () => { - const { commands, ctx, account, channelId, channelName } = createHarness({ + const { commands, ctx, account, channelId, channelName } = createPolicyHarness({ allowFrom: [], channelId: "G123", channelName: "private", diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index 61046ede922..b7db5115bcf 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -2,30 +2,15 @@ import type { SlackActionMiddlewareArgs, SlackCommandMiddlewareArgs } from "@sla import type { ChatCommandDefinition, CommandArgs } from "../../auto-reply/commands-registry.js"; import type { ResolvedSlackAccount } from "../accounts.js"; import type { SlackMonitorContext } from "./context.js"; -import { resolveChunkMode } from "../../auto-reply/chunk.js"; -import { - buildCommandTextFromArgs, - findCommandByNativeName, - listNativeCommandSpecsForConfig, - parseCommandArgs, - resolveCommandArgMenu, -} from "../../auto-reply/commands-registry.js"; -import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js"; -import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js"; -import { listSkillCommandsForAgents } from "../../auto-reply/skill-commands.js"; import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js"; -import { resolveConversationLabel } from "../../channels/conversation-label.js"; -import { createReplyPrefixOptions } from "../../channels/reply-prefix.js"; import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../../config/commands.js"; -import { resolveMarkdownTableMode } from "../../config/markdown-tables.js"; import { danger, logVerbose } from "../../globals.js"; import { buildPairingReply } from "../../pairing/pairing-messages.js"; import { readChannelAllowFromStore, upsertChannelPairingRequest, } from "../../pairing/pairing-store.js"; -import { resolveAgentRoute } from "../../routing/resolve-route.js"; import { normalizeAllowList, normalizeAllowListLower, @@ -36,7 +21,6 @@ import { resolveSlackChannelConfig, type SlackChannelConfigResolved } from "./ch import { buildSlackSlashCommandMatcher, resolveSlackSlashCommandConfig } from "./commands.js"; import { normalizeSlackChannelType } from "./context.js"; import { isSlackChannelAllowedByPolicy } from "./policy.js"; -import { deliverSlackSlashReplies } from "./replies.js"; import { resolveSlackRoomContextHints } from "./room-context.js"; type SlackBlock = { type: string; [key: string]: unknown }; @@ -44,6 +28,15 @@ type SlackBlock = { type: string; [key: string]: unknown }; const SLACK_COMMAND_ARG_ACTION_ID = "openclaw_cmdarg"; const SLACK_COMMAND_ARG_VALUE_PREFIX = "cmdarg"; +type CommandsRegistry = typeof import("../../auto-reply/commands-registry.js"); +let commandsRegistry: CommandsRegistry | undefined; +async function getCommandsRegistry(): Promise { + if (!commandsRegistry) { + commandsRegistry = await import("../../auto-reply/commands-registry.js"); + } + return commandsRegistry; +} + function chunkItems(items: T[], size: number): T[][] { if (size <= 0) { return [items]; @@ -139,10 +132,10 @@ function buildSlackCommandArgMenuBlocks(params: { ]; } -export function registerSlackMonitorSlashCommands(params: { +export async function registerSlackMonitorSlashCommands(params: { ctx: SlackMonitorContext; account: ResolvedSlackAccount; -}) { +}): Promise { const { ctx, account } = params; const cfg = ctx.cfg; const runtime = ctx.runtime; @@ -349,7 +342,8 @@ export function registerSlackMonitorSlashCommands(params: { } if (commandDefinition && supportsInteractiveArgMenus) { - const menu = resolveCommandArgMenu({ + const reg = await getCommandsRegistry(); + const menu = reg.resolveCommandArgMenu({ command: commandDefinition, args: commandArgs, cfg, @@ -376,6 +370,17 @@ export function registerSlackMonitorSlashCommands(params: { const channelName = channelInfo?.name; const roomLabel = channelName ? `#${channelName}` : `#${command.channel_id}`; + const [{ resolveAgentRoute }, { finalizeInboundContext }, { dispatchReplyWithDispatcher }] = + await Promise.all([ + import("../../routing/resolve-route.js"), + import("../../auto-reply/reply/inbound-context.js"), + import("../../auto-reply/reply/provider-dispatcher.js"), + ]); + const [{ resolveConversationLabel }, { createReplyPrefixOptions }] = await Promise.all([ + import("../../channels/conversation-label.js"), + import("../../channels/reply-prefix.js"), + ]); + const route = resolveAgentRoute({ cfg, channel: "slack", @@ -450,6 +455,15 @@ export function registerSlackMonitorSlashCommands(params: { dispatcherOptions: { ...prefixOptions, deliver: async (payload) => { + const [ + { deliverSlackSlashReplies }, + { resolveChunkMode }, + { resolveMarkdownTableMode }, + ] = await Promise.all([ + import("./replies.js"), + import("../../auto-reply/chunk.js"), + import("../../config/markdown-tables.js"), + ]); await deliverSlackSlashReplies({ replies: [payload], respond, @@ -473,6 +487,12 @@ export function registerSlackMonitorSlashCommands(params: { }, }); if (counts.final + counts.tool + counts.block === 0) { + const [{ deliverSlackSlashReplies }, { resolveChunkMode }, { resolveMarkdownTableMode }] = + await Promise.all([ + import("./replies.js"), + import("../../auto-reply/chunk.js"), + import("../../config/markdown-tables.js"), + ]); await deliverSlackSlashReplies({ replies: [], respond, @@ -505,25 +525,35 @@ export function registerSlackMonitorSlashCommands(params: { providerSetting: account.config.commands?.nativeSkills, globalSetting: cfg.commands?.nativeSkills, }); - const skillCommands = - nativeEnabled && nativeSkillsEnabled ? listSkillCommandsForAgents({ cfg }) : []; - const nativeCommands = nativeEnabled - ? listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "slack" }) - : []; + + let reg: CommandsRegistry | undefined; + let nativeCommands: Array<{ name: string }> = []; + if (nativeEnabled) { + reg = await getCommandsRegistry(); + const skillCommands = nativeSkillsEnabled + ? (await import("../../auto-reply/skill-commands.js")).listSkillCommandsForAgents({ cfg }) + : []; + nativeCommands = reg.listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "slack" }); + } + if (nativeCommands.length > 0) { + const registry = reg; + if (!registry) { + throw new Error("Missing commands registry for native Slack commands."); + } for (const command of nativeCommands) { ctx.app.command( `/${command.name}`, async ({ command: cmd, ack, respond }: SlackCommandMiddlewareArgs) => { - const commandDefinition = findCommandByNativeName(command.name, "slack"); + const commandDefinition = registry.findCommandByNativeName(command.name, "slack"); const rawText = cmd.text?.trim() ?? ""; const commandArgs = commandDefinition - ? parseCommandArgs(commandDefinition, rawText) + ? registry.parseCommandArgs(commandDefinition, rawText) : rawText ? ({ raw: rawText } satisfies CommandArgs) : undefined; const prompt = commandDefinition - ? buildCommandTextFromArgs(commandDefinition, commandArgs) + ? registry.buildCommandTextFromArgs(commandDefinition, commandArgs) : rawText ? `/${command.name} ${rawText}` : `/${command.name}`; @@ -596,12 +626,13 @@ export function registerSlackMonitorSlashCommands(params: { }); return; } - const commandDefinition = findCommandByNativeName(parsed.command, "slack"); + const reg = await getCommandsRegistry(); + const commandDefinition = reg.findCommandByNativeName(parsed.command, "slack"); const commandArgs: CommandArgs = { values: { [parsed.arg]: parsed.value }, }; const prompt = commandDefinition - ? buildCommandTextFromArgs(commandDefinition, commandArgs) + ? reg.buildCommandTextFromArgs(commandDefinition, commandArgs) : `/${parsed.command} ${parsed.value}`; const user = body.user; const userName =