thepopebot 1.2.75-beta.2 → 1.2.75-beta.20

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.
Files changed (118) hide show
  1. package/README.md +1 -1
  2. package/api/CLAUDE.md +1 -1
  3. package/api/index.js +5 -12
  4. package/bin/CLAUDE.md +1 -1
  5. package/bin/cli.js +329 -14
  6. package/bin/docker-build.js +5 -0
  7. package/bin/managed-paths.js +0 -7
  8. package/bin/sync.js +84 -0
  9. package/config/CLAUDE.md +1 -29
  10. package/config/instrumentation.js +1 -1
  11. package/lib/CLAUDE.md +3 -3
  12. package/lib/ai/CLAUDE.md +24 -3
  13. package/lib/ai/agent.js +8 -5
  14. package/lib/ai/async-channel.js +51 -0
  15. package/lib/ai/headless-stream.js +3 -0
  16. package/lib/ai/index.js +149 -173
  17. package/lib/ai/line-mappers.js +72 -9
  18. package/lib/ai/tools.js +40 -28
  19. package/lib/chat/actions.js +34 -6
  20. package/lib/chat/api.js +17 -1
  21. package/lib/chat/components/chat-header.js +4 -0
  22. package/lib/chat/components/chat-header.jsx +4 -0
  23. package/lib/chat/components/chat-input.js +1 -0
  24. package/lib/chat/components/chat-input.jsx +1 -0
  25. package/lib/chat/components/chat.js +9 -1
  26. package/lib/chat/components/chat.jsx +15 -2
  27. package/lib/chat/components/chats-page.js +3 -3
  28. package/lib/chat/components/chats-page.jsx +4 -6
  29. package/lib/chat/components/crons-page.js +1 -1
  30. package/lib/chat/components/crons-page.jsx +1 -1
  31. package/lib/chat/components/message.js +12 -4
  32. package/lib/chat/components/message.jsx +17 -4
  33. package/lib/chat/components/settings-chat-page.js +2 -1
  34. package/lib/chat/components/settings-chat-page.jsx +4 -1
  35. package/lib/chat/components/settings-coding-agents-page.js +139 -1
  36. package/lib/chat/components/settings-coding-agents-page.jsx +160 -0
  37. package/lib/chat/components/settings-jobs-page.js +13 -2
  38. package/lib/chat/components/settings-jobs-page.jsx +15 -1
  39. package/lib/chat/components/settings-secrets-layout.js +1 -1
  40. package/lib/chat/components/settings-secrets-layout.jsx +1 -1
  41. package/lib/chat/components/sidebar-history-item.js +3 -3
  42. package/lib/chat/components/sidebar-history-item.jsx +4 -6
  43. package/lib/chat/components/triggers-page.js +1 -1
  44. package/lib/chat/components/triggers-page.jsx +1 -1
  45. package/lib/cluster/actions.js +4 -4
  46. package/lib/cluster/execute.js +3 -1
  47. package/lib/code/actions.js +34 -11
  48. package/lib/code/port-forwards.js +17 -3
  49. package/lib/code/terminal-view.js +16 -0
  50. package/lib/code/terminal-view.jsx +18 -0
  51. package/lib/config.js +4 -0
  52. package/lib/cron.js +3 -3
  53. package/lib/db/api-keys.js +22 -61
  54. package/lib/db/config.js +23 -0
  55. package/lib/db/index.js +3 -1
  56. package/lib/maintenance.js +34 -11
  57. package/lib/paths.js +1 -38
  58. package/lib/tools/create-agent-job.js +0 -4
  59. package/lib/tools/docker.js +23 -16
  60. package/lib/triggers.js +4 -3
  61. package/lib/utils/render-md.js +3 -1
  62. package/package.json +1 -1
  63. package/setup/setup-ssl.mjs +414 -0
  64. package/templates/.github/workflows/rebuild-event-handler.yml +3 -0
  65. package/templates/.github/workflows/upgrade-event-handler.yml +1 -1
  66. package/templates/.gitignore.template +7 -3
  67. package/templates/.tmp/CLAUDE.md.template +5 -0
  68. package/templates/CLAUDE.md +3 -2
  69. package/templates/CLAUDE.md.template +24 -357
  70. package/templates/agent-job/CLAUDE.md.template +57 -0
  71. package/templates/agent-job/CRONS.json +16 -0
  72. package/templates/{config/agent-job → agent-job}/SOUL.md +3 -3
  73. package/templates/agent-job/SYSTEM.md +60 -0
  74. package/templates/agents/CLAUDE.md.template +54 -0
  75. package/templates/data/CLAUDE.md.template +5 -0
  76. package/templates/docker-compose.custom.yml +41 -62
  77. package/templates/docker-compose.yml +14 -21
  78. package/templates/event-handler/CLAUDE.md.template +0 -0
  79. package/templates/logs/CLAUDE.md.template +5 -0
  80. package/templates/skills/CLAUDE.md.template +57 -32
  81. package/templates/skills/active/.gitkeep +0 -0
  82. package/templates/skills/library/agent-job-secrets/SKILL.md +23 -0
  83. package/templates/skills/library/agent-job-secrets/agent-job-secrets.js +62 -0
  84. package/templates/.pi/extensions/env-sanitizer/index.ts +0 -48
  85. package/templates/.pi/extensions/env-sanitizer/package.json +0 -5
  86. package/templates/README.md +0 -75
  87. package/templates/config/CLAUDE.md.template +0 -40
  88. package/templates/config/CRONS.json +0 -56
  89. package/templates/config/agent-job/AGENT_JOB.md +0 -30
  90. package/templates/cron/CLAUDE.md.template +0 -24
  91. package/templates/docker-compose.litellm.yml +0 -82
  92. package/templates/docs/CLAUDE.md.template +0 -12
  93. package/templates/docs/CLI.md +0 -59
  94. package/templates/docs/CLUSTERS.md +0 -151
  95. package/templates/docs/CONFIGURATION.md +0 -181
  96. package/templates/docs/CRONS_AND_TRIGGERS.md +0 -132
  97. package/templates/docs/GETTING_STARTED.md +0 -64
  98. package/templates/docs/SECURITY.md +0 -61
  99. package/templates/docs/SKILLS.md +0 -113
  100. package/templates/docs/UPGRADING.md +0 -92
  101. package/templates/skills/LICENSE +0 -21
  102. package/templates/skills/README.md +0 -117
  103. package/templates/skills/agent-job-secrets/SKILL.md +0 -25
  104. package/templates/skills/agent-job-secrets/agent-job-secrets.js +0 -66
  105. package/templates/traefik-dynamic.yml.example +0 -7
  106. package/templates/triggers/CLAUDE.md.template +0 -41
  107. /package/templates/{config → agent-job}/HEARTBEAT.md +0 -0
  108. /package/templates/{cron → data}/.gitkeep +0 -0
  109. /package/templates/{logs → data/clusters}/.gitkeep +0 -0
  110. /package/templates/{triggers → data/db}/.gitkeep +0 -0
  111. /package/templates/{config/agent-job → event-handler}/SUMMARY.md +0 -0
  112. /package/templates/{config → event-handler}/TRIGGERS.json +0 -0
  113. /package/templates/{config → event-handler}/agent-chat/SYSTEM.md +0 -0
  114. /package/templates/{config/cluster → event-handler/clusters}/ROLE.md +0 -0
  115. /package/templates/{config/cluster → event-handler/clusters}/SYSTEM.md +0 -0
  116. /package/templates/{config → event-handler}/code-chat/SYSTEM.md +0 -0
  117. /package/templates/{config → event-handler}/litellm/main.yaml +0 -0
  118. /package/templates/skills/{playwright-cli → library/playwright-cli}/SKILL.md +0 -0
