@andespindola/brainlink 0.1.0-beta.3 → 0.1.0-beta.31
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 +5 -5
- package/CHANGELOG.md +37 -3
- package/CONTRIBUTING.md +2 -2
- package/COPYRIGHT.md +5 -0
- package/README.md +172 -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/frontend/client-css.js +214 -100
- package/dist/application/frontend/client-html.js +60 -45
- package/dist/application/frontend/client-js.js +553 -91
- package/dist/application/get-graph-layout.js +22 -7
- 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 +11 -4
- package/dist/application/list-agents.js +3 -3
- package/dist/application/list-links.js +5 -5
- package/dist/application/migrate-vault.js +91 -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 +27 -1
- package/dist/benchmarks/large-vault.js +1 -1
- package/dist/cli/commands/agent-commands.js +412 -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 +205 -4
- 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 +20 -14
- package/dist/domain/markdown.js +36 -4
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +94 -8
- package/dist/infrastructure/file-index.js +294 -0
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/paths.js +9 -1
- package/dist/infrastructure/private-pack-codec.js +73 -0
- package/dist/infrastructure/search-packs.js +348 -0
- package/dist/infrastructure/session-state.js +172 -0
- package/dist/mcp/main.js +11 -3
- package/dist/mcp/server.js +17 -2
- package/dist/mcp/startup.js +35 -0
- package/dist/mcp/tools.js +571 -19
- package/docs/AGENT_USAGE.md +112 -16
- package/docs/ARCHITECTURE.md +37 -26
- package/docs/QUICKSTART.md +111 -0
- package/package.json +2 -3
- 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
package/dist/mcp/tools.js
CHANGED
|
@@ -2,20 +2,26 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { basename, extname } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../application/analyze-vault.js';
|
|
5
|
-
import {
|
|
5
|
+
import { addNoteWithMetadata } from '../application/add-note.js';
|
|
6
6
|
import { buildContextPackage } from '../application/build-context.js';
|
|
7
7
|
import { getGraph } from '../application/get-graph.js';
|
|
8
8
|
import { indexVault } from '../application/index-vault.js';
|
|
9
9
|
import { searchKnowledge } from '../application/search-knowledge.js';
|
|
10
|
-
import { sanitizeSearchMode } from '../infrastructure/config.js';
|
|
10
|
+
import { resolveAgentRuntimeDefaults, sanitizeSearchMode } from '../infrastructure/config.js';
|
|
11
11
|
import { loadBrainlinkConfig } from '../infrastructure/config.js';
|
|
12
12
|
import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
|
|
13
|
+
import { getBootstrapPolicy, getBootstrapSessionStatus, getContextSessionStatus, setBootstrapPolicy, touchBootstrapSession, touchContextSession } from '../infrastructure/session-state.js';
|
|
13
14
|
const positiveInteger = (fallback) => z
|
|
14
15
|
.number()
|
|
15
16
|
.int()
|
|
16
17
|
.positive()
|
|
17
18
|
.optional()
|
|
18
19
|
.transform((value) => value ?? fallback);
|
|
20
|
+
const optionalPositiveInteger = () => z
|
|
21
|
+
.number()
|
|
22
|
+
.int()
|
|
23
|
+
.positive()
|
|
24
|
+
.optional();
|
|
19
25
|
const vaultInput = {
|
|
20
26
|
vault: z.string().min(1).optional().describe('Vault directory. Omit to use the configured Brainlink default vault.')
|
|
21
27
|
};
|
|
@@ -33,10 +39,12 @@ const resolveExecutionContext = async (input) => {
|
|
|
33
39
|
const config = await loadBrainlinkConfig();
|
|
34
40
|
const vault = await assertVaultAllowed(input.vault ?? config.vault, config.allowedVaults);
|
|
35
41
|
const agent = input.agent ?? config.defaultAgent;
|
|
42
|
+
const defaults = resolveAgentRuntimeDefaults(config, agent);
|
|
36
43
|
return {
|
|
37
44
|
config,
|
|
38
45
|
vault,
|
|
39
|
-
agent
|
|
46
|
+
agent,
|
|
47
|
+
defaults
|
|
40
48
|
};
|
|
41
49
|
};
|
|
42
50
|
const inferTitleFromPath = (filePath) => {
|
|
@@ -58,20 +66,164 @@ const jsonResult = (value) => ({
|
|
|
58
66
|
],
|
|
59
67
|
structuredContent: value
|
|
60
68
|
});
|
|
69
|
+
const preflightResult = (value) => jsonResult({
|
|
70
|
+
preflightRequired: true,
|
|
71
|
+
...value
|
|
72
|
+
});
|
|
73
|
+
const withNextActions = (value, nextActions) => ({
|
|
74
|
+
...value,
|
|
75
|
+
nextActions
|
|
76
|
+
});
|
|
77
|
+
const ensureBootstrapReady = async (context, input, toolName) => {
|
|
78
|
+
const policy = await getBootstrapPolicy();
|
|
79
|
+
if (!policy.enforceBootstrap) {
|
|
80
|
+
return {
|
|
81
|
+
bootstrap: {
|
|
82
|
+
autoBootstrapped: false,
|
|
83
|
+
policy,
|
|
84
|
+
statusBefore: {
|
|
85
|
+
ready: true,
|
|
86
|
+
stale: false
|
|
87
|
+
},
|
|
88
|
+
reason: 'Bootstrap enforcement is disabled by policy.'
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const status = await getBootstrapSessionStatus(context.vault, context.agent);
|
|
93
|
+
if (status.ready) {
|
|
94
|
+
return {
|
|
95
|
+
bootstrap: {
|
|
96
|
+
autoBootstrapped: false,
|
|
97
|
+
policy,
|
|
98
|
+
statusBefore: status,
|
|
99
|
+
reason: 'Bootstrap session is already fresh for this vault/agent.'
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (policy.autoBootstrapOnRead) {
|
|
104
|
+
const index = await indexVault(context.vault);
|
|
105
|
+
const session = await touchBootstrapSession(context.vault, context.agent);
|
|
106
|
+
const statusAfter = await getBootstrapSessionStatus(context.vault, context.agent);
|
|
107
|
+
return {
|
|
108
|
+
bootstrap: {
|
|
109
|
+
autoBootstrapped: true,
|
|
110
|
+
policy,
|
|
111
|
+
statusBefore: status,
|
|
112
|
+
statusAfter,
|
|
113
|
+
session,
|
|
114
|
+
index,
|
|
115
|
+
reason: 'Auto-bootstrap was applied for this read call because bootstrap was missing or stale.'
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const mode = typeof input.mode === 'string' && ['fts', 'semantic', 'hybrid'].includes(input.mode) ? input.mode : 'hybrid';
|
|
120
|
+
const query = typeof input.query === 'string' && input.query.trim().length > 0 ? input.query : undefined;
|
|
121
|
+
const bootstrapArgs = {
|
|
122
|
+
vault: context.vault,
|
|
123
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
124
|
+
...(query ? { query } : {}),
|
|
125
|
+
mode
|
|
126
|
+
};
|
|
127
|
+
const nextActions = [
|
|
128
|
+
{
|
|
129
|
+
tool: 'brainlink_bootstrap',
|
|
130
|
+
reason: 'Bootstrap is required before read tools so retrieval stays grounded in current vault state.',
|
|
131
|
+
args: bootstrapArgs
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
return {
|
|
135
|
+
preflight: preflightResult(withNextActions({
|
|
136
|
+
vault: context.vault,
|
|
137
|
+
agent: context.agent,
|
|
138
|
+
blockedTool: toolName,
|
|
139
|
+
policy,
|
|
140
|
+
bootstrapStatus: status,
|
|
141
|
+
guidance: 'Run brainlink_bootstrap first for this vault/agent before using read tools. This keeps retrieval grounded and memory state consistent.',
|
|
142
|
+
bootstrapArgs
|
|
143
|
+
}, nextActions))
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
const ensureContextReady = async (context, input, toolName) => {
|
|
147
|
+
const policy = await getBootstrapPolicy();
|
|
148
|
+
if (!policy.enforceContextFirst) {
|
|
149
|
+
return {
|
|
150
|
+
context: {
|
|
151
|
+
policy,
|
|
152
|
+
statusBefore: {
|
|
153
|
+
ready: true,
|
|
154
|
+
stale: false
|
|
155
|
+
},
|
|
156
|
+
reason: 'Context-first enforcement is disabled by policy.'
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const status = await getContextSessionStatus(context.vault, context.agent);
|
|
161
|
+
if (status.ready) {
|
|
162
|
+
return {
|
|
163
|
+
context: {
|
|
164
|
+
policy,
|
|
165
|
+
statusBefore: status,
|
|
166
|
+
reason: 'Context session is already fresh for this vault/agent.'
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const queryFromInput = typeof input.query === 'string' && input.query.trim().length > 0
|
|
171
|
+
? input.query
|
|
172
|
+
: typeof input.contextQuery === 'string' && input.contextQuery.trim().length > 0
|
|
173
|
+
? input.contextQuery
|
|
174
|
+
: '<task>';
|
|
175
|
+
const mode = sanitizeSearchMode(typeof input.mode === 'string' ? input.mode : undefined, context.defaults.defaultSearchMode);
|
|
176
|
+
const limit = typeof input.limit === 'number' && Number.isFinite(input.limit) && input.limit > 0
|
|
177
|
+
? input.limit
|
|
178
|
+
: typeof input.contextLimit === 'number' && Number.isFinite(input.contextLimit) && input.contextLimit > 0
|
|
179
|
+
? input.contextLimit
|
|
180
|
+
: context.defaults.defaultSearchLimit;
|
|
181
|
+
const tokens = typeof input.tokens === 'number' && Number.isFinite(input.tokens) && input.tokens > 0
|
|
182
|
+
? input.tokens
|
|
183
|
+
: typeof input.contextTokens === 'number' && Number.isFinite(input.contextTokens) && input.contextTokens > 0
|
|
184
|
+
? input.contextTokens
|
|
185
|
+
: context.defaults.defaultContextTokens;
|
|
186
|
+
const contextArgs = {
|
|
187
|
+
vault: context.vault,
|
|
188
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
189
|
+
query: queryFromInput,
|
|
190
|
+
mode,
|
|
191
|
+
limit,
|
|
192
|
+
tokens
|
|
193
|
+
};
|
|
194
|
+
const nextActions = [
|
|
195
|
+
{
|
|
196
|
+
tool: 'brainlink_context',
|
|
197
|
+
reason: 'Context must be loaded first so Brainlink is the primary retrieval source before other read tools.',
|
|
198
|
+
args: contextArgs
|
|
199
|
+
}
|
|
200
|
+
];
|
|
201
|
+
return {
|
|
202
|
+
preflight: preflightResult(withNextActions({
|
|
203
|
+
vault: context.vault,
|
|
204
|
+
agent: context.agent,
|
|
205
|
+
blockedTool: toolName,
|
|
206
|
+
policy,
|
|
207
|
+
contextStatus: status,
|
|
208
|
+
guidance: 'Run brainlink_context first for this vault/agent before other read tools so answers are grounded on Brainlink context.',
|
|
209
|
+
contextArgs
|
|
210
|
+
}, nextActions))
|
|
211
|
+
};
|
|
212
|
+
};
|
|
61
213
|
export const contextInputSchema = {
|
|
62
214
|
...vaultInput,
|
|
63
215
|
...agentInput,
|
|
64
216
|
...searchModeInput,
|
|
65
217
|
query: z.string().min(1).describe('Task or question to retrieve Brainlink context for.'),
|
|
66
|
-
limit:
|
|
67
|
-
tokens:
|
|
218
|
+
limit: optionalPositiveInteger().describe('Maximum search results before context selection.'),
|
|
219
|
+
tokens: optionalPositiveInteger().describe('Maximum estimated context tokens.')
|
|
68
220
|
};
|
|
69
221
|
export const searchInputSchema = {
|
|
70
222
|
...vaultInput,
|
|
71
223
|
...agentInput,
|
|
72
224
|
...searchModeInput,
|
|
73
225
|
query: z.string().min(1).describe('Search query.'),
|
|
74
|
-
limit:
|
|
226
|
+
limit: optionalPositiveInteger().describe('Maximum result count.')
|
|
75
227
|
};
|
|
76
228
|
export const addNoteInputSchema = {
|
|
77
229
|
...vaultInput,
|
|
@@ -120,37 +272,95 @@ export const syncInputSchema = {
|
|
|
120
272
|
...agentInput,
|
|
121
273
|
contextQuery: z.string().min(1).optional().describe('Optional context smoke query. Omit to skip context probe.'),
|
|
122
274
|
mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode for the optional context probe. Defaults to config value.'),
|
|
123
|
-
contextLimit:
|
|
124
|
-
contextTokens:
|
|
275
|
+
contextLimit: optionalPositiveInteger().describe('Context smoke result limit when contextQuery is provided.'),
|
|
276
|
+
contextTokens: optionalPositiveInteger().describe('Context smoke token target when contextQuery is provided.')
|
|
277
|
+
};
|
|
278
|
+
export const bootstrapInputSchema = {
|
|
279
|
+
...vaultInput,
|
|
280
|
+
...agentInput,
|
|
281
|
+
...searchModeInput,
|
|
282
|
+
query: z
|
|
283
|
+
.string()
|
|
284
|
+
.min(1)
|
|
285
|
+
.optional()
|
|
286
|
+
.describe('Optional task query. When provided, Brainlink also returns a context package in the same call.'),
|
|
287
|
+
limit: optionalPositiveInteger().describe('Context limit used when query is provided.'),
|
|
288
|
+
tokens: optionalPositiveInteger().describe('Context token target used when query is provided.')
|
|
289
|
+
};
|
|
290
|
+
export const policyInputSchema = {
|
|
291
|
+
...vaultInput,
|
|
292
|
+
...agentInput,
|
|
293
|
+
preset: z.enum(['fully-auto', 'strict']).optional().describe('Apply an opinionated policy preset before explicit overrides.'),
|
|
294
|
+
enforceBootstrap: z.boolean().optional().describe('Enable or disable bootstrap enforcement for MCP read tools.'),
|
|
295
|
+
enforceContextFirst: z.boolean().optional().describe('Require brainlink_context before other MCP read tools.'),
|
|
296
|
+
autoBootstrapOnRead: z
|
|
297
|
+
.boolean()
|
|
298
|
+
.optional()
|
|
299
|
+
.describe('When bootstrap is missing/stale, run automatic bootstrap on read tools instead of returning preflight-required responses.'),
|
|
300
|
+
autoBootstrapOnStartup: z
|
|
301
|
+
.boolean()
|
|
302
|
+
.optional()
|
|
303
|
+
.describe('Run automatic bootstrap during MCP server startup using configured default vault/agent.'),
|
|
304
|
+
staleAfterMinutes: positiveInteger(120).describe('Bootstrap freshness window in minutes before read tools require a new bootstrap.')
|
|
305
|
+
};
|
|
306
|
+
export const recommendationsInputSchema = {
|
|
307
|
+
...vaultInput,
|
|
308
|
+
...agentInput,
|
|
309
|
+
...searchModeInput,
|
|
310
|
+
query: z.string().min(1).optional().describe('Optional current task query to generate context-focused recommendations.'),
|
|
311
|
+
limit: optionalPositiveInteger().describe('Optional context limit override for generated recommendations.'),
|
|
312
|
+
tokens: optionalPositiveInteger().describe('Optional context token budget override for generated recommendations.')
|
|
125
313
|
};
|
|
126
314
|
export const contextTool = async (input) => {
|
|
127
315
|
const context = await resolveExecutionContext(input);
|
|
128
|
-
const
|
|
129
|
-
|
|
316
|
+
const readiness = await ensureBootstrapReady(context, input, 'brainlink_context');
|
|
317
|
+
if (readiness.preflight) {
|
|
318
|
+
return readiness.preflight;
|
|
319
|
+
}
|
|
320
|
+
const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
|
|
321
|
+
const limit = input.limit ?? context.defaults.defaultSearchLimit;
|
|
322
|
+
const tokens = input.tokens ?? context.defaults.defaultContextTokens;
|
|
323
|
+
const contextPackage = await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode);
|
|
324
|
+
const contextSession = await touchContextSession(context.vault, context.agent);
|
|
130
325
|
return jsonResult({
|
|
131
326
|
vault: context.vault,
|
|
132
327
|
agent: context.agent,
|
|
133
328
|
mode,
|
|
329
|
+
limit,
|
|
330
|
+
tokens,
|
|
331
|
+
contextSession,
|
|
332
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
134
333
|
...contextPackage
|
|
135
334
|
});
|
|
136
335
|
};
|
|
137
336
|
export const searchTool = async (input) => {
|
|
138
337
|
const context = await resolveExecutionContext(input);
|
|
139
|
-
const
|
|
140
|
-
|
|
338
|
+
const readiness = await ensureBootstrapReady(context, input, 'brainlink_search');
|
|
339
|
+
if (readiness.preflight) {
|
|
340
|
+
return readiness.preflight;
|
|
341
|
+
}
|
|
342
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_search');
|
|
343
|
+
if (contextReadiness.preflight) {
|
|
344
|
+
return contextReadiness.preflight;
|
|
345
|
+
}
|
|
346
|
+
const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
|
|
347
|
+
const limit = input.limit ?? context.defaults.defaultSearchLimit;
|
|
348
|
+
const results = await searchKnowledge(context.vault, input.query, limit, context.agent, mode);
|
|
141
349
|
return jsonResult({
|
|
142
350
|
vault: context.vault,
|
|
143
351
|
agent: context.agent,
|
|
144
352
|
query: input.query,
|
|
145
|
-
limit
|
|
353
|
+
limit,
|
|
146
354
|
mode,
|
|
355
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
356
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
147
357
|
results
|
|
148
358
|
});
|
|
149
359
|
};
|
|
150
360
|
export const addNoteTool = async (input) => {
|
|
151
361
|
const context = await resolveExecutionContext(input);
|
|
152
362
|
const shouldIndex = isTruthy(input.autoIndex);
|
|
153
|
-
const
|
|
363
|
+
const added = await addNoteWithMetadata(context.vault, input.title, input.content, context.agent, {
|
|
154
364
|
allowSensitive: input.allowSensitive
|
|
155
365
|
});
|
|
156
366
|
const index = shouldIndex ? await indexVault(context.vault) : undefined;
|
|
@@ -158,7 +368,12 @@ export const addNoteTool = async (input) => {
|
|
|
158
368
|
vault: context.vault,
|
|
159
369
|
title: input.title,
|
|
160
370
|
agent: context.agent,
|
|
161
|
-
path,
|
|
371
|
+
path: added.path,
|
|
372
|
+
writeConnectivity: {
|
|
373
|
+
autoLinked: added.autoLinked,
|
|
374
|
+
linkTarget: added.linkTarget,
|
|
375
|
+
guaranteedEdge: true
|
|
376
|
+
},
|
|
162
377
|
...(index ? { index } : {})
|
|
163
378
|
});
|
|
164
379
|
};
|
|
@@ -171,7 +386,7 @@ export const addFileTool = async (input) => {
|
|
|
171
386
|
throw new Error('Cannot infer note title from file path. Provide a title explicitly.');
|
|
172
387
|
}
|
|
173
388
|
const shouldIndex = isTruthy(input.autoIndex);
|
|
174
|
-
const
|
|
389
|
+
const added = await addNoteWithMetadata(context.vault, title, content, context.agent, {
|
|
175
390
|
allowSensitive: input.allowSensitive
|
|
176
391
|
});
|
|
177
392
|
const index = shouldIndex ? await indexVault(context.vault) : undefined;
|
|
@@ -180,7 +395,12 @@ export const addFileTool = async (input) => {
|
|
|
180
395
|
title,
|
|
181
396
|
agent: context.agent,
|
|
182
397
|
filePath: input.filePath,
|
|
183
|
-
path,
|
|
398
|
+
path: added.path,
|
|
399
|
+
writeConnectivity: {
|
|
400
|
+
autoLinked: added.autoLinked,
|
|
401
|
+
linkTarget: added.linkTarget,
|
|
402
|
+
guaranteedEdge: true
|
|
403
|
+
},
|
|
184
404
|
...(index ? { index } : {})
|
|
185
405
|
});
|
|
186
406
|
};
|
|
@@ -194,51 +414,109 @@ export const indexTool = async (input) => {
|
|
|
194
414
|
};
|
|
195
415
|
export const validateTool = async (input) => {
|
|
196
416
|
const context = await resolveExecutionContext(input);
|
|
417
|
+
const readiness = await ensureBootstrapReady(context, input, 'brainlink_validate');
|
|
418
|
+
if (readiness.preflight) {
|
|
419
|
+
return readiness.preflight;
|
|
420
|
+
}
|
|
421
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_validate');
|
|
422
|
+
if (contextReadiness.preflight) {
|
|
423
|
+
return contextReadiness.preflight;
|
|
424
|
+
}
|
|
197
425
|
const validation = await validateVault(context.vault, context.agent);
|
|
198
426
|
return jsonResult({
|
|
199
427
|
vault: context.vault,
|
|
200
428
|
agent: context.agent,
|
|
429
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
430
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
201
431
|
...validation
|
|
202
432
|
});
|
|
203
433
|
};
|
|
204
434
|
export const graphTool = async (input) => {
|
|
205
435
|
const context = await resolveExecutionContext(input);
|
|
436
|
+
const readiness = await ensureBootstrapReady(context, input, 'brainlink_graph');
|
|
437
|
+
if (readiness.preflight) {
|
|
438
|
+
return readiness.preflight;
|
|
439
|
+
}
|
|
440
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_graph');
|
|
441
|
+
if (contextReadiness.preflight) {
|
|
442
|
+
return contextReadiness.preflight;
|
|
443
|
+
}
|
|
206
444
|
const graph = await getGraph(context.vault, context.agent);
|
|
207
445
|
return jsonResult({
|
|
208
446
|
vault: context.vault,
|
|
209
447
|
agent: context.agent,
|
|
448
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
449
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
210
450
|
...graph
|
|
211
451
|
});
|
|
212
452
|
};
|
|
213
453
|
export const brokenLinksTool = async (input) => {
|
|
214
454
|
const context = await resolveExecutionContext(input);
|
|
455
|
+
const readiness = await ensureBootstrapReady(context, input, 'brainlink_broken_links');
|
|
456
|
+
if (readiness.preflight) {
|
|
457
|
+
return readiness.preflight;
|
|
458
|
+
}
|
|
459
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_broken_links');
|
|
460
|
+
if (contextReadiness.preflight) {
|
|
461
|
+
return contextReadiness.preflight;
|
|
462
|
+
}
|
|
215
463
|
const brokenLinks = await getBrokenLinksReport(context.vault, context.agent);
|
|
216
464
|
return jsonResult({
|
|
217
465
|
vault: context.vault,
|
|
218
466
|
agent: context.agent,
|
|
467
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
468
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
219
469
|
brokenLinks
|
|
220
470
|
});
|
|
221
471
|
};
|
|
222
472
|
export const orphansTool = async (input) => {
|
|
223
473
|
const context = await resolveExecutionContext(input);
|
|
474
|
+
const readiness = await ensureBootstrapReady(context, input, 'brainlink_orphans');
|
|
475
|
+
if (readiness.preflight) {
|
|
476
|
+
return readiness.preflight;
|
|
477
|
+
}
|
|
478
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_orphans');
|
|
479
|
+
if (contextReadiness.preflight) {
|
|
480
|
+
return contextReadiness.preflight;
|
|
481
|
+
}
|
|
224
482
|
const orphans = await getOrphansReport(context.vault, context.agent);
|
|
225
483
|
return jsonResult({
|
|
226
484
|
vault: context.vault,
|
|
227
485
|
agent: context.agent,
|
|
486
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
487
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
228
488
|
orphans
|
|
229
489
|
});
|
|
230
490
|
};
|
|
231
491
|
export const statsTool = async (input) => {
|
|
232
492
|
const context = await resolveExecutionContext(input);
|
|
493
|
+
const readiness = await ensureBootstrapReady(context, input, 'brainlink_stats');
|
|
494
|
+
if (readiness.preflight) {
|
|
495
|
+
return readiness.preflight;
|
|
496
|
+
}
|
|
497
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_stats');
|
|
498
|
+
if (contextReadiness.preflight) {
|
|
499
|
+
return contextReadiness.preflight;
|
|
500
|
+
}
|
|
233
501
|
const stats = await getStats(context.vault, context.agent);
|
|
234
502
|
return jsonResult({
|
|
235
503
|
vault: context.vault,
|
|
236
504
|
agent: context.agent,
|
|
505
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
506
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
237
507
|
stats
|
|
238
508
|
});
|
|
239
509
|
};
|
|
240
510
|
export const syncTool = async (input) => {
|
|
241
511
|
const context = await resolveExecutionContext(input);
|
|
512
|
+
const readiness = await ensureBootstrapReady(context, input, 'brainlink_sync');
|
|
513
|
+
if (readiness.preflight) {
|
|
514
|
+
return readiness.preflight;
|
|
515
|
+
}
|
|
516
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_sync');
|
|
517
|
+
if (contextReadiness.preflight) {
|
|
518
|
+
return contextReadiness.preflight;
|
|
519
|
+
}
|
|
242
520
|
const index = await indexVault(context.vault);
|
|
243
521
|
const stats = await getStats(context.vault, context.agent);
|
|
244
522
|
const validation = await validateVault(context.vault, context.agent);
|
|
@@ -247,6 +525,8 @@ export const syncTool = async (input) => {
|
|
|
247
525
|
const response = {
|
|
248
526
|
vault: context.vault,
|
|
249
527
|
agent: context.agent,
|
|
528
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
529
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
250
530
|
index,
|
|
251
531
|
stats,
|
|
252
532
|
validation,
|
|
@@ -256,13 +536,285 @@ export const syncTool = async (input) => {
|
|
|
256
536
|
if (!input.contextQuery) {
|
|
257
537
|
return jsonResult(response);
|
|
258
538
|
}
|
|
259
|
-
const mode = sanitizeSearchMode(input.mode, context.
|
|
260
|
-
const
|
|
539
|
+
const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
|
|
540
|
+
const contextLimit = input.contextLimit ?? context.defaults.defaultSearchLimit;
|
|
541
|
+
const contextTokens = input.contextTokens ?? context.defaults.defaultContextTokens;
|
|
542
|
+
const contextPackage = await buildContextPackage(context.vault, input.contextQuery, contextLimit, contextTokens, context.agent, mode);
|
|
543
|
+
const contextSession = await touchContextSession(context.vault, context.agent);
|
|
261
544
|
return jsonResult({
|
|
262
545
|
...response,
|
|
263
546
|
context: {
|
|
264
547
|
mode,
|
|
548
|
+
contextSession,
|
|
265
549
|
...contextPackage
|
|
266
550
|
}
|
|
267
551
|
});
|
|
268
552
|
};
|
|
553
|
+
export const bootstrapTool = async (input) => {
|
|
554
|
+
const context = await resolveExecutionContext(input);
|
|
555
|
+
const index = await indexVault(context.vault);
|
|
556
|
+
const stats = await getStats(context.vault, context.agent);
|
|
557
|
+
const validation = await validateVault(context.vault, context.agent);
|
|
558
|
+
const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
|
|
559
|
+
const limit = input.limit ?? context.defaults.defaultSearchLimit;
|
|
560
|
+
const tokens = input.tokens ?? context.defaults.defaultContextTokens;
|
|
561
|
+
const contextPackage = input.query
|
|
562
|
+
? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode)
|
|
563
|
+
: undefined;
|
|
564
|
+
const contextSession = input.query ? await touchContextSession(context.vault, context.agent) : undefined;
|
|
565
|
+
const guidance = stats.documentCount === 0
|
|
566
|
+
? 'Vault indexed with zero documents. Add durable notes with brainlink_add_note, then run brainlink_bootstrap again.'
|
|
567
|
+
: input.query
|
|
568
|
+
? 'Use returned context as grounding baseline, then write durable updates with brainlink_add_note when needed.'
|
|
569
|
+
: 'Run brainlink_context with the current task query to retrieve grounded context before answering.';
|
|
570
|
+
const session = await touchBootstrapSession(context.vault, context.agent);
|
|
571
|
+
const policy = await getBootstrapPolicy();
|
|
572
|
+
const nextActions = stats.documentCount === 0
|
|
573
|
+
? [
|
|
574
|
+
{
|
|
575
|
+
tool: 'brainlink_add_note',
|
|
576
|
+
reason: 'No indexed documents were found. Add durable Markdown memory first.',
|
|
577
|
+
args: {
|
|
578
|
+
vault: context.vault,
|
|
579
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
580
|
+
title: 'Architecture',
|
|
581
|
+
content: 'Durable memory with explicit [[links]] and #tags.'
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
tool: 'brainlink_bootstrap',
|
|
586
|
+
reason: 'Re-run bootstrap after writing notes so context tools can work on fresh index state.',
|
|
587
|
+
args: {
|
|
588
|
+
vault: context.vault,
|
|
589
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
590
|
+
mode
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
]
|
|
594
|
+
: input.query
|
|
595
|
+
? [
|
|
596
|
+
{
|
|
597
|
+
tool: 'brainlink_add_note',
|
|
598
|
+
reason: 'Persist relevant outcomes from this task as durable memory.',
|
|
599
|
+
args: {
|
|
600
|
+
vault: context.vault,
|
|
601
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
602
|
+
title: 'Task Update',
|
|
603
|
+
content: 'Summarize durable findings and connect with [[existing notes]].'
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
]
|
|
607
|
+
: [
|
|
608
|
+
{
|
|
609
|
+
tool: 'brainlink_context',
|
|
610
|
+
reason: 'Fetch grounded context for the current task.',
|
|
611
|
+
args: {
|
|
612
|
+
vault: context.vault,
|
|
613
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
614
|
+
query: '<task>',
|
|
615
|
+
mode,
|
|
616
|
+
limit,
|
|
617
|
+
tokens
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
];
|
|
621
|
+
return jsonResult(withNextActions({
|
|
622
|
+
vault: context.vault,
|
|
623
|
+
agent: context.agent,
|
|
624
|
+
mode,
|
|
625
|
+
limit,
|
|
626
|
+
tokens,
|
|
627
|
+
index,
|
|
628
|
+
stats,
|
|
629
|
+
validation,
|
|
630
|
+
policy,
|
|
631
|
+
session,
|
|
632
|
+
guidance,
|
|
633
|
+
...(contextPackage ? { context: contextPackage } : {}),
|
|
634
|
+
...(contextSession ? { contextSession } : {})
|
|
635
|
+
}, nextActions));
|
|
636
|
+
};
|
|
637
|
+
export const policyTool = async (input) => {
|
|
638
|
+
const context = await resolveExecutionContext(input);
|
|
639
|
+
const presetPatch = input.preset === 'strict'
|
|
640
|
+
? {
|
|
641
|
+
enforceBootstrap: true,
|
|
642
|
+
enforceContextFirst: true,
|
|
643
|
+
autoBootstrapOnRead: false,
|
|
644
|
+
autoBootstrapOnStartup: false
|
|
645
|
+
}
|
|
646
|
+
: input.preset === 'fully-auto'
|
|
647
|
+
? {
|
|
648
|
+
enforceBootstrap: true,
|
|
649
|
+
enforceContextFirst: true,
|
|
650
|
+
autoBootstrapOnRead: true,
|
|
651
|
+
autoBootstrapOnStartup: true
|
|
652
|
+
}
|
|
653
|
+
: {};
|
|
654
|
+
const policy = input.preset !== undefined ||
|
|
655
|
+
typeof input.enforceBootstrap === 'boolean' ||
|
|
656
|
+
typeof input.enforceContextFirst === 'boolean' ||
|
|
657
|
+
typeof input.autoBootstrapOnRead === 'boolean' ||
|
|
658
|
+
typeof input.autoBootstrapOnStartup === 'boolean' ||
|
|
659
|
+
typeof input.staleAfterMinutes === 'number'
|
|
660
|
+
? await setBootstrapPolicy({
|
|
661
|
+
...presetPatch,
|
|
662
|
+
...(typeof input.enforceBootstrap === 'boolean' ? { enforceBootstrap: input.enforceBootstrap } : {}),
|
|
663
|
+
...(typeof input.enforceContextFirst === 'boolean' ? { enforceContextFirst: input.enforceContextFirst } : {}),
|
|
664
|
+
...(typeof input.autoBootstrapOnRead === 'boolean' ? { autoBootstrapOnRead: input.autoBootstrapOnRead } : {}),
|
|
665
|
+
...(typeof input.autoBootstrapOnStartup === 'boolean' ? { autoBootstrapOnStartup: input.autoBootstrapOnStartup } : {}),
|
|
666
|
+
...(typeof input.staleAfterMinutes === 'number' ? { staleAfterMinutes: input.staleAfterMinutes } : {})
|
|
667
|
+
})
|
|
668
|
+
: await getBootstrapPolicy();
|
|
669
|
+
const bootstrapStatus = await getBootstrapSessionStatus(context.vault, context.agent);
|
|
670
|
+
const contextStatus = await getContextSessionStatus(context.vault, context.agent);
|
|
671
|
+
const nextActions = bootstrapStatus.ready
|
|
672
|
+
? []
|
|
673
|
+
: [
|
|
674
|
+
{
|
|
675
|
+
tool: 'brainlink_bootstrap',
|
|
676
|
+
reason: 'Bootstrap status is not ready. Run bootstrap before using read tools.',
|
|
677
|
+
args: {
|
|
678
|
+
vault: context.vault,
|
|
679
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
680
|
+
mode: context.defaults.defaultSearchMode
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
];
|
|
684
|
+
const withContextAction = policy.enforceContextFirst && !contextStatus.ready
|
|
685
|
+
? [
|
|
686
|
+
...nextActions,
|
|
687
|
+
{
|
|
688
|
+
tool: 'brainlink_context',
|
|
689
|
+
reason: 'Context-first policy is enabled. Load context before other read tools.',
|
|
690
|
+
args: {
|
|
691
|
+
vault: context.vault,
|
|
692
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
693
|
+
query: '<task>',
|
|
694
|
+
mode: context.defaults.defaultSearchMode,
|
|
695
|
+
limit: context.defaults.defaultSearchLimit,
|
|
696
|
+
tokens: context.defaults.defaultContextTokens
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
]
|
|
700
|
+
: nextActions;
|
|
701
|
+
return jsonResult(withNextActions({
|
|
702
|
+
vault: context.vault,
|
|
703
|
+
agent: context.agent,
|
|
704
|
+
policy,
|
|
705
|
+
bootstrapStatus,
|
|
706
|
+
contextStatus,
|
|
707
|
+
...(input.preset ? { presetApplied: input.preset } : {})
|
|
708
|
+
}, withContextAction));
|
|
709
|
+
};
|
|
710
|
+
export const recommendationsTool = async (input) => {
|
|
711
|
+
const context = await resolveExecutionContext(input);
|
|
712
|
+
const policy = await getBootstrapPolicy();
|
|
713
|
+
const bootstrapStatus = await getBootstrapSessionStatus(context.vault, context.agent);
|
|
714
|
+
const contextStatus = await getContextSessionStatus(context.vault, context.agent);
|
|
715
|
+
const stats = await getStats(context.vault, context.agent);
|
|
716
|
+
const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
|
|
717
|
+
const limit = input.limit ?? context.defaults.defaultSearchLimit;
|
|
718
|
+
const tokens = input.tokens ?? context.defaults.defaultContextTokens;
|
|
719
|
+
const query = input.query?.trim();
|
|
720
|
+
const recommendations = [
|
|
721
|
+
...(policy.enforceBootstrap && (!policy.autoBootstrapOnRead || !policy.autoBootstrapOnStartup)
|
|
722
|
+
? [
|
|
723
|
+
{
|
|
724
|
+
tool: 'brainlink_policy',
|
|
725
|
+
reason: 'Enable fully automatic bootstrap for plug-and-play agent usage.',
|
|
726
|
+
args: {
|
|
727
|
+
preset: 'fully-auto'
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
]
|
|
731
|
+
: []),
|
|
732
|
+
...(!bootstrapStatus.ready && !policy.autoBootstrapOnRead
|
|
733
|
+
? [
|
|
734
|
+
{
|
|
735
|
+
tool: 'brainlink_bootstrap',
|
|
736
|
+
reason: 'Bootstrap is required before read tools when auto-bootstrap-on-read is disabled.',
|
|
737
|
+
args: {
|
|
738
|
+
vault: context.vault,
|
|
739
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
740
|
+
mode,
|
|
741
|
+
...(query ? { query } : {})
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
]
|
|
745
|
+
: []),
|
|
746
|
+
...(policy.enforceContextFirst && !contextStatus.ready
|
|
747
|
+
? [
|
|
748
|
+
{
|
|
749
|
+
tool: 'brainlink_context',
|
|
750
|
+
reason: 'Context-first policy is enabled. Load context before other read operations.',
|
|
751
|
+
args: {
|
|
752
|
+
vault: context.vault,
|
|
753
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
754
|
+
query: query ?? '<task>',
|
|
755
|
+
mode,
|
|
756
|
+
limit,
|
|
757
|
+
tokens
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
]
|
|
761
|
+
: []),
|
|
762
|
+
...(stats.documentCount === 0
|
|
763
|
+
? [
|
|
764
|
+
{
|
|
765
|
+
tool: 'brainlink_add_note',
|
|
766
|
+
reason: 'Seed the vault with a first durable note so retrieval can return useful context.',
|
|
767
|
+
args: {
|
|
768
|
+
vault: context.vault,
|
|
769
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
770
|
+
title: 'Architecture',
|
|
771
|
+
content: 'Seed durable memory with explicit [[links]] and #tags.'
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
tool: 'brainlink_index',
|
|
776
|
+
reason: 'Rebuild index after writing the first notes.',
|
|
777
|
+
args: {
|
|
778
|
+
vault: context.vault
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
]
|
|
782
|
+
: []),
|
|
783
|
+
{
|
|
784
|
+
tool: 'brainlink_context',
|
|
785
|
+
reason: 'Retrieve grounded memory context before responding.',
|
|
786
|
+
args: {
|
|
787
|
+
vault: context.vault,
|
|
788
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
789
|
+
query: query ?? '<task>',
|
|
790
|
+
mode,
|
|
791
|
+
limit,
|
|
792
|
+
tokens
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
tool: 'brainlink_add_note',
|
|
797
|
+
reason: 'Persist durable outcomes after task completion (write responses include connectivity metadata).',
|
|
798
|
+
args: {
|
|
799
|
+
vault: context.vault,
|
|
800
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
801
|
+
title: 'Task Update',
|
|
802
|
+
content: 'Durable findings connected to [[existing notes]].'
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
];
|
|
806
|
+
return jsonResult({
|
|
807
|
+
vault: context.vault,
|
|
808
|
+
agent: context.agent,
|
|
809
|
+
defaults: {
|
|
810
|
+
mode,
|
|
811
|
+
limit,
|
|
812
|
+
tokens
|
|
813
|
+
},
|
|
814
|
+
policy,
|
|
815
|
+
bootstrapStatus,
|
|
816
|
+
contextStatus,
|
|
817
|
+
stats,
|
|
818
|
+
recommendations
|
|
819
|
+
});
|
|
820
|
+
};
|