thepopebot 1.2.74 → 1.2.75-beta.10

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 (88) hide show
  1. package/README.md +11 -0
  2. package/api/CLAUDE.md +2 -0
  3. package/api/index.js +95 -3
  4. package/bin/cli.js +35 -6
  5. package/bin/docker-build.js +5 -0
  6. package/bin/managed-paths.js +1 -1
  7. package/bin/sync.js +91 -2
  8. package/config/instrumentation.js +4 -0
  9. package/lib/ai/async-channel.js +51 -0
  10. package/lib/ai/index.js +154 -153
  11. package/lib/ai/tools.js +46 -30
  12. package/lib/chat/actions.js +32 -5
  13. package/lib/chat/components/chat-header.js +4 -0
  14. package/lib/chat/components/chat-header.jsx +4 -0
  15. package/lib/chat/components/chat-input.js +2 -2
  16. package/lib/chat/components/chat-input.jsx +2 -2
  17. package/lib/chat/components/settings-chat-page.js +42 -37
  18. package/lib/chat/components/settings-chat-page.jsx +42 -37
  19. package/lib/chat/components/settings-coding-agents-page.js +139 -1
  20. package/lib/chat/components/settings-coding-agents-page.jsx +160 -0
  21. package/lib/chat/components/settings-jobs-page.js +81 -12
  22. package/lib/chat/components/settings-jobs-page.jsx +114 -35
  23. package/lib/chat/components/settings-secrets-layout.js +1 -1
  24. package/lib/chat/components/settings-secrets-layout.jsx +1 -1
  25. package/lib/code/actions.js +26 -10
  26. package/lib/code/code-page.js +2 -1
  27. package/lib/code/code-page.jsx +2 -1
  28. package/lib/code/port-forwards.js +17 -3
  29. package/lib/code/terminal-view.js +6 -3
  30. package/lib/code/terminal-view.jsx +6 -3
  31. package/lib/config.js +4 -0
  32. package/lib/db/api-keys.js +39 -45
  33. package/lib/db/config.js +63 -4
  34. package/lib/maintenance.js +58 -0
  35. package/lib/oauth/helper.js +34 -0
  36. package/lib/tools/create-agent-job.js +0 -1
  37. package/lib/tools/docker.js +28 -7
  38. package/package.json +3 -2
  39. package/setup/setup-ssl.mjs +414 -0
  40. package/templates/.github/workflows/upgrade-event-handler.yml +1 -1
  41. package/templates/.gitignore.template +7 -0
  42. package/templates/CLAUDE.md.template +3 -4
  43. package/templates/README.md +1 -1
  44. package/templates/docker-compose.custom.yml +40 -58
  45. package/templates/docker-compose.yml +14 -17
  46. package/templates/docs/CLI.md +3 -3
  47. package/templates/docs/CONFIGURATION.md +31 -65
  48. package/templates/docs/GETTING_STARTED.md +1 -1
  49. package/templates/docs/SECURITY.md +3 -3
  50. package/templates/docs/SKILLS.md +2 -1
  51. package/templates/skills/agent-job-secrets/SKILL.md +25 -0
  52. package/templates/skills/agent-job-secrets/agent-job-secrets.js +75 -0
  53. package/templates/skills/playwright-cli/SKILL.md +294 -0
  54. package/templates/docker-compose.litellm.yml +0 -81
  55. package/templates/skills/LICENSE +0 -21
  56. package/templates/skills/brave-search/SKILL.md +0 -79
  57. package/templates/skills/brave-search/content.js +0 -86
  58. package/templates/skills/brave-search/package-lock.json +0 -621
  59. package/templates/skills/brave-search/package.json +0 -14
  60. package/templates/skills/brave-search/search.js +0 -199
  61. package/templates/skills/browser-tools/SKILL.md +0 -196
  62. package/templates/skills/browser-tools/browser-content.js +0 -103
  63. package/templates/skills/browser-tools/browser-cookies.js +0 -35
  64. package/templates/skills/browser-tools/browser-eval.js +0 -53
  65. package/templates/skills/browser-tools/browser-hn-scraper.js +0 -108
  66. package/templates/skills/browser-tools/browser-nav.js +0 -44
  67. package/templates/skills/browser-tools/browser-pick.js +0 -162
  68. package/templates/skills/browser-tools/browser-screenshot.js +0 -34
  69. package/templates/skills/browser-tools/browser-start.js +0 -87
  70. package/templates/skills/browser-tools/package-lock.json +0 -2556
  71. package/templates/skills/browser-tools/package.json +0 -19
  72. package/templates/skills/get-secret/SKILL.md +0 -34
  73. package/templates/skills/get-secret/get-secret.js +0 -33
  74. package/templates/skills/google-docs/SKILL.md +0 -23
  75. package/templates/skills/google-docs/create.sh +0 -69
  76. package/templates/skills/google-drive/SKILL.md +0 -47
  77. package/templates/skills/google-drive/delete.sh +0 -47
  78. package/templates/skills/google-drive/download.sh +0 -50
  79. package/templates/skills/google-drive/list.sh +0 -41
  80. package/templates/skills/google-drive/upload.sh +0 -76
  81. package/templates/skills/kie-ai/SKILL.md +0 -38
  82. package/templates/skills/kie-ai/generate-image.sh +0 -77
  83. package/templates/skills/kie-ai/generate-video.sh +0 -69
  84. package/templates/skills/youtube-transcript/SKILL.md +0 -41
  85. package/templates/skills/youtube-transcript/package-lock.json +0 -24
  86. package/templates/skills/youtube-transcript/package.json +0 -8
  87. package/templates/skills/youtube-transcript/transcript.js +0 -84
  88. package/templates/traefik-dynamic.yml.example +0 -7
