thepopebot 1.2.75-beta.8 → 1.2.75

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 (102) 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 +295 -9
  6. package/bin/managed-paths.js +0 -7
  7. package/config/CLAUDE.md +1 -29
  8. package/lib/CLAUDE.md +3 -3
  9. package/lib/ai/CLAUDE.md +24 -3
  10. package/lib/ai/agent.js +8 -5
  11. package/lib/ai/headless-stream.js +3 -0
  12. package/lib/ai/index.js +9 -34
  13. package/lib/ai/line-mappers.js +72 -9
  14. package/lib/ai/tools.js +12 -16
  15. package/lib/chat/actions.js +15 -5
  16. package/lib/chat/api.js +17 -1
  17. package/lib/chat/components/chat-input.js +1 -0
  18. package/lib/chat/components/chat-input.jsx +1 -0
  19. package/lib/chat/components/chat.js +9 -1
  20. package/lib/chat/components/chat.jsx +15 -2
  21. package/lib/chat/components/chats-page.js +9 -4
  22. package/lib/chat/components/chats-page.jsx +7 -7
  23. package/lib/chat/components/code-mode-toggle.js +9 -13
  24. package/lib/chat/components/code-mode-toggle.jsx +17 -13
  25. package/lib/chat/components/crons-page.js +1 -1
  26. package/lib/chat/components/crons-page.jsx +1 -1
  27. package/lib/chat/components/message.js +12 -4
  28. package/lib/chat/components/message.jsx +17 -4
  29. package/lib/chat/components/settings-chat-page.js +2 -1
  30. package/lib/chat/components/settings-chat-page.jsx +4 -1
  31. package/lib/chat/components/sidebar-history-item.js +3 -3
  32. package/lib/chat/components/sidebar-history-item.jsx +4 -6
  33. package/lib/chat/components/triggers-page.js +1 -1
  34. package/lib/chat/components/triggers-page.jsx +1 -1
  35. package/lib/chat/components/ui/confirm-dialog.js +32 -28
  36. package/lib/chat/components/ui/confirm-dialog.jsx +6 -4
  37. package/lib/cluster/actions.js +4 -4
  38. package/lib/cluster/execute.js +3 -1
  39. package/lib/code/actions.js +18 -11
  40. package/lib/code/code-page.js +40 -40
  41. package/lib/code/code-page.jsx +36 -36
  42. package/lib/code/terminal-view.js +23 -7
  43. package/lib/code/terminal-view.jsx +25 -7
  44. package/lib/cron.js +3 -3
  45. package/lib/db/index.js +3 -1
  46. package/lib/paths.js +1 -38
  47. package/lib/tools/docker.js +3 -10
  48. package/lib/triggers.js +4 -3
  49. package/lib/utils/render-md.js +3 -1
  50. package/package.json +2 -1
  51. package/templates/.github/workflows/rebuild-event-handler.yml +3 -0
  52. package/templates/.github/workflows/upgrade-event-handler.yml +1 -1
  53. package/templates/.gitignore.template +6 -3
  54. package/templates/.tmp/CLAUDE.md.template +5 -0
  55. package/templates/CLAUDE.md +3 -2
  56. package/templates/CLAUDE.md.template +24 -356
  57. package/templates/agent-job/CLAUDE.md.template +57 -0
  58. package/templates/agent-job/CRONS.json +16 -0
  59. package/templates/{config/agent-job → agent-job}/SOUL.md +3 -3
  60. package/templates/agent-job/SYSTEM.md +60 -0
  61. package/templates/agents/CLAUDE.md.template +54 -0
  62. package/templates/data/CLAUDE.md.template +5 -0
  63. package/templates/docker-compose.custom.yml +3 -5
  64. package/templates/docker-compose.yml +3 -5
  65. package/templates/event-handler/CLAUDE.md.template +0 -0
  66. package/templates/logs/CLAUDE.md.template +5 -0
  67. package/templates/skills/CLAUDE.md.template +57 -32
  68. package/templates/skills/active/.gitkeep +0 -0
  69. package/templates/skills/library/agent-job-secrets/SKILL.md +23 -0
  70. package/templates/skills/{agent-job-secrets → library/agent-job-secrets}/agent-job-secrets.js +26 -39
  71. package/templates/.pi/extensions/env-sanitizer/index.ts +0 -48
  72. package/templates/.pi/extensions/env-sanitizer/package.json +0 -5
  73. package/templates/README.md +0 -75
  74. package/templates/config/CLAUDE.md.template +0 -40
  75. package/templates/config/CRONS.json +0 -56
  76. package/templates/config/agent-job/AGENT_JOB.md +0 -30
  77. package/templates/cron/CLAUDE.md.template +0 -24
  78. package/templates/docs/CLAUDE.md.template +0 -12
  79. package/templates/docs/CLI.md +0 -59
  80. package/templates/docs/CLUSTERS.md +0 -151
  81. package/templates/docs/CONFIGURATION.md +0 -147
  82. package/templates/docs/CRONS_AND_TRIGGERS.md +0 -132
  83. package/templates/docs/GETTING_STARTED.md +0 -64
  84. package/templates/docs/SECURITY.md +0 -61
  85. package/templates/docs/SKILLS.md +0 -114
  86. package/templates/docs/UPGRADING.md +0 -92
  87. package/templates/skills/LICENSE +0 -21
  88. package/templates/skills/README.md +0 -117
  89. package/templates/skills/agent-job-secrets/SKILL.md +0 -25
  90. package/templates/triggers/CLAUDE.md.template +0 -41
  91. /package/templates/{config → agent-job}/HEARTBEAT.md +0 -0
  92. /package/templates/{cron → data}/.gitkeep +0 -0
  93. /package/templates/{logs → data/clusters}/.gitkeep +0 -0
  94. /package/templates/{triggers → data/db}/.gitkeep +0 -0
  95. /package/templates/{config/agent-job → event-handler}/SUMMARY.md +0 -0
  96. /package/templates/{config → event-handler}/TRIGGERS.json +0 -0
  97. /package/templates/{config → event-handler}/agent-chat/SYSTEM.md +0 -0
  98. /package/templates/{config/cluster → event-handler/clusters}/ROLE.md +0 -0
  99. /package/templates/{config/cluster → event-handler/clusters}/SYSTEM.md +0 -0
  100. /package/templates/{config → event-handler}/code-chat/SYSTEM.md +0 -0
  101. /package/templates/{config → event-handler}/litellm/main.yaml +0 -0
  102. /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
