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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stephen G. Pope
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # thepopebot
2
+
3
+ **Autonomous AI agents. All the power. None of the leaked API keys.**
4
+
5
+ ---
6
+
7
+ ## Why thepopebot?
8
+
9
+ **Secure by default** — Other frameworks hand credentials to the LLM and hope for the best. thepopebot is different: the AI literally cannot access your secrets, even if it tries. Secrets are filtered at the process level before the agent's shell even starts.
10
+
11
+ **The repository IS the agent** — Every action your agent takes is a git commit. You can see exactly what it did, when, and why. If it screws up, revert it. Want to clone your agent? Fork the repo — code, personality, scheduled jobs, full history, all of it goes with your fork.
12
+
13
+ **Free compute, built in** — Every GitHub account comes with free cloud computing time. thepopebot uses that to run your agent. One task or a hundred in parallel — the compute is already included.
14
+
15
+ **Self-evolving** — The agent modifies its own code through pull requests. Every change is auditable, every change is reversible. You stay in control.
16
+
17
+ ---
18
+
19
+ ## How It Works
20
+
21
+ ```
22
+ ┌───────────────────────────────────────────────────────────────────────┐
23
+ │ │
24
+ │ ┌─────────────────┐ ┌─────────────────┐ │
25
+ │ │ Event Handler │ ──1──► │ GitHub │ │
26
+ │ │ (creates job) │ │ (job/* branch) │ │
27
+ │ └────────▲────────┘ └────────┬────────┘ │
28
+ │ │ │ │
29
+ │ │ 2 (triggers run-job.yml) │
30
+ │ │ │ │
31
+ │ │ ▼ │
32
+ │ │ ┌─────────────────┐ │
33
+ │ │ │ Docker Agent │ │
34
+ │ │ │ (runs Pi, PRs) │ │
35
+ │ │ └────────┬────────┘ │
36
+ │ │ │ │
37
+ │ │ 3 (creates PR) │
38
+ │ │ │ │
39
+ │ │ ▼ │
40
+ │ │ ┌─────────────────┐ │
41
+ │ │ │ GitHub │ │
42
+ │ │ │ (PR opened) │ │
43
+ │ │ └────────┬────────┘ │
44
+ │ │ │ │
45
+ │ │ 4a (auto-merge.yml) │
46
+ │ │ 4b (update-event-handler.yml) │
47
+ │ │ │ │
48
+ │ 5 (Telegram notification) │ │
49
+ │ └───────────────────────────┘ │
50
+ │ │
51
+ └───────────────────────────────────────────────────────────────────────┘
52
+ ```
53
+
54
+ You talk to your bot on Telegram (or hit a webhook). The Event Handler creates a job branch. GitHub Actions spins up a Docker container with the Pi coding agent. The agent does the work, commits the results, and opens a PR. Auto-merge handles the rest. You get a Telegram notification when it's done.
55
+
56
+ ---
57
+
58
+ ## Get FREE server time on Github!
59
+
60
+ | | thepopebot | Other platforms |
61
+ |---|---|---|
62
+ | **Public repos** | Free. $0. GitHub Actions doesn't charge. | $20-100+/month |
63
+ | **Private repos** | 2,000 free minutes/month (every GitHub plan, including free) | $20-100+/month |
64
+ | **Infrastructure** | GitHub Actions (already included) | Dedicated servers |
65
+
66
+ You just bring your own [Anthropic API key](https://console.anthropic.com/).
67
+
68
+ ---
69
+
70
+ ## Get Started
71
+
72
+ ### Prerequisites
73
+
74
+ | Requirement | Install |
75
+ |-------------|---------|
76
+ | **Node.js 18+** | [nodejs.org](https://nodejs.org) |
77
+ | **npm** | Included with Node.js |
78
+ | **Git** | [git-scm.com](https://git-scm.com) |
79
+ | **GitHub CLI** | [cli.github.com](https://cli.github.com) |
80
+ | **ngrok*** | [ngrok.com](https://ngrok.com/download) |
81
+
82
+ *\*ngrok is only required for local development. Production deployments don't need it.*
83
+
84
+ ### Three steps
85
+
86
+ **Step 1** — Fork this repository:
87
+
88
+ [![Fork this repo](https://img.shields.io/badge/Fork_this_repo-238636?style=for-the-badge&logo=github&logoColor=white)](https://github.com/stephengpope/thepopebot/fork)
89
+
90
+ > GitHub Actions are disabled by default on forks. Go to the **Actions** tab in your fork and enable them.
91
+
92
+ **Step 2** — Clone your fork:
93
+
94
+ ```bash
95
+ git clone https://github.com/YOUR_USERNAME/thepopebot.git
96
+ cd thepopebot
97
+ ```
98
+
99
+ **Step 3** — Run the setup wizard:
100
+
101
+ ```bash
102
+ npm run setup
103
+ ```
104
+
105
+ The wizard handles everything:
106
+ - Checks prerequisites (Node.js, Git, GitHub CLI, ngrok)
107
+ - Creates a GitHub Personal Access Token
108
+ - Collects API keys (Anthropic required; OpenAI, Groq, and [Brave Search](https://api-dashboard.search.brave.com/app/keys) optional)
109
+ - Sets GitHub repository secrets and variables
110
+ - Sets up Telegram bot
111
+ - Starts the server + ngrok, generates `event_handler/.env`
112
+ - Registers webhooks and verifies everything works
113
+
114
+ **After setup, message your Telegram bot to create jobs!**
115
+
116
+ ---
117
+
118
+ ## Docs
119
+
120
+ | Document | Description |
121
+ |----------|-------------|
122
+ | [Architecture](docs/ARCHITECTURE.md) | Two-layer design, file structure, API endpoints, GitHub Actions, Docker agent |
123
+ | [Configuration](docs/CONFIGURATION.md) | Environment variables, GitHub secrets, repo variables, ngrok, Telegram setup |
124
+ | [Customization](docs/CUSTOMIZATION.md) | Personality, skills, operating system files, using your bot, security details |
125
+ | [Auto-Merge](docs/AUTO_MERGE.md) | Auto-merge controls, ALLOWED_PATHS configuration |
126
+ | [How to Use Pi](docs/HOW_TO_USE_PI.md) | Guide to the Pi coding agent |
127
+ | [Security](docs/SECURITY_TODO.md) | Security hardening plan |
package/api/index.js ADDED
@@ -0,0 +1,357 @@
1
+ const paths = require('../lib/paths');
2
+ const { render_md } = require('../lib/utils/render-md');
3
+ const { createJob } = require('../lib/tools/create-job');
4
+ const { setWebhook, sendMessage, downloadFile, reactToMessage, startTypingIndicator } = require('../lib/tools/telegram');
5
+ const { isWhisperEnabled, transcribeAudio } = require('../lib/tools/openai');
6
+ const { chat, getApiKey } = require('../lib/claude');
7
+ const { toolDefinitions, toolExecutors } = require('../lib/claude/tools');
8
+ const { getHistory, updateHistory } = require('../lib/claude/conversation');
9
+ const { getJobStatus } = require('../lib/tools/github');
10
+
11
+ // Bot token from env, can be overridden by /telegram/register
12
+ let telegramBotToken = null;
13
+
14
+ // Cached trigger firing function (initialized on first request)
15
+ let _fireTriggers = null;
16
+
17
+ function getTelegramBotToken() {
18
+ if (!telegramBotToken) {
19
+ telegramBotToken = process.env.TELEGRAM_BOT_TOKEN || null;
20
+ }
21
+ return telegramBotToken;
22
+ }
23
+
24
+ function getFireTriggers() {
25
+ if (!_fireTriggers) {
26
+ const { loadTriggers } = require('../lib/triggers');
27
+ const result = loadTriggers();
28
+ _fireTriggers = result.fireTriggers;
29
+ }
30
+ return _fireTriggers;
31
+ }
32
+
33
+ // Routes that have their own authentication
34
+ const PUBLIC_ROUTES = ['/telegram/webhook', '/github/webhook'];
35
+
36
+ /**
37
+ * Check API key authentication
38
+ * @param {string} routePath - The route path
39
+ * @param {Request} request - The incoming request
40
+ * @returns {Response|null} - Error response if unauthorized, null if OK
41
+ */
42
+ function checkAuth(routePath, request) {
43
+ if (PUBLIC_ROUTES.includes(routePath)) return null;
44
+
45
+ const apiKey = request.headers.get('x-api-key');
46
+ if (apiKey !== process.env.API_KEY) {
47
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Extract job ID from branch name (e.g., "job/abc123" -> "abc123")
54
+ */
55
+ function extractJobId(branchName) {
56
+ if (!branchName || !branchName.startsWith('job/')) return null;
57
+ return branchName.slice(4);
58
+ }
59
+
60
+ /**
61
+ * Summarize a completed job using Claude
62
+ * @param {Object} results - Job results from webhook payload
63
+ * @returns {Promise<string>} The message to send to Telegram
64
+ */
65
+ async function summarizeJob(results) {
66
+ try {
67
+ const apiKey = getApiKey();
68
+
69
+ // System prompt from JOB_SUMMARY.md (supports {{includes}})
70
+ const systemPrompt = render_md(paths.jobSummaryMd);
71
+
72
+ // User message: structured job results
73
+ const userMessage = [
74
+ results.job ? `## Task\n${results.job}` : '',
75
+ results.commit_message ? `## Commit Message\n${results.commit_message}` : '',
76
+ results.changed_files?.length ? `## Changed Files\n${results.changed_files.join('\n')}` : '',
77
+ results.pr_status ? `## PR Status\n${results.pr_status}` : '',
78
+ results.merge_result ? `## Merge Result\n${results.merge_result}` : '',
79
+ results.pr_url ? `## PR URL\n${results.pr_url}` : '',
80
+ results.log ? `## Agent Log\n${results.log}` : '',
81
+ ].filter(Boolean).join('\n\n');
82
+
83
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
84
+ method: 'POST',
85
+ headers: {
86
+ 'Content-Type': 'application/json',
87
+ 'x-api-key': apiKey,
88
+ 'anthropic-version': '2023-06-01',
89
+ },
90
+ body: JSON.stringify({
91
+ model: process.env.EVENT_HANDLER_MODEL || 'claude-sonnet-4-20250514',
92
+ max_tokens: 1024,
93
+ system: systemPrompt,
94
+ messages: [{ role: 'user', content: userMessage }],
95
+ }),
96
+ });
97
+
98
+ if (!response.ok) throw new Error(`Claude API error: ${response.status}`);
99
+
100
+ const result = await response.json();
101
+ return (result.content?.[0]?.text || '').trim() || 'Job completed.';
102
+ } catch (err) {
103
+ console.error('Failed to summarize job:', err);
104
+ return 'Job completed.';
105
+ }
106
+ }
107
+
108
+ // ─────────────────────────────────────────────────────────────────────────────
109
+ // Route handlers
110
+ // ─────────────────────────────────────────────────────────────────────────────
111
+
112
+ async function handleWebhook(request) {
113
+ const body = await request.json();
114
+ const { job } = body;
115
+ if (!job) return Response.json({ error: 'Missing job field' }, { status: 400 });
116
+
117
+ try {
118
+ const result = await createJob(job);
119
+ return Response.json(result);
120
+ } catch (err) {
121
+ console.error(err);
122
+ return Response.json({ error: 'Failed to create job' }, { status: 500 });
123
+ }
124
+ }
125
+
126
+ async function handleTelegramRegister(request) {
127
+ const body = await request.json();
128
+ const { bot_token, webhook_url } = body;
129
+ if (!bot_token || !webhook_url) {
130
+ return Response.json({ error: 'Missing bot_token or webhook_url' }, { status: 400 });
131
+ }
132
+
133
+ try {
134
+ const result = await setWebhook(bot_token, webhook_url, process.env.TELEGRAM_WEBHOOK_SECRET);
135
+ telegramBotToken = bot_token;
136
+ return Response.json({ success: true, result });
137
+ } catch (err) {
138
+ console.error(err);
139
+ return Response.json({ error: 'Failed to register webhook' }, { status: 500 });
140
+ }
141
+ }
142
+
143
+ async function handleTelegramWebhook(request) {
144
+ const { TELEGRAM_WEBHOOK_SECRET, TELEGRAM_CHAT_ID, TELEGRAM_VERIFICATION } = process.env;
145
+ const botToken = getTelegramBotToken();
146
+
147
+ // Validate secret token if configured
148
+ // Always return 200 to prevent Telegram retry loops on mismatch
149
+ if (TELEGRAM_WEBHOOK_SECRET) {
150
+ const headerSecret = request.headers.get('x-telegram-bot-api-secret-token');
151
+ if (headerSecret !== TELEGRAM_WEBHOOK_SECRET) {
152
+ return Response.json({ ok: true });
153
+ }
154
+ }
155
+
156
+ const update = await request.json();
157
+ const message = update.message || update.edited_message;
158
+
159
+ if (message && message.chat && botToken) {
160
+ const chatId = String(message.chat.id);
161
+
162
+ let messageText = null;
163
+
164
+ if (message.text) {
165
+ messageText = message.text;
166
+ }
167
+
168
+ // Check for verification code - this works even before TELEGRAM_CHAT_ID is set
169
+ if (TELEGRAM_VERIFICATION && messageText === TELEGRAM_VERIFICATION) {
170
+ await sendMessage(botToken, chatId, `Your chat ID:\n<code>${chatId}</code>`);
171
+ return Response.json({ ok: true });
172
+ }
173
+
174
+ // Security: if no TELEGRAM_CHAT_ID configured, ignore all messages (except verification above)
175
+ if (!TELEGRAM_CHAT_ID) {
176
+ return Response.json({ ok: true });
177
+ }
178
+
179
+ // Security: only accept messages from configured chat
180
+ if (chatId !== TELEGRAM_CHAT_ID) {
181
+ return Response.json({ ok: true });
182
+ }
183
+
184
+ // Acknowledge receipt with a thumbs up (await so it completes before typing indicator starts)
185
+ await reactToMessage(botToken, chatId, message.message_id).catch(() => {});
186
+
187
+ if (message.voice) {
188
+ // Handle voice messages
189
+ if (!isWhisperEnabled()) {
190
+ await sendMessage(botToken, chatId, 'Voice messages are not supported. Please set OPENAI_API_KEY to enable transcription.');
191
+ return Response.json({ ok: true });
192
+ }
193
+
194
+ try {
195
+ const { buffer, filename } = await downloadFile(botToken, message.voice.file_id);
196
+ messageText = await transcribeAudio(buffer, filename);
197
+ } catch (err) {
198
+ console.error('Failed to transcribe voice:', err);
199
+ await sendMessage(botToken, chatId, 'Sorry, I could not transcribe your voice message.');
200
+ return Response.json({ ok: true });
201
+ }
202
+ }
203
+
204
+ if (messageText) {
205
+ // Process message asynchronously (don't block the response)
206
+ processMessage(botToken, chatId, messageText).catch(err => {
207
+ console.error('Failed to process message:', err);
208
+ });
209
+ }
210
+ }
211
+
212
+ return Response.json({ ok: true });
213
+ }
214
+
215
+ /**
216
+ * Process a Telegram message with Claude (async, non-blocking)
217
+ */
218
+ async function processMessage(botToken, chatId, messageText) {
219
+ const stopTyping = startTypingIndicator(botToken, chatId);
220
+ try {
221
+ // Get conversation history and process with Claude
222
+ const history = getHistory(chatId);
223
+ const { response, history: newHistory } = await chat(
224
+ messageText,
225
+ history,
226
+ toolDefinitions,
227
+ toolExecutors
228
+ );
229
+ updateHistory(chatId, newHistory);
230
+
231
+ // Send response (auto-splits if needed)
232
+ await sendMessage(botToken, chatId, response);
233
+ } catch (err) {
234
+ console.error('Failed to process message with Claude:', err);
235
+ await sendMessage(botToken, chatId, 'Sorry, I encountered an error processing your message.').catch(() => {});
236
+ } finally {
237
+ stopTyping();
238
+ }
239
+ }
240
+
241
+ async function handleGithubWebhook(request) {
242
+ const { GH_WEBHOOK_SECRET, TELEGRAM_CHAT_ID } = process.env;
243
+ const botToken = getTelegramBotToken();
244
+
245
+ // Validate webhook secret
246
+ if (GH_WEBHOOK_SECRET) {
247
+ const headerSecret = request.headers.get('x-github-webhook-secret-token');
248
+ if (headerSecret !== GH_WEBHOOK_SECRET) {
249
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
250
+ }
251
+ }
252
+
253
+ const event = request.headers.get('x-github-event');
254
+ const payload = await request.json();
255
+
256
+ if (event !== 'pull_request') {
257
+ return Response.json({ ok: true, skipped: true });
258
+ }
259
+
260
+ const pr = payload.pull_request;
261
+ if (!pr) return Response.json({ ok: true, skipped: true });
262
+
263
+ const branchName = pr.head?.ref;
264
+ const jobId = extractJobId(branchName);
265
+ if (!jobId) return Response.json({ ok: true, skipped: true, reason: 'not a job branch' });
266
+
267
+ if (!TELEGRAM_CHAT_ID || !botToken) {
268
+ console.log(`Job ${jobId} completed but no chat ID to notify`);
269
+ return Response.json({ ok: true, skipped: true, reason: 'no chat to notify' });
270
+ }
271
+
272
+ try {
273
+ // All job data comes from the webhook payload — no GitHub API calls needed
274
+ const results = payload.job_results || {};
275
+ results.pr_url = pr.html_url;
276
+
277
+ const message = await summarizeJob(results);
278
+
279
+ await sendMessage(botToken, TELEGRAM_CHAT_ID, message);
280
+
281
+ // Add the summary to chat memory so Claude has context in future conversations
282
+ const history = getHistory(TELEGRAM_CHAT_ID);
283
+ history.push({ role: 'assistant', content: message });
284
+ updateHistory(TELEGRAM_CHAT_ID, history);
285
+
286
+ console.log(`Notified chat ${TELEGRAM_CHAT_ID} about job ${jobId.slice(0, 8)}`);
287
+
288
+ return Response.json({ ok: true, notified: true });
289
+ } catch (err) {
290
+ console.error('Failed to process GitHub webhook:', err);
291
+ return Response.json({ error: 'Failed to process webhook' }, { status: 500 });
292
+ }
293
+ }
294
+
295
+ async function handleJobStatus(request) {
296
+ try {
297
+ const url = new URL(request.url);
298
+ const jobId = url.searchParams.get('job_id');
299
+ const result = await getJobStatus(jobId);
300
+ return Response.json(result);
301
+ } catch (err) {
302
+ console.error('Failed to get job status:', err);
303
+ return Response.json({ error: 'Failed to get job status' }, { status: 500 });
304
+ }
305
+ }
306
+
307
+ // ─────────────────────────────────────────────────────────────────────────────
308
+ // Next.js Route Handlers (catch-all)
309
+ // ─────────────────────────────────────────────────────────────────────────────
310
+
311
+ async function POST(request) {
312
+ const url = new URL(request.url);
313
+ const routePath = url.pathname.replace(/^\/api/, '');
314
+
315
+ // Auth check
316
+ const authError = checkAuth(routePath, request);
317
+ if (authError) return authError;
318
+
319
+ // Fire triggers (non-blocking)
320
+ try {
321
+ const fireTriggers = getFireTriggers();
322
+ // Clone request to read body for triggers without consuming it for the handler
323
+ const clonedRequest = request.clone();
324
+ const body = await clonedRequest.json().catch(() => ({}));
325
+ const query = Object.fromEntries(url.searchParams);
326
+ const headers = Object.fromEntries(request.headers);
327
+ fireTriggers(routePath, body, query, headers);
328
+ } catch (e) {
329
+ // Trigger errors are non-fatal
330
+ }
331
+
332
+ // Route to handler
333
+ switch (routePath) {
334
+ case '/webhook': return handleWebhook(request);
335
+ case '/telegram/webhook': return handleTelegramWebhook(request);
336
+ case '/telegram/register': return handleTelegramRegister(request);
337
+ case '/github/webhook': return handleGithubWebhook(request);
338
+ default: return Response.json({ error: 'Not found' }, { status: 404 });
339
+ }
340
+ }
341
+
342
+ async function GET(request) {
343
+ const url = new URL(request.url);
344
+ const routePath = url.pathname.replace(/^\/api/, '');
345
+
346
+ // Auth check
347
+ const authError = checkAuth(routePath, request);
348
+ if (authError) return authError;
349
+
350
+ switch (routePath) {
351
+ case '/ping': return Response.json({ message: 'Pong!' });
352
+ case '/jobs/status': return handleJobStatus(request);
353
+ default: return Response.json({ error: 'Not found' }, { status: 404 });
354
+ }
355
+ }
356
+
357
+ module.exports = { GET, POST };