diff --git a/CHANGELOG.md b/CHANGELOG.md index eff098227..9bc9dce26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Memory: allow custom OpenAI-compatible embedding endpoints for memory search (remote baseUrl/apiKey/headers). (#819 — thanks @mukhtharcm) ### Fixes +- Slack: accept slash commands with or without leading `/` for custom command configs. (#798 — thanks @thewilloftheshadow) - Onboarding/Configure: refuse to proceed with invalid configs; run `clawdbot doctor` first to avoid wiping custom fields. (#764 — thanks @mukhtharcm) - Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake) - Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs. diff --git a/src/slack/monitor.test.ts b/src/slack/monitor.test.ts index 0a2b4dba6..65eb998e7 100644 --- a/src/slack/monitor.test.ts +++ b/src/slack/monitor.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "vitest"; -import { isSlackRoomAllowedByPolicy, resolveSlackThreadTs } from "./monitor.js"; +import { + buildSlackSlashCommandMatcher, + isSlackRoomAllowedByPolicy, + resolveSlackThreadTs, +} from "./monitor.js"; describe("slack groupPolicy gating", () => { it("allows when policy is open", () => { @@ -152,3 +156,19 @@ describe("resolveSlackThreadTs", () => { }); }); }); + +describe("buildSlackSlashCommandMatcher", () => { + it("matches with or without a leading slash", () => { + const matcher = buildSlackSlashCommandMatcher("clawd"); + + expect(matcher.test("clawd")).toBe(true); + expect(matcher.test("/clawd")).toBe(true); + }); + + it("does not match similar names", () => { + const matcher = buildSlackSlashCommandMatcher("clawd"); + + expect(matcher.test("/clawd-bot")).toBe(false); + expect(matcher.test("clawd-bot")).toBe(false); + }); +}); diff --git a/src/slack/monitor.ts b/src/slack/monitor.ts index e5f4a3c7f..314df9b3c 100644 --- a/src/slack/monitor.ts +++ b/src/slack/monitor.ts @@ -173,6 +173,15 @@ function normalizeSlackSlug(raw?: string) { return cleaned.replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, ""); } +function normalizeSlackSlashCommandName(raw: string) { + return raw.replace(/^\/+/, ""); +} + +export function buildSlackSlashCommandMatcher(name: string) { + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return new RegExp(`^/?${escaped}$`); +} + function normalizeAllowList(list?: Array) { return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean); } @@ -227,9 +236,13 @@ function resolveSlackUserAllowed(params: { function resolveSlackSlashCommandConfig( raw?: SlackSlashCommandConfig, ): Required { + const normalizedName = normalizeSlackSlashCommandName( + raw?.name?.trim() || "clawd", + ); + const name = normalizedName || "clawd"; return { enabled: raw?.enabled === true, - name: raw?.name?.trim() || "clawd", + name, sessionPrefix: raw?.sessionPrefix?.trim() || "slack:slash", ephemeral: raw?.ephemeral !== false, }; @@ -1980,7 +1993,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { } } else if (slashCommand.enabled) { app.command( - slashCommand.name, + buildSlackSlashCommandMatcher(slashCommand.name), async ({ command, ack, respond }: SlackCommandMiddlewareArgs) => { await handleSlashCommand({ command,