@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.
Files changed (43) hide show
  1. package/AGENTS.md +3 -0
  2. package/CHANGELOG.md +27 -0
  3. package/COPYRIGHT.md +5 -0
  4. package/README.md +140 -9
  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 +262 -23
  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 +849 -10
  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 +22 -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 +29 -11
  39. package/dist/mcp/tools.js +119 -2
  40. package/docs/AGENT_USAGE.md +89 -3
  41. package/docs/ARCHITECTURE.md +6 -0
  42. package/docs/QUICKSTART.md +7 -0
  43. package/package.json +7 -2
@@ -1,11 +1,19 @@
1
- import { loadBrainlinkConfig, resolveAgentRuntimeDefaults } from '../infrastructure/config.js';
1
+ import { autoMigrateConfiguredVaultIfChanged } from '../application/auto-migrate-configured-vault.js';
2
+ import { loadBrainlinkConfigWithSource, resolveAgentRuntimeDefaults } from '../infrastructure/config.js';
2
3
  import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
3
4
  export const parsePositiveInteger = (value, fallback) => {
4
5
  const parsed = Number.parseInt(value, 10);
5
6
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
6
7
  };
7
8
  export const resolveOptions = async (options) => {
8
- const config = await loadBrainlinkConfig();
9
+ const { config, vaultSource } = await loadBrainlinkConfigWithSource();
10
+ if (options.vault === undefined) {
11
+ const sourceKey = vaultSource.sourcePath ? `${vaultSource.source}:${vaultSource.sourcePath}` : vaultSource.source;
12
+ await autoMigrateConfiguredVaultIfChanged({
13
+ configKey: sourceKey,
14
+ configuredVault: config.vault
15
+ });
16
+ }
9
17
  const vault = options.vault ?? config.vault;
10
18
  const allowedVault = assertVaultAllowed(vault, config.allowedVaults);
11
19
  const agent = options.agent ?? config.defaultAgent;
@@ -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
  };
@@ -34,6 +76,7 @@ export const formatContextPackage = (query, sections) => {
34
76
  section.tags.length > 0 ? `Tags: ${section.tags.map((tag) => `#${tag}`).join(' ')}` : null,
35
77
  `Score: ${section.score.toFixed(3)}`,
36
78
  `Mode: ${section.searchMode}`,
79
+ section.volatile ? `Volatile: true${section.expiresAt ? `, expires ${section.expiresAt}` : ''}` : null,
37
80
  '',
38
81
  section.content
39
82
  ]
@@ -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,48 @@ 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 selectHighestDegreeNodeId = (nodes, degrees) => [...nodes].sort((left, right) => {
92
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
93
+ return degreeDelta === 0 ? left.title.localeCompare(right.title) : degreeDelta;
94
+ })[0]?.id ?? null;
95
+ const centerLayoutByNode = (nodes, nodeId) => {
96
+ if (!nodeId) {
97
+ return nodes;
98
+ }
99
+ const anchor = nodes.find((node) => node.id === nodeId);
100
+ if (!anchor) {
101
+ return nodes;
102
+ }
103
+ return nodes.map((node) => ({
104
+ ...node,
105
+ x: node.x - anchor.x,
106
+ y: node.y - anchor.y
107
+ }));
108
+ };
65
109
  const naturalSegmentSeed = (node) => groupKey(node) === '00-maps' || /\b(moc|map)\b/i.test(node.title);
66
110
  const segmentName = (node) => node.title.replace(/^MOC\s+/i, '').replace(/\s+Memory Map$/i, '').trim() || node.title;
