@andespindola/brainlink 0.1.0-beta.152 → 0.1.0-beta.153
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 +3 -0
- package/dist/application/frontend/client-css.js +8 -4
- package/dist/application/frontend/client-html.js +3 -0
- package/dist/application/frontend/client-js.js +103 -7
- package/dist/application/get-graph-contexts.js +19 -0
- package/dist/application/get-graph-layout.js +37 -17
- package/dist/application/get-graph-stream-chunk.js +4 -1
- package/dist/application/get-graph-view.js +4 -1
- package/dist/application/server/routes.js +16 -3
- package/docs/ARCHITECTURE.md +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -601,6 +601,7 @@ The graph UI shows:
|
|
|
601
601
|
- details opened in a non-modal side panel (tags, outgoing links, backlinks, full Markdown content), so zoom and pan remain available while inspecting data
|
|
602
602
|
- neutral graph nodes with segment/group metadata
|
|
603
603
|
- agent selector (id-only labels) for isolated views
|
|
604
|
+
- context selector for segment-scoped star subgraphs derived from the visual graph context
|
|
604
605
|
- graph filter matches title, path, tags and note content
|
|
605
606
|
- graph filter keeps hub context nodes visible (`Memory Hub`/`MOC`/high-degree fallback) to preserve relationship readability
|
|
606
607
|
- realtime refresh while `--watch` is enabled
|
|
@@ -632,6 +633,7 @@ The server always refuses non-loopback hosts. Brainlink HTTP only runs on localh
|
|
|
632
633
|
Routes:
|
|
633
634
|
|
|
634
635
|
- `GET /api/agents`
|
|
636
|
+
- `GET /api/graph-contexts`
|
|
635
637
|
- `GET /api/graph`
|
|
636
638
|
- `GET /api/graph-layout`
|
|
637
639
|
- `GET /api/graph-view?x=<x>&y=<y>&w=<width>&h=<height>&scale=<scale>`
|
|
@@ -650,6 +652,7 @@ Read routes accept `agent=<agent-id>`:
|
|
|
650
652
|
|
|
651
653
|
```txt
|
|
652
654
|
/api/graph-layout?agent=coding-agent
|
|
655
|
+
/api/graph-layout?agent=coding-agent&context=Architecture
|
|
653
656
|
/api/search?q=typescript&agent=coding-agent&mode=hybrid
|
|
654
657
|
/api/context?q=module-boundaries&agent=coding-agent&mode=semantic
|
|
655
658
|
```
|
|
@@ -113,7 +113,8 @@ select {
|
|
|
113
113
|
min-width: 220px;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
.agent-filter
|
|
116
|
+
.agent-filter,
|
|
117
|
+
.context-filter {
|
|
117
118
|
width: min(220px, 28vw);
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -125,7 +126,8 @@ select {
|
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
.search input,
|
|
128
|
-
.agent-filter select
|
|
129
|
+
.agent-filter select,
|
|
130
|
+
.context-filter select {
|
|
129
131
|
width: 100%;
|
|
130
132
|
height: 40px;
|
|
131
133
|
border: 1px solid var(--line);
|
|
@@ -137,7 +139,8 @@ select {
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
.search input:focus,
|
|
140
|
-
.agent-filter select:focus
|
|
142
|
+
.agent-filter select:focus,
|
|
143
|
+
.context-filter select:focus {
|
|
141
144
|
border-color: var(--accent);
|
|
142
145
|
}
|
|
143
146
|
|
|
@@ -405,7 +408,8 @@ li small {
|
|
|
405
408
|
order: 3;
|
|
406
409
|
}
|
|
407
410
|
|
|
408
|
-
.agent-filter
|
|
411
|
+
.agent-filter,
|
|
412
|
+
.context-filter {
|
|
409
413
|
width: 100%;
|
|
410
414
|
}
|
|
411
415
|
|
|
@@ -35,6 +35,9 @@ export const createClientHtml = () => `<!doctype html>
|
|
|
35
35
|
<label class="agent-filter">
|
|
36
36
|
<select id="agent"></select>
|
|
37
37
|
</label>
|
|
38
|
+
<label class="context-filter">
|
|
39
|
+
<select id="context"></select>
|
|
40
|
+
</label>
|
|
38
41
|
<div class="toolbar" aria-label="Graph controls">
|
|
39
42
|
<button id="zoomIn" type="button" title="Zoom in">+</button>
|
|
40
43
|
<button id="zoomOut" type="button" title="Zoom out">-</button>
|
|
@@ -4,6 +4,7 @@ const byId = (id) => document.getElementById(id)
|
|
|
4
4
|
const elements = {
|
|
5
5
|
search: byId('search'),
|
|
6
6
|
agent: byId('agent'),
|
|
7
|
+
context: byId('context'),
|
|
7
8
|
nodeCount: byId('nodeCount'),
|
|
8
9
|
edgeCount: byId('edgeCount'),
|
|
9
10
|
tagCount: byId('tagCount'),
|
|
@@ -49,6 +50,7 @@ const state = {
|
|
|
49
50
|
rendererMode: 'worker',
|
|
50
51
|
renderWorker: null,
|
|
51
52
|
agentId: '',
|
|
53
|
+
contextId: '',
|
|
52
54
|
graphSignature: '',
|
|
53
55
|
graphMode: 'near',
|
|
54
56
|
chunk: {
|
|
@@ -77,6 +79,7 @@ const zoomRange = {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
const selectedAgentStorageKey = 'brainlink:selected-agent'
|
|
82
|
+
const selectedContextStorageKey = 'brainlink:selected-context'
|
|
80
83
|
|
|
81
84
|
const escapeHtml = (value) => String(value)
|
|
82
85
|
.replaceAll('&', '&')
|
|
@@ -104,6 +107,25 @@ const writeStoredAgent = (agentId) => {
|
|
|
104
107
|
} catch {}
|
|
105
108
|
}
|
|
106
109
|
|
|
110
|
+
const readStoredContext = () => {
|
|
111
|
+
try {
|
|
112
|
+
const value = window.localStorage.getItem(selectedContextStorageKey)?.trim() ?? ''
|
|
113
|
+
return value.length > 0 ? value : ''
|
|
114
|
+
} catch {
|
|
115
|
+
return ''
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const writeStoredContext = (contextId) => {
|
|
120
|
+
try {
|
|
121
|
+
if (!contextId) {
|
|
122
|
+
window.localStorage.removeItem(selectedContextStorageKey)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
window.localStorage.setItem(selectedContextStorageKey, contextId)
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
|
|
107
129
|
const syncAgentInUrl = (agentId) => {
|
|
108
130
|
try {
|
|
109
131
|
const url = new URL(window.location.href)
|
|
@@ -116,6 +138,18 @@ const syncAgentInUrl = (agentId) => {
|
|
|
116
138
|
} catch {}
|
|
117
139
|
}
|
|
118
140
|
|
|
141
|
+
const syncContextInUrl = (contextId) => {
|
|
142
|
+
try {
|
|
143
|
+
const url = new URL(window.location.href)
|
|
144
|
+
if (contextId && contextId.trim().length > 0) {
|
|
145
|
+
url.searchParams.set('context', contextId)
|
|
146
|
+
} else {
|
|
147
|
+
url.searchParams.delete('context')
|
|
148
|
+
}
|
|
149
|
+
window.history.replaceState({}, '', url.toString())
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
|
|
119
153
|
const initialAgentFromUrl = (() => {
|
|
120
154
|
try {
|
|
121
155
|
const raw = new URL(window.location.href).searchParams.get('agent')
|
|
@@ -126,7 +160,28 @@ const initialAgentFromUrl = (() => {
|
|
|
126
160
|
}
|
|
127
161
|
})()
|
|
128
162
|
|
|
129
|
-
const
|
|
163
|
+
const initialContextFromUrl = (() => {
|
|
164
|
+
try {
|
|
165
|
+
const raw = new URL(window.location.href).searchParams.get('context')
|
|
166
|
+
const value = raw?.trim() ?? ''
|
|
167
|
+
return value.length > 0 ? value : ''
|
|
168
|
+
} catch {
|
|
169
|
+
return ''
|
|
170
|
+
}
|
|
171
|
+
})()
|
|
172
|
+
|
|
173
|
+
const scopeQuery = (separator = '?') => {
|
|
174
|
+
const params = new URLSearchParams()
|
|
175
|
+
if (state.agentId) {
|
|
176
|
+
params.set('agent', state.agentId)
|
|
177
|
+
}
|
|
178
|
+
if (state.contextId) {
|
|
179
|
+
params.set('context', state.contextId)
|
|
180
|
+
}
|
|
181
|
+
const query = params.toString()
|
|
182
|
+
|
|
183
|
+
return query ? separator + query : ''
|
|
184
|
+
}
|
|
130
185
|
|
|
131
186
|
const parseColor = (hex) => {
|
|
132
187
|
const normalized = String(hex || '#ffffff').replace('#', '')
|
|
@@ -375,7 +430,7 @@ const loadNodeDetails = async (nodeId) => {
|
|
|
375
430
|
return
|
|
376
431
|
}
|
|
377
432
|
|
|
378
|
-
const response = await fetch('/api/graph-node?id=' + encodeURIComponent(nodeId) +
|
|
433
|
+
const response = await fetch('/api/graph-node?id=' + encodeURIComponent(nodeId) + scopeQuery('&'))
|
|
379
434
|
if (!response.ok) {
|
|
380
435
|
throw new Error('Failed to load graph node details')
|
|
381
436
|
}
|
|
@@ -479,6 +534,9 @@ const fetchChunk = async ({ fit } = { fit: false }) => {
|
|
|
479
534
|
if (state.agentId) {
|
|
480
535
|
params.set('agent', state.agentId)
|
|
481
536
|
}
|
|
537
|
+
if (state.contextId) {
|
|
538
|
+
params.set('context', state.contextId)
|
|
539
|
+
}
|
|
482
540
|
|
|
483
541
|
const response = await fetch('/api/graph-stream?' + params.toString(), { signal: controller.signal })
|
|
484
542
|
if (!response.ok) {
|
|
@@ -751,7 +809,7 @@ const setupControls = () => {
|
|
|
751
809
|
return
|
|
752
810
|
}
|
|
753
811
|
|
|
754
|
-
fetch('/api/graph-filter?q=' + encodeURIComponent(query) + '&limit=1800' +
|
|
812
|
+
fetch('/api/graph-filter?q=' + encodeURIComponent(query) + '&limit=1800' + scopeQuery('&'))
|
|
755
813
|
.then((response) => response.json())
|
|
756
814
|
.then((payload) => {
|
|
757
815
|
if (token !== state.searchToken) {
|
|
@@ -795,12 +853,50 @@ const loadAgents = async () => {
|
|
|
795
853
|
state.agentId = elements.agent.value || ''
|
|
796
854
|
writeStoredAgent(state.agentId)
|
|
797
855
|
syncAgentInUrl(state.agentId)
|
|
798
|
-
scheduleChunkFetch({ fit: true })
|
|
856
|
+
loadContexts().then(() => scheduleChunkFetch({ fit: true })).catch((error) => console.error(error))
|
|
799
857
|
})
|
|
800
858
|
|
|
801
859
|
syncAgentInUrl(state.agentId)
|
|
802
860
|
}
|
|
803
861
|
|
|
862
|
+
const loadContexts = async () => {
|
|
863
|
+
const response = await fetch('/api/graph-contexts' + (state.agentId ? '?agent=' + encodeURIComponent(state.agentId) : ''))
|
|
864
|
+
if (!response.ok) {
|
|
865
|
+
throw new Error('Failed to load graph contexts')
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const payload = await response.json()
|
|
869
|
+
const contexts = Array.isArray(payload?.contexts) ? payload.contexts : []
|
|
870
|
+
const options = [
|
|
871
|
+
'<option value="">All contexts</option>',
|
|
872
|
+
...contexts.map((context) => {
|
|
873
|
+
const id = String(context?.id || '')
|
|
874
|
+
const title = String(context?.title || id || 'Untitled')
|
|
875
|
+
const count = Number.isFinite(context?.nodeCount) ? context.nodeCount : 0
|
|
876
|
+
return '<option value="' + escapeHtml(id) + '">' + escapeHtml(title) + ' (' + count + ')</option>'
|
|
877
|
+
})
|
|
878
|
+
]
|
|
879
|
+
|
|
880
|
+
elements.context.innerHTML = options.join('')
|
|
881
|
+
|
|
882
|
+
const preferredContext = initialContextFromUrl || readStoredContext()
|
|
883
|
+
const hasPreferred = preferredContext && contexts.some((context) => context?.id === preferredContext)
|
|
884
|
+
state.contextId = hasPreferred ? preferredContext : ''
|
|
885
|
+
elements.context.value = state.contextId
|
|
886
|
+
writeStoredContext(state.contextId)
|
|
887
|
+
syncContextInUrl(state.contextId)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const setupContextControl = () => {
|
|
891
|
+
elements.context.addEventListener('change', () => {
|
|
892
|
+
state.contextId = elements.context.value || ''
|
|
893
|
+
state.selectedNodeId = null
|
|
894
|
+
writeStoredContext(state.contextId)
|
|
895
|
+
syncContextInUrl(state.contextId)
|
|
896
|
+
scheduleChunkFetch({ fit: true })
|
|
897
|
+
})
|
|
898
|
+
}
|
|
899
|
+
|
|
804
900
|
const setupRenderWorker = () => {
|
|
805
901
|
const hasWorker = typeof Worker !== 'undefined'
|
|
806
902
|
const canTransfer = typeof canvas.transferControlToOffscreen === 'function'
|
|
@@ -892,6 +988,7 @@ const bootstrap = async () => {
|
|
|
892
988
|
setupRenderWorker()
|
|
893
989
|
setupInput()
|
|
894
990
|
setupControls()
|
|
991
|
+
setupContextControl()
|
|
895
992
|
wireNodeLinkClicks()
|
|
896
993
|
|
|
897
994
|
window.addEventListener('resize', () => {
|
|
@@ -900,12 +997,11 @@ const bootstrap = async () => {
|
|
|
900
997
|
})
|
|
901
998
|
|
|
902
999
|
await loadAgents()
|
|
1000
|
+
await loadContexts()
|
|
903
1001
|
updateTotals()
|
|
904
1002
|
updateTagCount()
|
|
905
1003
|
|
|
906
|
-
|
|
907
|
-
scheduleChunkFetch({ fit: true })
|
|
908
|
-
}
|
|
1004
|
+
scheduleChunkFetch({ fit: true })
|
|
909
1005
|
}
|
|
910
1006
|
|
|
911
1007
|
bootstrap().catch((error) => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getGraphLayout } from './get-graph-layout.js';
|
|
2
|
+
export const getGraphContexts = async (vaultPath, agentId) => {
|
|
3
|
+
const { layout } = await getGraphLayout(vaultPath, { agentId });
|
|
4
|
+
const nodeIdsByContext = layout.nodes.reduce((contexts, node) => {
|
|
5
|
+
const title = node.segment || node.group || 'root';
|
|
6
|
+
const nodeIds = contexts.get(title) ?? new Set();
|
|
7
|
+
nodeIds.add(node.id);
|
|
8
|
+
contexts.set(title, nodeIds);
|
|
9
|
+
return contexts;
|
|
10
|
+
}, new Map());
|
|
11
|
+
return Array.from(nodeIdsByContext.entries())
|
|
12
|
+
.map(([title, nodeIds]) => ({
|
|
13
|
+
id: title,
|
|
14
|
+
title,
|
|
15
|
+
nodeCount: nodeIds.size,
|
|
16
|
+
edgeCount: layout.edges.filter((edge) => nodeIds.has(edge.source) && Boolean(edge.target && nodeIds.has(edge.target))).length
|
|
17
|
+
}))
|
|
18
|
+
.sort((left, right) => right.nodeCount - left.nodeCount || left.title.localeCompare(right.title));
|
|
19
|
+
};
|
|
@@ -4,20 +4,25 @@ import { dirname, join } from 'node:path';
|
|
|
4
4
|
import { createStarGraphLayout } from '../domain/graph-layout.js';
|
|
5
5
|
import { indexStoragePath } from '../infrastructure/file-index.js';
|
|
6
6
|
import { getGraphSummary } from './get-graph-summary.js';
|
|
7
|
-
const graphLayoutVersion =
|
|
7
|
+
const graphLayoutVersion = 4;
|
|
8
8
|
const graphLayoutCache = new Map();
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const safeCacheSegment = (value, fallback) => value?.replace(/[^a-zA-Z0-9_-]/g, '_') || fallback;
|
|
10
|
+
const graphLayoutStoragePath = (vaultPath, options) => {
|
|
11
|
+
const agent = safeCacheSegment(options.agentId, 'all');
|
|
12
|
+
const context = safeCacheSegment(options.context, 'all-contexts');
|
|
13
|
+
return join(vaultPath, '.brainlink', `graph-layout-${agent}-${context}.json`);
|
|
14
|
+
};
|
|
15
|
+
const readPersistedLayout = async (vaultPath, databaseSignature, options) => {
|
|
11
16
|
try {
|
|
12
|
-
const parsed = JSON.parse(await readFile(graphLayoutStoragePath(vaultPath,
|
|
17
|
+
const parsed = JSON.parse(await readFile(graphLayoutStoragePath(vaultPath, options), 'utf8'));
|
|
13
18
|
return parsed.databaseSignature === databaseSignature && parsed.layoutVersion === graphLayoutVersion ? parsed : null;
|
|
14
19
|
}
|
|
15
20
|
catch {
|
|
16
21
|
return null;
|
|
17
22
|
}
|
|
18
23
|
};
|
|
19
|
-
const writePersistedLayout = async (vaultPath,
|
|
20
|
-
const target = graphLayoutStoragePath(vaultPath,
|
|
24
|
+
const writePersistedLayout = async (vaultPath, options, cached) => {
|
|
25
|
+
const target = graphLayoutStoragePath(vaultPath, options);
|
|
21
26
|
const temp = `${target}.tmp`;
|
|
22
27
|
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
23
28
|
await writeFile(temp, `${JSON.stringify(cached)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
@@ -41,9 +46,27 @@ const createGraphSignature = (graph) => {
|
|
|
41
46
|
.update(`${graph.nodes.length}:${nodesSignature}|${graph.edges.length}:${edgesSignature}`)
|
|
42
47
|
.digest('hex');
|
|
43
48
|
};
|
|
44
|
-
|
|
49
|
+
const createLayout = (graph) => {
|
|
50
|
+
const rawLayout = createStarGraphLayout(graph);
|
|
51
|
+
return {
|
|
52
|
+
...rawLayout,
|
|
53
|
+
nodes: rawLayout.nodes.map((node) => ({ ...node, content: '' }))
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const filterGraphByContext = (graph, context) => {
|
|
57
|
+
const baseLayout = createStarGraphLayout(graph);
|
|
58
|
+
const selectedNodeIds = new Set(baseLayout.nodes
|
|
59
|
+
.filter((node) => node.segment === context)
|
|
60
|
+
.map((node) => node.id));
|
|
61
|
+
return {
|
|
62
|
+
nodes: graph.nodes.filter((node) => selectedNodeIds.has(node.id)),
|
|
63
|
+
edges: graph.edges.filter((edge) => selectedNodeIds.has(edge.source) && Boolean(edge.target && selectedNodeIds.has(edge.target)))
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
export const getGraphLayout = async (vaultPath, optionsOrAgentId) => {
|
|
67
|
+
const options = typeof optionsOrAgentId === 'string' ? { agentId: optionsOrAgentId } : optionsOrAgentId ?? {};
|
|
45
68
|
const databaseSignature = await readDatabaseSignature(vaultPath);
|
|
46
|
-
const cacheKey = `${vaultPath}:${agentId ?? ''}`;
|
|
69
|
+
const cacheKey = `${vaultPath}:${options.agentId ?? ''}:${options.context ?? ''}`;
|
|
47
70
|
const cached = graphLayoutCache.get(cacheKey);
|
|
48
71
|
if (cached?.databaseSignature === databaseSignature && cached.layoutVersion === graphLayoutVersion) {
|
|
49
72
|
return {
|
|
@@ -51,7 +74,7 @@ export const getGraphLayout = async (vaultPath, agentId) => {
|
|
|
51
74
|
layout: cached.layout
|
|
52
75
|
};
|
|
53
76
|
}
|
|
54
|
-
const persisted = await readPersistedLayout(vaultPath, databaseSignature,
|
|
77
|
+
const persisted = await readPersistedLayout(vaultPath, databaseSignature, options);
|
|
55
78
|
if (persisted) {
|
|
56
79
|
graphLayoutCache.set(cacheKey, persisted);
|
|
57
80
|
return {
|
|
@@ -59,16 +82,13 @@ export const getGraphLayout = async (vaultPath, agentId) => {
|
|
|
59
82
|
layout: persisted.layout
|
|
60
83
|
};
|
|
61
84
|
}
|
|
62
|
-
const graph = await getGraphSummary(vaultPath, agentId);
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const layout =
|
|
66
|
-
...rawLayout,
|
|
67
|
-
nodes: rawLayout.nodes.map((node) => ({ ...node, content: '' }))
|
|
68
|
-
};
|
|
85
|
+
const graph = await getGraphSummary(vaultPath, options.agentId);
|
|
86
|
+
const scopedGraph = options.context ? filterGraphByContext(graph, options.context) : graph;
|
|
87
|
+
const signature = createGraphSignature(scopedGraph);
|
|
88
|
+
const layout = createLayout(scopedGraph);
|
|
69
89
|
const nextCache = { layoutVersion: graphLayoutVersion, databaseSignature, signature, layout };
|
|
70
90
|
graphLayoutCache.set(cacheKey, nextCache);
|
|
71
|
-
await writePersistedLayout(vaultPath,
|
|
91
|
+
await writePersistedLayout(vaultPath, options, nextCache);
|
|
72
92
|
return {
|
|
73
93
|
signature,
|
|
74
94
|
layout
|
|
@@ -244,7 +244,10 @@ const normalizeBudget = (value, fallback, min, max) => {
|
|
|
244
244
|
export const getGraphStreamChunk = async (vaultPath, input) => {
|
|
245
245
|
const nodeBudget = normalizeBudget(input.nodeBudget, 1800, 80, 12_000);
|
|
246
246
|
const edgeBudget = normalizeBudget(input.edgeBudget, 5000, 120, 60_000);
|
|
247
|
-
const { signature, layout } = await getGraphLayout(vaultPath,
|
|
247
|
+
const { signature, layout } = await getGraphLayout(vaultPath, {
|
|
248
|
+
agentId: input.agentId,
|
|
249
|
+
context: input.context
|
|
250
|
+
});
|
|
248
251
|
const groups = layout.groups ?? [];
|
|
249
252
|
const cache = getOrCreateLayoutCache(signature, layout.nodes, layout.edges, groups);
|
|
250
253
|
if (layout.nodes.length === 0) {
|
|
@@ -180,7 +180,10 @@ const arrangeChildGraphNodes = (nodes, group, degrees) => {
|
|
|
180
180
|
};
|
|
181
181
|
const limitEdges = (edges) => edges.slice(0, edgeLimit);
|
|
182
182
|
export const getGraphView = async (vaultPath, input) => {
|
|
183
|
-
const { signature, layout } = await getGraphLayout(vaultPath,
|
|
183
|
+
const { signature, layout } = await getGraphLayout(vaultPath, {
|
|
184
|
+
agentId: input.agentId,
|
|
185
|
+
context: input.context
|
|
186
|
+
});
|
|
184
187
|
const groups = layout.groups ?? [];
|
|
185
188
|
const degrees = degreeMap(layout.edges);
|
|
186
189
|
const groupById = new Map(groups.map((group) => [group.id, group]));
|
|
@@ -1,6 +1,7 @@
|
|
|
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';
|
|
6
7
|
import { getGraphView } from '../get-graph-view.js';
|
|
@@ -55,6 +56,10 @@ const sameEntityTag = (candidate, signature) => {
|
|
|
55
56
|
return decodeEntityTag(candidate) === signature;
|
|
56
57
|
};
|
|
57
58
|
const readAgentQuery = (url) => url.searchParams.get('agent') ?? undefined;
|
|
59
|
+
const readContextQuery = (url) => {
|
|
60
|
+
const value = url.searchParams.get('context')?.trim() ?? '';
|
|
61
|
+
return value.length > 0 ? value : undefined;
|
|
62
|
+
};
|
|
58
63
|
const parseNumber = (value, fallback) => {
|
|
59
64
|
const parsed = Number(value);
|
|
60
65
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
@@ -218,7 +223,10 @@ export const route = async (request, url, vaultPath) => {
|
|
|
218
223
|
return createResponse(createJsonResponse(await getGraph(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
|
|
219
224
|
}
|
|
220
225
|
if (isReadMethod(request) && url.pathname === '/api/graph-layout') {
|
|
221
|
-
const { signature, layout } = await getGraphLayout(vaultPath,
|
|
226
|
+
const { signature, layout } = await getGraphLayout(vaultPath, {
|
|
227
|
+
agentId: readAgentQuery(url),
|
|
228
|
+
context: readContextQuery(url)
|
|
229
|
+
});
|
|
222
230
|
const requestEtags = request.headers['if-none-match'];
|
|
223
231
|
const notModified = sameEntityTag(requestEtags, signature);
|
|
224
232
|
const etag = encodeEntityTag(signature);
|
|
@@ -253,7 +261,8 @@ export const route = async (request, url, vaultPath) => {
|
|
|
253
261
|
width: parseNumber(url.searchParams.get('w'), 2000),
|
|
254
262
|
height: parseNumber(url.searchParams.get('h'), 2000),
|
|
255
263
|
scale: parseNumber(url.searchParams.get('scale'), 1),
|
|
256
|
-
agentId: readAgentQuery(url)
|
|
264
|
+
agentId: readAgentQuery(url),
|
|
265
|
+
context: readContextQuery(url)
|
|
257
266
|
})), 200, contentTypes['.json']);
|
|
258
267
|
}
|
|
259
268
|
if (isReadMethod(request) && url.pathname === '/api/graph-stream') {
|
|
@@ -272,7 +281,8 @@ export const route = async (request, url, vaultPath) => {
|
|
|
272
281
|
scale,
|
|
273
282
|
nodeBudget,
|
|
274
283
|
edgeBudget,
|
|
275
|
-
agentId: readAgentQuery(url)
|
|
284
|
+
agentId: readAgentQuery(url),
|
|
285
|
+
context: readContextQuery(url)
|
|
276
286
|
})), 200, contentTypes['.json']);
|
|
277
287
|
}
|
|
278
288
|
if (isReadMethod(request) && url.pathname === '/api/graph-node') {
|
|
@@ -298,6 +308,9 @@ export const route = async (request, url, vaultPath) => {
|
|
|
298
308
|
if (isReadMethod(request) && url.pathname === '/api/agents') {
|
|
299
309
|
return createResponse(createJsonResponse({ agents: await listAgents(vaultPath) }), 200, contentTypes['.json']);
|
|
300
310
|
}
|
|
311
|
+
if (isReadMethod(request) && url.pathname === '/api/graph-contexts') {
|
|
312
|
+
return createResponse(createJsonResponse({ contexts: await getGraphContexts(vaultPath, readAgentQuery(url)) }), 200, contentTypes['.json']);
|
|
313
|
+
}
|
|
301
314
|
if (isReadMethod(request) && url.pathname === '/api/search') {
|
|
302
315
|
const query = url.searchParams.get('q') ?? '';
|
|
303
316
|
const limit = parsePositiveInteger(url.searchParams.get('limit'), 10);
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -154,8 +154,10 @@ server command
|
|
|
154
154
|
-> optional index rebuild
|
|
155
155
|
-> HTTP server
|
|
156
156
|
-> /api/agents lists indexed namespaces
|
|
157
|
+
-> /api/graph-contexts lists visual graph contexts
|
|
157
158
|
-> /api/graph reads indexed documents and links
|
|
158
159
|
-> /api/graph-layout derives a star layout from indexed graph data
|
|
160
|
+
-> optional context query narrows the layout to a segment-scoped star subgraph
|
|
159
161
|
-> browser renders graph canvas
|
|
160
162
|
```
|
|
161
163
|
|
package/package.json
CHANGED