thepopebot 1.2.75-beta.15 → 1.2.75-beta.17

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/bin/cli.js CHANGED
@@ -56,6 +56,7 @@ 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
59
60
  diff [file] Show differences between project files and package templates
60
61
  sync <path> Sync local package to a test install (build, pack, Docker)
61
62
  sync --fast <path> Fast sync — copy source into running container, rebuild .next
@@ -211,10 +212,13 @@ async function init() {
211
212
  const tmplPath = templatePath(relPath, templatesDir);
212
213
  const templateExists = fs.existsSync(path.join(templatesDir, tmplPath));
213
214
  if (!templateExists) {
214
- backupFile(fullPath, relPath);
215
- fs.unlinkSync(fullPath);
215
+ const bd = getBackupDir();
216
+ const dest = path.join(bd, relPath);
217
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
218
+ fs.renameSync(fullPath, dest);
219
+ backedUp.push(relPath);
216
220
  deleted.push(relPath);
217
- console.log(` Deleted ${relPath} (stale managed file)`);
221
+ console.log(` Removed ${relPath} (stale managed file)`);
218
222
  }
219
223
  }
220
224
  }
@@ -370,6 +374,36 @@ THEPOPEBOT_VERSION=${version}
370
374
  console.log('\nDone! Run: npm run setup\n');
371
375
  }
372
376
 
377
+ /**
378
+ * Create a timestamped backup directory and return { dir, ts }.
379
+ */
380
+ function createBackupDir(cwd) {
381
+ const now = new Date();
382
+ const ts = now.getFullYear().toString()
383
+ + String(now.getMonth() + 1).padStart(2, '0')
384
+ + String(now.getDate()).padStart(2, '0')
385
+ + '-'
386
+ + String(now.getHours()).padStart(2, '0')
387
+ + String(now.getMinutes()).padStart(2, '0')
388
+ + String(now.getSeconds()).padStart(2, '0');
389
+ return { dir: path.join(cwd, '.backups', ts), ts };
390
+ }
391
+
392
+ /**
393
+ * Move a file or symlink to the backup directory.
394
+ */
395
+ function backupAndRemove(fullPath, relPath, backupDir) {
396
+ const dest = path.join(backupDir, relPath);
397
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
398
+ if (fs.lstatSync(fullPath).isSymbolicLink()) {
399
+ const target = fs.readlinkSync(fullPath);
400
+ fs.symlinkSync(target, dest);
401
+ fs.unlinkSync(fullPath);
402
+ } else {
403
+ fs.renameSync(fullPath, dest);
404
+ }
405
+ }
406
+
373
407
  /**
374
408
  * List all available template files, or restore a specific one.
375
409
  */
@@ -399,13 +433,38 @@ function reset(filePath) {
399
433
  process.exit(1);
400
434
  }
401
435
 
436
+ // Back up existing file before overwriting
437
+ if (fs.existsSync(dest)) {
438
+ const { dir, ts } = createBackupDir(cwd);
439
+ if (fs.statSync(src).isDirectory()) {
440
+ // Back up all files in the directory
441
+ function walkBackup(d) {
442
+ const items = fs.readdirSync(d, { withFileTypes: true });
443
+ for (const item of items) {
444
+ const full = path.join(d, item.name);
445
+ const rel = path.relative(cwd, full);
446
+ if (item.isDirectory()) {
447
+ walkBackup(full);
448
+ } else {
449
+ backupAndRemove(full, rel, dir);
450
+ }
451
+ }
452
+ }
453
+ walkBackup(dest);
454
+ console.log(`\n Backed up to .backups/${ts}/`);
455
+ } else {
456
+ backupAndRemove(dest, filePath, dir);
457
+ console.log(`\n Backed up to .backups/${ts}/${filePath}`);
458
+ }
459
+ }
460
+
402
461
  if (fs.statSync(src).isDirectory()) {
403
462
  console.log(`\nRestoring ${filePath}/...\n`);
404
463
  copyDirSyncForce(src, dest, tmplPath);
405
464
  } else {
406
465
  fs.mkdirSync(path.dirname(dest), { recursive: true });
407
466
  fs.copyFileSync(src, dest);
408
- console.log(`\nRestored ${filePath}\n`);
467
+ console.log(`Restored ${filePath}\n`);
409
468
  }
