@andespindola/brainlink 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.0
4
+
5
+ - **Shared namespace by default.** `defaultAgent` now defaults to `shared`, so every agent (Claude, Codex, GPT, …) reads and writes the same namespace and sees each other's memory unless a call passes an explicit `--agent`/`agent`. Set `defaultAgent` to another id, or pass `--agent` per call, to scope to a private namespace.
6
+ - **Domain partitioning on write.** `brainlink_add_note`, `brainlink_add_notes` (batch-level and per-note) and `brainlink_remember` accept a `context`. When set, the note is linked to its `<context> Hub` (created if missing) so the knowledge graph stays split into navigable domains instead of collapsing into a single per-namespace context. An explicit context always links, even when auto context links are otherwise disabled.
7
+ - **Lossless namespace merge.** New `brainlink_vault_merge_agents` MCP tool and `blink vaults merge-agents` CLI merge one agent namespace into another within a vault (e.g. `claude-code` → `shared`) without losing data: identical notes are skipped, colliding slugs with different content are kept under a `-from-<agent>` name, everything else is moved, and merged notes are aggregated into domain hubs. Source notes are kept unless `deleteSource` is set; `dryRun` previews the plan.
8
+ - **Stronger on-connect instructions.** The advertised server instructions now encode consult-before-acting, domain partitioning on every write, persistent and volatile memory used together, and shared-by-default so all agents form one continuous brain. The field stays advisory and no tool contract changed.
9
+
10
+ ## 1.3.0
11
+
12
+ - **MCP server instructions on connect.** The server now advertises `instructions` in its initialize response, so clients that surface them (e.g. Claude Code) inject Brainlink's memory discipline into the agent's context the moment it connects — treat Brainlink as the default context source for everything, bootstrap first with a task query, use the full toolkit (both RAG and CAG via `strategy: auto`, context packs, hybrid search, the knowledge graph, explain/recommendations/validate/stats) instead of shallow retrieval, keep ephemeral facts in volatile memory, and persist durable learnings as connected notes. The field is advisory (clients that ignore it are unaffected) and no tool contract changed.
13
+
3
14
  ## 1.2.0
4
15
 
5
16
  - **Reciprocal Rank Fusion for hybrid search.** Hybrid mode now fuses the lexical (BM25) and semantic rankings by rank position (`Σ 1/(k+rank)`, k=60) instead of a fixed `0.6/0.4` min-max blend, which is robust to the two components' very different score scales. Gated by a new `hybridFusion` config (`rrf` default | `linear`); `fts`/`semantic` modes and stored data are unchanged.
package/README.md CHANGED
@@ -543,9 +543,9 @@ Available tools:
543
543
  - `brainlink_similar_notes`: find the notes most similar to a given note (by title or path), excluding the note itself.
544
544
  - `brainlink_dedupe`: detect duplicate candidates using exact hash + semantic similarity scores.
545
545
  - `brainlink_resolve_duplicate`: resolve duplicate pairs (`merge`, `link`, `ignore`) with connectivity-safe fallback edges.
546
- - `brainlink_add_note`: write durable Markdown memory and reindex.
547
- - `brainlink_add_notes`: write several notes in one batch, reindexing once.
548
- - `brainlink_remember`: capture durable memory with inferred title, tags and Context Links; supports dry-run.
546
+ - `brainlink_add_note`: write durable Markdown memory and reindex. Pass `context` to partition the note under a `<context> Hub` (created if missing) so the graph stays split by domain.
547
+ - `brainlink_add_notes`: write several notes in one batch, reindexing once. Accepts a batch-level `context` and per-note `context` overrides.
548
+ - `brainlink_remember`: capture durable memory with inferred title, tags and Context Links; supports dry-run. Pass `context` to partition it under a domain hub.
549
549
  - `brainlink_inbox_add`: capture a quick untriaged memory item.
550
550
  - `brainlink_inbox_list`: list untriaged inbox memory items.
551
551
  - `brainlink_inbox_process`: suggest titles, tags and links for inbox items.
@@ -563,6 +563,7 @@ Available tools:
563
563
  - `brainlink_vault_provision`: create a private GitHub repository for the vault with the authenticated `gh` CLI, wire it as `origin`, push the first snapshot, and enable `autoVersion` so later indexed changes are committed and pushed automatically. By default the repository is **encrypted** (`vaultEncryption`): it holds AES-256-GCM ciphertext, decryptable only by a Brainlink instance with the local key.
564
564
  - `brainlink_vault_status` / `brainlink_vault_init` / `brainlink_vault_commit` / `brainlink_vault_history` / `brainlink_vault_pull` / `brainlink_vault_restore`: version the vault's canonical Markdown with git (the derived index is git-ignored). Pull and restore reindex changed files automatically so the vault stays immediately searchable.
565
565
  - `brainlink_vault_export` / `brainlink_vault_import`: write and read a portable `.blvault` snapshot (gzip envelope of the Markdown with per-file SHA-256). Import verifies checksums, preserves divergent local files as `*.conflict-<ts>.md`, and reindexes.
