@andespindola/brainlink 1.6.14 → 1.6.15

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.15
4
+
5
+ - **Retrieve image bytes (`brainlink_get_image`).** A new MCP tool + `get-image` use-case reads an image previously stored by `brainlink_add_image` back out of the vault as base64 bytes + mime, decrypting when the vault is encrypted. Path is validated to stay inside `.brainlink/assets` (no traversal). This closes the cross-agent loop: one agent adds an image to the shared vault, another fetches and uses it.
6
+
3
7
  ## 1.6.14
4
8
 
5
9
  - **Images in notes and context.** New `brainlink_add_image` MCP tool (and application use-case) stores an image inside the vault (content-addressed, encrypted at rest when the vault is), and can attach it to a note that embeds it. Markdown image references (`![[image.png]]` embeds and `![alt](path)`) are now extracted per document without leaking into wiki-link graph edges, and image descriptors (path, mime, alt) are surfaced alongside text in retrieved context — never the raw bytes. Additive: image-free notes are unchanged.
@@ -0,0 +1,63 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { isAbsolute, join, relative, resolve } from 'node:path';
3
+ import { imageMimeForRef } from '../domain/image-refs.js';
4
+ import { ensureVault } from '../infrastructure/file-system-vault.js';
5
+ import { decryptVaultContent, isEncryptedVaultPayload, readOrCreateVaultKey } from '../infrastructure/vault-crypto.js';
6
+ // The assets tree images are stored under; the only location get-image is
7
+ // allowed to read from. Mirrors the constant used by add-image.
8
+ const assetsRoot = '.brainlink/assets';
9
+ const isInside = (parent, child) => {
10
+ const rel = relative(parent, child);
11
+ return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
12
+ };
13
+ // Normalize the requested path to the logical (plaintext) asset path so the
14
+ // returned descriptor is stable regardless of whether the caller passed the
15
+ // `.enc` mirror or the plaintext path.
16
+ const toLogicalPath = (assetPath) => assetPath.toLowerCase().endsWith('.enc') ? assetPath.slice(0, -'.enc'.length) : assetPath;
17
+ // Read the raw bytes for a stored image, decrypting transparently when the
18
+ // bytes on disk are an encrypted-at-rest payload. Prefers the plaintext file
19
+ // and falls back to the `.enc` mirror, so it works whether the vault is
20
+ // encrypted or not.
21
+ const readAssetBytes = async (vaultRoot, logicalAbsolute) => {
22
+ const decryptIfNeeded = async (raw) => {
23
+ if (!isEncryptedVaultPayload(raw)) {
24
+ return { bytes: raw, encrypted: false };
25
+ }
26
+ // Only touch the key when the on-disk bytes are actually ciphertext, so a
27
+ // plaintext vault never mints a versioning key just to read an image.
28
+ const vaultKey = await readOrCreateVaultKey(vaultRoot);
29
+ return { bytes: decryptVaultContent(vaultKey, raw), encrypted: true };
30
+ };
31
+ try {
32
+ return await decryptIfNeeded(await readFile(logicalAbsolute));
33
+ }
34
+ catch (error) {
35
+ if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') {
36
+ throw error;
37
+ }
38
+ }
39
+ return decryptIfNeeded(await readFile(`${logicalAbsolute}.enc`));
40
+ };
41
+ // Retrieve a stored image's raw bytes (base64) and mime so a second agent can
42
+ // pull an image another agent added to the shared vault. The path is validated
43
+ // to live inside `.brainlink/assets` (no traversal), and encrypted-at-rest
44
+ // payloads are decrypted with the existing vault key.
45
+ export const getImage = async (input) => {
46
+ const vaultRoot = await ensureVault(input.vaultPath);
47
+ const logicalPath = toLogicalPath(input.path);
48
+ const logicalAbsolute = resolve(vaultRoot, logicalPath);
49
+ const assetsAbsolute = join(vaultRoot, assetsRoot);
50
+ if (!isInside(assetsAbsolute, logicalAbsolute)) {
51
+ throw new Error(`Refusing to read image outside the vault assets tree: ${input.path}`);
52
+ }
53
+ const { bytes, encrypted } = await readAssetBytes(vaultRoot, logicalAbsolute);
54
+ const mime = imageMimeForRef(logicalPath) ?? 'application/octet-stream';
55
+ const relativePath = relative(vaultRoot, logicalAbsolute).split('\\').join('/');
56
+ return {
57
+ path: relativePath,
58
+ mime,
59
+ bytes: bytes.toString('base64'),
60
+ encrypted,
61
+ ...(input.alt?.trim() ? { alt: input.alt.trim() } : {})
62
+ };
63
+ };
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { addNoteInputSchema, addFileInputSchema, addFileTool, addImageInputSchema, addImageTool, addNoteTool, addNotesInputSchema, addNotesTool, deleteNoteInputSchema, deleteNoteTool, deleteNotesInputSchema, deleteNotesTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, contextInputSchema, contextPacksInputSchema, contextPacksTool, contextTool, doctorActionsInputSchema, doctorActionsTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, explainInputSchema, explainTool, indexInputSchema, indexTool, inboxAddInputSchema, inboxAddTool, inboxListInputSchema, inboxListTool, inboxProcessInputSchema, inboxProcessTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, projectInitInputSchema, projectInitTool, recommendationsInputSchema, recommendationsTool, rememberInputSchema, rememberTool, repairLinksInputSchema, repairLinksTool, searchInputSchema, searchTool, similarNotesInputSchema, similarNotesTool, sessionCloseInputSchema, sessionCloseTool, statsInputSchema, statsTool, suggestLinksInputSchema, suggestLinksTool, syncInputSchema, syncTool, validateInputSchema, validateTool, vaultBundleListInputSchema, vaultBundleListTool, vaultCommitInputSchema, vaultCommitTool, vaultExportBundleInputSchema, vaultExportBundleTool, vaultExportInputSchema, vaultExportTool, vaultHistoryInputSchema, vaultHistoryTool, vaultImportBundleInputSchema, vaultImportBundleTool, vaultImportInputSchema, vaultImportTool, vaultInitInputSchema, vaultInitTool, vaultMergeAgentsInputSchema, vaultMergeAgentsTool, vaultProvisionInputSchema, vaultProvisionTool, vaultPullInputSchema, vaultPullTool, vaultRestoreInputSchema, vaultRestoreTool, vaultStatusInputSchema, vaultStatusTool, versionInputSchema, versionTool } from './tools.js';
2
+ import { addNoteInputSchema, addFileInputSchema, addFileTool, addImageInputSchema, addImageTool, addNoteTool, addNotesInputSchema, addNotesTool, deleteNoteInputSchema, deleteNoteTool, deleteNotesInputSchema, deleteNotesTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, contextInputSchema, contextPacksInputSchema, contextPacksTool, contextTool, doctorActionsInputSchema, doctorActionsTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, getImageInputSchema, getImageTool, explainInputSchema, explainTool, indexInputSchema, indexTool, inboxAddInputSchema, inboxAddTool, inboxListInputSchema, inboxListTool, inboxProcessInputSchema, inboxProcessTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, projectInitInputSchema, projectInitTool, recommendationsInputSchema, recommendationsTool, rememberInputSchema, rememberTool, repairLinksInputSchema, repairLinksTool, searchInputSchema, searchTool, similarNotesInputSchema, similarNotesTool, sessionCloseInputSchema, sessionCloseTool, statsInputSchema, statsTool, suggestLinksInputSchema, suggestLinksTool, syncInputSchema, syncTool, validateInputSchema, validateTool, vaultBundleListInputSchema, vaultBundleListTool, vaultCommitInputSchema, vaultCommitTool, vaultExportBundleInputSchema, vaultExportBundleTool, vaultExportInputSchema, vaultExportTool, vaultHistoryInputSchema, vaultHistoryTool, vaultImportBundleInputSchema, vaultImportBundleTool, vaultImportInputSchema, vaultImportTool, vaultInitInputSchema, vaultInitTool, vaultMergeAgentsInputSchema, vaultMergeAgentsTool, vaultProvisionInputSchema, vaultProvisionTool, vaultPullInputSchema, vaultPullTool, vaultRestoreInputSchema, vaultRestoreTool, vaultStatusInputSchema, vaultStatusTool, versionInputSchema, versionTool } from './tools.js';
3
3
  import { getRuntimeVersion } from './runtime.js';
