@andespindola/brainlink 0.1.0-beta.13 → 0.1.0-beta.130

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 (55) hide show
  1. package/AGENTS.md +8 -5
  2. package/CHANGELOG.md +26 -2
  3. package/CONTRIBUTING.md +2 -2
  4. package/COPYRIGHT.md +5 -0
  5. package/README.md +143 -20
  6. package/SECURITY.md +1 -1
  7. package/dist/application/analyze-vault.js +1 -15
  8. package/dist/application/build-context.js +64 -3
  9. package/dist/application/dedupe-notes.js +226 -0
  10. package/dist/application/frontend/client-css.js +93 -45
  11. package/dist/application/frontend/client-html.js +34 -25
  12. package/dist/application/frontend/client-js.js +2724 -182
  13. package/dist/application/frontend/client-worker-js.js +66 -0
  14. package/dist/application/get-graph-layout.js +39 -6
  15. package/dist/application/get-graph-node.js +3 -3
  16. package/dist/application/get-graph-summary.js +3 -3
  17. package/dist/application/get-graph-view.js +243 -0
  18. package/dist/application/get-graph.js +3 -3
  19. package/dist/application/import-legacy-sqlite.js +296 -0
  20. package/dist/application/index-vault.js +253 -25
  21. package/dist/application/list-agents.js +3 -3
  22. package/dist/application/list-links.js +5 -5
  23. package/dist/application/offline-pack-backup.js +44 -0
  24. package/dist/application/search-graph-node-ids.js +3 -3
  25. package/dist/application/search-knowledge.js +6 -6
  26. package/dist/application/server/routes.js +105 -1
  27. package/dist/application/start-server.js +75 -4
  28. package/dist/application/watch-vault.js +23 -2
  29. package/dist/benchmarks/large-vault.js +1 -1
  30. package/dist/cli/commands/agent-commands.js +7 -0
  31. package/dist/cli/commands/write-commands.js +842 -8
  32. package/dist/domain/context.js +54 -11
  33. package/dist/domain/graph-layout.js +181 -3
  34. package/dist/domain/markdown.js +29 -9
  35. package/dist/domain/middle-out.js +18 -0
  36. package/dist/infrastructure/config.js +38 -0
  37. package/dist/infrastructure/file-index.js +358 -0
  38. package/dist/infrastructure/file-system-vault.js +15 -0
  39. package/dist/infrastructure/index-state.js +58 -0
  40. package/dist/infrastructure/private-pack-codec.js +71 -10
  41. package/dist/infrastructure/search-packs.js +313 -17
  42. package/dist/infrastructure/volatile-memory.js +100 -0
  43. package/dist/mcp/server.js +21 -1
  44. package/dist/mcp/tools.js +96 -0
  45. package/docs/AGENT_USAGE.md +101 -18
  46. package/docs/ARCHITECTURE.md +22 -27
  47. package/docs/QUICKSTART.md +7 -0
  48. package/package.json +6 -4
  49. package/dist/infrastructure/sqlite/document-writer.js +0 -51
  50. package/dist/infrastructure/sqlite/graph-reader.js +0 -267
  51. package/dist/infrastructure/sqlite/recovery.js +0 -163
  52. package/dist/infrastructure/sqlite/schema.js +0 -114
  53. package/dist/infrastructure/sqlite/search-reader.js +0 -188
  54. package/dist/infrastructure/sqlite/types.js +0 -1
  55. package/dist/infrastructure/sqlite-index.js +0 -38
@@ -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,12 +1,31 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { stat } from 'node:fs/promises';
3
- import { join } from 'node:path';
2
+ import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
4
  import { createCauliflowerGraphLayout } from '../domain/graph-layout.js';
5
+ import { indexStoragePath } from '../infrastructure/file-index.js';
5
6
  import { getGraphSummary } from './get-graph-summary.js';
7
+ const graphLayoutVersion = 2;
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
- const info = await stat(join(vaultPath, '.brainlink', 'brainlink.db'));
28
+ const info = await stat(indexStoragePath(vaultPath));
10
29
  return `${Math.floor(info.mtimeMs)}:${info.size}`;
11
30
  }
