@andespindola/brainlink 0.1.0-beta.0 → 0.1.0-beta.10

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 (41) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +252 -19
  3. package/dist/application/add-note.js +62 -13
  4. package/dist/application/analyze-vault.js +104 -9
  5. package/dist/application/frontend/client-css.js +154 -71
  6. package/dist/application/frontend/client-html.js +42 -33
  7. package/dist/application/frontend/client-js.js +255 -70
  8. package/dist/application/get-graph-layout.js +6 -3
  9. package/dist/application/get-graph-node.js +12 -0
  10. package/dist/application/get-graph-summary.js +12 -0
  11. package/dist/application/migrate-vault.js +91 -0
  12. package/dist/application/search-graph-node-ids.js +12 -0
  13. package/dist/application/search-knowledge.js +56 -1
  14. package/dist/application/server/routes.js +27 -1
  15. package/dist/cli/commands/agent-commands.js +412 -0
  16. package/dist/cli/commands/config-commands.js +167 -0
  17. package/dist/cli/commands/read-commands.js +25 -8
  18. package/dist/cli/commands/write-commands.js +191 -7
  19. package/dist/cli/main.js +4 -0
  20. package/dist/cli/runtime.js +5 -2
  21. package/dist/domain/embeddings.js +2 -1
  22. package/dist/domain/graph-layout.js +20 -14
  23. package/dist/domain/markdown.js +36 -4
  24. package/dist/infrastructure/config.js +96 -8
  25. package/dist/infrastructure/file-system-vault.js +15 -0
  26. package/dist/infrastructure/paths.js +9 -1
  27. package/dist/infrastructure/session-state.js +172 -0
  28. package/dist/infrastructure/sqlite/graph-reader.js +252 -105
  29. package/dist/infrastructure/sqlite/recovery.js +83 -0
  30. package/dist/infrastructure/sqlite/schema.js +4 -1
  31. package/dist/infrastructure/sqlite/search-reader.js +104 -72
  32. package/dist/infrastructure/sqlite-index.js +16 -3
  33. package/dist/mcp/main.js +11 -3
  34. package/dist/mcp/server.js +22 -2
  35. package/dist/mcp/startup.js +35 -0
  36. package/dist/mcp/tools.js +617 -21
  37. package/docs/AGENT_USAGE.md +95 -6
  38. package/docs/ARCHITECTURE.md +15 -1
  39. package/docs/QUICKSTART.md +104 -0
  40. package/docs/RELEASE.md +3 -3
  41. package/package.json +1 -1
@@ -1,10 +1,91 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { performance } from 'node:perf_hooks';
4
+ import { join } from 'node:path';
1
5
  import { validateGraph, getBrokenLinks, getOrphanNodes, getVaultStats } from '../domain/graph-analysis.js';
