@andespindola/brainlink 1.0.4 → 1.0.6

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 (53) hide show
  1. package/README.md +17 -9
  2. package/dist/application/add-note.js +2 -2
  3. package/dist/application/build-context.js +16 -10
  4. package/dist/application/canonical-context-links.js +44 -5
  5. package/dist/application/check-package-update.js +105 -0
  6. package/dist/application/frontend/client/chunk-fetch.js +236 -0
  7. package/dist/application/frontend/client/controls.js +178 -0
  8. package/dist/application/frontend/client/elements.js +122 -0
  9. package/dist/application/frontend/client/input.js +202 -0
  10. package/dist/application/frontend/client/node-details.js +191 -0
  11. package/dist/application/frontend/client/rendering.js +296 -0
  12. package/dist/application/frontend/client/scope-theme.js +114 -0
  13. package/dist/application/frontend/client/spatial.js +98 -0
  14. package/dist/application/frontend/client/storage.js +215 -0
  15. package/dist/application/frontend/client/upload.js +90 -0
  16. package/dist/application/frontend/client/worker-bootstrap.js +147 -0
  17. package/dist/application/frontend/client-js.js +24 -1837
  18. package/dist/application/frontend/client-render-worker-js.js +1 -1
  19. package/dist/application/index-vault-phases.js +189 -0
  20. package/dist/application/index-vault.js +44 -165
  21. package/dist/application/server/routes.js +12 -9
  22. package/dist/cli/commands/write/dedupe-commands.js +59 -0
  23. package/dist/cli/commands/write/index-commands.js +205 -0
  24. package/dist/cli/commands/write/link-commands.js +68 -0
  25. package/dist/cli/commands/write/note-commands.js +146 -0
  26. package/dist/cli/commands/write/server-commands.js +553 -0
  27. package/dist/cli/commands/write/shared.js +35 -0
  28. package/dist/cli/commands/write/vault-lifecycle-commands.js +270 -0
  29. package/dist/cli/commands/write-commands.js +12 -1303
  30. package/dist/cli/main.js +39 -3
  31. package/dist/domain/context.js +39 -3
  32. package/dist/domain/embeddings.js +31 -5
  33. package/dist/domain/graph-contexts.js +62 -57
  34. package/dist/domain/graph-layout/cauliflower-layout.js +116 -0
  35. package/dist/domain/graph-layout/collisions.js +100 -0
  36. package/dist/domain/graph-layout/hierarchy.js +135 -0
  37. package/dist/domain/graph-layout/metrics.js +111 -0
  38. package/dist/domain/graph-layout/segments.js +76 -0
  39. package/dist/domain/graph-layout/star-layout.js +110 -0
  40. package/dist/domain/graph-layout.js +4 -625
  41. package/dist/infrastructure/config.js +10 -4
  42. package/dist/infrastructure/file-index.js +13 -4
  43. package/dist/infrastructure/semantic-prefilter.js +24 -0
  44. package/dist/mcp/server.js +7 -0
  45. package/dist/mcp/tool-guard.js +29 -0
  46. package/dist/mcp/tools/maintenance-tools.js +409 -0
  47. package/dist/mcp/tools/read-tools.js +504 -0
  48. package/dist/mcp/tools/shared.js +216 -0
  49. package/dist/mcp/tools/write-tools.js +247 -0
  50. package/dist/mcp/tools.js +3 -1357
  51. package/docs/AGENT_USAGE.md +4 -4
  52. package/docs/QUICKSTART.md +5 -1
  53. package/package.json +2 -2
