@andespindola/brainlink 0.1.0-beta.2 → 0.1.0-beta.20
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 +5 -5
- package/CHANGELOG.md +50 -2
- package/CONTRIBUTING.md +2 -2
- package/README.md +157 -20
- package/SECURITY.md +1 -1
- package/dist/application/add-note.js +62 -13
- package/dist/application/analyze-vault.js +95 -8
- package/dist/application/frontend/client-css.js +190 -99
- package/dist/application/frontend/client-html.js +57 -45
- package/dist/application/frontend/client-js.js +416 -85
- package/dist/application/get-graph-layout.js +22 -7
- package/dist/application/get-graph-node.js +12 -0
- package/dist/application/get-graph-summary.js +12 -0
- package/dist/application/get-graph.js +3 -3
- package/dist/application/index-vault.js +11 -4
- package/dist/application/list-agents.js +3 -3
- package/dist/application/list-links.js +5 -5
- package/dist/application/migrate-vault.js +91 -0
- package/dist/application/search-graph-node-ids.js +12 -0
- package/dist/application/search-knowledge.js +75 -5
- package/dist/application/server/routes.js +27 -1
- package/dist/benchmarks/large-vault.js +1 -1
- package/dist/cli/commands/agent-commands.js +412 -0
- package/dist/cli/commands/config-commands.js +167 -0
- package/dist/cli/commands/read-commands.js +25 -8
- package/dist/cli/commands/write-commands.js +173 -4
- package/dist/cli/main.js +4 -0
- package/dist/cli/runtime.js +5 -2
- package/dist/domain/context.js +53 -11
- package/dist/domain/embeddings.js +2 -1
- package/dist/domain/graph-layout.js +20 -14
- package/dist/domain/markdown.js +36 -4
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +94 -8
- package/dist/infrastructure/file-index.js +294 -0
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/paths.js +9 -1
- package/dist/infrastructure/private-pack-codec.js +73 -0
- package/dist/infrastructure/search-packs.js +348 -0
- package/dist/infrastructure/session-state.js +172 -0
- package/dist/mcp/main.js +11 -3
- package/dist/mcp/server.js +17 -2
- package/dist/mcp/startup.js +35 -0
- package/dist/mcp/tools.js +571 -19
- package/docs/AGENT_USAGE.md +99 -15
- package/docs/ARCHITECTURE.md +37 -26
- package/docs/QUICKSTART.md +104 -0
- package/docs/RELEASE.md +3 -3
- package/package.json +1 -3
- package/dist/infrastructure/sqlite/document-writer.js +0 -51
- package/dist/infrastructure/sqlite/graph-reader.js +0 -120
- package/dist/infrastructure/sqlite/schema.js +0 -111
- package/dist/infrastructure/sqlite/search-reader.js +0 -156
- package/dist/infrastructure/sqlite/types.js +0 -1
- package/dist/infrastructure/sqlite-index.js +0 -25
|
@@ -1,26 +1,41 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
1
3
|
import { createCauliflowerGraphLayout } from '../domain/graph-layout.js';
|
|
2
|
-
import {
|
|
4
|
+
import { indexStoragePath } from '../infrastructure/file-index.js';
|
|
5
|
+
import { getGraphSummary } from './get-graph-summary.js';
|
|
3
6
|
const graphLayoutCache = new Map();
|
|
7
|
+
const readDatabaseSignature = async (vaultPath) => {
|
|
8
|
+
try {
|
|
9
|
+
const info = await stat(indexStoragePath(vaultPath));
|
|
10
|
+
return `${Math.floor(info.mtimeMs)}:${info.size}`;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return '0:0';
|
|
14
|
+
}
|
|
15
|
+
};
|
|
4
16
|
const createGraphSignature = (graph) => {
|
|
5
17
|
const nodesSignature = graph.nodes.map((node) => `${node.id}|${node.agentId}|${node.title}|${node.path}`).join('\n');
|
|
6
18
|
const edgesSignature = graph.edges
|
|
7
19
|
.map((edge) => `${edge.source}|${edge.target ?? ''}|${edge.targetTitle}|${edge.weight}|${edge.priority}`)
|
|
8
20
|
.join('\n');
|
|
9
|
-
return
|
|
21
|
+
return createHash('sha256')
|
|
22
|
+
.update(`${graph.nodes.length}:${nodesSignature}|${graph.edges.length}:${edgesSignature}`)
|
|
23
|
+
.digest('hex');
|
|
10
24
|
};
|
|
11
25
|
export const getGraphLayout = async (vaultPath, agentId) => {
|
|
12
|
-
const
|
|
13
|
-
const signature = createGraphSignature(graph);
|
|
26
|
+
const databaseSignature = await readDatabaseSignature(vaultPath);
|
|
14
27
|
const cacheKey = `${vaultPath}:${agentId ?? ''}`;
|
|
15
28
|
const cached = graphLayoutCache.get(cacheKey);
|
|
16
|
-
if (cached?.
|
|
29
|
+
if (cached?.databaseSignature === databaseSignature) {
|
|
17
30
|
return {
|
|
18
|
-
signature,
|
|
31
|
+
signature: cached.signature,
|
|
19
32
|
layout: cached.layout
|
|
20
33
|
};
|
|
21
34
|
}
|
|
35
|
+
const graph = await getGraphSummary(vaultPath, agentId);
|
|
36
|
+
const signature = createGraphSignature(graph);
|
|
22
37
|
const layout = createCauliflowerGraphLayout(graph);
|
|
23
|
-
graphLayoutCache.set(cacheKey, { signature, layout });
|
|
38
|
+
graphLayoutCache.set(cacheKey, { databaseSignature, signature, layout });
|
|
24
39
|
return {
|
|
25
40
|
signature,
|
|
26
41
|
layout
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
|
+
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
3
|
+
export const getGraphNode = async (vaultPath, id, agentId) => {
|
|
4
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
5
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
6
|
+
try {
|
|
7
|
+
return await index.getGraphNode(id, agentId);
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
index.close();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
|
+
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
3
|
+
export const getGraphSummary = async (vaultPath, agentId) => {
|
|
4
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
5
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
6
|
+
try {
|
|
7
|
+
return await index.getGraphSummary(agentId);
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
index.close();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
|
-
import {
|
|
2
|
+
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
3
3
|
export const getGraph = async (vaultPath, agentId) => {
|
|
4
4
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
5
|
-
const index =
|
|
5
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
6
6
|
try {
|
|
7
|
-
return index.getGraph(agentId);
|
|
7
|
+
return await index.getGraph(agentId);
|
|
8
8
|
}
|
|
9
9
|
finally {
|
|
10
10
|
index.close();
|
|
@@ -3,7 +3,8 @@ import { sharedAgentId } from '../domain/agents.js';
|
|
|
3
3
|
import { createEmbeddingProvider } from '../domain/embeddings.js';
|
|
4
4
|
import { loadBrainlinkConfig } from '../infrastructure/config.js';
|
|
5
5
|
import { ensureVault, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
|
|
6
|
-
import {
|
|
6
|
+
import { buildSearchPacks } from '../infrastructure/search-packs.js';
|
|
7
|
+
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
7
8
|
const toTitleKey = (title) => title.toLowerCase();
|
|
8
9
|
const appendTitleEntry = (map, document) => {
|
|
9
10
|
const key = toTitleKey(document.title);
|
|
@@ -59,10 +60,16 @@ export const indexVault = async (vaultPath) => {
|
|
|
59
60
|
}));
|
|
60
61
|
const titleMaps = createTitleMaps(documents);
|
|
61
62
|
const indexedDocuments = await embedIndexedDocuments(documents.map((document) => createIndexedDocument(document, createScopedTitleResolver(document, titleMaps), config.chunkSize)), config.embeddingProvider);
|
|
62
|
-
const index =
|
|
63
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
63
64
|
try {
|
|
64
|
-
index.reset();
|
|
65
|
-
index.saveDocuments(indexedDocuments);
|
|
65
|
+
await index.reset();
|
|
66
|
+
await index.saveDocuments(indexedDocuments);
|
|
67
|
+
try {
|
|
68
|
+
await buildSearchPacks(absoluteVaultPath, indexedDocuments);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Pack generation is best-effort. The JSON index remains the primary path.
|
|
72
|
+
}
|
|
66
73
|
return {
|
|
67
74
|
documentCount: indexedDocuments.length,
|
|
68
75
|
chunkCount: indexedDocuments.reduce((total, document) => total + document.chunks.length, 0),
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
|
-
import {
|
|
2
|
+
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
3
3
|
export const listAgents = async (vaultPath) => {
|
|
4
4
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
5
|
-
const index =
|
|
5
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
6
6
|
try {
|
|
7
|
-
return index.listAgents();
|
|
7
|
+
return await index.listAgents();
|
|
8
8
|
}
|
|
9
9
|
finally {
|
|
10
10
|
index.close();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
|
-
import {
|
|
2
|
+
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
3
3
|
export const listLinks = async (vaultPath, agentId) => {
|
|
4
4
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
5
|
-
const index =
|
|
5
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
6
6
|
try {
|
|
7
|
-
return index.listLinks(agentId);
|
|
7
|
+
return await index.listLinks(agentId);
|
|
8
8
|
}
|
|
9
9
|
finally {
|
|
10
10
|
index.close();
|
|
@@ -12,9 +12,9 @@ export const listLinks = async (vaultPath, agentId) => {
|
|
|
12
12
|
};
|
|
13
13
|
export const listBacklinks = async (vaultPath, title, agentId) => {
|
|
14
14
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
15
|
-
const index =
|
|
15
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
16
16
|
try {
|
|
17
|
-
return index.listBacklinks(title, agentId);
|
|
17
|
+
return await index.listBacklinks(title, agentId);
|
|
18
18
|
}
|
|
19
19
|
finally {
|
|
20
20
|
index.close();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, extname, isAbsolute, join, relative } from 'node:path';
|
|
3
|
+
import { ensureVault, isBucketVaultPath, listVaultFiles, resolveVaultPath, writeMarkdownFile } from '../infrastructure/file-system-vault.js';
|
|
4
|
+
const directoryMode = 0o700;
|
|
5
|
+
const fileMode = 0o600;
|
|
6
|
+
const isMarkdownPath = (path) => extname(path).toLowerCase() === '.md';
|
|
7
|
+
const timestamp = () => new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, 'Z');
|
|
8
|
+
const isPathInside = (parent, child) => {
|
|
9
|
+
const path = relative(parent, child);
|
|
10
|
+
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
11
|
+
};
|
|
12
|
+
const conflictPath = (targetPath) => {
|
|
13
|
+
const extension = extname(targetPath);
|
|
14
|
+
const base = extension ? targetPath.slice(0, -extension.length) : targetPath;
|
|
15
|
+
return `${base}.conflict-${timestamp()}${extension}`;
|
|
16
|
+
};
|
|
17
|
+
const writePreservedFile = async (absolutePath, content) => {
|
|
18
|
+
await mkdir(dirname(absolutePath), { recursive: true, mode: directoryMode });
|
|
19
|
+
await writeFile(absolutePath, content, { mode: fileMode });
|
|
20
|
+
await chmod(absolutePath, fileMode);
|
|
21
|
+
};
|
|
22
|
+
const writeMigratedFile = async (targetVault, targetRoot, absolutePath, content) => {
|
|
23
|
+
if (isBucketVaultPath(targetVault)) {
|
|
24
|
+
await writeMarkdownFile(targetVault, relative(targetRoot, absolutePath), content.toString('utf8'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await writePreservedFile(absolutePath, content);
|
|
28
|
+
};
|
|
29
|
+
export const planVaultMigration = async (source, target) => {
|
|
30
|
+
const sourceFiles = (await listVaultFiles(source)).filter(isMarkdownPath);
|
|
31
|
+
return sourceFiles.reduce(async (statePromise, sourceFile) => {
|
|
32
|
+
const state = await statePromise;
|
|
33
|
+
const targetFile = join(target, relative(source, sourceFile));
|
|
34
|
+
if (!isPathInside(target, targetFile)) {
|
|
35
|
+
return state;
|
|
36
|
+
}
|
|
37
|
+
const sourceContent = await readFile(sourceFile);
|
|
38
|
+
try {
|
|
39
|
+
const targetContent = await readFile(targetFile);
|
|
40
|
+
if (sourceContent.equals(targetContent)) {
|
|
41
|
+
return [...state, { kind: 'unchanged', sourcePath: sourceFile, targetPath: targetFile, sourceContent }];
|
|
42
|
+
}
|
|
43
|
+
return [...state, { kind: 'conflict', sourcePath: sourceFile, targetPath: conflictPath(targetFile), sourceContent }];
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
return [...state, { kind: 'copy', sourcePath: sourceFile, targetPath: targetFile, sourceContent }];
|
|
50
|
+
}
|
|
51
|
+
}, Promise.resolve([]));
|
|
52
|
+
};
|
|
53
|
+
export const previewVaultMigration = async (sourceVault, targetVault) => {
|
|
54
|
+
const source = await ensureVault(sourceVault);
|
|
55
|
+
const target = await ensureVault(targetVault);
|
|
56
|
+
if (source === target) {
|
|
57
|
+
return { source, target, copied: 0, unchanged: 0, conflicted: 0 };
|
|
58
|
+
}
|
|
59
|
+
const actions = await planVaultMigration(source, target);
|
|
60
|
+
const copied = actions.filter((action) => action.kind === 'copy').length;
|
|
61
|
+
const unchanged = actions.filter((action) => action.kind === 'unchanged').length;
|
|
62
|
+
const conflicted = actions.filter((action) => action.kind === 'conflict').length;
|
|
63
|
+
return { source, target, copied, unchanged, conflicted };
|
|
64
|
+
};
|
|
65
|
+
export const migrateVaultContent = async (sourceVault, targetVault) => {
|
|
66
|
+
const source = await ensureVault(sourceVault);
|
|
67
|
+
const target = await ensureVault(targetVault);
|
|
68
|
+
if (source === target) {
|
|
69
|
+
return { source, target, copied: 0, unchanged: 0, conflicted: 0 };
|
|
70
|
+
}
|
|
71
|
+
const actions = await planVaultMigration(source, target);
|
|
72
|
+
for (const action of actions) {
|
|
73
|
+
if (action.kind === 'unchanged') {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
await writeMigratedFile(targetVault, target, action.targetPath, action.sourceContent);
|
|
77
|
+
}
|
|
78
|
+
const copied = actions.filter((action) => action.kind === 'copy').length;
|
|
79
|
+
const unchanged = actions.filter((action) => action.kind === 'unchanged').length;
|
|
80
|
+
const conflicted = actions.filter((action) => action.kind === 'conflict').length;
|
|
81
|
+
return { source, target, copied, unchanged, conflicted };
|
|
82
|
+
};
|
|
83
|
+
export const shouldMigrateDefaultVault = async (sourceVault, targetVault) => {
|
|
84
|
+
const source = resolveVaultPath(sourceVault);
|
|
85
|
+
const target = resolveVaultPath(targetVault);
|
|
86
|
+
if (source === target) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const [sourceFiles, targetFiles] = await Promise.all([listVaultFiles(source), listVaultFiles(target)]);
|
|
90
|
+
return sourceFiles.filter(isMarkdownPath).length > 0 && targetFiles.filter(isMarkdownPath).length === 0;
|
|
91
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
|
+
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
3
|
+
export const searchGraphNodeIds = async (vaultPath, query, limit, agentId) => {
|
|
4
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
5
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
6
|
+
try {
|
|
7
|
+
return await index.searchGraphNodeIds(query, limit, agentId);
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
index.close();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -1,19 +1,89 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
1
2
|
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
2
|
-
import {
|
|
3
|
+
import { ensurePrivatePacksFromLegacyIndex, searchInPacks } from '../infrastructure/search-packs.js';
|
|
4
|
+
import { indexStoragePath, openFileIndex } from '../infrastructure/file-index.js';
|
|
3
5
|
import { createEmbeddingProvider } from '../domain/embeddings.js';
|
|
4
6
|
import { loadBrainlinkConfig, sanitizeSearchMode } from '../infrastructure/config.js';
|
|
7
|
+
const hybridCacheTtlMs = 30_000;
|
|
8
|
+
const hybridCacheMaxEntries = 200;
|
|
9
|
+
const hybridSearchCache = new Map();
|
|
10
|
+
const readIndexMtimeMs = async (vaultPath) => {
|
|
11
|
+
try {
|
|
12
|
+
return (await stat(indexStoragePath(vaultPath))).mtimeMs;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const toCacheKey = (vaultPath, query, limit, agentId) => JSON.stringify({
|
|
19
|
+
vaultPath,
|
|
20
|
+
query: query.trim().toLowerCase(),
|
|
21
|
+
limit,
|
|
22
|
+
agentId: agentId?.trim().toLowerCase() ?? '*'
|
|
23
|
+
});
|
|
24
|
+
const cacheGet = (key, indexMtimeMs) => {
|
|
25
|
+
const entry = hybridSearchCache.get(key);
|
|
26
|
+
if (!entry) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const fresh = Date.now() - entry.createdAt <= hybridCacheTtlMs && entry.indexMtimeMs === indexMtimeMs;
|
|
30
|
+
if (!fresh) {
|
|
31
|
+
hybridSearchCache.delete(key);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return entry.results;
|
|
35
|
+
};
|
|
36
|
+
const cacheSet = (entry) => {
|
|
37
|
+
hybridSearchCache.set(entry.key, entry);
|
|
38
|
+
if (hybridSearchCache.size <= hybridCacheMaxEntries) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const overflow = hybridSearchCache.size - hybridCacheMaxEntries;
|
|
42
|
+
const keys = Array.from(hybridSearchCache.keys()).slice(0, overflow);
|
|
43
|
+
keys.forEach((key) => hybridSearchCache.delete(key));
|
|
44
|
+
};
|
|
5
45
|
export const searchKnowledge = async (vaultPath, query, limit, agentId, mode) => {
|
|
6
46
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
7
47
|
const config = await loadBrainlinkConfig();
|
|
8
48
|
const searchMode = sanitizeSearchMode(mode, config.defaultSearchMode);
|
|
49
|
+
await ensurePrivatePacksFromLegacyIndex(absoluteVaultPath);
|
|
50
|
+
const cacheKey = searchMode === 'hybrid' ? toCacheKey(absoluteVaultPath, query, limit, agentId) : undefined;
|
|
51
|
+
const indexMtimeMs = cacheKey ? await readIndexMtimeMs(absoluteVaultPath) : 0;
|
|
52
|
+
const cached = cacheKey ? cacheGet(cacheKey, indexMtimeMs) : undefined;
|
|
53
|
+
if (cached) {
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
9
56
|
const provider = createEmbeddingProvider(config.embeddingProvider);
|
|
10
57
|
const shouldEmbedQuery = searchMode !== 'fts' && provider.name !== 'none';
|
|
11
58
|
const queryEmbedding = shouldEmbedQuery ? (await provider.embed([query]))[0] ?? [] : [];
|
|
12
|
-
const index = openSqliteIndex(absoluteVaultPath);
|
|
13
59
|
try {
|
|
14
|
-
|
|
60
|
+
const index = openFileIndex(absoluteVaultPath);
|
|
61
|
+
try {
|
|
62
|
+
const results = await index.search(query, limit, agentId, searchMode, queryEmbedding);
|
|
63
|
+
if (cacheKey) {
|
|
64
|
+
cacheSet({
|
|
65
|
+
key: cacheKey,
|
|
66
|
+
createdAt: Date.now(),
|
|
67
|
+
indexMtimeMs,
|
|
68
|
+
results
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
index.close();
|
|
75
|
+
}
|
|
15
76
|
}
|
|
16
|
-
|
|
17
|
-
|
|
77
|
+
catch {
|
|
78
|
+
const fallbackResults = await searchInPacks(absoluteVaultPath, query, limit, agentId);
|
|
79
|
+
if (cacheKey) {
|
|
80
|
+
cacheSet({
|
|
81
|
+
key: cacheKey,
|
|
82
|
+
createdAt: Date.now(),
|
|
83
|
+
indexMtimeMs,
|
|
84
|
+
results: fallbackResults
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return fallbackResults;
|
|
18
88
|
}
|
|
19
89
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { getBrokenLinksReport, getOrphansReport, getStats, validateVault } from '../analyze-vault.js';
|
|
2
2
|
import { buildContextPackage } from '../build-context.js';
|
|
3
3
|
import { getGraph } from '../get-graph.js';
|
|
4
|
+
import { getGraphNode } from '../get-graph-node.js';
|
|
4
5
|
import { getGraphLayout } from '../get-graph-layout.js';
|
|
5
6
|
import { listAgents } from '../list-agents.js';
|
|
6
7
|
import { listBacklinks, listLinks } from '../list-links.js';
|
|
8
|
+
import { searchGraphNodeIds } from '../search-graph-node-ids.js';
|
|
7
9
|
import { searchKnowledge } from '../search-knowledge.js';
|
|
8
10
|
import { loadBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/config.js';
|
|
9
11
|
import { createClientCss } from '../frontend/client-css.js';
|
|
@@ -49,6 +51,10 @@ const sameEntityTag = (candidate, signature) => {
|
|
|
49
51
|
return decodeEntityTag(candidate) === signature;
|
|
50
52
|
};
|
|
51
53
|
const readAgentQuery = (url) => url.searchParams.get('agent') ?? undefined;
|
|
54
|
+
const stripLayoutContent = (layout) => ({
|
|
55
|
+
...layout,
|
|
56
|
+
nodes: layout.nodes.map(({ content, ...node }) => node)
|
|
57
|
+
});
|
|
52
58
|
export const route = async (request, url, vaultPath) => {
|
|
53
59
|
if (isReadMethod(request) && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
54
60
|
return createResponse(createClientHtml(), 200, contentTypes['.html']);
|
|
@@ -67,7 +73,7 @@ export const route = async (request, url, vaultPath) => {
|
|
|
67
73
|
const requestEtags = request.headers['if-none-match'];
|
|
68
74
|
const notModified = sameEntityTag(requestEtags, signature);
|
|
69
75
|
const etag = encodeEntityTag(signature);
|
|
70
|
-
const body = createJsonResponse({ signature, layout });
|
|
76
|
+
const body = createJsonResponse({ signature, layout: stripLayoutContent(layout) });
|
|
71
77
|
const jsonResponse = createResponse(body, 200, contentTypes['.json']);
|
|
72
78
|
const notModifiedResponse = createResponse('', 304, contentTypes['.json']);
|
|
73
79
|
if (notModified) {
|
|
@@ -87,6 +93,26 @@ export const route = async (request, url, vaultPath) => {
|
|
|
87
93
|
}
|
|
88
94
|
};
|
|
89
95
|
}
|
|
96
|
+
if (isReadMethod(request) && url.pathname === '/api/graph-node') {
|
|
97
|
+
const id = url.searchParams.get('id')?.trim() ?? '';
|
|
98
|
+
if (!id) {
|
|
99
|
+
return createResponse(createJsonResponse({ error: 'Missing id query parameter' }), 400, contentTypes['.json']);
|
|
100
|
+
}
|
|
101
|
+
const node = await getGraphNode(vaultPath, id, readAgentQuery(url));
|
|
102
|
+
if (!node) {
|
|
103
|
+
return createResponse(createJsonResponse({ error: 'Node not found' }), 404, contentTypes['.json']);
|
|
104
|
+
}
|
|
105
|
+
return createResponse(createJsonResponse({ node }), 200, contentTypes['.json']);
|
|
106
|
+
}
|
|
107
|
+
if (isReadMethod(request) && url.pathname === '/api/graph-filter') {
|
|
108
|
+
const query = url.searchParams.get('q')?.trim() ?? '';
|
|
109
|
+
const limit = parsePositiveInteger(url.searchParams.get('limit'), 1200);
|
|
110
|
+
if (!query) {
|
|
111
|
+
return createResponse(createJsonResponse({ query, nodeIds: [] }), 200, contentTypes['.json']);
|
|
112
|
+
}
|
|
113
|
+
const nodeIds = await searchGraphNodeIds(vaultPath, query, limit, readAgentQuery(url));
|
|
114
|
+
return createResponse(createJsonResponse({ query, nodeIds }), 200, contentTypes['.json']);
|
|
115
|
+
}
|
|
90
116
|
if (isReadMethod(request) && url.pathname === '/api/agents') {
|
|
91
117
|
return createResponse(createJsonResponse({ agents: await listAgents(vaultPath) }), 200, contentTypes['.json']);
|
|
92
118
|
}
|
|
@@ -21,7 +21,7 @@ const readOptions = (args) => ({
|
|
|
21
21
|
});
|
|
22
22
|
const topics = [
|
|
23
23
|
'authentication jwt token refresh policy',
|
|
24
|
-
'
|
|
24
|
+
'graph backlinks markdown vault indexing',
|
|
25
25
|
'frontend canvas layout graph interaction',
|
|
26
26
|
'agent memory context retrieval summarization',
|
|
27
27
|
'security local server vault path allowlist',
|