thepopebot 1.2.75-beta.16 → 1.2.75-beta.18
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/api/CLAUDE.md +1 -1
- package/api/index.js +5 -12
- package/bin/cli.js +290 -4
- package/lib/chat/components/chats-page.js +3 -3
- package/lib/chat/components/chats-page.jsx +4 -6
- package/lib/chat/components/sidebar-history-item.js +3 -3
- package/lib/chat/components/sidebar-history-item.jsx +4 -6
- package/lib/tools/docker.js +0 -9
- package/package.json +1 -1
- package/templates/.gitignore.template +1 -1
- package/templates/agent-job/SYSTEM.md +6 -1
- package/templates/data/CLAUDE.md.template +5 -0
- package/templates/logs/CLAUDE.md.template +5 -0
- package/templates/skills/CLAUDE.md.template +47 -22
- package/templates/skills/agent-job-secrets/SKILL.md +7 -9
- package/templates/skills/agent-job-secrets/agent-job-secrets.js +26 -39
- package/templates/.pi/extensions/env-sanitizer/index.ts +0 -48
- package/templates/.pi/extensions/env-sanitizer/package.json +0 -5
- package/templates/agents/.gitkeep +0 -0
- package/templates/logs/.gitkeep +0 -0
package/api/CLAUDE.md
CHANGED
|
@@ -26,7 +26,7 @@ Browser-facing data fetching uses **fetch route handlers** colocated with pages
|
|
|
26
26
|
| GET | `/api/ping` | None | Health check |
|
|
27
27
|
| POST | `/api/create-agent-job` | `x-api-key` | Create agent job |
|
|
28
28
|
| GET | `/api/get-agent-job-secret` | `x-api-key` | Get an agent job secret; oauth2 credentials return only the access_token (auto-refreshed) |
|
|
29
|
-
|
|
|
29
|
+
| GET | `/api/agent-job-list-secrets` | `x-api-key` | List agent job secret keys (no values); returns `{secrets: [{key, isSet, updatedAt, secretType}]}` |
|
|
30
30
|
| GET | `/api/agent-jobs/status` | `x-api-key` | Agent job status (query: `?agent_job_id=`) |
|
|
31
31
|
| POST | `/api/telegram/webhook` | Telegram webhook secret | Telegram message handler |
|
|
32
32
|
| POST | `/api/telegram/register` | `x-api-key` | Register bot token + webhook URL |
|
package/api/index.js
CHANGED
|
@@ -166,20 +166,13 @@ async function handleGetAgentSecret(request) {
|
|
|
166
166
|
return Response.json({ value: raw });
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
async function
|
|
169
|
+
async function handleListAgentSecrets(request) {
|
|
170
170
|
const record = verifyApiKey(request.headers.get('x-api-key'));
|
|
171
171
|
if (record.type !== 'agent_job_api_key') {
|
|
172
172
|
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
173
173
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const { key, value } = body;
|
|
177
|
-
if (!key || typeof value !== 'string') {
|
|
178
|
-
return Response.json({ error: 'Missing key or value' }, { status: 400 });
|
|
179
|
-
}
|
|
180
|
-
const { setAgentJobSecret } = await import('../lib/db/config.js');
|
|
181
|
-
setAgentJobSecret(key, value, 'agent');
|
|
182
|
-
return Response.json({ success: true });
|
|
174
|
+
const { listAgentJobSecrets } = await import('../lib/db/config.js');
|
|
175
|
+
return Response.json({ secrets: listAgentJobSecrets() });
|
|
183
176
|
}
|
|
184
177
|
|
|
185
178
|
async function handleTelegramRegister(request) {
|
|
@@ -405,7 +398,6 @@ async function POST(request) {
|
|
|
405
398
|
// Route to handler
|
|
406
399
|
switch (routePath) {
|
|
407
400
|
case '/create-agent-job': return handleCreateAgentJob(request);
|
|
408
|
-
case '/set-agent-job-secret': return handleSetAgentSecret(request);
|
|
409
401
|
case '/telegram/webhook': return handleTelegramWebhook(request);
|
|
410
402
|
case '/telegram/register': return handleTelegramRegister(request);
|
|
411
403
|
case '/github/webhook': return handleGithubWebhook(request);
|
|
@@ -424,7 +416,8 @@ async function GET(request) {
|
|
|
424
416
|
switch (routePath) {
|
|
425
417
|
case '/ping': return Response.json({ message: 'Pong!' });
|
|
426
418
|
case '/agent-jobs/status': return handleAgentJobStatus(request);
|
|
427
|
-
case '/get-agent-job-secret':
|
|
419
|
+
case '/get-agent-job-secret': return handleGetAgentSecret(request);
|
|
420
|
+
case '/agent-job-list-secrets': return handleListAgentSecrets(request);
|
|
428
421
|
case '/oauth/callback': return handleOAuthCallback(request);
|
|
429
422
|
default: return Response.json({ error: 'Not found' }, { status: 404 });
|
|
430
423
|
}
|
package/bin/cli.js
CHANGED
|
@@ -56,6 +56,8 @@ Commands:
|
|
|
56
56
|
setup-telegram Reconfigure Telegram webhook
|
|
57
57
|
reset-auth Regenerate AUTH_SECRET (invalidates all sessions)
|
|
58
58
|
reset [file] Restore a template file (or list available templates)
|
|
59
|
+
reset-all Nuclear reset — restore entire project to fresh init state
|
|
60
|
+
audit Show project state vs. package templates (modified/missing/unknown)
|
|
59
61
|
diff [file] Show differences between project files and package templates
|
|
60
62
|
sync <path> Sync local package to a test install (build, pack, Docker)
|
|
61
63
|
sync --fast <path> Fast sync — copy source into running container, rebuild .next
|
|
@@ -211,10 +213,13 @@ async function init() {
|
|
|
211
213
|
const tmplPath = templatePath(relPath, templatesDir);
|
|
212
214
|
const templateExists = fs.existsSync(path.join(templatesDir, tmplPath));
|
|
213
215
|
if (!templateExists) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
const bd = getBackupDir();
|
|
217
|
+
const dest = path.join(bd, relPath);
|
|
218
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
219
|
+
fs.renameSync(fullPath, dest);
|
|
220
|
+
backedUp.push(relPath);
|
|
216
221
|
deleted.push(relPath);
|
|
217
|
-
console.log(`
|
|
222
|
+
console.log(` Removed ${relPath} (stale managed file)`);
|
|
218
223
|
}
|
|
219
224
|
}
|
|
220
225
|
}
|
|
@@ -370,6 +375,36 @@ THEPOPEBOT_VERSION=${version}
|
|
|
370
375
|
console.log('\nDone! Run: npm run setup\n');
|
|
371
376
|
}
|
|
372
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Create a timestamped backup directory and return { dir, ts }.
|
|
380
|
+
*/
|
|
381
|
+
function createBackupDir(cwd) {
|
|
382
|
+
const now = new Date();
|
|
383
|
+
const ts = now.getFullYear().toString()
|
|
384
|
+
+ String(now.getMonth() + 1).padStart(2, '0')
|
|
385
|
+
+ String(now.getDate()).padStart(2, '0')
|
|
386
|
+
+ '-'
|
|
387
|
+
+ String(now.getHours()).padStart(2, '0')
|
|
388
|
+
+ String(now.getMinutes()).padStart(2, '0')
|
|
389
|
+
+ String(now.getSeconds()).padStart(2, '0');
|
|
390
|
+
return { dir: path.join(cwd, '.backups', ts), ts };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Move a file or symlink to the backup directory.
|
|
395
|
+
*/
|
|
396
|
+
function backupAndRemove(fullPath, relPath, backupDir) {
|
|
397
|
+
const dest = path.join(backupDir, relPath);
|
|
398
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
399
|
+
if (fs.lstatSync(fullPath).isSymbolicLink()) {
|
|
400
|
+
const target = fs.readlinkSync(fullPath);
|
|
401
|
+
fs.symlinkSync(target, dest);
|
|
402
|
+
fs.unlinkSync(fullPath);
|
|
403
|
+
} else {
|
|
404
|
+
fs.renameSync(fullPath, dest);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
373
408
|
/**
|
|
374
409
|
* List all available template files, or restore a specific one.
|
|
375
410
|
*/
|
|
@@ -399,13 +434,38 @@ function reset(filePath) {
|
|
|
399
434
|
process.exit(1);
|
|
400
435
|
}
|
|
401
436
|
|
|
437
|
+
// Back up existing file before overwriting
|
|
438
|
+
if (fs.existsSync(dest)) {
|
|
439
|
+
const { dir, ts } = createBackupDir(cwd);
|
|
440
|
+
if (fs.statSync(src).isDirectory()) {
|
|
441
|
+
// Back up all files in the directory
|
|
442
|
+
function walkBackup(d) {
|
|
443
|
+
const items = fs.readdirSync(d, { withFileTypes: true });
|
|
444
|
+
for (const item of items) {
|
|
445
|
+
const full = path.join(d, item.name);
|
|
446
|
+
const rel = path.relative(cwd, full);
|
|
447
|
+
if (item.isDirectory()) {
|
|
448
|
+
walkBackup(full);
|
|
449
|
+
} else {
|
|
450
|
+
backupAndRemove(full, rel, dir);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
walkBackup(dest);
|
|
455
|
+
console.log(`\n Backed up to .backups/${ts}/`);
|
|
456
|
+
} else {
|
|
457
|
+
backupAndRemove(dest, filePath, dir);
|
|
458
|
+
console.log(`\n Backed up to .backups/${ts}/${filePath}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
402
462
|
if (fs.statSync(src).isDirectory()) {
|
|
403
463
|
console.log(`\nRestoring ${filePath}/...\n`);
|
|
404
464
|
copyDirSyncForce(src, dest, tmplPath);
|
|
405
465
|
} else {
|
|
406
466
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
407
467
|
fs.copyFileSync(src, dest);
|
|
408
|
-
console.log(
|
|
468
|
+
console.log(`Restored ${filePath}\n`);
|
|
409
469
|
}
|
|
410
470
|
}
|
|
411
471
|
|
|
@@ -471,6 +531,98 @@ function diff(filePath) {
|
|
|
471
531
|
}
|
|
472
532
|
}
|
|
473
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Audit project state against package templates.
|
|
536
|
+
* Groups all non-protected files into: matching, modified, missing, unknown.
|
|
537
|
+
*/
|
|
538
|
+
function audit() {
|
|
539
|
+
const packageDir = path.join(__dirname, '..');
|
|
540
|
+
const templatesDir = path.join(packageDir, 'templates');
|
|
541
|
+
const cwd = process.cwd();
|
|
542
|
+
|
|
543
|
+
const templateFiles = getTemplateFiles(templatesDir);
|
|
544
|
+
const matching = [];
|
|
545
|
+
const modified = [];
|
|
546
|
+
const missing = [];
|
|
547
|
+
|
|
548
|
+
// Check every template file against the project
|
|
549
|
+
for (const relPath of templateFiles) {
|
|
550
|
+
const src = path.join(templatesDir, relPath);
|
|
551
|
+
const outPath = destPath(relPath);
|
|
552
|
+
const dest = path.join(cwd, outPath);
|
|
553
|
+
|
|
554
|
+
if (!fs.existsSync(dest)) {
|
|
555
|
+
missing.push(outPath);
|
|
556
|
+
} else {
|
|
557
|
+
const srcContent = fs.readFileSync(src);
|
|
558
|
+
const destContent = fs.readFileSync(dest);
|
|
559
|
+
if (srcContent.equals(destContent)) {
|
|
560
|
+
matching.push(outPath);
|
|
561
|
+
} else {
|
|
562
|
+
modified.push(outPath);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Build a set of known template dest paths for lookup
|
|
568
|
+
const templateDestPaths = new Set(templateFiles.map(f => destPath(f)));
|
|
569
|
+
|
|
570
|
+
// Walk the project for unknown files (not in templates, not protected)
|
|
571
|
+
const unknown = [];
|
|
572
|
+
function walkProject(dir) {
|
|
573
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
574
|
+
for (const item of items) {
|
|
575
|
+
const fullPath = path.join(dir, item.name);
|
|
576
|
+
const relPath = path.relative(cwd, fullPath);
|
|
577
|
+
if (isProtected(relPath)) continue;
|
|
578
|
+
if (item.isDirectory() && !item.isSymbolicLink()) {
|
|
579
|
+
walkProject(fullPath);
|
|
580
|
+
} else if (!templateDestPaths.has(relPath)) {
|
|
581
|
+
unknown.push(relPath);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
walkProject(cwd);
|
|
586
|
+
|
|
587
|
+
// Report
|
|
588
|
+
console.log('\n Project audit\n');
|
|
589
|
+
|
|
590
|
+
if (modified.length > 0) {
|
|
591
|
+
console.log(` Modified (${modified.length}) — template exists, your version differs:`);
|
|
592
|
+
for (const f of modified) {
|
|
593
|
+
console.log(` ${f}`);
|
|
594
|
+
}
|
|
595
|
+
console.log('');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (missing.length > 0) {
|
|
599
|
+
console.log(` Missing (${missing.length}) — template exists, not in your project:`);
|
|
600
|
+
for (const f of missing) {
|
|
601
|
+
console.log(` ${f}`);
|
|
602
|
+
}
|
|
603
|
+
console.log('');
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (unknown.length > 0) {
|
|
607
|
+
console.log(` Unknown (${unknown.length}) — in your project, no template (reset-all would remove):`);
|
|
608
|
+
for (const f of unknown) {
|
|
609
|
+
console.log(` ${f}`);
|
|
610
|
+
}
|
|
611
|
+
console.log('');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
console.log(` ${matching.length} file(s) match package templates.`);
|
|
615
|
+
|
|
616
|
+
if (modified.length > 0 || missing.length > 0) {
|
|
617
|
+
console.log('\n To reset a file: thepopebot reset <file>');
|
|
618
|
+
console.log(' To view a diff: thepopebot diff <file>');
|
|
619
|
+
}
|
|
620
|
+
if (unknown.length > 0 || modified.length > 0 || missing.length > 0) {
|
|
621
|
+
console.log(' To reset everything: thepopebot reset-all');
|
|
622
|
+
}
|
|
623
|
+
console.log('');
|
|
624
|
+
}
|
|
625
|
+
|
|
474
626
|
function copyDirSyncForce(src, dest, templateRelBase = '') {
|
|
475
627
|
fs.mkdirSync(dest, { recursive: true });
|
|
476
628
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
@@ -491,6 +643,134 @@ function copyDirSyncForce(src, dest, templateRelBase = '') {
|
|
|
491
643
|
}
|
|
492
644
|
}
|
|
493
645
|
|
|
646
|
+
// Paths that reset-all must never touch (relative to project root).
|
|
647
|
+
// Entries ending with '/' are directory prefixes.
|
|
648
|
+
const PROTECTED_PATHS = [
|
|
649
|
+
'.env',
|
|
650
|
+
'.env.local',
|
|
651
|
+
'data/',
|
|
652
|
+
'logs/',
|
|
653
|
+
'.git/',
|
|
654
|
+
'.backups/',
|
|
655
|
+
'package-lock.json',
|
|
656
|
+
'package.json',
|
|
657
|
+
'docker-compose.custom.yml',
|
|
658
|
+
'.claude/',
|
|
659
|
+
'.pi/',
|
|
660
|
+
'skills/',
|
|
661
|
+
'node_modules/',
|
|
662
|
+
];
|
|
663
|
+
|
|
664
|
+
function isProtected(relPath) {
|
|
665
|
+
return PROTECTED_PATHS.some(p =>
|
|
666
|
+
p.endsWith('/') ? relPath === p.slice(0, -1) || relPath.startsWith(p) : relPath === p
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function resetAll() {
|
|
671
|
+
const cwd = process.cwd();
|
|
672
|
+
|
|
673
|
+
// Verify this is a thepopebot project
|
|
674
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
675
|
+
if (!fs.existsSync(pkgPath)) {
|
|
676
|
+
console.error('\n Not a thepopebot project (no package.json found).\n');
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
680
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
681
|
+
if (!deps.thepopebot) {
|
|
682
|
+
console.error('\n Not a thepopebot project (thepopebot not in dependencies).\n');
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Dry run — collect all files that would be moved
|
|
687
|
+
const filesToMove = [];
|
|
688
|
+
function collectFiles(dir) {
|
|
689
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
690
|
+
for (const item of items) {
|
|
691
|
+
const fullPath = path.join(dir, item.name);
|
|
692
|
+
const relPath = path.relative(cwd, fullPath);
|
|
693
|
+
if (isProtected(relPath)) continue;
|
|
694
|
+
if (item.isDirectory() && !item.isSymbolicLink()) {
|
|
695
|
+
collectFiles(fullPath);
|
|
696
|
+
} else {
|
|
697
|
+
filesToMove.push(relPath);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
collectFiles(cwd);
|
|
702
|
+
|
|
703
|
+
if (filesToMove.length === 0) {
|
|
704
|
+
console.log('\n Nothing to reset — no non-protected files found.\n');
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const { confirm, isCancel } = await import('@clack/prompts');
|
|
709
|
+
|
|
710
|
+
console.log('\n This will reset your entire project to a fresh thepopebot init state.');
|
|
711
|
+
console.log(` ${filesToMove.length} file(s) will be moved to .backups/:\n`);
|
|
712
|
+
for (const f of filesToMove) {
|
|
713
|
+
console.log(` ${f}`);
|
|
714
|
+
}
|
|
715
|
+
console.log('\n Protected (will NOT be touched):');
|
|
716
|
+
for (const p of PROTECTED_PATHS) {
|
|
717
|
+
console.log(` ${p}`);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const ok = await confirm({ message: '\nAre you sure? This is the nuclear option.' });
|
|
721
|
+
if (isCancel(ok) || !ok) {
|
|
722
|
+
console.log('\n Cancelled.\n');
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Move all files to backup
|
|
727
|
+
const { dir: backupDir, ts } = createBackupDir(cwd);
|
|
728
|
+
|
|
729
|
+
for (const relPath of filesToMove) {
|
|
730
|
+
const fullPath = path.join(cwd, relPath);
|
|
731
|
+
backupAndRemove(fullPath, relPath, backupDir);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Remove empty directories left behind
|
|
735
|
+
function removeEmptyDirs(dir) {
|
|
736
|
+
if (!fs.existsSync(dir)) return;
|
|
737
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
738
|
+
for (const item of items) {
|
|
739
|
+
if (item.isDirectory()) {
|
|
740
|
+
removeEmptyDirs(path.join(dir, item.name));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const relPath = path.relative(cwd, dir);
|
|
744
|
+
if (relPath && !isProtected(relPath) && fs.readdirSync(dir).length === 0) {
|
|
745
|
+
fs.rmdirSync(dir);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
removeEmptyDirs(cwd);
|
|
749
|
+
|
|
750
|
+
console.log(`\n Moved ${filesToMove.length} file(s) to .backups/${ts}/`);
|
|
751
|
+
|
|
752
|
+
// Run init to rebuild from templates
|
|
753
|
+
console.log('\n Running init to rebuild project...\n');
|
|
754
|
+
try {
|
|
755
|
+
execSync('npx thepopebot init --no-install', { stdio: 'inherit', cwd });
|
|
756
|
+
} catch {
|
|
757
|
+
console.error('\n Init failed. Your backup is at .backups/' + ts + '/\n');
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Run npm install separately
|
|
762
|
+
console.log('\nInstalling dependencies...\n');
|
|
763
|
+
try {
|
|
764
|
+
execSync('npm install', { stdio: 'inherit', cwd });
|
|
765
|
+
} catch {
|
|
766
|
+
console.error('\n npm install failed. Your backup is at .backups/' + ts + '/\n');
|
|
767
|
+
process.exit(1);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
console.log('\n Reset complete. Project restored to fresh init state.');
|
|
771
|
+
console.log(` Backup: .backups/${ts}/\n`);
|
|
772
|
+
}
|
|
773
|
+
|
|
494
774
|
function setup() {
|
|
495
775
|
const setupScript = path.join(__dirname, '..', 'setup', 'setup.mjs');
|
|
496
776
|
try {
|
|
@@ -801,6 +1081,12 @@ switch (command) {
|
|
|
801
1081
|
case 'reset':
|
|
802
1082
|
reset(args[0]);
|
|
803
1083
|
break;
|
|
1084
|
+
case 'reset-all':
|
|
1085
|
+
await resetAll();
|
|
1086
|
+
break;
|
|
1087
|
+
case 'audit':
|
|
1088
|
+
audit();
|
|
1089
|
+
break;
|
|
804
1090
|
case 'diff':
|
|
805
1091
|
diff(args[0]);
|
|
806
1092
|
break;
|
|
@@ -263,10 +263,10 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
|
|
|
263
263
|
}
|
|
264
264
|
},
|
|
265
265
|
children: [
|
|
266
|
-
|
|
267
|
-
/* @__PURE__ */ jsx(CodeIcon, { size: 16 }),
|
|
266
|
+
/* @__PURE__ */ jsxs("span", { className: "relative", children: [
|
|
267
|
+
chat.chatMode === "code" ? /* @__PURE__ */ jsx(CodeIcon, { size: 16 }) : /* @__PURE__ */ jsx(AgentIcon, { size: 16 }),
|
|
268
268
|
chat.hasChanges ? /* @__PURE__ */ jsx("span", { className: "absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-destructive" }) : null
|
|
269
|
-
] })
|
|
269
|
+
] }),
|
|
270
270
|
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
271
271
|
editing ? /* @__PURE__ */ jsx(
|
|
272
272
|
"input",
|
|
@@ -312,12 +312,10 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
|
|
|
312
312
|
}
|
|
313
313
|
}}
|
|
314
314
|
>
|
|
315
|
-
|
|
316
|
-
<
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
</span>
|
|
320
|
-
) : <AgentIcon size={16} />}
|
|
315
|
+
<span className="relative">
|
|
316
|
+
{chat.chatMode === 'code' ? <CodeIcon size={16} /> : <AgentIcon size={16} />}
|
|
317
|
+
{chat.hasChanges ? <span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-destructive" /> : null}
|
|
318
|
+
</span>
|
|
321
319
|
<div className="flex-1 min-w-0">
|
|
322
320
|
{editing ? (
|
|
323
321
|
<input
|
|
@@ -40,10 +40,10 @@ function SidebarHistoryItem({ chat, isActive, onDelete, onStar, onRename }) {
|
|
|
40
40
|
setOpenMobile(false);
|
|
41
41
|
},
|
|
42
42
|
children: [
|
|
43
|
-
|
|
44
|
-
/* @__PURE__ */ jsx(CodeIcon, { size: 14 }),
|
|
43
|
+
/* @__PURE__ */ jsxs("span", { className: "relative", children: [
|
|
44
|
+
chat.chatMode === "code" ? /* @__PURE__ */ jsx(CodeIcon, { size: 14 }) : /* @__PURE__ */ jsx(AgentIcon, { size: 14 }),
|
|
45
45
|
chat.hasChanges ? /* @__PURE__ */ jsx("span", { className: "absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-destructive" }) : null
|
|
46
|
-
] })
|
|
46
|
+
] }),
|
|
47
47
|
/* @__PURE__ */ jsx("span", { className: "truncate flex-1", children: chat.title })
|
|
48
48
|
]
|
|
49
49
|
}
|
|
@@ -40,12 +40,10 @@ export function SidebarHistoryItem({ chat, isActive, onDelete, onStar, onRename
|
|
|
40
40
|
setOpenMobile(false);
|
|
41
41
|
}}
|
|
42
42
|
>
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</span>
|
|
48
|
-
) : <AgentIcon size={14} />}
|
|
43
|
+
<span className="relative">
|
|
44
|
+
{chat.chatMode === 'code' ? <CodeIcon size={14} /> : <AgentIcon size={14} />}
|
|
45
|
+
{chat.hasChanges ? <span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-destructive" /> : null}
|
|
46
|
+
</span>
|
|
49
47
|
<span className="truncate flex-1">
|
|
50
48
|
{chat.title}
|
|
51
49
|
</span>
|
package/lib/tools/docker.js
CHANGED
|
@@ -233,9 +233,6 @@ async function runInteractiveContainer({ containerName, repo, branch, codingAgen
|
|
|
233
233
|
env.push(`${key}=${value}`);
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
|
-
if (jobSecrets.length > 0) {
|
|
237
|
-
env.push(`AGENT_JOB_SECRETS=${JSON.stringify(Object.fromEntries(jobSecrets.map(s => [s.key, s.value])))}`);
|
|
238
|
-
}
|
|
239
236
|
// Create per-container API key for agent-secrets access
|
|
240
237
|
const { createAgentJobApiKey } = await import('../db/api-keys.js');
|
|
241
238
|
const { key: agentJobToken } = createAgentJobApiKey(containerName);
|
|
@@ -446,9 +443,6 @@ async function runHeadlessContainer({ containerName, repo, branch, featureBranch
|
|
|
446
443
|
env.push(`${key}=${value}`);
|
|
447
444
|
}
|
|
448
445
|
}
|
|
449
|
-
if (jobSecrets.length > 0) {
|
|
450
|
-
env.push(`AGENT_JOB_SECRETS=${JSON.stringify(Object.fromEntries(jobSecrets.map(s => [s.key, s.value])))}`);
|
|
451
|
-
}
|
|
452
446
|
// Create per-container API key for agent-secrets access
|
|
453
447
|
const { createAgentJobApiKey } = await import('../db/api-keys.js');
|
|
454
448
|
const { key: agentJobToken } = createAgentJobApiKey(containerName);
|
|
@@ -916,9 +910,6 @@ async function runAgentJobContainer({ agentJobId, repo, branch, title, descripti
|
|
|
916
910
|
env.push(`${key}=${value}`);
|
|
917
911
|
}
|
|
918
912
|
}
|
|
919
|
-
if (jobSecrets.length > 0) {
|
|
920
|
-
env.push(`AGENT_JOB_SECRETS=${JSON.stringify(Object.fromEntries(jobSecrets.map(s => [s.key, s.value])))}`);
|
|
921
|
-
}
|
|
922
913
|
|
|
923
914
|
console.log(`[agent-job] id=${shortId} agent=${agent} image=${image} backendApi=${backendApi}`);
|
|
924
915
|
|
package/package.json
CHANGED
|
@@ -4,7 +4,12 @@ You are an autonomous AI agent running inside a Docker container on thepopebot.
|
|
|
4
4
|
|
|
5
5
|
## Runtime Environment
|
|
6
6
|
|
|
7
|
-
Your workspace is `/home/coding-agent/workspace` — a live git repository.
|
|
7
|
+
Your workspace is `/home/coding-agent/workspace` — a live git repository.
|
|
8
|
+
|
|
9
|
+
## Temporary Files
|
|
10
|
+
Use `/home/coding-agent/workspace/.tmp/` for working files — downloads, screenshots, intermediate data, scripts, generated files. `/home/coding-agent/workspace/.tmp/` is gitignored and nothing there gets committed. If a tool downloads a file, save it to `/home/coding-agent/workspace/.tmp/` and reference it directly.
|
|
11
|
+
|
|
12
|
+
**DO NOT USE** `/tmp` because that will continue waste disk space writing extra layers to the container.
|
|
8
13
|
|
|
9
14
|
Everything in the workspace is automatically committed and pushed when your job finishes. You do not control this. Be intentional about what you put here — **any file you create, move, or download into the workspace WILL be committed.**
|
|
10
15
|
|
|
@@ -10,7 +10,30 @@ Skills are lightweight plugins that extend agent abilities. Each skill lives in
|
|
|
10
10
|
|
|
11
11
|
Both Pi and Claude Code discover skills from the same `skills/active/` directory (via `.pi/skills` and `.claude/skills` symlink bridges).
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Conventions
|
|
14
|
+
|
|
15
|
+
### Language Preference
|
|
16
|
+
|
|
17
|
+
**Bash first.** Skills are glue code — API calls, data piping, file manipulation. Bash + curl + python3 (for JSON) handles nearly everything. No module systems, no dependency management, no surprises.
|
|
18
|
+
|
|
19
|
+
Use Node.js **only** when a required library has no alternative (e.g., `youtube-transcript-plus`). Never for new skills where bash + curl would work.
|
|
20
|
+
|
|
21
|
+
### Bash Script Standards
|
|
22
|
+
|
|
23
|
+
- Include `#!/bin/bash` and `set -euo pipefail` at the top
|
|
24
|
+
- `chmod +x` after creating
|
|
25
|
+
|
|
26
|
+
### Node.js Module Rules
|
|
27
|
+
|
|
28
|
+
The root `package.json` has `"type": "module"`, which forces **all** `.js` files in the project tree to be treated as ESM. This silently breaks any script using `require()`.
|
|
29
|
+
|
|
30
|
+
- **`.cjs`** — for CommonJS scripts (uses `require()`)
|
|
31
|
+
- **`.mjs`** — for ESM scripts (uses `import`)
|
|
32
|
+
- **Never use plain `.js`** for skill scripts. The behavior depends on the nearest `package.json` and will break unpredictably.
|
|
33
|
+
|
|
34
|
+
If you encounter a broken `.js` script in a skill, rename it to `.cjs` or `.mjs` as appropriate and update SKILL.md references.
|
|
35
|
+
|
|
36
|
+
### SKILL.md Format
|
|
14
37
|
|
|
15
38
|
Every skill must have a `SKILL.md` with YAML frontmatter:
|
|
16
39
|
|
|
@@ -32,12 +55,30 @@ skills/skill-name/script.sh <args>
|
|
|
32
55
|
- The `description` field appears in the system prompt — keep it concise and action-oriented.
|
|
33
56
|
- Use project-root-relative paths in documentation (e.g., `skills/skill-name/script.sh`).
|
|
34
57
|
|
|
35
|
-
|
|
58
|
+
### Skill Structure
|
|
36
59
|
|
|
37
60
|
- **`SKILL.md`** (required) — YAML frontmatter + markdown documentation
|
|
38
|
-
- **Scripts**
|
|
61
|
+
- **Scripts** — bash (`.sh`) by default, `.cjs`/`.mjs` only when necessary
|
|
39
62
|
- **`package.json`** (optional) — only if Node.js dependencies are truly needed
|
|
40
63
|
|
|
64
|
+
### Credential Setup
|
|
65
|
+
|
|
66
|
+
If a skill needs an API key, add it via the admin UI (Settings > Agent Jobs > Secrets). The secret will be injected as an env var into Docker containers. The agent can discover available secrets via the `get-secret` skill.
|
|
67
|
+
|
|
68
|
+
### Activation & Deactivation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Activate
|
|
72
|
+
ln -s ../skill-name skills/active/skill-name
|
|
73
|
+
|
|
74
|
+
# Deactivate
|
|
75
|
+
rm skills/active/skill-name
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The `skills/active/` directory is shared by both agent backends via symlink bridges:
|
|
79
|
+
- `.claude/skills → skills/active`
|
|
80
|
+
- `.pi/skills → skills/active`
|
|
81
|
+
|
|
41
82
|
## Creating a Skill
|
|
42
83
|
|
|
43
84
|
### Simple bash skill (most common)
|
|
@@ -67,6 +108,8 @@ skills/my-skill/run.sh <args>
|
|
|
67
108
|
**skills/my-skill/run.sh:**
|
|
68
109
|
```bash
|
|
69
110
|
#!/bin/bash
|
|
111
|
+
set -euo pipefail
|
|
112
|
+
|
|
70
113
|
if [ -z "$1" ]; then echo "Usage: run.sh <args>"; exit 1; fi
|
|
71
114
|
if [ -z "$MY_API_KEY" ]; then echo "Error: MY_API_KEY not set"; exit 1; fi
|
|
72
115
|
# ... skill logic
|
|
@@ -80,25 +123,7 @@ ln -s ../my-skill skills/active/my-skill
|
|
|
80
123
|
|
|
81
124
|
### Node.js skill
|
|
82
125
|
|
|
83
|
-
Use
|
|
84
|
-
|
|
85
|
-
## Activation & Deactivation
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
# Activate
|
|
89
|
-
ln -s ../skill-name skills/active/skill-name
|
|
90
|
-
|
|
91
|
-
# Deactivate
|
|
92
|
-
rm skills/active/skill-name
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
The `skills/active/` directory is shared by both agent backends via symlink bridges:
|
|
96
|
-
- `.claude/skills → skills/active`
|
|
97
|
-
- `.pi/skills → skills/active`
|
|
98
|
-
|
|
99
|
-
## Credential Setup
|
|
100
|
-
|
|
101
|
-
If a skill needs an API key, add it via the admin UI (Settings > Agent Jobs > Secrets). The secret will be injected as an env var into Docker containers. The agent can discover available secrets via the `get-secret` skill.
|
|
126
|
+
Use only when a required library has no bash/curl alternative. Add a `package.json` with dependencies — they're installed automatically in Docker. Use `.cjs` for CommonJS or `.mjs` for ESM — never plain `.js`.
|
|
102
127
|
|
|
103
128
|
## Testing
|
|
104
129
|
|
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-job-secrets
|
|
3
|
-
description: List
|
|
3
|
+
description: List and retrieve agent secrets. Plain secrets are also available as env vars. OAuth credentials are auto-refreshed on every get call.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Usage
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
# List available
|
|
9
|
+
# List available secret keys (fetches current list from server)
|
|
10
10
|
node skills/agent-job-secrets/agent-job-secrets.js
|
|
11
11
|
|
|
12
12
|
# Get a secret value (OAuth credentials are auto-refreshed)
|
|
13
13
|
node skills/agent-job-secrets/agent-job-secrets.js get MY_CREDENTIALS
|
|
14
|
-
|
|
15
|
-
# Set/update a secret (plain string or piped value)
|
|
16
|
-
node skills/agent-job-secrets/agent-job-secrets.js set MY_KEY "value"
|
|
17
|
-
echo "$UPDATED_CREDENTIALS" | node skills/agent-job-secrets/agent-job-secrets.js set MY_KEY
|
|
18
14
|
```
|
|
19
15
|
|
|
20
16
|
## Notes
|
|
21
17
|
|
|
22
18
|
- `AGENT_JOB_TOKEN` and `APP_URL` are injected automatically — no setup required
|
|
23
|
-
- OAuth
|
|
24
|
-
-
|
|
25
|
-
-
|
|
19
|
+
- Plain (non-OAuth) secrets are also available directly as env vars (e.g. `echo $MY_KEY`)
|
|
20
|
+
- OAuth credentials must be fetched via `get` — they are not available as env vars
|
|
21
|
+
- `get` on an OAuth credential refreshes it server-side and returns a fresh access token
|
|
22
|
+
- If a fetched credential stops working (expired token, 401 error), call `get` again to obtain a fresh one
|
|
23
|
+
- `list` always fetches from the server, so it reflects secrets added after the container started
|
|
@@ -1,31 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
3
2
|
|
|
4
|
-
const [cmd, key
|
|
3
|
+
const [cmd, key] = process.argv.slice(2);
|
|
4
|
+
|
|
5
|
+
const apiKey = process.env.AGENT_JOB_TOKEN;
|
|
6
|
+
const appUrl = process.env.APP_URL;
|
|
5
7
|
|
|
6
8
|
// Default to list
|
|
7
9
|
if (!cmd || cmd === 'list') {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
console.log('No agent secrets configured.');
|
|
10
|
+
if (!apiKey || !appUrl) {
|
|
11
|
+
console.log('No agent secrets available (missing AGENT_JOB_TOKEN or APP_URL).');
|
|
11
12
|
process.exit(0);
|
|
12
13
|
}
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const url = `${appUrl}/api/agent-job-list-secrets`;
|
|
15
|
+
const res = await fetch(url, {
|
|
16
|
+
headers: { 'x-api-key': apiKey },
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const body = await res.text();
|
|
20
|
+
console.error(`GET ${url} → ${res.status} ${body}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const json = await res.json();
|
|
24
|
+
const secrets = json.secrets;
|
|
25
|
+
if (!secrets || secrets.length === 0) {
|
|
16
26
|
console.log('No agent secrets configured.');
|
|
17
27
|
} else {
|
|
18
28
|
console.log('Available secrets:');
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
29
|
+
secrets.forEach(s => {
|
|
30
|
+
const hint = s.secretType === 'oauth2' ? ' (OAuth — use get to fetch access token)'
|
|
31
|
+
: s.secretType === 'oauth_token' ? ' (OAuth token — use get to fetch)'
|
|
32
|
+
: '';
|
|
33
|
+
console.log(` - ${s.key}${hint}`);
|
|
22
34
|
});
|
|
35
|
+
console.log('\nUse: agent-job-secrets get KEY_NAME');
|
|
36
|
+
console.log('If a fetched value stops working, call get again for a fresh one.');
|
|
23
37
|
}
|
|
24
38
|
process.exit(0);
|
|
25
39
|
}
|
|
26
40
|
|
|
27
|
-
const apiKey = process.env.AGENT_JOB_TOKEN;
|
|
28
|
-
const appUrl = process.env.APP_URL;
|
|
29
41
|
if (!apiKey) { console.error('AGENT_JOB_TOKEN not available'); process.exit(1); }
|
|
30
42
|
if (!appUrl) { console.error('APP_URL not available'); process.exit(1); }
|
|
31
43
|
|
|
@@ -45,31 +57,6 @@ if (cmd === 'get') {
|
|
|
45
57
|
process.exit(0);
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
if (cmd === 'set') {
|
|
49
|
-
if (!key) {
|
|
50
|
-
console.error('Usage: agent-job-secrets set KEY_NAME [value]');
|
|
51
|
-
console.error(' echo "value" | agent-job-secrets set KEY_NAME');
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
54
|
-
let value = inlineValue;
|
|
55
|
-
if (value === undefined) {
|
|
56
|
-
value = readFileSync('/dev/stdin', 'utf8').trim();
|
|
57
|
-
}
|
|
58
|
-
const url = `${appUrl}/api/set-agent-job-secret`;
|
|
59
|
-
const res = await fetch(url, {
|
|
60
|
-
method: 'POST',
|
|
61
|
-
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
62
|
-
body: JSON.stringify({ key, value }),
|
|
63
|
-
});
|
|
64
|
-
if (!res.ok) {
|
|
65
|
-
const body = await res.text();
|
|
66
|
-
console.error(`POST ${url} → ${res.status} ${body}`);
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
const json = await res.json();
|
|
70
|
-
console.log(`Secret "${key}" updated.`);
|
|
71
|
-
process.exit(0);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
60
|
console.error(`Unknown command: ${cmd}`);
|
|
61
|
+
console.error('Available commands: list, get');
|
|
75
62
|
process.exit(1);
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Env Sanitizer Extension - Protects credentials from AI agent access
|
|
3
|
-
*
|
|
4
|
-
* Uses Pi's spawnHook to filter sensitive env vars from bash subprocess calls
|
|
5
|
-
* while keeping them available in the main process for:
|
|
6
|
-
* - Anthropic SDK (needs ANTHROPIC_API_KEY at init)
|
|
7
|
-
* - GitHub CLI (needs GH_TOKEN)
|
|
8
|
-
* - Other extensions that may need credentials
|
|
9
|
-
*
|
|
10
|
-
* Dynamically filters all keys defined in the SECRETS JSON env var.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
14
|
-
import { createBashTool } from "@mariozechner/pi-coding-agent";
|
|
15
|
-
|
|
16
|
-
// Parse SECRETS JSON to get list of keys to filter
|
|
17
|
-
function getSecretKeys(): string[] {
|
|
18
|
-
const keys: string[] = [];
|
|
19
|
-
if (process.env.SECRETS) {
|
|
20
|
-
try {
|
|
21
|
-
const secrets = JSON.parse(process.env.SECRETS);
|
|
22
|
-
keys.push(...Object.keys(secrets));
|
|
23
|
-
} catch {
|
|
24
|
-
// Invalid JSON, ignore
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
// Always filter SECRETS itself
|
|
28
|
-
keys.push("SECRETS");
|
|
29
|
-
return [...new Set(keys)]; // Dedupe
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export default function (pi: ExtensionAPI) {
|
|
33
|
-
const secretKeys = getSecretKeys();
|
|
34
|
-
|
|
35
|
-
// Override bash tool with filtered environment for subprocesses
|
|
36
|
-
const bashTool = createBashTool(process.cwd(), {
|
|
37
|
-
spawnHook: ({ command, cwd, env }) => {
|
|
38
|
-
// Filter all secret keys from subprocess environment
|
|
39
|
-
const filteredEnv = { ...env };
|
|
40
|
-
for (const key of secretKeys) {
|
|
41
|
-
delete filteredEnv[key];
|
|
42
|
-
}
|
|
43
|
-
return { command, cwd, env: filteredEnv };
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
pi.registerTool(bashTool);
|
|
48
|
-
}
|
|
File without changes
|
package/templates/logs/.gitkeep
DELETED
|
File without changes
|