@andespindola/brainlink 0.1.0-beta.16 → 0.1.0-beta.161
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 +9 -6
- package/CHANGELOG.md +27 -0
- package/COPYRIGHT.md +5 -0
- package/README.md +177 -20
- package/dist/application/add-note.js +13 -44
- package/dist/application/auto-migrate-configured-vault.js +37 -0
- package/dist/application/build-context.js +64 -3
- package/dist/application/canonical-context-links.js +209 -0
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +258 -51
- package/dist/application/frontend/client-html.js +50 -27
- package/dist/application/frontend/client-js.js +1369 -605
- package/dist/application/frontend/client-render-worker-js.js +645 -0
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-contexts.js +33 -0
- package/dist/application/get-graph-layout.js +62 -8
- package/dist/application/get-graph-stream-chunk.js +326 -0
- package/dist/application/get-graph-view.js +246 -0
- package/dist/application/graph-view-state.js +66 -0
- package/dist/application/import-legacy-sqlite.js +266 -0
- package/dist/application/index-vault.js +262 -23
- package/dist/application/migrate-context-links.js +79 -0
- package/dist/application/offline-pack-backup.js +44 -0
- package/dist/application/search-graph-node-ids.js +63 -3
- package/dist/application/server/routes.js +247 -7
- package/dist/application/start-server.js +75 -4
- package/dist/application/watch-vault.js +23 -2
- package/dist/cli/commands/agent-commands.js +7 -0
- package/dist/cli/commands/write-commands.js +924 -14
- package/dist/cli/runtime.js +10 -2
- package/dist/domain/context.js +54 -11
- package/dist/domain/graph-contexts.js +180 -0
- package/dist/domain/graph-layout.js +389 -18
- package/dist/domain/markdown.js +53 -9
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +121 -4
- package/dist/infrastructure/file-index.js +76 -6
- 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 +286 -15
- package/dist/infrastructure/vault-migration-state.js +69 -0
- package/dist/infrastructure/volatile-memory.js +100 -0
- package/dist/mcp/runtime.js +20 -0
- package/dist/mcp/server.js +39 -11
- package/dist/mcp/tools.js +183 -7
- package/docs/AGENT_USAGE.md +96 -5
- package/docs/ARCHITECTURE.md +8 -0
- package/docs/QUICKSTART.md +7 -0
- package/package.json +7 -2
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { gunzipSync } from 'node:zlib';
|
|
2
2
|
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { middleOutIndices } from '../domain/middle-out.js';
|
|
4
5
|
import { decodePrivatePack, encodePrivatePack, isPrivatePackPayload } from './private-pack-codec.js';
|
|
5
6
|
const packsDirectoryName = 'search-packs';
|
|
6
7
|
const manifestFileName = 'manifest.json';
|
|
7
|
-
const
|
|
8
|
+
const defaultBuildOptions = {
|
|
9
|
+
rowChunkSize: 5_000,
|
|
10
|
+
compressionLevel: 5,
|
|
11
|
+
useDictionary: true
|
|
12
|
+
};
|
|
8
13
|
const queryTokenPattern = /[\p{L}\p{N}_-]+/gu;
|
|
14
|
+
const bloomBytes = 256;
|
|
15
|
+
const bloomBitSize = bloomBytes * 8;
|
|
16
|
+
const bloomSeeds = [0x9e3779b1, 0x85ebca6b, 0xc2b2ae35];
|
|
9
17
|
const toPackDirectory = (vaultPath) => join(vaultPath, '.brainlink', packsDirectoryName);
|
|
10
18
|
const toManifestPath = (vaultPath) => join(toPackDirectory(vaultPath), manifestFileName);
|
|
11
19
|
const parseRowsFromPack = async (vaultPath, content) => {
|
|
@@ -15,7 +23,29 @@ const parseRowsFromPack = async (vaultPath, content) => {
|
|
|
15
23
|
.split('\n')
|
|
16
24
|
.map((line) => line.trim())
|
|
17
25
|
.filter((line) => line.length > 0)
|
|
18
|
-
.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
|
+
});
|
|
19
49
|
};
|
|
20
50
|
const toRows = (documents) => documents.flatMap((document) => document.chunks.map((chunk) => ({
|
|
21
51
|
documentId: document.document.id,
|
|
@@ -23,12 +53,121 @@ const toRows = (documents) => documents.flatMap((document) => document.chunks.ma
|
|
|
23
53
|
title: document.document.title,
|
|
24
54
|
path: document.document.path,
|
|
25
55
|
chunkId: chunk.id,
|
|
56
|
+
chunkOrdinal: chunk.ordinal,
|
|
26
57
|
content: chunk.content,
|
|
27
58
|
tags: document.document.tags
|
|
28
59
|
})));
|
|
29
60
|
const writeManifest = async (vaultPath, manifest) => {
|
|
30
61
|
await writeFile(toManifestPath(vaultPath), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
31
62
|
};
|
|
63
|
+
const readManifest = async (vaultPath) => {
|
|
64
|
+
try {
|
|
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;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
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
|
+
};
|
|
32
171
|
const chunkRows = (rows, size) => {
|
|
33
172
|
const chunks = [];
|
|
34
173
|
for (let index = 0; index < rows.length; index += size) {
|
|
@@ -57,6 +196,51 @@ const countOccurrences = (text, token) => {
|
|
|
57
196
|
}
|
|
58
197
|
return hits;
|
|
59
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
|
+
};
|
|
60
244
|
const computeTextScore = (row, tokens) => {
|
|
61
245
|
if (tokens.length === 0) {
|
|
62
246
|
return 0;
|
|
@@ -79,6 +263,7 @@ const toSearchResult = (row, score) => ({
|
|
|
79
263
|
title: row.title,
|
|
80
264
|
path: row.path,
|
|
81
265
|
chunkId: row.chunkId,
|
|
266
|
+
chunkOrdinal: row.chunkOrdinal,
|
|
82
267
|
content: row.content,
|
|
83
268
|
score,
|
|
84
269
|
textScore: score,
|
|
@@ -100,7 +285,8 @@ const sortedPackFiles = async (vaultPath) => {
|
|
|
100
285
|
throw error;
|
|
101
286
|
}
|
|
102
287
|
};
|
|
103
|
-
const writeRowsAsPrivatePacks = async (vaultPath, rows, clearExisting) => {
|
|
288
|
+
const writeRowsAsPrivatePacks = async (vaultPath, rows, clearExisting, options) => {
|
|
289
|
+
const startedAt = process.hrtime.bigint();
|
|
104
290
|
const directory = toPackDirectory(vaultPath);
|
|
105
291
|
await mkdir(directory, { recursive: true });
|
|
106
292
|
if (clearExisting) {
|
|
@@ -109,27 +295,102 @@ const writeRowsAsPrivatePacks = async (vaultPath, rows, clearExisting) => {
|
|
|
109
295
|
.filter((name) => name.endsWith('.blpk') || name.endsWith('.jsonl.gz') || name === manifestFileName)
|
|
110
296
|
.map((name) => rm(join(directory, name), { force: true })));
|
|
111
297
|
}
|
|
112
|
-
const chunks = chunkRows(rows, rowChunkSize);
|
|
113
|
-
|
|
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];
|
|
114
304
|
const fileName = `pack-${String(index + 1).padStart(4, '0')}.blpk`;
|
|
115
305
|
const serialized = `${chunk.map((row) => JSON.stringify(row)).join('\n')}\n`;
|
|
116
|
-
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));
|
|
117
311
|
await writeFile(join(directory, fileName), compressed);
|
|
118
|
-
|
|
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
|
+
}
|
|
119
321
|
await writeManifest(vaultPath, {
|
|
120
|
-
version:
|
|
322
|
+
version: 3,
|
|
121
323
|
createdAt: new Date().toISOString(),
|
|
122
324
|
packCount: chunks.length,
|
|
123
325
|
recordCount: rows.length,
|
|
124
|
-
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
|
+
}
|
|
125
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);
|
|
126
343
|
return {
|
|
127
344
|
packCount: chunks.length,
|
|
128
|
-
recordCount: rows.length
|
|
345
|
+
recordCount: rows.length,
|
|
346
|
+
compression: {
|
|
347
|
+
inputBytes,
|
|
348
|
+
outputBytes,
|
|
349
|
+
ratio: outputBytes / safeInput,
|
|
350
|
+
savedBytes
|
|
351
|
+
},
|
|
352
|
+
durationMs
|
|
129
353
|
};
|
|
130
354
|
};
|
|
131
|
-
|
|
132
|
-
|
|
355
|
+
const selectCandidatePackFiles = async (vaultPath, tokens, agentId) => {
|
|
356
|
+
const allFiles = await sortedPackFiles(vaultPath);
|
|
357
|
+
if (allFiles.length === 0) {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
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;
|
|
375
|
+
}
|
|
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);
|
|
381
|
+
}
|
|
382
|
+
if (byToken.length > 0) {
|
|
383
|
+
return byToken.map((entry) => entry.fileName);
|
|
384
|
+
}
|
|
385
|
+
return byAgent.length > 0 ? byAgent.map((entry) => entry.fileName) : allFiles;
|
|
386
|
+
};
|
|
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);
|
|
133
394
|
};
|
|
134
395
|
export const ensurePrivatePacksFromLegacyIndex = async (vaultPath) => {
|
|
135
396
|
const files = await sortedPackFiles(vaultPath);
|
|
@@ -143,7 +404,7 @@ export const ensurePrivatePacksFromLegacyIndex = async (vaultPath) => {
|
|
|
143
404
|
const parsed = await parseRowsFromPack(vaultPath, await readFile(join(toPackDirectory(vaultPath), file)));
|
|
144
405
|
rows.push(...parsed);
|
|
145
406
|
}
|
|
146
|
-
const report = await writeRowsAsPrivatePacks(vaultPath, rows, true);
|
|
407
|
+
const report = await writeRowsAsPrivatePacks(vaultPath, rows, true, defaultBuildOptions);
|
|
147
408
|
return {
|
|
148
409
|
imported: true,
|
|
149
410
|
source: 'legacy-packs',
|
|
@@ -152,20 +413,30 @@ export const ensurePrivatePacksFromLegacyIndex = async (vaultPath) => {
|
|
|
152
413
|
}
|
|
153
414
|
return { imported: false };
|
|
154
415
|
};
|
|
416
|
+
export const toSearchPackBuildOptions = (config) => ({
|
|
417
|
+
rowChunkSize: config.searchPack.rowChunkSize,
|
|
418
|
+
compressionLevel: config.searchPack.compressionLevel,
|
|
419
|
+
useDictionary: config.searchPack.useDictionary
|
|
420
|
+
});
|
|
155
421
|
export const searchInPacks = async (vaultPath, query, limit, agentId) => {
|
|
156
422
|
const normalizedAgent = agentId?.trim();
|
|
157
423
|
const tokens = tokenize(query);
|
|
158
424
|
if (limit <= 0 || tokens.length === 0) {
|
|
159
425
|
return [];
|
|
160
426
|
}
|
|
161
|
-
const files = await
|
|
427
|
+
const files = await selectCandidatePackFiles(vaultPath, tokens, normalizedAgent);
|
|
162
428
|
if (files.length === 0) {
|
|
163
429
|
return [];
|
|
164
430
|
}
|
|
165
431
|
const scored = [];
|
|
166
432
|
for (const file of files) {
|
|
167
433
|
const rows = await parseRowsFromPack(vaultPath, await readFile(join(toPackDirectory(vaultPath), file)));
|
|
168
|
-
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
|
+
}
|
|
169
440
|
if (normalizedAgent && row.agentId !== normalizedAgent) {
|
|
170
441
|
return;
|
|
171
442
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { getBrainlinkHomePath } from './paths.js';
|
|
4
|
+
const defaultState = {
|
|
5
|
+
byConfigKey: {}
|
|
6
|
+
};
|
|
7
|
+
const statePath = () => join(getBrainlinkHomePath(), 'vault-migration-state.json');
|
|
8
|
+
const sanitizeState = (value) => {
|
|
9
|
+
if (typeof value !== 'object' || value === null) {
|
|
10
|
+
return defaultState;
|
|
11
|
+
}
|
|
12
|
+
const record = value;
|
|
13
|
+
const byConfigKeyRecord = typeof record.byConfigKey === 'object' && record.byConfigKey !== null ? record.byConfigKey : {};
|
|
14
|
+
const byConfigKey = Object.entries(byConfigKeyRecord).reduce((state, [key, vault]) => {
|
|
15
|
+
if (typeof key !== 'string' || key.trim().length === 0) {
|
|
16
|
+
return state;
|
|
17
|
+
}
|
|
18
|
+
if (typeof vault !== 'string' || vault.trim().length === 0) {
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
...state,
|
|
23
|
+
[key]: vault.trim()
|
|
24
|
+
};
|
|
25
|
+
}, {});
|
|
26
|
+
return {
|
|
27
|
+
byConfigKey
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const readState = async () => {
|
|
31
|
+
try {
|
|
32
|
+
const raw = await readFile(statePath(), 'utf8');
|
|
33
|
+
return sanitizeState(JSON.parse(raw));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
37
|
+
return defaultState;
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const writeState = async (state) => {
|
|
43
|
+
const path = statePath();
|
|
44
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
45
|
+
await writeFile(path, `${JSON.stringify(state, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
46
|
+
};
|
|
47
|
+
export const getVaultMigrationStatePath = () => statePath();
|
|
48
|
+
export const getLastConfiguredVaultForKey = async (configKey) => {
|
|
49
|
+
const state = await readState();
|
|
50
|
+
const value = state.byConfigKey[configKey];
|
|
51
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
52
|
+
};
|
|
53
|
+
export const setLastConfiguredVaultForKey = async (configKey, vault) => {
|
|
54
|
+
const key = configKey.trim();
|
|
55
|
+
const value = vault.trim();
|
|
56
|
+
if (key.length === 0 || value.length === 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const state = await readState();
|
|
60
|
+
if (state.byConfigKey[key] === value) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await writeState({
|
|
64
|
+
byConfigKey: {
|
|
65
|
+
...state.byConfigKey,
|
|
66
|
+
[key]: value
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
let cachedRuntimeMetadata = null;
|
|
5
|
+
const readPackageMetadata = () => {
|
|
6
|
+
const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
|
|
7
|
+
return JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
8
|
+
};
|
|
9
|
+
export const getRuntimeMetadata = () => {
|
|
10
|
+
if (cachedRuntimeMetadata) {
|
|
11
|
+
return cachedRuntimeMetadata;
|
|
12
|
+
}
|
|
13
|
+
const metadata = readPackageMetadata();
|
|
14
|
+
cachedRuntimeMetadata = {
|
|
15
|
+
name: metadata.name ?? 'brainlink',
|
|
16
|
+
version: metadata.version ?? '0.0.0'
|
|
17
|
+
};
|
|
18
|
+
return cachedRuntimeMetadata;
|
|
19
|
+
};
|
|
20
|
+
export const getRuntimeVersion = () => getRuntimeMetadata().version;
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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';
|
|
6
|
-
const readPackageVersion = () => {
|
|
7
|
-
const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
|
|
8
|
-
const metadata = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
9
|
-
return metadata.version ?? '0.0.0';
|
|
10
|
-
};
|
|
2
|
+
import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, contextInputSchema, contextTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool, versionInputSchema, versionTool } from './tools.js';
|
|
3
|
+
import { getRuntimeVersion } from './runtime.js';
|
|
11
4
|
export const createBrainlinkMcpServer = () => {
|
|
12
5
|
const server = new McpServer({
|
|
13
6
|
name: 'brainlink',
|
|
14
7
|
title: 'Brainlink',
|
|
15
|
-
version:
|
|
8
|
+
version: getRuntimeVersion(),
|
|
16
9
|
description: 'Local-first Markdown memory tools for AI agents.'
|
|
17
10
|
});
|
|
18
11
|
server.registerTool('brainlink_bootstrap', {
|
|
@@ -25,6 +18,11 @@ export const createBrainlinkMcpServer = () => {
|
|
|
25
18
|
description: 'Read or update bootstrap enforcement policy and inspect bootstrap readiness for the current vault/agent.',
|
|
26
19
|
inputSchema: policyInputSchema
|
|
27
20
|
}, policyTool);
|
|
21
|
+
server.registerTool('brainlink_version', {
|
|
22
|
+
title: 'Read Brainlink Runtime Version',
|
|
23
|
+
description: 'Return the current Brainlink MCP runtime package version and metadata.',
|
|
24
|
+
inputSchema: versionInputSchema
|
|
25
|
+
}, versionTool);
|
|
28
26
|
server.registerTool('brainlink_recommendations', {
|
|
29
27
|
title: 'Brainlink Recommended MCP Workflow',
|
|
30
28
|
description: 'Return a plug-and-play action plan for this vault/agent, including policy, bootstrap, context retrieval and durable write guidance.',
|
|
@@ -40,19 +38,44 @@ export const createBrainlinkMcpServer = () => {
|
|
|
40
38
|
description: 'Search indexed Brainlink notes with FTS, semantic or hybrid retrieval.',
|
|
41
39
|
inputSchema: searchInputSchema
|
|
42
40
|
}, searchTool);
|
|
41
|
+
server.registerTool('brainlink_dedupe', {
|
|
42
|
+
title: 'Detect Duplicate Notes',
|
|
43
|
+
description: 'Detect possible duplicate notes using exact content hash and semantic similarity scoring.',
|
|
44
|
+
inputSchema: dedupeInputSchema
|
|
45
|
+
}, dedupeTool);
|
|
46
|
+
server.registerTool('brainlink_resolve_duplicate', {
|
|
47
|
+
title: 'Resolve Duplicate Notes',
|
|
48
|
+
description: 'Resolve a duplicate pair with merge, link or ignore. Non-merge actions still create low-priority related edges.',
|
|
49
|
+
inputSchema: dedupeResolveInputSchema
|
|
50
|
+
}, dedupeResolveTool);
|
|
43
51
|
server.registerTool('brainlink_add_note', {
|
|
44
52
|
title: 'Add Brainlink Note',
|
|
45
53
|
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
54
|
inputSchema: addNoteInputSchema
|
|
47
55
|
}, addNoteTool);
|
|
56
|
+
server.registerTool('brainlink_volatile_add', {
|
|
57
|
+
title: 'Add Volatile Brainlink Memory',
|
|
58
|
+
description: 'Write temporary agent-decided memory with TTL. Use for transient task state without polluting durable Markdown memory.',
|
|
59
|
+
inputSchema: volatileAddInputSchema
|
|
60
|
+
}, volatileAddTool);
|
|
61
|
+
server.registerTool('brainlink_volatile_clear', {
|
|
62
|
+
title: 'Clear Volatile Brainlink Memory',
|
|
63
|
+
description: 'Clear active volatile memory for the current vault/agent namespace.',
|
|
64
|
+
inputSchema: volatileClearInputSchema
|
|
65
|
+
}, volatileClearTool);
|
|
48
66
|
server.registerTool('brainlink_add_file', {
|
|
49
67
|
title: 'Ingest Markdown File',
|
|
50
68
|
description: 'Read a local markdown/text file and ingest it as a Brainlink note. Reindex defaults to true.',
|
|
51
69
|
inputSchema: addFileInputSchema
|
|
52
70
|
}, addFileTool);
|
|
71
|
+
server.registerTool('brainlink_canonicalize_context_links', {
|
|
72
|
+
title: 'Canonicalize Brainlink Context Links',
|
|
73
|
+
description: 'Ensure notes have canonical Context Links to inferred context hubs. Supports dry-run and can create missing hub notes.',
|
|
74
|
+
inputSchema: canonicalizeContextLinksInputSchema
|
|
75
|
+
}, canonicalizeContextLinksTool);
|
|
53
76
|
server.registerTool('brainlink_index', {
|
|
54
77
|
title: 'Index Brainlink Vault',
|
|
55
|
-
description: 'Rebuild the local Brainlink index from Markdown notes.',
|
|
78
|
+
description: 'Rebuild the local Brainlink index from Markdown notes. Pass full=true to force a complete source rebuild.',
|
|
56
79
|
inputSchema: indexInputSchema
|
|
57
80
|
}, indexTool);
|
|
58
81
|
server.registerTool('brainlink_stats', {
|
|
@@ -75,6 +98,11 @@ export const createBrainlinkMcpServer = () => {
|
|
|
75
98
|
description: 'Read indexed graph nodes and wiki-link edges. Edges include weight and priority fields so agents can rank importance and priority.',
|
|
76
99
|
inputSchema: graphInputSchema
|
|
77
100
|
}, graphTool);
|
|
101
|
+
server.registerTool('brainlink_graph_contexts', {
|
|
102
|
+
title: 'List Brainlink Graph Contexts',
|
|
103
|
+
description: 'List visual graph contexts used by the Brainlink server to separate memory domains such as preferences, repositories and machine configuration.',
|
|
104
|
+
inputSchema: graphContextsInputSchema
|
|
105
|
+
}, graphContextsTool);
|
|
78
106
|
server.registerTool('brainlink_broken_links', {
|
|
79
107
|
title: 'List Brainlink Broken Links',
|
|
80
108
|
description: 'List unresolved indexed wiki links.',
|