Slack: accept slash command names with or without leading slash

Closes #798
This commit is contained in:
Shadow
2026-01-12 21:26:44 -06:00
parent 67d0ab3030
commit 68569afb4b
3 changed files with 37 additions and 3 deletions

View File

@@ -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.

View File

@@ -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);
});
});

View File

@@ -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<string | number>) {
return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean);
}
@@ -227,9 +236,13 @@ function resolveSlackUserAllowed(params: {
function resolveSlackSlashCommandConfig(
raw?: SlackSlashCommandConfig,
): Required<SlackSlashCommandConfig> {
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,