@soleri/cli 1.12.4 → 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.
Files changed (41) hide show
  1. package/dist/commands/add-pack.d.ts +2 -0
  2. package/dist/commands/add-pack.js +154 -0
  3. package/dist/commands/add-pack.js.map +1 -0
  4. package/dist/commands/agent.js +152 -3
  5. package/dist/commands/agent.js.map +1 -1
  6. package/dist/commands/cognee.d.ts +10 -0
  7. package/dist/commands/cognee.js +364 -0
  8. package/dist/commands/cognee.js.map +1 -0
  9. package/dist/commands/create.js +63 -5
  10. package/dist/commands/create.js.map +1 -1
  11. package/dist/commands/dev.js +104 -17
  12. package/dist/commands/dev.js.map +1 -1
  13. package/dist/commands/install.js +70 -18
  14. package/dist/commands/install.js.map +1 -1
  15. package/dist/commands/telegram.d.ts +10 -0
  16. package/dist/commands/telegram.js +423 -0
  17. package/dist/commands/telegram.js.map +1 -0
  18. package/dist/commands/uninstall.js +35 -6
  19. package/dist/commands/uninstall.js.map +1 -1
  20. package/dist/main.js +6 -0
  21. package/dist/main.js.map +1 -1
  22. package/dist/prompts/create-wizard.js +87 -6
  23. package/dist/prompts/create-wizard.js.map +1 -1
  24. package/dist/prompts/playbook.js +1 -1
  25. package/dist/prompts/playbook.js.map +1 -1
  26. package/dist/utils/agent-context.d.ts +9 -2
  27. package/dist/utils/agent-context.js +32 -0
  28. package/dist/utils/agent-context.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/commands/add-pack.ts +170 -0
  31. package/src/commands/agent.ts +174 -3
  32. package/src/commands/cognee.ts +416 -0
  33. package/src/commands/create.ts +90 -6
  34. package/src/commands/dev.ts +114 -18
  35. package/src/commands/install.ts +78 -19
  36. package/src/commands/telegram.ts +488 -0
  37. package/src/commands/uninstall.ts +41 -7
  38. package/src/main.ts +6 -0
  39. package/src/prompts/create-wizard.ts +93 -7
  40. package/src/prompts/playbook.ts +1 -1
  41. package/src/utils/agent-context.ts +39 -2
@@ -8,6 +8,7 @@ import {
8
8
  AgentConfigSchema,
9
9
  SETUP_TARGETS,
10
10
  type SetupTarget,
11
+ scaffoldFileTree,
11
12
  } from '@soleri/forge/lib';
12
13
  import { runCreateWizard } from '../prompts/create-wizard.js';
13
14
  import { listPacks } from '../hook-packs/registry.js';
@@ -21,9 +22,9 @@ function parseSetupTarget(value?: string): SetupTarget | undefined {
21
22
  return undefined;
22
23
  }
23
24
 
