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:
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
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" };