brainbank 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +155 -0
- package/assets/architecture.png +0 -0
- package/bin/brainbank +18 -0
- package/bin/brainbank-mcp +19 -0
- package/dist/chunk-3YBCD6DI.js +117 -0
- package/dist/chunk-3YBCD6DI.js.map +1 -0
- package/dist/chunk-63GBCDS5.js +3249 -0
- package/dist/chunk-63GBCDS5.js.map +1 -0
- package/dist/chunk-DMFMTOHF.js +123 -0
- package/dist/chunk-DMFMTOHF.js.map +1 -0
- package/dist/chunk-FQYKWB2Q.js +136 -0
- package/dist/chunk-FQYKWB2Q.js.map +1 -0
- package/dist/chunk-IMJJ2VEM.js +74 -0
- package/dist/chunk-IMJJ2VEM.js.map +1 -0
- package/dist/chunk-M744PCJQ.js +43 -0
- package/dist/chunk-M744PCJQ.js.map +1 -0
- package/dist/chunk-O3J6ZIXK.js +82 -0
- package/dist/chunk-O3J6ZIXK.js.map +1 -0
- package/dist/chunk-OPH7GZ7U.js +124 -0
- package/dist/chunk-OPH7GZ7U.js.map +1 -0
- package/dist/chunk-PXEWQMN7.js +89 -0
- package/dist/chunk-PXEWQMN7.js.map +1 -0
- package/dist/chunk-RDQYDLYZ.js +69 -0
- package/dist/chunk-RDQYDLYZ.js.map +1 -0
- package/dist/chunk-VIIHPCC4.js +254 -0
- package/dist/chunk-VIIHPCC4.js.map +1 -0
- package/dist/chunk-WCQVDF3K.js +14 -0
- package/dist/chunk-WCQVDF3K.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3076 -0
- package/dist/cli.js.map +1 -0
- package/dist/haiku-expander-YRSIPGKP.js +8 -0
- package/dist/haiku-expander-YRSIPGKP.js.map +1 -0
- package/dist/haiku-pruner-SHAXUPY6.js +8 -0
- package/dist/haiku-pruner-SHAXUPY6.js.map +1 -0
- package/dist/http-server-QUXHLWUM.js +9 -0
- package/dist/http-server-QUXHLWUM.js.map +1 -0
- package/dist/index.d.ts +2161 -0
- package/dist/index.js +357 -0
- package/dist/index.js.map +1 -0
- package/dist/local-embedding-NZQTILGV.js +8 -0
- package/dist/local-embedding-NZQTILGV.js.map +1 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +334 -0
- package/dist/mcp.js.map +1 -0
- package/dist/openai-embedding-ZP5TSUJG.js +8 -0
- package/dist/openai-embedding-ZP5TSUJG.js.map +1 -0
- package/dist/perplexity-context-embedding-GI5PHE6X.js +9 -0
- package/dist/perplexity-context-embedding-GI5PHE6X.js.map +1 -0
- package/dist/perplexity-embedding-KZRYGJRC.js +10 -0
- package/dist/perplexity-embedding-KZRYGJRC.js.map +1 -0
- package/dist/plugin-IKQ6IRSJ.js +32 -0
- package/dist/plugin-IKQ6IRSJ.js.map +1 -0
- package/dist/resolve-ASGLBNUC.js +10 -0
- package/dist/resolve-ASGLBNUC.js.map +1 -0
- package/dist/stats-tui-ZY2NQSEA.js +1904 -0
- package/dist/stats-tui-ZY2NQSEA.js.map +1 -0
- package/package.json +96 -0
- package/src/brainbank.ts +617 -0
- package/src/cli/commands/collection.ts +77 -0
- package/src/cli/commands/context.ts +179 -0
- package/src/cli/commands/daemon.ts +100 -0
- package/src/cli/commands/docs.ts +71 -0
- package/src/cli/commands/files.ts +69 -0
- package/src/cli/commands/help.ts +77 -0
- package/src/cli/commands/index.ts +482 -0
- package/src/cli/commands/kv.ts +140 -0
- package/src/cli/commands/mcp-export.ts +273 -0
- package/src/cli/commands/mcp.ts +6 -0
- package/src/cli/commands/reembed.ts +30 -0
- package/src/cli/commands/scan.ts +336 -0
- package/src/cli/commands/search.ts +203 -0
- package/src/cli/commands/stats.ts +68 -0
- package/src/cli/commands/status.ts +47 -0
- package/src/cli/commands/watch.ts +47 -0
- package/src/cli/factory/brain-context.ts +43 -0
- package/src/cli/factory/builtin-registration.ts +87 -0
- package/src/cli/factory/config-loader.ts +77 -0
- package/src/cli/factory/index.ts +69 -0
- package/src/cli/factory/plugin-loader.ts +325 -0
- package/src/cli/index.ts +71 -0
- package/src/cli/server-client.ts +178 -0
- package/src/cli/tui/index-tui.tsx +667 -0
- package/src/cli/tui/stats-data.ts +523 -0
- package/src/cli/tui/stats-search.ts +262 -0
- package/src/cli/tui/stats-tui.tsx +1465 -0
- package/src/cli/tui/tree-scanner.ts +650 -0
- package/src/cli/utils.ts +137 -0
- package/src/config.ts +49 -0
- package/src/constants.ts +21 -0
- package/src/db/adapter.ts +112 -0
- package/src/db/metadata.ts +130 -0
- package/src/db/migrations.ts +66 -0
- package/src/db/sqlite-adapter.ts +218 -0
- package/src/db/tracker.ts +91 -0
- package/src/engine/index-api.ts +81 -0
- package/src/engine/reembed.ts +206 -0
- package/src/engine/search-api.ts +218 -0
- package/src/index.ts +154 -0
- package/src/lib/fts.ts +57 -0
- package/src/lib/languages.ts +180 -0
- package/src/lib/logger.ts +126 -0
- package/src/lib/math.ts +87 -0
- package/src/lib/provider-key.ts +20 -0
- package/src/lib/prune.ts +71 -0
- package/src/lib/rrf.ts +133 -0
- package/src/lib/write-lock.ts +108 -0
- package/src/mcp/mcp-server.ts +195 -0
- package/src/mcp/workspace-factory.ts +68 -0
- package/src/mcp/workspace-pool.ts +224 -0
- package/src/plugin.ts +381 -0
- package/src/providers/embeddings/embedding-worker-thread.ts +95 -0
- package/src/providers/embeddings/embedding-worker.ts +141 -0
- package/src/providers/embeddings/local-embedding.ts +115 -0
- package/src/providers/embeddings/openai-embedding.ts +167 -0
- package/src/providers/embeddings/perplexity-context-embedding.ts +195 -0
- package/src/providers/embeddings/perplexity-embedding.ts +165 -0
- package/src/providers/embeddings/resolve.ts +34 -0
- package/src/providers/pruners/haiku-expander.ts +166 -0
- package/src/providers/pruners/haiku-pruner.ts +112 -0
- package/src/providers/vector/hnsw-index.ts +174 -0
- package/src/providers/vector/hnsw-loader.ts +129 -0
- package/src/search/bm25-boost.ts +69 -0
- package/src/search/context-builder.ts +251 -0
- package/src/search/keyword/composite-bm25-search.ts +47 -0
- package/src/search/types.ts +37 -0
- package/src/search/vector/composite-vector-search.ts +61 -0
- package/src/search/vector/mmr.ts +64 -0
- package/src/services/collection.ts +384 -0
- package/src/services/daemon.ts +87 -0
- package/src/services/http-server.ts +336 -0
- package/src/services/kv-service.ts +64 -0
- package/src/services/plugin-registry.ts +77 -0
- package/src/services/watch.ts +340 -0
- package/src/services/webhook-server.ts +100 -0
- package/src/types.ts +493 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brainbank search — Semantic search (vector)
|
|
3
|
+
* brainbank hsearch — Hybrid search (vector + BM25)
|
|
4
|
+
* brainbank ksearch — Keyword search (BM25)
|
|
5
|
+
*
|
|
6
|
+
* Source filtering:
|
|
7
|
+
* --code 10 Max code results
|
|
8
|
+
* --git 0 Skip git results
|
|
9
|
+
* --docs 5 Max document results
|
|
10
|
+
* --notes 10 Custom plugin results
|
|
11
|
+
* --slack_messages 5 Custom collection results
|
|
12
|
+
*
|
|
13
|
+
* Any --<name> <number> flag is treated as a source filter.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { c, args, getFlag, stripFlags, printResults } from '@/cli/utils.ts';
|
|
17
|
+
import { createBrain } from '@/cli/factory/index.ts';
|
|
18
|
+
import { tryServerSearch } from '@/cli/server-client.ts';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse dynamic source flags: each `--<name> <number>` becomes `{ name: number }`.
|
|
22
|
+
*
|
|
23
|
+
* Known non-source flags (--repo, --depth, etc.) are excluded.
|
|
24
|
+
* Returns sources map + the query string (positional args).
|
|
25
|
+
*/
|
|
26
|
+
function parseSourceFlags(): { sources: Record<string, number>; query: string } {
|
|
27
|
+
const NON_SOURCE_FLAGS = new Set([
|
|
28
|
+
'repo', 'depth', 'collection', 'pattern', 'context', 'name',
|
|
29
|
+
'keep', 'pruner', 'only', 'docs-path', 'mode', 'limit',
|
|
30
|
+
'ignore', 'include', 'meta', 'k', 'yes', 'y', 'force', 'verbose', 'path',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const sources: Record<string, number> = {};
|
|
34
|
+
const positional: string[] = [];
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
if (args[i].startsWith('--')) {
|
|
38
|
+
const name = args[i].slice(2);
|
|
39
|
+
|
|
40
|
+
// Skip boolean flags
|
|
41
|
+
if (name === 'yes' || name === 'force' || name === 'verbose') continue;
|
|
42
|
+
|
|
43
|
+
// If next arg is a number and flag is not a known non-source flag
|
|
44
|
+
const next = args[i + 1];
|
|
45
|
+
if (next !== undefined && /^\d+$/.test(next) && !NON_SOURCE_FLAGS.has(name)) {
|
|
46
|
+
sources[name] = parseInt(next, 10);
|
|
47
|
+
i++; // skip the value
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Known value flag — skip its value
|
|
52
|
+
if (NON_SOURCE_FLAGS.has(name) && next !== undefined && !next.startsWith('--')) {
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
positional.push(args[i]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const query = positional.slice(1).join(' '); // skip command name
|
|
61
|
+
return { sources, query };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Parse --path as comma-separated list of path prefixes. */
|
|
65
|
+
function parsePaths(): string | string[] | undefined {
|
|
66
|
+
const raw = getFlag('path');
|
|
67
|
+
if (!raw) return undefined;
|
|
68
|
+
const paths = raw.split(',').map(p => p.trim()).filter(Boolean);
|
|
69
|
+
return paths.length === 1 ? paths[0] : paths;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Print active source and path filters. */
|
|
73
|
+
function printFilterInfo(sources: Record<string, number>, pathPrefix?: string | string[]): void {
|
|
74
|
+
const parts: string[] = [];
|
|
75
|
+
const entries = Object.entries(sources);
|
|
76
|
+
if (entries.length > 0) parts.push(...entries.map(([k, v]) => `${k}=${v}`));
|
|
77
|
+
if (pathPrefix) {
|
|
78
|
+
const paths = Array.isArray(pathPrefix) ? pathPrefix : [pathPrefix];
|
|
79
|
+
parts.push(`path=${paths.join(',')}`);
|
|
80
|
+
}
|
|
81
|
+
if (parts.length > 0) console.log(c.dim(` Filters: ${parts.join(', ')}`));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Build search options from sources map + optional path prefix(es). */
|
|
85
|
+
function buildSearchOptions(sources: Record<string, number>, pathPrefix?: string | string[]): { sources: Record<string, number>; source: 'cli'; pathPrefix?: string | string[] } {
|
|
86
|
+
const opts: { sources: Record<string, number>; source: 'cli'; pathPrefix?: string | string[] } = {
|
|
87
|
+
sources: Object.keys(sources).length > 0 ? sources : {},
|
|
88
|
+
source: 'cli',
|
|
89
|
+
};
|
|
90
|
+
if (pathPrefix) opts.pathPrefix = pathPrefix;
|
|
91
|
+
return opts;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function cmdSearch(): Promise<void> {
|
|
95
|
+
const { sources, query } = parseSourceFlags();
|
|
96
|
+
if (!query) {
|
|
97
|
+
console.log(c.red('Usage: brainbank search <query> [--repo <path>] [--path <dir>] [--code <n>] [--git <n>]'));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const pathPrefix = parsePaths();
|
|
102
|
+
const repo = getFlag('repo') ?? process.cwd();
|
|
103
|
+
|
|
104
|
+
// Try daemon delegation first (hot HNSW)
|
|
105
|
+
const delegated = await tryServerSearch('search', {
|
|
106
|
+
query, repo,
|
|
107
|
+
sources: Object.keys(sources).length > 0 ? sources : undefined,
|
|
108
|
+
pathPrefix,
|
|
109
|
+
});
|
|
110
|
+
if (delegated) {
|
|
111
|
+
console.log(c.bold(`\n━━━ BrainBank Search: "${query}" ━━━\n`));
|
|
112
|
+
printFilterInfo(sources, pathPrefix);
|
|
113
|
+
printResults(delegated);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fall back to local
|
|
118
|
+
const brain = await createBrain();
|
|
119
|
+
console.log(c.bold(`\n━━━ BrainBank Search: "${query}" ━━━\n`));
|
|
120
|
+
printFilterInfo(sources, pathPrefix);
|
|
121
|
+
|
|
122
|
+
const opts = buildSearchOptions(sources, pathPrefix);
|
|
123
|
+
const results = await brain.search(query, opts);
|
|
124
|
+
printResults(results);
|
|
125
|
+
brain.close();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function cmdHybridSearch(): Promise<void> {
|
|
129
|
+
const { sources, query } = parseSourceFlags();
|
|
130
|
+
if (!query) {
|
|
131
|
+
console.log(c.red('Usage: brainbank hsearch <query> [--repo <path>] [--path <dir>] [--code <n>] [--git <n>] [--docs <n>]'));
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pathPrefix = parsePaths();
|
|
136
|
+
const repo = getFlag('repo') ?? process.cwd();
|
|
137
|
+
|
|
138
|
+
// Try daemon delegation first (hot HNSW)
|
|
139
|
+
const delegated = await tryServerSearch('hybrid', {
|
|
140
|
+
query, repo,
|
|
141
|
+
sources: Object.keys(sources).length > 0 ? sources : undefined,
|
|
142
|
+
pathPrefix,
|
|
143
|
+
});
|
|
144
|
+
if (delegated) {
|
|
145
|
+
console.log(c.bold(`\n━━━ BrainBank Hybrid Search: "${query}" ━━━`));
|
|
146
|
+
console.log(c.dim(` Mode: vector + BM25 → Reciprocal Rank Fusion`));
|
|
147
|
+
printFilterInfo(sources, pathPrefix);
|
|
148
|
+
console.log('');
|
|
149
|
+
printResults(delegated);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Fall back to local
|
|
154
|
+
const brain = await createBrain();
|
|
155
|
+
console.log(c.bold(`\n━━━ BrainBank Hybrid Search: "${query}" ━━━`));
|
|
156
|
+
console.log(c.dim(` Mode: vector + BM25 → Reciprocal Rank Fusion`));
|
|
157
|
+
printFilterInfo(sources, pathPrefix);
|
|
158
|
+
console.log('');
|
|
159
|
+
|
|
160
|
+
const opts = buildSearchOptions(sources, pathPrefix);
|
|
161
|
+
const results = await brain.hybridSearch(query, opts);
|
|
162
|
+
printResults(results);
|
|
163
|
+
brain.close();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function cmdKeywordSearch(): Promise<void> {
|
|
167
|
+
const { sources, query } = parseSourceFlags();
|
|
168
|
+
if (!query) {
|
|
169
|
+
console.log(c.red('Usage: brainbank ksearch <query> [--repo <path>] [--path <dir>] [--code <n>] [--git <n>]'));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const pathPrefix = parsePaths();
|
|
174
|
+
const repo = getFlag('repo') ?? process.cwd();
|
|
175
|
+
|
|
176
|
+
// Try daemon delegation first (hot HNSW)
|
|
177
|
+
const delegated = await tryServerSearch('keyword', {
|
|
178
|
+
query, repo,
|
|
179
|
+
sources: Object.keys(sources).length > 0 ? sources : undefined,
|
|
180
|
+
pathPrefix,
|
|
181
|
+
});
|
|
182
|
+
if (delegated) {
|
|
183
|
+
console.log(c.bold(`\n━━━ BrainBank Keyword Search: "${query}" ━━━`));
|
|
184
|
+
console.log(c.dim(` Mode: BM25 full-text (instant)`));
|
|
185
|
+
printFilterInfo(sources, pathPrefix);
|
|
186
|
+
console.log('');
|
|
187
|
+
printResults(delegated, 0.40);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Fall back to local
|
|
192
|
+
const brain = await createBrain();
|
|
193
|
+
await brain.initialize();
|
|
194
|
+
console.log(c.bold(`\n━━━ BrainBank Keyword Search: "${query}" ━━━`));
|
|
195
|
+
console.log(c.dim(` Mode: BM25 full-text (instant)`));
|
|
196
|
+
printFilterInfo(sources, pathPrefix);
|
|
197
|
+
console.log('');
|
|
198
|
+
|
|
199
|
+
const opts = buildSearchOptions(sources, pathPrefix);
|
|
200
|
+
const results = await brain.searchBM25(query, opts);
|
|
201
|
+
printResults(results, 0.40);
|
|
202
|
+
brain.close();
|
|
203
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** brainbank stats — Show index statistics (interactive TUI or plain text). */
|
|
2
|
+
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { c, args, getFlag } from '@/cli/utils.ts';
|
|
5
|
+
import { createBrain } from '@/cli/factory/index.ts';
|
|
6
|
+
|
|
7
|
+
/** Convert camelCase/snake_case stat keys to human-readable labels. */
|
|
8
|
+
function formatStatKey(key: string): string {
|
|
9
|
+
return key
|
|
10
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
11
|
+
.replace(/_/g, ' ')
|
|
12
|
+
.replace(/\b\w/g, c => c.toUpperCase())
|
|
13
|
+
.padEnd(16);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function cmdStats(): Promise<void> {
|
|
17
|
+
const plain = args.includes('--plain');
|
|
18
|
+
|
|
19
|
+
if (!plain) {
|
|
20
|
+
// Interactive TUI mode
|
|
21
|
+
try {
|
|
22
|
+
const repoPath = path.resolve(getFlag('repo') ?? process.cwd());
|
|
23
|
+
const dbPath = path.join(repoPath, '.brainbank', 'data', 'brainbank.db');
|
|
24
|
+
const configPath = path.join(repoPath, '.brainbank', 'config.json');
|
|
25
|
+
|
|
26
|
+
const { runStatsTui } = await import('@/cli/tui/stats-tui.tsx');
|
|
27
|
+
await runStatsTui(dbPath, repoPath, configPath);
|
|
28
|
+
return;
|
|
29
|
+
} catch (err: unknown) {
|
|
30
|
+
// Fall back to plain mode if TUI fails (e.g., no DB, piped output)
|
|
31
|
+
if (err instanceof Error && err.message.includes('ENOENT')) {
|
|
32
|
+
console.log(c.yellow('No BrainBank database found. Run `brainbank index` first.\n'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// For other errors, fall through to plain mode
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Plain text mode (original output)
|
|
40
|
+
const brain = await createBrain();
|
|
41
|
+
await brain.initialize();
|
|
42
|
+
|
|
43
|
+
const s = brain.stats();
|
|
44
|
+
|
|
45
|
+
console.log(c.bold('\n━━━ BrainBank Stats ━━━\n'));
|
|
46
|
+
console.log(` ${c.cyan('Plugins')}: ${brain.plugins.join(', ')}\n`);
|
|
47
|
+
|
|
48
|
+
for (const [name, pluginStats] of Object.entries(s)) {
|
|
49
|
+
if (!pluginStats) continue;
|
|
50
|
+
console.log(` ${c.cyan(name)}`);
|
|
51
|
+
for (const [key, value] of Object.entries(pluginStats)) {
|
|
52
|
+
console.log(` ${formatStatKey(key)}${value}`);
|
|
53
|
+
}
|
|
54
|
+
console.log('');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const kvNames = brain.listCollectionNames();
|
|
58
|
+
if (kvNames.length > 0) {
|
|
59
|
+
console.log(` ${c.cyan('KV Collections')}`);
|
|
60
|
+
for (const name of kvNames) {
|
|
61
|
+
const coll = brain.collection(name);
|
|
62
|
+
console.log(` ${name}: ${coll.count()} items`);
|
|
63
|
+
}
|
|
64
|
+
console.log('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
brain.close();
|
|
68
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Command — Show BrainBank server status.
|
|
3
|
+
*
|
|
4
|
+
* Usage: brainbank status
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { c } from '@/cli/utils.ts';
|
|
8
|
+
import { serverHealth } from '@/cli/server-client.ts';
|
|
9
|
+
import { isServerRunning } from '@/services/daemon.ts';
|
|
10
|
+
|
|
11
|
+
function formatUptime(seconds: number): string {
|
|
12
|
+
if (seconds < 60) return `${seconds}s`;
|
|
13
|
+
const minutes = Math.floor(seconds / 60);
|
|
14
|
+
if (minutes < 60) return `${minutes}m`;
|
|
15
|
+
const hours = Math.floor(minutes / 60);
|
|
16
|
+
const remainMinutes = minutes % 60;
|
|
17
|
+
return remainMinutes > 0 ? `${hours}h ${remainMinutes}m` : `${hours}h`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function cmdStatus(): Promise<void> {
|
|
21
|
+
const info = isServerRunning();
|
|
22
|
+
|
|
23
|
+
if (!info) {
|
|
24
|
+
console.log(`\n ${c.dim('HTTP Server:')} ${c.yellow('stopped')}\n`);
|
|
25
|
+
console.log(c.dim(' Start with: brainbank daemon'));
|
|
26
|
+
console.log('');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Try to get detailed health from the server
|
|
31
|
+
const health = await serverHealth();
|
|
32
|
+
|
|
33
|
+
if (health) {
|
|
34
|
+
const uptime = formatUptime(health.uptime);
|
|
35
|
+
console.log(`\n ${c.dim('HTTP Server:')} ${c.green('running')}`);
|
|
36
|
+
console.log(` ${c.dim('PID:')} ${health.pid}`);
|
|
37
|
+
console.log(` ${c.dim('Port:')} ${health.port}`);
|
|
38
|
+
console.log(` ${c.dim('Uptime:')} ${uptime}`);
|
|
39
|
+
console.log(` ${c.dim('Workspaces:')} ${health.workspaces}`);
|
|
40
|
+
console.log('');
|
|
41
|
+
} else {
|
|
42
|
+
// PID file exists but server not responding
|
|
43
|
+
console.log(`\n ${c.dim('HTTP Server:')} ${c.yellow('stale')} (PID ${info.pid} not responding)`);
|
|
44
|
+
console.log(c.dim(' The PID file may be stale. Restart with: brainbank daemon'));
|
|
45
|
+
console.log('');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -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
|
+
}
|