@testchimp/cli 0.1.38 → 0.1.39
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/dist/chimphands/run.js +134 -62
- package/package.json +1 -1
package/dist/chimphands/run.js
CHANGED
|
@@ -16,12 +16,8 @@ const STATUS_RUNNING = "CHIMPHANDS_SESSION_STATUS_RUNNING";
|
|
|
16
16
|
const STATUS_WAITING_USER = "CHIMPHANDS_SESSION_STATUS_WAITING_USER";
|
|
17
17
|
const STATUS_IDLE = "CHIMPHANDS_SESSION_STATUS_IDLE";
|
|
18
18
|
const STATUS_FAILED = "CHIMPHANDS_SESSION_STATUS_FAILED";
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
if (!trimmed)
|
|
22
|
-
return trimmed;
|
|
23
|
-
const rest = trimmed.replace(/^\/testchimp\s*/i, "").trim();
|
|
24
|
-
return rest ? `/testchimp ${rest}` : "/testchimp";
|
|
19
|
+
function normalizeUserMessage(content) {
|
|
20
|
+
return content.trim();
|
|
25
21
|
}
|
|
26
22
|
function apiHeaders(apiKey) {
|
|
27
23
|
return {
|
|
@@ -291,57 +287,83 @@ function runOpencode(prompt, model, childEnv, postEvent) {
|
|
|
291
287
|
return Promise.resolve({ code: errObj.status || 1, err: fatal });
|
|
292
288
|
}
|
|
293
289
|
}
|
|
294
|
-
function
|
|
290
|
+
function connectInboundStream(backend, apiKey, sessionId, handlers) {
|
|
295
291
|
const url = new URL(`${backend}/api/chimphands/sessions/${encodeURIComponent(sessionId)}/inbound`);
|
|
296
292
|
const lib = url.protocol === "https:" ? https : http;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
293
|
+
let stopped = false;
|
|
294
|
+
let reconnectTimer = null;
|
|
295
|
+
let reconnectDelayMs = 1000;
|
|
296
|
+
const scheduleReconnect = () => {
|
|
297
|
+
if (stopped || !handlers.shouldRun())
|
|
298
|
+
return;
|
|
299
|
+
if (reconnectTimer)
|
|
300
|
+
return;
|
|
301
|
+
reconnectTimer = setTimeout(() => {
|
|
302
|
+
reconnectTimer = null;
|
|
303
|
+
connect();
|
|
304
|
+
}, reconnectDelayMs);
|
|
305
|
+
reconnectDelayMs = Math.min(reconnectDelayMs * 2, 15_000);
|
|
306
|
+
};
|
|
307
|
+
const connect = () => {
|
|
308
|
+
if (stopped || !handlers.shouldRun())
|
|
309
|
+
return;
|
|
310
|
+
const req = lib.request({
|
|
311
|
+
hostname: url.hostname,
|
|
312
|
+
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
313
|
+
path: url.pathname + url.search,
|
|
314
|
+
method: "GET",
|
|
315
|
+
headers: {
|
|
316
|
+
"TestChimp-Api-Key": apiKey,
|
|
317
|
+
Accept: "text/event-stream",
|
|
318
|
+
"Cache-Control": "no-cache",
|
|
319
|
+
},
|
|
320
|
+
}, (res) => {
|
|
321
|
+
reconnectDelayMs = 1000;
|
|
322
|
+
let buf = "";
|
|
323
|
+
let eventName = "message";
|
|
324
|
+
res.on("data", (chunk) => {
|
|
325
|
+
buf += chunk.toString();
|
|
326
|
+
const parts = buf.split("\n");
|
|
327
|
+
buf = parts.pop() || "";
|
|
328
|
+
for (const line of parts) {
|
|
329
|
+
if (line.startsWith("event:")) {
|
|
330
|
+
eventName = line.slice(6).trim() || "message";
|
|
322
331
|
}
|
|
323
|
-
else if (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (content)
|
|
328
|
-
onUserMessage(content);
|
|
332
|
+
else if (line.startsWith("data:")) {
|
|
333
|
+
const data = line.slice(5).trim();
|
|
334
|
+
if (eventName === "idle") {
|
|
335
|
+
handlers.onIdle();
|
|
329
336
|
}
|
|
330
|
-
|
|
331
|
-
|
|
337
|
+
else if (eventName === "user_message" || eventName === "message") {
|
|
338
|
+
try {
|
|
339
|
+
const msg = JSON.parse(data);
|
|
340
|
+
handlers.onUserMessage({
|
|
341
|
+
id: msg.id || msg.message_id,
|
|
342
|
+
content: msg.content || "",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
/* ignore */
|
|
347
|
+
}
|
|
332
348
|
}
|
|
349
|
+
eventName = "message";
|
|
350
|
+
}
|
|
351
|
+
else if (line === "") {
|
|
352
|
+
eventName = "message";
|
|
333
353
|
}
|
|
334
|
-
eventName = "message";
|
|
335
|
-
}
|
|
336
|
-
else if (line === "") {
|
|
337
|
-
eventName = "message";
|
|
338
354
|
}
|
|
339
|
-
}
|
|
355
|
+
});
|
|
356
|
+
res.on("end", () => scheduleReconnect());
|
|
340
357
|
});
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
358
|
+
req.on("error", () => scheduleReconnect());
|
|
359
|
+
req.end();
|
|
360
|
+
};
|
|
361
|
+
connect();
|
|
362
|
+
return () => {
|
|
363
|
+
stopped = true;
|
|
364
|
+
if (reconnectTimer)
|
|
365
|
+
clearTimeout(reconnectTimer);
|
|
366
|
+
};
|
|
345
367
|
}
|
|
346
368
|
export async function runChimphands(opts) {
|
|
347
369
|
const apiKey = requireApiKey();
|
|
@@ -373,9 +395,41 @@ export async function runChimphands(opts) {
|
|
|
373
395
|
console.error(`ChimpHands OpenCode model: ${opencodeModel}`);
|
|
374
396
|
const idleMs = (Number(boot.idle_timeout_seconds) || 600) * 1000;
|
|
375
397
|
const queue = [];
|
|
398
|
+
const seenUserMessageIds = new Set();
|
|
376
399
|
let idle = false;
|
|
377
|
-
let
|
|
400
|
+
let sessionActive = true;
|
|
378
401
|
let lastUserActivity = Date.now();
|
|
402
|
+
const enqueueUserMessage = (msg) => {
|
|
403
|
+
const id = msg.id?.trim();
|
|
404
|
+
if (id) {
|
|
405
|
+
if (seenUserMessageIds.has(id))
|
|
406
|
+
return;
|
|
407
|
+
seenUserMessageIds.add(id);
|
|
408
|
+
}
|
|
409
|
+
const content = normalizeUserMessage(msg.content || "");
|
|
410
|
+
if (!content)
|
|
411
|
+
return;
|
|
412
|
+
queue.push(content);
|
|
413
|
+
lastUserActivity = Date.now();
|
|
414
|
+
idle = false;
|
|
415
|
+
};
|
|
416
|
+
const pollPendingUserMessages = async () => {
|
|
417
|
+
try {
|
|
418
|
+
const text = await postJson(backend, apiKey, "/api/chimphands/consume_pending_user_messages", {
|
|
419
|
+
sessionId,
|
|
420
|
+
});
|
|
421
|
+
const data = JSON.parse(text);
|
|
422
|
+
for (const msg of data.messages || []) {
|
|
423
|
+
enqueueUserMessage({
|
|
424
|
+
id: msg.id || msg.message_id,
|
|
425
|
+
content: msg.content || "",
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
// Polling is best-effort when inbound SSE misses an event.
|
|
431
|
+
}
|
|
432
|
+
};
|
|
379
433
|
const postEvent = (role, content, status, messageId) => {
|
|
380
434
|
const body = {
|
|
381
435
|
sessionId,
|
|
@@ -403,30 +457,46 @@ export async function runChimphands(opts) {
|
|
|
403
457
|
};
|
|
404
458
|
if (userId)
|
|
405
459
|
childEnv.TESTCHIMP_USER_ID = userId;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
closed = true;
|
|
460
|
+
const stopInbound = connectInboundStream(backend, apiKey, sessionId, {
|
|
461
|
+
onUserMessage: enqueueUserMessage,
|
|
462
|
+
onIdle: () => {
|
|
463
|
+
idle = true;
|
|
464
|
+
},
|
|
465
|
+
shouldRun: () => sessionActive,
|
|
413
466
|
});
|
|
414
467
|
postEvent(ROLE_STATUS, "Agent ready", STATUS_RUNNING);
|
|
415
|
-
let prompt =
|
|
468
|
+
let prompt = normalizeUserMessage(promptInput || boot.initial_prompt || "");
|
|
416
469
|
if (boot.conversation_summary) {
|
|
417
470
|
prompt = `Conversation so far:\n${boot.conversation_summary}\n\nCurrent task:\n${prompt}`;
|
|
418
471
|
}
|
|
419
472
|
for (const m of boot.pending_user_messages || []) {
|
|
420
473
|
if (m?.content)
|
|
421
|
-
|
|
474
|
+
enqueueUserMessage({ content: m.content });
|
|
422
475
|
}
|
|
423
476
|
const waitForNextPrompt = () => new Promise((resolve) => {
|
|
477
|
+
let lastPollAt = 0;
|
|
424
478
|
const tick = () => {
|
|
425
479
|
if (queue.length) {
|
|
426
|
-
resolve(
|
|
480
|
+
resolve(normalizeUserMessage(queue.shift()));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const now = Date.now();
|
|
484
|
+
if (now - lastPollAt >= 1500) {
|
|
485
|
+
lastPollAt = now;
|
|
486
|
+
void pollPendingUserMessages().then(() => {
|
|
487
|
+
if (queue.length) {
|
|
488
|
+
resolve(normalizeUserMessage(queue.shift()));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (idle || now - lastUserActivity >= idleMs) {
|
|
492
|
+
resolve(null);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
setTimeout(tick, 500);
|
|
496
|
+
});
|
|
427
497
|
return;
|
|
428
498
|
}
|
|
429
|
-
if (idle ||
|
|
499
|
+
if (idle || now - lastUserActivity >= idleMs) {
|
|
430
500
|
resolve(null);
|
|
431
501
|
return;
|
|
432
502
|
}
|
|
@@ -435,7 +505,7 @@ export async function runChimphands(opts) {
|
|
|
435
505
|
tick();
|
|
436
506
|
});
|
|
437
507
|
while (prompt) {
|
|
438
|
-
const result = await runOpencode(
|
|
508
|
+
const result = await runOpencode(prompt, opencodeModel, childEnv, postEvent);
|
|
439
509
|
if (result.code !== 0) {
|
|
440
510
|
const errMsg = (result.err || "opencode failed").trim() || "opencode failed";
|
|
441
511
|
console.error(`ChimpHands OpenCode failed: ${errMsg}`);
|
|
@@ -468,6 +538,8 @@ export async function runChimphands(opts) {
|
|
|
468
538
|
idle = false;
|
|
469
539
|
prompt = (await waitForNextPrompt()) || "";
|
|
470
540
|
}
|
|
541
|
+
sessionActive = false;
|
|
542
|
+
stopInbound();
|
|
471
543
|
console.error("ChimpHands session idle — no user input before timeout; completing.");
|
|
472
544
|
complete(STATUS_IDLE);
|
|
473
545
|
}
|
package/package.json
CHANGED