@soleri/forge 4.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/index.js +0 -0
- package/dist/knowledge-installer.d.ts +0 -17
- 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/types.d.ts +11 -0
- package/package.json +8 -1
- package/src/__tests__/knowledge-installer.test.ts +3 -7
- package/src/domain-manager.ts +168 -0
- package/src/facades/forge.facade.ts +18 -0
- package/src/knowledge-installer.ts +2 -117
- package/src/lib.ts +19 -0
- package/src/patching.ts +123 -0
- 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/dist/types.d.ts
CHANGED
|
@@ -72,6 +72,17 @@ export interface ScaffoldPreview {
|
|
|
72
72
|
role: string;
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
+
/** Result of adding a domain to an existing agent */
|
|
76
|
+
export interface AddDomainResult {
|
|
77
|
+
success: boolean;
|
|
78
|
+
agentPath: string;
|
|
79
|
+
domain: string;
|
|
80
|
+
agentId: string;
|
|
81
|
+
facadeGenerated: boolean;
|
|
82
|
+
buildOutput: string;
|
|
83
|
+
warnings: string[];
|
|
84
|
+
summary: string;
|
|
85
|
+
}
|
|
75
86
|
/** Result of installing knowledge packs into an existing agent */
|
|
76
87
|
export interface InstallKnowledgeResult {
|
|
77
88
|
success: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soleri/forge",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Scaffold AI agents that learn, remember, and grow with you.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -26,6 +26,13 @@
|
|
|
26
26
|
},
|
|
27
27
|
"type": "module",
|
|
28
28
|
"main": "dist/index.js",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./dist/index.js",
|
|
31
|
+
"./lib": {
|
|
32
|
+
"types": "./dist/lib.d.ts",
|
|
33
|
+
"import": "./dist/lib.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
29
36
|
"publishConfig": {
|
|
30
37
|
"access": "public"
|
|
31
38
|
},
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
patchIndexTs,
|
|
8
|
-
patchClaudeMdContent,
|
|
9
|
-
generateVaultOnlyDomainFacade,
|
|
10
|
-
} from '../knowledge-installer.js';
|
|
5
|
+
import { installKnowledge, generateVaultOnlyDomainFacade } from '../knowledge-installer.js';
|
|
6
|
+
import { patchIndexTs, patchClaudeMdContent } from '../patching.js';
|
|
11
7
|
|
|
12
8
|
describe('Knowledge Installer', () => {
|
|
13
9
|
let tempDir: string;
|
|
@@ -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 ----------
|
|
@@ -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
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -51,6 +51,18 @@ export interface ScaffoldPreview {
|
|
|
51
51
|
persona: { name: string; role: string };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Result of adding a domain to an existing agent */
|
|
55
|
+
export interface AddDomainResult {
|
|
56
|
+
success: boolean;
|
|
57
|
+
agentPath: string;
|
|
58
|
+
domain: string;
|
|
59
|
+
agentId: string;
|
|
60
|
+
facadeGenerated: boolean;
|
|
61
|
+
buildOutput: string;
|
|
62
|
+
warnings: string[];
|
|
63
|
+
summary: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
/** Result of installing knowledge packs into an existing agent */
|
|
55
67
|
export interface InstallKnowledgeResult {
|
|
56
68
|
success: boolean;
|