@andespindola/brainlink 0.1.0-beta.99 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/AGENTS.md +6 -6
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +186 -38
  4. package/dist/application/add-note.js +13 -44
  5. package/dist/application/analyze-vault.js +1 -1
  6. package/dist/application/auto-migrate-configured-vault.js +37 -0
  7. package/dist/application/build-context.js +119 -20
  8. package/dist/application/canonical-context-links.js +209 -0
  9. package/dist/application/frontend/client-css.js +212 -42
  10. package/dist/application/frontend/client-html.js +42 -28
  11. package/dist/application/frontend/client-js.js +1294 -3222
  12. package/dist/application/frontend/client-render-worker-js.js +676 -0
  13. package/dist/application/get-graph-contexts.js +33 -0
  14. package/dist/application/get-graph-layout.js +62 -8
  15. package/dist/application/get-graph-stream-chunk.js +326 -0
  16. package/dist/application/get-graph-view.js +246 -0
  17. package/dist/application/graph-view-state.js +66 -0
  18. package/dist/application/import-legacy-sqlite.js +3 -33
  19. package/dist/application/index-vault.js +35 -22
  20. package/dist/application/migrate-context-links.js +79 -0
  21. package/dist/application/search-graph-node-ids.js +63 -3
  22. package/dist/application/server/routes.js +197 -12
  23. package/dist/cli/commands/read-commands.js +39 -3
  24. package/dist/cli/commands/vault-commands.js +182 -0
  25. package/dist/cli/commands/write-commands.js +147 -12
  26. package/dist/cli/main.js +2 -0
  27. package/dist/cli/runtime.js +10 -2
  28. package/dist/domain/context.js +1 -0
  29. package/dist/domain/graph-contexts.js +180 -0
  30. package/dist/domain/graph-layout.js +347 -21
  31. package/dist/domain/markdown.js +53 -9
  32. package/dist/infrastructure/config.js +105 -6
  33. package/dist/infrastructure/context-packs.js +122 -0
  34. package/dist/infrastructure/file-index.js +6 -3
  35. package/dist/infrastructure/index-state.js +2 -0
  36. package/dist/infrastructure/vault-migration-state.js +69 -0
  37. package/dist/infrastructure/volatile-memory.js +100 -0
  38. package/dist/mcp/http-server.js +97 -0
  39. package/dist/mcp/runtime.js +20 -0
  40. package/dist/mcp/server.js +36 -13
  41. package/dist/mcp/tools.js +203 -14
  42. package/docs/AGENT_USAGE.md +50 -5
  43. package/docs/ARCHITECTURE.md +11 -0
  44. package/docs/QUICKSTART.md +3 -1
  45. package/docs/RELEASE.md +4 -3
  46. package/package.json +3 -1
