@andespindola/brainlink 0.1.0-beta.7 → 0.1.0-beta.9

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.
@@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, brokenLinksInputSchema, brokenLinksTool, contextInputSchema, contextTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool } from './tools.js';
5
+ import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, contextInputSchema, contextTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool } from './tools.js';
6
6
  const readPackageVersion = () => {
7
7
  const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
8
8
  const metadata = JSON.parse(readFileSync(packagePath, 'utf8'));
@@ -15,9 +15,24 @@ export const createBrainlinkMcpServer = () => {
15
15
  version: readPackageVersion(),
16
16
  description: 'Local-first Markdown memory tools for AI agents.'
17
17
  });
18
+ server.registerTool('brainlink_bootstrap', {
19
+ title: 'Bootstrap Brainlink For A Task (Default Entrypoint)',
20
+ description: 'Default entrypoint for agents. Run this first to index/check memory state, then optionally retrieve context for the current task query.',
21
+ inputSchema: bootstrapInputSchema
22
+ }, bootstrapTool);
23
+ server.registerTool('brainlink_policy', {
24
+ title: 'Brainlink Bootstrap Policy',
25
+ description: 'Read or update bootstrap enforcement policy and inspect bootstrap readiness for the current vault/agent.',
26
+ inputSchema: policyInputSchema
27
+ }, policyTool);
28
+ server.registerTool('brainlink_recommendations', {
29
+ title: 'Brainlink Recommended MCP Workflow',
30
+ description: 'Return a plug-and-play action plan for this vault/agent, including policy, bootstrap, context retrieval and durable write guidance.',
31
+ inputSchema: recommendationsInputSchema
32
+ }, recommendationsTool);
18
33
  server.registerTool('brainlink_context', {
19
34
  title: 'Build Brainlink Context',
20
- description: 'Read indexed Brainlink memory for a task or question. This is read-only and does not create graph links.',
35
+ description: 'Read indexed Brainlink memory for a task or question. Usually called after brainlink_bootstrap. This is read-only and does not create graph links.',
21
36
  inputSchema: contextInputSchema
22
37
  }, contextTool);
23
38
  server.registerTool('brainlink_search', {
@@ -0,0 +1,35 @@
1
+ import { indexVault } from '../application/index-vault.js';
2
+ import { loadBrainlinkConfig } from '../infrastructure/config.js';
3
+ import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
4
+ import { getBootstrapPolicy, touchBootstrapSession } from '../infrastructure/session-state.js';
5
+ export const runStartupBootstrap = async () => {
6
+ try {
7
+ const policy = await getBootstrapPolicy();
8
+ if (!policy.autoBootstrapOnStartup) {
9
+ return {
10
+ attempted: false,
11
+ skipped: true,
12
+ reason: 'autoBootstrapOnStartup=false'
13
+ };
14
+ }
15
+ const config = await loadBrainlinkConfig();
16
+ const vault = assertVaultAllowed(config.vault, config.allowedVaults);
17
+ const agent = config.defaultAgent;
18
+ const index = await indexVault(vault);
19
+ await touchBootstrapSession(vault, agent);
20
+ return {
21
+ attempted: true,
22
+ skipped: false,
23
+ vault,
24
+ agent: agent ?? '*',
25
+ index
26
+ };
27
+ }
28
+ catch (error) {
29
+ return {
30
+ attempted: true,
31
+ skipped: false,
32
+ error: error instanceof Error ? error.message : String(error)
33
+ };
34
+ }
35
+ };
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 { addNote } from '../application/add-note.js';
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, setBootstrapPolicy, touchBootstrapSession } 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,97 @@ 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
+ };
61
146
  export const contextInputSchema = {
62
147
  ...vaultInput,
63
148
  ...agentInput,
64
149
  ...searchModeInput,
65
150
  query: z.string().min(1).describe('Task or question to retrieve Brainlink context for.'),
66
- limit: positiveInteger(12).describe('Maximum search results before context selection.'),
67
- tokens: positiveInteger(2000).describe('Maximum estimated context tokens.')
151
+ limit: optionalPositiveInteger().describe('Maximum search results before context selection.'),
152
+ tokens: optionalPositiveInteger().describe('Maximum estimated context tokens.')
68
153
  };
