@andespindola/brainlink 1.0.2 → 1.0.4

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/dist/mcp/tools.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { basename, extname } from 'node:path';
2
+ import { basename, extname, resolve } from 'node:path';
3
3
  import { z } from 'zod';
4
4
  import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../application/analyze-vault.js';
5
5
  import { addNoteWithMetadata } from '../application/add-note.js';
@@ -9,7 +9,11 @@ import { deleteNote } from '../application/delete-note.js';
9
9
  import { resolveDuplicateNotes, scanDuplicateNotes } from '../application/dedupe-notes.js';
10
10
  import { getGraph } from '../application/get-graph.js';
11
11
  import { getGraphContexts } from '../application/get-graph-contexts.js';
12
+ import { addInboxItem, listInboxItems, processInboxItems } from '../application/inbox.js';
12
13
  import { indexVault } from '../application/index-vault.js';
14
+ import { buildRememberSuggestion, explainSearchResults, suggestBrokenLinkFixes, suggestContextLinks } from '../application/memory-suggestions.js';
15
+ import { buildActionableDoctor, closeSession, initializeProjectMemory } from '../application/operational-workflows.js';
16
+ import { repairBrokenLinks } from '../application/repair-broken-links.js';
13
17
  import { searchKnowledge } from '../application/search-knowledge.js';
14
18
  import { resolveAgentRuntimeDefaults, sanitizeContextStrategy, sanitizeSearchMode } from '../infrastructure/config.js';
15
19
  import { clearContextPacks, listContextPacks } from '../infrastructure/context-packs.js';
@@ -249,6 +253,13 @@ export const searchInputSchema = {
249
253
  query: z.string().min(1).describe('Search query.'),
250
254
  limit: optionalPositiveInteger().describe('Maximum result count.')
251
255
  };
