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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -6,19 +6,19 @@ This file tells coding agents and AI assistants how to use this repository.
6
6
 
7
7
  Brainlink is a local-first knowledge memory for agents.
8
8
 
9
- It reads a Markdown vault, extracts `[[wiki links]]` and `#tags`, builds a local SQLite full-text index, and returns compact context packages that agents can inject into prompts.
9
+ It reads a Markdown vault, extracts `[[wiki links]]` and `#tags`, builds a local file index at `.brainlink/index.json`, and returns compact context packages that agents can inject into prompts.
10
10
 
11
11
  ## Source Of Truth
12
12
 
13
13
  Markdown files are the source of truth.
14
14
 
15
- The SQLite database at `.brainlink/brainlink.db` is a derived index. It can be deleted and rebuilt with:
15
+ The JSON index at `.brainlink/index.json` is derived. It can be deleted and rebuilt with:
16
16
 
17
17
  ```bash
18
18
  npm run dev -- index --vault ./vault
19
19
  ```
20
20
 
21
- Do not store permanent knowledge only in SQLite.
21
+ Do not store permanent knowledge only in index artifacts.
22
22
 
23
23
  By default, the installed Brainlink CLI uses `$HOME/.brainlink/vault` as its vault. Passing `--vault` or setting `vault` in `brainlink.config.json` intentionally selects a custom vault such as `./vault`.
24
24
 
@@ -107,10 +107,10 @@ npm run dev -- doctor --vault ./vault
107
107
 
108
108
  - Keep domain rules in `src/domain`.
109
109
  - Keep use cases in `src/application`.
110
- - Keep filesystem and SQLite details in `src/infrastructure`.
110
+ - Keep filesystem and index details in `src/infrastructure`.
111
111
  - Keep CLI concerns in `src/cli`.
112
112
  - Prefer pure functions for parsing, ranking, formatting, and transformation.
113
- - Do not make SQLite the canonical storage layer.
113
+ - Do not make index artifacts the canonical storage layer.
114
114
  - Do not add comments with emojis.
115
115
  - Keep JSON output backwards compatible where possible.
116
116
 
package/CHANGELOG.md CHANGED
@@ -49,8 +49,8 @@
49
49
  ## 0.1.0-alpha.0
50
50
 
51
51
  - Added local-first Markdown vault indexing.
52
- - Added SQLite FTS, local semantic retrieval, wiki links, backlinks and graph retrieval.
53
- - Added SQLite semantic bucket indexing to narrow vector candidates for larger vaults.
52
+ - Added local full-text indexing, local semantic retrieval, wiki links, backlinks and graph retrieval.
53
+ - Added semantic candidate bucket indexing to narrow vector candidates for larger vaults.
54
54
  - Optimized title/link resolution with precomputed agent-scoped title maps.
55
55
  - Added CLI, JSON output, HTTP API and graph UI.
56
56
  - Added vault diagnostics: stats, broken links, orphans, validation and doctor.
package/CONTRIBUTING.md CHANGED
@@ -22,7 +22,7 @@ npm run pack:smoke
22
22
  ## Design Rules
23
23
 
24
24
  - Markdown files are the source of truth.
25
- - SQLite is a derived index and must remain rebuildable.
25
+ - Local index artifacts are derived and must remain rebuildable.
26
26
  - Domain parsing, graph analysis and layout should stay pure and testable.
27
- - CLI, HTTP, filesystem and SQLite code are adapters around application use cases.
27
+ - CLI, HTTP, filesystem and index code are adapters around application use cases.
28
28
  - MCP integration should live outside this package by wrapping the CLI with `--json`.
package/README.md CHANGED
@@ -52,14 +52,14 @@ LLMs do not have infinite context. Brainlink gives agents an external memory lay
52
52
  1. Durable knowledge is written as Markdown.
53
53
  2. Notes are connected with `[[wiki links]]`.
