@andespindola/brainlink 0.1.0-beta.33 → 0.1.0-beta.34

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
@@ -24,6 +24,8 @@
24
24
  - Added `docs/QUICKSTART.md` and aligned README/agent docs with the latest CLI/MCP flows.
25
25
  - Added middle-out context assembly so chunk selection expands around the strongest note chunk.
26
26
  - Added compressed-space pack prefiltering (token bloom index) before `.blpk` decryption and scan.
27
+ - Improved graph UI auto-fit and viewport recovery so loaded nodes are re-centered when zoom/pan drifts to empty canvas.
28
+ - Added cross-platform native desktop GUI auto-open for `blink server` (macOS Swift/WebKit, Windows PowerShell WinForms, Linux Python GTK/WebKit2), with app-window/browser fallback.
27
29
 
28
30
  ## 0.1.0-beta.3
29
31
 
package/README.md CHANGED
@@ -553,8 +553,13 @@ blink server --host 127.0.0.1 --port 4321 --watch
553
553
  ```
554
554
 
555
555
  By default, the server uses `$HOME/.brainlink/vault`. Pass `--vault ./vault` only when you want to inspect a custom vault.
556
- By default, `blink server` tries to open the graph in a dedicated app window (`--app=<url>` on compatible browsers).
557
- If app-window launch is unavailable, it falls back to the default browser. Use `--no-open` to keep it headless.
556
+ By default, `blink server` tries to open the graph in a native desktop GUI window:
557
+ - macOS: Swift + WebKit
558
+ - Windows: PowerShell WinForms WebBrowser
559
+ - Linux: Python GTK + WebKit2 (requires `python3` + `gi` + `WebKit2`)
560
+
561
+ If native GUI launch is unavailable on your system, it falls back to dedicated app-window mode and then to the default browser.
562
+ Use `--no-open` to keep it headless.
558
563
 
559
564
  The graph UI shows:
560
565
 
@@ -844,7 +849,8 @@ blink server --vault ./vault --watch --no-open
844
849
  ```
845
850
 
846
851
  Starts the local read-only graph UI and HTTP API.
847
- By default, it tries to open a dedicated app window for the graph URL and falls back to browser open when app-window mode is unavailable.
852
+ By default, it tries to open a native desktop GUI window for the graph URL.
853
+ If native GUI launch is unavailable, it falls back to dedicated app-window mode and then browser open.
848
854
  Use `--no-open` to skip that behavior.
849
855
 
850
856
  The HTTP server only binds to loopback hosts such as `127.0.0.1`, `localhost` or `::1`.
@@ -245,8 +245,8 @@ li small {
245
245
  }
246
246
 