@@ -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
  }
@@ -273,14 +278,14 @@ async function init() {
273
278
  }
274
279
 
275
280
  // Create default skill activation symlinks
276
- const defaultSkills = ['agent-job-secrets'];
281
+ const defaultSkills = [];
277
282
  const activeDir = path.join(cwd, 'skills', 'active');
278
283
  fs.mkdirSync(activeDir, { recursive: true });
279
284
  for (const skill of defaultSkills) {
280
285
  const symlink = path.join(activeDir, skill);
281
286
  if (!fs.existsSync(symlink)) {
282
- createDirLink(`../${skill}`, symlink);
283
- console.log(` Created skills/active/${skill} → ../${skill}`);
287
+ createDirLink(`../library/${skill}`, symlink);
288
+ console.log(` Created skills/active/${skill} → ../library/${skill}`);
284
289
  }
285
290
  }
286
291
 
@@ -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
  */
@@ -385,7 +420,7 @@ function reset(filePath) {
385
420
  console.log(` ${destPath(file)}`);
386
421
  }
387
422
  console.log('\nUsage: thepopebot reset <file>');
388
- console.log('Example: thepopebot reset config/SOUL.md\n');
423
+ console.log('Example: thepopebot reset agent-job/SOUL.md\n');
389
424
  return;
390
425
  }
391
426
 
@@ -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
 
@@ -442,7 +502,7 @@ function diff(filePath) {
442
502
  console.log(' All files match package templates.');
443
503
  }
444
504
  console.log('\nUsage: thepopebot diff <file>');
445
- console.log('Example: thepopebot diff config/SOUL.md\n');
505
+ console.log('Example: thepopebot diff agent-job/SOUL.md\n');
446
506
  return;
447
507
  }
448
508
 
