@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.
@@ -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(seeds.map((seed) => [seed.id, segmentName(seed)]));
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 ux = dx / distance;
351
- const uy = dy / distance;
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 ringRadius = Math.max(300 + (level - 1) * 360, (levelNodes.length * 112) / (Math.PI * 2));
482
- return levelNodes.map((node, index) => {
483
- const segment = segments.get(node.id) ?? groupLabel(groupKey(node));
484
- const segmentOffset = (segmentIndexByName.get(segment) ?? 0) / Math.max(segmentNames.length, 1);
485
- const angle = Math.PI * 2 * ((index / Math.max(levelNodes.length, 1) + segmentOffset * 0.18) % 1) - Math.PI / 2;
486
- const radialJitter = jitter(node.id, 48);
487
- return {
488
- ...node,
489
- group: groupLabel(groupKey(node)),
490
- segment,
491
- x: Math.cos(angle) * (ringRadius + radialJitter) + jitter(node.title, 18),
492
- y: Math.sin(angle) * (ringRadius + radialJitter) + jitter(node.path, 18)
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), 132, 18);
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,
@@ -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
- guaranteedEdge: false
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
- guaranteedEdge: false
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');
@@ -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
- Use `--watch` to keep the graph updated after Markdown edits:
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 --watch
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.153",
3
+ "version": "0.1.0-beta.155",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",