@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
@@ -3,6 +3,8 @@ import { buildContextPackage } from '../build-context.js';
3
3
  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
+ import { getGraphView } from '../get-graph-view.js';
7
+ import { getGraphStreamChunk } from '../get-graph-stream-chunk.js';
6
8
  import { listAgents } from '../list-agents.js';
7
9
  import { listBacklinks, listLinks } from '../list-links.js';
8
10
  import { searchGraphNodeIds } from '../search-graph-node-ids.js';
@@ -11,6 +13,8 @@ import { loadBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/co
11
13
  import { createClientCss } from '../frontend/client-css.js';
12
14
  import { createClientHtml } from '../frontend/client-html.js';
13
15
  import { createClientJs } from '../frontend/client-js.js';
16
+ import { createClientWorkerJs } from '../frontend/client-worker-js.js';
17
+ import { createClientRenderWorkerJs } from '../frontend/client-render-worker-js.js';
14
18
  import { contentTypes, createJsonResponse, isReadMethod, parsePositiveInteger } from './http.js';
15
19
  const readSearchMode = async (url) => {
16
20
  const config = await loadBrainlinkConfig();
@@ -51,19 +55,164 @@ const sameEntityTag = (candidate, signature) => {
51
55
  return decodeEntityTag(candidate) === signature;
52
56
  };
53
57
  const readAgentQuery = (url) => url.searchParams.get('agent') ?? undefined;
58
+ const parseNumber = (value, fallback) => {
59
+ const parsed = Number(value);
60
+ return Number.isFinite(parsed) ? parsed : fallback;
61
+ };
62
+ const compactGraphLayoutThreshold = 12_000;
63
+ const compactGraphLayoutEdgeLimit = 60_000;
64
+ const graphLayoutBodyCacheLimit = 8;
65
+ const graphLayoutBodyCache = new Map();
66
+ let cachedClientHtml = null;
67
+ let cachedClientCss = null;
68
+ let cachedClientJs = null;
69
+ let cachedClientWorkerJs = null;
70
+ let cachedClientRenderWorkerJs = null;
71
+ const readClientHtml = () => {
72
+ if (cachedClientHtml === null) {
73
+ cachedClientHtml = createClientHtml();
74
+ }
75
+ return cachedClientHtml;
76
+ };
77
+ const readClientCss = () => {
78
+ if (cachedClientCss === null) {
79
+ cachedClientCss = createClientCss();
80
+ }
81
+ return cachedClientCss;
82
+ };
83
+ const readClientJs = () => {
84
+ if (cachedClientJs === null) {
85
+ cachedClientJs = createClientJs();
86
+ }
87
+ return cachedClientJs;
88
+ };
89
+ const readClientWorkerJs = () => {
90
+ if (cachedClientWorkerJs === null) {
91
+ cachedClientWorkerJs = createClientWorkerJs();
92
+ }
93
+ return cachedClientWorkerJs;
94
+ };
95
+ const readClientRenderWorkerJs = () => {
96
+ if (cachedClientRenderWorkerJs === null) {
97
+ cachedClientRenderWorkerJs = createClientRenderWorkerJs();
98
+ }
99
+ return cachedClientRenderWorkerJs;
100
+ };
101
+ const compactGraphLayoutEdgeLimitFor = (nodeCount) => {
102
+ if (nodeCount > 100_000)
103
+ return 15_000;
104
+ if (nodeCount > 50_000)
105
+ return 22_000;
106
+ if (nodeCount > 25_000)
107
+ return 30_000;
108
+ return compactGraphLayoutEdgeLimit;
109
+ };
110
+ const edgeWeight = (weight) => Number.isFinite(weight) ? Number(weight) : 1;
111
+ const edgeKey = (source, target, priority) => `${source}|${target}|${priority}`;
112
+ const selectCompactEdges = (layout, limit) => {
113
+ const resolvedEdges = layout.edges.filter((edge) => typeof edge.target === 'string' && edge.target.length > 0);
114
+ if (resolvedEdges.length <= limit) {
115
+ return resolvedEdges;
116
+ }
117
+ const bestEdgeByEndpoint = new Map();
118
+ for (let index = 0; index < resolvedEdges.length; index += 1) {
119
+ const edge = resolvedEdges[index];
120
+ const endpoints = [edge.source, edge.target];
121
+ for (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex += 1) {
122
+ const endpoint = endpoints[endpointIndex];
123
+ const previous = bestEdgeByEndpoint.get(endpoint);
124
+ if (!previous || edgeWeight(edge.weight) > edgeWeight(previous.weight)) {
125
+ bestEdgeByEndpoint.set(endpoint, edge);
126
+ }
127
+ }
128
+ }
129
+ const selected = new Map();
130
+ for (const edge of bestEdgeByEndpoint.values()) {
131
+ selected.set(edgeKey(edge.source, edge.target, edge.priority), edge);
132
+ }
133
+ if (selected.size > limit) {
134
+ return Array.from(selected.values())
135
+ .sort((left, right) => edgeWeight(right.weight) - edgeWeight(left.weight))
136
+ .slice(0, limit);
137
+ }
138
+ const byWeight = [...resolvedEdges].sort((left, right) => edgeWeight(right.weight) - edgeWeight(left.weight));
139
+ for (let index = 0; index < byWeight.length; index += 1) {
140
+ if (selected.size >= limit) {
141
+ break;
142
+ }
143
+ const edge = byWeight[index];
144
+ const key = edgeKey(edge.source, edge.target, edge.priority);
145
+ if (!selected.has(key)) {
146
+ selected.set(key, edge);
147
+ }
148
+ }
149
+ return Array.from(selected.values());
150
+ };
54
151
  const stripLayoutContent = (layout) => ({
55
152
  ...layout,
56
153
  nodes: layout.nodes.map(({ content, ...node }) => node)
57
154
  });
155
+ const compactLayoutPayload = (layout) => {
156
+ const edgeLimit = compactGraphLayoutEdgeLimitFor(layout.nodes.length);
157
+ const compactEdges = selectCompactEdges(layout, edgeLimit);
158
+ const compactNodes = layout.nodes.map((node) => [node.id, node.title, node.x, node.y, node.group, node.segment]);
159
+ const compactEdgeRows = compactEdges
160
+ .map((edge) => [edge.source, edge.target, edge.weight, edge.priority]);
161
+ const compactGroups = layout.groups?.map((group) => [
162
+ group.id,
163
+ group.level,
164
+ group.parentId,
165
+ group.title,
166
+ group.x,
167
+ group.y,
168
+ group.radius,
169
+ group.segment,
170
+ group.group,
171
+ group.nodeIds,
172
+ group.childGroupIds
173
+ ]);
174
+ return {
175
+ compact: true,
176
+ layout: {
177
+ nodes: compactNodes,
178
+ edges: compactEdgeRows,
179
+ ...(compactGroups && compactGroups.length > 0 ? { groups: compactGroups } : {})
180
+ },
181
+ totals: {
182
+ nodes: layout.nodes.length,
183
+ edges: layout.edges.length
184
+ }
185
+ };
186
+ };
187
+ const encodeLayoutPayload = (layout) => layout.nodes.length > compactGraphLayoutThreshold ? compactLayoutPayload(layout) : { compact: false, layout: stripLayoutContent(layout) };
188
+ const readGraphLayoutBody = (signature) => graphLayoutBodyCache.get(signature) ?? null;
189
+ const storeGraphLayoutBody = (signature, body) => {
190
+ if (graphLayoutBodyCache.has(signature)) {
191
+ graphLayoutBodyCache.delete(signature);
192
+ }
193
+ graphLayoutBodyCache.set(signature, body);
194
+ while (graphLayoutBodyCache.size > graphLayoutBodyCacheLimit) {
195
+ const oldest = graphLayoutBodyCache.keys().next().value;
196
+ if (!oldest)
197
+ break;
198
+ graphLayoutBodyCache.delete(oldest);
199
+ }
200
+ };
58
201
  export const route = async (request, url, vaultPath) => {
59
202
  if (isReadMethod(request) && (url.pathname === '/' || url.pathname === '/index.html')) {
60
- return createResponse(createClientHtml(), 200, contentTypes['.html']);
203
+ return createResponse(readClientHtml(), 200, contentTypes['.html']);
61
204
  }
62
205
  if (isReadMethod(request) && url.pathname === '/styles.css') {
63
- return createResponse(createClientCss(), 200, contentTypes['.css']);
206
+ return createResponse(readClientCss(), 200, contentTypes['.css']);
64
207
  }
65
208
  if (isReadMethod(request) && url.pathname === '/app.js') {
66
- return createResponse(createClientJs(), 200, contentTypes['.js']);
209
+ return createResponse(readClientJs(), 200, contentTypes['.js']);
210
+ }
211
+ if (isReadMethod(request) && url.pathname === '/app-worker.js') {
212
+ return createResponse(readClientWorkerJs(), 200, contentTypes['.js']);
213
+ }
214
+ if (isReadMethod(request) && url.pathname === '/render-worker.js') {
215
+ return createResponse(readClientRenderWorkerJs(), 200, contentTypes['.js']);
67
216
  }
68
217
  if (isReadMethod(request) && url.pathname === '/api/graph') {
69
218
  return createResponse(createJsonResponse(await getGraph(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
@@ -73,8 +222,6 @@ export const route = async (request, url, vaultPath) => {
73
222
  const requestEtags = request.headers['if-none-match'];
74
223
  const notModified = sameEntityTag(requestEtags, signature);
75
224
  const etag = encodeEntityTag(signature);
76
- const body = createJsonResponse({ signature, layout: stripLayoutContent(layout) });
77
- const jsonResponse = createResponse(body, 200, contentTypes['.json']);
78
225
  const notModifiedResponse = createResponse('', 304, contentTypes['.json']);
79
226
  if (notModified) {
80
227
  return {
@@ -85,6 +232,12 @@ export const route = async (request, url, vaultPath) => {
85
232
  }
86
233
  };
87
234
  }
235
+ const cachedBody = readGraphLayoutBody(signature);
236
+ const body = cachedBody ?? createJsonResponse({ signature, ...encodeLayoutPayload(layout) });
237
+ if (!cachedBody) {
238
+ storeGraphLayoutBody(signature, body);
239
+ }
240
+ const jsonResponse = createResponse(body, 200, contentTypes['.json']);
88
241
  return {
89
242
  ...jsonResponse,
90
243
  headers: {
@@ -93,6 +246,35 @@ export const route = async (request, url, vaultPath) => {
93
246
  }
94
247
  };
95
248
  }
249
+ if (isReadMethod(request) && url.pathname === '/api/graph-view') {
250
+ return createResponse(createJsonResponse(await getGraphView(vaultPath, {
251
+ x: parseNumber(url.searchParams.get('x'), -1000),
252
+ y: parseNumber(url.searchParams.get('y'), -1000),
253
+ width: parseNumber(url.searchParams.get('w'), 2000),
254
+ height: parseNumber(url.searchParams.get('h'), 2000),
255
+ scale: parseNumber(url.searchParams.get('scale'), 1),
256
+ agentId: readAgentQuery(url)
257
+ })), 200, contentTypes['.json']);
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
+ }
96
278
  if (isReadMethod(request) && url.pathname === '/api/graph-node') {
97
279
  const id = url.searchParams.get('id')?.trim() ?? '';
98
280
  if (!id) {
@@ -1,9 +1,78 @@
1
1
  import { createServer } from 'node:http';
2
+ import { brotliCompressSync, constants, gzipSync } from 'node:zlib';
2
3
  import { indexVault } from './index-vault.js';
3
4
  import { startVaultWatcher } from './watch-vault.js';
4
5
  import { assertLoopbackHost } from './server/host-security.js';
5
6
  import { contentTypes, createJsonResponse, isHttpError } from './server/http.js';
6
7
  import { route } from './server/routes.js';
8
+ const compressionThresholdBytes = 1024;
9
+ const normalizeEncodingToken = (value) => value.trim().toLowerCase();
10
+ const supportsEncoding = (acceptEncoding, target) => {
11
+ if (!acceptEncoding) {
12
+ return false;
13
+ }
14
+ return acceptEncoding
15
+ .split(',')
16
+ .map((entry) => entry.split(';')[0] ?? '')
17
+ .map(normalizeEncodingToken)
18
+ .includes(target);
19
+ };
20
+ const isCompressibleContentType = (contentType) => {
21
+ const normalized = contentType?.toLowerCase() ?? '';
22
+ return (normalized.includes('application/json') ||
23
+ normalized.includes('text/javascript') ||
24
+ normalized.includes('text/css') ||
25
+ normalized.includes('text/html') ||
26
+ normalized.startsWith('text/'));
27
+ };
28
+ const maybeCompressResponse = (requestHeaders, statusCode, headers, body) => {
29
+ if (statusCode === 204 || statusCode === 304) {
30
+ return { headers, body: '' };
31
+ }
32
+ if (!isCompressibleContentType(headers['content-type'])) {
33
+ return { headers, body };
34
+ }
35
+ const bodyBuffer = Buffer.from(body, 'utf8');
36
+ if (bodyBuffer.byteLength < compressionThresholdBytes) {
37
+ return { headers, body };
38
+ }
39
+ if (headers['content-encoding']) {
40
+ return { headers, body };
41
+ }
42
+ const acceptEncodingHeader = Array.isArray(requestHeaders['accept-encoding'])
43
+ ? requestHeaders['accept-encoding'].join(',')
44
+ : requestHeaders['accept-encoding'];
45
+ const vary = headers.vary ? `${headers.vary}, Accept-Encoding` : 'Accept-Encoding';
46
+ const withVary = {
47
+ ...headers,
48
+ vary
49
+ };
50
+ if (supportsEncoding(acceptEncodingHeader, 'br')) {
51
+ return {
52
+ headers: {
53
+ ...withVary,
54
+ 'content-encoding': 'br'
55
+ },
56
+ body: brotliCompressSync(bodyBuffer, {
57
+ params: {
58
+ [constants.BROTLI_PARAM_QUALITY]: 5
59
+ }
60
+ })
61
+ };
62
+ }
63
+ if (supportsEncoding(acceptEncodingHeader, 'gzip')) {
64
+ return {
65
+ headers: {
66
+ ...withVary,
67
+ 'content-encoding': 'gzip'
68
+ },
69
+ body: gzipSync(bodyBuffer, {
70
+ level: 6
71
+ })
72
+ };
73
+ }
74
+ return { headers: withVary, body };
75
+ };
7
76
  export const startServer = async (input) => {
8
77
  assertLoopbackHost(input.host);
9
78
  if (input.shouldIndex) {
@@ -19,14 +88,16 @@ export const startServer = async (input) => {
19
88
  const url = new URL(request.url ?? '/', `http://${request.headers.host ?? input.host}`);
20
89
  route(request, url, input.vaultPath)
21
90
  .then((result) => {
22
- response.writeHead(result.statusCode, result.headers);
23
- response.end(result.body);
91
+ const encoded = maybeCompressResponse(request.headers, result.statusCode, result.headers, result.body);
92
+ response.writeHead(result.statusCode, encoded.headers);
93
+ response.end(encoded.body);
24
94
  })
25
95
  .catch((error) => {
26
96
  const message = error instanceof Error ? error.message : String(error);
27
97
  const statusCode = isHttpError(error) ? error.statusCode : 500;
28
- response.writeHead(statusCode, { 'content-type': contentTypes['.json'] });
29
- response.end(createJsonResponse({ error: message }));
98
+ const fallback = maybeCompressResponse(request.headers, statusCode, { 'content-type': contentTypes['.json'] }, createJsonResponse({ error: message }));
99
+ response.writeHead(statusCode, fallback.headers);
100
+ response.end(fallback.body);
30
101
  });
31
102
  });
32
103
  await new Promise((resolve, reject) => {
@@ -1,5 +1,5 @@
1
1
  import { watch } from 'node:fs';
2
- import { indexVault } from './index-vault.js';
2
+ import { indexVaultWithOptions } from './index-vault.js';
3
3
  import { isBucketVaultPath, resolveVaultPath } from '../infrastructure/file-system-vault.js';
4
4
  const shouldIgnore = (filename) => {
5
5
  if (!filename) {
@@ -14,6 +14,27 @@ export const startVaultWatcher = (input) => {
14
14
  const absoluteVaultPath = resolveVaultPath(input.vaultPath);
15
15
  const debounceMs = input.debounceMs ?? 350;
16
16
  let timeout = null;
17
+ let running = false;
18
+ let pending = false;
19
+ const runIndex = () => {
20
+ if (running) {
21
+ pending = true;
22
+ return;
23
+ }
24
+ running = true;
25
+ indexVaultWithOptions(absoluteVaultPath, {
26
+ onProgress: input.onProgress
27
+ })
28
+ .then(input.onIndex)
29
+ .catch(input.onError)
30
+ .finally(() => {
31
+ running = false;
32
+ if (pending) {
33
+ pending = false;
34
+ runIndex();
35
+ }
36
+ });
37
+ };
17
38
  const schedule = (filename) => {
18
39
  if (shouldIgnore(filename)) {
19
40
  return;
@@ -22,7 +43,7 @@ export const startVaultWatcher = (input) => {
22
43
  clearTimeout(timeout);
23
44
  }
24
45
  timeout = setTimeout(() => {
25
- indexVault(absoluteVaultPath).then(input.onIndex).catch(input.onError);
46
+ runIndex();
26
47
  }, debounceMs);
27
48
  };
28
49
  const watcher = watch(absoluteVaultPath, { recursive: true }, (_eventType, filename) => {
@@ -141,6 +141,12 @@ const parseAllowedVaults = (value) => {
141
141
  export const installAgentIntegration = async (input) => {
142
142
  const codexConfigPath = getCodexConfigPath();
143
143
  const allowedVaults = parseAllowedVaults(input.allowedVaults);
144
+ const bootstrapPolicy = await setBootstrapPolicy({
145
+ enforceBootstrap: true,
146
+ enforceContextFirst: true,
147
+ autoBootstrapOnRead: true,
148
+ autoBootstrapOnStartup: true
149
+ });
144
150
  await upsertCodexMcpConfig(codexConfigPath, {
145
151
  allowedVaults,
146
152
  brainlinkHome: input.brainlinkHome
@@ -195,6 +201,7 @@ export const installAgentIntegration = async (input) => {
195
201
  codexConfigPath,
196
202
  mcpServer: 'brainlink',
197
203
  command: 'brainlink-mcp',
204
+ bootstrapPolicy,
198
205
  ...(input.mcpOnly !== true ? { pluginSourcePath, pluginSymlinkPath, marketplacePath } : {}),
199
206
  ...(selfTestResult ? { selfTest: selfTestResult } : {}),
200
207
  ...(warnings.length > 0 ? { warnings } : {})