@andespindola/brainlink 0.1.0-beta.163 → 0.1.0-beta.165
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/README.md +14 -4
- package/dist/application/build-context.js +105 -11
- package/dist/application/frontend/client-css.js +37 -31
- package/dist/application/frontend/client-js.js +103 -37
- package/dist/application/server/routes.js +17 -3
- package/dist/cli/commands/read-commands.js +39 -3
- package/dist/cli/commands/write-commands.js +4 -2
- package/dist/infrastructure/config.js +11 -2
- package/dist/infrastructure/context-packs.js +122 -0
- package/dist/mcp/server.js +8 -3
- package/dist/mcp/tools.js +82 -7
- package/docs/AGENT_USAGE.md +8 -1
- package/docs/ARCHITECTURE.md +6 -0
- package/docs/QUICKSTART.md +2 -1
- package/package.json +1 -1
|
@@ -369,28 +369,28 @@ const parseColor = (hex) => {
|
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
const graphTheme = {
|
|
372
|
-
node: parseColor('#
|
|
373
|
-
nodeCluster: parseColor('#
|
|
374
|
-
nodeHighlight: parseColor('#
|
|
375
|
-
nodeSelected: parseColor('#
|
|
372
|
+
node: parseColor('#5aa8ff'),
|
|
373
|
+
nodeCluster: parseColor('#3f7fbd'),
|
|
374
|
+
nodeHighlight: parseColor('#ffcb67'),
|
|
375
|
+
nodeSelected: parseColor('#edf4ff'),
|
|
376
376
|
nodePalette: [
|
|
377
|
-
parseColor('#
|
|
378
|
-
parseColor('#
|
|
379
|
-
parseColor('#
|
|
380
|
-
parseColor('#
|
|
381
|
-
parseColor('#
|
|
382
|
-
parseColor('#
|
|
383
|
-
parseColor('#
|
|
384
|
-
parseColor('#
|
|
385
|
-
parseColor('#
|
|
386
|
-
parseColor('#
|
|
377
|
+
parseColor('#5aa8ff'),
|
|
378
|
+
parseColor('#5ecf92'),
|
|
379
|
+
parseColor('#ffb65c'),
|
|
380
|
+
parseColor('#ff7dac'),
|
|
381
|
+
parseColor('#a88fff'),
|
|
382
|
+
parseColor('#59d0dd'),
|
|
383
|
+
parseColor('#ff8f6a'),
|
|
384
|
+
parseColor('#a4b3c3'),
|
|
385
|
+
parseColor('#c9945f'),
|
|
386
|
+
parseColor('#7cb6ff')
|
|
387
387
|
],
|
|
388
|
-
edge: [0.
|
|
389
|
-
edgeHeavy: [0.
|
|
390
|
-
clear: parseColor('#
|
|
388
|
+
edge: [0.59, 0.71, 0.83, 0.14],
|
|
389
|
+
edgeHeavy: [0.59, 0.71, 0.83, 0.3],
|
|
390
|
+
clear: parseColor('#08131d')
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
const segmentPalette = ['#
|
|
393
|
+
const segmentPalette = ['#5aa8ff', '#5ecf92', '#ffb65c', '#ff7dac', '#a88fff', '#59d0dd', '#ff8f6a', '#a4b3c3', '#c9945f', '#7cb6ff']
|
|
394
394
|
|
|
395
395
|
const segmentColorIndex = (segment) => {
|
|
396
396
|
const value = String(segment || '')
|
|
@@ -402,6 +402,8 @@ const segmentColorIndex = (segment) => {
|
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
const segmentColor = (segment) => segmentPalette[segmentColorIndex(segment)] || segmentPalette[0]
|
|
405
|
+
const nodeKind = (node) => node?.[6] === 'cluster' ? 'cluster' : 'node'
|
|
406
|
+
const isRealGraphNode = (node) => nodeKind(node) === 'node'
|
|
405
407
|
|
|
406
408
|
const clampScale = (scale) => Math.max(zoomRange.min, Math.min(zoomRange.max, scale))
|
|
407
409
|
|
|
@@ -543,7 +545,7 @@ const drawFallback = () => {
|
|
|
543
545
|
canvas.width = Math.floor(width * ratio)
|
|
544
546
|
canvas.height = Math.floor(height * ratio)
|
|
545
547
|
ctx2dFallback.setTransform(ratio, 0, 0, ratio, 0, 0)
|
|
546
|
-
ctx2dFallback.fillStyle = '#
|
|
548
|
+
ctx2dFallback.fillStyle = '#08131d'
|
|
547
549
|
ctx2dFallback.fillRect(0, 0, width, height)
|
|
548
550
|
|
|
549
551
|
const nodes = Array.isArray(state.chunk.nodes) ? state.chunk.nodes : []
|
|
@@ -553,7 +555,7 @@ const drawFallback = () => {
|
|
|
553
555
|
nodeById.set(nodes[i][0], nodes[i])
|
|
554
556
|
}
|
|
555
557
|
|
|
556
|
-
ctx2dFallback.strokeStyle = 'rgba(
|
|
558
|
+
ctx2dFallback.strokeStyle = 'rgba(151,181,212,0.18)'
|
|
557
559
|
ctx2dFallback.lineWidth = 1
|
|
558
560
|
for (let i = 0; i < edges.length; i += 1) {
|
|
559
561
|
const edge = edges[i]
|
|
@@ -576,12 +578,12 @@ const drawFallback = () => {
|
|
|
576
578
|
const radius = Math.max(2.4, Math.min(14, 4 + node[7] * 0.55))
|
|
577
579
|
|
|
578
580
|
ctx2dFallback.beginPath()
|
|
579
|
-
ctx2dFallback.fillStyle = selected ? '#
|
|
581
|
+
ctx2dFallback.fillStyle = selected ? '#edf4ff' : color
|
|
580
582
|
ctx2dFallback.arc(p.x, p.y, radius, 0, Math.PI * 2)
|
|
581
583
|
ctx2dFallback.fill()
|
|
582
584
|
}
|
|
583
585
|
|
|
584
|
-
ctx2dFallback.fillStyle = '#
|
|
586
|
+
ctx2dFallback.fillStyle = '#97a9bd'
|
|
585
587
|
ctx2dFallback.font = '12px Inter, system-ui, sans-serif'
|
|
586
588
|
ctx2dFallback.textAlign = 'center'
|
|
587
589
|
ctx2dFallback.fillText('Fallback canvas mode', Math.max(width, 320) / 2, 24)
|
|
@@ -670,6 +672,28 @@ const updateNodePositionInChunk = (nodeId, x, y) => {
|
|
|
670
672
|
updateGraphOverlays()
|
|
671
673
|
}
|
|
672
674
|
|
|
675
|
+
const focusNodeInViewport = (nodeId, nextScale = null) => {
|
|
676
|
+
const node = nodeByIdFromChunk().get(nodeId)
|
|
677
|
+
if (!node) {
|
|
678
|
+
return false
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const x = Number(node[2])
|
|
682
|
+
const y = Number(node[3])
|
|
683
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
684
|
+
return false
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (Number.isFinite(nextScale)) {
|
|
688
|
+
state.camera.scale = clampScale(Number(nextScale))
|
|
689
|
+
}
|
|
690
|
+
state.camera.x = state.viewport.width / 2 - x * state.camera.scale
|
|
691
|
+
state.camera.y = state.viewport.height / 2 - y * state.camera.scale
|
|
692
|
+
updateWorkerCamera()
|
|
693
|
+
scheduleChunkFetch()
|
|
694
|
+
return true
|
|
695
|
+
}
|
|
696
|
+
|
|
673
697
|
const showTooltip = (node, pointer) => {
|
|
674
698
|
if (!elements.tooltip || !node) {
|
|
675
699
|
return
|
|
@@ -743,7 +767,7 @@ const drawMiniMap = () => {
|
|
|
743
767
|
miniMap.height = Math.floor(height * ratio)
|
|
744
768
|
ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
|
|
745
769
|
ctx.clearRect(0, 0, width, height)
|
|
746
|
-
ctx.fillStyle = 'rgba(
|
|
770
|
+
ctx.fillStyle = 'rgba(8, 19, 29, 0.88)'
|
|
747
771
|
ctx.fillRect(0, 0, width, height)
|
|
748
772
|
|
|
749
773
|
const xs = nodes.map((node) => Number(node[2])).filter(Number.isFinite)
|
|
@@ -763,7 +787,7 @@ const drawMiniMap = () => {
|
|
|
763
787
|
})
|
|
764
788
|
state.miniMapView = { minX, minY, scale, offsetX, offsetY, width, height }
|
|
765
789
|
|
|
766
|
-
ctx.fillStyle = 'rgba(
|
|
790
|
+
ctx.fillStyle = 'rgba(90, 168, 255, 0.62)'
|
|
767
791
|
nodes.forEach((node) => {
|
|
768
792
|
const point = toMini(Number(node[2]), Number(node[3]))
|
|
769
793
|
ctx.fillRect(point.x - 1, point.y - 1, 2, 2)
|
|
@@ -773,7 +797,7 @@ const drawMiniMap = () => {
|
|
|
773
797
|
const worldBottomRight = screenToWorld(state.viewport.width, state.viewport.height)
|
|
774
798
|
const topLeft = toMini(Math.min(worldTopLeft.x, worldBottomRight.x), Math.min(worldTopLeft.y, worldBottomRight.y))
|
|
775
799
|
const bottomRight = toMini(Math.max(worldTopLeft.x, worldBottomRight.x), Math.max(worldTopLeft.y, worldBottomRight.y))
|
|
776
|
-
ctx.strokeStyle = 'rgba(
|
|
800
|
+
ctx.strokeStyle = 'rgba(90, 168, 255, 0.86)'
|
|
777
801
|
ctx.lineWidth = 1
|
|
778
802
|
ctx.strokeRect(topLeft.x, topLeft.y, Math.max(3, bottomRight.x - topLeft.x), Math.max(3, bottomRight.y - topLeft.y))
|
|
779
803
|
}
|
|
@@ -1128,11 +1152,28 @@ const pickFallbackNodeId = (screenX, screenY) => {
|
|
|
1128
1152
|
return typeof node?.[0] === 'string' ? node[0] : ''
|
|
1129
1153
|
}
|
|
1130
1154
|
|
|
1155
|
+
const handlePickedNode = (node) => {
|
|
1156
|
+
const nodeId = typeof node?.id === 'string' ? node.id : typeof node?.[0] === 'string' ? node[0] : ''
|
|
1157
|
+
if (!nodeId) {
|
|
1158
|
+
return
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const kind = typeof node?.kind === 'string' ? node.kind : nodeKind(node)
|
|
1162
|
+
if (kind === 'cluster') {
|
|
1163
|
+
const currentScale = state.camera.scale
|
|
1164
|
+
const targetScale = currentScale < 0.22 ? 0.28 : Math.min(1.1, currentScale * 1.6)
|
|
1165
|
+
focusNodeInViewport(nodeId, targetScale)
|
|
1166
|
+
return
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
loadNodeDetails(nodeId).catch((error) => console.error(error))
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1131
1172
|
const pickAt = (screenX, screenY) => {
|
|
1132
1173
|
if (state.rendererMode === 'fallback') {
|
|
1133
|
-
const
|
|
1134
|
-
if (
|
|
1135
|
-
|
|
1174
|
+
const node = pickFallbackNode(screenX, screenY)
|
|
1175
|
+
if (node) {
|
|
1176
|
+
handlePickedNode(node)
|
|
1136
1177
|
}
|
|
1137
1178
|
return
|
|
1138
1179
|
}
|
|
@@ -1170,6 +1211,19 @@ const resolvePointer = (event) => {
|
|
|
1170
1211
|
|
|
1171
1212
|
const setupInput = () => {
|
|
1172
1213
|
const dragActivationDistance = 6
|
|
1214
|
+
const resetPointerState = (pointerId = null) => {
|
|
1215
|
+
state.pointer.down = false
|
|
1216
|
+
state.pointer.dragging = false
|
|
1217
|
+
state.pointer.dragNodeId = ''
|
|
1218
|
+
canvas.classList.remove('is-node-dragging')
|
|
1219
|
+
if (pointerId !== null) {
|
|
1220
|
+
try {
|
|
1221
|
+
if (canvas.hasPointerCapture(pointerId)) {
|
|
1222
|
+
canvas.releasePointerCapture(pointerId)
|
|
1223
|
+
}
|
|
1224
|
+
} catch {}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1173
1227
|
|
|
1174
1228
|
canvas.addEventListener('wheel', (event) => {
|
|
1175
1229
|
event.preventDefault()
|
|
@@ -1180,9 +1234,10 @@ const setupInput = () => {
|
|
|
1180
1234
|
}, { passive: false })
|
|
1181
1235
|
|
|
1182
1236
|
canvas.addEventListener('pointerdown', (event) => {
|
|
1237
|
+
event.preventDefault()
|
|
1183
1238
|
const pointer = resolvePointer(event)
|
|
1184
1239
|
const candidateNode = pickFallbackNode(pointer.x, pointer.y)
|
|
1185
|
-
const candidateNodeId = candidateNode
|
|
1240
|
+
const candidateNodeId = isRealGraphNode(candidateNode) && typeof candidateNode?.[0] === 'string' ? candidateNode[0] : ''
|
|
1186
1241
|
const candidateX = Number(candidateNode?.[2])
|
|
1187
1242
|
const candidateY = Number(candidateNode?.[3])
|
|
1188
1243
|
const world = screenToWorld(pointer.x, pointer.y)
|
|
@@ -1200,10 +1255,15 @@ const setupInput = () => {
|
|
|
1200
1255
|
state.pointer.nodeStartY = candidateNodeId && Number.isFinite(candidateY) ? candidateY : 0
|
|
1201
1256
|
state.pointer.worldAnchorX = world.x
|
|
1202
1257
|
state.pointer.worldAnchorY = world.y
|
|
1203
|
-
|
|
1258
|
+
try {
|
|
1259
|
+
canvas.setPointerCapture(event.pointerId)
|
|
1260
|
+
} catch {}
|
|
1204
1261
|
})
|
|
1205
1262
|
|
|
1206
1263
|
canvas.addEventListener('pointermove', (event) => {
|
|
1264
|
+
if (state.pointer.down) {
|
|
1265
|
+
event.preventDefault()
|
|
1266
|
+
}
|
|
1207
1267
|
const pointer = resolvePointer(event)
|
|
1208
1268
|
|
|
1209
1269
|
if (state.pointer.down) {
|
|
@@ -1241,7 +1301,7 @@ const setupInput = () => {
|
|
|
1241
1301
|
}
|
|
1242
1302
|
|
|
1243
1303
|
const hovered = pickFallbackNode(pointer.x, pointer.y)
|
|
1244
|
-
const hoveredId = hovered
|
|
1304
|
+
const hoveredId = isRealGraphNode(hovered) && typeof hovered?.[0] === 'string' ? hovered[0] : ''
|
|
1245
1305
|
if (state.hoveredNodeId !== hoveredId) {
|
|
1246
1306
|
state.hoveredNodeId = hoveredId
|
|
1247
1307
|
canvas.classList.toggle('is-node-hover', Boolean(hoveredId))
|
|
@@ -1260,11 +1320,7 @@ const setupInput = () => {
|
|
|
1260
1320
|
const shouldPick = !state.pointer.dragging && distanceFromStart < dragActivationDistance
|
|
1261
1321
|
const shouldRefreshAfterDrag = state.pointer.dragging
|
|
1262
1322
|
const shouldPersistNodePosition = state.pointer.dragging && Boolean(state.pointer.dragNodeId)
|
|
1263
|
-
|
|
1264
|
-
state.pointer.dragging = false
|
|
1265
|
-
canvas.classList.remove('is-node-dragging')
|
|
1266
|
-
state.pointer.dragNodeId = ''
|
|
1267
|
-
canvas.releasePointerCapture(event.pointerId)
|
|
1323
|
+
resetPointerState(event.pointerId)
|
|
1268
1324
|
|
|
1269
1325
|
if (shouldPick) {
|
|
1270
1326
|
pickAt(pointer.x, pointer.y)
|
|
@@ -1287,6 +1343,16 @@ const setupInput = () => {
|
|
|
1287
1343
|
updateGraphOverlays()
|
|
1288
1344
|
})
|
|
1289
1345
|
|
|
1346
|
+
canvas.addEventListener('pointercancel', (event) => {
|
|
1347
|
+
resetPointerState(event.pointerId)
|
|
1348
|
+
hideTooltip()
|
|
1349
|
+
updateGraphOverlays()
|
|
1350
|
+
})
|
|
1351
|
+
|
|
1352
|
+
canvas.addEventListener('lostpointercapture', () => {
|
|
1353
|
+
resetPointerState()
|
|
1354
|
+
})
|
|
1355
|
+
|
|
1290
1356
|
elements.miniMap.addEventListener('click', (event) => {
|
|
1291
1357
|
if (!state.miniMapView) {
|
|
1292
1358
|
return
|
|
@@ -1507,7 +1573,7 @@ const setupRenderWorker = () => {
|
|
|
1507
1573
|
|
|
1508
1574
|
if (payload.type === 'pick-result') {
|
|
1509
1575
|
if (payload.node && typeof payload.node.id === 'string' && payload.node.id.length > 0) {
|
|
1510
|
-
|
|
1576
|
+
handlePickedNode(payload.node)
|
|
1511
1577
|
}
|
|
1512
1578
|
return
|
|
1513
1579
|
}
|
|
@@ -11,7 +11,7 @@ import { listAgents } from '../list-agents.js';
|
|
|
11
11
|
import { listBacklinks, listLinks } from '../list-links.js';
|
|
12
12
|
import { searchGraphNodeIds } from '../search-graph-node-ids.js';
|
|
13
13
|
import { searchKnowledge } from '../search-knowledge.js';
|
|
14
|
-
import { loadBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
14
|
+
import { loadBrainlinkConfig, resolveAgentRuntimeDefaults, sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
15
15
|
import { createClientCss } from '../frontend/client-css.js';
|
|
16
16
|
import { createClientHtml } from '../frontend/client-html.js';
|
|
17
17
|
import { createClientJs } from '../frontend/client-js.js';
|
|
@@ -20,12 +20,22 @@ import { createClientRenderWorkerJs } from '../frontend/client-render-worker-js.
|
|
|
20
20
|
import { contentTypes, createJsonResponse, isReadMethod, parsePositiveInteger } from './http.js';
|
|
21
21
|
const readSearchMode = async (url) => {
|
|
22
22
|
const config = await loadBrainlinkConfig();
|
|
23
|
-
|
|
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);
|
|
24
30
|
};
|
|
25
31
|
const hasInvalidSearchMode = (url) => {
|
|
26
32
|
const mode = url.searchParams.get('mode');
|
|
27
33
|
return mode !== null && !['fts', 'semantic', 'hybrid'].includes(mode);
|
|
28
34
|
};
|
|
35
|
+
const hasInvalidContextStrategy = (url) => {
|
|
36
|
+
const strategy = url.searchParams.get('strategy');
|
|
37
|
+
return strategy !== null && !['rag', 'cag', 'auto'].includes(strategy);
|
|
38
|
+
};
|
|
29
39
|
const createResponse = (body, statusCode = 200, contentType = 'text/plain; charset=utf-8') => ({
|
|
30
40
|
body,
|
|
31
41
|
statusCode,
|
|
@@ -370,10 +380,14 @@ export const route = async (request, url, vaultPath) => {
|
|
|
370
380
|
const limit = parsePositiveInteger(url.searchParams.get('limit'), 12);
|
|
371
381
|
const tokens = parsePositiveInteger(url.searchParams.get('tokens'), 2000);
|
|
372
382
|
const mode = await readSearchMode(url);
|
|
383
|
+
const strategy = await readContextStrategy(url);
|
|
373
384
|
if (hasInvalidSearchMode(url)) {
|
|
374
385
|
return createResponse(createJsonResponse({ error: 'Invalid mode. Use fts, semantic or hybrid.' }), 400, contentTypes['.json']);
|
|
375
386
|
}
|
|
376
|
-
|
|
387
|
+
if (hasInvalidContextStrategy(url)) {
|
|
388
|
+
return createResponse(createJsonResponse({ error: 'Invalid strategy. Use rag, cag or auto.' }), 400, contentTypes['.json']);
|
|
389
|
+
}
|
|
390
|
+
return createResponse(createJsonResponse(await buildContextPackage(vaultPath, query, limit, tokens, readAgentQuery(url), mode, strategy)), 200, contentTypes['.json']);
|
|
377
391
|
}
|
|
378
392
|
if (isReadMethod(request) && url.pathname === '/api/links') {
|
|
379
393
|
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);
|
|
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')
|
|
@@ -15,7 +15,7 @@ import { createOfflinePackBackup } from '../../application/offline-pack-backup.j
|
|
|
15
15
|
import { startServer } from '../../application/start-server.js';
|
|
16
16
|
import { startVaultWatcher } from '../../application/watch-vault.js';
|
|
17
17
|
import { doctorVault, getStats, validateVault } from '../../application/analyze-vault.js';
|
|
18
|
-
import { defaultBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
18
|
+
import { defaultBrainlinkConfig, sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
19
19
|
import { loadBrainlinkConfig } from '../../infrastructure/config.js';
|
|
20
20
|
import { assertVaultAllowed, ensureVault, isBucketVaultPath } from '../../infrastructure/file-system-vault.js';
|
|
21
21
|
import { getBootstrapPolicy, getBootstrapSessionStatus, touchBootstrapSession } from '../../infrastructure/session-state.js';
|
|
@@ -1111,6 +1111,7 @@ export const registerWriteCommands = (program) => {
|
|
|
1111
1111
|
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
1112
1112
|
.option('--query <query>', 'optional task query to return immediate grounded context')
|
|
1113
1113
|
.option('--mode <mode>', 'search mode for context (fts|semantic|hybrid)')
|
|
1114
|
+
.option('--strategy <strategy>', 'context strategy for context (rag|cag|auto)')
|
|
1114
1115
|
.option('--limit <limit>', 'maximum context sections')
|
|
1115
1116
|
.option('--tokens <tokens>', 'maximum context token budget')
|
|
1116
1117
|
.option('--no-install-agent', 'skip agent MCP/plugin installation and upgrade automation')
|
|
@@ -1125,6 +1126,7 @@ export const registerWriteCommands = (program) => {
|
|
|
1125
1126
|
const limit = parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit);
|
|
1126
1127
|
const tokens = parsePositiveInteger(options.tokens ?? String(resolved.defaults.defaultContextTokens), resolved.defaults.defaultContextTokens);
|
|
1127
1128
|
const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
|
|
1129
|
+
const strategy = sanitizeContextStrategy(options.strategy, resolved.defaults.defaultContextStrategy);
|
|
1128
1130
|
const index = await indexVault(resolved.vault);
|
|
1129
1131
|
const stats = await getStats(resolved.vault, resolved.agent);
|
|
1130
1132
|
const validation = await validateVault(resolved.vault, resolved.agent);
|
|
@@ -1133,7 +1135,7 @@ export const registerWriteCommands = (program) => {
|
|
|
1133
1135
|
const policy = await getBootstrapPolicy();
|
|
1134
1136
|
const bootstrapStatus = await getBootstrapSessionStatus(resolved.vault, resolved.agent);
|
|
1135
1137
|
const context = options.query
|
|
1136
|
-
? await buildContextPackage(resolved.vault, options.query, limit, tokens, resolved.agent, mode)
|
|
1138
|
+
? await buildContextPackage(resolved.vault, options.query, limit, tokens, resolved.agent, mode, strategy)
|
|
1137
1139
|
: null;
|
|
1138
1140
|
const agentIntegration = options.installAgent === false
|
|
1139
1141
|
? null
|
|
@@ -13,6 +13,7 @@ export const defaultBrainlinkConfig = {
|
|
|
13
13
|
autoCanonicalContextLinks: true,
|
|
14
14
|
defaultSearchLimit: 10,
|
|
15
15
|
defaultContextTokens: 2000,
|
|
16
|
+
defaultContextStrategy: 'rag',
|
|
16
17
|
embeddingProvider: 'local',
|
|
17
18
|
defaultSearchMode: 'hybrid',
|
|
18
19
|
chunkSize: 1200,
|
|
@@ -41,8 +42,10 @@ const safeCwd = () => {
|
|
|
41
42
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
42
43
|
const embeddingProviders = new Set(['none', 'local']);
|
|
43
44
|
const searchModes = new Set(['fts', 'semantic', 'hybrid']);
|
|
45
|
+
const contextStrategies = new Set(['rag', 'cag', 'auto']);
|
|
44
46
|
const sanitizeEmbeddingProvider = (value) => typeof value === 'string' && embeddingProviders.has(value) ? value : defaultBrainlinkConfig.embeddingProvider;
|
|
45
47
|
export const sanitizeSearchMode = (value, fallback = defaultBrainlinkConfig.defaultSearchMode) => typeof value === 'string' && searchModes.has(value) ? value : fallback;
|
|
48
|
+
export const sanitizeContextStrategy = (value, fallback = 'rag') => typeof value === 'string' && contextStrategies.has(value) ? value : fallback;
|
|
46
49
|
const sanitizeAllowedVaults = (value) => Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.trim().length > 0) : [];
|
|
47
50
|
const sanitizePositiveNumber = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
48
51
|
const sanitizeIntegerInRange = (value, fallback, minimum, maximum) => {
|
|
@@ -84,10 +87,14 @@ const sanitizeAgentProfile = (value) => {
|
|
|
84
87
|
const defaultSearchMode = typeof value.defaultSearchMode === 'string' && searchModes.has(value.defaultSearchMode)
|
|
85
88
|
? value.defaultSearchMode
|
|
86
89
|
: undefined;
|
|
90
|
+
const defaultContextStrategy = typeof value.defaultContextStrategy === 'string' && contextStrategies.has(value.defaultContextStrategy)
|
|
91
|
+
? value.defaultContextStrategy
|
|
92
|
+
: undefined;
|
|
87
93
|
const profile = {
|
|
88
94
|
...(defaultSearchLimit ? { defaultSearchLimit } : {}),
|
|
89
95
|
...(defaultContextTokens ? { defaultContextTokens } : {}),
|
|
90
|
-
...(defaultSearchMode ? { defaultSearchMode } : {})
|
|
96
|
+
...(defaultSearchMode ? { defaultSearchMode } : {}),
|
|
97
|
+
...(defaultContextStrategy ? { defaultContextStrategy } : {})
|
|
91
98
|
};
|
|
92
99
|
return Object.keys(profile).length > 0 ? profile : null;
|
|
93
100
|
};
|
|
@@ -169,6 +176,7 @@ const sanitizeConfig = (value) => ({
|
|
|
169
176
|
defaultContextTokens: typeof value.defaultContextTokens === 'number' && value.defaultContextTokens > 0
|
|
170
177
|
? value.defaultContextTokens
|
|
171
178
|
: defaultBrainlinkConfig.defaultContextTokens,
|
|
179
|
+
defaultContextStrategy: sanitizeContextStrategy(value.defaultContextStrategy, defaultBrainlinkConfig.defaultContextStrategy),
|
|
172
180
|
allowedVaults: [...sanitizeAllowedVaults(value.allowedVaults), ...readAllowedVaultsFromEnv()],
|
|
173
181
|
chunkSize: typeof value.chunkSize === 'number' && value.chunkSize > 0 ? value.chunkSize : defaultBrainlinkConfig.chunkSize,
|
|
174
182
|
searchPack: sanitizeSearchPackConfig(value.searchPack),
|
|
@@ -182,7 +190,8 @@ export const resolveAgentRuntimeDefaults = (config, agent) => {
|
|
|
182
190
|
return {
|
|
183
191
|
defaultSearchLimit: profile?.defaultSearchLimit ?? config.defaultSearchLimit,
|
|
184
192
|
defaultContextTokens: profile?.defaultContextTokens ?? config.defaultContextTokens,
|
|
185
|
-
defaultSearchMode: profile?.defaultSearchMode ?? config.defaultSearchMode
|
|
193
|
+
defaultSearchMode: profile?.defaultSearchMode ?? config.defaultSearchMode,
|
|
194
|
+
defaultContextStrategy: profile?.defaultContextStrategy ?? config.defaultContextStrategy
|
|
186
195
|
};
|
|
187
196
|
};
|
|
188
197
|
const mergeConfigLayers = (layers) => layers.reduce((state, config) => ({
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
const normalizePackKey = (key) => ({
|
|
5
|
+
query: key.query.trim().toLowerCase(),
|
|
6
|
+
limit: key.limit,
|
|
7
|
+
maxTokens: key.maxTokens,
|
|
8
|
+
agentId: key.agentId?.trim().toLowerCase() || undefined,
|
|
9
|
+
mode: key.mode
|
|
10
|
+
});
|
|
11
|
+
export const contextPacksDirectory = (vaultPath) => join(vaultPath, '.brainlink', 'context-packs');
|
|
12
|
+
export const contextPackPath = (vaultPath, key) => {
|
|
13
|
+
const digest = createHash('sha256').update(JSON.stringify(normalizePackKey(key))).digest('hex');
|
|
14
|
+
return join(contextPacksDirectory(vaultPath), `${digest}.json`);
|
|
15
|
+
};
|
|
16
|
+
const isStoredContextPack = (value) => {
|
|
17
|
+
if (!value || typeof value !== 'object') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const candidate = value;
|
|
21
|
+
return candidate.version === 1 && typeof candidate.dataSignature === 'string' && Boolean(candidate.context);
|
|
22
|
+
};
|
|
23
|
+
const readStoredContextPack = async (path) => {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
26
|
+
return isStoredContextPack(parsed) ? parsed : null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const toContextPackSummary = async (path, dataSignature) => {
|
|
33
|
+
const [info, stored] = await Promise.all([
|
|
34
|
+
stat(path),
|
|
35
|
+
readStoredContextPack(path)
|
|
36
|
+
]);
|
|
37
|
+
const stale = !stored || (typeof dataSignature === 'string' && stored.dataSignature !== dataSignature);
|
|
38
|
+
return {
|
|
39
|
+
path,
|
|
40
|
+
filename: basename(path),
|
|
41
|
+
createdAt: stored?.createdAt ?? null,
|
|
42
|
+
dataSignature: stored?.dataSignature ?? null,
|
|
43
|
+
key: stored?.key ?? null,
|
|
44
|
+
sizeBytes: info.size,
|
|
45
|
+
stale
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
export const listContextPacks = async (vaultPath, dataSignature) => {
|
|
49
|
+
const directory = contextPacksDirectory(vaultPath);
|
|
50
|
+
try {
|
|
51
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
52
|
+
const paths = entries
|
|
53
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
54
|
+
.map((entry) => join(directory, entry.name));
|
|
55
|
+
const summaries = await Promise.all(paths.map((path) => toContextPackSummary(path, dataSignature)));
|
|
56
|
+
return summaries.sort((left, right) => (right.createdAt ?? '').localeCompare(left.createdAt ?? '') || left.filename.localeCompare(right.filename));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
export const clearContextPacks = async (vaultPath, options = {}) => {
|
|
66
|
+
const packs = await listContextPacks(vaultPath, options.dataSignature);
|
|
67
|
+
const removed = options.staleOnly ? packs.filter((pack) => pack.stale) : packs;
|
|
68
|
+
const kept = options.staleOnly ? packs.filter((pack) => !pack.stale) : [];
|
|
69
|
+
await Promise.all(removed.map((pack) => rm(pack.path, { force: true })));
|
|
70
|
+
return { removed, kept };
|
|
71
|
+
};
|
|
72
|
+
export const readContextPack = async (vaultPath, key, dataSignature) => {
|
|
73
|
+
const path = contextPackPath(vaultPath, key);
|
|
74
|
+
try {
|
|
75
|
+
const parsed = await readStoredContextPack(path);
|
|
76
|
+
if (!parsed) {
|
|
77
|
+
return { status: 'stale', path };
|
|
78
|
+
}
|
|
79
|
+
if (parsed.dataSignature !== dataSignature) {
|
|
80
|
+
return { status: 'stale', path };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
status: 'hit',
|
|
84
|
+
path,
|
|
85
|
+
context: {
|
|
86
|
+
...parsed.context,
|
|
87
|
+
strategy: 'cag',
|
|
88
|
+
cache: {
|
|
89
|
+
storage: 'context-pack',
|
|
90
|
+
status: 'hit',
|
|
91
|
+
dataSignature,
|
|
92
|
+
path
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { status: 'miss', path };
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
export const writeContextPack = async (vaultPath, key, dataSignature, context) => {
|
|
102
|
+
const path = contextPackPath(vaultPath, key);
|
|
103
|
+
const stored = {
|
|
104
|
+
version: 1,
|
|
105
|
+
createdAt: new Date().toISOString(),
|
|
106
|
+
dataSignature,
|
|
107
|
+
key: normalizePackKey(key),
|
|
108
|
+
context: {
|
|
109
|
+
...context,
|
|
110
|
+
strategy: 'cag',
|
|
111
|
+
cache: {
|
|
112
|
+
storage: 'context-pack',
|
|
113
|
+
status: 'refresh',
|
|
114
|
+
dataSignature,
|
|
115
|
+
path
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
await mkdir(contextPacksDirectory(vaultPath), { recursive: true });
|
|
120
|
+
await writeFile(path, `${JSON.stringify(stored, null, 2)}\n`, 'utf8');
|
|
121
|
+
return path;
|
|
122
|
+
};
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, contextInputSchema, contextTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool, versionInputSchema, versionTool } from './tools.js';
|
|
2
|
+
import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, contextInputSchema, contextPacksInputSchema, contextPacksTool, contextTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool, versionInputSchema, versionTool } from './tools.js';
|
|
3
3
|
import { getRuntimeVersion } from './runtime.js';
|
|
4
4
|
export const createBrainlinkMcpServer = () => {
|
|
5
5
|
const server = new McpServer({
|
|
@@ -25,14 +25,19 @@ export const createBrainlinkMcpServer = () => {
|
|
|
25
25
|
}, versionTool);
|
|
26
26
|
server.registerTool('brainlink_recommendations', {
|
|
27
27
|
title: 'Brainlink Recommended MCP Workflow',
|
|
28
|
-
description: 'Return a plug-and-play action plan for this vault/agent, including policy, bootstrap, context retrieval and durable write guidance.',
|
|
28
|
+
description: 'Return a plug-and-play action plan for this vault/agent, including policy, bootstrap, RAG/CAG context strategy, retrieval and durable write guidance.',
|
|
29
29
|
inputSchema: recommendationsInputSchema
|
|
30
30
|
}, recommendationsTool);
|
|
31
31
|
server.registerTool('brainlink_context', {
|
|
32
32
|
title: 'Build Brainlink Context',
|
|
33
|
-
description: 'Read indexed Brainlink memory for a task or question. Usually called after brainlink_bootstrap. This is read-only and does not create graph links.',
|
|
33
|
+
description: 'Read indexed Brainlink memory for a task or question. Agents can choose strategy per call: rag for fresh retrieval assembly, cag for persisted context packs, or auto for pack-hit CAG with RAG fallback. Usually called after brainlink_bootstrap. This is read-only and does not create graph links.',
|
|
34
34
|
inputSchema: contextInputSchema
|
|
35
35
|
}, contextTool);
|
|
36
|
+
server.registerTool('brainlink_context_packs', {
|
|
37
|
+
title: 'Manage Brainlink Context Packs',
|
|
38
|
+
description: 'List or clear persisted CAG context packs. Packs are derived artifacts and can be rebuilt from Markdown/index state.',
|
|
39
|
+
inputSchema: contextPacksInputSchema
|
|
40
|
+
}, contextPacksTool);
|
|
36
41
|
server.registerTool('brainlink_search', {
|
|
37
42
|
title: 'Search Brainlink Memory',
|
|
38
43
|
description: 'Search indexed Brainlink notes with FTS, semantic or hybrid retrieval.',
|