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/lib/paths.js CHANGED
@@ -1,4 +1,4 @@
1
- const path = require('path');
1
+ import path from 'path';
2
2
 
3
3
  /**
4
4
  * Central path resolver for thepopebot.
@@ -7,24 +7,30 @@ const path = require('path');
7
7
 
8
8
  const PROJECT_ROOT = process.cwd();
9
9
 
10
- module.exports = {
10
+ export {
11
11
  PROJECT_ROOT,
12
+ };
12
13
 
13
- // config/ files
14
- configDir: path.join(PROJECT_ROOT, 'config'),
15
- cronsFile: path.join(PROJECT_ROOT, 'config', 'CRONS.json'),
16
- triggersFile: path.join(PROJECT_ROOT, 'config', 'TRIGGERS.json'),
17
- chatbotMd: path.join(PROJECT_ROOT, 'config', 'CHATBOT.md'),
18
- jobSummaryMd: path.join(PROJECT_ROOT, 'config', 'JOB_SUMMARY.md'),
19
- soulMd: path.join(PROJECT_ROOT, 'config', 'SOUL.md'),
14
+ // config/ files
15
+ export const configDir = path.join(PROJECT_ROOT, 'config');
16
+ export const cronsFile = path.join(PROJECT_ROOT, 'config', 'CRONS.json');
17
+ export const triggersFile = path.join(PROJECT_ROOT, 'config', 'TRIGGERS.json');
18
+ export const chatbotMd = path.join(PROJECT_ROOT, 'config', 'CHATBOT.md');
19
+ export const jobSummaryMd = path.join(PROJECT_ROOT, 'config', 'JOB_SUMMARY.md');
20
+ export const soulMd = path.join(PROJECT_ROOT, 'config', 'SOUL.md');
20
21
 
21
- // Working directories for command-type actions
22
- cronDir: path.join(PROJECT_ROOT, 'cron'),
23
- triggersDir: path.join(PROJECT_ROOT, 'triggers'),
22
+ // Working directories for command-type actions
23
+ export const cronDir = path.join(PROJECT_ROOT, 'cron');
24
+ export const triggersDir = path.join(PROJECT_ROOT, 'triggers');
24
25
 
25
- // Logs
26
- logsDir: path.join(PROJECT_ROOT, 'logs'),
26
+ // Logs
27
+ export const logsDir = path.join(PROJECT_ROOT, 'logs');
27
28
 
28
- // .env
29
- envFile: path.join(PROJECT_ROOT, '.env'),
30
- };
29
+ // Data (SQLite memory, etc.)
30
+ export const dataDir = path.join(PROJECT_ROOT, 'data');
31
+
32
+ // Database
33
+ export const thepopebotDb = process.env.DATABASE_PATH || path.join(PROJECT_ROOT, 'data', 'thepopebot.sqlite');
34
+
35
+ // .env
36
+ export const envFile = path.join(PROJECT_ROOT, '.env');
@@ -1,5 +1,5 @@
1
- const { v4: uuidv4 } = require('uuid');
2
- const { githubApi } = require('./github');
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { githubApi } from './github.js';
3
3
 
4
4
  /**
5
5
  * Create a new job branch with updated job.md
@@ -37,4 +37,4 @@ async function createJob(jobDescription) {
37
37
  return { job_id: jobId, branch };
38
38
  }
39
39
 
40
- module.exports = { createJob };
40
+ export { createJob };
@@ -119,4 +119,148 @@ async function getJobStatus(jobId) {
119
119
  };
120
120
  }
121
121
 
122
- module.exports = { githubApi, getWorkflowRuns, getWorkflowRunJobs, getJobStatus };
122
+ /**
123
+ * Get full swarm status: active + completed jobs with counts
124
+ * @returns {Promise<object>} - { active, completed, counts }
125
+ */
126
+ async function getSwarmStatus() {
127
+ const [inProgress, queued, completed] = await Promise.all([
128
+ getWorkflowRuns('in_progress'),
129
+ getWorkflowRuns('queued'),
130
+ getWorkflowRuns('completed'),
131
+ ]);
132
+
133
+ const activeRuns = [
134
+ ...(inProgress.workflow_runs || []),
135
+ ...(queued.workflow_runs || []),
136
+ ].filter(run => run.head_branch?.startsWith('job/'));
137
+
138
+ const completedRuns = (completed.workflow_runs || [])
139
+ .filter(run => run.head_branch?.startsWith('job/'))
140
+ .slice(0, 20);
141
+
142
+ // Get step details for active jobs
143
+ const active = await Promise.all(
144
+ activeRuns.map(async (run) => {
145
+ const jobId = run.head_branch.slice(4);
146
+ const startedAt = new Date(run.created_at);
147
+ const durationSeconds = Math.round((Date.now() - startedAt.getTime()) / 1000);
148
+
149
+ let currentStep = null;
150
+ let stepsCompleted = 0;
151
+ let stepsTotal = 0;
152
+
153
+ try {
154
+ const jobsData = await getWorkflowRunJobs(run.id);
155
+ if (jobsData.jobs?.length > 0) {
156
+ const job = jobsData.jobs[0];
157
+ stepsTotal = job.steps?.length || 0;
158
+ stepsCompleted = job.steps?.filter(s => s.status === 'completed').length || 0;
159
+ currentStep = job.steps?.find(s => s.status === 'in_progress')?.name || null;
160
+ }
161
+ } catch (err) {
162
+ // Jobs endpoint may fail if run hasn't started yet
163
+ }
164
+
165
+ return {
166
+ job_id: jobId,
167
+ branch: run.head_branch,
168
+ status: run.status,
169
+ workflow_name: run.name,
170
+ started_at: run.created_at,
171
+ duration_seconds: durationSeconds,
172
+ current_step: currentStep,
173
+ steps_completed: stepsCompleted,
174
+ steps_total: stepsTotal,
175
+ run_id: run.id,
176
+ html_url: run.html_url,
177
+ };
178
+ })
179
+ );
180
+
181
+ // Completed jobs are lighter — no step detail needed
182
+ const completedJobs = completedRuns.map((run) => ({
183
+ job_id: run.head_branch.slice(4),
184
+ branch: run.head_branch,
185
+ status: run.status,
186
+ conclusion: run.conclusion,
187
+ workflow_name: run.name,
188
+ started_at: run.created_at,
189
+ updated_at: run.updated_at,
190
+ run_id: run.id,
191
+ html_url: run.html_url,
192
+ }));
193
+
194
+ const successCount = completedJobs.filter(j => j.conclusion === 'success').length;
195
+ const failedCount = completedJobs.length - successCount;
196
+
197
+ return {
198
+ active,
199
+ completed: completedJobs,
200
+ counts: {
201
+ running: active.filter(j => j.status === 'in_progress').length,
202
+ queued: active.filter(j => j.status === 'queued').length,
203
+ succeeded: successCount,
204
+ failed: failedCount,
205
+ },
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Cancel a workflow run
211
+ * @param {number} runId - Workflow run ID
212
+ */
213
+ async function cancelWorkflowRun(runId) {
214
+ const { GH_OWNER, GH_REPO } = process.env;
215
+ const res = await fetch(
216
+ `https://api.github.com/repos/${GH_OWNER}/${GH_REPO}/actions/runs/${runId}/cancel`,
217
+ {
218
+ method: 'POST',
219
+ headers: {
220
+ 'Authorization': `Bearer ${process.env.GH_TOKEN}`,
221
+ 'Accept': 'application/vnd.github+json',
222
+ 'X-GitHub-Api-Version': '2022-11-28',
223
+ },
224
+ }
225
+ );
226
+ if (!res.ok && res.status !== 202) {
227
+ const error = await res.text();
228
+ throw new Error(`GitHub API error: ${res.status} ${error}`);
229
+ }
230
+ return { success: true };
231
+ }
232
+
233
+ /**
234
+ * Re-run a workflow run (all jobs or failed only)
235
+ * @param {number} runId - Workflow run ID
236
+ * @param {boolean} [failedOnly=false] - Only rerun failed jobs
237
+ */
238
+ async function rerunWorkflowRun(runId, failedOnly = false) {
239
+ const { GH_OWNER, GH_REPO } = process.env;
240
+ const endpoint = failedOnly
241
+ ? `https://api.github.com/repos/${GH_OWNER}/${GH_REPO}/actions/runs/${runId}/rerun-failed-jobs`
242
+ : `https://api.github.com/repos/${GH_OWNER}/${GH_REPO}/actions/runs/${runId}/rerun`;
243
+ const res = await fetch(endpoint, {
244
+ method: 'POST',
245
+ headers: {
246
+ 'Authorization': `Bearer ${process.env.GH_TOKEN}`,
247
+ 'Accept': 'application/vnd.github+json',
248
+ 'X-GitHub-Api-Version': '2022-11-28',
249
+ },
250
+ });
251
+ if (!res.ok && res.status !== 201) {
252
+ const error = await res.text();
253
+ throw new Error(`GitHub API error: ${res.status} ${error}`);
254
+ }
255
+ return { success: true };
256
+ }
257
+
258
+ export {
259
+ githubApi,
260
+ getWorkflowRuns,
261
+ getWorkflowRunJobs,
262
+ getJobStatus,
263
+ getSwarmStatus,
264
+ cancelWorkflowRun,
265
+ rerunWorkflowRun,
266
+ };
@@ -32,4 +32,4 @@ async function transcribeAudio(audioBuffer, filename) {
32
32
  return result.text;
33
33
  }
34
34
 
35
- module.exports = { isWhisperEnabled, transcribeAudio };
35
+ export { isWhisperEnabled, transcribeAudio };
@@ -1,5 +1,6 @@
1
- const { Bot } = require('grammy');
2
- const { hydrateReply } = require('@grammyjs/parse-mode');
1
+ import { Bot } from 'grammy';
2
+ import parseModePlugin from '@grammyjs/parse-mode';
3
+ const { hydrateReply } = parseModePlugin;
3
4
 
4
5
  const MAX_LENGTH = 4096;
5
6
 
@@ -277,7 +278,7 @@ function startTypingIndicator(botToken, chatId) {
277
278
  };
278
279
  }
279
280
 
280
- module.exports = {
281
+ export {
281
282
  getBot,
282
283
  setWebhook,
283
284
  sendMessage,
package/lib/triggers.js CHANGED
@@ -1,7 +1,6 @@
1
- const fs = require('fs');
2
- const paths = require('./paths');
3
-
4
- const { executeAction } = require('./actions');
1
+ import fs from 'fs';
2
+ import { triggersFile, triggersDir } from './paths.js';
3
+ import { executeAction } from './actions.js';
5
4
 
6
5
  /**
7
6
  * Replace {{body.field}} templates with values from request context
@@ -30,7 +29,7 @@ async function executeActions(trigger, context) {
30
29
  const resolved = { ...action };
31
30
  if (resolved.command) resolved.command = resolveTemplate(resolved.command, context);
32
31
  if (resolved.job) resolved.job = resolveTemplate(resolved.job, context);
33
- const result = await executeAction(resolved, { cwd: paths.triggersDir, data: context.body });
32
+ const result = await executeAction(resolved, { cwd: triggersDir, data: context.body });
34
33
  console.log(`[TRIGGER] ${trigger.name}: ${result || 'ran'}`);
35
34
  } catch (err) {
36
35
  console.error(`[TRIGGER] ${trigger.name}: error - ${err.message}`);
@@ -43,7 +42,7 @@ async function executeActions(trigger, context) {
43
42
  * @returns {{ triggerMap: Map, fireTriggers: Function }}
44
43
  */
45
44
  function loadTriggers() {
46
- const triggerFile = paths.triggersFile;
45
+ const triggerFile = triggersFile;
47
46
  const triggerMap = new Map();
48
47
 
49
48
  console.log('\n--- Triggers ---');
@@ -102,4 +101,4 @@ function loadTriggers() {
102
101
  return { triggerMap, fireTriggers };
103
102
  }
104
103
 
105
- module.exports = { loadTriggers };
104
+ export { loadTriggers };
@@ -1,6 +1,6 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const paths = require('../paths');
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { PROJECT_ROOT } from '../paths.js';
4
4
 
5
5
  const INCLUDE_PATTERN = /\{\{([^}]+\.md)\}\}/g;
6
6
 
@@ -15,7 +15,7 @@ function render_md(filePath, chain = []) {
15
15
  const resolved = path.resolve(filePath);
16
16
 
17
17
  if (chain.includes(resolved)) {
18
- const cycle = [...chain, resolved].map((p) => path.relative(paths.PROJECT_ROOT, p)).join(' -> ');
18
+ const cycle = [...chain, resolved].map((p) => path.relative(PROJECT_ROOT, p)).join(' -> ');
19
19
  console.log(`[render_md] Circular include detected: ${cycle}`);
20
20
  return '';
21
21
  }
@@ -28,7 +28,7 @@ function render_md(filePath, chain = []) {
28
28
  const currentChain = [...chain, resolved];
29
29
 
30
30
  return content.replace(INCLUDE_PATTERN, (match, includePath) => {
31
- const includeResolved = path.resolve(paths.PROJECT_ROOT, includePath.trim());
31
+ const includeResolved = path.resolve(PROJECT_ROOT, includePath.trim());
32
32
  if (!fs.existsSync(includeResolved)) {
33
33
  return match;
34
34
  }
@@ -36,4 +36,4 @@ function render_md(filePath, chain = []) {
36
36
  });
37
37
  }
38
38
 
39
- module.exports = { render_md };
39
+ export { render_md };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "thepopebot",
3
- "version": "1.1.2",
3
+ "version": "1.2.3",
4
+ "type": "module",
4
5
  "description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
5
6
  "bin": {
6
7
  "thepopebot": "./bin/cli.js"
@@ -9,7 +10,14 @@
9
10
  "exports": {
10
11
  "./api": "./api/index.js",
11
12
  "./config": "./config/index.js",
12
- "./instrumentation": "./config/instrumentation.js"
13
+ "./instrumentation": "./config/instrumentation.js",
14
+ "./auth": "./lib/auth/index.js",
15
+ "./auth/actions": "./lib/auth/actions.js",
16
+ "./chat/api": "./lib/chat/api.js",
17
+ "./db": "./lib/db/index.js",
18
+ "./chat": "./lib/chat/components/index.js",
19
+ "./chat/actions": "./lib/chat/actions.js",
20
+ "./middleware": "./lib/auth/middleware.js"
13
21
  },
14
22
  "files": [
15
23
  "bin/",
@@ -20,6 +28,9 @@
20
28
  "templates/"
21
29
  ],
22
30
  "scripts": {
31
+ "build": "esbuild lib/chat/components/*.jsx lib/chat/components/**/*.jsx --outdir=lib/chat/components --format=esm --jsx=automatic --outbase=lib/chat/components",
32
+ "prepublishOnly": "npm run build",
33
+ "local": "bash bin/local.sh",
23
34
  "postinstall": "node bin/postinstall.js",
24
35
  "test": "echo \"No tests yet\" && exit 0"
25
36
  },
@@ -35,21 +46,47 @@
35
46
  "author": "Stephen Pope",
36
47
  "license": "MIT",
37
48
  "dependencies": {
49
+ "@ai-sdk/react": "^2.0.0",
38
50
  "@grammyjs/parse-mode": "^2.2.0",
51
+ "@langchain/anthropic": "^1.3.17",
52
+ "@langchain/core": "^1.1.24",
53
+ "@langchain/google-genai": "^2.1.18",
54
+ "@langchain/langgraph": "^1.1.4",
55
+ "@langchain/langgraph-checkpoint-sqlite": "^1.0.1",
56
+ "@langchain/openai": "^1.2.7",
57
+ "ai": "^5.0.0",
58
+ "bcrypt-ts": "^6.0.0",
59
+ "better-sqlite3": "^12.6.2",
60
+ "chalk": "^5.3.0",
61
+ "class-variance-authority": "^0.7.0",
62
+ "clsx": "^2.0.0",
39
63
  "dotenv": "^16.3.1",
64
+ "drizzle-orm": "^0.44.0",
40
65
  "grammy": "^1.39.3",
41
- "node-cron": "^3.0.3",
42
- "uuid": "^9.0.0",
43
- "chalk": "^5.3.0",
44
66
  "inquirer": "^9.2.12",
67
+ "lucide-react": "^0.400.0",
68
+ "node-cron": "^3.0.3",
45
69
  "open": "^10.0.0",
46
- "ora": "^8.0.1"
70
+ "ora": "^8.0.1",
71
+ "streamdown": "^2.2.0",
72
+ "tailwind-merge": "^3.0.0",
73
+ "uuid": "^9.0.0",
74
+ "zod": "^4.3.6"
47
75
  },
48
76
  "peerDependencies": {
49
77
  "next": ">=15.0.0",
78
+ "next-auth": "^5.0.0-beta.30",
50
79
  "react": ">=19.0.0",
51
80
  "react-dom": ">=19.0.0"
52
81
  },
82
+ "peerDependenciesMeta": {
83
+ "next-auth": {
84
+ "optional": false
85
+ }
86
+ },
87
+ "devDependencies": {
88
+ "esbuild": "^0.27.3"
89
+ },
53
90
  "engines": {
54
91
  "node": ">=18.0.0"
55
92
  }
@@ -1,5 +1,6 @@
1
1
  import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import { randomBytes } from 'crypto';
3
4
 
4
5
  const ROOT_DIR = process.cwd();
5
6
 
@@ -98,24 +99,36 @@ export function writeModelsJson(providerName, { baseUrl, apiKey, api, models })
98
99
  */
99
100
  export function writeEnvFile(config) {
100
101
  const {
101
- apiKey,
102
102
  githubToken,
103
103
  githubOwner,
104
104
  githubRepo,
105
105
  telegramBotToken,
106
106
  telegramWebhookSecret,
107
107
  ghWebhookSecret,
108
- anthropicApiKey,
108
+ llmProvider,
109
+ llmModel,
110
+ providerEnvKey,
111
+ providerApiKey,
109
112
  openaiApiKey,
110
113
  telegramChatId,
111
114
  telegramVerification,
112
115
  } = config;
113
116
 
117
+ // Build the provider API key line(s)
118
+ // Always write the provider's key; also write OPENAI_API_KEY if it's a separate key (for Whisper)
119
+ let apiKeyLines = `# LLM API key (${providerEnvKey})\n${providerEnvKey}=${providerApiKey}`;
120
+ if (providerEnvKey !== 'OPENAI_API_KEY') {
121
+ apiKeyLines += `\n\n# OpenAI API key for Whisper voice transcription (optional)\nOPENAI_API_KEY=${openaiApiKey || ''}`;
122
+ }
123
+
124
+ // Generate AUTH_SECRET (32 random bytes, base64 encoded)
125
+ const authSecret = randomBytes(32).toString('base64');
126
+
114
127
  const envContent = `# thepopebot Configuration
115
128
  # Generated by setup wizard
116
129
 
117
- # Authentication key for /api/webhook endpoint
118
- API_KEY=${apiKey}
130
+ # Auth.js secret (used for session encryption)
131
+ AUTH_SECRET=${authSecret}
119
132
 
120
133
  # GitHub Personal Access Token (fine-grained: Actions, Contents, Metadata, Pull requests)
121
134
  GH_TOKEN=${githubToken}
@@ -124,6 +137,10 @@ GH_TOKEN=${githubToken}
124
137
  GH_OWNER=${githubOwner}
125
138
  GH_REPO=${githubRepo}
126
139
 
140
+ # LLM provider and model (used by both Event Handler and Docker Agent)
141
+ LLM_PROVIDER=${llmProvider}
142
+ LLM_MODEL=${llmModel}
143
+
127
144
  # Telegram bot token from @BotFather
128
145
  TELEGRAM_BOT_TOKEN=${telegramBotToken || ''}
129
146
 
@@ -139,11 +156,7 @@ TELEGRAM_VERIFICATION=${telegramVerification || ''}
139
156
  # Secret for GitHub Actions webhook auth (must match GH_WEBHOOK_SECRET secret)
140
157
  GH_WEBHOOK_SECRET=${ghWebhookSecret}
141
158
 
142
- # Anthropic API key for Claude chat features
143
- ANTHROPIC_API_KEY=${anthropicApiKey}
144
-
145
- # OpenAI API key for Whisper voice transcription (optional)
146
- OPENAI_API_KEY=${openaiApiKey || ''}
159
+ ${apiKeyLines}
147
160
  `;
148
161
 
149
162
  const envPath = join(ROOT_DIR, '.env');
@@ -48,11 +48,11 @@ function getGitRemoteInfo() {
48
48
  // Handle both HTTPS and SSH formats
49
49
  // https://github.com/owner/repo.git
50
50
  // git@github.com:owner/repo.git
51
- const httpsMatch = remote.match(/github\.com\/([^/]+)\/([^/.]+)/);
52
- const sshMatch = remote.match(/github\.com:([^/]+)\/([^/.]+)/);
51
+ const httpsMatch = remote.match(/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/);
52
+ const sshMatch = remote.match(/github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
53
53
  const match = httpsMatch || sshMatch;
54
54
  if (match) {
55
- return { owner: match[1], repo: match[2].replace('.git', '') };
55
+ return { owner: match[1], repo: match[2] };
56
56
  }
57
57
  return null;
58
58
  } catch {
@@ -109,6 +109,13 @@ export async function checkPrerequisites() {
109
109
  // Check git
110
110
  results.git.installed = commandExists('git');
111
111
  if (results.git.installed) {
112
+ // Initialize git repo if needed (must happen before remote check)
113
+ try {
114
+ execSync('git rev-parse --git-dir', { stdio: 'ignore' });
115
+ results.git.initialized = true;
116
+ } catch {
117
+ results.git.initialized = false;
118
+ }
112
119
  results.git.remoteInfo = getGitRemoteInfo();
113
120
  }
114
121
 
@@ -37,31 +37,18 @@ export async function runVerificationFlow(verificationCode, { allowSkip = false
37
37
  /**
38
38
  * Wait for server to pick up .env changes and verify it's running
39
39
  * @param {string} ngrokUrl - The ngrok URL
40
- * @param {string} apiKey - The API key for authentication
41
40
  * @returns {Promise<boolean>} - True if verified successfully
42
41
  */
43
- export async function verifyRestart(ngrokUrl, apiKey) {
42
+ export async function verifyRestart(ngrokUrl) {
44
43
  console.log(chalk.dim('\n Waiting for server to pick up changes...\n'));
45
44
  await new Promise(resolve => setTimeout(resolve, 3000));
46
45
 
47
- // Verify server is up
46
+ // Verify server is up (any HTTP response means it's running)
48
47
  try {
49
- const response = await fetch(`${ngrokUrl}/api/ping`, {
48
+ await fetch(`${ngrokUrl}/api/ping`, {
50
49
  method: 'GET',
51
- headers: { 'x-api-key': apiKey },
52
50
  signal: AbortSignal.timeout(10000)
53
51
  });
54
-
55
- if (!response.ok) {
56
- console.log(chalk.red(' ✗ Could not reach server.\n'));
57
- return false;
58
- }
59
-
60
- const data = await response.json();
61
- if (data.message !== 'Pong!') {
62
- console.log(chalk.red(' ✗ Unexpected server response.\n'));
63
- return false;
64
- }
65
52
  } catch (err) {
66
53
  console.log(chalk.red(` ✗ Server not reachable: ${err.message}\n`));
67
54
  return false;
@@ -48,7 +48,7 @@ function loadEnvFile() {
48
48
 
49
49
  async function main() {
50
50
  console.log(chalk.bold.cyan('\n Telegram Webhook Setup\n'));
51
- console.log(chalk.dim(' Use this to reconfigure Telegram after restarting ngrok.\n'));
51
+ console.log(chalk.dim(' Use this to reconfigure the Telegram webhook.\n'));
52
52
 
53
53
  // Check prerequisites
54
54
  const prereqs = await checkPrerequisites();
@@ -64,81 +64,50 @@ async function main() {
64
64
  // Load existing config
65
65
  const env = loadEnvFile();
66
66
 
67
- // Get ngrok URL first (verify server is up)
68
- console.log(chalk.yellow('\n Make sure your server is running:\n'));
69
- console.log(chalk.dim(' Terminal 1: ') + chalk.cyan('npm run dev'));
70
- console.log(chalk.dim(' Terminal 2: ') + chalk.cyan('ngrok http 3000\n'));
67
+ // Get APP_URL (verify server is up)
68
+ console.log(chalk.yellow('\n Make sure your server is running and publicly accessible.\n'));
71
69
 
72
- let ngrokUrl = null;
73
- while (!ngrokUrl) {
70
+ let appUrl = null;
71
+
72
+ // Try to read APP_URL from .env first
73
+ const existingAppUrl = env?.APP_URL;
74
+ if (existingAppUrl) {
75
+ printInfo(`Found APP_URL in .env: ${existingAppUrl}`);
76
+ const useExisting = await confirm('Use this URL?');
77
+ if (useExisting) {
78
+ appUrl = existingAppUrl;
79
+ }
80
+ }
81
+
82
+ while (!appUrl) {
74
83
  const { url } = await inquirer.prompt([
75
84
  {
76
85
  type: 'input',
77
86
  name: 'url',
78
- message: 'Paste your ngrok URL (https://...ngrok...):',
87
+ message: 'Enter your APP_URL (https://...):',
79
88
  validate: (input) => {
80
89
  if (!input) return 'URL is required';
81
90
  if (!input.startsWith('https://')) return 'URL must start with https://';
82
- if (!input.includes('ngrok')) return 'URL should be an ngrok URL';
83
91
  return true;
84
92
  },
85
93
  },
86
94
  ]);
87
- const testUrl = url.replace(/\/$/, '');
88
-
89
- // Verify the server is reachable through ngrok
90
- const healthSpinner = ora('Verifying server is reachable...').start();
91
- const apiKey = env?.API_KEY;
92
- try {
93
- const response = await fetch(`${testUrl}/api/ping`, {
94
- method: 'GET',
95
- headers: apiKey ? { 'x-api-key': apiKey } : {},
96
- signal: AbortSignal.timeout(10000)
97
- });
98
- if (response.ok) {
99
- const data = await response.json();
100
- if (data.message === 'Pong!') {
101
- healthSpinner.succeed('Server is reachable and authenticated');
102
- ngrokUrl = testUrl;
103
- } else {
104
- healthSpinner.fail('Unexpected response from server');
105
- const retry = await confirm('Try again?');
106
- if (!retry) {
107
- ngrokUrl = testUrl;
108
- }
109
- }
110
- } else if (response.status === 401) {
111
- healthSpinner.fail('Server responded but API key mismatch');
112
- printWarning('Check that API_KEY in .env matches the running server');
113
- const retry = await confirm('Try again?');
114
- if (!retry) {
115
- ngrokUrl = testUrl;
116
- }
117
- } else {
118
- healthSpinner.fail(`Server returned status ${response.status}`);
119
- printWarning('Make sure the server is running (npm run dev)');
120
- const retry = await confirm('Try again?');
121
- if (!retry) {
122
- ngrokUrl = testUrl;
123
- }
124
- }
125
- } catch (error) {
126
- healthSpinner.fail(`Could not reach server: ${error.message}`);
127
- printWarning('Make sure both the server AND ngrok are running');
128
- const retry = await confirm('Try again?');
129
- if (!retry) {
130
- ngrokUrl = testUrl;
131
- }
132
- }
95
+ appUrl = url.replace(/\/$/, '');
133
96
  }
134
97
 
135
- // Set GH_WEBHOOK_URL variable
136
- const urlSpinner = ora('Updating GH_WEBHOOK_URL variable...').start();
137
- const urlResult = await setVariables(owner, repo, { GH_WEBHOOK_URL: ngrokUrl });
138
- if (urlResult.GH_WEBHOOK_URL.success) {
139
- urlSpinner.succeed('GH_WEBHOOK_URL variable updated');
98
+ // Update APP_URL and APP_HOSTNAME in .env
99
+ const appHostname = new URL(appUrl).hostname;
100
+ updateEnvVariable('APP_URL', appUrl);
101
+ updateEnvVariable('APP_HOSTNAME', appHostname);
102
+ printSuccess('APP_URL saved to .env');
103
+
104
+ // Set APP_URL variable on GitHub
105
+ const urlSpinner = ora('Updating APP_URL variable...').start();
106
+ const urlResult = await setVariables(owner, repo, { APP_URL: appUrl });
107
+ if (urlResult.APP_URL.success) {
108
+ urlSpinner.succeed('APP_URL variable updated');
140
109
  } else {
141
- urlSpinner.fail(`Failed: ${urlResult.GH_WEBHOOK_URL.error}`);
110
+ urlSpinner.fail(`Failed: ${urlResult.APP_URL.error}`);
142
111
  }
143
112
 
144
113
  // Get Telegram token - try .env first
@@ -193,7 +162,7 @@ async function main() {
193
162
  }
194
163
 
195
164
  // Register Telegram webhook
196
- const webhookUrl = `${ngrokUrl}/api/telegram/webhook`;
165
+ const webhookUrl = `${appUrl}/api/telegram/webhook`;
197
166
  const tgSpinner = ora('Registering Telegram webhook...').start();
198
167
  const tgResult = await setTelegramWebhook(token, webhookUrl, webhookSecret);
199
168
  if (tgResult.ok) {