@andespindola/brainlink 0.1.0-beta.99 → 1.0.1

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 (48) hide show
  1. package/AGENTS.md +6 -6
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +198 -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/delete-note.js +80 -0
  10. package/dist/application/frontend/client-css.js +212 -42
  11. package/dist/application/frontend/client-html.js +42 -28
  12. package/dist/application/frontend/client-js.js +1294 -3222
  13. package/dist/application/frontend/client-render-worker-js.js +676 -0
  14. package/dist/application/get-graph-contexts.js +33 -0
  15. package/dist/application/get-graph-layout.js +62 -8
  16. package/dist/application/get-graph-stream-chunk.js +326 -0
  17. package/dist/application/get-graph-view.js +246 -0
  18. package/dist/application/graph-view-state.js +66 -0
  19. package/dist/application/import-legacy-sqlite.js +3 -33
  20. package/dist/application/index-vault.js +35 -22
  21. package/dist/application/migrate-context-links.js +79 -0
  22. package/dist/application/search-graph-node-ids.js +63 -3
  23. package/dist/application/server/routes.js +197 -12
  24. package/dist/cli/commands/read-commands.js +39 -3
  25. package/dist/cli/commands/vault-commands.js +182 -0
  26. package/dist/cli/commands/write-commands.js +172 -12
  27. package/dist/cli/main.js +2 -0
  28. package/dist/cli/runtime.js +10 -2
  29. package/dist/domain/context.js +1 -0
  30. package/dist/domain/graph-contexts.js +180 -0
  31. package/dist/domain/graph-layout.js +347 -21
  32. package/dist/domain/markdown.js +53 -9
  33. package/dist/infrastructure/config.js +105 -6
  34. package/dist/infrastructure/context-packs.js +122 -0
  35. package/dist/infrastructure/file-index.js +6 -3
  36. package/dist/infrastructure/file-system-vault.js +21 -1
  37. package/dist/infrastructure/index-state.js +2 -0
  38. package/dist/infrastructure/vault-migration-state.js +69 -0
  39. package/dist/infrastructure/volatile-memory.js +100 -0
  40. package/dist/mcp/http-server.js +97 -0
  41. package/dist/mcp/runtime.js +20 -0
  42. package/dist/mcp/server.js +41 -13
  43. package/dist/mcp/tools.js +226 -14
  44. package/docs/AGENT_USAGE.md +60 -5
  45. package/docs/ARCHITECTURE.md +11 -0
  46. package/docs/QUICKSTART.md +3 -1
  47. package/docs/RELEASE.md +4 -3
  48. 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, deleteNoteInputSchema, deleteNoteTool, 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,34 @@ 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_delete_note', {
62
+ title: 'Delete Brainlink Note',
63
+ description: 'Delete a durable Markdown note from the vault after explicit confirmation. Select by title or path; reindexes by default.',
64
+ inputSchema: deleteNoteInputSchema
65
+ }, deleteNoteTool);
66
+ server.registerTool('brainlink_volatile_add', {
67
+ title: 'Add Volatile Brainlink Memory',
68
+ description: 'Write temporary agent-decided memory with TTL. Use for transient task state without polluting durable Markdown memory.',
69
+ inputSchema: volatileAddInputSchema
70
+ }, volatileAddTool);
71
+ server.registerTool('brainlink_volatile_clear', {
72
+ title: 'Clear Volatile Brainlink Memory',
73
+ description: 'Clear active volatile memory for the current vault/agent namespace.',
74
+ inputSchema: volatileClearInputSchema
75
+ }, volatileClearTool);
58
76
  server.registerTool('brainlink_add_file', {
59
77
  title: 'Ingest Markdown File',
60
78
  description: 'Read a local markdown/text file and ingest it as a Brainlink note. Reindex defaults to true.',
61
79
  inputSchema: addFileInputSchema
62
80
  }, addFileTool);
