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

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.
Files changed (43) hide show
  1. package/AGENTS.md +3 -0
  2. package/CHANGELOG.md +24 -0
  3. package/COPYRIGHT.md +5 -0
  4. package/README.md +135 -7
  5. package/dist/application/auto-migrate-configured-vault.js +37 -0
  6. package/dist/application/build-context.js +64 -3
  7. package/dist/application/dedupe-notes.js +226 -0
  8. package/dist/application/frontend/client-css.js +111 -47
  9. package/dist/application/frontend/client-html.js +42 -26
  10. package/dist/application/frontend/client-js.js +788 -554
  11. package/dist/application/frontend/client-render-worker-js.js +569 -0
  12. package/dist/application/frontend/client-worker-js.js +66 -0
  13. package/dist/application/get-graph-layout.js +38 -5
  14. package/dist/application/get-graph-stream-chunk.js +289 -0
  15. package/dist/application/get-graph-view.js +243 -0
  16. package/dist/application/import-legacy-sqlite.js +296 -0
  17. package/dist/application/index-vault.js +249 -21
  18. package/dist/application/offline-pack-backup.js +44 -0
  19. package/dist/application/server/routes.js +187 -5
  20. package/dist/application/start-server.js +75 -4
  21. package/dist/application/watch-vault.js +23 -2
  22. package/dist/cli/commands/agent-commands.js +7 -0
  23. package/dist/cli/commands/write-commands.js +842 -8
  24. package/dist/cli/runtime.js +10 -2
  25. package/dist/domain/context.js +54 -11
  26. package/dist/domain/graph-layout.js +275 -3
  27. package/dist/domain/markdown.js +29 -9
  28. package/dist/domain/middle-out.js +18 -0
  29. package/dist/infrastructure/config.js +117 -4
  30. package/dist/infrastructure/file-index.js +70 -3
  31. package/dist/infrastructure/file-system-vault.js +15 -0
  32. package/dist/infrastructure/index-state.js +58 -0
  33. package/dist/infrastructure/private-pack-codec.js +71 -10
  34. package/dist/infrastructure/search-packs.js +286 -15
  35. package/dist/infrastructure/vault-migration-state.js +69 -0
  36. package/dist/infrastructure/volatile-memory.js +100 -0
  37. package/dist/mcp/runtime.js +20 -0
  38. package/dist/mcp/server.js +28 -10
  39. package/dist/mcp/tools.js +110 -0
  40. package/docs/AGENT_USAGE.md +87 -3
  41. package/docs/ARCHITECTURE.md +6 -0
  42. package/docs/QUICKSTART.md +7 -0
  43. package/package.json +7 -2
package/AGENTS.md CHANGED
@@ -83,6 +83,9 @@ Use watch mode while editing notes:
83
83
  ```bash
84
84
  npm run dev -- server --vault ./vault --watch
85
85
  npm run dev -- watch --vault ./vault
86
+ npm run dev -- bench --vault ./vault
87
+ npm run dev -- bench --vault ./vault --watch
88
+ npm run dev -- pack-backup --vault ./vault
86
89
  ```
87
90
 
88
91
  Start MCP over stdio:
package/CHANGELOG.md CHANGED
@@ -22,6 +22,30 @@
22
22
  - Added short-lived hybrid search cache with automatic invalidation on index changes.
23
23
  - Added `stats --extended` observability output with storage, quality and latency probes.
24
24
  - Added `docs/QUICKSTART.md` and aligned README/agent docs with the latest CLI/MCP flows.
25
+ - Added middle-out context assembly so chunk selection expands around the strongest note chunk.
26
+ - Added compressed-space pack prefiltering (token bloom index) before `.blpk` decryption and scan.
27
+ - Improved graph UI auto-fit and viewport recovery so loaded nodes are re-centered when zoom/pan drifts to empty canvas.
28
+ - Added cross-platform native desktop GUI auto-open for `blink server` (macOS Swift/WebKit, Windows PowerShell WinForms, Linux Python GTK/WebKit2), with app-window/browser fallback.
29
+ - Changed Linux default UI launch to app-window/browser for lighter startup; Linux native GUI is now opt-in via `BRAINLINK_LINUX_NATIVE_GUI=1`.
30
+ - Added native GUI parent-process monitoring so GUI windows close automatically when `blink server` stops.
31
+ - Improved non-mac browser detection fallback to try installed Edge/Chrome/Firefox/Chromium candidates before system default open.
32
+ - Improved graph filter rendering to keep hub anchor nodes visible (`Memory Hub`/`MOC`/high-degree fallback) for coherent relationship context.
33
+ - Fixed graph modal content loading by correcting agent query parameter composition for `/api/graph-node` and `/api/graph-filter` requests.
34
+ - Improved 50k+ graph rendering performance with viewport-aware spatial node culling, cached render visibility, and node-adjacent edge selection to avoid full graph scans every frame.
35
+ - Added incremental vault indexing with file snapshots to reuse unchanged documents/chunks/embeddings, plus adaptive search-pack rebuild thresholds to avoid full re-compression on small edits.
36
+ - Reduced large-graph HTTP payload size with compact `/api/graph-layout` encoding for high-node vaults and capped transmitted edges to improve UI load responsiveness.
37
+ - Added aggressive graph LOD clustering when zoomed out, dynamic per-zoom edge render budgets, and a dedicated frontend worker for off-main-thread graph filter matching.
38
+ - Improved Linux browser fallback launch stability by auto-applying Chromium compatibility flags (`--ozone-platform=x11`, `--disable-gpu`, `--disable-features=Vulkan,VaapiVideoDecoder`, `--disable-background-networking`) for app-window/browser modes.
39
+ - Improved massive-graph UI responsiveness with stricter render budgets, adaptive heavy-graph frame throttling, reduced interaction hit-test frequency, and URL-first agent selection on initial graph load.
40
+ - Improved 50k+ graph LOD behavior so zoomed-out views render lightweight cluster overviews and progressively reveal nodes/edges only as zoom increases.
41
+ - Added `blink bench` with realtime index phase telemetry and per-run compressed-pack analysis (input/output bytes, ratio, saved space, rebuild reason and duration), including continuous watch mode.
42
+ - Added tunable single-stage search-pack compression settings (`searchPack.rowChunkSize`, `searchPack.compressionLevel`, `searchPack.useDictionary`).
43
+ - Added benchmark guardrails for compression savings and latency regression (`searchPack.guardrailMinSavingsPercent`, `searchPack.guardrailMaxLatencyRegressionPercent`), reported in `blink bench`.
44
+ - Added `blink pack-backup` for offline second-stage compression backups of encrypted `.blpk` packs, outside the online query path.
45
+ - Hardened Linux browser launch flags for Ubuntu 26 Chromium/Wayland compatibility (`--disable-vulkan`, `--use-gl=swiftshader`, `--ozone-platform-hint=x11`).
46
+ - Improved pack resilience by auto-repairing missing search-pack manifests from existing `.blpk` files, avoiding unnecessary full repacks on small incremental updates.
47
+ - Updated Linux graph auto-open behavior to prioritize the system default browser (`xdg-open`) before explicit browser fallbacks.
48
+ - Removed implicit Chromium dependency in Linux auto-open flow; app-window launch is now opt-in (`BRAINLINK_LINUX_APP_WINDOW=1`).
25
49
 
