@andespindola/brainlink 0.1.0-beta.24 → 0.1.0-beta.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -668,6 +668,18 @@ blink migrate-vault --from ~/.brainlink/vault --to ./team-vault --report ./migra
668
668
  Runs explicit markdown migration between vaults while preserving conflicts as `.conflict-<timestamp>` files.
669
669
  Use `--dry-run` to preview `copied`, `conflicted` and `unchanged` counts before writing.
670
670
 
671
+ ### `db-import`
672
+
673
+ ```bash
674
+ blink db-import --vault ./team-vault
675
+ blink db-import --vault ./team-vault --db ./legacy/brainlink.db
676
+ blink db-import --vault ./team-vault --db ./legacy/brainlink.db --table legacy_notes --dry-run
677
+ ```
678
+
679
+ Imports durable memory from a legacy SQLite database into Markdown notes (`agents/<agent-id>/*.md`) and reindexes by default.
680
+ When `--db` is omitted, Brainlink auto-detects common legacy paths such as `<vault>/.brainlink/brainlink.db`.
681
+ Use `--agent <id>` to force all imported rows into one namespace, `--limit` for incremental imports, `--dry-run` to preview without writing files, and `--no-index` to defer reindexing.
682
+
671
683
  ### `init`
672
684
 
673
685
  ```bash
@@ -156,14 +156,14 @@ const graphBounds = nodes => {
156
156
  }
157
157
 
158
158
  const fitScaleBiasByNodeCount = nodeCount => {
159
- if (nodeCount <= 6) return 2.4
160
- if (nodeCount <= 20) return 1.9
161
- if (nodeCount <= 60) return 1.5
162
- if (nodeCount <= 180) return 1.25
163
- if (nodeCount <= 600) return 1.05
159
+ if (nodeCount <= 6) return 2.8
160
+ if (nodeCount <= 20) return 2.2
161
+ if (nodeCount <= 60) return 1.72
162
+ if (nodeCount <= 180) return 1.34
163
+ if (nodeCount <= 600) return 1.08
164
164
  if (nodeCount <= 2000) return 0.9
165
165
  if (nodeCount <= 6000) return 0.72
166
- return 0.62
166
+ return 0.58
167
167
  }
168
168
 
169
169
  const fitView = (options = { useFiltered: true }) => {
@@ -178,13 +178,34 @@ const fitView = (options = { useFiltered: true }) => {
178
178
  return
179
179
  }
180
180
 
181
- const padding = 100
181
+ const paddingByNodeCount = nodeCount => {
182
+ if (nodeCount <= 6) return 28
183
+ if (nodeCount <= 20) return 44
184
+ if (nodeCount <= 60) return 68
185
+ if (nodeCount <= 180) return 86
186
+ if (nodeCount <= 600) return 110
187
+ if (nodeCount <= 2000) return 140
188
+ return 180
189
+ }
190
+ const minFitScaleByNodeCount = nodeCount => {
191
+ if (nodeCount <= 6) return 2.4
192
+ if (nodeCount <= 20) return 1.8
193
+ if (nodeCount <= 60) return 1.2
194
+ if (nodeCount <= 180) return 0.86
195
+ if (nodeCount <= 600) return 0.58
196
+ if (nodeCount <= 2000) return 0.34
197
+ if (nodeCount <= 6000) return 0.2
198
+ return 0.13
199
+ }
200
+
201
+ const padding = paddingByNodeCount(nodes.length)
182
202
  const scaleX = width / (bounds.width + padding * 2)
183
203
  const scaleY = height / (bounds.height + padding * 2)
184
204
  const fitScale = clampScale(Math.min(scaleX, scaleY))
185
205
  const biasedScale = clampScale(fitScale * fitScaleBiasByNodeCount(nodes.length))
206
+ const minimumScale = minFitScaleByNodeCount(nodes.length)
186
207
  const minimumLargeGraphScale = nodes.length > largeGraphNodeThreshold ? 0.13 : zoomRange.min
187
- const scale = Math.max(biasedScale, minimumLargeGraphScale)
208
+ const scale = Math.max(biasedScale, minimumScale, minimumLargeGraphScale)
188
209
  const centerX = (bounds.minX + bounds.maxX) / 2
189
210
  const centerY = (bounds.minY + bounds.maxY) / 2
190
211
 
@@ -653,16 +674,17 @@ const zoomAtPoint = (screenX, screenY, factor) => {
653
674
 
654
675
  const wheelZoomFactor = event => {
655
676
  const isModifierZoom = event.metaKey || event.ctrlKey
656
- const delta = Math.abs(event.deltaY)
677
+ const deltaModeFactor = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? 120 : 1
678
+ const normalizedDelta = Math.min(Math.abs(event.deltaY * deltaModeFactor), 1200) / 220
657
679
 
658
- if (delta < 1) {
659
- return event.deltaY < 0 ? 1.04 : 0.96
680
+ if (normalizedDelta <= 0.0001) {
681
+ return 1
660
682
  }
661
683
 
662
- const zoomInFactor = isModifierZoom ? 1.12 : 1.08
663
- const zoomOutFactor = isModifierZoom ? 0.88 : 0.92
684
+ const sensitivity = isModifierZoom ? 0.2 : 0.14
685
+ const direction = event.deltaY < 0 ? 1 : -1
664
686
 
665
- return event.deltaY < 0 ? zoomInFactor : zoomOutFactor
687
+ return Math.exp(direction * normalizedDelta * sensitivity)
666
688
  }
667
689
 
668
690
  const bindEvents = () => {
@@ -749,6 +771,9 @@ const bindEvents = () => {
749
771
  state.pointer = { x: 0, y: 0, down: false, dragNode: null, moved: false }
750
772
  canvas.releasePointerCapture(event.pointerId)
751
773
  })
774
+ canvas.addEventListener('pointercancel', () => {
775
+ state.pointer = { x: 0, y: 0, down: false, dragNode: null, moved: false }
776
+ })
752
777
  }
753
778
 
754
779
  const loadAgents = async () => {
@@ -0,0 +1,280 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { access } from 'node:fs/promises';
3
+ import { basename, extname, join, relative, resolve } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { extractTags, extractWikiLinks } from '../domain/markdown.js';
6
+ import { sanitizeAgentId, sharedAgentId } from '../domain/agents.js';
7
+ import { ensureVault, listVaultFiles, writeMarkdownFile } from '../infrastructure/file-system-vault.js';
8
+ import { getBrainlinkHomePath } from '../infrastructure/paths.js';
9
+ const execFileAsync = promisify(execFile);
10
+ const fieldSeparator = '\u001f';
11
+ const rowSeparator = '\u001e';
12
+ const contentColumnCandidates = ['content', 'markdown', 'body', 'text', 'note'];
13
+ const titleColumnCandidates = ['title', 'note_title', 'name', 'headline'];
14
+ const pathColumnCandidates = ['path', 'file_path', 'filepath', 'source_path', 'source'];
15
+ const agentColumnCandidates = ['agent', 'agent_id', 'namespace', 'scope'];
16
+ const tagColumnCandidates = ['tags', 'tag_list', 'keywords'];
17
+ const createdColumnCandidates = ['created_at', 'createdat', 'created', 'ctime'];
18
+ const updatedColumnCandidates = ['updated_at', 'updatedat', 'updated', 'mtime'];
19
+ const systemHubTitle = 'Memory Hub';
20
+ const systemRootTitle = 'Knowledge Root';
21
+ const normalizeTitle = (title) => title.trim().replace(/\.md$/i, '').toLowerCase();
22
+ const slugify = (title) => title
23
+ .normalize('NFKD')
24
+ .replace(/[\u0300-\u036f]/g, '')
25
+ .toLowerCase()
26
+ .replace(/[^a-z0-9]+/g, '-')
27
+ .replace(/^-+|-+$/g, '');
28
+ const quoteIdentifier = (value) => `"${value.replaceAll('"', '""')}"`;
29
+ const pickColumn = (columns, candidates) => {
30
+ const byLower = new Map(columns.map((column) => [column.toLowerCase(), column]));
31
+ return candidates.map((candidate) => byLower.get(candidate)).find((column) => Boolean(column)) ?? null;
32
+ };
33
+ const parseDelimitedRows = (rawOutput) => {
34
+ const normalized = rawOutput.trim();
35
+ if (normalized.length === 0) {
36
+ return [];
37
+ }
38
+ return normalized
39
+ .split(rowSeparator)
40
+ .map((row) => row.trim())
41
+ .filter(Boolean)
42
+ .map((row) => row.split(fieldSeparator));
43
+ };
44
+ const runSqliteQuery = async (databasePath, sql) => {
45
+ try {
46
+ const { stdout } = await execFileAsync('sqlite3', ['-readonly', '-noheader', '-separator', fieldSeparator, '-newline', rowSeparator, databasePath, sql], { maxBuffer: 1024 * 1024 * 64 });
47
+ return parseDelimitedRows(stdout);
48
+ }
49
+ catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ const lower = message.toLowerCase();
52
+ if (lower.includes('enoent') || lower.includes('not found')) {
53
+ throw new Error('sqlite3 CLI was not found. Install sqlite3 to use db-import.');
54
+ }
55
+ throw new Error(`Unable to read SQLite database: ${message}`);
56
+ }
57
+ };
58
+ const detectLegacyDbPath = async (vaultPath, explicitPath) => {
59
+ if (explicitPath) {
60
+ return resolve(explicitPath);
61
+ }
62
+ const vaultRoot = await ensureVault(vaultPath);
63
+ const candidates = [
64
+ join(vaultRoot, '.brainlink', 'brainlink.db'),
65
+ join(vaultRoot, '.brainlink', 'index.db'),
66
+ join(getBrainlinkHomePath(), 'brainlink.db'),
67
+ join(getBrainlinkHomePath(), 'vault', '.brainlink', 'brainlink.db')
68
+ ];
69
+ for (const candidate of candidates) {
70
+ try {
71
+ await access(candidate);
72
+ return candidate;
73
+ }
74
+ catch { }
75
+ }
76
+ throw new Error(`No legacy SQLite database found. Checked: ${candidates.join(', ')}. Use --db <path-to-db> to import explicitly.`);
77
+ };
78
+ const listTables = async (dbPath) => {
79
+ const rows = await runSqliteQuery(dbPath, `SELECT name
80
+ FROM sqlite_master
81
+ WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
82
+ ORDER BY name`);
83
+ return rows.map((columns) => columns[0]).filter(Boolean);
84
+ };
85
+ const listColumns = async (dbPath, table) => {
86
+ const rows = await runSqliteQuery(dbPath, `PRAGMA table_info(${quoteIdentifier(table)})`);
87
+ return rows.map((columns) => columns[1]).filter(Boolean);
88
+ };
89
+ const tableScore = (columns) => {
90
+ const contentColumn = pickColumn(columns, contentColumnCandidates);
91
+ const titleColumn = pickColumn(columns, titleColumnCandidates);
92
+ const pathColumn = pickColumn(columns, pathColumnCandidates);
93
+ const agentColumn = pickColumn(columns, agentColumnCandidates);
94
+ return (contentColumn ? 6 : 0) + (titleColumn ? 4 : 0) + (pathColumn ? 2 : 0) + (agentColumn ? 1 : 0);
95
+ };
96
+ const detectTableMapping = async (dbPath, tableOverride) => {
97
+ const tables = await listTables(dbPath);
98
+ if (tables.length === 0) {
99
+ throw new Error('Legacy SQLite database has no readable tables.');
100
+ }
101
+ const mappings = await Promise.all(tables.map(async (table) => {
102
+ const columns = await listColumns(dbPath, table);
103
+ return {
104
+ table,
105
+ columns,
106
+ titleColumn: pickColumn(columns, titleColumnCandidates),
107
+ contentColumn: pickColumn(columns, contentColumnCandidates),
108
+ pathColumn: pickColumn(columns, pathColumnCandidates),
109
+ agentColumn: pickColumn(columns, agentColumnCandidates),
110
+ tagsColumn: pickColumn(columns, tagColumnCandidates),
111
+ createdColumn: pickColumn(columns, createdColumnCandidates),
112
+ updatedColumn: pickColumn(columns, updatedColumnCandidates),
113
+ score: tableScore(columns)
114
+ };
115
+ }));
116
+ if (tableOverride) {
117
+ const overridden = mappings.find((mapping) => mapping.table === tableOverride);
118
+ if (!overridden) {
119
+ throw new Error(`Table not found in SQLite database: ${tableOverride}`);
120
+ }
121
+ if (!overridden.contentColumn) {
122
+ throw new Error(`Table ${tableOverride} does not expose a readable content column.`);
123
+ }
124
+ return { mapping: overridden, detectedTables: tables };
125
+ }
126
+ const selected = [...mappings]
127
+ .filter((mapping) => mapping.contentColumn)
128
+ .sort((left, right) => right.score - left.score)[0];
129
+ if (!selected) {
130
+ throw new Error('Could not detect a legacy table with content column in SQLite database.');
131
+ }
132
+ return { mapping: selected, detectedTables: tables };
133
+ };
134
+ const hexExpression = (column) => column ? `hex(COALESCE(CAST(${quoteIdentifier(column)} AS BLOB), X''))` : `hex(X'')`;
135
+ const decodeHexUtf8 = (value) => value ? Buffer.from(value, 'hex').toString('utf8') : '';
136
+ const parseLegacyTags = (value) => Array.from(new Set(value
137
+ .split(/[\s,;|]+/)
138
+ .map((item) => item.trim().replace(/^#/, '').toLowerCase())
139
+ .filter((item) => /^[a-z0-9][a-z0-9_-]*$/i.test(item))));
140
+ const titleFromPath = (pathValue) => basename(pathValue).replace(extname(pathValue), '').replace(/[-_]+/g, ' ').trim();
141
+ const appendMissingTags = (content, tags) => {
142
+ if (tags.length === 0) {
143
+ return content;
144
+ }
145
+ const existingTags = new Set(extractTags(content).map((tag) => tag.toLowerCase()));
146
+ const missing = tags.filter((tag) => !existingTags.has(tag.toLowerCase()));
147
+ if (missing.length === 0) {
148
+ return content;
149
+ }
150
+ return `${content.trim()}\n\nTags: ${missing.map((tag) => `#${tag}`).join(' ')}`;
151
+ };
152
+ const buildNote = (title, content, agentId) => [
153
+ '---',
154
+ `title: "${title.replaceAll('"', '\\"')}"`,
155
+ `agent: "${agentId}"`,
156
+ '---',
157
+ '',
158
+ `# ${title}`,
159
+ '',
160
+ content.trim(),
161
+ ''
162
+ ].join('\n');
163
+ const parseLegacyRow = (columns, rowIndex) => {
164
+ const [titleHex, contentHex, pathHex, agentHex, tagsHex] = columns;
165
+ const content = decodeHexUtf8(contentHex).trim();
166
+ const path = decodeHexUtf8(pathHex).trim();
167
+ const titleCandidate = decodeHexUtf8(titleHex).trim();
168
+ const fallbackTitleFromPath = path ? titleFromPath(path) : '';
169
+ const title = titleCandidate || fallbackTitleFromPath || `Imported Memory ${rowIndex + 1}`;
170
+ return {
171
+ title,
172
+ content,
173
+ path,
174
+ agent: decodeHexUtf8(agentHex).trim(),
175
+ tags: parseLegacyTags(decodeHexUtf8(tagsHex))
176
+ };
177
+ };
178
+ const noteRelativePath = (agentId, slug, suffix = 0) => `agents/${agentId}/${suffix > 0 ? `${slug}-${suffix + 1}` : slug || 'untitled'}.md`;
179
+ const reserveUniquePath = (agentId, title, reserved) => {
180
+ const slug = slugify(title);
181
+ for (let suffix = 0; suffix < 10_000; suffix += 1) {
182
+ const relativePath = noteRelativePath(agentId, slug, suffix);
183
+ if (!reserved.has(relativePath)) {
184
+ reserved.add(relativePath);
185
+ return relativePath;
186
+ }
187
+ }
188
+ throw new Error(`Could not allocate unique path for imported note: ${title}`);
189
+ };
190
+ const ensureSystemNote = async (vaultPath, reserved, created, agentId, title, content, dryRun) => {
191
+ const filename = noteRelativePath(agentId, slugify(title));
192
+ if (reserved.has(filename)) {
193
+ return;
194
+ }
195
+ reserved.add(filename);
196
+ created.add(filename);
197
+ if (dryRun) {
198
+ return;
199
+ }
200
+ await writeMarkdownFile(vaultPath, filename, buildNote(title, content, agentId));
201
+ };
202
+ const applyConnectivityRule = async (vaultPath, reserved, created, title, content, agentId, dryRun) => {
203
+ const links = extractWikiLinks(content).filter((link) => normalizeTitle(link) !== normalizeTitle(title));
204
+ if (links.length > 0) {
205
+ return content.trim();
206
+ }
207
+ const normalized = normalizeTitle(title);
208
+ if (normalized === normalizeTitle(systemHubTitle)) {
209
+ await ensureSystemNote(vaultPath, reserved, created, agentId, systemRootTitle, `Entry point for agent memory. [[${systemHubTitle}]] #memory #root`, dryRun);
210
+ return `${content.trim()}\n\nRelated: [[${systemRootTitle}]]`;
211
+ }
212
+ await ensureSystemNote(vaultPath, reserved, created, agentId, systemHubTitle, 'Central memory index for this agent namespace. #memory #hub', dryRun);
213
+ return `${content.trim()}\n\nRelated: [[${systemHubTitle}]]`;
214
+ };
215
+ const importRowsFromMapping = async (vaultPath, dbPath, mapping, options, reserved) => {
216
+ const limit = Number.isFinite(options.limit) && (options.limit ?? 0) > 0 ? Math.floor(options.limit ?? 0) : undefined;
217
+ const sql = [
218
+ 'SELECT',
219
+ `${hexExpression(mapping.titleColumn)} AS title_hex,`,
220
+ `${hexExpression(mapping.contentColumn)} AS content_hex,`,
221
+ `${hexExpression(mapping.pathColumn)} AS path_hex,`,
222
+ `${hexExpression(mapping.agentColumn)} AS agent_hex,`,
223
+ `${hexExpression(mapping.tagsColumn)} AS tags_hex,`,
224
+ `${hexExpression(mapping.createdColumn)} AS created_hex,`,
225
+ `${hexExpression(mapping.updatedColumn)} AS updated_hex`,
226
+ `FROM ${quoteIdentifier(mapping.table)}`,
227
+ ...(limit ? [`LIMIT ${limit}`] : [])
228
+ ].join(' ');
229
+ const rows = await runSqliteQuery(dbPath, sql);
230
+ const createdSystemNotes = new Set();
231
+ const importedFiles = [];
232
+ let imported = 0;
233
+ let skipped = 0;
234
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
235
+ const row = parseLegacyRow(rows[rowIndex], rowIndex);
236
+ if (!row.content) {
237
+ skipped += 1;
238
+ continue;
239
+ }
240
+ const agentId = sanitizeAgentId(options.agentOverride || row.agent || sharedAgentId);
241
+ const filename = reserveUniquePath(agentId, row.title, reserved);
242
+ const mergedContent = appendMissingTags(row.content, row.tags);
243
+ const connectedContent = await applyConnectivityRule(vaultPath, reserved, createdSystemNotes, row.title, mergedContent, agentId, options.dryRun === true);
244
+ const note = buildNote(row.title, connectedContent, agentId);
245
+ if (options.dryRun !== true) {
246
+ await writeMarkdownFile(vaultPath, filename, note);
247
+ }
248
+ importedFiles.push(filename);
249
+ imported += 1;
250
+ }
251
+ return {
252
+ rowsRead: rows.length,
253
+ imported,
254
+ skipped,
255
+ createdSystemNotes: createdSystemNotes.size,
256
+ importedFiles
257
+ };
258
+ };
259
+ export const importLegacySqliteDatabase = async (vaultPath, options = {}) => {
260
+ const vault = await ensureVault(vaultPath);
261
+ const dbPath = await detectLegacyDbPath(vaultPath, options.dbPath);
262
+ const { mapping, detectedTables } = await detectTableMapping(dbPath, options.table);
263
+ const existingFiles = (await listVaultFiles(vaultPath))
264
+ .filter((path) => extname(path).toLowerCase() === '.md')
265
+ .map((path) => relative(vault, path));
266
+ const reserved = new Set(existingFiles);
267
+ const imported = await importRowsFromMapping(vaultPath, dbPath, mapping, options, reserved);
268
+ return {
269
+ vault,
270
+ dbPath,
271
+ table: mapping.table,
272
+ detectedTables,
273
+ rowsRead: imported.rowsRead,
274
+ imported: imported.imported,
275
+ skipped: imported.skipped,
276
+ createdSystemNotes: imported.createdSystemNotes,
277
+ dryRun: options.dryRun === true,
278
+ importedFiles: imported.importedFiles
279
+ };
280
+ };
@@ -3,6 +3,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { dirname, relative, resolve } from 'node:path';
4
4
  import { addNote } from '../../application/add-note.js';
5
5
  import { buildContextPackage } from '../../application/build-context.js';
6
+ import { importLegacySqliteDatabase } from '../../application/import-legacy-sqlite.js';
6
7
  import { indexVault } from '../../application/index-vault.js';
7
8
  import { migrateVaultContent, planVaultMigration, previewVaultMigration, shouldMigrateDefaultVault } from '../../application/migrate-vault.js';
8
9
  import { startServer } from '../../application/start-server.js';
@@ -99,6 +100,37 @@ export const registerWriteCommands = (program) => {
99
100
  return `${summary}${indexMessage}${reportMessage}`;
100
101
  });
