thepopebot 1.2.76-beta.3 → 1.2.76-beta.31
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 +3 -3
- package/api/CLAUDE.md +12 -5
- package/api/index.js +135 -33
- package/bin/CLAUDE.md +7 -4
- package/bin/cli.js +23 -43
- package/bin/docker-build.js +80 -53
- package/bin/sync.js +18 -1
- package/config/CLAUDE.md +23 -4
- package/config/instrumentation.js +29 -1
- package/drizzle/0021_coding_agent_workspace.sql +1 -0
- package/drizzle/0022_organic_apocalypse.sql +16 -0
- package/drizzle/0023_needy_ender_wiggin.sql +1 -0
- package/drizzle/0024_third_moondragon.sql +3 -0
- package/drizzle/meta/0021_snapshot.json +639 -0
- package/drizzle/meta/0022_snapshot.json +743 -0
- package/drizzle/meta/0023_snapshot.json +750 -0
- package/drizzle/meta/0024_snapshot.json +771 -0
- package/drizzle/meta/_journal.json +28 -0
- package/lib/CLAUDE.md +2 -2
- package/lib/actions.js +2 -1
- package/lib/ai/CLAUDE.md +72 -57
- package/lib/ai/helper-llm.js +108 -0
- package/lib/ai/index.js +370 -437
- package/lib/ai/line-mappers.js +47 -26
- package/lib/ai/scope.js +26 -0
- package/lib/ai/sdk-adapters/CLAUDE.md +4 -3
- package/lib/ai/sdk-adapters/claude-code.js +76 -17
- package/lib/ai/system-prompt.js +23 -5
- package/lib/ai/workspace-setup.js +19 -35
- package/lib/auth/actions.js +13 -2
- package/lib/channels/CLAUDE.md +14 -4
- package/lib/channels/base.js +6 -2
- package/lib/channels/commands/index.js +42 -0
- package/lib/channels/commands/session.js +53 -0
- package/lib/channels/commands/verify.js +18 -0
- package/lib/channels/telegram.js +79 -28
- package/lib/chat/CLAUDE.md +4 -4
- package/lib/chat/actions.js +276 -49
- package/lib/chat/api.js +193 -31
- package/lib/chat/components/CLAUDE.md +6 -2
- package/lib/chat/components/app-sidebar.js +1 -14
- package/lib/chat/components/app-sidebar.jsx +19 -17
- package/lib/chat/components/chat-input.js +120 -53
- package/lib/chat/components/chat-input.jsx +116 -43
- package/lib/chat/components/chat-page.js +2 -0
- package/lib/chat/components/chat-page.jsx +3 -0
- package/lib/chat/components/chat.js +123 -99
- package/lib/chat/components/chat.jsx +92 -55
- package/lib/chat/components/code-mode-toggle.js +221 -69
- package/lib/chat/components/code-mode-toggle.jsx +221 -84
- package/lib/chat/components/containers-page.js +58 -40
- package/lib/chat/components/containers-page.jsx +64 -25
- package/lib/chat/components/crons-page.js +17 -3
- package/lib/chat/components/crons-page.jsx +34 -6
- package/lib/chat/components/index.js +3 -3
- package/lib/chat/components/message.js +18 -3
- package/lib/chat/components/message.jsx +18 -3
- package/lib/chat/components/profile-page.js +281 -11
- package/lib/chat/components/profile-page.jsx +311 -20
- package/lib/chat/components/scope-picker.js +21 -0
- package/lib/chat/components/scope-picker.jsx +27 -0
- package/lib/chat/components/settings-chat-page.js +11 -11
- package/lib/chat/components/settings-chat-page.jsx +14 -18
- package/lib/chat/components/settings-coding-agents-page.js +156 -16
- package/lib/chat/components/settings-coding-agents-page.jsx +129 -3
- package/lib/chat/components/settings-github-page.js +5 -0
- package/lib/chat/components/settings-github-page.jsx +5 -0
- package/lib/chat/components/settings-jobs-page.js +17 -3
- package/lib/chat/components/settings-jobs-page.jsx +23 -7
- package/lib/chat/components/settings-layout.js +3 -3
- package/lib/chat/components/settings-layout.jsx +3 -3
- package/lib/chat/components/settings-secrets-layout.js +1 -2
- package/lib/chat/components/settings-secrets-layout.jsx +1 -2
- package/lib/chat/components/settings-secrets-page.js +180 -75
- package/lib/chat/components/settings-secrets-page.jsx +212 -66
- package/lib/chat/components/settings-shared.js +2 -2
- package/lib/chat/components/settings-shared.jsx +2 -2
- package/lib/chat/components/triggers-page.js +17 -3
- package/lib/chat/components/triggers-page.jsx +34 -6
- package/lib/chat/components/ui/combobox.js +18 -2
- package/lib/chat/components/ui/combobox.jsx +17 -1
- package/lib/chat/components/ui/dropdown-menu.js +23 -2
- package/lib/chat/components/ui/dropdown-menu.jsx +27 -2
- package/lib/chat/telegram-profile.js +33 -0
- package/lib/cluster/CLAUDE.md +9 -3
- package/lib/code/CLAUDE.md +11 -3
- package/lib/code/actions.js +43 -10
- package/lib/code/code-page.js +1 -1
- package/lib/code/code-page.jsx +3 -1
- package/lib/code/terminal-view.js +51 -23
- package/lib/code/terminal-view.jsx +56 -24
- package/lib/config.js +19 -4
- package/lib/containers/CLAUDE.md +16 -6
- package/lib/cron.js +32 -4
- package/lib/db/CLAUDE.md +5 -2
- package/lib/db/chats.js +9 -17
- package/lib/db/code-workspaces.js +8 -3
- package/lib/db/config.js +0 -1
- package/lib/db/index.js +12 -0
- package/lib/db/schema.js +27 -1
- package/lib/db/user-channels.js +138 -0
- package/lib/db/users.js +29 -14
- package/lib/llm-providers.js +8 -0
- package/lib/maintenance.js +31 -21
- package/lib/tools/CLAUDE.md +12 -3
- package/lib/tools/assemblyai.js +17 -0
- package/lib/tools/create-agent-job.js +21 -8
- package/lib/tools/docker.js +14 -3
- package/lib/tools/github.js +34 -0
- package/lib/tools/telegram.js +106 -0
- package/lib/triggers.js +38 -1
- package/lib/utils/render-md.js +44 -18
- package/package.json +8 -7
- package/setup/CLAUDE.md +11 -5
- package/setup/lib/providers.mjs +2 -1
- package/setup/lib/targets.mjs +13 -16
- package/setup/lib/telegram.mjs +8 -69
- package/templates/.env.example +0 -7
- package/templates/.github/workflows/rebuild-event-handler.yml +1 -6
- package/templates/.gitignore.template +0 -2
- package/templates/CLAUDE.md.template +28 -6
- package/templates/agent-job/CLAUDE.md.template +4 -1
- package/templates/agent-job/CRONS.json +16 -0
- package/templates/agent-job/SYSTEM.md +14 -9
- package/templates/agents/CLAUDE.md.template +17 -17
- package/templates/coding-workspace/CLAUDE.md.template +7 -0
- package/templates/data/CLAUDE.md.template +1 -1
- package/templates/docker-compose.custom.yml +1 -0
- package/templates/docker-compose.yml +1 -0
- package/templates/event-handler/CLAUDE.md.template +79 -0
- package/templates/event-handler/TRIGGERS.json +18 -2
- package/templates/skills/CLAUDE.md.template +20 -22
- package/templates/skills/agent-job-background/SKILL.md +33 -0
- package/templates/skills/agent-job-background/agent-job-background.js +77 -0
- package/templates/skills/agent-job-dm/SKILL.md +33 -0
- package/templates/skills/agent-job-dm/agent-job-dm.js +69 -0
- package/templates/skills/agent-job-secrets/SKILL.md +23 -0
- package/templates/skills/agent-job-secrets/agent-job-secrets.js +68 -0
- package/lib/ai/agent.js +0 -65
- package/lib/ai/async-channel.js +0 -51
- package/lib/ai/model.js +0 -130
- package/lib/ai/tools.js +0 -167
- package/lib/tools/openai.js +0 -37
- package/setup/lib/telegram-verify.mjs +0 -63
- package/setup/setup-telegram.mjs +0 -260
- package/templates/skills/active/.gitkeep +0 -0
- package/templates/skills/library/agent-job-secrets/SKILL.md +0 -23
- package/templates/skills/library/agent-job-secrets/agent-job-secrets.js +0 -62
- /package/templates/skills/{library/playwright-cli → playwright-cli}/SKILL.md +0 -0
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ The wizard walks you through everything:
|
|
|
104
104
|
**That's it.** Visit your APP_URL when the wizard finishes.
|
|
105
105
|
|
|
106
106
|
- **Web Chat**: Visit your APP_URL to chat with your agent, create jobs, upload files
|
|
107
|
-
- **Telegram** (optional):
|
|
107
|
+
- **Telegram** (optional): Connect a Telegram bot from `/admin/event-handler/telegram`
|
|
108
108
|
- **Webhook**: Send a POST to `/api/create-agent-job` with your API key to create jobs programmatically
|
|
109
109
|
- **Cron**: Edit `agent-job/CRONS.json` to schedule recurring jobs
|
|
110
110
|
|
|
@@ -143,9 +143,9 @@ See [Coding Agents](docs/CODING_AGENTS.md) for details on all five agent backend
|
|
|
143
143
|
> ```bash
|
|
144
144
|
> # Update .env and GitHub variable in one command:
|
|
145
145
|
> npx thepopebot set-var APP_URL https://your-new-url.ngrok.io
|
|
146
|
-
> # If Telegram is configured, re-register the webhook:
|
|
147
|
-
> npm run setup-telegram
|
|
148
146
|
> ```
|
|
147
|
+
>
|
|
148
|
+
> If Telegram is configured, click **Re-register webhook** at `/admin/event-handler/telegram` after the URL change.
|
|
149
149
|
|
|
150
150
|
---
|
|
151
151
|
|
package/api/CLAUDE.md
CHANGED
|
@@ -4,9 +4,14 @@ This directory contains the route handlers for all `/api/*` endpoints. These rou
|
|
|
4
4
|
|
|
5
5
|
## Auth
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Most routes require a valid API key passed via the `x-api-key` header. API keys are stored in the SQLite database and managed through the admin UI — they are NOT environment variables.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**Public routes** (no API key needed): `/ping`, `/telegram/webhook` (Telegram webhook secret), `/github/webhook` (GitHub webhook secret), `/oauth/callback` (validated via short-lived `state` token).
|
|
10
|
+
|
|
11
|
+
Auth flow: `x-api-key` header → `verifyApiKey()` → DB lookup (hashed, timing-safe comparison). Two key types exist:
|
|
12
|
+
|
|
13
|
+
- **User-owned API keys** — long-lived, created via the admin UI, used by external callers (cURL, GitHub Actions, Telegram register).
|
|
14
|
+
- **Per-job agent API keys** (`agent_job_api_key`) — short-lived, auto-created when an agent-job container launches (`createAgentJobApiKey()` in `lib/db/api-keys.js`), tied to the container name, and cleaned up by the maintenance cron after expiry. Only this key type is allowed to call `/api/get-agent-job-secret` (the route rejects other types).
|
|
10
15
|
|
|
11
16
|
## Do NOT use these routes for browser UI
|
|
12
17
|
|
|
@@ -24,11 +29,13 @@ Browser-facing data fetching uses **fetch route handlers** colocated with pages
|
|
|
24
29
|
| Method | Path | Auth | Handler |
|
|
25
30
|
|--------|------|------|---------|
|
|
26
31
|
| GET | `/api/ping` | None | Health check |
|
|
27
|
-
| POST | `/api/create-agent-job` | `x-api-key` | Create agent job |
|
|
28
|
-
| GET | `/api/get-agent-job-secret` | `
|
|
32
|
+
| POST | `/api/create-agent-job` | `x-api-key` | Create agent job. Body: `{ agent_job, llm_model?, agent_backend?, scope? }` |
|
|
33
|
+
| GET | `/api/get-agent-job-secret` | `agent_job_api_key` only | Get an agent job secret. `oauth2` credentials return only the access_token (auto-refreshed under a lock; rotated refresh tokens are persisted back). Other secret types return the raw value. |
|
|
34
|
+
| POST | `/api/set-agent-job-secret` | `agent_job_api_key` only | Create or update an agent-job secret from inside the container (used by the `set-secret` skill). |
|
|
29
35
|
| GET | `/api/agent-job-list-secrets` | `x-api-key` | List agent job secret keys (no values); returns `{secrets: [{key, isSet, updatedAt, secretType}]}` |
|
|
30
36
|
| GET | `/api/agent-jobs/status` | `x-api-key` | Agent job status (query: `?agent_job_id=`) |
|
|
31
|
-
| POST | `/api/telegram/webhook` | Telegram webhook secret | Telegram message handler |
|
|
37
|
+
| POST | `/api/telegram/webhook` | Telegram webhook secret | Telegram message handler (per-user routing via `user_channels`; verifies via `/verify <code>`, dispatches `/session` commands) |
|
|
32
38
|
| POST | `/api/telegram/register` | `x-api-key` | Register bot token + webhook URL |
|
|
33
39
|
| POST | `/api/github/webhook` | GitHub webhook secret | GitHub event handler |
|
|
34
40
|
| POST | `/api/cluster/:clusterId/role/:roleId/webhook` | `x-api-key` | Trigger cluster role execution |
|
|
41
|
+
| GET/POST | `/api/oauth/callback` | `state` token | OAuth provider redirect target. Exchanges `code` for tokens, persists via `setAgentJobSecret(name, stored, 'oauth')`. |
|
package/api/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { createHash, timingSafeEqual } from 'crypto';
|
|
1
|
+
import { createHash, timingSafeEqual, randomUUID } from 'crypto';
|
|
2
2
|
import { createAgentJob } from '../lib/tools/create-agent-job.js';
|
|
3
3
|
import { setWebhook } from '../lib/tools/telegram.js';
|
|
4
4
|
import { getAgentJobStatus, fetchAgentJobLog } from '../lib/tools/github.js';
|
|
5
5
|
import { getTelegramAdapter } from '../lib/channels/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { dispatchCommand, dispatchPreAuthCommand } from '../lib/channels/commands/index.js';
|
|
7
|
+
import { getByChannelChatId, getVerifiedChannels, setActiveThread } from '../lib/db/user-channels.js';
|
|
8
|
+
import { getAllUsers, getUserById } from '../lib/db/users.js';
|
|
9
|
+
import { chat, chatStream, summarizeAgentJob } from '../lib/ai/index.js';
|
|
7
10
|
import { createNotification } from '../lib/db/notifications.js';
|
|
8
|
-
import {
|
|
11
|
+
import { getFireTriggers } from '../lib/triggers.js';
|
|
9
12
|
import { verifyApiKey } from '../lib/db/api-keys.js';
|
|
10
13
|
import { getConfig } from '../lib/config.js';
|
|
11
14
|
import { parseOAuthState, exchangeCodeForToken } from '../lib/oauth/helper.js';
|
|
@@ -17,8 +20,6 @@ const _refreshLocks = new Map();
|
|
|
17
20
|
// Bot token — resolved from DB/env, can be overridden by /telegram/register
|
|
18
21
|
let telegramBotToken = null;
|
|
19
22
|
|
|
20
|
-
// Cached trigger firing function (initialized on first request)
|
|
21
|
-
let _fireTriggers = null;
|
|
22
23
|
|
|
23
24
|
function getTelegramBotToken() {
|
|
24
25
|
if (!telegramBotToken) {
|
|
@@ -27,13 +28,6 @@ function getTelegramBotToken() {
|
|
|
27
28
|
return telegramBotToken;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function getFireTriggers() {
|
|
31
|
-
if (!_fireTriggers) {
|
|
32
|
-
const result = loadTriggers();
|
|
33
|
-
_fireTriggers = result.fireTriggers;
|
|
34
|
-
}
|
|
35
|
-
return _fireTriggers;
|
|
36
|
-
}
|
|
37
31
|
|
|
38
32
|
// Routes that have their own authentication
|
|
39
33
|
const PUBLIC_ROUTES = ['/telegram/webhook', '/github/webhook', '/ping', '/oauth/callback'];
|
|
@@ -92,13 +86,14 @@ function extractAgentJobId(branchName) {
|
|
|
92
86
|
|
|
93
87
|
async function handleCreateAgentJob(request) {
|
|
94
88
|
const body = await request.json();
|
|
95
|
-
const {
|
|
96
|
-
if (!
|
|
89
|
+
const { agent_job } = body;
|
|
90
|
+
if (!agent_job) return Response.json({ error: 'Missing agent_job field' }, { status: 400 });
|
|
97
91
|
|
|
98
92
|
try {
|
|
99
|
-
const result = await createAgentJob(
|
|
93
|
+
const result = await createAgentJob(agent_job, {
|
|
100
94
|
llmModel: body.llm_model,
|
|
101
95
|
agentBackend: body.agent_backend,
|
|
96
|
+
scope: body.scope,
|
|
102
97
|
});
|
|
103
98
|
return Response.json(result);
|
|
104
99
|
} catch (err) {
|
|
@@ -113,8 +108,9 @@ async function handleGetAgentSecret(request) {
|
|
|
113
108
|
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
114
109
|
}
|
|
115
110
|
|
|
116
|
-
const
|
|
117
|
-
if (!
|
|
111
|
+
const rawKey = new URL(request.url).searchParams.get('key');
|
|
112
|
+
if (!rawKey) return Response.json({ error: 'Missing key' }, { status: 400 });
|
|
113
|
+
const key = rawKey.toUpperCase();
|
|
118
114
|
|
|
119
115
|
const { getAgentJobSecretRaw, setAgentJobSecret: saveSecret } = await import('../lib/db/config.js');
|
|
120
116
|
const raw = getAgentJobSecretRaw(key);
|
|
@@ -166,6 +162,74 @@ async function handleGetAgentSecret(request) {
|
|
|
166
162
|
return Response.json({ value: raw });
|
|
167
163
|
}
|
|
168
164
|
|
|
165
|
+
async function handleListUsers() {
|
|
166
|
+
const users = getAllUsers();
|
|
167
|
+
const enriched = users.map((u) => {
|
|
168
|
+
const channels = getVerifiedChannels(u.id).map((c) => c.channel);
|
|
169
|
+
return {
|
|
170
|
+
id: u.id,
|
|
171
|
+
email: u.email,
|
|
172
|
+
first_name: u.firstName,
|
|
173
|
+
last_name: u.lastName,
|
|
174
|
+
nickname: u.nickname,
|
|
175
|
+
role: u.role,
|
|
176
|
+
channels,
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
return Response.json({ users: enriched });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function handleSendDm(request) {
|
|
183
|
+
let body;
|
|
184
|
+
try {
|
|
185
|
+
body = await request.json();
|
|
186
|
+
} catch {
|
|
187
|
+
return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { user_id, message, channel } = body;
|
|
191
|
+
if (!user_id) return Response.json({ error: 'Missing user_id' }, { status: 400 });
|
|
192
|
+
if (!message || typeof message !== 'string') {
|
|
193
|
+
return Response.json({ error: 'Missing message' }, { status: 400 });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const user = getUserById(user_id);
|
|
197
|
+
if (!user) return Response.json({ error: 'User not found' }, { status: 404 });
|
|
198
|
+
|
|
199
|
+
const verified = getVerifiedChannels(user_id);
|
|
200
|
+
if (verified.length === 0) {
|
|
201
|
+
return Response.json({ error: 'User has no verified DM channels' }, { status: 409 });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Pick channel: explicit name, or 'default' / omitted = first verified (Telegram preferred while it's the only impl).
|
|
205
|
+
const wantDefault = !channel || channel === 'default';
|
|
206
|
+
const target = wantDefault
|
|
207
|
+
? (verified.find((c) => c.channel === 'telegram') || verified[0])
|
|
208
|
+
: verified.find((c) => c.channel === channel);
|
|
209
|
+
|
|
210
|
+
if (!target) {
|
|
211
|
+
return Response.json(
|
|
212
|
+
{ error: `User has no verified ${channel} channel`, available: verified.map((c) => c.channel) },
|
|
213
|
+
{ status: 409 }
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (target.channel === 'telegram') {
|
|
218
|
+
const botToken = getTelegramBotToken();
|
|
219
|
+
if (!botToken) return Response.json({ error: 'Telegram bot token not configured' }, { status: 500 });
|
|
220
|
+
const adapter = getTelegramAdapter(botToken);
|
|
221
|
+
try {
|
|
222
|
+
await adapter.sendResponse(target.channelChatId, message, { chatId: target.channelChatId });
|
|
223
|
+
return Response.json({ ok: true, channel: 'telegram', user_id });
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.error('Failed to send DM:', err);
|
|
226
|
+
return Response.json({ error: 'Failed to send DM' }, { status: 502 });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return Response.json({ error: `Channel "${target.channel}" not implemented` }, { status: 501 });
|
|
231
|
+
}
|
|
232
|
+
|
|
169
233
|
async function handleListAgentSecrets(request) {
|
|
170
234
|
const record = verifyApiKey(request.headers.get('x-api-key'));
|
|
171
235
|
if (record.type !== 'agent_job_api_key') {
|
|
@@ -209,29 +273,65 @@ async function handleTelegramWebhook(request) {
|
|
|
209
273
|
}
|
|
210
274
|
|
|
211
275
|
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
276
|
+
* Resolve the incoming channel message to a user, dispatch any slash command,
|
|
277
|
+
* and otherwise stream the message through the AI layer using the user's
|
|
278
|
+
* active session.
|
|
214
279
|
*/
|
|
215
280
|
async function processChannelMessage(adapter, normalized) {
|
|
216
|
-
|
|
217
|
-
const
|
|
281
|
+
const { channel, channelChatId, metadata } = normalized;
|
|
282
|
+
const binding = getByChannelChatId(channel, channelChatId);
|
|
283
|
+
|
|
284
|
+
// Unbound chat → only /verify is accepted; everything else is silently ignored.
|
|
285
|
+
if (!binding || !binding.verifiedAt) {
|
|
286
|
+
const result = await dispatchPreAuthCommand(normalized, { channel, channelChatId });
|
|
287
|
+
if (result?.handled) {
|
|
288
|
+
await adapter.sendResponse(channelChatId, result.reply, metadata);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const ctx = { channel, channelChatId, userId: binding.userId };
|
|
294
|
+
|
|
295
|
+
// Post-auth slash commands short-circuit the AI path.
|
|
296
|
+
const cmd = await dispatchCommand(normalized, ctx);
|
|
297
|
+
if (cmd?.handled) {
|
|
298
|
+
await adapter.sendResponse(channelChatId, cmd.reply, metadata);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
await adapter.acknowledge(metadata);
|
|
303
|
+
const stopIndicator = adapter.startProcessingIndicator(metadata);
|
|
218
304
|
|
|
219
305
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
306
|
+
let threadId = binding.activeThreadId;
|
|
307
|
+
if (!threadId) {
|
|
308
|
+
threadId = randomUUID();
|
|
309
|
+
setActiveThread(binding.userId, channel, threadId);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const envRepo = process.env.GH_OWNER && process.env.GH_REPO
|
|
313
|
+
? `${process.env.GH_OWNER}/${process.env.GH_REPO}`
|
|
314
|
+
: '';
|
|
315
|
+
const streamOptions = {
|
|
316
|
+
userId: binding.userId,
|
|
317
|
+
chatTitle: 'Telegram',
|
|
318
|
+
repo: envRepo,
|
|
319
|
+
branch: 'main',
|
|
320
|
+
codeMode: false,
|
|
321
|
+
codeModeType: 'code',
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
if (adapter.streamChatResponse) {
|
|
325
|
+
const chunks = chatStream(threadId, normalized.text, normalized.attachments, streamOptions);
|
|
326
|
+
await adapter.streamChatResponse(channelChatId, chunks);
|
|
327
|
+
} else {
|
|
328
|
+
const response = await chat(threadId, normalized.text, normalized.attachments, streamOptions);
|
|
329
|
+
await adapter.sendResponse(channelChatId, response, metadata);
|
|
330
|
+
}
|
|
227
331
|
} catch (err) {
|
|
228
332
|
console.error('Failed to process message with AI:', err);
|
|
229
333
|
await adapter
|
|
230
|
-
.sendResponse(
|
|
231
|
-
normalized.threadId,
|
|
232
|
-
'Sorry, I encountered an error processing your message.',
|
|
233
|
-
normalized.metadata
|
|
234
|
-
)
|
|
334
|
+
.sendResponse(channelChatId, 'Sorry, I encountered an error processing your message.', metadata)
|
|
235
335
|
.catch(() => {});
|
|
236
336
|
} finally {
|
|
237
337
|
stopIndicator();
|
|
@@ -398,6 +498,7 @@ async function POST(request) {
|
|
|
398
498
|
// Route to handler
|
|
399
499
|
switch (routePath) {
|
|
400
500
|
case '/create-agent-job': return handleCreateAgentJob(request);
|
|
501
|
+
case '/send-dm': return handleSendDm(request);
|
|
401
502
|
case '/telegram/webhook': return handleTelegramWebhook(request);
|
|
402
503
|
case '/telegram/register': return handleTelegramRegister(request);
|
|
403
504
|
case '/github/webhook': return handleGithubWebhook(request);
|
|
@@ -418,6 +519,7 @@ async function GET(request) {
|
|
|
418
519
|
case '/agent-jobs/status': return handleAgentJobStatus(request);
|
|
419
520
|
case '/get-agent-job-secret': return handleGetAgentSecret(request);
|
|
420
521
|
case '/agent-job-list-secrets': return handleListAgentSecrets(request);
|
|
522
|
+
case '/users': return handleListUsers();
|
|
421
523
|
case '/oauth/callback': return handleOAuthCallback(request);
|
|
422
524
|
default: return Response.json({ error: 'Not found' }, { status: 404 });
|
|
423
525
|
}
|
package/bin/CLAUDE.md
CHANGED
|
@@ -8,20 +8,23 @@ Entry point: `cli.js` (invoked via `npx thepopebot <command>`).
|
|
|
8
8
|
|---------|---------|
|
|
9
9
|
| `init [--no-managed] [--no-install]` | Scaffold project from templates, sync managed files, create `.env`, install deps |
|
|
10
10
|
| `setup` | Run interactive setup wizard (see `setup/CLAUDE.md`) |
|
|
11
|
-
| `setup-
|
|
11
|
+
| `setup-ssl` | Configure SSL with Let's Encrypt wildcard cert |
|
|
12
12
|
| `upgrade [@beta\|version]` | Upgrade package, run init, rebuild, commit, push, restart Docker |
|
|
13
13
|
| `reset [file]` | Restore a template file to defaults |
|
|
14
|
+
| `reset-all` | Nuclear reset — restore entire project to fresh init state |
|
|
15
|
+
| `audit` | Show project state vs. package templates (modified / missing / unknown) |
|
|
14
16
|
| `diff [file]` | Show diff between user file and package template |
|
|
15
17
|
| `reset-auth` | Regenerate `AUTH_SECRET` (invalidates all sessions) |
|
|
16
|
-
| `set-var <KEY> [VALUE]` | Set GitHub repository variable |
|
|
18
|
+
| `set-var <KEY> [VALUE]` | Set GitHub repository variable (also reads piped stdin) |
|
|
17
19
|
| `user:password <email>` | Change user password |
|
|
18
|
-
| `sync <path>` | Dev helper —
|
|
20
|
+
| `sync <path>` | Dev helper — pack, build, upload local package to test install |
|
|
21
|
+
| `sync --fast <path>` | Fast variant — copy source into the running container and rebuild `.next` only |
|
|
19
22
|
|
|
20
23
|
## Managed Paths System
|
|
21
24
|
|
|
22
25
|
`managed-paths.js` defines files auto-synced by `init`. These are overwritten on every init/upgrade — users should not edit them.
|
|
23
26
|
|
|
24
|
-
**Managed paths**: `.github/workflows/`, `docker-compose.yml`, `.dockerignore`, `.gitignore
|
|
27
|
+
**Managed paths**: `.github/workflows/`, `docker-compose.yml`, `.dockerignore`, `.gitignore`.
|
|
25
28
|
|
|
26
29
|
`isManaged(relPath)` — returns true if a path is managed (exact match or directory prefix).
|
|
27
30
|
|
package/bin/cli.js
CHANGED
|
@@ -53,7 +53,6 @@ Commands:
|
|
|
53
53
|
upgrade|update [@beta|version] Upgrade thepopebot (install, init, build, commit, push)
|
|
54
54
|
setup Run interactive setup wizard
|
|
55
55
|
setup-ssl Configure SSL with Let's Encrypt wildcard cert
|
|
56
|
-
setup-telegram Reconfigure Telegram webhook
|
|
57
56
|
reset-auth Regenerate AUTH_SECRET (invalidates all sessions)
|
|
58
57
|
reset [file] Restore a template file (or list available templates)
|
|
59
58
|
reset-all Nuclear reset — restore entire project to fresh init state
|
|
@@ -86,12 +85,12 @@ function getTemplateFiles(templatesDir) {
|
|
|
86
85
|
return files;
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
async function init() {
|
|
88
|
+
async function init(options = {}) {
|
|
90
89
|
let cwd = process.cwd();
|
|
91
90
|
const packageDir = path.join(__dirname, '..');
|
|
92
91
|
const templatesDir = path.join(packageDir, 'templates');
|
|
93
|
-
const noManaged = args.includes('--no-managed');
|
|
94
|
-
const noInstall = args.includes('--no-install');
|
|
92
|
+
const noManaged = options.noManaged ?? args.includes('--no-managed');
|
|
93
|
+
const noInstall = options.noInstall ?? args.includes('--no-install');
|
|
95
94
|
|
|
96
95
|
// Guard: warn if the directory is not empty (unless it's an existing thepopebot project)
|
|
97
96
|
const entries = fs.readdirSync(cwd);
|
|
@@ -256,7 +255,6 @@ async function init() {
|
|
|
256
255
|
type: 'module',
|
|
257
256
|
scripts: {
|
|
258
257
|
setup: 'thepopebot setup',
|
|
259
|
-
'setup-telegram': 'thepopebot setup-telegram',
|
|
260
258
|
'reset-auth': 'thepopebot reset-auth',
|
|
261
259
|
},
|
|
262
260
|
dependencies: {
|
|
@@ -277,33 +275,23 @@ async function init() {
|
|
|
277
275
|
}
|
|
278
276
|
}
|
|
279
277
|
|
|
280
|
-
// Create
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
278
|
+
// Create agent skill bridge symlinks (point to ../skills)
|
|
279
|
+
const skillBridges = [
|
|
280
|
+
{ dir: '.pi', name: 'Pi' },
|
|
281
|
+
{ dir: '.claude', name: 'Claude' },
|
|
282
|
+
{ dir: '.codex', name: 'Codex' },
|
|
283
|
+
{ dir: '.gemini', name: 'Gemini' },
|
|
284
|
+
{ dir: '.kimi', name: 'Kimi' },
|
|
285
|
+
];
|
|
286
|
+
for (const { dir, name } of skillBridges) {
|
|
287
|
+
const link = path.join(cwd, dir, 'skills');
|
|
288
|
+
if (!fs.existsSync(link)) {
|
|
289
|
+
fs.mkdirSync(path.dirname(link), { recursive: true });
|
|
290
|
+
createDirLink('../skills', link);
|
|
291
|
+
console.log(` Created ${dir}/skills → ../skills`);
|
|
289
292
|
}
|
|
290
293
|
}
|
|
291
294
|
|
|
292
|
-
// Create .pi/skills → ../skills/active symlink
|
|
293
|
-
const piSkillsLink = path.join(cwd, '.pi', 'skills');
|
|
294
|
-
if (!fs.existsSync(piSkillsLink)) {
|
|
295
|
-
fs.mkdirSync(path.dirname(piSkillsLink), { recursive: true });
|
|
296
|
-
createDirLink('../skills/active', piSkillsLink);
|
|
297
|
-
console.log(' Created .pi/skills → ../skills/active');
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Create .claude/skills → ../skills/active symlink
|
|
301
|
-
const claudeSkillsLink = path.join(cwd, '.claude', 'skills');
|
|
302
|
-
if (!fs.existsSync(claudeSkillsLink)) {
|
|
303
|
-
fs.mkdirSync(path.dirname(claudeSkillsLink), { recursive: true });
|
|
304
|
-
createDirLink('../skills/active', claudeSkillsLink);
|
|
305
|
-
console.log(' Created .claude/skills → ../skills/active');
|
|
306
|
-
}
|
|
307
295
|
|
|
308
296
|
// Report backed-up files
|
|
309
297
|
if (backedUp.length > 0) {
|
|
@@ -656,8 +644,12 @@ const PROTECTED_PATHS = [
|
|
|
656
644
|
'package.json',
|
|
657
645
|
'docker-compose.custom.yml',
|
|
658
646
|
'.claude/',
|
|
647
|
+
'.codex/',
|
|
648
|
+
'.gemini/',
|
|
649
|
+
'.kimi/',
|
|
659
650
|
'.pi/',
|
|
660
651
|
'skills/',
|
|
652
|
+
'agents/',
|
|
661
653
|
'node_modules/',
|
|
662
654
|
];
|
|
663
655
|
|
|
@@ -749,10 +741,10 @@ async function resetAll() {
|
|
|
749
741
|
|
|
750
742
|
console.log(`\n Moved ${filesToMove.length} file(s) to .backups/${ts}/`);
|
|
751
743
|
|
|
752
|
-
// Run init to rebuild from templates
|
|
744
|
+
// Run init to rebuild from templates (call directly to use the same package version)
|
|
753
745
|
console.log('\n Running init to rebuild project...\n');
|
|
754
746
|
try {
|
|
755
|
-
|
|
747
|
+
await init({ noInstall: true });
|
|
756
748
|
} catch {
|
|
757
749
|
console.error('\n Init failed. Your backup is at .backups/' + ts + '/\n');
|
|
758
750
|
process.exit(1);
|
|
@@ -789,15 +781,6 @@ function setupSsl() {
|
|
|
789
781
|
}
|
|
790
782
|
}
|
|
791
783
|
|
|
792
|
-
function setupTelegram() {
|
|
793
|
-
const setupScript = path.join(__dirname, '..', 'setup', 'setup-telegram.mjs');
|
|
794
|
-
try {
|
|
795
|
-
execFileSync(process.execPath, [setupScript], { stdio: 'inherit', cwd: process.cwd() });
|
|
796
|
-
} catch {
|
|
797
|
-
process.exit(1);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
784
|
async function resetAuth() {
|
|
802
785
|
const { randomBytes } = await import('crypto');
|
|
803
786
|
const { updateEnvVariable } = await import(path.join(__dirname, '..', 'setup', 'lib', 'auth.mjs'));
|
|
@@ -1072,9 +1055,6 @@ switch (command) {
|
|
|
1072
1055
|
case 'setup-ssl':
|
|
1073
1056
|
setupSsl();
|
|
1074
1057
|
break;
|
|
1075
|
-
case 'setup-telegram':
|
|
1076
|
-
setupTelegram();
|
|
1077
|
-
break;
|
|
1078
1058
|
case 'reset-auth':
|
|
1079
1059
|
await resetAuth();
|
|
1080
1060
|
break;
|
package/bin/docker-build.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Build all Docker images locally
|
|
4
|
+
* Build all Docker images locally.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* npm run docker:build # build
|
|
8
|
-
* npm run docker:build -- --image event-handler # build one
|
|
7
|
+
* npm run docker:build # build everything
|
|
8
|
+
* npm run docker:build -- --image event-handler # build one (deps built first)
|
|
9
9
|
*
|
|
10
10
|
* Reads the version from package.json and tags each image as:
|
|
11
11
|
* stephengpope/thepopebot:{image}-{version}
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Image hierarchy:
|
|
14
|
+
*
|
|
15
|
+
* thepopebot-base ← Ubuntu + Node + locale + Chromium + playwright + user
|
|
16
|
+
* ├── coding-agent-base ← + tmux, ttyd, scripts, entrypoint
|
|
17
|
+
* │ ├── coding-agent-claude-code ← + per-agent CLI
|
|
18
|
+
* │ ├── coding-agent-pi-coding-agent
|
|
19
|
+
* │ └── ... (one per agent)
|
|
20
|
+
* └── event-handler ← + pm2, gosu, Next.js, server.js
|
|
21
|
+
*
|
|
22
|
+
* Build order:
|
|
23
|
+
* 1. thepopebot-base
|
|
24
|
+
* 2. coding-agent-base + event-handler in parallel
|
|
25
|
+
* 3. all coding-agent variants in parallel
|
|
26
|
+
*
|
|
27
|
+
* Base images are tagged both versioned and unversioned (no version) so child
|
|
28
|
+
* Dockerfiles can `FROM thepopebot-base` / `FROM coding-agent-base` without a
|
|
29
|
+
* build-arg for local development.
|
|
17
30
|
*/
|
|
18
31
|
|
|
19
32
|
import { spawn } from 'child_process';
|
|
@@ -28,14 +41,21 @@ const pkg = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
|
28
41
|
const VERSION = pkg.version;
|
|
29
42
|
const REPO = 'stephengpope/thepopebot';
|
|
30
43
|
|
|
31
|
-
//
|
|
32
|
-
const
|
|
44
|
+
// Built first — everything depends on this.
|
|
45
|
+
const THEPOPEBOT_BASE = {
|
|
46
|
+
name: 'thepopebot-base',
|
|
47
|
+
context: 'docker/base',
|
|
48
|
+
dockerfile: 'docker/base/Dockerfile',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Built second — depends on thepopebot-base.
|
|
52
|
+
const CODING_AGENT_BASE = {
|
|
33
53
|
name: 'coding-agent-base',
|
|
34
54
|
context: 'docker/coding-agent',
|
|
35
55
|
dockerfile: 'docker/coding-agent/Dockerfile',
|
|
36
56
|
};
|
|
37
57
|
|
|
38
|
-
//
|
|
58
|
+
// Built third — depend on coding-agent-base.
|
|
39
59
|
const CODING_AGENTS = [
|
|
40
60
|
{
|
|
41
61
|
name: 'coding-agent-claude-code',
|
|
@@ -69,16 +89,14 @@ const CODING_AGENTS = [
|
|
|
69
89
|
},
|
|
70
90
|
];
|
|
71
91
|
|
|
72
|
-
//
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
},
|
|
79
|
-
];
|
|
92
|
+
// Built second — depends on thepopebot-base. Built in parallel with coding-agent-base.
|
|
93
|
+
const EVENT_HANDLER = {
|
|
94
|
+
name: 'event-handler',
|
|
95
|
+
context: '.',
|
|
96
|
+
dockerfile: 'docker/event-handler/Dockerfile',
|
|
97
|
+
};
|
|
80
98
|
|
|
81
|
-
const ALL_IMAGES = [
|
|
99
|
+
const ALL_IMAGES = [THEPOPEBOT_BASE, CODING_AGENT_BASE, ...CODING_AGENTS, EVENT_HANDLER];
|
|
82
100
|
|
|
83
101
|
// Parse --image flag
|
|
84
102
|
const filterArg = process.argv.find((_, i, a) => a[i - 1] === '--image');
|
|
@@ -101,11 +119,14 @@ function buildImage(img) {
|
|
|
101
119
|
console.log(` ${label} building — ${tag}`);
|
|
102
120
|
|
|
103
121
|
return new Promise((resolve, reject) => {
|
|
104
|
-
// Tag base image as both versioned and unversioned (agent Dockerfiles use FROM coding-agent-base)
|
|
105
122
|
const args = ['build', '-t', tag, '-f', dockerfile];
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
|
|
124
|
+
// Base images get an unversioned tag too so child Dockerfiles can
|
|
125
|
+
// `FROM thepopebot-base` / `FROM coding-agent-base` without a build-arg.
|
|
126
|
+
if (img.name === 'thepopebot-base' || img.name === 'coding-agent-base') {
|
|
127
|
+
args.push('-t', img.name);
|
|
108
128
|
}
|
|
129
|
+
|
|
109
130
|
args.push(context);
|
|
110
131
|
|
|
111
132
|
const proc = spawn(
|
|
@@ -172,50 +193,56 @@ function buildImage(img) {
|
|
|
172
193
|
});
|
|
173
194
|
}
|
|
174
195
|
|
|
175
|
-
// Build logic: base first, then agents + others in parallel
|
|
176
196
|
async function run() {
|
|
177
197
|
if (filterArg) {
|
|
178
|
-
// Single image build
|
|
179
|
-
if (filterArg ===
|
|
198
|
+
// Single image build — build dependency chain too.
|
|
199
|
+
if (filterArg === THEPOPEBOT_BASE.name) {
|
|
180
200
|
console.log(`Building 1 image — version ${VERSION}\n`);
|
|
181
|
-
await buildImage(
|
|
182
|
-
} else {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
201
|
+
await buildImage(THEPOPEBOT_BASE);
|
|
202
|
+
} else if (filterArg === CODING_AGENT_BASE.name) {
|
|
203
|
+
console.log(`Building 2 images — version ${VERSION}\n`);
|
|
204
|
+
await buildImage(THEPOPEBOT_BASE);
|
|
205
|
+
await buildImage(CODING_AGENT_BASE);
|
|
206
|
+
} else if (filterArg === EVENT_HANDLER.name) {
|
|
207
|
+
console.log(`Building 2 images — version ${VERSION}\n`);
|
|
208
|
+
await buildImage(THEPOPEBOT_BASE);
|
|
209
|
+
await buildImage(EVENT_HANDLER);
|
|
210
|
+
} else if (CODING_AGENTS.some(img => img.name === filterArg)) {
|
|
211
|
+
console.log(`Building 3 images — version ${VERSION}\n`);
|
|
212
|
+
await buildImage(THEPOPEBOT_BASE);
|
|
213
|
+
await buildImage(CODING_AGENT_BASE);
|
|
214
|
+
const agent = CODING_AGENTS.find(img => img.name === filterArg);
|
|
215
|
+
await buildImage(agent);
|
|
195
216
|
}
|
|
196
|
-
console.log('\
|
|
217
|
+
console.log('\ndone.');
|
|
197
218
|
return;
|
|
198
219
|
}
|
|
199
220
|
|
|
200
|
-
// Full build: base
|
|
221
|
+
// Full build: thepopebot-base → (coding-agent-base + event-handler in parallel) → variants
|
|
201
222
|
const totalCount = ALL_IMAGES.length;
|
|
202
|
-
console.log(`Building ${totalCount} images
|
|
223
|
+
console.log(`Building ${totalCount} images — version ${VERSION}\n`);
|
|
203
224
|
|
|
204
|
-
// Step 1:
|
|
205
|
-
await buildImage(
|
|
225
|
+
// Step 1: thepopebot-base
|
|
226
|
+
await buildImage(THEPOPEBOT_BASE);
|
|
206
227
|
|
|
207
|
-
// Step 2:
|
|
208
|
-
const
|
|
209
|
-
const
|
|
228
|
+
// Step 2: coding-agent-base and event-handler in parallel (both extend thepopebot-base)
|
|
229
|
+
const tier2 = await Promise.allSettled([CODING_AGENT_BASE, EVENT_HANDLER].map(buildImage));
|
|
230
|
+
const tier2Failed = tier2.filter(r => r.status === 'rejected');
|
|
231
|
+
if (tier2Failed.length > 0) {
|
|
232
|
+
console.error(`Tier 2 failed: ${tier2Failed.map(r => r.reason.message).join(', ')}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
210
235
|
|
|
211
|
-
|
|
212
|
-
const
|
|
236
|
+
// Step 3: all coding-agent variants in parallel
|
|
237
|
+
const tier3 = await Promise.allSettled(CODING_AGENTS.map(buildImage));
|
|
238
|
+
const tier3Failed = tier3.filter(r => r.status === 'rejected');
|
|
239
|
+
const tier3Succeeded = tier3.filter(r => r.status === 'fulfilled');
|
|
213
240
|
|
|
214
|
-
|
|
215
|
-
console.log(`\n${
|
|
241
|
+
const succeededCount = 1 + 2 + tier3Succeeded.length;
|
|
242
|
+
console.log(`\n${succeededCount}/${totalCount} images built successfully.`);
|
|
216
243
|
|
|
217
|
-
if (
|
|
218
|
-
console.error(`${
|
|
244
|
+
if (tier3Failed.length > 0) {
|
|
245
|
+
console.error(`${tier3Failed.length} failed: ${tier3Failed.map(r => r.reason.message).join(', ')}`);
|
|
219
246
|
process.exit(1);
|
|
220
247
|
}
|
|
221
248
|
}
|