@andespindola/brainlink 1.0.0 → 1.0.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
@@ -537,6 +537,7 @@ Available tools:
537
537
  - `brainlink_dedupe`: detect duplicate candidates using exact hash + semantic similarity scores.
538
538
  - `brainlink_resolve_duplicate`: resolve duplicate pairs (`merge`, `link`, `ignore`) with connectivity-safe fallback edges.
539
539
  - `brainlink_add_note`: write durable Markdown memory and reindex.
540
+ - `brainlink_delete_note`: delete a durable Markdown note by title or path after explicit confirmation and reindex.
540
541
  - `brainlink_add_file`: ingest a local file as a note and reindex.
541
542
  - `brainlink_canonicalize_context_links`: ensure existing notes link to inferred context hubs.
542
543
  - `brainlink_volatile_add`: write temporary agent-decided memory with TTL; volatile sections are included in context and never create durable graph edges.
@@ -732,6 +733,17 @@ Lists known vaults from the configured default, `allowedVaults`, and the built-i
732
733
  `vaults use` chooses the default vault without migrating memory; use `migrate-vault` or `config set-vault --migrate-from` when you want to copy Markdown memory between vaults.
733
734
  `vaults delete` only deletes local filesystem vaults, requires `--yes`, refuses bucket vaults, and refuses deleting the current default vault. Choose another default first with `vaults use`.
734
735
 
736
+ ### `delete-note`
737
+
738
+ ```bash
739
+ blink delete-note --title "Architecture" --yes
740
+ blink delete-note --path agents/shared/architecture.md --yes
741
+ blink delete-note --title "Architecture" --agent coding-agent --yes
742
+ blink delete-note --path agents/shared/architecture.md --yes --no-auto-index
743
+ ```
744
+
745
+ Deletes a durable Markdown note from the selected vault. The command requires `--yes`, accepts exactly one selector (`--title` or `--path`), refuses non-Markdown/protected paths, and reindexes by default. Bucket vault note deletion is intentionally refused until provider-native object deletion is implemented safely.
746
+
735
747
  ### `migrate-vault`
736
748
 