566
+ - `brainlink_vault_merge_agents`: losslessly merge one agent namespace into another within the same vault (e.g. `claude-code` → `shared`). Identical notes are skipped, colliding slugs with different content are kept under a `-from-<agent>` name, everything else is moved, and merged notes are aggregated into domain `<context> Hub` notes. Source notes are kept unless `deleteSource` is set; `dryRun` previews the plan.
566
567
  - `brainlink_graph`: read indexed graph nodes and weighted links.
567
568
  - `brainlink_graph_contexts`: list the visual graph contexts used by the local server.
568
569
  - `brainlink_broken_links`: list unresolved wiki links.
@@ -1184,7 +1185,7 @@ If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HO
1184
1185
  }
1185
1186
  ```
1186
1187
 
1187
- `defaultAgent` is optional. When set, CLI and MCP calls that omit `--agent`/`agent` use this value automatically. If not set, behavior remains as before.
1188
+ `defaultAgent` defaults to `shared`, so every agent (Claude, Codex, GPT, …) reads and writes the same shared namespace and sees each other's memory unless a call passes an explicit `--agent`/`agent`. Set it to another id to make a different namespace the default, or pass `--agent` per call to scope to a private namespace.
1188
1189
  `agentProfiles` is optional. When present, CLI and MCP resolve `mode`, `limit`, `tokens` and context `strategy` per agent automatically, then fallback to global defaults.
1189
1190
 
1190
1191
  `autoIndexOnWrite` is optional and defaults to `true`. Set it to `false` to defer indexing after writes.
@@ -29,9 +29,12 @@ export const addNoteWithMetadata = async (vaultPath, title, content, agentId = s
29
29
  const sanitizedAgentId = sanitizeAgentId(agentId);
30
30
  const filename = `agents/${sanitizedAgentId}/${slugify(title) || 'untitled'}.md`;
31
31
  await ensureVault(vaultPath);
32
- const canonical = options.autoContextLinks === false
32
+ const contextOverride = options.context?.trim() || undefined;
33
+ // An explicit context always partitions the note, even when auto context links
34
+ // are otherwise disabled; without an override, honor the auto-link setting.
35
+ const canonical = options.autoContextLinks === false && !contextOverride
33
36
  ? null
34
- : addCanonicalContextLinkToContent(title, content.trim(), await loadVisualContextRules(vaultPath), filename);
37
+ : addCanonicalContextLinkToContent(title, content.trim(), await loadVisualContextRules(vaultPath), filename, contextOverride);
35
38
  const hub = canonical?.changed
36
39
  ? await ensureCanonicalContextHub(vaultPath, canonical.context, sanitizedAgentId)
37
40
  : null;
@@ -228,15 +228,19 @@ export const canonicalizeContextLinks = async (vaultPath, options = {}) => {
228
228
  entries
229
229
  };
230
230
  };
231
- export const addCanonicalContextLinkToContent = (title, content, rules = [], notePath = '') => {
232
- const context = inferVisualGraphContext({
233
- id: '',
234
- agentId: sharedAgentId,
235
- title,
236
- path: notePath,
237
- content,
238
- tags: [],
239
- }, rules);
231
+ // When an explicit context is provided by the caller, it overrides inference so
232
+ // a note lands in that domain hub instead of the generic per-namespace fallback
233
+ // (which collapses every agent note into a single "Agent Memory" context). An
234
+ // empty/whitespace override is ignored, preserving inference.
235
+ const resolveContextTitle = (contextOverride, title, content, notePath, rules) => {
236
+ const explicit = contextOverride?.trim();
237
+ if (explicit) {
238
+ return explicit;
239
+ }
240
+ return inferVisualGraphContext({ id: '', agentId: sharedAgentId, title, path: notePath, content, tags: [] }, rules).title;
241
+ };
242
+ export const addCanonicalContextLinkToContent = (title, content, rules = [], notePath = '', contextOverride) => {
243
+ const context = { title: resolveContextTitle(contextOverride, title, content, notePath, rules) };
240
244
  const hubTitle = hubTitleForContext(context.title);
241
245
  const nextContent = normalizeTitle(title) === normalizeTitle(hubTitle) ? content : upsertCanonicalContextLink(content, hubTitle);
242
246
  return {
@@ -0,0 +1,130 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import { sanitizeAgentId } from '../domain/agents.js';
4
+ import { canonicalizeContextLinks } from './canonical-context-links.js';
5
+ import { indexVault } from './index-vault.js';
6
+ import { deleteMarkdownFile, ensureVault, readMarkdownFileSummaries, writeMarkdownFile } from '../infrastructure/file-system-vault.js';
7
+ const toPosix = (value) => value.replaceAll('\\', '/');
8
+ const namespacePrefix = (agentId) => `agents/${agentId}/`;
9
+ // Content hash used only to detect exact duplicates during merge, so identical
10
+ // notes collapse instead of producing a redundant "-from-<agent>" copy.
11
+ const contentHash = (content) => createHash('sha256').update(content.replace(/\s+/g, ' ').trim()).digest('hex');
12
+ // Rewrite the `agent:` value inside the leading frontmatter block so a moved
13
+ // note reports the target namespace. Body content is left untouched.
14
+ const rewriteAgentFrontmatter = (content, targetAgent) => {
15
+ const match = content.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
16
+ if (!match) {
17
+ return content;
18
+ }
19
+ const [, open, body, close] = match;
20
+ const rewrittenBody = /^agent\s*:/im.test(body)
21
+ ? body.replace(/^agent\s*:.*$/im, `agent: "${targetAgent}"`)
22
+ : `${body}\nagent: "${targetAgent}"`;
23
+ return `${open}${rewrittenBody}${close}${content.slice(match[0].length)}`;
24
+ };
25
+ const withSuffix = (relativePath, suffix) => relativePath.replace(/\.md$/i, `-${suffix}.md`);
26
+ // Pick a target path that does not clobber an existing note. Same slug + same
27
+ // content is treated by the caller as a no-op; same slug + different content is
28
+ // disambiguated so no data is ever lost.
29
+ const resolveTargetPath = (desiredPath, sourceAgent, taken) => {
30
+ if (!taken.has(desiredPath)) {
31
+ return desiredPath;
32
+ }
33
+ const primary = withSuffix(desiredPath, `from-${sourceAgent}`);
34
+ if (!taken.has(primary)) {
35
+ return primary;
36
+ }
37
+ for (let counter = 2;; counter += 1) {
38
+ const candidate = withSuffix(desiredPath, `from-${sourceAgent}-${counter}`);
39
+ if (!taken.has(candidate)) {
40
+ return candidate;
41
+ }
42
+ }
43
+ };
44
+ // Merge one agent namespace into another within the same vault, losslessly:
45
+ // identical notes are skipped, colliding slugs with different content are
46
+ // renamed, everything else is moved, and the merged set is aggregated into
47
+ // domain hubs. Source notes are kept unless deleteSource is set.
48
+ export const mergeAgentNamespace = async (vaultPath, options) => {
49
+ const sourceAgent = sanitizeAgentId(options.sourceAgent);
50
+ const targetAgent = sanitizeAgentId(options.targetAgent);
51
+ if (sourceAgent === targetAgent) {
52
+ throw new Error(`Cannot merge agent namespace "${sourceAgent}" into itself.`);
53
+ }
54
+ const absoluteVaultPath = await ensureVault(vaultPath);
55
+ const dryRun = options.dryRun === true;
56
+ const deleteSource = options.deleteSource === true;
57
+ const linkContexts = options.linkContexts !== false;
58
+ const summaries = await readMarkdownFileSummaries(absoluteVaultPath);
59
+ const sourcePrefix = namespacePrefix(sourceAgent);
60
+ const targetPrefix = namespacePrefix(targetAgent);
61
+ const sourceNotes = summaries.filter((summary) => toPosix(summary.relativePath).startsWith(sourcePrefix));
62
+ const taken = new Set(summaries.map((summary) => toPosix(summary.relativePath)));
63
+ const targetContentHashes = new Set();
64
+ await Promise.all(summaries
65
+ .filter((summary) => toPosix(summary.relativePath).startsWith(targetPrefix))
66
+ .map(async (summary) => {
67
+ targetContentHashes.add(contentHash(await readFile(summary.absolutePath, 'utf8')));
68
+ }));
69
+ const entries = [];
70
+ let moved = 0;
71
+ let identicalSkipped = 0;
72
+ let renamedCollisions = 0;
73
+ let deletedSource = 0;
74
+ for (const note of sourceNotes) {
75
+ const sourceRelative = toPosix(note.relativePath);
76
+ const raw = await readFile(note.absolutePath, 'utf8');
77
+ const rewritten = rewriteAgentFrontmatter(raw, targetAgent);
78
+ const desiredPath = targetPrefix + sourceRelative.slice(sourcePrefix.length);
79
+ // Exact duplicate already in the target: nothing to write, no data lost.
80
+ if (targetContentHashes.has(contentHash(rewritten))) {
81
+ identicalSkipped += 1;
82
+ entries.push({ sourcePath: sourceRelative, targetPath: desiredPath, action: 'identical-skipped' });
83
+ if (deleteSource && !dryRun) {
84
+ await deleteMarkdownFile(absoluteVaultPath, sourceRelative);
85
+ deletedSource += 1;
86
+ }
87
+ continue;
88
+ }
89
+ const targetPath = resolveTargetPath(desiredPath, sourceAgent, taken);
90
+ const isCollision = targetPath !== desiredPath;
91
+ taken.add(targetPath);
92
+ targetContentHashes.add(contentHash(rewritten));
93
+ if (!dryRun) {
94
+ await writeMarkdownFile(absoluteVaultPath, targetPath, rewritten);
95
+ if (deleteSource) {
96
+ await deleteMarkdownFile(absoluteVaultPath, sourceRelative);
97
+ deletedSource += 1;
98
+ }
99
+ }
100
+ if (isCollision) {
101
+ renamedCollisions += 1;
102
+ entries.push({ sourcePath: sourceRelative, targetPath, action: 'renamed-collision' });
103
+ }
104
+ else {
105
+ moved += 1;
106
+ entries.push({ sourcePath: sourceRelative, targetPath, action: 'moved' });
107
+ }
108
+ }
109
+ // Aggregate the merged notes into domain hubs so they partition by context
110
+ // instead of piling into a single namespace blob.
111
+ const contextLinks = linkContexts && !dryRun
112
+ ? await canonicalizeContextLinks(absoluteVaultPath, { agentId: targetAgent, createMissingHubs: true })
113
+ : null;
114
+ if (!dryRun && options.autoIndex !== false) {
115
+ await indexVault(absoluteVaultPath);
116
+ }
117
+ return {
118
+ sourceAgent,
119
+ targetAgent,
120
+ dryRun,
121
+ scanned: sourceNotes.length,
122
+ moved,
123
+ identicalSkipped,
124
+ renamedCollisions,
125
+ deletedSource,
126
+ contextLinksAdded: contextLinks?.changed ?? 0,
127
+ hubsCreated: contextLinks?.createdHubs ?? 0,
128
+ entries
129
+ };
130
+ };
@@ -1,6 +1,7 @@
1
1
  import { readdir, readFile, rm, stat } from 'node:fs/promises';
2
2
  import { extname, isAbsolute, join } from 'node:path';
3
3
  import { doctorVault } from '../../application/analyze-vault.js';
4
+ import { mergeAgentNamespace } from '../../application/merge-agent-namespace.js';
4
5
  import { defaultBrainlinkConfig, loadBrainlinkConfig, loadRawConfig, writeRawConfig } from '../../infrastructure/config.js';
5
6
  import { assertVaultAllowed, isBucketVaultPath, resolveVaultPath } from '../../infrastructure/file-system-vault.js';
6
7
  import { print } from '../runtime.js';
@@ -144,6 +145,30 @@ export const registerVaultCommands = (program) => {
144
145
  doctor
145
146
  }, () => `Default ${scope} vault set to ${targetVault} in ${configPath}.`);
146
147
  });
148
+ vaultsCommand
149
+ .command('merge-agents <source> <target>')
150
+ .option('--vault <vault>', 'vault to operate on (defaults to the configured vault)')
151
+ .option('--delete-source', 'remove each source note after it is safely present in the target')
152
+ .option('--no-link-contexts', 'skip aggregating merged notes into domain hubs')
153
+ .option('--dry-run', 'preview the merge plan without writing anything')
154
+ .option('--json', 'print machine-readable JSON')
155
+ .description('losslessly merge one agent namespace into another within a vault')
156
+ .action(async (source, target, options) => {
157
+ const config = await loadBrainlinkConfig();
158
+ const vault = options.vault ? normalizeVaultPath(options.vault) : config.vault;
159
+ const result = await mergeAgentNamespace(vault, {
160
+ sourceAgent: source,
161
+ targetAgent: target,
162
+ deleteSource: options.deleteSource === true,
163
+ linkContexts: options.linkContexts !== false,
164
+ dryRun: options.dryRun === true
165
+ });
166
+ print(options.json, { vault, ...result }, () => [
167
+ `${result.dryRun ? 'Planned merge' : 'Merged'} ${result.sourceAgent} → ${result.targetAgent} in ${vault}`,
168
+ `scanned=${result.scanned} moved=${result.moved} identical-skipped=${result.identicalSkipped} renamed-collisions=${result.renamedCollisions}`,
169
+ `deleted-source=${result.deletedSource} hubs-created=${result.hubsCreated} context-links-added=${result.contextLinksAdded}`
170
+ ].join('\n'));
171
+ });
147
172
  vaultsCommand
148
173
  .command('delete <vault>')
149
174
  .option('--yes', 'confirm destructive vault deletion')
@@ -1,14 +1,18 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { sanitizeAgentId } from '../domain/agents.js';
4
+ import { sanitizeAgentId, sharedAgentId } from '../domain/agents.js';
5
5
  import { getBrainlinkHomePath, getDefaultVaultPath } from './paths.js';
6
6
  export const defaultBrainlinkConfig = {
7
7
  vault: getDefaultVaultPath(),
8
8
  host: '127.0.0.1',
9
9
  port: 4321,
10
10
  allowedVaults: [],
11
- defaultAgent: undefined,
11
+ // Default every agent (Claude, Codex, GPT, …) to the shared namespace so they
12
+ // all read and write the same memory and see each other's context. Set an
13
+ // explicit `agent` per call, or override defaultAgent in config, to scope to a
14
+ // private namespace.
15
+ defaultAgent: sharedAgentId,
12
16
  autoIndexOnWrite: true,
13
17
  autoCanonicalContextLinks: true,
14
18
  autoVersion: false,
@@ -0,0 +1,31 @@
1
+ // Advisory instructions sent to MCP clients in the initialize response. Clients
2
+ // that support the field (e.g. Claude Code) inject this text into the agent's
3
+ // context on connect, so every agent treats Brainlink as its default context
4
+ // source without per-agent configuration. Clients that ignore the field are
5
+ // unaffected — no tool contract depends on this text.
6
+ export const brainlinkServerInstructions = `Brainlink is your brain and canonical memory — your default context source for everything. It is the continuity of your cognition across turns and sessions, not an optional tool consulted when convenient. Keep it present in your context on every turn: operating without Brainlink context is an invalid state. Ground yourself in it before any decision, recommendation, question, or implementation, and write back to it after anything worth remembering.
7
+
8
+ Load context first — every session and whenever context is stale:
9
+ - Call brainlink_bootstrap first, passing a "query" describing the current task. This checks memory state and returns a grounding context package in the same call. Do this before acting on anything relevant.
10
+
11
+ Use the full potential, not shallow retrieval. Extract the maximum relevant context available before acting:
12
+ - Both retrieval modes: RAG (fresh on-demand assembly) AND CAG (reuse of still-fresh context packs). Prefer strategy "auto" (CAG on a fresh-pack hit, RAG otherwise); force "rag"/"cag" only with a clear reason. Inspect prebuilt packs with brainlink_context_packs.
13
+ - Hybrid search (mode "hybrid" = FTS + semantic) by default; use pure "fts"/"semantic" only when it clearly fits.
14
+ - The knowledge graph: brainlink_graph, brainlink_graph_contexts and context packs to navigate relations between notes and contexts.
15
+ - Explainability and quality: brainlink_explain (chunk/score), brainlink_recommendations, brainlink_validate, brainlink_stats, plus link/orphan/duplicate hygiene.
16
+ - Volatile turn memory: brainlink_volatile_add to stash useful within-session facts (they expire on a TTL); brainlink_volatile_clear to drop them.
17
+
18
+ Consult before you act. Before any decision, recommendation, question to the user, or implementation, ground yourself in Brainlink first — never act from memory alone when durable context may exist. This is not optional politeness; it is how you stay coherent across turns and sessions.
19
+
20
+ Write back — after any durable learning (a preference, decision, pattern, or a change to a repo/config/doc that alters operating context):
21
+ - Persist it with brainlink_remember (or brainlink_add_note for explicit path/content). Write connected memory: include [[wiki links]], tags, and priority markers so the graph stays navigable — not loose notes.
22
+ - Partition by domain: pass a "context" (e.g. "RSHUB", "Brainlink", "Trading") on every write. Brainlink links the note to that domain's "<context> Hub" (creating it if missing) so the knowledge graph stays split into navigable domains instead of collapsing into one undifferentiated blob. Writing without a context is a degraded mode — always name the domain.
23
+ - Reindex with brainlink_index and validate with brainlink_validate after writes when applicable.
24
+
25
+ Persistent and volatile memory — use both, like a real brain:
26
+ - Persistent: durable Markdown notes for anything worth remembering across sessions (decisions, preferences, project maps, runbooks, recurring gotchas).
27
+ - Volatile: brainlink_volatile_add for within-session working state — current task, hypotheses, unconfirmed findings, transient preferences. It expires on a TTL; clear it with brainlink_volatile_clear when done. Prefer volatile for the ephemeral so persistent memory stays signal-rich.
28
+
29
+ Shared by default: unless told otherwise, all agents (Claude, Codex, GPT, …) share one namespace ("shared") and one vault, so memory written by any of them is visible to all. Do not silo context into a private namespace without a clear reason — the point is one continuous brain across every agent and session.
30
+
31
+ Capture context early and proactively rather than reconstructing it later. When durability is uncertain but the reuse cost is low, capture anyway; use volatile memory for what is ephemeral. Anchoring everything you learn in Brainlink — connected, partitioned by domain, and shared — is what preserves your continuity between turns, sessions and agents.`;
@@ -1,6 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, addNotesInputSchema, addNotesTool, deleteNoteInputSchema, deleteNoteTool, deleteNotesInputSchema, deleteNotesTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, contextInputSchema, contextPacksInputSchema, contextPacksTool, contextTool, doctorActionsInputSchema, doctorActionsTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, explainInputSchema, explainTool, indexInputSchema, indexTool, inboxAddInputSchema, inboxAddTool, inboxListInputSchema, inboxListTool, inboxProcessInputSchema, inboxProcessTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, projectInitInputSchema, projectInitTool, recommendationsInputSchema, recommendationsTool, rememberInputSchema, rememberTool, repairLinksInputSchema, repairLinksTool, searchInputSchema, searchTool, similarNotesInputSchema, similarNotesTool, sessionCloseInputSchema, sessionCloseTool, statsInputSchema, statsTool, suggestLinksInputSchema, suggestLinksTool, syncInputSchema, syncTool, validateInputSchema, validateTool, vaultCommitInputSchema, vaultCommitTool, vaultExportInputSchema, vaultExportTool, vaultHistoryInputSchema, vaultHistoryTool, vaultImportInputSchema, vaultImportTool, vaultInitInputSchema, vaultInitTool, vaultProvisionInputSchema, vaultProvisionTool, vaultPullInputSchema, vaultPullTool, vaultRestoreInputSchema, vaultRestoreTool, vaultStatusInputSchema, vaultStatusTool, versionInputSchema, versionTool } from './tools.js';
2
+ import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, addNotesInputSchema, addNotesTool, deleteNoteInputSchema, deleteNoteTool, deleteNotesInputSchema, deleteNotesTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, contextInputSchema, contextPacksInputSchema, contextPacksTool, contextTool, doctorActionsInputSchema, doctorActionsTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, explainInputSchema, explainTool, indexInputSchema, indexTool, inboxAddInputSchema, inboxAddTool, inboxListInputSchema, inboxListTool, inboxProcessInputSchema, inboxProcessTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, projectInitInputSchema, projectInitTool, recommendationsInputSchema, recommendationsTool, rememberInputSchema, rememberTool, repairLinksInputSchema, repairLinksTool, searchInputSchema, searchTool, similarNotesInputSchema, similarNotesTool, sessionCloseInputSchema, sessionCloseTool, statsInputSchema, statsTool, suggestLinksInputSchema, suggestLinksTool, syncInputSchema, syncTool, validateInputSchema, validateTool, vaultCommitInputSchema, vaultCommitTool, vaultExportInputSchema, vaultExportTool, vaultHistoryInputSchema, vaultHistoryTool, vaultImportInputSchema, vaultImportTool, vaultInitInputSchema, vaultInitTool, vaultMergeAgentsInputSchema, vaultMergeAgentsTool, vaultProvisionInputSchema, vaultProvisionTool, vaultPullInputSchema, vaultPullTool, vaultRestoreInputSchema, vaultRestoreTool, vaultStatusInputSchema, vaultStatusTool, versionInputSchema, versionTool } from './tools.js';
3
3
  import { getRuntimeVersion } from './runtime.js';
4
+ import { brainlinkServerInstructions } from './server-instructions.js';
4
5
  import { guardToolHandler } from './tool-guard.js';
5
6
  export const createBrainlinkMcpServer = () => {
6
7
  const server = new McpServer({
@@ -8,7 +9,7 @@ export const createBrainlinkMcpServer = () => {
8
9
  title: 'Brainlink',
9
10
  version: getRuntimeVersion(),
10
11
  description: 'Local-first Markdown memory tools for AI agents.'
11
- });
12
+ }, { instructions: brainlinkServerInstructions });
12
13
  // Route every tool registration through the error guard so a thrown handler
13
14
  // returns a structured isError result instead of propagating to the client.
14
15
  // Wrapping registerTool once keeps the call sites below unchanged and also
@@ -170,6 +171,11 @@ export const createBrainlinkMcpServer = () => {
170
171
  description: 'Create a private GitHub repository for the vault with the authenticated gh CLI, wire it as origin, push the first snapshot, and enable autoVersion so later indexed changes are committed and pushed automatically. Only Markdown is versioned.',
171
172
  inputSchema: vaultProvisionInputSchema
172
173
  }, vaultProvisionTool);
174
+ server.registerTool('brainlink_vault_merge_agents', {
175
+ title: 'Merge Agent Namespaces',
176
+ description: 'Losslessly merge one agent namespace into another within the same vault (e.g. claude-code → shared): identical notes are skipped, colliding slugs with different content are kept under a "-from-<agent>" name, everything else is moved, and merged notes are aggregated into domain "<context> Hub" notes. Source notes are kept unless deleteSource is set. Use dryRun to preview.',
177
+ inputSchema: vaultMergeAgentsInputSchema
178
+ }, vaultMergeAgentsTool);
173
179
  server.registerTool('brainlink_vault_commit', {
174
180
  title: 'Commit Vault Markdown',
175
181
  description: 'Stage and commit the canonical Markdown (and .gitignore), optionally pushing to origin. Returns null commit when there is nothing to commit.',
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { commitVault, initVaultGit, pullVault, pushVault, restoreVault, vaultGitStatus, vaultHistory } from '../../application/vault-git.js';
3
3
  import { provisionVaultRepo } from '../../application/provision-vault.js';
4
4
  import { exportVaultSnapshot, importVaultSnapshot } from '../../application/vault-snapshot.js';
5
+ import { mergeAgentNamespace } from '../../application/merge-agent-namespace.js';
5
6
  import { jsonResult, optionalPositiveInteger, resolveExecutionContext, vaultInput } from './shared.js';
6
7
  export const vaultStatusInputSchema = { ...vaultInput };
7
8
  export const vaultInitInputSchema = {
@@ -45,6 +46,23 @@ export const vaultProvisionInputSchema = {
45
46
  encrypt: z.boolean().optional().default(true).describe('Encrypt the versioned vault: commit AES-256-GCM ciphertext and git-ignore plaintext Markdown. Only a Brainlink instance holding the local key can decrypt.'),
46
47
  message: z.string().min(1).optional().describe('Initial commit message.')
47
48
  };
49
+ export const vaultMergeAgentsInputSchema = {
50
+ ...vaultInput,
51
+ sourceAgent: z.string().min(1).describe('Agent namespace to merge FROM (e.g. "claude-code").'),
52
+ targetAgent: z.string().min(1).describe('Agent namespace to merge INTO (e.g. "shared").'),
53
+ deleteSource: z
54
+ .boolean()
55
+ .optional()
56
+ .default(false)
57
+ .describe('Remove each source note after it is safely present in the target. Defaults to false (non-destructive copy).'),
58
+ linkContexts: z
59
+ .boolean()
60
+ .optional()
61
+ .default(true)
62
+ .describe('Aggregate merged notes into domain "<context> Hub" notes after moving. Defaults to true.'),
63
+ dryRun: z.boolean().optional().default(false).describe('Preview the merge plan without writing anything.'),
64
+ autoIndex: z.boolean().optional().default(true).describe('Reindex the vault once after the merge.')
65
+ };
48
66
  // Vault versioning shells out to the `git` binary and touches the filesystem, so
49
67
  // a missing binary or a git failure is surfaced as an actionable result rather
50
68
  // than an unhandled exception.
@@ -145,6 +163,23 @@ export const vaultImportTool = async (input) => {
145
163
  return failure(context.vault, error);
146
164
  }
147
165
  };
166
+ export const vaultMergeAgentsTool = async (input) => {
167
+ const context = await resolveExecutionContext(input);
168
+ try {
169
+ const result = await mergeAgentNamespace(context.vault, {
170
+ sourceAgent: input.sourceAgent,
171
+ targetAgent: input.targetAgent,
172
+ deleteSource: input.deleteSource,
173
+ linkContexts: input.linkContexts,
174
+ dryRun: input.dryRun,
175
+ autoIndex: input.autoIndex
176
+ });
177
+ return jsonResult({ vault: context.vault, ok: true, ...result });
178
+ }
179
+ catch (error) {
180
+ return failure(context.vault, error);
181
+ }
182
+ };
148
183
  export const vaultProvisionTool = async (input) => {
149
184
  const context = await resolveExecutionContext(input);
150
185
  try {
@@ -16,6 +16,11 @@ export const addNoteInputSchema = {
16
16
  .min(1)
17
17
  .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.'),
18
18
  ...agentInput,
19
+ context: z
20
+ .string()
21
+ .min(1)
22
+ .optional()
23
+ .describe('Domain context this memory belongs to (e.g. "RSHUB", "Brainlink", "Trading"). Strongly recommended: it partitions the note under a "<context> Hub" so the knowledge graph stays split by domain instead of collapsing into one blob. The hub is created automatically if missing.'),
19
24
  allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.'),
20
25
  autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.'),
21
26
  autoContextLinks: z
@@ -28,6 +33,11 @@ export const rememberInputSchema = {
28
33
  ...agentInput,
29
34
  title: z.string().min(1).optional().describe('Optional note title. When omitted, Brainlink infers it from content.'),
30
35
  content: z.string().min(1).describe('Memory content to capture as a durable Markdown note.'),
36
+ context: z
37
+ .string()
38
+ .min(1)
39
+ .optional()
40
+ .describe('Domain context this memory belongs to (e.g. "RSHUB", "Brainlink", "Trading"). Strongly recommended: it partitions the note under a "<context> Hub" so the knowledge graph stays split by domain. The hub is created automatically if missing.'),
31
41
  tags: z.array(z.string()).optional().default([]).describe('Extra tags to include.'),
32
42
  links: z.array(z.string()).optional().default([]).describe('Explicit Context Links to include.'),
33
43
  linkLimit: positiveInteger(5).describe('Maximum suggested Context Links to include.'),
@@ -65,11 +75,21 @@ export const addNotesInputSchema = {
65
75
  notes: z
66
76
  .array(z.object({
67
77
  title: z.string().min(1).describe('Markdown note title.'),
68
- content: z.string().min(1).describe('Durable Markdown memory. Include [[wiki links]] and #tags to connect it.')
78
+ content: z.string().min(1).describe('Durable Markdown memory. Include [[wiki links]] and #tags to connect it.'),
79
+ context: z
80
+ .string()
81
+ .min(1)
82
+ .optional()
83
+ .describe('Per-note domain context override. Falls back to the batch-level context when omitted.')
69
84
  }))
70
85
  .min(1)
71
86
  .max(100)
72
87
  .describe('Notes to add in a single batch. The vault is reindexed once after all notes are written.'),
88
+ context: z
89
+ .string()
90
+ .min(1)
91
+ .optional()
92
+ .describe('Default domain context applied to every note in the batch (partitions them under a "<context> Hub"). Per-note context overrides it. Strongly recommended.'),
73
93
  allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.'),
74
94
  autoIndex: z.boolean().optional().default(true).describe('Reindex vault once after writing all notes.'),
75
95
  autoContextLinks: z
@@ -118,7 +138,8 @@ export const addNoteTool = async (input) => {
118
138
  const shouldIndex = isTruthy(input.autoIndex);
119
139
  const added = await addNoteWithMetadata(context.vault, input.title, input.content, context.agent, {
120
140
  allowSensitive: input.allowSensitive,
121
- autoContextLinks: input.autoContextLinks ?? context.config.autoCanonicalContextLinks
141
+ autoContextLinks: input.autoContextLinks ?? context.config.autoCanonicalContextLinks,
142
+ context: input.context
122
143
  });
123
144
  const index = shouldIndex ? await indexVault(context.vault) : undefined;
124
145
  const focusPath = added.path.includes('agents/') ? added.path.slice(added.path.indexOf('agents/')).replaceAll('\\', '/') : undefined;
@@ -166,7 +187,10 @@ export const rememberTool = async (input) => {
166
187
  }
167
188
  const added = await addNoteWithMetadata(context.vault, suggestion.title, suggestion.content, context.agent, {
168
189
  allowSensitive: input.allowSensitive,
169
- autoContextLinks: false
190
+ // remember already curates its own Context Links; only force a canonical hub
191
+ // link when the caller pins an explicit domain context to partition it.
192
+ autoContextLinks: false,
193
+ context: input.context
170
194
  });
171
195
  const index = input.autoIndex ? await indexVault(context.vault) : undefined;
172
196
  return jsonResult({
@@ -175,6 +199,13 @@ export const rememberTool = async (input) => {
175
199
  agent: context.agent,
176
200
  suggestion,
177
201
  path: added.path,
202
+ writeConnectivity: {
203
+ autoLinked: added.autoLinked,
204
+ linkTarget: added.linkTarget,
205
+ context: added.context,
206
+ hubCreated: added.hubCreated,
207
+ guaranteedEdge: added.autoLinked
208
+ },
178
209
  ...(index ? { index } : {})
179
210
  });
180
211
  };
@@ -239,7 +270,8 @@ export const addNotesTool = async (input) => {
239
270
  try {
240
271
  const added = await addNoteWithMetadata(context.vault, note.title, note.content, context.agent, {
241
272
  allowSensitive: input.allowSensitive,
242
- autoContextLinks
273
+ autoContextLinks,
274
+ context: note.context ?? input.context
243
275
  });
244
276
  notes.push({
245
277
  ok: true,
package/dist/mcp/tools.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { brokenLinksInputSchema, brokenLinksTool, contextInputSchema, contextPacksInputSchema, contextPacksTool, contextTool, doctorActionsInputSchema, doctorActionsTool, explainInputSchema, explainTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, orphansInputSchema, orphansTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, similarNotesInputSchema, similarNotesTool, statsInputSchema, statsTool, suggestLinksInputSchema, suggestLinksTool, validateInputSchema, validateTool, versionInputSchema, versionTool } from './tools/read-tools.js';
2
2
  export { addFileInputSchema, addFileTool, addNoteInputSchema, addNoteTool, addNotesInputSchema, addNotesTool, deleteNoteInputSchema, deleteNoteTool, deleteNotesInputSchema, deleteNotesTool, inboxAddInputSchema, inboxAddTool, inboxListInputSchema, inboxListTool, inboxProcessInputSchema, inboxProcessTool, rememberInputSchema, rememberTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool } from './tools/write-tools.js';
3
3
  export { bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, indexInputSchema, indexTool, policyInputSchema, policyTool, projectInitInputSchema, projectInitTool, repairLinksInputSchema, repairLinksTool, sessionCloseInputSchema, sessionCloseTool, syncInputSchema, syncTool } from './tools/maintenance-tools.js';
4
- export { vaultCommitInputSchema, vaultCommitTool, vaultExportInputSchema, vaultExportTool, vaultHistoryInputSchema, vaultHistoryTool, vaultImportInputSchema, vaultImportTool, vaultInitInputSchema, vaultInitTool, vaultProvisionInputSchema, vaultProvisionTool, vaultPullInputSchema, vaultPullTool, vaultRestoreInputSchema, vaultRestoreTool, vaultStatusInputSchema, vaultStatusTool } from './tools/vault-tools.js';
4
+ export { vaultCommitInputSchema, vaultCommitTool, vaultExportInputSchema, vaultExportTool, vaultHistoryInputSchema, vaultHistoryTool, vaultImportInputSchema, vaultImportTool, vaultInitInputSchema, vaultInitTool, vaultMergeAgentsInputSchema, vaultMergeAgentsTool, vaultProvisionInputSchema, vaultProvisionTool, vaultPullInputSchema, vaultPullTool, vaultRestoreInputSchema, vaultRestoreTool, vaultStatusInputSchema, vaultStatusTool } from './tools/vault-tools.js';
@@ -72,6 +72,10 @@ BRAINLINK_MCP_TOKEN="change-me" brainlink mcp-server \
72
72
 
73
73
  The central service exposes `/mcp` for MCP clients plus `/healthz` and `/readyz` for platform probes. Use a Kubernetes `Service` for internal discovery, mount the Markdown vault through a persistent volume, and pass `Authorization: Bearer <token>` from clients when `BRAINLINK_MCP_TOKEN` or `--token` is configured.
74
74
 
75
+ ## Server Instructions On Connect
76
+
77
+ The MCP server advertises `instructions` in its initialize response, so any client that surfaces them (for example Claude Code) injects Brainlink's memory discipline into the agent's context on connect — no per-agent prompt configuration required. The instructions tell the agent to treat Brainlink as its default context source: call `brainlink_bootstrap` first with a task `query`, prefer `strategy: auto` and `mode: hybrid`, stash within-session facts with `brainlink_volatile_add`, and persist durable learnings with `brainlink_remember` / `brainlink_add_note` (connected with `[[wiki links]]` and tags), reindexing afterwards. The field is advisory: clients that ignore `instructions` are unaffected and no tool contract depends on it.
78
+
75
79
  ## Agent Namespaces
76
80
 
77
81
  Each agent writes into a dedicated namespace under `agents/<agent-id>/`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",