101
102
  });
103
+ program
104
+ .command('db-import')
105
+ .option('-v, --vault <vault>', 'vault directory')
106
+ .option('--db <path>', 'legacy SQLite database path (default: <vault>/.brainlink/brainlink.db)')
107
+ .option('--table <name>', 'legacy table name override')
108
+ .option('-a, --agent <agent>', 'force imported notes into a target agent namespace')
109
+ .option('-l, --limit <limit>', 'maximum number of rows to import')
110
+ .option('--dry-run', 'preview import without writing Markdown files')
111
+ .option('--no-index', 'skip reindexing after import')
112
+ .option('--json', 'print machine-readable JSON')
113
+ .description('import legacy SQLite memory into Markdown vault and current index model')
114
+ .action(async (options) => {
115
+ const resolved = await resolveOptions(options);
116
+ const result = await importLegacySqliteDatabase(resolved.vault, {
117
+ dbPath: options.db,
118
+ table: options.table,
119
+ agentOverride: options.agent ? resolved.agent : undefined,
120
+ limit: options.limit ? parsePositiveInteger(options.limit, 100_000) : undefined,
121
+ dryRun: Boolean(options.dryRun)
122
+ });
123
+ const shouldIndex = options.index !== false && !result.dryRun && result.imported > 0;
124
+ const index = shouldIndex ? await indexVault(resolved.vault) : undefined;
125
+ print(options.json, { ...result, ...(index ? { index } : {}) }, () => {
126
+ const summary = `Imported ${result.imported}/${result.rowsRead} rows from ${result.table} (skipped ${result.skipped}).`;
127
+ const indexMessage = index
128
+ ? ` Indexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} links.`
129
+ : '';
130
+ const dryRunMessage = result.dryRun ? ' Dry run only; no files were written.' : '';
131
+ return `${summary}${indexMessage}${dryRunMessage}`;
132
+ });
133
+ });
102
134
  program
