brainbank 0.1.3 → 0.1.4

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 (167) hide show
  1. package/README.md +84 -1107
  2. package/assets/architecture.png +0 -0
  3. package/bin/brainbank +8 -1
  4. package/bin/brainbank-mcp +19 -0
  5. package/dist/chunk-3UIWA32X.js +3341 -0
  6. package/dist/chunk-3UIWA32X.js.map +1 -0
  7. package/dist/chunk-3YBCD6DI.js +117 -0
  8. package/dist/chunk-3YBCD6DI.js.map +1 -0
  9. package/dist/chunk-DAGVUEXL.js +258 -0
  10. package/dist/chunk-DAGVUEXL.js.map +1 -0
  11. package/dist/chunk-DMFMTOHF.js +123 -0
  12. package/dist/chunk-DMFMTOHF.js.map +1 -0
  13. package/dist/chunk-FQYKWB2Q.js +136 -0
  14. package/dist/chunk-FQYKWB2Q.js.map +1 -0
  15. package/dist/chunk-IMJJ2VEM.js +74 -0
  16. package/dist/chunk-IMJJ2VEM.js.map +1 -0
  17. package/dist/chunk-M744PCJQ.js +43 -0
  18. package/dist/chunk-M744PCJQ.js.map +1 -0
  19. package/dist/chunk-NNDY7P2R.js +211 -0
  20. package/dist/chunk-NNDY7P2R.js.map +1 -0
  21. package/dist/chunk-O3J6ZIXK.js +82 -0
  22. package/dist/chunk-O3J6ZIXK.js.map +1 -0
  23. package/dist/chunk-RDQYDLYZ.js +69 -0
  24. package/dist/chunk-RDQYDLYZ.js.map +1 -0
  25. package/dist/chunk-WCQVDF3K.js +14 -0
  26. package/dist/cli.js +2713 -325
  27. package/dist/cli.js.map +1 -1
  28. package/dist/haiku-pruner-5KVT5AI2.js +8 -0
  29. package/dist/http-server-2ZQ6I43B.js +9 -0
  30. package/dist/index.d.ts +1886 -626
  31. package/dist/index.js +319 -46
  32. package/dist/index.js.map +1 -1
  33. package/dist/local-embedding-NZQTILGV.js +8 -0
  34. package/dist/mcp.d.ts +2 -0
  35. package/dist/mcp.js +386 -0
  36. package/dist/mcp.js.map +1 -0
  37. package/dist/openai-embedding-ZP5TSUJG.js +8 -0
  38. package/dist/perplexity-context-embedding-GI5PHE6X.js +9 -0
  39. package/dist/perplexity-context-embedding-GI5PHE6X.js.map +1 -0
  40. package/dist/perplexity-embedding-KZRYGJRC.js +10 -0
  41. package/dist/perplexity-embedding-KZRYGJRC.js.map +1 -0
  42. package/dist/plugin-IKQ6IRSJ.js +32 -0
  43. package/dist/plugin-IKQ6IRSJ.js.map +1 -0
  44. package/dist/resolve-ASGLBNUC.js +10 -0
  45. package/dist/resolve-ASGLBNUC.js.map +1 -0
  46. package/dist/stats-tui-AD3AMYGV.js +1904 -0
  47. package/dist/stats-tui-AD3AMYGV.js.map +1 -0
  48. package/package.json +38 -53
  49. package/src/brainbank.ts +617 -0
  50. package/src/cli/commands/collection.ts +77 -0
  51. package/src/cli/commands/context.ts +59 -0
  52. package/src/cli/commands/daemon.ts +100 -0
  53. package/src/cli/commands/docs.ts +71 -0
  54. package/src/cli/commands/files.ts +69 -0
  55. package/src/cli/commands/help.ts +82 -0
  56. package/src/cli/commands/index.ts +478 -0
  57. package/src/cli/commands/kv.ts +140 -0
  58. package/src/cli/commands/mcp-export.ts +273 -0
  59. package/src/cli/commands/mcp.ts +6 -0
  60. package/src/cli/commands/query.ts +167 -0
  61. package/src/cli/commands/reembed.ts +30 -0
  62. package/src/cli/commands/reindex.ts +40 -0
  63. package/src/cli/commands/scan.ts +336 -0
  64. package/src/cli/commands/search.ts +203 -0
  65. package/src/cli/commands/stats.ts +68 -0
  66. package/src/cli/commands/status.ts +47 -0
  67. package/src/cli/commands/watch.ts +47 -0
  68. package/src/cli/factory/brain-context.ts +43 -0
  69. package/src/cli/factory/builtin-registration.ts +87 -0
  70. package/src/cli/factory/config-loader.ts +77 -0
  71. package/src/cli/factory/index.ts +69 -0
  72. package/src/cli/factory/plugin-loader.ts +324 -0
  73. package/src/cli/index.ts +76 -0
  74. package/src/cli/server-client.ts +186 -0
  75. package/src/cli/tui/index-tui.tsx +667 -0
  76. package/src/cli/tui/stats-data.ts +523 -0
  77. package/src/cli/tui/stats-search.ts +262 -0
  78. package/src/cli/tui/stats-tui.tsx +1465 -0
  79. package/src/cli/tui/tree-scanner.ts +650 -0
  80. package/src/cli/utils.ts +137 -0
  81. package/src/config.ts +48 -0
  82. package/src/constants.ts +21 -0
  83. package/src/db/adapter.ts +112 -0
  84. package/src/db/metadata.ts +130 -0
  85. package/src/db/migrations.ts +66 -0
  86. package/src/db/sqlite-adapter.ts +218 -0
  87. package/src/db/tracker.ts +91 -0
  88. package/src/engine/index-api.ts +81 -0
  89. package/src/engine/reembed.ts +206 -0
  90. package/src/engine/search-api.ts +218 -0
  91. package/src/index.ts +150 -0
  92. package/src/lib/fts.ts +57 -0
  93. package/src/lib/languages.ts +179 -0
  94. package/src/lib/logger.ts +126 -0
  95. package/src/lib/math.ts +87 -0
  96. package/src/lib/provider-key.ts +20 -0
  97. package/src/lib/prune.ts +72 -0
  98. package/src/lib/rrf.ts +133 -0
  99. package/src/lib/write-lock.ts +108 -0
  100. package/src/mcp/mcp-server.ts +268 -0
  101. package/src/mcp/workspace-factory.ts +68 -0
  102. package/src/mcp/workspace-pool.ts +224 -0
  103. package/src/plugin.ts +381 -0
  104. package/src/providers/embeddings/embedding-worker-thread.ts +95 -0
  105. package/src/providers/embeddings/embedding-worker.ts +141 -0
  106. package/src/providers/embeddings/local-embedding.ts +115 -0
  107. package/src/providers/embeddings/openai-embedding.ts +167 -0
  108. package/src/providers/embeddings/perplexity-context-embedding.ts +195 -0
  109. package/src/providers/embeddings/perplexity-embedding.ts +165 -0
  110. package/src/providers/embeddings/resolve.ts +34 -0
  111. package/src/providers/pruners/haiku-expander.ts +178 -0
  112. package/src/providers/pruners/haiku-pruner.ts +263 -0
  113. package/src/providers/vector/hnsw-index.ts +174 -0
  114. package/src/providers/vector/hnsw-loader.ts +129 -0
  115. package/src/search/bm25-boost.ts +76 -0
  116. package/src/search/context-builder.ts +209 -0
  117. package/src/search/keyword/composite-bm25-search.ts +47 -0
  118. package/src/search/query-decomposer.ts +124 -0
  119. package/src/search/types.ts +37 -0
  120. package/src/search/vector/composite-vector-search.ts +105 -0
  121. package/src/search/vector/mmr.ts +64 -0
  122. package/src/services/collection.ts +384 -0
  123. package/src/services/daemon.ts +87 -0
  124. package/src/services/http-server.ts +344 -0
  125. package/src/services/kv-service.ts +64 -0
  126. package/src/services/plugin-registry.ts +77 -0
  127. package/src/services/watch.ts +340 -0
  128. package/src/services/webhook-server.ts +100 -0
  129. package/src/types.ts +509 -0
  130. package/dist/chunk-2P3EGY6S.js +0 -37
  131. package/dist/chunk-2P3EGY6S.js.map +0 -1
  132. package/dist/chunk-3GAIDXRW.js +0 -105
  133. package/dist/chunk-3GAIDXRW.js.map +0 -1
  134. package/dist/chunk-4ZKBQ33J.js +0 -56
  135. package/dist/chunk-4ZKBQ33J.js.map +0 -1
  136. package/dist/chunk-7QVYU63E.js +0 -7
  137. package/dist/chunk-GOUBW7UA.js +0 -373
  138. package/dist/chunk-GOUBW7UA.js.map +0 -1
  139. package/dist/chunk-MJ3Y24H6.js +0 -185
  140. package/dist/chunk-MJ3Y24H6.js.map +0 -1
  141. package/dist/chunk-N6ZMBFDE.js +0 -224
  142. package/dist/chunk-N6ZMBFDE.js.map +0 -1
  143. package/dist/chunk-RAEBYV75.js +0 -709
  144. package/dist/chunk-RAEBYV75.js.map +0 -1
  145. package/dist/chunk-TW5NTYYZ.js +0 -2066
  146. package/dist/chunk-TW5NTYYZ.js.map +0 -1
  147. package/dist/chunk-Z5SU54HP.js +0 -171
  148. package/dist/chunk-Z5SU54HP.js.map +0 -1
  149. package/dist/code.d.ts +0 -31
  150. package/dist/code.js +0 -8
  151. package/dist/docs.d.ts +0 -19
  152. package/dist/docs.js +0 -8
  153. package/dist/git.d.ts +0 -31
  154. package/dist/git.js +0 -8
  155. package/dist/memory.d.ts +0 -19
  156. package/dist/memory.js +0 -146
  157. package/dist/memory.js.map +0 -1
  158. package/dist/notes.d.ts +0 -19
  159. package/dist/notes.js +0 -57
  160. package/dist/notes.js.map +0 -1
  161. package/dist/openai-PCTYLOWI.js +0 -8
  162. package/dist/types-Da_zLLOl.d.ts +0 -474
  163. /package/dist/{chunk-7QVYU63E.js.map → chunk-WCQVDF3K.js.map} +0 -0
  164. /package/dist/{code.js.map → haiku-pruner-5KVT5AI2.js.map} +0 -0
  165. /package/dist/{docs.js.map → http-server-2ZQ6I43B.js.map} +0 -0
  166. /package/dist/{git.js.map → local-embedding-NZQTILGV.js.map} +0 -0
  167. /package/dist/{openai-PCTYLOWI.js.map → openai-embedding-ZP5TSUJG.js.map} +0 -0