@@ -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;
@@ -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/config/CLAUDE.md CHANGED
@@ -1,32 +1,4 @@
1
- # config/ — Configuration Files
2
-
3
- ## Directory Structure
4
-
5
- ```
6
- config/
7
- ├── agent-chat/
8
- │ └── SYSTEM.md # Agent chat system prompt (supports {{skills}}, {{datetime}})
9
- ├── code-chat/
10
- │ └── SYSTEM.md # Code workspace system prompt
11
- ├── agent-job/
12
- │ ├── SOUL.md # Agent personality/identity (used by Docker agent)
13
- │ ├── AGENT_JOB.md # Agent runtime environment docs (used by Docker agent)
14
- │ └── SUMMARY.md # Prompt for summarizing completed jobs
15
- ├── cluster/
16
- │ ├── SYSTEM.md # Cluster worker system prompt
17
- │ └── ROLE.md # Per-role prompt template for cluster workers
18
- ├── HEARTBEAT.md # Self-monitoring behavior (cron task prompt)
19
- ├── CRONS.json # Scheduled job definitions
20
- └── TRIGGERS.json # Webhook trigger definitions
21
- ```
22
-
23
- ## Markdown File Includes
24
-
25
- Markdown files in `config/` support includes and built-in variables, powered by `lib/utils/render-md.js`.
26
-
27
- - **File includes**: `{{ filepath.md }}` — resolves relative to project root, recursive with circular detection. Missing files are left as-is.
28
- - **`{{datetime}}`** — Current ISO timestamp.
29
- - **`{{skills}}`** — Dynamic bullet list of active skill descriptions from `skills/active/*/SKILL.md` frontmatter. Never hardcode skill names — this is resolved at runtime.
1
+ # config/ — Next.js Config Wrapper
30
2
 
31
3
  ## Next.js Config Wrapper (index.js)
32
4
 
