@andespindola/brainlink 0.1.0-beta.14 → 0.1.0-beta.140
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/AGENTS.md +8 -5
- package/CHANGELOG.md +26 -2
- package/CONTRIBUTING.md +2 -2
- package/COPYRIGHT.md +5 -0
- package/README.md +144 -22
- package/SECURITY.md +1 -1
- package/dist/application/analyze-vault.js +1 -15
- package/dist/application/build-context.js +64 -3
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +110 -45
- package/dist/application/frontend/client-html.js +35 -26
- package/dist/application/frontend/client-js.js +2987 -153
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-layout.js +39 -6
- package/dist/application/get-graph-node.js +3 -3
- package/dist/application/get-graph-summary.js +3 -3
- package/dist/application/get-graph-view.js +243 -0
- package/dist/application/get-graph.js +3 -3
- package/dist/application/import-legacy-sqlite.js +296 -0
- package/dist/application/index-vault.js +253 -25
- package/dist/application/list-agents.js +3 -3
- package/dist/application/list-links.js +5 -5
- package/dist/application/offline-pack-backup.js +44 -0
- package/dist/application/search-graph-node-ids.js +3 -3
- package/dist/application/search-knowledge.js +4 -5
- package/dist/application/server/routes.js +156 -5
- package/dist/application/start-server.js +75 -4
- package/dist/application/watch-vault.js +23 -2
- package/dist/benchmarks/large-vault.js +1 -1
- package/dist/cli/commands/agent-commands.js +7 -0
- package/dist/cli/commands/write-commands.js +842 -8
- package/dist/domain/context.js +54 -11
- package/dist/domain/graph-layout.js +181 -3
- package/dist/domain/markdown.js +29 -9
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +38 -0
- package/dist/infrastructure/file-index.js +358 -0
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/index-state.js +58 -0
- package/dist/infrastructure/private-pack-codec.js +71 -10
- package/dist/infrastructure/search-packs.js +276 -87
- package/dist/infrastructure/volatile-memory.js +100 -0
- package/dist/mcp/server.js +21 -1
- package/dist/mcp/tools.js +96 -0
- package/docs/AGENT_USAGE.md +101 -19
- package/docs/ARCHITECTURE.md +23 -28
- package/docs/QUICKSTART.md +7 -0
- package/package.json +6 -4
- package/dist/infrastructure/sqlite/document-writer.js +0 -51
- package/dist/infrastructure/sqlite/graph-reader.js +0 -267
- package/dist/infrastructure/sqlite/recovery.js +0 -163
- package/dist/infrastructure/sqlite/schema.js +0 -114
- package/dist/infrastructure/sqlite/search-reader.js +0 -188
- package/dist/infrastructure/sqlite/types.js +0 -1
- package/dist/infrastructure/sqlite-index.js +0 -38
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import Database from 'better-sqlite3';
|
|
2
1
|
import { gunzipSync } from 'node:zlib';
|
|
3
2
|
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
|
-
import {
|
|
4
|
+
import { middleOutIndices } from '../domain/middle-out.js';
|
|
6
5
|
import { decodePrivatePack, encodePrivatePack, isPrivatePackPayload } from './private-pack-codec.js';
|
|
7
6
|
const packsDirectoryName = 'search-packs';
|
|
8
7
|
const manifestFileName = 'manifest.json';
|
|
9
|
-
const
|
|
8
|
+
const defaultBuildOptions = {
|
|
9
|
+
rowChunkSize: 5_000,
|
|
10
|
+
compressionLevel: 5,
|
|
11
|
+
useDictionary: true
|
|
12
|
+
};
|
|
10
13
|
const queryTokenPattern = /[\p{L}\p{N}_-]+/gu;
|
|
14
|
+
const bloomBytes = 256;
|
|
15
|
+
const bloomBitSize = bloomBytes * 8;
|
|
16
|
+
const bloomSeeds = [0x9e3779b1, 0x85ebca6b, 0xc2b2ae35];
|
|
11
17
|
const toPackDirectory = (vaultPath) => join(vaultPath, '.brainlink', packsDirectoryName);
|
|
12
18
|
const toManifestPath = (vaultPath) => join(toPackDirectory(vaultPath), manifestFileName);
|
|
13
|
-
const toDatabasePath = (vaultPath) => join(vaultPath, '.brainlink', 'brainlink.db');
|
|
14
19
|
const parseRowsFromPack = async (vaultPath, content) => {
|
|
15
20
|
const raw = isPrivatePackPayload(content) ? await decodePrivatePack(vaultPath, content) : gunzipSync(content);
|
|
16
21
|
return raw
|
|
@@ -18,7 +23,29 @@ const parseRowsFromPack = async (vaultPath, content) => {
|
|
|
18
23
|
.split('\n')
|
|
19
24
|
.map((line) => line.trim())
|
|
20
25
|
.filter((line) => line.length > 0)
|
|
21
|
-
.map((line) => JSON.parse(line))
|
|
26
|
+
.map((line) => JSON.parse(line))
|
|
27
|
+
.flatMap((row) => {
|
|
28
|
+
if (typeof row.documentId !== 'string' ||
|
|
29
|
+
typeof row.agentId !== 'string' ||
|
|
30
|
+
typeof row.title !== 'string' ||
|
|
31
|
+
typeof row.path !== 'string' ||
|
|
32
|
+
typeof row.chunkId !== 'string' ||
|
|
33
|
+
typeof row.content !== 'string') {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
documentId: row.documentId,
|
|
39
|
+
agentId: row.agentId,
|
|
40
|
+
title: row.title,
|
|
41
|
+
path: row.path,
|
|
42
|
+
chunkId: row.chunkId,
|
|
43
|
+
chunkOrdinal: typeof row.chunkOrdinal === 'number' ? row.chunkOrdinal : 0,
|
|
44
|
+
content: row.content,
|
|
45
|
+
tags: Array.isArray(row.tags) ? row.tags.filter((item) => typeof item === 'string') : []
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
});
|
|
22
49
|
};
|
|
23
50
|
const toRows = (documents) => documents.flatMap((document) => document.chunks.map((chunk) => ({
|
|
24
51
|
documentId: document.document.id,
|
|
@@ -26,21 +53,121 @@ const toRows = (documents) => documents.flatMap((document) => document.chunks.ma
|
|
|
26
53
|
title: document.document.title,
|
|
27
54
|
path: document.document.path,
|
|
28
55
|
chunkId: chunk.id,
|
|
56
|
+
chunkOrdinal: chunk.ordinal,
|
|
29
57
|
content: chunk.content,
|
|
30
58
|
tags: document.document.tags
|
|
31
59
|
})));
|
|
32
60
|
const writeManifest = async (vaultPath, manifest) => {
|
|
33
61
|
await writeFile(toManifestPath(vaultPath), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
34
62
|
};
|
|
35
|
-
const
|
|
63
|
+
const readManifest = async (vaultPath) => {
|
|
36
64
|
try {
|
|
37
|
-
const parsed = JSON.parse(
|
|
38
|
-
|
|
65
|
+
const parsed = JSON.parse(await readFile(toManifestPath(vaultPath), 'utf8'));
|
|
66
|
+
if (parsed.version === 2 && parsed.format === 'private-v2') {
|
|
67
|
+
return {
|
|
68
|
+
version: 2,
|
|
69
|
+
createdAt: typeof parsed.createdAt === 'string' ? parsed.createdAt : new Date().toISOString(),
|
|
70
|
+
packCount: typeof parsed.packCount === 'number' ? parsed.packCount : 0,
|
|
71
|
+
recordCount: typeof parsed.recordCount === 'number' ? parsed.recordCount : 0,
|
|
72
|
+
format: 'private-v2'
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (parsed.version === 3 && parsed.format === 'private-v2') {
|
|
76
|
+
const packIndex = Array.isArray(parsed.packIndex)
|
|
77
|
+
? parsed.packIndex.flatMap((entry) => {
|
|
78
|
+
if (!entry || typeof entry !== 'object') {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const candidate = entry;
|
|
82
|
+
if (typeof candidate.fileName !== 'string' || typeof candidate.tokenBloomB64 !== 'string') {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
fileName: candidate.fileName,
|
|
88
|
+
recordCount: typeof candidate.recordCount === 'number' ? candidate.recordCount : 0,
|
|
89
|
+
agents: Array.isArray(candidate.agents) ? candidate.agents.filter((item) => typeof item === 'string') : [],
|
|
90
|
+
tokenBloomB64: candidate.tokenBloomB64
|
|
91
|
+
}
|
|
92
|
+
];
|
|
93
|
+
})
|
|
94
|
+
: [];
|
|
95
|
+
return {
|
|
96
|
+
version: 3,
|
|
97
|
+
createdAt: typeof parsed.createdAt === 'string' ? parsed.createdAt : new Date().toISOString(),
|
|
98
|
+
packCount: typeof parsed.packCount === 'number' ? parsed.packCount : packIndex.length,
|
|
99
|
+
recordCount: typeof parsed.recordCount === 'number' ? parsed.recordCount : 0,
|
|
100
|
+
format: 'private-v2',
|
|
101
|
+
packIndex,
|
|
102
|
+
...(parsed.packConfig && typeof parsed.packConfig === 'object'
|
|
103
|
+
? {
|
|
104
|
+
packConfig: {
|
|
105
|
+
rowChunkSize: typeof parsed.packConfig.rowChunkSize === 'number'
|
|
106
|
+
? parsed.packConfig.rowChunkSize
|
|
107
|
+
: defaultBuildOptions.rowChunkSize,
|
|
108
|
+
compressionLevel: typeof parsed.packConfig.compressionLevel === 'number'
|
|
109
|
+
? parsed.packConfig.compressionLevel
|
|
110
|
+
: defaultBuildOptions.compressionLevel,
|
|
111
|
+
useDictionary: typeof parsed.packConfig.useDictionary === 'boolean'
|
|
112
|
+
? parsed.packConfig.useDictionary
|
|
113
|
+
: defaultBuildOptions.useDictionary
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
117
|
+
...(parsed.compression &&
|
|
118
|
+
typeof parsed.compression === 'object' &&
|
|
119
|
+
typeof parsed.compression.inputBytes === 'number' &&
|
|
120
|
+
typeof parsed.compression.outputBytes === 'number' &&
|
|
121
|
+
typeof parsed.compression.ratio === 'number' &&
|
|
122
|
+
typeof parsed.compression.savedBytes === 'number'
|
|
123
|
+
? {
|
|
124
|
+
compression: {
|
|
125
|
+
inputBytes: parsed.compression.inputBytes,
|
|
126
|
+
outputBytes: parsed.compression.outputBytes,
|
|
127
|
+
ratio: parsed.compression.ratio,
|
|
128
|
+
savedBytes: parsed.compression.savedBytes
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
: {})
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
39
135
|
}
|
|
40
136
|
catch {
|
|
41
|
-
return
|
|
137
|
+
return null;
|
|
42
138
|
}
|
|
43
139
|
};
|
|
140
|
+
export const ensureSearchPackManifest = async (vaultPath) => {
|
|
141
|
+
const manifest = await readManifest(vaultPath);
|
|
142
|
+
if (manifest) {
|
|
143
|
+
return {
|
|
144
|
+
repaired: false,
|
|
145
|
+
source: 'not-needed',
|
|
146
|
+
packCount: manifest.packCount
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const files = await sortedPackFiles(vaultPath);
|
|
150
|
+
const packFiles = files.filter((file) => file.endsWith('.blpk'));
|
|
151
|
+
if (packFiles.length === 0) {
|
|
152
|
+
return {
|
|
153
|
+
repaired: false,
|
|
154
|
+
source: 'no-packs',
|
|
155
|
+
packCount: 0
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
await writeManifest(vaultPath, {
|
|
159
|
+
version: 2,
|
|
160
|
+
createdAt: new Date().toISOString(),
|
|
161
|
+
packCount: packFiles.length,
|
|
162
|
+
recordCount: 0,
|
|
163
|
+
format: 'private-v2'
|
|
164
|
+
});
|
|
165
|
+
return {
|
|
166
|
+
repaired: true,
|
|
167
|
+
source: 'existing-packs',
|
|
168
|
+
packCount: packFiles.length
|
|
169
|
+
};
|
|
170
|
+
};
|
|
44
171
|
const chunkRows = (rows, size) => {
|
|
45
172
|
const chunks = [];
|
|
46
173
|
for (let index = 0; index < rows.length; index += size) {
|
|
@@ -69,6 +196,51 @@ const countOccurrences = (text, token) => {
|
|
|
69
196
|
}
|
|
70
197
|
return hits;
|
|
71
198
|
};
|
|
199
|
+
const hashToken = (token, seed) => {
|
|
200
|
+
let hash = seed >>> 0;
|
|
201
|
+
for (let index = 0; index < token.length; index += 1) {
|
|
202
|
+
hash ^= token.charCodeAt(index);
|
|
203
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
204
|
+
}
|
|
205
|
+
return hash >>> 0;
|
|
206
|
+
};
|
|
207
|
+
const createBloom = () => new Uint8Array(bloomBytes);
|
|
208
|
+
const bloomAdd = (bloom, token) => {
|
|
209
|
+
bloomSeeds.forEach((seed) => {
|
|
210
|
+
const bit = hashToken(token, seed) % bloomBitSize;
|
|
211
|
+
bloom[Math.floor(bit / 8)] |= 1 << (bit % 8);
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
const bloomMayContain = (bloom, token) => bloomSeeds.every((seed) => {
|
|
215
|
+
const bit = hashToken(token, seed) % bloomBitSize;
|
|
216
|
+
return (bloom[Math.floor(bit / 8)] & (1 << (bit % 8))) !== 0;
|
|
217
|
+
});
|
|
218
|
+
const bloomFromRows = (rows) => {
|
|
219
|
+
const bloom = createBloom();
|
|
220
|
+
rows.forEach((row) => {
|
|
221
|
+
tokenize([row.title, row.path, row.tags.join(' '), row.content].join(' ')).forEach((token) => bloomAdd(bloom, token));
|
|
222
|
+
});
|
|
223
|
+
return bloom;
|
|
224
|
+
};
|
|
225
|
+
const bloomToBase64 = (bloom) => Buffer.from(bloom).toString('base64url');
|
|
226
|
+
const bloomFromBase64 = (value) => {
|
|
227
|
+
try {
|
|
228
|
+
const decoded = Buffer.from(value, 'base64url');
|
|
229
|
+
if (decoded.byteLength === bloomBytes) {
|
|
230
|
+
return {
|
|
231
|
+
bloom: new Uint8Array(decoded),
|
|
232
|
+
valid: true
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// fallback below
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
bloom: createBloom(),
|
|
241
|
+
valid: false
|
|
242
|
+
};
|
|
243
|
+
};
|
|
72
244
|
const computeTextScore = (row, tokens) => {
|
|
73
245
|
if (tokens.length === 0) {
|
|
74
246
|
return 0;
|
|
@@ -91,6 +263,7 @@ const toSearchResult = (row, score) => ({
|
|
|
91
263
|
title: row.title,
|
|
92
264
|
path: row.path,
|
|
93
265
|
chunkId: row.chunkId,
|
|
266
|
+
chunkOrdinal: row.chunkOrdinal,
|
|
94
267
|
content: row.content,
|
|
95
268
|
score,
|
|
96
269
|
textScore: score,
|
|
@@ -112,7 +285,8 @@ const sortedPackFiles = async (vaultPath) => {
|
|
|
112
285
|
throw error;
|
|
113
286
|
}
|
|
114
287
|
};
|
|
115
|
-
const writeRowsAsPrivatePacks = async (vaultPath, rows, clearExisting) => {
|
|
288
|
+
const writeRowsAsPrivatePacks = async (vaultPath, rows, clearExisting, options) => {
|
|
289
|
+
const startedAt = process.hrtime.bigint();
|
|
116
290
|
const directory = toPackDirectory(vaultPath);
|
|
117
291
|
await mkdir(directory, { recursive: true });
|
|
118
292
|
if (clearExisting) {
|
|
@@ -121,88 +295,102 @@ const writeRowsAsPrivatePacks = async (vaultPath, rows, clearExisting) => {
|
|
|
121
295
|
.filter((name) => name.endsWith('.blpk') || name.endsWith('.jsonl.gz') || name === manifestFileName)
|
|
122
296
|
.map((name) => rm(join(directory, name), { force: true })));
|
|
123
297
|
}
|
|
124
|
-
const chunks = chunkRows(rows, rowChunkSize);
|
|
125
|
-
|
|
298
|
+
const chunks = chunkRows(rows, options.rowChunkSize);
|
|
299
|
+
const packIndex = [];
|
|
300
|
+
let inputBytes = 0;
|
|
301
|
+
let outputBytes = 0;
|
|
302
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
303
|
+
const chunk = chunks[index];
|
|
126
304
|
const fileName = `pack-${String(index + 1).padStart(4, '0')}.blpk`;
|
|
127
305
|
const serialized = `${chunk.map((row) => JSON.stringify(row)).join('\n')}\n`;
|
|
128
|
-
const compressed = await encodePrivatePack(vaultPath, Buffer.from(serialized, 'utf8')
|
|
306
|
+
const compressed = await encodePrivatePack(vaultPath, Buffer.from(serialized, 'utf8'), {
|
|
307
|
+
compressionLevel: options.compressionLevel,
|
|
308
|
+
useDictionary: options.useDictionary
|
|
309
|
+
});
|
|
310
|
+
const tokenBloomB64 = bloomToBase64(bloomFromRows(chunk));
|
|
129
311
|
await writeFile(join(directory, fileName), compressed);
|
|
130
|
-
|
|
312
|
+
inputBytes += Buffer.byteLength(serialized, 'utf8');
|
|
313
|
+
outputBytes += compressed.byteLength;
|
|
314
|
+
packIndex.push({
|
|
315
|
+
fileName,
|
|
316
|
+
recordCount: chunk.length,
|
|
317
|
+
agents: Array.from(new Set(chunk.map((row) => row.agentId))).sort((left, right) => left.localeCompare(right)),
|
|
318
|
+
tokenBloomB64
|
|
319
|
+
});
|
|
320
|
+
}
|
|
131
321
|
await writeManifest(vaultPath, {
|
|
132
|
-
version:
|
|
322
|
+
version: 3,
|
|
133
323
|
createdAt: new Date().toISOString(),
|
|
134
324
|
packCount: chunks.length,
|
|
135
325
|
recordCount: rows.length,
|
|
136
|
-
format: 'private-v2'
|
|
326
|
+
format: 'private-v2',
|
|
327
|
+
packIndex,
|
|
328
|
+
packConfig: {
|
|
329
|
+
rowChunkSize: options.rowChunkSize,
|
|
330
|
+
compressionLevel: options.compressionLevel,
|
|
331
|
+
useDictionary: options.useDictionary
|
|
332
|
+
},
|
|
333
|
+
compression: {
|
|
334
|
+
inputBytes,
|
|
335
|
+
outputBytes,
|
|
336
|
+
ratio: outputBytes / Math.max(inputBytes, 1),
|
|
337
|
+
savedBytes: Math.max(inputBytes - outputBytes, 0)
|
|
338
|
+
}
|
|
137
339
|
});
|
|
340
|
+
const durationMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
|
|
341
|
+
const safeInput = Math.max(inputBytes, 1);
|
|
342
|
+
const savedBytes = Math.max(inputBytes - outputBytes, 0);
|
|
138
343
|
return {
|
|
139
344
|
packCount: chunks.length,
|
|
140
|
-
recordCount: rows.length
|
|
345
|
+
recordCount: rows.length,
|
|
346
|
+
compression: {
|
|
347
|
+
inputBytes,
|
|
348
|
+
outputBytes,
|
|
349
|
+
ratio: outputBytes / safeInput,
|
|
350
|
+
savedBytes
|
|
351
|
+
},
|
|
352
|
+
durationMs
|
|
141
353
|
};
|
|
142
354
|
};
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
};
|
|
147
|
-
const tableColumns = (database, table) => {
|
|
148
|
-
const rows = database.prepare(`SELECT name FROM pragma_table_info('${table.replaceAll("'", "''")}')`).all();
|
|
149
|
-
return new Set(rows.map((row) => row.name));
|
|
150
|
-
};
|
|
151
|
-
const loadRowsFromLegacySqlite = (vaultPath) => {
|
|
152
|
-
const databasePath = toDatabasePath(vaultPath);
|
|
153
|
-
if (!existsSync(databasePath)) {
|
|
355
|
+
const selectCandidatePackFiles = async (vaultPath, tokens, agentId) => {
|
|
356
|
+
const allFiles = await sortedPackFiles(vaultPath);
|
|
357
|
+
if (allFiles.length === 0) {
|
|
154
358
|
return [];
|
|
155
359
|
}
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
360
|
+
const manifest = await readManifest(vaultPath);
|
|
361
|
+
if (!manifest || manifest.version !== 3 || !Array.isArray(manifest.packIndex)) {
|
|
362
|
+
return allFiles;
|
|
363
|
+
}
|
|
364
|
+
const normalizedAgent = agentId?.trim();
|
|
365
|
+
const byAgent = manifest.packIndex.filter((entry) => normalizedAgent ? entry.agents.includes(normalizedAgent) : true);
|
|
366
|
+
if (tokens.length === 0) {
|
|
367
|
+
return byAgent.map((entry) => entry.fileName);
|
|
368
|
+
}
|
|
369
|
+
let hasInvalidBloomIndex = false;
|
|
370
|
+
const byToken = byAgent.filter((entry) => {
|
|
371
|
+
const decoded = bloomFromBase64(entry.tokenBloomB64);
|
|
372
|
+
if (!decoded.valid) {
|
|
373
|
+
hasInvalidBloomIndex = true;
|
|
374
|
+
return true;
|
|
165
375
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
? 'chunks.content'
|
|
172
|
-
: documentColumns.has('content')
|
|
173
|
-
? 'documents.content'
|
|
174
|
-
: "''";
|
|
175
|
-
const chunkOrderExpr = chunkColumns.has('ordinal') ? 'chunks.ordinal' : 'chunks.rowid';
|
|
176
|
-
const statement = database.prepare(`
|
|
177
|
-
SELECT
|
|
178
|
-
documents.id AS document_id,
|
|
179
|
-
${agentExpr} AS agent_id,
|
|
180
|
-
documents.title AS title,
|
|
181
|
-
${pathExpr} AS path,
|
|
182
|
-
${chunkIdExpr} AS chunk_id,
|
|
183
|
-
${chunkContentExpr} AS content,
|
|
184
|
-
${tagsExpr} AS tags_json
|
|
185
|
-
FROM chunks
|
|
186
|
-
JOIN documents ON documents.id = chunks.document_id
|
|
187
|
-
ORDER BY documents.title, ${chunkOrderExpr}
|
|
188
|
-
`);
|
|
189
|
-
const rows = statement.all();
|
|
190
|
-
return rows.map((row) => ({
|
|
191
|
-
documentId: row.document_id,
|
|
192
|
-
agentId: typeof row.agent_id === 'string' && row.agent_id.length > 0 ? row.agent_id : 'shared',
|
|
193
|
-
title: row.title,
|
|
194
|
-
path: row.path,
|
|
195
|
-
chunkId: row.chunk_id,
|
|
196
|
-
content: row.content ?? '',
|
|
197
|
-
tags: parseTags(row.tags_json)
|
|
198
|
-
}));
|
|
376
|
+
return tokens.some((token) => bloomMayContain(decoded.bloom, token));
|
|
377
|
+
});
|
|
378
|
+
// Lossless guarantee: if compressed metadata is partially invalid, do not prune packs.
|
|
379
|
+
if (hasInvalidBloomIndex) {
|
|
380
|
+
return byAgent.map((entry) => entry.fileName);
|
|
199
381
|
}
|
|
200
|
-
|
|
201
|
-
|
|
382
|
+
if (byToken.length > 0) {
|
|
383
|
+
return byToken.map((entry) => entry.fileName);
|
|
202
384
|
}
|
|
385
|
+
return byAgent.length > 0 ? byAgent.map((entry) => entry.fileName) : allFiles;
|
|
203
386
|
};
|
|
204
|
-
export const buildSearchPacks = async (vaultPath, documents) => {
|
|
205
|
-
|
|
387
|
+
export const buildSearchPacks = async (vaultPath, documents, options) => {
|
|
388
|
+
const resolvedOptions = {
|
|
389
|
+
rowChunkSize: options?.rowChunkSize ?? defaultBuildOptions.rowChunkSize,
|
|
390
|
+
compressionLevel: options?.compressionLevel ?? defaultBuildOptions.compressionLevel,
|
|
391
|
+
useDictionary: options?.useDictionary ?? defaultBuildOptions.useDictionary
|
|
392
|
+
};
|
|
393
|
+
return writeRowsAsPrivatePacks(vaultPath, toRows(documents), true, resolvedOptions);
|
|
206
394
|
};
|
|
207
395
|
export const ensurePrivatePacksFromLegacyIndex = async (vaultPath) => {
|
|
208
396
|
const files = await sortedPackFiles(vaultPath);
|
|
@@ -216,38 +404,39 @@ export const ensurePrivatePacksFromLegacyIndex = async (vaultPath) => {
|
|
|
216
404
|
const parsed = await parseRowsFromPack(vaultPath, await readFile(join(toPackDirectory(vaultPath), file)));
|
|
217
405
|
rows.push(...parsed);
|
|
218
406
|
}
|
|
219
|
-
const report = await writeRowsAsPrivatePacks(vaultPath, rows, true);
|
|
407
|
+
const report = await writeRowsAsPrivatePacks(vaultPath, rows, true, defaultBuildOptions);
|
|
220
408
|
return {
|
|
221
409
|
imported: true,
|
|
222
410
|
source: 'legacy-packs',
|
|
223
411
|
...report
|
|
224
412
|
};
|
|
225
413
|
}
|
|
226
|
-
|
|
227
|
-
if (legacyRows.length === 0) {
|
|
228
|
-
return { imported: false };
|
|
229
|
-
}
|
|
230
|
-
const report = await writeRowsAsPrivatePacks(vaultPath, legacyRows, true);
|
|
231
|
-
return {
|
|
232
|
-
imported: true,
|
|
233
|
-
source: 'legacy-sqlite',
|
|
234
|
-
...report
|
|
235
|
-
};
|
|
414
|
+
return { imported: false };
|
|
236
415
|
};
|
|
416
|
+
export const toSearchPackBuildOptions = (config) => ({
|
|
417
|
+
rowChunkSize: config.searchPack.rowChunkSize,
|
|
418
|
+
compressionLevel: config.searchPack.compressionLevel,
|
|
419
|
+
useDictionary: config.searchPack.useDictionary
|
|
420
|
+
});
|
|
237
421
|
export const searchInPacks = async (vaultPath, query, limit, agentId) => {
|
|
238
422
|
const normalizedAgent = agentId?.trim();
|
|
239
423
|
const tokens = tokenize(query);
|
|
240
424
|
if (limit <= 0 || tokens.length === 0) {
|
|
241
425
|
return [];
|
|
242
426
|
}
|
|
243
|
-
const files = await
|
|
427
|
+
const files = await selectCandidatePackFiles(vaultPath, tokens, normalizedAgent);
|
|
244
428
|
if (files.length === 0) {
|
|
245
429
|
return [];
|
|
246
430
|
}
|
|
247
431
|
const scored = [];
|
|
248
432
|
for (const file of files) {
|
|
249
433
|
const rows = await parseRowsFromPack(vaultPath, await readFile(join(toPackDirectory(vaultPath), file)));
|
|
250
|
-
rows.
|
|
434
|
+
const traversal = middleOutIndices(rows.length, Math.floor(rows.length / 2));
|
|
435
|
+
traversal.forEach((rowIndex) => {
|
|
436
|
+
const row = rows[rowIndex];
|
|
437
|
+
if (!row) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
251
440
|
if (normalizedAgent && row.agentId !== normalizedAgent) {
|
|
252
441
|
return;
|
|
253
442
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
export const volatileMemoryStoragePath = (vaultPath) => join(vaultPath, '.brainlink', 'volatile.json');
|
|
4
|
+
const emptyStore = () => ({
|
|
5
|
+
version: 1,
|
|
6
|
+
entries: []
|
|
7
|
+
});
|
|
8
|
+
const normalizeToken = (value) => value
|
|
9
|
+
.normalize('NFKD')
|
|
10
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
11
|
+
.toLowerCase();
|
|
12
|
+
const tokens = (value) => value
|
|
13
|
+
.match(/[\p{L}\p{N}_-]+/gu)
|
|
14
|
+
?.map(normalizeToken)
|
|
15
|
+
.filter((token) => token.length > 1) ?? [];
|
|
16
|
+
const readStore = async (vaultPath) => {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(await readFile(volatileMemoryStoragePath(vaultPath), 'utf8'));
|
|
19
|
+
return {
|
|
20
|
+
version: 1,
|
|
21
|
+
entries: Array.isArray(parsed.entries) ? parsed.entries.filter(isEntry) : []
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return emptyStore();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const writeStore = async (vaultPath, store) => {
|
|
29
|
+
const target = volatileMemoryStoragePath(vaultPath);
|
|
30
|
+
const temp = `${target}.tmp`;
|
|
31
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
32
|
+
await writeFile(temp, `${JSON.stringify(store, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
33
|
+
await rename(temp, target);
|
|
34
|
+
};
|
|
35
|
+
const isEntry = (value) => {
|
|
36
|
+
const record = value;
|
|
37
|
+
return Boolean(record &&
|
|
38
|
+
typeof record.id === 'string' &&
|
|
39
|
+
typeof record.agentId === 'string' &&
|
|
40
|
+
typeof record.content === 'string' &&
|
|
41
|
+
typeof record.createdAt === 'string' &&
|
|
42
|
+
typeof record.expiresAt === 'string' &&
|
|
43
|
+
Array.isArray(record.tags));
|
|
44
|
+
};
|
|
45
|
+
const activeEntries = (entries, now = Date.now()) => entries.filter((entry) => Date.parse(entry.expiresAt) > now);
|
|
46
|
+
const scoreEntry = (entry, query) => {
|
|
47
|
+
const queryTokens = tokens(query);
|
|
48
|
+
if (queryTokens.length === 0) {
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
const haystack = normalizeToken([entry.content, entry.tags.join(' ')].join(' '));
|
|
52
|
+
return queryTokens.reduce((score, token) => score + (haystack.includes(token) ? 1 : 0), 0);
|
|
53
|
+
};
|
|
54
|
+
export const addVolatileMemory = async (vaultPath, content, agentId = 'shared', ttlMinutes = 240, tags = []) => {
|
|
55
|
+
const now = new Date();
|
|
56
|
+
const entry = {
|
|
57
|
+
id: `volatile-${now.getTime()}-${Math.random().toString(36).slice(2, 10)}`,
|
|
58
|
+
agentId,
|
|
59
|
+
content,
|
|
60
|
+
createdAt: now.toISOString(),
|
|
61
|
+
expiresAt: new Date(now.getTime() + Math.max(1, ttlMinutes) * 60_000).toISOString(),
|
|
62
|
+
source: 'agent',
|
|
63
|
+
tags
|
|
64
|
+
};
|
|
65
|
+
const store = await readStore(vaultPath);
|
|
66
|
+
await writeStore(vaultPath, {
|
|
67
|
+
version: 1,
|
|
68
|
+
entries: [...activeEntries(store.entries), entry]
|
|
69
|
+
});
|
|
70
|
+
return entry;
|
|
71
|
+
};
|
|
72
|
+
export const searchVolatileMemory = async (vaultPath, query, limit, agentId, mode = 'hybrid') => {
|
|
73
|
+
const store = await readStore(vaultPath);
|
|
74
|
+
const entries = activeEntries(store.entries)
|
|
75
|
+
.filter((entry) => (agentId ? entry.agentId === agentId : true))
|
|
76
|
+
.map((entry) => ({ entry, score: scoreEntry(entry, query) }))
|
|
77
|
+
.filter(({ score }) => score > 0)
|
|
78
|
+
.sort((left, right) => right.score - left.score || right.entry.createdAt.localeCompare(left.entry.createdAt))
|
|
79
|
+
.slice(0, Math.max(0, limit));
|
|
80
|
+
if (entries.length !== store.entries.length) {
|
|
81
|
+
await writeStore(vaultPath, { version: 1, entries: activeEntries(store.entries) });
|
|
82
|
+
}
|
|
83
|
+
return entries.map(({ entry, score }) => ({
|
|
84
|
+
title: 'Volatile Memory',
|
|
85
|
+
path: `volatile://${entry.id}`,
|
|
86
|
+
content: entry.content,
|
|
87
|
+
score,
|
|
88
|
+
searchMode: mode,
|
|
89
|
+
tags: ['volatile', ...entry.tags],
|
|
90
|
+
volatile: true,
|
|
91
|
+
expiresAt: entry.expiresAt
|
|
92
|
+
}));
|
|
93
|
+
};
|
|
94
|
+
export const clearVolatileMemory = async (vaultPath, agentId) => {
|
|
95
|
+
const store = await readStore(vaultPath);
|
|
96
|
+
const active = activeEntries(store.entries);
|
|
97
|
+
const kept = agentId ? active.filter((entry) => entry.agentId !== agentId) : [];
|
|
98
|
+
await writeStore(vaultPath, { version: 1, entries: kept });
|
|
99
|
+
return active.length - kept.length;
|
|
100
|
+
};
|
package/dist/mcp/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, contextInputSchema, contextTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool } from './tools.js';
|
|
5
|
+
import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, contextInputSchema, contextTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool } from './tools.js';
|
|
6
6
|
const readPackageVersion = () => {
|
|
7
7
|
const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
|
|
8
8
|
const metadata = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
@@ -40,11 +40,31 @@ export const createBrainlinkMcpServer = () => {
|
|
|
40
40
|
description: 'Search indexed Brainlink notes with FTS, semantic or hybrid retrieval.',
|
|
41
41
|
inputSchema: searchInputSchema
|
|
42
42
|
}, searchTool);
|
|
43
|
+
server.registerTool('brainlink_dedupe', {
|
|
44
|
+
title: 'Detect Duplicate Notes',
|
|
45
|
+
description: 'Detect possible duplicate notes using exact content hash and semantic similarity scoring.',
|
|
46
|
+
inputSchema: dedupeInputSchema
|
|
47
|
+
}, dedupeTool);
|
|
48
|
+
server.registerTool('brainlink_resolve_duplicate', {
|
|
49
|
+
title: 'Resolve Duplicate Notes',
|
|
50
|
+
description: 'Resolve a duplicate pair with merge, link or ignore. Non-merge actions still create low-priority related edges.',
|
|
51
|
+
inputSchema: dedupeResolveInputSchema
|
|
52
|
+
}, dedupeResolveTool);
|
|
43
53
|
server.registerTool('brainlink_add_note', {
|
|
44
54
|
title: 'Add Brainlink Note',
|
|
45
55
|
description: 'Write durable Markdown memory, then reindex the vault. Include explicit [[wiki links]] for connected graph memory. Add priority markers near links, such as priority: high, #important or #critical, when a relationship should be weighted higher.',
|
|
46
56
|
inputSchema: addNoteInputSchema
|
|
47
57
|
}, addNoteTool);
|
|
58
|
+
server.registerTool('brainlink_volatile_add', {
|
|
59
|
+
title: 'Add Volatile Brainlink Memory',
|
|
60
|
+
description: 'Write temporary agent-decided memory with TTL. Use for transient task state without polluting durable Markdown memory.',
|
|
61
|
+
inputSchema: volatileAddInputSchema
|
|
62
|
+
}, volatileAddTool);
|
|
63
|
+
server.registerTool('brainlink_volatile_clear', {
|
|
64
|
+
title: 'Clear Volatile Brainlink Memory',
|
|
65
|
+
description: 'Clear active volatile memory for the current vault/agent namespace.',
|
|
66
|
+
inputSchema: volatileClearInputSchema
|
|
67
|
+
}, volatileClearTool);
|
|
48
68
|
server.registerTool('brainlink_add_file', {
|
|
49
69
|
title: 'Ingest Markdown File',
|
|
50
70
|
description: 'Read a local markdown/text file and ingest it as a Brainlink note. Reindex defaults to true.',
|