@soleri/forge 4.2.2 → 5.0.1

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 (78) hide show
  1. package/dist/domain-manager.d.ts +2 -2
  2. package/dist/domain-manager.js +35 -16
  3. package/dist/domain-manager.js.map +1 -1
  4. package/dist/index.js +0 -0
  5. package/dist/knowledge-installer.js +18 -12
  6. package/dist/knowledge-installer.js.map +1 -1
  7. package/dist/patching.d.ts +15 -6
  8. package/dist/patching.js +37 -12
  9. package/dist/patching.js.map +1 -1
  10. package/dist/scaffolder.js +18 -28
  11. package/dist/scaffolder.js.map +1 -1
  12. package/dist/templates/brain.d.ts +6 -0
  13. package/dist/templates/brain.js +478 -0
  14. package/dist/templates/brain.js.map +1 -0
  15. package/dist/templates/core-facade.js +95 -47
  16. package/dist/templates/core-facade.js.map +1 -1
  17. package/dist/templates/entry-point.d.ts +4 -0
  18. package/dist/templates/entry-point.js +146 -89
  19. package/dist/templates/entry-point.js.map +1 -1
  20. package/dist/templates/facade-factory.d.ts +1 -0
  21. package/dist/templates/facade-factory.js +63 -0
  22. package/dist/templates/facade-factory.js.map +1 -0
  23. package/dist/templates/facade-types.d.ts +1 -0
  24. package/dist/templates/facade-types.js +46 -0
  25. package/dist/templates/facade-types.js.map +1 -0
  26. package/dist/templates/intelligence-loader.d.ts +1 -0
  27. package/dist/templates/intelligence-loader.js +43 -0
  28. package/dist/templates/intelligence-loader.js.map +1 -0
  29. package/dist/templates/intelligence-types.d.ts +1 -0
  30. package/dist/templates/intelligence-types.js +24 -0
  31. package/dist/templates/intelligence-types.js.map +1 -0
  32. package/dist/templates/llm-key-pool.d.ts +7 -0
  33. package/dist/templates/llm-key-pool.js +211 -0
  34. package/dist/templates/llm-key-pool.js.map +1 -0
  35. package/dist/templates/llm-types.d.ts +5 -0
  36. package/dist/templates/llm-types.js +161 -0
  37. package/dist/templates/llm-types.js.map +1 -0
  38. package/dist/templates/llm-utils.d.ts +5 -0
  39. package/dist/templates/llm-utils.js +260 -0
  40. package/dist/templates/llm-utils.js.map +1 -0
  41. package/dist/templates/package-json.js +3 -1
  42. package/dist/templates/package-json.js.map +1 -1
  43. package/dist/templates/planner.d.ts +5 -0
  44. package/dist/templates/planner.js +150 -0
  45. package/dist/templates/planner.js.map +1 -0
  46. package/dist/templates/test-brain.d.ts +6 -0
  47. package/dist/templates/test-brain.js +474 -0
  48. package/dist/templates/test-brain.js.map +1 -0
  49. package/dist/templates/test-facades.d.ts +1 -1
  50. package/dist/templates/test-facades.js +182 -456
  51. package/dist/templates/test-facades.js.map +1 -1
  52. package/dist/templates/test-llm.d.ts +7 -0
  53. package/dist/templates/test-llm.js +574 -0
  54. package/dist/templates/test-llm.js.map +1 -0
  55. package/dist/templates/test-loader.d.ts +5 -0
  56. package/dist/templates/test-loader.js +146 -0
  57. package/dist/templates/test-loader.js.map +1 -0
  58. package/dist/templates/test-planner.d.ts +5 -0
  59. package/dist/templates/test-planner.js +271 -0
  60. package/dist/templates/test-planner.js.map +1 -0
  61. package/dist/templates/test-vault.d.ts +5 -0
  62. package/dist/templates/test-vault.js +380 -0
  63. package/dist/templates/test-vault.js.map +1 -0
  64. package/dist/templates/vault.d.ts +5 -0
  65. package/dist/templates/vault.js +263 -0
  66. package/dist/templates/vault.js.map +1 -0
  67. package/dist/types.d.ts +2 -2
  68. package/package.json +1 -1
  69. package/src/__tests__/scaffolder.test.ts +52 -109
  70. package/src/domain-manager.ts +34 -15
  71. package/src/knowledge-installer.ts +23 -15
  72. package/src/patching.ts +44 -12
  73. package/src/scaffolder.ts +18 -29
  74. package/src/templates/entry-point.ts +146 -91
  75. package/src/templates/package-json.ts +3 -1
  76. package/src/templates/test-facades.ts +182 -458
  77. package/src/templates/core-facade.ts +0 -517
  78. package/src/templates/llm-client.ts +0 -301
