thepopebot 1.2.67 → 1.2.69-beta.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/README.md +74 -0
- package/bin/cli.js +138 -5
- package/lib/ai/agent.js +3 -2
- package/lib/cron.js +4 -1
- package/lib/utils/render-md.js +22 -2
- package/package.json +1 -1
- package/setup/lib/auth.mjs +0 -35
- package/setup/setup.mjs +21 -15
- package/templates/.github/workflows/run-job.yml +16 -2
- package/templates/.gitignore.template +2 -2
- package/templates/.pi/skills/llm-secrets/llm-secrets.js +3 -4
- package/templates/CLAUDE.md.template +4 -2
- package/templates/config/AGENT.md +3 -1
- package/templates/config/CHATBOT.md +2 -0
- package/templates/docker/job/Dockerfile +0 -7
- package/templates/docker/job/entrypoint.sh +47 -17
package/README.md
CHANGED
|
@@ -243,6 +243,80 @@ The `templates/` directory contains files scaffolded into user projects by `thep
|
|
|
243
243
|
|
|
244
244
|
---
|
|
245
245
|
|
|
246
|
+
## Production Deployment
|
|
247
|
+
|
|
248
|
+
Deploy your agent to a cloud VPS with HTTPS.
|
|
249
|
+
|
|
250
|
+
### 1. Server prerequisites
|
|
251
|
+
|
|
252
|
+
You need a VPS (any provider — Hetzner, DigitalOcean, AWS, etc.) with:
|
|
253
|
+
|
|
254
|
+
- Docker + Docker Compose
|
|
255
|
+
- Node.js 18+
|
|
256
|
+
- Git
|
|
257
|
+
- GitHub CLI (`gh`)
|
|
258
|
+
|
|
259
|
+
Point a domain (e.g., `mybot.example.com`) to your server's IP address with a DNS A record.
|
|
260
|
+
|
|
261
|
+
### 2. Scaffold and configure
|
|
262
|
+
|
|
263
|
+
SSH into your server and scaffold the project:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
mkdir my-agent && cd my-agent
|
|
267
|
+
npx thepopebot@latest init
|
|
268
|
+
npm run setup
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
When the setup wizard asks for `APP_URL`, enter your production URL with `https://` (e.g., `https://mybot.example.com`).
|
|
272
|
+
|
|
273
|
+
Set the `RUNS_ON` GitHub variable so workflows use your server's self-hosted runner instead of GitHub-hosted runners:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
gh variable set RUNS_ON --body "self-hosted" --repo OWNER/REPO
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 3. Enable HTTPS (Let's Encrypt)
|
|
280
|
+
|
|
281
|
+
The `docker-compose.yml` has Let's Encrypt support built in but commented out. Three edits to enable it:
|
|
282
|
+
|
|
283
|
+
**a) Add your email to `.env`:**
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
LETSENCRYPT_EMAIL=you@example.com
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**b) Uncomment the TLS lines in the traefik service command:**
|
|
290
|
+
|
|
291
|
+
```yaml
|
|
292
|
+
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
|
293
|
+
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
|
294
|
+
- --certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}
|
|
295
|
+
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
|
|
296
|
+
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**c) Switch the event-handler labels from HTTP to HTTPS:**
|
|
300
|
+
|
|
301
|
+
Comment out the HTTP entrypoint and uncomment the two HTTPS lines:
|
|
302
|
+
|
|
303
|
+
```yaml
|
|
304
|
+
# - traefik.http.routers.event-handler.entrypoints=web
|
|
305
|
+
- traefik.http.routers.event-handler.entrypoints=websecure
|
|
306
|
+
- traefik.http.routers.event-handler.tls.certresolver=letsencrypt
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### 4. Build and launch
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
npm run build
|
|
313
|
+
docker compose up -d
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Ports 80 and 443 must be open on your server. Port 80 is required even with HTTPS — Let's Encrypt uses it for the ACME HTTP challenge to verify domain ownership.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
246
320
|
## Docs
|
|
247
321
|
|
|
248
322
|
| Document | Description |
|
package/bin/cli.js
CHANGED
|
@@ -50,11 +50,15 @@ function printUsage() {
|
|
|
50
50
|
Usage: thepopebot <command>
|
|
51
51
|
|
|
52
52
|
Commands:
|
|
53
|
-
init
|
|
54
|
-
setup
|
|
55
|
-
setup-telegram
|
|
56
|
-
reset-auth
|
|
57
|
-
reset [file]
|
|
53
|
+
init Scaffold a new thepopebot project
|
|
54
|
+
setup Run interactive setup wizard
|
|
55
|
+
setup-telegram Reconfigure Telegram webhook
|
|
56
|
+
reset-auth Regenerate AUTH_SECRET (invalidates all sessions)
|
|
57
|
+
reset [file] Restore a template file (or list available templates)
|
|
58
|
+
diff [file] Show differences between project files and package templates
|
|
59
|
+
set-agent-secret <KEY> [VALUE] Set a GitHub secret with AGENT_ prefix (also updates .env)
|
|
60
|
+
set-agent-llm-secret <KEY> [VALUE] Set a GitHub secret with AGENT_LLM_ prefix
|
|
61
|
+
set-var <KEY> [VALUE] Set a GitHub repository variable
|
|
58
62
|
`);
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -199,6 +203,17 @@ async function init() {
|
|
|
199
203
|
}
|
|
200
204
|
}
|
|
201
205
|
|
|
206
|
+
// Create default skill symlinks (brave-search, browser-tools)
|
|
207
|
+
const defaultSkills = ['brave-search', 'browser-tools'];
|
|
208
|
+
for (const skill of defaultSkills) {
|
|
209
|
+
const symlink = path.join(cwd, '.pi', 'skills', skill);
|
|
210
|
+
if (!fs.existsSync(symlink)) {
|
|
211
|
+
fs.mkdirSync(path.dirname(symlink), { recursive: true });
|
|
212
|
+
fs.symlinkSync(`../../pi-skills/${skill}`, symlink);
|
|
213
|
+
console.log(` Created .pi/skills/${skill} → ../../pi-skills/${skill}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
202
217
|
// Report updated managed files
|
|
203
218
|
if (updated.length > 0) {
|
|
204
219
|
console.log('\n Updated managed files:');
|
|
@@ -399,6 +414,115 @@ async function resetAuth() {
|
|
|
399
414
|
console.log(' Restart your server for the change to take effect.\n');
|
|
400
415
|
}
|
|
401
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Load GH_OWNER and GH_REPO from .env
|
|
419
|
+
*/
|
|
420
|
+
function loadRepoInfo() {
|
|
421
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
422
|
+
if (!fs.existsSync(envPath)) {
|
|
423
|
+
console.error('\n No .env file found. Run "npm run setup" first.\n');
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
const content = fs.readFileSync(envPath, 'utf-8');
|
|
427
|
+
const env = {};
|
|
428
|
+
for (const line of content.split('\n')) {
|
|
429
|
+
const match = line.match(/^([^#=]+)=(.*)$/);
|
|
430
|
+
if (match) env[match[1].trim()] = match[2].trim();
|
|
431
|
+
}
|
|
432
|
+
if (!env.GH_OWNER || !env.GH_REPO) {
|
|
433
|
+
console.error('\n GH_OWNER and GH_REPO not found in .env. Run "npm run setup" first.\n');
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
return { owner: env.GH_OWNER, repo: env.GH_REPO };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Prompt for a secret value interactively if not provided as an argument
|
|
441
|
+
*/
|
|
442
|
+
async function promptForValue(key) {
|
|
443
|
+
const { default: inquirer } = await import('inquirer');
|
|
444
|
+
const { value } = await inquirer.prompt([{
|
|
445
|
+
type: 'password',
|
|
446
|
+
name: 'value',
|
|
447
|
+
message: `Enter value for ${key}:`,
|
|
448
|
+
mask: '*',
|
|
449
|
+
validate: (input) => input ? true : 'Value is required',
|
|
450
|
+
}]);
|
|
451
|
+
return value;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function setAgentSecret(key, value) {
|
|
455
|
+
if (!key) {
|
|
456
|
+
console.error('\n Usage: thepopebot set-agent-secret <KEY> [VALUE]\n');
|
|
457
|
+
console.error(' Example: thepopebot set-agent-secret ANTHROPIC_API_KEY\n');
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!value) value = await promptForValue(key);
|
|
462
|
+
|
|
463
|
+
const { owner, repo } = loadRepoInfo();
|
|
464
|
+
const prefixedName = `AGENT_${key}`;
|
|
465
|
+
|
|
466
|
+
const { setSecret } = await import(path.join(__dirname, '..', 'setup', 'lib', 'github.mjs'));
|
|
467
|
+
const { updateEnvVariable } = await import(path.join(__dirname, '..', 'setup', 'lib', 'auth.mjs'));
|
|
468
|
+
|
|
469
|
+
const result = await setSecret(owner, repo, prefixedName, value);
|
|
470
|
+
if (result.success) {
|
|
471
|
+
console.log(`\n Set GitHub secret: ${prefixedName}`);
|
|
472
|
+
updateEnvVariable(key, value);
|
|
473
|
+
console.log(` Updated .env: ${key}`);
|
|
474
|
+
console.log('');
|
|
475
|
+
} else {
|
|
476
|
+
console.error(`\n Failed to set ${prefixedName}: ${result.error}\n`);
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function setAgentLlmSecret(key, value) {
|
|
482
|
+
if (!key) {
|
|
483
|
+
console.error('\n Usage: thepopebot set-agent-llm-secret <KEY> [VALUE]\n');
|
|
484
|
+
console.error(' Example: thepopebot set-agent-llm-secret BRAVE_API_KEY\n');
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!value) value = await promptForValue(key);
|
|
489
|
+
|
|
490
|
+
const { owner, repo } = loadRepoInfo();
|
|
491
|
+
const prefixedName = `AGENT_LLM_${key}`;
|
|
492
|
+
|
|
493
|
+
const { setSecret } = await import(path.join(__dirname, '..', 'setup', 'lib', 'github.mjs'));
|
|
494
|
+
|
|
495
|
+
const result = await setSecret(owner, repo, prefixedName, value);
|
|
496
|
+
if (result.success) {
|
|
497
|
+
console.log(`\n Set GitHub secret: ${prefixedName}\n`);
|
|
498
|
+
} else {
|
|
499
|
+
console.error(`\n Failed to set ${prefixedName}: ${result.error}\n`);
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function setVar(key, value) {
|
|
505
|
+
if (!key) {
|
|
506
|
+
console.error('\n Usage: thepopebot set-var <KEY> [VALUE]\n');
|
|
507
|
+
console.error(' Example: thepopebot set-var LLM_MODEL claude-sonnet-4-5-20250929\n');
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!value) value = await promptForValue(key);
|
|
512
|
+
|
|
513
|
+
const { owner, repo } = loadRepoInfo();
|
|
514
|
+
|
|
515
|
+
const { setVariable } = await import(path.join(__dirname, '..', 'setup', 'lib', 'github.mjs'));
|
|
516
|
+
|
|
517
|
+
const result = await setVariable(owner, repo, key, value);
|
|
518
|
+
if (result.success) {
|
|
519
|
+
console.log(`\n Set GitHub variable: ${key}\n`);
|
|
520
|
+
} else {
|
|
521
|
+
console.error(`\n Failed to set ${key}: ${result.error}\n`);
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
402
526
|
switch (command) {
|
|
403
527
|
case 'init':
|
|
404
528
|
await init();
|
|
@@ -418,6 +542,15 @@ switch (command) {
|
|
|
418
542
|
case 'diff':
|
|
419
543
|
diff(args[0]);
|
|
420
544
|
break;
|
|
545
|
+
case 'set-agent-secret':
|
|
546
|
+
await setAgentSecret(args[0], args[1]);
|
|
547
|
+
break;
|
|
548
|
+
case 'set-agent-llm-secret':
|
|
549
|
+
await setAgentLlmSecret(args[0], args[1]);
|
|
550
|
+
break;
|
|
551
|
+
case 'set-var':
|
|
552
|
+
await setVar(args[0], args[1]);
|
|
553
|
+
break;
|
|
421
554
|
default:
|
|
422
555
|
printUsage();
|
|
423
556
|
process.exit(command ? 1 : 0);
|
package/lib/ai/agent.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
|
2
|
+
import { SystemMessage } from '@langchain/core/messages';
|
|
2
3
|
import { createModel } from './model.js';
|
|
3
4
|
import { createJobTool, getJobStatusTool } from './tools.js';
|
|
4
5
|
import { SqliteSaver } from '@langchain/langgraph-checkpoint-sqlite';
|
|
@@ -10,19 +11,19 @@ let _agent = null;
|
|
|
10
11
|
/**
|
|
11
12
|
* Get or create the LangGraph agent singleton.
|
|
12
13
|
* Uses createReactAgent which handles the tool loop automatically.
|
|
14
|
+
* Prompt is a function so {{datetime}} resolves fresh each invocation.
|
|
13
15
|
*/
|
|
14
16
|
export async function getAgent() {
|
|
15
17
|
if (!_agent) {
|
|
16
18
|
const model = await createModel();
|
|
17
19
|
const tools = [createJobTool, getJobStatusTool];
|
|
18
20
|
const checkpointer = SqliteSaver.fromConnString(thepopebotDb);
|
|
19
|
-
const systemPrompt = render_md(chatbotMd);
|
|
20
21
|
|
|
21
22
|
_agent = createReactAgent({
|
|
22
23
|
llm: model,
|
|
23
24
|
tools,
|
|
24
25
|
checkpointSaver: checkpointer,
|
|
25
|
-
prompt:
|
|
26
|
+
prompt: (state) => [new SystemMessage(render_md(chatbotMd)), ...state.messages],
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
return _agent;
|
package/lib/cron.js
CHANGED
|
@@ -35,8 +35,11 @@ function setUpdateAvailable(v) {
|
|
|
35
35
|
* @returns {boolean} true if candidate > baseline
|
|
36
36
|
*/
|
|
37
37
|
function isVersionNewer(candidate, baseline) {
|
|
38
|
+
// Pre-release candidate is never "newer" for upgrade purposes
|
|
39
|
+
if (candidate.includes('-')) return false;
|
|
40
|
+
|
|
38
41
|
const a = candidate.split('.').map(Number);
|
|
39
|
-
const b = baseline.split('.').map(Number);
|
|
42
|
+
const b = baseline.replace(/-.*$/, '').split('.').map(Number);
|
|
40
43
|
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
41
44
|
const av = a[i] || 0;
|
|
42
45
|
const bv = b[i] || 0;
|
package/lib/utils/render-md.js
CHANGED
|
@@ -3,9 +3,27 @@ import path from 'path';
|
|
|
3
3
|
import { PROJECT_ROOT } from '../paths.js';
|
|
4
4
|
|
|
5
5
|
const INCLUDE_PATTERN = /\{\{([^}]+\.md)\}\}/g;
|
|
6
|
+
const VARIABLE_PATTERN = /\{\{(datetime)\}\}/gi;
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
+
* Resolve built-in variables like {{datetime}}.
|
|
10
|
+
* @param {string} content - Content with possible variable placeholders
|
|
11
|
+
* @returns {string} Content with variables resolved
|
|
12
|
+
*/
|
|
13
|
+
function resolveVariables(content) {
|
|
14
|
+
return content.replace(VARIABLE_PATTERN, (match, variable) => {
|
|
15
|
+
switch (variable.toLowerCase()) {
|
|
16
|
+
case 'datetime':
|
|
17
|
+
return new Date().toISOString();
|
|
18
|
+
default:
|
|
19
|
+
return match;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render a markdown file, resolving {{filepath}} includes recursively
|
|
26
|
+
* and {{datetime}} built-in variables.
|
|
9
27
|
* Referenced file paths resolve relative to the project root.
|
|
10
28
|
* @param {string} filePath - Absolute path to the markdown file
|
|
11
29
|
* @param {string[]} [chain=[]] - Already-resolved file paths (for circular detection)
|
|
@@ -27,13 +45,15 @@ function render_md(filePath, chain = []) {
|
|
|
27
45
|
const content = fs.readFileSync(resolved, 'utf8');
|
|
28
46
|
const currentChain = [...chain, resolved];
|
|
29
47
|
|
|
30
|
-
|
|
48
|
+
const withIncludes = content.replace(INCLUDE_PATTERN, (match, includePath) => {
|
|
31
49
|
const includeResolved = path.resolve(PROJECT_ROOT, includePath.trim());
|
|
32
50
|
if (!fs.existsSync(includeResolved)) {
|
|
33
51
|
return match;
|
|
34
52
|
}
|
|
35
53
|
return render_md(includeResolved, currentChain);
|
|
36
54
|
});
|
|
55
|
+
|
|
56
|
+
return resolveVariables(withIncludes);
|
|
37
57
|
}
|
|
38
58
|
|
|
39
59
|
export { render_md };
|
package/package.json
CHANGED
package/setup/lib/auth.mjs
CHANGED
|
@@ -39,41 +39,6 @@ export async function validateAnthropicKey(key) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
* Build flat secrets JSON for SECRETS GitHub secret.
|
|
44
|
-
* Takes a flat { ENV_VAR: value } map of collected keys.
|
|
45
|
-
* entrypoint.sh decodes and exports each as an env var.
|
|
46
|
-
*/
|
|
47
|
-
export function buildSecretsJson(pat, collectedKeys) {
|
|
48
|
-
return { GH_TOKEN: pat, ...collectedKeys };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Encode secrets to base64 for SECRETS GitHub secret
|
|
53
|
-
*/
|
|
54
|
-
export function encodeSecretsBase64(pat, collectedKeys) {
|
|
55
|
-
const secrets = buildSecretsJson(pat, collectedKeys);
|
|
56
|
-
return Buffer.from(JSON.stringify(secrets)).toString('base64');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Build LLM secrets JSON for LLM_SECRETS GitHub secret.
|
|
61
|
-
* These credentials are accessible to the LLM (not filtered by env-sanitizer).
|
|
62
|
-
*/
|
|
63
|
-
export function buildLlmSecretsJson(llmKeys) {
|
|
64
|
-
return { ...llmKeys };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Encode LLM secrets to base64 for LLM_SECRETS GitHub secret.
|
|
69
|
-
* Returns null if no LLM secrets are configured.
|
|
70
|
-
*/
|
|
71
|
-
export function encodeLlmSecretsBase64(llmKeys) {
|
|
72
|
-
const secrets = buildLlmSecretsJson(llmKeys);
|
|
73
|
-
if (Object.keys(secrets).length === 0) return null;
|
|
74
|
-
return Buffer.from(JSON.stringify(secrets)).toString('base64');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
42
|
/**
|
|
78
43
|
* Generate .pi/agent/models.json for providers not built into PI.
|
|
79
44
|
* PI resolves the apiKey field as an env var name at runtime ($ENV_VAR).
|
package/setup/setup.mjs
CHANGED
|
@@ -37,8 +37,6 @@ import {
|
|
|
37
37
|
writeEnvFile,
|
|
38
38
|
updateEnvVariable,
|
|
39
39
|
writeModelsJson,
|
|
40
|
-
encodeSecretsBase64,
|
|
41
|
-
encodeLlmSecretsBase64,
|
|
42
40
|
} from './lib/auth.mjs';
|
|
43
41
|
|
|
44
42
|
const logo = `
|
|
@@ -575,24 +573,24 @@ async function main() {
|
|
|
575
573
|
}
|
|
576
574
|
|
|
577
575
|
// Skip GitHub secrets if nothing changed on re-run
|
|
578
|
-
let
|
|
576
|
+
let secrets = null;
|
|
579
577
|
if (isRerun && !credentialsChanged && env?.GH_WEBHOOK_SECRET) {
|
|
580
578
|
printSuccess('GitHub secrets unchanged');
|
|
581
579
|
webhookSecret = env.GH_WEBHOOK_SECRET;
|
|
582
580
|
} else {
|
|
583
581
|
webhookSecret = generateWebhookSecret();
|
|
584
|
-
const secretsBase64 = encodeSecretsBase64(pat, collectedKeys);
|
|
585
|
-
const llmKeys = {};
|
|
586
|
-
if (braveKey) llmKeys.BRAVE_API_KEY = braveKey;
|
|
587
|
-
llmSecretsBase64 = encodeLlmSecretsBase64(llmKeys);
|
|
588
582
|
|
|
589
|
-
|
|
590
|
-
SECRETS: secretsBase64,
|
|
583
|
+
secrets = {
|
|
591
584
|
GH_WEBHOOK_SECRET: webhookSecret,
|
|
585
|
+
AGENT_GH_TOKEN: pat,
|
|
592
586
|
};
|
|
593
587
|
|
|
594
|
-
|
|
595
|
-
secrets
|
|
588
|
+
for (const [key, value] of Object.entries(collectedKeys)) {
|
|
589
|
+
if (value) secrets[`AGENT_${key}`] = value;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (braveKey) {
|
|
593
|
+
secrets.AGENT_LLM_BRAVE_API_KEY = braveKey;
|
|
596
594
|
}
|
|
597
595
|
|
|
598
596
|
let allSecretsSet = false;
|
|
@@ -623,6 +621,12 @@ async function main() {
|
|
|
623
621
|
LLM_PROVIDER: agentProvider,
|
|
624
622
|
LLM_MODEL: agentModel,
|
|
625
623
|
};
|
|
624
|
+
if (openaiBaseUrl) {
|
|
625
|
+
defaultVars.OPENAI_BASE_URL = openaiBaseUrl;
|
|
626
|
+
}
|
|
627
|
+
if (agentProvider === 'custom') {
|
|
628
|
+
defaultVars.RUNS_ON = 'self-hosted';
|
|
629
|
+
}
|
|
626
630
|
|
|
627
631
|
let allVarsSet = false;
|
|
628
632
|
while (!allVarsSet) {
|
|
@@ -821,11 +825,11 @@ async function main() {
|
|
|
821
825
|
}
|
|
822
826
|
if (braveKey) console.log(` ${chalk.dim('Brave Search:')} ${maskSecret(braveKey)}`);
|
|
823
827
|
|
|
824
|
-
if (
|
|
828
|
+
if (secrets) {
|
|
825
829
|
console.log(chalk.bold('\n GitHub Secrets Set:\n'));
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
830
|
+
for (const name of Object.keys(secrets)) {
|
|
831
|
+
console.log(` \u2022 ${name}`);
|
|
832
|
+
}
|
|
829
833
|
|
|
830
834
|
console.log(chalk.bold('\n GitHub Variables Set:\n'));
|
|
831
835
|
console.log(' \u2022 APP_URL');
|
|
@@ -833,6 +837,8 @@ async function main() {
|
|
|
833
837
|
console.log(' \u2022 ALLOWED_PATHS = /logs');
|
|
834
838
|
console.log(` \u2022 LLM_PROVIDER = ${agentProvider}`);
|
|
835
839
|
console.log(` \u2022 LLM_MODEL = ${agentModel}`);
|
|
840
|
+
if (openaiBaseUrl) console.log(` \u2022 OPENAI_BASE_URL = ${openaiBaseUrl}`);
|
|
841
|
+
if (agentProvider === 'custom') console.log(' \u2022 RUNS_ON = self-hosted');
|
|
836
842
|
}
|
|
837
843
|
|
|
838
844
|
console.log(chalk.bold.green('\n You\'re all set!\n'));
|
|
@@ -35,10 +35,12 @@ jobs:
|
|
|
35
35
|
|
|
36
36
|
- name: Run thepopebot Agent
|
|
37
37
|
env:
|
|
38
|
+
ALL_SECRETS: ${{ toJson(secrets) }}
|
|
38
39
|
JOB_IMAGE_URL: ${{ vars.JOB_IMAGE_URL }}
|
|
39
40
|
THEPOPEBOT_VERSION: ${{ steps.version.outputs.tag }}
|
|
40
41
|
LLM_MODEL: ${{ vars.LLM_MODEL }}
|
|
41
42
|
LLM_PROVIDER: ${{ vars.LLM_PROVIDER }}
|
|
43
|
+
OPENAI_BASE_URL: ${{ vars.OPENAI_BASE_URL }}
|
|
42
44
|
run: |
|
|
43
45
|
if [ -n "$JOB_IMAGE_URL" ]; then
|
|
44
46
|
IMAGE="${JOB_IMAGE_URL}:latest"
|
|
@@ -46,11 +48,23 @@ jobs:
|
|
|
46
48
|
IMAGE="stephengpope/thepopebot:job-${THEPOPEBOT_VERSION}"
|
|
47
49
|
fi
|
|
48
50
|
echo "Using image: $IMAGE"
|
|
51
|
+
|
|
52
|
+
# Build SECRETS JSON from AGENT_* (excluding AGENT_LLM_*) secrets
|
|
53
|
+
export SECRETS=$(echo "$ALL_SECRETS" | jq -c '
|
|
54
|
+
[to_entries[] | select(.key | startswith("AGENT_")) | select(.key | startswith("AGENT_LLM_") | not)]
|
|
55
|
+
| map({key: (.key | sub("^AGENT_"; "")), value: .value}) | from_entries')
|
|
56
|
+
|
|
57
|
+
# Build LLM_SECRETS JSON from AGENT_LLM_* secrets
|
|
58
|
+
export LLM_SECRETS=$(echo "$ALL_SECRETS" | jq -c '
|
|
59
|
+
[to_entries[] | select(.key | startswith("AGENT_LLM_"))]
|
|
60
|
+
| map({key: (.key | sub("^AGENT_LLM_"; "")), value: .value}) | from_entries')
|
|
61
|
+
|
|
49
62
|
docker run --rm \
|
|
50
63
|
-e REPO_URL="${{ github.server_url }}/${{ github.repository }}.git" \
|
|
51
64
|
-e BRANCH="${{ github.ref_name }}" \
|
|
52
|
-
-e SECRETS
|
|
53
|
-
-e LLM_SECRETS
|
|
65
|
+
-e SECRETS \
|
|
66
|
+
-e LLM_SECRETS \
|
|
54
67
|
-e LLM_MODEL="${LLM_MODEL}" \
|
|
55
68
|
-e LLM_PROVIDER="${LLM_PROVIDER}" \
|
|
69
|
+
-e OPENAI_BASE_URL="${OPENAI_BASE_URL}" \
|
|
56
70
|
"$IMAGE"
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
# Pi system prompt (generated at runtime from SOUL.md)
|
|
11
11
|
.pi/SYSTEM.md
|
|
12
12
|
|
|
13
|
-
# Pi skills
|
|
14
|
-
|
|
13
|
+
# Pi skills dependencies (installed at runtime in Docker for correct arch)
|
|
14
|
+
pi-skills/*/node_modules/
|
|
15
15
|
|
|
16
16
|
# Node
|
|
17
17
|
node_modules/
|
|
@@ -9,16 +9,15 @@
|
|
|
9
9
|
* To get a value, use: echo $KEY_NAME
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const secretsJson = process.env.LLM_SECRETS;
|
|
13
13
|
|
|
14
|
-
if (!
|
|
14
|
+
if (!secretsJson) {
|
|
15
15
|
console.log('No LLM_SECRETS configured.');
|
|
16
16
|
process.exit(0);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
try {
|
|
20
|
-
const
|
|
21
|
-
const parsed = JSON.parse(decoded);
|
|
20
|
+
const parsed = JSON.parse(secretsJson);
|
|
22
21
|
const keys = Object.keys(parsed);
|
|
23
22
|
|
|
24
23
|
if (keys.length === 0) {
|
|
@@ -9,7 +9,8 @@ This is an autonomous AI agent powered by [thepopebot](https://github.com/stephe
|
|
|
9
9
|
```
|
|
10
10
|
/
|
|
11
11
|
├── .github/workflows/ # CI/CD workflows (managed by thepopebot)
|
|
12
|
-
├── .pi/skills/ #
|
|
12
|
+
├── .pi/skills/ # Symlinks to active Pi agent skills
|
|
13
|
+
├── pi-skills/ # All available Pi agent skills (from thepopebot package)
|
|
13
14
|
├── config/ # Agent configuration
|
|
14
15
|
│ ├── SOUL.md # Agent personality and identity
|
|
15
16
|
│ ├── CHATBOT.md # Telegram chat system prompt
|
|
@@ -39,7 +40,8 @@ This is an autonomous AI agent powered by [thepopebot](https://github.com/stephe
|
|
|
39
40
|
## Customization
|
|
40
41
|
|
|
41
42
|
- **config/** — Agent personality, prompts, crons, triggers
|
|
42
|
-
-
|
|
43
|
+
- **pi-skills/** — All available Pi agent skills (brave-search, browser-tools, youtube-transcript, etc.)
|
|
44
|
+
- **.pi/skills/** — Symlinks to active skills (e.g., `ln -s ../../pi-skills/youtube-transcript .pi/skills/youtube-transcript`)
|
|
43
45
|
- **cron/** and **triggers/** — Shell scripts for command-type actions
|
|
44
46
|
- **app/** — Add Next.js pages, API routes, components
|
|
45
47
|
|
|
@@ -29,4 +29,6 @@ So you can assume that:
|
|
|
29
29
|
|
|
30
30
|
**Always** use `/job/tmp/` for any temporary files you create.
|
|
31
31
|
|
|
32
|
-
Scripts in `/job/tmp/` can use `__dirname`-relative paths (e.g., `../docs/data.json`) to reference repo files, because they're inside the repo tree. The `.gitignore` excludes `tmp/` so nothing in this directory gets committed.
|
|
32
|
+
Scripts in `/job/tmp/` can use `__dirname`-relative paths (e.g., `../docs/data.json`) to reference repo files, because they're inside the repo tree. The `.gitignore` excludes `tmp/` so nothing in this directory gets committed.
|
|
33
|
+
|
|
34
|
+
Current datetime: {{datetime}}
|
|
@@ -36,13 +36,6 @@ RUN npm install -g @mariozechner/pi-coding-agent
|
|
|
36
36
|
# Create Pi config directory (extension loaded from repo at runtime)
|
|
37
37
|
RUN mkdir -p /root/.pi/agent
|
|
38
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
39
|
COPY entrypoint.sh /entrypoint.sh
|
|
47
40
|
RUN chmod +x /entrypoint.sh
|
|
48
41
|
|
|
@@ -9,25 +9,16 @@ else
|
|
|
9
9
|
fi
|
|
10
10
|
echo "Job ID: ${JOB_ID}"
|
|
11
11
|
|
|
12
|
-
#
|
|
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.)
|
|
12
|
+
# Export SECRETS (JSON) as flat env vars (GH_TOKEN, ANTHROPIC_API_KEY, etc.)
|
|
19
13
|
# These are filtered from LLM's bash subprocess by env-sanitizer extension
|
|
20
14
|
if [ -n "$SECRETS" ]; then
|
|
21
|
-
|
|
22
|
-
eval $(echo "$SECRETS_JSON" | jq -r 'to_entries | .[] | "export \(.key)=\"\(.value)\""')
|
|
23
|
-
export SECRETS="$SECRETS_JSON" # Keep decoded for extension to parse
|
|
15
|
+
eval $(echo "$SECRETS" | jq -r 'to_entries | .[] | "export \(.key)=\"\(.value)\""')
|
|
24
16
|
fi
|
|
25
17
|
|
|
26
|
-
# Export LLM_SECRETS (
|
|
18
|
+
# Export LLM_SECRETS (JSON) as flat env vars
|
|
27
19
|
# These are NOT filtered - LLM can access these (browser logins, skill API keys, etc.)
|
|
28
20
|
if [ -n "$LLM_SECRETS" ]; then
|
|
29
|
-
|
|
30
|
-
eval $(echo "$LLM_SECRETS_JSON" | jq -r 'to_entries | .[] | "export \(.key)=\"\(.value)\""')
|
|
21
|
+
eval $(echo "$LLM_SECRETS" | jq -r 'to_entries | .[] | "export \(.key)=\"\(.value)\""')
|
|
31
22
|
fi
|
|
32
23
|
|
|
33
24
|
# Git setup - derive identity from GitHub token
|
|
@@ -50,8 +41,22 @@ cd /job
|
|
|
50
41
|
# Create temp directory for agent use (gitignored via tmp/)
|
|
51
42
|
mkdir -p /job/tmp
|
|
52
43
|
|
|
53
|
-
#
|
|
54
|
-
|
|
44
|
+
# Install npm deps for symlinked skills (native deps need correct Linux arch)
|
|
45
|
+
for skill_dir in /job/.pi/skills/*/; do
|
|
46
|
+
if [ -f "${skill_dir}package.json" ]; then
|
|
47
|
+
echo "Installing skill deps: $(basename "$skill_dir")"
|
|
48
|
+
(cd "$skill_dir" && npm install --production)
|
|
49
|
+
fi
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
# Start Chrome if available (installed by browser-tools skill via Puppeteer)
|
|
53
|
+
CHROME_PID=""
|
|
54
|
+
CHROME_BIN=$(find /root/.cache/puppeteer -name "chrome" -type f 2>/dev/null | head -1)
|
|
55
|
+
if [ -n "$CHROME_BIN" ]; then
|
|
56
|
+
$CHROME_BIN --headless --no-sandbox --disable-gpu --remote-debugging-port=9222 2>/dev/null &
|
|
57
|
+
CHROME_PID=$!
|
|
58
|
+
sleep 2
|
|
59
|
+
fi
|
|
55
60
|
|
|
56
61
|
# Setup logs
|
|
57
62
|
LOG_DIR="/job/logs/${JOB_ID}"
|
|
@@ -67,6 +72,9 @@ for i in "${!SYSTEM_FILES[@]}"; do
|
|
|
67
72
|
fi
|
|
68
73
|
done
|
|
69
74
|
|
|
75
|
+
# Resolve {{datetime}} variable in SYSTEM.md
|
|
76
|
+
sed -i "s/{{datetime}}/$(date -u +"%Y-%m-%dT%H:%M:%SZ")/g" /job/.pi/SYSTEM.md
|
|
77
|
+
|
|
70
78
|
PROMPT="
|
|
71
79
|
|
|
72
80
|
# Your Job
|
|
@@ -80,7 +88,27 @@ if [ -n "$LLM_MODEL" ]; then
|
|
|
80
88
|
MODEL_FLAGS="$MODEL_FLAGS --model $LLM_MODEL"
|
|
81
89
|
fi
|
|
82
90
|
|
|
83
|
-
#
|
|
91
|
+
# Generate models.json for custom provider (OpenAI-compatible endpoints like Ollama)
|
|
92
|
+
if [ "$LLM_PROVIDER" = "custom" ] && [ -n "$OPENAI_BASE_URL" ]; then
|
|
93
|
+
# If no API key was provided, set a dummy so Pi doesn't send empty auth
|
|
94
|
+
if [ -z "$CUSTOM_API_KEY" ]; then
|
|
95
|
+
export CUSTOM_API_KEY="not-needed"
|
|
96
|
+
fi
|
|
97
|
+
cat > /root/.pi/agent/models.json <<MODELS
|
|
98
|
+
{
|
|
99
|
+
"providers": {
|
|
100
|
+
"custom": {
|
|
101
|
+
"baseUrl": "$OPENAI_BASE_URL",
|
|
102
|
+
"api": "openai-completions",
|
|
103
|
+
"apiKey": "CUSTOM_API_KEY",
|
|
104
|
+
"models": [{ "id": "$LLM_MODEL" }]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
MODELS
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# Copy custom models.json to PI's global config if present in repo (overrides generated)
|
|
84
112
|
if [ -f "/job/.pi/agent/models.json" ]; then
|
|
85
113
|
mkdir -p /root/.pi/agent
|
|
86
114
|
cp /job/.pi/agent/models.json /root/.pi/agent/models.json
|
|
@@ -104,5 +132,7 @@ git push origin
|
|
|
104
132
|
gh pr create --title "thepopebot: job ${JOB_ID}" --body "Automated job" --base main || true
|
|
105
133
|
|
|
106
134
|
# Cleanup
|
|
107
|
-
|
|
135
|
+
if [ -n "$CHROME_PID" ]; then
|
|
136
|
+
kill $CHROME_PID 2>/dev/null || true
|
|
137
|
+
fi
|
|
108
138
|
echo "Done. Job ID: ${JOB_ID}"
|