thepopebot 1.2.75-beta.2 → 1.2.75-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/api/CLAUDE.md +1 -1
- package/api/index.js +5 -12
- package/bin/CLAUDE.md +1 -1
- package/bin/cli.js +329 -14
- package/bin/docker-build.js +5 -0
- package/bin/managed-paths.js +0 -7
- package/bin/sync.js +84 -0
- package/config/CLAUDE.md +1 -29
- package/config/instrumentation.js +1 -1
- package/lib/CLAUDE.md +3 -3
- package/lib/ai/CLAUDE.md +24 -3
- package/lib/ai/agent.js +8 -5
- package/lib/ai/async-channel.js +51 -0
- package/lib/ai/headless-stream.js +3 -0
- package/lib/ai/index.js +149 -173
- package/lib/ai/line-mappers.js +72 -9
- package/lib/ai/tools.js +40 -28
- package/lib/chat/actions.js +34 -6
- package/lib/chat/api.js +17 -1
- package/lib/chat/components/chat-header.js +4 -0
- package/lib/chat/components/chat-header.jsx +4 -0
- package/lib/chat/components/chat-input.js +1 -0
- package/lib/chat/components/chat-input.jsx +1 -0
- package/lib/chat/components/chat.js +9 -1
- package/lib/chat/components/chat.jsx +15 -2
- package/lib/chat/components/chats-page.js +3 -3
- package/lib/chat/components/chats-page.jsx +4 -6
- package/lib/chat/components/crons-page.js +1 -1
- package/lib/chat/components/crons-page.jsx +1 -1
- package/lib/chat/components/message.js +12 -4
- package/lib/chat/components/message.jsx +17 -4
- package/lib/chat/components/settings-chat-page.js +2 -1
- package/lib/chat/components/settings-chat-page.jsx +4 -1
- package/lib/chat/components/settings-coding-agents-page.js +139 -1
- package/lib/chat/components/settings-coding-agents-page.jsx +160 -0
- package/lib/chat/components/settings-jobs-page.js +13 -2
- package/lib/chat/components/settings-jobs-page.jsx +15 -1
- package/lib/chat/components/settings-secrets-layout.js +1 -1
- package/lib/chat/components/settings-secrets-layout.jsx +1 -1
- package/lib/chat/components/sidebar-history-item.js +3 -3
- package/lib/chat/components/sidebar-history-item.jsx +4 -6
- package/lib/chat/components/triggers-page.js +1 -1
- package/lib/chat/components/triggers-page.jsx +1 -1
- package/lib/cluster/actions.js +4 -4
- package/lib/cluster/execute.js +3 -1
- package/lib/code/actions.js +34 -11
- package/lib/code/code-page.js +40 -40
- package/lib/code/code-page.jsx +36 -36
- package/lib/code/port-forwards.js +17 -3
- package/lib/code/terminal-view.js +16 -0
- package/lib/code/terminal-view.jsx +18 -0
- package/lib/config.js +4 -0
- package/lib/cron.js +3 -3
- package/lib/db/api-keys.js +22 -61
- package/lib/db/config.js +23 -0
- package/lib/db/index.js +3 -1
- package/lib/maintenance.js +34 -11
- package/lib/paths.js +1 -38
- package/lib/tools/create-agent-job.js +0 -4
- package/lib/tools/docker.js +23 -16
- package/lib/triggers.js +4 -3
- package/lib/utils/render-md.js +3 -1
- package/package.json +2 -1
- package/setup/setup-ssl.mjs +414 -0
- package/templates/.github/workflows/rebuild-event-handler.yml +3 -0
- package/templates/.github/workflows/upgrade-event-handler.yml +1 -1
- package/templates/.gitignore.template +7 -3
- package/templates/.tmp/CLAUDE.md.template +5 -0
- package/templates/CLAUDE.md +3 -2
- package/templates/CLAUDE.md.template +24 -357
- package/templates/agent-job/CLAUDE.md.template +57 -0
- package/templates/agent-job/CRONS.json +16 -0
- package/templates/{config/agent-job → agent-job}/SOUL.md +3 -3
- package/templates/agent-job/SYSTEM.md +60 -0
- package/templates/agents/CLAUDE.md.template +54 -0
- package/templates/data/CLAUDE.md.template +5 -0
- package/templates/docker-compose.custom.yml +41 -62
- package/templates/docker-compose.yml +14 -21
- package/templates/event-handler/CLAUDE.md.template +0 -0
- package/templates/logs/CLAUDE.md.template +5 -0
- package/templates/skills/CLAUDE.md.template +57 -32
- package/templates/skills/active/.gitkeep +0 -0
- package/templates/skills/library/agent-job-secrets/SKILL.md +23 -0
- package/templates/skills/library/agent-job-secrets/agent-job-secrets.js +62 -0
- package/templates/.pi/extensions/env-sanitizer/index.ts +0 -48
- package/templates/.pi/extensions/env-sanitizer/package.json +0 -5
- package/templates/README.md +0 -75
- package/templates/config/CLAUDE.md.template +0 -40
- package/templates/config/CRONS.json +0 -56
- package/templates/config/agent-job/AGENT_JOB.md +0 -30
- package/templates/cron/CLAUDE.md.template +0 -24
- package/templates/docker-compose.litellm.yml +0 -82
- package/templates/docs/CLAUDE.md.template +0 -12
- package/templates/docs/CLI.md +0 -59
- package/templates/docs/CLUSTERS.md +0 -151
- package/templates/docs/CONFIGURATION.md +0 -181
- package/templates/docs/CRONS_AND_TRIGGERS.md +0 -132
- package/templates/docs/GETTING_STARTED.md +0 -64
- package/templates/docs/SECURITY.md +0 -61
- package/templates/docs/SKILLS.md +0 -113
- package/templates/docs/UPGRADING.md +0 -92
- package/templates/skills/LICENSE +0 -21
- package/templates/skills/README.md +0 -117
- package/templates/skills/agent-job-secrets/SKILL.md +0 -25
- package/templates/skills/agent-job-secrets/agent-job-secrets.js +0 -66
- package/templates/traefik-dynamic.yml.example +0 -7
- package/templates/triggers/CLAUDE.md.template +0 -41
- /package/templates/{config → agent-job}/HEARTBEAT.md +0 -0
- /package/templates/{cron → data}/.gitkeep +0 -0
- /package/templates/{logs → data/clusters}/.gitkeep +0 -0
- /package/templates/{triggers → data/db}/.gitkeep +0 -0
- /package/templates/{config/agent-job → event-handler}/SUMMARY.md +0 -0
- /package/templates/{config → event-handler}/TRIGGERS.json +0 -0
- /package/templates/{config → event-handler}/agent-chat/SYSTEM.md +0 -0
- /package/templates/{config/cluster → event-handler/clusters}/ROLE.md +0 -0
- /package/templates/{config/cluster → event-handler/clusters}/SYSTEM.md +0 -0
- /package/templates/{config → event-handler}/code-chat/SYSTEM.md +0 -0
- /package/templates/{config → event-handler}/litellm/main.yaml +0 -0
- /package/templates/skills/{playwright-cli → library/playwright-cli}/SKILL.md +0 -0
package/lib/paths.js
CHANGED
|
@@ -1,43 +1,6 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Central path resolver for thepopebot.
|
|
5
3
|
* All paths resolve from process.cwd() (the user's project root).
|
|
6
4
|
*/
|
|
7
5
|
|
|
8
|
-
const PROJECT_ROOT = process.cwd();
|
|
9
|
-
|
|
10
|
-
export {
|
|
11
|
-
PROJECT_ROOT,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
// config/ files
|
|
15
|
-
export const configDir = path.join(PROJECT_ROOT, 'config');
|
|
16
|
-
export const cronsFile = path.join(PROJECT_ROOT, 'config', 'CRONS.json');
|
|
17
|
-
export const triggersFile = path.join(PROJECT_ROOT, 'config', 'TRIGGERS.json');
|
|
18
|
-
export const agentJobPlanningMd = path.join(PROJECT_ROOT, 'config', 'agent-chat', 'SYSTEM.md');
|
|
19
|
-
export const codePlanningMd = path.join(PROJECT_ROOT, 'config', 'code-chat', 'SYSTEM.md');
|
|
20
|
-
export const agentJobSummaryMd = path.join(PROJECT_ROOT, 'config', 'agent-job', 'SUMMARY.md');
|
|
21
|
-
export const claudeMd = path.join(PROJECT_ROOT, 'CLAUDE.md');
|
|
22
|
-
|
|
23
|
-
// Skills directory
|
|
24
|
-
export const skillsDir = path.join(PROJECT_ROOT, 'skills');
|
|
25
|
-
|
|
26
|
-
// Working directories for command-type actions
|
|
27
|
-
export const cronDir = path.join(PROJECT_ROOT, 'cron');
|
|
28
|
-
export const triggersDir = path.join(PROJECT_ROOT, 'triggers');
|
|
29
|
-
|
|
30
|
-
// Logs
|
|
31
|
-
export const logsDir = path.join(PROJECT_ROOT, 'logs');
|
|
32
|
-
|
|
33
|
-
// Database
|
|
34
|
-
export const thepopebotDb = process.env.DATABASE_PATH || path.join(PROJECT_ROOT, 'data', 'db', 'thepopebot.sqlite');
|
|
35
|
-
|
|
36
|
-
// Cluster data (bind-mount root for cluster containers)
|
|
37
|
-
export const clusterDataDir = process.env.CLUSTER_DATA_PATH || path.join(PROJECT_ROOT, 'data', 'clusters');
|
|
38
|
-
|
|
39
|
-
// Code workspace data (bind-mount root for workspace containers)
|
|
40
|
-
export const workspacesDir = path.join(PROJECT_ROOT, 'data', 'workspaces');
|
|
41
|
-
|
|
42
|
-
// .env
|
|
43
|
-
export const envFile = path.join(PROJECT_ROOT, '.env');
|
|
6
|
+
export const PROJECT_ROOT = process.cwd();
|
|
@@ -3,8 +3,6 @@ import { z } from 'zod';
|
|
|
3
3
|
import { githubApi } from './github.js';
|
|
4
4
|
import { createModel } from '../ai/model.js';
|
|
5
5
|
import { getConfig } from '../config.js';
|
|
6
|
-
import { createAgentJobApiKey } from '../db/api-keys.js';
|
|
7
|
-
|
|
8
6
|
/**
|
|
9
7
|
* Generate a short descriptive title for an agent job using the LLM.
|
|
10
8
|
* Uses structured output to avoid thinking-token leaks with extended-thinking models.
|
|
@@ -93,7 +91,6 @@ async function createAgentJob(agentJobDescription, options = {}) {
|
|
|
93
91
|
|
|
94
92
|
// 6. Launch Docker container locally (fire-and-forget with async cleanup)
|
|
95
93
|
const repoSlug = `${GH_OWNER}/${GH_REPO}`;
|
|
96
|
-
const { key: agentJobToken } = createAgentJobApiKey(agentJobId);
|
|
97
94
|
launchAgentJobContainer({
|
|
98
95
|
agentJobId,
|
|
99
96
|
repo: repoSlug,
|
|
@@ -102,7 +99,6 @@ async function createAgentJob(agentJobDescription, options = {}) {
|
|
|
102
99
|
description: agentJobDescription,
|
|
103
100
|
codingAgent: options.agentBackend,
|
|
104
101
|
llmModel: options.llmModel,
|
|
105
|
-
agentJobToken,
|
|
106
102
|
}).catch(err => {
|
|
107
103
|
console.error(`[agent-job] Failed to launch container for ${agentJobId}:`, err.message);
|
|
108
104
|
});
|
package/lib/tools/docker.js
CHANGED
|
@@ -4,7 +4,9 @@ import path from 'path';
|
|
|
4
4
|
import { getConfig } from '../config.js';
|
|
5
5
|
import { getCustomProvider } from '../db/config.js';
|
|
6
6
|
import { BUILTIN_PROVIDERS } from '../llm-providers.js';
|
|
7
|
-
import {
|
|
7
|
+
import { PROJECT_ROOT } from '../paths.js';
|
|
8
|
+
|
|
9
|
+
const workspacesDir = path.join(PROJECT_ROOT, 'data/workspaces');
|
|
8
10
|
|
|
9
11
|
// UID/GID of the coding-agent user inside agent containers (pinned in Dockerfile)
|
|
10
12
|
const CODING_AGENT_UID = 1001;
|
|
@@ -231,9 +233,12 @@ async function runInteractiveContainer({ containerName, repo, branch, codingAgen
|
|
|
231
233
|
env.push(`${key}=${value}`);
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
236
|
+
// Create per-container API key for agent-secrets access
|
|
237
|
+
const { createAgentJobApiKey } = await import('../db/api-keys.js');
|
|
238
|
+
const { key: agentJobToken } = createAgentJobApiKey(containerName);
|
|
239
|
+
env.push(`AGENT_JOB_TOKEN=${agentJobToken}`);
|
|
240
|
+
const appUrl = getConfig('APP_URL');
|
|
241
|
+
if (appUrl) env.push(`APP_URL=${appUrl}`);
|
|
237
242
|
}
|
|
238
243
|
|
|
239
244
|
const hostConfig = {};
|
|
@@ -334,9 +339,9 @@ function buildAgentAuthEnv(agent) {
|
|
|
334
339
|
}
|
|
335
340
|
}
|
|
336
341
|
}
|
|
337
|
-
} else if (agent === 'pi-coding-agent' || agent === 'opencode') {
|
|
338
|
-
// Pi and
|
|
339
|
-
const configPrefix = agent === 'opencode' ? 'CODING_AGENT_OPENCODE' : 'CODING_AGENT_PI';
|
|
342
|
+
} else if (agent === 'pi-coding-agent' || agent === 'opencode' || agent === 'kimi-cli') {
|
|
343
|
+
// Pi, OpenCode, and Kimi share the same multi-provider auth pattern
|
|
344
|
+
const configPrefix = agent === 'opencode' ? 'CODING_AGENT_OPENCODE' : agent === 'kimi-cli' ? 'CODING_AGENT_KIMI_CLI' : 'CODING_AGENT_PI';
|
|
340
345
|
const provider = getConfig(`${configPrefix}_PROVIDER`) || 'anthropic';
|
|
341
346
|
backendApi = provider;
|
|
342
347
|
const model = getConfig(`${configPrefix}_MODEL`);
|
|
@@ -438,9 +443,12 @@ async function runHeadlessContainer({ containerName, repo, branch, featureBranch
|
|
|
438
443
|
env.push(`${key}=${value}`);
|
|
439
444
|
}
|
|
440
445
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
446
|
+
// Create per-container API key for agent-secrets access
|
|
447
|
+
const { createAgentJobApiKey } = await import('../db/api-keys.js');
|
|
448
|
+
const { key: agentJobToken } = createAgentJobApiKey(containerName);
|
|
449
|
+
env.push(`AGENT_JOB_TOKEN=${agentJobToken}`);
|
|
450
|
+
const appUrl = getConfig('APP_URL');
|
|
451
|
+
if (appUrl) env.push(`APP_URL=${appUrl}`);
|
|
444
452
|
}
|
|
445
453
|
|
|
446
454
|
const hostConfig = {};
|
|
@@ -851,7 +859,7 @@ async function removeVolume(name) {
|
|
|
851
859
|
* @param {string} [options.llmModel] - Model override
|
|
852
860
|
* @returns {Promise<{containerId: string, containerName: string, volumeName: string}>}
|
|
853
861
|
*/
|
|
854
|
-
async function runAgentJobContainer({ agentJobId, repo, branch, title, description, codingAgent, llmModel
|
|
862
|
+
async function runAgentJobContainer({ agentJobId, repo, branch, title, description, codingAgent, llmModel }) {
|
|
855
863
|
const agent = codingAgent || getConfig('CODING_AGENT') || 'claude-code';
|
|
856
864
|
const version = process.env.THEPOPEBOT_VERSION;
|
|
857
865
|
const image = `stephengpope/thepopebot:coding-agent-${agent}-${version}`;
|
|
@@ -889,8 +897,10 @@ async function runAgentJobContainer({ agentJobId, repo, branch, title, descripti
|
|
|
889
897
|
const appUrl = getConfig('APP_URL');
|
|
890
898
|
if (appUrl) env.push(`APP_URL=${appUrl}`);
|
|
891
899
|
|
|
892
|
-
//
|
|
893
|
-
|
|
900
|
+
// Create per-container API key for agent-secrets access
|
|
901
|
+
const { createAgentJobApiKey } = await import('../db/api-keys.js');
|
|
902
|
+
const { key: agentJobToken } = createAgentJobApiKey(containerName);
|
|
903
|
+
env.push(`AGENT_JOB_TOKEN=${agentJobToken}`);
|
|
894
904
|
|
|
895
905
|
// Inject agent job secrets (plain secrets as env vars; oauth types are null — agent must fetch via get)
|
|
896
906
|
const { getAllAgentJobSecrets } = await import('../db/config.js');
|
|
@@ -900,9 +910,6 @@ async function runAgentJobContainer({ agentJobId, repo, branch, title, descripti
|
|
|
900
910
|
env.push(`${key}=${value}`);
|
|
901
911
|
}
|
|
902
912
|
}
|
|
903
|
-
if (jobSecrets.length > 0) {
|
|
904
|
-
env.push(`AGENT_JOB_SECRETS=${JSON.stringify(Object.fromEntries(jobSecrets.map(s => [s.key, s.value])))}`);
|
|
905
|
-
}
|
|
906
913
|
|
|
907
914
|
console.log(`[agent-job] id=${shortId} agent=${agent} image=${image} backendApi=${backendApi}`);
|
|
908
915
|
|
package/lib/triggers.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { PROJECT_ROOT } from './paths.js';
|
|
3
4
|
import { executeAction } from './actions.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -29,7 +30,7 @@ async function executeActions(trigger, context) {
|
|
|
29
30
|
const resolved = { ...action };
|
|
30
31
|
if (resolved.command) resolved.command = resolveTemplate(resolved.command, context);
|
|
31
32
|
if (resolved.job) resolved.job = resolveTemplate(resolved.job, context);
|
|
32
|
-
const result = await executeAction(resolved, { cwd:
|
|
33
|
+
const result = await executeAction(resolved, { cwd: PROJECT_ROOT, data: context.body });
|
|
33
34
|
console.log(`[TRIGGER] ${trigger.name}: ${result || 'ran'}`);
|
|
34
35
|
} catch (err) {
|
|
35
36
|
console.error(`[TRIGGER] ${trigger.name}: error - ${err.message}`);
|
|
@@ -42,7 +43,7 @@ async function executeActions(trigger, context) {
|
|
|
42
43
|
* @returns {{ triggerMap: Map, fireTriggers: Function }}
|
|
43
44
|
*/
|
|
44
45
|
function loadTriggers() {
|
|
45
|
-
const triggerFile =
|
|
46
|
+
const triggerFile = path.join(PROJECT_ROOT, 'event-handler/TRIGGERS.json');
|
|
46
47
|
const triggerMap = new Map();
|
|
47
48
|
|
|
48
49
|
console.log('\n--- Triggers ---');
|
package/lib/utils/render-md.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { PROJECT_ROOT
|
|
3
|
+
import { PROJECT_ROOT } from '../paths.js';
|
|
4
|
+
|
|
5
|
+
const skillsDir = path.join(PROJECT_ROOT, 'skills');
|
|
4
6
|
|
|
5
7
|
const INCLUDE_PATTERN = /\{\{([^}]+\.md)\}\}/g;
|
|
6
8
|
const VARIABLE_PATTERN = /\{\{(datetime|skills)\}\}/gi;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thepopebot",
|
|
3
|
-
"version": "1.2.75-beta.
|
|
3
|
+
"version": "1.2.75-beta.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"./auth/actions": "./lib/auth/actions.js",
|
|
16
16
|
"./chat/api": "./lib/chat/api.js",
|
|
17
17
|
"./db": "./lib/db/index.js",
|
|
18
|
+
"./db/chats": "./lib/db/chats.js",
|
|
18
19
|
"./db/oauth-tokens": "./lib/db/oauth-tokens.js",
|
|
19
20
|
"./chat": "./lib/chat/components/index.js",
|
|
20
21
|
"./chat/actions": "./lib/chat/actions.js",
|
|
@@ -0,0 +1,414 @@
|
|
|
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 SSL config to .env, DNS provider credentials to .env.traefik,
|
|
9
|
+
* and switches COMPOSE_FILE to docker-compose.custom.yml.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import * as clack from '@clack/prompts';
|
|
15
|
+
import { updateEnvVariable } from './lib/auth.mjs';
|
|
16
|
+
import { loadEnvFile } from './lib/env.mjs';
|
|
17
|
+
|
|
18
|
+
function handleCancel(value) {
|
|
19
|
+
if (clack.isCancel(value)) {
|
|
20
|
+
clack.cancel('Setup cancelled.');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// DNS providers supported by Traefik's ACME DNS challenge (via lego).
|
|
27
|
+
// Each entry maps to the Traefik provider name and the env vars it expects.
|
|
28
|
+
const DNS_PROVIDERS = {
|
|
29
|
+
cloudflare: {
|
|
30
|
+
label: 'Cloudflare',
|
|
31
|
+
traefikName: 'cloudflare',
|
|
32
|
+
envVars: [
|
|
33
|
+
{ key: 'CF_DNS_API_TOKEN', label: 'Cloudflare API Token', secret: true },
|
|
34
|
+
],
|
|
35
|
+
apiCreate: createCloudflareRecord,
|
|
36
|
+
helpUrl: 'https://dash.cloudflare.com/profile/api-tokens',
|
|
37
|
+
helpSteps: [
|
|
38
|
+
'Go to your Cloudflare dashboard → Profile → API Tokens',
|
|
39
|
+
'Click "Create Token"',
|
|
40
|
+
'Use the "Edit zone DNS" template',
|
|
41
|
+
'Select the zone (domain) you want to use',
|
|
42
|
+
'Copy the token',
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
route53: {
|
|
46
|
+
label: 'AWS Route 53',
|
|
47
|
+
traefikName: 'route53',
|
|
48
|
+
envVars: [
|
|
49
|
+
{ key: 'AWS_ACCESS_KEY_ID', label: 'AWS Access Key ID', secret: false },
|
|
50
|
+
{ key: 'AWS_SECRET_ACCESS_KEY', label: 'AWS Secret Access Key', secret: true },
|
|
51
|
+
{ key: 'AWS_REGION', label: 'AWS Region', secret: false, default: 'us-east-1' },
|
|
52
|
+
],
|
|
53
|
+
apiCreate: null, // TODO: implement
|
|
54
|
+
helpUrl: 'https://console.aws.amazon.com/iam/',
|
|
55
|
+
helpSteps: [
|
|
56
|
+
'Create an IAM user with Route53 permissions',
|
|
57
|
+
'Generate access keys for the user',
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
digitalocean: {
|
|
61
|
+
label: 'DigitalOcean',
|
|
62
|
+
traefikName: 'digitalocean',
|
|
63
|
+
envVars: [
|
|
64
|
+
{ key: 'DO_AUTH_TOKEN', label: 'DigitalOcean API Token', secret: true },
|
|
65
|
+
],
|
|
66
|
+
apiCreate: null, // TODO: implement
|
|
67
|
+
helpUrl: 'https://cloud.digitalocean.com/account/api/tokens',
|
|
68
|
+
helpSteps: [
|
|
69
|
+
'Go to DigitalOcean → API → Tokens',
|
|
70
|
+
'Generate a new personal access token with read/write scope',
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
namecheap: {
|
|
74
|
+
label: 'Namecheap',
|
|
75
|
+
traefikName: 'namecheap',
|
|
76
|
+
envVars: [
|
|
77
|
+
{ key: 'NAMECHEAP_API_USER', label: 'Namecheap API User', secret: false },
|
|
78
|
+
{ key: 'NAMECHEAP_API_KEY', label: 'Namecheap API Key', secret: true },
|
|
79
|
+
],
|
|
80
|
+
apiCreate: null, // TODO: implement
|
|
81
|
+
helpUrl: 'https://www.namecheap.com/support/api/intro/',
|
|
82
|
+
helpSteps: [
|
|
83
|
+
'Enable API access in your Namecheap account',
|
|
84
|
+
'Whitelist your server IP',
|
|
85
|
+
'Copy your API key',
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
godaddy: {
|
|
89
|
+
label: 'GoDaddy',
|
|
90
|
+
traefikName: 'godaddy',
|
|
91
|
+
envVars: [
|
|
92
|
+
{ key: 'GODADDY_API_KEY', label: 'GoDaddy API Key', secret: true },
|
|
93
|
+
{ key: 'GODADDY_API_SECRET', label: 'GoDaddy API Secret', secret: true },
|
|
94
|
+
],
|
|
95
|
+
apiCreate: null, // TODO: implement
|
|
96
|
+
helpUrl: 'https://developer.godaddy.com/keys',
|
|
97
|
+
helpSteps: [
|
|
98
|
+
'Go to GoDaddy Developer Portal → API Keys',
|
|
99
|
+
'Create a Production key',
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
porkbun: {
|
|
103
|
+
label: 'Porkbun',
|
|
104
|
+
traefikName: 'porkbun',
|
|
105
|
+
envVars: [
|
|
106
|
+
{ key: 'PORKBUN_API_KEY', label: 'Porkbun API Key', secret: true },
|
|
107
|
+
{ key: 'PORKBUN_SECRET_API_KEY', label: 'Porkbun Secret Key', secret: true },
|
|
108
|
+
],
|
|
109
|
+
apiCreate: null, // TODO: implement
|
|
110
|
+
helpUrl: 'https://porkbun.com/account/api',
|
|
111
|
+
helpSteps: [
|
|
112
|
+
'Go to Porkbun → Account → API Access',
|
|
113
|
+
'Create an API key pair',
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
async function main() {
|
|
119
|
+
clack.intro('SSL Setup — Let\'s Encrypt wildcard cert');
|
|
120
|
+
|
|
121
|
+
const env = loadEnvFile() || {};
|
|
122
|
+
|
|
123
|
+
// ── Domain ──────────────────────────────────────────────────────────
|
|
124
|
+
const existingDomain = env.SSL_DOMAIN;
|
|
125
|
+
let domain;
|
|
126
|
+
|
|
127
|
+
if (existingDomain) {
|
|
128
|
+
clack.log.info(`Current domain: ${existingDomain}`);
|
|
129
|
+
const reconfig = handleCancel(await clack.confirm({
|
|
130
|
+
message: 'Change domain?',
|
|
131
|
+
initialValue: false,
|
|
132
|
+
}));
|
|
133
|
+
domain = reconfig ? null : existingDomain;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!domain) {
|
|
137
|
+
domain = handleCancel(await clack.text({
|
|
138
|
+
message: 'Enter your domain (e.g., bot.example.com):',
|
|
139
|
+
placeholder: 'bot.example.com',
|
|
140
|
+
validate: (input) => {
|
|
141
|
+
if (!input) return 'Domain is required';
|
|
142
|
+
if (!input.includes('.')) return 'Enter a valid domain';
|
|
143
|
+
if (input.startsWith('*.')) return 'Enter the base domain without the wildcard (e.g., bot.example.com)';
|
|
144
|
+
},
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Email ───────────────────────────────────────────────────────────
|
|
149
|
+
const existingEmail = env.SSL_EMAIL;
|
|
150
|
+
let email;
|
|
151
|
+
|
|
152
|
+
if (existingEmail) {
|
|
153
|
+
clack.log.info(`Current email: ${existingEmail}`);
|
|
154
|
+
const reconfig = handleCancel(await clack.confirm({
|
|
155
|
+
message: 'Change email?',
|
|
156
|
+
initialValue: false,
|
|
157
|
+
}));
|
|
158
|
+
email = reconfig ? null : existingEmail;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!email) {
|
|
162
|
+
email = handleCancel(await clack.text({
|
|
163
|
+
message: 'Email for Let\'s Encrypt notifications:',
|
|
164
|
+
validate: (input) => {
|
|
165
|
+
if (!input) return 'Email is required';
|
|
166
|
+
if (!input.includes('@')) return 'Enter a valid email';
|
|
167
|
+
},
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── DNS Provider ────────────────────────────────────────────────────
|
|
172
|
+
const providerKey = handleCancel(await clack.select({
|
|
173
|
+
message: 'Who manages your DNS?',
|
|
174
|
+
options: Object.entries(DNS_PROVIDERS).map(([key, p]) => ({
|
|
175
|
+
label: p.label,
|
|
176
|
+
value: key,
|
|
177
|
+
})),
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
const provider = DNS_PROVIDERS[providerKey];
|
|
181
|
+
|
|
182
|
+
// Show help steps for getting the API credentials
|
|
183
|
+
clack.log.info(
|
|
184
|
+
`To get your ${provider.label} API credentials:\n` +
|
|
185
|
+
provider.helpSteps.map((s, i) => ` ${i + 1}. ${s}`).join('\n') +
|
|
186
|
+
`\n\n ${provider.helpUrl}`
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// ── API Credentials ─────────────────────────────────────────────────
|
|
190
|
+
const credentials = {};
|
|
191
|
+
for (const envVar of provider.envVars) {
|
|
192
|
+
const existing = env[envVar.key];
|
|
193
|
+
if (existing) {
|
|
194
|
+
const masked = '****' + existing.slice(-4);
|
|
195
|
+
clack.log.info(`${envVar.label}: ${masked}`);
|
|
196
|
+
const reconfig = handleCancel(await clack.confirm({
|
|
197
|
+
message: `Change ${envVar.label}?`,
|
|
198
|
+
initialValue: false,
|
|
199
|
+
}));
|
|
200
|
+
if (!reconfig) {
|
|
201
|
+
credentials[envVar.key] = existing;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (envVar.secret) {
|
|
207
|
+
credentials[envVar.key] = handleCancel(await clack.password({
|
|
208
|
+
message: `${envVar.label}:`,
|
|
209
|
+
validate: (input) => {
|
|
210
|
+
if (!input && !envVar.default) return `${envVar.label} is required`;
|
|
211
|
+
},
|
|
212
|
+
}));
|
|
213
|
+
} else {
|
|
214
|
+
credentials[envVar.key] = handleCancel(await clack.text({
|
|
215
|
+
message: `${envVar.label}:`,
|
|
216
|
+
defaultValue: envVar.default || '',
|
|
217
|
+
placeholder: envVar.default || '',
|
|
218
|
+
validate: (input) => {
|
|
219
|
+
if (!input && !envVar.default) return `${envVar.label} is required`;
|
|
220
|
+
},
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
credentials[envVar.key] = credentials[envVar.key] || envVar.default;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── DNS Record Target ──────────────────────────────────────────────
|
|
227
|
+
const targetType = handleCancel(await clack.select({
|
|
228
|
+
message: `How should *.${domain} resolve?`,
|
|
229
|
+
options: [
|
|
230
|
+
{ label: 'IP address (VPS, bare metal, cloud VM)', value: 'A' },
|
|
231
|
+
{ label: 'CNAME (Tailscale hostname, another domain)', value: 'CNAME' },
|
|
232
|
+
],
|
|
233
|
+
}));
|
|
234
|
+
|
|
235
|
+
let recordValue;
|
|
236
|
+
if (targetType === 'A') {
|
|
237
|
+
// Try to detect public IP
|
|
238
|
+
let detectedIp;
|
|
239
|
+
try {
|
|
240
|
+
const resp = await fetch('https://api.ipify.org');
|
|
241
|
+
if (resp.ok) detectedIp = (await resp.text()).trim();
|
|
242
|
+
} catch {}
|
|
243
|
+
|
|
244
|
+
recordValue = handleCancel(await clack.text({
|
|
245
|
+
message: 'Server IP address:',
|
|
246
|
+
defaultValue: detectedIp || '',
|
|
247
|
+
placeholder: detectedIp || '203.0.113.10',
|
|
248
|
+
validate: (input) => {
|
|
249
|
+
if (!input) return 'IP address is required';
|
|
250
|
+
if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(input)) return 'Enter a valid IPv4 address';
|
|
251
|
+
},
|
|
252
|
+
}));
|
|
253
|
+
} else {
|
|
254
|
+
recordValue = handleCancel(await clack.text({
|
|
255
|
+
message: 'CNAME target (e.g., mybox.tailnet.ts.net):',
|
|
256
|
+
validate: (input) => {
|
|
257
|
+
if (!input) return 'CNAME target is required';
|
|
258
|
+
if (!input.includes('.')) return 'Enter a valid hostname';
|
|
259
|
+
},
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Create DNS Record ──────────────────────────────────────────────
|
|
264
|
+
const s = clack.spinner();
|
|
265
|
+
|
|
266
|
+
if (provider.apiCreate) {
|
|
267
|
+
s.start(`Creating *.${domain} ${targetType} record → ${recordValue}`);
|
|
268
|
+
try {
|
|
269
|
+
await provider.apiCreate(domain, targetType, recordValue, credentials);
|
|
270
|
+
s.stop(`Created *.${domain} → ${recordValue}`);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
s.stop(`Failed to create DNS record: ${err.message}`);
|
|
273
|
+
clack.log.warn(
|
|
274
|
+
`Create this record manually in your ${provider.label} dashboard:\n` +
|
|
275
|
+
` Type: ${targetType}\n` +
|
|
276
|
+
` Name: *.${domain}\n` +
|
|
277
|
+
` Value: ${recordValue}`
|
|
278
|
+
);
|
|
279
|
+
const proceed = handleCancel(await clack.confirm({
|
|
280
|
+
message: 'Continue anyway? (you can create the record manually)',
|
|
281
|
+
initialValue: true,
|
|
282
|
+
}));
|
|
283
|
+
if (!proceed) process.exit(0);
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
clack.log.warn(
|
|
287
|
+
`Automatic DNS record creation is not yet supported for ${provider.label}.\n` +
|
|
288
|
+
` Create this record in your ${provider.label} dashboard:\n\n` +
|
|
289
|
+
` Type: ${targetType}\n` +
|
|
290
|
+
` Name: *.${domain}\n` +
|
|
291
|
+
` Value: ${recordValue}\n`
|
|
292
|
+
);
|
|
293
|
+
handleCancel(await clack.text({
|
|
294
|
+
message: 'Press enter once you\'ve created the record',
|
|
295
|
+
defaultValue: '',
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── Write .env ─────────────────────────────────────────────────────
|
|
300
|
+
s.start('Writing configuration to .env');
|
|
301
|
+
|
|
302
|
+
updateEnvVariable('SSL_DOMAIN', domain);
|
|
303
|
+
updateEnvVariable('SSL_EMAIL', email);
|
|
304
|
+
updateEnvVariable('SSL_DNS_PROVIDER', provider.traefikName);
|
|
305
|
+
|
|
306
|
+
// Set APP_HOSTNAME only if not already set (don't overwrite existing webhook hostname)
|
|
307
|
+
if (!env.APP_HOSTNAME) {
|
|
308
|
+
updateEnvVariable('APP_HOSTNAME', domain);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Write DNS provider credentials to .env.traefik (Traefik-only, not leaked to other services)
|
|
312
|
+
const traefikEnvPath = path.join(process.cwd(), '.env.traefik');
|
|
313
|
+
const traefikLines = provider.envVars.map((v) => `${v.key}=${credentials[v.key]}`);
|
|
314
|
+
fs.writeFileSync(traefikEnvPath, traefikLines.join('\n') + '\n');
|
|
315
|
+
|
|
316
|
+
// Switch to custom compose file
|
|
317
|
+
updateEnvVariable('COMPOSE_FILE', 'docker-compose.custom.yml');
|
|
318
|
+
|
|
319
|
+
s.stop('Configuration saved to .env and .env.traefik');
|
|
320
|
+
|
|
321
|
+
// ── Summary ────────────────────────────────────────────────────────
|
|
322
|
+
clack.log.success(
|
|
323
|
+
`SSL configured!\n\n` +
|
|
324
|
+
` Domain: ${domain}\n` +
|
|
325
|
+
` Wildcard: *.${domain}\n` +
|
|
326
|
+
` Provider: ${provider.label}\n` +
|
|
327
|
+
` Compose: docker-compose.custom.yml\n\n` +
|
|
328
|
+
` Traefik will automatically obtain and renew your wildcard cert.\n\n` +
|
|
329
|
+
` Next: restart your containers with \`docker compose up -d\``
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
clack.outro('Done!');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── Cloudflare API ─────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
async function createCloudflareRecord(domain, type, value, credentials) {
|
|
338
|
+
const token = credentials.CF_DNS_API_TOKEN;
|
|
339
|
+
|
|
340
|
+
// Find the zone — walk up the domain to find the registered zone.
|
|
341
|
+
// e.g., for "bot.example.com", the zone is "example.com"
|
|
342
|
+
const parts = domain.split('.');
|
|
343
|
+
let zoneId;
|
|
344
|
+
let zoneName;
|
|
345
|
+
|
|
346
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
347
|
+
const candidate = parts.slice(i).join('.');
|
|
348
|
+
const resp = await fetch(`https://api.cloudflare.com/client/v4/zones?name=${candidate}`, {
|
|
349
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
350
|
+
});
|
|
351
|
+
const data = await resp.json();
|
|
352
|
+
if (data.result?.length > 0) {
|
|
353
|
+
zoneId = data.result[0].id;
|
|
354
|
+
zoneName = data.result[0].name;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (!zoneId) {
|
|
360
|
+
throw new Error(`Could not find a Cloudflare zone for ${domain}. Make sure your domain is added to Cloudflare.`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Create the wildcard record: *.domain → value
|
|
364
|
+
const recordName = `*.${domain}`;
|
|
365
|
+
|
|
366
|
+
// Check if record already exists
|
|
367
|
+
const existingResp = await fetch(
|
|
368
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?type=${type}&name=${recordName}`,
|
|
369
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
370
|
+
);
|
|
371
|
+
const existing = await existingResp.json();
|
|
372
|
+
|
|
373
|
+
if (existing.result?.length > 0) {
|
|
374
|
+
// Update existing record
|
|
375
|
+
const recordId = existing.result[0].id;
|
|
376
|
+
const updateResp = await fetch(
|
|
377
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${recordId}`,
|
|
378
|
+
{
|
|
379
|
+
method: 'PUT',
|
|
380
|
+
headers: {
|
|
381
|
+
Authorization: `Bearer ${token}`,
|
|
382
|
+
'Content-Type': 'application/json',
|
|
383
|
+
},
|
|
384
|
+
body: JSON.stringify({ type, name: recordName, content: value, proxied: false }),
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
const updateData = await updateResp.json();
|
|
388
|
+
if (!updateData.success) {
|
|
389
|
+
throw new Error(updateData.errors?.[0]?.message || 'Failed to update DNS record');
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
// Create new record
|
|
393
|
+
const createResp = await fetch(
|
|
394
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`,
|
|
395
|
+
{
|
|
396
|
+
method: 'POST',
|
|
397
|
+
headers: {
|
|
398
|
+
Authorization: `Bearer ${token}`,
|
|
399
|
+
'Content-Type': 'application/json',
|
|
400
|
+
},
|
|
401
|
+
body: JSON.stringify({ type, name: recordName, content: value, proxied: false }),
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
const createData = await createResp.json();
|
|
405
|
+
if (!createData.success) {
|
|
406
|
+
throw new Error(createData.errors?.[0]?.message || 'Failed to create DNS record');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
main().catch((err) => {
|
|
412
|
+
clack.log.error(err.message);
|
|
413
|
+
process.exit(1);
|
|
414
|
+
});
|
|
@@ -48,6 +48,9 @@ jobs:
|
|
|
48
48
|
cd /project && npx --yes thepopebot@latest init --no-install
|
|
49
49
|
fi
|
|
50
50
|
|
|
51
|
+
# Install the new version into node_modules so local CLI stays current
|
|
52
|
+
cd /project && npm install
|
|
53
|
+
|
|
51
54
|
# Commit any template changes from init
|
|
52
55
|
git -C /project add -A
|
|
53
56
|
if ! git -C /project diff --cached --quiet; then
|
|
@@ -53,7 +53,7 @@ jobs:
|
|
|
53
53
|
git commit -m "chore: upgrade thepopebot to $NEW_VERSION"
|
|
54
54
|
git push origin "$BRANCH"
|
|
55
55
|
gh pr create --title "chore: upgrade thepopebot" --body "Automated upgrade via upgrade-event-handler workflow." --base main --head "$BRANCH"
|
|
56
|
-
gh pr merge "$BRANCH" --squash --
|
|
56
|
+
gh pr merge "$BRANCH" --squash --delete-branch
|
|
57
57
|
else
|
|
58
58
|
echo "No changes — thepopebot is already up to date."
|
|
59
59
|
fi
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
# Credentials - NEVER commit these
|
|
2
2
|
.env
|
|
3
3
|
.env.local
|
|
4
|
+
.env.traefik
|
|
4
5
|
*.pem
|
|
5
6
|
*.key
|
|
6
7
|
|
|
7
8
|
# Claude Code
|
|
8
9
|
.claude/*
|
|
9
10
|
|
|
11
|
+
# Playwright MCP
|
|
12
|
+
.playwright-mcp/
|
|
13
|
+
|
|
10
14
|
# Pi system prompt (generated at runtime from SOUL.md)
|
|
11
15
|
.pi/SYSTEM.md
|
|
12
16
|
|
|
13
|
-
#
|
|
14
|
-
skills
|
|
17
|
+
# Dynamically activated skills (created at container runtime, not user config)
|
|
18
|
+
skills/active/agent-job-secrets
|
|
15
19
|
|
|
16
20
|
# Database
|
|
17
|
-
data/
|
|
21
|
+
/data/
|
|
18
22
|
|
|
19
23
|
# Node
|
|
20
24
|
node_modules/
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# .tmp/ — Scratch Directory
|
|
2
|
+
|
|
3
|
+
Temporary working files — downloads, screenshots, snapshots, intermediate data, generated files. This directory is gitignored and nothing here gets committed.
|
|
4
|
+
|
|
5
|
+
Playwright MCP saves screenshots and snapshots here. Other tools and scripts should use this directory for any transient output that doesn't belong in the repo.
|