@andespindola/brainlink 0.1.0-beta.153 → 0.1.0-beta.155
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 +2 -2
- package/README.md +23 -7
- package/dist/application/add-note.js +12 -3
- package/dist/application/canonical-context-links.js +209 -0
- package/dist/application/frontend/client-css.js +77 -0
- package/dist/application/frontend/client-html.js +4 -0
- package/dist/application/frontend/client-js.js +490 -16
- package/dist/application/frontend/client-render-worker-js.js +53 -0
- package/dist/application/get-graph-layout.js +3 -2
- package/dist/application/graph-view-state.js +66 -0
- package/dist/application/search-graph-node-ids.js +14 -5
- package/dist/application/server/routes.js +46 -1
- package/dist/cli/commands/write-commands.js +47 -8
- package/dist/domain/graph-contexts.js +159 -0
- package/dist/domain/graph-layout.js +43 -17
- package/dist/infrastructure/config.js +4 -0
- package/dist/mcp/server.js +11 -1
- package/dist/mcp/tools.js +64 -5
- package/docs/AGENT_USAGE.md +7 -2
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inferExplicitVisualGraphContext } from './graph-contexts.js';
|
|
1
2
|
const hierarchyGroupNodeLimit = 1000;
|
|
2
3
|
const groupLabels = {
|
|
3
4
|
'00-maps': 'maps',
|
|
@@ -140,8 +141,16 @@ const selectSegmentSeeds = (nodes, edges, degrees) => {
|
|
|
140
141
|
const assignSegments = (nodes, edges, degrees) => {
|
|
141
142
|
const adjacency = createAdjacency(nodes, edges);
|
|
142
143
|
const seeds = selectSegmentSeeds(nodes, edges, degrees);
|
|
143
|
-
const assignments = new Map(
|
|
144
|
+
const assignments = new Map(nodes.flatMap((node) => {
|
|
145
|
+
const visualContext = inferExplicitVisualGraphContext(node);
|
|
146
|
+
return visualContext ? [[node.id, visualContext.title]] : [];
|
|
147
|
+
}));
|
|
144
148
|
const queue = seeds.map((seed) => seed.id);
|
|
149
|
+
seeds.forEach((seed) => {
|
|
150
|
+
if (!assignments.has(seed.id)) {
|
|
151
|
+
assignments.set(seed.id, segmentName(seed));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
145
154
|
for (let index = 0; index < queue.length; index += 1) {
|
|
146
155
|
const id = queue[index];
|
|
147
156
|
const segment = assignments.get(id);
|
|
@@ -347,8 +356,9 @@ const resolveCollisionPair = (left, right, minDistance) => {
|
|
|
347
356
|
return;
|
|
348
357
|
}
|
|
349
358
|
const push = (minDistance - distance) / 2;
|
|
350
|
-
const
|
|
351
|
-
const
|
|
359
|
+
const fallbackAngle = Math.PI * 2 * (Math.abs(hashText(`${left.id}:${right.id}`) % 1000) / 1000);
|
|
360
|
+
const ux = Math.abs(dx) + Math.abs(dy) < 0.001 ? Math.cos(fallbackAngle) : dx / distance;
|
|
361
|
+
const uy = Math.abs(dx) + Math.abs(dy) < 0.001 ? Math.sin(fallbackAngle) : dy / distance;
|
|
352
362
|
left.x -= ux * push;
|
|
353
363
|
left.y -= uy * push;
|
|
354
364
|
right.x += ux * push;
|
|
@@ -478,19 +488,35 @@ const createStarNodes = (nodes, segments, degrees, hubId, levels) => {
|
|
|
478
488
|
y: 0
|
|
479
489
|
}));
|
|
480
490
|
}
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
491
|
+
const levelNodesBySegment = segmentNames
|
|
492
|
+
.map((segment) => ({
|
|
493
|
+
segment,
|
|
494
|
+
nodes: levelNodes.filter((node) => (segments.get(node.id) ?? groupLabel(groupKey(node))) === segment)
|
|
495
|
+
}))
|
|
496
|
+
.filter((group) => group.nodes.length > 0);
|
|
497
|
+
const totalNodes = levelNodesBySegment.reduce((total, group) => total + group.nodes.length, 0);
|
|
498
|
+
const baseRadius = Math.max(360 + (level - 1) * 460, (levelNodes.length * 156) / (Math.PI * 2));
|
|
499
|
+
let arcCursor = -Math.PI / 2;
|
|
500
|
+
return levelNodesBySegment.flatMap((group) => {
|
|
501
|
+
const arcSize = (Math.PI * 2 * group.nodes.length) / Math.max(totalNodes, 1);
|
|
502
|
+
const arcPadding = Math.min(0.22, arcSize * 0.18);
|
|
503
|
+
const arcStart = arcCursor + arcPadding;
|
|
504
|
+
const arcEnd = arcCursor + arcSize - arcPadding;
|
|
505
|
+
const usableArc = Math.max(0.001, arcEnd - arcStart);
|
|
506
|
+
const segmentRadius = Math.max(baseRadius, (group.nodes.length * 156) / usableArc);
|
|
507
|
+
arcCursor += arcSize;
|
|
508
|
+
return group.nodes.map((node, index) => {
|
|
509
|
+
const lane = index % 3 - 1;
|
|
510
|
+
const angle = arcStart + usableArc * ((index + 0.5) / Math.max(group.nodes.length, 1)) + jitter(node.title, 0.035);
|
|
511
|
+
const radialJitter = jitter(node.id, 34);
|
|
512
|
+
return {
|
|
513
|
+
...node,
|
|
514
|
+
group: groupLabel(groupKey(node)),
|
|
515
|
+
segment: group.segment,
|
|
516
|
+
x: Math.cos(angle) * (segmentRadius + lane * 52 + radialJitter) + jitter(node.title, 16),
|
|
517
|
+
y: Math.sin(angle) * (segmentRadius + lane * 52 + radialJitter) + jitter(node.path, 16)
|
|
518
|
+
};
|
|
519
|
+
});
|
|
494
520
|
});
|
|
495
521
|
});
|
|
496
522
|
};
|
|
@@ -499,7 +525,7 @@ export const createStarGraphLayout = (graph) => {
|
|
|
499
525
|
const hubId = selectPrimaryHubId(graph.nodes, degrees) ?? selectHighestDegreeNodeId(graph.nodes, degrees);
|
|
500
526
|
const segments = assignSegments(graph.nodes, graph.edges, degrees);
|
|
501
527
|
const levels = assignStarLevels(graph.nodes, graph.edges, hubId);
|
|
502
|
-
const nodes = relaxCollisions(createStarNodes(graph.nodes, segments, degrees, hubId, levels),
|
|
528
|
+
const nodes = relaxCollisions(createStarNodes(graph.nodes, segments, degrees, hubId, levels), 156, 22);
|
|
503
529
|
const centeredNodes = centerLayoutByNode(nodes, hubId);
|
|
504
530
|
const groups = createGraphLayoutHierarchy(centeredNodes, graph.edges, degrees);
|
|
505
531
|
return {
|
|
@@ -10,6 +10,7 @@ export const defaultBrainlinkConfig = {
|
|
|
10
10
|
allowedVaults: [],
|
|
11
11
|
defaultAgent: undefined,
|
|
12
12
|
autoIndexOnWrite: true,
|
|
13
|
+
autoCanonicalContextLinks: true,
|
|
13
14
|
defaultSearchLimit: 10,
|
|
14
15
|
defaultContextTokens: 2000,
|
|
15
16
|
embeddingProvider: 'local',
|
|
@@ -159,6 +160,9 @@ const sanitizeConfig = (value) => ({
|
|
|
159
160
|
? sanitizeAgentId(value.defaultAgent)
|
|
160
161
|
: defaultBrainlinkConfig.defaultAgent,
|
|
161
162
|
autoIndexOnWrite: typeof value.autoIndexOnWrite === 'boolean' ? value.autoIndexOnWrite : defaultBrainlinkConfig.autoIndexOnWrite,
|
|
163
|
+
autoCanonicalContextLinks: typeof value.autoCanonicalContextLinks === 'boolean'
|
|
164
|
+
? value.autoCanonicalContextLinks
|
|
165
|
+
: defaultBrainlinkConfig.autoCanonicalContextLinks,
|
|
162
166
|
defaultSearchLimit: typeof value.defaultSearchLimit === 'number' && value.defaultSearchLimit > 0
|
|
163
167
|
? value.defaultSearchLimit
|
|
164
168
|
: defaultBrainlinkConfig.defaultSearchLimit,
|
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, contextInputSchema, contextTool, 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, 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({
|
|
@@ -68,6 +68,11 @@ export const createBrainlinkMcpServer = () => {
|
|
|
68
68
|
description: 'Read a local markdown/text file and ingest it as a Brainlink note. Reindex defaults to true.',
|
|
69
69
|
inputSchema: addFileInputSchema
|
|
70
70
|
}, addFileTool);
|
|
71
|
+
server.registerTool('brainlink_canonicalize_context_links', {
|
|
72
|
+
title: 'Canonicalize Brainlink Context Links',
|
|
73
|
+
description: 'Ensure notes have canonical Context Links to inferred context hubs. Supports dry-run and can create missing hub notes.',
|
|
74
|
+
inputSchema: canonicalizeContextLinksInputSchema
|
|
75
|
+
}, canonicalizeContextLinksTool);
|
|
71
76
|
server.registerTool('brainlink_index', {
|
|
72
77
|
title: 'Index Brainlink Vault',
|
|
73
78
|
description: 'Rebuild the local Brainlink index from Markdown notes. Pass full=true to force a complete source rebuild.',
|
|
@@ -93,6 +98,11 @@ export const createBrainlinkMcpServer = () => {
|
|
|
93
98
|
description: 'Read indexed graph nodes and wiki-link edges. Edges include weight and priority fields so agents can rank importance and priority.',
|
|
94
99
|
inputSchema: graphInputSchema
|
|
95
100
|
}, graphTool);
|
|
101
|
+
server.registerTool('brainlink_graph_contexts', {
|
|
102
|
+
title: 'List Brainlink Graph Contexts',
|
|
103
|
+
description: 'List visual graph contexts used by the Brainlink server to separate memory domains such as preferences, repositories and machine configuration.',
|
|
104
|
+
inputSchema: graphContextsInputSchema
|
|
105
|
+
}, graphContextsTool);
|
|
96
106
|
server.registerTool('brainlink_broken_links', {
|
|
97
107
|
title: 'List Brainlink Broken Links',
|
|
98
108
|
description: 'List unresolved indexed wiki links.',
|
package/dist/mcp/tools.js
CHANGED
|
@@ -4,8 +4,10 @@ import { z } from 'zod';
|
|
|
4
4
|
import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../application/analyze-vault.js';
|
|
5
5
|
import { addNoteWithMetadata } from '../application/add-note.js';
|
|
6
6
|
import { buildContextPackage } from '../application/build-context.js';
|
|
7
|
+
import { canonicalizeContextLinks } from '../application/canonical-context-links.js';
|
|
7
8
|
import { resolveDuplicateNotes, scanDuplicateNotes } from '../application/dedupe-notes.js';
|
|
8
9
|
import { getGraph } from '../application/get-graph.js';
|
|
10
|
+
import { getGraphContexts } from '../application/get-graph-contexts.js';
|
|
9
11
|
import { indexVault } from '../application/index-vault.js';
|
|
10
12
|
import { searchKnowledge } from '../application/search-knowledge.js';
|
|
11
13
|
import { resolveAgentRuntimeDefaults, sanitizeSearchMode } from '../infrastructure/config.js';
|
|
@@ -237,7 +239,11 @@ export const addNoteInputSchema = {
|
|
|
237
239
|
.describe('Durable Markdown memory. Include explicit [[wiki links]] and #tags when the memory should be connected. Put priority markers near important links, for example priority: high, #important or #critical.'),
|
|
238
240
|
...agentInput,
|
|
239
241
|
allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.'),
|
|
240
|
-
autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.')
|
|
242
|
+
autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.'),
|
|
243
|
+
autoContextLinks: z
|
|
244
|
+
.boolean()
|
|
245
|
+
.optional()
|
|
246
|
+
.describe('Automatically add canonical Context Links to the inferred visual context hub. Defaults to Brainlink config.')
|
|
241
247
|
};
|
|
242
248
|
export const volatileAddInputSchema = {
|
|
243
249
|
...vaultInput,
|
|
@@ -261,6 +267,13 @@ export const addFileInputSchema = {
|
|
|
261
267
|
autoIndex: z.boolean().optional().default(true).describe('Reindex vault after ingesting file.'),
|
|
262
268
|
allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.')
|
|
263
269
|
};
|
|
270
|
+
export const canonicalizeContextLinksInputSchema = {
|
|
271
|
+
...vaultInput,
|
|
272
|
+
...agentInput,
|
|
273
|
+
dryRun: z.boolean().optional().default(false).describe('Preview canonical context-link writes without changing Markdown.'),
|
|
274
|
+
createHubs: z.boolean().optional().default(true).describe('Create missing context hub notes when needed.'),
|
|
275
|
+
autoIndex: z.boolean().optional().default(true).describe('Reindex after canonicalization when files changed.')
|
|
276
|
+
};
|
|
264
277
|
export const indexInputSchema = {
|
|
265
278
|
...vaultInput,
|
|
266
279
|
full: z
|
|
@@ -277,6 +290,10 @@ export const graphInputSchema = {
|
|
|
277
290
|
...vaultInput,
|
|
278
291
|
...agentInput
|
|
279
292
|
};
|
|
293
|
+
export const graphContextsInputSchema = {
|
|
294
|
+
...vaultInput,
|
|
295
|
+
...agentInput
|
|
296
|
+
};
|
|
280
297
|
export const brokenLinksInputSchema = {
|
|
281
298
|
...vaultInput,
|
|
282
299
|
...agentInput
|
|
@@ -401,7 +418,8 @@ export const addNoteTool = async (input) => {
|
|
|
401
418
|
const context = await resolveExecutionContext(input);
|
|
402
419
|
const shouldIndex = isTruthy(input.autoIndex);
|
|
403
420
|
const added = await addNoteWithMetadata(context.vault, input.title, input.content, context.agent, {
|
|
404
|
-
allowSensitive: input.allowSensitive
|
|
421
|
+
allowSensitive: input.allowSensitive,
|
|
422
|
+
autoContextLinks: input.autoContextLinks ?? context.config.autoCanonicalContextLinks
|
|
405
423
|
});
|
|
406
424
|
const index = shouldIndex ? await indexVault(context.vault) : undefined;
|
|
407
425
|
const focusPath = added.path.includes('agents/') ? added.path.slice(added.path.indexOf('agents/')).replaceAll('\\', '/') : undefined;
|
|
@@ -420,7 +438,9 @@ export const addNoteTool = async (input) => {
|
|
|
420
438
|
writeConnectivity: {
|
|
421
439
|
autoLinked: added.autoLinked,
|
|
422
440
|
linkTarget: added.linkTarget,
|
|
423
|
-
|
|
441
|
+
context: added.context,
|
|
442
|
+
hubCreated: added.hubCreated,
|
|
443
|
+
guaranteedEdge: added.autoLinked
|
|
424
444
|
},
|
|
425
445
|
possibleDuplicates,
|
|
426
446
|
...(index ? { index } : {})
|
|
@@ -455,7 +475,8 @@ export const addFileTool = async (input) => {
|
|
|
455
475
|
}
|
|
456
476
|
const shouldIndex = isTruthy(input.autoIndex);
|
|
457
477
|
const added = await addNoteWithMetadata(context.vault, title, content, context.agent, {
|
|
458
|
-
allowSensitive: input.allowSensitive
|
|
478
|
+
allowSensitive: input.allowSensitive,
|
|
479
|
+
autoContextLinks: context.config.autoCanonicalContextLinks
|
|
459
480
|
});
|
|
460
481
|
const index = shouldIndex ? await indexVault(context.vault) : undefined;
|
|
461
482
|
return jsonResult({
|
|
@@ -467,11 +488,30 @@ export const addFileTool = async (input) => {
|
|
|
467
488
|
writeConnectivity: {
|
|
468
489
|
autoLinked: added.autoLinked,
|
|
469
490
|
linkTarget: added.linkTarget,
|
|
470
|
-
|
|
491
|
+
context: added.context,
|
|
492
|
+
hubCreated: added.hubCreated,
|
|
493
|
+
guaranteedEdge: added.autoLinked
|
|
471
494
|
},
|
|
472
495
|
...(index ? { index } : {})
|
|
473
496
|
});
|
|
474
497
|
};
|
|
498
|
+
export const canonicalizeContextLinksTool = async (input) => {
|
|
499
|
+
const context = await resolveExecutionContext(input);
|
|
500
|
+
const result = await canonicalizeContextLinks(context.vault, {
|
|
501
|
+
agentId: context.agent,
|
|
502
|
+
dryRun: input.dryRun === true,
|
|
503
|
+
createMissingHubs: input.createHubs !== false
|
|
504
|
+
});
|
|
505
|
+
const index = input.autoIndex !== false && !result.dryRun && result.changed > 0
|
|
506
|
+
? await indexVault(context.vault, { full: true })
|
|
507
|
+
: undefined;
|
|
508
|
+
return jsonResult({
|
|
509
|
+
vault: context.vault,
|
|
510
|
+
agent: context.agent,
|
|
511
|
+
...result,
|
|
512
|
+
...(index ? { index } : {})
|
|
513
|
+
});
|
|
514
|
+
};
|
|
475
515
|
export const indexTool = async (input) => {
|
|
476
516
|
const context = await resolveExecutionContext(input);
|
|
477
517
|
const result = await indexVault(context.vault, {
|
|
@@ -520,6 +560,25 @@ export const graphTool = async (input) => {
|
|
|
520
560
|
...graph
|
|
521
561
|
});
|
|
522
562
|
};
|
|
563
|
+
export const graphContextsTool = async (input) => {
|
|
564
|
+
const context = await resolveExecutionContext(input);
|
|
565
|
+
const readiness = await ensureBootstrapReady(context, input, 'brainlink_graph_contexts');
|
|
566
|
+
if (readiness.preflight) {
|
|
567
|
+
return readiness.preflight;
|
|
568
|
+
}
|
|
569
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_graph_contexts');
|
|
570
|
+
if (contextReadiness.preflight) {
|
|
571
|
+
return contextReadiness.preflight;
|
|
572
|
+
}
|
|
573
|
+
const contexts = await getGraphContexts(context.vault, context.agent);
|
|
574
|
+
return jsonResult({
|
|
575
|
+
vault: context.vault,
|
|
576
|
+
agent: context.agent,
|
|
577
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
578
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
579
|
+
contexts
|
|
580
|
+
});
|
|
581
|
+
};
|
|
523
582
|
export const brokenLinksTool = async (input) => {
|
|
524
583
|
const context = await resolveExecutionContext(input);
|
|
525
584
|
const readiness = await ensureBootstrapReady(context, input, 'brainlink_broken_links');
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -56,6 +56,7 @@ You can tune search-pack compression with `searchPack.rowChunkSize`, `searchPack
|
|
|
56
56
|
Guardrails for benchmark acceptance are configured with `searchPack.guardrailMinSavingsPercent` and `searchPack.guardrailMaxLatencyRegressionPercent`.
|
|
57
57
|
|
|
58
58
|
`autoIndexOnWrite` (default: `true`) controls whether `add` and MCP write tools index right after writing.
|
|
59
|
+
`autoCanonicalContextLinks` (default: `true`) controls whether CLI/MCP write tools add canonical `## Context Links` to inferred context hubs.
|
|
59
60
|
|
|
60
61
|
## Agent Namespaces
|
|
61
62
|
|
|
@@ -627,12 +628,14 @@ Use `--no-index` when you need to inspect the current index without rebuilding i
|
|
|
627
628
|
blink server --vault ./vault --no-index
|
|
628
629
|
```
|
|
629
630
|
|
|
630
|
-
|
|
631
|
+
The server watches Markdown files by default and keeps the graph updated after edits:
|
|
631
632
|
|
|
632
633
|
```bash
|
|
633
|
-
blink server --vault ./vault
|
|
634
|
+
blink server --vault ./vault
|
|
634
635
|
```
|
|
635
636
|
|
|
637
|
+
Use `--no-watch` when you need to run the graph server without realtime reindexing.
|
|
638
|
+
|
|
636
639
|
### Watch A Vault
|
|
637
640
|
|
|
638
641
|
```bash
|
|
@@ -672,6 +675,7 @@ Available MCP tools:
|
|
|
672
675
|
- `brainlink_resolve_duplicate`
|
|
673
676
|
- `brainlink_add_note`
|
|
674
677
|
- `brainlink_add_file`
|
|
678
|
+
- `brainlink_canonicalize_context_links`
|
|
675
679
|
- `brainlink_volatile_add`
|
|
676
680
|
- `brainlink_volatile_clear`
|
|
677
681
|
- `brainlink_index`
|
|
@@ -679,6 +683,7 @@ Available MCP tools:
|
|
|
679
683
|
- `brainlink_validate`
|
|
680
684
|
- `brainlink_sync`
|
|
681
685
|
- `brainlink_graph`
|
|
686
|
+
- `brainlink_graph_contexts`
|
|
682
687
|
- `brainlink_broken_links`
|
|
683
688
|
- `brainlink_orphans`
|
|
684
689
|
|
package/package.json
CHANGED