@soleri/forge 4.2.2 → 5.0.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 (78) hide show
  1. package/dist/domain-manager.d.ts +2 -2
  2. package/dist/domain-manager.js +35 -16
  3. package/dist/domain-manager.js.map +1 -1
  4. package/dist/index.js +0 -0
  5. package/dist/knowledge-installer.js +18 -12
  6. package/dist/knowledge-installer.js.map +1 -1
  7. package/dist/patching.d.ts +15 -6
  8. package/dist/patching.js +39 -12
  9. package/dist/patching.js.map +1 -1
  10. package/dist/scaffolder.js +18 -28
  11. package/dist/scaffolder.js.map +1 -1
  12. package/dist/templates/brain.d.ts +6 -0
  13. package/dist/templates/brain.js +478 -0
  14. package/dist/templates/brain.js.map +1 -0
  15. package/dist/templates/core-facade.js +95 -47
  16. package/dist/templates/core-facade.js.map +1 -1
  17. package/dist/templates/entry-point.d.ts +4 -0
  18. package/dist/templates/entry-point.js +146 -89
  19. package/dist/templates/entry-point.js.map +1 -1
  20. package/dist/templates/facade-factory.d.ts +1 -0
  21. package/dist/templates/facade-factory.js +63 -0
  22. package/dist/templates/facade-factory.js.map +1 -0
  23. package/dist/templates/facade-types.d.ts +1 -0
  24. package/dist/templates/facade-types.js +46 -0
  25. package/dist/templates/facade-types.js.map +1 -0
  26. package/dist/templates/intelligence-loader.d.ts +1 -0
  27. package/dist/templates/intelligence-loader.js +43 -0
  28. package/dist/templates/intelligence-loader.js.map +1 -0
  29. package/dist/templates/intelligence-types.d.ts +1 -0
  30. package/dist/templates/intelligence-types.js +24 -0
  31. package/dist/templates/intelligence-types.js.map +1 -0
  32. package/dist/templates/llm-key-pool.d.ts +7 -0
  33. package/dist/templates/llm-key-pool.js +211 -0
  34. package/dist/templates/llm-key-pool.js.map +1 -0
  35. package/dist/templates/llm-types.d.ts +5 -0
  36. package/dist/templates/llm-types.js +161 -0
  37. package/dist/templates/llm-types.js.map +1 -0
  38. package/dist/templates/llm-utils.d.ts +5 -0
  39. package/dist/templates/llm-utils.js +260 -0
  40. package/dist/templates/llm-utils.js.map +1 -0
  41. package/dist/templates/package-json.js +3 -1
  42. package/dist/templates/package-json.js.map +1 -1
  43. package/dist/templates/planner.d.ts +5 -0
  44. package/dist/templates/planner.js +150 -0
  45. package/dist/templates/planner.js.map +1 -0
  46. package/dist/templates/test-brain.d.ts +6 -0
  47. package/dist/templates/test-brain.js +474 -0
  48. package/dist/templates/test-brain.js.map +1 -0
  49. package/dist/templates/test-facades.d.ts +1 -1
  50. package/dist/templates/test-facades.js +170 -456
  51. package/dist/templates/test-facades.js.map +1 -1
  52. package/dist/templates/test-llm.d.ts +7 -0
  53. package/dist/templates/test-llm.js +574 -0
  54. package/dist/templates/test-llm.js.map +1 -0
  55. package/dist/templates/test-loader.d.ts +5 -0
  56. package/dist/templates/test-loader.js +146 -0
  57. package/dist/templates/test-loader.js.map +1 -0
  58. package/dist/templates/test-planner.d.ts +5 -0
  59. package/dist/templates/test-planner.js +271 -0
  60. package/dist/templates/test-planner.js.map +1 -0
  61. package/dist/templates/test-vault.d.ts +5 -0
  62. package/dist/templates/test-vault.js +380 -0
  63. package/dist/templates/test-vault.js.map +1 -0
  64. package/dist/templates/vault.d.ts +5 -0
  65. package/dist/templates/vault.js +263 -0
  66. package/dist/templates/vault.js.map +1 -0
  67. package/dist/types.d.ts +2 -2
  68. package/package.json +1 -1
  69. package/src/__tests__/scaffolder.test.ts +52 -109
  70. package/src/domain-manager.ts +34 -15
  71. package/src/knowledge-installer.ts +22 -15
  72. package/src/patching.ts +49 -11
  73. package/src/scaffolder.ts +18 -29
  74. package/src/templates/entry-point.ts +146 -91
  75. package/src/templates/package-json.ts +3 -1
  76. package/src/templates/test-facades.ts +170 -458
  77. package/src/templates/core-facade.ts +0 -517
  78. package/src/templates/llm-client.ts +0 -301
