@andespindola/brainlink 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/AGENTS.md +142 -0
  2. package/CHANGELOG.md +13 -0
  3. package/CONTRIBUTING.md +28 -0
  4. package/LICENSE +23 -0
  5. package/README.md +715 -0
  6. package/SECURITY.md +35 -0
  7. package/dist/application/add-note.js +30 -0
  8. package/dist/application/analyze-vault.js +28 -0
  9. package/dist/application/build-context.js +15 -0
  10. package/dist/application/frontend/client-css.js +294 -0
  11. package/dist/application/frontend/client-html.js +66 -0
  12. package/dist/application/frontend/client-js.js +416 -0
  13. package/dist/application/get-graph-layout.js +3 -0
  14. package/dist/application/get-graph.js +12 -0
  15. package/dist/application/index-vault.js +67 -0
  16. package/dist/application/list-agents.js +12 -0
  17. package/dist/application/list-links.js +22 -0
  18. package/dist/application/search-knowledge.js +19 -0
  19. package/dist/application/server/host-security.js +6 -0
  20. package/dist/application/server/http.js +13 -0
  21. package/dist/application/server/routes.js +88 -0
  22. package/dist/application/server/types.js +1 -0
  23. package/dist/application/start-server.js +54 -0
  24. package/dist/application/watch-vault.js +36 -0
  25. package/dist/benchmarks/large-vault.js +88 -0
  26. package/dist/cli/commands/read-commands.js +149 -0
  27. package/dist/cli/commands/write-commands.js +107 -0
  28. package/dist/cli/main.js +21 -0
  29. package/dist/cli/runtime.js +18 -0
  30. package/dist/cli/types.js +1 -0
  31. package/dist/domain/agents.js +11 -0
  32. package/dist/domain/context.js +44 -0
  33. package/dist/domain/embeddings.js +117 -0
  34. package/dist/domain/graph-analysis.js +48 -0
  35. package/dist/domain/graph-layout.js +187 -0
  36. package/dist/domain/ids.js +2 -0
  37. package/dist/domain/markdown.js +100 -0
  38. package/dist/domain/note-safety.js +54 -0
  39. package/dist/domain/tokens.js +1 -0
  40. package/dist/domain/types.js +1 -0
  41. package/dist/infrastructure/config.js +60 -0
  42. package/dist/infrastructure/file-system-vault.js +62 -0
  43. package/dist/infrastructure/sqlite/document-writer.js +50 -0
  44. package/dist/infrastructure/sqlite/graph-reader.js +108 -0
  45. package/dist/infrastructure/sqlite/schema.js +87 -0
  46. package/dist/infrastructure/sqlite/search-reader.js +156 -0
  47. package/dist/infrastructure/sqlite/types.js +1 -0
  48. package/dist/infrastructure/sqlite-index.js +20 -0
  49. package/docs/AGENT_USAGE.md +477 -0
  50. package/docs/ARCHITECTURE.md +286 -0
  51. package/docs/RELEASE.md +67 -0
  52. package/package.json +67 -0
