@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +252 -19
  3. package/dist/application/add-note.js +62 -13
  4. package/dist/application/analyze-vault.js +104 -9
  5. package/dist/application/frontend/client-css.js +154 -71
  6. package/dist/application/frontend/client-html.js +42 -33
  7. package/dist/application/frontend/client-js.js +255 -70
  8. package/dist/application/get-graph-layout.js +6 -3
  9. package/dist/application/get-graph-node.js +12 -0
  10. package/dist/application/get-graph-summary.js +12 -0
  11. package/dist/application/migrate-vault.js +91 -0
  12. package/dist/application/search-graph-node-ids.js +12 -0
  13. package/dist/application/search-knowledge.js +56 -1
  14. package/dist/application/server/routes.js +27 -1
  15. package/dist/cli/commands/agent-commands.js +412 -0
  16. package/dist/cli/commands/config-commands.js +167 -0
  17. package/dist/cli/commands/read-commands.js +25 -8
  18. package/dist/cli/commands/write-commands.js +191 -7
  19. package/dist/cli/main.js +4 -0
  20. package/dist/cli/runtime.js +5 -2
  21. package/dist/domain/embeddings.js +2 -1
  22. package/dist/domain/graph-layout.js +20 -14
  23. package/dist/domain/markdown.js +36 -4
  24. package/dist/infrastructure/config.js +96 -8
  25. package/dist/infrastructure/file-system-vault.js +15 -0
  26. package/dist/infrastructure/paths.js +9 -1
  27. package/dist/infrastructure/session-state.js +172 -0
  28. package/dist/infrastructure/sqlite/graph-reader.js +252 -105
  29. package/dist/infrastructure/sqlite/recovery.js +83 -0
  30. package/dist/infrastructure/sqlite/schema.js +4 -1
  31. package/dist/infrastructure/sqlite/search-reader.js +104 -72
  32. package/dist/infrastructure/sqlite-index.js +16 -3
  33. package/dist/mcp/main.js +11 -3
  34. package/dist/mcp/server.js +22 -2
  35. package/dist/mcp/startup.js +35 -0
  36. package/dist/mcp/tools.js +617 -21
  37. package/docs/AGENT_USAGE.md +95 -6
  38. package/docs/ARCHITECTURE.md +15 -1
  39. package/docs/QUICKSTART.md +104 -0
  40. package/docs/RELEASE.md +3 -3
  41. 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 { addNote } from '../application/add-note.js';
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
- agent
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: positiveInteger(12).describe('Maximum search results before context selection.'),
55
- tokens: positiveInteger(2000).describe('Maximum estimated context 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: positiveInteger(10).describe('Maximum result count.')
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
- agent: z.string().min(1).optional().describe('Agent memory namespace.'),
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: positiveInteger(12).describe('Context smoke result limit when contextQuery is provided.'),
103
- contextTokens: positiveInteger(2000).describe('Context smoke token target when contextQuery is provided.')
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 mode = sanitizeSearchMode(input.mode, context.config.defaultSearchMode);
108
- const contextPackage = await buildContextPackage(context.vault, input.query, input.limit, input.tokens, context.agent, mode);
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 mode = sanitizeSearchMode(input.mode, context.config.defaultSearchMode);
119
- const results = await searchKnowledge(context.vault, input.query, input.limit, context.agent, mode);
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: input.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 path = await addNote(context.vault, input.title, input.content, context.agent, {
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
- index
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.config.defaultSearchMode);
216
- const contextPackage = await buildContextPackage(context.vault, input.contextQuery, input.contextLimit, input.contextTokens, context.agent, mode);
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
+ };