@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
@@ -44,15 +44,16 @@ describe('Scaffolder', () => {
44
44
  expect(preview.domains).toEqual(['data-pipelines', 'data-quality', 'etl']);
45
45
  expect(preview.files.length).toBeGreaterThan(15);
46
46
 
47
- // Should include README, setup script, and brain
47
+ // Should include README, setup script, and LLM client (static modules now in @soleri/core)
48
48
  const paths = preview.files.map((f) => f.path);
49
49
  expect(paths).toContain('README.md');
50
50
  expect(paths).toContain('scripts/setup.sh');
51
- expect(paths).toContain('src/brain/brain.ts');
52
- expect(paths).toContain('src/llm/types.ts');
53
51
  expect(paths).toContain('src/llm/llm-client.ts');
54
- expect(paths).toContain('src/__tests__/llm.test.ts');
55
- expect(paths).toContain('src/__tests__/brain.test.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');
56
57
 
57
58
  // Should have domain facades + core facade
58
59
  expect(preview.facades).toHaveLength(4); // 3 domains + core
@@ -78,13 +79,14 @@ describe('Scaffolder', () => {
78
79
  const agentDir = join(tempDir, 'atlas');
79
80
 
80
81
  expect(existsSync(join(agentDir, 'src', 'facades'))).toBe(true);
81
- expect(existsSync(join(agentDir, 'src', 'vault'))).toBe(true);
82
82
  expect(existsSync(join(agentDir, 'src', 'intelligence', 'data'))).toBe(true);
83
83
  expect(existsSync(join(agentDir, 'src', 'identity'))).toBe(true);
84
84
  expect(existsSync(join(agentDir, 'src', 'activation'))).toBe(true);
85
- expect(existsSync(join(agentDir, 'src', 'planning'))).toBe(true);
86
- expect(existsSync(join(agentDir, 'src', 'brain'))).toBe(true);
87
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);
88
90
  });
89
91
 
90
92
  it('should create valid package.json', () => {
@@ -94,9 +96,11 @@ describe('Scaffolder', () => {
94
96
  expect(pkg.name).toBe('atlas-mcp');
95
97
  expect(pkg.type).toBe('module');
96
98
  expect(pkg.dependencies['@modelcontextprotocol/sdk']).toBeDefined();
97
- expect(pkg.dependencies['better-sqlite3']).toBeDefined();
99
+ expect(pkg.dependencies['@soleri/core']).toBeDefined();
98
100
  expect(pkg.dependencies['zod']).toBeDefined();
99
101
  expect(pkg.dependencies['@anthropic-ai/sdk']).toBeDefined();
102
+ // better-sqlite3 is now transitive via @soleri/core
103
+ expect(pkg.dependencies['better-sqlite3']).toBeUndefined();
100
104
  });
101
105
 
102
106
  it('should create persona with correct config', () => {
@@ -120,8 +124,9 @@ describe('Scaffolder', () => {
120
124
  expect(files).toContain('data-quality.facade.ts');
121
125
  expect(files).toContain('etl.facade.ts');
122
126
  expect(files).toContain('core.facade.ts');
123
- expect(files).toContain('facade-factory.ts');
124
- expect(files).toContain('types.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');
125
130
  });
126
131
 
127
132
  it('should create empty intelligence data files', () => {
@@ -139,7 +144,7 @@ describe('Scaffolder', () => {
139
144
  expect(bundle.entries).toEqual([]);
140
145
  });
141
146
 
142
- it('should create entry point importing all facades', () => {
147
+ it('should create entry point importing from @soleri/core', () => {
143
148
  scaffold(testConfig);
144
149
  const entry = readFileSync(join(tempDir, 'atlas', 'src', 'index.ts'), 'utf-8');
145
150
 
@@ -153,6 +158,9 @@ describe('Scaffolder', () => {
153
158
  expect(entry).toContain('KeyPool');
154
159
  expect(entry).toContain('loadKeyPoolConfig');
155
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')");
156
164
  });
157
165
 
158
166
  it('should create .mcp.json for client config', () => {
@@ -225,56 +233,34 @@ describe('Scaffolder', () => {
225
233
  expect(isExecutable).toBe(true);
226
234
  });
227
235
 
228
- it('should create planner file', () => {
229
- scaffold(testConfig);
230
- const plannerPath = join(tempDir, 'atlas', 'src', 'planning', 'planner.ts');
231
- expect(existsSync(plannerPath)).toBe(true);
232
- const content = readFileSync(plannerPath, 'utf-8');
233
- expect(content).toContain('class Planner');
234
- expect(content).toContain('getExecuting');
235
- });
236
-
237
- it('should create LLM files', () => {
236
+ it('should create LLM client file importing from @soleri/core', () => {
238
237
  scaffold(testConfig);
239
238
  const llmDir = join(tempDir, 'atlas', 'src', 'llm');
240
- expect(existsSync(join(llmDir, 'types.ts'))).toBe(true);
241
- expect(existsSync(join(llmDir, 'utils.ts'))).toBe(true);
242
- expect(existsSync(join(llmDir, 'key-pool.ts'))).toBe(true);
239
+ // Only llm-client.ts should exist (types, utils, key-pool are in @soleri/core)
243
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
244
 
245
245
  const client = readFileSync(join(llmDir, 'llm-client.ts'), 'utf-8');
246
246
  expect(client).toContain('class LLMClient');
247
247
  expect(client).toContain('class ModelRouter');
248
248
  expect(client).toContain('.atlas');
249
-
250
- const keyPool = readFileSync(join(llmDir, 'key-pool.ts'), 'utf-8');
251
- expect(keyPool).toContain('class KeyPool');
252
- expect(keyPool).toContain('.atlas');
253
- });
254
-
255
- it('should create brain file with intelligence layer', () => {
256
- scaffold(testConfig);
257
- const brainPath = join(tempDir, 'atlas', 'src', 'brain', 'brain.ts');
258
- expect(existsSync(brainPath)).toBe(true);
259
- const content = readFileSync(brainPath, 'utf-8');
260
- expect(content).toContain('class Brain');
261
- expect(content).toContain('intelligentSearch');
262
- expect(content).toContain('enrichAndCapture');
263
- expect(content).toContain('recordFeedback');
264
- expect(content).toContain('rebuildVocabulary');
249
+ expect(client).toContain("from '@soleri/core'");
265
250
  });
266
251
 
267
- it('should create test files', () => {
252
+ it('should only create facades test file (static tests in @soleri/core)', () => {
268
253
  scaffold(testConfig);
269
254
  const testsDir = join(tempDir, 'atlas', 'src', '__tests__');
270
255
  const files = readdirSync(testsDir);
271
256
 
272
- expect(files).toContain('vault.test.ts');
273
- expect(files).toContain('loader.test.ts');
274
257
  expect(files).toContain('facades.test.ts');
275
- expect(files).toContain('planner.test.ts');
276
- expect(files).toContain('brain.test.ts');
277
- expect(files).toContain('llm.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');
278
264
  });
279
265
 
280
266
  it('should generate facade tests referencing all domains', () => {
@@ -289,6 +275,8 @@ describe('Scaffolder', () => {
289
275
  expect(facadesTest).toContain('atlas_etl');
290
276
  expect(facadesTest).toContain('atlas_core');
291
277
  expect(facadesTest).toContain('createDataPipelinesFacade');
278
+ // Should import shared modules from @soleri/core
279
+ expect(facadesTest).toContain("from '@soleri/core'");
292
280
  // Activation ops should be tested
293
281
  expect(facadesTest).toContain('activate');
294
282
  expect(facadesTest).toContain('inject_claude_md');
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Domain manager — add new knowledge domains to existing agents.
3
+ */
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { generateDomainFacade } from './templates/domain-facade.js';
8
+ import { generateVaultOnlyDomainFacade } from './knowledge-installer.js';
9
+ import { patchIndexTs, patchClaudeMdContent } from './patching.js';
10
+ import type { AddDomainResult } from './types.js';
11
+
12
+ interface AddDomainParams {
13
+ agentPath: string;
14
+ domain: string;
15
+ noBuild?: boolean;
16
+ }
17
+
18
+ /**
19
+ * Add a new knowledge domain to an existing agent.
20
+ *
21
+ * Steps:
22
+ * 1. Validate agent path and domain name
23
+ * 2. Create empty intelligence bundle
24
+ * 3. Generate domain facade
25
+ * 4. Patch src/index.ts with import + registration
26
+ * 5. Patch src/activation/claude-md-content.ts with facade table rows
27
+ * 6. Rebuild (unless noBuild)
28
+ */
29
+ export async function addDomain(params: AddDomainParams): Promise<AddDomainResult> {
30
+ const { agentPath, domain, noBuild = false } = params;
31
+ const warnings: string[] = [];
32
+
33
+ // ── Validate agent ──
34
+
35
+ const pkgPath = join(agentPath, 'package.json');
36
+ if (!existsSync(pkgPath)) {
37
+ return fail(agentPath, domain, 'No package.json found — is this an agent project?');
38
+ }
39
+
40
+ let pkg: Record<string, unknown>;
41
+ try {
42
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
43
+ } catch {
44
+ return fail(agentPath, domain, 'Failed to parse package.json — is it valid JSON?');
45
+ }
46
+ const pkgName: string = (pkg.name as string) ?? '';
47
+ if (!pkgName.endsWith('-mcp')) {
48
+ return fail(agentPath, domain, `package.json name "${pkgName}" does not end with -mcp`);
49
+ }
50
+
51
+ const agentId = pkgName.replace(/-mcp$/, '');
52
+
53
+ // ── Validate domain name ──
54
+
55
+ if (!/^[a-z][a-z0-9-]*$/.test(domain)) {
56
+ return fail(agentPath, domain, `Invalid domain name "${domain}" — must be kebab-case`);
57
+ }
58
+
59
+ // ── Check if domain already exists ──
60
+
61
+ const dataDir = join(agentPath, 'src', 'intelligence', 'data');
62
+ if (!existsSync(dataDir)) {
63
+ return fail(agentPath, domain, 'src/intelligence/data/ directory not found');
64
+ }
65
+
66
+ const bundlePath = join(dataDir, `${domain}.json`);
67
+ if (existsSync(bundlePath)) {
68
+ return fail(agentPath, domain, `Domain "${domain}" already exists`);
69
+ }
70
+
71
+ const hasBrain = existsSync(join(agentPath, 'src', 'brain'));
72
+
73
+ // ── Step 1: Create empty bundle ──
74
+
75
+ const emptyBundle = JSON.stringify({ domain, version: '1.0.0', entries: [] }, null, 2);
76
+ writeFileSync(bundlePath, emptyBundle, 'utf-8');
77
+
78
+ // Also write to dist if it exists
79
+ const distDataDir = join(agentPath, 'dist', 'intelligence', 'data');
80
+ if (existsSync(join(agentPath, 'dist'))) {
81
+ mkdirSync(distDataDir, { recursive: true });
82
+ writeFileSync(join(distDataDir, `${domain}.json`), emptyBundle, 'utf-8');
83
+ }
84
+
85
+ // ── Step 2: Generate facade ──
86
+
87
+ const facadesDir = join(agentPath, 'src', 'facades');
88
+ const facadePath = join(facadesDir, `${domain}.facade.ts`);
89
+
90
+ if (existsSync(facadePath)) {
91
+ warnings.push(`Facade ${domain}.facade.ts already exists — skipped`);
92
+ } else {
93
+ const facadeCode = hasBrain
94
+ ? generateDomainFacade(agentId, domain)
95
+ : generateVaultOnlyDomainFacade(agentId, domain);
96
+ writeFileSync(facadePath, facadeCode, 'utf-8');
97
+ }
98
+
99
+ // ── Step 3: Patch src/index.ts ──
100
+
101
+ const indexPath = join(agentPath, 'src', 'index.ts');
102
+ if (existsSync(indexPath)) {
103
+ const patched = patchIndexTs(readFileSync(indexPath, 'utf-8'), [domain], hasBrain);
104
+ if (patched !== null) {
105
+ writeFileSync(indexPath, patched, 'utf-8');
106
+ } else {
107
+ warnings.push('Could not patch src/index.ts — anchor patterns not found');
108
+ }
109
+ }
110
+
111
+ // ── Step 4: Patch claude-md-content.ts ──
112
+
113
+ const claudeMdPath = join(agentPath, 'src', 'activation', 'claude-md-content.ts');
114
+ if (existsSync(claudeMdPath)) {
115
+ const patched = patchClaudeMdContent(readFileSync(claudeMdPath, 'utf-8'), agentId, [domain]);
116
+ if (patched !== null) {
117
+ writeFileSync(claudeMdPath, patched, 'utf-8');
118
+ } else {
119
+ warnings.push('Could not patch claude-md-content.ts — anchor not found');
120
+ }
121
+ }
122
+
123
+ // ── Step 5: Build ──
124
+
125
+ let buildOutput = '';
126
+ if (!noBuild) {
127
+ try {
128
+ buildOutput = execFileSync('npm', ['run', 'build'], {
129
+ cwd: agentPath,
130
+ encoding: 'utf-8',
131
+ timeout: 60_000,
132
+ stdio: ['pipe', 'pipe', 'pipe'],
133
+ });
134
+ } catch (err) {
135
+ const stderr = (err as { stderr?: string }).stderr ?? '';
136
+ buildOutput = `Build failed: ${stderr}`;
137
+ warnings.push('Build failed — check buildOutput for details');
138
+ }
139
+ }
140
+
141
+ const hasPatchFailure = warnings.some(
142
+ (w) => w.includes('Could not patch') || w.includes('Build failed'),
143
+ );
144
+
145
+ return {
146
+ success: !hasPatchFailure,
147
+ agentPath,
148
+ domain,
149
+ agentId,
150
+ facadeGenerated: !existsSync(facadePath) || !warnings.some((w) => w.includes('already exists')),
151
+ buildOutput,
152
+ warnings,
153
+ summary: `Added domain "${domain}" to ${agentId}${warnings.length > 0 ? ` (${warnings.length} warning(s))` : ''}`,
154
+ };
155
+ }
156
+
157
+ function fail(agentPath: string, domain: string, message: string): AddDomainResult {
158
+ return {
159
+ success: false,
160
+ agentPath,
161
+ domain,
162
+ agentId: '',
163
+ facadeGenerated: false,
164
+ buildOutput: '',
165
+ warnings: [message],
166
+ summary: message,
167
+ };
168
+ }
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { scaffold, previewScaffold, listAgents } from '../scaffolder.js';
3
3
  import { AgentConfigSchema } from '../types.js';
4
4
  import { installKnowledge } from '../knowledge-installer.js';
5
+ import { addDomain } from '../domain-manager.js';
5
6
 
6
7
  interface OpDef {
7
8
  name: string;
@@ -147,4 +148,21 @@ export const forgeOps: OpDef[] = [
147
148
  params as { agentPath: string; bundlePath: string; generateFacades?: boolean },
148
149
  ),
149
150
  },
151
+ {
152
+ name: 'add_domain',
153
+ description:
154
+ 'Add a new knowledge domain to an existing MCP agent. ' +
155
+ 'Creates an empty bundle, generates a domain facade, patches index.ts and claude-md-content.ts, then rebuilds.',
156
+ schema: z.object({
157
+ agentPath: z.string().describe('Absolute path to the target agent project'),
158
+ domain: z.string().describe('Domain name in kebab-case (e.g., "security", "api-design")'),
159
+ noBuild: z
160
+ .boolean()
161
+ .optional()
162
+ .default(false)
163
+ .describe('Skip the build step after adding the domain'),
164
+ }),
165
+ handler: async (params) =>
166
+ addDomain(params as { agentPath: string; domain: string; noBuild?: boolean }),
167
+ },
150
168
  ];
@@ -9,6 +9,7 @@ import {
9
9
  import { join, basename } from 'node:path';
10
10
  import { execFileSync } from 'node:child_process';
11
11
  import { generateDomainFacade, pascalCase, capitalize } from './templates/domain-facade.js';
12
+ import { patchIndexTs, patchClaudeMdContent } from './patching.js';
12
13
  import type { InstallKnowledgeResult } from './types.js';
13
14
 
14
15
  // ---------- Bundle validation types ----------
@@ -149,7 +150,7 @@ export function create${pascalCase(domain)}Facade(vault: Vault): FacadeConfig {
149
150
 
150
151
  // ---------- Main installer ----------
151
152
 
152
- export interface InstallKnowledgeParams {
153
+ interface InstallKnowledgeParams {
153
154
  agentPath: string;
154
155
  bundlePath: string;
155
156
  generateFacades?: boolean;
@@ -413,120 +414,4 @@ function validateBundle(bundle: Bundle, file: string): string[] {
413
414
  return issues;
414
415
  }
415
416
 
416
- // ---------- Source file patching ----------
417
-
418
- /**
419
- * Patch the agent's src/index.ts to add imports and facade registrations
420
- * for new domains.
421
- *
422
- * Anchor patterns:
423
- * - Import: insert before `import { createCoreFacade }`
424
- * - Facade array: insert before `createCoreFacade(`
425
- */
426
- export function patchIndexTs(
427
- source: string,
428
- newDomains: string[],
429
- hasBrain: boolean,
430
- ): string | null {
431
- // Filter out domains whose imports already exist (idempotent)
432
- const domainsToImport = newDomains.filter((d) => {
433
- const fn = `create${pascalCase(d)}Facade`;
434
- return !source.includes(`import { ${fn} }`);
435
- });
436
-
437
- // Filter out domains whose facade calls already exist
438
- const domainsToRegister = newDomains.filter((d) => {
439
- const fn = `create${pascalCase(d)}Facade(`;
440
- return !source.includes(fn);
441
- });
442
-
443
- // Nothing to patch
444
- if (domainsToImport.length === 0 && domainsToRegister.length === 0) {
445
- return source;
446
- }
447
-
448
- let patched = source;
449
-
450
- // ── Insert imports ──
451
- if (domainsToImport.length > 0) {
452
- const importAnchor = /^(import \{ createCoreFacade \}.*$)/m;
453
- if (!importAnchor.test(patched)) return null;
454
-
455
- const newImports = domainsToImport
456
- .map((d) => {
457
- const fn = `create${pascalCase(d)}Facade`;
458
- return `import { ${fn} } from './facades/${d}.facade.js';`;
459
- })
460
- .join('\n');
461
-
462
- patched = patched.replace(importAnchor, `${newImports}\n$1`);
463
- }
464
-
465
- // ── Insert facade creations ──
466
- if (domainsToRegister.length > 0) {
467
- const facadeAnchor = /^(\s+createCoreFacade\()/m;
468
- if (!facadeAnchor.test(patched)) return null;
469
-
470
- const newCreations = domainsToRegister
471
- .map((d) => {
472
- const fn = `create${pascalCase(d)}Facade`;
473
- const args = hasBrain ? 'vault, brain' : 'vault';
474
- return ` ${fn}(${args}),`;
475
- })
476
- .join('\n');
477
-
478
- patched = patched.replace(facadeAnchor, `${newCreations}\n$1`);
479
- }
480
-
481
- return patched;
482
- }
483
-
484
- /**
485
- * Patch the agent's src/activation/claude-md-content.ts to add
486
- * facade table rows for new domains.
487
- *
488
- * Primary anchor: line containing `| Memory search |` (newer agents)
489
- * Fallback anchor: line containing `## Intent Detection` (older agents without memory/brain rows)
490
- */
491
- export function patchClaudeMdContent(
492
- source: string,
493
- agentId: string,
494
- newDomains: string[],
495
- ): string | null {
496
- const facadeId = agentId.replace(/-/g, '_');
497
- const bt = '`';
498
-
499
- // Filter out domains whose rows already exist (idempotent)
500
- const domainsToAdd = newDomains.filter((d) => {
501
- const toolName = `${facadeId}_${d.replace(/-/g, '_')}`;
502
- return !source.includes(`${toolName}`);
503
- });
504
-
505
- if (domainsToAdd.length === 0) return source;
506
-
507
- // Try primary anchor first, then fallback for older agents
508
- let anchorIdx = source.indexOf("'| Memory search |");
509
- if (anchorIdx === -1) {
510
- // Older agents: insert before the empty line preceding ## Intent Detection
511
- anchorIdx = source.indexOf("'## Intent Detection'");
512
- if (anchorIdx === -1) return null;
513
- // Back up to include the preceding empty string line ('',)
514
- const emptyLineIdx = source.lastIndexOf("'',", anchorIdx);
515
- if (emptyLineIdx !== -1 && anchorIdx - emptyLineIdx < 20) {
516
- // Find the start of that line (the indentation)
517
- const lineStart = source.lastIndexOf('\n', emptyLineIdx);
518
- anchorIdx = lineStart === -1 ? emptyLineIdx : lineStart + 1;
519
- }
520
- }
521
-
522
- const newRows = domainsToAdd.flatMap((d) => {
523
- const toolName = `${facadeId}_${d.replace(/-/g, '_')}`;
524
- return [
525
- ` '| ${d} patterns | ${bt}${toolName}${bt} | ${bt}get_patterns${bt} |',`,
526
- ` '| Search ${d} | ${bt}${toolName}${bt} | ${bt}search${bt} |',`,
527
- ` '| Capture ${d} | ${bt}${toolName}${bt} | ${bt}capture${bt} |',`,
528
- ];
529
- });
530
-
531
- return source.slice(0, anchorIdx) + newRows.join('\n') + '\n' + source.slice(anchorIdx);
532
- }
417
+ // patching functions (patchIndexTs, patchClaudeMdContent) are in patching.ts
package/src/lib.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @soleri/forge — Public API
3
+ *
4
+ * Import this for programmatic access to forge functions.
5
+ * The main index.ts starts the MCP server — use this for library usage.
6
+ */
7
+ export { scaffold, previewScaffold, listAgents } from './scaffolder.js';
8
+ export { installKnowledge, generateVaultOnlyDomainFacade } from './knowledge-installer.js';
9
+ export { addDomain } from './domain-manager.js';
10
+ export { patchIndexTs, patchClaudeMdContent } from './patching.js';
11
+ export type {
12
+ AgentConfig,
13
+ ScaffoldResult,
14
+ ScaffoldPreview,
15
+ AgentInfo,
16
+ InstallKnowledgeResult,
17
+ AddDomainResult,
18
+ } from './types.js';
19
+ export { AgentConfigSchema } from './types.js';
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Source file patching utilities — shared between knowledge-installer and domain-manager.
3
+ *
4
+ * These functions modify an agent's src/index.ts and src/activation/claude-md-content.ts
5
+ * to register new domain facades and their CLAUDE.md table rows.
6
+ */
7
+ import { pascalCase } from './templates/domain-facade.js';
8
+
9
+ /**
10
+ * Patch the agent's src/index.ts to add imports and facade registrations
11
+ * for new domains.
12
+ *
13
+ * Anchor patterns:
14
+ * - Import: insert before `import { createCoreFacade }`
15
+ * - Facade array: insert before `createCoreFacade(`
16
+ */
17
+ export function patchIndexTs(
18
+ source: string,
19
+ newDomains: string[],
20
+ hasBrain: boolean,
21
+ ): string | null {
22
+ // Filter out domains whose imports already exist (idempotent)
23
+ const domainsToImport = newDomains.filter((d) => {
24
+ const fn = `create${pascalCase(d)}Facade`;
25
+ return !source.includes(`import { ${fn} }`);
26
+ });
27
+
28
+ // Filter out domains whose facade calls already exist
29
+ const domainsToRegister = newDomains.filter((d) => {
30
+ const fn = `create${pascalCase(d)}Facade(`;
31
+ return !source.includes(fn);
32
+ });
33
+
34
+ // Nothing to patch
35
+ if (domainsToImport.length === 0 && domainsToRegister.length === 0) {
36
+ return source;
37
+ }
38
+
39
+ let patched = source;
40
+
41
+ // ── Insert imports ──
42
+ if (domainsToImport.length > 0) {
43
+ const importAnchor = /^(import \{ createCoreFacade \}.*$)/m;
44
+ if (!importAnchor.test(patched)) return null;
45
+
46
+ const newImports = domainsToImport
47
+ .map((d) => {
48
+ const fn = `create${pascalCase(d)}Facade`;
49
+ return `import { ${fn} } from './facades/${d}.facade.js';`;
50
+ })
51
+ .join('\n');
52
+
53
+ patched = patched.replace(importAnchor, `${newImports}\n$1`);
54
+ }
55
+
56
+ // ── Insert facade creations ──
57
+ if (domainsToRegister.length > 0) {
58
+ const facadeAnchor = /^(\s+createCoreFacade\()/m;
59
+ if (!facadeAnchor.test(patched)) return null;
60
+
61
+ const newCreations = domainsToRegister
62
+ .map((d) => {
63
+ const fn = `create${pascalCase(d)}Facade`;
64
+ const args = hasBrain ? 'vault, brain' : 'vault';
65
+ return ` ${fn}(${args}),`;
66
+ })
67
+ .join('\n');
68
+
69
+ patched = patched.replace(facadeAnchor, `${newCreations}\n$1`);
70
+ }
71
+
72
+ return patched;
73
+ }
74
+
75
+ /**
76
+ * Patch the agent's src/activation/claude-md-content.ts to add
77
+ * facade table rows for new domains.
78
+ *
79
+ * Primary anchor: line containing `| Memory search |` (newer agents)
80
+ * Fallback anchor: line containing `## Intent Detection` (older agents without memory/brain rows)
81
+ */
82
+ export function patchClaudeMdContent(
83
+ source: string,
84
+ agentId: string,
85
+ newDomains: string[],
86
+ ): string | null {
87
+ const facadeId = agentId.replace(/-/g, '_');
88
+ const bt = '`';
89
+
90
+ // Filter out domains whose rows already exist (idempotent)
91
+ const domainsToAdd = newDomains.filter((d) => {
92
+ const toolName = `${facadeId}_${d.replace(/-/g, '_')}`;
93
+ return !source.includes(`${toolName}`);
94
+ });
95
+
96
+ if (domainsToAdd.length === 0) return source;
97
+
98
+ // Try primary anchor first, then fallback for older agents
99
+ let anchorIdx = source.indexOf("'| Memory search |");
100
+ if (anchorIdx === -1) {
101
+ // Older agents: insert before the empty line preceding ## Intent Detection
102
+ anchorIdx = source.indexOf("'## Intent Detection'");
103
+ if (anchorIdx === -1) return null;
104
+ // Back up to include the preceding empty string line ('',)
105
+ const emptyLineIdx = source.lastIndexOf("'',", anchorIdx);
106
+ if (emptyLineIdx !== -1 && anchorIdx - emptyLineIdx < 20) {
107
+ // Find the start of that line (the indentation)
108
+ const lineStart = source.lastIndexOf('\n', emptyLineIdx);
109
+ anchorIdx = lineStart === -1 ? emptyLineIdx : lineStart + 1;
110
+ }
111
+ }
112
+
113
+ const newRows = domainsToAdd.flatMap((d) => {
114
+ const toolName = `${facadeId}_${d.replace(/-/g, '_')}`;
115
+ return [
116
+ ` '| ${d} patterns | ${bt}${toolName}${bt} | ${bt}get_patterns${bt} |',`,
117
+ ` '| Search ${d} | ${bt}${toolName}${bt} | ${bt}search${bt} |',`,
118
+ ` '| Capture ${d} | ${bt}${toolName}${bt} | ${bt}capture${bt} |',`,
119
+ ];
120
+ });
121
+
122
+ return source.slice(0, anchorIdx) + newRows.join('\n') + '\n' + source.slice(anchorIdx);
123
+ }