2
- import { ensureVault, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
3
- import { getGraph } from './get-graph.js';
4
- export const getStats = async (vaultPath, agentId) => getVaultStats(await getGraph(vaultPath, agentId));
5
- export const getBrokenLinksReport = async (vaultPath, agentId) => getBrokenLinks(await getGraph(vaultPath, agentId));
6
- export const getOrphansReport = async (vaultPath, agentId) => getOrphanNodes(await getGraph(vaultPath, agentId));
7
- export const validateVault = async (vaultPath, agentId) => validateGraph(await getGraph(vaultPath, agentId));
6
+ import { ensureVault, listVaultFiles, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
7
+ import { resolveAgentRuntimeDefaults } from '../infrastructure/config.js';
8
+ import { getGraphSummary } from './get-graph-summary.js';
9
+ import { buildContextPackage } from './build-context.js';
10
+ import { indexVault } from './index-vault.js';
11
+ import { searchKnowledge } from './search-knowledge.js';
12
+ import { loadBrainlinkConfig } from '../infrastructure/config.js';
13
+ export const getStats = async (vaultPath, agentId) => getVaultStats(await getGraphSummary(vaultPath, agentId));
14
+ export const getBrokenLinksReport = async (vaultPath, agentId) => getBrokenLinks(await getGraphSummary(vaultPath, agentId));
15
+ export const getOrphansReport = async (vaultPath, agentId) => getOrphanNodes(await getGraphSummary(vaultPath, agentId));
16
+ export const validateVault = async (vaultPath, agentId) => validateGraph(await getGraphSummary(vaultPath, agentId));
17
+ const toRatio = (part, total) => total === 0 ? 0 : Number((part / total).toFixed(4));
18
+ export const getExtendedStats = async (vaultPath, agentId) => {
19
+ const absoluteVaultPath = await ensureVault(vaultPath);
20
+ const graph = await getGraphSummary(absoluteVaultPath, agentId);
21
+ const stats = getVaultStats(graph);
22
+ const markdownFiles = await readMarkdownFiles(absoluteVaultPath);
23
+ const allFiles = await listVaultFiles(absoluteVaultPath);
24
+ const totalBytes = (await Promise.all(allFiles.map(async (filePath) => {
25
+ try {
26
+ return (await stat(filePath)).size;
27
+ }
28
+ catch {
29
+ return 0;
30
+ }
31
+ }))).reduce((sum, value) => sum + value, 0);
32
+ const updatedAt = markdownFiles
33
+ .map((file) => file.updatedAt.getTime())
34
+ .filter((time) => Number.isFinite(time))
35
+ .sort((left, right) => left - right);
36
+ const priorities = graph.edges.reduce((state, edge) => ({
37
+ ...state,
38
+ [edge.priority]: state[edge.priority] + 1
39
+ }), {
40
+ low: 0,
41
+ normal: 0,
42
+ high: 0,
43
+ critical: 0
44
+ });
45
+ const config = await loadBrainlinkConfig();
46
+ const defaults = resolveAgentRuntimeDefaults(config, agentId);
47
+ const probeQuery = graph.nodes[0]?.title ?? 'architecture';
48
+ const indexStart = performance.now();
49
+ await indexVault(absoluteVaultPath);
50
+ const indexLatency = performance.now() - indexStart;
51
+ const searchStart = performance.now();
52
+ await searchKnowledge(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), agentId, 'hybrid');
53
+ const searchLatency = performance.now() - searchStart;
54
+ const contextStart = performance.now();
55
+ await buildContextPackage(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), defaults.defaultContextTokens, agentId, 'hybrid');
56
+ const contextLatency = performance.now() - contextStart;
57
+ return {
58
+ stats,
59
+ storage: {
60
+ markdownFileCount: markdownFiles.length,
61
+ totalFileCount: allFiles.length,
62
+ totalBytes,
63
+ averageMarkdownBytes: markdownFiles.length === 0
64
+ ? 0
65
+ : Math.round(markdownFiles.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0) / markdownFiles.length),
66
+ ...(updatedAt.length > 0
67
+ ? {
68
+ oldestNoteUpdatedAt: new Date(updatedAt[0]).toISOString(),
69
+ newestNoteUpdatedAt: new Date(updatedAt[updatedAt.length - 1]).toISOString()
70
+ }
71
+ : {})
72
+ },
73
+ quality: {
74
+ resolvedLinkRatio: toRatio(stats.resolvedLinkCount, stats.linkCount),
75
+ brokenLinkRatio: toRatio(stats.brokenLinkCount, stats.linkCount),
76
+ orphanRatio: toRatio(stats.orphanCount, Math.max(stats.documentCount, 1)),
77
+ priorityDistribution: priorities
78
+ },
79
+ observability: {
80
+ probeQuery,
81
+ latenciesMs: {
82
+ index: Number(indexLatency.toFixed(2)),
83
+ search: Number(searchLatency.toFixed(2)),
84
+ context: Number(contextLatency.toFixed(2))
85
+ }
86
+ }
87
+ };
88
+ };
8
89
  const createCheck = (name, ok, message) => ({
9
90
  name,
10
91
  ok,
@@ -13,16 +94,30 @@ const createCheck = (name, ok, message) => ({
13
94
  export const doctorVault = async (vaultPath) => {
14
95
  const absoluteVaultPath = await ensureVault(vaultPath);
15
96
  const files = await readMarkdownFiles(absoluteVaultPath);
16
- const graph = await getGraph(absoluteVaultPath);
97
+ const graph = await getGraphSummary(absoluteVaultPath);
17
98
  const validation = validateGraph(graph);
99
+ const backupPath = join(absoluteVaultPath, '.brainlink', 'brainlink.db.backup');
100
+ const hasBackup = existsSync(backupPath);
101
+ const backupReady = graph.nodes.length === 0 || hasBackup;
18
102
  const checks = [
19
103
  createCheck('vault', true, `Vault ready at ${absoluteVaultPath}`),
20
104
  createCheck('markdown-files', files.length > 0, `${files.length} markdown files found`),
21
105
  createCheck('index', graph.nodes.length > 0, `${graph.nodes.length} indexed documents found`),
22
- createCheck('broken-links', validation.brokenLinks.length === 0, `${validation.brokenLinks.length} broken links found`)
106
+ createCheck('broken-links', validation.brokenLinks.length === 0, `${validation.brokenLinks.length} broken links found`),
107
+ createCheck('index-backup', backupReady, backupReady
108
+ ? (hasBackup ? 'SQLite recovery snapshot is available' : 'No index yet. Snapshot will be created after first indexing run')
109
+ : 'Recovery snapshot missing. Run blink index to create a rollback snapshot')
23
110
  ];
111
+ const recommendations = files.length === 0 && graph.nodes.length === 0
112
+ ? [
113
+ `Vault is empty. Add your first note: blink add "Architecture" --vault "${absoluteVaultPath}" --content "Markdown source of truth. #architecture"`,
114
+ `If this path is not the expected vault, inspect active config: blink config where`,
115
+ `If you changed vault recently, migrate existing memory: blink migrate-vault --from ~/.brainlink/vault --to "${absoluteVaultPath}"`
116
+ ]
117
+ : [];
24
118
  return {
25
119
  ok: checks.every((check) => check.ok),
26
- checks
120
+ checks,
121
+ ...(recommendations.length > 0 ? { recommendations } : {})
27
122
  };
28
123
  };
@@ -32,8 +32,6 @@ select {
32
32
  }
33
33
 
34
34
  .shell {
35
- display: grid;
36
- grid-template-columns: minmax(0, 1fr) 360px;
37
35
  width: 100%;
38
36
  height: 100svh;
39
37
  overflow: hidden;
@@ -73,17 +71,14 @@ select {
73
71
 
74
72
  .topbar > div {
75
73
  display: flex;
76
- align-items: baseline;
77
- gap: 12px;
74
+ align-items: center;
78
75
  }
79
76
 
80
77
  .topbar strong {
81
78
  font-size: 18px;
82
79
  }
83
80
 
84
- .topbar span,
85
- .eyebrow,
86
- .inspector small {
81
+ .eyebrow {
87
82
  color: var(--muted);
88
83
  font-size: 12px;
89
84
  }
@@ -138,70 +133,37 @@ select {
138
133
  color: var(--accent);
139
134
  }
140
135
 
141
- .inspector {
142
- display: grid;
143
- grid-template-rows: auto auto auto auto auto 1fr 1fr;
144
- gap: 22px;
145
- min-width: 0;
146
- height: 100%;
147
- padding: 24px;
148
- border-left: 1px solid var(--line);
149
- background: var(--panel);
150
- overflow: auto;
136
+ .floating-metrics {
137
+ position: absolute;
138
+ top: 66px;
139
+ left: 18px;
140
+ display: flex;
141
+ gap: 10px;
142
+ pointer-events: none;
151
143
  }
152
144
 
153
- .inspector h1,
154
- .inspector h2,
155
- .inspector p {
156
- margin: 0;
145
+ .metric-chip {
146
+ min-width: 94px;
147
+ padding: 10px 12px;
148
+ border: 1px solid var(--line);
149
+ border-radius: 10px;
150
+ background: rgba(21, 25, 31, 0.88);
151
+ display: grid;
152
+ gap: 3px;
157
153
  }
158
154
 
159
- .inspector h1 {
160
- margin-top: 6px;
155
+ .metric-chip strong {
161
156
  font-size: 26px;
162
- line-height: 1.12;
163
- overflow-wrap: anywhere;
157
+ line-height: 1;
164
158
  }
165
159
 
166
- .inspector h2 {
167
- margin-bottom: 10px;
160
+ .metric-chip small {
168
161
  color: var(--muted);
169
- font-size: 12px;
170
- font-weight: 700;
162
+ font-size: 11px;
163
+ letter-spacing: 0.03em;
171
164
  text-transform: uppercase;
172
165
  }
173
166
 
174
- #path {
175
- margin-top: 10px;
176
- color: var(--muted);
177
- line-height: 1.45;
178
- overflow-wrap: anywhere;
179
- }
180
-
181
- .metrics {
182
- display: grid;
183
- grid-template-columns: repeat(3, 1fr);
184
- border: 1px solid var(--line);
185
- border-radius: 8px;
186
- overflow: hidden;
187
- }
188
-
189
- .metrics div {
190
- display: grid;
191
- gap: 4px;
192
- padding: 14px;
193
- background: var(--panel-strong);
194
- }
195
-
196
- .metrics div + div {
197
- border-left: 1px solid var(--line);
198
- }
199
-
200
- .metrics span {
201
- font-size: 22px;
202
- font-weight: 700;
203
- }
204
-
205
167
  .tags {
206
168
  display: flex;
207
169
  flex-wrap: wrap;
@@ -215,6 +177,7 @@ select {
215
177
  background: var(--accent-weak);
216
178
  color: var(--accent);
217
179
  font-size: 12px;
180
+ word-break: break-word;
218
181
  overflow-wrap: anywhere;
219
182
  }
220
183
 
@@ -230,6 +193,7 @@ li {
230
193
  padding: 10px 0;
231
194
  border-bottom: 1px solid var(--line);
232
195
  color: var(--text);
196
+ word-break: break-word;
233
197
  overflow-wrap: anywhere;
234
198
  }
235
199
 
@@ -253,7 +217,7 @@ li small {
253
217
  }
254
218
 
255
219
  .note-content {
256
- max-height: 32svh;
220
+ max-height: min(68svh, 760px);
257
221
  margin: 0;
258
222
  padding: 12px;
259
223
  border: 1px solid var(--line);
@@ -267,18 +231,116 @@ li small {
267
231
  line-height: 1.5;
268
232
  }
269
233
 
270
- @media (max-width: 860px) {
271
- .shell {
272
- grid-template-columns: 1fr;
273
- grid-template-rows: minmax(0, 1fr) 42svh;
274
- }
234
+ .content-dialog {
235
+ width: min(920px, calc(100vw - 32px));
236
+ max-height: calc(100svh - 32px);
237
+ padding: 0;
238
+ border: 1px solid var(--line);
239
+ border-radius: 8px;
240
+ background: var(--panel);
241
+ color: var(--text);
242
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.48);
243
+ }
275
244
 
276
- .inspector {
277
- border-left: 0;
278
- border-top: 1px solid var(--line);
279
- padding: 18px;
280
- }
245
+ .content-dialog::backdrop {
246
+ background: rgba(4, 7, 10, 0.72);
247
+ backdrop-filter: blur(4px);
248
+ }
281
249
 
250
+ .content-dialog article {
251
+ display: grid;
252
+ grid-template-rows: auto auto minmax(0, 1fr);
253
+ max-height: calc(100svh - 34px);
254
+ }
255
+
256
+ .content-dialog header {
257
+ display: flex;
258
+ align-items: flex-start;
259
+ justify-content: space-between;
260
+ gap: 18px;
261
+ padding: 22px;
262
+ border-bottom: 1px solid var(--line);
263
+ }
264
+
265
+ .content-dialog h2,
266
+ .content-dialog p {
267
+ margin: 0;
268
+ }
269
+
270
+ .content-dialog h2 {
271
+ margin-top: 6px;
272
+ font-size: 24px;
273
+ line-height: 1.15;
274
+ overflow-wrap: anywhere;
275
+ }
276
+
277
+ .content-dialog p {
278
+ margin-top: 8px;
279
+ color: var(--muted);
280
+ overflow-wrap: anywhere;
281
+ }
282
+
283
+ .content-dialog button {
284
+ flex: 0 0 auto;
285
+ height: 38px;
286
+ padding: 0 14px;
287
+ border: 1px solid var(--line);
288
+ border-radius: 8px;
289
+ background: var(--panel-strong);
290
+ color: var(--text);
291
+ cursor: pointer;
292
+ }
293
+
294
+ .content-dialog button:hover,
295
+ .content-dialog button:focus {
296
+ border-color: var(--accent);
297
+ color: var(--accent);
298
+ }
299
+
300
+ .content-meta {
301
+ display: grid;
302
+ grid-template-columns: repeat(3, minmax(0, 1fr));
303
+ gap: 10px;
304
+ padding: 14px 22px;
305
+ border-bottom: 1px solid var(--line);
306
+ }
307
+
308
+ .content-meta-section {
309
+ min-height: 0;
310
+ padding: 10px;
311
+ border: 1px solid var(--line);
312
+ border-radius: 8px;
313
+ background: var(--panel-strong);
314
+ display: grid;
315
+ grid-template-rows: auto minmax(0, 1fr);
316
+ gap: 8px;
317
+ }
318
+
319
+ .content-meta-section h3 {
320
+ margin: 0;
321
+ color: var(--muted);
322
+ font-size: 11px;
323
+ font-weight: 700;
324
+ text-transform: uppercase;
325
+ }
326
+
327
+ .content-meta-section ul,
328
+ .content-meta-section .tags {
329
+ max-height: 140px;
330
+ overflow: auto;
331
+ align-content: flex-start;
332
+ padding-right: 4px;
333
+ }
334
+
335
+ .content-dialog .note-content {
336
+ max-height: none;
337
+ min-height: 0;
338
+ border: 0;
339
+ border-radius: 0;
340
+ padding: 22px;
341
+ }
342
+
343
+ @media (max-width: 860px) {
282
344
  .topbar {
283
345
  align-items: stretch;
284
346
  flex-direction: column;
@@ -291,4 +353,25 @@ li small {
291
353
  .agent-filter {
292
354
  width: 100%;
293
355
  }
356
+
357
+ .content-dialog header {
358
+ align-items: stretch;
359
+ flex-direction: column;
360
+ }
361
+
362
+ .floating-metrics {
363
+ top: 116px;
364
+ right: 18px;
365
+ left: 18px;
366
+ justify-content: flex-start;
367
+ flex-wrap: wrap;
368
+ }
369
+
370
+ .metric-chip {
371
+ min-width: 82px;
372
+ }
373
+
374
+ .content-meta {
375
+ grid-template-columns: 1fr;
376
+ }
294
377
  }`;
@@ -13,7 +13,6 @@ export const createClientHtml = () => `<!doctype html>
13
13
  <div class="topbar">
14
14
  <div>
15
15
  <strong>Brainlink</strong>
16
- <span id="stats">Loading graph</span>
17
16
  </div>
18
17
  <label class="search">
19
18
  <input id="search" type="search" placeholder="Filter notes, tags or paths" autocomplete="off" />
@@ -22,45 +21,55 @@ export const createClientHtml = () => `<!doctype html>
22
21
  <select id="agent"></select>
23
22
  </label>
24
23
  </div>
24
+ <div class="floating-metrics" aria-label="Graph totals">
25
+ <div class="metric-chip">
26
+ <strong id="nodeCount">0</strong>
27
+ <small>Notes</small>
28
+ </div>
29
+ <div class="metric-chip">
30
+ <strong id="edgeCount">0</strong>
31
+ <small>Links</small>
32
+ </div>
33
+ <div class="metric-chip">
34
+ <strong id="tagCount">0</strong>
35
+ <small>Tags</small>
36
+ </div>
37
+ </div>
25
38
  <div class="toolbar" aria-label="Graph controls">
26
39
  <button id="zoomIn" type="button" title="Zoom in">+</button>
27
40
  <button id="zoomOut" type="button" title="Zoom out">-</button>
41
+ <button id="fit" type="button" title="Fit visible nodes">◎</button>
28
42
  <button id="reset" type="button" title="Reset view">⌂</button>
29
43
  </div>
30
44
  </section>
31
- <aside class="inspector" aria-label="Selected note">
32
- <div>
33
- <span class="eyebrow">Selected note</span>
34
- <h1 id="title">Graph Overview</h1>
35
- <p id="path">Select a node to inspect links and backlinks.</p>
36
- </div>
37
- <div class="metrics">
38
- <div><span id="nodeCount">0</span><small>Notes</small></div>
39
- <div><span id="edgeCount">0</span><small>Links</small></div>
40
- <div><span id="tagCount">0</span><small>Tags</small></div>
41
- </div>
42
- <section>
43
- <h2>Tags</h2>
44
- <div id="tags" class="tags"></div>
45
- </section>
46
- <section>
47
- <h2>Notes</h2>
48
- <ul id="notes"></ul>
49
- </section>
50
- <section>
51
- <h2>Content</h2>
52
- <pre id="content" class="note-content"></pre>
53
- </section>
54
- <section>
55
- <h2>Outgoing</h2>
56
- <ul id="outgoing"></ul>
57
- </section>
58
- <section>
59
- <h2>Backlinks</h2>
60
- <ul id="incoming"></ul>
61
- </section>
62
- </aside>
63
45
  </main>
46
+ <dialog id="contentDialog" class="content-dialog" aria-labelledby="contentTitle">
47
+ <article>
48
+ <header>
49
+ <div>
50
+ <span class="eyebrow">Markdown content</span>
51
+ <h2 id="contentTitle">Selected note</h2>
52
+ <p id="contentPath"></p>
53
+ </div>
54
+ <button id="contentClose" type="button">Close</button>
55
+ </header>
56
+ <div class="content-meta">
57
+ <section class="content-meta-section">
58
+ <h3>Tags</h3>
59
+ <div id="contentTags" class="tags"></div>
60
+ </section>
61
+ <section class="content-meta-section">
62
+ <h3>Outgoing</h3>
63
+ <ul id="contentOutgoing"></ul>
64
+ </section>
65
+ <section class="content-meta-section">
66
+ <h3>Backlinks</h3>
67
+ <ul id="contentIncoming"></ul>
68
+ </section>
69
+ </div>
70
+ <pre id="contentBody" class="note-content"></pre>
71
+ </article>
72
+ </dialog>
64
73
  <script src="/app.js"></script>
65
74
  </body>
66
75
  </html>`;