@andespindola/brainlink 0.1.0-alpha.9 → 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -0
- package/README.md +83 -7
- package/SECURITY.md +14 -2
- package/dist/application/frontend/client-js.js +80 -18
- package/dist/application/get-graph-layout.js +26 -1
- package/dist/application/index-vault.js +11 -3
- package/dist/application/server/host-security.js +3 -3
- package/dist/application/server/routes.js +45 -1
- package/dist/application/start-server.js +2 -2
- package/dist/application/watch-vault.js +4 -1
- package/dist/cli/commands/read-commands.js +10 -10
- package/dist/cli/commands/write-commands.js +4 -6
- package/dist/cli/runtime.js +2 -1
- package/dist/domain/agents.js +2 -1
- package/dist/domain/graph-layout.js +90 -29
- package/dist/domain/markdown.js +80 -3
- package/dist/infrastructure/bucket-vault.js +171 -0
- package/dist/infrastructure/config.js +5 -0
- package/dist/infrastructure/file-system-vault.js +21 -3
- package/dist/infrastructure/sqlite/document-writer.js +4 -3
- package/dist/infrastructure/sqlite/graph-reader.js +22 -10
- package/dist/infrastructure/sqlite/schema.js +12 -1
- package/dist/infrastructure/sqlite-index.js +6 -1
- package/dist/mcp/server.js +13 -3
- package/dist/mcp/tools.js +100 -42
- package/docs/AGENT_USAGE.md +64 -1
- package/docs/ARCHITECTURE.md +22 -1
- package/docs/RELEASE.md +1 -1
- package/docs/templates/agent-namespace-bootstrap.md +27 -0
- package/docs/templates/agent-note-template.md +31 -0
- package/package.json +5 -2
|
@@ -4,9 +4,12 @@ const toGraphLink = (row) => ({
|
|
|
4
4
|
fromTitle: row.from_title,
|
|
5
5
|
fromPath: row.from_path,
|
|
6
6
|
toTitle: row.to_title,
|
|
7
|
-
toPath: row.to_path
|
|
7
|
+
toPath: row.to_path,
|
|
8
|
+
weight: row.weight,
|
|
9
|
+
priority: row.priority
|
|
8
10
|
});
|
|
9
11
|
const normalizeAgentFilter = (agentId) => agentId ? sanitizeAgentId(agentId) : undefined;
|
|
12
|
+
const toTitleKey = (title) => title.toLowerCase();
|
|
10
13
|
export const createGraphReader = (database) => ({
|
|
11
14
|
listLinks: (agentId) => {
|
|
12
15
|
const normalizedAgentId = normalizeAgentFilter(agentId);
|
|
@@ -18,12 +21,14 @@ export const createGraphReader = (database) => ({
|
|
|
18
21
|
source.title AS from_title,
|
|
19
22
|
source.path AS from_path,
|
|
20
23
|
COALESCE(target.title, links.to_title) AS to_title,
|
|
21
|
-
target.path AS to_path
|
|
24
|
+
target.path AS to_path,
|
|
25
|
+
links.weight AS weight,
|
|
26
|
+
links.priority AS priority
|
|
22
27
|
FROM links
|
|
23
28
|
JOIN documents source ON source.id = links.from_document_id
|
|
24
29
|
LEFT JOIN documents target ON target.id = links.to_document_id
|
|
25
30
|
${agentFilter}
|
|
26
|
-
ORDER BY source.title, to_title
|
|
31
|
+
ORDER BY source.title, links.weight DESC, to_title
|
|
27
32
|
`)
|
|
28
33
|
.all(...(normalizedAgentId ? [normalizedAgentId] : []));
|
|
29
34
|
return rows.map(toGraphLink);
|
|
@@ -31,6 +36,7 @@ export const createGraphReader = (database) => ({
|
|
|
31
36
|
listBacklinks: (title, agentId) => {
|
|
32
37
|
const normalizedAgentId = normalizeAgentFilter(agentId);
|
|
33
38
|
const agentFilter = normalizedAgentId ? 'AND source.agent_id = ?' : '';
|
|
39
|
+
const titleKey = toTitleKey(title);
|
|
34
40
|
const rows = database
|
|
35
41
|
.prepare(`
|
|
36
42
|
SELECT
|
|
@@ -38,15 +44,17 @@ export const createGraphReader = (database) => ({
|
|
|
38
44
|
source.title AS from_title,
|
|
39
45
|
source.path AS from_path,
|
|
40
46
|
COALESCE(target.title, links.to_title) AS to_title,
|
|
41
|
-
target.path AS to_path
|
|
47
|
+
target.path AS to_path,
|
|
48
|
+
links.weight AS weight,
|
|
49
|
+
links.priority AS priority
|
|
42
50
|
FROM links
|
|
43
51
|
JOIN documents source ON source.id = links.from_document_id
|
|
44
52
|
LEFT JOIN documents target ON target.id = links.to_document_id
|
|
45
|
-
WHERE
|
|
53
|
+
WHERE links.to_title_key = ?
|
|
46
54
|
${agentFilter}
|
|
47
|
-
ORDER BY source.title
|
|
55
|
+
ORDER BY links.weight DESC, source.title
|
|
48
56
|
`)
|
|
49
|
-
.all(...(normalizedAgentId ? [
|
|
57
|
+
.all(...(normalizedAgentId ? [titleKey, normalizedAgentId] : [titleKey]));
|
|
50
58
|
return rows.map(toGraphLink);
|
|
51
59
|
},
|
|
52
60
|
getGraph: (agentId) => {
|
|
@@ -66,11 +74,13 @@ export const createGraphReader = (database) => ({
|
|
|
66
74
|
SELECT
|
|
67
75
|
links.from_document_id AS source,
|
|
68
76
|
links.to_document_id AS target,
|
|
69
|
-
links.to_title AS target_title
|
|
77
|
+
links.to_title AS target_title,
|
|
78
|
+
links.weight AS weight,
|
|
79
|
+
links.priority AS priority
|
|
70
80
|
FROM links
|
|
71
81
|
JOIN documents source ON source.id = links.from_document_id
|
|
72
82
|
${edgeAgentFilter}
|
|
73
|
-
ORDER BY links.from_document_id, links.to_title
|
|
83
|
+
ORDER BY links.from_document_id, links.weight DESC, links.to_title
|
|
74
84
|
`)
|
|
75
85
|
.all(...(normalizedAgentId ? [normalizedAgentId] : []));
|
|
76
86
|
const nodes = nodeRows.map((row) => ({
|
|
@@ -84,7 +94,9 @@ export const createGraphReader = (database) => ({
|
|
|
84
94
|
const edges = edgeRows.map((row) => ({
|
|
85
95
|
source: row.source,
|
|
86
96
|
target: row.target,
|
|
87
|
-
targetTitle: row.target_title
|
|
97
|
+
targetTitle: row.target_title,
|
|
98
|
+
weight: row.weight,
|
|
99
|
+
priority: row.priority
|
|
88
100
|
}));
|
|
89
101
|
return {
|
|
90
102
|
nodes,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
const schemaVersion =
|
|
1
|
+
const schemaVersion = 5;
|
|
2
2
|
const requiredTableColumns = {
|
|
3
3
|
documents: ['id', 'agent_id', 'title', 'path', 'content', 'tags_json', 'frontmatter_json', 'created_at', 'updated_at'],
|
|
4
4
|
chunks: ['id', 'document_id', 'ordinal', 'content', 'token_count', 'embedding_provider', 'embedding_json'],
|
|
5
|
+
links: ['from_document_id', 'to_title', 'to_title_key', 'to_document_id', 'weight', 'priority'],
|
|
5
6
|
chunks_fts: ['chunk_id', 'document_id', 'agent_id', 'title', 'content']
|
|
6
7
|
};
|
|
7
8
|
const getStoredSchemaVersion = (database) => {
|
|
@@ -65,6 +66,9 @@ export const createSchema = (database) => {
|
|
|
65
66
|
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
|
|
66
67
|
);
|
|
67
68
|
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_documents_agent_title ON documents(agent_id, title);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_document_ordinal ON chunks(document_id, ordinal);
|
|
71
|
+
|
|
68
72
|
CREATE TABLE IF NOT EXISTS embedding_buckets (
|
|
69
73
|
bucket TEXT NOT NULL,
|
|
70
74
|
chunk_id TEXT NOT NULL,
|
|
@@ -77,11 +81,18 @@ export const createSchema = (database) => {
|
|
|
77
81
|
CREATE TABLE IF NOT EXISTS links (
|
|
78
82
|
from_document_id TEXT NOT NULL,
|
|
79
83
|
to_title TEXT NOT NULL,
|
|
84
|
+
to_title_key TEXT NOT NULL,
|
|
80
85
|
to_document_id TEXT,
|
|
86
|
+
weight INTEGER NOT NULL,
|
|
87
|
+
priority TEXT NOT NULL,
|
|
88
|
+
PRIMARY KEY (from_document_id, to_title_key),
|
|
81
89
|
FOREIGN KEY (from_document_id) REFERENCES documents(id) ON DELETE CASCADE,
|
|
82
90
|
FOREIGN KEY (to_document_id) REFERENCES documents(id) ON DELETE SET NULL
|
|
83
91
|
);
|
|
84
92
|
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_links_to_document_id ON links(to_document_id);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_links_to_title_key ON links(to_title_key);
|
|
95
|
+
|
|
85
96
|
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
|
|
86
97
|
chunk_id UNINDEXED,
|
|
87
98
|
document_id UNINDEXED,
|
|
@@ -9,7 +9,12 @@ export const openSqliteIndex = (vaultPath) => {
|
|
|
9
9
|
const databasePath = join(vaultPath, '.brainlink', 'brainlink.db');
|
|
10
10
|
const database = new Database(databasePath);
|
|
11
11
|
chmodSync(databasePath, 0o600);
|
|
12
|
-
database.exec(
|
|
12
|
+
database.exec(`
|
|
13
|
+
PRAGMA foreign_keys = ON;
|
|
14
|
+
PRAGMA journal_mode = WAL;
|
|
15
|
+
PRAGMA synchronous = NORMAL;
|
|
16
|
+
PRAGMA temp_store = MEMORY;
|
|
17
|
+
`);
|
|
13
18
|
createSchema(database);
|
|
14
19
|
return {
|
|
15
20
|
...createIndexWriter(database),
|
package/dist/mcp/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { addNoteInputSchema, addNoteTool, brokenLinksInputSchema, brokenLinksTool, contextInputSchema, contextTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, searchInputSchema, searchTool, validateInputSchema, validateTool } from './tools.js';
|
|
5
|
+
import { addNoteInputSchema, addNoteTool, brokenLinksInputSchema, brokenLinksTool, contextInputSchema, contextTool, graphInputSchema, graphTool, indexInputSchema, indexTool, orphansInputSchema, orphansTool, searchInputSchema, searchTool, statsInputSchema, statsTool, syncInputSchema, syncTool, validateInputSchema, validateTool } from './tools.js';
|
|
6
6
|
const readPackageVersion = () => {
|
|
7
7
|
const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
|
|
8
8
|
const metadata = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
@@ -27,7 +27,7 @@ export const createBrainlinkMcpServer = () => {
|
|
|
27
27
|
}, searchTool);
|
|
28
28
|
server.registerTool('brainlink_add_note', {
|
|
29
29
|
title: 'Add Brainlink Note',
|
|
30
|
-
description: 'Write durable Markdown memory, then reindex the vault. Include explicit [[wiki links]] for connected graph memory.',
|
|
30
|
+
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.',
|
|
31
31
|
inputSchema: addNoteInputSchema
|
|
32
32
|
}, addNoteTool);
|
|
33
33
|
server.registerTool('brainlink_index', {
|
|
@@ -35,14 +35,24 @@ export const createBrainlinkMcpServer = () => {
|
|
|
35
35
|
description: 'Rebuild the local Brainlink index from Markdown notes.',
|
|
36
36
|
inputSchema: indexInputSchema
|
|
37
37
|
}, indexTool);
|
|
38
|
+
server.registerTool('brainlink_stats', {
|
|
39
|
+
title: 'Get Brainlink Vault Stats',
|
|
40
|
+
description: 'Read indexed vault statistics, including node, edge and tag totals.',
|
|
41
|
+
inputSchema: statsInputSchema
|
|
42
|
+
}, statsTool);
|
|
38
43
|
server.registerTool('brainlink_validate', {
|
|
39
44
|
title: 'Validate Brainlink Vault',
|
|
40
45
|
description: 'Validate indexed graph health, including broken links and orphan notes.',
|
|
41
46
|
inputSchema: validateInputSchema
|
|
42
47
|
}, validateTool);
|
|
48
|
+
server.registerTool('brainlink_sync', {
|
|
49
|
+
title: 'Run Brainlink Sync Flow',
|
|
50
|
+
description: 'Run index, stats, validate, broken-links and orphans checks in one call. Optionally run context probe.',
|
|
51
|
+
inputSchema: syncInputSchema
|
|
52
|
+
}, syncTool);
|
|
43
53
|
server.registerTool('brainlink_graph', {
|
|
44
54
|
title: 'Read Brainlink Graph',
|
|
45
|
-
description: 'Read indexed graph nodes and wiki-link edges.',
|
|
55
|
+
description: 'Read indexed graph nodes and wiki-link edges. Edges include weight and priority fields so agents can rank importance and priority.',
|
|
46
56
|
inputSchema: graphInputSchema
|
|
47
57
|
}, graphTool);
|
|
48
58
|
server.registerTool('brainlink_broken_links', {
|
package/dist/mcp/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { getBrokenLinksReport, getOrphansReport, validateVault } from '../application/analyze-vault.js';
|
|
2
|
+
import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../application/analyze-vault.js';
|
|
3
3
|
import { addNote } from '../application/add-note.js';
|
|
4
4
|
import { buildContextPackage } from '../application/build-context.js';
|
|
5
5
|
import { getGraph } from '../application/get-graph.js';
|
|
@@ -18,14 +18,24 @@ const vaultInput = {
|
|
|
18
18
|
vault: z.string().min(1).optional().describe('Vault directory. Omit to use the configured Brainlink default vault.')
|
|
19
19
|
};
|
|
20
20
|
const agentInput = {
|
|
21
|
-
agent: z
|
|
21
|
+
agent: z
|
|
22
|
+
.string()
|
|
23
|
+
.min(1)
|
|
24
|
+
.optional()
|
|
25
|
+
.describe('Agent memory namespace. Omit to use Brainlink.config defaultAgent, otherwise read all agent namespaces.')
|
|
22
26
|
};
|
|
23
27
|
const searchModeInput = {
|
|
24
28
|
mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode. Defaults to the Brainlink config value.')
|
|
25
29
|
};
|
|
26
|
-
const
|
|
30
|
+
const resolveExecutionContext = async (input) => {
|
|
27
31
|
const config = await loadBrainlinkConfig();
|
|
28
|
-
|
|
32
|
+
const vault = await assertVaultAllowed(input.vault ?? config.vault, config.allowedVaults);
|
|
33
|
+
const agent = input.agent ?? config.defaultAgent;
|
|
34
|
+
return {
|
|
35
|
+
vault,
|
|
36
|
+
config,
|
|
37
|
+
agent
|
|
38
|
+
};
|
|
29
39
|
};
|
|
30
40
|
const jsonResult = (value) => ({
|
|
31
41
|
content: [
|
|
@@ -57,8 +67,8 @@ export const addNoteInputSchema = {
|
|
|
57
67
|
content: z
|
|
58
68
|
.string()
|
|
59
69
|
.min(1)
|
|
60
|
-
.describe('Durable Markdown memory. Include explicit [[wiki links]] and #tags when the memory should be connected.'),
|
|
61
|
-
agent: z.string().min(1).optional().
|
|
70
|
+
.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.'),
|
|
71
|
+
agent: z.string().min(1).optional().describe('Agent memory namespace.'),
|
|
62
72
|
allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.')
|
|
63
73
|
};
|
|
64
74
|
export const indexInputSchema = {
|
|
@@ -80,26 +90,36 @@ export const orphansInputSchema = {
|
|
|
80
90
|
...vaultInput,
|
|
81
91
|
...agentInput
|
|
82
92
|
};
|
|
93
|
+
export const statsInputSchema = {
|
|
94
|
+
...vaultInput,
|
|
95
|
+
...agentInput
|
|
96
|
+
};
|
|
97
|
+
export const syncInputSchema = {
|
|
98
|
+
...vaultInput,
|
|
99
|
+
...agentInput,
|
|
100
|
+
contextQuery: z.string().min(1).optional().describe('Optional context smoke query. Omit to skip context probe.'),
|
|
101
|
+
mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode for the optional context probe. Defaults to config value.'),
|
|
102
|
+
contextLimit: positiveInteger(12).describe('Context smoke result limit when contextQuery is provided.'),
|
|
103
|
+
contextTokens: positiveInteger(2000).describe('Context smoke token target when contextQuery is provided.')
|
|
104
|
+
};
|
|
83
105
|
export const contextTool = async (input) => {
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const contextPackage = await buildContextPackage(vault, input.query, input.limit, input.tokens, input.agent, mode);
|
|
106
|
+
const context = await resolveExecutionContext(input);
|
|
107
|
+
const mode = sanitizeSearchMode(input.mode, context.config.defaultSearchMode);
|
|
108
|
+
const contextPackage = await buildContextPackage(context.vault, input.query, input.limit, input.tokens, context.agent, mode);
|
|
88
109
|
return jsonResult({
|
|
89
|
-
vault,
|
|
90
|
-
agent:
|
|
110
|
+
vault: context.vault,
|
|
111
|
+
agent: context.agent,
|
|
91
112
|
mode,
|
|
92
113
|
...contextPackage
|
|
93
114
|
});
|
|
94
115
|
};
|
|
95
116
|
export const searchTool = async (input) => {
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const results = await searchKnowledge(vault, input.query, input.limit, input.agent, mode);
|
|
117
|
+
const context = await resolveExecutionContext(input);
|
|
118
|
+
const mode = sanitizeSearchMode(input.mode, context.config.defaultSearchMode);
|
|
119
|
+
const results = await searchKnowledge(context.vault, input.query, input.limit, context.agent, mode);
|
|
100
120
|
return jsonResult({
|
|
101
|
-
vault,
|
|
102
|
-
agent:
|
|
121
|
+
vault: context.vault,
|
|
122
|
+
agent: context.agent,
|
|
103
123
|
query: input.query,
|
|
104
124
|
limit: input.limit,
|
|
105
125
|
mode,
|
|
@@ -107,60 +127,98 @@ export const searchTool = async (input) => {
|
|
|
107
127
|
});
|
|
108
128
|
};
|
|
109
129
|
export const addNoteTool = async (input) => {
|
|
110
|
-
const
|
|
111
|
-
const path = await addNote(vault, input.title, input.content,
|
|
130
|
+
const context = await resolveExecutionContext(input);
|
|
131
|
+
const path = await addNote(context.vault, input.title, input.content, context.agent, {
|
|
112
132
|
allowSensitive: input.allowSensitive
|
|
113
133
|
});
|
|
114
|
-
const index = await indexVault(vault);
|
|
134
|
+
const index = await indexVault(context.vault);
|
|
115
135
|
return jsonResult({
|
|
116
|
-
vault,
|
|
136
|
+
vault: context.vault,
|
|
117
137
|
title: input.title,
|
|
118
|
-
agent:
|
|
138
|
+
agent: context.agent,
|
|
119
139
|
path,
|
|
120
140
|
index
|
|
121
141
|
});
|
|
122
142
|
};
|
|
123
143
|
export const indexTool = async (input) => {
|
|
124
|
-
const
|
|
125
|
-
const result = await indexVault(vault);
|
|
144
|
+
const context = await resolveExecutionContext(input);
|
|
145
|
+
const result = await indexVault(context.vault);
|
|
126
146
|
return jsonResult({
|
|
127
|
-
vault,
|
|
147
|
+
vault: context.vault,
|
|
128
148
|
...result
|
|
129
149
|
});
|
|
130
150
|
};
|
|
131
151
|
export const validateTool = async (input) => {
|
|
132
|
-
const
|
|
133
|
-
const validation = await validateVault(vault,
|
|
152
|
+
const context = await resolveExecutionContext(input);
|
|
153
|
+
const validation = await validateVault(context.vault, context.agent);
|
|
134
154
|
return jsonResult({
|
|
135
|
-
vault,
|
|
136
|
-
agent:
|
|
155
|
+
vault: context.vault,
|
|
156
|
+
agent: context.agent,
|
|
137
157
|
...validation
|
|
138
158
|
});
|
|
139
159
|
};
|
|
140
160
|
export const graphTool = async (input) => {
|
|
141
|
-
const
|
|
142
|
-
const graph = await getGraph(vault,
|
|
161
|
+
const context = await resolveExecutionContext(input);
|
|
162
|
+
const graph = await getGraph(context.vault, context.agent);
|
|
143
163
|
return jsonResult({
|
|
144
|
-
vault,
|
|
145
|
-
agent:
|
|
164
|
+
vault: context.vault,
|
|
165
|
+
agent: context.agent,
|
|
146
166
|
...graph
|
|
147
167
|
});
|
|
148
168
|
};
|
|
149
169
|
export const brokenLinksTool = async (input) => {
|
|
150
|
-
const
|
|
151
|
-
const brokenLinks = await getBrokenLinksReport(vault,
|
|
170
|
+
const context = await resolveExecutionContext(input);
|
|
171
|
+
const brokenLinks = await getBrokenLinksReport(context.vault, context.agent);
|
|
152
172
|
return jsonResult({
|
|
153
|
-
vault,
|
|
154
|
-
agent:
|
|
173
|
+
vault: context.vault,
|
|
174
|
+
agent: context.agent,
|
|
155
175
|
brokenLinks
|
|
156
176
|
});
|
|
157
177
|
};
|
|
158
178
|
export const orphansTool = async (input) => {
|
|
159
|
-
const
|
|
160
|
-
const orphans = await getOrphansReport(vault,
|
|
179
|
+
const context = await resolveExecutionContext(input);
|
|
180
|
+
const orphans = await getOrphansReport(context.vault, context.agent);
|
|
161
181
|
return jsonResult({
|
|
162
|
-
vault,
|
|
163
|
-
agent:
|
|
182
|
+
vault: context.vault,
|
|
183
|
+
agent: context.agent,
|
|
164
184
|
orphans
|
|
165
185
|
});
|
|
166
186
|
};
|
|
187
|
+
export const statsTool = async (input) => {
|
|
188
|
+
const context = await resolveExecutionContext(input);
|
|
189
|
+
const stats = await getStats(context.vault, context.agent);
|
|
190
|
+
return jsonResult({
|
|
191
|
+
vault: context.vault,
|
|
192
|
+
agent: context.agent,
|
|
193
|
+
stats
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
export const syncTool = async (input) => {
|
|
197
|
+
const context = await resolveExecutionContext(input);
|
|
198
|
+
const index = await indexVault(context.vault);
|
|
199
|
+
const stats = await getStats(context.vault, context.agent);
|
|
200
|
+
const validation = await validateVault(context.vault, context.agent);
|
|
201
|
+
const brokenLinks = await getBrokenLinksReport(context.vault, context.agent);
|
|
202
|
+
const orphans = await getOrphansReport(context.vault, context.agent);
|
|
203
|
+
const response = {
|
|
204
|
+
vault: context.vault,
|
|
205
|
+
agent: context.agent,
|
|
206
|
+
index,
|
|
207
|
+
stats,
|
|
208
|
+
validation,
|
|
209
|
+
brokenLinks,
|
|
210
|
+
orphans
|
|
211
|
+
};
|
|
212
|
+
if (!input.contextQuery) {
|
|
213
|
+
return jsonResult(response);
|
|
214
|
+
}
|
|
215
|
+
const mode = sanitizeSearchMode(input.mode, context.config.defaultSearchMode);
|
|
216
|
+
const contextPackage = await buildContextPackage(context.vault, input.contextQuery, input.contextLimit, input.contextTokens, context.agent, mode);
|
|
217
|
+
return jsonResult({
|
|
218
|
+
...response,
|
|
219
|
+
context: {
|
|
220
|
+
mode,
|
|
221
|
+
...contextPackage
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
};
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -41,6 +41,8 @@ $HOME/.brainlink/vault
|
|
|
41
41
|
|
|
42
42
|
Use `--vault <path>` for a one-off custom vault, or set `vault` in `brainlink.config.json` / `.brainlink.json` for a workspace-level custom default. Set `BRAINLINK_HOME` when the whole Brainlink home directory should live somewhere else.
|
|
43
43
|
|
|
44
|
+
You can also set `defaultAgent` in `brainlink.config.json` / `.brainlink.json` (for example `"defaultAgent": "coding-agent"`). When set, CLI commands and MCP calls reuse it when `--agent`/`agent` is not passed.
|
|
45
|
+
|
|
44
46
|
## Agent Namespaces
|
|
45
47
|
|
|
46
48
|
Each agent writes into a dedicated namespace under `agents/<agent-id>/`:
|
|
@@ -133,6 +135,7 @@ Rules:
|
|
|
133
135
|
|
|
134
136
|
- Use a clear title.
|
|
135
137
|
- Use `[[Note Title]]` for relationships.
|
|
138
|
+
- Put priority markers near links when the relationship is important.
|
|
136
139
|
- Use tags for retrieval.
|
|
137
140
|
- Keep each note focused.
|
|
138
141
|
- Prefer summaries over raw transcripts.
|
|
@@ -144,6 +147,15 @@ Brainlink only builds graph edges from Markdown `[[wiki links]]`.
|
|
|
144
147
|
|
|
145
148
|
The `context` command is read-only. It retrieves indexed notes and returns a compact package for the model, but it does not write memory, create backlinks, infer relationships or modify the graph. If an agent reads context and then learns something durable, the agent must write a note with explicit links before that knowledge becomes connected memory.
|
|
146
149
|
|
|
150
|
+
Graph edges are weighted during indexing. Repeated links increase weight. Links inside headings or task-list lines receive a small boost. Priority markers on the same line as a link raise its priority:
|
|
151
|
+
|
|
152
|
+
```md
|
|
153
|
+
- [ ] Review [[Architecture]] priority: high
|
|
154
|
+
Related: [[Incident Runbook]] #critical
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Agents should use weighted graph output to sort relationships by importance. Edges expose `weight` and `priority`, where priority is one of `low`, `normal`, `high` or `critical`.
|
|
158
|
+
|
|
147
159
|
Required write behavior:
|
|
148
160
|
|
|
149
161
|
1. Choose a clear title for the new note.
|
|
@@ -196,6 +208,54 @@ If the context is empty or weak:
|
|
|
196
208
|
3. Inspect links and backlinks.
|
|
197
209
|
4. Only then answer from general reasoning.
|
|
198
210
|
|
|
211
|
+
## Optimized Agent Workflow (1 to 7)
|
|
212
|
+
|
|
213
|
+
Use this exact loop for higher signal and lower noise:
|
|
214
|
+
|
|
215
|
+
1. Read memory before decisions:
|
|
216
|
+
- `blink context "<task>" --agent "$BLINK_AGENT" --json`
|
|
217
|
+
- Add `--mode hybrid` for mixed retrieval.
|
|
218
|
+
2. Keep vault structure deterministic:
|
|
219
|
+
- Keep shared knowledge in `agents/shared`.
|
|
220
|
+
- Keep private work-in-progress in your own agent namespace.
|
|
221
|
+
3. Write durable notes only, with explicit links and tags:
|
|
222
|
+
- include at least one `[[...]]` link
|
|
223
|
+
- include `#tags` for retrieval
|
|
224
|
+
4. Store only stable decisions and update an existing note when possible.
|
|
225
|
+
5. Use cache-conscious read/refresh cycle:
|
|
226
|
+
- prefer targeted queries over broad dumps.
|
|
227
|
+
- avoid re-indexing unless note set changed.
|
|
228
|
+
6. Run guardrails regularly:
|
|
229
|
+
- `npm run brainlink:sync -- --vault ./vault --agent "$BLINK_AGENT"`.
|
|
230
|
+
- the sync flow runs `index`, `stats`, `validate`, `broken-links`, `orphans` and a quick context probe.
|
|
231
|
+
7. Before responding:
|
|
232
|
+
- cite sources from context output
|
|
233
|
+
- keep output anchored in retrieved references.
|
|
234
|
+
|
|
235
|
+
Templates are available in `docs/templates` for quick note creation.
|
|
236
|
+
|
|
237
|
+
Recommended template:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
cp docs/templates/agent-note-template.md /tmp/agent-note.md
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### MCP Usage for the Optimized Flow
|
|
244
|
+
|
|
245
|
+
When using MCP, use this compact sequence for the same memory discipline:
|
|
246
|
+
|
|
247
|
+
1. Bootstrap context:
|
|
248
|
+
- `brainlink_context` with `agent`, `query`, `mode: hybrid`, `limit`.
|
|
249
|
+
2. Capture durable decisions:
|
|
250
|
+
- `brainlink_add_note` with explicit `[[wiki links]]` and `#tags`.
|
|
251
|
+
3. Run maintenance before handoff or before the next step:
|
|
252
|
+
- `brainlink_sync` with `agent`, `contextQuery`, `mode: hybrid`.
|
|
253
|
+
4. Diagnose graph issues only when needed:
|
|
254
|
+
- `brainlink_validate`, `brainlink_broken_links`, `brainlink_orphans`.
|
|
255
|
+
5. Inspect relationships:
|
|
256
|
+
- `brainlink_graph`.
|
|
257
|
+
6. Use `brainlink_stats` for a quick health snapshot.
|
|
258
|
+
|
|
199
259
|
## Examples For Common Coding Agents
|
|
200
260
|
|
|
201
261
|
These examples assume the agent can run shell commands in the user workspace.
|
|
@@ -464,6 +524,8 @@ Available MCP tools:
|
|
|
464
524
|
|
|
465
525
|
MCP clients can pass `vault` and `agent` arguments per tool call. Set `BRAINLINK_ALLOWED_VAULTS` when exposing Brainlink to an external agent process so a tool cannot pass arbitrary vault paths:
|
|
466
526
|
|
|
527
|
+
`brainlink_graph` returns weighted edges. Agents should prefer higher `weight` and stronger `priority` when deciding which related notes matter most.
|
|
528
|
+
|
|
467
529
|
```bash
|
|
468
530
|
export BRAINLINK_ALLOWED_VAULTS="/absolute/path/to/project-vault"
|
|
469
531
|
```
|
|
@@ -552,4 +614,5 @@ Weak retrieval usually means:
|
|
|
552
614
|
- Local embeddings are deterministic and provider-free; remote embedding providers are not implemented yet.
|
|
553
615
|
- MCP integration is available through the `brainlink-mcp` stdio server.
|
|
554
616
|
- HTTP API is local and unauthenticated.
|
|
555
|
-
-
|
|
617
|
+
- Bucket vaults support S3-compatible `s3://bucket/prefix` URIs and use a local cache for SQLite indexes.
|
|
618
|
+
- Watch mode depends on platform filesystem watcher behavior and is only supported for local filesystem vaults.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -105,13 +105,15 @@ Application code depends on domain rules and infrastructure interfaces.
|
|
|
105
105
|
The infrastructure layer handles side effects:
|
|
106
106
|
|
|
107
107
|
- reading Markdown files from disk
|
|
108
|
+
- mirroring S3-compatible bucket Markdown into a local cache
|
|
108
109
|
- writing Markdown notes
|
|
109
110
|
- creating `.brainlink`
|
|
110
111
|
- writing and querying SQLite
|
|
111
112
|
- running FTS, semantic and hybrid retrieval
|
|
112
113
|
- narrowing semantic candidates through SQLite embedding buckets before cosine scoring
|
|
113
114
|
|
|
114
|
-
SQLite is an index, not the canonical storage model.
|
|
115
|
+
SQLite is an index, not the canonical storage model. For bucket vaults, Markdown
|
|
116
|
+
objects in the bucket remain canonical and SQLite is still local derived data.
|
|
115
117
|
|
|
116
118
|
## Indexing Flow
|
|
117
119
|
|
|
@@ -216,6 +218,23 @@ source note -> target note
|
|
|
216
218
|
|
|
217
219
|
The `backlinks` command queries indexed links pointing to a target title. With `--agent`, it only returns links from that namespace.
|
|
218
220
|
|
|
221
|
+
## Weighted Links
|
|
222
|
+
|
|
223
|
+
Each indexed wiki link is stored as a graph edge with:
|
|
224
|
+
|
|
225
|
+
- `weight`: numeric relationship strength.
|
|
226
|
+
- `priority`: one of `low`, `normal`, `high` or `critical`.
|
|
227
|
+
|
|
228
|
+
The parser derives weight from repeated links, task-list context, heading context and priority markers on the same line as a wiki link. Examples:
|
|
229
|
+
|
|
230
|
+
```md
|
|
231
|
+
Related: [[Architecture]]
|
|
232
|
+
- [ ] Review [[Architecture]] priority: high
|
|
233
|
+
Escalate [[Incident Runbook]] #critical
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Backlink and graph readers return those fields to CLI JSON, HTTP API and MCP clients. Backlink queries use the normalized `to_title_key` column instead of applying `lower(...)` at read time.
|
|
237
|
+
|
|
219
238
|
## Context Building
|
|
220
239
|
|
|
221
240
|
`context` uses search results and selects one chunk per document while staying inside an estimated token budget.
|
|
@@ -240,6 +259,7 @@ Relevant content
|
|
|
240
259
|
Permanent:
|
|
241
260
|
|
|
242
261
|
- Markdown files
|
|
262
|
+
- S3-compatible Markdown objects when the vault is `s3://bucket/prefix`
|
|
243
263
|
- optional Git history around the vault
|
|
244
264
|
|
|
245
265
|
Canonical agent memory lives under:
|
|
@@ -251,6 +271,7 @@ vault/agents/<agent-id>/**/*.md
|
|
|
251
271
|
Rebuildable:
|
|
252
272
|
|
|
253
273
|
- `.brainlink/brainlink.db`
|
|
274
|
+
- `$BRAINLINK_HOME/bucket-cache`
|
|
254
275
|
- FTS records
|
|
255
276
|
- local embedding vectors
|
|
256
277
|
- local embedding bucket index
|
package/docs/RELEASE.md
CHANGED
|
@@ -32,7 +32,7 @@ blink context "release smoke" --vault ./tmp-vault --mode hybrid --json
|
|
|
32
32
|
blink server --vault ./tmp-vault --host 127.0.0.1 --port 4321
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
9. Verify the server refuses
|
|
35
|
+
9. Verify the server refuses public binds:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
blink server --vault ./tmp-vault --host 0.0.0.0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Namespace Bootstrap for New Agent
|
|
2
|
+
|
|
3
|
+
# <agent-id> Agent Notes
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
- Scope:
|
|
8
|
+
- Owner:
|
|
9
|
+
- Active project contexts:
|
|
10
|
+
|
|
11
|
+
## Core Notes
|
|
12
|
+
|
|
13
|
+
- [[Architecture]]
|
|
14
|
+
- [[Coding Conventions]]
|
|
15
|
+
- [[Runbook]]
|
|
16
|
+
- [[Decision Log]]
|
|
17
|
+
- [[Open Questions]]
|
|
18
|
+
|
|
19
|
+
## Process
|
|
20
|
+
|
|
21
|
+
1. Before task: context "<task>" --agent <agent-id> --json
|
|
22
|
+
2. During task: document durable findings with explicit [[wiki links]]
|
|
23
|
+
3. After task: add note + index + validation run
|
|
24
|
+
|
|
25
|
+
## Tags
|
|
26
|
+
|
|
27
|
+
#agent #<agent-id> #process
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Agent Note Template
|
|
2
|
+
|
|
3
|
+
# <Title>
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
- What was decided or discovered.
|
|
8
|
+
- Why this happened.
|
|
9
|
+
- Expected impact or expected behavior.
|
|
10
|
+
|
|
11
|
+
## References
|
|
12
|
+
|
|
13
|
+
- [[Parent concept]]
|
|
14
|
+
- [[Related decision]]
|
|
15
|
+
|
|
16
|
+
## Notes
|
|
17
|
+
|
|
18
|
+
#decision #<domain> #agent
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Changelog
|
|
23
|
+
|
|
24
|
+
- YYYY-MM-DD: created
|
|
25
|
+
|
|
26
|
+
## Durability checklist
|
|
27
|
+
|
|
28
|
+
- The note has a clear title.
|
|
29
|
+
- It links to at least one existing note (unless this is a root concept).
|
|
30
|
+
- It has at least one `#tag`.
|
|
31
|
+
- The content is stable knowledge, not one-off chat or session noise.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andespindola/brainlink",
|
|
3
|
-
"version": "0.1.0-
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
4
|
"description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,9 +44,11 @@
|
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
47
|
-
"build": "npm run clean && tsc -p tsconfig.json",
|
|
47
|
+
"build": "npm run clean && npx --yes snyk test && tsc -p tsconfig.json",
|
|
48
48
|
"dev": "tsx src/cli/main.ts",
|
|
49
49
|
"dev:mcp": "tsx src/mcp/main.ts",
|
|
50
|
+
"brainlink:sync": "bash scripts/brainlink-sync.sh",
|
|
51
|
+
"security": "npx --yes snyk test",
|
|
50
52
|
"test": "vitest run --config vitest.config.ts",
|
|
51
53
|
"check": "npm run build && npm run test",
|
|
52
54
|
"benchmark:large": "tsx src/benchmarks/large-vault.ts",
|
|
@@ -54,6 +56,7 @@
|
|
|
54
56
|
"pack:smoke": "npm pack --dry-run"
|
|
55
57
|
},
|
|
56
58
|
"dependencies": {
|
|
59
|
+
"@aws-sdk/client-s3": "^3.1038.0",
|
|
57
60
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
58
61
|
"better-sqlite3": "^12.9.0",
|
|
59
62
|
"commander": "^14.0.2",
|