54
54
  3. Concepts are classified with `#tags`.
55
- 4. Brainlink builds a local SQLite index with FTS records and local embeddings.
55
+ 4. Brainlink builds a local JSON index (`.brainlink/index.json`) and private encrypted search packs.
56
56
  5. Agents query the index before responding.
57
57
  6. Brainlink returns compact, source-backed context.
58
58
 
59
- Markdown is the source of truth. `.brainlink/brainlink.db` is only a rebuildable index.
60
- Brainlink now keeps an automatic rollback snapshot at `.brainlink/brainlink.db.backup` plus rotating snapshots in `.brainlink/brainlink.db.backup.snapshots/`. If the main SQLite file is corrupted, Brainlink automatically restores the newest valid snapshot (or recreates a clean index when no snapshot exists).
61
- After each index run, Brainlink also writes private encrypted search packs at `.brainlink/search-packs/*.blpk`. If SQLite is unavailable, search falls back to these packs automatically.
59
+ Markdown is the source of truth. `.brainlink/index.json` is a rebuildable index artifact.
60
+ After each index run, Brainlink also writes private encrypted search packs at `.brainlink/search-packs/*.blpk` to preserve fast retrieval and portable recovery.
62
61
  Pack decryption uses a Brainlink key from `$BRAINLINK_HOME/keys` or from `BRAINLINK_SEARCH_PACK_KEY` when explicitly configured.
62
+ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/context access.
63
63
 
64
64
  ## Features
65
65
 
@@ -68,7 +68,7 @@ Pack decryption uses a Brainlink key from `$BRAINLINK_HOME/keys` or from `BRAINL
68
68
  - Weighted graph edges so agents can rank relationship importance and priority.
69
69
  - Backlinks, broken-link reports, orphan detection and validation.
70
70
  - Full-text, semantic and hybrid retrieval modes.
71
- - SQLite-backed semantic candidate buckets for larger vaults.
71
+ - Full-text, semantic and hybrid retrieval on a local file index.
72
72
  - Agent namespaces under `agents/<agent-id>/`.
73
73
  - S3-compatible bucket vaults through `s3://bucket/prefix` URIs.
74
74
  - CLI with machine-readable `--json` output.
@@ -285,7 +285,7 @@ export BRAINLINK_S3_FORCE_PATH_STYLE=1
285
285
 
286
286
  Bucket vaults mirror Markdown into a local cache under
287
287
  `$BRAINLINK_HOME/bucket-cache`. The bucket remains canonical; the local
288
- `.brainlink/brainlink.db` stays a disposable index. Run `index` after remote
288
+ `.brainlink/index.json` stays a disposable index artifact. Run `index` after remote
289
289
  bucket changes before relying on `search`, `context`, graph or validation
290
290
  commands. Watch mode is only supported for local filesystem vaults.
291
291
 
@@ -301,7 +301,7 @@ vault/
301
301
  research-agent/
302
302
  source-review-policy.md
303
303
  .brainlink/
304
- brainlink.db
304
+ index.json
305
305
  ```
306
306
 
307
307
  Permanent data:
@@ -311,7 +311,7 @@ Permanent data:
311
311
 
312
312
  Rebuildable data:
313
313
 
314
- - `.brainlink/brainlink.db`
314
+ - `.brainlink/index.json`
315
315
  - full-text records
316
316
  - local embedding vectors
317
317
  - local embedding buckets
@@ -723,8 +723,8 @@ If `--mode` or `--limit` is omitted, Brainlink resolves values from the current
723
723
 
724
724
  Modes:
725
725
 
726
- - `hybrid`: default; combines SQLite FTS with local embedding similarity.
727
- - `fts`: exact lexical retrieval through SQLite FTS.
726
+ - `hybrid`: default; combines lexical matching with local embedding similarity.
727
+ - `fts`: exact lexical retrieval from the file index.
728
728
  - `semantic`: local deterministic embedding similarity only.
729
729
 
730
730
  Hybrid results are cached in-memory for a short TTL and invalidated automatically when the local index file changes.
@@ -976,7 +976,7 @@ src/
976
976
  application/ use cases
977
977
  cli/ command-line adapter
978
978
  domain/ pure knowledge rules
979
- infrastructure/ filesystem and SQLite adapters
979
+ infrastructure/ filesystem and index adapters
980
980
  ```
