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/storage.ts ADDED
@@ -0,0 +1,221 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import type { Store, StorageConfig } from './types.js';
4
+
5
+ const DEFAULT_MAX_SIZE = 2147483648;
6
+ const DEFAULT_RETENTION = 7776000000;
7
+ const DEFAULT_MIN_FREE_DISK = 104857600;
8
+
9
+ export function parseSize(value: string): number {
10
+ if (!value || typeof value !== 'string') {
11
+ return -1;
12
+ }
13
+
14
+ const match = value.trim().toUpperCase().match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB|TB)?$/);
15
+ if (!match) {
16
+ return -1;
17
+ }
18
+
19
+ const num = parseFloat(match[1]);
20
+ const unit = match[2] || 'B';
21
+
22
+ const multipliers: Record<string, number> = {
23
+ 'B': 1,
24
+ 'KB': 1024,
25
+ 'MB': 1024 * 1024,
26
+ 'GB': 1024 * 1024 * 1024,
27
+ 'TB': 1024 * 1024 * 1024 * 1024,
28
+ };
29
+
30
+ return Math.floor(num * multipliers[unit]);
31
+ }
32
+
33
+ export function parseDuration(value: string): number {
34
+ if (!value || typeof value !== 'string') {
35
+ return -1;
36
+ }
37
+
38
+ const match = value.trim().toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(d|w|m|y)$/);
39
+ if (!match) {
40
+ return -1;
41
+ }
42
+
43
+ const num = parseFloat(match[1]);
44
+ const unit = match[2];
45
+
46
+ const msPerDay = 24 * 60 * 60 * 1000;
47
+ const multipliers: Record<string, number> = {
48
+ 'd': msPerDay,
49
+ 'w': 7 * msPerDay,
50
+ 'm': 30 * msPerDay,
51
+ 'y': 365 * msPerDay,
52
+ };
53
+
54
+ return Math.floor(num * multipliers[unit]);
55
+ }
56
+
57
+ export function parseStorageConfig(raw?: { maxSize?: string; retention?: string; minFreeDisk?: string }): StorageConfig {
58
+ let maxSize = DEFAULT_MAX_SIZE;
59
+ let retention = DEFAULT_RETENTION;
60
+ let minFreeDisk = DEFAULT_MIN_FREE_DISK;
61
+
62
+ if (raw?.maxSize) {
63
+ const parsed = parseSize(raw.maxSize);
64
+ if (parsed > 0) {
65
+ maxSize = parsed;
66
+ } else {
67
+ console.warn(`[storage] Invalid maxSize "${raw.maxSize}", using default 2GB`);
68
+ }
69
+ }
70
+
71
+ if (raw?.retention) {
72
+ const parsed = parseDuration(raw.retention);
73
+ if (parsed > 0) {
74
+ retention = parsed;
75
+ } else {
76
+ console.warn(`[storage] Invalid retention "${raw.retention}", using default 90d`);
77
+ }
78
+ }
79
+
80
+ if (raw?.minFreeDisk) {
81
+ const parsed = parseSize(raw.minFreeDisk);
82
+ if (parsed > 0) {
83
+ minFreeDisk = parsed;
84
+ } else {
85
+ console.warn(`[storage] Invalid minFreeDisk "${raw.minFreeDisk}", using default 100MB`);
86
+ }
87
+ }
88
+
89
+ return { maxSize, retention, minFreeDisk };
90
+ }
91
+
92
+ export function checkDiskSpace(dir: string, minFreeDisk: number): { ok: boolean; freeBytes: number } {
93
+ try {
94
+ const stats = fs.statfsSync(dir);
95
+ const freeBytes = stats.bfree * stats.bsize;
96
+ return {
97
+ ok: freeBytes >= minFreeDisk,
98
+ freeBytes,
99
+ };
100
+ } catch {
101
+ console.warn('[storage] statfs unavailable, disk safety check disabled');
102
+ return { ok: true, freeBytes: -1 };
103
+ }
104
+ }
105
+
106
+ function getDirectorySize(dirPath: string): number {
107
+ let totalSize = 0;
108
+
109
+ try {
110
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
111
+ for (const entry of entries) {
112
+ const fullPath = path.join(dirPath, entry.name);
113
+ if (entry.isDirectory()) {
114
+ totalSize += getDirectorySize(fullPath);
115
+ } else if (entry.isFile()) {
116
+ try {
117
+ totalSize += fs.statSync(fullPath).size;
118
+ } catch {
119
+ }
120
+ }
121
+ }
122
+ } catch {
123
+ }
124
+
125
+ return totalSize;
126
+ }
127
+
128
+ interface SessionFile {
129
+ path: string;
130
+ mtime: number;
131
+ size: number;
132
+ }
133
+
134
+ function collectSessionFiles(sessionsDir: string): SessionFile[] {
135
+ const files: SessionFile[] = [];
136
+
137
+ try {
138
+ const hashDirs = fs.readdirSync(sessionsDir, { withFileTypes: true });
139
+ for (const hashDir of hashDirs) {
140
+ if (!hashDir.isDirectory()) continue;
141
+
142
+ const hashDirPath = path.join(sessionsDir, hashDir.name);
143
+ try {
144
+ const mdFiles = fs.readdirSync(hashDirPath, { withFileTypes: true });
145
+ for (const mdFile of mdFiles) {
146
+ if (!mdFile.isFile() || !mdFile.name.endsWith('.md')) continue;
147
+
148
+ const filePath = path.join(hashDirPath, mdFile.name);
149
+ try {
150
+ const stats = fs.statSync(filePath);
151
+ files.push({
152
+ path: filePath,
153
+ mtime: stats.mtimeMs,
154
+ size: stats.size,
155
+ });
156
+ } catch {
157
+ }
158
+ }
159
+ } catch {
160
+ }
161
+ }
162
+ } catch {
163
+ }
164
+
165
+ return files;
166
+ }
167
+
168
+ export function evictExpiredSessions(sessionsDir: string, retention: number, store: Store): number {
169
+ const now = Date.now();
170
+ const files = collectSessionFiles(sessionsDir);
171
+ let evictedCount = 0;
172
+
173
+ for (const file of files) {
174
+ if (now - file.mtime > retention) {
175
+ try {
176
+ fs.unlinkSync(file.path);
177
+ store.deleteDocumentsByPath(file.path);
178
+ evictedCount++;
179
+ } catch {
180
+ }
181
+ }
182
+ }
183
+
184
+ return evictedCount;
185
+ }
186
+
187
+ export function evictBySize(sessionsDir: string, dbPath: string, maxSize: number, store: Store): number {
188
+ let dbSize = 0;
189
+ try {
190
+ dbSize = fs.statSync(dbPath).size;
191
+ } catch {
192
+ }
193
+
194
+ let sessionsSize = getDirectorySize(sessionsDir);
195
+ let totalSize = dbSize + sessionsSize;
196
+
197
+ if (totalSize <= maxSize) {
198
+ return 0;
199
+ }
200
+
201
+ const files = collectSessionFiles(sessionsDir);
202
+ files.sort((a, b) => a.mtime - b.mtime);
203
+
204
+ let evictedCount = 0;
205
+
206
+ for (const file of files) {
207
+ if (totalSize <= maxSize) {
208
+ break;
209
+ }
210
+
211
+ try {
212
+ fs.unlinkSync(file.path);
213
+ store.deleteDocumentsByPath(file.path);
214
+ totalSize -= file.size;
215
+ evictedCount++;
216
+ } catch {
217
+ }
218
+ }
219
+
220
+ return evictedCount;
221
+ }