@andespindola/brainlink 0.1.0-beta.6 → 0.1.0-beta.61

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 (63) hide show
  1. package/AGENTS.md +8 -5
  2. package/CHANGELOG.md +58 -2
  3. package/CONTRIBUTING.md +2 -2
  4. package/COPYRIGHT.md +5 -0
  5. package/README.md +266 -20
  6. package/SECURITY.md +1 -1
  7. package/dist/application/add-note.js +62 -13
  8. package/dist/application/analyze-vault.js +95 -8
  9. package/dist/application/build-context.js +56 -1
  10. package/dist/application/dedupe-notes.js +226 -0
  11. package/dist/application/frontend/client-css.js +214 -100
  12. package/dist/application/frontend/client-html.js +60 -45
  13. package/dist/application/frontend/client-js.js +1853 -139
  14. package/dist/application/frontend/client-worker-js.js +66 -0
  15. package/dist/application/get-graph-layout.js +18 -6
  16. package/dist/application/get-graph-node.js +12 -0
  17. package/dist/application/get-graph-summary.js +12 -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 +252 -19
  21. package/dist/application/list-agents.js +3 -3
  22. package/dist/application/list-links.js +5 -5
  23. package/dist/application/migrate-vault.js +91 -0
  24. package/dist/application/offline-pack-backup.js +44 -0
  25. package/dist/application/search-graph-node-ids.js +12 -0
  26. package/dist/application/search-knowledge.js +75 -5
  27. package/dist/application/server/routes.js +102 -1
  28. package/dist/application/start-server.js +75 -4
  29. package/dist/application/watch-vault.js +23 -2
  30. package/dist/benchmarks/large-vault.js +1 -1
  31. package/dist/cli/commands/agent-commands.js +419 -0
  32. package/dist/cli/commands/config-commands.js +167 -0
  33. package/dist/cli/commands/read-commands.js +25 -8
  34. package/dist/cli/commands/write-commands.js +989 -10
  35. package/dist/cli/main.js +4 -0
  36. package/dist/cli/runtime.js +5 -2
  37. package/dist/domain/context.js +53 -11
  38. package/dist/domain/embeddings.js +2 -1
  39. package/dist/domain/graph-layout.js +62 -15
  40. package/dist/domain/markdown.js +36 -4
  41. package/dist/domain/middle-out.js +18 -0
  42. package/dist/infrastructure/config.js +132 -8
  43. package/dist/infrastructure/file-index.js +358 -0
  44. package/dist/infrastructure/file-system-vault.js +30 -0
  45. package/dist/infrastructure/index-state.js +56 -0
  46. package/dist/infrastructure/paths.js +9 -1
  47. package/dist/infrastructure/private-pack-codec.js +134 -0
  48. package/dist/infrastructure/search-packs.js +452 -0
  49. package/dist/infrastructure/session-state.js +172 -0
  50. package/dist/mcp/main.js +11 -3
  51. package/dist/mcp/server.js +27 -2
  52. package/dist/mcp/startup.js +35 -0
  53. package/dist/mcp/tools.js +633 -19
  54. package/docs/AGENT_USAGE.md +178 -16
  55. package/docs/ARCHITECTURE.md +37 -26
  56. package/docs/QUICKSTART.md +111 -0
  57. package/package.json +6 -4
  58. package/dist/infrastructure/sqlite/document-writer.js +0 -51
  59. package/dist/infrastructure/sqlite/graph-reader.js +0 -120
  60. package/dist/infrastructure/sqlite/schema.js +0 -111
  61. package/dist/infrastructure/sqlite/search-reader.js +0 -156
  62. package/dist/infrastructure/sqlite/types.js +0 -1
  63. package/dist/infrastructure/sqlite-index.js +0 -25
@@ -1,14 +1,17 @@
1
1
  import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../analyze-vault.js';
2
2
  import { buildContextPackage } from '../build-context.js';
3
3
  import { getGraph } from '../get-graph.js';
4
+ import { getGraphNode } from '../get-graph-node.js';
4
5
  import { getGraphLayout } from '../get-graph-layout.js';
5
6
  import { listAgents } from '../list-agents.js';
6
7
  import { listBacklinks, listLinks } from '../list-links.js';
