@andespindola/brainlink 0.1.0-alpha.9 → 0.1.0-beta.1
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 +98 -18
- 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 +20 -7
- 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 +7 -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 +18 -3
- package/dist/mcp/tools.js +145 -43
- package/docs/AGENT_USAGE.md +72 -3
- 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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chmod, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
3
|
import { resolvePath } from './paths.js';
|
|
4
|
+
import { getBucketVaultCachePath, isBucketVaultUri, parseBucketVaultUri, syncBucketVaultToCache, writeBucketMarkdownFile } from './bucket-vault.js';
|
|
4
5
|
const excludedDirectories = new Set(['.brainlink', '.git', 'node_modules', 'dist']);
|
|
5
6
|
const directoryMode = 0o700;
|
|
6
7
|
const fileMode = 0o600;
|
|
@@ -15,30 +16,44 @@ const walkMarkdownFiles = async (directory) => {
|
|
|
15
16
|
}));
|
|
16
17
|
return nested.flat();
|
|
17
18
|
};
|
|
18
|
-
export const resolveVaultPath = (vaultPath) => resolvePath(vaultPath);
|
|
19
|
+
export const resolveVaultPath = (vaultPath) => isBucketVaultUri(vaultPath) ? getBucketVaultCachePath(vaultPath) : resolvePath(vaultPath);
|
|
20
|
+
export const isBucketVaultPath = (vaultPath) => isBucketVaultUri(vaultPath);
|
|
19
21
|
const isPathInside = (parent, child) => {
|
|
20
22
|
const path = relative(parent, child);
|
|
21
23
|
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
22
24
|
};
|
|
25
|
+
const isBucketPrefixInside = (parent, child) => parent === '' || child === parent || child.startsWith(`${parent}/`);
|
|
23
26
|
const secureDirectory = async (path) => {
|
|
24
27
|
await mkdir(path, { recursive: true, mode: directoryMode });
|
|
25
28
|
await chmod(path, directoryMode);
|
|
26
29
|
};
|
|
27
30
|
export const assertVaultAllowed = (vaultPath, allowedVaults) => {
|
|
31
|
+
if (isBucketVaultUri(vaultPath)) {
|
|
32
|
+
const vault = parseBucketVaultUri(vaultPath);
|
|
33
|
+
const allowed = allowedVaults.filter(isBucketVaultUri).map(parseBucketVaultUri);
|
|
34
|
+
if (allowedVaults.length > 0 &&
|
|
35
|
+
!allowed.some((allowedVault) => vault.bucket === allowedVault.bucket && isBucketPrefixInside(allowedVault.prefix, vault.prefix))) {
|
|
36
|
+
throw new Error(`Vault path is not allowed: ${vault.uri}. Configure BRAINLINK_ALLOWED_VAULTS or allowedVaults.`);
|
|
37
|
+
}
|
|
38
|
+
return vault.uri;
|
|
39
|
+
}
|
|
28
40
|
const absoluteVaultPath = resolveVaultPath(vaultPath);
|
|
29
|
-
const allowed = allowedVaults.map(resolveVaultPath);
|
|
41
|
+
const allowed = allowedVaults.filter((allowedVault) => !isBucketVaultUri(allowedVault)).map(resolveVaultPath);
|
|
30
42
|
if (allowed.length > 0 && !allowed.some((allowedPath) => isPathInside(allowedPath, absoluteVaultPath))) {
|
|
31
43
|
throw new Error(`Vault path is not allowed: ${absoluteVaultPath}. Configure BRAINLINK_ALLOWED_VAULTS or allowedVaults.`);
|
|
32
44
|
}
|
|
33
45
|
return absoluteVaultPath;
|
|
34
46
|
};
|
|
35
47
|
export const ensureVault = async (vaultPath) => {
|
|
48
|
+
if (isBucketVaultUri(vaultPath)) {
|
|
49
|
+
return syncBucketVaultToCache(vaultPath);
|
|
50
|
+
}
|
|
36
51
|
const absoluteVaultPath = resolveVaultPath(vaultPath);
|
|
37
52
|
await secureDirectory(join(absoluteVaultPath, '.brainlink'));
|
|
38
53
|
return absoluteVaultPath;
|
|
39
54
|
};
|
|
40
55
|
export const readMarkdownFiles = async (vaultPath) => {
|
|
41
|
-
const absoluteVaultPath =
|
|
56
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
42
57
|
const paths = await walkMarkdownFiles(absoluteVaultPath);
|
|
43
58
|
return Promise.all(paths.map(async (absolutePath) => {
|
|
44
59
|
const [content, stats] = await Promise.all([readFile(absolutePath, 'utf8'), stat(absolutePath)]);
|
|
@@ -51,6 +66,9 @@ export const readMarkdownFiles = async (vaultPath) => {
|
|
|
51
66
|
}));
|
|
52
67
|
};
|
|
53
68
|
export const writeMarkdownFile = async (vaultPath, filename, content) => {
|
|
69
|
+
if (isBucketVaultUri(vaultPath)) {
|
|
70
|
+
return writeBucketMarkdownFile(vaultPath, filename, content);
|
|
71
|
+
}
|
|
54
72
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
55
73
|
const absolutePath = resolve(absoluteVaultPath, filename.endsWith('.md') ? filename : `${filename}.md`);
|
|
56
74
|
if (!isPathInside(absoluteVaultPath, absolutePath)) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createEmbeddingBuckets } from '../../domain/embeddings.js';
|
|
2
|
+
const toTitleKey = (title) => title.toLowerCase();
|
|
2
3
|
export const createIndexWriter = (database) => ({
|
|
3
4
|
reset: () => {
|
|
4
5
|
database.exec(`
|
|
@@ -27,8 +28,8 @@ export const createIndexWriter = (database) => ({
|
|
|
27
28
|
VALUES (?, ?)
|
|
28
29
|
`);
|
|
29
30
|
const insertLink = database.prepare(`
|
|
30
|
-
INSERT INTO links (from_document_id, to_title, to_document_id)
|
|
31
|
-
VALUES (?, ?, ?)
|
|
31
|
+
INSERT INTO links (from_document_id, to_title, to_title_key, to_document_id, weight, priority)
|
|
32
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
32
33
|
`);
|
|
33
34
|
const transaction = database.transaction(() => {
|
|
34
35
|
documents.forEach(({ document, chunks, links }) => {
|
|
@@ -41,7 +42,7 @@ export const createIndexWriter = (database) => ({
|
|
|
41
42
|
});
|
|
42
43
|
documents.forEach(({ links }) => {
|
|
43
44
|
links.forEach((link) => {
|
|
44
|
-
insertLink.run(link.fromDocumentId, link.toTitle, link.toDocumentId);
|
|
45
|
+
insertLink.run(link.fromDocumentId, link.toTitle, toTitleKey(link.toTitle), link.toDocumentId, link.weight, link.priority);
|
|
45
46
|
});
|
|
46
47
|
});
|
|
47
48
|
});
|
|
@@ -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, addFileInputSchema, addFileTool, 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,22 +27,37 @@ 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
|
+
server.registerTool('brainlink_add_file', {
|
|
34
|
+
title: 'Ingest Markdown File',
|
|
35
|
+
description: 'Read a local markdown/text file and ingest it as a Brainlink note. Reindex defaults to true.',
|
|
36
|
+
inputSchema: addFileInputSchema
|
|
37
|
+
}, addFileTool);
|
|
33
38
|
server.registerTool('brainlink_index', {
|
|
34
39
|
title: 'Index Brainlink Vault',
|
|
35
40
|
description: 'Rebuild the local Brainlink index from Markdown notes.',
|
|
36
41
|
inputSchema: indexInputSchema
|
|
37
42
|
}, indexTool);
|
|
43
|
+
server.registerTool('brainlink_stats', {
|
|
44
|
+
title: 'Get Brainlink Vault Stats',
|
|
45
|
+
description: 'Read indexed vault statistics, including node, edge and tag totals.',
|
|
46
|
+
inputSchema: statsInputSchema
|
|
47
|
+
}, statsTool);
|
|
38
48
|
server.registerTool('brainlink_validate', {
|
|
39
49
|
title: 'Validate Brainlink Vault',
|
|
40
50
|
description: 'Validate indexed graph health, including broken links and orphan notes.',
|
|
41
51
|
inputSchema: validateInputSchema
|
|
42
52
|
}, validateTool);
|
|
53
|
+
server.registerTool('brainlink_sync', {
|
|
54
|
+
title: 'Run Brainlink Sync Flow',
|
|
55
|
+
description: 'Run index, stats, validate, broken-links and orphans checks in one call. Optionally run context probe.',
|
|
56
|
+
inputSchema: syncInputSchema
|
|
57
|
+
}, syncTool);
|
|
43
58
|
server.registerTool('brainlink_graph', {
|
|
44
59
|
title: 'Read Brainlink Graph',
|
|
45
|
-
description: 'Read indexed graph nodes and wiki-link edges.',
|
|
60
|
+
description: 'Read indexed graph nodes and wiki-link edges. Edges include weight and priority fields so agents can rank importance and priority.',
|
|
46
61
|
inputSchema: graphInputSchema
|
|
47
62
|
}, graphTool);
|
|
48
63
|
server.registerTool('brainlink_broken_links', {
|
package/dist/mcp/tools.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, extname } from 'node:path';
|
|
1
3
|
import { z } from 'zod';
|
|
2
|
-
import { getBrokenLinksReport, getOrphansReport, validateVault } from '../application/analyze-vault.js';
|
|
4
|
+
import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../application/analyze-vault.js';
|
|
3
5
|
import { addNote } from '../application/add-note.js';
|
|
4
6
|
import { buildContextPackage } from '../application/build-context.js';
|
|
5
7
|
import { getGraph } from '../application/get-graph.js';
|
|
@@ -18,15 +20,35 @@ const vaultInput = {
|
|
|
18
20
|
vault: z.string().min(1).optional().describe('Vault directory. Omit to use the configured Brainlink default vault.')
|
|
19
21
|
};
|
|
20
22
|
const agentInput = {
|
|
21
|
-
agent: z
|
|
23
|
+
agent: z
|
|
24
|
+
.string()
|
|
25
|
+
.min(1)
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Agent memory namespace. Omit to use Brainlink.config defaultAgent, otherwise read all agent namespaces.')
|
|
22
28
|
};
|
|
23
29
|
const searchModeInput = {
|
|
24
30
|
mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode. Defaults to the Brainlink config value.')
|
|
25
31
|
};
|
|
26
|
-
const
|
|
32
|
+
const resolveExecutionContext = async (input) => {
|
|
27
33
|
const config = await loadBrainlinkConfig();
|
|
28
|
-
|
|
34
|
+
const vault = await assertVaultAllowed(input.vault ?? config.vault, config.allowedVaults);
|
|
35
|
+
const agent = input.agent ?? config.defaultAgent;
|
|
36
|
+
return {
|
|
37
|
+
config,
|
|
38
|
+
vault,
|
|
39
|
+
agent
|
|
40
|
+
};
|
|
29
41
|
};
|
|
42
|
+
const inferTitleFromPath = (filePath) => {
|
|
43
|
+
const extension = extname(filePath);
|
|
44
|
+
const fromFileName = basename(filePath, extension);
|
|
45
|
+
return fromFileName
|
|
46
|
+
.trim()
|
|
47
|
+
.replace(/[-_]+/g, ' ')
|
|
48
|
+
.replace(/\s+/g, ' ')
|
|
49
|
+
.trim();
|
|
50
|
+
};
|
|
51
|
+
const isTruthy = (value) => value !== false;
|
|
30
52
|
const jsonResult = (value) => ({
|
|
31
53
|
content: [
|
|
32
54
|
{
|
|
@@ -57,8 +79,17 @@ export const addNoteInputSchema = {
|
|
|
57
79
|
content: z
|
|
58
80
|
.string()
|
|
59
81
|
.min(1)
|
|
60
|
-
.describe('Durable Markdown memory. Include explicit [[wiki links]] and #tags when the memory should be connected.'),
|
|
61
|
-
|
|
82
|
+
.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.'),
|
|
83
|
+
...agentInput,
|
|
84
|
+
allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.'),
|
|
85
|
+
autoIndex: z.boolean().optional().default(true).describe('Reindex vault after writing note.')
|
|
86
|
+
};
|
|
87
|
+
export const addFileInputSchema = {
|
|
88
|
+
...vaultInput,
|
|
89
|
+
...agentInput,
|
|
90
|
+
title: z.string().min(1).optional().describe('Optional note title override. If omitted, uses file name.'),
|
|
91
|
+
filePath: z.string().min(1).describe('Filesystem path to markdown or text file to ingest.'),
|
|
92
|
+
autoIndex: z.boolean().optional().default(true).describe('Reindex vault after ingesting file.'),
|
|
62
93
|
allowSensitive: z.boolean().optional().default(false).describe('Allow content that looks like a secret.')
|
|
63
94
|
};
|
|
64
95
|
export const indexInputSchema = {
|
|
@@ -80,26 +111,36 @@ export const orphansInputSchema = {
|
|
|
80
111
|
...vaultInput,
|
|
81
112
|
...agentInput
|
|
82
113
|
};
|
|
114
|
+
export const statsInputSchema = {
|
|
115
|
+
...vaultInput,
|
|
116
|
+
...agentInput
|
|
117
|
+
};
|
|
118
|
+
export const syncInputSchema = {
|
|
119
|
+
...vaultInput,
|
|
120
|
+
...agentInput,
|
|
121
|
+
contextQuery: z.string().min(1).optional().describe('Optional context smoke query. Omit to skip context probe.'),
|
|
122
|
+
mode: z.enum(['fts', 'semantic', 'hybrid']).optional().describe('Search mode for the optional context probe. Defaults to config value.'),
|
|
123
|
+
contextLimit: positiveInteger(12).describe('Context smoke result limit when contextQuery is provided.'),
|
|
124
|
+
contextTokens: positiveInteger(2000).describe('Context smoke token target when contextQuery is provided.')
|
|
125
|
+
};
|
|
83
126
|
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);
|
|
127
|
+
const context = await resolveExecutionContext(input);
|
|
128
|
+
const mode = sanitizeSearchMode(input.mode, context.config.defaultSearchMode);
|
|
129
|
+
const contextPackage = await buildContextPackage(context.vault, input.query, input.limit, input.tokens, context.agent, mode);
|
|
88
130
|
return jsonResult({
|
|
89
|
-
vault,
|
|
90
|
-
agent:
|
|
131
|
+
vault: context.vault,
|
|
132
|
+
agent: context.agent,
|
|
91
133
|
mode,
|
|
92
134
|
...contextPackage
|
|
93
135
|
});
|
|
94
136
|
};
|
|
95
137
|
export const searchTool = async (input) => {
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const results = await searchKnowledge(vault, input.query, input.limit, input.agent, mode);
|
|
138
|
+
const context = await resolveExecutionContext(input);
|
|
139
|
+
const mode = sanitizeSearchMode(input.mode, context.config.defaultSearchMode);
|
|
140
|
+
const results = await searchKnowledge(context.vault, input.query, input.limit, context.agent, mode);
|
|
100
141
|
return jsonResult({
|
|
101
|
-
vault,
|
|
102
|
-
agent:
|
|
142
|
+
vault: context.vault,
|
|
143
|
+
agent: context.agent,
|
|
103
144
|
query: input.query,
|
|
104
145
|
limit: input.limit,
|
|
105
146
|
mode,
|
|
@@ -107,60 +148,121 @@ export const searchTool = async (input) => {
|
|
|
107
148
|
});
|
|
108
149
|
};
|
|
109
150
|
export const addNoteTool = async (input) => {
|
|
110
|
-
const
|
|
111
|
-
const
|
|
151
|
+
const context = await resolveExecutionContext(input);
|
|
152
|
+
const shouldIndex = isTruthy(input.autoIndex);
|
|
153
|
+
const path = await addNote(context.vault, input.title, input.content, context.agent, {
|
|
112
154
|
allowSensitive: input.allowSensitive
|
|
113
155
|
});
|
|
114
|
-
const index = await indexVault(vault);
|
|
156
|
+
const index = shouldIndex ? await indexVault(context.vault) : undefined;
|
|
115
157
|
return jsonResult({
|
|
116
|
-
vault,
|
|
158
|
+
vault: context.vault,
|
|
117
159
|
title: input.title,
|
|
118
|
-
agent:
|
|
160
|
+
agent: context.agent,
|
|
119
161
|
path,
|
|
120
|
-
index
|
|
162
|
+
...(index ? { index } : {})
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
export const addFileTool = async (input) => {
|
|
166
|
+
const context = await resolveExecutionContext(input);
|
|
167
|
+
const content = await readFile(input.filePath, 'utf8');
|
|
168
|
+
const inferredTitle = inferTitleFromPath(input.filePath);
|
|
169
|
+
const title = input.title ?? inferredTitle;
|
|
170
|
+
if (title == null || title.length === 0) {
|
|
171
|
+
throw new Error('Cannot infer note title from file path. Provide a title explicitly.');
|
|
172
|
+
}
|
|
173
|
+
const shouldIndex = isTruthy(input.autoIndex);
|
|
174
|
+
const path = await addNote(context.vault, title, content, context.agent, {
|
|
175
|
+
allowSensitive: input.allowSensitive
|
|
176
|
+
});
|
|
177
|
+
const index = shouldIndex ? await indexVault(context.vault) : undefined;
|
|
178
|
+
return jsonResult({
|
|
179
|
+
vault: context.vault,
|
|
180
|
+
title,
|
|
181
|
+
agent: context.agent,
|
|
182
|
+
filePath: input.filePath,
|
|
183
|
+
path,
|
|
184
|
+
...(index ? { index } : {})
|
|
121
185
|
});
|
|
122
186
|
};
|
|
123
187
|
export const indexTool = async (input) => {
|
|
124
|
-
const
|
|
125
|
-
const result = await indexVault(vault);
|
|
188
|
+
const context = await resolveExecutionContext(input);
|
|
189
|
+
const result = await indexVault(context.vault);
|
|
126
190
|
return jsonResult({
|
|
127
|
-
vault,
|
|
191
|
+
vault: context.vault,
|
|
128
192
|
...result
|
|
129
193
|
});
|
|
130
194
|
};
|
|
131
195
|
export const validateTool = async (input) => {
|
|
132
|
-
const
|
|
133
|
-
const validation = await validateVault(vault,
|
|
196
|
+
const context = await resolveExecutionContext(input);
|
|
197
|
+
const validation = await validateVault(context.vault, context.agent);
|
|
134
198
|
return jsonResult({
|
|
135
|
-
vault,
|
|
136
|
-
agent:
|
|
199
|
+
vault: context.vault,
|
|
200
|
+
agent: context.agent,
|
|
137
201
|
...validation
|
|
138
202
|
});
|
|
139
203
|
};
|
|
140
204
|
export const graphTool = async (input) => {
|
|
141
|
-
const
|
|
142
|
-
const graph = await getGraph(vault,
|
|
205
|
+
const context = await resolveExecutionContext(input);
|
|
206
|
+
const graph = await getGraph(context.vault, context.agent);
|
|
143
207
|
return jsonResult({
|
|
144
|
-
vault,
|
|
145
|
-
agent:
|
|
208
|
+
vault: context.vault,
|
|
209
|
+
agent: context.agent,
|
|
146
210
|
...graph
|
|
147
211
|
});
|
|
148
212
|
};
|
|
149
213
|
export const brokenLinksTool = async (input) => {
|
|
150
|
-
const
|
|
151
|
-
const brokenLinks = await getBrokenLinksReport(vault,
|
|
214
|
+
const context = await resolveExecutionContext(input);
|
|
215
|
+
const brokenLinks = await getBrokenLinksReport(context.vault, context.agent);
|
|
152
216
|
return jsonResult({
|
|
153
|
-
vault,
|
|
154
|
-
agent:
|
|
217
|
+
vault: context.vault,
|
|
218
|
+
agent: context.agent,
|
|
155
219
|
brokenLinks
|
|
156
220
|
});
|
|
157
221
|
};
|
|
158
222
|
export const orphansTool = async (input) => {
|
|
159
|
-
const
|
|
160
|
-
const orphans = await getOrphansReport(vault,
|
|
223
|
+
const context = await resolveExecutionContext(input);
|
|
224
|
+
const orphans = await getOrphansReport(context.vault, context.agent);
|
|
161
225
|
return jsonResult({
|
|
162
|
-
vault,
|
|
163
|
-
agent:
|
|
226
|
+
vault: context.vault,
|
|
227
|
+
agent: context.agent,
|
|
164
228
|
orphans
|
|
165
229
|
});
|
|
166
230
|
};
|
|
231
|
+
export const statsTool = async (input) => {
|
|
232
|
+
const context = await resolveExecutionContext(input);
|
|
233
|
+
const stats = await getStats(context.vault, context.agent);
|
|
234
|
+
return jsonResult({
|
|
235
|
+
vault: context.vault,
|
|
236
|
+
agent: context.agent,
|
|
237
|
+
stats
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
export const syncTool = async (input) => {
|
|
241
|
+
const context = await resolveExecutionContext(input);
|
|
242
|
+
const index = await indexVault(context.vault);
|
|
243
|
+
const stats = await getStats(context.vault, context.agent);
|
|
244
|
+
const validation = await validateVault(context.vault, context.agent);
|
|
245
|
+
const brokenLinks = await getBrokenLinksReport(context.vault, context.agent);
|
|
246
|
+
const orphans = await getOrphansReport(context.vault, context.agent);
|
|
247
|
+
const response = {
|
|
248
|
+
vault: context.vault,
|
|
249
|
+
agent: context.agent,
|
|
250
|
+
index,
|
|
251
|
+
stats,
|
|
252
|
+
validation,
|
|
253
|
+
brokenLinks,
|
|
254
|
+
orphans
|
|
255
|
+
};
|
|
256
|
+
if (!input.contextQuery) {
|
|
257
|
+
return jsonResult(response);
|
|
258
|
+
}
|
|
259
|
+
const mode = sanitizeSearchMode(input.mode, context.config.defaultSearchMode);
|
|
260
|
+
const contextPackage = await buildContextPackage(context.vault, input.contextQuery, input.contextLimit, input.contextTokens, context.agent, mode);
|
|
261
|
+
return jsonResult({
|
|
262
|
+
...response,
|
|
263
|
+
context: {
|
|
264
|
+
mode,
|
|
265
|
+
...contextPackage
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
};
|