@andespindola/brainlink 0.1.0-beta.8 → 0.1.0-beta.9
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 +32 -0
- package/README.md +127 -4
- package/dist/application/add-note.js +62 -13
- package/dist/application/analyze-vault.js +89 -2
- package/dist/application/migrate-vault.js +46 -16
- package/dist/application/search-knowledge.js +56 -1
- package/dist/cli/commands/agent-commands.js +402 -0
- package/dist/cli/commands/config-commands.js +167 -0
- package/dist/cli/commands/read-commands.js +25 -8
- package/dist/cli/commands/write-commands.js +157 -4
- package/dist/cli/main.js +4 -0
- package/dist/cli/runtime.js +5 -2
- package/dist/domain/markdown.js +36 -4
- package/dist/infrastructure/config.js +94 -8
- package/dist/infrastructure/paths.js +9 -1
- package/dist/infrastructure/session-state.js +117 -0
- package/dist/mcp/main.js +11 -3
- package/dist/mcp/server.js +17 -2
- package/dist/mcp/startup.js +35 -0
- package/dist/mcp/tools.js +421 -19
- package/docs/AGENT_USAGE.md +77 -2
- package/docs/ARCHITECTURE.md +13 -1
- package/docs/QUICKSTART.md +103 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.4
|
|
4
|
+
|
|
5
|
+
- Added bootstrap session-state persistence in `$BRAINLINK_HOME/session-state.json` for vault/agent readiness tracking.
|
|
6
|
+
- Added MCP `brainlink_policy` tool and default bootstrap enforcement for read tools.
|
|
7
|
+
- Added `agent install --self-test` diagnostics and bootstrap readiness details in `agent status`.
|
|
8
|
+
- Added `agent upgrade` for legacy installations to reapply latest MCP/plugin defaults with self-test diagnostics.
|
|
9
|
+
- Added `config doctor --fix` safe autofix mode with dry-run default behavior.
|
|
10
|
+
- Added detailed per-file migration reporting through `migrate-vault --report`.
|
|
11
|
+
- Added `quickstart` command to run plug-and-play vault + bootstrap + agent setup in one flow.
|
|
12
|
+
- Added structured MCP `nextActions` in bootstrap/policy/preflight responses for automatic client continuation.
|
|
13
|
+
- Added default MCP read auto-bootstrap behavior controlled by `brainlink_policy.autoBootstrapOnRead`.
|
|
14
|
+
- Added default MCP startup bootstrap behavior controlled by `brainlink_policy.autoBootstrapOnStartup`.
|
|
15
|
+
- Added CLI MCP policy presets through `blink agent policy --preset fully-auto|strict`.
|
|
16
|
+
- Added write-time non-orphan enforcement by auto-linking notes without wiki edges to agent hub notes.
|
|
17
|
+
- Added MCP `brainlink_policy` presets (`fully-auto`, `strict`) for one-call policy switching.
|
|
18
|
+
- Added MCP write connectivity metadata in `brainlink_add_note`/`brainlink_add_file` responses.
|
|
19
|
+
- Added MCP `brainlink_recommendations` tool for plug-and-play workflow guidance.
|
|
20
|
+
- Improved graph/index robustness by splitting oversized paragraphs into bounded chunks and dropping self-referential links.
|
|
21
|
+
- Added `agentProfiles` configuration support so CLI and MCP can resolve per-agent defaults for mode/limit/tokens.
|
|
22
|
+
- Added short-lived hybrid search cache with automatic invalidation on index changes.
|
|
23
|
+
- Added `stats --extended` observability output with storage, quality and latency probes.
|
|
24
|
+
- Added `docs/QUICKSTART.md` and aligned README/agent docs with the latest CLI/MCP flows.
|
|
25
|
+
|
|
26
|
+
## 0.1.0-beta.3
|
|
27
|
+
|
|
28
|
+
- Added CLI configuration commands for effective vault management, including `config where`, `config get`, `config doctor` and `config set-vault`.
|
|
29
|
+
- Added explicit `migrate-vault` command with `--dry-run` preview and conflict-preserving copy behavior.
|
|
30
|
+
- Added one-command agent setup through `agent install` plus `agent status` diagnostics.
|
|
31
|
+
- Added MCP `brainlink_bootstrap` default entrypoint guidance for plug-and-play agent memory flows.
|
|
32
|
+
- Added migration coverage for S3 bucket vault targets.
|
|
33
|
+
- Updated architecture and agent-usage documentation to reflect current CLI/MCP behavior and configuration precedence.
|
|
34
|
+
|
|
3
35
|
## 0.1.0-beta.2
|
|
4
36
|
|
|
5
37
|
- Added MCP installation guidance for direct server configuration and local client stores.
|
package/README.md
CHANGED
|
@@ -106,6 +106,8 @@ npx @andespindola/brainlink@latest --help
|
|
|
106
106
|
|
|
107
107
|
This section is written for agents. If you are an AI agent operating in a user workspace, use this workflow to install and use Brainlink without requiring the user to run commands manually.
|
|
108
108
|
|
|
109
|
+
For a condensed setup path, see [docs/QUICKSTART.md](./docs/QUICKSTART.md).
|
|
110
|
+
|
|
109
111
|
### 1. Check Whether Brainlink Is Installed
|
|
110
112
|
|
|
111
113
|
```bash
|
|
@@ -380,6 +382,35 @@ Example MCP client configuration:
|
|
|
380
382
|
}
|
|
381
383
|
```
|
|
382
384
|
|
|
385
|
+
### One-Command Agent Setup
|
|
386
|
+
|
|
387
|
+
If your agent runtime is Codex-compatible, run:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
blink agent install --self-test
|
|
391
|
+
blink agent upgrade
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
This configures `~/.codex/config.toml` with Brainlink MCP (`brainlink-mcp`) so Brainlink is available by default in agent sessions.
|
|
395
|
+
|
|
396
|
+
If you are inside this repository and want plugin gallery setup too:
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
blink agent install --plugin-path ./plugins/brainlink
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
To verify:
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
blink agent status
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
For fully automated first run (vault index + health + bootstrap readiness + agent integration):
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
blink quickstart --query "what should I know before this task?" --json
|
|
412
|
+
```
|
|
413
|
+
|
|
383
414
|
For a locked-down setup, allowlist the vaults that MCP clients may access:
|
|
384
415
|
|
|
385
416
|
```json
|
|
@@ -475,6 +506,9 @@ Restart the client after changing marketplace or MCP configuration so it reloads
|
|
|
475
506
|
|
|
476
507
|
Available tools:
|
|
477
508
|
|
|
509
|
+
- `brainlink_bootstrap`: plug-and-play entrypoint that runs index + health checks and can return context in one call.
|
|
510
|
+
- `brainlink_policy`: read or update bootstrap enforcement policy, including presets (`preset: "fully-auto" | "strict"`).
|
|
511
|
+
- `brainlink_recommendations`: return an automatic action plan so agents can run Brainlink in the recommended order.
|
|
478
512
|
- `brainlink_context`: read indexed context for a task or question.
|
|
479
513
|
- `brainlink_search`: search indexed notes.
|
|
480
514
|
- `brainlink_add_note`: write durable Markdown memory and reindex.
|
|
@@ -487,7 +521,14 @@ Available tools:
|
|
|
487
521
|
- `brainlink_broken_links`: list unresolved wiki links.
|
|
488
522
|
- `brainlink_orphans`: list disconnected notes.
|
|
489
523
|
|
|
490
|
-
|
|
524
|
+
For the most automatic workflow, start MCP sessions with `brainlink_bootstrap` (optionally with `query`) and then continue with `brainlink_context`/`brainlink_add_note`.
|
|
525
|
+
By default, MCP startup already runs bootstrap on the configured default vault/agent (`autoBootstrapOnStartup=true`), so sessions begin warm.
|
|
526
|
+
By default, Brainlink enforces bootstrap and auto-runs it for read tools when session state is missing or stale (`autoBootstrapOnRead=true`).
|
|
527
|
+
If you disable `autoBootstrapOnRead` through `brainlink_policy`, read tools return a preflight instruction with suggested `brainlink_bootstrap` arguments.
|
|
528
|
+
`brainlink_bootstrap`, `brainlink_policy` and preflight responses include structured `nextActions` so MCP clients can continue automatically without custom parsing.
|
|
529
|
+
For one-call planning, use `brainlink_recommendations` to get the recommended tool sequence for the current vault/agent/query.
|
|
530
|
+
|
|
531
|
+
The same linking rule applies through MCP: `brainlink_context` is read-only, and real graph links require Markdown notes with explicit `[[wiki links]]`. `brainlink_add_note` and `brainlink_add_file` reindex by default and include index + `writeConnectivity` metadata. Brainlink guarantees at least one edge per new note by auto-linking when needed.
|
|
491
532
|
|
|
492
533
|
Agents can raise the importance of a relationship by putting priority markers on the same line as a wiki link:
|
|
493
534
|
|
|
@@ -556,6 +597,65 @@ Read routes accept `agent=<agent-id>`:
|
|
|
556
597
|
|
|
557
598
|
Every command works with either `brainlink` or `blink`.
|
|
558
599
|
|
|
600
|
+
### `agent`
|
|
601
|
+
|
|
602
|
+
```bash
|
|
603
|
+
blink agent install
|
|
604
|
+
blink agent install --self-test
|
|
605
|
+
blink agent upgrade
|
|
606
|
+
blink agent policy --preset fully-auto
|
|
607
|
+
blink agent policy --preset strict
|
|
608
|
+
blink agent install --plugin-path ./plugins/brainlink
|
|
609
|
+
blink agent install --mcp-only --allowed-vaults "/absolute/vault,/absolute/team-vault"
|
|
610
|
+
blink agent status
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
Installs/checks agent integration. `install` writes Brainlink MCP config into `~/.codex/config.toml`.
|
|
614
|
+
When plugin files are available, it also links Brainlink plugin files into `~/plugins/brainlink` and updates `~/.agents/plugins/marketplace.json`.
|
|
615
|
+
With `--self-test`, install also validates MCP block presence, command wiring and local plugin registration signals.
|
|
616
|
+
Use `agent upgrade` on legacy installations to reapply current defaults and run the same self-test diagnostics.
|
|
617
|
+
Use `agent policy --preset fully-auto` for plug-and-play defaults, or `agent policy --preset strict` to require explicit bootstrap calls.
|
|
618
|
+
|
|
619
|
+
### `quickstart`
|
|
620
|
+
|
|
621
|
+
```bash
|
|
622
|
+
blink quickstart --json
|
|
623
|
+
blink quickstart --vault ./team-vault --agent coding-agent --query "architecture decisions" --json
|
|
624
|
+
blink quickstart --vault ./team-vault --mcp-only --json
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
Runs index + doctor + stats + validation, refreshes bootstrap session readiness, optionally returns context for a query, and (by default) upgrades local agent integration for plug-and-play MCP usage.
|
|
628
|
+
When `--mode`, `--limit` or `--tokens` are omitted, quickstart uses agent profile defaults when available.
|
|
629
|
+
|
|
630
|
+
### `config`
|
|
631
|
+
|
|
632
|
+
```bash
|
|
633
|
+
blink config where
|
|
634
|
+
blink config get vault
|
|
635
|
+
blink config doctor
|
|
636
|
+
blink config doctor --fix
|
|
637
|
+
blink config set-vault /absolute/path/to/existing-vault
|
|
638
|
+
blink config set-vault /absolute/path/to/existing-vault --migrate-from ~/.brainlink/vault
|
|
639
|
+
blink config set-vault "s3://my-memory-bucket/brainlink" --global
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
`config set-vault` writes configuration through CLI (no manual file edits required).
|
|
643
|
+
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.
|
|
644
|
+
Use `--global` to write to `$BRAINLINK_HOME/brainlink.config.json`, `--no-migrate` to skip migration, and `--no-index` to skip post-migration indexing.
|
|
645
|
+
`config doctor` is dry-run by default; use `--fix` to apply safe config normalization and allowlist fixes.
|
|
646
|
+
|
|
647
|
+
### `migrate-vault`
|
|
648
|
+
|
|
649
|
+
```bash
|
|
650
|
+
blink migrate-vault --from ~/.brainlink/vault --to ./team-vault --dry-run
|
|
651
|
+
blink migrate-vault --from ~/.brainlink/vault --to ./team-vault
|
|
652
|
+
blink migrate-vault --from ~/.brainlink/vault --to "s3://my-memory-bucket/brainlink"
|
|
653
|
+
blink migrate-vault --from ~/.brainlink/vault --to ./team-vault --report ./migration-report.json
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Runs explicit markdown migration between vaults while preserving conflicts as `.conflict-<timestamp>` files.
|
|
657
|
+
Use `--dry-run` to preview `copied`, `conflicted` and `unchanged` counts before writing.
|
|
658
|
+
|
|
559
659
|
### `init`
|
|
560
660
|
|
|
561
661
|
```bash
|
|
@@ -579,6 +679,7 @@ blink add "Note Title" --vault ./vault --content-file ./notes.md --no-auto-index
|
|
|
579
679
|
`--content` and `--content-file` are mutually exclusive. Add `--no-auto-index` when you want to defer reindexing.
|
|
580
680
|
|
|
581
681
|
Creates a Markdown note under `agents/<agent-id>/`. Common secret patterns are blocked by default; use `--allow-sensitive` only for an intentionally protected vault.
|
|
682
|
+
To avoid disconnected memory, Brainlink auto-adds a fallback wiki edge when a note is written without links, creating agent hub notes when needed.
|
|
582
683
|
|
|
583
684
|
### `index`
|
|
584
685
|
|
|
@@ -607,6 +708,7 @@ blink search "query" --vault ./vault --mode semantic --json
|
|
|
607
708
|
```
|
|
608
709
|
|
|
609
710
|
Runs retrieval over indexed chunks.
|
|
711
|
+
If `--mode` or `--limit` is omitted, Brainlink resolves values from the current agent profile before falling back to global defaults.
|
|
610
712
|
|
|
611
713
|
Modes:
|
|
612
714
|
|
|
@@ -614,6 +716,8 @@ Modes:
|
|
|
614
716
|
- `fts`: exact lexical retrieval through SQLite FTS.
|
|
615
717
|
- `semantic`: local deterministic embedding similarity only.
|
|
616
718
|
|
|
719
|
+
Hybrid results are cached in-memory for a short TTL and invalidated automatically when the local index file changes.
|
|
720
|
+
|
|
617
721
|
### `context`
|
|
618
722
|
|
|
619
723
|
```bash
|
|
@@ -656,9 +760,11 @@ Prints indexed graph data. Edges include `weight` and `priority` so agents can c
|
|
|
656
760
|
```bash
|
|
657
761
|
blink stats --vault ./vault
|
|
658
762
|
blink stats --vault ./vault --agent coding-agent --json
|
|
763
|
+
blink stats --vault ./vault --agent coding-agent --extended --json
|
|
659
764
|
```
|
|
660
765
|
|
|
661
766
|
Prints vault metrics.
|
|
767
|
+
Use `--extended` to include storage footprint, link quality ratios and observability probes (`index`, `search`, `context` latencies).
|
|
662
768
|
|
|
663
769
|
### `broken-links`
|
|
664
770
|
|
|
@@ -690,7 +796,7 @@ Validates graph health. The command exits non-zero when required checks fail.
|
|
|
690
796
|
blink doctor --vault ./vault
|
|
691
797
|
```
|
|
692
798
|
|
|
693
|
-
Runs environment and vault checks.
|
|
799
|
+
Runs environment and vault checks. When vault has zero markdown and zero indexed documents, `doctor` prints recommended next steps (add note, inspect config source, migrate memory).
|
|
694
800
|
|
|
695
801
|
### `watch`
|
|
696
802
|
|
|
@@ -727,7 +833,13 @@ npm run --silent dev -- context "question" --vault ./vault --json
|
|
|
727
833
|
|
|
728
834
|
## Configuration
|
|
729
835
|
|
|
730
|
-
Brainlink
|
|
836
|
+
Brainlink merges configuration in this order:
|
|
837
|
+
|
|
838
|
+
1. Global: `$BRAINLINK_HOME/brainlink.config.json` (or `$HOME/.brainlink/brainlink.config.json` by default)
|
|
839
|
+
2. Local: `brainlink.config.json` in the current working directory
|
|
840
|
+
3. Local legacy compatibility: `.brainlink.json` in the current working directory
|
|
841
|
+
|
|
842
|
+
If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HOME/.brainlink/vault`.
|
|
731
843
|
|
|
732
844
|
```json
|
|
733
845
|
{
|
|
@@ -741,11 +853,22 @@ Brainlink reads `brainlink.config.json` or `.brainlink.json` from the current wo
|
|
|
741
853
|
"defaultContextTokens": 2000,
|
|
742
854
|
"embeddingProvider": "local",
|
|
743
855
|
"defaultSearchMode": "hybrid",
|
|
744
|
-
"chunkSize": 1200
|
|
856
|
+
"chunkSize": 1200,
|
|
857
|
+
"agentProfiles": {
|
|
858
|
+
"coding-agent": {
|
|
859
|
+
"defaultSearchMode": "semantic",
|
|
860
|
+
"defaultSearchLimit": 8,
|
|
861
|
+
"defaultContextTokens": 2400
|
|
862
|
+
},
|
|
863
|
+
"*": {
|
|
864
|
+
"defaultSearchMode": "hybrid"
|
|
865
|
+
}
|
|
866
|
+
}
|
|
745
867
|
}
|
|
746
868
|
```
|
|
747
869
|
|
|
748
870
|
`defaultAgent` is optional. When set, CLI and MCP calls that omit `--agent`/`agent` use this value automatically. If not set, behavior remains as before.
|
|
871
|
+
`agentProfiles` is optional. When present, CLI and MCP resolve `mode`, `limit` and `tokens` per agent automatically, then fallback to global defaults.
|
|
749
872
|
|
|
750
873
|
`autoIndexOnWrite` is optional and defaults to `true`. Set it to `false` to defer indexing after writes.
|
|
751
874
|
|
|
@@ -1,30 +1,79 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { writeMarkdownFile } from '../infrastructure/file-system-vault.js';
|
|
2
4
|
import { sanitizeAgentId, sharedAgentId } from '../domain/agents.js';
|
|
5
|
+
import { extractWikiLinks } from '../domain/markdown.js';
|
|
3
6
|
import { validateNoteInput } from '../domain/note-safety.js';
|
|
7
|
+
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
4
8
|
const slugify = (title) => title
|
|
5
9
|
.normalize('NFKD')
|
|
6
10
|
.replace(/[\u0300-\u036f]/g, '')
|
|
7
11
|
.toLowerCase()
|
|
8
12
|
.replace(/[^a-z0-9]+/g, '-')
|
|
9
13
|
.replace(/^-+|-+$/g, '');
|
|
10
|
-
|
|
14
|
+
const systemHubTitle = 'Memory Hub';
|
|
15
|
+
const systemRootTitle = 'Knowledge Root';
|
|
16
|
+
const normalizeTitle = (title) => title.trim().replace(/\.md$/i, '').toLowerCase();
|
|
17
|
+
const noteFilename = (agentId, title) => `agents/${agentId}/${slugify(title) || 'untitled'}.md`;
|
|
18
|
+
const buildNote = (title, content, agentId) => [
|
|
19
|
+
`---`,
|
|
20
|
+
`title: "${title.replaceAll('"', '\\"')}"`,
|
|
21
|
+
`agent: "${agentId}"`,
|
|
22
|
+
`---`,
|
|
23
|
+
'',
|
|
24
|
+
`# ${title}`,
|
|
25
|
+
'',
|
|
26
|
+
content.trim(),
|
|
27
|
+
''
|
|
28
|
+
].join('\n');
|
|
29
|
+
const ensureSystemNote = async (vaultPath, absoluteVaultPath, agentId, title, content) => {
|
|
30
|
+
const filename = noteFilename(agentId, title);
|
|
31
|
+
const absolutePath = join(absoluteVaultPath, filename);
|
|
32
|
+
try {
|
|
33
|
+
await access(absolutePath);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
await writeMarkdownFile(vaultPath, filename, buildNote(title, content, agentId));
|
|
38
|
+
};
|
|
39
|
+
const ensureNonOrphanContent = async (vaultPath, absoluteVaultPath, title, content, agentId) => {
|
|
40
|
+
const links = extractWikiLinks(content).filter((link) => normalizeTitle(link) !== normalizeTitle(title));
|
|
41
|
+
if (links.length > 0) {
|
|
42
|
+
return {
|
|
43
|
+
content: content.trim(),
|
|
44
|
+
autoLinked: false,
|
|
45
|
+
linkTarget: null
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const fallbackTitle = normalizeTitle(title) === normalizeTitle(systemHubTitle) ? systemRootTitle : systemHubTitle;
|
|
49
|
+
if (fallbackTitle === systemRootTitle) {
|
|
50
|
+
await ensureSystemNote(vaultPath, absoluteVaultPath, agentId, systemRootTitle, `Entry point for agent memory. [[${systemHubTitle}]] #memory #root`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await ensureSystemNote(vaultPath, absoluteVaultPath, agentId, systemHubTitle, 'Central memory index for this agent namespace. #memory #hub');
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
content: `${content.trim()}\n\nRelated: [[${fallbackTitle}]]`,
|
|
57
|
+
autoLinked: true,
|
|
58
|
+
linkTarget: fallbackTitle
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
export const addNoteWithMetadata = async (vaultPath, title, content, agentId = sharedAgentId, options = {}) => {
|
|
11
62
|
validateNoteInput({
|
|
12
63
|
title,
|
|
13
64
|
content,
|
|
14
65
|
allowSensitive: options.allowSensitive
|
|
15
66
|
});
|
|
16
67
|
const sanitizedAgentId = sanitizeAgentId(agentId);
|
|
68
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
17
69
|
const filename = `agents/${sanitizedAgentId}/${slugify(title) || 'untitled'}.md`;
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
content.trim(),
|
|
27
|
-
''
|
|
28
|
-
].join('\n');
|
|
29
|
-
return writeMarkdownFile(vaultPath, filename, note);
|
|
70
|
+
const linkedContent = await ensureNonOrphanContent(vaultPath, absoluteVaultPath, title, content, sanitizedAgentId);
|
|
71
|
+
const note = buildNote(title, linkedContent.content, sanitizedAgentId);
|
|
72
|
+
const path = await writeMarkdownFile(vaultPath, filename, note);
|
|
73
|
+
return {
|
|
74
|
+
path,
|
|
75
|
+
autoLinked: linkedContent.autoLinked,
|
|
76
|
+
linkTarget: linkedContent.linkTarget
|
|
77
|
+
};
|
|
30
78
|
};
|
|
79
|
+
export const addNote = async (vaultPath, title, content, agentId = sharedAgentId, options = {}) => (await addNoteWithMetadata(vaultPath, title, content, agentId, options)).path;
|
|
@@ -1,10 +1,89 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
1
3
|
import { validateGraph, getBrokenLinks, getOrphanNodes, getVaultStats } from '../domain/graph-analysis.js';
|
|
2
|
-
import { ensureVault, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
|
|
4
|
+
import { ensureVault, listVaultFiles, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
|
|
5
|
+
import { resolveAgentRuntimeDefaults } from '../infrastructure/config.js';
|
|
3
6
|
import { getGraph } from './get-graph.js';
|
|
7
|
+
import { buildContextPackage } from './build-context.js';
|
|
8
|
+
import { indexVault } from './index-vault.js';
|
|
9
|
+
import { searchKnowledge } from './search-knowledge.js';
|
|
10
|
+
import { loadBrainlinkConfig } from '../infrastructure/config.js';
|
|
4
11
|
export const getStats = async (vaultPath, agentId) => getVaultStats(await getGraph(vaultPath, agentId));
|
|
5
12
|
export const getBrokenLinksReport = async (vaultPath, agentId) => getBrokenLinks(await getGraph(vaultPath, agentId));
|
|
6
13
|
export const getOrphansReport = async (vaultPath, agentId) => getOrphanNodes(await getGraph(vaultPath, agentId));
|
|
7
14
|
export const validateVault = async (vaultPath, agentId) => validateGraph(await getGraph(vaultPath, agentId));
|
|
15
|
+
const toRatio = (part, total) => total === 0 ? 0 : Number((part / total).toFixed(4));
|
|
16
|
+
export const getExtendedStats = async (vaultPath, agentId) => {
|
|
17
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
18
|
+
const graph = await getGraph(absoluteVaultPath, agentId);
|
|
19
|
+
const stats = getVaultStats(graph);
|
|
20
|
+
const markdownFiles = await readMarkdownFiles(absoluteVaultPath);
|
|
21
|
+
const allFiles = await listVaultFiles(absoluteVaultPath);
|
|
22
|
+
const totalBytes = (await Promise.all(allFiles.map(async (filePath) => {
|
|
23
|
+
try {
|
|
24
|
+
return (await stat(filePath)).size;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
}))).reduce((sum, value) => sum + value, 0);
|
|
30
|
+
const updatedAt = markdownFiles
|
|
31
|
+
.map((file) => file.updatedAt.getTime())
|
|
32
|
+
.filter((time) => Number.isFinite(time))
|
|
33
|
+
.sort((left, right) => left - right);
|
|
34
|
+
const priorities = graph.edges.reduce((state, edge) => ({
|
|
35
|
+
...state,
|
|
36
|
+
[edge.priority]: state[edge.priority] + 1
|
|
37
|
+
}), {
|
|
38
|
+
low: 0,
|
|
39
|
+
normal: 0,
|
|
40
|
+
high: 0,
|
|
41
|
+
critical: 0
|
|
42
|
+
});
|
|
43
|
+
const config = await loadBrainlinkConfig();
|
|
44
|
+
const defaults = resolveAgentRuntimeDefaults(config, agentId);
|
|
45
|
+
const probeQuery = graph.nodes[0]?.title ?? 'architecture';
|
|
46
|
+
const indexStart = performance.now();
|
|
47
|
+
await indexVault(absoluteVaultPath);
|
|
48
|
+
const indexLatency = performance.now() - indexStart;
|
|
49
|
+
const searchStart = performance.now();
|
|
50
|
+
await searchKnowledge(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), agentId, 'hybrid');
|
|
51
|
+
const searchLatency = performance.now() - searchStart;
|
|
52
|
+
const contextStart = performance.now();
|
|
53
|
+
await buildContextPackage(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), defaults.defaultContextTokens, agentId, 'hybrid');
|
|
54
|
+
const contextLatency = performance.now() - contextStart;
|
|
55
|
+
return {
|
|
56
|
+
stats,
|
|
57
|
+
storage: {
|
|
58
|
+
markdownFileCount: markdownFiles.length,
|
|
59
|
+
totalFileCount: allFiles.length,
|
|
60
|
+
totalBytes,
|
|
61
|
+
averageMarkdownBytes: markdownFiles.length === 0
|
|
62
|
+
? 0
|
|
63
|
+
: Math.round(markdownFiles.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0) / markdownFiles.length),
|
|
64
|
+
...(updatedAt.length > 0
|
|
65
|
+
? {
|
|
66
|
+
oldestNoteUpdatedAt: new Date(updatedAt[0]).toISOString(),
|
|
67
|
+
newestNoteUpdatedAt: new Date(updatedAt[updatedAt.length - 1]).toISOString()
|
|
68
|
+
}
|
|
69
|
+
: {})
|
|
70
|
+
},
|
|
71
|
+
quality: {
|
|
72
|
+
resolvedLinkRatio: toRatio(stats.resolvedLinkCount, stats.linkCount),
|
|
73
|
+
brokenLinkRatio: toRatio(stats.brokenLinkCount, stats.linkCount),
|
|
74
|
+
orphanRatio: toRatio(stats.orphanCount, Math.max(stats.documentCount, 1)),
|
|
75
|
+
priorityDistribution: priorities
|
|
76
|
+
},
|
|
77
|
+
observability: {
|
|
78
|
+
probeQuery,
|
|
79
|
+
latenciesMs: {
|
|
80
|
+
index: Number(indexLatency.toFixed(2)),
|
|
81
|
+
search: Number(searchLatency.toFixed(2)),
|
|
82
|
+
context: Number(contextLatency.toFixed(2))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
};
|
|
8
87
|
const createCheck = (name, ok, message) => ({
|
|
9
88
|
name,
|
|
10
89
|
ok,
|
|
@@ -21,8 +100,16 @@ export const doctorVault = async (vaultPath) => {
|
|
|
21
100
|
createCheck('index', graph.nodes.length > 0, `${graph.nodes.length} indexed documents found`),
|
|
22
101
|
createCheck('broken-links', validation.brokenLinks.length === 0, `${validation.brokenLinks.length} broken links found`)
|
|
23
102
|
];
|
|
103
|
+
const recommendations = files.length === 0 && graph.nodes.length === 0
|
|
104
|
+
? [
|
|
105
|
+
`Vault is empty. Add your first note: blink add "Architecture" --vault "${absoluteVaultPath}" --content "Markdown source of truth. #architecture"`,
|
|
106
|
+
`If this path is not the expected vault, inspect active config: blink config where`,
|
|
107
|
+
`If you changed vault recently, migrate existing memory: blink migrate-vault --from ~/.brainlink/vault --to "${absoluteVaultPath}"`
|
|
108
|
+
]
|
|
109
|
+
: [];
|
|
24
110
|
return {
|
|
25
111
|
ok: checks.every((check) => check.ok),
|
|
26
|
-
checks
|
|
112
|
+
checks,
|
|
113
|
+
...(recommendations.length > 0 ? { recommendations } : {})
|
|
27
114
|
};
|
|
28
115
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, extname, isAbsolute, join, relative } from 'node:path';
|
|
3
|
-
import { ensureVault, listVaultFiles, resolveVaultPath } from '../infrastructure/file-system-vault.js';
|
|
3
|
+
import { ensureVault, isBucketVaultPath, listVaultFiles, resolveVaultPath, writeMarkdownFile } from '../infrastructure/file-system-vault.js';
|
|
4
4
|
const directoryMode = 0o700;
|
|
5
5
|
const fileMode = 0o600;
|
|
6
|
+
const isMarkdownPath = (path) => extname(path).toLowerCase() === '.md';
|
|
6
7
|
const timestamp = () => new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, 'Z');
|
|
7
8
|
const isPathInside = (parent, child) => {
|
|
8
9
|
const path = relative(parent, child);
|
|
@@ -18,14 +19,16 @@ const writePreservedFile = async (absolutePath, content) => {
|
|
|
18
19
|
await writeFile(absolutePath, content, { mode: fileMode });
|
|
19
20
|
await chmod(absolutePath, fileMode);
|
|
20
21
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return { source, target, copied: 0, unchanged: 0, conflicted: 0 };
|
|
22
|
+
const writeMigratedFile = async (targetVault, targetRoot, absolutePath, content) => {
|
|
23
|
+
if (isBucketVaultPath(targetVault)) {
|
|
24
|
+
await writeMarkdownFile(targetVault, relative(targetRoot, absolutePath), content.toString('utf8'));
|
|
25
|
+
return;
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
await writePreservedFile(absolutePath, content);
|
|
28
|
+
};
|
|
29
|
+
export const planVaultMigration = async (source, target) => {
|
|
30
|
+
const sourceFiles = (await listVaultFiles(source)).filter(isMarkdownPath);
|
|
31
|
+
return sourceFiles.reduce(async (statePromise, sourceFile) => {
|
|
29
32
|
const state = await statePromise;
|
|
30
33
|
const targetFile = join(target, relative(source, sourceFile));
|
|
31
34
|
if (!isPathInside(target, targetFile)) {
|
|
@@ -35,20 +38,47 @@ export const migrateVaultContent = async (sourceVault, targetVault) => {
|
|
|
35
38
|
try {
|
|
36
39
|
const targetContent = await readFile(targetFile);
|
|
37
40
|
if (sourceContent.equals(targetContent)) {
|
|
38
|
-
return
|
|
41
|
+
return [...state, { kind: 'unchanged', sourcePath: sourceFile, targetPath: targetFile, sourceContent }];
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
return { ...state, conflicted: state.conflicted + 1 };
|
|
43
|
+
return [...state, { kind: 'conflict', sourcePath: sourceFile, targetPath: conflictPath(targetFile), sourceContent }];
|
|
42
44
|
}
|
|
43
45
|
catch (error) {
|
|
44
46
|
if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') {
|
|
45
47
|
throw error;
|
|
46
48
|
}
|
|
47
|
-
|
|
48
|
-
return { ...state, copied: state.copied + 1 };
|
|
49
|
+
return [...state, { kind: 'copy', sourcePath: sourceFile, targetPath: targetFile, sourceContent }];
|
|
49
50
|
}
|
|
50
|
-
}, Promise.resolve(
|
|
51
|
-
|
|
51
|
+
}, Promise.resolve([]));
|
|
52
|
+
};
|
|
53
|
+
export const previewVaultMigration = async (sourceVault, targetVault) => {
|
|
54
|
+
const source = await ensureVault(sourceVault);
|
|
55
|
+
const target = await ensureVault(targetVault);
|
|
56
|
+
if (source === target) {
|
|
57
|
+
return { source, target, copied: 0, unchanged: 0, conflicted: 0 };
|
|
58
|
+
}
|
|
59
|
+
const actions = await planVaultMigration(source, target);
|
|
60
|
+
const copied = actions.filter((action) => action.kind === 'copy').length;
|
|
61
|
+
const unchanged = actions.filter((action) => action.kind === 'unchanged').length;
|
|
62
|
+
const conflicted = actions.filter((action) => action.kind === 'conflict').length;
|
|
63
|
+
return { source, target, copied, unchanged, conflicted };
|
|
64
|
+
};
|
|
65
|
+
export const migrateVaultContent = async (sourceVault, targetVault) => {
|
|
66
|
+
const source = await ensureVault(sourceVault);
|
|
67
|
+
const target = await ensureVault(targetVault);
|
|
68
|
+
if (source === target) {
|
|
69
|
+
return { source, target, copied: 0, unchanged: 0, conflicted: 0 };
|
|
70
|
+
}
|
|
71
|
+
const actions = await planVaultMigration(source, target);
|
|
72
|
+
for (const action of actions) {
|
|
73
|
+
if (action.kind === 'unchanged') {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
await writeMigratedFile(targetVault, target, action.targetPath, action.sourceContent);
|
|
77
|
+
}
|
|
78
|
+
const copied = actions.filter((action) => action.kind === 'copy').length;
|
|
79
|
+
const unchanged = actions.filter((action) => action.kind === 'unchanged').length;
|
|
80
|
+
const conflicted = actions.filter((action) => action.kind === 'conflict').length;
|
|
81
|
+
return { source, target, copied, unchanged, conflicted };
|
|
52
82
|
};
|
|
53
83
|
export const shouldMigrateDefaultVault = async (sourceVault, targetVault) => {
|
|
54
84
|
const source = resolveVaultPath(sourceVault);
|
|
@@ -57,5 +87,5 @@ export const shouldMigrateDefaultVault = async (sourceVault, targetVault) => {
|
|
|
57
87
|
return false;
|
|
58
88
|
}
|
|
59
89
|
const [sourceFiles, targetFiles] = await Promise.all([listVaultFiles(source), listVaultFiles(target)]);
|
|
60
|
-
return sourceFiles.length > 0 && targetFiles.length === 0;
|
|
90
|
+
return sourceFiles.filter(isMarkdownPath).length > 0 && targetFiles.filter(isMarkdownPath).length === 0;
|
|
61
91
|
};
|
|
@@ -1,17 +1,72 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
4
|
import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
|
|
3
5
|
import { createEmbeddingProvider } from '../domain/embeddings.js';
|
|
4
6
|
import { loadBrainlinkConfig, sanitizeSearchMode } from '../infrastructure/config.js';
|
|
7
|
+
const hybridCacheTtlMs = 30_000;
|
|
8
|
+
const hybridCacheMaxEntries = 200;
|
|
9
|
+
const hybridSearchCache = new Map();
|
|
10
|
+
const readIndexMtimeMs = async (vaultPath) => {
|
|
11
|
+
try {
|
|
12
|
+
return (await stat(join(vaultPath, '.brainlink', 'brainlink.db'))).mtimeMs;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const toCacheKey = (vaultPath, query, limit, agentId) => JSON.stringify({
|
|
19
|
+
vaultPath,
|
|
20
|
+
query: query.trim().toLowerCase(),
|
|
21
|
+
limit,
|
|
22
|
+
agentId: agentId?.trim().toLowerCase() ?? '*'
|
|
23
|
+
});
|
|
24
|
+
const cacheGet = (key, indexMtimeMs) => {
|
|
25
|
+
const entry = hybridSearchCache.get(key);
|
|
26
|
+
if (!entry) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const fresh = Date.now() - entry.createdAt <= hybridCacheTtlMs && entry.indexMtimeMs === indexMtimeMs;
|
|
30
|
+
if (!fresh) {
|
|
31
|
+
hybridSearchCache.delete(key);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return entry.results;
|
|
35
|
+
};
|
|
36
|
+
const cacheSet = (entry) => {
|
|
37
|
+
hybridSearchCache.set(entry.key, entry);
|
|
38
|
+
if (hybridSearchCache.size <= hybridCacheMaxEntries) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const overflow = hybridSearchCache.size - hybridCacheMaxEntries;
|
|
42
|
+
const keys = Array.from(hybridSearchCache.keys()).slice(0, overflow);
|
|
43
|
+
keys.forEach((key) => hybridSearchCache.delete(key));
|
|
44
|
+
};
|
|
5
45
|
export const searchKnowledge = async (vaultPath, query, limit, agentId, mode) => {
|
|
6
46
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
7
47
|
const config = await loadBrainlinkConfig();
|
|
8
48
|
const searchMode = sanitizeSearchMode(mode, config.defaultSearchMode);
|
|
49
|
+
const cacheKey = searchMode === 'hybrid' ? toCacheKey(absoluteVaultPath, query, limit, agentId) : undefined;
|
|
50
|
+
const indexMtimeMs = cacheKey ? await readIndexMtimeMs(absoluteVaultPath) : 0;
|
|
51
|
+
const cached = cacheKey ? cacheGet(cacheKey, indexMtimeMs) : undefined;
|
|
52
|
+
if (cached) {
|
|
53
|
+
return cached;
|
|
54
|
+
}
|
|
9
55
|
const provider = createEmbeddingProvider(config.embeddingProvider);
|
|
10
56
|
const shouldEmbedQuery = searchMode !== 'fts' && provider.name !== 'none';
|
|
11
57
|
const queryEmbedding = shouldEmbedQuery ? (await provider.embed([query]))[0] ?? [] : [];
|
|
12
58
|
const index = openSqliteIndex(absoluteVaultPath);
|
|
13
59
|
try {
|
|
14
|
-
|
|
60
|
+
const results = index.search(query, limit, agentId, searchMode, queryEmbedding);
|
|
61
|
+
if (cacheKey) {
|
|
62
|
+
cacheSet({
|
|
63
|
+
key: cacheKey,
|
|
64
|
+
createdAt: Date.now(),
|
|
65
|
+
indexMtimeMs,
|
|
66
|
+
results
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return results;
|
|
15
70
|
}
|
|
16
71
|
finally {
|
|
17
72
|
index.close();
|