81
+ server.registerTool('brainlink_canonicalize_context_links', {
82
+ title: 'Canonicalize Brainlink Context Links',
83
+ description: 'Ensure notes have canonical Context Links to inferred context hubs. Supports dry-run and can create missing hub notes.',
84
+ inputSchema: canonicalizeContextLinksInputSchema
85
+ }, canonicalizeContextLinksTool);
63
86
  server.registerTool('brainlink_index', {
64
87
  title: 'Index Brainlink Vault',
65
- description: 'Rebuild the local Brainlink index from Markdown notes.',
88
+ description: 'Rebuild the local Brainlink index from Markdown notes. Pass full=true to force a complete source rebuild.',
66
89
  inputSchema: indexInputSchema
67
90
  }, indexTool);
68
91
  server.registerTool('brainlink_stats', {
@@ -85,6 +108,11 @@ export const createBrainlinkMcpServer = () => {
85
108
  description: 'Read indexed graph nodes and wiki-link edges. Edges include weight and priority fields so agents can rank importance and priority.',
86
109
  inputSchema: graphInputSchema
87
110
  }, graphTool);
111
+ server.registerTool('brainlink_graph_contexts', {
112
+ title: 'List Brainlink Graph Contexts',
113
+ description: 'List visual graph contexts used by the Brainlink server to separate memory domains such as preferences, repositories and machine configuration.',
114
+ inputSchema: graphContextsInputSchema
115
+ }, graphContextsTool);
88
116
  server.registerTool('brainlink_broken_links', {
89
117
  title: 'List Brainlink Broken Links',
90
118
  description: 'List unresolved indexed wiki links.',
package/dist/mcp/tools.js CHANGED
@@ -3,15 +3,21 @@ 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';
8
+ import { deleteNote } from '../application/delete-note.js';
7
9
  import { resolveDuplicateNotes, scanDuplicateNotes } from '../application/dedupe-notes.js';
8
10
  import { getGraph } from '../application/get-graph.js';
11
+ import { getGraphContexts } from '../application/get-graph-contexts.js';
9
12
  import { indexVault } from '../application/index-vault.js';
10
13
  import { searchKnowledge } from '../application/search-knowledge.js';
11
- import { resolveAgentRuntimeDefaults, sanitizeSearchMode } from '../infrastructure/config.js';
14
+ import { resolveAgentRuntimeDefaults, sanitizeContextStrategy, sanitizeSearchMode } from '../infrastructure/config.js';
15
+ import { clearContextPacks, listContextPacks } from '../infrastructure/context-packs.js';
12
16
  import { loadBrainlinkConfig } from '../infrastructure/config.js';
13
17
  import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
18
+ import { addVolatileMemory, clearVolatileMemory } from '../infrastructure/volatile-memory.js';
14
19
  import { getBootstrapPolicy, getBootstrapSessionStatus, getContextSessionStatus, setBootstrapPolicy, touchBootstrapSession, touchContextSession } from '../infrastructure/session-state.js';
20
+ import { getRuntimeMetadata } from './runtime.js';
15
21
  const positiveInteger = (fallback) => z
16
22
  .number()
17
23
  .int()
@@ -36,6 +42,12 @@ const agentInput = {
36
42
  const searchModeInput = {
37
43
  mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode. Defaults to the Brainlink config value.')
38
44
  };
45
+ const contextStrategyInput = {
46
+ strategy: z
47
+ .enum(['rag', 'cag', 'auto'])
48
+ .optional()
49
+ .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.')
50
+ };
39
51
  const resolveExecutionContext = async (input) => {
40
52
  const config = await loadBrainlinkConfig();
41
53
  const vault = await assertVaultAllowed(input.vault ?? config.vault, config.allowedVaults);
@@ -118,12 +130,14 @@ const ensureBootstrapReady = async (context, input, toolName) => {
118
130
  };
119
131
  }
120
132
  const mode = typeof input.mode === 'string' && ['fts', 'semantic', 'hybrid'].includes(input.mode) ? input.mode : 'hybrid';
133
+ const strategy = sanitizeContextStrategy(typeof input.strategy === 'string' ? input.strategy : undefined, 'rag');
121
134
  const query = typeof input.query === 'string' && input.query.trim().length > 0 ? input.query : undefined;
122
135
  const bootstrapArgs = {
123
136
  vault: context.vault,
124
137
  ...(context.agent ? { agent: context.agent } : {}),
125
138
  ...(query ? { query } : {}),
126
- mode
139
+ mode,
140
+ strategy
127
141
  };
128
142
  const nextActions = [
129
143
  {
@@ -174,6 +188,7 @@ const ensureContextReady = async (context, input, toolName) => {
174
188
  ? input.contextQuery
175
189
  : '<task>';
176
190
  const mode = sanitizeSearchMode(typeof input.mode === 'string' ? input.mode : undefined, context.defaults.defaultSearchMode);
191
+ const strategy = sanitizeContextStrategy(typeof input.strategy === 'string' ? input.strategy : undefined, context.defaults.defaultContextStrategy);
177
192
  const limit = typeof input.limit === 'number' && Number.isFinite(input.limit) && input.limit > 0
178
193
  ? input.limit
179
194
  : typeof input.contextLimit === 'number' && Number.isFinite(input.contextLimit) && input.contextLimit > 0
@@ -189,6 +204,7 @@ const ensureContextReady = async (context, input, toolName) => {
189
204
  ...(context.agent ? { agent: context.agent } : {}),
190
205
  query: queryFromInput,
191
206
  mode,
207
+ strategy,
192
208
  limit,
193
209
  tokens
194
210
  };
@@ -215,10 +231,17 @@ export const contextInputSchema = {
215
231
  ...vaultInput,
216
232
  ...agentInput,
217
233
  ...searchModeInput,
234
+ ...contextStrategyInput,
218
235
  query: z.string().min(1).describe('Task or question to retrieve Brainlink context for.'),
219
236
  limit: optionalPositiveInteger().describe('Maximum search results before context selection.'),
220
237
  tokens: optionalPositiveInteger().describe('Maximum estimated context tokens.')
221
238
  };
239
+ export const contextPacksInputSchema = {
240
+ ...vaultInput,
241
+ ...agentInput,
242
+ action: z.enum(['list', 'clear']).optional().default('list').describe('Action to perform on persisted CAG context packs.'),
243
+ staleOnly: z.boolean().optional().default(false).describe('When clearing, remove only packs stale for the current index and volatile-memory signature.')
244
+ };
222
245
  export const searchInputSchema = {
223
246
  ...vaultInput,
224
247
  ...agentInput,
@@ -235,7 +258,33 @@ export const addNoteInputSchema = {
235
258
  .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
259
  ...agentInput,
237
260
  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.')
261
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.'),
262
+ autoContextLinks: z
263
+ .boolean()
264
+ .optional()
265
+ .describe('Automatically add canonical Context Links to the inferred visual context hub. Defaults to Brainlink config.')
266
+ };
267
+ export const deleteNoteInputSchema = {
268
+ ...vaultInput,
269
+ ...agentInput,
270
+ title: z.string().min(1).optional().describe('Note title to delete. Use agent to disambiguate namespaced notes.'),
271
+ path: z.string().min(1).optional().describe('Vault-relative or absolute Markdown note path to delete.'),
272
+ confirm: z.boolean().describe('Must be true to confirm deletion.'),
273
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after deletion. Defaults to true.')
274
+ };
275
+ export const volatileAddInputSchema = {
276
+ ...vaultInput,
277
+ ...agentInput,
278
+ content: z
279
+ .string()
280
+ .min(1)
281
+ .describe('Temporary agent-decided memory. Use for current task state, hypotheses, transient user preferences and unconfirmed findings.'),
282
+ ttlMinutes: optionalPositiveInteger().describe('Minutes before this volatile memory expires. Defaults to 240.'),
283
+ tags: z.array(z.string()).optional().default([]).describe('Optional tags for volatile retrieval.')
284
+ };
285
+ export const volatileClearInputSchema = {
286
+ ...vaultInput,
287
+ ...agentInput
239
288
  };
240
289
  export const addFileInputSchema = {
241
290
  ...vaultInput,
@@ -245,8 +294,20 @@ export const addFileInputSchema = {
245
294
  autoIndex: z.boolean().optional().default(true).describe('Reindex vault after ingesting file.'),
246
295
  allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.')
247
296
  };
297
+ export const canonicalizeContextLinksInputSchema = {
298
+ ...vaultInput,
299
+ ...agentInput,
300
+ dryRun: z.boolean().optional().default(false).describe('Preview canonical context-link writes without changing Markdown.'),
301
+ createHubs: z.boolean().optional().default(true).describe('Create missing context hub notes when needed.'),
302
+ autoIndex: z.boolean().optional().default(true).describe('Reindex after canonicalization when files changed.')
303
+ };
248
304
  export const indexInputSchema = {
249
- ...vaultInput
305
+ ...vaultInput,
306
+ full: z
307
+ .boolean()
308
+ .optional()
309
+ .default(false)
310
+ .describe('Force a complete reindex from Markdown source without reusing unchanged index entries.')
250
311
  };
251
312
  export const validateInputSchema = {
252
313
  ...vaultInput,
@@ -256,6 +317,10 @@ export const graphInputSchema = {
256
317
  ...vaultInput,
257
318
  ...agentInput
258
319
  };
320
+ export const graphContextsInputSchema = {
321
+ ...vaultInput,
322
+ ...agentInput
323
+ };
259
324
  export const brokenLinksInputSchema = {
260
325
  ...vaultInput,
261
326
  ...agentInput
@@ -273,6 +338,7 @@ export const syncInputSchema = {
273
338
  ...agentInput,
274
339
  contextQuery: z.string().min(1).optional().describe('Optional context smoke query. Omit to skip context probe.'),
275
340
  mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode for the optional context probe. Defaults to config value.'),
341
+ strategy: z.enum(['rag', 'cag', 'auto']).optional().describe('Context strategy for the optional context probe. Defaults to the Brainlink config value.'),
276
342
  contextLimit: optionalPositiveInteger().describe('Context smoke result limit when contextQuery is provided.'),
277
343
  contextTokens: optionalPositiveInteger().describe('Context smoke token target when contextQuery is provided.')
278
344
  };
@@ -280,6 +346,7 @@ export const bootstrapInputSchema = {
280
346
  ...vaultInput,
281
347
  ...agentInput,
282
348
  ...searchModeInput,
349
+ ...contextStrategyInput,
283
350
  query: z
284
351
  .string()
285
352
  .min(1)
@@ -304,10 +371,15 @@ export const policyInputSchema = {
304
371
  .describe('Run automatic bootstrap during MCP server startup using configured default vault/agent.'),
305
372
  staleAfterMinutes: positiveInteger(120).describe('Bootstrap freshness window in minutes before read tools require a new bootstrap.')
306
373
  };
374
+ export const versionInputSchema = {
375
+ ...vaultInput,
376
+ ...agentInput
377
+ };
307
378
  export const recommendationsInputSchema = {
308
379
  ...vaultInput,
309
380
  ...agentInput,
310
381
  ...searchModeInput,
382
+ ...contextStrategyInput,
311
383
  query: z.string().min(1).optional().describe('Optional current task query to generate context-focused recommendations.'),
312
384
  limit: optionalPositiveInteger().describe('Optional context limit override for generated recommendations.'),
313
385
  tokens: optionalPositiveInteger().describe('Optional context token budget override for generated recommendations.')
@@ -333,14 +405,16 @@ export const contextTool = async (input) => {
333
405
  return readiness.preflight;
334
406
  }
335
407
  const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
408
+ const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
336
409
  const limit = input.limit ?? context.defaults.defaultSearchLimit;
337
410
  const tokens = input.tokens ?? context.defaults.defaultContextTokens;
338
- const contextPackage = await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode);
411
+ const contextPackage = await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs);
339
412
  const contextSession = await touchContextSession(context.vault, context.agent);
340
413
  return jsonResult({
341
414
  vault: context.vault,
342
415
  agent: context.agent,
343
416
  mode,
417
+ strategy,
344
418
  limit,
345
419
  tokens,
346
420
  contextSession,
@@ -348,6 +422,32 @@ export const contextTool = async (input) => {
348
422
  ...contextPackage
349
423
  });
350
424
  };
425
+ export const contextPacksTool = async (input) => {
426
+ const context = await resolveExecutionContext(input);
427
+ const dataSignature = await readContextDataSignature(context.vault);
428
+ if (input.action === 'clear') {
429
+ const result = await clearContextPacks(context.vault, {
430
+ staleOnly: input.staleOnly === true,
431
+ dataSignature
432
+ });
433
+ return jsonResult({
434
+ vault: context.vault,
435
+ agent: context.agent,
436
+ dataSignature,
437
+ action: 'clear',
438
+ staleOnly: input.staleOnly === true,
439
+ ...result
440
+ });
441
+ }
442
+ const packs = await listContextPacks(context.vault, dataSignature);
443
+ return jsonResult({
444
+ vault: context.vault,
445
+ agent: context.agent,
446
+ dataSignature,
447
+ action: 'list',
448
+ packs
449
+ });
450
+ };
351
451
  export const searchTool = async (input) => {
352
452
  const context = await resolveExecutionContext(input);
353
453
  const readiness = await ensureBootstrapReady(context, input, 'brainlink_search');
@@ -376,7 +476,8 @@ export const addNoteTool = async (input) => {
376
476
  const context = await resolveExecutionContext(input);
377
477
  const shouldIndex = isTruthy(input.autoIndex);
378
478
  const added = await addNoteWithMetadata(context.vault, input.title, input.content, context.agent, {
379
- allowSensitive: input.allowSensitive
479
+ allowSensitive: input.allowSensitive,
480
+ autoContextLinks: input.autoContextLinks ?? context.config.autoCanonicalContextLinks
380
481
  });
381
482
  const index = shouldIndex ? await indexVault(context.vault) : undefined;
382
483
  const focusPath = added.path.includes('agents/') ? added.path.slice(added.path.indexOf('agents/')).replaceAll('\\', '/') : undefined;
@@ -395,12 +496,47 @@ export const addNoteTool = async (input) => {
395
496
  writeConnectivity: {
396
497
  autoLinked: added.autoLinked,
397
498
  linkTarget: added.linkTarget,
398
- guaranteedEdge: true
499
+ context: added.context,
500
+ hubCreated: added.hubCreated,
501
+ guaranteedEdge: added.autoLinked
399
502
  },
400
503
  possibleDuplicates,
401
504
  ...(index ? { index } : {})
402
505
  });
403
506
  };
507
+ export const deleteNoteTool = async (input) => {
508
+ const context = await resolveExecutionContext(input);
509
+ const result = await deleteNote(context.vault, {
510
+ title: input.title,
511
+ path: input.path,
512
+ agentId: context.agent,
513
+ confirm: input.confirm,
514
+ autoIndex: input.autoIndex
515
+ });
516
+ return jsonResult({
517
+ vault: context.vault,
518
+ ...result
519
+ });
520
+ };
521
+ export const volatileAddTool = async (input) => {
522
+ const context = await resolveExecutionContext(input);
523
+ const entry = await addVolatileMemory(context.vault, input.content, context.agent ?? 'shared', input.ttlMinutes ?? 240, input.tags);
524
+ return jsonResult({
525
+ vault: context.vault,
526
+ agent: context.agent,
527
+ volatile: true,
528
+ entry
529
+ });
530
+ };
531
+ export const volatileClearTool = async (input) => {
532
+ const context = await resolveExecutionContext(input);
533
+ const cleared = await clearVolatileMemory(context.vault, context.agent);
534
+ return jsonResult({
535
+ vault: context.vault,
536
+ agent: context.agent,
537
+ cleared
538
+ });
539
+ };
404
540
  export const addFileTool = async (input) => {
405
541
  const context = await resolveExecutionContext(input);
406
542
  const content = await readFile(input.filePath, 'utf8');
@@ -411,7 +547,8 @@ export const addFileTool = async (input) => {
411
547
  }
412
548
  const shouldIndex = isTruthy(input.autoIndex);
413
549
  const added = await addNoteWithMetadata(context.vault, title, content, context.agent, {
414
- allowSensitive: input.allowSensitive
550
+ allowSensitive: input.allowSensitive,
551
+ autoContextLinks: context.config.autoCanonicalContextLinks
415
552
  });
416
553
  const index = shouldIndex ? await indexVault(context.vault) : undefined;
417
554
  return jsonResult({
@@ -423,14 +560,35 @@ export const addFileTool = async (input) => {
423
560
  writeConnectivity: {
424
561
  autoLinked: added.autoLinked,
425
562
  linkTarget: added.linkTarget,
426
- guaranteedEdge: true
563
+ context: added.context,
564
+ hubCreated: added.hubCreated,
565
+ guaranteedEdge: added.autoLinked
427
566
  },
428
567
  ...(index ? { index } : {})
429
568
  });
430
569
  };
570
+ export const canonicalizeContextLinksTool = async (input) => {
571
+ const context = await resolveExecutionContext(input);
572
+ const result = await canonicalizeContextLinks(context.vault, {
573
+ agentId: context.agent,
574
+ dryRun: input.dryRun === true,
575
+ createMissingHubs: input.createHubs !== false
576
+ });
577
+ const index = input.autoIndex !== false && !result.dryRun && result.changed > 0
578
+ ? await indexVault(context.vault, { full: true })
579
+ : undefined;
580
+ return jsonResult({
581
+ vault: context.vault,
582
+ agent: context.agent,
583
+ ...result,
584
+ ...(index ? { index } : {})
585
+ });
586
+ };
431
587
  export const indexTool = async (input) => {
432
588
  const context = await resolveExecutionContext(input);
433
- const result = await indexVault(context.vault);
589
+ const result = await indexVault(context.vault, {
590
+ full: input.full === true
591
+ });
434
592
  return jsonResult({
435
593
  vault: context.vault,
436
594
  ...result
@@ -474,6 +632,25 @@ export const graphTool = async (input) => {
474
632
  ...graph
475
633
  });
476
634
  };
635
+ export const graphContextsTool = async (input) => {
636
+ const context = await resolveExecutionContext(input);
637
+ const readiness = await ensureBootstrapReady(context, input, 'brainlink_graph_contexts');
638
+ if (readiness.preflight) {
639
+ return readiness.preflight;
640
+ }
641
+ const contextReadiness = await ensureContextReady(context, input, 'brainlink_graph_contexts');
642
+ if (contextReadiness.preflight) {
643
+ return contextReadiness.preflight;
644
+ }
645
+ const contexts = await getGraphContexts(context.vault, context.agent);
646
+ return jsonResult({
647
+ vault: context.vault,
648
+ agent: context.agent,
649
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
650
+ ...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
651
+ contexts
652
+ });
653
+ };
477
654
  export const brokenLinksTool = async (input) => {
478
655
  const context = await resolveExecutionContext(input);
479
656
  const readiness = await ensureBootstrapReady(context, input, 'brainlink_broken_links');
@@ -561,14 +738,16 @@ export const syncTool = async (input) => {
561
738
  return jsonResult(response);
562
739
  }
563
740
  const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
741
+ const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
564
742
  const contextLimit = input.contextLimit ?? context.defaults.defaultSearchLimit;
565
743
  const contextTokens = input.contextTokens ?? context.defaults.defaultContextTokens;
566
- const contextPackage = await buildContextPackage(context.vault, input.contextQuery, contextLimit, contextTokens, context.agent, mode);
744
+ const contextPackage = await buildContextPackage(context.vault, input.contextQuery, contextLimit, contextTokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs);
567
745
  const contextSession = await touchContextSession(context.vault, context.agent);
568
746
  return jsonResult({
569
747
  ...response,
570
748
  context: {
571
749
  mode,
750
+ strategy,
572
751
  contextSession,
573
752
  ...contextPackage
574
753
  }
@@ -580,10 +759,11 @@ export const bootstrapTool = async (input) => {
580
759
  const stats = await getStats(context.vault, context.agent);
581
760
  const validation = await validateVault(context.vault, context.agent);
582
761
  const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
762
+ const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
583
763
  const limit = input.limit ?? context.defaults.defaultSearchLimit;
584
764
  const tokens = input.tokens ?? context.defaults.defaultContextTokens;
585
765
  const contextPackage = input.query
586
- ? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode)
766
+ ? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs)
587
767
  : undefined;
588
768
  const contextSession = input.query ? await touchContextSession(context.vault, context.agent) : undefined;
589
769
  const guidance = stats.documentCount === 0
@@ -637,6 +817,7 @@ export const bootstrapTool = async (input) => {
637
817
  ...(context.agent ? { agent: context.agent } : {}),
638
818
  query: '<task>',
639
819
  mode,
820
+ strategy,
640
821
  limit,
641
822
  tokens
642
823
  }
@@ -646,6 +827,7 @@ export const bootstrapTool = async (input) => {
646
827
  vault: context.vault,
647
828
  agent: context.agent,
648
829
  mode,
830
+ strategy,
649
831
  limit,
650
832
  tokens,
651
833
  index,
@@ -701,7 +883,8 @@ export const policyTool = async (input) => {
701
883
  args: {
702
884
  vault: context.vault,
703
885
  ...(context.agent ? { agent: context.agent } : {}),
704
- mode: context.defaults.defaultSearchMode
886
+ mode: context.defaults.defaultSearchMode,
887
+ strategy: context.defaults.defaultContextStrategy
705
888
  }
706
889
  }
707
890
  ];
@@ -716,6 +899,7 @@ export const policyTool = async (input) => {
716
899
  ...(context.agent ? { agent: context.agent } : {}),
717
900
  query: '<task>',
718
901
  mode: context.defaults.defaultSearchMode,
902
+ strategy: context.defaults.defaultContextStrategy,
719
903
  limit: context.defaults.defaultSearchLimit,
720
904
  tokens: context.defaults.defaultContextTokens
721
905
  }
@@ -725,12 +909,21 @@ export const policyTool = async (input) => {
725
909
  return jsonResult(withNextActions({
726
910
  vault: context.vault,
727
911
  agent: context.agent,
912
+ runtime: getRuntimeMetadata(),
728
913
  policy,
729
914
  bootstrapStatus,
730
915
  contextStatus,
731
916
  ...(input.preset ? { presetApplied: input.preset } : {})
732
917
  }, withContextAction));
733
918
  };
919
+ export const versionTool = async (input) => {
920
+ const context = await resolveExecutionContext(input);
921
+ return jsonResult({
922
+ vault: context.vault,
923
+ agent: context.agent,
924
+ runtime: getRuntimeMetadata()
925
+ });
926
+ };
734
927
  export const recommendationsTool = async (input) => {
735
928
  const context = await resolveExecutionContext(input);
736
929
  const policy = await getBootstrapPolicy();
@@ -738,6 +931,7 @@ export const recommendationsTool = async (input) => {
738
931
  const contextStatus = await getContextSessionStatus(context.vault, context.agent);
739
932
  const stats = await getStats(context.vault, context.agent);
740
933
  const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
934
+ const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
741
935
  const limit = input.limit ?? context.defaults.defaultSearchLimit;
742
936
  const tokens = input.tokens ?? context.defaults.defaultContextTokens;
743
937
  const query = input.query?.trim();
@@ -762,6 +956,7 @@ export const recommendationsTool = async (input) => {
762
956
  vault: context.vault,
763
957
  ...(context.agent ? { agent: context.agent } : {}),
764
958
  mode,
959
+ strategy,
765
960
  ...(query ? { query } : {})
766
961
  }
767
962
  }
@@ -777,6 +972,7 @@ export const recommendationsTool = async (input) => {
777
972
  ...(context.agent ? { agent: context.agent } : {}),
778
973
  query: query ?? '<task>',
779
974
  mode,
975
+ strategy,
780
976
  limit,
781
977
  tokens
782
978
  }
@@ -812,6 +1008,7 @@ export const recommendationsTool = async (input) => {
812
1008
  ...(context.agent ? { agent: context.agent } : {}),
813
1009
  query: query ?? '<task>',
814
1010
  mode,
1011
+ strategy,
815
1012
  limit,
816
1013
  tokens
817
1014
  }
@@ -843,9 +1040,24 @@ export const recommendationsTool = async (input) => {
843
1040
  agent: context.agent,
844
1041
  defaults: {
845
1042
  mode,
1043
+ strategy,
846
1044
  limit,
847
1045
  tokens
848
1046
  },
1047
+ contextStrategies: [
1048
+ {
1049
+ strategy: 'rag',
1050
+ useWhen: 'Use for fresh retrieval and context assembly from the current index.'
1051
+ },
1052
+ {
1053
+ strategy: 'cag',
1054
+ useWhen: 'Use for repeated or stable task context so Brainlink can reuse a fresh persisted context pack.'
1055
+ },
1056
+ {
1057
+ strategy: 'auto',
1058
+ useWhen: 'Use when the agent wants Brainlink to choose CAG on fresh pack hits and RAG otherwise.'
1059
+ }
1060
+ ],
849
1061
  policy,
850
1062
  bootstrapStatus,
851
1063
  contextStatus,