@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/README.md CHANGED
@@ -71,6 +71,7 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
71
71
  - Full-text, semantic and hybrid retrieval on a local file index.
72
72
  - Middle-out context assembly around the strongest chunk per document.
73
73
  - In-process index and context caching with automatic invalidation on index updates.
74
+ - Optional CAG context packs at `.brainlink/context-packs/*.json`, derived from the current index signature and reusable across repeated context calls.
74
75
  - HTTP graph server caches generated frontend assets and graph-layout JSON payloads by signature, and skips layout serialization when ETag returns `304`.
75
76
  - Compressed-space prefiltering for `.blpk` packs before decryption and scan.
76
77
  - Incremental indexing that reprocesses only changed markdown files and reuses existing chunks/embeddings for unchanged notes.
@@ -529,8 +530,9 @@ Available tools:
529
530
 
530
531
  - `brainlink_bootstrap`: plug-and-play entrypoint that runs index + health checks and can return context in one call.
531
532
  - `brainlink_policy`: read or update bootstrap/context-first policy, including presets (`preset: "fully-auto" | "strict"`).
532
- - `brainlink_recommendations`: return an automatic action plan so agents can run Brainlink in the recommended order.
533
- - `brainlink_context`: read indexed context for a task or question.
533
+ - `brainlink_recommendations`: return an automatic action plan so agents can run Brainlink in the recommended order, including RAG/CAG context strategy guidance.
534
+ - `brainlink_context`: read indexed context for a task or question; pass `strategy: "rag"` for fresh retrieval assembly, `strategy: "cag"` for persisted context packs or `strategy: "auto"` for CAG hits with RAG fallback.
535
+ - `brainlink_context_packs`: list or clear persisted CAG context packs.
534
536
  - `brainlink_search`: search indexed notes.
535
537
  - `brainlink_dedupe`: detect duplicate candidates using exact hash + semantic similarity scores.
536
538
  - `brainlink_resolve_duplicate`: resolve duplicate pairs (`merge`, `link`, `ignore`) with connectivity-safe fallback edges.
@@ -555,6 +557,7 @@ By default, Brainlink enforces bootstrap and auto-runs it for read tools when se
555
557
  If you disable `autoBootstrapOnRead` through `brainlink_policy`, read tools return a preflight instruction with suggested `brainlink_bootstrap` arguments.
556
558
  `brainlink_bootstrap`, `brainlink_policy` and preflight responses include structured `nextActions` so MCP clients can continue automatically without custom parsing.
557
559
  For one-call planning, use `brainlink_recommendations` to get the recommended tool sequence for the current vault/agent/query.
560
+ The MCP context tools are plug-and-play by default: omit `strategy` to use the configured default (`rag` unless changed), pass `strategy: "cag"` for repeated/stable task context, or pass `strategy: "auto"` so Brainlink chooses CAG on fresh pack hits and RAG otherwise. `brainlink_recommendations`, preflight responses and policy next actions include executable context arguments so clients can continue without custom parsing.
558
561
 
559
562
  The same linking rule applies through MCP: `brainlink_context` is read-only, and real graph links require Markdown notes with explicit `## Context Links` sections. `brainlink_add_note` and `brainlink_add_file` reindex by default and include index + `writeConnectivity` metadata. Brainlink does not auto-link new notes to fallback hubs.
560
563
 
@@ -864,10 +867,15 @@ Context selection uses a middle-out strategy: it starts from the strongest chunk
864
867
  blink context "question" --vault ./vault --limit 12 --tokens 2000
865
868
  blink context "question" --vault ./vault --agent coding-agent --json
866
869
  blink context "question" --vault ./vault --agent coding-agent --mode hybrid --json