package/README.md CHANGED
@@ -192,6 +192,17 @@ See [Different Models](docs/RUNNING_DIFFERENT_MODELS.md) for the full provider r
192
192
 
193
193
  ---
194
194
 
195
+ ## Known Issues
196
+
197
+ ### Windows: `SQLITE_IOERR_SHMOPEN`
198
+
199
+ SQLite can't create or open its shared-memory (`.shm`) file. Common causes:
200
+
201
+ - **Antivirus** (Windows Defender, etc.) locking the database files — add your project folder to the exclusion list
202
+ - **Cloud-synced folders** (OneDrive, Dropbox, Google Drive) — move your project to a non-synced directory like `C:\Projects\`
203
+
204
+ ---
205
+
195
206
  ## Docs
196
207
 
197
208
  | Document | Description |
package/api/CLAUDE.md CHANGED
@@ -25,6 +25,8 @@ Browser-facing data fetching uses **fetch route handlers** colocated with pages
25
25
  |--------|------|------|---------|
26
26
  | GET | `/api/ping` | None | Health check |
27
27
  | POST | `/api/create-agent-job` | `x-api-key` | Create agent job |
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) |
28
30
  | GET | `/api/agent-jobs/status` | `x-api-key` | Agent job status (query: `?agent_job_id=`) |
29
31
  | POST | `/api/telegram/webhook` | Telegram webhook secret | Telegram message handler |
30
32
  | POST | `/api/telegram/register` | `x-api-key` | Register bot token + webhook URL |
package/api/index.js CHANGED
@@ -11,6 +11,9 @@ import { getConfig } from '../lib/config.js';
11
11
  import { parseOAuthState, exchangeCodeForToken } from '../lib/oauth/helper.js';
12
12
  import { setAgentJobSecret } from '../lib/db/config.js';
13
13
 
14
+ // ── Per-key lock for OAuth token refresh ────────────────────────────
15
+ const _refreshLocks = new Map();
16
+
14
17
  // Bot token — resolved from DB/env, can be overridden by /telegram/register
15
18
  let telegramBotToken = null;
16
19
 
@@ -104,6 +107,81 @@ async function handleCreateAgentJob(request) {
104
107
  }
105
108
  }
106
109
 
110
+ async function handleGetAgentSecret(request) {
111
+ const record = verifyApiKey(request.headers.get('x-api-key'));
112
+ if (record.type !== 'agent_job_api_key') {
113
+ return Response.json({ error: 'Forbidden' }, { status: 403 });
114
+ }
115
+
116
+ const key = new URL(request.url).searchParams.get('key');
117
+ if (!key) return Response.json({ error: 'Missing key' }, { status: 400 });
118
+
119
+ const { getAgentJobSecretRaw, setAgentJobSecret: saveSecret } = await import('../lib/db/config.js');
120
+ const raw = getAgentJobSecretRaw(key);
121
+ if (!raw) return Response.json({ error: 'Not found' }, { status: 404 });
122
+
123
+ let parsed;
124
+ try {
125
+ parsed = JSON.parse(raw);
126
+ } catch {
127
+ // Plain string
128
+ return Response.json({ value: raw });
129
+ }
130
+
131
+ if (parsed.type === 'oauth2') {
132
+ // Serialize refresh per key — prevents concurrent requests from racing on token rotation
133
+ if (!_refreshLocks.has(key)) _refreshLocks.set(key, Promise.resolve());
134
+ let release;
135
+ const gate = new Promise((r) => { release = r; });
136
+ const prev = _refreshLocks.get(key);
137
+ _refreshLocks.set(key, gate);
138
+ await prev;
139
+
140
+ try {
141
+ // Re-read after acquiring lock — previous request may have already refreshed
142
+ const freshRaw = getAgentJobSecretRaw(key);
143
+ const freshParsed = freshRaw ? JSON.parse(freshRaw) : parsed;
144
+
145
+ const { refreshOAuthToken } = await import('../lib/oauth/helper.js');
146
+ const newToken = await refreshOAuthToken({
147
+ refreshToken: freshParsed.token.refresh_token,
148
+ clientId: freshParsed.clientId,
149
+ clientSecret: freshParsed.clientSecret,
150
+ tokenUrl: freshParsed.tokenUrl,
151
+ });
152
+ // Persist updated token (refresh token may have rotated)
153
+ saveSecret(key, JSON.stringify({ ...freshParsed, token: { ...freshParsed.token, ...newToken } }), 'refresh');
154
+ return Response.json({ value: newToken.access_token });
155
+ } catch (err) {
156
+ console.error(`[secrets] OAuth refresh failed for "${key}":`, err.message);
157
+ return Response.json({ error: `OAuth refresh failed: ${err.message}` }, { status: 502 });
158
+ } finally {
159
+ release();
160
+ }
161
+ }
162
+ if (parsed.type === 'oauth_token') {
163
+ return Response.json({ value: JSON.stringify(parsed.token) });
164
+ }
165
+ // Unknown structured value — return raw
166
+ return Response.json({ value: raw });
167
+ }
168
+
169
+ async function handleSetAgentSecret(request) {
170
+ const record = verifyApiKey(request.headers.get('x-api-key'));
171
+ if (record.type !== 'agent_job_api_key') {
172
+ return Response.json({ error: 'Forbidden' }, { status: 403 });
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 });
183
+ }
184
+
107
185
  async function handleTelegramRegister(request) {
108
186
  const body = await request.json();
109
187
  const { bot_token, webhook_url } = body;
@@ -248,9 +326,21 @@ async function handleOAuthCallback(request) {
248
326
  redirectUri,
249
327
  });
250
328
 
251
- // Save the full token JSON as the secret value
252
- const tokenJson = JSON.stringify(tokenData);
253
- setAgentJobSecret(state.secretName, tokenJson, 'oauth');
329
+ // Save token with typed wrapper so the API can auto-refresh on fetch
330
+ const secretType = state.secretType || 'oauth2';
331
+ let stored;
332
+ if (secretType === 'oauth_token') {
333
+ stored = JSON.stringify({ type: 'oauth_token', token: tokenData });
334
+ } else {
335
+ stored = JSON.stringify({
336
+ type: 'oauth2',
337
+ token: tokenData,
338
+ clientId: state.clientId,
339
+ clientSecret: state.clientSecret,
340
+ tokenUrl: state.tokenUrl,
341
+ });
342
+ }
343
+ setAgentJobSecret(state.secretName, stored, 'oauth');
254
344
 
255
345
  return oauthResultPage(true, state.secretName);
256
346
  } catch (err) {
@@ -315,6 +405,7 @@ async function POST(request) {
315
405
  // Route to handler
316
406
  switch (routePath) {
317
407
  case '/create-agent-job': return handleCreateAgentJob(request);
408
+ case '/set-agent-job-secret': return handleSetAgentSecret(request);
318
409
  case '/telegram/webhook': return handleTelegramWebhook(request);
319
410
  case '/telegram/register': return handleTelegramRegister(request);
320
411
  case '/github/webhook': return handleGithubWebhook(request);
@@ -333,6 +424,7 @@ async function GET(request) {
333
424
  switch (routePath) {
334
425
  case '/ping': return Response.json({ message: 'Pong!' });
335
426
  case '/agent-jobs/status': return handleAgentJobStatus(request);
427
+ case '/get-agent-job-secret': return handleGetAgentSecret(request);
336
428
  case '/oauth/callback': return handleOAuthCallback(request);
337
429
  default: return Response.json({ error: 'Not found' }, { status: 404 });
338
430
  }
package/bin/cli.js CHANGED
@@ -52,11 +52,13 @@ 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)
58
59
  diff [file] Show differences between project files and package templates
59
60
  sync <path> Sync local package to a test install (build, pack, Docker)
61
+ sync --fast <path> Fast sync — copy source into running container, rebuild .next
60
62
  set-var <KEY> [VALUE] Set a GitHub repository variable
61
63
  user:password <email> Change a user's password
62
64
  `);