67
111
  const collectComponent = (adjacency, startId, visited) => {
@@ -128,13 +172,29 @@ const groupNodesBySegment = (nodes, segments) => {
128
172
  return new Map(groups);
129
173
  };
130
174
  const segmentAngle = (segment, index, count) => segmentAngles[segment] ?? (Math.PI * 2 * index) / Math.max(count, 1) - Math.PI / 2;
175
+ const compareByStarOrder = (levels, degrees, segmentIndexByName, segments) => (left, right) => {
176
+ const levelDelta = (levels.get(left.id) ?? 1) - (levels.get(right.id) ?? 1);
177
+ if (levelDelta !== 0)
178
+ return levelDelta;
179
+ const leftSegment = segments.get(left.id) ?? groupLabel(groupKey(left));
180
+ const rightSegment = segments.get(right.id) ?? groupLabel(groupKey(right));
181
+ const segmentDelta = (segmentIndexByName.get(leftSegment) ?? 0) - (segmentIndexByName.get(rightSegment) ?? 0);
182
+ if (segmentDelta !== 0)
183
+ return segmentDelta;
184
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
185
+ return degreeDelta === 0 ? left.title.localeCompare(right.title) : degreeDelta;
186
+ };
187
+ const petalSpreadForSegmentSize = (size) => {
188
+ const safeSize = Math.max(size, 1);
189
+ return 180 + Math.log2(safeSize + 1) * 6;
190
+ };
131
191
  const createSegmentNodes = (segments, degrees, segmentCount) => ([segment, nodes], segmentIndex) => {
132
192
  const sortedNodes = [...nodes].sort(byDegreeThenTitle(degrees));
133
193
  const angle = segmentAngle(segment, segmentIndex, segmentCount);
134
194
  const baseRadius = segmentCount === 1 ? 0 : 340 + Math.min(sortedNodes.length, 22) * 10;
135
195
  const centerX = Math.cos(angle) * baseRadius;
136
196
  const centerY = Math.sin(angle) * (baseRadius * 0.78);
137
- const petalSpread = 40 + Math.sqrt(sortedNodes.length) * 14;
197
+ const petalSpread = petalSpreadForSegmentSize(sortedNodes.length);
138
198
  return sortedNodes.map((node, index) => {
139
199
  const localAngle = index * 2.399963 + jitter(node.title, 0.42);
140
200
  const localRadius = Math.sqrt(index + 1) * petalSpread;
@@ -149,6 +209,136 @@ const createSegmentNodes = (segments, degrees, segmentCount) => ([segment, nodes
149
209
  });
150
210
  };
151
211
  const distanceBetween = (left, right) => Math.hypot(right.x - left.x, right.y - left.y);
212
+ const layoutBounds = (nodes) => {
213
+ if (nodes.length === 0) {
214
+ return { x: 0, y: 0, radius: 1 };
215
+ }
216
+ const bounds = nodes.reduce((current, node) => ({
217
+ minX: Math.min(current.minX, node.x),
218
+ maxX: Math.max(current.maxX, node.x),
219
+ minY: Math.min(current.minY, node.y),
220
+ maxY: Math.max(current.maxY, node.y)
221
+ }), {
222
+ minX: Number.POSITIVE_INFINITY,
223
+ maxX: Number.NEGATIVE_INFINITY,
224
+ minY: Number.POSITIVE_INFINITY,
225
+ maxY: Number.NEGATIVE_INFINITY
226
+ });
227
+ const x = (bounds.minX + bounds.maxX) / 2;
228
+ const y = (bounds.minY + bounds.maxY) / 2;
229
+ const radius = nodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - x, node.y - y)), 1);
230
+ return { x, y, radius: Math.max(radius + 72, 120) };
231
+ };
232
+ const edgeTouchesGroup = (edge, nodeIds) => nodeIds.has(edge.source) || Boolean(edge.target && nodeIds.has(edge.target));
233
+ const edgeInsideGroup = (edge, nodeIds) => nodeIds.has(edge.source) && Boolean(edge.target && nodeIds.has(edge.target));
234
+ const groupTitle = (segment, level, index, nodes) => nodes.length === 1
235
+ ? nodes[0]?.title ?? segment
236
+ : `${segment} ${level + 1}.${index + 1}`;
237
+ const chunkNodes = (nodes, degrees, groupNodeLimit = hierarchyGroupNodeLimit) => {
238
+ const sortedNodes = [...nodes].sort((left, right) => {
239
+ const segmentDelta = left.segment.localeCompare(right.segment);
240
+ if (segmentDelta !== 0)
241
+ return segmentDelta;
242
+ const groupDelta = left.group.localeCompare(right.group);
243
+ if (groupDelta !== 0)
244
+ return groupDelta;
245
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
246
+ if (degreeDelta !== 0)
247
+ return degreeDelta;
248
+ return left.title.localeCompare(right.title);
249
+ });
250
+ const groupCountTarget = Math.min(groupNodeLimit, sortedNodes.length);
251
+ const chunkSize = sortedNodes.length <= groupNodeLimit * groupNodeLimit
252
+ ? Math.max(1, Math.ceil(sortedNodes.length / groupCountTarget))
253
+ : groupNodeLimit;
254
+ const chunks = [];
255
+ for (let index = 0; index < sortedNodes.length; index += chunkSize) {
256
+ chunks.push(sortedNodes.slice(index, index + chunkSize));
257
+ }
258
+ return chunks;
259
+ };
260
+ const groupEdges = (edges, nodeIds) => ({
261
+ internalEdges: edges.filter((edge) => edgeInsideGroup(edge, nodeIds)),
262
+ externalEdges: edges.filter((edge) => edgeTouchesGroup(edge, nodeIds) && !edgeInsideGroup(edge, nodeIds))
263
+ });
264
+ const groupBounds = (groups) => {
265
+ if (groups.length === 0) {
266
+ return { x: 0, y: 0, radius: 1 };
267
+ }
268
+ const nodes = groups.map((group) => ({
269
+ x: group.x,
270
+ y: group.y,
271
+ radius: group.radius
272
+ }));
273
+ const x = nodes.reduce((sum, node) => sum + node.x, 0) / nodes.length;
274
+ const y = nodes.reduce((sum, node) => sum + node.y, 0) / nodes.length;
275
+ const radius = nodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - x, node.y - y) + node.radius), 1);
276
+ return { x, y, radius: Math.max(radius + 120, 180) };
277
+ };
278
+ const descendantNodeIds = (groups) => groups.flatMap((group) => group.nodeIds);
279
+ const createParentGroups = (groups, edges, level, groupNodeLimit) => {
280
+ if (groups.length <= groupNodeLimit) {
281
+ return groups;
282
+ }
283
+ const parentGroups = [];
284
+ for (let index = 0; index < groups.length; index += groupNodeLimit) {
285
+ const chunk = groups.slice(index, index + groupNodeLimit);
286
+ const nodeIds = new Set(descendantNodeIds(chunk));
287
+ const bounds = groupBounds(chunk);
288
+ const segment = chunk[0]?.segment ?? 'root';
289
+ const group = chunk[0]?.group ?? 'root';
290
+ const groupIndex = index / groupNodeLimit;
291
+ const id = ['root', level, groupIndex, chunk[0]?.id ?? 'empty', chunk.length].join(':');
292
+ const edgeGroups = groupEdges(edges, nodeIds);
293
+ parentGroups.push({
294
+ id,
295
+ level,
296
+ parentId: null,
297
+ title: `${segment} ${level + 1}.${Math.floor(groupIndex) + 1}`,
298
+ segment,
299
+ group,
300
+ x: bounds.x,
301
+ y: bounds.y,
302
+ radius: bounds.radius,
303
+ nodeIds: [],
304
+ childGroupIds: chunk.map((child) => child.id),
305
+ internalEdges: edgeGroups.internalEdges,
306
+ externalEdges: edgeGroups.externalEdges
307
+ });
308
+ }
309
+ const relinkedChildren = groups.map((group) => {
310
+ const parent = parentGroups.find((candidate) => candidate.childGroupIds.includes(group.id));
311
+ return parent ? { ...group, parentId: parent.id } : group;
312
+ });
313
+ return [...createParentGroups(parentGroups, edges, level + 1, groupNodeLimit), ...relinkedChildren];
314
+ };
315
+ export const createGraphLayoutHierarchy = (nodes, edges, degrees, groupNodeLimit = hierarchyGroupNodeLimit) => {
316
+ if (nodes.length <= groupNodeLimit) {
317
+ return [];
318
+ }
319
+ const leafGroups = chunkNodes(nodes, degrees, groupNodeLimit).map((chunk, index) => {
320
+ const nodeIds = new Set(chunk.map((node) => node.id));
321
+ const bounds = layoutBounds(chunk);
322
+ const segment = chunk[0]?.segment ?? 'root';
323
+ const group = chunk[0]?.group ?? 'root';
324
+ const id = ['leaf', 0, index, chunk[0]?.id ?? 'empty', chunk.length].join(':');
325
+ return {
326
+ id,
327
+ level: 0,
328
+ parentId: null,
329
+ title: groupTitle(segment, 0, index, chunk),
330
+ segment,
331
+ group,
332
+ x: bounds.x,
333
+ y: bounds.y,
334
+ radius: bounds.radius,
335
+ nodeIds: chunk.map((node) => node.id),
336
+ childGroupIds: [],
337
+ ...groupEdges(edges, nodeIds)
338
+ };
339
+ });
340
+ return createParentGroups(leafGroups, edges, 1, groupNodeLimit);
341
+ };
152
342
  const resolveCollisionPair = (left, right, minDistance) => {
153
343
  const dx = right.x - left.x;
154
344
  const dy = right.y - left.y;
@@ -240,15 +430,97 @@ const relaxCollisions = (nodes, minDistance = 132, rounds = 32) => {
240
430
  }
241
431
  return layoutNodes;
242
432
  };
