@andespindola/brainlink 1.0.2 → 1.0.4
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 +47 -2
- package/dist/application/frontend/client-css.js +168 -0
- package/dist/application/frontend/client-html.js +37 -0
- package/dist/application/frontend/client-js.js +188 -1
- package/dist/application/import-file.js +45 -0
- package/dist/application/inbox.js +54 -0
- package/dist/application/memory-suggestions.js +220 -0
- package/dist/application/operational-workflows.js +153 -0
- package/dist/application/repair-broken-links.js +157 -0
- package/dist/application/server/multipart.js +80 -0
- package/dist/application/server/routes.js +62 -0
- package/dist/cli/commands/practical-commands.js +278 -0
- package/dist/cli/commands/read-commands.js +13 -0
- package/dist/cli/commands/write-commands.js +46 -0
- package/dist/cli/main.js +2 -0
- package/dist/infrastructure/docling.js +40 -0
- package/dist/mcp/server.js +51 -1
- package/dist/mcp/tools.js +264 -1
- package/docs/AGENT_USAGE.md +25 -2
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/QUICKSTART.md +13 -0
- package/package.json +1 -1
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
1
4
|
import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../analyze-vault.js';
|
|
2
5
|
import { buildContextPackage } from '../build-context.js';
|
|
3
6
|
import { getGraph } from '../get-graph.js';
|
|
@@ -7,8 +10,10 @@ import { getGraphLayout } from '../get-graph-layout.js';
|
|
|
7
10
|
import { getGraphView } from '../get-graph-view.js';
|
|
8
11
|
import { getGraphStreamChunk } from '../get-graph-stream-chunk.js';
|
|
9
12
|
import { deleteGraphViewState, getGraphViewState, saveGraphViewState } from '../graph-view-state.js';
|
|
13
|
+
import { importFile } from '../import-file.js';
|
|
10
14
|
import { listAgents } from '../list-agents.js';
|
|
11
15
|
import { listBacklinks, listLinks } from '../list-links.js';
|
|
16
|
+
import { suggestContextLinks } from '../memory-suggestions.js';
|
|
12
17
|
import { searchGraphNodeIds } from '../search-graph-node-ids.js';
|
|
13
18
|
import { searchKnowledge } from '../search-knowledge.js';
|
|
14
19
|
import { loadBrainlinkConfig, resolveAgentRuntimeDefaults, sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
@@ -18,6 +23,7 @@ import { createClientJs } from '../frontend/client-js.js';
|
|
|
18
23
|
import { createClientWorkerJs } from '../frontend/client-worker-js.js';
|
|
19
24
|
import { createClientRenderWorkerJs } from '../frontend/client-render-worker-js.js';
|
|
20
25
|
import { contentTypes, createJsonResponse, isReadMethod, parsePositiveInteger } from './http.js';
|
|
26
|
+
import { parseMultipartForm } from './multipart.js';
|
|
21
27
|
const readSearchMode = async (url) => {
|
|
22
28
|
const config = await loadBrainlinkConfig();
|
|
23
29
|
const defaults = resolveAgentRuntimeDefaults(config, readAgentQuery(url));
|
|
@@ -95,6 +101,27 @@ const readGraphViewStateInput = (url) => ({
|
|
|
95
101
|
agentId: readAgentQuery(url),
|
|
96
102
|
context: readContextQuery(url)
|
|
97
103
|
});
|
|
104
|
+
const booleanField = (value) => value === '1' || value === 'true' || value === 'on';
|
|
105
|
+
const writeUploadTempFile = async (filename, data) => {
|
|
106
|
+
const directory = await mkdtemp(join(tmpdir(), 'brainlink-upload-'));
|
|
107
|
+
const safeName = filename.replace(/[\\/]/g, '-').trim() || 'upload.bin';
|
|
108
|
+
const path = join(directory, safeName);
|
|
109
|
+
await writeFile(path, data);
|
|
110
|
+
return { directory, path };
|
|
111
|
+
};
|
|
112
|
+
const isSameOriginWrite = (request, url) => {
|
|
113
|
+
const originHeader = Array.isArray(request.headers.origin) ? request.headers.origin[0] : request.headers.origin;
|
|
114
|
+
if (!originHeader) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const origin = new URL(originHeader);
|
|
119
|
+
return origin.protocol === url.protocol && origin.host === url.host;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
98
125
|
const compactGraphLayoutThreshold = 12_000;
|
|
99
126
|
const compactGraphLayoutEdgeLimit = 60_000;
|
|
100
127
|
const graphLayoutBodyCacheLimit = 8;
|
|
@@ -345,6 +372,34 @@ export const route = async (request, url, vaultPath) => {
|
|
|
345
372
|
}
|
|
346
373
|
return createResponse(createJsonResponse(await deleteGraphViewState(vaultPath, input)), 200, contentTypes['.json']);
|
|
347
374
|
}
|
|
375
|
+
if (request.method === 'POST' && url.pathname === '/api/import-file') {
|
|
376
|
+
if (!isSameOriginWrite(request, url)) {
|
|
377
|
+
return createResponse(createJsonResponse({ error: 'Cross-origin imports are not allowed' }), 403, contentTypes['.json']);
|
|
378
|
+
}
|
|
379
|
+
const config = await loadBrainlinkConfig();
|
|
380
|
+
const form = await parseMultipartForm(request);
|
|
381
|
+
const file = form.files.find((candidate) => candidate.fieldName === 'file') ?? form.files[0];
|
|
382
|
+
if (!file || file.data.byteLength === 0) {
|
|
383
|
+
return createResponse(createJsonResponse({ error: 'Missing uploaded file' }), 400, contentTypes['.json']);
|
|
384
|
+
}
|
|
385
|
+
const temp = await writeUploadTempFile(file.filename, file.data);
|
|
386
|
+
try {
|
|
387
|
+
const result = await importFile({
|
|
388
|
+
vaultPath,
|
|
389
|
+
filePath: temp.path,
|
|
390
|
+
originalName: file.filename,
|
|
391
|
+
title: form.fields.title,
|
|
392
|
+
agentId: readAgentQuery(url) ?? form.fields.agent,
|
|
393
|
+
allowSensitive: booleanField(form.fields.allowSensitive),
|
|
394
|
+
autoContextLinks: config.autoCanonicalContextLinks,
|
|
395
|
+
autoIndex: config.autoIndexOnWrite
|
|
396
|
+
});
|
|
397
|
+
return createResponse(createJsonResponse(result), 200, contentTypes['.json']);
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
await rm(temp.directory, { recursive: true, force: true });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
348
403
|
if (isReadMethod(request) && url.pathname === '/api/graph-node') {
|
|
349
404
|
const id = url.searchParams.get('id')?.trim() ?? '';
|
|
350
405
|
if (!id) {
|
|
@@ -402,6 +457,13 @@ export const route = async (request, url, vaultPath) => {
|
|
|
402
457
|
const title = url.searchParams.get('title') ?? '';
|
|
403
458
|
return createResponse(createJsonResponse({ title, backlinks: await listBacklinks(vaultPath, title, readAgentQuery(url)) }), 200, contentTypes['.json']);
|
|
404
459
|
}
|
|
460
|
+
if (isReadMethod(request) && url.pathname === '/api/suggest-links') {
|
|
461
|
+
const content = url.searchParams.get('content') ?? '';
|
|
462
|
+
const limit = parsePositiveInteger(url.searchParams.get('limit'), 5);
|
|
463
|
+
return createResponse(createJsonResponse({
|
|
464
|
+
suggestions: await suggestContextLinks(vaultPath, content, readAgentQuery(url), limit)
|
|
465
|
+
}), 200, contentTypes['.json']);
|
|
466
|
+
}
|
|
405
467
|
if (isReadMethod(request) && url.pathname === '/api/stats') {
|
|
406
468
|
return createResponse(createJsonResponse(await getStats(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
|
|
407
469
|
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { addNoteWithMetadata } from '../../application/add-note.js';
|
|
3
|
+
import { indexVault } from '../../application/index-vault.js';
|
|
4
|
+
import { addInboxItem, listInboxItems, processInboxItems } from '../../application/inbox.js';
|
|
5
|
+
import { buildRememberSuggestion, explainSearchResults, readMemoryContentInput, suggestBrokenLinkFixes, suggestContextLinks } from '../../application/memory-suggestions.js';
|
|
6
|
+
import { buildActionableDoctor, closeSession, initializeProjectMemory } from '../../application/operational-workflows.js';
|
|
7
|
+
import { repairBrokenLinks } from '../../application/repair-broken-links.js';
|
|
8
|
+
import { sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
9
|
+
import { parsePositiveInteger, print, resolveOptions } from '../runtime.js';
|
|
10
|
+
const parseBoundedScore = (value, fallback) => {
|
|
11
|
+
if (value == null)
|
|
12
|
+
return fallback;
|
|
13
|
+
const parsed = Number.parseFloat(value);
|
|
14
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
15
|
+
throw new Error(`Invalid score value: ${value}. Expected a number between 0 and 1.`);
|
|
16
|
+
}
|
|
17
|
+
return parsed;
|
|
18
|
+
};
|
|
19
|
+
const registerRememberCommand = (program) => {
|
|
20
|
+
program
|
|
21
|
+
.command('remember')
|
|
22
|
+
.option('-t, --title <title>', 'note title; inferred when omitted')
|
|
23
|
+
.option('-c, --content <content>', 'markdown content')
|
|
24
|
+
.option('-f, --content-file <contentFile>', 'read markdown content from a file')
|
|
25
|
+
.option('--tag <tag...>', 'extra tags to include')
|
|
26
|
+
.option('--link <title...>', 'explicit Context Links to include')
|
|
27
|
+
.option('--link-limit <limit>', 'maximum suggested Context Links', '5')
|
|
28
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
29
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
30
|
+
.option('--allow-sensitive', 'allow writing content that looks like a secret')
|
|
31
|
+
.option('--dry-run', 'preview suggested note without writing it')
|
|
32
|
+
.option('--no-auto-index', 'skip reindexing after remember')
|
|
33
|
+
.option('--json', 'print machine-readable JSON')
|
|
34
|
+
.description('capture memory with inferred title, tags and Context Links')
|
|
35
|
+
.action(async (options) => {
|
|
36
|
+
const resolved = await resolveOptions(options);
|
|
37
|
+
const content = await readMemoryContentInput(options);
|
|
38
|
+
const suggestion = await buildRememberSuggestion({
|
|
39
|
+
vaultPath: resolved.vault,
|
|
40
|
+
content,
|
|
41
|
+
agentId: resolved.agent,
|
|
42
|
+
title: options.title,
|
|
43
|
+
tags: options.tag ?? [],
|
|
44
|
+
links: options.link ?? [],
|
|
45
|
+
linkLimit: parsePositiveInteger(options.linkLimit ?? '5', 5)
|
|
46
|
+
});
|
|
47
|
+
if (options.dryRun) {
|
|
48
|
+
print(options.json, { dryRun: true, vault: resolved.vault, agent: resolved.agent ?? 'shared', suggestion }, () => suggestion.content);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const added = await addNoteWithMetadata(resolved.vault, suggestion.title, suggestion.content, resolved.agent, {
|
|
52
|
+
allowSensitive: Boolean(options.allowSensitive),
|
|
53
|
+
autoContextLinks: false
|
|
54
|
+
});
|
|
55
|
+
const index = options.autoIndex !== false && resolved.config.autoIndexOnWrite ? await indexVault(resolved.vault) : undefined;
|
|
56
|
+
print(options.json, { dryRun: false, vault: resolved.vault, agent: resolved.agent ?? 'shared', suggestion, path: added.path, ...(index ? { index } : {}) }, () => `Remembered "${suggestion.title}" at ${added.path}.`);
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const registerInboxCommands = (program) => {
|
|
60
|
+
const inboxCommand = program.command('inbox').description('capture and triage quick memory items');
|
|
61
|
+
inboxCommand
|
|
62
|
+
.command('add')
|
|
63
|
+
.option('-c, --content <content>', 'inbox content')
|
|
64
|
+
.option('-f, --content-file <contentFile>', 'read inbox content from a file')
|
|
65
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
66
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
67
|
+
.option('--no-auto-index', 'skip reindexing after add')
|
|
68
|
+
.option('--json', 'print machine-readable JSON')
|
|
69
|
+
.description('add a quick untriaged memory item')
|
|
70
|
+
.action(async (options) => {
|
|
71
|
+
const resolved = await resolveOptions(options);
|
|
72
|
+
const content = await readMemoryContentInput(options);
|
|
73
|
+
const result = await addInboxItem({
|
|
74
|
+
vaultPath: resolved.vault,
|
|
75
|
+
content,
|
|
76
|
+
agentId: resolved.agent,
|
|
77
|
+
autoIndex: options.autoIndex !== false && resolved.config.autoIndexOnWrite
|
|
78
|
+
});
|
|
79
|
+
print(options.json, { vault: resolved.vault, agent: resolved.agent ?? 'shared', ...result }, () => `Added inbox item at ${result.item.path}.`);
|
|
80
|
+
});
|
|
81
|
+
inboxCommand
|
|
82
|
+
.command('list')
|
|
83
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
84
|
+
.option('-a, --agent <agent>', 'accepted for consistency; inbox scans vault Markdown')
|
|
85
|
+
.option('-l, --limit <limit>', 'maximum inbox items', '20')
|
|
86
|
+
.option('--json', 'print machine-readable JSON')
|
|
87
|
+
.description('list untriaged inbox memory items')
|
|
88
|
+
.action(async (options) => {
|
|
89
|
+
const resolved = await resolveOptions(options);
|
|
90
|
+
const items = await listInboxItems(resolved.vault, parsePositiveInteger(options.limit ?? '20', 20));
|
|
91
|
+
print(options.json, { vault: resolved.vault, items }, () => items.length === 0 ? 'Inbox is empty.' : items.map((item) => `${item.updatedAt} ${item.path}: ${item.preview}`).join('\n'));
|
|
92
|
+
});
|
|
93
|
+
inboxCommand
|
|
94
|
+
.command('process')
|
|
95
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
96
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
97
|
+
.option('-l, --limit <limit>', 'maximum inbox items to inspect', '10')
|
|
98
|
+
.option('--json', 'print machine-readable JSON')
|
|
99
|
+
.description('suggest titles, tags and links for inbox items')
|
|
100
|
+
.action(async (options) => {
|
|
101
|
+
const resolved = await resolveOptions(options);
|
|
102
|
+
const items = await processInboxItems({
|
|
103
|
+
vaultPath: resolved.vault,
|
|
104
|
+
agentId: resolved.agent,
|
|
105
|
+
limit: parsePositiveInteger(options.limit ?? '10', 10)
|
|
106
|
+
});
|
|
107
|
+
print(options.json, { vault: resolved.vault, agent: resolved.agent ?? 'shared', items }, () => items.length === 0
|
|
108
|
+
? 'Inbox is empty.'
|
|
109
|
+
: items
|
|
110
|
+
.map((item) => [
|
|
111
|
+
`${item.path}`,
|
|
112
|
+
` title: ${item.suggestedTitle}`,
|
|
113
|
+
` tags: ${item.suggestedTags.map((tag) => `#${tag}`).join(' ') || 'none'}`,
|
|
114
|
+
` links: ${item.suggestedLinks.map((link) => `[[${link}]]`).join(' ') || 'none'}`
|
|
115
|
+
].join('\n'))
|
|
116
|
+
.join('\n\n'));
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
const registerProjectCommands = (program) => {
|
|
120
|
+
const projectCommand = program.command('project').description('manage project-scoped Brainlink memory');
|
|
121
|
+
projectCommand
|
|
122
|
+
.command('init')
|
|
123
|
+
.option('-p, --path <path>', 'project path to inspect', process.cwd())
|
|
124
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
125
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
126
|
+
.option('--json', 'print machine-readable JSON')
|
|
127
|
+
.description('seed memory from project docs and agent instructions')
|
|
128
|
+
.action(async (options) => {
|
|
129
|
+
const resolved = await resolveOptions(options);
|
|
130
|
+
const result = await initializeProjectMemory({
|
|
131
|
+
vaultPath: resolved.vault,
|
|
132
|
+
projectPath: resolve(options.path ?? process.cwd()),
|
|
133
|
+
agentId: resolved.agent
|
|
134
|
+
});
|
|
135
|
+
print(options.json, { agent: resolved.agent ?? 'shared', ...result }, () => [
|
|
136
|
+
`Initialized project memory for ${result.projectPath}`,
|
|
137
|
+
`vault=${result.vault}`,
|
|
138
|
+
`notes=${result.notes.length}`,
|
|
139
|
+
`indexedDocuments=${result.index.documentCount}`
|
|
140
|
+
].join('\n'));
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
const registerSuggestionCommands = (program) => {
|
|
144
|
+
program
|
|
145
|
+
.command('suggest-links')
|
|
146
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
147
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
148
|
+
.option('-c, --content <content>', 'content to inspect for link suggestions')
|
|
149
|
+
.option('-f, --content-file <contentFile>', 'read content to inspect from a file')
|
|
150
|
+
.option('-l, --limit <limit>', 'maximum suggestions', '5')
|
|
151
|
+
.option('--broken', 'suggest fixes for unresolved wiki links')
|
|
152
|
+
.option('--json', 'print machine-readable JSON')
|
|
153
|
+
.description('suggest Context Links or fixes for broken wiki links')
|
|
154
|
+
.action(async (options) => {
|
|
155
|
+
const resolved = await resolveOptions(options);
|
|
156
|
+
const limit = parsePositiveInteger(options.limit ?? '5', 5);
|
|
157
|
+
if (options.broken) {
|
|
158
|
+
const suggestions = await suggestBrokenLinkFixes(resolved.vault, resolved.agent, limit);
|
|
159
|
+
print(options.json, { vault: resolved.vault, agent: resolved.agent ?? 'shared', suggestions }, () => suggestions.length === 0
|
|
160
|
+
? 'No broken links found.'
|
|
161
|
+
: suggestions.map((item) => {
|
|
162
|
+
const candidates = item.candidates.length > 0
|
|
163
|
+
? item.candidates.map((candidate) => ` - [[${candidate.title}]] (${candidate.path}) score=${candidate.score}`).join('\n')
|
|
164
|
+
: ' - no close candidate found';
|
|
165
|
+
return `${item.fromTitle} (${item.fromPath}) -> [[${item.toTitle}]]\n${candidates}`;
|
|
166
|
+
}).join('\n\n'));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const content = await readMemoryContentInput(options);
|
|
170
|
+
const suggestions = await suggestContextLinks(resolved.vault, content, resolved.agent, limit);
|
|
171
|
+
print(options.json, { vault: resolved.vault, agent: resolved.agent ?? 'shared', suggestions }, () => suggestions.map((item) => `[[${item.title}]] (${item.path}) score=${item.score} - ${item.reason}`).join('\n'));
|
|
172
|
+
});
|
|
173
|
+
program
|
|
174
|
+
.command('repair-links')
|
|
175
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
176
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
177
|
+
.option('--dry-run', 'preview repairs without writing files')
|
|
178
|
+
.option('--no-create-missing', 'do not create placeholder notes for unresolved targets')
|
|
179
|
+
.option('--no-auto-index', 'skip reindexing after repairs')
|
|
180
|
+
.option('--min-score <score>', 'minimum similarity score for automatic retargeting', '0.88')
|
|
181
|
+
.option('--margin <score>', 'minimum score gap between first and second candidate', '0.12')
|
|
182
|
+
.option('--json', 'print machine-readable JSON')
|
|
183
|
+
.description('repair broken wiki links by retargeting safe matches or creating placeholder targets')
|
|
184
|
+
.action(async (options) => {
|
|
185
|
+
const resolved = await resolveOptions(options);
|
|
186
|
+
const result = await repairBrokenLinks(resolved.vault, {
|
|
187
|
+
agentId: resolved.agent,
|
|
188
|
+
dryRun: options.dryRun === true,
|
|
189
|
+
autoIndex: options.autoIndex !== false && resolved.config.autoIndexOnWrite,
|
|
190
|
+
createMissing: options.createMissing !== false,
|
|
191
|
+
minScore: parseBoundedScore(options.minScore, 0.88),
|
|
192
|
+
margin: parseBoundedScore(options.margin, 0.12)
|
|
193
|
+
});
|
|
194
|
+
print(options.json, { vault: resolved.vault, agent: resolved.agent ?? 'shared', ...result }, () => [
|
|
195
|
+
`${result.dryRun ? 'Previewed' : 'Repaired'} broken links: scanned=${result.scanned}, changed=${result.changed}, created=${result.created}, skipped=${result.skipped}`,
|
|
196
|
+
...result.entries.map((entry) => `- ${entry.action}: ${entry.fromTitle} -> [[${entry.toTitle}]]${entry.targetTitle ? ` => [[${entry.targetTitle}]]` : ''} (${entry.reason})`)
|
|
197
|
+
].join('\n'));
|
|
198
|
+
});
|
|
199
|
+
program
|
|
200
|
+
.command('explain')
|
|
201
|
+
.argument('<query>', 'search query to explain')
|
|
202
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
203
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
204
|
+
.option('-l, --limit <limit>', 'maximum results', '10')
|
|
205
|
+
.option('-m, --mode <mode>', 'search mode: fts, semantic or hybrid')
|
|
206
|
+
.option('--json', 'print machine-readable JSON')
|
|
207
|
+
.description('explain why search results matched a query')
|
|
208
|
+
.action(async (query, options) => {
|
|
209
|
+
const resolved = await resolveOptions(options);
|
|
210
|
+
const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
|
|
211
|
+
const results = await explainSearchResults(resolved.vault, query, parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit), resolved.agent, mode);
|
|
212
|
+
print(options.json, { query, mode, agent: resolved.agent ?? 'shared', results }, () => results.map((result, index) => [
|
|
213
|
+
`${index + 1}. ${result.title} (${result.path}) score=${result.score.toFixed(3)}`,
|
|
214
|
+
...result.reasons.map((reason) => ` - ${reason}`)
|
|
215
|
+
].join('\n')).join('\n\n'));
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
const registerSessionCommands = (program) => {
|
|
219
|
+
const runSessionClose = async (options) => {
|
|
220
|
+
const resolved = await resolveOptions(options);
|
|
221
|
+
const content = options.content || options.contentFile ? await readMemoryContentInput(options) : undefined;
|
|
222
|
+
const result = await closeSession({
|
|
223
|
+
vaultPath: resolved.vault,
|
|
224
|
+
agentId: resolved.agent,
|
|
225
|
+
cwd: process.cwd(),
|
|
226
|
+
content,
|
|
227
|
+
write: options.dryRun !== true,
|
|
228
|
+
autoIndex: options.autoIndex !== false && resolved.config.autoIndexOnWrite
|
|
229
|
+
});
|
|
230
|
+
print(options.json, { vault: resolved.vault, agent: resolved.agent ?? 'shared', dryRun: options.dryRun === true, ...result }, () => options.dryRun ? result.content : `Wrote session handoff "${result.title}" at ${result.writtenPath}.`);
|
|
231
|
+
};
|
|
232
|
+
program
|
|
233
|
+
.command('session-close')
|
|
234
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
235
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
236
|
+
.option('-c, --content <content>', 'extra session notes')
|
|
237
|
+
.option('-f, --content-file <contentFile>', 'read extra session notes from a file')
|
|
238
|
+
.option('--dry-run', 'preview session handoff without writing it')
|
|
239
|
+
.option('--no-auto-index', 'skip reindexing after writing handoff')
|
|
240
|
+
.option('--json', 'print machine-readable JSON')
|
|
241
|
+
.description('write a session handoff note with vault and git state')
|
|
242
|
+
.action(runSessionClose);
|
|
243
|
+
program
|
|
244
|
+
.command('daily')
|
|
245
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
246
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
247
|
+
.option('-c, --content <content>', 'extra daily notes')
|
|
248
|
+
.option('-f, --content-file <contentFile>', 'read extra daily notes from a file')
|
|
249
|
+
.option('--dry-run', 'preview daily handoff without writing it')
|
|
250
|
+
.option('--no-auto-index', 'skip reindexing after writing handoff')
|
|
251
|
+
.option('--json', 'print machine-readable JSON')
|
|
252
|
+
.description('alias for session-close')
|
|
253
|
+
.action(runSessionClose);
|
|
254
|
+
};
|
|
255
|
+
export const registerPracticalCommands = (program) => {
|
|
256
|
+
registerRememberCommand(program);
|
|
257
|
+
registerInboxCommands(program);
|
|
258
|
+
registerProjectCommands(program);
|
|
259
|
+
registerSuggestionCommands(program);
|
|
260
|
+
registerSessionCommands(program);
|
|
261
|
+
program
|
|
262
|
+
.command('doctor-actions')
|
|
263
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
264
|
+
.option('-a, --agent <agent>', 'accepted for consistency')
|
|
265
|
+
.option('--json', 'print machine-readable JSON')
|
|
266
|
+
.description('print actionable vault health commands')
|
|
267
|
+
.action(async (options) => {
|
|
268
|
+
const resolved = await resolveOptions(options);
|
|
269
|
+
const report = await buildActionableDoctor(resolved.vault);
|
|
270
|
+
print(options.json, report, () => [
|
|
271
|
+
...report.doctor.checks.map((check) => `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.message}`),
|
|
272
|
+
'',
|
|
273
|
+
'Actionable next steps:',
|
|
274
|
+
...report.actions.map((action) => `- [${action.severity}] ${action.command} (${action.reason})`)
|
|
275
|
+
].join('\n'));
|
|
276
|
+
process.exitCode = report.doctor.ok ? 0 : 1;
|
|
277
|
+
});
|
|
278
|
+
};
|
|
@@ -3,6 +3,7 @@ import { buildContextPackage, readContextDataSignature } from '../../application
|
|
|
3
3
|
import { getGraph } from '../../application/get-graph.js';
|
|
4
4
|
import { listAgents } from '../../application/list-agents.js';
|
|
5
5
|
import { listBacklinks, listLinks } from '../../application/list-links.js';
|
|
6
|
+
import { explainSearchResults } from '../../application/memory-suggestions.js';
|
|
6
7
|
import { searchKnowledge } from '../../application/search-knowledge.js';
|
|
7
8
|
import { sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
8
9
|
import { clearContextPacks, listContextPacks } from '../../infrastructure/context-packs.js';
|
|
@@ -15,12 +16,24 @@ export const registerReadCommands = (program) => {
|
|
|
15
16
|
.option('-a, --agent <agent>', 'filter by agent memory namespace')
|
|
16
17
|
.option('-l, --limit <limit>', 'maximum results')
|
|
17
18
|
.option('-m, --mode <mode>', 'search mode: fts, semantic or hybrid')
|
|
19
|
+
.option('--explain', 'include match reasons and score components')
|
|
18
20
|
.option('--json', 'print machine-readable JSON')
|
|
19
21
|
.description('search indexed knowledge')
|
|
20
22
|
.action(async (query, options) => {
|
|
21
23
|
const resolved = await resolveOptions(options);
|
|
22
24
|
const limit = parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit);
|
|
23
25
|
const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
|
|
26
|
+
if (options.explain) {
|
|
27
|
+
const results = await explainSearchResults(resolved.vault, query, limit, resolved.agent, mode);
|
|
28
|
+
print(options.json, { query, agent: resolved.agent, limit, mode, results }, () => results
|
|
29
|
+
.map((result, index) => [
|
|
30
|
+
`${index + 1}. ${result.title} (${result.path}) score=${result.score.toFixed(3)} mode=${result.searchMode}`,
|
|
31
|
+
...result.reasons.map((reason) => ` - ${reason}`),
|
|
32
|
+
result.content
|
|
33
|
+
].join('\n'))
|
|
34
|
+
.join('\n\n'));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
24
37
|
const results = await searchKnowledge(resolved.vault, query, limit, resolved.agent, mode);
|
|
25
38
|
print(options.json, { query, agent: resolved.agent, limit, mode, results }, () => results
|
|
26
39
|
.map((result, index) => [`${index + 1}. ${result.title} (${result.path}) score=${result.score.toFixed(3)} mode=${result.searchMode}`, result.content].join('\n'))
|
|
@@ -8,6 +8,7 @@ import { buildContextPackage } from '../../application/build-context.js';
|
|
|
8
8
|
import { deleteNote } from '../../application/delete-note.js';
|
|
9
9
|
import { resolveDuplicateNotes, scanDuplicateNotes } from '../../application/dedupe-notes.js';
|
|
10
10
|
import { importLegacySqliteDatabase } from '../../application/import-legacy-sqlite.js';
|
|
11
|
+
import { importFile } from '../../application/import-file.js';
|
|
11
12
|
import { indexVault, indexVaultWithOptions } from '../../application/index-vault.js';
|
|
12
13
|
import { migrateContextLinks } from '../../application/migrate-context-links.js';
|
|
13
14
|
import { canonicalizeContextLinks } from '../../application/canonical-context-links.js';
|
|
@@ -16,6 +17,7 @@ import { createOfflinePackBackup } from '../../application/offline-pack-backup.j
|
|
|
16
17
|
import { startServer } from '../../application/start-server.js';
|
|
17
18
|
import { startVaultWatcher } from '../../application/watch-vault.js';
|
|
18
19
|
import { doctorVault, getStats, validateVault } from '../../application/analyze-vault.js';
|
|
20
|
+
import { buildActionableDoctor } from '../../application/operational-workflows.js';
|
|
19
21
|
import { defaultBrainlinkConfig, sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
20
22
|
import { loadBrainlinkConfig } from '../../infrastructure/config.js';
|
|
21
23
|
import { assertVaultAllowed, ensureVault, isBucketVaultPath } from '../../infrastructure/file-system-vault.js';
|
|
@@ -874,6 +876,38 @@ export const registerWriteCommands = (program) => {
|
|
|
874
876
|
return `Created note at ${added.path}.${linkMessage}${duplicateMessage}`;
|
|
875
877
|
});
|
|
876
878
|
});
|
|
879
|
+
program
|
|
880
|
+
.command('import-file')
|
|
881
|
+
.argument('<file>', 'file to convert and import into the vault')
|
|
882
|
+
.option('-t, --title <title>', 'note title; defaults to the source filename')
|
|
883
|
+
.option('-v, --vault <vault>', 'vault directory')
|
|
884
|
+
.option('-a, --agent <agent>', 'agent memory namespace')
|
|
885
|
+
.option('--allow-sensitive', 'allow writing converted content that looks like a secret')
|
|
886
|
+
.option('--no-auto-context-links', 'skip canonical Context Links for this imported note')
|
|
887
|
+
.option('--no-auto-index', 'skip reindexing after import')
|
|
888
|
+
.option('--json', 'print machine-readable JSON')
|
|
889
|
+
.description('convert a document with Docling and import it as a Markdown note')
|
|
890
|
+
.action(async (file, options) => {
|
|
891
|
+
const resolved = await resolveOptions(options);
|
|
892
|
+
const result = await importFile({
|
|
893
|
+
vaultPath: resolved.vault,
|
|
894
|
+
filePath: resolve(file),
|
|
895
|
+
title: options.title,
|
|
896
|
+
agentId: resolved.agent,
|
|
897
|
+
allowSensitive: Boolean(options.allowSensitive),
|
|
898
|
+
autoContextLinks: options.autoContextLinks !== false && resolved.config.autoCanonicalContextLinks,
|
|
899
|
+
autoIndex: options.autoIndex !== false && resolved.config.autoIndexOnWrite
|
|
900
|
+
});
|
|
901
|
+
print(options.json, {
|
|
902
|
+
vault: resolved.vault,
|
|
903
|
+
agent: resolved.agent ?? 'shared',
|
|
904
|
+
...result
|
|
905
|
+
}, () => {
|
|
906
|
+
const linkMessage = result.writeConnectivity.autoLinked ? ` Linked to [[${result.writeConnectivity.linkTarget}]].` : '';
|
|
907
|
+
const indexMessage = result.index ? ` Indexed ${result.index.documentCount} documents.` : '';
|
|
908
|
+
return `Imported ${result.sourceName} as "${result.title}" at ${result.path}.${linkMessage}${indexMessage}`;
|
|
909
|
+
});
|
|
910
|
+
});
|
|
877
911
|
program
|
|
878
912
|
.command('delete-note')
|
|
879
913
|
.option('-v, --vault <vault>', 'vault directory')
|
|
@@ -1027,10 +1061,22 @@ export const registerWriteCommands = (program) => {
|
|
|
1027
1061
|
program
|
|
1028
1062
|
.command('doctor')
|
|
1029
1063
|
.option('-v, --vault <vault>', 'vault directory')
|
|
1064
|
+
.option('--actionable', 'include prioritized commands for fixing or improving vault health')
|
|
1030
1065
|
.option('--json', 'print machine-readable JSON')
|
|
1031
1066
|
.description('run Brainlink environment and vault checks')
|
|
1032
1067
|
.action(async (options) => {
|
|
1033
1068
|
const resolved = await resolveOptions(options);
|
|
1069
|
+
if (options.actionable) {
|
|
1070
|
+
const report = await buildActionableDoctor(resolved.vault);
|
|
1071
|
+
print(options.json, report, () => [
|
|
1072
|
+
...report.doctor.checks.map((check) => `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.message}`),
|
|
1073
|
+
'',
|
|
1074
|
+
'Actionable next steps:',
|
|
1075
|
+
...report.actions.map((action) => `- [${action.severity}] ${action.command} (${action.reason})`)
|
|
1076
|
+
].join('\n'));
|
|
1077
|
+
process.exitCode = report.doctor.ok ? 0 : 1;
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1034
1080
|
const report = await doctorVault(resolved.vault);
|
|
1035
1081
|
print(options.json, report, () => {
|
|
1036
1082
|
const checks = report.checks.map((check) => `${check.ok ? 'OK' : 'FAIL'} ${check.name}: ${check.message}`).join('\n');
|
package/dist/cli/main.js
CHANGED
|
@@ -5,6 +5,7 @@ import { basename, dirname, join } from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { registerAgentCommands } from './commands/agent-commands.js';
|
|
7
7
|
import { registerConfigCommands } from './commands/config-commands.js';
|
|
8
|
+
import { registerPracticalCommands } from './commands/practical-commands.js';
|
|
8
9
|
import { registerReadCommands } from './commands/read-commands.js';
|
|
9
10
|
import { registerVaultCommands } from './commands/vault-commands.js';
|
|
10
11
|
import { registerWriteCommands } from './commands/write-commands.js';
|
|
@@ -24,6 +25,7 @@ program
|
|
|
24
25
|
.version(readPackageVersion());
|
|
25
26
|
registerWriteCommands(program);
|
|
26
27
|
registerReadCommands(program);
|
|
28
|
+
registerPracticalCommands(program);
|
|
27
29
|
registerConfigCommands(program);
|
|
28
30
|
registerVaultCommands(program);
|
|
29
31
|
registerAgentCommands(program);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, extname, join } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const readConvertedMarkdown = async (outputDirectory, sourcePath) => {
|
|
8
|
+
const sourceName = basename(sourcePath, extname(sourcePath));
|
|
9
|
+
const files = await readdir(outputDirectory);
|
|
10
|
+
const exact = files.find((file) => file === `${sourceName}.md`);
|
|
11
|
+
const fallback = files.find((file) => extname(file).toLowerCase() === '.md');
|
|
12
|
+
const markdownFile = exact ?? fallback;
|
|
13
|
+
if (!markdownFile) {
|
|
14
|
+
throw new Error('Docling did not produce a Markdown output file.');
|
|
15
|
+
}
|
|
16
|
+
return readFile(join(outputDirectory, markdownFile), 'utf8');
|
|
17
|
+
};
|
|
18
|
+
const toDoclingError = (error) => {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
const notFound = 'code' in Object(error) && Object(error).code === 'ENOENT';
|
|
21
|
+
return new Error(notFound
|
|
22
|
+
? 'Docling executable not found. Install the Python package and make the `docling` command available in PATH.'
|
|
23
|
+
: `Docling conversion failed: ${message}`);
|
|
24
|
+
};
|
|
25
|
+
export const convertDocumentWithDocling = async (input) => {
|
|
26
|
+
const outputDirectory = await mkdtemp(join(tmpdir(), 'brainlink-docling-'));
|
|
27
|
+
try {
|
|
28
|
+
await execFileAsync('docling', ['--to', 'md', '--output', outputDirectory, input.path], {
|
|
29
|
+
maxBuffer: 1024 * 1024 * 50
|
|
30
|
+
});
|
|
31
|
+
const markdown = await readConvertedMarkdown(outputDirectory, input.path);
|
|
32
|
+
return { markdown };
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw toDoclingError(error);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await rm(outputDirectory, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
};
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
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, graphContextsInputSchema, graphContextsTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, policyInputSchema, policyTool, recommendationsInputSchema, recommendationsTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool, versionInputSchema, versionTool } from './tools.js';
|
|
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
4
|
export const createBrainlinkMcpServer = () => {
|
|
5
5
|
const server = new McpServer({
|
|
@@ -43,6 +43,11 @@ export const createBrainlinkMcpServer = () => {
|
|
|
43
43
|
description: 'Search indexed Brainlink notes with FTS, semantic or hybrid retrieval.',
|
|
44
44
|
inputSchema: searchInputSchema
|
|
45
45
|
}, searchTool);
|
|
46
|
+
server.registerTool('brainlink_explain', {
|
|
47
|
+
title: 'Explain Brainlink Search Results',
|
|
48
|
+
description: 'Explain why indexed notes matched a query, including score components and match reasons.',
|
|
49
|
+
inputSchema: explainInputSchema
|
|
50
|
+
}, explainTool);
|
|
46
51
|
server.registerTool('brainlink_dedupe', {
|
|
47
52
|
title: 'Detect Duplicate Notes',
|
|
48
53
|
description: 'Detect possible duplicate notes using exact content hash and semantic similarity scoring.',
|
|
@@ -58,6 +63,26 @@ export const createBrainlinkMcpServer = () => {
|
|
|
58
63
|
description: 'Write durable Markdown memory, then reindex the vault. Include explicit [[wiki links]] for connected graph memory. Add priority markers near links, such as priority: high, #important or #critical, when a relationship should be weighted higher.',
|
|
59
64
|
inputSchema: addNoteInputSchema
|
|
60
65
|
}, addNoteTool);
|
|
66
|
+
server.registerTool('brainlink_remember', {
|
|
67
|
+
title: 'Capture Assisted Brainlink Memory',
|
|
68
|
+
description: 'Capture durable memory with inferred title, tags and Context Links. Supports dry-run preview before writing.',
|
|
69
|
+
inputSchema: rememberInputSchema
|
|
70
|
+
}, rememberTool);
|
|
71
|
+
server.registerTool('brainlink_inbox_add', {
|
|
72
|
+
title: 'Add Brainlink Inbox Item',
|
|
73
|
+
description: 'Capture a quick untriaged memory item tagged for later processing.',
|
|
74
|
+
inputSchema: inboxAddInputSchema
|
|
75
|
+
}, inboxAddTool);
|
|
76
|
+
server.registerTool('brainlink_inbox_list', {
|
|
77
|
+
title: 'List Brainlink Inbox Items',
|
|
78
|
+
description: 'List quick untriaged memory items from the vault.',
|
|
79
|
+
inputSchema: inboxListInputSchema
|
|
80
|
+
}, inboxListTool);
|
|
81
|
+
server.registerTool('brainlink_inbox_process', {
|
|
82
|
+
title: 'Process Brainlink Inbox Items',
|
|
83
|
+
description: 'Suggest titles, tags and Context Links for untriaged inbox items.',
|
|
84
|
+
inputSchema: inboxProcessInputSchema
|
|
85
|
+
}, inboxProcessTool);
|
|
61
86
|
server.registerTool('brainlink_delete_note', {
|
|
62
87
|
title: 'Delete Brainlink Note',
|
|
63
88
|
description: 'Delete a durable Markdown note from the vault after explicit confirmation. Select by title or path; reindexes by default.',
|
|
@@ -93,6 +118,11 @@ export const createBrainlinkMcpServer = () => {
|
|
|
93
118
|
description: 'Read indexed vault statistics, including node, edge and tag totals.',
|
|
94
119
|
inputSchema: statsInputSchema
|
|
95
120
|
}, statsTool);
|
|
121
|
+
server.registerTool('brainlink_doctor_actions', {
|
|
122
|
+
title: 'Get Actionable Brainlink Doctor Plan',
|
|
123
|
+
description: 'Run vault readiness checks and return prioritized executable next actions.',
|
|
124
|
+
inputSchema: doctorActionsInputSchema
|
|
125
|
+
}, doctorActionsTool);
|
|
96
126
|
server.registerTool('brainlink_validate', {
|
|
97
127
|
title: 'Validate Brainlink Vault',
|
|
98
128
|
description: 'Validate indexed graph health, including broken links and orphan notes.',
|
|
@@ -118,10 +148,30 @@ export const createBrainlinkMcpServer = () => {
|
|
|
118
148
|
description: 'List unresolved indexed wiki links.',
|
|
119
149
|
inputSchema: brokenLinksInputSchema
|
|
120
150
|
}, brokenLinksTool);
|
|
151
|
+
server.registerTool('brainlink_suggest_links', {
|
|
152
|
+
title: 'Suggest Brainlink Links',
|
|
153
|
+
description: 'Suggest Context Links for content or likely fixes for unresolved wiki links.',
|
|
154
|
+
inputSchema: suggestLinksInputSchema
|
|
155
|
+
}, suggestLinksTool);
|
|
156
|
+
server.registerTool('brainlink_repair_links', {
|
|
157
|
+
title: 'Repair Brainlink Broken Links',
|
|
158
|
+
description: 'Repair unresolved wiki links by retargeting safe high-confidence matches or creating placeholder target notes.',
|
|
159
|
+
inputSchema: repairLinksInputSchema
|
|
160
|
+
}, repairLinksTool);
|
|
121
161
|
server.registerTool('brainlink_orphans', {
|
|
122
162
|
title: 'List Brainlink Orphans',
|
|
123
163
|
description: 'List indexed notes without incoming or outgoing graph links.',
|
|
124
164
|
inputSchema: orphansInputSchema
|
|
125
165
|
}, orphansTool);
|
|
166
|
+
server.registerTool('brainlink_session_close', {
|
|
167
|
+
title: 'Write Brainlink Session Handoff',
|
|
168
|
+
description: 'Write or preview a session handoff note with vault health and workspace git status.',
|
|
169
|
+
inputSchema: sessionCloseInputSchema
|
|
170
|
+
}, sessionCloseTool);
|
|
171
|
+
server.registerTool('brainlink_project_init', {
|
|
172
|
+
title: 'Initialize Project Memory',
|
|
173
|
+
description: 'Seed Brainlink memory from project documents such as AGENTS.md, README.md and architecture docs.',
|
|
174
|
+
inputSchema: projectInitInputSchema
|
|
175
|
+
}, projectInitTool);
|
|
126
176
|
return server;
|
|
127
177
|
};
|