@soleri/forge 0.0.1 → 4.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 (128) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +199 -0
  3. package/dist/facades/forge.facade.d.ts +9 -0
  4. package/dist/facades/forge.facade.js +134 -0
  5. package/dist/facades/forge.facade.js.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +81 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/knowledge-installer.d.ts +31 -0
  10. package/dist/knowledge-installer.js +437 -0
  11. package/dist/knowledge-installer.js.map +1 -0
  12. package/dist/scaffolder.d.ts +13 -0
  13. package/dist/scaffolder.js +330 -0
  14. package/dist/scaffolder.js.map +1 -0
  15. package/dist/templates/activate.d.ts +9 -0
  16. package/dist/templates/activate.js +139 -0
  17. package/dist/templates/activate.js.map +1 -0
  18. package/dist/templates/brain.d.ts +6 -0
  19. package/dist/templates/brain.js +478 -0
  20. package/dist/templates/brain.js.map +1 -0
  21. package/dist/templates/claude-md-template.d.ts +11 -0
  22. package/dist/templates/claude-md-template.js +73 -0
  23. package/dist/templates/claude-md-template.js.map +1 -0
  24. package/dist/templates/core-facade.d.ts +6 -0
  25. package/dist/templates/core-facade.js +456 -0
  26. package/dist/templates/core-facade.js.map +1 -0
  27. package/dist/templates/domain-facade.d.ts +7 -0
  28. package/dist/templates/domain-facade.js +119 -0
  29. package/dist/templates/domain-facade.js.map +1 -0
  30. package/dist/templates/entry-point.d.ts +5 -0
  31. package/dist/templates/entry-point.js +116 -0
  32. package/dist/templates/entry-point.js.map +1 -0
  33. package/dist/templates/facade-factory.d.ts +1 -0
  34. package/dist/templates/facade-factory.js +63 -0
  35. package/dist/templates/facade-factory.js.map +1 -0
  36. package/dist/templates/facade-types.d.ts +1 -0
  37. package/dist/templates/facade-types.js +46 -0
  38. package/dist/templates/facade-types.js.map +1 -0
  39. package/dist/templates/inject-claude-md.d.ts +11 -0
  40. package/dist/templates/inject-claude-md.js +92 -0
  41. package/dist/templates/inject-claude-md.js.map +1 -0
  42. package/dist/templates/intelligence-loader.d.ts +1 -0
  43. package/dist/templates/intelligence-loader.js +43 -0
  44. package/dist/templates/intelligence-loader.js.map +1 -0
  45. package/dist/templates/intelligence-types.d.ts +1 -0
  46. package/dist/templates/intelligence-types.js +24 -0
  47. package/dist/templates/intelligence-types.js.map +1 -0
  48. package/dist/templates/llm-client.d.ts +7 -0
  49. package/dist/templates/llm-client.js +300 -0
  50. package/dist/templates/llm-client.js.map +1 -0
  51. package/dist/templates/llm-key-pool.d.ts +7 -0
  52. package/dist/templates/llm-key-pool.js +211 -0
  53. package/dist/templates/llm-key-pool.js.map +1 -0
  54. package/dist/templates/llm-types.d.ts +5 -0
  55. package/dist/templates/llm-types.js +161 -0
  56. package/dist/templates/llm-types.js.map +1 -0
  57. package/dist/templates/llm-utils.d.ts +5 -0
  58. package/dist/templates/llm-utils.js +260 -0
  59. package/dist/templates/llm-utils.js.map +1 -0
  60. package/dist/templates/package-json.d.ts +2 -0
  61. package/dist/templates/package-json.js +37 -0
  62. package/dist/templates/package-json.js.map +1 -0
  63. package/dist/templates/persona.d.ts +2 -0
  64. package/dist/templates/persona.js +42 -0
  65. package/dist/templates/persona.js.map +1 -0
  66. package/dist/templates/planner.d.ts +5 -0
  67. package/dist/templates/planner.js +150 -0
  68. package/dist/templates/planner.js.map +1 -0
  69. package/dist/templates/readme.d.ts +5 -0
  70. package/dist/templates/readme.js +316 -0
  71. package/dist/templates/readme.js.map +1 -0
  72. package/dist/templates/setup-script.d.ts +6 -0
  73. package/dist/templates/setup-script.js +112 -0
  74. package/dist/templates/setup-script.js.map +1 -0
  75. package/dist/templates/test-brain.d.ts +6 -0
  76. package/dist/templates/test-brain.js +474 -0
  77. package/dist/templates/test-brain.js.map +1 -0
  78. package/dist/templates/test-facades.d.ts +6 -0
  79. package/dist/templates/test-facades.js +649 -0
  80. package/dist/templates/test-facades.js.map +1 -0
  81. package/dist/templates/test-llm.d.ts +7 -0
  82. package/dist/templates/test-llm.js +574 -0
  83. package/dist/templates/test-llm.js.map +1 -0
  84. package/dist/templates/test-loader.d.ts +5 -0
  85. package/dist/templates/test-loader.js +146 -0
  86. package/dist/templates/test-loader.js.map +1 -0
  87. package/dist/templates/test-planner.d.ts +5 -0
  88. package/dist/templates/test-planner.js +271 -0
  89. package/dist/templates/test-planner.js.map +1 -0
  90. package/dist/templates/test-vault.d.ts +5 -0
  91. package/dist/templates/test-vault.js +380 -0
  92. package/dist/templates/test-vault.js.map +1 -0
  93. package/dist/templates/tsconfig.d.ts +1 -0
  94. package/dist/templates/tsconfig.js +25 -0
  95. package/dist/templates/tsconfig.js.map +1 -0
  96. package/dist/templates/vault.d.ts +5 -0
  97. package/dist/templates/vault.js +263 -0
  98. package/dist/templates/vault.js.map +1 -0
  99. package/dist/templates/vitest-config.d.ts +1 -0
  100. package/dist/templates/vitest-config.js +27 -0
  101. package/dist/templates/vitest-config.js.map +1 -0
  102. package/dist/types.d.ts +89 -0
  103. package/dist/types.js +21 -0
  104. package/dist/types.js.map +1 -0
  105. package/package.json +42 -4
  106. package/src/__tests__/knowledge-installer.test.ts +805 -0
  107. package/src/__tests__/scaffolder.test.ts +323 -0
  108. package/src/facades/forge.facade.ts +150 -0
  109. package/src/index.ts +101 -0
  110. package/src/knowledge-installer.ts +532 -0
  111. package/src/scaffolder.ts +386 -0
  112. package/src/templates/activate.ts +145 -0
  113. package/src/templates/claude-md-template.ts +137 -0
  114. package/src/templates/core-facade.ts +457 -0
  115. package/src/templates/domain-facade.ts +121 -0
  116. package/src/templates/entry-point.ts +120 -0
  117. package/src/templates/inject-claude-md.ts +94 -0
  118. package/src/templates/llm-client.ts +301 -0
  119. package/src/templates/package-json.ts +39 -0
  120. package/src/templates/persona.ts +45 -0
  121. package/src/templates/readme.ts +319 -0
  122. package/src/templates/setup-script.ts +113 -0
  123. package/src/templates/test-facades.ts +656 -0
  124. package/src/templates/tsconfig.ts +25 -0
  125. package/src/templates/vitest-config.ts +26 -0
  126. package/src/types.ts +68 -0
  127. package/tsconfig.json +21 -0
  128. package/vitest.config.ts +15 -0
