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.
Files changed (79) hide show
  1. package/AGENTS_SNIPPET.md +36 -0
  2. package/CHANGELOG.md +68 -0
  3. package/README.md +281 -0
  4. package/SKILL.md +153 -0
  5. package/bin/cli.js +18 -0
  6. package/index.html +929 -0
  7. package/nano-brain +4 -0
  8. package/opencode-mcp.json +9 -0
  9. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/.openspec.yaml +2 -0
  10. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/design.md +68 -0
  11. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/proposal.md +27 -0
  12. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/mcp-integration-testing/spec.md +50 -0
  13. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/mcp-server/spec.md +40 -0
  14. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/search-pipeline/spec.md +29 -0
  15. package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/tasks.md +37 -0
  16. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/.openspec.yaml +2 -0
  17. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/design.md +111 -0
  18. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/proposal.md +30 -0
  19. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/mcp-server/spec.md +33 -0
  20. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/storage-limits/spec.md +90 -0
  21. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/workspace-scoping/spec.md +66 -0
  22. package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/tasks.md +199 -0
  23. package/openspec/changes/codebase-indexing/.openspec.yaml +2 -0
  24. package/openspec/changes/codebase-indexing/design.md +169 -0
  25. package/openspec/changes/codebase-indexing/proposal.md +30 -0
  26. package/openspec/changes/codebase-indexing/specs/codebase-collection/spec.md +187 -0
  27. package/openspec/changes/codebase-indexing/specs/mcp-server/spec.md +36 -0
  28. package/openspec/changes/codebase-indexing/tasks.md +56 -0
  29. package/openspec/specs/mcp-integration-testing/spec.md +50 -0
  30. package/openspec/specs/mcp-server/spec.md +75 -0
  31. package/openspec/specs/search-pipeline/spec.md +29 -0
  32. package/openspec/specs/storage-limits/spec.md +94 -0
  33. package/openspec/specs/workspace-scoping/spec.md +70 -0
  34. package/package.json +34 -0
  35. package/site/build.js +66 -0
  36. package/site/partials/_api.html +83 -0
  37. package/site/partials/_compare.html +100 -0
  38. package/site/partials/_config.html +23 -0
  39. package/site/partials/_features.html +43 -0
  40. package/site/partials/_footer.html +6 -0
  41. package/site/partials/_hero.html +9 -0
  42. package/site/partials/_how-it-works.html +26 -0
  43. package/site/partials/_models.html +18 -0
  44. package/site/partials/_quick-start.html +15 -0
  45. package/site/partials/_stats.html +1 -0
  46. package/site/partials/_tech-stack.html +13 -0
  47. package/site/script.js +12 -0
  48. package/site/shell.html +44 -0
  49. package/site/styles.css +548 -0
  50. package/src/chunker.ts +427 -0
  51. package/src/codebase.ts +331 -0
  52. package/src/collections.ts +192 -0
  53. package/src/embeddings.ts +293 -0
  54. package/src/expansion.ts +79 -0
  55. package/src/harvester.ts +306 -0
  56. package/src/index.ts +503 -0
  57. package/src/reranker.ts +103 -0
  58. package/src/search.ts +294 -0
  59. package/src/server.ts +664 -0
  60. package/src/storage.ts +221 -0
  61. package/src/store.ts +623 -0
  62. package/src/types.ts +202 -0
  63. package/src/watcher.ts +384 -0
  64. package/test/chunker.test.ts +479 -0
  65. package/test/cli.test.ts +309 -0
  66. package/test/codebase-chunker.test.ts +446 -0
  67. package/test/codebase.test.ts +678 -0
  68. package/test/collections.test.ts +571 -0
  69. package/test/harvester.test.ts +636 -0
  70. package/test/integration.test.ts +150 -0
  71. package/test/llm.test.ts +322 -0
  72. package/test/search.test.ts +572 -0
  73. package/test/server.test.ts +541 -0
  74. package/test/storage.test.ts +302 -0
  75. package/test/store.test.ts +465 -0
  76. package/test/watcher.test.ts +656 -0
  77. package/test/workspace.test.ts +239 -0
  78. package/tsconfig.json +19 -0
  79. package/vitest.config.ts +16 -0