package/README.md CHANGED
@@ -106,7 +106,7 @@ The wizard walks you through everything:
106
106
  - **Web Chat**: Visit your APP_URL to chat with your agent, create jobs, upload files
107
107
  - **Telegram** (optional): Run `npm run setup-telegram` to connect a Telegram bot
108
108
  - **Webhook**: Send a POST to `/api/create-agent-job` with your API key to create jobs programmatically
109
- - **Cron**: Edit `config/CRONS.json` to schedule recurring jobs
109
+ - **Cron**: Edit `agent-job/CRONS.json` to schedule recurring jobs
110
110
 
111
111
  ### Chat vs Agent LLM
112
112
 
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/CLAUDE.md CHANGED
@@ -21,7 +21,7 @@ Entry point: `cli.js` (invoked via `npx thepopebot <command>`).
21
21
 
22
22
  `managed-paths.js` defines files auto-synced by `init`. These are overwritten on every init/upgrade — users should not edit them.
23
23
 
24
- **Managed paths**: `.github/workflows/`, `docker-compose.yml`, `.dockerignore`, `.gitignore`, `CLAUDE.md`, `config/CLAUDE.md`, `skills/CLAUDE.md`, `cron/CLAUDE.md`, `triggers/CLAUDE.md`, `docs/CLAUDE.md`.
24
+ **Managed paths**: `.github/workflows/`, `docker-compose.yml`, `.dockerignore`, `.gitignore`, `agent-job/CLAUDE.md`, `event-handler/CLAUDE.md`, `skills/CLAUDE.md`.
25
25
 
