thepopebot 1.2.75-beta.2 → 1.2.75-beta.4

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.
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Interactive SSL setup — configures Let's Encrypt wildcard cert via Traefik DNS challenge.
5
+ *
6
+ * Prompts for domain, DNS provider, API credentials, and server address.
7
+ * Creates the wildcard DNS record via the provider's API.
8
+ * Writes all config to .env and switches COMPOSE_FILE to docker-compose.custom.yml.
9
+ */
10
+
11
+ import * as clack from '@clack/prompts';
12
+ import { updateEnvVariable } from './lib/auth.mjs';
13
+ import { loadEnvFile } from './lib/env.mjs';
14
+
15
+ function handleCancel(value) {
16
+ if (clack.isCancel(value)) {
17
+ clack.cancel('Setup cancelled.');
18
+ process.exit(0);
19
+ }
20
+ return value;
21
+ }
22
+
23
+ // DNS providers supported by Traefik's ACME DNS challenge (via lego).
24
+ // Each entry maps to the Traefik provider name and the env vars it expects.
25
+ const DNS_PROVIDERS = {
26
+ cloudflare: {
27
+ label: 'Cloudflare',
28
+ traefikName: 'cloudflare',
29
+ envVars: [
30
+ { key: 'CF_DNS_API_TOKEN', label: 'Cloudflare API Token', secret: true },
31
+ ],
32
+ apiCreate: createCloudflareRecord,
33
+ helpUrl: 'https://dash.cloudflare.com/profile/api-tokens',
34
+ helpSteps: [
35
+ 'Go to your Cloudflare dashboard → Profile → API Tokens',
36
+ 'Click "Create Token"',
37
+ 'Use the "Edit zone DNS" template',
38
+ 'Select the zone (domain) you want to use',
39
+ 'Copy the token',
40
+ ],
41
+ },
42
+ route53: {
43
+ label: 'AWS Route 53',
44
+ traefikName: 'route53',
45
+ envVars: [
46
+ { key: 'AWS_ACCESS_KEY_ID', label: 'AWS Access Key ID', secret: false },
47
+ { key: 'AWS_SECRET_ACCESS_KEY', label: 'AWS Secret Access Key', secret: true },
48
+ { key: 'AWS_REGION', label: 'AWS Region', secret: false, default: 'us-east-1' },
49
+ ],
50
+ apiCreate: null, // TODO: implement
51
+ helpUrl: 'https://console.aws.amazon.com/iam/',
52
+ helpSteps: [
53
+ 'Create an IAM user with Route53 permissions',
54
+ 'Generate access keys for the user',
55
+ ],
56
+ },
57
+ digitalocean: {
58
+ label: 'DigitalOcean',
59
+ traefikName: 'digitalocean',
60
+ envVars: [
61
+ { key: 'DO_AUTH_TOKEN', label: 'DigitalOcean API Token', secret: true },
62
+ ],
63
+ apiCreate: null, // TODO: implement
64
+ helpUrl: 'https://cloud.digitalocean.com/account/api/tokens',
65
+ helpSteps: [
66
+ 'Go to DigitalOcean → API → Tokens',
67
+ 'Generate a new personal access token with read/write scope',
68
+ ],
69
+ },
70
+ namecheap: {
71
+ label: 'Namecheap',
72
+ traefikName: 'namecheap',
73
+ envVars: [
74
+ { key: 'NAMECHEAP_API_USER', label: 'Namecheap API User', secret: false },
75
+ { key: 'NAMECHEAP_API_KEY', label: 'Namecheap API Key', secret: true },
76
+ ],
77
+ apiCreate: null, // TODO: implement
78
+ helpUrl: 'https://www.namecheap.com/support/api/intro/',
79
+ helpSteps: [
80
+ 'Enable API access in your Namecheap account',
81
+ 'Whitelist your server IP',
82
+ 'Copy your API key',
83
+ ],
84
+ },
85
+ godaddy: {
86
+ label: 'GoDaddy',
87
+ traefikName: 'godaddy',
88
+ envVars: [
89
+ { key: 'GODADDY_API_KEY', label: 'GoDaddy API Key', secret: true },
90
+ { key: 'GODADDY_API_SECRET', label: 'GoDaddy API Secret', secret: true },
91
+ ],
92
+ apiCreate: null, // TODO: implement
93
+ helpUrl: 'https://developer.godaddy.com/keys',
94
+ helpSteps: [
95
+ 'Go to GoDaddy Developer Portal → API Keys',
96
+ 'Create a Production key',
97
+ ],
98
+ },
99
+ porkbun: {
100
+ label: 'Porkbun',
101
+ traefikName: 'porkbun',
102
+ envVars: [
103
+ { key: 'PORKBUN_API_KEY', label: 'Porkbun API Key', secret: true },
104
+ { key: 'PORKBUN_SECRET_API_KEY', label: 'Porkbun Secret Key', secret: true },
105
+ ],
106
+ apiCreate: null, // TODO: implement
107
+ helpUrl: 'https://porkbun.com/account/api',
108
+ helpSteps: [
109
+ 'Go to Porkbun → Account → API Access',
110
+ 'Create an API key pair',
111
+ ],
112
+ },
113
+ };
114
+
115
+ async function main() {
116
+ clack.intro('SSL Setup — Let\'s Encrypt wildcard cert');
117
+
118
+ const env = loadEnvFile() || {};
119
+
120
+ // ── Domain ──────────────────────────────────────────────────────────
121
+ const existingDomain = env.SSL_DOMAIN;
122
+ let domain;
123
+
124
+ if (existingDomain) {
125
+ clack.log.info(`Current domain: ${existingDomain}`);
126
+ const reconfig = handleCancel(await clack.confirm({
127
+ message: 'Change domain?',
128
+ initialValue: false,
129
+ }));
130
+ domain = reconfig ? null : existingDomain;
131
+ }
132
+
133
+ if (!domain) {
134
+ domain = handleCancel(await clack.text({
135
+ message: 'Enter your domain (e.g., bot.example.com):',
136
+ placeholder: 'bot.example.com',
137
+ validate: (input) => {
138
+ if (!input) return 'Domain is required';
139
+ if (!input.includes('.')) return 'Enter a valid domain';
140
+ if (input.startsWith('*.')) return 'Enter the base domain without the wildcard (e.g., bot.example.com)';
141
+ },
142
+ }));
143
+ }
144
+
145
+ // ── Email ───────────────────────────────────────────────────────────
146
+ const existingEmail = env.SSL_EMAIL;
147
+ let email;
148
+
149
+ if (existingEmail) {
150
+ clack.log.info(`Current email: ${existingEmail}`);
151
+ const reconfig = handleCancel(await clack.confirm({
152
+ message: 'Change email?',
153
+ initialValue: false,
154
+ }));
155
+ email = reconfig ? null : existingEmail;
156
+ }
157
+
158
+ if (!email) {
159
+ email = handleCancel(await clack.text({
160
+ message: 'Email for Let\'s Encrypt notifications:',
161
+ validate: (input) => {
162
+ if (!input) return 'Email is required';
163
+ if (!input.includes('@')) return 'Enter a valid email';
164
+ },
165
+ }));
166
+ }
167
+
168
+ // ── DNS Provider ────────────────────────────────────────────────────
169
+ const providerKey = handleCancel(await clack.select({
170
+ message: 'Who manages your DNS?',
171
+ options: Object.entries(DNS_PROVIDERS).map(([key, p]) => ({
172
+ label: p.label,
173
+ value: key,
174
+ })),
175
+ }));
176
+
177
+ const provider = DNS_PROVIDERS[providerKey];
178
+
179
+ // Show help steps for getting the API credentials
180
+ clack.log.info(
181
+ `To get your ${provider.label} API credentials:\n` +
182
+ provider.helpSteps.map((s, i) => ` ${i + 1}. ${s}`).join('\n') +
183
+ `\n\n ${provider.helpUrl}`
184
+ );
185
+
186
+ // ── API Credentials ─────────────────────────────────────────────────
187
+ const credentials = {};
188
+ for (const envVar of provider.envVars) {
189
+ const existing = env[envVar.key];
190
+ if (existing) {
191
+ const masked = '****' + existing.slice(-4);
192
+ clack.log.info(`${envVar.label}: ${masked}`);
193
+ const reconfig = handleCancel(await clack.confirm({
194
+ message: `Change ${envVar.label}?`,
195
+ initialValue: false,
196
+ }));
197
+ if (!reconfig) {
198
+ credentials[envVar.key] = existing;
199
+ continue;
200
+ }
201
+ }
202
+
203
+ if (envVar.secret) {
204
+ credentials[envVar.key] = handleCancel(await clack.password({
205
+ message: `${envVar.label}:`,
206
+ validate: (input) => {
207
+ if (!input && !envVar.default) return `${envVar.label} is required`;
208
+ },
209
+ }));
210
+ } else {
211
+ credentials[envVar.key] = handleCancel(await clack.text({
212
+ message: `${envVar.label}:`,
213
+ defaultValue: envVar.default || '',
214
+ placeholder: envVar.default || '',
215
+ validate: (input) => {
216
+ if (!input && !envVar.default) return `${envVar.label} is required`;
217
+ },
218
+ }));
219
+ }
220
+ credentials[envVar.key] = credentials[envVar.key] || envVar.default;
221
+ }
222
+
223
+ // ── DNS Record Target ──────────────────────────────────────────────
224
+ const targetType = handleCancel(await clack.select({
225
+ message: `How should *.${domain} resolve?`,
226
+ options: [
227
+ { label: 'IP address (VPS, bare metal, cloud VM)', value: 'A' },
228
+ { label: 'CNAME (Tailscale hostname, another domain)', value: 'CNAME' },
229
+ ],
230
+ }));
231
+
232
+ let recordValue;
233
+ if (targetType === 'A') {
234
+ // Try to detect public IP
235
+ let detectedIp;
236
+ try {
237
+ const resp = await fetch('https://api.ipify.org');
238
+ if (resp.ok) detectedIp = (await resp.text()).trim();
239
+ } catch {}
240
+
241
+ recordValue = handleCancel(await clack.text({
242
+ message: 'Server IP address:',
243
+ defaultValue: detectedIp || '',
244
+ placeholder: detectedIp || '203.0.113.10',
245
+ validate: (input) => {
246
+ if (!input) return 'IP address is required';
247
+ if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(input)) return 'Enter a valid IPv4 address';
248
+ },
249
+ }));
250
+ } else {
251
+ recordValue = handleCancel(await clack.text({
252
+ message: 'CNAME target (e.g., mybox.tailnet.ts.net):',
253
+ validate: (input) => {
254
+ if (!input) return 'CNAME target is required';
255
+ if (!input.includes('.')) return 'Enter a valid hostname';
256
+ },
257
+ }));
258
+ }
259
+
260
+ // ── Create DNS Record ──────────────────────────────────────────────
261
+ const s = clack.spinner();
262
+
263
+ if (provider.apiCreate) {
264
+ s.start(`Creating *.${domain} ${targetType} record → ${recordValue}`);
265
+ try {
266
+ await provider.apiCreate(domain, targetType, recordValue, credentials);
267
+ s.stop(`Created *.${domain} → ${recordValue}`);
268
+ } catch (err) {
269
+ s.stop(`Failed to create DNS record: ${err.message}`);
270
+ clack.log.warn(
271
+ `Create this record manually in your ${provider.label} dashboard:\n` +
272
+ ` Type: ${targetType}\n` +
273
+ ` Name: *.${domain}\n` +
274
+ ` Value: ${recordValue}`
275
+ );
276
+ const proceed = handleCancel(await clack.confirm({
277
+ message: 'Continue anyway? (you can create the record manually)',
278
+ initialValue: true,
279
+ }));
280
+ if (!proceed) process.exit(0);
281
+ }
282
+ } else {
283
+ clack.log.warn(
284
+ `Automatic DNS record creation is not yet supported for ${provider.label}.\n` +
285
+ ` Create this record in your ${provider.label} dashboard:\n\n` +
286
+ ` Type: ${targetType}\n` +
287
+ ` Name: *.${domain}\n` +
288
+ ` Value: ${recordValue}\n`
289
+ );
290
+ handleCancel(await clack.text({
291
+ message: 'Press enter once you\'ve created the record',
292
+ defaultValue: '',
293
+ }));
294
+ }
295
+
296
+ // ── Write .env ─────────────────────────────────────────────────────
297
+ s.start('Writing configuration to .env');
298
+
299
+ updateEnvVariable('SSL_DOMAIN', domain);
300
+ updateEnvVariable('SSL_EMAIL', email);
301
+ updateEnvVariable('SSL_DNS_PROVIDER', provider.traefikName);
302
+ updateEnvVariable('APP_HOSTNAME', domain);
303
+
304
+ // Write provider-specific credentials (standard names — Traefik reads them directly)
305
+ for (const envVar of provider.envVars) {
306
+ updateEnvVariable(envVar.key, credentials[envVar.key]);
307
+ }
308
+
309
+ // Switch to custom compose file
310
+ updateEnvVariable('COMPOSE_FILE', 'docker-compose.custom.yml');
311
+
312
+ s.stop('Configuration saved to .env');
313
+
314
+ // ── Summary ────────────────────────────────────────────────────────
315
+ clack.log.success(
316
+ `SSL configured!\n\n` +
317
+ ` Domain: ${domain}\n` +
318
+ ` Wildcard: *.${domain}\n` +
319
+ ` Provider: ${provider.label}\n` +
320
+ ` Compose: docker-compose.custom.yml\n\n` +
321
+ ` Traefik will automatically obtain and renew your wildcard cert.\n\n` +
322
+ ` Next: restart your containers with \`docker compose up -d\``
323
+ );
324
+
325
+ clack.outro('Done!');
326
+ }
327
+
328
+ // ── Cloudflare API ─────────────────────────────────────────────────────
329
+
330
+ async function createCloudflareRecord(domain, type, value, credentials) {
331
+ const token = credentials.CF_DNS_API_TOKEN;
332
+
333
+ // Find the zone — walk up the domain to find the registered zone.
334
+ // e.g., for "bot.example.com", the zone is "example.com"
335
+ const parts = domain.split('.');
336
+ let zoneId;
337
+ let zoneName;
338
+
339
+ for (let i = 0; i < parts.length - 1; i++) {
340
+ const candidate = parts.slice(i).join('.');
341
+ const resp = await fetch(`https://api.cloudflare.com/client/v4/zones?name=${candidate}`, {
342
+ headers: { Authorization: `Bearer ${token}` },
343
+ });
344
+ const data = await resp.json();
345
+ if (data.result?.length > 0) {
346
+ zoneId = data.result[0].id;
347
+ zoneName = data.result[0].name;
348
+ break;
349
+ }
350
+ }
351
+
352
+ if (!zoneId) {
353
+ throw new Error(`Could not find a Cloudflare zone for ${domain}. Make sure your domain is added to Cloudflare.`);
354
+ }
355
+
356
+ // Create the wildcard record: *.domain → value
357
+ const recordName = `*.${domain}`;
358
+
359
+ // Check if record already exists
360
+ const existingResp = await fetch(
361
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?type=${type}&name=${recordName}`,
362
+ { headers: { Authorization: `Bearer ${token}` } }
363
+ );
364
+ const existing = await existingResp.json();
365
+
366
+ if (existing.result?.length > 0) {
367
+ // Update existing record
368
+ const recordId = existing.result[0].id;
369
+ const updateResp = await fetch(
370
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${recordId}`,
371
+ {
372
+ method: 'PUT',
373
+ headers: {
374
+ Authorization: `Bearer ${token}`,
375
+ 'Content-Type': 'application/json',
376
+ },
377
+ body: JSON.stringify({ type, name: recordName, content: value, proxied: false }),
378
+ }
379
+ );
380
+ const updateData = await updateResp.json();
381
+ if (!updateData.success) {
382
+ throw new Error(updateData.errors?.[0]?.message || 'Failed to update DNS record');
383
+ }
384
+ } else {
385
+ // Create new record
386
+ const createResp = await fetch(
387
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`,
388
+ {
389
+ method: 'POST',
390
+ headers: {
391
+ Authorization: `Bearer ${token}`,
392
+ 'Content-Type': 'application/json',
393
+ },
394
+ body: JSON.stringify({ type, name: recordName, content: value, proxied: false }),
395
+ }
396
+ );
397
+ const createData = await createResp.json();
398
+ if (!createData.success) {
399
+ throw new Error(createData.errors?.[0]?.message || 'Failed to create DNS record');
400
+ }
401
+ }
402
+ }
403
+
404
+ main().catch((err) => {
405
+ clack.log.error(err.message);
406
+ process.exit(1);
407
+ });
@@ -36,8 +36,7 @@ project-root/
36
36
 
37
37
  ├── .github/workflows/ # GitHub Actions
38
38
  ├── docker-compose.yml # Docker Compose config (MANAGED)
39
- ├── docker-compose.custom.yml # User-owned compose overrides (not managed)
40
- ├── traefik-dynamic.yml.example # Traefik TLS config template (not managed)
39
+ ├── docker-compose.custom.yml # SSL compose with Let's Encrypt (not managed)
41
40
  ├── skills/ # All available agent skills
42
41
  │ └── active/ # Symlinks to active skills (shared by Pi + Claude Code)
43
42
  ├── .pi/skills → skills/active # Pi reads skills from here
@@ -343,7 +342,7 @@ Synced during setup for GitHub Actions workflows. The event handler reads creden
343
342
 
344
343
  The following paths are auto-synced by `thepopebot init` and `thepopebot upgrade`. **Do not edit them** — changes will be overwritten on package updates: `.github/workflows/`, `docker-compose.yml`, `.dockerignore`, `.gitignore`, `CLAUDE.md`, `config/CLAUDE.md`, `skills/CLAUDE.md`, `cron/CLAUDE.md`, `triggers/CLAUDE.md`, `docs/CLAUDE.md`.
345
344
 
346
- To customize Docker Compose config without losing changes on upgrade, set `COMPOSE_FILE=docker-compose.custom.yml` in `.env`. The custom file is scaffolded by init but never overwritten.
345
+ To enable SSL with Let's Encrypt wildcard cert, run `npx thepopebot setup-ssl`. This configures `docker-compose.custom.yml` (not managed, never overwritten).
347
346
 
348
347
  The Next.js app and `.next` build output are baked into the Docker image — they do not exist in user projects.
349
348
 
@@ -351,7 +350,7 @@ The Next.js app and `.next` build output are baked into the Docker image — the
351
350
 
352
351
  User-editable config files in `config/`: `agent-chat/SYSTEM.md` (agent chat system prompt), `code-chat/SYSTEM.md` (code workspace planning), `agent-job/SOUL.md` (personality), `agent-job/AGENT_JOB.md` (runtime docs), `agent-job/SUMMARY.md` (job summaries), `cluster/SYSTEM.md` (cluster system prompt), `cluster/ROLE.md` (cluster role prompt), `HEARTBEAT.md` (self-monitoring), `CRONS.json` (scheduled jobs), `TRIGGERS.json` (webhook triggers).
353
352
 
354
- To customize Docker Compose (TLS, ports, volumes, extra services), edit `docker-compose.custom.yml` and set `COMPOSE_FILE=docker-compose.custom.yml` in `.env`. For Tailscale TLS, copy `traefik-dynamic.yml.example` to `traefik-dynamic.yml` and follow the instructions inside.
353
+ To enable SSL, run `npx thepopebot setup-ssl` it configures Let's Encrypt wildcard cert via DNS challenge, sets up your domain, and switches to `docker-compose.custom.yml`. For additional Docker Compose customization (extra services, volumes, ports), edit `docker-compose.custom.yml` directly.
355
354
 
356
355
  Skills in `skills/` are activated by symlinking into `skills/active/`. Both `.pi/skills` and `.claude/skills` point to `skills/active/`. Scripts for command-type actions go in `cron/` and `triggers/`.
357
356
 
@@ -30,7 +30,7 @@ An autonomous AI agent powered by [thepopebot](https://github.com/stephengpope/t
30
30
  | `config/agent-job/AGENT_JOB.md` | Agent runtime environment docs |
31
31
  | `config/CRONS.json` | Scheduled job definitions |
32
32
  | `config/TRIGGERS.json` | Webhook trigger definitions |
33
- | `.env` | API keys, tokens, and settings |
33
+ | `.env` | Infrastructure settings (APP_URL, GH_OWNER, etc.) |
34
34
 
35
35
  See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for the complete list.
36
36
 
@@ -1,7 +1,17 @@
1
- # Custom Docker Compose configuration
1
+ # Custom Docker Compose — SSL with Let's Encrypt wildcard cert
2
2
  # This file is NOT managed by thepopebot and won't be overwritten on upgrade.
3
- # To activate: set COMPOSE_FILE=docker-compose.custom.yml in .env
4
- # Make your customizations here (TLS, ports, volumes, extra services, etc.)
3
+ #
4
+ # Activate: set COMPOSE_FILE=docker-compose.custom.yml in .env
5
+ # Setup: npx thepopebot setup-ssl
6
+ #
7
+ # Requires these .env vars (set by setup-ssl):
8
+ # SSL_DOMAIN — your domain (e.g., bot.example.com)
9
+ # SSL_EMAIL — email for Let's Encrypt notifications
10
+ # SSL_DNS_PROVIDER — Traefik DNS provider name (e.g., cloudflare)
11
+ # Provider API creds — standard env var names (e.g., CF_DNS_API_TOKEN for Cloudflare)
12
+ #
13
+ # DNS: *.${SSL_DOMAIN} must resolve to this server (A record or CNAME).
14
+ # The setup-ssl command can create this record for you.
5
15
 
6
16
  services:
7
17
  traefik:
@@ -9,20 +19,22 @@ services:
9
19
  command:
10
20
  - --providers.docker=true
11
21
  - --providers.docker.exposedByDefault=false
22
+ - --providers.file.directory=/traefik-config/
12
23
  - --entrypoints.web.address=:80
13
24
  - --entrypoints.websecure.address=:443
14
- ## Uncomment the following lines to enable TLS via Let's Encrypt
15
- ## (requires LETSENCRYPT_EMAIL in .env):
16
- # - --entrypoints.web.http.redirections.entrypoint.to=websecure
17
- # - --entrypoints.web.http.redirections.entrypoint.scheme=https
18
- # - --certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}
19
- # - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
20
- # - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
25
+ - --entrypoints.web.http.redirections.entrypoint.to=websecure
26
+ - --entrypoints.web.http.redirections.entrypoint.scheme=https
27
+ - --certificatesresolvers.letsencrypt.acme.email=${SSL_EMAIL}
28
+ - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
29
+ - --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=${SSL_DNS_PROVIDER}
30
+ - --certificatesresolvers.letsencrypt.acme.dnschallenge.resolvers=8.8.8.8:53,8.8.4.4:53
31
+ env_file: .env
21
32
  ports:
22
33
  - "80:80"
23
34
  - "443:443"
24
35
  volumes:
25
36
  - /var/run/docker.sock:/var/run/docker.sock:ro
37
+ - ./traefik-config:/traefik-config:ro
26
38
  - traefik_certs:/letsencrypt
27
39
  restart: unless-stopped
28
40
 
@@ -39,16 +51,16 @@ services:
39
51
  - ./cron:/app/cron
40
52
  - ./triggers:/app/triggers
41
53
  - ./CLAUDE.md:/app/CLAUDE.md
54
+ - ./traefik-config:/traefik-config
42
55
  - /var/run/docker.sock:/var/run/docker.sock
56
+ environment:
57
+ - TRAEFIK_CONFIG_DIR=/traefik-config
43
58
  labels:
44
59
  - traefik.enable=true
45
- # Set APP_HOSTNAME in .env to the domain from APP_URL (e.g., mybot.example.com)
46
60
  - traefik.http.routers.event-handler.rule=Host(`${APP_HOSTNAME}`)
47
- - traefik.http.routers.event-handler.entrypoints=web
61
+ - traefik.http.routers.event-handler.entrypoints=websecure
62
+ - traefik.http.routers.event-handler.tls.certresolver=letsencrypt
48
63
  - traefik.http.services.event-handler.loadbalancer.server.port=80
49
- ## Uncomment the following lines to enable TLS via Let's Encrypt:
50
- # - traefik.http.routers.event-handler.entrypoints=websecure
51
- # - traefik.http.routers.event-handler.tls.certresolver=letsencrypt
52
64
  stop_grace_period: 120s
53
65
  healthcheck:
54
66
  test: ["CMD", "curl", "-f", "http://localhost:80/api/ping"]
@@ -58,6 +70,13 @@ services:
58
70
  start_period: 30s
59
71
  restart: unless-stopped
60
72
 
73
+ litellm:
74
+ image: ghcr.io/berriai/litellm:main-latest
75
+ command: --config /litellm/main.yaml --port 4000
76
+ volumes:
77
+ - ./config/litellm:/litellm:ro
78
+ restart: unless-stopped
79
+
61
80
  runner:
62
81
  image: myoung34/github-runner:latest
63
82
  deploy:
@@ -68,6 +87,7 @@ services:
68
87
  RUNNER_SCOPE: repo
69
88
  LABELS: self-hosted
70
89
  EPHEMERAL: "1"
90
+ DISABLE_AUTO_UPDATE: "true"
71
91
  volumes:
72
92
  - /var/run/docker.sock:/var/run/docker.sock
73
93
  - .:/project
@@ -75,46 +95,3 @@ services:
75
95
 
76
96
  volumes:
77
97
  traefik_certs:
78
-
79
- # ──────────────────────────────────────────────────────────────────────
80
- # Tailscale TLS Example
81
- # ──────────────────────────────────────────────────────────────────────
82
- # To use Tailscale HTTPS certs instead of Let's Encrypt:
83
- #
84
- # 1. On your Tailscale machine, run:
85
- # sudo tailscale cert <your-machine-name>.<tailnet>.ts.net
86
- # This creates .crt and .key files in /var/lib/tailscale/certs/
87
- #
88
- # 2. Copy traefik-dynamic.yml.example to traefik-dynamic.yml and
89
- # replace YOUR_HOSTNAME with your full Tailscale machine name.
90
- #
91
- # 3. Replace the traefik service above with:
92
- #
93
- # traefik:
94
- # image: traefik:v3
95
- # command:
96
- # - --providers.docker=true
97
- # - --providers.docker.exposedByDefault=false
98
- # - --providers.file.filename=/etc/traefik/dynamic.yml
99
- # - --entrypoints.web.address=:80
100
- # - --entrypoints.web.http.redirections.entrypoint.to=websecure
101
- # - --entrypoints.web.http.redirections.entrypoint.scheme=https
102
- # - --entrypoints.websecure.address=:443
103
- # ports:
104
- # - "80:80"
105
- # - "443:443"
106
- # volumes:
107
- # - /var/run/docker.sock:/var/run/docker.sock:ro
108
- # - ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
109
- # - /var/lib/tailscale/certs:/certs:ro
110
- # restart: unless-stopped
111
- #
112
- # 4. Update the event-handler labels:
113
- #
114
- # event-handler:
115
- # labels:
116
- # - traefik.enable=true
117
- # - traefik.http.routers.event-handler.rule=Host(`${APP_HOSTNAME}`)
118
- # - traefik.http.routers.event-handler.entrypoints=websecure
119
- # - traefik.http.routers.event-handler.tls=true
120
- # - traefik.http.services.event-handler.loadbalancer.server.port=80
@@ -4,21 +4,13 @@ services:
4
4
  command:
5
5
  - --providers.docker=true
6
6
  - --providers.docker.exposedByDefault=false
7
+ - --providers.file.directory=/traefik-config/
7
8
  - --entrypoints.web.address=:80
8
- - --entrypoints.websecure.address=:443
9
- ## Uncomment the following lines to enable TLS via Let's Encrypt
10
- ## (requires LETSENCRYPT_EMAIL in .env):
11
- # - --entrypoints.web.http.redirections.entrypoint.to=websecure
12
- # - --entrypoints.web.http.redirections.entrypoint.scheme=https
13
- # - --certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}
14
- # - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
15
- # - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
16
9
  ports:
17
10
  - "80:80"
18
- - "443:443"
19
11
  volumes:
20
12
  - /var/run/docker.sock:/var/run/docker.sock:ro
21
- - traefik_certs:/letsencrypt
13
+ - ./traefik-config:/traefik-config:ro
22
14
  restart: unless-stopped
23
15
 
24
16
  event-handler:
@@ -34,16 +26,15 @@ services:
34
26
  - ./cron:/app/cron
35
27
  - ./triggers:/app/triggers
36
28
  - ./CLAUDE.md:/app/CLAUDE.md
29
+ - ./traefik-config:/traefik-config
37
30
  - /var/run/docker.sock:/var/run/docker.sock
31
+ environment:
32
+ - TRAEFIK_CONFIG_DIR=/traefik-config
38
33
  labels:
39
34
  - traefik.enable=true
40
- # Set APP_HOSTNAME in .env to the domain from APP_URL (e.g., mybot.example.com)
41
35
  - traefik.http.routers.event-handler.rule=Host(`${APP_HOSTNAME}`)
42
36
  - traefik.http.routers.event-handler.entrypoints=web
43
37
  - traefik.http.services.event-handler.loadbalancer.server.port=80
44
- ## Uncomment the following lines to enable TLS via Let's Encrypt:
45
- # - traefik.http.routers.event-handler.entrypoints=websecure
46
- # - traefik.http.routers.event-handler.tls.certresolver=letsencrypt
47
38
  stop_grace_period: 120s
48
39
  healthcheck:
49
40
  test: ["CMD", "curl", "-f", "http://localhost:80/api/ping"]
@@ -53,6 +44,13 @@ services:
53
44
  start_period: 30s
54
45
  restart: unless-stopped
55
46
 
47
+ litellm:
48
+ image: ghcr.io/berriai/litellm:main-latest
49
+ command: --config /litellm/main.yaml --port 4000
50
+ volumes:
51
+ - ./config/litellm:/litellm:ro
52
+ restart: unless-stopped
53
+
56
54
  runner:
57
55
  image: myoung34/github-runner:latest
58
56
  deploy:
@@ -68,6 +66,3 @@ services:
68
66
  - /var/run/docker.sock:/var/run/docker.sock
69
67
  - .:/project
70
68
  restart: unless-stopped
71
-
72
- volumes:
73
- traefik_certs:
@@ -29,7 +29,7 @@ These set GitHub repository secrets/variables using the `gh` CLI. They read `GH_
29
29
  |---------|-------------|
30
30
  | `set-var KEY [VALUE]` | Set a GitHub repository variable |
31
31
 
32
- Agent job secrets are now managed through the admin UI (Settings > Agent Jobs > Secrets), stored encrypted in SQLite, and injected directly into Docker containers.
32
+ Agent job secrets are managed at Admin > Event Handler > Agent Jobs, stored encrypted in SQLite, and injected directly into Docker containers.
33
33
 
34
34
  ## Common Workflows
35
35
 
@@ -54,6 +54,6 @@ npx thepopebot reset config/CRONS.json # accept new template
54
54
  ### Set up a new LLM provider for jobs
55
55
  ```bash
56
56
  npx thepopebot set-var LLM_PROVIDER openai
57
- npx thepopebot set-var LLM_MODEL gpt-4o
58
- # Set API keys via admin UI: Settings > Agent Jobs > Secrets
57
+ npx thepopebot set-var LLM_MODEL gpt-5.4
58
+ # Set API keys via admin UI: Admin > Event Handler > LLMs
59
59
  ```