diff --git a/docs/tools/slash-commands.md b/docs/tools/slash-commands.md index a69bf194d..9cfcee538 100644 --- a/docs/tools/slash-commands.md +++ b/docs/tools/slash-commands.md @@ -35,6 +35,8 @@ Directives (`/think`, `/verbose`, `/reasoning`, `/elevated`) are parsed even whe Text + native (when enabled): - `/help` +- `/commands` +- `/status` - `/status` (show current status; includes a short usage line when available) - `/usage` (alias: `/status`) - `/debug show|set|unset|reset` (runtime overrides, owner-only) diff --git a/src/auto-reply/command-detection.test.ts b/src/auto-reply/command-detection.test.ts index b43bbed1b..e21d35d65 100644 --- a/src/auto-reply/command-detection.test.ts +++ b/src/auto-reply/command-detection.test.ts @@ -40,6 +40,9 @@ describe("control command parsing", () => { it("treats bare commands as non-control", () => { expect(hasControlCommand("send")).toBe(false); expect(hasControlCommand("help")).toBe(false); + expect(hasControlCommand("/commands")).toBe(true); + expect(hasControlCommand("/commands:")).toBe(true); + expect(hasControlCommand("commands")).toBe(false); expect(hasControlCommand("status")).toBe(false); expect(hasControlCommand("usage")).toBe(false); diff --git a/src/auto-reply/commands-registry.ts b/src/auto-reply/commands-registry.ts index 63cf44a7d..9565968be 100644 --- a/src/auto-reply/commands-registry.ts +++ b/src/auto-reply/commands-registry.ts @@ -53,6 +53,12 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => { description: "Show available commands.", textAlias: "/help", }), + defineChatCommand({ + key: "commands", + nativeName: "commands", + description: "List all slash commands.", + textAlias: "/commands", + }), defineChatCommand({ key: "status", nativeName: "status", diff --git a/src/auto-reply/reply/commands.ts b/src/auto-reply/reply/commands.ts index 7299a846d..c5ffd26a2 100644 --- a/src/auto-reply/reply/commands.ts +++ b/src/auto-reply/reply/commands.ts @@ -57,6 +57,7 @@ import { } from "../group-activation.js"; import { parseSendPolicyCommand } from "../send-policy.js"; import { + buildCommandsMessage, buildHelpMessage, buildStatusMessage, formatContextUsageShort, @@ -592,6 +593,17 @@ export async function handleCommands(params: { return { shouldContinue: false, reply: { text: buildHelpMessage() } }; } + const commandsRequested = command.commandBodyNormalized === "/commands"; + if (allowTextCommands && commandsRequested) { + if (!command.isAuthorizedSender) { + logVerbose( + `Ignoring /commands from unauthorized sender: ${command.senderE164 || ""}`, + ); + return { shouldContinue: false }; + } + return { shouldContinue: false, reply: { text: buildCommandsMessage() } }; + } + const statusRequested = directives.hasStatusDirective || command.commandBodyNormalized === "/status"; diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index bbd272f2c..fb40ef69a 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -4,7 +4,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { normalizeTestText } from "../../test/helpers/normalize-text.js"; import { withTempHome } from "../../test/helpers/temp-home.js"; import type { ClawdbotConfig } from "../config/config.js"; -import { buildStatusMessage } from "./status.js"; +import { buildCommandsMessage, buildStatusMessage } from "./status.js"; afterEach(() => { vi.restoreAllMocks(); @@ -296,3 +296,10 @@ describe("buildStatusMessage", () => { ); }); }); + +describe("buildCommandsMessage", () => { + it("lists commands with aliases and text-only hints", () => { + const text = buildCommandsMessage(); + expect(text).toContain("/commands - List all slash commands."); + }); +}); diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index 9b9f96520..989d2c0eb 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -28,6 +28,7 @@ import { resolveModelCostConfig, } from "../utils/usage-format.js"; import { VERSION } from "../version.js"; +import { listChatCommands } from "./commands-registry.js"; import type { ElevatedLevel, ReasoningLevel, @@ -358,5 +359,29 @@ export function buildHelpMessage(): string { "ℹ️ Help", "Shortcuts: /new reset | /compact [instructions] | /restart relink (if enabled)", "Options: /think | /verbose on|off | /reasoning on|off | /elevated on|off | /model | /cost on|off | /debug show", + "More: /commands for all slash commands", ].join("\n"); } + +export function buildCommandsMessage(): string { + const lines = ["ℹ️ Slash commands"]; + for (const command of listChatCommands()) { + const primary = `/${command.nativeName}`; + const seen = new Set(); + const aliases = command.textAliases + .map((alias) => alias.trim()) + .filter(Boolean) + .filter((alias) => alias.toLowerCase() !== primary.toLowerCase()) + .filter((alias) => { + const key = alias.toLowerCase(); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + const aliasLabel = aliases.length + ? ` (aliases: ${aliases.join(", ")})` + : ""; + lines.push(`${primary}${aliasLabel} - ${command.description}`); + } + return lines.join("\n"); +}