@soleri/forge 5.14.10 → 8.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 +361 -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.js +39 -1
- 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 +9 -1
- package/dist/templates/claude-md-template.js.map +1 -1
- package/dist/templates/entry-point.js +83 -6
- 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 +71 -6
- package/dist/types.js +39 -1
- 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 +257 -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 +421 -0
- package/src/scaffolder.ts +299 -15
- package/src/templates/activate.ts +39 -0
- package/src/templates/agents-md.ts +78 -16
- package/src/templates/claude-md-template.ts +12 -1
- package/src/templates/entry-point.ts +90 -6
- 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 +44 -1
- package/src/utils/detect-domain-packs.ts +129 -0
- package/tsconfig.json +0 -1
- package/vitest.config.ts +1 -2
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import type { AgentConfig } from '../types.js';
|
|
2
2
|
|
|
3
|
+
/** Generate vault connection code for linked vaults. */
|
|
4
|
+
function generateVaultConnections(config: AgentConfig): string {
|
|
5
|
+
if (!config.vaults?.length) return '';
|
|
6
|
+
const lines = ['', ' // ─── Linked vaults ──────────────────────────────────────────────'];
|
|
7
|
+
for (const v of config.vaults) {
|
|
8
|
+
lines.push(
|
|
9
|
+
` try { runtime.vaultManager.connect('${v.name}', '${v.path}', ${v.priority ?? 0.5}); } catch { /* already connected or unavailable */ }`,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
lines.push(
|
|
13
|
+
` console.error(\`[\${tag}] Connected ${config.vaults.length} linked vault(s): ${config.vaults.map((v) => v.name).join(', ')}\`);`,
|
|
14
|
+
'',
|
|
15
|
+
);
|
|
16
|
+
return lines.join('\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
/**
|
|
4
20
|
* Generate the main index.ts entry point for the agent.
|
|
5
21
|
*
|
|
@@ -9,6 +25,7 @@ import type { AgentConfig } from '../types.js';
|
|
|
9
25
|
*/
|
|
10
26
|
export function generateEntryPoint(config: AgentConfig): string {
|
|
11
27
|
const domainsLiteral = JSON.stringify(config.domains);
|
|
28
|
+
const hasDomainPacks = config.domainPacks && config.domainPacks.length > 0;
|
|
12
29
|
|
|
13
30
|
return `#!/usr/bin/env node
|
|
14
31
|
|
|
@@ -24,12 +41,14 @@ import {
|
|
|
24
41
|
registerAllFacades,
|
|
25
42
|
seedDefaultPlaybooks,
|
|
26
43
|
wrapWithMiddleware,
|
|
44
|
+
CapabilityRegistry,
|
|
45
|
+
loadAllFlows,
|
|
27
46
|
} from '@soleri/core';
|
|
28
|
-
import type { OpDefinition, AgentExtensions } from '@soleri/core'
|
|
47
|
+
import type { OpDefinition, AgentExtensions } from '@soleri/core';${hasDomainPacks ? `\nimport { loadDomainPacksFromConfig, createPackRuntime } from '@soleri/core';` : ''}
|
|
29
48
|
import { z } from 'zod';
|
|
30
49
|
import { PERSONA, getPersonaPrompt } from './identity/persona.js';
|
|
31
50
|
import { activateAgent, deactivateAgent } from './activation/activate.js';
|
|
32
|
-
import { injectClaudeMd, injectClaudeMdGlobal, hasAgentMarker } from './activation/inject-claude-md.js';
|
|
51
|
+
import { injectClaudeMd, injectClaudeMdGlobal, hasAgentMarker, injectAgentsMd, injectAgentsMdGlobal, hasAgentMarkerInAgentsMd } from './activation/inject-claude-md.js';
|
|
33
52
|
|
|
34
53
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
35
54
|
|
|
@@ -37,11 +56,11 @@ async function main(): Promise<void> {
|
|
|
37
56
|
// ─── Runtime — vault, brain, planner, curator, LLM, key pools ───
|
|
38
57
|
const runtime = createAgentRuntime({
|
|
39
58
|
agentId: '${config.id}',
|
|
40
|
-
dataDir: join(__dirname, 'intelligence', 'data')
|
|
59
|
+
dataDir: join(__dirname, 'intelligence', 'data'),${config.sharedVaultPath ? `\n sharedVaultPath: '${config.sharedVaultPath}',` : ''}${config.cognee ? `\n cognee: true,` : ''}
|
|
41
60
|
});
|
|
42
61
|
|
|
43
62
|
const tag = PERSONA.name.toLowerCase();
|
|
44
|
-
|
|
63
|
+
${generateVaultConnections(config)}
|
|
45
64
|
// Seed built-in playbooks (idempotent)
|
|
46
65
|
const seedResult = seedDefaultPlaybooks(runtime.vault);
|
|
47
66
|
if (seedResult.seeded > 0) {
|
|
@@ -120,9 +139,24 @@ async function main(): Promise<void> {
|
|
|
120
139
|
return injectClaudeMd((params.projectPath as string) ?? '.');
|
|
121
140
|
},
|
|
122
141
|
},
|
|
142
|
+
{
|
|
143
|
+
name: 'inject_agents_md',
|
|
144
|
+
description: 'Inject agent sections into AGENTS.md — project-level or global (~/.config/opencode/AGENTS.md). For OpenCode and Codex. Idempotent.',
|
|
145
|
+
auth: 'write',
|
|
146
|
+
schema: z.object({
|
|
147
|
+
projectPath: z.string().optional().default('.'),
|
|
148
|
+
global: z.boolean().optional().describe('If true, inject into ~/.config/opencode/AGENTS.md instead of project-level'),
|
|
149
|
+
}),
|
|
150
|
+
handler: async (params) => {
|
|
151
|
+
if (params.global) {
|
|
152
|
+
return injectAgentsMdGlobal();
|
|
153
|
+
}
|
|
154
|
+
return injectAgentsMd((params.projectPath as string) ?? '.');
|
|
155
|
+
},
|
|
156
|
+
},
|
|
123
157
|
{
|
|
124
158
|
name: 'setup',
|
|
125
|
-
description: 'Check setup status — CLAUDE.md configured? Vault has entries? What to do next?',
|
|
159
|
+
description: 'Check setup status — CLAUDE.md configured? AGENTS.md configured? Vault has entries? What to do next?',
|
|
126
160
|
auth: 'read',
|
|
127
161
|
schema: z.object({
|
|
128
162
|
projectPath: z.string().optional().default('.'),
|
|
@@ -135,12 +169,19 @@ async function main(): Promise<void> {
|
|
|
135
169
|
|
|
136
170
|
const projectClaudeMd = joinPath(projectPath, 'CLAUDE.md');
|
|
137
171
|
const globalClaudeMd = joinPath(homedir(), '.claude', 'CLAUDE.md');
|
|
172
|
+
const projectAgentsMd = joinPath(projectPath, 'AGENTS.md');
|
|
173
|
+
const globalAgentsMd = joinPath(homedir(), '.config', 'opencode', 'AGENTS.md');
|
|
138
174
|
|
|
139
175
|
const projectExists = existsSync(projectClaudeMd);
|
|
140
176
|
const projectHasAgent = hasAgentMarker(projectClaudeMd);
|
|
141
177
|
const globalExists = existsSync(globalClaudeMd);
|
|
142
178
|
const globalHasAgent = hasAgentMarker(globalClaudeMd);
|
|
143
179
|
|
|
180
|
+
const agentsMdProjectExists = existsSync(projectAgentsMd);
|
|
181
|
+
const agentsMdProjectHasAgent = hasAgentMarkerInAgentsMd(projectAgentsMd);
|
|
182
|
+
const agentsMdGlobalExists = existsSync(globalAgentsMd);
|
|
183
|
+
const agentsMdGlobalHasAgent = hasAgentMarkerInAgentsMd(globalAgentsMd);
|
|
184
|
+
|
|
144
185
|
const s = runtime.vault.stats();
|
|
145
186
|
|
|
146
187
|
const recommendations: string[] = [];
|
|
@@ -149,6 +190,9 @@ async function main(): Promise<void> {
|
|
|
149
190
|
} else if (!globalHasAgent) {
|
|
150
191
|
recommendations.push('Global ~/.claude/CLAUDE.md not configured — run inject_claude_md with global: true to enable in all projects');
|
|
151
192
|
}
|
|
193
|
+
if (!agentsMdGlobalHasAgent && !agentsMdProjectHasAgent) {
|
|
194
|
+
recommendations.push('No AGENTS.md configured — run inject_agents_md for OpenCode/Codex support');
|
|
195
|
+
}
|
|
152
196
|
if (s.totalEntries === 0) {
|
|
153
197
|
recommendations.push('Vault is empty — add intelligence data or capture knowledge via domain facades');
|
|
154
198
|
}
|
|
@@ -193,6 +237,10 @@ async function main(): Promise<void> {
|
|
|
193
237
|
project: { exists: projectExists, has_agent_section: projectHasAgent },
|
|
194
238
|
global: { exists: globalExists, has_agent_section: globalHasAgent },
|
|
195
239
|
},
|
|
240
|
+
agents_md: {
|
|
241
|
+
project: { exists: agentsMdProjectExists, has_agent_section: agentsMdProjectHasAgent },
|
|
242
|
+
global: { exists: agentsMdGlobalExists, has_agent_section: agentsMdGlobalHasAgent },
|
|
243
|
+
},
|
|
196
244
|
vault: { entries: s.totalEntries, domains: Object.keys(s.byDomain) },
|
|
197
245
|
hooks: hookStatus,
|
|
198
246
|
recommendations,
|
|
@@ -209,7 +257,43 @@ async function main(): Promise<void> {
|
|
|
209
257
|
ops: agentOps,
|
|
210
258
|
};
|
|
211
259
|
|
|
212
|
-
|
|
260
|
+
${
|
|
261
|
+
hasDomainPacks
|
|
262
|
+
? ` // ─── Domain packs ─────────────────────────────────────────────
|
|
263
|
+
const domainPacks = await loadDomainPacksFromConfig(${JSON.stringify(config.domainPacks)});
|
|
264
|
+
console.error(\`[\${tag}] Loaded \${domainPacks.length} domain packs\`);
|
|
265
|
+
for (const pack of domainPacks) {
|
|
266
|
+
if (pack.onActivate) await pack.onActivate(runtime);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── Capability Registry ─────────────────────────────────────
|
|
270
|
+
const capabilityRegistry = new CapabilityRegistry();
|
|
271
|
+
const packRuntime = createPackRuntime(runtime);
|
|
272
|
+
|
|
273
|
+
// Register domain pack capabilities
|
|
274
|
+
for (const pack of domainPacks) {
|
|
275
|
+
if (pack.capabilities) {
|
|
276
|
+
const handlers = pack.capabilities(packRuntime);
|
|
277
|
+
// Use pack manifest capabilities if available, otherwise derive from handler keys
|
|
278
|
+
const definitions = (pack as Record<string, unknown>).manifest
|
|
279
|
+
? ((pack as Record<string, unknown>).manifest as { capabilities?: Array<{ id: string; description: string; provides: string[]; requires: string[] }> }).capabilities ?? []
|
|
280
|
+
: [...handlers.keys()].map(id => ({ id, description: id, provides: [], requires: [] }));
|
|
281
|
+
capabilityRegistry.registerPack(pack.name, definitions, handlers, 50);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Validate flows against installed capabilities
|
|
286
|
+
const flows = loadAllFlows();
|
|
287
|
+
for (const flow of flows) {
|
|
288
|
+
const validation = capabilityRegistry.validateFlow(flow);
|
|
289
|
+
if (validation.missing.length > 0) {
|
|
290
|
+
console.error(\`[\${tag}] Flow \${flow.id}: \${validation.missing.length} capabilities degraded (\${validation.missing.join(', ')})\`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
console.error(\`[\${tag}] Capability registry: \${capabilityRegistry.size} capabilities from \${capabilityRegistry.packCount} pack(s)\`);
|
|
294
|
+
`
|
|
295
|
+
: ''
|
|
296
|
+
} const domainFacades = createDomainFacades(runtime, '${config.id}', ${domainsLiteral}${hasDomainPacks ? `, domainPacks` : ''});
|
|
213
297
|
|
|
214
298
|
// ─── User extensions (auto-discovered from src/extensions/) ────
|
|
215
299
|
let extensions: AgentExtensions = {};
|
|
@@ -44,6 +44,10 @@ export function generateInjectClaudeMd(config: AgentConfig): string {
|
|
|
44
44
|
' if (existing.includes(startMarker)) {',
|
|
45
45
|
' const startIdx = existing.indexOf(startMarker);',
|
|
46
46
|
' const endIdx = existing.indexOf(endMarker);',
|
|
47
|
+
' if (endIdx !== -1) {',
|
|
48
|
+
' const currentBlock = existing.slice(startIdx, endIdx + endMarker.length);',
|
|
49
|
+
" if (currentBlock === content.trim()) return 'skipped';",
|
|
50
|
+
' }',
|
|
47
51
|
' if (endIdx === -1) {',
|
|
48
52
|
" const updated = existing.slice(0, startIdx) + content + '\\n' + existing.slice(startIdx + startMarker.length);",
|
|
49
53
|
" writeFileSync(filePath, updated, 'utf-8');",
|
|
@@ -166,5 +170,54 @@ export function generateInjectClaudeMd(config: AgentConfig): string {
|
|
|
166
170
|
" const content = readFileSync(filePath, 'utf-8');",
|
|
167
171
|
" return content.includes('<!-- ' + getEngineRulesMarker() + ' -->');",
|
|
168
172
|
'}',
|
|
173
|
+
'',
|
|
174
|
+
'// ─── AGENTS.md support (OpenCode, Codex) ──────────────────────',
|
|
175
|
+
'',
|
|
176
|
+
'/**',
|
|
177
|
+
' * Inject agent sections into a project AGENTS.md.',
|
|
178
|
+
' * Same engine rules + agent block as CLAUDE.md, targeting AGENTS.md instead.',
|
|
179
|
+
' * OpenCode reads AGENTS.md as its primary instruction file.',
|
|
180
|
+
' */',
|
|
181
|
+
'export function injectAgentsMd(projectPath: string): InjectResult {',
|
|
182
|
+
" return injectIntoFile(join(projectPath, 'AGENTS.md'));",
|
|
183
|
+
'}',
|
|
184
|
+
'',
|
|
185
|
+
'/**',
|
|
186
|
+
' * Inject into the global ~/.config/opencode/AGENTS.md.',
|
|
187
|
+
" * Creates ~/.config/opencode/ directory if it doesn't exist.",
|
|
188
|
+
' * This makes the activation phrase work in any OpenCode project.',
|
|
189
|
+
' */',
|
|
190
|
+
'export function injectAgentsMdGlobal(): InjectResult {',
|
|
191
|
+
" const opencodeDir = join(homedir(), '.config', 'opencode');",
|
|
192
|
+
' if (!existsSync(opencodeDir)) {',
|
|
193
|
+
' mkdirSync(opencodeDir, { recursive: true });',
|
|
194
|
+
' }',
|
|
195
|
+
" return injectIntoFile(join(opencodeDir, 'AGENTS.md'));",
|
|
196
|
+
'}',
|
|
197
|
+
'',
|
|
198
|
+
'/**',
|
|
199
|
+
' * Remove agent section from a project AGENTS.md.',
|
|
200
|
+
' */',
|
|
201
|
+
'export function removeAgentsMd(projectPath: string): { removed: boolean; path: string } {',
|
|
202
|
+
" const filePath = join(projectPath, 'AGENTS.md');",
|
|
203
|
+
' return { removed: removeBlock(filePath, getClaudeMdMarker()), path: filePath };',
|
|
204
|
+
'}',
|
|
205
|
+
'',
|
|
206
|
+
'/**',
|
|
207
|
+
' * Remove agent section from the global ~/.config/opencode/AGENTS.md.',
|
|
208
|
+
' */',
|
|
209
|
+
'export function removeAgentsMdGlobal(): { removed: boolean; path: string } {',
|
|
210
|
+
" const filePath = join(homedir(), '.config', 'opencode', 'AGENTS.md');",
|
|
211
|
+
' return { removed: removeBlock(filePath, getClaudeMdMarker()), path: filePath };',
|
|
212
|
+
'}',
|
|
213
|
+
'',
|
|
214
|
+
'/**',
|
|
215
|
+
' * Check if the agent marker exists in an AGENTS.md file.',
|
|
216
|
+
' */',
|
|
217
|
+
'export function hasAgentMarkerInAgentsMd(filePath: string): boolean {',
|
|
218
|
+
' if (!existsSync(filePath)) return false;',
|
|
219
|
+
" const content = readFileSync(filePath, 'utf-8');",
|
|
220
|
+
` return content.includes('<!-- ${marker} -->');`,
|
|
221
|
+
'}',
|
|
169
222
|
].join('\n');
|
|
170
223
|
}
|
|
@@ -2,7 +2,7 @@ import type { AgentConfig } from '../types.js';
|
|
|
2
2
|
|
|
3
3
|
export function generatePackageJson(config: AgentConfig): string {
|
|
4
4
|
const pkg = {
|
|
5
|
-
name:
|
|
5
|
+
name: config.id,
|
|
6
6
|
version: '1.0.0',
|
|
7
7
|
description: config.description,
|
|
8
8
|
type: 'module',
|
|
@@ -31,6 +31,9 @@ export function generatePackageJson(config: AgentConfig): string {
|
|
|
31
31
|
'@soleri/core': '^2.0.0',
|
|
32
32
|
zod: '^3.24.2',
|
|
33
33
|
...(config.telegram ? { grammy: '^1.35.0' } : {}),
|
|
34
|
+
...(config.domainPacks?.length
|
|
35
|
+
? Object.fromEntries(config.domainPacks.map((pack) => [pack.package, pack.version ?? '*']))
|
|
36
|
+
: {}),
|
|
34
37
|
},
|
|
35
38
|
optionalDependencies: {
|
|
36
39
|
'@anthropic-ai/sdk': '^0.39.0',
|
package/src/templates/readme.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { AgentConfig } from '../types.js';
|
|
|
4
4
|
* Generate a README.md for the scaffolded agent.
|
|
5
5
|
*/
|
|
6
6
|
export function generateReadme(config: AgentConfig): string {
|
|
7
|
+
const skillCount = config.skills?.length ?? 17;
|
|
7
8
|
const setupTarget = config.setupTarget ?? 'claude';
|
|
8
9
|
const claudeSetup = setupTarget === 'claude' || setupTarget === 'both';
|
|
9
10
|
const codexSetup = setupTarget === 'codex' || setupTarget === 'both';
|
|
@@ -78,10 +79,10 @@ export function generateReadme(config: AgentConfig): string {
|
|
|
78
79
|
|
|
79
80
|
const skillsLead =
|
|
80
81
|
claudeSetup && codexSetup
|
|
81
|
-
? `${config.name} ships with
|
|
82
|
+
? `${config.name} ships with ${skillCount} structured workflow skills. In Claude Code they are invocable via \`/<skill-name>\`; in Codex they are available via generated AGENTS.md + local skill files.`
|
|
82
83
|
: claudeSetup
|
|
83
|
-
? `${config.name} ships with
|
|
84
|
-
: `${config.name} ships with
|
|
84
|
+
? `${config.name} ships with ${skillCount} structured workflow skills, invocable via \`/<skill-name>\` in Claude Code:`
|
|
85
|
+
: `${config.name} ships with ${skillCount} structured workflow skills, available via generated AGENTS.md + local skill files in Codex:`;
|
|
85
86
|
|
|
86
87
|
const skillsInstallNote =
|
|
87
88
|
claudeSetup && codexSetup
|
|
@@ -6,10 +6,15 @@ import type { AgentConfig } from '../types.js';
|
|
|
6
6
|
*/
|
|
7
7
|
export function generateSetupScript(config: AgentConfig): string {
|
|
8
8
|
const setupTarget = config.setupTarget ?? 'claude';
|
|
9
|
-
const claudeSetup = setupTarget === 'claude' || setupTarget === 'both';
|
|
10
|
-
const codexSetup = setupTarget === 'codex' || setupTarget === 'both';
|
|
11
|
-
const
|
|
12
|
-
|
|
9
|
+
const claudeSetup = setupTarget === 'claude' || setupTarget === 'both' || setupTarget === 'all';
|
|
10
|
+
const codexSetup = setupTarget === 'codex' || setupTarget === 'both' || setupTarget === 'all';
|
|
11
|
+
const opencodeSetup = setupTarget === 'opencode' || setupTarget === 'all';
|
|
12
|
+
const hostParts = [
|
|
13
|
+
...(claudeSetup ? ['Claude Code'] : []),
|
|
14
|
+
...(codexSetup ? ['Codex'] : []),
|
|
15
|
+
...(opencodeSetup ? ['OpenCode'] : []),
|
|
16
|
+
];
|
|
17
|
+
const hostLabel = hostParts.join(' + ');
|
|
13
18
|
|
|
14
19
|
const claudeSection = claudeSetup
|
|
15
20
|
? `
|
|
@@ -195,6 +200,103 @@ if [ -d "$SKILLS_DIR" ]; then
|
|
|
195
200
|
done
|
|
196
201
|
echo "[ok] Codex skills: $skill_installed installed, $skill_skipped already present"
|
|
197
202
|
fi
|
|
203
|
+
`
|
|
204
|
+
: '';
|
|
205
|
+
|
|
206
|
+
const opencodeSection = opencodeSetup
|
|
207
|
+
? `
|
|
208
|
+
# Check and install OpenCode (Soleri fork with title branding)
|
|
209
|
+
if ! command -v opencode &>/dev/null; then
|
|
210
|
+
echo ""
|
|
211
|
+
INSTALLED=false
|
|
212
|
+
# Try Go install from Soleri fork (supports title branding)
|
|
213
|
+
if command -v go &>/dev/null; then
|
|
214
|
+
echo "Installing OpenCode (Soleri fork) via go install..."
|
|
215
|
+
if go install github.com/adrozdenko/opencode@latest 2>/dev/null; then
|
|
216
|
+
if command -v opencode &>/dev/null; then
|
|
217
|
+
echo "[ok] OpenCode installed from Soleri fork ($(opencode --version 2>/dev/null || echo 'installed'))"
|
|
218
|
+
INSTALLED=true
|
|
219
|
+
fi
|
|
220
|
+
fi
|
|
221
|
+
fi
|
|
222
|
+
# Fallback: upstream npm package (no title branding)
|
|
223
|
+
if [ "$INSTALLED" = false ]; then
|
|
224
|
+
echo "Installing OpenCode via npm (upstream — title branding requires Go)..."
|
|
225
|
+
npm install -g opencode-ai
|
|
226
|
+
if command -v opencode &>/dev/null; then
|
|
227
|
+
echo "[ok] OpenCode installed ($(opencode --version 2>/dev/null || echo 'unknown version'))"
|
|
228
|
+
else
|
|
229
|
+
echo ""
|
|
230
|
+
echo "Warning: Could not install OpenCode automatically."
|
|
231
|
+
echo "Install manually using one of:"
|
|
232
|
+
echo " go install github.com/adrozdenko/opencode@latest (recommended — includes title branding)"
|
|
233
|
+
echo " npm install -g opencode-ai (upstream)"
|
|
234
|
+
echo ""
|
|
235
|
+
fi
|
|
236
|
+
fi
|
|
237
|
+
else
|
|
238
|
+
echo "[ok] OpenCode found ($(opencode --version 2>/dev/null || echo 'installed'))"
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
# Register MCP server with OpenCode
|
|
242
|
+
echo ""
|
|
243
|
+
echo "Registering ${config.name} with OpenCode..."
|
|
244
|
+
OPENCODE_CONFIG="$HOME/.opencode.json"
|
|
245
|
+
AGENT_DIST="$AGENT_DIR/dist/index.js"
|
|
246
|
+
|
|
247
|
+
OPENCODE_CONFIG="$OPENCODE_CONFIG" AGENT_NAME="$AGENT_NAME" AGENT_DIST="$AGENT_DIST" node <<'NODE'
|
|
248
|
+
const fs = require('node:fs');
|
|
249
|
+
const path = process.env.OPENCODE_CONFIG;
|
|
250
|
+
const agentName = process.env.AGENT_NAME;
|
|
251
|
+
const distPath = process.env.AGENT_DIST;
|
|
252
|
+
|
|
253
|
+
let config = {};
|
|
254
|
+
if (fs.existsSync(path)) {
|
|
255
|
+
try {
|
|
256
|
+
const raw = fs.readFileSync(path, 'utf-8');
|
|
257
|
+
const stripped = raw.replace(/^\\s*\\/\\/.*$/gm, '');
|
|
258
|
+
config = JSON.parse(stripped);
|
|
259
|
+
} catch {
|
|
260
|
+
config = {};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!config.mcp || typeof config.mcp !== 'object') {
|
|
265
|
+
config.mcp = {};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
config.mcp[agentName] = {
|
|
269
|
+
type: 'local',
|
|
270
|
+
command: ['node', distPath],
|
|
271
|
+
enabled: true,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
fs.writeFileSync(path, JSON.stringify(config, null, 2) + '\\n', 'utf-8');
|
|
275
|
+
NODE
|
|
276
|
+
echo "[ok] Registered ${config.name} as MCP server (OpenCode)"
|
|
277
|
+
|
|
278
|
+
# Create launcher script — type "${config.id}" to start OpenCode
|
|
279
|
+
LAUNCHER_PATH="/usr/local/bin/$AGENT_NAME"
|
|
280
|
+
LAUNCHER_CONTENT="#!/usr/bin/env bash
|
|
281
|
+
# Soleri agent launcher — starts OpenCode with $AGENT_NAME MCP agent
|
|
282
|
+
cd \\"$AGENT_DIR\\" || exit 1
|
|
283
|
+
exec opencode \\"\\$@\\""
|
|
284
|
+
|
|
285
|
+
if [ -w "/usr/local/bin" ]; then
|
|
286
|
+
echo "$LAUNCHER_CONTENT" > "$LAUNCHER_PATH"
|
|
287
|
+
chmod +x "$LAUNCHER_PATH"
|
|
288
|
+
echo "[ok] Launcher created: type \\"${config.id}\\" to start OpenCode"
|
|
289
|
+
else
|
|
290
|
+
echo "$LAUNCHER_CONTENT" > "$AGENT_DIR/scripts/$AGENT_NAME"
|
|
291
|
+
chmod +x "$AGENT_DIR/scripts/$AGENT_NAME"
|
|
292
|
+
if command -v sudo &>/dev/null; then
|
|
293
|
+
sudo ln -sf "$AGENT_DIR/scripts/$AGENT_NAME" "$LAUNCHER_PATH" 2>/dev/null && \\
|
|
294
|
+
echo "[ok] Launcher created: type \\"${config.id}\\" to start OpenCode" || \\
|
|
295
|
+
echo "Note: Run 'sudo ln -sf $AGENT_DIR/scripts/$AGENT_NAME $LAUNCHER_PATH' to enable \\"${config.id}\\" command"
|
|
296
|
+
else
|
|
297
|
+
echo "Note: Add $AGENT_DIR/scripts to PATH, or symlink $AGENT_DIR/scripts/$AGENT_NAME to /usr/local/bin/$AGENT_NAME"
|
|
298
|
+
fi
|
|
299
|
+
fi
|
|
198
300
|
`
|
|
199
301
|
: '';
|
|
200
302
|
|
|
@@ -207,6 +309,9 @@ fi
|
|
|
207
309
|
? ['echo " - Start a new Claude Code session (or restart if one is open)"']
|
|
208
310
|
: []),
|
|
209
311
|
...(codexSetup ? ['echo " - Start a new Codex session (or restart if one is open)"'] : []),
|
|
312
|
+
...(opencodeSetup
|
|
313
|
+
? ['echo " - Start a new OpenCode session (or restart if one is open)"']
|
|
314
|
+
: []),
|
|
210
315
|
`echo " - Say: \\"Hello, ${config.name}!\\""`,
|
|
211
316
|
'echo ""',
|
|
212
317
|
`echo "${config.name} is ready."`,
|
|
@@ -248,6 +353,7 @@ fi
|
|
|
248
353
|
${claudeSection}
|
|
249
354
|
${hookPackSection}
|
|
250
355
|
${codexSection}
|
|
356
|
+
${opencodeSection}
|
|
251
357
|
${nextSteps}
|
|
252
358
|
`;
|
|
253
359
|
}
|
|
@@ -28,6 +28,39 @@ const ENGINE_RULES_LINES: string[] = [
|
|
|
28
28
|
"Shared behavioral rules for all Soleri agents. The active agent's facade table provides tool names.",
|
|
29
29
|
'',
|
|
30
30
|
|
|
31
|
+
// ─── What is Soleri ─────────────────────────────────────
|
|
32
|
+
'## What is Soleri',
|
|
33
|
+
'<!-- soleri:what-is-soleri -->',
|
|
34
|
+
'',
|
|
35
|
+
'You are powered by the **Soleri engine** — an intelligence framework that makes AI agents learn, remember, and improve over time. You are not a stateless chatbot. You are a knowledge-driven agent with:',
|
|
36
|
+
'',
|
|
37
|
+
"- **Vault** — your knowledge graph (Zettelkasten). Patterns, anti-patterns, principles you've learned. Grows with every session.",
|
|
38
|
+
'- **Brain** — pattern learning loop. Tracks what works (strengths) and recommends approaches based on past success.',
|
|
39
|
+
'- **Memory** — session history that persists across conversations and projects.',
|
|
40
|
+
'- **Planning** — structured workflow: plan → approve → execute → reconcile → capture knowledge.',
|
|
41
|
+
'- **Packs** — installable capability bundles (knowledge + skills + hooks). Add domains without code changes.',
|
|
42
|
+
'',
|
|
43
|
+
'### The 5-Step Rhythm',
|
|
44
|
+
'',
|
|
45
|
+
'Every task follows this cycle — each iteration makes the next one better:',
|
|
46
|
+
'',
|
|
47
|
+
'1. **Search** — check vault for existing patterns before deciding anything',
|
|
48
|
+
'2. **Plan** — create a structured plan, get user approval',
|
|
49
|
+
'3. **Work** — execute with vault-informed decisions',
|
|
50
|
+
'4. **Capture** — persist what you learned (patterns, anti-patterns, decisions)',
|
|
51
|
+
'5. **Complete** — reconcile, capture knowledge, feed the brain',
|
|
52
|
+
'',
|
|
53
|
+
'### Growing Your Capabilities',
|
|
54
|
+
'',
|
|
55
|
+
'You start with core capabilities (vault, brain, planning, memory). To add more:',
|
|
56
|
+
'',
|
|
57
|
+
'- **Install packs**: `soleri pack install <name>` — adds knowledge, skills, and hooks for a domain',
|
|
58
|
+
'- **Capture knowledge**: every pattern you capture makes you smarter for next time',
|
|
59
|
+
'- **Add domains**: `soleri add-domain <name>` — expands your expertise',
|
|
60
|
+
'',
|
|
61
|
+
'When a user asks "what can you do?" — list your current domains and capabilities from your activation context, not a generic list.',
|
|
62
|
+
'',
|
|
63
|
+
|
|
31
64
|
// ─── Response Integrity ─────────────────────────────────
|
|
32
65
|
'## Response Integrity',
|
|
33
66
|
'<!-- soleri:response-integrity -->',
|
|
@@ -48,14 +81,17 @@ const ENGINE_RULES_LINES: string[] = [
|
|
|
48
81
|
"If in doubt, don't save. Less memory with high signal beats more memory with noise.",
|
|
49
82
|
'',
|
|
50
83
|
|
|
51
|
-
// ─── Vault-First Protocol
|
|
52
|
-
'## Vault as Source of Truth',
|
|
84
|
+
// ─── Vault-First Protocol (Zettelkasten) ─────────────────
|
|
85
|
+
'## Vault as Source of Truth (Zettelkasten)',
|
|
53
86
|
'<!-- soleri:vault-protocol -->',
|
|
54
87
|
'',
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'-
|
|
58
|
-
'-
|
|
88
|
+
'The vault is a **Zettelkasten** — a connected knowledge graph. Every knowledge operation follows Zettelkasten principles: atomic entries, typed links, dense connections.',
|
|
89
|
+
'',
|
|
90
|
+
'- **MANDATORY**: Consult the vault BEFORE every decision — search + traverse the link graph.',
|
|
91
|
+
'- Lookup order: 1) VAULT search → 2) VAULT traverse (follow links 2 hops) → 3) MEMORY → 4) CODEBASE → 5) WEB/TRAINING.',
|
|
92
|
+
"- **Search + Traverse**: Don't just search — traverse from the best result to discover connected knowledge and anti-patterns.",
|
|
93
|
+
'- Check `contradicts` links to know what to avoid. Check `sequences` links for ordering dependencies.',
|
|
94
|
+
'- Persist lessons: capture + link. An unlinked entry is incomplete.',
|
|
59
95
|
'- Exceptions: runtime errors with stack traces → codebase first; user explicitly asks to search web.',
|
|
60
96
|
'',
|
|
61
97
|
|
|
@@ -163,20 +199,25 @@ const ENGINE_RULES_LINES: string[] = [
|
|
|
163
199
|
'```',
|
|
164
200
|
'',
|
|
165
201
|
|
|
166
|
-
// ─── Knowledge Capture
|
|
202
|
+
// ─── Knowledge Capture (Zettelkasten) ───────────────────
|
|
167
203
|
'## Knowledge Capture',
|
|
168
204
|
'<!-- soleri:knowledge-capture -->',
|
|
169
205
|
'',
|
|
170
|
-
"**MANDATORY**: Persist lessons, don't just promise them.",
|
|
206
|
+
"**MANDATORY**: Persist lessons, don't just promise them. **Always link after capturing.**",
|
|
171
207
|
'',
|
|
172
208
|
'When you learn something that should persist:',
|
|
173
209
|
'1. **DON\'T** just say "I will remember this"',
|
|
174
210
|
'2. **DO** call `op:capture_knowledge` to persist to vault',
|
|
175
|
-
|
|
211
|
+
'3. **DO** review `suggestedLinks` in the capture response',
|
|
212
|
+
'4. **DO** create links for relevant suggestions: `op:link_entries`',
|
|
213
|
+
"5. **DO** update relevant files if it's a behavioral change",
|
|
214
|
+
'',
|
|
215
|
+
'An unlinked entry is an orphan — it adds noise, not knowledge.',
|
|
176
216
|
'',
|
|
177
217
|
'| Type | Op | Persists To |',
|
|
178
218
|
'|------|-----|-------------|',
|
|
179
219
|
'| Patterns/Anti-patterns | `op:capture_knowledge` | vault |',
|
|
220
|
+
'| Links between entries | `op:link_entries` | vault_links table |',
|
|
180
221
|
'| Quick capture | `op:capture_quick` | vault |',
|
|
181
222
|
'| Session summaries | `op:session_capture` | memory |',
|
|
182
223
|
'',
|
|
@@ -277,12 +318,9 @@ const ENGINE_RULES_LINES: string[] = [
|
|
|
277
318
|
'',
|
|
278
319
|
'### Session Start Protocol',
|
|
279
320
|
'',
|
|
280
|
-
'
|
|
281
|
-
'
|
|
282
|
-
'
|
|
283
|
-
'3. Check for plans awaiting reconciliation via `op:check_persistence`:',
|
|
284
|
-
' - `executing` → Remind user to call `op:plan_reconcile`.',
|
|
285
|
-
' - `reconciling` → Remind user to call `op:plan_complete_lifecycle`.',
|
|
321
|
+
'Do NOT call tools automatically on session start — just greet the user in character.',
|
|
322
|
+
'Call `op:register` only when you need project context for a task (not on every message).',
|
|
323
|
+
'Call `op:activate` only when checking evolved capabilities or recovering session state.',
|
|
286
324
|
'',
|
|
287
325
|
'### Context Compaction',
|
|
288
326
|
'',
|
|
@@ -302,7 +340,7 @@ const ENGINE_RULES_LINES: string[] = [
|
|
|
302
340
|
'|---------|-------------|',
|
|
303
341
|
'| `soleri agent status` | Health check — version, packs, vault, update availability |',
|
|
304
342
|
'| `soleri agent update` | Update engine to latest compatible version (`--check` for dry run) |',
|
|
305
|
-
'| `soleri agent refresh` | Regenerate CLAUDE.md from latest forge templates (`--dry-run` to preview) |',
|
|
343
|
+
'| `soleri agent refresh` | Regenerate AGENTS.md/CLAUDE.md from latest forge templates (`--dry-run` to preview) |',
|
|
306
344
|
'| `soleri agent diff` | Show drift between current templates and latest engine |',
|
|
307
345
|
'| `soleri doctor` | Full system health and project status check |',
|
|
308
346
|
'| `soleri dev` | Run agent in development mode (stdio MCP) |',
|
|
@@ -343,7 +381,7 @@ const ENGINE_RULES_LINES: string[] = [
|
|
|
343
381
|
'',
|
|
344
382
|
'| Command | What it does |',
|
|
345
383
|
'|---------|-------------|',
|
|
346
|
-
'| `soleri install` | Register agent as MCP server (`--target claude\\|codex\\|
|
|
384
|
+
'| `soleri install` | Register agent as MCP server (`--target opencode\\|claude\\|codex\\|all`) |',
|
|
347
385
|
'| `soleri uninstall` | Remove agent MCP server entry |',
|
|
348
386
|
'| `soleri governance --show` | Show vault governance policy |',
|
|
349
387
|
'| `soleri governance --preset <name>` | Set policy preset (strict\\|moderate\\|permissive) |',
|