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.
- package/lib/ai/headless-stream.js +25 -9
- package/lib/ai/index.js +23 -2
- package/package.json +2 -2
- package/setup/setup.mjs +26 -13
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 (
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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);
|