@@ -1,18 +1,11 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { readFileSync } from 'node:fs';
3
- import { dirname, join } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, 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 } from './tools.js';
6
- const readPackageVersion = () => {
7
- const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
8
- const metadata = JSON.parse(readFileSync(packagePath, 'utf8'));
9
- return metadata.version ?? '0.0.0';
10
- };
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
+ import { getRuntimeVersion } from './runtime.js';
11
4
  export const createBrainlinkMcpServer = () => {
12
5
  const server = new McpServer({
13
6
  name: 'brainlink',
14
7
  title: 'Brainlink',
15
- version: readPackageVersion(),
8
+ version: getRuntimeVersion(),
16
9
  description: 'Local-first Markdown memory tools for AI agents.'
17
10
  });
18
11
  server.registerTool('brainlink_bootstrap', {
@@ -25,16 +18,26 @@ export const createBrainlinkMcpServer = () => {
25
18
  description: 'Read or update bootstrap enforcement policy and inspect bootstrap readiness for the current vault/agent.',
26
19
  inputSchema: policyInputSchema
27
20
  }, policyTool);
21
+ server.registerTool('brainlink_version', {
22
+ title: 'Read Brainlink Runtime Version',
23
+ description: 'Return the current Brainlink MCP runtime package version and metadata.',
24
+ inputSchema: versionInputSchema
25
+ }, versionTool);
28
26
  server.registerTool('brainlink_recommendations', {
29
27
  title: 'Brainlink Recommended MCP Workflow',
30
- 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.',
31
29
  inputSchema: recommendationsInputSchema
32
30
  }, recommendationsTool);
33
31
  server.registerTool('brainlink_context', {
34
32
  title: 'Build Brainlink Context',
35
- 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.',
36
34
  inputSchema: contextInputSchema
37
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);
38
41
  server.registerTool('brainlink_search', {
39
42
  title: 'Search Brainlink Memory',
40
43
  description: 'Search indexed Brainlink notes with FTS, semantic or hybrid retrieval.',
@@ -55,14 +58,29 @@ export const createBrainlinkMcpServer = () => {
55
58
  description: 'Write durable Markdown memory, then reindex the vault. Include explicit [[wiki links]] for connected graph memory. Add priority markers near links, such as priority: high, #important or #critical, when a relationship should be weighted higher.',
56
59
  inputSchema: addNoteInputSchema
57
60
  }, addNoteTool);
61
+ server.registerTool('brainlink_volatile_add', {
62
+ title: 'Add Volatile Brainlink Memory',
63
+ description: 'Write temporary agent-decided memory with TTL. Use for transient task state without polluting durable Markdown memory.',
64
+ inputSchema: volatileAddInputSchema
65
+ }, volatileAddTool);
66
+ server.registerTool('brainlink_volatile_clear', {
67
+ title: 'Clear Volatile Brainlink Memory',
68
+ description: 'Clear active volatile memory for the current vault/agent namespace.',
69
+ inputSchema: volatileClearInputSchema
70
+ }, volatileClearTool);
58
71
  server.registerTool('brainlink_add_file', {
59
72
  title: 'Ingest Markdown File',
60
73
  description: 'Read a local markdown/text file and ingest it as a Brainlink note. Reindex defaults to true.',
61
74
  inputSchema: addFileInputSchema
62
75
  }, addFileTool);
76
+ server.registerTool('brainlink_canonicalize_context_links', {
77
+ title: 'Canonicalize Brainlink Context Links',
78
+ description: 'Ensure notes have canonical Context Links to inferred context hubs. Supports dry-run and can create missing hub notes.',
79
+ inputSchema: canonicalizeContextLinksInputSchema
80
+ }, canonicalizeContextLinksTool);
63
81
  server.registerTool('brainlink_index', {
64
82
  title: 'Index Brainlink Vault',
65
- description: 'Rebuild the local Brainlink index from Markdown notes.',
83
+ description: 'Rebuild the local Brainlink index from Markdown notes. Pass full=true to force a complete source rebuild.',
66
84
  inputSchema: indexInputSchema
67
85
  }, indexTool);
68
86
  server.registerTool('brainlink_stats', {
@@ -85,6 +103,11 @@ export const createBrainlinkMcpServer = () => {
85
103
  description: 'Read indexed graph nodes and wiki-link edges. Edges include weight and priority fields so agents can rank importance and priority.',
86
104
  inputSchema: graphInputSchema
87
105
  }, graphTool);
106
+ server.registerTool('brainlink_graph_contexts', {
107
+ title: 'List Brainlink Graph Contexts',
108
+ description: 'List visual graph contexts used by the Brainlink server to separate memory domains such as preferences, repositories and machine configuration.',
109
+ inputSchema: graphContextsInputSchema
110
+ }, graphContextsTool);
88
111
  server.registerTool('brainlink_broken_links', {
89
112
  title: 'List Brainlink Broken Links',
90
113
  description: 'List unresolved indexed wiki links.',
package/dist/mcp/tools.js CHANGED
@@ -3,15 +3,20 @@ import { basename, extname } from 'node:path';
3
3
  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
- import { buildContextPackage } from '../application/build-context.js';
6
+ import { buildContextPackage, readContextDataSignature } 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
- import { resolveAgentRuntimeDefaults, sanitizeSearchMode } from '../infrastructure/config.js';
13
+ import { resolveAgentRuntimeDefaults, sanitizeContextStrategy, sanitizeSearchMode } from '../infrastructure/config.js';
14
+ import { clearContextPacks, listContextPacks } from '../infrastructure/context-packs.js';
12
15
  import { loadBrainlinkConfig } from '../infrastructure/config.js';
13
16
  import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
17
+ import { addVolatileMemory, clearVolatileMemory } from '../infrastructure/volatile-memory.js';
14
18
  import { getBootstrapPolicy, getBootstrapSessionStatus, getContextSessionStatus, setBootstrapPolicy, touchBootstrapSession, touchContextSession } from '../infrastructure/session-state.js';
19
+ import { getRuntimeMetadata } from './runtime.js';
15
20
  const positiveInteger = (fallback) => z
16
21
  .number()
17
22
  .int()
@@ -36,6 +41,12 @@ const agentInput = {
36
41
  const searchModeInput = {
37
42
  mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode. Defaults to the Brainlink config value.')
38
43
  };
44
+ const contextStrategyInput = {
45
+ strategy: z
46
+ .enum(['rag', 'cag', 'auto'])
47
+ .optional()
48
+ .describe('Context strategy per call. Use rag for fresh retrieval assembly, cag to reuse persisted context packs when fresh, or auto to choose CAG on fresh pack hits and RAG otherwise. Defaults to the Brainlink config value.')
49
+ };
39
50
  const resolveExecutionContext = async (input) => {
40
51
  const config = await loadBrainlinkConfig();
41
52
  const vault = await assertVaultAllowed(input.vault ?? config.vault, config.allowedVaults);
@@ -118,12 +129,14 @@ const ensureBootstrapReady = async (context, input, toolName) => {
118
129
  };
119
130
  }
120
131
  const mode = typeof input.mode === 'string' && ['fts', 'semantic', 'hybrid'].includes(input.mode) ? input.mode : 'hybrid';
132
+ const strategy = sanitizeContextStrategy(typeof input.strategy === 'string' ? input.strategy : undefined, 'rag');
121
133
  const query = typeof input.query === 'string' && input.query.trim().length > 0 ? input.query : undefined;
122
134
  const bootstrapArgs = {
123
135
  vault: context.vault,
124
136
  ...(context.agent ? { agent: context.agent } : {}),
125
137
  ...(query ? { query } : {}),
126
- mode
138
+ mode,
139
+ strategy
127
140
  };
128
141
  const nextActions = [
129
142
  {
@@ -174,6 +187,7 @@ const ensureContextReady = async (context, input, toolName) => {
174
187
  ? input.contextQuery
175
188
  : '<task>';
176
189
  const mode = sanitizeSearchMode(typeof input.mode === 'string' ? input.mode : undefined, context.defaults.defaultSearchMode);
190
+ const strategy = sanitizeContextStrategy(typeof input.strategy === 'string' ? input.strategy : undefined, context.defaults.defaultContextStrategy);
177
191
  const limit = typeof input.limit === 'number' && Number.isFinite(input.limit) && input.limit > 0
178
192
  ? input.limit
179
193
  : typeof input.contextLimit === 'number' && Number.isFinite(input.contextLimit) && input.contextLimit > 0
@@ -189,6 +203,7 @@ const ensureContextReady = async (context, input, toolName) => {
189
203
  ...(context.agent ? { agent: context.agent } : {}),
190
204
  query: queryFromInput,
191
205
  mode,
206
+ strategy,
192
207
  limit,
193
208
  tokens
194
209
  };
@@ -215,10 +230,17 @@ export const contextInputSchema = {
215
230
  ...vaultInput,
216
231
  ...agentInput,
217
232
  ...searchModeInput,
233
+ ...contextStrategyInput,
218
234
  query: z.string().min(1).describe('Task or question to retrieve Brainlink context for.'),
219
235
  limit: optionalPositiveInteger().describe('Maximum search results before context selection.'),
220
236
  tokens: optionalPositiveInteger().describe('Maximum estimated context tokens.')
221
237
  };
238
+ export const contextPacksInputSchema = {
239
+ ...vaultInput,
240
+ ...agentInput,
241
+ action: z.enum(['list', 'clear']).optional().default('list').describe('Action to perform on persisted CAG context packs.'),
242
+ staleOnly: z.boolean().optional().default(false).describe('When clearing, remove only packs stale for the current index and volatile-memory signature.')
243
+ };
222
244
  export const searchInputSchema = {
223
245
  ...vaultInput,
224
246
  ...agentInput,
@@ -235,7 +257,25 @@ export const addNoteInputSchema = {
235
257
  .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.'),
236
258
  ...agentInput,
237
259
  allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.'),
238
- autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.')
260
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.'),
261
+ autoContextLinks: z
262
+ .boolean()
263
+ .optional()
264
+ .describe('Automatically add canonical Context Links to the inferred visual context hub. Defaults to Brainlink config.')
265
+ };
266
+ export const volatileAddInputSchema = {
267
+ ...vaultInput,
268
+ ...agentInput,
269
+ content: z
270
+ .string()
271
+ .min(1)
272
+ .describe('Temporary agent-decided memory. Use for current task state, hypotheses, transient user preferences and unconfirmed findings.'),
273
+ ttlMinutes: optionalPositiveInteger().describe('Minutes before this volatile memory expires. Defaults to 240.'),
274
+ tags: z.array(z.string()).optional().default([]).describe('Optional tags for volatile retrieval.')
275
+ };
276
+ export const volatileClearInputSchema = {
277
+ ...vaultInput,
278
+ ...agentInput
239
279
  };
240
280
  export const addFileInputSchema = {
241
281
  ...vaultInput,
@@ -245,8 +285,20 @@ export const addFileInputSchema = {
245
285
  autoIndex: z.boolean().optional().default(true).describe('Reindex vault after ingesting file.'),
246
286
  allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.')
247
287
  };
288
+ export const canonicalizeContextLinksInputSchema = {
289
+ ...vaultInput,
290
+ ...agentInput,
291
+ dryRun: z.boolean().optional().default(false).describe('Preview canonical context-link writes without changing Markdown.'),
292
+ createHubs: z.boolean().optional().default(true).describe('Create missing context hub notes when needed.'),
293
+ autoIndex: z.boolean().optional().default(true).describe('Reindex after canonicalization when files changed.')
294
+ };
248
295
  export const indexInputSchema = {
249
- ...vaultInput
296
+ ...vaultInput,
297
+ full: z
298
+ .boolean()
299
+ .optional()
300
+ .default(false)
301
+ .describe('Force a complete reindex from Markdown source without reusing unchanged index entries.')
250
302
  };
251
303
  export const validateInputSchema = {
252
304
  ...vaultInput,
@@ -256,6 +308,10 @@ export const graphInputSchema = {
256
308
  ...vaultInput,
257
309
  ...agentInput
258
310
  };
311
+ export const graphContextsInputSchema = {
312
+ ...vaultInput,
313
+ ...agentInput
314
+ };
259
315
  export const brokenLinksInputSchema = {
260
316
  ...vaultInput,
261
317
  ...agentInput
@@ -273,6 +329,7 @@ export const syncInputSchema = {
273
329
  ...agentInput,
274
330
  contextQuery: z.string().min(1).optional().describe('Optional context smoke query. Omit to skip context probe.'),
275
331
  mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode for the optional context probe. Defaults to config value.'),
332
+ strategy: z.enum(['rag', 'cag', 'auto']).optional().describe('Context strategy for the optional context probe. Defaults to the Brainlink config value.'),
276
333
  contextLimit: optionalPositiveInteger().describe('Context smoke result limit when contextQuery is provided.'),
277
334
  contextTokens: optionalPositiveInteger().describe('Context smoke token target when contextQuery is provided.')
278
335
  };
@@ -280,6 +337,7 @@ export const bootstrapInputSchema = {
280
337
  ...vaultInput,
281
338
  ...agentInput,
282
339
  ...searchModeInput,
340
+ ...contextStrategyInput,
283
341
  query: z
284
342
  .string()
285
343
  .min(1)
@@ -304,10 +362,15 @@ export const policyInputSchema = {
304
362
  .describe('Run automatic bootstrap during MCP server startup using configured default vault/agent.'),
305
363
  staleAfterMinutes: positiveInteger(120).describe('Bootstrap freshness window in minutes before read tools require a new bootstrap.')
306
364
  };
365
+ export const versionInputSchema = {
366
+ ...vaultInput,
367
+ ...agentInput
368
+ };
307
369
  export const recommendationsInputSchema = {
308
370
  ...vaultInput,
309
371
  ...agentInput,
310
372
  ...searchModeInput,
373
+ ...contextStrategyInput,
311
374
  query: z.string().min(1).optional().describe('Optional current task query to generate context-focused recommendations.'),
312
375
  limit: optionalPositiveInteger().describe('Optional context limit override for generated recommendations.'),
313
376
  tokens: optionalPositiveInteger().describe('Optional context token budget override for generated recommendations.')
@@ -333,14 +396,16 @@ export const contextTool = async (input) => {
333
396
  return readiness.preflight;
334
397
  }
335
398
  const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
399
+ const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
336
400
  const limit = input.limit ?? context.defaults.defaultSearchLimit;
337
401
  const tokens = input.tokens ?? context.defaults.defaultContextTokens;
338
- const contextPackage = await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode);
402
+ const contextPackage = await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs);
339
403
  const contextSession = await touchContextSession(context.vault, context.agent);
340
404
  return jsonResult({
341
405
  vault: context.vault,
342
406
  agent: context.agent,
343
407
  mode,
408
+ strategy,
344
409
  limit,
345
410
  tokens,
346
411
  contextSession,
@@ -348,6 +413,32 @@ export const contextTool = async (input) => {
348
413
  ...contextPackage
349
414
  });
350
415
  };
416
+ export const contextPacksTool = async (input) => {
417
+ const context = await resolveExecutionContext(input);
418
+ const dataSignature = await readContextDataSignature(context.vault);
419
+ if (input.action === 'clear') {
420
+ const result = await clearContextPacks(context.vault, {
421
+ staleOnly: input.staleOnly === true,
422
+ dataSignature
423
+ });
424
+ return jsonResult({
425
+ vault: context.vault,
426
+ agent: context.agent,
427
+ dataSignature,
428
+ action: 'clear',
429
+ staleOnly: input.staleOnly === true,
430
+ ...result
431
+ });
432
+ }
433
+ const packs = await listContextPacks(context.vault, dataSignature);
434
+ return jsonResult({
435
+ vault: context.vault,
436
+ agent: context.agent,
437
+ dataSignature,
438
+ action: 'list',
439
+ packs
440
+ });
441
+ };
351
442
  export const searchTool = async (input) => {
352
443
  const context = await resolveExecutionContext(input);
353
444
  const readiness = await ensureBootstrapReady(context, input, 'brainlink_search');
@@ -376,7 +467,8 @@ export const addNoteTool = async (input) => {
376
467
  const context = await resolveExecutionContext(input);
377
468
  const shouldIndex = isTruthy(input.autoIndex);
378
469
  const added = await addNoteWithMetadata(context.vault, input.title, input.content, context.agent, {
379
- allowSensitive: input.allowSensitive
470
+ allowSensitive: input.allowSensitive,
471
+ autoContextLinks: input.autoContextLinks ?? context.config.autoCanonicalContextLinks
380
472
  });
381
473
  const index = shouldIndex ? await indexVault(context.vault) : undefined;
382
474
  const focusPath = added.path.includes('agents/') ? added.path.slice(added.path.indexOf('agents/')).replaceAll('\\', '/') : undefined;
@@ -395,12 +487,33 @@ export const addNoteTool = async (input) => {
395
487
  writeConnectivity: {
396
488
  autoLinked: added.autoLinked,
397
489
  linkTarget: added.linkTarget,
398
- guaranteedEdge: true
490
+ context: added.context,
491
+ hubCreated: added.hubCreated,
492
+ guaranteedEdge: added.autoLinked
399
493
  },
400
494
  possibleDuplicates,
401
495
  ...(index ? { index } : {})
402
496
  });
403
497
  };
498
+ export const volatileAddTool = async (input) => {
499
+ const context = await resolveExecutionContext(input);
500
+ const entry = await addVolatileMemory(context.vault, input.content, context.agent ?? 'shared', input.ttlMinutes ?? 240, input.tags);
501
+ return jsonResult({
502
+ vault: context.vault,
503
+ agent: context.agent,
504
+ volatile: true,
505
+ entry
506
+ });
507
+ };
508
+ export const volatileClearTool = async (input) => {
509
+ const context = await resolveExecutionContext(input);
510
+ const cleared = await clearVolatileMemory(context.vault, context.agent);
511
+ return jsonResult({
512
+ vault: context.vault,
513
+ agent: context.agent,
514
+ cleared
515
+ });
516
+ };
404
517
  export const addFileTool = async (input) => {
405
518
  const context = await resolveExecutionContext(input);
406
519
  const content = await readFile(input.filePath, 'utf8');
@@ -411,7 +524,8 @@ export const addFileTool = async (input) => {
411
524
  }
412
525
  const shouldIndex = isTruthy(input.autoIndex);
413
526
  const added = await addNoteWithMetadata(context.vault, title, content, context.agent, {
414
- allowSensitive: input.allowSensitive
527
+ allowSensitive: input.allowSensitive,
528
+ autoContextLinks: context.config.autoCanonicalContextLinks
415
529
  });
416
530
  const index = shouldIndex ? await indexVault(context.vault) : undefined;
417
531
  return jsonResult({
@@ -423,14 +537,35 @@ export const addFileTool = async (input) => {
423
537
  writeConnectivity: {
424
538
  autoLinked: added.autoLinked,
425
539
  linkTarget: added.linkTarget,
426
- guaranteedEdge: true
540
+ context: added.context,
541
+ hubCreated: added.hubCreated,
542
+ guaranteedEdge: added.autoLinked
427
543
  },
428
544
  ...(index ? { index } : {})
429
545
  });
430
546
  };
547
+ export const canonicalizeContextLinksTool = async (input) => {
548
+ const context = await resolveExecutionContext(input);
549
+ const result = await canonicalizeContextLinks(context.vault, {
550
+ agentId: context.agent,
551
+ dryRun: input.dryRun === true,
552
+ createMissingHubs: input.createHubs !== false
553
+ });
554
+ const index = input.autoIndex !== false && !result.dryRun && result.changed > 0
555
+ ? await indexVault(context.vault, { full: true })
556
+ : undefined;
557
+ return jsonResult({
558
+ vault: context.vault,
559
+ agent: context.agent,
560
+ ...result,
561
+ ...(index ? { index } : {})
562
+ });
563
+ };
431
564
  export const indexTool = async (input) => {
432
565
  const context = await resolveExecutionContext(input);
433
- const result = await indexVault(context.vault);
566
+ const result = await indexVault(context.vault, {
567
+ full: input.full === true
568
+ });
434
569
  return jsonResult({
435
570
  vault: context.vault,
436
571
  ...result
@@ -474,6 +609,25 @@ export const graphTool = async (input) => {
474
609
  ...graph
475
610
  });
476
611
  };
612
+ export const graphContextsTool = async (input) => {
613
+ const context = await resolveExecutionContext(input);
614
+ const readiness = await ensureBootstrapReady(context, input, 'brainlink_graph_contexts');
615
+ if (readiness.preflight) {
616
+ return readiness.preflight;
617
+ }
618
+ const contextReadiness = await ensureContextReady(context, input, 'brainlink_graph_contexts');
619
+ if (contextReadiness.preflight) {
620
+ return contextReadiness.preflight;
621
+ }
622
+ const contexts = await getGraphContexts(context.vault, context.agent);
623
+ return jsonResult({
624
+ vault: context.vault,
625
+ agent: context.agent,
626
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
627
+ ...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
628
+ contexts
629
+ });
630
+ };
477
631
  export const brokenLinksTool = async (input) => {
478
632
  const context = await resolveExecutionContext(input);
479
633
  const readiness = await ensureBootstrapReady(context, input, 'brainlink_broken_links');
@@ -561,14 +715,16 @@ export const syncTool = async (input) => {
561
715
  return jsonResult(response);
562
716
  }
563
717
  const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
718
+ const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
564
719
  const contextLimit = input.contextLimit ?? context.defaults.defaultSearchLimit;
565
720
  const contextTokens = input.contextTokens ?? context.defaults.defaultContextTokens;
566
- const contextPackage = await buildContextPackage(context.vault, input.contextQuery, contextLimit, contextTokens, context.agent, mode);
721
+ const contextPackage = await buildContextPackage(context.vault, input.contextQuery, contextLimit, contextTokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs);
567
722
  const contextSession = await touchContextSession(context.vault, context.agent);
568
723
  return jsonResult({
569
724
  ...response,
570
725
  context: {
571
726
  mode,
727
+ strategy,
572
728
  contextSession,
573
729
  ...contextPackage
574
730
  }
@@ -580,10 +736,11 @@ export const bootstrapTool = async (input) => {
580
736
  const stats = await getStats(context.vault, context.agent);
581
737
  const validation = await validateVault(context.vault, context.agent);
582
738
  const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
739
+ const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
583
740
  const limit = input.limit ?? context.defaults.defaultSearchLimit;
584
741
  const tokens = input.tokens ?? context.defaults.defaultContextTokens;
585
742
  const contextPackage = input.query
586
- ? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode)
743
+ ? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs)
587
744
  : undefined;
588
745
  const contextSession = input.query ? await touchContextSession(context.vault, context.agent) : undefined;
589
746
  const guidance = stats.documentCount === 0
@@ -637,6 +794,7 @@ export const bootstrapTool = async (input) => {
637
794
  ...(context.agent ? { agent: context.agent } : {}),
638
795
  query: '<task>',
639
796
  mode,
797
+ strategy,
640
798
  limit,
641
799
  tokens
642
800
  }
@@ -646,6 +804,7 @@ export const bootstrapTool = async (input) => {
646
804
  vault: context.vault,
647
805
  agent: context.agent,
648
806
  mode,
807
+ strategy,
649
808
  limit,
650
809
  tokens,
651
810
  index,
@@ -701,7 +860,8 @@ export const policyTool = async (input) => {
701
860
  args: {
702
861
  vault: context.vault,
703
862
  ...(context.agent ? { agent: context.agent } : {}),
704
- mode: context.defaults.defaultSearchMode
863
+ mode: context.defaults.defaultSearchMode,
864
+ strategy: context.defaults.defaultContextStrategy
705
865
  }
706
866
  }
707
867
  ];
@@ -716,6 +876,7 @@ export const policyTool = async (input) => {
716
876
  ...(context.agent ? { agent: context.agent } : {}),
717
877
  query: '<task>',
718
878
  mode: context.defaults.defaultSearchMode,
879
+ strategy: context.defaults.defaultContextStrategy,
719
880
  limit: context.defaults.defaultSearchLimit,
720
881
  tokens: context.defaults.defaultContextTokens
721
882
  }
@@ -725,12 +886,21 @@ export const policyTool = async (input) => {
725
886
  return jsonResult(withNextActions({
726
887
  vault: context.vault,
727
888
  agent: context.agent,
889
+ runtime: getRuntimeMetadata(),
728
890
  policy,
729
891
  bootstrapStatus,
730
892
  contextStatus,
731
893
  ...(input.preset ? { presetApplied: input.preset } : {})
732
894
  }, withContextAction));
733
895
  };
896
+ export const versionTool = async (input) => {
897
+ const context = await resolveExecutionContext(input);
898
+ return jsonResult({
899
+ vault: context.vault,
900
+ agent: context.agent,
901
+ runtime: getRuntimeMetadata()
902
+ });
903
+ };
734
904
  export const recommendationsTool = async (input) => {
735
905
  const context = await resolveExecutionContext(input);
736
906
  const policy = await getBootstrapPolicy();
@@ -738,6 +908,7 @@ export const recommendationsTool = async (input) => {
738
908
  const contextStatus = await getContextSessionStatus(context.vault, context.agent);
739
909
  const stats = await getStats(context.vault, context.agent);
740
910
  const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
911
+ const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
741
912
  const limit = input.limit ?? context.defaults.defaultSearchLimit;
742
913
  const tokens = input.tokens ?? context.defaults.defaultContextTokens;
743
914
  const query = input.query?.trim();
@@ -762,6 +933,7 @@ export const recommendationsTool = async (input) => {
762
933
  vault: context.vault,
763
934
  ...(context.agent ? { agent: context.agent } : {}),
764
935
  mode,
936
+ strategy,
765
937
  ...(query ? { query } : {})
766
938
  }
767
939
  }
@@ -777,6 +949,7 @@ export const recommendationsTool = async (input) => {
777
949
  ...(context.agent ? { agent: context.agent } : {}),
778
950
  query: query ?? '<task>',
779
951
  mode,
952
+ strategy,
780
953
  limit,
781
954
  tokens
782
955
  }
@@ -812,6 +985,7 @@ export const recommendationsTool = async (input) => {
812
985
  ...(context.agent ? { agent: context.agent } : {}),
813
986
  query: query ?? '<task>',
814
987
  mode,
988
+ strategy,
815
989
  limit,
816
990
  tokens
817
991
  }
@@ -843,9 +1017,24 @@ export const recommendationsTool = async (input) => {
843
1017
  agent: context.agent,
844
1018
  defaults: {
845
1019
  mode,
1020
+ strategy,
846
1021
  limit,
847
1022
  tokens
848
1023
  },
1024
+ contextStrategies: [
1025
+ {
1026
+ strategy: 'rag',
1027
+ useWhen: 'Use for fresh retrieval and context assembly from the current index.'
1028
+ },
1029
+ {
1030
+ strategy: 'cag',
1031
+ useWhen: 'Use for repeated or stable task context so Brainlink can reuse a fresh persisted context pack.'
1032
+ },
1033
+ {
1034
+ strategy: 'auto',
1035
+ useWhen: 'Use when the agent wants Brainlink to choose CAG on fresh pack hits and RAG otherwise.'
1036
+ }
1037
+ ],
849
1038
  policy,
850
1039
  bootstrapStatus,
851
1040
  contextStatus,