@andespindola/brainlink 1.7.0 → 1.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.8.1
4
+
5
+ - **Fix the Connect page's Codex snippet auth.** The `/connect` page's Codex `config.toml` block passed the bearer token via a `MCP_AUTHORIZATION` env var, but `mcp-remote` does not read that env — so the request went out unauthenticated and `mcp-remote` fell back to a hanging OAuth flow. The token is now passed as an `mcp-remote` `--header "Authorization: Bearer <token>"` argument (verified to connect), and both the remote and local Codex snippets gained `startup_timeout_sec = 60` so Codex doesn't time out the cold-start MCP handshake.
6
+
7
+ ## 1.8.0
8
+
9
+ - **"Connect your agent" page in the graph UI.** The exposed graph web UI (`brainlink server --expose`) now serves a new auth-gated `GET /connect` page, reachable from the account menu, with ready-to-paste MCP setup for wiring agents and chats (Codex, Claude Code, ChatGPT/generic MCP) to this brainlink. It detects its mode server-side: in **remote** mode — when the server is exposed and `BRAINLINK_MCP_TOKEN` is set — it builds the MCP endpoint from the request host as `https://<host>/mcp` and renders the Codex `~/.codex/config.toml` (`mcp-remote`), Claude Code `claude mcp add --transport http`, `.mcp.json` (streamable HTTP) and ChatGPT/generic connector snippets, each carrying the owner's bearer token; in **local** mode — no token / not exposed — it renders the stdio equivalents (`brainlink mcp-server --vault <path>`) and never shows a token. The page also ships a ready AGENTS.md operating contract, per-snippet copy-to-clipboard buttons, and the shared neon identity. It is served behind the same session as `/settings`, so the token is shown only to the authenticated graph owner and never on any unauthenticated route.
10
+
3
11
  ## 1.7.0
4
12
 
5
13
  - **Unified neon graph UI.** The graph web frontend now shares one visual identity with the SaaS app so the app → SSO → graph handoff reads as a single product. Every self-contained page served by the graph server — the login and change-password pages, the settings page, the OAuth authorize/consent page, the graph explorer, and the injected account menu — is repainted onto the neon palette: `#0a0e1a` base with a faint cyan/violet cyber grid and soft ambient glow, cyan `#22d3ee` nodes, violet `#8b5cf6` edges/context, glass panels, gradient "Brainlink" wordmark, cyan focus glow, and a neon gradient primary button. Typography moves to Space Grotesk (headings) + DM Sans (body) via a Google Fonts `@import` with a robust system fallback so it degrades gracefully offline, while a monospace stack stays for graph node ids and code. All graph behavior (WebGL/worker/2D-fallback rendering, layout, zoom, pan, search, streaming, context grouping, versioning UI) and every endpoint, form field and OAuth flow are unchanged — this is a visual/identity pass only. Contrast on the new palette meets WCAG AA, focus rings are visible, and motion respects `prefers-reduced-motion`.
@@ -136,6 +136,10 @@ const startAccountMenu = () => {
136
136
  button.setAttribute('aria-expanded', 'true')
137
137
  }
138
138
 
