@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
@@ -0,0 +1,54 @@
1
+ import { createServer } from 'node:http';
2
+ import { indexVault } from './index-vault.js';
3
+ import { startVaultWatcher } from './watch-vault.js';
4
+ import { assertPublicBindAllowed } from './server/host-security.js';
5
+ import { contentTypes, createJsonResponse, isHttpError } from './server/http.js';
6
+ import { route } from './server/routes.js';
7
+ export const startServer = async (input) => {
8
+ assertPublicBindAllowed(input.host, input.allowPublic);
9
+ if (input.shouldIndex) {
10
+ await indexVault(input.vaultPath);
11
+ }
12
+ const watcher = input.shouldWatch
13
+ ? startVaultWatcher({
14
+ vaultPath: input.vaultPath,
15
+ onError: (error) => console.error(error)
16
+ })
17
+ : null;
18
+ const server = createServer((request, response) => {
19
+ const url = new URL(request.url ?? '/', `http://${request.headers.host ?? input.host}`);
20
+ route(request, url, input.vaultPath)
21
+ .then((result) => {
22
+ response.writeHead(result.statusCode, result.headers);
23
+ response.end(result.body);
24
+ })
25
+ .catch((error) => {
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ const statusCode = isHttpError(error) ? error.statusCode : 500;
28
+ response.writeHead(statusCode, { 'content-type': contentTypes['.json'] });
29
+ response.end(createJsonResponse({ error: message }));
30
+ });
31
+ });
32
+ await new Promise((resolve, reject) => {
33
+ server.once('error', reject);
34
+ server.listen(input.port, input.host, () => {
35
+ server.off('error', reject);
36
+ resolve();
37
+ });
38
+ });
39
+ const address = server.address();
40
+ const port = typeof address === 'object' && address ? address.port : input.port;
41
+ return {
42
+ url: `http://${input.host}:${port}`,
43
+ close: () => new Promise((resolve, reject) => {
44
+ watcher?.close();
45
+ server.close((error) => {
46
+ if (error) {
47
+ reject(error);
48
+ return;
49
+ }
50
+ resolve();
51
+ });
52
+ })
53
+ };
54
+ };
@@ -0,0 +1,36 @@
1
+ import { watch } from 'node:fs';
2
+ import { indexVault } from './index-vault.js';
3
+ import { resolveVaultPath } from '../infrastructure/file-system-vault.js';
4
+ const shouldIgnore = (filename) => {
5
+ if (!filename) {
6
+ return false;
7
+ }
8
+ return filename.includes('.brainlink') || !filename.endsWith('.md');
9
+ };
10
+ export const startVaultWatcher = (input) => {
11
+ const absoluteVaultPath = resolveVaultPath(input.vaultPath);
12
+ const debounceMs = input.debounceMs ?? 350;
13
+ let timeout = null;
14
+ const schedule = (filename) => {
15
+ if (shouldIgnore(filename)) {
16
+ return;
17
+ }
18
+ if (timeout) {
19
+ clearTimeout(timeout);
20
+ }
21
+ timeout = setTimeout(() => {
22
+ indexVault(absoluteVaultPath).then(input.onIndex).catch(input.onError);
23
+ }, debounceMs);
24
+ };
25
+ const watcher = watch(absoluteVaultPath, { recursive: true }, (_eventType, filename) => {
26
+ schedule(filename?.toString() ?? null);
27
+ });
28
+ return {
29
+ close: () => {
30
+ if (timeout) {
31
+ clearTimeout(timeout);
32
+ }
33
+ watcher.close();
34
+ }
35
+ };
36
+ };
@@ -0,0 +1,88 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { performance } from 'node:perf_hooks';
5
+ import { indexVault } from '../application/index-vault.js';
6
+ import { searchKnowledge } from '../application/search-knowledge.js';
7
+ import { ensureVault } from '../infrastructure/file-system-vault.js';
8
+ const parsePositiveInteger = (value, fallback) => {
9
+ const parsed = Number.parseInt(value ?? '', 10);
10
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
11
+ };
12
+ const readStringOption = (args, name) => {
13
+ const index = args.indexOf(name);
14
+ const value = index >= 0 ? args[index + 1] : undefined;
15
+ return value && !value.startsWith('--') ? value : undefined;
16
+ };
17
+ const readOptions = (args) => ({
18
+ notes: parsePositiveInteger(readStringOption(args, '--notes'), 2000),
19
+ agent: readStringOption(args, '--agent') ?? 'benchmark-agent',
20
+ keep: args.includes('--keep')
21
+ });
22
+ const topics = [
23
+ 'authentication jwt token refresh policy',
24
+ 'sqlite graph backlinks markdown vault indexing',
25
+ 'frontend canvas layout graph interaction',
26
+ 'agent memory context retrieval summarization',
27
+ 'security local server vault path allowlist',
28
+ 'operations release npm github actions smoke test',
29
+ 'architecture functional core imperative shell boundaries'
30
+ ];
31
+ const formatNote = (index) => {
32
+ const topic = topics[index % topics.length];
33
+ const previous = index > 0 ? `[[Benchmark Note ${index - 1}]]` : '';
34
+ const related = index > 7 ? `[[Benchmark Note ${index - 7}]]` : '';
35
+ return [
36
+ `# Benchmark Note ${index}`,
37
+ '',
38
+ `This note captures ${topic}.`,
39
+ `The implementation detail number ${index} repeats ${topic} for semantic retrieval pressure.`,
40
+ `Related: ${[previous, related].filter(Boolean).join(' ')}`,
41
+ '',
42
+ `#benchmark #topic-${index % topics.length}`
43
+ ].join('\n');
44
+ };
45
+ const writeBenchmarkVault = async (vaultPath, options) => {
46
+ const agentPath = join(vaultPath, 'agents', options.agent);
47
+ await mkdir(agentPath, { recursive: true, mode: 0o700 });
48
+ await Promise.all(Array.from({ length: options.notes }, (_, index) => writeFile(join(agentPath, `benchmark-note-${index}.md`), formatNote(index), { encoding: 'utf8', mode: 0o600 })));
49
+ };
50
+ const timed = async (name, operation) => {
51
+ const start = performance.now();
52
+ const result = await operation();
53
+ return [name, performance.now() - start, result];
54
+ };
55
+ const printMetric = (name, durationMs) => {
56
+ console.log(`${name}: ${durationMs.toFixed(1)}ms`);
57
+ };
58
+ const main = async () => {
59
+ const options = readOptions(process.argv.slice(2));
60
+ const vaultPath = await mkdtemp(join(tmpdir(), 'brainlink-benchmark-'));
61
+ try {
62
+ await ensureVault(vaultPath);
63
+ const [, writeMs] = await timed('write', () => writeBenchmarkVault(vaultPath, options));
64
+ const [, indexMs, indexResult] = await timed('index', () => indexVault(vaultPath));
65
+ const [, semanticMs, semanticResults] = await timed('semantic search', () => searchKnowledge(vaultPath, 'authentication token policy', 10, options.agent, 'semantic'));
66
+ const [, hybridMs, hybridResults] = await timed('hybrid search', () => searchKnowledge(vaultPath, 'graph retrieval indexing', 10, options.agent, 'hybrid'));
67
+ console.log(`vault: ${vaultPath}`);
68
+ console.log(`notes: ${options.notes}`);
69
+ console.log(`documents: ${indexResult.documentCount}`);
70
+ console.log(`chunks: ${indexResult.chunkCount}`);
71
+ printMetric('write', writeMs);
72
+ printMetric('index', indexMs);
73
+ printMetric('semantic search', semanticMs);
74
+ printMetric('hybrid search', hybridMs);
75
+ console.log(`semantic top: ${semanticResults[0]?.title ?? 'none'}`);
76
+ console.log(`hybrid top: ${hybridResults[0]?.title ?? 'none'}`);
77
+ }
78
+ finally {
79
+ if (!options.keep) {
80
+ await rm(vaultPath, { recursive: true, force: true });
81
+ }
82
+ }
83
+ };
84
+ main().catch((error) => {
85
+ const message = error instanceof Error ? error.message : String(error);
86
+ console.error(message);
87
+ process.exitCode = 1;
88
+ });
@@ -0,0 +1,149 @@
1
+ import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../../application/analyze-vault.js';
2
+ import { buildContextPackage } from '../../application/build-context.js';
3
+ import { getGraph } from '../../application/get-graph.js';
4
+ import { listAgents } from '../../application/list-agents.js';
5
+ import { listBacklinks, listLinks } from '../../application/list-links.js';
6
+ import { searchKnowledge } from '../../application/search-knowledge.js';
7
+ import { sanitizeSearchMode } from '../../infrastructure/config.js';
8
+ import { parsePositiveInteger, print, resolveOptions } from '../runtime.js';
9
+ export const registerReadCommands = (program) => {
10
+ program
11
+ .command('search')
12
+ .argument('<query>', 'search query')
13
+ .option('-v, --vault <vault>', 'vault directory', '.')
14
+ .option('-a, --agent <agent>', 'filter by agent memory namespace')
15
+ .option('-l, --limit <limit>', 'maximum results', '10')
16
+ .option('-m, --mode <mode>', 'search mode: fts, semantic or hybrid')
17
+ .option('--json', 'print machine-readable JSON')
18
+ .description('search indexed knowledge')
19
+ .action(async (query, options) => {
20
+ const resolved = await resolveOptions(options);
21
+ const limit = parsePositiveInteger(options.limit ?? String(resolved.config.defaultSearchLimit), resolved.config.defaultSearchLimit);
22
+ const mode = sanitizeSearchMode(options.mode, resolved.config.defaultSearchMode);
23
+ const results = await searchKnowledge(resolved.vault, query, limit, options.agent, mode);
24
+ print(options.json, { query, agent: options.agent, limit, mode, results }, () => results
25
+ .map((result, index) => [`${index + 1}. ${result.title} (${result.path}) score=${result.score.toFixed(3)} mode=${result.searchMode}`, result.content].join('\n'))
26
+ .join('\n\n'));
27
+ });
28
+ program
29
+ .command('links')
30
+ .option('-v, --vault <vault>', 'vault directory', '.')
31
+ .option('-a, --agent <agent>', 'filter by agent memory namespace')
32
+ .option('--json', 'print machine-readable JSON')
33
+ .description('list indexed wiki links')
34
+ .action(async (options) => {
35
+ const resolved = await resolveOptions(options);
36
+ const links = await listLinks(resolved.vault, options.agent);
37
+ print(options.json, { links }, () => links
38
+ .map((link) => {
39
+ const target = link.toPath ? `${link.toTitle} (${link.toPath})` : `${link.toTitle} (unresolved)`;
40
+ return `${link.fromTitle} (${link.fromPath}) -> ${target}`;
41
+ })
42
+ .join('\n'));
43
+ });
44
+ program
45
+ .command('backlinks')
46
+ .argument('<title>', 'target note title')
47
+ .option('-v, --vault <vault>', 'vault directory', '.')
48
+ .option('-a, --agent <agent>', 'filter by agent memory namespace')
49
+ .option('--json', 'print machine-readable JSON')
50
+ .description('list notes linking to a target note')
51
+ .action(async (title, options) => {
52
+ const resolved = await resolveOptions(options);
53
+ const backlinks = await listBacklinks(resolved.vault, title, options.agent);
54
+ print(options.json, { title, backlinks }, () => backlinks.map((link) => `${link.fromTitle} (${link.fromPath}) -> ${link.toTitle}`).join('\n'));
55
+ });
56
+ program
57
+ .command('context')
58
+ .argument('<query>', 'context query')
59
+ .option('-v, --vault <vault>', 'vault directory', '.')
60
+ .option('-a, --agent <agent>', 'filter by agent memory namespace')
61
+ .option('-l, --limit <limit>', 'maximum search results before context selection', '12')
62
+ .option('-t, --tokens <tokens>', 'maximum estimated context tokens', '2000')
63
+ .option('-m, --mode <mode>', 'search mode: fts, semantic or hybrid')
64
+ .option('--json', 'print machine-readable JSON')
65
+ .description('build a compact context package for an agent')
66
+ .action(async (query, options) => {
67
+ const resolved = await resolveOptions(options);
68
+ const mode = sanitizeSearchMode(options.mode, resolved.config.defaultSearchMode);
69
+ const contextPackage = await buildContextPackage(resolved.vault, query, parsePositiveInteger(options.limit ?? '12', 12), parsePositiveInteger(options.tokens ?? String(resolved.config.defaultContextTokens), resolved.config.defaultContextTokens), options.agent, mode);
70
+ print(options.json, contextPackage, () => contextPackage.content);
71
+ });
72
+ program
73
+ .command('graph')
74
+ .option('-v, --vault <vault>', 'vault directory', '.')
75
+ .option('-a, --agent <agent>', 'filter by agent memory namespace')
76
+ .option('--json', 'print machine-readable JSON')
77
+ .description('print indexed graph data')
78
+ .action(async (options) => {
79
+ const resolved = await resolveOptions(options);
80
+ const graph = await getGraph(resolved.vault, options.agent);
81
+ print(options.json, graph, () => JSON.stringify(graph, null, 2));
82
+ });
83
+ program
84
+ .command('agents')
85
+ .option('-v, --vault <vault>', 'vault directory', '.')
86
+ .option('--json', 'print machine-readable JSON')
87
+ .description('list indexed agent memory namespaces')
88
+ .action(async (options) => {
89
+ const resolved = await resolveOptions(options);
90
+ const agents = await listAgents(resolved.vault);
91
+ print(options.json, { agents }, () => agents.map((agent) => `${agent.id}: ${agent.documentCount} documents`).join('\n'));
92
+ });
93
+ program
94
+ .command('stats')
95
+ .option('-v, --vault <vault>', 'vault directory', '.')
96
+ .option('-a, --agent <agent>', 'filter by agent memory namespace')
97
+ .option('--json', 'print machine-readable JSON')
98
+ .description('print indexed vault statistics')
99
+ .action(async (options) => {
100
+ const resolved = await resolveOptions(options);
101
+ const stats = await getStats(resolved.vault, options.agent);
102
+ print(options.json, stats, () => [
103
+ `Documents: ${stats.documentCount}`,
104
+ `Links: ${stats.linkCount}`,
105
+ `Resolved links: ${stats.resolvedLinkCount}`,
106
+ `Broken links: ${stats.brokenLinkCount}`,
107
+ `Orphans: ${stats.orphanCount}`,
108
+ `Tags: ${stats.tagCount}`
109
+ ].join('\n'));
110
+ });
111
+ program
112
+ .command('broken-links')
113
+ .option('-v, --vault <vault>', 'vault directory', '.')
114
+ .option('-a, --agent <agent>', 'filter by agent memory namespace')
115
+ .option('--json', 'print machine-readable JSON')
116
+ .description('list unresolved wiki links')
117
+ .action(async (options) => {
118
+ const resolved = await resolveOptions(options);
119
+ const brokenLinks = await getBrokenLinksReport(resolved.vault, options.agent);
120
+ print(options.json, { brokenLinks }, () => brokenLinks.length === 0
121
+ ? 'No broken links found'
122
+ : brokenLinks.map((link) => `${link.fromTitle} (${link.fromPath}) -> ${link.toTitle}`).join('\n'));
123
+ });
124
+ program
125
+ .command('orphans')
126
+ .option('-v, --vault <vault>', 'vault directory', '.')
127
+ .option('-a, --agent <agent>', 'filter by agent memory namespace')
128
+ .option('--json', 'print machine-readable JSON')
129
+ .description('list indexed notes without incoming or outgoing links')
130
+ .action(async (options) => {
131
+ const resolved = await resolveOptions(options);
132
+ const orphans = await getOrphansReport(resolved.vault, options.agent);
133
+ print(options.json, { orphans }, () => orphans.length === 0 ? 'No orphan notes found' : orphans.map((node) => `${node.title} (${node.path})`).join('\n'));
134
+ });
135
+ program
136
+ .command('validate')
137
+ .option('-v, --vault <vault>', 'vault directory', '.')
138
+ .option('-a, --agent <agent>', 'filter by agent memory namespace')
139
+ .option('--json', 'print machine-readable JSON')
140
+ .description('validate indexed vault graph health')
141
+ .action(async (options) => {
142
+ const resolved = await resolveOptions(options);
143
+ const validation = await validateVault(resolved.vault, options.agent);
144
+ print(options.json, validation, () => validation.ok
145
+ ? 'Vault validation passed'
146
+ : `Vault validation failed: ${validation.brokenLinks.length} broken links, ${validation.orphans.length} orphan notes`);
147
+ process.exitCode = validation.ok ? 0 : 1;
148
+ });
149
+ };
@@ -0,0 +1,107 @@
1
+ import { addNote } from '../../application/add-note.js';
2
+ import { indexVault } from '../../application/index-vault.js';
3
+ import { startServer } from '../../application/start-server.js';
4
+ import { startVaultWatcher } from '../../application/watch-vault.js';
5
+ import { doctorVault } from '../../application/analyze-vault.js';
6
+ import { loadBrainlinkConfig } from '../../infrastructure/config.js';
7
+ import { assertVaultAllowed, ensureVault } from '../../infrastructure/file-system-vault.js';
8
+ import { parsePositiveInteger, print, resolveOptions } from '../runtime.js';
9
+ export const registerWriteCommands = (program) => {
10
+ program
11
+ .command('init')
12
+ .argument('[vault]', 'vault directory', '.')
13
+ .option('--json', 'print machine-readable JSON')
14
+ .description('initialize a Brainlink vault')
15
+ .action(async (vault, options) => {
16
+ const config = await loadBrainlinkConfig();
17
+ const path = await ensureVault(assertVaultAllowed(vault, config.allowedVaults));
18
+ print(options.json, { path }, () => `Initialized Brainlink vault at ${path}`);
19
+ });
20
+ program
21
+ .command('add')
22
+ .argument('<title>', 'note title')
23
+ .requiredOption('-c, --content <content>', 'markdown content')
24
+ .option('-v, --vault <vault>', 'vault directory', '.')
25
+ .option('-a, --agent <agent>', 'agent memory namespace', 'shared')
26
+ .option('--allow-sensitive', 'allow writing content that looks like a secret')
27
+ .option('--json', 'print machine-readable JSON')
28
+ .description('add a markdown note to the vault')
29
+ .action(async (title, options) => {
30
+ const resolved = await resolveOptions(options);
31
+ const path = await addNote(resolved.vault, title, options.content, options.agent, {
32
+ allowSensitive: Boolean(options.allowSensitive)
33
+ });
34
+ print(options.json, { title, agent: options.agent ?? 'shared', path }, () => `Created note at ${path}`);
35
+ });
36
+ program
37
+ .command('index')
38
+ .option('-v, --vault <vault>', 'vault directory', '.')
39
+ .option('--json', 'print machine-readable JSON')
40
+ .description('index markdown notes, links, tags and chunks')
41
+ .action(async (options) => {
42
+ const resolved = await resolveOptions(options);
43
+ const result = await indexVault(resolved.vault);
44
+ print(options.json, result, () => `Indexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`);
45
+ });
46
+ program
47
+ .command('doctor')
48
+ .option('-v, --vault <vault>', 'vault directory', '.')
49
+ .option('--json', 'print machine-readable JSON')
50
+ .description('run Brainlink environment and vault checks')
51
+ .action(async (options) => {
52
+ const resolved = await resolveOptions(options);
53
+ const report = await doctorVault(resolved.vault);
54
+ print(options.json, report, () => report.checks.map((check) => `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.message}`).join('\n'));
55
+ process.exitCode = report.ok ? 0 : 1;
56
+ });
57
+ program
58
+ .command('watch')
59
+ .option('-v, --vault <vault>', 'vault directory', '.')
60
+ .option('--json', 'print machine-readable JSON events')
61
+ .description('watch markdown files and reindex on changes')
62
+ .action(async (options) => {
63
+ const resolved = await resolveOptions(options);
64
+ const initial = await indexVault(resolved.vault);
65
+ const watcher = startVaultWatcher({
66
+ vaultPath: resolved.vault,
67
+ onIndex: (result) => {
68
+ print(options.json, { event: 'indexed', result }, () => `Indexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`);
69
+ },
70
+ onError: (error) => {
71
+ const message = error instanceof Error ? error.message : String(error);
72
+ print(options.json, { event: 'error', message }, () => message);
73
+ }
74
+ });
75
+ print(options.json, { event: 'watching', vault: resolved.vault, initial }, () => `Watching ${resolved.vault}`);
76
+ process.once('SIGINT', () => {
77
+ watcher.close();
78
+ process.exit(0);
79
+ });
80
+ process.once('SIGTERM', () => {
81
+ watcher.close();
82
+ process.exit(0);
83
+ });
84
+ });
85
+ program
86
+ .command('server')
87
+ .option('-v, --vault <vault>', 'vault directory', '.')
88
+ .option('-h, --host <host>', 'server host', '127.0.0.1')
89
+ .option('-p, --port <port>', 'server port', '4321')
90
+ .option('--no-index', 'skip indexing before starting the server')
91
+ .option('-w, --watch', 'watch markdown files and reindex on changes')
92
+ .option('--allow-public', 'allow binding the server to a non-loopback host')
93
+ .option('--json', 'print machine-readable JSON')
94
+ .description('start a local web UI for the knowledge graph')
95
+ .action(async (options) => {
96
+ const resolved = await resolveOptions(options);
97
+ const server = await startServer({
98
+ vaultPath: resolved.vault,
99
+ host: options.host ?? resolved.config.host,
100
+ port: parsePositiveInteger(options.port ?? String(resolved.config.port), resolved.config.port),
101
+ shouldIndex: options.index,
102
+ shouldWatch: Boolean(options.watch),
103
+ allowPublic: Boolean(options.allowPublic)
104
+ });
105
+ print(options.json, { url: server.url, watch: Boolean(options.watch), readonly: true }, () => `Brainlink graph server running at ${server.url}`);
106
+ });
107
+ };
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { basename } from 'node:path';
4
+ import { registerReadCommands } from './commands/read-commands.js';
5
+ import { registerWriteCommands } from './commands/write-commands.js';
6
+ const program = new Command();
7
+ const cliName = basename(process.argv[1] ?? 'brainlink');
8
+ const displayName = cliName === 'blink' ? 'blink' : 'brainlink';
9
+ const aliasName = displayName === 'blink' ? 'brainlink' : 'blink';
10
+ program
11
+ .name(displayName)
12
+ .alias(aliasName)
13
+ .description('Local-first knowledge memory for agents')
14
+ .version('0.1.0');
15
+ registerWriteCommands(program);
16
+ registerReadCommands(program);
17
+ program.parseAsync().catch((error) => {
18
+ const message = error instanceof Error ? error.message : String(error);
19
+ console.error(message);
20
+ process.exitCode = 1;
21
+ });
@@ -0,0 +1,18 @@
1
+ import { loadBrainlinkConfig } from '../infrastructure/config.js';
2
+ import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
3
+ export const parsePositiveInteger = (value, fallback) => {
4
+ const parsed = Number.parseInt(value, 10);
5
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
6
+ };
7
+ export const resolveOptions = async (options) => {
8
+ const config = await loadBrainlinkConfig();
9
+ const vault = options.vault ?? config.vault;
10
+ const allowedVault = assertVaultAllowed(vault, config.allowedVaults);
11
+ return {
12
+ config,
13
+ vault: allowedVault
14
+ };
15
+ };
16
+ export const print = (json, value, human) => {
17
+ console.log(json ? JSON.stringify(value, null, 2) : human());
18
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ export const sharedAgentId = 'shared';
2
+ export const sanitizeAgentId = (agentId) => agentId
3
+ .normalize('NFKD')
4
+ .replace(/[\u0300-\u036f]/g, '')
5
+ .toLowerCase()
6
+ .replace(/[^a-z0-9_-]+/g, '-')
7
+ .replace(/^-+|-+$/g, '') || sharedAgentId;
8
+ export const resolveAgentIdFromPath = (path) => {
9
+ const [scope, agentId] = path.split('/');
10
+ return scope === 'agents' && agentId ? sanitizeAgentId(agentId) : sharedAgentId;
11
+ };
@@ -0,0 +1,44 @@
1
+ export const selectContextSections = (results, maxTokens) => {
2
+ const selected = results.reduce((state, result) => {
3
+ const tokenCost = Math.ceil(result.content.length / 4);
4
+ if (state.usedTokens + tokenCost > maxTokens || state.seenDocuments.has(result.documentId)) {
5
+ return state;
6
+ }
7
+ return {
8
+ usedTokens: state.usedTokens + tokenCost,
9
+ sections: [
10
+ ...state.sections,
11
+ {
12
+ title: result.title,
13
+ path: result.path,
14
+ content: result.content,
15
+ score: result.score,
16
+ searchMode: result.searchMode,
17
+ tags: result.tags
18
+ }
19
+ ],
20
+ seenDocuments: new Set([...state.seenDocuments, result.documentId])
21
+ };
22
+ }, {
23
+ usedTokens: 0,
24
+ sections: [],
25
+ seenDocuments: new Set()
26
+ });
27
+ return selected.sections;
28
+ };
29
+ export const formatContextPackage = (query, sections) => {
30
+ const body = sections
31
+ .map((section, index) => [
32
+ `## ${index + 1}. ${section.title}`,
33
+ `Source: ${section.path}`,
34
+ section.tags.length > 0 ? `Tags: ${section.tags.map((tag) => `#${tag}`).join(' ')}` : null,
35
+ `Score: ${section.score.toFixed(3)}`,
36
+ `Mode: ${section.searchMode}`,
37
+ '',
38
+ section.content
39
+ ]
40
+ .filter((value) => value !== null)
41
+ .join('\n'))
42
+ .join('\n\n');
43
+ return [`# Brainlink Context`, `Query: ${query}`, '', body || 'No relevant context found.'].join('\n');
44
+ };
@@ -0,0 +1,117 @@
1
+ const localDimensions = 192;
2
+ const defaultEmbeddingBucketCount = 24;
3
+ const tokenPattern = /[\p{L}\p{N}_-]+/gu;
4
+ const stopWords = new Set([
5
+ 'a',
6
+ 'as',
7
+ 'and',
8
+ 'ao',
9
+ 'aos',
10
+ 'com',
11
+ 'da',
12
+ 'das',
13
+ 'de',
14
+ 'do',
15
+ 'dos',
16
+ 'e',
17
+ 'em',
18
+ 'for',
19
+ 'is',
20
+ 'o',
21
+ 'os',
22
+ 'para',
23
+ 'por',
24
+ 'que',
25
+ 'the',
26
+ 'to',
27
+ 'um',
28
+ 'uma',
29
+ 'use',
30
+ 'uses',
31
+ 'using'
32
+ ]);
33
+ const aliases = {
34
+ ai: ['agent', 'model'],
35
+ api: ['interface', 'client'],
36
+ auth: ['authentication', 'authorization', 'identity'],
37
+ authentication: ['auth', 'identity'],
38
+ backend: ['server', 'api'],
39
+ cli: ['terminal', 'command'],
40
+ context: ['memory', 'knowledge'],
41
+ db: ['database', 'storage'],
42
+ frontend: ['ui', 'browser'],
43
+ jwt: ['token', 'auth', 'authentication'],
44
+ llm: ['ai', 'agent', 'model'],
45
+ mcp: ['agent', 'tool', 'integration'],
46
+ memory: ['context', 'knowledge'],
47
+ nodejs: ['node', 'runtime'],
48
+ test: ['tests', 'testing', 'validation'],
49
+ tests: ['test', 'testing', 'validation'],
50
+ token: ['jwt', 'auth'],
51
+ ui: ['frontend', 'browser']
52
+ };
53
+ const normalizeToken = (token) => token
54
+ .normalize('NFKD')
55
+ .replace(/\p{Diacritic}/gu, '')
56
+ .toLowerCase();
57
+ const tokenize = (input) => input
58
+ .match(tokenPattern)
59
+ ?.map(normalizeToken)
60
+ .filter((token) => token.length > 1 && !stopWords.has(token)) ?? [];
61
+ const expandTokens = (tokens) => tokens.flatMap((token) => [token, ...(aliases[token] ?? [])]);
62
+ const hash = (value) => Array.from(value).reduce((state, char) => Math.imul(state ^ char.charCodeAt(0), 16777619), 2166136261) >>> 0;
63
+ const featureHash = (feature) => {
64
+ const value = hash(feature);
65
+ const index = value % localDimensions;
66
+ const sign = value & 1 ? 1 : -1;
67
+ return [index, sign];
68
+ };
69
+ const normalizeVector = (vector) => {
70
+ const magnitude = Math.hypot(...vector);
71
+ return magnitude === 0 ? vector : vector.map((value) => value / magnitude);
72
+ };
73
+ const applyFeature = (vector, feature, weight) => {
74
+ const [index, sign] = featureHash(feature);
75
+ vector[index] = (vector[index] ?? 0) + sign * weight;
76
+ return vector;
77
+ };
78
+ const tokenFeatures = (tokens) => [
79
+ ...tokens.map((token) => `t:${token}`),
80
+ ...tokens.slice(0, -1).map((token, index) => `b:${token}:${tokens[index + 1]}`)
81
+ ];
82
+ export const createLocalEmbedding = (input) => {
83
+ const tokens = expandTokens(tokenize(input));
84
+ const initial = Array.from({ length: localDimensions }, () => 0);
85
+ const weighted = tokenFeatures(tokens).reduce((vector, feature) => applyFeature(vector, feature, feature.startsWith('b:') ? 0.65 : 1), initial);
86
+ return normalizeVector(weighted);
87
+ };
88
+ export const cosineSimilarity = (left, right) => {
89
+ const length = Math.min(left.length, right.length);
90
+ if (length === 0) {
91
+ return 0;
92
+ }
93
+ const dot = left.slice(0, length).reduce((total, value, index) => total + value * (right[index] ?? 0), 0);
94
+ const leftMagnitude = Math.hypot(...left.slice(0, length));
95
+ const rightMagnitude = Math.hypot(...right.slice(0, length));
96
+ return leftMagnitude === 0 || rightMagnitude === 0 ? 0 : dot / (leftMagnitude * rightMagnitude);
97
+ };
98
+ const bucketKey = (index, value) => `${value >= 0 ? 'p' : 'n'}:${index}`;
99
+ export const createEmbeddingBuckets = (vector, bucketCount = defaultEmbeddingBucketCount) => vector
100
+ .map((value, index) => ({
101
+ index,
102
+ value,
103
+ weight: Math.abs(value)
104
+ }))
105
+ .filter((item) => item.weight > 0)
106
+ .sort((left, right) => right.weight - left.weight || left.index - right.index)
107
+ .slice(0, bucketCount)
108
+ .map((item) => bucketKey(item.index, item.value));
109
+ export const createDisabledEmbeddingProvider = () => ({
110
+ name: 'none',
111
+ embed: async (input) => input.map(() => [])
112
+ });
113
+ export const createLocalEmbeddingProvider = () => ({
114
+ name: 'local',
115
+ embed: async (input) => input.map(createLocalEmbedding)
116
+ });
117
+ export const createEmbeddingProvider = (name) => name === 'local' ? createLocalEmbeddingProvider() : createDisabledEmbeddingProvider();