@andespindola/brainlink 0.1.0-beta.8 → 0.1.0-beta.81
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 +58 -2
- package/CONTRIBUTING.md +2 -2
- package/COPYRIGHT.md +5 -0
- package/README.md +266 -20
- package/SECURITY.md +1 -1
- package/dist/application/add-note.js +62 -13
- package/dist/application/analyze-vault.js +95 -8
- package/dist/application/build-context.js +56 -1
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +138 -103
- package/dist/application/frontend/client-html.js +47 -41
- package/dist/application/frontend/client-js.js +2449 -156
- 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/migrate-vault.js +46 -16
- 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 +75 -5
- 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 +419 -0
- package/dist/cli/commands/config-commands.js +167 -0
- package/dist/cli/commands/read-commands.js +25 -8
- package/dist/cli/commands/write-commands.js +973 -10
- package/dist/cli/main.js +4 -0
- package/dist/cli/runtime.js +5 -2
- 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/markdown.js +36 -4
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +132 -8
- 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/paths.js +9 -1
- package/dist/infrastructure/private-pack-codec.js +134 -0
- package/dist/infrastructure/search-packs.js +452 -0
- package/dist/infrastructure/session-state.js +172 -0
- package/dist/mcp/main.js +11 -3
- package/dist/mcp/server.js +27 -2
- package/dist/mcp/startup.js +35 -0
- package/dist/mcp/tools.js +633 -19
- package/docs/AGENT_USAGE.md +177 -15
- package/docs/ARCHITECTURE.md +37 -26
- package/docs/QUICKSTART.md +111 -0
- 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
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 {
|
|
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:
|
|
67
|
-
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:
|
|
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:
|
|
124
|
-
contextTokens:
|
|
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
|
|
129
|
-
|
|
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
|
|
140
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
260
|
-
const
|
|
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
|
+
};
|