@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
@@ -0,0 +1,569 @@
1
+ export const createClientRenderWorkerJs = () => `let canvas = null
2
+ let gl = null
3
+ let viewportWidth = 320
4
+ let viewportHeight = 320
5
+ let devicePixelRatio = 1
6
+ const camera = { x: 0, y: 0, scale: 1 }
7
+ const state = {
8
+ nodeCount: 0,
9
+ edgeCount: 0,
10
+ ids: [],
11
+ titles: [],
12
+ kinds: [],
13
+ x: new Float32Array(0),
14
+ y: new Float32Array(0),
15
+ relevance: new Float32Array(0),
16
+ radius: new Float32Array(0),
17
+ visible: new Uint8Array(0),
18
+ highlighted: new Uint8Array(0),
19
+ selected: new Uint8Array(0),
20
+ edgeSource: new Uint32Array(0),
21
+ edgeTarget: new Uint32Array(0),
22
+ edgeWeight: new Float32Array(0)
23
+ }
24
+ const nodeIndexById = new Map()
25
+ const highlightedIds = new Set()
26
+ let selectedNodeId = null
27
+ let dirty = true
28
+ let renderScheduled = false
29
+ let hoverX = null
30
+ let hoverY = null
31
+ let lastFrameAt = 0
32
+ let lastVisibleEdges = 0
33
+ let edgePositionsBuffer = new Float32Array(0)
34
+ let pointPositionsBuffer = new Float32Array(0)
35
+ let pointSizesBuffer = new Float32Array(0)
36
+
37
+ const defaultTheme = {
38
+ node: [0.68, 0.72, 0.78, 1],
39
+ nodeCluster: [0.42, 0.76, 0.92, 1],
40
+ nodeHighlight: [0.95, 0.76, 0.22, 1],
41
+ nodeSelected: [0.99, 0.99, 1, 1],
42
+ edge: [0.58, 0.64, 0.74, 0.24],
43
+ edgeHeavy: [0.78, 0.84, 0.92, 0.44],
44
+ clear: [0.05, 0.06, 0.08, 1]
45
+ }
46
+
47
+ const theme = { ...defaultTheme }
48
+
49
+ const createShader = (type, source) => {
50
+ const shader = gl.createShader(type)
51
+ if (!shader) return null
52
+ gl.shaderSource(shader, source)
53
+ gl.compileShader(shader)
54
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
55
+ gl.deleteShader(shader)
56
+ return null
57
+ }
58
+ return shader
59
+ }
60
+
61
+ const createProgram = (vertexSource, fragmentSource) => {
62
+ const vertexShader = createShader(gl.VERTEX_SHADER, vertexSource)
63
+ const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentSource)
64
+ if (!vertexShader || !fragmentShader) return null
65
+ const program = gl.createProgram()
66
+ if (!program) return null
67
+ gl.attachShader(program, vertexShader)
68
+ gl.attachShader(program, fragmentShader)
69
+ gl.linkProgram(program)
70
+ gl.deleteShader(vertexShader)
71
+ gl.deleteShader(fragmentShader)
72
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
73
+ gl.deleteProgram(program)
74
+ return null
75
+ }
76
+ return program
77
+ }
78
+
79
+ let lineProgram = null
80
+ let pointProgram = null
81
+ let lineBuffer = null
82
+ let pointPositionBuffer = null
83
+ let pointSizeBuffer = null
84
+ let linePositionLocation = -1
85
+ let lineResolutionLocation = null
86
+ let lineColorLocation = null
87
+ let pointPositionLocation = -1
88
+ let pointSizeLocation = -1
89
+ let pointResolutionLocation = null
90
+ let pointColorLocation = null
91
+
92
+ const initWebGl = () => {
93
+ if (!canvas) {
94
+ return false
95
+ }
96
+
97
+ gl = canvas.getContext('webgl2', { alpha: false, antialias: true, depth: false, stencil: false }) ||
98
+ canvas.getContext('webgl', { alpha: false, antialias: true, depth: false, stencil: false })
99
+
100
+ if (!gl) {
101
+ return false
102
+ }
103
+
104
+ lineProgram = createProgram(
105
+ 'attribute vec2 a_position; uniform vec2 u_resolution; void main() { vec2 zeroToOne = a_position / u_resolution; vec2 clip = zeroToOne * 2.0 - 1.0; gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); }',
106
+ 'precision mediump float; uniform vec4 u_color; void main() { gl_FragColor = u_color; }'
107
+ )
108
+
109
+ pointProgram = createProgram(
110
+ 'attribute vec2 a_position; attribute float a_size; uniform vec2 u_resolution; void main() { vec2 zeroToOne = a_position / u_resolution; vec2 clip = zeroToOne * 2.0 - 1.0; gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); gl_PointSize = a_size; }',
111
+ 'precision mediump float; uniform vec4 u_color; void main() { vec2 center = gl_PointCoord - vec2(0.5); float distanceFromCenter = length(center); if (distanceFromCenter > 0.5) discard; float alpha = smoothstep(0.5, 0.38, distanceFromCenter); gl_FragColor = vec4(u_color.rgb, u_color.a * alpha); }'
112
+ )
113
+
114
+ if (!lineProgram || !pointProgram) {
115
+ return false
116
+ }
117
+
118
+ lineBuffer = gl.createBuffer()
119
+ pointPositionBuffer = gl.createBuffer()
120
+ pointSizeBuffer = gl.createBuffer()
121
+ if (!lineBuffer || !pointPositionBuffer || !pointSizeBuffer) {
122
+ return false
123
+ }
124
+
125
+ linePositionLocation = gl.getAttribLocation(lineProgram, 'a_position')
126
+ lineResolutionLocation = gl.getUniformLocation(lineProgram, 'u_resolution')
127
+ lineColorLocation = gl.getUniformLocation(lineProgram, 'u_color')
128
+ pointPositionLocation = gl.getAttribLocation(pointProgram, 'a_position')
129
+ pointSizeLocation = gl.getAttribLocation(pointProgram, 'a_size')
130
+ pointResolutionLocation = gl.getUniformLocation(pointProgram, 'u_resolution')
131
+ pointColorLocation = gl.getUniformLocation(pointProgram, 'u_color')
132
+
133
+ gl.enable(gl.BLEND)
134
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
135
+ gl.disable(gl.DEPTH_TEST)
136
+
137
+ return true
138
+ }
139
+
140
+ const ensureFloat32Capacity = (buffer, neededLength) => {
141
+ if (buffer.length >= neededLength) {
142
+ return buffer
143
+ }
144
+ const next = Math.max(neededLength, Math.ceil(buffer.length * 1.6), 1024)
145
+ return new Float32Array(next)
146
+ }
147
+
148
+ const resizeCanvas = (width, height, ratio) => {
149
+ viewportWidth = Math.max(320, Number.isFinite(width) ? width : viewportWidth)
150
+ viewportHeight = Math.max(320, Number.isFinite(height) ? height : viewportHeight)
151
+ devicePixelRatio = Math.max(1, Number.isFinite(ratio) ? ratio : devicePixelRatio)
152
+
153
+ if (!canvas) return
154
+
155
+ canvas.width = Math.floor(viewportWidth * devicePixelRatio)
156
+ canvas.height = Math.floor(viewportHeight * devicePixelRatio)
157
+ if (gl) {
158
+ gl.viewport(0, 0, canvas.width, canvas.height)
159
+ }
160
+ dirty = true
161
+ requestRender()
162
+ }
163
+
164
+ const toScreenPoint = (x, y) => {
165
+ const sx = (x * camera.scale + camera.x) * devicePixelRatio
166
+ const sy = (y * camera.scale + camera.y) * devicePixelRatio
167
+ return [sx, sy]
168
+ }
169
+
170
+ const ensureNodeCapacity = (count) => {
171
+ if (state.x.length >= count) {
172
+ return
173
+ }
174
+
175
+ const nextCapacity = Math.max(count, Math.ceil(state.x.length * 1.5), 512)
176
+ state.x = new Float32Array(nextCapacity)
177
+ state.y = new Float32Array(nextCapacity)
178
+ state.relevance = new Float32Array(nextCapacity)
179
+ state.radius = new Float32Array(nextCapacity)
180
+ state.visible = new Uint8Array(nextCapacity)
181
+ state.highlighted = new Uint8Array(nextCapacity)
182
+ state.selected = new Uint8Array(nextCapacity)
183
+ }
184
+
185
+ const ensureEdgeCapacity = (count) => {
186
+ if (state.edgeSource.length >= count) {
187
+ return
188
+ }
189
+
190
+ const nextCapacity = Math.max(count, Math.ceil(state.edgeSource.length * 1.5), 1024)
191
+ state.edgeSource = new Uint32Array(nextCapacity)
192
+ state.edgeTarget = new Uint32Array(nextCapacity)
193
+ state.edgeWeight = new Float32Array(nextCapacity)
194
+ }
195
+
196
+ const nodeRadius = (relevance, kind) => {
197
+ const base = kind === 'cluster' ? 7.8 : 4.6
198
+ const modifier = Math.min(4.8, Math.max(0, relevance * 0.55))
199
+ return base + modifier
200
+ }
201
+
202
+ const loadChunk = (chunk) => {
203
+ const nodes = Array.isArray(chunk?.nodes) ? chunk.nodes : []
204
+ const edges = Array.isArray(chunk?.edges) ? chunk.edges : []
205
+
206
+ ensureNodeCapacity(nodes.length)
207
+ nodeIndexById.clear()
208
+ state.ids = new Array(nodes.length)
209
+ state.titles = new Array(nodes.length)
210
+ state.kinds = new Array(nodes.length)
211
+
212
+ for (let index = 0; index < nodes.length; index += 1) {
213
+ const row = nodes[index]
214
+ const id = typeof row?.[0] === 'string' ? row[0] : ''
215
+ if (!id) {
216
+ continue
217
+ }
218
+ const title = typeof row?.[1] === 'string' ? row[1] : id
219
+ const x = Number.isFinite(row?.[2]) ? Number(row[2]) : 0
220
+ const y = Number.isFinite(row?.[3]) ? Number(row[3]) : 0
221
+ const kind = row?.[6] === 'cluster' ? 'cluster' : 'node'
222
+ const relevance = Number.isFinite(row?.[7]) ? Number(row[7]) : 0
223
+
224
+ state.ids[index] = id
225
+ state.titles[index] = title
226
+ state.kinds[index] = kind
227
+ state.x[index] = x
228
+ state.y[index] = y
229
+ state.relevance[index] = relevance
230
+ state.radius[index] = nodeRadius(relevance, kind)
231
+ state.visible[index] = 0
232
+ state.highlighted[index] = highlightedIds.has(id) ? 1 : 0
233
+ state.selected[index] = selectedNodeId === id ? 1 : 0
234
+ nodeIndexById.set(id, index)
235
+ }
236
+
237
+ state.nodeCount = nodes.length
238
+
239
+ ensureEdgeCapacity(edges.length)
240
+ let edgeCount = 0
241
+ for (let index = 0; index < edges.length; index += 1) {
242
+ const row = edges[index]
243
+ const sourceId = typeof row?.[0] === 'string' ? row[0] : ''
244
+ const targetId = typeof row?.[1] === 'string' ? row[1] : ''
245
+ const source = nodeIndexById.get(sourceId)
246
+ const target = nodeIndexById.get(targetId)
247
+ if (source === undefined || target === undefined || source === target) {
248
+ continue
249
+ }
250
+ state.edgeSource[edgeCount] = source
251
+ state.edgeTarget[edgeCount] = target
252
+ state.edgeWeight[edgeCount] = Number.isFinite(row?.[2]) ? Number(row[2]) : 1
253
+ edgeCount += 1
254
+ }
255
+
256
+ state.edgeCount = edgeCount
257
+ dirty = true
258
+ }
259
+
260
+ const cullVisibleNodes = () => {
261
+ const minX = -280
262
+ const minY = -280
263
+ const maxX = viewportWidth + 280
264
+ const maxY = viewportHeight + 280
265
+
266
+ for (let index = 0; index < state.nodeCount; index += 1) {
267
+ const radius = state.radius[index] * camera.scale
268
+ const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
269
+ const screenX = sx / devicePixelRatio
270
+ const screenY = sy / devicePixelRatio
271
+ state.visible[index] = screenX + radius >= minX && screenX - radius <= maxX && screenY + radius >= minY && screenY - radius <= maxY ? 1 : 0
272
+ }
273
+ }
274
+
275
+ const drawEdges = () => {
276
+ if (!gl || state.edgeCount === 0) return
277
+
278
+ edgePositionsBuffer = ensureFloat32Capacity(edgePositionsBuffer, state.edgeCount * 4)
279
+ let cursor = 0
280
+ let visibleEdges = 0
281
+ for (let index = 0; index < state.edgeCount; index += 1) {
282
+ const source = state.edgeSource[index]
283
+ const target = state.edgeTarget[index]
284
+ if (state.visible[source] === 0 && state.visible[target] === 0) {
285
+ continue
286
+ }
287
+ const [sx, sy] = toScreenPoint(state.x[source], state.y[source])
288
+ const [tx, ty] = toScreenPoint(state.x[target], state.y[target])
289
+ edgePositionsBuffer[cursor] = sx
290
+ edgePositionsBuffer[cursor + 1] = sy
291
+ edgePositionsBuffer[cursor + 2] = tx
292
+ edgePositionsBuffer[cursor + 3] = ty
293
+ cursor += 4
294
+ visibleEdges += 1
295
+ }
296
+
297
+ lastVisibleEdges = visibleEdges
298
+ if (cursor === 0) return
299
+
300
+ gl.useProgram(lineProgram)
301
+ gl.bindBuffer(gl.ARRAY_BUFFER, lineBuffer)
302
+ gl.bufferData(gl.ARRAY_BUFFER, edgePositionsBuffer.subarray(0, cursor), gl.STREAM_DRAW)
303
+ gl.enableVertexAttribArray(linePositionLocation)
304
+ gl.vertexAttribPointer(linePositionLocation, 2, gl.FLOAT, false, 0, 0)
305
+ gl.uniform2f(lineResolutionLocation, canvas.width, canvas.height)
306
+ gl.uniform4fv(lineColorLocation, state.nodeCount > 4000 ? theme.edge : theme.edgeHeavy)
307
+ gl.lineWidth(1)
308
+ gl.drawArrays(gl.LINES, 0, cursor / 2)
309
+ }
310
+
311
+ const drawNodeLayer = (predicate, color, radiusBoost = 1) => {
312
+ if (!gl || state.nodeCount === 0) return
313
+
314
+ pointPositionsBuffer = ensureFloat32Capacity(pointPositionsBuffer, state.nodeCount * 2)
315
+ pointSizesBuffer = ensureFloat32Capacity(pointSizesBuffer, state.nodeCount)
316
+ let positionCursor = 0
317
+ let sizeCursor = 0
318
+ for (let index = 0; index < state.nodeCount; index += 1) {
319
+ if (!predicate(index)) continue
320
+ const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
321
+ pointPositionsBuffer[positionCursor] = sx
322
+ pointPositionsBuffer[positionCursor + 1] = sy
323
+ pointSizesBuffer[sizeCursor] = Math.max(1.2, state.radius[index] * camera.scale * devicePixelRatio * radiusBoost)
324
+ positionCursor += 2
325
+ sizeCursor += 1
326
+ }
327
+
328
+ if (positionCursor === 0) return
329
+
330
+ gl.useProgram(pointProgram)
331
+ gl.bindBuffer(gl.ARRAY_BUFFER, pointPositionBuffer)
332
+ gl.bufferData(gl.ARRAY_BUFFER, pointPositionsBuffer.subarray(0, positionCursor), gl.STREAM_DRAW)
333
+ gl.enableVertexAttribArray(pointPositionLocation)
334
+ gl.vertexAttribPointer(pointPositionLocation, 2, gl.FLOAT, false, 0, 0)
335
+
336
+ gl.bindBuffer(gl.ARRAY_BUFFER, pointSizeBuffer)
337
+ gl.bufferData(gl.ARRAY_BUFFER, pointSizesBuffer.subarray(0, sizeCursor), gl.STREAM_DRAW)
338
+ gl.enableVertexAttribArray(pointSizeLocation)
339
+ gl.vertexAttribPointer(pointSizeLocation, 1, gl.FLOAT, false, 0, 0)
340
+
341
+ gl.uniform2f(pointResolutionLocation, canvas.width, canvas.height)
342
+ gl.uniform4fv(pointColorLocation, color)
343
+ gl.drawArrays(gl.POINTS, 0, positionCursor / 2)
344
+ }
345
+
346
+ const clear = () => {
347
+ if (!gl || !canvas) return
348
+ gl.viewport(0, 0, canvas.width, canvas.height)
349
+ gl.clearColor(theme.clear[0], theme.clear[1], theme.clear[2], theme.clear[3])
350
+ gl.clear(gl.COLOR_BUFFER_BIT)
351
+ }
352
+
353
+ const renderFrame = (now) => {
354
+ renderScheduled = false
355
+ if (!dirty) {
356
+ return
357
+ }
358
+
359
+ const delta = now - lastFrameAt
360
+ const minInterval = state.nodeCount > 20000 ? 22 : 16
361
+ if (delta < minInterval) {
362
+ requestRender()
363
+ return
364
+ }
365
+ lastFrameAt = now
366
+
367
+ if (!gl) {
368
+ return
369
+ }
370
+
371
+ cullVisibleNodes()
372
+ clear()
373
+ drawEdges()
374
+
375
+ drawNodeLayer(
376
+ (index) => state.visible[index] === 1 && state.selected[index] === 0 && state.highlighted[index] === 0,
377
+ theme.node,
378
+ 1
379
+ )
380
+
381
+ drawNodeLayer(
382
+ (index) => state.visible[index] === 1 && state.kinds[index] === 'cluster' && state.selected[index] === 0,
383
+ theme.nodeCluster,
384
+ 1.15
385
+ )
386
+
387
+ drawNodeLayer(
388
+ (index) => state.visible[index] === 1 && state.highlighted[index] === 1,
389
+ theme.nodeHighlight,
390
+ 1.22
391
+ )
392
+
393
+ drawNodeLayer(
394
+ (index) => state.visible[index] === 1 && state.selected[index] === 1,
395
+ theme.nodeSelected,
396
+ 1.32
397
+ )
398
+
399
+ if (dirty) {
400
+ postMessage({
401
+ type: 'frame-stats',
402
+ visibleNodes: (() => {
403
+ let count = 0
404
+ for (let index = 0; index < state.nodeCount; index += 1) {
405
+ if (state.visible[index] === 1) count += 1
406
+ }
407
+ return count
408
+ })(),
409
+ visibleEdges: lastVisibleEdges
410
+ })
411
+ dirty = false
412
+ }
413
+ }
414
+
415
+ const requestRender = () => {
416
+ if (renderScheduled) {
417
+ return
418
+ }
419
+ renderScheduled = true
420
+ const raf = self.requestAnimationFrame
421
+ if (typeof raf === 'function') {
422
+ raf.call(self, renderFrame)
423
+ return
424
+ }
425
+ setTimeout(() => renderFrame(performance.now()), 16)
426
+ }
427
+
428
+ const setCamera = (nextCamera) => {
429
+ if (!nextCamera || typeof nextCamera !== 'object') {
430
+ return
431
+ }
432
+ camera.x = Number.isFinite(nextCamera.x) ? Number(nextCamera.x) : camera.x
433
+ camera.y = Number.isFinite(nextCamera.y) ? Number(nextCamera.y) : camera.y
434
+ camera.scale = Number.isFinite(nextCamera.scale) ? Math.max(0.0002, Math.min(8, Number(nextCamera.scale))) : camera.scale
435
+ dirty = true
436
+ requestRender()
437
+ }
438
+
439
+ const worldAtScreen = (screenX, screenY) => {
440
+ const x = (screenX - camera.x) / camera.scale
441
+ const y = (screenY - camera.y) / camera.scale
442
+ return [x, y]
443
+ }
444
+
445
+ const pickNode = (screenX, screenY) => {
446
+ const [worldX, worldY] = worldAtScreen(screenX, screenY)
447
+ let bestIndex = -1
448
+ let bestDistance = Infinity
449
+
450
+ for (let index = 0; index < state.nodeCount; index += 1) {
451
+ if (state.visible[index] === 0) continue
452
+ const dx = state.x[index] - worldX
453
+ const dy = state.y[index] - worldY
454
+ const distance = Math.hypot(dx, dy)
455
+ const maxDistance = state.radius[index] * 1.2
456
+ if (distance <= maxDistance && distance < bestDistance) {
457
+ bestDistance = distance
458
+ bestIndex = index
459
+ }
460
+ }
461
+
462
+ if (bestIndex < 0) {
463
+ return null
464
+ }
465
+
466
+ return {
467
+ id: state.ids[bestIndex],
468
+ title: state.titles[bestIndex],
469
+ kind: state.kinds[bestIndex],
470
+ x: state.x[bestIndex],
471
+ y: state.y[bestIndex]
472
+ }
473
+ }
474
+
475
+ const setHighlights = (ids) => {
476
+ highlightedIds.clear()
477
+ const list = Array.isArray(ids) ? ids : []
478
+ for (let index = 0; index < list.length; index += 1) {
479
+ const id = list[index]
480
+ if (typeof id === 'string' && id.length > 0) {
481
+ highlightedIds.add(id)
482
+ }
483
+ }
484
+
485
+ for (let index = 0; index < state.nodeCount; index += 1) {
486
+ state.highlighted[index] = highlightedIds.has(state.ids[index]) ? 1 : 0
487
+ }
488
+ dirty = true
489
+ requestRender()
490
+ }
491
+
492
+ const setSelected = (id) => {
493
+ selectedNodeId = typeof id === 'string' && id.length > 0 ? id : null
494
+ for (let index = 0; index < state.nodeCount; index += 1) {
495
+ state.selected[index] = selectedNodeId === state.ids[index] ? 1 : 0
496
+ }
497
+ dirty = true
498
+ requestRender()
499
+ }
500
+
501
+ self.onmessage = (event) => {
502
+ const payload = event.data
503
+ if (!payload || typeof payload !== 'object') {
504
+ return
505
+ }
506
+
507
+ if (payload.type === 'init') {
508
+ canvas = payload.canvas
509
+ if (payload.theme && typeof payload.theme === 'object') {
510
+ Object.assign(theme, payload.theme)
511
+ }
512
+ const initialized = initWebGl()
513
+ if (!initialized) {
514
+ postMessage({ type: 'fatal', message: 'WebGL is not available in render worker.' })
515
+ return
516
+ }
517
+ resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
518
+ setCamera(payload.camera)
519
+ requestRender()
520
+ postMessage({ type: 'ready' })
521
+ return
522
+ }
523
+
524
+ if (payload.type === 'resize') {
525
+ resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
526
+ return
527
+ }
528
+
529
+ if (payload.type === 'camera') {
530
+ setCamera(payload.camera)
531
+ return
532
+ }
533
+
534
+ if (payload.type === 'chunk') {
535
+ loadChunk(payload.chunk)
536
+ requestRender()
537
+ return
538
+ }
539
+
540
+ if (payload.type === 'highlight') {
541
+ setHighlights(payload.ids)
542
+ return
543
+ }
544
+
545
+ if (payload.type === 'select') {
546
+ setSelected(payload.id)
547
+ return
548
+ }
549
+
550
+ if (payload.type === 'pick') {
551
+ const node = pickNode(
552
+ Number.isFinite(payload.x) ? Number(payload.x) : 0,
553
+ Number.isFinite(payload.y) ? Number(payload.y) : 0
554
+ )
555
+ postMessage({
556
+ type: 'pick-result',
557
+ requestId: payload.requestId,
558
+ node
559
+ })
560
+ return
561
+ }
562
+
563
+ if (payload.type === 'pointer') {
564
+ hoverX = Number.isFinite(payload.x) ? Number(payload.x) : null
565
+ hoverY = Number.isFinite(payload.y) ? Number(payload.y) : null
566
+ return
567
+ }
568
+ }
569
+ `;
@@ -0,0 +1,66 @@
1
+ export const createClientWorkerJs = () => `const normalize = value => String(value || '')
2
+ .normalize('NFKD')
3
+ .replace(/\\p{Diacritic}/gu, '')
4
+ .toLowerCase()
5
+
6
+ let nodeIndex = []
7
+
8
+ const toNodeIndex = nodes =>
9
+ (Array.isArray(nodes) ? nodes : [])
10
+ .map(node => {
11
+ const id = typeof node.id === 'string' ? node.id : ''
12
+ if (!id) {
13
+ return null
14
+ }
15
+ const title = normalize(node.title)
16
+ const path = normalize(node.path)
17
+ const tags = Array.isArray(node.tags) ? node.tags.map(tag => normalize(tag)) : []
18
+ return {
19
+ id,
20
+ text: [title, path, ...tags].join(' ')
21
+ }
22
+ })
23
+ .filter(Boolean)
24
+
25
+ const scoreText = (text, query) => {
26
+ if (!query) return 0
27
+ if (!text.includes(query)) return 0
28
+ if (text.startsWith(query)) return 4
29
+ return 1
30
+ }
31
+
32
+ const filterIds = (query, limit) => {
33
+ const normalizedQuery = normalize(query).trim()
34
+ if (!normalizedQuery) {
35
+ return []
36
+ }
37
+ const rows = []
38
+ for (let index = 0; index < nodeIndex.length; index += 1) {
39
+ const row = nodeIndex[index]
40
+ const score = scoreText(row.text, normalizedQuery)
41
+ if (score > 0) {
42
+ rows.push({ id: row.id, score })
43
+ }
44
+ }
45
+ rows.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))
46
+ return rows.slice(0, Math.max(1, Number.isFinite(limit) ? limit : rows.length)).map(row => row.id)
47
+ }
48
+
49
+ self.onmessage = event => {
50
+ const payload = event.data
51
+ if (!payload || typeof payload !== 'object') {
52
+ return
53
+ }
54
+ if (payload.type === 'load-nodes') {
55
+ nodeIndex = toNodeIndex(payload.nodes)
56
+ return
57
+ }
58
+ if (payload.type === 'filter') {
59
+ const token = payload.token
60
+ const ids = filterIds(payload.query, payload.limit)
61
+ self.postMessage({ type: 'filter-result', token, ids })
62
+ }
63
+ }
64
+
65
+ self.postMessage({ type: 'ready' })
66
+ `;
@@ -1,9 +1,28 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { stat } from 'node:fs/promises';
3
- import { createCauliflowerGraphLayout } from '../domain/graph-layout.js';
2
+ import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { createStarGraphLayout } from '../domain/graph-layout.js';
4
5
  import { indexStoragePath } from '../infrastructure/file-index.js';
