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
package/src/cli/utils.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrainBank CLI — Shared Utilities
|
|
3
|
+
*
|
|
4
|
+
* Colors, argument parsing, result formatting, and plugin discovery.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { BrainBank } from '@/brainbank.ts';
|
|
8
|
+
import type { DocsPlugin } from '@/plugin.ts';
|
|
9
|
+
import type { SearchResult } from '@/types.ts';
|
|
10
|
+
|
|
11
|
+
import { isDocsPlugin } from '@/plugin.ts';
|
|
12
|
+
|
|
13
|
+
export const c = {
|
|
14
|
+
green: (s: string) => `\x1b[32m${s}\x1b[0m`,
|
|
15
|
+
red: (s: string) => `\x1b[31m${s}\x1b[0m`,
|
|
16
|
+
yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
|
|
17
|
+
cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
|
|
18
|
+
dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
|
|
19
|
+
bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
|
|
20
|
+
magenta: (s: string) => `\x1b[35m${s}\x1b[0m`,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/** Raw argv, sliced past the Node binary and script path. */
|
|
25
|
+
export const args = process.argv.slice(2);
|
|
26
|
+
|
|
27
|
+
export function getFlag(name: string): string | undefined {
|
|
28
|
+
const idx = args.indexOf(`--${name}`);
|
|
29
|
+
return idx >= 0 ? args[idx + 1] : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Collect all values for a repeated flag (--ignore a --ignore b) or comma-separated (--ignore a,b). */
|
|
33
|
+
export function getFlagAll(name: string): string[] {
|
|
34
|
+
const values: string[] = [];
|
|
35
|
+
const flag = `--${name}`;
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
if (args[i] === flag && args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
38
|
+
for (const v of args[i + 1].split(',')) {
|
|
39
|
+
const trimmed = v.trim();
|
|
40
|
+
if (trimmed) values.push(trimmed);
|
|
41
|
+
}
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return values;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function hasFlag(name: string): boolean {
|
|
49
|
+
return args.includes(`--${name}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Known flags that take a value (--flag <value>). */
|
|
53
|
+
const VALUE_FLAGS = new Set([
|
|
54
|
+
'repo', 'depth', 'collection', 'pattern', 'context', 'name',
|
|
55
|
+
'keep', 'pruner', 'only', 'docs', 'path',
|
|
56
|
+
'ignore', 'include', 'meta', 'k', 'mode', 'limit',
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Strip all --flags AND their values from an argv slice.
|
|
61
|
+
* Returns only positional arguments.
|
|
62
|
+
*
|
|
63
|
+
* Handles:
|
|
64
|
+
* - Known VALUE_FLAGS: --repo /path → skip both
|
|
65
|
+
* - Dynamic source flags: --code 10 → skip both (any --name <number>)
|
|
66
|
+
* - Boolean flags: --force, --yes → skip flag only
|
|
67
|
+
*
|
|
68
|
+
* stripFlags(['ksearch', 'auth', '--repo', '/path', '--code', '10'])
|
|
69
|
+
* → ['ksearch', 'auth']
|
|
70
|
+
*/
|
|
71
|
+
export function stripFlags(argv: string[]): string[] {
|
|
72
|
+
const result: string[] = [];
|
|
73
|
+
for (let i = 0; i < argv.length; i++) {
|
|
74
|
+
if (argv[i].startsWith('--')) {
|
|
75
|
+
const name = argv[i].slice(2);
|
|
76
|
+
const next = argv[i + 1];
|
|
77
|
+
// Known value flag or dynamic numeric value → skip both
|
|
78
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
79
|
+
if (VALUE_FLAGS.has(name) || /^\d+$/.test(next)) {
|
|
80
|
+
i++; // skip the value
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
result.push(argv[i]);
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
export function printResults(results: SearchResult[], minScore = 0.70): void {
|
|
92
|
+
const filtered = results.filter(r => r.score >= minScore).slice(0, 20);
|
|
93
|
+
|
|
94
|
+
if (filtered.length === 0) {
|
|
95
|
+
console.log(c.yellow(` No results above ${Math.round(minScore * 100)}% score.`));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const r of filtered) {
|
|
100
|
+
const score = Math.round(r.score * 100);
|
|
101
|
+
|
|
102
|
+
if (r.type === 'code') {
|
|
103
|
+
const m = r.metadata;
|
|
104
|
+
console.log(
|
|
105
|
+
`${c.green(`[CODE ${score}%]`)} ${c.bold(r.filePath!)} — ` +
|
|
106
|
+
`${m.name || m.chunkType} ${c.dim(`L${m.startLine}-${m.endLine}`)}`,
|
|
107
|
+
);
|
|
108
|
+
console.log(c.dim(r.content.split('\n').slice(0, 5).join('\n')));
|
|
109
|
+
console.log('');
|
|
110
|
+
} else if (r.type === 'commit') {
|
|
111
|
+
const m = r.metadata;
|
|
112
|
+
console.log(
|
|
113
|
+
`${c.cyan(`[COMMIT ${score}%]`)} ${c.bold(m.shortHash)} ` +
|
|
114
|
+
`${r.content} ${c.dim(`(${m.author})`)}`,
|
|
115
|
+
);
|
|
116
|
+
if (m.files?.length) console.log(c.dim(` Files: ${m.files.slice(0, 4).join(', ')}`));
|
|
117
|
+
console.log('');
|
|
118
|
+
} else if (r.type === 'document') {
|
|
119
|
+
const ctx = r.context ? ` — ${c.dim(r.context)}` : '';
|
|
120
|
+
console.log(
|
|
121
|
+
`${c.magenta(`[DOC ${score}%]`)} ${c.bold(r.filePath!)} ` +
|
|
122
|
+
`[${r.metadata.collection}]${ctx}`,
|
|
123
|
+
);
|
|
124
|
+
console.log(c.dim(r.content.split('\n').slice(0, 4).join('\n')));
|
|
125
|
+
console.log('');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Discover the first plugin that implements DocsPlugin by capability. */
|
|
131
|
+
export function findDocsPlugin(brain: BrainBank): DocsPlugin | undefined {
|
|
132
|
+
for (const name of brain.plugins) {
|
|
133
|
+
const p = brain.plugin(name);
|
|
134
|
+
if (p && isDocsPlugin(p)) return p;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { BrainBankConfig, ResolvedConfig } from './types.ts';
|
|
2
|
+
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export const DEFAULTS: ResolvedConfig = {
|
|
7
|
+
repoPath: '.',
|
|
8
|
+
dbPath: '.brainbank/data/brainbank.db',
|
|
9
|
+
gitDepth: 500,
|
|
10
|
+
maxFileSize: 512_000, // 500KB
|
|
11
|
+
maxDiffBytes: 8192,
|
|
12
|
+
hnswM: 16,
|
|
13
|
+
hnswEfConstruction: 200,
|
|
14
|
+
hnswEfSearch: 50,
|
|
15
|
+
embeddingDims: 384,
|
|
16
|
+
maxElements: 2_000_000,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Merge partial config with defaults.
|
|
22
|
+
* All fields become required.
|
|
23
|
+
* Relative dbPath is resolved against repoPath.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveConfig(partial: BrainBankConfig = {}): ResolvedConfig {
|
|
26
|
+
const repoPath = path.resolve(partial.repoPath ?? DEFAULTS.repoPath);
|
|
27
|
+
const rawDbPath = partial.dbPath ?? DEFAULTS.dbPath;
|
|
28
|
+
// Resolve relative dbPath against repoPath so DB lives alongside the repo
|
|
29
|
+
const dbPath = path.isAbsolute(rawDbPath) ? rawDbPath : path.join(repoPath, rawDbPath);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
repoPath,
|
|
33
|
+
dbPath,
|
|
34
|
+
gitDepth: partial.gitDepth ?? DEFAULTS.gitDepth,
|
|
35
|
+
maxFileSize: partial.maxFileSize ?? DEFAULTS.maxFileSize,
|
|
36
|
+
maxDiffBytes: partial.maxDiffBytes ?? DEFAULTS.maxDiffBytes,
|
|
37
|
+
hnswM: partial.hnswM ?? DEFAULTS.hnswM,
|
|
38
|
+
hnswEfConstruction: partial.hnswEfConstruction ?? DEFAULTS.hnswEfConstruction,
|
|
39
|
+
hnswEfSearch: partial.hnswEfSearch ?? DEFAULTS.hnswEfSearch,
|
|
40
|
+
embeddingDims: partial.embeddingDims ?? DEFAULTS.embeddingDims,
|
|
41
|
+
maxElements: partial.maxElements ?? DEFAULTS.maxElements,
|
|
42
|
+
embeddingProvider: partial.embeddingProvider,
|
|
43
|
+
pruner: partial.pruner,
|
|
44
|
+
expander: partial.expander,
|
|
45
|
+
webhookPort: partial.webhookPort,
|
|
46
|
+
contextFields: partial.contextFields,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrainBank — Constants
|
|
3
|
+
*
|
|
4
|
+
* Core-only constants. Plugin names are NOT defined here — they belong
|
|
5
|
+
* to their respective packages. Only keys owned by the core live here.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const pkg = require('../package.json') as { version: string };
|
|
12
|
+
|
|
13
|
+
/** Package version from package.json. */
|
|
14
|
+
export const VERSION: string = pkg.version;
|
|
15
|
+
|
|
16
|
+
/** HNSW index key for KV collections (core-owned). */
|
|
17
|
+
export const HNSW = {
|
|
18
|
+
KV: 'kv',
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export type HnswKey = typeof HNSW[keyof typeof HNSW];
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrainBank — Database Adapter Interface
|
|
3
|
+
*
|
|
4
|
+
* Abstract contract for database operations. All consumers depend on
|
|
5
|
+
* this interface — never on a concrete driver. The `SQLiteAdapter`
|
|
6
|
+
* is the built-in implementation; future adapters (LibSQL, Turso,
|
|
7
|
+
* PostgreSQL) implement the same contract.
|
|
8
|
+
*
|
|
9
|
+
* Phase 1: sync-first API matching the existing synchronous SQLite usage.
|
|
10
|
+
* Async variants will be added when needed by async-native adapters.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/** Result from mutating queries (INSERT / UPDATE / DELETE). */
|
|
15
|
+
export interface ExecuteResult {
|
|
16
|
+
/** Row ID of the last inserted row. */
|
|
17
|
+
lastInsertRowid: number | bigint;
|
|
18
|
+
/** Number of rows changed by the statement. */
|
|
19
|
+
changes: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A prepared statement with typed query methods. */
|
|
23
|
+
export interface PreparedStatement<T = unknown> {
|
|
24
|
+
/** Execute a query and return the first matching row, or `undefined`. */
|
|
25
|
+
get(...params: unknown[]): T | undefined;
|
|
26
|
+
/** Execute a query and return all matching rows. */
|
|
27
|
+
all(...params: unknown[]): T[];
|
|
28
|
+
/** Execute a mutating statement and return the result. */
|
|
29
|
+
run(...params: unknown[]): ExecuteResult;
|
|
30
|
+
/** Iterate over matching rows without loading them all into memory. */
|
|
31
|
+
iterate(...params: unknown[]): IterableIterator<T>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Adapter capability flags — describes what the underlying engine supports. */
|
|
35
|
+
export interface AdapterCapabilities {
|
|
36
|
+
/** Full-text search engine. */
|
|
37
|
+
fts: 'fts5' | 'tsvector' | 'none';
|
|
38
|
+
/** Upsert syntax dialect. */
|
|
39
|
+
upsert: 'or-replace' | 'on-conflict';
|
|
40
|
+
/** Native JSON column support. */
|
|
41
|
+
json: boolean;
|
|
42
|
+
/** Native vector column support (e.g. pgvector). */
|
|
43
|
+
vectors: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Database adapter interface.
|
|
48
|
+
*
|
|
49
|
+
* All BrainBank components depend on this contract instead of a
|
|
50
|
+
* concrete database driver. Keeps the door open for LibSQL, Turso,
|
|
51
|
+
* PostgreSQL, etc. without touching consumer code.
|
|
52
|
+
*/
|
|
53
|
+
export interface DatabaseAdapter {
|
|
54
|
+
/** Prepare a reusable statement. */
|
|
55
|
+
prepare<T = unknown>(sql: string): PreparedStatement<T>;
|
|
56
|
+
|
|
57
|
+
/** Execute raw DDL / multi-statement SQL (no results). */
|
|
58
|
+
exec(sql: string): void;
|
|
59
|
+
|
|
60
|
+
/** Run `fn` inside a transaction. Auto-commits on success, auto-rollbacks on error. */
|
|
61
|
+
transaction<T>(fn: () => T): T;
|
|
62
|
+
|
|
63
|
+
/** Run a prepared statement on multiple rows inside a single transaction. */
|
|
64
|
+
batch<T extends unknown[]>(sql: string, rows: T[]): void;
|
|
65
|
+
|
|
66
|
+
/** Close the database and release resources. */
|
|
67
|
+
close(): void;
|
|
68
|
+
|
|
69
|
+
/** Engine capabilities (FTS, upsert dialect, etc.). */
|
|
70
|
+
readonly capabilities: AdapterCapabilities;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Escape hatch: access the underlying raw driver.
|
|
74
|
+
* Returns `undefined` for adapters that don't support raw access.
|
|
75
|
+
*
|
|
76
|
+
* @deprecated Use `DatabaseAdapter` methods instead. This exists
|
|
77
|
+
* only for gradual migration of plugins that depend on driver internals.
|
|
78
|
+
*/
|
|
79
|
+
raw<T = unknown>(): T | undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Row Types ────────────────────────────────────────────────────────
|
|
83
|
+
// Typed interfaces for rows returned by core SQLite queries.
|
|
84
|
+
// Domain-specific row types live in their respective packages.
|
|
85
|
+
|
|
86
|
+
export interface KvDataRow {
|
|
87
|
+
id: number;
|
|
88
|
+
collection: string;
|
|
89
|
+
content: string;
|
|
90
|
+
meta_json: string;
|
|
91
|
+
tags_json: string;
|
|
92
|
+
expires_at: number | null;
|
|
93
|
+
created_at: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface KvVectorRow {
|
|
97
|
+
data_id: number;
|
|
98
|
+
embedding: Uint8Array;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface EmbeddingMetaRow {
|
|
102
|
+
value: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface VectorRow {
|
|
106
|
+
id: number;
|
|
107
|
+
embedding: Uint8Array;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface CountRow {
|
|
111
|
+
c: number;
|
|
112
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrainBank — Database Metadata
|
|
3
|
+
*
|
|
4
|
+
* Helpers for reading/writing metadata stored in core SQLite tables:
|
|
5
|
+
*
|
|
6
|
+
* - **Index State** — cross-process HNSW version tracking.
|
|
7
|
+
* Processes compare in-memory versions with DB to detect staleness
|
|
8
|
+
* and trigger hot-reload via `ensureFresh()`.
|
|
9
|
+
*
|
|
10
|
+
* - **Embedding Meta** — tracks which embedding provider is stored in
|
|
11
|
+
* the database. Detects dimension mismatches at startup and updates
|
|
12
|
+
* metadata after `reembed()`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { EmbeddingProvider } from '@/types.ts';
|
|
16
|
+
import type { DatabaseAdapter, EmbeddingMetaRow } from './adapter.ts';
|
|
17
|
+
|
|
18
|
+
import { providerKey } from '@/lib/provider-key.ts';
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
// ── Index State ─────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/** Row shape returned from index_state queries. */
|
|
24
|
+
interface IndexStateRow {
|
|
25
|
+
name: string;
|
|
26
|
+
version: number;
|
|
27
|
+
writer_pid: number;
|
|
28
|
+
updated_at: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Increment the version for a given index name.
|
|
33
|
+
* Sets writer_pid to current process PID.
|
|
34
|
+
* Uses UPSERT so the row is created on first call.
|
|
35
|
+
*/
|
|
36
|
+
export function bumpVersion(db: DatabaseAdapter, name: string): number {
|
|
37
|
+
const row = db.prepare(`
|
|
38
|
+
INSERT INTO index_state (name, version, writer_pid, updated_at)
|
|
39
|
+
VALUES (?, 1, ?, unixepoch())
|
|
40
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
41
|
+
version = version + 1,
|
|
42
|
+
writer_pid = excluded.writer_pid,
|
|
43
|
+
updated_at = excluded.updated_at
|
|
44
|
+
RETURNING version
|
|
45
|
+
`).get(name, process.pid) as { version: number };
|
|
46
|
+
return row.version;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get all index versions as a Map.
|
|
51
|
+
* Used by `ensureFresh()` to compare against in-memory versions.
|
|
52
|
+
*/
|
|
53
|
+
export function getVersions(db: DatabaseAdapter): Map<string, number> {
|
|
54
|
+
const rows = db.prepare('SELECT name, version FROM index_state').all() as IndexStateRow[];
|
|
55
|
+
const map = new Map<string, number>();
|
|
56
|
+
for (const row of rows) {
|
|
57
|
+
map.set(row.name, row.version);
|
|
58
|
+
}
|
|
59
|
+
return map;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Get the version of a single index. Returns 0 if not found. */
|
|
63
|
+
export function getVersion(db: DatabaseAdapter, name: string): number {
|
|
64
|
+
const row = db.prepare('SELECT version FROM index_state WHERE name = ?').get(name) as { version: number } | undefined;
|
|
65
|
+
return row?.version ?? 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
// ── Embedding Meta ──────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/** Stored embedding metadata shape. */
|
|
72
|
+
export interface EmbeddingMeta {
|
|
73
|
+
provider: string;
|
|
74
|
+
dims: number;
|
|
75
|
+
/** Stable key for auto-resolving provider on startup (e.g. 'openai', 'local'). */
|
|
76
|
+
providerKey: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Get stored embedding metadata. Returns null if not set. */
|
|
80
|
+
export function getEmbeddingMeta(db: DatabaseAdapter): EmbeddingMeta | null {
|
|
81
|
+
try {
|
|
82
|
+
const provider = db.prepare(
|
|
83
|
+
"SELECT value FROM embedding_meta WHERE key = 'provider'"
|
|
84
|
+
).get() as EmbeddingMetaRow | undefined;
|
|
85
|
+
const dims = db.prepare(
|
|
86
|
+
"SELECT value FROM embedding_meta WHERE key = 'dims'"
|
|
87
|
+
).get() as EmbeddingMetaRow | undefined;
|
|
88
|
+
const key = db.prepare(
|
|
89
|
+
"SELECT value FROM embedding_meta WHERE key = 'provider_key'"
|
|
90
|
+
).get() as EmbeddingMetaRow | undefined;
|
|
91
|
+
|
|
92
|
+
if (!provider || !dims) return null;
|
|
93
|
+
return {
|
|
94
|
+
provider: provider.value,
|
|
95
|
+
dims: Number(dims.value),
|
|
96
|
+
providerKey: key?.value ?? 'local',
|
|
97
|
+
};
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Store current provider info. */
|
|
104
|
+
export function setEmbeddingMeta(db: DatabaseAdapter, embedding: EmbeddingProvider): void {
|
|
105
|
+
const upsert = db.prepare(
|
|
106
|
+
'INSERT OR REPLACE INTO embedding_meta (key, value) VALUES (?, ?)'
|
|
107
|
+
);
|
|
108
|
+
upsert.run('provider', embedding.constructor?.name ?? 'unknown');
|
|
109
|
+
upsert.run('dims', String(embedding.dims));
|
|
110
|
+
upsert.run('provider_key', providerKey(embedding));
|
|
111
|
+
upsert.run('indexed_at', new Date().toISOString());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Check if the configured provider differs from what's stored. */
|
|
115
|
+
export function detectProviderMismatch(
|
|
116
|
+
db: DatabaseAdapter,
|
|
117
|
+
embedding: EmbeddingProvider,
|
|
118
|
+
): { mismatch: boolean; stored: string; current: string } | null {
|
|
119
|
+
const meta = getEmbeddingMeta(db);
|
|
120
|
+
if (!meta) return null; // First time, no mismatch
|
|
121
|
+
|
|
122
|
+
const currentName = embedding.constructor?.name ?? 'unknown';
|
|
123
|
+
const mismatch = meta.dims !== embedding.dims || meta.provider !== currentName;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
mismatch,
|
|
127
|
+
stored: `${meta.provider}/${meta.dims}`,
|
|
128
|
+
current: `${currentName}/${embedding.dims}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrainBank — Plugin Migration System
|
|
3
|
+
*
|
|
4
|
+
* Per-plugin versioned schema migrations.
|
|
5
|
+
* Each plugin declares a schemaVersion + ordered migrations array.
|
|
6
|
+
* Core stores applied versions in `plugin_versions` table.
|
|
7
|
+
*
|
|
8
|
+
* Plugins call `runPluginMigrations()` at the top of their `initialize()`.
|
|
9
|
+
* Migrations use `IF NOT EXISTS` so first run on an existing DB is a no-op.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { DatabaseAdapter } from './adapter.ts';
|
|
13
|
+
|
|
14
|
+
/** A single migration step. */
|
|
15
|
+
export interface Migration {
|
|
16
|
+
/** Version this migration brings the schema to. */
|
|
17
|
+
version: number;
|
|
18
|
+
/** Apply the migration. Must be idempotent (use IF NOT EXISTS). */
|
|
19
|
+
up(adapter: DatabaseAdapter): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Get the currently stored schema version for a plugin. Returns 0 if no record. */
|
|
23
|
+
export function getPluginVersion(adapter: DatabaseAdapter, pluginName: string): number {
|
|
24
|
+
try {
|
|
25
|
+
const row = adapter.prepare(
|
|
26
|
+
'SELECT version FROM plugin_versions WHERE plugin_name = ?'
|
|
27
|
+
).get(pluginName) as { version: number } | undefined;
|
|
28
|
+
return row?.version ?? 0;
|
|
29
|
+
} catch {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Set the schema version for a plugin. */
|
|
35
|
+
export function setPluginVersion(adapter: DatabaseAdapter, pluginName: string, version: number): void {
|
|
36
|
+
adapter.prepare(`
|
|
37
|
+
INSERT OR REPLACE INTO plugin_versions (plugin_name, version, applied_at)
|
|
38
|
+
VALUES (?, ?, unixepoch())
|
|
39
|
+
`).run(pluginName, version);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run pending migrations for a plugin.
|
|
44
|
+
* Skips migrations whose version <= stored version.
|
|
45
|
+
* Each migration runs in its own transaction.
|
|
46
|
+
*/
|
|
47
|
+
export function runPluginMigrations(
|
|
48
|
+
adapter: DatabaseAdapter,
|
|
49
|
+
pluginName: string,
|
|
50
|
+
schemaVersion: number,
|
|
51
|
+
migrations: Migration[],
|
|
52
|
+
): void {
|
|
53
|
+
const current = getPluginVersion(adapter, pluginName);
|
|
54
|
+
if (current >= schemaVersion) return;
|
|
55
|
+
|
|
56
|
+
const sorted = [...migrations].sort((a, b) => a.version - b.version);
|
|
57
|
+
|
|
58
|
+
for (const m of sorted) {
|
|
59
|
+
if (m.version <= current) continue;
|
|
60
|
+
|
|
61
|
+
adapter.transaction(() => {
|
|
62
|
+
m.up(adapter);
|
|
63
|
+
setPluginVersion(adapter, pluginName, m.version);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|