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
@@ -0,0 +1,465 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { createStore, computeHash, indexDocument } from '../src/store.js';
3
+ import type { Store } from '../src/types.js';
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import * as os from 'os';
7
+
8
+ describe('Store', () => {
9
+ let store: Store;
10
+ let dbPath: string;
11
+
12
+ beforeEach(() => {
13
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nano-brain-test-'));
14
+ dbPath = path.join(tmpDir, 'test.db');
15
+ store = createStore(dbPath);
16
+ });
17
+
18
+ afterEach(() => {
19
+ store.close();
20
+ const dir = path.dirname(dbPath);
21
+ if (fs.existsSync(dir)) {
22
+ fs.rmSync(dir, { recursive: true, force: true });
23
+ }
24
+ });
25
+
26
+ describe('schema creation', () => {
27
+ it('should create all required tables', () => {
28
+ const health = store.getIndexHealth();
29
+ expect(health).toBeDefined();
30
+ expect(health.documentCount).toBe(0);
31
+ expect(health.chunkCount).toBe(0);
32
+ });
33
+ });
34
+
35
+ describe('insertContent + insertDocument', () => {
36
+ it('should insert content and document', () => {
37
+ const body = '# Test Document\n\nThis is test content.';
38
+ const hash = computeHash(body);
39
+
40
+ store.insertContent(hash, body);
41
+ const docId = store.insertDocument({
42
+ collection: 'test-collection',
43
+ path: 'test/doc.md',
44
+ title: 'Test Document',
45
+ hash,
46
+ createdAt: new Date().toISOString(),
47
+ modifiedAt: new Date().toISOString(),
48
+ active: true,
49
+ });
50
+
51
+ expect(docId).toBeGreaterThan(0);
52
+ });
53
+ });
54
+
55
+ describe('findDocument', () => {
56
+ it('should find document by path', () => {
57
+ const body = '# Find Me\n\nContent here.';
58
+ const hash = computeHash(body);
59
+
60
+ store.insertContent(hash, body);
61
+ store.insertDocument({
62
+ collection: 'docs',
63
+ path: 'find/me.md',
64
+ title: 'Find Me',
65
+ hash,
66
+ createdAt: new Date().toISOString(),
67
+ modifiedAt: new Date().toISOString(),
68
+ active: true,
69
+ });
70
+
71
+ const doc = store.findDocument('find/me.md');
72
+ expect(doc).not.toBeNull();
73
+ expect(doc?.title).toBe('Find Me');
74
+ expect(doc?.collection).toBe('docs');
75
+ });
76
+
77
+ it('should find document by docid (6-char hash prefix)', () => {
78
+ const body = '# Docid Test\n\nFind by hash prefix.';
79
+ const hash = computeHash(body);
80
+ const docid = hash.substring(0, 6);
81
+
82
+ store.insertContent(hash, body);
83
+ store.insertDocument({
84
+ collection: 'docs',
85
+ path: 'docid/test.md',
86
+ title: 'Docid Test',
87
+ hash,
88
+ createdAt: new Date().toISOString(),
89
+ modifiedAt: new Date().toISOString(),
90
+ active: true,
91
+ });
92
+
93
+ const doc = store.findDocument(docid);
94
+ expect(doc).not.toBeNull();
95
+ expect(doc?.title).toBe('Docid Test');
96
+ });
97
+
98
+ it('should return null for non-existent document', () => {
99
+ const doc = store.findDocument('nonexistent/path.md');
100
+ expect(doc).toBeNull();
101
+ });
102
+ });
103
+
104
+ describe('deactivateDocument', () => {
105
+ it('should deactivate a document', () => {
106
+ const body = '# Deactivate Me';
107
+ const hash = computeHash(body);
108
+
109
+ store.insertContent(hash, body);
110
+ store.insertDocument({
111
+ collection: 'docs',
112
+ path: 'deactivate/me.md',
113
+ title: 'Deactivate Me',
114
+ hash,
115
+ createdAt: new Date().toISOString(),
116
+ modifiedAt: new Date().toISOString(),
117
+ active: true,
118
+ });
119
+
120
+ let doc = store.findDocument('deactivate/me.md');
121
+ expect(doc).not.toBeNull();
122
+
123
+ store.deactivateDocument('docs', 'deactivate/me.md');
124
+
125
+ doc = store.findDocument('deactivate/me.md');
126
+ expect(doc).toBeNull();
127
+ });
128
+ });
129
+
130
+ describe('FTS trigger', () => {
131
+ it('should index document in FTS on insert', () => {
132
+ const body = '# Searchable Document\n\nThis contains unique searchterm xyz123.';
133
+ const hash = computeHash(body);
134
+
135
+ store.insertContent(hash, body);
136
+ store.insertDocument({
137
+ collection: 'searchable',
138
+ path: 'search/doc.md',
139
+ title: 'Searchable Document',
140
+ hash,
141
+ createdAt: new Date().toISOString(),
142
+ modifiedAt: new Date().toISOString(),
143
+ active: true,
144
+ });
145
+
146
+ const results = store.searchFTS('xyz123');
147
+ expect(results.length).toBe(1);
148
+ expect(results[0].title).toBe('Searchable Document');
149
+ });
150
+
151
+ it('should search by title', () => {
152
+ const body = '# Unique Title ABC\n\nSome content.';
153
+ const hash = computeHash(body);
154
+
155
+ store.insertContent(hash, body);
156
+ store.insertDocument({
157
+ collection: 'titles',
158
+ path: 'title/doc.md',
159
+ title: 'Unique Title ABC',
160
+ hash,
161
+ createdAt: new Date().toISOString(),
162
+ modifiedAt: new Date().toISOString(),
163
+ active: true,
164
+ });
165
+
166
+ const results = store.searchFTS('Unique Title ABC');
167
+ expect(results.length).toBe(1);
168
+ });
169
+
170
+ it('should filter by collection', () => {
171
+ const body1 = '# Doc One\n\nShared keyword findme.';
172
+ const hash1 = computeHash(body1);
173
+ const body2 = '# Doc Two\n\nShared keyword findme.';
174
+ const hash2 = computeHash(body2);
175
+
176
+ store.insertContent(hash1, body1);
177
+ store.insertDocument({
178
+ collection: 'collection-a',
179
+ path: 'a/doc.md',
180
+ title: 'Doc One',
181
+ hash: hash1,
182
+ createdAt: new Date().toISOString(),
183
+ modifiedAt: new Date().toISOString(),
184
+ active: true,
185
+ });
186
+
187
+ store.insertContent(hash2, body2);
188
+ store.insertDocument({
189
+ collection: 'collection-b',
190
+ path: 'b/doc.md',
191
+ title: 'Doc Two',
192
+ hash: hash2,
193
+ createdAt: new Date().toISOString(),
194
+ modifiedAt: new Date().toISOString(),
195
+ active: true,
196
+ });
197
+
198
+ const allResults = store.searchFTS('findme');
199
+ expect(allResults.length).toBe(2);
200
+
201
+ const filteredResults = store.searchFTS('findme', 10, 'collection-a');
202
+ expect(filteredResults.length).toBe(1);
203
+ expect(filteredResults[0].collection).toBe('collection-a');
204
+ });
205
+ });
206
+
207
+ describe('getIndexHealth', () => {
208
+ it('should return correct counts', () => {
209
+ const body1 = '# Health Doc 1';
210
+ const hash1 = computeHash(body1);
211
+ const body2 = '# Health Doc 2';
212
+ const hash2 = computeHash(body2);
213
+
214
+ store.insertContent(hash1, body1);
215
+ store.insertDocument({
216
+ collection: 'health',
217
+ path: 'health/doc1.md',
218
+ title: 'Health Doc 1',
219
+ hash: hash1,
220
+ createdAt: new Date().toISOString(),
221
+ modifiedAt: new Date().toISOString(),
222
+ active: true,
223
+ });
224
+
225
+ store.insertContent(hash2, body2);
226
+ store.insertDocument({
227
+ collection: 'health',
228
+ path: 'health/doc2.md',
229
+ title: 'Health Doc 2',
230
+ hash: hash2,
231
+ createdAt: new Date().toISOString(),
232
+ modifiedAt: new Date().toISOString(),
233
+ active: true,
234
+ });
235
+
236
+ const health = store.getIndexHealth();
237
+ expect(health.documentCount).toBe(2);
238
+ expect(health.collections.length).toBe(1);
239
+ expect(health.collections[0].name).toBe('health');
240
+ expect(health.collections[0].documentCount).toBe(2);
241
+ });
242
+ });
243
+
244
+ describe('getDocumentBody', () => {
245
+ it('should return full body', () => {
246
+ const body = '# Line 1\nLine 2\nLine 3\nLine 4';
247
+ const hash = computeHash(body);
248
+
249
+ store.insertContent(hash, body);
250
+
251
+ const result = store.getDocumentBody(hash);
252
+ expect(result).toBe(body);
253
+ });
254
+
255
+ it('should return partial body with fromLine and maxLines', () => {
256
+ const body = 'Line 0\nLine 1\nLine 2\nLine 3\nLine 4';
257
+ const hash = computeHash(body);
258
+
259
+ store.insertContent(hash, body);
260
+
261
+ const result = store.getDocumentBody(hash, 1, 2);
262
+ expect(result).toBe('Line 1\nLine 2');
263
+ });
264
+ });
265
+
266
+ describe('LLM cache', () => {
267
+ it('should store and retrieve cached results', () => {
268
+ const hash = 'test-cache-key';
269
+ const result = '{"expanded": ["query1", "query2"]}';
270
+
271
+ expect(store.getCachedResult(hash)).toBeNull();
272
+
273
+ store.setCachedResult(hash, result);
274
+
275
+ expect(store.getCachedResult(hash)).toBe(result);
276
+ });
277
+ });
278
+
279
+ describe('bulkDeactivateExcept', () => {
280
+ it('should deactivate documents not in active list', () => {
281
+ const body1 = '# Doc 1';
282
+ const hash1 = computeHash(body1);
283
+ const body2 = '# Doc 2';
284
+ const hash2 = computeHash(body2);
285
+ const body3 = '# Doc 3';
286
+ const hash3 = computeHash(body3);
287
+
288
+ store.insertContent(hash1, body1);
289
+ store.insertDocument({
290
+ collection: 'bulk-test',
291
+ path: 'doc1.md',
292
+ title: 'Doc 1',
293
+ hash: hash1,
294
+ createdAt: new Date().toISOString(),
295
+ modifiedAt: new Date().toISOString(),
296
+ active: true,
297
+ });
298
+
299
+ store.insertContent(hash2, body2);
300
+ store.insertDocument({
301
+ collection: 'bulk-test',
302
+ path: 'doc2.md',
303
+ title: 'Doc 2',
304
+ hash: hash2,
305
+ createdAt: new Date().toISOString(),
306
+ modifiedAt: new Date().toISOString(),
307
+ active: true,
308
+ });
309
+
310
+ store.insertContent(hash3, body3);
311
+ store.insertDocument({
312
+ collection: 'bulk-test',
313
+ path: 'doc3.md',
314
+ title: 'Doc 3',
315
+ hash: hash3,
316
+ createdAt: new Date().toISOString(),
317
+ modifiedAt: new Date().toISOString(),
318
+ active: true,
319
+ });
320
+
321
+ const deactivatedCount = store.bulkDeactivateExcept('bulk-test', ['doc1.md', 'doc3.md']);
322
+ expect(deactivatedCount).toBe(1);
323
+
324
+ expect(store.findDocument('doc1.md')).not.toBeNull();
325
+ expect(store.findDocument('doc2.md')).toBeNull();
326
+ expect(store.findDocument('doc3.md')).not.toBeNull();
327
+ });
328
+
329
+ it('should deactivate all documents when active list is empty', () => {
330
+ const body1 = '# Doc A';
331
+ const hash1 = computeHash(body1);
332
+ const body2 = '# Doc B';
333
+ const hash2 = computeHash(body2);
334
+
335
+ store.insertContent(hash1, body1);
336
+ store.insertDocument({
337
+ collection: 'empty-test',
338
+ path: 'docA.md',
339
+ title: 'Doc A',
340
+ hash: hash1,
341
+ createdAt: new Date().toISOString(),
342
+ modifiedAt: new Date().toISOString(),
343
+ active: true,
344
+ });
345
+
346
+ store.insertContent(hash2, body2);
347
+ store.insertDocument({
348
+ collection: 'empty-test',
349
+ path: 'docB.md',
350
+ title: 'Doc B',
351
+ hash: hash2,
352
+ createdAt: new Date().toISOString(),
353
+ modifiedAt: new Date().toISOString(),
354
+ active: true,
355
+ });
356
+
357
+ const deactivatedCount = store.bulkDeactivateExcept('empty-test', []);
358
+ expect(deactivatedCount).toBe(2);
359
+
360
+ expect(store.findDocument('docA.md')).toBeNull();
361
+ expect(store.findDocument('docB.md')).toBeNull();
362
+ });
363
+
364
+ it('should only affect specified collection', () => {
365
+ const body1 = '# Collection A Doc';
366
+ const hash1 = computeHash(body1);
367
+ const body2 = '# Collection B Doc';
368
+ const hash2 = computeHash(body2);
369
+
370
+ store.insertContent(hash1, body1);
371
+ store.insertDocument({
372
+ collection: 'collection-a',
373
+ path: 'doc.md',
374
+ title: 'Collection A Doc',
375
+ hash: hash1,
376
+ createdAt: new Date().toISOString(),
377
+ modifiedAt: new Date().toISOString(),
378
+ active: true,
379
+ });
380
+
381
+ store.insertContent(hash2, body2);
382
+ store.insertDocument({
383
+ collection: 'collection-b',
384
+ path: 'doc.md',
385
+ title: 'Collection B Doc',
386
+ hash: hash2,
387
+ createdAt: new Date().toISOString(),
388
+ modifiedAt: new Date().toISOString(),
389
+ active: true,
390
+ });
391
+
392
+ const deactivatedCount = store.bulkDeactivateExcept('collection-a', []);
393
+ expect(deactivatedCount).toBe(1);
394
+
395
+ const docA = store.findDocument('doc.md');
396
+ expect(docA).not.toBeNull();
397
+ expect(docA?.collection).toBe('collection-b');
398
+ });
399
+ });
400
+
401
+ describe('indexDocument', () => {
402
+ it('should index a new document', () => {
403
+ const content = '# Test Document\n\nThis is test content for indexing.';
404
+ const result = indexDocument(store, 'test-collection', 'test/doc.md', content, 'Test Document');
405
+
406
+ expect(result.skipped).toBe(false);
407
+ expect(result.chunks).toBeGreaterThan(0);
408
+ expect(result.hash).toBe(computeHash(content));
409
+
410
+ const doc = store.findDocument('test/doc.md');
411
+ expect(doc).not.toBeNull();
412
+ expect(doc?.title).toBe('Test Document');
413
+ expect(doc?.collection).toBe('test-collection');
414
+ expect(doc?.hash).toBe(result.hash);
415
+ });
416
+
417
+ it('should skip indexing when content hash matches', () => {
418
+ const content = '# Unchanged Document\n\nThis content will not change.';
419
+
420
+ const result1 = indexDocument(store, 'test-collection', 'unchanged/doc.md', content, 'Unchanged Document');
421
+ expect(result1.skipped).toBe(false);
422
+ expect(result1.chunks).toBeGreaterThan(0);
423
+
424
+ const result2 = indexDocument(store, 'test-collection', 'unchanged/doc.md', content, 'Unchanged Document');
425
+ expect(result2.skipped).toBe(true);
426
+ expect(result2.chunks).toBe(0);
427
+ expect(result2.hash).toBe(result1.hash);
428
+ });
429
+
430
+ it('should re-index when content changes', () => {
431
+ const content1 = '# Original Content\n\nThis is the original version.';
432
+ const content2 = '# Updated Content\n\nThis is the updated version.';
433
+
434
+ const result1 = indexDocument(store, 'test-collection', 'updated/doc.md', content1, 'Document');
435
+ expect(result1.skipped).toBe(false);
436
+ const hash1 = result1.hash;
437
+
438
+ const result2 = indexDocument(store, 'test-collection', 'updated/doc.md', content2, 'Document');
439
+ expect(result2.skipped).toBe(false);
440
+ expect(result2.chunks).toBeGreaterThan(0);
441
+ expect(result2.hash).not.toBe(hash1);
442
+
443
+ const doc = store.findDocument('updated/doc.md');
444
+ expect(doc?.hash).toBe(result2.hash);
445
+ });
446
+
447
+ it('should make document searchable via FTS', () => {
448
+ const content = '# Searchable Content\n\nThis document contains the unique term xyzabc123.';
449
+ indexDocument(store, 'search-test', 'searchable/doc.md', content, 'Searchable Content');
450
+
451
+ const results = store.searchFTS('xyzabc123');
452
+ expect(results.length).toBe(1);
453
+ expect(results[0].title).toBe('Searchable Content');
454
+ expect(results[0].collection).toBe('search-test');
455
+ });
456
+
457
+ it('should handle large documents with multiple chunks', () => {
458
+ const largeContent = '# Large Document\n\n' + 'Lorem ipsum dolor sit amet. '.repeat(500);
459
+ const result = indexDocument(store, 'large-test', 'large/doc.md', largeContent, 'Large Document');
460
+
461
+ expect(result.skipped).toBe(false);
462
+ expect(result.chunks).toBeGreaterThan(1);
463
+ });
464
+ });
465
+ });