@andespindola/brainlink 0.1.0-beta.8 → 0.1.0-beta.81

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 (63) hide show
  1. package/AGENTS.md +8 -5
  2. package/CHANGELOG.md +58 -2
  3. package/CONTRIBUTING.md +2 -2
  4. package/COPYRIGHT.md +5 -0
  5. package/README.md +266 -20
  6. package/SECURITY.md +1 -1
  7. package/dist/application/add-note.js +62 -13
  8. package/dist/application/analyze-vault.js +95 -8
  9. package/dist/application/build-context.js +56 -1
  10. package/dist/application/dedupe-notes.js +226 -0
  11. package/dist/application/frontend/client-css.js +138 -103
  12. package/dist/application/frontend/client-html.js +47 -41
  13. package/dist/application/frontend/client-js.js +2449 -156
  14. package/dist/application/frontend/client-worker-js.js +66 -0
  15. package/dist/application/get-graph-layout.js +18 -6
  16. package/dist/application/get-graph-node.js +12 -0
  17. package/dist/application/get-graph-summary.js +12 -0
  18. package/dist/application/get-graph.js +3 -3
  19. package/dist/application/import-legacy-sqlite.js +296 -0
  20. package/dist/application/index-vault.js +252 -19
  21. package/dist/application/list-agents.js +3 -3
  22. package/dist/application/list-links.js +5 -5
  23. package/dist/application/migrate-vault.js +46 -16
  24. package/dist/application/offline-pack-backup.js +44 -0
  25. package/dist/application/search-graph-node-ids.js +12 -0
  26. package/dist/application/search-knowledge.js +75 -5
  27. package/dist/application/server/routes.js +102 -1
  28. package/dist/application/start-server.js +75 -4
  29. package/dist/application/watch-vault.js +23 -2
  30. package/dist/benchmarks/large-vault.js +1 -1
  31. package/dist/cli/commands/agent-commands.js +419 -0
  32. package/dist/cli/commands/config-commands.js +167 -0
  33. package/dist/cli/commands/read-commands.js +25 -8
  34. package/dist/cli/commands/write-commands.js +973 -10
  35. package/dist/cli/main.js +4 -0
  36. package/dist/cli/runtime.js +5 -2
  37. package/dist/domain/context.js +53 -11
  38. package/dist/domain/embeddings.js +2 -1
  39. package/dist/domain/graph-layout.js +67 -16
  40. package/dist/domain/markdown.js +36 -4
  41. package/dist/domain/middle-out.js +18 -0
  42. package/dist/infrastructure/config.js +132 -8
  43. package/dist/infrastructure/file-index.js +358 -0
  44. package/dist/infrastructure/file-system-vault.js +15 -0
  45. package/dist/infrastructure/index-state.js +56 -0
  46. package/dist/infrastructure/paths.js +9 -1
  47. package/dist/infrastructure/private-pack-codec.js +134 -0
  48. package/dist/infrastructure/search-packs.js +452 -0
  49. package/dist/infrastructure/session-state.js +172 -0
  50. package/dist/mcp/main.js +11 -3
  51. package/dist/mcp/server.js +27 -2
  52. package/dist/mcp/startup.js +35 -0
  53. package/dist/mcp/tools.js +633 -19
  54. package/docs/AGENT_USAGE.md +177 -15
  55. package/docs/ARCHITECTURE.md +37 -26
  56. package/docs/QUICKSTART.md +111 -0
  57. package/package.json +6 -4
  58. package/dist/infrastructure/sqlite/document-writer.js +0 -51
  59. package/dist/infrastructure/sqlite/graph-reader.js +0 -120
  60. package/dist/infrastructure/sqlite/schema.js +0 -111
  61. package/dist/infrastructure/sqlite/search-reader.js +0 -156
  62. package/dist/infrastructure/sqlite/types.js +0 -1
  63. package/dist/infrastructure/sqlite-index.js +0 -25
