From ad5c87c19336d5ce5d75f2fed1df1082af6534be Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 8 Jan 2026 03:22:14 +0100 Subject: [PATCH] fix: relax slash command parsing --- CHANGELOG.md | 1 + README.md | 18 ++-- docs/tools/elevated.md | 1 + docs/tools/slash-commands.md | 3 + docs/tools/thinking.md | 3 + src/auto-reply/command-detection.test.ts | 14 +++ src/auto-reply/command-detection.ts | 8 +- src/auto-reply/commands-registry.test.ts | 2 + src/auto-reply/commands-registry.ts | 14 ++- src/auto-reply/group-activation.ts | 7 +- src/auto-reply/model.test.ts | 9 ++ src/auto-reply/model.ts | 2 +- src/auto-reply/reply.directive.test.ts | 110 +++++++++++++++++++++ src/auto-reply/reply.ts | 11 +++ src/auto-reply/reply/commands.ts | 11 ++- src/auto-reply/reply/directive-handling.ts | 27 ++++- src/auto-reply/reply/directives.ts | 10 +- src/auto-reply/send-policy.ts | 6 +- 18 files changed, 226 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 113b4ae4b..a7471ddd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ - iMessage: ignore disconnect errors during shutdown (avoid unhandled promise rejections). Thanks @antons for PR #359. - Messages: stop defaulting ack reactions to 👀 when identity emoji is missing. - Auto-reply: require slash for control commands to avoid false triggers in normal text. +- Commands: accept optional `:` in slash commands and show current levels for /think, /verbose, /reasoning, and /elevated when no args are provided. Thanks @lutr0 for PR #382. - Auto-reply: add `/reasoning on|off` to expose model reasoning blocks (italic). - Auto-reply: place reasoning blocks before the final reply text when appended. - Auto-reply: flag error payloads and improve Bun socket error messaging. Thanks @emanuelst for PR #331. diff --git a/README.md b/README.md index 03c446cc1..b814a56fe 100644 --- a/README.md +++ b/README.md @@ -447,12 +447,12 @@ AI/vibe-coded PRs welcome! 🤖 Thanks to all clawtributors:

- steipete thewilloftheshadow mcinteerj joshp123 joaohlisboa petter-b mukhtharcm dan-dr Nachx639 jeffersonwarrior - mbelinky julianengel CashWilliams omniwired jverdi Syhids meaningfool rafaelreis-r wstock vsabavat - scald sreekaransrinath ratulsarna osolmaz conhecendocontato hrdwdmrbl jayhickey jamesgroat gtsifrikas djangonavarro220 - azade-c andranik-sahakyan - adamgall jalehman jarvis-medmatic mneves75 regenrek tobiasbischoff MSch obviyus dbhurley - Asleep123 Iamadig imfing kitze nachoiacovino VACInc cash-echo-bot claude kiranjd pcty-nextgen-service-account minghinmatthewlam - ngutman onutc oswalpalash snopoke ManuelHettich loukotal hugobarauna AbhisekBasu1 emanuelst dantelex erikpr1994 antons RandyVentures - reeltimeapps fcatuhe maxsumrall carlulsoe alejandroOPI pasogott -

+ steipete thewilloftheshadow joshp123 mukhtharcm mcinteerj joaohlisboa mneves75 azade-c petter-b jalehman + julianengel xadenryan obviyus jeffersonwarrior Nachx639 dan-dr zats emanuelst Syhids mbelinky + maxsumrall pcty-nextgen-service-account hsrvc dbhurley oswalpalash kiranjd meaningfool scald sreekaransrinath sircrumpet + nachoiacovino jverdi fcatuhe omniwired dantelex CashWilliams claude AbhisekBasu1 kkarimi ngutman + onutc osolmaz nexty5870 RandyVentures ratulsarna snopoke timkrase VACInc vsabavat wstock + imfing buddyh gupsammy kitze minghinmatthewlam rafaelreis-r andranik-sahakyan antons Asleep123 djangonavarro220 + cash-echo-bot erikpr1994 gtsifrikas hugobarauna Iamadig jamesgroat jayhickey jonasjancarik loukotal ManuelHettich + hrdwdmrbl conhecendocontato MSch reeltimeapps mrdbstn +

