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.
- package/bin/cli.js +24 -4
- package/bin/docker-build.js +5 -0
- package/bin/sync.js +84 -0
- package/lib/ai/async-channel.js +51 -0
- package/lib/ai/index.js +154 -153
- package/lib/ai/tools.js +46 -30
- package/lib/chat/actions.js +12 -0
- package/lib/chat/components/settings-coding-agents-page.js +139 -1
- package/lib/chat/components/settings-coding-agents-page.jsx +160 -0
- package/lib/code/port-forwards.js +14 -3
- package/lib/config.js +4 -0
- package/lib/tools/docker.js +3 -3
- package/package.json +1 -1
- package/setup/setup-ssl.mjs +407 -0
- package/templates/CLAUDE.md.template +3 -4
- package/templates/README.md +1 -1
- package/templates/docker-compose.custom.yml +35 -58
- package/templates/docker-compose.yml +12 -17
- package/templates/docs/CLI.md +3 -3
- package/templates/docs/CONFIGURATION.md +31 -65
- package/templates/docs/GETTING_STARTED.md +1 -1
- package/templates/docs/SECURITY.md +3 -3
- package/templates/docs/SKILLS.md +2 -1
- package/templates/docker-compose.litellm.yml +0 -82
- package/templates/traefik-dynamic.yml.example +0 -7
|
@@ -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 #
|
|
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
|
|
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
|
|
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
|
|
package/templates/README.md
CHANGED
|
@@ -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` |
|
|
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
|
|
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
|
-
#
|
|
4
|
-
#
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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=
|
|
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
|
-
-
|
|
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:
|
package/templates/docs/CLI.md
CHANGED
|
@@ -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
|
|
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-
|
|
58
|
-
# Set API keys via admin UI:
|
|
57
|
+
npx thepopebot set-var LLM_MODEL gpt-5.4
|
|
58
|
+
# Set API keys via admin UI: Admin > Event Handler > LLMs
|
|
59
59
|
```
|