@soleri/cli 1.12.5 → 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 (38) 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 +151 -2
  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/utils/agent-context.d.ts +9 -2
  25. package/dist/utils/agent-context.js +32 -0
  26. package/dist/utils/agent-context.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/commands/add-pack.ts +170 -0
  29. package/src/commands/agent.ts +174 -3
  30. package/src/commands/cognee.ts +416 -0
  31. package/src/commands/create.ts +90 -6
  32. package/src/commands/dev.ts +114 -18
  33. package/src/commands/install.ts +78 -19
  34. package/src/commands/telegram.ts +488 -0
  35. package/src/commands/uninstall.ts +41 -7
  36. package/src/main.ts +6 -0
  37. package/src/prompts/create-wizard.ts +93 -7
  38. package/src/utils/agent-context.ts +39 -2
@@ -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.`);
@@ -0,0 +1,488 @@
1
+ /**
2
+ * Telegram transport management — enable, disable, setup, status.
3
+ *
4
+ * `soleri telegram enable` — Add Telegram files to the current agent
5
+ * `soleri telegram disable` — Remove Telegram files from the current agent
6
+ * `soleri telegram setup` — Interactive config wizard (bot token, API key, model)
7
+ * `soleri telegram status` — Check Telegram configuration status
8
+ */
9
+
10
+ import { join } from 'node:path';
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { execFileSync } from 'node:child_process';
14
+ import type { Command } from 'commander';
15
+ import * as p from '@clack/prompts';
16
+ import {
17
+ generateTelegramBot,
18
+ generateTelegramAgent,
19
+ generateTelegramConfig,
20
+ generateTelegramSupervisor,
21
+ } from '@soleri/forge/lib';
22
+ import type { AgentConfig } from '@soleri/forge/lib';
23
+ import { detectAgent } from '../utils/agent-context.js';
24
+
25
+ // ─── Telegram file paths relative to agent src/ ─────────────────────
26
+
27
+ const TELEGRAM_FILES = [
28
+ 'src/telegram-bot.ts',
29
+ 'src/telegram-agent.ts',
30
+ 'src/telegram-config.ts',
31
+ 'src/telegram-supervisor.ts',
32
+ ] as const;
33
+
34
+ // ─── Registration ───────────────────────────────────────────────────
35
+
36
+ export function registerTelegram(program: Command): void {
37
+ const tg = program
38
+ .command('telegram')
39
+ .description('Manage Telegram transport for the current agent');
40
+
41
+ // ─── enable ─────────────────────────────────────────────────────
42
+ tg.command('enable')
43
+ .description('Add Telegram transport files to the current agent')
44
+ .action(() => {
45
+ const ctx = detectAgent();
46
+ if (!ctx) {
47
+ p.log.error('No agent project detected in current directory.');
48
+ process.exit(1);
49
+ return;
50
+ }
51
+
52
+ // Check if already enabled
53
+ const existingFiles = TELEGRAM_FILES.filter((f) => existsSync(join(ctx.agentPath, f)));
54
+ if (existingFiles.length === TELEGRAM_FILES.length) {
55
+ p.log.warn('Telegram is already enabled for this agent.');
56
+ p.log.info('Run `soleri telegram setup` to configure it.');
57
+ return;
58
+ }
59
+ if (existingFiles.length > 0) {
60
+ p.log.warn(
61
+ `Partial Telegram setup detected (${existingFiles.length}/${TELEGRAM_FILES.length} files). Regenerating all files.`,
62
+ );
63
+ }
64
+
65
+ // Reconstruct AgentConfig
66
+ const config = readAgentConfig(ctx.agentPath, ctx.agentId);
67
+ if (!config) {
68
+ p.log.error('Could not read agent config from persona.ts and entry point.');
69
+ process.exit(1);
70
+ return;
71
+ }
72
+
73
+ // Generate the 4 Telegram files
74
+ const s = p.spinner();
75
+ s.start('Generating Telegram transport files...');
76
+
77
+ const telegramFiles: Array<[string, string]> = [
78
+ ['src/telegram-bot.ts', generateTelegramBot(config)],
79
+ ['src/telegram-agent.ts', generateTelegramAgent(config)],
80
+ ['src/telegram-config.ts', generateTelegramConfig(config)],
81
+ ['src/telegram-supervisor.ts', generateTelegramSupervisor(config)],
82
+ ];
83
+
84
+ for (const [relPath, content] of telegramFiles) {
85
+ writeFileSync(join(ctx.agentPath, relPath), content, 'utf-8');
86
+ }
87
+
88
+ s.stop('Generated 4 Telegram transport files');
89
+
90
+ // Add grammy to package.json if not present
91
+ const pkgPath = join(ctx.agentPath, 'package.json');
92
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
93
+ let pkgChanged = false;
94
+
95
+ if (!pkg.dependencies?.grammy) {
96
+ if (!pkg.dependencies) pkg.dependencies = {};
97
+ pkg.dependencies.grammy = '^1.35.0';
98
+ pkgChanged = true;
99
+ p.log.info('Added grammy dependency to package.json');
100
+ }
101
+
102
+ // Add npm scripts if not present
103
+ if (!pkg.scripts) pkg.scripts = {};
104
+ if (!pkg.scripts['telegram:start']) {
105
+ pkg.scripts['telegram:start'] = 'node dist/telegram-supervisor.js';
106
+ pkgChanged = true;
107
+ }
108
+ if (!pkg.scripts['telegram:dev']) {
109
+ pkg.scripts['telegram:dev'] = 'tsx src/telegram-bot.ts';
110
+ pkgChanged = true;
111
+ }
112
+
113
+ if (pkgChanged) {
114
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
115
+ p.log.info('Updated package.json with telegram scripts');
116
+ }
117
+
118
+ // Install grammy
119
+ const installSpinner = p.spinner();
120
+ installSpinner.start('Installing grammy...');
121
+ try {
122
+ execFileSync('npm', ['install', '--no-fund', '--no-audit'], {
123
+ cwd: ctx.agentPath,
124
+ stdio: 'pipe',
125
+ timeout: 120_000,
126
+ });
127
+ installSpinner.stop('Installed grammy');
128
+ } catch {
129
+ installSpinner.stop('npm install skipped — run `npm install` manually');
130
+ }
131
+
132
+ p.log.success('Telegram enabled!');
133
+ p.log.info(`Run \`soleri telegram setup\` to configure bot token and API key.`);
134
+ });
135
+
136
+ // ─── setup ──────────────────────────────────────────────────────
137
+ tg.command('setup')
138
+ .description('Interactive Telegram configuration wizard')
139
+ .action(async () => {
140
+ const ctx = detectAgent();
141
+ if (!ctx) {
142
+ p.log.error('No agent project detected in current directory.');
143
+ process.exit(1);
144
+ return;
145
+ }
146
+
147
+ // Check that Telegram files exist
148
+ const missingFiles = TELEGRAM_FILES.filter((f) => !existsSync(join(ctx.agentPath, f)));
149
+ if (missingFiles.length > 0) {
150
+ p.log.error('Telegram is not enabled. Run `soleri telegram enable` first.');
151
+ process.exit(1);
152
+ return;
153
+ }
154
+
155
+ p.intro(`Telegram Setup for ${ctx.agentId}`);
156
+
157
+ // Step 1: Bot token
158
+ p.log.step('Step 1: Create a Telegram bot');
159
+ p.log.message(
160
+ [
161
+ ' Open Telegram and talk to @BotFather',
162
+ ' Send /newbot and follow the instructions',
163
+ ' Copy the bot token when you get it',
164
+ ].join('\n'),
165
+ );
166
+
167
+ const botToken = await p.text({
168
+ message: 'Paste your bot token:',
169
+ placeholder: '123456789:ABCdefGHIjklMNOpqrsTUVwxyz',
170
+ validate: (val) => {
171
+ if (!val || val.trim().length === 0) return 'Bot token is required';
172
+ if (!val.includes(':')) return 'Invalid token format (expected number:string)';
173
+ return undefined;
174
+ },
175
+ });
176
+ if (p.isCancel(botToken)) {
177
+ p.cancel('Setup cancelled.');
178
+ process.exit(0);
179
+ }
180
+
181
+ // Step 2: LLM API key
182
+ p.log.step('Step 2: LLM API Key');
183
+ p.log.message(' Your agent needs an API key to think');
184
+
185
+ const provider = await p.select({
186
+ message: 'Which provider?',
187
+ options: [
188
+ { value: 'anthropic', label: 'Anthropic (Claude)' },
189
+ { value: 'openai', label: 'OpenAI' },
190
+ { value: 'env', label: 'Use environment variable (skip)' },
191
+ ],
192
+ });
193
+ if (p.isCancel(provider)) {
194
+ p.cancel('Setup cancelled.');
195
+ process.exit(0);
196
+ }
197
+
198
+ let apiKey = '';
199
+ if (provider !== 'env') {
200
+ const keyPlaceholder = provider === 'anthropic' ? 'sk-ant-...' : 'sk-...';
201
+ const envHint = provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY';
202
+
203
+ const keyInput = await p.text({
204
+ message: `Paste your ${provider === 'anthropic' ? 'Anthropic' : 'OpenAI'} API key:`,
205
+ placeholder: keyPlaceholder,
206
+ validate: (val) => {
207
+ if (!val || val.trim().length === 0)
208
+ return `API key is required (or set ${envHint} env var)`;
209
+ return undefined;
210
+ },
211
+ });
212
+ if (p.isCancel(keyInput)) {
213
+ p.cancel('Setup cancelled.');
214
+ process.exit(0);
215
+ }
216
+ apiKey = keyInput;
217
+ }
218
+
219
+ // Step 3: Security
220
+ p.log.step('Step 3: Security (optional)');
221
+
222
+ const passphrase = await p.text({
223
+ message: 'Set a passphrase? Users must send this to authenticate.',
224
+ placeholder: '(empty for open access)',
225
+ defaultValue: '',
226
+ });
227
+ if (p.isCancel(passphrase)) {
228
+ p.cancel('Setup cancelled.');
229
+ process.exit(0);
230
+ }
231
+
232
+ // Step 4: Model selection
233
+ p.log.step('Step 4: Model selection');
234
+
235
+ const modelOptions =
236
+ provider === 'openai'
237
+ ? [
238
+ { value: 'gpt-4.1', label: 'gpt-4.1 (recommended)' },
239
+ { value: 'gpt-4.1-mini', label: 'gpt-4.1-mini (fast)' },
240
+ { value: 'o3', label: 'o3 (reasoning)' },
241
+ ]
242
+ : [
243
+ { value: 'claude-sonnet-4-20250514', label: 'claude-sonnet-4 (fast, recommended)' },
244
+ { value: 'claude-opus-4-20250514', label: 'claude-opus-4 (powerful)' },
245
+ { value: 'claude-haiku-3-5-20241022', label: 'claude-3.5-haiku (economical)' },
246
+ ];
247
+
248
+ const model = await p.select({
249
+ message: 'Default model:',
250
+ options: modelOptions,
251
+ });
252
+ if (p.isCancel(model)) {
253
+ p.cancel('Setup cancelled.');
254
+ process.exit(0);
255
+ }
256
+
257
+ // Save config
258
+ const configDir = join(homedir(), `.${ctx.agentId}`);
259
+ mkdirSync(configDir, { recursive: true });
260
+ const configPath = join(configDir, 'telegram.json');
261
+
262
+ const telegramConfig: Record<string, unknown> = {
263
+ botToken: (botToken as string).trim(),
264
+ ...(apiKey ? { apiKey } : {}),
265
+ provider: provider === 'env' ? 'anthropic' : provider,
266
+ model,
267
+ ...(passphrase ? { passphrase } : {}),
268
+ allowedUsers: [],
269
+ };
270
+
271
+ writeFileSync(configPath, JSON.stringify(telegramConfig, null, 2) + '\n', 'utf-8');
272
+
273
+ p.outro(`Configuration saved to ${configPath}`);
274
+
275
+ console.log('');
276
+ p.log.info(' Run: npm run telegram:start');
277
+ p.log.info(' Or: npm run telegram:dev (with auto-restart)');
278
+ console.log('');
279
+ });
280
+
281
+ // ─── disable ────────────────────────────────────────────────────
282
+ tg.command('disable')
283
+ .description('Remove Telegram transport from the current agent')
284
+ .action(async () => {
285
+ const ctx = detectAgent();
286
+ if (!ctx) {
287
+ p.log.error('No agent project detected in current directory.');
288
+ process.exit(1);
289
+ return;
290
+ }
291
+
292
+ // Check if any Telegram files exist
293
+ const existingFiles = TELEGRAM_FILES.filter((f) => existsSync(join(ctx.agentPath, f)));
294
+ if (existingFiles.length === 0) {
295
+ p.log.warn('Telegram is not enabled for this agent.');
296
+ return;
297
+ }
298
+
299
+ const confirmed = await p.confirm({
300
+ message: `Remove ${existingFiles.length} Telegram files and related config from ${ctx.agentId}?`,
301
+ });
302
+ if (p.isCancel(confirmed) || !confirmed) {
303
+ p.cancel('Cancelled.');
304
+ return;
305
+ }
306
+
307
+ // Remove Telegram source files
308
+ for (const relPath of TELEGRAM_FILES) {
309
+ const fullPath = join(ctx.agentPath, relPath);
310
+ if (existsSync(fullPath)) {
311
+ unlinkSync(fullPath);
312
+ }
313
+ }
314
+ p.log.info(`Removed ${existingFiles.length} Telegram source files`);
315
+
316
+ // Remove grammy from package.json and telegram scripts
317
+ const pkgPath = join(ctx.agentPath, 'package.json');
318
+ if (existsSync(pkgPath)) {
319
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
320
+ let changed = false;
321
+
322
+ if (pkg.dependencies?.grammy) {
323
+ delete pkg.dependencies.grammy;
324
+ changed = true;
325
+ }
326
+ if (pkg.scripts?.['telegram:start']) {
327
+ delete pkg.scripts['telegram:start'];
328
+ changed = true;
329
+ }
330
+ if (pkg.scripts?.['telegram:dev']) {
331
+ delete pkg.scripts['telegram:dev'];
332
+ changed = true;
333
+ }
334
+
335
+ if (changed) {
336
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
337
+ p.log.info('Removed grammy dependency and telegram scripts from package.json');
338
+ }
339
+ }
340
+
341
+ p.log.success('Telegram disabled.');
342
+ p.log.info('Run `npm install` to clean up node_modules.');
343
+ });
344
+
345
+ // ─── status ─────────────────────────────────────────────────────
346
+ tg.command('status')
347
+ .description('Check Telegram configuration status')
348
+ .action(() => {
349
+ const ctx = detectAgent();
350
+ if (!ctx) {
351
+ p.log.error('No agent project detected in current directory.');
352
+ process.exit(1);
353
+ return;
354
+ }
355
+
356
+ console.log(`\n Agent: ${ctx.agentId}`);
357
+
358
+ // Check source files
359
+ const presentFiles = TELEGRAM_FILES.filter((f) => existsSync(join(ctx.agentPath, f)));
360
+ const filesEnabled = presentFiles.length === TELEGRAM_FILES.length;
361
+ console.log(
362
+ ` Files: ${filesEnabled ? 'all present' : `${presentFiles.length}/${TELEGRAM_FILES.length} present`}`,
363
+ );
364
+ if (!filesEnabled && presentFiles.length > 0) {
365
+ for (const f of TELEGRAM_FILES) {
366
+ const exists = existsSync(join(ctx.agentPath, f));
367
+ console.log(` ${exists ? '+' : '-'} ${f}`);
368
+ }
369
+ }
370
+
371
+ // Check grammy dependency
372
+ const pkgPath = join(ctx.agentPath, 'package.json');
373
+ if (existsSync(pkgPath)) {
374
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
375
+ const hasGrammy = !!pkg.dependencies?.grammy;
376
+ console.log(
377
+ ` Grammy: ${hasGrammy ? `installed (${pkg.dependencies.grammy})` : 'not in dependencies'}`,
378
+ );
379
+
380
+ const hasStartScript = !!pkg.scripts?.['telegram:start'];
381
+ const hasDevScript = !!pkg.scripts?.['telegram:dev'];
382
+ console.log(
383
+ ` Scripts: ${hasStartScript && hasDevScript ? 'telegram:start, telegram:dev' : hasStartScript ? 'telegram:start only' : hasDevScript ? 'telegram:dev only' : 'none'}`,
384
+ );
385
+ }
386
+
387
+ // Check config file
388
+ const configPath = join(homedir(), `.${ctx.agentId}`, 'telegram.json');
389
+ if (existsSync(configPath)) {
390
+ try {
391
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
392
+ const hasToken = !!config.botToken;
393
+ const hasKey = !!config.apiKey;
394
+ console.log(` Config: ${configPath}`);
395
+ console.log(` Bot token: ${hasToken ? 'set' : 'not set'}`);
396
+ console.log(` API key: ${hasKey ? 'set' : 'not set (check env vars)'}`);
397
+ if (config.model) console.log(` Model: ${config.model}`);
398
+ if (config.passphrase) console.log(` Passphrase: set`);
399
+ } catch {
400
+ console.log(` Config: ${configPath} (invalid JSON)`);
401
+ }
402
+ } else {
403
+ console.log(` Config: not found at ${configPath}`);
404
+ if (filesEnabled) {
405
+ console.log(' Run `soleri telegram setup` to configure.');
406
+ }
407
+ }
408
+
409
+ // Overall status
410
+ const ready = filesEnabled && existsSync(configPath);
411
+ console.log(`\n Status: ${ready ? 'ready to start' : 'needs configuration'}`);
412
+ if (!filesEnabled) {
413
+ console.log(' Next: Run `soleri telegram enable`');
414
+ } else if (!existsSync(configPath)) {
415
+ console.log(' Next: Run `soleri telegram setup`');
416
+ } else {
417
+ console.log(' Next: Run `npm run telegram:start` or `npm run telegram:dev`');
418
+ }
419
+ console.log('');
420
+ });
421
+ }
422
+
423
+ // ─── Helpers ──────────────────────────────────────────────────────────
424
+
425
+ /**
426
+ * Reconstruct an AgentConfig from an existing scaffolded agent.
427
+ * Mirrors the logic in agent.ts but returns a config with telegram: true.
428
+ */
429
+ function readAgentConfig(agentPath: string, agentId: string): AgentConfig | null {
430
+ // Try both locations: v6+ (src/identity/) and v5 (src/activation/)
431
+ const personaCandidates = [
432
+ join(agentPath, 'src', 'identity', 'persona.ts'),
433
+ join(agentPath, 'src', 'activation', 'persona.ts'),
434
+ ];
435
+ const personaPath = personaCandidates.find((candidate) => existsSync(candidate));
436
+ if (!personaPath) return null;
437
+ const personaSrc = readFileSync(personaPath, 'utf-8');
438
+
439
+ const name = extractStringField(personaSrc, 'name') ?? agentId;
440
+ const role = extractStringField(personaSrc, 'role') ?? '';
441
+ const description = extractStringField(personaSrc, 'description') ?? '';
442
+ const tone =
443
+ (extractStringField(personaSrc, 'tone') as 'precise' | 'mentor' | 'pragmatic') ?? 'pragmatic';
444
+ const greeting = extractStringField(personaSrc, 'greeting') ?? `Hello! I'm ${name}.`;
445
+ const principles = extractArrayField(personaSrc, 'principles');
446
+
447
+ // Read domains from entry point
448
+ const indexPath = join(agentPath, 'src', 'index.ts');
449
+ const domains = existsSync(indexPath) ? extractDomains(readFileSync(indexPath, 'utf-8')) : [];
450
+
451
+ const pkg = JSON.parse(readFileSync(join(agentPath, 'package.json'), 'utf-8'));
452
+
453
+ return {
454
+ id: agentId,
455
+ name,
456
+ role,
457
+ description,
458
+ domains,
459
+ principles,
460
+ tone,
461
+ greeting,
462
+ outputDir: agentPath,
463
+ hookPacks: [],
464
+ model: pkg.soleri?.model ?? 'claude-code-sonnet-4',
465
+ setupTarget: pkg.soleri?.setupTarget ?? 'claude',
466
+ telegram: true, // Force true — we're enabling telegram
467
+ cognee: pkg.soleri?.cognee ?? false,
468
+ };
469
+ }
470
+
471
+ function extractStringField(src: string, field: string): string | undefined {
472
+ const re = new RegExp(`${field}:\\s*'([^']*)'`);
473
+ const m = src.match(re);
474
+ return m ? m[1].replace(/\\'/g, "'") : undefined;
475
+ }
476
+
477
+ function extractArrayField(src: string, field: string): string[] {
478
+ const re = new RegExp(`${field}:\\s*\\[([\\s\\S]*?)\\]`);
479
+ const m = src.match(re);
480
+ if (!m) return [];
481
+ return [...m[1].matchAll(/'([^']*)'/g)].map((x) => x[1]);
482
+ }
483
+
484
+ function extractDomains(indexSrc: string): string[] {
485
+ const m = indexSrc.match(/createDomainFacades\(runtime,\s*['"][^'"]+['"]\s*,\s*\[([\s\S]*?)\]\)/);
486
+ if (!m) return [];
487
+ return [...m[1].matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]);
488
+ }