870
+ blink context "question" --vault ./vault --agent coding-agent --strategy cag --json
871
+ blink context "question" --vault ./vault --agent coding-agent --strategy auto --json
872
+ blink context-packs --vault ./vault --json
873
+ blink context-packs --vault ./vault --stale --clear
867
874
  ```
868
875
 
869
876
  Builds a compact context package for an agent.
870
877
  Repeated calls with the same vault, agent, query, mode and token/limit settings are served from a short in-memory cache while the index is unchanged.
878
+ The default strategy is configured by `defaultContextStrategy` and starts as `rag`, which retrieves and assembles context from the current index. `--strategy cag` enables cache-augmented context generation by reading or refreshing a persisted context pack under `.brainlink/context-packs`; `--strategy auto` uses CAG when a fresh pack exists and RAG otherwise, refreshing a pack for future calls. Context responses include `cache`, `metrics`, `requestedStrategy` and `recommendedStrategy` metadata. Packs are derived artifacts and become stale when the index or volatile memory signature changes.
871
879
 
872
880
  ### `links`
873
881
 
@@ -1010,6 +1018,7 @@ If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HO
1010
1018
  "autoCanonicalContextLinks": true,
1011
1019
  "defaultSearchLimit": 10,
1012
1020
  "defaultContextTokens": 2000,
1021
+ "defaultContextStrategy": "rag",
1013
1022
  "embeddingProvider": "local",
1014
1023
  "defaultSearchMode": "hybrid",
1015
1024
  "chunkSize": 1200,
@@ -1024,7 +1033,8 @@ If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HO
1024
1033
  "coding-agent": {
1025
1034
  "defaultSearchMode": "semantic",
1026
1035
  "defaultSearchLimit": 8,
1027
- "defaultContextTokens": 2400
1036
+ "defaultContextTokens": 2400,
1037
+ "defaultContextStrategy": "auto"
1028
1038
  },
1029
1039
  "*": {
1030
1040
  "defaultSearchMode": "hybrid"
@@ -1034,7 +1044,7 @@ If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HO
1034
1044
  ```
1035
1045
 
1036
1046
  `defaultAgent` is optional. When set, CLI and MCP calls that omit `--agent`/`agent` use this value automatically. If not set, behavior remains as before.
1037
- `agentProfiles` is optional. When present, CLI and MCP resolve `mode`, `limit` and `tokens` per agent automatically, then fallback to global defaults.
1047
+ `agentProfiles` is optional. When present, CLI and MCP resolve `mode`, `limit`, `tokens` and context `strategy` per agent automatically, then fallback to global defaults.
1038
1048
 
1039
1049
  `autoIndexOnWrite` is optional and defaults to `true`. Set it to `false` to defer indexing after writes.
1040
1050
  `autoCanonicalContextLinks` is optional and defaults to `true`. When enabled, `blink add`, `brainlink_add_note` and `brainlink_add_file` add a canonical `## Context Links` entry to the inferred context hub, creating that hub when needed.
@@ -1,5 +1,7 @@
1
1
  import { stat } from 'node:fs/promises';
2
+ import { performance } from 'node:perf_hooks';
2
3
  import { formatContextPackage, selectContextSections } from '../domain/context.js';
4
+ import { readContextPack, writeContextPack } from '../infrastructure/context-packs.js';
3
5
  import { indexStoragePath } from '../infrastructure/file-index.js';
4
6
  import { searchVolatileMemory, volatileMemoryStoragePath } from '../infrastructure/volatile-memory.js';
5
7
  import { searchKnowledge } from './search-knowledge.js';
@@ -15,14 +17,19 @@ const readFileSignature = async (path) => {
15
17
  return '0:0';
16
18
  }
17
19
  };