@@ -0,0 +1,416 @@
1
+ export const createClientJs = () => `const canvas = document.getElementById('graph')
2
+ const ctx = canvas.getContext('2d')
3
+ const state = {
4
+ graph: { nodes: [], edges: [] },
5
+ nodes: [],
6
+ edges: [],
7
+ selected: null,
8
+ hovered: null,
9
+ query: '',
10
+ agentId: '',
11
+ agentsSignature: '',
12
+ transform: { x: 0, y: 0, scale: 1 },
13
+ pointer: { x: 0, y: 0, down: false, dragNode: null, moved: false },
14
+ graphSignature: '',
15
+ last: performance.now()
16
+ }
17
+
18
+ const byId = id => document.getElementById(id)
19
+ const escapeHtml = value => String(value)
20
+ .replaceAll('&', '&')
21
+ .replaceAll('<', '&lt;')
22
+ .replaceAll('>', '&gt;')
23
+ .replaceAll('"', '&quot;')
24
+ .replaceAll("'", '&#039;')
25
+ const elements = {
26
+ stats: byId('stats'),
27
+ search: byId('search'),
28
+ agent: byId('agent'),
29
+ title: byId('title'),
30
+ path: byId('path'),
31
+ tags: byId('tags'),
32
+ notes: byId('notes'),
33
+ content: byId('content'),
34
+ outgoing: byId('outgoing'),
35
+ incoming: byId('incoming'),
36
+ nodeCount: byId('nodeCount'),
37
+ edgeCount: byId('edgeCount'),
38
+ tagCount: byId('tagCount'),
39
+ zoomIn: byId('zoomIn'),
40
+ zoomOut: byId('zoomOut'),
41
+ reset: byId('reset')
42
+ }
43
+
44
+ const agentQuery = () => state.agentId ? '?agent=' + encodeURIComponent(state.agentId) : ''
45
+
46
+ const graphTheme = {
47
+ node: '#aeb8c5',
48
+ nodeSelected: '#f3f7fb',
49
+ nodeHover: '#cbd5e1',
50
+ nodeHalo: 'rgba(203, 213, 225, 0.14)',
51
+ nodeHaloActive: 'rgba(243, 247, 251, 0.2)',
52
+ nodeStroke: '#0d0f12',
53
+ nodeStrokeActive: '#ffffff',
54
+ edge: 'rgba(153, 165, 181, 0.16)',
55
+ edgeActive: 'rgba(226, 232, 240, 0.52)',
56
+ label: '#edf2f7'
57
+ }
58
+
59
+ const resize = () => {
60
+ const rect = canvas.getBoundingClientRect()
61
+ const width = Math.max(rect.width, 320)
62
+ const height = Math.max(rect.height, 320)
63
+ const ratio = window.devicePixelRatio || 1
64
+ canvas.width = Math.floor(width * ratio)
65
+ canvas.height = Math.floor(height * ratio)
66
+ ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
67
+ }
68
+
69
+ const filteredNodes = () => {
70
+ const query = state.query.trim().toLowerCase()
71
+ if (!query) return state.nodes
72
+ return state.nodes.filter(node =>
73
+ node.title.toLowerCase().includes(query) ||
74
+ node.path.toLowerCase().includes(query) ||
75
+ node.tags.some(tag => tag.toLowerCase().includes(query))
76
+ )
77
+ }
78
+
79
+ const visibleIds = () => new Set(filteredNodes().map(node => node.id))
80
+
81
+ const visibleEdges = () => {
82
+ const ids = visibleIds()
83
+ return state.edges.filter(edge => ids.has(edge.source) && edge.target && ids.has(edge.target))
84
+ }
85
+
86
+ const resetView = () => {
87
+ const rect = canvas.getBoundingClientRect()
88
+ state.transform = { x: Math.max(rect.width, 320) / 2, y: Math.max(rect.height, 320) / 2, scale: 1 }
89
+ }
90
+
91
+ const createLayout = graph => {
92
+ const nodes = graph.nodes.map(node => ({
93
+ ...node,
94
+ x: Number.isFinite(node.x) ? node.x : 0,
95
+ y: Number.isFinite(node.y) ? node.y : 0
96
+ }))
97
+ const nodeMap = new Map(nodes.map(node => [node.id, node]))
98
+ const edges = graph.edges
99
+ .filter(edge => edge.target && nodeMap.has(edge.source) && nodeMap.has(edge.target))
100
+ .map(edge => ({ ...edge, sourceNode: nodeMap.get(edge.source), targetNode: nodeMap.get(edge.target) }))
101
+ return { nodes, edges }
102
+ }
103
+
104
+ const graphSignature = graph => JSON.stringify({
105
+ nodes: graph.nodes.map(node => [node.id, node.title, node.path, node.content, node.tags]),
106
+ edges: graph.edges.map(edge => [edge.source, edge.target, edge.targetTitle])
107
+ })
108
+
109
+ const tick = delta => {
110
+ const nodes = filteredNodes()
111
+ const ids = new Set(nodes.map(node => node.id))
112
+ const edges = state.edges.filter(edge => ids.has(edge.source) && edge.target && ids.has(edge.target))
113
+ const strength = Math.min(delta / 16, 2)
114
+
115
+ edges.forEach(edge => {
116
+ const source = edge.sourceNode
117
+ const target = edge.targetNode
118
+ const dx = target.x - source.x
119
+ const dy = target.y - source.y
120
+ const distance = Math.max(Math.hypot(dx, dy), 1)
121
+ const force = (distance - 150) * 0.002 * strength
122
+ const fx = dx * force
123
+ const fy = dy * force
124
+ source.vx += fx
125
+ source.vy += fy
126
+ target.vx -= fx
127
+ target.vy -= fy
128
+ })
129
+
130
+ for (let i = 0; i < nodes.length; i += 1) {
131
+ for (let j = i + 1; j < nodes.length; j += 1) {
132
+ const a = nodes[i]
133
+ const b = nodes[j]
134
+ const dx = b.x - a.x
135
+ const dy = b.y - a.y
136
+ const distance = Math.max(Math.hypot(dx, dy), 1)
137
+ const force = Math.min(2600 / (distance * distance), 0.12) * strength
138
+ const fx = (dx / distance) * force
139
+ const fy = (dy / distance) * force
140
+ a.vx -= fx
141
+ a.vy -= fy
142
+ b.vx += fx
143
+ b.vy += fy
144
+ }
145
+ }
146
+
147
+ nodes.forEach(node => {
148
+ if (state.pointer.dragNode === node) {
149
+ node.vx = 0
150
+ node.vy = 0
151
+ return
152
+ }
153
+ node.vx += -node.x * 0.0008 * strength
154
+ node.vy += -node.y * 0.0008 * strength
155
+ node.vx *= 0.88
156
+ node.vy *= 0.88
157
+ node.x += node.vx * strength
158
+ node.y += node.vy * strength
159
+ })
160
+ }
161
+
162
+ const worldPoint = event => {
163
+ const rect = canvas.getBoundingClientRect()
164
+ return {
165
+ x: (event.clientX - rect.left - state.transform.x) / state.transform.scale,
166
+ y: (event.clientY - rect.top - state.transform.y) / state.transform.scale
167
+ }
168
+ }
169
+
170
+ const hitNode = point => {
171
+ const nodes = filteredNodes()
172
+ for (let index = nodes.length - 1; index >= 0; index -= 1) {
173
+ const node = nodes[index]
174
+ const radius = nodeRadius(node)
175
+ if (Math.hypot(point.x - node.x, point.y - node.y) <= radius + 5) return node
176
+ }
177
+ return null
178
+ }
179
+
180
+ const nodeRadius = node => {
181
+ const degree = state.edges.filter(edge => edge.source === node.id || edge.target === node.id).length
182
+ return 9 + Math.min(degree, 8) * 1.6
183
+ }
184
+
185
+ const render = now => {
186
+ const delta = now - state.last
187
+ state.last = now
188
+ const rect = canvas.getBoundingClientRect()
189
+ const width = Math.max(rect.width, 320)
190
+ const height = Math.max(rect.height, 320)
191
+ ctx.clearRect(0, 0, width, height)
192
+ if (state.nodes.length === 0) {
193
+ ctx.fillStyle = '#99a5b5'
194
+ ctx.font = '14px Inter, system-ui, sans-serif'
195
+ ctx.textAlign = 'center'
196
+ ctx.fillText('No indexed notes found', width / 2, height / 2)
197
+ requestAnimationFrame(render)
198
+ return
199
+ }
200
+ ctx.save()
201
+ ctx.translate(state.transform.x, state.transform.y)
202
+ ctx.scale(state.transform.scale, state.transform.scale)
203
+
204
+ visibleEdges().forEach(edge => {
205
+ const selectedEdge = state.selected && (edge.source === state.selected.id || edge.target === state.selected.id)
206
+ ctx.beginPath()
207
+ ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
208
+ ctx.lineTo(edge.targetNode.x, edge.targetNode.y)
209
+ ctx.strokeStyle = selectedEdge ? graphTheme.edgeActive : graphTheme.edge
210
+ ctx.lineWidth = selectedEdge ? 1.8 : 1
211
+ ctx.stroke()
212
+ })
213
+
214
+ filteredNodes().forEach(node => {
215
+ const radius = nodeRadius(node)
216
+ const isSelected = state.selected?.id === node.id
217
+ const isHovered = state.hovered?.id === node.id
218
+ ctx.beginPath()
219
+ ctx.arc(node.x, node.y, radius + (isSelected ? 7 : isHovered ? 4 : 0), 0, Math.PI * 2)
220
+ ctx.fillStyle = isSelected || isHovered ? graphTheme.nodeHaloActive : graphTheme.nodeHalo
221
+ ctx.fill()
222
+ ctx.beginPath()
223
+ ctx.arc(node.x, node.y, radius, 0, Math.PI * 2)
224
+ ctx.fillStyle = isSelected ? graphTheme.nodeSelected : isHovered ? graphTheme.nodeHover : graphTheme.node
225
+ ctx.fill()
226
+ ctx.lineWidth = isSelected ? 2.6 : 1.5
227
+ ctx.strokeStyle = isSelected ? graphTheme.nodeStrokeActive : graphTheme.nodeStroke
228
+ ctx.stroke()
229
+
230
+ if (isSelected || isHovered || state.transform.scale > 1.18 || state.nodes.length <= 25) {
231
+ ctx.fillStyle = graphTheme.label
232
+ ctx.font = '12px Inter, system-ui, sans-serif'
233
+ ctx.textAlign = 'center'
234
+ ctx.textBaseline = 'top'
235
+ ctx.fillText(node.title.slice(0, 34), node.x, node.y + radius + 8)
236
+ }
237
+ })
238
+
239
+ ctx.restore()
240
+ requestAnimationFrame(render)
241
+ }
242
+
243
+ const list = items => items.length
244
+ ? items.map(item => '<li>' + (item.id ? '<button type="button" data-node-id="' + escapeHtml(item.id) + '">' + escapeHtml(item.title) + '</button>' : escapeHtml(item.title)) + '<small>' + escapeHtml(item.path) + '</small></li>').join('')
245
+ : '<li><small>No links found.</small></li>'
246
+
247
+ const allNotesList = () => state.nodes.length
248
+ ? state.nodes.map(node => '<li><button type="button" data-node-id="' + escapeHtml(node.id) + '">' + escapeHtml(node.title) + '</button><small>' + escapeHtml(node.path) + '</small></li>').join('')
249
+ : '<li><small>No notes indexed.</small></li>'
250
+
251
+ const selectNode = node => {
252
+ state.selected = node
253
+ if (!node) {
254
+ elements.title.textContent = 'Graph Overview'
255
+ elements.path.textContent = state.nodes.length + ' notes and ' + state.graph.edges.length + ' links indexed.'
256
+ elements.tags.innerHTML = ''
257
+ elements.notes.innerHTML = allNotesList()
258
+ elements.content.textContent = 'Selecione uma nota no grafo ou na lista para ver o Markdown completo, backlinks e links de saida.'
259
+ elements.outgoing.innerHTML = '<li><small>Select a note to inspect outgoing links.</small></li>'
260
+ elements.incoming.innerHTML = '<li><small>Select a note to inspect backlinks.</small></li>'
261
+ return
262
+ }
263
+ const nodeById = new Map(state.nodes.map(item => [item.id, item]))
264
+ const outgoing = state.graph.edges
265
+ .filter(edge => edge.source === node.id)
266
+ .map(edge => edge.target ? nodeById.get(edge.target) : { title: edge.targetTitle + ' (unresolved)', path: 'Missing note' })
267
+ .filter(Boolean)
268
+ const incoming = state.graph.edges
269
+ .filter(edge => edge.target === node.id)
270
+ .map(edge => nodeById.get(edge.source))
271
+ .filter(Boolean)
272
+
273
+ elements.title.textContent = node.title
274
+ elements.path.textContent = node.path
275
+ elements.tags.innerHTML = node.tags.length
276
+ ? node.tags.map(tag => '<span>#' + escapeHtml(tag) + '</span>').join('')
277
+ : '<span>No tags</span>'
278
+ elements.notes.innerHTML = allNotesList()
279
+ elements.content.textContent = node.content
280
+ elements.outgoing.innerHTML = list(outgoing)
281
+ elements.incoming.innerHTML = list(incoming)
282
+ }
283
+
284
+ const selectNodeById = id => {
285
+ const node = state.nodes.find(item => item.id === id)
286
+ if (node) selectNode(node)
287
+ }
288
+
289
+ const zoom = factor => {
290
+ state.transform.scale = Math.max(0.25, Math.min(3.5, state.transform.scale * factor))
291
+ }
292
+
293
+ const bindEvents = () => {
294
+ window.addEventListener('resize', resize)
295
+ elements.search.addEventListener('input', event => {
296
+ state.query = event.target.value
297
+ elements.stats.textContent = state.query
298
+ ? filteredNodes().length + ' filtered notes'
299
+ : state.nodes.length + ' notes · ' + state.edges.length + ' links'
300
+ })
301
+ elements.agent.addEventListener('change', event => {
302
+ state.agentId = event.target.value
303
+ state.selected = null
304
+ loadGraph({ reset: true }).catch(error => {
305
+ elements.stats.textContent = 'Failed to load agent graph'
306
+ console.error(error)
307
+ })
308
+ })
309
+ elements.zoomIn.addEventListener('click', () => zoom(1.18))
310
+ elements.zoomOut.addEventListener('click', () => zoom(0.84))
311
+ elements.reset.addEventListener('click', resetView)
312
+ ;[elements.notes, elements.outgoing, elements.incoming].forEach(element => {
313
+ element.addEventListener('click', event => {
314
+ const target = event.target
315
+ if (!(target instanceof HTMLElement)) return
316
+ const nodeId = target.dataset.nodeId
317
+ if (nodeId) selectNodeById(nodeId)
318
+ })
319
+ })
320
+ canvas.addEventListener('wheel', event => {
321
+ event.preventDefault()
322
+ zoom(event.deltaY < 0 ? 1.08 : 0.92)
323
+ }, { passive: false })
324
+ canvas.addEventListener('pointerdown', event => {
325
+ const point = worldPoint(event)
326
+ const node = hitNode(point)
327
+ state.pointer = { x: event.clientX, y: event.clientY, down: true, dragNode: node, moved: false }
328
+ if (node) {
329
+ node.x = point.x
330
+ node.y = point.y
331
+ }
332
+ canvas.setPointerCapture(event.pointerId)
333
+ })
334
+ canvas.addEventListener('pointermove', event => {
335
+ const point = worldPoint(event)
336
+ state.hovered = hitNode(point)
337
+ if (!state.pointer.down) return
338
+ const dx = event.clientX - state.pointer.x
339
+ const dy = event.clientY - state.pointer.y
340
+ state.pointer.x = event.clientX
341
+ state.pointer.y = event.clientY
342
+ state.pointer.moved = state.pointer.moved || Math.abs(dx) + Math.abs(dy) > 3
343
+ if (state.pointer.dragNode) {
344
+ state.pointer.dragNode.x = point.x
345
+ state.pointer.dragNode.y = point.y
346
+ return
347
+ }
348
+ state.transform.x += dx
349
+ state.transform.y += dy
350
+ })
351
+ canvas.addEventListener('pointerup', event => {
352
+ if (state.pointer.dragNode && !state.pointer.moved) selectNode(state.pointer.dragNode)
353
+ if (!state.pointer.dragNode && !state.pointer.moved) selectNode(state.hovered)
354
+ state.pointer = { x: 0, y: 0, down: false, dragNode: null, moved: false }
355
+ canvas.releasePointerCapture(event.pointerId)
356
+ })
357
+ }
358
+
359
+ const loadAgents = async () => {
360
+ const response = await fetch('/api/agents')
361
+ const payload = await response.json()
362
+ const agents = Array.isArray(payload.agents) ? payload.agents : []
363
+ const currentExists = agents.some(agent => agent.id === state.agentId)
364
+ const selected = currentExists
365
+ ? state.agentId
366
+ : (agents.find(agent => agent.id === 'shared')?.id ?? agents[0]?.id ?? 'shared')
367
+ const signature = JSON.stringify(agents.map(agent => [agent.id, agent.documentCount]))
368
+
369
+ state.agentId = selected
370
+ if (signature !== state.agentsSignature) {
371
+ elements.agent.innerHTML = agents.length
372
+ ? agents.map(agent => '<option value="' + escapeHtml(agent.id) + '">' + escapeHtml(agent.id) + ' · ' + agent.documentCount + '</option>').join('')
373
+ : '<option value="shared">shared · 0</option>'
374
+ state.agentsSignature = signature
375
+ }
376
+ elements.agent.value = selected
377
+ }
378
+
379
+ const loadGraph = async (options = { reset: false }) => {
380
+ const response = await fetch('/api/graph-layout' + agentQuery())
381
+ const graph = await response.json()
382
+ const signature = graphSignature(graph)
383
+ if (!options.reset && signature === state.graphSignature) return
384
+ const selectedId = state.selected?.id
385
+ const layout = createLayout(graph)
386
+ state.graphSignature = signature
387
+ state.graph = graph
388
+ state.nodes = layout.nodes
389
+ state.edges = layout.edges
390
+ const tags = new Set(graph.nodes.flatMap(node => node.tags))
391
+ elements.stats.textContent = state.agentId + ' · ' + graph.nodes.length + ' notes · ' + graph.edges.length + ' links · live'
392
+ elements.nodeCount.textContent = graph.nodes.length
393
+ elements.edgeCount.textContent = graph.edges.length
394
+ elements.tagCount.textContent = tags.size
395
+ resize()
396
+ if (options.reset) resetView()
397
+ selectNode(state.nodes.find(node => node.id === selectedId) ?? null)
398
+ }
399
+
400
+ bindEvents()
401
+ requestAnimationFrame(() => {
402
+ resize()
403
+ resetView()
404
+ })
405
+ loadAgents().then(() => loadGraph({ reset: true })).then(() => {
406
+ requestAnimationFrame(render)
407
+ setInterval(() => {
408
+ loadAgents().then(() => loadGraph()).catch(error => {
409
+ elements.stats.textContent = 'Failed to refresh graph'
410
+ console.error(error)
411
+ })
412
+ }, 2000)
413
+ }).catch(error => {
414
+ elements.stats.textContent = 'Failed to load graph'
415
+ console.error(error)
416
+ })`;
@@ -0,0 +1,3 @@
1
+ import { createCauliflowerGraphLayout } from '../domain/graph-layout.js';
2
+ import { getGraph } from './get-graph.js';
3
+ export const getGraphLayout = async (vaultPath, agentId) => createCauliflowerGraphLayout(await getGraph(vaultPath, agentId));
@@ -0,0 +1,12 @@
1
+ import { ensureVault } from '../infrastructure/file-system-vault.js';
2
+ import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
3
+ export const getGraph = async (vaultPath, agentId) => {
4
+ const absoluteVaultPath = await ensureVault(vaultPath);
5
+ const index = openSqliteIndex(absoluteVaultPath);
6
+ try {
7
+ return index.getGraph(agentId);
8
+ }
9
+ finally {
10
+ index.close();
11
+ }
12
+ };
@@ -0,0 +1,67 @@
1
+ import { createIndexedDocument, parseMarkdownDocument } from '../domain/markdown.js';
2
+ import { sharedAgentId } from '../domain/agents.js';
3
+ import { createEmbeddingProvider } from '../domain/embeddings.js';
4
+ import { loadBrainlinkConfig } from '../infrastructure/config.js';
5
+ import { ensureVault, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
6
+ import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
7
+ const toTitleKey = (title) => title.toLowerCase();
8
+ const appendTitleEntry = (map, document) => {
9
+ map.set(toTitleKey(document.title), document.id);
10
+ return map;
11
+ };
12
+ const createTitleMaps = (documents) => documents.reduce((state, document) => {
13
+ const agentMap = state.byAgent.get(document.agentId) ?? new Map();
14
+ appendTitleEntry(agentMap, document);
15
+ state.byAgent.set(document.agentId, agentMap);
16
+ if (document.agentId === sharedAgentId) {
17
+ appendTitleEntry(state.shared, document);
18
+ }
19
+ return state;
20
+ }, {
21
+ shared: new Map(),
22
+ byAgent: new Map()
23
+ });
24
+ const createScopedTitleResolver = (document, titleMaps) => ({
25
+ get: (title) => titleMaps.byAgent.get(document.agentId)?.get(title) ?? titleMaps.shared.get(title)
26
+ });
27
+ const embedIndexedDocuments = async (documents, providerName) => {
28
+ const provider = createEmbeddingProvider(providerName);
29
+ const chunks = documents.flatMap((document) => document.chunks);
30
+ const embeddings = await provider.embed(chunks.map((chunk) => chunk.content));
31
+ const embeddingByChunkId = new Map(chunks.map((chunk, index) => [chunk.id, embeddings[index] ?? []]));
32
+ return documents.map((indexedDocument) => ({
33
+ ...indexedDocument,
34
+ chunks: indexedDocument.chunks.map((chunk) => ({
35
+ ...chunk,
36
+ embeddingProvider: provider.name,
37
+ embedding: embeddingByChunkId.get(chunk.id) ?? []
38
+ }))
39
+ }));
40
+ };
41
+ export const indexVault = async (vaultPath) => {
42
+ const absoluteVaultPath = await ensureVault(vaultPath);
43
+ const config = await loadBrainlinkConfig();
44
+ const files = await readMarkdownFiles(absoluteVaultPath);
45
+ const documents = files.map((file) => parseMarkdownDocument({
46
+ absolutePath: file.absolutePath,
47
+ vaultPath: absoluteVaultPath,
48
+ content: file.content,
49
+ createdAt: file.createdAt,
50
+ updatedAt: file.updatedAt
51
+ }));
52
+ const titleMaps = createTitleMaps(documents);
53
+ const indexedDocuments = await embedIndexedDocuments(documents.map((document) => createIndexedDocument(document, createScopedTitleResolver(document, titleMaps), config.chunkSize)), config.embeddingProvider);
54
+ const index = openSqliteIndex(absoluteVaultPath);
55
+ try {
56
+ index.reset();
57
+ index.saveDocuments(indexedDocuments);
58
+ return {
59
+ documentCount: indexedDocuments.length,
60
+ chunkCount: indexedDocuments.reduce((total, document) => total + document.chunks.length, 0),
61
+ linkCount: indexedDocuments.reduce((total, document) => total + document.links.length, 0)
62
+ };
63
+ }
64
+ finally {
65
+ index.close();
66
+ }
67
+ };
@@ -0,0 +1,12 @@
1
+ import { ensureVault } from '../infrastructure/file-system-vault.js';
2
+ import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
3
+ export const listAgents = async (vaultPath) => {
4
+ const absoluteVaultPath = await ensureVault(vaultPath);
5
+ const index = openSqliteIndex(absoluteVaultPath);
6
+ try {
7
+ return index.listAgents();
8
+ }
9
+ finally {
10
+ index.close();
11
+ }
12
+ };
@@ -0,0 +1,22 @@
1
+ import { ensureVault } from '../infrastructure/file-system-vault.js';
2
+ import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
3
+ export const listLinks = async (vaultPath, agentId) => {
4
+ const absoluteVaultPath = await ensureVault(vaultPath);
5
+ const index = openSqliteIndex(absoluteVaultPath);
6
+ try {
7
+ return index.listLinks(agentId);
8
+ }
9
+ finally {
10
+ index.close();
11
+ }
12
+ };
13
+ export const listBacklinks = async (vaultPath, title, agentId) => {
14
+ const absoluteVaultPath = await ensureVault(vaultPath);
15
+ const index = openSqliteIndex(absoluteVaultPath);
16
+ try {
17
+ return index.listBacklinks(title, agentId);
18
+ }
19
+ finally {
20
+ index.close();
21
+ }
22
+ };
@@ -0,0 +1,19 @@
1
+ import { ensureVault } from '../infrastructure/file-system-vault.js';
2
+ import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
3
+ import { createEmbeddingProvider } from '../domain/embeddings.js';
4
+ import { loadBrainlinkConfig, sanitizeSearchMode } from '../infrastructure/config.js';
5
+ export const searchKnowledge = async (vaultPath, query, limit, agentId, mode) => {
6
+ const absoluteVaultPath = await ensureVault(vaultPath);
7
+ const config = await loadBrainlinkConfig();
8
+ const searchMode = sanitizeSearchMode(mode, config.defaultSearchMode);
9
+ const provider = createEmbeddingProvider(config.embeddingProvider);
10
+ const shouldEmbedQuery = searchMode !== 'fts' && provider.name !== 'none';
11
+ const queryEmbedding = shouldEmbedQuery ? (await provider.embed([query]))[0] ?? [] : [];
12
+ const index = openSqliteIndex(absoluteVaultPath);
13
+ try {
14
+ return index.search(query, limit, agentId, searchMode, queryEmbedding);
15
+ }
16
+ finally {
17
+ index.close();
18
+ }
19
+ };
@@ -0,0 +1,6 @@
1
+ export const isLoopbackHost = (host) => host === 'localhost' || host === '::1' || host === '[::1]' || host.startsWith('127.');
2
+ export const assertPublicBindAllowed = (host, allowPublic = false) => {
3
+ if (!allowPublic && !isLoopbackHost(host)) {
4
+ throw new Error(`Refusing to bind Brainlink server to non-loopback host ${host}. Pass --allow-public only behind your own auth and TLS.`);
5
+ }
6
+ };
@@ -0,0 +1,13 @@
1
+ export const contentTypes = {
2
+ '.html': 'text/html; charset=utf-8',
3
+ '.css': 'text/css; charset=utf-8',
4
+ '.js': 'text/javascript; charset=utf-8',
5
+ '.json': 'application/json; charset=utf-8'
6
+ };
7
+ export const createJsonResponse = (value) => JSON.stringify(value, null, 2);
8
+ export const parsePositiveInteger = (value, fallback) => {
9
+ const parsed = Number.parseInt(value ?? '', 10);
10
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
11
+ };
12
+ export const isHttpError = (error) => error instanceof Error && 'statusCode' in error && typeof error.statusCode === 'number';
13
+ export const isReadMethod = (request) => request.method === 'GET' || request.method === 'HEAD';
@@ -0,0 +1,88 @@
1
+ import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../analyze-vault.js';
2
+ import { buildContextPackage } from '../build-context.js';
3
+ import { getGraph } from '../get-graph.js';
4
+ import { getGraphLayout } from '../get-graph-layout.js';
5
+ import { listAgents } from '../list-agents.js';
6
+ import { listBacklinks, listLinks } from '../list-links.js';
7
+ import { searchKnowledge } from '../search-knowledge.js';
8
+ import { loadBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/config.js';
9
+ import { createClientCss } from '../frontend/client-css.js';
10
+ import { createClientHtml } from '../frontend/client-html.js';
11
+ import { createClientJs } from '../frontend/client-js.js';
12
+ import { contentTypes, createJsonResponse, isReadMethod, parsePositiveInteger } from './http.js';
13
+ const readSearchMode = async (url) => {
14
+ const config = await loadBrainlinkConfig();
15
+ return sanitizeSearchMode(url.searchParams.get('mode'), config.defaultSearchMode);
16
+ };
17
+ const hasInvalidSearchMode = (url) => {
18
+ const mode = url.searchParams.get('mode');
19
+ return mode !== null && !['fts', 'semantic', 'hybrid'].includes(mode);
20
+ };
21
+ const createResponse = (body, statusCode = 200, contentType = 'text/plain; charset=utf-8') => ({
22
+ body,
23
+ statusCode,
24
+ headers: {
25
+ 'content-type': contentType,
26
+ 'cache-control': 'no-store'
27
+ }
28
+ });
29
+ const readAgentQuery = (url) => url.searchParams.get('agent') ?? undefined;
30
+ export const route = async (request, url, vaultPath) => {
31
+ if (isReadMethod(request) && (url.pathname === '/' || url.pathname === '/index.html')) {
32
+ return createResponse(createClientHtml(), 200, contentTypes['.html']);
33
+ }
34
+ if (isReadMethod(request) && url.pathname === '/styles.css') {
35
+ return createResponse(createClientCss(), 200, contentTypes['.css']);
36
+ }
37
+ if (isReadMethod(request) && url.pathname === '/app.js') {
38
+ return createResponse(createClientJs(), 200, contentTypes['.js']);
39
+ }
40
+ if (isReadMethod(request) && url.pathname === '/api/graph') {
41
+ return createResponse(createJsonResponse(await getGraph(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
42
+ }
43
+ if (isReadMethod(request) && url.pathname === '/api/graph-layout') {
44
+ return createResponse(createJsonResponse(await getGraphLayout(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
45
+ }
46
+ if (isReadMethod(request) && url.pathname === '/api/agents') {
47
+ return createResponse(createJsonResponse({ agents: await listAgents(vaultPath) }), 200, contentTypes['.json']);
48
+ }
49
+ if (isReadMethod(request) && url.pathname === '/api/search') {
50
+ const query = url.searchParams.get('q') ?? '';
51
+ const limit = parsePositiveInteger(url.searchParams.get('limit'), 10);
52
+ const mode = await readSearchMode(url);
53
+ if (hasInvalidSearchMode(url)) {
54
+ return createResponse(createJsonResponse({ error: 'Invalid mode. Use fts, semantic or hybrid.' }), 400, contentTypes['.json']);
55
+ }
56
+ return createResponse(createJsonResponse({ query, agent: readAgentQuery(url), limit, mode, results: await searchKnowledge(vaultPath, query, limit, readAgentQuery(url), mode) }), 200, contentTypes['.json']);
57
+ }
58
+ if (isReadMethod(request) && url.pathname === '/api/context') {
59
+ const query = url.searchParams.get('q') ?? '';
60
+ const limit = parsePositiveInteger(url.searchParams.get('limit'), 12);
61
+ const tokens = parsePositiveInteger(url.searchParams.get('tokens'), 2000);
62
+ const mode = await readSearchMode(url);
63
+ if (hasInvalidSearchMode(url)) {
64
+ return createResponse(createJsonResponse({ error: 'Invalid mode. Use fts, semantic or hybrid.' }), 400, contentTypes['.json']);
65
+ }
66
+ return createResponse(createJsonResponse(await buildContextPackage(vaultPath, query, limit, tokens, readAgentQuery(url), mode)), 200, contentTypes['.json']);
67
+ }
68
+ if (isReadMethod(request) && url.pathname === '/api/links') {
69
+ return createResponse(createJsonResponse({ links: await listLinks(vaultPath, readAgentQuery(url)) }), 200, contentTypes['.json']);
70
+ }
71
+ if (isReadMethod(request) && url.pathname === '/api/backlinks') {
72
+ const title = url.searchParams.get('title') ?? '';
73
+ return createResponse(createJsonResponse({ title, backlinks: await listBacklinks(vaultPath, title, readAgentQuery(url)) }), 200, contentTypes['.json']);
74
+ }
75
+ if (isReadMethod(request) && url.pathname === '/api/stats') {
76
+ return createResponse(createJsonResponse(await getStats(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
77
+ }
78
+ if (isReadMethod(request) && url.pathname === '/api/broken-links') {
79
+ return createResponse(createJsonResponse({ brokenLinks: await getBrokenLinksReport(vaultPath, readAgentQuery(url)) }), 200, contentTypes['.json']);
80
+ }
81
+ if (isReadMethod(request) && url.pathname === '/api/orphans') {
82
+ return createResponse(createJsonResponse({ orphans: await getOrphansReport(vaultPath, readAgentQuery(url)) }), 200, contentTypes['.json']);
83
+ }
84
+ if (isReadMethod(request) && url.pathname === '/api/validate') {
85
+ return createResponse(createJsonResponse(await validateVault(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
86
+ }
87
+ return createResponse('Not found', 404);
88
+ };
@@ -0,0 +1 @@
1
+ export {};