4
4
  import { brainlinkServerInstructions } from './server-instructions.js';
5
5
  import { guardToolHandler } from './tool-guard.js';
@@ -131,6 +131,11 @@ export const createBrainlinkMcpServer = () => {
131
131
  description: 'Store an image in the vault (encrypted at rest when vault encryption is on) and address it by a stable vault-relative path. Provide imageBase64 or filePath. Optionally pass noteTitle to create a note that embeds the image so retrieval can surface it in context. Alt text is included in the returned context descriptor.',
132
132
  inputSchema: addImageInputSchema
133
133
  }, addImageTool);
134
+ server.registerTool('brainlink_get_image', {
135
+ title: 'Get Brainlink Image',
136
+ description: 'Retrieve the raw bytes of an image stored in the vault by a stable vault-relative asset path (as returned by brainlink_add_image). Returns the descriptor plus base64-encoded bytes so a second agent can fetch and use an image another agent added to the shared vault. Decrypts transparently when the vault stores images encrypted at rest; the path must resolve inside .brainlink/assets.',
137
+ inputSchema: getImageInputSchema
138
+ }, getImageTool);
134
139
  server.registerTool('brainlink_canonicalize_context_links', {
135
140
  title: 'Canonicalize Brainlink Context Links',
136
141
  description: 'Ensure notes have canonical Context Links to inferred context hubs. Supports dry-run and can create missing hub notes.',
@@ -3,6 +3,7 @@ import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from
3
3
  import { buildContextPackage, readContextDataSignature, readVolatileSignature } from '../../application/build-context.js';
4
4
  import { getGraph } from '../../application/get-graph.js';
5
5
  import { getGraphContexts } from '../../application/get-graph-contexts.js';
6
+ import { getImage } from '../../application/get-image.js';
6
7
  import { explainSearchResults, suggestBrokenLinkFixes, suggestContextLinks } from '../../application/memory-suggestions.js';
7
8
  import { buildActionableDoctor } from '../../application/operational-workflows.js';
8
9
  import { searchKnowledge } from '../../application/search-knowledge.js';
@@ -539,3 +540,28 @@ export const recommendationsTool = async (input) => {
539
540
  recommendations
540
541
  });
541
542
  };
