@andespindola/brainlink 1.0.5 → 1.0.6
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/README.md +8 -0
- package/dist/application/add-note.js +2 -2
- package/dist/application/build-context.js +16 -10
- package/dist/application/canonical-context-links.js +44 -5
- package/dist/application/check-package-update.js +105 -0
- package/dist/application/frontend/client/chunk-fetch.js +236 -0
- package/dist/application/frontend/client/controls.js +178 -0
- package/dist/application/frontend/client/elements.js +122 -0
- package/dist/application/frontend/client/input.js +202 -0
- package/dist/application/frontend/client/node-details.js +191 -0
- package/dist/application/frontend/client/rendering.js +296 -0
- package/dist/application/frontend/client/scope-theme.js +114 -0
- package/dist/application/frontend/client/spatial.js +98 -0
- package/dist/application/frontend/client/storage.js +215 -0
- package/dist/application/frontend/client/upload.js +90 -0
- package/dist/application/frontend/client/worker-bootstrap.js +147 -0
- package/dist/application/frontend/client-js.js +24 -1837
- package/dist/application/frontend/client-render-worker-js.js +1 -1
- package/dist/application/index-vault-phases.js +189 -0
- package/dist/application/index-vault.js +44 -165
- package/dist/cli/commands/write/dedupe-commands.js +59 -0
- package/dist/cli/commands/write/index-commands.js +205 -0
- package/dist/cli/commands/write/link-commands.js +68 -0
- package/dist/cli/commands/write/note-commands.js +146 -0
- package/dist/cli/commands/write/server-commands.js +553 -0
- package/dist/cli/commands/write/shared.js +35 -0
- package/dist/cli/commands/write/vault-lifecycle-commands.js +270 -0
- package/dist/cli/commands/write-commands.js +12 -1303
- package/dist/cli/main.js +39 -3
- package/dist/domain/context.js +39 -3
- package/dist/domain/embeddings.js +31 -5
- package/dist/domain/graph-contexts.js +62 -57
- package/dist/domain/graph-layout/cauliflower-layout.js +116 -0
- package/dist/domain/graph-layout/collisions.js +100 -0
- package/dist/domain/graph-layout/hierarchy.js +135 -0
- package/dist/domain/graph-layout/metrics.js +111 -0
- package/dist/domain/graph-layout/segments.js +76 -0
- package/dist/domain/graph-layout/star-layout.js +110 -0
- package/dist/domain/graph-layout.js +4 -625
- package/dist/infrastructure/config.js +6 -0
- package/dist/infrastructure/file-index.js +13 -4
- package/dist/infrastructure/semantic-prefilter.js +24 -0
- package/dist/mcp/server.js +7 -0
- package/dist/mcp/tool-guard.js +29 -0
- package/dist/mcp/tools/maintenance-tools.js +409 -0
- package/dist/mcp/tools/read-tools.js +504 -0
- package/dist/mcp/tools/shared.js +216 -0
- package/dist/mcp/tools/write-tools.js +247 -0
- package/dist/mcp/tools.js +3 -1357
- package/docs/QUICKSTART.md +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { platform, tmpdir } from 'node:os';
|
|
4
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
5
|
+
import { startServer } from '../../../application/start-server.js';
|
|
6
|
+
import { isBucketVaultPath } from '../../../infrastructure/file-system-vault.js';
|
|
7
|
+
import { startRemoteMcpServer } from '../../../mcp/http-server.js';
|
|
8
|
+
import { parsePositiveInteger, print, resolveOptions } from '../../runtime.js';
|
|
9
|
+
const spawnDetached = (command, args, envOverrides) => {
|
|
10
|
+
try {
|
|
11
|
+
const child = spawn(command, args, {
|
|
12
|
+
detached: true,
|
|
13
|
+
stdio: 'ignore',
|
|
14
|
+
env: envOverrides ? { ...process.env, ...envOverrides } : process.env
|
|
15
|
+
});
|
|
16
|
+
child.unref();
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const nativeGuiSwiftScriptPath = join(tmpdir(), 'brainlink-native-gui.swift');
|
|
24
|
+
const nativeGuiPowershellScriptPath = join(tmpdir(), 'brainlink-native-gui.ps1');
|
|
25
|
+
const nativeGuiLinuxScriptPath = join(tmpdir(), 'brainlink-native-gui-linux.py');
|
|
26
|
+
const nativeGuiSwiftScript = `import Foundation
|
|
27
|
+
import AppKit
|
|
28
|
+
import WebKit
|
|
29
|
+
import Darwin
|
|
30
|
+
|
|
31
|
+
final class BrainlinkAppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|
32
|
+
private let targetUrl: URL
|
|
33
|
+
private let parentPid: Int32
|
|
34
|
+
private var window: NSWindow?
|
|
35
|
+
private var webView: WKWebView?
|
|
36
|
+
private var monitorTimer: Timer?
|
|
37
|
+
|
|
38
|
+
init(targetUrl: URL, parentPid: Int32) {
|
|
39
|
+
self.targetUrl = targetUrl
|
|
40
|
+
self.parentPid = parentPid
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
44
|
+
let window = NSWindow(
|
|
45
|
+
contentRect: NSRect(x: 0, y: 0, width: 1320, height: 860),
|
|
46
|
+
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
|
47
|
+
backing: .buffered,
|
|
48
|
+
defer: false
|
|
49
|
+
)
|
|
50
|
+
window.title = "Brainlink Graph"
|
|
51
|
+
window.center()
|
|
52
|
+
window.isReleasedWhenClosed = false
|
|
53
|
+
window.delegate = self
|
|
54
|
+
|
|
55
|
+
let webView = WKWebView(frame: window.contentView?.bounds ?? .zero)
|
|
56
|
+
webView.autoresizingMask = [.width, .height]
|
|
57
|
+
webView.allowsBackForwardNavigationGestures = true
|
|
58
|
+
webView.load(URLRequest(url: targetUrl))
|
|
59
|
+
window.contentView?.addSubview(webView)
|
|
60
|
+
|
|
61
|
+
self.window = window
|
|
62
|
+
self.webView = webView
|
|
63
|
+
|
|
64
|
+
if parentPid > 0 {
|
|
65
|
+
monitorTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
|
66
|
+
if kill(self.parentPid, 0) != 0 {
|
|
67
|
+
NSApp.terminate(nil)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
window.makeKeyAndOrderFront(nil)
|
|
73
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func windowWillClose(_ notification: Notification) {
|
|
77
|
+
monitorTimer?.invalidate()
|
|
78
|
+
NSApp.terminate(nil)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let args = Array(CommandLine.arguments.dropFirst())
|
|
83
|
+
let rawTarget = args.indices.contains(0) ? args[0] : "http://127.0.0.1:4321"
|
|
84
|
+
let parentPid: Int32 = args.indices.contains(1) ? (Int32(args[1]) ?? 0) : 0
|
|
85
|
+
|
|
86
|
+
guard let targetUrl = URL(string: rawTarget) else {
|
|
87
|
+
fputs("Invalid URL for Brainlink GUI: \\(rawTarget)\\n", stderr)
|
|
88
|
+
exit(1)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let app = NSApplication.shared
|
|
92
|
+
app.setActivationPolicy(.regular)
|
|
93
|
+
let delegate = BrainlinkAppDelegate(targetUrl: targetUrl, parentPid: parentPid)
|
|
94
|
+
app.delegate = delegate
|
|
95
|
+
app.run()
|
|
96
|
+
`;
|
|
97
|
+
const nativeGuiPowershellScript = `param(
|
|
98
|
+
[string]$TargetUrl = "http://127.0.0.1:4321",
|
|
99
|
+
[int]$ParentPid = 0
|
|
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
|
+
$timer = New-Object System.Windows.Forms.Timer
|
|
119
|
+
$timer.Interval = 1000
|
|
120
|
+
$timer.Add_Tick({
|
|
121
|
+
if ($ParentPid -le 0) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
Get-Process -Id $ParentPid -ErrorAction Stop | Out-Null
|
|
126
|
+
} catch {
|
|
127
|
+
$timer.Stop()
|
|
128
|
+
$form.Close()
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
$form.Add_FormClosed({
|
|
132
|
+
$timer.Stop()
|
|
133
|
+
})
|
|
134
|
+
$timer.Start()
|
|
135
|
+
[void]$form.ShowDialog()
|
|
136
|
+
`;
|
|
137
|
+
const nativeGuiLinuxPythonScript = `#!/usr/bin/env python3
|
|
138
|
+
import sys
|
|
139
|
+
|
|
140
|
+
def run() -> int:
|
|
141
|
+
try:
|
|
142
|
+
import gi
|
|
143
|
+
gi.require_version("Gtk", "3.0")
|
|
144
|
+
try:
|
|
145
|
+
gi.require_version("WebKit2", "4.1")
|
|
146
|
+
except ValueError:
|
|
147
|
+
gi.require_version("WebKit2", "4.0")
|
|
148
|
+
from gi.repository import Gtk, WebKit2, GLib
|
|
149
|
+
except Exception:
|
|
150
|
+
return 1
|
|
151
|
+
|
|
152
|
+
target_url = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:4321"
|
|
153
|
+
parent_pid = int(sys.argv[2]) if len(sys.argv) > 2 else 0
|
|
154
|
+
|
|
155
|
+
window = Gtk.Window(title="Brainlink Graph")
|
|
156
|
+
window.set_default_size(1320, 860)
|
|
157
|
+
window.connect("destroy", Gtk.main_quit)
|
|
158
|
+
|
|
159
|
+
webview = WebKit2.WebView()
|
|
160
|
+
webview.load_uri(target_url)
|
|
161
|
+
window.add(webview)
|
|
162
|
+
window.show_all()
|
|
163
|
+
|
|
164
|
+
if parent_pid > 0:
|
|
165
|
+
def _watch_parent() -> bool:
|
|
166
|
+
try:
|
|
167
|
+
import os
|
|
168
|
+
os.kill(parent_pid, 0)
|
|
169
|
+
except Exception:
|
|
170
|
+
Gtk.main_quit()
|
|
171
|
+
return False
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
GLib.timeout_add(1000, _watch_parent)
|
|
175
|
+
|
|
176
|
+
Gtk.main()
|
|
177
|
+
return 0
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
raise SystemExit(run())
|
|
181
|
+
`;
|
|
182
|
+
const commandExists = (command) => {
|
|
183
|
+
try {
|
|
184
|
+
const probe = platform() === 'win32'
|
|
185
|
+
? spawnSync('where', [command], { stdio: 'ignore' })
|
|
186
|
+
: spawnSync('which', [command], { stdio: 'ignore' });
|
|
187
|
+
return probe.status === 0;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const readLinuxDefaultBrowserDesktopEntry = () => {
|
|
194
|
+
try {
|
|
195
|
+
const preferred = spawnSync('xdg-settings', ['get', 'default-web-browser'], { encoding: 'utf8' });
|
|
196
|
+
const rawPreferred = preferred.status === 0 ? preferred.stdout.trim() : '';
|
|
197
|
+
if (rawPreferred.length > 0) {
|
|
198
|
+
return rawPreferred;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// fallback below
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const fallback = spawnSync('xdg-mime', ['query', 'default', 'x-scheme-handler/https'], { encoding: 'utf8' });
|
|
206
|
+
const rawFallback = fallback.status === 0 ? fallback.stdout.trim() : '';
|
|
207
|
+
return rawFallback.length > 0 ? rawFallback : null;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const toLinuxDefaultBrowserCommands = (desktopEntry) => {
|
|
214
|
+
if (!desktopEntry) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
const normalized = desktopEntry.toLowerCase().trim();
|
|
218
|
+
if (normalized.includes('firefox')) {
|
|
219
|
+
return ['firefox'];
|
|
220
|
+
}
|
|
221
|
+
if (normalized.includes('edge')) {
|
|
222
|
+
return ['microsoft-edge', 'microsoft-edge-stable'];
|
|
223
|
+
}
|
|
224
|
+
if (normalized.includes('brave')) {
|
|
225
|
+
return ['brave-browser'];
|
|
226
|
+
}
|
|
227
|
+
if (normalized.includes('chromium')) {
|
|
228
|
+
return ['chromium', 'chromium-browser'];
|
|
229
|
+
}
|
|
230
|
+
if (normalized.includes('chrome')) {
|
|
231
|
+
return ['google-chrome', 'google-chrome-stable'];
|
|
232
|
+
}
|
|
233
|
+
return [];
|
|
234
|
+
};
|
|
235
|
+
const readBrowserEnvCommands = () => {
|
|
236
|
+
const value = process.env.BROWSER?.trim();
|
|
237
|
+
if (!value) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
return value
|
|
241
|
+
.split(':')
|
|
242
|
+
.map((entry) => entry.trim().split(/\s+/)[0] ?? '')
|
|
243
|
+
.map((entry) => entry.trim())
|
|
244
|
+
.filter((entry) => entry.length > 0);
|
|
245
|
+
};
|
|
246
|
+
const prioritizeLinuxBrowserCandidates = (candidates) => {
|
|
247
|
+
const preferredCommands = toLinuxDefaultBrowserCommands(readLinuxDefaultBrowserDesktopEntry());
|
|
248
|
+
if (preferredCommands.length === 0) {
|
|
249
|
+
return candidates;
|
|
250
|
+
}
|
|
251
|
+
const priorityMap = new Map(preferredCommands.map((command, index) => [command, index]));
|
|
252
|
+
return [...candidates].sort((left, right) => {
|
|
253
|
+
const leftPriority = priorityMap.get(left[0]);
|
|
254
|
+
const rightPriority = priorityMap.get(right[0]);
|
|
255
|
+
const leftScore = leftPriority == null ? Number.POSITIVE_INFINITY : leftPriority;
|
|
256
|
+
const rightScore = rightPriority == null ? Number.POSITIVE_INFINITY : rightPriority;
|
|
257
|
+
return leftScore - rightScore;
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
const envFlagEnabled = (name) => process.env[name] === '1' || process.env[name] === 'true';
|
|
261
|
+
const spawnAnyDetached = (candidates) => candidates.some(([command, args]) => spawnDetached(command, args));
|
|
262
|
+
const spawnAnyDetachedWithEnv = (candidates) => candidates.some(([command, args, env]) => spawnDetached(command, args, env));
|
|
263
|
+
const windowsStartCandidates = (program, args = []) => [
|
|
264
|
+
['cmd', ['/c', 'start', '', program, ...args]]
|
|
265
|
+
];
|
|
266
|
+
const resolveSwiftExecutable = () => {
|
|
267
|
+
const directSwift = '/usr/bin/swift';
|
|
268
|
+
if (existsSync(directSwift)) {
|
|
269
|
+
return directSwift;
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const probe = spawnSync('xcrun', ['--find', 'swift'], { encoding: 'utf8' });
|
|
273
|
+
const swiftPath = probe.status === 0 ? probe.stdout.trim() : '';
|
|
274
|
+
return swiftPath.length > 0 ? swiftPath : null;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const openGraphInMacNativeGui = (url, parentPid) => {
|
|
281
|
+
const swiftBinary = resolveSwiftExecutable();
|
|
282
|
+
if (!swiftBinary) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
writeFileSync(nativeGuiSwiftScriptPath, nativeGuiSwiftScript, 'utf8');
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
return spawnDetached(swiftBinary, [nativeGuiSwiftScriptPath, url, String(parentPid)]);
|
|
292
|
+
};
|
|
293
|
+
const resolveWindowsPowershellExecutable = () => {
|
|
294
|
+
if (commandExists('powershell')) {
|
|
295
|
+
return 'powershell';
|
|
296
|
+
}
|
|
297
|
+
if (commandExists('pwsh')) {
|
|
298
|
+
return 'pwsh';
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
};
|
|
302
|
+
const openGraphInWindowsNativeGui = (url, parentPid) => {
|
|
303
|
+
const powershell = resolveWindowsPowershellExecutable();
|
|
304
|
+
if (!powershell) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
writeFileSync(nativeGuiPowershellScriptPath, nativeGuiPowershellScript, 'utf8');
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
return spawnDetached(powershell, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-STA', '-File', nativeGuiPowershellScriptPath, url, String(parentPid)]);
|
|
314
|
+
};
|
|
315
|
+
const openGraphInLinuxNativeGui = (url, parentPid) => {
|
|
316
|
+
if (!commandExists('python3')) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
writeFileSync(nativeGuiLinuxScriptPath, nativeGuiLinuxPythonScript, 'utf8');
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
return spawnDetached('python3', [nativeGuiLinuxScriptPath, url, String(parentPid)]);
|
|
326
|
+
};
|
|
327
|
+
const openGraphInNativeGui = (url, parentPid) => {
|
|
328
|
+
if (platform() === 'darwin') {
|
|
329
|
+
return openGraphInMacNativeGui(url, parentPid);
|
|
330
|
+
}
|
|
331
|
+
if (platform() === 'win32') {
|
|
332
|
+
return openGraphInWindowsNativeGui(url, parentPid);
|
|
333
|
+
}
|
|
334
|
+
return openGraphInLinuxNativeGui(url, parentPid);
|
|
335
|
+
};
|
|
336
|
+
const openGraphInAppWindow = (url) => {
|
|
337
|
+
if (platform() === 'darwin') {
|
|
338
|
+
const macCandidates = [
|
|
339
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
340
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
341
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'
|
|
342
|
+
]
|
|
343
|
+
.filter((candidate) => existsSync(candidate))
|
|
344
|
+
.map((binary) => ({ binary, args: [`--app=${url}`, '--new-window'] }));
|
|
345
|
+
for (const candidate of macCandidates) {
|
|
346
|
+
if (spawnDetached(candidate.binary, candidate.args)) {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
if (platform() === 'win32') {
|
|
353
|
+
const appArgument = `--app=${url}`;
|
|
354
|
+
return spawnAnyDetached([
|
|
355
|
+
...windowsStartCandidates('msedge', [appArgument, '--new-window']),
|
|
356
|
+
...windowsStartCandidates('chrome', [appArgument, '--new-window']),
|
|
357
|
+
...windowsStartCandidates('chromium', [appArgument, '--new-window']),
|
|
358
|
+
...windowsStartCandidates('brave', [appArgument, '--new-window'])
|
|
359
|
+
]);
|
|
360
|
+
}
|
|
361
|
+
const appArgument = `--app=${url}`;
|
|
362
|
+
const linuxAppWindowEnabled = envFlagEnabled('BRAINLINK_LINUX_APP_WINDOW');
|
|
363
|
+
if (!linuxAppWindowEnabled) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
const linuxChromiumStableFlags = [
|
|
367
|
+
'--ozone-platform=x11',
|
|
368
|
+
'--ozone-platform-hint=x11',
|
|
369
|
+
'--disable-gpu',
|
|
370
|
+
'--disable-vulkan',
|
|
371
|
+
'--use-gl=swiftshader',
|
|
372
|
+
'--disable-features=Vulkan,VaapiVideoDecoder',
|
|
373
|
+
'--disable-background-networking'
|
|
374
|
+
];
|
|
375
|
+
const linuxChromiumEnv = {
|
|
376
|
+
GDK_BACKEND: 'x11',
|
|
377
|
+
OZONE_PLATFORM: 'x11'
|
|
378
|
+
};
|
|
379
|
+
const linuxAppWindowCandidates = [
|
|
380
|
+
'microsoft-edge',
|
|
381
|
+
'microsoft-edge-stable',
|
|
382
|
+
'google-chrome',
|
|
383
|
+
'google-chrome-stable',
|
|
384
|
+
'chromium',
|
|
385
|
+
'chromium-browser',
|
|
386
|
+
'brave-browser'
|
|
387
|
+
].filter((candidate) => commandExists(candidate));
|
|
388
|
+
return spawnAnyDetachedWithEnv(linuxAppWindowCandidates.map((command) => [
|
|
389
|
+
command,
|
|
390
|
+
[...linuxChromiumStableFlags, appArgument, '--new-window'],
|
|
391
|
+
linuxChromiumEnv
|
|
392
|
+
]));
|
|
393
|
+
};
|
|
394
|
+
const openGraphInDetectedBrowser = (url) => {
|
|
395
|
+
if (platform() === 'win32') {
|
|
396
|
+
return spawnAnyDetached([
|
|
397
|
+
...windowsStartCandidates('msedge', [url]),
|
|
398
|
+
...windowsStartCandidates('chrome', [url]),
|
|
399
|
+
...windowsStartCandidates('firefox', ['-new-window', url]),
|
|
400
|
+
...windowsStartCandidates('chromium', [url]),
|
|
401
|
+
...windowsStartCandidates('brave', [url])
|
|
402
|
+
]);
|
|
403
|
+
}
|
|
404
|
+
const linuxChromiumStableFlags = [
|
|
405
|
+
'--ozone-platform=x11',
|
|
406
|
+
'--ozone-platform-hint=x11',
|
|
407
|
+
'--disable-gpu',
|
|
408
|
+
'--disable-vulkan',
|
|
409
|
+
'--use-gl=swiftshader',
|
|
410
|
+
'--disable-features=Vulkan,VaapiVideoDecoder',
|
|
411
|
+
'--disable-background-networking'
|
|
412
|
+
];
|
|
413
|
+
const linuxChromiumEnv = {
|
|
414
|
+
GDK_BACKEND: 'x11',
|
|
415
|
+
OZONE_PLATFORM: 'x11'
|
|
416
|
+
};
|
|
417
|
+
const envBrowserCandidates = readBrowserEnvCommands()
|
|
418
|
+
.map((command) => command.includes('firefox')
|
|
419
|
+
? [command, ['-new-window', url], undefined]
|
|
420
|
+
: [command, [url], undefined])
|
|
421
|
+
.filter(([command]) => commandExists(command));
|
|
422
|
+
if (envBrowserCandidates.length > 0 && spawnAnyDetachedWithEnv(envBrowserCandidates)) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
const linuxBrowserCandidates = [
|
|
426
|
+
['firefox', ['-new-window', url], undefined],
|
|
427
|
+
['microsoft-edge', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
|
|
428
|
+
['microsoft-edge-stable', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
|
|
429
|
+
['google-chrome', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
|
|
430
|
+
['google-chrome-stable', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
|
|
431
|
+
['brave-browser', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
|
|
432
|
+
['chromium', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
|
|
433
|
+
['chromium-browser', [...linuxChromiumStableFlags, url], linuxChromiumEnv]
|
|
434
|
+
];
|
|
435
|
+
const available = prioritizeLinuxBrowserCandidates(linuxBrowserCandidates.filter(([command]) => commandExists(command)));
|
|
436
|
+
return spawnAnyDetachedWithEnv(available);
|
|
437
|
+
};
|
|
438
|
+
const openUrlInUi = (url, parentPid) => {
|
|
439
|
+
const openDisabled = process.env.BRAINLINK_NO_BROWSER === '1' ||
|
|
440
|
+
process.env.BRAINLINK_NO_BROWSER === 'true' ||
|
|
441
|
+
process.env.CI === 'true';
|
|
442
|
+
if (openDisabled) {
|
|
443
|
+
return { opened: false, mode: 'none' };
|
|
444
|
+
}
|
|
445
|
+
const currentPlatform = platform();
|
|
446
|
+
const nativeGuiEnabled = !envFlagEnabled('BRAINLINK_NO_NATIVE_GUI') &&
|
|
447
|
+
(currentPlatform !== 'linux' || envFlagEnabled('BRAINLINK_LINUX_NATIVE_GUI') || envFlagEnabled('BRAINLINK_FORCE_NATIVE_GUI'));
|
|
448
|
+
if (nativeGuiEnabled && openGraphInNativeGui(url, parentPid)) {
|
|
449
|
+
return { opened: true, mode: 'native-gui' };
|
|
450
|
+
}
|
|
451
|
+
if (platform() === 'linux') {
|
|
452
|
+
if (spawnDetached('xdg-open', [url])) {
|
|
453
|
+
return { opened: true, mode: 'browser' };
|
|
454
|
+
}
|
|
455
|
+
if (openGraphInDetectedBrowser(url)) {
|
|
456
|
+
return { opened: true, mode: 'browser' };
|
|
457
|
+
}
|
|
458
|
+
if (openGraphInAppWindow(url)) {
|
|
459
|
+
return { opened: true, mode: 'app-window' };
|
|
460
|
+
}
|
|
461
|
+
return { opened: false, mode: 'none' };
|
|
462
|
+
}
|
|
463
|
+
if (openGraphInAppWindow(url)) {
|
|
464
|
+
return { opened: true, mode: 'app-window' };
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
if (platform() === 'darwin') {
|
|
468
|
+
return { opened: spawnDetached('open', [url]), mode: 'browser' };
|
|
469
|
+
}
|
|
470
|
+
if (openGraphInDetectedBrowser(url)) {
|
|
471
|
+
return { opened: true, mode: 'browser' };
|
|
472
|
+
}
|
|
473
|
+
if (platform() === 'win32') {
|
|
474
|
+
return { opened: spawnDetached('cmd', ['/c', 'start', '', url]), mode: 'browser' };
|
|
475
|
+
}
|
|
476
|
+
return { opened: spawnDetached('xdg-open', [url]), mode: 'browser' };
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return { opened: false, mode: 'none' };
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
export const registerServerCommands = (program) => {
|
|
483
|
+
program
|
|
484
|
+
.command('server')
|
|
485
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
486
|
+
.option('-h, --host <host>', 'server host', '127.0.0.1')
|
|
487
|
+
.option('-p, --port <port>', 'server port', '4321')
|
|
488
|
+
.option('--no-index', 'skip indexing before starting the server')
|
|
489
|
+
.option('--no-open', 'do not open the graph UI automatically')
|
|
490
|
+
.option('-w, --watch', 'watch markdown files and reindex on changes', true)
|
|
491
|
+
.option('--no-watch', 'disable markdown file watching')
|
|
492
|
+
.option('--json', 'print machine-readable JSON')
|
|
493
|
+
.description('start a local web UI for the knowledge graph')
|
|
494
|
+
.action(async (options) => {
|
|
495
|
+
const resolved = await resolveOptions(options);
|
|
496
|
+
const shouldWatch = options.watch !== false && !isBucketVaultPath(resolved.vault);
|
|
497
|
+
const server = await startServer({
|
|
498
|
+
vaultPath: resolved.vault,
|
|
499
|
+
host: options.host ?? resolved.config.host,
|
|
500
|
+
port: parsePositiveInteger(options.port ?? String(resolved.config.port), resolved.config.port),
|
|
501
|
+
shouldIndex: options.index,
|
|
502
|
+
shouldWatch
|
|
503
|
+
});
|
|
504
|
+
const openResult = options.open !== false ? openUrlInUi(server.url, process.pid) : { opened: false, mode: 'none' };
|
|
505
|
+
print(options.json, {
|
|
506
|
+
url: server.url,
|
|
507
|
+
watch: shouldWatch,
|
|
508
|
+
readonly: true,
|
|
509
|
+
openedUi: openResult.opened,
|
|
510
|
+
openMode: openResult.mode
|
|
511
|
+
}, () => `Brainlink graph server running at ${server.url} (${shouldWatch ? 'watching for changes' : 'watch disabled'})${openResult.opened
|
|
512
|
+
? openResult.mode === 'native-gui'
|
|
513
|
+
? ' (opened in native desktop GUI)'
|
|
514
|
+
: openResult.mode === 'app-window'
|
|
515
|
+
? ' (opened in dedicated app window)'
|
|
516
|
+
: ' (opened in browser)'
|
|
517
|
+
: options.open === false
|
|
518
|
+
? ' (auto-open disabled)'
|
|
519
|
+
: ''}`);
|
|
520
|
+
});
|
|
521
|
+
program
|
|
522
|
+
.command('mcp-server')
|
|
523
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
524
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
525
|
+
.option('-h, --host <host>', 'remote MCP server host', '0.0.0.0')
|
|
526
|
+
.option('-p, --port <port>', 'remote MCP server port', '3333')
|
|
527
|
+
.option('--path <path>', 'remote MCP endpoint path', '/mcp')
|
|
528
|
+
.option('--token <token>', 'bearer token required for MCP requests')
|
|
529
|
+
.option('--no-index', 'skip indexing before starting the MCP server')
|
|
530
|
+
.option('--json', 'print machine-readable JSON')
|
|
531
|
+
.description('start a remote MCP server for centralized cluster access')
|
|
532
|
+
.action(async (options) => {
|
|
533
|
+
const resolved = await resolveOptions(options);
|
|
534
|
+
const token = options.token ?? process.env.BRAINLINK_MCP_TOKEN;
|
|
535
|
+
const server = await startRemoteMcpServer({
|
|
536
|
+
vaultPath: resolved.vault,
|
|
537
|
+
agent: resolved.agent,
|
|
538
|
+
host: options.host ?? '0.0.0.0',
|
|
539
|
+
port: parsePositiveInteger(options.port ?? '3333', 3333),
|
|
540
|
+
path: options.path ?? '/mcp',
|
|
541
|
+
token,
|
|
542
|
+
shouldIndex: options.index
|
|
543
|
+
});
|
|
544
|
+
print(options.json, {
|
|
545
|
+
url: server.url,
|
|
546
|
+
healthUrl: server.healthUrl,
|
|
547
|
+
readyUrl: server.readyUrl,
|
|
548
|
+
vault: resolved.vault,
|
|
549
|
+
agent: resolved.agent ?? '*',
|
|
550
|
+
auth: token === undefined ? 'disabled' : 'bearer'
|
|
551
|
+
}, () => `Brainlink remote MCP server running at ${server.url} (health: ${server.healthUrl}, readiness: ${server.readyUrl})`);
|
|
552
|
+
});
|
|
553
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
export const resolveAddContent = (options) => {
|
|
3
|
+
if (options.content != null && options.content.trim().length > 0) {
|
|
4
|
+
return options.content;
|
|
5
|
+
}
|
|
6
|
+
if (options.contentFile == null || options.contentFile.trim().length === 0) {
|
|
7
|
+
throw new Error('Use --content or --content-file to provide note content.');
|
|
8
|
+
}
|
|
9
|
+
return readFileSync(options.contentFile, 'utf8');
|
|
10
|
+
};
|
|
11
|
+
export const parseScore = (value, fallback) => {
|
|
12
|
+
if (value == null) {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
const parsed = Number.parseFloat(value);
|
|
16
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
17
|
+
throw new Error(`Invalid score value: ${value}. Expected a number between 0 and 1.`);
|
|
18
|
+
}
|
|
19
|
+
return parsed;
|
|
20
|
+
};
|
|
21
|
+
export const formatBytes = (bytes) => {
|
|
22
|
+
if (!Number.isFinite(bytes) || bytes == null) {
|
|
23
|
+
return 'n/a';
|
|
24
|
+
}
|
|
25
|
+
if (bytes < 1024)
|
|
26
|
+
return `${bytes} B`;
|
|
27
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
28
|
+
let value = bytes / 1024;
|
|
29
|
+
let unitIndex = 0;
|
|
30
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
31
|
+
value /= 1024;
|
|
32
|
+
unitIndex += 1;
|
|
33
|
+
}
|
|
34
|
+
return `${value.toFixed(value >= 10 ? 1 : 2)} ${units[unitIndex]}`;
|
|
35
|
+
};
|