256
+ export const explainInputSchema = {
257
+ ...vaultInput,
258
+ ...agentInput,
259
+ ...searchModeInput,
260
+ query: z.string().min(1).describe('Search query to explain.'),
261
+ limit: optionalPositiveInteger().describe('Maximum result count.')
262
+ };
252
263
  export const addNoteInputSchema = {
253
264
  ...vaultInput,
254
265
  title: z.string().min(1).describe('Markdown note title.'),
@@ -264,6 +275,34 @@ export const addNoteInputSchema = {
264
275
  .optional()
265
276
  .describe('Automatically add canonical Context Links to the inferred visual context hub. Defaults to Brainlink config.')
266
277
  };
278
+ export const rememberInputSchema = {
279
+ ...vaultInput,
280
+ ...agentInput,
281
+ title: z.string().min(1).optional().describe('Optional note title. When omitted, Brainlink infers it from content.'),
282
+ content: z.string().min(1).describe('Memory content to capture as a durable Markdown note.'),
283
+ tags: z.array(z.string()).optional().default([]).describe('Extra tags to include.'),
284
+ links: z.array(z.string()).optional().default([]).describe('Explicit Context Links to include.'),
285
+ linkLimit: positiveInteger(5).describe('Maximum suggested Context Links to include.'),
286
+ dryRun: z.boolean().optional().default(false).describe('Preview inferred note without writing it.'),
287
+ allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.'),
288
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.')
289
+ };
290
+ export const inboxAddInputSchema = {
291
+ ...vaultInput,
292
+ ...agentInput,
293
+ content: z.string().min(1).describe('Quick memory content to store as an untriaged inbox note.'),
294
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing inbox item.')
295
+ };
296
+ export const inboxListInputSchema = {
297
+ ...vaultInput,
298
+ ...agentInput,
299
+ limit: positiveInteger(20).describe('Maximum inbox items to return.')
300
+ };
301
+ export const inboxProcessInputSchema = {
302
+ ...vaultInput,
303
+ ...agentInput,
304
+ limit: positiveInteger(10).describe('Maximum inbox items to inspect.')
305
+ };
267
306
  export const deleteNoteInputSchema = {
268
307
  ...vaultInput,
269
308
  ...agentInput,
@@ -313,6 +352,10 @@ export const validateInputSchema = {
313
352
  ...vaultInput,
314
353
  ...agentInput
315
354
  };
355
+ export const doctorActionsInputSchema = {
356
+ ...vaultInput,
357
+ ...agentInput
358
+ };
316
359
  export const graphInputSchema = {
317
360
  ...vaultInput,
318
361
  ...agentInput
@@ -325,6 +368,22 @@ export const brokenLinksInputSchema = {
325
368
  ...vaultInput,
326
369
  ...agentInput
327
370
  };
371
+ export const suggestLinksInputSchema = {
372
+ ...vaultInput,
373
+ ...agentInput,
374
+ content: z.string().min(1).optional().describe('Content to inspect for Context Link suggestions. Required unless broken=true.'),
375
+ broken: z.boolean().optional().default(false).describe('Suggest fixes for unresolved wiki links instead of content links.'),
376
+ limit: positiveInteger(5).describe('Maximum suggestions to return.')
377
+ };
378
+ export const repairLinksInputSchema = {
379
+ ...vaultInput,
380
+ ...agentInput,
381
+ dryRun: z.boolean().optional().default(false).describe('Preview repairs without writing files.'),
382
+ createMissing: z.boolean().optional().default(true).describe('Create placeholder notes for unresolved targets without a safe existing match.'),
383
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after repairs.'),
384
+ minScore: z.number().min(0).max(1).optional().default(0.88).describe('Minimum similarity score for automatic retargeting.'),
385
+ margin: z.number().min(0).max(1).optional().default(0.12).describe('Minimum score gap between first and second candidate.')
386
+ };
328
387
  export const orphansInputSchema = {
329
388
  ...vaultInput,
330
389
  ...agentInput
@@ -375,6 +434,19 @@ export const versionInputSchema = {
375
434
  ...vaultInput,
376
435
  ...agentInput
377
436
  };
437
+ export const sessionCloseInputSchema = {
438
+ ...vaultInput,
439
+ ...agentInput,
440
+ content: z.string().min(1).optional().describe('Optional extra session notes to include in the handoff.'),
441
+ cwd: z.string().min(1).optional().describe('Workspace path used for git status. Defaults to current process working directory.'),
442
+ dryRun: z.boolean().optional().default(false).describe('Preview handoff note without writing it.'),
443
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing handoff.')
444
+ };
445
+ export const projectInitInputSchema = {
446
+ ...vaultInput,
447
+ ...agentInput,
448
+ projectPath: z.string().min(1).optional().describe('Project path to inspect. Defaults to current process working directory.')
449
+ };
378
450
  export const recommendationsInputSchema = {
379
451
  ...vaultInput,
380
452
  ...agentInput,
@@ -472,6 +544,30 @@ export const searchTool = async (input) => {
472
544
  results
473
545
  });
474
546
  };
547
+ export const explainTool = async (input) => {
548
+ const context = await resolveExecutionContext(input);
549
+ const readiness = await ensureBootstrapReady(context, input, 'brainlink_explain');
550
+ if (readiness.preflight) {
551
+ return readiness.preflight;
552
+ }
553
+ const contextReadiness = await ensureContextReady(context, input, 'brainlink_explain');
554
+ if (contextReadiness.preflight) {
555
+ return contextReadiness.preflight;
556
+ }
557
+ const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
558
+ const limit = input.limit ?? context.defaults.defaultSearchLimit;
559
+ const results = await explainSearchResults(context.vault, input.query, limit, context.agent, mode);
560
+ return jsonResult({
561
+ vault: context.vault,
562
+ agent: context.agent,
563
+ query: input.query,
564
+ limit,
565
+ mode,
566
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
567
+ ...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
568
+ results
569
+ });
570
+ };
475
571
  export const addNoteTool = async (input) => {
476
572
  const context = await resolveExecutionContext(input);
477
573
  const shouldIndex = isTruthy(input.autoIndex);
@@ -504,6 +600,75 @@ export const addNoteTool = async (input) => {
504
600
  ...(index ? { index } : {})
505
601
  });
506
602
  };
603
+ export const rememberTool = async (input) => {
604
+ const context = await resolveExecutionContext(input);
605
+ const suggestion = await buildRememberSuggestion({
606
+ vaultPath: context.vault,
607
+ content: input.content,
608
+ agentId: context.agent,
609
+ title: input.title,
610
+ tags: input.tags,
611
+ links: input.links,
612
+ linkLimit: input.linkLimit
613
+ });
614
+ if (input.dryRun) {
615
+ return jsonResult({
616
+ dryRun: true,
617
+ vault: context.vault,
618
+ agent: context.agent,
619
+ suggestion
620
+ });
621
+ }
622
+ const added = await addNoteWithMetadata(context.vault, suggestion.title, suggestion.content, context.agent, {
623
+ allowSensitive: input.allowSensitive,
624
+ autoContextLinks: false
625
+ });
626
+ const index = input.autoIndex ? await indexVault(context.vault) : undefined;
627
+ return jsonResult({
628
+ dryRun: false,
629
+ vault: context.vault,
630
+ agent: context.agent,
631
+ suggestion,
632
+ path: added.path,
633
+ ...(index ? { index } : {})
634
+ });
635
+ };
636
+ export const inboxAddTool = async (input) => {
637
+ const context = await resolveExecutionContext(input);
638
+ const result = await addInboxItem({
639
+ vaultPath: context.vault,
640
+ content: input.content,
641
+ agentId: context.agent,
642
+ autoIndex: input.autoIndex
643
+ });
644
+ return jsonResult({
645
+ vault: context.vault,
646
+ agent: context.agent,
647
+ ...result
648
+ });
649
+ };
650
+ export const inboxListTool = async (input) => {
651
+ const context = await resolveExecutionContext(input);
652
+ const items = await listInboxItems(context.vault, input.limit);
653
+ return jsonResult({
654
+ vault: context.vault,
655
+ agent: context.agent,
656
+ items
657
+ });
658
+ };
659
+ export const inboxProcessTool = async (input) => {
660
+ const context = await resolveExecutionContext(input);
661
+ const items = await processInboxItems({
662
+ vaultPath: context.vault,
663
+ agentId: context.agent,
664
+ limit: input.limit
665
+ });
666
+ return jsonResult({
667
+ vault: context.vault,
668
+ agent: context.agent,
669
+ items
670
+ });
671
+ };
507
672
  export const deleteNoteTool = async (input) => {
508
673
  const context = await resolveExecutionContext(input);
509
674
  const result = await deleteNote(context.vault, {
@@ -613,6 +778,25 @@ export const validateTool = async (input) => {
613
778
  ...validation
614
779
  });
615
780
  };
781
+ export const doctorActionsTool = async (input) => {
782
+ const context = await resolveExecutionContext(input);
783
+ const readiness = await ensureBootstrapReady(context, input, 'brainlink_doctor_actions');
784
+ if (readiness.preflight) {
785
+ return readiness.preflight;
786
+ }
787
+ const contextReadiness = await ensureContextReady(context, input, 'brainlink_doctor_actions');
788
+ if (contextReadiness.preflight) {
789
+ return contextReadiness.preflight;
790
+ }
791
+ const report = await buildActionableDoctor(context.vault);
792
+ return jsonResult({
793
+ vault: context.vault,
794
+ agent: context.agent,
795
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
796
+ ...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
797
+ ...report
798
+ });
799
+ };
616
800
  export const graphTool = async (input) => {
617
801
  const context = await resolveExecutionContext(input);
618
802
  const readiness = await ensureBootstrapReady(context, input, 'brainlink_graph');
@@ -670,6 +854,56 @@ export const brokenLinksTool = async (input) => {
670
854
  brokenLinks
671
855
  });
672
856
  };
857
+ export const suggestLinksTool = async (input) => {
858
+ const context = await resolveExecutionContext(input);
859
+ const readiness = await ensureBootstrapReady(context, input, 'brainlink_suggest_links');
860
+ if (readiness.preflight) {
861
+ return readiness.preflight;
862
+ }
863
+ const contextReadiness = await ensureContextReady(context, input, 'brainlink_suggest_links');
864
+ if (contextReadiness.preflight) {
865
+ return contextReadiness.preflight;
866
+ }
867
+ if (input.broken) {
868
+ const suggestions = await suggestBrokenLinkFixes(context.vault, context.agent, input.limit);
869
+ return jsonResult({
870
+ vault: context.vault,
871
+ agent: context.agent,
872
+ mode: 'broken-links',
873
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
874
+ ...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
875
+ suggestions
876
+ });
877
+ }
878
+ if (!input.content || input.content.trim().length === 0) {
879
+ throw new Error('content is required when broken=false.');
880
+ }
881
+ const suggestions = await suggestContextLinks(context.vault, input.content, context.agent, input.limit);
882
+ return jsonResult({
883
+ vault: context.vault,
884
+ agent: context.agent,
885
+ mode: 'content',
886
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
887
+ ...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
888
+ suggestions
889
+ });
890
+ };
891
+ export const repairLinksTool = async (input) => {
892
+ const context = await resolveExecutionContext(input);
893
+ const result = await repairBrokenLinks(context.vault, {
894
+ agentId: context.agent,
895
+ dryRun: input.dryRun,
896
+ createMissing: input.createMissing,
897
+ autoIndex: input.autoIndex,
898
+ minScore: input.minScore,
899
+ margin: input.margin
900
+ });
901
+ return jsonResult({
902
+ vault: context.vault,
903
+ agent: context.agent,
904
+ ...result
905
+ });
906
+ };
673
907
  export const orphansTool = async (input) => {
674
908
  const context = await resolveExecutionContext(input);
675
909
  const readiness = await ensureBootstrapReady(context, input, 'brainlink_orphans');
@@ -924,6 +1158,35 @@ export const versionTool = async (input) => {
924
1158
  runtime: getRuntimeMetadata()
925
1159
  });
926
1160
  };
1161
+ export const sessionCloseTool = async (input) => {
1162
+ const context = await resolveExecutionContext(input);
1163
+ const result = await closeSession({
1164
+ vaultPath: context.vault,
1165
+ agentId: context.agent,
1166
+ cwd: resolve(input.cwd ?? process.cwd()),
1167
+ content: input.content,
1168
+ write: input.dryRun !== true,
1169
+ autoIndex: input.autoIndex
1170
+ });
1171
+ return jsonResult({
1172
+ vault: context.vault,
1173
+ agent: context.agent,
1174
+ dryRun: input.dryRun === true,
1175
+ ...result
1176
+ });
1177
+ };
1178
+ export const projectInitTool = async (input) => {
1179
+ const context = await resolveExecutionContext(input);
1180
+ const result = await initializeProjectMemory({
1181
+ vaultPath: context.vault,
1182
+ projectPath: resolve(input.projectPath ?? process.cwd()),
1183
+ agentId: context.agent
1184
+ });
1185
+ return jsonResult({
1186
+ agent: context.agent,
1187
+ ...result
1188
+ });
1189
+ };
927
1190
  export const recommendationsTool = async (input) => {
928
1191
  const context = await resolveExecutionContext(input);
929
1192
  const policy = await getBootstrapPolicy();
@@ -430,6 +430,17 @@ blink db-import --vault ./team-vault --db ./legacy/brainlink.db --table legacy_n
430
430
  Without `--db`, Brainlink auto-detects common legacy database paths.
431
431
  Use `--agent` to force namespace, `--limit` for staged migration, `--dry-run` to preview writes, and `--no-index` to postpone indexing.
432
432
 
433
+ ### Import A Document File
434
+
435
+ ```bash
436
+ blink import-file ./report.pdf --vault ./team-vault
437
+ blink import-file ./report.pdf --vault ./team-vault --agent coding-agent
438
+ blink import-file ./report.pdf --vault ./team-vault --title "Quarterly Report"
439
+ ```
440
+
441
+ `import-file` converts a source document with Docling, writes the converted Markdown as a durable note, and indexes the result by default.
442
+ The `docling` executable must be installed and available in `PATH`. The graph UI exposes the same import path through its upload modal.
443
+
433
444
  ### Install Agent Integration
434
445
 
435
446
  ```bash
@@ -636,7 +647,7 @@ blink server --vault ./vault --host 127.0.0.1 --port 4321
636
647
  blink server --vault ./vault --host 127.0.0.1 --port 4321 --no-open
637
648
  ```
638
649
 
639
- This starts a local frontend for inspecting the knowledge graph.
650
+ This starts a local frontend for inspecting the knowledge graph and importing document files into the selected vault.
640
651
  By default it tries to open the graph in a native desktop GUI window:
641
652
  - macOS: Swift + WebKit
642
653
  - Windows: PowerShell WinForms WebBrowser
@@ -713,9 +724,14 @@ Available MCP tools:
713
724
  - `brainlink_context`
714
725
  - `brainlink_context_packs`
715
726
  - `brainlink_search`
727
+ - `brainlink_explain`
716
728
  - `brainlink_dedupe`
717
729
  - `brainlink_resolve_duplicate`
718
730
  - `brainlink_add_note`
731
+ - `brainlink_remember`
732
+ - `brainlink_inbox_add`
733
+ - `brainlink_inbox_list`
734
+ - `brainlink_inbox_process`
719
735
  - `brainlink_delete_note`
720
736
  - `brainlink_add_file`
721
737
  - `brainlink_canonicalize_context_links`
@@ -723,14 +739,20 @@ Available MCP tools:
723
739
  - `brainlink_volatile_clear`
724
740
  - `brainlink_index`
725
741
  - `brainlink_stats`
742
+ - `brainlink_doctor_actions`
726
743
  - `brainlink_validate`
727
744
  - `brainlink_sync`
728
745
  - `brainlink_graph`
729
746
  - `brainlink_graph_contexts`
730
747
  - `brainlink_broken_links`
748
+ - `brainlink_suggest_links`
749
+ - `brainlink_repair_links`
731
750
  - `brainlink_orphans`
751
+ - `brainlink_session_close`
752
+ - `brainlink_project_init`
732
753
 
733
754
  Recommended start of every memory-dependent task: call `brainlink_bootstrap` first, then `brainlink_context`. By default, Brainlink enforces context-first for non-context MCP reads (`enforceContextFirst=true`), and also enforces bootstrap with auto-bootstrap on reads when state is missing or stale (`autoBootstrapOnRead=true`).
755
+ MCP is expected to stay functionally aligned with practical Brainlink CLI workflows whenever the operation is safe and useful for tool clients.
734
756
  MCP startup also bootstraps the configured default vault/agent automatically (`autoBootstrapOnStartup=true`), so sessions start warm without manual calls.
735
757
  If `autoBootstrapOnRead` or `enforceContextFirst` are disabled through `brainlink_policy`, behavior is relaxed accordingly; otherwise read tools return preflight-required responses when requirements are not satisfied.
736
758
  `brainlink_bootstrap`, `brainlink_policy` and preflight responses include structured `nextActions` so clients can continue tool flows automatically.
@@ -764,9 +786,10 @@ GET /api/stats
764
786
  GET /api/broken-links
765
787
  GET /api/orphans
766
788
  GET /api/validate
789
+ POST /api/import-file
767
790
  ```
768
791
 
769
- The HTTP API is read-only. Use the CLI for writes and indexing.
792
+ The HTTP API is read-oriented, with `POST /api/import-file` reserved for the local upload modal. Use the CLI for other writes and indexing.
770
793
 
771
794
  Indexing writes private encrypted search packs at `.brainlink/search-packs/*.blpk` for resilient retrieval and portability.
772
795
  Pack search now uses compressed-space prefiltering (token bloom index per pack) before decrypting/reading pack payloads.
@@ -165,9 +165,10 @@ server command
165
165
  -> /api/graph-layout derives a cauliflower hub layout from indexed graph data
166
166
  -> optional context query narrows the layout to a segment-scoped cauliflower subgraph
167
167
  -> browser renders graph canvas
168
+ -> /api/import-file converts uploaded documents and writes Markdown notes
168
169
  ```
169
170
 
170
- The graph UI is intentionally read-only. Markdown remains the write interface and index artifacts remain derived data.
171
+ The graph UI is primarily an inspection surface. Its upload modal is the narrow write path: uploaded files are converted to Markdown notes through the same application import use case used by the CLI. Markdown remains the source of truth and index artifacts remain derived data.
171
172
  The cauliflower layout is a visual projection over the indexed graph. Indexed weighted wiki-link edges remain unchanged for backlinks, ranking, graph reads and context traversal. The browser layout renders a simplified hierarchy instead: primary hub -> segment hubs -> local context nodes. This avoids unstable cross-context visual edges while preserving the real relationship model outside the render layer. Zoomed-out graph streams summarize those lobes as segment clusters before revealing individual nodes on zoom-in.
172
173
 
173
174
  ## HTTP API Flow
@@ -181,7 +182,7 @@ HTTP request
181
182
  ```
182
183
 
183
184
  The HTTP API is local-first and unauthenticated. It is meant for local agents, browser UI, and development workflows.
184
- The route adapter caches generated frontend assets (`/`, `/styles.css`, `/app.js`, `/app-worker.js`) and graph-layout JSON payloads by layout signature to reduce repeated CPU and allocation pressure during navigation.
185
+ The route adapter caches generated frontend assets (`/`, `/styles.css`, `/app.js`, `/app-worker.js`) and graph-layout JSON payloads by layout signature to reduce repeated CPU and allocation pressure during navigation. File upload conversion is intentionally routed through the import-file use case instead of writing vault files directly in the HTTP adapter.
185
186
 
186
187
  ## MCP Flow
187
188
 
@@ -75,6 +75,15 @@ blink context "what should I know before this task?" --mode hybrid --json
75
75
 
76
76
  ```bash
77
77
  blink add "Architecture Decision" --content "Use explicit [[Bounded Context]] links and #tags. #architecture #decision"
78
+ blink remember --content "Use explicit [[Bounded Context]] links and #tags. #architecture #decision"
79
+ ```
80
+
81
+ Use `remember` when you want Brainlink to infer a title, tags and Context Links. Use `inbox add` for quick capture, then `inbox process` to triage later.
82
+
83
+ ```bash
84
+ blink inbox add --content "Quick note to classify later"
85
+ blink inbox process --json
86
+ blink session-close --content "Validated the current task"
78
87
  ```
79
88
 
80
89
  ## 6) Validate Health
@@ -82,7 +91,11 @@ blink add "Architecture Decision" --content "Use explicit [[Bounded Context]] li
82
91
  ```bash
83
92
  blink validate
84
93
  blink doctor
94
+ blink doctor --actionable
85
95
  blink stats --extended --json
96
+ blink suggest-links --broken
97
+ blink repair-links
98
+ blink search "architecture" --explain
86
99
  ```
87
100
 
88
101
  ## 7) Migrate Existing Memory (Optional)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",