8
+ import { searchGraphNodeIds } from '../search-graph-node-ids.js';
7
9
  import { searchKnowledge } from '../search-knowledge.js';
8
10
  import { loadBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/config.js';
9
11
  import { createClientCss } from '../frontend/client-css.js';
10
12
  import { createClientHtml } from '../frontend/client-html.js';
11
13
  import { createClientJs } from '../frontend/client-js.js';
14
+ import { createClientWorkerJs } from '../frontend/client-worker-js.js';
12
15
  import { contentTypes, createJsonResponse, isReadMethod, parsePositiveInteger } from './http.js';
13
16
  const readSearchMode = async (url) => {
14
17
  const config = await loadBrainlinkConfig();
@@ -49,6 +52,81 @@ const sameEntityTag = (candidate, signature) => {
49
52
  return decodeEntityTag(candidate) === signature;
50
53
  };
51
54
  const readAgentQuery = (url) => url.searchParams.get('agent') ?? undefined;
55
+ const compactGraphLayoutThreshold = 12_000;
56
+ const compactGraphLayoutEdgeLimit = 60_000;
57
+ const compactGraphLayoutEdgeLimitFor = (nodeCount) => {
58
+ if (nodeCount > 100_000)
59
+ return 15_000;
60
+ if (nodeCount > 50_000)
61
+ return 22_000;
62
+ if (nodeCount > 25_000)
63
+ return 30_000;
64
+ return compactGraphLayoutEdgeLimit;
65
+ };
66
+ const edgeWeight = (weight) => Number.isFinite(weight) ? Number(weight) : 1;
67
+ const edgeKey = (source, target, priority) => `${source}|${target}|${priority}`;
68
+ const selectCompactEdges = (layout, limit) => {
69
+ const resolvedEdges = layout.edges.filter((edge) => typeof edge.target === 'string' && edge.target.length > 0);
70
+ if (resolvedEdges.length <= limit) {
71
+ return resolvedEdges;
72
+ }
73
+ const bestEdgeByEndpoint = new Map();
74
+ for (let index = 0; index < resolvedEdges.length; index += 1) {
75
+ const edge = resolvedEdges[index];
76
+ const endpoints = [edge.source, edge.target];
77
+ for (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex += 1) {
78
+ const endpoint = endpoints[endpointIndex];
79
+ const previous = bestEdgeByEndpoint.get(endpoint);
80
+ if (!previous || edgeWeight(edge.weight) > edgeWeight(previous.weight)) {
81
+ bestEdgeByEndpoint.set(endpoint, edge);
82
+ }
83
+ }
84
+ }
85
+ const selected = new Map();
86
+ for (const edge of bestEdgeByEndpoint.values()) {
87
+ selected.set(edgeKey(edge.source, edge.target, edge.priority), edge);
88
+ }
89
+ if (selected.size > limit) {
90
+ return Array.from(selected.values())
91
+ .sort((left, right) => edgeWeight(right.weight) - edgeWeight(left.weight))
92
+ .slice(0, limit);
93
+ }
94
+ const byWeight = [...resolvedEdges].sort((left, right) => edgeWeight(right.weight) - edgeWeight(left.weight));
95
+ for (let index = 0; index < byWeight.length; index += 1) {
96
+ if (selected.size >= limit) {
97
+ break;
98
+ }
99
+ const edge = byWeight[index];
100
+ const key = edgeKey(edge.source, edge.target, edge.priority);
101
+ if (!selected.has(key)) {
102
+ selected.set(key, edge);
103
+ }
104
+ }
105
+ return Array.from(selected.values());
106
+ };
107
+ const stripLayoutContent = (layout) => ({
108
+ ...layout,
109
+ nodes: layout.nodes.map(({ content, ...node }) => node)
110
+ });
111
+ const compactLayoutPayload = (layout) => {
112
+ const edgeLimit = compactGraphLayoutEdgeLimitFor(layout.nodes.length);
113
+ const compactEdges = selectCompactEdges(layout, edgeLimit);
114
+ const compactNodes = layout.nodes.map((node) => [node.id, node.title, node.x, node.y, node.group, node.segment]);
115
+ const compactEdgeRows = compactEdges
116
+ .map((edge) => [edge.source, edge.target, edge.weight, edge.priority]);
117
+ return {
118
+ compact: true,
119
+ layout: {
120
+ nodes: compactNodes,
121
+ edges: compactEdgeRows
122
+ },
123
+ totals: {
124
+ nodes: layout.nodes.length,
125
+ edges: layout.edges.length
126
+ }
127
+ };
128
+ };
129
+ const encodeLayoutPayload = (layout) => layout.nodes.length > compactGraphLayoutThreshold ? compactLayoutPayload(layout) : { compact: false, layout: stripLayoutContent(layout) };
52
130
  export const route = async (request, url, vaultPath) => {
53
131
  if (isReadMethod(request) && (url.pathname === '/' || url.pathname === '/index.html')) {
54
132
  return createResponse(createClientHtml(), 200, contentTypes['.html']);
@@ -59,6 +137,9 @@ export const route = async (request, url, vaultPath) => {
59
137
  if (isReadMethod(request) && url.pathname === '/app.js') {
60
138
  return createResponse(createClientJs(), 200, contentTypes['.js']);
61
139
  }
140
+ if (isReadMethod(request) && url.pathname === '/app-worker.js') {
141
+ return createResponse(createClientWorkerJs(), 200, contentTypes['.js']);
142
+ }
62
143
  if (isReadMethod(request) && url.pathname === '/api/graph') {
63
144
  return createResponse(createJsonResponse(await getGraph(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
64
145
  }
@@ -67,7 +148,7 @@ export const route = async (request, url, vaultPath) => {
67
148
  const requestEtags = request.headers['if-none-match'];
68
149
  const notModified = sameEntityTag(requestEtags, signature);
69
150
  const etag = encodeEntityTag(signature);
70
- const body = createJsonResponse({ signature, layout });
151
+ const body = createJsonResponse({ signature, ...encodeLayoutPayload(layout) });
71
152
  const jsonResponse = createResponse(body, 200, contentTypes['.json']);
72
153
  const notModifiedResponse = createResponse('', 304, contentTypes['.json']);
73
154
  if (notModified) {
@@ -87,6 +168,26 @@ export const route = async (request, url, vaultPath) => {
87
168
  }
88
169
  };
89
170
  }
171
+ if (isReadMethod(request) && url.pathname === '/api/graph-node') {
172
+ const id = url.searchParams.get('id')?.trim() ?? '';
173
+ if (!id) {
174
+ return createResponse(createJsonResponse({ error: 'Missing id query parameter' }), 400, contentTypes['.json']);
175
+ }
176
+ const node = await getGraphNode(vaultPath, id, readAgentQuery(url));
177
+ if (!node) {
178
+ return createResponse(createJsonResponse({ error: 'Node not found' }), 404, contentTypes['.json']);
179
+ }
180
+ return createResponse(createJsonResponse({ node }), 200, contentTypes['.json']);
181
+ }
182
+ if (isReadMethod(request) && url.pathname === '/api/graph-filter') {
183
+ const query = url.searchParams.get('q')?.trim() ?? '';
184
+ const limit = parsePositiveInteger(url.searchParams.get('limit'), 1200);
185
+ if (!query) {
186
+ return createResponse(createJsonResponse({ query, nodeIds: [] }), 200, contentTypes['.json']);
187
+ }
188
+ const nodeIds = await searchGraphNodeIds(vaultPath, query, limit, readAgentQuery(url));
189
+ return createResponse(createJsonResponse({ query, nodeIds }), 200, contentTypes['.json']);
190
+ }
90
191
  if (isReadMethod(request) && url.pathname === '/api/agents') {
91
192
  return createResponse(createJsonResponse({ agents: await listAgents(vaultPath) }), 200, contentTypes['.json']);
92
193
  }
@@ -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) => {
@@ -21,7 +21,7 @@ const readOptions = (args) => ({
21
21
  });
22
22
  const topics = [
23
23
  'authentication jwt token refresh policy',
24
- 'sqlite graph backlinks markdown vault indexing',
24
+ 'graph backlinks markdown vault indexing',
25
25
  'frontend canvas layout graph interaction',
26
26
  'agent memory context retrieval summarization',
27
27
  'security local server vault path allowlist',