diff --git a/src/auto-reply/reply.directive.test.ts b/src/auto-reply/reply.directive.test.ts new file mode 100644 index 000000000..925d5b975 --- /dev/null +++ b/src/auto-reply/reply.directive.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { extractVerboseDirective, extractThinkDirective } from "./reply.js"; + +describe("directive parsing", () => { + it("ignores verbose directive inside URL", () => { + const body = "https://x.com/verioussmith/status/1997066835133669687"; + const res = extractVerboseDirective(body); + expect(res.hasDirective).toBe(false); + expect(res.cleaned).toBe(body); + }); + + it("ignores think directive inside URL", () => { + const body = "see https://example.com/path/thinkstuff"; + const res = extractThinkDirective(body); + expect(res.hasDirective).toBe(false); + }); + + it("matches verbose with leading space", () => { + const res = extractVerboseDirective(" please /verbose on now"); + expect(res.hasDirective).toBe(true); + expect(res.verboseLevel).toBe("on"); + }); + + it("matches think at start of line", () => { + const res = extractThinkDirective("/think:high run slow"); + expect(res.hasDirective).toBe(true); + expect(res.thinkLevel).toBe("high"); + }); +}); diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index c4b2d7591..b52671c31 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -36,15 +36,18 @@ export type { GetReplyOptions, ReplyPayload } from "./types.js"; const ABORT_TRIGGERS = new Set(["stop", "esc", "abort", "wait", "exit"]); const ABORT_MEMORY = new Map(); -function extractThinkDirective(body?: string): { +export function extractThinkDirective(body?: string): { cleaned: string; thinkLevel?: ThinkLevel; rawLevel?: string; hasDirective: boolean; } { if (!body) return { cleaned: "", hasDirective: false }; - // Match the longest keyword first to avoid partial captures (e.g. "/think:high") - const match = body.match(/\/(?:thinking|think|t)\s*:?\s*([a-zA-Z-]+)\b/i); + // Match the longest keyword first to avoid partial captures (e.g. "/think:high"). + // Require start of string or whitespace before "/" to avoid catching URLs. + const match = body.match( + /(?:^|\s)\/(?:thinking|think|t)\s*:?\s*([a-zA-Z-]+)\b/i, + ); const thinkLevel = normalizeThinkLevel(match?.[1]); const cleaned = match ? body.replace(match[0], "").replace(/\s+/g, " ").trim() @@ -57,14 +60,15 @@ function extractThinkDirective(body?: string): { }; } -function extractVerboseDirective(body?: string): { +export function extractVerboseDirective(body?: string): { cleaned: string; verboseLevel?: VerboseLevel; rawLevel?: string; hasDirective: boolean; } { if (!body) return { cleaned: "", hasDirective: false }; - const match = body.match(/\/(?:verbose|v)\s*:?\s*([a-zA-Z-]+)\b/i); + // Require start or whitespace before "/verbose" to avoid matching URLs like /verioussmith. + const match = body.match(/(?:^|\s)\/(?:verbose|v)\s*:?\s*([a-zA-Z-]+)\b/i); const verboseLevel = normalizeVerboseLevel(match?.[1]); const cleaned = match ? body.replace(match[0], "").replace(/\s+/g, " ").trim()