26
50
  ## 0.1.0-beta.3
27
51
 
package/COPYRIGHT.md ADDED
@@ -0,0 +1,5 @@
1
+ Copyright (c) 2026 Substructa
2
+
3
+ This project is licensed under the MIT License.
4
+
5
+ See [LICENSE](./LICENSE) for full terms.
package/README.md CHANGED
@@ -58,6 +58,7 @@ LLMs do not have infinite context. Brainlink gives agents an external memory lay
58
58
 
59
59
  Markdown is the source of truth. `.brainlink/index.json` is a rebuildable index artifact.
60
60
  After each index run, Brainlink also writes private encrypted search packs at `.brainlink/search-packs/*.blpk` to preserve fast retrieval and portable recovery.
61
+ Online retrieval always uses a single compression stage per pack; optional second-stage compression is reserved for offline backup artifacts only.
61
62
  Pack decryption uses a Brainlink key from `$BRAINLINK_HOME/keys` or from `BRAINLINK_SEARCH_PACK_KEY` when explicitly configured.
62
63
  Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/context access.
63
64
 
@@ -67,8 +68,13 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
67
68
  - Obsidian-compatible `[[wiki links]]` and `#tags`.
68
69
  - Weighted graph edges so agents can rank relationship importance and priority.
69
70
  - Backlinks, broken-link reports, orphan detection and validation.
70
- - Full-text, semantic and hybrid retrieval modes.
71
71
  - Full-text, semantic and hybrid retrieval on a local file index.
72
+ - Middle-out context assembly around the strongest chunk per document.
73
+ - In-process index and context caching with automatic invalidation on index updates.
74
+ - HTTP graph server caches generated frontend assets and graph-layout JSON payloads by signature, and skips layout serialization when ETag returns `304`.
75
+ - Compressed-space prefiltering for `.blpk` packs before decryption and scan.
76
+ - Incremental indexing that reprocesses only changed markdown files and reuses existing chunks/embeddings for unchanged notes.
77
+ - Adaptive compressed-pack rebuild policy to keep indexing fast during small edit batches.
72
78
  - Agent namespaces under `agents/<agent-id>/`.
73
79
  - S3-compatible bucket vaults through `s3://bucket/prefix` URIs.
74
80
  - CLI with machine-readable `--json` output.
@@ -76,6 +82,17 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
76
82
  - Built-in MCP stdio server for agent tool integration.
77
83
  - Local HTTP API.
78
84
  - Realtime graph UI with agent selector and colored knowledge groups.
85
+ - Graph renderer uses a star layout centered on the primary hub while preserving real weighted `[[wiki link]]` edges for backlinks, ranking and context traversal.
86
+ - The full filtered graph stays visible during zoom/pan, rendering every visible node and edge without viewport culling or edge caps in the main view.
87
+ - Graph exploration uses viewport-first chunk streaming (`/api/graph-stream`) with explicit node/edge budgets.
88
+ - Render pipeline uses WebGL in a dedicated worker through `OffscreenCanvas`, keeping the main thread focused on UI controls and details panels.
89
+ - Large graph layout API automatically uses compact payload encoding with link-coverage-aware edge selection to reduce initial client load without hiding major relationships.
90
+ - Large-segment layout spacing now grows logarithmically to keep initial visual density consistent between medium and very large vaults (for example, ~1k vs ~50k notes).
91
+ - Graph coordinates are visually compacted across graph sizes so reset starts from a stable fitted scene and zoom-in progressively reveals local detail.
92
+ - Zoomed-out graph keeps the same flat graph scene and preserves complete filtered relationships without switching to nested subgraphs.
93
+ - Graph reset fits the full graph scene instead of starting in a separate macro overview mode.
94
+ - Graph filtering runs in a dedicated browser worker to keep the UI thread responsive during heavy datasets.
95
+ - Node titles are shown as the user zooms closer, while labels remain bounded to visible on-screen nodes in very large graphs.
79
96
 
