@andespindola/brainlink 0.1.0-beta.6 → 0.1.0-beta.61

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