@@ -1,5 +1,9 @@
1
1
  /**
2
2
  * Domain manager — add new knowledge domains to existing agents.
3
+ *
4
+ * v5.0: Domain facades come from @soleri/core at runtime (createDomainFacades).
5
+ * Adding a domain just needs: empty data bundle + patch index.ts domains array.
6
+ * Falls back to generating facade files for v4.x agents.
3
7
  */
4
8
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
5
9
  import { join } from 'node:path';
@@ -15,14 +19,24 @@ interface AddDomainParams {
15
19
  noBuild?: boolean;
16
20
  }
17
21
 
22
+ /**
23
+ * Detect if this is a v5.0+ agent (uses createAgentRuntime from @soleri/core).
24
+ */
25
+ function isV5Agent(agentPath: string): boolean {
26
+ const indexPath = join(agentPath, 'src', 'index.ts');
27
+ if (!existsSync(indexPath)) return false;
28
+ const content = readFileSync(indexPath, 'utf-8');
29
+ return content.includes('createAgentRuntime') || content.includes('createDomainFacades');
30
+ }
31
+
18
32
  /**
19
33
  * Add a new knowledge domain to an existing agent.
20
34
  *
21
35
  * Steps:
22
36
  * 1. Validate agent path and domain name
23
37
  * 2. Create empty intelligence bundle
24
- * 3. Generate domain facade
25
- * 4. Patch src/index.ts with import + registration
38
+ * 3. Generate domain facade (v4.x only — v5.0+ uses core factory)
39
+ * 4. Patch src/index.ts with domain registration
26
40
  * 5. Patch src/activation/claude-md-content.ts with facade table rows
27
41
  * 6. Rebuild (unless noBuild)
28
42
  */
@@ -68,7 +82,8 @@ export async function addDomain(params: AddDomainParams): Promise<AddDomainResul
68
82
  return fail(agentPath, domain, `Domain "${domain}" already exists`);
69
83
  }
70
84
 
71
- const hasBrain = existsSync(join(agentPath, 'src', 'brain'));
85
+ const v5 = isV5Agent(agentPath);
86
+ const hasBrain = v5 || existsSync(join(agentPath, 'src', 'brain'));
72
87
 
73
88
  // ── Step 1: Create empty bundle ──
74
89
 
@@ -82,18 +97,22 @@ export async function addDomain(params: AddDomainParams): Promise<AddDomainResul
82
97
  writeFileSync(join(distDataDir, `${domain}.json`), emptyBundle, 'utf-8');
83
98
  }
84
99
 
85
- // ── Step 2: Generate facade ──
100
+ // ── Step 2: Generate facade (v4.x only) ──
86
101
 
87
- const facadesDir = join(agentPath, 'src', 'facades');
88
- const facadePath = join(facadesDir, `${domain}.facade.ts`);
102
+ let facadeGenerated = false;
103
+ if (!v5) {
104
+ const facadesDir = join(agentPath, 'src', 'facades');
105
+ const facadePath = join(facadesDir, `${domain}.facade.ts`);
89
106
 
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');
107
+ if (existsSync(facadePath)) {
108
+ warnings.push(`Facade ${domain}.facade.ts already exists — skipped`);
109
+ } else {
110
+ const facadeCode = hasBrain
111
+ ? generateDomainFacade(agentId, domain)
112
+ : generateVaultOnlyDomainFacade(agentId, domain);
113
+ writeFileSync(facadePath, facadeCode, 'utf-8');
114
+ facadeGenerated = true;
115
+ }
97
116
  }
98
117
 
99
118
  // ── Step 3: Patch src/index.ts ──
@@ -147,10 +166,10 @@ export async function addDomain(params: AddDomainParams): Promise<AddDomainResul
147
166
  agentPath,
148
167
  domain,
149
168
  agentId,
150
- facadeGenerated: !existsSync(facadePath) || !warnings.some((w) => w.includes('already exists')),
169
+ facadeGenerated,
151
170
  buildOutput,
152
171
  warnings,
153
- summary: `Added domain "${domain}" to ${agentId}${warnings.length > 0 ? ` (${warnings.length} warning(s))` : ''}`,
172
+ summary: `Added domain "${domain}" to ${agentId}${v5 ? ' (v5.0 — no facade file needed)' : ''}${warnings.length > 0 ? ` (${warnings.length} warning(s))` : ''}`,
154
173
  };
155
174
  }
156
175
 
