@andespindola/brainlink 0.1.0-beta.13 → 0.1.0-beta.130

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 (55) hide show
  1. package/AGENTS.md +8 -5
  2. package/CHANGELOG.md +26 -2
  3. package/CONTRIBUTING.md +2 -2
  4. package/COPYRIGHT.md +5 -0
  5. package/README.md +143 -20
  6. package/SECURITY.md +1 -1
  7. package/dist/application/analyze-vault.js +1 -15
  8. package/dist/application/build-context.js +64 -3
  9. package/dist/application/dedupe-notes.js +226 -0
  10. package/dist/application/frontend/client-css.js +93 -45
  11. package/dist/application/frontend/client-html.js +34 -25
  12. package/dist/application/frontend/client-js.js +2724 -182
  13. package/dist/application/frontend/client-worker-js.js +66 -0
  14. package/dist/application/get-graph-layout.js +39 -6
  15. package/dist/application/get-graph-node.js +3 -3
  16. package/dist/application/get-graph-summary.js +3 -3
  17. package/dist/application/get-graph-view.js +243 -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 +253 -25
  21. package/dist/application/list-agents.js +3 -3
  22. package/dist/application/list-links.js +5 -5
  23. package/dist/application/offline-pack-backup.js +44 -0
  24. package/dist/application/search-graph-node-ids.js +3 -3
  25. package/dist/application/search-knowledge.js +6 -6
  26. package/dist/application/server/routes.js +105 -1
  27. package/dist/application/start-server.js +75 -4
  28. package/dist/application/watch-vault.js +23 -2
  29. package/dist/benchmarks/large-vault.js +1 -1
  30. package/dist/cli/commands/agent-commands.js +7 -0
  31. package/dist/cli/commands/write-commands.js +842 -8
  32. package/dist/domain/context.js +54 -11
  33. package/dist/domain/graph-layout.js +181 -3
  34. package/dist/domain/markdown.js +29 -9
  35. package/dist/domain/middle-out.js +18 -0
  36. package/dist/infrastructure/config.js +38 -0
  37. package/dist/infrastructure/file-index.js +358 -0
  38. package/dist/infrastructure/file-system-vault.js +15 -0
  39. package/dist/infrastructure/index-state.js +58 -0
  40. package/dist/infrastructure/private-pack-codec.js +71 -10
  41. package/dist/infrastructure/search-packs.js +313 -17
  42. package/dist/infrastructure/volatile-memory.js +100 -0
  43. package/dist/mcp/server.js +21 -1
  44. package/dist/mcp/tools.js +96 -0
  45. package/docs/AGENT_USAGE.md +101 -18
  46. package/docs/ARCHITECTURE.md +22 -27
  47. package/docs/QUICKSTART.md +7 -0
  48. package/package.json +6 -4
  49. package/dist/infrastructure/sqlite/document-writer.js +0 -51
  50. package/dist/infrastructure/sqlite/graph-reader.js +0 -267
  51. package/dist/infrastructure/sqlite/recovery.js +0 -163
  52. package/dist/infrastructure/sqlite/schema.js +0 -114
  53. package/dist/infrastructure/sqlite/search-reader.js +0 -188
  54. package/dist/infrastructure/sqlite/types.js +0 -1
  55. package/dist/infrastructure/sqlite-index.js +0 -38