981
981
 
982
982
  Detailed notes:
@@ -988,7 +988,6 @@ Detailed notes:
988
988
  ## Current Limits
989
989
 
990
990
  - Semantic search uses deterministic local embeddings, not a remote model provider.
991
- - Semantic search uses SQLite embedding buckets to narrow candidates before cosine scoring.
992
991
  - `embeddingProvider` currently supports `local` and `none`.
993
992
  - Link resolution is title-based inside each agent namespace, with `shared` as fallback.
994
993
  - HTTP API is local and unauthenticated.
@@ -999,7 +998,7 @@ Detailed notes:
999
998
  The `0.1.0-beta` line is intended to stabilize the local-first memory loop:
1000
999
 
1001
1000
  - Markdown as durable memory.
1002
- - SQLite FTS plus local embeddings and semantic buckets as rebuildable retrieval index.
1001
+ - Rebuildable file index plus local embeddings and encrypted pack exports.
1003
1002
  - CLI as the primary agent interface.
1004
1003
  - HTTP graph API and frontend as inspection tools.
1005
1004
  - Agent namespaces to avoid context mixing.
@@ -1015,7 +1014,7 @@ Brainlink is local-first by default.
1015
1014
  - Brainlink HTTP is localhost-only and refuses non-loopback hosts.
1016
1015
  - Brainlink blocks common secret patterns by default when adding notes. Use `--allow-sensitive` only for intentional, protected vaults.
1017
1016
  - Do not store secrets, credentials, API keys or regulated personal data unless the vault is protected by your own storage controls.
1018
- - Treat `.brainlink/brainlink.db` as disposable derived data.
1017
+ - Treat `.brainlink/index.json` and `.brainlink/search-packs/` as disposable derived artifacts.
1019
1018
 
1020
1019
  See [SECURITY.md](SECURITY.md).
1021
1020
 
package/SECURITY.md CHANGED
@@ -7,7 +7,7 @@ Brainlink is local-first.
7
7
  - The HTTP server binds to `127.0.0.1` by default.
8
8
  - The HTTP server always refuses non-loopback hosts.
9
9
  - The HTTP server is read-only and does not expose note creation, indexing or update routes.
10
- - The SQLite database is a derived local index.
10
+ - Local index artifacts (`.brainlink/index.json` and `.brainlink/search-packs/`) are derived data.
11
11
  - Markdown files are user-owned source data.
12
12
  - Brainlink-created Markdown files use `0600` permissions.
13
13
  - Brainlink-created directories and `.brainlink` use `0700` permissions.
@@ -1,7 +1,5 @@
1
1
  import { stat } from 'node:fs/promises';
2
- import { existsSync, readdirSync } from 'node:fs';
3
2
  import { performance } from 'node:perf_hooks';
4
- import { join } from 'node:path';
5
3
  import { validateGraph, getBrokenLinks, getOrphanNodes, getVaultStats } from '../domain/graph-analysis.js';