18
- const readContextDataSignature = async (vaultPath) => `${await readFileSignature(indexStoragePath(vaultPath))}|${await readFileSignature(volatileMemoryStoragePath(vaultPath))}`;
19
- const toCacheKey = (vaultPath, query, limit, maxTokens, agentId, mode) => JSON.stringify({
20
+ export const readContextDataSignature = async (vaultPath) => `${await readFileSignature(indexStoragePath(vaultPath))}|${await readFileSignature(volatileMemoryStoragePath(vaultPath))}`;
21
+ const toCacheKey = (vaultPath, query, limit, maxTokens, agentId, mode, strategy) => JSON.stringify({
20
22
  vaultPath,
21
23
  query: query.trim().toLowerCase(),
22
24
  limit,
23
25
  maxTokens,
24
26
  agentId: agentId?.trim().toLowerCase() ?? '*',
25
- mode: mode ?? 'default'
27
+ mode: mode ?? 'default',
28
+ strategy
29
+ });
30
+ const withCacheMetadata = (context, cache) => ({
31
+ ...context,
32
+ cache
26
33
  });
27
34
  const contextCacheGet = (key, dataSignature) => {
28
35
  const entry = contextCache.get(key);
@@ -34,7 +41,11 @@ const contextCacheGet = (key, dataSignature) => {
34
41
  contextCache.delete(key);
35
42
  return undefined;
36
43
  }
37
- return entry.context;
44
+ return withCacheMetadata(entry.context, {
45
+ storage: 'memory',
46
+ status: 'hit',
47
+ dataSignature
48
+ });
38
49
  };
39
50
  const contextCacheSet = (entry) => {
40
51
  contextCache.set(entry.key, entry);
@@ -45,32 +56,115 @@ const contextCacheSet = (entry) => {
45
56
  const keys = Array.from(contextCache.keys()).slice(0, overflow);
46
57
  keys.forEach((key) => contextCache.delete(key));
47
58
  };
48
- export const buildContextPackage = async (vaultPath, query, limit, maxTokens, agentId, mode) => {
49
- const cacheKey = toCacheKey(vaultPath, query, limit, maxTokens, agentId, mode);
59
+ const elapsedMs = (start) => Math.round((performance.now() - start) * 1000) / 1000;
60
+ const estimateSectionTokens = (context) => Math.ceil(context.content.length / 4);
61
+ const emptyMetrics = (context, totalMs, overrides = {}) => ({
62
+ totalMs,
63
+ packReadMs: 0,
64
+ searchMs: 0,
65
+ selectionMs: 0,
66
+ volatileMs: 0,
67
+ packWriteMs: 0,
68
+ sectionCount: context.sections.length,
69
+ volatileSectionCount: context.volatileSections?.length ?? 0,
70
+ estimatedTokens: estimateSectionTokens(context),
71
+ ...overrides
72
+ });
73
+ export const buildContextPackage = async (vaultPath, query, limit, maxTokens, agentId, mode, strategy = 'rag') => {
74
+ const totalStart = performance.now();
75
+ const cacheKey = toCacheKey(vaultPath, query, limit, maxTokens, agentId, mode, strategy);
50
76
  const dataSignature = await readContextDataSignature(vaultPath);
51
77
  const cached = contextCacheGet(cacheKey, dataSignature);
52
78
  if (cached) {
53
79
  return cached;
54
80
  }
81
+ const shouldUseContextPack = strategy === 'cag' || strategy === 'auto';
82
+ let packReadMs = 0;
83
+ if (shouldUseContextPack) {
84
+ const packReadStart = performance.now();
85
+ const pack = await readContextPack(vaultPath, { query, limit, maxTokens, agentId, mode }, dataSignature);
86
+ packReadMs = elapsedMs(packReadStart);
87
+ if (pack.status === 'hit') {
88
+ const contextFromPack = {
89
+ ...pack.context,
90
+ strategy: 'cag',
91
+ requestedStrategy: strategy,
92
+ recommendedStrategy: {
93
+ strategy: 'cag',
94
+ reason: strategy === 'auto'
95
+ ? 'A fresh context pack exists, so auto selected CAG.'
96
+ : 'The requested CAG context pack is fresh.'
97
+ },
98
+ metrics: emptyMetrics(pack.context, elapsedMs(totalStart), { packReadMs })
99
+ };
100
+ contextCacheSet({
101
+ key: cacheKey,
102
+ createdAt: Date.now(),
103
+ dataSignature,
104
+ strategy,
105
+ context: contextFromPack
106
+ });
107
+ return contextFromPack;
108
+ }
109
+ }
110
+ const searchStart = performance.now();
55
111
  const results = await searchKnowledge(vaultPath, query, limit, agentId, mode);
112
+ const searchMs = elapsedMs(searchStart);
113
+ const selectionStart = performance.now();
56
114
  const durableSections = selectContextSections(results, maxTokens);
115
+ const selectionMs = elapsedMs(selectionStart);
116
+ const volatileStart = performance.now();
57
117
  const volatileSections = await searchVolatileMemory(vaultPath, query, Math.min(3, limit), agentId, mode ?? 'hybrid');
118
+ const volatileMs = elapsedMs(volatileStart);
58
119
  const sections = [...volatileSections, ...durableSections];
120
+ const effectiveStrategy = strategy === 'cag' ? 'cag' : 'rag';
59
121
  const context = {
60
122
  query,
61
123
  sections,
62
124
  content: formatContextPackage(query, sections),
63
- ...(volatileSections.length > 0 ? { volatileSections } : {})
125
+ ...(volatileSections.length > 0 ? { volatileSections } : {}),
126
+ strategy: effectiveStrategy,
127
+ requestedStrategy: strategy,
128
+ recommendedStrategy: {
129
+ strategy: effectiveStrategy,
130
+ reason: strategy === 'auto'
131
+ ? 'No fresh context pack was available, so auto selected fresh RAG assembly and refreshed a pack for future reuse.'
132
+ : effectiveStrategy === 'cag'
133
+ ? 'CAG was requested; Brainlink refreshed the context pack from current retrieval results.'
134
+ : 'RAG was requested; Brainlink assembled context directly from current retrieval results.'
135
+ }
136
+ };
137
+ const packWriteStart = performance.now();
138
+ const packPath = shouldUseContextPack
139
+ ? await writeContextPack(vaultPath, { query, limit, maxTokens, agentId, mode }, dataSignature, context)
140
+ : undefined;
141
+ const packWriteMs = packPath ? elapsedMs(packWriteStart) : 0;
142
+ const contextWithMetadata = withCacheMetadata(context, {
143
+ storage: shouldUseContextPack ? 'context-pack' : 'memory',
144
+ status: shouldUseContextPack ? 'refresh' : 'miss',
145
+ dataSignature,
146
+ ...(packPath ? { path: packPath } : {})
147
+ });
148
+ const contextWithMetrics = {
149
+ ...contextWithMetadata,
150
+ metrics: emptyMetrics(contextWithMetadata, elapsedMs(totalStart), {
151
+ packReadMs,
152
+ searchMs,
153
+ selectionMs,
154
+ volatileMs,
155
+ packWriteMs
156
+ })
64
157
  };
65
158
  contextCacheSet({
66
159
  key: cacheKey,
67
160
  createdAt: Date.now(),
68
161
  dataSignature,
69
- context
162
+ strategy,
163
+ context: contextWithMetrics
70
164
  });
71
- return context;
165
+ return contextWithMetrics;
72
166
  };
73
- export const buildContext = async (vaultPath, query, limit, maxTokens, agentId, mode) => {
74
- const contextPackage = await buildContextPackage(vaultPath, query, limit, maxTokens, agentId, mode);
167
+ export const buildContext = async (vaultPath, query, limit, maxTokens, agentId, mode, strategy) => {
168
+ const contextPackage = await buildContextPackage(vaultPath, query, limit, maxTokens, agentId, mode, strategy);
75
169
  return contextPackage.content;
76
170
  };
@@ -1,14 +1,14 @@
1
1
  export const createClientCss = () => `:root {
2
- color-scheme: light;
3
- --bg: #eef2f7;
4
- --panel: #ffffff;
5
- --panel-strong: #f7f9fc;
6
- --line: #d9e1ec;
7
- --text: #172033;
8
- --muted: #657386;
9
- --accent: #0b6fcb;
10
- --accent-weak: rgba(11, 111, 203, 0.12);
11
- --danger: #d84949;
2
+ color-scheme: dark;
3
+ --bg: #071019;
4
+ --panel: #0d1823;
5
+ --panel-strong: #112130;
6
+ --line: rgba(143, 172, 204, 0.18);
7
+ --text: #edf4ff;
8
+ --muted: #97a9bd;
9
+ --accent: #5aa8ff;
10
+ --accent-weak: rgba(90, 168, 255, 0.18);
11
+ --danger: #ff6b6b;
12
12
  }
13
13
 
14
14
  * {
@@ -63,8 +63,8 @@ select {
63
63
  min-height: 72px;
64
64
  padding: 10px 16px;
65
65
  border-bottom: 1px solid var(--line);
66
- background: rgba(255, 255, 255, 0.96);
67
- box-shadow: 0 1px 8px rgba(23, 32, 51, 0.08);
66
+ background: rgba(9, 18, 28, 0.92);
67
+ box-shadow: 0 1px 14px rgba(1, 6, 13, 0.42);
68
68
  backdrop-filter: blur(8px);
69
69
  }
70
70
 
@@ -82,10 +82,14 @@ select {
82
82
  position: relative;
83
83
  width: 100%;
84
84
  height: 100%;
85
+ touch-action: none;
86
+ user-select: none;
87
+ -webkit-user-select: none;
85
88
  background:
86
- linear-gradient(rgba(23, 32, 51, 0.035) 1px, transparent 1px),
87
- linear-gradient(90deg, rgba(23, 32, 51, 0.035) 1px, transparent 1px),
88
- #f6f8fb;
89
+ linear-gradient(rgba(164, 197, 230, 0.05) 1px, transparent 1px),
90
+ linear-gradient(90deg, rgba(164, 197, 230, 0.05) 1px, transparent 1px),
91
+ radial-gradient(circle at top, rgba(43, 93, 143, 0.22), transparent 44%),
92
+ #08131d;
89
93
  background-size: 28px 28px, 28px 28px, auto;
90
94
  overflow: hidden;
91
95
  }
@@ -96,6 +100,7 @@ select {
96
100
  inset: 0;
97
101
  width: 100%;
98
102
  height: 100%;
103
+ touch-action: none;
99
104
  }
100
105
 
101
106
  #graph {
@@ -131,21 +136,21 @@ select {
131
136
  max-width: 220px;
132
137
  transform: translate(-50%, calc(-100% - 12px));
133
138
  padding: 4px 8px;
134
- border: 1px solid rgba(101, 115, 134, 0.24);
139
+ border: 1px solid rgba(143, 172, 204, 0.18);
135
140
  border-radius: 6px;
136
- background: rgba(255, 255, 255, 0.92);
141
+ background: rgba(7, 16, 25, 0.92);
137
142
  color: var(--text);
138
143
  font-size: 11px;
139
144
  line-height: 1.25;
140
145
  white-space: nowrap;
141
146
  overflow: hidden;
142
147
  text-overflow: ellipsis;
143
- box-shadow: 0 8px 22px rgba(23, 32, 51, 0.12);
148
+ box-shadow: 0 12px 28px rgba(1, 6, 13, 0.36);
144
149
  }
145
150
 
146
151
  .graph-label.is-focused {
147
- border-color: rgba(11, 111, 203, 0.56);
148
- color: #0b4f92;
152
+ border-color: rgba(90, 168, 255, 0.56);
153
+ color: #d7e9ff;
149
154
  }
150
155
 
151
156
  .graph-tooltip {
@@ -155,12 +160,12 @@ select {
155
160
  padding: 8px 10px;
156
161
  border: 1px solid var(--line);
157
162
  border-radius: 6px;
158
- background: rgba(255, 255, 255, 0.96);
163
+ background: rgba(9, 18, 28, 0.96);
159
164
  color: var(--text);
160
165
  font-size: 12px;
161
166
  line-height: 1.35;
162
167
  pointer-events: none;
163
- box-shadow: 0 16px 40px rgba(23, 32, 51, 0.18);
168
+ box-shadow: 0 18px 44px rgba(1, 6, 13, 0.42);
164
169
  }
165
170
 
166
171
  .graph-tooltip strong,
@@ -182,10 +187,10 @@ select {
182
187
  z-index: 3;
183
188
  width: 180px;
184
189
  height: 120px;
185
- border: 1px solid rgba(129, 146, 170, 0.28);
190
+ border: 1px solid rgba(143, 172, 204, 0.18);
186
191
  border-radius: 8px;
187
- background: rgba(255, 255, 255, 0.88);
188
- box-shadow: 0 16px 42px rgba(23, 32, 51, 0.16);
192
+ background: rgba(8, 19, 29, 0.84);
193
+ box-shadow: 0 18px 40px rgba(1, 6, 13, 0.36);
189
194
  }
190
195
 
191
196
  .eyebrow {
@@ -218,7 +223,7 @@ select {
218
223
  border: 1px solid var(--line);
219
224
  border-radius: 8px;
220
225
  outline: none;
221
- background: rgba(255, 255, 255, 0.94);
226
+ background: rgba(12, 24, 36, 0.94);
222
227
  color: var(--text);
223
228
  padding: 0 14px;
224
229
  }
@@ -239,7 +244,7 @@ select {
239
244
  height: 38px;
240
245
  border: 1px solid var(--line);
241
246
  border-radius: 8px;
242
- background: rgba(255, 255, 255, 0.94);
247
+ background: rgba(12, 24, 36, 0.94);
243
248
  color: var(--text);
244
249
  cursor: pointer;
245
250
  }
@@ -260,7 +265,7 @@ select {
260
265
  padding: 10px 12px;
261
266
  border: 1px solid var(--line);
262
267
  border-radius: 10px;
263
- background: rgba(255, 255, 255, 0.94);
268
+ background: rgba(12, 24, 36, 0.94);
264
269
  display: grid;
265
270
  gap: 3px;
266
271
  }
@@ -335,7 +340,7 @@ li small {
335
340
  padding: 12px;
336
341
  border: 1px solid var(--line);
337
342
  border-radius: 8px;
338
- background: #f8fafc;
343
+ background: #091521;
339
344
  color: var(--text);
340
345
  white-space: pre-wrap;
341
346
  overflow: auto;
@@ -366,6 +371,7 @@ li small {
366
371
  background: var(--panel);
367
372
  color: var(--text);
368
373
  box-shadow: 0 24px 80px rgba(23, 32, 51, 0.22);
374
+ backdrop-filter: blur(10px);
369
375
  overflow: hidden;
370
376
  }
371
377
 
@@ -469,7 +475,7 @@ li small {
469
475
  padding: 10px;
470
476
  border: 1px solid var(--line);
471
477
  border-radius: 8px;
472
- background: #f8fafc;
478
+ background: #091521;
473
479
  display: grid;
474
480
  grid-template-rows: auto minmax(0, 1fr);
475
481
  gap: 8px;
@@ -509,7 +515,7 @@ li small {
509
515
  display: flex;
510
516
  align-items: center;
511
517
  justify-content: center;
512
- background: transparent;
518
+ background: linear-gradient(180deg, rgba(7, 16, 25, 0), rgba(7, 16, 25, 0.84));
513
519
  }
514
520
 
515
521
  .app-footer small {