@andespindola/brainlink 1.0.4 → 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.
Files changed (53) hide show
  1. package/README.md +17 -9
  2. package/dist/application/add-note.js +2 -2
  3. package/dist/application/build-context.js +16 -10
  4. package/dist/application/canonical-context-links.js +44 -5
  5. package/dist/application/check-package-update.js +105 -0
  6. package/dist/application/frontend/client/chunk-fetch.js +236 -0
  7. package/dist/application/frontend/client/controls.js +178 -0
  8. package/dist/application/frontend/client/elements.js +122 -0
  9. package/dist/application/frontend/client/input.js +202 -0
  10. package/dist/application/frontend/client/node-details.js +191 -0
  11. package/dist/application/frontend/client/rendering.js +296 -0
  12. package/dist/application/frontend/client/scope-theme.js +114 -0
  13. package/dist/application/frontend/client/spatial.js +98 -0
  14. package/dist/application/frontend/client/storage.js +215 -0
  15. package/dist/application/frontend/client/upload.js +90 -0
  16. package/dist/application/frontend/client/worker-bootstrap.js +147 -0
  17. package/dist/application/frontend/client-js.js +24 -1837
  18. package/dist/application/frontend/client-render-worker-js.js +1 -1
  19. package/dist/application/index-vault-phases.js +189 -0
  20. package/dist/application/index-vault.js +44 -165
  21. package/dist/application/server/routes.js +12 -9
  22. package/dist/cli/commands/write/dedupe-commands.js +59 -0
  23. package/dist/cli/commands/write/index-commands.js +205 -0
  24. package/dist/cli/commands/write/link-commands.js +68 -0
  25. package/dist/cli/commands/write/note-commands.js +146 -0
  26. package/dist/cli/commands/write/server-commands.js +553 -0
  27. package/dist/cli/commands/write/shared.js +35 -0
  28. package/dist/cli/commands/write/vault-lifecycle-commands.js +270 -0
  29. package/dist/cli/commands/write-commands.js +12 -1303
  30. package/dist/cli/main.js +39 -3
  31. package/dist/domain/context.js +39 -3
  32. package/dist/domain/embeddings.js +31 -5
  33. package/dist/domain/graph-contexts.js +62 -57
  34. package/dist/domain/graph-layout/cauliflower-layout.js +116 -0
  35. package/dist/domain/graph-layout/collisions.js +100 -0
  36. package/dist/domain/graph-layout/hierarchy.js +135 -0
  37. package/dist/domain/graph-layout/metrics.js +111 -0
  38. package/dist/domain/graph-layout/segments.js +76 -0
  39. package/dist/domain/graph-layout/star-layout.js +110 -0
  40. package/dist/domain/graph-layout.js +4 -625
  41. package/dist/infrastructure/config.js +10 -4
  42. package/dist/infrastructure/file-index.js +13 -4
  43. package/dist/infrastructure/semantic-prefilter.js +24 -0
  44. package/dist/mcp/server.js +7 -0
  45. package/dist/mcp/tool-guard.js +29 -0
  46. package/dist/mcp/tools/maintenance-tools.js +409 -0
  47. package/dist/mcp/tools/read-tools.js +504 -0
  48. package/dist/mcp/tools/shared.js +216 -0
  49. package/dist/mcp/tools/write-tools.js +247 -0
  50. package/dist/mcp/tools.js +3 -1357
  51. package/docs/AGENT_USAGE.md +4 -4
  52. package/docs/QUICKSTART.md +5 -1
  53. package/package.json +2 -2