247
247
  .content-dialog {
248
- width: min(920px, calc(100vw - 32px));
249
- max-height: calc(100svh - 32px);
248
+ width: min(1240px, calc(100vw - 24px));
249
+ max-height: calc(100svh - 20px);
250
250
  padding: 0;
251
251
  border: 1px solid var(--line);
252
252
  border-radius: 8px;
@@ -263,7 +263,7 @@ li small {
263
263
  .content-dialog article {
264
264
  display: grid;
265
265
  grid-template-rows: auto auto minmax(0, 1fr);
266
- max-height: calc(100svh - 34px);
266
+ max-height: calc(100svh - 22px);
267
267
  }
268
268
 
269
269
  .content-dialog header {
@@ -339,7 +339,7 @@ li small {
339
339
 
340
340
  .content-meta-section ul,
341
341
  .content-meta-section .tags {
342
- max-height: 140px;
342
+ max-height: 220px;
343
343
  overflow: auto;
344
344
  align-content: flex-start;
345
345
  padding-right: 4px;
@@ -26,7 +26,9 @@ const state = {
26
26
  cursor: { x: 0, y: 0, inCanvas: false },
27
27
  graphSignature: '',
28
28
  graphStatus: '',
29
- last: performance.now()
29
+ last: performance.now(),
30
+ offscreenFrameCount: 0,
31
+ recoveringViewport: false
30
32
  }
31
33
 
32
34
  const byId = id => document.getElementById(id)
@@ -130,6 +132,7 @@ const recomputeVisibility = () => {
130
132
  const edgeWeight = edge => Number.isFinite(edge.weight) ? Math.max(1, edge.weight) : 1
131
133
 
132
134
  const clampScale = value => Math.max(zoomRange.min, Math.min(zoomRange.max, value))
135
+ const isFiniteNumber = value => Number.isFinite(value)
133
136
 
134
137
  const graphBounds = nodes => {
135
138
  if (nodes.length === 0) return null
@@ -157,14 +160,25 @@ const graphBounds = nodes => {
157
160
  }
158
161
 
159
162
  const fitScaleBiasByNodeCount = nodeCount => {
160
- if (nodeCount <= 6) return 2.8
161
- if (nodeCount <= 20) return 2.2
162
- if (nodeCount <= 60) return 1.72
163
- if (nodeCount <= 180) return 1.34
164
- if (nodeCount <= 600) return 1.08
165
- if (nodeCount <= 2000) return 0.9
166
- if (nodeCount <= 6000) return 0.72
167
- return 0.58
163
+ if (nodeCount <= 6) return 1.22
164
+ if (nodeCount <= 20) return 1.12
165
+ if (nodeCount <= 60) return 1.04
166
+ if (nodeCount <= 180) return 1
167
+ if (nodeCount <= 600) return 0.94
168
+ if (nodeCount <= 2000) return 0.82
169
+ if (nodeCount <= 6000) return 0.68
170
+ return 0.56
171
+ }
172
+
173
+ const autoFitScaleRangeByNodeCount = nodeCount => {
174
+ if (nodeCount <= 6) return { min: 0.4, max: 2.2 }
175
+ if (nodeCount <= 20) return { min: 0.34, max: 1.65 }
176
+ if (nodeCount <= 60) return { min: 0.25, max: 1.22 }
177
+ if (nodeCount <= 180) return { min: 0.18, max: 0.92 }
178
+ if (nodeCount <= 600) return { min: 0.12, max: 0.72 }
179
+ if (nodeCount <= 2000) return { min: 0.08, max: 0.52 }
180
+ if (nodeCount <= 6000) return { min: 0.06, max: 0.32 }
181
+ return { min: zoomRange.min, max: 0.24 }
168
182
  }
169
183
 
170
184
  const fitView = (options = { useFiltered: true }) => {
@@ -188,25 +202,13 @@ const fitView = (options = { useFiltered: true }) => {
188
202
  if (nodeCount <= 2000) return 140
189
203
  return 180
190
204
  }
191
- const minFitScaleByNodeCount = nodeCount => {
192
- if (nodeCount <= 6) return 2.4
193
- if (nodeCount <= 20) return 1.8
194
- if (nodeCount <= 60) return 1.2
195
- if (nodeCount <= 180) return 0.86
196
- if (nodeCount <= 600) return 0.58
197
- if (nodeCount <= 2000) return 0.34
198
- if (nodeCount <= 6000) return 0.2
199
- return 0.13
200
- }
201
-
202
205
  const padding = paddingByNodeCount(nodes.length)
203
206
  const scaleX = width / (bounds.width + padding * 2)
204
207
  const scaleY = height / (bounds.height + padding * 2)
205
- const fitScale = clampScale(Math.min(scaleX, scaleY))
208
+ const fitScale = Math.min(scaleX, scaleY)
206
209
  const biasedScale = clampScale(fitScale * fitScaleBiasByNodeCount(nodes.length))
207
- const minimumScale = minFitScaleByNodeCount(nodes.length)
208
- const minimumLargeGraphScale = nodes.length > largeGraphNodeThreshold ? 0.13 : zoomRange.min
209
- const scale = Math.max(biasedScale, minimumScale, minimumLargeGraphScale)
210
+ const scaleRange = autoFitScaleRangeByNodeCount(nodes.length)
211
+ const scale = clampScale(Math.min(scaleRange.max, Math.max(scaleRange.min, biasedScale)))
210
212
  const centerX = (bounds.minX + bounds.maxX) / 2
211
213
  const centerY = (bounds.minY + bounds.maxY) / 2
212
214
 
@@ -328,8 +330,8 @@ const tick = delta => {
328
330
  const dy = target.y - source.y
329
331
  const distance = Math.max(Math.hypot(dx, dy), 1)
330
332
  const force = (distance - 150) * 0.002 * strength
331
- const fx = dx * force
332
- const fy = dy * force
333
+ const fx = (dx / distance) * force
334
+ const fy = (dy / distance) * force
333
335
  source.vx += fx
334
336
  source.vy += fy
335
337
  target.vx -= fx
@@ -502,6 +504,38 @@ const computeRenderVisibility = () => {
502
504
  state.renderEdges = edges
503
505
  }
504
506
 
507
+ const isNodeVisibleOnScreen = (node, width, height) => {
508
+ const radius = nodeRadius(node) * state.transform.scale
509
+ const screenX = node.x * state.transform.scale + state.transform.x
510
+ const screenY = node.y * state.transform.scale + state.transform.y
511
+
512
+ return (
513
+ screenX + radius >= 0 &&
514
+ screenX - radius <= width &&
515
+ screenY + radius >= 0 &&
516
+ screenY - radius <= height
517
+ )
518
+ }
519
+
520
+ const hasValidTransform = () =>
521
+ isFiniteNumber(state.transform.x) &&
522
+ isFiniteNumber(state.transform.y) &&
523
+ isFiniteNumber(state.transform.scale) &&
524
+ state.transform.scale > 0
525
+
526
+ const sanitizeNodePosition = node => {
527
+ if (!isFiniteNumber(node.x)) node.x = 0
528
+ if (!isFiniteNumber(node.y)) node.y = 0
529
+ if (!isFiniteNumber(node.vx)) node.vx = 0
530
+ if (!isFiniteNumber(node.vy)) node.vy = 0
531
+ }
532
+
533
+ const sanitizeGraphState = () => {
534
+ state.nodes.forEach(sanitizeNodePosition)
535
+ state.visibleNodes.forEach(sanitizeNodePosition)
536
+ state.renderNodes.forEach(sanitizeNodePosition)
537
+ }
538
+
505
539
  const render = now => {
506
540
  const delta = now - state.last
507
541
  state.last = now
@@ -513,6 +547,10 @@ const render = now => {
513
547
  const rect = canvas.getBoundingClientRect()
514
548
  const width = Math.max(rect.width, 320)
515
549
  const height = Math.max(rect.height, 320)
550
+ sanitizeGraphState()
551
+ if (!hasValidTransform()) {
552
+ resetView()
553
+ }
516
554
  ctx.clearRect(0, 0, width, height)
517
555
  if (state.nodes.length === 0) {
518
556
  ctx.fillStyle = '#99a5b5'
@@ -528,6 +566,20 @@ const render = now => {
528
566
 
529
567
  computeRenderVisibility()
530
568
  tick(delta)
569
+ const hasVisibleNodeOnScreen = state.renderNodes.some((node) => isNodeVisibleOnScreen(node, width, height))
570
+ if (!hasVisibleNodeOnScreen && state.renderNodes.length > 0) {
571
+ state.offscreenFrameCount += 1
572
+ if (state.offscreenFrameCount >= 6 && !state.recoveringViewport) {
573
+ state.recoveringViewport = true
574
+ fitView({ useFiltered: true })
575
+ state.offscreenFrameCount = 0
576
+ requestAnimationFrame(() => {
577
+ state.recoveringViewport = false
578
+ })
579
+ }
580
+ } else {
581
+ state.offscreenFrameCount = 0
582
+ }
531
583
  const drawEdges = !(state.nodes.length > largeGraphNodeThreshold && state.transform.scale < 0.22)
532
584
  if (drawEdges) {
533
585
  state.renderEdges.forEach(edge => {
@@ -695,6 +747,10 @@ const isScreenPointInsideCanvas = (screenX, screenY) => {
695
747
  }
696
748
 
697
749
  const handleWheelZoom = event => {
750
+ if (elements.contentDialog?.open) {
751
+ return
752
+ }
753
+
698
754
  if (!isScreenPointInsideCanvas(event.clientX, event.clientY)) {
699
755
  return
700
756
  }
@@ -1,9 +1,8 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
- import { existsSync } from 'node:fs';
4
- import { dirname, relative, resolve } from 'node:path';
5
- import { platform } from 'node:os';
6
- import { spawn } from 'node:child_process';
3
+ import { dirname, join, relative, resolve } from 'node:path';
4
+ import { platform, tmpdir } from 'node:os';
5
+ import { spawn, spawnSync } from 'node:child_process';
7
6
  import { addNote } from '../../application/add-note.js';
8
7
  import { buildContextPackage } from '../../application/build-context.js';
9
8
  import { importLegacySqliteDatabase } from '../../application/import-legacy-sqlite.js';
@@ -37,6 +36,199 @@ const spawnDetached = (command, args) => {
37
36
  return false;
38
37
  }
39
38
  };
39
+ const nativeGuiSwiftScriptPath = join(tmpdir(), 'brainlink-native-gui.swift');
40
+ const nativeGuiPowershellScriptPath = join(tmpdir(), 'brainlink-native-gui.ps1');
41
+ const nativeGuiLinuxScriptPath = join(tmpdir(), 'brainlink-native-gui-linux.py');
42
+ const nativeGuiSwiftScript = `import Foundation
43
+ import AppKit
44
+ import WebKit
45
+
46
+ final class BrainlinkAppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
47
+ private let targetUrl: URL
48
+ private var window: NSWindow?
49
+ private var webView: WKWebView?
50
+
51
+ init(targetUrl: URL) {
52
+ self.targetUrl = targetUrl
53
+ }
54
+
55
+ func applicationDidFinishLaunching(_ notification: Notification) {
56
+ let window = NSWindow(
57
+ contentRect: NSRect(x: 0, y: 0, width: 1320, height: 860),
58
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
59
+ backing: .buffered,
60
+ defer: false
61
+ )
62
+ window.title = "Brainlink Graph"
63
+ window.center()
64
+ window.isReleasedWhenClosed = false
65
+ window.delegate = self
66
+
67
+ let webView = WKWebView(frame: window.contentView?.bounds ?? .zero)
68
+ webView.autoresizingMask = [.width, .height]
69
+ webView.allowsBackForwardNavigationGestures = true
70
+ webView.load(URLRequest(url: targetUrl))
71
+ window.contentView?.addSubview(webView)
72
+
73
+ self.window = window
74
+ self.webView = webView
75
+
76
+ window.makeKeyAndOrderFront(nil)
77
+ NSApp.activate(ignoringOtherApps: true)
78
+ }
79
+
80
+ func windowWillClose(_ notification: Notification) {
81
+ NSApp.terminate(nil)
82
+ }
83
+ }
84
+
85
+ let rawTarget = CommandLine.arguments.dropFirst().first ?? "http://127.0.0.1:4321"
86
+
87
+ guard let targetUrl = URL(string: rawTarget) else {
88
+ fputs("Invalid URL for Brainlink GUI: \\(rawTarget)\\n", stderr)
89
+ exit(1)
90
+ }
91
+
92
+ let app = NSApplication.shared
93
+ app.setActivationPolicy(.regular)
94
+ let delegate = BrainlinkAppDelegate(targetUrl: targetUrl)
95
+ app.delegate = delegate
96
+ app.run()
97
+ `;
98
+ const nativeGuiPowershellScript = `param(
99
+ [string]$TargetUrl = "http://127.0.0.1:4321"
100
+ )
101
+
102
+ Add-Type -AssemblyName System.Windows.Forms
103
+ Add-Type -AssemblyName System.Drawing
104
+ [System.Windows.Forms.Application]::EnableVisualStyles()
105
+
106
+ $form = New-Object System.Windows.Forms.Form
107
+ $form.Text = "Brainlink Graph"
108
+ $form.Width = 1320
109
+ $form.Height = 860
110
+ $form.StartPosition = "CenterScreen"
111
+
112
+ $browser = New-Object System.Windows.Forms.WebBrowser
113
+ $browser.Dock = [System.Windows.Forms.DockStyle]::Fill
114
+ $browser.ScriptErrorsSuppressed = $true
115
+ $browser.Navigate($TargetUrl)
116
+
117
+ $form.Controls.Add($browser)
118
+ [void]$form.ShowDialog()
119
+ `;
120
+ const nativeGuiLinuxPythonScript = `#!/usr/bin/env python3
121
+ import sys
122
+
123
+ def run() -> int:
124
+ try:
125
+ import gi
126
+ gi.require_version("Gtk", "3.0")
127
+ try:
128
+ gi.require_version("WebKit2", "4.1")
129
+ except ValueError:
130
+ gi.require_version("WebKit2", "4.0")
131
+ from gi.repository import Gtk, WebKit2
132
+ except Exception:
133
+ return 1
134
+
135
+ target_url = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:4321"
136
+
137
+ window = Gtk.Window(title="Brainlink Graph")
138
+ window.set_default_size(1320, 860)
139
+ window.connect("destroy", Gtk.main_quit)
140
+
141
+ webview = WebKit2.WebView()
142
+ webview.load_uri(target_url)
143
+ window.add(webview)
144
+ window.show_all()
145
+ Gtk.main()
146
+ return 0
147
+
148
+ if __name__ == "__main__":
149
+ raise SystemExit(run())
150
+ `;
151
+ const commandExists = (command) => {
152
+ try {
153
+ const probe = platform() === 'win32'
154
+ ? spawnSync('where', [command], { stdio: 'ignore' })
155
+ : spawnSync('which', [command], { stdio: 'ignore' });
156
+ return probe.status === 0;
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ };
162
+ const resolveSwiftExecutable = () => {
163
+ const directSwift = '/usr/bin/swift';
164
+ if (existsSync(directSwift)) {
165
+ return directSwift;
166
+ }
167
+ try {
168
+ const probe = spawnSync('xcrun', ['--find', 'swift'], { encoding: 'utf8' });
169
+ const swiftPath = probe.status === 0 ? probe.stdout.trim() : '';
170
+ return swiftPath.length > 0 ? swiftPath : null;
171
+ }
172
+ catch {
173
+ return null;
174
+ }
175
+ };
176
+ const openGraphInMacNativeGui = (url) => {
177
+ const swiftBinary = resolveSwiftExecutable();
178
+ if (!swiftBinary) {
179
+ return false;
180
+ }
181
+ try {
182
+ writeFileSync(nativeGuiSwiftScriptPath, nativeGuiSwiftScript, 'utf8');
183
+ }
184
+ catch {
185
+ return false;
186
+ }
187
+ return spawnDetached(swiftBinary, [nativeGuiSwiftScriptPath, url]);
188
+ };
189
+ const resolveWindowsPowershellExecutable = () => {
190
+ if (commandExists('powershell')) {
191
+ return 'powershell';
192
+ }
193
+ if (commandExists('pwsh')) {
194
+ return 'pwsh';
195
+ }
196
+ return null;
197
+ };
198
+ const openGraphInWindowsNativeGui = (url) => {
199
+ const powershell = resolveWindowsPowershellExecutable();
200
+ if (!powershell) {
201
+ return false;
202
+ }
203
+ try {
204
+ writeFileSync(nativeGuiPowershellScriptPath, nativeGuiPowershellScript, 'utf8');
205
+ }
206
+ catch {
207
+ return false;
208
+ }
209
+ return spawnDetached(powershell, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-STA', '-File', nativeGuiPowershellScriptPath, url]);
210
+ };
211
+ const openGraphInLinuxNativeGui = (url) => {
212
+ if (!commandExists('python3')) {
213
+ return false;
214
+ }
215
+ try {
216
+ writeFileSync(nativeGuiLinuxScriptPath, nativeGuiLinuxPythonScript, 'utf8');
217
+ }
218
+ catch {
219
+ return false;
220
+ }
221
+ return spawnDetached('python3', [nativeGuiLinuxScriptPath, url]);
222
+ };
223
+ const openGraphInNativeGui = (url) => {
224
+ if (platform() === 'darwin') {
225
+ return openGraphInMacNativeGui(url);
226
+ }
227
+ if (platform() === 'win32') {
228
+ return openGraphInWindowsNativeGui(url);
229
+ }
230
+ return openGraphInLinuxNativeGui(url);
231
+ };
40
232
  const openGraphInAppWindow = (url) => {
41
233
  if (platform() === 'darwin') {
42
234
  const macCandidates = [
@@ -66,13 +258,16 @@ const openGraphInAppWindow = (url) => {
66
258
  spawnDetached('microsoft-edge', [appArgument, '--new-window']) ||
67
259
  spawnDetached('microsoft-edge-stable', [appArgument, '--new-window']));
68
260
  };
69
- const openUrlInBrowser = (url) => {
261
+ const openUrlInUi = (url) => {
70
262
  const openDisabled = process.env.BRAINLINK_NO_BROWSER === '1' ||
71
263
  process.env.BRAINLINK_NO_BROWSER === 'true' ||
72
264
  process.env.CI === 'true';
73
265
  if (openDisabled) {
74
266
  return { opened: false, mode: 'none' };
75
267
  }
268
+ if (openGraphInNativeGui(url)) {
269
+ return { opened: true, mode: 'native-gui' };
270
+ }
76
271
  if (openGraphInAppWindow(url)) {
77
272
  return { opened: true, mode: 'app-window' };
78
273
  }
@@ -278,7 +473,7 @@ export const registerWriteCommands = (program) => {
278
473
  .option('-h, --host <host>', 'server host', '127.0.0.1')
279
474
  .option('-p, --port <port>', 'server port', '4321')
280
475
  .option('--no-index', 'skip indexing before starting the server')
281
- .option('--no-open', 'do not open the graph UI in the default browser')
476
+ .option('--no-open', 'do not open the graph UI automatically')
282
477
  .option('-w, --watch', 'watch markdown files and reindex on changes')
283
478
  .option('--json', 'print machine-readable JSON')
284
479
  .description('start a local web UI for the knowledge graph')
@@ -291,7 +486,7 @@ export const registerWriteCommands = (program) => {
291
486
  shouldIndex: options.index,
292
487
  shouldWatch: Boolean(options.watch)
293
488
  });
294
- const openResult = options.open !== false ? openUrlInBrowser(server.url) : { opened: false, mode: 'none' };
489
+ const openResult = options.open !== false ? openUrlInUi(server.url) : { opened: false, mode: 'none' };
295
490
  print(options.json, {
296
491
  url: server.url,
297
492
  watch: Boolean(options.watch),
@@ -299,9 +494,11 @@ export const registerWriteCommands = (program) => {
299
494
  openedUi: openResult.opened,
300
495
  openMode: openResult.mode
301
496
  }, () => `Brainlink graph server running at ${server.url}${openResult.opened
302
- ? openResult.mode === 'app-window'
303
- ? ' (opened in dedicated app window)'
304
- : ' (opened in browser)'
497
+ ? openResult.mode === 'native-gui'
498
+ ? ' (opened in native desktop GUI)'
499
+ : openResult.mode === 'app-window'
500
+ ? ' (opened in dedicated app window)'
501
+ : ' (opened in browser)'
305
502
  : options.open === false
306
503
  ? ' (auto-open disabled)'
307
504
  : ''}`);
@@ -540,7 +540,12 @@ blink server --vault ./vault --host 127.0.0.1 --port 4321 --no-open
540
540
  ```
541
541
 
542
542
  This starts a local frontend for inspecting the knowledge graph.
543
- By default it tries to open the graph in a dedicated app window and falls back to the default browser when app-window mode is unavailable.
543
+ By default it tries to open the graph in a native desktop GUI window:
544
+ - macOS: Swift + WebKit
545
+ - Windows: PowerShell WinForms WebBrowser
546
+ - Linux: Python GTK + WebKit2 (requires `python3` + `gi` + `WebKit2`)
547
+
548
+ If native GUI launch is unavailable, it falls back to dedicated app-window mode and then to the default browser.
544
549
  Use `--no-open` to keep the server headless.
545
550
 
546
551
  Without `--vault`, the graph UI serves `$HOME/.brainlink/vault`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.33",
3
+ "version": "0.1.0-beta.34",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",