brainbank 0.7.0 → 0.8.0

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.
Files changed (156) hide show
  1. package/README.md +76 -1398
  2. package/bin/brainbank +5 -1
  3. package/dist/{chunk-N2OJRXSB.js → chunk-3HVCONGF.js} +1 -1
  4. package/dist/{chunk-N2OJRXSB.js.map → chunk-3HVCONGF.js.map} +1 -1
  5. package/dist/{chunk-CCXVL56V.js → chunk-3JZIM5AU.js} +6 -3
  6. package/dist/chunk-3JZIM5AU.js.map +1 -0
  7. package/dist/{chunk-6XOXM7MI.js → chunk-5KU2PP34.js} +2 -2
  8. package/dist/{chunk-6XOXM7MI.js.map → chunk-5KU2PP34.js.map} +1 -1
  9. package/dist/chunk-7JDCHUJV.js +89 -0
  10. package/dist/chunk-7JDCHUJV.js.map +1 -0
  11. package/dist/{chunk-B77KABWH.js → chunk-7T2ZCZQA.js} +17 -15
  12. package/dist/chunk-7T2ZCZQA.js.map +1 -0
  13. package/dist/chunk-E3J37GDA.js +74 -0
  14. package/dist/chunk-E3J37GDA.js.map +1 -0
  15. package/dist/chunk-JEFWMS5Z.js +217 -0
  16. package/dist/chunk-JEFWMS5Z.js.map +1 -0
  17. package/dist/chunk-JRVYSTMP.js +3256 -0
  18. package/dist/chunk-JRVYSTMP.js.map +1 -0
  19. package/dist/chunk-OLDHLOMT.js +69 -0
  20. package/dist/chunk-OLDHLOMT.js.map +1 -0
  21. package/dist/{chunk-424UFCY7.js → chunk-QTZNB6AK.js} +6 -2
  22. package/dist/chunk-QTZNB6AK.js.map +1 -0
  23. package/dist/chunk-RFF7HMP6.js +109 -0
  24. package/dist/chunk-RFF7HMP6.js.map +1 -0
  25. package/dist/{chunk-ZNLN2VWV.js → chunk-TD3TEFI3.js} +1 -1
  26. package/dist/chunk-TD3TEFI3.js.map +1 -0
  27. package/dist/cli.js +1036 -341
  28. package/dist/cli.js.map +1 -1
  29. package/dist/haiku-expander-WOVJIVXD.js +8 -0
  30. package/dist/haiku-pruner-DB77ZQLJ.js +8 -0
  31. package/dist/http-server-GIRELCCL.js +9 -0
  32. package/dist/index.d.ts +1774 -611
  33. package/dist/index.js +282 -70
  34. package/dist/index.js.map +1 -1
  35. package/dist/{local-embedding-ZIMTK6PU.js → local-embedding-2RNCC5EU.js} +2 -2
  36. package/dist/{openai-embedding-VQZCZQYT.js → openai-embedding-Z5I4K4CN.js} +2 -2
  37. package/dist/perplexity-context-embedding-V5YUMXDR.js +9 -0
  38. package/dist/{perplexity-embedding-227WQY4R.js → perplexity-embedding-X2S72OAC.js} +2 -2
  39. package/dist/plugin-FF4Q34TI.js +32 -0
  40. package/dist/{qwen3-reranker-3MHEENT5.js → qwen3-reranker-HVIQOLKS.js} +2 -2
  41. package/dist/{resolve-CUJWY6HP.js → resolve-Q5D6HECY.js} +2 -2
  42. package/package.json +25 -52
  43. package/src/brainbank.ts +620 -0
  44. package/src/cli/commands/collection.ts +77 -0
  45. package/src/cli/commands/context.ts +171 -0
  46. package/src/cli/commands/daemon.ts +100 -0
  47. package/src/cli/commands/docs.ts +71 -0
  48. package/src/cli/commands/files.ts +69 -0
  49. package/src/cli/commands/help.ts +72 -0
  50. package/src/cli/commands/index.ts +282 -0
  51. package/src/cli/commands/kv.ts +140 -0
  52. package/src/cli/commands/mcp.ts +13 -0
  53. package/src/cli/commands/reembed.ts +30 -0
  54. package/src/cli/commands/scan.ts +365 -0
  55. package/src/cli/commands/search.ts +130 -0
  56. package/src/cli/commands/stats.ts +44 -0
  57. package/src/cli/commands/status.ts +47 -0
  58. package/src/cli/commands/watch.ts +43 -0
  59. package/src/cli/factory/brain-context.ts +43 -0
  60. package/src/cli/factory/builtin-registration.ts +123 -0
  61. package/src/cli/factory/config-loader.ts +72 -0
  62. package/src/cli/factory/index.ts +65 -0
  63. package/src/cli/factory/plugin-loader.ts +146 -0
  64. package/src/cli/index.ts +63 -0
  65. package/src/cli/server-client.ts +135 -0
  66. package/src/cli/utils.ts +121 -0
  67. package/src/config.ts +50 -0
  68. package/src/constants.ts +13 -0
  69. package/src/db/adapter.ts +112 -0
  70. package/src/db/metadata.ts +130 -0
  71. package/src/db/migrations.ts +66 -0
  72. package/src/db/sqlite-adapter.ts +208 -0
  73. package/src/db/tracker.ts +91 -0
  74. package/src/engine/index-api.ts +85 -0
  75. package/src/engine/reembed.ts +206 -0
  76. package/src/engine/search-api.ts +222 -0
  77. package/src/index.ts +159 -0
  78. package/src/lib/fts.ts +57 -0
  79. package/src/lib/languages.ts +180 -0
  80. package/src/lib/logger.ts +125 -0
  81. package/src/lib/math.ts +87 -0
  82. package/src/lib/provider-key.ts +20 -0
  83. package/src/lib/prune.ts +71 -0
  84. package/src/lib/rerank.ts +33 -0
  85. package/src/lib/rrf.ts +133 -0
  86. package/src/lib/write-lock.ts +108 -0
  87. package/src/plugin.ts +323 -0
  88. package/src/providers/embeddings/embedding-worker-thread.ts +95 -0
  89. package/src/providers/embeddings/embedding-worker.ts +141 -0
  90. package/src/providers/embeddings/local-embedding.ts +115 -0
  91. package/src/providers/embeddings/openai-embedding.ts +167 -0
  92. package/src/providers/embeddings/perplexity-context-embedding.ts +195 -0
  93. package/src/providers/embeddings/perplexity-embedding.ts +165 -0
  94. package/src/providers/embeddings/resolve.ts +34 -0
  95. package/src/providers/pruners/haiku-expander.ts +152 -0
  96. package/src/providers/pruners/haiku-pruner.ts +112 -0
  97. package/src/providers/rerankers/qwen3-reranker.ts +180 -0
  98. package/src/providers/vector/hnsw-index.ts +174 -0
  99. package/src/providers/vector/hnsw-loader.ts +129 -0
  100. package/src/search/bm25-boost.ts +61 -0
  101. package/src/search/context-builder.ts +298 -0
  102. package/src/search/keyword/composite-bm25-search.ts +62 -0
  103. package/src/search/types.ts +35 -0
  104. package/src/search/vector/composite-vector-search.ts +76 -0
  105. package/src/search/vector/mmr.ts +64 -0
  106. package/src/services/collection.ts +405 -0
  107. package/src/services/daemon.ts +87 -0
  108. package/src/services/http-server.ts +288 -0
  109. package/src/services/kv-service.ts +65 -0
  110. package/src/services/plugin-registry.ts +109 -0
  111. package/src/services/watch.ts +348 -0
  112. package/src/services/webhook-server.ts +100 -0
  113. package/src/types.ts +504 -0
  114. package/dist/base-3SNc_CeY.d.ts +0 -593
  115. package/dist/chunk-424UFCY7.js.map +0 -1
  116. package/dist/chunk-7EZR47JV.js +0 -232
  117. package/dist/chunk-7EZR47JV.js.map +0 -1
  118. package/dist/chunk-B77KABWH.js.map +0 -1
  119. package/dist/chunk-CCXVL56V.js.map +0 -1
  120. package/dist/chunk-DI3H6JVZ.js +0 -2432
  121. package/dist/chunk-DI3H6JVZ.js.map +0 -1
  122. package/dist/chunk-FGL32LUJ.js +0 -754
  123. package/dist/chunk-FGL32LUJ.js.map +0 -1
  124. package/dist/chunk-JRSKWF6K.js +0 -313
  125. package/dist/chunk-JRSKWF6K.js.map +0 -1
  126. package/dist/chunk-U2Q2XGPZ.js +0 -42
  127. package/dist/chunk-U2Q2XGPZ.js.map +0 -1
  128. package/dist/chunk-VQ27YUHH.js +0 -629
  129. package/dist/chunk-VQ27YUHH.js.map +0 -1
  130. package/dist/chunk-VVXYZIIB.js +0 -304
  131. package/dist/chunk-VVXYZIIB.js.map +0 -1
  132. package/dist/chunk-YOLKSYWK.js +0 -79
  133. package/dist/chunk-YOLKSYWK.js.map +0 -1
  134. package/dist/chunk-ZNLN2VWV.js.map +0 -1
  135. package/dist/code.d.ts +0 -33
  136. package/dist/code.js +0 -9
  137. package/dist/docs.d.ts +0 -21
  138. package/dist/docs.js +0 -9
  139. package/dist/git.d.ts +0 -33
  140. package/dist/git.js +0 -9
  141. package/dist/memory.d.ts +0 -17
  142. package/dist/memory.js +0 -9
  143. package/dist/notes.d.ts +0 -17
  144. package/dist/notes.js +0 -10
  145. package/dist/perplexity-context-embedding-KSVSZXMD.js +0 -9
  146. package/dist/resolve-CUJWY6HP.js.map +0 -1
  147. /package/dist/{code.js.map → haiku-expander-WOVJIVXD.js.map} +0 -0
  148. /package/dist/{docs.js.map → haiku-pruner-DB77ZQLJ.js.map} +0 -0
  149. /package/dist/{git.js.map → http-server-GIRELCCL.js.map} +0 -0
  150. /package/dist/{local-embedding-ZIMTK6PU.js.map → local-embedding-2RNCC5EU.js.map} +0 -0
  151. /package/dist/{memory.js.map → openai-embedding-Z5I4K4CN.js.map} +0 -0
  152. /package/dist/{notes.js.map → perplexity-context-embedding-V5YUMXDR.js.map} +0 -0
  153. /package/dist/{openai-embedding-VQZCZQYT.js.map → perplexity-embedding-X2S72OAC.js.map} +0 -0
  154. /package/dist/{perplexity-context-embedding-KSVSZXMD.js.map → plugin-FF4Q34TI.js.map} +0 -0
  155. /package/dist/{perplexity-embedding-227WQY4R.js.map → qwen3-reranker-HVIQOLKS.js.map} +0 -0
  156. /package/dist/{qwen3-reranker-3MHEENT5.js.map → resolve-Q5D6HECY.js.map} +0 -0