@@ -185,6 +185,11 @@ export async function installKnowledge(
185
185
 
186
186
  const hasBrain = existsSync(join(agentPath, 'src', 'brain'));
187
187
 
188
+ // v5.0+ agents use createDomainFacades() from @soleri/core — no facade files needed
189
+ const indexPath5 = join(agentPath, 'src', 'index.ts');
190
+ const isV5 =
191
+ existsSync(indexPath5) && readFileSync(indexPath5, 'utf-8').includes('createDomainFacades');
192
+
188
193
  // ── Step 2: Read and validate bundles ────────────────────────────
189
194
 
190
195
  const bundleFiles = collectBundleFiles(bundlePath);
@@ -253,22 +258,25 @@ export async function installKnowledge(
253
258
  // ── Step 4: Generate facades for new domains ─────────────────────
254
259
 
255
260
  if (generateFacades && domainsAdded.length > 0) {
256
- const facadesDir = join(agentPath, 'src', 'facades');
257
-
258
- for (const domain of domainsAdded) {
259
- const facadePath = join(facadesDir, `${domain}.facade.ts`);
260
- // Skip if facade already exists (idempotent)
261
- if (existsSync(facadePath)) {
262
- warnings.push(`Facade ${domain}.facade.ts already exists — skipped`);
263
- continue;
261
+ // v5.0+ agents: no facade files needed (createDomainFacades from @soleri/core)
262
+ // v4.x agents: generate facade files
263
+ if (!isV5) {
264
+ const facadesDir = join(agentPath, 'src', 'facades');
265
+
266
+ for (const domain of domainsAdded) {
267
+ const facadePath = join(facadesDir, `${domain}.facade.ts`);
268
+ if (existsSync(facadePath)) {
269
+ warnings.push(`Facade ${domain}.facade.ts already exists — skipped`);
270
+ continue;
271
+ }
272
+
273
+ const facadeCode = hasBrain
274
+ ? generateDomainFacade(agentId, domain)
275
+ : generateVaultOnlyDomainFacade(agentId, domain);
276
+
277
+ writeFileSync(facadePath, facadeCode, 'utf-8');
278
+ facadesGenerated.push(`${domain}.facade.ts`);
264
279
  }
265
-
266
- const facadeCode = hasBrain
267
- ? generateDomainFacade(agentId, domain)
268
- : generateVaultOnlyDomainFacade(agentId, domain);
269
-
270
- writeFileSync(facadePath, facadeCode, 'utf-8');
271
- facadesGenerated.push(`${domain}.facade.ts`);
272
280
  }
273
281
 
274
282
  // ── Step 5: Patch src/index.ts ───────────────────────────────────
package/src/patching.ts CHANGED
@@ -3,42 +3,68 @@
3
3
  *
4
4
  * These functions modify an agent's src/index.ts and src/activation/claude-md-content.ts
5
5
  * to register new domain facades and their CLAUDE.md table rows.
6
+ *
7
+ * v5.0: Entry point uses createDomainFacades() from @soleri/core.
8
+ * New domain patching inserts domain names into the domains array literal.
6
9
  */
7
- import { pascalCase } from './templates/domain-facade.js';
8
10
 
9
11
  /**
10
- * Patch the agent's src/index.ts to add imports and facade registrations
11
- * for new domains.
12
+ * Patch the agent's src/index.ts to add new domains.
13
+ *
14
+ * v5.0 format: The entry point has a `createDomainFacades(runtime, 'agentId', [...domains])` call.
15
+ * We insert new domain strings into that array.
12
16
  *
13
- * Anchor patterns:
14
- * - Import: insert before `import { createCoreFacade }`
15
- * - Facade array: insert before `createCoreFacade(`
17
+ * Falls back to v4.x format (import + facade creation) for older agents.
16
18
  */
17
19
  export function patchIndexTs(
18
20
  source: string,
19
21
  newDomains: string[],
20
- hasBrain: boolean,
22
+ _hasBrain?: boolean,
21
23
  ): string | null {
22
- // Filter out domains whose imports already exist (idempotent)
24
+ // ── v5.0 format: createDomainFacades() with domains array literal ──
25
+ const v5Pattern = /createDomainFacades\(runtime,\s*'[^']+',\s*\[([\s\S]*?)\]\)/;
26
+ const v5Match = source.match(v5Pattern);
27
+
28
+ if (v5Match) {
29
+ const existingArrayContent = v5Match[1];
30
+ const domainsToAdd = newDomains.filter(
31
+ (d) => !existingArrayContent.includes(`'${d}'`) && !existingArrayContent.includes(`"${d}"`),
32
+ );
33
+ if (domainsToAdd.length === 0) return source;
34
+
35
+ const newEntries = domainsToAdd.map((d) => `'${d}'`).join(', ');
36
+ const currentContent = existingArrayContent.trim();
37
+ const updatedContent = currentContent ? `${currentContent}, ${newEntries}` : newEntries;
38
+
39
+ return source.replace(v5Pattern, (match) => {
40
+ return match.replace(v5Match[1], updatedContent);
41
+ });
42
+ }
43
+
44
+ // ── v4.x fallback: import + facade creation anchors ──
45
+ return patchIndexTsV4(source, newDomains, _hasBrain ?? true);
46
+ }
47
+
48
+ /**
49
+ * Legacy v4.x patching — import-based domain facade registration.
50
+ */
51
+ function patchIndexTsV4(source: string, newDomains: string[], hasBrain: boolean): string | null {
23
52
  const domainsToImport = newDomains.filter((d) => {
24
53
  const fn = `create${pascalCase(d)}Facade`;
25
54
  return !source.includes(`import { ${fn} }`);
26
55
  });
27
56
 
28
- // Filter out domains whose facade calls already exist
29
57
  const domainsToRegister = newDomains.filter((d) => {
30
58
  const fn = `create${pascalCase(d)}Facade(`;
31
59
  return !source.includes(fn);
32
60
  });
33
61
 
34
- // Nothing to patch
35
62
  if (domainsToImport.length === 0 && domainsToRegister.length === 0) {
36
63
  return source;
37
64
  }
38
65
 
39
66
  let patched = source;
40
67
 
41
- // ── Insert imports ──
42
68
  if (domainsToImport.length > 0) {
43
69
  const importAnchor = /^(import \{ createCoreFacade \}.*$)/m;
44
70
  if (!importAnchor.test(patched)) return null;
@@ -53,7 +79,6 @@ export function patchIndexTs(
53
79
  patched = patched.replace(importAnchor, `${newImports}\n$1`);
54
80
  }
55
81
 
56
- // ── Insert facade creations ──
57
82
  if (domainsToRegister.length > 0) {
58
83
  const facadeAnchor = /^(\s+createCoreFacade\()/m;
59
84
  if (!facadeAnchor.test(patched)) return null;
@@ -121,3 +146,10 @@ export function patchClaudeMdContent(
121
146
 
122
147
  return source.slice(0, anchorIdx) + newRows.join('\n') + '\n' + source.slice(anchorIdx);
123
148
  }
149
+
150
+ function pascalCase(s: string): string {
151
+ return s
152
+ .split(/[-_\s]+/)
153
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
154
+ .join('');
155
+ }
package/src/scaffolder.ts CHANGED
@@ -14,8 +14,6 @@ import { generatePackageJson } from './templates/package-json.js';
14
14
  import { generateTsconfig } from './templates/tsconfig.js';
15
15
  import { generateVitestConfig } from './templates/vitest-config.js';
16
16
  import { generatePersona } from './templates/persona.js';
17
- import { generateDomainFacade } from './templates/domain-facade.js';
18
- import { generateCoreFacade } from './templates/core-facade.js';
19
17
  import { generateEntryPoint } from './templates/entry-point.js';
20
18
  import { generateFacadesTest } from './templates/test-facades.js';
21
19
  import { generateClaudeMdTemplate } from './templates/claude-md-template.js';
@@ -23,7 +21,6 @@ import { generateInjectClaudeMd } from './templates/inject-claude-md.js';
23
21
  import { generateActivate } from './templates/activate.js';
24
22
  import { generateReadme } from './templates/readme.js';
25
23
  import { generateSetupScript } from './templates/setup-script.js';
26
- import { generateLLMClient } from './templates/llm-client.js';
27
24
 
28
25
  /**
29
26
  * Preview what scaffold will create without writing anything.
@@ -43,21 +40,8 @@ export function previewScaffold(config: AgentConfig): ScaffoldPreview {
43
40
  {
44
41
  path: 'src/index.ts',
45
42
  description:
46
- 'Entry point — initializes vault, planner, brain, registers facades, starts stdio',
47
- },
48
- {
49
- path: 'src/llm/llm-client.ts',
50
- description:
51
- 'LLM client — unified OpenAI/Anthropic caller with model routing (optional, needs API keys)',
52
- },
53
- {
54
- path: 'src/facades/core.facade.ts',
55
- description: 'Core facade — search, vault stats, health, identity',
43
+ 'Entry point — thin shell using createAgentRuntime() + createCoreOps() from @soleri/core',
56
44
  },
57
- ...config.domains.map((d) => ({
58
- path: `src/facades/${d}.facade.ts`,
59
- description: `${d} facade — search, get_patterns, capture, remove`,
60
- })),
61
45
  ...config.domains.map((d) => ({
62
46
  path: `src/intelligence/data/${d}.json`,
63
47
  description: `Empty ${d} intelligence bundle — ready for knowledge capture`,
@@ -101,14 +85,10 @@ export function previewScaffold(config: AgentConfig): ScaffoldPreview {
101
85
  {
102
86
  name: `${config.id}_core`,
103
87
  ops: [
88
+ // From createCoreOps() — 26 generic ops
104
89
  'search',
105
90
  'vault_stats',
106
91
  'list_all',
107
- 'health',
108
- 'identity',
109
- 'activate',
110
- 'inject_claude_md',
111
- 'setup',
112
92
  'register',
113
93
  'memory_search',
114
94
  'memory_capture',
@@ -124,6 +104,20 @@ export function previewScaffold(config: AgentConfig): ScaffoldPreview {
124
104
  'rebuild_vocabulary',
125
105
  'brain_stats',
126
106
  'llm_status',
107
+ 'curator_status',
108
+ 'curator_detect_duplicates',
109
+ 'curator_contradictions',
110
+ 'curator_resolve_contradiction',
111
+ 'curator_groom',
112
+ 'curator_groom_all',
113
+ 'curator_consolidate',
114
+ 'curator_health_audit',
115
+ // Agent-specific ops — 5
116
+ 'health',
117
+ 'identity',
118
+ 'activate',
119
+ 'inject_claude_md',
120
+ 'setup',
127
121
  ],
128
122
  },
129
123
  ];
@@ -159,12 +153,10 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
159
153
  '',
160
154
  'scripts',
161
155
  'src',
162
- 'src/facades',
163
156
  'src/intelligence',
164
157
  'src/intelligence/data',
165
158
  'src/identity',
166
159
  'src/activation',
167
- 'src/llm',
168
160
  'src/__tests__',
169
161
  ];
170
162
 
@@ -201,19 +193,16 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
201
193
 
202
194
  // Write source files
203
195
  const sourceFiles: Array<[string, string]> = [
204
- ['src/facades/core.facade.ts', generateCoreFacade(config)],
205
196
  ['src/identity/persona.ts', generatePersona(config)],
206
197
  ['src/activation/claude-md-content.ts', generateClaudeMdTemplate(config)],
207
198
  ['src/activation/inject-claude-md.ts', generateInjectClaudeMd(config)],
208
199
  ['src/activation/activate.ts', generateActivate(config)],
209
200
  ['src/index.ts', generateEntryPoint(config)],
210
- ['src/llm/llm-client.ts', generateLLMClient(config)],
211
201
  ['src/__tests__/facades.test.ts', generateFacadesTest(config)],
212
202
  ];
213
203
 
214
- // Domain facades and empty data files
204
+ // Empty intelligence data bundles (domain facades come from @soleri/core at runtime)
215
205
  for (const domain of config.domains) {
216
- sourceFiles.push([`src/facades/${domain}.facade.ts`, generateDomainFacade(config.id, domain)]);
217
206
  sourceFiles.push([`src/intelligence/data/${domain}.json`, generateEmptyBundle(domain)]);
218
207
  }
219
208
 
@@ -222,7 +211,7 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
222
211
  filesCreated.push(path);
223
212
  }
224
213
 
225
- const totalOps = config.domains.length * 5 + 24; // 5 per domain + 24 core (activation, registration, memory, session, export, planning, brain, llm)
214
+ const totalOps = config.domains.length * 5 + 31; // 5 per domain + 26 core (from createCoreOps) + 5 agent-specific
226
215
 
227
216
  // Register the agent as an MCP server in ~/.claude.json
228
217
  const mcpReg = registerMcpServer(config.id, agentDir);
@@ -2,19 +2,13 @@ import type { AgentConfig } from '../types.js';
2
2
 
3
3
  /**
4
4
  * Generate the main index.ts entry point for the agent.
5
+ *
6
+ * v5.0: Thin shell — delegates to createAgentRuntime(), createCoreOps(),
7
+ * and createDomainFacades() from @soleri/core. Only agent-specific code
8
+ * (persona, activation) lives here.
5
9
  */
6
10
  export function generateEntryPoint(config: AgentConfig): string {
7
- const facadeImports = config.domains
8
- .map((d) => {
9
- const fn = `create${pascalCase(d)}Facade`;
10
- const file = `${d}.facade.js`;
11
- return `import { ${fn} } from './facades/${file}';`;
12
- })
13
- .join('\n');
14
-
15
- const facadeCreations = config.domains
16
- .map((d) => ` create${pascalCase(d)}Facade(vault, brain),`)
17
- .join('\n');
11
+ const domainsLiteral = JSON.stringify(config.domains);
18
12
 
19
13
  return `#!/usr/bin/env node
20
14
 
@@ -22,102 +16,170 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
22
16
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
23
17
  import { dirname, join } from 'node:path';
24
18
  import { fileURLToPath } from 'node:url';
25
- import { homedir } from 'node:os';
26
19
 
27
- import { Vault, Brain, Planner, KeyPool, CogneeClient, loadKeyPoolConfig, loadIntelligenceData, registerAllFacades } from '@soleri/core';
28
- ${facadeImports}
29
- import { createCoreFacade } from './facades/core.facade.js';
30
- import { LLMClient } from './llm/llm-client.js';
20
+ import {
21
+ createAgentRuntime,
22
+ createCoreOps,
23
+ createDomainFacades,
24
+ registerAllFacades,
25
+ } from '@soleri/core';
26
+ import type { OpDefinition } from '@soleri/core';
27
+ import { z } from 'zod';
31
28
  import { PERSONA, getPersonaPrompt } from './identity/persona.js';
29
+ import { activateAgent, deactivateAgent } from './activation/activate.js';
30
+ import { injectClaudeMd, injectClaudeMdGlobal, hasAgentMarker } from './activation/inject-claude-md.js';
32
31
 
33
32
  const __dirname = dirname(fileURLToPath(import.meta.url));
34
33
 
35
34
  async function main(): Promise<void> {
36
- // Initialize persistent vault at ~/.${config.id}/vault.db
37
- const vaultPath = join(homedir(), '.${config.id}', 'vault.db');
38
- const vault = new Vault(vaultPath);
39
- console.error(\`[\${PERSONA.name.toLowerCase()}] Vault: \${vaultPath}\`);
40
-
41
- // Load and seed intelligence data
42
- const dataDir = join(__dirname, 'intelligence', 'data');
43
- const entries = loadIntelligenceData(dataDir);
44
- if (entries.length > 0) {
45
- const seeded = vault.seed(entries);
46
- console.error(\`[\${PERSONA.name.toLowerCase()}] Seeded vault with \${seeded} intelligence entries\`);
47
- } else {
48
- console.error(\`[\${PERSONA.name.toLowerCase()}] Vault is empty — ready for knowledge capture\`);
49
- }
50
-
51
- // Initialize planner at ~/.${config.id}/plans.json
52
- const plansPath = join(homedir(), '.${config.id}', 'plans.json');
53
- const planner = new Planner(plansPath);
54
- console.error(\`[\${PERSONA.name.toLowerCase()}] Planner: \${plansPath}\`);
55
-
56
- // Initialize Cognee client (optional — vector search + knowledge graph)
57
- // Auto-registers a service account on first use. Override with env vars.
58
- const cognee = new CogneeClient({
59
- baseUrl: process.env.COGNEE_URL ?? 'http://localhost:8000',
60
- dataset: '${config.id}-vault',
61
- ...(process.env.COGNEE_API_TOKEN ? { apiToken: process.env.COGNEE_API_TOKEN } : {}),
62
- ...(process.env.COGNEE_EMAIL ? { serviceEmail: process.env.COGNEE_EMAIL } : {}),
63
- ...(process.env.COGNEE_PASSWORD ? { servicePassword: process.env.COGNEE_PASSWORD } : {}),
35
+ // ─── Runtime vault, brain, planner, curator, LLM, key pools ───
36
+ const runtime = createAgentRuntime({
37
+ agentId: '${config.id}',
38
+ dataDir: join(__dirname, 'intelligence', 'data'),
64
39
  });
65
- await cognee.healthCheck();
66
- console.error(\`[\${PERSONA.name.toLowerCase()}] Cognee: \${cognee.isAvailable ? 'available' : 'not running (FTS5 fallback)'}\`);
67
-
68
- // Initialize brain intelligence layer for ranked search, auto-tagging, dedup
69
- const brain = new Brain(vault, cognee.isAvailable ? cognee : undefined);
70
- console.error(\`[\${PERSONA.name.toLowerCase()}] Brain: vocabulary \${brain.getVocabularySize()} terms\`);
71
-
72
- // Background sync vault to Cognee if available
73
- if (cognee.isAvailable) {
74
- brain.syncToCognee().catch((err) => {
75
- console.error(\`[\${PERSONA.name.toLowerCase()}] Cognee sync failed:\`, err);
76
- });
77
- }
78
-
79
- // Initialize LLM client (optional works without API keys)
80
- const keyPoolFiles = loadKeyPoolConfig('${config.id}');
81
- const openaiKeyPool = new KeyPool(keyPoolFiles.openai);
82
- const anthropicKeyPool = new KeyPool(keyPoolFiles.anthropic);
83
- const llmClient = new LLMClient(openaiKeyPool, anthropicKeyPool);
84
- const llmAvail = llmClient.isAvailable();
85
- console.error(\`[\${PERSONA.name.toLowerCase()}] LLM: OpenAI \${llmAvail.openai ? 'available' : 'not configured'}, Anthropic \${llmAvail.anthropic ? 'available' : 'not configured'}\`);
86
-
87
- // Create MCP server
40
+
41
+ const tag = PERSONA.name.toLowerCase();
42
+ const stats = runtime.vault.stats();
43
+ console.error(\`[\${tag}] Vault: \${stats.totalEntries} entries, Brain: \${runtime.brain.getVocabularySize()} terms\`);
44
+
45
+ const llmAvail = runtime.llmClient.isAvailable();
46
+ console.error(\`[\${tag}] LLM: OpenAI \${llmAvail.openai ? 'available' : 'not configured'}, Anthropic \${llmAvail.anthropic ? 'available' : 'not configured'}\`);
47
+
48
+ // ─── Agent-specific ops (reference persona + activation) ────────
49
+ const agentOps: OpDefinition[] = [
50
+ {
51
+ name: 'health',
52
+ description: 'Health check — vault status and agent info.',
53
+ auth: 'read',
54
+ handler: async () => {
55
+ const s = runtime.vault.stats();
56
+ return {
57
+ status: 'ok',
58
+ agent: { name: PERSONA.name, role: PERSONA.role },
59
+ vault: { entries: s.totalEntries, domains: Object.keys(s.byDomain) },
60
+ };
61
+ },
62
+ },
63
+ {
64
+ name: 'identity',
65
+ description: 'Get agent identity — name, role, principles.',
66
+ auth: 'read',
67
+ handler: async () => PERSONA,
68
+ },
69
+ {
70
+ name: 'activate',
71
+ description: 'Activate agent persona — returns full context for Claude to adopt. Say "Hello, ${config.name}!" to trigger.',
72
+ auth: 'read',
73
+ schema: z.object({
74
+ projectPath: z.string().optional().default('.'),
75
+ deactivate: z.boolean().optional(),
76
+ }),
77
+ handler: async (params) => {
78
+ if (params.deactivate) {
79
+ return deactivateAgent();
80
+ }
81
+ return activateAgent(runtime.vault, (params.projectPath as string) ?? '.', runtime.planner);
82
+ },
83
+ },
84
+ {
85
+ name: 'inject_claude_md',
86
+ description: 'Inject agent sections into CLAUDE.md — project-level or global (~/.claude/CLAUDE.md). Idempotent.',
87
+ auth: 'write',
88
+ schema: z.object({
89
+ projectPath: z.string().optional().default('.'),
90
+ global: z.boolean().optional().describe('If true, inject into ~/.claude/CLAUDE.md instead of project-level'),
91
+ }),
92
+ handler: async (params) => {
93
+ if (params.global) {
94
+ return injectClaudeMdGlobal();
95
+ }
96
+ return injectClaudeMd((params.projectPath as string) ?? '.');
97
+ },
98
+ },
99
+ {
100
+ name: 'setup',
101
+ description: 'Check setup status — CLAUDE.md configured? Vault has entries? What to do next?',
102
+ auth: 'read',
103
+ schema: z.object({
104
+ projectPath: z.string().optional().default('.'),
105
+ }),
106
+ handler: async (params) => {
107
+ const { existsSync } = await import('node:fs');
108
+ const { join: joinPath } = await import('node:path');
109
+ const { homedir } = await import('node:os');
110
+ const projectPath = (params.projectPath as string) ?? '.';
111
+
112
+ const projectClaudeMd = joinPath(projectPath, 'CLAUDE.md');
113
+ const globalClaudeMd = joinPath(homedir(), '.claude', 'CLAUDE.md');
114
+
115
+ const projectExists = existsSync(projectClaudeMd);
116
+ const projectHasAgent = hasAgentMarker(projectClaudeMd);
117
+ const globalExists = existsSync(globalClaudeMd);
118
+ const globalHasAgent = hasAgentMarker(globalClaudeMd);
119
+
120
+ const s = runtime.vault.stats();
121
+
122
+ const recommendations: string[] = [];
123
+ if (!globalHasAgent && !projectHasAgent) {
124
+ recommendations.push('No CLAUDE.md configured — run inject_claude_md with global: true for all projects, or without for this project');
125
+ } else if (!globalHasAgent) {
126
+ recommendations.push('Global ~/.claude/CLAUDE.md not configured — run inject_claude_md with global: true to enable in all projects');
127
+ }
128
+ if (s.totalEntries === 0) {
129
+ recommendations.push('Vault is empty — add intelligence data or capture knowledge via domain facades');
130
+ }
131
+ if (recommendations.length === 0) {
132
+ recommendations.push('${config.name} is fully set up and ready!');
133
+ }
134
+
135
+ return {
136
+ agent: { name: PERSONA.name, role: PERSONA.role },
137
+ claude_md: {
138
+ project: { exists: projectExists, has_agent_section: projectHasAgent },
139
+ global: { exists: globalExists, has_agent_section: globalHasAgent },
140
+ },
141
+ vault: { entries: s.totalEntries, domains: Object.keys(s.byDomain) },
142
+ recommendations,
143
+ };
144
+ },
145
+ },
146
+ ];
147
+
148
+ // ─── Assemble facades ──────────────────────────────────────────
149
+ const coreOps = createCoreOps(runtime);
150
+ const coreFacade = {
151
+ name: '${config.id}_core',
152
+ description: 'Core operations — vault stats, cross-domain search, health check, identity, and activation system.',
153
+ ops: [...coreOps, ...agentOps],
154
+ };
155
+
156
+ const domainFacades = createDomainFacades(runtime, '${config.id}', ${domainsLiteral});
157
+
158
+ // ─── MCP server ────────────────────────────────────────────────
88
159
  const server = new McpServer({
89
160
  name: '${config.id}-mcp',
90
161
  version: '1.0.0',
91
162
  });
92
163
 
93
- // Register persona prompt
94
164
  server.prompt('persona', 'Get agent persona and principles', async () => ({
95
165
  messages: [{ role: 'assistant' as const, content: { type: 'text' as const, text: getPersonaPrompt() } }],
96
166
  }));
97
167
 
98
- // Create and register facades
99
- const facades = [
100
- ${facadeCreations}
101
- createCoreFacade(vault, planner, brain, cognee.isAvailable ? cognee : undefined, llmClient, openaiKeyPool, anthropicKeyPool),
102
- ];
103
-
168
+ const facades = [coreFacade, ...domainFacades];
104
169
  registerAllFacades(server, facades);
105
170
 
106
- const stats = vault.stats();
107
- console.error(\`[\${PERSONA.name.toLowerCase()}] \${PERSONA.name} \${PERSONA.role}\`);
108
- console.error(\`[\${PERSONA.name.toLowerCase()}] Vault: \${stats.totalEntries} entries across \${Object.keys(stats.byDomain).length} domains\`);
109
- console.error(\`[\${PERSONA.name.toLowerCase()}] Registered \${facades.length} facades with \${facades.reduce((sum, f) => sum + f.ops.length, 0)} operations\`);
171
+ console.error(\`[\${tag}] \${PERSONA.name} \${PERSONA.role}\`);
172
+ console.error(\`[\${tag}] Registered \${facades.length} facades with \${facades.reduce((sum, f) => sum + f.ops.length, 0)} operations\`);
110
173
 
111
- // Stdio transport
174
+ // ─── Transport + shutdown ──────────────────────────────────────
112
175
  const transport = new StdioServerTransport();
113
176
  await server.connect(transport);
114
- console.error(\`[\${PERSONA.name.toLowerCase()}] Connected via stdio transport\`);
115
- console.error(\`[\${PERSONA.name.toLowerCase()}] Say "Hello, \${PERSONA.name}!" to activate\`);
177
+ console.error(\`[\${tag}] Connected via stdio transport\`);
178
+ console.error(\`[\${tag}] Say "Hello, \${PERSONA.name}!" to activate\`);
116
179
 
117
- // Graceful shutdown
118
180
  const shutdown = async (): Promise<void> => {
119
- console.error(\`[\${PERSONA.name.toLowerCase()}] Shutting down...\`);
120
- vault.close();
181
+ console.error(\`[\${tag}] Shutting down...\`);
182
+ runtime.close();
121
183
  process.exit(0);
122
184
  };
123
185
  process.on('SIGTERM', shutdown);
@@ -130,10 +192,3 @@ main().catch((err) => {
130
192
  });
131
193
  `;
132
194
  }
133
-
134
- function pascalCase(s: string): string {
135
- return s
136
- .split(/[-_\s]+/)
137
- .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
138
- .join('');
139
- }
@@ -21,11 +21,13 @@ export function generatePackageJson(config: AgentConfig): string {
21
21
  license: 'MIT',
22
22
  engines: { node: '>=18.0.0' },
23
23
  dependencies: {
24
- '@anthropic-ai/sdk': '^0.39.0',
25
24
  '@modelcontextprotocol/sdk': '^1.12.1',
26
25
  '@soleri/core': '^2.0.0',
27
26
  zod: '^3.24.2',
28
27
  },
28
+ optionalDependencies: {
29
+ '@anthropic-ai/sdk': '^0.39.0',
30
+ },
29
31
  devDependencies: {
30
32
  '@types/node': '^22.13.4',
31
33
  '@vitest/coverage-v8': '^3.0.5',