@andespindola/brainlink 0.1.0-beta.40 → 0.1.0-beta.42
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/CHANGELOG.md +7 -0
- package/README.md +10 -0
- package/dist/application/frontend/client-js.js +667 -68
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/index-vault.js +137 -21
- package/dist/application/server/routes.js +36 -1
- package/dist/application/start-server.js +75 -4
- package/dist/cli/commands/write-commands.js +41 -12
- package/dist/infrastructure/file-index.js +30 -0
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/index-state.js +50 -0
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const createClientWorkerJs = () => `const normalize = value => String(value || '')
|
|
2
|
+
.normalize('NFKD')
|
|
3
|
+
.replace(/\\p{Diacritic}/gu, '')
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
|
|
6
|
+
let nodeIndex = []
|
|
7
|
+
|
|
8
|
+
const toNodeIndex = nodes =>
|
|
9
|
+
(Array.isArray(nodes) ? nodes : [])
|
|
10
|
+
.map(node => {
|
|
11
|
+
const id = typeof node.id === 'string' ? node.id : ''
|
|
12
|
+
if (!id) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
const title = normalize(node.title)
|
|
16
|
+
const path = normalize(node.path)
|
|
17
|
+
const tags = Array.isArray(node.tags) ? node.tags.map(tag => normalize(tag)) : []
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
text: [title, path, ...tags].join(' ')
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
|
|
25
|
+
const scoreText = (text, query) => {
|
|
26
|
+
if (!query) return 0
|
|
27
|
+
if (!text.includes(query)) return 0
|
|
28
|
+
if (text.startsWith(query)) return 4
|
|
29
|
+
return 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const filterIds = (query, limit) => {
|
|
33
|
+
const normalizedQuery = normalize(query).trim()
|
|
34
|
+
if (!normalizedQuery) {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
const rows = []
|
|
38
|
+
for (let index = 0; index < nodeIndex.length; index += 1) {
|
|
39
|
+
const row = nodeIndex[index]
|
|
40
|
+
const score = scoreText(row.text, normalizedQuery)
|
|
41
|
+
if (score > 0) {
|
|
42
|
+
rows.push({ id: row.id, score })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
rows.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))
|
|
46
|
+
return rows.slice(0, Math.max(1, Number.isFinite(limit) ? limit : rows.length)).map(row => row.id)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
self.onmessage = event => {
|
|
50
|
+
const payload = event.data
|
|
51
|
+
if (!payload || typeof payload !== 'object') {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if (payload.type === 'load-nodes') {
|
|
55
|
+
nodeIndex = toNodeIndex(payload.nodes)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if (payload.type === 'filter') {
|
|
59
|
+
const token = payload.token
|
|
60
|
+
const ids = filterIds(payload.query, payload.limit)
|
|
61
|
+
self.postMessage({ type: 'filter-result', token, ids })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
self.postMessage({ type: 'ready' })
|
|
66
|
+
`;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { createIndexedDocument, parseMarkdownDocument } from '../domain/markdown.js';
|
|
2
4
|
import { sharedAgentId } from '../domain/agents.js';
|
|
3
5
|
import { createEmbeddingProvider } from '../domain/embeddings.js';
|
|
4
6
|
import { loadBrainlinkConfig } from '../infrastructure/config.js';
|
|
5
|
-
import { ensureVault,
|
|
7
|
+
import { ensureVault, readMarkdownFileSummaries } from '../infrastructure/file-system-vault.js';
|
|
8
|
+
import { readIndexState, writeIndexState } from '../infrastructure/index-state.js';
|
|
6
9
|
import { buildSearchPacks } from '../infrastructure/search-packs.js';
|
|
7
10
|
import { openFileIndex } from '../infrastructure/file-index.js';
|
|
8
11
|
const toTitleKey = (title) => title.toLowerCase();
|
|
@@ -34,6 +37,9 @@ const createScopedTitleResolver = (document, titleMaps) => ({
|
|
|
34
37
|
get: (title) => titleMaps.byAgent.get(document.agentId)?.get(title)?.id ?? titleMaps.shared.get(title)?.id
|
|
35
38
|
});
|
|
36
39
|
const embedIndexedDocuments = async (documents, providerName) => {
|
|
40
|
+
if (documents.length === 0) {
|
|
41
|
+
return documents;
|
|
42
|
+
}
|
|
37
43
|
const provider = createEmbeddingProvider(providerName);
|
|
38
44
|
const chunks = documents.flatMap((document) => document.chunks);
|
|
39
45
|
const embeddings = await provider.embed(chunks.map((chunk) => chunk.content));
|
|
@@ -47,34 +53,144 @@ const embedIndexedDocuments = async (documents, providerName) => {
|
|
|
47
53
|
}))
|
|
48
54
|
}));
|
|
49
55
|
};
|
|
56
|
+
const relinkIndexedDocument = (indexedDocument, titleMaps) => {
|
|
57
|
+
const resolver = createScopedTitleResolver(indexedDocument.document, titleMaps);
|
|
58
|
+
return {
|
|
59
|
+
...indexedDocument,
|
|
60
|
+
links: indexedDocument.links
|
|
61
|
+
.map((link) => ({
|
|
62
|
+
...link,
|
|
63
|
+
toDocumentId: resolver.get(link.toTitle.toLowerCase()) ?? null
|
|
64
|
+
}))
|
|
65
|
+
.filter((link) => link.toDocumentId !== indexedDocument.document.id)
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
const toIndexResult = (documents) => ({
|
|
69
|
+
documentCount: documents.length,
|
|
70
|
+
chunkCount: documents.reduce((total, document) => total + document.chunks.length, 0),
|
|
71
|
+
linkCount: documents.reduce((total, document) => total + document.links.length, 0)
|
|
72
|
+
});
|
|
73
|
+
const toSnapshot = (summaries) => summaries.map((summary) => ({
|
|
74
|
+
path: summary.relativePath,
|
|
75
|
+
mtimeMs: summary.updatedAt.getTime(),
|
|
76
|
+
size: summary.size
|
|
77
|
+
}));
|
|
78
|
+
const createSnapshotMap = (snapshot) => new Map(snapshot.map((entry) => [entry.path, entry]));
|
|
79
|
+
const packManifestPath = (vaultPath) => join(vaultPath, '.brainlink', 'search-packs', 'manifest.json');
|
|
80
|
+
const hasPackManifest = async (vaultPath) => {
|
|
81
|
+
try {
|
|
82
|
+
await stat(packManifestPath(vaultPath));
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const readChangedDocuments = async (absoluteVaultPath, changedSummaries) => {
|
|
90
|
+
const parsed = await Promise.all(changedSummaries.map(async (summary) => parseMarkdownDocument({
|
|
91
|
+
absolutePath: summary.absolutePath,
|
|
92
|
+
vaultPath: absoluteVaultPath,
|
|
93
|
+
content: await readFile(summary.absolutePath, 'utf8'),
|
|
94
|
+
createdAt: summary.createdAt,
|
|
95
|
+
updatedAt: summary.updatedAt
|
|
96
|
+
})));
|
|
97
|
+
return new Map(parsed.map((document) => [document.path, document]));
|
|
98
|
+
};
|
|
50
99
|
export const indexVault = async (vaultPath) => {
|
|
51
100
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
52
101
|
const config = await loadBrainlinkConfig();
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
content: file.content,
|
|
58
|
-
createdAt: file.createdAt,
|
|
59
|
-
updatedAt: file.updatedAt
|
|
60
|
-
}));
|
|
61
|
-
const titleMaps = createTitleMaps(documents);
|
|
62
|
-
const indexedDocuments = await embedIndexedDocuments(documents.map((document) => createIndexedDocument(document, createScopedTitleResolver(document, titleMaps), config.chunkSize)), config.embeddingProvider);
|
|
102
|
+
const [summaries, previousState] = await Promise.all([
|
|
103
|
+
readMarkdownFileSummaries(absoluteVaultPath),
|
|
104
|
+
readIndexState(absoluteVaultPath)
|
|
105
|
+
]);
|
|
63
106
|
const index = openFileIndex(absoluteVaultPath);
|
|
64
107
|
try {
|
|
108
|
+
const existingIndexedDocuments = await index.getIndexedDocuments();
|
|
109
|
+
const existingByPath = new Map(existingIndexedDocuments.map((document) => [document.document.path, document]));
|
|
110
|
+
const currentSnapshot = toSnapshot(summaries);
|
|
111
|
+
const currentSnapshotMap = createSnapshotMap(currentSnapshot);
|
|
112
|
+
const previousSnapshotMap = createSnapshotMap(previousState?.files ?? []);
|
|
113
|
+
const settingsChanged = previousState == null ||
|
|
114
|
+
previousState.chunkSize !== config.chunkSize ||
|
|
115
|
+
previousState.embeddingProvider !== config.embeddingProvider;
|
|
116
|
+
const changedPaths = new Set();
|
|
117
|
+
for (let index = 0; index < summaries.length; index += 1) {
|
|
118
|
+
const summary = summaries[index];
|
|
119
|
+
const previous = previousSnapshotMap.get(summary.relativePath);
|
|
120
|
+
const changed = settingsChanged ||
|
|
121
|
+
previous == null ||
|
|
122
|
+
previous.mtimeMs !== summary.updatedAt.getTime() ||
|
|
123
|
+
previous.size !== summary.size ||
|
|
124
|
+
!existingByPath.has(summary.relativePath);
|
|
125
|
+
if (changed) {
|
|
126
|
+
changedPaths.add(summary.relativePath);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const hasDeletes = previousState
|
|
130
|
+
? previousState.files.some((entry) => !currentSnapshotMap.has(entry.path))
|
|
131
|
+
: false;
|
|
132
|
+
if (changedPaths.size === 0 &&
|
|
133
|
+
!hasDeletes &&
|
|
134
|
+
existingIndexedDocuments.length === summaries.length &&
|
|
135
|
+
previousState != null) {
|
|
136
|
+
return toIndexResult(existingIndexedDocuments);
|
|
137
|
+
}
|
|
138
|
+
const changedSummaries = summaries.filter((summary) => changedPaths.has(summary.relativePath));
|
|
139
|
+
const changedDocumentsByPath = await readChangedDocuments(absoluteVaultPath, changedSummaries);
|
|
140
|
+
const documents = summaries.flatMap((summary) => {
|
|
141
|
+
const changed = changedDocumentsByPath.get(summary.relativePath);
|
|
142
|
+
if (changed) {
|
|
143
|
+
return [changed];
|
|
144
|
+
}
|
|
145
|
+
const existing = existingByPath.get(summary.relativePath);
|
|
146
|
+
return existing ? [existing.document] : [];
|
|
147
|
+
});
|
|
148
|
+
const titleMaps = createTitleMaps(documents);
|
|
149
|
+
const changedIndexedDocuments = changedDocumentsByPath.size > 0
|
|
150
|
+
? await embedIndexedDocuments(Array.from(changedDocumentsByPath.values()).map((document) => createIndexedDocument(document, createScopedTitleResolver(document, titleMaps), config.chunkSize)), config.embeddingProvider)
|
|
151
|
+
: [];
|
|
152
|
+
const changedIndexedByPath = new Map(changedIndexedDocuments.map((document) => [document.document.path, document]));
|
|
153
|
+
const needsRelink = settingsChanged || hasDeletes || changedPaths.size > 0;
|
|
154
|
+
const indexedDocuments = documents.map((document) => {
|
|
155
|
+
const changed = changedIndexedByPath.get(document.path);
|
|
156
|
+
if (changed) {
|
|
157
|
+
return changed;
|
|
158
|
+
}
|
|
159
|
+
const existing = existingByPath.get(document.path);
|
|
160
|
+
if (!existing) {
|
|
161
|
+
return createIndexedDocument(document, createScopedTitleResolver(document, titleMaps), config.chunkSize);
|
|
162
|
+
}
|
|
163
|
+
return needsRelink ? relinkIndexedDocument(existing, titleMaps) : existing;
|
|
164
|
+
});
|
|
65
165
|
await index.reset();
|
|
66
166
|
await index.saveDocuments(indexedDocuments);
|
|
67
|
-
|
|
68
|
-
|
|
167
|
+
const existingPackManifest = await hasPackManifest(absoluteVaultPath);
|
|
168
|
+
const changedCount = changedPaths.size;
|
|
169
|
+
const documentCount = Math.max(indexedDocuments.length, 1);
|
|
170
|
+
const changeRatio = changedCount / documentCount;
|
|
171
|
+
const previousPendingPackChanges = previousState?.pendingPackChanges ?? 0;
|
|
172
|
+
const pendingPackChanges = previousPendingPackChanges + changedCount;
|
|
173
|
+
const shouldRebuildPacks = !existingPackManifest ||
|
|
174
|
+
settingsChanged ||
|
|
175
|
+
hasDeletes ||
|
|
176
|
+
changedCount >= 400 ||
|
|
177
|
+
changeRatio >= 0.04 ||
|
|
178
|
+
pendingPackChanges >= 1200;
|
|
179
|
+
if (shouldRebuildPacks) {
|
|
180
|
+
try {
|
|
181
|
+
await buildSearchPacks(absoluteVaultPath, indexedDocuments);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Pack generation is best-effort. The JSON index remains the primary path.
|
|
185
|
+
}
|
|
69
186
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
};
|
|
187
|
+
await writeIndexState(absoluteVaultPath, {
|
|
188
|
+
chunkSize: config.chunkSize,
|
|
189
|
+
embeddingProvider: config.embeddingProvider,
|
|
190
|
+
files: currentSnapshot,
|
|
191
|
+
pendingPackChanges: shouldRebuildPacks ? 0 : pendingPackChanges
|
|
192
|
+
});
|
|
193
|
+
return toIndexResult(indexedDocuments);
|
|
78
194
|
}
|
|
79
195
|
finally {
|
|
80
196
|
index.close();
|
|
@@ -11,6 +11,7 @@ import { loadBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/co
|
|
|
11
11
|
import { createClientCss } from '../frontend/client-css.js';
|
|
12
12
|
import { createClientHtml } from '../frontend/client-html.js';
|
|
13
13
|
import { createClientJs } from '../frontend/client-js.js';
|
|
14
|
+
import { createClientWorkerJs } from '../frontend/client-worker-js.js';
|
|
14
15
|
import { contentTypes, createJsonResponse, isReadMethod, parsePositiveInteger } from './http.js';
|
|
15
16
|
const readSearchMode = async (url) => {
|
|
16
17
|
const config = await loadBrainlinkConfig();
|
|
@@ -51,10 +52,41 @@ const sameEntityTag = (candidate, signature) => {
|
|
|
51
52
|
return decodeEntityTag(candidate) === signature;
|
|
52
53
|
};
|
|
53
54
|
const readAgentQuery = (url) => url.searchParams.get('agent') ?? undefined;
|
|
55
|
+
const compactGraphLayoutThreshold = 12_000;
|
|
56
|
+
const compactGraphLayoutEdgeLimit = 60_000;
|
|
57
|
+
const compactGraphLayoutEdgeLimitFor = (nodeCount) => {
|
|
58
|
+
if (nodeCount > 100_000)
|
|
59
|
+
return 15_000;
|
|
60
|
+
if (nodeCount > 50_000)
|
|
61
|
+
return 22_000;
|
|
62
|
+
if (nodeCount > 25_000)
|
|
63
|
+
return 30_000;
|
|
64
|
+
return compactGraphLayoutEdgeLimit;
|
|
65
|
+
};
|
|
54
66
|
const stripLayoutContent = (layout) => ({
|
|
55
67
|
...layout,
|
|
56
68
|
nodes: layout.nodes.map(({ content, ...node }) => node)
|
|
57
69
|
});
|
|
70
|
+
const compactLayoutPayload = (layout) => {
|
|
71
|
+
const edgeLimit = compactGraphLayoutEdgeLimitFor(layout.nodes.length);
|
|
72
|
+
const compactNodes = layout.nodes.map((node) => [node.id, node.title, node.x, node.y, node.group, node.segment]);
|
|
73
|
+
const compactEdges = [...layout.edges]
|
|
74
|
+
.sort((left, right) => (right.weight ?? 1) - (left.weight ?? 1))
|
|
75
|
+
.slice(0, edgeLimit)
|
|
76
|
+
.map((edge) => [edge.source, edge.target, edge.weight, edge.priority]);
|
|
77
|
+
return {
|
|
78
|
+
compact: true,
|
|
79
|
+
layout: {
|
|
80
|
+
nodes: compactNodes,
|
|
81
|
+
edges: compactEdges
|
|
82
|
+
},
|
|
83
|
+
totals: {
|
|
84
|
+
nodes: layout.nodes.length,
|
|
85
|
+
edges: layout.edges.length
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
const encodeLayoutPayload = (layout) => layout.nodes.length > compactGraphLayoutThreshold ? compactLayoutPayload(layout) : { compact: false, layout: stripLayoutContent(layout) };
|
|
58
90
|
export const route = async (request, url, vaultPath) => {
|
|
59
91
|
if (isReadMethod(request) && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
60
92
|
return createResponse(createClientHtml(), 200, contentTypes['.html']);
|
|
@@ -65,6 +97,9 @@ export const route = async (request, url, vaultPath) => {
|
|
|
65
97
|
if (isReadMethod(request) && url.pathname === '/app.js') {
|
|
66
98
|
return createResponse(createClientJs(), 200, contentTypes['.js']);
|
|
67
99
|
}
|
|
100
|
+
if (isReadMethod(request) && url.pathname === '/app-worker.js') {
|
|
101
|
+
return createResponse(createClientWorkerJs(), 200, contentTypes['.js']);
|
|
102
|
+
}
|
|
68
103
|
if (isReadMethod(request) && url.pathname === '/api/graph') {
|
|
69
104
|
return createResponse(createJsonResponse(await getGraph(vaultPath, readAgentQuery(url))), 200, contentTypes['.json']);
|
|
70
105
|
}
|
|
@@ -73,7 +108,7 @@ export const route = async (request, url, vaultPath) => {
|
|
|
73
108
|
const requestEtags = request.headers['if-none-match'];
|
|
74
109
|
const notModified = sameEntityTag(requestEtags, signature);
|
|
75
110
|
const etag = encodeEntityTag(signature);
|
|
76
|
-
const body = createJsonResponse({ signature,
|
|
111
|
+
const body = createJsonResponse({ signature, ...encodeLayoutPayload(layout) });
|
|
77
112
|
const jsonResponse = createResponse(body, 200, contentTypes['.json']);
|
|
78
113
|
const notModifiedResponse = createResponse('', 304, contentTypes['.json']);
|
|
79
114
|
if (notModified) {
|
|
@@ -1,9 +1,78 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
+
import { brotliCompressSync, constants, gzipSync } from 'node:zlib';
|
|
2
3
|
import { indexVault } from './index-vault.js';
|
|
3
4
|
import { startVaultWatcher } from './watch-vault.js';
|
|
4
5
|
import { assertLoopbackHost } from './server/host-security.js';
|
|
5
6
|
import { contentTypes, createJsonResponse, isHttpError } from './server/http.js';
|
|
6
7
|
import { route } from './server/routes.js';
|
|
8
|
+
const compressionThresholdBytes = 1024;
|
|
9
|
+
const normalizeEncodingToken = (value) => value.trim().toLowerCase();
|
|
10
|
+
const supportsEncoding = (acceptEncoding, target) => {
|
|
11
|
+
if (!acceptEncoding) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return acceptEncoding
|
|
15
|
+
.split(',')
|
|
16
|
+
.map((entry) => entry.split(';')[0] ?? '')
|
|
17
|
+
.map(normalizeEncodingToken)
|
|
18
|
+
.includes(target);
|
|
19
|
+
};
|
|
20
|
+
const isCompressibleContentType = (contentType) => {
|
|
21
|
+
const normalized = contentType?.toLowerCase() ?? '';
|
|
22
|
+
return (normalized.includes('application/json') ||
|
|
23
|
+
normalized.includes('text/javascript') ||
|
|
24
|
+
normalized.includes('text/css') ||
|
|
25
|
+
normalized.includes('text/html') ||
|
|
26
|
+
normalized.startsWith('text/'));
|
|
27
|
+
};
|
|
28
|
+
const maybeCompressResponse = (requestHeaders, statusCode, headers, body) => {
|
|
29
|
+
if (statusCode === 204 || statusCode === 304) {
|
|
30
|
+
return { headers, body: '' };
|
|
31
|
+
}
|
|
32
|
+
if (!isCompressibleContentType(headers['content-type'])) {
|
|
33
|
+
return { headers, body };
|
|
34
|
+
}
|
|
35
|
+
const bodyBuffer = Buffer.from(body, 'utf8');
|
|
36
|
+
if (bodyBuffer.byteLength < compressionThresholdBytes) {
|
|
37
|
+
return { headers, body };
|
|
38
|
+
}
|
|
39
|
+
if (headers['content-encoding']) {
|
|
40
|
+
return { headers, body };
|
|
41
|
+
}
|
|
42
|
+
const acceptEncodingHeader = Array.isArray(requestHeaders['accept-encoding'])
|
|
43
|
+
? requestHeaders['accept-encoding'].join(',')
|
|
44
|
+
: requestHeaders['accept-encoding'];
|
|
45
|
+
const vary = headers.vary ? `${headers.vary}, Accept-Encoding` : 'Accept-Encoding';
|
|
46
|
+
const withVary = {
|
|
47
|
+
...headers,
|
|
48
|
+
vary
|
|
49
|
+
};
|
|
50
|
+
if (supportsEncoding(acceptEncodingHeader, 'br')) {
|
|
51
|
+
return {
|
|
52
|
+
headers: {
|
|
53
|
+
...withVary,
|
|
54
|
+
'content-encoding': 'br'
|
|
55
|
+
},
|
|
56
|
+
body: brotliCompressSync(bodyBuffer, {
|
|
57
|
+
params: {
|
|
58
|
+
[constants.BROTLI_PARAM_QUALITY]: 5
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (supportsEncoding(acceptEncodingHeader, 'gzip')) {
|
|
64
|
+
return {
|
|
65
|
+
headers: {
|
|
66
|
+
...withVary,
|
|
67
|
+
'content-encoding': 'gzip'
|
|
68
|
+
},
|
|
69
|
+
body: gzipSync(bodyBuffer, {
|
|
70
|
+
level: 6
|
|
71
|
+
})
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return { headers: withVary, body };
|
|
75
|
+
};
|
|
7
76
|
export const startServer = async (input) => {
|
|
8
77
|
assertLoopbackHost(input.host);
|
|
9
78
|
if (input.shouldIndex) {
|
|
@@ -19,14 +88,16 @@ export const startServer = async (input) => {
|
|
|
19
88
|
const url = new URL(request.url ?? '/', `http://${request.headers.host ?? input.host}`);
|
|
20
89
|
route(request, url, input.vaultPath)
|
|
21
90
|
.then((result) => {
|
|
22
|
-
|
|
23
|
-
response.
|
|
91
|
+
const encoded = maybeCompressResponse(request.headers, result.statusCode, result.headers, result.body);
|
|
92
|
+
response.writeHead(result.statusCode, encoded.headers);
|
|
93
|
+
response.end(encoded.body);
|
|
24
94
|
})
|
|
25
95
|
.catch((error) => {
|
|
26
96
|
const message = error instanceof Error ? error.message : String(error);
|
|
27
97
|
const statusCode = isHttpError(error) ? error.statusCode : 500;
|
|
28
|
-
|
|
29
|
-
response.
|
|
98
|
+
const fallback = maybeCompressResponse(request.headers, statusCode, { 'content-type': contentTypes['.json'] }, createJsonResponse({ error: message }));
|
|
99
|
+
response.writeHead(statusCode, fallback.headers);
|
|
100
|
+
response.end(fallback.body);
|
|
30
101
|
});
|
|
31
102
|
});
|
|
32
103
|
await new Promise((resolve, reject) => {
|
|
@@ -37,9 +37,13 @@ const parseScore = (value, fallback) => {
|
|
|
37
37
|
}
|
|
38
38
|
return parsed;
|
|
39
39
|
};
|
|
40
|
-
const spawnDetached = (command, args) => {
|
|
40
|
+
const spawnDetached = (command, args, envOverrides) => {
|
|
41
41
|
try {
|
|
42
|
-
const child = spawn(command, args, {
|
|
42
|
+
const child = spawn(command, args, {
|
|
43
|
+
detached: true,
|
|
44
|
+
stdio: 'ignore',
|
|
45
|
+
env: envOverrides ? { ...process.env, ...envOverrides } : process.env
|
|
46
|
+
});
|
|
43
47
|
child.unref();
|
|
44
48
|
return true;
|
|
45
49
|
}
|
|
@@ -219,6 +223,7 @@ const commandExists = (command) => {
|
|
|
219
223
|
};
|
|
220
224
|
const envFlagEnabled = (name) => process.env[name] === '1' || process.env[name] === 'true';
|
|
221
225
|
const spawnAnyDetached = (candidates) => candidates.some(([command, args]) => spawnDetached(command, args));
|
|
226
|
+
const spawnAnyDetachedWithEnv = (candidates) => candidates.some(([command, args, env]) => spawnDetached(command, args, env));
|
|
222
227
|
const windowsStartCandidates = (program, args = []) => [
|
|
223
228
|
['cmd', ['/c', 'start', '', program, ...args]]
|
|
224
229
|
];
|
|
@@ -318,6 +323,16 @@ const openGraphInAppWindow = (url) => {
|
|
|
318
323
|
]);
|
|
319
324
|
}
|
|
320
325
|
const appArgument = `--app=${url}`;
|
|
326
|
+
const linuxChromiumStableFlags = [
|
|
327
|
+
'--ozone-platform=x11',
|
|
328
|
+
'--disable-gpu',
|
|
329
|
+
'--disable-features=Vulkan,VaapiVideoDecoder',
|
|
330
|
+
'--disable-background-networking'
|
|
331
|
+
];
|
|
332
|
+
const linuxChromiumEnv = {
|
|
333
|
+
GDK_BACKEND: 'x11',
|
|
334
|
+
OZONE_PLATFORM: 'x11'
|
|
335
|
+
};
|
|
321
336
|
const linuxAppWindowCandidates = [
|
|
322
337
|
'microsoft-edge',
|
|
323
338
|
'microsoft-edge-stable',
|
|
@@ -327,7 +342,11 @@ const openGraphInAppWindow = (url) => {
|
|
|
327
342
|
'chromium-browser',
|
|
328
343
|
'brave-browser'
|
|
329
344
|
].filter((candidate) => commandExists(candidate));
|
|
330
|
-
return
|
|
345
|
+
return spawnAnyDetachedWithEnv(linuxAppWindowCandidates.map((command) => [
|
|
346
|
+
command,
|
|
347
|
+
[...linuxChromiumStableFlags, appArgument, '--new-window'],
|
|
348
|
+
linuxChromiumEnv
|
|
349
|
+
]));
|
|
331
350
|
};
|
|
332
351
|
const openGraphInDetectedBrowser = (url) => {
|
|
333
352
|
if (platform() === 'win32') {
|
|
@@ -339,18 +358,28 @@ const openGraphInDetectedBrowser = (url) => {
|
|
|
339
358
|
...windowsStartCandidates('brave', [url])
|
|
340
359
|
]);
|
|
341
360
|
}
|
|
361
|
+
const linuxChromiumStableFlags = [
|
|
362
|
+
'--ozone-platform=x11',
|
|
363
|
+
'--disable-gpu',
|
|
364
|
+
'--disable-features=Vulkan,VaapiVideoDecoder',
|
|
365
|
+
'--disable-background-networking'
|
|
366
|
+
];
|
|
367
|
+
const linuxChromiumEnv = {
|
|
368
|
+
GDK_BACKEND: 'x11',
|
|
369
|
+
OZONE_PLATFORM: 'x11'
|
|
370
|
+
};
|
|
342
371
|
const linuxBrowserCandidates = [
|
|
343
|
-
['microsoft-edge', [url]],
|
|
344
|
-
['microsoft-edge-stable', [url]],
|
|
345
|
-
['google-chrome', [url]],
|
|
346
|
-
['google-chrome-stable', [url]],
|
|
347
|
-
['chromium', [url]],
|
|
348
|
-
['chromium-browser', [url]],
|
|
349
|
-
['brave-browser', [url]],
|
|
350
|
-
['firefox', ['-new-window', url]]
|
|
372
|
+
['microsoft-edge', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
|
|
373
|
+
['microsoft-edge-stable', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
|
|
374
|
+
['google-chrome', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
|
|
375
|
+
['google-chrome-stable', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
|
|
376
|
+
['chromium', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
|
|
377
|
+
['chromium-browser', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
|
|
378
|
+
['brave-browser', [...linuxChromiumStableFlags, url], linuxChromiumEnv],
|
|
379
|
+
['firefox', ['-new-window', url], undefined]
|
|
351
380
|
];
|
|
352
381
|
const available = linuxBrowserCandidates.filter(([command]) => commandExists(command));
|
|
353
|
-
return
|
|
382
|
+
return spawnAnyDetachedWithEnv(available);
|
|
354
383
|
};
|
|
355
384
|
const openUrlInUi = (url, parentPid) => {
|
|
356
385
|
const openDisabled = process.env.BRAINLINK_NO_BROWSER === '1' ||
|
|
@@ -155,6 +155,36 @@ export const openFileIndex = (vaultPath) => {
|
|
|
155
155
|
links
|
|
156
156
|
});
|
|
157
157
|
},
|
|
158
|
+
getIndexedDocuments: async (agentId) => {
|
|
159
|
+
const index = await load();
|
|
160
|
+
const documents = agentId ? index.documents.filter((document) => document.agentId === agentId) : index.documents;
|
|
161
|
+
const selectedDocumentIds = new Set(documents.map((document) => document.id));
|
|
162
|
+
const chunksByDocumentId = index.chunks.reduce((state, chunk) => {
|
|
163
|
+
if (!selectedDocumentIds.has(chunk.documentId)) {
|
|
164
|
+
return state;
|
|
165
|
+
}
|
|
166
|
+
const current = state.get(chunk.documentId) ?? [];
|
|
167
|
+
current.push(chunk);
|
|
168
|
+
state.set(chunk.documentId, current);
|
|
169
|
+
return state;
|
|
170
|
+
}, new Map());
|
|
171
|
+
const linksByDocumentId = index.links.reduce((state, link) => {
|
|
172
|
+
if (!selectedDocumentIds.has(link.fromDocumentId)) {
|
|
173
|
+
return state;
|
|
174
|
+
}
|
|
175
|
+
const current = state.get(link.fromDocumentId) ?? [];
|
|
176
|
+
current.push(link);
|
|
177
|
+
state.set(link.fromDocumentId, current);
|
|
178
|
+
return state;
|
|
179
|
+
}, new Map());
|
|
180
|
+
return documents
|
|
181
|
+
.map((document) => ({
|
|
182
|
+
document,
|
|
183
|
+
chunks: [...(chunksByDocumentId.get(document.id) ?? [])].sort((left, right) => left.ordinal - right.ordinal),
|
|
184
|
+
links: linksByDocumentId.get(document.id) ?? []
|
|
185
|
+
}))
|
|
186
|
+
.sort((left, right) => left.document.path.localeCompare(right.document.path));
|
|
187
|
+
},
|
|
158
188
|
search: async (query, limit, agentId, mode = 'hybrid', queryEmbedding = []) => {
|
|
159
189
|
const index = await load();
|
|
160
190
|
const documentsById = new Map(index.documents.map((document) => [document.id, document]));
|
|
@@ -76,6 +76,21 @@ export const readMarkdownFiles = async (vaultPath) => {
|
|
|
76
76
|
};
|
|
77
77
|
}));
|
|
78
78
|
};
|
|
79
|
+
export const readMarkdownFileSummaries = async (vaultPath) => {
|
|
80
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
81
|
+
const paths = await walkMarkdownFiles(absoluteVaultPath);
|
|
82
|
+
const summaries = await Promise.all(paths.map(async (absolutePath) => {
|
|
83
|
+
const fileStats = await stat(absolutePath);
|
|
84
|
+
return {
|
|
85
|
+
absolutePath,
|
|
86
|
+
relativePath: relative(absoluteVaultPath, absolutePath),
|
|
87
|
+
createdAt: fileStats.birthtime,
|
|
88
|
+
updatedAt: fileStats.mtime,
|
|
89
|
+
size: fileStats.size
|
|
90
|
+
};
|
|
91
|
+
}));
|
|
92
|
+
return summaries.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
93
|
+
};
|
|
79
94
|
export const listVaultFiles = async (vaultPath) => {
|
|
80
95
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
81
96
|
return walkVaultFiles(absoluteVaultPath);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const indexStateFileName = 'index-state.json';
|
|
4
|
+
const toIndexStatePath = (vaultPath) => join(vaultPath, '.brainlink', indexStateFileName);
|
|
5
|
+
export const readIndexState = async (vaultPath) => {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = JSON.parse(await readFile(toIndexStatePath(vaultPath), 'utf8'));
|
|
8
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.files)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const files = parsed.files.flatMap((entry) => {
|
|
12
|
+
if (!entry || typeof entry !== 'object') {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const row = entry;
|
|
16
|
+
if (typeof row.path !== 'string' || typeof row.mtimeMs !== 'number' || typeof row.size !== 'number') {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
path: row.path,
|
|
22
|
+
mtimeMs: row.mtimeMs,
|
|
23
|
+
size: row.size
|
|
24
|
+
}
|
|
25
|
+
];
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
version: 1,
|
|
29
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
|
|
30
|
+
chunkSize: typeof parsed.chunkSize === 'number' ? parsed.chunkSize : 1200,
|
|
31
|
+
embeddingProvider: typeof parsed.embeddingProvider === 'string' ? parsed.embeddingProvider : 'none',
|
|
32
|
+
files,
|
|
33
|
+
pendingPackChanges: typeof parsed.pendingPackChanges === 'number' && parsed.pendingPackChanges >= 0 ? parsed.pendingPackChanges : 0
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export const writeIndexState = async (vaultPath, state) => {
|
|
41
|
+
const payload = {
|
|
42
|
+
version: 1,
|
|
43
|
+
updatedAt: new Date().toISOString(),
|
|
44
|
+
chunkSize: state.chunkSize,
|
|
45
|
+
embeddingProvider: state.embeddingProvider,
|
|
46
|
+
files: [...state.files].sort((left, right) => left.path.localeCompare(right.path)),
|
|
47
|
+
pendingPackChanges: Math.max(0, Math.floor(state.pendingPackChanges))
|
|
48
|
+
};
|
|
49
|
+
await writeFile(toIndexStatePath(vaultPath), `${JSON.stringify(payload)}\n`, 'utf8');
|
|
50
|
+
};
|
package/package.json
CHANGED