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 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
- | POST | `/api/set-agent-job-secret` | `x-api-key` | Set/update an agent job secret (for agents to persist rotated credentials) |
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 handleSetAgentSecret(request) {
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
- const body = await request.json();
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': return handleGetAgentSecret(request);
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
- backupFile(fullPath, relPath);
215
- fs.unlinkSync(fullPath);
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(` Deleted ${relPath} (stale managed file)`);
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(`\nRestored ${filePath}\n`);
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
- chat.chatMode === "code" ? /* @__PURE__ */ jsxs("span", { className: "relative", children: [
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
- ] }) : /* @__PURE__ */ jsx(AgentIcon, { size: 16 }),
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
- {chat.chatMode === 'code' ? (
316
- <span className="relative">
317
- <CodeIcon size={16} />
318
- {chat.hasChanges ? <span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-destructive" /> : null}
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
- chat.chatMode === "code" ? /* @__PURE__ */ jsxs("span", { className: "relative", children: [
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
- ] }) : /* @__PURE__ */ jsx(AgentIcon, { size: 14 }),
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
- {chat.chatMode === 'code' ? (
44
- <span className="relative">
45
- <CodeIcon size={14} />
46
- {chat.hasChanges ? <span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-destructive" /> : null}
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>
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thepopebot",
3
- "version": "1.2.75-beta.16",
3
+ "version": "1.2.75-beta.18",
4
4
  "type": "module",
5
5
  "description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
6
6
  "bin": {
@@ -21,7 +21,7 @@ skills/*/node_modules/
21
21
  skills/active/agent-job-secrets
22
22
 
23
23
  # Database
24
- data/
24
+ /data/
25
25
 
26
26
  # Node
27
27
  node_modules/
@@ -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. Use `/tmp` for working files — downloads, intermediate data, scripts, generated files. `/tmp` is outside the repo and nothing there gets committed. If a tool downloads a file to `/tmp`, leave it there and reference it directly.
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
 
@@ -0,0 +1,5 @@
1
+ # Data Directory
2
+
3
+ Runtime data including the SQLite database (`thepopebot.sqlite`) and cluster state. Created automatically on server start.
4
+
5
+ Not checked into git.
@@ -0,0 +1,5 @@
1
+ # Logs Directory
2
+
3
+ Agent job logs, organized by job ID. Each subdirectory contains the system prompt, task prompt, and agent output for a single job run.
4
+
5
+ Not checked into git.
@@ -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
- ## SKILL.md Format
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
- ## Skill Structure
58
+ ### Skill Structure
36
59
 
37
60
  - **`SKILL.md`** (required) — YAML frontmatter + markdown documentation
38
- - **Scripts** (optional) prefer bash (`.sh`) for simplicity
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 this pattern only when bash + curl isn't sufficient (e.g., HTML parsing, complex data processing). Add a `package.json` with dependencies — they're installed automatically in Docker.
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, get, or update agent secrets. Use get for OAuth credentials (auto-refreshed on every call). Use set to persist updated credentials back to the event handler.
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 secrets (null = must fetch, plain = already in env)
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 credentials show as `null` in the list and must be fetched via `get`
24
- - `get` on an OAuth credential refreshes it and persists the updated token immediately
25
- - Plain secrets are available directly as env vars (e.g. `echo $MY_KEY`)
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, inlineValue] = process.argv.slice(2);
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
- const secretsJson = process.env.AGENT_JOB_SECRETS;
9
- if (!secretsJson) {
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 secrets = JSON.parse(secretsJson);
14
- const keys = Object.keys(secrets);
15
- if (keys.length === 0) {
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
- keys.forEach(k => {
20
- const fetchRequired = secrets[k] === null;
21
- console.log(` - ${k}${fetchRequired ? ' (use agent-job-secrets skill to fetch)' : ''}`);
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
- }
@@ -1,5 +0,0 @@
1
- {
2
- "name": "env-sanitizer",
3
- "private": true,
4
- "type": "module"
5
- }
File without changes
File without changes