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.
Files changed (143) hide show
  1. package/README.md +1 -1
  2. package/api/index.js +72 -165
  3. package/bin/cli.js +36 -6
  4. package/bin/local.sh +31 -0
  5. package/bin/postinstall.js +6 -2
  6. package/config/index.js +2 -11
  7. package/config/instrumentation.js +17 -5
  8. package/lib/actions.js +7 -6
  9. package/lib/ai/agent.js +36 -0
  10. package/lib/ai/index.js +274 -0
  11. package/lib/ai/model.js +67 -0
  12. package/lib/ai/tools.js +49 -0
  13. package/lib/auth/actions.js +28 -0
  14. package/lib/auth/config.js +45 -0
  15. package/lib/auth/index.js +27 -0
  16. package/lib/auth/middleware.js +30 -0
  17. package/lib/channels/base.js +56 -0
  18. package/lib/channels/index.js +15 -0
  19. package/lib/channels/telegram.js +146 -0
  20. package/lib/chat/actions.js +239 -0
  21. package/lib/chat/api.js +103 -0
  22. package/lib/chat/components/app-sidebar.js +161 -0
  23. package/lib/chat/components/app-sidebar.jsx +214 -0
  24. package/lib/chat/components/chat-header.js +9 -0
  25. package/lib/chat/components/chat-header.jsx +14 -0
  26. package/lib/chat/components/chat-input.js +230 -0
  27. package/lib/chat/components/chat-input.jsx +232 -0
  28. package/lib/chat/components/chat-nav-context.js +11 -0
  29. package/lib/chat/components/chat-nav-context.jsx +11 -0
  30. package/lib/chat/components/chat-page.js +70 -0
  31. package/lib/chat/components/chat-page.jsx +89 -0
  32. package/lib/chat/components/chat.js +78 -0
  33. package/lib/chat/components/chat.jsx +91 -0
  34. package/lib/chat/components/chats-page.js +170 -0
  35. package/lib/chat/components/chats-page.jsx +203 -0
  36. package/lib/chat/components/crons-page.js +144 -0
  37. package/lib/chat/components/crons-page.jsx +204 -0
  38. package/lib/chat/components/greeting.js +11 -0
  39. package/lib/chat/components/greeting.jsx +14 -0
  40. package/lib/chat/components/icons.js +518 -0
  41. package/lib/chat/components/icons.jsx +482 -0
  42. package/lib/chat/components/index.js +19 -0
  43. package/lib/chat/components/message.js +66 -0
  44. package/lib/chat/components/message.jsx +92 -0
  45. package/lib/chat/components/messages.js +63 -0
  46. package/lib/chat/components/messages.jsx +72 -0
  47. package/lib/chat/components/notifications-page.js +54 -0
  48. package/lib/chat/components/notifications-page.jsx +83 -0
  49. package/lib/chat/components/page-layout.js +21 -0
  50. package/lib/chat/components/page-layout.jsx +28 -0
  51. package/lib/chat/components/settings-layout.js +37 -0
  52. package/lib/chat/components/settings-layout.jsx +51 -0
  53. package/lib/chat/components/settings-secrets-page.js +216 -0
  54. package/lib/chat/components/settings-secrets-page.jsx +264 -0
  55. package/lib/chat/components/sidebar-history-item.js +54 -0
  56. package/lib/chat/components/sidebar-history-item.jsx +50 -0
  57. package/lib/chat/components/sidebar-history.js +92 -0
  58. package/lib/chat/components/sidebar-history.jsx +132 -0
  59. package/lib/chat/components/sidebar-user-nav.js +59 -0
  60. package/lib/chat/components/sidebar-user-nav.jsx +69 -0
  61. package/lib/chat/components/swarm-page.js +250 -0
  62. package/lib/chat/components/swarm-page.jsx +356 -0
  63. package/lib/chat/components/triggers-page.js +121 -0
  64. package/lib/chat/components/triggers-page.jsx +177 -0
  65. package/lib/chat/components/ui/dropdown-menu.js +98 -0
  66. package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
  67. package/lib/chat/components/ui/scroll-area.js +13 -0
  68. package/lib/chat/components/ui/scroll-area.jsx +17 -0
  69. package/lib/chat/components/ui/separator.js +21 -0
  70. package/lib/chat/components/ui/separator.jsx +18 -0
  71. package/lib/chat/components/ui/sheet.js +75 -0
  72. package/lib/chat/components/ui/sheet.jsx +95 -0
  73. package/lib/chat/components/ui/sidebar.js +227 -0
  74. package/lib/chat/components/ui/sidebar.jsx +245 -0
  75. package/lib/chat/components/ui/tooltip.js +56 -0
  76. package/lib/chat/components/ui/tooltip.jsx +66 -0
  77. package/lib/chat/utils.js +11 -0
  78. package/lib/cron.js +7 -8
  79. package/lib/db/api-keys.js +160 -0
  80. package/lib/db/chats.js +129 -0
  81. package/lib/db/index.js +106 -0
  82. package/lib/db/notifications.js +99 -0
  83. package/lib/db/schema.js +51 -0
  84. package/lib/db/users.js +89 -0
  85. package/lib/paths.js +23 -17
  86. package/lib/tools/create-job.js +3 -3
  87. package/lib/tools/github.js +145 -1
  88. package/lib/tools/openai.js +1 -1
  89. package/lib/tools/telegram.js +4 -3
  90. package/lib/triggers.js +6 -7
  91. package/lib/utils/render-md.js +6 -6
  92. package/package.json +43 -6
  93. package/setup/lib/auth.mjs +22 -9
  94. package/setup/lib/prerequisites.mjs +10 -3
  95. package/setup/lib/telegram-verify.mjs +3 -16
  96. package/setup/setup-telegram.mjs +31 -62
  97. package/setup/setup.mjs +58 -98
  98. package/templates/.dockerignore +5 -0
  99. package/templates/.env.example +18 -2
  100. package/templates/.github/workflows/auto-merge.yml +1 -1
  101. package/templates/.github/workflows/build-image.yml +6 -4
  102. package/templates/.github/workflows/notify-job-failed.yml +2 -2
  103. package/templates/.github/workflows/notify-pr-complete.yml +2 -2
  104. package/templates/.github/workflows/run-job.yml +24 -10
  105. package/templates/CLAUDE.md +5 -3
  106. package/templates/app/api/auth/[...nextauth]/route.js +1 -0
  107. package/templates/app/api/chat/route.js +1 -0
  108. package/templates/app/chat/[chatId]/page.js +8 -0
  109. package/templates/app/chats/page.js +7 -0
  110. package/templates/app/components/ascii-logo.jsx +10 -0
  111. package/templates/app/components/login-form.jsx +81 -0
  112. package/templates/app/components/setup-form.jsx +82 -0
  113. package/templates/app/components/theme-provider.jsx +11 -0
  114. package/templates/app/components/theme-toggle.jsx +38 -0
  115. package/templates/app/components/ui/button.jsx +21 -0
  116. package/templates/app/components/ui/card.jsx +23 -0
  117. package/templates/app/components/ui/input.jsx +10 -0
  118. package/templates/app/components/ui/label.jsx +10 -0
  119. package/templates/app/crons/page.js +7 -0
  120. package/templates/app/globals.css +66 -0
  121. package/templates/app/layout.js +9 -2
  122. package/templates/app/login/page.js +15 -0
  123. package/templates/app/notifications/page.js +7 -0
  124. package/templates/app/page.js +6 -30
  125. package/templates/app/settings/layout.js +7 -0
  126. package/templates/app/settings/page.js +5 -0
  127. package/templates/app/settings/secrets/page.js +5 -0
  128. package/templates/app/swarm/page.js +7 -0
  129. package/templates/app/triggers/page.js +7 -0
  130. package/templates/config/CRONS.json +2 -2
  131. package/templates/config/TRIGGERS.json +2 -2
  132. package/templates/docker/event_handler/Dockerfile +19 -0
  133. package/templates/docker/{entrypoint.sh → job/entrypoint.sh} +4 -4
  134. package/templates/docker/runner/Dockerfile +38 -0
  135. package/templates/docker/runner/entrypoint.sh +41 -0
  136. package/templates/docker-compose.yml +52 -0
  137. package/templates/instrumentation.js +6 -1
  138. package/templates/middleware.js +1 -0
  139. package/templates/postcss.config.mjs +5 -0
  140. package/lib/claude/conversation.js +0 -76
  141. package/lib/claude/index.js +0 -142
  142. package/lib/claude/tools.js +0 -54
  143. /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/webhook` with your API key to create jobs programmatically.
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
- 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');
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
- * Check API key authentication
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 if unauthorized, null if OK
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 !== process.env.API_KEY) {
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
- // Validate secret token if configured
149
- // Always return 200 to prevent Telegram retry loops on mismatch
150
- if (TELEGRAM_WEBHOOK_SECRET) {
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
- if (message && message.chat && botToken) {
161
- const chatId = String(message.chat.id);
162
-
163
- let messageText = null;
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 Telegram message with Claude (async, non-blocking)
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 processMessage(botToken, chatId, messageText) {
220
- const stopTyping = startTypingIndicator(botToken, chatId);
135
+ async function processChannelMessage(adapter, normalized) {
136
+ await adapter.acknowledge(normalized.metadata);
137
+ const stopIndicator = adapter.startProcessingIndicator(normalized.metadata);
138
+
221
139
  try {
222
- // Get conversation history and process with Claude
223
- const history = getHistory(chatId);
224
- const { response, history: newHistory } = await chat(
225
- messageText,
226
- history,
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
- updateHistory(chatId, newHistory);
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 Claude:', err);
236
- await sendMessage(botToken, chatId, 'Sorry, I encountered an error processing your message.').catch(() => {});
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
- stopTyping();
157
+ stopIndicator();
239
158
  }
240
159
  }
241
160
 
242
161
  async function handleGithubWebhook(request) {
243
- const { GH_WEBHOOK_SECRET, TELEGRAM_CHAT_ID } = process.env;
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 !== GH_WEBHOOK_SECRET) {
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
- await sendMessage(botToken, TELEGRAM_CHAT_ID, message);
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 '/webhook': return handleWebhook(request);
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': return Response.json({ message: 'Pong!' });
350
- case '/jobs/status': return handleJobStatus(request);
351
- default: return Response.json({ error: 'Not found' }, { status: 404 });
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
- module.exports = { GET, POST };
262
+ export { GET, POST };
package/bin/cli.js CHANGED
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync } = require('child_process');
4
- const fs = require('fs');
5
- const path = require('path');
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: '^16.0.0',
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
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
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
- 'thepopebot',
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
- require('dotenv').config();
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 } = require('../lib/cron');
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
- const { exec } = require('child_process');
2
- const { promisify } = require('util');
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 === 'http') {
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
- module.exports = { executeAction };
41
+ export { executeAction };
@@ -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
+ }