@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.
Files changed (71) 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/index.js +0 -0
  9. package/dist/knowledge-installer.d.ts +0 -17
  10. package/dist/knowledge-installer.js +2 -96
  11. package/dist/knowledge-installer.js.map +1 -1
  12. package/dist/lib.d.ts +12 -0
  13. package/dist/lib.js +12 -0
  14. package/dist/lib.js.map +1 -0
  15. package/dist/patching.d.ts +17 -0
  16. package/dist/patching.js +103 -0
  17. package/dist/patching.js.map +1 -0
  18. package/dist/types.d.ts +11 -0
  19. package/package.json +8 -1
  20. package/src/__tests__/knowledge-installer.test.ts +3 -7
  21. package/src/domain-manager.ts +168 -0
  22. package/src/facades/forge.facade.ts +18 -0
  23. package/src/knowledge-installer.ts +2 -117
  24. package/src/lib.ts +19 -0
  25. package/src/patching.ts +123 -0
  26. package/src/types.ts +12 -0
  27. package/dist/templates/brain.d.ts +0 -6
  28. package/dist/templates/brain.js +0 -478
  29. package/dist/templates/brain.js.map +0 -1
  30. package/dist/templates/facade-factory.d.ts +0 -1
  31. package/dist/templates/facade-factory.js +0 -63
  32. package/dist/templates/facade-factory.js.map +0 -1
  33. package/dist/templates/facade-types.d.ts +0 -1
  34. package/dist/templates/facade-types.js +0 -46
  35. package/dist/templates/facade-types.js.map +0 -1
  36. package/dist/templates/intelligence-loader.d.ts +0 -1
  37. package/dist/templates/intelligence-loader.js +0 -43
  38. package/dist/templates/intelligence-loader.js.map +0 -1
  39. package/dist/templates/intelligence-types.d.ts +0 -1
  40. package/dist/templates/intelligence-types.js +0 -24
  41. package/dist/templates/intelligence-types.js.map +0 -1
  42. package/dist/templates/llm-key-pool.d.ts +0 -7
  43. package/dist/templates/llm-key-pool.js +0 -211
  44. package/dist/templates/llm-key-pool.js.map +0 -1
  45. package/dist/templates/llm-types.d.ts +0 -5
  46. package/dist/templates/llm-types.js +0 -161
  47. package/dist/templates/llm-types.js.map +0 -1
  48. package/dist/templates/llm-utils.d.ts +0 -5
  49. package/dist/templates/llm-utils.js +0 -260
  50. package/dist/templates/llm-utils.js.map +0 -1
  51. package/dist/templates/planner.d.ts +0 -5
  52. package/dist/templates/planner.js +0 -150
  53. package/dist/templates/planner.js.map +0 -1
  54. package/dist/templates/test-brain.d.ts +0 -6
  55. package/dist/templates/test-brain.js +0 -474
  56. package/dist/templates/test-brain.js.map +0 -1
  57. package/dist/templates/test-llm.d.ts +0 -7
  58. package/dist/templates/test-llm.js +0 -574
  59. package/dist/templates/test-llm.js.map +0 -1
  60. package/dist/templates/test-loader.d.ts +0 -5
  61. package/dist/templates/test-loader.js +0 -146
  62. package/dist/templates/test-loader.js.map +0 -1
  63. package/dist/templates/test-planner.d.ts +0 -5
  64. package/dist/templates/test-planner.js +0 -271
  65. package/dist/templates/test-planner.js.map +0 -1
  66. package/dist/templates/test-vault.d.ts +0 -5
  67. package/dist/templates/test-vault.js +0 -380
  68. package/dist/templates/test-vault.js.map +0 -1
  69. package/dist/templates/vault.d.ts +0 -5
  70. package/dist/templates/vault.js +0 -263
  71. 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.0.0",
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, readdirSync } from 'node:fs';
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
- installKnowledge,
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
- // ---------- 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
+ }
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;
@@ -1,6 +0,0 @@
1
- /**
2
- * Generates the brain.ts source file for a new agent.
3
- * The Brain provides a local, zero-dependency intelligence layer:
4
- * TF-IDF scoring, auto-tagging, duplicate detection, and search feedback.
5
- */
6
- export declare function generateBrain(): string;