@andespindola/brainlink 0.1.0-beta.9 → 0.1.0-beta.90
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 +3130 -166
- 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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const createClientWorkerJs = () => `const normalize = value => String(value || '')
|
|
2
|
+
.normalize('NFKD')
|
|
3
|
+
.replace(/\\p{Diacritic}/gu, '')
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
|
|
6
|
+
let nodeIndex = []
|
|
7
|
+
|
|
8
|
+
const toNodeIndex = nodes =>
|
|
9
|
+
(Array.isArray(nodes) ? nodes : [])
|
|
10
|
+
.map(node => {
|
|
11
|
+
const id = typeof node.id === 'string' ? node.id : ''
|
|
12
|
+
if (!id) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
const title = normalize(node.title)
|
|
16
|
+
const path = normalize(node.path)
|
|
17
|
+
const tags = Array.isArray(node.tags) ? node.tags.map(tag => normalize(tag)) : []
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
text: [title, path, ...tags].join(' ')
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
|
|
25
|
+
const scoreText = (text, query) => {
|
|
26
|
+
if (!query) return 0
|
|
27
|
+
if (!text.includes(query)) return 0
|
|
28
|
+
if (text.startsWith(query)) return 4
|
|
29
|
+
return 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const filterIds = (query, limit) => {
|
|
33
|
+
const normalizedQuery = normalize(query).trim()
|
|
34
|
+
if (!normalizedQuery) {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
const rows = []
|
|
38
|
+
for (let index = 0; index < nodeIndex.length; index += 1) {
|
|
39
|
+
const row = nodeIndex[index]
|
|
40
|
+
const score = scoreText(row.text, normalizedQuery)
|
|
41
|
+
if (score > 0) {
|
|
42
|
+
rows.push({ id: row.id, score })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
rows.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))
|
|
46
|
+
return rows.slice(0, Math.max(1, Number.isFinite(limit) ? limit : rows.length)).map(row => row.id)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
self.onmessage = event => {
|
|
50
|
+
const payload = event.data
|
|
51
|
+
if (!payload || typeof payload !== 'object') {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if (payload.type === 'load-nodes') {
|
|
55
|
+
nodeIndex = toNodeIndex(payload.nodes)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if (payload.type === 'filter') {
|
|
59
|
+
const token = payload.token
|
|
60
|
+
const ids = filterIds(payload.query, payload.limit)
|
|
61
|
+
self.postMessage({ type: 'filter-result', token, ids })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
self.postMessage({ type: 'ready' })
|
|
66
|
+
`;
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
2
3
|
import { createCauliflowerGraphLayout } from '../domain/graph-layout.js';
|
|
3
|
-
import {
|
|
4
|
+
import { indexStoragePath } from '../infrastructure/file-index.js';
|
|
5
|
+
import { getGraphSummary } from './get-graph-summary.js';
|
|
4
6
|
const graphLayoutCache = new Map();
|
|
7
|
+
const readDatabaseSignature = async (vaultPath) => {
|
|
8
|
+
try {
|
|
9
|
+
const info = await stat(indexStoragePath(vaultPath));
|
|
10
|
+
return `${Math.floor(info.mtimeMs)}:${info.size}`;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return '0:0';
|
|
14
|
+
}
|
|
15
|
+
};
|
|
5
16
|
const createGraphSignature = (graph) => {
|
|
6
17
|
const nodesSignature = graph.nodes.map((node) => `${node.id}|${node.agentId}|${node.title}|${node.path}`).join('\n');
|
|
7
18
|
const edgesSignature = graph.edges
|
|
@@ -12,18 +23,19 @@ const createGraphSignature = (graph) => {
|
|
|
12
23
|
.digest('hex');
|
|
13
24
|
};
|
|
14
25
|
export const getGraphLayout = async (vaultPath, agentId) => {
|
|
15
|
-
const
|
|
16
|
-
const signature = createGraphSignature(graph);
|
|
26
|
+
const databaseSignature = await readDatabaseSignature(vaultPath);
|
|
17
27
|
const cacheKey = `${vaultPath}:${agentId ?? ''}`;
|
|
18
28
|
const cached = graphLayoutCache.get(cacheKey);
|
|
19
|
-
if (cached?.
|
|
29
|
+
if (cached?.databaseSignature === databaseSignature) {
|
|
20
30
|
return {
|
|
21
|
-
signature,
|
|
31
|
+
signature: cached.signature,
|
|
22
32
|
layout: cached.layout
|
|
23
33
|
};
|
|
24
34
|
}
|
|
35
|
+
const graph = await getGraphSummary(vaultPath, agentId);
|
|
36
|
+
const signature = createGraphSignature(graph);
|
|
25
37
|
const layout = createCauliflowerGraphLayout(graph);
|
|
26
|
-
graphLayoutCache.set(cacheKey, { signature, layout });
|
|
38
|
+
graphLayoutCache.set(cacheKey, { databaseSignature, signature, layout });
|
|
27
39
|
return {
|
|
28
40
|
signature,
|
|
29
41
|
layout
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
|
+
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
3
|
+
export const getGraphNode = async (vaultPath, id, agentId) => {
|
|
4
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
5
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
6
|
+
try {
|
|
7
|
+
return await index.getGraphNode(id, agentId);
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
index.close();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
|
+
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
3
|
+
export const getGraphSummary = async (vaultPath, agentId) => {
|
|
4
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
5
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
6
|
+
try {
|
|
7
|
+
return await index.getGraphSummary(agentId);
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
index.close();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
|
-
import {
|
|
2
|
+
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
3
3
|
export const getGraph = async (vaultPath, agentId) => {
|
|
4
4
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
5
|
-
const index =
|
|
5
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
6
6
|
try {
|
|
7
|
-
return index.getGraph(agentId);
|
|
7
|
+
return await index.getGraph(agentId);
|
|
8
8
|
}
|
|
9
9
|
finally {
|
|
10
10
|
index.close();
|
|
@@ -0,0 +1,296 @@
|
|
|
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 { pathToFileURL } from 'node:url';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { extractTags, extractWikiLinks } from '../domain/markdown.js';
|
|
7
|
+
import { sanitizeAgentId, sharedAgentId } from '../domain/agents.js';
|
|
8
|
+
import { ensureVault, listVaultFiles, writeMarkdownFile } from '../infrastructure/file-system-vault.js';
|
|
9
|
+
import { getBrainlinkHomePath } from '../infrastructure/paths.js';
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const fieldSeparator = '\u001f';
|
|
12
|
+
const rowSeparator = '\u001e';
|
|
13
|
+
const contentColumnCandidates = ['content', 'markdown', 'body', 'text', 'note'];
|
|
14
|
+
const titleColumnCandidates = ['title', 'note_title', 'name', 'headline'];
|
|
15
|
+
const pathColumnCandidates = ['path', 'file_path', 'filepath', 'source_path', 'source'];
|
|
16
|
+
const agentColumnCandidates = ['agent', 'agent_id', 'namespace', 'scope'];
|
|
17
|
+
const tagColumnCandidates = ['tags', 'tag_list', 'keywords'];
|
|
18
|
+
const createdColumnCandidates = ['created_at', 'createdat', 'created', 'ctime'];
|
|
19
|
+
const updatedColumnCandidates = ['updated_at', 'updatedat', 'updated', 'mtime'];
|
|
20
|
+
const systemHubTitle = 'Memory Hub';
|
|
21
|
+
const systemRootTitle = 'Knowledge Root';
|
|
22
|
+
const normalizeTitle = (title) => title.trim().replace(/\.md$/i, '').toLowerCase();
|
|
23
|
+
const slugify = (title) => title
|
|
24
|
+
.normalize('NFKD')
|
|
25
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
28
|
+
.replace(/^-+|-+$/g, '');
|
|
29
|
+
const quoteIdentifier = (value) => `"${value.replaceAll('"', '""')}"`;
|
|
30
|
+
const pickColumn = (columns, candidates) => {
|
|
31
|
+
const byLower = new Map(columns.map((column) => [column.toLowerCase(), column]));
|
|
32
|
+
return candidates.map((candidate) => byLower.get(candidate)).find((column) => Boolean(column)) ?? null;
|
|
33
|
+
};
|
|
34
|
+
const parseDelimitedRows = (rawOutput) => {
|
|
35
|
+
const normalized = rawOutput.trim();
|
|
36
|
+
if (normalized.length === 0) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
return normalized
|
|
40
|
+
.split(rowSeparator)
|
|
41
|
+
.map((row) => row.trim())
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.map((row) => row.split(fieldSeparator));
|
|
44
|
+
};
|
|
45
|
+
const runSqliteQuery = async (databasePath, sql) => {
|
|
46
|
+
const baseArgs = ['-noheader', '-separator', fieldSeparator, '-newline', rowSeparator, '-cmd', '.timeout 5000'];
|
|
47
|
+
const runQuery = async (args) => {
|
|
48
|
+
const { stdout } = await execFileAsync('sqlite3', [...args, sql], { maxBuffer: 1024 * 1024 * 64 });
|
|
49
|
+
return parseDelimitedRows(stdout);
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
return await runQuery(['--readonly', ...baseArgs, databasePath]);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
const lower = message.toLowerCase();
|
|
57
|
+
if (lower.includes('enoent') || lower.includes('not found')) {
|
|
58
|
+
throw new Error('sqlite3 CLI was not found. Install sqlite3 to use db-import.');
|
|
59
|
+
}
|
|
60
|
+
if (lower.includes('database is locked') || lower.includes('(5)')) {
|
|
61
|
+
try {
|
|
62
|
+
const uri = pathToFileURL(databasePath);
|
|
63
|
+
uri.search = 'mode=ro&immutable=1';
|
|
64
|
+
return await runQuery(['-uri', ...baseArgs, uri.toString()]);
|
|
65
|
+
}
|
|
66
|
+
catch (fallbackError) {
|
|
67
|
+
const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
|
|
68
|
+
throw new Error(`Unable to read SQLite database (locked). Close writers (server/watch/mcp) or rerun with DB idle. Details: ${fallbackMessage}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Unable to read SQLite database: ${message}`);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const detectLegacyDbPath = async (vaultPath, explicitPath) => {
|
|
75
|
+
if (explicitPath) {
|
|
76
|
+
return resolve(explicitPath);
|
|
77
|
+
}
|
|
78
|
+
const vaultRoot = await ensureVault(vaultPath);
|
|
79
|
+
const candidates = [
|
|
80
|
+
join(vaultRoot, '.brainlink', 'brainlink.db'),
|
|
81
|
+
join(vaultRoot, '.brainlink', 'index.db'),
|
|
82
|
+
join(getBrainlinkHomePath(), 'brainlink.db'),
|
|
83
|
+
join(getBrainlinkHomePath(), 'vault', '.brainlink', 'brainlink.db')
|
|
84
|
+
];
|
|
85
|
+
for (const candidate of candidates) {
|
|
86
|
+
try {
|
|
87
|
+
await access(candidate);
|
|
88
|
+
return candidate;
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`No legacy SQLite database found. Checked: ${candidates.join(', ')}. Use --db <path-to-db> to import explicitly.`);
|
|
93
|
+
};
|
|
94
|
+
const listTables = async (dbPath) => {
|
|
95
|
+
const rows = await runSqliteQuery(dbPath, `SELECT name
|
|
96
|
+
FROM sqlite_master
|
|
97
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
98
|
+
ORDER BY name`);
|
|
99
|
+
return rows.map((columns) => columns[0]).filter(Boolean);
|
|
100
|
+
};
|
|
101
|
+
const listColumns = async (dbPath, table) => {
|
|
102
|
+
const rows = await runSqliteQuery(dbPath, `PRAGMA table_info(${quoteIdentifier(table)})`);
|
|
103
|
+
return rows.map((columns) => columns[1]).filter(Boolean);
|
|
104
|
+
};
|
|
105
|
+
const tableScore = (columns) => {
|
|
106
|
+
const contentColumn = pickColumn(columns, contentColumnCandidates);
|
|
107
|
+
const titleColumn = pickColumn(columns, titleColumnCandidates);
|
|
108
|
+
const pathColumn = pickColumn(columns, pathColumnCandidates);
|
|
109
|
+
const agentColumn = pickColumn(columns, agentColumnCandidates);
|
|
110
|
+
return (contentColumn ? 6 : 0) + (titleColumn ? 4 : 0) + (pathColumn ? 2 : 0) + (agentColumn ? 1 : 0);
|
|
111
|
+
};
|
|
112
|
+
const detectTableMapping = async (dbPath, tableOverride) => {
|
|
113
|
+
const tables = await listTables(dbPath);
|
|
114
|
+
if (tables.length === 0) {
|
|
115
|
+
throw new Error('Legacy SQLite database has no readable tables.');
|
|
116
|
+
}
|
|
117
|
+
const mappings = await Promise.all(tables.map(async (table) => {
|
|
118
|
+
const columns = await listColumns(dbPath, table);
|
|
119
|
+
return {
|
|
120
|
+
table,
|
|
121
|
+
columns,
|
|
122
|
+
titleColumn: pickColumn(columns, titleColumnCandidates),
|
|
123
|
+
contentColumn: pickColumn(columns, contentColumnCandidates),
|
|
124
|
+
pathColumn: pickColumn(columns, pathColumnCandidates),
|
|
125
|
+
agentColumn: pickColumn(columns, agentColumnCandidates),
|
|
126
|
+
tagsColumn: pickColumn(columns, tagColumnCandidates),
|
|
127
|
+
createdColumn: pickColumn(columns, createdColumnCandidates),
|
|
128
|
+
updatedColumn: pickColumn(columns, updatedColumnCandidates),
|
|
129
|
+
score: tableScore(columns)
|
|
130
|
+
};
|
|
131
|
+
}));
|
|
132
|
+
if (tableOverride) {
|
|
133
|
+
const overridden = mappings.find((mapping) => mapping.table === tableOverride);
|
|
134
|
+
if (!overridden) {
|
|
135
|
+
throw new Error(`Table not found in SQLite database: ${tableOverride}`);
|
|
136
|
+
}
|
|
137
|
+
if (!overridden.contentColumn) {
|
|
138
|
+
throw new Error(`Table ${tableOverride} does not expose a readable content column.`);
|
|
139
|
+
}
|
|
140
|
+
return { mapping: overridden, detectedTables: tables };
|
|
141
|
+
}
|
|
142
|
+
const selected = [...mappings]
|
|
143
|
+
.filter((mapping) => mapping.contentColumn)
|
|
144
|
+
.sort((left, right) => right.score - left.score)[0];
|
|
145
|
+
if (!selected) {
|
|
146
|
+
throw new Error('Could not detect a legacy table with content column in SQLite database.');
|
|
147
|
+
}
|
|
148
|
+
return { mapping: selected, detectedTables: tables };
|
|
149
|
+
};
|
|
150
|
+
const hexExpression = (column) => column ? `hex(COALESCE(CAST(${quoteIdentifier(column)} AS BLOB), X''))` : `hex(X'')`;
|
|
151
|
+
const decodeHexUtf8 = (value) => value ? Buffer.from(value, 'hex').toString('utf8') : '';
|
|
152
|
+
const parseLegacyTags = (value) => Array.from(new Set(value
|
|
153
|
+
.split(/[\s,;|]+/)
|
|
154
|
+
.map((item) => item.trim().replace(/^#/, '').toLowerCase())
|
|
155
|
+
.filter((item) => /^[a-z0-9][a-z0-9_-]*$/i.test(item))));
|
|
156
|
+
const titleFromPath = (pathValue) => basename(pathValue).replace(extname(pathValue), '').replace(/[-_]+/g, ' ').trim();
|
|
157
|
+
const appendMissingTags = (content, tags) => {
|
|
158
|
+
if (tags.length === 0) {
|
|
159
|
+
return content;
|
|
160
|
+
}
|
|
161
|
+
const existingTags = new Set(extractTags(content).map((tag) => tag.toLowerCase()));
|
|
162
|
+
const missing = tags.filter((tag) => !existingTags.has(tag.toLowerCase()));
|
|
163
|
+
if (missing.length === 0) {
|
|
164
|
+
return content;
|
|
165
|
+
}
|
|
166
|
+
return `${content.trim()}\n\nTags: ${missing.map((tag) => `#${tag}`).join(' ')}`;
|
|
167
|
+
};
|
|
168
|
+
const buildNote = (title, content, agentId) => [
|
|
169
|
+
'---',
|
|
170
|
+
`title: "${title.replaceAll('"', '\\"')}"`,
|
|
171
|
+
`agent: "${agentId}"`,
|
|
172
|
+
'---',
|
|
173
|
+
'',
|
|
174
|
+
`# ${title}`,
|
|
175
|
+
'',
|
|
176
|
+
content.trim(),
|
|
177
|
+
''
|
|
178
|
+
].join('\n');
|
|
179
|
+
const parseLegacyRow = (columns, rowIndex) => {
|
|
180
|
+
const [titleHex, contentHex, pathHex, agentHex, tagsHex] = columns;
|
|
181
|
+
const content = decodeHexUtf8(contentHex).trim();
|
|
182
|
+
const path = decodeHexUtf8(pathHex).trim();
|
|
183
|
+
const titleCandidate = decodeHexUtf8(titleHex).trim();
|
|
184
|
+
const fallbackTitleFromPath = path ? titleFromPath(path) : '';
|
|
185
|
+
const title = titleCandidate || fallbackTitleFromPath || `Imported Memory ${rowIndex + 1}`;
|
|
186
|
+
return {
|
|
187
|
+
title,
|
|
188
|
+
content,
|
|
189
|
+
path,
|
|
190
|
+
agent: decodeHexUtf8(agentHex).trim(),
|
|
191
|
+
tags: parseLegacyTags(decodeHexUtf8(tagsHex))
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
const noteRelativePath = (agentId, slug, suffix = 0) => `agents/${agentId}/${suffix > 0 ? `${slug}-${suffix + 1}` : slug || 'untitled'}.md`;
|
|
195
|
+
const reserveUniquePath = (agentId, title, reserved) => {
|
|
196
|
+
const slug = slugify(title);
|
|
197
|
+
for (let suffix = 0; suffix < 10_000; suffix += 1) {
|
|
198
|
+
const relativePath = noteRelativePath(agentId, slug, suffix);
|
|
199
|
+
if (!reserved.has(relativePath)) {
|
|
200
|
+
reserved.add(relativePath);
|
|
201
|
+
return relativePath;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
throw new Error(`Could not allocate unique path for imported note: ${title}`);
|
|
205
|
+
};
|
|
206
|
+
const ensureSystemNote = async (vaultPath, reserved, created, agentId, title, content, dryRun) => {
|
|
207
|
+
const filename = noteRelativePath(agentId, slugify(title));
|
|
208
|
+
if (reserved.has(filename)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
reserved.add(filename);
|
|
212
|
+
created.add(filename);
|
|
213
|
+
if (dryRun) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
await writeMarkdownFile(vaultPath, filename, buildNote(title, content, agentId));
|
|
217
|
+
};
|
|
218
|
+
const applyConnectivityRule = async (vaultPath, reserved, created, title, content, agentId, dryRun) => {
|
|
219
|
+
const links = extractWikiLinks(content).filter((link) => normalizeTitle(link) !== normalizeTitle(title));
|
|
220
|
+
if (links.length > 0) {
|
|
221
|
+
return content.trim();
|
|
222
|
+
}
|
|
223
|
+
const normalized = normalizeTitle(title);
|
|
224
|
+
if (normalized === normalizeTitle(systemHubTitle)) {
|
|
225
|
+
await ensureSystemNote(vaultPath, reserved, created, agentId, systemRootTitle, `Entry point for agent memory. [[${systemHubTitle}]] #memory #root`, dryRun);
|
|
226
|
+
return `${content.trim()}\n\nRelated: [[${systemRootTitle}]]`;
|
|
227
|
+
}
|
|
228
|
+
await ensureSystemNote(vaultPath, reserved, created, agentId, systemHubTitle, 'Central memory index for this agent namespace. #memory #hub', dryRun);
|
|
229
|
+
return `${content.trim()}\n\nRelated: [[${systemHubTitle}]]`;
|
|
230
|
+
};
|
|
231
|
+
const importRowsFromMapping = async (vaultPath, dbPath, mapping, options, reserved) => {
|
|
232
|
+
const limit = Number.isFinite(options.limit) && (options.limit ?? 0) > 0 ? Math.floor(options.limit ?? 0) : undefined;
|
|
233
|
+
const sql = [
|
|
234
|
+
'SELECT',
|
|
235
|
+
`${hexExpression(mapping.titleColumn)} AS title_hex,`,
|
|
236
|
+
`${hexExpression(mapping.contentColumn)} AS content_hex,`,
|
|
237
|
+
`${hexExpression(mapping.pathColumn)} AS path_hex,`,
|
|
238
|
+
`${hexExpression(mapping.agentColumn)} AS agent_hex,`,
|
|
239
|
+
`${hexExpression(mapping.tagsColumn)} AS tags_hex,`,
|
|
240
|
+
`${hexExpression(mapping.createdColumn)} AS created_hex,`,
|
|
241
|
+
`${hexExpression(mapping.updatedColumn)} AS updated_hex`,
|
|
242
|
+
`FROM ${quoteIdentifier(mapping.table)}`,
|
|
243
|
+
...(limit ? [`LIMIT ${limit}`] : [])
|
|
244
|
+
].join(' ');
|
|
245
|
+
const rows = await runSqliteQuery(dbPath, sql);
|
|
246
|
+
const createdSystemNotes = new Set();
|
|
247
|
+
const importedFiles = [];
|
|
248
|
+
let imported = 0;
|
|
249
|
+
let skipped = 0;
|
|
250
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
251
|
+
const row = parseLegacyRow(rows[rowIndex], rowIndex);
|
|
252
|
+
if (!row.content) {
|
|
253
|
+
skipped += 1;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const agentId = sanitizeAgentId(options.agentOverride || row.agent || sharedAgentId);
|
|
257
|
+
const filename = reserveUniquePath(agentId, row.title, reserved);
|
|
258
|
+
const mergedContent = appendMissingTags(row.content, row.tags);
|
|
259
|
+
const connectedContent = await applyConnectivityRule(vaultPath, reserved, createdSystemNotes, row.title, mergedContent, agentId, options.dryRun === true);
|
|
260
|
+
const note = buildNote(row.title, connectedContent, agentId);
|
|
261
|
+
if (options.dryRun !== true) {
|
|
262
|
+
await writeMarkdownFile(vaultPath, filename, note);
|
|
263
|
+
}
|
|
264
|
+
importedFiles.push(filename);
|
|
265
|
+
imported += 1;
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
rowsRead: rows.length,
|
|
269
|
+
imported,
|
|
270
|
+
skipped,
|
|
271
|
+
createdSystemNotes: createdSystemNotes.size,
|
|
272
|
+
importedFiles
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
export const importLegacySqliteDatabase = async (vaultPath, options = {}) => {
|
|
276
|
+
const vault = await ensureVault(vaultPath);
|
|
277
|
+
const dbPath = await detectLegacyDbPath(vaultPath, options.dbPath);
|
|
278
|
+
const { mapping, detectedTables } = await detectTableMapping(dbPath, options.table);
|
|
279
|
+
const existingFiles = (await listVaultFiles(vaultPath))
|
|
280
|
+
.filter((path) => extname(path).toLowerCase() === '.md')
|
|
281
|
+
.map((path) => relative(vault, path));
|
|
282
|
+
const reserved = new Set(existingFiles);
|
|
283
|
+
const imported = await importRowsFromMapping(vaultPath, dbPath, mapping, options, reserved);
|
|
284
|
+
return {
|
|
285
|
+
vault,
|
|
286
|
+
dbPath,
|
|
287
|
+
table: mapping.table,
|
|
288
|
+
detectedTables,
|
|
289
|
+
rowsRead: imported.rowsRead,
|
|
290
|
+
imported: imported.imported,
|
|
291
|
+
skipped: imported.skipped,
|
|
292
|
+
createdSystemNotes: imported.createdSystemNotes,
|
|
293
|
+
dryRun: options.dryRun === true,
|
|
294
|
+
importedFiles: imported.importedFiles
|
|
295
|
+
};
|
|
296
|
+
};
|