@andespindola/brainlink 1.0.3 → 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();
@@ -724,9 +724,14 @@ Available MCP tools:
724
724
  - `brainlink_context`
725
725
  - `brainlink_context_packs`
726
726
  - `brainlink_search`
727
+ - `brainlink_explain`
727
728
  - `brainlink_dedupe`
728
729
  - `brainlink_resolve_duplicate`
729
730
  - `brainlink_add_note`
731
+ - `brainlink_remember`
732
+ - `brainlink_inbox_add`
733
+ - `brainlink_inbox_list`
734
+ - `brainlink_inbox_process`
730
735
  - `brainlink_delete_note`
731
736
  - `brainlink_add_file`
732
737
  - `brainlink_canonicalize_context_links`
@@ -734,14 +739,20 @@ Available MCP tools:
734
739
  - `brainlink_volatile_clear`
735
740
  - `brainlink_index`
736
741
  - `brainlink_stats`
742
+ - `brainlink_doctor_actions`
737
743
  - `brainlink_validate`
738
744
  - `brainlink_sync`
739
745
  - `brainlink_graph`
740
746
  - `brainlink_graph_contexts`
741
747
  - `brainlink_broken_links`
748
+ - `brainlink_suggest_links`
749
+ - `brainlink_repair_links`
742
750
  - `brainlink_orphans`
751
+ - `brainlink_session_close`
752
+ - `brainlink_project_init`
743
753
 
744
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.
745
756
  MCP startup also bootstraps the configured default vault/agent automatically (`autoBootstrapOnStartup=true`), so sessions start warm without manual calls.
746
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.
747
758
  `brainlink_bootstrap`, `brainlink_policy` and preflight responses include structured `nextActions` so clients can continue tool flows automatically.
@@ -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.3",
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",