fix: require slash for control commands
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
- Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior.
|
- Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior.
|
||||||
|
|
||||||
### Fixes
|
### 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.
|
- 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.
|
- 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.
|
- Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins.
|
||||||
|
|||||||
35
src/auto-reply/command-detection.test.ts
Normal file
35
src/auto-reply/command-detection.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
/(?:^|\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([
|
const CONTROL_COMMAND_EXACT = new Set([
|
||||||
"help",
|
|
||||||
"/help",
|
"/help",
|
||||||
"status",
|
|
||||||
"/status",
|
"/status",
|
||||||
"restart",
|
|
||||||
"/restart",
|
"/restart",
|
||||||
"activation",
|
|
||||||
"/activation",
|
"/activation",
|
||||||
"send",
|
|
||||||
"/send",
|
"/send",
|
||||||
"reset",
|
|
||||||
"/reset",
|
"/reset",
|
||||||
"new",
|
|
||||||
"/new",
|
"/new",
|
||||||
"compact",
|
|
||||||
"/compact",
|
"/compact",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function parseActivationCommand(raw?: string): {
|
|||||||
if (!raw) return { hasCommand: false };
|
if (!raw) return { hasCommand: false };
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return { hasCommand: false };
|
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 };
|
if (!match) return { hasCommand: false };
|
||||||
const mode = normalizeGroupActivation(match[1]);
|
const mode = normalizeGroupActivation(match[1]);
|
||||||
return { hasCommand: true, mode };
|
return { hasCommand: true, mode };
|
||||||
|
|||||||
@@ -102,11 +102,7 @@ function extractCompactInstructions(params: {
|
|||||||
const trimmed = stripped.trim();
|
const trimmed = stripped.trim();
|
||||||
if (!trimmed) return undefined;
|
if (!trimmed) return undefined;
|
||||||
const lowered = trimmed.toLowerCase();
|
const lowered = trimmed.toLowerCase();
|
||||||
const prefix = lowered.startsWith("/compact")
|
const prefix = lowered.startsWith("/compact") ? "/compact" : null;
|
||||||
? "/compact"
|
|
||||||
: lowered.startsWith("compact")
|
|
||||||
? "compact"
|
|
||||||
: null;
|
|
||||||
if (!prefix) return undefined;
|
if (!prefix) return undefined;
|
||||||
let rest = trimmed.slice(prefix.length).trimStart();
|
let rest = trimmed.slice(prefix.length).trimStart();
|
||||||
if (rest.startsWith(":")) rest = rest.slice(1).trimStart();
|
if (rest.startsWith(":")) rest = rest.slice(1).trimStart();
|
||||||
@@ -197,9 +193,7 @@ export async function handleCommands(params: {
|
|||||||
|
|
||||||
const resetRequested =
|
const resetRequested =
|
||||||
command.commandBodyNormalized === "/reset" ||
|
command.commandBodyNormalized === "/reset" ||
|
||||||
command.commandBodyNormalized === "reset" ||
|
command.commandBodyNormalized === "/new";
|
||||||
command.commandBodyNormalized === "/new" ||
|
|
||||||
command.commandBodyNormalized === "new";
|
|
||||||
if (resetRequested && !command.isAuthorizedSender) {
|
if (resetRequested && !command.isAuthorizedSender) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`Ignoring /reset from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
|
`Ignoring /reset from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
|
||||||
@@ -300,7 +294,6 @@ export async function handleCommands(params: {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
command.commandBodyNormalized === "/restart" ||
|
command.commandBodyNormalized === "/restart" ||
|
||||||
command.commandBodyNormalized === "restart" ||
|
|
||||||
command.commandBodyNormalized.startsWith("/restart ")
|
command.commandBodyNormalized.startsWith("/restart ")
|
||||||
) {
|
) {
|
||||||
if (!command.isAuthorizedSender) {
|
if (!command.isAuthorizedSender) {
|
||||||
@@ -320,7 +313,6 @@ export async function handleCommands(params: {
|
|||||||
|
|
||||||
const helpRequested =
|
const helpRequested =
|
||||||
command.commandBodyNormalized === "/help" ||
|
command.commandBodyNormalized === "/help" ||
|
||||||
command.commandBodyNormalized === "help" ||
|
|
||||||
/(?:^|\s)\/help(?=$|\s|:)\b/i.test(command.commandBodyNormalized);
|
/(?:^|\s)\/help(?=$|\s|:)\b/i.test(command.commandBodyNormalized);
|
||||||
if (helpRequested) {
|
if (helpRequested) {
|
||||||
if (!command.isAuthorizedSender) {
|
if (!command.isAuthorizedSender) {
|
||||||
@@ -335,7 +327,6 @@ export async function handleCommands(params: {
|
|||||||
const statusRequested =
|
const statusRequested =
|
||||||
directives.hasStatusDirective ||
|
directives.hasStatusDirective ||
|
||||||
command.commandBodyNormalized === "/status" ||
|
command.commandBodyNormalized === "/status" ||
|
||||||
command.commandBodyNormalized === "status" ||
|
|
||||||
command.commandBodyNormalized.startsWith("/status ");
|
command.commandBodyNormalized.startsWith("/status ");
|
||||||
if (statusRequested) {
|
if (statusRequested) {
|
||||||
if (!command.isAuthorizedSender) {
|
if (!command.isAuthorizedSender) {
|
||||||
@@ -383,9 +374,7 @@ export async function handleCommands(params: {
|
|||||||
|
|
||||||
const compactRequested =
|
const compactRequested =
|
||||||
command.commandBodyNormalized === "/compact" ||
|
command.commandBodyNormalized === "/compact" ||
|
||||||
command.commandBodyNormalized === "compact" ||
|
command.commandBodyNormalized.startsWith("/compact ");
|
||||||
command.commandBodyNormalized.startsWith("/compact ") ||
|
|
||||||
command.commandBodyNormalized.startsWith("compact ");
|
|
||||||
if (compactRequested) {
|
if (compactRequested) {
|
||||||
if (!command.isAuthorizedSender) {
|
if (!command.isAuthorizedSender) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function parseSendPolicyCommand(raw?: string): {
|
|||||||
if (!raw) return { hasCommand: false };
|
if (!raw) return { hasCommand: false };
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return { hasCommand: false };
|
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 };
|
if (!match) return { hasCommand: false };
|
||||||
const token = match[1]?.trim().toLowerCase();
|
const token = match[1]?.trim().toLowerCase();
|
||||||
if (!token) return { hasCommand: true };
|
if (!token) return { hasCommand: true };
|
||||||
|
|||||||
Reference in New Issue
Block a user