12
31
  catch {
@@ -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 = createCauliflowerGraphLayout(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
@@ -1,10 +1,10 @@
1
1
  import { ensureVault } from '../infrastructure/file-system-vault.js';
2
- import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
2
+ import { openFileIndex } from '../infrastructure/file-index.js';
3
3
  export const getGraphNode = async (vaultPath, id, agentId) => {
4
4
  const absoluteVaultPath = await ensureVault(vaultPath);
5
- const index = openSqliteIndex(absoluteVaultPath);
5
+ const index = openFileIndex(absoluteVaultPath);
6
6
  try {
7
- return index.getGraphNode(id, agentId);
7
+ return await index.getGraphNode(id, agentId);
8
8
  }
9
9
  finally {
10
10
  index.close();
@@ -1,10 +1,10 @@
1
1
  import { ensureVault } from '../infrastructure/file-system-vault.js';
2
- import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
2
+ import { openFileIndex } from '../infrastructure/file-index.js';
3
3
  export const getGraphSummary = async (vaultPath, agentId) => {
4
4
  const absoluteVaultPath = await ensureVault(vaultPath);
5
- const index = openSqliteIndex(absoluteVaultPath);
5
+ const index = openFileIndex(absoluteVaultPath);
6
6
  try {
7
- return index.getGraphSummary(agentId);
7
+ return await index.getGraphSummary(agentId);
8
8
  }
9
9
  finally {
10
10
  index.close();
@@ -0,0 +1,243 @@
1
+ import { getGraphLayout } from './get-graph-layout.js';
2
+ const macroScale = 0.24;
3
+ const microCoverage = 0.72;
4
+ const nodeLimit = 1000;
5
+ const edgeLimit = 1400;
6
+ const groupEdgeLimit = 900;
7
+ const inViewport = (item, input) => {
8
+ const radius = item.radius ?? 48;
9
+ return (item.x + radius >= input.x &&
10
+ item.x - radius <= input.x + input.width &&
11
+ item.y + radius >= input.y &&
12
+ item.y - radius <= input.y + input.height);
13
+ };
14
+ const groupCoverage = (group, input) => {
15
+ const viewportRadius = Math.max(input.width, input.height) / 2;
16
+ const centerX = input.x + input.width / 2;
17
+ const centerY = input.y + input.height / 2;
18
+ const centerDistance = Math.hypot(group.x - centerX, group.y - centerY);
19
+ const fitCoverage = Math.min(1, childGraphRenderRadius(group) / Math.max(viewportRadius, 1));
20
+ const centerCoverage = 1 - Math.min(1, centerDistance / Math.max(viewportRadius, 1));
21
+ return fitCoverage * 0.72 + centerCoverage * 0.28;
22
+ };
23
+ const childGraphRenderRadius = (group) => {
24
+ const childCount = Math.max(group.nodeIds.length, group.childGroupIds.length, 1);
25
+ return Math.max(420, Math.min(1800, Math.sqrt(childCount) * 24));
26
+ };
27
+ const groupRenderRadius = (group) => {
28
+ const childCount = Math.max(group.nodeIds.length, group.childGroupIds.length, 1);
29
+ return 10 + Math.min(Math.log2(childCount + 1) * 4.2, 22);
30
+ };
31
+ const arrangeGraphLevelGroups = (groups) => {
32
+ if (groups.length <= 1) {
33
+ return groups.map((group) => ({ ...group, radius: groupRenderRadius(group) }));
34
+ }
35
+ const centerGroup = groups
36
+ .map((group) => ({
37
+ group,
38
+ score: Math.max(group.nodeIds.length, group.childGroupIds.length, 1) + group.externalEdges.length
39
+ }))
40
+ .sort((left, right) => right.score - left.score || left.group.title.localeCompare(right.group.title))[0]?.group;
41
+ const outerGroups = groups
42
+ .filter((group) => group.id !== centerGroup?.id)
43
+ .sort((left, right) => left.segment.localeCompare(right.segment) || left.title.localeCompare(right.title));
44
+ const baseRadius = Math.max(520, Math.min(2200, Math.sqrt(groups.length) * 135));
45
+ const goldenAngle = Math.PI * (3 - Math.sqrt(5));
46
+ const arranged = centerGroup
47
+ ? [{ ...centerGroup, x: 0, y: 0, radius: groupRenderRadius(centerGroup) }]
48
+ : [];
49
+ outerGroups.forEach((group, index) => {
50
+ const ringRadius = baseRadius * Math.sqrt((index + 1) / Math.max(outerGroups.length, 1));
51
+ const angle = index * goldenAngle;
52
+ arranged.push({
53
+ ...group,
54
+ x: Math.cos(angle) * ringRadius,
55
+ y: Math.sin(angle) * ringRadius,
56
+ radius: groupRenderRadius(group)
57
+ });
58
+ });
59
+ return arranged;
60
+ };
61
+ const distanceToViewportCenter = (item, input) => {
62
+ const centerX = input.x + input.width / 2;
63
+ const centerY = input.y + input.height / 2;
64
+ return Math.hypot(item.x - centerX, item.y - centerY);
65
+ };
66
+ const selectViewportItemsWithFill = (items, input, limit = nodeLimit) => {
67
+ const visible = items.filter((item) => inViewport(item, input));
68
+ if (visible.length >= limit) {
69
+ return visible.slice(0, limit);
70
+ }
71
+ const selectedIds = new Set(visible.map((item) => item.id));
72
+ const fill = items
73
+ .filter((item) => !selectedIds.has(item.id))
74
+ .sort((left, right) => distanceToViewportCenter(left, input) - distanceToViewportCenter(right, input) || left.id.localeCompare(right.id))
75
+ .slice(0, Math.max(0, limit - visible.length));
76
+ return visible.concat(fill);
77
+ };
78
+ const groupNode = (group) => [
79
+ `group:${group.id}`,
80
+ group.title,
81
+ group.x,
82
+ group.y,
83
+ group.group,
84
+ group.segment,
85
+ 'group'
86
+ ];
87
+ const realNode = (node) => [
88
+ node.id,
89
+ node.title,
90
+ node.x,
91
+ node.y,
92
+ node.group,
93
+ node.segment,
94
+ 'node'
95
+ ];
96
+ const descendants = (group, groupById) => group.nodeIds.length > 0
97
+ ? group.nodeIds
98
+ : group.childGroupIds.flatMap((childId) => {
99
+ const child = groupById.get(childId);
100
+ return child ? descendants(child, groupById) : [];
101
+ });
102
+ const aggregateGroupEdges = (groups, edges, groupById) => {
103
+ const groupNodeByNodeId = new Map();
104
+ groups.forEach((group) => {
105
+ descendants(group, groupById).forEach((nodeId) => groupNodeByNodeId.set(nodeId, `group:${group.id}`));
106
+ });
107
+ const selected = new Map();
108
+ edges.forEach((edge) => {
109
+ if (!edge.target)
110
+ return;
111
+ const source = groupNodeByNodeId.get(edge.source);
112
+ const target = groupNodeByNodeId.get(edge.target);
113
+ if (!source || !target || source === target)
114
+ return;
115
+ const key = source < target ? `${source}|${target}` : `${target}|${source}`;
116
+ const current = selected.get(key);
117
+ if (current && current[2] >= edge.weight)
118
+ return;
119
+ selected.set(key, [source, target, edge.weight, edge.priority]);
120
+ });
121
+ const degreeCounts = new Map();
122
+ return Array.from(selected.values())
123
+ .sort((left, right) => right[2] - left[2] || left[0].localeCompare(right[0]) || left[1].localeCompare(right[1]))
124
+ .filter((edge) => {
125
+ const sourceCount = degreeCounts.get(edge[0]) ?? 0;
126
+ const targetCount = degreeCounts.get(edge[1]) ?? 0;
127
+ if (sourceCount >= 3 || targetCount >= 3) {
128
+ return false;
129
+ }
130
+ degreeCounts.set(edge[0], sourceCount + 1);
131
+ degreeCounts.set(edge[1], targetCount + 1);
132
+ return true;
133
+ })
134
+ .slice(0, groupEdgeLimit);
135
+ };
136
+ const realEdges = (edges, nodeIds) => edges
137
+ .filter((edge) => Boolean(edge.target && nodeIds.has(edge.source) && nodeIds.has(edge.target)))
138
+ .slice(0, edgeLimit)
139
+ .map((edge) => [edge.source, edge.target, edge.weight, edge.priority]);
140
+ const degreeMap = (edges) => edges.reduce((degrees, edge) => {
141
+ degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + edge.weight);
142
+ if (edge.target) {
143
+ degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + edge.weight);
144
+ }
145
+ return degrees;
146
+ }, new Map());
147
+ const arrangeChildGraphNodes = (nodes, group, degrees) => {
148
+ if (nodes.length <= 1) {
149
+ return nodes.map((node) => ({ ...node, x: group.x, y: group.y }));
150
+ }
151
+ const centerNode = nodes
152
+ .map((node) => ({
153
+ node,
154
+ score: (degrees.get(node.id) ?? 0) + node.tags.length
155
+ }))
156
+ .sort((left, right) => right.score - left.score || left.node.title.localeCompare(right.node.title))[0]?.node;
157
+ const outerNodes = nodes
158
+ .filter((node) => node.id !== centerNode?.id)
159
+ .sort((left, right) => {
160
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
161
+ if (degreeDelta !== 0)
162
+ return degreeDelta;
163
+ return left.title.localeCompare(right.title);
164
+ });
165
+ const targetRadius = childGraphRenderRadius(group);
166
+ const goldenAngle = Math.PI * (3 - Math.sqrt(5));
167
+ const arranged = centerNode
168
+ ? [{ ...centerNode, x: group.x, y: group.y }]
169
+ : [];
170
+ outerNodes.forEach((node, index) => {
171
+ const ringRadius = targetRadius * Math.sqrt((index + 1) / Math.max(outerNodes.length, 1));
172
+ const angle = index * goldenAngle;
173
+ arranged.push({
174
+ ...node,
175
+ x: group.x + Math.cos(angle) * ringRadius,
176
+ y: group.y + Math.sin(angle) * ringRadius
177
+ });
178
+ });
179
+ return arranged;
180
+ };
181
+ const limitEdges = (edges) => edges.slice(0, edgeLimit);
182
+ export const getGraphView = async (vaultPath, input) => {
183
+ const { signature, layout } = await getGraphLayout(vaultPath, input.agentId);
184
+ const groups = layout.groups ?? [];
185
+ const degrees = degreeMap(layout.edges);
186
+ const groupById = new Map(groups.map((group) => [group.id, group]));
187
+ if (groups.length === 0) {
188
+ const nodes = layout.nodes.filter((node) => inViewport(node, input)).slice(0, nodeLimit);
189
+ const nodeIds = new Set(nodes.map((node) => node.id));
190
+ const viewNodes = nodes.map(realNode);
191
+ return {
192
+ signature,
193
+ mode: 'flat',
194
+ nodes: viewNodes,
195
+ edges: limitEdges(realEdges(layout.edges, nodeIds)),
196
+ totals: {
197
+ nodes: layout.nodes.length,
198
+ edges: layout.edges.length
199
+ }
200
+ };
201
+ }
202
+ const rootGroups = arrangeGraphLevelGroups(groups.filter((group) => group.parentId === null));
203
+ const leafGroups = arrangeGraphLevelGroups(groups.filter((group) => group.nodeIds.length > 0));
204
+ const visibleGroups = selectViewportItemsWithFill(rootGroups, input);
205
+ const focused = leafGroups
206
+ .filter((group) => group.nodeIds.length > 0 && inViewport(group, input))
207
+ .map((group) => ({ group, coverage: groupCoverage(group, input) }))
208
+ .sort((left, right) => right.coverage - left.coverage)[0];
209
+ if (focused && input.scale >= macroScale && focused.coverage >= microCoverage) {
210
+ const nodeIds = new Set(focused.group.nodeIds);
211
+ const arrangedNodes = arrangeChildGraphNodes(layout.nodes.filter((node) => nodeIds.has(node.id)), focused.group, degrees);
212
+ const nodesInViewport = arrangedNodes
213
+ .filter((node) => inViewport(node, input))
214
+ .slice(0, nodeLimit);
215
+ const nodes = nodesInViewport.length > 0
216
+ ? nodesInViewport
217
+ : arrangedNodes.slice(0, nodeLimit);
218
+ const visibleNodeIds = new Set(nodes.map((node) => node.id));
219
+ const viewNodes = nodes.map(realNode);
220
+ return {
221
+ signature,
222
+ mode: 'micro',
223
+ nodes: viewNodes,
224
+ edges: limitEdges(realEdges(layout.edges, visibleNodeIds)),
225
+ totals: {
226
+ nodes: layout.nodes.length,
227
+ edges: layout.edges.length
228
+ }
229
+ };
230
+ }
231
+ const groupsToRender = visibleGroups.slice(0, nodeLimit);
232
+ const viewNodes = groupsToRender.map(groupNode);
233
+ return {
234
+ signature,
235
+ mode: 'macro',
236
+ nodes: viewNodes,
237
+ edges: limitEdges(aggregateGroupEdges(groupsToRender, layout.edges, groupById)),
238
+ totals: {
239
+ nodes: layout.nodes.length,
240
+ edges: layout.edges.length
241
+ }
242
+ };
243
+ };
@@ -1,10 +1,10 @@
1
1
  import { ensureVault } from '../infrastructure/file-system-vault.js';
2
- import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
2
+ import { openFileIndex } from '../infrastructure/file-index.js';
3
3
  export const getGraph = async (vaultPath, agentId) => {
4
4
  const absoluteVaultPath = await ensureVault(vaultPath);
5
- const index = openSqliteIndex(absoluteVaultPath);
5
+ const index = openFileIndex(absoluteVaultPath);
6
6
  try {
7
- return index.getGraph(agentId);
7
+ return await index.getGraph(agentId);
8
8
  }
9
9
  finally {
10
10
  index.close();