@andespindola/brainlink 0.1.0-beta.8 → 0.1.0-beta.80
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 +8 -5
- package/CHANGELOG.md +58 -2
- package/CONTRIBUTING.md +2 -2
- package/COPYRIGHT.md +5 -0
- package/README.md +266 -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/build-context.js +56 -1
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +138 -103
- package/dist/application/frontend/client-html.js +47 -41
- package/dist/application/frontend/client-js.js +2469 -156
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-layout.js +18 -6
- 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/import-legacy-sqlite.js +296 -0
- package/dist/application/index-vault.js +252 -19
- package/dist/application/list-agents.js +3 -3
- package/dist/application/list-links.js +5 -5
- package/dist/application/migrate-vault.js +46 -16
- package/dist/application/offline-pack-backup.js +44 -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 +102 -1
- package/dist/application/start-server.js +75 -4
- package/dist/application/watch-vault.js +23 -2
- package/dist/benchmarks/large-vault.js +1 -1
- package/dist/cli/commands/agent-commands.js +419 -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 +973 -10
- 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 +67 -16
- package/dist/domain/markdown.js +36 -4
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +132 -8
- package/dist/infrastructure/file-index.js +358 -0
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/index-state.js +56 -0
- package/dist/infrastructure/paths.js +9 -1
- package/dist/infrastructure/private-pack-codec.js +134 -0
- package/dist/infrastructure/search-packs.js +452 -0
- package/dist/infrastructure/session-state.js +172 -0
- package/dist/mcp/main.js +11 -3
- package/dist/mcp/server.js +27 -2
- package/dist/mcp/startup.js +35 -0
- package/dist/mcp/tools.js +633 -19
- package/docs/AGENT_USAGE.md +177 -15
- package/docs/ARCHITECTURE.md +37 -26
- package/docs/QUICKSTART.md +111 -0
- package/package.json +6 -4
- 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
package/dist/cli/main.js
CHANGED
|
@@ -3,6 +3,8 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { basename, dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { registerAgentCommands } from './commands/agent-commands.js';
|
|
7
|
+
import { registerConfigCommands } from './commands/config-commands.js';
|
|
6
8
|
import { registerReadCommands } from './commands/read-commands.js';
|
|
7
9
|
import { registerWriteCommands } from './commands/write-commands.js';
|
|
8
10
|
const readPackageVersion = () => {
|
|
@@ -21,6 +23,8 @@ program
|
|
|
21
23
|
.version(readPackageVersion());
|
|
22
24
|
registerWriteCommands(program);
|
|
23
25
|
registerReadCommands(program);
|
|
26
|
+
registerConfigCommands(program);
|
|
27
|
+
registerAgentCommands(program);
|
|
24
28
|
program.parseAsync().catch((error) => {
|
|
25
29
|
const message = error instanceof Error ? error.message : String(error);
|
|
26
30
|
console.error(message);
|
package/dist/cli/runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadBrainlinkConfig } from '../infrastructure/config.js';
|
|
1
|
+
import { loadBrainlinkConfig, resolveAgentRuntimeDefaults } from '../infrastructure/config.js';
|
|
2
2
|
import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
|
|
3
3
|
export const parsePositiveInteger = (value, fallback) => {
|
|
4
4
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -8,10 +8,13 @@ export const resolveOptions = async (options) => {
|
|
|
8
8
|
const config = await loadBrainlinkConfig();
|
|
9
9
|
const vault = options.vault ?? config.vault;
|
|
10
10
|
const allowedVault = assertVaultAllowed(vault, config.allowedVaults);
|
|
11
|
+
const agent = options.agent ?? config.defaultAgent;
|
|
12
|
+
const defaults = resolveAgentRuntimeDefaults(config, agent);
|
|
11
13
|
return {
|
|
12
14
|
config,
|
|
13
15
|
vault: allowedVault,
|
|
14
|
-
agent
|
|
16
|
+
agent,
|
|
17
|
+
defaults
|
|
15
18
|
};
|
|
16
19
|
};
|
|
17
20
|
export const print = (json, value, human) => {
|
package/dist/domain/context.js
CHANGED
|
@@ -1,13 +1,50 @@
|
|
|
1
|
+
import { middleOutIndices } from './middle-out.js';
|
|
2
|
+
const maxSectionsPerDocument = 3;
|
|
3
|
+
const byScore = (left, right) => right.score - left.score || left.title.localeCompare(right.title);
|
|
4
|
+
const byOrdinal = (left, right) => (left.chunkOrdinal ?? Number.MAX_SAFE_INTEGER) - (right.chunkOrdinal ?? Number.MAX_SAFE_INTEGER);
|
|
5
|
+
const middleOutDocumentResults = (results) => {
|
|
6
|
+
if (results.length <= 1) {
|
|
7
|
+
return results;
|
|
8
|
+
}
|
|
9
|
+
const sortedByOrdinal = [...results].sort(byOrdinal);
|
|
10
|
+
const pivotChunkId = [...results].sort(byScore)[0]?.chunkId;
|
|
11
|
+
const pivotIndex = sortedByOrdinal.findIndex((result) => result.chunkId === pivotChunkId);
|
|
12
|
+
if (pivotIndex < 0) {
|
|
13
|
+
return [...results].sort(byScore);
|
|
14
|
+
}
|
|
15
|
+
return middleOutIndices(sortedByOrdinal.length, pivotIndex).map((index) => sortedByOrdinal[index]);
|
|
16
|
+
};
|
|
1
17
|
export const selectContextSections = (results, maxTokens) => {
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
18
|
+
const grouped = results.reduce((state, result) => {
|
|
19
|
+
const current = state.get(result.documentId) ?? [];
|
|
20
|
+
state.set(result.documentId, [...current, result]);
|
|
21
|
+
return state;
|
|
22
|
+
}, new Map());
|
|
23
|
+
const documentOrder = Array.from(results.reduce((state, result) => {
|
|
24
|
+
if (!state.has(result.documentId)) {
|
|
25
|
+
state.set(result.documentId, result.score);
|
|
6
26
|
}
|
|
7
|
-
return
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
27
|
+
return state;
|
|
28
|
+
}, new Map()).entries())
|
|
29
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
30
|
+
.map(([documentId]) => documentId);
|
|
31
|
+
const selected = documentOrder.reduce((state, documentId) => {
|
|
32
|
+
const ordered = middleOutDocumentResults(grouped.get(documentId) ?? []);
|
|
33
|
+
let usedTokens = state.usedTokens;
|
|
34
|
+
let sections = state.sections;
|
|
35
|
+
let seenChunks = state.seenChunks;
|
|
36
|
+
for (let index = 0; index < ordered.length && index < maxSectionsPerDocument; index += 1) {
|
|
37
|
+
const result = ordered[index];
|
|
38
|
+
if (seenChunks.has(result.chunkId)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const tokenCost = Math.ceil(result.content.length / 4);
|
|
42
|
+
if (usedTokens + tokenCost > maxTokens) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
usedTokens += tokenCost;
|
|
46
|
+
sections = [
|
|
47
|
+
...sections,
|
|
11
48
|
{
|
|
12
49
|
title: result.title,
|
|
13
50
|
path: result.path,
|
|
@@ -16,13 +53,18 @@ export const selectContextSections = (results, maxTokens) => {
|
|
|
16
53
|
searchMode: result.searchMode,
|
|
17
54
|
tags: result.tags
|
|
18
55
|
}
|
|
19
|
-
]
|
|
20
|
-
|
|
56
|
+
];
|
|
57
|
+
seenChunks = new Set([...seenChunks, result.chunkId]);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
usedTokens,
|
|
61
|
+
sections,
|
|
62
|
+
seenChunks
|
|
21
63
|
};
|
|
22
64
|
}, {
|
|
23
65
|
usedTokens: 0,
|
|
24
66
|
sections: [],
|
|
25
|
-
|
|
67
|
+
seenChunks: new Set()
|
|
26
68
|
});
|
|
27
69
|
return selected.sections;
|
|
28
70
|
};
|
|
@@ -58,7 +58,8 @@ const tokenize = (input) => input
|
|
|
58
58
|
.match(tokenPattern)
|
|
59
59
|
?.map(normalizeToken)
|
|
60
60
|
.filter((token) => token.length > 1 && !stopWords.has(token)) ?? [];
|
|
61
|
-
const
|
|
61
|
+
const getAliasesForToken = (token) => Object.hasOwn(aliases, token) ? aliases[token] ?? [] : [];
|
|
62
|
+
const expandTokens = (tokens) => tokens.flatMap((token) => [token, ...getAliasesForToken(token)]);
|
|
62
63
|
const hash = (value) => Array.from(value).reduce((state, char) => Math.imul(state ^ char.charCodeAt(0), 16777619), 2166136261) >>> 0;
|
|
63
64
|
const featureHash = (feature) => {
|
|
64
65
|
const value = hash(feature);
|
|
@@ -20,6 +20,7 @@ const segmentAngles = {
|
|
|
20
20
|
Evaluation: 2.08,
|
|
21
21
|
Security: 2.82
|
|
22
22
|
};
|
|
23
|
+
const hubTitlePattern = /\b(memory\s*hub|knowledge\s*root|moc|map)\b/i;
|
|
23
24
|
const hashText = (value) => Array.from(value).reduce((hash, char) => ((hash << 5) - hash + char.charCodeAt(0)) | 0, 0);
|
|
24
25
|
const jitter = (value, range) => {
|
|
25
26
|
const normalized = Math.abs(hashText(value) % 1000) / 1000;
|
|
@@ -45,26 +46,61 @@ const countDegrees = (edges) => edges.reduce((degrees, edge) => {
|
|
|
45
46
|
? incrementDegreeBy(incrementDegreeBy(degrees, edge.source, weight), edge.target, weight)
|
|
46
47
|
: incrementDegreeBy(degrees, edge.source, weight);
|
|
47
48
|
}, new Map());
|
|
48
|
-
const uniqueIds = (ids) => Array.from(new Set(ids));
|
|
49
49
|
const createAdjacency = (nodes, edges) => {
|
|
50
50
|
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
51
|
-
const
|
|
52
|
-
|
|
51
|
+
const adjacency = new Map(nodes.map((node) => [node.id, new Set()]));
|
|
52
|
+
edges.forEach((edge) => {
|
|
53
53
|
if (!edge.target || !nodeIds.has(edge.source) || !nodeIds.has(edge.target)) {
|
|
54
|
-
return
|
|
54
|
+
return;
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
]);
|
|
61
|
-
}, emptyAdjacency);
|
|
56
|
+
adjacency.get(edge.source)?.add(edge.target);
|
|
57
|
+
adjacency.get(edge.target)?.add(edge.source);
|
|
58
|
+
});
|
|
59
|
+
return new Map(Array.from(adjacency.entries(), ([id, neighbors]) => [id, Array.from(neighbors)]));
|
|
62
60
|
};
|
|
63
61
|
const byTitle = (left, right) => left.title.localeCompare(right.title);
|
|
64
62
|
const byDegreeThenTitle = (degrees) => (left, right) => {
|
|
65
63
|
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
66
64
|
return degreeDelta === 0 ? byTitle(left, right) : degreeDelta;
|
|
67
65
|
};
|
|
66
|
+
const hubScore = (node) => {
|
|
67
|
+
const title = node.title.trim().toLowerCase();
|
|
68
|
+
if (title === 'memory hub')
|
|
69
|
+
return 5;
|
|
70
|
+
if (title === 'knowledge root')
|
|
71
|
+
return 4;
|
|
72
|
+
if (/\bmoc\b/i.test(node.title))
|
|
73
|
+
return 3;
|
|
74
|
+
return hubTitlePattern.test(node.title) ? 2 : 0;
|
|
75
|
+
};
|
|
76
|
+
const selectPrimaryHubId = (nodes, degrees) => {
|
|
77
|
+
const ranked = [...nodes]
|
|
78
|
+
.filter((node) => hubScore(node) > 0)
|
|
79
|
+
.sort((left, right) => {
|
|
80
|
+
const scoreDelta = hubScore(right) - hubScore(left);
|
|
81
|
+
if (scoreDelta !== 0)
|
|
82
|
+
return scoreDelta;
|
|
83
|
+
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
84
|
+
if (degreeDelta !== 0)
|
|
85
|
+
return degreeDelta;
|
|
86
|
+
return left.title.localeCompare(right.title);
|
|
87
|
+
});
|
|
88
|
+
return ranked[0]?.id ?? null;
|
|
89
|
+
};
|
|
90
|
+
const centerLayoutByNode = (nodes, nodeId) => {
|
|
91
|
+
if (!nodeId) {
|
|
92
|
+
return nodes;
|
|
93
|
+
}
|
|
94
|
+
const anchor = nodes.find((node) => node.id === nodeId);
|
|
95
|
+
if (!anchor) {
|
|
96
|
+
return nodes;
|
|
97
|
+
}
|
|
98
|
+
return nodes.map((node) => ({
|
|
99
|
+
...node,
|
|
100
|
+
x: node.x - anchor.x,
|
|
101
|
+
y: node.y - anchor.y
|
|
102
|
+
}));
|
|
103
|
+
};
|
|
68
104
|
const naturalSegmentSeed = (node) => groupKey(node) === '00-maps' || /\b(moc|map)\b/i.test(node.title);
|
|
69
105
|
const segmentName = (node) => node.title.replace(/^MOC\s+/i, '').replace(/\s+Memory Map$/i, '').trim() || node.title;
|
|
70
106
|
const collectComponent = (adjacency, startId, visited) => {
|
|
@@ -117,18 +153,31 @@ const assignSegments = (nodes, edges, degrees) => {
|
|
|
117
153
|
}
|
|
118
154
|
return new Map(nodes.map((node) => [node.id, assignments.get(node.id) ?? groupLabel(groupKey(node))]));
|
|
119
155
|
};
|
|
120
|
-
const groupNodesBySegment = (nodes, segments) =>
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
156
|
+
const groupNodesBySegment = (nodes, segments) => {
|
|
157
|
+
const groups = new Map();
|
|
158
|
+
nodes.forEach((node) => {
|
|
159
|
+
const segment = segments.get(node.id) ?? groupLabel(groupKey(node));
|
|
160
|
+
const bucket = groups.get(segment);
|
|
161
|
+
if (bucket) {
|
|
162
|
+
bucket.push(node);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
groups.set(segment, [node]);
|
|
166
|
+
});
|
|
167
|
+
return new Map(groups);
|
|
168
|
+
};
|
|
124
169
|
const segmentAngle = (segment, index, count) => segmentAngles[segment] ?? (Math.PI * 2 * index) / Math.max(count, 1) - Math.PI / 2;
|
|
170
|
+
const petalSpreadForSegmentSize = (size) => {
|
|
171
|
+
const safeSize = Math.max(size, 1);
|
|
172
|
+
return 180 + Math.log2(safeSize + 1) * 6;
|
|
173
|
+
};
|
|
125
174
|
const createSegmentNodes = (segments, degrees, segmentCount) => ([segment, nodes], segmentIndex) => {
|
|
126
175
|
const sortedNodes = [...nodes].sort(byDegreeThenTitle(degrees));
|
|
127
176
|
const angle = segmentAngle(segment, segmentIndex, segmentCount);
|
|
128
177
|
const baseRadius = segmentCount === 1 ? 0 : 340 + Math.min(sortedNodes.length, 22) * 10;
|
|
129
178
|
const centerX = Math.cos(angle) * baseRadius;
|
|
130
179
|
const centerY = Math.sin(angle) * (baseRadius * 0.78);
|
|
131
|
-
const petalSpread =
|
|
180
|
+
const petalSpread = petalSpreadForSegmentSize(sortedNodes.length);
|
|
132
181
|
return sortedNodes.map((node, index) => {
|
|
133
182
|
const localAngle = index * 2.399963 + jitter(node.title, 0.42);
|
|
134
183
|
const localRadius = Math.sqrt(index + 1) * petalSpread;
|
|
@@ -240,8 +289,10 @@ export const createCauliflowerGraphLayout = (graph) => {
|
|
|
240
289
|
const segmentGroups = Array.from(groupNodesBySegment(graph.nodes, segments).entries())
|
|
241
290
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
242
291
|
const nodes = relaxCollisions(segmentGroups.flatMap(createSegmentNodes(segments, degrees, segmentGroups.length)));
|
|
292
|
+
const primaryHubId = selectPrimaryHubId(graph.nodes, degrees);
|
|
293
|
+
const centeredNodes = centerLayoutByNode(nodes, primaryHubId);
|
|
243
294
|
return {
|
|
244
|
-
nodes,
|
|
295
|
+
nodes: centeredNodes,
|
|
245
296
|
edges: graph.edges
|
|
246
297
|
};
|
|
247
298
|
};
|
package/dist/domain/markdown.js
CHANGED
|
@@ -77,11 +77,13 @@ export const extractWikiLinkReferences = (content) => visibleMarkdownLines(conte
|
|
|
77
77
|
}));
|
|
78
78
|
});
|
|
79
79
|
const priorityFromWeight = (weight) => weight >= 8 ? 'critical' : weight >= 4 ? 'high' : 'normal';
|
|
80
|
+
const normalizeAccumulatedWeight = (weight) => Math.max(1, Math.min(12, weight));
|
|
80
81
|
export const extractWikiLinkWeights = (content) => {
|
|
81
82
|
const weights = extractWikiLinkReferences(content).reduce((state, reference) => {
|
|
82
83
|
const titleKey = reference.title.toLowerCase();
|
|
83
84
|
const current = state.get(titleKey);
|
|
84
|
-
const
|
|
85
|
+
const rawWeight = (current?.weight ?? 0) + reference.weight;
|
|
86
|
+
const weight = normalizeAccumulatedWeight(rawWeight);
|
|
85
87
|
const explicitPriority = reference.priority
|
|
86
88
|
? maxPriority(current?.priority ?? reference.priority, reference.priority)
|
|
87
89
|
: current?.priority;
|
|
@@ -116,10 +118,38 @@ const normalizeChunkContent = (content) => content
|
|
|
116
118
|
.join('\n')
|
|
117
119
|
.replace(/\n{3,}/g, '\n\n')
|
|
118
120
|
.trim();
|
|
121
|
+
const splitLongParagraph = (paragraph, maxCharacters) => {
|
|
122
|
+
if (paragraph.length <= maxCharacters) {
|
|
123
|
+
return [paragraph];
|
|
124
|
+
}
|
|
125
|
+
const sentences = paragraph
|
|
126
|
+
.split(/(?<=[.!?])\s+/)
|
|
127
|
+
.map((sentence) => sentence.trim())
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
if (sentences.length <= 1) {
|
|
130
|
+
const chunks = [];
|
|
131
|
+
for (let index = 0; index < paragraph.length; index += maxCharacters) {
|
|
132
|
+
chunks.push(paragraph.slice(index, index + maxCharacters).trim());
|
|
133
|
+
}
|
|
134
|
+
return chunks.filter(Boolean);
|
|
135
|
+
}
|
|
136
|
+
return sentences.reduce((state, sentence) => {
|
|
137
|
+
const last = state.at(-1);
|
|
138
|
+
if (!last) {
|
|
139
|
+
return [sentence];
|
|
140
|
+
}
|
|
141
|
+
const merged = `${last} ${sentence}`;
|
|
142
|
+
if (merged.length <= maxCharacters) {
|
|
143
|
+
return [...state.slice(0, -1), merged];
|
|
144
|
+
}
|
|
145
|
+
return [...state, sentence];
|
|
146
|
+
}, []);
|
|
147
|
+
};
|
|
119
148
|
export const splitIntoChunks = (documentId, content, maxCharacters = 1200) => {
|
|
120
149
|
const paragraphs = normalizeChunkContent(stripFrontmatter(content))
|
|
121
150
|
.split(/\n{2,}/)
|
|
122
|
-
.filter(Boolean)
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.flatMap((paragraph) => splitLongParagraph(paragraph, maxCharacters));
|
|
123
153
|
const chunks = paragraphs.reduce((state, paragraph) => {
|
|
124
154
|
const lastChunk = state.at(-1);
|
|
125
155
|
if (!lastChunk) {
|
|
@@ -162,13 +192,15 @@ export const parseMarkdownDocument = (input) => {
|
|
|
162
192
|
export const createIndexedDocument = (document, titleToDocumentId, maxChunkCharacters = 1200) => {
|
|
163
193
|
const chunks = splitIntoChunks(document.id, document.content, maxChunkCharacters);
|
|
164
194
|
const linkWeights = new Map(extractWikiLinkWeights(document.content).map((link) => [link.title.toLowerCase(), link]));
|
|
165
|
-
const links = document.links
|
|
195
|
+
const links = document.links
|
|
196
|
+
.map((toTitle) => ({
|
|
166
197
|
fromDocumentId: document.id,
|
|
167
198
|
toTitle,
|
|
168
199
|
toDocumentId: titleToDocumentId.get(toTitle.toLowerCase()) ?? null,
|
|
169
200
|
weight: linkWeights.get(toTitle.toLowerCase())?.weight ?? 1,
|
|
170
201
|
priority: linkWeights.get(toTitle.toLowerCase())?.priority ?? 'normal'
|
|
171
|
-
}))
|
|
202
|
+
}))
|
|
203
|
+
.filter((link) => link.toDocumentId !== document.id);
|
|
172
204
|
return {
|
|
173
205
|
document,
|
|
174
206
|
chunks,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const middleOutIndices = (size, pivotIndex) => {
|
|
2
|
+
if (!Number.isFinite(size) || size <= 0) {
|
|
3
|
+
return [];
|
|
4
|
+
}
|
|
5
|
+
const clampedPivot = Math.max(0, Math.min(Math.floor(pivotIndex), size - 1));
|
|
6
|
+
const indices = [clampedPivot];
|
|
7
|
+
for (let offset = 1; indices.length < size; offset += 1) {
|
|
8
|
+
const left = clampedPivot - offset;
|
|
9
|
+
const right = clampedPivot + offset;
|
|
10
|
+
if (left >= 0) {
|
|
11
|
+
indices.push(left);
|
|
12
|
+
}
|
|
13
|
+
if (right < size) {
|
|
14
|
+
indices.push(right);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return indices;
|
|
18
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
3
4
|
import { sanitizeAgentId } from '../domain/agents.js';
|
|
4
|
-
import { getDefaultVaultPath } from './paths.js';
|
|
5
|
+
import { getBrainlinkHomePath, getDefaultVaultPath } from './paths.js';
|
|
5
6
|
export const defaultBrainlinkConfig = {
|
|
6
7
|
vault: getDefaultVaultPath(),
|
|
7
8
|
host: '127.0.0.1',
|
|
@@ -13,15 +14,98 @@ export const defaultBrainlinkConfig = {
|
|
|
13
14
|
defaultContextTokens: 2000,
|
|
14
15
|
embeddingProvider: 'local',
|
|
15
16
|
defaultSearchMode: 'hybrid',
|
|
16
|
-
chunkSize: 1200
|
|
17
|
+
chunkSize: 1200,
|
|
18
|
+
searchPack: {
|
|
19
|
+
rowChunkSize: 5_000,
|
|
20
|
+
compressionLevel: 5,
|
|
21
|
+
useDictionary: true,
|
|
22
|
+
guardrailMinSavingsPercent: 8,
|
|
23
|
+
guardrailMaxLatencyRegressionPercent: 5
|
|
24
|
+
},
|
|
25
|
+
agentProfiles: {}
|
|
17
26
|
};
|
|
18
27
|
const configFilenames = ['brainlink.config.json', '.brainlink.json'];
|
|
28
|
+
const localConfigFilename = 'brainlink.config.json';
|
|
29
|
+
const globalConfigFilename = 'brainlink.config.json';
|
|
30
|
+
const globalConfigDirectoryMode = 0o700;
|
|
31
|
+
const globalConfigFileMode = 0o600;
|
|
32
|
+
const safeCwd = () => {
|
|
33
|
+
try {
|
|
34
|
+
return process.cwd();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return homedir();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
19
40
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
41
|
const embeddingProviders = new Set(['none', 'local']);
|
|
21
42
|
const searchModes = new Set(['fts', 'semantic', 'hybrid']);
|
|
22
43
|
const sanitizeEmbeddingProvider = (value) => typeof value === 'string' && embeddingProviders.has(value) ? value : defaultBrainlinkConfig.embeddingProvider;
|
|
23
44
|
export const sanitizeSearchMode = (value, fallback = defaultBrainlinkConfig.defaultSearchMode) => typeof value === 'string' && searchModes.has(value) ? value : fallback;
|
|
24
45
|
const sanitizeAllowedVaults = (value) => Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.trim().length > 0) : [];
|
|
46
|
+
const sanitizePositiveNumber = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
47
|
+
const sanitizeIntegerInRange = (value, fallback, minimum, maximum) => {
|
|
48
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
const rounded = Math.round(value);
|
|
52
|
+
if (rounded < minimum) {
|
|
53
|
+
return minimum;
|
|
54
|
+
}
|
|
55
|
+
if (rounded > maximum) {
|
|
56
|
+
return maximum;
|
|
57
|
+
}
|
|
58
|
+
return rounded;
|
|
59
|
+
};
|
|
60
|
+
const sanitizeSearchPackConfig = (value) => {
|
|
61
|
+
const fallback = defaultBrainlinkConfig.searchPack;
|
|
62
|
+
if (!isRecord(value)) {
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
rowChunkSize: sanitizeIntegerInRange(value.rowChunkSize, fallback.rowChunkSize, 100, 100_000),
|
|
67
|
+
compressionLevel: sanitizeIntegerInRange(value.compressionLevel, fallback.compressionLevel, 0, 11),
|
|
68
|
+
useDictionary: typeof value.useDictionary === 'boolean' ? value.useDictionary : fallback.useDictionary,
|
|
69
|
+
guardrailMinSavingsPercent: typeof value.guardrailMinSavingsPercent === 'number' && Number.isFinite(value.guardrailMinSavingsPercent)
|
|
70
|
+
? Math.max(0, Math.min(95, value.guardrailMinSavingsPercent))
|
|
71
|
+
: fallback.guardrailMinSavingsPercent,
|
|
72
|
+
guardrailMaxLatencyRegressionPercent: typeof value.guardrailMaxLatencyRegressionPercent === 'number' && Number.isFinite(value.guardrailMaxLatencyRegressionPercent)
|
|
73
|
+
? Math.max(0, Math.min(300, value.guardrailMaxLatencyRegressionPercent))
|
|
74
|
+
: fallback.guardrailMaxLatencyRegressionPercent
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const sanitizeAgentProfile = (value) => {
|
|
78
|
+
if (!isRecord(value)) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const defaultSearchLimit = sanitizePositiveNumber(value.defaultSearchLimit);
|
|
82
|
+
const defaultContextTokens = sanitizePositiveNumber(value.defaultContextTokens);
|
|
83
|
+
const defaultSearchMode = typeof value.defaultSearchMode === 'string' && searchModes.has(value.defaultSearchMode)
|
|
84
|
+
? value.defaultSearchMode
|
|
85
|
+
: undefined;
|
|
86
|
+
const profile = {
|
|
87
|
+
...(defaultSearchLimit ? { defaultSearchLimit } : {}),
|
|
88
|
+
...(defaultContextTokens ? { defaultContextTokens } : {}),
|
|
89
|
+
...(defaultSearchMode ? { defaultSearchMode } : {})
|
|
90
|
+
};
|
|
91
|
+
return Object.keys(profile).length > 0 ? profile : null;
|
|
92
|
+
};
|
|
93
|
+
const sanitizeAgentProfiles = (value) => {
|
|
94
|
+
if (!isRecord(value)) {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
return Object.entries(value).reduce((state, [key, profile]) => {
|
|
98
|
+
const normalizedKey = key === '*' ? '*' : sanitizeAgentId(key);
|
|
99
|
+
const sanitizedProfile = sanitizeAgentProfile(profile);
|
|
100
|
+
if (!sanitizedProfile || normalizedKey.length === 0) {
|
|
101
|
+
return state;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
...state,
|
|
105
|
+
[normalizedKey]: sanitizedProfile
|
|
106
|
+
};
|
|
107
|
+
}, {});
|
|
108
|
+
};
|
|
25
109
|
const readAllowedVaultsFromEnv = () => (process.env.BRAINLINK_ALLOWED_VAULTS ?? '')
|
|
26
110
|
.split(',')
|
|
27
111
|
.map((value) => value.trim())
|
|
@@ -39,6 +123,34 @@ const readJsonConfig = async (path) => {
|
|
|
39
123
|
throw error;
|
|
40
124
|
}
|
|
41
125
|
};
|
|
126
|
+
export const getGlobalConfigPath = () => join(getBrainlinkHomePath(), globalConfigFilename);
|
|
127
|
+
export const getLocalConfigPath = (cwd = safeCwd()) => resolve(cwd, localConfigFilename);
|
|
128
|
+
export const resolveConfigPath = (scope, cwd = safeCwd()) => scope === 'global' ? getGlobalConfigPath() : getLocalConfigPath(cwd);
|
|
129
|
+
export const loadRawConfig = async (scope, cwd = safeCwd()) => readJsonConfig(resolveConfigPath(scope, cwd));
|
|
130
|
+
export const loadLegacyLocalRawConfig = async (cwd = safeCwd()) => readJsonConfig(resolve(cwd, '.brainlink.json'));
|
|
131
|
+
export const detectVaultConfigSource = async (cwd = safeCwd()) => {
|
|
132
|
+
const [globalConfig, localConfig, legacyLocalConfig] = await Promise.all([
|
|
133
|
+
loadRawConfig('global', cwd),
|
|
134
|
+
loadRawConfig('local', cwd),
|
|
135
|
+
loadLegacyLocalRawConfig(cwd)
|
|
136
|
+
]);
|
|
137
|
+
if (typeof legacyLocalConfig.vault === 'string' && legacyLocalConfig.vault.trim().length > 0) {
|
|
138
|
+
return 'local-legacy';
|
|
139
|
+
}
|
|
140
|
+
if (typeof localConfig.vault === 'string' && localConfig.vault.trim().length > 0) {
|
|
141
|
+
return 'local';
|
|
142
|
+
}
|
|
143
|
+
if (typeof globalConfig.vault === 'string' && globalConfig.vault.trim().length > 0) {
|
|
144
|
+
return 'global';
|
|
145
|
+
}
|
|
146
|
+
return 'default';
|
|
147
|
+
};
|
|
148
|
+
export const writeRawConfig = async (scope, value, cwd = safeCwd()) => {
|
|
149
|
+
const path = resolveConfigPath(scope, cwd);
|
|
150
|
+
await mkdir(dirname(path), { recursive: true, mode: globalConfigDirectoryMode });
|
|
151
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: globalConfigFileMode });
|
|
152
|
+
return path;
|
|
153
|
+
};
|
|
42
154
|
const sanitizeConfig = (value) => ({
|
|
43
155
|
...defaultBrainlinkConfig,
|
|
44
156
|
...value,
|
|
@@ -55,12 +167,24 @@ const sanitizeConfig = (value) => ({
|
|
|
55
167
|
: defaultBrainlinkConfig.defaultContextTokens,
|
|
56
168
|
allowedVaults: [...sanitizeAllowedVaults(value.allowedVaults), ...readAllowedVaultsFromEnv()],
|
|
57
169
|
chunkSize: typeof value.chunkSize === 'number' && value.chunkSize > 0 ? value.chunkSize : defaultBrainlinkConfig.chunkSize,
|
|
170
|
+
searchPack: sanitizeSearchPackConfig(value.searchPack),
|
|
58
171
|
embeddingProvider: sanitizeEmbeddingProvider(value.embeddingProvider),
|
|
59
|
-
defaultSearchMode: sanitizeSearchMode(value.defaultSearchMode)
|
|
172
|
+
defaultSearchMode: sanitizeSearchMode(value.defaultSearchMode),
|
|
173
|
+
agentProfiles: sanitizeAgentProfiles(value.agentProfiles)
|
|
60
174
|
});
|
|
61
|
-
export const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
175
|
+
export const resolveAgentRuntimeDefaults = (config, agent) => {
|
|
176
|
+
const normalizedAgent = agent?.trim().length ? sanitizeAgentId(agent) : undefined;
|
|
177
|
+
const profile = (normalizedAgent ? config.agentProfiles[normalizedAgent] : undefined) ?? config.agentProfiles['*'];
|
|
178
|
+
return {
|
|
179
|
+
defaultSearchLimit: profile?.defaultSearchLimit ?? config.defaultSearchLimit,
|
|
180
|
+
defaultContextTokens: profile?.defaultContextTokens ?? config.defaultContextTokens,
|
|
181
|
+
defaultSearchMode: profile?.defaultSearchMode ?? config.defaultSearchMode
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
export const loadBrainlinkConfig = async (cwd = safeCwd()) => {
|
|
185
|
+
const globalConfig = await readJsonConfig(getGlobalConfigPath());
|
|
186
|
+
const localConfigs = await Promise.all(configFilenames.map((filename) => readJsonConfig(resolve(cwd, filename))));
|
|
187
|
+
const merged = [globalConfig, ...localConfigs].reduce((state, config) => ({
|
|
64
188
|
...state,
|
|
65
189
|
...config
|
|
66
190
|
}), {});
|