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,540 @@
1
+ #!/usr/bin/env node
2
+
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import open from 'open';
6
+ import inquirer from 'inquirer';
7
+
8
+ import {
9
+ checkPrerequisites,
10
+ runGhAuth,
11
+ } from './lib/prerequisites.mjs';
12
+ import {
13
+ promptForPAT,
14
+ promptForAnthropicKey,
15
+ promptForOpenAIKey,
16
+ promptForGroqKey,
17
+ promptForBraveKey,
18
+ promptForTelegramToken,
19
+ generateTelegramWebhookSecret,
20
+ confirm,
21
+ maskSecret,
22
+ } from './lib/prompts.mjs';
23
+ import {
24
+ validatePAT,
25
+ checkPATScopes,
26
+ setSecrets,
27
+ setVariables,
28
+ generateWebhookSecret,
29
+ getPATCreationURL,
30
+ } from './lib/github.mjs';
31
+ import {
32
+ validateAnthropicKey,
33
+ writeEnvFile,
34
+ encodeSecretsBase64,
35
+ encodeLlmSecretsBase64,
36
+ updateEnvVariable,
37
+ } from './lib/auth.mjs';
38
+ import { setTelegramWebhook, validateBotToken, generateVerificationCode } from './lib/telegram.mjs';
39
+ import { runVerificationFlow, verifyRestart } from './lib/telegram-verify.mjs';
40
+
41
+ const logo = `
42
+ _____ _ ____ ____ _
43
+ |_ _| |__ ___| _ \\ ___ _ __ ___| __ ) ___ | |_
44
+ | | | '_ \\ / _ \\ |_) / _ \\| '_ \\ / _ \\ _ \\ / _ \\| __|
45
+ | | | | | | __/ __/ (_) | |_) | __/ |_) | (_) | |_
46
+ |_| |_| |_|\\___|_| \\___/| .__/ \\___|____/ \\___/ \\__|
47
+ |_|
48
+ `;
49
+
50
+ function printHeader() {
51
+ console.log(chalk.cyan(logo));
52
+ console.log(chalk.bold('Interactive Setup Wizard\n'));
53
+ }
54
+
55
+ function printStep(step, total, title) {
56
+ console.log(chalk.bold.blue(`\n[${step}/${total}] ${title}\n`));
57
+ }
58
+
59
+ function printSuccess(message) {
60
+ console.log(chalk.green(' \u2713 ') + message);
61
+ }
62
+
63
+ function printWarning(message) {
64
+ console.log(chalk.yellow(' \u26a0 ') + message);
65
+ }
66
+
67
+ function printError(message) {
68
+ console.log(chalk.red(' \u2717 ') + message);
69
+ }
70
+
71
+ function printInfo(message) {
72
+ console.log(chalk.dim(' \u2192 ') + message);
73
+ }
74
+
75
+ async function main() {
76
+ printHeader();
77
+
78
+ const TOTAL_STEPS = 7;
79
+ let currentStep = 0;
80
+
81
+ // Collected values
82
+ let pat = null;
83
+ let anthropicKey = null;
84
+ let openaiKey = null;
85
+ let groqKey = null;
86
+ let braveKey = null;
87
+ let telegramToken = null;
88
+ let telegramWebhookSecret = null;
89
+ let webhookSecret = null;
90
+ let owner = null;
91
+ let repo = null;
92
+
93
+ // Step 1: Prerequisites Check
94
+ printStep(++currentStep, TOTAL_STEPS, 'Checking prerequisites');
95
+
96
+ const spinner = ora('Checking system requirements...').start();
97
+ const prereqs = await checkPrerequisites();
98
+ spinner.stop();
99
+
100
+ // Node.js
101
+ if (prereqs.node.ok) {
102
+ printSuccess(`Node.js ${prereqs.node.version}`);
103
+ } else if (prereqs.node.installed) {
104
+ printError(`Node.js ${prereqs.node.version} (need >= 18)`);
105
+ console.log(chalk.red('\n Please upgrade Node.js to version 18 or higher.'));
106
+ process.exit(1);
107
+ } else {
108
+ printError('Node.js not found');
109
+ console.log(chalk.red('\n Please install Node.js 18+: https://nodejs.org'));
110
+ process.exit(1);
111
+ }
112
+
113
+ // Package manager
114
+ if (prereqs.packageManager.installed) {
115
+ printSuccess(`Package manager: ${prereqs.packageManager.name}`);
116
+ } else {
117
+ printError('No package manager found (need pnpm or npm)');
118
+ process.exit(1);
119
+ }
120
+
121
+ // Git
122
+ if (prereqs.git.installed) {
123
+ printSuccess('Git installed');
124
+ if (prereqs.git.remoteInfo) {
125
+ owner = prereqs.git.remoteInfo.owner;
126
+ repo = prereqs.git.remoteInfo.repo;
127
+ printSuccess(`Repository: ${owner}/${repo}`);
128
+ } else {
129
+ printWarning('Could not detect GitHub repository from git remote');
130
+ }
131
+ } else {
132
+ printError('Git not found');
133
+ process.exit(1);
134
+ }
135
+
136
+ // gh CLI
137
+ if (prereqs.gh.installed) {
138
+ if (prereqs.gh.authenticated) {
139
+ printSuccess('GitHub CLI authenticated');
140
+ } else {
141
+ printWarning('GitHub CLI installed but not authenticated');
142
+ const shouldAuth = await confirm('Run gh auth login now?');
143
+ if (shouldAuth) {
144
+ try {
145
+ runGhAuth();
146
+ printSuccess('GitHub CLI authenticated');
147
+ } catch {
148
+ printError('Failed to authenticate gh CLI');
149
+ process.exit(1);
150
+ }
151
+ } else {
152
+ printError('GitHub CLI authentication required');
153
+ process.exit(1);
154
+ }
155
+ }
156
+ } else {
157
+ printError('GitHub CLI (gh) not found');
158
+ printInfo('Install with: brew install gh');
159
+ const shouldInstall = await confirm('Try to install gh with homebrew?');
160
+ if (shouldInstall) {
161
+ const installSpinner = ora('Installing gh CLI...').start();
162
+ try {
163
+ const { execSync } = await import('child_process');
164
+ execSync('brew install gh', { stdio: 'inherit' });
165
+ installSpinner.succeed('gh CLI installed');
166
+ runGhAuth();
167
+ } catch {
168
+ installSpinner.fail('Failed to install gh CLI');
169
+ process.exit(1);
170
+ }
171
+ } else {
172
+ process.exit(1);
173
+ }
174
+ }
175
+
176
+ // ngrok check (informational only)
177
+ if (prereqs.ngrok.installed) {
178
+ printSuccess('ngrok installed');
179
+ } else {
180
+ printWarning('ngrok not installed (needed to expose local server)');
181
+ printInfo('Install with: brew install ngrok/ngrok/ngrok');
182
+ }
183
+
184
+ // Step 2: GitHub PAT
185
+ printStep(++currentStep, TOTAL_STEPS, 'GitHub Personal Access Token');
186
+
187
+ console.log(chalk.dim(' Create a fine-grained PAT with these repository permissions:\n'));
188
+ console.log(chalk.dim(' \u2022 Actions: Read-only'));
189
+ console.log(chalk.dim(' \u2022 Contents: Read and write'));
190
+ console.log(chalk.dim(' \u2022 Metadata: Read-only (required, auto-selected)'));
191
+ console.log(chalk.dim(' \u2022 Pull requests: Read and write\n'));
192
+
193
+ const openPATPage = await confirm('Open GitHub PAT creation page in browser?');
194
+ if (openPATPage) {
195
+ await open(getPATCreationURL());
196
+ printInfo('Opened in browser. Select the permissions listed above.');
197
+ }
198
+
199
+ let patValid = false;
200
+ while (!patValid) {
201
+ pat = await promptForPAT();
202
+
203
+ const validateSpinner = ora('Validating PAT...').start();
204
+ const validation = await validatePAT(pat);
205
+
206
+ if (!validation.valid) {
207
+ validateSpinner.fail(`Invalid PAT: ${validation.error}`);
208
+ continue;
209
+ }
210
+
211
+ const scopes = await checkPATScopes(pat);
212
+ if (!scopes.hasRepo || !scopes.hasWorkflow) {
213
+ validateSpinner.fail('PAT missing required scopes');
214
+ printInfo(`Found scopes: ${scopes.scopes.join(', ') || 'none'}`);
215
+ continue;
216
+ }
217
+
218
+ if (scopes.isFineGrained) {
219
+ validateSpinner.succeed(`Fine-grained PAT valid for user: ${validation.user}`);
220
+ } else {
221
+ validateSpinner.succeed(`PAT valid for user: ${validation.user}`);
222
+ }
223
+ patValid = true;
224
+ }
225
+
226
+ // Step 3: API Keys
227
+ printStep(++currentStep, TOTAL_STEPS, 'API Keys');
228
+
229
+ console.log(chalk.dim(' Anthropic API key is required. Others are optional.\n'));
230
+
231
+ // Anthropic (required)
232
+ const openAnthropicPage = await confirm('Open Anthropic API key page in browser?');
233
+ if (openAnthropicPage) {
234
+ await open('https://platform.claude.com/settings/keys');
235
+ printInfo('Opened in browser. Create an API key and copy it.');
236
+ }
237
+
238
+ let anthropicValid = false;
239
+ while (!anthropicValid) {
240
+ anthropicKey = await promptForAnthropicKey();
241
+
242
+ const validateSpinner = ora('Validating Anthropic API key...').start();
243
+ const validation = await validateAnthropicKey(anthropicKey);
244
+
245
+ if (validation.valid) {
246
+ validateSpinner.succeed('Anthropic API key valid');
247
+ anthropicValid = true;
248
+ } else {
249
+ validateSpinner.fail(`Invalid key: ${validation.error}`);
250
+ }
251
+ }
252
+
253
+ // OpenAI (optional)
254
+ openaiKey = await promptForOpenAIKey();
255
+ if (openaiKey) {
256
+ printSuccess(`OpenAI key added (${maskSecret(openaiKey)})`);
257
+ }
258
+
259
+ // Groq (optional)
260
+ groqKey = await promptForGroqKey();
261
+ if (groqKey) {
262
+ printSuccess(`Groq key added (${maskSecret(groqKey)})`);
263
+ }
264
+
265
+ // Brave Search (optional, default: true since it's free)
266
+ braveKey = await promptForBraveKey();
267
+ if (braveKey) {
268
+ printSuccess(`Brave Search key added (${maskSecret(braveKey)})`);
269
+ }
270
+
271
+ const keys = {
272
+ anthropic: anthropicKey,
273
+ openai: openaiKey,
274
+ groq: groqKey,
275
+ brave: braveKey,
276
+ };
277
+
278
+ // Step 4: Set GitHub Secrets
279
+ printStep(++currentStep, TOTAL_STEPS, 'Set GitHub Secrets');
280
+
281
+ if (!owner || !repo) {
282
+ printWarning('Could not detect repository. Please enter manually.');
283
+ const answers = await inquirer.prompt([
284
+ { type: 'input', name: 'owner', message: 'GitHub owner/org:' },
285
+ { type: 'input', name: 'repo', message: 'Repository name:' },
286
+ ]);
287
+ owner = answers.owner;
288
+ repo = answers.repo;
289
+ }
290
+
291
+ webhookSecret = generateWebhookSecret();
292
+ const secretsBase64 = encodeSecretsBase64(pat, keys);
293
+ const llmSecretsBase64 = encodeLlmSecretsBase64(keys);
294
+
295
+ const secrets = {
296
+ SECRETS: secretsBase64,
297
+ GH_WEBHOOK_SECRET: webhookSecret,
298
+ };
299
+
300
+ if (llmSecretsBase64) {
301
+ secrets.LLM_SECRETS = llmSecretsBase64;
302
+ }
303
+
304
+ const secretSpinner = ora('Setting GitHub secrets...').start();
305
+ const secretResults = await setSecrets(owner, repo, secrets);
306
+
307
+ secretSpinner.stop();
308
+ let allSecretsSet = true;
309
+ for (const [name, result] of Object.entries(secretResults)) {
310
+ if (result.success) {
311
+ printSuccess(`Set ${name}`);
312
+ } else {
313
+ printError(`Failed to set ${name}: ${result.error}`);
314
+ allSecretsSet = false;
315
+ }
316
+ }
317
+
318
+ if (!allSecretsSet) {
319
+ printWarning('Some secrets failed - you may need to set them manually');
320
+ }
321
+
322
+ // Set default GitHub repository variables
323
+ const varsSpinner = ora('Setting GitHub repository variables...').start();
324
+ const defaultVars = {
325
+ AUTO_MERGE: 'true',
326
+ ALLOWED_PATHS: '/logs',
327
+ MODEL: 'claude-sonnet-4-5-20250929',
328
+ };
329
+ const varResults = await setVariables(owner, repo, defaultVars);
330
+ varsSpinner.stop();
331
+ for (const [name, result] of Object.entries(varResults)) {
332
+ if (result.success) {
333
+ printSuccess(`Set ${name} = ${defaultVars[name]}`);
334
+ } else {
335
+ printError(`Failed to set ${name}: ${result.error}`);
336
+ }
337
+ }
338
+
339
+ // Step 5: Telegram Setup
340
+ printStep(++currentStep, TOTAL_STEPS, 'Telegram Setup');
341
+
342
+ telegramToken = await promptForTelegramToken();
343
+
344
+ if (telegramToken) {
345
+ const validateSpinner = ora('Validating bot token...').start();
346
+ const validation = await validateBotToken(telegramToken);
347
+
348
+ if (!validation.valid) {
349
+ validateSpinner.fail(`Invalid token: ${validation.error}`);
350
+ telegramToken = null;
351
+ } else {
352
+ validateSpinner.succeed(`Bot: @${validation.botInfo.username}`);
353
+ telegramWebhookSecret = await generateTelegramWebhookSecret();
354
+ }
355
+ } else {
356
+ printInfo('Skipped Telegram setup');
357
+ }
358
+
359
+ // Write .env file (now at project root, not event_handler/)
360
+ const apiKey = generateWebhookSecret().slice(0, 32); // Random API key for webhook endpoint
361
+ const telegramVerification = telegramToken ? generateVerificationCode() : null;
362
+ const envPath = writeEnvFile({
363
+ apiKey,
364
+ githubToken: pat,
365
+ githubOwner: owner,
366
+ githubRepo: repo,
367
+ telegramBotToken: telegramToken,
368
+ telegramWebhookSecret,
369
+ ghWebhookSecret: webhookSecret,
370
+ anthropicApiKey: anthropicKey,
371
+ openaiApiKey: openaiKey,
372
+ telegramChatId: null,
373
+ telegramVerification,
374
+ });
375
+ printSuccess(`Created ${envPath}`);
376
+
377
+ // Step 6: Start Server & ngrok
378
+ printStep(++currentStep, TOTAL_STEPS, 'Start Server & ngrok');
379
+
380
+ console.log(chalk.bold(' Now we need to start the server and expose it via ngrok.\n'));
381
+ console.log(chalk.yellow(' Open TWO new terminal windows and run:\n'));
382
+ console.log(chalk.bold(' Terminal 1:'));
383
+ console.log(chalk.cyan(' npm run dev\n'));
384
+ console.log(chalk.bold(' Terminal 2:'));
385
+ console.log(chalk.cyan(' ngrok http 3000\n'));
386
+
387
+ console.log(chalk.dim(' ngrok will show a "Forwarding" URL like: https://abc123.ngrok.io\n'));
388
+ console.log(chalk.yellow(' Note: ') + chalk.dim('ngrok URLs change each time you restart it (unless you have a paid plan).'));
389
+ console.log(chalk.dim(' When your URL changes, run: ') + chalk.cyan('npm run setup-telegram') + chalk.dim(' to reconfigure.\n'));
390
+
391
+ let ngrokUrl = null;
392
+ while (!ngrokUrl) {
393
+ const { url } = await inquirer.prompt([
394
+ {
395
+ type: 'input',
396
+ name: 'url',
397
+ message: 'Paste your ngrok URL (https://...ngrok...):',
398
+ validate: (input) => {
399
+ if (!input) return 'URL is required';
400
+ if (!input.startsWith('https://')) return 'URL must start with https://';
401
+ if (!input.includes('ngrok')) return 'URL should be an ngrok URL';
402
+ return true;
403
+ },
404
+ },
405
+ ]);
406
+ const testUrl = url.replace(/\/$/, '');
407
+
408
+ // Verify the server is reachable through ngrok
409
+ const healthSpinner = ora('Verifying server is reachable...').start();
410
+ try {
411
+ const response = await fetch(`${testUrl}/api/ping`, {
412
+ method: 'GET',
413
+ headers: { 'x-api-key': apiKey },
414
+ signal: AbortSignal.timeout(10000)
415
+ });
416
+ if (response.ok) {
417
+ const data = await response.json();
418
+ if (data.message === 'Pong!') {
419
+ healthSpinner.succeed('Server is reachable and authenticated');
420
+ ngrokUrl = testUrl;
421
+ } else {
422
+ healthSpinner.fail('Unexpected response from server');
423
+ const retry = await confirm('Try again?');
424
+ if (!retry) {
425
+ ngrokUrl = testUrl;
426
+ }
427
+ }
428
+ } else if (response.status === 401) {
429
+ healthSpinner.fail('Server is running but returned 401 (unauthorized)');
430
+ console.log('');
431
+ printWarning('This means the server is using an old API key that doesn\'t match the one we just generated.');
432
+ printInfo('The setup created a new .env file with a fresh API key, but your running server hasn\'t picked it up yet.');
433
+ console.log('');
434
+ console.log(chalk.bold(' To fix this, restart your server:\n'));
435
+ console.log(chalk.cyan(' 1. Go to Terminal 1 (where the server is running)'));
436
+ console.log(chalk.cyan(' 2. Press Ctrl+C to stop it'));
437
+ console.log(chalk.cyan(' 3. Run: npm run dev\n'));
438
+ const retry = await confirm('Retry after restarting the server?');
439
+ if (!retry) {
440
+ ngrokUrl = testUrl;
441
+ }
442
+ } else {
443
+ healthSpinner.fail(`Server returned status ${response.status}`);
444
+ printWarning('Make sure the server is running (npm run dev)');
445
+ const retry = await confirm('Try again?');
446
+ if (!retry) {
447
+ ngrokUrl = testUrl;
448
+ }
449
+ }
450
+ } catch (error) {
451
+ healthSpinner.fail(`Could not reach server: ${error.message}`);
452
+ printWarning('Make sure both the server AND ngrok are running');
453
+ printInfo('Terminal 1: npm run dev');
454
+ printInfo('Terminal 2: ngrok http 3000');
455
+ const retry = await confirm('Try again?');
456
+ if (!retry) {
457
+ ngrokUrl = testUrl; // Continue anyway
458
+ }
459
+ }
460
+ }
461
+
462
+ // Set GH_WEBHOOK_URL variable
463
+ const urlSpinner = ora('Setting GH_WEBHOOK_URL variable...').start();
464
+ const urlResult = await setVariables(owner, repo, { GH_WEBHOOK_URL: ngrokUrl });
465
+ if (urlResult.GH_WEBHOOK_URL.success) {
466
+ urlSpinner.succeed('GH_WEBHOOK_URL variable set');
467
+ } else {
468
+ urlSpinner.fail(`Failed: ${urlResult.GH_WEBHOOK_URL.error}`);
469
+ }
470
+
471
+ // Register Telegram webhook if configured
472
+ if (telegramToken) {
473
+ const webhookUrl = `${ngrokUrl}/api/telegram/webhook`;
474
+ const tgSpinner = ora('Registering Telegram webhook...').start();
475
+ const tgResult = await setTelegramWebhook(telegramToken, webhookUrl, telegramWebhookSecret);
476
+ if (tgResult.ok) {
477
+ tgSpinner.succeed(`Telegram webhook registered: ${webhookUrl}`);
478
+ } else {
479
+ tgSpinner.fail(`Failed: ${tgResult.description}`);
480
+ }
481
+
482
+ // Chat ID verification
483
+ const chatId = await runVerificationFlow(telegramVerification);
484
+
485
+ if (chatId) {
486
+ updateEnvVariable('TELEGRAM_CHAT_ID', chatId);
487
+ printSuccess(`Chat ID saved: ${chatId}`);
488
+
489
+ const verified = await verifyRestart(ngrokUrl, apiKey);
490
+ if (verified) {
491
+ printSuccess('Telegram bot is configured and working!');
492
+ } else {
493
+ printWarning('Could not verify bot. Check your configuration.');
494
+ }
495
+ } else {
496
+ printWarning('Chat ID is required \u2014 the bot will not respond without it.');
497
+ printInfo('Run npm run setup-telegram to complete setup.');
498
+ }
499
+ }
500
+
501
+ // Step 7: Summary
502
+ printStep(++currentStep, TOTAL_STEPS, 'Setup Complete!');
503
+
504
+ console.log(chalk.bold.green('\n Configuration Summary:\n'));
505
+
506
+ console.log(` ${chalk.dim('Repository:')} ${owner}/${repo}`);
507
+ console.log(` ${chalk.dim('Webhook URL:')} ${ngrokUrl}`);
508
+ console.log(` ${chalk.dim('GitHub PAT:')} ${maskSecret(pat)}`);
509
+ console.log(` ${chalk.dim('Anthropic Key:')} ${maskSecret(anthropicKey)}`);
510
+ if (openaiKey) console.log(` ${chalk.dim('OpenAI Key:')} ${maskSecret(openaiKey)}`);
511
+ if (groqKey) console.log(` ${chalk.dim('Groq Key:')} ${maskSecret(groqKey)}`);
512
+ if (braveKey) console.log(` ${chalk.dim('Brave Search:')} ${maskSecret(braveKey)}`);
513
+ if (telegramToken) console.log(` ${chalk.dim('Telegram Bot:')} Webhook registered`);
514
+
515
+ console.log(chalk.bold('\n GitHub Secrets Set:\n'));
516
+ console.log(' \u2022 SECRETS');
517
+ if (llmSecretsBase64) console.log(' \u2022 LLM_SECRETS');
518
+ console.log(' \u2022 GH_WEBHOOK_SECRET');
519
+
520
+ console.log(chalk.bold('\n GitHub Variables Set:\n'));
521
+ console.log(' \u2022 GH_WEBHOOK_URL');
522
+ console.log(' \u2022 AUTO_MERGE = true');
523
+ console.log(' \u2022 ALLOWED_PATHS = /logs');
524
+ console.log(' \u2022 MODEL = claude-sonnet-4-5-20250929');
525
+
526
+ console.log(chalk.bold.green('\n You\'re all set!\n'));
527
+
528
+ if (telegramToken) {
529
+ console.log(chalk.cyan(' Message your Telegram bot to create your first job!'));
530
+ } else {
531
+ console.log(chalk.dim(' Use the /api/webhook endpoint to create jobs.'));
532
+ }
533
+
534
+ console.log('\n');
535
+ }
536
+
537
+ main().catch((error) => {
538
+ console.error(chalk.red('\nSetup failed:'), error.message);
539
+ process.exit(1);
540
+ });
@@ -0,0 +1,38 @@
1
+ # thepopebot Configuration
2
+ # Copy this file to .env and fill in your values
3
+ # NEVER commit the actual .env file with real secrets!
4
+
5
+ # Authentication key for /api/webhook endpoint (generate random string)
6
+ API_KEY=your_random_api_key_here
7
+
8
+ # GitHub Personal Access Token (needs repo, workflow scopes)
9
+ GH_TOKEN=ghp_your_token_here
10
+
11
+ # Repository info
12
+ GH_OWNER=your_github_username
13
+ GH_REPO=your_repo_name
14
+
15
+ # Telegram bot token from @BotFather (optional)
16
+ TELEGRAM_BOT_TOKEN=
17
+
18
+ # Secret for validating Telegram webhooks (generate with: openssl rand -hex 32)
19
+ TELEGRAM_WEBHOOK_SECRET=
20
+
21
+ # Verification code for getting your chat ID (e.g., verify-abc12345)
22
+ TELEGRAM_VERIFICATION=
23
+
24
+ # Secret for GitHub Actions webhook auth (must match GH_WEBHOOK_SECRET secret)
25
+ GH_WEBHOOK_SECRET=
26
+
27
+ # Anthropic API key for Claude chat features
28
+ ANTHROPIC_API_KEY=sk-ant-your_key_here
29
+
30
+ # OpenAI API key for Whisper voice transcription (optional)
31
+ OPENAI_API_KEY=
32
+
33
+ # Claude model for chat (optional, default: claude-sonnet-4)
34
+ # EVENT_HANDLER_MODEL=claude-sonnet-4
35
+
36
+ # Default Telegram chat ID for scheduled notifications
37
+ # Get this by messaging your bot and checking the webhook logs, or use @userinfobot
38
+ TELEGRAM_CHAT_ID=
@@ -0,0 +1,117 @@
1
+ name: Auto-Merge Job PR
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened]
6
+ branches: [main]
7
+
8
+ jobs:
9
+ auto-merge:
10
+ runs-on: ubuntu-latest
11
+ if: startsWith(github.event.pull_request.head.ref, 'job/')
12
+ permissions:
13
+ contents: write
14
+ pull-requests: write
15
+
16
+ steps:
17
+ - name: Wait for merge check
18
+ id: merge-check
19
+ env:
20
+ GH_TOKEN: ${{ github.token }}
21
+ run: |
22
+ PR_NUMBER="${{ github.event.pull_request.number }}"
23
+ for i in $(seq 1 30); do
24
+ sleep 10
25
+ MERGEABLE=$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json mergeable -q '.mergeable')
26
+ if [ "$MERGEABLE" != "" ] && [ "$MERGEABLE" != "UNKNOWN" ]; then
27
+ echo "mergeable=$MERGEABLE" >> "$GITHUB_OUTPUT"
28
+ echo "Mergeable: $MERGEABLE"
29
+ exit 0
30
+ fi
31
+ echo "Still computing... (attempt $i/30)"
32
+ done
33
+ echo "mergeable=UNKNOWN" >> "$GITHUB_OUTPUT"
34
+ echo "Timed out waiting for merge check"
35
+
36
+ - name: Check AUTO_MERGE setting
37
+ if: steps.merge-check.outputs.mergeable == 'MERGEABLE'
38
+ id: check-setting
39
+ run: |
40
+ AUTO_MERGE="${{ vars.AUTO_MERGE }}"
41
+ if [ "$AUTO_MERGE" = "false" ]; then
42
+ echo "Auto-merge disabled via AUTO_MERGE=false"
43
+ echo "enabled=false" >> "$GITHUB_OUTPUT"
44
+ else
45
+ echo "Auto-merge enabled"
46
+ echo "enabled=true" >> "$GITHUB_OUTPUT"
47
+ fi
48
+
49
+ - name: Check ALLOWED_PATHS
50
+ if: steps.merge-check.outputs.mergeable == 'MERGEABLE' && steps.check-setting.outputs.enabled == 'true'
51
+ id: check-paths
52
+ env:
53
+ GH_TOKEN: ${{ github.token }}
54
+ run: |
55
+ ALLOWED_PATHS="${{ vars.ALLOWED_PATHS }}"
56
+ PR_NUMBER="${{ github.event.pull_request.number }}"
57
+
58
+ # Default: only logs allowed
59
+ if [ -z "$ALLOWED_PATHS" ]; then
60
+ ALLOWED_PATHS="/logs"
61
+ fi
62
+
63
+ # Normalize: ensure each prefix starts with /
64
+ IFS=',' read -ra RAW_PREFIXES <<< "$ALLOWED_PATHS"
65
+ PREFIXES=()
66
+ for p in "${RAW_PREFIXES[@]}"; do
67
+ trimmed=$(echo "$p" | xargs)
68
+ [ -z "$trimmed" ] && continue
69
+ # Ensure leading /
70
+ [[ "$trimmed" != /* ]] && trimmed="/$trimmed"
71
+ PREFIXES+=("$trimmed")
72
+ done
73
+
74
+ # "/" means everything allowed
75
+ for p in "${PREFIXES[@]}"; do
76
+ if [ "$p" = "/" ]; then
77
+ echo "All paths allowed (ALLOWED_PATHS contains /)"
78
+ echo "allowed=true" >> "$GITHUB_OUTPUT"
79
+ exit 0
80
+ fi
81
+ done
82
+
83
+ # Get changed files
84
+ CHANGED_FILES=$(gh pr diff "$PR_NUMBER" --name-only --repo "${{ github.repository }}")
85
+ if [ -z "$CHANGED_FILES" ]; then
86
+ echo "No changed files"
87
+ echo "allowed=true" >> "$GITHUB_OUTPUT"
88
+ exit 0
89
+ fi
90
+
91
+ # Check each file against prefixes
92
+ # Strip leading / from prefixes for comparison (git paths are relative)
93
+ ALL_OK=true
94
+ while IFS= read -r file; do
95
+ [ -z "$file" ] && continue
96
+ FILE_OK=false
97
+ for prefix in "${PREFIXES[@]}"; do
98
+ compare="${prefix#/}"
99
+ if [[ "$file" == "$compare"* ]]; then
100
+ FILE_OK=true
101
+ break
102
+ fi
103
+ done
104
+ if [ "$FILE_OK" = "false" ]; then
105
+ echo "BLOCKED: $file"
106
+ ALL_OK=false
107
+ fi
108
+ done <<< "$CHANGED_FILES"
109
+
110
+ echo "allowed=$ALL_OK" >> "$GITHUB_OUTPUT"
111
+
112
+ - name: Merge PR
113
+ if: steps.merge-check.outputs.mergeable == 'MERGEABLE' && steps.check-setting.outputs.enabled == 'true' && steps.check-paths.outputs.allowed == 'true'
114
+ env:
115
+ GH_TOKEN: ${{ github.token }}
116
+ run: |
117
+ gh pr merge ${{ github.event.pull_request.number }} --squash --repo "${{ github.repository }}"