@andespindola/brainlink 0.1.0-beta.15 → 0.1.0-beta.151

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.
Files changed (43) hide show
  1. package/AGENTS.md +3 -0
  2. package/CHANGELOG.md +27 -0
  3. package/COPYRIGHT.md +5 -0
  4. package/README.md +140 -9
  5. package/dist/application/auto-migrate-configured-vault.js +37 -0
  6. package/dist/application/build-context.js +64 -3
  7. package/dist/application/dedupe-notes.js +226 -0
  8. package/dist/application/frontend/client-css.js +111 -47
  9. package/dist/application/frontend/client-html.js +42 -26
  10. package/dist/application/frontend/client-js.js +788 -554
  11. package/dist/application/frontend/client-render-worker-js.js +569 -0
  12. package/dist/application/frontend/client-worker-js.js +66 -0
  13. package/dist/application/get-graph-layout.js +38 -5
  14. package/dist/application/get-graph-stream-chunk.js +289 -0
  15. package/dist/application/get-graph-view.js +243 -0
  16. package/dist/application/import-legacy-sqlite.js +296 -0
  17. package/dist/application/index-vault.js +262 -23
  18. package/dist/application/offline-pack-backup.js +44 -0
  19. package/dist/application/server/routes.js +187 -5
  20. package/dist/application/start-server.js +75 -4
  21. package/dist/application/watch-vault.js +23 -2
  22. package/dist/cli/commands/agent-commands.js +7 -0
  23. package/dist/cli/commands/write-commands.js +849 -10
  24. package/dist/cli/runtime.js +10 -2
  25. package/dist/domain/context.js +54 -11
  26. package/dist/domain/graph-layout.js +275 -3
  27. package/dist/domain/markdown.js +22 -9
  28. package/dist/domain/middle-out.js +18 -0
  29. package/dist/infrastructure/config.js +117 -4
  30. package/dist/infrastructure/file-index.js +70 -3
  31. package/dist/infrastructure/file-system-vault.js +15 -0
  32. package/dist/infrastructure/index-state.js +58 -0
  33. package/dist/infrastructure/private-pack-codec.js +71 -10
  34. package/dist/infrastructure/search-packs.js +286 -15
  35. package/dist/infrastructure/vault-migration-state.js +69 -0
  36. package/dist/infrastructure/volatile-memory.js +100 -0
  37. package/dist/mcp/runtime.js +20 -0
  38. package/dist/mcp/server.js +29 -11
  39. package/dist/mcp/tools.js +119 -2
  40. package/docs/AGENT_USAGE.md +89 -3
  41. package/docs/ARCHITECTURE.md +6 -0
  42. package/docs/QUICKSTART.md +7 -0
  43. package/package.json +7 -2
@@ -1,10 +1,15 @@
1
- import { readFileSync } from 'node:fs';
2
- import { mkdir, writeFile } from 'node:fs/promises';
3
- import { dirname, relative, resolve } from 'node:path';
4
- import { addNote } from '../../application/add-note.js';
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, relative, resolve } from 'node:path';
4
+ import { platform, tmpdir } from 'node:os';
5
+ import { spawn, spawnSync } from 'node:child_process';
6
+ import { addNoteWithMetadata } from '../../application/add-note.js';
5
7
  import { buildContextPackage } from '../../application/build-context.js';
6
- import { indexVault } from '../../application/index-vault.js';
8
+ import { resolveDuplicateNotes, scanDuplicateNotes } from '../../application/dedupe-notes.js';
9
+ import { importLegacySqliteDatabase } from '../../application/import-legacy-sqlite.js';
10
+ import { indexVault, indexVaultWithOptions } from '../../application/index-vault.js';
7
11
  import { migrateVaultContent, planVaultMigration, previewVaultMigration, shouldMigrateDefaultVault } from '../../application/migrate-vault.js';
12
+ import { createOfflinePackBackup } from '../../application/offline-pack-backup.js';
8
13
  import { startServer } from '../../application/start-server.js';
9
14
  import { startVaultWatcher } from '../../application/watch-vault.js';
10
15
  import { doctorVault, getStats, validateVault } from '../../application/analyze-vault.js';
