@andespindola/brainlink 0.1.0-beta.0 → 0.1.0-beta.2

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/README.md CHANGED
@@ -191,7 +191,9 @@ blink add "Testing Policy" \
191
191
  --content "Run npm run check before final delivery. Related: [[Release Checklist]]. #testing #process"
192
192
  ```
193
193
 
194
- Brainlink does not infer durable graph relationships from generated context. A context result is only a read package for the model. To create a real link in the knowledge graph, the agent must write Markdown that contains an explicit `[[Note Title]]` wiki link and then rebuild the index.
194
+ Brainlink does not infer durable graph relationships from generated context. A context result is only a read package for the model. To create a real link in the knowledge graph, the agent must write Markdown that contains an explicit `[[Note Title]]` wiki link.
195
+
196
+ Writes with `blink add` reindex the vault automatically by default. This can be disabled with `--no-auto-index` and controlled globally with `autoIndexOnWrite` in `brainlink.config.json`.
195
197
 
196
198
  When adding memory, follow this contract:
197
199
 
@@ -200,11 +202,7 @@ When adding memory, follow this contract:
200
202
  - Add retrieval tags such as `#architecture`, `#decision`, `#runbook` or `#preference`.
201
203
  - Do not leave isolated notes unless they are intentionally root concepts.
202
204
 
203
- Rebuild the index:
204
-
205
- ```bash
206
- blink index
207
- ```
205
+ If you disable auto-index, run `blink index` after batched writes.
208
206
 
209
207
  ### 6. Validate Memory Health
210
208
 
@@ -223,7 +221,7 @@ Use this loop during real work:
223
221
  3. Use returned sources as project memory.
224
222
  4. Perform the task.
225
223
  5. Save only durable learnings with `blink add`, including `[[wiki links]]` to related notes.
226
- 6. Run `blink index`.
224
+ 6. Run `blink index` only when auto-index was disabled during a batch.
227
225
  7. Validate with `blink validate`, `blink broken-links` and `blink orphans` when graph links matter.
228
226
 
229
227
  Do not store secrets, credentials, private keys, access tokens or transient chat noise.
@@ -241,8 +239,6 @@ blink add "Auth Decision" \
241
239
  --vault ./vault \
242
240
  --content "We chose JWT for API clients. [[Architecture]] #auth #jwt"
243
241
 
244
- blink index --vault ./vault
245
-
246
242
  blink search "jwt auth" --vault ./vault
247
243
 
248
244
  blink context "how does auth work?" --vault ./vault
@@ -384,18 +380,114 @@ Example MCP client configuration:
384
380
  }
