@andespindola/brainlink 0.1.0-beta.15 → 0.1.0-beta.151
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -0
- package/CHANGELOG.md +27 -0
- package/COPYRIGHT.md +5 -0
- package/README.md +140 -9
- package/dist/application/auto-migrate-configured-vault.js +37 -0
- package/dist/application/build-context.js +64 -3
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +111 -47
- package/dist/application/frontend/client-html.js +42 -26
- package/dist/application/frontend/client-js.js +788 -554
- package/dist/application/frontend/client-render-worker-js.js +569 -0
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-layout.js +38 -5
- package/dist/application/get-graph-stream-chunk.js +289 -0
- package/dist/application/get-graph-view.js +243 -0
- package/dist/application/import-legacy-sqlite.js +296 -0
- package/dist/application/index-vault.js +262 -23
- package/dist/application/offline-pack-backup.js +44 -0
- package/dist/application/server/routes.js +187 -5
- package/dist/application/start-server.js +75 -4
- package/dist/application/watch-vault.js +23 -2
- package/dist/cli/commands/agent-commands.js +7 -0
- package/dist/cli/commands/write-commands.js +849 -10
- package/dist/cli/runtime.js +10 -2
- package/dist/domain/context.js +54 -11
- package/dist/domain/graph-layout.js +275 -3
- package/dist/domain/markdown.js +22 -9
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +117 -4
- package/dist/infrastructure/file-index.js +70 -3
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/index-state.js +58 -0
- package/dist/infrastructure/private-pack-codec.js +71 -10
- package/dist/infrastructure/search-packs.js +286 -15
- package/dist/infrastructure/vault-migration-state.js +69 -0
- package/dist/infrastructure/volatile-memory.js +100 -0
- package/dist/mcp/runtime.js +20 -0
- package/dist/mcp/server.js +29 -11
- package/dist/mcp/tools.js +119 -2
- package/docs/AGENT_USAGE.md +89 -3
- package/docs/ARCHITECTURE.md +6 -0
- package/docs/QUICKSTART.md +7 -0
- package/package.json +7 -2
package/dist/cli/runtime.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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;
|
package/dist/domain/context.js
CHANGED
|
@@ -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
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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);
|
package/dist/domain/markdown.js
CHANGED
|
@@ -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
|
|
195
|
-
const
|
|
196
|
-
|
|
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(
|
|
200
|
-
weight: linkWeights.get(
|
|
201
|
-
priority: linkWeights.get(
|
|
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]
|
|
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
|
+
};
|