nano-brain 2026.1.0
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_SNIPPET.md +36 -0
- package/CHANGELOG.md +68 -0
- package/README.md +281 -0
- package/SKILL.md +153 -0
- package/bin/cli.js +18 -0
- package/index.html +929 -0
- package/nano-brain +4 -0
- package/opencode-mcp.json +9 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/design.md +68 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/proposal.md +27 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/mcp-integration-testing/spec.md +50 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/mcp-server/spec.md +40 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/search-pipeline/spec.md +29 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/tasks.md +37 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/design.md +111 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/proposal.md +30 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/mcp-server/spec.md +33 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/storage-limits/spec.md +90 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/workspace-scoping/spec.md +66 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/tasks.md +199 -0
- package/openspec/changes/codebase-indexing/.openspec.yaml +2 -0
- package/openspec/changes/codebase-indexing/design.md +169 -0
- package/openspec/changes/codebase-indexing/proposal.md +30 -0
- package/openspec/changes/codebase-indexing/specs/codebase-collection/spec.md +187 -0
- package/openspec/changes/codebase-indexing/specs/mcp-server/spec.md +36 -0
- package/openspec/changes/codebase-indexing/tasks.md +56 -0
- package/openspec/specs/mcp-integration-testing/spec.md +50 -0
- package/openspec/specs/mcp-server/spec.md +75 -0
- package/openspec/specs/search-pipeline/spec.md +29 -0
- package/openspec/specs/storage-limits/spec.md +94 -0
- package/openspec/specs/workspace-scoping/spec.md +70 -0
- package/package.json +34 -0
- package/site/build.js +66 -0
- package/site/partials/_api.html +83 -0
- package/site/partials/_compare.html +100 -0
- package/site/partials/_config.html +23 -0
- package/site/partials/_features.html +43 -0
- package/site/partials/_footer.html +6 -0
- package/site/partials/_hero.html +9 -0
- package/site/partials/_how-it-works.html +26 -0
- package/site/partials/_models.html +18 -0
- package/site/partials/_quick-start.html +15 -0
- package/site/partials/_stats.html +1 -0
- package/site/partials/_tech-stack.html +13 -0
- package/site/script.js +12 -0
- package/site/shell.html +44 -0
- package/site/styles.css +548 -0
- package/src/chunker.ts +427 -0
- package/src/codebase.ts +331 -0
- package/src/collections.ts +192 -0
- package/src/embeddings.ts +293 -0
- package/src/expansion.ts +79 -0
- package/src/harvester.ts +306 -0
- package/src/index.ts +503 -0
- package/src/reranker.ts +103 -0
- package/src/search.ts +294 -0
- package/src/server.ts +664 -0
- package/src/storage.ts +221 -0
- package/src/store.ts +623 -0
- package/src/types.ts +202 -0
- package/src/watcher.ts +384 -0
- package/test/chunker.test.ts +479 -0
- package/test/cli.test.ts +309 -0
- package/test/codebase-chunker.test.ts +446 -0
- package/test/codebase.test.ts +678 -0
- package/test/collections.test.ts +571 -0
- package/test/harvester.test.ts +636 -0
- package/test/integration.test.ts +150 -0
- package/test/llm.test.ts +322 -0
- package/test/search.test.ts +572 -0
- package/test/server.test.ts +541 -0
- package/test/storage.test.ts +302 -0
- package/test/store.test.ts +465 -0
- package/test/watcher.test.ts +656 -0
- package/test/workspace.test.ts +239 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +16 -0
package/src/search.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { SearchResult, Store } from './types.js';
|
|
2
|
+
import { computeHash } from './store.js';
|
|
3
|
+
|
|
4
|
+
export interface SearchOptions {
|
|
5
|
+
query: string;
|
|
6
|
+
limit?: number;
|
|
7
|
+
collection?: string;
|
|
8
|
+
useVec?: boolean;
|
|
9
|
+
rerank?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HybridSearchOptions {
|
|
13
|
+
query: string;
|
|
14
|
+
limit?: number;
|
|
15
|
+
collection?: string;
|
|
16
|
+
minScore?: number;
|
|
17
|
+
useExpansion?: boolean;
|
|
18
|
+
useReranking?: boolean;
|
|
19
|
+
topK?: number;
|
|
20
|
+
projectHash?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SearchProviders {
|
|
24
|
+
embedder?: { embed(text: string): Promise<{ embedding: number[] }> } | null;
|
|
25
|
+
reranker?: { rerank(query: string, docs: any[]): Promise<{ results: Array<{ file: string; score: number; index: number }> }> } | null;
|
|
26
|
+
expander?: { expand(query: string): Promise<string[]> } | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function searchFTS(
|
|
30
|
+
store: Store,
|
|
31
|
+
query: string,
|
|
32
|
+
options?: { limit?: number; collection?: string }
|
|
33
|
+
): SearchResult[] {
|
|
34
|
+
const limit = options?.limit;
|
|
35
|
+
const collection = options?.collection;
|
|
36
|
+
return store.searchFTS(query, limit, collection);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function searchVec(
|
|
40
|
+
store: Store,
|
|
41
|
+
query: string,
|
|
42
|
+
embedding: number[],
|
|
43
|
+
options?: { limit?: number; collection?: string }
|
|
44
|
+
): SearchResult[] {
|
|
45
|
+
const limit = options?.limit;
|
|
46
|
+
const collection = options?.collection;
|
|
47
|
+
return store.searchVec(query, embedding, limit, collection);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function rrfFuse(
|
|
51
|
+
resultSets: SearchResult[][],
|
|
52
|
+
k: number = 60,
|
|
53
|
+
weights?: number[]
|
|
54
|
+
): SearchResult[] {
|
|
55
|
+
const scoreMap = new Map<string, { result: SearchResult; score: number }>();
|
|
56
|
+
|
|
57
|
+
resultSets.forEach((results, setIndex) => {
|
|
58
|
+
const weight = weights?.[setIndex] ?? 1;
|
|
59
|
+
|
|
60
|
+
results.forEach((result, rank) => {
|
|
61
|
+
const rrfScore = weight / (k + rank + 1);
|
|
62
|
+
|
|
63
|
+
const existing = scoreMap.get(result.id);
|
|
64
|
+
if (existing) {
|
|
65
|
+
existing.score += rrfScore;
|
|
66
|
+
} else {
|
|
67
|
+
scoreMap.set(result.id, {
|
|
68
|
+
result: { ...result },
|
|
69
|
+
score: rrfScore,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const merged = Array.from(scoreMap.values()).map(({ result, score }) => ({
|
|
76
|
+
...result,
|
|
77
|
+
score,
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
return merged.sort((a, b) => b.score - a.score);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function applyTopRankBonus(
|
|
84
|
+
results: SearchResult[],
|
|
85
|
+
originalFtsResults: SearchResult[]
|
|
86
|
+
): SearchResult[] {
|
|
87
|
+
const bonusMap = new Map<string, number>();
|
|
88
|
+
|
|
89
|
+
if (originalFtsResults.length > 0) {
|
|
90
|
+
bonusMap.set(originalFtsResults[0].id, 0.05);
|
|
91
|
+
}
|
|
92
|
+
if (originalFtsResults.length > 1) {
|
|
93
|
+
bonusMap.set(originalFtsResults[1].id, 0.02);
|
|
94
|
+
}
|
|
95
|
+
if (originalFtsResults.length > 2) {
|
|
96
|
+
bonusMap.set(originalFtsResults[2].id, 0.02);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const boosted = results.map(r => ({
|
|
100
|
+
...r,
|
|
101
|
+
score: r.score + (bonusMap.get(r.id) ?? 0),
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
return boosted.sort((a, b) => b.score - a.score);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function positionAwareBlend(
|
|
108
|
+
rrfResults: SearchResult[],
|
|
109
|
+
rerankScores: Map<string, number>
|
|
110
|
+
): SearchResult[] {
|
|
111
|
+
const blended = rrfResults.map((result, index) => {
|
|
112
|
+
const rerankScore = rerankScores.get(result.id);
|
|
113
|
+
|
|
114
|
+
if (rerankScore === undefined) {
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let rrfWeight: number;
|
|
119
|
+
let rerankWeight: number;
|
|
120
|
+
|
|
121
|
+
if (index <= 2) {
|
|
122
|
+
rrfWeight = 0.75;
|
|
123
|
+
rerankWeight = 0.25;
|
|
124
|
+
} else if (index <= 9) {
|
|
125
|
+
rrfWeight = 0.60;
|
|
126
|
+
rerankWeight = 0.40;
|
|
127
|
+
} else {
|
|
128
|
+
rrfWeight = 0.40;
|
|
129
|
+
rerankWeight = 0.60;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const finalScore = rrfWeight * result.score + rerankWeight * rerankScore;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
...result,
|
|
136
|
+
score: finalScore,
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return blended.sort((a, b) => b.score - a.score);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function formatSnippet(text: string, maxLength: number = 700): string {
|
|
144
|
+
if (text.length <= maxLength) {
|
|
145
|
+
return text;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const truncated = text.substring(0, maxLength);
|
|
149
|
+
const lastSpace = truncated.lastIndexOf(' ');
|
|
150
|
+
|
|
151
|
+
if (lastSpace > maxLength * 0.8) {
|
|
152
|
+
return truncated.substring(0, lastSpace) + '...';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return truncated + '...';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function cacheHash(prefix: string, ...parts: string[]): string {
|
|
159
|
+
return computeHash(prefix + ':' + parts.join(':'));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function hybridSearch(
|
|
163
|
+
store: Store,
|
|
164
|
+
options: HybridSearchOptions,
|
|
165
|
+
providers: SearchProviders = {}
|
|
166
|
+
): Promise<SearchResult[]> {
|
|
167
|
+
const {
|
|
168
|
+
query,
|
|
169
|
+
limit = 10,
|
|
170
|
+
collection,
|
|
171
|
+
minScore = 0,
|
|
172
|
+
useExpansion = true,
|
|
173
|
+
useReranking = true,
|
|
174
|
+
topK = 30,
|
|
175
|
+
projectHash,
|
|
176
|
+
} = options;
|
|
177
|
+
|
|
178
|
+
const { embedder, reranker, expander } = providers;
|
|
179
|
+
|
|
180
|
+
let queries: string[] = [query];
|
|
181
|
+
|
|
182
|
+
if (useExpansion && expander) {
|
|
183
|
+
const expansionCacheKey = cacheHash('expand', query);
|
|
184
|
+
const cached = store.getCachedResult(expansionCacheKey);
|
|
185
|
+
|
|
186
|
+
if (cached) {
|
|
187
|
+
try {
|
|
188
|
+
const variants = JSON.parse(cached) as string[];
|
|
189
|
+
queries = [query, ...variants];
|
|
190
|
+
} catch {
|
|
191
|
+
queries = [query];
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
try {
|
|
195
|
+
const variants = await expander.expand(query);
|
|
196
|
+
store.setCachedResult(expansionCacheKey, JSON.stringify(variants));
|
|
197
|
+
queries = [query, ...variants];
|
|
198
|
+
} catch {
|
|
199
|
+
queries = [query];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const allResultSets: SearchResult[][] = [];
|
|
205
|
+
const weights: number[] = [];
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < queries.length; i++) {
|
|
208
|
+
const q = queries[i];
|
|
209
|
+
const isOriginal = i === 0;
|
|
210
|
+
const weight = isOriginal ? 2 : 1;
|
|
211
|
+
|
|
212
|
+
const ftsResults = store.searchFTS(q, topK, collection, projectHash);
|
|
213
|
+
allResultSets.push(ftsResults);
|
|
214
|
+
weights.push(weight);
|
|
215
|
+
|
|
216
|
+
if (embedder) {
|
|
217
|
+
try {
|
|
218
|
+
const { embedding } = await embedder.embed(q);
|
|
219
|
+
const vecResults = store.searchVec(q, embedding, topK, collection, projectHash);
|
|
220
|
+
allResultSets.push(vecResults);
|
|
221
|
+
weights.push(weight);
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const originalFtsResults = allResultSets[0] || [];
|
|
228
|
+
|
|
229
|
+
let fusedResults = rrfFuse(allResultSets, 60, weights);
|
|
230
|
+
|
|
231
|
+
fusedResults = applyTopRankBonus(fusedResults, originalFtsResults);
|
|
232
|
+
|
|
233
|
+
const candidates = fusedResults.slice(0, topK);
|
|
234
|
+
|
|
235
|
+
if (useReranking && reranker && candidates.length > 0) {
|
|
236
|
+
const candidateIds = candidates.map(c => c.id).join(',');
|
|
237
|
+
const rerankCacheKey = cacheHash('rerank', query, candidateIds);
|
|
238
|
+
const cachedRerank = store.getCachedResult(rerankCacheKey);
|
|
239
|
+
|
|
240
|
+
let rerankScores = new Map<string, number>();
|
|
241
|
+
|
|
242
|
+
if (cachedRerank) {
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(cachedRerank) as Array<{ file: string; score: number }>;
|
|
245
|
+
parsed.forEach(r => rerankScores.set(r.file, r.score));
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
try {
|
|
250
|
+
const docs = candidates.map((c, index) => ({
|
|
251
|
+
text: c.snippet,
|
|
252
|
+
file: c.id,
|
|
253
|
+
index,
|
|
254
|
+
}));
|
|
255
|
+
|
|
256
|
+
const rerankResult = await reranker.rerank(query, docs);
|
|
257
|
+
|
|
258
|
+
rerankResult.results.forEach(r => {
|
|
259
|
+
rerankScores.set(r.file, r.score);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const cacheData = rerankResult.results.map(r => ({
|
|
263
|
+
file: r.file,
|
|
264
|
+
score: r.score,
|
|
265
|
+
}));
|
|
266
|
+
store.setCachedResult(rerankCacheKey, JSON.stringify(cacheData));
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
fusedResults = positionAwareBlend(candidates, rerankScores);
|
|
272
|
+
} else {
|
|
273
|
+
fusedResults = candidates;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let filtered = fusedResults;
|
|
277
|
+
if (minScore > 0) {
|
|
278
|
+
filtered = fusedResults.filter(r => r.score >= minScore);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const final = filtered.slice(0, limit);
|
|
282
|
+
|
|
283
|
+
return final.map(r => ({
|
|
284
|
+
...r,
|
|
285
|
+
snippet: formatSnippet(r.snippet, 700),
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function search(
|
|
290
|
+
store: Store,
|
|
291
|
+
options: SearchOptions
|
|
292
|
+
): Promise<SearchResult[]> {
|
|
293
|
+
return store.searchFTS(options.query, options.limit, options.collection);
|
|
294
|
+
}
|