@andespindola/brainlink 0.1.0-beta.15 → 0.1.0-beta.150

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.
Files changed (43) hide show
  1. package/AGENTS.md +3 -0
  2. package/CHANGELOG.md +24 -0
  3. package/COPYRIGHT.md +5 -0
  4. package/README.md +135 -7
  5. package/dist/application/auto-migrate-configured-vault.js +37 -0
  6. package/dist/application/build-context.js +64 -3
  7. package/dist/application/dedupe-notes.js +226 -0
  8. package/dist/application/frontend/client-css.js +111 -47
  9. package/dist/application/frontend/client-html.js +42 -26
  10. package/dist/application/frontend/client-js.js +788 -554
  11. package/dist/application/frontend/client-render-worker-js.js +569 -0
  12. package/dist/application/frontend/client-worker-js.js +66 -0
  13. package/dist/application/get-graph-layout.js +38 -5
  14. package/dist/application/get-graph-stream-chunk.js +289 -0
  15. package/dist/application/get-graph-view.js +243 -0
  16. package/dist/application/import-legacy-sqlite.js +296 -0
  17. package/dist/application/index-vault.js +249 -21
  18. package/dist/application/offline-pack-backup.js +44 -0
  19. package/dist/application/server/routes.js +187 -5
  20. package/dist/application/start-server.js +75 -4
  21. package/dist/application/watch-vault.js +23 -2
  22. package/dist/cli/commands/agent-commands.js +7 -0
  23. package/dist/cli/commands/write-commands.js +842 -8
  24. package/dist/cli/runtime.js +10 -2
  25. package/dist/domain/context.js +54 -11
  26. package/dist/domain/graph-layout.js +275 -3
  27. package/dist/domain/markdown.js +29 -9
  28. package/dist/domain/middle-out.js +18 -0
  29. package/dist/infrastructure/config.js +117 -4
  30. package/dist/infrastructure/file-index.js +70 -3
  31. package/dist/infrastructure/file-system-vault.js +15 -0
  32. package/dist/infrastructure/index-state.js +58 -0
  33. package/dist/infrastructure/private-pack-codec.js +71 -10
  34. package/dist/infrastructure/search-packs.js +286 -15
  35. package/dist/infrastructure/vault-migration-state.js +69 -0
  36. package/dist/infrastructure/volatile-memory.js +100 -0
  37. package/dist/mcp/runtime.js +20 -0
  38. package/dist/mcp/server.js +28 -10
  39. package/dist/mcp/tools.js +110 -0
  40. package/docs/AGENT_USAGE.md +87 -3
  41. package/docs/ARCHITECTURE.md +6 -0
  42. package/docs/QUICKSTART.md +7 -0
  43. 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 parsed = JSON.parse(await readFile(indexStoragePath(vaultPath), 'utf8'));
16
- return {
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 version = 1;
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 !== version) {
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 nonceStart = magic.length + 1;
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
- export const encodePrivatePack = async (vaultPath, content) => {
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 compressed = brotliCompressSync(content);
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
- return Buffer.concat([magic, Buffer.from([version]), nonce, authTag, ciphertext]);
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 brotliDecompressSync(compressed);
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);