brainbank 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  printResults,
24
24
  registerConfigCollections,
25
25
  stripFlags
26
- } from "./chunk-3UIWA32X.js";
26
+ } from "./chunk-I46ALO53.js";
27
27
  import "./chunk-M744PCJQ.js";
28
28
  import "./chunk-IMJJ2VEM.js";
29
29
  import {
@@ -2744,7 +2744,7 @@ async function cmdStats() {
2744
2744
  const repoPath = path5.resolve(getFlag("repo") ?? process.cwd());
2745
2745
  const dbPath = path5.join(repoPath, ".brainbank", "data", "brainbank.db");
2746
2746
  const configPath = path5.join(repoPath, ".brainbank", "config.json");
2747
- const { runStatsTui } = await import("./stats-tui-AD3AMYGV.js");
2747
+ const { runStatsTui } = await import("./stats-tui-RI42XJZ5.js");
2748
2748
  await runStatsTui(dbPath, repoPath, configPath);
2749
2749
  return;
2750
2750
  } catch (err) {
package/dist/index.js CHANGED
@@ -50,7 +50,7 @@ import {
50
50
  sanitizeFTS,
51
51
  vecToBuffer,
52
52
  withLock
53
- } from "./chunk-3UIWA32X.js";
53
+ } from "./chunk-I46ALO53.js";
54
54
  import {
55
55
  providerKey,
56
56
  resolveEmbedding
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createBrain,
3
3
  resetFactoryCache
4
- } from "./chunk-3UIWA32X.js";
4
+ } from "./chunk-I46ALO53.js";
5
5
  import "./chunk-M744PCJQ.js";
6
6
  import "./chunk-IMJJ2VEM.js";