80
97
  ## Install
81
98
 
@@ -395,6 +412,7 @@ blink agent upgrade
395
412
  ```
396
413
 
397
414
  This configures `~/.codex/config.toml` with Brainlink MCP (`brainlink-mcp`) so Brainlink is available by default in agent sessions.
415
+ `agent install` and `agent upgrade` also apply the MCP `fully-auto` bootstrap policy by default (`enforceBootstrap`, `enforceContextFirst`, `autoBootstrapOnRead`, `autoBootstrapOnStartup` all enabled).
398
416
 
399
417
  If you are inside this repository and want plugin gallery setup too:
400
418
 
@@ -514,8 +532,12 @@ Available tools:
514
532
  - `brainlink_recommendations`: return an automatic action plan so agents can run Brainlink in the recommended order.
515
533
  - `brainlink_context`: read indexed context for a task or question.
516
534
  - `brainlink_search`: search indexed notes.
535
+ - `brainlink_dedupe`: detect duplicate candidates using exact hash + semantic similarity scores.
536
+ - `brainlink_resolve_duplicate`: resolve duplicate pairs (`merge`, `link`, `ignore`) with connectivity-safe fallback edges.
517
537
  - `brainlink_add_note`: write durable Markdown memory and reindex.
518
538
  - `brainlink_add_file`: ingest a local file as a note and reindex.
539
+ - `brainlink_volatile_add`: write temporary agent-decided memory with TTL; volatile sections are included in context and never create durable graph edges.
540
+ - `brainlink_volatile_clear`: clear temporary memory for the current vault/agent namespace.
519
541
  - `brainlink_index`: rebuild the vault index.
520
542
  - `brainlink_stats`: read indexed vault statistics.
521
543
  - `brainlink_validate`: validate broken links and orphan notes.
@@ -541,7 +563,7 @@ Agents can raise the importance of a relationship by putting priority markers on
541
563
  Related: [[Incident Runbook]] #critical
542
564
  ```
543
565
 
544
- Indexed edges expose `weight` and `priority` (`low`, `normal`, `high`, `critical`) through CLI JSON, HTTP graph APIs and `brainlink_graph`.
566
+ Indexed edges expose `weight` and `priority` (`low`, `normal`, `high`, `critical`) through CLI JSON, HTTP graph APIs and `brainlink_graph`. Brainlink promotes only representative graph links per note: high-priority and high-weight links win, structural hub links such as `Memory Hub`, `Knowledge Root`, `MOC` and map notes are suppressed when stronger direct links exist, and old indexes are rebuilt automatically when their graph link model version is missing or stale.
545
567
 
546
568
  ## Graph UI
547
569
 
