@andespindola/brainlink 1.0.5 → 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.
- package/README.md +8 -0
- package/dist/application/add-note.js +2 -2
- package/dist/application/build-context.js +16 -10
- package/dist/application/canonical-context-links.js +44 -5
- package/dist/application/check-package-update.js +105 -0
- package/dist/application/frontend/client/chunk-fetch.js +236 -0
- package/dist/application/frontend/client/controls.js +178 -0
- package/dist/application/frontend/client/elements.js +122 -0
- package/dist/application/frontend/client/input.js +202 -0
- package/dist/application/frontend/client/node-details.js +191 -0
- package/dist/application/frontend/client/rendering.js +296 -0
- package/dist/application/frontend/client/scope-theme.js +114 -0
- package/dist/application/frontend/client/spatial.js +98 -0
- package/dist/application/frontend/client/storage.js +215 -0
- package/dist/application/frontend/client/upload.js +90 -0
- package/dist/application/frontend/client/worker-bootstrap.js +147 -0
- package/dist/application/frontend/client-js.js +24 -1837
- package/dist/application/frontend/client-render-worker-js.js +1 -1
- package/dist/application/index-vault-phases.js +189 -0
- package/dist/application/index-vault.js +44 -165
- package/dist/cli/commands/write/dedupe-commands.js +59 -0
- package/dist/cli/commands/write/index-commands.js +205 -0
- package/dist/cli/commands/write/link-commands.js +68 -0
- package/dist/cli/commands/write/note-commands.js +146 -0
- package/dist/cli/commands/write/server-commands.js +553 -0
- package/dist/cli/commands/write/shared.js +35 -0
- package/dist/cli/commands/write/vault-lifecycle-commands.js +270 -0
- package/dist/cli/commands/write-commands.js +12 -1303
- package/dist/cli/main.js +39 -3
- package/dist/domain/context.js +39 -3
- package/dist/domain/embeddings.js +31 -5
- package/dist/domain/graph-contexts.js +62 -57
- package/dist/domain/graph-layout/cauliflower-layout.js +116 -0
- package/dist/domain/graph-layout/collisions.js +100 -0
- package/dist/domain/graph-layout/hierarchy.js +135 -0
- package/dist/domain/graph-layout/metrics.js +111 -0
- package/dist/domain/graph-layout/segments.js +76 -0
- package/dist/domain/graph-layout/star-layout.js +110 -0
- package/dist/domain/graph-layout.js +4 -625
- package/dist/infrastructure/config.js +6 -0
- package/dist/infrastructure/file-index.js +13 -4
- package/dist/infrastructure/semantic-prefilter.js +24 -0
- package/dist/mcp/server.js +7 -0
- package/dist/mcp/tool-guard.js +29 -0
- package/dist/mcp/tools/maintenance-tools.js +409 -0
- package/dist/mcp/tools/read-tools.js +504 -0
- package/dist/mcp/tools/shared.js +216 -0
- package/dist/mcp/tools/write-tools.js +247 -0
- package/dist/mcp/tools.js +3 -1357
- package/docs/QUICKSTART.md +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Below this chunk count a full scan is already sub-millisecond, so the
|
|
2
|
+
// prefilter only engages on large vaults where pruning candidates pays off.
|
|
3
|
+
export const semanticPrefilterMinChunks = 4000;
|
|
4
|
+
// Pure-semantic candidate prefilter. Returns the subset of rows that share at
|
|
5
|
+
// least one embedding bucket with the query, or ALL rows when the prefilter
|
|
6
|
+
// should not or cannot safely engage:
|
|
7
|
+
// - the index is small (full scan is cheap),
|
|
8
|
+
// - the query produced no buckets,
|
|
9
|
+
// - any row is missing buckets (a pre-feature / partially indexed vault), or
|
|
10
|
+
// - too few candidates survive (protect recall — fall back to a full scan).
|
|
11
|
+
// It is only sound for pure-semantic scoring; hybrid/fts must keep every row so
|
|
12
|
+
// lexical matches are not dropped.
|
|
13
|
+
export const selectSemanticCandidates = (rows, queryBuckets, limit, minChunks = semanticPrefilterMinChunks) => {
|
|
14
|
+
if (rows.length <= minChunks || queryBuckets.length === 0) {
|
|
15
|
+
return rows;
|
|
16
|
+
}
|
|
17
|
+
if (rows.some((row) => !row.buckets || row.buckets.length === 0)) {
|
|
18
|
+
return rows;
|
|
19
|
+
}
|
|
20
|
+
const bucketSet = new Set(queryBuckets);
|
|
21
|
+
const candidates = rows.filter((row) => row.buckets.some((bucket) => bucketSet.has(bucket)));
|
|
22
|
+
const recallFloor = Math.max(limit * 4, 64);
|
|
23
|
+
return candidates.length >= recallFloor ? candidates : rows;
|
|
24
|
+
};
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { addNoteInputSchema, addFileInputSchema, addFileTool, addNoteTool, deleteNoteInputSchema, deleteNoteTool, volatileAddInputSchema, volatileAddTool, volatileClearInputSchema, volatileClearTool, dedupeInputSchema, dedupeResolveInputSchema, dedupeResolveTool, dedupeTool, brokenLinksInputSchema, brokenLinksTool, bootstrapInputSchema, bootstrapTool, canonicalizeContextLinksInputSchema, canonicalizeContextLinksTool, contextInputSchema, contextPacksInputSchema, contextPacksTool, contextTool, doctorActionsInputSchema, doctorActionsTool, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, explainInputSchema, explainTool, indexInputSchema, indexTool, inboxAddInputSchema, inboxAddTool, inboxListInputSchema, inboxListTool, inboxProcessInputSchema, inboxProcessTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, projectInitInputSchema, projectInitTool, recommendationsInputSchema, recommendationsTool, rememberInputSchema, rememberTool, repairLinksInputSchema, repairLinksTool, searchInputSchema, searchTool, sessionCloseInputSchema, sessionCloseTool, statsInputSchema, statsTool, suggestLinksInputSchema, suggestLinksTool, syncInputSchema, syncTool, validateInputSchema, validateTool, versionInputSchema, versionTool } from './tools.js';
|
|
3
3
|
import { getRuntimeVersion } from './runtime.js';
|
|
4
|
+
import { guardToolHandler } from './tool-guard.js';
|
|
4
5
|
export const createBrainlinkMcpServer = () => {
|
|
5
6
|
const server = new McpServer({
|
|
6
7
|
name: 'brainlink',
|
|
@@ -8,6 +9,12 @@ export const createBrainlinkMcpServer = () => {
|
|
|
8
9
|
version: getRuntimeVersion(),
|
|
9
10
|
description: 'Local-first Markdown memory tools for AI agents.'
|
|
10
11
|
});
|
|
12
|
+
// Route every tool registration through the error guard so a thrown handler
|
|
13
|
+
// returns a structured isError result instead of propagating to the client.
|
|
14
|
+
// Wrapping registerTool once keeps the call sites below unchanged and also
|
|
15
|
+
// covers any tools added later.
|
|
16
|
+
const baseRegisterTool = server.registerTool.bind(server);
|
|
17
|
+
server.registerTool = ((name, config, handler) => baseRegisterTool(name, config, guardToolHandler(name, handler)));
|
|
11
18
|
server.registerTool('brainlink_bootstrap', {
|
|
12
19
|
title: 'Bootstrap Brainlink For A Task (Default Entrypoint)',
|
|
13
20
|
description: 'Default entrypoint for agents. Run this first to index/check memory state, then optionally retrieve context for the current task query.',
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const errorResult = (toolName, error) => {
|
|
2
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3
|
+
const payload = {
|
|
4
|
+
error: message,
|
|
5
|
+
tool: toolName
|
|
6
|
+
};
|
|
7
|
+
return {
|
|
8
|
+
isError: true,
|
|
9
|
+
content: [
|
|
10
|
+
{
|
|
11
|
+
type: 'text',
|
|
12
|
+
text: JSON.stringify(payload, null, 2)
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
structuredContent: payload
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
// Wrap a tool handler so an unexpected throw becomes a structured error result
|
|
19
|
+
// instead of propagating a raw exception to the MCP client. Agents then receive
|
|
20
|
+
// an actionable `isError` payload naming the failing tool rather than a stack
|
|
21
|
+
// trace or a dropped call.
|
|
22
|
+
export const guardToolHandler = (toolName, handler) => async (args, extra) => {
|
|
23
|
+
try {
|
|
24
|
+
return await handler(args, extra);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return errorResult(toolName, error);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../../application/analyze-vault.js';
|
|
4
|
+
import { buildContextPackage } from '../../application/build-context.js';
|
|
5
|
+
import { canonicalizeContextLinks } from '../../application/canonical-context-links.js';
|
|
6
|
+
import { resolveDuplicateNotes, scanDuplicateNotes } from '../../application/dedupe-notes.js';
|
|
7
|
+
import { indexVault } from '../../application/index-vault.js';
|
|
8
|
+
import { closeSession, initializeProjectMemory } from '../../application/operational-workflows.js';
|
|
9
|
+
import { repairBrokenLinks } from '../../application/repair-broken-links.js';
|
|
10
|
+
import { sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
11
|
+
import { getBootstrapPolicy, getBootstrapSessionStatus, getContextSessionStatus, setBootstrapPolicy, touchBootstrapSession, touchContextSession } from '../../infrastructure/session-state.js';
|
|
12
|
+
import { getRuntimeMetadata } from '../runtime.js';
|
|
13
|
+
import { agentInput, contextStrategyInput, ensureBootstrapReady, ensureContextReady, isTruthy, jsonResult, optionalPositiveInteger, positiveInteger, resolveExecutionContext, searchModeInput, vaultInput, withNextActions } from './shared.js';
|
|
14
|
+
export const canonicalizeContextLinksInputSchema = {
|
|
15
|
+
...vaultInput,
|
|
16
|
+
...agentInput,
|
|
17
|
+
dryRun: z.boolean().optional().default(false).describe('Preview canonical context-link writes without changing Markdown.'),
|
|
18
|
+
createHubs: z.boolean().optional().default(true).describe('Create missing context hub notes when needed.'),
|
|
19
|
+
autoIndex: z.boolean().optional().default(true).describe('Reindex after canonicalization when files changed.')
|
|
20
|
+
};
|
|
21
|
+
export const indexInputSchema = {
|
|
22
|
+
...vaultInput,
|
|
23
|
+
full: z
|
|
24
|
+
.boolean()
|
|
25
|
+
.optional()
|
|
26
|
+
.default(false)
|
|
27
|
+
.describe('Force a complete reindex from Markdown source without reusing unchanged index entries.')
|
|
28
|
+
};
|
|
29
|
+
export const repairLinksInputSchema = {
|
|
30
|
+
...vaultInput,
|
|
31
|
+
...agentInput,
|
|
32
|
+
dryRun: z.boolean().optional().default(false).describe('Preview repairs without writing files.'),
|
|
33
|
+
createMissing: z.boolean().optional().default(true).describe('Create placeholder notes for unresolved targets without a safe existing match.'),
|
|
34
|
+
autoIndex: z.boolean().optional().default(true).describe('Reindex vault after repairs.'),
|
|
35
|
+
minScore: z.number().min(0).max(1).optional().default(0.88).describe('Minimum similarity score for automatic retargeting.'),
|
|
36
|
+
margin: z.number().min(0).max(1).optional().default(0.12).describe('Minimum score gap between first and second candidate.')
|
|
37
|
+
};
|
|
38
|
+
export const syncInputSchema = {
|
|
39
|
+
...vaultInput,
|
|
40
|
+
...agentInput,
|
|
41
|
+
contextQuery: z.string().min(1).optional().describe('Optional context smoke query. Omit to skip context probe.'),
|
|
42
|
+
mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode for the optional context probe. Defaults to config value.'),
|
|
43
|
+
strategy: z.enum(['rag', 'cag', 'auto']).optional().describe('Context strategy for the optional context probe. Defaults to the Brainlink config value.'),
|
|
44
|
+
contextLimit: optionalPositiveInteger().describe('Context smoke result limit when contextQuery is provided.'),
|
|
45
|
+
contextTokens: optionalPositiveInteger().describe('Context smoke token target when contextQuery is provided.')
|
|
46
|
+
};
|
|
47
|
+
export const bootstrapInputSchema = {
|
|
48
|
+
...vaultInput,
|
|
49
|
+
...agentInput,
|
|
50
|
+
...searchModeInput,
|
|
51
|
+
...contextStrategyInput,
|
|
52
|
+
query: z
|
|
53
|
+
.string()
|
|
54
|
+
.min(1)
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('Optional task query. When provided, Brainlink also returns a context package in the same call.'),
|
|
57
|
+
limit: optionalPositiveInteger().describe('Context limit used when query is provided.'),
|
|
58
|
+
tokens: optionalPositiveInteger().describe('Context token target used when query is provided.')
|
|
59
|
+
};
|
|
60
|
+
export const policyInputSchema = {
|
|
61
|
+
...vaultInput,
|
|
62
|
+
...agentInput,
|
|
63
|
+
preset: z.enum(['fully-auto', 'strict']).optional().describe('Apply an opinionated policy preset before explicit overrides.'),
|
|
64
|
+
enforceBootstrap: z.boolean().optional().describe('Enable or disable bootstrap enforcement for MCP read tools.'),
|
|
65
|
+
enforceContextFirst: z.boolean().optional().describe('Require brainlink_context before other MCP read tools.'),
|
|
66
|
+
autoBootstrapOnRead: z
|
|
67
|
+
.boolean()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('When bootstrap is missing/stale, run automatic bootstrap on read tools instead of returning preflight-required responses.'),
|
|
70
|
+
autoBootstrapOnStartup: z
|
|
71
|
+
.boolean()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Run automatic bootstrap during MCP server startup using configured default vault/agent.'),
|
|
74
|
+
staleAfterMinutes: positiveInteger(120).describe('Bootstrap freshness window in minutes before read tools require a new bootstrap.')
|
|
75
|
+
};
|
|
76
|
+
export const sessionCloseInputSchema = {
|
|
77
|
+
...vaultInput,
|
|
78
|
+
...agentInput,
|
|
79
|
+
content: z.string().min(1).optional().describe('Optional extra session notes to include in the handoff.'),
|
|
80
|
+
cwd: z.string().min(1).optional().describe('Workspace path used for git status. Defaults to current process working directory.'),
|
|
81
|
+
dryRun: z.boolean().optional().default(false).describe('Preview handoff note without writing it.'),
|
|
82
|
+
autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing handoff.')
|
|
83
|
+
};
|
|
84
|
+
export const projectInitInputSchema = {
|
|
85
|
+
...vaultInput,
|
|
86
|
+
...agentInput,
|
|
87
|
+
projectPath: z.string().min(1).optional().describe('Project path to inspect. Defaults to current process working directory.')
|
|
88
|
+
};
|
|
89
|
+
export const dedupeInputSchema = {
|
|
90
|
+
...vaultInput,
|
|
91
|
+
...agentInput,
|
|
92
|
+
limit: optionalPositiveInteger().describe('Maximum duplicate candidate pairs to return.'),
|
|
93
|
+
minScore: z.number().min(0).max(1).optional().describe('Minimum semantic similarity score between 0 and 1.'),
|
|
94
|
+
semantic: z.boolean().optional().default(true).describe('Enable semantic duplicate detection in addition to exact content hash matches.')
|
|
95
|
+
};
|
|
96
|
+
export const dedupeResolveInputSchema = {
|
|
97
|
+
...vaultInput,
|
|
98
|
+
leftPath: z.string().min(1).describe('Left note path from dedupe results.'),
|
|
99
|
+
rightPath: z.string().min(1).describe('Right note path from dedupe results.'),
|
|
100
|
+
action: z.enum(['merge', 'link', 'ignore']).describe('Resolution action.'),
|
|
101
|
+
autoIndex: z.boolean().optional().default(true).describe('Reindex after duplicate resolution.')
|
|
102
|
+
};
|
|
103
|
+
export const canonicalizeContextLinksTool = async (input) => {
|
|
104
|
+
const context = await resolveExecutionContext(input);
|
|
105
|
+
const result = await canonicalizeContextLinks(context.vault, {
|
|
106
|
+
agentId: context.agent,
|
|
107
|
+
dryRun: input.dryRun === true,
|
|
108
|
+
createMissingHubs: input.createHubs !== false
|
|
109
|
+
});
|
|
110
|
+
const index = input.autoIndex !== false && !result.dryRun && result.changed > 0
|
|
111
|
+
? await indexVault(context.vault, { full: true })
|
|
112
|
+
: undefined;
|
|
113
|
+
return jsonResult({
|
|
114
|
+
vault: context.vault,
|
|
115
|
+
agent: context.agent,
|
|
116
|
+
...result,
|
|
117
|
+
...(index ? { index } : {})
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
export const indexTool = async (input) => {
|
|
121
|
+
const context = await resolveExecutionContext(input);
|
|
122
|
+
const result = await indexVault(context.vault, {
|
|
123
|
+
full: input.full === true
|
|
124
|
+
});
|
|
125
|
+
return jsonResult({
|
|
126
|
+
vault: context.vault,
|
|
127
|
+
...result
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
export const repairLinksTool = async (input) => {
|
|
131
|
+
const context = await resolveExecutionContext(input);
|
|
132
|
+
const result = await repairBrokenLinks(context.vault, {
|
|
133
|
+
agentId: context.agent,
|
|
134
|
+
dryRun: input.dryRun,
|
|
135
|
+
createMissing: input.createMissing,
|
|
136
|
+
autoIndex: input.autoIndex,
|
|
137
|
+
minScore: input.minScore,
|
|
138
|
+
margin: input.margin
|
|
139
|
+
});
|
|
140
|
+
return jsonResult({
|
|
141
|
+
vault: context.vault,
|
|
142
|
+
agent: context.agent,
|
|
143
|
+
...result
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
export const syncTool = async (input) => {
|
|
147
|
+
const context = await resolveExecutionContext(input);
|
|
148
|
+
const readiness = await ensureBootstrapReady(context, input, 'brainlink_sync');
|
|
149
|
+
if (readiness.preflight) {
|
|
150
|
+
return readiness.preflight;
|
|
151
|
+
}
|
|
152
|
+
const contextReadiness = await ensureContextReady(context, input, 'brainlink_sync');
|
|
153
|
+
if (contextReadiness.preflight) {
|
|
154
|
+
return contextReadiness.preflight;
|
|
155
|
+
}
|
|
156
|
+
const index = await indexVault(context.vault);
|
|
157
|
+
const stats = await getStats(context.vault, context.agent);
|
|
158
|
+
const validation = await validateVault(context.vault, context.agent);
|
|
159
|
+
const brokenLinks = await getBrokenLinksReport(context.vault, context.agent);
|
|
160
|
+
const orphans = await getOrphansReport(context.vault, context.agent);
|
|
161
|
+
const response = {
|
|
162
|
+
vault: context.vault,
|
|
163
|
+
agent: context.agent,
|
|
164
|
+
...(readiness.bootstrap ? { bootstrap: readiness.bootstrap } : {}),
|
|
165
|
+
...(contextReadiness.context ? { contextReadiness: contextReadiness.context } : {}),
|
|
166
|
+
index,
|
|
167
|
+
stats,
|
|
168
|
+
validation,
|
|
169
|
+
brokenLinks,
|
|
170
|
+
orphans
|
|
171
|
+
};
|
|
172
|
+
if (!input.contextQuery) {
|
|
173
|
+
return jsonResult(response);
|
|
174
|
+
}
|
|
175
|
+
const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
|
|
176
|
+
const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
|
|
177
|
+
const contextLimit = input.contextLimit ?? context.defaults.defaultSearchLimit;
|
|
178
|
+
const contextTokens = input.contextTokens ?? context.defaults.defaultContextTokens;
|
|
179
|
+
const contextPackage = await buildContextPackage(context.vault, input.contextQuery, contextLimit, contextTokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs);
|
|
180
|
+
const contextSession = await touchContextSession(context.vault, context.agent);
|
|
181
|
+
return jsonResult({
|
|
182
|
+
...response,
|
|
183
|
+
context: {
|
|
184
|
+
mode,
|
|
185
|
+
strategy,
|
|
186
|
+
contextSession,
|
|
187
|
+
...contextPackage
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
export const bootstrapTool = async (input) => {
|
|
192
|
+
const context = await resolveExecutionContext(input);
|
|
193
|
+
const index = await indexVault(context.vault);
|
|
194
|
+
const stats = await getStats(context.vault, context.agent);
|
|
195
|
+
const validation = await validateVault(context.vault, context.agent);
|
|
196
|
+
const mode = sanitizeSearchMode(input.mode, context.defaults.defaultSearchMode);
|
|
197
|
+
const strategy = sanitizeContextStrategy(input.strategy, context.defaults.defaultContextStrategy);
|
|
198
|
+
const limit = input.limit ?? context.defaults.defaultSearchLimit;
|
|
199
|
+
const tokens = input.tokens ?? context.defaults.defaultContextTokens;
|
|
200
|
+
const contextPackage = input.query
|
|
201
|
+
? await buildContextPackage(context.vault, input.query, limit, tokens, context.agent, mode, strategy, context.defaults.defaultContextCacheTtlMs)
|
|
202
|
+
: undefined;
|
|
203
|
+
const contextSession = input.query ? await touchContextSession(context.vault, context.agent) : undefined;
|
|
204
|
+
const guidance = stats.documentCount === 0
|
|
205
|
+
? 'Vault indexed with zero documents. Add durable notes with brainlink_add_note, then run brainlink_bootstrap again.'
|
|
206
|
+
: input.query
|
|
207
|
+
? 'Use returned context as grounding baseline, then write durable updates with brainlink_add_note when needed.'
|
|
208
|
+
: 'Run brainlink_context with the current task query to retrieve grounded context before answering.';
|
|
209
|
+
const session = await touchBootstrapSession(context.vault, context.agent);
|
|
210
|
+
const policy = await getBootstrapPolicy();
|
|
211
|
+
const nextActions = stats.documentCount === 0
|
|
212
|
+
? [
|
|
213
|
+
{
|
|
214
|
+
tool: 'brainlink_add_note',
|
|
215
|
+
reason: 'No indexed documents were found. Add durable Markdown memory first.',
|
|
216
|
+
args: {
|
|
217
|
+
vault: context.vault,
|
|
218
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
219
|
+
title: 'Architecture',
|
|
220
|
+
content: 'Durable memory with explicit [[links]] and #tags.'
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
tool: 'brainlink_bootstrap',
|
|
225
|
+
reason: 'Re-run bootstrap after writing notes so context tools can work on fresh index state.',
|
|
226
|
+
args: {
|
|
227
|
+
vault: context.vault,
|
|
228
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
229
|
+
mode
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
: input.query
|
|
234
|
+
? [
|
|
235
|
+
{
|
|
236
|
+
tool: 'brainlink_add_note',
|
|
237
|
+
reason: 'Persist relevant outcomes from this task as durable memory.',
|
|
238
|
+
args: {
|
|
239
|
+
vault: context.vault,
|
|
240
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
241
|
+
title: 'Task Update',
|
|
242
|
+
content: 'Summarize durable findings and connect with [[existing notes]].'
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
: [
|
|
247
|
+
{
|
|
248
|
+
tool: 'brainlink_context',
|
|
249
|
+
reason: 'Fetch grounded context for the current task.',
|
|
250
|
+
args: {
|
|
251
|
+
vault: context.vault,
|
|
252
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
253
|
+
query: '<task>',
|
|
254
|
+
mode,
|
|
255
|
+
strategy,
|
|
256
|
+
limit,
|
|
257
|
+
tokens
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
];
|
|
261
|
+
return jsonResult(withNextActions({
|
|
262
|
+
vault: context.vault,
|
|
263
|
+
agent: context.agent,
|
|
264
|
+
mode,
|
|
265
|
+
strategy,
|
|
266
|
+
limit,
|
|
267
|
+
tokens,
|
|
268
|
+
index,
|
|
269
|
+
stats,
|
|
270
|
+
validation,
|
|
271
|
+
policy,
|
|
272
|
+
session,
|
|
273
|
+
guidance,
|
|
274
|
+
...(contextPackage ? { context: contextPackage } : {}),
|
|
275
|
+
...(contextSession ? { contextSession } : {})
|
|
276
|
+
}, nextActions));
|
|
277
|
+
};
|
|
278
|
+
export const policyTool = async (input) => {
|
|
279
|
+
const context = await resolveExecutionContext(input);
|
|
280
|
+
const presetPatch = input.preset === 'strict'
|
|
281
|
+
? {
|
|
282
|
+
enforceBootstrap: true,
|
|
283
|
+
enforceContextFirst: true,
|
|
284
|
+
autoBootstrapOnRead: false,
|
|
285
|
+
autoBootstrapOnStartup: false
|
|
286
|
+
}
|
|
287
|
+
: input.preset === 'fully-auto'
|
|
288
|
+
? {
|
|
289
|
+
enforceBootstrap: true,
|
|
290
|
+
enforceContextFirst: true,
|
|
291
|
+
autoBootstrapOnRead: true,
|
|
292
|
+
autoBootstrapOnStartup: true
|
|
293
|
+
}
|
|
294
|
+
: {};
|
|
295
|
+
const policy = input.preset !== undefined ||
|
|
296
|
+
typeof input.enforceBootstrap === 'boolean' ||
|
|
297
|
+
typeof input.enforceContextFirst === 'boolean' ||
|
|
298
|
+
typeof input.autoBootstrapOnRead === 'boolean' ||
|
|
299
|
+
typeof input.autoBootstrapOnStartup === 'boolean' ||
|
|
300
|
+
typeof input.staleAfterMinutes === 'number'
|
|
301
|
+
? await setBootstrapPolicy({
|
|
302
|
+
...presetPatch,
|
|
303
|
+
...(typeof input.enforceBootstrap === 'boolean' ? { enforceBootstrap: input.enforceBootstrap } : {}),
|
|
304
|
+
...(typeof input.enforceContextFirst === 'boolean' ? { enforceContextFirst: input.enforceContextFirst } : {}),
|
|
305
|
+
...(typeof input.autoBootstrapOnRead === 'boolean' ? { autoBootstrapOnRead: input.autoBootstrapOnRead } : {}),
|
|
306
|
+
...(typeof input.autoBootstrapOnStartup === 'boolean' ? { autoBootstrapOnStartup: input.autoBootstrapOnStartup } : {}),
|
|
307
|
+
...(typeof input.staleAfterMinutes === 'number' ? { staleAfterMinutes: input.staleAfterMinutes } : {})
|
|
308
|
+
})
|
|
309
|
+
: await getBootstrapPolicy();
|
|
310
|
+
const bootstrapStatus = await getBootstrapSessionStatus(context.vault, context.agent);
|
|
311
|
+
const contextStatus = await getContextSessionStatus(context.vault, context.agent);
|
|
312
|
+
const nextActions = bootstrapStatus.ready
|
|
313
|
+
? []
|
|
314
|
+
: [
|
|
315
|
+
{
|
|
316
|
+
tool: 'brainlink_bootstrap',
|
|
317
|
+
reason: 'Bootstrap status is not ready. Run bootstrap before using read tools.',
|
|
318
|
+
args: {
|
|
319
|
+
vault: context.vault,
|
|
320
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
321
|
+
mode: context.defaults.defaultSearchMode,
|
|
322
|
+
strategy: context.defaults.defaultContextStrategy
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
];
|
|
326
|
+
const withContextAction = policy.enforceContextFirst && !contextStatus.ready
|
|
327
|
+
? [
|
|
328
|
+
...nextActions,
|
|
329
|
+
{
|
|
330
|
+
tool: 'brainlink_context',
|
|
331
|
+
reason: 'Context-first policy is enabled. Load context before other read tools.',
|
|
332
|
+
args: {
|
|
333
|
+
vault: context.vault,
|
|
334
|
+
...(context.agent ? { agent: context.agent } : {}),
|
|
335
|
+
query: '<task>',
|
|
336
|
+
mode: context.defaults.defaultSearchMode,
|
|
337
|
+
strategy: context.defaults.defaultContextStrategy,
|
|
338
|
+
limit: context.defaults.defaultSearchLimit,
|
|
339
|
+
tokens: context.defaults.defaultContextTokens
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
: nextActions;
|
|
344
|
+
return jsonResult(withNextActions({
|
|
345
|
+
vault: context.vault,
|
|
346
|
+
agent: context.agent,
|
|
347
|
+
runtime: getRuntimeMetadata(),
|
|
348
|
+
policy,
|
|
349
|
+
bootstrapStatus,
|
|
350
|
+
contextStatus,
|
|
351
|
+
...(input.preset ? { presetApplied: input.preset } : {})
|
|
352
|
+
}, withContextAction));
|
|
353
|
+
};
|
|
354
|
+
export const sessionCloseTool = async (input) => {
|
|
355
|
+
const context = await resolveExecutionContext(input);
|
|
356
|
+
const result = await closeSession({
|
|
357
|
+
vaultPath: context.vault,
|
|
358
|
+
agentId: context.agent,
|
|
359
|
+
cwd: resolve(input.cwd ?? process.cwd()),
|
|
360
|
+
content: input.content,
|
|
361
|
+
write: input.dryRun !== true,
|
|
362
|
+
autoIndex: input.autoIndex
|
|
363
|
+
});
|
|
364
|
+
return jsonResult({
|
|
365
|
+
vault: context.vault,
|
|
366
|
+
agent: context.agent,
|
|
367
|
+
dryRun: input.dryRun === true,
|
|
368
|
+
...result
|
|
369
|
+
});
|
|
370
|
+
};
|
|
371
|
+
export const projectInitTool = async (input) => {
|
|
372
|
+
const context = await resolveExecutionContext(input);
|
|
373
|
+
const result = await initializeProjectMemory({
|
|
374
|
+
vaultPath: context.vault,
|
|
375
|
+
projectPath: resolve(input.projectPath ?? process.cwd()),
|
|
376
|
+
agentId: context.agent
|
|
377
|
+
});
|
|
378
|
+
return jsonResult({
|
|
379
|
+
agent: context.agent,
|
|
380
|
+
...result
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
export const dedupeTool = async (input) => {
|
|
384
|
+
const context = await resolveExecutionContext(input);
|
|
385
|
+
const duplicates = await scanDuplicateNotes(context.vault, {
|
|
386
|
+
agentId: context.agent,
|
|
387
|
+
limit: input.limit ?? 25,
|
|
388
|
+
minSemanticScore: input.minScore ?? 0.92,
|
|
389
|
+
includeSemantic: input.semantic !== false
|
|
390
|
+
});
|
|
391
|
+
return jsonResult({
|
|
392
|
+
vault: context.vault,
|
|
393
|
+
agent: context.agent,
|
|
394
|
+
duplicates
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
export const dedupeResolveTool = async (input) => {
|
|
398
|
+
const context = await resolveExecutionContext(input);
|
|
399
|
+
const result = await resolveDuplicateNotes(context.vault, {
|
|
400
|
+
leftPath: input.leftPath,
|
|
401
|
+
rightPath: input.rightPath,
|
|
402
|
+
action: input.action,
|
|
403
|
+
autoIndex: isTruthy(input.autoIndex)
|
|
404
|
+
});
|
|
405
|
+
return jsonResult({
|
|
406
|
+
vault: context.vault,
|
|
407
|
+
...result
|
|
408
|
+
});
|
|
409
|
+
};
|