thepopebot 1.0.0
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/LICENSE +21 -0
- package/README.md +127 -0
- package/api/index.js +357 -0
- package/bin/cli.js +278 -0
- package/config/index.js +29 -0
- package/config/instrumentation.js +29 -0
- package/docker/Dockerfile +51 -0
- package/docker/entrypoint.sh +100 -0
- package/lib/actions.js +40 -0
- package/lib/claude/conversation.js +76 -0
- package/lib/claude/index.js +142 -0
- package/lib/claude/tools.js +54 -0
- package/lib/cron.js +60 -0
- package/lib/paths.js +30 -0
- package/lib/tools/create-job.js +40 -0
- package/lib/tools/github.js +122 -0
- package/lib/tools/openai.js +35 -0
- package/lib/tools/telegram.js +222 -0
- package/lib/triggers.js +105 -0
- package/lib/utils/render-md.js +39 -0
- package/package.json +57 -0
- package/pi/extensions/env-sanitizer/index.ts +48 -0
- package/pi/extensions/env-sanitizer/package.json +5 -0
- package/pi/skills/llm-secrets/SKILL.md +34 -0
- package/pi/skills/llm-secrets/llm-secrets.js +34 -0
- package/setup/lib/auth.mjs +160 -0
- package/setup/lib/github.mjs +148 -0
- package/setup/lib/prerequisites.mjs +135 -0
- package/setup/lib/prompts.mjs +268 -0
- package/setup/lib/telegram-verify.mjs +66 -0
- package/setup/lib/telegram.mjs +76 -0
- package/setup/package.json +6 -0
- package/setup/setup-telegram.mjs +236 -0
- package/setup/setup.mjs +540 -0
- package/templates/.env.example +38 -0
- package/templates/.github/workflows/auto-merge.yml +117 -0
- package/templates/.github/workflows/docker-build.yml +34 -0
- package/templates/.github/workflows/run-job.yml +40 -0
- package/templates/.github/workflows/update-event-handler.yml +126 -0
- package/templates/.pi/skills/modify-self/SKILL.md +12 -0
- package/templates/CLAUDE.md +52 -0
- package/templates/app/api/[...thepopebot]/route.js +1 -0
- package/templates/app/layout.js +12 -0
- package/templates/app/page.js +8 -0
- package/templates/instrumentation.js +1 -0
- package/templates/next.config.mjs +3 -0
- package/templates/operating_system/AGENT.md +32 -0
- package/templates/operating_system/CHATBOT.md +74 -0
- package/templates/operating_system/CRONS.json +16 -0
- package/templates/operating_system/HEARTBEAT.md +3 -0
- package/templates/operating_system/JOB_SUMMARY.md +36 -0
- package/templates/operating_system/SOUL.md +17 -0
- package/templates/operating_system/TELEGRAM.md +21 -0
- package/templates/operating_system/TRIGGERS.json +18 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run the chat ID verification flow
|
|
6
|
+
* @param {string} verificationCode - The code user should send to bot
|
|
7
|
+
* @returns {Promise<string|null>} - The chat ID or null if skipped/failed
|
|
8
|
+
*/
|
|
9
|
+
export async function runVerificationFlow(verificationCode) {
|
|
10
|
+
console.log(chalk.bold.yellow('\n Chat ID Verification\n'));
|
|
11
|
+
console.log(chalk.dim(' To lock the bot to your chat, send the verification code.\n'));
|
|
12
|
+
console.log(chalk.cyan(' Send this message to your bot: ') + chalk.bold(verificationCode));
|
|
13
|
+
console.log(chalk.dim('\n The bot will reply with your chat ID. Paste it below.\n'));
|
|
14
|
+
|
|
15
|
+
const { chatId } = await inquirer.prompt([{
|
|
16
|
+
type: 'input',
|
|
17
|
+
name: 'chatId',
|
|
18
|
+
message: 'Paste your chat ID from the bot (or press Enter to skip):',
|
|
19
|
+
validate: (input) => {
|
|
20
|
+
if (!input) return true; // Allow empty to skip
|
|
21
|
+
if (!/^-?\d+$/.test(input.trim())) {
|
|
22
|
+
return 'Chat ID should be a number (can be negative for groups)';
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}]);
|
|
27
|
+
|
|
28
|
+
return chatId.trim() || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Wait for server to pick up .env changes and verify it's running
|
|
33
|
+
* @param {string} ngrokUrl - The ngrok URL
|
|
34
|
+
* @param {string} apiKey - The API key for authentication
|
|
35
|
+
* @returns {Promise<boolean>} - True if verified successfully
|
|
36
|
+
*/
|
|
37
|
+
export async function verifyRestart(ngrokUrl, apiKey) {
|
|
38
|
+
console.log(chalk.dim('\n Waiting for server to pick up changes...\n'));
|
|
39
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
40
|
+
|
|
41
|
+
// Verify server is up
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetch(`${ngrokUrl}/api/ping`, {
|
|
44
|
+
method: 'GET',
|
|
45
|
+
headers: { 'x-api-key': apiKey },
|
|
46
|
+
signal: AbortSignal.timeout(10000)
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
console.log(chalk.red(' ✗ Could not reach server.\n'));
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
if (data.message !== 'Pong!') {
|
|
56
|
+
console.log(chalk.red(' ✗ Unexpected server response.\n'));
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.log(chalk.red(` ✗ Server not reachable: ${err.message}\n`));
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(chalk.green(' ✓ Server is running\n'));
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a verification code for Telegram chat ID capture
|
|
5
|
+
*/
|
|
6
|
+
export function generateVerificationCode() {
|
|
7
|
+
return 'verify-' + randomBytes(4).toString('hex');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Register a Telegram webhook
|
|
12
|
+
*/
|
|
13
|
+
export async function setTelegramWebhook(botToken, webhookUrl, secretToken = null) {
|
|
14
|
+
// Delete first — Telegram ignores secret_token changes if the URL is unchanged
|
|
15
|
+
await deleteTelegramWebhook(botToken);
|
|
16
|
+
|
|
17
|
+
const body = { url: webhookUrl };
|
|
18
|
+
if (secretToken) {
|
|
19
|
+
body.secret_token = secretToken;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const response = await fetch(
|
|
23
|
+
`https://api.telegram.org/bot${botToken}/setWebhook`,
|
|
24
|
+
{
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const result = await response.json();
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get current webhook info
|
|
37
|
+
*/
|
|
38
|
+
export async function getTelegramWebhookInfo(botToken) {
|
|
39
|
+
const response = await fetch(`https://api.telegram.org/bot${botToken}/getWebhookInfo`);
|
|
40
|
+
const result = await response.json();
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Delete existing webhook
|
|
46
|
+
*/
|
|
47
|
+
export async function deleteTelegramWebhook(botToken) {
|
|
48
|
+
const response = await fetch(`https://api.telegram.org/bot${botToken}/deleteWebhook`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
});
|
|
51
|
+
const result = await response.json();
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate bot token by calling getMe
|
|
57
|
+
*/
|
|
58
|
+
export async function validateBotToken(botToken) {
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
|
|
61
|
+
const result = await response.json();
|
|
62
|
+
if (result.ok) {
|
|
63
|
+
return { valid: true, botInfo: result.result };
|
|
64
|
+
}
|
|
65
|
+
return { valid: false, error: result.description };
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return { valid: false, error: error.message };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get BotFather URL for creating a new bot
|
|
73
|
+
*/
|
|
74
|
+
export function getBotFatherURL() {
|
|
75
|
+
return 'https://t.me/BotFather';
|
|
76
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import { readFileSync, existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
import { checkPrerequisites } from './lib/prerequisites.mjs';
|
|
10
|
+
import { setVariables } from './lib/github.mjs';
|
|
11
|
+
import { setTelegramWebhook, validateBotToken, generateVerificationCode } from './lib/telegram.mjs';
|
|
12
|
+
import { confirm, generateTelegramWebhookSecret } from './lib/prompts.mjs';
|
|
13
|
+
import { updateEnvVariable } from './lib/auth.mjs';
|
|
14
|
+
import { runVerificationFlow } from './lib/telegram-verify.mjs';
|
|
15
|
+
|
|
16
|
+
const ROOT_DIR = process.cwd();
|
|
17
|
+
|
|
18
|
+
function printSuccess(message) {
|
|
19
|
+
console.log(chalk.green(' \u2713 ') + message);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function printWarning(message) {
|
|
23
|
+
console.log(chalk.yellow(' \u26a0 ') + message);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function printInfo(message) {
|
|
27
|
+
console.log(chalk.dim(' \u2192 ') + message);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse .env file and return object
|
|
32
|
+
*/
|
|
33
|
+
function loadEnvFile() {
|
|
34
|
+
const envPath = join(ROOT_DIR, '.env');
|
|
35
|
+
if (!existsSync(envPath)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
39
|
+
const env = {};
|
|
40
|
+
for (const line of content.split('\n')) {
|
|
41
|
+
const match = line.match(/^([^#=]+)=(.*)$/);
|
|
42
|
+
if (match) {
|
|
43
|
+
env[match[1].trim()] = match[2].trim();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return env;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function main() {
|
|
50
|
+
console.log(chalk.bold.cyan('\n Telegram Webhook Setup\n'));
|
|
51
|
+
console.log(chalk.dim(' Use this to reconfigure Telegram after restarting ngrok.\n'));
|
|
52
|
+
|
|
53
|
+
// Check prerequisites
|
|
54
|
+
const prereqs = await checkPrerequisites();
|
|
55
|
+
|
|
56
|
+
if (!prereqs.git.remoteInfo) {
|
|
57
|
+
console.log(chalk.red('Could not detect GitHub repository from git remote.'));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { owner, repo } = prereqs.git.remoteInfo;
|
|
62
|
+
printInfo(`Repository: ${owner}/${repo}`);
|
|
63
|
+
|
|
64
|
+
// Load existing config
|
|
65
|
+
const env = loadEnvFile();
|
|
66
|
+
|
|
67
|
+
// Get ngrok URL first (verify server is up)
|
|
68
|
+
console.log(chalk.yellow('\n Make sure your server is running:\n'));
|
|
69
|
+
console.log(chalk.dim(' Terminal 1: ') + chalk.cyan('npm run dev'));
|
|
70
|
+
console.log(chalk.dim(' Terminal 2: ') + chalk.cyan('ngrok http 3000\n'));
|
|
71
|
+
|
|
72
|
+
let ngrokUrl = null;
|
|
73
|
+
while (!ngrokUrl) {
|
|
74
|
+
const { url } = await inquirer.prompt([
|
|
75
|
+
{
|
|
76
|
+
type: 'input',
|
|
77
|
+
name: 'url',
|
|
78
|
+
message: 'Paste your ngrok URL (https://...ngrok...):',
|
|
79
|
+
validate: (input) => {
|
|
80
|
+
if (!input) return 'URL is required';
|
|
81
|
+
if (!input.startsWith('https://')) return 'URL must start with https://';
|
|
82
|
+
if (!input.includes('ngrok')) return 'URL should be an ngrok URL';
|
|
83
|
+
return true;
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
const testUrl = url.replace(/\/$/, '');
|
|
88
|
+
|
|
89
|
+
// Verify the server is reachable through ngrok
|
|
90
|
+
const healthSpinner = ora('Verifying server is reachable...').start();
|
|
91
|
+
const apiKey = env?.API_KEY;
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(`${testUrl}/api/ping`, {
|
|
94
|
+
method: 'GET',
|
|
95
|
+
headers: apiKey ? { 'x-api-key': apiKey } : {},
|
|
96
|
+
signal: AbortSignal.timeout(10000)
|
|
97
|
+
});
|
|
98
|
+
if (response.ok) {
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
if (data.message === 'Pong!') {
|
|
101
|
+
healthSpinner.succeed('Server is reachable and authenticated');
|
|
102
|
+
ngrokUrl = testUrl;
|
|
103
|
+
} else {
|
|
104
|
+
healthSpinner.fail('Unexpected response from server');
|
|
105
|
+
const retry = await confirm('Try again?');
|
|
106
|
+
if (!retry) {
|
|
107
|
+
ngrokUrl = testUrl;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} else if (response.status === 401) {
|
|
111
|
+
healthSpinner.fail('Server responded but API key mismatch');
|
|
112
|
+
printWarning('Check that API_KEY in .env matches the running server');
|
|
113
|
+
const retry = await confirm('Try again?');
|
|
114
|
+
if (!retry) {
|
|
115
|
+
ngrokUrl = testUrl;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
healthSpinner.fail(`Server returned status ${response.status}`);
|
|
119
|
+
printWarning('Make sure the server is running (npm run dev)');
|
|
120
|
+
const retry = await confirm('Try again?');
|
|
121
|
+
if (!retry) {
|
|
122
|
+
ngrokUrl = testUrl;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
healthSpinner.fail(`Could not reach server: ${error.message}`);
|
|
127
|
+
printWarning('Make sure both the server AND ngrok are running');
|
|
128
|
+
const retry = await confirm('Try again?');
|
|
129
|
+
if (!retry) {
|
|
130
|
+
ngrokUrl = testUrl;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Set GH_WEBHOOK_URL variable
|
|
136
|
+
const urlSpinner = ora('Updating GH_WEBHOOK_URL variable...').start();
|
|
137
|
+
const urlResult = await setVariables(owner, repo, { GH_WEBHOOK_URL: ngrokUrl });
|
|
138
|
+
if (urlResult.GH_WEBHOOK_URL.success) {
|
|
139
|
+
urlSpinner.succeed('GH_WEBHOOK_URL variable updated');
|
|
140
|
+
} else {
|
|
141
|
+
urlSpinner.fail(`Failed: ${urlResult.GH_WEBHOOK_URL.error}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get Telegram token - try .env first
|
|
145
|
+
let token = env?.TELEGRAM_BOT_TOKEN;
|
|
146
|
+
if (token) {
|
|
147
|
+
printInfo('Using Telegram token from .env');
|
|
148
|
+
const validateSpinner = ora('Validating bot token...').start();
|
|
149
|
+
const validation = await validateBotToken(token);
|
|
150
|
+
if (validation.valid) {
|
|
151
|
+
validateSpinner.succeed(`Bot: @${validation.botInfo.username}`);
|
|
152
|
+
} else {
|
|
153
|
+
validateSpinner.fail(`Invalid token in .env: ${validation.error}`);
|
|
154
|
+
token = null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!token) {
|
|
159
|
+
const { inputToken } = await inquirer.prompt([
|
|
160
|
+
{
|
|
161
|
+
type: 'password',
|
|
162
|
+
name: 'inputToken',
|
|
163
|
+
message: 'Telegram bot token:',
|
|
164
|
+
mask: '*',
|
|
165
|
+
validate: (input) => {
|
|
166
|
+
if (!input) return 'Token is required';
|
|
167
|
+
if (!/^\d+:[A-Za-z0-9_-]+$/.test(input)) {
|
|
168
|
+
return 'Invalid format. Should be like 123456789:ABC-DEF...';
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
]);
|
|
174
|
+
token = inputToken;
|
|
175
|
+
|
|
176
|
+
const validateSpinner = ora('Validating bot token...').start();
|
|
177
|
+
const validation = await validateBotToken(token);
|
|
178
|
+
if (!validation.valid) {
|
|
179
|
+
validateSpinner.fail(`Invalid token: ${validation.error}`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
validateSpinner.succeed(`Bot: @${validation.botInfo.username}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Handle webhook secret
|
|
186
|
+
let webhookSecret = env?.TELEGRAM_WEBHOOK_SECRET;
|
|
187
|
+
if (webhookSecret) {
|
|
188
|
+
printInfo('Using existing webhook secret');
|
|
189
|
+
} else {
|
|
190
|
+
webhookSecret = await generateTelegramWebhookSecret();
|
|
191
|
+
updateEnvVariable('TELEGRAM_WEBHOOK_SECRET', webhookSecret);
|
|
192
|
+
printSuccess('Generated webhook secret');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Register Telegram webhook
|
|
196
|
+
const webhookUrl = `${ngrokUrl}/api/telegram/webhook`;
|
|
197
|
+
const tgSpinner = ora('Registering Telegram webhook...').start();
|
|
198
|
+
const tgResult = await setTelegramWebhook(token, webhookUrl, webhookSecret);
|
|
199
|
+
if (tgResult.ok) {
|
|
200
|
+
tgSpinner.succeed('Telegram webhook registered');
|
|
201
|
+
} else {
|
|
202
|
+
tgSpinner.fail(`Failed: ${tgResult.description}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Handle chat ID verification (required — bot ignores all messages without it)
|
|
206
|
+
let telegramChatId = env?.TELEGRAM_CHAT_ID;
|
|
207
|
+
|
|
208
|
+
if (telegramChatId) {
|
|
209
|
+
printInfo(`Using existing chat ID: ${telegramChatId}`);
|
|
210
|
+
} else {
|
|
211
|
+
// Generate new code and update .env
|
|
212
|
+
const verificationCode = generateVerificationCode();
|
|
213
|
+
updateEnvVariable('TELEGRAM_VERIFICATION', verificationCode);
|
|
214
|
+
|
|
215
|
+
console.log(chalk.yellow('\n Waiting for server to restart with new verification code...\n'));
|
|
216
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
217
|
+
|
|
218
|
+
const chatId = await runVerificationFlow(verificationCode);
|
|
219
|
+
|
|
220
|
+
if (chatId) {
|
|
221
|
+
updateEnvVariable('TELEGRAM_CHAT_ID', chatId);
|
|
222
|
+
printSuccess(`Chat ID saved: ${chatId}`);
|
|
223
|
+
} else {
|
|
224
|
+
printWarning('Chat ID is required \u2014 the bot will not respond without it.');
|
|
225
|
+
printInfo('Run npm run setup-telegram again to complete setup.');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(chalk.green('\n Done!\n'));
|
|
230
|
+
console.log(chalk.dim(` Webhook URL: ${webhookUrl}\n`));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
main().catch((error) => {
|
|
234
|
+
console.error(chalk.red('\nFailed:'), error.message);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
});
|