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.
- package/README.md +84 -1107
- package/assets/architecture.png +0 -0
- package/bin/brainbank +8 -1
- package/bin/brainbank-mcp +19 -0
- package/dist/chunk-3UIWA32X.js +3341 -0
- package/dist/chunk-3UIWA32X.js.map +1 -0
- package/dist/chunk-3YBCD6DI.js +117 -0
- package/dist/chunk-3YBCD6DI.js.map +1 -0
- package/dist/chunk-DAGVUEXL.js +258 -0
- package/dist/chunk-DAGVUEXL.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-NNDY7P2R.js +211 -0
- package/dist/chunk-NNDY7P2R.js.map +1 -0
- package/dist/chunk-O3J6ZIXK.js +82 -0
- package/dist/chunk-O3J6ZIXK.js.map +1 -0
- package/dist/chunk-RDQYDLYZ.js +69 -0
- package/dist/chunk-RDQYDLYZ.js.map +1 -0
- package/dist/chunk-WCQVDF3K.js +14 -0
- package/dist/cli.js +2713 -325
- package/dist/cli.js.map +1 -1
- package/dist/haiku-pruner-5KVT5AI2.js +8 -0
- package/dist/http-server-2ZQ6I43B.js +9 -0
- package/dist/index.d.ts +1886 -626
- package/dist/index.js +319 -46
- package/dist/index.js.map +1 -1
- package/dist/local-embedding-NZQTILGV.js +8 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +386 -0
- package/dist/mcp.js.map +1 -0
- package/dist/openai-embedding-ZP5TSUJG.js +8 -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-AD3AMYGV.js +1904 -0
- package/dist/stats-tui-AD3AMYGV.js.map +1 -0
- package/package.json +38 -53
- package/src/brainbank.ts +617 -0
- package/src/cli/commands/collection.ts +77 -0
- package/src/cli/commands/context.ts +59 -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 +82 -0
- package/src/cli/commands/index.ts +478 -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/query.ts +167 -0
- package/src/cli/commands/reembed.ts +30 -0
- package/src/cli/commands/reindex.ts +40 -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 +324 -0
- package/src/cli/index.ts +76 -0
- package/src/cli/server-client.ts +186 -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 +48 -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 +150 -0
- package/src/lib/fts.ts +57 -0
- package/src/lib/languages.ts +179 -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 +72 -0
- package/src/lib/rrf.ts +133 -0
- package/src/lib/write-lock.ts +108 -0
- package/src/mcp/mcp-server.ts +268 -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 +178 -0
- package/src/providers/pruners/haiku-pruner.ts +263 -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 +76 -0
- package/src/search/context-builder.ts +209 -0
- package/src/search/keyword/composite-bm25-search.ts +47 -0
- package/src/search/query-decomposer.ts +124 -0
- package/src/search/types.ts +37 -0
- package/src/search/vector/composite-vector-search.ts +105 -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 +344 -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 +509 -0
- package/dist/chunk-2P3EGY6S.js +0 -37
- package/dist/chunk-2P3EGY6S.js.map +0 -1
- package/dist/chunk-3GAIDXRW.js +0 -105
- package/dist/chunk-3GAIDXRW.js.map +0 -1
- package/dist/chunk-4ZKBQ33J.js +0 -56
- package/dist/chunk-4ZKBQ33J.js.map +0 -1
- package/dist/chunk-7QVYU63E.js +0 -7
- package/dist/chunk-GOUBW7UA.js +0 -373
- package/dist/chunk-GOUBW7UA.js.map +0 -1
- package/dist/chunk-MJ3Y24H6.js +0 -185
- package/dist/chunk-MJ3Y24H6.js.map +0 -1
- package/dist/chunk-N6ZMBFDE.js +0 -224
- package/dist/chunk-N6ZMBFDE.js.map +0 -1
- package/dist/chunk-RAEBYV75.js +0 -709
- package/dist/chunk-RAEBYV75.js.map +0 -1
- package/dist/chunk-TW5NTYYZ.js +0 -2066
- package/dist/chunk-TW5NTYYZ.js.map +0 -1
- package/dist/chunk-Z5SU54HP.js +0 -171
- package/dist/chunk-Z5SU54HP.js.map +0 -1
- package/dist/code.d.ts +0 -31
- package/dist/code.js +0 -8
- package/dist/docs.d.ts +0 -19
- package/dist/docs.js +0 -8
- package/dist/git.d.ts +0 -31
- package/dist/git.js +0 -8
- package/dist/memory.d.ts +0 -19
- package/dist/memory.js +0 -146
- package/dist/memory.js.map +0 -1
- package/dist/notes.d.ts +0 -19
- package/dist/notes.js +0 -57
- package/dist/notes.js.map +0 -1
- package/dist/openai-PCTYLOWI.js +0 -8
- package/dist/types-Da_zLLOl.d.ts +0 -474
- /package/dist/{chunk-7QVYU63E.js.map → chunk-WCQVDF3K.js.map} +0 -0
- /package/dist/{code.js.map → haiku-pruner-5KVT5AI2.js.map} +0 -0
- /package/dist/{docs.js.map → http-server-2ZQ6I43B.js.map} +0 -0
- /package/dist/{git.js.map → local-embedding-NZQTILGV.js.map} +0 -0
- /package/dist/{openai-PCTYLOWI.js.map → openai-embedding-ZP5TSUJG.js.map} +0 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brainbank scan — Lightweight repo scanner for the interactive index flow.
|
|
3
|
+
*
|
|
4
|
+
* Scans the filesystem WITHOUT initializing BrainBank. Returns a ScanResult
|
|
5
|
+
* describing what's available to index via dynamic ScanModule descriptors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
import picomatch from 'picomatch';
|
|
12
|
+
import { SUPPORTED_EXTENSIONS, isIgnoredDir, isIgnoredFile } from '@/lib/languages.ts';
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/** A single scannable module (plugin). */
|
|
16
|
+
export interface ScanModule {
|
|
17
|
+
/** Plugin name (e.g. 'code', 'git', 'docs'). */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Whether there's content available to index. */
|
|
20
|
+
available: boolean;
|
|
21
|
+
/** Human-readable summary (e.g. '1243 files (5 languages)'). */
|
|
22
|
+
summary: string;
|
|
23
|
+
/** Emoji icon for display. */
|
|
24
|
+
icon: string;
|
|
25
|
+
/** Whether checked by default in the prompt. */
|
|
26
|
+
checked: boolean;
|
|
27
|
+
/** Reason this module is disabled (shown in prompt). */
|
|
28
|
+
disabled?: string;
|
|
29
|
+
/** Detail lines for the scan tree (e.g. per-language breakdown). */
|
|
30
|
+
details?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ScanResult {
|
|
34
|
+
repoPath: string;
|
|
35
|
+
modules: ScanModule[];
|
|
36
|
+
config: { exists: boolean; ignore?: string[]; include?: string[]; plugins?: string[] };
|
|
37
|
+
db: { exists: boolean; sizeMB: number; lastModified?: Date } | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
/** Scan a repo path and return what's available to index. */
|
|
42
|
+
export function scanRepo(repoPath: string): ScanResult {
|
|
43
|
+
const resolved = path.resolve(repoPath);
|
|
44
|
+
const config = scanConfig(resolved);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
repoPath: resolved,
|
|
48
|
+
modules: scanModules(resolved, config),
|
|
49
|
+
config,
|
|
50
|
+
db: scanDb(resolved),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Produce ScanModule descriptors for known plugin types. */
|
|
55
|
+
function scanModules(repoPath: string, config: ScanResult['config']): ScanModule[] {
|
|
56
|
+
return [
|
|
57
|
+
scanCodeModule(repoPath, config.include, config.ignore),
|
|
58
|
+
scanGitModule(repoPath),
|
|
59
|
+
scanDocsModule(repoPath),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
/** Scan for indexable code files. Respects include/ignore from config. */
|
|
65
|
+
function scanCodeModule(repoPath: string, include?: string[], ignore?: string[]): ScanModule {
|
|
66
|
+
const byLanguage = new Map<string, number>();
|
|
67
|
+
let total = 0;
|
|
68
|
+
|
|
69
|
+
// Build matchers from config patterns
|
|
70
|
+
let isIncluded: ((p: string) => boolean) | null = null;
|
|
71
|
+
let isIgnoredPat: ((p: string) => boolean) | null = null;
|
|
72
|
+
let includeBases: string[] | null = null;
|
|
73
|
+
if (include?.length) {
|
|
74
|
+
isIncluded = picomatch(include, { dot: true });
|
|
75
|
+
includeBases = include.map(p => picomatch.scan(p).base).filter(b => b && b !== '.');
|
|
76
|
+
}
|
|
77
|
+
if (ignore?.length) isIgnoredPat = picomatch(ignore, { dot: true });
|
|
78
|
+
|
|
79
|
+
function walk(dir: string): void {
|
|
80
|
+
let entries: fs.Dirent[];
|
|
81
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
82
|
+
catch { return; }
|
|
83
|
+
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const fullPath = path.join(dir, entry.name);
|
|
86
|
+
const isDir = entry.isDirectory() || (entry.isSymbolicLink() && (() => { try { return fs.statSync(fullPath).isDirectory(); } catch { return false; } })());
|
|
87
|
+
if (isDir) {
|
|
88
|
+
if (isIgnoredDir(entry.name)) continue;
|
|
89
|
+
// Early prune: if include bases are set, skip dirs that can't match
|
|
90
|
+
if (includeBases && includeBases.length > 0) {
|
|
91
|
+
const relDir = path.relative(repoPath, fullPath);
|
|
92
|
+
const canMatch = includeBases.some(base =>
|
|
93
|
+
relDir.startsWith(base) || base.startsWith(relDir),
|
|
94
|
+
);
|
|
95
|
+
if (!canMatch) continue;
|
|
96
|
+
}
|
|
97
|
+
walk(fullPath);
|
|
98
|
+
} else if (entry.isFile()) {
|
|
99
|
+
if (isIgnoredFile(entry.name)) continue;
|
|
100
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
101
|
+
const lang = SUPPORTED_EXTENSIONS[ext];
|
|
102
|
+
if (!lang) continue;
|
|
103
|
+
|
|
104
|
+
// Apply include/ignore filters using relative path
|
|
105
|
+
const rel = path.relative(repoPath, fullPath);
|
|
106
|
+
if (isIncluded && !isIncluded(rel)) continue;
|
|
107
|
+
if (isIgnoredPat && isIgnoredPat(rel)) continue;
|
|
108
|
+
|
|
109
|
+
byLanguage.set(lang, (byLanguage.get(lang) ?? 0) + 1);
|
|
110
|
+
total++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
walk(repoPath);
|
|
116
|
+
|
|
117
|
+
if (total === 0) {
|
|
118
|
+
return { name: 'code', available: false, summary: 'no supported source files found', icon: '📁', checked: false, disabled: 'nothing to index' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const langCount = byLanguage.size;
|
|
122
|
+
const sorted = [...byLanguage.entries()].sort((a, b) => b[1] - a[1]);
|
|
123
|
+
const maxShow = 7;
|
|
124
|
+
const shown = sorted.slice(0, maxShow);
|
|
125
|
+
const remaining = sorted.length - maxShow;
|
|
126
|
+
|
|
127
|
+
const details: string[] = [];
|
|
128
|
+
for (let i = 0; i < shown.length; i++) {
|
|
129
|
+
const [lang, count] = shown[i];
|
|
130
|
+
const isLast = i === shown.length - 1 && remaining <= 0;
|
|
131
|
+
const prefix = isLast ? '└──' : '├──';
|
|
132
|
+
details.push(`${prefix} ${lang.padEnd(14)} ${count} files`);
|
|
133
|
+
}
|
|
134
|
+
if (remaining > 0) {
|
|
135
|
+
details.push(`└── ...and ${remaining} more`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
name: 'code',
|
|
140
|
+
available: true,
|
|
141
|
+
summary: `${total} files (${langCount} language${langCount > 1 ? 's' : ''})`,
|
|
142
|
+
icon: '📁',
|
|
143
|
+
checked: true,
|
|
144
|
+
details,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Scan for git history. */
|
|
149
|
+
function scanGitModule(repoPath: string): ScanModule {
|
|
150
|
+
const stats = scanGitStats(repoPath);
|
|
151
|
+
|
|
152
|
+
if (!stats) {
|
|
153
|
+
return { name: 'git', available: false, summary: 'no .git directory found', icon: '📜', checked: false, disabled: 'not a git repo' };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const details: string[] = [];
|
|
157
|
+
if (stats.lastMessage) {
|
|
158
|
+
details.push(`Last: ${stats.lastMessage} (${stats.lastDate})`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
name: 'git',
|
|
163
|
+
available: true,
|
|
164
|
+
summary: `${stats.commitCount.toLocaleString()} commits`,
|
|
165
|
+
icon: '📜',
|
|
166
|
+
checked: true,
|
|
167
|
+
details,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Scan for document collections. */
|
|
172
|
+
function scanDocsModule(repoPath: string): ScanModule {
|
|
173
|
+
const collections = scanDocsCollections(repoPath);
|
|
174
|
+
|
|
175
|
+
if (collections.length === 0) {
|
|
176
|
+
return { name: 'docs', available: false, summary: 'no documents found', icon: '📄', checked: false, disabled: 'no .md/.mdx files' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const totalFiles = collections.reduce((s, d) => s + d.fileCount, 0);
|
|
180
|
+
const details = collections.map((d, i) => {
|
|
181
|
+
const isLast = i === collections.length - 1;
|
|
182
|
+
const prefix = isLast ? '└──' : '├──';
|
|
183
|
+
return `${prefix} ${d.name.padEnd(10)} → ${d.path} (${d.fileCount} files)`;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
name: 'docs',
|
|
188
|
+
available: true,
|
|
189
|
+
summary: `${collections.length} collection${collections.length > 1 ? 's' : ''} (${totalFiles} files)`,
|
|
190
|
+
icon: '📄',
|
|
191
|
+
checked: true,
|
|
192
|
+
details,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
/** Get git stats for this repo. */
|
|
198
|
+
function scanGitStats(repoPath: string): { commitCount: number; lastMessage: string; lastDate: string } | null {
|
|
199
|
+
if (!fs.existsSync(path.join(repoPath, '.git'))) return null;
|
|
200
|
+
return gitStats(repoPath);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Get git stats for a single directory. */
|
|
204
|
+
function gitStats(dir: string): { commitCount: number; lastMessage: string; lastDate: string } | null {
|
|
205
|
+
try {
|
|
206
|
+
const count = parseInt(execSync('git rev-list --count HEAD', { cwd: dir, encoding: 'utf-8' }).trim(), 10);
|
|
207
|
+
const log = execSync('git log -1 --format="%s|%ar"', { cwd: dir, encoding: 'utf-8' }).trim();
|
|
208
|
+
const [lastMessage, lastDate] = log.split('|');
|
|
209
|
+
return { commitCount: count, lastMessage: lastMessage ?? '', lastDate: lastDate ?? '' };
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Scan for document collections (config + auto-detect). */
|
|
216
|
+
function scanDocsCollections(repoPath: string): { name: string; path: string; fileCount: number }[] {
|
|
217
|
+
const results: { name: string; path: string; fileCount: number }[] = [];
|
|
218
|
+
const seen = new Set<string>();
|
|
219
|
+
|
|
220
|
+
// 1. Read explicit collections from config.json
|
|
221
|
+
const configPath = path.join(repoPath, '.brainbank', 'config.json');
|
|
222
|
+
try {
|
|
223
|
+
if (fs.existsSync(configPath)) {
|
|
224
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as Record<string, unknown>;
|
|
225
|
+
const docsCfg = config?.docs as Record<string, unknown> | undefined;
|
|
226
|
+
const collections = docsCfg?.collections as { name: string; path: string }[] | undefined;
|
|
227
|
+
if (collections) {
|
|
228
|
+
for (const coll of collections) {
|
|
229
|
+
const absPath = path.resolve(repoPath, coll.path);
|
|
230
|
+
results.push({ name: coll.name, path: coll.path, fileCount: countDocs(absPath) });
|
|
231
|
+
seen.add(absPath);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch {}
|
|
236
|
+
|
|
237
|
+
// 2. Auto-detect .md/.mdx in the repo root and top-level dirs
|
|
238
|
+
const rootDocs = countDocsShallow(repoPath);
|
|
239
|
+
if (rootDocs > 0) {
|
|
240
|
+
results.push({ name: '(root)', path: '.', fileCount: rootDocs });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
for (const entry of fs.readdirSync(repoPath, { withFileTypes: true })) {
|
|
245
|
+
if (!entry.isDirectory()) continue;
|
|
246
|
+
if (isIgnoredDir(entry.name)) continue;
|
|
247
|
+
if (entry.name.startsWith('.')) continue;
|
|
248
|
+
|
|
249
|
+
const dirPath = path.join(repoPath, entry.name);
|
|
250
|
+
if (seen.has(dirPath)) continue;
|
|
251
|
+
|
|
252
|
+
const count = countDocs(dirPath);
|
|
253
|
+
if (count > 0) {
|
|
254
|
+
results.push({ name: entry.name, path: `./${entry.name}`, fileCount: count });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch {}
|
|
258
|
+
|
|
259
|
+
return results;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Count .md/.mdx files recursively in a directory. */
|
|
263
|
+
function countDocs(dir: string): number {
|
|
264
|
+
let count = 0;
|
|
265
|
+
try {
|
|
266
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
267
|
+
const ePath = path.join(dir, e.name);
|
|
268
|
+
const isDir = e.isDirectory() || (e.isSymbolicLink() && (() => { try { return fs.statSync(ePath).isDirectory(); } catch { return false; } })());
|
|
269
|
+
if (isDir) {
|
|
270
|
+
if (isIgnoredDir(e.name)) continue;
|
|
271
|
+
count += countDocs(ePath);
|
|
272
|
+
} else if ((e.isFile() || e.isSymbolicLink()) && /\.mdx?$/i.test(e.name)) {
|
|
273
|
+
count++;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch {}
|
|
277
|
+
return count;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Count .md/.mdx files in a directory (non-recursive, root level only). */
|
|
281
|
+
function countDocsShallow(dir: string): number {
|
|
282
|
+
let count = 0;
|
|
283
|
+
try {
|
|
284
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
285
|
+
if (e.isFile() && /\.mdx?$/i.test(e.name)) count++;
|
|
286
|
+
}
|
|
287
|
+
} catch {}
|
|
288
|
+
return count;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Check if config.json exists and read key fields. */
|
|
292
|
+
function scanConfig(repoPath: string): ScanResult['config'] {
|
|
293
|
+
const configPath = path.join(repoPath, '.brainbank', 'config.json');
|
|
294
|
+
if (!fs.existsSync(configPath)) return { exists: false };
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as Record<string, unknown>;
|
|
298
|
+
const codeCfg = config?.code as Record<string, unknown> | undefined;
|
|
299
|
+
|
|
300
|
+
// Merge root-level and per-plugin include/ignore
|
|
301
|
+
const rootInclude = config?.include as string[] | undefined;
|
|
302
|
+
const rootIgnore = config?.ignore as string[] | undefined;
|
|
303
|
+
const pluginInclude = codeCfg?.include as string[] | undefined;
|
|
304
|
+
const pluginIgnore = codeCfg?.ignore as string[] | undefined;
|
|
305
|
+
|
|
306
|
+
const include = [...(rootInclude ?? []), ...(pluginInclude ?? [])];
|
|
307
|
+
const ignore = [...(rootIgnore ?? []), ...(pluginIgnore ?? [])];
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
exists: true,
|
|
311
|
+
ignore: ignore.length > 0 ? ignore : undefined,
|
|
312
|
+
include: include.length > 0 ? include : undefined,
|
|
313
|
+
plugins: config?.plugins as string[] | undefined,
|
|
314
|
+
};
|
|
315
|
+
} catch {
|
|
316
|
+
return { exists: false };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Check DB existence and size. */
|
|
321
|
+
function scanDb(repoPath: string): ScanResult['db'] {
|
|
322
|
+
const dbPath = path.join(repoPath, '.brainbank', 'data', 'brainbank.db');
|
|
323
|
+
if (!fs.existsSync(dbPath)) return { exists: false, sizeMB: 0 };
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const stat = fs.statSync(dbPath);
|
|
327
|
+
return {
|
|
328
|
+
exists: true,
|
|
329
|
+
sizeMB: Math.round(stat.size / 1024 / 1024 * 10) / 10,
|
|
330
|
+
lastModified: stat.mtime,
|
|
331
|
+
};
|
|
332
|
+
} catch {
|
|
333
|
+
return { exists: false, sizeMB: 0 };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
@@ -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
|
+
}
|