From b5c604b7b7b2a59bd4ea3af97043160f8db2daa1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 6 Jan 2026 07:05:08 +0100 Subject: [PATCH] fix: require slash for control commands --- CHANGELOG.md | 1 + src/auto-reply/command-detection.test.ts | 35 ++++++++++++++++++++++++ src/auto-reply/command-detection.ts | 8 ------ src/auto-reply/group-activation.ts | 2 +- src/auto-reply/reply/commands.ts | 17 ++---------- src/auto-reply/send-policy.ts | 2 +- 6 files changed, 41 insertions(+), 24 deletions(-) create mode 100644 src/auto-reply/command-detection.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index dcb1d0825..acaa581fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior. ### Fixes +- Auto-reply: require slash for control commands to avoid false triggers in normal text. - Auto-reply: treat steer during compaction as a follow-up, queued until compaction completes. - Auth: lock auth profile refreshes to avoid multi-instance OAuth logouts; keep credentials on refresh failure. - Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins. diff --git a/src/auto-reply/command-detection.test.ts b/src/auto-reply/command-detection.test.ts new file mode 100644 index 000000000..5f7d758a9 --- /dev/null +++ b/src/auto-reply/command-detection.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import { hasControlCommand } from "./command-detection.js"; +import { parseActivationCommand } from "./group-activation.js"; +import { parseSendPolicyCommand } from "./send-policy.js"; + +describe("control command parsing", () => { + it("requires slash for send policy", () => { + expect(parseSendPolicyCommand("/send on")).toEqual({ + hasCommand: true, + mode: "allow", + }); + expect(parseSendPolicyCommand("/send")).toEqual({ hasCommand: true }); + expect(parseSendPolicyCommand("send on")).toEqual({ hasCommand: false }); + expect(parseSendPolicyCommand("send")).toEqual({ hasCommand: false }); + }); + + it("requires slash for activation", () => { + expect(parseActivationCommand("/activation mention")).toEqual({ + hasCommand: true, + mode: "mention", + }); + expect(parseActivationCommand("activation mention")).toEqual({ + hasCommand: false, + }); + }); + + it("treats bare commands as non-control", () => { + expect(hasControlCommand("/send")).toBe(true); + expect(hasControlCommand("send")).toBe(false); + expect(hasControlCommand("/help")).toBe(true); + expect(hasControlCommand("help")).toBe(false); + 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 1782f66f9..df3f1104c 100644 --- a/src/auto-reply/command-detection.ts +++ b/src/auto-reply/command-detection.ts @@ -2,21 +2,13 @@ const CONTROL_COMMAND_RE = /(?:^|\s)\/(?:status|help|thinking|think|t|verbose|v|elevated|elev|model|queue|activation|send|restart|reset|new|compact)(?=$|\s|:)\b/i; const CONTROL_COMMAND_EXACT = new Set([ - "help", "/help", - "status", "/status", - "restart", "/restart", - "activation", "/activation", - "send", "/send", - "reset", "/reset", - "new", "/new", - "compact", "/compact", ]); diff --git a/src/auto-reply/group-activation.ts b/src/auto-reply/group-activation.ts index 9372da5fa..83f08a0d9 100644 --- a/src/auto-reply/group-activation.ts +++ b/src/auto-reply/group-activation.ts @@ -16,7 +16,7 @@ export function parseActivationCommand(raw?: string): { if (!raw) return { hasCommand: false }; const trimmed = raw.trim(); if (!trimmed) return { hasCommand: false }; - const match = trimmed.match(/^\/?activation\b(?:\s+([a-zA-Z]+))?/i); + const match = trimmed.match(/^\/activation\b(?:\s+([a-zA-Z]+))?/i); if (!match) return { hasCommand: false }; const mode = normalizeGroupActivation(match[1]); return { hasCommand: true, mode }; diff --git a/src/auto-reply/reply/commands.ts b/src/auto-reply/reply/commands.ts index 22cc7f7c8..7ade976b8 100644 --- a/src/auto-reply/reply/commands.ts +++ b/src/auto-reply/reply/commands.ts @@ -102,11 +102,7 @@ function extractCompactInstructions(params: { const trimmed = stripped.trim(); if (!trimmed) return undefined; const lowered = trimmed.toLowerCase(); - const prefix = lowered.startsWith("/compact") - ? "/compact" - : lowered.startsWith("compact") - ? "compact" - : null; + const prefix = lowered.startsWith("/compact") ? "/compact" : null; if (!prefix) return undefined; let rest = trimmed.slice(prefix.length).trimStart(); if (rest.startsWith(":")) rest = rest.slice(1).trimStart(); @@ -197,9 +193,7 @@ export async function handleCommands(params: { const resetRequested = command.commandBodyNormalized === "/reset" || - command.commandBodyNormalized === "reset" || - command.commandBodyNormalized === "/new" || - command.commandBodyNormalized === "new"; + command.commandBodyNormalized === "/new"; if (resetRequested && !command.isAuthorizedSender) { logVerbose( `Ignoring /reset from unauthorized sender: ${command.senderE164 || ""}`, @@ -300,7 +294,6 @@ export async function handleCommands(params: { if ( command.commandBodyNormalized === "/restart" || - command.commandBodyNormalized === "restart" || command.commandBodyNormalized.startsWith("/restart ") ) { if (!command.isAuthorizedSender) { @@ -320,7 +313,6 @@ export async function handleCommands(params: { const helpRequested = command.commandBodyNormalized === "/help" || - command.commandBodyNormalized === "help" || /(?:^|\s)\/help(?=$|\s|:)\b/i.test(command.commandBodyNormalized); if (helpRequested) { if (!command.isAuthorizedSender) { @@ -335,7 +327,6 @@ export async function handleCommands(params: { const statusRequested = directives.hasStatusDirective || command.commandBodyNormalized === "/status" || - command.commandBodyNormalized === "status" || command.commandBodyNormalized.startsWith("/status "); if (statusRequested) { if (!command.isAuthorizedSender) { @@ -383,9 +374,7 @@ export async function handleCommands(params: { const compactRequested = command.commandBodyNormalized === "/compact" || - command.commandBodyNormalized === "compact" || - command.commandBodyNormalized.startsWith("/compact ") || - command.commandBodyNormalized.startsWith("compact "); + command.commandBodyNormalized.startsWith("/compact "); if (compactRequested) { if (!command.isAuthorizedSender) { logVerbose( diff --git a/src/auto-reply/send-policy.ts b/src/auto-reply/send-policy.ts index 4b4ad6dbe..e7fb95d4c 100644 --- a/src/auto-reply/send-policy.ts +++ b/src/auto-reply/send-policy.ts @@ -17,7 +17,7 @@ export function parseSendPolicyCommand(raw?: string): { if (!raw) return { hasCommand: false }; const trimmed = raw.trim(); if (!trimmed) return { hasCommand: false }; - const match = trimmed.match(/^\/?send\b(?:\s+([a-zA-Z]+))?/i); + const match = trimmed.match(/^\/send\b(?:\s+([a-zA-Z]+))?/i); if (!match) return { hasCommand: false }; const token = match[1]?.trim().toLowerCase(); if (!token) return { hasCommand: true };