@soleri/forge 0.0.1 → 4.0.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 (128) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +199 -0
  3. package/dist/facades/forge.facade.d.ts +9 -0
  4. package/dist/facades/forge.facade.js +134 -0
  5. package/dist/facades/forge.facade.js.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +81 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/knowledge-installer.d.ts +31 -0
  10. package/dist/knowledge-installer.js +437 -0
  11. package/dist/knowledge-installer.js.map +1 -0
  12. package/dist/scaffolder.d.ts +13 -0
  13. package/dist/scaffolder.js +330 -0
  14. package/dist/scaffolder.js.map +1 -0
  15. package/dist/templates/activate.d.ts +9 -0
  16. package/dist/templates/activate.js +139 -0
  17. package/dist/templates/activate.js.map +1 -0
  18. package/dist/templates/brain.d.ts +6 -0
  19. package/dist/templates/brain.js +478 -0
  20. package/dist/templates/brain.js.map +1 -0
  21. package/dist/templates/claude-md-template.d.ts +11 -0
  22. package/dist/templates/claude-md-template.js +73 -0
  23. package/dist/templates/claude-md-template.js.map +1 -0
  24. package/dist/templates/core-facade.d.ts +6 -0
  25. package/dist/templates/core-facade.js +456 -0
  26. package/dist/templates/core-facade.js.map +1 -0
  27. package/dist/templates/domain-facade.d.ts +7 -0
  28. package/dist/templates/domain-facade.js +119 -0
  29. package/dist/templates/domain-facade.js.map +1 -0
  30. package/dist/templates/entry-point.d.ts +5 -0
  31. package/dist/templates/entry-point.js +116 -0
  32. package/dist/templates/entry-point.js.map +1 -0
  33. package/dist/templates/facade-factory.d.ts +1 -0
  34. package/dist/templates/facade-factory.js +63 -0
  35. package/dist/templates/facade-factory.js.map +1 -0
  36. package/dist/templates/facade-types.d.ts +1 -0
  37. package/dist/templates/facade-types.js +46 -0
  38. package/dist/templates/facade-types.js.map +1 -0
  39. package/dist/templates/inject-claude-md.d.ts +11 -0
  40. package/dist/templates/inject-claude-md.js +92 -0
  41. package/dist/templates/inject-claude-md.js.map +1 -0
  42. package/dist/templates/intelligence-loader.d.ts +1 -0
  43. package/dist/templates/intelligence-loader.js +43 -0
  44. package/dist/templates/intelligence-loader.js.map +1 -0
  45. package/dist/templates/intelligence-types.d.ts +1 -0
  46. package/dist/templates/intelligence-types.js +24 -0
  47. package/dist/templates/intelligence-types.js.map +1 -0
  48. package/dist/templates/llm-client.d.ts +7 -0
  49. package/dist/templates/llm-client.js +300 -0
  50. package/dist/templates/llm-client.js.map +1 -0
  51. package/dist/templates/llm-key-pool.d.ts +7 -0
  52. package/dist/templates/llm-key-pool.js +211 -0
  53. package/dist/templates/llm-key-pool.js.map +1 -0
  54. package/dist/templates/llm-types.d.ts +5 -0
  55. package/dist/templates/llm-types.js +161 -0
  56. package/dist/templates/llm-types.js.map +1 -0
  57. package/dist/templates/llm-utils.d.ts +5 -0
  58. package/dist/templates/llm-utils.js +260 -0
  59. package/dist/templates/llm-utils.js.map +1 -0
  60. package/dist/templates/package-json.d.ts +2 -0
  61. package/dist/templates/package-json.js +37 -0
  62. package/dist/templates/package-json.js.map +1 -0
  63. package/dist/templates/persona.d.ts +2 -0
  64. package/dist/templates/persona.js +42 -0
  65. package/dist/templates/persona.js.map +1 -0
  66. package/dist/templates/planner.d.ts +5 -0
  67. package/dist/templates/planner.js +150 -0
  68. package/dist/templates/planner.js.map +1 -0
  69. package/dist/templates/readme.d.ts +5 -0
  70. package/dist/templates/readme.js +316 -0
  71. package/dist/templates/readme.js.map +1 -0
  72. package/dist/templates/setup-script.d.ts +6 -0
  73. package/dist/templates/setup-script.js +112 -0
  74. package/dist/templates/setup-script.js.map +1 -0
  75. package/dist/templates/test-brain.d.ts +6 -0
  76. package/dist/templates/test-brain.js +474 -0
  77. package/dist/templates/test-brain.js.map +1 -0
  78. package/dist/templates/test-facades.d.ts +6 -0
  79. package/dist/templates/test-facades.js +649 -0
  80. package/dist/templates/test-facades.js.map +1 -0
  81. package/dist/templates/test-llm.d.ts +7 -0
  82. package/dist/templates/test-llm.js +574 -0
  83. package/dist/templates/test-llm.js.map +1 -0
  84. package/dist/templates/test-loader.d.ts +5 -0
  85. package/dist/templates/test-loader.js +146 -0
  86. package/dist/templates/test-loader.js.map +1 -0
  87. package/dist/templates/test-planner.d.ts +5 -0
  88. package/dist/templates/test-planner.js +271 -0
  89. package/dist/templates/test-planner.js.map +1 -0
  90. package/dist/templates/test-vault.d.ts +5 -0
  91. package/dist/templates/test-vault.js +380 -0
  92. package/dist/templates/test-vault.js.map +1 -0
  93. package/dist/templates/tsconfig.d.ts +1 -0
  94. package/dist/templates/tsconfig.js +25 -0
  95. package/dist/templates/tsconfig.js.map +1 -0
  96. package/dist/templates/vault.d.ts +5 -0
  97. package/dist/templates/vault.js +263 -0
  98. package/dist/templates/vault.js.map +1 -0
  99. package/dist/templates/vitest-config.d.ts +1 -0
  100. package/dist/templates/vitest-config.js +27 -0
  101. package/dist/templates/vitest-config.js.map +1 -0
  102. package/dist/types.d.ts +89 -0
  103. package/dist/types.js +21 -0
  104. package/dist/types.js.map +1 -0
  105. package/package.json +42 -4
  106. package/src/__tests__/knowledge-installer.test.ts +805 -0
  107. package/src/__tests__/scaffolder.test.ts +323 -0
  108. package/src/facades/forge.facade.ts +150 -0
  109. package/src/index.ts +101 -0
  110. package/src/knowledge-installer.ts +532 -0
  111. package/src/scaffolder.ts +386 -0
  112. package/src/templates/activate.ts +145 -0
  113. package/src/templates/claude-md-template.ts +137 -0
  114. package/src/templates/core-facade.ts +457 -0
  115. package/src/templates/domain-facade.ts +121 -0
  116. package/src/templates/entry-point.ts +120 -0
  117. package/src/templates/inject-claude-md.ts +94 -0
  118. package/src/templates/llm-client.ts +301 -0
  119. package/src/templates/package-json.ts +39 -0
  120. package/src/templates/persona.ts +45 -0
  121. package/src/templates/readme.ts +319 -0
  122. package/src/templates/setup-script.ts +113 -0
  123. package/src/templates/test-facades.ts +656 -0
  124. package/src/templates/tsconfig.ts +25 -0
  125. package/src/templates/vitest-config.ts +26 -0
  126. package/src/types.ts +68 -0
  127. package/tsconfig.json +21 -0
  128. package/vitest.config.ts +15 -0
