@andespindola/brainlink 0.1.0-beta.163 → 0.1.0-beta.165

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/dist/mcp/tools.js CHANGED
@@ -3,14 +3,15 @@ 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
7
  import { canonicalizeContextLinks } from '../application/canonical-context-links.js';
8
8
  import { resolveDuplicateNotes, scanDuplicateNotes } from '../application/dedupe-notes.js';
9
9
  import { getGraph } from '../application/get-graph.js';
10
10
  import { getGraphContexts } from '../application/get-graph-contexts.js';
11
11
  import { indexVault } from '../application/index-vault.js';
12
12
  import { searchKnowledge } from '../application/search-knowledge.js';
13
- 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';
14
15
  import { loadBrainlinkConfig } from '../infrastructure/config.js';
15
16
  import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
16
17
  import { addVolatileMemory, clearVolatileMemory } from '../infrastructure/volatile-memory.js';
@@ -40,6 +41,12 @@ const agentInput = {
40
41
  const searchModeInput = {
41
42
  mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode. Defaults to the Brainlink config value.')
42
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
+ };
43
50
  const resolveExecutionContext = async (input) => {
44
51
  const config = await loadBrainlinkConfig();
45
52
  const vault = await assertVaultAllowed(input.vault ?? config.vault, config.allowedVaults);
@@ -122,12 +129,14 @@ const ensureBootstrapReady = async (context, input, toolName) => {
122
129
  };
123
130
  }
124
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');
125
133
  const query = typeof input.query === 'string' && input.query.trim().length > 0 ? input.query : undefined;
126
134
  const bootstrapArgs = {
127
135
  vault: context.vault,
128
136
  ...(context.agent ? { agent: context.agent } : {}),
129
137
  ...(query ? { query } : {}),
130
- mode
138
+ mode,
139
+ strategy
131
140
  };
132
141
  const nextActions = [
133
142
  {
@@ -178,6 +187,7 @@ const ensureContextReady = async (context, input, toolName) => {
178
187
  ? input.contextQuery
179
188
  : '<task>';
180
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);
181
191
  const limit = typeof input.limit === 'number' && Number.isFinite(input.limit) && input.limit > 0
182
192
  ? input.limit
183
193
  : typeof input.contextLimit === 'number' && Number.isFinite(input.contextLimit) && input.contextLimit > 0
@@ -193,6 +203,7 @@ const ensureContextReady = async (context, input, toolName) => {
193
203
  ...(context.agent ? { agent: context.agent } : {}),
194
204
  query: queryFromInput,
195
205
  mode,
206
+ strategy,
196
207
  limit,
197
208
  tokens
198
209
  };
@@ -219,10 +230,17 @@ export const contextInputSchema = {
219
230
  ...vaultInput,
220
231
  ...agentInput,
221
232
  ...searchModeInput,
233
+ ...contextStrategyInput,
222
234
  query: z.string().min(1).describe('Task or question to retrieve Brainlink context for.'),
223
235
  limit: optionalPositiveInteger().describe('Maximum search results before context selection.'),
224
236
  tokens: optionalPositiveInteger().describe('Maximum estimated context tokens.')
225
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
+ };
226
244
  export const searchInputSchema = {
227
245
  ...vaultInput,
228
246
  ...agentInput,
@@ -311,6 +329,7 @@ export const syncInputSchema = {
311
329
  ...agentInput,
312
330
  contextQuery: z.string().min(1).optional().describe('Optional context smoke query. Omit to skip context probe.'),
313
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.'),
314
333
  contextLimit: optionalPositiveInteger().describe('Context smoke result limit when contextQuery is provided.'),
315
334
  contextTokens: optionalPositiveInteger().describe('Context smoke token target when contextQuery is provided.')
316
335
  };
@@ -318,6 +337,7 @@ export const bootstrapInputSchema = {
318
337
  ...vaultInput,
319
338
  ...agentInput,
320
339
  ...searchModeInput,
340
+ ...contextStrategyInput,
321
341
  query: z
322
342
  .string()
323
343
  .min(1)
@@ -350,6 +370,7 @@ export const recommendationsInputSchema = {
350
370
  ...vaultInput,
351
371
  ...agentInput,
352
372
  ...searchModeInput,
373
+ ...contextStrategyInput,
353
374
  query: z.string().min(1).optional().describe('Optional current task query to generate context-focused recommendations.'),
354
375
  limit: optionalPositiveInteger().describe('Optional context limit override for generated recommendations.'),
355
376
  tokens: optionalPositiveInteger().describe('Optional context token budget override for generated recommendations.')
@@ -375,14 +396,16 @@ export const contextTool = async (input) => {
375
396
  return readiness.preflight;
376
397
  }
377
398
  const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
399
+ const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
378
400
  const limit = input.limit ?? context.defaults.defaultSearchLimit;
379
401
  const tokens = input.tokens ?? context.defaults.defaultContextTokens;
380
- 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);
381
403
  const contextSession = await touchContextSession(context.vault, context.agent);
382
404
  return jsonResult({
383
405
  vault: context.vault,
384
406
  agent: context.agent,
385
407
  mode,
408
+ strategy,
386
409
  limit,
387
410
  tokens,
388
411
  contextSession,
@@ -390,6 +413,32 @@ export const contextTool = async (input) => {
390
413
  ...contextPackage
391
414
  });
392
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
+ };
393
442
  export const searchTool = async (input) => {
394
443
  const context = await resolveExecutionContext(input);
395
444
  const readiness = await ensureBootstrapReady(context, input, 'brainlink_search');
@@ -666,14 +715,16 @@ export const syncTool = async (input) => {
666
715
  return jsonResult(response);
667
716
  }
668
717
  const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
718
+ const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
669
719
  const contextLimit = input.contextLimit ?? context.defaults.defaultSearchLimit;
670
720
  const contextTokens = input.contextTokens ?? context.defaults.defaultContextTokens;
671
- 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);
672
722
  const contextSession = await touchContextSession(context.vault, context.agent);
673
723
  return jsonResult({
674
724
  ...response,
675
725
  context: {
676
726
  mode,
727
+ strategy,
677
728
  contextSession,
678
729
  ...contextPackage
679
730
  }
@@ -685,10 +736,11 @@ export const bootstrapTool = async (input) => {
685
736
  const stats = await getStats(context.vault, context.agent);
686
737
  const validation = await validateVault(context.vault, context.agent);
687
738
  const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
739
+ const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
688
740
  const limit = input.limit ?? context.defaults.defaultSearchLimit;
689
741
  const tokens = input.tokens ?? context.defaults.defaultContextTokens;
690
742
  const contextPackage = input.query
691
- ? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode)
743
+ ? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode, strategy)
692
744
  : undefined;
693
745
  const contextSession = input.query ? await touchContextSession(context.vault, context.agent) : undefined;
694
746
  const guidance = stats.documentCount === 0
@@ -742,6 +794,7 @@ export const bootstrapTool = async (input) => {
742
794
  ...(context.agent ? { agent: context.agent } : {}),
743
795
  query: '<task>',
744
796
  mode,
797
+ strategy,
745
798
  limit,
746
799
  tokens
747
800
  }
@@ -751,6 +804,7 @@ export const bootstrapTool = async (input) => {
751
804
  vault: context.vault,
752
805
  agent: context.agent,
753
806
  mode,
807
+ strategy,
754
808
  limit,
755
809
  tokens,
756
810
  index,
@@ -806,7 +860,8 @@ export const policyTool = async (input) => {
806
860
  args: {
807
861
  vault: context.vault,
808
862
  ...(context.agent ? { agent: context.agent } : {}),
809
- mode: context.defaults.defaultSearchMode
863
+ mode: context.defaults.defaultSearchMode,
864
+ strategy: context.defaults.defaultContextStrategy
810
865
  }
811
866
  }
812
867
  ];
@@ -821,6 +876,7 @@ export const policyTool = async (input) => {
821
876
  ...(context.agent ? { agent: context.agent } : {}),
822
877
  query: '<task>',
823
878
  mode: context.defaults.defaultSearchMode,
879
+ strategy: context.defaults.defaultContextStrategy,
824
880
  limit: context.defaults.defaultSearchLimit,
825
881
  tokens: context.defaults.defaultContextTokens
826
882
  }
@@ -852,6 +908,7 @@ export const recommendationsTool = async (input) => {
852
908
  const contextStatus = await getContextSessionStatus(context.vault, context.agent);
853
909
  const stats = await getStats(context.vault, context.agent);
854
910
  const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
911
+ const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
855
912
  const limit = input.limit ?? context.defaults.defaultSearchLimit;
856
913
  const tokens = input.tokens ?? context.defaults.defaultContextTokens;
857
914
  const query = input.query?.trim();
@@ -876,6 +933,7 @@ export const recommendationsTool = async (input) => {
876
933
  vault: context.vault,
877
934
  ...(context.agent ? { agent: context.agent } : {}),
878
935
  mode,
936
+ strategy,
879
937
  ...(query ? { query } : {})
880
938
  }
881
939
  }
@@ -891,6 +949,7 @@ export const recommendationsTool = async (input) => {
891
949
  ...(context.agent ? { agent: context.agent } : {}),
892
950
  query: query ?? '<task>',
893
951
  mode,
952
+ strategy,
894
953
  limit,
895
954
  tokens
896
955
  }
@@ -926,6 +985,7 @@ export const recommendationsTool = async (input) => {
926
985
  ...(context.agent ? { agent: context.agent } : {}),
927
986
  query: query ?? '<task>',
928
987
  mode,
988
+ strategy,
929
989
  limit,
930
990
  tokens
931
991
  }
@@ -957,9 +1017,24 @@ export const recommendationsTool = async (input) => {
957
1017
  agent: context.agent,
958
1018
  defaults: {
959
1019
  mode,
1020
+ strategy,
960
1021
  limit,
961
1022
  tokens
962
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
+ ],
963
1038
  policy,
964
1039
  bootstrapStatus,
965
1040
  contextStatus,
@@ -51,7 +51,7 @@ Set `BRAINLINK_HOME` when the whole Brainlink home directory should live somewhe
51
51
  Use `blink config where` and `blink config doctor` to inspect active paths and effective source.
52
52
 
53
53
  You can also set `defaultAgent` in `brainlink.config.json` / `.brainlink.json` (for example `"defaultAgent": "coding-agent"`). When set, CLI commands and MCP calls reuse it when `--agent`/`agent` is not passed.
54
- You can set `agentProfiles` to define per-agent defaults for `defaultSearchMode`, `defaultSearchLimit` and `defaultContextTokens`.
54
+ You can set `agentProfiles` to define per-agent defaults for `defaultSearchMode`, `defaultSearchLimit`, `defaultContextTokens` and `defaultContextStrategy`.
55
55
  You can tune search-pack compression with `searchPack.rowChunkSize`, `searchPack.compressionLevel` and `searchPack.useDictionary`.
56
56
  Guardrails for benchmark acceptance are configured with `searchPack.guardrailMinSavingsPercent` and `searchPack.guardrailMaxLatencyRegressionPercent`.
57
57
 
@@ -542,9 +542,14 @@ blink context "how does authentication work?" --vault ./vault --limit 12 --token
542
542
  blink context "how does authentication work?" --vault ./vault --json
543
543
  blink context "how does authentication work?" --vault ./vault --agent coding-agent --json
544
544
  blink context "how does authentication work?" --vault ./vault --agent coding-agent --mode hybrid --json
545
+ blink context "how does authentication work?" --vault ./vault --agent coding-agent --strategy cag --json
546
+ blink context "how does authentication work?" --vault ./vault --agent coding-agent --strategy auto --json
547
+ blink context-packs --vault ./vault --json
548
+ blink context-packs --vault ./vault --stale --clear
545
549
  ```
546
550
 
547
551
  This returns a Markdown context package optimized for prompt injection.
552
+ The default strategy is configured by `defaultContextStrategy` and starts as `rag`. Use `--strategy cag` when the same agent/task context should be served from a persisted context pack when the index and volatile-memory signatures are unchanged. Use `--strategy auto` when Brainlink should use CAG on fresh pack hits and RAG otherwise. Context responses include `cache`, `metrics`, `requestedStrategy` and `recommendedStrategy` metadata. CAG packs live under `.brainlink/context-packs` and are rebuildable derived artifacts.
548
553
 
549
554
  ### Inspect Links
550
555
 
@@ -670,6 +675,7 @@ Available MCP tools:
670
675
  - `brainlink_policy`
671
676
  - `brainlink_recommendations`
672
677
  - `brainlink_context`
678
+ - `brainlink_context_packs`
673
679
  - `brainlink_search`
674
680
  - `brainlink_dedupe`
675
681
  - `brainlink_resolve_duplicate`
@@ -693,6 +699,7 @@ If `autoBootstrapOnRead` or `enforceContextFirst` are disabled through `brainlin
693
699
  `brainlink_bootstrap`, `brainlink_policy` and preflight responses include structured `nextActions` so clients can continue tool flows automatically.
694
700
  `brainlink_policy` also accepts policy presets (`fully-auto`, `strict`) so MCP clients can switch behavior in one call.
695
701
  `brainlink_recommendations` returns the suggested execution order so an agent can follow Brainlink best practices automatically.
702
+ MCP context is plug-and-play: agents may omit `strategy` for the configured default, pass `strategy: "rag"` for explicit fresh retrieval assembly, pass `strategy: "cag"` to reuse persisted context packs when the task context is stable or repeated, or pass `strategy: "auto"` to use CAG on fresh pack hits and RAG otherwise. `brainlink_recommendations`, policy next actions and preflight responses return executable context arguments, so clients can continue without custom parsing. `brainlink_context_packs` lists or clears persisted CAG packs.
696
703
 
697
704
  MCP clients can pass `vault` and `agent` arguments per tool call. Set `BRAINLINK_ALLOWED_VAULTS` when exposing Brainlink to an external agent process so a tool cannot pass arbitrary vault paths:
698
705
 
@@ -136,6 +136,8 @@ read markdown files
136
136
 
137
137
  ```txt
138
138
  question
139
+ -> selected strategy: rag | cag | auto
140
+ -> if cag/auto and fresh context pack exists, return cached Markdown package
139
141
  -> selected mode: fts | semantic | hybrid
140
142
  -> optional query embedding
141
143
  -> optional compressed pack prefilter (token bloom)
@@ -145,8 +147,12 @@ question
145
147
  -> ranked chunks with textScore and semanticScore
146
148
  -> token-budget selection
147
149
  -> Markdown context package
150
+ -> if cag/auto, persist derived context pack with current data signature
151
+ -> return cache, strategy and timing metrics
148
152
  ```
149
153
 
154
+ Context packs are stored under `.brainlink/context-packs/*.json`. They are derived artifacts keyed by query, agent, mode, limit and token budget, and are invalidated by the combined index and volatile-memory signature. Markdown remains the source of truth. The `auto` strategy uses CAG on fresh pack hits and falls back to RAG assembly otherwise, refreshing a pack for later calls.
155
+
150
156
  ## Graph Server Flow
151
157
 
152
158
  ```txt
@@ -50,7 +50,8 @@ Optional per-agent retrieval defaults in `brainlink.config.json`:
50
50
  "coding-agent": {
51
51
  "defaultSearchMode": "semantic",
52
52
  "defaultSearchLimit": 8,
53
- "defaultContextTokens": 2400
53
+ "defaultContextTokens": 2400,
54
+ "defaultContextStrategy": "auto"
54
55
  }
55
56
  }
56
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.163",
3
+ "version": "0.1.0-beta.165",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",