433
+ const assignStarLevels = (nodes, edges, hubId) => {
434
+ if (!hubId) {
435
+ return new Map(nodes.map((node) => [node.id, 1]));
436
+ }
437
+ const adjacency = createAdjacency(nodes, edges);
438
+ const levels = new Map([[hubId, 0]]);
439
+ const queue = [hubId];
440
+ for (let index = 0; index < queue.length; index += 1) {
441
+ const id = queue[index];
442
+ const nextLevel = (levels.get(id) ?? 0) + 1;
443
+ (adjacency.get(id) ?? []).forEach((nextId) => {
444
+ if (!levels.has(nextId)) {
445
+ levels.set(nextId, nextLevel);
446
+ queue.push(nextId);
447
+ }
448
+ });
449
+ }
450
+ nodes.forEach((node) => {
451
+ if (!levels.has(node.id)) {
452
+ levels.set(node.id, 2);
453
+ }
454
+ });
455
+ return levels;
456
+ };
457
+ const createStarNodes = (nodes, segments, degrees, hubId, levels) => {
458
+ const segmentNames = Array.from(new Set(nodes.map((node) => segments.get(node.id) ?? groupLabel(groupKey(node)))))
459
+ .sort((left, right) => segmentAngle(left, 0, 1) - segmentAngle(right, 0, 1) || left.localeCompare(right));
460
+ const segmentIndexByName = new Map(segmentNames.map((segment, index) => [segment, index]));
461
+ const sortedNodes = [...nodes].sort(compareByStarOrder(levels, degrees, segmentIndexByName, segments));
462
+ const nodesByLevel = sortedNodes.reduce((state, node) => {
463
+ const level = node.id === hubId ? 0 : Math.max(1, levels.get(node.id) ?? 1);
464
+ const levelNodes = state.get(level) ?? [];
465
+ levelNodes.push(node);
466
+ state.set(level, levelNodes);
467
+ return state;
468
+ }, new Map());
469
+ return Array.from(nodesByLevel.entries())
470
+ .sort(([left], [right]) => left - right)
471
+ .flatMap(([level, levelNodes]) => {
472
+ if (level === 0) {
473
+ return levelNodes.map((node) => ({
474
+ ...node,
475
+ group: groupLabel(groupKey(node)),
476
+ segment: segments.get(node.id) ?? groupLabel(groupKey(node)),
477
+ x: 0,
478
+ y: 0
479
+ }));
480
+ }
481
+ const ringRadius = Math.max(300 + (level - 1) * 360, (levelNodes.length * 112) / (Math.PI * 2));
482
+ return levelNodes.map((node, index) => {
483
+ const segment = segments.get(node.id) ?? groupLabel(groupKey(node));
484
+ const segmentOffset = (segmentIndexByName.get(segment) ?? 0) / Math.max(segmentNames.length, 1);
485
+ const angle = Math.PI * 2 * ((index / Math.max(levelNodes.length, 1) + segmentOffset * 0.18) % 1) - Math.PI / 2;
486
+ const radialJitter = jitter(node.id, 48);
487
+ return {
488
+ ...node,
489
+ group: groupLabel(groupKey(node)),
490
+ segment,
491
+ x: Math.cos(angle) * (ringRadius + radialJitter) + jitter(node.title, 18),
492
+ y: Math.sin(angle) * (ringRadius + radialJitter) + jitter(node.path, 18)
493
+ };
494
+ });
495
+ });
496
+ };
497
+ export const createStarGraphLayout = (graph) => {
498
+ const degrees = countDegrees(graph.edges);
499
+ const hubId = selectPrimaryHubId(graph.nodes, degrees) ?? selectHighestDegreeNodeId(graph.nodes, degrees);
500
+ const segments = assignSegments(graph.nodes, graph.edges, degrees);
501
+ const levels = assignStarLevels(graph.nodes, graph.edges, hubId);
502
+ const nodes = relaxCollisions(createStarNodes(graph.nodes, segments, degrees, hubId, levels), 132, 18);
503
+ const centeredNodes = centerLayoutByNode(nodes, hubId);
504
+ const groups = createGraphLayoutHierarchy(centeredNodes, graph.edges, degrees);
505
+ return {
506
+ nodes: centeredNodes,
507
+ edges: graph.edges,
508
+ ...(groups.length > 0 ? { groups } : {})
509
+ };
510
+ };
243
511
  export const createCauliflowerGraphLayout = (graph) => {
244
512
  const degrees = countDegrees(graph.edges);
245
513
  const segments = assignSegments(graph.nodes, graph.edges, degrees);
246
514
  const segmentGroups = Array.from(groupNodesBySegment(graph.nodes, segments).entries())
247
515
  .sort(([left], [right]) => left.localeCompare(right));
248
516
  const nodes = relaxCollisions(segmentGroups.flatMap(createSegmentNodes(segments, degrees, segmentGroups.length)));
517
+ const primaryHubId = selectPrimaryHubId(graph.nodes, degrees);
518
+ const centeredNodes = centerLayoutByNode(nodes, primaryHubId);
519
+ const groups = createGraphLayoutHierarchy(centeredNodes, graph.edges, degrees);
249
520
  return {
250
- nodes,
251
- edges: graph.edges
521
+ nodes: centeredNodes,
522
+ edges: graph.edges,
523
+ ...(groups.length > 0 ? { groups } : {})
252
524
  };
253
525
  };