24
- function includesClaudeSetup(target: SetupTarget | undefined): boolean {
25
- const resolved = target ?? 'claude';
26
- return resolved === 'claude' || resolved === 'both';
25
+ function includesClaudeSetup(target: SetupTarget | string | undefined): boolean {
26
+ const resolved = target ?? 'opencode';
27
+ return resolved === 'claude' || resolved === 'both' || resolved === 'all';
27
28
  }
28
29
 
29
30
  export function registerCreate(program: Command): void {
@@ -36,9 +37,20 @@ export function registerCreate(program: Command): void {
36
37
  `Setup target: ${SETUP_TARGETS.join(', ')} (default: claude)`,
37
38
  )
38
39
  .option('-y, --yes', 'Skip confirmation prompts (use with --config for fully non-interactive)')
40
+ .option('--filetree', 'Create a file-tree agent (v7 — no TypeScript, no build step)')
41
+ .option('--legacy', 'Create a legacy TypeScript agent (v6 — requires npm install + build)')
39
42
  .description('Create a new Soleri agent')
40
43
  .action(
41
- async (name?: string, opts?: { config?: string; yes?: boolean; setupTarget?: string }) => {
44
+ async (
45
+ name?: string,
46
+ opts?: {
47
+ config?: string;
48
+ yes?: boolean;
49
+ setupTarget?: string;
50
+ filetree?: boolean;
51
+ legacy?: boolean;
52
+ },
53
+ ) => {
42
54
  try {
43
55
  let config;
44
56
 
@@ -81,6 +93,78 @@ export function registerCreate(program: Command): void {
81
93
  if (setupTarget) {
82
94
  config = { ...config, setupTarget };
83
95
  }
96
+ // ─── File-tree agent (v7) ──────────────────────────────
97
+ // Default to filetree unless --legacy is explicitly passed
98
+ const useFileTree = opts?.filetree || !opts?.legacy;
99
+
100
+ if (useFileTree) {
101
+ // Convert to AgentYaml format
102
+ // Cast to Record to access fields that may exist on the parsed config
103
+ // but aren't in the strict AgentConfig type (model, cognee, vaults, domainPacks)
104
+ const raw = config as Record<string, unknown>;
105
+ const agentYamlInput = {
106
+ id: config.id,
107
+ name: config.name,
108
+ role: config.role,
109
+ description: config.description,
110
+ domains: config.domains,
111
+ principles: config.principles,
112
+ tone: config.tone,
113
+ greeting: config.greeting,
114
+ setup: {
115
+ target: config.setupTarget,
116
+ model: (raw.model as string) ?? 'claude-code-sonnet-4',
117
+ },
118
+ engine: {
119
+ cognee: (raw.cognee as boolean) ?? false,
120
+ },
121
+ vaults: raw.vaults as
122
+ | Array<{ name: string; path: string; priority?: number }>
123
+ | undefined,
124
+ packs: (
125
+ raw.domainPacks as
126
+ | Array<{ name: string; package: string; version?: string }>
127
+ | undefined
128
+ )?.map((dp) => ({
129
+ name: dp.name,
130
+ package: dp.package,
131
+ version: dp.version,
132
+ })),
133
+ };
134
+
135
+ const outputDir = config.outputDir ?? process.cwd();
136
+ const nonInteractive = !!(opts?.yes || opts?.config);
137
+
138
+ if (!nonInteractive) {
139
+ p.log.info(
140
+ `Will create file-tree agent "${config.name}" in ${outputDir}/${config.id}`,
141
+ );
142
+ p.log.info(`Domains: ${config.domains.join(', ')}`);
143
+ p.log.info('No build step — agent is ready to use immediately.');
144
+
145
+ const confirmed = await p.confirm({ message: 'Create agent?' });
146
+ if (p.isCancel(confirmed) || !confirmed) {
147
+ p.outro('Cancelled.');
148
+ return;
149
+ }
150
+ }
151
+
152
+ const s = p.spinner();
153
+ s.start('Creating file-tree agent...');
154
+ const result = scaffoldFileTree(agentYamlInput, outputDir);
155
+ s.stop(result.success ? 'Agent created!' : 'Creation failed');
156
+
157
+ if (!result.success) {
158
+ p.log.error(result.summary);
159
+ process.exit(1);
160
+ }
161
+
162
+ p.note(result.summary, 'Next steps');
163
+ p.outro('Done!');
164
+ return;
165
+ }
166
+
167
+ // ─── Legacy TypeScript agent (v6) ─────────────────────
84
168
  const claudeSetup = includesClaudeSetup(config.setupTarget);
85
169
 
86
170
  const nonInteractive = !!(opts?.yes || opts?.config);
@@ -98,9 +182,9 @@ export function registerCreate(program: Command): void {
98
182
  const available = listPacks().map((pk) => pk.name);
99
183
  const unknown = selectedPacks.filter((pk) => !available.includes(pk));
100
184
  if (unknown.length > 0) {
101
- for (const name of unknown) {
185
+ for (const packName of unknown) {
102
186
  p.log.warn(
103
- `Unknown hook pack "${name}" — skipping. Available: ${available.join(', ')}`,
187
+ `Unknown hook pack "${packName}" — skipping. Available: ${available.join(', ')}`,
104
188
  );
105
189
  }
106
190
  selectedPacks = selectedPacks.filter((pk) => available.includes(pk));
@@ -1,4 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { watch, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
2
4
  import type { Command } from 'commander';
3
5
  import * as p from '@clack/prompts';
4
6
  import { detectAgent } from '../utils/agent-context.js';
@@ -14,26 +16,120 @@ export function registerDev(program: Command): void {
14
16
  process.exit(1);
15
17
  }
16
18
 
17
- p.log.info(`Starting ${ctx.agentId} in dev mode...`);
19
+ if (ctx.format === 'filetree') {
20
+ // v7: File-tree agent — watch files and regenerate CLAUDE.md
21
+ runFileTreeDev(ctx.agentPath, ctx.agentId);
22
+ } else {
23
+ // Legacy: TypeScript agent — run via tsx
24
+ runLegacyDev(ctx.agentPath, ctx.agentId);
25
+ }
26
+ });
27
+ }
18
28
 
19
- const child = spawn('npx', ['tsx', 'src/index.ts'], {
20
- cwd: ctx.agentPath,
21
- stdio: 'inherit',
22
- env: { ...process.env },
23
- });
29
+ function runFileTreeDev(agentPath: string, agentId: string): void {
30
+ p.log.info(`Starting ${agentId} in file-tree dev mode...`);
31
+ p.log.info('Starting Knowledge Engine + watching for file changes.');
32
+ p.log.info('CLAUDE.md will be regenerated automatically on changes.');
33
+ p.log.info('Press Ctrl+C to stop.\n');
24
34
 
25
- child.on('error', (err) => {
26
- p.log.error(`Failed to start: ${err.message}`);
27
- p.log.info('Make sure tsx is available: npm install -g tsx');
28
- process.exit(1);
29
- });
35
+ regenerateClaudeMd(agentPath);
36
+
37
+ // Start the engine server
38
+ const engineBin = require.resolve('@soleri/core/dist/engine/bin/soleri-engine.js');
39
+ const engine = spawn('node', [engineBin, '--agent', join(agentPath, 'agent.yaml')], {
40
+ stdio: ['pipe', 'inherit', 'inherit'],
41
+ env: { ...process.env },
42
+ });
43
+
44
+ engine.on('error', (err) => {
45
+ p.log.error(`Engine failed to start: ${err.message}`);
46
+ p.log.info('Make sure @soleri/core is built: cd packages/core && npm run build');
47
+ });
48
+
49
+ // Watch directories for changes
50
+ const watchPaths = [
51
+ join(agentPath, 'agent.yaml'),
52
+ join(agentPath, 'instructions'),
53
+ join(agentPath, 'workflows'),
54
+ join(agentPath, 'skills'),
55
+ ];
56
+
57
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
58
+
59
+ for (const watchPath of watchPaths) {
60
+ try {
61
+ watch(watchPath, { recursive: true }, (_event, filename) => {
62
+ // Ignore CLAUDE.md changes (we generate it)
63
+ if (filename === 'CLAUDE.md' || filename === 'AGENTS.md') return;
64
+ // Ignore _engine.md changes (we generate it)
65
+ if (filename === '_engine.md') return;
30
66
 
31
- child.on('exit', (code, signal) => {
32
- if (signal) {
33
- p.log.warn(`Process terminated by signal ${signal}`);
34
- process.exit(1);
35
- }
36
- process.exit(code ?? 0);
67
+ // Debounce regenerate at most once per 200ms
68
+ if (debounceTimer) clearTimeout(debounceTimer);
69
+ debounceTimer = setTimeout(() => {
70
+ const changedFile = filename ? ` (${filename})` : '';
71
+ p.log.info(`Change detected${changedFile} — regenerating CLAUDE.md`);
72
+ regenerateClaudeMd(agentPath);
73
+ }, 200);
37
74
  });
38
- });
75
+ } catch {
76
+ // Directory may not exist yet — that's OK
77
+ }
78
+ }
79
+
80
+ // Graceful shutdown — kill engine too
81
+ const shutdown = () => {
82
+ p.log.info('\nStopping dev mode...');
83
+ engine.kill();
84
+ process.exit(0);
85
+ };
86
+
87
+ process.on('SIGINT', shutdown);
88
+ process.on('SIGTERM', shutdown);
89
+
90
+ engine.on('exit', (code) => {
91
+ if (code !== 0 && code !== null) {
92
+ p.log.error(`Engine exited with code ${code}`);
93
+ process.exit(1);
94
+ }
95
+ });
96
+ }
97
+
98
+ function regenerateClaudeMd(agentPath: string): void {
99
+ try {
100
+ // Dynamic import to avoid loading forge at CLI startup
101
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
102
+ const { composeClaudeMd } = require('@soleri/forge/lib');
103
+ const { content } = composeClaudeMd(agentPath);
104
+ writeFileSync(join(agentPath, 'CLAUDE.md'), content, 'utf-8');
105
+ p.log.success('CLAUDE.md regenerated');
106
+ } catch (err) {
107
+ p.log.error(
108
+ `Failed to regenerate CLAUDE.md: ${err instanceof Error ? err.message : String(err)}`,
109
+ );
110
+ }
111
+ }
112
+
113
+ function runLegacyDev(agentPath: string, agentId: string): void {
114
+ p.log.info(`Starting ${agentId} in dev mode...`);
115
+
116
+ const child = spawn('npx', ['tsx', 'src/index.ts'], {
117
+ cwd: agentPath,
118
+ stdio: 'inherit',
119
+ env: { ...process.env },
120
+ });
121
+
122
+ child.on('error', (err) => {
123
+ p.log.error(`Failed to start: ${err.message}`);
124
+ p.log.info('Make sure tsx is available: npm install -g tsx');
125
+ process.exit(1);
126
+ });
127
+
128
+ child.on('exit', (code, signal) => {
129
+ if (signal) {
130
+ p.log.warn(`Process terminated by signal ${signal}`);
131
+ process.exit(1);
132
+ }
133
+ process.exit(code ?? 0);
134
+ });
39
135
  }
@@ -5,9 +5,28 @@ import { homedir } from 'node:os';
5
5
  import * as p from '@clack/prompts';
6
6
  import { detectAgent } from '../utils/agent-context.js';
7
7
 
8
- type Target = 'claude' | 'codex' | 'both';
8
+ type Target = 'claude' | 'codex' | 'opencode' | 'both' | 'all';
9
9
 
10
- function installClaude(agentId: string, agentDir: string): void {
10
+ /** MCP server entry for file-tree agents (uses npx @soleri/engine) */
11
+ function fileTreeMcpEntry(agentDir: string): Record<string, unknown> {
12
+ return {
13
+ type: 'stdio',
14
+ command: 'npx',
15
+ args: ['@soleri/engine', '--agent', join(agentDir, 'agent.yaml')],
16
+ };
17
+ }
18
+
19
+ /** MCP server entry for legacy TypeScript agents (uses node dist/index.js) */
20
+ function legacyMcpEntry(agentDir: string): Record<string, unknown> {
21
+ return {
22
+ type: 'stdio',
23
+ command: 'node',
24
+ args: [join(agentDir, 'dist', 'index.js')],
25
+ env: {},
26
+ };
27
+ }
28
+
29
+ function installClaude(agentId: string, agentDir: string, isFileTree: boolean): void {
11
30
  const configPath = join(homedir(), '.claude.json');
12
31
  let config: Record<string, unknown> = {};
13
32
 
@@ -24,18 +43,15 @@ function installClaude(agentId: string, agentDir: string): void {
24
43
  config.mcpServers = {};
25
44
  }
26
45
 
27
- (config.mcpServers as Record<string, unknown>)[agentId] = {
28
- type: 'stdio',
29
- command: 'node',
30
- args: [join(agentDir, 'dist', 'index.js')],
31
- env: {},
32
- };
46
+ (config.mcpServers as Record<string, unknown>)[agentId] = isFileTree
47
+ ? fileTreeMcpEntry(agentDir)
48
+ : legacyMcpEntry(agentDir);
33
49
 
34
50
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
35
51
  p.log.success(`Registered ${agentId} in ~/.claude.json`);
36
52
  }
37
53
 
38
- function installCodex(agentId: string, agentDir: string): void {
54
+ function installCodex(agentId: string, agentDir: string, isFileTree: boolean): void {
39
55
  const codexDir = join(homedir(), '.codex');
40
56
  const configPath = join(codexDir, 'config.toml');
41
57
 
@@ -53,8 +69,14 @@ function installCodex(agentId: string, agentDir: string): void {
53
69
  const sectionRegex = new RegExp(`\\[mcp_servers\\.${escapeRegExp(agentId)}\\][^\\[]*`, 's');
54
70
  content = content.replace(sectionRegex, '').trim();
55
71
 
56
- const entryPoint = join(agentDir, 'dist', 'index.js');
57
- const section = `\n\n${sectionHeader}\ncommand = "node"\nargs = ["${entryPoint}"]\n`;
72
+ let section: string;
73
+ if (isFileTree) {
74
+ const agentYamlPath = join(agentDir, 'agent.yaml');
75
+ section = `\n\n${sectionHeader}\ncommand = "npx"\nargs = ["@soleri/engine", "--agent", "${agentYamlPath}"]\n`;
76
+ } else {
77
+ const entryPoint = join(agentDir, 'dist', 'index.js');
78
+ section = `\n\n${sectionHeader}\ncommand = "node"\nargs = ["${entryPoint}"]\n`;
79
+ }
58
80
 
59
81
  content = content + section;
60
82
 
@@ -62,6 +84,33 @@ function installCodex(agentId: string, agentDir: string): void {
62
84
  p.log.success(`Registered ${agentId} in ~/.codex/config.toml`);
63
85
  }
64
86
 
87
+ function installOpencode(agentId: string, agentDir: string, isFileTree: boolean): void {
88
+ const configPath = join(homedir(), '.opencode.json');
89
+
90
+ let config: Record<string, unknown> = {};
91
+ if (existsSync(configPath)) {
92
+ try {
93
+ const raw = readFileSync(configPath, 'utf-8');
94
+ const stripped = raw.replace(/^\s*\/\/.*$/gm, '');
95
+ config = JSON.parse(stripped);
96
+ } catch {
97
+ config = {};
98
+ }
99
+ }
100
+
101
+ if (!config.mcpServers || typeof config.mcpServers !== 'object') {
102
+ config.mcpServers = {};
103
+ }
104
+
105
+ const servers = config.mcpServers as Record<string, unknown>;
106
+ servers[agentId] = isFileTree
107
+ ? fileTreeMcpEntry(agentDir)
108
+ : { type: 'stdio', command: 'node', args: [join(agentDir, 'dist', 'index.js')] };
109
+
110
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
111
+ p.log.success(`Registered ${agentId} in ~/.opencode.json`);
112
+ }
113
+
65
114
  function escapeRegExp(s: string): string {
66
115
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
67
116
  }
@@ -70,7 +119,7 @@ export function registerInstall(program: Command): void {
70
119
  program
71
120
  .command('install')
72
121
  .argument('[dir]', 'Agent directory (defaults to cwd)')
73
- .option('--target <target>', 'Registration target: claude, codex, or both', 'claude')
122
+ .option('--target <target>', 'Registration target: opencode, claude, codex, or all', 'opencode')
74
123
  .description('Register agent as MCP server in editor config')
75
124
  .action(async (dir?: string, opts?: { target?: string }) => {
76
125
  const resolvedDir = dir ? resolve(dir) : undefined;
@@ -81,19 +130,29 @@ export function registerInstall(program: Command): void {
81
130
  process.exit(1);
82
131
  }
83
132
 
84
- const target = (opts?.target ?? 'claude') as Target;
133
+ const target = (opts?.target ?? 'opencode') as Target;
134
+ const validTargets: Target[] = ['claude', 'codex', 'opencode', 'both', 'all'];
135
+ const isFileTree = ctx.format === 'filetree';
85
136
 
86
- if (target !== 'claude' && target !== 'codex' && target !== 'both') {
87
- p.log.error(`Invalid target "${target}". Use: claude, codex, or both`);
137
+ if (!validTargets.includes(target)) {
138
+ p.log.error(`Invalid target "${target}". Use: ${validTargets.join(', ')}`);
88
139
  process.exit(1);
89
140
  }
90
141
 
91
- if (target === 'claude' || target === 'both') {
92
- installClaude(ctx.agentId, ctx.agentPath);
142
+ if (isFileTree) {
143
+ p.log.info(`Detected file-tree agent (v7) — registering via @soleri/engine`);
144
+ }
145
+
146
+ if (target === 'claude' || target === 'both' || target === 'all') {
147
+ installClaude(ctx.agentId, ctx.agentPath, isFileTree);
148
+ }
149
+
150
+ if (target === 'codex' || target === 'both' || target === 'all') {
151
+ installCodex(ctx.agentId, ctx.agentPath, isFileTree);
93
152
  }
94
153
 
95
- if (target === 'codex' || target === 'both') {
96
- installCodex(ctx.agentId, ctx.agentPath);
154
+ if (target === 'opencode' || target === 'all') {
155
+ installOpencode(ctx.agentId, ctx.agentPath, isFileTree);
97
156
  }
98
157
 
99
158
  p.log.info(`Agent ${ctx.agentId} is now available as an MCP server.`);