@@ -0,0 +1,123 @@
1
+ /**
2
+ * BrainBank CLI — Plugin Registration
3
+ *
4
+ * Generic plugin registration with multi-repo detection
5
+ * and per-plugin config resolution. No hardcoded plugin names.
6
+ */
7
+
8
+ import type { BrainBank } from '@/brainbank.ts';
9
+ import type { DocumentCollection } from '@/types.ts';
10
+ import type { ProjectConfig } from './config-loader.ts';
11
+
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+ import { c } from '../utils.ts';
15
+ import { loadPlugin, isMultiRepoCapable, resolveEmbeddingKey } from './plugin-loader.ts';
16
+
17
+ /** Read a nested property from a generic config section. */
18
+ function pluginCfg(config: ProjectConfig | null, pluginName: string): Record<string, unknown> {
19
+ const section = config?.[pluginName];
20
+ if (section && typeof section === 'object' && !Array.isArray(section)) {
21
+ return section as Record<string, unknown>;
22
+ }
23
+ return {};
24
+ }
25
+
26
+ /** Detect subdirectories that have their own .git repo. Respects optional `repos` whitelist. */
27
+ function detectGitSubdirs(parentPath: string, repos?: string[]): { name: string; path: string }[] {
28
+ try {
29
+ const entries = fs.readdirSync(parentPath, { withFileTypes: true });
30
+ let subdirs = entries
31
+ .filter(e => {
32
+ if (e.name.startsWith('.') || e.name.startsWith('node_modules')) return false;
33
+ // Follow symlinks: isDirectory() is false for symlinks, so check via statSync
34
+ const isDir = e.isDirectory() || (e.isSymbolicLink() && fs.statSync(path.join(parentPath, e.name)).isDirectory());
35
+ return isDir && fs.existsSync(path.join(parentPath, e.name, '.git'));
36
+ })
37
+ .map(e => ({ name: e.name, path: path.join(parentPath, e.name) }));
38
+
39
+ if (repos && repos.length > 0) {
40
+ const allowed = new Set(repos);
41
+ subdirs = subdirs.filter(s => allowed.has(s.name));
42
+ }
43
+
44
+ return subdirs;
45
+ } catch { return []; }
46
+ }
47
+
48
+ /** Register plugins with multi-repo detection and per-plugin config. */
49
+ export async function registerBuiltins(
50
+ brain: BrainBank, rp: string, pluginNames: string[],
51
+ config: ProjectConfig | null, ignorePatterns: string[] = [],
52
+ ): Promise<void> {
53
+ const resolvedRp = path.resolve(rp);
54
+ const hasRootGit = fs.existsSync(path.join(resolvedRp, '.git'));
55
+ const configRepos = config?.repos as string[] | undefined;
56
+ const gitSubdirs = !hasRootGit ? detectGitSubdirs(resolvedRp, configRepos) : [];
57
+
58
+ for (const name of pluginNames) {
59
+ const factory = await loadPlugin(name);
60
+ if (!factory) {
61
+ console.error(c.yellow(` ⚠ @brainbank/${name} not installed — skipping ${name} indexing`));
62
+ console.error(c.dim(` Install: npm i -g @brainbank/${name}`));
63
+ continue;
64
+ }
65
+
66
+ const cfg = pluginCfg(config, name);
67
+
68
+ // Resolve per-plugin embedding if configured
69
+ const embKey = cfg.embedding as string | undefined;
70
+ const embeddingProvider = embKey ? await resolveEmbeddingKey(embKey) : undefined;
71
+
72
+ // Multi-repo: create one plugin instance per git subdir
73
+ if (gitSubdirs.length > 0 && isMultiRepoCapable(name)) {
74
+ console.error(c.cyan(` Multi-repo: found ${gitSubdirs.length} git repos: ${gitSubdirs.map(d => d.name).join(', ')}`));
75
+ for (const sub of gitSubdirs) {
76
+ const mergedIgnore = [...(cfg.ignore as string[] ?? []), ...ignorePatterns];
77
+ brain.use(factory({
78
+ ...cfg,
79
+ repoPath: sub.path,
80
+ name: `${name}:${sub.name}`,
81
+ embeddingProvider,
82
+ ignore: mergedIgnore.length > 0 ? mergedIgnore : undefined,
83
+ }));
84
+ }
85
+ } else {
86
+ // Single repo: merge ignore patterns for plugins that support them
87
+ const configIgnore = cfg.ignore as string[] | undefined ?? [];
88
+ const mergedIgnore = [...configIgnore, ...ignorePatterns];
89
+
90
+ brain.use(factory({
91
+ ...cfg,
92
+ repoPath: rp,
93
+ embeddingProvider,
94
+ ignore: mergedIgnore.length > 0 ? mergedIgnore : undefined,
95
+ }));
96
+ }
97
+ }
98
+ }
99
+
100
+ /** Register doc collections from config. Call after brain.initialize(). */
101
+ export async function registerConfigCollections(brain: BrainBank, rp: string, config: ProjectConfig | null): Promise<void> {
102
+ const docsCfg = pluginCfg(config, 'docs');
103
+ const collections = docsCfg.collections as DocumentCollection[] | undefined;
104
+ if (!collections?.length) return;
105
+
106
+ const { isDocsPlugin } = await import('@/plugin.ts');
107
+ const rawPlugin = brain.plugin('docs');
108
+ if (!rawPlugin || !isDocsPlugin(rawPlugin)) return;
109
+
110
+ const repoPath = path.resolve(rp);
111
+ for (const coll of collections) {
112
+ const absPath = path.resolve(repoPath, coll.path);
113
+ try {
114
+ await rawPlugin.addCollection({
115
+ name: coll.name, path: absPath,
116
+ pattern: coll.pattern ?? '**/*.md', ignore: coll.ignore, context: coll.context,
117
+ });
118
+ } catch (e: unknown) {
119
+ if (!(e instanceof Error && e.message.includes('already'))) throw e;
120
+ // Collection already registered — skip
121
+ }
122
+ }
123
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * BrainBank CLI — Config Loader
3
+ *
4
+ * Loads .brainbank/config.json (or .ts/.js/.mjs fallback).
5
+ * Config priority: CLI flags > config file > defaults.
6
+ */
7
+
8
+ import type { Plugin } from '@/plugin.ts';
9
+ import type { BrainBankConfig, DocumentCollection } from '@/types.ts';
10
+
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { c } from '../utils.ts';
14
+
15
+ /** Full .brainbank/config.json schema. */
16
+ export interface ProjectConfig {
17
+ plugins?: string[];
18
+ embedding?: string;
19
+ reranker?: string;
20
+ pruner?: string;
21
+ maxFileSize?: number;
22
+ indexers?: Plugin[];
23
+ brainbank?: Partial<BrainBankConfig>;
24
+ /** Context field defaults (e.g. { lines: true, callTree: true, symbols: false }). */
25
+ context?: Record<string, unknown>;
26
+ /** Per-plugin config sections (e.g. code, git, docs). */
27
+ [pluginName: string]: unknown;
28
+ }
29
+
30
+ const CONFIG_NAMES = ['config.json', 'config.ts', 'config.js', 'config.mjs'];
31
+ const NOT_LOADED = Symbol('not-loaded');
32
+ let _configCache: ProjectConfig | null | typeof NOT_LOADED = NOT_LOADED;
33
+
34
+ /** Load .brainbank/config.json (or .ts fallback) if present. */
35
+ export async function loadConfig(repoPath: string): Promise<ProjectConfig | null> {
36
+ if (_configCache !== NOT_LOADED) return _configCache;
37
+
38
+ const brainbankDir = path.resolve(repoPath, '.brainbank');
39
+
40
+ for (const name of CONFIG_NAMES) {
41
+ const configPath = path.join(brainbankDir, name);
42
+ if (!fs.existsSync(configPath)) continue;
43
+
44
+ try {
45
+ if (name === 'config.json') {
46
+ const raw = fs.readFileSync(configPath, 'utf-8');
47
+ _configCache = JSON.parse(raw) as ProjectConfig;
48
+ } else {
49
+ const mod = await import(configPath);
50
+ _configCache = (mod.default ?? mod) as ProjectConfig;
51
+ }
52
+ return _configCache;
53
+ } catch (err: unknown) {
54
+ const message = err instanceof Error ? err.message : String(err);
55
+ console.error(c.red(`Error loading .brainbank/${name}: ${message}`));
56
+ process.exit(1);
57
+ }
58
+ }
59
+
60
+ _configCache = null;
61
+ return null;
62
+ }
63
+
64
+ /** Get the loaded config (for use by commands). */
65
+ export async function getConfig(repoPath?: string): Promise<ProjectConfig | null> {
66
+ return loadConfig(repoPath ?? '.');
67
+ }
68
+
69
+ /** Reset config cache. Useful for tests. */
70
+ export function resetConfigCache(): void {
71
+ _configCache = NOT_LOADED;
72
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * BrainBank CLI — Brain Factory
3
+ *
4
+ * Creates a configured BrainBank instance with dynamically loaded plugins,
5
+ * auto-discovered indexers, and config file support.
6
+ * Delegates to focused modules in factory/.
7
+ */
8
+
9
+ import type { Plugin } from '@/plugin.ts';
10
+ import type { BrainBankConfig } from '@/types.ts';
11
+ import type { BrainContext } from './brain-context.ts';
12
+
13
+ import { BrainBank } from '@/brainbank.ts';
14
+ import { contextFromCLI, ctxFlag, ctxEnv } from './brain-context.ts';
15
+ import { registerBuiltins, registerConfigCollections } from './builtin-registration.ts';
16
+ import { loadConfig, getConfig, resetConfigCache } from './config-loader.ts';
17
+ import { discoverFolderPlugins, resetPluginCache, setupProviders } from './plugin-loader.ts';
18
+
19
+ export type { ProjectConfig } from './config-loader.ts';
20
+ export type { BrainContext } from './brain-context.ts';
21
+ export { contextFromCLI } from './brain-context.ts';
22
+ export { getConfig, registerConfigCollections };
23
+
24
+ /** Reset factory caches. Useful for tests. */
25
+ export function resetFactoryCache(): void {
26
+ resetConfigCache();
27
+ resetPluginCache();
28
+ }
29
+
30
+ /**
31
+ * Create a BrainBank with built-in + discovered + config plugins.
32
+ *
33
+ * Accepts either a `BrainContext` (for programmatic use) or an optional
34
+ * `repoPath` string (for CLI backward compat — builds context from argv).
35
+ */
36
+ export async function createBrain(contextOrRepo?: BrainContext | string): Promise<BrainBank> {
37
+ const ctx: BrainContext = typeof contextOrRepo === 'string'
38
+ ? contextFromCLI(contextOrRepo)
39
+ : contextOrRepo ?? contextFromCLI();
40
+
41
+ const rp = ctx.repoPath;
42
+ const config = await loadConfig(rp);
43
+ const folderPlugins = await discoverFolderPlugins(rp);
44
+
45
+ const brainOpts: Partial<BrainBankConfig> & Record<string, unknown> = { repoPath: rp, ...(config?.brainbank ?? {}) };
46
+ if (config?.maxFileSize) brainOpts.maxFileSize = config.maxFileSize as number;
47
+ await setupProviders(brainOpts, config, ctx.flags, ctx.env);
48
+
49
+ const brain = new BrainBank(brainOpts);
50
+ const builtins = config?.plugins ?? ['code', 'git', 'docs'];
51
+
52
+ // Merge ignore patterns from context flags
53
+ const ignoreFlag = ctxFlag(ctx, 'ignore');
54
+ const ignorePatterns = ignoreFlag ? ignoreFlag.split(',').map(s => s.trim()) : [];
55
+
56
+ await registerBuiltins(brain, rp, builtins, config, ignorePatterns);
57
+
58
+ for (const plugin of folderPlugins) brain.use(plugin);
59
+
60
+ if (config?.indexers) {
61
+ for (const plugin of config.indexers as Plugin[]) brain.use(plugin);
62
+ }
63
+
64
+ return brain;
65
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * BrainBank CLI — Plugin Loader
3
+ *
4
+ * Generic plugin loader registry with dynamic @brainbank/* package loading
5
+ * and auto-discovery of user plugins from .brainbank/plugins/.
6
+ */
7
+
8
+ import type { Plugin } from '@/plugin.ts';
9
+ import type { EmbeddingProvider } from '@/types.ts';
10
+ import type { ProjectConfig } from './config-loader.ts';
11
+
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+ import { c } from '../utils.ts';
15
+
16
+ /** Plugin factory — accepts config, returns Plugin. */
17
+ type PluginFactory = (opts: Record<string, unknown>) => Plugin;
18
+
19
+ /** Loader function: dynamically imports a package and returns its factory. */
20
+ type PluginLoaderFn = () => Promise<PluginFactory | null>;
21
+
22
+ /** Built-in plugin loader registry. Extensible at runtime. */
23
+ const PLUGIN_LOADERS = new Map<string, PluginLoaderFn>([
24
+ ['code', async () => { try { return (await import('@brainbank/code')).code as PluginFactory; } catch { return null; } }],
25
+ ['git', async () => { try { return (await import('@brainbank/git')).git as PluginFactory; } catch { return null; } }],
26
+ ['docs', async () => { try { return (await import('@brainbank/docs')).docs as PluginFactory; } catch { return null; } }],
27
+ ]);
28
+
29
+ /** Plugins that support multi-repo mode (one instance per git subdir). */
30
+ const MULTI_REPO_PLUGINS = new Set(['code', 'git']);
31
+
32
+ /** Load a plugin factory by name. Returns null if not installed. */
33
+ export async function loadPlugin(name: string): Promise<PluginFactory | null> {
34
+ const loader = PLUGIN_LOADERS.get(name);
35
+ if (!loader) return null;
36
+ return loader();
37
+ }
38
+
39
+ /** Register a custom plugin loader. */
40
+ export function registerPluginLoader(name: string, loader: PluginLoaderFn): void {
41
+ PLUGIN_LOADERS.set(name, loader);
42
+ }
43
+
44
+ /** Check if a plugin supports multi-repo mode. */
45
+ export function isMultiRepoCapable(name: string): boolean {
46
+ return MULTI_REPO_PLUGINS.has(name);
47
+ }
48
+
49
+ const INDEXER_EXTENSIONS = ['.ts', '.js', '.mjs'];
50
+ const NOT_LOADED = Symbol('not-loaded');
51
+ let _folderPluginsCache: Plugin[] | typeof NOT_LOADED = NOT_LOADED;
52
+
53
+ /** Auto-discover plugins from .brainbank/plugins/ folder. */
54
+ export async function discoverFolderPlugins(repoPath: string): Promise<Plugin[]> {
55
+ if (_folderPluginsCache !== NOT_LOADED) return _folderPluginsCache;
56
+
57
+ const pluginsDir = path.resolve(repoPath, '.brainbank', 'plugins');
58
+
59
+ if (!fs.existsSync(pluginsDir)) {
60
+ _folderPluginsCache = [];
61
+ return [];
62
+ }
63
+
64
+ const files = fs.readdirSync(pluginsDir)
65
+ .filter(f => INDEXER_EXTENSIONS.some(ext => f.endsWith(ext)))
66
+ .sort();
67
+
68
+ const plugins: Plugin[] = [];
69
+
70
+ for (const file of files) {
71
+ const filePath = path.join(pluginsDir, file);
72
+ try {
73
+ const mod = await import(filePath);
74
+ const plugin = mod.default ?? mod;
75
+
76
+ if (plugin && typeof plugin === 'object' && plugin.name) {
77
+ plugins.push(plugin as Plugin);
78
+ } else {
79
+ console.error(c.yellow(`⚠ ${file}: must export a default Plugin with a 'name' property, skipping`));
80
+ }
81
+ } catch (err: unknown) {
82
+ const message = err instanceof Error ? err.message : String(err);
83
+ console.error(c.red(`Error loading plugin ${file}: ${message}`));
84
+ }
85
+ }
86
+
87
+ _folderPluginsCache = plugins;
88
+ return plugins;
89
+ }
90
+
91
+ /** Reset folder plugins cache. Useful for tests. */
92
+ export function resetPluginCache(): void {
93
+ _folderPluginsCache = NOT_LOADED;
94
+ }
95
+
96
+ /** Resolve an embedding key string to an EmbeddingProvider instance. */
97
+ export async function resolveEmbeddingKey(key: string): Promise<EmbeddingProvider> {
98
+ const { resolveEmbedding } = await import('@/providers/embeddings/resolve.ts');
99
+ return resolveEmbedding(key);
100
+ }
101
+
102
+ /** Configure reranker and global embedding provider on brainOpts. */
103
+ export async function setupProviders(
104
+ brainOpts: Record<string, unknown>,
105
+ config: ProjectConfig | null,
106
+ flags?: Record<string, string | undefined>,
107
+ env?: Record<string, string | undefined>,
108
+ ): Promise<void> {
109
+ const rerankerFlag = flags?.reranker ?? (config?.reranker as string | undefined);
110
+ if (rerankerFlag === 'qwen3') {
111
+ const { Qwen3Reranker } = await import('@/providers/rerankers/qwen3-reranker.ts');
112
+ brainOpts.reranker = new Qwen3Reranker();
113
+ }
114
+
115
+ const prunerFlag = flags?.pruner ?? (config?.pruner as string | undefined);
116
+ if (prunerFlag === 'haiku') {
117
+ const { HaikuPruner } = await import('@/providers/pruners/haiku-pruner.ts');
118
+ brainOpts.pruner = new HaikuPruner();
119
+ }
120
+
121
+ // Expander: explicit opt-in only (config.json `expander: "haiku"` or --expander flag)
122
+ const expanderFlag = flags?.expander ?? (config?.expander as string | undefined);
123
+ if (expanderFlag === 'haiku') {
124
+ try {
125
+ const { HaikuExpander } = await import('@/providers/pruners/haiku-expander.ts');
126
+ brainOpts.expander = new HaikuExpander();
127
+ } catch {
128
+ // Fail-open: if API key missing, skip expander silently
129
+ }
130
+ }
131
+
132
+ const embFlag = flags?.embedding
133
+ ?? (config?.embedding as string | undefined)
134
+ ?? env?.BRAINBANK_EMBEDDING
135
+ ?? process.env.BRAINBANK_EMBEDDING;
136
+ if (embFlag) {
137
+ const provider = await resolveEmbeddingKey(embFlag);
138
+ brainOpts.embeddingProvider = provider;
139
+ brainOpts.embeddingDims = provider.dims;
140
+ }
141
+
142
+ // Context field defaults from config.json "context" section
143
+ if (config?.context) {
144
+ brainOpts.contextFields = config.context;
145
+ }
146
+ }
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * BrainBank — CLI Entry Point
5
+ *
6
+ * Dispatcher that routes commands to their handler modules.
7
+ */
8
+
9
+ import { args, c } from './utils.ts';
10
+ import { cmdIndex } from './commands/index.ts';
11
+ import { cmdCollection } from './commands/collection.ts';
12
+ import { cmdKv } from './commands/kv.ts';
13
+ import { cmdDocs, cmdDocSearch } from './commands/docs.ts';
14
+ import { cmdSearch, cmdHybridSearch, cmdKeywordSearch } from './commands/search.ts';
15
+ import { cmdContext } from './commands/context.ts';
16
+ import { cmdFiles } from './commands/files.ts';
17
+ import { cmdStats } from './commands/stats.ts';
18
+ import { cmdReembed } from './commands/reembed.ts';
19
+ import { cmdWatch } from './commands/watch.ts';
20
+ import { cmdMcp } from './commands/mcp.ts';
21
+ import { cmdDaemon } from './commands/daemon.ts';
22
+ import { cmdStatus } from './commands/status.ts';
23
+ import { showHelp } from './commands/help.ts';
24
+
25
+ const command = args[0];
26
+
27
+ async function main(): Promise<void> {
28
+ switch (command) {
29
+ case 'i':
30
+ case 'index': return cmdIndex();
31
+ case 'collection': return cmdCollection();
32
+ case 'kv': return cmdKv();
33
+ case 'docs': return cmdDocs();
34
+ case 'dsearch': return cmdDocSearch();
35
+ case 'search': return cmdSearch();
36
+ case 'hsearch': return cmdHybridSearch();
37
+ case 'ksearch': return cmdKeywordSearch();
38
+ case 'context': return cmdContext();
39
+ case 'files': return cmdFiles();
40
+ case 'stats': return cmdStats();
41
+ case 'reembed': return cmdReembed();
42
+ case 'watch': return cmdWatch();
43
+ case 'mcp': return cmdMcp();
44
+ case 'serve': return cmdMcp(); // backward compat
45
+ case 'daemon': return cmdDaemon();
46
+ case 'status': return cmdStatus();
47
+ case 'help':
48
+ case '--help':
49
+ case '-h':
50
+ showHelp();
51
+ break;
52
+ default:
53
+ if (command) console.log(c.red(`Unknown command: ${command}\n`));
54
+ showHelp();
55
+ process.exit(command ? 1 : 0);
56
+ }
57
+ }
58
+
59
+ main().catch(err => {
60
+ console.error(c.red(`Error: ${err.message}`));
61
+ if (process.env.BRAINBANK_DEBUG) console.error(err.stack);
62
+ process.exit(1);
63
+ });
@@ -0,0 +1,135 @@
1
+ /**
2
+ * ServerClient — Lightweight HTTP client for the BrainBank daemon.
3
+ *
4
+ * Used by CLI commands to delegate to a running HTTP server
5
+ * instead of loading models locally. Falls back gracefully
6
+ * (returns null) if the server is unreachable.
7
+ */
8
+
9
+ import * as http from 'node:http';
10
+
11
+ import { isServerRunning } from '@/services/daemon.ts';
12
+
13
+ interface ContextOptions {
14
+ task: string;
15
+ repo?: string;
16
+ sources?: Record<string, number>;
17
+ pathPrefix?: string;
18
+ affectedFiles?: string[];
19
+ }
20
+
21
+ /**
22
+ * Try to get context from the running HTTP server.
23
+ * Returns the context string if successful, null if server is unreachable.
24
+ */
25
+ export async function tryServerContext(options: ContextOptions): Promise<string | null> {
26
+ const info = isServerRunning();
27
+ if (!info) return null;
28
+
29
+ try {
30
+ const body = JSON.stringify({
31
+ task: options.task,
32
+ repo: options.repo,
33
+ sources: options.sources,
34
+ pathPrefix: options.pathPrefix,
35
+ affectedFiles: options.affectedFiles,
36
+ });
37
+
38
+ const response = await httpPost(info.port, '/context', body);
39
+ const data = JSON.parse(response) as { context?: string; error?: string };
40
+
41
+ if (data.error) return null;
42
+ return data.context ?? null;
43
+ } catch {
44
+ // Server unreachable or error — fall back to local
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Try to trigger indexing on the running HTTP server.
51
+ * Returns the result if successful, null if server is unreachable.
52
+ */
53
+ export async function tryServerIndex(repo?: string, forceReindex?: boolean): Promise<Record<string, unknown> | null> {
54
+ const info = isServerRunning();
55
+ if (!info) return null;
56
+
57
+ try {
58
+ const body = JSON.stringify({ repo, forceReindex });
59
+ const response = await httpPost(info.port, '/index', body);
60
+ const data = JSON.parse(response) as { result?: Record<string, unknown>; error?: string };
61
+
62
+ if (data.error) return null;
63
+ return data.result ?? null;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Check server health. Returns health info or null.
71
+ */
72
+ export async function serverHealth(): Promise<{
73
+ ok: boolean;
74
+ pid: number;
75
+ port: number;
76
+ uptime: number;
77
+ workspaces: number;
78
+ } | null> {
79
+ const info = isServerRunning();
80
+ if (!info) return null;
81
+
82
+ try {
83
+ const response = await httpGet(info.port, '/health');
84
+ return JSON.parse(response) as { ok: boolean; pid: number; port: number; uptime: number; workspaces: number };
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ // ── HTTP helpers ────────────────────────────────────
91
+
92
+ function httpPost(port: number, path: string, body: string): Promise<string> {
93
+ return new Promise((resolve, reject) => {
94
+ const req = http.request({
95
+ hostname: '127.0.0.1',
96
+ port,
97
+ path,
98
+ method: 'POST',
99
+ headers: {
100
+ 'Content-Type': 'application/json',
101
+ 'Content-Length': Buffer.byteLength(body),
102
+ },
103
+ timeout: 120_000, // 2 minutes — context queries can be slow on first load
104
+ }, (res) => {
105
+ const chunks: Buffer[] = [];
106
+ res.on('data', (chunk: Buffer) => chunks.push(chunk));
107
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
108
+ });
109
+
110
+ req.on('error', reject);
111
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
112
+ req.write(body);
113
+ req.end();
114
+ });
115
+ }
116
+
117
+ function httpGet(port: number, path: string): Promise<string> {
118
+ return new Promise((resolve, reject) => {
119
+ const req = http.request({
120
+ hostname: '127.0.0.1',
121
+ port,
122
+ path,
123
+ method: 'GET',
124
+ timeout: 5_000,
125
+ }, (res) => {
126
+ const chunks: Buffer[] = [];
127
+ res.on('data', (chunk: Buffer) => chunks.push(chunk));
128
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
129
+ });
130
+
131
+ req.on('error', reject);
132
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
133
+ req.end();
134
+ });
135
+ }