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

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
@@ -26,6 +26,8 @@
26
26
  - Added compressed-space pack prefiltering (token bloom index) before `.blpk` decryption and scan.
27
27
  - Improved graph UI auto-fit and viewport recovery so loaded nodes are re-centered when zoom/pan drifts to empty canvas.
28
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.
29
+ - Changed Linux default UI launch to app-window/browser for lighter startup; Linux native GUI is now opt-in via `BRAINLINK_LINUX_NATIVE_GUI=1`.
30
+ - Added native GUI parent-process monitoring so GUI windows close automatically when `blink server` stops.
29
31
 
30
32
  ## 0.1.0-beta.3
31
33
 
package/README.md CHANGED
@@ -556,10 +556,12 @@ By default, the server uses `$HOME/.brainlink/vault`. Pass `--vault ./vault` onl
556
556
  By default, `blink server` tries to open the graph in a native desktop GUI window:
557
557
  - macOS: Swift + WebKit
558
558
  - Windows: PowerShell WinForms WebBrowser
559
- - Linux: Python GTK + WebKit2 (requires `python3` + `gi` + `WebKit2`)
559
+ - Linux: optional Python GTK + WebKit2 (requires `python3` + `gi` + `WebKit2`)
560
560
 
561
+ On Linux, native GUI is disabled by default for better startup performance. Enable it with `BRAINLINK_LINUX_NATIVE_GUI=1`.
561
562
  If native GUI launch is unavailable on your system, it falls back to dedicated app-window mode and then to the default browser.
562
563
  Use `--no-open` to keep it headless.
564
+ When native GUI is used, the GUI window automatically closes when the `blink server` process stops.
563
565
 
564
566
  The graph UI shows:
565
567
 
@@ -850,6 +852,7 @@ blink server --vault ./vault --watch --no-open
850
852
 
851
853
  Starts the local read-only graph UI and HTTP API.
852
854
  By default, it tries to open a native desktop GUI window for the graph URL.
855
+ On Linux, native GUI is disabled by default; enable it with `BRAINLINK_LINUX_NATIVE_GUI=1`.
853
856
  If native GUI launch is unavailable, it falls back to dedicated app-window mode and then browser open.
854
857
  Use `--no-open` to skip that behavior.
855
858
 
@@ -5,6 +5,8 @@ const largeGraphEdgeRenderLimit = 16000
5
5
  const renderNodeBudget = 1800
6
6
  const minNodePixelRadius = 1.8
7
7
  const viewportPaddingPx = 280