@@ -0,0 +1,323 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdirSync, rmSync, existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { scaffold, previewScaffold, listAgents } from '../scaffolder.js';
6
+ import type { AgentConfig } from '../types.js';
7
+
8
+ describe('Scaffolder', () => {
9
+ let tempDir: string;
10
+
11
+ const testConfig: AgentConfig = {
12
+ id: 'atlas',
13
+ name: 'Atlas',
14
+ role: 'Data Engineering Advisor',
15
+ description:
16
+ 'Atlas provides guidance on data pipelines, ETL patterns, and data quality practices.',
17
+ domains: ['data-pipelines', 'data-quality', 'etl'],
18
+ principles: [
19
+ 'Data quality is non-negotiable',
20
+ 'Idempotent pipelines always',
21
+ 'Schema evolution over breaking changes',
22
+ ],
23
+ greeting: 'Atlas here. I help with data engineering patterns and best practices.',
24
+ outputDir: '', // set in beforeEach
25
+ };
26
+
27
+ beforeEach(() => {
28
+ tempDir = join(tmpdir(), `forge-test-${Date.now()}`);
29
+ mkdirSync(tempDir, { recursive: true });
30
+ testConfig.outputDir = tempDir;
31
+ });
32
+
33
+ afterEach(() => {
34
+ rmSync(tempDir, { recursive: true, force: true });
35
+ });
36
+
37
+ describe('previewScaffold', () => {
38
+ it('should return preview without creating files', () => {
39
+ const preview = previewScaffold(testConfig);
40
+
41
+ expect(preview.agentDir).toBe(join(tempDir, 'atlas'));
42
+ expect(preview.persona.name).toBe('Atlas');
43
+ expect(preview.persona.role).toBe('Data Engineering Advisor');
44
+ expect(preview.domains).toEqual(['data-pipelines', 'data-quality', 'etl']);
45
+ expect(preview.files.length).toBeGreaterThan(15);
46
+
47
+ // Should include README, setup script, and LLM client (static modules now in @soleri/core)
48
+ const paths = preview.files.map((f) => f.path);
49
+ expect(paths).toContain('README.md');
50
+ expect(paths).toContain('scripts/setup.sh');
51
+ expect(paths).toContain('src/llm/llm-client.ts');
52
+ 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
+
58
+ // Should have domain facades + core facade
59
+ expect(preview.facades).toHaveLength(4); // 3 domains + core
60
+ expect(preview.facades[0].name).toBe('atlas_data_pipelines');
61
+
62
+ // Should NOT create any files
63
+ expect(existsSync(join(tempDir, 'atlas'))).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe('scaffold', () => {
68
+ it('should create a complete agent project', () => {
69
+ const result = scaffold(testConfig);
70
+
71
+ expect(result.success).toBe(true);
72
+ expect(result.agentDir).toBe(join(tempDir, 'atlas'));
73
+ expect(result.domains).toEqual(['data-pipelines', 'data-quality', 'etl']);
74
+ expect(result.filesCreated.length).toBeGreaterThan(10);
75
+ });
76
+
77
+ it('should create all expected directories', () => {
78
+ scaffold(testConfig);
79
+ const agentDir = join(tempDir, 'atlas');
80
+
81
+ expect(existsSync(join(agentDir, 'src', 'facades'))).toBe(true);
82
+ expect(existsSync(join(agentDir, 'src', 'intelligence', 'data'))).toBe(true);
83
+ expect(existsSync(join(agentDir, 'src', 'identity'))).toBe(true);
84
+ 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
+ });
91
+
92
+ it('should create valid package.json', () => {
93
+ scaffold(testConfig);
94
+ const pkg = JSON.parse(readFileSync(join(tempDir, 'atlas', 'package.json'), 'utf-8'));
95
+
96
+ expect(pkg.name).toBe('atlas-mcp');
97
+ expect(pkg.type).toBe('module');
98
+ expect(pkg.dependencies['@modelcontextprotocol/sdk']).toBeDefined();
99
+ expect(pkg.dependencies['@soleri/core']).toBeDefined();
100
+ 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
+ });
105
+
106
+ it('should create persona with correct config', () => {
107
+ scaffold(testConfig);
108
+ const persona = readFileSync(
109
+ join(tempDir, 'atlas', 'src', 'identity', 'persona.ts'),
110
+ 'utf-8',
111
+ );
112
+
113
+ expect(persona).toContain("name: 'Atlas'");
114
+ expect(persona).toContain("role: 'Data Engineering Advisor'");
115
+ expect(persona).toContain('Data quality is non-negotiable');
116
+ });
117
+
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
+ it('should create empty intelligence data files', () => {
133
+ scaffold(testConfig);
134
+ const dataDir = join(tempDir, 'atlas', 'src', 'intelligence', 'data');
135
+ const files = readdirSync(dataDir);
136
+
137
+ expect(files).toContain('data-pipelines.json');
138
+ expect(files).toContain('data-quality.json');
139
+ expect(files).toContain('etl.json');
140
+
141
+ // Each file should have empty entries array
142
+ const bundle = JSON.parse(readFileSync(join(dataDir, 'data-pipelines.json'), 'utf-8'));
143
+ expect(bundle.domain).toBe('data-pipelines');
144
+ expect(bundle.entries).toEqual([]);
145
+ });
146
+
147
+ it('should create entry point importing from @soleri/core', () => {
148
+ scaffold(testConfig);
149
+ const entry = readFileSync(join(tempDir, 'atlas', 'src', 'index.ts'), 'utf-8');
150
+
151
+ expect(entry).toContain('createDataPipelinesFacade');
152
+ expect(entry).toContain('createDataQualityFacade');
153
+ expect(entry).toContain('createEtlFacade');
154
+ expect(entry).toContain('createCoreFacade');
155
+ 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
+ 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')");
164
+ });
165
+
166
+ it('should create .mcp.json for client config', () => {
167
+ scaffold(testConfig);
168
+ const mcp = JSON.parse(readFileSync(join(tempDir, 'atlas', '.mcp.json'), 'utf-8'));
169
+
170
+ expect(mcp.mcpServers.atlas).toBeDefined();
171
+ expect(mcp.mcpServers.atlas.command).toBe('node');
172
+ });
173
+
174
+ it('should create activation files', () => {
175
+ scaffold(testConfig);
176
+ const activationDir = join(tempDir, 'atlas', 'src', 'activation');
177
+ const files = readdirSync(activationDir);
178
+
179
+ expect(files).toContain('claude-md-content.ts');
180
+ expect(files).toContain('inject-claude-md.ts');
181
+ expect(files).toContain('activate.ts');
182
+ });
183
+
184
+ it('should create activation files with correct content', () => {
185
+ scaffold(testConfig);
186
+ const activationDir = join(tempDir, 'atlas', 'src', 'activation');
187
+
188
+ const claudeMd = readFileSync(join(activationDir, 'claude-md-content.ts'), 'utf-8');
189
+ expect(claudeMd).toContain('atlas:mode');
190
+ expect(claudeMd).toContain('getClaudeMdContent');
191
+
192
+ const inject = readFileSync(join(activationDir, 'inject-claude-md.ts'), 'utf-8');
193
+ expect(inject).toContain('injectClaudeMd');
194
+ expect(inject).toContain('getClaudeMdContent');
195
+
196
+ const activate = readFileSync(join(activationDir, 'activate.ts'), 'utf-8');
197
+ expect(activate).toContain('activateAgent');
198
+ expect(activate).toContain('deactivateAgent');
199
+ expect(activate).toContain('PERSONA');
200
+ });
201
+
202
+ it('should create README.md with agent-specific content', () => {
203
+ scaffold(testConfig);
204
+ const readme = readFileSync(join(tempDir, 'atlas', 'README.md'), 'utf-8');
205
+
206
+ expect(readme).toContain('# Atlas');
207
+ expect(readme).toContain('Data Engineering Advisor');
208
+ expect(readme).toContain('Hello, Atlas!');
209
+ expect(readme).toContain('Goodbye, Atlas!');
210
+ 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
+ });
216
+
217
+ it('should create executable setup.sh with agent-specific content', () => {
218
+ scaffold(testConfig);
219
+ const setupPath = join(tempDir, 'atlas', 'scripts', 'setup.sh');
220
+ const setup = readFileSync(setupPath, 'utf-8');
221
+
222
+ // Content checks
223
+ 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
+ expect(setup).toContain('#!/usr/bin/env bash');
228
+ expect(setup).toContain('claude mcp add');
229
+
230
+ // Executable permission check
231
+ const stats = statSync(setupPath);
232
+ const isExecutable = (stats.mode & 0o111) !== 0;
233
+ expect(isExecutable).toBe(true);
234
+ });
235
+
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', () => {
267
+ scaffold(testConfig);
268
+ const facadesTest = readFileSync(
269
+ join(tempDir, 'atlas', 'src', '__tests__', 'facades.test.ts'),
270
+ 'utf-8',
271
+ );
272
+
273
+ expect(facadesTest).toContain('atlas_data_pipelines');
274
+ expect(facadesTest).toContain('atlas_data_quality');
275
+ expect(facadesTest).toContain('atlas_etl');
276
+ 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
281
+ expect(facadesTest).toContain('activate');
282
+ expect(facadesTest).toContain('inject_claude_md');
283
+ 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');
297
+ });
298
+
299
+ it('should fail if directory already exists', () => {
300
+ scaffold(testConfig);
301
+ const result = scaffold(testConfig); // second time
302
+
303
+ expect(result.success).toBe(false);
304
+ expect(result.summary).toContain('already exists');
305
+ });
306
+ });
307
+
308
+ describe('listAgents', () => {
309
+ it('should list scaffolded agents', () => {
310
+ scaffold(testConfig);
311
+
312
+ const agents = listAgents(tempDir);
313
+ expect(agents).toHaveLength(1);
314
+ expect(agents[0].id).toBe('atlas');
315
+ expect(agents[0].domains).toEqual(['data-pipelines', 'data-quality', 'etl']);
316
+ });
317
+
318
+ it('should return empty for non-existent directory', () => {
319
+ const agents = listAgents('/non/existent/path');
320
+ expect(agents).toEqual([]);
321
+ });
322
+ });
323
+ });
@@ -0,0 +1,150 @@
1
+ import { z } from 'zod';
2
+ import { scaffold, previewScaffold, listAgents } from '../scaffolder.js';
3
+ import { AgentConfigSchema } from '../types.js';
4
+ import { installKnowledge } from '../knowledge-installer.js';
5
+
6
+ interface OpDef {
7
+ name: string;
8
+ description: string;
9
+ schema?: z.ZodType;
10
+ handler: (params: Record<string, unknown>) => Promise<unknown>;
11
+ }
12
+
13
+ export const forgeOps: OpDef[] = [
14
+ {
15
+ name: 'preview',
16
+ description:
17
+ 'Preview what will be created for an agent BEFORE scaffolding. ' +
18
+ 'Show this to the user so they can confirm the structure looks right. ' +
19
+ 'Always call preview before create.',
20
+ schema: AgentConfigSchema,
21
+ handler: async (params) => {
22
+ const config = AgentConfigSchema.parse(params);
23
+ return previewScaffold(config);
24
+ },
25
+ },
26
+ {
27
+ name: 'create',
28
+ description:
29
+ 'Scaffold a complete MCP agent project with activation system. ' +
30
+ 'This creates the full directory structure, source files, config, and activation module. ' +
31
+ 'After creation: npm install && npm run build, then say "Hello, {Name}!" to activate.',
32
+ schema: AgentConfigSchema,
33
+ handler: async (params) => {
34
+ const config = AgentConfigSchema.parse(params);
35
+ return scaffold(config);
36
+ },
37
+ },
38
+ {
39
+ name: 'list_agents',
40
+ description:
41
+ 'List all agents found in a directory. ' +
42
+ 'Useful to see what agents already exist before creating a new one.',
43
+ schema: z.object({
44
+ directory: z.string().describe('Parent directory to scan for agent projects'),
45
+ }),
46
+ handler: async (params) => {
47
+ const agents = listAgents(params.directory as string);
48
+ return {
49
+ agents,
50
+ count: agents.length,
51
+ note: agents.length === 0 ? 'No agents found. Use create to scaffold one.' : undefined,
52
+ };
53
+ },
54
+ },
55
+ {
56
+ name: 'guide',
57
+ description:
58
+ 'Get a structured guide for the AI agent to follow when helping a user create an agent. ' +
59
+ 'This provides the recommended conversation flow and questions to ask.',
60
+ handler: async () => {
61
+ return {
62
+ title: 'Agent Creation Guide',
63
+ instructions:
64
+ 'Follow these steps to help the user create a new MCP agent. Ask questions conversationally — do not dump all questions at once.',
65
+ steps: [
66
+ {
67
+ step: 1,
68
+ action: 'Understand the need',
69
+ ask: 'What role should this agent fill? Who will use it? What kind of guidance should it provide?',
70
+ examples: [
71
+ 'backend architecture advisor',
72
+ 'QA testing patterns',
73
+ 'data engineering best practices',
74
+ ],
75
+ },
76
+ {
77
+ step: 2,
78
+ action: 'Choose name and identity',
79
+ ask: 'What should we name this agent? A good name is memorable and reflects its domain.',
80
+ tips: [
81
+ 'Single word works best (Gaudi, Atlas, Nova)',
82
+ 'The name becomes the persona — it will introduce itself with this name',
83
+ ],
84
+ },
85
+ {
86
+ step: 3,
87
+ action: 'Select domains',
88
+ ask: 'What knowledge domains should this agent cover? Domain names should be kebab-case (e.g., api-design, database, security).',
89
+ tips: [
90
+ '3-6 domains is ideal',
91
+ 'Each domain becomes a facade (MCP tool) and an intelligence data file',
92
+ 'User can always add more later',
93
+ ],
94
+ },
95
+ {
96
+ step: 4,
97
+ action: 'Define principles',
98
+ ask: 'What are 3-5 core principles this agent should follow? These shape its personality and advice style.',
99
+ examples: [
100
+ 'Security first — always validate inputs',
101
+ 'Simplicity over cleverness',
102
+ 'Test everything that can break',
103
+ ],
104
+ },
105
+ {
106
+ step: 5,
107
+ action: 'Preview and confirm',
108
+ ask: 'Call preview with the collected config. Show the user what will be created. Ask for confirmation.',
109
+ },
110
+ {
111
+ step: 6,
112
+ action: 'Create the agent',
113
+ ask: 'Call create with the confirmed config. Then guide the user through npm install and npm run build. The MCP server is automatically registered in ~/.claude.json.',
114
+ },
115
+ {
116
+ step: 7,
117
+ action: 'Next steps',
118
+ suggest: [
119
+ 'Restart Claude Code so the new MCP server is picked up',
120
+ 'Say "Hello, {AgentName}!" to activate the persona in any session',
121
+ 'The agent will check setup status and offer to inject its CLAUDE.md sections',
122
+ 'Add initial knowledge by creating entries in the intelligence data JSON files',
123
+ 'Or use the agent and capture knowledge via the capture ops as you work',
124
+ ],
125
+ },
126
+ ],
127
+ };
128
+ },
129
+ },
130
+ {
131
+ name: 'install_knowledge',
132
+ description:
133
+ 'Install knowledge packs (intelligence bundles) into an existing MCP agent. ' +
134
+ 'Copies bundle JSON files, generates domain facades for new domains, ' +
135
+ 'patches index.ts and claude-md-content.ts, then rebuilds.',
136
+ schema: z.object({
137
+ agentPath: z.string().describe('Absolute path to the target agent project'),
138
+ bundlePath: z.string().describe('Path to bundles directory or single .json file'),
139
+ generateFacades: z
140
+ .boolean()
141
+ .optional()
142
+ .default(true)
143
+ .describe('Generate domain facades for new domains (default true)'),
144
+ }),
145
+ handler: async (params) =>
146
+ installKnowledge(
147
+ params as { agentPath: string; bundlePath: string; generateFacades?: boolean },
148
+ ),
149
+ },
150
+ ];
package/src/index.ts ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { z } from 'zod';
6
+ import { forgeOps } from './facades/forge.facade.js';
7
+
8
+ async function main(): Promise<void> {
9
+ const server = new McpServer({
10
+ name: 'soleri',
11
+ version: '3.0.0',
12
+ });
13
+
14
+ // Register the forge tool with op dispatch
15
+ const opNames = forgeOps.map((o) => o.name);
16
+
17
+ server.tool(
18
+ 'forge',
19
+ `Soleri Forge — scaffold AI agents that learn, remember, and grow.\n\nOperations: ${opNames.join(', ')}\n\nStart by calling 'guide' to get the recommended conversation flow for helping a user create an agent.`,
20
+ {
21
+ op: z.string().describe(`Operation: ${opNames.join(' | ')}`),
22
+ params: z.record(z.unknown()).optional().default({}).describe('Operation parameters'),
23
+ },
24
+ async ({ op, params }) => {
25
+ const opDef = forgeOps.find((o) => o.name === op);
26
+ if (!opDef) {
27
+ return {
28
+ content: [
29
+ {
30
+ type: 'text' as const,
31
+ text: JSON.stringify(
32
+ {
33
+ success: false,
34
+ error: `Unknown operation "${op}". Available: ${opNames.join(', ')}`,
35
+ },
36
+ null,
37
+ 2,
38
+ ),
39
+ },
40
+ ],
41
+ };
42
+ }
43
+
44
+ try {
45
+ let validatedParams = params;
46
+ if (opDef.schema) {
47
+ const result = opDef.schema.safeParse(params);
48
+ if (!result.success) {
49
+ return {
50
+ content: [
51
+ {
52
+ type: 'text' as const,
53
+ text: JSON.stringify({ success: false, error: result.error.message }, null, 2),
54
+ },
55
+ ],
56
+ };
57
+ }
58
+ validatedParams = result.data as Record<string, unknown>;
59
+ }
60
+
61
+ const data = await opDef.handler(validatedParams);
62
+ return {
63
+ content: [
64
+ {
65
+ type: 'text' as const,
66
+ text: JSON.stringify({ success: true, data }, null, 2),
67
+ },
68
+ ],
69
+ };
70
+ } catch (err) {
71
+ return {
72
+ content: [
73
+ {
74
+ type: 'text' as const,
75
+ text: JSON.stringify(
76
+ {
77
+ success: false,
78
+ error: err instanceof Error ? err.message : String(err),
79
+ },
80
+ null,
81
+ 2,
82
+ ),
83
+ },
84
+ ],
85
+ };
86
+ }
87
+ },
88
+ );
89
+
90
+ console.error('[soleri] Soleri Forge — AI agent scaffolding tool');
91
+ console.error(`[soleri] ${forgeOps.length} operations available`);
92
+
93
+ const transport = new StdioServerTransport();
94
+ await server.connect(transport);
95
+ console.error('[soleri] Connected via stdio');
96
+ }
97
+
98
+ main().catch((err) => {
99
+ console.error('[soleri] Fatal:', err);
100
+ process.exit(1);
101
+ });