@andespindola/brainlink 0.1.0-beta.99 → 1.0.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 (46) hide show
  1. package/AGENTS.md +6 -6
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +186 -38
  4. package/dist/application/add-note.js +13 -44
  5. package/dist/application/analyze-vault.js +1 -1
  6. package/dist/application/auto-migrate-configured-vault.js +37 -0
  7. package/dist/application/build-context.js +119 -20
  8. package/dist/application/canonical-context-links.js +209 -0
  9. package/dist/application/frontend/client-css.js +212 -42
  10. package/dist/application/frontend/client-html.js +42 -28
  11. package/dist/application/frontend/client-js.js +1294 -3222
  12. package/dist/application/frontend/client-render-worker-js.js +676 -0
  13. package/dist/application/get-graph-contexts.js +33 -0
  14. package/dist/application/get-graph-layout.js +62 -8
  15. package/dist/application/get-graph-stream-chunk.js +326 -0
  16. package/dist/application/get-graph-view.js +246 -0
  17. package/dist/application/graph-view-state.js +66 -0
  18. package/dist/application/import-legacy-sqlite.js +3 -33
  19. package/dist/application/index-vault.js +35 -22
  20. package/dist/application/migrate-context-links.js +79 -0
  21. package/dist/application/search-graph-node-ids.js +63 -3
  22. package/dist/application/server/routes.js +197 -12
  23. package/dist/cli/commands/read-commands.js +39 -3
  24. package/dist/cli/commands/vault-commands.js +182 -0
  25. package/dist/cli/commands/write-commands.js +147 -12
  26. package/dist/cli/main.js +2 -0
  27. package/dist/cli/runtime.js +10 -2
  28. package/dist/domain/context.js +1 -0
  29. package/dist/domain/graph-contexts.js +180 -0
  30. package/dist/domain/graph-layout.js +347 -21
  31. package/dist/domain/markdown.js +53 -9
  32. package/dist/infrastructure/config.js +105 -6
  33. package/dist/infrastructure/context-packs.js +122 -0
  34. package/dist/infrastructure/file-index.js +6 -3
  35. package/dist/infrastructure/index-state.js +2 -0
  36. package/dist/infrastructure/vault-migration-state.js +69 -0
  37. package/dist/infrastructure/volatile-memory.js +100 -0
  38. package/dist/mcp/http-server.js +97 -0
  39. package/dist/mcp/runtime.js +20 -0
  40. package/dist/mcp/server.js +36 -13
  41. package/dist/mcp/tools.js +203 -14
  42. package/docs/AGENT_USAGE.md +50 -5
  43. package/docs/ARCHITECTURE.md +11 -0
  44. package/docs/QUICKSTART.md +3 -1
  45. package/docs/RELEASE.md +4 -3
  46. package/package.json +3 -1
@@ -1,37 +1,50 @@
1
1
  import { stat } from 'node:fs/promises';
2
+ import { performance } from 'node:perf_hooks';
2
3
  import { formatContextPackage, selectContextSections } from '../domain/context.js';
4
+ import { readContextPack, writeContextPack } from '../infrastructure/context-packs.js';
3
5
  import { indexStoragePath } from '../infrastructure/file-index.js';
6
+ import { searchVolatileMemory, volatileMemoryStoragePath } from '../infrastructure/volatile-memory.js';
4
7
  import { searchKnowledge } from './search-knowledge.js';
5
- const contextCacheTtlMs = 45_000;
6
8
  const contextCacheMaxEntries = 200;
7
9
  const contextCache = new Map();
