@andespindola/brainlink 0.1.0-beta.16 → 0.1.0-beta.160
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.
- package/AGENTS.md +9 -6
- package/CHANGELOG.md +27 -0
- package/COPYRIGHT.md +5 -0
- package/README.md +177 -20
- package/dist/application/add-note.js +13 -44
- package/dist/application/auto-migrate-configured-vault.js +37 -0
- package/dist/application/build-context.js +64 -3
- package/dist/application/canonical-context-links.js +209 -0
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +241 -51
- package/dist/application/frontend/client-html.js +50 -27
- package/dist/application/frontend/client-js.js +1369 -605
- package/dist/application/frontend/client-render-worker-js.js +622 -0
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-contexts.js +33 -0
- package/dist/application/get-graph-layout.js +62 -8
- package/dist/application/get-graph-stream-chunk.js +326 -0
- package/dist/application/get-graph-view.js +246 -0
- package/dist/application/graph-view-state.js +66 -0
- package/dist/application/import-legacy-sqlite.js +266 -0
- package/dist/application/index-vault.js +262 -23
- package/dist/application/migrate-context-links.js +79 -0
- package/dist/application/offline-pack-backup.js +44 -0
- package/dist/application/search-graph-node-ids.js +63 -3
- package/dist/application/server/routes.js +247 -7
- package/dist/application/start-server.js +75 -4
- package/dist/application/watch-vault.js +23 -2
- package/dist/cli/commands/agent-commands.js +7 -0
- package/dist/cli/commands/write-commands.js +924 -14
- package/dist/cli/runtime.js +10 -2
- package/dist/domain/context.js +54 -11
- package/dist/domain/graph-contexts.js +180 -0
- package/dist/domain/graph-layout.js +389 -18
- package/dist/domain/markdown.js +53 -9
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +121 -4
- package/dist/infrastructure/file-index.js +76 -6
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/index-state.js +58 -0
- package/dist/infrastructure/private-pack-codec.js +71 -10
- package/dist/infrastructure/search-packs.js +286 -15
- package/dist/infrastructure/vault-migration-state.js +69 -0
- package/dist/infrastructure/volatile-memory.js +100 -0
- package/dist/mcp/runtime.js +20 -0
- package/dist/mcp/server.js +39 -11
- package/dist/mcp/tools.js +183 -7
- package/docs/AGENT_USAGE.md +96 -5
- package/docs/ARCHITECTURE.md +8 -0
- package/docs/QUICKSTART.md +7 -0
- package/package.json +7 -2
|
@@ -1,8 +1,12 @@
|
|
|
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 { getGraphContexts } from '../get-graph-contexts.js';
|
|
4
5
|
import { getGraphNode } from '../get-graph-node.js';
|
|
5
6
|
import { getGraphLayout } from '../get-graph-layout.js';
|
|
7
|
+
import { getGraphView } from '../get-graph-view.js';
|
|
8
|
+
import { getGraphStreamChunk } from '../get-graph-stream-chunk.js';
|
|
9
|
+
import { deleteGraphViewState, getGraphViewState, saveGraphViewState } from '../graph-view-state.js';
|
|
6
10
|
import { listAgents } from '../list-agents.js';
|
|
7
11
|
import { listBacklinks, listLinks } from '../list-links.js';
|
|
8
12
|
import { searchGraphNodeIds } from '../search-graph-node-ids.js';
|
|
@@ -11,6 +15,8 @@ import { loadBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/co
|
|
|
11
15
|
import { createClientCss } from '../frontend/client-css.js';
|
|
12
16
|
import { createClientHtml } from '../frontend/client-html.js';
|
|
13
17
|
import { createClientJs } from '../frontend/client-js.js';
|
|
18
|
+
import { createClientWorkerJs } from '../frontend/client-worker-js.js';
|
|
19
|
+
import { createClientRenderWorkerJs } from '../frontend/client-render-worker-js.js';
|
|
14
20
|
import { contentTypes, createJsonResponse, isReadMethod, parsePositiveInteger } from './http.js';
|
|
15
21
|
const readSearchMode = async (url) => {
|
|
16
22
|
const config = await loadBrainlinkConfig();
|
|
@@ -51,30 +57,195 @@ const sameEntityTag = (candidate, signature) => {
|
|
|
51
57
|
return decodeEntityTag(candidate) === signature;
|
|
52
58
|
};
|
|
53
59
|
const readAgentQuery = (url) => url.searchParams.get('agent') ?? undefined;
|
|
60
|
+
const readContextQuery = (url) => {
|
|
61
|
+
const value = url.searchParams.get('context')?.trim() ?? '';
|
|
62
|
+
return value.length > 0 ? value : undefined;
|
|
63
|
+
};
|
|
64
|
+
const parseNumber = (value, fallback) => {
|
|
65
|
+
const parsed = Number(value);
|
|
66
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
67
|
+
};
|
|
68
|
+
const readJsonBody = async (request, limitBytes = 1_000_000) => {
|
|
69
|
+
let body = '';
|
|
70
|
+
for await (const chunk of request) {
|
|
71
|
+
body += String(chunk);
|
|
72
|
+
if (Buffer.byteLength(body, 'utf8') > limitBytes) {
|
|
73
|
+
throw Object.assign(new Error('Request body too large'), { statusCode: 413 });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return body.trim().length > 0 ? JSON.parse(body) : {};
|
|
77
|
+
};
|
|
78
|
+
const readGraphViewStateInput = (url) => ({
|
|
79
|
+
signature: url.searchParams.get('signature')?.trim() ?? '',
|
|
80
|
+
agentId: readAgentQuery(url),
|
|
81
|
+
context: readContextQuery(url)
|
|
82
|
+
});
|
|
83
|
+
const compactGraphLayoutThreshold = 12_000;
|
|
84
|
+
const compactGraphLayoutEdgeLimit = 60_000;
|
|
85
|
+
const graphLayoutBodyCacheLimit = 8;
|
|
86
|
+
const graphLayoutBodyCache = new Map();
|
|
87
|
+
let cachedClientHtml = null;
|
|
88
|
+
let cachedClientCss = null;
|
|
89
|
+
let cachedClientJs = null;
|
|
90
|
+
let cachedClientWorkerJs = null;
|
|
91
|
+
let cachedClientRenderWorkerJs = null;
|
|
92
|
+
const readClientHtml = () => {
|
|
93
|
+
if (cachedClientHtml === null) {
|
|
94
|
+
cachedClientHtml = createClientHtml();
|
|
95
|
+
}
|
|
96
|
+
return cachedClientHtml;
|
|
97
|
+
};
|
|
98
|
+
const readClientCss = () => {
|
|
99
|
+
if (cachedClientCss === null) {
|
|
100
|
+
cachedClientCss = createClientCss();
|
|
101
|
+
}
|
|
102
|
+
return cachedClientCss;
|
|
103
|
+
};
|
|
104
|
+
const readClientJs = () => {
|
|
105
|
+
if (cachedClientJs === null) {
|
|
106
|
+
cachedClientJs = createClientJs();
|
|
107
|
+
}
|
|
108
|
+
return cachedClientJs;
|
|
109
|
+
};
|
|
110
|
+
const readClientWorkerJs = () => {
|
|
111
|
+
if (cachedClientWorkerJs === null) {
|
|
112
|
+
cachedClientWorkerJs = createClientWorkerJs();
|
|
113
|
+
}
|
|
114
|
+
return cachedClientWorkerJs;
|
|
115
|
+
};
|
|
116
|
+
const readClientRenderWorkerJs = () => {
|
|
117
|
+
if (cachedClientRenderWorkerJs === null) {
|
|
118
|
+
cachedClientRenderWorkerJs = createClientRenderWorkerJs();
|
|
119
|
+
}
|
|
120
|
+
return cachedClientRenderWorkerJs;
|
|
121
|
+
};
|
|
122
|
+
const compactGraphLayoutEdgeLimitFor = (nodeCount) => {
|
|
123
|
+
if (nodeCount > 100_000)
|
|
124
|
+
return 15_000;
|
|
125
|
+
if (nodeCount > 50_000)
|
|
126
|
+
return 22_000;
|
|
127
|
+
if (nodeCount > 25_000)
|
|
128
|
+
return 30_000;
|
|
129
|
+
return compactGraphLayoutEdgeLimit;
|
|
130
|
+
};
|
|
131
|
+
const edgeWeight = (weight) => Number.isFinite(weight) ? Number(weight) : 1;
|
|
132
|
+
const edgeKey = (source, target, priority) => `${source}|${target}|${priority}`;
|
|
133
|
+
const selectCompactEdges = (layout, limit) => {
|
|
134
|
+
const resolvedEdges = layout.edges.filter((edge) => typeof edge.target === 'string' && edge.target.length > 0);
|
|
135
|
+
if (resolvedEdges.length <= limit) {
|
|
136
|
+
return resolvedEdges;
|
|
137
|
+
}
|
|
138
|
+
const bestEdgeByEndpoint = new Map();
|
|
139
|
+
for (let index = 0; index < resolvedEdges.length; index += 1) {
|
|
140
|
+
const edge = resolvedEdges[index];
|
|
141
|
+
const endpoints = [edge.source, edge.target];
|
|
142
|
+
for (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex += 1) {
|
|
143
|
+
const endpoint = endpoints[endpointIndex];
|
|
144
|
+
const previous = bestEdgeByEndpoint.get(endpoint);
|
|
145
|
+
if (!previous || edgeWeight(edge.weight) > edgeWeight(previous.weight)) {
|
|
146
|
+
bestEdgeByEndpoint.set(endpoint, edge);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const selected = new Map();
|
|
151
|
+
for (const edge of bestEdgeByEndpoint.values()) {
|
|
152
|
+
selected.set(edgeKey(edge.source, edge.target, edge.priority), edge);
|
|
153
|
+
}
|
|
154
|
+
if (selected.size > limit) {
|
|
155
|
+
return Array.from(selected.values())
|
|
156
|
+
.sort((left, right) => edgeWeight(right.weight) - edgeWeight(left.weight))
|
|
157
|
+
.slice(0, limit);
|
|
158
|
+
}
|
|
159
|
+
const byWeight = [...resolvedEdges].sort((left, right) => edgeWeight(right.weight) - edgeWeight(left.weight));
|
|
160
|
+
for (let index = 0; index < byWeight.length; index += 1) {
|
|
161
|
+
if (selected.size >= limit) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
const edge = byWeight[index];
|
|
165
|
+
const key = edgeKey(edge.source, edge.target, edge.priority);
|
|
166
|
+
if (!selected.has(key)) {
|
|
167
|
+
selected.set(key, edge);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return Array.from(selected.values());
|
|
171
|
+
};
|
|
54
172
|
const stripLayoutContent = (layout) => ({
|
|
55
173
|
...layout,
|
|
56
174
|
nodes: layout.nodes.map(({ content, ...node }) => node)
|
|
57
175
|
});
|
|
176
|
+
const compactLayoutPayload = (layout) => {
|
|
177
|
+
const edgeLimit = compactGraphLayoutEdgeLimitFor(layout.nodes.length);
|
|
178
|
+
const compactEdges = selectCompactEdges(layout, edgeLimit);
|
|
179
|
+
const compactNodes = layout.nodes.map((node) => [node.id, node.title, node.x, node.y, node.group, node.segment]);
|
|
180
|
+
const compactEdgeRows = compactEdges
|
|
181
|
+
.map((edge) => [edge.source, edge.target, edge.weight, edge.priority]);
|
|
182
|
+
const compactGroups = layout.groups?.map((group) => [
|
|
183
|
+
group.id,
|
|
184
|
+
group.level,
|
|
185
|
+
group.parentId,
|
|
186
|
+
group.title,
|
|
187
|
+
group.x,
|
|
188
|
+
group.y,
|
|
189
|
+
group.radius,
|
|
190
|
+
group.segment,
|
|
191
|
+
group.group,
|
|
192
|
+
group.nodeIds,
|
|
193
|
+
group.childGroupIds
|
|
194
|
+
]);
|
|
195
|
+
return {
|
|
196
|
+
compact: true,
|
|
197
|
+
layout: {
|
|
198
|
+
nodes: compactNodes,
|
|
199
|
+
edges: compactEdgeRows,
|
|
200
|
+
...(compactGroups && compactGroups.length > 0 ? { groups: compactGroups } : {})
|
|
201
|
+
},
|
|
202
|
+
totals: {
|
|
203
|
+
nodes: layout.nodes.length,
|
|
204
|
+
edges: layout.edges.length
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
const encodeLayoutPayload = (layout) => layout.nodes.length > compactGraphLayoutThreshold ? compactLayoutPayload(layout) : { compact: false, layout: stripLayoutContent(layout) };
|
|
209
|
+
const readGraphLayoutBody = (signature) => graphLayoutBodyCache.get(signature) ?? null;
|
|
210
|
+
const storeGraphLayoutBody = (signature, body) => {
|
|
211
|
+
if (graphLayoutBodyCache.has(signature)) {
|
|
212
|
+
graphLayoutBodyCache.delete(signature);
|
|
213
|
+
}
|
|
214
|
+
graphLayoutBodyCache.set(signature, body);
|
|
215
|
+
while (graphLayoutBodyCache.size > graphLayoutBodyCacheLimit) {
|
|
216
|
+
const oldest = graphLayoutBodyCache.keys().next().value;
|
|
217
|
+
if (!oldest)
|
|
218
|
+
break;
|
|
219
|
+
graphLayoutBodyCache.delete(oldest);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
58
222
|
export const route = async (request, url, vaultPath) => {
|
|
59
223
|
if (isReadMethod(request) && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
60
|
-
return createResponse(
|
|
224
|
+
return createResponse(readClientHtml(), 200, contentTypes['.html']);
|
|
61
225
|
}
|
|
62
226
|
if (isReadMethod(request) && url.pathname === '/styles.css') {
|
|
63
|
-
return createResponse(
|
|
227
|
+
return createResponse(readClientCss(), 200, contentTypes['.css']);
|
|
64
228
|
}
|
|
65
229
|
if (isReadMethod(request) && url.pathname === '/app.js') {
|
|
66
|
-
return createResponse(
|
|
230
|
+
return createResponse(readClientJs(), 200, contentTypes['.js']);
|
|
231
|
+
}
|
|
232
|
+
if (isReadMethod(request) && url.pathname === '/app-worker.js') {
|
|
233
|
+
return createResponse(readClientWorkerJs(), 200, contentTypes['.js']);
|
|
234
|
+
}
|
|
235
|
+
if (isReadMethod(request) && url.pathname === '/render-worker.js') {
|
|
236
|
+
return createResponse(readClientRenderWorkerJs(), 200, contentTypes['.js']);
|
|
67
237
|
}
|
|
68
238
|
if (isReadMethod(request) && url.pathname === '/api/graph') {
|
|
69
239
|
return createResponse(createJsonResponse(await getGraph(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
|
|
70
240
|
}
|
|
71
241
|
if (isReadMethod(request) && url.pathname === '/api/graph-layout') {
|
|
72
|
-
const { signature, layout } = await getGraphLayout(vaultPath,
|
|
242
|
+
const { signature, layout } = await getGraphLayout(vaultPath, {
|
|
243
|
+
agentId: readAgentQuery(url),
|
|
244
|
+
context: readContextQuery(url)
|
|
245
|
+
});
|
|
73
246
|
const requestEtags = request.headers['if-none-match'];
|
|
74
247
|
const notModified = sameEntityTag(requestEtags, signature);
|
|
75
248
|
const etag = encodeEntityTag(signature);
|
|
76
|
-
const body = createJsonResponse({ signature, layout: stripLayoutContent(layout) });
|
|
77
|
-
const jsonResponse = createResponse(body, 200, contentTypes['.json']);
|
|
78
249
|
const notModifiedResponse = createResponse('', 304, contentTypes['.json']);
|
|
79
250
|
if (notModified) {
|
|
80
251
|
return {
|
|
@@ -85,6 +256,12 @@ export const route = async (request, url, vaultPath) => {
|
|
|
85
256
|
}
|
|
86
257
|
};
|
|
87
258
|
}
|
|
259
|
+
const cachedBody = readGraphLayoutBody(signature);
|
|
260
|
+
const body = cachedBody ?? createJsonResponse({ signature, ...encodeLayoutPayload(layout) });
|
|
261
|
+
if (!cachedBody) {
|
|
262
|
+
storeGraphLayoutBody(signature, body);
|
|
263
|
+
}
|
|
264
|
+
const jsonResponse = createResponse(body, 200, contentTypes['.json']);
|
|
88
265
|
return {
|
|
89
266
|
...jsonResponse,
|
|
90
267
|
headers: {
|
|
@@ -93,6 +270,66 @@ export const route = async (request, url, vaultPath) => {
|
|
|
93
270
|
}
|
|
94
271
|
};
|
|
95
272
|
}
|
|
273
|
+
if (isReadMethod(request) && url.pathname === '/api/graph-view') {
|
|
274
|
+
return createResponse(createJsonResponse(await getGraphView(vaultPath, {
|
|
275
|
+
x: parseNumber(url.searchParams.get('x'), -1000),
|
|
276
|
+
y: parseNumber(url.searchParams.get('y'), -1000),
|
|
277
|
+
width: parseNumber(url.searchParams.get('w'), 2000),
|
|
278
|
+
height: parseNumber(url.searchParams.get('h'), 2000),
|
|
279
|
+
scale: parseNumber(url.searchParams.get('scale'), 1),
|
|
280
|
+
agentId: readAgentQuery(url),
|
|
281
|
+
context: readContextQuery(url)
|
|
282
|
+
})), 200, contentTypes['.json']);
|
|
283
|
+
}
|
|
284
|
+
if (isReadMethod(request) && url.pathname === '/api/graph-stream') {
|
|
285
|
+
const x = parseNumber(url.searchParams.get('x'), -1000);
|
|
286
|
+
const y = parseNumber(url.searchParams.get('y'), -1000);
|
|
287
|
+
const width = parseNumber(url.searchParams.get('w'), 2000);
|
|
288
|
+
const height = parseNumber(url.searchParams.get('h'), 2000);
|
|
289
|
+
const scale = parseNumber(url.searchParams.get('scale'), 0.24);
|
|
290
|
+
const nodeBudget = parsePositiveInteger(url.searchParams.get('nodeBudget'), 1800);
|
|
291
|
+
const edgeBudget = parsePositiveInteger(url.searchParams.get('edgeBudget'), 5000);
|
|
292
|
+
return createResponse(createJsonResponse(await getGraphStreamChunk(vaultPath, {
|
|
293
|
+
x,
|
|
294
|
+
y,
|
|
295
|
+
width,
|
|
296
|
+
height,
|
|
297
|
+
scale,
|
|
298
|
+
nodeBudget,
|
|
299
|
+
edgeBudget,
|
|
300
|
+
agentId: readAgentQuery(url),
|
|
301
|
+
context: readContextQuery(url)
|
|
302
|
+
})), 200, contentTypes['.json']);
|
|
303
|
+
}
|
|
304
|
+
if (isReadMethod(request) && url.pathname === '/api/graph-view-state') {
|
|
305
|
+
const input = readGraphViewStateInput(url);
|
|
306
|
+
if (!input.signature) {
|
|
307
|
+
return createResponse(createJsonResponse({ error: 'Missing signature query parameter' }), 400, contentTypes['.json']);
|
|
308
|
+
}
|
|
309
|
+
return createResponse(createJsonResponse(await getGraphViewState(vaultPath, input)), 200, contentTypes['.json']);
|
|
310
|
+
}
|
|
311
|
+
if (request.method === 'POST' && url.pathname === '/api/graph-view-state') {
|
|
312
|
+
const input = readGraphViewStateInput(url);
|
|
313
|
+
if (!input.signature) {
|
|
314
|
+
return createResponse(createJsonResponse({ error: 'Missing signature query parameter' }), 400, contentTypes['.json']);
|
|
315
|
+
}
|
|
316
|
+
const body = await readJsonBody(request);
|
|
317
|
+
const positions = Array.isArray(body.positions)
|
|
318
|
+
? body.positions.map((position) => ({
|
|
319
|
+
id: String(position.id ?? ''),
|
|
320
|
+
x: Number(position.x),
|
|
321
|
+
y: Number(position.y)
|
|
322
|
+
}))
|
|
323
|
+
: [];
|
|
324
|
+
return createResponse(createJsonResponse(await saveGraphViewState(vaultPath, { ...input, positions })), 200, contentTypes['.json']);
|
|
325
|
+
}
|
|
326
|
+
if (request.method === 'DELETE' && url.pathname === '/api/graph-view-state') {
|
|
327
|
+
const input = readGraphViewStateInput(url);
|
|
328
|
+
if (!input.signature) {
|
|
329
|
+
return createResponse(createJsonResponse({ error: 'Missing signature query parameter' }), 400, contentTypes['.json']);
|
|
330
|
+
}
|
|
331
|
+
return createResponse(createJsonResponse(await deleteGraphViewState(vaultPath, input)), 200, contentTypes['.json']);
|
|
332
|
+
}
|
|
96
333
|
if (isReadMethod(request) && url.pathname === '/api/graph-node') {
|
|
97
334
|
const id = url.searchParams.get('id')?.trim() ?? '';
|
|
98
335
|
if (!id) {
|
|
@@ -110,12 +347,15 @@ export const route = async (request, url, vaultPath) => {
|
|
|
110
347
|
if (!query) {
|
|
111
348
|
return createResponse(createJsonResponse({ query, nodeIds: [] }), 200, contentTypes['.json']);
|
|
112
349
|
}
|
|
113
|
-
const nodeIds = await searchGraphNodeIds(vaultPath, query, limit, readAgentQuery(url));
|
|
350
|
+
const nodeIds = await searchGraphNodeIds(vaultPath, query, limit, readAgentQuery(url), readContextQuery(url));
|
|
114
351
|
return createResponse(createJsonResponse({ query, nodeIds }), 200, contentTypes['.json']);
|
|
115
352
|
}
|
|
116
353
|
if (isReadMethod(request) && url.pathname === '/api/agents') {
|
|
117
354
|
return createResponse(createJsonResponse({ agents: await listAgents(vaultPath) }), 200, contentTypes['.json']);
|
|
118
355
|
}
|
|
356
|
+
if (isReadMethod(request) && url.pathname === '/api/graph-contexts') {
|
|
357
|
+
return createResponse(createJsonResponse({ contexts: await getGraphContexts(vaultPath, readAgentQuery(url)) }), 200, contentTypes['.json']);
|
|
358
|
+
}
|
|
119
359
|
if (isReadMethod(request) && url.pathname === '/api/search') {
|
|
120
360
|
const query = url.searchParams.get('q') ?? '';
|
|
121
361
|
const limit = parsePositiveInteger(url.searchParams.get('limit'), 10);
|
|
@@ -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
|
-
|
|
23
|
-
response.
|
|
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
|
-
|
|
29
|
-
response.
|
|
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 {
|
|
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
|
-
|
|
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 } : {})
|