@andespindola/brainlink 0.1.0-beta.141 → 0.1.0-beta.143
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/README.md +7 -5
- package/dist/application/auto-migrate-configured-vault.js +37 -0
- package/dist/application/frontend/client-css.js +1 -6
- package/dist/application/frontend/client-html.js +0 -1
- package/dist/application/frontend/client-js.js +580 -3334
- package/dist/application/frontend/client-render-worker-js.js +534 -0
- package/dist/application/get-graph-stream-chunk.js +285 -0
- package/dist/application/server/routes.js +31 -0
- package/dist/cli/runtime.js +10 -2
- package/dist/infrastructure/config.js +79 -4
- package/dist/infrastructure/vault-migration-state.js +69 -0
- package/package.json +1 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { getGraphLayout } from './get-graph-layout.js';
|
|
2
|
+
const layoutCacheBySignature = new Map();
|
|
3
|
+
const maxLayoutCacheEntries = 6;
|
|
4
|
+
const farScaleThreshold = 0.22;
|
|
5
|
+
const midScaleThreshold = 0.78;
|
|
6
|
+
const viewportPaddingFactor = 0.18;
|
|
7
|
+
const maxNearEdgePerNode = 24;
|
|
8
|
+
const maxMidEdgePerNode = 12;
|
|
9
|
+
const maxFarEdgePerCluster = 8;
|
|
10
|
+
const inViewport = (item, input, padding) => {
|
|
11
|
+
const radius = item.radius ?? 24;
|
|
12
|
+
const paddedX = input.x - padding;
|
|
13
|
+
const paddedY = input.y - padding;
|
|
14
|
+
const paddedWidth = input.width + padding * 2;
|
|
15
|
+
const paddedHeight = input.height + padding * 2;
|
|
16
|
+
return (item.x + radius >= paddedX &&
|
|
17
|
+
item.x - radius <= paddedX + paddedWidth &&
|
|
18
|
+
item.y + radius >= paddedY &&
|
|
19
|
+
item.y - radius <= paddedY + paddedHeight);
|
|
20
|
+
};
|
|
21
|
+
const nodeDistanceToCenter = (item, input) => {
|
|
22
|
+
const cx = input.x + input.width / 2;
|
|
23
|
+
const cy = input.y + input.height / 2;
|
|
24
|
+
return Math.hypot(item.x - cx, item.y - cy);
|
|
25
|
+
};
|
|
26
|
+
const rankNodeRelevance = (node, input, degrees) => {
|
|
27
|
+
const degree = degrees.get(node.id) ?? 0;
|
|
28
|
+
const centerDistance = nodeDistanceToCenter(node, input);
|
|
29
|
+
const viewportRadius = Math.max(input.width, input.height) / 2;
|
|
30
|
+
const centerScore = 1 - Math.min(1, centerDistance / Math.max(viewportRadius, 1));
|
|
31
|
+
const degreeScore = Math.log1p(Math.max(0, degree));
|
|
32
|
+
const tagScore = Math.min(node.tags.length, 6) * 0.18;
|
|
33
|
+
return degreeScore * 1.1 + centerScore * 1.3 + tagScore;
|
|
34
|
+
};
|
|
35
|
+
const rankGroupRelevance = (group, input) => {
|
|
36
|
+
const centerDistance = nodeDistanceToCenter(group, input);
|
|
37
|
+
const viewportRadius = Math.max(input.width, input.height) / 2;
|
|
38
|
+
const centerScore = 1 - Math.min(1, centerDistance / Math.max(viewportRadius, 1));
|
|
39
|
+
const massScore = Math.log1p(group.nodeIds.length + group.childGroupIds.length * 2);
|
|
40
|
+
const edgeScore = Math.log1p(group.externalEdges.length + group.internalEdges.length);
|
|
41
|
+
return centerScore * 1.3 + massScore + edgeScore * 0.5;
|
|
42
|
+
};
|
|
43
|
+
const edgePriorityScore = (priority) => {
|
|
44
|
+
if (priority === 'critical')
|
|
45
|
+
return 4;
|
|
46
|
+
if (priority === 'high')
|
|
47
|
+
return 3;
|
|
48
|
+
if (priority === 'normal')
|
|
49
|
+
return 2;
|
|
50
|
+
return 1;
|
|
51
|
+
};
|
|
52
|
+
const edgeRank = (edge) => edge.weight * 1.35 + edgePriorityScore(edge.priority);
|
|
53
|
+
const createLayoutCache = (signature, nodes, edges, groups) => {
|
|
54
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
55
|
+
const groupById = new Map(groups.map((group) => [group.id, group]));
|
|
56
|
+
const degrees = new Map();
|
|
57
|
+
const adjacencyByNodeId = new Map();
|
|
58
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
59
|
+
const edge = edges[index];
|
|
60
|
+
degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + edge.weight);
|
|
61
|
+
const sourceAdjacency = adjacencyByNodeId.get(edge.source) ?? [];
|
|
62
|
+
sourceAdjacency.push(index);
|
|
63
|
+
adjacencyByNodeId.set(edge.source, sourceAdjacency);
|
|
64
|
+
if (!edge.target) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + edge.weight);
|
|
68
|
+
const targetAdjacency = adjacencyByNodeId.get(edge.target) ?? [];
|
|
69
|
+
targetAdjacency.push(index);
|
|
70
|
+
adjacencyByNodeId.set(edge.target, targetAdjacency);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
signature,
|
|
74
|
+
degrees,
|
|
75
|
+
nodeById,
|
|
76
|
+
groupById,
|
|
77
|
+
adjacencyByNodeId
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const getOrCreateLayoutCache = (signature, nodes, edges, groups) => {
|
|
81
|
+
const cached = layoutCacheBySignature.get(signature);
|
|
82
|
+
if (cached) {
|
|
83
|
+
return cached;
|
|
84
|
+
}
|
|
85
|
+
const next = createLayoutCache(signature, nodes, edges, groups);
|
|
86
|
+
layoutCacheBySignature.set(signature, next);
|
|
87
|
+
while (layoutCacheBySignature.size > maxLayoutCacheEntries) {
|
|
88
|
+
const oldest = layoutCacheBySignature.keys().next().value;
|
|
89
|
+
if (!oldest)
|
|
90
|
+
break;
|
|
91
|
+
layoutCacheBySignature.delete(oldest);
|
|
92
|
+
}
|
|
93
|
+
return next;
|
|
94
|
+
};
|
|
95
|
+
const selectTopByRelevance = (items, relevanceById, budget) => [...items]
|
|
96
|
+
.sort((left, right) => {
|
|
97
|
+
const relevanceDelta = (relevanceById.get(right.id) ?? 0) - (relevanceById.get(left.id) ?? 0);
|
|
98
|
+
if (relevanceDelta !== 0)
|
|
99
|
+
return relevanceDelta;
|
|
100
|
+
return left.id.localeCompare(right.id);
|
|
101
|
+
})
|
|
102
|
+
.slice(0, Math.max(1, budget));
|
|
103
|
+
const selectNearNodes = (nodes, cache, input) => {
|
|
104
|
+
const padding = Math.max(input.width, input.height) * viewportPaddingFactor;
|
|
105
|
+
const viewportNodes = nodes.filter((node) => inViewport(node, input, padding));
|
|
106
|
+
const relevance = new Map();
|
|
107
|
+
for (let index = 0; index < viewportNodes.length; index += 1) {
|
|
108
|
+
const node = viewportNodes[index];
|
|
109
|
+
relevance.set(node.id, rankNodeRelevance(node, input, cache.degrees));
|
|
110
|
+
}
|
|
111
|
+
const selected = selectTopByRelevance(viewportNodes, relevance, input.nodeBudget);
|
|
112
|
+
return selected.map((node) => [
|
|
113
|
+
node.id,
|
|
114
|
+
node.title,
|
|
115
|
+
node.x,
|
|
116
|
+
node.y,
|
|
117
|
+
node.group,
|
|
118
|
+
node.segment,
|
|
119
|
+
'node',
|
|
120
|
+
relevance.get(node.id) ?? 0
|
|
121
|
+
]);
|
|
122
|
+
};
|
|
123
|
+
const selectMidNodes = (nodes, cache, input) => {
|
|
124
|
+
const padding = Math.max(input.width, input.height) * (viewportPaddingFactor + 0.08);
|
|
125
|
+
const viewportNodes = nodes.filter((node) => inViewport(node, input, padding));
|
|
126
|
+
const relevance = new Map();
|
|
127
|
+
for (let index = 0; index < viewportNodes.length; index += 1) {
|
|
128
|
+
const node = viewportNodes[index];
|
|
129
|
+
relevance.set(node.id, rankNodeRelevance(node, input, cache.degrees) * 1.1);
|
|
130
|
+
}
|
|
131
|
+
const selected = selectTopByRelevance(viewportNodes, relevance, input.nodeBudget);
|
|
132
|
+
return selected.map((node) => [
|
|
133
|
+
node.id,
|
|
134
|
+
node.title,
|
|
135
|
+
node.x,
|
|
136
|
+
node.y,
|
|
137
|
+
node.group,
|
|
138
|
+
node.segment,
|
|
139
|
+
'node',
|
|
140
|
+
relevance.get(node.id) ?? 0
|
|
141
|
+
]);
|
|
142
|
+
};
|
|
143
|
+
const selectFarClusters = (groups, input, nodeBudget) => {
|
|
144
|
+
const roots = groups.filter((group) => group.parentId === null);
|
|
145
|
+
const padding = Math.max(input.width, input.height) * 0.12;
|
|
146
|
+
const candidates = roots.filter((group) => inViewport(group, input, padding));
|
|
147
|
+
const relevance = new Map();
|
|
148
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
149
|
+
const group = candidates[index];
|
|
150
|
+
relevance.set(group.id, rankGroupRelevance(group, input));
|
|
151
|
+
}
|
|
152
|
+
const selected = selectTopByRelevance(candidates, relevance, nodeBudget);
|
|
153
|
+
return selected.map((group) => [
|
|
154
|
+
`cluster:${group.id}`,
|
|
155
|
+
group.title,
|
|
156
|
+
group.x,
|
|
157
|
+
group.y,
|
|
158
|
+
group.group,
|
|
159
|
+
group.segment,
|
|
160
|
+
'cluster',
|
|
161
|
+
relevance.get(group.id) ?? 0
|
|
162
|
+
]);
|
|
163
|
+
};
|
|
164
|
+
const collectEdgesForNodes = (allEdges, cache, nodeRows, edgeBudget, maxEdgesPerNode) => {
|
|
165
|
+
const isClusterMode = nodeRows.length > 0 && nodeRows[0]?.[6] === 'cluster';
|
|
166
|
+
if (isClusterMode) {
|
|
167
|
+
const clusterIds = new Set(nodeRows.map((row) => row[0].replace(/^cluster:/, '')));
|
|
168
|
+
const clusterEdges = new Map();
|
|
169
|
+
for (let index = 0; index < allEdges.length; index += 1) {
|
|
170
|
+
const edge = allEdges[index];
|
|
171
|
+
if (!edge.target)
|
|
172
|
+
continue;
|
|
173
|
+
const sourceNode = cache.nodeById.get(edge.source);
|
|
174
|
+
const targetNode = cache.nodeById.get(edge.target);
|
|
175
|
+
if (!sourceNode || !targetNode)
|
|
176
|
+
continue;
|
|
177
|
+
const sourceCluster = sourceNode.group;
|
|
178
|
+
const targetCluster = targetNode.group;
|
|
179
|
+
if (!clusterIds.has(sourceCluster) || !clusterIds.has(targetCluster) || sourceCluster === targetCluster)
|
|
180
|
+
continue;
|
|
181
|
+
const key = sourceCluster < targetCluster ? `${sourceCluster}|${targetCluster}` : `${targetCluster}|${sourceCluster}`;
|
|
182
|
+
const current = clusterEdges.get(key);
|
|
183
|
+
if (!current || edgeRank(edge) > edgeRank(current)) {
|
|
184
|
+
clusterEdges.set(key, edge);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return [...clusterEdges.values()]
|
|
188
|
+
.sort((left, right) => edgeRank(right) - edgeRank(left))
|
|
189
|
+
.slice(0, Math.max(1, edgeBudget))
|
|
190
|
+
.map((edge) => [
|
|
191
|
+
`cluster:${cache.nodeById.get(edge.source)?.group ?? ''}`,
|
|
192
|
+
`cluster:${cache.nodeById.get(edge.target ?? '')?.group ?? ''}`,
|
|
193
|
+
edge.weight,
|
|
194
|
+
edge.priority
|
|
195
|
+
])
|
|
196
|
+
.filter((edge) => edge[0] !== edge[1] && edge[0] !== 'cluster:' && edge[1] !== 'cluster:');
|
|
197
|
+
}
|
|
198
|
+
const nodeIds = new Set(nodeRows.map((row) => row[0]));
|
|
199
|
+
const collected = new Map();
|
|
200
|
+
for (const nodeId of nodeIds) {
|
|
201
|
+
const adjacency = cache.adjacencyByNodeId.get(nodeId);
|
|
202
|
+
if (!adjacency || adjacency.length === 0) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
let perNodeCount = 0;
|
|
206
|
+
const rankedAdjacency = [...adjacency].sort((leftIndex, rightIndex) => edgeRank(allEdges[rightIndex]) - edgeRank(allEdges[leftIndex]));
|
|
207
|
+
for (let index = 0; index < rankedAdjacency.length; index += 1) {
|
|
208
|
+
if (perNodeCount >= maxEdgesPerNode) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
const edge = allEdges[rankedAdjacency[index]];
|
|
212
|
+
if (!edge.target || !nodeIds.has(edge.source) || !nodeIds.has(edge.target)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const key = edge.source < edge.target ? `${edge.source}|${edge.target}` : `${edge.target}|${edge.source}`;
|
|
216
|
+
const current = collected.get(key);
|
|
217
|
+
if (!current || edgeRank(edge) > edgeRank(current)) {
|
|
218
|
+
collected.set(key, edge);
|
|
219
|
+
}
|
|
220
|
+
perNodeCount += 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return [...collected.values()]
|
|
224
|
+
.sort((left, right) => edgeRank(right) - edgeRank(left))
|
|
225
|
+
.slice(0, Math.max(1, edgeBudget))
|
|
226
|
+
.map((edge) => [edge.source, edge.target ?? '', edge.weight, edge.priority])
|
|
227
|
+
.filter((edge) => edge[1].length > 0);
|
|
228
|
+
};
|
|
229
|
+
const normalizeBudget = (value, fallback, min, max) => {
|
|
230
|
+
if (!Number.isFinite(value)) {
|
|
231
|
+
return fallback;
|
|
232
|
+
}
|
|
233
|
+
const rounded = Math.round(value);
|
|
234
|
+
if (rounded < min)
|
|
235
|
+
return min;
|
|
236
|
+
if (rounded > max)
|
|
237
|
+
return max;
|
|
238
|
+
return rounded;
|
|
239
|
+
};
|
|
240
|
+
export const getGraphStreamChunk = async (vaultPath, input) => {
|
|
241
|
+
const nodeBudget = normalizeBudget(input.nodeBudget, 1800, 80, 12_000);
|
|
242
|
+
const edgeBudget = normalizeBudget(input.edgeBudget, 5000, 120, 60_000);
|
|
243
|
+
const { signature, layout } = await getGraphLayout(vaultPath, input.agentId);
|
|
244
|
+
const groups = layout.groups ?? [];
|
|
245
|
+
const cache = getOrCreateLayoutCache(signature, layout.nodes, layout.edges, groups);
|
|
246
|
+
if (layout.nodes.length === 0) {
|
|
247
|
+
return {
|
|
248
|
+
signature,
|
|
249
|
+
mode: 'near',
|
|
250
|
+
nodes: [],
|
|
251
|
+
edges: [],
|
|
252
|
+
totals: {
|
|
253
|
+
nodes: 0,
|
|
254
|
+
edges: 0
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const mode = input.scale < farScaleThreshold
|
|
259
|
+
? 'far'
|
|
260
|
+
: input.scale < midScaleThreshold
|
|
261
|
+
? 'mid'
|
|
262
|
+
: 'near';
|
|
263
|
+
const nodes = mode === 'far' && groups.length > 0
|
|
264
|
+
? selectFarClusters(groups, input, nodeBudget)
|
|
265
|
+
: mode === 'mid'
|
|
266
|
+
? selectMidNodes(layout.nodes, cache, {
|
|
267
|
+
...input,
|
|
268
|
+
nodeBudget
|
|
269
|
+
})
|
|
270
|
+
: selectNearNodes(layout.nodes, cache, {
|
|
271
|
+
...input,
|
|
272
|
+
nodeBudget
|
|
273
|
+
});
|
|
274
|
+
const edges = collectEdgesForNodes(layout.edges, cache, nodes, edgeBudget, mode === 'near' ? maxNearEdgePerNode : mode === 'mid' ? maxMidEdgePerNode : maxFarEdgePerCluster);
|
|
275
|
+
return {
|
|
276
|
+
signature,
|
|
277
|
+
mode,
|
|
278
|
+
nodes,
|
|
279
|
+
edges,
|
|
280
|
+
totals: {
|
|
281
|
+
nodes: layout.nodes.length,
|
|
282
|
+
edges: layout.edges.length
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
};
|
|
@@ -4,6 +4,7 @@ import { getGraph } from '../get-graph.js';
|
|
|
4
4
|
import { getGraphNode } from '../get-graph-node.js';
|
|
5
5
|
import { getGraphLayout } from '../get-graph-layout.js';
|
|
6
6
|
import { getGraphView } from '../get-graph-view.js';
|
|
7
|
+
import { getGraphStreamChunk } from '../get-graph-stream-chunk.js';
|
|
7
8
|
import { listAgents } from '../list-agents.js';
|
|
8
9
|
import { listBacklinks, listLinks } from '../list-links.js';
|
|
9
10
|
import { searchGraphNodeIds } from '../search-graph-node-ids.js';
|
|
@@ -13,6 +14,7 @@ import { createClientCss } from '../frontend/client-css.js';
|
|
|
13
14
|
import { createClientHtml } from '../frontend/client-html.js';
|
|
14
15
|
import { createClientJs } from '../frontend/client-js.js';
|
|
15
16
|
import { createClientWorkerJs } from '../frontend/client-worker-js.js';
|
|
17
|
+
import { createClientRenderWorkerJs } from '../frontend/client-render-worker-js.js';
|
|
16
18
|
import { contentTypes, createJsonResponse, isReadMethod, parsePositiveInteger } from './http.js';
|
|
17
19
|
const readSearchMode = async (url) => {
|
|
18
20
|
const config = await loadBrainlinkConfig();
|
|
@@ -65,6 +67,7 @@ let cachedClientHtml = null;
|
|
|
65
67
|
let cachedClientCss = null;
|
|
66
68
|
let cachedClientJs = null;
|
|
67
69
|
let cachedClientWorkerJs = null;
|
|
70
|
+
let cachedClientRenderWorkerJs = null;
|
|
68
71
|
const readClientHtml = () => {
|
|
69
72
|
if (cachedClientHtml === null) {
|
|
70
73
|
cachedClientHtml = createClientHtml();
|
|
@@ -89,6 +92,12 @@ const readClientWorkerJs = () => {
|
|
|
89
92
|
}
|
|
90
93
|
return cachedClientWorkerJs;
|
|
91
94
|
};
|
|
95
|
+
const readClientRenderWorkerJs = () => {
|
|
96
|
+
if (cachedClientRenderWorkerJs === null) {
|
|
97
|
+
cachedClientRenderWorkerJs = createClientRenderWorkerJs();
|
|
98
|
+
}
|
|
99
|
+
return cachedClientRenderWorkerJs;
|
|
100
|
+
};
|
|
92
101
|
const compactGraphLayoutEdgeLimitFor = (nodeCount) => {
|
|
93
102
|
if (nodeCount > 100_000)
|
|
94
103
|
return 15_000;
|
|
@@ -202,6 +211,9 @@ export const route = async (request, url, vaultPath) => {
|
|
|
202
211
|
if (isReadMethod(request) && url.pathname === '/app-worker.js') {
|
|
203
212
|
return createResponse(readClientWorkerJs(), 200, contentTypes['.js']);
|
|
204
213
|
}
|
|
214
|
+
if (isReadMethod(request) && url.pathname === '/render-worker.js') {
|
|
215
|
+
return createResponse(readClientRenderWorkerJs(), 200, contentTypes['.js']);
|
|
216
|
+
}
|
|
205
217
|
if (isReadMethod(request) && url.pathname === '/api/graph') {
|
|
206
218
|
return createResponse(createJsonResponse(await getGraph(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
|
|
207
219
|
}
|
|
@@ -244,6 +256,25 @@ export const route = async (request, url, vaultPath) => {
|
|
|
244
256
|
agentId: readAgentQuery(url)
|
|
245
257
|
})), 200, contentTypes['.json']);
|
|
246
258
|
}
|
|
259
|
+
if (isReadMethod(request) && url.pathname === '/api/graph-stream') {
|
|
260
|
+
const x = parseNumber(url.searchParams.get('x'), -1000);
|
|
261
|
+
const y = parseNumber(url.searchParams.get('y'), -1000);
|
|
262
|
+
const width = parseNumber(url.searchParams.get('w'), 2000);
|
|
263
|
+
const height = parseNumber(url.searchParams.get('h'), 2000);
|
|
264
|
+
const scale = parseNumber(url.searchParams.get('scale'), 0.24);
|
|
265
|
+
const nodeBudget = parsePositiveInteger(url.searchParams.get('nodeBudget'), 1800);
|
|
266
|
+
const edgeBudget = parsePositiveInteger(url.searchParams.get('edgeBudget'), 5000);
|
|
267
|
+
return createResponse(createJsonResponse(await getGraphStreamChunk(vaultPath, {
|
|
268
|
+
x,
|
|
269
|
+
y,
|
|
270
|
+
width,
|
|
271
|
+
height,
|
|
272
|
+
scale,
|
|
273
|
+
nodeBudget,
|
|
274
|
+
edgeBudget,
|
|
275
|
+
agentId: readAgentQuery(url)
|
|
276
|
+
})), 200, contentTypes['.json']);
|
|
277
|
+
}
|
|
247
278
|
if (isReadMethod(request) && url.pathname === '/api/graph-node') {
|
|
248
279
|
const id = url.searchParams.get('id')?.trim() ?? '';
|
|
249
280
|
if (!id) {
|
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;
|
|
@@ -181,12 +181,87 @@ export const resolveAgentRuntimeDefaults = (config, agent) => {
|
|
|
181
181
|
defaultSearchMode: profile?.defaultSearchMode ?? config.defaultSearchMode
|
|
182
182
|
};
|
|
183
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
|
+
};
|
|
184
217
|
export const loadBrainlinkConfig = async (cwd = safeCwd()) => {
|
|
185
218
|
const globalConfig = await readJsonConfig(getGlobalConfigPath());
|
|
186
219
|
const localConfigs = await Promise.all(configFilenames.map((filename) => readJsonConfig(resolve(cwd, filename))));
|
|
187
|
-
const merged = [globalConfig, ...localConfigs]
|
|
188
|
-
...state,
|
|
189
|
-
...config
|
|
190
|
-
}), {});
|
|
220
|
+
const merged = mergeConfigLayers([globalConfig, ...localConfigs]);
|
|
191
221
|
return sanitizeConfig(merged);
|
|
192
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
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { getBrainlinkHomePath } from './paths.js';
|
|
4
|
+
const defaultState = {
|
|
5
|
+
byConfigKey: {}
|
|
6
|
+
};
|
|
7
|
+
const statePath = () => join(getBrainlinkHomePath(), 'vault-migration-state.json');
|
|
8
|
+
const sanitizeState = (value) => {
|
|
9
|
+
if (typeof value !== 'object' || value === null) {
|
|
10
|
+
return defaultState;
|
|
11
|
+
}
|
|
12
|
+
const record = value;
|
|
13
|
+
const byConfigKeyRecord = typeof record.byConfigKey === 'object' && record.byConfigKey !== null ? record.byConfigKey : {};
|
|
14
|
+
const byConfigKey = Object.entries(byConfigKeyRecord).reduce((state, [key, vault]) => {
|
|
15
|
+
if (typeof key !== 'string' || key.trim().length === 0) {
|
|
16
|
+
return state;
|
|
17
|
+
}
|
|
18
|
+
if (typeof vault !== 'string' || vault.trim().length === 0) {
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
...state,
|
|
23
|
+
[key]: vault.trim()
|
|
24
|
+
};
|
|
25
|
+
}, {});
|
|
26
|
+
return {
|
|
27
|
+
byConfigKey
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const readState = async () => {
|
|
31
|
+
try {
|
|
32
|
+
const raw = await readFile(statePath(), 'utf8');
|
|
33
|
+
return sanitizeState(JSON.parse(raw));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
37
|
+
return defaultState;
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const writeState = async (state) => {
|
|
43
|
+
const path = statePath();
|
|
44
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
45
|
+
await writeFile(path, `${JSON.stringify(state, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
46
|
+
};
|
|
47
|
+
export const getVaultMigrationStatePath = () => statePath();
|
|
48
|
+
export const getLastConfiguredVaultForKey = async (configKey) => {
|
|
49
|
+
const state = await readState();
|
|
50
|
+
const value = state.byConfigKey[configKey];
|
|
51
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
52
|
+
};
|
|
53
|
+
export const setLastConfiguredVaultForKey = async (configKey, vault) => {
|
|
54
|
+
const key = configKey.trim();
|
|
55
|
+
const value = vault.trim();
|
|
56
|
+
if (key.length === 0 || value.length === 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const state = await readState();
|
|
60
|
+
if (state.byConfigKey[key] === value) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await writeState({
|
|
64
|
+
byConfigKey: {
|
|
65
|
+
...state.byConfigKey,
|
|
66
|
+
[key]: value
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|
package/package.json
CHANGED