385
381
  ```
386
382
 
383
+ For a locked-down setup, allowlist the vaults that MCP clients may access:
384
+
385
+ ```json
386
+ {
387
+ "mcpServers": {
388
+ "brainlink": {
389
+ "command": "brainlink-mcp",
390
+ "env": {
391
+ "BRAINLINK_ALLOWED_VAULTS": "/absolute/path/to/project-vault,/absolute/path/to/team-vault"
392
+ }
393
+ }
394
+ }
395
+ }
396
+ ```
397
+
398
+ ### Install In MCP Client Stores
399
+
400
+ Brainlink can be exposed to MCP-compatible client stores in two ways:
401
+
402
+ 1. Register the stdio server directly when the client accepts `mcpServers` configuration.
403
+ 2. Register the local plugin from this repository when the client supports a plugin gallery or local marketplace.
404
+
405
+ Direct MCP server setup:
406
+
407
+ ```bash
408
+ npm install -g @andespindola/brainlink@latest
409
+ command -v brainlink-mcp
410
+ ```
411
+
412
+ Use this server configuration in any MCP-compatible client that reads a JSON MCP manifest:
413
+
414
+ ```json
415
+ {
416
+ "mcpServers": {
417
+ "brainlink": {
418
+ "command": "brainlink-mcp"
419
+ }
420
+ }
421
+ }
422
+ ```
423
+
424
+ Local plugin gallery setup:
425
+
426
+ ```bash
427
+ npm install -g @andespindola/brainlink@latest
428
+ git clone https://github.com/andersonflima/brainlink.git "$HOME/brainlink"
429
+ mkdir -p "$HOME/plugins"
430
+ ln -s "$HOME/brainlink/plugins/brainlink" "$HOME/plugins/brainlink"
431
+ ```
432
+
433
+ Then register the plugin in the local marketplace file used by compatible clients:
434
+
435
+ ```bash
436
+ node <<'NODE'
437
+ const fs = require('node:fs')
438
+ const os = require('node:os')
439
+ const path = require('node:path')
440
+
441
+ const marketplacePath = path.join(os.homedir(), '.agents', 'plugins', 'marketplace.json')
442
+ const pluginEntry = {
443
+ name: 'brainlink',
444
+ source: {
445
+ source: 'local',
446
+ path: './plugins/brainlink'
447
+ },
448
+ policy: {
449
+ installation: 'AVAILABLE',
450
+ authentication: 'ON_INSTALL'
451
+ },
452
+ category: 'Productivity'
453
+ }
454
+
455
+ fs.mkdirSync(path.dirname(marketplacePath), { recursive: true })
456
+
457
+ const marketplace = fs.existsSync(marketplacePath)
458
+ ? JSON.parse(fs.readFileSync(marketplacePath, 'utf8'))
459
+ : {
460
+ name: 'local',
461
+ interface: {
462
+ displayName: 'Local'
463
+ },
464
+ plugins: []
465
+ }
466
+
467
+ const plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : []
468
+ marketplace.plugins = [...plugins.filter((plugin) => plugin?.name !== 'brainlink'), pluginEntry]
469
+
470
+ fs.writeFileSync(marketplacePath, `${JSON.stringify(marketplace, null, 2)}\n`)
471
+ NODE
472
+ ```
473
+
474
+ Restart the client after changing marketplace or MCP configuration so it reloads the Brainlink entry. The plugin starts `brainlink-mcp` and exposes the same tool set listed below.
475
+
387
476
  Available tools:
388
477
 
389
478
  - `brainlink_context`: read indexed context for a task or question.
390
479
  - `brainlink_search`: search indexed notes.
391
480
  - `brainlink_add_note`: write durable Markdown memory and reindex.
481
+ - `brainlink_add_file`: ingest a local file as a note and reindex.
392
482
  - `brainlink_index`: rebuild the vault index.
483
+ - `brainlink_stats`: read indexed vault statistics.
393
484
  - `brainlink_validate`: validate broken links and orphan notes.
485
+ - `brainlink_sync`: run index, stats, validation, broken-link and orphan checks in one call.
394
486
  - `brainlink_graph`: read indexed graph nodes and weighted links.
395
487
  - `brainlink_broken_links`: list unresolved wiki links.
396
488
  - `brainlink_orphans`: list disconnected notes.
397
489
 
398
- The same linking rule applies through MCP: `brainlink_context` is read-only, and real graph links require Markdown notes with explicit `[[wiki links]]` followed by indexing.
490
+ 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 the index result when enabled.
399
491
 
400
492
  Agents can raise the importance of a relationship by putting priority markers on the same line as a wiki link:
401
493
 
@@ -478,8 +570,12 @@ Initializes vault metadata. Without an argument, Brainlink initializes the defau
478
570
  ```bash
479
571
  blink add "Note Title" --agent coding-agent --content "Markdown content"
480
572
  blink add "Note Title" --vault ./vault --agent coding-agent --content "Markdown content"
573
+ blink add "Note Title" --vault ./vault --content-file ./notes.md
574
+ blink add "Note Title" --vault ./vault --content-file ./notes.md --no-auto-index
481
575
  ```
482
576
 
577
+ `--content` and `--content-file` are mutually exclusive. Add `--no-auto-index` when you want to defer reindexing.
578
+
483
579
  Creates a Markdown note under `agents/<agent-id>/`. Common secret patterns are blocked by default; use `--allow-sensitive` only for an intentionally protected vault.
484
580
 
485
581
  ### `index`
@@ -638,15 +734,18 @@ Brainlink reads `brainlink.config.json` or `.brainlink.json` from the current wo
638
734
  "port": 4321,
639
735
  "allowedVaults": [".brainlink-vault"],
640
736
  "defaultAgent": "shared",
737
+ "autoIndexOnWrite": true,
641
738
  "defaultSearchLimit": 10,
642
739
  "defaultContextTokens": 2000,
643
740
  "embeddingProvider": "local",
644
741
  "defaultSearchMode": "hybrid",
645
742
  "chunkSize": 1200
646
743
  }
744
+ ```
647
745
 
648
746
  `defaultAgent` is optional. When set, CLI and MCP calls that omit `--agent`/`agent` use this value automatically. If not set, behavior remains as before.