26
26
  `isManaged(relPath)` — returns true if a path is managed (exact match or directory prefix).
27
27
 
package/bin/cli.js CHANGED
@@ -52,11 +52,15 @@ Commands:
52
52
  init Scaffold a new thepopebot project
53
53
  upgrade|update [@beta|version] Upgrade thepopebot (install, init, build, commit, push)
54
54
  setup Run interactive setup wizard
55
+ setup-ssl Configure SSL with Let's Encrypt wildcard cert
55
56
  setup-telegram Reconfigure Telegram webhook
56
57
  reset-auth Regenerate AUTH_SECRET (invalidates all sessions)
57
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)
58
61
  diff [file] Show differences between project files and package templates
59
62
  sync <path> Sync local package to a test install (build, pack, Docker)
63
+ sync --fast <path> Fast sync — copy source into running container, rebuild .next
60
64
  set-var <KEY> [VALUE] Set a GitHub repository variable
61
65
  user:password <email> Change a user's password
62
66
  `);
@@ -209,10 +213,13 @@ async function init() {
209
213
  const tmplPath = templatePath(relPath, templatesDir);
210
214
  const templateExists = fs.existsSync(path.join(templatesDir, tmplPath));
211
215
  if (!templateExists) {
212
- backupFile(fullPath, relPath);
213
- 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);
214
221
  deleted.push(relPath);
215
- console.log(` Deleted ${relPath} (stale managed file)`);
222
+ console.log(` Removed ${relPath} (stale managed file)`);
216
223
  }
217
224
  }
218
225
  }
@@ -246,6 +253,7 @@ async function init() {
246
253
  const pkg = {
247
254
  name: dirName,
248
255
  private: true,
256
+ type: 'module',
249
257
  scripts: {
250
258
  setup: 'thepopebot setup',
251
259
  'setup-telegram': 'thepopebot setup-telegram',
@@ -258,18 +266,26 @@ async function init() {
258
266
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
259
267
  console.log(' Created package.json');
260
268
  } else {
261
- console.log(' Skipped package.json (already exists)');
269
+ // Ensure "type": "module" is set for ESM support
270
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
271
+ if (!pkg.type) {
272
+ pkg.type = 'module';
273
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
274
+ console.log(' Added "type": "module" to package.json');
275
+ } else {
276
+ console.log(' Skipped package.json (already exists)');
277
+ }
262
278
  }
263
279
 
264
280
  // Create default skill activation symlinks
265
- const defaultSkills = ['agent-job-secrets'];
281
+ const defaultSkills = [];
266
282
  const activeDir = path.join(cwd, 'skills', 'active');
267
283
  fs.mkdirSync(activeDir, { recursive: true });
268
284
  for (const skill of defaultSkills) {
269
285
  const symlink = path.join(activeDir, skill);
270
286
  if (!fs.existsSync(symlink)) {
271
- createDirLink(`../${skill}`, symlink);
272
- console.log(` Created skills/active/${skill} → ../${skill}`);
287
+ createDirLink(`../library/${skill}`, symlink);
288
+ console.log(` Created skills/active/${skill} → ../library/${skill}`);
273
289
  }
274
290
  }
275
291
 
@@ -337,8 +353,7 @@ AUTH_TRUST_HOST=true
337
353
  DATABASE_PATH=data/db/thepopebot.sqlite
338
354
  THEPOPEBOT_VERSION=${version}
339
355
 
340
- # Uncomment to use a custom docker-compose file that won't be overwritten by upgrades.
341
- # Edit docker-compose.custom.yml with your changes, then uncomment:
356
+ # To enable SSL with Let's Encrypt, run: npx thepopebot setup-ssl
342
357
  # COMPOSE_FILE=docker-compose.custom.yml
343
358
  `;
