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

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
@@ -389,13 +385,14 @@ Available tools:
389
385
  - `brainlink_context`: read indexed context for a task or question.
390
386
  - `brainlink_search`: search indexed notes.
391
387
  - `brainlink_add_note`: write durable Markdown memory and reindex.
388
+ - `brainlink_add_file`: ingest a local file as a note and reindex.
392
389
  - `brainlink_index`: rebuild the vault index.
393
390
  - `brainlink_validate`: validate broken links and orphan notes.
394
391
  - `brainlink_graph`: read indexed graph nodes and weighted links.
395
392
  - `brainlink_broken_links`: list unresolved wiki links.
396
393
  - `brainlink_orphans`: list disconnected notes.
397
394
 
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.
395
+ 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
396
 
400
397
  Agents can raise the importance of a relationship by putting priority markers on the same line as a wiki link:
401
398
 
@@ -478,8 +475,12 @@ Initializes vault metadata. Without an argument, Brainlink initializes the defau
478
475
  ```bash
479
476
  blink add "Note Title" --agent coding-agent --content "Markdown content"
480
477
  blink add "Note Title" --vault ./vault --agent coding-agent --content "Markdown content"
478
+ blink add "Note Title" --vault ./vault --content-file ./notes.md
479
+ blink add "Note Title" --vault ./vault --content-file ./notes.md --no-auto-index
481
480
  ```
482
481
 
482
+ `--content` and `--content-file` are mutually exclusive. Add `--no-auto-index` when you want to defer reindexing.
483
+
483
484
  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
485
 
485
486
  ### `index`
@@ -638,6 +639,7 @@ Brainlink reads `brainlink.config.json` or `.brainlink.json` from the current wo
638
639
  "port": 4321,
639
640
  "allowedVaults": [".brainlink-vault"],
640
641
  "defaultAgent": "shared",
642
+ "autoIndexOnWrite": true,
641
643
  "defaultSearchLimit": 10,
642
644
  "defaultContextTokens": 2000,
643
645
  "embeddingProvider": "local",
@@ -646,7 +648,9 @@ Brainlink reads `brainlink.config.json` or `.brainlink.json` from the current wo
646
648
  }
647
649
 
648
650
  `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
- ```
651
+
652
+ `autoIndexOnWrite` is optional and defaults to `true`. Set it to `false` to defer indexing after writes.
653
+ ```
650
654
 
651
655
  Use `"embeddingProvider": "none"` when you want FTS-only indexing.
652
656
 
@@ -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,6 +521,7 @@ Available MCP tools:
516
521
  - `brainlink_context`
517
522
  - `brainlink_search`
518
523
  - `brainlink_add_note`
524
+ - `brainlink_add_file`
519
525
  - `brainlink_index`
520
526
  - `brainlink_validate`
521
527
  - `brainlink_graph`
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.1",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",