@andespindola/brainlink 0.1.0-beta.98 → 1.0.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.
- package/AGENTS.md +6 -6
- package/CHANGELOG.md +14 -0
- package/README.md +186 -38
- package/dist/application/add-note.js +13 -44
- package/dist/application/analyze-vault.js +1 -1
- package/dist/application/auto-migrate-configured-vault.js +37 -0
- package/dist/application/build-context.js +119 -20
- package/dist/application/canonical-context-links.js +209 -0
- package/dist/application/frontend/client-css.js +212 -42
- package/dist/application/frontend/client-html.js +42 -28
- package/dist/application/frontend/client-js.js +1294 -3217
- package/dist/application/frontend/client-render-worker-js.js +676 -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 +3 -33
- package/dist/application/index-vault.js +35 -22
- package/dist/application/migrate-context-links.js +79 -0
- package/dist/application/search-graph-node-ids.js +63 -3
- package/dist/application/server/routes.js +197 -12
- package/dist/cli/commands/read-commands.js +39 -3
- package/dist/cli/commands/vault-commands.js +182 -0
- package/dist/cli/commands/write-commands.js +147 -12
- package/dist/cli/main.js +2 -0
- package/dist/cli/runtime.js +10 -2
- package/dist/domain/context.js +1 -0
- package/dist/domain/graph-contexts.js +180 -0
- package/dist/domain/graph-layout.js +347 -21
- package/dist/domain/markdown.js +53 -9
- package/dist/infrastructure/config.js +105 -6
- package/dist/infrastructure/context-packs.js +122 -0
- package/dist/infrastructure/file-index.js +6 -3
- package/dist/infrastructure/index-state.js +2 -0
- package/dist/infrastructure/vault-migration-state.js +69 -0
- package/dist/infrastructure/volatile-memory.js +100 -0
- package/dist/mcp/http-server.js +97 -0
- package/dist/mcp/runtime.js +20 -0
- package/dist/mcp/server.js +36 -13
- package/dist/mcp/tools.js +203 -14
- package/docs/AGENT_USAGE.md +50 -5
- package/docs/ARCHITECTURE.md +11 -0
- package/docs/QUICKSTART.md +3 -1
- package/docs/RELEASE.md +4 -3
- package/package.json +3 -1
|
@@ -1,26 +1,46 @@
|
|
|
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';
|
|
9
13
|
import { searchKnowledge } from '../search-knowledge.js';
|
|
10
|
-
import { loadBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
14
|
+
import { loadBrainlinkConfig, resolveAgentRuntimeDefaults, sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
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';
|
|
14
18
|
import { createClientWorkerJs } from '../frontend/client-worker-js.js';
|
|
19
|
+
import { createClientRenderWorkerJs } from '../frontend/client-render-worker-js.js';
|
|
15
20
|
import { contentTypes, createJsonResponse, isReadMethod, parsePositiveInteger } from './http.js';
|
|
16
21
|
const readSearchMode = async (url) => {
|
|
17
22
|
const config = await loadBrainlinkConfig();
|
|
18
|
-
|
|
23
|
+
const defaults = resolveAgentRuntimeDefaults(config, readAgentQuery(url));
|
|
24
|
+
return sanitizeSearchMode(url.searchParams.get('mode'), defaults.defaultSearchMode);
|
|
25
|
+
};
|
|
26
|
+
const readContextStrategy = async (url) => {
|
|
27
|
+
const config = await loadBrainlinkConfig();
|
|
28
|
+
const defaults = resolveAgentRuntimeDefaults(config, readAgentQuery(url));
|
|
29
|
+
return sanitizeContextStrategy(url.searchParams.get('strategy'), defaults.defaultContextStrategy);
|
|
30
|
+
};
|
|
31
|
+
const readContextCacheTtlMs = async (url) => {
|
|
32
|
+
const config = await loadBrainlinkConfig();
|
|
33
|
+
const defaults = resolveAgentRuntimeDefaults(config, readAgentQuery(url));
|
|
34
|
+
return defaults.defaultContextCacheTtlMs;
|
|
19
35
|
};
|
|
20
36
|
const hasInvalidSearchMode = (url) => {
|
|
21
37
|
const mode = url.searchParams.get('mode');
|
|
22
38
|
return mode !== null && !['fts', 'semantic', 'hybrid'].includes(mode);
|
|
23
39
|
};
|
|
40
|
+
const hasInvalidContextStrategy = (url) => {
|
|
41
|
+
const strategy = url.searchParams.get('strategy');
|
|
42
|
+
return strategy !== null && !['rag', 'cag', 'auto'].includes(strategy);
|
|
43
|
+
};
|
|
24
44
|
const createResponse = (body, statusCode = 200, contentType = 'text/plain; charset=utf-8') => ({
|
|
25
45
|
body,
|
|
26
46
|
statusCode,
|
|
@@ -52,8 +72,68 @@ const sameEntityTag = (candidate, signature) => {
|
|
|
52
72
|
return decodeEntityTag(candidate) === signature;
|
|
53
73
|
};
|
|
54
74
|
const readAgentQuery = (url) => url.searchParams.get('agent') ?? undefined;
|
|
75
|
+
const readContextQuery = (url) => {
|
|
76
|
+
const value = url.searchParams.get('context')?.trim() ?? '';
|
|
77
|
+
return value.length > 0 ? value : undefined;
|
|
78
|
+
};
|
|
79
|
+
const parseNumber = (value, fallback) => {
|
|
80
|
+
const parsed = Number(value);
|
|
81
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
82
|
+
};
|
|
83
|
+
const readJsonBody = async (request, limitBytes = 1_000_000) => {
|
|
84
|
+
let body = '';
|
|
85
|
+
for await (const chunk of request) {
|
|
86
|
+
body += String(chunk);
|
|
87
|
+
if (Buffer.byteLength(body, 'utf8') > limitBytes) {
|
|
88
|
+
throw Object.assign(new Error('Request body too large'), { statusCode: 413 });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return body.trim().length > 0 ? JSON.parse(body) : {};
|
|
92
|
+
};
|
|
93
|
+
const readGraphViewStateInput = (url) => ({
|
|
94
|
+
signature: url.searchParams.get('signature')?.trim() ?? '',
|
|
95
|
+
agentId: readAgentQuery(url),
|
|
96
|
+
context: readContextQuery(url)
|
|
97
|
+
});
|
|
55
98
|
const compactGraphLayoutThreshold = 12_000;
|
|
56
99
|
const compactGraphLayoutEdgeLimit = 60_000;
|
|
100
|
+
const graphLayoutBodyCacheLimit = 8;
|
|
101
|
+
const graphLayoutBodyCache = new Map();
|
|
102
|
+
let cachedClientHtml = null;
|
|
103
|
+
let cachedClientCss = null;
|
|
104
|
+
let cachedClientJs = null;
|
|
105
|
+
let cachedClientWorkerJs = null;
|
|
106
|
+
let cachedClientRenderWorkerJs = null;
|
|
107
|
+
const readClientHtml = () => {
|
|
108
|
+
if (cachedClientHtml === null) {
|
|
109
|
+
cachedClientHtml = createClientHtml();
|
|
110
|
+
}
|
|
111
|
+
return cachedClientHtml;
|
|
112
|
+
};
|
|
113
|
+
const readClientCss = () => {
|
|
114
|
+
if (cachedClientCss === null) {
|
|
115
|
+
cachedClientCss = createClientCss();
|
|
116
|
+
}
|
|
117
|
+
return cachedClientCss;
|
|
118
|
+
};
|
|
119
|
+
const readClientJs = () => {
|
|
120
|
+
if (cachedClientJs === null) {
|
|
121
|
+
cachedClientJs = createClientJs();
|
|
122
|
+
}
|
|
123
|
+
return cachedClientJs;
|
|
124
|
+
};
|
|
125
|
+
const readClientWorkerJs = () => {
|
|
126
|
+
if (cachedClientWorkerJs === null) {
|
|
127
|
+
cachedClientWorkerJs = createClientWorkerJs();
|
|
128
|
+
}
|
|
129
|
+
return cachedClientWorkerJs;
|
|
130
|
+
};
|
|
131
|
+
const readClientRenderWorkerJs = () => {
|
|
132
|
+
if (cachedClientRenderWorkerJs === null) {
|
|
133
|
+
cachedClientRenderWorkerJs = createClientRenderWorkerJs();
|
|
134
|
+
}
|
|
135
|
+
return cachedClientRenderWorkerJs;
|
|
136
|
+
};
|
|
57
137
|
const compactGraphLayoutEdgeLimitFor = (nodeCount) => {
|
|
58
138
|
if (nodeCount > 100_000)
|
|
59
139
|
return 15_000;
|
|
@@ -114,11 +194,25 @@ const compactLayoutPayload = (layout) => {
|
|
|
114
194
|
const compactNodes = layout.nodes.map((node) => [node.id, node.title, node.x, node.y, node.group, node.segment]);
|
|
115
195
|
const compactEdgeRows = compactEdges
|
|
116
196
|
.map((edge) => [edge.source, edge.target, edge.weight, edge.priority]);
|
|
197
|
+
const compactGroups = layout.groups?.map((group) => [
|
|
198
|
+
group.id,
|
|
199
|
+
group.level,
|
|
200
|
+
group.parentId,
|
|
201
|
+
group.title,
|
|
202
|
+
group.x,
|
|
203
|
+
group.y,
|
|
204
|
+
group.radius,
|
|
205
|
+
group.segment,
|
|
206
|
+
group.group,
|
|
207
|
+
group.nodeIds,
|
|
208
|
+
group.childGroupIds
|
|
209
|
+
]);
|
|
117
210
|
return {
|
|
118
211
|
compact: true,
|
|
119
212
|
layout: {
|
|
120
213
|
nodes: compactNodes,
|
|
121
|
-
edges: compactEdgeRows
|
|
214
|
+
edges: compactEdgeRows,
|
|
215
|
+
...(compactGroups && compactGroups.length > 0 ? { groups: compactGroups } : {})
|
|
122
216
|
},
|
|
123
217
|
totals: {
|
|
124
218
|
nodes: layout.nodes.length,
|
|
@@ -127,29 +221,46 @@ const compactLayoutPayload = (layout) => {
|
|
|
127
221
|
};
|
|
128
222
|
};
|
|
129
223
|
const encodeLayoutPayload = (layout) => layout.nodes.length > compactGraphLayoutThreshold ? compactLayoutPayload(layout) : { compact: false, layout: stripLayoutContent(layout) };
|
|
224
|
+
const readGraphLayoutBody = (signature) => graphLayoutBodyCache.get(signature) ?? null;
|
|
225
|
+
const storeGraphLayoutBody = (signature, body) => {
|
|
226
|
+
if (graphLayoutBodyCache.has(signature)) {
|
|
227
|
+
graphLayoutBodyCache.delete(signature);
|
|
228
|
+
}
|
|
229
|
+
graphLayoutBodyCache.set(signature, body);
|
|
230
|
+
while (graphLayoutBodyCache.size > graphLayoutBodyCacheLimit) {
|
|
231
|
+
const oldest = graphLayoutBodyCache.keys().next().value;
|
|
232
|
+
if (!oldest)
|
|
233
|
+
break;
|
|
234
|
+
graphLayoutBodyCache.delete(oldest);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
130
237
|
export const route = async (request, url, vaultPath) => {
|
|
131
238
|
if (isReadMethod(request) && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
132
|
-
return createResponse(
|
|
239
|
+
return createResponse(readClientHtml(), 200, contentTypes['.html']);
|
|
133
240
|
}
|
|
134
241
|
if (isReadMethod(request) && url.pathname === '/styles.css') {
|
|
135
|
-
return createResponse(
|
|
242
|
+
return createResponse(readClientCss(), 200, contentTypes['.css']);
|
|
136
243
|
}
|
|
137
244
|
if (isReadMethod(request) && url.pathname === '/app.js') {
|
|
138
|
-
return createResponse(
|
|
245
|
+
return createResponse(readClientJs(), 200, contentTypes['.js']);
|
|
139
246
|
}
|
|
140
247
|
if (isReadMethod(request) && url.pathname === '/app-worker.js') {
|
|
141
|
-
return createResponse(
|
|
248
|
+
return createResponse(readClientWorkerJs(), 200, contentTypes['.js']);
|
|
249
|
+
}
|
|
250
|
+
if (isReadMethod(request) && url.pathname === '/render-worker.js') {
|
|
251
|
+
return createResponse(readClientRenderWorkerJs(), 200, contentTypes['.js']);
|
|
142
252
|
}
|
|
143
253
|
if (isReadMethod(request) && url.pathname === '/api/graph') {
|
|
144
254
|
return createResponse(createJsonResponse(await getGraph(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
|
|
145
255
|
}
|
|
146
256
|
if (isReadMethod(request) && url.pathname === '/api/graph-layout') {
|
|
147
|
-
const { signature, layout } = await getGraphLayout(vaultPath,
|
|
257
|
+
const { signature, layout } = await getGraphLayout(vaultPath, {
|
|
258
|
+
agentId: readAgentQuery(url),
|
|
259
|
+
context: readContextQuery(url)
|
|
260
|
+
});
|
|
148
261
|
const requestEtags = request.headers['if-none-match'];
|
|
149
262
|
const notModified = sameEntityTag(requestEtags, signature);
|
|
150
263
|
const etag = encodeEntityTag(signature);
|
|
151
|
-
const body = createJsonResponse({ signature, ...encodeLayoutPayload(layout) });
|
|
152
|
-
const jsonResponse = createResponse(body, 200, contentTypes['.json']);
|
|
153
264
|
const notModifiedResponse = createResponse('', 304, contentTypes['.json']);
|
|
154
265
|
if (notModified) {
|
|
155
266
|
return {
|
|
@@ -160,6 +271,12 @@ export const route = async (request, url, vaultPath) => {
|
|
|
160
271
|
}
|
|
161
272
|
};
|
|
162
273
|
}
|
|
274
|
+
const cachedBody = readGraphLayoutBody(signature);
|
|
275
|
+
const body = cachedBody ?? createJsonResponse({ signature, ...encodeLayoutPayload(layout) });
|
|
276
|
+
if (!cachedBody) {
|
|
277
|
+
storeGraphLayoutBody(signature, body);
|
|
278
|
+
}
|
|
279
|
+
const jsonResponse = createResponse(body, 200, contentTypes['.json']);
|
|
163
280
|
return {
|
|
164
281
|
...jsonResponse,
|
|
165
282
|
headers: {
|
|
@@ -168,6 +285,66 @@ export const route = async (request, url, vaultPath) => {
|
|
|
168
285
|
}
|
|
169
286
|
};
|
|
170
287
|
}
|
|
288
|
+
if (isReadMethod(request) && url.pathname === '/api/graph-view') {
|
|
289
|
+
return createResponse(createJsonResponse(await getGraphView(vaultPath, {
|
|
290
|
+
x: parseNumber(url.searchParams.get('x'), -1000),
|
|
291
|
+
y: parseNumber(url.searchParams.get('y'), -1000),
|
|
292
|
+
width: parseNumber(url.searchParams.get('w'), 2000),
|
|
293
|
+
height: parseNumber(url.searchParams.get('h'), 2000),
|
|
294
|
+
scale: parseNumber(url.searchParams.get('scale'), 1),
|
|
295
|
+
agentId: readAgentQuery(url),
|
|
296
|
+
context: readContextQuery(url)
|
|
297
|
+
})), 200, contentTypes['.json']);
|
|
298
|
+
}
|
|
299
|
+
if (isReadMethod(request) && url.pathname === '/api/graph-stream') {
|
|
300
|
+
const x = parseNumber(url.searchParams.get('x'), -1000);
|
|
301
|
+
const y = parseNumber(url.searchParams.get('y'), -1000);
|
|
302
|
+
const width = parseNumber(url.searchParams.get('w'), 2000);
|
|
303
|
+
const height = parseNumber(url.searchParams.get('h'), 2000);
|
|
304
|
+
const scale = parseNumber(url.searchParams.get('scale'), 0.24);
|
|
305
|
+
const nodeBudget = parsePositiveInteger(url.searchParams.get('nodeBudget'), 1800);
|
|
306
|
+
const edgeBudget = parsePositiveInteger(url.searchParams.get('edgeBudget'), 5000);
|
|
307
|
+
return createResponse(createJsonResponse(await getGraphStreamChunk(vaultPath, {
|
|
308
|
+
x,
|
|
309
|
+
y,
|
|
310
|
+
width,
|
|
311
|
+
height,
|
|
312
|
+
scale,
|
|
313
|
+
nodeBudget,
|
|
314
|
+
edgeBudget,
|
|
315
|
+
agentId: readAgentQuery(url),
|
|
316
|
+
context: readContextQuery(url)
|
|
317
|
+
})), 200, contentTypes['.json']);
|
|
318
|
+
}
|
|
319
|
+
if (isReadMethod(request) && url.pathname === '/api/graph-view-state') {
|
|
320
|
+
const input = readGraphViewStateInput(url);
|
|
321
|
+
if (!input.signature) {
|
|
322
|
+
return createResponse(createJsonResponse({ error: 'Missing signature query parameter' }), 400, contentTypes['.json']);
|
|
323
|
+
}
|
|
324
|
+
return createResponse(createJsonResponse(await getGraphViewState(vaultPath, input)), 200, contentTypes['.json']);
|
|
325
|
+
}
|
|
326
|
+
if (request.method === 'POST' && 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
|
+
const body = await readJsonBody(request);
|
|
332
|
+
const positions = Array.isArray(body.positions)
|
|
333
|
+
? body.positions.map((position) => ({
|
|
334
|
+
id: String(position.id ?? ''),
|
|
335
|
+
x: Number(position.x),
|
|
336
|
+
y: Number(position.y)
|
|
337
|
+
}))
|
|
338
|
+
: [];
|
|
339
|
+
return createResponse(createJsonResponse(await saveGraphViewState(vaultPath, { ...input, positions })), 200, contentTypes['.json']);
|
|
340
|
+
}
|
|
341
|
+
if (request.method === 'DELETE' && url.pathname === '/api/graph-view-state') {
|
|
342
|
+
const input = readGraphViewStateInput(url);
|
|
343
|
+
if (!input.signature) {
|
|
344
|
+
return createResponse(createJsonResponse({ error: 'Missing signature query parameter' }), 400, contentTypes['.json']);
|
|
345
|
+
}
|
|
346
|
+
return createResponse(createJsonResponse(await deleteGraphViewState(vaultPath, input)), 200, contentTypes['.json']);
|
|
347
|
+
}
|
|
171
348
|
if (isReadMethod(request) && url.pathname === '/api/graph-node') {
|
|
172
349
|
const id = url.searchParams.get('id')?.trim() ?? '';
|
|
173
350
|
if (!id) {
|
|
@@ -185,12 +362,15 @@ export const route = async (request, url, vaultPath) => {
|
|
|
185
362
|
if (!query) {
|
|
186
363
|
return createResponse(createJsonResponse({ query, nodeIds: [] }), 200, contentTypes['.json']);
|
|
187
364
|
}
|
|
188
|
-
const nodeIds = await searchGraphNodeIds(vaultPath, query, limit, readAgentQuery(url));
|
|
365
|
+
const nodeIds = await searchGraphNodeIds(vaultPath, query, limit, readAgentQuery(url), readContextQuery(url));
|
|
189
366
|
return createResponse(createJsonResponse({ query, nodeIds }), 200, contentTypes['.json']);
|
|
190
367
|
}
|
|
191
368
|
if (isReadMethod(request) && url.pathname === '/api/agents') {
|
|
192
369
|
return createResponse(createJsonResponse({ agents: await listAgents(vaultPath) }), 200, contentTypes['.json']);
|
|
193
370
|
}
|
|
371
|
+
if (isReadMethod(request) && url.pathname === '/api/graph-contexts') {
|
|
372
|
+
return createResponse(createJsonResponse({ contexts: await getGraphContexts(vaultPath, readAgentQuery(url)) }), 200, contentTypes['.json']);
|
|
373
|
+
}
|
|
194
374
|
if (isReadMethod(request) && url.pathname === '/api/search') {
|
|
195
375
|
const query = url.searchParams.get('q') ?? '';
|
|
196
376
|
const limit = parsePositiveInteger(url.searchParams.get('limit'), 10);
|
|
@@ -205,10 +385,15 @@ export const route = async (request, url, vaultPath) => {
|
|
|
205
385
|
const limit = parsePositiveInteger(url.searchParams.get('limit'), 12);
|
|
206
386
|
const tokens = parsePositiveInteger(url.searchParams.get('tokens'), 2000);
|
|
207
387
|
const mode = await readSearchMode(url);
|
|
388
|
+
const strategy = await readContextStrategy(url);
|
|
389
|
+
const contextCacheTtlMs = await readContextCacheTtlMs(url);
|
|
208
390
|
if (hasInvalidSearchMode(url)) {
|
|
209
391
|
return createResponse(createJsonResponse({ error: 'Invalid mode. Use fts, semantic or hybrid.' }), 400, contentTypes['.json']);
|
|
210
392
|
}
|
|
211
|
-
|
|
393
|
+
if (hasInvalidContextStrategy(url)) {
|
|
394
|
+
return createResponse(createJsonResponse({ error: 'Invalid strategy. Use rag, cag or auto.' }), 400, contentTypes['.json']);
|
|
395
|
+
}
|
|
396
|
+
return createResponse(createJsonResponse(await buildContextPackage(vaultPath, query, limit, tokens, readAgentQuery(url), mode, strategy, contextCacheTtlMs)), 200, contentTypes['.json']);
|
|
212
397
|
}
|
|
213
398
|
if (isReadMethod(request) && url.pathname === '/api/links') {
|
|
214
399
|
return createResponse(createJsonResponse({ links: await listLinks(vaultPath, readAgentQuery(url)) }), 200, contentTypes['.json']);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { getBrokenLinksReport, getExtendedStats, getOrphansReport, getStats, validateVault } from '../../application/analyze-vault.js';
|
|
2
|
-
import { buildContextPackage } from '../../application/build-context.js';
|
|
2
|
+
import { buildContextPackage, readContextDataSignature } from '../../application/build-context.js';
|
|
3
3
|
import { getGraph } from '../../application/get-graph.js';
|
|
4
4
|
import { listAgents } from '../../application/list-agents.js';
|
|
5
5
|
import { listBacklinks, listLinks } from '../../application/list-links.js';
|
|
6
6
|
import { searchKnowledge } from '../../application/search-knowledge.js';
|
|
7
|
-
import { sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
7
|
+
import { sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
8
|
+
import { clearContextPacks, listContextPacks } from '../../infrastructure/context-packs.js';
|
|
8
9
|
import { parsePositiveInteger, print, resolveOptions } from '../runtime.js';
|
|
9
10
|
export const registerReadCommands = (program) => {
|
|
10
11
|
program
|
|
@@ -61,14 +62,49 @@ export const registerReadCommands = (program) => {
|
|
|
61
62
|
.option('-l, --limit <limit>', 'maximum search results before context selection')
|
|
62
63
|
.option('-t, --tokens <tokens>', 'maximum estimated context tokens')
|
|
63
64
|
.option('-m, --mode <mode>', 'search mode: fts, semantic or hybrid')
|
|
65
|
+
.option('--strategy <strategy>', 'context strategy: rag, cag or auto')
|
|
64
66
|
.option('--json', 'print machine-readable JSON')
|
|
65
67
|
.description('build a compact context package for an agent')
|
|
66
68
|
.action(async (query, options) => {
|
|
67
69
|
const resolved = await resolveOptions(options);
|
|
68
70
|
const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
|
|
69
|
-
const
|
|
71
|
+
const strategy = sanitizeContextStrategy(options.strategy, resolved.defaults.defaultContextStrategy);
|
|
72
|
+
const contextPackage = await buildContextPackage(resolved.vault, query, parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit), parsePositiveInteger(options.tokens ?? String(resolved.defaults.defaultContextTokens), resolved.defaults.defaultContextTokens), resolved.agent, mode, strategy, resolved.defaults.defaultContextCacheTtlMs);
|
|
70
73
|
print(options.json, contextPackage, () => contextPackage.content);
|
|
71
74
|
});
|
|
75
|
+
program
|
|
76
|
+
.command('context-packs')
|
|
77
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
78
|
+
.option('-a, --agent <agent>', 'accepted for consistency; context packs are already keyed by agent')
|
|
79
|
+
.option('--stale', 'operate only on packs stale for the current index and volatile-memory signature')
|
|
80
|
+
.option('--clear', 'remove context packs instead of listing them')
|
|
81
|
+
.option('--json', 'print machine-readable JSON')
|
|
82
|
+
.description('list or clear persisted CAG context packs')
|
|
83
|
+
.action(async (options) => {
|
|
84
|
+
const resolved = await resolveOptions(options);
|
|
85
|
+
const dataSignature = await readContextDataSignature(resolved.vault);
|
|
86
|
+
if (options.clear) {
|
|
87
|
+
const result = await clearContextPacks(resolved.vault, {
|
|
88
|
+
staleOnly: options.stale === true,
|
|
89
|
+
dataSignature
|
|
90
|
+
});
|
|
91
|
+
print(options.json, { vault: resolved.vault, dataSignature, ...result }, () => [
|
|
92
|
+
`Removed context packs: ${result.removed.length}`,
|
|
93
|
+
`Kept context packs: ${result.kept.length}`
|
|
94
|
+
].join('\n'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const packs = await listContextPacks(resolved.vault, dataSignature);
|
|
98
|
+
const visiblePacks = options.stale ? packs.filter((pack) => pack.stale) : packs;
|
|
99
|
+
print(options.json, { vault: resolved.vault, dataSignature, packs: visiblePacks }, () => visiblePacks.length === 0
|
|
100
|
+
? 'No context packs found.'
|
|
101
|
+
: visiblePacks
|
|
102
|
+
.map((pack) => [
|
|
103
|
+
`${pack.filename} ${pack.stale ? 'stale' : 'fresh'} ${pack.sizeBytes} bytes`,
|
|
104
|
+
pack.key ? `query="${pack.key.query}" agent=${pack.key.agentId ?? '*'} mode=${pack.key.mode ?? 'default'} limit=${pack.key.limit} tokens=${pack.key.maxTokens}` : 'unreadable pack'
|
|
105
|
+
].join('\n'))
|
|
106
|
+
.join('\n\n'));
|
|
107
|
+
});
|
|
72
108
|
program
|
|
73
109
|
.command('graph')
|
|
74
110
|
.option('-v, --vault <vault>', 'vault directory')
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { extname, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { doctorVault } from '../../application/analyze-vault.js';
|
|
4
|
+
import { defaultBrainlinkConfig, loadBrainlinkConfig, loadRawConfig, writeRawConfig } from '../../infrastructure/config.js';
|
|
5
|
+
import { assertVaultAllowed, isBucketVaultPath, resolveVaultPath } from '../../infrastructure/file-system-vault.js';
|
|
6
|
+
import { print } from '../runtime.js';
|
|
7
|
+
const excludedDirectories = new Set(['.git', 'node_modules', 'dist']);
|
|
8
|
+
const resolveScope = (globalOption) => globalOption ? 'global' : 'local';
|
|
9
|
+
const normalizeVaultPath = (vault) => assertVaultAllowed(vault, []);
|
|
10
|
+
const uniqueValues = (values) => Array.from(new Set(values));
|
|
11
|
+
const sameVault = (left, right) => normalizeVaultPath(left) === normalizeVaultPath(right);
|
|
12
|
+
const isDirectory = async (path) => {
|
|
13
|
+
try {
|
|
14
|
+
return (await stat(path)).isDirectory();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const countMarkdownFiles = async (directory) => {
|
|
21
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
22
|
+
const counts = await Promise.all(entries.map(async (entry) => {
|
|
23
|
+
const absolutePath = join(directory, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
return excludedDirectories.has(entry.name) ? 0 : countMarkdownFiles(absolutePath);
|
|
26
|
+
}
|
|
27
|
+
return entry.isFile() && extname(entry.name).toLowerCase() === '.md' ? 1 : 0;
|
|
28
|
+
}));
|
|
29
|
+
return counts.reduce((total, count) => total + count, 0);
|
|
30
|
+
};
|
|
31
|
+
const readIndexedDocumentCount = async (vaultPath) => {
|
|
32
|
+
try {
|
|
33
|
+
const raw = await readFile(join(vaultPath, '.brainlink', 'index.json'), 'utf8');
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
return Array.isArray(parsed.documents) ? parsed.documents.length : null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const addCandidate = (state, path, source) => {
|
|
42
|
+
const normalized = normalizeVaultPath(path);
|
|
43
|
+
const current = state.get(normalized);
|
|
44
|
+
return new Map(state).set(normalized, {
|
|
45
|
+
path: normalized,
|
|
46
|
+
sources: uniqueValues([...(current?.sources ?? []), source])
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
const listVaultEntries = async () => {
|
|
50
|
+
const config = await loadBrainlinkConfig();
|
|
51
|
+
const candidates = [config.vault, ...config.allowedVaults, defaultBrainlinkConfig.vault].reduce((state, path, index) => addCandidate(state, path, index === 0 ? 'configured' : path === defaultBrainlinkConfig.vault ? 'default' : 'allowed'), new Map());
|
|
52
|
+
const entries = await Promise.all(Array.from(candidates.values()).map(async (candidate) => {
|
|
53
|
+
if (isBucketVaultPath(candidate.path)) {
|
|
54
|
+
return {
|
|
55
|
+
path: candidate.path,
|
|
56
|
+
sources: candidate.sources,
|
|
57
|
+
current: sameVault(candidate.path, config.vault),
|
|
58
|
+
kind: 'bucket',
|
|
59
|
+
exists: null,
|
|
60
|
+
markdownCount: null,
|
|
61
|
+
indexedDocumentCount: null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const path = resolveVaultPath(candidate.path);
|
|
65
|
+
const exists = await isDirectory(path);
|
|
66
|
+
return {
|
|
67
|
+
path,
|
|
68
|
+
sources: candidate.sources,
|
|
69
|
+
current: sameVault(path, config.vault),
|
|
70
|
+
kind: 'local',
|
|
71
|
+
exists,
|
|
72
|
+
markdownCount: exists ? await countMarkdownFiles(path) : null,
|
|
73
|
+
indexedDocumentCount: exists ? await readIndexedDocumentCount(path) : null
|
|
74
|
+
};
|
|
75
|
+
}));
|
|
76
|
+
return entries.sort((left, right) => Number(right.current) - Number(left.current) || left.path.localeCompare(right.path));
|
|
77
|
+
};
|
|
78
|
+
const removeVaultFromAllowedVaults = async (vaultPath) => {
|
|
79
|
+
const scopes = ['local', 'global'];
|
|
80
|
+
await Promise.all(scopes.map(async (scope) => {
|
|
81
|
+
const rawConfig = await loadRawConfig(scope);
|
|
82
|
+
const allowedVaults = Array.isArray(rawConfig.allowedVaults)
|
|
83
|
+
? rawConfig.allowedVaults.filter((path) => typeof path === 'string')
|
|
84
|
+
: [];
|
|
85
|
+
const nextAllowedVaults = allowedVaults.filter((path) => !sameVault(path, vaultPath));
|
|
86
|
+
if (nextAllowedVaults.length === allowedVaults.length) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await writeRawConfig(scope, {
|
|
90
|
+
...rawConfig,
|
|
91
|
+
allowedVaults: nextAllowedVaults
|
|
92
|
+
});
|
|
93
|
+
}));
|
|
94
|
+
};
|
|
95
|
+
export const registerVaultCommands = (program) => {
|
|
96
|
+
const vaultsCommand = program.command('vaults').description('list, choose and delete Brainlink vaults');
|
|
97
|
+
vaultsCommand
|
|
98
|
+
.command('list')
|
|
99
|
+
.option('--json', 'print machine-readable JSON')
|
|
100
|
+
.description('list known Brainlink vaults from config and defaults')
|
|
101
|
+
.action(async (options) => {
|
|
102
|
+
const config = await loadBrainlinkConfig();
|
|
103
|
+
const vaults = await listVaultEntries();
|
|
104
|
+
print(options.json, {
|
|
105
|
+
currentVault: config.vault,
|
|
106
|
+
vaults
|
|
107
|
+
}, () => vaults
|
|
108
|
+
.map((vault) => {
|
|
109
|
+
const status = vault.exists === null ? 'remote' : vault.exists ? 'exists' : 'missing';
|
|
110
|
+
const counts = vault.markdownCount === null
|
|
111
|
+
? ''
|
|
112
|
+
: ` markdown=${vault.markdownCount} indexed=${vault.indexedDocumentCount ?? 'unknown'}`;
|
|
113
|
+
return `${vault.current ? '* ' : ' '}${vault.path} [${vault.sources.join(',')}; ${status}; ${vault.kind}]${counts}`;
|
|
114
|
+
})
|
|
115
|
+
.join('\n'));
|
|
116
|
+
});
|
|
117
|
+
vaultsCommand
|
|
118
|
+
.command('use <vault>')
|
|
119
|
+
.option('--global', 'write to global config in $BRAINLINK_HOME/brainlink.config.json')
|
|
120
|
+
.option('--no-allowlist', 'do not append the vault to allowedVaults in the target config file')
|
|
121
|
+
.option('--json', 'print machine-readable JSON')
|
|
122
|
+
.description('choose the default Brainlink vault without migrating memory')
|
|
123
|
+
.action(async (vault, options) => {
|
|
124
|
+
const scope = resolveScope(options.global);
|
|
125
|
+
const before = await loadBrainlinkConfig();
|
|
126
|
+
const targetVault = normalizeVaultPath(vault);
|
|
127
|
+
const rawConfig = await loadRawConfig(scope);
|
|
128
|
+
const shouldAllowlist = options.allowlist !== false;
|
|
129
|
+
const allowedVaults = Array.isArray(rawConfig.allowedVaults)
|
|
130
|
+
? rawConfig.allowedVaults.filter((path) => typeof path === 'string')
|
|
131
|
+
: [];
|
|
132
|
+
const nextAllowedVaults = shouldAllowlist ? uniqueValues([...allowedVaults, targetVault]) : allowedVaults;
|
|
133
|
+
const configPath = await writeRawConfig(scope, {
|
|
134
|
+
...rawConfig,
|
|
135
|
+
vault: targetVault,
|
|
136
|
+
allowedVaults: nextAllowedVaults
|
|
137
|
+
});
|
|
138
|
+
const doctor = await doctorVault(targetVault);
|
|
139
|
+
print(options.json, {
|
|
140
|
+
scope,
|
|
141
|
+
configPath,
|
|
142
|
+
previousVault: before.vault,
|
|
143
|
+
vault: targetVault,
|
|
144
|
+
doctor
|
|
145
|
+
}, () => `Default ${scope} vault set to ${targetVault} in ${configPath}.`);
|
|
146
|
+
});
|
|
147
|
+
vaultsCommand
|
|
148
|
+
.command('delete <vault>')
|
|
149
|
+
.option('--yes', 'confirm destructive vault deletion')
|
|
150
|
+
.option('--prune-config', 'remove the vault from allowedVaults after deletion')
|
|
151
|
+
.option('--json', 'print machine-readable JSON')
|
|
152
|
+
.description('delete a local filesystem vault after explicit confirmation')
|
|
153
|
+
.action(async (vault, options) => {
|
|
154
|
+
const config = await loadBrainlinkConfig();
|
|
155
|
+
const targetVault = normalizeVaultPath(vault);
|
|
156
|
+
if (isBucketVaultPath(targetVault)) {
|
|
157
|
+
throw new Error('Refusing to delete bucket vaults from the CLI. Remove bucket data with your storage provider tooling.');
|
|
158
|
+
}
|
|
159
|
+
const absoluteVault = resolveVaultPath(targetVault);
|
|
160
|
+
if (!isAbsolute(absoluteVault)) {
|
|
161
|
+
throw new Error(`Refusing to delete non-absolute vault path: ${absoluteVault}`);
|
|
162
|
+
}
|
|
163
|
+
if (sameVault(absoluteVault, config.vault)) {
|
|
164
|
+
throw new Error('Refusing to delete the current default vault. Choose another default with `blink vaults use <vault>` first.');
|
|
165
|
+
}
|
|
166
|
+
if (options.yes !== true) {
|
|
167
|
+
throw new Error('Refusing to delete vault without --yes.');
|
|
168
|
+
}
|
|
169
|
+
const existed = await isDirectory(absoluteVault);
|
|
170
|
+
if (existed) {
|
|
171
|
+
await rm(absoluteVault, { recursive: true, force: false });
|
|
172
|
+
}
|
|
173
|
+
if (options.pruneConfig) {
|
|
174
|
+
await removeVaultFromAllowedVaults(absoluteVault);
|
|
175
|
+
}
|
|
176
|
+
print(options.json, {
|
|
177
|
+
vault: absoluteVault,
|
|
178
|
+
deleted: existed,
|
|
179
|
+
prunedConfig: options.pruneConfig === true
|
|
180
|
+
}, () => `${existed ? 'Deleted' : 'Vault did not exist'}: ${absoluteVault}`);
|
|
181
|
+
});
|
|
182
|
+
};
|