/** * brainbank query — Get formatted context for a task * * Primary command for semantic code search + LLM-pruned context. * * Flags: * --context General task context for the pruner (inline or @file) * --pruner Specific pruning instructions for the pruner * --code 20 Max code results (default: 20) * --git 5 Max git results * --no-git Skip git results * --no-code Skip code results * --path Filter results to files under path prefix(es) * --ignore Exclude paths (comma-separated or repeated) */ import * as fs from 'node:fs'; import { c, args, stripFlags, getFlag, getFlagAll } from '@/cli/utils.ts'; import { createBrain } from '@/cli/factory/index.ts'; import { tryServerContext } from '@/cli/server-client.ts'; /** Parse --code N, --git N, --no-git, --no-code flags into sources map. */ function parseSourceFlags(): Record { const NON_SOURCE = new Set([ 'repo', 'depth', 'collection', 'pattern', 'context', 'pruner', 'name', 'keep', 'only', 'docs-path', 'mode', 'limit', 'ignore', 'meta', 'k', 'yes', 'y', 'force', 'verbose', 'path', ]); const sources: Record = {}; for (let i = 0; i < args.length; i++) { if (!args[i].startsWith('--')) continue; const name = args[i].slice(2); // --no-git, --no-code → set to 0 if (name.startsWith('no-')) { sources[name.slice(3)] = 0; continue; } // --code 20, --git 5 const next = args[i + 1]; if (next !== undefined && /^\d+$/.test(next) && !NON_SOURCE.has(name)) { sources[name] = parseInt(next, 10); i++; } } return sources; } /** Read a flag value that supports inline string or @file references. */ function readFlagValue(flagName: string): string | undefined { const raw = getFlag(flagName); if (!raw) return undefined; if (raw.startsWith('@')) { const filePath = raw.slice(1); try { return fs.readFileSync(filePath, 'utf-8').trim(); } catch { console.error(c.red(`Cannot read ${flagName} file: ${filePath}`)); process.exit(1); } } return raw; } export async function cmdQuery(): Promise { const task = stripFlags(args).slice(1).join(' '); if (!task) { console.log(c.red('Usage: brainbank query ')); console.log(c.dim(' Options:')); console.log(c.dim(' --context General task context for the pruner')); console.log(c.dim(' --pruner Specific pruning focus')); console.log(c.dim(' --path Filter to files under path')); console.log(c.dim(' --code N --git N Source limits')); process.exit(1); } const sources = parseSourceFlags(); const rawPath = getFlag('path'); const pathPrefix = rawPath ? rawPath.split(',').map(p => p.trim()).filter(Boolean) : undefined; const normalizedPath = pathPrefix && pathPrefix.length === 1 ? pathPrefix[0] : pathPrefix; const ignorePaths = getFlagAll('ignore'); const repo = getFlag('repo'); // Parse --context and --pruner flags (both support inline or @file) const contextDesc = readFlagValue('context'); const prunerDesc = readFlagValue('pruner'); // Parse BrainBankQL field flags const fields = parseFieldFlags(); // Try HTTP server delegation first const serverResult = await tryServerContext({ task, repo: repo ?? process.cwd(), sources: Object.keys(sources).length > 0 ? sources : undefined, pathPrefix: normalizedPath, ignorePaths: ignorePaths.length > 0 ? ignorePaths : undefined, fields: Object.keys(fields).length > 0 ? fields : undefined, context: contextDesc, prunerContext: prunerDesc, }); if (serverResult !== null) { console.log(serverResult); return; } // Fall back to local const brain = await createBrain(); const result = await brain.getContext(task, { sources: Object.keys(sources).length > 0 ? sources : undefined, pathPrefix: normalizedPath, ignorePaths: ignorePaths.length > 0 ? ignorePaths : undefined, source: 'cli', fields: Object.keys(fields).length > 0 ? fields : undefined, context: contextDesc, prunerContext: prunerDesc, }); console.log(result); brain.close(); } /** Parse BrainBankQL field flags: --lines, --symbols, --compact, --no-callTree, --callTree.depth=N, etc. */ function parseFieldFlags(): Record { const FIELD_BOOLEANS = new Set(['lines', 'symbols', 'compact']); const FIELD_NEGATABLE = new Set(['callTree', 'imports']); const fields: Record = {}; for (let i = 0; i < args.length; i++) { if (!args[i].startsWith('--')) continue; const raw = args[i].slice(2); // --no-callTree, --no-imports → set to false if (raw.startsWith('no-')) { const name = raw.slice(3); if (FIELD_NEGATABLE.has(name)) { fields[name] = false; } continue; } // --callTree.depth=4 → { depth: 4 } const dotIdx = raw.indexOf('.'); if (dotIdx > 0) { const fieldName = raw.slice(0, dotIdx); const rest = raw.slice(dotIdx + 1); const eqIdx = rest.indexOf('='); if (eqIdx > 0) { const key = rest.slice(0, eqIdx); const val = parseInt(rest.slice(eqIdx + 1), 10); if (!isNaN(val)) { fields[fieldName] = { [key]: val }; } } continue; } // --lines, --symbols, --compact → true if (FIELD_BOOLEANS.has(raw)) { fields[raw] = true; } } return fields; }