344
359
  fs.writeFileSync(envPath, seedEnv);
@@ -360,6 +375,36 @@ THEPOPEBOT_VERSION=${version}
360
375
  console.log('\nDone! Run: npm run setup\n');
361
376
  }
362
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
+
363
408
  /**
364
409
  * List all available template files, or restore a specific one.
365
410
  */
@@ -375,7 +420,7 @@ function reset(filePath) {
375
420
  console.log(` ${destPath(file)}`);
376
421
  }
377
422
  console.log('\nUsage: thepopebot reset <file>');
378
- console.log('Example: thepopebot reset config/SOUL.md\n');
423
+ console.log('Example: thepopebot reset agent-job/SOUL.md\n');
379
424
  return;
380
425
  }
381
426
 
@@ -389,13 +434,38 @@ function reset(filePath) {
389
434
  process.exit(1);
390
435
  }
391
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
+
392
462
  if (fs.statSync(src).isDirectory()) {
393
463
  console.log(`\nRestoring ${filePath}/...\n`);
394
464
  copyDirSyncForce(src, dest, tmplPath);
395
465
  } else {
396
466
  fs.mkdirSync(path.dirname(dest), { recursive: true });
397
467
  fs.copyFileSync(src, dest);
398
- console.log(`\nRestored ${filePath}\n`);
468
+ console.log(`Restored ${filePath}\n`);
399
469
  }
400
470
  }
401
471
 
@@ -432,7 +502,7 @@ function diff(filePath) {
432
502
  console.log(' All files match package templates.');
433
503
  }
434
504
  console.log('\nUsage: thepopebot diff <file>');
435
- console.log('Example: thepopebot diff config/SOUL.md\n');
505
+ console.log('Example: thepopebot diff agent-job/SOUL.md\n');
436
506
  return;
437
507
  }
438
508
 
@@ -461,6 +531,98 @@ function diff(filePath) {
461
531
  }
462
532
  }
463
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
+
464
626
  function copyDirSyncForce(src, dest, templateRelBase = '') {
465
627
  fs.mkdirSync(dest, { recursive: true });
466
628
  const entries = fs.readdirSync(src, { withFileTypes: true });
@@ -481,6 +643,134 @@ function copyDirSyncForce(src, dest, templateRelBase = '') {
481
643
  }
482
644
  }
