@andespindola/brainlink 0.1.0-beta.33 → 0.1.0-beta.35
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,9 @@
|
|
|
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.
|
|
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`.
|
|
27
30
|
|
|
28
31
|
## 0.1.0-beta.3
|
|
29
32
|
|
package/README.md
CHANGED
|
@@ -553,8 +553,14 @@ 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
|
|
557
|
-
|
|
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: optional Python GTK + WebKit2 (requires `python3` + `gi` + `WebKit2`)
|
|
560
|
+
|
|
561
|
+
On Linux, native GUI is disabled by default for better startup performance. Enable it with `BRAINLINK_LINUX_NATIVE_GUI=1`.
|
|
562
|
+
If native GUI launch is unavailable on your system, it falls back to dedicated app-window mode and then to the default browser.
|
|
563
|
+
Use `--no-open` to keep it headless.
|
|
558
564
|
|
|
559
565
|
The graph UI shows:
|
|
560
566
|
|
|
@@ -844,7 +850,9 @@ blink server --vault ./vault --watch --no-open
|
|
|
844
850
|
```
|
|
845
851
|
|
|
846
852
|
Starts the local read-only graph UI and HTTP API.
|
|
847
|
-
By default, it tries to open a
|
|
853
|
+
By default, it tries to open a native desktop GUI window for the graph URL.
|
|
854
|
+
On Linux, native GUI is disabled by default; enable it with `BRAINLINK_LINUX_NATIVE_GUI=1`.
|
|
855
|
+
If native GUI launch is unavailable, it falls back to dedicated app-window mode and then browser open.
|
|
848
856
|
Use `--no-open` to skip that behavior.
|
|
849
857
|
|
|
850
858
|
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(
|
|
249
|
-
max-height: calc(100svh -
|
|
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 -
|
|
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:
|
|
342
|
+
max-height: 220px;
|
|
343
343
|
overflow: auto;
|
|
344
344
|
align-content: flex-start;
|
|
345
345
|
padding-right: 4px;
|
|
@@ -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: [],
|
|
@@ -26,7 +28,9 @@ const state = {
|
|
|
26
28
|
cursor: { x: 0, y: 0, inCanvas: false },
|
|
27
29
|
graphSignature: '',
|
|
28
30
|
graphStatus: '',
|
|
29
|
-
last: performance.now()
|
|
31
|
+
last: performance.now(),
|
|
32
|
+
offscreenFrameCount: 0,
|
|
33
|
+
recoveringViewport: false
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
const byId = id => document.getElementById(id)
|
|
@@ -130,6 +134,8 @@ const recomputeVisibility = () => {
|
|
|
130
134
|
const edgeWeight = edge => Number.isFinite(edge.weight) ? Math.max(1, edge.weight) : 1
|
|
131
135
|
|
|
132
136
|
const clampScale = value => Math.max(zoomRange.min, Math.min(zoomRange.max, value))
|
|
137
|
+
const isFiniteNumber = value => Number.isFinite(value)
|
|
138
|
+
const isReasonableCoordinate = value => isFiniteNumber(value) && Math.abs(value) <= worldCoordinateLimit
|
|
133
139
|
|
|
134
140
|
const graphBounds = nodes => {
|
|
135
141
|
if (nodes.length === 0) return null
|
|
@@ -157,14 +163,25 @@ const graphBounds = nodes => {
|
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
const fitScaleBiasByNodeCount = nodeCount => {
|
|
160
|
-
if (nodeCount <= 6) return
|
|
161
|
-
if (nodeCount <= 20) return
|
|
162
|
-
if (nodeCount <= 60) return 1.
|
|
163
|
-
if (nodeCount <= 180) return 1
|
|
164
|
-
if (nodeCount <= 600) return
|
|
165
|
-
if (nodeCount <= 2000) return 0.
|
|
166
|
-
if (nodeCount <= 6000) return 0.
|
|
167
|
-
return 0.
|
|
166
|
+
if (nodeCount <= 6) return 1.22
|
|
167
|
+
if (nodeCount <= 20) return 1.12
|
|
168
|
+
if (nodeCount <= 60) return 1.04
|
|
169
|
+
if (nodeCount <= 180) return 1
|
|
170
|
+
if (nodeCount <= 600) return 0.94
|
|
171
|
+
if (nodeCount <= 2000) return 0.82
|
|
172
|
+
if (nodeCount <= 6000) return 0.68
|
|
173
|
+
return 0.56
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const autoFitScaleRangeByNodeCount = nodeCount => {
|
|
177
|
+
if (nodeCount <= 6) return { min: 0.4, max: 2.2 }
|
|
178
|
+
if (nodeCount <= 20) return { min: 0.34, max: 1.65 }
|
|
179
|
+
if (nodeCount <= 60) return { min: 0.25, max: 1.22 }
|
|
180
|
+
if (nodeCount <= 180) return { min: 0.18, max: 0.92 }
|
|
181
|
+
if (nodeCount <= 600) return { min: 0.12, max: 0.72 }
|
|
182
|
+
if (nodeCount <= 2000) return { min: 0.08, max: 0.52 }
|
|
183
|
+
if (nodeCount <= 6000) return { min: 0.06, max: 0.32 }
|
|
184
|
+
return { min: zoomRange.min, max: 0.24 }
|
|
168
185
|
}
|
|
169
186
|
|
|
170
187
|
const fitView = (options = { useFiltered: true }) => {
|
|
@@ -188,25 +205,13 @@ const fitView = (options = { useFiltered: true }) => {
|
|
|
188
205
|
if (nodeCount <= 2000) return 140
|
|
189
206
|
return 180
|
|
190
207
|
}
|
|
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
208
|
const padding = paddingByNodeCount(nodes.length)
|
|
203
209
|
const scaleX = width / (bounds.width + padding * 2)
|
|
204
210
|
const scaleY = height / (bounds.height + padding * 2)
|
|
205
|
-
const fitScale =
|
|
211
|
+
const fitScale = Math.min(scaleX, scaleY)
|
|
206
212
|
const biasedScale = clampScale(fitScale * fitScaleBiasByNodeCount(nodes.length))
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
const scale = Math.max(biasedScale, minimumScale, minimumLargeGraphScale)
|
|
213
|
+
const scaleRange = autoFitScaleRangeByNodeCount(nodes.length)
|
|
214
|
+
const scale = clampScale(Math.min(scaleRange.max, Math.max(scaleRange.min, biasedScale)))
|
|
210
215
|
const centerX = (bounds.minX + bounds.maxX) / 2
|
|
211
216
|
const centerY = (bounds.minY + bounds.maxY) / 2
|
|
212
217
|
|
|
@@ -328,8 +333,8 @@ const tick = delta => {
|
|
|
328
333
|
const dy = target.y - source.y
|
|
329
334
|
const distance = Math.max(Math.hypot(dx, dy), 1)
|
|
330
335
|
const force = (distance - 150) * 0.002 * strength
|
|
331
|
-
const fx = dx * force
|
|
332
|
-
const fy = dy * force
|
|
336
|
+
const fx = (dx / distance) * force
|
|
337
|
+
const fy = (dy / distance) * force
|
|
333
338
|
source.vx += fx
|
|
334
339
|
source.vy += fy
|
|
335
340
|
target.vx -= fx
|
|
@@ -502,6 +507,40 @@ const computeRenderVisibility = () => {
|
|
|
502
507
|
state.renderEdges = edges
|
|
503
508
|
}
|
|
504
509
|
|
|
510
|
+
const isNodeVisibleOnScreen = (node, width, height) => {
|
|
511
|
+
const radius = nodeRadius(node) * state.transform.scale
|
|
512
|
+
const screenX = node.x * state.transform.scale + state.transform.x
|
|
513
|
+
const screenY = node.y * state.transform.scale + state.transform.y
|
|
514
|
+
|
|
515
|
+
return (
|
|
516
|
+
screenX + radius >= 0 &&
|
|
517
|
+
screenX - radius <= width &&
|
|
518
|
+
screenY + radius >= 0 &&
|
|
519
|
+
screenY - radius <= height
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const hasValidTransform = () =>
|
|
524
|
+
isFiniteNumber(state.transform.x) &&
|
|
525
|
+
isFiniteNumber(state.transform.y) &&
|
|
526
|
+
isFiniteNumber(state.transform.scale) &&
|
|
527
|
+
Math.abs(state.transform.x) <= transformCoordinateLimit &&
|
|
528
|
+
Math.abs(state.transform.y) <= transformCoordinateLimit &&
|
|
529
|
+
state.transform.scale > 0
|
|
530
|
+
|
|
531
|
+
const sanitizeNodePosition = node => {
|
|
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
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const sanitizeGraphState = () => {
|
|
539
|
+
state.nodes.forEach(sanitizeNodePosition)
|
|
540
|
+
state.visibleNodes.forEach(sanitizeNodePosition)
|
|
541
|
+
state.renderNodes.forEach(sanitizeNodePosition)
|
|
542
|
+
}
|
|
543
|
+
|
|
505
544
|
const render = now => {
|
|
506
545
|
const delta = now - state.last
|
|
507
546
|
state.last = now
|
|
@@ -513,6 +552,10 @@ const render = now => {
|
|
|
513
552
|
const rect = canvas.getBoundingClientRect()
|
|
514
553
|
const width = Math.max(rect.width, 320)
|
|
515
554
|
const height = Math.max(rect.height, 320)
|
|
555
|
+
sanitizeGraphState()
|
|
556
|
+
if (!hasValidTransform()) {
|
|
557
|
+
resetView()
|
|
558
|
+
}
|
|
516
559
|
ctx.clearRect(0, 0, width, height)
|
|
517
560
|
if (state.nodes.length === 0) {
|
|
518
561
|
ctx.fillStyle = '#99a5b5'
|
|
@@ -528,6 +571,20 @@ const render = now => {
|
|
|
528
571
|
|
|
529
572
|
computeRenderVisibility()
|
|
530
573
|
tick(delta)
|
|
574
|
+
const hasVisibleNodeOnScreen = state.renderNodes.some((node) => isNodeVisibleOnScreen(node, width, height))
|
|
575
|
+
if (!hasVisibleNodeOnScreen && state.renderNodes.length > 0) {
|
|
576
|
+
state.offscreenFrameCount += 1
|
|
577
|
+
if (state.offscreenFrameCount >= 6 && !state.recoveringViewport) {
|
|
578
|
+
state.recoveringViewport = true
|
|
579
|
+
fitView({ useFiltered: true })
|
|
580
|
+
state.offscreenFrameCount = 0
|
|
581
|
+
requestAnimationFrame(() => {
|
|
582
|
+
state.recoveringViewport = false
|
|
583
|
+
})
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
state.offscreenFrameCount = 0
|
|
587
|
+
}
|
|
531
588
|
const drawEdges = !(state.nodes.length > largeGraphNodeThreshold && state.transform.scale < 0.22)
|
|
532
589
|
if (drawEdges) {
|
|
533
590
|
state.renderEdges.forEach(edge => {
|
|
@@ -695,6 +752,10 @@ const isScreenPointInsideCanvas = (screenX, screenY) => {
|
|
|
695
752
|
}
|
|
696
753
|
|
|
697
754
|
const handleWheelZoom = event => {
|
|
755
|
+
if (elements.contentDialog?.open) {
|
|
756
|
+
return
|
|
757
|
+
}
|
|
758
|
+
|
|
698
759
|
if (!isScreenPointInsideCanvas(event.clientX, event.clientY)) {
|
|
699
760
|
return
|
|
700
761
|
}
|
|
@@ -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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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,200 @@ 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 envFlagEnabled = (name) => process.env[name] === '1' || process.env[name] === 'true';
|
|
163
|
+
const resolveSwiftExecutable = () => {
|
|
164
|
+
const directSwift = '/usr/bin/swift';
|
|
165
|
+
if (existsSync(directSwift)) {
|
|
166
|
+
return directSwift;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const probe = spawnSync('xcrun', ['--find', 'swift'], { encoding: 'utf8' });
|
|
170
|
+
const swiftPath = probe.status === 0 ? probe.stdout.trim() : '';
|
|
171
|
+
return swiftPath.length > 0 ? swiftPath : null;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const openGraphInMacNativeGui = (url) => {
|
|
178
|
+
const swiftBinary = resolveSwiftExecutable();
|
|
179
|
+
if (!swiftBinary) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
writeFileSync(nativeGuiSwiftScriptPath, nativeGuiSwiftScript, 'utf8');
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return spawnDetached(swiftBinary, [nativeGuiSwiftScriptPath, url]);
|
|
189
|
+
};
|
|
190
|
+
const resolveWindowsPowershellExecutable = () => {
|
|
191
|
+
if (commandExists('powershell')) {
|
|
192
|
+
return 'powershell';
|
|
193
|
+
}
|
|
194
|
+
if (commandExists('pwsh')) {
|
|
195
|
+
return 'pwsh';
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
};
|
|
199
|
+
const openGraphInWindowsNativeGui = (url) => {
|
|
200
|
+
const powershell = resolveWindowsPowershellExecutable();
|
|
201
|
+
if (!powershell) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
writeFileSync(nativeGuiPowershellScriptPath, nativeGuiPowershellScript, 'utf8');
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return spawnDetached(powershell, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-STA', '-File', nativeGuiPowershellScriptPath, url]);
|
|
211
|
+
};
|
|
212
|
+
const openGraphInLinuxNativeGui = (url) => {
|
|
213
|
+
if (!commandExists('python3')) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
writeFileSync(nativeGuiLinuxScriptPath, nativeGuiLinuxPythonScript, 'utf8');
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
return spawnDetached('python3', [nativeGuiLinuxScriptPath, url]);
|
|
223
|
+
};
|
|
224
|
+
const openGraphInNativeGui = (url) => {
|
|
225
|
+
if (platform() === 'darwin') {
|
|
226
|
+
return openGraphInMacNativeGui(url);
|
|
227
|
+
}
|
|
228
|
+
if (platform() === 'win32') {
|
|
229
|
+
return openGraphInWindowsNativeGui(url);
|
|
230
|
+
}
|
|
231
|
+
return openGraphInLinuxNativeGui(url);
|
|
232
|
+
};
|
|
40
233
|
const openGraphInAppWindow = (url) => {
|
|
41
234
|
if (platform() === 'darwin') {
|
|
42
235
|
const macCandidates = [
|
|
@@ -66,13 +259,19 @@ const openGraphInAppWindow = (url) => {
|
|
|
66
259
|
spawnDetached('microsoft-edge', [appArgument, '--new-window']) ||
|
|
67
260
|
spawnDetached('microsoft-edge-stable', [appArgument, '--new-window']));
|
|
68
261
|
};
|
|
69
|
-
const
|
|
262
|
+
const openUrlInUi = (url) => {
|
|
70
263
|
const openDisabled = process.env.BRAINLINK_NO_BROWSER === '1' ||
|
|
71
264
|
process.env.BRAINLINK_NO_BROWSER === 'true' ||
|
|
72
265
|
process.env.CI === 'true';
|
|
73
266
|
if (openDisabled) {
|
|
74
267
|
return { opened: false, mode: 'none' };
|
|
75
268
|
}
|
|
269
|
+
const currentPlatform = platform();
|
|
270
|
+
const nativeGuiEnabled = !envFlagEnabled('BRAINLINK_NO_NATIVE_GUI') &&
|
|
271
|
+
(currentPlatform !== 'linux' || envFlagEnabled('BRAINLINK_LINUX_NATIVE_GUI') || envFlagEnabled('BRAINLINK_FORCE_NATIVE_GUI'));
|
|
272
|
+
if (nativeGuiEnabled && openGraphInNativeGui(url)) {
|
|
273
|
+
return { opened: true, mode: 'native-gui' };
|
|
274
|
+
}
|
|
76
275
|
if (openGraphInAppWindow(url)) {
|
|
77
276
|
return { opened: true, mode: 'app-window' };
|
|
78
277
|
}
|
|
@@ -278,7 +477,7 @@ export const registerWriteCommands = (program) => {
|
|
|
278
477
|
.option('-h, --host <host>', 'server host', '127.0.0.1')
|
|
279
478
|
.option('-p, --port <port>', 'server port', '4321')
|
|
280
479
|
.option('--no-index', 'skip indexing before starting the server')
|
|
281
|
-
.option('--no-open', 'do not open the graph UI
|
|
480
|
+
.option('--no-open', 'do not open the graph UI automatically')
|
|
282
481
|
.option('-w, --watch', 'watch markdown files and reindex on changes')
|
|
283
482
|
.option('--json', 'print machine-readable JSON')
|
|
284
483
|
.description('start a local web UI for the knowledge graph')
|
|
@@ -291,7 +490,7 @@ export const registerWriteCommands = (program) => {
|
|
|
291
490
|
shouldIndex: options.index,
|
|
292
491
|
shouldWatch: Boolean(options.watch)
|
|
293
492
|
});
|
|
294
|
-
const openResult = options.open !== false ?
|
|
493
|
+
const openResult = options.open !== false ? openUrlInUi(server.url) : { opened: false, mode: 'none' };
|
|
295
494
|
print(options.json, {
|
|
296
495
|
url: server.url,
|
|
297
496
|
watch: Boolean(options.watch),
|
|
@@ -299,9 +498,11 @@ export const registerWriteCommands = (program) => {
|
|
|
299
498
|
openedUi: openResult.opened,
|
|
300
499
|
openMode: openResult.mode
|
|
301
500
|
}, () => `Brainlink graph server running at ${server.url}${openResult.opened
|
|
302
|
-
? openResult.mode === '
|
|
303
|
-
? ' (opened in
|
|
304
|
-
:
|
|
501
|
+
? openResult.mode === 'native-gui'
|
|
502
|
+
? ' (opened in native desktop GUI)'
|
|
503
|
+
: openResult.mode === 'app-window'
|
|
504
|
+
? ' (opened in dedicated app window)'
|
|
505
|
+
: ' (opened in browser)'
|
|
305
506
|
: options.open === false
|
|
306
507
|
? ' (auto-open disabled)'
|
|
307
508
|
: ''}`);
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -540,7 +540,13 @@ 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
|
|
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: optional Python GTK + WebKit2 (requires `python3` + `gi` + `WebKit2`)
|
|
547
|
+
|
|
548
|
+
On Linux, native GUI is disabled by default for better startup performance. Enable it with `BRAINLINK_LINUX_NATIVE_GUI=1`.
|
|
549
|
+
If native GUI launch is unavailable, it falls back to dedicated app-window mode and then to the default browser.
|
|
544
550
|
Use `--no-open` to keep the server headless.
|
|
545
551
|
|
|
546
552
|
Without `--vault`, the graph UI serves `$HOME/.brainlink/vault`.
|
package/package.json
CHANGED