@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.
@@ -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 = resolveVaultPath(vaultPath);
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 (lower(links.to_title) = lower(?) OR lower(target.title) = lower(?))
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 ? [title, title, normalizedAgentId] : [title, title]));
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 = 4;
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('PRAGMA foreign_keys = ON;');
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),
@@ -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.string().min(1).optional().describe('Agent memory namespace. Omit to read shared/default indexed memory.')
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 resolveVault = async (vault) => {
32
+ const resolveExecutionContext = async (input) => {
27
33
  const config = await loadBrainlinkConfig();
28
- return assertVaultAllowed(vault ?? config.vault, config.allowedVaults);
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
- agent: z.string().min(1).optional().default('shared').describe('Agent memory namespace. Defaults to shared.'),
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 vault = await resolveVault(input.vault);
85
- const config = await loadBrainlinkConfig();
86
- const mode = sanitizeSearchMode(input.mode, config.defaultSearchMode);
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: input.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 vault = await resolveVault(input.vault);
97
- const config = await loadBrainlinkConfig();
98
- const mode = sanitizeSearchMode(input.mode, config.defaultSearchMode);
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: input.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 vault = await resolveVault(input.vault);
111
- const path = await addNote(vault, input.title, input.content, input.agent, {
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: input.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 vault = await resolveVault(input.vault);
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 vault = await resolveVault(input.vault);
133
- const validation = await validateVault(vault, input.agent);
196
+ const context = await resolveExecutionContext(input);
197
+ const validation = await validateVault(context.vault, context.agent);
134
198
  return jsonResult({
135
- vault,
136
- agent: input.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 vault = await resolveVault(input.vault);
142
- const graph = await getGraph(vault, input.agent);
205
+ const context = await resolveExecutionContext(input);
206
+ const graph = await getGraph(context.vault, context.agent);
143
207
  return jsonResult({
144
- vault,
145
- agent: input.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 vault = await resolveVault(input.vault);
151
- const brokenLinks = await getBrokenLinksReport(vault, input.agent);
214
+ const context = await resolveExecutionContext(input);
215
+ const brokenLinks = await getBrokenLinksReport(context.vault, context.agent);
152
216
  return jsonResult({
153
- vault,
154
- agent: input.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 vault = await resolveVault(input.vault);
160
- const orphans = await getOrphansReport(vault, input.agent);
223
+ const context = await resolveExecutionContext(input);
224
+ const orphans = await getOrphansReport(context.vault, context.agent);
161
225
  return jsonResult({
162
- vault,
163
- agent: input.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
+ };