@andespindola/brainlink 1.3.0 → 1.5.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 +14 -0
- package/COPYRIGHT.md +1 -1
- package/README.md +7 -6
- package/assets/brainlink-logo.png +0 -0
- package/assets/brainlink-logo.svg +96 -21
- package/assets/fonts/CrowquillMono-Bold.woff2 +0 -0
- package/assets/fonts/CrowquillMono-BoldItalic.woff2 +0 -0
- package/assets/fonts/CrowquillMono-Italic.woff2 +0 -0
- package/assets/fonts/CrowquillMono-Regular.woff2 +0 -0
- package/dist/application/add-note.js +5 -2
- package/dist/application/canonical-context-links.js +13 -9
- package/dist/application/frontend/client/rendering.js +65 -21
- package/dist/application/frontend/client/scope-theme.js +2 -2
- package/dist/application/frontend/client-css.js +40 -13
- package/dist/application/frontend/client-html.js +1 -1
- package/dist/application/frontend/client-render-worker-js.js +7 -5
- package/dist/application/get-graph-stream-chunk.js +18 -9
- package/dist/application/merge-agent-namespace.js +130 -0
- package/dist/cli/commands/vault-commands.js +25 -0
- package/dist/infrastructure/config.js +6 -2
- package/dist/mcp/server-instructions.js +10 -1
- package/dist/mcp/server.js +6 -1
- package/dist/mcp/tools/vault-tools.js +35 -0
- package/dist/mcp/tools/write-tools.js +36 -4
- package/dist/mcp/tools.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
- **Graph shows every node at any zoom.** The viewport stream previously replaced nodes with per-context clusters below a zoom threshold, so nodes vanished when you zoomed out and reappeared on approach. Graphs that fit a cap (6000 nodes) now render every node in near mode at all zoom levels; larger graphs keep the far/mid/near level-of-detail.
|
|
6
|
+
- **Graph visual refresh.** Bigger nodes with a screen-space radius floor so they stay visible dots at any zoom, brighter and more legible edges, and labels decluttered by collision (placed by importance, skipping overlaps) so density scales smoothly with zoom instead of flipping on at a hard threshold. Labels reposition live during interaction (no flicker) and are throttled to keep pan/zoom fluid.
|
|
7
|
+
- **Crowquill Mono UI font.** The graph UI now ships and uses Crowquill Mono, inlined as base64 woff2 in the served CSS so it is self-contained (graceful monospace fallback if unavailable). The header metrics were made compact so the Notes/Links counters no longer wrap.
|
|
8
|
+
- **New logo and npm image fix.** The README logo is now a brain drawn as a knowledge graph, exported to PNG and referenced by absolute URL so it renders on the npm package page (npm does not render SVG). Copyright holder set to Anderson Espindola.
|
|
9
|
+
|
|
10
|
+
## 1.4.0
|
|
11
|
+
|
|
12
|
+
- **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.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
|
|
3
17
|
## 1.3.0
|
|
4
18
|
|
|
5
19
|
- **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.
|
package/COPYRIGHT.md
CHANGED
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="
|
|
2
|
+
<img src="https://raw.githubusercontent.com/andersonflima/brainlink/main/assets/brainlink-logo.png" alt="Brainlink" width="720">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# Brainlink
|
|
@@ -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`
|
|
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.
|
|
@@ -1455,7 +1456,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
1455
1456
|
## License
|
|
1456
1457
|
|
|
1457
1458
|
MIT. See [LICENSE](LICENSE).
|
|
1458
|
-
Copyright (c) 2026
|
|
1459
|
+
Copyright (c) 2026 Anderson Espindola. See [COPYRIGHT.md](COPYRIGHT.md).
|
|
1459
1460
|
|
|
1460
1461
|
### Memory Optimization Loop (1-7)
|
|
1461
1462
|
|
|
Binary file
|
|
@@ -1,25 +1,100 @@
|
|
|
1
|
-
<svg width="
|
|
1
|
+
<svg width="1240" height="440" viewBox="0 0 1240 440" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
|
|
2
2
|
<title id="title">Brainlink</title>
|
|
3
|
-
<desc id="desc">Brainlink logo
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
<desc id="desc">Brainlink logo: a brain drawn as a knowledge graph of connected memory nodes, for AI agents.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<radialGradient id="bg" cx="26%" cy="30%" r="95%">
|
|
6
|
+
<stop offset="0%" stop-color="#0e2233"/>
|
|
7
|
+
<stop offset="55%" stop-color="#0a1926"/>
|
|
8
|
+
<stop offset="100%" stop-color="#06111b"/>
|
|
9
|
+
</radialGradient>
|
|
10
|
+
<linearGradient id="brainStroke" x1="0" y1="0" x2="1" y2="1">
|
|
11
|
+
<stop offset="0%" stop-color="#5aa8ff"/>
|
|
12
|
+
<stop offset="100%" stop-color="#59d0dd"/>
|
|
13
|
+
</linearGradient>
|
|
14
|
+
<linearGradient id="wordmark" x1="0" y1="0" x2="0" y2="1">
|
|
15
|
+
<stop offset="0%" stop-color="#eef5ff"/>
|
|
16
|
+
<stop offset="100%" stop-color="#bcd4f2"/>
|
|
17
|
+
</linearGradient>
|
|
18
|
+
<filter id="glow" x="-60%" y="-60%" width="220%" height="220%">
|
|
19
|
+
<feGaussianBlur stdDeviation="5" result="b"/>
|
|
20
|
+
<feMerge>
|
|
21
|
+
<feMergeNode in="b"/>
|
|
22
|
+
<feMergeNode in="SourceGraphic"/>
|
|
23
|
+
</feMerge>
|
|
24
|
+
</filter>
|
|
25
|
+
</defs>
|
|
26
|
+
|
|
27
|
+
<rect width="1240" height="440" rx="34" fill="url(#bg)"/>
|
|
28
|
+
|
|
29
|
+
<!-- Brain drawn as a graph -->
|
|
30
|
+
<g transform="translate(70 26)" filter="url(#glow)">
|
|
31
|
+
<!-- brain silhouette -->
|
|
32
|
+
<path d="M270,58 C232,40 190,50 168,84 C138,74 110,96 116,130 C88,140 82,180 108,200
|
|
33
|
+
C86,214 92,256 122,266 C120,306 158,336 202,326 C224,352 286,352 312,326
|
|
34
|
+
C356,340 398,312 392,270 C420,258 424,214 398,196 C420,180 416,138 386,132
|
|
35
|
+
C392,98 360,72 322,84 C308,64 288,58 270,58 Z"
|
|
36
|
+
fill="#5aa8ff" fill-opacity="0.05" stroke="url(#brainStroke)" stroke-width="3" stroke-opacity="0.65"/>
|
|
37
|
+
<!-- central sulcus + a few gyri -->
|
|
38
|
+
<path d="M268,72 C246,120 300,150 270,196 C242,240 300,272 272,318"
|
|
39
|
+
fill="none" stroke="#5aa8ff" stroke-width="2.2" stroke-opacity="0.32"/>
|
|
40
|
+
<path d="M150,110 C176,128 176,156 150,176" fill="none" stroke="#5aa8ff" stroke-width="2" stroke-opacity="0.2"/>
|
|
41
|
+
<path d="M360,120 C334,140 336,168 362,186" fill="none" stroke="#5aa8ff" stroke-width="2" stroke-opacity="0.2"/>
|
|
42
|
+
<path d="M196,250 C222,262 250,258 268,240" fill="none" stroke="#5aa8ff" stroke-width="2" stroke-opacity="0.2"/>
|
|
43
|
+
|
|
44
|
+
<!-- graph edges -->
|
|
45
|
+
<g stroke="#7fb2e6" stroke-width="2" stroke-opacity="0.42" stroke-linecap="round">
|
|
46
|
+
<line x1="150" y1="120" x2="210" y2="96"/>
|
|
47
|
+
<line x1="210" y1="96" x2="250" y2="150"/>
|
|
48
|
+
<line x1="150" y1="120" x2="190" y2="170"/>
|
|
49
|
+
<line x1="190" y1="170" x2="250" y2="150"/>
|
|
50
|
+
<line x1="190" y1="170" x2="140" y2="200"/>
|
|
51
|
+
<line x1="140" y1="200" x2="172" y2="248"/>
|
|
52
|
+
<line x1="172" y1="248" x2="225" y2="228"/>
|
|
53
|
+
<line x1="225" y1="228" x2="250" y2="150"/>
|
|
54
|
+
<line x1="172" y1="248" x2="208" y2="298"/>
|
|
55
|
+
<line x1="208" y1="298" x2="266" y2="262"/>
|
|
56
|
+
<line x1="266" y1="262" x2="225" y2="228"/>
|
|
57
|
+
<line x1="250" y1="150" x2="270" y2="205"/>
|
|
58
|
+
<line x1="270" y1="205" x2="315" y2="200"/>
|
|
59
|
+
<line x1="300" y1="112" x2="340" y2="152"/>
|
|
60
|
+
<line x1="300" y1="112" x2="250" y2="150"/>
|
|
61
|
+
<line x1="340" y1="152" x2="315" y2="200"/>
|
|
62
|
+
<line x1="315" y1="200" x2="362" y2="236"/>
|
|
63
|
+
<line x1="362" y1="236" x2="345" y2="292"/>
|
|
64
|
+
<line x1="345" y1="292" x2="300" y2="300"/>
|
|
65
|
+
<line x1="300" y1="300" x2="266" y2="262"/>
|
|
66
|
+
<line x1="300" y1="300" x2="208" y2="298"/>
|
|
67
|
+
<line x1="270" y1="205" x2="300" y2="300"/>
|
|
68
|
+
<line x1="270" y1="205" x2="266" y2="262"/>
|
|
69
|
+
</g>
|
|
70
|
+
|
|
71
|
+
<!-- graph nodes -->
|
|
72
|
+
<g>
|
|
73
|
+
<circle cx="270" cy="205" r="12" fill="#5aa8ff"/>
|
|
74
|
+
<circle cx="210" cy="96" r="8" fill="#59d0dd"/>
|
|
75
|
+
<circle cx="150" cy="120" r="7" fill="#5aa8ff"/>
|
|
76
|
+
<circle cx="250" cy="150" r="10" fill="#7cb6ff"/>
|
|
77
|
+
<circle cx="190" cy="170" r="7" fill="#5ecf92"/>
|
|
78
|
+
<circle cx="140" cy="200" r="7" fill="#5aa8ff"/>
|
|
79
|
+
<circle cx="172" cy="248" r="8" fill="#a88fff"/>
|
|
80
|
+
<circle cx="225" cy="228" r="9" fill="#5aa8ff"/>
|
|
81
|
+
<circle cx="208" cy="298" r="7" fill="#ffb65c"/>
|
|
82
|
+
<circle cx="266" cy="262" r="8" fill="#59d0dd"/>
|
|
83
|
+
<circle cx="300" cy="112" r="8" fill="#ff7dac"/>
|
|
84
|
+
<circle cx="340" cy="152" r="7" fill="#5aa8ff"/>
|
|
85
|
+
<circle cx="315" cy="200" r="10" fill="#5ecf92"/>
|
|
86
|
+
<circle cx="362" cy="236" r="7" fill="#7cb6ff"/>
|
|
87
|
+
<circle cx="345" cy="292" r="8" fill="#5aa8ff"/>
|
|
88
|
+
<circle cx="300" cy="300" r="9" fill="#ff8f6a"/>
|
|
89
|
+
</g>
|
|
18
90
|
</g>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
<
|
|
91
|
+
|
|
92
|
+
<!-- wordmark -->
|
|
93
|
+
<g transform="translate(560 0)" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
|
|
94
|
+
<text x="0" y="212" fill="url(#wordmark)" font-size="118" font-weight="780" letter-spacing="-3">Brainlink</text>
|
|
95
|
+
<text x="4" y="262" fill="#7f9ad0" font-size="30" font-weight="500">Canonical memory graph for AI agents</text>
|
|
96
|
+
<line x1="6" y1="292" x2="150" y2="292" stroke="#5aa8ff" stroke-width="6" stroke-linecap="round"/>
|
|
97
|
+
<line x1="166" y1="292" x2="238" y2="292" stroke="#59d0dd" stroke-width="6" stroke-linecap="round"/>
|
|
98
|
+
<line x1="254" y1="292" x2="298" y2="292" stroke="#5ecf92" stroke-width="6" stroke-linecap="round"/>
|
|
24
99
|
</g>
|
|
25
100
|
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
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 {
|
|
@@ -23,8 +23,8 @@ const drawFallback = () => {
|
|
|
23
23
|
nodeById.set(nodes[i][0], nodes[i])
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
ctx2dFallback.strokeStyle = 'rgba(
|
|
27
|
-
ctx2dFallback.lineWidth = 1
|
|
26
|
+
ctx2dFallback.strokeStyle = 'rgba(158,188,220,0.32)'
|
|
27
|
+
ctx2dFallback.lineWidth = 1.2
|
|
28
28
|
for (let i = 0; i < edges.length; i += 1) {
|
|
29
29
|
const edge = edges[i]
|
|
30
30
|
const source = nodeById.get(edge[0])
|
|
@@ -43,7 +43,7 @@ const drawFallback = () => {
|
|
|
43
43
|
const p = worldToScreen(node[2], node[3])
|
|
44
44
|
const selected = state.selectedNodeId === node[0]
|
|
45
45
|
const color = segmentColor(node[5] || node[4] || node[1])
|
|
46
|
-
const radius = Math.max(3.
|
|
46
|
+
const radius = Math.max(3.8, Math.min(18, 5.5 + node[7] * 0.7))
|
|
47
47
|
|
|
48
48
|
ctx2dFallback.beginPath()
|
|
49
49
|
ctx2dFallback.fillStyle = selected ? '#edf4ff' : color
|
|
@@ -179,25 +179,57 @@ const hideTooltip = () => {
|
|
|
179
179
|
|
|
180
180
|
const labelCandidates = () => {
|
|
181
181
|
const nodes = normalizeList(state.chunk.nodes)
|
|
182
|
-
const
|
|
182
|
+
const focused = state.focusedNodeIds
|
|
183
|
+
const isPinned = (node) => focused.has(node[0]) || node[0] === state.hoveredNodeId || node[0] === state.selectedNodeId
|
|
184
|
+
const margin = 80
|
|
185
|
+
|
|
186
|
+
// Collect on-screen nodes with their screen position, ranked by importance
|
|
187
|
+
// (pinned first, then relevance, deterministic id tie-break).
|
|
188
|
+
const scored = []
|
|
189
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
190
|
+
const node = nodes[i]
|
|
183
191
|
const x = Number(node?.[2])
|
|
184
192
|
const y = Number(node?.[3])
|
|
185
|
-
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
193
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue
|
|
186
194
|
const point = worldToScreen(x, y)
|
|
187
|
-
|
|
195
|
+
if (point.x < -margin || point.x > state.viewport.width + margin || point.y < -margin || point.y > state.viewport.height + margin) continue
|
|
196
|
+
scored.push({ node, x: point.x, y: point.y, pinned: isPinned(node), relevance: Number(node?.[7] ?? 0) })
|
|
197
|
+
}
|
|
198
|
+
scored.sort((left, right) => {
|
|
199
|
+
const focusDelta = (right.pinned ? 1 : 0) - (left.pinned ? 1 : 0)
|
|
200
|
+
if (focusDelta !== 0) return focusDelta
|
|
201
|
+
const relevanceDelta = right.relevance - left.relevance
|
|
202
|
+
if (relevanceDelta !== 0) return relevanceDelta
|
|
203
|
+
return String(left.node[0]).localeCompare(String(right.node[0]))
|
|
188
204
|
})
|
|
189
|
-
const shouldShowMany = state.camera.scale >= 0.72 || visible.length <= 120
|
|
190
|
-
const focused = state.focusedNodeIds
|
|
191
205
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
206
|
+
// Greedy overlap-free placement: a label is shown only if its box does not
|
|
207
|
+
// collide with an already-placed one, so density scales smoothly with zoom
|
|
208
|
+
// (few when nodes are packed, more as they spread out) instead of flipping on
|
|
209
|
+
// at a hard zoom threshold. Pinned labels are always kept.
|
|
210
|
+
const gap = 4
|
|
211
|
+
const height = 22
|
|
212
|
+
const maxLabels = state.camera.scale >= 0.72 ? 220 : 120
|
|
213
|
+
const placed = []
|
|
214
|
+
const result = []
|
|
215
|
+
for (let i = 0; i < scored.length && result.length < maxLabels; i += 1) {
|
|
216
|
+
const item = scored[i]
|
|
217
|
+
const title = String(item.node[1] || item.node[0])
|
|
218
|
+
const width = Math.min(220, 18 + title.length * 6.6)
|
|
219
|
+
const box = { left: item.x - width / 2, right: item.x + width / 2, bottom: item.y - 6, top: item.y - 6 - height }
|
|
220
|
+
let overlaps = false
|
|
221
|
+
for (let j = 0; j < placed.length; j += 1) {
|
|
222
|
+
const other = placed[j]
|
|
223
|
+
if (box.left < other.right + gap && box.right > other.left - gap && box.top < other.bottom + gap && box.bottom > other.top - gap) {
|
|
224
|
+
overlaps = true
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (overlaps && !item.pinned) continue
|
|
229
|
+
placed.push(box)
|
|
230
|
+
result.push(item.node)
|
|
231
|
+
}
|
|
232
|
+
return result
|
|
201
233
|
}
|
|
202
234
|
|
|
203
235
|
const drawLabels = () => {
|
|
@@ -268,6 +300,12 @@ const drawMiniMap = () => {
|
|
|
268
300
|
|
|
269
301
|
const shouldDeferGraphOverlays = () => state.pointer.down || performance.now() - state.lastWheelAt < 150
|
|
270
302
|
|
|
303
|
+
// Rebuilding the label overlay is the main per-frame cost. During interaction we
|
|
304
|
+
// cap it to ~25fps (the canvas keeps 60fps in the worker), which halves the DOM
|
|
305
|
+
// churn while labels still track the nodes. A trailing timer guarantees a final
|
|
306
|
+
// pass so labels settle in the right place when interaction stops.
|
|
307
|
+
const graphOverlayInteractionThrottleMs = 40
|
|
308
|
+
|
|
271
309
|
const updateGraphOverlays = () => {
|
|
272
310
|
if (state.overlayScheduled) {
|
|
273
311
|
return
|
|
@@ -275,19 +313,25 @@ const updateGraphOverlays = () => {
|
|
|
275
313
|
state.overlayScheduled = true
|
|
276
314
|
requestAnimationFrame(() => {
|
|
277
315
|
state.overlayScheduled = false
|
|
278
|
-
|
|
279
|
-
|
|
316
|
+
const interacting = shouldDeferGraphOverlays()
|
|
317
|
+
|
|
318
|
+
if (interacting && performance.now() - (state.lastOverlayDrawAt || 0) < graphOverlayInteractionThrottleMs) {
|
|
280
319
|
if (!state.overlayIdleTimer) {
|
|
281
320
|
state.overlayIdleTimer = setTimeout(() => {
|
|
282
321
|
state.overlayIdleTimer = null
|
|
283
322
|
updateGraphOverlays()
|
|
284
|
-
},
|
|
323
|
+
}, graphOverlayInteractionThrottleMs)
|
|
285
324
|
}
|
|
286
325
|
return
|
|
287
326
|
}
|
|
327
|
+
|
|
288
328
|
elements.labels?.classList.remove('is-stale')
|
|
289
329
|
drawLabels()
|
|
290
|
-
|
|
330
|
+
state.lastOverlayDrawAt = performance.now()
|
|
331
|
+
|
|
332
|
+
// The minimap is heavier and irrelevant mid-gesture, so only refresh it once
|
|
333
|
+
// the camera settles.
|
|
334
|
+
if (!interacting && state.miniMapDirty) {
|
|
291
335
|
drawMiniMap()
|
|
292
336
|
state.miniMapDirty = false
|
|
293
337
|
}
|
|
@@ -63,8 +63,8 @@ const graphTheme = {
|
|
|
63
63
|
parseColor('#c9945f'),
|
|
64
64
|
parseColor('#7cb6ff')
|
|
65
65
|
],
|
|
66
|
-
edge: [0.
|
|
67
|
-
edgeHeavy: [0.
|
|
66
|
+
edge: [0.62, 0.74, 0.86, 0.24],
|
|
67
|
+
edgeHeavy: [0.66, 0.78, 0.9, 0.46],
|
|
68
68
|
clear: parseColor('#08131d')
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
// Crowquill Mono ships with the package under assets/fonts. Inline each face as a
|
|
4
|
+
// base64 woff2 data URI so the served CSS is self-contained (no extra route, no
|
|
5
|
+
// path resolution) and works identically for a global npm install. A missing
|
|
6
|
+
// file degrades gracefully to the monospace fallback stack.
|
|
7
|
+
const embedFontFace = (weight, style, file) => {
|
|
8
|
+
try {
|
|
9
|
+
const path = fileURLToPath(new URL(`../../../assets/fonts/${file}`, import.meta.url));
|
|
10
|
+
const base64 = readFileSync(path).toString('base64');
|
|
11
|
+
return `@font-face{font-family:"Crowquill Mono";font-style:${style};font-weight:${weight};font-display:swap;src:url("data:font/woff2;base64,${base64}") format("woff2")}`;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const crowquillFontFaces = () => [
|
|
18
|
+
embedFontFace(400, 'normal', 'CrowquillMono-Regular.woff2'),
|
|
19
|
+
embedFontFace(700, 'normal', 'CrowquillMono-Bold.woff2'),
|
|
20
|
+
embedFontFace(400, 'italic', 'CrowquillMono-Italic.woff2'),
|
|
21
|
+
embedFontFace(700, 'italic', 'CrowquillMono-BoldItalic.woff2')
|
|
22
|
+
]
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.join('\n');
|
|
25
|
+
export const createClientCss = () => `${crowquillFontFaces()}
|
|
26
|
+
:root {
|
|
2
27
|
color-scheme: dark;
|
|
3
28
|
--bg: #071019;
|
|
4
29
|
--panel: #0d1823;
|
|
@@ -22,7 +47,7 @@ body {
|
|
|
22
47
|
margin: 0;
|
|
23
48
|
background: var(--bg);
|
|
24
49
|
color: var(--text);
|
|
25
|
-
font-family:
|
|
50
|
+
font-family: "Crowquill Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
26
51
|
}
|
|
27
52
|
|
|
28
53
|
body {
|
|
@@ -256,28 +281,30 @@ select {
|
|
|
256
281
|
|
|
257
282
|
.floating-metrics {
|
|
258
283
|
display: flex;
|
|
259
|
-
gap:
|
|
260
|
-
flex-wrap:
|
|
284
|
+
gap: 8px;
|
|
285
|
+
flex-wrap: nowrap;
|
|
286
|
+
flex: 0 0 auto;
|
|
261
287
|
}
|
|
262
288
|
|
|
263
289
|
.metric-chip {
|
|
264
|
-
|
|
265
|
-
padding: 10px 12px;
|
|
290
|
+
padding: 6px 10px;
|
|
266
291
|
border: 1px solid var(--line);
|
|
267
|
-
border-radius:
|
|
292
|
+
border-radius: 9px;
|
|
268
293
|
background: rgba(12, 24, 36, 0.94);
|
|
269
|
-
display:
|
|
270
|
-
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: baseline;
|
|
296
|
+
gap: 6px;
|
|
297
|
+
white-space: nowrap;
|
|
271
298
|
}
|
|
272
299
|
|
|
273
300
|
.metric-chip strong {
|
|
274
|
-
font-size:
|
|
301
|
+
font-size: 15px;
|
|
275
302
|
line-height: 1;
|
|
276
303
|
}
|
|
277
304
|
|
|
278
305
|
.metric-chip small {
|
|
279
306
|
color: var(--muted);
|
|
280
|
-
font-size:
|
|
307
|
+
font-size: 10px;
|
|
281
308
|
letter-spacing: 0.03em;
|
|
282
309
|
text-transform: uppercase;
|
|
283
310
|
}
|
|
@@ -344,7 +371,7 @@ li small {
|
|
|
344
371
|
color: var(--text);
|
|
345
372
|
white-space: pre-wrap;
|
|
346
373
|
overflow: auto;
|
|
347
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
374
|
+
font-family: "Crowquill Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
348
375
|
font-size: 12px;
|
|
349
376
|
line-height: 1.5;
|
|
350
377
|
}
|
|
@@ -742,7 +769,7 @@ li small {
|
|
|
742
769
|
}
|
|
743
770
|
|
|
744
771
|
.metric-chip {
|
|
745
|
-
min-width:
|
|
772
|
+
min-width: 0;
|
|
746
773
|
}
|
|
747
774
|
|
|
748
775
|
.content-meta {
|
|
@@ -124,7 +124,7 @@ export const createClientHtml = () => `<!doctype html>
|
|
|
124
124
|
</section>
|
|
125
125
|
</main>
|
|
126
126
|
<footer class="app-footer" aria-label="Copyright notice">
|
|
127
|
-
<small>Copyright © 2026
|
|
127
|
+
<small>Copyright © 2026 Anderson Espindola</small>
|
|
128
128
|
</footer>
|
|
129
129
|
<script src="/app.js"></script>
|
|
130
130
|
</body>
|
|
@@ -56,8 +56,8 @@ const defaultTheme = {
|
|
|
56
56
|
[0.72, 0.51, 0.33, 1],
|
|
57
57
|
[0.44, 0.62, 0.85, 1]
|
|
58
58
|
],
|
|
59
|
-
edge: [0.
|
|
60
|
-
edgeHeavy: [0.
|
|
59
|
+
edge: [0.36, 0.46, 0.60, 0.26],
|
|
60
|
+
edgeHeavy: [0.40, 0.52, 0.68, 0.5],
|
|
61
61
|
clear: [0.031, 0.075, 0.114, 1]
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -213,8 +213,8 @@ const ensureEdgeCapacity = (count) => {
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
const nodeRadius = (relevance, kind) => {
|
|
216
|
-
const base = kind === 'cluster' ?
|
|
217
|
-
const modifier = Math.min(
|
|
216
|
+
const base = kind === 'cluster' ? 11.2 : 7.2
|
|
217
|
+
const modifier = Math.min(7.6, Math.max(0, relevance * 0.78))
|
|
218
218
|
return base + modifier
|
|
219
219
|
}
|
|
220
220
|
|
|
@@ -352,7 +352,9 @@ const drawNodeLayer = (predicate, color, radiusBoost = 1) => {
|
|
|
352
352
|
const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
|
|
353
353
|
pointPositionsBuffer[positionCursor] = sx
|
|
354
354
|
pointPositionsBuffer[positionCursor + 1] = sy
|
|
355
|
-
|
|
355
|
+
// Screen-space floor so every node stays a visible dot at any zoom (a low
|
|
356
|
+
// floor made nodes sub-pixel until zoomed in). Scaled by devicePixelRatio.
|
|
357
|
+
pointSizesBuffer[sizeCursor] = Math.max(3.0 * devicePixelRatio, state.radius[index] * camera.scale * devicePixelRatio * radiusBoost)
|
|
356
358
|
positionCursor += 2
|
|
357
359
|
sizeCursor += 1
|
|
358
360
|
}
|
|
@@ -3,6 +3,9 @@ const layoutCacheBySignature = new Map();
|
|
|
3
3
|
const maxLayoutCacheEntries = 6;
|
|
4
4
|
const farScaleThreshold = 0.22;
|
|
5
5
|
const midScaleThreshold = 0.78;
|
|
6
|
+
// Graphs with up to this many nodes render every node at all zoom levels (no
|
|
7
|
+
// zoom-based clustering). Above it, the far/mid/near LOD keeps huge graphs fast.
|
|
8
|
+
const renderAllNodeCap = 6000;
|
|
6
9
|
const viewportPaddingFactor = 0.18;
|
|
7
10
|
const maxNearEdgePerNode = 24;
|
|
8
11
|
const maxMidEdgePerNode = 12;
|
|
@@ -296,23 +299,29 @@ export const getGraphStreamChunk = async (vaultPath, input) => {
|
|
|
296
299
|
}
|
|
297
300
|
};
|
|
298
301
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
// When the whole graph fits a sane cap, render every node at every zoom (no
|
|
303
|
+
// clustering / budget-shrink), so nodes never disappear as you zoom out and
|
|
304
|
+
// reappear on approach. Only genuinely large graphs keep the far/mid/near LOD.
|
|
305
|
+
const rendersAll = layout.nodes.length <= renderAllNodeCap;
|
|
306
|
+
const effectiveNodeBudget = rendersAll ? Math.max(nodeBudget, layout.nodes.length) : nodeBudget;
|
|
307
|
+
const effectiveEdgeBudget = rendersAll ? Math.max(edgeBudget, layout.edges.length) : edgeBudget;
|
|
308
|
+
const mode = rendersAll || input.scale >= midScaleThreshold
|
|
309
|
+
? 'near'
|
|
310
|
+
: input.scale < farScaleThreshold
|
|
311
|
+
? 'far'
|
|
312
|
+
: 'mid';
|
|
304
313
|
const nodes = mode === 'far'
|
|
305
|
-
? selectFarClusters(layout.nodes, input,
|
|
314
|
+
? selectFarClusters(layout.nodes, input, effectiveNodeBudget)
|
|
306
315
|
: mode === 'mid'
|
|
307
316
|
? selectMidNodes(layout.nodes, cache, {
|
|
308
317
|
...input,
|
|
309
|
-
nodeBudget
|
|
318
|
+
nodeBudget: effectiveNodeBudget
|
|
310
319
|
})
|
|
311
320
|
: selectNearNodes(layout.nodes, cache, {
|
|
312
321
|
...input,
|
|
313
|
-
nodeBudget
|
|
322
|
+
nodeBudget: effectiveNodeBudget
|
|
314
323
|
});
|
|
315
|
-
const edges = collectEdgesForNodes(layout.edges, cache, nodes,
|
|
324
|
+
const edges = collectEdgesForNodes(layout.edges, cache, nodes, effectiveEdgeBudget, mode === 'near' ? maxNearEdgePerNode : mode === 'mid' ? maxMidEdgePerNode : maxFarEdgePerCluster);
|
|
316
325
|
return {
|
|
317
326
|
signature,
|
|
318
327
|
mode,
|
|
@@ -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
|
-
|
|
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,
|
|
@@ -15,8 +15,17 @@ Use the full potential, not shallow retrieval. Extract the maximum relevant cont
|
|
|
15
15
|
- Explainability and quality: brainlink_explain (chunk/score), brainlink_recommendations, brainlink_validate, brainlink_stats, plus link/orphan/duplicate hygiene.
|
|
16
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
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
|
+
|
|
18
20
|
Write back — after any durable learning (a preference, decision, pattern, or a change to a repo/config/doc that alters operating context):
|
|
19
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.
|
|
20
23
|
- Reindex with brainlink_index and validate with brainlink_validate after writes when applicable.
|
|
21
24
|
|
|
22
|
-
|
|
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.`;
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
4
|
import { brainlinkServerInstructions } from './server-instructions.js';
|
|
5
5
|
import { guardToolHandler } from './tool-guard.js';
|
|
@@ -171,6 +171,11 @@ export const createBrainlinkMcpServer = () => {
|
|
|
171
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.',
|
|
172
172
|
inputSchema: vaultProvisionInputSchema
|
|
173
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);
|
|
174
179
|
server.registerTool('brainlink_vault_commit', {
|
|
175
180
|
title: 'Commit Vault Markdown',
|
|
176
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
|
-
|
|
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';
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andespindola/brainlink",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|
|
7
|
-
"author": "
|
|
7
|
+
"author": "Anderson Espindola",
|
|
8
8
|
"homepage": "https://github.com/andersonflima/brainlink#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|