@@ -0,0 +1,44 @@
1
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { gzipSync } from 'node:zlib';
4
+ import { ensureVault } from '../infrastructure/file-system-vault.js';
5
+ const packsDirectory = (vaultPath) => join(vaultPath, '.brainlink', 'search-packs');
6
+ const toSortedBackupFiles = async (vaultPath) => {
7
+ const directory = packsDirectory(vaultPath);
8
+ const names = await readdir(directory);
9
+ return names
10
+ .filter((name) => name.endsWith('.blpk') || name === 'manifest.json')
11
+ .sort((left, right) => left.localeCompare(right));
12
+ };
13
+ export const createOfflinePackBackup = async (input) => {
14
+ const vaultPath = await ensureVault(input.vaultPath);
15
+ const fileNames = await toSortedBackupFiles(vaultPath);
16
+ const files = [];
17
+ let inputBytes = 0;
18
+ for (const name of fileNames) {
19
+ const content = await readFile(join(packsDirectory(vaultPath), name));
20
+ inputBytes += content.byteLength;
21
+ files.push({
22
+ name,
23
+ contentB64: content.toString('base64')
24
+ });
25
+ }
26
+ const envelope = {
27
+ version: 1,
28
+ createdAt: new Date().toISOString(),
29
+ files
30
+ };
31
+ const serialized = Buffer.from(JSON.stringify(envelope), 'utf8');
32
+ const compressed = gzipSync(serialized, { level: 9 });
33
+ await mkdir(dirname(input.outputPath), { recursive: true });
34
+ await writeFile(input.outputPath, compressed);
35
+ const safeInput = Math.max(inputBytes, 1);
36
+ return {
37
+ outputPath: input.outputPath,
38
+ fileCount: files.length,
39
+ inputBytes,
40
+ outputBytes: compressed.byteLength,
41
+ ratio: compressed.byteLength / safeInput,
42
+ savedBytes: Math.max(inputBytes - compressed.byteLength, 0)
43
+ };
44
+ };
@@ -1,10 +1,10 @@
1
1
  import { ensureVault } from '../infrastructure/file-system-vault.js';
