@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
@@ -0,0 +1,270 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, join, relative, resolve } from 'node:path';
3
+ import { buildContextPackage } from '../../../application/build-context.js';
4
+ import { importLegacySqliteDatabase } from '../../../application/import-legacy-sqlite.js';
5
+ import { indexVault } from '../../../application/index-vault.js';
6
+ import { migrateVaultContent, planVaultMigration, previewVaultMigration, shouldMigrateDefaultVault } from '../../../application/migrate-vault.js';
7
+ import { createOfflinePackBackup } from '../../../application/offline-pack-backup.js';
8
+ import { doctorVault, getStats, validateVault } from '../../../application/analyze-vault.js';
9
+ import { buildActionableDoctor } from '../../../application/operational-workflows.js';
10
+ import { defaultBrainlinkConfig, sanitizeContextStrategy, sanitizeSearchMode } from '../../../infrastructure/config.js';
11
+ import { loadBrainlinkConfig } from '../../../infrastructure/config.js';
12
+ import { assertVaultAllowed, ensureVault } from '../../../infrastructure/file-system-vault.js';
13
+ import { getBootstrapPolicy, getBootstrapSessionStatus, touchBootstrapSession } from '../../../infrastructure/session-state.js';
14
+ import { installAgentIntegration } from '../agent-commands.js';
15
+ import { parsePositiveInteger, print, resolveOptions } from '../../runtime.js';
16
+ import { formatBytes } from './shared.js';
17
+ export const registerVaultLifecycleCommands = (program) => {
18
+ program
19
+ .command('init')
20
+ .argument('[vault]', 'vault directory')
21
+ .option('--migrate-from <vault>', 'copy existing vault content into the initialized vault')
22
+ .option('--no-migrate-existing', 'skip automatic migration from the default Brainlink vault into an empty custom vault')
23
+ .option('--json', 'print machine-readable JSON')
24
+ .description('initialize a Brainlink vault')
25
+ .action(async (vault, options) => {
26
+ const config = await loadBrainlinkConfig();
27
+ const targetVault = assertVaultAllowed(vault ?? config.vault, config.allowedVaults);
28
+ const path = await ensureVault(targetVault);
29
+ const explicitSource = options.migrateFrom ? assertVaultAllowed(options.migrateFrom, config.allowedVaults) : undefined;
30
+ const shouldAutoMigrate = explicitSource === undefined &&
31
+ options.migrateExisting !== false &&
32
+ (await shouldMigrateDefaultVault(defaultBrainlinkConfig.vault, targetVault));
33
+ const migration = explicitSource || shouldAutoMigrate ? await migrateVaultContent(explicitSource ?? defaultBrainlinkConfig.vault, targetVault) : undefined;
34
+ const index = migration && migration.copied + migration.conflicted > 0 ? await indexVault(targetVault) : undefined;
35
+ print(options.json, { path, ...(migration ? { migration } : {}), ...(index ? { index } : {}) }, () => {
36
+ const migrated = migration
37
+ ? ` Migrated ${migration.copied} files, preserved ${migration.conflicted} conflicts and kept ${migration.unchanged} unchanged files.`
38
+ : '';
39
+ return `Initialized Brainlink vault at ${path}.${migrated}`;
40
+ });
41
+ });
42
+ program
43
+ .command('migrate-vault')
44
+ .option('--from <vault>', 'source vault path')
45
+ .option('--to <vault>', 'target vault path')
46
+ .option('--dry-run', 'preview migration without writing files')
47
+ .option('--report <path>', 'write detailed per-file migration report to JSON file')
48
+ .option('--no-index', 'skip reindexing target vault after migration')
49
+ .option('--json', 'print machine-readable JSON')
50
+ .description('copy markdown memory from one vault to another with conflict preservation')
51
+ .action(async (options) => {
52
+ const config = await loadBrainlinkConfig();
53
+ const sourceVault = assertVaultAllowed(options.from ?? config.vault, config.allowedVaults);
54
+ const targetVault = assertVaultAllowed(options.to ?? defaultBrainlinkConfig.vault, config.allowedVaults);
55
+ const sourceRoot = await ensureVault(sourceVault);
56
+ const targetRoot = await ensureVault(targetVault);
57
+ const preview = await previewVaultMigration(sourceVault, targetVault);
58
+ const actions = await planVaultMigration(sourceRoot, targetRoot);
59
+ const reportEntries = actions.map((action) => ({
60
+ kind: action.kind,
61
+ sourcePath: action.sourcePath,
62
+ sourceRelativePath: relative(sourceRoot, action.sourcePath),
63
+ targetPath: action.targetPath,
64
+ targetRelativePath: relative(targetRoot, action.targetPath)
65
+ }));
66
+ const writeReport = async () => {
67
+ if (!options.report) {
68
+ return null;
69
+ }
70
+ const reportPath = resolve(options.report);
71
+ await mkdir(dirname(reportPath), { recursive: true });
72
+ await writeFile(reportPath, `${JSON.stringify({ source: sourceVault, target: targetVault, summary: preview, entries: reportEntries }, null, 2)}\n`, 'utf8');
73
+ return reportPath;
74
+ };
75
+ if (options.dryRun) {
76
+ const reportPath = await writeReport();
77
+ 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}` : ''}`);
78
+ return;
79
+ }
80
+ const migration = await migrateVaultContent(sourceVault, targetVault);
81
+ const shouldIndex = options.index !== false && migration.copied + migration.conflicted > 0;
82
+ const index = shouldIndex ? await indexVault(targetVault) : undefined;
83
+ const reportPath = await writeReport();
84
+ print(options.json, { dryRun: false, ...migration, entries: reportEntries, ...(index ? { index } : {}), ...(reportPath ? { reportPath } : {}) }, () => {
85
+ const summary = `Migrated ${migration.copied} files, preserved ${migration.conflicted} conflicts and kept ${migration.unchanged} unchanged files.`;
86
+ const indexMessage = index
87
+ ? ` Indexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} links.`
88
+ : '';
89
+ const reportMessage = reportPath ? ` Report written to ${reportPath}.` : '';
90
+ return `${summary}${indexMessage}${reportMessage}`;
91
+ });
92
+ });
93
+ program
94
+ .command('db-import')
95
+ .option('-v, --vault <vault>', 'vault directory')
96
+ .option('--db <path>', 'legacy SQLite database path (default: <vault>/.brainlink/brainlink.db)')
97
+ .option('--table <name>', 'legacy table name override')
98
+ .option('-a, --agent <agent>', 'force imported notes into a target agent namespace')
99
+ .option('-l, --limit <limit>', 'maximum number of rows to import')
100
+ .option('--dry-run', 'preview import without writing Markdown files')
101
+ .option('--no-index', 'skip reindexing after import')
102
+ .option('--json', 'print machine-readable JSON')
103
+ .description('import legacy SQLite memory into Markdown vault and current index model')
104
+ .action(async (options) => {
105
+ const resolved = await resolveOptions(options);
106
+ const result = await importLegacySqliteDatabase(resolved.vault, {
107
+ dbPath: options.db,
108
+ table: options.table,
109
+ agentOverride: options.agent ? resolved.agent : undefined,
110
+ limit: options.limit ? parsePositiveInteger(options.limit, 100_000) : undefined,
111
+ dryRun: Boolean(options.dryRun)
112
+ });
113
+ const shouldIndex = options.index !== false && !result.dryRun && result.imported > 0;
114
+ const index = shouldIndex ? await indexVault(resolved.vault) : undefined;
115
+ print(options.json, { ...result, ...(index ? { index } : {}) }, () => {
116
+ const summary = `Imported ${result.imported}/${result.rowsRead} rows from ${result.table} (skipped ${result.skipped}).`;
117
+ const indexMessage = index
118
+ ? ` Indexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} links.`
119
+ : '';
120
+ const dryRunMessage = result.dryRun ? ' Dry run only; no files were written.' : '';
121
+ return `${summary}${indexMessage}${dryRunMessage}`;
122
+ });
123
+ });
124
+ program
125
+ .command('doctor')
126
+ .option('-v, --vault <vault>', 'vault directory')
127
+ .option('--actionable', 'include prioritized commands for fixing or improving vault health')
128
+ .option('--json', 'print machine-readable JSON')
129
+ .description('run Brainlink environment and vault checks')
130
+ .action(async (options) => {
131
+ const resolved = await resolveOptions(options);
132
+ if (options.actionable) {
133
+ const report = await buildActionableDoctor(resolved.vault);
134
+ print(options.json, report, () => [
135
+ ...report.doctor.checks.map((check) => `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.message}`),
136
+ '',
137
+ 'Actionable next steps:',
138
+ ...report.actions.map((action) => `- [${action.severity}] ${action.command} (${action.reason})`)
139
+ ].join('\n'));
140
+ process.exitCode = report.doctor.ok ? 0 : 1;
141
+ return;
142
+ }
143
+ const report = await doctorVault(resolved.vault);
144
+ print(options.json, report, () => {
145
+ const checks = report.checks.map((check) => `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.message}`).join('\n');
146
+ const recommendations = report.recommendations && report.recommendations.length > 0
147
+ ? `\n\nRecommended next steps:\n${report.recommendations.map((step) => `- ${step}`).join('\n')}`
148
+ : '';
149
+ return `${checks}${recommendations}`;
150
+ });
151
+ process.exitCode = report.ok ? 0 : 1;
152
+ });
153
+ program
154
+ .command('pack-backup')
155
+ .option('-v, --vault <vault>', 'vault directory')
156
+ .option('-o, --output <path>', 'output file path (.blpkbak.gz)')
157
+ .option('--json', 'print machine-readable JSON')
158
+ .description('create offline backup with second-stage compression for encrypted search packs')
159
+ .action(async (options) => {
160
+ const resolved = await resolveOptions(options);
161
+ const outputPath = options.output?.trim().length
162
+ ? resolve(options.output)
163
+ : join(resolved.vault, '.brainlink', 'backups', `search-packs-${new Date().toISOString().replace(/[:.]/g, '-')}.blpkbak.gz`);
164
+ const backup = await createOfflinePackBackup({
165
+ vaultPath: resolved.vault,
166
+ outputPath
167
+ });
168
+ print(options.json, {
169
+ vault: resolved.vault,
170
+ backup
171
+ }, () => [
172
+ `Offline backup created: ${backup.outputPath}`,
173
+ `files=${backup.fileCount}`,
174
+ `input=${formatBytes(backup.inputBytes)} output=${formatBytes(backup.outputBytes)} saved=${((1 - backup.ratio) * 100).toFixed(2)}%`
175
+ ].join('\n'));
176
+ });
177
+ program
178
+ .command('quickstart')
179
+ .option('-v, --vault <vault>', 'vault directory')
180
+ .option('-a, --agent <agent>', 'agent memory namespace')
181
+ .option('--query <query>', 'optional task query to return immediate grounded context')
182
+ .option('--mode <mode>', 'search mode for context (fts|semantic|hybrid)')
183
+ .option('--strategy <strategy>', 'context strategy for context (rag|cag|auto)')
184
+ .option('--limit <limit>', 'maximum context sections')
185
+ .option('--tokens <tokens>', 'maximum context token budget')
186
+ .option('--no-install-agent', 'skip agent MCP/plugin installation and upgrade automation')
187
+ .option('--mcp-only', 'when installing agent integration, only configure MCP section')
188
+ .option('--plugin-path <path>', 'custom source path for Brainlink plugin files')
189
+ .option('--allowed-vaults <paths>', 'comma separated vault allowlist to inject in MCP env')
190
+ .option('--brainlink-home <path>', 'BRAINLINK_HOME value to inject in MCP env')
191
+ .option('--json', 'print machine-readable JSON')
192
+ .description('run plug-and-play setup for vault, MCP integration and bootstrap readiness')
193
+ .action(async (options) => {
194
+ const resolved = await resolveOptions(options);
195
+ const limit = parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit);
196
+ const tokens = parsePositiveInteger(options.tokens ?? String(resolved.defaults.defaultContextTokens), resolved.defaults.defaultContextTokens);
197
+ const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
198
+ const strategy = sanitizeContextStrategy(options.strategy, resolved.defaults.defaultContextStrategy);
199
+ const index = await indexVault(resolved.vault);
200
+ const stats = await getStats(resolved.vault, resolved.agent);
201
+ const validation = await validateVault(resolved.vault, resolved.agent);
202
+ const doctor = await doctorVault(resolved.vault);
203
+ const session = await touchBootstrapSession(resolved.vault, resolved.agent);
204
+ const policy = await getBootstrapPolicy();
205
+ const bootstrapStatus = await getBootstrapSessionStatus(resolved.vault, resolved.agent);
206
+ const context = options.query
207
+ ? await buildContextPackage(resolved.vault, options.query, limit, tokens, resolved.agent, mode, strategy, resolved.defaults.defaultContextCacheTtlMs)
208
+ : null;
209
+ const agentIntegration = options.installAgent === false
210
+ ? null
211
+ : await installAgentIntegration({
212
+ mcpOnly: options.mcpOnly,
213
+ pluginPath: options.pluginPath,
214
+ allowedVaults: options.allowedVaults,
215
+ brainlinkHome: options.brainlinkHome,
216
+ selfTest: true
217
+ });
218
+ const nextActions = stats.documentCount === 0
219
+ ? [
220
+ {
221
+ priority: 'required',
222
+ command: `blink add "Architecture" --vault "${resolved.vault}" --content "Durable memory with [[Links]] and #tags."`,
223
+ reason: 'Seed your vault with at least one durable Markdown note.'
224
+ },
225
+ {
226
+ priority: 'required',
227
+ command: `blink index --vault "${resolved.vault}"`,
228
+ reason: 'Rebuild index after adding notes so retrieval can find new memory.'
229
+ }
230
+ ]
231
+ : options.query
232
+ ? [
233
+ {
234
+ priority: 'recommended',
235
+ command: `blink add "Task Update" --vault "${resolved.vault}" --agent "${resolved.agent ?? 'shared'}" --content "<durable memory>"`,
236
+ reason: 'Persist important findings as Markdown notes after using the returned context.'
237
+ }
238
+ ]
239
+ : [
240
+ {
241
+ priority: 'recommended',
242
+ command: `blink context "<task>" --vault "${resolved.vault}" --agent "${resolved.agent ?? 'shared'}" --mode ${mode}`,
243
+ reason: 'Retrieve grounded context for each task before responding.'
244
+ }
245
+ ];
246
+ print(options.json, {
247
+ vault: resolved.vault,
248
+ agent: resolved.agent ?? 'shared',
249
+ mode,
250
+ index,
251
+ stats,
252
+ validation,
253
+ doctor,
254
+ policy,
255
+ bootstrapStatus,
256
+ session,
257
+ context,
258
+ agentIntegration,
259
+ nextActions
260
+ }, () => [
261
+ `quickstart vault=${resolved.vault}`,
262
+ `agent=${resolved.agent ?? 'shared'}`,
263
+ `documents=${stats.documentCount}`,
264
+ `links=${stats.linkCount}`,
265
+ `bootstrapReady=${bootstrapStatus.ready}`,
266
+ ...(agentIntegration?.selfTest ? [`agentIntegrationSelfTest=${agentIntegration.selfTest.ok}`] : []),
267
+ ...(nextActions.length > 0 ? ['Next actions:', ...nextActions.map((step) => `- ${step.command}`)] : [])
268
+ ].join('\n'));
269
+ });
270
+ };