From 902c06fa79a9c6b0fba57275fde996efa18c4a59 Mon Sep 17 00:00:00 2001 From: breken-ai <312387581+breken-ai@users.noreply.github.com> Date: Mon, 28 Sep 2026 04:00:53 -0700 Subject: [PATCH 1/2] fix(grep): match every repeated expression --- src/hooks/grep-direct.ts | 13 ++++++++++++- tests/claude-code/grep-direct.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/hooks/grep-direct.ts b/src/hooks/grep-direct.ts index 2c8bb52be..c1f706f77 100644 --- a/src/hooks/grep-direct.ts +++ b/src/hooks/grep-direct.ts @@ -294,9 +294,20 @@ export function parseBashGrep(cmd: string): GrepParams | null { ti++; } - const pattern = explicitPatterns.length > 0 ? explicitPatterns[0] : tokens[ti]; + let pattern = explicitPatterns.length > 0 ? explicitPatterns[0] : tokens[ti]; if (!pattern) return null; + // grep treats repeated -e/--regexp options as alternative patterns. Keep + // them together for the downstream line matcher, which accepts one regex. + // Fixed-string patterns must be escaped before building the alternation. + if (explicitPatterns.length > 1) { + const alternatives = explicitPatterns.map((p) => + fixedString ? p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : p, + ); + pattern = alternatives.map((p) => `(?:${p})`).join("|"); + fixedString = false; + } + let target = explicitPatterns.length > 0 ? (tokens[ti] ?? "/") : (tokens[ti + 1] ?? "/"); if (target === "." || target === "./") target = "/"; diff --git a/tests/claude-code/grep-direct.test.ts b/tests/claude-code/grep-direct.test.ts index c4c2fa0b1..6d78f5641 100644 --- a/tests/claude-code/grep-direct.test.ts +++ b/tests/claude-code/grep-direct.test.ts @@ -419,6 +419,28 @@ describe("parseBashGrep", () => { }); }); +describe("parseBashGrep: repeated patterns", () => { + it("matches lines for every -e expression", async () => { + const parsed = parseBashGrep("grep -e apple -e banana /notes"); + expect(parsed).not.toBeNull(); + const api = { + query: vi.fn().mockResolvedValueOnce([{ path: "/notes/a.md", content: "apple pie\nbanana bread" }]), + } as any; + const result = await handleGrepDirect(api, "memory", "sessions", parsed!); + expect(result).toBe("apple pie\nbanana bread"); + }); + + it("keeps repeated -F expressions literal", async () => { + const parsed = parseBashGrep("grep -F -e a.b -e banana /notes"); + expect(parsed).not.toBeNull(); + const api = { + query: vi.fn().mockResolvedValueOnce([{ path: "/notes/a.md", content: "a.b\naxb\nbanana" }]), + } as any; + const result = await handleGrepDirect(api, "memory", "sessions", parsed!); + expect(result).toBe("a.b\nbanana"); + }); +}); + // ─── rg (ripgrep) ────────────────────────────────────────────────────────── // // Modern coding agents reach for `rg` by default for directory searches. From 40197e18c09bfda8d01c2a39fd14781ce12f2764 Mon Sep 17 00:00:00 2001 From: breken-ai <312387581+breken-ai@users.noreply.github.com> Date: Mon, 28 Sep 2026 04:13:59 -0700 Subject: [PATCH 2/2] fix: preserve grep prefilters for repeated patterns --- src/hooks/grep-direct.ts | 4 +++- src/shell/grep-core.ts | 11 ++++++----- tests/claude-code/grep-core.test.ts | 4 ++++ tests/claude-code/grep-direct.test.ts | 5 +++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/hooks/grep-direct.ts b/src/hooks/grep-direct.ts index c1f706f77..e0ffee318 100644 --- a/src/hooks/grep-direct.ts +++ b/src/hooks/grep-direct.ts @@ -304,7 +304,9 @@ export function parseBashGrep(cmd: string): GrepParams | null { const alternatives = explicitPatterns.map((p) => fixedString ? p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : p, ); - pattern = alternatives.map((p) => `(?:${p})`).join("|"); + // A bare alternation preserves grep's per-pattern OR semantics and keeps + // the alternatives visible to grep-core's SQL prefilter extractor. + pattern = alternatives.join("|"); fixedString = false; } diff --git a/src/shell/grep-core.ts b/src/shell/grep-core.ts index 02709ba9d..76098b5e3 100644 --- a/src/shell/grep-core.ts +++ b/src/shell/grep-core.ts @@ -614,11 +614,12 @@ export function extractRegexAlternationPrefilters(pattern: string): string[] | n if (escaped || !current) return null; parts.push(current); - const literals = [...new Set( - parts - .map((part) => extractRegexLiteralPrefilter(part)) - .filter((part): part is string => typeof part === "string" && part.length >= 2), - )]; + const branchLiterals = parts.map((part) => extractRegexLiteralPrefilter(part)); + // An OR prefilter must retain at least one safe anchor from every branch. + // Dropping an unanchored branch would turn it into a false-negative filter. + if (branchLiterals.some((literal) => literal === null)) return null; + + const literals = [...new Set(branchLiterals.filter((part): part is string => part !== null))]; return literals.length > 0 ? literals : null; } diff --git a/tests/claude-code/grep-core.test.ts b/tests/claude-code/grep-core.test.ts index 995ad1b5d..4ba77cd16 100644 --- a/tests/claude-code/grep-core.test.ts +++ b/tests/claude-code/grep-core.test.ts @@ -1010,6 +1010,10 @@ describe("regex literal prefilter", () => { expect(extractRegexAlternationPrefilters(".|.|.")).toBeNull(); }); + it("returns null when any branch has no safe literal anchor", () => { + expect(extractRegexAlternationPrefilters("apple|\\d+")).toBeNull(); + }); + it("returns null when input has no alternation pipe", () => { expect(extractRegexAlternationPrefilters("foobar")).toBeNull(); }); diff --git a/tests/claude-code/grep-direct.test.ts b/tests/claude-code/grep-direct.test.ts index 6d78f5641..2b4588a3b 100644 --- a/tests/claude-code/grep-direct.test.ts +++ b/tests/claude-code/grep-direct.test.ts @@ -428,6 +428,11 @@ describe("parseBashGrep: repeated patterns", () => { } as any; const result = await handleGrepDirect(api, "memory", "sessions", parsed!); expect(result).toBe("apple pie\nbanana bread"); + const sql = api.query.mock.calls[0][0] as string; + expect(sql).toContain("summary::text ILIKE '%apple%'"); + expect(sql).toContain("summary::text ILIKE '%banana%'"); + expect(sql).toContain("message::text ILIKE '%apple%'"); + expect(sql).toContain("message::text ILIKE '%banana%'"); }); it("keeps repeated -F expressions literal", async () => {