649
- ```
747
+
748
+ `autoIndexOnWrite` is optional and defaults to `true`. Set it to `false` to defer indexing after writes.
650
749
 
651
750
  Use `"embeddingProvider": "none"` when you want FTS-only indexing.
652
751
 
@@ -759,17 +858,18 @@ Detailed notes:
759
858
  - HTTP API is local and unauthenticated.
760
859
  - Watch mode depends on the platform filesystem watcher.
761
860
 
762
- ## Alpha Scope
861
+ ## Beta Scope
763
862
 
764
- `0.1.0-alpha.0` is intended to prove the local-first memory loop:
863
+ `0.1.0-beta.0` is intended to stabilize the local-first memory loop:
765
864
 
766
865
  - Markdown as durable memory.
767
866
  - SQLite FTS plus local embeddings and semantic buckets as rebuildable retrieval index.
768
867
  - CLI as the primary agent interface.
769
868
  - HTTP graph API and frontend as inspection tools.
770
869
  - Agent namespaces to avoid context mixing.
870
+ - MCP tools for context retrieval, durable memory writes and graph maintenance.
771
871
 
772
- The alpha includes local semantic retrieval. Remote embedding providers, remote auth, advanced deduplication and graph editing are future milestones.
872
+ The beta includes local semantic retrieval. Remote embedding providers, remote auth, advanced deduplication and graph editing are future milestones.
773
873
 
774
874
  ## Security
775
875
 
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from 'node:fs';
1
2
  import { addNote } from '../../application/add-note.js';
2
3
  import { indexVault } from '../../application/index-vault.js';
3
4
  import { startServer } from '../../application/start-server.js';
@@ -6,6 +7,15 @@ import { doctorVault } from '../../application/analyze-vault.js';
6
7
  import { loadBrainlinkConfig } from '../../infrastructure/config.js';
7
8
  import { assertVaultAllowed, ensureVault } from '../../infrastructure/file-system-vault.js';
8
9
  import { parsePositiveInteger, print, resolveOptions } from '../runtime.js';
10
+ const resolveAddContent = (options) => {
11
+ if (options.content != null && options.content.trim().length > 0) {
12
+ return options.content;
13
+ }
14
+ if (options.contentFile == null || options.contentFile.trim().length === 0) {
15
+ throw new Error('Use --content or --content-file to provide note content.');
16
+ }
17
+ return readFileSync(options.contentFile, 'utf8');
18
+ };
9
19
  export const registerWriteCommands = (program) => {
10
20
  program
11
21
  .command('init')
@@ -20,18 +30,23 @@ export const registerWriteCommands = (program) => {
20
30
  program
21
31
  .command('add')
22
32
  .argument('<title>', 'note title')
23
- .requiredOption('-c, --content <content>', 'markdown content')
33
+ .option('-c, --content <content>', 'markdown content')
34
+ .option('-f, --content-file <contentFile>', 'read markdown content from a file')
24
35
  .option('-v, --vault <vault>', 'vault directory')
25
36
  .option('-a, --agent <agent>', 'agent memory namespace')
26
37
  .option('--allow-sensitive', 'allow writing content that looks like a secret')
38
+ .option('--no-auto-index', 'skip reindexing after add')
27
39
  .option('--json', 'print machine-readable JSON')
28
40
  .description('add a markdown note to the vault')
29
41
  .action(async (title, options) => {
30
42
  const resolved = await resolveOptions(options);
31
- const path = await addNote(resolved.vault, title, options.content, resolved.agent, {
43
+ const content = resolveAddContent(options);
44
+ const notePath = await addNote(resolved.vault, title, content, resolved.agent, {
32
45
  allowSensitive: Boolean(options.allowSensitive)
33
46
  });
34
- print(options.json, { title, agent: resolved.agent ?? 'shared', path }, () => `Created note at ${path}`);
47
+ const shouldAutoIndex = options.autoIndex !== false && resolved.config.autoIndexOnWrite;
48
+ const index = shouldAutoIndex ? await indexVault(resolved.vault) : undefined;
49
+ print(options.json, { title, agent: resolved.agent ?? 'shared', path: notePath, ...(index ? { index } : {}) }, () => `Created note at ${notePath}`);
35
50
  });
36
51
  program
37
52
  .command('index')
@@ -8,6 +8,7 @@ export const defaultBrainlinkConfig = {
8
8
  port: 4321,
9
9
  allowedVaults: [],
10
10
  defaultAgent: undefined,
11
+ autoIndexOnWrite: true,
11
12
  defaultSearchLimit: 10,
12
13
  defaultContextTokens: 2000,
13
14
  embeddingProvider: 'local',
@@ -45,6 +46,7 @@ const sanitizeConfig = (value) => ({
45
46
  defaultAgent: typeof value.defaultAgent === 'string' && value.defaultAgent.trim().length > 0
46
47
  ? sanitizeAgentId(value.defaultAgent)
47
48
  : defaultBrainlinkConfig.defaultAgent,
49
+ autoIndexOnWrite: typeof value.autoIndexOnWrite === 'boolean' ? value.autoIndexOnWrite : defaultBrainlinkConfig.autoIndexOnWrite,
48
50
  defaultSearchLimit: typeof value.defaultSearchLimit === 'number' && value.defaultSearchLimit > 0
49
51
  ? value.defaultSearchLimit
50
52
  : defaultBrainlinkConfig.defaultSearchLimit,
@@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { addNoteInputSchema, addNoteTool, brokenLinksInputSchema, brokenLinksTool, contextInputSchema, contextTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool } from './tools.js';
5
+ import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, brokenLinksInputSchema, brokenLinksTool, contextInputSchema, contextTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool } from './tools.js';
6
6
  const readPackageVersion = () => {
7
7
  const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
8
8
  const metadata = JSON.parse(readFileSync(packagePath, 'utf8'));
@@ -30,6 +30,11 @@ export const createBrainlinkMcpServer = () => {
30
30
  description: 'Write durable Markdown memory, then reindex the vault. Include explicit [[wiki links]] for connected graph memory. Add priority markers near links, such as priority: high, #important or #critical, when a relationship should be weighted higher.',
31
31
  inputSchema: addNoteInputSchema
32
32
  }, addNoteTool);
33
+ server.registerTool('brainlink_add_file', {
34
+ title: 'Ingest Markdown File',
35
+ description: 'Read a local markdown/text file and ingest it as a Brainlink note. Reindex defaults to true.',
36
+ inputSchema: addFileInputSchema
37
+ }, addFileTool);
33
38
  server.registerTool('brainlink_index', {
34
39
  title: 'Index Brainlink Vault',
35
40
  description: 'Rebuild the local Brainlink index from Markdown notes.',
package/dist/mcp/tools.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename, extname } from 'node:path';
1
3
  import { z } from 'zod';
2
4
  import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../application/analyze-vault.js';
3
5
  import { addNote } from '../application/add-note.js';
@@ -32,11 +34,21 @@ const resolveExecutionContext = async (input) => {
32
34
  const vault = await assertVaultAllowed(input.vault ?? config.vault, config.allowedVaults);
33
35
  const agent = input.agent ?? config.defaultAgent;
34
36
  return {
35
- vault,
36
37
  config,
38
+ vault,
37
39
  agent
38
40
  };
39
41
  };
42
+ const inferTitleFromPath = (filePath) => {
43
+ const extension = extname(filePath);
44
+ const fromFileName = basename(filePath, extension);
45
+ return fromFileName
46
+ .trim()
47
+ .replace(/[-_]+/g, ' ')
48
+ .replace(/\s+/g, ' ')
49
+ .trim();
50
+ };
51
+ const isTruthy = (value) => value !== false;
40
52
  const jsonResult = (value) => ({
41
53
  content: [
42
54
  {
@@ -68,7 +80,16 @@ export const addNoteInputSchema = {
68
80
  .string()
69
81
  .min(1)
70
82
  .describe('Durable Markdown memory. Include explicit [[wiki links]] and #tags when the memory should be connected. Put priority markers near important links, for example priority: high, #important or #critical.'),
71
- agent: z.string().min(1).optional().describe('Agent memory namespace.'),
83
+ ...agentInput,
84
+ allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.'),
85
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.')
86
+ };
87
+ export const addFileInputSchema = {
88
+ ...vaultInput,
89
+ ...agentInput,
90
+ title: z.string().min(1).optional().describe('Optional note title override. If omitted, uses file name.'),
91
+ filePath: z.string().min(1).describe('Filesystem path to markdown or text file to ingest.'),
92
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after ingesting file.'),
72
93
  allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.')
73
94
  };
74
95
  export const indexInputSchema = {
@@ -128,16 +149,39 @@ export const searchTool = async (input) => {
128
149
  };
129
150
  export const addNoteTool = async (input) => {
130
151
  const context = await resolveExecutionContext(input);
152
+ const shouldIndex = isTruthy(input.autoIndex);
131
153
  const path = await addNote(context.vault, input.title, input.content, context.agent, {
132
154
  allowSensitive: input.allowSensitive
133
155
  });
134
- const index = await indexVault(context.vault);
156
+ const index = shouldIndex ? await indexVault(context.vault) : undefined;
135
157
  return jsonResult({
136
158
  vault: context.vault,
137
159
  title: input.title,
138
160
  agent: context.agent,
139
161
  path,
140
- index
162
+ ...(index ? { index } : {})
163
+ });
164
+ };
165
+ export const addFileTool = async (input) => {
166
+ const context = await resolveExecutionContext(input);
167
+ const content = await readFile(input.filePath, 'utf8');
168
+ const inferredTitle = inferTitleFromPath(input.filePath);
169
+ const title = input.title ?? inferredTitle;
170
+ if (title == null || title.length === 0) {
171
+ throw new Error('Cannot infer note title from file path. Provide a title explicitly.');
172
+ }
173
+ const shouldIndex = isTruthy(input.autoIndex);
174
+ const path = await addNote(context.vault, title, content, context.agent, {
175
+ allowSensitive: input.allowSensitive
176
+ });
177
+ const index = shouldIndex ? await indexVault(context.vault) : undefined;
178
+ return jsonResult({
179
+ vault: context.vault,
180
+ title,
181
+ agent: context.agent,
182
+ filePath: input.filePath,
183
+ path,
184
+ ...(index ? { index } : {})
141
185
  });
142
186
  };
143
187
  export const indexTool = async (input) => {
@@ -43,6 +43,8 @@ Use `--vault <path>` for a one-off custom vault, or set `vault` in `brainlink.co
43
43
 