5
6
  import { getGraphSummary } from './get-graph-summary.js';
7
+ const graphLayoutVersion = 3;
6
8
  const graphLayoutCache = new Map();
9
+ const graphLayoutStoragePath = (vaultPath, agentId) => join(vaultPath, '.brainlink', `graph-layout-${agentId?.replace(/[^a-zA-Z0-9_-]/g, '_') ?? 'all'}.json`);
10
+ const readPersistedLayout = async (vaultPath, databaseSignature, agentId) => {
11
+ try {
12
+ const parsed = JSON.parse(await readFile(graphLayoutStoragePath(vaultPath, agentId), 'utf8'));
13
+ return parsed.databaseSignature === databaseSignature && parsed.layoutVersion === graphLayoutVersion ? parsed : null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ };
19
+ const writePersistedLayout = async (vaultPath, agentId, cached) => {
20
+ const target = graphLayoutStoragePath(vaultPath, agentId);
21
+ const temp = `${target}.tmp`;
22
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
23
+ await writeFile(temp, `${JSON.stringify(cached)}\n`, { encoding: 'utf8', mode: 0o600 });
24
+ await rename(temp, target);
25
+ };
7
26
  const readDatabaseSignature = async (vaultPath) => {
8
27
  try {
9
28
  const info = await stat(indexStoragePath(vaultPath));
@@ -26,16 +45,30 @@ export const getGraphLayout = async (vaultPath, agentId) => {
26
45
  const databaseSignature = await readDatabaseSignature(vaultPath);
27
46
  const cacheKey = `${vaultPath}:${agentId ?? ''}`;
28
47
  const cached = graphLayoutCache.get(cacheKey);
29
- if (cached?.databaseSignature === databaseSignature) {
48
+ if (cached?.databaseSignature === databaseSignature && cached.layoutVersion === graphLayoutVersion) {
30
49
  return {
31
50
  signature: cached.signature,
32
51
  layout: cached.layout
33
52
  };
34
53
  }
54
+ const persisted = await readPersistedLayout(vaultPath, databaseSignature, agentId);
55
+ if (persisted) {
56
+ graphLayoutCache.set(cacheKey, persisted);
57
+ return {
58
+ signature: persisted.signature,
59
+ layout: persisted.layout
60
+ };
61
+ }
35
62
  const graph = await getGraphSummary(vaultPath, agentId);
36
63
  const signature = createGraphSignature(graph);
37
- const layout = createCauliflowerGraphLayout(graph);
38
- graphLayoutCache.set(cacheKey, { databaseSignature, signature, layout });
64
+ const rawLayout = createStarGraphLayout(graph);
65
+ const layout = {
66
+ ...rawLayout,
67
+ nodes: rawLayout.nodes.map((node) => ({ ...node, content: '' }))
68
+ };
69
+ const nextCache = { layoutVersion: graphLayoutVersion, databaseSignature, signature, layout };
70
+ graphLayoutCache.set(cacheKey, nextCache);
71
+ await writePersistedLayout(vaultPath, agentId, nextCache);
39
72
  return {
40
73
  signature,
41
74
  layout