@andespindola/brainlink 0.1.0-beta.150 → 0.1.0-beta.152

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/AGENTS.md CHANGED
@@ -6,7 +6,7 @@ This file tells coding agents and AI assistants how to use this repository.
6
6
 
7
7
  Brainlink is a local-first knowledge memory for agents.
8
8
 
9
- It reads a Markdown vault, extracts `[[wiki links]]` and `#tags`, builds a local file index at `.brainlink/index.json`, and returns compact context packages that agents can inject into prompts.
9
+ It reads a Markdown vault, extracts concise graph links from `## Context Links`, extracts `#tags`, builds a local file index at `.brainlink/index.json`, and returns compact context packages that agents can inject into prompts.
10
10
 
11
11
  ## Source Of Truth
12
12
 
@@ -27,15 +27,15 @@ By default, the installed Brainlink CLI uses `$HOME/.brainlink/vault` as its vau
27
27
  Use this loop when using Brainlink as memory:
28
28
 
29
29
  1. Write durable knowledge into Markdown notes.
30
- 2. Link related notes with explicit `[[Note Title]]` wiki links inside the note body.
30
+ 2. Link related notes with explicit `[[Note Title]]` wiki links inside a `## Context Links` section.
31
31
  3. Add explicit `#tags` for retrieval.
32
32
  4. Run `index` after writes.
33
33
  5. Run `context "<task or question>"` before answering.
34
34
  6. Use the returned sources as grounded context.
35
35
 
36
- `context` is read-only. It does not create notes, backlinks, graph edges or durable memory by itself. A relationship exists only when a Markdown note contains a `[[wiki link]]` to another note and the vault has been indexed after that write.
36
+ `context` is read-only. It does not create notes, backlinks, graph edges or durable memory by itself. A relationship exists only when a Markdown note contains a `[[wiki link]]` inside `## Context Links` and the vault has been indexed after that write.
37
37
 
38
- When an agent adds durable memory, it should connect the new note to at least one existing concept unless the note is intentionally a root concept. Prefer exact note titles in links, for example `[[Architecture]]`, and run `broken-links`, `orphans` or `validate` when the graph looks disconnected.
38
+ When an agent adds durable memory, it should add only the canonical relationships to `## Context Links`. Prefer exact note titles in links, for example `[[Architecture]]`, and run `broken-links`, `orphans` or `validate` when graph health matters.
39
39
 
40
40
  Agents can mark important relationships by placing priority hints on the same line as a wiki link, for example `[[Architecture]] priority: high`, `[[Incident Runbook]] #important` or `[[Incident Runbook]] #critical`. Indexed graph edges expose `weight` and `priority` so agents can sort related notes by importance.
41
41
 
package/CHANGELOG.md CHANGED
@@ -14,6 +14,9 @@
14
14
  - Added default MCP startup bootstrap behavior controlled by `brainlink_policy.autoBootstrapOnStartup`.
15
15
  - Added CLI MCP policy presets through `blink agent policy --preset fully-auto|strict`.
16
16
  - Added write-time non-orphan enforcement by auto-linking notes without wiki edges to agent hub notes.
17
+ - Changed graph indexing to keep every non-self Markdown wiki link as a weighted graph edge so the default star graph represents complete note connectivity.
18
+ - Added `blink index --full` and MCP `brainlink_index` `full=true` for complete source reindexing without clearing the existing index first.
19
+ - Improved index migration so stale graph link model metadata automatically triggers a complete source reindex.
17
20
  - Added MCP `brainlink_policy` presets (`fully-auto`, `strict`) for one-call policy switching.
18
21
  - Added MCP write connectivity metadata in `brainlink_add_note`/`brainlink_add_file` responses.
19
22
  - Added MCP `brainlink_recommendations` tool for plug-and-play workflow guidance.
package/README.md CHANGED
@@ -210,19 +210,19 @@ Only store knowledge that is likely to matter later:
210
210
  ```bash
211
211
  blink add "Testing Policy" \
212
212
  --agent "$BLINK_AGENT" \
213
- --content "Run npm run check before final delivery. Related: [[Release Checklist]]. #testing #process"
213
+ --content $'Run npm run check before final delivery. #testing #process\n\n## Context Links\n\n- [[Release Checklist]]'
214
214
  ```
215
215
 
216
- Brainlink does not infer durable graph relationships from generated context. A context result is only a read package for the model. To create a real link in the knowledge graph, the agent must write Markdown that contains an explicit `[[Note Title]]` wiki link.
216
+ Brainlink does not infer durable graph relationships from generated context. A context result is only a read package for the model. To create a real graph link, write a concise `## Context Links` section and put the canonical `[[Note Title]]` links there.
217
217
 
218
218
  Writes with `blink add` reindex the vault automatically by default. This can be disabled with `--no-auto-index` and controlled globally with `autoIndexOnWrite` in `brainlink.config.json`.
219
219
 
220
220
  When adding memory, follow this contract:
221
221
 
222
- - Link the new note to at least one existing note when there is a related concept.
222
+ - Link the new note to existing notes through `## Context Links` when there is a related concept.
223
223
  - Use the exact target note title inside `[[...]]`.
224
224
  - Add retrieval tags such as `#architecture`, `#decision`, `#runbook` or `#preference`.
225
- - Do not leave isolated notes unless they are intentionally root concepts.
225
+ - General wiki-link mentions outside `## Context Links` remain searchable Markdown content, but they do not become graph edges.
226
226
 
227
227
  If you disable auto-index, run `blink index` after batched writes.
228
228
 
@@ -538,7 +538,7 @@ Available tools:
538
538
  - `brainlink_add_file`: ingest a local file as a note and reindex.
539
539
  - `brainlink_volatile_add`: write temporary agent-decided memory with TTL; volatile sections are included in context and never create durable graph edges.
540
540
  - `brainlink_volatile_clear`: clear temporary memory for the current vault/agent namespace.
541
- - `brainlink_index`: rebuild the vault index.
541
+ - `brainlink_index`: rebuild the vault index. Pass `full=true` for a complete source reindex.
542
542
  - `brainlink_stats`: read indexed vault statistics.
543
543
  - `brainlink_validate`: validate broken links and orphan notes.
544
544
  - `brainlink_sync`: run index, stats, validation, broken-link and orphan checks in one call.