@@ -552,20 +574,41 @@ blink server --host 127.0.0.1 --port 4321 --watch
552
574
  ```
553
575
 
554
576
  By default, the server uses `$HOME/.brainlink/vault`. Pass `--vault ./vault` only when you want to inspect a custom vault.
577
+ By default, `blink server` tries to open the graph in a native desktop GUI window:
578
+ - macOS: Swift + WebKit
579
+ - Windows: PowerShell WinForms WebBrowser
580
+ - Linux: optional Python GTK + WebKit2 (requires `python3` + `gi` + `WebKit2`)
581
+
582
+ On Linux, native GUI is disabled by default for better startup performance. Enable it with `BRAINLINK_LINUX_NATIVE_GUI=1`.
583
+ If native GUI launch is unavailable on your system, it falls back to dedicated app-window mode and then to the default browser.
584
+ For Chromium-family browsers on Linux (`chromium`, `chromium-browser`, `google-chrome`, `microsoft-edge`, `brave-browser`), Brainlink now auto-applies compatibility flags during launch (`--ozone-platform=x11`, `--ozone-platform-hint=x11`, `--disable-gpu`, `--disable-vulkan`, `--use-gl=swiftshader`, `--disable-features=Vulkan,VaapiVideoDecoder`, `--disable-background-networking`) to avoid common Wayland/Vulkan/VAAPI startup issues.
585
+ On Linux, Brainlink opens the graph through the system default browser first (`xdg-open`), then `$BROWSER`/detected browsers as fallback. Chromium-family app-window mode is optional via `BRAINLINK_LINUX_APP_WINDOW=1`.
586
+ Use `--no-open` to keep it headless.
587
+ When native GUI is used, the GUI window automatically closes when the `blink server` process stops.
555
588
 
556
589
  The graph UI shows:
557
590
 
558
591
  - notes as nodes
559
- - `[[wiki links]]` as weighted edges
560
- - details opened on node click (tags, outgoing links, backlinks, full Markdown content)
592
+ - representative `[[wiki links]]` as weighted edges
593
+ - star layout centered on the primary hub, without rewriting or flattening underlying relationships
594
+ - details opened in a non-modal side panel (tags, outgoing links, backlinks, full Markdown content), so zoom and pan remain available while inspecting data
561
595
  - neutral graph nodes with segment/group metadata
562
- - agent selector for isolated views
596
+ - agent selector (id-only labels) for isolated views
563
597
  - graph filter matches title, path, tags and note content
598
+ - graph filter keeps hub context nodes visible (`Memory Hub`/`MOC`/high-degree fallback) to preserve relationship readability
564
599
  - realtime refresh while `--watch` is enabled
565
600
  - graph controls for zoom in, zoom out, fit visible nodes and reset-to-fit-all
566
- - wheel zoom anchored to cursor position for faster navigation in large graphs
601
+ - wheel zoom (including `cmd+scroll` and `ctrl+scroll`) anchored to cursor position for faster navigation in large graphs
602
+ - wheel/button zoom updates immediately at the cursor anchor without delayed focus-transition interpolation
603
+ - Bloom-like scene navigation: reset fits the current graph scene, wheel zoom stays anchored to the cursor, and worker-driven WebGL rendering keeps pan/zoom interaction responsive
604
+ - zoom-out floor for large and massive graphs to keep the scene reachable without switching into a separate macro graph mode
605
+ - keyboard shortcuts: `+` zoom in, `-` zoom out, `0` reset fit
606
+ - click on a node opens its details panel; double-click on empty canvas zooms in at cursor position
567
607
  - floating graph totals (notes, links, tags) below the Brainlink title
568
- - large-graph rendering safeguards (edge draw caps, lower redraw rate, zoom-aware interaction)
608
+ - graph rendering safeguards (batched GPU draw calls, lower redraw rate, zoom-aware interaction)
609
+ - adaptive CPU safeguards for large graphs: idle frame pacing, throttled background physics updates and cached viewport dimensions to reduce redraw/layout overhead while preserving interaction responsiveness
610
+ - worker-first WebGL rendering with Canvas fallback when `OffscreenCanvas` or worker rendering is unavailable
611
+ - large graph view keeps a single-level graph model across zoom levels, renders the full filtered scene instead of viewport-sampled subsets, and shows node titles as zoom approaches readable scale
569
612
 
570
613
  The server indexes before starting by default. Use `--no-index` to skip that step:
571
614
 
@@ -584,6 +627,8 @@ Routes:
584
627
  - `GET /api/agents`
585
628
  - `GET /api/graph`
586
629
  - `GET /api/graph-layout`
630
+ - `GET /api/graph-view?x=<x>&y=<y>&w=<width>&h=<height>&scale=<scale>`
631
+ - `GET /api/graph-stream?x=<x>&y=<y>&w=<width>&h=<height>&scale=<scale>&nodeBudget=<n>&edgeBudget=<n>`
587
632
  - `GET /api/graph-node?id=<node-id>`
588
633
  - `GET /api/search?q=<query>&limit=10&mode=hybrid`
589
634
  - `GET /api/context?q=<query>&limit=12&tokens=2000&mode=hybrid`
@@ -652,6 +697,7 @@ blink config set-vault "s3://my-memory-bucket/brainlink" --global
652
697
 
653
698
  `config set-vault` writes configuration through CLI (no manual file edits required).
654
699
  By default it writes local config (`./brainlink.config.json`), appends the vault to `allowedVaults`, and migrates Markdown memory from the current configured vault when the target is empty.
700
+ When the configured default vault is changed manually in config files, Brainlink also performs automatic migration on the next command that uses the configured vault (without explicit `--vault`).
655
701
  Use `--global` to write to `$BRAINLINK_HOME/brainlink.config.json`, `--no-migrate` to skip migration, and `--no-index` to skip post-migration indexing.
656
702
  `config doctor` is dry-run by default; use `--fix` to apply safe config normalization and allowlist fixes.
657
703
 
@@ -667,6 +713,18 @@ blink migrate-vault --from ~/.brainlink/vault --to ./team-vault --report ./migra
667
713
  Runs explicit markdown migration between vaults while preserving conflicts as `.conflict-<timestamp>` files.
668
714
  Use `--dry-run` to preview `copied`, `conflicted` and `unchanged` counts before writing.
669
715
 
716
+ ### `db-import`
717
+
718
+ ```bash
719
+ blink db-import --vault ./team-vault
720
+ blink db-import --vault ./team-vault --db ./legacy/brainlink.db
721
+ blink db-import --vault ./team-vault --db ./legacy/brainlink.db --table legacy_notes --dry-run
722
+ ```
723
+
724
+ Imports durable memory from a legacy SQLite database into Markdown notes (`agents/<agent-id>/*.md`) and reindexes by default.
725
+ When `--db` is omitted, Brainlink auto-detects common legacy paths such as `<vault>/.brainlink/brainlink.db`.
726
+ Use `--agent <id>` to force all imported rows into one namespace, `--limit` for incremental imports, `--dry-run` to preview without writing files, and `--no-index` to defer reindexing.
727
+
670
728
  ### `init`
671
729
 
672
730
  ```bash
@@ -691,6 +749,28 @@ blink add "Note Title" --vault ./vault --content-file ./notes.md --no-auto-index
691
749
 
692
750
  Creates a Markdown note under `agents/<agent-id>/`. Common secret patterns are blocked by default; use `--allow-sensitive` only for an intentionally protected vault.
693
751
  To avoid disconnected memory, Brainlink auto-adds a fallback wiki edge when a note is written without links, creating agent hub notes when needed.
752
+ `add` also returns `possibleDuplicates` (exact hash + semantic candidates) so agents can resolve duplicate memory right after writes.
753
+
754
+ ### `dedupe`
755
+
756
+ ```bash
757
+ blink dedupe --vault ./vault --json
758
+ blink dedupe --vault ./vault --agent coding-agent --limit 20 --min-score 0.92 --json
759
+ blink dedupe --vault ./vault --no-semantic --json
760
+ ```
761
+
762
+ Detects `possibleDuplicate` pairs using exact content hashes and optional semantic similarity.
763
+
764
+ ### `dedupe-resolve`
765
+
766
+ ```bash
767
+ blink dedupe-resolve --vault ./vault --left agents/shared/a.md --right agents/shared/b.md --action merge --json
768
+ blink dedupe-resolve --vault ./vault --left agents/shared/a.md --right agents/shared/b.md --action link --json
769
+ blink dedupe-resolve --vault ./vault --left agents/shared/a.md --right agents/shared/b.md --action ignore --json
770
+ ```
771
+
772
+ Resolves a duplicate pair with `merge`, `link` or `ignore`.
773
+ When action is not `merge`, Brainlink still creates a low-priority related edge (`#related-to`) so notes remain connected.
694
774
 
695
775
  ### `index`
696
776
 
@@ -701,6 +781,38 @@ blink index --vault ./vault
701
781
 
702
782
  Rebuilds the local index from Markdown files.
703
783
 
784
+ ### `bench`
785
+
786
+ ```bash
787
+ blink bench --vault ./vault
788
+ blink bench --vault ./vault --watch
789
+ blink bench --vault ./vault --watch --debounce 500
790
+ blink bench --vault ./vault --json
791
+ ```
792
+
793
+ Runs indexing with realtime phase telemetry (`start`, `scan`, `parse`, `embed`, `persist`, `packs`, `complete`) and prints a benchmark summary at the end of each run.
794
+
795
+ Summary includes compression behavior for `.blpk` packs when rebuild happens:
796
+ - pack rebuild reason
797
+ - pack count and pack build duration
798
+ - uncompressed input bytes vs compressed output bytes
799
+ - saved percentage
800
+ - objective guardrails (minimum savings and maximum latency regression thresholds)
801
+
802
+ Use `--watch` to keep benchmarking incremental reindex runs after Markdown changes (local filesystem vaults only).
803
+ When `.brainlink/search-packs/manifest.json` is missing but `.blpk` files exist, Brainlink repairs the manifest first and avoids unnecessary full pack rebuild on small edits.
804
+
805
+ ### `pack-backup`
806
+
807
+ ```bash
808
+ blink pack-backup --vault ./vault
809
+ blink pack-backup --vault ./vault --output ./vault/.brainlink/backups/custom.blpkbak.gz
810
+ blink pack-backup --vault ./vault --json
811
+ ```
812
+
813
+ Creates an offline backup artifact of encrypted search packs with a second compression pass.
814
+ This is intentionally outside the online retrieval path (`index`, `search`, `context`).
815
+
704
816
  ### `agents`
705
817
 
706
818
  ```bash
@@ -728,6 +840,7 @@ Modes:
728
840
  - `semantic`: local deterministic embedding similarity only.
729
841
 
730
842
  Hybrid results are cached in-memory for a short TTL and invalidated automatically when the local index file changes.
843
+ Context selection uses a middle-out strategy: it starts from the strongest chunk in a note and expands to neighboring chunks while respecting token budget.
731
844
 
732
845
  ### `context`
733
846
 
@@ -738,6 +851,7 @@ blink context "question" --vault ./vault --agent coding-agent --mode hybrid --js
738
851
  ```
739
852
 
740
853
  Builds a compact context package for an agent.
854
+ Repeated calls with the same vault, agent, query, mode and token/limit settings are served from a short in-memory cache while the index is unchanged.
741
855
 
742
856
  ### `links`
743
857
 
@@ -822,9 +936,15 @@ Watches Markdown files and rebuilds the index when notes change.
822
936
  ```bash
823
937
  blink server --watch
824
938
  blink server --vault ./vault --watch
939
+ blink server --vault ./vault --watch --no-open
825
940
  ```
826
941
 
827
942
  Starts the local read-only graph UI and HTTP API.
943
+ By default, it tries to open a native desktop GUI window for the graph URL.
944
+ On Linux, native GUI is disabled by default; enable it with `BRAINLINK_LINUX_NATIVE_GUI=1`.
945
+ If native GUI launch is unavailable, it falls back to dedicated app-window mode and then browser open.
946
+ When fallback opens Chromium-family browsers on Linux, Brainlink automatically uses compatibility launch flags for stable rendering on Ubuntu/Wayland setups.
947
+ Use `--no-open` to skip that behavior.
828
948
 
829
949
  The HTTP server only binds to loopback hosts such as `127.0.0.1`, `localhost` or `::1`.
830
950
 
@@ -865,6 +985,13 @@ If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HO
865
985
  "embeddingProvider": "local",
866
986
  "defaultSearchMode": "hybrid",
867
987
  "chunkSize": 1200,
988
+ "searchPack": {
989
+ "rowChunkSize": 5000,
990
+ "compressionLevel": 5,
991
+ "useDictionary": true,
992
+ "guardrailMinSavingsPercent": 8,
993
+ "guardrailMaxLatencyRegressionPercent": 5
994
+ },
868
995
  "agentProfiles": {
869
996
  "coding-agent": {
870
997
  "defaultSearchMode": "semantic",
@@ -1025,6 +1152,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
1025
1152
  ## License
1026
1153
 
1027
1154
  MIT. See [LICENSE](LICENSE).
1155
+ Copyright (c) 2026 Substructa. See [COPYRIGHT.md](COPYRIGHT.md).
1028
1156
 
1029
1157
  ### Memory Optimization Loop (1-7)
1030
1158
 
@@ -0,0 +1,37 @@
1
+ import { indexVault } from './index-vault.js';
2
+ import { migrateVaultContent } from './migrate-vault.js';
3
+ import { getLastConfiguredVaultForKey, setLastConfiguredVaultForKey } from '../infrastructure/vault-migration-state.js';
4
+ export const autoMigrateConfiguredVaultIfChanged = async (input) => {
5
+ const configKey = input.configKey.trim();
6
+ const configuredVault = input.configuredVault.trim();
7
+ if (configKey.length === 0 || configuredVault.length === 0) {
8
+ return {
9
+ changed: false,
10
+ migrated: false
11
+ };
12
+ }
13
+ const previousVault = await getLastConfiguredVaultForKey(configKey);
14
+ if (!previousVault) {
15
+ await setLastConfiguredVaultForKey(configKey, configuredVault);
16
+ return {
17
+ changed: false,
18
+ migrated: false
19
+ };
20
+ }
21
+ if (previousVault === configuredVault) {
22
+ return {
23
+ changed: false,
24
+ migrated: false
25
+ };
26
+ }
27
+ const migration = await migrateVaultContent(previousVault, configuredVault);
28
+ const shouldIndex = migration.copied + migration.conflicted > 0;
29
+ if (shouldIndex) {
30
+ await indexVault(configuredVault);
31
+ }
32
+ await setLastConfiguredVaultForKey(configKey, configuredVault);
33
+ return {
34
+ changed: true,
35
+ migrated: shouldIndex
36
+ };
37
+ };
@@ -1,13 +1,74 @@
1
+ import { stat } from 'node:fs/promises';
1
2
  import { formatContextPackage, selectContextSections } from '../domain/context.js';
3
+ import { indexStoragePath } from '../infrastructure/file-index.js';
4
+ import { searchVolatileMemory, volatileMemoryStoragePath } from '../infrastructure/volatile-memory.js';
2
5
  import { searchKnowledge } from './search-knowledge.js';
6
+ const contextCacheTtlMs = 45_000;
7
+ const contextCacheMaxEntries = 200;
8
+ const contextCache = new Map();
9
+ const readFileSignature = async (path) => {
10
+ try {
11
+ const info = await stat(path);
12
+ return `${Math.floor(info.mtimeMs)}:${info.size}`;
13
+ }
14
+ catch {
15
+ return '0:0';
16
+ }
17
+ };
18
+ const readContextDataSignature = async (vaultPath) => `${await readFileSignature(indexStoragePath(vaultPath))}|${await readFileSignature(volatileMemoryStoragePath(vaultPath))}`;
19
+ const toCacheKey = (vaultPath, query, limit, maxTokens, agentId, mode) => JSON.stringify({
20
+ vaultPath,
21
+ query: query.trim().toLowerCase(),
22
+ limit,
23
+ maxTokens,
24
+ agentId: agentId?.trim().toLowerCase() ?? '*',
25
+ mode: mode ?? 'default'
26
+ });
27
+ const contextCacheGet = (key, dataSignature) => {
28
+ const entry = contextCache.get(key);
29
+ if (!entry) {
30
+ return undefined;
31
+ }
32
+ const fresh = Date.now() - entry.createdAt <= contextCacheTtlMs && entry.dataSignature === dataSignature;
33
+ if (!fresh) {
34
+ contextCache.delete(key);
35
+ return undefined;
36
+ }
37
+ return entry.context;
38
+ };
39
+ const contextCacheSet = (entry) => {
40
+ contextCache.set(entry.key, entry);
41
+ if (contextCache.size <= contextCacheMaxEntries) {
42
+ return;
43
+ }
44
+ const overflow = contextCache.size - contextCacheMaxEntries;
45
+ const keys = Array.from(contextCache.keys()).slice(0, overflow);
46
+ keys.forEach((key) => contextCache.delete(key));
47
+ };
3
48
  export const buildContextPackage = async (vaultPath, query, limit, maxTokens, agentId, mode) => {
49
+ const cacheKey = toCacheKey(vaultPath, query, limit, maxTokens, agentId, mode);
50
+ const dataSignature = await readContextDataSignature(vaultPath);
51
+ const cached = contextCacheGet(cacheKey, dataSignature);
52
+ if (cached) {
53
+ return cached;
54
+ }
4
55
  const results = await searchKnowledge(vaultPath, query, limit, agentId, mode);
5
- const sections = selectContextSections(results, maxTokens);
6
- return {
56
+ const durableSections = selectContextSections(results, maxTokens);
57
+ const volatileSections = await searchVolatileMemory(vaultPath, query, Math.min(3, limit), agentId, mode ?? 'hybrid');
58
+ const sections = [...volatileSections, ...durableSections];
59
+ const context = {
7
60
  query,
8
61
  sections,
9
- content: formatContextPackage(query, sections)
62
+ content: formatContextPackage(query, sections),
63
+ ...(volatileSections.length > 0 ? { volatileSections } : {})
10
64
  };
65
+ contextCacheSet({
66
+ key: cacheKey,
67
+ createdAt: Date.now(),
68
+ dataSignature,
69
+ context
70
+ });
71
+ return context;
11
72
  };
12
73
  export const buildContext = async (vaultPath, query, limit, maxTokens, agentId, mode) => {
13
74
  const contextPackage = await buildContextPackage(vaultPath, query, limit, maxTokens, agentId, mode);
@@ -0,0 +1,226 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createEmbeddingBuckets, createLocalEmbedding, cosineSimilarity } from '../domain/embeddings.js';
3
+ import { parseMarkdownDocument } from '../domain/markdown.js';
4
+ import { writeMarkdownFile, ensureVault, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
5
+ import { indexVault } from './index-vault.js';
6
+ const tokenPattern = /[\p{L}\p{N}_-]+/gu;
7
+ const frontmatterPattern = /^---\n[\s\S]*?\n---\n?/m;
8
+ const rootHeadingPattern = /^#\s+.+\n+/m;
9
+ const maxCandidatesPerBucket = 240;
10
+ const normalizePath = (path) => path.replaceAll('\\', '/').replace(/^\.\//, '');
11
+ const toComparableBody = (content) => content
12
+ .replace(frontmatterPattern, '')
13
+ .replace(rootHeadingPattern, '')
14
+ .replaceAll('\r\n', '\n')
15
+ .trim();
16
+ const normalizeStrictContent = (content) => toComparableBody(content);
17
+ const normalizeSemanticContent = (content) => toComparableBody(content)
18
+ .replace(/\s+/g, ' ')
19
+ .trim();
20
+ const toHash = (value) => createHash('sha256').update(value, 'utf8').digest('hex');
21
+ const toCandidateId = (leftPath, rightPath) => [normalizePath(leftPath), normalizePath(rightPath)].sort((left, right) => left.localeCompare(right)).join('|');
22
+ const hasSharedTokens = (left, right) => {
23
+ const leftTokens = new Set((left.match(tokenPattern) ?? []).map((token) => token.toLowerCase()).filter((token) => token.length > 2));
24
+ const rightTokens = new Set((right.match(tokenPattern) ?? []).map((token) => token.toLowerCase()).filter((token) => token.length > 2));
25
+ if (leftTokens.size === 0 || rightTokens.size === 0) {
26
+ return false;
27
+ }
28
+ for (const token of leftTokens) {
29
+ if (rightTokens.has(token)) {
30
+ return true;
31
+ }
32
+ }
33
+ return false;
34
+ };
35
+ const relatedMarker = (targetTitle) => `Related: [[${targetTitle}]] priority: low #related-to`;
36
+ const ensureRelatedEdgeLine = (content, targetTitle) => {
37
+ const linkPattern = new RegExp(`\\[\\[\\s*${targetTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*(?:[\\]|#])?`, 'i');
38
+ if (linkPattern.test(content)) {
39
+ return content;
40
+ }
41
+ const trimmed = content.trimEnd();
42
+ return `${trimmed}\n\n${relatedMarker(targetTitle)}\n`;
43
+ };
44
+ const ensureMergedMarker = (content, targetTitle) => {
45
+ const marker = `Merged into [[${targetTitle}]]`;
46
+ if (content.includes(marker)) {
47
+ return content;
48
+ }
49
+ return `${content.trimEnd()}\n\n${marker} priority: low #related-to\n`;
50
+ };
51
+ const appendMergedContent = (baseContent, mergedTitle, mergedContent) => {
52
+ const marker = `## Merged Memory From [[${mergedTitle}]]`;
53
+ if (baseContent.includes(marker)) {
54
+ return baseContent;
55
+ }
56
+ const mergedBody = normalizeSemanticContent(mergedContent);
57
+ return `${baseContent.trimEnd()}\n\n${marker}\n\n${mergedBody}\n`;
58
+ };
59
+ const loadNoteRecords = async (vaultPath, agentId) => {
60
+ const absoluteVaultPath = await ensureVault(vaultPath);
61
+ const files = await readMarkdownFiles(vaultPath);
62
+ return files
63
+ .map((file) => {
64
+ const parsed = parseMarkdownDocument({
65
+ absolutePath: file.absolutePath,
66
+ vaultPath: absoluteVaultPath,
67
+ content: file.content,
68
+ createdAt: file.createdAt,
69
+ updatedAt: file.updatedAt
70
+ });
71
+ const strict = normalizeStrictContent(parsed.content);
72
+ const semantic = normalizeSemanticContent(parsed.content);
73
+ const embedding = createLocalEmbedding(`${parsed.title}\n${semantic}`);
74
+ return {
75
+ title: parsed.title,
76
+ path: normalizePath(parsed.path),
77
+ agentId: parsed.agentId,
78
+ content: parsed.content,
79
+ normalizedStrictContent: strict,
80
+ semanticContent: semantic,
81
+ embedding,
82
+ buckets: createEmbeddingBuckets(embedding, 20)
83
+ };
84
+ })
85
+ .filter((record) => (agentId ? record.agentId === agentId : true));
86
+ };
87
+ const pairToCandidate = (left, right, kind, score, reason) => ({
88
+ id: toCandidateId(left.path, right.path),
89
+ possibleDuplicate: true,
90
+ kind,
91
+ score: Number(score.toFixed(4)),
92
+ left: {
93
+ title: left.title,
94
+ path: left.path,
95
+ agentId: left.agentId
96
+ },
97
+ right: {
98
+ title: right.title,
99
+ path: right.path,
100
+ agentId: right.agentId
101
+ },
102
+ reason
103
+ });
104
+ const indexCandidatePairs = (notes) => {
105
+ const bucketMap = new Map();
106
+ notes.forEach((note, index) => {
107
+ note.buckets.forEach((bucket) => {
108
+ const current = bucketMap.get(bucket) ?? [];
109
+ if (current.length < maxCandidatesPerBucket) {
110
+ current.push(index);
111
+ bucketMap.set(bucket, current);
112
+ }
113
+ });
114
+ });
115
+ const pairKeys = new Set();
116
+ const pairs = [];
117
+ bucketMap.forEach((indexes) => {
118
+ for (let leftIndex = 0; leftIndex < indexes.length; leftIndex += 1) {
119
+ for (let rightIndex = leftIndex + 1; rightIndex < indexes.length; rightIndex += 1) {
120
+ const left = Math.min(indexes[leftIndex] ?? 0, indexes[rightIndex] ?? 0);
121
+ const right = Math.max(indexes[leftIndex] ?? 0, indexes[rightIndex] ?? 0);
122
+ const key = `${left}|${right}`;
123
+ if (!pairKeys.has(key)) {
124
+ pairKeys.add(key);
125
+ pairs.push([left, right]);
126
+ }
127
+ }
128
+ }
129
+ });
130
+ return pairs;
131
+ };
132
+ export const scanDuplicateNotes = async (vaultPath, options = {}) => {
133
+ const notes = await loadNoteRecords(vaultPath, options.agentId);
134
+ if (notes.length < 2) {
135
+ return [];
136
+ }
137
+ const minSemanticScore = options.minSemanticScore ?? 0.92;
138
+ const includeSemantic = options.includeSemantic !== false;
139
+ const seen = new Map();
140
+ const byHash = notes.reduce((state, note) => {
141
+ const key = toHash(note.normalizedStrictContent);
142
+ const current = state.get(key) ?? [];
143
+ current.push(note);
144
+ state.set(key, current);
145
+ return state;
146
+ }, new Map());
147
+ byHash.forEach((group) => {
148
+ if (group.length < 2) {
149
+ return;
150
+ }
151
+ const [base, ...rest] = group.sort((left, right) => left.path.localeCompare(right.path));
152
+ rest.forEach((note) => {
153
+ const candidate = pairToCandidate(base, note, 'exact', 1, 'Exact content hash match');
154
+ seen.set(candidate.id, candidate);
155
+ });
156
+ });
157
+ if (includeSemantic) {
158
+ const pairs = indexCandidatePairs(notes);
159
+ pairs.forEach(([leftIndex, rightIndex]) => {
160
+ const left = notes[leftIndex];
161
+ const right = notes[rightIndex];
162
+ if (!left || !right || left.path === right.path) {
163
+ return;
164
+ }
165
+ const id = toCandidateId(left.path, right.path);
166
+ if (seen.has(id)) {
167
+ return;
168
+ }
169
+ const score = cosineSimilarity(left.embedding, right.embedding);
170
+ const titleShared = hasSharedTokens(left.title, right.title);
171
+ const contentShared = hasSharedTokens(left.semanticContent, right.semanticContent);
172
+ if (score >= minSemanticScore && (titleShared || contentShared || score >= 0.975)) {
173
+ const candidate = pairToCandidate(left, right, 'semantic', score, 'High semantic similarity');
174
+ seen.set(id, candidate);
175
+ }
176
+ });
177
+ }
178
+ const focusPath = options.focusPath ? normalizePath(options.focusPath) : undefined;
179
+ const limited = Array.from(seen.values())
180
+ .filter((item) => (focusPath ? item.left.path === focusPath || item.right.path === focusPath : true))
181
+ .sort((left, right) => right.score - left.score || left.left.path.localeCompare(right.left.path))
182
+ .slice(0, Math.max(1, options.limit ?? 25));
183
+ return limited;
184
+ };
185
+ export const resolveDuplicateNotes = async (vaultPath, options) => {
186
+ const leftPath = normalizePath(options.leftPath);
187
+ const rightPath = normalizePath(options.rightPath);
188
+ if (leftPath === rightPath) {
189
+ throw new Error('leftPath and rightPath must be different notes.');
190
+ }
191
+ const notes = await loadNoteRecords(vaultPath);
192
+ const byPath = new Map(notes.map((note) => [note.path, note]));
193
+ const left = byPath.get(leftPath);
194
+ const right = byPath.get(rightPath);
195
+ if (!left || !right) {
196
+ throw new Error(`Duplicate resolution paths were not found in vault index source: ${leftPath}, ${rightPath}`);
197
+ }
198
+ const updates = new Map();
199
+ const leftRelated = ensureRelatedEdgeLine(left.content, right.title);
200
+ const rightRelated = ensureRelatedEdgeLine(right.content, left.title);
201
+ if (options.action === 'link') {
202
+ updates.set(left.path, leftRelated);
203
+ updates.set(right.path, rightRelated);
204
+ }
205
+ else if (options.action === 'ignore') {
206
+ updates.set(left.path, leftRelated);
207
+ }
208
+ else {
209
+ const mergedLeft = appendMergedContent(leftRelated, right.title, right.content);
210
+ const mergedRight = ensureMergedMarker(rightRelated, left.title);
211
+ updates.set(left.path, mergedLeft);
212
+ updates.set(right.path, mergedRight);
213
+ }
214
+ for (const [path, content] of updates) {
215
+ await writeMarkdownFile(vaultPath, path, content);
216
+ }
217
+ const shouldIndex = options.autoIndex !== false;
218
+ const index = shouldIndex ? await indexVault(vaultPath) : undefined;
219
+ return {
220
+ action: options.action,
221
+ leftPath,
222
+ rightPath,
223
+ updatedPaths: Array.from(updates.keys()).sort((leftValue, rightValue) => leftValue.localeCompare(rightValue)),
224
+ ...(index ? { index } : {})
225
+ };
226
+ };