@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.
- package/CHANGELOG.md +16 -1
- package/README.md +16 -1
- package/dist/domain-manager.d.ts +19 -0
- package/dist/domain-manager.js +139 -0
- package/dist/domain-manager.js.map +1 -0
- package/dist/facades/forge.facade.js +16 -0
- package/dist/facades/forge.facade.js.map +1 -1
- package/dist/knowledge-installer.d.ts +2 -18
- package/dist/knowledge-installer.js +2 -96
- package/dist/knowledge-installer.js.map +1 -1
- package/dist/lib.d.ts +12 -0
- package/dist/lib.js +12 -0
- package/dist/lib.js.map +1 -0
- package/dist/patching.d.ts +17 -0
- package/dist/patching.js +103 -0
- package/dist/patching.js.map +1 -0
- package/dist/scaffolder.js +1 -91
- package/dist/scaffolder.js.map +1 -1
- package/dist/templates/activate.js +1 -2
- package/dist/templates/activate.js.map +1 -1
- package/dist/templates/core-facade.js +1 -6
- package/dist/templates/core-facade.js.map +1 -1
- package/dist/templates/domain-facade.js +1 -3
- package/dist/templates/domain-facade.js.map +1 -1
- package/dist/templates/entry-point.js +2 -7
- package/dist/templates/entry-point.js.map +1 -1
- package/dist/templates/llm-client.js +3 -4
- package/dist/templates/llm-client.js.map +1 -1
- package/dist/templates/package-json.js +1 -2
- package/dist/templates/package-json.js.map +1 -1
- package/dist/templates/test-facades.js +2 -5
- package/dist/templates/test-facades.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/package.json +8 -1
- package/src/__tests__/knowledge-installer.test.ts +3 -7
- package/src/__tests__/scaffolder.test.ts +35 -47
- package/src/domain-manager.ts +168 -0
- package/src/facades/forge.facade.ts +18 -0
- package/src/knowledge-installer.ts +3 -118
- package/src/lib.ts +19 -0
- package/src/patching.ts +123 -0
- package/src/scaffolder.ts +1 -97
- package/src/templates/activate.ts +1 -2
- package/src/templates/core-facade.ts +1 -6
- package/src/templates/domain-facade.ts +1 -3
- package/src/templates/entry-point.ts +2 -7
- package/src/templates/llm-client.ts +3 -4
- package/src/templates/package-json.ts +1 -2
- package/src/templates/test-facades.ts +2 -5
- package/src/types.ts +12 -0
- package/dist/templates/brain.d.ts +0 -6
- package/dist/templates/brain.js +0 -478
- package/dist/templates/brain.js.map +0 -1
- package/dist/templates/facade-factory.d.ts +0 -1
- package/dist/templates/facade-factory.js +0 -63
- package/dist/templates/facade-factory.js.map +0 -1
- package/dist/templates/facade-types.d.ts +0 -1
- package/dist/templates/facade-types.js +0 -46
- package/dist/templates/facade-types.js.map +0 -1
- package/dist/templates/intelligence-loader.d.ts +0 -1
- package/dist/templates/intelligence-loader.js +0 -43
- package/dist/templates/intelligence-loader.js.map +0 -1
- package/dist/templates/intelligence-types.d.ts +0 -1
- package/dist/templates/intelligence-types.js +0 -24
- package/dist/templates/intelligence-types.js.map +0 -1
- package/dist/templates/llm-key-pool.d.ts +0 -7
- package/dist/templates/llm-key-pool.js +0 -211
- package/dist/templates/llm-key-pool.js.map +0 -1
- package/dist/templates/llm-types.d.ts +0 -5
- package/dist/templates/llm-types.js +0 -161
- package/dist/templates/llm-types.js.map +0 -1
- package/dist/templates/llm-utils.d.ts +0 -5
- package/dist/templates/llm-utils.js +0 -260
- package/dist/templates/llm-utils.js.map +0 -1
- package/dist/templates/planner.d.ts +0 -5
- package/dist/templates/planner.js +0 -150
- package/dist/templates/planner.js.map +0 -1
- package/dist/templates/test-brain.d.ts +0 -6
- package/dist/templates/test-brain.js +0 -474
- package/dist/templates/test-brain.js.map +0 -1
- package/dist/templates/test-llm.d.ts +0 -7
- package/dist/templates/test-llm.js +0 -574
- package/dist/templates/test-llm.js.map +0 -1
- package/dist/templates/test-loader.d.ts +0 -5
- package/dist/templates/test-loader.js +0 -146
- package/dist/templates/test-loader.js.map +0 -1
- package/dist/templates/test-planner.d.ts +0 -5
- package/dist/templates/test-planner.js +0 -271
- package/dist/templates/test-planner.js.map +0 -1
- package/dist/templates/test-vault.d.ts +0 -5
- package/dist/templates/test-vault.js +0 -380
- package/dist/templates/test-vault.js.map +0 -1
- package/dist/templates/vault.d.ts +0 -5
- package/dist/templates/vault.js +0 -263
- package/dist/templates/vault.js.map +0 -1
- package/src/templates/brain.ts +0 -478
- package/src/templates/facade-factory.ts +0 -62
- package/src/templates/facade-types.ts +0 -45
- package/src/templates/intelligence-loader.ts +0 -42
- package/src/templates/intelligence-types.ts +0 -23
- package/src/templates/llm-key-pool.ts +0 -212
- package/src/templates/llm-types.ts +0 -160
- package/src/templates/llm-utils.ts +0 -259
- package/src/templates/planner.ts +0 -150
- package/src/templates/test-brain.ts +0 -474
- package/src/templates/test-llm.ts +0 -575
- package/src/templates/test-loader.ts +0 -146
- package/src/templates/test-planner.ts +0 -271
- package/src/templates/test-vault.ts +0 -380
- 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
|
|
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__/
|
|
55
|
-
|
|
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['
|
|
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
|
-
|
|
124
|
-
expect(files).toContain('
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
276
|
-
expect(files).toContain('
|
|
277
|
-
expect(files).toContain('
|
|
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
|
-
|
|
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
|
-
//
|
|
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';
|
package/src/patching.ts
ADDED
|
@@ -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
|
+
}
|