thepopebot 1.1.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/api/index.js +72 -165
- package/bin/cli.js +36 -6
- package/bin/local.sh +31 -0
- package/bin/postinstall.js +6 -2
- package/config/index.js +2 -11
- package/config/instrumentation.js +17 -5
- package/lib/actions.js +7 -6
- package/lib/ai/agent.js +36 -0
- package/lib/ai/index.js +274 -0
- package/lib/ai/model.js +67 -0
- package/lib/ai/tools.js +49 -0
- package/lib/auth/actions.js +28 -0
- package/lib/auth/config.js +45 -0
- package/lib/auth/index.js +27 -0
- package/lib/auth/middleware.js +30 -0
- package/lib/channels/base.js +56 -0
- package/lib/channels/index.js +15 -0
- package/lib/channels/telegram.js +146 -0
- package/lib/chat/actions.js +239 -0
- package/lib/chat/api.js +103 -0
- package/lib/chat/components/app-sidebar.js +161 -0
- package/lib/chat/components/app-sidebar.jsx +214 -0
- package/lib/chat/components/chat-header.js +9 -0
- package/lib/chat/components/chat-header.jsx +14 -0
- package/lib/chat/components/chat-input.js +230 -0
- package/lib/chat/components/chat-input.jsx +232 -0
- package/lib/chat/components/chat-nav-context.js +11 -0
- package/lib/chat/components/chat-nav-context.jsx +11 -0
- package/lib/chat/components/chat-page.js +70 -0
- package/lib/chat/components/chat-page.jsx +89 -0
- package/lib/chat/components/chat.js +78 -0
- package/lib/chat/components/chat.jsx +91 -0
- package/lib/chat/components/chats-page.js +170 -0
- package/lib/chat/components/chats-page.jsx +203 -0
- package/lib/chat/components/crons-page.js +144 -0
- package/lib/chat/components/crons-page.jsx +204 -0
- package/lib/chat/components/greeting.js +11 -0
- package/lib/chat/components/greeting.jsx +14 -0
- package/lib/chat/components/icons.js +518 -0
- package/lib/chat/components/icons.jsx +482 -0
- package/lib/chat/components/index.js +19 -0
- package/lib/chat/components/message.js +66 -0
- package/lib/chat/components/message.jsx +92 -0
- package/lib/chat/components/messages.js +63 -0
- package/lib/chat/components/messages.jsx +72 -0
- package/lib/chat/components/notifications-page.js +54 -0
- package/lib/chat/components/notifications-page.jsx +83 -0
- package/lib/chat/components/page-layout.js +21 -0
- package/lib/chat/components/page-layout.jsx +28 -0
- package/lib/chat/components/settings-layout.js +37 -0
- package/lib/chat/components/settings-layout.jsx +51 -0
- package/lib/chat/components/settings-secrets-page.js +216 -0
- package/lib/chat/components/settings-secrets-page.jsx +264 -0
- package/lib/chat/components/sidebar-history-item.js +54 -0
- package/lib/chat/components/sidebar-history-item.jsx +50 -0
- package/lib/chat/components/sidebar-history.js +92 -0
- package/lib/chat/components/sidebar-history.jsx +132 -0
- package/lib/chat/components/sidebar-user-nav.js +59 -0
- package/lib/chat/components/sidebar-user-nav.jsx +69 -0
- package/lib/chat/components/swarm-page.js +250 -0
- package/lib/chat/components/swarm-page.jsx +356 -0
- package/lib/chat/components/triggers-page.js +121 -0
- package/lib/chat/components/triggers-page.jsx +177 -0
- package/lib/chat/components/ui/dropdown-menu.js +98 -0
- package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
- package/lib/chat/components/ui/scroll-area.js +13 -0
- package/lib/chat/components/ui/scroll-area.jsx +17 -0
- package/lib/chat/components/ui/separator.js +21 -0
- package/lib/chat/components/ui/separator.jsx +18 -0
- package/lib/chat/components/ui/sheet.js +75 -0
- package/lib/chat/components/ui/sheet.jsx +95 -0
- package/lib/chat/components/ui/sidebar.js +227 -0
- package/lib/chat/components/ui/sidebar.jsx +245 -0
- package/lib/chat/components/ui/tooltip.js +56 -0
- package/lib/chat/components/ui/tooltip.jsx +66 -0
- package/lib/chat/utils.js +11 -0
- package/lib/cron.js +7 -8
- package/lib/db/api-keys.js +160 -0
- package/lib/db/chats.js +129 -0
- package/lib/db/index.js +106 -0
- package/lib/db/notifications.js +99 -0
- package/lib/db/schema.js +51 -0
- package/lib/db/users.js +89 -0
- package/lib/paths.js +23 -17
- package/lib/tools/create-job.js +3 -3
- package/lib/tools/github.js +145 -1
- package/lib/tools/openai.js +1 -1
- package/lib/tools/telegram.js +4 -3
- package/lib/triggers.js +6 -7
- package/lib/utils/render-md.js +6 -6
- package/package.json +43 -6
- package/setup/lib/auth.mjs +22 -9
- package/setup/lib/prerequisites.mjs +10 -3
- package/setup/lib/telegram-verify.mjs +3 -16
- package/setup/setup-telegram.mjs +31 -62
- package/setup/setup.mjs +58 -98
- package/templates/.dockerignore +5 -0
- package/templates/.env.example +18 -2
- package/templates/.github/workflows/auto-merge.yml +1 -1
- package/templates/.github/workflows/build-image.yml +6 -4
- package/templates/.github/workflows/notify-job-failed.yml +2 -2
- package/templates/.github/workflows/notify-pr-complete.yml +2 -2
- package/templates/.github/workflows/run-job.yml +24 -10
- package/templates/CLAUDE.md +5 -3
- package/templates/app/api/auth/[...nextauth]/route.js +1 -0
- package/templates/app/api/chat/route.js +1 -0
- package/templates/app/chat/[chatId]/page.js +8 -0
- package/templates/app/chats/page.js +7 -0
- package/templates/app/components/ascii-logo.jsx +10 -0
- package/templates/app/components/login-form.jsx +81 -0
- package/templates/app/components/setup-form.jsx +82 -0
- package/templates/app/components/theme-provider.jsx +11 -0
- package/templates/app/components/theme-toggle.jsx +38 -0
- package/templates/app/components/ui/button.jsx +21 -0
- package/templates/app/components/ui/card.jsx +23 -0
- package/templates/app/components/ui/input.jsx +10 -0
- package/templates/app/components/ui/label.jsx +10 -0
- package/templates/app/crons/page.js +7 -0
- package/templates/app/globals.css +66 -0
- package/templates/app/layout.js +9 -2
- package/templates/app/login/page.js +15 -0
- package/templates/app/notifications/page.js +7 -0
- package/templates/app/page.js +6 -30
- package/templates/app/settings/layout.js +7 -0
- package/templates/app/settings/page.js +5 -0
- package/templates/app/settings/secrets/page.js +5 -0
- package/templates/app/swarm/page.js +7 -0
- package/templates/app/triggers/page.js +7 -0
- package/templates/config/CRONS.json +2 -2
- package/templates/config/TRIGGERS.json +2 -2
- package/templates/docker/event_handler/Dockerfile +19 -0
- package/templates/docker/{entrypoint.sh → job/entrypoint.sh} +4 -4
- package/templates/docker/runner/Dockerfile +38 -0
- package/templates/docker/runner/entrypoint.sh +41 -0
- package/templates/docker-compose.yml +52 -0
- package/templates/instrumentation.js +6 -1
- package/templates/middleware.js +1 -0
- package/templates/postcss.config.mjs +5 -0
- package/lib/claude/conversation.js +0 -76
- package/lib/claude/index.js +0 -142
- package/lib/claude/tools.js +0 -54
- /package/templates/docker/{Dockerfile → job/Dockerfile} +0 -0
package/lib/paths.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
10
|
+
export {
|
|
11
11
|
PROJECT_ROOT,
|
|
12
|
+
};
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
26
|
+
// Logs
|
|
27
|
+
export const logsDir = path.join(PROJECT_ROOT, 'logs');
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
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');
|
package/lib/tools/create-job.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
40
|
+
export { createJob };
|
package/lib/tools/github.js
CHANGED
|
@@ -119,4 +119,148 @@ async function getJobStatus(jobId) {
|
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
|
|
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
|
+
};
|
package/lib/tools/openai.js
CHANGED
package/lib/tools/telegram.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
281
|
+
export {
|
|
281
282
|
getBot,
|
|
282
283
|
setWebhook,
|
|
283
284
|
sendMessage,
|
package/lib/triggers.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
104
|
+
export { loadTriggers };
|
package/lib/utils/render-md.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
39
|
+
export { render_md };
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thepopebot",
|
|
3
|
-
"version": "1.
|
|
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
|
}
|
package/setup/lib/auth.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
118
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
|
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
|
-
|
|
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;
|
package/setup/setup-telegram.mjs
CHANGED
|
@@ -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
|
|
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
|
|
68
|
-
console.log(chalk.yellow('\n Make sure your server is running
|
|
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
|
|
73
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
//
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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.
|
|
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 = `${
|
|
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) {
|