@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.
Files changed (50) hide show
  1. package/AGENTS.md +9 -6
  2. package/CHANGELOG.md +27 -0
  3. package/COPYRIGHT.md +5 -0
  4. package/README.md +177 -20
  5. package/dist/application/add-note.js +13 -44
  6. package/dist/application/auto-migrate-configured-vault.js +37 -0
  7. package/dist/application/build-context.js +64 -3
  8. package/dist/application/canonical-context-links.js +209 -0
  9. package/dist/application/dedupe-notes.js +226 -0
  10. package/dist/application/frontend/client-css.js +258 -51
  11. package/dist/application/frontend/client-html.js +50 -27
  12. package/dist/application/frontend/client-js.js +1369 -605
  13. package/dist/application/frontend/client-render-worker-js.js +645 -0
  14. package/dist/application/frontend/client-worker-js.js +66 -0
  15. package/dist/application/get-graph-contexts.js +33 -0
  16. package/dist/application/get-graph-layout.js +62 -8
  17. package/dist/application/get-graph-stream-chunk.js +326 -0
  18. package/dist/application/get-graph-view.js +246 -0
  19. package/dist/application/graph-view-state.js +66 -0
  20. package/dist/application/import-legacy-sqlite.js +266 -0
  21. package/dist/application/index-vault.js +262 -23
  22. package/dist/application/migrate-context-links.js +79 -0
  23. package/dist/application/offline-pack-backup.js +44 -0
  24. package/dist/application/search-graph-node-ids.js +63 -3
  25. package/dist/application/server/routes.js +247 -7
  26. package/dist/application/start-server.js +75 -4
  27. package/dist/application/watch-vault.js +23 -2
  28. package/dist/cli/commands/agent-commands.js +7 -0
  29. package/dist/cli/commands/write-commands.js +924 -14
  30. package/dist/cli/runtime.js +10 -2
  31. package/dist/domain/context.js +54 -11
  32. package/dist/domain/graph-contexts.js +180 -0
  33. package/dist/domain/graph-layout.js +389 -18
  34. package/dist/domain/markdown.js +53 -9
  35. package/dist/domain/middle-out.js +18 -0
  36. package/dist/infrastructure/config.js +121 -4
  37. package/dist/infrastructure/file-index.js +76 -6
  38. package/dist/infrastructure/file-system-vault.js +15 -0
  39. package/dist/infrastructure/index-state.js +58 -0
  40. package/dist/infrastructure/private-pack-codec.js +71 -10
  41. package/dist/infrastructure/search-packs.js +286 -15
  42. package/dist/infrastructure/vault-migration-state.js +69 -0
  43. package/dist/infrastructure/volatile-memory.js +100 -0
  44. package/dist/mcp/runtime.js +20 -0
  45. package/dist/mcp/server.js +39 -11
  46. package/dist/mcp/tools.js +183 -7
  47. package/docs/AGENT_USAGE.md +96 -5
  48. package/docs/ARCHITECTURE.md +8 -0
  49. package/docs/QUICKSTART.md +7 -0
  50. package/package.json +7 -2
@@ -10,11 +10,19 @@ export const defaultBrainlinkConfig = {
10
10
  allowedVaults: [],
11
11
  defaultAgent: undefined,
12
12
  autoIndexOnWrite: true,
13
+ autoCanonicalContextLinks: true,
13
14
  defaultSearchLimit: 10,
14
15
  defaultContextTokens: 2000,
15
16
  embeddingProvider: 'local',
16
17
  defaultSearchMode: 'hybrid',
17
18
  chunkSize: 1200,
19
+ searchPack: {
20
+ rowChunkSize: 5_000,
21
+ compressionLevel: 5,
22
+ useDictionary: true,
23
+ guardrailMinSavingsPercent: 8,
24
+ guardrailMaxLatencyRegressionPercent: 5
25
+ },
18
26
  agentProfiles: {}
19
27
  };
20
28
  const configFilenames = ['brainlink.config.json', '.brainlink.json'];
@@ -37,6 +45,36 @@ const sanitizeEmbeddingProvider = (value) => typeof value === 'string' && embedd
37
45
  export const sanitizeSearchMode = (value, fallback = defaultBrainlinkConfig.defaultSearchMode) => typeof value === 'string' && searchModes.has(value) ? value : fallback;
38
46
  const sanitizeAllowedVaults = (value) => Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.trim().length > 0) : [];
39
47
  const sanitizePositiveNumber = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