69
154
  export const searchInputSchema = {
70
155
  ...vaultInput,
71
156
  ...agentInput,
72
157
  ...searchModeInput,
73
158
  query: z.string().min(1).describe('Search query.'),
74
- limit: positiveInteger(10).describe('Maximum result count.')
159
+ limit: optionalPositiveInteger().describe('Maximum result count.')
75
160
  };
76
161
  export const addNoteInputSchema = {
77
162
  ...vaultInput,
@@ -120,37 +205,87 @@ export const syncInputSchema = {
120
205
  ...agentInput,
121
206
  contextQuery: z.string().min(1).optional().describe('Optional context smoke query. Omit to skip context probe.'),
122
207
  mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode for the optional context probe. Defaults to config value.'),
123
- contextLimit: positiveInteger(12).describe('Context smoke result limit when contextQuery is provided.'),
124
- contextTokens: positiveInteger(2000).describe('Context smoke token target when contextQuery is provided.')
208
+ contextLimit: optionalPositiveInteger().describe('Context smoke result limit when contextQuery is provided.'),
209
+ contextTokens: optionalPositiveInteger().describe('Context smoke token target when contextQuery is provided.')
210
+ };
211
+ export const bootstrapInputSchema = {
212
+ ...vaultInput,
213
+ ...agentInput,
214
+ ...searchModeInput,
215
+ query: z
216
+ .string()
217
+ .min(1)
218
+ .optional()
219
+ .describe('Optional task query. When provided, Brainlink also returns a context package in the same call.'),
220
+ limit: optionalPositiveInteger().describe('Context limit used when query is provided.'),
221
+ tokens: optionalPositiveInteger().describe('Context token target used when query is provided.')
222
+ };
223
+ export const policyInputSchema = {
224
+ ...vaultInput,
225
+ ...agentInput,
226
+ preset: z.enum(['fully-auto', 'strict']).optional().describe('Apply an opinionated policy preset before explicit overrides.'),
227
+ enforceBootstrap: z.boolean().optional().describe('Enable or disable bootstrap enforcement for MCP read tools.'),
228
+ autoBootstrapOnRead: z
229
+ .boolean()
230
+ .optional()
231
+ .describe('When bootstrap is missing/stale, run automatic bootstrap on read tools instead of returning preflight-required responses.'),
232
+ autoBootstrapOnStartup: z
233
+ .boolean()
234
+ .optional()
235
+ .describe('Run automatic bootstrap during MCP server startup using configured default vault/agent.'),
236
+ staleAfterMinutes: positiveInteger(120).describe('Bootstrap freshness window in minutes before read tools require a new bootstrap.')
237
+ };
238
+ export const recommendationsInputSchema = {
239
+ ...vaultInput,
240
+ ...agentInput,
241
+ ...searchModeInput,
242
+ query: z.string().min(1).optional().describe('Optional current task query to generate context-focused recommendations.'),
243
+ limit: optionalPositiveInteger().describe('Optional context limit override for generated recommendations.'),
244
+ tokens: optionalPositiveInteger().describe('Optional context token budget override for generated recommendations.')
125
245
  };
126
246
  export const contextTool = async (input) => {
127
247
  const context = await resolveExecutionContext(input);
128
- const mode = sanitizeSearchMode(input.mode, context.config.defaultSearchMode);
129
- const contextPackage = await buildContextPackage(context.vault, input.query, input.limit, input.tokens, context.agent, mode);
248
+ const readiness = await ensureBootstrapReady(context, input, 'brainlink_context');
249
+ if (readiness.preflight) {
250
+ return readiness.preflight;
251
+ }
252
+ const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
253
+ const limit = input.limit ?? context.defaults.defaultSearchLimit;
254
+ const tokens = input.tokens ?? context.defaults.defaultContextTokens;
255
+ const contextPackage = await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode);
130
256
  return jsonResult({
131
257
  vault: context.vault,
132
258
  agent: context.agent,
133
259
  mode,
260
+ limit,
261
+ tokens,
262
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
134
263
  ...contextPackage
135
264
  });
136
265
  };
