@andespindola/brainlink 1.0.3 → 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 +32 -0
- package/dist/application/frontend/client-css.js +34 -0
- package/dist/application/frontend/client-html.js +9 -0
- package/dist/application/frontend/client-js.js +85 -1
- 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/routes.js +8 -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 +13 -0
- package/dist/cli/main.js +2 -0
- package/dist/mcp/server.js +51 -1
- package/dist/mcp/tools.js +264 -1
- package/docs/AGENT_USAGE.md +11 -0
- package/docs/QUICKSTART.md +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { sanitizeAgentId, sharedAgentId } from '../domain/agents.js';
|
|
4
|
+
import { parseMarkdownDocument } from '../domain/markdown.js';
|
|
5
|
+
import { ensureVault, readMarkdownFileSummaries, writeMarkdownFile } from '../infrastructure/file-system-vault.js';
|
|
6
|
+
import { indexVault } from './index-vault.js';
|
|
7
|
+
import { suggestBrokenLinkFixes } from './memory-suggestions.js';
|
|
8
|
+
const slugify = (title) => title
|
|
9
|
+
.normalize('NFKD')
|
|
10
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
13
|
+
.replace(/^-+|-+$/g, '') || 'untitled';
|
|
14
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
+
const replaceWikiLinkTitle = (content, fromTitle, toTitle) => {
|
|
16
|
+
const pattern = new RegExp(`\\[\\[${escapeRegExp(fromTitle)}((?:#[^\\]|]+)?(?:\\|[^\\]]+)?)\\]\\]`, 'g');
|
|
17
|
+
return content.replace(pattern, `[[${toTitle}$1]]`);
|
|
18
|
+
};
|
|
19
|
+
const buildPlaceholderNote = (title, sources, agentId) => [
|
|
20
|
+
'---',
|
|
21
|
+
`title: "${title.replaceAll('"', '\\"')}"`,
|
|
22
|
+
`agent: "${sanitizeAgentId(agentId)}"`,
|
|
23
|
+
'---',
|
|
24
|
+
'',
|
|
25
|
+
`# ${title}`,
|
|
26
|
+
'',
|
|
27
|
+
'Auto-created target for unresolved Brainlink wiki links. Replace this placeholder with durable context when the concept is clarified. #triage',
|
|
28
|
+
'',
|
|
29
|
+
'## Referenced By',
|
|
30
|
+
'',
|
|
31
|
+
...sources.map((source) => `- [[${source}]]`),
|
|
32
|
+
''
|
|
33
|
+
].join('\n');
|
|
34
|
+
const readNotes = async (vaultPath) => {
|
|
35
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
36
|
+
const summaries = await readMarkdownFileSummaries(absoluteVaultPath);
|
|
37
|
+
return Promise.all(summaries.map(async (summary) => {
|
|
38
|
+
const content = await readFile(summary.absolutePath, 'utf8');
|
|
39
|
+
const document = parseMarkdownDocument({
|
|
40
|
+
absolutePath: summary.absolutePath,
|
|
41
|
+
vaultPath: absoluteVaultPath,
|
|
42
|
+
content,
|
|
43
|
+
createdAt: summary.createdAt,
|
|
44
|
+
updatedAt: summary.updatedAt
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
relativePath: summary.relativePath,
|
|
48
|
+
title: document.title,
|
|
49
|
+
agentId: document.agentId,
|
|
50
|
+
content
|
|
51
|
+
};
|
|
52
|
+
}));
|
|
53
|
+
};
|
|
54
|
+
const isConfident = (candidates, minScore, margin) => {
|
|
55
|
+
const [best, second] = candidates;
|
|
56
|
+
if (!best || best.score < minScore)
|
|
57
|
+
return false;
|
|
58
|
+
if (!second)
|
|
59
|
+
return true;
|
|
60
|
+
return best.score - second.score >= margin;
|
|
61
|
+
};
|
|
62
|
+
export const repairBrokenLinks = async (vaultPath, options = {}) => {
|
|
63
|
+
const minScore = options.minScore ?? 0.88;
|
|
64
|
+
const margin = options.margin ?? 0.12;
|
|
65
|
+
const createMissing = options.createMissing !== false;
|
|
66
|
+
const suggestions = await suggestBrokenLinkFixes(vaultPath, options.agentId, 5);
|
|
67
|
+
const notes = await readNotes(vaultPath);
|
|
68
|
+
const notesByPath = new Map(notes.map((note) => [note.relativePath, note]));
|
|
69
|
+
const nextContentByPath = new Map();
|
|
70
|
+
const placeholderSources = new Map();
|
|
71
|
+
const entries = [];
|
|
72
|
+
for (const brokenLink of suggestions) {
|
|
73
|
+
const candidates = brokenLink.candidates;
|
|
74
|
+
const best = candidates[0];
|
|
75
|
+
const source = notesByPath.get(brokenLink.fromPath);
|
|
76
|
+
if (best && isConfident(candidates, minScore, margin)) {
|
|
77
|
+
const currentContent = nextContentByPath.get(brokenLink.fromPath) ?? source?.content;
|
|
78
|
+
if (!source || currentContent == null) {
|
|
79
|
+
entries.push({
|
|
80
|
+
...brokenLink,
|
|
81
|
+
action: 'skipped',
|
|
82
|
+
targetTitle: null,
|
|
83
|
+
targetPath: null,
|
|
84
|
+
score: best.score,
|
|
85
|
+
reason: 'source note was not found'
|
|
86
|
+
});
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const nextContent = replaceWikiLinkTitle(currentContent, brokenLink.toTitle, best.title);
|
|
90
|
+
if (nextContent !== currentContent) {
|
|
91
|
+
nextContentByPath.set(brokenLink.fromPath, nextContent);
|
|
92
|
+
}
|
|
93
|
+
entries.push({
|
|
94
|
+
...brokenLink,
|
|
95
|
+
action: 'retargeted',
|
|
96
|
+
targetTitle: best.title,
|
|
97
|
+
targetPath: best.path,
|
|
98
|
+
score: best.score,
|
|
99
|
+
reason: 'single high-confidence existing title match'
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (createMissing) {
|
|
104
|
+
const sourceTitles = placeholderSources.get(brokenLink.toTitle) ?? new Set();
|
|
105
|
+
sourceTitles.add(brokenLink.fromTitle);
|
|
106
|
+
placeholderSources.set(brokenLink.toTitle, sourceTitles);
|
|
107
|
+
entries.push({
|
|
108
|
+
...brokenLink,
|
|
109
|
+
action: 'created-target',
|
|
110
|
+
targetTitle: brokenLink.toTitle,
|
|
111
|
+
targetPath: null,
|
|
112
|
+
score: best?.score ?? null,
|
|
113
|
+
reason: best ? 'no unambiguous existing target; placeholder target will be created' : 'no existing target candidate; placeholder target will be created'
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
entries.push({
|
|
118
|
+
...brokenLink,
|
|
119
|
+
action: 'skipped',
|
|
120
|
+
targetTitle: null,
|
|
121
|
+
targetPath: null,
|
|
122
|
+
score: best?.score ?? null,
|
|
123
|
+
reason: best ? 'candidate score or margin was too low' : 'no candidate found'
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (!options.dryRun) {
|
|
127
|
+
await Promise.all(Array.from(nextContentByPath.entries()).map(([relativePath, content]) => writeMarkdownFile(vaultPath, relativePath, content)));
|
|
128
|
+
await Promise.all(Array.from(placeholderSources.entries()).map(([title, sources]) => {
|
|
129
|
+
const agentId = sanitizeAgentId(options.agentId ?? sharedAgentId);
|
|
130
|
+
const path = join('agents', agentId, 'unresolved-links', `${slugify(title)}.md`).replaceAll('\\', '/');
|
|
131
|
+
return writeMarkdownFile(vaultPath, path, buildPlaceholderNote(title, Array.from(sources), agentId));
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
const changed = nextContentByPath.size;
|
|
135
|
+
const created = placeholderSources.size;
|
|
136
|
+
const index = !options.dryRun && options.autoIndex !== false && (changed > 0 || created > 0)
|
|
137
|
+
? await indexVault(vaultPath)
|
|
138
|
+
: undefined;
|
|
139
|
+
return {
|
|
140
|
+
dryRun: options.dryRun === true,
|
|
141
|
+
scanned: suggestions.length,
|
|
142
|
+
changed,
|
|
143
|
+
created,
|
|
144
|
+
skipped: entries.filter((entry) => entry.action === 'skipped').length,
|
|
145
|
+
entries: entries.map((entry) => {
|
|
146
|
+
if (entry.action !== 'created-target' || entry.targetPath) {
|
|
147
|
+
return entry;
|
|
148
|
+
}
|
|
149
|
+
const agentId = sanitizeAgentId(options.agentId ?? sharedAgentId);
|
|
150
|
+
return {
|
|
151
|
+
...entry,
|
|
152
|
+
targetPath: join('agents', agentId, 'unresolved-links', `${slugify(entry.toTitle)}.md`).replaceAll('\\', '/')
|
|
153
|
+
};
|
|
154
|
+
}),
|
|
155
|
+
...(index ? { index } : {})
|
|
156
|
+
};
|
|
157
|
+
};
|
|
@@ -13,6 +13,7 @@ import { deleteGraphViewState, getGraphViewState, saveGraphViewState } from '../
|
|
|
13
13
|
import { importFile } from '../import-file.js';
|
|
14
14
|
import { listAgents } from '../list-agents.js';
|
|
15
15
|
import { listBacklinks, listLinks } from '../list-links.js';
|
|
16
|
+
import { suggestContextLinks } from '../memory-suggestions.js';
|
|
16
17
|
import { searchGraphNodeIds } from '../search-graph-node-ids.js';
|
|
17
18
|
import { searchKnowledge } from '../search-knowledge.js';
|
|
18
19
|
import { loadBrainlinkConfig, resolveAgentRuntimeDefaults, sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
@@ -456,6 +457,13 @@ export const route = async (request, url, vaultPath) => {
|
|
|
456
457
|
const title = url.searchParams.get('title') ?? '';
|
|
457
458
|
return createResponse(createJsonResponse({ title, backlinks: await listBacklinks(vaultPath, title, readAgentQuery(url)) }), 200, contentTypes['.json']);
|
|
458
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
|
+
}
|
|
459
467
|
if (isReadMethod(request) && url.pathname === '/api/stats') {
|
|
460
468
|
return createResponse(createJsonResponse(await getStats(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
|
|
461
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'))
|
|
@@ -17,6 +17,7 @@ import { createOfflinePackBackup } from '../../application/offline-pack-backup.j
|
|
|
17
17
|
import { startServer } from '../../application/start-server.js';
|
|
18
18
|
import { startVaultWatcher } from '../../application/watch-vault.js';
|
|
19
19
|
import { doctorVault, getStats, validateVault } from '../../application/analyze-vault.js';
|
|
20
|
+
import { buildActionableDoctor } from '../../application/operational-workflows.js';
|
|
20
21
|
import { defaultBrainlinkConfig, sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
21
22
|
import { loadBrainlinkConfig } from '../../infrastructure/config.js';
|
|
22
23
|
import { assertVaultAllowed, ensureVault, isBucketVaultPath } from '../../infrastructure/file-system-vault.js';
|
|
@@ -1060,10 +1061,22 @@ export const registerWriteCommands = (program) => {
|
|
|
1060
1061
|
program
|
|
1061
1062
|
.command('doctor')
|
|
1062
1063
|
.option('-v, --vault <vault>', 'vault directory')
|
|
1064
|
+
.option('--actionable', 'include prioritized commands for fixing or improving vault health')
|
|
1063
1065
|
.option('--json', 'print machine-readable JSON')
|
|
1064
1066
|
.description('run Brainlink environment and vault checks')
|
|
1065
1067
|
.action(async (options) => {
|
|
1066
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
|
+
}
|
|
1067
1080
|
const report = await doctorVault(resolved.vault);
|
|
1068
1081
|
print(options.json, report, () => {
|
|
1069
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);
|
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
|
};
|