@andespindola/brainlink 0.1.0-beta.3 → 0.1.0-beta.31

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