139
+ addItem('Connect your agent', () => {
140
+ closeMenu()
141
+ window.location.href = '/connect'
142
+ })
139
143
  addItem('Settings', () => {
140
144
  closeMenu()
141
145
  window.location.href = '/settings'
@@ -0,0 +1,438 @@
1
+ // Self-contained branded "Connect your agent" page for the graph web server.
2
+ // Served as a plain string (inline <style> + inline <script>, no external assets
3
+ // beyond the shared Google Fonts @import, no framework) so it works identically
4
+ // for a global npm install. The visual identity is the unified neon system
5
+ // shared with the auth pages, the settings page and the graph client (see
6
+ // neon-theme.ts): #0a0e1a base, faint cyber grid, cyan #22d3ee + violet #8b5cf6,
7
+ // Space Grotesk headings / DM Sans body, glass card + gradient wordmark.
8
+ //
9
+ // The page renders ready-to-paste MCP setup so a graph owner can wire their
10
+ // agents (Codex, Claude Code, ChatGPT/MCP) to this brainlink. It has two modes,
11
+ // picked server-side:
12
+ // - remote: the server is exposed AND an MCP token is configured. Snippets
13
+ // point at the request host over HTTPS and carry a bearer token.
14
+ // - local: no token / not exposed. Snippets run the local stdio MCP server
15
+ // against the server's configured vault path — never a token.
16
+ // This page is only ever served behind the same session gate as /settings, so
17
+ // the token is shown only to the authenticated graph owner.
18
+ import { neonAmbientEffects, neonFontImport, neonGridBackground, neonRootTokens, neonWordmark } from './neon-theme.js';
19
+ // HTML-escape for text interpolated into the served document. Snippets are
20
+ // rendered as textContent client-side, but the AGENTS.md / labels are inlined,
21
+ // so escape defensively to keep the page well-formed and injection-free.
22
+ const escapeHtml = (value) => value
23
+ .replace(/&/g, '&amp;')
24
+ .replace(/</g, '&lt;')
25
+ .replace(/>/g, '&gt;')
26
+ .replace(/"/g, '&quot;');
27
+ // The compact operating contract shown on the page and offered as a ready
28
+ // AGENTS.md. Neutral/professional, referencing only the agent product names.
29
+ export const connectAgentsMarkdown = () => `# Brainlink — canonical memory
30
+
31
+ Brainlink is your canonical, persistent memory across turns and sessions. Treat
32
+ it as the continuity of your cognition, not an optional lookup.
33
+
34
+ ## At session start
35
+ - Run \`brainlink_bootstrap\` with a short \`query\` describing the task. Re-run it
36
+ whenever your context is stale.
37
+
38
+ ## Before you act
39
+ - Consult Brainlink before any decision, recommendation, question, or
40
+ implementation. Never act from memory alone when durable context may exist.
41
+ - Retrieve with \`strategy: "auto"\` (CAG on a fresh pack, RAG otherwise) and
42
+ \`mode: "hybrid"\` (FTS + semantic) unless a narrower mode clearly fits.
43
+
44
+ ## After you learn
45
+ - Write durable learnings back with \`brainlink_remember\` (or
46
+ \`brainlink_add_note\` for structured notes), then reindex with
47
+ \`brainlink_index\`.
48
+ - Write connected memory: include \`[[wiki links]]\`, tags, and priority markers
49
+ so notes join the knowledge graph instead of sitting orphaned.
50
+ `;
51
+ const connectStyles = () => `${neonFontImport()}
52
+ :root {
53
+ ${neonRootTokens()}
54
+ }
55
+
56
+ * {
57
+ box-sizing: border-box;
58
+ }
59
+
60
+ html,
61
+ body {
62
+ width: 100%;
63
+ min-height: 100%;
64
+ margin: 0;
65
+ }
66
+
67
+ body {
68
+ display: flex;
69
+ align-items: flex-start;
70
+ justify-content: center;
71
+ min-height: 100vh;
72
+ min-height: 100dvh;
73
+ padding: 40px 24px;
74
+ color: var(--text);
75
+ font-family: var(--font-body);
76
+ ${neonGridBackground()}
77
+ }
78
+
79
+ ${neonAmbientEffects()}
80
+
81
+ button {
82
+ font: inherit;
83
+ }
84
+
85
+ :focus-visible {
86
+ outline: 2px solid var(--accent);
87
+ outline-offset: 2px;
88
+ }
89
+
90
+ .connect-card {
91
+ width: 100%;
92
+ max-width: 720px;
93
+ padding: 32px 28px;
94
+ border: 1px solid var(--line);
95
+ border-radius: 16px;
96
+ background: var(--panel);
97
+ box-shadow: 0 24px 80px rgba(2, 6, 18, 0.55);
98
+ backdrop-filter: blur(16px);
99
+ animation: neon-fade-up 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
100
+ }
101
+
102
+ @media (prefers-reduced-motion: reduce) {
103
+ .connect-card {
104
+ animation: none;
105
+ }
106
+ }
107
+
108
+ .connect-brand {
109
+ display: grid;
110
+ gap: 4px;
111
+ margin-bottom: 20px;
112
+ }
113
+
114
+ .connect-brand strong {
115
+ font-family: var(--font-heading);
116
+ font-size: 28px;
117
+ font-weight: 700;
118
+ letter-spacing: -0.01em;
119
+ ${neonWordmark()}
120
+ }
121
+
122
+ .connect-brand span {
123
+ color: var(--muted);
124
+ font-size: 13px;
125
+ }
126
+
127
+ .connect-lede {
128
+ margin: 0 0 8px;
129
+ color: var(--text);
130
+ font-size: 14px;
131
+ line-height: 1.5;
132
+ }
133
+
134
+ .connect-mode {
135
+ display: inline-flex;
136
+ align-items: center;
137
+ gap: 8px;
138
+ margin-bottom: 20px;
139
+ padding: 6px 12px;
140
+ border: 1px solid var(--line);
141
+ border-radius: 999px;
142
+ background: rgba(19, 26, 42, 0.55);
143
+ color: var(--muted);
144
+ font-size: 12px;
145
+ }
146
+
147
+ .connect-mode strong {
148
+ color: var(--accent);
149
+ font-weight: 600;
150
+ }
151
+
152
+ .connect-endpoint {
153
+ display: grid;
154
+ gap: 6px;
155
+ margin-bottom: 24px;
156
+ padding: 12px 14px;
157
+ border: 1px solid var(--line);
158
+ border-radius: 10px;
159
+ background: rgba(19, 26, 42, 0.55);
160
+ }
161
+
162
+ .connect-endpoint span {
163
+ color: var(--muted);
164
+ font-size: 11px;
165
+ letter-spacing: 0.02em;
166
+ }
167
+
168
+ .connect-endpoint code {
169
+ font-family: var(--font-mono);
170
+ font-size: 12px;
171
+ color: var(--accent);
172
+ overflow-wrap: anywhere;
173
+ }
174
+
175
+ .connect-section {
176
+ margin-top: 26px;
177
+ padding-top: 22px;
178
+ border-top: 1px solid var(--line);
179
+ display: grid;
180
+ gap: 12px;
181
+ }
182
+
183
+ .connect-section h2 {
184
+ margin: 0;
185
+ font-family: var(--font-heading);
186
+ font-weight: 600;
187
+ font-size: 16px;
188
+ letter-spacing: 0.01em;
189
+ }
190
+
191
+ .connect-hint {
192
+ margin: 0;
193
+ color: var(--muted);
194
+ font-size: 12px;
195
+ line-height: 1.5;
196
+ }
197
+
198
+ .connect-hint code {
199
+ font-family: var(--font-mono);
200
+ color: var(--accent);
201
+ }
202
+
203
+ .connect-snippet {
204
+ position: relative;
205
+ display: grid;
206
+ gap: 8px;
207
+ }
208
+
209
+ .connect-snippet pre {
210
+ margin: 0;
211
+ padding: 14px 16px;
212
+ border: 1px solid var(--line);
213
+ border-radius: 10px;
214
+ background: rgba(10, 14, 26, 0.75);
215
+ overflow-x: auto;
216
+ }
217
+
218
+ .connect-snippet code {
219
+ font-family: var(--font-mono);
220
+ font-size: 12px;
221
+ line-height: 1.5;
222
+ color: var(--text);
223
+ white-space: pre;
224
+ }
225
+
226
+ .connect-copy {
227
+ justify-self: start;
228
+ height: 32px;
229
+ padding: 0 14px;
230
+ border: 1px solid var(--line);
231
+ border-radius: 10px;
232
+ background: rgba(19, 26, 42, 0.7);
233
+ color: var(--text);
234
+ cursor: pointer;
235
+ font-size: 12px;
236
+ transition: border-color 150ms ease, background 150ms ease, box-shadow 150ms ease;
237
+ }
238
+
239
+ .connect-copy:hover,
240
+ .connect-copy:focus {
241
+ border-color: var(--accent);
242
+ background: var(--accent-weak);
243
+ box-shadow: var(--glow-cyan);
244
+ }
245
+
246
+ .connect-back {
247
+ display: inline-block;
248
+ margin-top: 24px;
249
+ color: var(--muted);
250
+ font-size: 12px;
251
+ text-decoration: none;
252
+ }
253
+
254
+ .connect-back:hover,
255
+ .connect-back:focus {
256
+ color: var(--accent);
257
+ }`;
258
+ // One inline copy-button behaviour reused for every snippet, mirroring the
259
+ // settings page: prefer the async clipboard API, fall back to a text selection.
260
+ const connectScript = () => `(function () {
261
+ var buttons = document.querySelectorAll('.connect-copy');
262
+
263
+ function flash(button) {
264
+ var original = button.getAttribute('data-label') || 'Copy';
265
+ button.textContent = 'Copied';
266
+ setTimeout(function () {
267
+ button.textContent = original;
268
+ }, 1200);
269
+ }
270
+
271
+ Array.prototype.forEach.call(buttons, function (button) {
272
+ button.setAttribute('data-label', button.textContent);
273
+ button.addEventListener('click', function () {
274
+ var targetId = button.getAttribute('data-target');
275
+ var target = targetId ? document.getElementById(targetId) : null;
276
+ if (!target) {
277
+ return;
278
+ }
279
+ var text = target.textContent || '';
280
+ if (navigator.clipboard && navigator.clipboard.writeText) {
281
+ navigator.clipboard.writeText(text).then(
282
+ function () {
283
+ flash(button);
284
+ },
285
+ function () {
286
+ selectText(target);
287
+ }
288
+ );
289
+ return;
290
+ }
291
+ selectText(target);
292
+ try {
293
+ document.execCommand('copy');
294
+ flash(button);
295
+ } catch (err) {
296
+ // Selection is left in place so the user can copy manually.
297
+ }
298
+ });
299
+ });
300
+
301
+ function selectText(node) {
302
+ if (typeof window.getSelection !== 'function' || typeof document.createRange !== 'function') {
303
+ return;
304
+ }
305
+ var selection = window.getSelection();
306
+ var range = document.createRange();
307
+ range.selectNodeContents(node);
308
+ selection.removeAllRanges();
309
+ selection.addRange(range);
310
+ }
311
+ })();`;
312
+ // A snippet block: a titled hint, a <pre><code> with a stable id, and its copy
313
+ // button. The code body is escaped so it survives interpolation verbatim.
314
+ const snippetBlock = (id, title, hint, body) => `
315
+ <div class="connect-snippet">
316
+ <p class="connect-hint"><strong>${escapeHtml(title)}</strong> — ${hint}</p>
317
+ <pre><code id="${escapeHtml(id)}">${escapeHtml(body)}</code></pre>
318
+ <button class="connect-copy" type="button" data-target="${escapeHtml(id)}">Copy</button>
319
+ </div>`;
320
+ const remoteSnippets = (mcpUrl, token) => {
321
+ // The bearer token MUST be an mcp-remote `--header` arg — mcp-remote does not
322
+ // read a MCP_AUTHORIZATION env var, so the env form leaves the request
323
+ // unauthenticated and mcp-remote falls back to a hanging OAuth flow.
324
+ const codexToml = `[mcp_servers.brainlink]
325
+ command = "npx"
326
+ args = ["-y", "mcp-remote", "${mcpUrl}", "--header", "Authorization: Bearer ${token}"]
327
+ startup_timeout_sec = 60`;
328
+ const claudeCmd = `claude mcp add --transport http brainlink ${mcpUrl} --header "Authorization: Bearer ${token}"`;
329
+ const mcpJson = `{
330
+ "mcpServers": {
331
+ "brainlink": {
332
+ "type": "http",
333
+ "url": "${mcpUrl}",
334
+ "headers": {
335
+ "Authorization": "Bearer ${token}"
336
+ }
337
+ }
338
+ }
339
+ }`;
340
+ const chatgptCmd = `Connector URL: ${mcpUrl}
341
+ Authorization: Bearer ${token}`;
342
+ return `
343
+ <section class="connect-section" aria-labelledby="codexTitle">
344
+ <h2 id="codexTitle">Codex</h2>
345
+ ${snippetBlock('codexSnippet', '~/.codex/config.toml', 'add this MCP server block, then restart Codex.', codexToml)}
346
+ </section>
347
+ <section class="connect-section" aria-labelledby="claudeTitle">
348
+ <h2 id="claudeTitle">Claude Code</h2>
349
+ ${snippetBlock('claudeSnippet', 'terminal', 'register the remote server over streamable HTTP.', claudeCmd)}
350
+ </section>
351
+ <section class="connect-section" aria-labelledby="mcpJsonTitle">
352
+ <h2 id="mcpJsonTitle">.mcp.json</h2>
353
+ ${snippetBlock('mcpJsonSnippet', 'project .mcp.json', 'drop-in config for any MCP client that reads .mcp.json.', mcpJson)}
354
+ </section>
355
+ <section class="connect-section" aria-labelledby="chatgptTitle">
356
+ <h2 id="chatgptTitle">ChatGPT / generic MCP</h2>
357
+ ${snippetBlock('chatgptSnippet', 'connector', 'point a generic MCP connector at the endpoint with a bearer token.', chatgptCmd)}
358
+ </section>`;
359
+ };
360
+ const localSnippets = (vaultPath) => {
361
+ const codexToml = `[mcp_servers.brainlink]
362
+ command = "brainlink"
363
+ args = ["mcp-server", "--vault", "${vaultPath}"]
364
+ startup_timeout_sec = 60`;
365
+ const claudeCmd = `claude mcp add brainlink -- brainlink mcp-server --vault ${vaultPath}`;
366
+ const mcpJson = `{
367
+ "mcpServers": {
368
+ "brainlink": {
369
+ "command": "brainlink",
370
+ "args": ["mcp-server", "--vault", "${vaultPath}"]
371
+ }
372
+ }
373
+ }`;
374
+ return `
375
+ <section class="connect-section" aria-labelledby="codexTitle">
376
+ <h2 id="codexTitle">Codex</h2>
377
+ ${snippetBlock('codexSnippet', '~/.codex/config.toml', 'run the local MCP server over stdio, then restart Codex.', codexToml)}
378
+ </section>
379
+ <section class="connect-section" aria-labelledby="claudeTitle">
380
+ <h2 id="claudeTitle">Claude Code</h2>
381
+ ${snippetBlock('claudeSnippet', 'terminal', 'register the local stdio server.', claudeCmd)}
382
+ </section>
383
+ <section class="connect-section" aria-labelledby="mcpJsonTitle">
384
+ <h2 id="mcpJsonTitle">.mcp.json</h2>
385
+ ${snippetBlock('mcpJsonSnippet', 'project .mcp.json', 'drop-in stdio config for any MCP client that reads .mcp.json.', mcpJson)}
386
+ </section>`;
387
+ };
388
+ const modeBadge = (mode) => mode === 'remote'
389
+ ? '<div class="connect-mode">Mode <strong>Remote</strong> · authenticated over HTTPS with your token</div>'
390
+ : '<div class="connect-mode">Mode <strong>Local</strong> · stdio MCP server on this machine</div>';
391
+ const endpointBlock = (data) => data.mode === 'remote'
392
+ ? `<div class="connect-endpoint">
393
+ <span>MCP endpoint</span>
394
+ <code>${escapeHtml(data.mcpUrl)}</code>
395
+ <span>Authorization</span>
396
+ <code>Bearer ${escapeHtml(data.token)}</code>
397
+ </div>`
398
+ : `<div class="connect-endpoint">
399
+ <span>Vault path</span>
400
+ <code>${escapeHtml(data.vaultPath)}</code>
401
+ </div>`;
402
+ export const createConnectPageHtml = (data) => {
403
+ const snippets = data.mode === 'remote' ? remoteSnippets(data.mcpUrl, data.token) : localSnippets(data.vaultPath);
404
+ const lede = data.mode === 'remote'
405
+ ? 'Wire your agents and chats to this brainlink over the remote MCP endpoint. Paste a snippet, keep your token private.'
406
+ : 'Wire your agents and chats to this brainlink by running the local MCP server against your vault. Paste a snippet to get started.';
407
+ return `<!DOCTYPE html>
408
+ <html lang="en">
409
+ <head>
410
+ <meta charset="utf-8" />
411
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
412
+ <title>Connect your agent · Brainlink</title>
413
+ <style>${connectStyles()}</style>
414
+ </head>
415
+ <body>
416
+ <main class="connect-card">
417
+ <div class="connect-brand">
418
+ <strong>Brainlink</strong>
419
+ <span>Connect your agent</span>
420
+ </div>
421
+ <p class="connect-lede">${escapeHtml(lede)}</p>
422
+ ${modeBadge(data.mode)}
423
+ ${endpointBlock(data)}
424
+ ${snippets}
425
+ <section class="connect-section" aria-labelledby="agentsTitle">
426
+ <h2 id="agentsTitle">Ready AGENTS.md</h2>
427
+ <p class="connect-hint">Drop this operating contract into your agent so it treats this brainlink as canonical memory.</p>
428
+ <div class="connect-snippet">
429
+ <pre><code id="agentsSnippet">${escapeHtml(connectAgentsMarkdown())}</code></pre>
430
+ <button class="connect-copy" type="button" data-target="agentsSnippet">Copy</button>
431
+ </div>
432
+ </section>
433
+ <a class="connect-back" href="/">&larr; Back to graph</a>
434
+ </main>
435
+ <script>${connectScript()}</script>
436
+ </body>
437
+ </html>`;
438
+ };
@@ -29,6 +29,7 @@ import { getGraphVersion } from './graph-version.js';
29
29
  import { readAuthStatus, readEditableSettings, updateEditableSettings } from './settings-store.js';
30
30
  import { connectVaultRemote, ensureDeployKey, readVersioningStatus } from './versioning.js';
31
31
  import { createSettingsPageHtml } from '../frontend/settings-page.js';
32
+ import { createConnectPageHtml } from '../frontend/connect-page.js';
32
33
  import { contentTypes, createJsonResponse, isReadMethod, parsePositiveInteger } from './http.js';
33
34
  import { parseMultipartForm } from './multipart.js';
34
35
  const readRuntimeDefaults = async (url) => {
@@ -90,6 +91,29 @@ const requestIsSecure = (request) => {
90
91
  const proto = firstHeaderValue(request.headers['x-forwarded-proto']);
91
92
  return typeof proto === 'string' && proto.split(',')[0]?.trim() === 'https';
92
93
  };
94
+ // Resolve the "Connect your agent" page data at request time. Remote mode is
95
+ // picked only when the server is exposed (auth-gated) AND an MCP token is
96
+ // configured: only then can we hand out a working bearer + public endpoint.
97
+ // Otherwise the page falls back to the local stdio config and never shows a
98
+ // token. The host prefers the proxy-forwarded value since exposed mode sits
99
+ // behind a TLS-terminating reverse proxy.
100
+ const resolveConnectHost = (request, url) => {
101
+ const forwarded = firstHeaderValue(request.headers['x-forwarded-host']);
102
+ const forwardedHost = typeof forwarded === 'string' ? forwarded.split(',')[0]?.trim() : '';
103
+ if (forwardedHost) {
104
+ return forwardedHost;
105
+ }
106
+ const host = firstHeaderValue(request.headers.host);
107
+ return typeof host === 'string' && host.length > 0 ? host : url.host;
108
+ };
109
+ const resolveConnectPageData = (request, url, vaultPath, requireAuth) => {
110
+ const token = process.env.BRAINLINK_MCP_TOKEN?.trim() ?? '';
111
+ if (requireAuth && token.length > 0) {
112
+ const host = resolveConnectHost(request, url);
113
+ return { mode: 'remote', mcpUrl: `https://${host}/mcp`, token, vaultPath };
114
+ }
115
+ return { mode: 'local', mcpUrl: '', token: '', vaultPath };
116
+ };
93
117
  const isHtmlNavigation = (request, url) => {
94
118
  if (url.pathname.startsWith('/api/')) {
95
119
  return false;
@@ -566,6 +590,12 @@ export const route = async (request, url, vaultPath, options = {}) => {
566
590
  if (isReadMethod(request) && url.pathname === '/settings') {
567
591
  return createResponse(createSettingsPageHtml(), 200, contentTypes['.html']);
568
592
  }
593
+ // "Connect your agent": ready-to-paste MCP setup for wiring agents to this
594
+ // brainlink. Auth-gated with the rest of the exposed graph (runAuthGate ran
595
+ // above), so it may surface the owner's bearer token in remote mode.
596
+ if (isReadMethod(request) && url.pathname === '/connect') {
597
+ return createResponse(createConnectPageHtml(resolveConnectPageData(request, url, vaultPath, options.requireAuth === true)), 200, contentTypes['.html']);
598
+ }
569
599
  // Vault git versioning: generate an SSH deploy key on demand and connect a
570
600
  // GitHub repo (clone-and-aggregate, conflict-preserving — never overwrites).
571
601
  if (isReadMethod(request) && url.pathname === '/api/versioning-status') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "1.7.0",
3
+ "version": "1.8.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",