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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { createStore, computeHash } 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
|
+
import * as crypto from 'crypto';
|
|
8
|
+
|
|
9
|
+
describe('Workspace Scoping', () => {
|
|
10
|
+
let store: Store;
|
|
11
|
+
let dbPath: string;
|
|
12
|
+
let tmpDir: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nano-brain-workspace-test-'));
|
|
16
|
+
dbPath = path.join(tmpDir, 'test.db');
|
|
17
|
+
store = createStore(dbPath);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
store.close();
|
|
22
|
+
if (fs.existsSync(tmpDir)) {
|
|
23
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('migration', () => {
|
|
28
|
+
it('should add project_hash column on first create', () => {
|
|
29
|
+
const Database = require('better-sqlite3');
|
|
30
|
+
const db = new Database(dbPath, { readonly: true });
|
|
31
|
+
const columns = db.prepare("PRAGMA table_info(documents)").all() as Array<{ name: string }>;
|
|
32
|
+
db.close();
|
|
33
|
+
|
|
34
|
+
const hasProjectHash = columns.some(col => col.name === 'project_hash');
|
|
35
|
+
expect(hasProjectHash).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should not fail on subsequent creates', () => {
|
|
39
|
+
store.close();
|
|
40
|
+
|
|
41
|
+
expect(() => {
|
|
42
|
+
const store2 = createStore(dbPath);
|
|
43
|
+
store2.close();
|
|
44
|
+
}).not.toThrow();
|
|
45
|
+
|
|
46
|
+
store = createStore(dbPath);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should backfill project_hash from session paths on migration', () => {
|
|
50
|
+
const Database = require('better-sqlite3');
|
|
51
|
+
store.close();
|
|
52
|
+
const db = new Database(dbPath);
|
|
53
|
+
// Must drop index before dropping column in SQLite
|
|
54
|
+
db.exec("DROP INDEX IF EXISTS idx_documents_project_hash");
|
|
55
|
+
db.exec("ALTER TABLE documents DROP COLUMN project_hash");
|
|
56
|
+
const body = '# Session Doc\n\nContent.';
|
|
57
|
+
const hash = computeHash(body);
|
|
58
|
+
const sessionPath = 'sessions/abc123def456/file.md';
|
|
59
|
+
db.prepare("INSERT OR IGNORE INTO content (hash, body) VALUES (?, ?)").run(hash, body);
|
|
60
|
+
db.prepare(`
|
|
61
|
+
INSERT INTO documents (collection, path, title, hash, created_at, modified_at, active)
|
|
62
|
+
VALUES (?, ?, ?, ?, datetime('now'), datetime('now'), 1)
|
|
63
|
+
`).run('sessions', sessionPath, 'Session Doc', hash);
|
|
64
|
+
db.close();
|
|
65
|
+
store = createStore(dbPath);
|
|
66
|
+
const doc = store.findDocument(sessionPath);
|
|
67
|
+
expect(doc).not.toBeNull();
|
|
68
|
+
expect(doc?.projectHash).toBe('abc123def456');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('document tagging', () => {
|
|
73
|
+
it('should set projectHash when provided', () => {
|
|
74
|
+
const body = '# Tagged Doc\n\nContent.';
|
|
75
|
+
const hash = computeHash(body);
|
|
76
|
+
|
|
77
|
+
store.insertContent(hash, body);
|
|
78
|
+
store.insertDocument({
|
|
79
|
+
collection: 'test',
|
|
80
|
+
path: 'tagged/doc.md',
|
|
81
|
+
title: 'Tagged Doc',
|
|
82
|
+
hash,
|
|
83
|
+
createdAt: new Date().toISOString(),
|
|
84
|
+
modifiedAt: new Date().toISOString(),
|
|
85
|
+
active: true,
|
|
86
|
+
projectHash: 'abc123def456',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const doc = store.findDocument('tagged/doc.md');
|
|
90
|
+
expect(doc).not.toBeNull();
|
|
91
|
+
expect(doc?.projectHash).toBe('abc123def456');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should default projectHash to global', () => {
|
|
95
|
+
const body = '# Global Doc\n\nContent.';
|
|
96
|
+
const hash = computeHash(body);
|
|
97
|
+
|
|
98
|
+
store.insertContent(hash, body);
|
|
99
|
+
store.insertDocument({
|
|
100
|
+
collection: 'test',
|
|
101
|
+
path: 'global/doc.md',
|
|
102
|
+
title: 'Global Doc',
|
|
103
|
+
hash,
|
|
104
|
+
createdAt: new Date().toISOString(),
|
|
105
|
+
modifiedAt: new Date().toISOString(),
|
|
106
|
+
active: true,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const doc = store.findDocument('global/doc.md');
|
|
110
|
+
expect(doc).not.toBeNull();
|
|
111
|
+
expect(doc?.projectHash).toBe('global');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should extract projectHash from session path pattern', () => {
|
|
115
|
+
const sessionPathRegex = /sessions\/([a-f0-9]{12})\//i;
|
|
116
|
+
|
|
117
|
+
const testCases = [
|
|
118
|
+
{ path: 'sessions/abc123def456/file.md', expected: 'abc123def456' },
|
|
119
|
+
{ path: 'sessions/000000000000/test.md', expected: '000000000000' },
|
|
120
|
+
{ path: 'sessions/ABCDEF123456/doc.md', expected: 'ABCDEF123456' },
|
|
121
|
+
{ path: 'other/path/file.md', expected: null },
|
|
122
|
+
{ path: 'sessions/short/file.md', expected: null },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
for (const tc of testCases) {
|
|
126
|
+
const match = tc.path.match(sessionPathRegex);
|
|
127
|
+
if (tc.expected) {
|
|
128
|
+
expect(match).not.toBeNull();
|
|
129
|
+
expect(match?.[1]).toBe(tc.expected);
|
|
130
|
+
} else {
|
|
131
|
+
expect(match).toBeNull();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('workspace-filtered FTS search', () => {
|
|
138
|
+
beforeEach(() => {
|
|
139
|
+
const docs = [
|
|
140
|
+
{ path: 'ws1/doc.md', title: 'Workspace One Doc', projectHash: 'ws1hash12345', content: 'unique searchterm alpha' },
|
|
141
|
+
{ path: 'ws2/doc.md', title: 'Workspace Two Doc', projectHash: 'ws2hash12345', content: 'unique searchterm beta' },
|
|
142
|
+
{ path: 'global/doc.md', title: 'Global Doc', projectHash: 'global', content: 'unique searchterm gamma' },
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
for (const doc of docs) {
|
|
146
|
+
const hash = computeHash(doc.content);
|
|
147
|
+
store.insertContent(hash, doc.content);
|
|
148
|
+
store.insertDocument({
|
|
149
|
+
collection: 'test',
|
|
150
|
+
path: doc.path,
|
|
151
|
+
title: doc.title,
|
|
152
|
+
hash,
|
|
153
|
+
createdAt: new Date().toISOString(),
|
|
154
|
+
modifiedAt: new Date().toISOString(),
|
|
155
|
+
active: true,
|
|
156
|
+
projectHash: doc.projectHash,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should filter search by workspace', () => {
|
|
162
|
+
const results = store.searchFTS('searchterm', 10, undefined, 'ws1hash12345');
|
|
163
|
+
|
|
164
|
+
const paths = results.map(r => r.path);
|
|
165
|
+
expect(paths).toContain('ws1/doc.md');
|
|
166
|
+
expect(paths).toContain('global/doc.md');
|
|
167
|
+
expect(paths).not.toContain('ws2/doc.md');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should include global docs in workspace search', () => {
|
|
171
|
+
const results = store.searchFTS('searchterm', 10, undefined, 'ws1hash12345');
|
|
172
|
+
|
|
173
|
+
const paths = results.map(r => r.path);
|
|
174
|
+
expect(paths).toContain('global/doc.md');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should return all docs when projectHash is all', () => {
|
|
178
|
+
const results = store.searchFTS('searchterm', 10, undefined, 'all');
|
|
179
|
+
|
|
180
|
+
expect(results.length).toBe(3);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should return all docs when no projectHash provided', () => {
|
|
184
|
+
const results = store.searchFTS('searchterm', 10);
|
|
185
|
+
|
|
186
|
+
expect(results.length).toBe(3);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('workspace stats', () => {
|
|
191
|
+
it('should return workspace stats grouped by projectHash', () => {
|
|
192
|
+
const docs = [
|
|
193
|
+
{ path: 'a/1.md', projectHash: 'hash1' },
|
|
194
|
+
{ path: 'a/2.md', projectHash: 'hash1' },
|
|
195
|
+
{ path: 'b/1.md', projectHash: 'hash2' },
|
|
196
|
+
{ path: 'c/1.md', projectHash: 'global' },
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
for (const doc of docs) {
|
|
200
|
+
const content = `Content for ${doc.path}`;
|
|
201
|
+
const hash = computeHash(content);
|
|
202
|
+
store.insertContent(hash, content);
|
|
203
|
+
store.insertDocument({
|
|
204
|
+
collection: 'test',
|
|
205
|
+
path: doc.path,
|
|
206
|
+
title: doc.path,
|
|
207
|
+
hash,
|
|
208
|
+
createdAt: new Date().toISOString(),
|
|
209
|
+
modifiedAt: new Date().toISOString(),
|
|
210
|
+
active: true,
|
|
211
|
+
projectHash: doc.projectHash,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const stats = store.getWorkspaceStats();
|
|
216
|
+
|
|
217
|
+
const hash1Stats = stats.find(s => s.projectHash === 'hash1');
|
|
218
|
+
const hash2Stats = stats.find(s => s.projectHash === 'hash2');
|
|
219
|
+
const globalStats = stats.find(s => s.projectHash === 'global');
|
|
220
|
+
|
|
221
|
+
expect(hash1Stats?.count).toBe(2);
|
|
222
|
+
expect(hash2Stats?.count).toBe(1);
|
|
223
|
+
expect(globalStats?.count).toBe(1);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('hash computation', () => {
|
|
228
|
+
it('should compute projectHash matching harvester convention', () => {
|
|
229
|
+
const testPath = '/some/path';
|
|
230
|
+
const expectedHash = crypto.createHash('sha256').update(testPath).digest('hex').substring(0, 12);
|
|
231
|
+
|
|
232
|
+
expect(expectedHash).toMatch(/^[a-f0-9]{12}$/);
|
|
233
|
+
expect(expectedHash.length).toBe(12);
|
|
234
|
+
|
|
235
|
+
const hash = computeHash(testPath);
|
|
236
|
+
expect(hash.substring(0, 12)).toMatch(/^[a-f0-9]{12}$/);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"outDir": "dist",
|
|
15
|
+
"types": ["bun-types"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts"],
|
|
18
|
+
"exclude": ["node_modules", "dist", "test"]
|
|
19
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
include: ['test/**/*.test.ts'],
|
|
8
|
+
testTimeout: 10000,
|
|
9
|
+
pool: 'forks',
|
|
10
|
+
poolOptions: {
|
|
11
|
+
forks: {
|
|
12
|
+
execArgv: ['--max-old-space-size=8192'],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|