@@ -0,0 +1,216 @@
1
+ import { basename, extname } from 'node:path';
2
+ import { z } from 'zod';
3
+ import { resolveAgentRuntimeDefaults, sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
4
+ import { loadBrainlinkConfig } from '../../infrastructure/config.js';
5
+ import { assertVaultAllowed } from '../../infrastructure/file-system-vault.js';
6
+ import { indexVault } from '../../application/index-vault.js';
7
+ import { getBootstrapPolicy, getBootstrapSessionStatus, getContextSessionStatus, touchBootstrapSession } from '../../infrastructure/session-state.js';
8
+ export const positiveInteger = (fallback) => z
9
+ .number()
10
+ .int()
11
+ .positive()
12
+ .optional()
13
+ .transform((value) => value ?? fallback);
14
+ export const optionalPositiveInteger = () => z
15
+ .number()
16
+ .int()
17
+ .positive()
18
+ .optional();
19
+ export const vaultInput = {
20
+ vault: z.string().min(1).optional().describe('Vault directory. Omit to use the configured Brainlink default vault.')
21
+ };
22
+ export const agentInput = {
23
+ agent: z
24
+ .string()
25
+ .min(1)
26
+ .optional()
27
+ .describe('Agent memory namespace. Omit to use Brainlink.config defaultAgent, otherwise read all agent namespaces.')
28
+ };
29
+ export const searchModeInput = {
30
+ mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode. Defaults to the Brainlink config value.')
31
+ };
32
+ export const contextStrategyInput = {
33
+ strategy: z
34
+ .enum(['rag', 'cag', 'auto'])
35
+ .optional()
36
+ .describe('Context strategy per call. Use rag for fresh retrieval assembly, cag to reuse persisted context packs when fresh, or auto to choose CAG on fresh pack hits and RAG otherwise. Defaults to the Brainlink config value.')
37
+ };
38
+ export const resolveExecutionContext = async (input) => {
39
+ const config = await loadBrainlinkConfig();
40
+ const vault = await assertVaultAllowed(input.vault ?? config.vault, config.allowedVaults);
41
+ const agent = input.agent ?? config.defaultAgent;
42
+ const defaults = resolveAgentRuntimeDefaults(config, agent);
43
+ return {
44
+ config,
45
+ vault,
46
+ agent,
47
+ defaults
48
+ };
49
+ };
50
+ export 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
+ export const isTruthy = (value) => value !== false;
60
+ export const jsonResult = (value) => ({
61
+ content: [
62
+ {
63
+ type: 'text',
64
+ text: JSON.stringify(value, null, 2)
65
+ }
66
+ ],
67
+ structuredContent: value
68
+ });
69
+ export const preflightResult = (value) => jsonResult({
70
+ preflightRequired: true,
71
+ ...value
72
+ });
73
+ export const withNextActions = (value, nextActions) => ({
74
+ ...value,
75
+ nextActions
76
+ });
77
+ export 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 strategy = sanitizeContextStrategy(typeof input.strategy === 'string' ? input.strategy : undefined, 'rag');
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
+ strategy
128
+ };
129
+ const nextActions = [
130
+ {
131
+ tool: 'brainlink_bootstrap',
132
+ reason: 'Bootstrap is required before read tools so retrieval stays grounded in current vault state.',
133
+ args: bootstrapArgs
134
+ }
135
+ ];
136
+ return {
137
+ preflight: preflightResult(withNextActions({
138
+ vault: context.vault,
139
+ agent: context.agent,
140
+ blockedTool: toolName,
141
+ policy,
142
+ bootstrapStatus: status,
143
+ guidance: 'Run brainlink_bootstrap first for this vault/agent before using read tools. This keeps retrieval grounded and memory state consistent.',
144
+ bootstrapArgs
145
+ }, nextActions))
146
+ };
147
+ };
148
+ export const ensureContextReady = async (context, input, toolName) => {
149
+ const policy = await getBootstrapPolicy();
150
+ if (!policy.enforceContextFirst) {
151
+ return {
152
+ context: {
153
+ policy,
154
+ statusBefore: {
155
+ ready: true,
156
+ stale: false
157
+ },
158
+ reason: 'Context-first enforcement is disabled by policy.'
159
+ }
160
+ };
161
+ }
162
+ const status = await getContextSessionStatus(context.vault, context.agent);
163
+ if (status.ready) {
164
+ return {
165
+ context: {
166
+ policy,
167
+ statusBefore: status,
168
+ reason: 'Context session is already fresh for this vault/agent.'
169
+ }
170
+ };
171
+ }
172
+ const queryFromInput = typeof input.query === 'string' && input.query.trim().length > 0
173
+ ? input.query
174
+ : typeof input.contextQuery === 'string' && input.contextQuery.trim().length > 0
175
+ ? input.contextQuery
176
+ : '<task>';
177
+ const mode = sanitizeSearchMode(typeof input.mode === 'string' ? input.mode : undefined, context.defaults.defaultSearchMode);
178
+ const strategy = sanitizeContextStrategy(typeof input.strategy === 'string' ? input.strategy : undefined, context.defaults.defaultContextStrategy);
179
+ const limit = typeof input.limit === 'number' && Number.isFinite(input.limit) && input.limit > 0
180
+ ? input.limit
181
+ : typeof input.contextLimit === 'number' && Number.isFinite(input.contextLimit) && input.contextLimit > 0
182
+ ? input.contextLimit
183
+ : context.defaults.defaultSearchLimit;
184
+ const tokens = typeof input.tokens === 'number' && Number.isFinite(input.tokens) && input.tokens > 0
185
+ ? input.tokens
186
+ : typeof input.contextTokens === 'number' && Number.isFinite(input.contextTokens) && input.contextTokens > 0
187
+ ? input.contextTokens
188
+ : context.defaults.defaultContextTokens;
189
+ const contextArgs = {
190
+ vault: context.vault,
191
+ ...(context.agent ? { agent: context.agent } : {}),
192
+ query: queryFromInput,
193
+ mode,
194
+ strategy,
195
+ limit,
196
+ tokens
197
+ };
198
+ const nextActions = [
199
+ {
200
+ tool: 'brainlink_context',
201
+ reason: 'Context must be loaded first so Brainlink is the primary retrieval source before other read tools.',
202
+ args: contextArgs
203
+ }
204
+ ];
205
+ return {
206
+ preflight: preflightResult(withNextActions({
207
+ vault: context.vault,
208
+ agent: context.agent,
209
+ blockedTool: toolName,
210
+ policy,
211
+ contextStatus: status,
212
+ guidance: 'Run brainlink_context first for this vault/agent before other read tools so answers are grounded on Brainlink context.',
213
+ contextArgs
214
+ }, nextActions))
215
+ };
216
+ };
@@ -0,0 +1,247 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { z } from 'zod';
3
+ import { addNoteWithMetadata } from '../../application/add-note.js';
4
+ import { deleteNote } from '../../application/delete-note.js';
5
+ import { scanDuplicateNotes } from '../../application/dedupe-notes.js';
6
+ import { addInboxItem, listInboxItems, processInboxItems } from '../../application/inbox.js';
7
+ import { indexVault } from '../../application/index-vault.js';
8
+ import { buildRememberSuggestion } from '../../application/memory-suggestions.js';
9
+ import { addVolatileMemory, clearVolatileMemory } from '../../infrastructure/volatile-memory.js';
10
+ import { agentInput, inferTitleFromPath, isTruthy, jsonResult, optionalPositiveInteger, positiveInteger, resolveExecutionContext, vaultInput } from './shared.js';
11
+ export const addNoteInputSchema = {
12
+ ...vaultInput,
13
+ title: z.string().min(1).describe('Markdown note title.'),
14
+ content: z
15
+ .string()
16
+ .min(1)
17
+ .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.'),
18
+ ...agentInput,
19
+ allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.'),
20
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.'),
21
+ autoContextLinks: z
22
+ .boolean()
23
+ .optional()
24
+ .describe('Automatically add canonical Context Links to the inferred visual context hub. Defaults to Brainlink config.')
25
+ };
26
+ export const rememberInputSchema = {
27
+ ...vaultInput,
28
+ ...agentInput,
29
+ title: z.string().min(1).optional().describe('Optional note title. When omitted, Brainlink infers it from content.'),
30
+ content: z.string().min(1).describe('Memory content to capture as a durable Markdown note.'),
31
+ tags: z.array(z.string()).optional().default([]).describe('Extra tags to include.'),
32
+ links: z.array(z.string()).optional().default([]).describe('Explicit Context Links to include.'),
33
+ linkLimit: positiveInteger(5).describe('Maximum suggested Context Links to include.'),
34
+ dryRun: z.boolean().optional().default(false).describe('Preview inferred note without writing it.'),
35
+ allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.'),
36
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.')
37
+ };
38
+ export const inboxAddInputSchema = {
39
+ ...vaultInput,
40
+ ...agentInput,
41
+ content: z.string().min(1).describe('Quick memory content to store as an untriaged inbox note.'),
42
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing inbox item.')
43
+ };
44
+ export const inboxListInputSchema = {
45
+ ...vaultInput,
46
+ ...agentInput,
47
+ limit: positiveInteger(20).describe('Maximum inbox items to return.')
48
+ };
49
+ export const inboxProcessInputSchema = {
50
+ ...vaultInput,
51
+ ...agentInput,
52
+ limit: positiveInteger(10).describe('Maximum inbox items to inspect.')
53
+ };
54
+ export const deleteNoteInputSchema = {
55
+ ...vaultInput,
56
+ ...agentInput,
57
+ title: z.string().min(1).optional().describe('Note title to delete. Use agent to disambiguate namespaced notes.'),
58
+ path: z.string().min(1).optional().describe('Vault-relative or absolute Markdown note path to delete.'),
59
+ confirm: z.boolean().describe('Must be true to confirm deletion.'),
60
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after deletion. Defaults to true.')
61
+ };
62
+ export const volatileAddInputSchema = {
63
+ ...vaultInput,
64
+ ...agentInput,
65
+ content: z
66
+ .string()
67
+ .min(1)
68
+ .describe('Temporary agent-decided memory. Use for current task state, hypotheses, transient user preferences and unconfirmed findings.'),
69
+ ttlMinutes: optionalPositiveInteger().describe('Minutes before this volatile memory expires. Defaults to 240.'),
70
+ tags: z.array(z.string()).optional().default([]).describe('Optional tags for volatile retrieval.')
71
+ };
72
+ export const volatileClearInputSchema = {
73
+ ...vaultInput,
74
+ ...agentInput
75
+ };
76
+ export const addFileInputSchema = {
77
+ ...vaultInput,
78
+ ...agentInput,
79
+ title: z.string().min(1).optional().describe('Optional note title override. If omitted, uses file name.'),
80
+ filePath: z.string().min(1).describe('Filesystem path to markdown or text file to ingest.'),
81
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after ingesting file.'),
82
+ allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.')
83
+ };
84
+ export const addNoteTool = async (input) => {
85
+ const context = await resolveExecutionContext(input);
86
+ const shouldIndex = isTruthy(input.autoIndex);
87
+ const added = await addNoteWithMetadata(context.vault, input.title, input.content, context.agent, {
88
+ allowSensitive: input.allowSensitive,
89
+ autoContextLinks: input.autoContextLinks ?? context.config.autoCanonicalContextLinks
90
+ });
91
+ const index = shouldIndex ? await indexVault(context.vault) : undefined;
92
+ const focusPath = added.path.includes('agents/') ? added.path.slice(added.path.indexOf('agents/')).replaceAll('\\', '/') : undefined;
93
+ const possibleDuplicates = await scanDuplicateNotes(context.vault, {
94
+ agentId: context.agent,
95
+ focusPath,
96
+ limit: 5,
97
+ minSemanticScore: 0.92,
98
+ includeSemantic: true
99
+ });
100
+ return jsonResult({
101
+ vault: context.vault,
102
+ title: input.title,
103
+ agent: context.agent,
104
+ path: added.path,
105
+ writeConnectivity: {
106
+ autoLinked: added.autoLinked,
107
+ linkTarget: added.linkTarget,
108
+ context: added.context,
109
+ hubCreated: added.hubCreated,
110
+ guaranteedEdge: added.autoLinked
111
+ },
112
+ possibleDuplicates,
113
+ ...(index ? { index } : {})
114
+ });
115
+ };
116
+ export const rememberTool = async (input) => {
117
+ const context = await resolveExecutionContext(input);
118
+ const suggestion = await buildRememberSuggestion({
119
+ vaultPath: context.vault,
120
+ content: input.content,
121
+ agentId: context.agent,
122
+ title: input.title,
123
+ tags: input.tags,
124
+ links: input.links,
125
+ linkLimit: input.linkLimit
126
+ });
127
+ if (input.dryRun) {
128
+ return jsonResult({
129
+ dryRun: true,
130
+ vault: context.vault,
131
+ agent: context.agent,
132
+ suggestion
133
+ });
134
+ }
135
+ const added = await addNoteWithMetadata(context.vault, suggestion.title, suggestion.content, context.agent, {
136
+ allowSensitive: input.allowSensitive,
137
+ autoContextLinks: false
138
+ });
139
+ const index = input.autoIndex ? await indexVault(context.vault) : undefined;
140
+ return jsonResult({
141
+ dryRun: false,
142
+ vault: context.vault,
143
+ agent: context.agent,
144
+ suggestion,
145
+ path: added.path,
146
+ ...(index ? { index } : {})
147
+ });
148
+ };
149
+ export const inboxAddTool = async (input) => {
150
+ const context = await resolveExecutionContext(input);
151
+ const result = await addInboxItem({
152
+ vaultPath: context.vault,
153
+ content: input.content,
154
+ agentId: context.agent,
155
+ autoIndex: input.autoIndex
156
+ });
157
+ return jsonResult({
158
+ vault: context.vault,
159
+ agent: context.agent,
160
+ ...result
161
+ });
162
+ };
163
+ export const inboxListTool = async (input) => {
164
+ const context = await resolveExecutionContext(input);
165
+ const items = await listInboxItems(context.vault, input.limit);
166
+ return jsonResult({
167
+ vault: context.vault,
168
+ agent: context.agent,
169
+ items
170
+ });
171
+ };
172
+ export const inboxProcessTool = async (input) => {
173
+ const context = await resolveExecutionContext(input);
174
+ const items = await processInboxItems({
175
+ vaultPath: context.vault,
176
+ agentId: context.agent,
177
+ limit: input.limit
178
+ });
179
+ return jsonResult({
180
+ vault: context.vault,
181
+ agent: context.agent,
182
+ items
183
+ });
184
+ };
185
+ export const deleteNoteTool = async (input) => {
186
+ const context = await resolveExecutionContext(input);
187
+ const result = await deleteNote(context.vault, {
188
+ title: input.title,
189
+ path: input.path,
190
+ agentId: context.agent,
191
+ confirm: input.confirm,
192
+ autoIndex: input.autoIndex
193
+ });
194
+ return jsonResult({
195
+ vault: context.vault,
196
+ ...result
197
+ });
198
+ };
199
+ export const volatileAddTool = async (input) => {
200
+ const context = await resolveExecutionContext(input);
201
+ const entry = await addVolatileMemory(context.vault, input.content, context.agent ?? 'shared', input.ttlMinutes ?? 240, input.tags);
202
+ return jsonResult({
203
+ vault: context.vault,
204
+ agent: context.agent,
205
+ volatile: true,
206
+ entry
207
+ });
208
+ };
209
+ export const volatileClearTool = async (input) => {
210
+ const context = await resolveExecutionContext(input);
211
+ const cleared = await clearVolatileMemory(context.vault, context.agent);
212
+ return jsonResult({
213
+ vault: context.vault,
214
+ agent: context.agent,
215
+ cleared
216
+ });
217
+ };
218
+ export const addFileTool = async (input) => {
219
+ const context = await resolveExecutionContext(input);
220
+ const content = await readFile(input.filePath, 'utf8');
221
+ const inferredTitle = inferTitleFromPath(input.filePath);
222
+ const title = input.title ?? inferredTitle;
223
+ if (title == null || title.length === 0) {
224
+ throw new Error('Cannot infer note title from file path. Provide a title explicitly.');
225
+ }
226
+ const shouldIndex = isTruthy(input.autoIndex);
227
+ const added = await addNoteWithMetadata(context.vault, title, content, context.agent, {
228
+ allowSensitive: input.allowSensitive,
229
+ autoContextLinks: context.config.autoCanonicalContextLinks
230
+ });
231
+ const index = shouldIndex ? await indexVault(context.vault) : undefined;
232
+ return jsonResult({
233
+ vault: context.vault,
234
+ title,
235
+ agent: context.agent,
236
+ filePath: input.filePath,
237
+ path: added.path,
238
+ writeConnectivity: {
239
+ autoLinked: added.autoLinked,
240
+ linkTarget: added.linkTarget,
241
+ context: added.context,
242
+ hubCreated: added.hubCreated,
243
+ guaranteedEdge: added.autoLinked
244
+ },
245
+ ...(index ? { index } : {})
246
+ });
247
+ };