@@ -1,120 +0,0 @@
1
- import { sanitizeAgentId } from '../../domain/agents.js';
2
- const toGraphLink = (row) => ({
3
- agentId: row.agent_id,
4
- fromTitle: row.from_title,
5
- fromPath: row.from_path,
6
- toTitle: row.to_title,
7
- toPath: row.to_path,
8
- weight: row.weight,
9
- priority: row.priority
10
- });
11
- const normalizeAgentFilter = (agentId) => agentId ? sanitizeAgentId(agentId) : undefined;
12
- const toTitleKey = (title) => title.toLowerCase();
13
- export const createGraphReader = (database) => ({
14
- listLinks: (agentId) => {
15
- const normalizedAgentId = normalizeAgentFilter(agentId);
16
- const agentFilter = normalizedAgentId ? 'WHERE source.agent_id = ?' : '';
17
- const rows = database
18
- .prepare(`
19
- SELECT
20
- source.agent_id AS agent_id,
21
- source.title AS from_title,
22
- source.path AS from_path,
23
- COALESCE(target.title, links.to_title) AS to_title,
24
- target.path AS to_path,
25
- links.weight AS weight,
26
- links.priority AS priority
27
- FROM links
28
- JOIN documents source ON source.id = links.from_document_id
29
- LEFT JOIN documents target ON target.id = links.to_document_id
30
- ${agentFilter}
31
- ORDER BY source.title, links.weight DESC, to_title
32
- `)
33
- .all(...(normalizedAgentId ? [normalizedAgentId] : []));
34
- return rows.map(toGraphLink);
35
- },
36
- listBacklinks: (title, agentId) => {
37
- const normalizedAgentId = normalizeAgentFilter(agentId);
38
- const agentFilter = normalizedAgentId ? 'AND source.agent_id = ?' : '';
39
- const titleKey = toTitleKey(title);
40
- const rows = database
41
- .prepare(`
42
- SELECT
43
- source.agent_id AS agent_id,
44
- source.title AS from_title,
45
- source.path AS from_path,
46
- COALESCE(target.title, links.to_title) AS to_title,
47
- target.path AS to_path,
48
- links.weight AS weight,
49
- links.priority AS priority
50
- FROM links
51
- JOIN documents source ON source.id = links.from_document_id
52
- LEFT JOIN documents target ON target.id = links.to_document_id
53
- WHERE links.to_title_key = ?
54
- ${agentFilter}
55
- ORDER BY links.weight DESC, source.title
56
- `)
57
- .all(...(normalizedAgentId ? [titleKey, normalizedAgentId] : [titleKey]));
58
- return rows.map(toGraphLink);
59
- },
60
- getGraph: (agentId) => {
61
- const normalizedAgentId = normalizeAgentFilter(agentId);
62
- const documentAgentFilter = normalizedAgentId ? 'WHERE agent_id = ?' : '';
63
- const edgeAgentFilter = normalizedAgentId ? 'WHERE source.agent_id = ?' : '';
64
- const nodeRows = database
65
- .prepare(`
66
- SELECT id, agent_id, title, path, content, tags_json
67
- FROM documents
68
- ${documentAgentFilter}
69
- ORDER BY title
70
- `)
71
- .all(...(normalizedAgentId ? [normalizedAgentId] : []));
72
- const edgeRows = database
73
- .prepare(`
74
- SELECT
75
- links.from_document_id AS source,
76
- links.to_document_id AS target,
77
- links.to_title AS target_title,
78
- links.weight AS weight,
79
- links.priority AS priority
80
- FROM links
81
- JOIN documents source ON source.id = links.from_document_id
82
- ${edgeAgentFilter}
83
- ORDER BY links.from_document_id, links.weight DESC, links.to_title
84
- `)
85
- .all(...(normalizedAgentId ? [normalizedAgentId] : []));
86
- const nodes = nodeRows.map((row) => ({
87
- id: row.id,
88
- agentId: row.agent_id,
89
- title: row.title,
90
- path: row.path,
91
- content: row.content,
92
- tags: JSON.parse(row.tags_json)
93
- }));
94
- const edges = edgeRows.map((row) => ({
95
- source: row.source,
96
- target: row.target,
97
- targetTitle: row.target_title,
98
- weight: row.weight,
99
- priority: row.priority
100
- }));
101
- return {
102
- nodes,
103
- edges
104
- };
105
- },
106
- listAgents: () => {
107
- const rows = database
108
- .prepare(`
109
- SELECT agent_id AS id, count(*) AS document_count
110
- FROM documents
111
- GROUP BY agent_id
112
- ORDER BY agent_id
113
- `)
114
- .all();
115
- return rows.map((row) => ({
116
- id: row.id,
117
- documentCount: row.document_count
118
- }));
119
- }
120
- });
@@ -1,111 +0,0 @@
1
- const schemaVersion = 5;
2
- const requiredTableColumns = {
3
- documents: ['id', 'agent_id', 'title', 'path', 'content', 'tags_json', 'frontmatter_json', 'created_at', 'updated_at'],
4
- chunks: ['id', 'document_id', 'ordinal', 'content', 'token_count', 'embedding_provider', 'embedding_json'],
5
- links: ['from_document_id', 'to_title', 'to_title_key', 'to_document_id', 'weight', 'priority'],
6
- chunks_fts: ['chunk_id', 'document_id', 'agent_id', 'title', 'content']
7
- };
8
- const getStoredSchemaVersion = (database) => {
9
- const hasMetadata = database
10
- .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'metadata'")
11
- .get();
12
- if (!hasMetadata) {
13
- return 0;
14
- }
15
- const row = database.prepare("SELECT value FROM metadata WHERE key = 'schema_version'").get();
16
- return Number.parseInt(row?.value ?? '0', 10);
17
- };
18
- const dropDerivedSchema = (database) => {
19
- database.exec(`
20
- DROP TABLE IF EXISTS embedding_buckets;
21
- DROP TABLE IF EXISTS chunks_fts;
22
- DROP TABLE IF EXISTS links;
23
- DROP TABLE IF EXISTS chunks;
24
- DROP TABLE IF EXISTS documents;
25
- `);
26
- };
27
- const getTableColumns = (database, tableName) => {
28
- const rows = database.prepare(`SELECT name FROM pragma_table_info(?)`).all(tableName);
29
- return rows.map((row) => row.name);
30
- };
31
- const hasCompatibleSchemaShape = (database) => Object.entries(requiredTableColumns).every(([tableName, requiredColumns]) => {
32
- const columns = getTableColumns(database, tableName);
33
- return columns.length === 0 || requiredColumns.every((column) => columns.includes(column));
34
- });
35
- export const createSchema = (database) => {
36
- const storedSchemaVersion = getStoredSchemaVersion(database);
37
- if ((storedSchemaVersion > 0 && storedSchemaVersion < schemaVersion) || !hasCompatibleSchemaShape(database)) {
38
- dropDerivedSchema(database);
39
- }
40
- database.exec(`
41
- CREATE TABLE IF NOT EXISTS metadata (
42
- key TEXT PRIMARY KEY,
43
- value TEXT NOT NULL
44
- );
45
-
46
- CREATE TABLE IF NOT EXISTS documents (
47
- id TEXT PRIMARY KEY,
48
- agent_id TEXT NOT NULL,
49
- title TEXT NOT NULL,
50
- path TEXT NOT NULL UNIQUE,
51
- content TEXT NOT NULL,
52
- tags_json TEXT NOT NULL,
53
- frontmatter_json TEXT NOT NULL,
54
- created_at TEXT NOT NULL,
55
- updated_at TEXT NOT NULL
56
- );
57
-
58
- CREATE TABLE IF NOT EXISTS chunks (
59
- id TEXT PRIMARY KEY,
60
- document_id TEXT NOT NULL,
61
- ordinal INTEGER NOT NULL,
62
- content TEXT NOT NULL,
63
- token_count INTEGER NOT NULL,
64
- embedding_provider TEXT NOT NULL,
65
- embedding_json TEXT NOT NULL,
66
- FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
67
- );
68
-
69
- CREATE INDEX IF NOT EXISTS idx_documents_agent_title ON documents(agent_id, title);
70
- CREATE INDEX IF NOT EXISTS idx_chunks_document_ordinal ON chunks(document_id, ordinal);
71
-
72
- CREATE TABLE IF NOT EXISTS embedding_buckets (
73
- bucket TEXT NOT NULL,
74
- chunk_id TEXT NOT NULL,
75
- PRIMARY KEY (bucket, chunk_id),
76
- FOREIGN KEY (chunk_id) REFERENCES chunks(id) ON DELETE CASCADE
77
- );
78
-
79
- CREATE INDEX IF NOT EXISTS idx_embedding_buckets_bucket ON embedding_buckets(bucket);
80
-
81
- CREATE TABLE IF NOT EXISTS links (
82
- from_document_id TEXT NOT NULL,
83
- to_title TEXT NOT NULL,
84
- to_title_key TEXT NOT NULL,
85
- to_document_id TEXT,
86
- weight INTEGER NOT NULL,
87
- priority TEXT NOT NULL,
88
- PRIMARY KEY (from_document_id, to_title_key),
89
- FOREIGN KEY (from_document_id) REFERENCES documents(id) ON DELETE CASCADE,
90
- FOREIGN KEY (to_document_id) REFERENCES documents(id) ON DELETE SET NULL
91
- );
92
-
93
- CREATE INDEX IF NOT EXISTS idx_links_to_document_id ON links(to_document_id);
94
- CREATE INDEX IF NOT EXISTS idx_links_to_title_key ON links(to_title_key);
95
-
96
- CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
97
- chunk_id UNINDEXED,
98
- document_id UNINDEXED,
99
- agent_id UNINDEXED,
100
- title,
101
- content
102
- );
103
- `);
104
- database
105
- .prepare(`
106
- INSERT INTO metadata (key, value)
107
- VALUES ('schema_version', ?)
108
- ON CONFLICT(key) DO UPDATE SET value = excluded.value
109
- `)
110
- .run(String(schemaVersion));
111
- };
@@ -1,156 +0,0 @@
1
- import { sanitizeAgentId } from '../../domain/agents.js';
2
- import { cosineSimilarity, createEmbeddingBuckets } from '../../domain/embeddings.js';
3
- const toFtsQuery = (query) => query
4
- .toLowerCase()
5
- .match(/[\p{L}\p{N}_-]+/gu)
6
- ?.map((term) => `"${term.replaceAll('"', '""')}"*`)
7
- .join(' OR ') ?? '';
8
- const normalizeAgentFilter = (agentId) => agentId ? sanitizeAgentId(agentId) : undefined;
9
- const parseJsonArray = (value) => {
10
- if (!value) {
11
- return [];
12
- }
13
- try {
14
- const parsed = JSON.parse(value);
15
- return Array.isArray(parsed) ? parsed : [];
16
- }
17
- catch {
18
- return [];
19
- }
20
- };
21
- const toTextScore = (index, total) => total === 0 ? 0 : 1 - index / (total + 1);
22
- const toSearchResult = (row, score, textScore, semanticScore, searchMode) => ({
23
- documentId: row.document_id,
24
- agentId: row.agent_id,
25
- title: row.title,
26
- path: row.path,
27
- chunkId: row.chunk_id,
28
- content: row.content,
29
- score,
30
- textScore,
31
- semanticScore,
32
- searchMode,
33
- tags: parseJsonArray(row.tags_json).filter((value) => typeof value === 'string')
34
- });
35
- const sortByScore = (results) => [...results].sort((left, right) => right.score - left.score || left.title.localeCompare(right.title));
36
- const mergeHybridResults = (ftsResults, semanticResults, limit) => {
37
- const rows = new Map();
38
- [...semanticResults, ...ftsResults].forEach((result) => {
39
- const current = rows.get(result.chunkId);
40
- const textScore = Math.max(current?.textScore ?? 0, result.textScore);
41
- const semanticScore = Math.max(current?.semanticScore ?? 0, result.semanticScore);
42
- const score = textScore * 0.62 + semanticScore * 0.38;
43
- rows.set(result.chunkId, {
44
- ...result,
45
- score,
46
- textScore,
47
- semanticScore,
48
- searchMode: 'hybrid'
49
- });
50
- });
51
- return sortByScore(Array.from(rows.values())).slice(0, limit);
52
- };
53
- const placeholders = (count) => Array.from({ length: count }, () => '?').join(', ');
54
- const readAllSemanticRows = (database, normalizedAgentId) => {
55
- const semanticAgentFilter = normalizedAgentId ? 'WHERE documents.agent_id = ?' : '';
56
- return database
57
- .prepare(`
58
- SELECT
59
- documents.id AS document_id,
60
- documents.agent_id AS agent_id,
61
- documents.title AS title,
62
- documents.path AS path,
63
- chunks.id AS chunk_id,
64
- chunks.content AS content,
65
- documents.tags_json AS tags_json,
66
- chunks.embedding_json AS embedding_json
67
- FROM chunks
68
- JOIN documents ON documents.id = chunks.document_id
69
- ${semanticAgentFilter}
70
- `)
71
- .all(...(normalizedAgentId ? [normalizedAgentId] : []));
72
- };
73
- const readBucketedSemanticRows = (database, normalizedAgentId, queryEmbedding, limit) => {
74
- const buckets = createEmbeddingBuckets(queryEmbedding);
75
- if (buckets.length === 0) {
76
- return [];
77
- }
78
- const agentFilter = normalizedAgentId ? 'AND documents.agent_id = ?' : '';
79
- const params = normalizedAgentId ? [...buckets, normalizedAgentId, limit] : [...buckets, limit];
80
- return database
81
- .prepare(`
82
- SELECT
83
- documents.id AS document_id,
84
- documents.agent_id AS agent_id,
85
- documents.title AS title,
86
- documents.path AS path,
87
- chunks.id AS chunk_id,
88
- chunks.content AS content,
89
- documents.tags_json AS tags_json,
90
- chunks.embedding_json AS embedding_json,
91
- count(*) AS score
92
- FROM embedding_buckets
93
- JOIN chunks ON chunks.id = embedding_buckets.chunk_id
94
- JOIN documents ON documents.id = chunks.document_id
95
- WHERE embedding_buckets.bucket IN (${placeholders(buckets.length)})
96
- ${agentFilter}
97
- GROUP BY chunks.id
98
- ORDER BY score DESC, chunks.token_count ASC, documents.title ASC
99
- LIMIT ?
100
- `)
101
- .all(...params);
102
- };
103
- const readSemanticRows = (database, normalizedAgentId, queryEmbedding, limit) => {
104
- const candidateLimit = Math.max(limit * 96, 768);
105
- const bucketedRows = readBucketedSemanticRows(database, normalizedAgentId, queryEmbedding, candidateLimit);
106
- return bucketedRows.length > 0 ? bucketedRows : readAllSemanticRows(database, normalizedAgentId);
107
- };
108
- export const createSearchReader = (database) => ({
109
- search: (query, limit, agentId, mode = 'hybrid', queryEmbedding = []) => {
110
- const normalizedQuery = query.trim();
111
- if (!normalizedQuery || limit <= 0) {
112
- return [];
113
- }
114
- const normalizedAgentId = normalizeAgentFilter(agentId);
115
- const ftsQuery = toFtsQuery(query);
116
- const expandedLimit = Math.max(limit * 4, 24);
117
- const ftsAgentFilter = normalizedAgentId ? 'AND documents.agent_id = ?' : '';
118
- const ftsParams = normalizedAgentId ? [ftsQuery, normalizedAgentId, expandedLimit] : [ftsQuery, expandedLimit];
119
- const ftsRows = mode === 'semantic' || !ftsQuery
120
- ? []
121
- : database
122
- .prepare(`
123
- SELECT
124
- documents.id AS document_id,
125
- documents.agent_id AS agent_id,
126
- documents.title AS title,
127
- documents.path AS path,
128
- chunks_fts.chunk_id AS chunk_id,
129
- chunks_fts.content AS content,
130
- bm25(chunks_fts) * -1 AS score,
131
- documents.tags_json AS tags_json
132
- FROM chunks_fts
133
- JOIN documents ON documents.id = chunks_fts.document_id
134
- WHERE chunks_fts MATCH ?
135
- ${ftsAgentFilter}
136
- ORDER BY bm25(chunks_fts)
137
- LIMIT ?
138
- `)
139
- .all(...ftsParams);
140
- const ftsResults = ftsRows.map((row, index) => toSearchResult(row, toTextScore(index, ftsRows.length), toTextScore(index, ftsRows.length), 0, 'fts'));
141
- const semanticRows = mode === 'fts' || queryEmbedding.length === 0 ? [] : readSemanticRows(database, normalizedAgentId, queryEmbedding, expandedLimit);
142
- const semanticResults = sortByScore(semanticRows
143
- .map((row) => {
144
- const semanticScore = Math.max(0, cosineSimilarity(queryEmbedding, parseJsonArray(row.embedding_json).filter((value) => typeof value === 'number')));
145
- return toSearchResult(row, semanticScore, 0, semanticScore, 'semantic');
146
- })
147
- .filter((result) => result.semanticScore > 0)).slice(0, expandedLimit);
148
- if (mode === 'fts') {
149
- return ftsResults.slice(0, limit);
150
- }
151
- if (mode === 'semantic') {
152
- return semanticResults.slice(0, limit);
153
- }
154
- return mergeHybridResults(ftsResults, semanticResults, limit);
155
- }
156
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,25 +0,0 @@
1
- import Database from 'better-sqlite3';
2
- import { chmodSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { createIndexWriter } from './sqlite/document-writer.js';
5
- import { createGraphReader } from './sqlite/graph-reader.js';
6
- import { createSchema } from './sqlite/schema.js';
7
- import { createSearchReader } from './sqlite/search-reader.js';
8
- export const openSqliteIndex = (vaultPath) => {
9
- const databasePath = join(vaultPath, '.brainlink', 'brainlink.db');
10
- const database = new Database(databasePath);
11
- chmodSync(databasePath, 0o600);
12
- database.exec(`
13
- PRAGMA foreign_keys = ON;
14
- PRAGMA journal_mode = WAL;
15
- PRAGMA synchronous = NORMAL;
16
- PRAGMA temp_store = MEMORY;
17
- `);
18
- createSchema(database);
19
- return {
20
- ...createIndexWriter(database),
21
- ...createSearchReader(database),
22
- ...createGraphReader(database),
23
- close: () => database.close()
24
- };
25
- };