@andespindola/brainlink 0.1.0-beta.15 → 0.1.0-beta.151
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 +3 -0
- package/CHANGELOG.md +27 -0
- package/COPYRIGHT.md +5 -0
- package/README.md +140 -9
- package/dist/application/auto-migrate-configured-vault.js +37 -0
- package/dist/application/build-context.js +64 -3
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +111 -47
- package/dist/application/frontend/client-html.js +42 -26
- package/dist/application/frontend/client-js.js +788 -554
- package/dist/application/frontend/client-render-worker-js.js +569 -0
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-layout.js +38 -5
- package/dist/application/get-graph-stream-chunk.js +289 -0
- package/dist/application/get-graph-view.js +243 -0
- package/dist/application/import-legacy-sqlite.js +296 -0
- package/dist/application/index-vault.js +262 -23
- package/dist/application/offline-pack-backup.js +44 -0
- package/dist/application/server/routes.js +187 -5
- 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 +849 -10
- package/dist/cli/runtime.js +10 -2
- package/dist/domain/context.js +54 -11
- package/dist/domain/graph-layout.js +275 -3
- package/dist/domain/markdown.js +22 -9
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +117 -4
- package/dist/infrastructure/file-index.js +70 -3
- 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 +29 -11
- package/dist/mcp/tools.js +119 -2
- package/docs/AGENT_USAGE.md +89 -3
- package/docs/ARCHITECTURE.md +6 -0
- package/docs/QUICKSTART.md +7 -0
- package/package.json +7 -2
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { cosineSimilarity } from '../domain/embeddings.js';
|
|
4
4
|
const queryTokenPattern = /[\p{L}\p{N}_-]+/gu;
|
|
5
|
+
const indexCacheMaxEntries = 16;
|
|
6
|
+
const indexCache = new Map();
|
|
5
7
|
const emptyIndex = () => ({
|
|
6
8
|
version: 1,
|
|
7
9
|
updatedAt: new Date().toISOString(),
|
|
@@ -11,18 +13,44 @@ const emptyIndex = () => ({
|
|
|
11
13
|
});
|
|
12
14
|
export const indexStoragePath = (vaultPath) => join(vaultPath, '.brainlink', 'index.json');
|
|
13
15
|
const readIndex = async (vaultPath) => {
|
|
16
|
+
const path = indexStoragePath(vaultPath);
|
|
17
|
+
let stats = null;
|
|
14
18
|
try {
|
|
15
|
-
const
|
|
16
|
-
|
|
19
|
+
const fileStats = await stat(path);
|
|
20
|
+
stats = { mtimeMs: fileStats.mtimeMs, size: fileStats.size };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
24
|
+
indexCache.delete(path);
|
|
25
|
+
return emptyIndex();
|
|
26
|
+
}
|
|
27
|
+
return emptyIndex();
|
|
28
|
+
}
|
|
29
|
+
const cached = indexCache.get(path);
|
|
30
|
+
if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
31
|
+
return cached.index;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
35
|
+
const loaded = {
|
|
17
36
|
version: 1,
|
|
18
37
|
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
|
|
19
38
|
documents: Array.isArray(parsed.documents) ? parsed.documents : [],
|
|
20
39
|
chunks: Array.isArray(parsed.chunks) ? parsed.chunks : [],
|
|
21
40
|
links: Array.isArray(parsed.links) ? parsed.links : []
|
|
22
41
|
};
|
|
42
|
+
indexCache.set(path, { ...stats, index: loaded });
|
|
43
|
+
if (indexCache.size > indexCacheMaxEntries) {
|
|
44
|
+
const oldest = indexCache.keys().next().value;
|
|
45
|
+
if (typeof oldest === 'string') {
|
|
46
|
+
indexCache.delete(oldest);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return loaded;
|
|
23
50
|
}
|
|
24
51
|
catch (error) {
|
|
25
52
|
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
53
|
+
indexCache.delete(path);
|
|
26
54
|
return emptyIndex();
|
|
27
55
|
}
|
|
28
56
|
return emptyIndex();
|
|
@@ -34,6 +62,12 @@ const writeIndex = async (vaultPath, index) => {
|
|
|
34
62
|
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
35
63
|
await writeFile(temp, `${JSON.stringify(index)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
36
64
|
await rename(temp, target);
|
|
65
|
+
const fileStats = await stat(target);
|
|
66
|
+
indexCache.set(target, {
|
|
67
|
+
mtimeMs: fileStats.mtimeMs,
|
|
68
|
+
size: fileStats.size,
|
|
69
|
+
index
|
|
70
|
+
});
|
|
37
71
|
};
|
|
38
72
|
const normalizeToken = (value) => value
|
|
39
73
|
.normalize('NFKD')
|
|
@@ -81,6 +115,7 @@ const toResult = (row, mode, text, semantic) => {
|
|
|
81
115
|
title: row.title,
|
|
82
116
|
path: row.path,
|
|
83
117
|
chunkId: row.chunkId,
|
|
118
|
+
chunkOrdinal: row.chunkOrdinal,
|
|
84
119
|
content: row.content,
|
|
85
120
|
score,
|
|
86
121
|
textScore: text,
|
|
@@ -120,6 +155,36 @@ export const openFileIndex = (vaultPath) => {
|
|
|
120
155
|
links
|
|
121
156
|
});
|
|
122
157
|
},
|
|
158
|
+
getIndexedDocuments: async (agentId) => {
|
|
159
|
+
const index = await load();
|
|
160
|
+
const documents = agentId ? index.documents.filter((document) => document.agentId === agentId) : index.documents;
|
|
161
|
+
const selectedDocumentIds = new Set(documents.map((document) => document.id));
|
|
162
|
+
const chunksByDocumentId = index.chunks.reduce((state, chunk) => {
|
|
163
|
+
if (!selectedDocumentIds.has(chunk.documentId)) {
|
|
164
|
+
return state;
|
|
165
|
+
}
|
|
166
|
+
const current = state.get(chunk.documentId) ?? [];
|
|
167
|
+
current.push(chunk);
|
|
168
|
+
state.set(chunk.documentId, current);
|
|
169
|
+
return state;
|
|
170
|
+
}, new Map());
|
|
171
|
+
const linksByDocumentId = index.links.reduce((state, link) => {
|
|
172
|
+
if (!selectedDocumentIds.has(link.fromDocumentId)) {
|
|
173
|
+
return state;
|
|
174
|
+
}
|
|
175
|
+
const current = state.get(link.fromDocumentId) ?? [];
|
|
176
|
+
current.push(link);
|
|
177
|
+
state.set(link.fromDocumentId, current);
|
|
178
|
+
return state;
|
|
179
|
+
}, new Map());
|
|
180
|
+
return documents
|
|
181
|
+
.map((document) => ({
|
|
182
|
+
document,
|
|
183
|
+
chunks: [...(chunksByDocumentId.get(document.id) ?? [])].sort((left, right) => left.ordinal - right.ordinal),
|
|
184
|
+
links: linksByDocumentId.get(document.id) ?? []
|
|
185
|
+
}))
|
|
186
|
+
.sort((left, right) => left.document.path.localeCompare(right.document.path));
|
|
187
|
+
},
|
|
123
188
|
search: async (query, limit, agentId, mode = 'hybrid', queryEmbedding = []) => {
|
|
124
189
|
const index = await load();
|
|
125
190
|
const documentsById = new Map(index.documents.map((document) => [document.id, document]));
|
|
@@ -138,6 +203,7 @@ export const openFileIndex = (vaultPath) => {
|
|
|
138
203
|
title: document.title,
|
|
139
204
|
path: document.path,
|
|
140
205
|
chunkId: chunk.id,
|
|
206
|
+
chunkOrdinal: chunk.ordinal,
|
|
141
207
|
content: chunk.content,
|
|
142
208
|
tags: document.tags,
|
|
143
209
|
embedding: chunk.embedding
|
|
@@ -263,6 +329,7 @@ export const openFileIndex = (vaultPath) => {
|
|
|
263
329
|
title: document.title,
|
|
264
330
|
path: document.path,
|
|
265
331
|
chunkId: document.id,
|
|
332
|
+
chunkOrdinal: 0,
|
|
266
333
|
content: document.content,
|
|
267
334
|
tags: document.tags,
|
|
268
335
|
embedding: []
|
|
@@ -76,6 +76,21 @@ export const readMarkdownFiles = async (vaultPath) => {
|
|
|
76
76
|
};
|
|
77
77
|
}));
|
|
78
78
|
};
|
|
79
|
+
export const readMarkdownFileSummaries = async (vaultPath) => {
|
|
80
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
81
|
+
const paths = await walkMarkdownFiles(absoluteVaultPath);
|
|
82
|
+
const summaries = await Promise.all(paths.map(async (absolutePath) => {
|
|
83
|
+
const fileStats = await stat(absolutePath);
|
|
84
|
+
return {
|
|
85
|
+
absolutePath,
|
|
86
|
+
relativePath: relative(absoluteVaultPath, absolutePath),
|
|
87
|
+
createdAt: fileStats.birthtime,
|
|
88
|
+
updatedAt: fileStats.mtime,
|
|
89
|
+
size: fileStats.size
|
|
90
|
+
};
|
|
91
|
+
}));
|
|
92
|
+
return summaries.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
93
|
+
};
|
|
79
94
|
export const listVaultFiles = async (vaultPath) => {
|
|
80
95
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
81
96
|
return walkVaultFiles(absoluteVaultPath);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const indexStateFileName = 'index-state.json';
|
|
4
|
+
const toIndexStatePath = (vaultPath) => join(vaultPath, '.brainlink', indexStateFileName);
|
|
5
|
+
export const readIndexState = async (vaultPath) => {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = JSON.parse(await readFile(toIndexStatePath(vaultPath), 'utf8'));
|
|
8
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.files)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const files = parsed.files.flatMap((entry) => {
|
|
12
|
+
if (!entry || typeof entry !== 'object') {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const row = entry;
|
|
16
|
+
if (typeof row.path !== 'string' || typeof row.mtimeMs !== 'number' || typeof row.size !== 'number') {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
path: row.path,
|
|
22
|
+
mtimeMs: row.mtimeMs,
|
|
23
|
+
size: row.size
|
|
24
|
+
}
|
|
25
|
+
];
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
version: 1,
|
|
29
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
|
|
30
|
+
chunkSize: typeof parsed.chunkSize === 'number' ? parsed.chunkSize : 1200,
|
|
31
|
+
embeddingProvider: typeof parsed.embeddingProvider === 'string' ? parsed.embeddingProvider : 'none',
|
|
32
|
+
graphLinkModelVersion: typeof parsed.graphLinkModelVersion === 'number' ? parsed.graphLinkModelVersion : 1,
|
|
33
|
+
searchPackRowChunkSize: typeof parsed.searchPackRowChunkSize === 'number' ? parsed.searchPackRowChunkSize : 5_000,
|
|
34
|
+
searchPackCompressionLevel: typeof parsed.searchPackCompressionLevel === 'number' ? parsed.searchPackCompressionLevel : 5,
|
|
35
|
+
searchPackUseDictionary: typeof parsed.searchPackUseDictionary === 'boolean' ? parsed.searchPackUseDictionary : true,
|
|
36
|
+
files,
|
|
37
|
+
pendingPackChanges: typeof parsed.pendingPackChanges === 'number' && parsed.pendingPackChanges >= 0 ? parsed.pendingPackChanges : 0
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export const writeIndexState = async (vaultPath, state) => {
|
|
45
|
+
const payload = {
|
|
46
|
+
version: 1,
|
|
47
|
+
updatedAt: new Date().toISOString(),
|
|
48
|
+
chunkSize: state.chunkSize,
|
|
49
|
+
embeddingProvider: state.embeddingProvider,
|
|
50
|
+
graphLinkModelVersion: state.graphLinkModelVersion,
|
|
51
|
+
searchPackRowChunkSize: state.searchPackRowChunkSize,
|
|
52
|
+
searchPackCompressionLevel: state.searchPackCompressionLevel,
|
|
53
|
+
searchPackUseDictionary: state.searchPackUseDictionary,
|
|
54
|
+
files: [...state.files].sort((left, right) => left.path.localeCompare(right.path)),
|
|
55
|
+
pendingPackChanges: Math.max(0, Math.floor(state.pendingPackChanges))
|
|
56
|
+
};
|
|
57
|
+
await writeFile(toIndexStatePath(vaultPath), `${JSON.stringify(payload)}\n`, 'utf8');
|
|
58
|
+
};
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import { brotliCompressSync, brotliDecompressSync } from 'node:zlib';
|
|
2
|
+
import { brotliCompressSync, brotliDecompressSync, constants as zlibConstants } from 'node:zlib';
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { getBrainlinkHomePath } from './paths.js';
|
|
6
6
|
const magic = Buffer.from('BLPK2', 'ascii');
|
|
7
|
-
const
|
|
7
|
+
const legacyVersion = 1;
|
|
8
|
+
const currentVersion = 2;
|
|
8
9
|
const nonceLength = 12;
|
|
9
10
|
const authTagLength = 16;
|
|
10
11
|
const algorithm = 'aes-256-gcm';
|
|
12
|
+
const compressionLevelMask = 0x0f;
|
|
13
|
+
const compressionDictionaryMask = 0x10;
|
|
14
|
+
const defaultCompressionLevel = 5;
|
|
15
|
+
const builtinDictionary = Buffer.from([
|
|
16
|
+
'"documentId","agentId","title","path","chunkId","chunkOrdinal","content","tags"',
|
|
17
|
+
'"searchMode","textScore","semanticScore","weight","priority","shared"',
|
|
18
|
+
'agents/shared memory-hub architecture context index search graph markdown tags links',
|
|
19
|
+
'#memory #architecture #context #graph #search #index [[Memory Hub]] [[Architecture]]',
|
|
20
|
+
'The quick brown fox jumps over the lazy dog. Brainlink context package metadata.',
|
|
21
|
+
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-:/.#[]{}(), '
|
|
22
|
+
].join('\n'), 'utf8');
|
|
11
23
|
const keyFilePath = (vaultPath) => {
|
|
12
24
|
const vaultHash = createHash('sha256').update(vaultPath).digest('hex').slice(0, 24);
|
|
13
25
|
return join(getBrainlinkHomePath(), 'keys', `search-pack-${vaultHash}.key`);
|
|
@@ -40,34 +52,83 @@ const parseHeader = (payload) => {
|
|
|
40
52
|
throw new Error('Invalid private pack payload: too short.');
|
|
41
53
|
}
|
|
42
54
|
const payloadMagic = payload.subarray(0, magic.length);
|
|
43
|
-
const payloadVersion = payload[magic.length];
|
|
44
|
-
if (!payloadMagic.equals(magic) || payloadVersion !==
|
|
55
|
+
const payloadVersion = payload[magic.length] ?? 0;
|
|
56
|
+
if (!payloadMagic.equals(magic) || (payloadVersion !== legacyVersion && payloadVersion !== currentVersion)) {
|
|
45
57
|
throw new Error('Invalid private pack payload: unsupported format.');
|
|
46
58
|
}
|
|
47
|
-
const
|
|
59
|
+
const hasCompressionSettings = payloadVersion >= 2;
|
|
60
|
+
const settingsByte = hasCompressionSettings ? payload[magic.length + 1] ?? 0 : null;
|
|
61
|
+
const nonceStart = magic.length + 1 + (hasCompressionSettings ? 1 : 0);
|
|
48
62
|
const authTagStart = nonceStart + nonceLength;
|
|
49
63
|
const dataStart = authTagStart + authTagLength;
|
|
50
64
|
return {
|
|
65
|
+
compression: settingsByte != null
|
|
66
|
+
? {
|
|
67
|
+
compressionLevel: settingsByte & compressionLevelMask,
|
|
68
|
+
useDictionary: (settingsByte & compressionDictionaryMask) !== 0
|
|
69
|
+
}
|
|
70
|
+
: {
|
|
71
|
+
compressionLevel: defaultCompressionLevel,
|
|
72
|
+
useDictionary: false
|
|
73
|
+
},
|
|
51
74
|
nonce: payload.subarray(nonceStart, authTagStart),
|
|
52
75
|
authTag: payload.subarray(authTagStart, dataStart),
|
|
53
76
|
ciphertext: payload.subarray(dataStart)
|
|
54
77
|
};
|
|
55
78
|
};
|
|
56
|
-
|
|
79
|
+
const toCompressionLevel = (value) => {
|
|
80
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
81
|
+
return defaultCompressionLevel;
|
|
82
|
+
}
|
|
83
|
+
const normalized = Math.round(value);
|
|
84
|
+
if (normalized < 0) {
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
if (normalized > 11) {
|
|
88
|
+
return 11;
|
|
89
|
+
}
|
|
90
|
+
return normalized;
|
|
91
|
+
};
|
|
92
|
+
const encodeCompressionSettings = (settings) => (settings.compressionLevel & compressionLevelMask) | (settings.useDictionary ? compressionDictionaryMask : 0);
|
|
93
|
+
const brotliEncode = (content, settings) => {
|
|
94
|
+
const options = {
|
|
95
|
+
params: {
|
|
96
|
+
[zlibConstants.BROTLI_PARAM_MODE]: zlibConstants.BROTLI_MODE_TEXT,
|
|
97
|
+
[zlibConstants.BROTLI_PARAM_QUALITY]: settings.compressionLevel
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
if (settings.useDictionary) {
|
|
101
|
+
options.dictionary = builtinDictionary;
|
|
102
|
+
}
|
|
103
|
+
return brotliCompressSync(content, options);
|
|
104
|
+
};
|
|
105
|
+
const brotliDecode = (content, settings) => {
|
|
106
|
+
const options = {};
|
|
107
|
+
if (settings.useDictionary) {
|
|
108
|
+
options.dictionary = builtinDictionary;
|
|
109
|
+
}
|
|
110
|
+
return brotliDecompressSync(content, options);
|
|
111
|
+
};
|
|
112
|
+
export const encodePrivatePack = async (vaultPath, content, settings) => {
|
|
57
113
|
const key = await readOrCreateKey(vaultPath);
|
|
58
114
|
const nonce = randomBytes(nonceLength);
|
|
59
|
-
const
|
|
115
|
+
const normalizedSettings = {
|
|
116
|
+
compressionLevel: toCompressionLevel(settings?.compressionLevel),
|
|
117
|
+
useDictionary: settings?.useDictionary ?? true
|
|
118
|
+
};
|
|
119
|
+
const compressed = brotliEncode(content, normalizedSettings);
|
|
60
120
|
const cipher = createCipheriv(algorithm, key, nonce);
|
|
61
121
|
const ciphertext = Buffer.concat([cipher.update(compressed), cipher.final()]);
|
|
62
122
|
const authTag = cipher.getAuthTag();
|
|
63
|
-
|
|
123
|
+
const settingsByte = Buffer.from([encodeCompressionSettings(normalizedSettings)]);
|
|
124
|
+
return Buffer.concat([magic, Buffer.from([currentVersion]), settingsByte, nonce, authTag, ciphertext]);
|
|
64
125
|
};
|
|
65
126
|
export const decodePrivatePack = async (vaultPath, payload) => {
|
|
66
127
|
const key = await readOrCreateKey(vaultPath);
|
|
67
|
-
const { nonce, authTag, ciphertext } = parseHeader(payload);
|
|
128
|
+
const { nonce, authTag, ciphertext, compression } = parseHeader(payload);
|
|
68
129
|
const decipher = createDecipheriv(algorithm, key, nonce);
|
|
69
130
|
decipher.setAuthTag(authTag);
|
|
70
131
|
const compressed = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
71
|
-
return
|
|
132
|
+
return brotliDecode(compressed, compression);
|
|
72
133
|
};
|
|
73
134
|
export const isPrivatePackPayload = (payload) => payload.length >= magic.length + 1 && payload.subarray(0, magic.length).equals(magic);
|