@andespindola/brainlink 0.1.0-beta.9 → 0.1.0-beta.91
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.
- package/AGENTS.md +8 -5
- package/CHANGELOG.md +26 -2
- package/CONTRIBUTING.md +2 -2
- package/COPYRIGHT.md +5 -0
- package/README.md +146 -17
- package/SECURITY.md +1 -1
- package/dist/application/analyze-vault.js +7 -7
- package/dist/application/build-context.js +56 -1
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +154 -102
- package/dist/application/frontend/client-html.js +49 -40
- package/dist/application/frontend/client-js.js +3118 -167
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-layout.js +18 -6
- package/dist/application/get-graph-node.js +12 -0
- package/dist/application/get-graph-summary.js +12 -0
- package/dist/application/get-graph.js +3 -3
- package/dist/application/import-legacy-sqlite.js +296 -0
- package/dist/application/index-vault.js +252 -19
- package/dist/application/list-agents.js +3 -3
- package/dist/application/list-links.js +5 -5
- package/dist/application/offline-pack-backup.js +44 -0
- package/dist/application/search-graph-node-ids.js +12 -0
- package/dist/application/search-knowledge.js +25 -10
- package/dist/application/server/routes.js +102 -1
- package/dist/application/start-server.js +75 -4
- package/dist/application/watch-vault.js +23 -2
- package/dist/benchmarks/large-vault.js +1 -1
- package/dist/cli/commands/agent-commands.js +20 -3
- package/dist/cli/commands/write-commands.js +818 -8
- package/dist/domain/context.js +53 -11
- package/dist/domain/embeddings.js +2 -1
- package/dist/domain/graph-layout.js +67 -16
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +38 -0
- package/dist/infrastructure/file-index.js +358 -0
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/index-state.js +56 -0
- package/dist/infrastructure/private-pack-codec.js +134 -0
- package/dist/infrastructure/search-packs.js +452 -0
- package/dist/infrastructure/session-state.js +57 -2
- package/dist/mcp/server.js +11 -1
- package/dist/mcp/tools.js +215 -3
- package/docs/AGENT_USAGE.md +103 -16
- package/docs/ARCHITECTURE.md +25 -26
- package/docs/QUICKSTART.md +9 -1
- package/package.json +6 -4
- package/dist/infrastructure/sqlite/document-writer.js +0 -51
- package/dist/infrastructure/sqlite/graph-reader.js +0 -120
- package/dist/infrastructure/sqlite/schema.js +0 -111
- package/dist/infrastructure/sqlite/search-reader.js +0 -156
- package/dist/infrastructure/sqlite/types.js +0 -1
- package/dist/infrastructure/sqlite-index.js +0 -25
|
@@ -3,13 +3,15 @@ import { dirname, join } from 'node:path';
|
|
|
3
3
|
import { getBrainlinkHomePath } from './paths.js';
|
|
4
4
|
const defaultPolicy = {
|
|
5
5
|
enforceBootstrap: true,
|
|
6
|
+
enforceContextFirst: true,
|
|
6
7
|
autoBootstrapOnRead: true,
|
|
7
8
|
autoBootstrapOnStartup: true,
|
|
8
9
|
staleAfterMinutes: 120
|
|
9
10
|
};
|
|
10
11
|
const defaultState = {
|
|
11
12
|
policy: defaultPolicy,
|
|
12
|
-
bootstraps: []
|
|
13
|
+
bootstraps: [],
|
|
14
|
+
contexts: []
|
|
13
15
|
};
|
|
14
16
|
const sessionStatePath = () => join(getBrainlinkHomePath(), 'session-state.json');
|
|
15
17
|
const normalizeAgent = (agent) => agent?.trim() ? agent.trim() : '*';
|
|
@@ -21,6 +23,7 @@ const sanitizeState = (value) => {
|
|
|
21
23
|
const record = value;
|
|
22
24
|
const policyRecord = typeof record.policy === 'object' && record.policy !== null ? record.policy : {};
|
|
23
25
|
const rawBootstraps = Array.isArray(record.bootstraps) ? record.bootstraps : [];
|
|
26
|
+
const rawContexts = Array.isArray(record.contexts) ? record.contexts : [];
|
|
24
27
|
const bootstraps = rawBootstraps.flatMap((entry) => {
|
|
25
28
|
if (typeof entry !== 'object' || entry === null) {
|
|
26
29
|
return [];
|
|
@@ -31,9 +34,22 @@ const sanitizeState = (value) => {
|
|
|
31
34
|
const lastBootstrappedAt = typeof row.lastBootstrappedAt === 'string' && row.lastBootstrappedAt.trim().length > 0 ? row.lastBootstrappedAt.trim() : undefined;
|
|
32
35
|
return vault && agent && lastBootstrappedAt ? [{ vault, agent, lastBootstrappedAt }] : [];
|
|
33
36
|
});
|
|
37
|
+
const contexts = rawContexts.flatMap((entry) => {
|
|
38
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const row = entry;
|
|
42
|
+
const vault = typeof row.vault === 'string' && row.vault.trim().length > 0 ? row.vault.trim() : undefined;
|
|
43
|
+
const agent = typeof row.agent === 'string' && row.agent.trim().length > 0 ? row.agent.trim() : undefined;
|
|
44
|
+
const lastContextAt = typeof row.lastContextAt === 'string' && row.lastContextAt.trim().length > 0 ? row.lastContextAt.trim() : undefined;
|
|
45
|
+
return vault && agent && lastContextAt ? [{ vault, agent, lastContextAt }] : [];
|
|
46
|
+
});
|
|
34
47
|
return {
|
|
35
48
|
policy: {
|
|
36
49
|
enforceBootstrap: typeof policyRecord.enforceBootstrap === 'boolean' ? policyRecord.enforceBootstrap : defaultPolicy.enforceBootstrap,
|
|
50
|
+
enforceContextFirst: typeof policyRecord.enforceContextFirst === 'boolean'
|
|
51
|
+
? policyRecord.enforceContextFirst
|
|
52
|
+
: defaultPolicy.enforceContextFirst,
|
|
37
53
|
autoBootstrapOnRead: typeof policyRecord.autoBootstrapOnRead === 'boolean'
|
|
38
54
|
? policyRecord.autoBootstrapOnRead
|
|
39
55
|
: defaultPolicy.autoBootstrapOnRead,
|
|
@@ -42,7 +58,8 @@ const sanitizeState = (value) => {
|
|
|
42
58
|
: defaultPolicy.autoBootstrapOnStartup,
|
|
43
59
|
staleAfterMinutes: safePositive(policyRecord.staleAfterMinutes, defaultPolicy.staleAfterMinutes)
|
|
44
60
|
},
|
|
45
|
-
bootstraps
|
|
61
|
+
bootstraps,
|
|
62
|
+
contexts
|
|
46
63
|
};
|
|
47
64
|
};
|
|
48
65
|
const readState = async () => {
|
|
@@ -68,6 +85,7 @@ export const setBootstrapPolicy = async (patch) => {
|
|
|
68
85
|
const state = await readState();
|
|
69
86
|
const next = {
|
|
70
87
|
enforceBootstrap: typeof patch.enforceBootstrap === 'boolean' ? patch.enforceBootstrap : state.policy.enforceBootstrap,
|
|
88
|
+
enforceContextFirst: typeof patch.enforceContextFirst === 'boolean' ? patch.enforceContextFirst : state.policy.enforceContextFirst,
|
|
71
89
|
autoBootstrapOnRead: typeof patch.autoBootstrapOnRead === 'boolean' ? patch.autoBootstrapOnRead : state.policy.autoBootstrapOnRead,
|
|
72
90
|
autoBootstrapOnStartup: typeof patch.autoBootstrapOnStartup === 'boolean' ? patch.autoBootstrapOnStartup : state.policy.autoBootstrapOnStartup,
|
|
73
91
|
staleAfterMinutes: safePositive(patch.staleAfterMinutes, state.policy.staleAfterMinutes)
|
|
@@ -96,6 +114,24 @@ export const touchBootstrapSession = async (vault, agent) => {
|
|
|
96
114
|
});
|
|
97
115
|
return entry;
|
|
98
116
|
};
|
|
117
|
+
export const touchContextSession = async (vault, agent) => {
|
|
118
|
+
const state = await readState();
|
|
119
|
+
const normalizedAgent = normalizeAgent(agent);
|
|
120
|
+
const entry = {
|
|
121
|
+
vault,
|
|
122
|
+
agent: normalizedAgent,
|
|
123
|
+
lastContextAt: new Date().toISOString()
|
|
124
|
+
};
|
|
125
|
+
const contexts = [
|
|
126
|
+
entry,
|
|
127
|
+
...state.contexts.filter((item) => !(item.vault === entry.vault && item.agent === entry.agent))
|
|
128
|
+
].slice(0, 500);
|
|
129
|
+
await writeState({
|
|
130
|
+
...state,
|
|
131
|
+
contexts
|
|
132
|
+
});
|
|
133
|
+
return entry;
|
|
134
|
+
};
|
|
99
135
|
export const getBootstrapSessionStatus = async (vault, agent) => {
|
|
100
136
|
const state = await readState();
|
|
101
137
|
const normalizedAgent = normalizeAgent(agent);
|
|
@@ -115,3 +151,22 @@ export const getBootstrapSessionStatus = async (vault, agent) => {
|
|
|
115
151
|
ageMinutes
|
|
116
152
|
};
|
|
117
153
|
};
|
|
154
|
+
export const getContextSessionStatus = async (vault, agent) => {
|
|
155
|
+
const state = await readState();
|
|
156
|
+
const normalizedAgent = normalizeAgent(agent);
|
|
157
|
+
const match = state.contexts.find((entry) => entry.vault === vault && entry.agent === normalizedAgent);
|
|
158
|
+
if (!match) {
|
|
159
|
+
return {
|
|
160
|
+
ready: false,
|
|
161
|
+
stale: true
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const ageMinutes = Math.max(0, (Date.now() - new Date(match.lastContextAt).getTime()) / 60000);
|
|
165
|
+
const stale = ageMinutes > state.policy.staleAfterMinutes;
|
|
166
|
+
return {
|
|
167
|
+
ready: !stale,
|
|
168
|
+
stale,
|
|
169
|
+
lastContextAt: match.lastContextAt,
|
|
170
|
+
ageMinutes
|
|
171
|
+
};
|
|
172
|
+
};
|
package/dist/mcp/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, contextInputSchema, contextTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool } from './tools.js';
|
|
5
|
+
import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, contextInputSchema, contextTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool } from './tools.js';
|
|
6
6
|
const readPackageVersion = () => {
|
|
7
7
|
const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
|
|
8
8
|
const metadata = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
@@ -40,6 +40,16 @@ export const createBrainlinkMcpServer = () => {
|
|
|
40
40
|
description: 'Search indexed Brainlink notes with FTS, semantic or hybrid retrieval.',
|
|
41
41
|
inputSchema: searchInputSchema
|
|
42
42
|
}, searchTool);
|
|
43
|
+
server.registerTool('brainlink_dedupe', {
|
|
44
|
+
title: 'Detect Duplicate Notes',
|
|
45
|
+
description: 'Detect possible duplicate notes using exact content hash and semantic similarity scoring.',
|
|
46
|
+
inputSchema: dedupeInputSchema
|
|
47
|
+
}, dedupeTool);
|
|
48
|
+
server.registerTool('brainlink_resolve_duplicate', {
|
|
49
|
+
title: 'Resolve Duplicate Notes',
|
|
50
|
+
description: 'Resolve a duplicate pair with merge, link or ignore. Non-merge actions still create low-priority related edges.',
|
|
51
|
+
inputSchema: dedupeResolveInputSchema
|
|
52
|
+
}, dedupeResolveTool);
|
|
43
53
|
server.registerTool('brainlink_add_note', {
|
|
44
54
|
title: 'Add Brainlink Note',
|
|
45
55
|
description: 'Write durable Markdown memory, then reindex the vault. Include explicit [[wiki links]] for connected graph memory. Add priority markers near links, such as priority: high, #important or #critical, when a relationship should be weighted higher.',
|
package/dist/mcp/tools.js
CHANGED
|
@@ -4,13 +4,14 @@ import { z } from 'zod';
|
|
|
4
4
|
import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../application/analyze-vault.js';
|
|
5
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
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';
|
|
13
|
-
import { getBootstrapPolicy, getBootstrapSessionStatus, setBootstrapPolicy, touchBootstrapSession } from '../infrastructure/session-state.js';
|
|
14
|
+
import { getBootstrapPolicy, getBootstrapSessionStatus, getContextSessionStatus, setBootstrapPolicy, touchBootstrapSession, touchContextSession } from '../infrastructure/session-state.js';
|
|
14
15
|
const positiveInteger = (fallback) => z
|
|
15
16
|
.number()
|
|
16
17
|
.int()
|
|
@@ -143,6 +144,73 @@ const ensureBootstrapReady = async (context, input, toolName) => {
|
|
|
143
144
|
}, nextActions))
|
|
144
145
|
};
|
|
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
|
+
};
|
|
146
214
|
export const contextInputSchema = {
|
|
147
215
|
...vaultInput,
|
|
148
216
|
...agentInput,
|
|
@@ -225,6 +293,7 @@ export const policyInputSchema = {
|
|
|
225
293
|
...agentInput,
|
|
226
294
|
preset: z.enum(['fully-auto', 'strict']).optional().describe('Apply an opinionated policy preset before explicit overrides.'),
|
|
227
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.'),
|
|
228
297
|
autoBootstrapOnRead: z
|
|
229
298
|
.boolean()
|
|
230
299
|
.optional()
|
|
@@ -243,6 +312,20 @@ export const recommendationsInputSchema = {
|
|
|
243
312
|
limit: optionalPositiveInteger().describe('Optional context limit override for generated recommendations.'),
|
|
244
313
|
tokens: optionalPositiveInteger().describe('Optional context token budget override for generated recommendations.')
|
|
245
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.')
|
|
328
|
+
};
|
|
246
329
|
export const contextTool = async (input) => {
|
|
247
330
|
const context = await resolveExecutionContext(input);
|
|
248
331
|
const readiness = await ensureBootstrapReady(context, input, 'brainlink_context');
|
|
@@ -253,12 +336,14 @@ export const contextTool = async (input) => {
|
|
|
253
336
|
const limit = input.limit ?? context.defaults.defaultSearchLimit;
|
|
254
337
|
const tokens = input.tokens ?? context.defaults.defaultContextTokens;
|
|
255
338
|
const contextPackage = await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode);
|
|
339
|
+
const contextSession = await touchContextSession(context.vault, context.agent);
|
|
256
340
|
return jsonResult({
|
|
257
341
|
vault: context.vault,
|
|
258
342
|
agent: context.agent,
|
|
259
343
|
mode,
|
|
260
344
|
limit,
|
|
261
345
|
tokens,
|
|
346
|
+
contextSession,
|
|
262
347
|
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
263
348
|
...contextPackage
|
|
264
349
|
});
|
|
@@ -269,6 +354,10 @@ export const searchTool = async (input) => {
|
|
|
269
354
|
if (readiness.preflight) {
|
|
270
355
|
return readiness.preflight;
|
|
271
356
|
}
|
|
357
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_search');
|
|
358
|
+
if (contextReadiness.preflight) {
|
|
359
|
+
return contextReadiness.preflight;
|
|
360
|
+
}
|
|
272
361
|
const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
|
|
273
362
|
const limit = input.limit ?? context.defaults.defaultSearchLimit;
|
|
274
363
|
const results = await searchKnowledge(context.vault, input.query, limit, context.agent, mode);
|
|
@@ -279,6 +368,7 @@ export const searchTool = async (input) => {
|
|
|
279
368
|
limit,
|
|
280
369
|
mode,
|
|
281
370
|
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
371
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
282
372
|
results
|
|
283
373
|
});
|
|
284
374
|
};
|
|
@@ -289,6 +379,14 @@ export const addNoteTool = async (input) => {
|
|
|
289
379
|
allowSensitive: input.allowSensitive
|
|
290
380
|
});
|
|
291
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
|
+
});
|
|
292
390
|
return jsonResult({
|
|
293
391
|
vault: context.vault,
|
|
294
392
|
title: input.title,
|
|
@@ -299,6 +397,7 @@ export const addNoteTool = async (input) => {
|
|
|
299
397
|
linkTarget: added.linkTarget,
|
|
300
398
|
guaranteedEdge: true
|
|
301
399
|
},
|
|
400
|
+
possibleDuplicates,
|
|
302
401
|
...(index ? { index } : {})
|
|
303
402
|
});
|
|
304
403
|
};
|
|
@@ -343,11 +442,16 @@ export const validateTool = async (input) => {
|
|
|
343
442
|
if (readiness.preflight) {
|
|
344
443
|
return readiness.preflight;
|
|
345
444
|
}
|
|
445
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_validate');
|
|
446
|
+
if (contextReadiness.preflight) {
|
|
447
|
+
return contextReadiness.preflight;
|
|
448
|
+
}
|
|
346
449
|
const validation = await validateVault(context.vault, context.agent);
|
|
347
450
|
return jsonResult({
|
|
348
451
|
vault: context.vault,
|
|
349
452
|
agent: context.agent,
|
|
350
453
|
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
454
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
351
455
|
...validation
|
|
352
456
|
});
|
|
353
457
|
};
|
|
@@ -357,11 +461,16 @@ export const graphTool = async (input) => {
|
|
|
357
461
|
if (readiness.preflight) {
|
|
358
462
|
return readiness.preflight;
|
|
359
463
|
}
|
|
464
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_graph');
|
|
465
|
+
if (contextReadiness.preflight) {
|
|
466
|
+
return contextReadiness.preflight;
|
|
467
|
+
}
|
|
360
468
|
const graph = await getGraph(context.vault, context.agent);
|
|
361
469
|
return jsonResult({
|
|
362
470
|
vault: context.vault,
|
|
363
471
|
agent: context.agent,
|
|
364
472
|
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
473
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
365
474
|
...graph
|
|
366
475
|
});
|
|
367
476
|
};
|
|
@@ -371,11 +480,16 @@ export const brokenLinksTool = async (input) => {
|
|
|
371
480
|
if (readiness.preflight) {
|
|
372
481
|
return readiness.preflight;
|
|
373
482
|
}
|
|
483
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_broken_links');
|
|
484
|
+
if (contextReadiness.preflight) {
|
|
485
|
+
return contextReadiness.preflight;
|
|
486
|
+
}
|
|
374
487
|
const brokenLinks = await getBrokenLinksReport(context.vault, context.agent);
|
|
375
488
|
return jsonResult({
|
|
376
489
|
vault: context.vault,
|
|
377
490
|
agent: context.agent,
|
|
378
491
|
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
492
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
379
493
|
brokenLinks
|
|
380
494
|
});
|
|
381
495
|
};
|
|
@@ -385,11 +499,16 @@ export const orphansTool = async (input) => {
|
|
|
385
499
|
if (readiness.preflight) {
|
|
386
500
|
return readiness.preflight;
|
|
387
501
|
}
|
|
502
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_orphans');
|
|
503
|
+
if (contextReadiness.preflight) {
|
|
504
|
+
return contextReadiness.preflight;
|
|
505
|
+
}
|
|
388
506
|
const orphans = await getOrphansReport(context.vault, context.agent);
|
|
389
507
|
return jsonResult({
|
|
390
508
|
vault: context.vault,
|
|
391
509
|
agent: context.agent,
|
|
392
510
|
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
511
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
393
512
|
orphans
|
|
394
513
|
});
|
|
395
514
|
};
|
|
@@ -399,11 +518,16 @@ export const statsTool = async (input) => {
|
|
|
399
518
|
if (readiness.preflight) {
|
|
400
519
|
return readiness.preflight;
|
|
401
520
|
}
|
|
521
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_stats');
|
|
522
|
+
if (contextReadiness.preflight) {
|
|
523
|
+
return contextReadiness.preflight;
|
|
524
|
+
}
|
|
402
525
|
const stats = await getStats(context.vault, context.agent);
|
|
403
526
|
return jsonResult({
|
|
404
527
|
vault: context.vault,
|
|
405
528
|
agent: context.agent,
|
|
406
529
|
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
530
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
407
531
|
stats
|
|
408
532
|
});
|
|
409
533
|
};
|
|
@@ -413,6 +537,10 @@ export const syncTool = async (input) => {
|
|
|
413
537
|
if (readiness.preflight) {
|
|
414
538
|
return readiness.preflight;
|
|
415
539
|
}
|
|
540
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_sync');
|
|
541
|
+
if (contextReadiness.preflight) {
|
|
542
|
+
return contextReadiness.preflight;
|
|
543
|
+
}
|
|
416
544
|
const index = await indexVault(context.vault);
|
|
417
545
|
const stats = await getStats(context.vault, context.agent);
|
|
418
546
|
const validation = await validateVault(context.vault, context.agent);
|
|
@@ -422,6 +550,7 @@ export const syncTool = async (input) => {
|
|
|
422
550
|
vault: context.vault,
|
|
423
551
|
agent: context.agent,
|
|
424
552
|
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
553
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
425
554
|
index,
|
|
426
555
|
stats,
|
|
427
556
|
validation,
|
|
@@ -435,10 +564,12 @@ export const syncTool = async (input) => {
|
|
|
435
564
|
const contextLimit = input.contextLimit ?? context.defaults.defaultSearchLimit;
|
|
436
565
|
const contextTokens = input.contextTokens ?? context.defaults.defaultContextTokens;
|
|
437
566
|
const contextPackage = await buildContextPackage(context.vault, input.contextQuery, contextLimit, contextTokens, context.agent, mode);
|
|
567
|
+
const contextSession = await touchContextSession(context.vault, context.agent);
|
|
438
568
|
return jsonResult({
|
|
439
569
|
...response,
|
|
440
570
|
context: {
|
|
441
571
|
mode,
|
|
572
|
+
contextSession,
|
|
442
573
|
...contextPackage
|
|
443
574
|
}
|
|
444
575
|
});
|
|
@@ -454,6 +585,7 @@ export const bootstrapTool = async (input) => {
|
|
|
454
585
|
const contextPackage = input.query
|
|
455
586
|
? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode)
|
|
456
587
|
: undefined;
|
|
588
|
+
const contextSession = input.query ? await touchContextSession(context.vault, context.agent) : undefined;
|
|
457
589
|
const guidance = stats.documentCount === 0
|
|
458
590
|
? 'Vault indexed with zero documents. Add durable notes with brainlink_add_note, then run brainlink_bootstrap again.'
|
|
459
591
|
: input.query
|
|
@@ -522,7 +654,8 @@ export const bootstrapTool = async (input) => {
|
|
|
522
654
|
policy,
|
|
523
655
|
session,
|
|
524
656
|
guidance,
|
|
525
|
-
...(contextPackage ? { context: contextPackage } : {})
|
|
657
|
+
...(contextPackage ? { context: contextPackage } : {}),
|
|
658
|
+
...(contextSession ? { contextSession } : {})
|
|
526
659
|
}, nextActions));
|
|
527
660
|
};
|
|
528
661
|
export const policyTool = async (input) => {
|
|
@@ -530,30 +663,35 @@ export const policyTool = async (input) => {
|
|
|
530
663
|
const presetPatch = input.preset === 'strict'
|
|
531
664
|
? {
|
|
532
665
|
enforceBootstrap: true,
|
|
666
|
+
enforceContextFirst: true,
|
|
533
667
|
autoBootstrapOnRead: false,
|
|
534
668
|
autoBootstrapOnStartup: false
|
|
535
669
|
}
|
|
536
670
|
: input.preset === 'fully-auto'
|
|
537
671
|
? {
|
|
538
672
|
enforceBootstrap: true,
|
|
673
|
+
enforceContextFirst: true,
|
|
539
674
|
autoBootstrapOnRead: true,
|
|
540
675
|
autoBootstrapOnStartup: true
|
|
541
676
|
}
|
|
542
677
|
: {};
|
|
543
678
|
const policy = input.preset !== undefined ||
|
|
544
679
|
typeof input.enforceBootstrap === 'boolean' ||
|
|
680
|
+
typeof input.enforceContextFirst === 'boolean' ||
|
|
545
681
|
typeof input.autoBootstrapOnRead === 'boolean' ||
|
|
546
682
|
typeof input.autoBootstrapOnStartup === 'boolean' ||
|
|
547
683
|
typeof input.staleAfterMinutes === 'number'
|
|
548
684
|
? await setBootstrapPolicy({
|
|
549
685
|
...presetPatch,
|
|
550
686
|
...(typeof input.enforceBootstrap === 'boolean' ? { enforceBootstrap: input.enforceBootstrap } : {}),
|
|
687
|
+
...(typeof input.enforceContextFirst === 'boolean' ? { enforceContextFirst: input.enforceContextFirst } : {}),
|
|
551
688
|
...(typeof input.autoBootstrapOnRead === 'boolean' ? { autoBootstrapOnRead: input.autoBootstrapOnRead } : {}),
|
|
552
689
|
...(typeof input.autoBootstrapOnStartup === 'boolean' ? { autoBootstrapOnStartup: input.autoBootstrapOnStartup } : {}),
|
|
553
690
|
...(typeof input.staleAfterMinutes === 'number' ? { staleAfterMinutes: input.staleAfterMinutes } : {})
|
|
554
691
|
})
|
|
555
692
|
: await getBootstrapPolicy();
|
|
556
693
|
const bootstrapStatus = await getBootstrapSessionStatus(context.vault, context.agent);
|
|
694
|
+
const contextStatus = await getContextSessionStatus(context.vault, context.agent);
|
|
557
695
|
const nextActions = bootstrapStatus.ready
|
|
558
696
|
? []
|
|
559
697
|
: [
|
|
@@ -567,18 +705,37 @@ export const policyTool = async (input) => {
|
|
|
567
705
|
}
|
|
568
706
|
}
|
|
569
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;
|
|
570
725
|
return jsonResult(withNextActions({
|
|
571
726
|
vault: context.vault,
|
|
572
727
|
agent: context.agent,
|
|
573
728
|
policy,
|
|
574
729
|
bootstrapStatus,
|
|
730
|
+
contextStatus,
|
|
575
731
|
...(input.preset ? { presetApplied: input.preset } : {})
|
|
576
|
-
},
|
|
732
|
+
}, withContextAction));
|
|
577
733
|
};
|
|
578
734
|
export const recommendationsTool = async (input) => {
|
|
579
735
|
const context = await resolveExecutionContext(input);
|
|
580
736
|
const policy = await getBootstrapPolicy();
|
|
581
737
|
const bootstrapStatus = await getBootstrapSessionStatus(context.vault, context.agent);
|
|
738
|
+
const contextStatus = await getContextSessionStatus(context.vault, context.agent);
|
|
582
739
|
const stats = await getStats(context.vault, context.agent);
|
|
583
740
|
const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
|
|
584
741
|
const limit = input.limit ?? context.defaults.defaultSearchLimit;
|
|
@@ -610,6 +767,22 @@ export const recommendationsTool = async (input) => {
|
|
|
610
767
|
}
|
|
611
768
|
]
|
|
612
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
|
+
: []),
|
|
613
786
|
...(stats.documentCount === 0
|
|
614
787
|
? [
|
|
615
788
|
{
|
|
@@ -643,6 +816,17 @@ export const recommendationsTool = async (input) => {
|
|
|
643
816
|
tokens
|
|
644
817
|
}
|
|
645
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
|
+
},
|
|
646
830
|
{
|
|
647
831
|
tool: 'brainlink_add_note',
|
|
648
832
|
reason: 'Persist durable outcomes after task completion (write responses include connectivity metadata).',
|
|
@@ -664,7 +848,35 @@ export const recommendationsTool = async (input) => {
|
|
|
664
848
|
},
|
|
665
849
|
policy,
|
|
666
850
|
bootstrapStatus,
|
|
851
|
+
contextStatus,
|
|
667
852
|
stats,
|
|
668
853
|
recommendations
|
|
669
854
|
});
|
|
670
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
|
+
};
|