@andespindola/brainlink 0.1.0-alpha.0

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 (52) hide show
  1. package/AGENTS.md +142 -0
  2. package/CHANGELOG.md +13 -0
  3. package/CONTRIBUTING.md +28 -0
  4. package/LICENSE +23 -0
  5. package/README.md +715 -0
  6. package/SECURITY.md +35 -0
  7. package/dist/application/add-note.js +30 -0
  8. package/dist/application/analyze-vault.js +28 -0
  9. package/dist/application/build-context.js +15 -0
  10. package/dist/application/frontend/client-css.js +294 -0
  11. package/dist/application/frontend/client-html.js +66 -0
  12. package/dist/application/frontend/client-js.js +416 -0
  13. package/dist/application/get-graph-layout.js +3 -0
  14. package/dist/application/get-graph.js +12 -0
  15. package/dist/application/index-vault.js +67 -0
  16. package/dist/application/list-agents.js +12 -0
  17. package/dist/application/list-links.js +22 -0
  18. package/dist/application/search-knowledge.js +19 -0
  19. package/dist/application/server/host-security.js +6 -0
  20. package/dist/application/server/http.js +13 -0
  21. package/dist/application/server/routes.js +88 -0
  22. package/dist/application/server/types.js +1 -0
  23. package/dist/application/start-server.js +54 -0
  24. package/dist/application/watch-vault.js +36 -0
  25. package/dist/benchmarks/large-vault.js +88 -0
  26. package/dist/cli/commands/read-commands.js +149 -0
  27. package/dist/cli/commands/write-commands.js +107 -0
  28. package/dist/cli/main.js +21 -0
  29. package/dist/cli/runtime.js +18 -0
  30. package/dist/cli/types.js +1 -0
  31. package/dist/domain/agents.js +11 -0
  32. package/dist/domain/context.js +44 -0
  33. package/dist/domain/embeddings.js +117 -0
  34. package/dist/domain/graph-analysis.js +48 -0
  35. package/dist/domain/graph-layout.js +187 -0
  36. package/dist/domain/ids.js +2 -0
  37. package/dist/domain/markdown.js +100 -0
  38. package/dist/domain/note-safety.js +54 -0
  39. package/dist/domain/tokens.js +1 -0
  40. package/dist/domain/types.js +1 -0
  41. package/dist/infrastructure/config.js +60 -0
  42. package/dist/infrastructure/file-system-vault.js +62 -0
  43. package/dist/infrastructure/sqlite/document-writer.js +50 -0
  44. package/dist/infrastructure/sqlite/graph-reader.js +108 -0
  45. package/dist/infrastructure/sqlite/schema.js +87 -0
  46. package/dist/infrastructure/sqlite/search-reader.js +156 -0
  47. package/dist/infrastructure/sqlite/types.js +1 -0
  48. package/dist/infrastructure/sqlite-index.js +20 -0
  49. package/docs/AGENT_USAGE.md +477 -0
  50. package/docs/ARCHITECTURE.md +286 -0
  51. package/docs/RELEASE.md +67 -0
  52. package/package.json +67 -0