737
749
  ```bash
@@ -0,0 +1,80 @@
1
+ import { relative, resolve } from 'node:path';
2
+ import { parseMarkdownDocument } from '../domain/markdown.js';
3
+ import { deleteMarkdownFile, ensureVault, isBucketVaultPath, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
4
+ import { indexVault } from './index-vault.js';
5
+ const normalizeTitleKey = (title) => title.trim().replace(/\.md$/i, '').toLowerCase();
6
+ const validateSelector = (input) => {
7
+ const hasTitle = input.title != null && input.title.trim().length > 0;
8
+ const hasPath = input.path != null && input.path.trim().length > 0;
9
+ if (!input.confirm) {
10
+ throw new Error('Refusing to delete note without explicit confirmation.');
11
+ }
12
+ if (hasTitle === hasPath) {
13
+ throw new Error('Use exactly one selector: title or path.');
14
+ }
15
+ };
16
+ const readCandidates = async (vaultPath) => {
17
+ const absoluteVaultPath = await ensureVault(vaultPath);
18
+ const files = await readMarkdownFiles(vaultPath);
19
+ return files.map((file) => {
20
+ const document = parseMarkdownDocument({
21
+ absolutePath: file.absolutePath,
22
+ vaultPath: absoluteVaultPath,
23
+ content: file.content,
24
+ createdAt: file.createdAt,
25
+ updatedAt: file.updatedAt
26
+ });
27
+ return {
28
+ absolutePath: file.absolutePath,
29
+ relativePath: document.path,
30
+ title: document.title,
31
+ agentId: document.agentId
32
+ };
33
+ });
34
+ };
35
+ const findByTitle = async (vaultPath, title, agentId) => {
36
+ const expected = normalizeTitleKey(title);
37
+ const matches = (await readCandidates(vaultPath)).filter((candidate) => normalizeTitleKey(candidate.title) === expected && (agentId == null || candidate.agentId === agentId));
38
+ if (matches.length === 0) {
39
+ throw new Error(`No note found with title: ${title}`);
40
+ }
41
+ if (matches.length > 1) {
42
+ throw new Error(`Multiple notes match title "${title}". Delete by path instead: ${matches.map((match) => match.relativePath).join(', ')}`);
43
+ }
44
+ return matches[0];
45
+ };
46
+ const findByPath = async (vaultPath, path) => {
47
+ const absoluteVaultPath = await ensureVault(vaultPath);
48
+ const expectedPath = resolve(absoluteVaultPath, path);
49
+ const candidates = await readCandidates(vaultPath);
50
+ const match = candidates.find((candidate) => resolve(candidate.absolutePath) === expectedPath);
51
+ if (match) {
52
+ return match;
53
+ }
54
+ const absolutePath = expectedPath;
55
+ return {
56
+ absolutePath,
57
+ relativePath: relative(absoluteVaultPath, absolutePath).replaceAll('\\', '/'),
58
+ title: relative(absoluteVaultPath, absolutePath).replace(/\.md$/i, ''),
59
+ agentId: 'shared'
60
+ };
61
+ };
62
+ export const deleteNote = async (vaultPath, input) => {
63
+ validateSelector(input);
64
+ if (isBucketVaultPath(vaultPath)) {
65
+ throw new Error('Deleting bucket vault notes is not supported from Brainlink yet. Remove bucket objects with your storage provider tooling.');
66
+ }
67
+ const candidate = input.title != null
68
+ ? await findByTitle(vaultPath, input.title, input.agentId)
69
+ : await findByPath(vaultPath, input.path ?? '');
70
+ await deleteMarkdownFile(vaultPath, candidate.relativePath);
71
+ const index = input.autoIndex === false ? undefined : await indexVault(vaultPath);
72
+ return {
73
+ deleted: true,
74
+ path: candidate.absolutePath,
75
+ relativePath: candidate.relativePath,
76
+ title: candidate.title,
77
+ agentId: candidate.agentId,
78
+ ...(index ? { index } : {})
79
+ };
80
+ };
@@ -5,6 +5,7 @@ import { platform, tmpdir } from 'node:os';
5
5
  import { spawn, spawnSync } from 'node:child_process';
6
6
  import { addNoteWithMetadata } from '../../application/add-note.js';
7
7
  import { buildContextPackage } from '../../application/build-context.js';
8
+ import { deleteNote } from '../../application/delete-note.js';
8
9
  import { resolveDuplicateNotes, scanDuplicateNotes } from '../../application/dedupe-notes.js';
9
10
  import { importLegacySqliteDatabase } from '../../application/import-legacy-sqlite.js';
10
11
  import { indexVault, indexVaultWithOptions } from '../../application/index-vault.js';
@@ -873,6 +874,30 @@ export const registerWriteCommands = (program) => {
873
874
  return `Created note at ${added.path}.${linkMessage}${duplicateMessage}`;
874
875
  });
875
876
  });
877
+ program
878
+ .command('delete-note')
879
+ .option('-v, --vault <vault>', 'vault directory')
880
+ .option('-a, --agent <agent>', 'agent memory namespace when deleting by title')
881
+ .option('--title <title>', 'note title to delete')
882
+ .option('--path <path>', 'vault-relative or absolute markdown note path to delete')
883
+ .option('--yes', 'confirm note deletion')
884
+ .option('--no-auto-index', 'skip reindexing after delete')
885
+ .option('--json', 'print machine-readable JSON')
886
+ .description('delete a Markdown note from the vault after explicit confirmation')
887
+ .action(async (options) => {
888
+ const resolved = await resolveOptions(options);
889
+ const result = await deleteNote(resolved.vault, {
890
+ title: options.title,
891
+ path: options.path,
892
+ agentId: resolved.agent,
893
+ confirm: Boolean(options.yes),
894
+ autoIndex: options.autoIndex !== false
895
+ });
896
+ print(options.json, {
897
+ vault: resolved.vault,
898
+ ...result
899
+ }, () => `Deleted note ${result.relativePath}.`);
900
+ });
876
901
  program
877
902
  .command('dedupe')
878
903
  .option('-v, --vault <vault>', 'vault directory')
@@ -1,4 +1,4 @@
1
- import { chmod, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
1
+ import { chmod, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
2
2
  import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
3
3
  import { resolvePath } from './paths.js';
4
4
  import { getBucketVaultCachePath, isBucketVaultUri, parseBucketVaultUri, syncBucketVaultToCache, writeBucketMarkdownFile } from './bucket-vault.js';
@@ -109,3 +109,23 @@ export const writeMarkdownFile = async (vaultPath, filename, content) => {
109
109
  await chmod(absolutePath, fileMode);
110
110
  return absolutePath;
111
111
  };
112
+ export const deleteMarkdownFile = async (vaultPath, filename) => {
113
+ if (isBucketVaultUri(vaultPath)) {
114
+ throw new Error('Deleting bucket vault notes is not supported from Brainlink yet. Remove bucket objects with your storage provider tooling.');
115
+ }
116
+ const absoluteVaultPath = await ensureVault(vaultPath);
117
+ const absolutePath = isAbsolute(filename) ? resolve(filename) : resolve(absoluteVaultPath, filename);
118
+ if (!isPathInside(absoluteVaultPath, absolutePath)) {
119
+ throw new Error(`Refusing to delete outside vault: ${absolutePath}`);
120
+ }
121
+ if (extname(absolutePath).toLowerCase() !== '.md') {
122
+ throw new Error(`Refusing to delete non-Markdown vault item: ${absolutePath}`);
123
+ }
124
+ const relativePath = relative(absoluteVaultPath, absolutePath);
125
+ const segments = relativePath.split(/[\\/]+/);
126
+ if (segments.some((segment) => excludedDirectories.has(segment))) {
127
+ throw new Error(`Refusing to delete protected vault path: ${relativePath}`);
128
+ }
129
+ await rm(absolutePath, { force: false });
130
+ return absolutePath;
131
+ };
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, contextInputSchema, contextPacksInputSchema, contextPacksTool, contextTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool, versionInputSchema, versionTool } from './tools.js';
2
+ import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, deleteNoteInputSchema, deleteNoteTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, contextInputSchema, contextPacksInputSchema, contextPacksTool, contextTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool, versionInputSchema, versionTool } from './tools.js';
3
3
  import { getRuntimeVersion } from './runtime.js';
4
4
  export const createBrainlinkMcpServer = () => {
5
5
  const server = new McpServer({
@@ -58,6 +58,11 @@ export const createBrainlinkMcpServer = () => {
58
58
  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.',
59
59
  inputSchema: addNoteInputSchema
60
60
  }, addNoteTool);
61
+ server.registerTool('brainlink_delete_note', {
62
+ title: 'Delete Brainlink Note',
63
+ description: 'Delete a durable Markdown note from the vault after explicit confirmation. Select by title or path; reindexes by default.',
64
+ inputSchema: deleteNoteInputSchema
65
+ }, deleteNoteTool);
61
66
  server.registerTool('brainlink_volatile_add', {
62
67
  title: 'Add Volatile Brainlink Memory',
63
68
  description: 'Write temporary agent-decided memory with TTL. Use for transient task state without polluting durable Markdown memory.',
package/dist/mcp/tools.js CHANGED
@@ -5,6 +5,7 @@ import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from
5
5
  import { addNoteWithMetadata } from '../application/add-note.js';
6
6
  import { buildContextPackage, readContextDataSignature } from '../application/build-context.js';
7
7
  import { canonicalizeContextLinks } from '../application/canonical-context-links.js';
8
+ import { deleteNote } from '../application/delete-note.js';
8
9
  import { resolveDuplicateNotes, scanDuplicateNotes } from '../application/dedupe-notes.js';
9
10
  import { getGraph } from '../application/get-graph.js';
10
11
  import { getGraphContexts } from '../application/get-graph-contexts.js';
@@ -263,6 +264,14 @@ export const addNoteInputSchema = {
263
264
  .optional()
264
265
  .describe('Automatically add canonical Context Links to the inferred visual context hub. Defaults to Brainlink config.')
265
266
  };
267
+ export const deleteNoteInputSchema = {
268
+ ...vaultInput,
269
+ ...agentInput,
270
+ title: z.string().min(1).optional().describe('Note title to delete. Use agent to disambiguate namespaced notes.'),
271
+ path: z.string().min(1).optional().describe('Vault-relative or absolute Markdown note path to delete.'),
272
+ confirm: z.boolean().describe('Must be true to confirm deletion.'),
273
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after deletion. Defaults to true.')
274
+ };
266
275
  export const volatileAddInputSchema = {
267
276
  ...vaultInput,
268
277
  ...agentInput,
@@ -495,6 +504,20 @@ export const addNoteTool = async (input) => {
495
504
  ...(index ? { index } : {})
496
505
  });
497
506
  };
507
+ export const deleteNoteTool = async (input) => {
508
+ const context = await resolveExecutionContext(input);
509
+ const result = await deleteNote(context.vault, {
510
+ title: input.title,
511
+ path: input.path,
512
+ agentId: context.agent,
513
+ confirm: input.confirm,
514
+ autoIndex: input.autoIndex
515
+ });
516
+ return jsonResult({
517
+ vault: context.vault,
518
+ ...result
519
+ });
520
+ };
498
521
  export const volatileAddTool = async (input) => {
499
522
  const context = await resolveExecutionContext(input);
500
523
  const entry = await addVolatileMemory(context.vault, input.content, context.agent ?? 'shared', input.ttlMinutes ?? 240, input.tags);
@@ -398,6 +398,15 @@ blink vaults delete /absolute/path/to/vault --yes
398
398
  `vaults use` switches the default vault without migrating memory.
399
399
  `vaults delete` deletes only local filesystem vaults, requires `--yes`, and refuses deleting the current default vault.
400
400
 
401
+ ### Delete Notes
402
+
403
+ ```bash
404
+ blink delete-note --title "Architecture" --yes
405
+ blink delete-note --path agents/shared/architecture.md --yes
406
+ ```
407
+
408
+ `delete-note` deletes durable Markdown notes only, requires explicit confirmation, accepts either `--title` or `--path`, and reindexes by default. MCP exposes the same capability as `brainlink_delete_note` with `confirm: true`.
409
+
401
410
  ### Migrate Vaults Explicitly
402
411
 
403
412
  ```bash
@@ -707,6 +716,7 @@ Available MCP tools:
707
716
  - `brainlink_dedupe`
708
717
  - `brainlink_resolve_duplicate`
709
718
  - `brainlink_add_note`
719
+ - `brainlink_delete_note`
710
720
  - `brainlink_add_file`
711
721
  - `brainlink_canonicalize_context_links`
712
722
  - `brainlink_volatile_add`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "1.0.0",
3
+ "version": "1.0.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",