@@ -0,0 +1,47 @@
1
+ /** brainbank watch — Watch for file changes. */
2
+
3
+ import { c } from '@/cli/utils.ts';
4
+ import { createBrain } from '@/cli/factory/index.ts';
5
+ import { loadConfig } from '@/cli/factory/config-loader.ts';
6
+
7
+ export async function cmdWatch(): Promise<void> {
8
+ const brain = await createBrain();
9
+ await brain.initialize();
10
+
11
+ // Read ignore/include patterns from config (code.ignore + code.include)
12
+ const config = await loadConfig(brain.config.repoPath);
13
+ const codeIgnore = (config?.code as Record<string, unknown> | undefined)?.ignore as string[] ?? [];
14
+ const codeInclude = (config?.code as Record<string, unknown> | undefined)?.include as string[] ?? [];
15
+
16
+ console.log(c.bold('\n━━━ BrainBank Watch ━━━\n'));
17
+ console.log(c.dim(` Watching ${brain.config.repoPath} for changes...`));
18
+ if (codeInclude.length > 0) {
19
+ console.log(c.dim(` Include: ${codeInclude.join(', ')}`));
20
+ }
21
+ if (codeIgnore.length > 0) {
22
+ console.log(c.dim(` Ignoring: ${codeIgnore.join(', ')}`));
23
+ }
24
+ console.log(c.dim(' Press Ctrl+C to stop.\n'));
25
+
26
+ const watcher = brain.watch({
27
+ debounceMs: 2000,
28
+ ignore: codeIgnore,
29
+ onIndex: (sourceId: string, pluginName: string) => {
30
+ const ts = new Date().toLocaleTimeString();
31
+ console.log(` ${c.dim(ts)} ${c.green('✓')} ${c.cyan(pluginName)}: ${sourceId}`);
32
+ },
33
+ onError: (err: Error) => {
34
+ console.error(` ${c.red('✗')} ${err.message}`);
35
+ },
36
+ });
37
+
38
+ process.on('SIGINT', () => {
39
+ console.log(c.dim('\n Stopping watcher...'));
40
+ watcher.close();
41
+ brain.close();
42
+ process.exit(0);
43
+ });
44
+
45
+ await new Promise(() => {});
46
+ }
47
+
@@ -0,0 +1,43 @@
1
+ /**
2
+ * BrainBank — Brain Context
3
+ *
4
+ * Portable input for `createBrain()`. Decouples the factory from
5
+ * `process.argv` / `process.env` so it can be called from the CLI,
6
+ * MCP server, tests, or any programmatic consumer.
7
+ */
8
+
9
+ import { getFlag } from '../utils.ts';
10
+
11
+ /** Everything the factory needs to build a BrainBank instance. */
12
+ export interface BrainContext {
13
+ /** Repository root path. */
14
+ repoPath: string;
15
+ /** Environment variable overrides. Falls back to `process.env`. */
16
+ env?: Record<string, string | undefined>;
17
+ /** CLI flag overrides (e.g. `{ ignore: 'dist,vendor' }`). */
18
+ flags?: Record<string, string | undefined>;
19
+ }
20
+
21
+ /** Build a `BrainContext` from CLI argv + process.env. */
22
+ export function contextFromCLI(repoPath?: string): BrainContext {
23
+ return {
24
+ repoPath: repoPath ?? getFlag('repo') ?? '.',
25
+ env: process.env as Record<string, string | undefined>,
26
+ flags: {
27
+ ignore: getFlag('ignore'),
28
+ include: getFlag('include'),
29
+ pruner: getFlag('pruner'),
30
+ embedding: getFlag('embedding'),
31
+ },
32
+ };
33
+ }
34
+
35
+ /** Read a flag from context, falling back to process.env equivalent. */
36
+ export function ctxFlag(ctx: BrainContext, name: string): string | undefined {
37
+ return ctx.flags?.[name];
38
+ }
39
+
40
+ /** Read an env var from context, falling back to process.env. */
41
+ export function ctxEnv(ctx: BrainContext, name: string): string | undefined {
42
+ return ctx.env?.[name] ?? process.env[name];
43
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * BrainBank CLI — Plugin Registration
3
+ *
4
+ * Generic plugin registration with per-plugin config resolution.
5
+ */
6
+
7
+ import type { BrainBank } from '@/brainbank.ts';
8
+ import type { DocumentCollection } from '@/types.ts';
9
+ import type { ProjectConfig } from './config-loader.ts';
10
+
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { c } from '../utils.ts';
14
+ import { loadPlugin, resolveEmbeddingKey } from './plugin-loader.ts';
15
+
16
+ /** Read a nested property from a generic config section. */
17
+ function pluginCfg(config: ProjectConfig | null, pluginName: string): Record<string, unknown> {
18
+ const section = config?.[pluginName];
19
+ if (section && typeof section === 'object' && !Array.isArray(section)) {
20
+ return section as Record<string, unknown>;
21
+ }
22
+ return {};
23
+ }
24
+
25
+ /** Register plugins with per-plugin config. */
26
+ export async function registerBuiltins(
27
+ brain: BrainBank, rp: string, pluginNames: string[],
28
+ config: ProjectConfig | null, ignorePatterns: string[] = [], includePatterns: string[] = [],
29
+ ): Promise<void> {
30
+ for (const name of pluginNames) {
31
+ const factory = await loadPlugin(name);
32
+ if (!factory) {
33
+ console.error(c.yellow(` ⚠ @brainbank/${name} not installed — skipping ${name} indexing`));
34
+ console.error(c.dim(` Install: npm i -g @brainbank/${name}`));
35
+ continue;
36
+ }
37
+
38
+ const cfg = pluginCfg(config, name);
39
+
40
+ // Resolve per-plugin embedding if configured
41
+ const embKey = cfg.embedding as string | undefined;
42
+ const embeddingProvider = embKey ? await resolveEmbeddingKey(embKey) : undefined;
43
+
44
+ // Merge ignore/include patterns for plugins that support them
45
+ // Sources: per-plugin config (e.g. config.code.ignore), root config (config.ignore), CLI flags
46
+ const configIgnore = cfg.ignore as string[] | undefined ?? [];
47
+ const rootIgnore = (config?.ignore ?? []) as string[];
48
+ const mergedIgnore = [...configIgnore, ...rootIgnore, ...ignorePatterns];
49
+
50
+ const configInclude = cfg.include as string[] | undefined ?? [];
51
+ const rootInclude = (config?.include ?? []) as string[];
52
+ const mergedInclude = [...configInclude, ...rootInclude, ...includePatterns];
53
+
54
+ brain.use(factory({
55
+ ...cfg,
56
+ repoPath: rp,
57
+ embeddingProvider,
58
+ ignore: mergedIgnore.length > 0 ? mergedIgnore : undefined,
59
+ include: mergedInclude.length > 0 ? mergedInclude : undefined,
60
+ }));
61
+ }
62
+ }
63
+
64
+ /** Register doc collections from config. Call after brain.initialize(). */
65
+ export async function registerConfigCollections(brain: BrainBank, rp: string, config: ProjectConfig | null): Promise<void> {
66
+ const docsCfg = pluginCfg(config, 'docs');
67
+ const collections = docsCfg.collections as DocumentCollection[] | undefined;
68
+ if (!collections?.length) return;
69
+
70
+ const { isDocsPlugin } = await import('@/plugin.ts');
71
+ const rawPlugin = brain.plugin('docs');
72
+ if (!rawPlugin || !isDocsPlugin(rawPlugin)) return;
73
+
74
+ const repoPath = path.resolve(rp);
75
+ for (const coll of collections) {
76
+ const absPath = path.resolve(repoPath, coll.path);
77
+ try {
78
+ await rawPlugin.addCollection({
79
+ name: coll.name, path: absPath,
80
+ pattern: coll.pattern ?? '**/*.md', ignore: coll.ignore, context: coll.context,
81
+ });
82
+ } catch (e: unknown) {
83
+ if (!(e instanceof Error && e.message.includes('already'))) throw e;
84
+ // Collection already registered — skip
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,77 @@
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
+ pruner?: string;
20
+ maxFileSize?: number;
21
+ indexers?: Plugin[];
22
+ brainbank?: Partial<BrainBankConfig>;
23
+ /** Optional API keys — override env vars. Kept out of version control. */
24
+ keys?: {
25
+ anthropic?: string;
26
+ perplexity?: string;
27
+ openai?: string;
28
+ };
29
+ /** Context field defaults (e.g. { lines: true, callTree: true, symbols: false }). */
30
+ context?: Record<string, unknown>;
31
+ /** Per-plugin config sections (e.g. code, git, docs). */
32
+ [pluginName: string]: unknown;
33
+ }
34
+
35
+ const CONFIG_NAMES = ['config.json', 'config.ts', 'config.js', 'config.mjs'];
36
+ const NOT_LOADED = Symbol('not-loaded');
37
+ let _configCache: ProjectConfig | null | typeof NOT_LOADED = NOT_LOADED;
38
+
39
+ /** Load .brainbank/config.json (or .ts fallback) if present. */
40
+ export async function loadConfig(repoPath: string): Promise<ProjectConfig | null> {
41
+ if (_configCache !== NOT_LOADED) return _configCache;
42
+
43
+ const brainbankDir = path.resolve(repoPath, '.brainbank');
44
+
45
+ for (const name of CONFIG_NAMES) {
46
+ const configPath = path.join(brainbankDir, name);
47
+ if (!fs.existsSync(configPath)) continue;
48
+
49
+ try {
50
+ if (name === 'config.json') {
51
+ const raw = fs.readFileSync(configPath, 'utf-8');
52
+ _configCache = JSON.parse(raw) as ProjectConfig;
53
+ } else {
54
+ const mod = await import(configPath);
55
+ _configCache = (mod.default ?? mod) as ProjectConfig;
56
+ }
57
+ return _configCache;
58
+ } catch (err: unknown) {
59
+ const message = err instanceof Error ? err.message : String(err);
60
+ console.error(c.red(`Error loading .brainbank/${name}: ${message}`));
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ _configCache = null;
66
+ return null;
67
+ }
68
+
69
+ /** Get the loaded config (for use by commands). */
70
+ export async function getConfig(repoPath?: string): Promise<ProjectConfig | null> {
71
+ return loadConfig(repoPath ?? '.');
72
+ }
73
+
74
+ /** Reset config cache. Useful for tests. */
75
+ export function resetConfigCache(): void {
76
+ _configCache = NOT_LOADED;
77
+ }
@@ -0,0 +1,69 @@
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
+ // Merge include patterns from context flags
57
+ const includeFlag = ctxFlag(ctx, 'include');
58
+ const includePatterns = includeFlag ? includeFlag.split(',').map(s => s.trim()) : [];
59
+
60
+ await registerBuiltins(brain, rp, builtins, config, ignorePatterns, includePatterns);
61
+
62
+ for (const plugin of folderPlugins) brain.use(plugin);
63
+
64
+ if (config?.indexers) {
65
+ for (const plugin of config.indexers as Plugin[]) brain.use(plugin);
66
+ }
67
+
68
+ return brain;
69
+ }
@@ -0,0 +1,324 @@
1
+ /**
2
+ * BrainBank CLI — Plugin Loader
3
+ *
4
+ * Generic plugin loader registry with dynamic @brainbank/* package loading,
5
+ * npm package fallback for third-party plugins, and auto-discovery of
6
+ * user plugins from .brainbank/plugins/.
7
+ */
8
+
9
+ import type { Plugin, PluginScanInfo, PluginPreviewLine } from '@/plugin.ts';
10
+ import type { EmbeddingProvider } from '@/types.ts';
11
+ import type { ProjectConfig } from './config-loader.ts';
12
+ import type { ScanModule } from '@/cli/commands/scan.ts';
13
+ import type { PreviewLine } from '@/cli/tui/tree-scanner.ts';
14
+
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import { c } from '../utils.ts';
18
+
19
+ /** Plugin factory — accepts config, returns Plugin. */
20
+ type PluginFactory = (opts: Record<string, unknown>) => Plugin;
21
+
22
+ /** Loader function: dynamically imports a package and returns its factory. */
23
+ type PluginLoaderFn = () => Promise<PluginFactory | null>;
24
+
25
+ /** Built-in plugin loader registry. Extensible at runtime. */
26
+ const PLUGIN_LOADERS = new Map<string, PluginLoaderFn>([
27
+ ['code', async () => { try { return (await import('@brainbank/code')).code as PluginFactory; } catch { return null; } }],
28
+ ['git', async () => { try { return (await import('@brainbank/git')).git as PluginFactory; } catch { return null; } }],
29
+ ['docs', async () => { try { return (await import('@brainbank/docs')).docs as PluginFactory; } catch { return null; } }],
30
+ ]);
31
+
32
+ /** Built-in plugin names — used to skip npm fallback. */
33
+ const BUILTIN_PLUGINS = new Set(['code', 'git', 'docs']);
34
+
35
+
36
+
37
+ /**
38
+ * Load a plugin factory by name.
39
+ * 1. Check the built-in loader registry.
40
+ * 2. Fallback: try `import(name)` for npm packages (third-party plugins).
41
+ * Returns null if not installed.
42
+ */
43
+ export async function loadPlugin(name: string): Promise<PluginFactory | null> {
44
+ // Built-in loader
45
+ const loader = PLUGIN_LOADERS.get(name);
46
+ if (loader) return loader();
47
+
48
+ // npm package fallback — try importing the package directly
49
+ try {
50
+ const mod = await import(name) as Record<string, unknown>;
51
+ // Convention: export default factory, or named export matching short name
52
+ // e.g. 'brainbank-csv' exports `csv()`, '@myorg/brainbank-csv' exports `csv()`
53
+ const shortName = name.replace(/^@[^/]+\//, '').replace(/^brainbank-/, '');
54
+ const factory = mod.default ?? mod[shortName];
55
+ if (typeof factory === 'function') return factory as PluginFactory;
56
+
57
+ // Fallback: check all exports for a function that returns a Plugin-like object
58
+ for (const val of Object.values(mod)) {
59
+ if (typeof val === 'function') return val as PluginFactory;
60
+ }
61
+ } catch {
62
+ // Not installed — will be reported by registerBuiltins
63
+ }
64
+
65
+ return null;
66
+ }
67
+
68
+ /** Register a custom plugin loader. */
69
+ export function registerPluginLoader(name: string, loader: PluginLoaderFn): void {
70
+ PLUGIN_LOADERS.set(name, loader);
71
+ }
72
+
73
+
74
+
75
+ /** Check if a plugin name is a built-in. */
76
+ export function isBuiltinPlugin(name: string): boolean {
77
+ return BUILTIN_PLUGINS.has(name);
78
+ }
79
+
80
+ const INDEXER_EXTENSIONS = ['.ts', '.js', '.mjs'];
81
+ const NOT_LOADED = Symbol('not-loaded');
82
+ let _folderPluginsCache: Plugin[] | typeof NOT_LOADED = NOT_LOADED;
83
+
84
+ /** Auto-discover plugins from .brainbank/plugins/ folder. */
85
+ export async function discoverFolderPlugins(repoPath: string): Promise<Plugin[]> {
86
+ if (_folderPluginsCache !== NOT_LOADED) return _folderPluginsCache;
87
+
88
+ const pluginsDir = path.resolve(repoPath, '.brainbank', 'plugins');
89
+
90
+ if (!fs.existsSync(pluginsDir)) {
91
+ _folderPluginsCache = [];
92
+ return [];
93
+ }
94
+
95
+ const files = fs.readdirSync(pluginsDir)
96
+ .filter(f => INDEXER_EXTENSIONS.some(ext => f.endsWith(ext)))
97
+ .sort();
98
+
99
+ const plugins: Plugin[] = [];
100
+
101
+ for (const file of files) {
102
+ const filePath = path.join(pluginsDir, file);
103
+ try {
104
+ const mod = await import(filePath);
105
+ const plugin = mod.default ?? mod;
106
+
107
+ if (plugin && typeof plugin === 'object' && plugin.name) {
108
+ plugins.push(plugin as Plugin);
109
+ } else {
110
+ console.error(c.yellow(`⚠ ${file}: must export a default Plugin with a 'name' property, skipping`));
111
+ }
112
+ } catch (err: unknown) {
113
+ const message = err instanceof Error ? err.message : String(err);
114
+ console.error(c.red(`Error loading plugin ${file}: ${message}`));
115
+ }
116
+ }
117
+
118
+ _folderPluginsCache = plugins;
119
+ return plugins;
120
+ }
121
+
122
+ /** Reset folder plugins cache. Useful for tests. */
123
+ export function resetPluginCache(): void {
124
+ _folderPluginsCache = NOT_LOADED;
125
+ }
126
+
127
+
128
+ // ── External Plugin Discovery (TUI integration) ──────────────────
129
+
130
+ /** Result of discovering external (non-built-in) plugins. */
131
+ export interface ExternalPluginDiscovery {
132
+ /** ScanModule entries to merge into the TUI sidebar. */
133
+ modules: ScanModule[];
134
+ /** Preview lines keyed by plugin name, for the TUI explorer panel. */
135
+ previews: Map<string, PreviewLine[]>;
136
+ }
137
+
138
+ /**
139
+ * Discover external plugins from config and produce scan/preview data for the TUI.
140
+ *
141
+ * For each non-built-in plugin in the list:
142
+ * 1. Try `import(name)` to load the package.
143
+ * 2. If it exports `scan(repoPath)`, use it for the sidebar.
144
+ * 3. If it exports `preview(repoPath)`, use it for the explorer panel.
145
+ * 4. If not installed, show as unavailable with install hint.
146
+ *
147
+ * Also scans `.brainbank/plugins/` folder plugins for scan/preview exports.
148
+ */
149
+ export async function discoverExternalPlugins(
150
+ repoPath: string,
151
+ pluginNames: string[],
152
+ ): Promise<ExternalPluginDiscovery> {
153
+ const modules: ScanModule[] = [];
154
+ const previews = new Map<string, PreviewLine[]>();
155
+ const resolvedRp = path.resolve(repoPath);
156
+
157
+ // 1. Discover npm plugins listed in config
158
+ for (const name of pluginNames) {
159
+ if (BUILTIN_PLUGINS.has(name)) continue;
160
+
161
+ try {
162
+ const mod = await import(name) as Record<string, unknown>;
163
+
164
+ // scan() → ScanModule for sidebar
165
+ if (typeof mod.scan === 'function') {
166
+ const info = mod.scan(resolvedRp) as PluginScanInfo;
167
+ modules.push(scanInfoToModule(info));
168
+ } else {
169
+ // No scan export — generic entry
170
+ modules.push({
171
+ name,
172
+ available: true,
173
+ checked: true,
174
+ icon: '🔌',
175
+ summary: `${name} plugin`,
176
+ });
177
+ }
178
+
179
+ // preview() → PreviewLine[] for explorer
180
+ if (typeof mod.preview === 'function') {
181
+ const lines = mod.preview(resolvedRp) as PluginPreviewLine[];
182
+ previews.set(name, lines.map(previewLineToInternal));
183
+ }
184
+ } catch {
185
+ // Package not installed
186
+ modules.push({
187
+ name,
188
+ available: false,
189
+ checked: false,
190
+ icon: '🔌',
191
+ summary: 'not installed',
192
+ disabled: `npm i ${name}`,
193
+ });
194
+ }
195
+ }
196
+
197
+ // 2. Discover folder plugins that export scan/preview
198
+ const pluginsDir = path.resolve(repoPath, '.brainbank', 'plugins');
199
+ if (fs.existsSync(pluginsDir)) {
200
+ const files = fs.readdirSync(pluginsDir)
201
+ .filter(f => INDEXER_EXTENSIONS.some(ext => f.endsWith(ext)))
202
+ .sort();
203
+
204
+ for (const file of files) {
205
+ const filePath = path.join(pluginsDir, file);
206
+ try {
207
+ const mod = await import(filePath) as Record<string, unknown>;
208
+ const plugin = mod.default ?? mod;
209
+ const pluginName = (plugin && typeof plugin === 'object' && 'name' in plugin)
210
+ ? (plugin as { name: string }).name
211
+ : file.replace(/\.[^.]+$/, '');
212
+
213
+ // Skip if already added from config
214
+ if (modules.some(m => m.name === pluginName)) continue;
215
+
216
+ if (typeof mod.scan === 'function') {
217
+ const info = mod.scan(resolvedRp) as PluginScanInfo;
218
+ modules.push(scanInfoToModule(info));
219
+ } else {
220
+ modules.push({
221
+ name: pluginName,
222
+ available: true,
223
+ checked: true,
224
+ icon: '🔌',
225
+ summary: `local plugin (${file})`,
226
+ });
227
+ }
228
+
229
+ if (typeof mod.preview === 'function') {
230
+ const lines = mod.preview(resolvedRp) as PluginPreviewLine[];
231
+ previews.set(pluginName, lines.map(previewLineToInternal));
232
+ }
233
+ } catch {
234
+ // Failed to load folder plugin — skip for discovery
235
+ }
236
+ }
237
+ }
238
+
239
+ return { modules, previews };
240
+ }
241
+
242
+ /** Convert PluginScanInfo (public type) to ScanModule (internal type). */
243
+ function scanInfoToModule(info: PluginScanInfo): ScanModule {
244
+ return {
245
+ name: info.name,
246
+ available: info.available,
247
+ summary: info.summary,
248
+ icon: info.icon,
249
+ checked: info.checked,
250
+ disabled: info.disabled,
251
+ details: info.details,
252
+ };
253
+ }
254
+
255
+ /** Convert PluginPreviewLine (public type) to PreviewLine (internal type). */
256
+ function previewLineToInternal(line: PluginPreviewLine): PreviewLine {
257
+ return {
258
+ text: line.text,
259
+ color: line.color,
260
+ bold: line.bold,
261
+ dim: line.dim,
262
+ };
263
+ }
264
+
265
+
266
+ // ── Provider Setup ───────────────────────────────────────────────
267
+
268
+ /** Resolve an embedding key string to an EmbeddingProvider instance. */
269
+ export async function resolveEmbeddingKey(key: string): Promise<EmbeddingProvider> {
270
+ const { resolveEmbedding } = await import('@/providers/embeddings/resolve.ts');
271
+ return resolveEmbedding(key);
272
+ }
273
+
274
+ /** Configure pruner and global embedding provider on brainOpts. */
275
+ export async function setupProviders(
276
+ brainOpts: Record<string, unknown>,
277
+ config: ProjectConfig | null,
278
+ flags?: Record<string, string | undefined>,
279
+ env?: Record<string, string | undefined>,
280
+ ): Promise<void> {
281
+ // Resolve API keys: config.keys > env vars
282
+ const keys = config?.keys ?? {};
283
+ const anthropicKey = keys.anthropic || process.env.ANTHROPIC_API_KEY;
284
+ const perplexityKey = keys.perplexity || process.env.PERPLEXITY_API_KEY;
285
+ const openaiKey = keys.openai || process.env.OPENAI_API_KEY;
286
+
287
+ // Inject resolved keys into process.env so downstream providers auto-detect them
288
+ if (anthropicKey) process.env.ANTHROPIC_API_KEY = anthropicKey;
289
+ if (perplexityKey) process.env.PERPLEXITY_API_KEY = perplexityKey;
290
+ if (openaiKey) process.env.OPENAI_API_KEY = openaiKey;
291
+
292
+
293
+
294
+ const prunerFlag = flags?.pruner ?? (config?.pruner as string | undefined);
295
+ if (prunerFlag === 'haiku' || prunerFlag === 'sonnet') {
296
+ const { HaikuPruner } = await import('@/providers/pruners/haiku-pruner.ts');
297
+ const model = prunerFlag === 'sonnet' ? 'claude-sonnet-4-6' : undefined;
298
+ brainOpts.pruner = new HaikuPruner({ apiKey: anthropicKey, model });
299
+ } else if (!prunerFlag && anthropicKey) {
300
+ // Auto-detect: ANTHROPIC_API_KEY available → enable Haiku pruner by default
301
+ const { HaikuPruner } = await import('@/providers/pruners/haiku-pruner.ts');
302
+ brainOpts.pruner = new HaikuPruner({ apiKey: anthropicKey });
303
+ }
304
+
305
+ const embFlag = flags?.embedding
306
+ ?? (config?.embedding as string | undefined)
307
+ ?? env?.BRAINBANK_EMBEDDING
308
+ ?? process.env.BRAINBANK_EMBEDDING;
309
+ if (embFlag) {
310
+ const provider = await resolveEmbeddingKey(embFlag);
311
+ brainOpts.embeddingProvider = provider;
312
+ brainOpts.embeddingDims = provider.dims;
313
+ } else if (perplexityKey) {
314
+ // Auto-detect: PERPLEXITY_API_KEY available → use Perplexity embeddings by default
315
+ const provider = await resolveEmbeddingKey('perplexity');
316
+ brainOpts.embeddingProvider = provider;
317
+ brainOpts.embeddingDims = provider.dims;
318
+ }
319
+
320
+ // Context field defaults from config.json "context" section
321
+ if (config?.context) {
322
+ brainOpts.contextFields = config.context;
323
+ }
324
+ }