@soleri/forge 0.0.1 → 3.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.
- package/CHANGELOG.md +98 -0
- package/README.md +199 -0
- package/dist/facades/forge.facade.d.ts +9 -0
- package/dist/facades/forge.facade.js +134 -0
- package/dist/facades/forge.facade.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge-installer.d.ts +30 -0
- package/dist/knowledge-installer.js +437 -0
- package/dist/knowledge-installer.js.map +1 -0
- package/dist/scaffolder.d.ts +13 -0
- package/dist/scaffolder.js +420 -0
- package/dist/scaffolder.js.map +1 -0
- package/dist/templates/activate.d.ts +9 -0
- package/dist/templates/activate.js +140 -0
- package/dist/templates/activate.js.map +1 -0
- package/dist/templates/brain.d.ts +6 -0
- package/dist/templates/brain.js +478 -0
- package/dist/templates/brain.js.map +1 -0
- package/dist/templates/claude-md-template.d.ts +11 -0
- package/dist/templates/claude-md-template.js +73 -0
- package/dist/templates/claude-md-template.js.map +1 -0
- package/dist/templates/core-facade.d.ts +6 -0
- package/dist/templates/core-facade.js +461 -0
- package/dist/templates/core-facade.js.map +1 -0
- package/dist/templates/domain-facade.d.ts +7 -0
- package/dist/templates/domain-facade.js +121 -0
- package/dist/templates/domain-facade.js.map +1 -0
- package/dist/templates/entry-point.d.ts +5 -0
- package/dist/templates/entry-point.js +121 -0
- package/dist/templates/entry-point.js.map +1 -0
- package/dist/templates/facade-factory.d.ts +1 -0
- package/dist/templates/facade-factory.js +63 -0
- package/dist/templates/facade-factory.js.map +1 -0
- package/dist/templates/facade-types.d.ts +1 -0
- package/dist/templates/facade-types.js +46 -0
- package/dist/templates/facade-types.js.map +1 -0
- package/dist/templates/inject-claude-md.d.ts +11 -0
- package/dist/templates/inject-claude-md.js +92 -0
- package/dist/templates/inject-claude-md.js.map +1 -0
- package/dist/templates/intelligence-loader.d.ts +1 -0
- package/dist/templates/intelligence-loader.js +43 -0
- package/dist/templates/intelligence-loader.js.map +1 -0
- package/dist/templates/intelligence-types.d.ts +1 -0
- package/dist/templates/intelligence-types.js +24 -0
- package/dist/templates/intelligence-types.js.map +1 -0
- package/dist/templates/llm-client.d.ts +7 -0
- package/dist/templates/llm-client.js +301 -0
- package/dist/templates/llm-client.js.map +1 -0
- package/dist/templates/llm-key-pool.d.ts +7 -0
- package/dist/templates/llm-key-pool.js +211 -0
- package/dist/templates/llm-key-pool.js.map +1 -0
- package/dist/templates/llm-types.d.ts +5 -0
- package/dist/templates/llm-types.js +161 -0
- package/dist/templates/llm-types.js.map +1 -0
- package/dist/templates/llm-utils.d.ts +5 -0
- package/dist/templates/llm-utils.js +260 -0
- package/dist/templates/llm-utils.js.map +1 -0
- package/dist/templates/package-json.d.ts +2 -0
- package/dist/templates/package-json.js +38 -0
- package/dist/templates/package-json.js.map +1 -0
- package/dist/templates/persona.d.ts +2 -0
- package/dist/templates/persona.js +42 -0
- package/dist/templates/persona.js.map +1 -0
- package/dist/templates/planner.d.ts +5 -0
- package/dist/templates/planner.js +150 -0
- package/dist/templates/planner.js.map +1 -0
- package/dist/templates/readme.d.ts +5 -0
- package/dist/templates/readme.js +316 -0
- package/dist/templates/readme.js.map +1 -0
- package/dist/templates/setup-script.d.ts +6 -0
- package/dist/templates/setup-script.js +112 -0
- package/dist/templates/setup-script.js.map +1 -0
- package/dist/templates/test-brain.d.ts +6 -0
- package/dist/templates/test-brain.js +474 -0
- package/dist/templates/test-brain.js.map +1 -0
- package/dist/templates/test-facades.d.ts +6 -0
- package/dist/templates/test-facades.js +652 -0
- package/dist/templates/test-facades.js.map +1 -0
- package/dist/templates/test-llm.d.ts +7 -0
- package/dist/templates/test-llm.js +574 -0
- package/dist/templates/test-llm.js.map +1 -0
- package/dist/templates/test-loader.d.ts +5 -0
- package/dist/templates/test-loader.js +146 -0
- package/dist/templates/test-loader.js.map +1 -0
- package/dist/templates/test-planner.d.ts +5 -0
- package/dist/templates/test-planner.js +271 -0
- package/dist/templates/test-planner.js.map +1 -0
- package/dist/templates/test-vault.d.ts +5 -0
- package/dist/templates/test-vault.js +380 -0
- package/dist/templates/test-vault.js.map +1 -0
- package/dist/templates/tsconfig.d.ts +1 -0
- package/dist/templates/tsconfig.js +25 -0
- package/dist/templates/tsconfig.js.map +1 -0
- package/dist/templates/vault.d.ts +5 -0
- package/dist/templates/vault.js +263 -0
- package/dist/templates/vault.js.map +1 -0
- package/dist/templates/vitest-config.d.ts +1 -0
- package/dist/templates/vitest-config.js +27 -0
- package/dist/templates/vitest-config.js.map +1 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -0
- package/package.json +42 -4
- package/src/__tests__/knowledge-installer.test.ts +805 -0
- package/src/__tests__/scaffolder.test.ts +335 -0
- package/src/facades/forge.facade.ts +150 -0
- package/src/index.ts +101 -0
- package/src/knowledge-installer.ts +532 -0
- package/src/scaffolder.ts +482 -0
- package/src/templates/activate.ts +146 -0
- package/src/templates/brain.ts +478 -0
- package/src/templates/claude-md-template.ts +137 -0
- package/src/templates/core-facade.ts +462 -0
- package/src/templates/domain-facade.ts +123 -0
- package/src/templates/entry-point.ts +125 -0
- package/src/templates/facade-factory.ts +62 -0
- package/src/templates/facade-types.ts +45 -0
- package/src/templates/inject-claude-md.ts +94 -0
- package/src/templates/intelligence-loader.ts +42 -0
- package/src/templates/intelligence-types.ts +23 -0
- package/src/templates/llm-client.ts +302 -0
- package/src/templates/llm-key-pool.ts +212 -0
- package/src/templates/llm-types.ts +160 -0
- package/src/templates/llm-utils.ts +259 -0
- package/src/templates/package-json.ts +40 -0
- package/src/templates/persona.ts +45 -0
- package/src/templates/planner.ts +150 -0
- package/src/templates/readme.ts +319 -0
- package/src/templates/setup-script.ts +113 -0
- package/src/templates/test-brain.ts +474 -0
- package/src/templates/test-facades.ts +659 -0
- package/src/templates/test-llm.ts +575 -0
- package/src/templates/test-loader.ts +146 -0
- package/src/templates/test-planner.ts +271 -0
- package/src/templates/test-vault.ts +380 -0
- package/src/templates/tsconfig.ts +25 -0
- package/src/templates/vault.ts +263 -0
- package/src/templates/vitest-config.ts +26 -0
- package/src/types.ts +68 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,482 @@
|
|
|
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 { generateFacadeTypes } from './templates/facade-types.js';
|
|
17
|
+
import { generateFacadeFactory } from './templates/facade-factory.js';
|
|
18
|
+
import { generateVault } from './templates/vault.js';
|
|
19
|
+
import { generateIntelligenceTypes } from './templates/intelligence-types.js';
|
|
20
|
+
import { generateIntelligenceLoader } from './templates/intelligence-loader.js';
|
|
21
|
+
import { generatePersona } from './templates/persona.js';
|
|
22
|
+
import { generateDomainFacade } from './templates/domain-facade.js';
|
|
23
|
+
import { generateCoreFacade } from './templates/core-facade.js';
|
|
24
|
+
import { generateEntryPoint } from './templates/entry-point.js';
|
|
25
|
+
import { generateVaultTest } from './templates/test-vault.js';
|
|
26
|
+
import { generateLoaderTest } from './templates/test-loader.js';
|
|
27
|
+
import { generateFacadesTest } from './templates/test-facades.js';
|
|
28
|
+
import { generateClaudeMdTemplate } from './templates/claude-md-template.js';
|
|
29
|
+
import { generateInjectClaudeMd } from './templates/inject-claude-md.js';
|
|
30
|
+
import { generateActivate } from './templates/activate.js';
|
|
31
|
+
import { generateReadme } from './templates/readme.js';
|
|
32
|
+
import { generateSetupScript } from './templates/setup-script.js';
|
|
33
|
+
import { generatePlanner } from './templates/planner.js';
|
|
34
|
+
import { generatePlannerTest } from './templates/test-planner.js';
|
|
35
|
+
import { generateBrain } from './templates/brain.js';
|
|
36
|
+
import { generateBrainTest } from './templates/test-brain.js';
|
|
37
|
+
import { generateLLMTypes } from './templates/llm-types.js';
|
|
38
|
+
import { generateLLMUtils } from './templates/llm-utils.js';
|
|
39
|
+
import { generateLLMKeyPool } from './templates/llm-key-pool.js';
|
|
40
|
+
import { generateLLMClient } from './templates/llm-client.js';
|
|
41
|
+
import { generateLLMTest } from './templates/test-llm.js';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Preview what scaffold will create without writing anything.
|
|
45
|
+
*/
|
|
46
|
+
export function previewScaffold(config: AgentConfig): ScaffoldPreview {
|
|
47
|
+
const agentDir = join(config.outputDir, config.id);
|
|
48
|
+
|
|
49
|
+
const files = [
|
|
50
|
+
{ path: 'package.json', description: 'NPM package with MCP SDK, SQLite, Zod dependencies' },
|
|
51
|
+
{ path: 'tsconfig.json', description: 'TypeScript config (ES2022, NodeNext, strict)' },
|
|
52
|
+
{ path: 'vitest.config.ts', description: 'Test config (vitest, forks pool, coverage)' },
|
|
53
|
+
{ path: '.gitignore', description: 'Git ignore (node_modules, dist, coverage)' },
|
|
54
|
+
{
|
|
55
|
+
path: 'scripts/copy-assets.js',
|
|
56
|
+
description: 'Build script to copy intelligence data to dist',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
path: 'src/index.ts',
|
|
60
|
+
description:
|
|
61
|
+
'Entry point — initializes vault, planner, brain, registers facades, starts stdio',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
path: 'src/planning/planner.ts',
|
|
65
|
+
description: 'Plan state machine — draft → approved → executing → completed',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
path: 'src/brain/brain.ts',
|
|
69
|
+
description:
|
|
70
|
+
'Intelligence layer — TF-IDF scoring, auto-tagging, duplicate detection, adaptive weights',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
path: 'src/llm/types.ts',
|
|
74
|
+
description:
|
|
75
|
+
'LLM types — SecretString, LLMError, call options/result, circuit breaker, key pool, routing',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
path: 'src/llm/utils.ts',
|
|
79
|
+
description:
|
|
80
|
+
'LLM utilities — CircuitBreaker, retry with backoff+jitter, rate limit header parser',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
path: 'src/llm/key-pool.ts',
|
|
84
|
+
description:
|
|
85
|
+
'Key pool — multi-key rotation with per-key circuit breakers, preemptive quota rotation',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
path: 'src/llm/llm-client.ts',
|
|
89
|
+
description:
|
|
90
|
+
'LLM client — unified OpenAI/Anthropic caller with model routing (optional, needs API keys)',
|
|
91
|
+
},
|
|
92
|
+
{ path: 'src/facades/types.ts', description: 'Facade type system (OpHandler, FacadeConfig)' },
|
|
93
|
+
{
|
|
94
|
+
path: 'src/facades/facade-factory.ts',
|
|
95
|
+
description: 'Registers facades as MCP tools with op dispatch',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
path: 'src/facades/core.facade.ts',
|
|
99
|
+
description: 'Core facade — search, vault stats, health, identity',
|
|
100
|
+
},
|
|
101
|
+
...config.domains.map((d) => ({
|
|
102
|
+
path: `src/facades/${d}.facade.ts`,
|
|
103
|
+
description: `${d} facade — search, get_patterns, capture, remove`,
|
|
104
|
+
})),
|
|
105
|
+
{
|
|
106
|
+
path: 'src/vault/vault.ts',
|
|
107
|
+
description: 'SQLite vault with FTS5 full-text search (BM25 ranking)',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
path: 'src/intelligence/types.ts',
|
|
111
|
+
description: 'IntelligenceEntry and IntelligenceBundle types',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
path: 'src/intelligence/loader.ts',
|
|
115
|
+
description: 'Loads and validates JSON intelligence data files',
|
|
116
|
+
},
|
|
117
|
+
...config.domains.map((d) => ({
|
|
118
|
+
path: `src/intelligence/data/${d}.json`,
|
|
119
|
+
description: `Empty ${d} intelligence bundle — ready for knowledge capture`,
|
|
120
|
+
})),
|
|
121
|
+
{
|
|
122
|
+
path: 'src/identity/persona.ts',
|
|
123
|
+
description: `${config.name} persona — name, role, principles, greeting`,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
path: 'src/activation/claude-md-content.ts',
|
|
127
|
+
description: `${config.name} CLAUDE.md content with activation triggers and facades table`,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
path: 'src/activation/inject-claude-md.ts',
|
|
131
|
+
description: 'Idempotent CLAUDE.md injection — project-level or global (~/.claude/CLAUDE.md)',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
path: 'src/activation/activate.ts',
|
|
135
|
+
description: `${config.name} activation system — persona adoption, setup status, tool recommendations`,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
path: 'src/__tests__/vault.test.ts',
|
|
139
|
+
description: 'Vault tests — CRUD, FTS5 search, stats, project registration (32 tests)',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
path: 'src/__tests__/loader.test.ts',
|
|
143
|
+
description: 'Intelligence loader tests — valid/invalid JSON, edge cases (9 tests)',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
path: 'src/__tests__/facades.test.ts',
|
|
147
|
+
description: `Facade integration tests — all ${config.domains.length + 1} facades`,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
path: 'src/__tests__/planner.test.ts',
|
|
151
|
+
description: 'Planner tests — state machine, task lifecycle, persistence (~20 tests)',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
path: 'src/__tests__/brain.test.ts',
|
|
155
|
+
description:
|
|
156
|
+
'Brain tests — TF-IDF scoring, auto-tagging, duplicate detection, adaptive weights (~38 tests)',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
path: 'src/__tests__/llm.test.ts',
|
|
160
|
+
description:
|
|
161
|
+
'LLM tests — SecretString, CircuitBreaker, retry, rate limits, KeyPool, ModelRouter (~30 tests)',
|
|
162
|
+
},
|
|
163
|
+
{ path: '.mcp.json', description: 'MCP client config for connecting to this agent' },
|
|
164
|
+
{
|
|
165
|
+
path: 'README.md',
|
|
166
|
+
description: `${config.name} documentation — quick start, domains, principles, commands`,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
path: 'scripts/setup.sh',
|
|
170
|
+
description: 'Automated setup — Node.js check, build, Claude Code MCP registration',
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const facades = [
|
|
175
|
+
...config.domains.map((d) => ({
|
|
176
|
+
name: `${config.id}_${d.replace(/-/g, '_')}`,
|
|
177
|
+
ops: ['get_patterns', 'search', 'get_entry', 'capture', 'remove'],
|
|
178
|
+
})),
|
|
179
|
+
{
|
|
180
|
+
name: `${config.id}_core`,
|
|
181
|
+
ops: [
|
|
182
|
+
'search',
|
|
183
|
+
'vault_stats',
|
|
184
|
+
'list_all',
|
|
185
|
+
'health',
|
|
186
|
+
'identity',
|
|
187
|
+
'activate',
|
|
188
|
+
'inject_claude_md',
|
|
189
|
+
'setup',
|
|
190
|
+
'register',
|
|
191
|
+
'memory_search',
|
|
192
|
+
'memory_capture',
|
|
193
|
+
'memory_list',
|
|
194
|
+
'session_capture',
|
|
195
|
+
'export',
|
|
196
|
+
'create_plan',
|
|
197
|
+
'get_plan',
|
|
198
|
+
'approve_plan',
|
|
199
|
+
'update_task',
|
|
200
|
+
'complete_plan',
|
|
201
|
+
'record_feedback',
|
|
202
|
+
'rebuild_vocabulary',
|
|
203
|
+
'brain_stats',
|
|
204
|
+
'llm_status',
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
agentDir,
|
|
211
|
+
files,
|
|
212
|
+
facades,
|
|
213
|
+
domains: config.domains,
|
|
214
|
+
persona: { name: config.name, role: config.role },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Scaffold a complete MCP agent project.
|
|
220
|
+
*/
|
|
221
|
+
export function scaffold(config: AgentConfig): ScaffoldResult {
|
|
222
|
+
const agentDir = join(config.outputDir, config.id);
|
|
223
|
+
const filesCreated: string[] = [];
|
|
224
|
+
|
|
225
|
+
if (existsSync(agentDir)) {
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
agentDir,
|
|
229
|
+
filesCreated: [],
|
|
230
|
+
domains: config.domains,
|
|
231
|
+
summary: `Directory already exists: ${agentDir}. Choose a different ID or remove the existing directory.`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Create directory structure
|
|
236
|
+
const dirs = [
|
|
237
|
+
'',
|
|
238
|
+
'scripts',
|
|
239
|
+
'src',
|
|
240
|
+
'src/facades',
|
|
241
|
+
'src/vault',
|
|
242
|
+
'src/intelligence',
|
|
243
|
+
'src/intelligence/data',
|
|
244
|
+
'src/identity',
|
|
245
|
+
'src/activation',
|
|
246
|
+
'src/planning',
|
|
247
|
+
'src/brain',
|
|
248
|
+
'src/llm',
|
|
249
|
+
'src/__tests__',
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
for (const dir of dirs) {
|
|
253
|
+
mkdirSync(join(agentDir, dir), { recursive: true });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Write project config files
|
|
257
|
+
const projectFiles: Array<[string, string]> = [
|
|
258
|
+
['package.json', generatePackageJson(config)],
|
|
259
|
+
['tsconfig.json', generateTsconfig()],
|
|
260
|
+
['vitest.config.ts', generateVitestConfig()],
|
|
261
|
+
['.gitignore', 'node_modules/\ndist/\ncoverage/\n*.tsbuildinfo\n.env\n.DS_Store\n*.log\n'],
|
|
262
|
+
[
|
|
263
|
+
'.mcp.json',
|
|
264
|
+
JSON.stringify(
|
|
265
|
+
{ mcpServers: { [config.id]: { command: 'node', args: ['dist/index.js'], cwd: '.' } } },
|
|
266
|
+
null,
|
|
267
|
+
2,
|
|
268
|
+
),
|
|
269
|
+
],
|
|
270
|
+
['scripts/copy-assets.js', generateCopyAssetsScript()],
|
|
271
|
+
['README.md', generateReadme(config)],
|
|
272
|
+
['scripts/setup.sh', generateSetupScript(config)],
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
for (const [path, content] of projectFiles) {
|
|
276
|
+
writeFileSync(join(agentDir, path), content, 'utf-8');
|
|
277
|
+
filesCreated.push(path);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Make setup script executable
|
|
281
|
+
chmodSync(join(agentDir, 'scripts', 'setup.sh'), 0o755);
|
|
282
|
+
|
|
283
|
+
// Write source files
|
|
284
|
+
const sourceFiles: Array<[string, string]> = [
|
|
285
|
+
['src/facades/types.ts', generateFacadeTypes()],
|
|
286
|
+
['src/facades/facade-factory.ts', generateFacadeFactory()],
|
|
287
|
+
['src/facades/core.facade.ts', generateCoreFacade(config)],
|
|
288
|
+
['src/vault/vault.ts', generateVault()],
|
|
289
|
+
['src/intelligence/types.ts', generateIntelligenceTypes()],
|
|
290
|
+
['src/intelligence/loader.ts', generateIntelligenceLoader()],
|
|
291
|
+
['src/identity/persona.ts', generatePersona(config)],
|
|
292
|
+
['src/activation/claude-md-content.ts', generateClaudeMdTemplate(config)],
|
|
293
|
+
['src/activation/inject-claude-md.ts', generateInjectClaudeMd(config)],
|
|
294
|
+
['src/activation/activate.ts', generateActivate(config)],
|
|
295
|
+
['src/index.ts', generateEntryPoint(config)],
|
|
296
|
+
['src/planning/planner.ts', generatePlanner()],
|
|
297
|
+
['src/brain/brain.ts', generateBrain()],
|
|
298
|
+
['src/llm/types.ts', generateLLMTypes()],
|
|
299
|
+
['src/llm/utils.ts', generateLLMUtils()],
|
|
300
|
+
['src/llm/key-pool.ts', generateLLMKeyPool(config)],
|
|
301
|
+
['src/llm/llm-client.ts', generateLLMClient(config)],
|
|
302
|
+
['src/__tests__/vault.test.ts', generateVaultTest()],
|
|
303
|
+
['src/__tests__/loader.test.ts', generateLoaderTest()],
|
|
304
|
+
['src/__tests__/facades.test.ts', generateFacadesTest(config)],
|
|
305
|
+
['src/__tests__/planner.test.ts', generatePlannerTest()],
|
|
306
|
+
['src/__tests__/brain.test.ts', generateBrainTest()],
|
|
307
|
+
['src/__tests__/llm.test.ts', generateLLMTest(config)],
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
// Domain facades and empty data files
|
|
311
|
+
for (const domain of config.domains) {
|
|
312
|
+
sourceFiles.push([`src/facades/${domain}.facade.ts`, generateDomainFacade(config.id, domain)]);
|
|
313
|
+
sourceFiles.push([`src/intelligence/data/${domain}.json`, generateEmptyBundle(domain)]);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const [path, content] of sourceFiles) {
|
|
317
|
+
writeFileSync(join(agentDir, path), content, 'utf-8');
|
|
318
|
+
filesCreated.push(path);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const totalOps = config.domains.length * 5 + 24; // 5 per domain + 24 core (activation, registration, memory, session, export, planning, brain, llm)
|
|
322
|
+
|
|
323
|
+
// Register the agent as an MCP server in ~/.claude.json
|
|
324
|
+
const mcpReg = registerMcpServer(config.id, agentDir);
|
|
325
|
+
|
|
326
|
+
const summaryLines = [
|
|
327
|
+
`Created ${config.name} agent at ${agentDir}`,
|
|
328
|
+
`${config.domains.length + 1} facades with ${totalOps} operations`,
|
|
329
|
+
`${config.domains.length} empty knowledge domains ready for capture`,
|
|
330
|
+
`Intelligence layer (Brain) — TF-IDF scoring, auto-tagging, duplicate detection`,
|
|
331
|
+
`Activation system included — say "Hello, ${config.name}!" to activate`,
|
|
332
|
+
`6 test suites — vault, loader, facades, planner, brain, llm`,
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
if (mcpReg.registered) {
|
|
336
|
+
summaryLines.push(`MCP server registered in ${mcpReg.path}`);
|
|
337
|
+
} else {
|
|
338
|
+
summaryLines.push(`Warning: Failed to register MCP server in ${mcpReg.path}: ${mcpReg.error}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
summaryLines.push(
|
|
342
|
+
'',
|
|
343
|
+
'Next steps:',
|
|
344
|
+
` cd ${agentDir}`,
|
|
345
|
+
' npm install && npm run build',
|
|
346
|
+
' npm test # verify all tests pass',
|
|
347
|
+
' Restart Claude Code',
|
|
348
|
+
` Say "Hello, ${config.name}!" to activate the persona`,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
success: true,
|
|
353
|
+
agentDir,
|
|
354
|
+
filesCreated,
|
|
355
|
+
domains: config.domains,
|
|
356
|
+
summary: summaryLines.join('\n'),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* List agents in a directory.
|
|
362
|
+
*/
|
|
363
|
+
export function listAgents(parentDir: string): AgentInfo[] {
|
|
364
|
+
if (!existsSync(parentDir)) return [];
|
|
365
|
+
|
|
366
|
+
const agents: AgentInfo[] = [];
|
|
367
|
+
let entries: string[];
|
|
368
|
+
try {
|
|
369
|
+
entries = readdirSync(parentDir, { withFileTypes: true })
|
|
370
|
+
.filter((e) => e.isDirectory())
|
|
371
|
+
.map((e) => e.name);
|
|
372
|
+
} catch {
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const name of entries) {
|
|
377
|
+
const dir = join(parentDir, name);
|
|
378
|
+
const pkgPath = join(dir, 'package.json');
|
|
379
|
+
if (!existsSync(pkgPath)) continue;
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
383
|
+
if (!pkg.name?.endsWith('-mcp')) continue;
|
|
384
|
+
|
|
385
|
+
const dataDir = join(dir, 'src', 'intelligence', 'data');
|
|
386
|
+
let domains: string[] = [];
|
|
387
|
+
try {
|
|
388
|
+
domains = readdirSync(dataDir)
|
|
389
|
+
.filter((f) => f.endsWith('.json'))
|
|
390
|
+
.map((f) => f.replace('.json', ''));
|
|
391
|
+
} catch {
|
|
392
|
+
/* empty */
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
agents.push({
|
|
396
|
+
id: name,
|
|
397
|
+
name: pkg.name.replace('-mcp', ''),
|
|
398
|
+
role: pkg.description || '',
|
|
399
|
+
path: dir,
|
|
400
|
+
domains,
|
|
401
|
+
hasNodeModules: existsSync(join(dir, 'node_modules')),
|
|
402
|
+
hasDistDir: existsSync(join(dir, 'dist')),
|
|
403
|
+
});
|
|
404
|
+
} catch {
|
|
405
|
+
/* skip non-agent directories */
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return agents;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Register the agent as an MCP server in ~/.claude.json (User MCPs).
|
|
414
|
+
* Idempotent — updates existing entry if present.
|
|
415
|
+
*/
|
|
416
|
+
function registerMcpServer(
|
|
417
|
+
agentId: string,
|
|
418
|
+
agentDir: string,
|
|
419
|
+
): { registered: boolean; path: string; error?: string } {
|
|
420
|
+
const claudeJsonPath = join(homedir(), '.claude.json');
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
let config: Record<string, unknown> = {};
|
|
424
|
+
|
|
425
|
+
if (existsSync(claudeJsonPath)) {
|
|
426
|
+
config = JSON.parse(readFileSync(claudeJsonPath, 'utf-8'));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
|
|
430
|
+
config.mcpServers = {};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const servers = config.mcpServers as Record<string, unknown>;
|
|
434
|
+
servers[agentId] = {
|
|
435
|
+
type: 'stdio',
|
|
436
|
+
command: 'node',
|
|
437
|
+
args: [join(agentDir, 'dist', 'index.js')],
|
|
438
|
+
env: {},
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
writeFileSync(claudeJsonPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
442
|
+
return { registered: true, path: claudeJsonPath };
|
|
443
|
+
} catch (err) {
|
|
444
|
+
return {
|
|
445
|
+
registered: false,
|
|
446
|
+
path: claudeJsonPath,
|
|
447
|
+
error: err instanceof Error ? err.message : String(err),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function generateEmptyBundle(domain: string): string {
|
|
453
|
+
return JSON.stringify(
|
|
454
|
+
{
|
|
455
|
+
domain,
|
|
456
|
+
version: '1.0.0',
|
|
457
|
+
entries: [],
|
|
458
|
+
},
|
|
459
|
+
null,
|
|
460
|
+
2,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function generateCopyAssetsScript(): string {
|
|
465
|
+
return [
|
|
466
|
+
"import { cpSync, existsSync, mkdirSync } from 'node:fs';",
|
|
467
|
+
"import { join, dirname } from 'node:path';",
|
|
468
|
+
"import { fileURLToPath } from 'node:url';",
|
|
469
|
+
'',
|
|
470
|
+
'const __dirname = dirname(fileURLToPath(import.meta.url));',
|
|
471
|
+
"const root = join(__dirname, '..');",
|
|
472
|
+
"const dist = join(root, 'dist');",
|
|
473
|
+
"const dataSource = join(root, 'src', 'intelligence', 'data');",
|
|
474
|
+
"const dataDest = join(dist, 'intelligence', 'data');",
|
|
475
|
+
'',
|
|
476
|
+
'if (existsSync(dataSource)) {',
|
|
477
|
+
' mkdirSync(dataDest, { recursive: true });',
|
|
478
|
+
' cpSync(dataSource, dataDest, { recursive: true });',
|
|
479
|
+
" console.log('Copied intelligence data to dist/');",
|
|
480
|
+
'}',
|
|
481
|
+
].join('\n');
|
|
482
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
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 } from '../vault/vault.js';",
|
|
39
|
+
"import type { Planner, Plan } from '../planning/planner.js';",
|
|
40
|
+
'',
|
|
41
|
+
'export interface ActivationResult {',
|
|
42
|
+
' activated: boolean;',
|
|
43
|
+
' persona: {',
|
|
44
|
+
' name: string;',
|
|
45
|
+
' role: string;',
|
|
46
|
+
' description: string;',
|
|
47
|
+
' greeting: string;',
|
|
48
|
+
' };',
|
|
49
|
+
' guidelines: string[];',
|
|
50
|
+
' tool_recommendations: Array<{ intent: string; facade: string; op: string }>;',
|
|
51
|
+
' session_instruction: string;',
|
|
52
|
+
' setup_status: {',
|
|
53
|
+
' claude_md_injected: boolean;',
|
|
54
|
+
' global_claude_md_injected: boolean;',
|
|
55
|
+
' vault_has_entries: boolean;',
|
|
56
|
+
' vault_entry_count: number;',
|
|
57
|
+
' };',
|
|
58
|
+
' executing_plans: Array<{ id: string; objective: string; tasks: number; completed: number }>;',
|
|
59
|
+
' next_steps: string[];',
|
|
60
|
+
'}',
|
|
61
|
+
'',
|
|
62
|
+
'export interface DeactivationResult {',
|
|
63
|
+
' deactivated: boolean;',
|
|
64
|
+
' message: string;',
|
|
65
|
+
'}',
|
|
66
|
+
'',
|
|
67
|
+
'/**',
|
|
68
|
+
` * Activate ${config.name} — returns full context for Claude to adopt the persona.`,
|
|
69
|
+
' */',
|
|
70
|
+
'export function activateAgent(vault: Vault, projectPath: string, planner?: Planner): ActivationResult {',
|
|
71
|
+
' // Check CLAUDE.md injection status (project-level and global)',
|
|
72
|
+
" const projectClaudeMd = join(projectPath, 'CLAUDE.md');",
|
|
73
|
+
" const globalClaudeMd = join(homedir(), '.claude', 'CLAUDE.md');",
|
|
74
|
+
' const claudeMdInjected = hasAgentMarker(projectClaudeMd);',
|
|
75
|
+
' const globalClaudeMdInjected = hasAgentMarker(globalClaudeMd);',
|
|
76
|
+
'',
|
|
77
|
+
' // Check vault status',
|
|
78
|
+
' const stats = vault.stats();',
|
|
79
|
+
' const vaultHasEntries = stats.totalEntries > 0;',
|
|
80
|
+
'',
|
|
81
|
+
" // Build next steps based on what's missing",
|
|
82
|
+
' const nextSteps: string[] = [];',
|
|
83
|
+
' if (!globalClaudeMdInjected && !claudeMdInjected) {',
|
|
84
|
+
` nextSteps.push('No CLAUDE.md configured — run inject_claude_md with global: true for all projects, or without for this project only');`,
|
|
85
|
+
' } else if (!globalClaudeMdInjected) {',
|
|
86
|
+
` nextSteps.push('Global CLAUDE.md not configured — run inject_claude_md with global: true to enable activation in all projects');`,
|
|
87
|
+
' }',
|
|
88
|
+
' if (!vaultHasEntries) {',
|
|
89
|
+
" nextSteps.push('Vault is empty — start capturing knowledge with the domain capture ops');",
|
|
90
|
+
' }',
|
|
91
|
+
' if (nextSteps.length === 0) {',
|
|
92
|
+
` nextSteps.push('All set! ${config.name} is fully integrated.');`,
|
|
93
|
+
' }',
|
|
94
|
+
'',
|
|
95
|
+
' // Check for executing plans',
|
|
96
|
+
' const executingPlans = planner ? planner.getExecuting().map((p) => ({',
|
|
97
|
+
' id: p.id,',
|
|
98
|
+
' objective: p.objective,',
|
|
99
|
+
' tasks: p.tasks.length,',
|
|
100
|
+
" completed: p.tasks.filter((t) => t.status === 'completed').length,",
|
|
101
|
+
' })) : [];',
|
|
102
|
+
' if (executingPlans.length > 0) {',
|
|
103
|
+
' nextSteps.unshift(`${executingPlans.length} plan(s) in progress — use get_plan to review`);',
|
|
104
|
+
' }',
|
|
105
|
+
'',
|
|
106
|
+
' return {',
|
|
107
|
+
' activated: true,',
|
|
108
|
+
' persona: {',
|
|
109
|
+
' name: PERSONA.name,',
|
|
110
|
+
' role: PERSONA.role,',
|
|
111
|
+
' description: PERSONA.description,',
|
|
112
|
+
' greeting: PERSONA.greeting,',
|
|
113
|
+
' },',
|
|
114
|
+
' guidelines: [',
|
|
115
|
+
guidelineLines,
|
|
116
|
+
' ],',
|
|
117
|
+
' tool_recommendations: [',
|
|
118
|
+
` { intent: 'health check', facade: '${facadeId}_core', op: 'health' },`,
|
|
119
|
+
` { intent: 'search all', facade: '${facadeId}_core', op: 'search' },`,
|
|
120
|
+
` { intent: 'vault stats', facade: '${facadeId}_core', op: 'vault_stats' },`,
|
|
121
|
+
...toolRecLines,
|
|
122
|
+
' ],',
|
|
123
|
+
` session_instruction: 'You are now ' + PERSONA.name + ', a ' + PERSONA.role + '. Stay in character for the ENTIRE session. ' +`,
|
|
124
|
+
` 'Reference patterns from the knowledge vault. Provide concrete examples. Flag anti-patterns with severity.',`,
|
|
125
|
+
' setup_status: {',
|
|
126
|
+
' claude_md_injected: claudeMdInjected,',
|
|
127
|
+
' global_claude_md_injected: globalClaudeMdInjected,',
|
|
128
|
+
' vault_has_entries: vaultHasEntries,',
|
|
129
|
+
' vault_entry_count: stats.totalEntries,',
|
|
130
|
+
' },',
|
|
131
|
+
' executing_plans: executingPlans,',
|
|
132
|
+
' next_steps: nextSteps,',
|
|
133
|
+
' };',
|
|
134
|
+
'}',
|
|
135
|
+
'',
|
|
136
|
+
'/**',
|
|
137
|
+
` * Deactivate ${config.name} — drops persona for the session.`,
|
|
138
|
+
' */',
|
|
139
|
+
'export function deactivateAgent(): DeactivationResult {',
|
|
140
|
+
' return {',
|
|
141
|
+
' deactivated: true,',
|
|
142
|
+
` message: 'Goodbye! ' + PERSONA.name + ' persona deactivated. Reverting to default behavior.',`,
|
|
143
|
+
' };',
|
|
144
|
+
'}',
|
|
145
|
+
].join('\n');
|
|
146
|
+
}
|