thepopebot 1.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/api/index.js +357 -0
  4. package/bin/cli.js +278 -0
  5. package/config/index.js +29 -0
  6. package/config/instrumentation.js +29 -0
  7. package/docker/Dockerfile +51 -0
  8. package/docker/entrypoint.sh +100 -0
  9. package/lib/actions.js +40 -0
  10. package/lib/claude/conversation.js +76 -0
  11. package/lib/claude/index.js +142 -0
  12. package/lib/claude/tools.js +54 -0
  13. package/lib/cron.js +60 -0
  14. package/lib/paths.js +30 -0
  15. package/lib/tools/create-job.js +40 -0
  16. package/lib/tools/github.js +122 -0
  17. package/lib/tools/openai.js +35 -0
  18. package/lib/tools/telegram.js +222 -0
  19. package/lib/triggers.js +105 -0
  20. package/lib/utils/render-md.js +39 -0
  21. package/package.json +57 -0
  22. package/pi/extensions/env-sanitizer/index.ts +48 -0
  23. package/pi/extensions/env-sanitizer/package.json +5 -0
  24. package/pi/skills/llm-secrets/SKILL.md +34 -0
  25. package/pi/skills/llm-secrets/llm-secrets.js +34 -0
  26. package/setup/lib/auth.mjs +160 -0
  27. package/setup/lib/github.mjs +148 -0
  28. package/setup/lib/prerequisites.mjs +135 -0
  29. package/setup/lib/prompts.mjs +268 -0
  30. package/setup/lib/telegram-verify.mjs +66 -0
  31. package/setup/lib/telegram.mjs +76 -0
  32. package/setup/package.json +6 -0
  33. package/setup/setup-telegram.mjs +236 -0
  34. package/setup/setup.mjs +540 -0
  35. package/templates/.env.example +38 -0
  36. package/templates/.github/workflows/auto-merge.yml +117 -0
  37. package/templates/.github/workflows/docker-build.yml +34 -0
  38. package/templates/.github/workflows/run-job.yml +40 -0
  39. package/templates/.github/workflows/update-event-handler.yml +126 -0
  40. package/templates/.pi/skills/modify-self/SKILL.md +12 -0
  41. package/templates/CLAUDE.md +52 -0
  42. package/templates/app/api/[...thepopebot]/route.js +1 -0
  43. package/templates/app/layout.js +12 -0
  44. package/templates/app/page.js +8 -0
  45. package/templates/instrumentation.js +1 -0
  46. package/templates/next.config.mjs +3 -0
  47. package/templates/operating_system/AGENT.md +32 -0
  48. package/templates/operating_system/CHATBOT.md +74 -0
  49. package/templates/operating_system/CRONS.json +16 -0
  50. package/templates/operating_system/HEARTBEAT.md +3 -0
  51. package/templates/operating_system/JOB_SUMMARY.md +36 -0
  52. package/templates/operating_system/SOUL.md +17 -0
  53. package/templates/operating_system/TELEGRAM.md +21 -0
  54. package/templates/operating_system/TRIGGERS.json +18 -0
