@testchimp/cli 0.1.51 → 0.1.52
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 +166 -105
- package/package.json +1 -1
package/dist/chimphands/run.js
CHANGED
|
@@ -137,9 +137,12 @@ class AgentEventPoster {
|
|
|
137
137
|
if (opts?.pullRequestUrl)
|
|
138
138
|
body.pullRequestUrl = opts.pullRequestUrl;
|
|
139
139
|
const streamRole = isStreamFanoutRole(role);
|
|
140
|
-
// Never drop liveStream tokens: ephemeral when UI is on this replica path,
|
|
141
|
-
// otherwise durable post_agent_event (cross-replica / detached safe).
|
|
142
140
|
this.chain = this.chain.then(async () => {
|
|
141
|
+
// Mid-turn live tokens only while UI is attached — async runs skip FS fanout
|
|
142
|
+
// and rely on turn-end reconcile for durable PG.
|
|
143
|
+
if (opts?.liveStream && !this.uiAttached) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
143
146
|
if (opts?.throttle || (streamRole && opts?.liveStream)) {
|
|
144
147
|
const now = Date.now();
|
|
145
148
|
const wait = STREAM_POST_MIN_INTERVAL_MS - (now - this.lastStreamPostAt);
|
|
@@ -170,11 +173,21 @@ class AgentEventPoster {
|
|
|
170
173
|
}
|
|
171
174
|
if (delivered)
|
|
172
175
|
return;
|
|
173
|
-
//
|
|
176
|
+
// Mid-turn liveStream must not fall through to durable — that would write
|
|
177
|
+
// every token to PG. Turn-end reconcile persists the final transcript.
|
|
178
|
+
if (opts?.liveStream) {
|
|
179
|
+
console.error("ChimpHands ephemeral not delivered (replica miss?) — skipping durable for liveStream");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Completed/non-live: durable fallback for cross-replica UI.
|
|
174
183
|
console.error("ChimpHands ephemeral not delivered (replica miss?) — persisting via post_agent_event");
|
|
175
184
|
}
|
|
176
185
|
catch (err) {
|
|
177
186
|
const detail = err instanceof Error ? err.message : String(err);
|
|
187
|
+
if (opts?.liveStream) {
|
|
188
|
+
console.error(`ChimpHands ephemeral post failed — skipping durable for liveStream: ${detail}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
178
191
|
console.error(`ChimpHands ephemeral post failed — durable fallback: ${detail}`);
|
|
179
192
|
}
|
|
180
193
|
}
|
|
@@ -274,7 +287,43 @@ function normalizeStdoutOpencodeEvent(raw) {
|
|
|
274
287
|
const props = (inner.properties && typeof inner.properties === "object"
|
|
275
288
|
? inner.properties
|
|
276
289
|
: {});
|
|
277
|
-
if (type === "message.part.
|
|
290
|
+
if (type === "message.part.delta") {
|
|
291
|
+
// OpenCode streams tokens as { partID, field, delta } with no `part`.
|
|
292
|
+
const deltaProps = props;
|
|
293
|
+
const part = deltaProps.part;
|
|
294
|
+
if (part) {
|
|
295
|
+
const partType = part.type || deltaProps.field || "";
|
|
296
|
+
let mapped;
|
|
297
|
+
if (partType === "text")
|
|
298
|
+
mapped = "text";
|
|
299
|
+
else if (partType === "reasoning")
|
|
300
|
+
mapped = "reasoning";
|
|
301
|
+
else if (partType === "tool")
|
|
302
|
+
mapped = "tool_use";
|
|
303
|
+
else
|
|
304
|
+
return null;
|
|
305
|
+
if (deltaProps.delta && !part.text)
|
|
306
|
+
part.text = deltaProps.delta;
|
|
307
|
+
return {
|
|
308
|
+
type: mapped,
|
|
309
|
+
sessionID: part.sessionID || deltaProps.sessionID,
|
|
310
|
+
part,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const partID = deltaProps.partID;
|
|
314
|
+
const field = deltaProps.field || "text";
|
|
315
|
+
const delta = deltaProps.delta;
|
|
316
|
+
if (!partID || delta == null || delta === "")
|
|
317
|
+
return null;
|
|
318
|
+
if (field !== "text" && field !== "reasoning")
|
|
319
|
+
return null;
|
|
320
|
+
return {
|
|
321
|
+
type: field === "reasoning" ? "reasoning" : "text",
|
|
322
|
+
sessionID: deltaProps.sessionID,
|
|
323
|
+
part: { id: partID, type: field, text: delta },
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
if (type === "message.part.updated") {
|
|
278
327
|
const part = props.part;
|
|
279
328
|
if (!part)
|
|
280
329
|
return null;
|
|
@@ -288,7 +337,6 @@ function normalizeStdoutOpencodeEvent(raw) {
|
|
|
288
337
|
mapped = "tool_use";
|
|
289
338
|
else
|
|
290
339
|
return null;
|
|
291
|
-
// Prefer cumulative text; append delta when that's all we got.
|
|
292
340
|
if (props.delta && !part.text) {
|
|
293
341
|
part.text = props.delta;
|
|
294
342
|
}
|
|
@@ -311,7 +359,7 @@ function normalizeStdoutOpencodeEvent(raw) {
|
|
|
311
359
|
}
|
|
312
360
|
return null;
|
|
313
361
|
}
|
|
314
|
-
/** Pull assistant/tool parts from OpenCode HTTP
|
|
362
|
+
/** Pull assistant/tool parts from OpenCode HTTP for durable PG (turn-end). */
|
|
315
363
|
async function reconcileOpencodeSessionMessages(attachUrl, opencodeSessionId, postEvent, onWorkingBranch) {
|
|
316
364
|
const base = attachUrl.replace(/\/$/, "");
|
|
317
365
|
const url = `${base}/session/${encodeURIComponent(opencodeSessionId)}/message`;
|
|
@@ -332,8 +380,18 @@ async function reconcileOpencodeSessionMessages(attachUrl, opencodeSessionId, po
|
|
|
332
380
|
const data = (await res.json());
|
|
333
381
|
if (!Array.isArray(data))
|
|
334
382
|
return 0;
|
|
383
|
+
// Only parts after the latest user message (this turn). Full-history reconcile
|
|
384
|
+
// every turn would O(n) upsert the entire transcript for no benefit.
|
|
385
|
+
let lastUserIdx = -1;
|
|
386
|
+
for (let i = data.length - 1; i >= 0; i--) {
|
|
387
|
+
if ((data[i]?.info?.role || "").toLowerCase() === "user") {
|
|
388
|
+
lastUserIdx = i;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const turnSlice = lastUserIdx >= 0 ? data.slice(lastUserIdx + 1) : data;
|
|
335
393
|
let posted = 0;
|
|
336
|
-
for (const msg of
|
|
394
|
+
for (const msg of turnSlice) {
|
|
337
395
|
if ((msg.info?.role || "").toLowerCase() !== "assistant")
|
|
338
396
|
continue;
|
|
339
397
|
for (const part of msg.parts || []) {
|
|
@@ -367,7 +425,7 @@ async function reconcileOpencodeSessionMessages(attachUrl, opencodeSessionId, po
|
|
|
367
425
|
}
|
|
368
426
|
}
|
|
369
427
|
if (posted) {
|
|
370
|
-
console.error(`ChimpHands reconciled ${posted} part(s) from OpenCode session API`);
|
|
428
|
+
console.error(`ChimpHands reconciled ${posted} part(s) from OpenCode session API (this turn)`);
|
|
371
429
|
}
|
|
372
430
|
return posted;
|
|
373
431
|
}
|
|
@@ -434,7 +492,36 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
434
492
|
const ev = unwrapped;
|
|
435
493
|
const type = ev.type || "";
|
|
436
494
|
const props = ev.properties || {};
|
|
437
|
-
|
|
495
|
+
// Token stream: { partID, field, delta } — often no `part` object.
|
|
496
|
+
if (type === "message.part.delta") {
|
|
497
|
+
const part = props.part;
|
|
498
|
+
const partId = props.partID || part?.id || part?.messageID;
|
|
499
|
+
const field = props.field || part?.type || "text";
|
|
500
|
+
const sessionId = part?.sessionID || props.sessionID;
|
|
501
|
+
if (!sessionMatches(sessionId))
|
|
502
|
+
return;
|
|
503
|
+
callbacks.noteSessionId(sessionId);
|
|
504
|
+
if (!partId || props.delta == null || props.delta === "")
|
|
505
|
+
return;
|
|
506
|
+
if (field === "text") {
|
|
507
|
+
const next = (textByPartId.get(partId) || "") + props.delta;
|
|
508
|
+
textByPartId.set(partId, next);
|
|
509
|
+
callbacks.postEvent(ROLE_ASSISTANT, next, liveOpts({
|
|
510
|
+
messageId: `oc_text_${partId}`,
|
|
511
|
+
}));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (field === "reasoning") {
|
|
515
|
+
const key = `reasoning:${partId}`;
|
|
516
|
+
const next = (textByPartId.get(key) || "") + props.delta;
|
|
517
|
+
textByPartId.set(key, next);
|
|
518
|
+
callbacks.postEvent(ROLE_REASONING, next, liveOpts({
|
|
519
|
+
messageId: `oc_reasoning_${partId}`,
|
|
520
|
+
}));
|
|
521
|
+
}
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (type === "message.part.updated") {
|
|
438
525
|
const part = props.part;
|
|
439
526
|
if (!part)
|
|
440
527
|
return;
|
|
@@ -447,13 +534,12 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
447
534
|
if (!partId)
|
|
448
535
|
return;
|
|
449
536
|
let next = part.text || "";
|
|
450
|
-
if (props.delta) {
|
|
537
|
+
if (props.delta && !part.text) {
|
|
451
538
|
next = (textByPartId.get(partId) || "") + props.delta;
|
|
452
539
|
}
|
|
453
540
|
else if (!next && props.delta === undefined) {
|
|
454
541
|
return;
|
|
455
542
|
}
|
|
456
|
-
// Prefer cumulative part.text when present (idempotent); else delta accumulation.
|
|
457
543
|
if (part.text)
|
|
458
544
|
next = part.text;
|
|
459
545
|
textByPartId.set(partId, next);
|
|
@@ -484,8 +570,17 @@ function startOpencodeSseRelay(attachUrl, callbacks) {
|
|
|
484
570
|
}
|
|
485
571
|
if (part.type === "tool") {
|
|
486
572
|
const status = part.state?.status;
|
|
487
|
-
if (!status || status === "pending"
|
|
573
|
+
if (!status || status === "pending")
|
|
574
|
+
return;
|
|
575
|
+
// Ephemeral "(running)" bubbles while UI attached (liveStream gated in poster).
|
|
576
|
+
if (status === "running") {
|
|
577
|
+
const toolContent = formatToolUseContent(part);
|
|
578
|
+
callbacks.postEvent(ROLE_TOOL, toolContent, liveOpts({
|
|
579
|
+
messageId: opencodeMessageId("oc_tool_", part),
|
|
580
|
+
throttle: false,
|
|
581
|
+
}));
|
|
488
582
|
return;
|
|
583
|
+
}
|
|
489
584
|
const toolContent = formatToolUseContent(part);
|
|
490
585
|
callbacks.postEvent(ROLE_TOOL, toolContent, liveOpts({
|
|
491
586
|
messageId: opencodeMessageId("oc_tool_", part),
|
|
@@ -897,6 +992,19 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
897
992
|
if (!ev?.type)
|
|
898
993
|
return;
|
|
899
994
|
noteSessionId(ev.sessionID);
|
|
995
|
+
switch (ev.type) {
|
|
996
|
+
case "text":
|
|
997
|
+
case "reasoning":
|
|
998
|
+
case "tool_use": {
|
|
999
|
+
// Attach mode: live tokens come from OpenCode SSE; durable from turn-end
|
|
1000
|
+
// reconcile. Avoid double-fanout / double-accumulation with stdout.
|
|
1001
|
+
if (attachUrl)
|
|
1002
|
+
return;
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
default:
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
900
1008
|
switch (ev.type) {
|
|
901
1009
|
case "text": {
|
|
902
1010
|
const chunk = ev.part?.text;
|
|
@@ -907,7 +1015,14 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
907
1015
|
callbacks.postEvent(ROLE_ASSISTANT, chunk, { throttle: true });
|
|
908
1016
|
return;
|
|
909
1017
|
}
|
|
910
|
-
|
|
1018
|
+
// Without attach, --format json may emit completed cumulative or deltas.
|
|
1019
|
+
// Prefer replace when we already have longer text (cumulative); else append.
|
|
1020
|
+
const prev = textByPartId.get(partId) || "";
|
|
1021
|
+
const next = !prev
|
|
1022
|
+
? chunk
|
|
1023
|
+
: chunk.startsWith(prev)
|
|
1024
|
+
? chunk
|
|
1025
|
+
: prev + chunk;
|
|
911
1026
|
textByPartId.set(partId, next);
|
|
912
1027
|
callbacks.postEvent(ROLE_ASSISTANT, next, {
|
|
913
1028
|
throttle: true,
|
|
@@ -925,7 +1040,12 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
925
1040
|
return;
|
|
926
1041
|
}
|
|
927
1042
|
const reasoningKey = `reasoning:${partId}`;
|
|
928
|
-
const
|
|
1043
|
+
const prev = textByPartId.get(reasoningKey) || "";
|
|
1044
|
+
const next = !prev
|
|
1045
|
+
? chunk
|
|
1046
|
+
: chunk.startsWith(prev)
|
|
1047
|
+
? chunk
|
|
1048
|
+
: prev + chunk;
|
|
929
1049
|
textByPartId.set(reasoningKey, next);
|
|
930
1050
|
callbacks.postEvent(ROLE_REASONING, next, {
|
|
931
1051
|
throttle: true,
|
|
@@ -934,8 +1054,9 @@ function runOpencode(prompt, model, childEnv, opencodeSessionId, callbacks, atta
|
|
|
934
1054
|
return;
|
|
935
1055
|
}
|
|
936
1056
|
case "tool_use": {
|
|
1057
|
+
// Durable-only path (no attach). Skip in-progress.
|
|
937
1058
|
const status = ev.part?.state?.status;
|
|
938
|
-
if (!status || status === "pending")
|
|
1059
|
+
if (!status || status === "pending" || status === "running")
|
|
939
1060
|
return;
|
|
940
1061
|
const toolContent = formatToolUseContent(ev.part);
|
|
941
1062
|
callbacks.postEvent(ROLE_TOOL, toolContent, {
|
|
@@ -1122,11 +1243,14 @@ export async function runChimphands(opts) {
|
|
|
1122
1243
|
});
|
|
1123
1244
|
};
|
|
1124
1245
|
const syncLiveSse = (attached) => {
|
|
1125
|
-
poster.uiAttached = attached;
|
|
1126
1246
|
pendingUiAttached = attached;
|
|
1127
|
-
if (!attachUrl)
|
|
1247
|
+
if (!attachUrl) {
|
|
1248
|
+
poster.uiAttached = attached;
|
|
1128
1249
|
return;
|
|
1250
|
+
}
|
|
1129
1251
|
if (!opencodeHttpReady) {
|
|
1252
|
+
// Defer SSE; still track intent so first sync after ready is correct.
|
|
1253
|
+
poster.uiAttached = attached;
|
|
1130
1254
|
if (attached) {
|
|
1131
1255
|
console.error("ChimpHands UI attached — deferring OpenCode SSE until serve is ready");
|
|
1132
1256
|
}
|
|
@@ -1134,18 +1258,18 @@ export async function runChimphands(opts) {
|
|
|
1134
1258
|
}
|
|
1135
1259
|
if (attached) {
|
|
1136
1260
|
consecutiveUiDetached = 0;
|
|
1261
|
+
// Allow liveStream immediately when heartbeat says attached.
|
|
1262
|
+
poster.uiAttached = true;
|
|
1137
1263
|
startLiveSseIfNeeded("UI attached — starting OpenCode SSE fanout");
|
|
1138
1264
|
return;
|
|
1139
1265
|
}
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1266
|
+
// Detached / async: hysteresis avoids flap from cross-replica heartbeats.
|
|
1267
|
+
// Keep poster.uiAttached true + SSE up during the window so we don't drop
|
|
1268
|
+
// live tokens while the UI is still actually listening on another replica.
|
|
1142
1269
|
consecutiveUiDetached += 1;
|
|
1143
|
-
if (consecutiveUiDetached < UI_DETACHED_STOP_AFTER)
|
|
1144
|
-
if (!stopLiveSse) {
|
|
1145
|
-
startLiveSseIfNeeded("starting OpenCode SSE fanout (durable until UI attaches)");
|
|
1146
|
-
}
|
|
1270
|
+
if (consecutiveUiDetached < UI_DETACHED_STOP_AFTER)
|
|
1147
1271
|
return;
|
|
1148
|
-
|
|
1272
|
+
poster.uiAttached = false;
|
|
1149
1273
|
if (stopLiveSse) {
|
|
1150
1274
|
console.error(`ChimpHands UI detached for ${consecutiveUiDetached} heartbeats — stopping OpenCode SSE fanout`);
|
|
1151
1275
|
stopLiveSse();
|
|
@@ -1211,30 +1335,6 @@ export async function runChimphands(opts) {
|
|
|
1211
1335
|
const conversationSummary = bootStr(boot, "conversation_summary", "conversationSummary");
|
|
1212
1336
|
let workingBranch = bootStr(boot, "working_branch", "workingBranch") || undefined;
|
|
1213
1337
|
let pullRequestUrl = bootStr(boot, "pull_request_url", "pullRequestUrl") || undefined;
|
|
1214
|
-
const exportSignedUrl = bootStr(boot, "opencode_export_signed_url", "opencodeExportSignedUrl");
|
|
1215
|
-
if (exportSignedUrl && attachUrl) {
|
|
1216
|
-
try {
|
|
1217
|
-
const importedId = await importOpencodeExportFromUrl(exportSignedUrl);
|
|
1218
|
-
if (importedId) {
|
|
1219
|
-
opencodeSessionId = importedId;
|
|
1220
|
-
console.error(`ChimpHands rehydrated OpenCode session from export: ${importedId}`);
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
catch (err) {
|
|
1224
|
-
console.error(`ChimpHands export import failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
const snapshotExport = async () => {
|
|
1228
|
-
const sid = opencodeSessionId?.trim();
|
|
1229
|
-
if (!sid)
|
|
1230
|
-
return;
|
|
1231
|
-
try {
|
|
1232
|
-
await putOpencodeExport(backend, apiKey, sessionId, sid);
|
|
1233
|
-
}
|
|
1234
|
-
catch (err) {
|
|
1235
|
-
console.error(`ChimpHands export snapshot failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1236
|
-
}
|
|
1237
|
-
};
|
|
1238
1338
|
noteWorkingBranch = (branch, prUrl) => {
|
|
1239
1339
|
const normalizedBranch = branch.trim();
|
|
1240
1340
|
if (!normalizedBranch)
|
|
@@ -1334,7 +1434,6 @@ export async function runChimphands(opts) {
|
|
|
1334
1434
|
}
|
|
1335
1435
|
await commitAndPushDirtyWorktree("chimphands: commit before session idle/shutdown");
|
|
1336
1436
|
await poster.flush();
|
|
1337
|
-
await snapshotExport();
|
|
1338
1437
|
if (runtimeId) {
|
|
1339
1438
|
await postJson(backend, apiKey, "/api/chimphands/complete_runtime", {
|
|
1340
1439
|
runtimeId,
|
|
@@ -1434,6 +1533,24 @@ export async function runChimphands(opts) {
|
|
|
1434
1533
|
if (result.opencodeSessionId) {
|
|
1435
1534
|
opencodeSessionId = result.opencodeSessionId;
|
|
1436
1535
|
}
|
|
1536
|
+
// Turn-end durable reconcile into PG (idempotent messageIds). Mid-turn was
|
|
1537
|
+
// ephemeral-only when UI attached; async runs get their transcript here.
|
|
1538
|
+
// Also reconcile on failure so partial assistant/tool output is not lost.
|
|
1539
|
+
if (attachUrl && opencodeSessionId) {
|
|
1540
|
+
try {
|
|
1541
|
+
await reconcileOpencodeSessionMessages(attachUrl, opencodeSessionId, (role, content, opts) => {
|
|
1542
|
+
void poster.enqueue(role, content, {
|
|
1543
|
+
...opts,
|
|
1544
|
+
opencodeSessionId,
|
|
1545
|
+
// Explicitly not liveStream → always durable post_agent_event.
|
|
1546
|
+
});
|
|
1547
|
+
}, noteWorkingBranch);
|
|
1548
|
+
await poster.flush();
|
|
1549
|
+
}
|
|
1550
|
+
catch (err) {
|
|
1551
|
+
console.error(`ChimpHands turn-end reconcile failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1437
1554
|
if (result.code !== 0) {
|
|
1438
1555
|
const errMsg = (result.err || "opencode failed").trim() || "opencode failed";
|
|
1439
1556
|
console.error(`ChimpHands OpenCode failed: ${errMsg}`);
|
|
@@ -1459,7 +1576,6 @@ export async function runChimphands(opts) {
|
|
|
1459
1576
|
break;
|
|
1460
1577
|
}
|
|
1461
1578
|
postEvent(ROLE_STATUS, "Waiting for user input", { status: STATUS_WAITING_USER });
|
|
1462
|
-
await snapshotExport();
|
|
1463
1579
|
lastUserActivity = Date.now();
|
|
1464
1580
|
idle = false;
|
|
1465
1581
|
prompt = (await waitForNextPrompt()) || "";
|
|
@@ -1575,61 +1691,6 @@ async function commitAndPushDirtyWorktree(message) {
|
|
|
1575
1691
|
console.error(`ChimpHands commit-before-idle failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1576
1692
|
}
|
|
1577
1693
|
}
|
|
1578
|
-
async function putOpencodeExport(backend, apiKey, sessionId, opencodeSessionId) {
|
|
1579
|
-
const exported = await new Promise((resolve, reject) => {
|
|
1580
|
-
const child = spawn("opencode", ["export", opencodeSessionId], {
|
|
1581
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1582
|
-
});
|
|
1583
|
-
let out = "";
|
|
1584
|
-
let err = "";
|
|
1585
|
-
child.stdout.on("data", (d) => {
|
|
1586
|
-
out += d.toString();
|
|
1587
|
-
});
|
|
1588
|
-
child.stderr.on("data", (d) => {
|
|
1589
|
-
err += d.toString();
|
|
1590
|
-
});
|
|
1591
|
-
child.on("close", (code) => {
|
|
1592
|
-
if (code === 0 && out.trim())
|
|
1593
|
-
resolve(out);
|
|
1594
|
-
else
|
|
1595
|
-
reject(new Error(err.trim() || `opencode export exited ${code}`));
|
|
1596
|
-
});
|
|
1597
|
-
});
|
|
1598
|
-
const exportBase64 = Buffer.from(exported, "utf8").toString("base64");
|
|
1599
|
-
await postJson(backend, apiKey, "/api/chimphands/put_opencode_export", {
|
|
1600
|
-
sessionId,
|
|
1601
|
-
exportBase64,
|
|
1602
|
-
});
|
|
1603
|
-
}
|
|
1604
|
-
async function importOpencodeExportFromUrl(signedUrl) {
|
|
1605
|
-
const res = await fetch(signedUrl);
|
|
1606
|
-
if (!res.ok) {
|
|
1607
|
-
throw new Error(`download export failed: ${res.status}`);
|
|
1608
|
-
}
|
|
1609
|
-
const text = await res.text();
|
|
1610
|
-
writeFileSync("/tmp/chimphands-opencode-export.json", text, "utf8");
|
|
1611
|
-
return await new Promise((resolve, reject) => {
|
|
1612
|
-
const child = spawn("opencode", ["import", "/tmp/chimphands-opencode-export.json"], {
|
|
1613
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1614
|
-
});
|
|
1615
|
-
let out = "";
|
|
1616
|
-
let err = "";
|
|
1617
|
-
child.stdout.on("data", (d) => {
|
|
1618
|
-
out += d.toString();
|
|
1619
|
-
});
|
|
1620
|
-
child.stderr.on("data", (d) => {
|
|
1621
|
-
err += d.toString();
|
|
1622
|
-
});
|
|
1623
|
-
child.on("close", (code) => {
|
|
1624
|
-
if (code !== 0) {
|
|
1625
|
-
reject(new Error(err.trim() || `opencode import exited ${code}`));
|
|
1626
|
-
return;
|
|
1627
|
-
}
|
|
1628
|
-
const match = (out + "\n" + err).match(/ses_[A-Za-z0-9]+/);
|
|
1629
|
-
resolve(match?.[0]);
|
|
1630
|
-
});
|
|
1631
|
-
});
|
|
1632
|
-
}
|
|
1633
1694
|
function startTunnelWorker(backend, apiKey, runtimeId, attachUrl) {
|
|
1634
1695
|
let stopped = false;
|
|
1635
1696
|
const base = attachUrl.replace(/\/$/, "");
|
package/package.json
CHANGED