@andespindola/brainlink 0.1.0-beta.8 → 0.1.0-beta.81
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.
- package/AGENTS.md +8 -5
- package/CHANGELOG.md +58 -2
- package/CONTRIBUTING.md +2 -2
- package/COPYRIGHT.md +5 -0
- package/README.md +266 -20
- package/SECURITY.md +1 -1
- package/dist/application/add-note.js +62 -13
- package/dist/application/analyze-vault.js +95 -8
- package/dist/application/build-context.js +56 -1
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +138 -103
- package/dist/application/frontend/client-html.js +47 -41
- package/dist/application/frontend/client-js.js +2449 -156
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-layout.js +18 -6
- package/dist/application/get-graph-node.js +12 -0
- package/dist/application/get-graph-summary.js +12 -0
- package/dist/application/get-graph.js +3 -3
- package/dist/application/import-legacy-sqlite.js +296 -0
- package/dist/application/index-vault.js +252 -19
- package/dist/application/list-agents.js +3 -3
- package/dist/application/list-links.js +5 -5
- package/dist/application/migrate-vault.js +46 -16
- package/dist/application/offline-pack-backup.js +44 -0
- package/dist/application/search-graph-node-ids.js +12 -0
- package/dist/application/search-knowledge.js +75 -5
- package/dist/application/server/routes.js +102 -1
- package/dist/application/start-server.js +75 -4
- package/dist/application/watch-vault.js +23 -2
- package/dist/benchmarks/large-vault.js +1 -1
- package/dist/cli/commands/agent-commands.js +419 -0
- package/dist/cli/commands/config-commands.js +167 -0
- package/dist/cli/commands/read-commands.js +25 -8
- package/dist/cli/commands/write-commands.js +973 -10
- package/dist/cli/main.js +4 -0
- package/dist/cli/runtime.js +5 -2
- package/dist/domain/context.js +53 -11
- package/dist/domain/embeddings.js +2 -1
- package/dist/domain/graph-layout.js +67 -16
- package/dist/domain/markdown.js +36 -4
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +132 -8
- package/dist/infrastructure/file-index.js +358 -0
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/index-state.js +56 -0
- package/dist/infrastructure/paths.js +9 -1
- package/dist/infrastructure/private-pack-codec.js +134 -0
- package/dist/infrastructure/search-packs.js +452 -0
- package/dist/infrastructure/session-state.js +172 -0
- package/dist/mcp/main.js +11 -3
- package/dist/mcp/server.js +27 -2
- package/dist/mcp/startup.js +35 -0
- package/dist/mcp/tools.js +633 -19
- package/docs/AGENT_USAGE.md +177 -15
- package/docs/ARCHITECTURE.md +37 -26
- package/docs/QUICKSTART.md +111 -0
- package/package.json +6 -4
- package/dist/infrastructure/sqlite/document-writer.js +0 -51
- package/dist/infrastructure/sqlite/graph-reader.js +0 -120
- package/dist/infrastructure/sqlite/schema.js +0 -111
- package/dist/infrastructure/sqlite/search-reader.js +0 -156
- package/dist/infrastructure/sqlite/types.js +0 -1
- package/dist/infrastructure/sqlite-index.js +0 -25
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { access, lstat, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { loadBrainlinkConfig } from '../../infrastructure/config.js';
|
|
7
|
+
import { getBootstrapPolicy, getBootstrapSessionStatus, getContextSessionStatus, getSessionStatePath, setBootstrapPolicy } from '../../infrastructure/session-state.js';
|
|
8
|
+
import { print } from '../runtime.js';
|
|
9
|
+
const getCodexConfigPath = () => join(homedir(), '.codex', 'config.toml');
|
|
10
|
+
const getMarketplacePath = () => join(homedir(), '.agents', 'plugins', 'marketplace.json');
|
|
11
|
+
const getDefaultPluginSourcePath = () => resolve(process.cwd(), 'plugins', 'brainlink');
|
|
12
|
+
const getPluginSymlinkPath = () => join(homedir(), 'plugins', 'brainlink');
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
const toTomlValue = (value) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
15
|
+
const removeBrainlinkMcpSection = (content) => {
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
const output = [];
|
|
18
|
+
let skip = false;
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
const sectionMatch = line.match(/^\[(.+)\]\s*$/);
|
|
21
|
+
if (!skip && sectionMatch?.[1] === 'mcp_servers.brainlink') {
|
|
22
|
+
skip = true;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (skip && sectionMatch) {
|
|
26
|
+
skip = false;
|
|
27
|
+
}
|
|
28
|
+
if (!skip) {
|
|
29
|
+
output.push(line);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return output.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
33
|
+
};
|
|
34
|
+
const buildBrainlinkMcpSection = (options) => {
|
|
35
|
+
const envEntries = [];
|
|
36
|
+
if (options.allowedVaults) {
|
|
37
|
+
envEntries.push(`BRAINLINK_ALLOWED_VAULTS = ${toTomlValue(options.allowedVaults)}`);
|
|
38
|
+
}
|
|
39
|
+
if (options.brainlinkHome) {
|
|
40
|
+
envEntries.push(`BRAINLINK_HOME = ${toTomlValue(options.brainlinkHome)}`);
|
|
41
|
+
}
|
|
42
|
+
return [
|
|
43
|
+
'[mcp_servers.brainlink]',
|
|
44
|
+
`command = ${toTomlValue('brainlink-mcp')}`,
|
|
45
|
+
...(envEntries.length > 0 ? [`env = { ${envEntries.join(', ')} }`] : [])
|
|
46
|
+
].join('\n');
|
|
47
|
+
};
|
|
48
|
+
const upsertCodexMcpConfig = async (configPath, options) => {
|
|
49
|
+
let existing = '';
|
|
50
|
+
try {
|
|
51
|
+
existing = await readFile(configPath, 'utf8');
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const withoutSection = removeBrainlinkMcpSection(existing);
|
|
59
|
+
const section = buildBrainlinkMcpSection(options);
|
|
60
|
+
const merged = `${withoutSection}\n\n${section}\n`.replace(/^\n+/, '');
|
|
61
|
+
await mkdir(dirname(configPath), { recursive: true, mode: 0o700 });
|
|
62
|
+
await writeFile(configPath, merged, { encoding: 'utf8', mode: 0o600 });
|
|
63
|
+
};
|
|
64
|
+
const ensurePluginSymlink = async (sourcePath, symlinkPath) => {
|
|
65
|
+
await access(sourcePath);
|
|
66
|
+
await mkdir(dirname(symlinkPath), { recursive: true, mode: 0o700 });
|
|
67
|
+
try {
|
|
68
|
+
const info = await lstat(symlinkPath);
|
|
69
|
+
if (info.isSymbolicLink()) {
|
|
70
|
+
await rm(symlinkPath, { force: true });
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
await rm(symlinkPath, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
await symlink(sourcePath, symlinkPath);
|
|
82
|
+
};
|
|
83
|
+
const loadMarketplace = async (path) => {
|
|
84
|
+
try {
|
|
85
|
+
const content = await readFile(path, 'utf8');
|
|
86
|
+
const parsed = JSON.parse(content);
|
|
87
|
+
const plugins = Array.isArray(parsed.plugins) ? parsed.plugins : [];
|
|
88
|
+
return {
|
|
89
|
+
name: typeof parsed.name === 'string' ? parsed.name : 'local',
|
|
90
|
+
interface: {
|
|
91
|
+
displayName: typeof parsed.interface?.displayName === 'string' ? parsed.interface.displayName : 'Local'
|
|
92
|
+
},
|
|
93
|
+
plugins: plugins
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
name: 'local',
|
|
102
|
+
interface: {
|
|
103
|
+
displayName: 'Local'
|
|
104
|
+
},
|
|
105
|
+
plugins: []
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const upsertMarketplacePlugin = async (marketplacePath) => {
|
|
110
|
+
const marketplace = await loadMarketplace(marketplacePath);
|
|
111
|
+
const pluginEntry = {
|
|
112
|
+
name: 'brainlink',
|
|
113
|
+
source: {
|
|
114
|
+
source: 'local',
|
|
115
|
+
path: './plugins/brainlink'
|
|
116
|
+
},
|
|
117
|
+
policy: {
|
|
118
|
+
installation: 'AVAILABLE',
|
|
119
|
+
authentication: 'ON_INSTALL'
|
|
120
|
+
},
|
|
121
|
+
category: 'Productivity'
|
|
122
|
+
};
|
|
123
|
+
const plugins = marketplace.plugins.filter((plugin) => plugin?.name !== 'brainlink');
|
|
124
|
+
const next = {
|
|
125
|
+
...marketplace,
|
|
126
|
+
plugins: [...plugins, pluginEntry]
|
|
127
|
+
};
|
|
128
|
+
await mkdir(dirname(marketplacePath), { recursive: true, mode: 0o700 });
|
|
129
|
+
await writeFile(marketplacePath, `${JSON.stringify(next, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
130
|
+
};
|
|
131
|
+
const parseAllowedVaults = (value) => {
|
|
132
|
+
if (!value) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const normalized = value
|
|
136
|
+
.split(',')
|
|
137
|
+
.map((item) => item.trim())
|
|
138
|
+
.filter(Boolean);
|
|
139
|
+
return normalized.length > 0 ? normalized.join(',') : undefined;
|
|
140
|
+
};
|
|
141
|
+
export const installAgentIntegration = async (input) => {
|
|
142
|
+
const codexConfigPath = getCodexConfigPath();
|
|
143
|
+
const allowedVaults = parseAllowedVaults(input.allowedVaults);
|
|
144
|
+
const bootstrapPolicy = await setBootstrapPolicy({
|
|
145
|
+
enforceBootstrap: true,
|
|
146
|
+
enforceContextFirst: true,
|
|
147
|
+
autoBootstrapOnRead: true,
|
|
148
|
+
autoBootstrapOnStartup: true
|
|
149
|
+
});
|
|
150
|
+
await upsertCodexMcpConfig(codexConfigPath, {
|
|
151
|
+
allowedVaults,
|
|
152
|
+
brainlinkHome: input.brainlinkHome
|
|
153
|
+
});
|
|
154
|
+
const warnings = [];
|
|
155
|
+
const pluginSourcePath = input.pluginPath ? resolve(input.pluginPath) : getDefaultPluginSourcePath();
|
|
156
|
+
const pluginSymlinkPath = getPluginSymlinkPath();
|
|
157
|
+
const marketplacePath = getMarketplacePath();
|
|
158
|
+
if (input.mcpOnly !== true) {
|
|
159
|
+
try {
|
|
160
|
+
await ensurePluginSymlink(pluginSourcePath, pluginSymlinkPath);
|
|
161
|
+
await upsertMarketplacePlugin(marketplacePath);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
165
|
+
warnings.push(`Plugin marketplace setup skipped: ${message}. MCP is configured, but install repository plugin files to enable local gallery auto-discovery.`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const selfTestResult = input.selfTest
|
|
169
|
+
? await (async () => {
|
|
170
|
+
const codexConfig = await readFile(codexConfigPath, 'utf8');
|
|
171
|
+
const mcp = isBrainlinkConfigured(codexConfig);
|
|
172
|
+
const mcpCommandInPath = await hasMcpCommandInPath();
|
|
173
|
+
const pluginSymlinkExists = input.mcpOnly === true
|
|
174
|
+
? null
|
|
175
|
+
: await (async () => {
|
|
176
|
+
try {
|
|
177
|
+
return (await lstat(pluginSymlinkPath)).isSymbolicLink();
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
})();
|
|
183
|
+
const marketplaceEntryExists = input.mcpOnly === true
|
|
184
|
+
? null
|
|
185
|
+
: (await loadMarketplace(marketplacePath)).plugins.some((plugin) => plugin?.name === 'brainlink');
|
|
186
|
+
return {
|
|
187
|
+
ok: mcp.hasMcpSection &&
|
|
188
|
+
mcp.hasCommand &&
|
|
189
|
+
mcpCommandInPath &&
|
|
190
|
+
(input.mcpOnly === true || (Boolean(pluginSymlinkExists) && Boolean(marketplaceEntryExists))),
|
|
191
|
+
mcpCommandInPath,
|
|
192
|
+
hasMcpSection: mcp.hasMcpSection,
|
|
193
|
+
hasCommand: mcp.hasCommand,
|
|
194
|
+
pluginSymlinkExists,
|
|
195
|
+
marketplaceEntryExists
|
|
196
|
+
};
|
|
197
|
+
})()
|
|
198
|
+
: undefined;
|
|
199
|
+
return {
|
|
200
|
+
installed: true,
|
|
201
|
+
codexConfigPath,
|
|
202
|
+
mcpServer: 'brainlink',
|
|
203
|
+
command: 'brainlink-mcp',
|
|
204
|
+
bootstrapPolicy,
|
|
205
|
+
...(input.mcpOnly !== true ? { pluginSourcePath, pluginSymlinkPath, marketplacePath } : {}),
|
|
206
|
+
...(selfTestResult ? { selfTest: selfTestResult } : {}),
|
|
207
|
+
...(warnings.length > 0 ? { warnings } : {})
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
const hasMcpCommandInPath = async () => {
|
|
211
|
+
try {
|
|
212
|
+
const { stdout } = await execFileAsync('sh', ['-lc', 'command -v brainlink-mcp'], { maxBuffer: 1024 * 1024 });
|
|
213
|
+
return stdout.trim().length > 0;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const isBrainlinkConfigured = (codexConfig) => {
|
|
220
|
+
const hasMcpSection = codexConfig.includes('[mcp_servers.brainlink]');
|
|
221
|
+
const hasCommand = /(^|\n)\s*command\s*=\s*"brainlink-mcp"\s*(\n|$)/m.test(codexConfig);
|
|
222
|
+
return { hasMcpSection, hasCommand };
|
|
223
|
+
};
|
|
224
|
+
const parseBooleanOption = (value, name) => {
|
|
225
|
+
if (value === undefined) {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
if (value === 'true') {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
if (value === 'false') {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
throw new Error(`Invalid value for ${name}: ${value}. Use true or false.`);
|
|
235
|
+
};
|
|
236
|
+
const parsePositiveIntegerOption = (value, name) => {
|
|
237
|
+
if (value === undefined) {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
const parsed = Number.parseInt(value, 10);
|
|
241
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
242
|
+
throw new Error(`Invalid value for ${name}: ${value}. Use a positive integer.`);
|
|
243
|
+
}
|
|
244
|
+
return parsed;
|
|
245
|
+
};
|
|
246
|
+
const applyPolicyPreset = (preset) => {
|
|
247
|
+
if (!preset) {
|
|
248
|
+
return {};
|
|
249
|
+
}
|
|
250
|
+
if (preset === 'fully-auto') {
|
|
251
|
+
return {
|
|
252
|
+
enforceBootstrap: true,
|
|
253
|
+
enforceContextFirst: true,
|
|
254
|
+
autoBootstrapOnRead: true,
|
|
255
|
+
autoBootstrapOnStartup: true
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (preset === 'strict') {
|
|
259
|
+
return {
|
|
260
|
+
enforceBootstrap: true,
|
|
261
|
+
enforceContextFirst: true,
|
|
262
|
+
autoBootstrapOnRead: false,
|
|
263
|
+
autoBootstrapOnStartup: false
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
throw new Error(`Unknown policy preset: ${preset}. Use "fully-auto" or "strict".`);
|
|
267
|
+
};
|
|
268
|
+
export const registerAgentCommands = (program) => {
|
|
269
|
+
const agent = program.command('agent').description('install or inspect Brainlink agent integration');
|
|
270
|
+
agent
|
|
271
|
+
.command('install')
|
|
272
|
+
.option('--mcp-only', 'only configure MCP server in Codex config')
|
|
273
|
+
.option('--plugin-path <path>', 'custom source path for Brainlink plugin files')
|
|
274
|
+
.option('--allowed-vaults <paths>', 'comma separated vault allowlist to inject in MCP env')
|
|
275
|
+
.option('--brainlink-home <path>', 'BRAINLINK_HOME value to inject in MCP env')
|
|
276
|
+
.option('--self-test', 'run post-install checks and include diagnostics in the result')
|
|
277
|
+
.option('--json', 'print machine-readable JSON')
|
|
278
|
+
.description('install Brainlink as default MCP memory integration for the local agent')
|
|
279
|
+
.action(async (options) => {
|
|
280
|
+
const result = await installAgentIntegration({
|
|
281
|
+
mcpOnly: options.mcpOnly,
|
|
282
|
+
pluginPath: options.pluginPath,
|
|
283
|
+
allowedVaults: options.allowedVaults,
|
|
284
|
+
brainlinkHome: options.brainlinkHome,
|
|
285
|
+
selfTest: options.selfTest
|
|
286
|
+
});
|
|
287
|
+
print(options.json, result, () => [
|
|
288
|
+
`Installed Brainlink MCP at ${result.codexConfigPath}`,
|
|
289
|
+
...(options.mcpOnly === true ? [] : [`Plugin symlink: ${result.pluginSymlinkPath}`, `Marketplace: ${result.marketplacePath}`]),
|
|
290
|
+
...(result.selfTest ? [`Self-test: ${result.selfTest.ok ? 'ok' : 'failed'}`] : []),
|
|
291
|
+
...(result.warnings && result.warnings.length > 0 ? ['Warnings:', ...result.warnings.map((warning) => `- ${warning}`)] : [])
|
|
292
|
+
].join('\n'));
|
|
293
|
+
});
|
|
294
|
+
agent
|
|
295
|
+
.command('upgrade')
|
|
296
|
+
.option('--mcp-only', 'only configure MCP server in Codex config')
|
|
297
|
+
.option('--plugin-path <path>', 'custom source path for Brainlink plugin files')
|
|
298
|
+
.option('--allowed-vaults <paths>', 'comma separated vault allowlist to inject in MCP env')
|
|
299
|
+
.option('--brainlink-home <path>', 'BRAINLINK_HOME value to inject in MCP env')
|
|
300
|
+
.option('--json', 'print machine-readable JSON')
|
|
301
|
+
.description('reapply latest Brainlink agent integration defaults for legacy installs')
|
|
302
|
+
.action(async (options) => {
|
|
303
|
+
const result = await installAgentIntegration({
|
|
304
|
+
mcpOnly: options.mcpOnly,
|
|
305
|
+
pluginPath: options.pluginPath,
|
|
306
|
+
allowedVaults: options.allowedVaults,
|
|
307
|
+
brainlinkHome: options.brainlinkHome,
|
|
308
|
+
selfTest: true
|
|
309
|
+
});
|
|
310
|
+
print(options.json, {
|
|
311
|
+
upgraded: true,
|
|
312
|
+
...result
|
|
313
|
+
}, () => `Upgraded Brainlink agent integration at ${result.codexConfigPath}. Self-test: ${result.selfTest?.ok ? 'ok' : 'failed'}`);
|
|
314
|
+
});
|
|
315
|
+
agent
|
|
316
|
+
.command('policy')
|
|
317
|
+
.option('--preset <preset>', 'policy preset: fully-auto or strict')
|
|
318
|
+
.option('--enforce-bootstrap <true|false>', 'override enforceBootstrap')
|
|
319
|
+
.option('--enforce-context-first <true|false>', 'override enforceContextFirst')
|
|
320
|
+
.option('--auto-bootstrap-on-read <true|false>', 'override autoBootstrapOnRead')
|
|
321
|
+
.option('--auto-bootstrap-on-startup <true|false>', 'override autoBootstrapOnStartup')
|
|
322
|
+
.option('--stale-after-minutes <minutes>', 'override staleAfterMinutes with positive integer')
|
|
323
|
+
.option('--json', 'print machine-readable JSON')
|
|
324
|
+
.description('read or update Brainlink MCP bootstrap policy')
|
|
325
|
+
.action(async (options) => {
|
|
326
|
+
const presetPatch = applyPolicyPreset(options.preset);
|
|
327
|
+
const enforceBootstrap = parseBooleanOption(options.enforceBootstrap, '--enforce-bootstrap');
|
|
328
|
+
const enforceContextFirst = parseBooleanOption(options.enforceContextFirst, '--enforce-context-first');
|
|
329
|
+
const autoBootstrapOnRead = parseBooleanOption(options.autoBootstrapOnRead, '--auto-bootstrap-on-read');
|
|
330
|
+
const autoBootstrapOnStartup = parseBooleanOption(options.autoBootstrapOnStartup, '--auto-bootstrap-on-startup');
|
|
331
|
+
const staleAfterMinutes = parsePositiveIntegerOption(options.staleAfterMinutes, '--stale-after-minutes');
|
|
332
|
+
const overridePatch = {
|
|
333
|
+
...(enforceBootstrap !== undefined ? { enforceBootstrap } : {}),
|
|
334
|
+
...(enforceContextFirst !== undefined ? { enforceContextFirst } : {}),
|
|
335
|
+
...(autoBootstrapOnRead !== undefined ? { autoBootstrapOnRead } : {}),
|
|
336
|
+
...(autoBootstrapOnStartup !== undefined ? { autoBootstrapOnStartup } : {}),
|
|
337
|
+
...(staleAfterMinutes !== undefined ? { staleAfterMinutes } : {})
|
|
338
|
+
};
|
|
339
|
+
const patch = {
|
|
340
|
+
...presetPatch,
|
|
341
|
+
...overridePatch
|
|
342
|
+
};
|
|
343
|
+
const policy = Object.keys(patch).length === 0 ? await getBootstrapPolicy() : await setBootstrapPolicy(patch);
|
|
344
|
+
print(options.json, {
|
|
345
|
+
policy,
|
|
346
|
+
...(options.preset ? { presetApplied: options.preset } : {})
|
|
347
|
+
}, () => [
|
|
348
|
+
...(options.preset ? [`presetApplied=${options.preset}`] : []),
|
|
349
|
+
`enforceBootstrap=${policy.enforceBootstrap}`,
|
|
350
|
+
`enforceContextFirst=${policy.enforceContextFirst}`,
|
|
351
|
+
`autoBootstrapOnRead=${policy.autoBootstrapOnRead}`,
|
|
352
|
+
`autoBootstrapOnStartup=${policy.autoBootstrapOnStartup}`,
|
|
353
|
+
`staleAfterMinutes=${policy.staleAfterMinutes}`
|
|
354
|
+
].join('\n'));
|
|
355
|
+
});
|
|
356
|
+
agent
|
|
357
|
+
.command('status')
|
|
358
|
+
.option('-a, --agent <agent>', 'agent memory namespace for bootstrap session status')
|
|
359
|
+
.option('--json', 'print machine-readable JSON')
|
|
360
|
+
.description('check if Brainlink MCP integration is configured for the local agent')
|
|
361
|
+
.action(async (options) => {
|
|
362
|
+
const codexConfigPath = getCodexConfigPath();
|
|
363
|
+
let codexConfig = '';
|
|
364
|
+
try {
|
|
365
|
+
codexConfig = await readFile(codexConfigPath, 'utf8');
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') {
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const { hasMcpSection, hasCommand } = isBrainlinkConfigured(codexConfig);
|
|
373
|
+
const pluginSymlinkPath = getPluginSymlinkPath();
|
|
374
|
+
const marketplacePath = getMarketplacePath();
|
|
375
|
+
let pluginSymlinkExists = false;
|
|
376
|
+
let marketplaceEntryExists = false;
|
|
377
|
+
try {
|
|
378
|
+
pluginSymlinkExists = (await lstat(pluginSymlinkPath)).isSymbolicLink();
|
|
379
|
+
}
|
|
380
|
+
catch { }
|
|
381
|
+
try {
|
|
382
|
+
const marketplace = await loadMarketplace(marketplacePath);
|
|
383
|
+
marketplaceEntryExists = marketplace.plugins.some((plugin) => plugin?.name === 'brainlink');
|
|
384
|
+
}
|
|
385
|
+
catch { }
|
|
386
|
+
const config = await loadBrainlinkConfig();
|
|
387
|
+
const policy = await getBootstrapPolicy();
|
|
388
|
+
const bootstrapStatus = await getBootstrapSessionStatus(config.vault, options.agent ?? config.defaultAgent);
|
|
389
|
+
const contextStatus = await getContextSessionStatus(config.vault, options.agent ?? config.defaultAgent);
|
|
390
|
+
const sessionStatePath = getSessionStatePath();
|
|
391
|
+
print(options.json, {
|
|
392
|
+
configured: hasMcpSection && hasCommand,
|
|
393
|
+
codexConfigPath,
|
|
394
|
+
hasMcpSection,
|
|
395
|
+
hasCommand,
|
|
396
|
+
pluginSymlinkPath,
|
|
397
|
+
pluginSymlinkExists,
|
|
398
|
+
marketplacePath,
|
|
399
|
+
marketplaceEntryExists,
|
|
400
|
+
sessionStatePath,
|
|
401
|
+
vault: config.vault,
|
|
402
|
+
agent: options.agent ?? config.defaultAgent ?? '*',
|
|
403
|
+
bootstrapPolicy: policy,
|
|
404
|
+
bootstrapStatus,
|
|
405
|
+
contextStatus
|
|
406
|
+
}, () => [
|
|
407
|
+
`codexConfigPath=${codexConfigPath}`,
|
|
408
|
+
`configured=${hasMcpSection && hasCommand}`,
|
|
409
|
+
`pluginSymlinkExists=${pluginSymlinkExists}`,
|
|
410
|
+
`marketplaceEntryExists=${marketplaceEntryExists}`,
|
|
411
|
+
`vault=${config.vault}`,
|
|
412
|
+
`agent=${options.agent ?? config.defaultAgent ?? '*'}`,
|
|
413
|
+
`bootstrapReady=${bootstrapStatus.ready}`,
|
|
414
|
+
`bootstrapStale=${bootstrapStatus.stale}`,
|
|
415
|
+
`contextReady=${contextStatus.ready}`,
|
|
416
|
+
`contextStale=${contextStatus.stale}`
|
|
417
|
+
].join('\n'));
|
|
418
|
+
});
|
|
419
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { doctorVault } from '../../application/analyze-vault.js';
|
|
2
|
+
import { indexVault } from '../../application/index-vault.js';
|
|
3
|
+
import { migrateVaultContent, shouldMigrateDefaultVault } from '../../application/migrate-vault.js';
|
|
4
|
+
import { defaultBrainlinkConfig, detectVaultConfigSource, loadBrainlinkConfig, loadLegacyLocalRawConfig, loadRawConfig, resolveConfigPath, writeRawConfig } from '../../infrastructure/config.js';
|
|
5
|
+
import { assertVaultAllowed } from '../../infrastructure/file-system-vault.js';
|
|
6
|
+
import { print } from '../runtime.js';
|
|
7
|
+
const resolveScope = (globalOption) => globalOption ? 'global' : 'local';
|
|
8
|
+
const normalizeVaultPath = (vault) => assertVaultAllowed(vault, []);
|
|
9
|
+
const uniqueValues = (values) => Array.from(new Set(values));
|
|
10
|
+
const resolveScopeFromSource = (source) => source === 'global' || source === 'default' ? 'global' : 'local';
|
|
11
|
+
export const registerConfigCommands = (program) => {
|
|
12
|
+
const configCommand = program.command('config').description('read or update Brainlink configuration');
|
|
13
|
+
configCommand
|
|
14
|
+
.command('get [key]')
|
|
15
|
+
.option('--json', 'print machine-readable JSON')
|
|
16
|
+
.description('read effective Brainlink config values')
|
|
17
|
+
.action(async (key, options) => {
|
|
18
|
+
const config = await loadBrainlinkConfig();
|
|
19
|
+
if (!key) {
|
|
20
|
+
print(options.json, config, () => JSON.stringify(config, null, 2));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!(key in config)) {
|
|
24
|
+
throw new Error(`Unknown config key: ${key}`);
|
|
25
|
+
}
|
|
26
|
+
const value = config[key];
|
|
27
|
+
print(options.json, { key, value }, () => `${key}=${typeof value === 'string' ? value : JSON.stringify(value)}`);
|
|
28
|
+
});
|
|
29
|
+
configCommand
|
|
30
|
+
.command('set-vault <vault>')
|
|
31
|
+
.option('--global', 'write to global config in $BRAINLINK_HOME/brainlink.config.json')
|
|
32
|
+
.option('--no-allowlist', 'do not append the vault to allowedVaults in the target config file')
|
|
33
|
+
.option('--migrate-from <vault>', 'copy existing Markdown memory from another vault into the configured vault')
|
|
34
|
+
.option('--no-migrate', 'skip migration step')
|
|
35
|
+
.option('--no-index', 'skip reindex after migration')
|
|
36
|
+
.option('--json', 'print machine-readable JSON')
|
|
37
|
+
.description('set the default vault path in Brainlink config')
|
|
38
|
+
.action(async (vault, options) => {
|
|
39
|
+
const scope = resolveScope(options.global);
|
|
40
|
+
const before = await loadBrainlinkConfig();
|
|
41
|
+
const targetVault = normalizeVaultPath(vault);
|
|
42
|
+
const rawConfig = await loadRawConfig(scope);
|
|
43
|
+
const configPath = resolveConfigPath(scope);
|
|
44
|
+
const shouldAllowlist = options.allowlist !== false;
|
|
45
|
+
const nextAllowedVaults = shouldAllowlist
|
|
46
|
+
? uniqueValues([...(rawConfig.allowedVaults ?? []), targetVault])
|
|
47
|
+
: rawConfig.allowedVaults;
|
|
48
|
+
const nextRawConfig = {
|
|
49
|
+
...rawConfig,
|
|
50
|
+
vault: targetVault,
|
|
51
|
+
...(nextAllowedVaults ? { allowedVaults: nextAllowedVaults } : {})
|
|
52
|
+
};
|
|
53
|
+
await writeRawConfig(scope, nextRawConfig);
|
|
54
|
+
const shouldMigrate = options.migrate !== false;
|
|
55
|
+
const explicitSource = options.migrateFrom ? normalizeVaultPath(options.migrateFrom) : undefined;
|
|
56
|
+
const shouldAutoMigrate = shouldMigrate &&
|
|
57
|
+
explicitSource === undefined &&
|
|
58
|
+
(await shouldMigrateDefaultVault(before.vault, targetVault));
|
|
59
|
+
const migrationSource = shouldMigrate ? explicitSource ?? (shouldAutoMigrate ? before.vault : undefined) : undefined;
|
|
60
|
+
const migration = migrationSource ? await migrateVaultContent(migrationSource, targetVault) : undefined;
|
|
61
|
+
const shouldIndex = options.index !== false && migration !== undefined && migration.copied + migration.conflicted > 0;
|
|
62
|
+
const index = shouldIndex ? await indexVault(targetVault) : undefined;
|
|
63
|
+
const after = await loadBrainlinkConfig();
|
|
64
|
+
print(options.json, {
|
|
65
|
+
scope,
|
|
66
|
+
configPath,
|
|
67
|
+
beforeVault: before.vault,
|
|
68
|
+
vault: targetVault,
|
|
69
|
+
migration: migration ?? null,
|
|
70
|
+
index: index ?? null,
|
|
71
|
+
config: after
|
|
72
|
+
}, () => {
|
|
73
|
+
const migrationMessage = migration
|
|
74
|
+
? ` Migrated ${migration.copied} files, preserved ${migration.conflicted} conflicts and kept ${migration.unchanged} unchanged files.`
|
|
75
|
+
: '';
|
|
76
|
+
const indexMessage = index
|
|
77
|
+
? ` Indexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} links.`
|
|
78
|
+
: '';
|
|
79
|
+
return `Configured ${scope} vault at ${targetVault} in ${configPath}.${migrationMessage}${indexMessage}`;
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
configCommand
|
|
83
|
+
.command('where')
|
|
84
|
+
.option('--json', 'print machine-readable JSON')
|
|
85
|
+
.description('show effective vault path and config file locations')
|
|
86
|
+
.action(async (options) => {
|
|
87
|
+
const config = await loadBrainlinkConfig();
|
|
88
|
+
print(options.json, {
|
|
89
|
+
vault: config.vault,
|
|
90
|
+
localConfigPath: resolveConfigPath('local'),
|
|
91
|
+
globalConfigPath: resolveConfigPath('global'),
|
|
92
|
+
defaultVault: defaultBrainlinkConfig.vault
|
|
93
|
+
}, () => [
|
|
94
|
+
`vault=${config.vault}`,
|
|
95
|
+
`localConfigPath=${resolveConfigPath('local')}`,
|
|
96
|
+
`globalConfigPath=${resolveConfigPath('global')}`,
|
|
97
|
+
`defaultVault=${defaultBrainlinkConfig.vault}`
|
|
98
|
+
].join('\n'));
|
|
99
|
+
});
|
|
100
|
+
configCommand
|
|
101
|
+
.command('doctor')
|
|
102
|
+
.option('--fix', 'apply safe config fixes (without this flag, doctor is dry-run)')
|
|
103
|
+
.option('--json', 'print machine-readable JSON')
|
|
104
|
+
.description('inspect effective config sources and run vault readiness checks')
|
|
105
|
+
.action(async (options) => {
|
|
106
|
+
const config = await loadBrainlinkConfig();
|
|
107
|
+
const source = await detectVaultConfigSource();
|
|
108
|
+
const globalConfigPath = resolveConfigPath('global');
|
|
109
|
+
const localConfigPath = resolveConfigPath('local');
|
|
110
|
+
const allowedVaultCheck = assertVaultAllowed(config.vault, config.allowedVaults);
|
|
111
|
+
const vaultDoctor = await doctorVault(config.vault);
|
|
112
|
+
const targetScope = resolveScopeFromSource(source);
|
|
113
|
+
const rawConfig = source === 'local-legacy'
|
|
114
|
+
? await loadLegacyLocalRawConfig()
|
|
115
|
+
: await loadRawConfig(targetScope);
|
|
116
|
+
const normalizedVault = normalizeVaultPath(typeof rawConfig.vault === 'string' ? rawConfig.vault : config.vault);
|
|
117
|
+
const normalizedAllowedVaults = uniqueValues([
|
|
118
|
+
...(Array.isArray(rawConfig.allowedVaults) ? rawConfig.allowedVaults.filter((item) => typeof item === 'string') : []),
|
|
119
|
+
normalizedVault
|
|
120
|
+
].map((value) => normalizeVaultPath(value)));
|
|
121
|
+
const nextRawConfig = {
|
|
122
|
+
...rawConfig,
|
|
123
|
+
vault: normalizedVault,
|
|
124
|
+
allowedVaults: normalizedAllowedVaults
|
|
125
|
+
};
|
|
126
|
+
const plannedFixes = [
|
|
127
|
+
`normalize vault path in ${targetScope} config`,
|
|
128
|
+
`ensure allowedVaults includes ${normalizedVault}`,
|
|
129
|
+
...(source === 'local-legacy' ? ['migrate .brainlink.json settings into brainlink.config.json'] : []),
|
|
130
|
+
...(source === 'default' ? ['create global brainlink.config.json with explicit vault'] : [])
|
|
131
|
+
];
|
|
132
|
+
let fixApplied = false;
|
|
133
|
+
let fixedConfigPath = null;
|
|
134
|
+
if (options.fix) {
|
|
135
|
+
fixedConfigPath = await writeRawConfig(targetScope, nextRawConfig);
|
|
136
|
+
fixApplied = true;
|
|
137
|
+
}
|
|
138
|
+
const response = {
|
|
139
|
+
vault: config.vault,
|
|
140
|
+
vaultSource: source,
|
|
141
|
+
allowedVaultCheck,
|
|
142
|
+
localConfigPath,
|
|
143
|
+
globalConfigPath,
|
|
144
|
+
doctor: vaultDoctor,
|
|
145
|
+
fix: {
|
|
146
|
+
dryRun: options.fix !== true,
|
|
147
|
+
applied: fixApplied,
|
|
148
|
+
scope: targetScope,
|
|
149
|
+
path: fixedConfigPath,
|
|
150
|
+
plannedFixes
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
print(options.json, response, () => [
|
|
154
|
+
`vault=${response.vault}`,
|
|
155
|
+
`vaultSource=${response.vaultSource}`,
|
|
156
|
+
`localConfigPath=${response.localConfigPath}`,
|
|
157
|
+
`globalConfigPath=${response.globalConfigPath}`,
|
|
158
|
+
`configFixDryRun=${response.fix.dryRun}`,
|
|
159
|
+
...(response.fix.applied && response.fix.path ? [`configFixAppliedAt=${response.fix.path}`] : []),
|
|
160
|
+
...(response.fix.plannedFixes.length > 0 ? ['Planned config fixes:', ...response.fix.plannedFixes.map((step) => `- ${step}`)] : []),
|
|
161
|
+
...response.doctor.checks.map((check) => `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.message}`),
|
|
162
|
+
...(response.doctor.recommendations && response.doctor.recommendations.length > 0
|
|
163
|
+
? ['Recommended next steps:', ...response.doctor.recommendations.map((recommendation) => `- ${recommendation}`)]
|
|
164
|
+
: [])
|
|
165
|
+
].join('\n'));
|
|
166
|
+
});
|
|
167
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../../application/analyze-vault.js';
|
|
1
|
+
import { getBrokenLinksReport, getExtendedStats, getOrphansReport, getStats, validateVault } from '../../application/analyze-vault.js';
|
|
2
2
|
import { buildContextPackage } from '../../application/build-context.js';
|
|
3
3
|
import { getGraph } from '../../application/get-graph.js';
|
|
4
4
|
import { listAgents } from '../../application/list-agents.js';
|
|
@@ -12,14 +12,14 @@ export const registerReadCommands = (program) => {
|
|
|
12
12
|
.argument('<query>', 'search query')
|
|
13
13
|
.option('-v, --vault <vault>', 'vault directory')
|
|
14
14
|
.option('-a, --agent <agent>', 'filter by agent memory namespace')
|
|
15
|
-
.option('-l, --limit <limit>', 'maximum results'
|
|
15
|
+
.option('-l, --limit <limit>', 'maximum results')
|
|
16
16
|
.option('-m, --mode <mode>', 'search mode: fts, semantic or hybrid')
|
|
17
17
|
.option('--json', 'print machine-readable JSON')
|
|
18
18
|
.description('search indexed knowledge')
|
|
19
19
|
.action(async (query, options) => {
|
|
20
20
|
const resolved = await resolveOptions(options);
|
|
21
|
-
const limit = parsePositiveInteger(options.limit ?? String(resolved.
|
|
22
|
-
const mode = sanitizeSearchMode(options.mode, resolved.
|
|
21
|
+
const limit = parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit);
|
|
22
|
+
const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
|
|
23
23
|
const results = await searchKnowledge(resolved.vault, query, limit, resolved.agent, mode);
|
|
24
24
|
print(options.json, { query, agent: resolved.agent, limit, mode, results }, () => results
|
|
25
25
|
.map((result, index) => [`${index + 1}. ${result.title} (${result.path}) score=${result.score.toFixed(3)} mode=${result.searchMode}`, result.content].join('\n'))
|
|
@@ -58,15 +58,15 @@ export const registerReadCommands = (program) => {
|
|
|
58
58
|
.argument('<query>', 'context query')
|
|
59
59
|
.option('-v, --vault <vault>', 'vault directory')
|
|
60
60
|
.option('-a, --agent <agent>', 'filter by agent memory namespace')
|
|
61
|
-
.option('-l, --limit <limit>', 'maximum search results before context selection'
|
|
62
|
-
.option('-t, --tokens <tokens>', 'maximum estimated context tokens'
|
|
61
|
+
.option('-l, --limit <limit>', 'maximum search results before context selection')
|
|
62
|
+
.option('-t, --tokens <tokens>', 'maximum estimated context tokens')
|
|
63
63
|
.option('-m, --mode <mode>', 'search mode: fts, semantic or hybrid')
|
|
64
64
|
.option('--json', 'print machine-readable JSON')
|
|
65
65
|
.description('build a compact context package for an agent')
|
|
66
66
|
.action(async (query, options) => {
|
|
67
67
|
const resolved = await resolveOptions(options);
|
|
68
|
-
const mode = sanitizeSearchMode(options.mode, resolved.
|
|
69
|
-
const contextPackage = await buildContextPackage(resolved.vault, query, parsePositiveInteger(options.limit ??
|
|
68
|
+
const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
|
|
69
|
+
const contextPackage = await buildContextPackage(resolved.vault, query, parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit), parsePositiveInteger(options.tokens ?? String(resolved.defaults.defaultContextTokens), resolved.defaults.defaultContextTokens), resolved.agent, mode);
|
|
70
70
|
print(options.json, contextPackage, () => contextPackage.content);
|
|
71
71
|
});
|
|
72
72
|
program
|
|
@@ -94,10 +94,27 @@ export const registerReadCommands = (program) => {
|
|
|
94
94
|
.command('stats')
|
|
95
95
|
.option('-v, --vault <vault>', 'vault directory')
|
|
96
96
|
.option('-a, --agent <agent>', 'filter by agent memory namespace')
|
|
97
|
+
.option('--extended', 'include storage, quality and latency observability probes')
|
|
97
98
|
.option('--json', 'print machine-readable JSON')
|
|
98
99
|
.description('print indexed vault statistics')
|
|
99
100
|
.action(async (options) => {
|
|
100
101
|
const resolved = await resolveOptions(options);
|
|
102
|
+
if (options.extended) {
|
|
103
|
+
const stats = await getExtendedStats(resolved.vault, resolved.agent);
|
|
104
|
+
print(options.json, stats, () => [
|
|
105
|
+
`Documents: ${stats.stats.documentCount}`,
|
|
106
|
+
`Links: ${stats.stats.linkCount}`,
|
|
107
|
+
`Resolved links: ${stats.stats.resolvedLinkCount}`,
|
|
108
|
+
`Broken links: ${stats.stats.brokenLinkCount}`,
|
|
109
|
+
`Orphans: ${stats.stats.orphanCount}`,
|
|
110
|
+
`Tags: ${stats.stats.tagCount}`,
|
|
111
|
+
`Total files: ${stats.storage.totalFileCount}`,
|
|
112
|
+
`Markdown files: ${stats.storage.markdownFileCount}`,
|
|
113
|
+
`Vault bytes: ${stats.storage.totalBytes}`,
|
|
114
|
+
`Latency index/search/context (ms): ${stats.observability.latenciesMs.index}/${stats.observability.latenciesMs.search}/${stats.observability.latenciesMs.context}`
|
|
115
|
+
].join('\n'));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
101
118
|
const stats = await getStats(resolved.vault, resolved.agent);
|
|
102
119
|
print(options.json, stats, () => [
|
|
103
120
|
`Documents: ${stats.documentCount}`,
|