8
+ const worldCoordinateLimit = 5_000_000
9
+ const transformCoordinateLimit = 20_000_000
8
10
  const state = {
9
11
  graph: { nodes: [], edges: [] },
10
12
  nodes: [],
@@ -133,6 +135,7 @@ const edgeWeight = edge => Number.isFinite(edge.weight) ? Math.max(1, edge.weigh
133
135
 
134
136
  const clampScale = value => Math.max(zoomRange.min, Math.min(zoomRange.max, value))
135
137
  const isFiniteNumber = value => Number.isFinite(value)
138
+ const isReasonableCoordinate = value => isFiniteNumber(value) && Math.abs(value) <= worldCoordinateLimit
136
139
 
137
140
  const graphBounds = nodes => {
138
141
  if (nodes.length === 0) return null
@@ -521,13 +524,15 @@ const hasValidTransform = () =>
521
524
  isFiniteNumber(state.transform.x) &&
522
525
  isFiniteNumber(state.transform.y) &&
523
526
  isFiniteNumber(state.transform.scale) &&
527
+ Math.abs(state.transform.x) <= transformCoordinateLimit &&
528
+ Math.abs(state.transform.y) <= transformCoordinateLimit &&
524
529
  state.transform.scale > 0
525
530
 
526
531
  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
532
+ if (!isReasonableCoordinate(node.x)) node.x = 0
533
+ if (!isReasonableCoordinate(node.y)) node.y = 0
534
+ if (!isFiniteNumber(node.vx) || Math.abs(node.vx) > worldCoordinateLimit) node.vx = 0
535
+ if (!isFiniteNumber(node.vy) || Math.abs(node.vy) > worldCoordinateLimit) node.vy = 0
531
536
  }
532
537
 
533
538
  const sanitizeGraphState = () => {
@@ -42,14 +42,18 @@ const nativeGuiLinuxScriptPath = join(tmpdir(), 'brainlink-native-gui-linux.py')
42
42
  const nativeGuiSwiftScript = `import Foundation
43
43
  import AppKit
44
44
  import WebKit
45
+ import Darwin
45
46
 
46
47
  final class BrainlinkAppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
47
48
  private let targetUrl: URL
49
+ private let parentPid: Int32
48
50
  private var window: NSWindow?
49
51
  private var webView: WKWebView?
52
+ private var monitorTimer: Timer?
50
53
 
51
- init(targetUrl: URL) {
54
+ init(targetUrl: URL, parentPid: Int32) {
52
55
  self.targetUrl = targetUrl
56
+ self.parentPid = parentPid
53
57
  }
54
58
 
55
59
  func applicationDidFinishLaunching(_ notification: Notification) {
@@ -73,16 +77,27 @@ final class BrainlinkAppDelegate: NSObject, NSApplicationDelegate, NSWindowDeleg
73
77
  self.window = window
74
78
  self.webView = webView
75
79
 
80
+ if parentPid > 0 {
81
+ monitorTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
82
+ if kill(self.parentPid, 0) != 0 {
83
+ NSApp.terminate(nil)
84
+ }
85
+ }
86
+ }
87
+
76
88
  window.makeKeyAndOrderFront(nil)
77
89
  NSApp.activate(ignoringOtherApps: true)
78
90
  }
79
91
 
80
92
  func windowWillClose(_ notification: Notification) {
93
+ monitorTimer?.invalidate()
81
94
  NSApp.terminate(nil)
82
95
  }
83
96
  }
84
97
 
85
- let rawTarget = CommandLine.arguments.dropFirst().first ?? "http://127.0.0.1:4321"
98
+ let args = Array(CommandLine.arguments.dropFirst())
99
+ let rawTarget = args.indices.contains(0) ? args[0] : "http://127.0.0.1:4321"
100
+ let parentPid: Int32 = args.indices.contains(1) ? (Int32(args[1]) ?? 0) : 0
86
101
 
87
102
  guard let targetUrl = URL(string: rawTarget) else {
88
103
  fputs("Invalid URL for Brainlink GUI: \\(rawTarget)\\n", stderr)
@@ -91,12 +106,13 @@ guard let targetUrl = URL(string: rawTarget) else {
91
106
 
92
107
  let app = NSApplication.shared
93
108
  app.setActivationPolicy(.regular)
94
- let delegate = BrainlinkAppDelegate(targetUrl: targetUrl)
109
+ let delegate = BrainlinkAppDelegate(targetUrl: targetUrl, parentPid: parentPid)
95
110
  app.delegate = delegate
96
111
  app.run()
97
112
  `;
98
113
  const nativeGuiPowershellScript = `param(
99
- [string]$TargetUrl = "http://127.0.0.1:4321"
114
+ [string]$TargetUrl = "http://127.0.0.1:4321",
115
+ [int]$ParentPid = 0
100
116
  )
101
117
 
102
118
  Add-Type -AssemblyName System.Windows.Forms
@@ -115,6 +131,23 @@ $browser.ScriptErrorsSuppressed = $true
115
131
  $browser.Navigate($TargetUrl)
116
132
 
117
133
  $form.Controls.Add($browser)
134
+ $timer = New-Object System.Windows.Forms.Timer
135
+ $timer.Interval = 1000
136
+ $timer.Add_Tick({
137
+ if ($ParentPid -le 0) {
138
+ return
139
+ }
140
+ try {
141
+ Get-Process -Id $ParentPid -ErrorAction Stop | Out-Null
142
+ } catch {
143
+ $timer.Stop()
144
+ $form.Close()
145
+ }
146
+ })
147
+ $form.Add_FormClosed({
148
+ $timer.Stop()
149
+ })
150
+ $timer.Start()
118
151
  [void]$form.ShowDialog()
119
152
  `;
120
153
  const nativeGuiLinuxPythonScript = `#!/usr/bin/env python3
@@ -128,11 +161,12 @@ def run() -> int:
128
161
  gi.require_version("WebKit2", "4.1")
129
162
  except ValueError:
130
163
  gi.require_version("WebKit2", "4.0")
131
- from gi.repository import Gtk, WebKit2
164
+ from gi.repository import Gtk, WebKit2, GLib
132
165
  except Exception:
133
166
  return 1
134
167
 
135
168
  target_url = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:4321"
169
+ parent_pid = int(sys.argv[2]) if len(sys.argv) > 2 else 0
136
170
 
137
171
  window = Gtk.Window(title="Brainlink Graph")
138
172
  window.set_default_size(1320, 860)
@@ -142,6 +176,19 @@ def run() -> int:
142
176
  webview.load_uri(target_url)
143
177
  window.add(webview)
144
178
  window.show_all()
179
+
180
+ if parent_pid > 0:
181
+ def _watch_parent() -> bool:
182
+ try:
183
+ import os
184
+ os.kill(parent_pid, 0)
185
+ except Exception:
186
+ Gtk.main_quit()
187
+ return False
188
+ return True
189
+
190
+ GLib.timeout_add(1000, _watch_parent)
191
+
145
192
  Gtk.main()
146
193
  return 0
147
194
 
@@ -159,6 +206,7 @@ const commandExists = (command) => {
159
206
  return false;
160
207
  }
161
208
  };
209
+ const envFlagEnabled = (name) => process.env[name] === '1' || process.env[name] === 'true';
162
210
  const resolveSwiftExecutable = () => {
163
211
  const directSwift = '/usr/bin/swift';
164
212
  if (existsSync(directSwift)) {
@@ -173,7 +221,7 @@ const resolveSwiftExecutable = () => {
173
221
  return null;
174
222
  }
175
223
  };
176
- const openGraphInMacNativeGui = (url) => {
224
+ const openGraphInMacNativeGui = (url, parentPid) => {
177
225
  const swiftBinary = resolveSwiftExecutable();
178
226
  if (!swiftBinary) {
179
227
  return false;
@@ -184,7 +232,7 @@ const openGraphInMacNativeGui = (url) => {
184
232
  catch {
185
233
  return false;
186
234
  }
187
- return spawnDetached(swiftBinary, [nativeGuiSwiftScriptPath, url]);
235
+ return spawnDetached(swiftBinary, [nativeGuiSwiftScriptPath, url, String(parentPid)]);
188
236
  };
189
237
  const resolveWindowsPowershellExecutable = () => {
190
238
  if (commandExists('powershell')) {
@@ -195,7 +243,7 @@ const resolveWindowsPowershellExecutable = () => {
195
243
  }
196
244
  return null;
197
245
  };
198
- const openGraphInWindowsNativeGui = (url) => {
246
+ const openGraphInWindowsNativeGui = (url, parentPid) => {
199
247
  const powershell = resolveWindowsPowershellExecutable();
200
248
  if (!powershell) {
201
249
  return false;
@@ -206,9 +254,9 @@ const openGraphInWindowsNativeGui = (url) => {
206
254
  catch {
207
255
  return false;
208
256
  }
209
- return spawnDetached(powershell, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-STA', '-File', nativeGuiPowershellScriptPath, url]);
257
+ return spawnDetached(powershell, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-STA', '-File', nativeGuiPowershellScriptPath, url, String(parentPid)]);
210
258
  };
211
- const openGraphInLinuxNativeGui = (url) => {
259
+ const openGraphInLinuxNativeGui = (url, parentPid) => {
212
260
  if (!commandExists('python3')) {
213
261
  return false;
214
262
  }
@@ -218,16 +266,16 @@ const openGraphInLinuxNativeGui = (url) => {
218
266
  catch {
219
267
  return false;
220
268
  }
221
- return spawnDetached('python3', [nativeGuiLinuxScriptPath, url]);
269
+ return spawnDetached('python3', [nativeGuiLinuxScriptPath, url, String(parentPid)]);
222
270
  };
223
- const openGraphInNativeGui = (url) => {
271
+ const openGraphInNativeGui = (url, parentPid) => {
224
272
  if (platform() === 'darwin') {
225
- return openGraphInMacNativeGui(url);
273
+ return openGraphInMacNativeGui(url, parentPid);
226
274
  }
227
275
  if (platform() === 'win32') {
228
- return openGraphInWindowsNativeGui(url);
276
+ return openGraphInWindowsNativeGui(url, parentPid);
229
277
  }
230
- return openGraphInLinuxNativeGui(url);
278
+ return openGraphInLinuxNativeGui(url, parentPid);
231
279
  };
232
280
  const openGraphInAppWindow = (url) => {
233
281
  if (platform() === 'darwin') {
@@ -258,14 +306,17 @@ const openGraphInAppWindow = (url) => {
258
306
  spawnDetached('microsoft-edge', [appArgument, '--new-window']) ||
259
307
  spawnDetached('microsoft-edge-stable', [appArgument, '--new-window']));
260
308
  };
261
- const openUrlInUi = (url) => {
309
+ const openUrlInUi = (url, parentPid) => {
262
310
  const openDisabled = process.env.BRAINLINK_NO_BROWSER === '1' ||
263
311
  process.env.BRAINLINK_NO_BROWSER === 'true' ||
264
312
  process.env.CI === 'true';
265
313
  if (openDisabled) {
266
314
  return { opened: false, mode: 'none' };
267
315
  }
268
- if (openGraphInNativeGui(url)) {
316
+ const currentPlatform = platform();
317
+ const nativeGuiEnabled = !envFlagEnabled('BRAINLINK_NO_NATIVE_GUI') &&
318
+ (currentPlatform !== 'linux' || envFlagEnabled('BRAINLINK_LINUX_NATIVE_GUI') || envFlagEnabled('BRAINLINK_FORCE_NATIVE_GUI'));
319
+ if (nativeGuiEnabled && openGraphInNativeGui(url, parentPid)) {
269
320
  return { opened: true, mode: 'native-gui' };
270
321
  }
271
322
  if (openGraphInAppWindow(url)) {
@@ -486,7 +537,7 @@ export const registerWriteCommands = (program) => {
486
537
  shouldIndex: options.index,
487
538
  shouldWatch: Boolean(options.watch)
488
539
  });
489
- const openResult = options.open !== false ? openUrlInUi(server.url) : { opened: false, mode: 'none' };
540
+ const openResult = options.open !== false ? openUrlInUi(server.url, process.pid) : { opened: false, mode: 'none' };
490
541
  print(options.json, {
491
542
  url: server.url,
492
543
  watch: Boolean(options.watch),
@@ -543,10 +543,12 @@ This starts a local frontend for inspecting the knowledge graph.
543
543
  By default it tries to open the graph in a native desktop GUI window:
544
544
  - macOS: Swift + WebKit
545
545
  - Windows: PowerShell WinForms WebBrowser
546
- - Linux: Python GTK + WebKit2 (requires `python3` + `gi` + `WebKit2`)
546
+ - Linux: optional Python GTK + WebKit2 (requires `python3` + `gi` + `WebKit2`)
547
547
 
548
+ On Linux, native GUI is disabled by default for better startup performance. Enable it with `BRAINLINK_LINUX_NATIVE_GUI=1`.
548
549
  If native GUI launch is unavailable, it falls back to dedicated app-window mode and then to the default browser.
549
550
  Use `--no-open` to keep the server headless.
551
+ When native GUI is active, the GUI window closes automatically when the `blink server` process stops.
550
552
 
551
553
  Without `--vault`, the graph UI serves `$HOME/.brainlink/vault`.
552
554
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.34",
3
+ "version": "0.1.0-beta.36",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",