/** * brainbank search — Semantic search (vector) * brainbank hsearch — Hybrid search (vector + BM25) * brainbank ksearch — Keyword search (BM25) * * Source filtering: * --code 10 Max code results * --git 0 Skip git results * --docs 5 Max document results * --notes 10 Custom plugin results * --slack_messages 5 Custom collection results * * Any -- flag is treated as a source filter. */ import { c, args, getFlag, stripFlags, printResults } from '@/cli/utils.ts'; import { createBrain } from '@/cli/factory/index.ts'; import { tryServerSearch } from '@/cli/server-client.ts'; /** * Parse dynamic source flags: each `-- ` becomes `{ name: number }`. * * Known non-source flags (--repo, --depth, etc.) are excluded. * Returns sources map + the query string (positional args). */ function parseSourceFlags(): { sources: Record; query: string } { const NON_SOURCE_FLAGS = new Set([ 'repo', 'depth', 'collection', 'pattern', 'context', 'name', 'keep', 'pruner', 'only', 'docs-path', 'mode', 'limit', 'ignore', 'include', 'meta', 'k', 'yes', 'y', 'force', 'verbose', 'path', ]); const sources: Record = {}; const positional: string[] = []; for (let i = 0; i < args.length; i++) { if (args[i].startsWith('--')) { const name = args[i].slice(2); // Skip boolean flags if (name === 'yes' || name === 'force' || name === 'verbose') continue; // If next arg is a number and flag is not a known non-source flag const next = args[i + 1]; if (next !== undefined && /^\d+$/.test(next) && !NON_SOURCE_FLAGS.has(name)) { sources[name] = parseInt(next, 10); i++; // skip the value continue; } // Known value flag — skip its value if (NON_SOURCE_FLAGS.has(name) && next !== undefined && !next.startsWith('--')) { i++; } continue; } positional.push(args[i]); } const query = positional.slice(1).join(' '); // skip command name return { sources, query }; } /** Parse --path as comma-separated list of path prefixes. */ function parsePaths(): string | string[] | undefined { const raw = getFlag('path'); if (!raw) return undefined; const paths = raw.split(',').map(p => p.trim()).filter(Boolean); return paths.length === 1 ? paths[0] : paths; } /** Print active source and path filters. */ function printFilterInfo(sources: Record, pathPrefix?: string | string[]): void { const parts: string[] = []; const entries = Object.entries(sources); if (entries.length > 0) parts.push(...entries.map(([k, v]) => `${k}=${v}`)); if (pathPrefix) { const paths = Array.isArray(pathPrefix) ? pathPrefix : [pathPrefix]; parts.push(`path=${paths.join(',')}`); } if (parts.length > 0) console.log(c.dim(` Filters: ${parts.join(', ')}`)); } /** Build search options from sources map + optional path prefix(es). */ function buildSearchOptions(sources: Record, pathPrefix?: string | string[]): { sources: Record; source: 'cli'; pathPrefix?: string | string[] } { const opts: { sources: Record; source: 'cli'; pathPrefix?: string | string[] } = { sources: Object.keys(sources).length > 0 ? sources : {}, source: 'cli', }; if (pathPrefix) opts.pathPrefix = pathPrefix; return opts; } export async function cmdSearch(): Promise { const { sources, query } = parseSourceFlags(); if (!query) { console.log(c.red('Usage: brainbank search [--repo ] [--path ] [--code ] [--git ]')); process.exit(1); } const pathPrefix = parsePaths(); const repo = getFlag('repo') ?? process.cwd(); // Try daemon delegation first (hot HNSW) const delegated = await tryServerSearch('search', { query, repo, sources: Object.keys(sources).length > 0 ? sources : undefined, pathPrefix, }); if (delegated) { console.log(c.bold(`\n━━━ BrainBank Search: "${query}" ━━━\n`)); printFilterInfo(sources, pathPrefix); printResults(delegated); return; } // Fall back to local const brain = await createBrain(); console.log(c.bold(`\n━━━ BrainBank Search: "${query}" ━━━\n`)); printFilterInfo(sources, pathPrefix); const opts = buildSearchOptions(sources, pathPrefix); const results = await brain.search(query, opts); printResults(results); brain.close(); } export async function cmdHybridSearch(): Promise { const { sources, query } = parseSourceFlags(); if (!query) { console.log(c.red('Usage: brainbank hsearch [--repo ] [--path ] [--code ] [--git ] [--docs ]')); process.exit(1); } const pathPrefix = parsePaths(); const repo = getFlag('repo') ?? process.cwd(); // Try daemon delegation first (hot HNSW) const delegated = await tryServerSearch('hybrid', { query, repo, sources: Object.keys(sources).length > 0 ? sources : undefined, pathPrefix, }); if (delegated) { console.log(c.bold(`\n━━━ BrainBank Hybrid Search: "${query}" ━━━`)); console.log(c.dim(` Mode: vector + BM25 → Reciprocal Rank Fusion`)); printFilterInfo(sources, pathPrefix); console.log(''); printResults(delegated); return; } // Fall back to local const brain = await createBrain(); console.log(c.bold(`\n━━━ BrainBank Hybrid Search: "${query}" ━━━`)); console.log(c.dim(` Mode: vector + BM25 → Reciprocal Rank Fusion`)); printFilterInfo(sources, pathPrefix); console.log(''); const opts = buildSearchOptions(sources, pathPrefix); const results = await brain.hybridSearch(query, opts); printResults(results); brain.close(); } export async function cmdKeywordSearch(): Promise { const { sources, query } = parseSourceFlags(); if (!query) { console.log(c.red('Usage: brainbank ksearch [--repo ] [--path ] [--code ] [--git ]')); process.exit(1); } const pathPrefix = parsePaths(); const repo = getFlag('repo') ?? process.cwd(); // Try daemon delegation first (hot HNSW) const delegated = await tryServerSearch('keyword', { query, repo, sources: Object.keys(sources).length > 0 ? sources : undefined, pathPrefix, }); if (delegated) { console.log(c.bold(`\n━━━ BrainBank Keyword Search: "${query}" ━━━`)); console.log(c.dim(` Mode: BM25 full-text (instant)`)); printFilterInfo(sources, pathPrefix); console.log(''); printResults(delegated, 0.40); return; } // Fall back to local const brain = await createBrain(); await brain.initialize(); console.log(c.bold(`\n━━━ BrainBank Keyword Search: "${query}" ━━━`)); console.log(c.dim(` Mode: BM25 full-text (instant)`)); printFilterInfo(sources, pathPrefix); console.log(''); const opts = buildSearchOptions(sources, pathPrefix); const results = await brain.searchBM25(query, opts); printResults(results, 0.40); brain.close(); }