6
4
  import { ensureVault, listVaultFiles, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
7
5
  import { resolveAgentRuntimeDefaults } from '../infrastructure/config.js';
@@ -96,23 +94,11 @@ export const doctorVault = async (vaultPath) => {
96
94
  const files = await readMarkdownFiles(absoluteVaultPath);
97
95
  const graph = await getGraphSummary(absoluteVaultPath);
98
96
  const validation = validateGraph(graph);
99
- const backupPath = join(absoluteVaultPath, '.brainlink', 'brainlink.db.backup');
100
- const snapshotDirectory = join(absoluteVaultPath, '.brainlink', 'brainlink.db.backup.snapshots');
101
- const hasBackup = existsSync(backupPath);
102
- const snapshotCount = existsSync(snapshotDirectory)
103
- ? readdirSync(snapshotDirectory).filter((name) => name.endsWith('.db')).length
104
- : 0;
105
- const backupReady = graph.nodes.length === 0 || hasBackup;
106
97
  const checks = [
107
98
  createCheck('vault', true, `Vault ready at ${absoluteVaultPath}`),
108
99
  createCheck('markdown-files', files.length > 0, `${files.length} markdown files found`),
109
100
  createCheck('index', graph.nodes.length > 0, `${graph.nodes.length} indexed documents found`),
110
- createCheck('broken-links', validation.brokenLinks.length === 0, `${validation.brokenLinks.length} broken links found`),
111
- createCheck('index-backup', backupReady, backupReady
112
- ? (hasBackup
113
- ? `SQLite recovery snapshot is available (${snapshotCount} rotating snapshots)`
114
- : 'No index yet. Snapshot will be created after first indexing run')
115
- : 'Recovery snapshot missing. Run blink index to create a rollback snapshot')
101
+ createCheck('broken-links', validation.brokenLinks.length === 0, `${validation.brokenLinks.length} broken links found`)
116
102
  ];
117
103
  const recommendations = files.length === 0 && graph.nodes.length === 0
118
104
  ? [
@@ -1,12 +1,12 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { stat } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
3
  import { createCauliflowerGraphLayout } from '../domain/graph-layout.js';
4
+ import { indexStoragePath } from '../infrastructure/file-index.js';
5
5
  import { getGraphSummary } from './get-graph-summary.js';
6
6
  const graphLayoutCache = new Map();
7
7
  const readDatabaseSignature = async (vaultPath) => {
8
8
  try {
9
- const info = await stat(join(vaultPath, '.brainlink', 'brainlink.db'));
9
+ const info = await stat(indexStoragePath(vaultPath));
10
10
  return `${Math.floor(info.mtimeMs)}:${info.size}`;
11
11
  }
12
12
  catch {
@@ -1,10 +1,10 @@
1
1
  import { ensureVault } from '../infrastructure/file-system-vault.js';
2
- import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
2
+ import { openFileIndex } from '../infrastructure/file-index.js';
3
3
  export const getGraphNode = async (vaultPath, id, agentId) => {
4
4
  const absoluteVaultPath = await ensureVault(vaultPath);
5
- const index = openSqliteIndex(absoluteVaultPath);
5
+ const index = openFileIndex(absoluteVaultPath);
6
6
  try {
7
- return index.getGraphNode(id, agentId);
7
+ return await index.getGraphNode(id, agentId);
8
8
  }
9
9
  finally {
10
10
  index.close();
@@ -1,10 +1,10 @@
1
1
  import { ensureVault } from '../infrastructure/file-system-vault.js';
2
- import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
2
+ import { openFileIndex } from '../infrastructure/file-index.js';
3
3
  export const getGraphSummary = async (vaultPath, agentId) => {
4
4
  const absoluteVaultPath = await ensureVault(vaultPath);
5
- const index = openSqliteIndex(absoluteVaultPath);
5
+ const index = openFileIndex(absoluteVaultPath);
6
6
  try {
7
- return index.getGraphSummary(agentId);
7
+ return await index.getGraphSummary(agentId);
8
8
  }
9
9
  finally {
10
10
  index.close();
@@ -1,10 +1,10 @@
1
1
  import { ensureVault } from '../infrastructure/file-system-vault.js';
2
- import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
2
+ import { openFileIndex } from '../infrastructure/file-index.js';
3
3
  export const getGraph = async (vaultPath, agentId) => {
4
4
  const absoluteVaultPath = await ensureVault(vaultPath);
5
- const index = openSqliteIndex(absoluteVaultPath);
5
+ const index = openFileIndex(absoluteVaultPath);
6
6
  try {
7
- return index.getGraph(agentId);
7
+ return await index.getGraph(agentId);
8
8
  }
9
9
  finally {
10
10
  index.close();
@@ -4,7 +4,7 @@ import { createEmbeddingProvider } from '../domain/embeddings.js';
4
4
  import { loadBrainlinkConfig } from '../infrastructure/config.js';
5
5
  import { ensureVault, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
6
6
  import { buildSearchPacks } from '../infrastructure/search-packs.js';
7
- import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
7
+ import { openFileIndex } from '../infrastructure/file-index.js';
8
8
  const toTitleKey = (title) => title.toLowerCase();
9
9
  const appendTitleEntry = (map, document) => {
10
10
  const key = toTitleKey(document.title);
@@ -60,15 +60,15 @@ export const indexVault = async (vaultPath) => {
60
60
  }));
61
61
  const titleMaps = createTitleMaps(documents);
62
62
  const indexedDocuments = await embedIndexedDocuments(documents.map((document) => createIndexedDocument(document, createScopedTitleResolver(document, titleMaps), config.chunkSize)), config.embeddingProvider);
63
- const index = openSqliteIndex(absoluteVaultPath);
63
+ const index = openFileIndex(absoluteVaultPath);
64
64
  try {
65
- index.reset();
66
- index.saveDocuments(indexedDocuments);
65
+ await index.reset();
66
+ await index.saveDocuments(indexedDocuments);
67
67
  try {
68
68
  await buildSearchPacks(absoluteVaultPath, indexedDocuments);
69
69
  }
70
70
  catch {
71
- // Pack generation is best-effort. SQLite index remains the primary path.
71
+ // Pack generation is best-effort. The JSON index remains the primary path.
72
72
  }
73
73
  return {
74
74
  documentCount: indexedDocuments.length,
@@ -1,10 +1,10 @@
1
1
  import { ensureVault } from '../infrastructure/file-system-vault.js';
2
- import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
2
+ import { openFileIndex } from '../infrastructure/file-index.js';
3
3
  export const listAgents = async (vaultPath) => {
4
4
  const absoluteVaultPath = await ensureVault(vaultPath);
5
- const index = openSqliteIndex(absoluteVaultPath);
5
+ const index = openFileIndex(absoluteVaultPath);
6
6
  try {
7
- return index.listAgents();
7
+ return await index.listAgents();
8
8
  }
9
9
  finally {
10
10
  index.close();
@@ -1,10 +1,10 @@
1
1
  import { ensureVault } from '../infrastructure/file-system-vault.js';
2
- import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
2
+ import { openFileIndex } from '../infrastructure/file-index.js';
3
3
  export const listLinks = async (vaultPath, agentId) => {
4
4
  const absoluteVaultPath = await ensureVault(vaultPath);
5
- const index = openSqliteIndex(absoluteVaultPath);
5
+ const index = openFileIndex(absoluteVaultPath);
6
6
  try {
7
- return index.listLinks(agentId);
7
+ return await index.listLinks(agentId);
8
8
  }
9
9
  finally {
10
10
  index.close();
@@ -12,9 +12,9 @@ export const listLinks = async (vaultPath, agentId) => {
12
12
  };
13
13
  export const listBacklinks = async (vaultPath, title, agentId) => {
14
14
  const absoluteVaultPath = await ensureVault(vaultPath);
15
- const index = openSqliteIndex(absoluteVaultPath);
15
+ const index = openFileIndex(absoluteVaultPath);
16
16
  try {
17
- return index.listBacklinks(title, agentId);
17
+ return await index.listBacklinks(title, agentId);
18
18
  }
19
19
  finally {
20
20
  index.close();
@@ -1,10 +1,10 @@
1
1
  import { ensureVault } from '../infrastructure/file-system-vault.js';
2
- import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
2
+ import { openFileIndex } from '../infrastructure/file-index.js';
3
3
  export const searchGraphNodeIds = async (vaultPath, query, limit, agentId) => {
4
4
  const absoluteVaultPath = await ensureVault(vaultPath);
5
- const index = openSqliteIndex(absoluteVaultPath);
5
+ const index = openFileIndex(absoluteVaultPath);
6
6
  try {
7
- return index.searchGraphNodeIds(query, limit, agentId);
7
+ return await index.searchGraphNodeIds(query, limit, agentId);
8
8
  }
9
9
  finally {
10
10
  index.close();
@@ -1,8 +1,7 @@
1
1
  import { stat } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
2
  import { ensureVault } from '../infrastructure/file-system-vault.js';
4
- import { searchInPacks } from '../infrastructure/search-packs.js';
5
- import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
3
+ import { ensurePrivatePacksFromLegacyIndex, searchInPacks } from '../infrastructure/search-packs.js';
4
+ import { indexStoragePath, openFileIndex } from '../infrastructure/file-index.js';
6
5
  import { createEmbeddingProvider } from '../domain/embeddings.js';
7
6
  import { loadBrainlinkConfig, sanitizeSearchMode } from '../infrastructure/config.js';
8
7
  const hybridCacheTtlMs = 30_000;
@@ -10,7 +9,7 @@ const hybridCacheMaxEntries = 200;
10
9
  const hybridSearchCache = new Map();
11
10
  const readIndexMtimeMs = async (vaultPath) => {
12
11
  try {
13
- return (await stat(join(vaultPath, '.brainlink', 'brainlink.db'))).mtimeMs;
12
+ return (await stat(indexStoragePath(vaultPath))).mtimeMs;
14
13
  }
15
14
  catch {
16
15
  return 0;
@@ -47,6 +46,7 @@ export const searchKnowledge = async (vaultPath, query, limit, agentId, mode) =>
47
46
  const absoluteVaultPath = await ensureVault(vaultPath);
48
47
  const config = await loadBrainlinkConfig();
49
48
  const searchMode = sanitizeSearchMode(mode, config.defaultSearchMode);
49
+ await ensurePrivatePacksFromLegacyIndex(absoluteVaultPath);
50
50
  const cacheKey = searchMode === 'hybrid' ? toCacheKey(absoluteVaultPath, query, limit, agentId) : undefined;
51
51
  const indexMtimeMs = cacheKey ? await readIndexMtimeMs(absoluteVaultPath) : 0;
52
52
  const cached = cacheKey ? cacheGet(cacheKey, indexMtimeMs) : undefined;
@@ -57,9 +57,9 @@ export const searchKnowledge = async (vaultPath, query, limit, agentId, mode) =>
57
57
  const shouldEmbedQuery = searchMode !== 'fts' && provider.name !== 'none';
58
58
  const queryEmbedding = shouldEmbedQuery ? (await provider.embed([query]))[0] ?? [] : [];
59
59
  try {
60
- const index = openSqliteIndex(absoluteVaultPath);
60
+ const index = openFileIndex(absoluteVaultPath);
61
61
  try {
62
- const results = index.search(query, limit, agentId, searchMode, queryEmbedding);
62
+ const results = await index.search(query, limit, agentId, searchMode, queryEmbedding);
63
63
  if (cacheKey) {
64
64
  cacheSet({
65
65
  key: cacheKey,
@@ -21,7 +21,7 @@ const readOptions = (args) => ({
21
21
  });
22
22
  const topics = [
23
23
  'authentication jwt token refresh policy',
24
- 'sqlite graph backlinks markdown vault indexing',
24
+ 'graph backlinks markdown vault indexing',
25
25
  'frontend canvas layout graph interaction',
26
26
  'agent memory context retrieval summarization',
27
27
  'security local server vault path allowlist',