2
- import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
2
+ import { openFileIndex } from '../infrastructure/file-index.js';
3
3
  export const searchGraphNodeIds = async (vaultPath, query, limit, agentId) => {
4
4
  const absoluteVaultPath = await ensureVault(vaultPath);
5
- const index = openSqliteIndex(absoluteVaultPath);
5
+ const index = openFileIndex(absoluteVaultPath);
6
6
  try {
7
- return index.searchGraphNodeIds(query, limit, agentId);
7
+ return await index.searchGraphNodeIds(query, limit, agentId);
8
8
  }
9
9
  finally {
10
10
  index.close();
@@ -1,8 +1,7 @@
1
1
  import { stat } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
2
  import { ensureVault } from '../infrastructure/file-system-vault.js';
4
- import { searchInPacks } from '../infrastructure/search-packs.js';
5
- import { openSqliteIndex } from '../infrastructure/sqlite-index.js';
3
+ import { ensurePrivatePacksFromLegacyIndex, searchInPacks } from '../infrastructure/search-packs.js';
4
+ import { indexStoragePath, openFileIndex } from '../infrastructure/file-index.js';
6
5
  import { createEmbeddingProvider } from '../domain/embeddings.js';
7
6
  import { loadBrainlinkConfig, sanitizeSearchMode } from '../infrastructure/config.js';
8
7
  const hybridCacheTtlMs = 30_000;
@@ -10,7 +9,7 @@ const hybridCacheMaxEntries = 200;
10
9
  const hybridSearchCache = new Map();
11
10
  const readIndexMtimeMs = async (vaultPath) => {
12
11
  try {
13
- return (await stat(join(vaultPath, '.brainlink', 'brainlink.db'))).mtimeMs;
12
+ return (await stat(indexStoragePath(vaultPath))).mtimeMs;
14
13
  }
15
14
  catch {
16
15
  return 0;
@@ -47,6 +46,7 @@ export const searchKnowledge = async (vaultPath, query, limit, agentId, mode) =>
47
46
  const absoluteVaultPath = await ensureVault(vaultPath);
48
47
  const config = await loadBrainlinkConfig();
49
48
  const searchMode = sanitizeSearchMode(mode, config.defaultSearchMode);
49
+ await ensurePrivatePacksFromLegacyIndex(absoluteVaultPath);
50
50
  const cacheKey = searchMode === 'hybrid' ? toCacheKey(absoluteVaultPath, query, limit, agentId) : undefined;
51
51
  const indexMtimeMs = cacheKey ? await readIndexMtimeMs(absoluteVaultPath) : 0;
52
52
  const cached = cacheKey ? cacheGet(cacheKey, indexMtimeMs) : undefined;
@@ -57,9 +57,9 @@ export const searchKnowledge = async (vaultPath, query, limit, agentId, mode) =>
57
57
  const shouldEmbedQuery = searchMode !== 'fts' && provider.name !== 'none';
58
58
  const queryEmbedding = shouldEmbedQuery ? (await provider.embed([query]))[0] ?? [] : [];
59
59
  try {
60
- const index = openSqliteIndex(absoluteVaultPath);
60
+ const index = openFileIndex(absoluteVaultPath);
61
61
  try {
62
- const results = index.search(query, limit, agentId, searchMode, queryEmbedding);
62
+ const results = await index.search(query, limit, agentId, searchMode, queryEmbedding);
63
63
  if (cacheKey) {
64
64
  cacheSet({
65
65
  key: cacheKey,
@@ -3,6 +3,7 @@ import { buildContextPackage } from '../build-context.js';
3
3
  import { getGraph } from '../get-graph.js';
4
4
  import { getGraphNode } from '../get-graph-node.js';
5
5
  import { getGraphLayout } from '../get-graph-layout.js';
6
+ import { getGraphView } from '../get-graph-view.js';
6
7
  import { listAgents } from '../list-agents.js';
7
8
  import { listBacklinks, listLinks } from '../list-links.js';
8
9
  import { searchGraphNodeIds } from '../search-graph-node-ids.js';
@@ -11,6 +12,7 @@ import { loadBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/co
11
12
  import { createClientCss } from '../frontend/client-css.js';
12
13
  import { createClientHtml } from '../frontend/client-html.js';
13
14
  import { createClientJs } from '../frontend/client-js.js';
15
+ import { createClientWorkerJs } from '../frontend/client-worker-js.js';
14
16
  import { contentTypes, createJsonResponse, isReadMethod, parsePositiveInteger } from './http.js';
15
17
  const readSearchMode = async (url) => {
16
18
  const config = await loadBrainlinkConfig();
@@ -51,10 +53,99 @@ const sameEntityTag = (candidate, signature) => {
51
53
  return decodeEntityTag(candidate) === signature;
52
54
  };
53
55
  const readAgentQuery = (url) => url.searchParams.get('agent') ?? undefined;
56
+ const parseNumber = (value, fallback) => {
57
+ const parsed = Number(value);
58
+ return Number.isFinite(parsed) ? parsed : fallback;
59
+ };
60
+ const compactGraphLayoutThreshold = 12_000;
61
+ const compactGraphLayoutEdgeLimit = 60_000;
62
+ const compactGraphLayoutEdgeLimitFor = (nodeCount) => {
63
+ if (nodeCount > 100_000)
64
+ return 15_000;
65
+ if (nodeCount > 50_000)
66
+ return 22_000;
67
+ if (nodeCount > 25_000)
68
+ return 30_000;
69
+ return compactGraphLayoutEdgeLimit;
70
+ };
71
+ const edgeWeight = (weight) => Number.isFinite(weight) ? Number(weight) : 1;
72
+ const edgeKey = (source, target, priority) => `${source}|${target}|${priority}`;
73
+ const selectCompactEdges = (layout, limit) => {
74
+ const resolvedEdges = layout.edges.filter((edge) => typeof edge.target === 'string' && edge.target.length > 0);
75
+ if (resolvedEdges.length <= limit) {
76
+ return resolvedEdges;
77
+ }
78
+ const bestEdgeByEndpoint = new Map();
79
+ for (let index = 0; index < resolvedEdges.length; index += 1) {
80
+ const edge = resolvedEdges[index];
81
+ const endpoints = [edge.source, edge.target];
82
+ for (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex += 1) {
83
+ const endpoint = endpoints[endpointIndex];
84
+ const previous = bestEdgeByEndpoint.get(endpoint);
85
+ if (!previous || edgeWeight(edge.weight) > edgeWeight(previous.weight)) {
86
+ bestEdgeByEndpoint.set(endpoint, edge);
87
+ }
88
+ }
89
+ }
90
+ const selected = new Map();
91
+ for (const edge of bestEdgeByEndpoint.values()) {
92
+ selected.set(edgeKey(edge.source, edge.target, edge.priority), edge);
93
+ }
94
+ if (selected.size > limit) {
95
+ return Array.from(selected.values())
96
+ .sort((left, right) => edgeWeight(right.weight) - edgeWeight(left.weight))
97
+ .slice(0, limit);
98
+ }
99
+ const byWeight = [...resolvedEdges].sort((left, right) => edgeWeight(right.weight) - edgeWeight(left.weight));
100
+ for (let index = 0; index < byWeight.length; index += 1) {
101
+ if (selected.size >= limit) {
102
+ break;
103
+ }
104
+ const edge = byWeight[index];
105
+ const key = edgeKey(edge.source, edge.target, edge.priority);
106
+ if (!selected.has(key)) {
107
+ selected.set(key, edge);
108
+ }
109
+ }
110
+ return Array.from(selected.values());
111
+ };
54
112
  const stripLayoutContent = (layout) => ({
55
113
  ...layout,
56
114
  nodes: layout.nodes.map(({ content, ...node }) => node)
57
115
  });
116
+ const compactLayoutPayload = (layout) => {
117
+ const edgeLimit = compactGraphLayoutEdgeLimitFor(layout.nodes.length);
118
+ const compactEdges = selectCompactEdges(layout, edgeLimit);
119
+ const compactNodes = layout.nodes.map((node) => [node.id, node.title, node.x, node.y, node.group, node.segment]);
120
+ const compactEdgeRows = compactEdges
121
+ .map((edge) => [edge.source, edge.target, edge.weight, edge.priority]);
122
+ const compactGroups = layout.groups?.map((group) => [
123
+ group.id,
124
+ group.level,
125
+ group.parentId,
126
+ group.title,
127
+ group.x,
128
+ group.y,
129
+ group.radius,
130
+ group.segment,
131
+ group.group,
132
+ group.nodeIds,
133
+ group.childGroupIds
134
+ ]);
135
+ return {
136
+ compact: true,
137
+ layout: {
138
+ nodes: compactNodes,
139
+ edges: compactEdgeRows,
140
+ ...(compactGroups && compactGroups.length > 0 ? { groups: compactGroups } : {})
141
+ },
142
+ totals: {
143
+ nodes: layout.nodes.length,
144
+ edges: layout.edges.length
145
+ }
146
+ };
147
+ };
148
+ const encodeLayoutPayload = (layout) => layout.nodes.length > compactGraphLayoutThreshold ? compactLayoutPayload(layout) : { compact: false, layout: stripLayoutContent(layout) };
58
149
  export const route = async (request, url, vaultPath) => {
59
150
  if (isReadMethod(request) && (url.pathname === '/' || url.pathname === '/index.html')) {
60
151
  return createResponse(createClientHtml(), 200, contentTypes['.html']);
@@ -65,6 +156,9 @@ export const route = async (request, url, vaultPath) => {
65
156
  if (isReadMethod(request) && url.pathname === '/app.js') {
66
157
  return createResponse(createClientJs(), 200, contentTypes['.js']);
67
158
  }
159
+ if (isReadMethod(request) && url.pathname === '/app-worker.js') {
160
+ return createResponse(createClientWorkerJs(), 200, contentTypes['.js']);
161
+ }
68
162
  if (isReadMethod(request) && url.pathname === '/api/graph') {
69
163
  return createResponse(createJsonResponse(await getGraph(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
70
164
  }
@@ -73,7 +167,7 @@ export const route = async (request, url, vaultPath) => {
73
167
  const requestEtags = request.headers['if-none-match'];
74
168
  const notModified = sameEntityTag(requestEtags, signature);
75
169
  const etag = encodeEntityTag(signature);
76
- const body = createJsonResponse({ signature, layout: stripLayoutContent(layout) });
170
+ const body = createJsonResponse({ signature, ...encodeLayoutPayload(layout) });
77
171
  const jsonResponse = createResponse(body, 200, contentTypes['.json']);
78
172
  const notModifiedResponse = createResponse('', 304, contentTypes['.json']);
79
173
  if (notModified) {
@@ -93,6 +187,16 @@ export const route = async (request, url, vaultPath) => {
93
187
  }
94
188
  };
95
189
  }
190
+ if (isReadMethod(request) && url.pathname === '/api/graph-view') {
191
+ return createResponse(createJsonResponse(await getGraphView(vaultPath, {
192
+ x: parseNumber(url.searchParams.get('x'), -1000),
193
+ y: parseNumber(url.searchParams.get('y'), -1000),
194
+ width: parseNumber(url.searchParams.get('w'), 2000),
195
+ height: parseNumber(url.searchParams.get('h'), 2000),
196
+ scale: parseNumber(url.searchParams.get('scale'), 1),
197
+ agentId: readAgentQuery(url)
198
+ })), 200, contentTypes['.json']);
199
+ }
96
200
  if (isReadMethod(request) && url.pathname === '/api/graph-node') {
97
201
  const id = url.searchParams.get('id')?.trim() ?? '';
98
202
  if (!id) {
@@ -1,9 +1,78 @@
1
1
  import { createServer } from 'node:http';
2
+ import { brotliCompressSync, constants, gzipSync } from 'node:zlib';
2
3
  import { indexVault } from './index-vault.js';
3
4
  import { startVaultWatcher } from './watch-vault.js';
4
5
  import { assertLoopbackHost } from './server/host-security.js';
5
6
  import { contentTypes, createJsonResponse, isHttpError } from './server/http.js';
6
7
  import { route } from './server/routes.js';
8
+ const compressionThresholdBytes = 1024;
9
+ const normalizeEncodingToken = (value) => value.trim().toLowerCase();
10
+ const supportsEncoding = (acceptEncoding, target) => {
11
+ if (!acceptEncoding) {
12
+ return false;
13
+ }
14
+ return acceptEncoding
15
+ .split(',')
16
+ .map((entry) => entry.split(';')[0] ?? '')
17
+ .map(normalizeEncodingToken)
18
+ .includes(target);
19
+ };
20
+ const isCompressibleContentType = (contentType) => {
21
+ const normalized = contentType?.toLowerCase() ?? '';
22
+ return (normalized.includes('application/json') ||
23
+ normalized.includes('text/javascript') ||
24
+ normalized.includes('text/css') ||
25
+ normalized.includes('text/html') ||
26
+ normalized.startsWith('text/'));
27
+ };
28
+ const maybeCompressResponse = (requestHeaders, statusCode, headers, body) => {
29
+ if (statusCode === 204 || statusCode === 304) {
30
+ return { headers, body: '' };
31
+ }
32
+ if (!isCompressibleContentType(headers['content-type'])) {
33
+ return { headers, body };
34
+ }
35
+ const bodyBuffer = Buffer.from(body, 'utf8');
36
+ if (bodyBuffer.byteLength < compressionThresholdBytes) {
37
+ return { headers, body };
38
+ }
39
+ if (headers['content-encoding']) {
40
+ return { headers, body };
41
+ }
42
+ const acceptEncodingHeader = Array.isArray(requestHeaders['accept-encoding'])
43
+ ? requestHeaders['accept-encoding'].join(',')
44
+ : requestHeaders['accept-encoding'];
45
+ const vary = headers.vary ? `${headers.vary}, Accept-Encoding` : 'Accept-Encoding';
46
+ const withVary = {
47
+ ...headers,
48
+ vary
49
+ };
50
+ if (supportsEncoding(acceptEncodingHeader, 'br')) {
51
+ return {
52
+ headers: {
53
+ ...withVary,
54
+ 'content-encoding': 'br'
55
+ },
56
+ body: brotliCompressSync(bodyBuffer, {
57
+ params: {
58
+ [constants.BROTLI_PARAM_QUALITY]: 5
59
+ }
60
+ })
61
+ };
62
+ }
63
+ if (supportsEncoding(acceptEncodingHeader, 'gzip')) {
64
+ return {
65
+ headers: {
66
+ ...withVary,
67
+ 'content-encoding': 'gzip'
68
+ },
69
+ body: gzipSync(bodyBuffer, {
70
+ level: 6
71
+ })
72
+ };
73
+ }
74
+ return { headers: withVary, body };
75
+ };
7
76
  export const startServer = async (input) => {
8
77
  assertLoopbackHost(input.host);
9
78
  if (input.shouldIndex) {
@@ -19,14 +88,16 @@ export const startServer = async (input) => {
19
88
  const url = new URL(request.url ?? '/', `http://${request.headers.host ?? input.host}`);
20
89
  route(request, url, input.vaultPath)
21
90
  .then((result) => {
22
- response.writeHead(result.statusCode, result.headers);
23
- response.end(result.body);
91
+ const encoded = maybeCompressResponse(request.headers, result.statusCode, result.headers, result.body);
92
+ response.writeHead(result.statusCode, encoded.headers);
93
+ response.end(encoded.body);
24
94
  })
25
95
  .catch((error) => {
26
96
  const message = error instanceof Error ? error.message : String(error);
27
97
  const statusCode = isHttpError(error) ? error.statusCode : 500;
28
- response.writeHead(statusCode, { 'content-type': contentTypes['.json'] });
29
- response.end(createJsonResponse({ error: message }));
98
+ const fallback = maybeCompressResponse(request.headers, statusCode, { 'content-type': contentTypes['.json'] }, createJsonResponse({ error: message }));
99
+ response.writeHead(statusCode, fallback.headers);
100
+ response.end(fallback.body);
30
101
  });
31
102
  });
32
103
  await new Promise((resolve, reject) => {
@@ -1,5 +1,5 @@
1
1
  import { watch } from 'node:fs';
2
- import { indexVault } from './index-vault.js';
2
+ import { indexVaultWithOptions } from './index-vault.js';
3
3
  import { isBucketVaultPath, resolveVaultPath } from '../infrastructure/file-system-vault.js';
4
4
  const shouldIgnore = (filename) => {
5
5
  if (!filename) {
@@ -14,6 +14,27 @@ export const startVaultWatcher = (input) => {
14
14
  const absoluteVaultPath = resolveVaultPath(input.vaultPath);
15
15
  const debounceMs = input.debounceMs ?? 350;
16
16
  let timeout = null;
17
+ let running = false;
18
+ let pending = false;
19
+ const runIndex = () => {
20
+ if (running) {
21
+ pending = true;
22
+ return;
23
+ }
24
+ running = true;
25
+ indexVaultWithOptions(absoluteVaultPath, {
26
+ onProgress: input.onProgress
27
+ })
28
+ .then(input.onIndex)
29
+ .catch(input.onError)
30
+ .finally(() => {
31
+ running = false;
32
+ if (pending) {
33
+ pending = false;
34
+ runIndex();
35
+ }
36
+ });
37
+ };
17
38
  const schedule = (filename) => {
18
39
  if (shouldIgnore(filename)) {
19
40
  return;
@@ -22,7 +43,7 @@ export const startVaultWatcher = (input) => {
22
43
  clearTimeout(timeout);
23
44
  }
24
45
  timeout = setTimeout(() => {
25
- indexVault(absoluteVaultPath).then(input.onIndex).catch(input.onError);
46
+ runIndex();
26
47
  }, debounceMs);
27
48
  };
28
49
  const watcher = watch(absoluteVaultPath, { recursive: true }, (_eventType, filename) => {
@@ -21,7 +21,7 @@ const readOptions = (args) => ({
21
21
  });
22
22
  const topics = [
23
23
  'authentication jwt token refresh policy',
24
- 'sqlite graph backlinks markdown vault indexing',
24
+ 'graph backlinks markdown vault indexing',
25
25
  'frontend canvas layout graph interaction',
26
26
  'agent memory context retrieval summarization',
27
27
  'security local server vault path allowlist',
@@ -141,6 +141,12 @@ const parseAllowedVaults = (value) => {
141
141
  export const installAgentIntegration = async (input) => {
142
142
  const codexConfigPath = getCodexConfigPath();
143
143
  const allowedVaults = parseAllowedVaults(input.allowedVaults);
144
+ const bootstrapPolicy = await setBootstrapPolicy({
145
+ enforceBootstrap: true,
146
+ enforceContextFirst: true,
147
+ autoBootstrapOnRead: true,
148
+ autoBootstrapOnStartup: true
149
+ });
144
150
  await upsertCodexMcpConfig(codexConfigPath, {
145
151
  allowedVaults,
146
152
  brainlinkHome: input.brainlinkHome
@@ -195,6 +201,7 @@ export const installAgentIntegration = async (input) => {
195
201
  codexConfigPath,
196
202
  mcpServer: 'brainlink',
197
203
  command: 'brainlink-mcp',
204
+ bootstrapPolicy,
198
205
  ...(input.mcpOnly !== true ? { pluginSourcePath, pluginSymlinkPath, marketplacePath } : {}),
199
206
  ...(selfTestResult ? { selfTest: selfTestResult } : {}),
200
207
  ...(warnings.length > 0 ? { warnings } : {})