@@ -12,6 +17,7 @@ import { defaultBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure
12
17
  import { loadBrainlinkConfig } from '../../infrastructure/config.js';
13
18
  import { assertVaultAllowed, ensureVault } from '../../infrastructure/file-system-vault.js';
14
19
  import { getBootstrapPolicy, getBootstrapSessionStatus, touchBootstrapSession } from '../../infrastructure/session-state.js';
20
+ import { addVolatileMemory, clearVolatileMemory } from '../../infrastructure/volatile-memory.js';
15
21
  import { installAgentIntegration } from './agent-commands.js';
16
22
  import { parsePositiveInteger, print, resolveOptions } from '../runtime.js';
17
23
  const resolveAddContent = (options) => {
@@ -23,6 +29,600 @@ const resolveAddContent = (options) => {
23
29
  }
24
30
  return readFileSync(options.contentFile, 'utf8');
25
31
  };
32
+ const parseScore = (value, fallback) => {
33
+ if (value == null) {
34
+ return fallback;
35
+ }
36
+ const parsed = Number.parseFloat(value);
37
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
38
+ throw new Error(`Invalid score value: ${value}. Expected a number between 0 and 1.`);
39
+ }
40
+ return parsed;
41
+ };
42
+ const formatBytes = (bytes) => {
43
+ if (!Number.isFinite(bytes) || bytes == null) {
44
+ return 'n/a';
45
+ }
46
+ if (bytes < 1024)
47
+ return `${bytes} B`;
48
+ const units = ['KB', 'MB', 'GB', 'TB'];
49
+ let value = bytes / 1024;
50
+ let unitIndex = 0;
51
+ while (value >= 1024 && unitIndex < units.length - 1) {
52
+ value /= 1024;
53
+ unitIndex += 1;
54
+ }
55
+ return `${value.toFixed(value >= 10 ? 1 : 2)} ${units[unitIndex]}`;
56
+ };
57
+ const formatMs = (value) => Number.isFinite(value) && value != null ? `${value.toFixed(value >= 100 ? 0 : 1)}ms` : 'n/a';
58
+ const benchEventLabel = (event) => `${event.phase}:${event.status}`;
59
+ const printBenchRealtimeEvent = (json, event) => {
60
+ print(json, {
61
+ event: 'bench-progress',
62
+ ...event
63
+ }, () => `[bench] ${benchEventLabel(event)} ${event.message} (${formatMs(event.elapsedMs)})`);
64
+ };
65
+ const printBenchSummary = (json, trigger, vault, result) => {
66
+ print(json, {
67
+ event: 'bench-result',
68
+ trigger,
69
+ vault,
70
+ result
71
+ }, () => {
72
+ const packs = result.packs;
73
+ const compression = packs?.compression;
74
+ const savedPercent = compression && compression.inputBytes > 0
75
+ ? `${((1 - compression.ratio) * 100).toFixed(1)}%`
76
+ : 'n/a';
77
+ return [
78
+ `[bench] trigger=${trigger}`,
79
+ `documents=${result.documentCount} chunks=${result.chunkCount} links=${result.linkCount}`,
80
+ `changedDocuments=${result.changedDocumentCount ?? 0} totalElapsed=${formatMs(result.elapsedMs)}`,
81
+ `packsRebuilt=${packs?.rebuilt ? 'yes' : 'no'} reason=${packs?.reason ?? 'n/a'}`,
82
+ packs?.rebuilt
83
+ ? `packCount=${packs.packCount ?? 0} packDuration=${formatMs(packs.durationMs)} input=${formatBytes(compression?.inputBytes)} output=${formatBytes(compression?.outputBytes)} saved=${savedPercent}`
84
+ : 'packCompression=n/a'
85
+ ].join('\n');
86
+ });
87
+ };
88
+ const benchHistoryPath = (vaultPath) => join(vaultPath, '.brainlink', 'benchmarks', 'latest.json');
89
+ const readBenchHistory = async (vaultPath) => {
90
+ try {
91
+ const parsed = JSON.parse(await readFile(benchHistoryPath(vaultPath), 'utf8'));
92
+ if (typeof parsed.elapsedMs !== 'number' || typeof parsed.timestamp !== 'string') {
93
+ return null;
94
+ }
95
+ return {
96
+ elapsedMs: parsed.elapsedMs,
97
+ timestamp: parsed.timestamp,
98
+ ...(typeof parsed.compressionRatio === 'number' ? { compressionRatio: parsed.compressionRatio } : {})
99
+ };
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ };
105
+ const writeBenchHistory = async (vaultPath, result) => {
106
+ await mkdir(dirname(benchHistoryPath(vaultPath)), { recursive: true });
107
+ const payload = {
108
+ elapsedMs: result.elapsedMs ?? 0,
109
+ timestamp: new Date().toISOString(),
110
+ ...(typeof result.packs?.compression?.ratio === 'number' ? { compressionRatio: result.packs.compression.ratio } : {})
111
+ };
112
+ await writeFile(benchHistoryPath(vaultPath), `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
113
+ };
114
+ const evaluateBenchGuardrails = (config, result, baseline) => {
115
+ const compressionRatio = result.packs?.compression?.ratio;
116
+ const compressionSavingsPercent = typeof compressionRatio === 'number' ? Math.max(0, (1 - compressionRatio) * 100) : undefined;
117
+ const compressionPass = compressionSavingsPercent != null
118
+ ? compressionSavingsPercent >= config.searchPack.guardrailMinSavingsPercent
119
+ : undefined;
120
+ const latencyRegressionPercent = baseline && baseline.elapsedMs > 0 && typeof result.elapsedMs === 'number'
121
+ ? ((result.elapsedMs - baseline.elapsedMs) / baseline.elapsedMs) * 100
122
+ : undefined;
123
+ const latencyPass = latencyRegressionPercent != null
124
+ ? latencyRegressionPercent <= config.searchPack.guardrailMaxLatencyRegressionPercent
125
+ : undefined;
126
+ return {
127
+ ...(compressionSavingsPercent != null ? { compressionSavingsPercent } : {}),
128
+ ...(compressionPass != null ? { compressionPass } : {}),
129
+ ...(latencyRegressionPercent != null ? { latencyRegressionPercent } : {}),
130
+ ...(latencyPass != null ? { latencyPass } : {})
131
+ };
132
+ };
133
+ const printBenchGuardrails = (json, vault, config, guardrails) => {
134
+ print(json, {
135
+ event: 'bench-guardrails',
136
+ vault,
137
+ thresholds: {
138
+ minSavingsPercent: config.searchPack.guardrailMinSavingsPercent,
139
+ maxLatencyRegressionPercent: config.searchPack.guardrailMaxLatencyRegressionPercent
140
+ },
141
+ guardrails
142
+ }, () => {
143
+ const savings = guardrails.compressionSavingsPercent;
144
+ const latency = guardrails.latencyRegressionPercent;
145
+ return [
146
+ '[bench] guardrails',
147
+ `minSavings=${config.searchPack.guardrailMinSavingsPercent.toFixed(1)}% maxLatencyRegression=${config.searchPack.guardrailMaxLatencyRegressionPercent.toFixed(1)}%`,
148
+ `compressionSavings=${savings != null ? `${savings.toFixed(2)}%` : 'n/a'} pass=${guardrails.compressionPass != null ? (guardrails.compressionPass ? 'yes' : 'no') : 'n/a'}`,
149
+ `latencyRegression=${latency != null ? `${latency.toFixed(2)}%` : 'n/a'} pass=${guardrails.latencyPass != null ? (guardrails.latencyPass ? 'yes' : 'no') : 'n/a'}`
150
+ ].join('\n');
151
+ });
152
+ };
153
+ const spawnDetached = (command, args, envOverrides) => {
154
+ try {
155
+ const child = spawn(command, args, {
156
+ detached: true,
157
+ stdio: 'ignore',
158
+ env: envOverrides ? { ...process.env, ...envOverrides } : process.env
159
+ });
160
+ child.unref();
161
+ return true;
162
+ }
163
+ catch {
164
+ return false;
165
+ }
166
+ };
167
+ const nativeGuiSwiftScriptPath = join(tmpdir(), 'brainlink-native-gui.swift');
168
+ const nativeGuiPowershellScriptPath = join(tmpdir(), 'brainlink-native-gui.ps1');
169
+ const nativeGuiLinuxScriptPath = join(tmpdir(), 'brainlink-native-gui-linux.py');
170
+ const nativeGuiSwiftScript = `import Foundation
171
+ import AppKit
172
+ import WebKit
173
+ import Darwin
174
+
175
+ final class BrainlinkAppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
176
+ private let targetUrl: URL
177
+ private let parentPid: Int32
178
+ private var window: NSWindow?
179
+ private var webView: WKWebView?
180
+ private var monitorTimer: Timer?
181
+
182
+ init(targetUrl: URL, parentPid: Int32) {
183
+ self.targetUrl = targetUrl
184
+ self.parentPid = parentPid
185
+ }
186
+
187
+ func applicationDidFinishLaunching(_ notification: Notification) {
188
+ let window = NSWindow(
189
+ contentRect: NSRect(x: 0, y: 0, width: 1320, height: 860),
190
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
191
+ backing: .buffered,
192
+ defer: false
193
+ )
194
+ window.title = "Brainlink Graph"
195
+ window.center()
196
+ window.isReleasedWhenClosed = false
197
+ window.delegate = self
198
+
199
+ let webView = WKWebView(frame: window.contentView?.bounds ?? .zero)
200
+ webView.autoresizingMask = [.width, .height]
201
+ webView.allowsBackForwardNavigationGestures = true
202
+ webView.load(URLRequest(url: targetUrl))
203
+ window.contentView?.addSubview(webView)
204
+
205
+ self.window = window
206
+ self.webView = webView
207
+
208
+ if parentPid > 0 {
209
+ monitorTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
210
+ if kill(self.parentPid, 0) != 0 {
211
+ NSApp.terminate(nil)
212
+ }
213
+ }
214
+ }
215
+
216
+ window.makeKeyAndOrderFront(nil)
217
+ NSApp.activate(ignoringOtherApps: true)
218
+ }
219
+
220
+ func windowWillClose(_ notification: Notification) {
221
+ monitorTimer?.invalidate()
222
+ NSApp.terminate(nil)
223
+ }
224
+ }
225
+
226
+ let args = Array(CommandLine.arguments.dropFirst())
227
+ let rawTarget = args.indices.contains(0) ? args[0] : "http://127.0.0.1:4321"
228
+ let parentPid: Int32 = args.indices.contains(1) ? (Int32(args[1]) ?? 0) : 0
229
+
230
+ guard let targetUrl = URL(string: rawTarget) else {
231
+ fputs("Invalid URL for Brainlink GUI: \\(rawTarget)\\n", stderr)
232
+ exit(1)
233
+ }
234
+
235
+ let app = NSApplication.shared
236
+ app.setActivationPolicy(.regular)
237
+ let delegate = BrainlinkAppDelegate(targetUrl: targetUrl, parentPid: parentPid)
238
+ app.delegate = delegate
239
+ app.run()
240
+ `;
241
+ const nativeGuiPowershellScript = `param(
242
+ [string]$TargetUrl = "http://127.0.0.1:4321",
243
+ [int]$ParentPid = 0
244
+ )
245
+
246
+ Add-Type -AssemblyName System.Windows.Forms
247
+ Add-Type -AssemblyName System.Drawing
248
+ [System.Windows.Forms.Application]::EnableVisualStyles()
249
+
250
+ $form = New-Object System.Windows.Forms.Form
251
+ $form.Text = "Brainlink Graph"
252
+ $form.Width = 1320
253
+ $form.Height = 860
254
+ $form.StartPosition = "CenterScreen"
255
+
256
+ $browser = New-Object System.Windows.Forms.WebBrowser
257
+ $browser.Dock = [System.Windows.Forms.DockStyle]::Fill
258
+ $browser.ScriptErrorsSuppressed = $true
259
+ $browser.Navigate($TargetUrl)
260
+
261
+ $form.Controls.Add($browser)
262
+ $timer = New-Object System.Windows.Forms.Timer
263
+ $timer.Interval = 1000
264
+ $timer.Add_Tick({
265
+ if ($ParentPid -le 0) {
266
+ return
267
+ }
268
+ try {
269
+ Get-Process -Id $ParentPid -ErrorAction Stop | Out-Null
270
+ } catch {
271
+ $timer.Stop()
272
+ $form.Close()
273
+ }
274
+ })
275
+ $form.Add_FormClosed({
276
+ $timer.Stop()
277
+ })
278
+ $timer.Start()
279
+ [void]$form.ShowDialog()
280
+ `;
281
+ const nativeGuiLinuxPythonScript = `#!/usr/bin/env python3
282
+ import sys
283
+
284
+ def run() -> int:
285
+ try:
286
+ import gi
287
+ gi.require_version("Gtk", "3.0")
288
+ try:
289
+ gi.require_version("WebKit2", "4.1")
290
+ except ValueError:
291
+ gi.require_version("WebKit2", "4.0")
292
+ from gi.repository import Gtk, WebKit2, GLib
293
+ except Exception:
294
+ return 1
295
+
296
+ target_url = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:4321"
297
+ parent_pid = int(sys.argv[2]) if len(sys.argv) > 2 else 0
298
+
299
+ window = Gtk.Window(title="Brainlink Graph")
300
+ window.set_default_size(1320, 860)
301
+ window.connect("destroy", Gtk.main_quit)
302
+
303
+ webview = WebKit2.WebView()
304
+ webview.load_uri(target_url)
305
+ window.add(webview)
306
+ window.show_all()
307
+
308
+ if parent_pid > 0:
309
+ def _watch_parent() -> bool:
310
+ try:
311
+ import os
312
+ os.kill(parent_pid, 0)
313
+ except Exception:
314
+ Gtk.main_quit()
315
+ return False
316
+ return True
317
+
318
+ GLib.timeout_add(1000, _watch_parent)
319
+
320
+ Gtk.main()
321
+ return 0
322
+
323
+ if __name__ == "__main__":
324
+ raise SystemExit(run())
325
+ `;
326
+ const commandExists = (command) => {
327
+ try {
328
+ const probe = platform() === 'win32'
329
+ ? spawnSync('where', [command], { stdio: 'ignore' })
330
+ : spawnSync('which', [command], { stdio: 'ignore' });
331
+ return probe.status === 0;
332
+ }
333
+ catch {
334
+ return false;
335
+ }
336
+ };
337
+ const readLinuxDefaultBrowserDesktopEntry = () => {
338
+ try {
339
+ const preferred = spawnSync('xdg-settings', ['get', 'default-web-browser'], { encoding: 'utf8' });
340
+ const rawPreferred = preferred.status === 0 ? preferred.stdout.trim() : '';
341
+ if (rawPreferred.length > 0) {
342
+ return rawPreferred;
343
+ }
344
+ }
345
+ catch {
346
+ // fallback below
347
+ }
348
+ try {
349
+ const fallback = spawnSync('xdg-mime', ['query', 'default', 'x-scheme-handler/https'], { encoding: 'utf8' });
350
+ const rawFallback = fallback.status === 0 ? fallback.stdout.trim() : '';
351
+ return rawFallback.length > 0 ? rawFallback : null;
352
+ }
353
+ catch {
354
+ return null;
355
+ }
356
+ };
357
+ const toLinuxDefaultBrowserCommands = (desktopEntry) => {
358
+ if (!desktopEntry) {
359
+ return [];
360
+ }
361
+ const normalized = desktopEntry.toLowerCase().trim();
362
+ if (normalized.includes('firefox')) {
363
+ return ['firefox'];
364
+ }
365
+ if (normalized.includes('edge')) {
366
+ return ['microsoft-edge', 'microsoft-edge-stable'];
367
+ }
368
+ if (normalized.includes('brave')) {
369
+ return ['brave-browser'];
370
+ }
371
+ if (normalized.includes('chromium')) {
372
+ return ['chromium', 'chromium-browser'];
373
+ }
374
+ if (normalized.includes('chrome')) {
375
+ return ['google-chrome', 'google-chrome-stable'];
376
+ }
377
+ return [];
378
+ };
379
+ const readBrowserEnvCommands = () => {
380
+ const value = process.env.BROWSER?.trim();
381
+ if (!value) {
382
+ return [];
383
+ }
384
+ return value
385
+ .split(':')
386
+ .map((entry) => entry.trim().split(/\s+/)[0] ?? '')
387
+ .map((entry) => entry.trim())
388
+ .filter((entry) => entry.length > 0);
389
+ };
390
+ const prioritizeLinuxBrowserCandidates = (candidates) => {
391
+ const preferredCommands = toLinuxDefaultBrowserCommands(readLinuxDefaultBrowserDesktopEntry());
392
+ if (preferredCommands.length === 0) {
393
+ return candidates;
394
+ }
395
+ const priorityMap = new Map(preferredCommands.map((command, index) => [command, index]));
396
+ return [...candidates].sort((left, right) => {
397
+ const leftPriority = priorityMap.get(left[0]);
398
+ const rightPriority = priorityMap.get(right[0]);
399
+ const leftScore = leftPriority == null ? Number.POSITIVE_INFINITY : leftPriority;
400
+ const rightScore = rightPriority == null ? Number.POSITIVE_INFINITY : rightPriority;
401
+ return leftScore - rightScore;
402
+ });
403
+ };
404
+ const envFlagEnabled = (name) => process.env[name] === '1' || process.env[name] === 'true';
405
+ const spawnAnyDetached = (candidates) => candidates.some(([command, args]) => spawnDetached(command, args));
406
+ const spawnAnyDetachedWithEnv = (candidates) => candidates.some(([command, args, env]) => spawnDetached(command, args, env));
407
+ const windowsStartCandidates = (program, args = []) => [
408
+ ['cmd', ['/c', 'start', '', program, ...args]]
409
+ ];
410
+ const resolveSwiftExecutable = () => {
411
+ const directSwift = '/usr/bin/swift';
412
+ if (existsSync(directSwift)) {
413
+ return directSwift;
414
+ }
415
+ try {
416
+ const probe = spawnSync('xcrun', ['--find', 'swift'], { encoding: 'utf8' });
417
+ const swiftPath = probe.status === 0 ? probe.stdout.trim() : '';
418
+ return swiftPath.length > 0 ? swiftPath : null;
419
+ }
420
+ catch {
421
+ return null;
422
+ }
423
+ };
424
+ const openGraphInMacNativeGui = (url, parentPid) => {
425
+ const swiftBinary = resolveSwiftExecutable();
426
+ if (!swiftBinary) {
427
+ return false;
428
+ }
429
+ try {
430
+ writeFileSync(nativeGuiSwiftScriptPath, nativeGuiSwiftScript, 'utf8');
431
+ }
432
+ catch {
433
+ return false;
434
+ }
435
+ return spawnDetached(swiftBinary, [nativeGuiSwiftScriptPath, url, String(parentPid)]);
436
+ };
437
+ const resolveWindowsPowershellExecutable = () => {
438
+ if (commandExists('powershell')) {
439
+ return 'powershell';
440
+ }
441
+ if (commandExists('pwsh')) {
442
+ return 'pwsh';
443
+ }
444
+ return null;
445
+ };
446
+ const openGraphInWindowsNativeGui = (url, parentPid) => {
447
+ const powershell = resolveWindowsPowershellExecutable();
448
+ if (!powershell) {
449
+ return false;
450
+ }
451
+ try {
452
+ writeFileSync(nativeGuiPowershellScriptPath, nativeGuiPowershellScript, 'utf8');
453
+ }
454
+ catch {
455
+ return false;
456
+ }
457
+ return spawnDetached(powershell, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-STA', '-File', nativeGuiPowershellScriptPath, url, String(parentPid)]);
458
+ };
459
+ const openGraphInLinuxNativeGui = (url, parentPid) => {
460
+ if (!commandExists('python3')) {
461
+ return false;
462
+ }
463
+ try {
464
+ writeFileSync(nativeGuiLinuxScriptPath, nativeGuiLinuxPythonScript, 'utf8');
465
+ }
466
+ catch {
467
+ return false;
468
+ }
469
+ return spawnDetached('python3', [nativeGuiLinuxScriptPath, url, String(parentPid)]);
470
+ };
471
+ const openGraphInNativeGui = (url, parentPid) => {
472
+ if (platform() === 'darwin') {
473
+ return openGraphInMacNativeGui(url, parentPid);
474
+ }
475
+ if (platform() === 'win32') {
476
+ return openGraphInWindowsNativeGui(url, parentPid);
477
+ }
478
+ return openGraphInLinuxNativeGui(url, parentPid);
479
+ };
480
+ const openGraphInAppWindow = (url) => {
481
+ if (platform() === 'darwin') {
482
+ const macCandidates = [
483
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
484
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
485
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'
486
+ ]
487
+ .filter((candidate) => existsSync(candidate))
488
+ .map((binary) => ({ binary, args: [`--app=${url}`, '--new-window'] }));
489
+ for (const candidate of macCandidates) {
490
+ if (spawnDetached(candidate.binary, candidate.args)) {
491
+ return true;
492
+ }
493
+ }
494
+ return false;
495
+ }
496
+ if (platform() === 'win32') {
497
+ const appArgument = `--app=${url}`;
498
+ return spawnAnyDetached([
499
+ ...windowsStartCandidates('msedge', [appArgument, '--new-window']),
500
+ ...windowsStartCandidates('chrome', [appArgument, '--new-window']),
501
+ ...windowsStartCandidates('chromium', [appArgument, '--new-window']),
502
+ ...windowsStartCandidates('brave', [appArgument, '--new-window'])
503
+ ]);
504
+ }
505
+ const appArgument = `--app=${url}`;
506
+ const linuxAppWindowEnabled = envFlagEnabled('BRAINLINK_LINUX_APP_WINDOW');
507
+ if (!linuxAppWindowEnabled) {
508
+ return false;
509
+ }
510
+ const linuxChromiumStableFlags = [
511
+ '--ozone-platform=x11',
512
+ '--ozone-platform-hint=x11',
513
+ '--disable-gpu',
514
+ '--disable-vulkan',
515
+ '--use-gl=swiftshader',
516
+ '--disable-features=Vulkan,VaapiVideoDecoder',
517
+ '--disable-background-networking'
518
+ ];
519
+ const linuxChromiumEnv = {
520
+ GDK_BACKEND: 'x11',
521
+ OZONE_PLATFORM: 'x11'
522
+ };
523
+ const linuxAppWindowCandidates = [
524
+ 'microsoft-edge',
525
+ 'microsoft-edge-stable',
526
+ 'google-chrome',
527
+ 'google-chrome-stable',
528
+ 'chromium',
529
+ 'chromium-browser',
530
+ 'brave-browser'
531
+ ].filter((candidate) => commandExists(candidate));
532
+ return spawnAnyDetachedWithEnv(linuxAppWindowCandidates.map((command) => [
533
+ command,
534
+ [...linuxChromiumStableFlags, appArgument, '--new-window'],
535
+ linuxChromiumEnv
536
+ ]));
537
+ };
538
+ const openGraphInDetectedBrowser = (url) => {
539
+ if (platform() === 'win32') {
540
+ return spawnAnyDetached([
541
+ ...windowsStartCandidates('msedge', [url]),
542
+ ...windowsStartCandidates('chrome', [url]),
543
+ ...windowsStartCandidates('firefox', ['-new-window', url]),
544
+ ...windowsStartCandidates('chromium', [url]),
545
+ ...windowsStartCandidates('brave', [url])
546
+ ]);
547
+ }
548
+ const linuxChromiumStableFlags = [
549
+ '--ozone-platform=x11',
550
+ '--ozone-platform-hint=x11',
551
+ '--disable-gpu',
552
+ '--disable-vulkan',
553
+ '--use-gl=swiftshader',
554
+ '--disable-features=Vulkan,VaapiVideoDecoder',
555
+ '--disable-background-networking'
556
+ ];
557
+ const linuxChromiumEnv = {
558
+ GDK_BACKEND: 'x11',
559
+ OZONE_PLATFORM: 'x11'
560
+ };
561
+ const envBrowserCandidates = readBrowserEnvCommands()
562
+ .map((command) => command.includes('firefox')
563
+ ? [command, ['-new-window', url], undefined]
564
+ : [command, [url], undefined])
565
+ .filter(([command]) => commandExists(command));
566
+ if (envBrowserCandidates.length > 0 && spawnAnyDetachedWithEnv(envBrowserCandidates)) {
567
+ return true;
568
+ }
569
+ const linuxBrowserCandidates = [
570
+ ['firefox', ['-new-window', url], undefined],
571
+ ['microsoft-edge', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
572
+ ['microsoft-edge-stable', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
573
+ ['google-chrome', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
574
+ ['google-chrome-stable', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
575
+ ['brave-browser', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
576
+ ['chromium', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
577
+ ['chromium-browser', [...linuxChromiumStableFlags, url], linuxChromiumEnv]
578
+ ];
579
+ const available = prioritizeLinuxBrowserCandidates(linuxBrowserCandidates.filter(([command]) => commandExists(command)));
580
+ return spawnAnyDetachedWithEnv(available);
581
+ };
582
+ const openUrlInUi = (url, parentPid) => {
583
+ const openDisabled = process.env.BRAINLINK_NO_BROWSER === '1' ||
584
+ process.env.BRAINLINK_NO_BROWSER === 'true' ||
585
+ process.env.CI === 'true';
586
+ if (openDisabled) {
587
+ return { opened: false, mode: 'none' };
588
+ }
589
+ const currentPlatform = platform();
590
+ const nativeGuiEnabled = !envFlagEnabled('BRAINLINK_NO_NATIVE_GUI') &&
591
+ (currentPlatform !== 'linux' || envFlagEnabled('BRAINLINK_LINUX_NATIVE_GUI') || envFlagEnabled('BRAINLINK_FORCE_NATIVE_GUI'));
592
+ if (nativeGuiEnabled && openGraphInNativeGui(url, parentPid)) {
593
+ return { opened: true, mode: 'native-gui' };
594
+ }
595
+ if (platform() === 'linux') {
596
+ if (spawnDetached('xdg-open', [url])) {
597
+ return { opened: true, mode: 'browser' };
598
+ }
599
+ if (openGraphInDetectedBrowser(url)) {
600
+ return { opened: true, mode: 'browser' };
601
+ }
602
+ if (openGraphInAppWindow(url)) {
603
+ return { opened: true, mode: 'app-window' };
604
+ }
605
+ return { opened: false, mode: 'none' };
606
+ }
607
+ if (openGraphInAppWindow(url)) {
608
+ return { opened: true, mode: 'app-window' };
609
+ }
610
+ try {
611
+ if (platform() === 'darwin') {
612
+ return { opened: spawnDetached('open', [url]), mode: 'browser' };
613
+ }
614
+ if (openGraphInDetectedBrowser(url)) {
615
+ return { opened: true, mode: 'browser' };
616
+ }
617
+ if (platform() === 'win32') {
618
+ return { opened: spawnDetached('cmd', ['/c', 'start', '', url]), mode: 'browser' };
619
+ }
620
+ return { opened: spawnDetached('xdg-open', [url]), mode: 'browser' };
621
+ }
622
+ catch {
623
+ return { opened: false, mode: 'none' };
624
+ }
625
+ };
26
626
  export const registerWriteCommands = (program) => {
27
627
  program
28
628
  .command('init')
@@ -99,6 +699,60 @@ export const registerWriteCommands = (program) => {
99
699
  return `${summary}${indexMessage}${reportMessage}`;
100
700
  });
101
701
  });
702
+ program
703
+ .command('db-import')
704
+ .option('-v, --vault <vault>', 'vault directory')
705
+ .option('--db <path>', 'legacy SQLite database path (default: <vault>/.brainlink/brainlink.db)')
706
+ .option('--table <name>', 'legacy table name override')
707
+ .option('-a, --agent <agent>', 'force imported notes into a target agent namespace')
708
+ .option('-l, --limit <limit>', 'maximum number of rows to import')
709
+ .option('--dry-run', 'preview import without writing Markdown files')
710
+ .option('--no-index', 'skip reindexing after import')
711
+ .option('--json', 'print machine-readable JSON')
712
+ .description('import legacy SQLite memory into Markdown vault and current index model')
713
+ .action(async (options) => {
714
+ const resolved = await resolveOptions(options);
715
+ const result = await importLegacySqliteDatabase(resolved.vault, {
716
+ dbPath: options.db,
717
+ table: options.table,
718
+ agentOverride: options.agent ? resolved.agent : undefined,
719
+ limit: options.limit ? parsePositiveInteger(options.limit, 100_000) : undefined,
720
+ dryRun: Boolean(options.dryRun)
721
+ });
722
+ const shouldIndex = options.index !== false && !result.dryRun && result.imported > 0;
723
+ const index = shouldIndex ? await indexVault(resolved.vault) : undefined;
724
+ print(options.json, { ...result, ...(index ? { index } : {}) }, () => {
725
+ const summary = `Imported ${result.imported}/${result.rowsRead} rows from ${result.table} (skipped ${result.skipped}).`;
726
+ const indexMessage = index
727
+ ? ` Indexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} links.`
728
+ : '';
729
+ const dryRunMessage = result.dryRun ? ' Dry run only; no files were written.' : '';
730
+ return `${summary}${indexMessage}${dryRunMessage}`;
731
+ });
732
+ });
733
+ program
734
+ .command('volatile')
735
+ .option('-c, --content <content>', 'temporary memory content to add')
736
+ .option('--ttl <minutes>', 'time-to-live in minutes', '240')
737
+ .option('--tag <tag...>', 'volatile memory tag')
738
+ .option('-v, --vault <vault>', 'vault directory')
739
+ .option('-a, --agent <agent>', 'agent memory namespace')
740
+ .option('--clear', 'clear volatile memory for the current agent namespace')
741
+ .option('--json', 'print machine-readable JSON')
742
+ .description('add or clear temporary agent-decided memory')
743
+ .action(async (options) => {
744
+ const resolved = await resolveOptions(options);
745
+ if (options.clear) {
746
+ const cleared = await clearVolatileMemory(resolved.vault, resolved.agent);
747
+ print(options.json, { cleared, agent: resolved.agent ?? 'shared' }, () => `Cleared ${cleared} volatile memories.`);
748
+ return;
749
+ }
750
+ if (!options.content || options.content.trim().length === 0) {
751
+ throw new Error('Use --content to add volatile memory, or --clear to remove it.');
752
+ }
753
+ const entry = await addVolatileMemory(resolved.vault, options.content, resolved.agent ?? 'shared', parsePositiveInteger(options.ttl ?? '240', 240), options.tag ?? []);
754
+ print(options.json, { entry }, () => `Stored volatile memory until ${entry.expiresAt}.`);
755
+ });
102
756
  program
103
757
  .command('add')
104
758
  .argument('<title>', 'note title')
@@ -113,22 +767,167 @@ export const registerWriteCommands = (program) => {
113
767
  .action(async (title, options) => {
114
768
  const resolved = await resolveOptions(options);
115
769
  const content = resolveAddContent(options);
116
- const notePath = await addNote(resolved.vault, title, content, resolved.agent, {
770
+ const added = await addNoteWithMetadata(resolved.vault, title, content, resolved.agent, {
117
771
  allowSensitive: Boolean(options.allowSensitive)
118
772
  });
119
773
  const shouldAutoIndex = options.autoIndex !== false && resolved.config.autoIndexOnWrite;
120
774
  const index = shouldAutoIndex ? await indexVault(resolved.vault) : undefined;
121
- print(options.json, { title, agent: resolved.agent ?? 'shared', path: notePath, ...(index ? { index } : {}) }, () => `Created note at ${notePath}`);
775
+ const absoluteVaultPath = await ensureVault(resolved.vault);
776
+ const focusPath = added.path.startsWith(absoluteVaultPath)
777
+ ? relative(absoluteVaultPath, added.path).replaceAll('\\', '/')
778
+ : added.path.includes('agents/')
779
+ ? added.path.slice(added.path.indexOf('agents/')).replaceAll('\\', '/')
780
+ : undefined;
781
+ const possibleDuplicates = await scanDuplicateNotes(resolved.vault, {
782
+ agentId: resolved.agent,
783
+ focusPath,
784
+ limit: 5,
785
+ minSemanticScore: 0.92,
786
+ includeSemantic: true
787
+ });
788
+ print(options.json, {
789
+ title,
790
+ agent: resolved.agent ?? 'shared',
791
+ path: added.path,
792
+ writeConnectivity: {
793
+ autoLinked: added.autoLinked,
794
+ linkTarget: added.linkTarget,
795
+ guaranteedEdge: true
796
+ },
797
+ possibleDuplicates,
798
+ ...(index ? { index } : {})
799
+ }, () => {
800
+ const duplicateMessage = possibleDuplicates.length > 0
801
+ ? `\nPotential duplicates: ${possibleDuplicates.length}. Use "blink dedupe --json" or "blink dedupe-resolve".`
802
+ : '';
803
+ return `Created note at ${added.path}${duplicateMessage}`;
804
+ });
805
+ });
806
+ program
807
+ .command('dedupe')
808
+ .option('-v, --vault <vault>', 'vault directory')
809
+ .option('-a, --agent <agent>', 'agent memory namespace')
810
+ .option('-l, --limit <limit>', 'maximum duplicate candidate pairs')
811
+ .option('--min-score <score>', 'minimum semantic similarity score between 0 and 1', '0.92')
812
+ .option('--no-semantic', 'disable semantic duplicate detection and keep exact-content matching only')
813
+ .option('--json', 'print machine-readable JSON')
814
+ .description('detect possible duplicate notes with exact hash and semantic similarity scores')
815
+ .action(async (options) => {
816
+ const resolved = await resolveOptions(options);
817
+ const duplicates = await scanDuplicateNotes(resolved.vault, {
818
+ agentId: resolved.agent,
819
+ limit: parsePositiveInteger(options.limit ?? '25', 25),
820
+ minSemanticScore: parseScore(options.minScore, 0.92),
821
+ includeSemantic: options.semantic !== false
822
+ });
823
+ print(options.json, { vault: resolved.vault, agent: resolved.agent, duplicates }, () => {
824
+ if (duplicates.length === 0) {
825
+ return 'No possible duplicates found.';
826
+ }
827
+ return duplicates
828
+ .map((item, index) => `${index + 1}. [${item.kind}] score=${item.score.toFixed(4)} ${item.left.path} <-> ${item.right.path} (${item.reason})`)
829
+ .join('\n');
830
+ });
831
+ });
832
+ program
833
+ .command('dedupe-resolve')
834
+ .option('-v, --vault <vault>', 'vault directory')
835
+ .option('--left <path>', 'left note relative path from dedupe result')
836
+ .option('--right <path>', 'right note relative path from dedupe result')
837
+ .option('--action <action>', 'resolution action: merge, link or ignore')
838
+ .option('--no-auto-index', 'skip reindex after duplicate resolution')
839
+ .option('--json', 'print machine-readable JSON')
840
+ .description('resolve a duplicate candidate with merge, link or ignore')
841
+ .action(async (options) => {
842
+ const resolved = await resolveOptions(options);
843
+ if (!options.left || !options.right) {
844
+ throw new Error('Use --left <path> and --right <path> to resolve a duplicate pair.');
845
+ }
846
+ if (options.action !== 'merge' && options.action !== 'link' && options.action !== 'ignore') {
847
+ throw new Error('Use --action merge|link|ignore.');
848
+ }
849
+ const result = await resolveDuplicateNotes(resolved.vault, {
850
+ leftPath: options.left,
851
+ rightPath: options.right,
852
+ action: options.action,
853
+ autoIndex: options.autoIndex !== false
854
+ });
855
+ print(options.json, {
856
+ vault: resolved.vault,
857
+ ...result
858
+ }, () => `Resolved duplicate (${result.action}) for ${result.leftPath} <-> ${result.rightPath}`);
122
859
  });
123
860
  program
124
861
  .command('index')
125
862
  .option('-v, --vault <vault>', 'vault directory')
863
+ .option('--full', 'force a complete reindex from Markdown source without reusing unchanged index entries')
126
864
  .option('--json', 'print machine-readable JSON')
127
865
  .description('index markdown notes, links, tags and chunks')
128
866
  .action(async (options) => {
129
867
  const resolved = await resolveOptions(options);
130
- const result = await indexVault(resolved.vault);
131
- print(options.json, result, () => `Indexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`);
868
+ const result = await indexVault(resolved.vault, {
869
+ full: options.full === true
870
+ });
871
+ print(options.json, result, () => options.full === true
872
+ ? `Fully reindexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`
873
+ : `Indexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`);
874
+ });
875
+ program
876
+ .command('bench')
877
+ .option('-v, --vault <vault>', 'vault directory')
878
+ .option('-w, --watch', 'watch markdown changes and re-run benchmark in realtime')
879
+ .option('--debounce <ms>', 'watch debounce in milliseconds', '350')
880
+ .option('--json', 'print machine-readable JSON events')
881
+ .description('benchmark indexing in realtime, including compressed pack behavior')
882
+ .action(async (options) => {
883
+ const resolved = await resolveOptions(options);
884
+ const config = await loadBrainlinkConfig();
885
+ const emitProgress = (event) => {
886
+ printBenchRealtimeEvent(options.json, event);
887
+ };
888
+ const printBenchError = (error) => {
889
+ const message = error instanceof Error ? error.message : String(error);
890
+ print(options.json, { event: 'bench-error', message }, () => `[bench] error ${message}`);
891
+ };
892
+ const runAndPrint = async (trigger) => {
893
+ const baseline = await readBenchHistory(resolved.vault);
894
+ const result = await indexVaultWithOptions(resolved.vault, {
895
+ onProgress: emitProgress
896
+ });
897
+ printBenchSummary(options.json, trigger, resolved.vault, result);
898
+ const guardrails = evaluateBenchGuardrails(config, result, baseline);
899
+ printBenchGuardrails(options.json, resolved.vault, config, guardrails);
900
+ await writeBenchHistory(resolved.vault, result);
901
+ return result;
902
+ };
903
+ if (!options.watch) {
904
+ await runAndPrint('manual');
905
+ return;
906
+ }
907
+ const debounceMs = parsePositiveInteger(options.debounce ?? '350', 350);
908
+ await runAndPrint('manual');
909
+ print(options.json, {
910
+ event: 'bench-watching',
911
+ vault: resolved.vault,
912
+ debounceMs
913
+ }, () => `[bench] watching ${resolved.vault} (debounce=${debounceMs}ms)`);
914
+ const watcher = startVaultWatcher({
915
+ vaultPath: resolved.vault,
916
+ debounceMs,
917
+ onProgress: emitProgress,
918
+ onIndex: (result) => {
919
+ printBenchSummary(options.json, 'watch', resolved.vault, result);
920
+ },
921
+ onError: printBenchError
922
+ });
923
+ await new Promise((resolveSignal) => {
924
+ const shutdown = () => {
925
+ watcher.close();
926
+ resolveSignal();
927
+ };
928
+ process.once('SIGINT', shutdown);
929
+ process.once('SIGTERM', shutdown);
930
+ });
132
931
  });
133
932
  program
134
933
  .command('doctor')
@@ -147,6 +946,30 @@ export const registerWriteCommands = (program) => {
147
946
  });
148
947
  process.exitCode = report.ok ? 0 : 1;
149
948
  });
949
+ program
950
+ .command('pack-backup')
951
+ .option('-v, --vault <vault>', 'vault directory')
952
+ .option('-o, --output <path>', 'output file path (.blpkbak.gz)')
953
+ .option('--json', 'print machine-readable JSON')
954
+ .description('create offline backup with second-stage compression for encrypted search packs')
955
+ .action(async (options) => {
956
+ const resolved = await resolveOptions(options);
957
+ const outputPath = options.output?.trim().length
958
+ ? resolve(options.output)
959
+ : join(resolved.vault, '.brainlink', 'backups', `search-packs-${new Date().toISOString().replace(/[:.]/g, '-')}.blpkbak.gz`);
960
+ const backup = await createOfflinePackBackup({
961
+ vaultPath: resolved.vault,
962
+ outputPath
963
+ });
964
+ print(options.json, {
965
+ vault: resolved.vault,
966
+ backup
967
+ }, () => [
968
+ `Offline backup created: ${backup.outputPath}`,
969
+ `files=${backup.fileCount}`,
970
+ `input=${formatBytes(backup.inputBytes)} output=${formatBytes(backup.outputBytes)} saved=${((1 - backup.ratio) * 100).toFixed(2)}%`
971
+ ].join('\n'));
972
+ });
150
973
  program
151
974
  .command('watch')
152
975
  .option('-v, --vault <vault>', 'vault directory')
@@ -181,6 +1004,7 @@ export const registerWriteCommands = (program) => {
181
1004
  .option('-h, --host <host>', 'server host', '127.0.0.1')
182
1005
  .option('-p, --port <port>', 'server port', '4321')
183
1006
  .option('--no-index', 'skip indexing before starting the server')
1007
+ .option('--no-open', 'do not open the graph UI automatically')
184
1008
  .option('-w, --watch', 'watch markdown files and reindex on changes')
185
1009
  .option('--json', 'print machine-readable JSON')
186
1010
  .description('start a local web UI for the knowledge graph')
@@ -193,7 +1017,22 @@ export const registerWriteCommands = (program) => {
193
1017
  shouldIndex: options.index,
194
1018
  shouldWatch: Boolean(options.watch)
195
1019
  });
196
- print(options.json, { url: server.url, watch: Boolean(options.watch), readonly: true }, () => `Brainlink graph server running at ${server.url}`);
1020
+ const openResult = options.open !== false ? openUrlInUi(server.url, process.pid) : { opened: false, mode: 'none' };
1021
+ print(options.json, {
1022
+ url: server.url,
1023
+ watch: Boolean(options.watch),
1024
+ readonly: true,
1025
+ openedUi: openResult.opened,
1026
+ openMode: openResult.mode
1027
+ }, () => `Brainlink graph server running at ${server.url}${openResult.opened
1028
+ ? openResult.mode === 'native-gui'
1029
+ ? ' (opened in native desktop GUI)'
1030
+ : openResult.mode === 'app-window'
1031
+ ? ' (opened in dedicated app window)'
1032
+ : ' (opened in browser)'
1033
+ : options.open === false
1034
+ ? ' (auto-open disabled)'
1035
+ : ''}`);
197
1036
  });
198
1037
  program
199
1038
  .command('quickstart')