@andespindola/brainlink 0.1.0-beta.12 → 0.1.0-beta.121

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 (52) hide show
  1. package/AGENTS.md +8 -5
  2. package/CHANGELOG.md +26 -2
  3. package/CONTRIBUTING.md +2 -2
  4. package/COPYRIGHT.md +5 -0
  5. package/README.md +138 -18
  6. package/SECURITY.md +1 -1
  7. package/dist/application/analyze-vault.js +1 -9
  8. package/dist/application/build-context.js +56 -1
  9. package/dist/application/dedupe-notes.js +226 -0
  10. package/dist/application/frontend/client-css.js +93 -45
  11. package/dist/application/frontend/client-html.js +34 -25
  12. package/dist/application/frontend/client-js.js +2698 -181
  13. package/dist/application/frontend/client-worker-js.js +66 -0
  14. package/dist/application/get-graph-layout.js +2 -2
  15. package/dist/application/get-graph-node.js +3 -3
  16. package/dist/application/get-graph-summary.js +3 -3
  17. package/dist/application/get-graph.js +3 -3
  18. package/dist/application/import-legacy-sqlite.js +296 -0
  19. package/dist/application/index-vault.js +250 -24
  20. package/dist/application/list-agents.js +3 -3
  21. package/dist/application/list-links.js +5 -5
  22. package/dist/application/offline-pack-backup.js +44 -0
  23. package/dist/application/search-graph-node-ids.js +3 -3
  24. package/dist/application/search-knowledge.js +6 -6
  25. package/dist/application/server/routes.js +90 -1
  26. package/dist/application/start-server.js +75 -4
  27. package/dist/application/watch-vault.js +23 -2
  28. package/dist/benchmarks/large-vault.js +1 -1
  29. package/dist/cli/commands/agent-commands.js +7 -0
  30. package/dist/cli/commands/write-commands.js +818 -8
  31. package/dist/domain/context.js +53 -11
  32. package/dist/domain/graph-layout.js +177 -3
  33. package/dist/domain/middle-out.js +18 -0
  34. package/dist/infrastructure/config.js +38 -0
  35. package/dist/infrastructure/file-index.js +358 -0
  36. package/dist/infrastructure/file-system-vault.js +15 -0
  37. package/dist/infrastructure/index-state.js +56 -0
  38. package/dist/infrastructure/private-pack-codec.js +71 -10
  39. package/dist/infrastructure/search-packs.js +313 -17
  40. package/dist/mcp/server.js +11 -1
  41. package/dist/mcp/tools.js +62 -0
  42. package/docs/AGENT_USAGE.md +96 -17
  43. package/docs/ARCHITECTURE.md +22 -27
  44. package/docs/QUICKSTART.md +7 -0
  45. package/package.json +6 -4
  46. package/dist/infrastructure/sqlite/document-writer.js +0 -51
  47. package/dist/infrastructure/sqlite/graph-reader.js +0 -267
  48. package/dist/infrastructure/sqlite/recovery.js +0 -83
  49. package/dist/infrastructure/sqlite/schema.js +0 -114
  50. package/dist/infrastructure/sqlite/search-reader.js +0 -188
  51. package/dist/infrastructure/sqlite/types.js +0 -1
  52. package/dist/infrastructure/sqlite-index.js +0 -38
