thepopebot 1.2.77 → 1.2.78-beta.1

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.
@@ -1,4 +1,4 @@
1
- import { Transform } from 'stream';
1
+ import { PassThrough } from 'stream';
2
2
  import split2 from 'split2';
3
3
  import { DockerFrameParser } from '../tools/docker.js';
4
4
 
@@ -47,23 +47,35 @@ export async function* parseHeadlessStream(dockerLogStream, codingAgent = 'claud
47
47
  };
48
48
  const mapper = mapperMap[codingAgent] || mapClaudeCodeLine;
49
49
 
50
- // Layer 1: Docker frame decoder using shared DockerFrameParser
50
+ // Layer 1: Docker frame decoder. Stdout frames feed the line splitter;
51
+ // stderr frames are buffered and yielded as `{type:'stderr'}` chunks so the
52
+ // caller can surface them on silent failures (agent crashed before producing
53
+ // JSON).
54
+ const stdoutPipe = new PassThrough();
55
+ const stderrChunks = [];
51
56
  const parser = new DockerFrameParser();
52
- const frameDecoder = new Transform({
53
- transform(chunk, encoding, callback) {
57
+ let frameError = null;
58
+ dockerLogStream.on('data', (chunk) => {
59
+ try {
54
60
  for (const frame of parser.push(chunk)) {
55
61
  if (frame.stream === 'stdout') {
56
- this.push(Buffer.from(frame.text, 'utf8'));
62
+ stdoutPipe.write(Buffer.from(frame.text, 'utf8'));
63
+ } else if (frame.stream === 'stderr' && frame.text) {
64
+ stderrChunks.push(frame.text);
57
65
  }
58
66
  }
59
- callback();
67
+ } catch (err) {
68
+ frameError = err;
69
+ stdoutPipe.destroy(err);
60
70
  }
61
71
  });
72
+ dockerLogStream.on('end', () => stdoutPipe.end());
73
+ dockerLogStream.on('error', (err) => stdoutPipe.destroy(err));
62
74
 
63
- // Layer 2: split2 for reliable line splitting
64
- const lines = dockerLogStream.pipe(frameDecoder).pipe(split2());
75
+ // Layer 2: split2 for reliable line splitting (stdout only)
76
+ const lines = stdoutPipe.pipe(split2());
65
77
 
66
- // Layer 3: map each complete line to chat events
78
+ // Layer 3: map each complete line to chat events; flush stderr at end
67
79
  for await (const line of lines) {
68
80
  const trimmed = line.trim();
69
81
  if (!trimmed) continue;
@@ -71,4 +83,8 @@ export async function* parseHeadlessStream(dockerLogStream, codingAgent = 'claud
71
83
  yield event;
72
84
  }
73
85
  }
86
+ if (frameError) throw frameError;
87
+ if (stderrChunks.length) {
88
+ yield { type: 'stderr', text: stderrChunks.join('') };
89
+ }
74
90
  }
package/lib/ai/index.js CHANGED
@@ -350,6 +350,9 @@ async function* streamViaContainer({ threadId, message, options, codingAgent, wo
350
350
  const pendingToolCalls = new Map();
351
351
  let started = false;
352
352
  let wrapperClosed = false;
353
+ let producedAny = false;
354
+ let stderrTail = '';
355
+ const STDERR_TAIL_MAX = 4096;
353
356
 
354
357
  const closeWrapper = (result) => {
355
358
  if (wrapperClosed) return null;
@@ -384,6 +387,15 @@ async function* streamViaContainer({ threadId, message, options, codingAgent, wo
384
387
  const logStream = await tailContainerLogs(containerName);
385
388
 
386
389
  for await (const chunk of parseHeadlessStream(logStream, codingAgent)) {
390
+ // Buffer stderr server-side; never yield to the caller. Surfaced only
391
+ // when the agent died silently (producedAny stays false at stream end).
392
+ if (chunk.type === 'stderr') {
393
+ stderrTail = (stderrTail + chunk.text).slice(-STDERR_TAIL_MAX);
394
+ continue;
395
+ }
396
+ if (chunk.type === 'text' || chunk.type === 'tool-call' || chunk.type === 'tool-result') {
397
+ producedAny = true;
398
+ }
387
399
  if (chunk.type === 'text') {
388
400
  pendingText += chunk.text;
389
401
  } else if (chunk.type === 'tool-call') {
@@ -413,11 +425,20 @@ async function* streamViaContainer({ threadId, message, options, codingAgent, wo
413
425
  const exitCode = await waitForContainer(containerName);
414
426
  await removeContainer(containerName);
415
427
 
416
- const closeResult = exitCode === 0 ? 'Completed' : `Exited with code ${exitCode}`;
428
+ const stderrTrimmed = stderrTail.trim();
429
+ const silentFailure = !producedAny && stderrTrimmed;
430
+
431
+ const closeResult = silentFailure
432
+ ? `Error: ${stderrTrimmed}`
433
+ : exitCode === 0 ? 'Completed' : `Exited with code ${exitCode}`;
417
434
  const closeChunk = closeWrapper(closeResult);
418
435
  if (closeChunk) yield closeChunk;
419
436
 
420
- if (exitCode !== 0) {
437
+ if (silentFailure) {
438
+ const msg = `Coding agent produced no output. Stderr:\n${stderrTrimmed}`;
439
+ yield { type: 'error', message: msg };
440
+ try { saveMessage(threadId, options.userId,'assistant', JSON.stringify({ type: 'error', message: msg })); } catch {}
441
+ } else if (exitCode !== 0) {
421
442
  const msg = `Coding agent exited with code ${exitCode}`;
422
443
  yield { type: 'error', message: msg };
423
444
  try { saveMessage(threadId, options.userId,'assistant', JSON.stringify({ type: 'error', message: msg })); } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thepopebot",
3
- "version": "1.2.77",
3
+ "version": "1.2.78-beta.1",
4
4
  "type": "module",
5
5
  "description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
6
6
  "bin": {
@@ -112,7 +112,7 @@
112
112
  "zod": "^4.3.6"
113
113
  },
114
114
  "peerDependencies": {
115
- "next": ">=15.5.12",
115
+ "next": "^15.5.12",
116
116
  "next-auth": "^5.0.0-beta.30",
117
117
  "next-themes": ">=0.4.0",
118
118
  "react": ">=19.0.0",
package/setup/setup.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execSync } from 'child_process';
4
+ import http from 'http';
4
5
  import fs from 'fs';
5
6
  import path from 'path';
6
7
  import chalk from 'chalk';
@@ -438,19 +439,31 @@ async function main() {
438
439
  // Probe /login (not /api/ping) to confirm Next.js can actually render
439
440
  // the page, not just answer API routes. /login serves SetupForm on a
440
441
  // fresh install; LoginForm once a user exists. Either way it's HTML.
441
- async function isLoginPageReady(timeoutMs = 2000) {
442
- try {
443
- const res = await fetch('http://localhost:80/login', {
444
- method: 'GET',
445
- signal: AbortSignal.timeout(timeoutMs),
446
- redirect: 'manual',
447
- });
448
- if (!res.ok) return false;
449
- const ct = res.headers.get('content-type') || '';
450
- return ct.includes('text/html');
451
- } catch {
452
- return false;
453
- }
442
+ // Traefik routes by Host(`${APP_HOSTNAME}`), so a bare localhost:80
443
+ // request hits Traefik's default 404 — set the Host header to match.
444
+ // node:http is used (not fetch) because undici silently strips Host.
445
+ const appHostname = collected.APP_HOSTNAME;
446
+ function isLoginPageReady(timeoutMs = 2000) {
447
+ return new Promise((resolve) => {
448
+ const req = http.request(
449
+ {
450
+ host: '127.0.0.1',
451
+ port: 80,
452
+ method: 'GET',
453
+ path: '/login',
454
+ headers: { Host: appHostname },
455
+ timeout: timeoutMs,
456
+ },
457
+ (res) => {
458
+ const ok = res.statusCode === 200 && (res.headers['content-type'] || '').includes('text/html');
459
+ res.resume();
460
+ resolve(ok);
461
+ }
462
+ );
463
+ req.on('error', () => resolve(false));
464
+ req.on('timeout', () => { req.destroy(); resolve(false); });
465
+ req.end();
466
+ });
454
467
  }
455
468
 
456
469
  let serverUp = await isLoginPageReady(3000);