44
44
  You can also set `defaultAgent` in `brainlink.config.json` / `.brainlink.json` (for example `"defaultAgent": "coding-agent"`). When set, CLI commands and MCP calls reuse it when `--agent`/`agent` is not passed.
45
45
 
46
+ `autoIndexOnWrite` (default: `true`) controls whether `add` and MCP write tools index right after writing.
47
+
46
48
  ## Agent Namespaces
47
49
 
48
50
  Each agent writes into a dedicated namespace under `agents/<agent-id>/`:
@@ -162,7 +164,7 @@ Required write behavior:
162
164
  2. Look for an existing related concept with `search`, `links` or `backlinks`.
163
165
  3. Add at least one `[[Existing Note Title]]` link unless the note is intentionally a root concept.
164
166
  4. Add useful `#tags` for retrieval.
165
- 5. Run `index` after the write.
167
+ 5. `add` writes are indexed by default. Only batch with explicit `--no-auto-index`, then run `index` once.
166
168
  6. Run `validate`, `broken-links` or `orphans` when the graph should be connected.
167
169
 
168
170
  Good linked note:
@@ -171,7 +173,6 @@ Good linked note:
171
173
  blink add "SQLite Index Rebuild" \
172
174
  --agent coding-agent \
173
175
  --content "Legacy derived indexes without agent columns are rebuilt because SQLite is disposable. Related: [[Architecture]], [[Agent Namespaces]]. #sqlite #architecture #decision"
