@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
|
|
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: 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
|
|
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(
|
|
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;
|
|
@@ -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
|
|
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.
|
|
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 =
|
|
208
|
+
const fitScale = Math.min(scaleX, scaleY)
|
|
206
209
|
const biasedScale = clampScale(fitScale * fitScaleBiasByNodeCount(nodes.length))
|
|
207
|
-
const
|
|
208
|
-
const
|
|
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 {
|
|
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,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
|
|
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
|
|
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 ?
|
|
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 === '
|
|
303
|
-
? ' (opened in
|
|
304
|
-
:
|
|
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
|
: ''}`);
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -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
|
|
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