@andespindola/brainlink 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +46 -0
  3. package/dist/application/add-note.js +2 -2
  4. package/dist/application/build-context.js +16 -10
  5. package/dist/application/canonical-context-links.js +44 -5
  6. package/dist/application/check-package-update.js +105 -0
  7. package/dist/application/find-similar-notes.js +75 -0
  8. package/dist/application/frontend/client/chunk-fetch.js +236 -0
  9. package/dist/application/frontend/client/controls.js +178 -0
  10. package/dist/application/frontend/client/elements.js +122 -0
  11. package/dist/application/frontend/client/input.js +202 -0
  12. package/dist/application/frontend/client/node-details.js +191 -0
  13. package/dist/application/frontend/client/rendering.js +296 -0
  14. package/dist/application/frontend/client/scope-theme.js +114 -0
  15. package/dist/application/frontend/client/spatial.js +98 -0
  16. package/dist/application/frontend/client/storage.js +215 -0
  17. package/dist/application/frontend/client/upload.js +90 -0
  18. package/dist/application/frontend/client/worker-bootstrap.js +147 -0
  19. package/dist/application/frontend/client-js.js +24 -1837
  20. package/dist/application/frontend/client-render-worker-js.js +1 -1
  21. package/dist/application/index-vault-phases.js +190 -0
  22. package/dist/application/index-vault.js +46 -167
  23. package/dist/application/memory-suggestions.js +4 -1
  24. package/dist/application/search-knowledge.js +19 -3
  25. package/dist/cli/commands/write/dedupe-commands.js +59 -0
  26. package/dist/cli/commands/write/index-commands.js +205 -0
  27. package/dist/cli/commands/write/link-commands.js +68 -0
  28. package/dist/cli/commands/write/note-commands.js +146 -0
  29. package/dist/cli/commands/write/server-commands.js +553 -0
  30. package/dist/cli/commands/write/shared.js +35 -0
  31. package/dist/cli/commands/write/vault-lifecycle-commands.js +270 -0
  32. package/dist/cli/commands/write-commands.js +12 -1303
  33. package/dist/cli/main.js +39 -3
  34. package/dist/domain/context.js +60 -11
  35. package/dist/domain/diversity.js +64 -0
  36. package/dist/domain/embeddings.js +148 -6
  37. package/dist/domain/graph-contexts.js +62 -57
  38. package/dist/domain/graph-layout/cauliflower-layout.js +116 -0
  39. package/dist/domain/graph-layout/collisions.js +100 -0
  40. package/dist/domain/graph-layout/hierarchy.js +135 -0
  41. package/dist/domain/graph-layout/metrics.js +111 -0
  42. package/dist/domain/graph-layout/segments.js +76 -0
  43. package/dist/domain/graph-layout/star-layout.js +110 -0
  44. package/dist/domain/graph-layout.js +4 -625
  45. package/dist/domain/markdown.js +10 -1
  46. package/dist/domain/scoring.js +42 -0
  47. package/dist/domain/tokens.js +17 -1
  48. package/dist/infrastructure/config.js +33 -1
  49. package/dist/infrastructure/file-index.js +227 -60
  50. package/dist/infrastructure/semantic-prefilter.js +24 -0
  51. package/dist/mcp/server.js +23 -1
  52. package/dist/mcp/tool-guard.js +29 -0
  53. package/dist/mcp/tools/maintenance-tools.js +409 -0
  54. package/dist/mcp/tools/read-tools.js +537 -0
  55. package/dist/mcp/tools/shared.js +216 -0
  56. package/dist/mcp/tools/write-tools.js +353 -0
  57. package/dist/mcp/tools.js +3 -1357
  58. package/docs/AGENT_USAGE.md +4 -1
  59. package/docs/ARCHITECTURE.md +4 -3
  60. package/docs/QUICKSTART.md +4 -0
  61. package/package.json +2 -2
