thepopebot 1.2.0 → 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 +50 -30
- package/bin/cli.js +36 -6
- package/bin/{dev.sh → local.sh} +2 -1
- package/bin/postinstall.js +6 -2
- package/config/index.js +1 -15
- package/config/instrumentation.js +17 -5
- package/lib/actions.js +7 -6
- package/lib/ai/agent.js +11 -13
- package/lib/ai/index.js +153 -26
- package/lib/ai/model.js +35 -12
- package/lib/ai/tools.js +5 -5
- 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 +1 -1
- package/lib/channels/index.js +2 -4
- package/lib/channels/telegram.js +5 -5
- 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 +22 -19
- 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 +33 -3
- 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/ai/memory.js +0 -39
- /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,8 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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';
|
|
6
10
|
|
|
7
11
|
// Bot token from env, can be overridden by /telegram/register
|
|
8
12
|
let telegramBotToken = null;
|
|
@@ -19,7 +23,6 @@ function getTelegramBotToken() {
|
|
|
19
23
|
|
|
20
24
|
function getFireTriggers() {
|
|
21
25
|
if (!_fireTriggers) {
|
|
22
|
-
const { loadTriggers } = require('../lib/triggers');
|
|
23
26
|
const result = loadTriggers();
|
|
24
27
|
_fireTriggers = result.fireTriggers;
|
|
25
28
|
}
|
|
@@ -30,18 +33,39 @@ function getFireTriggers() {
|
|
|
30
33
|
const PUBLIC_ROUTES = ['/telegram/webhook', '/github/webhook'];
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
|
-
*
|
|
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.
|
|
34
52
|
* @param {string} routePath - The route path
|
|
35
53
|
* @param {Request} request - The incoming request
|
|
36
|
-
* @returns {Response|null} - Error response
|
|
54
|
+
* @returns {Response|null} - Error response or null if authorized
|
|
37
55
|
*/
|
|
38
56
|
function checkAuth(routePath, request) {
|
|
39
57
|
if (PUBLIC_ROUTES.includes(routePath)) return null;
|
|
40
58
|
|
|
41
59
|
const apiKey = request.headers.get('x-api-key');
|
|
42
|
-
if (apiKey
|
|
60
|
+
if (!apiKey) {
|
|
61
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const record = verifyApiKey(apiKey);
|
|
65
|
+
if (!record) {
|
|
43
66
|
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
44
67
|
}
|
|
68
|
+
|
|
45
69
|
return null;
|
|
46
70
|
}
|
|
47
71
|
|
|
@@ -106,13 +130,19 @@ async function handleTelegramWebhook(request) {
|
|
|
106
130
|
|
|
107
131
|
/**
|
|
108
132
|
* Process a normalized message through the AI layer with channel UX.
|
|
133
|
+
* Message persistence is handled centrally by the AI layer.
|
|
109
134
|
*/
|
|
110
135
|
async function processChannelMessage(adapter, normalized) {
|
|
111
136
|
await adapter.acknowledge(normalized.metadata);
|
|
112
137
|
const stopIndicator = adapter.startProcessingIndicator(normalized.metadata);
|
|
113
138
|
|
|
114
139
|
try {
|
|
115
|
-
const response = await chat(
|
|
140
|
+
const response = await chat(
|
|
141
|
+
normalized.threadId,
|
|
142
|
+
normalized.text,
|
|
143
|
+
normalized.attachments,
|
|
144
|
+
{ userId: 'telegram', chatTitle: 'Telegram' }
|
|
145
|
+
);
|
|
116
146
|
await adapter.sendResponse(normalized.threadId, response, normalized.metadata);
|
|
117
147
|
} catch (err) {
|
|
118
148
|
console.error('Failed to process message with AI:', err);
|
|
@@ -129,13 +159,12 @@ async function processChannelMessage(adapter, normalized) {
|
|
|
129
159
|
}
|
|
130
160
|
|
|
131
161
|
async function handleGithubWebhook(request) {
|
|
132
|
-
const { GH_WEBHOOK_SECRET
|
|
133
|
-
const botToken = getTelegramBotToken();
|
|
162
|
+
const { GH_WEBHOOK_SECRET } = process.env;
|
|
134
163
|
|
|
135
|
-
// Validate webhook secret
|
|
164
|
+
// Validate webhook secret (timing-safe)
|
|
136
165
|
if (GH_WEBHOOK_SECRET) {
|
|
137
166
|
const headerSecret = request.headers.get('x-github-webhook-secret-token');
|
|
138
|
-
if (headerSecret
|
|
167
|
+
if (!safeCompare(headerSecret, GH_WEBHOOK_SECRET)) {
|
|
139
168
|
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
140
169
|
}
|
|
141
170
|
}
|
|
@@ -144,11 +173,6 @@ async function handleGithubWebhook(request) {
|
|
|
144
173
|
const jobId = payload.job_id || extractJobId(payload.branch);
|
|
145
174
|
if (!jobId) return Response.json({ ok: true, skipped: true, reason: 'not a job' });
|
|
146
175
|
|
|
147
|
-
if (!TELEGRAM_CHAT_ID || !botToken) {
|
|
148
|
-
console.log(`Job ${jobId} completed but no chat ID to notify`);
|
|
149
|
-
return Response.json({ ok: true, skipped: true, reason: 'no chat to notify' });
|
|
150
|
-
}
|
|
151
|
-
|
|
152
176
|
try {
|
|
153
177
|
const results = {
|
|
154
178
|
job: payload.job || '',
|
|
@@ -162,13 +186,9 @@ async function handleGithubWebhook(request) {
|
|
|
162
186
|
};
|
|
163
187
|
|
|
164
188
|
const message = await summarizeJob(results);
|
|
189
|
+
await createNotification(message, payload);
|
|
165
190
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// Add the summary to chat memory so the agent has context in future conversations
|
|
169
|
-
await addToThread(TELEGRAM_CHAT_ID, message);
|
|
170
|
-
|
|
171
|
-
console.log(`Notified chat ${TELEGRAM_CHAT_ID} about job ${jobId.slice(0, 8)}`);
|
|
191
|
+
console.log(`Notification saved for job ${jobId.slice(0, 8)}`);
|
|
172
192
|
|
|
173
193
|
return Response.json({ ok: true, notified: true });
|
|
174
194
|
} catch (err) {
|
|
@@ -216,7 +236,7 @@ async function POST(request) {
|
|
|
216
236
|
|
|
217
237
|
// Route to handler
|
|
218
238
|
switch (routePath) {
|
|
219
|
-
case '/
|
|
239
|
+
case '/create-job': return handleWebhook(request);
|
|
220
240
|
case '/telegram/webhook': return handleTelegramWebhook(request);
|
|
221
241
|
case '/telegram/register': return handleTelegramRegister(request);
|
|
222
242
|
case '/github/webhook': return handleGithubWebhook(request);
|
|
@@ -233,10 +253,10 @@ async function GET(request) {
|
|
|
233
253
|
if (authError) return authError;
|
|
234
254
|
|
|
235
255
|
switch (routePath) {
|
|
236
|
-
case '/ping':
|
|
237
|
-
case '/jobs/status':
|
|
238
|
-
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 });
|
|
239
259
|
}
|
|
240
260
|
}
|
|
241
261
|
|
|
242
|
-
|
|
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/{dev.sh → local.sh}
RENAMED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
4
|
PACKAGE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
-
DEV_DIR="${1:-/tmp/thepopebot}"
|
|
5
|
+
DEV_DIR="${1:-/tmp/thepopebot.local}"
|
|
6
6
|
ENV_BACKUP="/tmp/env.$(uuidgen)"
|
|
7
7
|
|
|
8
8
|
HAS_ENV=false
|
|
@@ -22,6 +22,7 @@ sed -i '' "s|\"thepopebot\": \".*\"|\"thepopebot\": \"file:$PACKAGE_DIR\"|" pack
|
|
|
22
22
|
rm -rf node_modules package-lock.json
|
|
23
23
|
npm install --install-links
|
|
24
24
|
|
|
25
|
+
|
|
25
26
|
if [ "$HAS_ENV" = true ]; then
|
|
26
27
|
mv "$ENV_BACKUP" .env
|
|
27
28
|
echo "Restored .env from previous build"
|
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,24 +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',
|
|
25
|
-
'@langchain/langgraph',
|
|
26
|
-
'@langchain/anthropic',
|
|
27
|
-
'@langchain/core',
|
|
28
|
-
'zod',
|
|
29
17
|
'better-sqlite3',
|
|
30
18
|
],
|
|
31
19
|
};
|
|
32
20
|
}
|
|
33
|
-
|
|
34
|
-
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
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
7
|
|
|
8
8
|
let _agent = null;
|
|
9
9
|
|
|
@@ -11,12 +11,12 @@ let _agent = null;
|
|
|
11
11
|
* Get or create the LangGraph agent singleton.
|
|
12
12
|
* Uses createReactAgent which handles the tool loop automatically.
|
|
13
13
|
*/
|
|
14
|
-
function getAgent() {
|
|
14
|
+
export async function getAgent() {
|
|
15
15
|
if (!_agent) {
|
|
16
|
-
const model = createModel();
|
|
16
|
+
const model = await createModel();
|
|
17
17
|
const tools = [createJobTool, getJobStatusTool];
|
|
18
|
-
const checkpointer =
|
|
19
|
-
const systemPrompt = render_md(
|
|
18
|
+
const checkpointer = SqliteSaver.fromConnString(thepopebotDb);
|
|
19
|
+
const systemPrompt = render_md(chatbotMd);
|
|
20
20
|
|
|
21
21
|
_agent = createReactAgent({
|
|
22
22
|
llm: model,
|
|
@@ -31,8 +31,6 @@ function getAgent() {
|
|
|
31
31
|
/**
|
|
32
32
|
* Reset the agent singleton (e.g., when config changes).
|
|
33
33
|
*/
|
|
34
|
-
function resetAgent() {
|
|
34
|
+
export function resetAgent() {
|
|
35
35
|
_agent = null;
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
module.exports = { getAgent, resetAgent };
|
package/lib/ai/index.js
CHANGED
|
@@ -1,19 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { HumanMessage, AIMessage } from '@langchain/core/messages';
|
|
2
|
+
import { getAgent } from './agent.js';
|
|
3
|
+
import { createModel } from './model.js';
|
|
4
|
+
import { jobSummaryMd } from '../paths.js';
|
|
5
|
+
import { render_md } from '../utils/render-md.js';
|
|
6
|
+
import { getChatById, createChat, saveMessage, updateChatTitle } from '../db/chats.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Ensure a chat exists in the DB and save a message.
|
|
10
|
+
* Centralized so every channel gets persistence automatically.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} threadId - Chat/thread ID
|
|
13
|
+
* @param {string} role - 'user' or 'assistant'
|
|
14
|
+
* @param {string} text - Message text
|
|
15
|
+
* @param {object} [options] - { userId, chatTitle }
|
|
16
|
+
*/
|
|
17
|
+
function persistMessage(threadId, role, text, options = {}) {
|
|
18
|
+
try {
|
|
19
|
+
if (!getChatById(threadId)) {
|
|
20
|
+
createChat(options.userId || 'unknown', options.chatTitle || 'New Chat', threadId);
|
|
21
|
+
}
|
|
22
|
+
saveMessage(threadId, role, text);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
// DB persistence is best-effort — don't break chat if DB fails
|
|
25
|
+
console.error('Failed to persist message:', err);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
6
28
|
|
|
7
29
|
/**
|
|
8
30
|
* Process a chat message through the LangGraph agent.
|
|
31
|
+
* Saves user and assistant messages to the DB automatically.
|
|
9
32
|
*
|
|
10
33
|
* @param {string} threadId - Conversation thread ID (from channel adapter)
|
|
11
34
|
* @param {string} message - User's message text
|
|
12
35
|
* @param {Array} [attachments=[]] - Normalized attachments from adapter
|
|
36
|
+
* @param {object} [options] - { userId, chatTitle } for DB persistence
|
|
13
37
|
* @returns {Promise<string>} AI response text
|
|
14
38
|
*/
|
|
15
|
-
async function chat(threadId, message, attachments = []) {
|
|
16
|
-
const agent = getAgent();
|
|
39
|
+
async function chat(threadId, message, attachments = [], options = {}) {
|
|
40
|
+
const agent = await getAgent();
|
|
41
|
+
|
|
42
|
+
// Save user message to DB
|
|
43
|
+
persistMessage(threadId, 'user', message || '[attachment]', options);
|
|
17
44
|
|
|
18
45
|
// Build content blocks: text + any image attachments as base64 vision
|
|
19
46
|
const content = [];
|
|
@@ -47,37 +74,137 @@ async function chat(threadId, message, attachments = []) {
|
|
|
47
74
|
const lastMessage = result.messages[result.messages.length - 1];
|
|
48
75
|
|
|
49
76
|
// LangChain message content can be a string or an array of content blocks
|
|
77
|
+
let response;
|
|
50
78
|
if (typeof lastMessage.content === 'string') {
|
|
51
|
-
|
|
79
|
+
response = lastMessage.content;
|
|
80
|
+
} else {
|
|
81
|
+
response = lastMessage.content
|
|
82
|
+
.filter((block) => block.type === 'text')
|
|
83
|
+
.map((block) => block.text)
|
|
84
|
+
.join('\n');
|
|
52
85
|
}
|
|
53
86
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
87
|
+
// Save assistant response to DB
|
|
88
|
+
persistMessage(threadId, 'assistant', response, options);
|
|
89
|
+
|
|
90
|
+
// Auto-generate title for new chats
|
|
91
|
+
if (options.userId && message) {
|
|
92
|
+
autoTitle(threadId, message).catch(() => {});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return response;
|
|
59
96
|
}
|
|
60
97
|
|
|
61
98
|
/**
|
|
62
99
|
* Process a chat message with streaming (for channels that support it).
|
|
100
|
+
* Saves user and assistant messages to the DB automatically.
|
|
63
101
|
*
|
|
64
102
|
* @param {string} threadId - Conversation thread ID
|
|
65
103
|
* @param {string} message - User's message text
|
|
104
|
+
* @param {Array} [attachments=[]] - Image/PDF attachments: { category, mimeType, dataUrl }
|
|
105
|
+
* @param {object} [options] - { userId, chatTitle } for DB persistence
|
|
66
106
|
* @returns {AsyncIterableIterator<string>} Stream of text chunks
|
|
67
107
|
*/
|
|
68
|
-
async function* chatStream(threadId, message) {
|
|
69
|
-
const agent = getAgent();
|
|
108
|
+
async function* chatStream(threadId, message, attachments = [], options = {}) {
|
|
109
|
+
const agent = await getAgent();
|
|
70
110
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
{ configurable: { thread_id: threadId }, streamMode: 'messages' }
|
|
74
|
-
);
|
|
111
|
+
// Save user message to DB
|
|
112
|
+
persistMessage(threadId, 'user', message || '[attachment]', options);
|
|
75
113
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
114
|
+
// Build content blocks: text + any image/PDF attachments as vision
|
|
115
|
+
const content = [];
|
|
116
|
+
|
|
117
|
+
if (message) {
|
|
118
|
+
content.push({ type: 'text', text: message });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const att of attachments) {
|
|
122
|
+
if (att.category === 'image') {
|
|
123
|
+
// Support both dataUrl (web) and Buffer (Telegram) formats
|
|
124
|
+
const url = att.dataUrl
|
|
125
|
+
? att.dataUrl
|
|
126
|
+
: `data:${att.mimeType};base64,${att.data.toString('base64')}`;
|
|
127
|
+
content.push({
|
|
128
|
+
type: 'image_url',
|
|
129
|
+
image_url: { url },
|
|
130
|
+
});
|
|
79
131
|
}
|
|
80
132
|
}
|
|
133
|
+
|
|
134
|
+
// If only text and no attachments, simplify to a string
|
|
135
|
+
const messageContent = content.length === 1 && content[0].type === 'text'
|
|
136
|
+
? content[0].text
|
|
137
|
+
: content;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const stream = await agent.stream(
|
|
141
|
+
{ messages: [new HumanMessage({ content: messageContent })] },
|
|
142
|
+
{ configurable: { thread_id: threadId }, streamMode: 'messages' }
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
let fullText = '';
|
|
146
|
+
|
|
147
|
+
for await (const event of stream) {
|
|
148
|
+
// streamMode: 'messages' yields [message, metadata] tuples
|
|
149
|
+
const msg = Array.isArray(event) ? event[0] : event;
|
|
150
|
+
const isAI = msg._getType?.() === 'ai';
|
|
151
|
+
if (!isAI) continue;
|
|
152
|
+
|
|
153
|
+
// Content can be a string or an array of content blocks
|
|
154
|
+
let text = '';
|
|
155
|
+
if (typeof msg.content === 'string') {
|
|
156
|
+
text = msg.content;
|
|
157
|
+
} else if (Array.isArray(msg.content)) {
|
|
158
|
+
text = msg.content
|
|
159
|
+
.filter((b) => b.type === 'text' && b.text)
|
|
160
|
+
.map((b) => b.text)
|
|
161
|
+
.join('');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (text) {
|
|
165
|
+
fullText += text;
|
|
166
|
+
yield text;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Save assistant response to DB
|
|
171
|
+
if (fullText) {
|
|
172
|
+
persistMessage(threadId, 'assistant', fullText, options);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Auto-generate title for new chats
|
|
176
|
+
if (options.userId && message) {
|
|
177
|
+
autoTitle(threadId, message).catch(() => {});
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.error('[chatStream] error:', err);
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Auto-generate a chat title from the first user message (fire-and-forget).
|
|
187
|
+
*/
|
|
188
|
+
async function autoTitle(threadId, firstMessage) {
|
|
189
|
+
try {
|
|
190
|
+
const chat = getChatById(threadId);
|
|
191
|
+
if (!chat || chat.title !== 'New Chat') return;
|
|
192
|
+
|
|
193
|
+
const model = await createModel({ maxTokens: 50 });
|
|
194
|
+
const response = await model.invoke([
|
|
195
|
+
['system', 'Generate a short (3-6 word) title for this chat based on the user\'s first message. Return ONLY the title, nothing else.'],
|
|
196
|
+
['human', firstMessage],
|
|
197
|
+
]);
|
|
198
|
+
const title = typeof response.content === 'string'
|
|
199
|
+
? response.content
|
|
200
|
+
: response.content.filter(b => b.type === 'text').map(b => b.text).join('');
|
|
201
|
+
const cleaned = title.replace(/^["']+|["']+$/g, '').trim();
|
|
202
|
+
if (cleaned) {
|
|
203
|
+
updateChatTitle(threadId, cleaned);
|
|
204
|
+
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
// Title generation is best-effort
|
|
207
|
+
}
|
|
81
208
|
}
|
|
82
209
|
|
|
83
210
|
/**
|
|
@@ -89,8 +216,8 @@ async function* chatStream(threadId, message) {
|
|
|
89
216
|
*/
|
|
90
217
|
async function summarizeJob(results) {
|
|
91
218
|
try {
|
|
92
|
-
const model = createModel({ maxTokens: 1024 });
|
|
93
|
-
const systemPrompt = render_md(
|
|
219
|
+
const model = await createModel({ maxTokens: 1024 });
|
|
220
|
+
const systemPrompt = render_md(jobSummaryMd);
|
|
94
221
|
|
|
95
222
|
const userMessage = [
|
|
96
223
|
results.job ? `## Task\n${results.job}` : '',
|
|
@@ -134,7 +261,7 @@ async function summarizeJob(results) {
|
|
|
134
261
|
*/
|
|
135
262
|
async function addToThread(threadId, text) {
|
|
136
263
|
try {
|
|
137
|
-
const agent = getAgent();
|
|
264
|
+
const agent = await getAgent();
|
|
138
265
|
await agent.updateState(
|
|
139
266
|
{ configurable: { thread_id: threadId } },
|
|
140
267
|
{ messages: [new AIMessage(text)] }
|
|
@@ -144,4 +271,4 @@ async function addToThread(threadId, text) {
|
|
|
144
271
|
}
|
|
145
272
|
}
|
|
146
273
|
|
|
147
|
-
|
|
274
|
+
export { chat, chatStream, summarizeJob, addToThread, persistMessage };
|