package/SECURITY.md ADDED
@@ -0,0 +1,35 @@
1
+ # Security
2
+
3
+ Brainlink is local-first.
4
+
5
+ ## Defaults
6
+
7
+ - The HTTP server binds to `127.0.0.1` by default.
8
+ - The HTTP server refuses non-loopback hosts unless `--allow-public` is passed.
9
+ - The HTTP server is read-only and does not expose note creation, indexing or update routes.
10
+ - The SQLite database is a derived local index.
11
+ - Markdown files are user-owned source data.
12
+ - Brainlink-created Markdown files use `0600` permissions.
13
+ - Brainlink-created directories and `.brainlink` use `0700` permissions.
14
+
15
+ ## Remote Exposure
16
+
17
+ Do not expose the HTTP server on a public interface without adding authentication, authorization and transport security.
18
+
19
+ ## Sensitive Memory
20
+
21
+ Brainlink blocks common secret patterns by default when adding notes through the CLI.
22
+
23
+ Use `--allow-sensitive` only when the vault is intentionally protected by your own storage and access controls.
24
+
25
+ Avoid storing secrets, credentials, private keys, tokens or regulated personal data in a vault unless the vault is protected by your own storage and access controls.
26
+
27
+ ## Vault Allowlist
28
+
29
+ External tool wrappers, including MCP servers, should set `BRAINLINK_ALLOWED_VAULTS` to restrict which vault paths the CLI can access.
30
+
31
+ ```bash
32
+ export BRAINLINK_ALLOWED_VAULTS="/absolute/path/to/project-vault"
33
+ ```
34
+
35
+ When the allowlist is set, CLI commands fail if `--vault` points outside the allowed roots.
@@ -0,0 +1,30 @@
1
+ import { writeMarkdownFile } from '../infrastructure/file-system-vault.js';
2
+ import { sanitizeAgentId, sharedAgentId } from '../domain/agents.js';
3
+ import { validateNoteInput } from '../domain/note-safety.js';
4
+ const slugify = (title) => title
5
+ .normalize('NFKD')
6
+ .replace(/[\u0300-\u036f]/g, '')
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9]+/g, '-')
9
+ .replace(/^-+|-+$/g, '');
10
+ export const addNote = async (vaultPath, title, content, agentId = sharedAgentId, options = {}) => {
11
+ validateNoteInput({
12
+ title,
13
+ content,
14
+ allowSensitive: options.allowSensitive
15
+ });
16
+ const sanitizedAgentId = sanitizeAgentId(agentId);
17
+ const filename = `agents/${sanitizedAgentId}/${slugify(title) || 'untitled'}.md`;
18
+ const note = [
19
+ `---`,
20
+ `title: "${title.replaceAll('"', '\\"')}"`,
21
+ `agent: "${sanitizedAgentId}"`,
22
+ `---`,
23
+ '',
24
+ `# ${title}`,
25
+ '',
26
+ content.trim(),
27
+ ''
28
+ ].join('\n');
29
+ return writeMarkdownFile(vaultPath, filename, note);
30
+ };
@@ -0,0 +1,28 @@
1
+ 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));
8
+ const createCheck = (name, ok, message) => ({
9
+ name,
10
+ ok,
11
+ message
12
+ });
13
+ export const doctorVault = async (vaultPath) => {
14
+ const absoluteVaultPath = await ensureVault(vaultPath);
15
+ const files = await readMarkdownFiles(absoluteVaultPath);
16
+ const graph = await getGraph(absoluteVaultPath);
17
+ const validation = validateGraph(graph);
18
+ const checks = [
19
+ createCheck('vault', true, `Vault ready at ${absoluteVaultPath}`),
20
+ createCheck('markdown-files', files.length > 0, `${files.length} markdown files found`),
21
+ 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`)
23
+ ];
24
+ return {
25
+ ok: checks.every((check) => check.ok),
26
+ checks
27
+ };
28
+ };
@@ -0,0 +1,15 @@
1
+ import { formatContextPackage, selectContextSections } from '../domain/context.js';
2
+ import { searchKnowledge } from './search-knowledge.js';
3
+ export const buildContextPackage = async (vaultPath, query, limit, maxTokens, agentId, mode) => {
4
+ const results = await searchKnowledge(vaultPath, query, limit, agentId, mode);
5
+ const sections = selectContextSections(results, maxTokens);
6
+ return {
7
+ query,
8
+ sections,
9
+ content: formatContextPackage(query, sections)
10
+ };
11
+ };
12
+ export const buildContext = async (vaultPath, query, limit, maxTokens, agentId, mode) => {
13
+ const contextPackage = await buildContextPackage(vaultPath, query, limit, maxTokens, agentId, mode);
14
+ return contextPackage.content;
15
+ };
@@ -0,0 +1,294 @@
1
+ export const createClientCss = () => `:root {
2
+ color-scheme: dark;
3
+ --bg: #0d0f12;
4
+ --panel: #15191f;
5
+ --panel-strong: #1c222b;
6
+ --line: #29313c;
7
+ --text: #edf2f7;
8
+ --muted: #99a5b5;
9
+ --accent: #35d0a2;
10
+ --accent-weak: rgba(53, 208, 162, 0.14);
11
+ --danger: #ff6b6b;
12
+ }
13
+
14
+ * {
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ html,
19
+ body {
20
+ width: 100%;
21
+ height: 100%;
22
+ margin: 0;
23
+ background: var(--bg);
24
+ color: var(--text);
25
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
26
+ }
27
+
28
+ button,
29
+ input,
30
+ select {
31
+ font: inherit;
32
+ }
33
+
34
+ .shell {
35
+ display: grid;
36
+ grid-template-columns: minmax(0, 1fr) 360px;
37
+ width: 100%;
38
+ height: 100svh;
39
+ overflow: hidden;
40
+ }
41
+
42
+ .workspace {
43
+ position: relative;
44
+ min-width: 0;
45
+ min-height: 0;
46
+ }
47
+
48
+ #graph {
49
+ display: block;
50
+ width: 100%;
51
+ height: 100%;
52
+ background:
53
+ radial-gradient(circle at 18% 20%, rgba(53, 208, 162, 0.12), transparent 28rem),
54
+ linear-gradient(135deg, #0d0f12 0%, #12161c 55%, #0a0d10 100%);
55
+ cursor: grab;
56
+ }
57
+
58
+ #graph:active {
59
+ cursor: grabbing;
60
+ }
61
+
62
+ .topbar {
63
+ position: absolute;
64
+ top: 18px;
65
+ left: 18px;
66
+ right: 18px;
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: space-between;
70
+ gap: 18px;
71
+ pointer-events: none;
72
+ }
73
+
74
+ .topbar > div {
75
+ display: flex;
76
+ align-items: baseline;
77
+ gap: 12px;
78
+ }
79
+
80
+ .topbar strong {
81
+ font-size: 18px;
82
+ }
83
+
84
+ .topbar span,
85
+ .eyebrow,
86
+ .inspector small {
87
+ color: var(--muted);
88
+ font-size: 12px;
89
+ }
90
+
91
+ .search {
92
+ width: min(420px, 42vw);
93
+ pointer-events: auto;
94
+ }
95
+
96
+ .agent-filter {
97
+ width: min(220px, 28vw);
98
+ pointer-events: auto;
99
+ }
100
+
101
+ .search input,
102
+ .agent-filter select {
103
+ width: 100%;
104
+ height: 40px;
105
+ border: 1px solid var(--line);
106
+ border-radius: 8px;
107
+ outline: none;
108
+ background: rgba(21, 25, 31, 0.88);
109
+ color: var(--text);
110
+ padding: 0 14px;
111
+ }
112
+
113
+ .search input:focus,
114
+ .agent-filter select:focus {
115
+ border-color: var(--accent);
116
+ }
117
+
118
+ .toolbar {
119
+ position: absolute;
120
+ left: 18px;
121
+ bottom: 18px;
122
+ display: flex;
123
+ gap: 8px;
124
+ }
125
+
126
+ .toolbar button {
127
+ width: 38px;
128
+ height: 38px;
129
+ border: 1px solid var(--line);
130
+ border-radius: 8px;
131
+ background: rgba(21, 25, 31, 0.88);
132
+ color: var(--text);
133
+ cursor: pointer;
134
+ }
135
+
136
+ .toolbar button:hover {
137
+ border-color: var(--accent);
138
+ color: var(--accent);
139
+ }
140
+
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;
151
+ }
152
+
153
+ .inspector h1,
154
+ .inspector h2,
155
+ .inspector p {
156
+ margin: 0;
157
+ }
158
+
159
+ .inspector h1 {
160
+ margin-top: 6px;
161
+ font-size: 26px;
162
+ line-height: 1.12;
163
+ overflow-wrap: anywhere;
164
+ }
165
+
166
+ .inspector h2 {
167
+ margin-bottom: 10px;
168
+ color: var(--muted);
169
+ font-size: 12px;
170
+ font-weight: 700;
171
+ text-transform: uppercase;
172
+ }
173
+
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
+ .tags {
206
+ display: flex;
207
+ flex-wrap: wrap;
208
+ gap: 8px;
209
+ }
210
+
211
+ .tags span {
212
+ max-width: 100%;
213
+ padding: 6px 9px;
214
+ border-radius: 999px;
215
+ background: var(--accent-weak);
216
+ color: var(--accent);
217
+ font-size: 12px;
218
+ overflow-wrap: anywhere;
219
+ }
220
+
221
+ ul {
222
+ display: grid;
223
+ gap: 8px;
224
+ margin: 0;
225
+ padding: 0;
226
+ list-style: none;
227
+ }
228
+
229
+ li {
230
+ padding: 10px 0;
231
+ border-bottom: 1px solid var(--line);
232
+ color: var(--text);
233
+ overflow-wrap: anywhere;
234
+ }
235
+
236
+ li button {
237
+ width: 100%;
238
+ padding: 0;
239
+ border: 0;
240
+ background: transparent;
241
+ color: var(--text);
242
+ text-align: left;
243
+ cursor: pointer;
244
+ }
245
+
246
+ li button:hover {
247
+ color: var(--accent);
248
+ }
249
+
250
+ li small {
251
+ display: block;
252
+ margin-top: 4px;
253
+ }
254
+
255
+ .note-content {
256
+ max-height: 32svh;
257
+ margin: 0;
258
+ padding: 12px;
259
+ border: 1px solid var(--line);
260
+ border-radius: 8px;
261
+ background: #101419;
262
+ color: var(--text);
263
+ white-space: pre-wrap;
264
+ overflow: auto;
265
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
266
+ font-size: 12px;
267
+ line-height: 1.5;
268
+ }
269
+
270
+ @media (max-width: 860px) {
271
+ .shell {
272
+ grid-template-columns: 1fr;
273
+ grid-template-rows: minmax(0, 1fr) 42svh;
274
+ }
275
+
276
+ .inspector {
277
+ border-left: 0;
278
+ border-top: 1px solid var(--line);
279
+ padding: 18px;
280
+ }
281
+
282
+ .topbar {
283
+ align-items: stretch;
284
+ flex-direction: column;
285
+ }
286
+
287
+ .search {
288
+ width: 100%;
289
+ }
290
+
291
+ .agent-filter {
292
+ width: 100%;
293
+ }
294
+ }`;
@@ -0,0 +1,66 @@
1
+ export const createClientHtml = () => `<!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Brainlink Graph</title>
7
+ <link rel="stylesheet" href="/styles.css" />
8
+ </head>
9
+ <body>
10
+ <main class="shell">
11
+ <section class="workspace" aria-label="Knowledge graph">
12
+ <canvas id="graph" aria-label="Brainlink knowledge graph"></canvas>
13
+ <div class="topbar">
14
+ <div>
15
+ <strong>Brainlink</strong>
16
+ <span id="stats">Loading graph</span>
17
+ </div>
18
+ <label class="search">
19
+ <input id="search" type="search" placeholder="Filter notes, tags or paths" autocomplete="off" />
20
+ </label>
21
+ <label class="agent-filter">
22
+ <select id="agent"></select>
23
+ </label>
24
+ </div>
25
+ <div class="toolbar" aria-label="Graph controls">
26
+ <button id="zoomIn" type="button" title="Zoom in">+</button>
27
+ <button id="zoomOut" type="button" title="Zoom out">-</button>
28
+ <button id="reset" type="button" title="Reset view">⌂</button>
29
+ </div>
30
+ </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
+ </main>
64
+ <script src="/app.js"></script>
65
+ </body>
66
+ </html>`;