174
- blink index
175
176
  blink validate --agent coding-agent
176
177
  ```
177
178
 
@@ -247,7 +248,7 @@ When using MCP, use this compact sequence for the same memory discipline:
247
248
  1. Bootstrap context:
248
249
  - `brainlink_context` with `agent`, `query`, `mode: hybrid`, `limit`.
249
250
  2. Capture durable decisions:
250
- - `brainlink_add_note` with explicit `[[wiki links]]` and `#tags`.
251
+ - `brainlink_add_note` or `brainlink_add_file` with explicit `[[wiki links]]` and `#tags`.
251
252
  3. Run maintenance before handoff or before the next step:
252
253
  - `brainlink_sync` with `agent`, `contextQuery`, `mode: hybrid`.
253
254
  4. Diagnose graph issues only when needed:
@@ -346,8 +347,12 @@ $HOME/.brainlink/vault/
346
347
 
347
348
  ```bash
348
349
  blink add "Note Title" --vault ./vault --content "Markdown content"
350
+ blink add "Note Title" --vault ./vault --content-file ./notes.md
351
+ blink add "Note Title" --vault ./vault --content-file ./notes.md --no-auto-index
349
352
  ```
350
353
 
354
+ `--content` and `--content-file` are mutually exclusive. Use `--no-auto-index` if you want to defer indexing in batch operations.
355
+
351
356
  This creates a slugged Markdown file with frontmatter and a heading.
352
357
 
353
358
  The CLI blocks common secret patterns by default. Do not use `--allow-sensitive` unless the vault is intentionally protected.
@@ -516,8 +521,11 @@ Available MCP tools:
516
521
  - `brainlink_context`
517
522
  - `brainlink_search`
518
523
  - `brainlink_add_note`
524
+ - `brainlink_add_file`
519
525
  - `brainlink_index`
526
+ - `brainlink_stats`
520
527
  - `brainlink_validate`
528
+ - `brainlink_sync`
521
529
  - `brainlink_graph`
522
530
  - `brainlink_broken_links`
523
531
  - `brainlink_orphans`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",