137
266
  export const searchTool = async (input) => {
138
267
  const context = await resolveExecutionContext(input);
139
- const mode = sanitizeSearchMode(input.mode, context.config.defaultSearchMode);
140
- const results = await searchKnowledge(context.vault, input.query, input.limit, context.agent, mode);
268
+ const readiness = await ensureBootstrapReady(context, input, 'brainlink_search');
269
+ if (readiness.preflight) {
270
+ return readiness.preflight;
271
+ }
272
+ const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
273
+ const limit = input.limit ?? context.defaults.defaultSearchLimit;
274
+ const results = await searchKnowledge(context.vault, input.query, limit, context.agent, mode);
141
275
  return jsonResult({
142
276
  vault: context.vault,
143
277
  agent: context.agent,
144
278
  query: input.query,
145
- limit: input.limit,
279
+ limit,
146
280
  mode,
281
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
147
282
  results
148
283
  });
149
284
  };
150
285
  export const addNoteTool = async (input) => {
151
286
  const context = await resolveExecutionContext(input);
152
287
  const shouldIndex = isTruthy(input.autoIndex);
153
- const path = await addNote(context.vault, input.title, input.content, context.agent, {
288
+ const added = await addNoteWithMetadata(context.vault, input.title, input.content, context.agent, {
154
289
  allowSensitive: input.allowSensitive
155
290
  });
156
291
  const index = shouldIndex ? await indexVault(context.vault) : undefined;
@@ -158,7 +293,12 @@ export const addNoteTool = async (input) => {
158
293
  vault: context.vault,
159
294
  title: input.title,
160
295
  agent: context.agent,
161
- path,
296
+ path: added.path,
297
+ writeConnectivity: {
298
+ autoLinked: added.autoLinked,
299
+ linkTarget: added.linkTarget,
300
+ guaranteedEdge: true
301
+ },
162
302
  ...(index ? { index } : {})
163
303
  });
164
304
  };
@@ -171,7 +311,7 @@ export const addFileTool = async (input) => {
171
311
  throw new Error('Cannot infer note title from file path. Provide a title explicitly.');
172
312
  }
173
313
  const shouldIndex = isTruthy(input.autoIndex);
174
- const path = await addNote(context.vault, title, content, context.agent, {
314
+ const added = await addNoteWithMetadata(context.vault, title, content, context.agent, {
175
315
  allowSensitive: input.allowSensitive
176
316
  });
177
317
  const index = shouldIndex ? await indexVault(context.vault) : undefined;
@@ -180,7 +320,12 @@ export const addFileTool = async (input) => {
180
320
  title,
181
321
  agent: context.agent,
182
322
  filePath: input.filePath,
183
- path,
323
+ path: added.path,
324
+ writeConnectivity: {
325
+ autoLinked: added.autoLinked,
326
+ linkTarget: added.linkTarget,
327
+ guaranteedEdge: true
328
+ },
184
329
  ...(index ? { index } : {})
185
330
  });
186
331
  };
@@ -194,51 +339,80 @@ export const indexTool = async (input) => {
194
339
  };
195
340
  export const validateTool = async (input) => {
196
341
  const context = await resolveExecutionContext(input);
342
+ const readiness = await ensureBootstrapReady(context, input, 'brainlink_validate');
343
+ if (readiness.preflight) {
344
+ return readiness.preflight;
345
+ }
197
346
  const validation = await validateVault(context.vault, context.agent);
198
347
  return jsonResult({
199
348
  vault: context.vault,
200
349
  agent: context.agent,
350
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
201
351
  ...validation
202
352
  });
203
353
  };
204
354
  export const graphTool = async (input) => {
205
355
  const context = await resolveExecutionContext(input);
356
+ const readiness = await ensureBootstrapReady(context, input, 'brainlink_graph');
357
+ if (readiness.preflight) {
358
+ return readiness.preflight;
359
+ }
206
360
  const graph = await getGraph(context.vault, context.agent);
207
361
  return jsonResult({
208
362
  vault: context.vault,
209
363
  agent: context.agent,
364
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
210
365
  ...graph
211
366
  });
212
367
  };
213
368
  export const brokenLinksTool = async (input) => {
214
369
  const context = await resolveExecutionContext(input);
370
+ const readiness = await ensureBootstrapReady(context, input, 'brainlink_broken_links');
371
+ if (readiness.preflight) {
372
+ return readiness.preflight;
373
+ }
215
374
  const brokenLinks = await getBrokenLinksReport(context.vault, context.agent);
216
375
  return jsonResult({
217
376
  vault: context.vault,
218
377
  agent: context.agent,
378
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
219
379
  brokenLinks
220
380
  });
221
381
  };
222
382
  export const orphansTool = async (input) => {
223
383
  const context = await resolveExecutionContext(input);
384
+ const readiness = await ensureBootstrapReady(context, input, 'brainlink_orphans');
385
+ if (readiness.preflight) {
386
+ return readiness.preflight;
387
+ }
224
388
  const orphans = await getOrphansReport(context.vault, context.agent);
225
389
  return jsonResult({
226
390
  vault: context.vault,
227
391
  agent: context.agent,
392
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
228
393
  orphans
229
394
  });
230
395
  };
231
396
  export const statsTool = async (input) => {
232
397
  const context = await resolveExecutionContext(input);
398
+ const readiness = await ensureBootstrapReady(context, input, 'brainlink_stats');
399
+ if (readiness.preflight) {
400
+ return readiness.preflight;
401
+ }
233
402
  const stats = await getStats(context.vault, context.agent);
234
403
  return jsonResult({
235
404
  vault: context.vault,
236
405
  agent: context.agent,
406
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
237
407
  stats
238
408
  });
239
409
  };
240
410
  export const syncTool = async (input) => {
241
411
  const context = await resolveExecutionContext(input);
412
+ const readiness = await ensureBootstrapReady(context, input, 'brainlink_sync');
413
+ if (readiness.preflight) {
414
+ return readiness.preflight;
415
+ }
242
416
  const index = await indexVault(context.vault);
243
417
  const stats = await getStats(context.vault, context.agent);
244
418
  const validation = await validateVault(context.vault, context.agent);
@@ -247,6 +421,7 @@ export const syncTool = async (input) => {
247
421
  const response = {
248
422
  vault: context.vault,
249
423
  agent: context.agent,
424
+ ...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
250
425
  index,
251
426
  stats,
252
427
  validation,
@@ -256,8 +431,10 @@ export const syncTool = async (input) => {
256
431
  if (!input.contextQuery) {
257
432
  return jsonResult(response);
258
433
  }
259
- const mode = sanitizeSearchMode(input.mode, context.config.defaultSearchMode);
260
- const contextPackage = await buildContextPackage(context.vault, input.contextQuery, input.contextLimit, input.contextTokens, context.agent, mode);
434
+ const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
435
+ const contextLimit = input.contextLimit ?? context.defaults.defaultSearchLimit;
436
+ const contextTokens = input.contextTokens ?? context.defaults.defaultContextTokens;
437
+ const contextPackage = await buildContextPackage(context.vault, input.contextQuery, contextLimit, contextTokens, context.agent, mode);
261
438
  return jsonResult({
262
439
  ...response,
263
440
  context: {
@@ -266,3 +443,228 @@ export const syncTool = async (input) => {
266
443
  }
267
444
  });
268
445
  };
446
+ export const bootstrapTool = async (input) => {
447
+ const context = await resolveExecutionContext(input);
448
+ const index = await indexVault(context.vault);
449
+ const stats = await getStats(context.vault, context.agent);
450
+ const validation = await validateVault(context.vault, context.agent);
451
+ const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
452
+ const limit = input.limit ?? context.defaults.defaultSearchLimit;
453
+ const tokens = input.tokens ?? context.defaults.defaultContextTokens;
454
+ const contextPackage = input.query
455
+ ? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode)
456
+ : undefined;
457
+ const guidance = stats.documentCount === 0
458
+ ? 'Vault indexed with zero documents. Add durable notes with brainlink_add_note, then run brainlink_bootstrap again.'
459
+ : input.query
460
+ ? 'Use returned context as grounding baseline, then write durable updates with brainlink_add_note when needed.'
461
+ : 'Run brainlink_context with the current task query to retrieve grounded context before answering.';
462
+ const session = await touchBootstrapSession(context.vault, context.agent);
463
+ const policy = await getBootstrapPolicy();
464
+ const nextActions = stats.documentCount === 0
465
+ ? [
466
+ {
467
+ tool: 'brainlink_add_note',
468
+ reason: 'No indexed documents were found. Add durable Markdown memory first.',
469
+ args: {
470
+ vault: context.vault,
471
+ ...(context.agent ? { agent: context.agent } : {}),
472
+ title: 'Architecture',
473
+ content: 'Durable memory with explicit [[links]] and #tags.'
474
+ }
475
+ },
476
+ {
477
+ tool: 'brainlink_bootstrap',
478
+ reason: 'Re-run bootstrap after writing notes so context tools can work on fresh index state.',
479
+ args: {
480
+ vault: context.vault,
481
+ ...(context.agent ? { agent: context.agent } : {}),
482
+ mode
483
+ }
484
+ }
485
+ ]
486
+ : input.query
487
+ ? [
488
+ {
489
+ tool: 'brainlink_add_note',
490
+ reason: 'Persist relevant outcomes from this task as durable memory.',
491
+ args: {
492
+ vault: context.vault,
493
+ ...(context.agent ? { agent: context.agent } : {}),
494
+ title: 'Task Update',
495
+ content: 'Summarize durable findings and connect with [[existing notes]].'
496
+ }
497
+ }
498
+ ]
499
+ : [
500
+ {
501
+ tool: 'brainlink_context',
502
+ reason: 'Fetch grounded context for the current task.',
503
+ args: {
504
+ vault: context.vault,
505
+ ...(context.agent ? { agent: context.agent } : {}),
506
+ query: '<task>',
507
+ mode,
508
+ limit,
509
+ tokens
510
+ }
511
+ }
512
+ ];
513
+ return jsonResult(withNextActions({
514
+ vault: context.vault,
515
+ agent: context.agent,
516
+ mode,
517
+ limit,
518
+ tokens,
519
+ index,
520
+ stats,
521
+ validation,
522
+ policy,
523
+ session,
524
+ guidance,
525
+ ...(contextPackage ? { context: contextPackage } : {})
526
+ }, nextActions));
527
+ };
528
+ export const policyTool = async (input) => {
529
+ const context = await resolveExecutionContext(input);
530
+ const presetPatch = input.preset === 'strict'
531
+ ? {
532
+ enforceBootstrap: true,
533
+ autoBootstrapOnRead: false,
534
+ autoBootstrapOnStartup: false
535
+ }
536
+ : input.preset === 'fully-auto'
537
+ ? {
538
+ enforceBootstrap: true,
539
+ autoBootstrapOnRead: true,
540
+ autoBootstrapOnStartup: true
541
+ }
542
+ : {};
543
+ const policy = input.preset !== undefined ||
544
+ typeof input.enforceBootstrap === 'boolean' ||
545
+ typeof input.autoBootstrapOnRead === 'boolean' ||
546
+ typeof input.autoBootstrapOnStartup === 'boolean' ||
547
+ typeof input.staleAfterMinutes === 'number'
548
+ ? await setBootstrapPolicy({
549
+ ...presetPatch,
550
+ ...(typeof input.enforceBootstrap === 'boolean' ? { enforceBootstrap: input.enforceBootstrap } : {}),
551
+ ...(typeof input.autoBootstrapOnRead === 'boolean' ? { autoBootstrapOnRead: input.autoBootstrapOnRead } : {}),
552
+ ...(typeof input.autoBootstrapOnStartup === 'boolean' ? { autoBootstrapOnStartup: input.autoBootstrapOnStartup } : {}),
553
+ ...(typeof input.staleAfterMinutes === 'number' ? { staleAfterMinutes: input.staleAfterMinutes } : {})
554
+ })
555
+ : await getBootstrapPolicy();
556
+ const bootstrapStatus = await getBootstrapSessionStatus(context.vault, context.agent);
557
+ const nextActions = bootstrapStatus.ready
558
+ ? []
559
+ : [
560
+ {
561
+ tool: 'brainlink_bootstrap',
562
+ reason: 'Bootstrap status is not ready. Run bootstrap before using read tools.',
563
+ args: {
564
+ vault: context.vault,
565
+ ...(context.agent ? { agent: context.agent } : {}),
566
+ mode: context.defaults.defaultSearchMode
567
+ }
568
+ }
569
+ ];
570
+ return jsonResult(withNextActions({
571
+ vault: context.vault,
572
+ agent: context.agent,
573
+ policy,
574
+ bootstrapStatus,
575
+ ...(input.preset ? { presetApplied: input.preset } : {})
576
+ }, nextActions));
577
+ };
578
+ export const recommendationsTool = async (input) => {
579
+ const context = await resolveExecutionContext(input);
580
+ const policy = await getBootstrapPolicy();
581
+ const bootstrapStatus = await getBootstrapSessionStatus(context.vault, context.agent);
582
+ const stats = await getStats(context.vault, context.agent);
583
+ const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
584
+ const limit = input.limit ?? context.defaults.defaultSearchLimit;
585
+ const tokens = input.tokens ?? context.defaults.defaultContextTokens;
586
+ const query = input.query?.trim();
587
+ const recommendations = [
588
+ ...(policy.enforceBootstrap && (!policy.autoBootstrapOnRead || !policy.autoBootstrapOnStartup)
589
+ ? [
590
+ {
591
+ tool: 'brainlink_policy',
592
+ reason: 'Enable fully automatic bootstrap for plug-and-play agent usage.',
593
+ args: {
594
+ preset: 'fully-auto'
595
+ }
596
+ }
597
+ ]
598
+ : []),
599
+ ...(!bootstrapStatus.ready && !policy.autoBootstrapOnRead
600
+ ? [
601
+ {
602
+ tool: 'brainlink_bootstrap',
603
+ reason: 'Bootstrap is required before read tools when auto-bootstrap-on-read is disabled.',
604
+ args: {
605
+ vault: context.vault,
606
+ ...(context.agent ? { agent: context.agent } : {}),
607
+ mode,
608
+ ...(query ? { query } : {})
609
+ }
610
+ }
611
+ ]
612
+ : []),
613
+ ...(stats.documentCount === 0
614
+ ? [
615
+ {
616
+ tool: 'brainlink_add_note',
617
+ reason: 'Seed the vault with a first durable note so retrieval can return useful context.',
618
+ args: {
619
+ vault: context.vault,
620
+ ...(context.agent ? { agent: context.agent } : {}),
621
+ title: 'Architecture',
622
+ content: 'Seed durable memory with explicit [[links]] and #tags.'
623
+ }
624
+ },
625
+ {
626
+ tool: 'brainlink_index',
627
+ reason: 'Rebuild index after writing the first notes.',
628
+ args: {
629
+ vault: context.vault
630
+ }
631
+ }
632
+ ]
633
+ : []),
634
+ {
635
+ tool: 'brainlink_context',
636
+ reason: 'Retrieve grounded memory context before responding.',
637
+ args: {
638
+ vault: context.vault,
639
+ ...(context.agent ? { agent: context.agent } : {}),
640
+ query: query ?? '<task>',
641
+ mode,
642
+ limit,
643
+ tokens
644
+ }
645
+ },
646
+ {
647
+ tool: 'brainlink_add_note',
648
+ reason: 'Persist durable outcomes after task completion (write responses include connectivity metadata).',
649
+ args: {
650
+ vault: context.vault,
651
+ ...(context.agent ? { agent: context.agent } : {}),
652
+ title: 'Task Update',
653
+ content: 'Durable findings connected to [[existing notes]].'
654
+ }
655
+ }
656
+ ];
657
+ return jsonResult({
658
+ vault: context.vault,
659
+ agent: context.agent,
660
+ defaults: {
661
+ mode,
662
+ limit,
663
+ tokens
664
+ },
665
+ policy,
666
+ bootstrapStatus,
667
+ stats,
668
+ recommendations
669
+ });
670
+ };