@@ -246,6 +248,7 @@ async function init() {
246
248
  const pkg = {
247
249
  name: dirName,
248
250
  private: true,
251
+ type: 'module',
249
252
  scripts: {
250
253
  setup: 'thepopebot setup',
251
254
  'setup-telegram': 'thepopebot setup-telegram',
@@ -258,11 +261,19 @@ async function init() {
258
261
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
259
262
  console.log(' Created package.json');
260
263
  } else {
261
- console.log(' Skipped package.json (already exists)');
264
+ // Ensure "type": "module" is set for ESM support
265
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
266
+ if (!pkg.type) {
267
+ pkg.type = 'module';
268
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
269
+ console.log(' Added "type": "module" to package.json');
270
+ } else {
271
+ console.log(' Skipped package.json (already exists)');
272
+ }
262
273
  }
263
274
 
264
275
  // Create default skill activation symlinks
265
- const defaultSkills = ['get-secret'];
276
+ const defaultSkills = [];
266
277
  const activeDir = path.join(cwd, 'skills', 'active');
267
278
  fs.mkdirSync(activeDir, { recursive: true });
268
279
  for (const skill of defaultSkills) {
@@ -337,8 +348,7 @@ AUTH_TRUST_HOST=true
337
348
  DATABASE_PATH=data/db/thepopebot.sqlite
338
349
  THEPOPEBOT_VERSION=${version}
339
350
 
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:
351
+ # To enable SSL with Let's Encrypt, run: npx thepopebot setup-ssl
342
352
  # COMPOSE_FILE=docker-compose.custom.yml
343
353
  `;
344
354
  fs.writeFileSync(envPath, seedEnv);
@@ -490,6 +500,15 @@ function setup() {
490
500
  }
491
501
  }
492
502
 
503
+ function setupSsl() {
504
+ const setupScript = path.join(__dirname, '..', 'setup', 'setup-ssl.mjs');
505
+ try {
506
+ execFileSync(process.execPath, [setupScript], { stdio: 'inherit', cwd: process.cwd() });
507
+ } catch {
508
+ process.exit(1);
509
+ }
510
+ }
511
+
493
512
  function setupTelegram() {
494
513
  const setupScript = path.join(__dirname, '..', 'setup', 'setup-telegram.mjs');
495
514
  try {
@@ -770,6 +789,9 @@ switch (command) {
770
789
  case 'setup':
771
790
  setup();
772
791
  break;
792
+ case 'setup-ssl':
793
+ setupSsl();
794
+ break;
773
795
  case 'setup-telegram':
774
796
  setupTelegram();
775
797
  break;
@@ -787,8 +809,15 @@ switch (command) {
787
809
  await upgrade();
788
810
  break;
789
811
  case 'sync': {
790
- const { sync } = await import('./sync.js');
791
- await sync(args[0]);
812
+ const fast = args.includes('--fast');
813
+ const syncArgs = args.filter(a => a !== '--fast');
814
+ if (fast) {
815
+ const { syncFast } = await import('./sync.js');
816
+ await syncFast(syncArgs[0]);
817
+ } else {
818
+ const { sync } = await import('./sync.js');
819
+ await sync(syncArgs[0]);
820
+ }
792
821
  break;
793
822
  }
794
823
  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)
@@ -13,7 +13,7 @@ export const MANAGED_PATHS = [
13
13
  'skills/CLAUDE.md',
14
14
  'cron/CLAUDE.md',
15
15
  'triggers/CLAUDE.md',
16
- 'docs/CLAUDE.md',
16
+ 'docs/',
17
17
  ];
18
18
 
19
19
  export function isManaged(relPath) {
package/bin/sync.js CHANGED
@@ -277,8 +277,7 @@ function buildDockerImage(projectPath) {
277
277
  fs.cpSync(webSrc, webDest, { recursive: true });
278
278
 
279
279
  try {
280
- // Build using stdin Dockerfile with project dir as context (no cache to ensure fresh package)
281
- execSync(`docker build --no-cache -f - -t ${imageTag} .`, {
280
+ execSync(`docker build -f - -t ${imageTag} .`, {
282
281
  input: dockerfile,
283
282
  stdio: ['pipe', 'inherit', 'inherit'],
284
283
  cwd: projectPath,
@@ -287,6 +286,12 @@ function buildDockerImage(projectPath) {
287
286
  fs.rmSync(webDest, { recursive: true, force: true });
288
287
  }
289
288
 
289
+ // Clean up dangling images from previous builds
290
+ try {
291
+ execSync('docker image prune -f', { stdio: 'ignore' });
292
+ } catch {}
293
+
294
+
290
295
  // Update THEPOPEBOT_VERSION in .env
291
296
  const envPath = path.join(projectPath, '.env');
292
297
  if (fs.existsSync(envPath)) {
@@ -301,6 +306,90 @@ function buildDockerImage(projectPath) {
301
306
  }
302
307
  }
303
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
+
304
393
  export async function sync(projectPath) {
305
394
  if (!projectPath) {
306
395
  console.error('\n Usage: thepopebot sync <path-to-project>\n');
@@ -70,5 +70,9 @@ export async function register() {
70
70
  const { startClusterRuntime } = await import('../lib/cluster/runtime.js');
71
71
  startClusterRuntime();
72
72
 
73
+ // Start internal maintenance cron (cleanup orphaned agent job keys, etc.)
74
+ const { startMaintenanceCron } = await import('../lib/maintenance.js');
75
+ startMaintenanceCron();
76
+
73
77
  console.log('thepopebot initialized');
74
78
  }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Async push/pull queue. Producer calls push()/done(), consumer uses for-await.
3
+ */
4
+ export function createChannel() {
5
+ const queue = [];
6
+ const waiters = [];
7
+ let isDone = false;
8
+
9
+ return {
10
+ push(value) {
11
+ if (waiters.length > 0) waiters.shift()(value);
12
+ else queue.push(value);
13
+ },
14
+ done() {
15
+ isDone = true;
16
+ while (waiters.length > 0) waiters.shift()(Symbol.for('done'));
17
+ },
18
+ async *[Symbol.asyncIterator]() {
19
+ while (true) {
20
+ if (queue.length > 0) {
21
+ yield queue.shift();
22
+ } else if (isDone) {
23
+ return;
24
+ } else {
25
+ const value = await new Promise(resolve => waiters.push(resolve));
26
+ if (value === Symbol.for('done')) return;
27
+ yield value;
28
+ }
29
+ }
30
+ }
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Merge two async iterables — yields from whichever has data first.
36
+ * Completes when BOTH are exhausted.
37
+ */
38
+ export async function* mergeAsyncIterables(iter1, iter2) {
39
+ const channel = createChannel();
40
+ let active = 2;
41
+
42
+ const consume = async (iter) => {
43
+ for await (const item of iter) channel.push(item);
44
+ if (--active === 0) channel.done();
45
+ };
46
+
47
+ consume(iter1);
48
+ consume(iter2);
49
+
50
+ yield* channel;
51
+ }