7
7
  import {
@@ -218,13 +218,13 @@ async function getBrainBank(targetRepo) {
218
218
  __name(getBrainBank, "getBrainBank");
219
219
  var server = new McpServer({
220
220
  name: "brainbank",
221
- version: "0.1.3"
221
+ version: "0.1.5"
222
222
  });
223
223
  server.registerTool(
224
- "brainbank_context",
224
+ "brainbank_query",
225
225
  {
226
226
  title: "BrainBank Context",
227
- description: "Get a formatted knowledge context block for a task. Returns a Workflow Trace: search hits + full call tree with `called by` annotations, topologically ordered. All source code included \u2014 no trimming, no truncation.",
227
+ description: "Get AI-pruned semantic context for a task. Returns search hits + full call tree with `called by` annotations, topologically ordered. Pass `context` for general background and `pruner` for specific filtering instructions.",
228
228
  inputSchema: z.object({
229
229
  task: z.string().describe("Description of the task you need context for"),
230
230
  affectedFiles: z.array(z.string()).optional().default([]).describe("Files you plan to modify (improves co-edit suggestions)"),
@@ -270,59 +270,6 @@ server.registerTool(
270
270
  return { content: [{ type: "text", text: context }] };
271
271
  }
272
272
  );
273
- function formatSearchResults(results) {
274
- if (results.length === 0) return "No results found.";
275
- const lines = [];
276
- for (const r of results) {
277
- const score = Math.round(r.score * 100);
278
- if (r.type === "code") {
279
- const m = r.metadata;
280
- lines.push(`[CODE ${score}%] ${r.filePath} \u2014 ${m.name || m.chunkType} L${m.startLine}-${m.endLine}`);
281
- lines.push(r.content);
282
- lines.push("");
283
- } else if (r.type === "commit") {
284
- const m = r.metadata;
285
- lines.push(`[COMMIT ${score}%] ${m.shortHash} ${r.content} (${m.author})`);
286
- if (m.files?.length) lines.push(` Files: ${m.files.slice(0, 6).join(", ")}`);
287
- lines.push("");
288
- } else if (r.type === "document") {
289
- const ctx = r.context ? ` \u2014 ${r.context}` : "";
290
- lines.push(`[DOC ${score}%] ${r.filePath} [${r.metadata.collection}]${ctx}`);
291
- lines.push(r.content);
292
- lines.push("");
293
- } else if (r.type === "collection") {
294
- lines.push(`[COLLECTION ${score}%] ${r.metadata.collection ?? "kv"}`);
295
- lines.push(r.content);
296
- lines.push("");
297
- }
298
- }
299
- return lines.join("\n");
300
- }
301
- __name(formatSearchResults, "formatSearchResults");
302
- server.registerTool(
303
- "brainbank_search",
304
- {
305
- title: "BrainBank Search",
306
- description: "Hybrid search across the codebase: combines vector similarity + BM25 keyword matching via Reciprocal Rank Fusion. Returns code chunks, git commits, and documents ranked by relevance. Use for finding code, understanding systems, and locating files by meaning.",
307
- inputSchema: z.object({
308
- query: z.string().describe('Natural language search query (e.g. "authentication middleware", "WebSocket reconnection")'),
309
- repo: z.string().describe("Repository path (default: BRAINBANK_REPO)"),
310
- sources: z.record(z.number()).optional().describe("Per-source result limits (e.g. { code: 10, git: 0, docs: 5 }). Set source to 0 to skip it"),
311
- path: z.union([z.string(), z.array(z.string())]).optional().describe('Filter results to files under these path prefixes (e.g. "src/services" or ["src/", "lib/"])'),
312
- maxResults: z.number().optional().default(20).describe("Maximum total results to return")
313
- })
314
- },
315
- async ({ query, repo, sources, path: path2, maxResults }) => {
316
- const brainbank = await getBrainBank(repo);
317
- const opts = { source: "mcp" };
318
- if (sources) opts.sources = sources;
319
- if (path2) opts.pathPrefix = path2;
320
- const results = await brainbank.hybridSearch(query, opts);
321
- const limited = results.slice(0, maxResults);
322
- const text = formatSearchResults(limited);
323
- return { content: [{ type: "text", text }] };
324
- }
325
- );
326
273
  server.registerTool(
327
274
  "brainbank_index",
328
275
  {
package/dist/mcp.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mcp/mcp-server.ts","../src/mcp/workspace-pool.ts","../src/mcp/workspace-factory.ts"],"sourcesContent":["/**\n * BrainBank — MCP Server\n *\n * Exposes BrainBank as an MCP server via stdio transport.\n * Works with Google Antigravity, Claude Desktop, and any MCP-compatible client.\n *\n * Usage in mcp_config.json:\n * {\n * \"mcpServers\": {\n * \"brainbank\": {\n * \"command\": \"brainbank-mcp\"\n * }\n * }\n * }\n *\n * Tools:\n * brainbank_context — Workflow Trace: search + call tree + called-by annotations\n * brainbank_search — Hybrid search (vector + BM25 → RRF), returns structured results\n *\n * Indexing is handled by the CLI (`brainbank index`) — not exposed as an MCP tool.\n */\n\nimport type { SearchResult } from '@/types.ts';\nimport type { SearchOptions } from '@/search/types.ts';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod/v3';\n\nimport { WorkspacePool } from './workspace-pool.ts';\nimport { createWorkspaceBrain, resolveRepoPath } from './workspace-factory.ts';\n\n\n\n// ── Multi-Workspace BrainBank Pool ─────────────────────\n\nconst pool = new WorkspacePool({\n factory: createWorkspaceBrain,\n maxMemoryMB: parseInt(process.env.BRAINBANK_MAX_MEMORY_MB ?? '2048', 10),\n ttlMinutes: parseInt(process.env.BRAINBANK_TTL_MINUTES ?? '30', 10),\n onError: (repo, err) => {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`BrainBank pool error [${repo}]: ${msg}`);\n },\n});\n\n/** Resolve repo and get a BrainBank from the pool. */\nasync function getBrainBank(targetRepo?: string) {\n return pool.get(resolveRepoPath(targetRepo));\n}\n\n// ── MCP Server Setup ────────────────────────────────\n\nconst server = new McpServer({\n name: 'brainbank',\n version: '0.1.3',\n});\n\n// ── Tool: brainbank_context ─────────────────────────\n\nserver.registerTool(\n 'brainbank_context',\n {\n title: 'BrainBank Context',\n description:\n 'Get a formatted knowledge context block for a task. Returns a Workflow Trace: ' +\n 'search hits + full call tree with `called by` annotations, topologically ordered. ' +\n 'All source code included — no trimming, no truncation.',\n inputSchema: z.object({\n task: z.string().describe('Description of the task you need context for'),\n affectedFiles: z.array(z.string()).optional().default([]).describe('Files you plan to modify (improves co-edit suggestions)'),\n codeResults: z.number().optional().default(20).describe('Max code results'),\n gitResults: z.number().optional().default(5).describe('Max git commit results'),\n docsResults: z.number().optional().describe('Max document results (omit to skip docs)'),\n sources: z.record(z.number()).optional().describe('Per-source result limits, overrides codeResults/gitResults/docsResults (e.g. { code: 10, git: 0, docs: 5 })'),\n path: z.union([z.string(), z.array(z.string())]).optional().describe('Filter results to files under these path prefixes. Pass a single string or array (e.g. [\"src/services/\", \"lib/\"])'),\n ignore: z.array(z.string()).optional().describe('Exclude results whose filePath starts with any of these prefixes (e.g. [\"src/tests/\", \"src/mocks/\"])'),\n repo: z.string().describe('Repository path (default: BRAINBANK_REPO)'),\n // BrainBankQL context fields\n lines: z.boolean().optional().describe('Prefix each code line with its source line number (e.g. 127| code)'),\n symbols: z.boolean().optional().describe('Append symbol index (all functions, classes, interfaces) for matched files'),\n compact: z.boolean().optional().describe('Show only function/class signatures, skip bodies'),\n callTree: z.union([z.boolean(), z.object({ depth: z.number() })]).optional().describe('Include call tree expansion. Pass { depth: N } to control depth'),\n imports: z.boolean().optional().describe('Include dependency/import summary section'),\n context: z.string().optional().describe('General task context for the pruner — e.g. GitHub issue body, requirements doc, conversation context. Helps the AI pruner understand the broader task'),\n pruner: z.string().optional().describe('Specific pruning instructions — tells the pruner exactly what to focus on (e.g. \"only keep files related to WebSocket JWT refresh\")'),\n }),\n },\n async ({ task, affectedFiles, codeResults, gitResults, docsResults, sources, path, ignore, repo, lines, symbols, compact, callTree, imports, context: ctx, pruner: prunerCtx }) => {\n const repoPath = resolveRepoPath(repo);\n const brainbank = await getBrainBank(repo);\n\n // Build sources from explicit params, then let `sources` override\n const base: Record<string, number> = { code: codeResults, git: gitResults };\n if (docsResults !== undefined) base.docs = docsResults;\n const resolvedSources = sources ? { ...base, ...sources } : base;\n\n // Build fields from explicit params (only include defined values)\n const fields: Record<string, unknown> = {};\n if (lines !== undefined) fields.lines = lines;\n if (symbols !== undefined) fields.symbols = symbols;\n if (compact !== undefined) fields.compact = compact;\n if (callTree !== undefined) fields.callTree = callTree;\n if (imports !== undefined) fields.imports = imports;\n\n const context = await brainbank.getContext(task, {\n affectedFiles,\n sources: resolvedSources,\n pathPrefix: path,\n ignorePaths: ignore,\n source: 'mcp',\n fields: Object.keys(fields).length > 0 ? fields : undefined,\n context: ctx,\n prunerContext: prunerCtx,\n });\n\n return { content: [{ type: 'text' as const, text: context }] };\n },\n);\n\n// ── Tool: brainbank_search ──────────────────────────\n\n/** Format SearchResult[] into readable text blocks for the agent. */\nfunction formatSearchResults(results: SearchResult[]): string {\n if (results.length === 0) return 'No results found.';\n\n const lines: string[] = [];\n for (const r of results) {\n const score = Math.round(r.score * 100);\n if (r.type === 'code') {\n const m = r.metadata;\n lines.push(`[CODE ${score}%] ${r.filePath} — ${m.name || m.chunkType} L${m.startLine}-${m.endLine}`);\n lines.push(r.content);\n lines.push('');\n } else if (r.type === 'commit') {\n const m = r.metadata;\n lines.push(`[COMMIT ${score}%] ${m.shortHash} ${r.content} (${m.author})`);\n if (m.files?.length) lines.push(` Files: ${m.files.slice(0, 6).join(', ')}`);\n lines.push('');\n } else if (r.type === 'document') {\n const ctx = r.context ? ` — ${r.context}` : '';\n lines.push(`[DOC ${score}%] ${r.filePath} [${r.metadata.collection}]${ctx}`);\n lines.push(r.content);\n lines.push('');\n } else if (r.type === 'collection') {\n lines.push(`[COLLECTION ${score}%] ${r.metadata.collection ?? 'kv'}`);\n lines.push(r.content);\n lines.push('');\n }\n }\n return lines.join('\\n');\n}\n\nserver.registerTool(\n 'brainbank_search',\n {\n title: 'BrainBank Search',\n description:\n 'Hybrid search across the codebase: combines vector similarity + BM25 keyword matching ' +\n 'via Reciprocal Rank Fusion. Returns code chunks, git commits, and documents ranked by relevance. ' +\n 'Use for finding code, understanding systems, and locating files by meaning.',\n inputSchema: z.object({\n query: z.string().describe('Natural language search query (e.g. \"authentication middleware\", \"WebSocket reconnection\")'),\n repo: z.string().describe('Repository path (default: BRAINBANK_REPO)'),\n sources: z.record(z.number()).optional().describe('Per-source result limits (e.g. { code: 10, git: 0, docs: 5 }). Set source to 0 to skip it'),\n path: z.union([z.string(), z.array(z.string())]).optional().describe('Filter results to files under these path prefixes (e.g. \"src/services\" or [\"src/\", \"lib/\"])'),\n maxResults: z.number().optional().default(20).describe('Maximum total results to return'),\n }),\n },\n async ({ query, repo, sources, path, maxResults }) => {\n const brainbank = await getBrainBank(repo);\n\n const opts: Record<string, unknown> = { source: 'mcp' as const };\n if (sources) opts.sources = sources;\n if (path) opts.pathPrefix = path;\n\n const results = await brainbank.hybridSearch(query, opts as SearchOptions);\n const limited = results.slice(0, maxResults);\n const text = formatSearchResults(limited);\n\n return { content: [{ type: 'text' as const, text }] };\n },\n);\n\n\n// ── Tool: brainbank_index ───────────────────────────\n\nserver.registerTool(\n 'brainbank_index',\n {\n title: 'BrainBank Index',\n description:\n 'Index (or re-index) a repository. Scans code files, git history, and documents, ' +\n 'then builds vector embeddings and FTS5 indices. Run this after significant code changes ' +\n 'or when search results feel stale. Returns a summary of what changed.',\n inputSchema: z.object({\n repo: z.string().describe('Repository path to index'),\n forceReindex: z.boolean().optional().default(false).describe('Force re-index all files (ignore cache). Use when embeddings changed or index is corrupted'),\n modules: z.array(z.string()).optional().describe('Which plugins to index (e.g. [\"code\", \"git\", \"docs\"]). Omit to use config defaults'),\n }),\n },\n async ({ repo, forceReindex, modules }) => {\n const brainbank = await getBrainBank(repo);\n\n const result = await brainbank.index({\n forceReindex,\n modules: modules && modules.length > 0 ? modules : undefined,\n });\n\n // Format results summary\n const lines: string[] = ['# Index Results\\n'];\n let hasChanges = false;\n\n for (const [name, value] of Object.entries(result)) {\n if (!value) continue;\n const v = value as Record<string, unknown>;\n if (typeof v.indexed !== 'number') {\n lines.push(`✓ ${name}: done`);\n continue;\n }\n\n const indexed = v.indexed as number;\n const skipped = (v.skipped ?? 0) as number;\n const removed = (v.removed ?? 0) as number;\n const chunks = (v.chunks ?? 0) as number;\n\n if (indexed > 0 || removed > 0) hasChanges = true;\n\n const parts: string[] = [];\n if (indexed > 0) parts.push(`+${indexed} files (${chunks} chunks)`);\n if (removed > 0) parts.push(`−${removed} files`);\n if (skipped > 0) parts.push(`${skipped} unchanged`);\n\n lines.push(`${name}: ${parts.join(' ')}`);\n }\n\n if (!hasChanges) {\n lines.push('No changes — everything up to date');\n }\n\n // Stats\n const stats = brainbank.stats();\n lines.push('\\n## Totals\\n');\n for (const [name, s] of Object.entries(stats)) {\n if (!s || typeof s !== 'object') continue;\n const entries = Object.entries(s as Record<string, unknown>)\n .map(([k, v]) => `${k}: ${v}`)\n .join(', ');\n lines.push(`${name}: ${entries}`);\n }\n\n return { content: [{ type: 'text' as const, text: lines.join('\\n') }] };\n },\n);\n\n\n// ── Start Server ────────────────────────────────────\n\nasync function main() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch(err => {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`BrainBank MCP Server Error: ${message}`);\n process.exit(1);\n});\n","/**\n * WorkspacePool — BrainBank instance lifecycle manager.\n *\n * Manages cached BrainBank instances per workspace with:\n * - Memory-pressure eviction (oldest idle first)\n * - TTL eviction for inactive workspaces\n * - Active-operation tracking (prevents mid-query eviction)\n * - Hot-reload of stale HNSW indices\n */\n\nimport type { BrainBank } from '@/brainbank.ts';\n\n/** Pool configuration. */\nexport interface PoolOptions {\n /** Max total estimated memory in MB. Default: 2048. */\n maxMemoryMB?: number;\n /** Minutes of inactivity before eviction. Default: 30. */\n ttlMinutes?: number;\n /** Factory function to create a BrainBank for a repo path. */\n factory: (repoPath: string) => Promise<BrainBank>;\n /** Called when a workspace is evicted. */\n onEvict?: (repoPath: string) => void;\n /** Called when an error occurs during pool operations. */\n onError?: (repoPath: string, err: unknown) => void;\n}\n\n/** Internal pool entry. */\ninterface PoolEntry {\n brain: BrainBank;\n repoPath: string;\n lastAccess: number;\n createdAt: number;\n activeOps: number;\n}\n\n/** Public pool statistics. */\nexport interface PoolStats {\n size: number;\n totalMemoryMB: number;\n entries: PoolEntryStats[];\n}\n\n/** Per-entry statistics. */\nexport interface PoolEntryStats {\n repoPath: string;\n lastAccessAgo: string;\n memoryMB: number;\n activeOps: number;\n}\n\nconst DEFAULT_MAX_MEMORY_MB = 2048;\nconst DEFAULT_TTL_MINUTES = 30;\nconst EVICTION_INTERVAL_MS = 60_000;\n\n/** Format milliseconds as a human-readable \"ago\" string. */\nfunction formatAgo(ms: number): string {\n if (ms < 60_000) return `${Math.round(ms / 1000)}s ago`;\n if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m ago`;\n return `${Math.round(ms / 3_600_000)}h ago`;\n}\n\nexport class WorkspacePool {\n private _pool = new Map<string, PoolEntry>();\n private _timer: ReturnType<typeof setInterval> | null = null;\n private _maxMemoryBytes: number;\n private _ttlMs: number;\n private _factory: (repoPath: string) => Promise<BrainBank>;\n private _onEvict?: (repoPath: string) => void;\n private _onError?: (repoPath: string, err: unknown) => void;\n\n constructor(options: PoolOptions) {\n this._maxMemoryBytes = (options.maxMemoryMB ?? DEFAULT_MAX_MEMORY_MB) * 1024 * 1024;\n this._ttlMs = (options.ttlMinutes ?? DEFAULT_TTL_MINUTES) * 60 * 1000;\n this._factory = options.factory;\n this._onEvict = options.onEvict;\n this._onError = options.onError;\n\n this._timer = setInterval(() => this._evictStale(), EVICTION_INTERVAL_MS);\n // Don't hold the process open for the timer\n if (this._timer.unref) this._timer.unref();\n }\n\n /** Number of cached workspaces. */\n get size(): number {\n return this._pool.size;\n }\n\n /**\n * Get a BrainBank for the given repo path.\n * Returns a cached instance (with hot-reload) or creates a new one.\n */\n async get(repoPath: string): Promise<BrainBank> {\n const key = repoPath.replace(/\\/+$/, '');\n\n const existing = this._pool.get(key);\n if (existing) {\n existing.lastAccess = Date.now();\n try { await existing.brain.ensureFresh(); } catch { /* stale is better than nothing */ }\n return existing.brain;\n }\n\n this._evictByMemoryPressure();\n\n const brain = await this._factory(key);\n this._pool.set(key, {\n brain,\n repoPath: key,\n lastAccess: Date.now(),\n createdAt: Date.now(),\n activeOps: 0,\n });\n\n return brain;\n }\n\n /**\n * Execute an operation with active-op tracking.\n * Prevents the workspace from being evicted while the operation runs.\n */\n async withBrain<T>(repoPath: string, fn: (brain: BrainBank) => Promise<T>): Promise<T> {\n const brain = await this.get(repoPath);\n const key = repoPath.replace(/\\/+$/, '');\n const entry = this._pool.get(key);\n if (entry) entry.activeOps++;\n\n try {\n return await fn(brain);\n } finally {\n if (entry) {\n entry.activeOps--;\n entry.lastAccess = Date.now();\n }\n }\n }\n\n /** Manually evict a specific workspace. */\n evict(repoPath: string): void {\n const key = repoPath.replace(/\\/+$/, '');\n this._evictEntry(key);\n }\n\n /** Get pool statistics. */\n stats(): PoolStats {\n const now = Date.now();\n let totalMemory = 0;\n const entries: PoolEntryStats[] = [];\n\n for (const entry of this._pool.values()) {\n const memBytes = entry.brain.memoryHint();\n const memMB = Math.round(memBytes / 1024 / 1024 * 100) / 100;\n totalMemory += memBytes;\n\n entries.push({\n repoPath: entry.repoPath,\n lastAccessAgo: formatAgo(now - entry.lastAccess),\n memoryMB: memMB,\n activeOps: entry.activeOps,\n });\n }\n\n return {\n size: this._pool.size,\n totalMemoryMB: Math.round(totalMemory / 1024 / 1024 * 100) / 100,\n entries,\n };\n }\n\n /** Close all entries and stop the eviction timer. */\n close(): void {\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n for (const key of [...this._pool.keys()]) {\n this._evictEntry(key);\n }\n }\n\n /** Evict workspaces that haven't been accessed within the TTL. */\n private _evictStale(): void {\n const cutoff = Date.now() - this._ttlMs;\n for (const [key, entry] of this._pool) {\n if (entry.lastAccess < cutoff && entry.activeOps === 0) {\n this._evictEntry(key);\n }\n }\n }\n\n /** Evict oldest idle entries until total memory is under the limit. */\n private _evictByMemoryPressure(): void {\n let totalMemory = 0;\n for (const entry of this._pool.values()) {\n totalMemory += entry.brain.memoryHint();\n }\n\n if (totalMemory < this._maxMemoryBytes) return;\n\n // Sort by lastAccess ascending (oldest first), filter idle\n const candidates = [...this._pool.entries()]\n .filter(([, e]) => e.activeOps === 0)\n .sort(([, a], [, b]) => a.lastAccess - b.lastAccess);\n\n for (const [key, entry] of candidates) {\n if (totalMemory < this._maxMemoryBytes) break;\n totalMemory -= entry.brain.memoryHint();\n this._evictEntry(key);\n }\n }\n\n /** Evict a single entry by key. */\n private _evictEntry(key: string): void {\n const entry = this._pool.get(key);\n if (!entry) return;\n\n try {\n entry.brain.close();\n } catch (err: unknown) {\n this._onError?.(key, err);\n }\n\n this._pool.delete(key);\n this._onEvict?.(key);\n }\n}\n","/**\n * WorkspaceFactory — creates BrainBank instances via the core factory.\n *\n * Delegates to `createBrain()` from the core factory, passing\n * a portable `BrainContext`. No plugin hardcoding — the factory handles\n * plugin discovery from config and installed packages.\n */\n\nimport type { BrainBank } from '@/brainbank.ts';\nimport type { BrainContext } from '@/cli/factory/brain-context.ts';\n\nimport { createBrain, resetFactoryCache } from '@/cli/factory/index.ts';\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/**\n * Detect repo root by walking up from startDir until we find `.git/`.\n * Returns startDir itself if no `.git/` is found.\n */\nexport function findRepoRoot(startDir: string): string {\n let dir = path.resolve(startDir);\n while (true) {\n if (fs.existsSync(path.join(dir, '.git'))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return path.resolve(startDir);\n}\n\n/** Resolve the effective repo path from an optional target, env, or cwd. */\nexport function resolveRepoPath(targetRepo?: string): string {\n const rp = targetRepo\n ?? process.env.BRAINBANK_REPO\n ?? findRepoRoot(process.cwd());\n return rp.replace(/\\/+$/, '');\n}\n\n/**\n * Create a BrainBank instance for a workspace.\n * Uses the core factory which handles:\n * - Config loading from .brainbank/config.json\n * - Dynamic plugin discovery and registration\n * - Embedding/pruner/expander provider setup\n * - Folder plugin auto-discovery\n */\nexport async function createWorkspaceBrain(repoPath: string): Promise<BrainBank> {\n resetFactoryCache();\n\n const context: BrainContext = {\n repoPath,\n env: process.env as Record<string, string | undefined>,\n };\n\n // Silence stdout during initialization — the core factory emits ANSI-colored\n // console.log messages (plugin loading) that corrupt\n // the MCP JSON-RPC stdio transport. Redirect console.log → stderr temporarily.\n const origLog = console.log;\n console.log = (...args: unknown[]) => console.error(...args);\n try {\n const brain = await createBrain(context);\n await brain.initialize();\n return brain;\n } finally {\n console.log = origLog;\n }\n}\n"],"mappings":";;;;;;;;;;;AAyBA,SAAS,iBAAiB;AAC1B,SAAS,4BAA4B;AACrC,SAAS,SAAS;;;ACuBlB,IAAM,wBAAwB;AAC9B,IAAM,sBAAsB;AAC5B,IAAM,uBAAuB;AAG7B,SAAS,UAAU,IAAoB;AACnC,MAAI,KAAK,IAAQ,QAAO,GAAG,KAAK,MAAM,KAAK,GAAI,CAAC;AAChD,MAAI,KAAK,KAAW,QAAO,GAAG,KAAK,MAAM,KAAK,GAAM,CAAC;AACrD,SAAO,GAAG,KAAK,MAAM,KAAK,IAAS,CAAC;AACxC;AAJS;AAMF,IAAM,gBAAN,MAAoB;AAAA,EA7D3B,OA6D2B;AAAA;AAAA;AAAA,EACf,QAAQ,oBAAI,IAAuB;AAAA,EACnC,SAAgD;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,SAAsB;AAC9B,SAAK,mBAAmB,QAAQ,eAAe,yBAAyB,OAAO;AAC/E,SAAK,UAAU,QAAQ,cAAc,uBAAuB,KAAK;AACjE,SAAK,WAAW,QAAQ;AACxB,SAAK,WAAW,QAAQ;AACxB,SAAK,WAAW,QAAQ;AAExB,SAAK,SAAS,YAAY,MAAM,KAAK,YAAY,GAAG,oBAAoB;AAExE,QAAI,KAAK,OAAO,MAAO,MAAK,OAAO,MAAM;AAAA,EAC7C;AAAA;AAAA,EAGA,IAAI,OAAe;AACf,WAAO,KAAK,MAAM;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,IAAI,UAAsC;AAC5C,UAAM,MAAM,SAAS,QAAQ,QAAQ,EAAE;AAEvC,UAAM,WAAW,KAAK,MAAM,IAAI,GAAG;AACnC,QAAI,UAAU;AACV,eAAS,aAAa,KAAK,IAAI;AAC/B,UAAI;AAAE,cAAM,SAAS,MAAM,YAAY;AAAA,MAAG,QAAQ;AAAA,MAAqC;AACvF,aAAO,SAAS;AAAA,IACpB;AAEA,SAAK,uBAAuB;AAE5B,UAAM,QAAQ,MAAM,KAAK,SAAS,GAAG;AACrC,SAAK,MAAM,IAAI,KAAK;AAAA,MAChB;AAAA,MACA,UAAU;AAAA,MACV,YAAY,KAAK,IAAI;AAAA,MACrB,WAAW,KAAK,IAAI;AAAA,MACpB,WAAW;AAAA,IACf,CAAC;AAED,WAAO;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAa,UAAkB,IAAkD;AACnF,UAAM,QAAQ,MAAM,KAAK,IAAI,QAAQ;AACrC,UAAM,MAAM,SAAS,QAAQ,QAAQ,EAAE;AACvC,UAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;AAChC,QAAI,MAAO,OAAM;AAEjB,QAAI;AACA,aAAO,MAAM,GAAG,KAAK;AAAA,IACzB,UAAE;AACE,UAAI,OAAO;AACP,cAAM;AACN,cAAM,aAAa,KAAK,IAAI;AAAA,MAChC;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA,EAGA,MAAM,UAAwB;AAC1B,UAAM,MAAM,SAAS,QAAQ,QAAQ,EAAE;AACvC,SAAK,YAAY,GAAG;AAAA,EACxB;AAAA;AAAA,EAGA,QAAmB;AACf,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,cAAc;AAClB,UAAM,UAA4B,CAAC;AAEnC,eAAW,SAAS,KAAK,MAAM,OAAO,GAAG;AACrC,YAAM,WAAW,MAAM,MAAM,WAAW;AACxC,YAAM,QAAQ,KAAK,MAAM,WAAW,OAAO,OAAO,GAAG,IAAI;AACzD,qBAAe;AAEf,cAAQ,KAAK;AAAA,QACT,UAAU,MAAM;AAAA,QAChB,eAAe,UAAU,MAAM,MAAM,UAAU;AAAA,QAC/C,UAAU;AAAA,QACV,WAAW,MAAM;AAAA,MACrB,CAAC;AAAA,IACL;AAEA,WAAO;AAAA,MACH,MAAM,KAAK,MAAM;AAAA,MACjB,eAAe,KAAK,MAAM,cAAc,OAAO,OAAO,GAAG,IAAI;AAAA,MAC7D;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA,EAGA,QAAc;AACV,QAAI,KAAK,QAAQ;AACb,oBAAc,KAAK,MAAM;AACzB,WAAK,SAAS;AAAA,IAClB;AACA,eAAW,OAAO,CAAC,GAAG,KAAK,MAAM,KAAK,CAAC,GAAG;AACtC,WAAK,YAAY,GAAG;AAAA,IACxB;AAAA,EACJ;AAAA;AAAA,EAGQ,cAAoB;AACxB,UAAM,SAAS,KAAK,IAAI,IAAI,KAAK;AACjC,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,OAAO;AACnC,UAAI,MAAM,aAAa,UAAU,MAAM,cAAc,GAAG;AACpD,aAAK,YAAY,GAAG;AAAA,MACxB;AAAA,IACJ;AAAA,EACJ;AAAA;AAAA,EAGQ,yBAA+B;AACnC,QAAI,cAAc;AAClB,eAAW,SAAS,KAAK,MAAM,OAAO,GAAG;AACrC,qBAAe,MAAM,MAAM,WAAW;AAAA,IAC1C;AAEA,QAAI,cAAc,KAAK,gBAAiB;AAGxC,UAAM,aAAa,CAAC,GAAG,KAAK,MAAM,QAAQ,CAAC,EACtC,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC,EACnC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU;AAEvD,eAAW,CAAC,KAAK,KAAK,KAAK,YAAY;AACnC,UAAI,cAAc,KAAK,gBAAiB;AACxC,qBAAe,MAAM,MAAM,WAAW;AACtC,WAAK,YAAY,GAAG;AAAA,IACxB;AAAA,EACJ;AAAA;AAAA,EAGQ,YAAY,KAAmB;AACnC,UAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;AAChC,QAAI,CAAC,MAAO;AAEZ,QAAI;AACA,YAAM,MAAM,MAAM;AAAA,IACtB,SAAS,KAAc;AACnB,WAAK,WAAW,KAAK,GAAG;AAAA,IAC5B;AAEA,SAAK,MAAM,OAAO,GAAG;AACrB,SAAK,WAAW,GAAG;AAAA,EACvB;AACJ;;;AClNA,YAAY,QAAQ;AACpB,YAAY,UAAU;AAMf,SAAS,aAAa,UAA0B;AACnD,MAAI,MAAW,aAAQ,QAAQ;AAC/B,SAAO,MAAM;AACT,QAAO,cAAgB,UAAK,KAAK,MAAM,CAAC,EAAG,QAAO;AAClD,UAAM,SAAc,aAAQ,GAAG;AAC/B,QAAI,WAAW,IAAK;AACpB,UAAM;AAAA,EACV;AACA,SAAY,aAAQ,QAAQ;AAChC;AATgB;AAYT,SAAS,gBAAgB,YAA6B;AACzD,QAAM,KAAK,cACJ,QAAQ,IAAI,kBACZ,aAAa,QAAQ,IAAI,CAAC;AACjC,SAAO,GAAG,QAAQ,QAAQ,EAAE;AAChC;AALgB;AAehB,eAAsB,qBAAqB,UAAsC;AAC7E,oBAAkB;AAElB,QAAM,UAAwB;AAAA,IAC1B;AAAA,IACA,KAAK,QAAQ;AAAA,EACjB;AAKA,QAAM,UAAU,QAAQ;AACxB,UAAQ,MAAM,IAAI,SAAoB,QAAQ,MAAM,GAAG,IAAI;AAC3D,MAAI;AACA,UAAM,QAAQ,MAAM,YAAY,OAAO;AACvC,UAAM,MAAM,WAAW;AACvB,WAAO;AAAA,EACX,UAAE;AACE,YAAQ,MAAM;AAAA,EAClB;AACJ;AApBsB;;;AFXtB,IAAM,OAAO,IAAI,cAAc;AAAA,EAC3B,SAAS;AAAA,EACT,aAAa,SAAS,QAAQ,IAAI,2BAA2B,QAAQ,EAAE;AAAA,EACvE,YAAY,SAAS,QAAQ,IAAI,yBAAyB,MAAM,EAAE;AAAA,EAClE,SAAS,wBAAC,MAAM,QAAQ;AACpB,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,YAAQ,MAAM,yBAAyB,IAAI,MAAM,GAAG,EAAE;AAAA,EAC1D,GAHS;AAIb,CAAC;AAGD,eAAe,aAAa,YAAqB;AAC7C,SAAO,KAAK,IAAI,gBAAgB,UAAU,CAAC;AAC/C;AAFe;AAMf,IAAM,SAAS,IAAI,UAAU;AAAA,EACzB,MAAM;AAAA,EACN,SAAS;AACb,CAAC;AAID,OAAO;AAAA,EACH;AAAA,EACA;AAAA,IACI,OAAO;AAAA,IACP,aACI;AAAA,IAGJ,aAAa,EAAE,OAAO;AAAA,MAClB,MAAM,EAAE,OAAO,EAAE,SAAS,8CAA8C;AAAA,MACxE,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC,EAAE,SAAS,yDAAyD;AAAA,MAC5H,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,SAAS,kBAAkB;AAAA,MAC1E,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,SAAS,wBAAwB;AAAA,MAC9E,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,0CAA0C;AAAA,MACtF,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS,6GAA6G;AAAA,MAC/J,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,mHAAmH;AAAA,MACxL,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS,sGAAsG;AAAA,MACtJ,MAAM,EAAE,OAAO,EAAE,SAAS,2CAA2C;AAAA;AAAA,MAErE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,oEAAoE;AAAA,MAC3G,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,4EAA4E;AAAA,MACrH,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,kDAAkD;AAAA,MAC3F,UAAU,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,iEAAiE;AAAA,MACvJ,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,2CAA2C;AAAA,MACpF,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,4JAAuJ;AAAA,MAC/L,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,0IAAqI;AAAA,IAChL,CAAC;AAAA,EACL;AAAA,EACA,OAAO,EAAE,MAAM,eAAe,aAAa,YAAY,aAAa,SAAS,MAAAA,OAAM,QAAQ,MAAM,OAAO,SAAS,SAAS,UAAU,SAAS,SAAS,KAAK,QAAQ,UAAU,MAAM;AAC/K,UAAM,WAAW,gBAAgB,IAAI;AACrC,UAAM,YAAY,MAAM,aAAa,IAAI;AAGzC,UAAM,OAA+B,EAAE,MAAM,aAAa,KAAK,WAAW;AAC1E,QAAI,gBAAgB,OAAW,MAAK,OAAO;AAC3C,UAAM,kBAAkB,UAAU,EAAE,GAAG,MAAM,GAAG,QAAQ,IAAI;AAG5D,UAAM,SAAkC,CAAC;AACzC,QAAI,UAAU,OAAW,QAAO,QAAQ;AACxC,QAAI,YAAY,OAAW,QAAO,UAAU;AAC5C,QAAI,YAAY,OAAW,QAAO,UAAU;AAC5C,QAAI,aAAa,OAAW,QAAO,WAAW;AAC9C,QAAI,YAAY,OAAW,QAAO,UAAU;AAE5C,UAAM,UAAU,MAAM,UAAU,WAAW,MAAM;AAAA,MAC7C;AAAA,MACA,SAAS;AAAA,MACT,YAAYA;AAAA,MACZ,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,QAAQ,OAAO,KAAK,MAAM,EAAE,SAAS,IAAI,SAAS;AAAA,MAClD,SAAS;AAAA,MACT,eAAe;AAAA,IACnB,CAAC;AAED,WAAO,EAAE,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,QAAQ,CAAC,EAAE;AAAA,EACjE;AACJ;AAKA,SAAS,oBAAoB,SAAiC;AAC1D,MAAI,QAAQ,WAAW,EAAG,QAAO;AAEjC,QAAM,QAAkB,CAAC;AACzB,aAAW,KAAK,SAAS;AACrB,UAAM,QAAQ,KAAK,MAAM,EAAE,QAAQ,GAAG;AACtC,QAAI,EAAE,SAAS,QAAQ;AACnB,YAAM,IAAI,EAAE;AACZ,YAAM,KAAK,SAAS,KAAK,MAAM,EAAE,QAAQ,WAAM,EAAE,QAAQ,EAAE,SAAS,KAAK,EAAE,SAAS,IAAI,EAAE,OAAO,EAAE;AACnG,YAAM,KAAK,EAAE,OAAO;AACpB,YAAM,KAAK,EAAE;AAAA,IACjB,WAAW,EAAE,SAAS,UAAU;AAC5B,YAAM,IAAI,EAAE;AACZ,YAAM,KAAK,WAAW,KAAK,MAAM,EAAE,SAAS,IAAI,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG;AACzE,UAAI,EAAE,OAAO,OAAQ,OAAM,KAAK,YAAY,EAAE,MAAM,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE;AAC5E,YAAM,KAAK,EAAE;AAAA,IACjB,WAAW,EAAE,SAAS,YAAY;AAC9B,YAAM,MAAM,EAAE,UAAU,WAAM,EAAE,OAAO,KAAK;AAC5C,YAAM,KAAK,QAAQ,KAAK,MAAM,EAAE,QAAQ,KAAK,EAAE,SAAS,UAAU,IAAI,GAAG,EAAE;AAC3E,YAAM,KAAK,EAAE,OAAO;AACpB,YAAM,KAAK,EAAE;AAAA,IACjB,WAAW,EAAE,SAAS,cAAc;AAChC,YAAM,KAAK,eAAe,KAAK,MAAM,EAAE,SAAS,cAAc,IAAI,EAAE;AACpE,YAAM,KAAK,EAAE,OAAO;AACpB,YAAM,KAAK,EAAE;AAAA,IACjB;AAAA,EACJ;AACA,SAAO,MAAM,KAAK,IAAI;AAC1B;AA5BS;AA8BT,OAAO;AAAA,EACH;AAAA,EACA;AAAA,IACI,OAAO;AAAA,IACP,aACI;AAAA,IAGJ,aAAa,EAAE,OAAO;AAAA,MAClB,OAAO,EAAE,OAAO,EAAE,SAAS,4FAA4F;AAAA,MACvH,MAAM,EAAE,OAAO,EAAE,SAAS,2CAA2C;AAAA,MACrE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS,2FAA2F;AAAA,MAC7I,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,6FAA6F;AAAA,MAClK,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,SAAS,iCAAiC;AAAA,IAC5F,CAAC;AAAA,EACL;AAAA,EACA,OAAO,EAAE,OAAO,MAAM,SAAS,MAAAA,OAAM,WAAW,MAAM;AAClD,UAAM,YAAY,MAAM,aAAa,IAAI;AAEzC,UAAM,OAAgC,EAAE,QAAQ,MAAe;AAC/D,QAAI,QAAS,MAAK,UAAU;AAC5B,QAAIA,MAAM,MAAK,aAAaA;AAE5B,UAAM,UAAU,MAAM,UAAU,aAAa,OAAO,IAAqB;AACzE,UAAM,UAAU,QAAQ,MAAM,GAAG,UAAU;AAC3C,UAAM,OAAO,oBAAoB,OAAO;AAExC,WAAO,EAAE,SAAS,CAAC,EAAE,MAAM,QAAiB,KAAK,CAAC,EAAE;AAAA,EACxD;AACJ;AAKA,OAAO;AAAA,EACH;AAAA,EACA;AAAA,IACI,OAAO;AAAA,IACP,aACI;AAAA,IAGJ,aAAa,EAAE,OAAO;AAAA,MAClB,MAAM,EAAE,OAAO,EAAE,SAAS,0BAA0B;AAAA,MACpD,cAAc,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,KAAK,EAAE,SAAS,4FAA4F;AAAA,MACzJ,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS,oFAAoF;AAAA,IACzI,CAAC;AAAA,EACL;AAAA,EACA,OAAO,EAAE,MAAM,cAAc,QAAQ,MAAM;AACvC,UAAM,YAAY,MAAM,aAAa,IAAI;AAEzC,UAAM,SAAS,MAAM,UAAU,MAAM;AAAA,MACjC;AAAA,MACA,SAAS,WAAW,QAAQ,SAAS,IAAI,UAAU;AAAA,IACvD,CAAC;AAGD,UAAM,QAAkB,CAAC,mBAAmB;AAC5C,QAAI,aAAa;AAEjB,eAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AAChD,UAAI,CAAC,MAAO;AACZ,YAAM,IAAI;AACV,UAAI,OAAO,EAAE,YAAY,UAAU;AAC/B,cAAM,KAAK,UAAK,IAAI,QAAQ;AAC5B;AAAA,MACJ;AAEA,YAAM,UAAU,EAAE;AAClB,YAAM,UAAW,EAAE,WAAW;AAC9B,YAAM,UAAW,EAAE,WAAW;AAC9B,YAAM,SAAU,EAAE,UAAU;AAE5B,UAAI,UAAU,KAAK,UAAU,EAAG,cAAa;AAE7C,YAAM,QAAkB,CAAC;AACzB,UAAI,UAAU,EAAG,OAAM,KAAK,IAAI,OAAO,WAAW,MAAM,UAAU;AAClE,UAAI,UAAU,EAAG,OAAM,KAAK,SAAI,OAAO,QAAQ;AAC/C,UAAI,UAAU,EAAG,OAAM,KAAK,GAAG,OAAO,YAAY;AAElD,YAAM,KAAK,GAAG,IAAI,KAAK,MAAM,KAAK,IAAI,CAAC,EAAE;AAAA,IAC7C;AAEA,QAAI,CAAC,YAAY;AACb,YAAM,KAAK,yCAAoC;AAAA,IACnD;AAGA,UAAM,QAAQ,UAAU,MAAM;AAC9B,UAAM,KAAK,eAAe;AAC1B,eAAW,CAAC,MAAM,CAAC,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC3C,UAAI,CAAC,KAAK,OAAO,MAAM,SAAU;AACjC,YAAM,UAAU,OAAO,QAAQ,CAA4B,EACtD,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,EAAE,EAC5B,KAAK,IAAI;AACd,YAAM,KAAK,GAAG,IAAI,KAAK,OAAO,EAAE;AAAA,IACpC;AAEA,WAAO,EAAE,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,MAAM,KAAK,IAAI,EAAE,CAAC,EAAE;AAAA,EAC1E;AACJ;AAKA,eAAe,OAAO;AAClB,QAAM,YAAY,IAAI,qBAAqB;AAC3C,QAAM,OAAO,QAAQ,SAAS;AAClC;AAHe;AAKf,KAAK,EAAE,MAAM,SAAO;AAChB,QAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,UAAQ,MAAM,+BAA+B,OAAO,EAAE;AACtD,UAAQ,KAAK,CAAC;AAClB,CAAC;","names":["path"]}
1
+ {"version":3,"sources":["../src/mcp/mcp-server.ts","../src/mcp/workspace-pool.ts","../src/mcp/workspace-factory.ts"],"sourcesContent":["/**\n * BrainBank — MCP Server\n *\n * Exposes BrainBank as an MCP server via stdio transport.\n * Works with Google Antigravity, Claude Code, and any MCP-compatible client.\n *\n * Tools:\n * brainbank_query — AI-pruned semantic context for a task\n * brainbank_index — Index/re-index a repository\n */\n\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod/v3';\n\nimport { WorkspacePool } from './workspace-pool.ts';\nimport { createWorkspaceBrain, resolveRepoPath } from './workspace-factory.ts';\n\n\n\n// ── Multi-Workspace BrainBank Pool ─────────────────────\n\nconst pool = new WorkspacePool({\n factory: createWorkspaceBrain,\n maxMemoryMB: parseInt(process.env.BRAINBANK_MAX_MEMORY_MB ?? '2048', 10),\n ttlMinutes: parseInt(process.env.BRAINBANK_TTL_MINUTES ?? '30', 10),\n onError: (repo, err) => {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`BrainBank pool error [${repo}]: ${msg}`);\n },\n});\n\n/** Resolve repo and get a BrainBank from the pool. */\nasync function getBrainBank(targetRepo?: string) {\n return pool.get(resolveRepoPath(targetRepo));\n}\n\n// ── MCP Server Setup ────────────────────────────────\n\nconst server = new McpServer({\n name: 'brainbank',\n version: '0.1.5',\n});\n\n// ── Tool: brainbank_query ───────────────────────────\n\nserver.registerTool(\n 'brainbank_query',\n {\n title: 'BrainBank Context',\n description:\n 'Get AI-pruned semantic context for a task. Returns search hits + full call tree ' +\n 'with `called by` annotations, topologically ordered. Pass `context` for general ' +\n 'background and `pruner` for specific filtering instructions.',\n inputSchema: z.object({\n task: z.string().describe('Description of the task you need context for'),\n affectedFiles: z.array(z.string()).optional().default([]).describe('Files you plan to modify (improves co-edit suggestions)'),\n codeResults: z.number().optional().default(20).describe('Max code results'),\n gitResults: z.number().optional().default(5).describe('Max git commit results'),\n docsResults: z.number().optional().describe('Max document results (omit to skip docs)'),\n sources: z.record(z.number()).optional().describe('Per-source result limits, overrides codeResults/gitResults/docsResults (e.g. { code: 10, git: 0, docs: 5 })'),\n path: z.union([z.string(), z.array(z.string())]).optional().describe('Filter results to files under these path prefixes. Pass a single string or array (e.g. [\"src/services/\", \"lib/\"])'),\n ignore: z.array(z.string()).optional().describe('Exclude results whose filePath starts with any of these prefixes (e.g. [\"src/tests/\", \"src/mocks/\"])'),\n repo: z.string().describe('Repository path (default: BRAINBANK_REPO)'),\n // BrainBankQL context fields\n lines: z.boolean().optional().describe('Prefix each code line with its source line number (e.g. 127| code)'),\n symbols: z.boolean().optional().describe('Append symbol index (all functions, classes, interfaces) for matched files'),\n compact: z.boolean().optional().describe('Show only function/class signatures, skip bodies'),\n callTree: z.union([z.boolean(), z.object({ depth: z.number() })]).optional().describe('Include call tree expansion. Pass { depth: N } to control depth'),\n imports: z.boolean().optional().describe('Include dependency/import summary section'),\n context: z.string().optional().describe('General task context for the pruner — e.g. GitHub issue body, requirements doc, conversation context. Helps the AI pruner understand the broader task'),\n pruner: z.string().optional().describe('Specific pruning instructions — tells the pruner exactly what to focus on (e.g. \"only keep files related to WebSocket JWT refresh\")'),\n }),\n },\n async ({ task, affectedFiles, codeResults, gitResults, docsResults, sources, path, ignore, repo, lines, symbols, compact, callTree, imports, context: ctx, pruner: prunerCtx }) => {\n const repoPath = resolveRepoPath(repo);\n const brainbank = await getBrainBank(repo);\n\n // Build sources from explicit params, then let `sources` override\n const base: Record<string, number> = { code: codeResults, git: gitResults };\n if (docsResults !== undefined) base.docs = docsResults;\n const resolvedSources = sources ? { ...base, ...sources } : base;\n\n // Build fields from explicit params (only include defined values)\n const fields: Record<string, unknown> = {};\n if (lines !== undefined) fields.lines = lines;\n if (symbols !== undefined) fields.symbols = symbols;\n if (compact !== undefined) fields.compact = compact;\n if (callTree !== undefined) fields.callTree = callTree;\n if (imports !== undefined) fields.imports = imports;\n\n const context = await brainbank.getContext(task, {\n affectedFiles,\n sources: resolvedSources,\n pathPrefix: path,\n ignorePaths: ignore,\n source: 'mcp',\n fields: Object.keys(fields).length > 0 ? fields : undefined,\n context: ctx,\n prunerContext: prunerCtx,\n });\n\n return { content: [{ type: 'text' as const, text: context }] };\n },\n);\n\n\n\n// ── Tool: brainbank_index ───────────────────────────\n\nserver.registerTool(\n 'brainbank_index',\n {\n title: 'BrainBank Index',\n description:\n 'Index (or re-index) a repository. Scans code files, git history, and documents, ' +\n 'then builds vector embeddings and FTS5 indices. Run this after significant code changes ' +\n 'or when search results feel stale. Returns a summary of what changed.',\n inputSchema: z.object({\n repo: z.string().describe('Repository path to index'),\n forceReindex: z.boolean().optional().default(false).describe('Force re-index all files (ignore cache). Use when embeddings changed or index is corrupted'),\n modules: z.array(z.string()).optional().describe('Which plugins to index (e.g. [\"code\", \"git\", \"docs\"]). Omit to use config defaults'),\n }),\n },\n async ({ repo, forceReindex, modules }) => {\n const brainbank = await getBrainBank(repo);\n\n const result = await brainbank.index({\n forceReindex,\n modules: modules && modules.length > 0 ? modules : undefined,\n });\n\n // Format results summary\n const lines: string[] = ['# Index Results\\n'];\n let hasChanges = false;\n\n for (const [name, value] of Object.entries(result)) {\n if (!value) continue;\n const v = value as Record<string, unknown>;\n if (typeof v.indexed !== 'number') {\n lines.push(`✓ ${name}: done`);\n continue;\n }\n\n const indexed = v.indexed as number;\n const skipped = (v.skipped ?? 0) as number;\n const removed = (v.removed ?? 0) as number;\n const chunks = (v.chunks ?? 0) as number;\n\n if (indexed > 0 || removed > 0) hasChanges = true;\n\n const parts: string[] = [];\n if (indexed > 0) parts.push(`+${indexed} files (${chunks} chunks)`);\n if (removed > 0) parts.push(`−${removed} files`);\n if (skipped > 0) parts.push(`${skipped} unchanged`);\n\n lines.push(`${name}: ${parts.join(' ')}`);\n }\n\n if (!hasChanges) {\n lines.push('No changes — everything up to date');\n }\n\n // Stats\n const stats = brainbank.stats();\n lines.push('\\n## Totals\\n');\n for (const [name, s] of Object.entries(stats)) {\n if (!s || typeof s !== 'object') continue;\n const entries = Object.entries(s as Record<string, unknown>)\n .map(([k, v]) => `${k}: ${v}`)\n .join(', ');\n lines.push(`${name}: ${entries}`);\n }\n\n return { content: [{ type: 'text' as const, text: lines.join('\\n') }] };\n },\n);\n\n\n// ── Start Server ────────────────────────────────────\n\nasync function main() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch(err => {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`BrainBank MCP Server Error: ${message}`);\n process.exit(1);\n});\n","/**\n * WorkspacePool — BrainBank instance lifecycle manager.\n *\n * Manages cached BrainBank instances per workspace with:\n * - Memory-pressure eviction (oldest idle first)\n * - TTL eviction for inactive workspaces\n * - Active-operation tracking (prevents mid-query eviction)\n * - Hot-reload of stale HNSW indices\n */\n\nimport type { BrainBank } from '@/brainbank.ts';\n\n/** Pool configuration. */\nexport interface PoolOptions {\n /** Max total estimated memory in MB. Default: 2048. */\n maxMemoryMB?: number;\n /** Minutes of inactivity before eviction. Default: 30. */\n ttlMinutes?: number;\n /** Factory function to create a BrainBank for a repo path. */\n factory: (repoPath: string) => Promise<BrainBank>;\n /** Called when a workspace is evicted. */\n onEvict?: (repoPath: string) => void;\n /** Called when an error occurs during pool operations. */\n onError?: (repoPath: string, err: unknown) => void;\n}\n\n/** Internal pool entry. */\ninterface PoolEntry {\n brain: BrainBank;\n repoPath: string;\n lastAccess: number;\n createdAt: number;\n activeOps: number;\n}\n\n/** Public pool statistics. */\nexport interface PoolStats {\n size: number;\n totalMemoryMB: number;\n entries: PoolEntryStats[];\n}\n\n/** Per-entry statistics. */\nexport interface PoolEntryStats {\n repoPath: string;\n lastAccessAgo: string;\n memoryMB: number;\n activeOps: number;\n}\n\nconst DEFAULT_MAX_MEMORY_MB = 2048;\nconst DEFAULT_TTL_MINUTES = 30;\nconst EVICTION_INTERVAL_MS = 60_000;\n\n/** Format milliseconds as a human-readable \"ago\" string. */\nfunction formatAgo(ms: number): string {\n if (ms < 60_000) return `${Math.round(ms / 1000)}s ago`;\n if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m ago`;\n return `${Math.round(ms / 3_600_000)}h ago`;\n}\n\nexport class WorkspacePool {\n private _pool = new Map<string, PoolEntry>();\n private _timer: ReturnType<typeof setInterval> | null = null;\n private _maxMemoryBytes: number;\n private _ttlMs: number;\n private _factory: (repoPath: string) => Promise<BrainBank>;\n private _onEvict?: (repoPath: string) => void;\n private _onError?: (repoPath: string, err: unknown) => void;\n\n constructor(options: PoolOptions) {\n this._maxMemoryBytes = (options.maxMemoryMB ?? DEFAULT_MAX_MEMORY_MB) * 1024 * 1024;\n this._ttlMs = (options.ttlMinutes ?? DEFAULT_TTL_MINUTES) * 60 * 1000;\n this._factory = options.factory;\n this._onEvict = options.onEvict;\n this._onError = options.onError;\n\n this._timer = setInterval(() => this._evictStale(), EVICTION_INTERVAL_MS);\n // Don't hold the process open for the timer\n if (this._timer.unref) this._timer.unref();\n }\n\n /** Number of cached workspaces. */\n get size(): number {\n return this._pool.size;\n }\n\n /**\n * Get a BrainBank for the given repo path.\n * Returns a cached instance (with hot-reload) or creates a new one.\n */\n async get(repoPath: string): Promise<BrainBank> {\n const key = repoPath.replace(/\\/+$/, '');\n\n const existing = this._pool.get(key);\n if (existing) {\n existing.lastAccess = Date.now();\n try { await existing.brain.ensureFresh(); } catch { /* stale is better than nothing */ }\n return existing.brain;\n }\n\n this._evictByMemoryPressure();\n\n const brain = await this._factory(key);\n this._pool.set(key, {\n brain,\n repoPath: key,\n lastAccess: Date.now(),\n createdAt: Date.now(),\n activeOps: 0,\n });\n\n return brain;\n }\n\n /**\n * Execute an operation with active-op tracking.\n * Prevents the workspace from being evicted while the operation runs.\n */\n async withBrain<T>(repoPath: string, fn: (brain: BrainBank) => Promise<T>): Promise<T> {\n const brain = await this.get(repoPath);\n const key = repoPath.replace(/\\/+$/, '');\n const entry = this._pool.get(key);\n if (entry) entry.activeOps++;\n\n try {\n return await fn(brain);\n } finally {\n if (entry) {\n entry.activeOps--;\n entry.lastAccess = Date.now();\n }\n }\n }\n\n /** Manually evict a specific workspace. */\n evict(repoPath: string): void {\n const key = repoPath.replace(/\\/+$/, '');\n this._evictEntry(key);\n }\n\n /** Get pool statistics. */\n stats(): PoolStats {\n const now = Date.now();\n let totalMemory = 0;\n const entries: PoolEntryStats[] = [];\n\n for (const entry of this._pool.values()) {\n const memBytes = entry.brain.memoryHint();\n const memMB = Math.round(memBytes / 1024 / 1024 * 100) / 100;\n totalMemory += memBytes;\n\n entries.push({\n repoPath: entry.repoPath,\n lastAccessAgo: formatAgo(now - entry.lastAccess),\n memoryMB: memMB,\n activeOps: entry.activeOps,\n });\n }\n\n return {\n size: this._pool.size,\n totalMemoryMB: Math.round(totalMemory / 1024 / 1024 * 100) / 100,\n entries,\n };\n }\n\n /** Close all entries and stop the eviction timer. */\n close(): void {\n if (this._timer) {\n clearInterval(this._timer);\n this._timer = null;\n }\n for (const key of [...this._pool.keys()]) {\n this._evictEntry(key);\n }\n }\n\n /** Evict workspaces that haven't been accessed within the TTL. */\n private _evictStale(): void {\n const cutoff = Date.now() - this._ttlMs;\n for (const [key, entry] of this._pool) {\n if (entry.lastAccess < cutoff && entry.activeOps === 0) {\n this._evictEntry(key);\n }\n }\n }\n\n /** Evict oldest idle entries until total memory is under the limit. */\n private _evictByMemoryPressure(): void {\n let totalMemory = 0;\n for (const entry of this._pool.values()) {\n totalMemory += entry.brain.memoryHint();\n }\n\n if (totalMemory < this._maxMemoryBytes) return;\n\n // Sort by lastAccess ascending (oldest first), filter idle\n const candidates = [...this._pool.entries()]\n .filter(([, e]) => e.activeOps === 0)\n .sort(([, a], [, b]) => a.lastAccess - b.lastAccess);\n\n for (const [key, entry] of candidates) {\n if (totalMemory < this._maxMemoryBytes) break;\n totalMemory -= entry.brain.memoryHint();\n this._evictEntry(key);\n }\n }\n\n /** Evict a single entry by key. */\n private _evictEntry(key: string): void {\n const entry = this._pool.get(key);\n if (!entry) return;\n\n try {\n entry.brain.close();\n } catch (err: unknown) {\n this._onError?.(key, err);\n }\n\n this._pool.delete(key);\n this._onEvict?.(key);\n }\n}\n","/**\n * WorkspaceFactory — creates BrainBank instances via the core factory.\n *\n * Delegates to `createBrain()` from the core factory, passing\n * a portable `BrainContext`. No plugin hardcoding — the factory handles\n * plugin discovery from config and installed packages.\n */\n\nimport type { BrainBank } from '@/brainbank.ts';\nimport type { BrainContext } from '@/cli/factory/brain-context.ts';\n\nimport { createBrain, resetFactoryCache } from '@/cli/factory/index.ts';\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\n/**\n * Detect repo root by walking up from startDir until we find `.git/`.\n * Returns startDir itself if no `.git/` is found.\n */\nexport function findRepoRoot(startDir: string): string {\n let dir = path.resolve(startDir);\n while (true) {\n if (fs.existsSync(path.join(dir, '.git'))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return path.resolve(startDir);\n}\n\n/** Resolve the effective repo path from an optional target, env, or cwd. */\nexport function resolveRepoPath(targetRepo?: string): string {\n const rp = targetRepo\n ?? process.env.BRAINBANK_REPO\n ?? findRepoRoot(process.cwd());\n return rp.replace(/\\/+$/, '');\n}\n\n/**\n * Create a BrainBank instance for a workspace.\n * Uses the core factory which handles:\n * - Config loading from .brainbank/config.json\n * - Dynamic plugin discovery and registration\n * - Embedding/pruner/expander provider setup\n * - Folder plugin auto-discovery\n */\nexport async function createWorkspaceBrain(repoPath: string): Promise<BrainBank> {\n resetFactoryCache();\n\n const context: BrainContext = {\n repoPath,\n env: process.env as Record<string, string | undefined>,\n };\n\n // Silence stdout during initialization — the core factory emits ANSI-colored\n // console.log messages (plugin loading) that corrupt\n // the MCP JSON-RPC stdio transport. Redirect console.log → stderr temporarily.\n const origLog = console.log;\n console.log = (...args: unknown[]) => console.error(...args);\n try {\n const brain = await createBrain(context);\n await brain.initialize();\n return brain;\n } finally {\n console.log = origLog;\n }\n}\n"],"mappings":"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","names":["path"]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createBrain,
3
3
  pruneResults
4
- } from "./chunk-3UIWA32X.js";
4
+ } from "./chunk-I46ALO53.js";
5
5
  import "./chunk-M744PCJQ.js";
6
6
  import {
7
7
  isExpandablePlugin
@@ -1901,4 +1901,4 @@ __name(runStatsTui, "runStatsTui");
1901
1901
  export {
1902
1902
  runStatsTui
1903
1903
  };
1904
- //# sourceMappingURL=stats-tui-AD3AMYGV.js.map
1904
+ //# sourceMappingURL=stats-tui-RI42XJZ5.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainbank",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Pluggable semantic memory for AI agents — hybrid search (vector + BM25) in a single SQLite file. Built-in code, git, and docs indexers. Bring your own.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -2,26 +2,13 @@
2
2
  * BrainBank — MCP Server
3
3
  *
4
4
  * Exposes BrainBank as an MCP server via stdio transport.
5
- * Works with Google Antigravity, Claude Desktop, and any MCP-compatible client.
6
- *
7
- * Usage in mcp_config.json:
8
- * {
9
- * "mcpServers": {
10
- * "brainbank": {
11
- * "command": "brainbank-mcp"
12
- * }
13
- * }
14
- * }
5
+ * Works with Google Antigravity, Claude Code, and any MCP-compatible client.
15
6
  *
16
7
  * Tools:
17
- * brainbank_contextWorkflow Trace: search + call tree + called-by annotations
18
- * brainbank_search Hybrid search (vector + BM25 → RRF), returns structured results
19
- *
20
- * Indexing is handled by the CLI (`brainbank index`) — not exposed as an MCP tool.
8
+ * brainbank_queryAI-pruned semantic context for a task
9
+ * brainbank_index Index/re-index a repository
21
10
  */
22
11
 
23
- import type { SearchResult } from '@/types.ts';
24
- import type { SearchOptions } from '@/search/types.ts';
25
12
 
26
13
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
27
14
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -53,19 +40,19 @@ async function getBrainBank(targetRepo?: string) {
53
40
 
54
41
  const server = new McpServer({
55
42
  name: 'brainbank',
56
- version: '0.1.3',
43
+ version: '0.1.5',
57
44
  });
58
45
 
59
- // ── Tool: brainbank_context ─────────────────────────
46
+ // ── Tool: brainbank_query ───────────────────────────
60
47
 
61
48
  server.registerTool(
62
- 'brainbank_context',
49
+ 'brainbank_query',
63
50
  {
64
51
  title: 'BrainBank Context',
65
52
  description:
66
- 'Get a formatted knowledge context block for a task. Returns a Workflow Trace: ' +
67
- 'search hits + full call tree with `called by` annotations, topologically ordered. ' +
68
- 'All source code included no trimming, no truncation.',
53
+ 'Get AI-pruned semantic context for a task. Returns search hits + full call tree ' +
54
+ 'with `called by` annotations, topologically ordered. Pass `context` for general ' +
55
+ 'background and `pruner` for specific filtering instructions.',
69
56
  inputSchema: z.object({
70
57
  task: z.string().describe('Description of the task you need context for'),
71
58
  affectedFiles: z.array(z.string()).optional().default([]).describe('Files you plan to modify (improves co-edit suggestions)'),
@@ -118,69 +105,6 @@ server.registerTool(
118
105
  },
119
106
  );
120
107
 
121
- // ── Tool: brainbank_search ──────────────────────────
122
-
123
- /** Format SearchResult[] into readable text blocks for the agent. */
124
- function formatSearchResults(results: SearchResult[]): string {
125
- if (results.length === 0) return 'No results found.';
126
-
127
- const lines: string[] = [];
128
- for (const r of results) {
129
- const score = Math.round(r.score * 100);
130
- if (r.type === 'code') {
131
- const m = r.metadata;
132
- lines.push(`[CODE ${score}%] ${r.filePath} — ${m.name || m.chunkType} L${m.startLine}-${m.endLine}`);
133
- lines.push(r.content);
134
- lines.push('');
135
- } else if (r.type === 'commit') {
136
- const m = r.metadata;
137
- lines.push(`[COMMIT ${score}%] ${m.shortHash} ${r.content} (${m.author})`);
138
- if (m.files?.length) lines.push(` Files: ${m.files.slice(0, 6).join(', ')}`);
139
- lines.push('');
140
- } else if (r.type === 'document') {
141
- const ctx = r.context ? ` — ${r.context}` : '';
142
- lines.push(`[DOC ${score}%] ${r.filePath} [${r.metadata.collection}]${ctx}`);
143
- lines.push(r.content);
144
- lines.push('');
145
- } else if (r.type === 'collection') {
146
- lines.push(`[COLLECTION ${score}%] ${r.metadata.collection ?? 'kv'}`);
147
- lines.push(r.content);
148
- lines.push('');
149
- }
150
- }
151
- return lines.join('\n');
152
- }
153
-
154
- server.registerTool(
155
- 'brainbank_search',
156
- {
157
- title: 'BrainBank Search',
158
- description:
159
- 'Hybrid search across the codebase: combines vector similarity + BM25 keyword matching ' +
160
- 'via Reciprocal Rank Fusion. Returns code chunks, git commits, and documents ranked by relevance. ' +
161
- 'Use for finding code, understanding systems, and locating files by meaning.',
162
- inputSchema: z.object({
163
- query: z.string().describe('Natural language search query (e.g. "authentication middleware", "WebSocket reconnection")'),
164
- repo: z.string().describe('Repository path (default: BRAINBANK_REPO)'),
165
- sources: z.record(z.number()).optional().describe('Per-source result limits (e.g. { code: 10, git: 0, docs: 5 }). Set source to 0 to skip it'),
166
- path: z.union([z.string(), z.array(z.string())]).optional().describe('Filter results to files under these path prefixes (e.g. "src/services" or ["src/", "lib/"])'),
167
- maxResults: z.number().optional().default(20).describe('Maximum total results to return'),
168
- }),
169
- },
170
- async ({ query, repo, sources, path, maxResults }) => {
171
- const brainbank = await getBrainBank(repo);
172
-
173
- const opts: Record<string, unknown> = { source: 'mcp' as const };
174
- if (sources) opts.sources = sources;
175
- if (path) opts.pathPrefix = path;
176
-
177
- const results = await brainbank.hybridSearch(query, opts as SearchOptions);
178
- const limited = results.slice(0, maxResults);
179
- const text = formatSearchResults(limited);
180
-
181
- return { content: [{ type: 'text' as const, text }] };
182
- },
183
- );
184
108
 
185
109
 
186
110
  // ── Tool: brainbank_index ───────────────────────────
@@ -77,8 +77,11 @@ export class ContextBuilder {
77
77
  if (pruner && results.length > 1) {
78
78
  dbg(`[pruner] Running ${_prunerName(pruner)} on ${results.length} results...`);
79
79
  const pruneT0 = Date.now();
80
- // Merge context + prunerContext into a single pruner description
81
- const prunerDesc = [options.context, options.prunerContext].filter(Boolean).join('\n\n') || undefined;
80
+ // Merge context + prunerContext into a single pruner description.
81
+ // Auto-mode: when no explicit context/pruner is given, inject a default
82
+ // instruction so the pruner always filters noise aggressively.
83
+ const explicitDesc = [options.context, options.prunerContext].filter(Boolean).join('\n\n');
84
+ const prunerDesc = explicitDesc || 'Auto-mode: aggressively drop files that are not directly related to the query. Keep only core implementation, types, and orchestration files. Drop infrastructure, config, unrelated services, and boilerplate.';
82
85
  results = await pruneResults(task, results, pruner, prunerDesc);
83
86
  const pruneMs = Date.now() - pruneT0;
84
87
  const dropped = beforePrune.filter(r => !results.includes(r));