8
- const readIndexMtimeMs = async (vaultPath) => {
10
+ const readFileSignature = async (path) => {
9
11
  try {
10
- return (await stat(indexStoragePath(vaultPath))).mtimeMs;
12
+ const info = await stat(path);
13
+ return `${Math.floor(info.mtimeMs)}:${info.size}`;
11
14
  }
12
15
  catch {
13
- return 0;
16
+ return '0:0';
14
17
  }
15
18
  };
16
- const toCacheKey = (vaultPath, query, limit, maxTokens, agentId, mode) => JSON.stringify({
19
+ export const readContextDataSignature = async (vaultPath) => `${await readFileSignature(indexStoragePath(vaultPath))}|${await readFileSignature(volatileMemoryStoragePath(vaultPath))}`;
20
+ const toCacheKey = (vaultPath, query, limit, maxTokens, agentId, mode, strategy) => JSON.stringify({
17
21
  vaultPath,
18
22
  query: query.trim().toLowerCase(),
19
23
  limit,
20
24
  maxTokens,
21
25
  agentId: agentId?.trim().toLowerCase() ?? '*',
22
- mode: mode ?? 'default'
26
+ mode: mode ?? 'default',
27
+ strategy
23
28
  });
24
- const contextCacheGet = (key, indexMtimeMs) => {
29
+ const withCacheMetadata = (context, cache) => ({
30
+ ...context,
31
+ cache
32
+ });
33
+ const contextCacheGet = (key, dataSignature, contextCacheTtlMs) => {
25
34
  const entry = contextCache.get(key);
26
35
  if (!entry) {
27
36
  return undefined;
28
37
  }
29
- const fresh = Date.now() - entry.createdAt <= contextCacheTtlMs && entry.indexMtimeMs === indexMtimeMs;
38
+ const fresh = Date.now() - entry.createdAt <= contextCacheTtlMs && entry.dataSignature === dataSignature;
30
39
  if (!fresh) {
31
40
  contextCache.delete(key);
32
41
  return undefined;
33
42
  }
34
- return entry.context;
43
+ return withCacheMetadata(entry.context, {
44
+ storage: 'memory',
45
+ status: 'hit',
46
+ dataSignature
47
+ });
35
48
  };
36
49
  const contextCacheSet = (entry) => {
37
50
  contextCache.set(entry.key, entry);
@@ -42,29 +55,115 @@ const contextCacheSet = (entry) => {
42
55
  const keys = Array.from(contextCache.keys()).slice(0, overflow);
43
56
  keys.forEach((key) => contextCache.delete(key));
44
57
  };
45
- export const buildContextPackage = async (vaultPath, query, limit, maxTokens, agentId, mode) => {
46
- const cacheKey = toCacheKey(vaultPath, query, limit, maxTokens, agentId, mode);
47
- const indexMtimeMs = await readIndexMtimeMs(vaultPath);
48
- const cached = contextCacheGet(cacheKey, indexMtimeMs);
58
+ const elapsedMs = (start) => Math.round((performance.now() - start) * 1000) / 1000;
59
+ const estimateSectionTokens = (context) => Math.ceil(context.content.length / 4);
60
+ const emptyMetrics = (context, totalMs, overrides = {}) => ({
61
+ totalMs,
62
+ packReadMs: 0,
63
+ searchMs: 0,
64
+ selectionMs: 0,
65
+ volatileMs: 0,
66
+ packWriteMs: 0,
67
+ sectionCount: context.sections.length,
68
+ volatileSectionCount: context.volatileSections?.length ?? 0,
69
+ estimatedTokens: estimateSectionTokens(context),
70
+ ...overrides
71
+ });
72
+ export const buildContextPackage = async (vaultPath, query, limit, maxTokens, agentId, mode, strategy = 'rag', contextCacheTtlMs = 120_000) => {
73
+ const totalStart = performance.now();
74
+ const cacheKey = toCacheKey(vaultPath, query, limit, maxTokens, agentId, mode, strategy);
75
+ const dataSignature = await readContextDataSignature(vaultPath);
76
+ const cached = contextCacheGet(cacheKey, dataSignature, contextCacheTtlMs);
49
77
  if (cached) {
50
78
  return cached;
51
79
  }
80
+ const shouldUseContextPack = strategy === 'cag' || strategy === 'auto';
81
+ let packReadMs = 0;
82
+ if (shouldUseContextPack) {
83
+ const packReadStart = performance.now();
84
+ const pack = await readContextPack(vaultPath, { query, limit, maxTokens, agentId, mode }, dataSignature);
85
+ packReadMs = elapsedMs(packReadStart);
86
+ if (pack.status === 'hit') {
87
+ const contextFromPack = {
88
+ ...pack.context,
89
+ strategy: 'cag',
90
+ requestedStrategy: strategy,
91
+ recommendedStrategy: {
92
+ strategy: 'cag',
93
+ reason: strategy === 'auto'
94
+ ? 'A fresh context pack exists, so auto selected CAG.'
95
+ : 'The requested CAG context pack is fresh.'
96
+ },
97
+ metrics: emptyMetrics(pack.context, elapsedMs(totalStart), { packReadMs })
98
+ };
99
+ contextCacheSet({
100
+ key: cacheKey,
101
+ createdAt: Date.now(),
102
+ dataSignature,
103
+ strategy,
104
+ context: contextFromPack
105
+ });
106
+ return contextFromPack;
107
+ }
108
+ }
109
+ const searchStart = performance.now();
52
110
  const results = await searchKnowledge(vaultPath, query, limit, agentId, mode);
53
- const sections = selectContextSections(results, maxTokens);
111
+ const searchMs = elapsedMs(searchStart);
112
+ const selectionStart = performance.now();
113
+ const durableSections = selectContextSections(results, maxTokens);
114
+ const selectionMs = elapsedMs(selectionStart);
115
+ const volatileStart = performance.now();
116
+ const volatileSections = await searchVolatileMemory(vaultPath, query, Math.min(3, limit), agentId, mode ?? 'hybrid');
117
+ const volatileMs = elapsedMs(volatileStart);
118
+ const sections = [...volatileSections, ...durableSections];
119
+ const effectiveStrategy = strategy === 'cag' ? 'cag' : 'rag';
54
120
  const context = {
55
121
  query,
56
122
  sections,
57
- content: formatContextPackage(query, sections)
123
+ content: formatContextPackage(query, sections),
124
+ ...(volatileSections.length > 0 ? { volatileSections } : {}),
125
+ strategy: effectiveStrategy,
126
+ requestedStrategy: strategy,
127
+ recommendedStrategy: {
128
+ strategy: effectiveStrategy,
129
+ reason: strategy === 'auto'
130
+ ? 'No fresh context pack was available, so auto selected fresh RAG assembly and refreshed a pack for future reuse.'
131
+ : effectiveStrategy === 'cag'
132
+ ? 'CAG was requested; Brainlink refreshed the context pack from current retrieval results.'
133
+ : 'RAG was requested; Brainlink assembled context directly from current retrieval results.'
134
+ }
135
+ };
136
+ const packWriteStart = performance.now();
137
+ const packPath = shouldUseContextPack
138
+ ? await writeContextPack(vaultPath, { query, limit, maxTokens, agentId, mode }, dataSignature, context)
139
+ : undefined;
140
+ const packWriteMs = packPath ? elapsedMs(packWriteStart) : 0;
141
+ const contextWithMetadata = withCacheMetadata(context, {
142
+ storage: shouldUseContextPack ? 'context-pack' : 'memory',
143
+ status: shouldUseContextPack ? 'refresh' : 'miss',
144
+ dataSignature,
145
+ ...(packPath ? { path: packPath } : {})
146
+ });
147
+ const contextWithMetrics = {
148
+ ...contextWithMetadata,
149
+ metrics: emptyMetrics(contextWithMetadata, elapsedMs(totalStart), {
150
+ packReadMs,
151
+ searchMs,
152
+ selectionMs,
153
+ volatileMs,
154
+ packWriteMs
155
+ })
58
156
  };
59
157
  contextCacheSet({
60
158
  key: cacheKey,
61
159
  createdAt: Date.now(),
62
- indexMtimeMs,
63
- context
160
+ dataSignature,
161
+ strategy,
162
+ context: contextWithMetrics
64
163
  });
65
- return context;
164
+ return contextWithMetrics;
66
165
  };
67
- export const buildContext = async (vaultPath, query, limit, maxTokens, agentId, mode) => {
68
- const contextPackage = await buildContextPackage(vaultPath, query, limit, maxTokens, agentId, mode);
166
+ export const buildContext = async (vaultPath, query, limit, maxTokens, agentId, mode, strategy, contextCacheTtlMs = 120_000) => {
167
+ const contextPackage = await buildContextPackage(vaultPath, query, limit, maxTokens, agentId, mode, strategy, contextCacheTtlMs);
69
168
  return contextPackage.content;
70
169
  };
@@ -0,0 +1,209 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { inferVisualGraphContext } from '../domain/graph-contexts.js';
4
+ import { sanitizeAgentId, sharedAgentId } from '../domain/agents.js';
5
+ import { extractContextLinkWeights, parseMarkdownDocument } from '../domain/markdown.js';
6
+ import { ensureVault, readMarkdownFileSummaries, writeMarkdownFile } from '../infrastructure/file-system-vault.js';
7
+ const canonicalPriority = 'high';
8
+ const slugify = (title) => title
9
+ .normalize('NFKD')
10
+ .replace(/[\u0300-\u036f]/g, '')
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9]+/g, '-')
13
+ .replace(/^-+|-+$/g, '');
14
+ export const hubTitleForContext = (contextTitle) => `${contextTitle} Hub`;
15
+ const hubPathForContext = (contextTitle, agentId) => {
16
+ if (contextTitle === 'GitHub Repositories')
17
+ return 'github-repos/github-repositories-hub.md';
18
+ if (contextTitle === 'GitHub Organizations')
19
+ return 'github-org-repos/github-organizations-hub.md';
20
+ if (contextTitle === 'Machine Configuration')
21
+ return 'machine-config/machine-configuration-hub.md';
22
+ return join('agents', sanitizeAgentId(agentId), `${slugify(hubTitleForContext(contextTitle))}.md`).replaceAll('\\', '/');
23
+ };
24
+ const normalizeTitle = (title) => title.trim().replace(/\.md$/i, '').toLowerCase();
25
+ const hasCanonicalLink = (content, hubTitle) => extractContextLinkWeights(content).some((link) => normalizeTitle(link.title) === normalizeTitle(hubTitle));
26
+ const linkLine = (hubTitle) => `- [[${hubTitle}]] priority: ${canonicalPriority}`;
27
+ const contextLinksHeading = (line) => line.match(/^(#{2,6})\s+(Context Links|Links de Contexto)\s*$/i);
28
+ export const upsertCanonicalContextLink = (content, hubTitle) => {
29
+ if (hasCanonicalLink(content, hubTitle)) {
30
+ return content;
31
+ }
32
+ const lines = content.replace(/\s+$/u, '').split('\n');
33
+ const headingIndex = lines.findIndex((line) => contextLinksHeading(line.trim()));
34
+ if (headingIndex === -1) {
35
+ return `${lines.join('\n')}\n\n## Context Links\n\n${linkLine(hubTitle)}\n`;
36
+ }
37
+ const heading = contextLinksHeading(lines[headingIndex].trim());
38
+ const headingDepth = heading?.[1]?.length ?? 2;
39
+ const insertIndex = lines.findIndex((line, index) => {
40
+ if (index <= headingIndex)
41
+ return false;
42
+ const candidate = line.match(/^(#{2,6})\s+/);
43
+ return Boolean(candidate && candidate[1].length <= headingDepth);
44
+ });
45
+ const targetIndex = insertIndex === -1 ? lines.length : insertIndex;
46
+ const before = lines.slice(0, targetIndex);
47
+ const after = lines.slice(targetIndex);
48
+ const needsSpacer = before[before.length - 1]?.trim() !== '';
49
+ const nextLines = [...before, ...(needsSpacer ? [''] : []), linkLine(hubTitle), ...after];
50
+ return `${nextLines.join('\n').replace(/\s+$/u, '')}\n`;
51
+ };
52
+ const buildHubContent = (hubTitle, contextTitle, agentId) => [
53
+ '---',
54
+ `title: "${hubTitle.replaceAll('"', '\\"')}"`,
55
+ `agent: "${sanitizeAgentId(agentId)}"`,
56
+ '---',
57
+ '',
58
+ `# ${hubTitle}`,
59
+ '',
60
+ `Canonical hub for the ${contextTitle} context. #memory #hub`,
61
+ ''
62
+ ].join('\n');
63
+ const readNotes = async (vaultPath) => {
64
+ const absoluteVaultPath = await ensureVault(vaultPath);
65
+ const summaries = await readMarkdownFileSummaries(absoluteVaultPath);
66
+ return Promise.all(summaries.map(async (summary) => {
67
+ const content = await readFile(summary.absolutePath, 'utf8');
68
+ const document = parseMarkdownDocument({
69
+ absolutePath: summary.absolutePath,
70
+ vaultPath: absoluteVaultPath,
71
+ content,
72
+ createdAt: summary.createdAt,
73
+ updatedAt: summary.updatedAt
74
+ });
75
+ return {
76
+ summary,
77
+ content,
78
+ document
79
+ };
80
+ }));
81
+ };
82
+ export const ensureCanonicalContextHub = async (vaultPath, contextTitle, agentId = sharedAgentId) => {
83
+ const hubTitle = hubTitleForContext(contextTitle);
84
+ const notes = await readNotes(vaultPath);
85
+ const existing = notes.find((note) => normalizeTitle(note.document.title) === normalizeTitle(hubTitle));
86
+ const hubPath = existing?.summary.relativePath ?? hubPathForContext(contextTitle, agentId);
87
+ if (existing) {
88
+ return {
89
+ created: false,
90
+ title: hubTitle,
91
+ path: hubPath
92
+ };
93
+ }
94
+ const path = await writeMarkdownFile(vaultPath, hubPath, buildHubContent(hubTitle, contextTitle, agentId));
95
+ return {
96
+ created: true,
97
+ title: hubTitle,
98
+ path
99
+ };
100
+ };
101
+ export const canonicalizeContextLinks = async (vaultPath, options = {}) => {
102
+ const agentId = options.agentId ? sanitizeAgentId(options.agentId) : undefined;
103
+ const createMissingHubs = options.createMissingHubs !== false;
104
+ const notes = await readNotes(vaultPath);
105
+ const scopedNotes = agentId ? notes.filter((note) => note.document.agentId === agentId) : notes;
106
+ const knownTitles = new Set(notes.map((note) => normalizeTitle(note.document.title)));
107
+ const entries = [];
108
+ const ensureHub = async (contextTitle, hubTitle, targetAgentId) => {
109
+ if (knownTitles.has(normalizeTitle(hubTitle))) {
110
+ return true;
111
+ }
112
+ const path = hubPathForContext(contextTitle, targetAgentId);
113
+ if (!createMissingHubs) {
114
+ entries.push({
115
+ path,
116
+ title: hubTitle,
117
+ context: contextTitle,
118
+ hubTitle,
119
+ changed: false,
120
+ reason: 'missing-hub'
121
+ });
122
+ return false;
123
+ }
124
+ knownTitles.add(normalizeTitle(hubTitle));
125
+ if (!options.dryRun) {
126
+ await writeMarkdownFile(vaultPath, path, buildHubContent(hubTitle, contextTitle, targetAgentId));
127
+ }
128
+ entries.push({
129
+ path,
130
+ title: hubTitle,
131
+ context: contextTitle,
132
+ hubTitle,
133
+ changed: true,
134
+ reason: 'created-hub'
135
+ });
136
+ return true;
137
+ };
138
+ for (const note of scopedNotes) {
139
+ const context = inferVisualGraphContext(note.document);
140
+ const hubTitle = hubTitleForContext(context.title);
141
+ const isHub = normalizeTitle(note.document.title) === normalizeTitle(hubTitle);
142
+ if (isHub) {
143
+ entries.push({
144
+ path: note.summary.relativePath,
145
+ title: note.document.title,
146
+ context: context.title,
147
+ hubTitle,
148
+ changed: false,
149
+ reason: 'hub-note'
150
+ });
151
+ continue;
152
+ }
153
+ const hubAvailable = await ensureHub(context.title, hubTitle, note.document.agentId || sharedAgentId);
154
+ if (!hubAvailable) {
155
+ continue;
156
+ }
157
+ if (hasCanonicalLink(note.content, hubTitle)) {
158
+ entries.push({
159
+ path: note.summary.relativePath,
160
+ title: note.document.title,
161
+ context: context.title,
162
+ hubTitle,
163
+ changed: false,
164
+ reason: 'already-linked'
165
+ });
166
+ continue;
167
+ }
168
+ const nextContent = upsertCanonicalContextLink(note.content, hubTitle);
169
+ if (!options.dryRun) {
170
+ await writeMarkdownFile(vaultPath, note.summary.relativePath, nextContent);
171
+ }
172
+ entries.push({
173
+ path: note.summary.relativePath,
174
+ title: note.document.title,
175
+ context: context.title,
176
+ hubTitle,
177
+ changed: true,
178
+ reason: 'added-context-link'
179
+ });
180
+ }
181
+ const changed = entries.filter((entry) => entry.changed).length;
182
+ const createdHubs = entries.filter((entry) => entry.reason === 'created-hub' && entry.changed).length;
183
+ return {
184
+ dryRun: options.dryRun === true,
185
+ scanned: scopedNotes.length,
186
+ changed,
187
+ createdHubs,
188
+ skipped: entries.length - changed,
189
+ entries
190
+ };
191
+ };
192
+ export const addCanonicalContextLinkToContent = (title, content) => {
193
+ const context = inferVisualGraphContext({
194
+ id: '',
195
+ agentId: sharedAgentId,
196
+ title,
197
+ path: '',
198
+ content,
199
+ tags: [],
200
+ });
201
+ const hubTitle = hubTitleForContext(context.title);
202
+ const nextContent = normalizeTitle(title) === normalizeTitle(hubTitle) ? content : upsertCanonicalContextLink(content, hubTitle);
203
+ return {
204
+ content: nextContent,
205
+ context: context.title,
206
+ hubTitle,
207
+ changed: nextContent !== content
208
+ };
209
+ };