@@ -0,0 +1,216 @@
1
+ import { basename, extname } from 'node:path';
2
+ import { z } from 'zod';
3
+ import { resolveAgentRuntimeDefaults, sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
4
+ import { loadBrainlinkConfig } from '../../infrastructure/config.js';
5
+ import { assertVaultAllowed } from '../../infrastructure/file-system-vault.js';
6
+ import { indexVault } from '../../application/index-vault.js';
7
+ import { getBootstrapPolicy, getBootstrapSessionStatus, getContextSessionStatus, touchBootstrapSession } from '../../infrastructure/session-state.js';
8
+ export const positiveInteger = (fallback) => z
9
+ .number()
10
+ .int()
11
+ .positive()
12
+ .optional()
13
+ .transform((value) => value ?? fallback);
14
+ export const optionalPositiveInteger = () => z
15
+ .number()
16
+ .int()
17
+ .positive()
18
+ .optional();
19
+ export const vaultInput = {
20
+ vault: z.string().min(1).optional().describe('Vault directory. Omit to use the configured Brainlink default vault.')
21
+ };
22
+ export const agentInput = {
23
+ agent: z
24
+ .string()
25
+ .min(1)
26
+ .optional()
27
+ .describe('Agent memory namespace. Omit to use Brainlink.config defaultAgent, otherwise read all agent namespaces.')
28
+ };
29
+ export const searchModeInput = {
30
+ mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode. Defaults to the Brainlink config value.')
31
+ };
32
+ export const contextStrategyInput = {
33
+ strategy: z
34
+ .enum(['rag', 'cag', 'auto'])
35
+ .optional()
36
+ .describe('Context strategy per call. Use rag for fresh retrieval assembly, cag to reuse persisted context packs when fresh, or auto to choose CAG on fresh pack hits and RAG otherwise. Defaults to the Brainlink config value.')
37
+ };
38
+ export const resolveExecutionContext = async (input) => {
39
+ const config = await loadBrainlinkConfig();
40
+ const vault = await assertVaultAllowed(input.vault ?? config.vault, config.allowedVaults);
41
+ const agent = input.agent ?? config.defaultAgent;
42
+ const defaults = resolveAgentRuntimeDefaults(config, agent);
43
+ return {
44
+ config,
45
+ vault,
46
+ agent,
47
+ defaults
48
+ };
49
+ };
50
+ export const inferTitleFromPath = (filePath) => {
51
+ const extension = extname(filePath);
52
+ const fromFileName = basename(filePath, extension);
53
+ return fromFileName
54
+ .trim()
55
+ .replace(/[-_]+/g, ' ')
56
+ .replace(/\s+/g, ' ')
57
+ .trim();
58
+ };
59
+ export const isTruthy = (value) => value !== false;
60
+ export const jsonResult = (value) => ({
61
+ content: [
62
+ {
63
+ type: 'text',
64
+ text: JSON.stringify(value, null, 2)
65
+ }
66
+ ],
67
+ structuredContent: value
68
+ });
69
+ export const preflightResult = (value) => jsonResult({
70
+ preflightRequired: true,
71
+ ...value
72
+ });
73
+ export const withNextActions = (value, nextActions) => ({
74
+ ...value,
75
+ nextActions
76
+ });
77
+ export const ensureBootstrapReady = async (context, input, toolName) => {
78
+ const policy = await getBootstrapPolicy();
79
+ if (!policy.enforceBootstrap) {
80
+ return {
81
+ bootstrap: {
82
+ autoBootstrapped: false,
83
+ policy,
84
+ statusBefore: {
85
+ ready: true,
86
+ stale: false
87
+ },
88
+ reason: 'Bootstrap enforcement is disabled by policy.'
89
+ }
90
+ };
91
+ }
92
+ const status = await getBootstrapSessionStatus(context.vault, context.agent);
93
+ if (status.ready) {
94
+ return {
95
+ bootstrap: {
96
+ autoBootstrapped: false,
97
+ policy,
98
+ statusBefore: status,
99
+ reason: 'Bootstrap session is already fresh for this vault/agent.'
100
+ }
101
+ };
102
+ }
103
+ if (policy.autoBootstrapOnRead) {
104
+ const index = await indexVault(context.vault);
105
+ const session = await touchBootstrapSession(context.vault, context.agent);
106
+ const statusAfter = await getBootstrapSessionStatus(context.vault, context.agent);
107
+ return {
108
+ bootstrap: {
109
+ autoBootstrapped: true,
110
+ policy,
111
+ statusBefore: status,
112
+ statusAfter,
113
+ session,
114
+ index,
115
+ reason: 'Auto-bootstrap was applied for this read call because bootstrap was missing or stale.'
116
+ }
117
+ };
118
+ }
119
+ const mode = typeof input.mode === 'string' && ['fts', 'semantic', 'hybrid'].includes(input.mode) ? input.mode : 'hybrid';
120
+ const strategy = sanitizeContextStrategy(typeof input.strategy === 'string' ? input.strategy : undefined, 'rag');
121
+ const query = typeof input.query === 'string' && input.query.trim().length > 0 ? input.query : undefined;
122
+ const bootstrapArgs = {
123
+ vault: context.vault,
124
+ ...(context.agent ? { agent: context.agent } : {}),
125
+ ...(query ? { query } : {}),
126
+ mode,
127
+ strategy
128
+ };
129
+ const nextActions = [
130
+ {
131
+ tool: 'brainlink_bootstrap',
132
+ reason: 'Bootstrap is required before read tools so retrieval stays grounded in current vault state.',
133
+ args: bootstrapArgs
134
+ }
135
+ ];
136
+ return {
137
+ preflight: preflightResult(withNextActions({
138
+ vault: context.vault,
139
+ agent: context.agent,
140
+ blockedTool: toolName,
141
+ policy,
142
+ bootstrapStatus: status,
143
+ guidance: 'Run brainlink_bootstrap first for this vault/agent before using read tools. This keeps retrieval grounded and memory state consistent.',
144
+ bootstrapArgs
145
+ }, nextActions))
146
+ };
147
+ };
148
+ export const ensureContextReady = async (context, input, toolName) => {
149
+ const policy = await getBootstrapPolicy();
150
+ if (!policy.enforceContextFirst) {
151
+ return {
152
+ context: {
153
+ policy,
154
+ statusBefore: {
155
+ ready: true,
156
+ stale: false
157
+ },
158
+ reason: 'Context-first enforcement is disabled by policy.'
159
+ }
160
+ };
161
+ }
162
+ const status = await getContextSessionStatus(context.vault, context.agent);
163
+ if (status.ready) {
164
+ return {
165
+ context: {
166
+ policy,
167
+ statusBefore: status,
168
+ reason: 'Context session is already fresh for this vault/agent.'
169
+ }
170
+ };
171
+ }
172
+ const queryFromInput = typeof input.query === 'string' && input.query.trim().length > 0
173
+ ? input.query
174
+ : typeof input.contextQuery === 'string' && input.contextQuery.trim().length > 0
175
+ ? input.contextQuery
176
+ : '<task>';
177
+ const mode = sanitizeSearchMode(typeof input.mode === 'string' ? input.mode : undefined, context.defaults.defaultSearchMode);
178
+ const strategy = sanitizeContextStrategy(typeof input.strategy === 'string' ? input.strategy : undefined, context.defaults.defaultContextStrategy);
179
+ const limit = typeof input.limit === 'number' && Number.isFinite(input.limit) && input.limit > 0
180
+ ? input.limit
181
+ : typeof input.contextLimit === 'number' && Number.isFinite(input.contextLimit) && input.contextLimit > 0
182
+ ? input.contextLimit
183
+ : context.defaults.defaultSearchLimit;
184
+ const tokens = typeof input.tokens === 'number' && Number.isFinite(input.tokens) && input.tokens > 0
185
+ ? input.tokens
186
+ : typeof input.contextTokens === 'number' && Number.isFinite(input.contextTokens) && input.contextTokens > 0
187
+ ? input.contextTokens
188
+ : context.defaults.defaultContextTokens;
189
+ const contextArgs = {
190
+ vault: context.vault,
191
+ ...(context.agent ? { agent: context.agent } : {}),
192
+ query: queryFromInput,
193
+ mode,
194
+ strategy,
195
+ limit,
196
+ tokens
197
+ };
198
+ const nextActions = [
199
+ {
200
+ tool: 'brainlink_context',
201
+ reason: 'Context must be loaded first so Brainlink is the primary retrieval source before other read tools.',
202
+ args: contextArgs
203
+ }
204
+ ];
205
+ return {
206
+ preflight: preflightResult(withNextActions({
207
+ vault: context.vault,
208
+ agent: context.agent,
209
+ blockedTool: toolName,
210
+ policy,
211
+ contextStatus: status,
212
+ guidance: 'Run brainlink_context first for this vault/agent before other read tools so answers are grounded on Brainlink context.',
213
+ contextArgs
214
+ }, nextActions))
215
+ };
216
+ };
@@ -0,0 +1,353 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { z } from 'zod';
3
+ import { addNoteWithMetadata } from '../../application/add-note.js';
4
+ import { deleteNote } from '../../application/delete-note.js';
5
+ import { scanDuplicateNotes } from '../../application/dedupe-notes.js';
6
+ import { addInboxItem, listInboxItems, processInboxItems } from '../../application/inbox.js';
7
+ import { indexVault } from '../../application/index-vault.js';
8
+ import { buildRememberSuggestion } from '../../application/memory-suggestions.js';
9
+ import { addVolatileMemory, clearVolatileMemory } from '../../infrastructure/volatile-memory.js';
10
+ import { agentInput, inferTitleFromPath, isTruthy, jsonResult, optionalPositiveInteger, positiveInteger, resolveExecutionContext, vaultInput } from './shared.js';
11
+ export const addNoteInputSchema = {
12
+ ...vaultInput,
13
+ title: z.string().min(1).describe('Markdown note title.'),
14
+ content: z
15
+ .string()
16
+ .min(1)
17
+ .describe('Durable Markdown memory. Include explicit [[wiki links]] and #tags when the memory should be connected. Put priority markers near important links, for example priority: high, #important or #critical.'),
18
+ ...agentInput,
19
+ allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.'),
20
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.'),
21
+ autoContextLinks: z
22
+ .boolean()
23
+ .optional()
24
+ .describe('Automatically add canonical Context Links to the inferred visual context hub. Defaults to Brainlink config.')
25
+ };
26
+ export const rememberInputSchema = {
27
+ ...vaultInput,
28
+ ...agentInput,
29
+ title: z.string().min(1).optional().describe('Optional note title. When omitted, Brainlink infers it from content.'),
30
+ content: z.string().min(1).describe('Memory content to capture as a durable Markdown note.'),
31
+ tags: z.array(z.string()).optional().default([]).describe('Extra tags to include.'),
32
+ links: z.array(z.string()).optional().default([]).describe('Explicit Context Links to include.'),
33
+ linkLimit: positiveInteger(5).describe('Maximum suggested Context Links to include.'),
34
+ dryRun: z.boolean().optional().default(false).describe('Preview inferred note without writing it.'),
35
+ allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.'),
36
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.')
37
+ };
38
+ export const inboxAddInputSchema = {
39
+ ...vaultInput,
40
+ ...agentInput,
41
+ content: z.string().min(1).describe('Quick memory content to store as an untriaged inbox note.'),
42
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing inbox item.')
43
+ };
44
+ export const inboxListInputSchema = {
45
+ ...vaultInput,
46
+ ...agentInput,
47
+ limit: positiveInteger(20).describe('Maximum inbox items to return.')
48
+ };
49
+ export const inboxProcessInputSchema = {
50
+ ...vaultInput,
51
+ ...agentInput,
52
+ limit: positiveInteger(10).describe('Maximum inbox items to inspect.')
53
+ };
54
+ export const deleteNoteInputSchema = {
55
+ ...vaultInput,
56
+ ...agentInput,
57
+ title: z.string().min(1).optional().describe('Note title to delete. Use agent to disambiguate namespaced notes.'),
58
+ path: z.string().min(1).optional().describe('Vault-relative or absolute Markdown note path to delete.'),
59
+ confirm: z.boolean().describe('Must be true to confirm deletion.'),
60
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after deletion. Defaults to true.')
61
+ };
62
+ export const addNotesInputSchema = {
63
+ ...vaultInput,
64
+ ...agentInput,
65
+ notes: z
66
+ .array(z.object({
67
+ title: z.string().min(1).describe('Markdown note title.'),
68
+ content: z.string().min(1).describe('Durable Markdown memory. Include [[wiki links]] and #tags to connect it.')
69
+ }))
70
+ .min(1)
71
+ .max(100)
72
+ .describe('Notes to add in a single batch. The vault is reindexed once after all notes are written.'),
73
+ allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.'),
74
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault once after writing all notes.'),
75
+ autoContextLinks: z
76
+ .boolean()
77
+ .optional()
78
+ .describe('Automatically add canonical Context Links to the inferred context hub. Defaults to Brainlink config.')
79
+ };
80
+ export const deleteNotesInputSchema = {
81
+ ...vaultInput,
82
+ ...agentInput,
83
+ notes: z
84
+ .array(z.object({
85
+ title: z.string().min(1).optional().describe('Note title to delete.'),
86
+ path: z.string().min(1).optional().describe('Vault-relative or absolute Markdown note path to delete.')
87
+ }))
88
+ .min(1)
89
+ .max(100)
90
+ .describe('Notes to delete in a single batch, each selected by exactly one of title or path.'),
91
+ confirm: z.boolean().describe('Must be true to confirm deletion of every listed note.'),
92
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault once after all deletions.')
93
+ };
94
+ export const volatileAddInputSchema = {
95
+ ...vaultInput,
96
+ ...agentInput,
97
+ content: z
98
+ .string()
99
+ .min(1)
100
+ .describe('Temporary agent-decided memory. Use for current task state, hypotheses, transient user preferences and unconfirmed findings.'),
101
+ ttlMinutes: optionalPositiveInteger().describe('Minutes before this volatile memory expires. Defaults to 240.'),
102
+ tags: z.array(z.string()).optional().default([]).describe('Optional tags for volatile retrieval.')
103
+ };
104
+ export const volatileClearInputSchema = {
105
+ ...vaultInput,
106
+ ...agentInput
107
+ };
108
+ export const addFileInputSchema = {
109
+ ...vaultInput,
110
+ ...agentInput,
111
+ title: z.string().min(1).optional().describe('Optional note title override. If omitted, uses file name.'),
112
+ filePath: z.string().min(1).describe('Filesystem path to markdown or text file to ingest.'),
113
+ autoIndex: z.boolean().optional().default(true).describe('Reindex vault after ingesting file.'),
114
+ allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.')
115
+ };
116
+ export const addNoteTool = async (input) => {
117
+ const context = await resolveExecutionContext(input);
118
+ const shouldIndex = isTruthy(input.autoIndex);
119
+ const added = await addNoteWithMetadata(context.vault, input.title, input.content, context.agent, {
120
+ allowSensitive: input.allowSensitive,
121
+ autoContextLinks: input.autoContextLinks ?? context.config.autoCanonicalContextLinks
122
+ });
123
+ const index = shouldIndex ? await indexVault(context.vault) : undefined;
124
+ const focusPath = added.path.includes('agents/') ? added.path.slice(added.path.indexOf('agents/')).replaceAll('\\', '/') : undefined;
125
+ const possibleDuplicates = await scanDuplicateNotes(context.vault, {
126
+ agentId: context.agent,
127
+ focusPath,
128
+ limit: 5,
129
+ minSemanticScore: 0.92,
130
+ includeSemantic: true
131
+ });
132
+ return jsonResult({
133
+ vault: context.vault,
134
+ title: input.title,
135
+ agent: context.agent,
136
+ path: added.path,
137
+ writeConnectivity: {
138
+ autoLinked: added.autoLinked,
139
+ linkTarget: added.linkTarget,
140
+ context: added.context,
141
+ hubCreated: added.hubCreated,
142
+ guaranteedEdge: added.autoLinked
143
+ },
144
+ possibleDuplicates,
145
+ ...(index ? { index } : {})
146
+ });
147
+ };
148
+ export const rememberTool = async (input) => {
149
+ const context = await resolveExecutionContext(input);
150
+ const suggestion = await buildRememberSuggestion({
151
+ vaultPath: context.vault,
152
+ content: input.content,
153
+ agentId: context.agent,
154
+ title: input.title,
155
+ tags: input.tags,
156
+ links: input.links,
157
+ linkLimit: input.linkLimit
158
+ });
159
+ if (input.dryRun) {
160
+ return jsonResult({
161
+ dryRun: true,
162
+ vault: context.vault,
163
+ agent: context.agent,
164
+ suggestion
165
+ });
166
+ }
167
+ const added = await addNoteWithMetadata(context.vault, suggestion.title, suggestion.content, context.agent, {
168
+ allowSensitive: input.allowSensitive,
169
+ autoContextLinks: false
170
+ });
171
+ const index = input.autoIndex ? await indexVault(context.vault) : undefined;
172
+ return jsonResult({
173
+ dryRun: false,
174
+ vault: context.vault,
175
+ agent: context.agent,
176
+ suggestion,
177
+ path: added.path,
178
+ ...(index ? { index } : {})
179
+ });
180
+ };
181
+ export const inboxAddTool = async (input) => {
182
+ const context = await resolveExecutionContext(input);
183
+ const result = await addInboxItem({
184
+ vaultPath: context.vault,
185
+ content: input.content,
186
+ agentId: context.agent,
187
+ autoIndex: input.autoIndex
188
+ });
189
+ return jsonResult({
190
+ vault: context.vault,
191
+ agent: context.agent,
192
+ ...result
193
+ });
194
+ };
195
+ export const inboxListTool = async (input) => {
196
+ const context = await resolveExecutionContext(input);
197
+ const items = await listInboxItems(context.vault, input.limit);
198
+ return jsonResult({
199
+ vault: context.vault,
200
+ agent: context.agent,
201
+ items
202
+ });
203
+ };
204
+ export const inboxProcessTool = async (input) => {
205
+ const context = await resolveExecutionContext(input);
206
+ const items = await processInboxItems({
207
+ vaultPath: context.vault,
208
+ agentId: context.agent,
209
+ limit: input.limit
210
+ });
211
+ return jsonResult({
212
+ vault: context.vault,
213
+ agent: context.agent,
214
+ items
215
+ });
216
+ };
217
+ export const deleteNoteTool = async (input) => {
218
+ const context = await resolveExecutionContext(input);
219
+ const result = await deleteNote(context.vault, {
220
+ title: input.title,
221
+ path: input.path,
222
+ agentId: context.agent,
223
+ confirm: input.confirm,
224
+ autoIndex: input.autoIndex
225
+ });
226
+ return jsonResult({
227
+ vault: context.vault,
228
+ ...result
229
+ });
230
+ };
231
+ export const addNotesTool = async (input) => {
232
+ const context = await resolveExecutionContext(input);
233
+ const autoContextLinks = input.autoContextLinks ?? context.config.autoCanonicalContextLinks;
234
+ // Per-note isolation mirrors deleteNotesTool: one failing note (secret,
235
+ // validation, FS error) is reported without discarding the notes that did
236
+ // write or skipping the single end-of-batch reindex.
237
+ const notes = [];
238
+ for (const note of input.notes) {
239
+ try {
240
+ const added = await addNoteWithMetadata(context.vault, note.title, note.content, context.agent, {
241
+ allowSensitive: input.allowSensitive,
242
+ autoContextLinks
243
+ });
244
+ notes.push({
245
+ ok: true,
246
+ title: note.title,
247
+ path: added.path,
248
+ writeConnectivity: {
249
+ autoLinked: added.autoLinked,
250
+ linkTarget: added.linkTarget,
251
+ context: added.context,
252
+ hubCreated: added.hubCreated,
253
+ guaranteedEdge: added.autoLinked
254
+ }
255
+ });
256
+ }
257
+ catch (error) {
258
+ notes.push({ ok: false, title: note.title, error: error instanceof Error ? error.message : String(error) });
259
+ }
260
+ }
261
+ const added = notes.filter((entry) => entry.ok === true).length;
262
+ const index = isTruthy(input.autoIndex) && added > 0 ? await indexVault(context.vault) : undefined;
263
+ return jsonResult({
264
+ vault: context.vault,
265
+ agent: context.agent,
266
+ added,
267
+ failed: notes.length - added,
268
+ notes,
269
+ ...(index ? { index } : {})
270
+ });
271
+ };
272
+ export const deleteNotesTool = async (input) => {
273
+ const context = await resolveExecutionContext(input);
274
+ if (!input.confirm) {
275
+ throw new Error('Refusing to delete notes without explicit confirmation.');
276
+ }
277
+ const results = [];
278
+ for (const note of input.notes) {
279
+ try {
280
+ // Defer indexing: reindex once after the whole batch instead of per note.
281
+ const result = await deleteNote(context.vault, {
282
+ title: note.title,
283
+ path: note.path,
284
+ agentId: context.agent,
285
+ confirm: true,
286
+ autoIndex: false
287
+ });
288
+ results.push({ ok: true, title: result.title, path: result.relativePath });
289
+ }
290
+ catch (error) {
291
+ results.push({ ok: false, selector: note, error: error instanceof Error ? error.message : String(error) });
292
+ }
293
+ }
294
+ const deleted = results.filter((entry) => entry.ok === true).length;
295
+ const index = isTruthy(input.autoIndex) && deleted > 0 ? await indexVault(context.vault) : undefined;
296
+ return jsonResult({
297
+ vault: context.vault,
298
+ agent: context.agent,
299
+ deleted,
300
+ failed: results.length - deleted,
301
+ results,
302
+ ...(index ? { index } : {})
303
+ });
304
+ };
305
+ export const volatileAddTool = async (input) => {
306
+ const context = await resolveExecutionContext(input);
307
+ const entry = await addVolatileMemory(context.vault, input.content, context.agent ?? 'shared', input.ttlMinutes ?? 240, input.tags);
308
+ return jsonResult({
309
+ vault: context.vault,
310
+ agent: context.agent,
311
+ volatile: true,
312
+ entry
313
+ });
314
+ };
315
+ export const volatileClearTool = async (input) => {
316
+ const context = await resolveExecutionContext(input);
317
+ const cleared = await clearVolatileMemory(context.vault, context.agent);
318
+ return jsonResult({
319
+ vault: context.vault,
320
+ agent: context.agent,
321
+ cleared
322
+ });
323
+ };
324
+ export const addFileTool = async (input) => {
325
+ const context = await resolveExecutionContext(input);
326
+ const content = await readFile(input.filePath, 'utf8');
327
+ const inferredTitle = inferTitleFromPath(input.filePath);
328
+ const title = input.title ?? inferredTitle;
329
+ if (title == null || title.length === 0) {
330
+ throw new Error('Cannot infer note title from file path. Provide a title explicitly.');
331
+ }
332
+ const shouldIndex = isTruthy(input.autoIndex);
333
+ const added = await addNoteWithMetadata(context.vault, title, content, context.agent, {
334
+ allowSensitive: input.allowSensitive,
335
+ autoContextLinks: context.config.autoCanonicalContextLinks
336
+ });
337
+ const index = shouldIndex ? await indexVault(context.vault) : undefined;
338
+ return jsonResult({
339
+ vault: context.vault,
340
+ title,
341
+ agent: context.agent,
342
+ filePath: input.filePath,
343
+ path: added.path,
344
+ writeConnectivity: {
345
+ autoLinked: added.autoLinked,
346
+ linkTarget: added.linkTarget,
347
+ context: added.context,
348
+ hubCreated: added.hubCreated,
349
+ guaranteedEdge: added.autoLinked
350
+ },
351
+ ...(index ? { index } : {})
352
+ });
353
+ };