package/src/chunker.ts ADDED
@@ -0,0 +1,427 @@
1
+ import type { MemoryChunk, BreakPoint, CodeFenceRegion } from './types.js';
2
+ import * as path from 'path'
3
+
4
+ export interface ChunkOptions {
5
+ maxChunkSize?: number;
6
+ minChunkSize?: number;
7
+ overlap?: number;
8
+ }
9
+
10
+ export function findBreakPoints(content: string): BreakPoint[] {
11
+ const breakPoints: BreakPoint[] = [];
12
+ const lines = content.split('\n');
13
+ let pos = 0;
14
+
15
+ for (let i = 0; i < lines.length; i++) {
16
+ const line = lines[i];
17
+ const lineNo = i + 1;
18
+
19
+ if (line.startsWith('# ')) {
20
+ breakPoints.push({ pos, score: 100, type: 'h1', lineNo });
21
+ } else if (line.startsWith('## ')) {
22
+ breakPoints.push({ pos, score: 90, type: 'h2', lineNo });
23
+ } else if (line.startsWith('### ')) {
24
+ breakPoints.push({ pos, score: 80, type: 'h3', lineNo });
25
+ } else if (line.startsWith('#### ') || line.startsWith('##### ') || line.startsWith('###### ')) {
26
+ breakPoints.push({ pos, score: 70, type: 'h4-h6', lineNo });
27
+ } else if (line.startsWith('```')) {
28
+ breakPoints.push({ pos, score: 80, type: 'code-fence', lineNo });
29
+ } else if (line.trim() === '---' || line.trim() === '***' || line.trim() === '___') {
30
+ breakPoints.push({ pos, score: 60, type: 'hr', lineNo });
31
+ } else if (line.trim() === '') {
32
+ breakPoints.push({ pos, score: 20, type: 'blank', lineNo });
33
+ } else if (/^(\s*)([-*+]|\d+\.)\s/.test(line)) {
34
+ breakPoints.push({ pos, score: 5, type: 'list', lineNo });
35
+ } else {
36
+ breakPoints.push({ pos, score: 1, type: 'newline', lineNo });
37
+ }
38
+
39
+ pos += line.length + 1;
40
+ }
41
+
42
+ return breakPoints;
43
+ }
44
+
45
+ export function findCodeFences(content: string): CodeFenceRegion[] {
46
+ const regions: CodeFenceRegion[] = [];
47
+ const lines = content.split('\n');
48
+ let pos = 0;
49
+ let fenceStart: number | null = null;
50
+
51
+ for (let i = 0; i < lines.length; i++) {
52
+ const line = lines[i];
53
+
54
+ if (line.startsWith('```')) {
55
+ if (fenceStart === null) {
56
+ fenceStart = pos;
57
+ } else {
58
+ regions.push({ start: fenceStart, end: pos + line.length });
59
+ fenceStart = null;
60
+ }
61
+ }
62
+
63
+ pos += line.length + 1;
64
+ }
65
+
66
+ if (fenceStart !== null) {
67
+ regions.push({ start: fenceStart, end: content.length });
68
+ }
69
+
70
+ return regions;
71
+ }
72
+
73
+ export function findBestCutoff(
74
+ breakPoints: BreakPoint[],
75
+ targetPos: number,
76
+ windowSize: number,
77
+ codeFences: CodeFenceRegion[]
78
+ ): number {
79
+ const windowStart = targetPos - windowSize;
80
+ const windowEnd = targetPos + windowSize;
81
+
82
+ const candidateBreaks = breakPoints.filter(
83
+ bp => bp.pos >= windowStart && bp.pos <= windowEnd
84
+ );
85
+
86
+ if (candidateBreaks.length === 0) {
87
+ const insideTargetFence = codeFences.some(
88
+ fence => targetPos >= fence.start && targetPos < fence.end
89
+ );
90
+ if (insideTargetFence) {
91
+ const fence = codeFences.find(f => targetPos >= f.start && targetPos < f.end);
92
+ if (fence) {
93
+ return fence.end;
94
+ }
95
+ }
96
+ return targetPos;
97
+ }
98
+
99
+ let bestBreak = candidateBreaks[0];
100
+ let bestScore = -1;
101
+
102
+ for (const bp of candidateBreaks) {
103
+ const insideFence = codeFences.some(
104
+ fence => bp.pos >= fence.start && bp.pos < fence.end
105
+ );
106
+
107
+ if (insideFence) {
108
+ continue;
109
+ }
110
+
111
+ const distance = Math.abs(bp.pos - targetPos);
112
+ const distancePenalty = Math.pow(distance / windowSize, 2) * 0.7;
113
+ const finalScore = bp.score * (1 - distancePenalty);
114
+
115
+ if (finalScore > bestScore) {
116
+ bestScore = finalScore;
117
+ bestBreak = bp;
118
+ }
119
+ }
120
+
121
+ if (bestScore === -1) {
122
+ const insideTargetFence = codeFences.some(
123
+ fence => targetPos >= fence.start && targetPos < fence.end
124
+ );
125
+ if (insideTargetFence) {
126
+ const fence = codeFences.find(f => targetPos >= f.start && targetPos < f.end);
127
+ if (fence) {
128
+ return fence.end;
129
+ }
130
+ }
131
+ return targetPos;
132
+ }
133
+
134
+ return bestBreak.pos;
135
+ }
136
+
137
+ export function chunkMarkdown(
138
+ content: string,
139
+ hash: string,
140
+ options?: ChunkOptions
141
+ ): MemoryChunk[] {
142
+ const maxChunkSize = options?.maxChunkSize ?? 3600;
143
+ const minChunkSize = options?.minChunkSize ?? 200;
144
+ const overlap = options?.overlap ?? 540;
145
+ const windowSize = 800;
146
+
147
+ if (content.length <= maxChunkSize) {
148
+ return [{
149
+ hash,
150
+ seq: 0,
151
+ pos: 0,
152
+ text: content,
153
+ startLine: 1,
154
+ endLine: content.split('\n').length,
155
+ }];
156
+ }
157
+
158
+ const breakPoints = findBreakPoints(content);
159
+ const codeFences = findCodeFences(content);
160
+ const chunks: MemoryChunk[] = [];
161
+ let currentPos = 0;
162
+ let seq = 0;
163
+ let runningLineCount = 1;
164
+ while (currentPos < content.length) {
165
+ const targetPos = currentPos + maxChunkSize;
166
+ let cutoff: number;
167
+ if (targetPos >= content.length) {
168
+ cutoff = content.length;
169
+ } else {
170
+ cutoff = findBestCutoff(breakPoints, targetPos, windowSize, codeFences);
171
+ }
172
+ const chunkText = content.slice(currentPos, cutoff);
173
+ const startLine = runningLineCount;
174
+ const endLine = startLine + chunkText.split('\n').length - 1;
175
+ chunks.push({
176
+ hash,
177
+ seq,
178
+ pos: currentPos,
179
+ text: chunkText,
180
+ startLine,
181
+ endLine,
182
+ });
183
+ if (cutoff >= content.length) {
184
+ break;
185
+ }
186
+ const nextPos = cutoff - overlap;
187
+ const prevPos = currentPos;
188
+ if (nextPos <= currentPos) {
189
+ currentPos = cutoff;
190
+ } else {
191
+ currentPos = nextPos;
192
+ }
193
+ const advancedSlice = content.slice(prevPos, currentPos);
194
+ runningLineCount += (advancedSlice.match(/\n/g) || []).length;
195
+ seq++;
196
+ }
197
+ return chunks;
198
+ }
199
+
200
+
201
+ const EXTENSION_TO_LANGUAGE: Record<string, string> = {
202
+ '.ts': 'typescript',
203
+ '.tsx': 'typescript',
204
+ '.js': 'javascript',
205
+ '.jsx': 'javascript',
206
+ '.mjs': 'javascript',
207
+ '.cjs': 'javascript',
208
+ '.py': 'python',
209
+ '.pyi': 'python',
210
+ '.go': 'go',
211
+ '.rs': 'rust',
212
+ '.java': 'java',
213
+ '.kt': 'kotlin',
214
+ '.kts': 'kotlin',
215
+ '.rb': 'ruby',
216
+ '.erb': 'ruby',
217
+ '.c': 'c',
218
+ '.h': 'c',
219
+ '.cpp': 'cpp',
220
+ '.hpp': 'cpp',
221
+ '.cc': 'cpp',
222
+ '.cs': 'csharp',
223
+ '.swift': 'swift',
224
+ '.php': 'php',
225
+ '.sh': 'bash',
226
+ '.bash': 'bash',
227
+ '.zsh': 'zsh',
228
+ '.json': 'json',
229
+ '.yaml': 'yaml',
230
+ '.yml': 'yaml',
231
+ '.toml': 'toml',
232
+ '.md': 'markdown',
233
+ '.sql': 'sql',
234
+ '.html': 'html',
235
+ '.css': 'css',
236
+ '.scss': 'scss',
237
+ '.less': 'less',
238
+ '.vue': 'vue',
239
+ '.svelte': 'svelte',
240
+ }
241
+
242
+ export function inferLanguage(filePath: string): string {
243
+ const ext = path.extname(filePath).toLowerCase()
244
+ return EXTENSION_TO_LANGUAGE[ext] || 'text'
245
+ }
246
+
247
+ const FUNCTION_DEF_PATTERNS = [
248
+ /^(export\s+)?(async\s+)?function\s+/,
249
+ /^(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\(/,
250
+ /^(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?function/,
251
+ /^(export\s+)?class\s+/,
252
+ /^(export\s+)?(interface|type)\s+/,
253
+ /^def\s+\w+\s*\(/,
254
+ /^class\s+\w+/,
255
+ /^func\s+\w+\s*\(/,
256
+ /^fn\s+\w+/,
257
+ /^pub\s+(fn|struct|enum|trait)\s+/,
258
+ /^(public|private|protected)?\s*(static)?\s*(async)?\s*\w+\s*\([^)]*\)\s*{/,
259
+ ]
260
+
261
+ const IMPORT_EXPORT_PATTERNS = [
262
+ /^import\s+/,
263
+ /^export\s+/,
264
+ /^from\s+/,
265
+ /^require\s*\(/,
266
+ /^module\.exports/,
267
+ /^package\s+/,
268
+ /^use\s+/,
269
+ ]
270
+
271
+ export function findSourceCodeBreakPoints(content: string): BreakPoint[] {
272
+ const breakPoints: BreakPoint[] = []
273
+ const lines = content.split('\n')
274
+ let pos = 0
275
+ let prevLineBlank = false
276
+
277
+ for (let i = 0; i < lines.length; i++) {
278
+ const line = lines[i]
279
+ const lineNo = i + 1
280
+ const trimmed = line.trim()
281
+
282
+ if (trimmed === '') {
283
+ if (prevLineBlank) {
284
+ breakPoints.push({ pos, score: 90, type: 'double-blank', lineNo })
285
+ } else {
286
+ breakPoints.push({ pos, score: 40, type: 'blank', lineNo })
287
+ }
288
+ prevLineBlank = true
289
+ } else {
290
+ prevLineBlank = false
291
+
292
+ let matched = false
293
+
294
+ for (const pattern of FUNCTION_DEF_PATTERNS) {
295
+ if (pattern.test(trimmed)) {
296
+ breakPoints.push({ pos, score: 80, type: 'function-def', lineNo })
297
+ matched = true
298
+ break
299
+ }
300
+ }
301
+
302
+ if (!matched) {
303
+ for (const pattern of IMPORT_EXPORT_PATTERNS) {
304
+ if (pattern.test(trimmed)) {
305
+ breakPoints.push({ pos, score: 60, type: 'import-export', lineNo })
306
+ matched = true
307
+ break
308
+ }
309
+ }
310
+ }
311
+
312
+ if (!matched) {
313
+ breakPoints.push({ pos, score: 1, type: 'line', lineNo })
314
+ }
315
+ }
316
+
317
+ pos += line.length + 1
318
+ }
319
+
320
+ return breakPoints
321
+ }
322
+
323
+ export function chunkSourceCode(
324
+ content: string,
325
+ hash: string,
326
+ filePath: string,
327
+ workspaceRoot: string,
328
+ options?: ChunkOptions
329
+ ): MemoryChunk[] {
330
+ const maxChunkSize = options?.maxChunkSize ?? 3600
331
+ const overlap = options?.overlap ?? 540
332
+ const windowSize = 800
333
+
334
+ const relativePath = path.relative(workspaceRoot, filePath)
335
+ const language = inferLanguage(filePath)
336
+
337
+ const createMetadataHeader = (startLine: number, endLine: number): string => {
338
+ return `File: ${relativePath}\nLanguage: ${language}\nLines: ${startLine}-${endLine}\n\n`
339
+ }
340
+
341
+ if (content.length <= maxChunkSize) {
342
+ const lineCount = content.split('\n').length
343
+ const header = createMetadataHeader(1, lineCount)
344
+ return [{
345
+ hash,
346
+ seq: 0,
347
+ pos: 0,
348
+ text: header + content,
349
+ startLine: 1,
350
+ endLine: lineCount,
351
+ }]
352
+ }
353
+
354
+ const breakPoints = findSourceCodeBreakPoints(content)
355
+ const chunks: MemoryChunk[] = []
356
+ let currentPos = 0
357
+ let seq = 0
358
+ let runningLineCount = 1
359
+ while (currentPos < content.length) {
360
+ const targetPos = currentPos + maxChunkSize
361
+ let cutoff: number
362
+ if (targetPos >= content.length) {
363
+ cutoff = content.length
364
+ } else {
365
+ cutoff = findBestSourceCodeCutoff(breakPoints, targetPos, windowSize)
366
+ }
367
+ const chunkText = content.slice(currentPos, cutoff)
368
+ const startLine = runningLineCount
369
+ const endLine = startLine + chunkText.split('\n').length - 1
370
+ const header = createMetadataHeader(startLine, endLine)
371
+ chunks.push({
372
+ hash,
373
+ seq,
374
+ pos: currentPos,
375
+ text: header + chunkText,
376
+ startLine,
377
+ endLine,
378
+ })
379
+ if (cutoff >= content.length) {
380
+ break
381
+ }
382
+ const nextPos = cutoff - overlap
383
+ const prevPos = currentPos
384
+ if (nextPos <= currentPos) {
385
+ currentPos = cutoff
386
+ } else {
387
+ currentPos = nextPos
388
+ }
389
+ const advancedSlice = content.slice(prevPos, currentPos)
390
+ runningLineCount += (advancedSlice.match(/\n/g) || []).length
391
+ seq++
392
+ }
393
+ return chunks
394
+ }
395
+
396
+ function findBestSourceCodeCutoff(
397
+ breakPoints: BreakPoint[],
398
+ targetPos: number,
399
+ windowSize: number
400
+ ): number {
401
+ const windowStart = targetPos - windowSize
402
+ const windowEnd = targetPos + windowSize
403
+
404
+ const candidateBreaks = breakPoints.filter(
405
+ bp => bp.pos >= windowStart && bp.pos <= windowEnd
406
+ )
407
+
408
+ if (candidateBreaks.length === 0) {
409
+ return targetPos
410
+ }
411
+
412
+ let bestBreak = candidateBreaks[0]
413
+ let bestScore = -1
414
+
415
+ for (const bp of candidateBreaks) {
416
+ const distance = Math.abs(bp.pos - targetPos)
417
+ const distancePenalty = Math.pow(distance / windowSize, 2) * 0.7
418
+ const finalScore = bp.score * (1 - distancePenalty)
419
+
420
+ if (finalScore > bestScore) {
421
+ bestScore = finalScore
422
+ bestBreak = bp
423
+ }
424
+ }
425
+
426
+ return bestBreak.pos
427
+ }