@@ -554,7 +554,7 @@ If you disable `autoBootstrapOnRead` through `brainlink_policy`, read tools retu
554
554
  `brainlink_bootstrap`, `brainlink_policy` and preflight responses include structured `nextActions` so MCP clients can continue automatically without custom parsing.
555
555
  For one-call planning, use `brainlink_recommendations` to get the recommended tool sequence for the current vault/agent/query.
556
556
 
557
- The same linking rule applies through MCP: `brainlink_context` is read-only, and real graph links require Markdown notes with explicit `[[wiki links]]`. `brainlink_add_note` and `brainlink_add_file` reindex by default and include index + `writeConnectivity` metadata. Brainlink guarantees at least one edge per new note by auto-linking when needed.
557
+ 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.
558
558
 
559
559
  Agents can raise the importance of a relationship by putting priority markers on the same line as a wiki link:
560
560
 
@@ -563,7 +563,14 @@ Agents can raise the importance of a relationship by putting priority markers on
563
563
  Related: [[Incident Runbook]] #critical
564
564
  ```
565
565
 
566
- Indexed edges expose `weight` and `priority` (`low`, `normal`, `high`, `critical`) through CLI JSON, HTTP graph APIs and `brainlink_graph`. Brainlink promotes only representative graph links per note: high-priority and high-weight links win, structural hub links such as `Memory Hub`, `Knowledge Root`, `MOC` and map notes are suppressed when stronger direct links exist, and old indexes are rebuilt automatically when their graph link model version is missing or stale.
566
+ Indexed edges expose `weight` and `priority` (`low`, `normal`, `high`, `critical`) through CLI JSON, HTTP graph APIs and `brainlink_graph`. Brainlink indexes every non-self `[[wiki link]]` inside `## Context Links` as a graph edge. Old indexes are rebuilt automatically when their graph link model version is missing or stale.
567
+
568
+ To migrate older vaults without deleting existing Markdown, generate concise context-link sections from current wiki-link mentions:
569
+
570
+ ```bash
571
+ blink migrate-context-links --vault ./vault --limit 5
572
+ blink index --vault ./vault --full
573
+ ```
567
574
 
568
575
  ## Graph UI
569
576
 
@@ -589,8 +596,8 @@ When native GUI is used, the GUI window automatically closes when the `blink ser
589
596
  The graph UI shows:
590
597
 
591
598
  - notes as nodes
592
- - representative `[[wiki links]]` as weighted edges
593
- - star layout centered on the primary hub, without rewriting or flattening underlying relationships
599
+ - all non-self `[[wiki links]]` inside `## Context Links` as weighted edges
600
+ - default star layout centered on the primary hub, without rewriting or flattening underlying relationships
594
601
  - details opened in a non-modal side panel (tags, outgoing links, backlinks, full Markdown content), so zoom and pan remain available while inspecting data
595
602
  - neutral graph nodes with segment/group metadata
596
603
  - agent selector (id-only labels) for isolated views
@@ -777,9 +784,12 @@ When action is not `merge`, Brainlink still creates a low-priority related edge
777
784
  ```bash
778
785
  blink index
779
786
  blink index --vault ./vault
787
+ blink index --vault ./vault --full
780
788
  ```
781
789
 
782
- Rebuilds the local index from Markdown files.
790
+ Rebuilds the local index from Markdown files. By default, unchanged notes reuse existing indexed chunks for speed.
791
+ Use `--full` to force a complete source reindex of every Markdown note. Full reindex builds the replacement index before persisting it, so existing context is not cleared first.
792
+ Brainlink also performs this complete source reindex automatically when it detects that the stored graph link model is older than the current model.
783
793
 
784
794
  ### `bench`
785
795
 
@@ -1,8 +1,5 @@
1
- import { access } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
1
  import { writeMarkdownFile } from '../infrastructure/file-system-vault.js';
4
2
  import { sanitizeAgentId, sharedAgentId } from '../domain/agents.js';
5
- import { extractWikiLinks } from '../domain/markdown.js';
6
3
  import { validateNoteInput } from '../domain/note-safety.js';
7
4
  import { ensureVault } from '../infrastructure/file-system-vault.js';
8
5
  const slugify = (title) => title
@@ -11,10 +8,6 @@ const slugify = (title) => title
11
8
  .toLowerCase()
12
9
  .replace(/[^a-z0-9]+/g, '-')
13
10
  .replace(/^-+|-+$/g, '');