@@ -0,0 +1,386 @@
1
+ import {
2
+ mkdirSync,
3
+ writeFileSync,
4
+ chmodSync,
5
+ existsSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+ import type { AgentConfig, ScaffoldResult, ScaffoldPreview, AgentInfo } from './types.js';
12
+
13
+ import { generatePackageJson } from './templates/package-json.js';
14
+ import { generateTsconfig } from './templates/tsconfig.js';
15
+ import { generateVitestConfig } from './templates/vitest-config.js';
16
+ import { generatePersona } from './templates/persona.js';
17
+ import { generateDomainFacade } from './templates/domain-facade.js';
18
+ import { generateCoreFacade } from './templates/core-facade.js';
19
+ import { generateEntryPoint } from './templates/entry-point.js';
20
+ import { generateFacadesTest } from './templates/test-facades.js';
21
+ import { generateClaudeMdTemplate } from './templates/claude-md-template.js';
22
+ import { generateInjectClaudeMd } from './templates/inject-claude-md.js';
23
+ import { generateActivate } from './templates/activate.js';
24
+ import { generateReadme } from './templates/readme.js';
25
+ import { generateSetupScript } from './templates/setup-script.js';
26
+ import { generateLLMClient } from './templates/llm-client.js';
27
+
28
+ /**
29
+ * Preview what scaffold will create without writing anything.
30
+ */
31
+ export function previewScaffold(config: AgentConfig): ScaffoldPreview {
32
+ const agentDir = join(config.outputDir, config.id);
33
+
34
+ const files = [
35
+ { path: 'package.json', description: 'NPM package with MCP SDK, SQLite, Zod dependencies' },
36
+ { path: 'tsconfig.json', description: 'TypeScript config (ES2022, NodeNext, strict)' },
37
+ { path: 'vitest.config.ts', description: 'Test config (vitest, forks pool, coverage)' },
38
+ { path: '.gitignore', description: 'Git ignore (node_modules, dist, coverage)' },
39
+ {
40
+ path: 'scripts/copy-assets.js',
41
+ description: 'Build script to copy intelligence data to dist',
42
+ },
43
+ {
44
+ path: 'src/index.ts',
45
+ 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',
56
+ },
57
+ ...config.domains.map((d) => ({
58
+ path: `src/facades/${d}.facade.ts`,
59
+ description: `${d} facade — search, get_patterns, capture, remove`,
60
+ })),
61
+ ...config.domains.map((d) => ({
62
+ path: `src/intelligence/data/${d}.json`,
63
+ description: `Empty ${d} intelligence bundle — ready for knowledge capture`,
64
+ })),
65
+ {
66
+ path: 'src/identity/persona.ts',
67
+ description: `${config.name} persona — name, role, principles, greeting`,
68
+ },
69
+ {
70
+ path: 'src/activation/claude-md-content.ts',
71
+ description: `${config.name} CLAUDE.md content with activation triggers and facades table`,
72
+ },
73
+ {
74
+ path: 'src/activation/inject-claude-md.ts',
75
+ description: 'Idempotent CLAUDE.md injection — project-level or global (~/.claude/CLAUDE.md)',
76
+ },
77
+ {
78
+ path: 'src/activation/activate.ts',
79
+ description: `${config.name} activation system — persona adoption, setup status, tool recommendations`,
80
+ },
81
+ {
82
+ path: 'src/__tests__/facades.test.ts',
83
+ description: `Facade integration tests — all ${config.domains.length + 1} facades`,
84
+ },
85
+ { path: '.mcp.json', description: 'MCP client config for connecting to this agent' },
86
+ {
87
+ path: 'README.md',
88
+ description: `${config.name} documentation — quick start, domains, principles, commands`,
89
+ },
90
+ {
91
+ path: 'scripts/setup.sh',
92
+ description: 'Automated setup — Node.js check, build, Claude Code MCP registration',
93
+ },
94
+ ];
95
+
96
+ const facades = [
97
+ ...config.domains.map((d) => ({
98
+ name: `${config.id}_${d.replace(/-/g, '_')}`,
99
+ ops: ['get_patterns', 'search', 'get_entry', 'capture', 'remove'],
100
+ })),
101
+ {
102
+ name: `${config.id}_core`,
103
+ ops: [
104
+ 'search',
105
+ 'vault_stats',
106
+ 'list_all',
107
+ 'health',
108
+ 'identity',
109
+ 'activate',
110
+ 'inject_claude_md',
111
+ 'setup',
112
+ 'register',
113
+ 'memory_search',
114
+ 'memory_capture',
115
+ 'memory_list',
116
+ 'session_capture',
117
+ 'export',
118
+ 'create_plan',
119
+ 'get_plan',
120
+ 'approve_plan',
121
+ 'update_task',
122
+ 'complete_plan',
123
+ 'record_feedback',
124
+ 'rebuild_vocabulary',
125
+ 'brain_stats',
126
+ 'llm_status',
127
+ ],
128
+ },
129
+ ];
130
+
131
+ return {
132
+ agentDir,
133
+ files,
134
+ facades,
135
+ domains: config.domains,
136
+ persona: { name: config.name, role: config.role },
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Scaffold a complete MCP agent project.
142
+ */
143
+ export function scaffold(config: AgentConfig): ScaffoldResult {
144
+ const agentDir = join(config.outputDir, config.id);
145
+ const filesCreated: string[] = [];
146
+
147
+ if (existsSync(agentDir)) {
148
+ return {
149
+ success: false,
150
+ agentDir,
151
+ filesCreated: [],
152
+ domains: config.domains,
153
+ summary: `Directory already exists: ${agentDir}. Choose a different ID or remove the existing directory.`,
154
+ };
155
+ }
156
+
157
+ // Create directory structure
158
+ const dirs = [
159
+ '',
160
+ 'scripts',
161
+ 'src',
162
+ 'src/facades',
163
+ 'src/intelligence',
164
+ 'src/intelligence/data',
165
+ 'src/identity',
166
+ 'src/activation',
167
+ 'src/llm',
168
+ 'src/__tests__',
169
+ ];
170
+
171
+ for (const dir of dirs) {
172
+ mkdirSync(join(agentDir, dir), { recursive: true });
173
+ }
174
+
175
+ // Write project config files
176
+ const projectFiles: Array<[string, string]> = [
177
+ ['package.json', generatePackageJson(config)],
178
+ ['tsconfig.json', generateTsconfig()],
179
+ ['vitest.config.ts', generateVitestConfig()],
180
+ ['.gitignore', 'node_modules/\ndist/\ncoverage/\n*.tsbuildinfo\n.env\n.DS_Store\n*.log\n'],
181
+ [
182
+ '.mcp.json',
183
+ JSON.stringify(
184
+ { mcpServers: { [config.id]: { command: 'node', args: ['dist/index.js'], cwd: '.' } } },
185
+ null,
186
+ 2,
187
+ ),
188
+ ],
189
+ ['scripts/copy-assets.js', generateCopyAssetsScript()],
190
+ ['README.md', generateReadme(config)],
191
+ ['scripts/setup.sh', generateSetupScript(config)],
192
+ ];
193
+
194
+ for (const [path, content] of projectFiles) {
195
+ writeFileSync(join(agentDir, path), content, 'utf-8');
196
+ filesCreated.push(path);
197
+ }
198
+
199
+ // Make setup script executable
200
+ chmodSync(join(agentDir, 'scripts', 'setup.sh'), 0o755);
201
+
202
+ // Write source files
203
+ const sourceFiles: Array<[string, string]> = [
204
+ ['src/facades/core.facade.ts', generateCoreFacade(config)],
205
+ ['src/identity/persona.ts', generatePersona(config)],
206
+ ['src/activation/claude-md-content.ts', generateClaudeMdTemplate(config)],
207
+ ['src/activation/inject-claude-md.ts', generateInjectClaudeMd(config)],
208
+ ['src/activation/activate.ts', generateActivate(config)],
209
+ ['src/index.ts', generateEntryPoint(config)],
210
+ ['src/llm/llm-client.ts', generateLLMClient(config)],
211
+ ['src/__tests__/facades.test.ts', generateFacadesTest(config)],
212
+ ];
213
+
214
+ // Domain facades and empty data files
215
+ for (const domain of config.domains) {
216
+ sourceFiles.push([`src/facades/${domain}.facade.ts`, generateDomainFacade(config.id, domain)]);
217
+ sourceFiles.push([`src/intelligence/data/${domain}.json`, generateEmptyBundle(domain)]);
218
+ }
219
+
220
+ for (const [path, content] of sourceFiles) {
221
+ writeFileSync(join(agentDir, path), content, 'utf-8');
222
+ filesCreated.push(path);
223
+ }
224
+
225
+ const totalOps = config.domains.length * 5 + 24; // 5 per domain + 24 core (activation, registration, memory, session, export, planning, brain, llm)
226
+
227
+ // Register the agent as an MCP server in ~/.claude.json
228
+ const mcpReg = registerMcpServer(config.id, agentDir);
229
+
230
+ const summaryLines = [
231
+ `Created ${config.name} agent at ${agentDir}`,
232
+ `${config.domains.length + 1} facades with ${totalOps} operations`,
233
+ `${config.domains.length} empty knowledge domains ready for capture`,
234
+ `Intelligence layer (Brain) — TF-IDF scoring, auto-tagging, duplicate detection`,
235
+ `Activation system included — say "Hello, ${config.name}!" to activate`,
236
+ `1 test suite — facades (vault, brain, planner, llm tests provided by @soleri/core)`,
237
+ ];
238
+
239
+ if (mcpReg.registered) {
240
+ summaryLines.push(`MCP server registered in ${mcpReg.path}`);
241
+ } else {
242
+ summaryLines.push(`Warning: Failed to register MCP server in ${mcpReg.path}: ${mcpReg.error}`);
243
+ }
244
+
245
+ summaryLines.push(
246
+ '',
247
+ 'Next steps:',
248
+ ` cd ${agentDir}`,
249
+ ' npm install && npm run build',
250
+ ' npm test # verify all tests pass',
251
+ ' Restart Claude Code',
252
+ ` Say "Hello, ${config.name}!" to activate the persona`,
253
+ );
254
+
255
+ return {
256
+ success: true,
257
+ agentDir,
258
+ filesCreated,
259
+ domains: config.domains,
260
+ summary: summaryLines.join('\n'),
261
+ };
262
+ }
263
+
264
+ /**
265
+ * List agents in a directory.
266
+ */
267
+ export function listAgents(parentDir: string): AgentInfo[] {
268
+ if (!existsSync(parentDir)) return [];
269
+
270
+ const agents: AgentInfo[] = [];
271
+ let entries: string[];
272
+ try {
273
+ entries = readdirSync(parentDir, { withFileTypes: true })
274
+ .filter((e) => e.isDirectory())
275
+ .map((e) => e.name);
276
+ } catch {
277
+ return [];
278
+ }
279
+
280
+ for (const name of entries) {
281
+ const dir = join(parentDir, name);
282
+ const pkgPath = join(dir, 'package.json');
283
+ if (!existsSync(pkgPath)) continue;
284
+
285
+ try {
286
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
287
+ if (!pkg.name?.endsWith('-mcp')) continue;
288
+
289
+ const dataDir = join(dir, 'src', 'intelligence', 'data');
290
+ let domains: string[] = [];
291
+ try {
292
+ domains = readdirSync(dataDir)
293
+ .filter((f) => f.endsWith('.json'))
294
+ .map((f) => f.replace('.json', ''));
295
+ } catch {
296
+ /* empty */
297
+ }
298
+
299
+ agents.push({
300
+ id: name,
301
+ name: pkg.name.replace('-mcp', ''),
302
+ role: pkg.description || '',
303
+ path: dir,
304
+ domains,
305
+ hasNodeModules: existsSync(join(dir, 'node_modules')),
306
+ hasDistDir: existsSync(join(dir, 'dist')),
307
+ });
308
+ } catch {
309
+ /* skip non-agent directories */
310
+ }
311
+ }
312
+
313
+ return agents;
314
+ }
315
+
316
+ /**
317
+ * Register the agent as an MCP server in ~/.claude.json (User MCPs).
318
+ * Idempotent — updates existing entry if present.
319
+ */
320
+ function registerMcpServer(
321
+ agentId: string,
322
+ agentDir: string,
323
+ ): { registered: boolean; path: string; error?: string } {
324
+ const claudeJsonPath = join(homedir(), '.claude.json');
325
+
326
+ try {
327
+ let config: Record<string, unknown> = {};
328
+
329
+ if (existsSync(claudeJsonPath)) {
330
+ config = JSON.parse(readFileSync(claudeJsonPath, 'utf-8'));
331
+ }
332
+
333
+ if (!config.mcpServers || typeof config.mcpServers !== 'object') {
334
+ config.mcpServers = {};
335
+ }
336
+
337
+ const servers = config.mcpServers as Record<string, unknown>;
338
+ servers[agentId] = {
339
+ type: 'stdio',
340
+ command: 'node',
341
+ args: [join(agentDir, 'dist', 'index.js')],
342
+ env: {},
343
+ };
344
+
345
+ writeFileSync(claudeJsonPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
346
+ return { registered: true, path: claudeJsonPath };
347
+ } catch (err) {
348
+ return {
349
+ registered: false,
350
+ path: claudeJsonPath,
351
+ error: err instanceof Error ? err.message : String(err),
352
+ };
353
+ }
354
+ }
355
+
356
+ function generateEmptyBundle(domain: string): string {
357
+ return JSON.stringify(
358
+ {
359
+ domain,
360
+ version: '1.0.0',
361
+ entries: [],
362
+ },
363
+ null,
364
+ 2,
365
+ );
366
+ }
367
+
368
+ function generateCopyAssetsScript(): string {
369
+ return [
370
+ "import { cpSync, existsSync, mkdirSync } from 'node:fs';",
371
+ "import { join, dirname } from 'node:path';",
372
+ "import { fileURLToPath } from 'node:url';",
373
+ '',
374
+ 'const __dirname = dirname(fileURLToPath(import.meta.url));',
375
+ "const root = join(__dirname, '..');",
376
+ "const dist = join(root, 'dist');",
377
+ "const dataSource = join(root, 'src', 'intelligence', 'data');",
378
+ "const dataDest = join(dist, 'intelligence', 'data');",
379
+ '',
380
+ 'if (existsSync(dataSource)) {',
381
+ ' mkdirSync(dataDest, { recursive: true });',
382
+ ' cpSync(dataSource, dataDest, { recursive: true });',
383
+ " console.log('Copied intelligence data to dist/');",
384
+ '}',
385
+ ].join('\n');
386
+ }
@@ -0,0 +1,145 @@
1
+ import type { AgentConfig } from '../types.js';
2
+
3
+ /**
4
+ * Generates src/activation/activate.ts for a new agent.
5
+ * Provides the activate/deactivate system that returns full context
6
+ * to Claude — persona, guidelines, tool recommendations, setup status.
7
+ *
8
+ * Uses array-joined pattern because generated code contains template literals.
9
+ */
10
+ export function generateActivate(config: AgentConfig): string {
11
+ const facadeId = config.id.replace(/-/g, '_');
12
+ const _marker = `${config.id}:mode`;
13
+
14
+ // Build tool recommendations from config domains
15
+ const toolRecLines: string[] = [];
16
+ for (const d of config.domains) {
17
+ const toolName = `${facadeId}_${d.replace(/-/g, '_')}`;
18
+ toolRecLines.push(` { intent: 'search ${d}', facade: '${toolName}', op: 'search' },`);
19
+ toolRecLines.push(
20
+ ` { intent: '${d} patterns', facade: '${toolName}', op: 'get_patterns' },`,
21
+ );
22
+ toolRecLines.push(` { intent: 'capture ${d}', facade: '${toolName}', op: 'capture' },`);
23
+ }
24
+
25
+ // Build behavioral guidelines from config principles
26
+ const guidelineLines = config.principles
27
+ .map((p) => {
28
+ const escaped = p.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
29
+ return ` '${escaped}',`;
30
+ })
31
+ .join('\n');
32
+
33
+ return [
34
+ "import { join } from 'node:path';",
35
+ "import { homedir } from 'node:os';",
36
+ "import { PERSONA } from '../identity/persona.js';",
37
+ "import { hasAgentMarker } from './inject-claude-md.js';",
38
+ "import type { Vault, Planner, Plan } from '@soleri/core';",
39
+ '',
40
+ 'export interface ActivationResult {',
41
+ ' activated: boolean;',
42
+ ' persona: {',
43
+ ' name: string;',
44
+ ' role: string;',
45
+ ' description: string;',
46
+ ' greeting: string;',
47
+ ' };',
48
+ ' guidelines: string[];',
49
+ ' tool_recommendations: Array<{ intent: string; facade: string; op: string }>;',
50
+ ' session_instruction: string;',
51
+ ' setup_status: {',
52
+ ' claude_md_injected: boolean;',
53
+ ' global_claude_md_injected: boolean;',
54
+ ' vault_has_entries: boolean;',
55
+ ' vault_entry_count: number;',
56
+ ' };',
57
+ ' executing_plans: Array<{ id: string; objective: string; tasks: number; completed: number }>;',
58
+ ' next_steps: string[];',
59
+ '}',
60
+ '',
61
+ 'export interface DeactivationResult {',
62
+ ' deactivated: boolean;',
63
+ ' message: string;',
64
+ '}',
65
+ '',
66
+ '/**',
67
+ ` * Activate ${config.name} — returns full context for Claude to adopt the persona.`,
68
+ ' */',
69
+ 'export function activateAgent(vault: Vault, projectPath: string, planner?: Planner): ActivationResult {',
70
+ ' // Check CLAUDE.md injection status (project-level and global)',
71
+ " const projectClaudeMd = join(projectPath, 'CLAUDE.md');",
72
+ " const globalClaudeMd = join(homedir(), '.claude', 'CLAUDE.md');",
73
+ ' const claudeMdInjected = hasAgentMarker(projectClaudeMd);',
74
+ ' const globalClaudeMdInjected = hasAgentMarker(globalClaudeMd);',
75
+ '',
76
+ ' // Check vault status',
77
+ ' const stats = vault.stats();',
78
+ ' const vaultHasEntries = stats.totalEntries > 0;',
79
+ '',
80
+ " // Build next steps based on what's missing",
81
+ ' const nextSteps: string[] = [];',
82
+ ' if (!globalClaudeMdInjected && !claudeMdInjected) {',
83
+ ` nextSteps.push('No CLAUDE.md configured — run inject_claude_md with global: true for all projects, or without for this project only');`,
84
+ ' } else if (!globalClaudeMdInjected) {',
85
+ ` nextSteps.push('Global CLAUDE.md not configured — run inject_claude_md with global: true to enable activation in all projects');`,
86
+ ' }',
87
+ ' if (!vaultHasEntries) {',
88
+ " nextSteps.push('Vault is empty — start capturing knowledge with the domain capture ops');",
89
+ ' }',
90
+ ' if (nextSteps.length === 0) {',
91
+ ` nextSteps.push('All set! ${config.name} is fully integrated.');`,
92
+ ' }',
93
+ '',
94
+ ' // Check for executing plans',
95
+ ' const executingPlans = planner ? planner.getExecuting().map((p) => ({',
96
+ ' id: p.id,',
97
+ ' objective: p.objective,',
98
+ ' tasks: p.tasks.length,',
99
+ " completed: p.tasks.filter((t) => t.status === 'completed').length,",
100
+ ' })) : [];',
101
+ ' if (executingPlans.length > 0) {',
102
+ ' nextSteps.unshift(`${executingPlans.length} plan(s) in progress — use get_plan to review`);',
103
+ ' }',
104
+ '',
105
+ ' return {',
106
+ ' activated: true,',
107
+ ' persona: {',
108
+ ' name: PERSONA.name,',
109
+ ' role: PERSONA.role,',
110
+ ' description: PERSONA.description,',
111
+ ' greeting: PERSONA.greeting,',
112
+ ' },',
113
+ ' guidelines: [',
114
+ guidelineLines,
115
+ ' ],',
116
+ ' tool_recommendations: [',
117
+ ` { intent: 'health check', facade: '${facadeId}_core', op: 'health' },`,
118
+ ` { intent: 'search all', facade: '${facadeId}_core', op: 'search' },`,
119
+ ` { intent: 'vault stats', facade: '${facadeId}_core', op: 'vault_stats' },`,
120
+ ...toolRecLines,
121
+ ' ],',
122
+ ` session_instruction: 'You are now ' + PERSONA.name + ', a ' + PERSONA.role + '. Stay in character for the ENTIRE session. ' +`,
123
+ ` 'Reference patterns from the knowledge vault. Provide concrete examples. Flag anti-patterns with severity.',`,
124
+ ' setup_status: {',
125
+ ' claude_md_injected: claudeMdInjected,',
126
+ ' global_claude_md_injected: globalClaudeMdInjected,',
127
+ ' vault_has_entries: vaultHasEntries,',
128
+ ' vault_entry_count: stats.totalEntries,',
129
+ ' },',
130
+ ' executing_plans: executingPlans,',
131
+ ' next_steps: nextSteps,',
132
+ ' };',
133
+ '}',
134
+ '',
135
+ '/**',
136
+ ` * Deactivate ${config.name} — drops persona for the session.`,
137
+ ' */',
138
+ 'export function deactivateAgent(): DeactivationResult {',
139
+ ' return {',
140
+ ' deactivated: true,',
141
+ ` message: 'Goodbye! ' + PERSONA.name + ' persona deactivated. Reverting to default behavior.',`,
142
+ ' };',
143
+ '}',
144
+ ].join('\n');
145
+ }
@@ -0,0 +1,137 @@
1
+ import type { AgentConfig } from '../types.js';
2
+
3
+ /**
4
+ * Generates src/activation/claude-md-content.ts for a new agent.
5
+ * Returns the CLAUDE.md section content with activation triggers,
6
+ * facade tables, intent detection, and knowledge protocol.
7
+ *
8
+ * Strategy: build the markdown as plain strings, then escape into
9
+ * single-quoted TypeScript string literals. This avoids nested
10
+ * backtick/template-literal escaping nightmares entirely.
11
+ */
12
+ export function generateClaudeMdTemplate(config: AgentConfig): string {
13
+ const facadeId = config.id.replace(/-/g, '_');
14
+ const marker = `${config.id}:mode`;
15
+ const bt = '`'; // backtick — keeps template clean
16
+
17
+ // Build the raw markdown lines (plain text, no escaping needed)
18
+ const mdLines: string[] = [
19
+ `<!-- ${marker} -->`,
20
+ '',
21
+ `# ${config.name} Mode`,
22
+ '',
23
+ `## ${config.name} Integration`,
24
+ '',
25
+ `**Activate:** "Hello, ${config.name}!" \u2192 ${bt}${facadeId}_core op:activate params:{ projectPath: "." }${bt}`,
26
+ `**Deactivate:** "Goodbye, ${config.name}!" \u2192 ${bt}${facadeId}_core op:activate params:{ deactivate: true }${bt}`,
27
+ '',
28
+ 'On activation, adopt the returned persona. Stay in character until deactivated.',
29
+ '',
30
+ '## Session Start',
31
+ '',
32
+ `On every new session: ${bt}${facadeId}_core op:register params:{ projectPath: "." }${bt}`,
33
+ '',
34
+ '## Facades',
35
+ '',
36
+ '| Need | Facade | Op |',
37
+ '|------|--------|----|',
38
+ `| Health check | ${bt}${facadeId}_core${bt} | ${bt}health${bt} |`,
39
+ `| Search all | ${bt}${facadeId}_core${bt} | ${bt}search${bt} |`,
40
+ `| Vault stats | ${bt}${facadeId}_core${bt} | ${bt}vault_stats${bt} |`,
41
+ `| Identity | ${bt}${facadeId}_core${bt} | ${bt}identity${bt} |`,
42
+ ];
43
+
44
+ // Domain-specific facade rows
45
+ for (const d of config.domains) {
46
+ const toolName = `${facadeId}_${d.replace(/-/g, '_')}`;
47
+ mdLines.push(`| ${d} patterns | ${bt}${toolName}${bt} | ${bt}get_patterns${bt} |`);
48
+ mdLines.push(`| Search ${d} | ${bt}${toolName}${bt} | ${bt}search${bt} |`);
49
+ mdLines.push(`| Capture ${d} | ${bt}${toolName}${bt} | ${bt}capture${bt} |`);
50
+ }
51
+
52
+ // Memory + Session + Export + Brain + Planning rows
53
+ mdLines.push(
54
+ `| Memory search | ${bt}${facadeId}_core${bt} | ${bt}memory_search${bt} |`,
55
+ `| Memory capture | ${bt}${facadeId}_core${bt} | ${bt}memory_capture${bt} |`,
56
+ `| Memory list | ${bt}${facadeId}_core${bt} | ${bt}memory_list${bt} |`,
57
+ `| Session capture | ${bt}${facadeId}_core${bt} | ${bt}session_capture${bt} |`,
58
+ `| Export knowledge | ${bt}${facadeId}_core${bt} | ${bt}export${bt} |`,
59
+ `| Record feedback | ${bt}${facadeId}_core${bt} | ${bt}record_feedback${bt} |`,
60
+ `| Rebuild vocabulary | ${bt}${facadeId}_core${bt} | ${bt}rebuild_vocabulary${bt} |`,
61
+ `| Brain stats | ${bt}${facadeId}_core${bt} | ${bt}brain_stats${bt} |`,
62
+ `| LLM status | ${bt}${facadeId}_core${bt} | ${bt}llm_status${bt} |`,
63
+ `| Create plan | ${bt}${facadeId}_core${bt} | ${bt}create_plan${bt} |`,
64
+ `| Get plan | ${bt}${facadeId}_core${bt} | ${bt}get_plan${bt} |`,
65
+ `| Approve plan | ${bt}${facadeId}_core${bt} | ${bt}approve_plan${bt} |`,
66
+ `| Update task | ${bt}${facadeId}_core${bt} | ${bt}update_task${bt} |`,
67
+ `| Complete plan | ${bt}${facadeId}_core${bt} | ${bt}complete_plan${bt} |`,
68
+ );
69
+
70
+ mdLines.push(
71
+ '',
72
+ '## Intent Detection',
73
+ '',
74
+ '| Signal | Intent |',
75
+ '|--------|--------|',
76
+ '| Problem described ("broken", "janky", "weird") | FIX |',
77
+ '| Need expressed ("I need", "we should have") | BUILD |',
78
+ '| Quality questioned ("is this right?") | REVIEW |',
79
+ '| Advice sought ("how should I", "best way") | PLAN |',
80
+ '| Improvement requested ("make it faster") | IMPROVE |',
81
+ '',
82
+ '## Knowledge Protocol',
83
+ '',
84
+ 'When seeking guidance: vault before codebase before web.',
85
+ '',
86
+ `1. Search vault \u2014 ${bt}${facadeId}_core op:search${bt}`,
87
+ '2. Codebase \u2014 only if vault has nothing',
88
+ '3. Web \u2014 last resort',
89
+ '',
90
+ '## Knowledge Capture',
91
+ '',
92
+ 'When learning something that should persist, use the domain capture ops.',
93
+ '',
94
+ '## Session Capture',
95
+ '',
96
+ 'A PreCompact hook is configured to call `session_capture` before context compaction.',
97
+ 'This automatically preserves session summaries as memories for future sessions.',
98
+ `To manually capture: ${bt}${facadeId}_core op:session_capture params:{ summary: "..." }${bt}`,
99
+ '',
100
+ '## Planning',
101
+ '',
102
+ 'For multi-step tasks, use the planning system:',
103
+ `1. Create: ${bt}${facadeId}_core op:create_plan params:{ objective: "...", scope: "...", tasks: [...] }${bt}`,
104
+ `2. Approve: ${bt}${facadeId}_core op:approve_plan params:{ planId: "...", startExecution: true }${bt}`,
105
+ `3. Track: ${bt}${facadeId}_core op:update_task params:{ planId: "...", taskId: "...", status: "completed" }${bt}`,
106
+ `4. Complete: ${bt}${facadeId}_core op:complete_plan params:{ planId: "..." }${bt}`,
107
+ '',
108
+ 'Check activation response for recovered plans in `executing` state — remind the user.',
109
+ '',
110
+ `<!-- /${marker} -->`,
111
+ );
112
+
113
+ // Escape each markdown line for use in a single-quoted TS string literal
114
+ const quotedLines = mdLines.map((line) => {
115
+ const escaped = line.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
116
+ return ` '${escaped}',`;
117
+ });
118
+
119
+ // Build the generated TypeScript file
120
+ const tsLines = [
121
+ '/**',
122
+ ` * CLAUDE.md content for ${config.name}.`,
123
+ ' * Generated by Soleri — do not edit manually.',
124
+ ' */',
125
+ 'export function getClaudeMdContent(): string {',
126
+ ' return [',
127
+ ...quotedLines,
128
+ " ].join('\\n');",
129
+ '}',
130
+ '',
131
+ 'export function getClaudeMdMarker(): string {',
132
+ ` return '${marker}';`,
133
+ '}',
134
+ ];
135
+
136
+ return tsLines.join('\n');
137
+ }