@@ -1,13 +1,50 @@
1
+ import { middleOutIndices } from './middle-out.js';
2
+ const maxSectionsPerDocument = 3;
3
+ const byScore = (left, right) => right.score - left.score || left.title.localeCompare(right.title);
4
+ const byOrdinal = (left, right) => (left.chunkOrdinal ?? Number.MAX_SAFE_INTEGER) - (right.chunkOrdinal ?? Number.MAX_SAFE_INTEGER);
5
+ const middleOutDocumentResults = (results) => {
6
+ if (results.length <= 1) {
7
+ return results;
8
+ }
9
+ const sortedByOrdinal = [...results].sort(byOrdinal);
10
+ const pivotChunkId = [...results].sort(byScore)[0]?.chunkId;
11
+ const pivotIndex = sortedByOrdinal.findIndex((result) => result.chunkId === pivotChunkId);
12
+ if (pivotIndex < 0) {
13
+ return [...results].sort(byScore);
14
+ }
15
+ return middleOutIndices(sortedByOrdinal.length, pivotIndex).map((index) => sortedByOrdinal[index]);
16
+ };
1
17
  export const selectContextSections = (results, maxTokens) => {
2
- const selected = results.reduce((state, result) => {
3
- const tokenCost = Math.ceil(result.content.length / 4);
4
- if (state.usedTokens + tokenCost > maxTokens || state.seenDocuments.has(result.documentId)) {
5
- return state;
18
+ const grouped = results.reduce((state, result) => {
19
+ const current = state.get(result.documentId) ?? [];
20
+ state.set(result.documentId, [...current, result]);
21
+ return state;
22
+ }, new Map());
23
+ const documentOrder = Array.from(results.reduce((state, result) => {
24
+ if (!state.has(result.documentId)) {
25
+ state.set(result.documentId, result.score);
6
26
  }
7
- return {
8
- usedTokens: state.usedTokens + tokenCost,
9
- sections: [
10
- ...state.sections,
27
+ return state;
28
+ }, new Map()).entries())
29
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
30
+ .map(([documentId]) => documentId);
31
+ const selected = documentOrder.reduce((state, documentId) => {
32
+ const ordered = middleOutDocumentResults(grouped.get(documentId) ?? []);
33
+ let usedTokens = state.usedTokens;
34
+ let sections = state.sections;
35
+ let seenChunks = state.seenChunks;
36
+ for (let index = 0; index < ordered.length && index < maxSectionsPerDocument; index += 1) {
37
+ const result = ordered[index];
38
+ if (seenChunks.has(result.chunkId)) {
39
+ continue;
40
+ }
41
+ const tokenCost = Math.ceil(result.content.length / 4);
42
+ if (usedTokens + tokenCost > maxTokens) {
43
+ break;
44
+ }
45
+ usedTokens += tokenCost;
46
+ sections = [
47
+ ...sections,
11
48
  {
12
49
  title: result.title,
13
50
  path: result.path,
@@ -16,13 +53,18 @@ export const selectContextSections = (results, maxTokens) => {
16
53
  searchMode: result.searchMode,
17
54
  tags: result.tags
18
55
  }
19
- ],
20
- seenDocuments: new Set([...state.seenDocuments, result.documentId])
56
+ ];
57
+ seenChunks = new Set([...seenChunks, result.chunkId]);
58
+ }
59
+ return {
60
+ usedTokens,
61
+ sections,
62
+ seenChunks
21
63
  };
22
64
  }, {
23
65
  usedTokens: 0,
24
66
  sections: [],
25
- seenDocuments: new Set()
67
+ seenChunks: new Set()
26
68
  });
27
69
  return selected.sections;
28
70
  };
@@ -1,3 +1,4 @@
1
+ const hierarchyGroupNodeLimit = 1000;
1
2
  const groupLabels = {
2
3
  '00-maps': 'maps',
3
4
  '10-agent-memory': 'agent-memory',
@@ -20,6 +21,7 @@ const segmentAngles = {
20
21
  Evaluation: 2.08,
21
22
  Security: 2.82
22
23
  };
24
+ const hubTitlePattern = /\b(memory\s*hub|knowledge\s*root|moc|map)\b/i;
23
25
  const hashText = (value) => Array.from(value).reduce((hash, char) => ((hash << 5) - hash + char.charCodeAt(0)) | 0, 0);
24
26
  const jitter = (value, range) => {
25
27
  const normalized = Math.abs(hashText(value) % 1000) / 1000;
@@ -62,6 +64,44 @@ const byDegreeThenTitle = (degrees) => (left, right) => {
62
64
  const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
63
65
  return degreeDelta === 0 ? byTitle(left, right) : degreeDelta;
64
66
  };
67
+ const hubScore = (node) => {
68
+ const title = node.title.trim().toLowerCase();
69
+ if (title === 'memory hub')
70
+ return 5;
71
+ if (title === 'knowledge root')
72
+ return 4;
73
+ if (/\bmoc\b/i.test(node.title))
74
+ return 3;
75
+ return hubTitlePattern.test(node.title) ? 2 : 0;
76
+ };
77
+ const selectPrimaryHubId = (nodes, degrees) => {
78
+ const ranked = [...nodes]
79
+ .filter((node) => hubScore(node) > 0)
80
+ .sort((left, right) => {
81
+ const scoreDelta = hubScore(right) - hubScore(left);
82
+ if (scoreDelta !== 0)
83
+ return scoreDelta;
84
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
85
+ if (degreeDelta !== 0)
86
+ return degreeDelta;
87
+ return left.title.localeCompare(right.title);
88
+ });
89
+ return ranked[0]?.id ?? null;
90
+ };
91
+ const centerLayoutByNode = (nodes, nodeId) => {
92
+ if (!nodeId) {
93
+ return nodes;
94
+ }
95
+ const anchor = nodes.find((node) => node.id === nodeId);
96
+ if (!anchor) {
97
+ return nodes;
98
+ }
99
+ return nodes.map((node) => ({
100
+ ...node,
101
+ x: node.x - anchor.x,
102
+ y: node.y - anchor.y
103
+ }));
104
+ };
65
105
  const naturalSegmentSeed = (node) => groupKey(node) === '00-maps' || /\b(moc|map)\b/i.test(node.title);
66
106
  const segmentName = (node) => node.title.replace(/^MOC\s+/i, '').replace(/\s+Memory Map$/i, '').trim() || node.title;
67
107
  const collectComponent = (adjacency, startId, visited) => {
@@ -128,13 +168,17 @@ const groupNodesBySegment = (nodes, segments) => {
128
168
  return new Map(groups);
129
169
  };
130
170
  const segmentAngle = (segment, index, count) => segmentAngles[segment] ?? (Math.PI * 2 * index) / Math.max(count, 1) - Math.PI / 2;
171
+ const petalSpreadForSegmentSize = (size) => {
172
+ const safeSize = Math.max(size, 1);
173
+ return 180 + Math.log2(safeSize + 1) * 6;
174
+ };
131
175
  const createSegmentNodes = (segments, degrees, segmentCount) => ([segment, nodes], segmentIndex) => {
132
176
  const sortedNodes = [...nodes].sort(byDegreeThenTitle(degrees));
133
177
  const angle = segmentAngle(segment, segmentIndex, segmentCount);
134
178
  const baseRadius = segmentCount === 1 ? 0 : 340 + Math.min(sortedNodes.length, 22) * 10;
135
179
  const centerX = Math.cos(angle) * baseRadius;
136
180
  const centerY = Math.sin(angle) * (baseRadius * 0.78);
137
- const petalSpread = 40 + Math.sqrt(sortedNodes.length) * 14;
181
+ const petalSpread = petalSpreadForSegmentSize(sortedNodes.length);
138
182
  return sortedNodes.map((node, index) => {
139
183
  const localAngle = index * 2.399963 + jitter(node.title, 0.42);
140
184
  const localRadius = Math.sqrt(index + 1) * petalSpread;
@@ -149,6 +193,132 @@ const createSegmentNodes = (segments, degrees, segmentCount) => ([segment, nodes
149
193
  });
150
194
  };
151
195
  const distanceBetween = (left, right) => Math.hypot(right.x - left.x, right.y - left.y);
196
+ const layoutBounds = (nodes) => {
197
+ if (nodes.length === 0) {
198
+ return { x: 0, y: 0, radius: 1 };
199
+ }
200
+ const bounds = nodes.reduce((current, node) => ({
201
+ minX: Math.min(current.minX, node.x),
202
+ maxX: Math.max(current.maxX, node.x),
203
+ minY: Math.min(current.minY, node.y),
204
+ maxY: Math.max(current.maxY, node.y)
205
+ }), {
206
+ minX: Number.POSITIVE_INFINITY,
207
+ maxX: Number.NEGATIVE_INFINITY,
208
+ minY: Number.POSITIVE_INFINITY,
209
+ maxY: Number.NEGATIVE_INFINITY
210
+ });
211
+ const x = (bounds.minX + bounds.maxX) / 2;
212
+ const y = (bounds.minY + bounds.maxY) / 2;
213
+ const radius = nodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - x, node.y - y)), 1);
214
+ return { x, y, radius: Math.max(radius + 72, 120) };
215
+ };
216
+ const edgeTouchesGroup = (edge, nodeIds) => nodeIds.has(edge.source) || Boolean(edge.target && nodeIds.has(edge.target));
217
+ const edgeInsideGroup = (edge, nodeIds) => nodeIds.has(edge.source) && Boolean(edge.target && nodeIds.has(edge.target));
218
+ const groupTitle = (segment, level, index, nodes) => nodes.length === 1
219
+ ? nodes[0]?.title ?? segment
220
+ : `${segment} ${level + 1}.${index + 1}`;
221
+ const chunkNodes = (nodes, degrees, groupNodeLimit = hierarchyGroupNodeLimit) => {
222
+ const sortedNodes = [...nodes].sort((left, right) => {
223
+ const segmentDelta = left.segment.localeCompare(right.segment);
224
+ if (segmentDelta !== 0)
225
+ return segmentDelta;
226
+ const groupDelta = left.group.localeCompare(right.group);
227
+ if (groupDelta !== 0)
228
+ return groupDelta;
229
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
230
+ if (degreeDelta !== 0)
231
+ return degreeDelta;
232
+ return left.title.localeCompare(right.title);
233
+ });
234
+ const chunks = [];
235
+ for (let index = 0; index < sortedNodes.length; index += groupNodeLimit) {
236
+ chunks.push(sortedNodes.slice(index, index + groupNodeLimit));
237
+ }
238
+ return chunks;
239
+ };
240
+ const groupEdges = (edges, nodeIds) => ({
241
+ internalEdges: edges.filter((edge) => edgeInsideGroup(edge, nodeIds)),
242
+ externalEdges: edges.filter((edge) => edgeTouchesGroup(edge, nodeIds) && !edgeInsideGroup(edge, nodeIds))
243
+ });
244
+ const groupBounds = (groups) => {
245
+ if (groups.length === 0) {
246
+ return { x: 0, y: 0, radius: 1 };
247
+ }
248
+ const nodes = groups.map((group) => ({
249
+ x: group.x,
250
+ y: group.y,
251
+ radius: group.radius
252
+ }));
253
+ const x = nodes.reduce((sum, node) => sum + node.x, 0) / nodes.length;
254
+ const y = nodes.reduce((sum, node) => sum + node.y, 0) / nodes.length;
255
+ const radius = nodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - x, node.y - y) + node.radius), 1);
256
+ return { x, y, radius: Math.max(radius + 120, 180) };
257
+ };
258
+ const descendantNodeIds = (groups) => groups.flatMap((group) => group.nodeIds);
259
+ const createParentGroups = (groups, edges, level, groupNodeLimit) => {
260
+ if (groups.length <= groupNodeLimit) {
261
+ return groups;
262
+ }
263
+ const parentGroups = [];
264
+ for (let index = 0; index < groups.length; index += groupNodeLimit) {
265
+ const chunk = groups.slice(index, index + groupNodeLimit);
266
+ const nodeIds = new Set(descendantNodeIds(chunk));
267
+ const bounds = groupBounds(chunk);
268
+ const segment = chunk[0]?.segment ?? 'root';
269
+ const group = chunk[0]?.group ?? 'root';
270
+ const groupIndex = index / groupNodeLimit;
271
+ const id = ['root', level, groupIndex, chunk[0]?.id ?? 'empty', chunk.length].join(':');
272
+ const edgeGroups = groupEdges(edges, nodeIds);
273
+ parentGroups.push({
274
+ id,
275
+ level,
276
+ parentId: null,
277
+ title: `${segment} ${level + 1}.${Math.floor(groupIndex) + 1}`,
278
+ segment,
279
+ group,
280
+ x: bounds.x,
281
+ y: bounds.y,
282
+ radius: bounds.radius,
283
+ nodeIds: [],
284
+ childGroupIds: chunk.map((child) => child.id),
285
+ internalEdges: edgeGroups.internalEdges,
286
+ externalEdges: edgeGroups.externalEdges
287
+ });
288
+ }
289
+ const relinkedChildren = groups.map((group) => {
290
+ const parent = parentGroups.find((candidate) => candidate.childGroupIds.includes(group.id));
291
+ return parent ? { ...group, parentId: parent.id } : group;
292
+ });
293
+ return [...createParentGroups(parentGroups, edges, level + 1, groupNodeLimit), ...relinkedChildren];
294
+ };
295
+ export const createGraphLayoutHierarchy = (nodes, edges, degrees, groupNodeLimit = hierarchyGroupNodeLimit) => {
296
+ if (nodes.length <= groupNodeLimit) {
297
+ return [];
298
+ }
299
+ const leafGroups = chunkNodes(nodes, degrees, groupNodeLimit).map((chunk, index) => {
300
+ const nodeIds = new Set(chunk.map((node) => node.id));
301
+ const bounds = layoutBounds(chunk);
302
+ const segment = chunk[0]?.segment ?? 'root';
303
+ const group = chunk[0]?.group ?? 'root';
304
+ const id = ['leaf', 0, index, chunk[0]?.id ?? 'empty', chunk.length].join(':');
305
+ return {
306
+ id,
307
+ level: 0,
308
+ parentId: null,
309
+ title: groupTitle(segment, 0, index, chunk),
310
+ segment,
311
+ group,
312
+ x: bounds.x,
313
+ y: bounds.y,
314
+ radius: bounds.radius,
315
+ nodeIds: chunk.map((node) => node.id),
316
+ childGroupIds: [],
317
+ ...groupEdges(edges, nodeIds)
318
+ };
319
+ });
320
+ return createParentGroups(leafGroups, edges, 1, groupNodeLimit);
321
+ };
152
322
  const resolveCollisionPair = (left, right, minDistance) => {
153
323
  const dx = right.x - left.x;
154
324
  const dy = right.y - left.y;
@@ -246,9 +416,13 @@ export const createCauliflowerGraphLayout = (graph) => {
246
416
  const segmentGroups = Array.from(groupNodesBySegment(graph.nodes, segments).entries())
247
417
  .sort(([left], [right]) => left.localeCompare(right));
248
418
  const nodes = relaxCollisions(segmentGroups.flatMap(createSegmentNodes(segments, degrees, segmentGroups.length)));
419
+ const primaryHubId = selectPrimaryHubId(graph.nodes, degrees);
420
+ const centeredNodes = centerLayoutByNode(nodes, primaryHubId);
421
+ const groups = createGraphLayoutHierarchy(centeredNodes, graph.edges, degrees);
249
422
  return {
250
- nodes,
251
- edges: graph.edges
423
+ nodes: centeredNodes,
424
+ edges: graph.edges,
425
+ ...(groups.length > 0 ? { groups } : {})
252
426
  };
253
427
  };
254
428
  export const getMinimumLayoutDistance = (nodes) => nodes.reduce((minimumDistance, leftNode, leftIndex) => nodes.slice(leftIndex + 1).reduce((innerMinimum, rightNode) => Math.min(innerMinimum, distanceBetween(leftNode, rightNode)), minimumDistance), Number.POSITIVE_INFINITY);
@@ -0,0 +1,18 @@
1
+ export const middleOutIndices = (size, pivotIndex) => {
2
+ if (!Number.isFinite(size) || size <= 0) {
3
+ return [];
4
+ }
5
+ const clampedPivot = Math.max(0, Math.min(Math.floor(pivotIndex), size - 1));
6
+ const indices = [clampedPivot];
7
+ for (let offset = 1; indices.length < size; offset += 1) {
8
+ const left = clampedPivot - offset;
9
+ const right = clampedPivot + offset;
10
+ if (left >= 0) {
11
+ indices.push(left);
12
+ }
13
+ if (right < size) {
14
+ indices.push(right);
15
+ }
16
+ }
17
+ return indices;
18
+ };
@@ -15,6 +15,13 @@ export const defaultBrainlinkConfig = {
15
15
  embeddingProvider: 'local',
16
16
  defaultSearchMode: 'hybrid',
17
17
  chunkSize: 1200,
18
+ searchPack: {
19
+ rowChunkSize: 5_000,
20
+ compressionLevel: 5,
21
+ useDictionary: true,
22
+ guardrailMinSavingsPercent: 8,
23
+ guardrailMaxLatencyRegressionPercent: 5
24
+ },
18
25
  agentProfiles: {}
19
26
  };
20
27
  const configFilenames = ['brainlink.config.json', '.brainlink.json'];
@@ -37,6 +44,36 @@ const sanitizeEmbeddingProvider = (value) => typeof value === 'string' && embedd
37
44
  export const sanitizeSearchMode = (value, fallback = defaultBrainlinkConfig.defaultSearchMode) => typeof value === 'string' && searchModes.has(value) ? value : fallback;
38
45
  const sanitizeAllowedVaults = (value) => Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.trim().length > 0) : [];
39
46
  const sanitizePositiveNumber = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
47
+ const sanitizeIntegerInRange = (value, fallback, minimum, maximum) => {
48
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
49
+ return fallback;
50
+ }
51
+ const rounded = Math.round(value);
52
+ if (rounded < minimum) {
53
+ return minimum;
54
+ }
55
+ if (rounded > maximum) {
56
+ return maximum;
57
+ }
58
+ return rounded;
59
+ };
60
+ const sanitizeSearchPackConfig = (value) => {
61
+ const fallback = defaultBrainlinkConfig.searchPack;
62
+ if (!isRecord(value)) {
63
+ return fallback;
64
+ }
65
+ return {
66
+ rowChunkSize: sanitizeIntegerInRange(value.rowChunkSize, fallback.rowChunkSize, 100, 100_000),
67
+ compressionLevel: sanitizeIntegerInRange(value.compressionLevel, fallback.compressionLevel, 0, 11),
68
+ useDictionary: typeof value.useDictionary === 'boolean' ? value.useDictionary : fallback.useDictionary,
69
+ guardrailMinSavingsPercent: typeof value.guardrailMinSavingsPercent === 'number' && Number.isFinite(value.guardrailMinSavingsPercent)
70
+ ? Math.max(0, Math.min(95, value.guardrailMinSavingsPercent))
71
+ : fallback.guardrailMinSavingsPercent,
72
+ guardrailMaxLatencyRegressionPercent: typeof value.guardrailMaxLatencyRegressionPercent === 'number' && Number.isFinite(value.guardrailMaxLatencyRegressionPercent)
73
+ ? Math.max(0, Math.min(300, value.guardrailMaxLatencyRegressionPercent))
74
+ : fallback.guardrailMaxLatencyRegressionPercent
75
+ };
76
+ };
40
77
  const sanitizeAgentProfile = (value) => {
41
78
  if (!isRecord(value)) {
42
79
  return null;
@@ -130,6 +167,7 @@ const sanitizeConfig = (value) => ({
130
167
  : defaultBrainlinkConfig.defaultContextTokens,
131
168
  allowedVaults: [...sanitizeAllowedVaults(value.allowedVaults), ...readAllowedVaultsFromEnv()],
132
169
  chunkSize: typeof value.chunkSize === 'number' && value.chunkSize > 0 ? value.chunkSize : defaultBrainlinkConfig.chunkSize,
170
+ searchPack: sanitizeSearchPackConfig(value.searchPack),
133
171
  embeddingProvider: sanitizeEmbeddingProvider(value.embeddingProvider),
134
172
  defaultSearchMode: sanitizeSearchMode(value.defaultSearchMode),
135
173
  agentProfiles: sanitizeAgentProfiles(value.agentProfiles)