@@ -1,1305 +1,14 @@
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';
7
- import { buildContextPackage } from '../../application/build-context.js';
8
- import { deleteNote } from '../../application/delete-note.js';
9
- import { resolveDuplicateNotes, scanDuplicateNotes } from '../../application/dedupe-notes.js';
10
- import { importLegacySqliteDatabase } from '../../application/import-legacy-sqlite.js';
11
- import { importFile } from '../../application/import-file.js';
12
- import { indexVault, indexVaultWithOptions } from '../../application/index-vault.js';
13
- import { migrateContextLinks } from '../../application/migrate-context-links.js';
14
- import { canonicalizeContextLinks } from '../../application/canonical-context-links.js';
15
- import { migrateVaultContent, planVaultMigration, previewVaultMigration, shouldMigrateDefaultVault } from '../../application/migrate-vault.js';
16
- import { createOfflinePackBackup } from '../../application/offline-pack-backup.js';
17
- import { startServer } from '../../application/start-server.js';
18
- import { startVaultWatcher } from '../../application/watch-vault.js';
19
- import { doctorVault, getStats, validateVault } from '../../application/analyze-vault.js';
20
- import { buildActionableDoctor } from '../../application/operational-workflows.js';
21
- import { defaultBrainlinkConfig, sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
22
- import { loadBrainlinkConfig } from '../../infrastructure/config.js';
23
- import { assertVaultAllowed, ensureVault, isBucketVaultPath } from '../../infrastructure/file-system-vault.js';
24
- import { getBootstrapPolicy, getBootstrapSessionStatus, touchBootstrapSession } from '../../infrastructure/session-state.js';
25
- import { addVolatileMemory, clearVolatileMemory } from '../../infrastructure/volatile-memory.js';
26
- import { startRemoteMcpServer } from '../../mcp/http-server.js';
27
- import { installAgentIntegration } from './agent-commands.js';
28
- import { parsePositiveInteger, print, resolveOptions } from '../runtime.js';
29
- const resolveAddContent = (options) => {
30
- if (options.content != null && options.content.trim().length > 0) {
31
- return options.content;
32
- }
33
- if (options.contentFile == null || options.contentFile.trim().length === 0) {
34
- throw new Error('Use --content or --content-file to provide note content.');
35
- }
36
- return readFileSync(options.contentFile, 'utf8');
37
- };
38
- const parseScore = (value, fallback) => {
39
- if (value == null) {
40
- return fallback;
41
- }
42
- const parsed = Number.parseFloat(value);
43
- if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
44
- throw new Error(`Invalid score value: ${value}. Expected a number between 0 and 1.`);
45
- }
46
- return parsed;
47
- };
48
- const formatBytes = (bytes) => {
49
- if (!Number.isFinite(bytes) || bytes == null) {
50
- return 'n/a';
51
- }
52
- if (bytes < 1024)
53
- return `${bytes} B`;
54
- const units = ['KB', 'MB', 'GB', 'TB'];
55
- let value = bytes / 1024;
56
- let unitIndex = 0;
57
- while (value >= 1024 && unitIndex < units.length - 1) {
58
- value /= 1024;
59
- unitIndex += 1;
60
- }
61
- return `${value.toFixed(value >= 10 ? 1 : 2)} ${units[unitIndex]}`;
62
- };
63
- const formatMs = (value) => Number.isFinite(value) && value != null ? `${value.toFixed(value >= 100 ? 0 : 1)}ms` : 'n/a';
64
- const benchEventLabel = (event) => `${event.phase}:${event.status}`;
65
- const printBenchRealtimeEvent = (json, event) => {
66
- print(json, {
67
- event: 'bench-progress',
68
- ...event
69
- }, () => `[bench] ${benchEventLabel(event)} ${event.message} (${formatMs(event.elapsedMs)})`);
70
- };
71
- const printBenchSummary = (json, trigger, vault, result) => {
72
- print(json, {
73
- event: 'bench-result',
74
- trigger,
75
- vault,
76
- result
77
- }, () => {
78
- const packs = result.packs;
79
- const compression = packs?.compression;
80
- const savedPercent = compression && compression.inputBytes > 0
81
- ? `${((1 - compression.ratio) * 100).toFixed(1)}%`
82
- : 'n/a';
83
- return [
84
- `[bench] trigger=${trigger}`,
85
- `documents=${result.documentCount} chunks=${result.chunkCount} links=${result.linkCount}`,
86
- `changedDocuments=${result.changedDocumentCount ?? 0} totalElapsed=${formatMs(result.elapsedMs)}`,
87
- `packsRebuilt=${packs?.rebuilt ? 'yes' : 'no'} reason=${packs?.reason ?? 'n/a'}`,
88
- packs?.rebuilt
89
- ? `packCount=${packs.packCount ?? 0} packDuration=${formatMs(packs.durationMs)} input=${formatBytes(compression?.inputBytes)} output=${formatBytes(compression?.outputBytes)} saved=${savedPercent}`
90
- : 'packCompression=n/a'
91
- ].join('\n');
92
- });
93
- };
94
- const benchHistoryPath = (vaultPath) => join(vaultPath, '.brainlink', 'benchmarks', 'latest.json');
95
- const readBenchHistory = async (vaultPath) => {
96
- try {
97
- const parsed = JSON.parse(await readFile(benchHistoryPath(vaultPath), 'utf8'));
98
- if (typeof parsed.elapsedMs !== 'number' || typeof parsed.timestamp !== 'string') {
99
- return null;
100
- }
101
- return {
102
- elapsedMs: parsed.elapsedMs,
103
- timestamp: parsed.timestamp,
104
- ...(typeof parsed.compressionRatio === 'number' ? { compressionRatio: parsed.compressionRatio } : {})
105
- };
106
- }
107
- catch {
108
- return null;
109
- }
110
- };
111
- const writeBenchHistory = async (vaultPath, result) => {
112
- await mkdir(dirname(benchHistoryPath(vaultPath)), { recursive: true });
113
- const payload = {
114
- elapsedMs: result.elapsedMs ?? 0,
115
- timestamp: new Date().toISOString(),
116
- ...(typeof result.packs?.compression?.ratio === 'number' ? { compressionRatio: result.packs.compression.ratio } : {})
117
- };
118
- await writeFile(benchHistoryPath(vaultPath), `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
119
- };
120
- const evaluateBenchGuardrails = (config, result, baseline) => {
121
- const compressionRatio = result.packs?.compression?.ratio;
122
- const compressionSavingsPercent = typeof compressionRatio === 'number' ? Math.max(0, (1 - compressionRatio) * 100) : undefined;
123
- const compressionPass = compressionSavingsPercent != null
124
- ? compressionSavingsPercent >= config.searchPack.guardrailMinSavingsPercent
125
- : undefined;
126
- const latencyRegressionPercent = baseline && baseline.elapsedMs > 0 && typeof result.elapsedMs === 'number'
127
- ? ((result.elapsedMs - baseline.elapsedMs) / baseline.elapsedMs) * 100
128
- : undefined;
129
- const latencyPass = latencyRegressionPercent != null
130
- ? latencyRegressionPercent <= config.searchPack.guardrailMaxLatencyRegressionPercent
131
- : undefined;
132
- return {
133
- ...(compressionSavingsPercent != null ? { compressionSavingsPercent } : {}),
134
- ...(compressionPass != null ? { compressionPass } : {}),
135
- ...(latencyRegressionPercent != null ? { latencyRegressionPercent } : {}),
136
- ...(latencyPass != null ? { latencyPass } : {})
137
- };
138
- };
139
- const printBenchGuardrails = (json, vault, config, guardrails) => {
140
- print(json, {
141
- event: 'bench-guardrails',
142
- vault,
143
- thresholds: {
144
- minSavingsPercent: config.searchPack.guardrailMinSavingsPercent,
145
- maxLatencyRegressionPercent: config.searchPack.guardrailMaxLatencyRegressionPercent
146
- },
147
- guardrails
148
- }, () => {
149
- const savings = guardrails.compressionSavingsPercent;
150
- const latency = guardrails.latencyRegressionPercent;
151
- return [
152
- '[bench] guardrails',
153
- `minSavings=${config.searchPack.guardrailMinSavingsPercent.toFixed(1)}% maxLatencyRegression=${config.searchPack.guardrailMaxLatencyRegressionPercent.toFixed(1)}%`,
154
- `compressionSavings=${savings != null ? `${savings.toFixed(2)}%` : 'n/a'} pass=${guardrails.compressionPass != null ? (guardrails.compressionPass ? 'yes' : 'no') : 'n/a'}`,
155
- `latencyRegression=${latency != null ? `${latency.toFixed(2)}%` : 'n/a'} pass=${guardrails.latencyPass != null ? (guardrails.latencyPass ? 'yes' : 'no') : 'n/a'}`
156
- ].join('\n');
157
- });
158
- };
159
- const spawnDetached = (command, args, envOverrides) => {
160
- try {
161
- const child = spawn(command, args, {
162
- detached: true,
163
- stdio: 'ignore',
164
- env: envOverrides ? { ...process.env, ...envOverrides } : process.env
165
- });
166
- child.unref();
167
- return true;
168
- }
169
- catch {
170
- return false;
171
- }
172
- };
173
- const nativeGuiSwiftScriptPath = join(tmpdir(), 'brainlink-native-gui.swift');
174
- const nativeGuiPowershellScriptPath = join(tmpdir(), 'brainlink-native-gui.ps1');
175
- const nativeGuiLinuxScriptPath = join(tmpdir(), 'brainlink-native-gui-linux.py');
176
- const nativeGuiSwiftScript = `import Foundation
177
- import AppKit
178
- import WebKit
179
- import Darwin
180
-
181
- final class BrainlinkAppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
182
- private let targetUrl: URL
183
- private let parentPid: Int32
184
- private var window: NSWindow?
185
- private var webView: WKWebView?
186
- private var monitorTimer: Timer?
187
-
188
- init(targetUrl: URL, parentPid: Int32) {
189
- self.targetUrl = targetUrl
190
- self.parentPid = parentPid
191
- }
192
-
193
- func applicationDidFinishLaunching(_ notification: Notification) {
194
- let window = NSWindow(
195
- contentRect: NSRect(x: 0, y: 0, width: 1320, height: 860),
196
- styleMask: [.titled, .closable, .miniaturizable, .resizable],
197
- backing: .buffered,
198
- defer: false
199
- )
200
- window.title = "Brainlink Graph"
201
- window.center()
202
- window.isReleasedWhenClosed = false
203
- window.delegate = self
204
-
205
- let webView = WKWebView(frame: window.contentView?.bounds ?? .zero)
206
- webView.autoresizingMask = [.width, .height]
207
- webView.allowsBackForwardNavigationGestures = true
208
- webView.load(URLRequest(url: targetUrl))
209
- window.contentView?.addSubview(webView)
210
-
211
- self.window = window
212
- self.webView = webView
213
-
214
- if parentPid > 0 {
215
- monitorTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
216
- if kill(self.parentPid, 0) != 0 {
217
- NSApp.terminate(nil)
218
- }
219
- }
220
- }
221
-
222
- window.makeKeyAndOrderFront(nil)
223
- NSApp.activate(ignoringOtherApps: true)
224
- }
225
-
226
- func windowWillClose(_ notification: Notification) {
227
- monitorTimer?.invalidate()
228
- NSApp.terminate(nil)
229
- }
230
- }
231
-
232
- let args = Array(CommandLine.arguments.dropFirst())
233
- let rawTarget = args.indices.contains(0) ? args[0] : "http://127.0.0.1:4321"
234
- let parentPid: Int32 = args.indices.contains(1) ? (Int32(args[1]) ?? 0) : 0
235
-
236
- guard let targetUrl = URL(string: rawTarget) else {
237
- fputs("Invalid URL for Brainlink GUI: \\(rawTarget)\\n", stderr)
238
- exit(1)
239
- }
240
-
241
- let app = NSApplication.shared
242
- app.setActivationPolicy(.regular)
243
- let delegate = BrainlinkAppDelegate(targetUrl: targetUrl, parentPid: parentPid)
244
- app.delegate = delegate
245
- app.run()
246
- `;
247
- const nativeGuiPowershellScript = `param(
248
- [string]$TargetUrl = "http://127.0.0.1:4321",
249
- [int]$ParentPid = 0
250
- )
251
-
252
- Add-Type -AssemblyName System.Windows.Forms
253
- Add-Type -AssemblyName System.Drawing
254
- [System.Windows.Forms.Application]::EnableVisualStyles()
255
-
256
- $form = New-Object System.Windows.Forms.Form
257
- $form.Text = "Brainlink Graph"
258
- $form.Width = 1320
259
- $form.Height = 860
260
- $form.StartPosition = "CenterScreen"
261
-
262
- $browser = New-Object System.Windows.Forms.WebBrowser
263
- $browser.Dock = [System.Windows.Forms.DockStyle]::Fill
264
- $browser.ScriptErrorsSuppressed = $true
265
- $browser.Navigate($TargetUrl)
266
-
267
- $form.Controls.Add($browser)
268
- $timer = New-Object System.Windows.Forms.Timer
269
- $timer.Interval = 1000
270
- $timer.Add_Tick({
271
- if ($ParentPid -le 0) {
272
- return
273
- }
274
- try {
275
- Get-Process -Id $ParentPid -ErrorAction Stop | Out-Null
276
- } catch {
277
- $timer.Stop()
278
- $form.Close()
279
- }
280
- })
281
- $form.Add_FormClosed({
282
- $timer.Stop()
283
- })
284
- $timer.Start()
285
- [void]$form.ShowDialog()
286
- `;
287
- const nativeGuiLinuxPythonScript = `#!/usr/bin/env python3
288
- import sys
289
-
290
- def run() -> int:
291
- try:
292
- import gi
293
- gi.require_version("Gtk", "3.0")
294
- try:
295
- gi.require_version("WebKit2", "4.1")
296
- except ValueError:
297
- gi.require_version("WebKit2", "4.0")
298
- from gi.repository import Gtk, WebKit2, GLib
299
- except Exception:
300
- return 1
301
-
302
- target_url = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:4321"
303
- parent_pid = int(sys.argv[2]) if len(sys.argv) > 2 else 0
304
-
305
- window = Gtk.Window(title="Brainlink Graph")
306
- window.set_default_size(1320, 860)
307
- window.connect("destroy", Gtk.main_quit)
308
-
309
- webview = WebKit2.WebView()
310
- webview.load_uri(target_url)
311
- window.add(webview)
312
- window.show_all()
313
-
314
- if parent_pid > 0:
315
- def _watch_parent() -> bool:
316
- try:
317
- import os
318
- os.kill(parent_pid, 0)
319
- except Exception:
320
- Gtk.main_quit()
321
- return False
322
- return True
323
-
324
- GLib.timeout_add(1000, _watch_parent)
325
-
326
- Gtk.main()
327
- return 0
328
-
329
- if __name__ == "__main__":
330
- raise SystemExit(run())
331
- `;
332
- const commandExists = (command) => {
333
- try {
334
- const probe = platform() === 'win32'
335
- ? spawnSync('where', [command], { stdio: 'ignore' })
336
- : spawnSync('which', [command], { stdio: 'ignore' });
337
- return probe.status === 0;
338
- }
339
- catch {
340
- return false;
341
- }
342
- };
343
- const readLinuxDefaultBrowserDesktopEntry = () => {
344
- try {
345
- const preferred = spawnSync('xdg-settings', ['get', 'default-web-browser'], { encoding: 'utf8' });
346
- const rawPreferred = preferred.status === 0 ? preferred.stdout.trim() : '';
347
- if (rawPreferred.length > 0) {
348
- return rawPreferred;
349
- }
350
- }
351
- catch {
352
- // fallback below
353
- }
354
- try {
355
- const fallback = spawnSync('xdg-mime', ['query', 'default', 'x-scheme-handler/https'], { encoding: 'utf8' });
356
- const rawFallback = fallback.status === 0 ? fallback.stdout.trim() : '';
357
- return rawFallback.length > 0 ? rawFallback : null;
358
- }
359
- catch {
360
- return null;
361
- }
362
- };
363
- const toLinuxDefaultBrowserCommands = (desktopEntry) => {
364
- if (!desktopEntry) {
365
- return [];
366
- }
367
- const normalized = desktopEntry.toLowerCase().trim();
368
- if (normalized.includes('firefox')) {
369
- return ['firefox'];
370
- }
371
- if (normalized.includes('edge')) {
372
- return ['microsoft-edge', 'microsoft-edge-stable'];
373
- }
374
- if (normalized.includes('brave')) {
375
- return ['brave-browser'];
376
- }
377
- if (normalized.includes('chromium')) {
378
- return ['chromium', 'chromium-browser'];
379
- }
380
- if (normalized.includes('chrome')) {
381
- return ['google-chrome', 'google-chrome-stable'];
382
- }
383
- return [];
384
- };
385
- const readBrowserEnvCommands = () => {
386
- const value = process.env.BROWSER?.trim();
387
- if (!value) {
388
- return [];
389
- }
390
- return value
391
- .split(':')
392
- .map((entry) => entry.trim().split(/\s+/)[0] ?? '')
393
- .map((entry) => entry.trim())
394
- .filter((entry) => entry.length > 0);
395
- };
396
- const prioritizeLinuxBrowserCandidates = (candidates) => {
397
- const preferredCommands = toLinuxDefaultBrowserCommands(readLinuxDefaultBrowserDesktopEntry());
398
- if (preferredCommands.length === 0) {
399
- return candidates;
400
- }
401
- const priorityMap = new Map(preferredCommands.map((command, index) => [command, index]));
402
- return [...candidates].sort((left, right) => {
403
- const leftPriority = priorityMap.get(left[0]);
404
- const rightPriority = priorityMap.get(right[0]);
405
- const leftScore = leftPriority == null ? Number.POSITIVE_INFINITY : leftPriority;
406
- const rightScore = rightPriority == null ? Number.POSITIVE_INFINITY : rightPriority;
407
- return leftScore - rightScore;
408
- });
409
- };
410
- const envFlagEnabled = (name) => process.env[name] === '1' || process.env[name] === 'true';
411
- const spawnAnyDetached = (candidates) => candidates.some(([command, args]) => spawnDetached(command, args));
412
- const spawnAnyDetachedWithEnv = (candidates) => candidates.some(([command, args, env]) => spawnDetached(command, args, env));
413
- const windowsStartCandidates = (program, args = []) => [
414
- ['cmd', ['/c', 'start', '', program, ...args]]
415
- ];
416
- const resolveSwiftExecutable = () => {
417
- const directSwift = '/usr/bin/swift';
418
- if (existsSync(directSwift)) {
419
- return directSwift;
420
- }
421
- try {
422
- const probe = spawnSync('xcrun', ['--find', 'swift'], { encoding: 'utf8' });
423
- const swiftPath = probe.status === 0 ? probe.stdout.trim() : '';
424
- return swiftPath.length > 0 ? swiftPath : null;
425
- }
426
- catch {
427
- return null;
428
- }
429
- };
430
- const openGraphInMacNativeGui = (url, parentPid) => {
431
- const swiftBinary = resolveSwiftExecutable();
432
- if (!swiftBinary) {
433
- return false;
434
- }
435
- try {
436
- writeFileSync(nativeGuiSwiftScriptPath, nativeGuiSwiftScript, 'utf8');
437
- }
438
- catch {
439
- return false;
440
- }
441
- return spawnDetached(swiftBinary, [nativeGuiSwiftScriptPath, url, String(parentPid)]);
442
- };
443
- const resolveWindowsPowershellExecutable = () => {
444
- if (commandExists('powershell')) {
445
- return 'powershell';
446
- }
447
- if (commandExists('pwsh')) {
448
- return 'pwsh';
449
- }
450
- return null;
451
- };
452
- const openGraphInWindowsNativeGui = (url, parentPid) => {
453
- const powershell = resolveWindowsPowershellExecutable();
454
- if (!powershell) {
455
- return false;
456
- }
457
- try {
458
- writeFileSync(nativeGuiPowershellScriptPath, nativeGuiPowershellScript, 'utf8');
459
- }
460
- catch {
461
- return false;
462
- }
463
- return spawnDetached(powershell, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-STA', '-File', nativeGuiPowershellScriptPath, url, String(parentPid)]);
464
- };
465
- const openGraphInLinuxNativeGui = (url, parentPid) => {
466
- if (!commandExists('python3')) {
467
- return false;
468
- }
469
- try {
470
- writeFileSync(nativeGuiLinuxScriptPath, nativeGuiLinuxPythonScript, 'utf8');
471
- }
472
- catch {
473
- return false;
474
- }
475
- return spawnDetached('python3', [nativeGuiLinuxScriptPath, url, String(parentPid)]);
476
- };
477
- const openGraphInNativeGui = (url, parentPid) => {
478
- if (platform() === 'darwin') {
479
- return openGraphInMacNativeGui(url, parentPid);
480
- }
481
- if (platform() === 'win32') {
482
- return openGraphInWindowsNativeGui(url, parentPid);
483
- }
484
- return openGraphInLinuxNativeGui(url, parentPid);
485
- };
486
- const openGraphInAppWindow = (url) => {
487
- if (platform() === 'darwin') {
488
- const macCandidates = [
489
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
490
- '/Applications/Chromium.app/Contents/MacOS/Chromium',
491
- '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'
492
- ]
493
- .filter((candidate) => existsSync(candidate))
494
- .map((binary) => ({ binary, args: [`--app=${url}`, '--new-window'] }));
495
- for (const candidate of macCandidates) {
496
- if (spawnDetached(candidate.binary, candidate.args)) {
497
- return true;
498
- }
499
- }
500
- return false;
501
- }
502
- if (platform() === 'win32') {
503
- const appArgument = `--app=${url}`;
504
- return spawnAnyDetached([
505
- ...windowsStartCandidates('msedge', [appArgument, '--new-window']),
506
- ...windowsStartCandidates('chrome', [appArgument, '--new-window']),
507
- ...windowsStartCandidates('chromium', [appArgument, '--new-window']),
508
- ...windowsStartCandidates('brave', [appArgument, '--new-window'])
509
- ]);
510
- }
511
- const appArgument = `--app=${url}`;
512
- const linuxAppWindowEnabled = envFlagEnabled('BRAINLINK_LINUX_APP_WINDOW');
513
- if (!linuxAppWindowEnabled) {
514
- return false;
515
- }
516
- const linuxChromiumStableFlags = [
517
- '--ozone-platform=x11',
518
- '--ozone-platform-hint=x11',
519
- '--disable-gpu',
520
- '--disable-vulkan',
521
- '--use-gl=swiftshader',
522
- '--disable-features=Vulkan,VaapiVideoDecoder',
523
- '--disable-background-networking'
524
- ];
525
- const linuxChromiumEnv = {
526
- GDK_BACKEND: 'x11',
527
- OZONE_PLATFORM: 'x11'
528
- };
529
- const linuxAppWindowCandidates = [
530
- 'microsoft-edge',
531
- 'microsoft-edge-stable',
532
- 'google-chrome',
533
- 'google-chrome-stable',
534
- 'chromium',
535
- 'chromium-browser',
536
- 'brave-browser'
537
- ].filter((candidate) => commandExists(candidate));
538
- return spawnAnyDetachedWithEnv(linuxAppWindowCandidates.map((command) => [
539
- command,
540
- [...linuxChromiumStableFlags, appArgument, '--new-window'],
541
- linuxChromiumEnv
542
- ]));
543
- };
544
- const openGraphInDetectedBrowser = (url) => {
545
- if (platform() === 'win32') {
546
- return spawnAnyDetached([
547
- ...windowsStartCandidates('msedge', [url]),
548
- ...windowsStartCandidates('chrome', [url]),
549
- ...windowsStartCandidates('firefox', ['-new-window', url]),
550
- ...windowsStartCandidates('chromium', [url]),
551
- ...windowsStartCandidates('brave', [url])
552
- ]);
553
- }
554
- const linuxChromiumStableFlags = [
555
- '--ozone-platform=x11',
556
- '--ozone-platform-hint=x11',
557
- '--disable-gpu',
558
- '--disable-vulkan',
559
- '--use-gl=swiftshader',
560
- '--disable-features=Vulkan,VaapiVideoDecoder',
561
- '--disable-background-networking'
562
- ];
563
- const linuxChromiumEnv = {
564
- GDK_BACKEND: 'x11',
565
- OZONE_PLATFORM: 'x11'
566
- };
567
- const envBrowserCandidates = readBrowserEnvCommands()
568
- .map((command) => command.includes('firefox')
569
- ? [command, ['-new-window', url], undefined]
570
- : [command, [url], undefined])
571
- .filter(([command]) => commandExists(command));
572
- if (envBrowserCandidates.length > 0 && spawnAnyDetachedWithEnv(envBrowserCandidates)) {
573
- return true;
574
- }
575
- const linuxBrowserCandidates = [
576
- ['firefox', ['-new-window', url], undefined],
577
- ['microsoft-edge', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
578
- ['microsoft-edge-stable', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
579
- ['google-chrome', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
580
- ['google-chrome-stable', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
581
- ['brave-browser', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
582
- ['chromium', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
583
- ['chromium-browser', [...linuxChromiumStableFlags, url], linuxChromiumEnv]
584
- ];
585
- const available = prioritizeLinuxBrowserCandidates(linuxBrowserCandidates.filter(([command]) => commandExists(command)));
586
- return spawnAnyDetachedWithEnv(available);
587
- };
588
- const openUrlInUi = (url, parentPid) => {
589
- const openDisabled = process.env.BRAINLINK_NO_BROWSER === '1' ||
590
- process.env.BRAINLINK_NO_BROWSER === 'true' ||
591
- process.env.CI === 'true';
592
- if (openDisabled) {
593
- return { opened: false, mode: 'none' };
594
- }
595
- const currentPlatform = platform();
596
- const nativeGuiEnabled = !envFlagEnabled('BRAINLINK_NO_NATIVE_GUI') &&
597
- (currentPlatform !== 'linux' || envFlagEnabled('BRAINLINK_LINUX_NATIVE_GUI') || envFlagEnabled('BRAINLINK_FORCE_NATIVE_GUI'));
598
- if (nativeGuiEnabled && openGraphInNativeGui(url, parentPid)) {
599
- return { opened: true, mode: 'native-gui' };
600
- }
601
- if (platform() === 'linux') {
602
- if (spawnDetached('xdg-open', [url])) {
603
- return { opened: true, mode: 'browser' };
604
- }
605
- if (openGraphInDetectedBrowser(url)) {
606
- return { opened: true, mode: 'browser' };
607
- }
608
- if (openGraphInAppWindow(url)) {
609
- return { opened: true, mode: 'app-window' };
610
- }
611
- return { opened: false, mode: 'none' };
612
- }
613
- if (openGraphInAppWindow(url)) {
614
- return { opened: true, mode: 'app-window' };
615
- }
616
- try {
617
- if (platform() === 'darwin') {
618
- return { opened: spawnDetached('open', [url]), mode: 'browser' };
619
- }
620
- if (openGraphInDetectedBrowser(url)) {
621
- return { opened: true, mode: 'browser' };
622
- }
623
- if (platform() === 'win32') {
624
- return { opened: spawnDetached('cmd', ['/c', 'start', '', url]), mode: 'browser' };
625
- }
626
- return { opened: spawnDetached('xdg-open', [url]), mode: 'browser' };
627
- }
628
- catch {
629
- return { opened: false, mode: 'none' };
630
- }
631
- };
1
+ import { registerVaultLifecycleCommands } from './write/vault-lifecycle-commands.js';
2
+ import { registerLinkCommands } from './write/link-commands.js';
3
+ import { registerNoteCommands } from './write/note-commands.js';
4
+ import { registerDedupeCommands } from './write/dedupe-commands.js';
5
+ import { registerIndexCommands } from './write/index-commands.js';
6
+ import { registerServerCommands } from './write/server-commands.js';
632
7
  export const registerWriteCommands = (program) => {
633
- program
634
- .command('init')
635
- .argument('[vault]', 'vault directory')
636
- .option('--migrate-from <vault>', 'copy existing vault content into the initialized vault')
637
- .option('--no-migrate-existing', 'skip automatic migration from the default Brainlink vault into an empty custom vault')
638
- .option('--json', 'print machine-readable JSON')
639
- .description('initialize a Brainlink vault')
640
- .action(async (vault, options) => {
641
- const config = await loadBrainlinkConfig();
642
- const targetVault = assertVaultAllowed(vault ?? config.vault, config.allowedVaults);
643
- const path = await ensureVault(targetVault);
644
- const explicitSource = options.migrateFrom ? assertVaultAllowed(options.migrateFrom, config.allowedVaults) : undefined;
645
- const shouldAutoMigrate = explicitSource === undefined &&
646
- options.migrateExisting !== false &&
647
- (await shouldMigrateDefaultVault(defaultBrainlinkConfig.vault, targetVault));
648
- const migration = explicitSource || shouldAutoMigrate ? await migrateVaultContent(explicitSource ?? defaultBrainlinkConfig.vault, targetVault) : undefined;
649
- const index = migration && migration.copied + migration.conflicted > 0 ? await indexVault(targetVault) : undefined;
650
- print(options.json, { path, ...(migration ? { migration } : {}), ...(index ? { index } : {}) }, () => {
651
- const migrated = migration
652
- ? ` Migrated ${migration.copied} files, preserved ${migration.conflicted} conflicts and kept ${migration.unchanged} unchanged files.`
653
- : '';
654
- return `Initialized Brainlink vault at ${path}.${migrated}`;
655
- });
656
- });
657
- program
658
- .command('migrate-vault')
659
- .option('--from <vault>', 'source vault path')
660
- .option('--to <vault>', 'target vault path')
661
- .option('--dry-run', 'preview migration without writing files')
662
- .option('--report <path>', 'write detailed per-file migration report to JSON file')
663
- .option('--no-index', 'skip reindexing target vault after migration')
664
- .option('--json', 'print machine-readable JSON')
665
- .description('copy markdown memory from one vault to another with conflict preservation')
666
- .action(async (options) => {
667
- const config = await loadBrainlinkConfig();
668
- const sourceVault = assertVaultAllowed(options.from ?? config.vault, config.allowedVaults);
669
- const targetVault = assertVaultAllowed(options.to ?? defaultBrainlinkConfig.vault, config.allowedVaults);
670
- const sourceRoot = await ensureVault(sourceVault);
671
- const targetRoot = await ensureVault(targetVault);
672
- const preview = await previewVaultMigration(sourceVault, targetVault);
673
- const actions = await planVaultMigration(sourceRoot, targetRoot);
674
- const reportEntries = actions.map((action) => ({
675
- kind: action.kind,
676
- sourcePath: action.sourcePath,
677
- sourceRelativePath: relative(sourceRoot, action.sourcePath),
678
- targetPath: action.targetPath,
679
- targetRelativePath: relative(targetRoot, action.targetPath)
680
- }));
681
- const writeReport = async () => {
682
- if (!options.report) {
683
- return null;
684
- }
685
- const reportPath = resolve(options.report);
686
- await mkdir(dirname(reportPath), { recursive: true });
687
- await writeFile(reportPath, `${JSON.stringify({ source: sourceVault, target: targetVault, summary: preview, entries: reportEntries }, null, 2)}\n`, 'utf8');
688
- return reportPath;
689
- };
690
- if (options.dryRun) {
691
- const reportPath = await writeReport();
692
- print(options.json, { dryRun: true, ...preview, entries: reportEntries, ...(reportPath ? { reportPath } : {}) }, () => `Dry run migration ${preview.source} -> ${preview.target}: copy=${preview.copied}, conflicts=${preview.conflicted}, unchanged=${preview.unchanged}${reportPath ? ` report=${reportPath}` : ''}`);
693
- return;
694
- }
695
- const migration = await migrateVaultContent(sourceVault, targetVault);
696
- const shouldIndex = options.index !== false && migration.copied + migration.conflicted > 0;
697
- const index = shouldIndex ? await indexVault(targetVault) : undefined;
698
- const reportPath = await writeReport();
699
- print(options.json, { dryRun: false, ...migration, entries: reportEntries, ...(index ? { index } : {}), ...(reportPath ? { reportPath } : {}) }, () => {
700
- const summary = `Migrated ${migration.copied} files, preserved ${migration.conflicted} conflicts and kept ${migration.unchanged} unchanged files.`;
701
- const indexMessage = index
702
- ? ` Indexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} links.`
703
- : '';
704
- const reportMessage = reportPath ? ` Report written to ${reportPath}.` : '';
705
- return `${summary}${indexMessage}${reportMessage}`;
706
- });
707
- });
708
- program
709
- .command('migrate-context-links')
710
- .option('-v, --vault <vault>', 'vault directory')
711
- .option('-a, --agent <agent>', 'agent memory namespace')
712
- .option('-l, --limit <limit>', 'maximum context links to add per note', '5')
713
- .option('--dry-run', 'preview context-link migration without writing files')
714
- .option('--no-index', 'skip reindexing after migration')
715
- .option('--json', 'print machine-readable JSON')
716
- .description('add concise Context Links sections from existing wiki-link mentions')
717
- .action(async (options) => {
718
- const resolved = await resolveOptions(options);
719
- const result = await migrateContextLinks(resolved.vault, {
720
- dryRun: options.dryRun === true,
721
- limit: parsePositiveInteger(options.limit ?? '5', 5),
722
- agentId: resolved.agent
723
- });
724
- const shouldIndex = options.index !== false && !result.dryRun && result.changed > 0;
725
- const index = shouldIndex ? await indexVault(resolved.vault, { full: true }) : undefined;
726
- print(options.json, {
727
- vault: resolved.vault,
728
- agent: resolved.agent ?? 'shared',
729
- ...result,
730
- ...(index ? { index } : {})
731
- }, () => {
732
- const mode = result.dryRun ? 'Previewed' : 'Migrated';
733
- const indexMessage = index
734
- ? ` Fully reindexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} context links.`
735
- : '';
736
- return `${mode} ${result.scanned} notes: changed=${result.changed}, skipped=${result.skipped}, limit=${result.limit}.${indexMessage}`;
737
- });
738
- });
739
- program
740
- .command('canonicalize-context-links')
741
- .option('-v, --vault <vault>', 'vault directory')
742
- .option('-a, --agent <agent>', 'agent memory namespace')
743
- .option('--dry-run', 'preview canonical context links without writing files')
744
- .option('--no-create-hubs', 'do not create missing context hub notes')
745
- .option('--no-index', 'skip reindexing after canonicalization')
746
- .option('--json', 'print machine-readable JSON')
747
- .description('ensure notes have canonical Context Links to their inferred context hubs')
748
- .action(async (options) => {
749
- const resolved = await resolveOptions(options);
750
- const result = await canonicalizeContextLinks(resolved.vault, {
751
- dryRun: options.dryRun === true,
752
- agentId: resolved.agent,
753
- createMissingHubs: options.createHubs !== false
754
- });
755
- const shouldIndex = options.index !== false && !result.dryRun && result.changed > 0;
756
- const index = shouldIndex ? await indexVault(resolved.vault, { full: true }) : undefined;
757
- print(options.json, {
758
- vault: resolved.vault,
759
- agent: resolved.agent ?? 'shared',
760
- ...result,
761
- ...(index ? { index } : {})
762
- }, () => {
763
- const mode = result.dryRun ? 'Previewed' : 'Canonicalized';
764
- const indexMessage = index
765
- ? ` Fully reindexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} context links.`
766
- : '';
767
- return `${mode} ${result.scanned} notes: changed=${result.changed}, createdHubs=${result.createdHubs}, skipped=${result.skipped}.${indexMessage}`;
768
- });
769
- });
770
- program
771
- .command('db-import')
772
- .option('-v, --vault <vault>', 'vault directory')
773
- .option('--db <path>', 'legacy SQLite database path (default: <vault>/.brainlink/brainlink.db)')
774
- .option('--table <name>', 'legacy table name override')
775
- .option('-a, --agent <agent>', 'force imported notes into a target agent namespace')
776
- .option('-l, --limit <limit>', 'maximum number of rows to import')
777
- .option('--dry-run', 'preview import without writing Markdown files')
778
- .option('--no-index', 'skip reindexing after import')
779
- .option('--json', 'print machine-readable JSON')
780
- .description('import legacy SQLite memory into Markdown vault and current index model')
781
- .action(async (options) => {
782
- const resolved = await resolveOptions(options);
783
- const result = await importLegacySqliteDatabase(resolved.vault, {
784
- dbPath: options.db,
785
- table: options.table,
786
- agentOverride: options.agent ? resolved.agent : undefined,
787
- limit: options.limit ? parsePositiveInteger(options.limit, 100_000) : undefined,
788
- dryRun: Boolean(options.dryRun)
789
- });
790
- const shouldIndex = options.index !== false && !result.dryRun && result.imported > 0;
791
- const index = shouldIndex ? await indexVault(resolved.vault) : undefined;
792
- print(options.json, { ...result, ...(index ? { index } : {}) }, () => {
793
- const summary = `Imported ${result.imported}/${result.rowsRead} rows from ${result.table} (skipped ${result.skipped}).`;
794
- const indexMessage = index
795
- ? ` Indexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} links.`
796
- : '';
797
- const dryRunMessage = result.dryRun ? ' Dry run only; no files were written.' : '';
798
- return `${summary}${indexMessage}${dryRunMessage}`;
799
- });
800
- });
801
- program
802
- .command('volatile')
803
- .option('-c, --content <content>', 'temporary memory content to add')
804
- .option('--ttl <minutes>', 'time-to-live in minutes', '240')
805
- .option('--tag <tag...>', 'volatile memory tag')
806
- .option('-v, --vault <vault>', 'vault directory')
807
- .option('-a, --agent <agent>', 'agent memory namespace')
808
- .option('--clear', 'clear volatile memory for the current agent namespace')
809
- .option('--json', 'print machine-readable JSON')
810
- .description('add or clear temporary agent-decided memory')
811
- .action(async (options) => {
812
- const resolved = await resolveOptions(options);
813
- if (options.clear) {
814
- const cleared = await clearVolatileMemory(resolved.vault, resolved.agent);
815
- print(options.json, { cleared, agent: resolved.agent ?? 'shared' }, () => `Cleared ${cleared} volatile memories.`);
816
- return;
817
- }
818
- if (!options.content || options.content.trim().length === 0) {
819
- throw new Error('Use --content to add volatile memory, or --clear to remove it.');
820
- }
821
- const entry = await addVolatileMemory(resolved.vault, options.content, resolved.agent ?? 'shared', parsePositiveInteger(options.ttl ?? '240', 240), options.tag ?? []);
822
- print(options.json, { entry }, () => `Stored volatile memory until ${entry.expiresAt}.`);
823
- });
824
- program
825
- .command('add')
826
- .argument('<title>', 'note title')
827
- .option('-c, --content <content>', 'markdown content')
828
- .option('-f, --content-file <contentFile>', 'read markdown content from a file')
829
- .option('-v, --vault <vault>', 'vault directory')
830
- .option('-a, --agent <agent>', 'agent memory namespace')
831
- .option('--allow-sensitive', 'allow writing content that looks like a secret')
832
- .option('--no-auto-context-links', 'skip canonical Context Links for this note')
833
- .option('--no-auto-index', 'skip reindexing after add')
834
- .option('--json', 'print machine-readable JSON')
835
- .description('add a markdown note to the vault')
836
- .action(async (title, options) => {
837
- const resolved = await resolveOptions(options);
838
- const content = resolveAddContent(options);
839
- const added = await addNoteWithMetadata(resolved.vault, title, content, resolved.agent, {
840
- allowSensitive: Boolean(options.allowSensitive),
841
- autoContextLinks: options.autoContextLinks !== false && resolved.config.autoCanonicalContextLinks
842
- });
843
- const shouldAutoIndex = options.autoIndex !== false && resolved.config.autoIndexOnWrite;
844
- const index = shouldAutoIndex ? await indexVault(resolved.vault) : undefined;
845
- const absoluteVaultPath = await ensureVault(resolved.vault);
846
- const focusPath = added.path.startsWith(absoluteVaultPath)
847
- ? relative(absoluteVaultPath, added.path).replaceAll('\\', '/')
848
- : added.path.includes('agents/')
849
- ? added.path.slice(added.path.indexOf('agents/')).replaceAll('\\', '/')
850
- : undefined;
851
- const possibleDuplicates = await scanDuplicateNotes(resolved.vault, {
852
- agentId: resolved.agent,
853
- focusPath,
854
- limit: 5,
855
- minSemanticScore: 0.92,
856
- includeSemantic: true
857
- });
858
- print(options.json, {
859
- title,
860
- agent: resolved.agent ?? 'shared',
861
- path: added.path,
862
- writeConnectivity: {
863
- autoLinked: added.autoLinked,
864
- linkTarget: added.linkTarget,
865
- context: added.context,
866
- hubCreated: added.hubCreated,
867
- guaranteedEdge: added.autoLinked
868
- },
869
- possibleDuplicates,
870
- ...(index ? { index } : {})
871
- }, () => {
872
- const duplicateMessage = possibleDuplicates.length > 0
873
- ? `\nPotential duplicates: ${possibleDuplicates.length}. Use "blink dedupe --json" or "blink dedupe-resolve".`
874
- : '';
875
- const linkMessage = added.autoLinked ? ` Linked to [[${added.linkTarget}]].` : '';
876
- return `Created note at ${added.path}.${linkMessage}${duplicateMessage}`;
877
- });
878
- });
879
- program
880
- .command('import-file')
881
- .argument('<file>', 'file to convert and import into the vault')
882
- .option('-t, --title <title>', 'note title; defaults to the source filename')
883
- .option('-v, --vault <vault>', 'vault directory')
884
- .option('-a, --agent <agent>', 'agent memory namespace')
885
- .option('--allow-sensitive', 'allow writing converted content that looks like a secret')
886
- .option('--no-auto-context-links', 'skip canonical Context Links for this imported note')
887
- .option('--no-auto-index', 'skip reindexing after import')
888
- .option('--json', 'print machine-readable JSON')
889
- .description('convert a document with Docling and import it as a Markdown note')
890
- .action(async (file, options) => {
891
- const resolved = await resolveOptions(options);
892
- const result = await importFile({
893
- vaultPath: resolved.vault,
894
- filePath: resolve(file),
895
- title: options.title,
896
- agentId: resolved.agent,
897
- allowSensitive: Boolean(options.allowSensitive),
898
- autoContextLinks: options.autoContextLinks !== false && resolved.config.autoCanonicalContextLinks,
899
- autoIndex: options.autoIndex !== false && resolved.config.autoIndexOnWrite
900
- });
901
- print(options.json, {
902
- vault: resolved.vault,
903
- agent: resolved.agent ?? 'shared',
904
- ...result
905
- }, () => {
906
- const linkMessage = result.writeConnectivity.autoLinked ? ` Linked to [[${result.writeConnectivity.linkTarget}]].` : '';
907
- const indexMessage = result.index ? ` Indexed ${result.index.documentCount} documents.` : '';
908
- return `Imported ${result.sourceName} as "${result.title}" at ${result.path}.${linkMessage}${indexMessage}`;
909
- });
910
- });
911
- program
912
- .command('delete-note')
913
- .option('-v, --vault <vault>', 'vault directory')
914
- .option('-a, --agent <agent>', 'agent memory namespace when deleting by title')
915
- .option('--title <title>', 'note title to delete')
916
- .option('--path <path>', 'vault-relative or absolute markdown note path to delete')
917
- .option('--yes', 'confirm note deletion')
918
- .option('--no-auto-index', 'skip reindexing after delete')
919
- .option('--json', 'print machine-readable JSON')
920
- .description('delete a Markdown note from the vault after explicit confirmation')
921
- .action(async (options) => {
922
- const resolved = await resolveOptions(options);
923
- const result = await deleteNote(resolved.vault, {
924
- title: options.title,
925
- path: options.path,
926
- agentId: resolved.agent,
927
- confirm: Boolean(options.yes),
928
- autoIndex: options.autoIndex !== false
929
- });
930
- print(options.json, {
931
- vault: resolved.vault,
932
- ...result
933
- }, () => `Deleted note ${result.relativePath}.`);
934
- });
935
- program
936
- .command('dedupe')
937
- .option('-v, --vault <vault>', 'vault directory')
938
- .option('-a, --agent <agent>', 'agent memory namespace')
939
- .option('-l, --limit <limit>', 'maximum duplicate candidate pairs')
940
- .option('--min-score <score>', 'minimum semantic similarity score between 0 and 1', '0.92')
941
- .option('--no-semantic', 'disable semantic duplicate detection and keep exact-content matching only')
942
- .option('--json', 'print machine-readable JSON')
943
- .description('detect possible duplicate notes with exact hash and semantic similarity scores')
944
- .action(async (options) => {
945
- const resolved = await resolveOptions(options);
946
- const duplicates = await scanDuplicateNotes(resolved.vault, {
947
- agentId: resolved.agent,
948
- limit: parsePositiveInteger(options.limit ?? '25', 25),
949
- minSemanticScore: parseScore(options.minScore, 0.92),
950
- includeSemantic: options.semantic !== false
951
- });
952
- print(options.json, { vault: resolved.vault, agent: resolved.agent, duplicates }, () => {
953
- if (duplicates.length === 0) {
954
- return 'No possible duplicates found.';
955
- }
956
- return duplicates
957
- .map((item, index) => `${index + 1}. [${item.kind}] score=${item.score.toFixed(4)} ${item.left.path} <-> ${item.right.path} (${item.reason})`)
958
- .join('\n');
959
- });
960
- });
961
- program
962
- .command('dedupe-resolve')
963
- .option('-v, --vault <vault>', 'vault directory')
964
- .option('--left <path>', 'left note relative path from dedupe result')
965
- .option('--right <path>', 'right note relative path from dedupe result')
966
- .option('--action <action>', 'resolution action: merge, link or ignore')
967
- .option('--no-auto-index', 'skip reindex after duplicate resolution')
968
- .option('--json', 'print machine-readable JSON')
969
- .description('resolve a duplicate candidate with merge, link or ignore')
970
- .action(async (options) => {
971
- const resolved = await resolveOptions(options);
972
- if (!options.left || !options.right) {
973
- throw new Error('Use --left <path> and --right <path> to resolve a duplicate pair.');
974
- }
975
- if (options.action !== 'merge' && options.action !== 'link' && options.action !== 'ignore') {
976
- throw new Error('Use --action merge|link|ignore.');
977
- }
978
- const result = await resolveDuplicateNotes(resolved.vault, {
979
- leftPath: options.left,
980
- rightPath: options.right,
981
- action: options.action,
982
- autoIndex: options.autoIndex !== false
983
- });
984
- print(options.json, {
985
- vault: resolved.vault,
986
- ...result
987
- }, () => `Resolved duplicate (${result.action}) for ${result.leftPath} <-> ${result.rightPath}`);
988
- });
989
- program
990
- .command('index')
991
- .option('-v, --vault <vault>', 'vault directory')
992
- .option('--full', 'force a complete reindex from Markdown source without reusing unchanged index entries')
993
- .option('--json', 'print machine-readable JSON')
994
- .description('index markdown notes, links, tags and chunks')
995
- .action(async (options) => {
996
- const resolved = await resolveOptions(options);
997
- const result = await indexVault(resolved.vault, {
998
- full: options.full === true
999
- });
1000
- print(options.json, result, () => options.full === true
1001
- ? `Fully reindexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`
1002
- : `Indexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`);
1003
- });
1004
- program
1005
- .command('bench')
1006
- .option('-v, --vault <vault>', 'vault directory')
1007
- .option('-w, --watch', 'watch markdown changes and re-run benchmark in realtime')
1008
- .option('--debounce <ms>', 'watch debounce in milliseconds', '350')
1009
- .option('--json', 'print machine-readable JSON events')
1010
- .description('benchmark indexing in realtime, including compressed pack behavior')
1011
- .action(async (options) => {
1012
- const resolved = await resolveOptions(options);
1013
- const config = await loadBrainlinkConfig();
1014
- const emitProgress = (event) => {
1015
- printBenchRealtimeEvent(options.json, event);
1016
- };
1017
- const printBenchError = (error) => {
1018
- const message = error instanceof Error ? error.message : String(error);
1019
- print(options.json, { event: 'bench-error', message }, () => `[bench] error ${message}`);
1020
- };
1021
- const runAndPrint = async (trigger) => {
1022
- const baseline = await readBenchHistory(resolved.vault);
1023
- const result = await indexVaultWithOptions(resolved.vault, {
1024
- onProgress: emitProgress
1025
- });
1026
- printBenchSummary(options.json, trigger, resolved.vault, result);
1027
- const guardrails = evaluateBenchGuardrails(config, result, baseline);
1028
- printBenchGuardrails(options.json, resolved.vault, config, guardrails);
1029
- await writeBenchHistory(resolved.vault, result);
1030
- return result;
1031
- };
1032
- if (!options.watch) {
1033
- await runAndPrint('manual');
1034
- return;
1035
- }
1036
- const debounceMs = parsePositiveInteger(options.debounce ?? '350', 350);
1037
- await runAndPrint('manual');
1038
- print(options.json, {
1039
- event: 'bench-watching',
1040
- vault: resolved.vault,
1041
- debounceMs
1042
- }, () => `[bench] watching ${resolved.vault} (debounce=${debounceMs}ms)`);
1043
- const watcher = startVaultWatcher({
1044
- vaultPath: resolved.vault,
1045
- debounceMs,
1046
- onProgress: emitProgress,
1047
- onIndex: (result) => {
1048
- printBenchSummary(options.json, 'watch', resolved.vault, result);
1049
- },
1050
- onError: printBenchError
1051
- });
1052
- await new Promise((resolveSignal) => {
1053
- const shutdown = () => {
1054
- watcher.close();
1055
- resolveSignal();
1056
- };
1057
- process.once('SIGINT', shutdown);
1058
- process.once('SIGTERM', shutdown);
1059
- });
1060
- });
1061
- program
1062
- .command('doctor')
1063
- .option('-v, --vault <vault>', 'vault directory')
1064
- .option('--actionable', 'include prioritized commands for fixing or improving vault health')
1065
- .option('--json', 'print machine-readable JSON')
1066
- .description('run Brainlink environment and vault checks')
1067
- .action(async (options) => {
1068
- const resolved = await resolveOptions(options);
1069
- if (options.actionable) {
1070
- const report = await buildActionableDoctor(resolved.vault);
1071
- print(options.json, report, () => [
1072
- ...report.doctor.checks.map((check) => `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.message}`),
1073
- '',
1074
- 'Actionable next steps:',
1075
- ...report.actions.map((action) => `- [${action.severity}] ${action.command} (${action.reason})`)
1076
- ].join('\n'));
1077
- process.exitCode = report.doctor.ok ? 0 : 1;
1078
- return;
1079
- }
1080
- const report = await doctorVault(resolved.vault);
1081
- print(options.json, report, () => {
1082
- const checks = report.checks.map((check) => `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.message}`).join('\n');
1083
- const recommendations = report.recommendations && report.recommendations.length > 0
1084
- ? `\n\nRecommended next steps:\n${report.recommendations.map((step) => `- ${step}`).join('\n')}`
1085
- : '';
1086
- return `${checks}${recommendations}`;
1087
- });
1088
- process.exitCode = report.ok ? 0 : 1;
1089
- });
1090
- program
1091
- .command('pack-backup')
1092
- .option('-v, --vault <vault>', 'vault directory')
1093
- .option('-o, --output <path>', 'output file path (.blpkbak.gz)')
1094
- .option('--json', 'print machine-readable JSON')
1095
- .description('create offline backup with second-stage compression for encrypted search packs')
1096
- .action(async (options) => {
1097
- const resolved = await resolveOptions(options);
1098
- const outputPath = options.output?.trim().length
1099
- ? resolve(options.output)
1100
- : join(resolved.vault, '.brainlink', 'backups', `search-packs-${new Date().toISOString().replace(/[:.]/g, '-')}.blpkbak.gz`);
1101
- const backup = await createOfflinePackBackup({
1102
- vaultPath: resolved.vault,
1103
- outputPath
1104
- });
1105
- print(options.json, {
1106
- vault: resolved.vault,
1107
- backup
1108
- }, () => [
1109
- `Offline backup created: ${backup.outputPath}`,
1110
- `files=${backup.fileCount}`,
1111
- `input=${formatBytes(backup.inputBytes)} output=${formatBytes(backup.outputBytes)} saved=${((1 - backup.ratio) * 100).toFixed(2)}%`
1112
- ].join('\n'));
1113
- });
1114
- program
1115
- .command('watch')
1116
- .option('-v, --vault <vault>', 'vault directory')
1117
- .option('--json', 'print machine-readable JSON events')
1118
- .description('watch markdown files and reindex on changes')
1119
- .action(async (options) => {
1120
- const resolved = await resolveOptions(options);
1121
- const initial = await indexVault(resolved.vault);
1122
- const watcher = startVaultWatcher({
1123
- vaultPath: resolved.vault,
1124
- onIndex: (result) => {
1125
- print(options.json, { event: 'indexed', result }, () => `Indexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`);
1126
- },
1127
- onError: (error) => {
1128
- const message = error instanceof Error ? error.message : String(error);
1129
- print(options.json, { event: 'error', message }, () => message);
1130
- }
1131
- });
1132
- print(options.json, { event: 'watching', vault: resolved.vault, initial }, () => `Watching ${resolved.vault}`);
1133
- process.once('SIGINT', () => {
1134
- watcher.close();
1135
- process.exit(0);
1136
- });
1137
- process.once('SIGTERM', () => {
1138
- watcher.close();
1139
- process.exit(0);
1140
- });
1141
- });
1142
- program
1143
- .command('server')
1144
- .option('-v, --vault <vault>', 'vault directory')
1145
- .option('-h, --host <host>', 'server host', '127.0.0.1')
1146
- .option('-p, --port <port>', 'server port', '4321')
1147
- .option('--no-index', 'skip indexing before starting the server')
1148
- .option('--no-open', 'do not open the graph UI automatically')
1149
- .option('-w, --watch', 'watch markdown files and reindex on changes', true)
1150
- .option('--no-watch', 'disable markdown file watching')
1151
- .option('--json', 'print machine-readable JSON')
1152
- .description('start a local web UI for the knowledge graph')
1153
- .action(async (options) => {
1154
- const resolved = await resolveOptions(options);
1155
- const shouldWatch = options.watch !== false && !isBucketVaultPath(resolved.vault);
1156
- const server = await startServer({
1157
- vaultPath: resolved.vault,
1158
- host: options.host ?? resolved.config.host,
1159
- port: parsePositiveInteger(options.port ?? String(resolved.config.port), resolved.config.port),
1160
- shouldIndex: options.index,
1161
- shouldWatch
1162
- });
1163
- const openResult = options.open !== false ? openUrlInUi(server.url, process.pid) : { opened: false, mode: 'none' };
1164
- print(options.json, {
1165
- url: server.url,
1166
- watch: shouldWatch,
1167
- readonly: true,
1168
- openedUi: openResult.opened,
1169
- openMode: openResult.mode
1170
- }, () => `Brainlink graph server running at ${server.url} (${shouldWatch ? 'watching for changes' : 'watch disabled'})${openResult.opened
1171
- ? openResult.mode === 'native-gui'
1172
- ? ' (opened in native desktop GUI)'
1173
- : openResult.mode === 'app-window'
1174
- ? ' (opened in dedicated app window)'
1175
- : ' (opened in browser)'
1176
- : options.open === false
1177
- ? ' (auto-open disabled)'
1178
- : ''}`);
1179
- });
1180
- program
1181
- .command('mcp-server')
1182
- .option('-v, --vault <vault>', 'vault directory')
1183
- .option('-a, --agent <agent>', 'agent memory namespace')
1184
- .option('-h, --host <host>', 'remote MCP server host', '0.0.0.0')
1185
- .option('-p, --port <port>', 'remote MCP server port', '3333')
1186
- .option('--path <path>', 'remote MCP endpoint path', '/mcp')
1187
- .option('--token <token>', 'bearer token required for MCP requests')
1188
- .option('--no-index', 'skip indexing before starting the MCP server')
1189
- .option('--json', 'print machine-readable JSON')
1190
- .description('start a remote MCP server for centralized cluster access')
1191
- .action(async (options) => {
1192
- const resolved = await resolveOptions(options);
1193
- const token = options.token ?? process.env.BRAINLINK_MCP_TOKEN;
1194
- const server = await startRemoteMcpServer({
1195
- vaultPath: resolved.vault,
1196
- agent: resolved.agent,
1197
- host: options.host ?? '0.0.0.0',
1198
- port: parsePositiveInteger(options.port ?? '3333', 3333),
1199
- path: options.path ?? '/mcp',
1200
- token,
1201
- shouldIndex: options.index
1202
- });
1203
- print(options.json, {
1204
- url: server.url,
1205
- healthUrl: server.healthUrl,
1206
- readyUrl: server.readyUrl,
1207
- vault: resolved.vault,
1208
- agent: resolved.agent ?? '*',
1209
- auth: token === undefined ? 'disabled' : 'bearer'
1210
- }, () => `Brainlink remote MCP server running at ${server.url} (health: ${server.healthUrl}, readiness: ${server.readyUrl})`);
1211
- });
1212
- program
1213
- .command('quickstart')
1214
- .option('-v, --vault <vault>', 'vault directory')
1215
- .option('-a, --agent <agent>', 'agent memory namespace')
1216
- .option('--query <query>', 'optional task query to return immediate grounded context')
1217
- .option('--mode <mode>', 'search mode for context (fts|semantic|hybrid)')
1218
- .option('--strategy <strategy>', 'context strategy for context (rag|cag|auto)')
1219
- .option('--limit <limit>', 'maximum context sections')
1220
- .option('--tokens <tokens>', 'maximum context token budget')
1221
- .option('--no-install-agent', 'skip agent MCP/plugin installation and upgrade automation')
1222
- .option('--mcp-only', 'when installing agent integration, only configure MCP section')
1223
- .option('--plugin-path <path>', 'custom source path for Brainlink plugin files')
1224
- .option('--allowed-vaults <paths>', 'comma separated vault allowlist to inject in MCP env')
1225
- .option('--brainlink-home <path>', 'BRAINLINK_HOME value to inject in MCP env')
1226
- .option('--json', 'print machine-readable JSON')
1227
- .description('run plug-and-play setup for vault, MCP integration and bootstrap readiness')
1228
- .action(async (options) => {
1229
- const resolved = await resolveOptions(options);
1230
- const limit = parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit);
1231
- const tokens = parsePositiveInteger(options.tokens ?? String(resolved.defaults.defaultContextTokens), resolved.defaults.defaultContextTokens);
1232
- const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
1233
- const strategy = sanitizeContextStrategy(options.strategy, resolved.defaults.defaultContextStrategy);
1234
- const index = await indexVault(resolved.vault);
1235
- const stats = await getStats(resolved.vault, resolved.agent);
1236
- const validation = await validateVault(resolved.vault, resolved.agent);
1237
- const doctor = await doctorVault(resolved.vault);
1238
- const session = await touchBootstrapSession(resolved.vault, resolved.agent);
1239
- const policy = await getBootstrapPolicy();
1240
- const bootstrapStatus = await getBootstrapSessionStatus(resolved.vault, resolved.agent);
1241
- const context = options.query
1242
- ? await buildContextPackage(resolved.vault, options.query, limit, tokens, resolved.agent, mode, strategy, resolved.defaults.defaultContextCacheTtlMs)
1243
- : null;
1244
- const agentIntegration = options.installAgent === false
1245
- ? null
1246
- : await installAgentIntegration({
1247
- mcpOnly: options.mcpOnly,
1248
- pluginPath: options.pluginPath,
1249
- allowedVaults: options.allowedVaults,
1250
- brainlinkHome: options.brainlinkHome,
1251
- selfTest: true
1252
- });
1253
- const nextActions = stats.documentCount === 0
1254
- ? [
1255
- {
1256
- priority: 'required',
1257
- command: `blink add "Architecture" --vault "${resolved.vault}" --content "Durable memory with [[Links]] and #tags."`,
1258
- reason: 'Seed your vault with at least one durable Markdown note.'
1259
- },
1260
- {
1261
- priority: 'required',
1262
- command: `blink index --vault "${resolved.vault}"`,
1263
- reason: 'Rebuild index after adding notes so retrieval can find new memory.'
1264
- }
1265
- ]
1266
- : options.query
1267
- ? [
1268
- {
1269
- priority: 'recommended',
1270
- command: `blink add "Task Update" --vault "${resolved.vault}" --agent "${resolved.agent ?? 'shared'}" --content "<durable memory>"`,
1271
- reason: 'Persist important findings as Markdown notes after using the returned context.'
1272
- }
1273
- ]
1274
- : [
1275
- {
1276
- priority: 'recommended',
1277
- command: `blink context "<task>" --vault "${resolved.vault}" --agent "${resolved.agent ?? 'shared'}" --mode ${mode}`,
1278
- reason: 'Retrieve grounded context for each task before responding.'
1279
- }
1280
- ];
1281
- print(options.json, {
1282
- vault: resolved.vault,
1283
- agent: resolved.agent ?? 'shared',
1284
- mode,
1285
- index,
1286
- stats,
1287
- validation,
1288
- doctor,
1289
- policy,
1290
- bootstrapStatus,
1291
- session,
1292
- context,
1293
- agentIntegration,
1294
- nextActions
1295
- }, () => [
1296
- `quickstart vault=${resolved.vault}`,
1297
- `agent=${resolved.agent ?? 'shared'}`,
1298
- `documents=${stats.documentCount}`,
1299
- `links=${stats.linkCount}`,
1300
- `bootstrapReady=${bootstrapStatus.ready}`,
1301
- ...(agentIntegration?.selfTest ? [`agentIntegrationSelfTest=${agentIntegration.selfTest.ok}`] : []),
1302
- ...(nextActions.length > 0 ? ['Next actions:', ...nextActions.map((step) => `- ${step.command}`)] : [])
1303
- ].join('\n'));
1304
- });
8
+ registerVaultLifecycleCommands(program);
9
+ registerLinkCommands(program);
10
+ registerNoteCommands(program);
11
+ registerDedupeCommands(program);
12
+ registerIndexCommands(program);
13
+ registerServerCommands(program);
1305
14
  };