@@ -0,0 +1,160 @@
1
+ import { writeFileSync, readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ const ROOT_DIR = process.cwd();
5
+
6
+ /**
7
+ * Validate Anthropic API key by making a minimal test call
8
+ */
9
+ export async function validateAnthropicKey(key) {
10
+ try {
11
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
12
+ method: 'POST',
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ 'x-api-key': key,
16
+ 'anthropic-version': '2023-06-01',
17
+ },
18
+ body: JSON.stringify({
19
+ model: 'claude-3-haiku-20240307',
20
+ max_tokens: 1,
21
+ messages: [{ role: 'user', content: 'Hi' }],
22
+ }),
23
+ });
24
+
25
+ if (response.status === 401) {
26
+ return { valid: false, error: 'Invalid API key' };
27
+ }
28
+ if (response.status === 400) {
29
+ // Bad request but key is valid (e.g., rate limit, model error)
30
+ return { valid: true };
31
+ }
32
+ if (response.ok) {
33
+ return { valid: true };
34
+ }
35
+ return { valid: false, error: `HTTP ${response.status}` };
36
+ } catch (error) {
37
+ return { valid: false, error: error.message };
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Build flat secrets JSON for SECRETS GitHub secret.
43
+ * Includes GH_TOKEN + API keys. entrypoint.sh decodes and exports each as an env var.
44
+ */
45
+ export function buildSecretsJson(pat, keys) {
46
+ const secrets = { GH_TOKEN: pat, ANTHROPIC_API_KEY: keys.anthropic };
47
+
48
+ if (keys.openai) secrets.OPENAI_API_KEY = keys.openai;
49
+ if (keys.groq) secrets.GROQ_API_KEY = keys.groq;
50
+
51
+ return secrets;
52
+ }
53
+
54
+ /**
55
+ * Encode secrets to base64 for SECRETS GitHub secret
56
+ */
57
+ export function encodeSecretsBase64(pat, keys) {
58
+ const secrets = buildSecretsJson(pat, keys);
59
+ return Buffer.from(JSON.stringify(secrets)).toString('base64');
60
+ }
61
+
62
+ /**
63
+ * Build LLM secrets JSON for LLM_SECRETS GitHub secret.
64
+ * These credentials are accessible to the LLM (not filtered by env-sanitizer).
65
+ */
66
+ export function buildLlmSecretsJson(keys) {
67
+ const secrets = {};
68
+ if (keys.brave) secrets.BRAVE_API_KEY = keys.brave;
69
+ return secrets;
70
+ }
71
+
72
+ /**
73
+ * Encode LLM secrets to base64 for LLM_SECRETS GitHub secret.
74
+ * Returns null if no LLM secrets are configured.
75
+ */
76
+ export function encodeLlmSecretsBase64(keys) {
77
+ const secrets = buildLlmSecretsJson(keys);
78
+ if (Object.keys(secrets).length === 0) return null;
79
+ return Buffer.from(JSON.stringify(secrets)).toString('base64');
80
+ }
81
+
82
+ /**
83
+ * Write .env file at project root
84
+ */
85
+ export function writeEnvFile(config) {
86
+ const {
87
+ apiKey,
88
+ githubToken,
89
+ githubOwner,
90
+ githubRepo,
91
+ telegramBotToken,
92
+ telegramWebhookSecret,
93
+ ghWebhookSecret,
94
+ anthropicApiKey,
95
+ openaiApiKey,
96
+ telegramChatId,
97
+ telegramVerification,
98
+ } = config;
99
+
100
+ const envContent = `# thepopebot Configuration
101
+ # Generated by setup wizard
102
+
103
+ # Authentication key for /api/webhook endpoint
104
+ API_KEY=${apiKey}
105
+
106
+ # GitHub Personal Access Token (fine-grained: Actions, Contents, Metadata, Pull requests)
107
+ GH_TOKEN=${githubToken}
108
+
109
+ # Repository info
110
+ GH_OWNER=${githubOwner}
111
+ GH_REPO=${githubRepo}
112
+
113
+ # Telegram bot token from @BotFather
114
+ TELEGRAM_BOT_TOKEN=${telegramBotToken || ''}
115
+
116
+ # Telegram webhook secret (validates requests are from Telegram)
117
+ TELEGRAM_WEBHOOK_SECRET=${telegramWebhookSecret || ''}
118
+
119
+ # Telegram chat ID (restricts bot to this chat only)
120
+ TELEGRAM_CHAT_ID=${telegramChatId || ''}
121
+
122
+ # Telegram verification code (used during setup, can be removed after)
123
+ TELEGRAM_VERIFICATION=${telegramVerification || ''}
124
+
125
+ # Secret for GitHub Actions webhook auth (must match GH_WEBHOOK_SECRET secret)
126
+ GH_WEBHOOK_SECRET=${ghWebhookSecret}
127
+
128
+ # Anthropic API key for Claude chat features
129
+ ANTHROPIC_API_KEY=${anthropicApiKey}
130
+
131
+ # OpenAI API key for Whisper voice transcription (optional)
132
+ OPENAI_API_KEY=${openaiApiKey || ''}
133
+ `;
134
+
135
+ const envPath = join(ROOT_DIR, '.env');
136
+ writeFileSync(envPath, envContent);
137
+ return envPath;
138
+ }
139
+
140
+ /**
141
+ * Update a single variable in an existing .env file
142
+ */
143
+ export function updateEnvVariable(key, value) {
144
+ const envPath = join(ROOT_DIR, '.env');
145
+ if (!existsSync(envPath)) {
146
+ throw new Error('.env file not found. Run npm run setup first.');
147
+ }
148
+
149
+ let content = readFileSync(envPath, 'utf-8');
150
+ const regex = new RegExp(`^${key}=.*$`, 'm');
151
+
152
+ if (regex.test(content)) {
153
+ content = content.replace(regex, `${key}=${value}`);
154
+ } else {
155
+ content = content.trimEnd() + `\n${key}=${value}\n`;
156
+ }
157
+
158
+ writeFileSync(envPath, content);
159
+ return envPath;
160
+ }
@@ -0,0 +1,148 @@
1
+ import { execSync, exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { randomBytes } from 'crypto';
4
+
5
+ const execAsync = promisify(exec);
6
+
7
+ /**
8
+ * Validate GitHub PAT by making a test API call
9
+ */
10
+ export async function validatePAT(token) {
11
+ try {
12
+ const response = await fetch('https://api.github.com/user', {
13
+ headers: {
14
+ Authorization: `token ${token}`,
15
+ Accept: 'application/vnd.github.v3+json',
16
+ },
17
+ });
18
+ if (!response.ok) return { valid: false, error: 'Invalid token' };
19
+ const user = await response.json();
20
+ return { valid: true, user: user.login };
21
+ } catch (error) {
22
+ return { valid: false, error: error.message };
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Check PAT scopes/permissions
28
+ * Works with both classic tokens (x-oauth-scopes header) and fine-grained tokens
29
+ */
30
+ export async function checkPATScopes(token) {
31
+ try {
32
+ const response = await fetch('https://api.github.com/user', {
33
+ headers: {
34
+ Authorization: `token ${token}`,
35
+ Accept: 'application/vnd.github.v3+json',
36
+ },
37
+ });
38
+ const scopes = response.headers.get('x-oauth-scopes') || '';
39
+ const scopeList = scopes.split(',').map((s) => s.trim()).filter(Boolean);
40
+
41
+ // Classic tokens have x-oauth-scopes header
42
+ if (scopeList.length > 0) {
43
+ return {
44
+ hasRepo: scopeList.includes('repo'),
45
+ hasWorkflow: scopeList.includes('workflow'),
46
+ scopes: scopeList,
47
+ isFineGrained: false,
48
+ };
49
+ }
50
+
51
+ // Fine-grained tokens don't have x-oauth-scopes header
52
+ // We can't check permissions directly, so we assume valid if token works
53
+ return {
54
+ hasRepo: true,
55
+ hasWorkflow: true,
56
+ scopes: [],
57
+ isFineGrained: true,
58
+ };
59
+ } catch {
60
+ return { hasRepo: false, hasWorkflow: false, scopes: [], isFineGrained: false };
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Set a GitHub repository secret using gh CLI
66
+ */
67
+ export async function setSecret(owner, repo, name, value) {
68
+ try {
69
+ // Use stdin to pass the secret value securely
70
+ const { stdout, stderr } = await execAsync(
71
+ `echo "${value.replace(/"/g, '\\"')}" | gh secret set ${name} --repo ${owner}/${repo}`,
72
+ { encoding: 'utf-8' }
73
+ );
74
+ return { success: true };
75
+ } catch (error) {
76
+ return { success: false, error: error.message };
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Set multiple GitHub secrets
82
+ */
83
+ export async function setSecrets(owner, repo, secrets) {
84
+ const results = {};
85
+ for (const [name, value] of Object.entries(secrets)) {
86
+ results[name] = await setSecret(owner, repo, name, value);
87
+ }
88
+ return results;
89
+ }
90
+
91
+ /**
92
+ * List existing secrets
93
+ */
94
+ export async function listSecrets(owner, repo) {
95
+ try {
96
+ const { stdout } = await execAsync(`gh secret list --repo ${owner}/${repo}`, {
97
+ encoding: 'utf-8',
98
+ });
99
+ const secrets = stdout
100
+ .trim()
101
+ .split('\n')
102
+ .filter(Boolean)
103
+ .map((line) => line.split('\t')[0]);
104
+ return secrets;
105
+ } catch {
106
+ return [];
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Set a GitHub repository variable using gh CLI
112
+ */
113
+ export async function setVariable(owner, repo, name, value) {
114
+ try {
115
+ const { stdout, stderr } = await execAsync(
116
+ `echo "${value.replace(/"/g, '\\"')}" | gh variable set ${name} --repo ${owner}/${repo}`,
117
+ { encoding: 'utf-8' }
118
+ );
119
+ return { success: true };
120
+ } catch (error) {
121
+ return { success: false, error: error.message };
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Set multiple GitHub repository variables
127
+ */
128
+ export async function setVariables(owner, repo, variables) {
129
+ const results = {};
130
+ for (const [name, value] of Object.entries(variables)) {
131
+ results[name] = await setVariable(owner, repo, name, value);
132
+ }
133
+ return results;
134
+ }
135
+
136
+ /**
137
+ * Generate a random webhook secret
138
+ */
139
+ export function generateWebhookSecret() {
140
+ return randomBytes(32).toString('hex');
141
+ }
142
+
143
+ /**
144
+ * Get the GitHub PAT creation URL with pre-selected scopes
145
+ */
146
+ export function getPATCreationURL() {
147
+ return 'https://github.com/settings/personal-access-tokens/new';
148
+ }
@@ -0,0 +1,135 @@
1
+ import { execSync, exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+
4
+ const execAsync = promisify(exec);
5
+
6
+ /**
7
+ * Check if a command exists
8
+ */
9
+ function commandExists(cmd) {
10
+ try {
11
+ execSync(`which ${cmd}`, { stdio: 'ignore' });
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Get Node.js version
20
+ */
21
+ function getNodeVersion() {
22
+ try {
23
+ const version = execSync('node --version', { encoding: 'utf-8' }).trim();
24
+ return version.replace('v', '');
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Check if gh CLI is authenticated
32
+ */
33
+ async function isGhAuthenticated() {
34
+ try {
35
+ await execAsync('gh auth status');
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Get git remote info (owner/repo)
44
+ */
45
+ function getGitRemoteInfo() {
46
+ try {
47
+ const remote = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
48
+ // Handle both HTTPS and SSH formats
49
+ // https://github.com/owner/repo.git
50
+ // git@github.com:owner/repo.git
51
+ const httpsMatch = remote.match(/github\.com\/([^/]+)\/([^/.]+)/);
52
+ const sshMatch = remote.match(/github\.com:([^/]+)\/([^/.]+)/);
53
+ const match = httpsMatch || sshMatch;
54
+ if (match) {
55
+ return { owner: match[1], repo: match[2].replace('.git', '') };
56
+ }
57
+ return null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get package manager (pnpm preferred, npm fallback)
65
+ */
66
+ function getPackageManager() {
67
+ if (commandExists('pnpm')) return 'pnpm';
68
+ if (commandExists('npm')) return 'npm';
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Check all prerequisites and return status
74
+ */
75
+ export async function checkPrerequisites() {
76
+ const results = {
77
+ node: { installed: false, version: null, ok: false },
78
+ packageManager: { installed: false, name: null },
79
+ gh: { installed: false, authenticated: false },
80
+ ngrok: { installed: false },
81
+ git: { installed: false, remoteInfo: null },
82
+ };
83
+
84
+ // Check Node.js
85
+ const nodeVersion = getNodeVersion();
86
+ if (nodeVersion) {
87
+ results.node.installed = true;
88
+ results.node.version = nodeVersion;
89
+ const [major] = nodeVersion.split('.').map(Number);
90
+ results.node.ok = major >= 18;
91
+ }
92
+
93
+ // Check package manager
94
+ const pm = getPackageManager();
95
+ if (pm) {
96
+ results.packageManager.installed = true;
97
+ results.packageManager.name = pm;
98
+ }
99
+
100
+ // Check gh CLI
101
+ results.gh.installed = commandExists('gh');
102
+ if (results.gh.installed) {
103
+ results.gh.authenticated = await isGhAuthenticated();
104
+ }
105
+
106
+ // Check ngrok
107
+ results.ngrok.installed = commandExists('ngrok');
108
+
109
+ // Check git
110
+ results.git.installed = commandExists('git');
111
+ if (results.git.installed) {
112
+ results.git.remoteInfo = getGitRemoteInfo();
113
+ }
114
+
115
+ return results;
116
+ }
117
+
118
+ /**
119
+ * Install a global npm package
120
+ */
121
+ export async function installGlobalPackage(packageName) {
122
+ const pm = getPackageManager();
123
+ const cmd = pm === 'pnpm' ? `pnpm add -g ${packageName}` : `npm install -g ${packageName}`;
124
+ await execAsync(cmd);
125
+ }
126
+
127
+ /**
128
+ * Run gh auth login
129
+ */
130
+ export async function runGhAuth() {
131
+ // This needs to be interactive, so we use execSync
132
+ execSync('gh auth login', { stdio: 'inherit' });
133
+ }
134
+
135
+ export { commandExists, getGitRemoteInfo, getPackageManager };
@@ -0,0 +1,268 @@
1
+ import inquirer from 'inquirer';
2
+ import open from 'open';
3
+
4
+ /**
5
+ * Mask a secret, showing only last 4 characters
6
+ */
7
+ export function maskSecret(secret) {
8
+ if (!secret || secret.length < 8) return '****';
9
+ return '****' + secret.slice(-4);
10
+ }
11
+
12
+ /**
13
+ * Prompt for GitHub PAT
14
+ */
15
+ export async function promptForPAT() {
16
+ const { pat } = await inquirer.prompt([
17
+ {
18
+ type: 'password',
19
+ name: 'pat',
20
+ message: 'Paste your GitHub Personal Access Token:',
21
+ mask: '*',
22
+ validate: (input) => {
23
+ if (!input) return 'PAT is required';
24
+ if (!input.startsWith('ghp_') && !input.startsWith('github_pat_')) {
25
+ return 'Invalid PAT format. Should start with ghp_ or github_pat_';
26
+ }
27
+ return true;
28
+ },
29
+ },
30
+ ]);
31
+ return pat;
32
+ }
33
+
34
+ /**
35
+ * Prompt for Anthropic API key
36
+ */
37
+ export async function promptForAnthropicKey() {
38
+ const { key } = await inquirer.prompt([
39
+ {
40
+ type: 'password',
41
+ name: 'key',
42
+ message: 'Enter your Anthropic API key:',
43
+ mask: '*',
44
+ validate: (input) => {
45
+ if (!input) return 'Anthropic API key is required';
46
+ if (!input.startsWith('sk-ant-')) {
47
+ return 'Invalid format. Should start with sk-ant-';
48
+ }
49
+ return true;
50
+ },
51
+ },
52
+ ]);
53
+ return key;
54
+ }
55
+
56
+ /**
57
+ * Prompt for optional OpenAI API key
58
+ */
59
+ export async function promptForOpenAIKey() {
60
+ const { addKey } = await inquirer.prompt([
61
+ {
62
+ type: 'confirm',
63
+ name: 'addKey',
64
+ message: 'Add OpenAI API key? (optional)',
65
+ default: false,
66
+ },
67
+ ]);
68
+
69
+ if (!addKey) return null;
70
+
71
+ const { openPage } = await inquirer.prompt([
72
+ {
73
+ type: 'confirm',
74
+ name: 'openPage',
75
+ message: 'Open OpenAI API key page in browser?',
76
+ default: true,
77
+ },
78
+ ]);
79
+ if (openPage) {
80
+ await open('https://platform.openai.com/settings/organization/api-keys');
81
+ }
82
+
83
+ const { key } = await inquirer.prompt([
84
+ {
85
+ type: 'password',
86
+ name: 'key',
87
+ message: 'Enter your OpenAI API key:',
88
+ mask: '*',
89
+ validate: (input) => {
90
+ if (!input) return 'Key is required if adding';
91
+ if (!input.startsWith('sk-')) {
92
+ return 'Invalid format. Should start with sk-';
93
+ }
94
+ return true;
95
+ },
96
+ },
97
+ ]);
98
+ return key;
99
+ }
100
+
101
+ /**
102
+ * Prompt for optional Groq API key
103
+ */
104
+ export async function promptForGroqKey() {
105
+ const { addKey } = await inquirer.prompt([
106
+ {
107
+ type: 'confirm',
108
+ name: 'addKey',
109
+ message: 'Add Groq API key? (optional)',
110
+ default: false,
111
+ },
112
+ ]);
113
+
114
+ if (!addKey) return null;
115
+
116
+ const { key } = await inquirer.prompt([
117
+ {
118
+ type: 'password',
119
+ name: 'key',
120
+ message: 'Enter your Groq API key:',
121
+ mask: '*',
122
+ validate: (input) => {
123
+ if (!input) return 'Key is required if adding';
124
+ return true;
125
+ },
126
+ },
127
+ ]);
128
+ return key;
129
+ }
130
+
131
+ /**
132
+ * Prompt for optional Brave Search API key
133
+ */
134
+ export async function promptForBraveKey() {
135
+ const { addKey } = await inquirer.prompt([
136
+ {
137
+ type: 'confirm',
138
+ name: 'addKey',
139
+ message: 'Add Brave Search API key? (free tier, greatly improves agent)',
140
+ default: true,
141
+ },
142
+ ]);
143
+
144
+ if (!addKey) return null;
145
+
146
+ console.log('\n To get a free Brave Search API key:');
147
+ console.log(' 1. Go to https://api-dashboard.search.brave.com/app/keys');
148
+ console.log(' 2. Click "Get Started"');
149
+ console.log(' 3. Create an account (or sign in)');
150
+ console.log(' 4. Subscribe to the "Free" plan (2,000 queries/month)');
151
+ console.log(' 5. Copy your API key\n');
152
+
153
+ const { openPage } = await inquirer.prompt([
154
+ {
155
+ type: 'confirm',
156
+ name: 'openPage',
157
+ message: 'Open Brave Search API page in browser?',
158
+ default: true,
159
+ },
160
+ ]);
161
+ if (openPage) {
162
+ await open('https://api-dashboard.search.brave.com/app/keys');
163
+ }
164
+
165
+ const { key } = await inquirer.prompt([
166
+ {
167
+ type: 'password',
168
+ name: 'key',
169
+ message: 'Enter your Brave Search API key:',
170
+ mask: '*',
171
+ validate: (input) => {
172
+ if (!input) return 'Key is required if adding';
173
+ return true;
174
+ },
175
+ },
176
+ ]);
177
+ return key;
178
+ }
179
+
180
+ /**
181
+ * Prompt for Telegram bot token
182
+ */
183
+ export async function promptForTelegramToken() {
184
+ const { addTelegram } = await inquirer.prompt([
185
+ {
186
+ type: 'confirm',
187
+ name: 'addTelegram',
188
+ message: 'Set up Telegram bot?',
189
+ default: true,
190
+ },
191
+ ]);
192
+
193
+ if (!addTelegram) return null;
194
+
195
+ const { token } = await inquirer.prompt([
196
+ {
197
+ type: 'password',
198
+ name: 'token',
199
+ message: 'Enter your Telegram bot token from @BotFather:',
200
+ mask: '*',
201
+ validate: (input) => {
202
+ if (!input) return 'Token is required';
203
+ if (!/^\d+:[A-Za-z0-9_-]+$/.test(input)) {
204
+ return 'Invalid format. Should be like 123456789:ABC-DEF...';
205
+ }
206
+ return true;
207
+ },
208
+ },
209
+ ]);
210
+ return token;
211
+ }
212
+
213
+ /**
214
+ * Generate a Telegram webhook secret
215
+ */
216
+ export async function generateTelegramWebhookSecret() {
217
+ const { randomBytes } = await import('crypto');
218
+ return randomBytes(32).toString('hex');
219
+ }
220
+
221
+ /**
222
+ * Prompt for deployment method
223
+ */
224
+ export async function promptForDeployMethod() {
225
+ const { method } = await inquirer.prompt([
226
+ {
227
+ type: 'list',
228
+ name: 'method',
229
+ message: 'How would you like to deploy the event handler?',
230
+ choices: [
231
+ { name: 'Deploy to Vercel via CLI (recommended)', value: 'vercel' },
232
+ { name: 'Open Vercel Deploy Button in browser', value: 'button' },
233
+ { name: 'Skip - I\'ll deploy manually later', value: 'skip' },
234
+ ],
235
+ },
236
+ ]);
237
+ return method;
238
+ }
239
+
240
+ /**
241
+ * Prompt for confirmation
242
+ */
243
+ export async function confirm(message, defaultValue = true) {
244
+ const { confirmed } = await inquirer.prompt([
245
+ {
246
+ type: 'confirm',
247
+ name: 'confirmed',
248
+ message,
249
+ default: defaultValue,
250
+ },
251
+ ]);
252
+ return confirmed;
253
+ }
254
+
255
+ /**
256
+ * Prompt for text input
257
+ */
258
+ export async function promptText(message, defaultValue = '') {
259
+ const { value } = await inquirer.prompt([
260
+ {
261
+ type: 'input',
262
+ name: 'value',
263
+ message,
264
+ default: defaultValue,
265
+ },
266
+ ]);
267
+ return value;
268
+ }