thepopebot 1.2.75-beta.1 → 1.2.75-beta.12

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 (89) hide show
  1. package/README.md +12 -1
  2. package/bin/CLAUDE.md +1 -1
  3. package/bin/cli.js +37 -8
  4. package/bin/docker-build.js +5 -0
  5. package/bin/managed-paths.js +2 -5
  6. package/bin/sync.js +84 -0
  7. package/config/CLAUDE.md +1 -29
  8. package/config/instrumentation.js +1 -1
  9. package/lib/CLAUDE.md +3 -3
  10. package/lib/ai/CLAUDE.md +24 -3
  11. package/lib/ai/agent.js +8 -5
  12. package/lib/ai/async-channel.js +51 -0
  13. package/lib/ai/headless-stream.js +3 -0
  14. package/lib/ai/index.js +148 -172
  15. package/lib/ai/line-mappers.js +72 -9
  16. package/lib/ai/tools.js +40 -28
  17. package/lib/chat/actions.js +34 -6
  18. package/lib/chat/components/chat-header.js +4 -0
  19. package/lib/chat/components/chat-header.jsx +4 -0
  20. package/lib/chat/components/crons-page.js +1 -1
  21. package/lib/chat/components/crons-page.jsx +1 -1
  22. package/lib/chat/components/message.js +3 -2
  23. package/lib/chat/components/message.jsx +3 -2
  24. package/lib/chat/components/settings-chat-page.js +2 -1
  25. package/lib/chat/components/settings-chat-page.jsx +4 -1
  26. package/lib/chat/components/settings-coding-agents-page.js +139 -1
  27. package/lib/chat/components/settings-coding-agents-page.jsx +160 -0
  28. package/lib/chat/components/settings-jobs-page.js +13 -2
  29. package/lib/chat/components/settings-jobs-page.jsx +15 -1
  30. package/lib/chat/components/settings-secrets-layout.js +1 -1
  31. package/lib/chat/components/settings-secrets-layout.jsx +1 -1
  32. package/lib/chat/components/triggers-page.js +1 -1
  33. package/lib/chat/components/triggers-page.jsx +1 -1
  34. package/lib/cluster/actions.js +4 -4
  35. package/lib/cluster/execute.js +3 -1
  36. package/lib/code/actions.js +26 -10
  37. package/lib/code/code-page.js +1 -1
  38. package/lib/code/code-page.jsx +1 -1
  39. package/lib/code/port-forwards.js +17 -3
  40. package/lib/config.js +4 -0
  41. package/lib/cron.js +3 -3
  42. package/lib/db/api-keys.js +22 -61
  43. package/lib/db/config.js +23 -0
  44. package/lib/db/index.js +3 -1
  45. package/lib/maintenance.js +34 -11
  46. package/lib/paths.js +1 -38
  47. package/lib/tools/create-agent-job.js +0 -4
  48. package/lib/tools/docker.js +23 -7
  49. package/lib/triggers.js +4 -3
  50. package/lib/utils/render-md.js +3 -1
  51. package/package.json +1 -1
  52. package/setup/setup-ssl.mjs +414 -0
  53. package/templates/.github/workflows/upgrade-event-handler.yml +1 -1
  54. package/templates/.gitignore.template +7 -0
  55. package/templates/CLAUDE.md +3 -2
  56. package/templates/agent-job/CLAUDE.md.template +34 -0
  57. package/templates/{config → agent-job}/CRONS.json +1 -1
  58. package/templates/docker-compose.custom.yml +41 -62
  59. package/templates/docker-compose.yml +15 -21
  60. package/templates/skills/agent-job-secrets/agent-job-secrets.js +18 -9
  61. package/templates/CLAUDE.md.template +0 -368
  62. package/templates/README.md +0 -75
  63. package/templates/config/CLAUDE.md.template +0 -40
  64. package/templates/cron/CLAUDE.md.template +0 -24
  65. package/templates/docker-compose.litellm.yml +0 -82
  66. package/templates/docs/CLAUDE.md.template +0 -12
  67. package/templates/docs/CLI.md +0 -59
  68. package/templates/docs/CLUSTERS.md +0 -151
  69. package/templates/docs/CONFIGURATION.md +0 -181
  70. package/templates/docs/CRONS_AND_TRIGGERS.md +0 -132
  71. package/templates/docs/GETTING_STARTED.md +0 -64
  72. package/templates/docs/SECURITY.md +0 -61
  73. package/templates/docs/SKILLS.md +0 -113
  74. package/templates/docs/UPGRADING.md +0 -92
  75. package/templates/skills/LICENSE +0 -21
  76. package/templates/traefik-dynamic.yml.example +0 -7
  77. package/templates/triggers/.gitkeep +0 -0
  78. package/templates/triggers/CLAUDE.md.template +0 -41
  79. /package/templates/{config → agent-job}/HEARTBEAT.md +0 -0
  80. /package/templates/{config/agent-job → agent-job}/SOUL.md +0 -0
  81. /package/templates/{config/agent-job/AGENT_JOB.md → agent-job/SYSTEM.md} +0 -0
  82. /package/templates/{cron/.gitkeep → event-handler/CLAUDE.md.template} +0 -0
  83. /package/templates/{config/agent-job → event-handler}/SUMMARY.md +0 -0
  84. /package/templates/{config → event-handler}/TRIGGERS.json +0 -0
  85. /package/templates/{config → event-handler}/agent-chat/SYSTEM.md +0 -0
  86. /package/templates/{config/cluster → event-handler/clusters}/ROLE.md +0 -0
  87. /package/templates/{config/cluster → event-handler/clusters}/SYSTEM.md +0 -0
  88. /package/templates/{config → event-handler}/code-chat/SYSTEM.md +0 -0
  89. /package/templates/{config → event-handler}/litellm/main.yaml +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
 
@@ -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/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,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 = ['agent-job-secrets'];
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);
@@ -375,7 +385,7 @@ function reset(filePath) {
375
385
  console.log(` ${destPath(file)}`);
376
386
  }
377
387
  console.log('\nUsage: thepopebot reset <file>');
378
- console.log('Example: thepopebot reset config/SOUL.md\n');
388
+ console.log('Example: thepopebot reset agent-job/SOUL.md\n');
379
389
  return;
380
390
  }
381
391
 
@@ -432,7 +442,7 @@ function diff(filePath) {
432
442
  console.log(' All files match package templates.');
433
443
  }
434
444
  console.log('\nUsage: thepopebot diff <file>');
435
- console.log('Example: thepopebot diff config/SOUL.md\n');
445
+ console.log('Example: thepopebot diff agent-job/SOUL.md\n');
436
446
  return;
437
447
  }
438
448
 
@@ -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)
@@ -8,12 +8,9 @@ export const MANAGED_PATHS = [
8
8
  'docker-compose.yml',
9
9
  '.dockerignore',
10
10
  '.gitignore',
11
- 'CLAUDE.md',
12
- 'config/CLAUDE.md',
11
+ 'agent-job/CLAUDE.md',
12
+ 'event-handler/CLAUDE.md',
13
13
  'skills/CLAUDE.md',
14
- 'cron/CLAUDE.md',
15
- 'triggers/CLAUDE.md',
16
- 'docs/',
17
14
  ];
18
15
 
19
16
  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');
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
 
@@ -70,7 +70,7 @@ 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 expired agent job keys, etc.)
73
+ // Start internal maintenance cron (cleanup orphaned agent job keys, etc.)
74
74
  const { startMaintenanceCron } = await import('../lib/maintenance.js');
75
75
  startMaintenanceCron();
76
76
 
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;
@@ -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
+ }
@@ -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