@andespindola/brainlink 0.1.0-beta.0 → 0.1.0-beta.10
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/CHANGELOG.md +46 -0
- package/README.md +252 -19
- package/dist/application/add-note.js +62 -13
- package/dist/application/analyze-vault.js +104 -9
- package/dist/application/frontend/client-css.js +154 -71
- package/dist/application/frontend/client-html.js +42 -33
- package/dist/application/frontend/client-js.js +255 -70
- package/dist/application/get-graph-layout.js +6 -3
- package/dist/application/get-graph-node.js +12 -0
- package/dist/application/get-graph-summary.js +12 -0
- package/dist/application/migrate-vault.js +91 -0
- package/dist/application/search-graph-node-ids.js +12 -0
- package/dist/application/search-knowledge.js +56 -1
- package/dist/application/server/routes.js +27 -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 +191 -7
- package/dist/cli/main.js +4 -0
- package/dist/cli/runtime.js +5 -2
- package/dist/domain/embeddings.js +2 -1
- package/dist/domain/graph-layout.js +20 -14
- package/dist/domain/markdown.js +36 -4
- package/dist/infrastructure/config.js +96 -8
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/paths.js +9 -1
- package/dist/infrastructure/session-state.js +172 -0
- package/dist/infrastructure/sqlite/graph-reader.js +252 -105
- package/dist/infrastructure/sqlite/recovery.js +83 -0
- package/dist/infrastructure/sqlite/schema.js +4 -1
- package/dist/infrastructure/sqlite/search-reader.js +104 -72
- package/dist/infrastructure/sqlite-index.js +16 -3
- package/dist/mcp/main.js +11 -3
- package/dist/mcp/server.js +22 -2
- package/dist/mcp/startup.js +35 -0
- package/dist/mcp/tools.js +617 -21
- package/docs/AGENT_USAGE.md +95 -6
- package/docs/ARCHITECTURE.md +15 -1
- package/docs/QUICKSTART.md +104 -0
- package/docs/RELEASE.md +3 -3
- package/package.json +1 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, extname } from 'node:path';
|
|
1
3
|
import { z } from 'zod';
|
|
2
4
|
import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../application/analyze-vault.js';
|
|
3
|
-
import {
|
|
5
|
+
import { addNoteWithMetadata } from '../application/add-note.js';
|
|
4
6
|
import { buildContextPackage } from '../application/build-context.js';
|
|
5
7
|
import { getGraph } from '../application/get-graph.js';
|
|
6
8
|
import { indexVault } from '../application/index-vault.js';
|
|
7
9
|
import { searchKnowledge } from '../application/search-knowledge.js';
|
|
8
|
-
import { sanitizeSearchMode } from '../infrastructure/config.js';
|
|
10
|
+
import { resolveAgentRuntimeDefaults, sanitizeSearchMode } from '../infrastructure/config.js';
|
|
9
11
|
import { loadBrainlinkConfig } from '../infrastructure/config.js';
|
|
10
12
|
import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
|
|
13
|
+
import { getBootstrapPolicy, getBootstrapSessionStatus, getContextSessionStatus, setBootstrapPolicy, touchBootstrapSession, touchContextSession } from '../infrastructure/session-state.js';
|
|
11
14
|
const positiveInteger = (fallback) => z
|
|
12
15
|
.number()
|
|
13
16
|
.int()
|
|
14
17
|
.positive()
|
|
15
18
|
.optional()
|
|
16
19
|
.transform((value) => value ?? fallback);
|
|
20
|
+
const optionalPositiveInteger = () => z
|
|
21
|
+
.number()
|
|
22
|
+
.int()
|
|
23
|
+
.positive()
|
|
24
|
+
.optional();
|
|
17
25
|
const vaultInput = {
|
|
18
26
|
vault: z.string().min(1).optional().describe('Vault directory. Omit to use the configured Brainlink default vault.')
|
|
19
27
|
};
|
|
@@ -31,12 +39,24 @@ const resolveExecutionContext = async (input) => {
|
|
|
31
39
|
const config = await loadBrainlinkConfig();
|
|
32
40
|
const vault = await assertVaultAllowed(input.vault ?? config.vault, config.allowedVaults);
|
|
33
41
|
const agent = input.agent ?? config.defaultAgent;
|
|
42
|
+
const defaults = resolveAgentRuntimeDefaults(config, agent);
|
|
34
43
|
return {
|
|
35
|
-
vault,
|
|
36
44
|
config,
|
|
37
|
-
|
|
45
|
+
vault,
|
|
46
|
+
agent,
|
|
47
|
+
defaults
|
|
38
48
|
};
|
|
39
49
|
};
|
|
50
|
+
const inferTitleFromPath = (filePath) => {
|
|
51
|
+
const extension = extname(filePath);
|
|
52
|
+
const fromFileName = basename(filePath, extension);
|
|
53
|
+
return fromFileName
|
|
54
|
+
.trim()
|
|
55
|
+
.replace(/[-_]+/g, ' ')
|
|
56
|
+
.replace(/\s+/g, ' ')
|
|
57
|
+
.trim();
|
|
58
|
+
};
|
|
59
|
+
const isTruthy = (value) => value !== false;
|
|
40
60
|
const jsonResult = (value) => ({
|
|
41
61
|
content: [
|
|
42
62
|
{
|
|
@@ -46,20 +66,164 @@ const jsonResult = (value) => ({
|
|
|
46
66
|
],
|
|
47
67
|
structuredContent: value
|
|
48
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
|
+
};
|
|
49
213
|
export const contextInputSchema = {
|
|
50
214
|
...vaultInput,
|
|
51
215
|
...agentInput,
|
|
52
216
|
...searchModeInput,
|
|
53
217
|
query: z.string().min(1).describe('Task or question to retrieve Brainlink context for.'),
|
|
54
|
-
limit:
|
|
55
|
-
tokens:
|
|
218
|
+
limit: optionalPositiveInteger().describe('Maximum search results before context selection.'),
|
|
219
|
+
tokens: optionalPositiveInteger().describe('Maximum estimated context tokens.')
|
|
56
220
|
};
|
|
57
221
|
export const searchInputSchema = {
|
|
58
222
|
...vaultInput,
|
|
59
223
|
...agentInput,
|
|
60
224
|
...searchModeInput,
|
|
61
225
|
query: z.string().min(1).describe('Search query.'),
|
|
62
|
-
limit:
|
|
226
|
+
limit: optionalPositiveInteger().describe('Maximum result count.')
|
|
63
227
|
};
|
|
64
228
|
export const addNoteInputSchema = {
|
|
65
229
|
...vaultInput,
|
|
@@ -68,7 +232,16 @@ export const addNoteInputSchema = {
|
|
|
68
232
|
.string()
|
|
69
233
|
.min(1)
|
|
70
234
|
.describe('Durable Markdown memory. Include explicit [[wiki links]] and #tags when the memory should be connected. Put priority markers near important links, for example priority: high, #important or #critical.'),
|
|
71
|
-
|
|
235
|
+
...agentInput,
|
|
236
|
+
allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.'),
|
|
237
|
+
autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.')
|
|
238
|
+
};
|
|
239
|
+
export const addFileInputSchema = {
|
|
240
|
+
...vaultInput,
|
|
241
|
+
...agentInput,
|
|
242
|
+
title: z.string().min(1).optional().describe('Optional note title override. If omitted, uses file name.'),
|
|
243
|
+
filePath: z.string().min(1).describe('Filesystem path to markdown or text file to ingest.'),
|
|
244
|
+
autoIndex: z.boolean().optional().default(true).describe('Reindex vault after ingesting file.'),
|
|
72
245
|
allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.')
|
|
73
246
|
};
|
|
74
247
|
export const indexInputSchema = {
|
|
@@ -99,45 +272,136 @@ export const syncInputSchema = {
|
|
|
99
272
|
...agentInput,
|
|
100
273
|
contextQuery: z.string().min(1).optional().describe('Optional context smoke query. Omit to skip context probe.'),
|
|
101
274
|
mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode for the optional context probe. Defaults to config value.'),
|
|
102
|
-
contextLimit:
|
|
103
|
-
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.')
|
|
104
313
|
};
|
|
105
314
|
export const contextTool = async (input) => {
|
|
106
315
|
const context = await resolveExecutionContext(input);
|
|
107
|
-
const
|
|
108
|
-
|
|
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);
|
|
109
325
|
return jsonResult({
|
|
110
326
|
vault: context.vault,
|
|
111
327
|
agent: context.agent,
|
|
112
328
|
mode,
|
|
329
|
+
limit,
|
|
330
|
+
tokens,
|
|
331
|
+
contextSession,
|
|
332
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
113
333
|
...contextPackage
|
|
114
334
|
});
|
|
115
335
|
};
|
|
116
336
|
export const searchTool = async (input) => {
|
|
117
337
|
const context = await resolveExecutionContext(input);
|
|
118
|
-
const
|
|
119
|
-
|
|
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);
|
|
120
349
|
return jsonResult({
|
|
121
350
|
vault: context.vault,
|
|
122
351
|
agent: context.agent,
|
|
123
352
|
query: input.query,
|
|
124
|
-
limit
|
|
353
|
+
limit,
|
|
125
354
|
mode,
|
|
355
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
356
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
126
357
|
results
|
|
127
358
|
});
|
|
128
359
|
};
|
|
129
360
|
export const addNoteTool = async (input) => {
|
|
130
361
|
const context = await resolveExecutionContext(input);
|
|
131
|
-
const
|
|
362
|
+
const shouldIndex = isTruthy(input.autoIndex);
|
|
363
|
+
const added = await addNoteWithMetadata(context.vault, input.title, input.content, context.agent, {
|
|
132
364
|
allowSensitive: input.allowSensitive
|
|
133
365
|
});
|
|
134
|
-
const index = await indexVault(context.vault);
|
|
366
|
+
const index = shouldIndex ? await indexVault(context.vault) : undefined;
|
|
135
367
|
return jsonResult({
|
|
136
368
|
vault: context.vault,
|
|
137
369
|
title: input.title,
|
|
138
370
|
agent: context.agent,
|
|
139
|
-
path,
|
|
140
|
-
|
|
371
|
+
path: added.path,
|
|
372
|
+
writeConnectivity: {
|
|
373
|
+
autoLinked: added.autoLinked,
|
|
374
|
+
linkTarget: added.linkTarget,
|
|
375
|
+
guaranteedEdge: true
|
|
376
|
+
},
|
|
377
|
+
...(index ? { index } : {})
|
|
378
|
+
});
|
|
379
|
+
};
|
|
380
|
+
export const addFileTool = async (input) => {
|
|
381
|
+
const context = await resolveExecutionContext(input);
|
|
382
|
+
const content = await readFile(input.filePath, 'utf8');
|
|
383
|
+
const inferredTitle = inferTitleFromPath(input.filePath);
|
|
384
|
+
const title = input.title ?? inferredTitle;
|
|
385
|
+
if (title == null || title.length === 0) {
|
|
386
|
+
throw new Error('Cannot infer note title from file path. Provide a title explicitly.');
|
|
387
|
+
}
|
|
388
|
+
const shouldIndex = isTruthy(input.autoIndex);
|
|
389
|
+
const added = await addNoteWithMetadata(context.vault, title, content, context.agent, {
|
|
390
|
+
allowSensitive: input.allowSensitive
|
|
391
|
+
});
|
|
392
|
+
const index = shouldIndex ? await indexVault(context.vault) : undefined;
|
|
393
|
+
return jsonResult({
|
|
394
|
+
vault: context.vault,
|
|
395
|
+
title,
|
|
396
|
+
agent: context.agent,
|
|
397
|
+
filePath: input.filePath,
|
|
398
|
+
path: added.path,
|
|
399
|
+
writeConnectivity: {
|
|
400
|
+
autoLinked: added.autoLinked,
|
|
401
|
+
linkTarget: added.linkTarget,
|
|
402
|
+
guaranteedEdge: true
|
|
403
|
+
},
|
|
404
|
+
...(index ? { index } : {})
|
|
141
405
|
});
|
|
142
406
|
};
|
|
143
407
|
export const indexTool = async (input) => {
|
|
@@ -150,51 +414,109 @@ export const indexTool = async (input) => {
|
|
|
150
414
|
};
|
|
151
415
|
export const validateTool = async (input) => {
|
|
152
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
|
+
}
|
|
153
425
|
const validation = await validateVault(context.vault, context.agent);
|
|
154
426
|
return jsonResult({
|
|
155
427
|
vault: context.vault,
|
|
156
428
|
agent: context.agent,
|
|
429
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
430
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
157
431
|
...validation
|
|
158
432
|
});
|
|
159
433
|
};
|
|
160
434
|
export const graphTool = async (input) => {
|
|
161
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
|
+
}
|
|
162
444
|
const graph = await getGraph(context.vault, context.agent);
|
|
163
445
|
return jsonResult({
|
|
164
446
|
vault: context.vault,
|
|
165
447
|
agent: context.agent,
|
|
448
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
449
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
166
450
|
...graph
|
|
167
451
|
});
|
|
168
452
|
};
|
|
169
453
|
export const brokenLinksTool = async (input) => {
|
|
170
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
|
+
}
|
|
171
463
|
const brokenLinks = await getBrokenLinksReport(context.vault, context.agent);
|
|
172
464
|
return jsonResult({
|
|
173
465
|
vault: context.vault,
|
|
174
466
|
agent: context.agent,
|
|
467
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
468
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
175
469
|
brokenLinks
|
|
176
470
|
});
|
|
177
471
|
};
|
|
178
472
|
export const orphansTool = async (input) => {
|
|
179
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
|
+
}
|
|
180
482
|
const orphans = await getOrphansReport(context.vault, context.agent);
|
|
181
483
|
return jsonResult({
|
|
182
484
|
vault: context.vault,
|
|
183
485
|
agent: context.agent,
|
|
486
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
487
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
184
488
|
orphans
|
|
185
489
|
});
|
|
186
490
|
};
|
|
187
491
|
export const statsTool = async (input) => {
|
|
188
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
|
+
}
|
|
189
501
|
const stats = await getStats(context.vault, context.agent);
|
|
190
502
|
return jsonResult({
|
|
191
503
|
vault: context.vault,
|
|
192
504
|
agent: context.agent,
|
|
505
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
506
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
193
507
|
stats
|
|
194
508
|
});
|
|
195
509
|
};
|
|
196
510
|
export const syncTool = async (input) => {
|
|
197
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
|
+
}
|
|
198
520
|
const index = await indexVault(context.vault);
|
|
199
521
|
const stats = await getStats(context.vault, context.agent);
|
|
200
522
|
const validation = await validateVault(context.vault, context.agent);
|
|
@@ -203,6 +525,8 @@ export const syncTool = async (input) => {
|
|
|
203
525
|
const response = {
|
|
204
526
|
vault: context.vault,
|
|
205
527
|
agent: context.agent,
|
|
528
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
529
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
206
530
|
index,
|
|
207
531
|
stats,
|
|
208
532
|
validation,
|
|
@@ -212,13 +536,285 @@ export const syncTool = async (input) => {
|
|
|
212
536
|
if (!input.contextQuery) {
|
|
213
537
|
return jsonResult(response);
|
|
214
538
|
}
|
|
215
|
-
const mode = sanitizeSearchMode(input.mode, context.
|
|
216
|
-
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);
|
|
217
544
|
return jsonResult({
|
|
218
545
|
...response,
|
|
219
546
|
context: {
|
|
220
547
|
mode,
|
|
548
|
+
contextSession,
|
|
221
549
|
...contextPackage
|
|
222
550
|
}
|
|
223
551
|
});
|
|
224
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
|
+
};
|