thepopebot 1.1.2 → 1.2.3
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.
- package/README.md +1 -1
- package/api/index.js +72 -165
- package/bin/cli.js +36 -6
- package/bin/local.sh +31 -0
- package/bin/postinstall.js +6 -2
- package/config/index.js +2 -11
- package/config/instrumentation.js +17 -5
- package/lib/actions.js +7 -6
- package/lib/ai/agent.js +36 -0
- package/lib/ai/index.js +274 -0
- package/lib/ai/model.js +67 -0
- package/lib/ai/tools.js +49 -0
- package/lib/auth/actions.js +28 -0
- package/lib/auth/config.js +45 -0
- package/lib/auth/index.js +27 -0
- package/lib/auth/middleware.js +30 -0
- package/lib/channels/base.js +56 -0
- package/lib/channels/index.js +15 -0
- package/lib/channels/telegram.js +146 -0
- package/lib/chat/actions.js +239 -0
- package/lib/chat/api.js +103 -0
- package/lib/chat/components/app-sidebar.js +161 -0
- package/lib/chat/components/app-sidebar.jsx +214 -0
- package/lib/chat/components/chat-header.js +9 -0
- package/lib/chat/components/chat-header.jsx +14 -0
- package/lib/chat/components/chat-input.js +230 -0
- package/lib/chat/components/chat-input.jsx +232 -0
- package/lib/chat/components/chat-nav-context.js +11 -0
- package/lib/chat/components/chat-nav-context.jsx +11 -0
- package/lib/chat/components/chat-page.js +70 -0
- package/lib/chat/components/chat-page.jsx +89 -0
- package/lib/chat/components/chat.js +78 -0
- package/lib/chat/components/chat.jsx +91 -0
- package/lib/chat/components/chats-page.js +170 -0
- package/lib/chat/components/chats-page.jsx +203 -0
- package/lib/chat/components/crons-page.js +144 -0
- package/lib/chat/components/crons-page.jsx +204 -0
- package/lib/chat/components/greeting.js +11 -0
- package/lib/chat/components/greeting.jsx +14 -0
- package/lib/chat/components/icons.js +518 -0
- package/lib/chat/components/icons.jsx +482 -0
- package/lib/chat/components/index.js +19 -0
- package/lib/chat/components/message.js +66 -0
- package/lib/chat/components/message.jsx +92 -0
- package/lib/chat/components/messages.js +63 -0
- package/lib/chat/components/messages.jsx +72 -0
- package/lib/chat/components/notifications-page.js +54 -0
- package/lib/chat/components/notifications-page.jsx +83 -0
- package/lib/chat/components/page-layout.js +21 -0
- package/lib/chat/components/page-layout.jsx +28 -0
- package/lib/chat/components/settings-layout.js +37 -0
- package/lib/chat/components/settings-layout.jsx +51 -0
- package/lib/chat/components/settings-secrets-page.js +216 -0
- package/lib/chat/components/settings-secrets-page.jsx +264 -0
- package/lib/chat/components/sidebar-history-item.js +54 -0
- package/lib/chat/components/sidebar-history-item.jsx +50 -0
- package/lib/chat/components/sidebar-history.js +92 -0
- package/lib/chat/components/sidebar-history.jsx +132 -0
- package/lib/chat/components/sidebar-user-nav.js +59 -0
- package/lib/chat/components/sidebar-user-nav.jsx +69 -0
- package/lib/chat/components/swarm-page.js +250 -0
- package/lib/chat/components/swarm-page.jsx +356 -0
- package/lib/chat/components/triggers-page.js +121 -0
- package/lib/chat/components/triggers-page.jsx +177 -0
- package/lib/chat/components/ui/dropdown-menu.js +98 -0
- package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
- package/lib/chat/components/ui/scroll-area.js +13 -0
- package/lib/chat/components/ui/scroll-area.jsx +17 -0
- package/lib/chat/components/ui/separator.js +21 -0
- package/lib/chat/components/ui/separator.jsx +18 -0
- package/lib/chat/components/ui/sheet.js +75 -0
- package/lib/chat/components/ui/sheet.jsx +95 -0
- package/lib/chat/components/ui/sidebar.js +227 -0
- package/lib/chat/components/ui/sidebar.jsx +245 -0
- package/lib/chat/components/ui/tooltip.js +56 -0
- package/lib/chat/components/ui/tooltip.jsx +66 -0
- package/lib/chat/utils.js +11 -0
- package/lib/cron.js +7 -8
- package/lib/db/api-keys.js +160 -0
- package/lib/db/chats.js +129 -0
- package/lib/db/index.js +106 -0
- package/lib/db/notifications.js +99 -0
- package/lib/db/schema.js +51 -0
- package/lib/db/users.js +89 -0
- package/lib/paths.js +23 -17
- package/lib/tools/create-job.js +3 -3
- package/lib/tools/github.js +145 -1
- package/lib/tools/openai.js +1 -1
- package/lib/tools/telegram.js +4 -3
- package/lib/triggers.js +6 -7
- package/lib/utils/render-md.js +6 -6
- package/package.json +43 -6
- package/setup/lib/auth.mjs +22 -9
- package/setup/lib/prerequisites.mjs +10 -3
- package/setup/lib/telegram-verify.mjs +3 -16
- package/setup/setup-telegram.mjs +31 -62
- package/setup/setup.mjs +58 -98
- package/templates/.dockerignore +5 -0
- package/templates/.env.example +18 -2
- package/templates/.github/workflows/auto-merge.yml +1 -1
- package/templates/.github/workflows/build-image.yml +6 -4
- package/templates/.github/workflows/notify-job-failed.yml +2 -2
- package/templates/.github/workflows/notify-pr-complete.yml +2 -2
- package/templates/.github/workflows/run-job.yml +24 -10
- package/templates/CLAUDE.md +5 -3
- package/templates/app/api/auth/[...nextauth]/route.js +1 -0
- package/templates/app/api/chat/route.js +1 -0
- package/templates/app/chat/[chatId]/page.js +8 -0
- package/templates/app/chats/page.js +7 -0
- package/templates/app/components/ascii-logo.jsx +10 -0
- package/templates/app/components/login-form.jsx +81 -0
- package/templates/app/components/setup-form.jsx +82 -0
- package/templates/app/components/theme-provider.jsx +11 -0
- package/templates/app/components/theme-toggle.jsx +38 -0
- package/templates/app/components/ui/button.jsx +21 -0
- package/templates/app/components/ui/card.jsx +23 -0
- package/templates/app/components/ui/input.jsx +10 -0
- package/templates/app/components/ui/label.jsx +10 -0
- package/templates/app/crons/page.js +7 -0
- package/templates/app/globals.css +66 -0
- package/templates/app/layout.js +9 -2
- package/templates/app/login/page.js +15 -0
- package/templates/app/notifications/page.js +7 -0
- package/templates/app/page.js +6 -30
- package/templates/app/settings/layout.js +7 -0
- package/templates/app/settings/page.js +5 -0
- package/templates/app/settings/secrets/page.js +5 -0
- package/templates/app/swarm/page.js +7 -0
- package/templates/app/triggers/page.js +7 -0
- package/templates/config/CRONS.json +2 -2
- package/templates/config/TRIGGERS.json +2 -2
- package/templates/docker/event_handler/Dockerfile +19 -0
- package/templates/docker/{entrypoint.sh → job/entrypoint.sh} +4 -4
- package/templates/docker/runner/Dockerfile +38 -0
- package/templates/docker/runner/entrypoint.sh +41 -0
- package/templates/docker-compose.yml +52 -0
- package/templates/instrumentation.js +6 -1
- package/templates/middleware.js +1 -0
- package/templates/postcss.config.mjs +5 -0
- package/lib/claude/conversation.js +0 -76
- package/lib/claude/index.js +0 -142
- package/lib/claude/tools.js +0 -54
- /package/templates/docker/{Dockerfile → job/Dockerfile} +0 -0
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ The wizard walks you through everything:
|
|
|
111
111
|
**Step 3** — Start using your agent:
|
|
112
112
|
|
|
113
113
|
- **Telegram**: Message your bot to create jobs conversationally. Ask it to do tasks, check job status, or just chat.
|
|
114
|
-
- **Webhook**: Send a POST to `/api/
|
|
114
|
+
- **Webhook**: Send a POST to `/api/create-job` with your API key to create jobs programmatically.
|
|
115
115
|
- **Cron**: Edit `config/CRONS.json` to schedule recurring jobs.
|
|
116
116
|
|
|
117
117
|
---
|
package/api/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { createHash, timingSafeEqual } from 'crypto';
|
|
2
|
+
import { createJob } from '../lib/tools/create-job.js';
|
|
3
|
+
import { setWebhook } from '../lib/tools/telegram.js';
|
|
4
|
+
import { getJobStatus } from '../lib/tools/github.js';
|
|
5
|
+
import { getTelegramAdapter } from '../lib/channels/index.js';
|
|
6
|
+
import { chat, summarizeJob } from '../lib/ai/index.js';
|
|
7
|
+
import { createNotification } from '../lib/db/notifications.js';
|
|
8
|
+
import { loadTriggers } from '../lib/triggers.js';
|
|
9
|
+
import { verifyApiKey } from '../lib/db/api-keys.js';
|
|
10
10
|
|
|
11
11
|
// Bot token from env, can be overridden by /telegram/register
|
|
12
12
|
let telegramBotToken = null;
|
|
@@ -23,7 +23,6 @@ function getTelegramBotToken() {
|
|
|
23
23
|
|
|
24
24
|
function getFireTriggers() {
|
|
25
25
|
if (!_fireTriggers) {
|
|
26
|
-
const { loadTriggers } = require('../lib/triggers');
|
|
27
26
|
const result = loadTriggers();
|
|
28
27
|
_fireTriggers = result.fireTriggers;
|
|
29
28
|
}
|
|
@@ -34,18 +33,39 @@ function getFireTriggers() {
|
|
|
34
33
|
const PUBLIC_ROUTES = ['/telegram/webhook', '/github/webhook'];
|
|
35
34
|
|
|
36
35
|
/**
|
|
37
|
-
*
|
|
36
|
+
* Timing-safe string comparison.
|
|
37
|
+
* @param {string} a
|
|
38
|
+
* @param {string} b
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
function safeCompare(a, b) {
|
|
42
|
+
if (!a || !b) return false;
|
|
43
|
+
const bufA = Buffer.from(a);
|
|
44
|
+
const bufB = Buffer.from(b);
|
|
45
|
+
if (bufA.length !== bufB.length) return false;
|
|
46
|
+
return timingSafeEqual(bufA, bufB);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Centralized auth gate for all API routes.
|
|
51
|
+
* Public routes pass through; everything else requires a valid API key from the database.
|
|
38
52
|
* @param {string} routePath - The route path
|
|
39
53
|
* @param {Request} request - The incoming request
|
|
40
|
-
* @returns {Response|null} - Error response
|
|
54
|
+
* @returns {Response|null} - Error response or null if authorized
|
|
41
55
|
*/
|
|
42
56
|
function checkAuth(routePath, request) {
|
|
43
57
|
if (PUBLIC_ROUTES.includes(routePath)) return null;
|
|
44
58
|
|
|
45
59
|
const apiKey = request.headers.get('x-api-key');
|
|
46
|
-
if (apiKey
|
|
60
|
+
if (!apiKey) {
|
|
61
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const record = verifyApiKey(apiKey);
|
|
65
|
+
if (!record) {
|
|
47
66
|
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
48
67
|
}
|
|
68
|
+
|
|
49
69
|
return null;
|
|
50
70
|
}
|
|
51
71
|
|
|
@@ -57,55 +77,6 @@ function extractJobId(branchName) {
|
|
|
57
77
|
return branchName.slice(4);
|
|
58
78
|
}
|
|
59
79
|
|
|
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.status ? `## Status\n${results.status}` : '',
|
|
78
|
-
results.merge_result ? `## Merge Result\n${results.merge_result}` : '',
|
|
79
|
-
results.pr_url ? `## PR URL\n${results.pr_url}` : '',
|
|
80
|
-
results.run_url ? `## Run URL\n${results.run_url}` : '',
|
|
81
|
-
results.log ? `## Agent Log\n${results.log}` : '',
|
|
82
|
-
].filter(Boolean).join('\n\n');
|
|
83
|
-
|
|
84
|
-
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
85
|
-
method: 'POST',
|
|
86
|
-
headers: {
|
|
87
|
-
'Content-Type': 'application/json',
|
|
88
|
-
'x-api-key': apiKey,
|
|
89
|
-
'anthropic-version': '2023-06-01',
|
|
90
|
-
},
|
|
91
|
-
body: JSON.stringify({
|
|
92
|
-
model: process.env.EVENT_HANDLER_MODEL || 'claude-sonnet-4-20250514',
|
|
93
|
-
max_tokens: 1024,
|
|
94
|
-
system: systemPrompt,
|
|
95
|
-
messages: [{ role: 'user', content: userMessage }],
|
|
96
|
-
}),
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
if (!response.ok) throw new Error(`Claude API error: ${response.status}`);
|
|
100
|
-
|
|
101
|
-
const result = await response.json();
|
|
102
|
-
return (result.content?.[0]?.text || '').trim() || 'Job finished.';
|
|
103
|
-
} catch (err) {
|
|
104
|
-
console.error('Failed to summarize job:', err);
|
|
105
|
-
return 'Job finished.';
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
80
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
110
81
|
// Route handlers
|
|
111
82
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -142,111 +113,58 @@ async function handleTelegramRegister(request) {
|
|
|
142
113
|
}
|
|
143
114
|
|
|
144
115
|
async function handleTelegramWebhook(request) {
|
|
145
|
-
const { TELEGRAM_WEBHOOK_SECRET, TELEGRAM_CHAT_ID, TELEGRAM_VERIFICATION } = process.env;
|
|
146
116
|
const botToken = getTelegramBotToken();
|
|
117
|
+
if (!botToken) return Response.json({ ok: true });
|
|
147
118
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
const headerSecret = request.headers.get('x-telegram-bot-api-secret-token');
|
|
152
|
-
if (headerSecret !== TELEGRAM_WEBHOOK_SECRET) {
|
|
153
|
-
return Response.json({ ok: true });
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const update = await request.json();
|
|
158
|
-
const message = update.message || update.edited_message;
|
|
119
|
+
const adapter = getTelegramAdapter(botToken);
|
|
120
|
+
const normalized = await adapter.receive(request);
|
|
121
|
+
if (!normalized) return Response.json({ ok: true });
|
|
159
122
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (message.text) {
|
|
166
|
-
messageText = message.text;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Check for verification code - this works even before TELEGRAM_CHAT_ID is set
|
|
170
|
-
if (TELEGRAM_VERIFICATION && messageText === TELEGRAM_VERIFICATION) {
|
|
171
|
-
await sendMessage(botToken, chatId, `Your chat ID:\n<code>${chatId}</code>`);
|
|
172
|
-
return Response.json({ ok: true });
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Security: if no TELEGRAM_CHAT_ID configured, ignore all messages (except verification above)
|
|
176
|
-
if (!TELEGRAM_CHAT_ID) {
|
|
177
|
-
return Response.json({ ok: true });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Security: only accept messages from configured chat
|
|
181
|
-
if (chatId !== TELEGRAM_CHAT_ID) {
|
|
182
|
-
return Response.json({ ok: true });
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Acknowledge receipt with a thumbs up (await so it completes before typing indicator starts)
|
|
186
|
-
await reactToMessage(botToken, chatId, message.message_id).catch(() => {});
|
|
187
|
-
|
|
188
|
-
if (message.voice) {
|
|
189
|
-
// Handle voice messages
|
|
190
|
-
if (!isWhisperEnabled()) {
|
|
191
|
-
await sendMessage(botToken, chatId, 'Voice messages are not supported. Please set OPENAI_API_KEY to enable transcription.');
|
|
192
|
-
return Response.json({ ok: true });
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
const { buffer, filename } = await downloadFile(botToken, message.voice.file_id);
|
|
197
|
-
messageText = await transcribeAudio(buffer, filename);
|
|
198
|
-
} catch (err) {
|
|
199
|
-
console.error('Failed to transcribe voice:', err);
|
|
200
|
-
await sendMessage(botToken, chatId, 'Sorry, I could not transcribe your voice message.');
|
|
201
|
-
return Response.json({ ok: true });
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (messageText) {
|
|
206
|
-
// Process message asynchronously (don't block the response)
|
|
207
|
-
processMessage(botToken, chatId, messageText).catch(err => {
|
|
208
|
-
console.error('Failed to process message:', err);
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
123
|
+
// Process message asynchronously (don't block the webhook response)
|
|
124
|
+
processChannelMessage(adapter, normalized).catch((err) => {
|
|
125
|
+
console.error('Failed to process message:', err);
|
|
126
|
+
});
|
|
212
127
|
|
|
213
128
|
return Response.json({ ok: true });
|
|
214
129
|
}
|
|
215
130
|
|
|
216
131
|
/**
|
|
217
|
-
* Process a
|
|
132
|
+
* Process a normalized message through the AI layer with channel UX.
|
|
133
|
+
* Message persistence is handled centrally by the AI layer.
|
|
218
134
|
*/
|
|
219
|
-
async function
|
|
220
|
-
|
|
135
|
+
async function processChannelMessage(adapter, normalized) {
|
|
136
|
+
await adapter.acknowledge(normalized.metadata);
|
|
137
|
+
const stopIndicator = adapter.startProcessingIndicator(normalized.metadata);
|
|
138
|
+
|
|
221
139
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
toolDefinitions,
|
|
228
|
-
toolExecutors
|
|
140
|
+
const response = await chat(
|
|
141
|
+
normalized.threadId,
|
|
142
|
+
normalized.text,
|
|
143
|
+
normalized.attachments,
|
|
144
|
+
{ userId: 'telegram', chatTitle: 'Telegram' }
|
|
229
145
|
);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// Send response (auto-splits if needed)
|
|
233
|
-
await sendMessage(botToken, chatId, response);
|
|
146
|
+
await adapter.sendResponse(normalized.threadId, response, normalized.metadata);
|
|
234
147
|
} catch (err) {
|
|
235
|
-
console.error('Failed to process message with
|
|
236
|
-
await
|
|
148
|
+
console.error('Failed to process message with AI:', err);
|
|
149
|
+
await adapter
|
|
150
|
+
.sendResponse(
|
|
151
|
+
normalized.threadId,
|
|
152
|
+
'Sorry, I encountered an error processing your message.',
|
|
153
|
+
normalized.metadata
|
|
154
|
+
)
|
|
155
|
+
.catch(() => {});
|
|
237
156
|
} finally {
|
|
238
|
-
|
|
157
|
+
stopIndicator();
|
|
239
158
|
}
|
|
240
159
|
}
|
|
241
160
|
|
|
242
161
|
async function handleGithubWebhook(request) {
|
|
243
|
-
const { GH_WEBHOOK_SECRET
|
|
244
|
-
const botToken = getTelegramBotToken();
|
|
162
|
+
const { GH_WEBHOOK_SECRET } = process.env;
|
|
245
163
|
|
|
246
|
-
// Validate webhook secret
|
|
164
|
+
// Validate webhook secret (timing-safe)
|
|
247
165
|
if (GH_WEBHOOK_SECRET) {
|
|
248
166
|
const headerSecret = request.headers.get('x-github-webhook-secret-token');
|
|
249
|
-
if (headerSecret
|
|
167
|
+
if (!safeCompare(headerSecret, GH_WEBHOOK_SECRET)) {
|
|
250
168
|
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
251
169
|
}
|
|
252
170
|
}
|
|
@@ -255,11 +173,6 @@ async function handleGithubWebhook(request) {
|
|
|
255
173
|
const jobId = payload.job_id || extractJobId(payload.branch);
|
|
256
174
|
if (!jobId) return Response.json({ ok: true, skipped: true, reason: 'not a job' });
|
|
257
175
|
|
|
258
|
-
if (!TELEGRAM_CHAT_ID || !botToken) {
|
|
259
|
-
console.log(`Job ${jobId} completed but no chat ID to notify`);
|
|
260
|
-
return Response.json({ ok: true, skipped: true, reason: 'no chat to notify' });
|
|
261
|
-
}
|
|
262
|
-
|
|
263
176
|
try {
|
|
264
177
|
const results = {
|
|
265
178
|
job: payload.job || '',
|
|
@@ -273,15 +186,9 @@ async function handleGithubWebhook(request) {
|
|
|
273
186
|
};
|
|
274
187
|
|
|
275
188
|
const message = await summarizeJob(results);
|
|
189
|
+
await createNotification(message, payload);
|
|
276
190
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
// Add the summary to chat memory so Claude has context in future conversations
|
|
280
|
-
const history = getHistory(TELEGRAM_CHAT_ID);
|
|
281
|
-
history.push({ role: 'assistant', content: message });
|
|
282
|
-
updateHistory(TELEGRAM_CHAT_ID, history);
|
|
283
|
-
|
|
284
|
-
console.log(`Notified chat ${TELEGRAM_CHAT_ID} about job ${jobId.slice(0, 8)}`);
|
|
191
|
+
console.log(`Notification saved for job ${jobId.slice(0, 8)}`);
|
|
285
192
|
|
|
286
193
|
return Response.json({ ok: true, notified: true });
|
|
287
194
|
} catch (err) {
|
|
@@ -329,7 +236,7 @@ async function POST(request) {
|
|
|
329
236
|
|
|
330
237
|
// Route to handler
|
|
331
238
|
switch (routePath) {
|
|
332
|
-
case '/
|
|
239
|
+
case '/create-job': return handleWebhook(request);
|
|
333
240
|
case '/telegram/webhook': return handleTelegramWebhook(request);
|
|
334
241
|
case '/telegram/register': return handleTelegramRegister(request);
|
|
335
242
|
case '/github/webhook': return handleGithubWebhook(request);
|
|
@@ -346,10 +253,10 @@ async function GET(request) {
|
|
|
346
253
|
if (authError) return authError;
|
|
347
254
|
|
|
348
255
|
switch (routePath) {
|
|
349
|
-
case '/ping':
|
|
350
|
-
case '/jobs/status':
|
|
351
|
-
default:
|
|
256
|
+
case '/ping': return Response.json({ message: 'Pong!' });
|
|
257
|
+
case '/jobs/status': return handleJobStatus(request);
|
|
258
|
+
default: return Response.json({ error: 'Not found' }, { status: 404 });
|
|
352
259
|
}
|
|
353
260
|
}
|
|
354
261
|
|
|
355
|
-
|
|
262
|
+
export { GET, POST };
|
package/bin/cli.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
6
10
|
|
|
7
11
|
const command = process.argv[2];
|
|
8
12
|
const args = process.argv.slice(3);
|
|
@@ -15,6 +19,7 @@ Commands:
|
|
|
15
19
|
init Scaffold a new thepopebot project
|
|
16
20
|
setup Run interactive setup wizard
|
|
17
21
|
setup-telegram Reconfigure Telegram webhook
|
|
22
|
+
reset-auth Regenerate AUTH_SECRET (invalidates all sessions)
|
|
18
23
|
reset [file] Restore a template file (or list available templates)
|
|
19
24
|
`);
|
|
20
25
|
}
|
|
@@ -82,17 +87,22 @@ function init() {
|
|
|
82
87
|
name: dirName,
|
|
83
88
|
private: true,
|
|
84
89
|
scripts: {
|
|
85
|
-
dev: 'next dev',
|
|
90
|
+
dev: 'next dev --turbopack',
|
|
86
91
|
build: 'next build',
|
|
87
92
|
start: 'next start',
|
|
88
93
|
setup: 'thepopebot setup',
|
|
89
94
|
'setup-telegram': 'thepopebot setup-telegram',
|
|
95
|
+
'reset-auth': 'thepopebot reset-auth',
|
|
90
96
|
},
|
|
91
97
|
dependencies: {
|
|
92
98
|
thepopebot: '^1.0.0',
|
|
93
|
-
next: '^
|
|
99
|
+
next: '^15.5.12',
|
|
100
|
+
'next-auth': '5.0.0-beta.30',
|
|
101
|
+
'next-themes': '^0.4.0',
|
|
94
102
|
react: '^19.0.0',
|
|
95
103
|
'react-dom': '^19.0.0',
|
|
104
|
+
tailwindcss: '^4.0.0',
|
|
105
|
+
'@tailwindcss/postcss': '^4.0.0',
|
|
96
106
|
},
|
|
97
107
|
};
|
|
98
108
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
@@ -102,7 +112,7 @@ function init() {
|
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
// Create .gitkeep files for empty dirs
|
|
105
|
-
const gitkeepDirs = ['cron', 'triggers', 'logs', 'tmp'];
|
|
115
|
+
const gitkeepDirs = ['cron', 'triggers', 'logs', 'tmp', 'data'];
|
|
106
116
|
for (const dir of gitkeepDirs) {
|
|
107
117
|
const gitkeep = path.join(cwd, dir, '.gitkeep');
|
|
108
118
|
if (!fs.existsSync(gitkeep)) {
|
|
@@ -261,6 +271,23 @@ function setupTelegram() {
|
|
|
261
271
|
}
|
|
262
272
|
}
|
|
263
273
|
|
|
274
|
+
async function resetAuth() {
|
|
275
|
+
const { randomBytes } = await import('crypto');
|
|
276
|
+
const { updateEnvVariable } = await import(path.join(__dirname, '..', 'setup', 'lib', 'auth.mjs'));
|
|
277
|
+
|
|
278
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
279
|
+
if (!fs.existsSync(envPath)) {
|
|
280
|
+
console.error('\n No .env file found. Run "npm run setup" first.\n');
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const newSecret = randomBytes(32).toString('base64');
|
|
285
|
+
updateEnvVariable('AUTH_SECRET', newSecret);
|
|
286
|
+
console.log('\n AUTH_SECRET regenerated.');
|
|
287
|
+
console.log(' All existing sessions have been invalidated.');
|
|
288
|
+
console.log(' Restart your server for the change to take effect.\n');
|
|
289
|
+
}
|
|
290
|
+
|
|
264
291
|
switch (command) {
|
|
265
292
|
case 'init':
|
|
266
293
|
init();
|
|
@@ -271,6 +298,9 @@ switch (command) {
|
|
|
271
298
|
case 'setup-telegram':
|
|
272
299
|
setupTelegram();
|
|
273
300
|
break;
|
|
301
|
+
case 'reset-auth':
|
|
302
|
+
await resetAuth();
|
|
303
|
+
break;
|
|
274
304
|
case 'reset':
|
|
275
305
|
reset(args[0]);
|
|
276
306
|
break;
|
package/bin/local.sh
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
PACKAGE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
DEV_DIR="${1:-/tmp/thepopebot.local}"
|
|
6
|
+
ENV_BACKUP="/tmp/env.$(uuidgen)"
|
|
7
|
+
|
|
8
|
+
HAS_ENV=false
|
|
9
|
+
if [ -f "$DEV_DIR/.env" ]; then
|
|
10
|
+
mv "$DEV_DIR/.env" "$ENV_BACKUP"
|
|
11
|
+
HAS_ENV=true
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
rm -rf "$DEV_DIR"
|
|
15
|
+
mkdir -p "$DEV_DIR"
|
|
16
|
+
cd "$DEV_DIR"
|
|
17
|
+
|
|
18
|
+
node "$PACKAGE_DIR/bin/cli.js" init
|
|
19
|
+
|
|
20
|
+
sed -i '' "s|\"thepopebot\": \".*\"|\"thepopebot\": \"file:$PACKAGE_DIR\"|" package.json
|
|
21
|
+
|
|
22
|
+
rm -rf node_modules package-lock.json
|
|
23
|
+
npm install --install-links
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if [ "$HAS_ENV" = true ]; then
|
|
27
|
+
mv "$ENV_BACKUP" .env
|
|
28
|
+
echo "Restored .env from previous build"
|
|
29
|
+
else
|
|
30
|
+
npm run setup
|
|
31
|
+
fi
|
package/bin/postinstall.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
5
9
|
|
|
6
10
|
// postinstall runs from the package dir inside node_modules.
|
|
7
11
|
// The user's project root is two levels up: node_modules/thepopebot/ -> project root
|
package/config/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Next.js config wrapper for thepopebot.
|
|
5
3
|
* Enables instrumentation hook for cron scheduling on server start.
|
|
@@ -11,19 +9,12 @@ const path = require('path');
|
|
|
11
9
|
* @param {Object} nextConfig - User's Next.js config
|
|
12
10
|
* @returns {Object} Enhanced Next.js config
|
|
13
11
|
*/
|
|
14
|
-
function withThepopebot(nextConfig = {}) {
|
|
12
|
+
export function withThepopebot(nextConfig = {}) {
|
|
15
13
|
return {
|
|
16
14
|
...nextConfig,
|
|
17
|
-
// Ensure server-only packages aren't bundled for client
|
|
18
15
|
serverExternalPackages: [
|
|
19
16
|
...(nextConfig.serverExternalPackages || []),
|
|
20
|
-
'
|
|
21
|
-
'grammy',
|
|
22
|
-
'@grammyjs/parse-mode',
|
|
23
|
-
'node-cron',
|
|
24
|
-
'uuid',
|
|
17
|
+
'better-sqlite3',
|
|
25
18
|
],
|
|
26
19
|
};
|
|
27
20
|
}
|
|
28
|
-
|
|
29
|
-
module.exports = { withThepopebot };
|
|
@@ -11,19 +11,31 @@
|
|
|
11
11
|
|
|
12
12
|
let initialized = false;
|
|
13
13
|
|
|
14
|
-
async function register() {
|
|
14
|
+
export async function register() {
|
|
15
15
|
// Only run on the server, and only once
|
|
16
16
|
if (typeof window !== 'undefined' || initialized) return;
|
|
17
17
|
initialized = true;
|
|
18
18
|
|
|
19
19
|
// Load .env from project root
|
|
20
|
-
|
|
20
|
+
const dotenv = await import('dotenv');
|
|
21
|
+
dotenv.config();
|
|
22
|
+
|
|
23
|
+
// Validate AUTH_SECRET is set (required by Auth.js for session encryption)
|
|
24
|
+
if (!process.env.AUTH_SECRET) {
|
|
25
|
+
console.error('\n ERROR: AUTH_SECRET is not set in your .env file.');
|
|
26
|
+
console.error(' This is required for session encryption.');
|
|
27
|
+
console.error(' Run "npm run setup" to generate it automatically, or add manually:');
|
|
28
|
+
console.error(' openssl rand -base64 32\n');
|
|
29
|
+
throw new Error('AUTH_SECRET environment variable is required');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Initialize auth database
|
|
33
|
+
const { initDatabase } = await import('../lib/db/index.js');
|
|
34
|
+
initDatabase();
|
|
21
35
|
|
|
22
36
|
// Start cron scheduler
|
|
23
|
-
const { loadCrons } =
|
|
37
|
+
const { loadCrons } = await import('../lib/cron.js');
|
|
24
38
|
loadCrons();
|
|
25
39
|
|
|
26
40
|
console.log('thepopebot initialized');
|
|
27
41
|
}
|
|
28
|
-
|
|
29
|
-
module.exports = { register };
|
package/lib/actions.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { createJob } from './tools/create-job.js';
|
|
4
|
+
|
|
3
5
|
const execAsync = promisify(exec);
|
|
4
|
-
const { createJob } = require('./tools/create-job');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Execute a single action
|
|
8
|
-
* @param {Object} action - { type, job, command, url, method, headers, vars }
|
|
9
|
+
* @param {Object} action - { type, job, command, url, method, headers, vars } (type: agent|command|webhook)
|
|
9
10
|
* @param {Object} opts - { cwd, data }
|
|
10
11
|
* @returns {Promise<string>} Result description for logging
|
|
11
12
|
*/
|
|
@@ -17,7 +18,7 @@ async function executeAction(action, opts = {}) {
|
|
|
17
18
|
return (stdout || stderr || '').trim();
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
if (type === '
|
|
21
|
+
if (type === 'webhook') {
|
|
21
22
|
const method = (action.method || 'POST').toUpperCase();
|
|
22
23
|
const headers = { 'Content-Type': 'application/json', ...action.headers };
|
|
23
24
|
const fetchOpts = { method, headers };
|
|
@@ -37,4 +38,4 @@ async function executeAction(action, opts = {}) {
|
|
|
37
38
|
return `job ${result.job_id}`;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
export { executeAction };
|
package/lib/ai/agent.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
|
2
|
+
import { createModel } from './model.js';
|
|
3
|
+
import { createJobTool, getJobStatusTool } from './tools.js';
|
|
4
|
+
import { SqliteSaver } from '@langchain/langgraph-checkpoint-sqlite';
|
|
5
|
+
import { chatbotMd, thepopebotDb } from '../paths.js';
|
|
6
|
+
import { render_md } from '../utils/render-md.js';
|
|
7
|
+
|
|
8
|
+
let _agent = null;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get or create the LangGraph agent singleton.
|
|
12
|
+
* Uses createReactAgent which handles the tool loop automatically.
|
|
13
|
+
*/
|
|
14
|
+
export async function getAgent() {
|
|
15
|
+
if (!_agent) {
|
|
16
|
+
const model = await createModel();
|
|
17
|
+
const tools = [createJobTool, getJobStatusTool];
|
|
18
|
+
const checkpointer = SqliteSaver.fromConnString(thepopebotDb);
|
|
19
|
+
const systemPrompt = render_md(chatbotMd);
|
|
20
|
+
|
|
21
|
+
_agent = createReactAgent({
|
|
22
|
+
llm: model,
|
|
23
|
+
tools,
|
|
24
|
+
checkpointSaver: checkpointer,
|
|
25
|
+
prompt: systemPrompt,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return _agent;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reset the agent singleton (e.g., when config changes).
|
|
33
|
+
*/
|
|
34
|
+
export function resetAgent() {
|
|
35
|
+
_agent = null;
|
|
36
|
+
}
|