14
- const systemHubTitle = 'Memory Hub';
15
- const systemRootTitle = 'Knowledge Root';
16
- const normalizeTitle = (title) => title.trim().replace(/\.md$/i, '').toLowerCase();
17
- const noteFilename = (agentId, title) => `agents/${agentId}/${slugify(title) || 'untitled'}.md`;
18
11
  const buildNote = (title, content, agentId) => [
19
12
  `---`,
20
13
  `title: "${title.replaceAll('"', '\\"')}"`,
@@ -26,38 +19,6 @@ const buildNote = (title, content, agentId) => [
26
19
  content.trim(),
27
20
  ''
28
21
  ].join('\n');
29
- const ensureSystemNote = async (vaultPath, absoluteVaultPath, agentId, title, content) => {
30
- const filename = noteFilename(agentId, title);
31
- const absolutePath = join(absoluteVaultPath, filename);
32
- try {
33
- await access(absolutePath);
34
- return;
35
- }
36
- catch { }
37
- await writeMarkdownFile(vaultPath, filename, buildNote(title, content, agentId));
38
- };
39
- const ensureNonOrphanContent = async (vaultPath, absoluteVaultPath, title, content, agentId) => {
40
- const links = extractWikiLinks(content).filter((link) => normalizeTitle(link) !== normalizeTitle(title));
41
- if (links.length > 0) {
42
- return {
43
- content: content.trim(),
44
- autoLinked: false,
45
- linkTarget: null
46
- };
47
- }
48
- const fallbackTitle = normalizeTitle(title) === normalizeTitle(systemHubTitle) ? systemRootTitle : systemHubTitle;
49
- if (fallbackTitle === systemRootTitle) {
50
- await ensureSystemNote(vaultPath, absoluteVaultPath, agentId, systemRootTitle, `Entry point for agent memory. [[${systemHubTitle}]] #memory #root`);
51
- }
52
- else {
53
- await ensureSystemNote(vaultPath, absoluteVaultPath, agentId, systemHubTitle, 'Central memory index for this agent namespace. #memory #hub');
54
- }
55
- return {
56
- content: `${content.trim()}\n\nRelated: [[${fallbackTitle}]]`,
57
- autoLinked: true,
58
- linkTarget: fallbackTitle
59
- };
60
- };
61
22
  export const addNoteWithMetadata = async (vaultPath, title, content, agentId = sharedAgentId, options = {}) => {
62
23
  validateNoteInput({
63
24
  title,
@@ -65,15 +26,14 @@ export const addNoteWithMetadata = async (vaultPath, title, content, agentId = s
65
26
  allowSensitive: options.allowSensitive
66
27
  });
67
28
  const sanitizedAgentId = sanitizeAgentId(agentId);
68
- const absoluteVaultPath = await ensureVault(vaultPath);
69
29
  const filename = `agents/${sanitizedAgentId}/${slugify(title) || 'untitled'}.md`;
70
- const linkedContent = await ensureNonOrphanContent(vaultPath, absoluteVaultPath, title, content, sanitizedAgentId);
71
- const note = buildNote(title, linkedContent.content, sanitizedAgentId);
30
+ await ensureVault(vaultPath);
31
+ const note = buildNote(title, content.trim(), sanitizedAgentId);
72
32
  const path = await writeMarkdownFile(vaultPath, filename, note);
73
33
  return {
74
34
  path,
75
- autoLinked: linkedContent.autoLinked,
76
- linkTarget: linkedContent.linkTarget
35
+ autoLinked: false,
36
+ linkTarget: null
77
37
  };
78
38
  };
79
39
  export const addNote = async (vaultPath, title, content, agentId = sharedAgentId, options = {}) => (await addNoteWithMetadata(vaultPath, title, content, agentId, options)).path;
@@ -298,51 +298,6 @@ const list = (items) => {
298
298
  .join('')
299
299
  }
300
300
 
301
- const extractContextLinks = (content) => {
302
- if (typeof content !== 'string' || content.length === 0) {
303
- return []
304
- }
305
- const lines = content.split(/\\r?\\n/)
306
- let start = -1
307
- for (let index = 0; index < lines.length; index += 1) {
308
- if (/^#{1,6}\\s+(?:context\\s+links?|links?\\s+de\\s+contexto)\\b/i.test(lines[index].trim())) {
309
- start = index + 1
310
- break
311
- }
312
- }
313
- if (start < 0) {
314
- return []
315
- }
316
-
317
- const links = []
318
- const seenTitles = new Set()
319
- for (let index = start; index < lines.length; index += 1) {
320
- const line = lines[index].trim()
321
- if (!line) {
322
- continue
323
- }
324
- if (/^#{1,6}\\s+/.test(line)) {
325
- break
326
- }
327
- const matches = Array.from(line.matchAll(/\\[\\[([^\\]]+)\\]\\]/g))
328
- if (matches.length === 0) {
329
- continue
330
- }
331
- const priorityMatch = line.match(/#(critical|important)\\b|priority:\\s*(high|critical)/i)
332
- const priority = priorityMatch ? String(priorityMatch[1] || priorityMatch[2] || 'normal').toLowerCase() : 'normal'
333
-
334
- for (let matchIndex = 0; matchIndex < matches.length; matchIndex += 1) {
335
- const title = String(matches[matchIndex][1] || '').trim()
336
- if (!title || seenTitles.has(title.toLowerCase())) {
337
- continue
338
- }
339
- seenTitles.add(title.toLowerCase())
340
- links.push({ title, priority })
341
- }
342
- }
343
- return links
344
- }
345
-
346
301
  const buildFacts = (node, outgoingCount, incomingCount) => {
347
302
  const content = typeof node?.content === 'string' ? node.content : ''
348
303
  const words = content.trim().length > 0 ? content.trim().split(/\\s+/).length : 0
@@ -368,6 +323,21 @@ const listContextLinks = (links) => {
368
323
  .join('')
369
324
  }
370
325
 
326
+ const nodeContextLinks = (node, outgoing) => {
327
+ const titles = Array.isArray(node?.contextLinks) ? node.contextLinks : []
328
+ const outgoingByTitle = new Map(normalizeList(outgoing).map((link) => [String(link.title || '').toLowerCase(), link]))
329
+
330
+ return titles
331
+ .map((title) => {
332
+ const match = outgoingByTitle.get(String(title).toLowerCase())
333
+ return {
334
+ title,
335
+ priority: match?.priority || 'normal'
336
+ }
337
+ })
338
+ .filter((link) => typeof link.title === 'string' && link.title.trim().length > 0)
339
+ }
340
+
371
341
  const linkedNodes = (node) => {
372
342
  const nodeById = new Map((state.chunk.nodes || []).map((item) => [item[0], item]))
373
343
  const edges = normalizeList(state.chunk.edges)
@@ -431,7 +401,7 @@ const loadNodeDetails = async (nodeId) => {
431
401
  : '<span>No tags</span>'
432
402
 
433
403
  const related = linkedNodes(node)
434
- const contextLinks = extractContextLinks(node.content)
404
+ const contextLinks = nodeContextLinks(node, related.outgoing)
435
405
  const facts = buildFacts(node, related.outgoing.length, related.incoming.length)
436
406
  elements.contentFacts.innerHTML = listFacts(facts)
437
407
  elements.contentContextLinks.innerHTML = listContextLinks(contextLinks)
@@ -3,7 +3,7 @@ import { access } from 'node:fs/promises';
3
3
  import { basename, extname, join, relative, resolve } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import { promisify } from 'node:util';
6
- import { extractTags, extractWikiLinks } from '../domain/markdown.js';
6
+ import { extractTags } from '../domain/markdown.js';
7
7
  import { sanitizeAgentId, sharedAgentId } from '../domain/agents.js';
8
8
  import { ensureVault, listVaultFiles, writeMarkdownFile } from '../infrastructure/file-system-vault.js';
9
9
  import { getBrainlinkHomePath } from '../infrastructure/paths.js';
@@ -17,9 +17,6 @@ const agentColumnCandidates = ['agent', 'agent_id', 'namespace', 'scope'];
17
17
  const tagColumnCandidates = ['tags', 'tag_list', 'keywords'];
18
18
  const createdColumnCandidates = ['created_at', 'createdat', 'created', 'ctime'];
19
19
  const updatedColumnCandidates = ['updated_at', 'updatedat', 'updated', 'mtime'];
20
- const systemHubTitle = 'Memory Hub';
21
- const systemRootTitle = 'Knowledge Root';
22
- const normalizeTitle = (title) => title.trim().replace(/\.md$/i, '').toLowerCase();
23
20
  const slugify = (title) => title
24
21
  .normalize('NFKD')
25
22
  .replace(/[\u0300-\u036f]/g, '')
@@ -203,31 +200,6 @@ const reserveUniquePath = (agentId, title, reserved) => {
203
200
  }
204
201
  throw new Error(`Could not allocate unique path for imported note: ${title}`);
205
202
  };
206
- const ensureSystemNote = async (vaultPath, reserved, created, agentId, title, content, dryRun) => {
207
- const filename = noteRelativePath(agentId, slugify(title));
208
- if (reserved.has(filename)) {
209
- return;
210
- }
211
- reserved.add(filename);
212
- created.add(filename);
213
- if (dryRun) {
214
- return;
215
- }
216
- await writeMarkdownFile(vaultPath, filename, buildNote(title, content, agentId));
217
- };
218
- const applyConnectivityRule = async (vaultPath, reserved, created, title, content, agentId, dryRun) => {
219
- const links = extractWikiLinks(content).filter((link) => normalizeTitle(link) !== normalizeTitle(title));
220
- if (links.length > 0) {
221
- return content.trim();
222
- }
223
- const normalized = normalizeTitle(title);
224
- if (normalized === normalizeTitle(systemHubTitle)) {
225
- await ensureSystemNote(vaultPath, reserved, created, agentId, systemRootTitle, `Entry point for agent memory. [[${systemHubTitle}]] #memory #root`, dryRun);
226
- return `${content.trim()}\n\nRelated: [[${systemRootTitle}]]`;
227
- }
228
- await ensureSystemNote(vaultPath, reserved, created, agentId, systemHubTitle, 'Central memory index for this agent namespace. #memory #hub', dryRun);
229
- return `${content.trim()}\n\nRelated: [[${systemHubTitle}]]`;
230
- };
231
203
  const importRowsFromMapping = async (vaultPath, dbPath, mapping, options, reserved) => {
232
204
  const limit = Number.isFinite(options.limit) && (options.limit ?? 0) > 0 ? Math.floor(options.limit ?? 0) : undefined;
233
205
  const sql = [
@@ -243,7 +215,6 @@ const importRowsFromMapping = async (vaultPath, dbPath, mapping, options, reserv
243
215
  ...(limit ? [`LIMIT ${limit}`] : [])
244
216
  ].join(' ');
245
217
  const rows = await runSqliteQuery(dbPath, sql);
246
- const createdSystemNotes = new Set();
247
218
  const importedFiles = [];
248
219
  let imported = 0;
249
220
  let skipped = 0;
@@ -256,8 +227,7 @@ const importRowsFromMapping = async (vaultPath, dbPath, mapping, options, reserv
256
227
  const agentId = sanitizeAgentId(options.agentOverride || row.agent || sharedAgentId);
257
228
  const filename = reserveUniquePath(agentId, row.title, reserved);
258
229
  const mergedContent = appendMissingTags(row.content, row.tags);
259
- const connectedContent = await applyConnectivityRule(vaultPath, reserved, createdSystemNotes, row.title, mergedContent, agentId, options.dryRun === true);
260
- const note = buildNote(row.title, connectedContent, agentId);
230
+ const note = buildNote(row.title, mergedContent.trim(), agentId);
261
231
  if (options.dryRun !== true) {
262
232
  await writeMarkdownFile(vaultPath, filename, note);
263
233
  }
@@ -268,7 +238,7 @@ const importRowsFromMapping = async (vaultPath, dbPath, mapping, options, reserv
268
238
  rowsRead: rows.length,
269
239
  imported,
270
240
  skipped,
271
- createdSystemNotes: createdSystemNotes.size,
241
+ createdSystemNotes: 0,
272
242
  importedFiles
273
243
  };
274
244
  };
@@ -85,8 +85,8 @@ const readChangedDocuments = async (absoluteVaultPath, changedSummaries) => {
85
85
  })));
86
86
  return new Map(parsed.map((document) => [document.path, document]));
87
87
  };
88
- export const indexVault = async (vaultPath) => {
89
- return indexVaultWithOptions(vaultPath, {});
88
+ export const indexVault = async (vaultPath, options = {}) => {
89
+ return indexVaultWithOptions(vaultPath, options);
90
90
  };
91
91
  export const indexVaultWithOptions = async (vaultPath, options) => {
92
92
  const startedAt = process.hrtime.bigint();
@@ -113,6 +113,7 @@ export const indexVaultWithOptions = async (vaultPath, options) => {
113
113
  markdownFiles: summaries.length,
114
114
  hasPreviousState: previousState != null
115
115
  });
116
+ const fullReindex = options.full === true;
116
117
  const index = openFileIndex(absoluteVaultPath);
117
118
  try {
118
119
  const existingIndexedDocuments = await index.getIndexedDocuments();
@@ -120,10 +121,13 @@ export const indexVaultWithOptions = async (vaultPath, options) => {
120
121
  const currentSnapshot = toSnapshot(summaries);
121
122
  const currentSnapshotMap = createSnapshotMap(currentSnapshot);
122
123
  const previousSnapshotMap = createSnapshotMap(previousState?.files ?? []);
124
+ const graphLinkModelChanged = previousState != null &&
125
+ previousState.graphLinkModelVersion !== graphLinkModelVersion;
126
+ const fullSourceReindex = fullReindex || graphLinkModelChanged;
123
127
  const settingsChanged = previousState == null ||
124
128
  previousState.chunkSize !== config.chunkSize ||
125
129
  previousState.embeddingProvider !== config.embeddingProvider ||
126
- previousState.graphLinkModelVersion !== graphLinkModelVersion;
130
+ graphLinkModelChanged;
127
131
  const packSettingsChanged = previousState == null ||
128
132
  previousState.searchPackRowChunkSize !== config.searchPack.rowChunkSize ||
129
133
  previousState.searchPackCompressionLevel !== config.searchPack.compressionLevel ||
@@ -132,7 +136,8 @@ export const indexVaultWithOptions = async (vaultPath, options) => {
132
136
  for (let index = 0; index < summaries.length; index += 1) {
133
137
  const summary = summaries[index];
134
138
  const previous = previousSnapshotMap.get(summary.relativePath);
135
- const changed = settingsChanged ||
139
+ const changed = fullSourceReindex ||
140
+ settingsChanged ||
136
141
  previous == null ||
137
142
  previous.mtimeMs !== summary.updatedAt.getTime() ||
138
143
  previous.size !== summary.size ||
@@ -148,7 +153,8 @@ export const indexVaultWithOptions = async (vaultPath, options) => {
148
153
  if (changedPaths.size === 0 &&
149
154
  !hasDeletes &&
150
155
  existingIndexedDocuments.length === summaries.length &&
151
- previousState != null) {
156
+ previousState != null &&
157
+ !fullReindex) {
152
158
  const result = {
153
159
  ...toIndexResult(existingIndexedDocuments),
154
160
  elapsedMs: elapsedMs(),
@@ -205,7 +211,6 @@ export const indexVaultWithOptions = async (vaultPath, options) => {
205
211
  return needsRelink ? relinkIndexedDocument(existing, titleMaps) : existing;
206
212
  });
207
213
  emit('persist', 'start', 'Persisting index');
208
- await index.reset();
209
214
  await index.saveDocuments(indexedDocuments);
210
215
  emit('persist', 'finish', 'Index persisted', {
211
216
  indexedDocuments: indexedDocuments.length
@@ -217,6 +222,8 @@ export const indexVaultWithOptions = async (vaultPath, options) => {
217
222
  const previousPendingPackChanges = previousState?.pendingPackChanges ?? 0;
218
223
  const pendingPackChanges = previousPendingPackChanges + changedCount;
219
224
  const shouldRebuildPacks = !existingPackManifest ||
225
+ fullReindex ||
226
+ graphLinkModelChanged ||
220
227
  settingsChanged ||
221
228
  packSettingsChanged ||
222
229
  hasDeletes ||
@@ -226,21 +233,25 @@ export const indexVaultWithOptions = async (vaultPath, options) => {
226
233
  let packResult;
227
234
  const packReason = !existingPackManifest
228
235
  ? 'Missing pack manifest'
229
- : manifestRecovery.repaired
230
- ? 'Pack manifest repaired from existing packs'
231
- : settingsChanged
232
- ? 'Index settings changed'
233
- : packSettingsChanged
234
- ? 'Search pack settings changed'
235
- : hasDeletes
236
- ? 'Document deletions detected'
237
- : changedCount >= 400
238
- ? 'Changed file count threshold reached'
239
- : changeRatio >= 0.04
240
- ? 'Change ratio threshold reached'
241
- : pendingPackChanges >= 1200
242
- ? 'Pending pack changes threshold reached'
243
- : 'Pack rebuild skipped';
236
+ : fullReindex
237
+ ? 'Full reindex requested'
238
+ : graphLinkModelChanged
239
+ ? 'Graph link model changed'
240
+ : manifestRecovery.repaired
241
+ ? 'Pack manifest repaired from existing packs'
242
+ : settingsChanged
243
+ ? 'Index settings changed'
244
+ : packSettingsChanged
245
+ ? 'Search pack settings changed'
246
+ : hasDeletes
247
+ ? 'Document deletions detected'
248
+ : changedCount >= 400
249
+ ? 'Changed file count threshold reached'
250
+ : changeRatio >= 0.04
251
+ ? 'Change ratio threshold reached'
252
+ : pendingPackChanges >= 1200
253
+ ? 'Pending pack changes threshold reached'
254
+ : 'Pack rebuild skipped';
244
255
  if (shouldRebuildPacks) {
245
256
  emit('packs', 'start', 'Rebuilding compressed search packs', {
246
257
  reason: packReason
@@ -0,0 +1,79 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { ensureVault, readMarkdownFileSummaries, writeMarkdownFile } from '../infrastructure/file-system-vault.js';
3
+ import { extractContextLinkWeights, extractWikiLinkWeights, hasContextLinksSection, parseMarkdownDocument } from '../domain/markdown.js';
4
+ const defaultContextLinkLimit = 5;
5
+ const normalizeTitle = (title) => title.trim().replace(/\.md$/i, '').toLowerCase();
6
+ const formatPriority = (priority) => priority === 'normal' ? '' : ` priority: ${priority}`;
7
+ const formatContextLinksSection = (links) => [
8
+ '## Context Links',
9
+ '',
10
+ ...links.map((link) => `- [[${link.title}]]${formatPriority(link.priority)}`)
11
+ ].join('\n');
12
+ const appendContextLinksSection = (content, links) => `${content.trimEnd()}\n\n${formatContextLinksSection(links)}\n`;
13
+ const selectContextLinkCandidates = (content, title, limit) => extractWikiLinkWeights(content)
14
+ .filter((link) => normalizeTitle(link.title) !== normalizeTitle(title))
15
+ .slice(0, limit)
16
+ .map((link) => ({
17
+ title: link.title,
18
+ priority: link.priority,
19
+ weight: link.weight
20
+ }));
21
+ export const migrateContextLinks = async (vaultPath, options = {}) => {
22
+ const absoluteVaultPath = await ensureVault(vaultPath);
23
+ const limit = Math.max(1, Math.floor(options.limit ?? defaultContextLinkLimit));
24
+ const summaries = await readMarkdownFileSummaries(absoluteVaultPath);
25
+ const entries = [];
26
+ for (const summary of summaries) {
27
+ const content = await readFile(summary.absolutePath, 'utf8');
28
+ const document = parseMarkdownDocument({
29
+ absolutePath: summary.absolutePath,
30
+ vaultPath: absoluteVaultPath,
31
+ content,
32
+ createdAt: summary.createdAt,
33
+ updatedAt: summary.updatedAt
34
+ });
35
+ if (options.agentId && document.agentId !== options.agentId) {
36
+ continue;
37
+ }
38
+ if (hasContextLinksSection(content)) {
39
+ entries.push({
40
+ path: summary.relativePath,
41
+ title: document.title,
42
+ changed: false,
43
+ reason: 'already-has-context-links',
44
+ links: extractContextLinkWeights(content)
45
+ });
46
+ continue;
47
+ }
48
+ const links = selectContextLinkCandidates(content, document.title, limit);
49
+ if (links.length === 0) {
50
+ entries.push({
51
+ path: summary.relativePath,
52
+ title: document.title,
53
+ changed: false,
54
+ reason: 'no-link-candidates',
55
+ links
56
+ });
57
+ continue;
58
+ }
59
+ if (!options.dryRun) {
60
+ await writeMarkdownFile(vaultPath, summary.relativePath, appendContextLinksSection(content, links));
61
+ }
62
+ entries.push({
63
+ path: summary.relativePath,
64
+ title: document.title,
65
+ changed: true,
66
+ reason: 'added-context-links',
67
+ links
68
+ });
69
+ }
70
+ const changed = entries.filter((entry) => entry.changed).length;
71
+ return {
72
+ dryRun: options.dryRun === true,
73
+ scanned: entries.length,
74
+ changed,
75
+ skipped: entries.length - changed,
76
+ limit,
77
+ entries
78
+ };
79
+ };
@@ -1,10 +1,61 @@
1
+ import { stat } from 'node:fs/promises';
1
2
  import { ensureVault } from '../infrastructure/file-system-vault.js';
2
- import { openFileIndex } from '../infrastructure/file-index.js';
3
+ import { indexStoragePath, openFileIndex } from '../infrastructure/file-index.js';
4
+ const graphSearchCacheTtlMs = 20_000;
5
+ const graphSearchCacheMaxEntries = 120;
6
+ const graphSearchCache = new Map();
7
+ const readIndexSignature = async (vaultPath) => {
8
+ try {
9
+ const info = await stat(indexStoragePath(vaultPath));
10
+ return `${Math.floor(info.mtimeMs)}:${info.size}`;
11
+ }
12
+ catch {
13
+ return '0:0';
14
+ }
15
+ };
16
+ const cacheKey = (vaultPath, query, limit, agentId) => JSON.stringify({
17
+ vaultPath,
18
+ query: query.trim().toLowerCase(),
19
+ limit,
20
+ agentId: agentId?.trim().toLowerCase() ?? '*'
21
+ });
22
+ const readCached = (key, indexSignature) => {
23
+ const entry = graphSearchCache.get(key);
24
+ if (!entry) {
25
+ return undefined;
26
+ }
27
+ const fresh = Date.now() - entry.createdAt <= graphSearchCacheTtlMs && entry.indexSignature === indexSignature;
28
+ if (!fresh) {
29
+ graphSearchCache.delete(key);
30
+ return undefined;
31
+ }
32
+ return entry.nodeIds;
33
+ };
34
+ const writeCached = (key, entry) => {
35
+ graphSearchCache.set(key, entry);
36
+ if (graphSearchCache.size <= graphSearchCacheMaxEntries) {
37
+ return;
38
+ }
39
+ const overflow = graphSearchCache.size - graphSearchCacheMaxEntries;
40
+ Array.from(graphSearchCache.keys()).slice(0, overflow).forEach((cacheKey) => graphSearchCache.delete(cacheKey));
41
+ };
3
42
  export const searchGraphNodeIds = async (vaultPath, query, limit, agentId) => {
4
43
  const absoluteVaultPath = await ensureVault(vaultPath);
44
+ const indexSignature = await readIndexSignature(absoluteVaultPath);
45
+ const key = cacheKey(absoluteVaultPath, query, limit, agentId);
46
+ const cached = readCached(key, indexSignature);
47
+ if (cached) {
48
+ return cached;
49
+ }
5
50
  const index = openFileIndex(absoluteVaultPath);
6
51
  try {
7
- return await index.searchGraphNodeIds(query, limit, agentId);
52
+ const nodeIds = await index.searchGraphNodeIds(query, limit, agentId);
53
+ writeCached(key, {
54
+ createdAt: Date.now(),
55
+ indexSignature,
56
+ nodeIds
57
+ });
58
+ return nodeIds;
8
59
  }
9
60
  finally {
10
61
  index.close();
@@ -8,6 +8,7 @@ import { buildContextPackage } from '../../application/build-context.js';
8
8
  import { resolveDuplicateNotes, scanDuplicateNotes } from '../../application/dedupe-notes.js';
9
9
  import { importLegacySqliteDatabase } from '../../application/import-legacy-sqlite.js';
10
10
  import { indexVault, indexVaultWithOptions } from '../../application/index-vault.js';
11
+ import { migrateContextLinks } from '../../application/migrate-context-links.js';
11
12
  import { migrateVaultContent, planVaultMigration, previewVaultMigration, shouldMigrateDefaultVault } from '../../application/migrate-vault.js';
12
13
  import { createOfflinePackBackup } from '../../application/offline-pack-backup.js';
13
14
  import { startServer } from '../../application/start-server.js';
@@ -699,6 +700,37 @@ export const registerWriteCommands = (program) => {
699
700
  return `${summary}${indexMessage}${reportMessage}`;
700
701
  });
701
702
  });
703
+ program
704
+ .command('migrate-context-links')
705
+ .option('-v, --vault <vault>', 'vault directory')
706
+ .option('-a, --agent <agent>', 'agent memory namespace')
707
+ .option('-l, --limit <limit>', 'maximum context links to add per note', '5')
708
+ .option('--dry-run', 'preview context-link migration without writing files')
709
+ .option('--no-index', 'skip reindexing after migration')
710
+ .option('--json', 'print machine-readable JSON')
711
+ .description('add concise Context Links sections from existing wiki-link mentions')
712
+ .action(async (options) => {
713
+ const resolved = await resolveOptions(options);
714
+ const result = await migrateContextLinks(resolved.vault, {
715
+ dryRun: options.dryRun === true,
716
+ limit: parsePositiveInteger(options.limit ?? '5', 5),
717
+ agentId: resolved.agent
718
+ });
719
+ const shouldIndex = options.index !== false && !result.dryRun && result.changed > 0;
720
+ const index = shouldIndex ? await indexVault(resolved.vault, { full: true }) : undefined;
721
+ print(options.json, {
722
+ vault: resolved.vault,
723
+ agent: resolved.agent ?? 'shared',
724
+ ...result,
725
+ ...(index ? { index } : {})
726
+ }, () => {
727
+ const mode = result.dryRun ? 'Previewed' : 'Migrated';
728
+ const indexMessage = index
729
+ ? ` Fully reindexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} context links.`
730
+ : '';
731
+ return `${mode} ${result.scanned} notes: changed=${result.changed}, skipped=${result.skipped}, limit=${result.limit}.${indexMessage}`;
732
+ });
733
+ });
702
734
  program
703
735
  .command('db-import')
704
736
  .option('-v, --vault <vault>', 'vault directory')
@@ -792,7 +824,7 @@ export const registerWriteCommands = (program) => {
792
824
  writeConnectivity: {
793
825
  autoLinked: added.autoLinked,
794
826
  linkTarget: added.linkTarget,
795
- guaranteedEdge: true
827
+ guaranteedEdge: false
796
828
  },
797
829
  possibleDuplicates,
798
830
  ...(index ? { index } : {})
@@ -860,12 +892,17 @@ export const registerWriteCommands = (program) => {
860
892
  program
861
893
  .command('index')
862
894
  .option('-v, --vault <vault>', 'vault directory')
895
+ .option('--full', 'force a complete reindex from Markdown source without reusing unchanged index entries')
863
896
  .option('--json', 'print machine-readable JSON')
864
897
  .description('index markdown notes, links, tags and chunks')
865
898
  .action(async (options) => {
866
899
  const resolved = await resolveOptions(options);
867
- const result = await indexVault(resolved.vault);
868
- print(options.json, result, () => `Indexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`);
900
+ const result = await indexVault(resolved.vault, {
901
+ full: options.full === true
902
+ });
903
+ print(options.json, result, () => options.full === true
904
+ ? `Fully reindexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`
905
+ : `Indexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`);
869
906
  });
870
907
  program
871
908
  .command('bench')
@@ -6,6 +6,7 @@ const frontmatterPattern = /^---\n([\s\S]*?)\n---\n?/;
6
6
  const wikiLinkPattern = /\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]/g;
7
7
  const tagPattern = /(^|\s)#([A-Za-z0-9][A-Za-z0-9_-]*)/g;
8
8
  const headingPattern = /^#\s+(.+)$/m;
9
+ const contextLinksHeadingPattern = /^(#{1,6})\s+(?:context\s+links?|links?\s+de\s+contexto)\b/i;
9
10
  const priorityRanks = {
10
11
  low: 0,
11
12
  normal: 1,
@@ -18,9 +19,7 @@ const priorityBoosts = {
18
19
  high: 3,
19
20
  critical: 6
20
21
  };
21
- const graphLinkLimit = 4;
22
- export const graphLinkModelVersion = 2;
23
- const hubLinkTitlePattern = /\b(?:memory\s*hub|knowledge\s*root|moc|map)\b/i;
22
+ export const graphLinkModelVersion = 4;
24
23
  const priorityPatterns = [
25
24
  ['critical', /\b(?:priority|prioridade|importance|importancia|importância)\s*[:=]\s*(?:critical|critica|crítica|urgent|urgente|p0)\b/i],
26
25
  ['critical', /#(?:critical|critica|crítica|urgent|urgente|p0)\b/i],
@@ -34,7 +33,6 @@ const priorityPatterns = [
34
33
  const normalizeTitle = (title) => title.trim().replace(/\.md$/i, '');
35
34
  const unique = (values) => Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
36
35
  const maxPriority = (left, right) => priorityRanks[left] >= priorityRanks[right] ? left : right;
37
- const isHubLinkTitle = (title) => hubLinkTitlePattern.test(title);
38
36
  const parseFrontmatter = (content) => {
39
37
  const match = content.match(frontmatterPattern);
40
38
  if (!match) {
@@ -114,12 +112,37 @@ const compareGraphLinks = (left, right) => {
114
112
  return left.title.localeCompare(right.title);
115
113
  };
116
114
  export const selectGraphWikiLinkWeights = (links) => {
117
- const sorted = [...links].sort(compareGraphLinks);
118
- const structuralLinks = sorted.filter((link) => isHubLinkTitle(link.title) && !['high', 'critical'].includes(link.priority));
119
- const directLinks = sorted.filter((link) => !structuralLinks.includes(link));
120
- const selected = (directLinks.length > 0 ? directLinks : sorted).slice(0, graphLinkLimit);
121
- return selected.length > 0 ? selected : structuralLinks.slice(0, graphLinkLimit);
115
+ return [...links].sort(compareGraphLinks);
122
116
  };
117
+ const contextLinkLines = (content) => {
118
+ const lines = visibleMarkdownLines(content);
119
+ const selected = [];
120
+ let insideContextLinks = false;
121
+ let headingDepth = 0;
122
+ for (let index = 0; index < lines.length; index += 1) {
123
+ const line = lines[index];
124
+ const trimmed = line.content.trim();
125
+ if (line.fenced) {
126
+ continue;
127
+ }
128
+ const heading = trimmed.match(/^(#{1,6})\s+/);
129
+ const contextHeading = trimmed.match(contextLinksHeadingPattern);
130
+ if (contextHeading) {
131
+ insideContextLinks = true;
132
+ headingDepth = contextHeading[1].length;
133
+ continue;
134
+ }
135
+ if (insideContextLinks && heading && heading[1].length <= headingDepth) {
136
+ insideContextLinks = false;
137
+ }
138
+ if (insideContextLinks) {
139
+ selected.push(line.content);
140
+ }
141
+ }
142
+ return selected;
143
+ };
144
+ export const hasContextLinksSection = (content) => visibleMarkdownLines(content).some((line) => !line.fenced && contextLinksHeadingPattern.test(line.content.trim()));
145
+ export const extractContextLinkWeights = (content) => selectGraphWikiLinkWeights(extractWikiLinkWeights(contextLinkLines(content).join('\n')));
123
146
  const extractTitle = (filePath, content, frontmatter) => {
124
147
  if (frontmatter.title) {
125
148
  return normalizeTitle(frontmatter.title);
@@ -204,6 +227,7 @@ export const parseMarkdownDocument = (input) => {
204
227
  content: input.content,
205
228
  tags: extractTags(input.content),
206
229
  links: extractWikiLinks(input.content),
230
+ contextLinks: extractContextLinkWeights(input.content).map((link) => link.title),
207
231
  frontmatter,
208
232
  createdAt: input.createdAt.toISOString(),
209
233
  updatedAt: input.updatedAt.toISOString()
@@ -211,7 +235,7 @@ export const parseMarkdownDocument = (input) => {
211
235
  };
212
236
  export const createIndexedDocument = (document, titleToDocumentId, maxChunkCharacters = 1200) => {
213
237
  const chunks = splitIntoChunks(document.id, document.content, maxChunkCharacters);
214
- const graphLinkWeights = selectGraphWikiLinkWeights(extractWikiLinkWeights(document.content).filter((link) => link.title.toLowerCase() !== document.title.toLowerCase()));
238
+ const graphLinkWeights = selectGraphWikiLinkWeights(extractContextLinkWeights(document.content).filter((link) => link.title.toLowerCase() !== document.title.toLowerCase()));
215
239
  const linkWeights = new Map(graphLinkWeights.map((link) => [link.title.toLowerCase(), link]));
216
240
  const links = graphLinkWeights
217
241
  .map((link) => ({
@@ -266,7 +266,8 @@ export const openFileIndex = (vaultPath) => {
266
266
  title: document.title,
267
267
  path: document.path,
268
268
  content: document.content,
269
- tags: document.tags
269
+ tags: document.tags,
270
+ contextLinks: document.contextLinks ?? []
270
271
  })),
271
272
  edges
272
273
  };
@@ -292,7 +293,8 @@ export const openFileIndex = (vaultPath) => {
292
293
  title: document.title,
293
294
  path: document.path,
294
295
  content: '',
295
- tags: document.tags
296
+ tags: document.tags,
297
+ contextLinks: document.contextLinks ?? []
296
298
  })),
297
299
  edges
298
300
  };
@@ -309,7 +311,8 @@ export const openFileIndex = (vaultPath) => {
309
311
  title: document.title,
310
312
  path: document.path,
311
313
  content: document.content,
312
- tags: document.tags
314
+ tags: document.tags,
315
+ contextLinks: document.contextLinks ?? []
313
316
  }
314
317
  : undefined;
315
318
  },
@@ -70,7 +70,7 @@ export const createBrainlinkMcpServer = () => {
70
70
  }, addFileTool);
71
71
  server.registerTool('brainlink_index', {
72
72
  title: 'Index Brainlink Vault',
73
- description: 'Rebuild the local Brainlink index from Markdown notes.',
73
+ description: 'Rebuild the local Brainlink index from Markdown notes. Pass full=true to force a complete source rebuild.',
74
74
  inputSchema: indexInputSchema
75
75
  }, indexTool);
76
76
  server.registerTool('brainlink_stats', {
package/dist/mcp/tools.js CHANGED
@@ -262,7 +262,12 @@ export const addFileInputSchema = {
262
262
  allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.')
263
263
  };
264
264
  export const indexInputSchema = {
265
- ...vaultInput
265
+ ...vaultInput,
266
+ full: z
267
+ .boolean()
268
+ .optional()
269
+ .default(false)
270
+ .describe('Force a complete reindex from Markdown source without reusing unchanged index entries.')
266
271
  };
267
272
  export const validateInputSchema = {
268
273
  ...vaultInput,
@@ -415,7 +420,7 @@ export const addNoteTool = async (input) => {
415
420
  writeConnectivity: {
416
421
  autoLinked: added.autoLinked,
417
422
  linkTarget: added.linkTarget,
418
- guaranteedEdge: true
423
+ guaranteedEdge: false
419
424
  },
420
425
  possibleDuplicates,
421
426
  ...(index ? { index } : {})
@@ -462,14 +467,16 @@ export const addFileTool = async (input) => {
462
467
  writeConnectivity: {
463
468
  autoLinked: added.autoLinked,
464
469
  linkTarget: added.linkTarget,
465
- guaranteedEdge: true
470
+ guaranteedEdge: false
466
471
  },
467
472
  ...(index ? { index } : {})
468
473
  });
469
474
  };
470
475
  export const indexTool = async (input) => {
471
476
  const context = await resolveExecutionContext(input);
472
- const result = await indexVault(context.vault);
477
+ const result = await indexVault(context.vault, {
478
+ full: input.full === true
479
+ });
473
480
  return jsonResult({
474
481
  vault: context.vault,
475
482
  ...result
@@ -161,7 +161,7 @@ Brainlink only builds graph edges from Markdown `[[wiki links]]`.
161
161
 
162
162
  The `context` command is read-only. It retrieves indexed notes and returns a compact package for the model, but it does not write memory, create backlinks, infer relationships or modify the graph. If an agent reads context and then learns something durable, the agent must write a note with explicit links before that knowledge becomes connected memory.
163
163
 
164
- Graph edges are weighted during indexing. Repeated links increase weight. Links inside headings or task-list lines receive a small boost. Priority markers on the same line as a link raise its priority. The graph relationship model promotes only representative links per note: high-priority and high-weight links win, structural hub links such as `Memory Hub`, `Knowledge Root`, `MOC` and map notes are not promoted when stronger direct links exist, and each note emits a small bounded set of graph edges. Older indexes without the current graph link model version are automatically rebuilt on the next index run.
164
+ Graph edges are weighted during indexing. Repeated links increase weight. Links inside headings or task-list lines receive a small boost. Priority markers on the same line as a link raise its priority. The graph relationship model indexes every non-self Markdown `[[wiki link]]` as an edge, including structural hub links such as `Memory Hub`, `Knowledge Root`, `MOC` and map notes. Older indexes without the current graph link model version are automatically rebuilt on the next index run.
165
165
 
166
166
  ```md
167
167
  - [ ] Review [[Architecture]] priority: high
@@ -292,6 +292,8 @@ blink add "Implementation Boundary" \
292
292
  blink index
293
293
  ```
294
294
 
295
+ Use `blink index --full` when you need to rebuild every note from Markdown source. Full reindexing builds the replacement index before persisting it, preserving the previous context until the new index is ready. Brainlink also runs this complete source reindex automatically when it detects an older stored graph link model.
296
+
295
297
  ### Claude Code-Style Agent
296
298
 
297
299
  Use Brainlink as a preflight memory read before editing files:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.150",
3
+ "version": "0.1.0-beta.152",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",