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