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,309 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { parseGlobalOptions, showHelp, showVersion, formatSearchOutput } from '../src/index.js';
3
+ import type { SearchResult } from '../src/types.js';
4
+
5
+ function createMockResult(id: string, score: number, snippet: string = 'test snippet'): SearchResult {
6
+ return {
7
+ id,
8
+ path: `path/${id}`,
9
+ collection: 'test',
10
+ title: `Title ${id}`,
11
+ snippet,
12
+ score,
13
+ startLine: 1,
14
+ endLine: 10,
15
+ docid: id.substring(0, 6),
16
+ };
17
+ }
18
+
19
+ describe('CLI Argument Parsing', () => {
20
+ describe('parseGlobalOptions', () => {
21
+ it('should parse --db flag with equals syntax', () => {
22
+ const args = ['--db=/custom/path.db', 'status'];
23
+ const result = parseGlobalOptions(args);
24
+
25
+ expect(result.dbPath).toBe('/custom/path.db');
26
+ expect(result.remaining).toEqual(['status']);
27
+ });
28
+
29
+ it('should parse --db flag with space syntax', () => {
30
+ const args = ['--db', '/custom/path.db', 'status'];
31
+ const result = parseGlobalOptions(args);
32
+
33
+ expect(result.dbPath).toBe('/custom/path.db');
34
+ expect(result.remaining).toEqual(['status']);
35
+ });
36
+
37
+ it('should parse --config flag with equals syntax', () => {
38
+ const args = ['--config=/custom/config.yml', 'update'];
39
+ const result = parseGlobalOptions(args);
40
+
41
+ expect(result.configPath).toBe('/custom/config.yml');
42
+ expect(result.remaining).toEqual(['update']);
43
+ });
44
+
45
+ it('should parse --config flag with space syntax', () => {
46
+ const args = ['--config', '/custom/config.yml', 'update'];
47
+ const result = parseGlobalOptions(args);
48
+
49
+ expect(result.configPath).toBe('/custom/config.yml');
50
+ expect(result.remaining).toEqual(['update']);
51
+ });
52
+
53
+ it('should use default paths when no flags provided', () => {
54
+ const args = ['status'];
55
+ const result = parseGlobalOptions(args);
56
+
57
+ expect(result.dbPath).toContain('.cache/nano-brain/default.sqlite');
58
+ expect(result.configPath).toContain('.config/nano-brain/config.yml');
59
+ expect(result.remaining).toEqual(['status']);
60
+ });
61
+
62
+ it('should extract remaining args after global options', () => {
63
+ const args = ['--db=/test.db', 'search', 'query text', '-n', '20'];
64
+ const result = parseGlobalOptions(args);
65
+
66
+ expect(result.dbPath).toBe('/test.db');
67
+ expect(result.remaining).toEqual(['search', 'query text', '-n', '20']);
68
+ });
69
+
70
+ it('should handle multiple global options', () => {
71
+ const args = ['--db=/test.db', '--config=/test.yml', 'collection', 'list'];
72
+ const result = parseGlobalOptions(args);
73
+
74
+ expect(result.dbPath).toBe('/test.db');
75
+ expect(result.configPath).toBe('/test.yml');
76
+ expect(result.remaining).toEqual(['collection', 'list']);
77
+ });
78
+
79
+ it('should handle no remaining args', () => {
80
+ const args = ['--db=/test.db'];
81
+ const result = parseGlobalOptions(args);
82
+
83
+ expect(result.dbPath).toBe('/test.db');
84
+ expect(result.remaining).toEqual([]);
85
+ });
86
+ });
87
+
88
+ describe('showHelp', () => {
89
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>;
90
+
91
+ beforeEach(() => {
92
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
93
+ });
94
+
95
+ afterEach(() => {
96
+ consoleLogSpy.mockRestore();
97
+ });
98
+
99
+ it('should output help text', () => {
100
+ showHelp();
101
+
102
+ expect(consoleLogSpy).toHaveBeenCalledOnce();
103
+ const output = consoleLogSpy.mock.calls[0][0] as string;
104
+
105
+ expect(output).toContain('nano-brain');
106
+ expect(output).toContain('Usage:');
107
+ expect(output).toContain('Commands:');
108
+ expect(output).toContain('mcp');
109
+ expect(output).toContain('collection');
110
+ expect(output).toContain('status');
111
+ expect(output).toContain('search');
112
+ });
113
+ });
114
+
115
+ describe('showVersion', () => {
116
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>;
117
+
118
+ beforeEach(() => {
119
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
120
+ });
121
+
122
+ afterEach(() => {
123
+ consoleLogSpy.mockRestore();
124
+ });
125
+
126
+ it('should output version', () => {
127
+ showVersion();
128
+
129
+ expect(consoleLogSpy).toHaveBeenCalledWith('nano-brain v0.1.0');
130
+ });
131
+ });
132
+ });
133
+
134
+ describe('Search Output Formatting', () => {
135
+ describe('formatSearchOutput - text', () => {
136
+ it('should format results as readable text', () => {
137
+ const results = [
138
+ createMockResult('doc1', 0.95, 'This is a test snippet'),
139
+ createMockResult('doc2', 0.87, 'Another snippet here'),
140
+ ];
141
+
142
+ const output = formatSearchOutput(results, 'text');
143
+
144
+ expect(output).toContain('[doc1]');
145
+ expect(output).toContain('test/path/doc1');
146
+ expect(output).toContain('Score: 0.9500');
147
+ expect(output).toContain('Title doc1');
148
+ expect(output).toContain('This is a test snippet');
149
+
150
+ expect(output).toContain('[doc2]');
151
+ expect(output).toContain('test/path/doc2');
152
+ expect(output).toContain('Score: 0.8700');
153
+ expect(output).toContain('Another snippet here');
154
+ });
155
+
156
+ it('should handle empty results', () => {
157
+ const output = formatSearchOutput([], 'text');
158
+ expect(output).toBe('');
159
+ });
160
+
161
+ it('should handle results without snippets', () => {
162
+ const results = [
163
+ { ...createMockResult('doc1', 0.95), snippet: '' },
164
+ ];
165
+
166
+ const output = formatSearchOutput(results, 'text');
167
+
168
+ expect(output).toContain('[doc1]');
169
+ expect(output).toContain('Score: 0.9500');
170
+ expect(output).not.toContain('test snippet');
171
+ });
172
+ });
173
+
174
+ describe('formatSearchOutput - json', () => {
175
+ it('should format results as JSON', () => {
176
+ const results = [
177
+ createMockResult('doc1', 0.95, 'Test snippet'),
178
+ ];
179
+
180
+ const output = formatSearchOutput(results, 'json');
181
+ const parsed = JSON.parse(output);
182
+
183
+ expect(parsed).toHaveLength(1);
184
+ expect(parsed[0].id).toBe('doc1');
185
+ expect(parsed[0].score).toBe(0.95);
186
+ expect(parsed[0].snippet).toBe('Test snippet');
187
+ expect(parsed[0].path).toBe('path/doc1');
188
+ });
189
+
190
+ it('should handle empty results as JSON', () => {
191
+ const output = formatSearchOutput([], 'json');
192
+ const parsed = JSON.parse(output);
193
+
194
+ expect(parsed).toEqual([]);
195
+ });
196
+
197
+ it('should format multiple results as JSON array', () => {
198
+ const results = [
199
+ createMockResult('doc1', 0.95),
200
+ createMockResult('doc2', 0.87),
201
+ createMockResult('doc3', 0.75),
202
+ ];
203
+
204
+ const output = formatSearchOutput(results, 'json');
205
+ const parsed = JSON.parse(output);
206
+
207
+ expect(parsed).toHaveLength(3);
208
+ expect(parsed[0].id).toBe('doc1');
209
+ expect(parsed[1].id).toBe('doc2');
210
+ expect(parsed[2].id).toBe('doc3');
211
+ });
212
+ });
213
+
214
+ describe('formatSearchOutput - files', () => {
215
+ it('should show only file paths', () => {
216
+ const results = [
217
+ createMockResult('doc1', 0.95),
218
+ createMockResult('doc2', 0.87),
219
+ ];
220
+
221
+ const output = formatSearchOutput(results, 'files');
222
+
223
+ expect(output).toBe('path/doc1\npath/doc2');
224
+ });
225
+
226
+ it('should handle empty results', () => {
227
+ const output = formatSearchOutput([], 'files');
228
+ expect(output).toBe('');
229
+ });
230
+
231
+ it('should handle single result', () => {
232
+ const results = [createMockResult('doc1', 0.95)];
233
+ const output = formatSearchOutput(results, 'files');
234
+
235
+ expect(output).toBe('path/doc1');
236
+ });
237
+ });
238
+ });
239
+
240
+ describe('Command Dispatch', () => {
241
+ it('should identify mcp command', () => {
242
+ const args = ['mcp', '--http'];
243
+ const result = parseGlobalOptions(args);
244
+
245
+ expect(result.remaining[0]).toBe('mcp');
246
+ expect(result.remaining[1]).toBe('--http');
247
+ });
248
+
249
+ it('should identify collection command', () => {
250
+ const args = ['collection', 'add', 'test', '/path'];
251
+ const result = parseGlobalOptions(args);
252
+
253
+ expect(result.remaining[0]).toBe('collection');
254
+ expect(result.remaining.slice(1)).toEqual(['add', 'test', '/path']);
255
+ });
256
+
257
+ it('should identify status command', () => {
258
+ const args = ['status'];
259
+ const result = parseGlobalOptions(args);
260
+
261
+ expect(result.remaining[0]).toBe('status');
262
+ });
263
+
264
+ it('should identify search command with args', () => {
265
+ const args = ['search', 'test query', '-n', '20'];
266
+ const result = parseGlobalOptions(args);
267
+
268
+ expect(result.remaining[0]).toBe('search');
269
+ expect(result.remaining.slice(1)).toEqual(['test query', '-n', '20']);
270
+ });
271
+
272
+ it('should default to mcp when no command provided', () => {
273
+ const args: string[] = [];
274
+ const result = parseGlobalOptions(args);
275
+
276
+ expect(result.remaining).toEqual([]);
277
+ });
278
+
279
+ it('should handle vsearch command', () => {
280
+ const args = ['vsearch', 'semantic query'];
281
+ const result = parseGlobalOptions(args);
282
+
283
+ expect(result.remaining[0]).toBe('vsearch');
284
+ expect(result.remaining[1]).toBe('semantic query');
285
+ });
286
+
287
+ it('should handle query command', () => {
288
+ const args = ['query', 'hybrid search', '--min-score=0.5'];
289
+ const result = parseGlobalOptions(args);
290
+
291
+ expect(result.remaining[0]).toBe('query');
292
+ expect(result.remaining.slice(1)).toEqual(['hybrid search', '--min-score=0.5']);
293
+ });
294
+
295
+ it('should handle get command', () => {
296
+ const args = ['get', 'abc123', '--full'];
297
+ const result = parseGlobalOptions(args);
298
+
299
+ expect(result.remaining[0]).toBe('get');
300
+ expect(result.remaining.slice(1)).toEqual(['abc123', '--full']);
301
+ });
302
+
303
+ it('should handle harvest command', () => {
304
+ const args = ['harvest'];
305
+ const result = parseGlobalOptions(args);
306
+
307
+ expect(result.remaining[0]).toBe('harvest');
308
+ });
309
+ });