Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/shell/grep-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,18 @@ export function extractRegexAlternationPrefilters(pattern: string): string[] | n
}

export function buildGrepSearchOptions(params: GrepMatchParams, targetPath: string): SearchOptions {
// `grep -v` wants the lines that do NOT match, so a row that never contains
// the pattern is exactly the one it must keep. Skip every content prefilter
// and let the line-by-line refinement do the inversion.
if (params.invertMatch) {
return {
pathFilter: buildPathFilter(targetPath),
contentScanOnly: true,
likeOp: process.env.HIVEMIND_GREP_LIKE === "case-sensitive" ? "LIKE" : "ILIKE",
escapedPattern: sqlLike(params.pattern),
};
}

const hasRegexMeta = !params.fixedString && /[.*+?^${}()|[\]\\]/.test(params.pattern);
const literalPrefilter = hasRegexMeta ? extractRegexLiteralPrefilter(params.pattern) : null;
const alternationPrefilters = hasRegexMeta ? extractRegexAlternationPrefilters(params.pattern) : null;
Expand Down
18 changes: 18 additions & 0 deletions tests/claude-code/grep-core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,24 @@ describe("grepBothTables", () => {
expect(out).toContain("/summaries/a.md:trailing");
});

it("grep -v keeps files that never contain the pattern", async () => {
// Fake backend that applies the ILIKE content filter the way the server does:
// with `summary::text ILIKE '%foo%'` in the SQL, only rows containing foo return.
const stored = [
{ path: "/summaries/a.md", content: "foo line\nkeep a" },
{ path: "/summaries/b.md", content: "only b" },
];
const api = {
query: vi.fn(async (sql: string) =>
sql.includes("ILIKE '%foo%'")
? stored.filter((r) => r.content.toLowerCase().includes("foo"))
: stored),
} as any;
const out = await grepBothTables(api, "m", "s", { ...baseParams, invertMatch: true }, "/summaries");
// `grep -v foo` prints every line without foo, including all of b.md.
expect(out).toEqual(["/summaries/a.md:keep a", "/summaries/b.md:only b"]);
});

it("falls back to refined output when HIVEMIND_SEMANTIC_EMIT_ALL=false even with an embedding", async () => {
const prev = process.env.HIVEMIND_SEMANTIC_EMIT_ALL;
process.env.HIVEMIND_SEMANTIC_EMIT_ALL = "false";
Expand Down