543
+ export const getImageInputSchema = {
544
+ ...vaultInput,
545
+ ...agentInput,
546
+ path: z
547
+ .string()
548
+ .min(1)
549
+ .describe('Vault-relative asset path to fetch, as returned by brainlink_add_image (e.g. ".brainlink/assets/shared/<sha>.png"). Must resolve inside the vault .brainlink/assets tree.'),
550
+ alt: z.string().min(1).optional().describe('Optional alt text / description to echo back in the descriptor.')
551
+ };
552
+ // Fetch the raw bytes of a stored image so another agent can retrieve and use an
553
+ // image a first agent added to the shared vault. Decrypts transparently when the
554
+ // bytes are stored encrypted-at-rest, and rejects paths outside .brainlink/assets.
555
+ export const getImageTool = async (input) => {
556
+ const context = await resolveExecutionContext(input);
557
+ const image = await getImage({
558
+ vaultPath: context.vault,
559
+ path: input.path,
560
+ alt: input.alt
561
+ });
562
+ return jsonResult({
563
+ vault: context.vault,
564
+ agent: context.agent,
565
+ image
566
+ });
567
+ };
package/dist/mcp/tools.js CHANGED
@@ -1,4 +1,4 @@
1
- export { brokenLinksInputSchema, brokenLinksTool, contextInputSchema, contextPacksInputSchema, contextPacksTool, contextTool, doctorActionsInputSchema, doctorActionsTool, explainInputSchema, explainTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, orphansInputSchema, orphansTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, similarNotesInputSchema, similarNotesTool, statsInputSchema, statsTool, suggestLinksInputSchema, suggestLinksTool, validateInputSchema, validateTool, versionInputSchema, versionTool } from './tools/read-tools.js';
1
+ export { brokenLinksInputSchema, brokenLinksTool, contextInputSchema, contextPacksInputSchema, contextPacksTool, contextTool, doctorActionsInputSchema, doctorActionsTool, explainInputSchema, explainTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, getImageInputSchema, getImageTool, orphansInputSchema, orphansTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, similarNotesInputSchema, similarNotesTool, statsInputSchema, statsTool, suggestLinksInputSchema, suggestLinksTool, validateInputSchema, validateTool, versionInputSchema, versionTool } from './tools/read-tools.js';
2
2
  export { addFileInputSchema, addFileTool, addImageInputSchema, addImageTool, addNoteInputSchema, addNoteTool, addNotesInputSchema, addNotesTool, deleteNoteInputSchema, deleteNoteTool, deleteNotesInputSchema, deleteNotesTool, inboxAddInputSchema, inboxAddTool, inboxListInputSchema, inboxListTool, inboxProcessInputSchema, inboxProcessTool, rememberInputSchema, rememberTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool } from './tools/write-tools.js';
3
3
  export { bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, indexInputSchema, indexTool, policyInputSchema, policyTool, projectInitInputSchema, projectInitTool, repairLinksInputSchema, repairLinksTool, sessionCloseInputSchema, sessionCloseTool, syncInputSchema, syncTool } from './tools/maintenance-tools.js';
4
4
  export { vaultBundleListInputSchema, vaultBundleListTool, vaultCommitInputSchema, vaultCommitTool, vaultExportBundleInputSchema, vaultExportBundleTool, vaultExportInputSchema, vaultExportTool, vaultHistoryInputSchema, vaultHistoryTool, vaultImportBundleInputSchema, vaultImportBundleTool, vaultImportInputSchema, vaultImportTool, vaultInitInputSchema, vaultInitTool, vaultMergeAgentsInputSchema, vaultMergeAgentsTool, vaultProvisionInputSchema, vaultProvisionTool, vaultPullInputSchema, vaultPullTool, vaultRestoreInputSchema, vaultRestoreTool, vaultStatusInputSchema, vaultStatusTool } from './tools/vault-tools.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "1.6.14",
3
+ "version": "1.6.15",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",