48
+ const sanitizeIntegerInRange = (value, fallback, minimum, maximum) => {
49
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
50
+ return fallback;
51
+ }
52
+ const rounded = Math.round(value);
53
+ if (rounded < minimum) {
54
+ return minimum;
55
+ }
56
+ if (rounded > maximum) {
57
+ return maximum;
58
+ }
59
+ return rounded;
60
+ };
61
+ const sanitizeSearchPackConfig = (value) => {
62
+ const fallback = defaultBrainlinkConfig.searchPack;
63
+ if (!isRecord(value)) {
64
+ return fallback;
65
+ }
66
+ return {
67
+ rowChunkSize: sanitizeIntegerInRange(value.rowChunkSize, fallback.rowChunkSize, 100, 100_000),
68
+ compressionLevel: sanitizeIntegerInRange(value.compressionLevel, fallback.compressionLevel, 0, 11),
69
+ useDictionary: typeof value.useDictionary === 'boolean' ? value.useDictionary : fallback.useDictionary,
70
+ guardrailMinSavingsPercent: typeof value.guardrailMinSavingsPercent === 'number' && Number.isFinite(value.guardrailMinSavingsPercent)
71
+ ? Math.max(0, Math.min(95, value.guardrailMinSavingsPercent))
72
+ : fallback.guardrailMinSavingsPercent,
73
+ guardrailMaxLatencyRegressionPercent: typeof value.guardrailMaxLatencyRegressionPercent === 'number' && Number.isFinite(value.guardrailMaxLatencyRegressionPercent)
74
+ ? Math.max(0, Math.min(300, value.guardrailMaxLatencyRegressionPercent))
75
+ : fallback.guardrailMaxLatencyRegressionPercent
76
+ };
77
+ };
40
78
  const sanitizeAgentProfile = (value) => {
41
79
  if (!isRecord(value)) {
42
80
  return null;
@@ -122,6 +160,9 @@ const sanitizeConfig = (value) => ({
122
160
  ? sanitizeAgentId(value.defaultAgent)
123
161
  : defaultBrainlinkConfig.defaultAgent,
124
162
  autoIndexOnWrite: typeof value.autoIndexOnWrite === 'boolean' ? value.autoIndexOnWrite : defaultBrainlinkConfig.autoIndexOnWrite,
163
+ autoCanonicalContextLinks: typeof value.autoCanonicalContextLinks === 'boolean'
164
+ ? value.autoCanonicalContextLinks
165
+ : defaultBrainlinkConfig.autoCanonicalContextLinks,
125
166
  defaultSearchLimit: typeof value.defaultSearchLimit === 'number' && value.defaultSearchLimit > 0
126
167
  ? value.defaultSearchLimit
127
168
  : defaultBrainlinkConfig.defaultSearchLimit,
@@ -130,6 +171,7 @@ const sanitizeConfig = (value) => ({
130
171
  : defaultBrainlinkConfig.defaultContextTokens,
131
172
  allowedVaults: [...sanitizeAllowedVaults(value.allowedVaults), ...readAllowedVaultsFromEnv()],
132
173
  chunkSize: typeof value.chunkSize === 'number' && value.chunkSize > 0 ? value.chunkSize : defaultBrainlinkConfig.chunkSize,
174
+ searchPack: sanitizeSearchPackConfig(value.searchPack),
133
175
  embeddingProvider: sanitizeEmbeddingProvider(value.embeddingProvider),
134
176
  defaultSearchMode: sanitizeSearchMode(value.defaultSearchMode),
135
177
  agentProfiles: sanitizeAgentProfiles(value.agentProfiles)
@@ -143,12 +185,87 @@ export const resolveAgentRuntimeDefaults = (config, agent) => {
143
185
  defaultSearchMode: profile?.defaultSearchMode ?? config.defaultSearchMode
144
186
  };
145
187
  };
188
+ const mergeConfigLayers = (layers) => layers.reduce((state, config) => ({
189
+ ...state,
190
+ ...config
191
+ }), {});
192
+ export const getVaultConfigSourceDetails = async (cwd = safeCwd()) => {
193
+ const [globalConfig, localConfig, legacyLocalConfig] = await Promise.all([
194
+ loadRawConfig('global', cwd),
195
+ loadRawConfig('local', cwd),
196
+ loadLegacyLocalRawConfig(cwd)
197
+ ]);
198
+ if (typeof legacyLocalConfig.vault === 'string' && legacyLocalConfig.vault.trim().length > 0) {
199
+ return {
200
+ source: 'local-legacy',
201
+ sourcePath: resolve(cwd, '.brainlink.json')
202
+ };
203
+ }
204
+ if (typeof localConfig.vault === 'string' && localConfig.vault.trim().length > 0) {
205
+ return {
206
+ source: 'local',
207
+ sourcePath: getLocalConfigPath(cwd)
208
+ };
209
+ }
210
+ if (typeof globalConfig.vault === 'string' && globalConfig.vault.trim().length > 0) {
211
+ return {
212
+ source: 'global',
213
+ sourcePath: getGlobalConfigPath()
214
+ };
215
+ }
216
+ return {
217
+ source: 'default',
218
+ sourcePath: null
219
+ };
220
+ };
146
221
  export const loadBrainlinkConfig = async (cwd = safeCwd()) => {
147
222
  const globalConfig = await readJsonConfig(getGlobalConfigPath());
148
223
  const localConfigs = await Promise.all(configFilenames.map((filename) => readJsonConfig(resolve(cwd, filename))));
149
- const merged = [globalConfig, ...localConfigs].reduce((state, config) => ({
150
- ...state,
151
- ...config
152
- }), {});
224
+ const merged = mergeConfigLayers([globalConfig, ...localConfigs]);
153
225
  return sanitizeConfig(merged);
154
226
  };
227
+ export const loadBrainlinkConfigWithSource = async (cwd = safeCwd()) => {
228
+ const globalConfigPath = getGlobalConfigPath();
229
+ const localConfigPath = getLocalConfigPath(cwd);
230
+ const legacyLocalConfigPath = resolve(cwd, '.brainlink.json');
231
+ const [globalConfig, localConfig, legacyLocalConfig] = await Promise.all([
232
+ readJsonConfig(globalConfigPath),
233
+ readJsonConfig(localConfigPath),
234
+ readJsonConfig(legacyLocalConfigPath)
235
+ ]);
236
+ const config = sanitizeConfig(mergeConfigLayers([globalConfig, localConfig, legacyLocalConfig]));
237
+ if (typeof legacyLocalConfig.vault === 'string' && legacyLocalConfig.vault.trim().length > 0) {
238
+ return {
239
+ config,
240
+ vaultSource: {
241
+ source: 'local-legacy',
242
+ sourcePath: legacyLocalConfigPath
243
+ }
244
+ };
245
+ }
246
+ if (typeof localConfig.vault === 'string' && localConfig.vault.trim().length > 0) {
247
+ return {
248
+ config,
249
+ vaultSource: {
250
+ source: 'local',
251
+ sourcePath: localConfigPath
252
+ }
253
+ };
254
+ }
255
+ if (typeof globalConfig.vault === 'string' && globalConfig.vault.trim().length > 0) {
256
+ return {
257
+ config,
258
+ vaultSource: {
259
+ source: 'global',
260
+ sourcePath: globalConfigPath
261
+ }
262
+ };
263
+ }
264
+ return {
265
+ config,
266
+ vaultSource: {
267
+ source: 'default',
268
+ sourcePath: null
269
+ }
270
+ };
271
+ };
@@ -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
@@ -200,7 +266,8 @@ export const openFileIndex = (vaultPath) => {
200
266
  title: document.title,
201
267
  path: document.path,
202
268
  content: document.content,
203
- tags: document.tags
269
+ tags: document.tags,
270
+ contextLinks: document.contextLinks ?? []
204
271
  })),
205
272
  edges
206
273
  };
@@ -226,7 +293,8 @@ export const openFileIndex = (vaultPath) => {
226
293
  title: document.title,
227
294
  path: document.path,
228
295
  content: '',
229
- tags: document.tags
296
+ tags: document.tags,
297
+ contextLinks: document.contextLinks ?? []
230
298
  })),
231
299
  edges
232
300
  };
@@ -243,7 +311,8 @@ export const openFileIndex = (vaultPath) => {
243
311
  title: document.title,
244
312
  path: document.path,
245
313
  content: document.content,
246
- tags: document.tags
314
+ tags: document.tags,
315
+ contextLinks: document.contextLinks ?? []
247
316
  }
248
317
  : undefined;
249
318
  },
@@ -263,6 +332,7 @@ export const openFileIndex = (vaultPath) => {
263
332
  title: document.title,
264
333
  path: document.path,
265
334
  chunkId: document.id,
335
+ chunkOrdinal: 0,
266
336
  content: document.content,
267
337
  tags: document.tags,
268
338
  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);