410
469
  }
411
470
 
@@ -491,6 +550,134 @@ function copyDirSyncForce(src, dest, templateRelBase = '') {
491
550
  }
492
551
  }
493
552
 
553
+ // Paths that reset-all must never touch (relative to project root).
554
+ // Entries ending with '/' are directory prefixes.
555
+ const PROTECTED_PATHS = [
556
+ '.env',
557
+ '.env.local',
558
+ 'data/',
559
+ 'logs/',
560
+ '.git/',
561
+ '.backups/',
562
+ 'package-lock.json',
563
+ 'package.json',
564
+ 'docker-compose.custom.yml',
565
+ '.claude/',
566
+ '.pi/',
567
+ 'skills/',
568
+ 'node_modules/',
569
+ ];
570
+
571
+ function isProtected(relPath) {
572
+ return PROTECTED_PATHS.some(p =>
573
+ p.endsWith('/') ? relPath === p.slice(0, -1) || relPath.startsWith(p) : relPath === p
574
+ );
575
+ }
576
+
577
+ async function resetAll() {
578
+ const cwd = process.cwd();
579
+
580
+ // Verify this is a thepopebot project
581
+ const pkgPath = path.join(cwd, 'package.json');
582
+ if (!fs.existsSync(pkgPath)) {
583
+ console.error('\n Not a thepopebot project (no package.json found).\n');
584
+ process.exit(1);
585
+ }
586
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
587
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
588
+ if (!deps.thepopebot) {
589
+ console.error('\n Not a thepopebot project (thepopebot not in dependencies).\n');
590
+ process.exit(1);
591
+ }
592
+
593
+ // Dry run — collect all files that would be moved
594
+ const filesToMove = [];
595
+ function collectFiles(dir) {
596
+ const items = fs.readdirSync(dir, { withFileTypes: true });
597
+ for (const item of items) {
598
+ const fullPath = path.join(dir, item.name);
599
+ const relPath = path.relative(cwd, fullPath);
600
+ if (isProtected(relPath)) continue;
601
+ if (item.isDirectory() && !item.isSymbolicLink()) {
602
+ collectFiles(fullPath);
603
+ } else {
604
+ filesToMove.push(relPath);
605
+ }
606
+ }
607
+ }
608
+ collectFiles(cwd);
609
+
610
+ if (filesToMove.length === 0) {
611
+ console.log('\n Nothing to reset — no non-protected files found.\n');
612
+ return;
613
+ }
614
+
615
+ const { confirm, isCancel } = await import('@clack/prompts');
616
+
617
+ console.log('\n This will reset your entire project to a fresh thepopebot init state.');
618
+ console.log(` ${filesToMove.length} file(s) will be moved to .backups/:\n`);
619
+ for (const f of filesToMove) {
620
+ console.log(` ${f}`);
621
+ }
622
+ console.log('\n Protected (will NOT be touched):');
623
+ for (const p of PROTECTED_PATHS) {
624
+ console.log(` ${p}`);
625
+ }
626
+
627
+ const ok = await confirm({ message: '\nAre you sure? This is the nuclear option.' });
628
+ if (isCancel(ok) || !ok) {
629
+ console.log('\n Cancelled.\n');
630
+ return;
631
+ }
632
+
633
+ // Move all files to backup
634
+ const { dir: backupDir, ts } = createBackupDir(cwd);
635
+
636
+ for (const relPath of filesToMove) {
637
+ const fullPath = path.join(cwd, relPath);
638
+ backupAndRemove(fullPath, relPath, backupDir);
639
+ }
640
+
641
+ // Remove empty directories left behind
642
+ function removeEmptyDirs(dir) {
643
+ if (!fs.existsSync(dir)) return;
644
+ const items = fs.readdirSync(dir, { withFileTypes: true });
645
+ for (const item of items) {
646
+ if (item.isDirectory()) {
647
+ removeEmptyDirs(path.join(dir, item.name));
648
+ }
649
+ }
650
+ const relPath = path.relative(cwd, dir);
651
+ if (relPath && !isProtected(relPath) && fs.readdirSync(dir).length === 0) {
652
+ fs.rmdirSync(dir);
653
+ }
654
+ }
655
+ removeEmptyDirs(cwd);
656
+
657
+ console.log(`\n Moved ${filesToMove.length} file(s) to .backups/${ts}/`);
658
+
659
+ // Run init to rebuild from templates
660
+ console.log('\n Running init to rebuild project...\n');
661
+ try {
662
+ execSync('npx thepopebot init --no-install', { stdio: 'inherit', cwd });
663
+ } catch {
664
+ console.error('\n Init failed. Your backup is at .backups/' + ts + '/\n');
665
+ process.exit(1);
666
+ }
667
+
668
+ // Run npm install separately
669
+ console.log('\nInstalling dependencies...\n');
670
+ try {
671
+ execSync('npm install', { stdio: 'inherit', cwd });
672
+ } catch {
673
+ console.error('\n npm install failed. Your backup is at .backups/' + ts + '/\n');
674
+ process.exit(1);
675
+ }
676
+
677
+ console.log('\n Reset complete. Project restored to fresh init state.');
678
+ console.log(` Backup: .backups/${ts}/\n`);
679
+ }
680
+
494
681
  function setup() {
495
682
  const setupScript = path.join(__dirname, '..', 'setup', 'setup.mjs');
496
683
  try {
@@ -801,6 +988,9 @@ switch (command) {
801
988
  case 'reset':
802
989
  reset(args[0]);
803
990
  break;
991
+ case 'reset-all':
992
+ await resetAll();
993
+ break;
804
994
  case 'diff':
805
995
  diff(args[0]);
806
996
  break;
@@ -293,7 +293,6 @@ function WorkspaceCommandButton({ workspaceId, diffStats, onDiffStatsRefresh, on
293
293
  onClick: onShowDiff,
294
294
  className: "text-xs leading-4 px-2.5 h-[28px] flex items-center gap-1.5 font-medium border border-border rounded-md whitespace-nowrap hover:bg-accent transition-colors cursor-pointer",
295
295
  children: [
296
- diffStats?.currentBranch && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground truncate max-w-[120px]", title: diffStats.currentBranch, children: diffStats.currentBranch }),
297
296
  /* @__PURE__ */ jsxs("span", { className: "text-green-500", children: [
298
297
  "+",
299
298
  diffStats?.insertions ?? 0
@@ -341,9 +341,6 @@ function WorkspaceCommandButton({ workspaceId, diffStats, onDiffStatsRefresh, on
341
341
  onClick={onShowDiff}
342
342
  className="text-xs leading-4 px-2.5 h-[28px] flex items-center gap-1.5 font-medium border border-border rounded-md whitespace-nowrap hover:bg-accent transition-colors cursor-pointer"
343
343
  >
344
- {diffStats?.currentBranch && (
345
- <span className="text-muted-foreground truncate max-w-[120px]" title={diffStats.currentBranch}>{diffStats.currentBranch}</span>
346
- )}
347
344
  <span className="text-green-500">+{diffStats?.insertions ?? 0}</span>
348
345
  <span className="text-destructive">-{diffStats?.deletions ?? 0}</span>
349
346
  </button>
@@ -675,6 +675,15 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
675
675
  ] })
676
676
  ] })
677
677
  ] }),
678
+ diffStats?.currentBranch && /* @__PURE__ */ jsx(
679
+ "button",
680
+ {
681
+ className: "code-toolbar-btn",
682
+ style: { cursor: "default", opacity: 0.7, maxWidth: 140, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
683
+ title: diffStats.currentBranch,
684
+ children: diffStats.currentBranch
685
+ }
686
+ ),
678
687
  /* @__PURE__ */ jsx(
679
688
  ToolbarCommandButton,
680
689
  {
@@ -741,6 +741,15 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
741
741
  )}
742
742
  </div>
743
743
  )}
744
+ {diffStats?.currentBranch && (
745
+ <button
746
+ className="code-toolbar-btn"
747
+ style={{ cursor: 'default', opacity: 0.7, maxWidth: 140, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
748
+ title={diffStats.currentBranch}
749
+ >
750
+ {diffStats.currentBranch}
751
+ </button>
752
+ )}
744
753
  <ToolbarCommandButton
745
754
  codeWorkspaceId={codeWorkspaceId}
746
755
  diffStats={diffStats}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thepopebot",
3
- "version": "1.2.75-beta.15",
3
+ "version": "1.2.75-beta.17",
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/
@@ -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.
@@ -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