483
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
+
484
774
  function setup() {
485
775
  const setupScript = path.join(__dirname, '..', 'setup', 'setup.mjs');
486
776
  try {
@@ -490,6 +780,15 @@ function setup() {
490
780
  }
491
781
  }
492
782
 
783
+ function setupSsl() {
784
+ const setupScript = path.join(__dirname, '..', 'setup', 'setup-ssl.mjs');
785
+ try {
786
+ execFileSync(process.execPath, [setupScript], { stdio: 'inherit', cwd: process.cwd() });
787
+ } catch {
788
+ process.exit(1);
789
+ }
790
+ }
791
+
493
792
  function setupTelegram() {
494
793
  const setupScript = path.join(__dirname, '..', 'setup', 'setup-telegram.mjs');
495
794
  try {
@@ -770,6 +1069,9 @@ switch (command) {
770
1069
  case 'setup':
771
1070
  setup();
772
1071
  break;
1072
+ case 'setup-ssl':
1073
+ setupSsl();
1074
+ break;
773
1075
  case 'setup-telegram':
774
1076
  setupTelegram();
775
1077
  break;
@@ -779,6 +1081,12 @@ switch (command) {
779
1081
  case 'reset':
780
1082
  reset(args[0]);
781
1083
  break;
1084
+ case 'reset-all':
1085
+ await resetAll();
1086
+ break;
1087
+ case 'audit':
1088
+ audit();
1089
+ break;
782
1090
  case 'diff':
783
1091
  diff(args[0]);
784
1092
  break;
@@ -787,8 +1095,15 @@ switch (command) {
787
1095
  await upgrade();
788
1096
  break;
789
1097
  case 'sync': {
790
- const { sync } = await import('./sync.js');
791
- await sync(args[0]);
1098
+ const fast = args.includes('--fast');
1099
+ const syncArgs = args.filter(a => a !== '--fast');
1100
+ if (fast) {
1101
+ const { syncFast } = await import('./sync.js');
1102
+ await syncFast(syncArgs[0]);
1103
+ } else {
1104
+ const { sync } = await import('./sync.js');
1105
+ await sync(syncArgs[0]);
1106
+ }
792
1107
  break;
793
1108
  }
794
1109
  case 'set-var':
@@ -62,6 +62,11 @@ const CODING_AGENTS = [
62
62
  context: 'docker/coding-agent',
63
63
  dockerfile: 'docker/coding-agent/Dockerfile.opencode',
64
64
  },
65
+ {
66
+ name: 'coding-agent-kimi-cli',
67
+ context: 'docker/coding-agent',
68
+ dockerfile: 'docker/coding-agent/Dockerfile.kimi-cli',
69
+ },
65
70
  ];
66
71
 
67
72
  // Non-coding-agent images (independent, built in parallel)
@@ -4,16 +4,9 @@
4
4
  // Paths ending with '/' are directories (all contents are managed).
5
5
  export const MANAGED_PATHS = [
6
6
  '.github/workflows/',
7
-
8
7
  'docker-compose.yml',
9
8
  '.dockerignore',
10
9
  '.gitignore',
11
- 'CLAUDE.md',
12
- 'config/CLAUDE.md',
13
- 'skills/CLAUDE.md',
14
- 'cron/CLAUDE.md',
15
- 'triggers/CLAUDE.md',
16
- 'docs/',
17
10
  ];
18
11
 
19
12
  export function isManaged(relPath) {
package/bin/sync.js CHANGED
@@ -306,6 +306,90 @@ function buildDockerImage(projectPath) {
306
306
  }
307
307
  }
308
308
 
309
+ /**
310
+ * Fast sync — skip Docker image rebuild entirely.
311
+ *
312
+ * 1. Build package JSX (npm run build)
313
+ * 2. mirrorTemplates() — scaffold using init's managed-path logic
314
+ * 3. docker cp package source (lib/, api/, config/, package.json) into
315
+ * the running container's /app/node_modules/thepopebot/
316
+ * 4. docker cp web/app/ + web/postcss.config.mjs into container
317
+ * 5. docker exec next build inside the container (tailwindcss already there)
318
+ * 6. Clean up copied source from container
319
+ * 7. docker exec pm2 restart all
320
+ */
321
+ export async function syncFast(projectPath) {
322
+ if (!projectPath) {
323
+ console.error('\n Usage: thepopebot sync --fast <path-to-project>\n');
324
+ process.exit(1);
325
+ }
326
+
327
+ projectPath = path.resolve(projectPath);
328
+
329
+ if (!fs.existsSync(path.join(projectPath, 'package.json'))) {
330
+ console.error(`\n Not a project directory (no package.json): ${projectPath}\n`);
331
+ process.exit(1);
332
+ }
333
+
334
+ // 1. Build JSX
335
+ console.log('\n Building package...');
336
+ execSync('npm run build', { stdio: 'inherit', cwd: PACKAGE_DIR });
337
+
338
+ // 2. Mirror templates
339
+ console.log('\n Mirroring templates...');
340
+ mirrorTemplates(projectPath);
341
+
342
+ // 3. Get running container ID
343
+ const container = execSync('docker compose ps -q event-handler', {
344
+ encoding: 'utf8',
345
+ cwd: projectPath,
346
+ }).trim();
347
+
348
+ if (!container) {
349
+ console.error('\n event-handler container is not running. Use full sync instead.\n');
350
+ process.exit(1);
351
+ }
352
+
353
+ // 4. Copy package source into container's node_modules/thepopebot/
354
+ const PKG_DEST = '/app/node_modules/thepopebot';
355
+ const PACKAGE_DIRS = ['lib', 'api', 'config'];
356
+
357
+ console.log('\n Copying package source into container...');
358
+ for (const dir of PACKAGE_DIRS) {
359
+ execSync(`docker exec ${container} rm -rf ${PKG_DEST}/${dir}`, { stdio: 'inherit' });
360
+ execSync(`docker cp ${path.join(PACKAGE_DIR, dir)} ${container}:${PKG_DEST}/${dir}`, { stdio: 'inherit' });
361
+ }
362
+ // Also copy package.json for exports resolution
363
+ execSync(`docker cp ${path.join(PACKAGE_DIR, 'package.json')} ${container}:${PKG_DEST}/package.json`, { stdio: 'inherit' });
364
+
365
+ // 5. Copy web/app/ source into container for next build
366
+ const webDir = path.join(PACKAGE_DIR, 'web');
367
+ console.log('\n Copying web source into container...');
368
+ execSync(`docker cp ${path.join(webDir, 'app')} ${container}:/app/app`, { stdio: 'inherit' });
369
+ execSync(`docker cp ${path.join(webDir, 'postcss.config.mjs')} ${container}:/app/postcss.config.mjs`, { stdio: 'inherit' });
370
+ execSync(`docker cp ${path.join(webDir, 'next.config.mjs')} ${container}:/app/next.config.mjs`, { stdio: 'inherit' });
371
+
372
+ // 6. Run next build inside the container
373
+ // Hide data/logs dirs so webpack's FileSystemInfo doesn't crawl them (causes OOM/RangeError
374
+ // when workspaces contain thousands of files). Restored immediately after build.
375
+ console.log('\n Building Next.js inside container...');
376
+ execSync(`docker exec ${container} sh -c 'mv /app/data /app/.data-build-tmp 2>/dev/null; mv /app/logs /app/.logs-build-tmp 2>/dev/null; true'`, { stdio: 'inherit' });
377
+ try {
378
+ execSync(`docker exec ${container} ./node_modules/.bin/next build`, { stdio: 'inherit' });
379
+ } finally {
380
+ execSync(`docker exec ${container} sh -c 'mv /app/.data-build-tmp /app/data 2>/dev/null; mv /app/.logs-build-tmp /app/logs 2>/dev/null; true'`, { stdio: 'inherit' });
381
+ }
382
+
383
+ // 7. Clean up web source from container (not needed at runtime)
384
+ execSync(`docker exec ${container} rm -rf /app/app`, { stdio: 'inherit' });
385
+
386
+ // 8. Restart PM2
387
+ console.log('\n Restarting server...');
388
+ execSync(`docker exec ${container} pm2 restart all`, { stdio: 'inherit' });
389
+
390
+ console.log('\n Fast synced!\n');
391
+ }
392
+
309
393
  export async function sync(projectPath) {
310
394
  if (!projectPath) {
311
395
  console.error('\n Usage: thepopebot sync <path-to-project>\n');