@andespindola/brainlink 0.1.0-beta.23 → 0.1.0-beta.25
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
|
|
@@ -201,7 +201,9 @@ const createLayout = graph => {
|
|
|
201
201
|
const nodes = graph.nodes.map(node => ({
|
|
202
202
|
...node,
|
|
203
203
|
x: Number.isFinite(node.x) ? node.x : 0,
|
|
204
|
-
y: Number.isFinite(node.y) ? node.y : 0
|
|
204
|
+
y: Number.isFinite(node.y) ? node.y : 0,
|
|
205
|
+
vx: Number.isFinite(node.vx) ? node.vx : 0,
|
|
206
|
+
vy: Number.isFinite(node.vy) ? node.vy : 0
|
|
205
207
|
}))
|
|
206
208
|
const nodeMap = new Map(nodes.map(node => [node.id, node]))
|
|
207
209
|
const edges = graph.edges
|
|
@@ -296,6 +298,10 @@ const tick = delta => {
|
|
|
296
298
|
edges.forEach(edge => {
|
|
297
299
|
const source = edge.sourceNode
|
|
298
300
|
const target = edge.targetNode
|
|
301
|
+
source.vx = Number.isFinite(source.vx) ? source.vx : 0
|
|
302
|
+
source.vy = Number.isFinite(source.vy) ? source.vy : 0
|
|
303
|
+
target.vx = Number.isFinite(target.vx) ? target.vx : 0
|
|
304
|
+
target.vy = Number.isFinite(target.vy) ? target.vy : 0
|
|
299
305
|
const dx = target.x - source.x
|
|
300
306
|
const dy = target.y - source.y
|
|
301
307
|
const distance = Math.max(Math.hypot(dx, dy), 1)
|
|
@@ -312,6 +318,10 @@ const tick = delta => {
|
|
|
312
318
|
for (let j = i + 1; j < nodes.length; j += 1) {
|
|
313
319
|
const a = nodes[i]
|
|
314
320
|
const b = nodes[j]
|
|
321
|
+
a.vx = Number.isFinite(a.vx) ? a.vx : 0
|
|
322
|
+
a.vy = Number.isFinite(a.vy) ? a.vy : 0
|
|
323
|
+
b.vx = Number.isFinite(b.vx) ? b.vx : 0
|
|
324
|
+
b.vy = Number.isFinite(b.vy) ? b.vy : 0
|
|
315
325
|
const dx = b.x - a.x
|
|
316
326
|
const dy = b.y - a.y
|
|
317
327
|
const distance = Math.max(Math.hypot(dx, dy), 1)
|
|
@@ -326,6 +336,10 @@ const tick = delta => {
|
|
|
326
336
|
}
|
|
327
337
|
|
|
328
338
|
nodes.forEach(node => {
|
|
339
|
+
node.vx = Number.isFinite(node.vx) ? node.vx : 0
|
|
340
|
+
node.vy = Number.isFinite(node.vy) ? node.vy : 0
|
|
341
|
+
node.x = Number.isFinite(node.x) ? node.x : 0
|
|
342
|
+
node.y = Number.isFinite(node.y) ? node.y : 0
|
|
329
343
|
if (state.pointer.dragNode === node) {
|
|
330
344
|
node.vx = 0
|
|
331
345
|
node.vy = 0
|
|
@@ -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')
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -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
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -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