@soleri/forge 5.14.9 → 7.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/dist/agent-schema.d.ts +323 -0
- package/dist/agent-schema.js +151 -0
- package/dist/agent-schema.js.map +1 -0
- package/dist/compose-claude-md.d.ts +24 -0
- package/dist/compose-claude-md.js +197 -0
- package/dist/compose-claude-md.js.map +1 -0
- package/dist/index.js +0 -0
- package/dist/lib.d.ts +12 -1
- package/dist/lib.js +10 -1
- package/dist/lib.js.map +1 -1
- package/dist/scaffold-filetree.d.ts +22 -0
- package/dist/scaffold-filetree.js +349 -0
- package/dist/scaffold-filetree.js.map +1 -0
- package/dist/scaffolder.js +261 -11
- package/dist/scaffolder.js.map +1 -1
- package/dist/templates/activate.d.ts +5 -2
- package/dist/templates/activate.js +136 -35
- package/dist/templates/activate.js.map +1 -1
- package/dist/templates/agents-md.d.ts +10 -1
- package/dist/templates/agents-md.js +76 -16
- package/dist/templates/agents-md.js.map +1 -1
- package/dist/templates/claude-md-template.js +25 -4
- package/dist/templates/claude-md-template.js.map +1 -1
- package/dist/templates/entry-point.js +84 -7
- package/dist/templates/entry-point.js.map +1 -1
- package/dist/templates/inject-claude-md.js +53 -0
- package/dist/templates/inject-claude-md.js.map +1 -1
- package/dist/templates/package-json.js +4 -1
- package/dist/templates/package-json.js.map +1 -1
- package/dist/templates/readme.js +4 -3
- package/dist/templates/readme.js.map +1 -1
- package/dist/templates/setup-script.js +109 -3
- package/dist/templates/setup-script.js.map +1 -1
- package/dist/templates/shared-rules.js +54 -17
- package/dist/templates/shared-rules.js.map +1 -1
- package/dist/templates/test-facades.js +151 -6
- package/dist/templates/test-facades.js.map +1 -1
- package/dist/types.d.ts +75 -10
- package/dist/types.js +40 -2
- package/dist/types.js.map +1 -1
- package/dist/utils/detect-domain-packs.d.ts +25 -0
- package/dist/utils/detect-domain-packs.js +104 -0
- package/dist/utils/detect-domain-packs.js.map +1 -0
- package/package.json +2 -1
- package/src/__tests__/detect-domain-packs.test.ts +178 -0
- package/src/__tests__/scaffold-filetree.test.ts +243 -0
- package/src/__tests__/scaffolder.test.ts +5 -3
- package/src/agent-schema.ts +184 -0
- package/src/compose-claude-md.ts +252 -0
- package/src/lib.ts +14 -1
- package/src/scaffold-filetree.ts +409 -0
- package/src/scaffolder.ts +299 -15
- package/src/templates/activate.ts +137 -39
- package/src/templates/agents-md.ts +78 -16
- package/src/templates/claude-md-template.ts +29 -4
- package/src/templates/entry-point.ts +91 -7
- package/src/templates/inject-claude-md.ts +53 -0
- package/src/templates/package-json.ts +4 -1
- package/src/templates/readme.ts +4 -3
- package/src/templates/setup-script.ts +110 -4
- package/src/templates/shared-rules.ts +55 -17
- package/src/templates/test-facades.ts +156 -6
- package/src/types.ts +45 -2
- package/src/utils/detect-domain-packs.ts +129 -0
- package/tsconfig.json +0 -1
- package/vitest.config.ts +1 -2
package/src/scaffolder.ts
CHANGED
|
@@ -9,7 +9,13 @@ import {
|
|
|
9
9
|
import { join, dirname } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { execFileSync } from 'node:child_process';
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
AgentConfig,
|
|
14
|
+
SetupTarget,
|
|
15
|
+
ScaffoldResult,
|
|
16
|
+
ScaffoldPreview,
|
|
17
|
+
AgentInfo,
|
|
18
|
+
} from './types.js';
|
|
13
19
|
|
|
14
20
|
import { generatePackageJson } from './templates/package-json.js';
|
|
15
21
|
import { generateTsconfig } from './templates/tsconfig.js';
|
|
@@ -29,30 +35,49 @@ import { generateTelegramBot } from './templates/telegram-bot.js';
|
|
|
29
35
|
import { generateTelegramConfig } from './templates/telegram-config.js';
|
|
30
36
|
import { generateTelegramAgent } from './templates/telegram-agent.js';
|
|
31
37
|
import { generateTelegramSupervisor } from './templates/telegram-supervisor.js';
|
|
38
|
+
import { detectInstalledDomainPacks } from './utils/detect-domain-packs.js';
|
|
32
39
|
|
|
33
|
-
function getSetupTarget(config: AgentConfig):
|
|
40
|
+
function getSetupTarget(config: AgentConfig): SetupTarget {
|
|
34
41
|
return config.setupTarget ?? 'claude';
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
function includesClaudeSetup(config: AgentConfig): boolean {
|
|
38
45
|
const target = getSetupTarget(config);
|
|
39
|
-
return target === 'claude' || target === 'both';
|
|
46
|
+
return target === 'claude' || target === 'both' || target === 'all';
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
function includesCodexSetup(config: AgentConfig): boolean {
|
|
43
50
|
const target = getSetupTarget(config);
|
|
44
|
-
return target === 'codex' || target === 'both';
|
|
51
|
+
return target === 'codex' || target === 'both' || target === 'all';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function includesOpencodeSetup(config: AgentConfig): boolean {
|
|
55
|
+
const target = getSetupTarget(config);
|
|
56
|
+
return target === 'opencode' || target === 'all';
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
/**
|
|
48
60
|
* Preview what scaffold will create without writing anything.
|
|
49
61
|
*/
|
|
50
62
|
export function previewScaffold(config: AgentConfig): ScaffoldPreview {
|
|
63
|
+
// Auto-detect domain packs if not explicitly configured
|
|
64
|
+
if (!config.domainPacks || config.domainPacks.length === 0) {
|
|
65
|
+
const detected = detectInstalledDomainPacks(config.outputDir);
|
|
66
|
+
if (detected.length > 0) {
|
|
67
|
+
config = { ...config, domainPacks: detected };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
51
71
|
const agentDir = join(config.outputDir, config.id);
|
|
52
72
|
const claudeSetup = includesClaudeSetup(config);
|
|
53
73
|
const codexSetup = includesCodexSetup(config);
|
|
54
|
-
const
|
|
55
|
-
|
|
74
|
+
const opencodeSetup = includesOpencodeSetup(config);
|
|
75
|
+
const setupParts = [
|
|
76
|
+
...(claudeSetup ? ['Claude Code'] : []),
|
|
77
|
+
...(codexSetup ? ['Codex'] : []),
|
|
78
|
+
...(opencodeSetup ? ['OpenCode'] : []),
|
|
79
|
+
];
|
|
80
|
+
const setupLabel = setupParts.join(' + ');
|
|
56
81
|
|
|
57
82
|
const files = [
|
|
58
83
|
{ path: 'package.json', description: 'NPM package with MCP SDK, SQLite, Zod dependencies' },
|
|
@@ -113,10 +138,20 @@ export function previewScaffold(config: AgentConfig): ScaffoldPreview {
|
|
|
113
138
|
},
|
|
114
139
|
];
|
|
115
140
|
|
|
116
|
-
if (
|
|
141
|
+
if (opencodeSetup) {
|
|
142
|
+
files.push({
|
|
143
|
+
path: '.opencode.json',
|
|
144
|
+
description: 'OpenCode MCP server config for connecting to this agent',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (codexSetup || opencodeSetup) {
|
|
149
|
+
const hosts = [...(codexSetup ? ['Codex'] : []), ...(opencodeSetup ? ['OpenCode'] : [])].join(
|
|
150
|
+
' + ',
|
|
151
|
+
);
|
|
117
152
|
files.push({
|
|
118
153
|
path: 'AGENTS.md',
|
|
119
|
-
description:
|
|
154
|
+
description: `${hosts} project instructions and activation workflow`,
|
|
120
155
|
});
|
|
121
156
|
}
|
|
122
157
|
|
|
@@ -253,8 +288,21 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
|
|
|
253
288
|
greeting: `Hello! I'm ${config.name}, your AI assistant for ${config.role}.`,
|
|
254
289
|
};
|
|
255
290
|
}
|
|
291
|
+
|
|
292
|
+
// Auto-detect domain packs if not explicitly configured
|
|
293
|
+
if (!config.domainPacks || config.domainPacks.length === 0) {
|
|
294
|
+
const detected = detectInstalledDomainPacks(config.outputDir);
|
|
295
|
+
if (detected.length > 0) {
|
|
296
|
+
config = { ...config, domainPacks: detected };
|
|
297
|
+
console.error(
|
|
298
|
+
`[forge] Auto-detected ${detected.length} domain pack(s): ${detected.map((d) => d.package).join(', ')}`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
256
303
|
const claudeSetup = includesClaudeSetup(config);
|
|
257
304
|
const codexSetup = includesCodexSetup(config);
|
|
305
|
+
const opencodeSetup = includesOpencodeSetup(config);
|
|
258
306
|
const agentDir = join(config.outputDir, config.id);
|
|
259
307
|
const filesCreated: string[] = [];
|
|
260
308
|
|
|
@@ -312,7 +360,36 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
|
|
|
312
360
|
['scripts/setup.sh', generateSetupScript(config)],
|
|
313
361
|
];
|
|
314
362
|
|
|
315
|
-
if (
|
|
363
|
+
if (opencodeSetup) {
|
|
364
|
+
projectFiles.push([
|
|
365
|
+
'.opencode.json',
|
|
366
|
+
JSON.stringify(
|
|
367
|
+
{
|
|
368
|
+
$schema: 'https://opencode.ai/config.json',
|
|
369
|
+
title: config.name,
|
|
370
|
+
tui: { theme: 'soleri' },
|
|
371
|
+
mcpServers: {
|
|
372
|
+
[config.id]: {
|
|
373
|
+
type: 'stdio',
|
|
374
|
+
command: 'node',
|
|
375
|
+
args: ['dist/index.js'],
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
agents: {
|
|
379
|
+
coder: { model: config.model ?? 'claude-code-sonnet-4' },
|
|
380
|
+
summarizer: { model: 'claude-code-3.5-haiku' },
|
|
381
|
+
task: { model: 'claude-code-3.5-haiku' },
|
|
382
|
+
title: { model: 'claude-code-3.5-haiku' },
|
|
383
|
+
},
|
|
384
|
+
contextPaths: ['AGENTS.md'],
|
|
385
|
+
},
|
|
386
|
+
null,
|
|
387
|
+
2,
|
|
388
|
+
),
|
|
389
|
+
]);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (codexSetup || opencodeSetup) {
|
|
316
393
|
projectFiles.push(['AGENTS.md', generateAgentsMd(config)]);
|
|
317
394
|
}
|
|
318
395
|
|
|
@@ -386,8 +463,22 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
|
|
|
386
463
|
buildError = err instanceof Error ? err.message : String(err);
|
|
387
464
|
}
|
|
388
465
|
|
|
466
|
+
// Install OpenCode CLI if needed and not already available
|
|
467
|
+
const opencodeInstallResult = opencodeSetup ? ensureOpencodeInstalled() : undefined;
|
|
468
|
+
|
|
469
|
+
// Create launcher script so typing the agent name starts OpenCode
|
|
470
|
+
if (opencodeSetup && buildSuccess) {
|
|
471
|
+
const launcherResult = createOpencodeLauncher(config.id, agentDir);
|
|
472
|
+
if (launcherResult.created) {
|
|
473
|
+
// Launcher details added to summary below
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
389
477
|
// Register the agent as an MCP server in selected host configs (only if build succeeded)
|
|
390
|
-
const mcpRegistrations: Array<{
|
|
478
|
+
const mcpRegistrations: Array<{
|
|
479
|
+
host: 'Claude Code' | 'Codex' | 'OpenCode';
|
|
480
|
+
result: RegistrationResult;
|
|
481
|
+
}> = [];
|
|
391
482
|
if (claudeSetup) {
|
|
392
483
|
if (buildSuccess) {
|
|
393
484
|
mcpRegistrations.push({
|
|
@@ -422,13 +513,30 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
|
|
|
422
513
|
});
|
|
423
514
|
}
|
|
424
515
|
}
|
|
516
|
+
if (opencodeSetup) {
|
|
517
|
+
if (buildSuccess) {
|
|
518
|
+
mcpRegistrations.push({
|
|
519
|
+
host: 'OpenCode',
|
|
520
|
+
result: registerOpencodeMcpServer(config.id, agentDir),
|
|
521
|
+
});
|
|
522
|
+
} else {
|
|
523
|
+
mcpRegistrations.push({
|
|
524
|
+
host: 'OpenCode',
|
|
525
|
+
result: {
|
|
526
|
+
registered: false,
|
|
527
|
+
path: join(homedir(), '.opencode.json'),
|
|
528
|
+
error: 'Skipped — build failed',
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
425
533
|
|
|
426
534
|
const summaryLines = [
|
|
427
535
|
`Created ${config.name} agent at ${agentDir}`,
|
|
428
536
|
`${config.domains.length + 11} facades with ${totalOps} operations`,
|
|
429
537
|
`${config.domains.length} empty knowledge domains ready for capture`,
|
|
430
538
|
`Intelligence layer (Brain) — TF-IDF scoring, auto-tagging, duplicate detection`,
|
|
431
|
-
`
|
|
539
|
+
`Persistent identity — ${config.name} is active from the start`,
|
|
432
540
|
`1 test suite — facades (vault, brain, planner, llm tests provided by @soleri/core)`,
|
|
433
541
|
`${skillFiles.length} built-in skills (TDD, debugging, planning, vault, brain debrief)`,
|
|
434
542
|
];
|
|
@@ -440,6 +548,23 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
|
|
|
440
548
|
summaryLines.push(` Run manually: cd ${agentDir} && npm install && npm run build`);
|
|
441
549
|
}
|
|
442
550
|
|
|
551
|
+
if (opencodeInstallResult) {
|
|
552
|
+
if (opencodeInstallResult.installed) {
|
|
553
|
+
summaryLines.push(`OpenCode CLI installed (${opencodeInstallResult.method})`);
|
|
554
|
+
} else if (!opencodeInstallResult.alreadyPresent && opencodeInstallResult.error) {
|
|
555
|
+
summaryLines.push('Warning: Failed to install OpenCode CLI');
|
|
556
|
+
summaryLines.push(' Install manually: npm install -g opencode-ai');
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Report launcher status
|
|
561
|
+
if (opencodeSetup && buildSuccess) {
|
|
562
|
+
const launcherPath = join('/usr', 'local', 'bin', config.id);
|
|
563
|
+
if (existsSync(launcherPath)) {
|
|
564
|
+
summaryLines.push(`Launcher created: type "${config.id}" in terminal to start OpenCode`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
443
568
|
if (claudeSetup && config.hookPacks?.length) {
|
|
444
569
|
summaryLines.push(`${config.hookPacks.length} hook pack(s) bundled in .claude/`);
|
|
445
570
|
}
|
|
@@ -463,7 +588,10 @@ export function scaffold(config: AgentConfig): ScaffoldResult {
|
|
|
463
588
|
if (codexSetup) {
|
|
464
589
|
nextSteps.push(' Restart Codex');
|
|
465
590
|
}
|
|
466
|
-
|
|
591
|
+
if (opencodeSetup) {
|
|
592
|
+
nextSteps.push(' Restart OpenCode');
|
|
593
|
+
}
|
|
594
|
+
nextSteps.push(` ${config.name} identity is active from the start — no activation needed`);
|
|
467
595
|
summaryLines.push(...nextSteps);
|
|
468
596
|
|
|
469
597
|
return {
|
|
@@ -498,7 +626,10 @@ export function listAgents(parentDir: string): AgentInfo[] {
|
|
|
498
626
|
|
|
499
627
|
try {
|
|
500
628
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
501
|
-
|
|
629
|
+
// Accept both old format (name-mcp) and new format (name)
|
|
630
|
+
const hasMcpSuffix = pkg.name?.endsWith('-mcp');
|
|
631
|
+
const hasIntelligenceDir = existsSync(join(dir, 'src', 'intelligence', 'data'));
|
|
632
|
+
if (!hasMcpSuffix && !hasIntelligenceDir) continue;
|
|
502
633
|
|
|
503
634
|
const dataDir = join(dir, 'src', 'intelligence', 'data');
|
|
504
635
|
let domains: string[] = [];
|
|
@@ -512,7 +643,7 @@ export function listAgents(parentDir: string): AgentInfo[] {
|
|
|
512
643
|
|
|
513
644
|
agents.push({
|
|
514
645
|
id: name,
|
|
515
|
-
name: pkg.name.replace('-mcp', ''),
|
|
646
|
+
name: hasMcpSuffix ? pkg.name.replace('-mcp', '') : pkg.name,
|
|
516
647
|
role: pkg.description || '',
|
|
517
648
|
path: dir,
|
|
518
649
|
domains,
|
|
@@ -614,12 +745,165 @@ args = ["${join(agentDir, 'dist', 'index.js')}"]
|
|
|
614
745
|
}
|
|
615
746
|
}
|
|
616
747
|
|
|
748
|
+
/**
|
|
749
|
+
* Create a launcher script at /usr/local/bin/<agentId> that starts OpenCode
|
|
750
|
+
* in the agent's project directory. Typing the agent name in terminal → OpenCode starts.
|
|
751
|
+
*/
|
|
752
|
+
function createOpencodeLauncher(
|
|
753
|
+
agentId: string,
|
|
754
|
+
agentDir: string,
|
|
755
|
+
): { created: boolean; path: string; error?: string } {
|
|
756
|
+
const launcherPath = join('/usr', 'local', 'bin', agentId);
|
|
757
|
+
const script = [
|
|
758
|
+
'#!/usr/bin/env bash',
|
|
759
|
+
`# Soleri agent launcher — starts OpenCode with ${agentId} MCP agent`,
|
|
760
|
+
`# Set terminal title to agent name`,
|
|
761
|
+
`printf '\\033]0;${agentId}\\007'`,
|
|
762
|
+
`cd "${agentDir}" || exit 1`,
|
|
763
|
+
'exec opencode "$@"',
|
|
764
|
+
'',
|
|
765
|
+
].join('\n');
|
|
766
|
+
|
|
767
|
+
try {
|
|
768
|
+
writeFileSync(launcherPath, script, { mode: 0o755 });
|
|
769
|
+
return { created: true, path: launcherPath };
|
|
770
|
+
} catch {
|
|
771
|
+
// /usr/local/bin may need sudo — try via agent's scripts/ directory instead
|
|
772
|
+
const localLauncher = join(agentDir, 'scripts', agentId);
|
|
773
|
+
try {
|
|
774
|
+
writeFileSync(localLauncher, script, { mode: 0o755 });
|
|
775
|
+
// Try to symlink to /usr/local/bin
|
|
776
|
+
try {
|
|
777
|
+
const { symlinkSync, unlinkSync } = require('node:fs') as typeof import('node:fs');
|
|
778
|
+
if (existsSync(launcherPath)) unlinkSync(launcherPath);
|
|
779
|
+
symlinkSync(localLauncher, launcherPath);
|
|
780
|
+
return { created: true, path: launcherPath };
|
|
781
|
+
} catch {
|
|
782
|
+
return { created: true, path: localLauncher };
|
|
783
|
+
}
|
|
784
|
+
} catch (err) {
|
|
785
|
+
return {
|
|
786
|
+
created: false,
|
|
787
|
+
path: launcherPath,
|
|
788
|
+
error: err instanceof Error ? err.message : String(err),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Ensure OpenCode CLI is installed (Soleri fork with title branding support).
|
|
796
|
+
* Tries `go install` from the fork first, falls back to upstream npm package.
|
|
797
|
+
*/
|
|
798
|
+
function ensureOpencodeInstalled(): {
|
|
799
|
+
alreadyPresent: boolean;
|
|
800
|
+
installed: boolean;
|
|
801
|
+
method?: string;
|
|
802
|
+
error?: string;
|
|
803
|
+
} {
|
|
804
|
+
// Check if already available
|
|
805
|
+
try {
|
|
806
|
+
execFileSync('opencode', ['--version'], { stdio: 'pipe', timeout: 10_000 });
|
|
807
|
+
return { alreadyPresent: true, installed: false };
|
|
808
|
+
} catch {
|
|
809
|
+
// Not installed — proceed to install
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Try Go install from Soleri fork (supports title branding)
|
|
813
|
+
try {
|
|
814
|
+
execFileSync('go', ['version'], { stdio: 'pipe', timeout: 5_000 });
|
|
815
|
+
execFileSync('go', ['install', 'github.com/adrozdenko/opencode@latest'], {
|
|
816
|
+
stdio: 'pipe',
|
|
817
|
+
timeout: 120_000,
|
|
818
|
+
});
|
|
819
|
+
return {
|
|
820
|
+
alreadyPresent: false,
|
|
821
|
+
installed: true,
|
|
822
|
+
method: 'go install github.com/adrozdenko/opencode@latest',
|
|
823
|
+
};
|
|
824
|
+
} catch {
|
|
825
|
+
// Go not available or install failed — fall back to npm
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Fallback: upstream npm package (no title branding)
|
|
829
|
+
try {
|
|
830
|
+
execFileSync('npm', ['install', '-g', 'opencode-ai'], {
|
|
831
|
+
stdio: 'pipe',
|
|
832
|
+
timeout: 60_000,
|
|
833
|
+
});
|
|
834
|
+
return {
|
|
835
|
+
alreadyPresent: false,
|
|
836
|
+
installed: true,
|
|
837
|
+
method: 'npm install -g opencode-ai (upstream — title branding requires Go)',
|
|
838
|
+
};
|
|
839
|
+
} catch (err) {
|
|
840
|
+
return {
|
|
841
|
+
alreadyPresent: false,
|
|
842
|
+
installed: false,
|
|
843
|
+
error: err instanceof Error ? err.message : String(err),
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Register the agent as an MCP server in ~/.opencode.json.
|
|
850
|
+
* Idempotent — updates existing entry if present.
|
|
851
|
+
*/
|
|
852
|
+
function registerOpencodeMcpServer(agentId: string, agentDir: string): RegistrationResult {
|
|
853
|
+
const opencodeConfigPath = join(homedir(), '.opencode.json');
|
|
854
|
+
|
|
855
|
+
try {
|
|
856
|
+
let config: Record<string, unknown> = {};
|
|
857
|
+
|
|
858
|
+
if (existsSync(opencodeConfigPath)) {
|
|
859
|
+
// Strip single-line comments before parsing (JSONC support)
|
|
860
|
+
const raw = readFileSync(opencodeConfigPath, 'utf-8');
|
|
861
|
+
const stripped = raw.replace(/^\s*\/\/.*$/gm, '');
|
|
862
|
+
try {
|
|
863
|
+
config = JSON.parse(stripped);
|
|
864
|
+
} catch {
|
|
865
|
+
config = {};
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
|
|
870
|
+
config.mcpServers = {};
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const servers = config.mcpServers as Record<string, unknown>;
|
|
874
|
+
servers[agentId] = {
|
|
875
|
+
type: 'stdio',
|
|
876
|
+
command: 'node',
|
|
877
|
+
args: [join(agentDir, 'dist', 'index.js')],
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
writeFileSync(opencodeConfigPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
881
|
+
return { registered: true, path: opencodeConfigPath };
|
|
882
|
+
} catch (err) {
|
|
883
|
+
return {
|
|
884
|
+
registered: false,
|
|
885
|
+
path: opencodeConfigPath,
|
|
886
|
+
error: err instanceof Error ? err.message : String(err),
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
617
891
|
function generateEmptyBundle(domain: string): string {
|
|
618
892
|
return JSON.stringify(
|
|
619
893
|
{
|
|
620
894
|
domain,
|
|
621
895
|
version: '1.0.0',
|
|
622
|
-
entries: [
|
|
896
|
+
entries: [
|
|
897
|
+
{
|
|
898
|
+
id: `${domain}-seed`,
|
|
899
|
+
type: 'pattern',
|
|
900
|
+
domain,
|
|
901
|
+
title: `${domain.replace(/-/g, ' ')} domain seed`,
|
|
902
|
+
severity: 'suggestion',
|
|
903
|
+
description: `Seed entry for the ${domain.replace(/-/g, ' ')} domain. Replace or remove once real knowledge is captured.`,
|
|
904
|
+
tags: [domain, 'seed'],
|
|
905
|
+
},
|
|
906
|
+
],
|
|
623
907
|
},
|
|
624
908
|
null,
|
|
625
909
|
2,
|
|
@@ -2,26 +2,15 @@ import type { AgentConfig } from '../types.js';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Generates src/activation/activate.ts for a new agent.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* Activation is ADAPTIVE — it discovers current capabilities at runtime
|
|
7
|
+
* (vault domains, installed packs, identity changes) rather than returning
|
|
8
|
+
* a frozen snapshot from scaffold time. PERSONA is the birth config;
|
|
9
|
+
* the activation response reflects what the agent has become.
|
|
7
10
|
*
|
|
8
11
|
* Uses array-joined pattern because generated code contains template literals.
|
|
9
12
|
*/
|
|
10
13
|
export function generateActivate(config: AgentConfig): string {
|
|
11
|
-
const toolPrefix = config.id; // keep hyphens — matches MCP tool registration
|
|
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 = `${toolPrefix}_${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
14
|
// Build behavioral guidelines from config principles
|
|
26
15
|
const guidelineLines = config.principles
|
|
27
16
|
.map((p) => {
|
|
@@ -30,23 +19,34 @@ export function generateActivate(config: AgentConfig): string {
|
|
|
30
19
|
})
|
|
31
20
|
.join('\n');
|
|
32
21
|
|
|
22
|
+
// Static domain list (from scaffold time) — used as baseline
|
|
23
|
+
const configDomains = JSON.stringify(config.domains);
|
|
24
|
+
|
|
33
25
|
return [
|
|
34
26
|
"import { join } from 'node:path';",
|
|
27
|
+
"import { existsSync, readFileSync } from 'node:fs';",
|
|
35
28
|
"import { homedir } from 'node:os';",
|
|
36
29
|
"import { PERSONA } from '../identity/persona.js';",
|
|
37
30
|
"import { hasAgentMarker, removeClaudeMdGlobal } from './inject-claude-md.js';",
|
|
38
|
-
"import type {
|
|
31
|
+
"import type { AgentRuntime } from '@soleri/core';",
|
|
39
32
|
'',
|
|
40
33
|
'export interface ActivationResult {',
|
|
41
34
|
' activated: boolean;',
|
|
42
|
-
'
|
|
35
|
+
' origin: {',
|
|
43
36
|
' name: string;',
|
|
44
37
|
' role: string;',
|
|
45
38
|
' description: string;',
|
|
39
|
+
' };',
|
|
40
|
+
' current: {',
|
|
41
|
+
' role: string;',
|
|
46
42
|
' greeting: string;',
|
|
43
|
+
' domains: string[];',
|
|
44
|
+
' capabilities: Array<{ domain: string; entries: number }>;',
|
|
45
|
+
' installed_packs: Array<{ id: string; type: string }>;',
|
|
46
|
+
' what_you_can_do: string[];',
|
|
47
|
+
' growth_suggestions: string[];',
|
|
47
48
|
' };',
|
|
48
49
|
' guidelines: string[];',
|
|
49
|
-
' tool_recommendations: Array<{ intent: string; facade: string; op: string }>;',
|
|
50
50
|
' session_instruction: string;',
|
|
51
51
|
' setup_status: {',
|
|
52
52
|
' claude_md_injected: boolean;',
|
|
@@ -67,20 +67,115 @@ export function generateActivate(config: AgentConfig): string {
|
|
|
67
67
|
'}',
|
|
68
68
|
'',
|
|
69
69
|
'/**',
|
|
70
|
-
` * Activate ${config.name} —
|
|
70
|
+
` * Activate ${config.name} — discovers current capabilities and returns adaptive context.`,
|
|
71
|
+
' *',
|
|
72
|
+
' * PERSONA is the birth config. The activation response reflects what the agent',
|
|
73
|
+
' * has become through installed packs, captured knowledge, and identity updates.',
|
|
71
74
|
' */',
|
|
72
|
-
'export function activateAgent(
|
|
73
|
-
'
|
|
75
|
+
'export function activateAgent(runtime: AgentRuntime, projectPath: string): ActivationResult {',
|
|
76
|
+
' const { vault, planner, identityManager } = runtime;',
|
|
77
|
+
'',
|
|
78
|
+
' // ─── Setup status ──────────────────────────────────────────',
|
|
74
79
|
" const projectClaudeMd = join(projectPath, 'CLAUDE.md');",
|
|
75
80
|
" const globalClaudeMd = join(homedir(), '.claude', 'CLAUDE.md');",
|
|
76
81
|
' const claudeMdInjected = hasAgentMarker(projectClaudeMd);',
|
|
77
82
|
' const globalClaudeMdInjected = hasAgentMarker(globalClaudeMd);',
|
|
78
83
|
'',
|
|
79
|
-
' //
|
|
84
|
+
' // ─── Vault stats — what the agent actually knows ──────────',
|
|
80
85
|
' const stats = vault.stats();',
|
|
81
86
|
' const vaultHasEntries = stats.totalEntries > 0;',
|
|
82
87
|
'',
|
|
83
|
-
|
|
88
|
+
' // ─── Discover domains ─────────────────────────────────────',
|
|
89
|
+
` const configuredDomains: string[] = ${configDomains};`,
|
|
90
|
+
' const vaultDomains = Object.keys(stats.byDomain);',
|
|
91
|
+
'',
|
|
92
|
+
' // Merge configured + vault-discovered domains (dedup)',
|
|
93
|
+
' const allDomains = [...new Set([...configuredDomains, ...vaultDomains])];',
|
|
94
|
+
'',
|
|
95
|
+
' // Build capability map — entries per domain',
|
|
96
|
+
' const capabilities = allDomains.map((d) => ({',
|
|
97
|
+
' domain: d,',
|
|
98
|
+
' entries: stats.byDomain[d] ?? 0,',
|
|
99
|
+
' }));',
|
|
100
|
+
'',
|
|
101
|
+
' // ─── Discover installed packs ─────────────────────────────',
|
|
102
|
+
' const installedPacks: Array<{ id: string; type: string }> = [];',
|
|
103
|
+
' try {',
|
|
104
|
+
" const lockPath = join(projectPath, 'soleri.lock');",
|
|
105
|
+
' if (existsSync(lockPath)) {',
|
|
106
|
+
" const lockData = JSON.parse(readFileSync(lockPath, 'utf-8'));",
|
|
107
|
+
' if (lockData.packs) {',
|
|
108
|
+
' for (const [id, entry] of Object.entries(lockData.packs)) {',
|
|
109
|
+
' installedPacks.push({ id, type: (entry as Record<string, string>).type ?? "unknown" });',
|
|
110
|
+
' }',
|
|
111
|
+
' }',
|
|
112
|
+
' }',
|
|
113
|
+
' } catch {',
|
|
114
|
+
' // Lock file missing or corrupt — proceed without pack info',
|
|
115
|
+
' }',
|
|
116
|
+
'',
|
|
117
|
+
' // ─── Dynamic role — based on what the agent actually covers ─',
|
|
118
|
+
" const currentIdentity = identityManager.getIdentity('" + config.id + "');",
|
|
119
|
+
' const newDomains = allDomains.filter((d) => !configuredDomains.includes(d));',
|
|
120
|
+
' let currentRole = currentIdentity?.role ?? PERSONA.role;',
|
|
121
|
+
'',
|
|
122
|
+
' // If the agent has grown beyond its birth domains, reflect that',
|
|
123
|
+
' if (newDomains.length > 0) {',
|
|
124
|
+
' const formatted = newDomains.map((d) => d.replace(/-/g, " ")).join(", ");',
|
|
125
|
+
' currentRole = `${PERSONA.role} (also covering ${formatted})`;',
|
|
126
|
+
' }',
|
|
127
|
+
'',
|
|
128
|
+
' // ─── Dynamic greeting ─────────────────────────────────────',
|
|
129
|
+
" let greeting = `Hello! I'm ${PERSONA.name}.`;",
|
|
130
|
+
' if (allDomains.length > configuredDomains.length) {',
|
|
131
|
+
" greeting += ` I started as a ${PERSONA.role} and have expanded to also cover ${newDomains.map((d) => d.replace(/-/g, ' ')).join(', ')}.`;",
|
|
132
|
+
' } else {',
|
|
133
|
+
' greeting += ` ${PERSONA.role} ready to help.`;',
|
|
134
|
+
' }',
|
|
135
|
+
' if (stats.totalEntries > 0) {',
|
|
136
|
+
' const domainSummary = capabilities',
|
|
137
|
+
' .filter((c) => c.entries > 0)',
|
|
138
|
+
' .map((c) => `${c.entries} ${c.domain.replace(/-/g, " ")}`)',
|
|
139
|
+
' .join(", ");',
|
|
140
|
+
' greeting += ` Vault: ${stats.totalEntries} entries (${domainSummary}).`;',
|
|
141
|
+
' }',
|
|
142
|
+
'',
|
|
143
|
+
' // ─── Capability self-awareness ────────────────────────────',
|
|
144
|
+
' const whatYouCanDo: string[] = [',
|
|
145
|
+
' "Search and traverse a connected knowledge graph (vault) before every decision",',
|
|
146
|
+
' "Create structured plans with approval gates and drift reconciliation",',
|
|
147
|
+
' "Learn from sessions — brain tracks pattern strengths and recommends approaches",',
|
|
148
|
+
' "Remember across conversations and projects (cross-project memory)",',
|
|
149
|
+
' "Capture knowledge as typed entries with Zettelkasten links",',
|
|
150
|
+
' "Run iterative validation loops until quality targets are met",',
|
|
151
|
+
' "Orchestrate multi-step workflows: plan → execute → capture",',
|
|
152
|
+
' ];',
|
|
153
|
+
'',
|
|
154
|
+
' // Add domain-specific capabilities',
|
|
155
|
+
' for (const cap of capabilities) {',
|
|
156
|
+
' if (cap.entries > 0) {',
|
|
157
|
+
' whatYouCanDo.push(`${cap.domain.replace(/-/g, " ")}: ${cap.entries} patterns and knowledge entries`);',
|
|
158
|
+
' }',
|
|
159
|
+
' }',
|
|
160
|
+
'',
|
|
161
|
+
' // Add pack-specific capabilities',
|
|
162
|
+
' for (const pack of installedPacks) {',
|
|
163
|
+
' whatYouCanDo.push(`Pack "${pack.id}" (${pack.type}) installed — provides domain-specific intelligence`);',
|
|
164
|
+
' }',
|
|
165
|
+
'',
|
|
166
|
+
' const growthSuggestions: string[] = [];',
|
|
167
|
+
' if (stats.totalEntries < 10) {',
|
|
168
|
+
' growthSuggestions.push("Vault has few entries — start capturing patterns to build your knowledge base");',
|
|
169
|
+
' }',
|
|
170
|
+
' if (installedPacks.length === 0) {',
|
|
171
|
+
' growthSuggestions.push("No packs installed — try: soleri pack install <name> to add domain intelligence");',
|
|
172
|
+
' growthSuggestions.push("Available starter packs: soleri pack available");',
|
|
173
|
+
' }',
|
|
174
|
+
' if (allDomains.length <= 1) {',
|
|
175
|
+
' growthSuggestions.push("Only one domain configured — add more with: soleri add-domain <name>");',
|
|
176
|
+
' }',
|
|
177
|
+
'',
|
|
178
|
+
' // ─── Next steps ───────────────────────────────────────────',
|
|
84
179
|
' const nextSteps: string[] = [];',
|
|
85
180
|
' if (!globalClaudeMdInjected && !claudeMdInjected) {',
|
|
86
181
|
` nextSteps.push('No CLAUDE.md configured — run inject_claude_md with global: true for all projects, or without for this project only');`,
|
|
@@ -88,10 +183,7 @@ export function generateActivate(config: AgentConfig): string {
|
|
|
88
183
|
` nextSteps.push('Global CLAUDE.md not configured — run inject_claude_md with global: true to enable activation in all projects');`,
|
|
89
184
|
' }',
|
|
90
185
|
' if (!vaultHasEntries) {',
|
|
91
|
-
" nextSteps.push('Vault is empty — start capturing knowledge with the domain capture ops');",
|
|
92
|
-
' }',
|
|
93
|
-
' if (nextSteps.length === 0) {',
|
|
94
|
-
` nextSteps.push('All set! ${config.name} is fully integrated.');`,
|
|
186
|
+
" nextSteps.push('Vault is empty — start capturing knowledge with the domain capture ops, or install a knowledge pack with soleri pack install');",
|
|
95
187
|
' }',
|
|
96
188
|
'',
|
|
97
189
|
' // Check for executing plans',
|
|
@@ -105,25 +197,32 @@ export function generateActivate(config: AgentConfig): string {
|
|
|
105
197
|
' nextSteps.unshift(`${executingPlans.length} plan(s) in progress — use get_plan to review`);',
|
|
106
198
|
' }',
|
|
107
199
|
'',
|
|
200
|
+
' if (nextSteps.length === 0) {',
|
|
201
|
+
` nextSteps.push('All set! ${config.name} is ready.');`,
|
|
202
|
+
' }',
|
|
203
|
+
'',
|
|
108
204
|
' return {',
|
|
109
205
|
' activated: true,',
|
|
110
|
-
'
|
|
206
|
+
' origin: {',
|
|
111
207
|
' name: PERSONA.name,',
|
|
112
208
|
' role: PERSONA.role,',
|
|
113
209
|
' description: PERSONA.description,',
|
|
114
|
-
'
|
|
210
|
+
' },',
|
|
211
|
+
' current: {',
|
|
212
|
+
' role: currentRole,',
|
|
213
|
+
' greeting,',
|
|
214
|
+
' domains: allDomains,',
|
|
215
|
+
' capabilities,',
|
|
216
|
+
' installed_packs: installedPacks,',
|
|
217
|
+
' what_you_can_do: whatYouCanDo,',
|
|
218
|
+
' growth_suggestions: growthSuggestions,',
|
|
115
219
|
' },',
|
|
116
220
|
' guidelines: [',
|
|
117
221
|
guidelineLines,
|
|
118
222
|
' ],',
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
` { intent: 'vault stats', facade: '${toolPrefix}_core', op: 'vault_stats' },`,
|
|
123
|
-
...toolRecLines,
|
|
124
|
-
' ],',
|
|
125
|
-
` session_instruction: 'You are now ' + PERSONA.name + ', a ' + PERSONA.role + '. Stay in character for the ENTIRE session. ' +`,
|
|
126
|
-
` 'Reference patterns from the knowledge vault. Provide concrete examples. Flag anti-patterns with severity.',`,
|
|
223
|
+
" session_instruction: `You are ${PERSONA.name}. Your origin role is ${PERSONA.role}, but you have grown — your current capabilities span: ${allDomains.join(', ')}. ` +",
|
|
224
|
+
" 'Adapt your expertise to match your actual knowledge. ' +",
|
|
225
|
+
" 'Reference patterns from the knowledge vault. Provide concrete examples. Flag anti-patterns with severity.',",
|
|
127
226
|
' setup_status: {',
|
|
128
227
|
' claude_md_injected: claudeMdInjected,',
|
|
129
228
|
' global_claude_md_injected: globalClaudeMdInjected,',
|
|
@@ -139,7 +238,6 @@ export function generateActivate(config: AgentConfig): string {
|
|
|
139
238
|
` * Deactivate ${config.name} — drops persona and cleans up CLAUDE.md sections.`,
|
|
140
239
|
' */',
|
|
141
240
|
'export function deactivateAgent(): DeactivationResult {',
|
|
142
|
-
' // Remove agent sections from global CLAUDE.md on deactivation',
|
|
143
241
|
' const globalResult = removeClaudeMdGlobal();',
|
|
144
242
|
' return {',
|
|
145
243
|
' deactivated: true,',
|