diff --git a/docs/tools/elevated.md b/docs/tools/elevated.md index b95a9eb78..a88d5ea9c 100644 --- a/docs/tools/elevated.md +++ b/docs/tools/elevated.md @@ -19,6 +19,7 @@ read_when: - Send a message that is **only** the directive (whitespace allowed), e.g. `/elevated on`. - Confirmation reply is sent (`Elevated mode enabled.` / `Elevated mode disabled.`). - If elevated access is disabled or the sender is not on the approved allowlist, the directive replies `elevated is not available right now.` and does not change session state. +- Send `/elevated` (or `/elevated:`) with no argument to see the current elevated level. ## Availability + allowlists - Feature gate: `agent.elevated.enabled` (default can be off via config even if the code supports it). diff --git a/docs/tools/slash-commands.md b/docs/tools/slash-commands.md index 58af62b71..9f5715d41 100644 --- a/docs/tools/slash-commands.md +++ b/docs/tools/slash-commands.md @@ -48,6 +48,9 @@ Text + native (when enabled): Text-only: - `/compact [instructions]` (see [/concepts/compaction](/concepts/compaction)) +Notes: +- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`). + ## Surface notes - **Text commands** run in the normal chat session (DMs share `main`, groups have their own session). diff --git a/docs/tools/thinking.md b/docs/tools/thinking.md index 9ac0980ca..e43701566 100644 --- a/docs/tools/thinking.md +++ b/docs/tools/thinking.md @@ -24,6 +24,7 @@ read_when: - Send a message that is **only** the directive (whitespace allowed), e.g. `/think:medium` or `/t high`. - That sticks for the current session (per-sender by default); cleared by `/think:off` or session idle reset. - Confirmation reply is sent (`Thinking level set to high.` / `Thinking disabled.`). If the level is invalid (e.g. `/thinking big`), the command is rejected with a hint and the session state is left unchanged. +- Send `/think` (or `/think:`) with no argument to see the current thinking level. ## Application by agent - **Embedded Pi**: the resolved level is passed to the in-process Pi agent runtime. @@ -32,6 +33,7 @@ read_when: - Levels: `on|full` or `off` (default). - Directive-only message toggles session verbose and replies `Verbose logging enabled.` / `Verbose logging disabled.`; invalid levels return a hint without changing state. - Inline directive affects only that message; session/global defaults apply otherwise. +- Send `/verbose` (or `/verbose:`) with no argument to see the current verbose level. - When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool result back as its own metadata-only message, prefixed with ` : ` when available (path/command); the tool output itself is not forwarded. These tool summaries are sent as soon as each tool finishes (separate bubbles), not as streaming deltas. If you toggle `/verbose on|off` while a run is in-flight, subsequent tool bubbles honor the new setting. ## Reasoning visibility (/reasoning) @@ -40,6 +42,7 @@ read_when: - When enabled, any model-provided reasoning content is appended as a separate italic block. - `stream` (Telegram only): streams reasoning into the Telegram draft bubble while the reply is generating, then sends the final answer without reasoning. - Alias: `/reason`. +- Send `/reasoning` (or `/reasoning:`) with no argument to see the current reasoning level. ## Related - Elevated mode docs live in [`docs/elevated.md`](/tools/elevated). diff --git a/src/auto-reply/command-detection.test.ts b/src/auto-reply/command-detection.test.ts index 755da8b12..e8a14a898 100644 --- a/src/auto-reply/command-detection.test.ts +++ b/src/auto-reply/command-detection.test.ts @@ -9,7 +9,12 @@ describe("control command parsing", () => { hasCommand: true, mode: "allow", }); + expect(parseSendPolicyCommand("/send: on")).toEqual({ + hasCommand: true, + mode: "allow", + }); expect(parseSendPolicyCommand("/send")).toEqual({ hasCommand: true }); + expect(parseSendPolicyCommand("/send:")).toEqual({ hasCommand: true }); expect(parseSendPolicyCommand("send on")).toEqual({ hasCommand: false }); expect(parseSendPolicyCommand("send")).toEqual({ hasCommand: false }); }); @@ -19,6 +24,13 @@ describe("control command parsing", () => { hasCommand: true, mode: "mention", }); + expect(parseActivationCommand("/activation: mention")).toEqual({ + hasCommand: true, + mode: "mention", + }); + expect(parseActivationCommand("/activation:")).toEqual({ + hasCommand: true, + }); expect(parseActivationCommand("activation mention")).toEqual({ hasCommand: false, }); @@ -28,8 +40,10 @@ describe("control command parsing", () => { expect(hasControlCommand("/send")).toBe(true); expect(hasControlCommand("send")).toBe(false); expect(hasControlCommand("/help")).toBe(true); + expect(hasControlCommand("/help:")).toBe(true); expect(hasControlCommand("help")).toBe(false); expect(hasControlCommand("/status")).toBe(true); + expect(hasControlCommand("/status:")).toBe(true); expect(hasControlCommand("status")).toBe(false); }); diff --git a/src/auto-reply/command-detection.ts b/src/auto-reply/command-detection.ts index ae6279459..e9345a5d9 100644 --- a/src/auto-reply/command-detection.ts +++ b/src/auto-reply/command-detection.ts @@ -1,17 +1,19 @@ -import { listChatCommands } from "./commands-registry.js"; +import { listChatCommands, normalizeCommandBody } from "./commands-registry.js"; export function hasControlCommand(text?: string): boolean { if (!text) return false; const trimmed = text.trim(); if (!trimmed) return false; - const lowered = trimmed.toLowerCase(); + const normalizedBody = normalizeCommandBody(trimmed); + if (!normalizedBody) return false; + const lowered = normalizedBody.toLowerCase(); for (const command of listChatCommands()) { for (const alias of command.textAliases) { const normalized = alias.trim().toLowerCase(); if (!normalized) continue; if (lowered === normalized) return true; if (command.acceptsArgs && lowered.startsWith(normalized)) { - const nextChar = trimmed.charAt(normalized.length); + const nextChar = normalizedBody.charAt(normalized.length); if (nextChar && /\s/.test(nextChar)) return true; } } diff --git a/src/auto-reply/commands-registry.test.ts b/src/auto-reply/commands-registry.test.ts index 394540a1d..58bfa00c0 100644 --- a/src/auto-reply/commands-registry.test.ts +++ b/src/auto-reply/commands-registry.test.ts @@ -23,7 +23,9 @@ describe("commands registry", () => { const detection = getCommandDetection(); expect(detection.exact.has("/help")).toBe(true); expect(detection.regex.test("/status")).toBe(true); + expect(detection.regex.test("/status:")).toBe(true); expect(detection.regex.test("/stop")).toBe(true); + expect(detection.regex.test("/send:")).toBe(true); expect(detection.regex.test("try /status")).toBe(false); }); diff --git a/src/auto-reply/commands-registry.ts b/src/auto-reply/commands-registry.ts index 175369024..8fbbe611e 100644 --- a/src/auto-reply/commands-registry.ts +++ b/src/auto-reply/commands-registry.ts @@ -148,6 +148,16 @@ export function buildCommandText(commandName: string, args?: string): string { return trimmedArgs ? `/${commandName} ${trimmedArgs}` : `/${commandName}`; } +export function normalizeCommandBody(raw: string): string { + const trimmed = raw.trim(); + if (!trimmed.startsWith("/")) return trimmed; + const match = trimmed.match(/^\/([^\s:]+)\s*:(.*)$/); + if (!match) return trimmed; + const [, command, rest] = match; + const normalizedRest = rest.trimStart(); + return normalizedRest ? `/${command} ${normalizedRest}` : `/${command}`; +} + export function getCommandDetection(): { exact: Set; regex: RegExp } { if (cachedDetection) return cachedDetection; const exact = new Set(); @@ -160,9 +170,9 @@ export function getCommandDetection(): { exact: Set; regex: RegExp } { const escaped = escapeRegExp(normalized); if (!escaped) continue; if (command.acceptsArgs) { - patterns.push(`${escaped}(?:\\s+.+)?`); + patterns.push(`${escaped}(?:\\s+.+|\\s*:\\s*.*)?`); } else { - patterns.push(escaped); + patterns.push(`${escaped}(?:\\s*:\\s*)?`); } } } diff --git a/src/auto-reply/group-activation.ts b/src/auto-reply/group-activation.ts index b60ae0e20..f05075099 100644 --- a/src/auto-reply/group-activation.ts +++ b/src/auto-reply/group-activation.ts @@ -16,8 +16,11 @@ export function parseActivationCommand(raw?: string): { if (!raw) return { hasCommand: false }; const trimmed = raw.trim(); if (!trimmed) return { hasCommand: false }; - const match = trimmed.match(/^\/activation(?:\s+([a-zA-Z]+))?\s*$/i); + const match = trimmed.match( + /^\/activation(?:\s*:\s*([a-zA-Z]+)?\s*|\s+([a-zA-Z]+)\s*)?$/i, + ); if (!match) return { hasCommand: false }; - const mode = normalizeGroupActivation(match[1]); + const token = match[1] ?? match[2]; + const mode = normalizeGroupActivation(token); return { hasCommand: true, mode }; } diff --git a/src/auto-reply/model.test.ts b/src/auto-reply/model.test.ts index 85a3b3560..6737349aa 100644 --- a/src/auto-reply/model.test.ts +++ b/src/auto-reply/model.test.ts @@ -40,6 +40,15 @@ describe("extractModelDirective", () => { expect(result.cleaned).toBe(""); }); + it("recognizes /gpt: as model directive when alias is configured", () => { + const result = extractModelDirective("/gpt:", { + aliases: ["gpt", "sonnet", "opus"], + }); + expect(result.hasDirective).toBe(true); + expect(result.rawModel).toBe("gpt"); + expect(result.cleaned).toBe(""); + }); + it("recognizes /sonnet as model directive", () => { const result = extractModelDirective("/sonnet", { aliases: ["gpt", "sonnet", "opus"], diff --git a/src/auto-reply/model.ts b/src/auto-reply/model.ts index f85cb4ba5..814e258e7 100644 --- a/src/auto-reply/model.ts +++ b/src/auto-reply/model.ts @@ -25,7 +25,7 @@ export function extractModelDirective( ? null : body.match( new RegExp( - `(?:^|\\s)\\/(${aliases.map(escapeRegExp).join("|")})(?=$|\\s|:)`, + `(?:^|\\s)\\/(${aliases.map(escapeRegExp).join("|")})(?=$|\\s|:)(?:\\s*:\\s*)?`, "i", ), ); diff --git a/src/auto-reply/reply.directive.test.ts b/src/auto-reply/reply.directive.test.ts index 1f8d44242..fdeb9e8e0 100644 --- a/src/auto-reply/reply.directive.test.ts +++ b/src/auto-reply/reply.directive.test.ts @@ -143,6 +143,38 @@ describe("directive parsing", () => { expect(res.thinkLevel).toBeUndefined(); }); + it("matches think with no argument and consumes colon", () => { + const res = extractThinkDirective("/think:"); + expect(res.hasDirective).toBe(true); + expect(res.thinkLevel).toBeUndefined(); + expect(res.rawLevel).toBeUndefined(); + expect(res.cleaned).toBe(""); + }); + + it("matches verbose with no argument", () => { + const res = extractVerboseDirective("/verbose:"); + expect(res.hasDirective).toBe(true); + expect(res.verboseLevel).toBeUndefined(); + expect(res.rawLevel).toBeUndefined(); + expect(res.cleaned).toBe(""); + }); + + it("matches reasoning with no argument", () => { + const res = extractReasoningDirective("/reasoning:"); + expect(res.hasDirective).toBe(true); + expect(res.reasoningLevel).toBeUndefined(); + expect(res.rawLevel).toBeUndefined(); + expect(res.cleaned).toBe(""); + }); + + it("matches elevated with no argument", () => { + const res = extractElevatedDirective("/elevated:"); + expect(res.hasDirective).toBe(true); + expect(res.elevatedLevel).toBeUndefined(); + expect(res.rawLevel).toBeUndefined(); + expect(res.cleaned).toBe(""); + }); + it("matches queue directive", () => { const res = extractQueueDirective("please /queue interrupt now"); expect(res.hasDirective).toBe(true); @@ -419,6 +451,84 @@ describe("directive parsing", () => { }); }); + it("shows current verbose level when /verbose has no argument", async () => { + await withTempHome(async (home) => { + vi.mocked(runEmbeddedPiAgent).mockReset(); + + const res = await getReplyFromConfig( + { Body: "/verbose", From: "+1222", To: "+1222" }, + {}, + { + agent: { + model: "anthropic/claude-opus-4-5", + workspace: path.join(home, "clawd"), + verboseDefault: "on", + }, + session: { store: path.join(home, "sessions.json") }, + }, + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toContain("Current verbose level: on"); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + + it("shows current reasoning level when /reasoning has no argument", async () => { + await withTempHome(async (home) => { + vi.mocked(runEmbeddedPiAgent).mockReset(); + + const res = await getReplyFromConfig( + { Body: "/reasoning", From: "+1222", To: "+1222" }, + {}, + { + agent: { + model: "anthropic/claude-opus-4-5", + workspace: path.join(home, "clawd"), + }, + session: { store: path.join(home, "sessions.json") }, + }, + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toContain("Current reasoning level: off"); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + + it("shows current elevated level when /elevated has no argument", async () => { + await withTempHome(async (home) => { + vi.mocked(runEmbeddedPiAgent).mockReset(); + + const res = await getReplyFromConfig( + { + Body: "/elevated", + From: "+1222", + To: "+1222", + Provider: "whatsapp", + SenderE164: "+1222", + }, + {}, + { + agent: { + model: "anthropic/claude-opus-4-5", + workspace: path.join(home, "clawd"), + elevatedDefault: "on", + elevated: { + allowFrom: { whatsapp: ["+1222"] }, + }, + }, + whatsapp: { allowFrom: ["+1222"] }, + session: { store: path.join(home, "sessions.json") }, + }, + ); + + const text = Array.isArray(res) ? res[0]?.text : res?.text; + expect(text).toContain("Current elevated level: on"); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + it("rejects invalid elevated level", async () => { await withTempHome(async (home) => { vi.mocked(runEmbeddedPiAgent).mockReset(); diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index a63c33cbd..1a8511174 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -472,6 +472,14 @@ export async function getReplyFromConfig( const currentThinkLevel = (sessionEntry?.thinkingLevel as ThinkLevel | undefined) ?? (agentCfg?.thinkingDefault as ThinkLevel | undefined); + const currentVerboseLevel = + (sessionEntry?.verboseLevel as VerboseLevel | undefined) ?? + (agentCfg?.verboseDefault as VerboseLevel | undefined); + const currentReasoningLevel = + (sessionEntry?.reasoningLevel as ReasoningLevel | undefined) ?? "off"; + const currentElevatedLevel = + (sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ?? + (agentCfg?.elevatedDefault as ElevatedLevel | undefined); const directiveReply = await handleDirectiveOnly({ cfg, directives, @@ -492,6 +500,9 @@ export async function getReplyFromConfig( initialModelLabel, formatModelSwitchEvent, currentThinkLevel, + currentVerboseLevel, + currentReasoningLevel, + currentElevatedLevel, }); typing.cleanup(); return directiveReply; diff --git a/src/auto-reply/reply/commands.ts b/src/auto-reply/reply/commands.ts index 30152dda9..28d78230e 100644 --- a/src/auto-reply/reply/commands.ts +++ b/src/auto-reply/reply/commands.ts @@ -30,7 +30,10 @@ import { parseAgentSessionKey } from "../../routing/session-key.js"; import { resolveSendPolicy } from "../../sessions/send-policy.js"; import { normalizeE164 } from "../../utils.js"; import { resolveCommandAuthorization } from "../command-auth.js"; -import { shouldHandleTextCommands } from "../commands-registry.js"; +import { + normalizeCommandBody, + shouldHandleTextCommands, +} from "../commands-registry.js"; import { normalizeGroupActivation, parseActivationCommand, @@ -154,9 +157,9 @@ export function buildCommandContext(params: { const abortKey = sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined); const rawBodyNormalized = triggerBodyNormalized; - const commandBodyNormalized = isGroup - ? stripMentions(rawBodyNormalized, ctx, cfg) - : rawBodyNormalized; + const commandBodyNormalized = normalizeCommandBody( + isGroup ? stripMentions(rawBodyNormalized, ctx, cfg) : rawBodyNormalized, + ); return { surface, diff --git a/src/auto-reply/reply/directive-handling.ts b/src/auto-reply/reply/directive-handling.ts index 9b0276786..1248d7bcb 100644 --- a/src/auto-reply/reply/directive-handling.ts +++ b/src/auto-reply/reply/directive-handling.ts @@ -310,6 +310,9 @@ export async function handleDirectiveOnly(params: { initialModelLabel: string; formatModelSwitchEvent: (label: string, alias?: string) => string; currentThinkLevel?: ThinkLevel; + currentVerboseLevel?: VerboseLevel; + currentReasoningLevel?: ReasoningLevel; + currentElevatedLevel?: ElevatedLevel; }): Promise { const { directives, @@ -328,6 +331,9 @@ export async function handleDirectiveOnly(params: { initialModelLabel, formatModelSwitchEvent, currentThinkLevel, + currentVerboseLevel, + currentReasoningLevel, + currentElevatedLevel, } = params; if (directives.hasModelDirective) { @@ -391,18 +397,33 @@ export async function handleDirectiveOnly(params: { }; } if (directives.hasVerboseDirective && !directives.verboseLevel) { + if (!directives.rawVerboseLevel) { + const level = currentVerboseLevel ?? "off"; + return { text: `Current verbose level: ${level}.` }; + } return { - text: `Unrecognized verbose level "${directives.rawVerboseLevel ?? ""}". Valid levels: off, on.`, + text: `Unrecognized verbose level "${directives.rawVerboseLevel}". Valid levels: off, on.`, }; } if (directives.hasReasoningDirective && !directives.reasoningLevel) { + if (!directives.rawReasoningLevel) { + const level = currentReasoningLevel ?? "off"; + return { text: `Current reasoning level: ${level}.` }; + } return { - text: `Unrecognized reasoning level "${directives.rawReasoningLevel ?? ""}". Valid levels: on, off, stream.`, + text: `Unrecognized reasoning level "${directives.rawReasoningLevel}". Valid levels: on, off, stream.`, }; } if (directives.hasElevatedDirective && !directives.elevatedLevel) { + if (!directives.rawElevatedLevel) { + if (!elevatedEnabled || !elevatedAllowed) { + return { text: "elevated is not available right now." }; + } + const level = currentElevatedLevel ?? "off"; + return { text: `Current elevated level: ${level}.` }; + } return { - text: `Unrecognized elevated level "${directives.rawElevatedLevel ?? ""}". Valid levels: off, on.`, + text: `Unrecognized elevated level "${directives.rawElevatedLevel}". Valid levels: off, on.`, }; } if ( diff --git a/src/auto-reply/reply/directives.ts b/src/auto-reply/reply/directives.ts index 613ded005..ef07dbf88 100644 --- a/src/auto-reply/reply/directives.ts +++ b/src/auto-reply/reply/directives.ts @@ -18,7 +18,7 @@ export function extractThinkDirective(body?: string): { if (!body) return { cleaned: "", hasDirective: false }; // Match with optional argument - require word boundary via lookahead after keyword const match = body.match( - /(?:^|\s)\/(?:thinking|think|t)(?=$|\s|:)(?:\s*:?\s*([a-zA-Z-]+)\b)?/i, + /(?:^|\s)\/(?:thinking|think|t)(?=$|\s|:)(?:\s*:?\s*(?:([a-zA-Z-]+)\b)?)?/i, ); const thinkLevel = normalizeThinkLevel(match?.[1]); const cleaned = match @@ -40,7 +40,7 @@ export function extractVerboseDirective(body?: string): { } { if (!body) return { cleaned: "", hasDirective: false }; const match = body.match( - /(?:^|\s)\/(?:verbose|v)(?=$|\s|:)\s*:?\s*([a-zA-Z-]+)\b/i, + /(?:^|\s)\/(?:verbose|v)(?=$|\s|:)(?:\s*:?\s*(?:([a-zA-Z-]+)\b)?)?/i, ); const verboseLevel = normalizeVerboseLevel(match?.[1]); const cleaned = match @@ -62,7 +62,7 @@ export function extractElevatedDirective(body?: string): { } { if (!body) return { cleaned: "", hasDirective: false }; const match = body.match( - /(?:^|\s)\/(?:elevated|elev)(?=$|\s|:)\s*:?\s*([a-zA-Z-]+)\b/i, + /(?:^|\s)\/(?:elevated|elev)(?=$|\s|:)(?:\s*:?\s*(?:([a-zA-Z-]+)\b)?)?/i, ); const elevatedLevel = normalizeElevatedLevel(match?.[1]); const cleaned = match @@ -84,7 +84,7 @@ export function extractReasoningDirective(body?: string): { } { if (!body) return { cleaned: "", hasDirective: false }; const match = body.match( - /(?:^|\s)\/(?:reasoning|reason)(?=$|\s|:)\s*:?\s*([a-zA-Z-]+)\b/i, + /(?:^|\s)\/(?:reasoning|reason)(?=$|\s|:)(?:\s*:?\s*(?:([a-zA-Z-]+)\b)?)?/i, ); const reasoningLevel = normalizeReasoningLevel(match?.[1]); const cleaned = match @@ -103,7 +103,7 @@ export function extractStatusDirective(body?: string): { hasDirective: boolean; } { if (!body) return { cleaned: "", hasDirective: false }; - const match = body.match(/(?:^|\s)\/status(?=$|\s|:)\b/i); + const match = body.match(/(?:^|\s)\/status(?=$|\s|:)(?:\s*:\s*)?/i); const cleaned = match ? body.replace(match[0], "").replace(/\s+/g, " ").trim() : body.trim(); diff --git a/src/auto-reply/send-policy.ts b/src/auto-reply/send-policy.ts index 272720949..1c7e7ff31 100644 --- a/src/auto-reply/send-policy.ts +++ b/src/auto-reply/send-policy.ts @@ -17,9 +17,11 @@ export function parseSendPolicyCommand(raw?: string): { if (!raw) return { hasCommand: false }; const trimmed = raw.trim(); if (!trimmed) return { hasCommand: false }; - const match = trimmed.match(/^\/send(?:\s+([a-zA-Z]+))?\s*$/i); + const match = trimmed.match( + /^\/send(?:\s*:\s*([a-zA-Z]+)?\s*|\s+([a-zA-Z]+)\s*)?$/i, + ); if (!match) return { hasCommand: false }; - const token = match[1]?.trim().toLowerCase(); + const token = (match[1] ?? match[2])?.trim().toLowerCase(); if (!token) return { hasCommand: true }; if (token === "inherit" || token === "default" || token === "reset") { return { hasCommand: true, mode: "inherit" };