@soleri/forge 3.0.0 → 4.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 (110) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/README.md +16 -1
  3. package/dist/domain-manager.d.ts +19 -0
  4. package/dist/domain-manager.js +139 -0
  5. package/dist/domain-manager.js.map +1 -0
  6. package/dist/facades/forge.facade.js +16 -0
  7. package/dist/facades/forge.facade.js.map +1 -1
  8. package/dist/knowledge-installer.d.ts +2 -18
  9. package/dist/knowledge-installer.js +2 -96
  10. package/dist/knowledge-installer.js.map +1 -1
  11. package/dist/lib.d.ts +12 -0
  12. package/dist/lib.js +12 -0
  13. package/dist/lib.js.map +1 -0
  14. package/dist/patching.d.ts +17 -0
  15. package/dist/patching.js +103 -0
  16. package/dist/patching.js.map +1 -0
  17. package/dist/scaffolder.js +1 -91
  18. package/dist/scaffolder.js.map +1 -1
  19. package/dist/templates/activate.js +1 -2
  20. package/dist/templates/activate.js.map +1 -1
  21. package/dist/templates/core-facade.js +1 -6
  22. package/dist/templates/core-facade.js.map +1 -1
  23. package/dist/templates/domain-facade.js +1 -3
  24. package/dist/templates/domain-facade.js.map +1 -1
  25. package/dist/templates/entry-point.js +2 -7
  26. package/dist/templates/entry-point.js.map +1 -1
  27. package/dist/templates/llm-client.js +3 -4
  28. package/dist/templates/llm-client.js.map +1 -1
  29. package/dist/templates/package-json.js +1 -2
  30. package/dist/templates/package-json.js.map +1 -1
  31. package/dist/templates/test-facades.js +2 -5
  32. package/dist/templates/test-facades.js.map +1 -1
  33. package/dist/types.d.ts +11 -0
  34. package/package.json +8 -1
  35. package/src/__tests__/knowledge-installer.test.ts +3 -7
  36. package/src/__tests__/scaffolder.test.ts +35 -47
  37. package/src/domain-manager.ts +168 -0
  38. package/src/facades/forge.facade.ts +18 -0
  39. package/src/knowledge-installer.ts +3 -118
  40. package/src/lib.ts +19 -0
  41. package/src/patching.ts +123 -0
  42. package/src/scaffolder.ts +1 -97
  43. package/src/templates/activate.ts +1 -2
  44. package/src/templates/core-facade.ts +1 -6
  45. package/src/templates/domain-facade.ts +1 -3
  46. package/src/templates/entry-point.ts +2 -7
  47. package/src/templates/llm-client.ts +3 -4
  48. package/src/templates/package-json.ts +1 -2
  49. package/src/templates/test-facades.ts +2 -5
  50. package/src/types.ts +12 -0
  51. package/dist/templates/brain.d.ts +0 -6
  52. package/dist/templates/brain.js +0 -478
  53. package/dist/templates/brain.js.map +0 -1
  54. package/dist/templates/facade-factory.d.ts +0 -1
  55. package/dist/templates/facade-factory.js +0 -63
  56. package/dist/templates/facade-factory.js.map +0 -1
  57. package/dist/templates/facade-types.d.ts +0 -1
  58. package/dist/templates/facade-types.js +0 -46
  59. package/dist/templates/facade-types.js.map +0 -1
  60. package/dist/templates/intelligence-loader.d.ts +0 -1
  61. package/dist/templates/intelligence-loader.js +0 -43
  62. package/dist/templates/intelligence-loader.js.map +0 -1
  63. package/dist/templates/intelligence-types.d.ts +0 -1
  64. package/dist/templates/intelligence-types.js +0 -24
  65. package/dist/templates/intelligence-types.js.map +0 -1
  66. package/dist/templates/llm-key-pool.d.ts +0 -7
  67. package/dist/templates/llm-key-pool.js +0 -211
  68. package/dist/templates/llm-key-pool.js.map +0 -1
  69. package/dist/templates/llm-types.d.ts +0 -5
  70. package/dist/templates/llm-types.js +0 -161
  71. package/dist/templates/llm-types.js.map +0 -1
  72. package/dist/templates/llm-utils.d.ts +0 -5
  73. package/dist/templates/llm-utils.js +0 -260
  74. package/dist/templates/llm-utils.js.map +0 -1
  75. package/dist/templates/planner.d.ts +0 -5
  76. package/dist/templates/planner.js +0 -150
  77. package/dist/templates/planner.js.map +0 -1
  78. package/dist/templates/test-brain.d.ts +0 -6
  79. package/dist/templates/test-brain.js +0 -474
  80. package/dist/templates/test-brain.js.map +0 -1
  81. package/dist/templates/test-llm.d.ts +0 -7
  82. package/dist/templates/test-llm.js +0 -574
  83. package/dist/templates/test-llm.js.map +0 -1
  84. package/dist/templates/test-loader.d.ts +0 -5
  85. package/dist/templates/test-loader.js +0 -146
  86. package/dist/templates/test-loader.js.map +0 -1
  87. package/dist/templates/test-planner.d.ts +0 -5
  88. package/dist/templates/test-planner.js +0 -271
  89. package/dist/templates/test-planner.js.map +0 -1
  90. package/dist/templates/test-vault.d.ts +0 -5
  91. package/dist/templates/test-vault.js +0 -380
  92. package/dist/templates/test-vault.js.map +0 -1
  93. package/dist/templates/vault.d.ts +0 -5
  94. package/dist/templates/vault.js +0 -263
  95. package/dist/templates/vault.js.map +0 -1
  96. package/src/templates/brain.ts +0 -478
  97. package/src/templates/facade-factory.ts +0 -62
  98. package/src/templates/facade-types.ts +0 -45
  99. package/src/templates/intelligence-loader.ts +0 -42
  100. package/src/templates/intelligence-types.ts +0 -23
  101. package/src/templates/llm-key-pool.ts +0 -212
  102. package/src/templates/llm-types.ts +0 -160
  103. package/src/templates/llm-utils.ts +0 -259
  104. package/src/templates/planner.ts +0 -150
  105. package/src/templates/test-brain.ts +0 -474
  106. package/src/templates/test-llm.ts +0 -575
  107. package/src/templates/test-loader.ts +0 -146
  108. package/src/templates/test-planner.ts +0 -271
  109. package/src/templates/test-vault.ts +0 -380
  110. package/src/templates/vault.ts +0 -263