254
526
  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);
@@ -18,6 +18,7 @@ const priorityBoosts = {
18
18
  high: 3,
19
19
  critical: 6
20
20
  };
21
+ export const graphLinkModelVersion = 3;
21
22
  const priorityPatterns = [
22
23
  ['critical', /\b(?:priority|prioridade|importance|importancia|importância)\s*[:=]\s*(?:critical|critica|crítica|urgent|urgente|p0)\b/i],
23
24
  ['critical', /#(?:critical|critica|crítica|urgent|urgente|p0)\b/i],
@@ -100,6 +101,18 @@ export const extractWikiLinkWeights = (content) => {
100
101
  }, new Map());
101
102
  return Array.from(weights.values());
102
103
  };
104
+ const compareGraphLinks = (left, right) => {
105
+ const priorityDelta = priorityRanks[right.priority] - priorityRanks[left.priority];
106
+ if (priorityDelta !== 0)
107
+ return priorityDelta;
108
+ const weightDelta = right.weight - left.weight;
109
+ if (weightDelta !== 0)
110
+ return weightDelta;
111
+ return left.title.localeCompare(right.title);
112
+ };
113
+ export const selectGraphWikiLinkWeights = (links) => {
114
+ return [...links].sort(compareGraphLinks);
115
+ };
103
116
  const extractTitle = (filePath, content, frontmatter) => {
104
117
  if (frontmatter.title) {
105
118
  return normalizeTitle(frontmatter.title);
@@ -191,16 +204,16 @@ export const parseMarkdownDocument = (input) => {
191
204
  };
192
205
  export const createIndexedDocument = (document, titleToDocumentId, maxChunkCharacters = 1200) => {
193
206
  const chunks = splitIntoChunks(document.id, document.content, maxChunkCharacters);
194
- const linkWeights = new Map(extractWikiLinkWeights(document.content).map((link) => [link.title.toLowerCase(), link]));
195
- const links = document.links
196
- .map((toTitle) => ({
207
+ const graphLinkWeights = selectGraphWikiLinkWeights(extractWikiLinkWeights(document.content).filter((link) => link.title.toLowerCase() !== document.title.toLowerCase()));
208
+ const linkWeights = new Map(graphLinkWeights.map((link) => [link.title.toLowerCase(), link]));
209
+ const links = graphLinkWeights
210
+ .map((link) => ({
197
211
  fromDocumentId: document.id,
198
- toTitle,
199
- toDocumentId: titleToDocumentId.get(toTitle.toLowerCase()) ?? null,
200
- weight: linkWeights.get(toTitle.toLowerCase())?.weight ?? 1,
201
- priority: linkWeights.get(toTitle.toLowerCase())?.priority ?? 'normal'
202
- }))
203
- .filter((link) => link.toDocumentId !== document.id);
212
+ toTitle: link.title,
213
+ toDocumentId: titleToDocumentId.get(link.title.toLowerCase()) ?? null,
214
+ weight: linkWeights.get(link.title.toLowerCase())?.weight ?? 1,
215
+ priority: linkWeights.get(link.title.toLowerCase())?.priority ?? 'normal'
216
+ }));
204
217
  return {
205
218
  document,
206
219
  chunks,
@@ -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)
@@ -143,12 +181,87 @@ export const resolveAgentRuntimeDefaults = (config, agent) => {
143
181
  defaultSearchMode: profile?.defaultSearchMode ?? config.defaultSearchMode
144
182
  };
145
183
  };
184
+ const mergeConfigLayers = (layers) => layers.reduce((state, config) => ({
185
+ ...state,
186
+ ...config
187
+ }), {});
188
+ export const getVaultConfigSourceDetails = async (cwd = safeCwd()) => {
189
+ const [globalConfig, localConfig, legacyLocalConfig] = await Promise.all([
190
+ loadRawConfig('global', cwd),
191
+ loadRawConfig('local', cwd),
192
+ loadLegacyLocalRawConfig(cwd)
193
+ ]);
194
+ if (typeof legacyLocalConfig.vault === 'string' && legacyLocalConfig.vault.trim().length > 0) {
195
+ return {
196
+ source: 'local-legacy',
197
+ sourcePath: resolve(cwd, '.brainlink.json')
198
+ };
199
+ }
200
+ if (typeof localConfig.vault === 'string' && localConfig.vault.trim().length > 0) {
201
+ return {
202
+ source: 'local',
203
+ sourcePath: getLocalConfigPath(cwd)
204
+ };
205
+ }
206
+ if (typeof globalConfig.vault === 'string' && globalConfig.vault.trim().length > 0) {
207
+ return {
208
+ source: 'global',
209
+ sourcePath: getGlobalConfigPath()
210
+ };
211
+ }
212
+ return {
213
+ source: 'default',
214
+ sourcePath: null
215
+ };
216
+ };
146
217
  export const loadBrainlinkConfig = async (cwd = safeCwd()) => {
147
218
  const globalConfig = await readJsonConfig(getGlobalConfigPath());
148
219
  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
- }), {});
220
+ const merged = mergeConfigLayers([globalConfig, ...localConfigs]);
153
221
  return sanitizeConfig(merged);