@@ -0,0 +1,263 @@
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() {
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
+ const VAULT_TEMPLATE = [
11
+ "import Database from 'better-sqlite3';",
12
+ "import { mkdirSync } from 'node:fs';",
13
+ "import { dirname } from 'node:path';",
14
+ "import type { IntelligenceEntry } from '../intelligence/types.js';",
15
+ '',
16
+ 'export interface SearchResult { entry: IntelligenceEntry; score: number; }',
17
+ 'export interface VaultStats { totalEntries: number; byType: Record<string, number>; byDomain: Record<string, number>; bySeverity: Record<string, number>; }',
18
+ 'export interface ProjectInfo { path: string; name: string; registeredAt: number; lastSeenAt: number; sessionCount: number; }',
19
+ "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; }",
20
+ 'export interface MemoryStats { total: number; byType: Record<string, number>; byProject: Record<string, number>; }',
21
+ '',
22
+ 'export class Vault {',
23
+ ' private db: Database.Database;',
24
+ '',
25
+ " constructor(dbPath: string = ':memory:') {",
26
+ " if (dbPath !== ':memory:') mkdirSync(dirname(dbPath), { recursive: true });",
27
+ ' this.db = new Database(dbPath);',
28
+ " this.db.pragma('journal_mode = WAL');",
29
+ " this.db.pragma('foreign_keys = ON');",
30
+ ' this.initialize();',
31
+ ' }',
32
+ '',
33
+ ' private initialize(): void {',
34
+ ' this.db.exec(`',
35
+ ' CREATE TABLE IF NOT EXISTS entries (',
36
+ ' id TEXT PRIMARY KEY,',
37
+ " type TEXT NOT NULL CHECK(type IN ('pattern', 'anti-pattern', 'rule')),",
38
+ ' domain TEXT NOT NULL,',
39
+ ' title TEXT NOT NULL,',
40
+ " severity TEXT NOT NULL CHECK(severity IN ('critical', 'warning', 'suggestion')),",
41
+ ' description TEXT NOT NULL,',
42
+ ' context TEXT, example TEXT, counter_example TEXT, why TEXT,',
43
+ " tags TEXT NOT NULL DEFAULT '[]',",
44
+ " applies_to TEXT DEFAULT '[]',",
45
+ ' created_at INTEGER NOT NULL DEFAULT (unixepoch()),',
46
+ ' updated_at INTEGER NOT NULL DEFAULT (unixepoch())',
47
+ ' );',
48
+ ' CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(',
49
+ ' id, title, description, context, tags,',
50
+ " content='entries', content_rowid='rowid', tokenize='porter unicode61'",
51
+ ' );',
52
+ ' CREATE TRIGGER IF NOT EXISTS entries_ai AFTER INSERT ON entries BEGIN',
53
+ ' INSERT INTO entries_fts(rowid,id,title,description,context,tags) VALUES(new.rowid,new.id,new.title,new.description,new.context,new.tags);',
54
+ ' END;',
55
+ ' CREATE TRIGGER IF NOT EXISTS entries_ad AFTER DELETE ON entries BEGIN',
56
+ " 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);",
57
+ ' END;',
58
+ ' CREATE TRIGGER IF NOT EXISTS entries_au AFTER UPDATE ON entries BEGIN',
59
+ " 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);",
60
+ ' INSERT INTO entries_fts(rowid,id,title,description,context,tags) VALUES(new.rowid,new.id,new.title,new.description,new.context,new.tags);',
61
+ ' END;',
62
+ ' CREATE TABLE IF NOT EXISTS projects (',
63
+ ' path TEXT PRIMARY KEY,',
64
+ ' name TEXT NOT NULL,',
65
+ ' registered_at INTEGER NOT NULL DEFAULT (unixepoch()),',
66
+ ' last_seen_at INTEGER NOT NULL DEFAULT (unixepoch()),',
67
+ ' session_count INTEGER NOT NULL DEFAULT 1',
68
+ ' );',
69
+ ' CREATE TABLE IF NOT EXISTS memories (',
70
+ ' id TEXT PRIMARY KEY,',
71
+ ' project_path TEXT NOT NULL,',
72
+ " type TEXT NOT NULL CHECK(type IN ('session', 'lesson', 'preference')),",
73
+ ' context TEXT NOT NULL,',
74
+ ' summary TEXT NOT NULL,',
75
+ " topics TEXT NOT NULL DEFAULT '[]',",
76
+ " files_modified TEXT NOT NULL DEFAULT '[]',",
77
+ " tools_used TEXT NOT NULL DEFAULT '[]',",
78
+ ' created_at INTEGER NOT NULL DEFAULT (unixepoch()),',
79
+ ' archived_at INTEGER',
80
+ ' );',
81
+ ' CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(',
82
+ ' id, context, summary, topics,',
83
+ " content='memories', content_rowid='rowid', tokenize='porter unicode61'",
84
+ ' );',
85
+ ' CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN',
86
+ ' INSERT INTO memories_fts(rowid,id,context,summary,topics) VALUES(new.rowid,new.id,new.context,new.summary,new.topics);',
87
+ ' END;',
88
+ ' CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN',
89
+ " INSERT INTO memories_fts(memories_fts,rowid,id,context,summary,topics) VALUES('delete',old.rowid,old.id,old.context,old.summary,old.topics);",
90
+ ' END;',
91
+ ' CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN',
92
+ " INSERT INTO memories_fts(memories_fts,rowid,id,context,summary,topics) VALUES('delete',old.rowid,old.id,old.context,old.summary,old.topics);",
93
+ ' INSERT INTO memories_fts(rowid,id,context,summary,topics) VALUES(new.rowid,new.id,new.context,new.summary,new.topics);',
94
+ ' END;',
95
+ ' CREATE TABLE IF NOT EXISTS brain_vocabulary (',
96
+ ' term TEXT PRIMARY KEY,',
97
+ ' idf REAL NOT NULL,',
98
+ ' doc_count INTEGER NOT NULL DEFAULT 1,',
99
+ ' updated_at INTEGER NOT NULL DEFAULT (unixepoch())',
100
+ ' );',
101
+ ' CREATE TABLE IF NOT EXISTS brain_feedback (',
102
+ ' id INTEGER PRIMARY KEY AUTOINCREMENT,',
103
+ ' query TEXT NOT NULL,',
104
+ ' entry_id TEXT NOT NULL,',
105
+ " action TEXT NOT NULL CHECK(action IN ('accepted', 'dismissed')),",
106
+ ' created_at INTEGER NOT NULL DEFAULT (unixepoch())',
107
+ ' );',
108
+ ' CREATE INDEX IF NOT EXISTS idx_brain_feedback_query ON brain_feedback(query);',
109
+ ' `);',
110
+ ' }',
111
+ '',
112
+ ' seed(entries: IntelligenceEntry[]): number {',
113
+ ' const upsert = this.db.prepare(`',
114
+ ' INSERT INTO entries (id,type,domain,title,severity,description,context,example,counter_example,why,tags,applies_to)',
115
+ ' VALUES (@id,@type,@domain,@title,@severity,@description,@context,@example,@counterExample,@why,@tags,@appliesTo)',
116
+ ' ON CONFLICT(id) DO UPDATE SET type=excluded.type,domain=excluded.domain,title=excluded.title,severity=excluded.severity,',
117
+ ' description=excluded.description,context=excluded.context,example=excluded.example,counter_example=excluded.counter_example,',
118
+ ' why=excluded.why,tags=excluded.tags,applies_to=excluded.applies_to,updated_at=unixepoch()',
119
+ ' `);',
120
+ ' const tx = this.db.transaction((items: IntelligenceEntry[]) => {',
121
+ ' let count = 0;',
122
+ ' for (const entry of items) {',
123
+ ' upsert.run({ id: entry.id, type: entry.type, domain: entry.domain, title: entry.title,',
124
+ ' severity: entry.severity, description: entry.description, context: entry.context ?? null,',
125
+ ' example: entry.example ?? null, counterExample: entry.counterExample ?? null,',
126
+ ' why: entry.why ?? null, tags: JSON.stringify(entry.tags), appliesTo: JSON.stringify(entry.appliesTo ?? []) });',
127
+ ' count++;',
128
+ ' }',
129
+ ' return count;',
130
+ ' });',
131
+ ' return tx(entries);',
132
+ ' }',
133
+ '',
134
+ ' search(query: string, options?: { domain?: string; type?: string; severity?: string; limit?: number }): SearchResult[] {',
135
+ ' const limit = options?.limit ?? 10;',
136
+ ' const filters: string[] = []; const fp: Record<string, string> = {};',
137
+ " if (options?.domain) { filters.push('e.domain = @domain'); fp.domain = options.domain; }",
138
+ " if (options?.type) { filters.push('e.type = @type'); fp.type = options.type; }",
139
+ " if (options?.severity) { filters.push('e.severity = @severity'); fp.severity = options.severity; }",
140
+ " const wc = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';",
141
+ ' 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`)',
142
+ ' .all({ query, limit, ...fp }) as Array<Record<string, unknown>>;',
143
+ ' return rows.map(rowToSearchResult);',
144
+ ' }',
145
+ '',
146
+ ' get(id: string): IntelligenceEntry | null {',
147
+ " const row = this.db.prepare('SELECT * FROM entries WHERE id = ?').get(id) as Record<string, unknown> | undefined;",
148
+ ' return row ? rowToEntry(row) : null;',
149
+ ' }',
150
+ '',
151
+ ' list(options?: { domain?: string; type?: string; severity?: string; tags?: string[]; limit?: number; offset?: number }): IntelligenceEntry[] {',
152
+ ' const filters: string[] = []; const params: Record<string, unknown> = {};',
153
+ " if (options?.domain) { filters.push('domain = @domain'); params.domain = options.domain; }",
154
+ " if (options?.type) { filters.push('type = @type'); params.type = options.type; }",
155
+ " if (options?.severity) { filters.push('severity = @severity'); params.severity = options.severity; }",
156
+ ' 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 \')})`); }',
157
+ " const wc = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';",
158
+ ' const rows = this.db.prepare(`SELECT * FROM entries ${wc} ORDER BY severity, domain, title LIMIT @limit OFFSET @offset`)',
159
+ ' .all({ ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 }) as Array<Record<string, unknown>>;',
160
+ ' return rows.map(rowToEntry);',
161
+ ' }',
162
+ '',
163
+ ' stats(): VaultStats {',
164
+ " const total = (this.db.prepare('SELECT COUNT(*) as count FROM entries').get() as { count: number }).count;",
165
+ " return { totalEntries: total, byType: gc(this.db, 'type'), byDomain: gc(this.db, 'domain'), bySeverity: gc(this.db, 'severity') };",
166
+ ' }',
167
+ '',
168
+ ' add(entry: IntelligenceEntry): void { this.seed([entry]); }',
169
+ " remove(id: string): boolean { return this.db.prepare('DELETE FROM entries WHERE id = ?').run(id).changes > 0; }",
170
+ '',
171
+ ' registerProject(path: string, name?: string): ProjectInfo {',
172
+ " const projectName = name ?? path.split('/').filter(Boolean).pop() ?? path;",
173
+ ' const existing = this.getProject(path);',
174
+ ' if (existing) {',
175
+ " this.db.prepare('UPDATE projects SET last_seen_at = unixepoch(), session_count = session_count + 1 WHERE path = ?').run(path);",
176
+ ' return this.getProject(path)!;',
177
+ ' }',
178
+ " this.db.prepare('INSERT INTO projects (path, name) VALUES (?, ?)').run(path, projectName);",
179
+ ' return this.getProject(path)!;',
180
+ ' }',
181
+ '',
182
+ ' getProject(path: string): ProjectInfo | null {',
183
+ " const row = this.db.prepare('SELECT * FROM projects WHERE path = ?').get(path) as Record<string, unknown> | undefined;",
184
+ ' if (!row) return null;',
185
+ ' 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 };',
186
+ ' }',
187
+ '',
188
+ ' listProjects(): ProjectInfo[] {',
189
+ " const rows = this.db.prepare('SELECT * FROM projects ORDER BY last_seen_at DESC').all() as Array<Record<string, unknown>>;",
190
+ ' 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 }));',
191
+ ' }',
192
+ '',
193
+ " captureMemory(memory: Omit<Memory, 'id' | 'createdAt' | 'archivedAt'>): Memory {",
194
+ ' const id = `mem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;',
195
+ ' 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)`)',
196
+ ' .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) });',
197
+ ' return this.getMemory(id)!;',
198
+ ' }',
199
+ '',
200
+ ' searchMemories(query: string, options?: { type?: string; projectPath?: string; limit?: number }): Memory[] {',
201
+ ' const limit = options?.limit ?? 10;',
202
+ " const filters: string[] = ['m.archived_at IS NULL']; const fp: Record<string, unknown> = {};",
203
+ " if (options?.type) { filters.push('m.type = @type'); fp.type = options.type; }",
204
+ " if (options?.projectPath) { filters.push('m.project_path = @projectPath'); fp.projectPath = options.projectPath; }",
205
+ " const wc = filters.length > 0 ? `AND ${filters.join(' AND ')}` : '';",
206
+ ' 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`)',
207
+ ' .all({ query, limit, ...fp }) as Array<Record<string, unknown>>;',
208
+ ' return rows.map(rowToMemory);',
209
+ ' }',
210
+ '',
211
+ ' listMemories(options?: { type?: string; projectPath?: string; limit?: number; offset?: number }): Memory[] {',
212
+ " const filters: string[] = ['archived_at IS NULL']; const params: Record<string, unknown> = {};",
213
+ " if (options?.type) { filters.push('type = @type'); params.type = options.type; }",
214
+ " if (options?.projectPath) { filters.push('project_path = @projectPath'); params.projectPath = options.projectPath; }",
215
+ " const wc = `WHERE ${filters.join(' AND ')}`;",
216
+ ' const rows = this.db.prepare(`SELECT * FROM memories ${wc} ORDER BY created_at DESC LIMIT @limit OFFSET @offset`)',
217
+ ' .all({ ...params, limit: options?.limit ?? 50, offset: options?.offset ?? 0 }) as Array<Record<string, unknown>>;',
218
+ ' return rows.map(rowToMemory);',
219
+ ' }',
220
+ '',
221
+ ' memoryStats(): MemoryStats {',
222
+ " const total = (this.db.prepare('SELECT COUNT(*) as count FROM memories WHERE archived_at IS NULL').get() as { count: number }).count;",
223
+ " 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 }>;",
224
+ " 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 }>;",
225
+ ' return { total, byType: Object.fromEntries(byTypeRows.map((r) => [r.key, r.count])), byProject: Object.fromEntries(byProjectRows.map((r) => [r.key, r.count])) };',
226
+ ' }',
227
+ '',
228
+ ' getMemory(id: string): Memory | null {',
229
+ " const row = this.db.prepare('SELECT * FROM memories WHERE id = ?').get(id) as Record<string, unknown> | undefined;",
230
+ ' return row ? rowToMemory(row) : null;',
231
+ ' }',
232
+ '',
233
+ ' getDb(): Database.Database { return this.db; }',
234
+ '',
235
+ ' close(): void { this.db.close(); }',
236
+ '}',
237
+ '',
238
+ 'function gc(db: Database.Database, col: string): Record<string, number> {',
239
+ ' const rows = db.prepare(`SELECT ${col} as key, COUNT(*) as count FROM entries GROUP BY ${col}`).all() as Array<{ key: string; count: number }>;',
240
+ ' return Object.fromEntries(rows.map((r) => [r.key, r.count]));',
241
+ '}',
242
+ '',
243
+ 'function rowToEntry(row: Record<string, unknown>): IntelligenceEntry {',
244
+ " return { id: row.id as string, type: row.type as IntelligenceEntry['type'], domain: row.domain as IntelligenceEntry['domain'],",
245
+ " title: row.title as string, severity: row.severity as IntelligenceEntry['severity'], description: row.description as string,",
246
+ ' context: (row.context as string) ?? undefined, example: (row.example as string) ?? undefined,',
247
+ ' counterExample: (row.counter_example as string) ?? undefined, why: (row.why as string) ?? undefined,',
248
+ " tags: JSON.parse((row.tags as string) || '[]'), appliesTo: JSON.parse((row.applies_to as string) || '[]') };",
249
+ '}',
250
+ '',
251
+ 'function rowToSearchResult(row: Record<string, unknown>): SearchResult {',
252
+ ' return { entry: rowToEntry(row), score: row.score as number };',
253
+ '}',
254
+ '',
255
+ 'function rowToMemory(row: Record<string, unknown>): Memory {',
256
+ " return { id: row.id as string, projectPath: row.project_path as string, type: row.type as Memory['type'],",
257
+ ' context: row.context as string, summary: row.summary as string,',
258
+ " topics: JSON.parse((row.topics as string) || '[]'), filesModified: JSON.parse((row.files_modified as string) || '[]'),",
259
+ " toolsUsed: JSON.parse((row.tools_used as string) || '[]'), createdAt: row.created_at as number,",
260
+ ' archivedAt: (row.archived_at as number) ?? null };',
261
+ '}',
262
+ ].join('\n');
263
+ //# sourceMappingURL=vault.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vault.js","sourceRoot":"","sources":["../../src/templates/vault.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -18,8 +18,8 @@ export declare const AgentConfigSchema: z.ZodObject<{
18
18
  /** Output directory (parent — agent dir will be created inside) */
19
19
  outputDir: z.ZodString;
20
20
  }, "strip", z.ZodTypeAny, {
21
- id: string;
22
21
  name: string;
22
+ id: string;
23
23
  role: string;
24
24
  description: string;
25
25
  domains: string[];
@@ -27,8 +27,8 @@ export declare const AgentConfigSchema: z.ZodObject<{
27
27
  greeting: string;
28
28
  outputDir: string;
29
29
  }, {
30
- id: string;
31
30
  name: string;
31
+ id: string;
32
32
  role: string;
33
33
  description: string;
34
34
  domains: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soleri/forge",
3
- "version": "4.2.2",
3
+ "version": "5.0.0",
4
4
  "description": "Scaffold AI agents that learn, remember, and grow with you.",
5
5
  "keywords": [
6
6
  "agent",
@@ -42,23 +42,29 @@ describe('Scaffolder', () => {
42
42
  expect(preview.persona.name).toBe('Atlas');
43
43
  expect(preview.persona.role).toBe('Data Engineering Advisor');
44
44
  expect(preview.domains).toEqual(['data-pipelines', 'data-quality', 'etl']);
45
- expect(preview.files.length).toBeGreaterThan(15);
45
+ expect(preview.files.length).toBeGreaterThan(10);
46
46
 
47
- // Should include README, setup script, and LLM client (static modules now in @soleri/core)
48
47
  const paths = preview.files.map((f) => f.path);
49
48
  expect(paths).toContain('README.md');
50
49
  expect(paths).toContain('scripts/setup.sh');
51
- expect(paths).toContain('src/llm/llm-client.ts');
50
+ expect(paths).toContain('src/index.ts');
52
51
  expect(paths).toContain('src/__tests__/facades.test.ts');
53
- // Static modules should NOT be in preview (they live in @soleri/core now)
54
- expect(paths).not.toContain('src/vault/vault.ts');
55
- expect(paths).not.toContain('src/brain/brain.ts');
56
- expect(paths).not.toContain('src/planning/planner.ts');
57
52
 
58
- // Should have domain facades + core facade
53
+ // v5.0: These are no longer generated (live in @soleri/core)
54
+ expect(paths).not.toContain('src/llm/llm-client.ts');
55
+ expect(paths).not.toContain('src/facades/core.facade.ts');
56
+ expect(paths).not.toContain('src/facades/data-pipelines.facade.ts');
57
+
58
+ // Should have domain facades + core facade in preview
59
59
  expect(preview.facades).toHaveLength(4); // 3 domains + core
60
60
  expect(preview.facades[0].name).toBe('atlas_data_pipelines');
61
61
 
62
+ // Core facade should list all 31 ops
63
+ const coreFacade = preview.facades.find((f) => f.name === 'atlas_core')!;
64
+ expect(coreFacade.ops.length).toBe(31);
65
+ expect(coreFacade.ops).toContain('curator_status');
66
+ expect(coreFacade.ops).toContain('health');
67
+
62
68
  // Should NOT create any files
63
69
  expect(existsSync(join(tempDir, 'atlas'))).toBe(false);
64
70
  });
@@ -71,36 +77,33 @@ describe('Scaffolder', () => {
71
77
  expect(result.success).toBe(true);
72
78
  expect(result.agentDir).toBe(join(tempDir, 'atlas'));
73
79
  expect(result.domains).toEqual(['data-pipelines', 'data-quality', 'etl']);
74
- expect(result.filesCreated.length).toBeGreaterThan(10);
80
+ expect(result.filesCreated.length).toBeGreaterThan(8);
75
81
  });
76
82
 
77
- it('should create all expected directories', () => {
83
+ it('should create expected directories (no facades/ or llm/ dirs)', () => {
78
84
  scaffold(testConfig);
79
85
  const agentDir = join(tempDir, 'atlas');
80
86
 
81
- expect(existsSync(join(agentDir, 'src', 'facades'))).toBe(true);
82
87
  expect(existsSync(join(agentDir, 'src', 'intelligence', 'data'))).toBe(true);
83
88
  expect(existsSync(join(agentDir, 'src', 'identity'))).toBe(true);
84
89
  expect(existsSync(join(agentDir, 'src', 'activation'))).toBe(true);
85
- expect(existsSync(join(agentDir, 'src', 'llm'))).toBe(true);
86
- // Static module directories should NOT exist (now in @soleri/core)
87
- expect(existsSync(join(agentDir, 'src', 'vault'))).toBe(false);
88
- expect(existsSync(join(agentDir, 'src', 'planning'))).toBe(false);
89
- expect(existsSync(join(agentDir, 'src', 'brain'))).toBe(false);
90
+ // v5.0: facades/ and llm/ dirs are no longer generated
91
+ expect(existsSync(join(agentDir, 'src', 'facades'))).toBe(false);
92
+ expect(existsSync(join(agentDir, 'src', 'llm'))).toBe(false);
90
93
  });
91
94
 
92
- it('should create valid package.json', () => {
95
+ it('should create valid package.json with @soleri/core ^2.0.0', () => {
93
96
  scaffold(testConfig);
94
97
  const pkg = JSON.parse(readFileSync(join(tempDir, 'atlas', 'package.json'), 'utf-8'));
95
98
 
96
99
  expect(pkg.name).toBe('atlas-mcp');
97
100
  expect(pkg.type).toBe('module');
98
101
  expect(pkg.dependencies['@modelcontextprotocol/sdk']).toBeDefined();
99
- expect(pkg.dependencies['@soleri/core']).toBeDefined();
102
+ expect(pkg.dependencies['@soleri/core']).toBe('^2.0.0');
100
103
  expect(pkg.dependencies['zod']).toBeDefined();
101
- expect(pkg.dependencies['@anthropic-ai/sdk']).toBeDefined();
102
- // better-sqlite3 is now transitive via @soleri/core
103
- expect(pkg.dependencies['better-sqlite3']).toBeUndefined();
104
+ // Anthropic SDK is now optional (LLMClient in core handles dynamic import)
105
+ expect(pkg.dependencies['@anthropic-ai/sdk']).toBeUndefined();
106
+ expect(pkg.optionalDependencies['@anthropic-ai/sdk']).toBeDefined();
104
107
  });
105
108
 
106
109
  it('should create persona with correct config', () => {
@@ -115,20 +118,6 @@ describe('Scaffolder', () => {
115
118
  expect(persona).toContain('Data quality is non-negotiable');
116
119
  });
117
120
 
118
- it('should create domain facades', () => {
119
- scaffold(testConfig);
120
- const facadesDir = join(tempDir, 'atlas', 'src', 'facades');
121
- const files = readdirSync(facadesDir);
122
-
123
- expect(files).toContain('data-pipelines.facade.ts');
124
- expect(files).toContain('data-quality.facade.ts');
125
- expect(files).toContain('etl.facade.ts');
126
- expect(files).toContain('core.facade.ts');
127
- // facade-factory.ts and types.ts are now in @soleri/core
128
- expect(files).not.toContain('facade-factory.ts');
129
- expect(files).not.toContain('types.ts');
130
- });
131
-
132
121
  it('should create empty intelligence data files', () => {
133
122
  scaffold(testConfig);
134
123
  const dataDir = join(tempDir, 'atlas', 'src', 'intelligence', 'data');
@@ -138,29 +127,30 @@ describe('Scaffolder', () => {
138
127
  expect(files).toContain('data-quality.json');
139
128
  expect(files).toContain('etl.json');
140
129
 
141
- // Each file should have empty entries array
142
130
  const bundle = JSON.parse(readFileSync(join(dataDir, 'data-pipelines.json'), 'utf-8'));
143
131
  expect(bundle.domain).toBe('data-pipelines');
144
132
  expect(bundle.entries).toEqual([]);
145
133
  });
146
134
 
147
- it('should create entry point importing from @soleri/core', () => {
135
+ it('should create entry point using runtime factories from @soleri/core', () => {
148
136
  scaffold(testConfig);
149
137
  const entry = readFileSync(join(tempDir, 'atlas', 'src', 'index.ts'), 'utf-8');
150
138
 
151
- expect(entry).toContain('createDataPipelinesFacade');
152
- expect(entry).toContain('createDataQualityFacade');
153
- expect(entry).toContain('createEtlFacade');
154
- expect(entry).toContain('createCoreFacade');
139
+ // v5.0 runtime factory pattern
140
+ expect(entry).toContain('createAgentRuntime');
141
+ expect(entry).toContain('createCoreOps');
142
+ expect(entry).toContain('createDomainFacades');
143
+ expect(entry).toContain("from '@soleri/core'");
144
+ expect(entry).toContain("agentId: 'atlas'");
155
145
  expect(entry).toContain("name: 'atlas-mcp'");
156
- expect(entry).toContain('Brain');
157
- expect(entry).toContain('LLMClient');
158
- expect(entry).toContain('KeyPool');
159
- expect(entry).toContain('loadKeyPoolConfig');
160
146
  expect(entry).toContain('Hello');
161
- // Should import shared modules from @soleri/core
162
- expect(entry).toContain("from '@soleri/core'");
163
- expect(entry).toContain("loadKeyPoolConfig('atlas')");
147
+ // Agent-specific ops still reference persona/activation
148
+ expect(entry).toContain('PERSONA');
149
+ expect(entry).toContain('activateAgent');
150
+ // Domain list is embedded
151
+ expect(entry).toContain('data-pipelines');
152
+ expect(entry).toContain('data-quality');
153
+ expect(entry).toContain('etl');
164
154
  });
165
155
 
166
156
  it('should create .mcp.json for client config', () => {
@@ -206,99 +196,52 @@ describe('Scaffolder', () => {
206
196
  expect(readme).toContain('# Atlas');
207
197
  expect(readme).toContain('Data Engineering Advisor');
208
198
  expect(readme).toContain('Hello, Atlas!');
209
- expect(readme).toContain('Goodbye, Atlas!');
210
199
  expect(readme).toContain('data-pipelines');
211
- expect(readme).toContain('data-quality');
212
- expect(readme).toContain('etl');
213
- expect(readme).toContain('Data quality is non-negotiable');
214
- expect(readme).toContain('./scripts/setup.sh');
215
200
  });
216
201
 
217
- it('should create executable setup.sh with agent-specific content', () => {
202
+ it('should create executable setup.sh', () => {
218
203
  scaffold(testConfig);
219
204
  const setupPath = join(tempDir, 'atlas', 'scripts', 'setup.sh');
220
205
  const setup = readFileSync(setupPath, 'utf-8');
221
206
 
222
- // Content checks
223
207
  expect(setup).toContain('AGENT_NAME="atlas"');
224
- expect(setup).toContain('=== Atlas Setup ===');
225
- expect(setup).toContain('Building Atlas...');
226
- expect(setup).toContain('Hello, Atlas!');
227
208
  expect(setup).toContain('#!/usr/bin/env bash');
228
- expect(setup).toContain('claude mcp add');
229
209
 
230
- // Executable permission check
231
210
  const stats = statSync(setupPath);
232
211
  const isExecutable = (stats.mode & 0o111) !== 0;
233
212
  expect(isExecutable).toBe(true);
234
213
  });
235
214
 
236
- it('should create LLM client file importing from @soleri/core', () => {
237
- scaffold(testConfig);
238
- const llmDir = join(tempDir, 'atlas', 'src', 'llm');
239
- // Only llm-client.ts should exist (types, utils, key-pool are in @soleri/core)
240
- expect(existsSync(join(llmDir, 'llm-client.ts'))).toBe(true);
241
- expect(existsSync(join(llmDir, 'types.ts'))).toBe(false);
242
- expect(existsSync(join(llmDir, 'utils.ts'))).toBe(false);
243
- expect(existsSync(join(llmDir, 'key-pool.ts'))).toBe(false);
244
-
245
- const client = readFileSync(join(llmDir, 'llm-client.ts'), 'utf-8');
246
- expect(client).toContain('class LLMClient');
247
- expect(client).toContain('class ModelRouter');
248
- expect(client).toContain('.atlas');
249
- expect(client).toContain("from '@soleri/core'");
250
- });
251
-
252
- it('should only create facades test file (static tests in @soleri/core)', () => {
253
- scaffold(testConfig);
254
- const testsDir = join(tempDir, 'atlas', 'src', '__tests__');
255
- const files = readdirSync(testsDir);
256
-
257
- expect(files).toContain('facades.test.ts');
258
- // Static module tests are now in @soleri/core
259
- expect(files).not.toContain('vault.test.ts');
260
- expect(files).not.toContain('loader.test.ts');
261
- expect(files).not.toContain('planner.test.ts');
262
- expect(files).not.toContain('brain.test.ts');
263
- expect(files).not.toContain('llm.test.ts');
264
- });
265
-
266
- it('should generate facade tests referencing all domains', () => {
215
+ it('should generate facade tests using runtime factories', () => {
267
216
  scaffold(testConfig);
268
217
  const facadesTest = readFileSync(
269
218
  join(tempDir, 'atlas', 'src', '__tests__', 'facades.test.ts'),
270
219
  'utf-8',
271
220
  );
272
221
 
222
+ // Should use runtime factories from @soleri/core
223
+ expect(facadesTest).toContain('createAgentRuntime');
224
+ expect(facadesTest).toContain('createCoreOps');
225
+ expect(facadesTest).toContain('createDomainFacade');
226
+ expect(facadesTest).toContain("from '@soleri/core'");
227
+
228
+ // Domain facades
273
229
  expect(facadesTest).toContain('atlas_data_pipelines');
274
230
  expect(facadesTest).toContain('atlas_data_quality');
275
231
  expect(facadesTest).toContain('atlas_etl');
276
232
  expect(facadesTest).toContain('atlas_core');
277
- expect(facadesTest).toContain('createDataPipelinesFacade');
278
- // Should import shared modules from @soleri/core
279
- expect(facadesTest).toContain("from '@soleri/core'");
280
- // Activation ops should be tested
233
+
234
+ // Agent-specific ops tested
281
235
  expect(facadesTest).toContain('activate');
282
236
  expect(facadesTest).toContain('inject_claude_md');
283
237
  expect(facadesTest).toContain('setup');
284
- // Memory + planning ops should be tested
285
- expect(facadesTest).toContain('memory_capture');
286
- expect(facadesTest).toContain('memory_search');
287
- expect(facadesTest).toContain('create_plan');
288
- expect(facadesTest).toContain('complete_plan');
289
- // Brain ops should be tested
290
- expect(facadesTest).toContain('record_feedback');
291
- expect(facadesTest).toContain('rebuild_vocabulary');
292
- expect(facadesTest).toContain('brain_stats');
293
- // LLM ops should be tested
294
- expect(facadesTest).toContain('llm_status');
295
- expect(facadesTest).toContain('LLMClient');
296
- expect(facadesTest).toContain('KeyPool');
238
+ expect(facadesTest).toContain('health');
239
+ expect(facadesTest).toContain('identity');
297
240
  });
298
241
 
299
242
  it('should fail if directory already exists', () => {
300
243
  scaffold(testConfig);
301
- const result = scaffold(testConfig); // second time
244
+ const result = scaffold(testConfig);
302
245
 
303
246
  expect(result.success).toBe(false);
304
247
  expect(result.summary).toContain('already exists');