@@ -1,380 +0,0 @@
1
- /**
2
- * Generates vault test file for a new agent.
3
- * Tests cover: constructor, seed, search (FTS5), get, list, stats, add, remove.
4
- */
5
- export function generateVaultTest(): string {
6
- return VAULT_TEST_TEMPLATE;
7
- }
8
-
9
- const VAULT_TEST_TEMPLATE = [
10
- "import { describe, it, expect, beforeEach, afterEach } from 'vitest';",
11
- "import { Vault } from '../vault/vault.js';",
12
- "import type { IntelligenceEntry } from '../intelligence/types.js';",
13
- '',
14
- 'function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntry {',
15
- ' return {',
16
- " id: overrides.id ?? 'test-entry-1',",
17
- " type: overrides.type ?? 'pattern',",
18
- " domain: overrides.domain ?? 'testing',",
19
- " title: overrides.title ?? 'Test Pattern',",
20
- " severity: overrides.severity ?? 'warning',",
21
- " description: overrides.description ?? 'A test pattern for unit tests.',",
22
- " context: overrides.context ?? 'Use in test suites.',",
23
- " example: overrides.example ?? 'expect(result).toBe(true);',",
24
- " counterExample: overrides.counterExample ?? 'assert(result);',",
25
- " why: overrides.why ?? 'Tests should be explicit about expectations.',",
26
- " tags: overrides.tags ?? ['testing', 'assertions'],",
27
- " appliesTo: overrides.appliesTo ?? ['*.test.ts'],",
28
- ' };',
29
- '}',
30
- '',
31
- "describe('Vault', () => {",
32
- ' let vault: Vault;',
33
- '',
34
- ' beforeEach(() => {',
35
- " vault = new Vault(':memory:');",
36
- ' });',
37
- '',
38
- ' afterEach(() => {',
39
- ' vault.close();',
40
- ' });',
41
- '',
42
- " describe('constructor', () => {",
43
- " it('should create an in-memory vault', () => {",
44
- ' const stats = vault.stats();',
45
- ' expect(stats.totalEntries).toBe(0);',
46
- ' });',
47
- '',
48
- " it('should expose db via getDb()', () => {",
49
- ' const db = vault.getDb();',
50
- ' expect(db).toBeDefined();',
51
- " const row = db.prepare('SELECT COUNT(*) as count FROM brain_vocabulary').get() as { count: number };",
52
- ' expect(row.count).toBe(0);',
53
- ' });',
54
- ' });',
55
- '',
56
- " describe('seed', () => {",
57
- " it('should seed entries and return count', () => {",
58
- " const entries = [makeEntry({ id: 'e1' }), makeEntry({ id: 'e2' })];",
59
- ' const count = vault.seed(entries);',
60
- ' expect(count).toBe(2);',
61
- ' expect(vault.stats().totalEntries).toBe(2);',
62
- ' });',
63
- '',
64
- " it('should upsert on duplicate id', () => {",
65
- " vault.seed([makeEntry({ id: 'e1', title: 'Original' })]);",
66
- " vault.seed([makeEntry({ id: 'e1', title: 'Updated' })]);",
67
- ' expect(vault.stats().totalEntries).toBe(1);',
68
- " const entry = vault.get('e1');",
69
- " expect(entry?.title).toBe('Updated');",
70
- ' });',
71
- '',
72
- " it('should handle empty array', () => {",
73
- ' const count = vault.seed([]);',
74
- ' expect(count).toBe(0);',
75
- ' });',
76
- ' });',
77
- '',
78
- " describe('search', () => {",
79
- ' beforeEach(() => {',
80
- ' vault.seed([',
81
- " makeEntry({ id: 'search-1', title: 'Input validation pattern', description: 'Always validate user input at boundaries.', domain: 'security', tags: ['validation'] }),",
82
- " makeEntry({ id: 'search-2', title: 'Caching strategy', description: 'Use cache-aside for read-heavy workloads.', domain: 'performance', tags: ['caching'] }),",
83
- " makeEntry({ id: 'search-3', title: 'Error handling pattern', description: 'Use typed errors with context for debugging.', domain: 'clean-code', tags: ['errors'] }),",
84
- ' ]);',
85
- ' });',
86
- '',
87
- " it('should find entries matching query', () => {",
88
- " const results = vault.search('validation input');",
89
- ' expect(results.length).toBeGreaterThan(0);',
90
- " expect(results[0].entry.id).toBe('search-1');",
91
- ' });',
92
- '',
93
- " it('should return scores with results', () => {",
94
- " const results = vault.search('caching');",
95
- ' expect(results[0].score).toBeGreaterThan(0);',
96
- ' });',
97
- '',
98
- " it('should filter by domain', () => {",
99
- " const results = vault.search('pattern', { domain: 'security' });",
100
- " expect(results.every((r) => r.entry.domain === 'security')).toBe(true);",
101
- ' });',
102
- '',
103
- " it('should respect limit', () => {",
104
- " const results = vault.search('pattern', { limit: 1 });",
105
- ' expect(results.length).toBeLessThanOrEqual(1);',
106
- ' });',
107
- '',
108
- " it('should return empty for no matches', () => {",
109
- " const results = vault.search('xyznonexistent');",
110
- ' expect(results).toEqual([]);',
111
- ' });',
112
- ' });',
113
- '',
114
- " describe('get', () => {",
115
- " it('should return entry by id', () => {",
116
- " vault.seed([makeEntry({ id: 'get-1', title: 'Get Test' })]);",
117
- " const entry = vault.get('get-1');",
118
- ' expect(entry).not.toBeNull();',
119
- " expect(entry!.title).toBe('Get Test');",
120
- ' });',
121
- '',
122
- " it('should return null for missing id', () => {",
123
- " expect(vault.get('nonexistent')).toBeNull();",
124
- ' });',
125
- '',
126
- " it('should preserve all fields', () => {",
127
- " const original = makeEntry({ id: 'full-1' });",
128
- ' vault.seed([original]);',
129
- " const entry = vault.get('full-1')!;",
130
- ' expect(entry.id).toBe(original.id);',
131
- ' expect(entry.type).toBe(original.type);',
132
- ' expect(entry.domain).toBe(original.domain);',
133
- ' expect(entry.tags).toEqual(original.tags);',
134
- ' });',
135
- ' });',
136
- '',
137
- " describe('list', () => {",
138
- ' beforeEach(() => {',
139
- ' vault.seed([',
140
- " makeEntry({ id: 'l1', domain: 'api', type: 'pattern', severity: 'critical' }),",
141
- " makeEntry({ id: 'l2', domain: 'api', type: 'anti-pattern', severity: 'warning' }),",
142
- " makeEntry({ id: 'l3', domain: 'db', type: 'rule', severity: 'suggestion' }),",
143
- " makeEntry({ id: 'l4', domain: 'db', type: 'pattern', severity: 'critical', tags: ['indexing', 'query'] }),",
144
- ' ]);',
145
- ' });',
146
- '',
147
- " it('should list all entries', () => {",
148
- ' const entries = vault.list();',
149
- ' expect(entries).toHaveLength(4);',
150
- ' });',
151
- '',
152
- " it('should filter by domain', () => {",
153
- " const entries = vault.list({ domain: 'api' });",
154
- ' expect(entries).toHaveLength(2);',
155
- " expect(entries.every((e) => e.domain === 'api')).toBe(true);",
156
- ' });',
157
- '',
158
- " it('should filter by type', () => {",
159
- " const entries = vault.list({ type: 'pattern' });",
160
- ' expect(entries).toHaveLength(2);',
161
- ' });',
162
- '',
163
- " it('should filter by severity', () => {",
164
- " const entries = vault.list({ severity: 'critical' });",
165
- ' expect(entries).toHaveLength(2);',
166
- ' });',
167
- '',
168
- " it('should filter by tags', () => {",
169
- " const entries = vault.list({ tags: ['indexing'] });",
170
- ' expect(entries).toHaveLength(1);',
171
- " expect(entries[0].id).toBe('l4');",
172
- ' });',
173
- '',
174
- " it('should support limit and offset', () => {",
175
- ' const page1 = vault.list({ limit: 2, offset: 0 });',
176
- ' const page2 = vault.list({ limit: 2, offset: 2 });',
177
- ' expect(page1).toHaveLength(2);',
178
- ' expect(page2).toHaveLength(2);',
179
- ' expect(page1[0].id).not.toBe(page2[0].id);',
180
- ' });',
181
- ' });',
182
- '',
183
- " describe('stats', () => {",
184
- " it('should return zero counts for empty vault', () => {",
185
- ' const stats = vault.stats();',
186
- ' expect(stats.totalEntries).toBe(0);',
187
- ' expect(stats.byType).toEqual({});',
188
- ' expect(stats.byDomain).toEqual({});',
189
- ' expect(stats.bySeverity).toEqual({});',
190
- ' });',
191
- '',
192
- " it('should return correct breakdowns', () => {",
193
- ' vault.seed([',
194
- " makeEntry({ id: 's1', domain: 'api', type: 'pattern', severity: 'critical' }),",
195
- " makeEntry({ id: 's2', domain: 'api', type: 'rule', severity: 'warning' }),",
196
- " makeEntry({ id: 's3', domain: 'db', type: 'pattern', severity: 'critical' }),",
197
- ' ]);',
198
- ' const stats = vault.stats();',
199
- ' expect(stats.totalEntries).toBe(3);',
200
- ' expect(stats.byDomain).toEqual({ api: 2, db: 1 });',
201
- ' expect(stats.byType).toEqual({ pattern: 2, rule: 1 });',
202
- ' expect(stats.bySeverity).toEqual({ critical: 2, warning: 1 });',
203
- ' });',
204
- ' });',
205
- '',
206
- " describe('add', () => {",
207
- " it('should add a single entry', () => {",
208
- " vault.add(makeEntry({ id: 'add-1' }));",
209
- ' expect(vault.stats().totalEntries).toBe(1);',
210
- " expect(vault.get('add-1')).not.toBeNull();",
211
- ' });',
212
- ' });',
213
- '',
214
- " describe('remove', () => {",
215
- " it('should remove an existing entry', () => {",
216
- " vault.seed([makeEntry({ id: 'rm-1' })]);",
217
- " const removed = vault.remove('rm-1');",
218
- ' expect(removed).toBe(true);',
219
- " expect(vault.get('rm-1')).toBeNull();",
220
- ' expect(vault.stats().totalEntries).toBe(0);',
221
- ' });',
222
- '',
223
- " it('should return false for nonexistent id', () => {",
224
- " expect(vault.remove('nonexistent')).toBe(false);",
225
- ' });',
226
- ' });',
227
- '',
228
- " describe('registerProject', () => {",
229
- " it('should register a new project', () => {",
230
- " const project = vault.registerProject('/home/user/my-project', 'my-project');",
231
- " expect(project.path).toBe('/home/user/my-project');",
232
- " expect(project.name).toBe('my-project');",
233
- ' expect(project.sessionCount).toBe(1);',
234
- ' });',
235
- '',
236
- " it('should derive name from path when not provided', () => {",
237
- " const project = vault.registerProject('/home/user/cool-app');",
238
- " expect(project.name).toBe('cool-app');",
239
- ' });',
240
- '',
241
- " it('should increment session count on re-registration', () => {",
242
- " vault.registerProject('/home/user/app');",
243
- " const second = vault.registerProject('/home/user/app');",
244
- ' expect(second.sessionCount).toBe(2);',
245
- " const third = vault.registerProject('/home/user/app');",
246
- ' expect(third.sessionCount).toBe(3);',
247
- ' });',
248
- '',
249
- " it('should update last_seen_at on re-registration', () => {",
250
- " const first = vault.registerProject('/home/user/app');",
251
- " const second = vault.registerProject('/home/user/app');",
252
- ' expect(second.lastSeenAt).toBeGreaterThanOrEqual(first.lastSeenAt);',
253
- ' });',
254
- ' });',
255
- '',
256
- " describe('getProject', () => {",
257
- " it('should return null for unregistered project', () => {",
258
- " expect(vault.getProject('/nonexistent')).toBeNull();",
259
- ' });',
260
- '',
261
- " it('should return registered project', () => {",
262
- " vault.registerProject('/home/user/app', 'app');",
263
- " const project = vault.getProject('/home/user/app');",
264
- ' expect(project).not.toBeNull();',
265
- " expect(project!.name).toBe('app');",
266
- ' });',
267
- ' });',
268
- '',
269
- " describe('listProjects', () => {",
270
- " it('should return empty array when no projects', () => {",
271
- ' expect(vault.listProjects()).toEqual([]);',
272
- ' });',
273
- '',
274
- " it('should list all registered projects', () => {",
275
- " vault.registerProject('/home/user/app-a', 'app-a');",
276
- " vault.registerProject('/home/user/app-b', 'app-b');",
277
- ' const projects = vault.listProjects();',
278
- ' expect(projects).toHaveLength(2);',
279
- ' });',
280
- ' });',
281
- '',
282
- " describe('captureMemory', () => {",
283
- " it('should capture a memory and return it', () => {",
284
- " const memory = vault.captureMemory({ projectPath: '/test', type: 'lesson', context: 'Debugging session', summary: 'Learned about FTS5 tokenizers', topics: ['sqlite', 'fts5'], filesModified: ['vault.ts'], toolsUsed: ['Bash'] });",
285
- ' expect(memory.id).toMatch(/^mem-/);',
286
- " expect(memory.type).toBe('lesson');",
287
- " expect(memory.summary).toBe('Learned about FTS5 tokenizers');",
288
- " expect(memory.topics).toEqual(['sqlite', 'fts5']);",
289
- ' expect(memory.archivedAt).toBeNull();',
290
- ' });',
291
- '',
292
- " it('should capture session memories', () => {",
293
- " const memory = vault.captureMemory({ projectPath: '/test', type: 'session', context: 'refactoring vault module', summary: 'Refactored vault to use FTS5', topics: ['vault'], filesModified: [], toolsUsed: [] });",
294
- " expect(memory.type).toBe('session');",
295
- ' expect(memory.createdAt).toBeGreaterThan(0);',
296
- ' });',
297
- '',
298
- " it('should capture preference memories', () => {",
299
- " const memory = vault.captureMemory({ projectPath: '/test', type: 'preference', context: 'user prefers bun over npm', summary: 'Use bun for package management', topics: ['tooling'], filesModified: [], toolsUsed: [] });",
300
- " expect(memory.type).toBe('preference');",
301
- ' });',
302
- ' });',
303
- '',
304
- " describe('searchMemories', () => {",
305
- ' beforeEach(() => {',
306
- " vault.captureMemory({ projectPath: '/test', type: 'lesson', context: 'Debugging SQL queries', summary: 'Always use parameterized queries to prevent injection', topics: ['sql', 'security'], filesModified: [], toolsUsed: [] });",
307
- " vault.captureMemory({ projectPath: '/test', type: 'session', context: 'Working on API design', summary: 'Implemented REST endpoints with pagination', topics: ['api', 'rest'], filesModified: ['routes.ts'], toolsUsed: ['Edit'] });",
308
- " vault.captureMemory({ projectPath: '/other', type: 'preference', context: 'User likes TypeScript strict mode', summary: 'Always enable strict mode in tsconfig', topics: ['typescript'], filesModified: [], toolsUsed: [] });",
309
- ' });',
310
- '',
311
- " it('should find memories matching query', () => {",
312
- " const results = vault.searchMemories('parameterized queries');",
313
- ' expect(results.length).toBeGreaterThan(0);',
314
- " expect(results[0].summary).toContain('parameterized');",
315
- ' });',
316
- '',
317
- " it('should filter by type', () => {",
318
- " const results = vault.searchMemories('queries OR endpoints OR strict', { type: 'lesson' });",
319
- " expect(results.every((m) => m.type === 'lesson')).toBe(true);",
320
- ' });',
321
- '',
322
- " it('should filter by project path', () => {",
323
- " const results = vault.searchMemories('queries OR endpoints OR strict', { projectPath: '/other' });",
324
- " expect(results.every((m) => m.projectPath === '/other')).toBe(true);",
325
- ' });',
326
- '',
327
- " it('should respect limit', () => {",
328
- " const results = vault.searchMemories('queries OR endpoints OR strict', { limit: 1 });",
329
- ' expect(results.length).toBeLessThanOrEqual(1);',
330
- ' });',
331
- '',
332
- " it('should return empty for no matches', () => {",
333
- " const results = vault.searchMemories('xyznonexistent');",
334
- ' expect(results).toEqual([]);',
335
- ' });',
336
- ' });',
337
- '',
338
- " describe('listMemories', () => {",
339
- ' beforeEach(() => {',
340
- " vault.captureMemory({ projectPath: '/proj-a', type: 'lesson', context: 'ctx', summary: 'lesson one', topics: [], filesModified: [], toolsUsed: [] });",
341
- " vault.captureMemory({ projectPath: '/proj-a', type: 'session', context: 'ctx', summary: 'session one', topics: [], filesModified: [], toolsUsed: [] });",
342
- " vault.captureMemory({ projectPath: '/proj-b', type: 'preference', context: 'ctx', summary: 'pref one', topics: [], filesModified: [], toolsUsed: [] });",
343
- ' });',
344
- '',
345
- " it('should list all memories', () => {",
346
- ' const memories = vault.listMemories();',
347
- ' expect(memories).toHaveLength(3);',
348
- ' });',
349
- '',
350
- " it('should filter by type', () => {",
351
- " const memories = vault.listMemories({ type: 'lesson' });",
352
- ' expect(memories).toHaveLength(1);',
353
- " expect(memories[0].type).toBe('lesson');",
354
- ' });',
355
- '',
356
- " it('should filter by project path', () => {",
357
- " const memories = vault.listMemories({ projectPath: '/proj-a' });",
358
- ' expect(memories).toHaveLength(2);',
359
- ' });',
360
- ' });',
361
- '',
362
- " describe('memoryStats', () => {",
363
- " it('should return zero counts for empty memories', () => {",
364
- ' const stats = vault.memoryStats();',
365
- ' expect(stats.total).toBe(0);',
366
- ' expect(stats.byType).toEqual({});',
367
- ' });',
368
- '',
369
- " it('should return correct breakdown', () => {",
370
- " vault.captureMemory({ projectPath: '/a', type: 'lesson', context: 'ctx', summary: 's', topics: [], filesModified: [], toolsUsed: [] });",
371
- " vault.captureMemory({ projectPath: '/a', type: 'lesson', context: 'ctx', summary: 's', topics: [], filesModified: [], toolsUsed: [] });",
372
- " vault.captureMemory({ projectPath: '/b', type: 'session', context: 'ctx', summary: 's', topics: [], filesModified: [], toolsUsed: [] });",
373
- ' const stats = vault.memoryStats();',
374
- ' expect(stats.total).toBe(3);',
375
- ' expect(stats.byType).toEqual({ lesson: 2, session: 1 });',
376
- " expect(stats.byProject).toEqual({ '/a': 2, '/b': 1 });",
377
- ' });',
378
- ' });',
379
- '});',
380
- ].join('\n');
@@ -1,263 +0,0 @@
1
- /**
2
- * Generates the vault.ts source file for a new agent.
3
- * The vault provides SQLite-backed knowledge storage with FTS5 search.
4
- */
5
- export function generateVault(): string {
6
- // The vault template is stored as a static string to avoid
7
- // template literal escaping issues with nested backticks.
8
- return VAULT_TEMPLATE;
9
- }
10
-
11
- const VAULT_TEMPLATE = [
12
- "import Database from 'better-sqlite3';",
13
- "import { mkdirSync } from 'node:fs';",
14
- "import { dirname } from 'node:path';",
15
- "import type { IntelligenceEntry } from '../intelligence/types.js';",
16
- '',
17
- 'export interface SearchResult { entry: IntelligenceEntry; score: number; }',
18
- 'export interface VaultStats { totalEntries: number; byType: Record<string, number>; byDomain: Record<string, number>; bySeverity: Record<string, number>; }',
19
- 'export interface ProjectInfo { path: string; name: string; registeredAt: number; lastSeenAt: number; sessionCount: number; }',
20
- "export interface Memory { id: string; projectPath: string; type: 'session' | 'lesson' | 'preference'; context: string; summary: string; topics: string[]; filesModified: string[]; toolsUsed: string[]; createdAt: number; archivedAt: number | null; }",
21
- 'export interface MemoryStats { total: number; byType: Record<string, number>; byProject: Record<string, number>; }',
22
- '',
23
- 'export class Vault {',
24
- ' private db: Database.Database;',
25
- '',
26
- " constructor(dbPath: string = ':memory:') {",
27
- " if (dbPath !== ':memory:') mkdirSync(dirname(dbPath), { recursive: true });",
28
- ' this.db = new Database(dbPath);',
29
- " this.db.pragma('journal_mode = WAL');",
30
- " this.db.pragma('foreign_keys = ON');",
31
- ' this.initialize();',
32
- ' }',
33
- '',
34
- ' private initialize(): void {',
35
- ' this.db.exec(`',
36
- ' CREATE TABLE IF NOT EXISTS entries (',
37
- ' id TEXT PRIMARY KEY,',
38
- " type TEXT NOT NULL CHECK(type IN ('pattern', 'anti-pattern', 'rule')),",
39
- ' domain TEXT NOT NULL,',
40
- ' title TEXT NOT NULL,',
41
- " severity TEXT NOT NULL CHECK(severity IN ('critical', 'warning', 'suggestion')),",
42
- ' description TEXT NOT NULL,',
43
- ' context TEXT, example TEXT, counter_example TEXT, why TEXT,',
44
- " tags TEXT NOT NULL DEFAULT '[]',",
45
- " applies_to TEXT DEFAULT '[]',",
46
- ' created_at INTEGER NOT NULL DEFAULT (unixepoch()),',
47
- ' updated_at INTEGER NOT NULL DEFAULT (unixepoch())',
48
- ' );',
49
- ' CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(',
50
- ' id, title, description, context, tags,',
51
- " content='entries', content_rowid='rowid', tokenize='porter unicode61'",
52
- ' );',
53
- ' CREATE TRIGGER IF NOT EXISTS entries_ai AFTER INSERT ON entries BEGIN',
54
- ' INSERT INTO entries_fts(rowid,id,title,description,context,tags) VALUES(new.rowid,new.id,new.title,new.description,new.context,new.tags);',
55
- ' END;',
56
- ' CREATE TRIGGER IF NOT EXISTS entries_ad AFTER DELETE ON entries BEGIN',
57
- " INSERT INTO entries_fts(entries_fts,rowid,id,title,description,context,tags) VALUES('delete',old.rowid,old.id,old.title,old.description,old.context,old.tags);",
58
- ' END;',
59
- ' CREATE TRIGGER IF NOT EXISTS entries_au AFTER UPDATE ON entries BEGIN',
60
- " INSERT INTO entries_fts(entries_fts,rowid,id,title,description,context,tags) VALUES('delete',old.rowid,old.id,old.title,old.description,old.context,old.tags);",
61
- ' INSERT INTO entries_fts(rowid,id,title,description,context,tags) VALUES(new.rowid,new.id,new.title,new.description,new.context,new.tags);',
62
- ' END;',
63
- ' CREATE TABLE IF NOT EXISTS projects (',
64
- ' path TEXT PRIMARY KEY,',
65
- ' name TEXT NOT NULL,',
66
- ' registered_at INTEGER NOT NULL DEFAULT (unixepoch()),',
67
- ' last_seen_at INTEGER NOT NULL DEFAULT (unixepoch()),',
68
- ' session_count INTEGER NOT NULL DEFAULT 1',
69
- ' );',
70
- ' CREATE TABLE IF NOT EXISTS memories (',
71
- ' id TEXT PRIMARY KEY,',
72
- ' project_path TEXT NOT NULL,',
73
- " type TEXT NOT NULL CHECK(type IN ('session', 'lesson', 'preference')),",
74
- ' context TEXT NOT NULL,',
75
- ' summary TEXT NOT NULL,',
76
- " topics TEXT NOT NULL DEFAULT '[]',",
77
- " files_modified TEXT NOT NULL DEFAULT '[]',",
78
- " tools_used TEXT NOT NULL DEFAULT '[]',",
79
- ' created_at INTEGER NOT NULL DEFAULT (unixepoch()),',
80
- ' archived_at INTEGER',
81
- ' );',
82
- ' CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(',
83
- ' id, context, summary, topics,',
84
- " content='memories', content_rowid='rowid', tokenize='porter unicode61'",
85
- ' );',
86
- ' CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN',
87
- ' INSERT INTO memories_fts(rowid,id,context,summary,topics) VALUES(new.rowid,new.id,new.context,new.summary,new.topics);',
88
- ' END;',
89
- ' CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN',
90
- " INSERT INTO memories_fts(memories_fts,rowid,id,context,summary,topics) VALUES('delete',old.rowid,old.id,old.context,old.summary,old.topics);",
91
- ' END;',
92
- ' CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN',
93
- " INSERT INTO memories_fts(memories_fts,rowid,id,context,summary,topics) VALUES('delete',old.rowid,old.id,old.context,old.summary,old.topics);",
94
- ' INSERT INTO memories_fts(rowid,id,context,summary,topics) VALUES(new.rowid,new.id,new.context,new.summary,new.topics);',
95
- ' END;',
96
- ' CREATE TABLE IF NOT EXISTS brain_vocabulary (',
97
- ' term TEXT PRIMARY KEY,',
98
- ' idf REAL NOT NULL,',
99
- ' doc_count INTEGER NOT NULL DEFAULT 1,',
100
- ' updated_at INTEGER NOT NULL DEFAULT (unixepoch())',
101
- ' );',
102
- ' CREATE TABLE IF NOT EXISTS brain_feedback (',
103
- ' id INTEGER PRIMARY KEY AUTOINCREMENT,',
104
- ' query TEXT NOT NULL,',
105
- ' entry_id TEXT NOT NULL,',
106
- " action TEXT NOT NULL CHECK(action IN ('accepted', 'dismissed')),",
107
- ' created_at INTEGER NOT NULL DEFAULT (unixepoch())',
108
- ' );',
109
- ' CREATE INDEX IF NOT EXISTS idx_brain_feedback_query ON brain_feedback(query);',
110
- ' `);',
111
- ' }',
112
- '',
113
- ' seed(entries: IntelligenceEntry[]): number {',
114
- ' const upsert = this.db.prepare(`',
115
- ' INSERT INTO entries (id,type,domain,title,severity,description,context,example,counter_example,why,tags,applies_to)',
116
- ' VALUES (@id,@type,@domain,@title,@severity,@description,@context,@example,@counterExample,@why,@tags,@appliesTo)',
117
- ' ON CONFLICT(id) DO UPDATE SET type=excluded.type,domain=excluded.domain,title=excluded.title,severity=excluded.severity,',
118
- ' description=excluded.description,context=excluded.context,example=excluded.example,counter_example=excluded.counter_example,',
119
- ' why=excluded.why,tags=excluded.tags,applies_to=excluded.applies_to,updated_at=unixepoch()',
120
- ' `);',
121
- ' const tx = this.db.transaction((items: IntelligenceEntry[]) => {',
122
- ' let count = 0;',
123
- ' for (const entry of items) {',
124
- ' upsert.run({ id: entry.id, type: entry.type, domain: entry.domain, title: entry.title,',
125
- ' severity: entry.severity, description: entry.description, context: entry.context ?? null,',
126
- ' example: entry.example ?? null, counterExample: entry.counterExample ?? null,',
127
- ' why: entry.why ?? null, tags: JSON.stringify(entry.tags), appliesTo: JSON.stringify(entry.appliesTo ?? []) });',
128
- ' count++;',
129
- ' }',
130
- ' return count;',
131
- ' });',
132
- ' return tx(entries);',
133
- ' }',
134
- '',
135
- ' search(query: string, options?: { domain?: string; type?: string; severity?: string; limit?: number }): SearchResult[] {',
136
- ' const limit = options?.limit ?? 10;',
137
- ' const filters: string[] = []; const fp: Record<string, string> = {};',
138
- " if (options?.domain) { filters.push('e.domain = @domain'); fp.domain = options.domain; }",
139
- " if (options?.type) { filters.push('e.type = @type'); fp.type = options.type; }",
140
- " if (options?.severity) { filters.push('e.severity = @severity'); fp.severity = options.severity; }",
141
- " const wc = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';",
142
- ' const rows = this.db.prepare(`SELECT e.*, -rank as score FROM entries_fts fts JOIN entries e ON e.rowid = fts.rowid WHERE entries_fts MATCH @query ${wc} ORDER BY score DESC LIMIT @limit`)',
143
- ' .all({ query, limit, ...fp }) as Array<Record<string, unknown>>;',
144
- ' return rows.map(rowToSearchResult);',
145
- ' }',
146
- '',
147
- ' get(id: string): IntelligenceEntry | null {',
148
- " const row = this.db.prepare('SELECT * FROM entries WHERE id = ?').get(id) as Record<string, unknown> | undefined;",
149
- ' return row ? rowToEntry(row) : null;',
150
- ' }',
151
- '',
152
- ' list(options?: { domain?: string; type?: string; severity?: string; tags?: string[]; limit?: number; offset?: number }): IntelligenceEntry[] {',
153
- ' const filters: string[] = []; const params: Record<string, unknown> = {};',
154
- " if (options?.domain) { filters.push('domain = @domain'); params.domain = options.domain; }",
155
- " if (options?.type) { filters.push('type = @type'); params.type = options.type; }",
156
- " if (options?.severity) { filters.push('severity = @severity'); params.severity = options.severity; }",
157
- ' if (options?.tags?.length) { const c = options.tags.map((t, i) => { params[`tag${i}`] = `%"${t}"%`; return `tags LIKE @tag${i}`; }); filters.push(`(${c.join(\' OR \')})`); }',
158
- " const wc = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';",
159
- ' const rows = this.db.prepare(`SELECT * FROM entries ${wc} ORDER BY severity, domain, title LIMIT @limit OFFSET @offset`)',
160
- ' .all({ ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 }) as Array<Record<string, unknown>>;',
161
- ' return rows.map(rowToEntry);',
162
- ' }',
163
- '',
164
- ' stats(): VaultStats {',
165
- " const total = (this.db.prepare('SELECT COUNT(*) as count FROM entries').get() as { count: number }).count;",
166
- " return { totalEntries: total, byType: gc(this.db, 'type'), byDomain: gc(this.db, 'domain'), bySeverity: gc(this.db, 'severity') };",
167
- ' }',
168
- '',
169
- ' add(entry: IntelligenceEntry): void { this.seed([entry]); }',
170
- " remove(id: string): boolean { return this.db.prepare('DELETE FROM entries WHERE id = ?').run(id).changes > 0; }",
171
- '',
172
- ' registerProject(path: string, name?: string): ProjectInfo {',
173
- " const projectName = name ?? path.split('/').filter(Boolean).pop() ?? path;",
174
- ' const existing = this.getProject(path);',
175
- ' if (existing) {',
176
- " this.db.prepare('UPDATE projects SET last_seen_at = unixepoch(), session_count = session_count + 1 WHERE path = ?').run(path);",
177
- ' return this.getProject(path)!;',
178
- ' }',
179
- " this.db.prepare('INSERT INTO projects (path, name) VALUES (?, ?)').run(path, projectName);",
180
- ' return this.getProject(path)!;',
181
- ' }',
182
- '',
183
- ' getProject(path: string): ProjectInfo | null {',
184
- " const row = this.db.prepare('SELECT * FROM projects WHERE path = ?').get(path) as Record<string, unknown> | undefined;",
185
- ' if (!row) return null;',
186
- ' return { path: row.path as string, name: row.name as string, registeredAt: row.registered_at as number, lastSeenAt: row.last_seen_at as number, sessionCount: row.session_count as number };',
187
- ' }',
188
- '',
189
- ' listProjects(): ProjectInfo[] {',
190
- " const rows = this.db.prepare('SELECT * FROM projects ORDER BY last_seen_at DESC').all() as Array<Record<string, unknown>>;",
191
- ' return rows.map((row) => ({ path: row.path as string, name: row.name as string, registeredAt: row.registered_at as number, lastSeenAt: row.last_seen_at as number, sessionCount: row.session_count as number }));',
192
- ' }',
193
- '',
194
- " captureMemory(memory: Omit<Memory, 'id' | 'createdAt' | 'archivedAt'>): Memory {",
195
- ' const id = `mem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;',
196
- ' this.db.prepare(`INSERT INTO memories (id, project_path, type, context, summary, topics, files_modified, tools_used) VALUES (@id, @projectPath, @type, @context, @summary, @topics, @filesModified, @toolsUsed)`)',
197
- ' .run({ id, projectPath: memory.projectPath, type: memory.type, context: memory.context, summary: memory.summary, topics: JSON.stringify(memory.topics), filesModified: JSON.stringify(memory.filesModified), toolsUsed: JSON.stringify(memory.toolsUsed) });',
198
- ' return this.getMemory(id)!;',
199
- ' }',
200
- '',
201
- ' searchMemories(query: string, options?: { type?: string; projectPath?: string; limit?: number }): Memory[] {',
202
- ' const limit = options?.limit ?? 10;',
203
- " const filters: string[] = ['m.archived_at IS NULL']; const fp: Record<string, unknown> = {};",
204
- " if (options?.type) { filters.push('m.type = @type'); fp.type = options.type; }",
205
- " if (options?.projectPath) { filters.push('m.project_path = @projectPath'); fp.projectPath = options.projectPath; }",
206
- " const wc = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';",
207
- ' const rows = this.db.prepare(`SELECT m.* FROM memories_fts fts JOIN memories m ON m.rowid = fts.rowid WHERE memories_fts MATCH @query ${wc} ORDER BY rank LIMIT @limit`)',
208
- ' .all({ query, limit, ...fp }) as Array<Record<string, unknown>>;',
209
- ' return rows.map(rowToMemory);',
210
- ' }',
211
- '',
212
- ' listMemories(options?: { type?: string; projectPath?: string; limit?: number; offset?: number }): Memory[] {',
213
- " const filters: string[] = ['archived_at IS NULL']; const params: Record<string, unknown> = {};",
214
- " if (options?.type) { filters.push('type = @type'); params.type = options.type; }",
215
- " if (options?.projectPath) { filters.push('project_path = @projectPath'); params.projectPath = options.projectPath; }",
216
- " const wc = `WHERE ${filters.join(' AND ')}`;",
217
- ' const rows = this.db.prepare(`SELECT * FROM memories ${wc} ORDER BY created_at DESC LIMIT @limit OFFSET @offset`)',
218
- ' .all({ ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 }) as Array<Record<string, unknown>>;',
219
- ' return rows.map(rowToMemory);',
220
- ' }',
221
- '',
222
- ' memoryStats(): MemoryStats {',
223
- " const total = (this.db.prepare('SELECT COUNT(*) as count FROM memories WHERE archived_at IS NULL').get() as { count: number }).count;",
224
- " const byTypeRows = this.db.prepare('SELECT type as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY type').all() as Array<{ key: string; count: number }>;",
225
- " const byProjectRows = this.db.prepare('SELECT project_path as key, COUNT(*) as count FROM memories WHERE archived_at IS NULL GROUP BY project_path').all() as Array<{ key: string; count: number }>;",
226
- ' return { total, byType: Object.fromEntries(byTypeRows.map((r) => [r.key, r.count])), byProject: Object.fromEntries(byProjectRows.map((r) => [r.key, r.count])) };',
227
- ' }',
228
- '',
229
- ' getMemory(id: string): Memory | null {',
230
- " const row = this.db.prepare('SELECT * FROM memories WHERE id = ?').get(id) as Record<string, unknown> | undefined;",
231
- ' return row ? rowToMemory(row) : null;',
232
- ' }',
233
- '',
234
- ' getDb(): Database.Database { return this.db; }',
235
- '',
236
- ' close(): void { this.db.close(); }',
237
- '}',
238
- '',
239
- 'function gc(db: Database.Database, col: string): Record<string, number> {',
240
- ' const rows = db.prepare(`SELECT ${col} as key, COUNT(*) as count FROM entries GROUP BY ${col}`).all() as Array<{ key: string; count: number }>;',
241
- ' return Object.fromEntries(rows.map((r) => [r.key, r.count]));',
242
- '}',
243
- '',
244
- 'function rowToEntry(row: Record<string, unknown>): IntelligenceEntry {',
245
- " return { id: row.id as string, type: row.type as IntelligenceEntry['type'], domain: row.domain as IntelligenceEntry['domain'],",
246
- " title: row.title as string, severity: row.severity as IntelligenceEntry['severity'], description: row.description as string,",
247
- ' context: (row.context as string) ?? undefined, example: (row.example as string) ?? undefined,',
248
- ' counterExample: (row.counter_example as string) ?? undefined, why: (row.why as string) ?? undefined,',
249
- " tags: JSON.parse((row.tags as string) || '[]'), appliesTo: JSON.parse((row.applies_to as string) || '[]') };",
250
- '}',
251
- '',
252
- 'function rowToSearchResult(row: Record<string, unknown>): SearchResult {',
253
- ' return { entry: rowToEntry(row), score: row.score as number };',
254
- '}',
255
- '',
256
- 'function rowToMemory(row: Record<string, unknown>): Memory {',
257
- " return { id: row.id as string, projectPath: row.project_path as string, type: row.type as Memory['type'],",
258
- ' context: row.context as string, summary: row.summary as string,',
259
- " topics: JSON.parse((row.topics as string) || '[]'), filesModified: JSON.parse((row.files_modified as string) || '[]'),",
260
- " toolsUsed: JSON.parse((row.tools_used as string) || '[]'), createdAt: row.created_at as number,",
261
- ' archivedAt: (row.archived_at as number) ?? null };',
262
- '}',
263
- ].join('\n');