154
222
  };
223
+ export const loadBrainlinkConfigWithSource = async (cwd = safeCwd()) => {
224
+ const globalConfigPath = getGlobalConfigPath();
225
+ const localConfigPath = getLocalConfigPath(cwd);
226
+ const legacyLocalConfigPath = resolve(cwd, '.brainlink.json');
227
+ const [globalConfig, localConfig, legacyLocalConfig] = await Promise.all([
228
+ readJsonConfig(globalConfigPath),
229
+ readJsonConfig(localConfigPath),
230
+ readJsonConfig(legacyLocalConfigPath)
231
+ ]);
232
+ const config = sanitizeConfig(mergeConfigLayers([globalConfig, localConfig, legacyLocalConfig]));
233
+ if (typeof legacyLocalConfig.vault === 'string' && legacyLocalConfig.vault.trim().length > 0) {
234
+ return {
235
+ config,
236
+ vaultSource: {
237
+ source: 'local-legacy',
238
+ sourcePath: legacyLocalConfigPath
239
+ }
240
+ };
241
+ }
242
+ if (typeof localConfig.vault === 'string' && localConfig.vault.trim().length > 0) {
243
+ return {
244
+ config,
245
+ vaultSource: {
246
+ source: 'local',
247
+ sourcePath: localConfigPath
248
+ }
249
+ };
250
+ }
251
+ if (typeof globalConfig.vault === 'string' && globalConfig.vault.trim().length > 0) {
252
+ return {
253
+ config,
254
+ vaultSource: {
255
+ source: 'global',
256
+ sourcePath: globalConfigPath
257
+ }
258
+ };
259
+ }
260
+ return {
261
+ config,
262
+ vaultSource: {
263
+ source: 'default',
264
+ sourcePath: null
265
+ }
266
+ };
267
+ };