package/lib/CLAUDE.md CHANGED
@@ -12,16 +12,16 @@ If the task needs to *think*, use `agent`. If it just needs to *do*, use `comman
12
12
 
13
13
  **Agent**: Creates a Docker Agent job via `createAgentJob()`. Pushes an `agent-job/*` branch and launches a local Docker container. The `job` string is the LLM task prompt. Agent backend selected via `agent_backend` in `agent-job.config.json`.
14
14
 
15
- **Command**: Runs a shell command on the event handler. Working directory: `cron/` for crons, `triggers/` for triggers.
15
+ **Command**: Runs a shell command on the event handler. Working directory: project root.
16
16
 
17
17
  **Webhook**: Makes an HTTP request. `GET` skips the body; `POST` (default) sends `{ ...vars }` or `{ ...vars, data: <payload> }`.
18
18
 
19
19
  ## Cron Jobs
20
20
 
21
- Defined in `config/CRONS.json`, loaded by `lib/cron.js` at startup via `node-cron`. Each entry has `name`, `schedule` (cron expression), `type` (`agent`/`command`/`webhook`), and the corresponding action fields (`job`, `command`, or `url`/`method`/`headers`/`vars`). Set `enabled: false` to disable. Agent-type entries support optional `llm_provider` and `llm_model` fields to override the default LLM (passed to Docker agent via `agent-job.config.json`).
21
+ Defined in `agent-job/CRONS.json`, loaded by `lib/cron.js` at startup via `node-cron`. Each entry has `name`, `schedule` (cron expression), `type` (`agent`/`command`/`webhook`), and the corresponding action fields (`job`, `command`, or `url`/`method`/`headers`/`vars`). Set `enabled: false` to disable. Agent-type entries support optional `llm_provider` and `llm_model` fields to override the default LLM (passed to Docker agent via `agent-job.config.json`).
22
22
 
23
23
  ## Webhook Triggers
24
24
 
25
- Defined in `config/TRIGGERS.json`, loaded by `lib/triggers.js`. Each trigger watches an endpoint path (`watch_path`) and fires an array of actions (fire-and-forget, after auth, before route handler). Actions use the same `type`/`job`/`command`/`url` fields as cron jobs, including optional `llm_provider`/`llm_model` overrides.
25
+ Defined in `event-handler/TRIGGERS.json`, loaded by `lib/triggers.js`. Each trigger watches an endpoint path (`watch_path`) and fires an array of actions (fire-and-forget, after auth, before route handler). Actions use the same `type`/`job`/`command`/`url` fields as cron jobs, including optional `llm_provider`/`llm_model` overrides.
26
26
 
27
27
  Template tokens in `job` and `command` strings: `{{body}}`, `{{body.field}}`, `{{query}}`, `{{query.field}}`, `{{headers}}`, `{{headers.field}}`.
package/lib/ai/CLAUDE.md CHANGED
@@ -5,12 +5,12 @@
5
5
  Two agent singletons, both using `createReactAgent` from `@langchain/langgraph/prebuilt` with `SqliteSaver` for conversation memory:
6
6
 
7
7
  **Agent Chat** — singleton via `getAgentChat()`:
8
- - System prompt: `config/agent-chat/SYSTEM.md` (rendered fresh each invocation via `render_md()`)
8
+ - System prompt: `event-handler/agent-chat/SYSTEM.md` (rendered fresh each invocation via `render_md()`)
9
9
  - Tools: `agent_job`, `coding_agent`
10
10
  - Call `resetAgentChats()` to clear both singletons (required if hot-reloading)
11
11
 
12
12
  **Code Chat** — singleton via `getCodeChat()`:
13
- - System prompt: `config/code-chat/SYSTEM.md` (rendered fresh each invocation)
13
+ - System prompt: `event-handler/code-chat/SYSTEM.md` (rendered fresh each invocation)
14
14
  - Tools: `coding_agent` (reads repo/branch/workspace from `runtime.configurable`)
15
15
 
16
16
  ## Adding a New Tool
@@ -73,7 +73,28 @@ Three-layer parser for Claude Code agents running in headless Docker containers:
73
73
  3. **Event mapper** (`mapLine()`) — Converts each line to chat events:
74
74
  - `assistant` messages: `text` blocks → `{ type: 'text' }`, `tool_use` blocks → `{ type: 'tool-call' }`
75
75
  - `user` messages: `tool_result` blocks → `{ type: 'tool-result' }` (priority: stdout > string content > array)
76
- - `result` messages: → `{ type: 'text', _resultSummary }` (injected into LangGraph memory)
76
+ - `result` messages: → `{ type: 'text' }` (final summary from the agent)
77
77
  - Non-JSON lines (e.g. `NO_CHANGES`, `AGENT_FAILED`): wrapped as plain text events
78
78
 
79
79
  `parseHeadlessStream(dockerLogStream)` is an async generator consuming `http.IncomingMessage`. `mapLine()` is also reused by `lib/cluster/stream.js` for worker log parsing.
80
+
81
+ ### Tool Return Format
82
+
83
+ The `coding_agent` tool (in `tools.js`) returns the **full container session** as a flat JSON array. This becomes the ToolMessage in LangGraph's checkpoint, giving the LLM complete context on the current turn. The array contains:
84
+
85
+ - `{ type: 'meta', codingAgent, backendApi }` — first event, agent identity
86
+ - `{ type: 'text', text }` — agent text output
87
+ - `{ type: 'tool-call', toolCallId, toolName, args }` — agent tool invocations
88
+ - `{ type: 'tool-result', toolCallId, result }` — tool execution results
89
+ - `{ type: 'exit', exitCode }` — last event, container exit status
90
+
91
+ On error before streaming starts: `[{ type: 'error', message }]`.
92
+
93
+ ### Adding a New Agent Mapper (line-mappers.js)
94
+
95
+ Each coding agent CLI has its own mapper function (`mapClaudeCodeLine`, `mapPiLine`, `mapGeminiLine`, `mapCodexLine`, `mapOpenCodeLine`, `mapKimiLine`). When adding a new agent:
96
+
97
+ 1. Create `mapXxxLine(parsed)` in `line-mappers.js` that returns an array of `{ type, ... }` events
98
+ 2. Register it in `headless-stream.js`: add to imports, re-exports, and the `mapperMap` object
99
+ 3. Map the agent's JSON output to three event types: `{ type: 'text', text }`, `{ type: 'tool-call', toolCallId, toolName, args }`, `{ type: 'tool-result', toolCallId, result }`
100
+ 4. Return `[{ type: 'skip' }]` for noise events (session init, rate limits, etc.) to suppress them without triggering the unknown fallback
package/lib/ai/agent.js CHANGED
@@ -3,7 +3,8 @@ import { SystemMessage } from '@langchain/core/messages';
3
3
  import { createModel } from './model.js';
4
4
  import { agentJobTool, agentChatCodingTool, codeChatCodingTool } from './tools.js';
5
5
  import { SqliteSaver } from '@langchain/langgraph-checkpoint-sqlite';
6
- import { agentJobPlanningMd, codePlanningMd, thepopebotDb } from '../paths.js';
6
+ import path from 'path';
7
+ import { PROJECT_ROOT } from '../paths.js';
7
8
  import { render_md } from '../utils/render-md.js';
8
9
 
9
10
  // Singletons on globalThis to survive Next.js webpack chunk duplication.
@@ -20,13 +21,14 @@ export async function getAgentChat() {
20
21
  const model = await createModel();
21
22
  const tools = [agentJobTool, agentChatCodingTool];
22
23
 
23
- const checkpointer = SqliteSaver.fromConnString(thepopebotDb);
24
+ const dbPath = process.env.DATABASE_PATH || path.join(PROJECT_ROOT, 'data/db/thepopebot.sqlite');
25
+ const checkpointer = SqliteSaver.fromConnString(dbPath);
24
26
 
25
27
  globalThis.__popebotAgentChat = createReactAgent({
26
28
  llm: model,
27
29
  tools,
28
30
  checkpointSaver: checkpointer,
29
- prompt: (state) => [new SystemMessage(render_md(agentJobPlanningMd)), ...state.messages],
31
+ prompt: (state) => [new SystemMessage(render_md(path.join(PROJECT_ROOT, 'event-handler/agent-chat/SYSTEM.md'))), ...state.messages],
30
32
  });
31
33
  }
32
34
  return globalThis.__popebotAgentChat;
@@ -41,13 +43,14 @@ export async function getCodeChat() {
41
43
  const model = await createModel();
42
44
  const tools = [codeChatCodingTool];
43
45
 
44
- const checkpointer = SqliteSaver.fromConnString(thepopebotDb);
46
+ const dbPath = process.env.DATABASE_PATH || path.join(PROJECT_ROOT, 'data/db/thepopebot.sqlite');
47
+ const checkpointer = SqliteSaver.fromConnString(dbPath);
45
48
 
46
49
  globalThis.__popebotCodeChat = createReactAgent({
47
50
  llm: model,
48
51
  tools,
49
52
  checkpointSaver: checkpointer,
50
- prompt: (state) => [new SystemMessage(render_md(codePlanningMd)), ...state.messages],
53
+ prompt: (state) => [new SystemMessage(render_md(path.join(PROJECT_ROOT, 'event-handler/code-chat/SYSTEM.md'))), ...state.messages],
51
54
  });
52
55
  }
53
56
  return globalThis.__popebotCodeChat;
@@ -10,6 +10,7 @@ export {
10
10
  mapGeminiLine,
11
11
  mapCodexLine,
12
12
  mapOpenCodeLine,
13
+ mapKimiLine,
13
14
  } from './line-mappers.js';
14
15
 
15
16
  import {
@@ -19,6 +20,7 @@ import {
19
20
  mapGeminiLine,
20
21
  mapCodexLine,
21
22
  mapOpenCodeLine,
23
+ mapKimiLine,
22
24
  } from './line-mappers.js';
23
25
 
24
26
  /**
@@ -41,6 +43,7 @@ export async function* parseHeadlessStream(dockerLogStream, codingAgent = 'claud
41
43
  'gemini-cli': mapGeminiLine,
42
44
  'codex-cli': mapCodexLine,
43
45
  'opencode': mapOpenCodeLine,
46
+ 'kimi-cli': mapKimiLine,
44
47
  };
45
48
  const mapper = mapperMap[codingAgent] || mapClaudeCodeLine;
46
49