103
135
  .command('add')
104
136
  .argument('<title>', 'note title')
@@ -377,6 +377,18 @@ blink migrate-vault --from ~/.brainlink/vault --to ./team-vault --report ./migra
377
377
 
378
378
  Use `--dry-run` to preview `copied`, `conflicted`, `unchanged` before writing files.
379
379
 
380
+ ### Import Legacy SQLite DB
381
+
382
+ ```bash
383
+ blink db-import --vault ./team-vault
384
+ blink db-import --vault ./team-vault --db ./legacy/brainlink.db
385
+ blink db-import --vault ./team-vault --db ./legacy/brainlink.db --table legacy_notes --dry-run
386
+ ```
387
+
388
+ `db-import` migrates rows from legacy SQLite memory into Markdown notes in the current vault and indexes the result by default.
389
+ Without `--db`, Brainlink auto-detects common legacy database paths.
390
+ Use `--agent` to force namespace, `--limit` for staged migration, `--dry-run` to preview writes, and `--no-index` to postpone indexing.
391
+
380
392
  ### Install Agent Integration
381
393
 
382
394
  ```bash
@@ -102,3 +102,10 @@ S3 target:
102
102
  ```bash
103
103
  blink migrate-vault --from ~/.brainlink/vault --to "s3://my-memory-bucket/brainlink" --dry-run
104
104
  ```
105
+
106
+ Legacy SQLite import:
107
+
108
+ ```bash
109
+ blink db-import --vault ./team-vault
110
+ blink db-import --vault ./team-vault --db ./legacy/brainlink.db --dry-run
111
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.24",
3
+ "version": "0.1.0-beta.26",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",