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
package/bin/cli.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const command = process.argv[2];
|
|
8
|
+
const args = process.argv.slice(3);
|
|
9
|
+
|
|
10
|
+
function printUsage() {
|
|
11
|
+
console.log(`
|
|
12
|
+
Usage: thepopebot <command>
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
init Scaffold a new thepopebot project
|
|
16
|
+
setup Run interactive setup wizard
|
|
17
|
+
setup-telegram Reconfigure Telegram webhook
|
|
18
|
+
reset [file] Restore a template file (or list available templates)
|
|
19
|
+
`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Collect all template files as relative paths.
|
|
24
|
+
*/
|
|
25
|
+
function getTemplateFiles(templatesDir) {
|
|
26
|
+
const files = [];
|
|
27
|
+
function walk(dir) {
|
|
28
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const fullPath = path.join(dir, entry.name);
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
walk(fullPath);
|
|
33
|
+
} else {
|
|
34
|
+
files.push(path.relative(templatesDir, fullPath));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
walk(templatesDir);
|
|
39
|
+
return files;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function init() {
|
|
43
|
+
const cwd = process.cwd();
|
|
44
|
+
const packageDir = path.join(__dirname, '..');
|
|
45
|
+
const templatesDir = path.join(packageDir, 'templates');
|
|
46
|
+
|
|
47
|
+
console.log('\nScaffolding thepopebot project...\n');
|
|
48
|
+
|
|
49
|
+
const templateFiles = getTemplateFiles(templatesDir);
|
|
50
|
+
const created = [];
|
|
51
|
+
const skipped = [];
|
|
52
|
+
const changed = [];
|
|
53
|
+
|
|
54
|
+
for (const relPath of templateFiles) {
|
|
55
|
+
const src = path.join(templatesDir, relPath);
|
|
56
|
+
const dest = path.join(cwd, relPath);
|
|
57
|
+
|
|
58
|
+
if (!fs.existsSync(dest)) {
|
|
59
|
+
// File doesn't exist — create it
|
|
60
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
61
|
+
fs.copyFileSync(src, dest);
|
|
62
|
+
created.push(relPath);
|
|
63
|
+
console.log(` Created ${relPath}`);
|
|
64
|
+
} else {
|
|
65
|
+
// File exists — check if template has changed
|
|
66
|
+
const srcContent = fs.readFileSync(src);
|
|
67
|
+
const destContent = fs.readFileSync(dest);
|
|
68
|
+
if (srcContent.equals(destContent)) {
|
|
69
|
+
skipped.push(relPath);
|
|
70
|
+
} else {
|
|
71
|
+
changed.push(relPath);
|
|
72
|
+
console.log(` Skipped ${relPath} (already exists)`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create package.json if it doesn't exist
|
|
78
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
79
|
+
if (!fs.existsSync(pkgPath)) {
|
|
80
|
+
const dirName = path.basename(cwd);
|
|
81
|
+
const pkg = {
|
|
82
|
+
name: dirName,
|
|
83
|
+
private: true,
|
|
84
|
+
scripts: {
|
|
85
|
+
dev: 'next dev',
|
|
86
|
+
build: 'next build',
|
|
87
|
+
start: 'next start',
|
|
88
|
+
setup: 'thepopebot setup',
|
|
89
|
+
'setup-telegram': 'thepopebot setup-telegram',
|
|
90
|
+
},
|
|
91
|
+
dependencies: {
|
|
92
|
+
thepopebot: '^1.0.0',
|
|
93
|
+
next: '^16.0.0',
|
|
94
|
+
react: '^19.0.0',
|
|
95
|
+
'react-dom': '^19.0.0',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
99
|
+
console.log(' Created package.json');
|
|
100
|
+
} else {
|
|
101
|
+
console.log(' Skipped package.json (already exists)');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Create .gitkeep files for empty dirs
|
|
105
|
+
const gitkeepDirs = ['cron', 'triggers', 'logs'];
|
|
106
|
+
for (const dir of gitkeepDirs) {
|
|
107
|
+
const gitkeep = path.join(cwd, dir, '.gitkeep');
|
|
108
|
+
if (!fs.existsSync(gitkeep)) {
|
|
109
|
+
fs.mkdirSync(path.join(cwd, dir), { recursive: true });
|
|
110
|
+
fs.writeFileSync(gitkeep, '');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Report changed templates
|
|
115
|
+
if (changed.length > 0) {
|
|
116
|
+
console.log('\n Updated templates available:');
|
|
117
|
+
console.log(' These files differ from the current package templates.');
|
|
118
|
+
console.log(' This may be from your edits, or from a thepopebot update.\n');
|
|
119
|
+
for (const file of changed) {
|
|
120
|
+
console.log(` ${file}`);
|
|
121
|
+
}
|
|
122
|
+
console.log('\n To view differences: npx thepopebot diff <file>');
|
|
123
|
+
console.log(' To reset to default: npx thepopebot reset <file>');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (created.length > 0 || !fs.existsSync(path.join(cwd, 'node_modules'))) {
|
|
127
|
+
console.log('\nDone! Next steps:\n');
|
|
128
|
+
console.log(' 1. npm install');
|
|
129
|
+
console.log(' 2. npm run setup');
|
|
130
|
+
console.log(' 3. npm run dev\n');
|
|
131
|
+
} else {
|
|
132
|
+
console.log('\nDone!\n');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* List all available template files, or restore a specific one.
|
|
138
|
+
*/
|
|
139
|
+
function reset(filePath) {
|
|
140
|
+
const packageDir = path.join(__dirname, '..');
|
|
141
|
+
const templatesDir = path.join(packageDir, 'templates');
|
|
142
|
+
const cwd = process.cwd();
|
|
143
|
+
|
|
144
|
+
if (!filePath) {
|
|
145
|
+
console.log('\nAvailable template files:\n');
|
|
146
|
+
const files = getTemplateFiles(templatesDir);
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
console.log(` ${file}`);
|
|
149
|
+
}
|
|
150
|
+
console.log('\nUsage: thepopebot reset <file>');
|
|
151
|
+
console.log('Example: thepopebot reset operating_system/SOUL.md\n');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const src = path.join(templatesDir, filePath);
|
|
156
|
+
const dest = path.join(cwd, filePath);
|
|
157
|
+
|
|
158
|
+
if (!fs.existsSync(src)) {
|
|
159
|
+
console.error(`\nTemplate not found: ${filePath}`);
|
|
160
|
+
console.log('Run "thepopebot reset" to see available templates.\n');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (fs.statSync(src).isDirectory()) {
|
|
165
|
+
console.log(`\nRestoring ${filePath}/...\n`);
|
|
166
|
+
copyDirSyncForce(src, dest);
|
|
167
|
+
} else {
|
|
168
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
169
|
+
fs.copyFileSync(src, dest);
|
|
170
|
+
console.log(`\nRestored ${filePath}\n`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Show the diff between a user's file and the package template.
|
|
176
|
+
*/
|
|
177
|
+
function diff(filePath) {
|
|
178
|
+
const packageDir = path.join(__dirname, '..');
|
|
179
|
+
const templatesDir = path.join(packageDir, 'templates');
|
|
180
|
+
const cwd = process.cwd();
|
|
181
|
+
|
|
182
|
+
if (!filePath) {
|
|
183
|
+
// Show all files that differ
|
|
184
|
+
console.log('\nFiles that differ from package templates:\n');
|
|
185
|
+
const files = getTemplateFiles(templatesDir);
|
|
186
|
+
let anyDiff = false;
|
|
187
|
+
for (const file of files) {
|
|
188
|
+
const src = path.join(templatesDir, file);
|
|
189
|
+
const dest = path.join(cwd, file);
|
|
190
|
+
if (fs.existsSync(dest)) {
|
|
191
|
+
const srcContent = fs.readFileSync(src);
|
|
192
|
+
const destContent = fs.readFileSync(dest);
|
|
193
|
+
if (!srcContent.equals(destContent)) {
|
|
194
|
+
console.log(` ${file}`);
|
|
195
|
+
anyDiff = true;
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
console.log(` ${file} (missing)`);
|
|
199
|
+
anyDiff = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!anyDiff) {
|
|
203
|
+
console.log(' All files match package templates.');
|
|
204
|
+
}
|
|
205
|
+
console.log('\nUsage: thepopebot diff <file>');
|
|
206
|
+
console.log('Example: thepopebot diff operating_system/SOUL.md\n');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const src = path.join(templatesDir, filePath);
|
|
211
|
+
const dest = path.join(cwd, filePath);
|
|
212
|
+
|
|
213
|
+
if (!fs.existsSync(src)) {
|
|
214
|
+
console.error(`\nTemplate not found: ${filePath}`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!fs.existsSync(dest)) {
|
|
219
|
+
console.log(`\n${filePath} does not exist in your project.`);
|
|
220
|
+
console.log(`Run "thepopebot reset ${filePath}" to create it.\n`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
// Use git diff for nice colored output, fall back to plain diff
|
|
226
|
+
execSync(`git diff --no-index -- "${dest}" "${src}"`, { stdio: 'inherit' });
|
|
227
|
+
console.log('\nFiles are identical.\n');
|
|
228
|
+
} catch (e) {
|
|
229
|
+
// git diff exits with 1 when files differ (output already printed)
|
|
230
|
+
console.log(`\n To reset: thepopebot reset ${filePath}\n`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function copyDirSyncForce(src, dest) {
|
|
235
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
236
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
const srcPath = path.join(src, entry.name);
|
|
239
|
+
const destPath = path.join(dest, entry.name);
|
|
240
|
+
if (entry.isDirectory()) {
|
|
241
|
+
copyDirSyncForce(srcPath, destPath);
|
|
242
|
+
} else {
|
|
243
|
+
fs.copyFileSync(srcPath, destPath);
|
|
244
|
+
console.log(` Restored ${path.relative(process.cwd(), destPath)}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function setup() {
|
|
250
|
+
const setupScript = path.join(__dirname, '..', 'setup', 'setup.mjs');
|
|
251
|
+
execSync(`node ${setupScript}`, { stdio: 'inherit', cwd: process.cwd() });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function setupTelegram() {
|
|
255
|
+
const setupScript = path.join(__dirname, '..', 'setup', 'setup-telegram.mjs');
|
|
256
|
+
execSync(`node ${setupScript}`, { stdio: 'inherit', cwd: process.cwd() });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
switch (command) {
|
|
260
|
+
case 'init':
|
|
261
|
+
init();
|
|
262
|
+
break;
|
|
263
|
+
case 'setup':
|
|
264
|
+
setup();
|
|
265
|
+
break;
|
|
266
|
+
case 'setup-telegram':
|
|
267
|
+
setupTelegram();
|
|
268
|
+
break;
|
|
269
|
+
case 'reset':
|
|
270
|
+
reset(args[0]);
|
|
271
|
+
break;
|
|
272
|
+
case 'diff':
|
|
273
|
+
diff(args[0]);
|
|
274
|
+
break;
|
|
275
|
+
default:
|
|
276
|
+
printUsage();
|
|
277
|
+
process.exit(command ? 1 : 0);
|
|
278
|
+
}
|
package/config/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Next.js config wrapper for thepopebot.
|
|
5
|
+
* Enables instrumentation hook for cron scheduling on server start.
|
|
6
|
+
*
|
|
7
|
+
* Usage in user's next.config.mjs:
|
|
8
|
+
* import { withThepopebot } from 'thepopebot/config';
|
|
9
|
+
* export default withThepopebot({});
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} nextConfig - User's Next.js config
|
|
12
|
+
* @returns {Object} Enhanced Next.js config
|
|
13
|
+
*/
|
|
14
|
+
function withThepopebot(nextConfig = {}) {
|
|
15
|
+
return {
|
|
16
|
+
...nextConfig,
|
|
17
|
+
// Ensure server-only packages aren't bundled for client
|
|
18
|
+
serverExternalPackages: [
|
|
19
|
+
...(nextConfig.serverExternalPackages || []),
|
|
20
|
+
'thepopebot',
|
|
21
|
+
'grammy',
|
|
22
|
+
'@grammyjs/parse-mode',
|
|
23
|
+
'node-cron',
|
|
24
|
+
'uuid',
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { withThepopebot };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js instrumentation hook for thepopebot.
|
|
3
|
+
* This file is loaded by Next.js on server start when instrumentationHook is enabled.
|
|
4
|
+
*
|
|
5
|
+
* Users should create an instrumentation.js in their project root that imports this:
|
|
6
|
+
*
|
|
7
|
+
* export { register } from 'thepopebot/instrumentation';
|
|
8
|
+
*
|
|
9
|
+
* Or they can re-export and add their own logic.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
let initialized = false;
|
|
13
|
+
|
|
14
|
+
async function register() {
|
|
15
|
+
// Only run on the server, and only once
|
|
16
|
+
if (typeof window !== 'undefined' || initialized) return;
|
|
17
|
+
initialized = true;
|
|
18
|
+
|
|
19
|
+
// Load .env from project root
|
|
20
|
+
require('dotenv').config();
|
|
21
|
+
|
|
22
|
+
// Start cron scheduler
|
|
23
|
+
const { loadCrons } = require('../lib/cron');
|
|
24
|
+
loadCrons();
|
|
25
|
+
|
|
26
|
+
console.log('thepopebot initialized');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { register };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
FROM node:22-bookworm-slim
|
|
2
|
+
|
|
3
|
+
RUN apt-get update && apt-get install -y \
|
|
4
|
+
git \
|
|
5
|
+
jq \
|
|
6
|
+
curl \
|
|
7
|
+
procps \
|
|
8
|
+
# Chrome/Chromium dependencies
|
|
9
|
+
libnss3 \
|
|
10
|
+
libnspr4 \
|
|
11
|
+
libatk1.0-0 \
|
|
12
|
+
libatk-bridge2.0-0 \
|
|
13
|
+
libcups2 \
|
|
14
|
+
libdrm2 \
|
|
15
|
+
libdbus-1-3 \
|
|
16
|
+
libxkbcommon0 \
|
|
17
|
+
libatspi2.0-0 \
|
|
18
|
+
libxcomposite1 \
|
|
19
|
+
libxdamage1 \
|
|
20
|
+
libxfixes3 \
|
|
21
|
+
libxrandr2 \
|
|
22
|
+
libgbm1 \
|
|
23
|
+
libasound2 \
|
|
24
|
+
libpango-1.0-0 \
|
|
25
|
+
libcairo2 \
|
|
26
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
27
|
+
|
|
28
|
+
# Install GitHub CLI
|
|
29
|
+
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
|
30
|
+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
|
31
|
+
&& apt-get update && apt-get install -y gh \
|
|
32
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
33
|
+
|
|
34
|
+
RUN npm install -g @mariozechner/pi-coding-agent
|
|
35
|
+
|
|
36
|
+
# Create Pi config directory (extension loaded from repo at runtime)
|
|
37
|
+
RUN mkdir -p /root/.pi/agent
|
|
38
|
+
|
|
39
|
+
# Clone pi-skills and install browser-tools (includes Puppeteer + Chromium)
|
|
40
|
+
RUN git clone https://github.com/badlogic/pi-skills.git /pi-skills
|
|
41
|
+
WORKDIR /pi-skills/browser-tools
|
|
42
|
+
RUN npm install
|
|
43
|
+
WORKDIR /pi-skills/brave-search
|
|
44
|
+
RUN npm install
|
|
45
|
+
|
|
46
|
+
COPY entrypoint.sh /entrypoint.sh
|
|
47
|
+
RUN chmod +x /entrypoint.sh
|
|
48
|
+
|
|
49
|
+
WORKDIR /job
|
|
50
|
+
|
|
51
|
+
ENTRYPOINT ["/entrypoint.sh"]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Extract job ID from branch name (job/uuid -> uuid), fallback to random UUID
|
|
5
|
+
if [[ "$BRANCH" == job/* ]]; then
|
|
6
|
+
JOB_ID="${BRANCH#job/}"
|
|
7
|
+
else
|
|
8
|
+
JOB_ID=$(cat /proc/sys/kernel/random/uuid)
|
|
9
|
+
fi
|
|
10
|
+
echo "Job ID: ${JOB_ID}"
|
|
11
|
+
|
|
12
|
+
# Start Chrome (using Puppeteer's chromium from pi-skills browser-tools)
|
|
13
|
+
CHROME_BIN=$(find /root/.cache/puppeteer -name "chrome" -type f | head -1)
|
|
14
|
+
$CHROME_BIN --headless --no-sandbox --disable-gpu --remote-debugging-port=9222 2>/dev/null &
|
|
15
|
+
CHROME_PID=$!
|
|
16
|
+
sleep 2
|
|
17
|
+
|
|
18
|
+
# Export SECRETS (base64 JSON) as flat env vars (GH_TOKEN, ANTHROPIC_API_KEY, etc.)
|
|
19
|
+
# These are filtered from LLM's bash subprocess by env-sanitizer extension
|
|
20
|
+
if [ -n "$SECRETS" ]; then
|
|
21
|
+
SECRETS_JSON=$(echo "$SECRETS" | base64 -d)
|
|
22
|
+
eval $(echo "$SECRETS_JSON" | jq -r 'to_entries | .[] | "export \(.key)=\"\(.value)\""')
|
|
23
|
+
export SECRETS="$SECRETS_JSON" # Keep decoded for extension to parse
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Export LLM_SECRETS (base64 JSON) as flat env vars
|
|
27
|
+
# These are NOT filtered - LLM can access these (browser logins, skill API keys, etc.)
|
|
28
|
+
if [ -n "$LLM_SECRETS" ]; then
|
|
29
|
+
LLM_SECRETS_JSON=$(echo "$LLM_SECRETS" | base64 -d)
|
|
30
|
+
eval $(echo "$LLM_SECRETS_JSON" | jq -r 'to_entries | .[] | "export \(.key)=\"\(.value)\""')
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Git setup - derive identity from GitHub token
|
|
34
|
+
gh auth setup-git
|
|
35
|
+
GH_USER_JSON=$(gh api user -q '{name: .name, login: .login, email: .email, id: .id}')
|
|
36
|
+
GH_USER_NAME=$(echo "$GH_USER_JSON" | jq -r '.name // .login')
|
|
37
|
+
GH_USER_EMAIL=$(echo "$GH_USER_JSON" | jq -r '.email // "\(.id)+\(.login)@users.noreply.github.com"')
|
|
38
|
+
git config --global user.name "$GH_USER_NAME"
|
|
39
|
+
git config --global user.email "$GH_USER_EMAIL"
|
|
40
|
+
|
|
41
|
+
# Clone branch
|
|
42
|
+
if [ -n "$REPO_URL" ]; then
|
|
43
|
+
git clone --single-branch --branch "$BRANCH" --depth 1 "$REPO_URL" /job
|
|
44
|
+
else
|
|
45
|
+
echo "No REPO_URL provided"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
cd /job
|
|
49
|
+
|
|
50
|
+
# Create temp directory for agent use (gitignored via tmp/)
|
|
51
|
+
mkdir -p /job/tmp
|
|
52
|
+
|
|
53
|
+
# Symlink pi-skills into .pi/skills/ so Pi discovers them
|
|
54
|
+
ln -sf /pi-skills/brave-search /job/.pi/skills/brave-search
|
|
55
|
+
|
|
56
|
+
# Setup logs
|
|
57
|
+
LOG_DIR="/job/logs/${JOB_ID}"
|
|
58
|
+
mkdir -p "${LOG_DIR}"
|
|
59
|
+
|
|
60
|
+
# 1. Build system prompt from operating_system MD files
|
|
61
|
+
SYSTEM_FILES=("SOUL.md" "AGENT.md")
|
|
62
|
+
> /job/.pi/SYSTEM.md
|
|
63
|
+
for i in "${!SYSTEM_FILES[@]}"; do
|
|
64
|
+
cat "/job/operating_system/${SYSTEM_FILES[$i]}" >> /job/.pi/SYSTEM.md
|
|
65
|
+
if [ "$i" -lt $((${#SYSTEM_FILES[@]} - 1)) ]; then
|
|
66
|
+
echo -e "\n\n" >> /job/.pi/SYSTEM.md
|
|
67
|
+
fi
|
|
68
|
+
done
|
|
69
|
+
|
|
70
|
+
PROMPT="
|
|
71
|
+
|
|
72
|
+
# Your Job
|
|
73
|
+
|
|
74
|
+
$(cat /job/logs/${JOB_ID}/job.md)"
|
|
75
|
+
|
|
76
|
+
MODEL_FLAGS=""
|
|
77
|
+
if [ -n "$MODEL" ]; then
|
|
78
|
+
MODEL_FLAGS="--provider anthropic --model $MODEL"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
pi $MODEL_FLAGS -p "$PROMPT" --session-dir "${LOG_DIR}"
|
|
82
|
+
|
|
83
|
+
# 2. Commit changes + logs
|
|
84
|
+
git add -A
|
|
85
|
+
git add -f "${LOG_DIR}"
|
|
86
|
+
git commit -m "thepopebot: job ${JOB_ID}" || true
|
|
87
|
+
git push origin
|
|
88
|
+
|
|
89
|
+
# 3. Merge (pi has memory of job via session)
|
|
90
|
+
#if [ -n "$REPO_URL" ] && [ -f "/job/MERGE_JOB.md" ]; then
|
|
91
|
+
# echo "MERGED"
|
|
92
|
+
# pi -p "$(cat /job/MERGE_JOB.md)" --session-dir "${LOG_DIR}" --continue
|
|
93
|
+
#fi
|
|
94
|
+
|
|
95
|
+
# 5. Create PR (auto-merge handled by GitHub Actions workflow)
|
|
96
|
+
gh pr create --title "thepopebot: job ${JOB_ID}" --body "Automated job" --base main || true
|
|
97
|
+
|
|
98
|
+
# Cleanup
|
|
99
|
+
kill $CHROME_PID 2>/dev/null || true
|
|
100
|
+
echo "Done. Job ID: ${JOB_ID}"
|
package/lib/actions.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
const { promisify } = require('util');
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
const { createJob } = require('./tools/create-job');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Execute a single action
|
|
8
|
+
* @param {Object} action - { type, job, command, url, method, headers, vars }
|
|
9
|
+
* @param {Object} opts - { cwd, data }
|
|
10
|
+
* @returns {Promise<string>} Result description for logging
|
|
11
|
+
*/
|
|
12
|
+
async function executeAction(action, opts = {}) {
|
|
13
|
+
const type = action.type || 'agent';
|
|
14
|
+
|
|
15
|
+
if (type === 'command') {
|
|
16
|
+
const { stdout, stderr } = await execAsync(action.command, { cwd: opts.cwd });
|
|
17
|
+
return (stdout || stderr || '').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (type === 'http') {
|
|
21
|
+
const method = (action.method || 'POST').toUpperCase();
|
|
22
|
+
const headers = { 'Content-Type': 'application/json', ...action.headers };
|
|
23
|
+
const fetchOpts = { method, headers };
|
|
24
|
+
|
|
25
|
+
if (method !== 'GET') {
|
|
26
|
+
const body = { ...action.vars };
|
|
27
|
+
if (opts.data) body.data = opts.data;
|
|
28
|
+
fetchOpts.body = JSON.stringify(body);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const res = await fetch(action.url, fetchOpts);
|
|
32
|
+
return `${method} ${action.url} → ${res.status}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Default: agent
|
|
36
|
+
const result = await createJob(action.job);
|
|
37
|
+
return `job ${result.job_id}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { executeAction };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory conversation history management per Telegram chat.
|
|
3
|
+
* - Keyed by chat_id
|
|
4
|
+
* - 30-minute TTL per conversation
|
|
5
|
+
* - Max 20 messages per conversation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const MAX_MESSAGES = 20;
|
|
9
|
+
const TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
10
|
+
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
11
|
+
|
|
12
|
+
// Map<chatId, { messages: Array, lastAccess: number }>
|
|
13
|
+
const conversations = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get conversation history for a chat
|
|
17
|
+
* @param {string} chatId - Telegram chat ID
|
|
18
|
+
* @returns {Array} - Message history array
|
|
19
|
+
*/
|
|
20
|
+
function getHistory(chatId) {
|
|
21
|
+
const entry = conversations.get(chatId);
|
|
22
|
+
if (!entry) return [];
|
|
23
|
+
|
|
24
|
+
// Check if expired
|
|
25
|
+
if (Date.now() - entry.lastAccess > TTL_MS) {
|
|
26
|
+
conversations.delete(chatId);
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
entry.lastAccess = Date.now();
|
|
31
|
+
return entry.messages;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Update conversation history for a chat
|
|
36
|
+
* @param {string} chatId - Telegram chat ID
|
|
37
|
+
* @param {Array} messages - New message history
|
|
38
|
+
*/
|
|
39
|
+
function updateHistory(chatId, messages) {
|
|
40
|
+
// Trim to max messages (keep most recent)
|
|
41
|
+
const trimmed = messages.slice(-MAX_MESSAGES);
|
|
42
|
+
|
|
43
|
+
conversations.set(chatId, {
|
|
44
|
+
messages: trimmed,
|
|
45
|
+
lastAccess: Date.now(),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Clear conversation history for a chat
|
|
51
|
+
* @param {string} chatId - Telegram chat ID
|
|
52
|
+
*/
|
|
53
|
+
function clearHistory(chatId) {
|
|
54
|
+
conversations.delete(chatId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Clean up expired conversations
|
|
59
|
+
*/
|
|
60
|
+
function cleanupExpired() {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
for (const [chatId, entry] of conversations) {
|
|
63
|
+
if (now - entry.lastAccess > TTL_MS) {
|
|
64
|
+
conversations.delete(chatId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Start cleanup interval
|
|
70
|
+
setInterval(cleanupExpired, CLEANUP_INTERVAL_MS);
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
getHistory,
|
|
74
|
+
updateHistory,
|
|
75
|
+
clearHistory,
|
|
76
|
+
};
|