@evident-ai/cli 0.2.1-dev.7768f72 → 0.2.1-dev.810d5f7
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/README.md +10 -3
- package/dist/index.js +929 -1178
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -12,41 +12,30 @@ import chalk2 from "chalk";
|
|
|
12
12
|
import Conf from "conf";
|
|
13
13
|
import { homedir } from "os";
|
|
14
14
|
import { join } from "path";
|
|
15
|
-
var
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
dev: {
|
|
21
|
-
apiUrl: "https://api.dev.evident.run/v1",
|
|
22
|
-
tunnelUrl: "wss://tunnel.dev.evident.run"
|
|
23
|
-
},
|
|
24
|
-
production: {
|
|
25
|
-
// Production URLs also have aliases: api.evident.run, tunnel.evident.run
|
|
26
|
-
apiUrl: "https://api.production.evident.run/v1",
|
|
27
|
-
tunnelUrl: "wss://tunnel.production.evident.run"
|
|
28
|
-
}
|
|
15
|
+
var PRODUCTION_API_URL = "https://api.production.evident.run/v1";
|
|
16
|
+
var PRODUCTION_TUNNEL_URL = "wss://tunnel.production.evident.run";
|
|
17
|
+
var defaults = {
|
|
18
|
+
apiUrl: PRODUCTION_API_URL,
|
|
19
|
+
tunnelUrl: PRODUCTION_TUNNEL_URL
|
|
29
20
|
};
|
|
30
|
-
var
|
|
31
|
-
var
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const envVar = process.env.EVIDENT_ENV;
|
|
37
|
-
if (envVar && environmentPresets[envVar]) {
|
|
38
|
-
return envVar;
|
|
21
|
+
var endpointOverride;
|
|
22
|
+
var tunnelOverride;
|
|
23
|
+
function setEndpoint(url) {
|
|
24
|
+
if (!url) {
|
|
25
|
+
endpointOverride = void 0;
|
|
26
|
+
return;
|
|
39
27
|
}
|
|
40
|
-
|
|
28
|
+
const trimmed = url.replace(/\/+$/, "");
|
|
29
|
+
endpointOverride = /\/v1$/.test(trimmed) ? trimmed : `${trimmed}/v1`;
|
|
41
30
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
31
|
+
function setTunnelUrl(url) {
|
|
32
|
+
tunnelOverride = url ? url.replace(/\/+$/, "") : void 0;
|
|
44
33
|
}
|
|
45
34
|
function getApiUrl() {
|
|
46
|
-
return process.env.EVIDENT_API_URL ??
|
|
35
|
+
return process.env.EVIDENT_API_URL ?? endpointOverride ?? defaults.apiUrl;
|
|
47
36
|
}
|
|
48
37
|
function getTunnelUrl() {
|
|
49
|
-
return process.env.EVIDENT_TUNNEL_URL ??
|
|
38
|
+
return process.env.EVIDENT_TUNNEL_URL ?? tunnelOverride ?? defaults.tunnelUrl;
|
|
50
39
|
}
|
|
51
40
|
var config = new Conf({
|
|
52
41
|
projectName: "evident",
|
|
@@ -444,9 +433,9 @@ async function whoami() {
|
|
|
444
433
|
}
|
|
445
434
|
|
|
446
435
|
// src/commands/run.ts
|
|
447
|
-
import
|
|
448
|
-
import
|
|
449
|
-
import { select as
|
|
436
|
+
import chalk6 from "chalk";
|
|
437
|
+
import ora3 from "ora";
|
|
438
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
450
439
|
|
|
451
440
|
// ../../packages/types/src/telemetry/index.ts
|
|
452
441
|
var TelemetryEventTypes = {
|
|
@@ -459,10 +448,7 @@ var TelemetryEventTypes = {
|
|
|
459
448
|
};
|
|
460
449
|
|
|
461
450
|
// ../../packages/types/src/tunnel/index.ts
|
|
462
|
-
var
|
|
463
|
-
var TUNNEL_CHUNK_SIZE = 768 * 1024;
|
|
464
|
-
var TUNNEL_MAX_RESPONSE_SIZE = 50 * 1024 * 1024;
|
|
465
|
-
var TUNNEL_CHUNK_TIMEOUT_MS = 30 * 1e3;
|
|
451
|
+
var MAX_FRAME_BYTES = 256 * 1024;
|
|
466
452
|
|
|
467
453
|
// src/lib/telemetry.ts
|
|
468
454
|
var CLI_VERSION = process.env.npm_package_version || "unknown";
|
|
@@ -574,33 +560,6 @@ function emitAgentDisconnected(agentId, metadata) {
|
|
|
574
560
|
agent_id: agentId
|
|
575
561
|
});
|
|
576
562
|
}
|
|
577
|
-
function emitAgentMessageProcessing(agentId, metadata) {
|
|
578
|
-
emitEvent({
|
|
579
|
-
event_type: TelemetryEventTypes.AGENT_MESSAGE_PROCESSING,
|
|
580
|
-
severity: "info",
|
|
581
|
-
message: `Processing message ${metadata.message_id.slice(0, 8)}...`,
|
|
582
|
-
metadata,
|
|
583
|
-
agent_id: agentId
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
function emitAgentMessageDone(agentId, metadata) {
|
|
587
|
-
emitEvent({
|
|
588
|
-
event_type: TelemetryEventTypes.AGENT_MESSAGE_DONE,
|
|
589
|
-
severity: "info",
|
|
590
|
-
message: `Message ${metadata.message_id.slice(0, 8)} processed`,
|
|
591
|
-
metadata,
|
|
592
|
-
agent_id: agentId
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
function emitAgentMessageFailed(agentId, metadata) {
|
|
596
|
-
emitEvent({
|
|
597
|
-
event_type: TelemetryEventTypes.AGENT_MESSAGE_FAILED,
|
|
598
|
-
severity: "error",
|
|
599
|
-
message: metadata.error ? `Message ${metadata.message_id.slice(0, 8)} failed: ${metadata.error}` : `Message ${metadata.message_id.slice(0, 8)} ${metadata.reason || "failed"}`,
|
|
600
|
-
metadata,
|
|
601
|
-
agent_id: agentId
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
563
|
var EventTypes = {
|
|
605
564
|
// Tunnel lifecycle
|
|
606
565
|
TUNNEL_STARTING: "tunnel.starting",
|
|
@@ -665,7 +624,7 @@ function isInteractive(jsonOutput) {
|
|
|
665
624
|
// src/lib/opencode/health.ts
|
|
666
625
|
async function checkOpenCodeHealth(port) {
|
|
667
626
|
try {
|
|
668
|
-
const response = await fetch(`http://
|
|
627
|
+
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
|
669
628
|
signal: AbortSignal.timeout(2e3)
|
|
670
629
|
// 2 second timeout
|
|
671
630
|
});
|
|
@@ -844,12 +803,12 @@ async function findHealthyOpenCodeInstances() {
|
|
|
844
803
|
}
|
|
845
804
|
async function startOpenCode(port) {
|
|
846
805
|
let command = "opencode";
|
|
847
|
-
let args = ["serve", "--port", port.toString()];
|
|
806
|
+
let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1"];
|
|
848
807
|
try {
|
|
849
808
|
execSync("which opencode", { stdio: "ignore" });
|
|
850
809
|
} catch {
|
|
851
810
|
command = "npx";
|
|
852
|
-
args = ["opencode", "serve", "--port", port.toString()];
|
|
811
|
+
args = ["opencode", "serve", "--port", port.toString(), "--hostname", "127.0.0.1"];
|
|
853
812
|
}
|
|
854
813
|
const child = spawn(command, args, {
|
|
855
814
|
detached: true,
|
|
@@ -1082,176 +1041,143 @@ async function sendMessageToOpenCode(port, sessionId, content, options, hooks, m
|
|
|
1082
1041
|
}
|
|
1083
1042
|
|
|
1084
1043
|
// src/lib/tunnel/connection.ts
|
|
1085
|
-
import
|
|
1044
|
+
import WebSocket2 from "ws";
|
|
1086
1045
|
|
|
1087
1046
|
// src/lib/tunnel/forwarding.ts
|
|
1088
1047
|
import WebSocket from "ws";
|
|
1089
|
-
var
|
|
1090
|
-
var
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1048
|
+
var LOOPBACK_HOST = "127.0.0.1";
|
|
1049
|
+
var STRIP_REQ = /* @__PURE__ */ new Set([
|
|
1050
|
+
"host",
|
|
1051
|
+
"connection",
|
|
1052
|
+
"keep-alive",
|
|
1053
|
+
"proxy-authorization",
|
|
1054
|
+
"transfer-encoding",
|
|
1055
|
+
"upgrade",
|
|
1056
|
+
"content-length"
|
|
1057
|
+
]);
|
|
1058
|
+
var STRIP_RES = /* @__PURE__ */ new Set([
|
|
1059
|
+
"connection",
|
|
1060
|
+
"keep-alive",
|
|
1061
|
+
"transfer-encoding",
|
|
1062
|
+
"content-encoding",
|
|
1063
|
+
"content-length"
|
|
1064
|
+
]);
|
|
1065
|
+
var StreamForwarder = class {
|
|
1066
|
+
constructor(ws, port, callbacks = {}) {
|
|
1067
|
+
this.ws = ws;
|
|
1068
|
+
this.port = port;
|
|
1069
|
+
this.callbacks = callbacks;
|
|
1070
|
+
}
|
|
1071
|
+
inflight = /* @__PURE__ */ new Map();
|
|
1072
|
+
/**
|
|
1073
|
+
* Handle an edge→agent frame. Unknown frame types are ignored.
|
|
1074
|
+
*/
|
|
1075
|
+
handleFrame(frame) {
|
|
1076
|
+
switch (frame.type) {
|
|
1077
|
+
case "open":
|
|
1078
|
+
this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);
|
|
1079
|
+
void this.handleOpen(frame);
|
|
1080
|
+
break;
|
|
1081
|
+
case "req_data":
|
|
1082
|
+
this.inflight.get(frame.sid)?.pushBody?.(Buffer.from(frame.b64, "base64"));
|
|
1083
|
+
break;
|
|
1084
|
+
case "req_end":
|
|
1085
|
+
this.inflight.get(frame.sid)?.endBody?.();
|
|
1086
|
+
break;
|
|
1087
|
+
case "abort":
|
|
1088
|
+
this.inflight.get(frame.sid)?.abort?.();
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Abort every in-flight stream (e.g. on WebSocket close).
|
|
1094
|
+
*/
|
|
1095
|
+
abortAll() {
|
|
1096
|
+
for (const stream of this.inflight.values()) {
|
|
1111
1097
|
try {
|
|
1112
|
-
|
|
1098
|
+
stream.abort();
|
|
1113
1099
|
} catch {
|
|
1114
|
-
body = text;
|
|
1115
1100
|
}
|
|
1116
|
-
} else {
|
|
1117
|
-
body = text;
|
|
1118
1101
|
}
|
|
1119
|
-
|
|
1120
|
-
status: response.status,
|
|
1121
|
-
body
|
|
1122
|
-
};
|
|
1123
|
-
} catch (error2) {
|
|
1124
|
-
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1125
|
-
return {
|
|
1126
|
-
status: 502,
|
|
1127
|
-
body: { error: "Failed to connect to OpenCode", message }
|
|
1128
|
-
};
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
function sendResponse(ws, requestId, response) {
|
|
1132
|
-
if (ws.readyState !== WebSocket.OPEN) {
|
|
1133
|
-
return;
|
|
1134
|
-
}
|
|
1135
|
-
const bodyStr = JSON.stringify(response.body ?? null);
|
|
1136
|
-
const bodyBytes = Buffer.from(bodyStr, "utf-8");
|
|
1137
|
-
if (bodyBytes.length < CHUNK_THRESHOLD) {
|
|
1138
|
-
ws.send(
|
|
1139
|
-
JSON.stringify({
|
|
1140
|
-
type: "response",
|
|
1141
|
-
id: requestId,
|
|
1142
|
-
payload: response
|
|
1143
|
-
})
|
|
1144
|
-
);
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
sendResponseAsChunks(ws, requestId, response, bodyBytes);
|
|
1148
|
-
}
|
|
1149
|
-
function sendResponseAsChunks(ws, requestId, response, bodyBytes) {
|
|
1150
|
-
if (ws.readyState !== WebSocket.OPEN) {
|
|
1151
|
-
return;
|
|
1102
|
+
this.inflight.clear();
|
|
1152
1103
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1104
|
+
send(frame) {
|
|
1105
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
1106
|
+
this.ws.send(JSON.stringify(frame));
|
|
1107
|
+
}
|
|
1156
1108
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1109
|
+
async handleOpen(frame) {
|
|
1110
|
+
const { sid, method, path, headers, has_body } = frame;
|
|
1111
|
+
const ac = new AbortController();
|
|
1112
|
+
let bodyPromise;
|
|
1113
|
+
let pushBody;
|
|
1114
|
+
let endBody;
|
|
1115
|
+
if (has_body) {
|
|
1116
|
+
const chunks = [];
|
|
1117
|
+
bodyPromise = new Promise((resolve) => {
|
|
1118
|
+
pushBody = (buf) => {
|
|
1119
|
+
chunks.push(buf);
|
|
1120
|
+
};
|
|
1121
|
+
endBody = () => {
|
|
1122
|
+
resolve(Buffer.concat(chunks));
|
|
1123
|
+
};
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
const fwdHeaders = {};
|
|
1127
|
+
for (const [k, v] of Object.entries(headers ?? {})) {
|
|
1128
|
+
if (!STRIP_REQ.has(k.toLowerCase())) fwdHeaders[k] = v;
|
|
1129
|
+
}
|
|
1130
|
+
this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
|
|
1131
|
+
const body = bodyPromise ? await bodyPromise : void 0;
|
|
1132
|
+
if (ac.signal.aborted) {
|
|
1133
|
+
this.inflight.delete(sid);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
let upstream;
|
|
1137
|
+
try {
|
|
1138
|
+
upstream = await fetch(`http://${LOOPBACK_HOST}:${this.port}${path}`, {
|
|
1139
|
+
method,
|
|
1140
|
+
headers: fwdHeaders,
|
|
1141
|
+
body,
|
|
1142
|
+
redirect: "manual",
|
|
1143
|
+
signal: ac.signal
|
|
1144
|
+
});
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
this.inflight.delete(sid);
|
|
1147
|
+
if (!ac.signal.aborted) {
|
|
1148
|
+
this.send({ type: "res_err", sid, message: `upstream fetch failed: ${String(err)}` });
|
|
1166
1149
|
}
|
|
1167
|
-
})
|
|
1168
|
-
);
|
|
1169
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
1170
|
-
if (ws.readyState !== WebSocket.OPEN) {
|
|
1171
1150
|
return;
|
|
1172
1151
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
id: requestId,
|
|
1177
|
-
chunk_index: i,
|
|
1178
|
-
data: chunks[i].toString("base64")
|
|
1179
|
-
})
|
|
1180
|
-
);
|
|
1181
|
-
}
|
|
1182
|
-
if (ws.readyState !== WebSocket.OPEN) {
|
|
1183
|
-
return;
|
|
1184
|
-
}
|
|
1185
|
-
ws.send(
|
|
1186
|
-
JSON.stringify({
|
|
1187
|
-
type: "response_end",
|
|
1188
|
-
id: requestId
|
|
1189
|
-
})
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
function splitIntoChunks(data, chunkSize) {
|
|
1193
|
-
const chunks = [];
|
|
1194
|
-
for (let i = 0; i < data.length; i += chunkSize) {
|
|
1195
|
-
chunks.push(data.subarray(i, i + chunkSize));
|
|
1196
|
-
}
|
|
1197
|
-
return chunks;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
// src/lib/tunnel/events.ts
|
|
1201
|
-
import WebSocket2 from "ws";
|
|
1202
|
-
async function subscribeToOpenCodeEvents(port, subscriptionId, ws, abortController) {
|
|
1203
|
-
const url = `http://localhost:${port}/event`;
|
|
1204
|
-
try {
|
|
1205
|
-
const response = await fetch(url, {
|
|
1206
|
-
headers: { Accept: "text/event-stream" },
|
|
1207
|
-
signal: abortController.signal
|
|
1152
|
+
const resHeaders = {};
|
|
1153
|
+
upstream.headers.forEach((value, key) => {
|
|
1154
|
+
if (!STRIP_RES.has(key.toLowerCase())) resHeaders[key] = value;
|
|
1208
1155
|
});
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
while (true) {
|
|
1222
|
-
const { done, value } = await reader.read();
|
|
1223
|
-
if (done) {
|
|
1224
|
-
if (ws.readyState === WebSocket2.OPEN) {
|
|
1225
|
-
ws.send(JSON.stringify({ type: "event_end", id: subscriptionId }));
|
|
1226
|
-
}
|
|
1227
|
-
break;
|
|
1228
|
-
}
|
|
1229
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1230
|
-
const lines = buffer.split("\n");
|
|
1231
|
-
buffer = lines.pop() || "";
|
|
1232
|
-
for (const line of lines) {
|
|
1233
|
-
if (line.startsWith("data: ")) {
|
|
1234
|
-
try {
|
|
1235
|
-
const event = JSON.parse(line.slice(6));
|
|
1236
|
-
if (ws.readyState === WebSocket2.OPEN) {
|
|
1237
|
-
ws.send(JSON.stringify({ type: "event", id: subscriptionId, event }));
|
|
1238
|
-
}
|
|
1239
|
-
} catch {
|
|
1156
|
+
this.send({ type: "head", sid, status: upstream.status, headers: resHeaders });
|
|
1157
|
+
this.callbacks.onHead?.(sid, upstream.status);
|
|
1158
|
+
try {
|
|
1159
|
+
if (upstream.body) {
|
|
1160
|
+
const reader = upstream.body.getReader();
|
|
1161
|
+
while (true) {
|
|
1162
|
+
const { done, value } = await reader.read();
|
|
1163
|
+
if (done) break;
|
|
1164
|
+
const chunk = Buffer.from(value);
|
|
1165
|
+
for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
|
|
1166
|
+
const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
|
|
1167
|
+
this.send({ type: "res_data", sid, b64: slice.toString("base64") });
|
|
1240
1168
|
}
|
|
1241
1169
|
}
|
|
1242
1170
|
}
|
|
1171
|
+
this.send({ type: "res_end", sid });
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
if (!ac.signal.aborted) {
|
|
1174
|
+
this.send({ type: "res_err", sid, message: String(err) });
|
|
1175
|
+
}
|
|
1176
|
+
} finally {
|
|
1177
|
+
this.inflight.delete(sid);
|
|
1243
1178
|
}
|
|
1244
|
-
} catch (error2) {
|
|
1245
|
-
if (abortController.signal.aborted) {
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1249
|
-
if (ws.readyState === WebSocket2.OPEN) {
|
|
1250
|
-
ws.send(JSON.stringify({ type: "event_error", id: subscriptionId, error: message }));
|
|
1251
|
-
}
|
|
1252
|
-
throw error2;
|
|
1253
1179
|
}
|
|
1254
|
-
}
|
|
1180
|
+
};
|
|
1255
1181
|
|
|
1256
1182
|
// src/lib/tunnel/connection.ts
|
|
1257
1183
|
var MAX_RECONNECT_DELAY = 3e4;
|
|
@@ -1261,6 +1187,33 @@ function getReconnectDelay(attempt) {
|
|
|
1261
1187
|
const jitter = Math.random() * 1e3;
|
|
1262
1188
|
return Math.min(exponentialDelay + jitter, MAX_RECONNECT_DELAY);
|
|
1263
1189
|
}
|
|
1190
|
+
function describeSocketError(error2, url) {
|
|
1191
|
+
const code = error2.code;
|
|
1192
|
+
switch (code) {
|
|
1193
|
+
case "ECONNREFUSED":
|
|
1194
|
+
return `connection refused at ${url} \u2014 is the tunnel relay running? (ECONNREFUSED)`;
|
|
1195
|
+
case "ENOTFOUND":
|
|
1196
|
+
return `host not found for ${url} \u2014 check the tunnel URL (ENOTFOUND)`;
|
|
1197
|
+
case "ETIMEDOUT":
|
|
1198
|
+
return `connection timed out to ${url} (ETIMEDOUT)`;
|
|
1199
|
+
case "ECONNRESET":
|
|
1200
|
+
return `connection reset by ${url} (ECONNRESET)`;
|
|
1201
|
+
default: {
|
|
1202
|
+
const base = error2.message?.trim();
|
|
1203
|
+
const suffix = code ? ` (${code})` : "";
|
|
1204
|
+
return `${base && base.length > 0 ? base : "socket error"}${suffix} connecting to ${url}`;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
var STREAM_FRAME_TYPES = /* @__PURE__ */ new Set([
|
|
1209
|
+
"open",
|
|
1210
|
+
"req_data",
|
|
1211
|
+
"req_end",
|
|
1212
|
+
"abort"
|
|
1213
|
+
]);
|
|
1214
|
+
function isStreamFrame(message) {
|
|
1215
|
+
return STREAM_FRAME_TYPES.has(message.type);
|
|
1216
|
+
}
|
|
1264
1217
|
function connectTunnel(options) {
|
|
1265
1218
|
const {
|
|
1266
1219
|
agentId,
|
|
@@ -1275,334 +1228,688 @@ function connectTunnel(options) {
|
|
|
1275
1228
|
} = options;
|
|
1276
1229
|
const tunnelUrl = getTunnelUrlConfig();
|
|
1277
1230
|
const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
|
|
1278
|
-
const activeEventSubscriptions = /* @__PURE__ */ new Map();
|
|
1279
1231
|
return new Promise((resolve, reject) => {
|
|
1280
|
-
const ws = new
|
|
1232
|
+
const ws = new WebSocket2(url, {
|
|
1281
1233
|
headers: {
|
|
1282
1234
|
Authorization: authHeader
|
|
1283
1235
|
}
|
|
1284
1236
|
});
|
|
1237
|
+
const streamStartTimes = /* @__PURE__ */ new Map();
|
|
1238
|
+
const forwarder = new StreamForwarder(ws, port, {
|
|
1239
|
+
onOpen: (sid, method, path) => {
|
|
1240
|
+
streamStartTimes.set(sid, Date.now());
|
|
1241
|
+
onRequest?.(method, path, sid);
|
|
1242
|
+
},
|
|
1243
|
+
onHead: (sid, status) => {
|
|
1244
|
+
const startedAt = streamStartTimes.get(sid);
|
|
1245
|
+
streamStartTimes.delete(sid);
|
|
1246
|
+
onResponse?.(status, startedAt ? Date.now() - startedAt : 0, sid);
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1285
1249
|
const connectionTimeout = setTimeout(() => {
|
|
1286
1250
|
ws.close();
|
|
1287
1251
|
reject(new Error("Connection timeout"));
|
|
1288
1252
|
}, 3e4);
|
|
1253
|
+
let upgradeRejection = null;
|
|
1254
|
+
ws.on("unexpected-response", (_req, res) => {
|
|
1255
|
+
clearTimeout(connectionTimeout);
|
|
1256
|
+
const chunks = [];
|
|
1257
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
1258
|
+
res.on("end", () => {
|
|
1259
|
+
const bodyRaw = Buffer.concat(chunks).toString("utf8").trim();
|
|
1260
|
+
let detail = bodyRaw;
|
|
1261
|
+
try {
|
|
1262
|
+
const parsed = JSON.parse(bodyRaw);
|
|
1263
|
+
detail = parsed.error ?? parsed.message ?? bodyRaw;
|
|
1264
|
+
if (parsed.details) detail += ` (${parsed.details})`;
|
|
1265
|
+
} catch {
|
|
1266
|
+
}
|
|
1267
|
+
const statusLine = `HTTP ${res.statusCode}${res.statusMessage ? ` ${res.statusMessage}` : ""}`;
|
|
1268
|
+
upgradeRejection = detail ? `${statusLine}: ${detail}` : statusLine;
|
|
1269
|
+
onError?.(`Tunnel refused by relay (${upgradeRejection})`);
|
|
1270
|
+
reject(new Error(`Tunnel handshake rejected: ${upgradeRejection}`));
|
|
1271
|
+
});
|
|
1272
|
+
});
|
|
1289
1273
|
ws.on("open", () => {
|
|
1290
1274
|
onInfo?.("WebSocket connection established");
|
|
1291
1275
|
});
|
|
1292
|
-
ws.on("message",
|
|
1276
|
+
ws.on("message", (data) => {
|
|
1277
|
+
let message;
|
|
1293
1278
|
try {
|
|
1294
|
-
|
|
1295
|
-
switch (message.type) {
|
|
1296
|
-
case "connected": {
|
|
1297
|
-
clearTimeout(connectionTimeout);
|
|
1298
|
-
const connectedAgentId = message.agent_id ?? agentId;
|
|
1299
|
-
onConnected?.(connectedAgentId);
|
|
1300
|
-
resolve({
|
|
1301
|
-
ws,
|
|
1302
|
-
close: () => ws.close(1e3, "CLI shutdown")
|
|
1303
|
-
});
|
|
1304
|
-
break;
|
|
1305
|
-
}
|
|
1306
|
-
case "error":
|
|
1307
|
-
clearTimeout(connectionTimeout);
|
|
1308
|
-
onError?.(message.message || "Unknown tunnel error");
|
|
1309
|
-
if (message.code === "unauthorized") {
|
|
1310
|
-
ws.close();
|
|
1311
|
-
reject(new Error("Unauthorized"));
|
|
1312
|
-
}
|
|
1313
|
-
break;
|
|
1314
|
-
case "ping":
|
|
1315
|
-
ws.send(JSON.stringify({ type: "pong" }));
|
|
1316
|
-
break;
|
|
1317
|
-
case "request":
|
|
1318
|
-
if (message.id && message.payload) {
|
|
1319
|
-
const startTime = Date.now();
|
|
1320
|
-
onRequest?.(message.payload.method, message.payload.path, message.id);
|
|
1321
|
-
const response = await forwardToOpenCode(port, message.payload);
|
|
1322
|
-
const durationMs = Date.now() - startTime;
|
|
1323
|
-
onResponse?.(response.status, durationMs, message.id);
|
|
1324
|
-
sendResponse(ws, message.id, response);
|
|
1325
|
-
}
|
|
1326
|
-
break;
|
|
1327
|
-
case "subscribe_events":
|
|
1328
|
-
if (message.id) {
|
|
1329
|
-
const abortController = new AbortController();
|
|
1330
|
-
activeEventSubscriptions.set(message.id, abortController);
|
|
1331
|
-
onInfo?.(`Starting event subscription ${message.id.slice(0, 8)}`);
|
|
1332
|
-
subscribeToOpenCodeEvents(port, message.id, ws, abortController).catch((error2) => {
|
|
1333
|
-
if (!abortController.signal.aborted) {
|
|
1334
|
-
onError?.(`Event subscription failed: ${error2.message}`);
|
|
1335
|
-
}
|
|
1336
|
-
}).finally(() => {
|
|
1337
|
-
activeEventSubscriptions.delete(message.id);
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1340
|
-
break;
|
|
1341
|
-
case "unsubscribe_events":
|
|
1342
|
-
if (message.id) {
|
|
1343
|
-
const controller = activeEventSubscriptions.get(message.id);
|
|
1344
|
-
if (controller) {
|
|
1345
|
-
controller.abort();
|
|
1346
|
-
activeEventSubscriptions.delete(message.id);
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
break;
|
|
1350
|
-
}
|
|
1279
|
+
message = JSON.parse(data.toString());
|
|
1351
1280
|
} catch (error2) {
|
|
1352
1281
|
const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1353
1282
|
onError?.(`Failed to handle message: ${errorMessage}`);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
if (isStreamFrame(message)) {
|
|
1286
|
+
forwarder.handleFrame(message);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
switch (message.type) {
|
|
1290
|
+
case "connected": {
|
|
1291
|
+
clearTimeout(connectionTimeout);
|
|
1292
|
+
const connectedAgentId = message.agent_id ?? agentId;
|
|
1293
|
+
onConnected?.(connectedAgentId);
|
|
1294
|
+
resolve({
|
|
1295
|
+
ws,
|
|
1296
|
+
close: () => ws.close(1e3, "CLI shutdown")
|
|
1297
|
+
});
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1300
|
+
case "error":
|
|
1301
|
+
clearTimeout(connectionTimeout);
|
|
1302
|
+
onError?.(message.message || "Unknown tunnel error");
|
|
1303
|
+
if (message.code === "unauthorized") {
|
|
1304
|
+
ws.close();
|
|
1305
|
+
reject(new Error("Unauthorized"));
|
|
1306
|
+
}
|
|
1307
|
+
break;
|
|
1308
|
+
case "ping":
|
|
1309
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
1310
|
+
break;
|
|
1354
1311
|
}
|
|
1355
1312
|
});
|
|
1356
1313
|
ws.on("error", (error2) => {
|
|
1357
1314
|
clearTimeout(connectionTimeout);
|
|
1358
|
-
|
|
1359
|
-
|
|
1315
|
+
const detail = upgradeRejection ?? describeSocketError(error2, url);
|
|
1316
|
+
onError?.(`Connection error: ${detail}`);
|
|
1317
|
+
reject(upgradeRejection ? new Error(upgradeRejection) : new Error(detail));
|
|
1360
1318
|
});
|
|
1361
1319
|
ws.on("close", (code, reason) => {
|
|
1362
|
-
const reasonStr = reason.toString() || "No reason provided";
|
|
1320
|
+
const reasonStr = reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
|
|
1321
|
+
forwarder.abortAll();
|
|
1322
|
+
streamStartTimes.clear();
|
|
1363
1323
|
onDisconnected?.(code, reasonStr);
|
|
1364
|
-
for (const [, controller] of activeEventSubscriptions) {
|
|
1365
|
-
controller.abort();
|
|
1366
|
-
}
|
|
1367
|
-
activeEventSubscriptions.clear();
|
|
1368
1324
|
});
|
|
1369
1325
|
});
|
|
1370
1326
|
}
|
|
1371
1327
|
|
|
1372
|
-
// src/
|
|
1373
|
-
var
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1328
|
+
// src/lib/tunnel/runner-connection.ts
|
|
1329
|
+
var RunnerConnection = class {
|
|
1330
|
+
opts;
|
|
1331
|
+
sleep;
|
|
1332
|
+
connection = null;
|
|
1333
|
+
resolvedAgentId;
|
|
1334
|
+
/** True while a (re)connect loop is in flight. */
|
|
1335
|
+
reconnecting = false;
|
|
1336
|
+
/** The in-flight reconnect promise, awaitable by the caller. */
|
|
1337
|
+
reconnectPromise = null;
|
|
1338
|
+
/** 1-based count of the current reconnect attempt streak. */
|
|
1339
|
+
reconnectAttempt = 0;
|
|
1340
|
+
constructor(opts) {
|
|
1341
|
+
this.opts = opts;
|
|
1342
|
+
this.resolvedAgentId = opts.agentId;
|
|
1343
|
+
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1344
|
+
}
|
|
1345
|
+
get agentId() {
|
|
1346
|
+
return this.resolvedAgentId;
|
|
1347
|
+
}
|
|
1348
|
+
/** Establish the initial tunnel connection (with retry/backoff). */
|
|
1349
|
+
async connect() {
|
|
1350
|
+
await this.connectWithRetry(false);
|
|
1351
|
+
}
|
|
1352
|
+
/** Close the active connection (idempotent). */
|
|
1353
|
+
close() {
|
|
1354
|
+
if (this.connection) {
|
|
1355
|
+
try {
|
|
1356
|
+
this.connection.close();
|
|
1357
|
+
} catch {
|
|
1358
|
+
}
|
|
1359
|
+
this.connection = null;
|
|
1389
1360
|
}
|
|
1390
|
-
return {
|
|
1391
|
-
error: "Cannot resolve agent ID: auth type is not agent_key. Please provide --agent explicitly."
|
|
1392
|
-
};
|
|
1393
|
-
} catch (error2) {
|
|
1394
|
-
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1395
|
-
return { error: `Failed to resolve agent from key: ${message}` };
|
|
1396
1361
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
const
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1362
|
+
async connectWithRetry(isReconnect) {
|
|
1363
|
+
if (isReconnect && this.reconnecting) return;
|
|
1364
|
+
this.reconnecting = true;
|
|
1365
|
+
this.close();
|
|
1366
|
+
const { events } = this.opts;
|
|
1367
|
+
while (this.opts.isRunning()) {
|
|
1368
|
+
try {
|
|
1369
|
+
this.connection = await connectTunnel({
|
|
1370
|
+
agentId: this.resolvedAgentId,
|
|
1371
|
+
authHeader: this.opts.getAuthHeader(),
|
|
1372
|
+
port: this.opts.port,
|
|
1373
|
+
onConnected: (agentId) => {
|
|
1374
|
+
this.reconnectAttempt = 0;
|
|
1375
|
+
this.reconnecting = false;
|
|
1376
|
+
this.resolvedAgentId = agentId;
|
|
1377
|
+
events.onConnected(agentId, isReconnect);
|
|
1378
|
+
},
|
|
1379
|
+
onDisconnected: (code, reason) => {
|
|
1380
|
+
events.onDisconnected(code, reason);
|
|
1381
|
+
if (this.opts.isRunning() && code !== 1e3 && !this.reconnecting) {
|
|
1382
|
+
this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
|
|
1383
|
+
events.onError?.(`Reconnection failed: ${err.message}`);
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
onError: (error2) => events.onError?.(error2),
|
|
1388
|
+
onResponse: () => events.onResponse?.(),
|
|
1389
|
+
onInfo: (message) => events.onInfo?.(message)
|
|
1390
|
+
});
|
|
1391
|
+
return;
|
|
1392
|
+
} catch (error2) {
|
|
1393
|
+
this.reconnectAttempt++;
|
|
1394
|
+
if (error2.message === "Unauthorized") {
|
|
1395
|
+
this.reconnecting = false;
|
|
1396
|
+
throw error2;
|
|
1397
|
+
}
|
|
1398
|
+
const delay = getReconnectDelay(this.reconnectAttempt);
|
|
1399
|
+
events.onReconnecting?.(this.reconnectAttempt);
|
|
1400
|
+
events.onError?.(`Connection failed, retrying in ${Math.round(delay / 1e3)}s...`);
|
|
1401
|
+
await this.sleep(delay);
|
|
1402
|
+
}
|
|
1419
1403
|
}
|
|
1420
|
-
|
|
1421
|
-
} catch (error2) {
|
|
1422
|
-
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1423
|
-
return { valid: false, error: `Failed to validate agent: ${message}` };
|
|
1404
|
+
this.reconnecting = false;
|
|
1424
1405
|
}
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
// src/lib/channels/driver.ts
|
|
1409
|
+
var DEFAULT_RETRY_POLICY = {
|
|
1410
|
+
maxAttempts: 6,
|
|
1411
|
+
baseDelayMs: 500,
|
|
1412
|
+
maxDelayMs: 3e4
|
|
1413
|
+
};
|
|
1414
|
+
var ChannelAuthError = class extends Error {
|
|
1427
1415
|
constructor(message) {
|
|
1428
1416
|
super(message);
|
|
1429
|
-
this.name = "
|
|
1417
|
+
this.name = "ChannelAuthError";
|
|
1430
1418
|
}
|
|
1431
1419
|
};
|
|
1432
|
-
function
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1420
|
+
function backoffDelay(attempt, policy) {
|
|
1421
|
+
const exp = policy.baseDelayMs * Math.pow(2, attempt);
|
|
1422
|
+
const capped = Math.min(policy.maxDelayMs, exp);
|
|
1423
|
+
return Math.floor(Math.random() * capped);
|
|
1424
|
+
}
|
|
1425
|
+
function isRetryableStatus(status) {
|
|
1426
|
+
return status === 429 || status >= 500 && status <= 599;
|
|
1427
|
+
}
|
|
1428
|
+
var ChannelDriver = class {
|
|
1429
|
+
agentId;
|
|
1430
|
+
port;
|
|
1431
|
+
apiUrl;
|
|
1432
|
+
getAuthHeader;
|
|
1433
|
+
conversationFilter;
|
|
1434
|
+
retry;
|
|
1435
|
+
log;
|
|
1436
|
+
fetchImpl;
|
|
1437
|
+
sleep;
|
|
1438
|
+
/** Cache of conversationId → opencode sessionId. */
|
|
1439
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1440
|
+
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
1441
|
+
draining = false;
|
|
1442
|
+
constructor(config2) {
|
|
1443
|
+
this.agentId = config2.agentId;
|
|
1444
|
+
this.port = config2.port;
|
|
1445
|
+
this.apiUrl = config2.apiUrl.replace(/\/$/, "");
|
|
1446
|
+
this.getAuthHeader = config2.getAuthHeader;
|
|
1447
|
+
this.conversationFilter = config2.conversationFilter ?? null;
|
|
1448
|
+
this.retry = { ...DEFAULT_RETRY_POLICY, ...config2.retry };
|
|
1449
|
+
this.log = config2.log ?? (() => {
|
|
1450
|
+
});
|
|
1451
|
+
this.fetchImpl = config2.fetchImpl ?? fetch;
|
|
1452
|
+
this.sleep = config2.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1452
1453
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
const apiUrl = getApiUrlConfig();
|
|
1457
|
-
const response = await fetch(
|
|
1458
|
-
`${apiUrl}/agents/${agentId}/threads/${conversationId}/messages?status=pending`,
|
|
1459
|
-
{ headers: { Authorization: authHeader } }
|
|
1460
|
-
);
|
|
1461
|
-
checkAuthResponse(response, "fetching pending messages");
|
|
1462
|
-
if (!response.ok) {
|
|
1463
|
-
throw new Error(`Failed to get messages: HTTP ${response.status}`);
|
|
1454
|
+
/** The IPv4-loopback base URL for the local `opencode serve`. */
|
|
1455
|
+
get opencodeBase() {
|
|
1456
|
+
return `http://127.0.0.1:${this.port}`;
|
|
1464
1457
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
{
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1458
|
+
// -------------------------------------------------------------------------
|
|
1459
|
+
// Public API
|
|
1460
|
+
// -------------------------------------------------------------------------
|
|
1461
|
+
/**
|
|
1462
|
+
* Drain all pending channel conversations once: poll → process → callback.
|
|
1463
|
+
* Called on tunnel `connected` (WI-CHAN-4) and on each poll tick by `run.ts`.
|
|
1464
|
+
* Re-entrant calls while a drain is in flight are skipped (return 0).
|
|
1465
|
+
*
|
|
1466
|
+
* @returns the number of messages processed.
|
|
1467
|
+
*/
|
|
1468
|
+
async drainPending() {
|
|
1469
|
+
if (this.draining) return 0;
|
|
1470
|
+
this.draining = true;
|
|
1471
|
+
let processed = 0;
|
|
1472
|
+
try {
|
|
1473
|
+
const conversations = await this.getPendingConversations();
|
|
1474
|
+
for (const conv of conversations) {
|
|
1475
|
+
processed += await this.processConversation(conv);
|
|
1476
|
+
}
|
|
1477
|
+
} finally {
|
|
1478
|
+
this.draining = false;
|
|
1479
|
+
}
|
|
1480
|
+
return processed;
|
|
1481
|
+
}
|
|
1482
|
+
// -------------------------------------------------------------------------
|
|
1483
|
+
// Conversation processing
|
|
1484
|
+
// -------------------------------------------------------------------------
|
|
1485
|
+
async processConversation(conv) {
|
|
1486
|
+
const sessionId = await this.ensureSession(conv);
|
|
1487
|
+
const messages = await this.getPendingMessages(conv.id);
|
|
1488
|
+
let processed = 0;
|
|
1489
|
+
for (const message of messages) {
|
|
1490
|
+
const claimed = await this.markProcessing(conv.id, message.id);
|
|
1491
|
+
if (!claimed) {
|
|
1492
|
+
this.log({
|
|
1493
|
+
level: "info",
|
|
1494
|
+
message: `Message ${message.id.slice(0, 8)} already claimed \u2014 skipping`,
|
|
1495
|
+
conversation_id: conv.id,
|
|
1496
|
+
message_id: message.id
|
|
1497
|
+
});
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
try {
|
|
1501
|
+
await sendMessageToOpenCode(
|
|
1502
|
+
this.port,
|
|
1503
|
+
sessionId,
|
|
1504
|
+
message.content,
|
|
1505
|
+
{
|
|
1506
|
+
agent: message.opencode_agent ?? void 0,
|
|
1507
|
+
model: message.opencode_model ?? void 0
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
onQuestion: (question) => this.reportInteraction(conv.id, "question", question),
|
|
1511
|
+
onPermission: (permission) => this.reportInteraction(conv.id, "permission", permission)
|
|
1512
|
+
}
|
|
1513
|
+
);
|
|
1514
|
+
await this.confirmCompletion(sessionId);
|
|
1515
|
+
await this.markDone(conv.id, message.id, sessionId);
|
|
1516
|
+
processed += 1;
|
|
1517
|
+
this.log({
|
|
1518
|
+
level: "info",
|
|
1519
|
+
message: `Message ${message.id.slice(0, 8)} processed`,
|
|
1520
|
+
conversation_id: conv.id,
|
|
1521
|
+
message_id: message.id
|
|
1522
|
+
});
|
|
1523
|
+
} catch (err) {
|
|
1524
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
1525
|
+
await this.markFailed(conv.id, message.id).catch(() => {
|
|
1526
|
+
});
|
|
1527
|
+
this.log({
|
|
1528
|
+
level: "error",
|
|
1529
|
+
message: `Message ${message.id.slice(0, 8)} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1530
|
+
conversation_id: conv.id,
|
|
1531
|
+
message_id: message.id
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1488
1534
|
}
|
|
1489
|
-
|
|
1490
|
-
checkAuthResponse(response, "reporting interactive event");
|
|
1491
|
-
if (!response.ok) {
|
|
1492
|
-
throw new Error(`Failed to report interactive event: HTTP ${response.status}`);
|
|
1535
|
+
return processed;
|
|
1493
1536
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
}
|
|
1501
|
-
const response = await fetch(
|
|
1502
|
-
`${apiUrl}/agents/${agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
1503
|
-
{
|
|
1504
|
-
method: "PATCH",
|
|
1505
|
-
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
1506
|
-
body: JSON.stringify(body)
|
|
1507
|
-
}
|
|
1508
|
-
);
|
|
1509
|
-
checkAuthResponse(response, "marking message as done");
|
|
1510
|
-
}
|
|
1511
|
-
async function markMessageFailed(agentId, conversationId, messageId, authHeader) {
|
|
1512
|
-
const apiUrl = getApiUrlConfig();
|
|
1513
|
-
const response = await fetch(
|
|
1514
|
-
`${apiUrl}/agents/${agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
1515
|
-
{
|
|
1516
|
-
method: "PATCH",
|
|
1517
|
-
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
1518
|
-
body: JSON.stringify({ status: "failed" })
|
|
1537
|
+
async ensureSession(conv) {
|
|
1538
|
+
const cached = this.sessions.get(conv.id);
|
|
1539
|
+
if (cached) return cached;
|
|
1540
|
+
if (conv.opencode_session_id) {
|
|
1541
|
+
this.sessions.set(conv.id, conv.opencode_session_id);
|
|
1542
|
+
return conv.opencode_session_id;
|
|
1519
1543
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
async function acquireConversationLock(agentId, conversationId, correlationId, authHeader) {
|
|
1524
|
-
const apiUrl = getApiUrlConfig();
|
|
1525
|
-
try {
|
|
1526
|
-
const response = await fetch(`${apiUrl}/agents/${agentId}/threads/${conversationId}/lock`, {
|
|
1527
|
-
method: "POST",
|
|
1528
|
-
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
1529
|
-
body: JSON.stringify({ correlation_id: correlationId })
|
|
1544
|
+
const sessionId = await createOpenCodeSession(this.port);
|
|
1545
|
+
this.sessions.set(conv.id, sessionId);
|
|
1546
|
+
await this.persistSession(conv.id, sessionId).catch(() => {
|
|
1530
1547
|
});
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1548
|
+
return sessionId;
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Local reconcile: re-query `GET /session/:id` and check `time.completed`.
|
|
1552
|
+
* Best-effort — if opencode is unreachable or the field is absent we proceed
|
|
1553
|
+
* to mark done anyway (the blocking call already returned).
|
|
1554
|
+
*/
|
|
1555
|
+
async confirmCompletion(sessionId) {
|
|
1556
|
+
try {
|
|
1557
|
+
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
|
|
1558
|
+
if (!res.ok) return;
|
|
1559
|
+
const session = await res.json();
|
|
1560
|
+
if (session.time && session.time.completed == null) {
|
|
1561
|
+
this.log({
|
|
1562
|
+
level: "info",
|
|
1563
|
+
message: `Session ${sessionId.slice(0, 8)} not marked completed on reconcile \u2014 delivering anyway`
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
} catch {
|
|
1542
1567
|
}
|
|
1543
|
-
return { acquired: true };
|
|
1544
|
-
} catch (error2) {
|
|
1545
|
-
if (error2 instanceof AuthenticationError) throw error2;
|
|
1546
|
-
return { acquired: false, error: String(error2) };
|
|
1547
1568
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
const
|
|
1553
|
-
`${apiUrl}/agents/${agentId}/
|
|
1569
|
+
// -------------------------------------------------------------------------
|
|
1570
|
+
// Evident API calls (combinedAuth thread routes)
|
|
1571
|
+
// -------------------------------------------------------------------------
|
|
1572
|
+
async getPendingConversations() {
|
|
1573
|
+
const res = await this.fetchImpl(
|
|
1574
|
+
`${this.apiUrl}/agents/${this.agentId}/conversations/pending`,
|
|
1554
1575
|
{
|
|
1555
|
-
|
|
1556
|
-
headers: { Authorization: authHeader, "Content-Type": "application/json" },
|
|
1557
|
-
body: JSON.stringify({ correlation_id: correlationId })
|
|
1576
|
+
headers: { Authorization: this.getAuthHeader() }
|
|
1558
1577
|
}
|
|
1559
1578
|
);
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1579
|
+
this.assertAuth(res, "fetching pending conversations");
|
|
1580
|
+
if (!res.ok) {
|
|
1581
|
+
throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);
|
|
1582
|
+
}
|
|
1583
|
+
const data = await res.json();
|
|
1584
|
+
let conversations = data.conversations;
|
|
1585
|
+
if (this.conversationFilter) {
|
|
1586
|
+
conversations = conversations.filter((c) => c.id === this.conversationFilter);
|
|
1587
|
+
}
|
|
1588
|
+
return conversations;
|
|
1563
1589
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1590
|
+
async getPendingMessages(conversationId) {
|
|
1591
|
+
const res = await this.fetchImpl(
|
|
1592
|
+
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages?status=pending`,
|
|
1593
|
+
{ headers: { Authorization: this.getAuthHeader() } }
|
|
1594
|
+
);
|
|
1595
|
+
this.assertAuth(res, "fetching pending messages");
|
|
1596
|
+
if (!res.ok) {
|
|
1597
|
+
throw new Error(`Failed to get messages: HTTP ${res.status}`);
|
|
1598
|
+
}
|
|
1599
|
+
return await res.json();
|
|
1600
|
+
}
|
|
1601
|
+
async markProcessing(conversationId, messageId) {
|
|
1602
|
+
const res = await this.fetchImpl(
|
|
1603
|
+
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
1570
1604
|
{
|
|
1571
|
-
method: "
|
|
1572
|
-
headers: { Authorization:
|
|
1605
|
+
method: "PATCH",
|
|
1606
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
1607
|
+
body: JSON.stringify({ status: "processing" })
|
|
1573
1608
|
}
|
|
1574
1609
|
);
|
|
1575
|
-
|
|
1576
|
-
|
|
1610
|
+
this.assertAuth(res, "marking message as processing");
|
|
1611
|
+
return res.ok;
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* EXISTING combinedAuth completion route — idempotent + retried (WI-CHAN-2).
|
|
1615
|
+
* `PATCH .../messages/:id {status:'done', opencode_session_id}`. The server's
|
|
1616
|
+
* `queued_conversation_messages.status`/`processed_at` gate makes a re-call
|
|
1617
|
+
* for an already-`done` message a no-op (no double Slack post).
|
|
1618
|
+
*/
|
|
1619
|
+
async markDone(conversationId, messageId, sessionId) {
|
|
1620
|
+
await this.callWithRetry(
|
|
1621
|
+
"marking message as done",
|
|
1622
|
+
() => this.fetchImpl(
|
|
1623
|
+
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
1624
|
+
{
|
|
1625
|
+
method: "PATCH",
|
|
1626
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
1627
|
+
body: JSON.stringify({ status: "done", opencode_session_id: sessionId })
|
|
1628
|
+
}
|
|
1629
|
+
)
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
async markFailed(conversationId, messageId) {
|
|
1633
|
+
await this.callWithRetry(
|
|
1634
|
+
"marking message as failed",
|
|
1635
|
+
() => this.fetchImpl(
|
|
1636
|
+
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
1637
|
+
{
|
|
1638
|
+
method: "PATCH",
|
|
1639
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
1640
|
+
body: JSON.stringify({ status: "failed" })
|
|
1641
|
+
}
|
|
1642
|
+
)
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
async persistSession(conversationId, sessionId) {
|
|
1646
|
+
const res = await this.fetchImpl(
|
|
1647
|
+
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}`,
|
|
1648
|
+
{
|
|
1649
|
+
method: "PATCH",
|
|
1650
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
1651
|
+
body: JSON.stringify({ opencode_session_id: sessionId })
|
|
1652
|
+
}
|
|
1653
|
+
);
|
|
1654
|
+
this.assertAuth(res, "persisting session id");
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* EXISTING combinedAuth interaction route (WI-CHAN-3) — idempotent + retried.
|
|
1658
|
+
* `POST .../interactive-event {type, data}`. The server persists the
|
|
1659
|
+
* interaction and posts a link to the proxied opencode-web conversation.
|
|
1660
|
+
*/
|
|
1661
|
+
async reportInteraction(conversationId, type, data) {
|
|
1662
|
+
try {
|
|
1663
|
+
await this.callWithRetry(
|
|
1664
|
+
"reporting interactive event",
|
|
1665
|
+
() => this.fetchImpl(
|
|
1666
|
+
`${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/interactive-event`,
|
|
1667
|
+
{
|
|
1668
|
+
method: "POST",
|
|
1669
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
1670
|
+
body: JSON.stringify({ type, data })
|
|
1671
|
+
}
|
|
1672
|
+
)
|
|
1673
|
+
);
|
|
1674
|
+
this.log({
|
|
1675
|
+
level: "info",
|
|
1676
|
+
message: `${type} surfaced to channel (id: ${data.id.slice(0, 8)})`,
|
|
1677
|
+
conversation_id: conversationId
|
|
1678
|
+
});
|
|
1679
|
+
} catch (err) {
|
|
1680
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
1681
|
+
this.log({
|
|
1682
|
+
level: "error",
|
|
1683
|
+
message: `Failed to surface ${type}: ${err instanceof Error ? err.message : String(err)}`,
|
|
1684
|
+
conversation_id: conversationId
|
|
1685
|
+
});
|
|
1577
1686
|
}
|
|
1578
|
-
} catch (error2) {
|
|
1579
|
-
console.error(`[lock] Failed to release lock on ${conversationId}: ${error2}`);
|
|
1580
1687
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1688
|
+
// -------------------------------------------------------------------------
|
|
1689
|
+
// Retry wrapper
|
|
1690
|
+
// -------------------------------------------------------------------------
|
|
1691
|
+
/**
|
|
1692
|
+
* Invoke an Evident API call, retrying on transient failures (5xx / 429 /
|
|
1693
|
+
* network errors) with exponential backoff + jitter (capped). Auth failures
|
|
1694
|
+
* (401/403) are terminal and surface as `ChannelAuthError`; other 4xx are
|
|
1695
|
+
* terminal too. No on-disk persistence — a crash mid-retry drops the callback
|
|
1696
|
+
* (accepted by ADR-0039).
|
|
1697
|
+
*/
|
|
1698
|
+
async callWithRetry(context, call) {
|
|
1699
|
+
let lastError;
|
|
1700
|
+
for (let attempt = 0; attempt < this.retry.maxAttempts; attempt += 1) {
|
|
1701
|
+
let res;
|
|
1702
|
+
try {
|
|
1703
|
+
res = await call();
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
lastError = err;
|
|
1706
|
+
if (attempt < this.retry.maxAttempts - 1) {
|
|
1707
|
+
await this.sleep(backoffDelay(attempt, this.retry));
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
throw err;
|
|
1711
|
+
}
|
|
1712
|
+
if (res.status === 401 || res.status === 403) {
|
|
1713
|
+
throw new ChannelAuthError(
|
|
1714
|
+
`Authentication failed during ${context}: HTTP ${res.status}. Your session may have expired.`
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
if (res.ok) return;
|
|
1718
|
+
if (isRetryableStatus(res.status)) {
|
|
1719
|
+
lastError = new Error(`${context}: HTTP ${res.status}`);
|
|
1720
|
+
if (attempt < this.retry.maxAttempts - 1) {
|
|
1721
|
+
await this.sleep(backoffDelay(attempt, this.retry));
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
throw new Error(`${context}: HTTP ${res.status}`);
|
|
1726
|
+
}
|
|
1727
|
+
throw lastError instanceof Error ? lastError : new Error(`${context}: exhausted retries`);
|
|
1728
|
+
}
|
|
1729
|
+
assertAuth(res, context) {
|
|
1730
|
+
if (res.status === 401 || res.status === 403) {
|
|
1731
|
+
throw new ChannelAuthError(
|
|
1732
|
+
`Authentication failed during ${context}: HTTP ${res.status}. Your session may have expired.`
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
|
|
1738
|
+
// src/commands/ensure-opencode.ts
|
|
1739
|
+
import chalk5 from "chalk";
|
|
1740
|
+
import ora2 from "ora";
|
|
1741
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
1742
|
+
async function ensureOpenCodeRunning(ctx) {
|
|
1743
|
+
const healthCheck = await checkOpenCodeHealth(ctx.port);
|
|
1744
|
+
if (healthCheck.healthy) {
|
|
1745
|
+
return { port: ctx.port, process: null, version: healthCheck.version ?? null };
|
|
1746
|
+
}
|
|
1747
|
+
const runningInstances = await findHealthyOpenCodeInstances();
|
|
1748
|
+
if (runningInstances.length > 0) {
|
|
1749
|
+
if (!ctx.interactive) {
|
|
1750
|
+
throw new Error(
|
|
1751
|
+
`OpenCode not found on port ${ctx.port}, but running on port ${runningInstances[0].port}. Use --port ${runningInstances[0].port}`
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
blank();
|
|
1755
|
+
console.log(chalk5.yellow("Found OpenCode running on different port(s):"));
|
|
1756
|
+
for (const instance of runningInstances) {
|
|
1757
|
+
const ver = instance.version ? ` (v${instance.version})` : "";
|
|
1758
|
+
const cwd = instance.cwd ? ` in ${instance.cwd}` : "";
|
|
1759
|
+
console.log(chalk5.dim(` * Port ${instance.port}${ver}${cwd}`));
|
|
1760
|
+
}
|
|
1761
|
+
blank();
|
|
1762
|
+
if (runningInstances.length === 1) {
|
|
1763
|
+
console.log(chalk5.yellow("Tip: Run with the correct port:"));
|
|
1764
|
+
console.log(
|
|
1765
|
+
chalk5.dim(
|
|
1766
|
+
` ${getCliName()} run --agent ${ctx.agentId} --port ${runningInstances[0].port}`
|
|
1767
|
+
)
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
blank();
|
|
1771
|
+
throw new Error(`OpenCode not running on port ${ctx.port}`);
|
|
1772
|
+
}
|
|
1773
|
+
if (!isOpenCodeInstalled()) {
|
|
1774
|
+
if (!ctx.interactive) {
|
|
1775
|
+
throw new Error("OpenCode is not installed. Install it with: npm install -g opencode-ai");
|
|
1776
|
+
}
|
|
1777
|
+
const result = await promptOpenCodeInstall(true);
|
|
1778
|
+
if (result === "exit") process.exit(0);
|
|
1779
|
+
if (result !== "installed" && !isOpenCodeInstalled()) {
|
|
1780
|
+
throw new Error("OpenCode is not installed");
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
if (!ctx.interactive) {
|
|
1784
|
+
ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
|
|
1785
|
+
const proc = await startOpenCode(ctx.port);
|
|
1786
|
+
const health = await waitForOpenCodeHealth(ctx.port, 3e4);
|
|
1787
|
+
if (!health.healthy) {
|
|
1788
|
+
throw new Error(
|
|
1789
|
+
`OpenCode failed to start on port ${ctx.port}. Install with: npm install -g opencode-ai`
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
ctx.log(`OpenCode started on port ${ctx.port}${health.version ? ` (v${health.version})` : ""}`);
|
|
1793
|
+
return { port: ctx.port, process: proc, version: health.version ?? null };
|
|
1794
|
+
}
|
|
1795
|
+
let port = ctx.port;
|
|
1796
|
+
if (isPortInUse(port)) {
|
|
1797
|
+
console.log(chalk5.yellow(`
|
|
1798
|
+
Port ${port} is already in use.`));
|
|
1799
|
+
const alternativePort = findAvailablePort(port + 1);
|
|
1800
|
+
if (alternativePort) {
|
|
1801
|
+
const useAlternative = await select2({
|
|
1802
|
+
message: `Use port ${alternativePort} instead?`,
|
|
1803
|
+
choices: [
|
|
1804
|
+
{ name: `Yes, use port ${alternativePort}`, value: "yes" },
|
|
1805
|
+
{ name: "No, I will free the port manually", value: "no" }
|
|
1806
|
+
]
|
|
1807
|
+
});
|
|
1808
|
+
if (useAlternative === "yes") {
|
|
1809
|
+
port = alternativePort;
|
|
1810
|
+
} else {
|
|
1811
|
+
throw new Error(`Port ${ctx.port} is in use`);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
const action = await select2({
|
|
1816
|
+
message: "OpenCode is not running. What would you like to do?",
|
|
1817
|
+
choices: [
|
|
1818
|
+
{
|
|
1819
|
+
name: "Start OpenCode for me",
|
|
1820
|
+
value: "start",
|
|
1821
|
+
description: `Run 'opencode serve --port ${port}'`
|
|
1822
|
+
},
|
|
1823
|
+
{
|
|
1824
|
+
name: "Show me the command",
|
|
1825
|
+
value: "manual",
|
|
1826
|
+
description: "Display the command to run manually"
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
name: "Continue without OpenCode",
|
|
1830
|
+
value: "continue",
|
|
1831
|
+
description: "Requests will fail until OpenCode starts"
|
|
1832
|
+
}
|
|
1833
|
+
]
|
|
1588
1834
|
});
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1835
|
+
if (action === "manual") {
|
|
1836
|
+
blank();
|
|
1837
|
+
console.log(chalk5.bold("Run this command in another terminal:"));
|
|
1838
|
+
blank();
|
|
1839
|
+
console.log(` ${chalk5.cyan(`opencode serve --port ${port}`)}`);
|
|
1840
|
+
blank();
|
|
1841
|
+
throw new Error("Please start OpenCode manually");
|
|
1842
|
+
}
|
|
1843
|
+
if (action === "start") {
|
|
1844
|
+
const spinner = ora2("Starting OpenCode...").start();
|
|
1845
|
+
const proc = await startOpenCode(port);
|
|
1846
|
+
const health = await waitForOpenCodeHealth(port, 3e4);
|
|
1847
|
+
if (!health.healthy) {
|
|
1848
|
+
spinner.fail("Failed to start OpenCode");
|
|
1849
|
+
throw new Error("OpenCode failed to start");
|
|
1850
|
+
}
|
|
1851
|
+
spinner.succeed(
|
|
1852
|
+
`OpenCode running on port ${port}${health.version ? ` (v${health.version})` : ""}`
|
|
1594
1853
|
);
|
|
1854
|
+
return { port, process: proc, version: health.version ?? null };
|
|
1595
1855
|
}
|
|
1856
|
+
return { port, process: null, version: null };
|
|
1596
1857
|
}
|
|
1597
|
-
|
|
1858
|
+
|
|
1859
|
+
// src/commands/agent-lookup.ts
|
|
1860
|
+
async function resolveAgentIdFromKey(authHeader) {
|
|
1598
1861
|
const apiUrl = getApiUrlConfig();
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1862
|
+
try {
|
|
1863
|
+
const response = await fetch(`${apiUrl}/me`, {
|
|
1864
|
+
headers: { Authorization: authHeader }
|
|
1865
|
+
});
|
|
1866
|
+
if (!response.ok) {
|
|
1867
|
+
return { error: `Failed to resolve agent from key: HTTP ${response.status}` };
|
|
1868
|
+
}
|
|
1869
|
+
const data = await response.json();
|
|
1870
|
+
if (data.auth_type === "agent_key" && data.agent_id) {
|
|
1871
|
+
return { agent_id: data.agent_id };
|
|
1872
|
+
}
|
|
1873
|
+
return {
|
|
1874
|
+
error: "Cannot resolve agent ID: auth type is not agent_key. Please provide --agent explicitly."
|
|
1875
|
+
};
|
|
1876
|
+
} catch (error2) {
|
|
1877
|
+
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1878
|
+
return { error: `Failed to resolve agent from key: ${message}` };
|
|
1879
|
+
}
|
|
1605
1880
|
}
|
|
1881
|
+
async function getAgentInfo(agentId, authHeader) {
|
|
1882
|
+
const apiUrl = getApiUrlConfig();
|
|
1883
|
+
try {
|
|
1884
|
+
const response = await fetch(`${apiUrl}/agents/${agentId}`, {
|
|
1885
|
+
headers: { Authorization: authHeader }
|
|
1886
|
+
});
|
|
1887
|
+
if (response.status === 404) {
|
|
1888
|
+
return { valid: false, error: "Agent not found" };
|
|
1889
|
+
}
|
|
1890
|
+
if (response.status === 401) {
|
|
1891
|
+
return { valid: false, error: "Authentication failed", authFailed: true };
|
|
1892
|
+
}
|
|
1893
|
+
if (!response.ok) {
|
|
1894
|
+
return { valid: false, error: `API error: ${response.status}` };
|
|
1895
|
+
}
|
|
1896
|
+
const agent = await response.json();
|
|
1897
|
+
if (agent.agent_type !== "local") {
|
|
1898
|
+
return {
|
|
1899
|
+
valid: false,
|
|
1900
|
+
error: `Agent is type '${agent.agent_type}', must be 'local' for CLI connection`
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
return { valid: true, agent };
|
|
1904
|
+
} catch (error2) {
|
|
1905
|
+
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1906
|
+
return { valid: false, error: `Failed to validate agent: ${message}` };
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// src/commands/run.ts
|
|
1911
|
+
var MAX_ACTIVITY_LOG_ENTRIES = 10;
|
|
1912
|
+
var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
|
|
1606
1913
|
function log(state, message, isError = false) {
|
|
1607
1914
|
if (state.json) {
|
|
1608
1915
|
console.log(
|
|
@@ -1613,7 +1920,7 @@ function log(state, message, isError = false) {
|
|
|
1613
1920
|
})
|
|
1614
1921
|
);
|
|
1615
1922
|
} else if (!state.interactive) {
|
|
1616
|
-
const prefix = isError ?
|
|
1923
|
+
const prefix = isError ? chalk6.red("\u2717") : chalk6.green("\u2022");
|
|
1617
1924
|
console.log(`${prefix} ${message}`);
|
|
1618
1925
|
}
|
|
1619
1926
|
}
|
|
@@ -1626,7 +1933,6 @@ function logActivity(state, entry) {
|
|
|
1626
1933
|
if (state.activityLog.length > MAX_ACTIVITY_LOG_ENTRIES) {
|
|
1627
1934
|
state.activityLog.shift();
|
|
1628
1935
|
}
|
|
1629
|
-
state.lastActivity = fullEntry.timestamp;
|
|
1630
1936
|
if (!state.interactive) {
|
|
1631
1937
|
if (entry.type === "error") {
|
|
1632
1938
|
log(state, entry.error ?? "Unknown error", true);
|
|
@@ -1635,130 +1941,21 @@ function logActivity(state, entry) {
|
|
|
1635
1941
|
}
|
|
1636
1942
|
}
|
|
1637
1943
|
}
|
|
1638
|
-
var ANSI = {
|
|
1639
|
-
moveUp: (n) => `\x1B[${n}A`
|
|
1640
|
-
};
|
|
1641
|
-
var STATUS_DISPLAY_HEIGHT = 22;
|
|
1642
|
-
function colorizeStatus(status) {
|
|
1643
|
-
if (status >= 200 && status < 300) {
|
|
1644
|
-
return chalk5.green(status.toString());
|
|
1645
|
-
} else if (status >= 300 && status < 400) {
|
|
1646
|
-
return chalk5.yellow(status.toString());
|
|
1647
|
-
} else if (status >= 400 && status < 500) {
|
|
1648
|
-
return chalk5.red(status.toString());
|
|
1649
|
-
} else if (status >= 500) {
|
|
1650
|
-
return chalk5.bgRed.white(` ${status} `);
|
|
1651
|
-
}
|
|
1652
|
-
return status.toString();
|
|
1653
|
-
}
|
|
1654
|
-
function formatActivityEntry(entry) {
|
|
1655
|
-
const time = entry.timestamp.toLocaleTimeString("en-US", {
|
|
1656
|
-
hour12: false,
|
|
1657
|
-
hour: "2-digit",
|
|
1658
|
-
minute: "2-digit",
|
|
1659
|
-
second: "2-digit"
|
|
1660
|
-
});
|
|
1661
|
-
switch (entry.type) {
|
|
1662
|
-
case "request": {
|
|
1663
|
-
const duration = entry.durationMs ? ` (${entry.durationMs}ms)` : "";
|
|
1664
|
-
const status = entry.status ? ` -> ${colorizeStatus(entry.status)}` : " ...";
|
|
1665
|
-
return ` ${chalk5.dim(`[${time}]`)} ${chalk5.cyan("<-")} ${entry.method} ${entry.path}${status}${duration}`;
|
|
1666
|
-
}
|
|
1667
|
-
case "response": {
|
|
1668
|
-
const duration = entry.durationMs ? ` (${entry.durationMs}ms)` : "";
|
|
1669
|
-
return ` ${chalk5.dim(`[${time}]`)} ${chalk5.green("->")} ${entry.method} ${entry.path} ${colorizeStatus(entry.status)}${duration}`;
|
|
1670
|
-
}
|
|
1671
|
-
case "error": {
|
|
1672
|
-
const errorMsg = entry.error || "Unknown error";
|
|
1673
|
-
const path = entry.path ? ` ${entry.method} ${entry.path}` : "";
|
|
1674
|
-
return ` ${chalk5.dim(`[${time}]`)} ${chalk5.red("x")}${path} - ${chalk5.red(errorMsg)}`;
|
|
1675
|
-
}
|
|
1676
|
-
case "info": {
|
|
1677
|
-
return ` ${chalk5.dim(`[${time}]`)} ${chalk5.blue("*")} ${entry.message}`;
|
|
1678
|
-
}
|
|
1679
|
-
default:
|
|
1680
|
-
return ` ${chalk5.dim(`[${time}]`)} ${entry.message || "Unknown"}`;
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
1944
|
function displayStatus(state) {
|
|
1684
1945
|
if (!state.interactive) return;
|
|
1685
|
-
const
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
lines.push(` Filter: conversation ${state.conversationFilter.slice(0, 8)}...`);
|
|
1695
|
-
}
|
|
1696
|
-
lines.push("");
|
|
1697
|
-
if (state.connected) {
|
|
1698
|
-
lines.push(` ${chalk5.green("*")} Tunnel: ${chalk5.green("Connected to Evident")}`);
|
|
1699
|
-
} else {
|
|
1700
|
-
if (state.reconnectAttempt > 0) {
|
|
1701
|
-
lines.push(
|
|
1702
|
-
` ${chalk5.yellow("o")} Tunnel: ${chalk5.yellow(`Reconnecting... (attempt ${state.reconnectAttempt})`)}`
|
|
1703
|
-
);
|
|
1704
|
-
} else {
|
|
1705
|
-
lines.push(` ${chalk5.yellow("o")} Tunnel: ${chalk5.yellow("Connecting...")}`);
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
if (state.opencodeConnected) {
|
|
1709
|
-
const version = state.opencodeVersion ? `, v${state.opencodeVersion}` : "";
|
|
1710
|
-
lines.push(
|
|
1711
|
-
` ${chalk5.green("*")} OpenCode: ${chalk5.green(`Running on port ${state.port}${version}`)}`
|
|
1712
|
-
);
|
|
1713
|
-
} else {
|
|
1714
|
-
lines.push(` ${chalk5.red("o")} OpenCode: ${chalk5.red(`Not connected (port ${state.port})`)}`);
|
|
1715
|
-
}
|
|
1716
|
-
lines.push("");
|
|
1717
|
-
if (state.messageCount > 0) {
|
|
1718
|
-
lines.push(` Messages: ${state.messageCount} processed`);
|
|
1719
|
-
lines.push("");
|
|
1720
|
-
}
|
|
1721
|
-
if (state.activityLog.length > 0) {
|
|
1722
|
-
lines.push(chalk5.bold(" Activity:"));
|
|
1723
|
-
for (const entry of state.activityLog) {
|
|
1724
|
-
lines.push(formatActivityEntry(entry));
|
|
1725
|
-
}
|
|
1726
|
-
} else {
|
|
1727
|
-
lines.push(chalk5.dim(" No activity yet. Waiting for requests..."));
|
|
1728
|
-
}
|
|
1729
|
-
lines.push("");
|
|
1730
|
-
lines.push(chalk5.dim("-".repeat(60)));
|
|
1731
|
-
if (state.verbose) {
|
|
1732
|
-
lines.push(chalk5.dim(" Verbose mode: ON"));
|
|
1733
|
-
}
|
|
1734
|
-
lines.push("");
|
|
1735
|
-
lines.push(
|
|
1736
|
-
chalk5.dim(` Tip: Run \`opencode attach http://localhost:${state.port}\` to see live activity`)
|
|
1946
|
+
const attempt = state.connection?.reconnectAttempt ?? 0;
|
|
1947
|
+
const tunnel = state.connected ? chalk6.green("tunnel: connected") : attempt > 0 ? chalk6.yellow(`tunnel: reconnecting (#${attempt})`) : chalk6.yellow("tunnel: connecting");
|
|
1948
|
+
const opencode = state.opencodeConnected ? chalk6.green(`opencode: :${state.port}`) : chalk6.red(`opencode: :${state.port} (down)`);
|
|
1949
|
+
const messages = state.messageCount > 0 ? chalk6.dim(` \xB7 ${state.messageCount} processed`) : "";
|
|
1950
|
+
const last = state.activityLog[state.activityLog.length - 1];
|
|
1951
|
+
const detail = last ? chalk6.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
|
|
1952
|
+
const agent = state.agentName ?? state.agentId;
|
|
1953
|
+
console.log(
|
|
1954
|
+
`${chalk6.bold("Evident")} ${chalk6.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
|
|
1737
1955
|
);
|
|
1738
|
-
lines.push(chalk5.dim(" Press Ctrl+C to disconnect"));
|
|
1739
|
-
while (lines.length < STATUS_DISPLAY_HEIGHT) {
|
|
1740
|
-
lines.push("");
|
|
1741
|
-
}
|
|
1742
|
-
if (!state.displayInitialized) {
|
|
1743
|
-
console.log("");
|
|
1744
|
-
console.log(chalk5.dim("=".repeat(60)));
|
|
1745
|
-
console.log("");
|
|
1746
|
-
for (const line of lines) {
|
|
1747
|
-
console.log(line);
|
|
1748
|
-
}
|
|
1749
|
-
state.displayInitialized = true;
|
|
1750
|
-
} else {
|
|
1751
|
-
process.stdout.write(ANSI.moveUp(STATUS_DISPLAY_HEIGHT + 3));
|
|
1752
|
-
console.log(chalk5.dim("=".repeat(60)));
|
|
1753
|
-
console.log("");
|
|
1754
|
-
for (const line of lines) {
|
|
1755
|
-
process.stdout.write("\x1B[2K");
|
|
1756
|
-
console.log(line);
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
1956
|
}
|
|
1760
1957
|
async function promptForLogin(promptMessage, successMessage) {
|
|
1761
|
-
const action = await
|
|
1958
|
+
const action = await select3({
|
|
1762
1959
|
message: promptMessage,
|
|
1763
1960
|
choices: [
|
|
1764
1961
|
{
|
|
@@ -1774,7 +1971,7 @@ async function promptForLogin(promptMessage, successMessage) {
|
|
|
1774
1971
|
]
|
|
1775
1972
|
});
|
|
1776
1973
|
if (action === "exit") {
|
|
1777
|
-
console.log(
|
|
1974
|
+
console.log(chalk6.dim(`
|
|
1778
1975
|
You can log in later by running: ${getCliName()} login`));
|
|
1779
1976
|
process.exit(0);
|
|
1780
1977
|
}
|
|
@@ -1785,137 +1982,10 @@ You can log in later by running: ${getCliName()} login`));
|
|
|
1785
1982
|
process.exit(1);
|
|
1786
1983
|
}
|
|
1787
1984
|
blank();
|
|
1788
|
-
console.log(
|
|
1985
|
+
console.log(chalk6.green(successMessage));
|
|
1789
1986
|
blank();
|
|
1790
1987
|
return { token: credentials2.token, authType: "bearer", user: credentials2.user };
|
|
1791
1988
|
}
|
|
1792
|
-
async function ensureOpenCodeRunning(state) {
|
|
1793
|
-
const healthCheck = await checkOpenCodeHealth(state.port);
|
|
1794
|
-
if (healthCheck.healthy) {
|
|
1795
|
-
state.opencodeConnected = true;
|
|
1796
|
-
state.opencodeVersion = healthCheck.version ?? null;
|
|
1797
|
-
return;
|
|
1798
|
-
}
|
|
1799
|
-
const runningInstances = await findHealthyOpenCodeInstances();
|
|
1800
|
-
if (runningInstances.length > 0) {
|
|
1801
|
-
if (!state.interactive) {
|
|
1802
|
-
throw new Error(
|
|
1803
|
-
`OpenCode not found on port ${state.port}, but running on port ${runningInstances[0].port}. Use --port ${runningInstances[0].port}`
|
|
1804
|
-
);
|
|
1805
|
-
}
|
|
1806
|
-
blank();
|
|
1807
|
-
console.log(chalk5.yellow("Found OpenCode running on different port(s):"));
|
|
1808
|
-
for (const instance of runningInstances) {
|
|
1809
|
-
const ver = instance.version ? ` (v${instance.version})` : "";
|
|
1810
|
-
const cwd = instance.cwd ? ` in ${instance.cwd}` : "";
|
|
1811
|
-
console.log(chalk5.dim(` * Port ${instance.port}${ver}${cwd}`));
|
|
1812
|
-
}
|
|
1813
|
-
blank();
|
|
1814
|
-
if (runningInstances.length === 1) {
|
|
1815
|
-
console.log(chalk5.yellow("Tip: Run with the correct port:"));
|
|
1816
|
-
console.log(
|
|
1817
|
-
chalk5.dim(
|
|
1818
|
-
` ${getCliName()} run --agent ${state.agentId} --port ${runningInstances[0].port}`
|
|
1819
|
-
)
|
|
1820
|
-
);
|
|
1821
|
-
}
|
|
1822
|
-
blank();
|
|
1823
|
-
throw new Error(`OpenCode not running on port ${state.port}`);
|
|
1824
|
-
}
|
|
1825
|
-
if (!isOpenCodeInstalled()) {
|
|
1826
|
-
if (!state.interactive) {
|
|
1827
|
-
throw new Error("OpenCode is not installed. Install it with: npm install -g opencode-ai");
|
|
1828
|
-
}
|
|
1829
|
-
const result = await promptOpenCodeInstall(true);
|
|
1830
|
-
if (result === "exit") {
|
|
1831
|
-
process.exit(0);
|
|
1832
|
-
}
|
|
1833
|
-
if (result === "installed" || isOpenCodeInstalled()) {
|
|
1834
|
-
} else {
|
|
1835
|
-
throw new Error("OpenCode is not installed");
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
|
-
if (state.interactive) {
|
|
1839
|
-
let actualPort = state.port;
|
|
1840
|
-
if (isPortInUse(state.port)) {
|
|
1841
|
-
console.log(chalk5.yellow(`
|
|
1842
|
-
Port ${state.port} is already in use.`));
|
|
1843
|
-
const alternativePort = findAvailablePort(state.port + 1);
|
|
1844
|
-
if (alternativePort) {
|
|
1845
|
-
const useAlternative = await select2({
|
|
1846
|
-
message: `Use port ${alternativePort} instead?`,
|
|
1847
|
-
choices: [
|
|
1848
|
-
{ name: `Yes, use port ${alternativePort}`, value: "yes" },
|
|
1849
|
-
{ name: "No, I will free the port manually", value: "no" }
|
|
1850
|
-
]
|
|
1851
|
-
});
|
|
1852
|
-
if (useAlternative === "yes") {
|
|
1853
|
-
actualPort = alternativePort;
|
|
1854
|
-
state.port = actualPort;
|
|
1855
|
-
} else {
|
|
1856
|
-
throw new Error(`Port ${state.port} is in use`);
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
const action = await select2({
|
|
1861
|
-
message: "OpenCode is not running. What would you like to do?",
|
|
1862
|
-
choices: [
|
|
1863
|
-
{
|
|
1864
|
-
name: "Start OpenCode for me",
|
|
1865
|
-
value: "start",
|
|
1866
|
-
description: `Run 'opencode serve --port ${actualPort}'`
|
|
1867
|
-
},
|
|
1868
|
-
{
|
|
1869
|
-
name: "Show me the command",
|
|
1870
|
-
value: "manual",
|
|
1871
|
-
description: "Display the command to run manually"
|
|
1872
|
-
},
|
|
1873
|
-
{
|
|
1874
|
-
name: "Continue without OpenCode",
|
|
1875
|
-
value: "continue",
|
|
1876
|
-
description: "Requests will fail until OpenCode starts"
|
|
1877
|
-
}
|
|
1878
|
-
]
|
|
1879
|
-
});
|
|
1880
|
-
if (action === "manual") {
|
|
1881
|
-
blank();
|
|
1882
|
-
console.log(chalk5.bold("Run this command in another terminal:"));
|
|
1883
|
-
blank();
|
|
1884
|
-
console.log(` ${chalk5.cyan(`opencode serve --port ${actualPort}`)}`);
|
|
1885
|
-
blank();
|
|
1886
|
-
throw new Error("Please start OpenCode manually");
|
|
1887
|
-
}
|
|
1888
|
-
if (action === "start") {
|
|
1889
|
-
const spinner = ora2("Starting OpenCode...").start();
|
|
1890
|
-
state.opencodeProcess = await startOpenCode(actualPort);
|
|
1891
|
-
const health = await waitForOpenCodeHealth(actualPort, 3e4);
|
|
1892
|
-
if (!health.healthy) {
|
|
1893
|
-
spinner.fail("Failed to start OpenCode");
|
|
1894
|
-
throw new Error("OpenCode failed to start");
|
|
1895
|
-
}
|
|
1896
|
-
spinner.succeed(
|
|
1897
|
-
`OpenCode running on port ${actualPort}${health.version ? ` (v${health.version})` : ""}`
|
|
1898
|
-
);
|
|
1899
|
-
state.opencodeConnected = true;
|
|
1900
|
-
state.opencodeVersion = health.version ?? null;
|
|
1901
|
-
}
|
|
1902
|
-
} else {
|
|
1903
|
-
log(state, `OpenCode is not running on port ${state.port}. Starting it automatically...`);
|
|
1904
|
-
state.opencodeProcess = await startOpenCode(state.port);
|
|
1905
|
-
const health = await waitForOpenCodeHealth(state.port, 3e4);
|
|
1906
|
-
if (!health.healthy) {
|
|
1907
|
-
throw new Error(
|
|
1908
|
-
`OpenCode failed to start on port ${state.port}. Install with: npm install -g opencode-ai`
|
|
1909
|
-
);
|
|
1910
|
-
}
|
|
1911
|
-
log(
|
|
1912
|
-
state,
|
|
1913
|
-
`OpenCode started on port ${state.port}${health.version ? ` (v${health.version})` : ""}`
|
|
1914
|
-
);
|
|
1915
|
-
state.opencodeConnected = true;
|
|
1916
|
-
state.opencodeVersion = health.version ?? null;
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
1989
|
var AUTH_EXPIRED_EXIT_CODE = 77;
|
|
1920
1990
|
async function handleAuthError(state, error2) {
|
|
1921
1991
|
logActivity(state, {
|
|
@@ -1925,12 +1995,12 @@ async function handleAuthError(state, error2) {
|
|
|
1925
1995
|
if (state.interactive) displayStatus(state);
|
|
1926
1996
|
if (!state.interactive) {
|
|
1927
1997
|
blank();
|
|
1928
|
-
console.log(
|
|
1929
|
-
console.log(
|
|
1998
|
+
console.log(chalk6.red("Authentication expired"));
|
|
1999
|
+
console.log(chalk6.dim("Your authentication token is no longer valid."));
|
|
1930
2000
|
blank();
|
|
1931
|
-
console.log(
|
|
1932
|
-
console.log(
|
|
1933
|
-
console.log(
|
|
2001
|
+
console.log(chalk6.dim("To fix this:"));
|
|
2002
|
+
console.log(chalk6.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
|
|
2003
|
+
console.log(chalk6.dim(" 2. Restart this command"));
|
|
1934
2004
|
blank();
|
|
1935
2005
|
await cleanup(state);
|
|
1936
2006
|
await shutdownTelemetry();
|
|
@@ -1938,7 +2008,7 @@ async function handleAuthError(state, error2) {
|
|
|
1938
2008
|
return { success: false };
|
|
1939
2009
|
}
|
|
1940
2010
|
blank();
|
|
1941
|
-
console.log(
|
|
2011
|
+
console.log(chalk6.yellow("Your authentication has expired."));
|
|
1942
2012
|
blank();
|
|
1943
2013
|
try {
|
|
1944
2014
|
const credentials2 = await promptForLogin(
|
|
@@ -1951,293 +2021,62 @@ async function handleAuthError(state, error2) {
|
|
|
1951
2021
|
return { success: false };
|
|
1952
2022
|
}
|
|
1953
2023
|
}
|
|
1954
|
-
function
|
|
1955
|
-
if (error2 instanceof Error) {
|
|
1956
|
-
const message = error2.message.toLowerCase();
|
|
1957
|
-
return message.includes("fetch failed") || message.includes("network") || message.includes("econnrefused") || message.includes("econnreset") || message.includes("etimedout") || message.includes("socket hang up");
|
|
1958
|
-
}
|
|
1959
|
-
return false;
|
|
1960
|
-
}
|
|
1961
|
-
async function processQueue(state, authHeader, triggerReconnect) {
|
|
2024
|
+
async function driveChannels(state, driver) {
|
|
1962
2025
|
let idlePolls = 0;
|
|
1963
|
-
let currentAuthHeader = authHeader;
|
|
1964
2026
|
while (state.running) {
|
|
1965
|
-
if (state.reconnecting && state.reconnectPromise) {
|
|
1966
|
-
logActivity(state, {
|
|
1967
|
-
type: "info",
|
|
1968
|
-
message: "Waiting for tunnel reconnection..."
|
|
1969
|
-
});
|
|
2027
|
+
if (state.connection?.reconnecting && state.connection.reconnectPromise) {
|
|
2028
|
+
logActivity(state, { type: "info", message: "Waiting for tunnel reconnection..." });
|
|
1970
2029
|
if (state.interactive) displayStatus(state);
|
|
1971
|
-
await state.reconnectPromise;
|
|
2030
|
+
await state.connection.reconnectPromise;
|
|
1972
2031
|
}
|
|
1973
2032
|
try {
|
|
1974
|
-
const
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
state.conversationFilter ?? void 0
|
|
1978
|
-
);
|
|
1979
|
-
state.consecutiveFetchFailures = 0;
|
|
1980
|
-
if (conversations.length > 0) {
|
|
2033
|
+
const processed = await driver.drainPending();
|
|
2034
|
+
state.messageCount += processed;
|
|
2035
|
+
if (processed > 0) {
|
|
1981
2036
|
idlePolls = 0;
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
state.agentId,
|
|
1987
|
-
conv.id,
|
|
1988
|
-
state.lockCorrelationId,
|
|
1989
|
-
currentAuthHeader
|
|
1990
|
-
);
|
|
1991
|
-
if (!lockResult.acquired) {
|
|
1992
|
-
const ttlMessage = lockResult.expires_at ? ` (lock expires in ${Math.max(0, Math.ceil((new Date(lockResult.expires_at).getTime() - Date.now()) / 1e3))}s)` : "";
|
|
1993
|
-
logActivity(state, {
|
|
1994
|
-
type: "info",
|
|
1995
|
-
message: `Conversation ${conv.id.slice(0, 8)} locked by another runner \u2014 skipping${ttlMessage}`
|
|
1996
|
-
});
|
|
1997
|
-
if (state.interactive) displayStatus(state);
|
|
1998
|
-
continue;
|
|
1999
|
-
}
|
|
2000
|
-
state.lockedConversations.add(conv.id);
|
|
2001
|
-
logActivity(state, {
|
|
2002
|
-
type: "info",
|
|
2003
|
-
message: `Lock acquired on conversation ${conv.id.slice(0, 8)}`
|
|
2004
|
-
});
|
|
2005
|
-
}
|
|
2037
|
+
if (state.interactive) displayStatus(state);
|
|
2038
|
+
} else if (state.idleTimeout !== null) {
|
|
2039
|
+
idlePolls++;
|
|
2040
|
+
if (idlePolls === 1) {
|
|
2006
2041
|
logActivity(state, {
|
|
2007
2042
|
type: "info",
|
|
2008
|
-
message: `
|
|
2043
|
+
message: `Queue empty, waiting (timeout: ${state.idleTimeout}s)...`
|
|
2009
2044
|
});
|
|
2010
2045
|
if (state.interactive) displayStatus(state);
|
|
2011
|
-
let sessionId = state.sessions.get(conv.id);
|
|
2012
|
-
if (!sessionId) {
|
|
2013
|
-
if (conv.opencode_session_id) {
|
|
2014
|
-
sessionId = conv.opencode_session_id;
|
|
2015
|
-
} else {
|
|
2016
|
-
sessionId = await createOpenCodeSession(state.port);
|
|
2017
|
-
await updateConversationSession(state.agentId, conv.id, sessionId, currentAuthHeader);
|
|
2018
|
-
logActivity(state, {
|
|
2019
|
-
type: "info",
|
|
2020
|
-
message: `Created session ${sessionId.slice(0, 8)}`
|
|
2021
|
-
});
|
|
2022
|
-
}
|
|
2023
|
-
state.sessions.set(conv.id, sessionId);
|
|
2024
|
-
}
|
|
2025
|
-
const messages = await getPendingMessages(state.agentId, conv.id, currentAuthHeader);
|
|
2026
|
-
for (const message of messages) {
|
|
2027
|
-
if (!state.running) break;
|
|
2028
|
-
logActivity(state, {
|
|
2029
|
-
type: "info",
|
|
2030
|
-
message: `Processing message ${message.id.slice(0, 8)}...`
|
|
2031
|
-
});
|
|
2032
|
-
if (state.interactive) displayStatus(state);
|
|
2033
|
-
const claimed = await markMessageProcessing(
|
|
2034
|
-
state.agentId,
|
|
2035
|
-
conv.id,
|
|
2036
|
-
message.id,
|
|
2037
|
-
currentAuthHeader
|
|
2038
|
-
);
|
|
2039
|
-
if (!claimed) {
|
|
2040
|
-
logActivity(state, {
|
|
2041
|
-
type: "info",
|
|
2042
|
-
message: `Message ${message.id.slice(0, 8)} already claimed`
|
|
2043
|
-
});
|
|
2044
|
-
continue;
|
|
2045
|
-
}
|
|
2046
|
-
emitAgentMessageProcessing(state.agentId, {
|
|
2047
|
-
message_id: message.id,
|
|
2048
|
-
conversation_id: conv.id
|
|
2049
|
-
});
|
|
2050
|
-
try {
|
|
2051
|
-
const result = await sendMessageToOpenCode(
|
|
2052
|
-
state.port,
|
|
2053
|
-
sessionId,
|
|
2054
|
-
message.content,
|
|
2055
|
-
{
|
|
2056
|
-
agent: message.opencode_agent ?? void 0,
|
|
2057
|
-
model: message.opencode_model ?? void 0
|
|
2058
|
-
},
|
|
2059
|
-
{
|
|
2060
|
-
onQuestion: async (question) => {
|
|
2061
|
-
try {
|
|
2062
|
-
await reportInteractiveEvent(
|
|
2063
|
-
state.agentId,
|
|
2064
|
-
conv.id,
|
|
2065
|
-
"question",
|
|
2066
|
-
question,
|
|
2067
|
-
currentAuthHeader
|
|
2068
|
-
);
|
|
2069
|
-
logActivity(state, {
|
|
2070
|
-
type: "info",
|
|
2071
|
-
message: `Question surfaced to user (id: ${question.id.slice(0, 8)})`
|
|
2072
|
-
});
|
|
2073
|
-
} catch (err) {
|
|
2074
|
-
logActivity(state, {
|
|
2075
|
-
type: "error",
|
|
2076
|
-
error: `Failed to surface question: ${err}`
|
|
2077
|
-
});
|
|
2078
|
-
}
|
|
2079
|
-
},
|
|
2080
|
-
onPermission: async (permission) => {
|
|
2081
|
-
try {
|
|
2082
|
-
await reportInteractiveEvent(
|
|
2083
|
-
state.agentId,
|
|
2084
|
-
conv.id,
|
|
2085
|
-
"permission",
|
|
2086
|
-
permission,
|
|
2087
|
-
currentAuthHeader
|
|
2088
|
-
);
|
|
2089
|
-
logActivity(state, {
|
|
2090
|
-
type: "info",
|
|
2091
|
-
message: `Permission request surfaced to user (id: ${permission.id.slice(0, 8)})`
|
|
2092
|
-
});
|
|
2093
|
-
} catch (err) {
|
|
2094
|
-
logActivity(state, {
|
|
2095
|
-
type: "error",
|
|
2096
|
-
error: `Failed to surface permission: ${err}`
|
|
2097
|
-
});
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
);
|
|
2102
|
-
if (result.title) {
|
|
2103
|
-
try {
|
|
2104
|
-
await updateConversationTitle(
|
|
2105
|
-
state.agentId,
|
|
2106
|
-
conv.id,
|
|
2107
|
-
result.title,
|
|
2108
|
-
currentAuthHeader
|
|
2109
|
-
);
|
|
2110
|
-
} catch {
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
await markMessageDone(
|
|
2114
|
-
state.agentId,
|
|
2115
|
-
conv.id,
|
|
2116
|
-
message.id,
|
|
2117
|
-
currentAuthHeader,
|
|
2118
|
-
sessionId
|
|
2119
|
-
);
|
|
2120
|
-
state.messageCount++;
|
|
2121
|
-
logActivity(state, {
|
|
2122
|
-
type: "info",
|
|
2123
|
-
message: `Message ${message.id.slice(0, 8)} processed`
|
|
2124
|
-
});
|
|
2125
|
-
emitAgentMessageDone(state.agentId, {
|
|
2126
|
-
message_id: message.id,
|
|
2127
|
-
conversation_id: conv.id
|
|
2128
|
-
});
|
|
2129
|
-
} catch (error2) {
|
|
2130
|
-
if (error2 instanceof AuthenticationError) {
|
|
2131
|
-
throw error2;
|
|
2132
|
-
}
|
|
2133
|
-
await markMessageFailed(state.agentId, conv.id, message.id, currentAuthHeader);
|
|
2134
|
-
logActivity(state, {
|
|
2135
|
-
type: "error",
|
|
2136
|
-
error: `Message ${message.id.slice(0, 8)} failed: ${error2}`
|
|
2137
|
-
});
|
|
2138
|
-
emitAgentMessageFailed(state.agentId, {
|
|
2139
|
-
message_id: message.id,
|
|
2140
|
-
conversation_id: conv.id,
|
|
2141
|
-
error: String(error2)
|
|
2142
|
-
});
|
|
2143
|
-
}
|
|
2144
|
-
if (state.interactive) displayStatus(state);
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
} else {
|
|
2148
|
-
if (state.idleTimeout !== null) {
|
|
2149
|
-
idlePolls++;
|
|
2150
|
-
if (idlePolls === 1) {
|
|
2151
|
-
logActivity(state, {
|
|
2152
|
-
type: "info",
|
|
2153
|
-
message: `Queue empty, waiting (timeout: ${state.idleTimeout}s)...`
|
|
2154
|
-
});
|
|
2155
|
-
if (state.interactive) displayStatus(state);
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
await new Promise((resolve) => setTimeout(resolve, MESSAGE_POLL_INTERVAL_MS));
|
|
2160
|
-
if (state.idleTimeout !== null && idlePolls >= 2) {
|
|
2161
|
-
const idleMs = idlePolls * MESSAGE_POLL_INTERVAL_MS;
|
|
2162
|
-
if (idleMs > state.idleTimeout * 1e3) {
|
|
2163
|
-
logActivity(state, {
|
|
2164
|
-
type: "info",
|
|
2165
|
-
message: "Idle timeout reached"
|
|
2166
|
-
});
|
|
2167
|
-
if (state.interactive) displayStatus(state);
|
|
2168
|
-
if (state.lockedConversations.size > 0) {
|
|
2169
|
-
logActivity(state, {
|
|
2170
|
-
type: "info",
|
|
2171
|
-
message: `Releasing ${state.lockedConversations.size} lock(s) before idle exit...`
|
|
2172
|
-
});
|
|
2173
|
-
if (state.interactive) displayStatus(state);
|
|
2174
|
-
}
|
|
2175
|
-
break;
|
|
2176
2046
|
}
|
|
2177
2047
|
}
|
|
2178
2048
|
} catch (error2) {
|
|
2179
|
-
if (error2 instanceof
|
|
2049
|
+
if (error2 instanceof ChannelAuthError) {
|
|
2180
2050
|
const result = await handleAuthError(state, error2);
|
|
2181
2051
|
if (result.success && result.newAuthHeader) {
|
|
2182
|
-
currentAuthHeader = result.newAuthHeader;
|
|
2183
2052
|
state.authHeader = result.newAuthHeader;
|
|
2184
|
-
logActivity(state, {
|
|
2185
|
-
type: "info",
|
|
2186
|
-
message: "Continuing with new credentials..."
|
|
2187
|
-
});
|
|
2053
|
+
logActivity(state, { type: "info", message: "Continuing with new credentials..." });
|
|
2188
2054
|
if (state.interactive) displayStatus(state);
|
|
2189
2055
|
continue;
|
|
2190
|
-
} else {
|
|
2191
|
-
state.running = false;
|
|
2192
|
-
break;
|
|
2193
2056
|
}
|
|
2057
|
+
state.running = false;
|
|
2058
|
+
break;
|
|
2194
2059
|
}
|
|
2195
2060
|
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
2196
|
-
logActivity(state, {
|
|
2197
|
-
type: "error",
|
|
2198
|
-
error: `Queue processing error: ${errorMessage}`
|
|
2199
|
-
});
|
|
2061
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage}` });
|
|
2200
2062
|
if (state.interactive) displayStatus(state);
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
await triggerReconnect();
|
|
2210
|
-
state.consecutiveFetchFailures = 0;
|
|
2211
|
-
}
|
|
2063
|
+
}
|
|
2064
|
+
await new Promise((resolve) => setTimeout(resolve, CHANNEL_POLL_INTERVAL_MS));
|
|
2065
|
+
if (state.idleTimeout !== null && idlePolls >= 2) {
|
|
2066
|
+
const idleMs = idlePolls * CHANNEL_POLL_INTERVAL_MS;
|
|
2067
|
+
if (idleMs > state.idleTimeout * 1e3) {
|
|
2068
|
+
logActivity(state, { type: "info", message: "Idle timeout reached" });
|
|
2069
|
+
if (state.interactive) displayStatus(state);
|
|
2070
|
+
break;
|
|
2212
2071
|
}
|
|
2213
|
-
await new Promise((resolve) => setTimeout(resolve, MESSAGE_POLL_INTERVAL_MS));
|
|
2214
2072
|
}
|
|
2215
2073
|
}
|
|
2216
2074
|
}
|
|
2217
|
-
async function cleanup(state
|
|
2075
|
+
async function cleanup(state) {
|
|
2218
2076
|
state.running = false;
|
|
2219
|
-
if (state.
|
|
2220
|
-
|
|
2221
|
-
state.
|
|
2222
|
-
}
|
|
2223
|
-
if (authHeader && authHeader.trim() !== "" && state.lockedConversations.size > 0) {
|
|
2224
|
-
for (const convId of state.lockedConversations) {
|
|
2225
|
-
await releaseConversationLock(state.agentId, convId, state.lockCorrelationId, authHeader);
|
|
2226
|
-
}
|
|
2227
|
-
if (state.interactive) {
|
|
2228
|
-
logActivity(state, {
|
|
2229
|
-
type: "info",
|
|
2230
|
-
message: `Released ${state.lockedConversations.size} lock(s)`
|
|
2231
|
-
});
|
|
2232
|
-
displayStatus(state);
|
|
2233
|
-
} else {
|
|
2234
|
-
log(state, `Released ${state.lockedConversations.size} lock(s)`);
|
|
2235
|
-
}
|
|
2236
|
-
state.lockedConversations.clear();
|
|
2237
|
-
}
|
|
2238
|
-
if (state.tunnelConnection) {
|
|
2239
|
-
state.tunnelConnection.close();
|
|
2240
|
-
state.tunnelConnection = null;
|
|
2077
|
+
if (state.connection) {
|
|
2078
|
+
state.connection.close();
|
|
2079
|
+
state.connection = null;
|
|
2241
2080
|
}
|
|
2242
2081
|
if (state.opencodeProcess) {
|
|
2243
2082
|
stopOpenCode(state.opencodeProcess);
|
|
@@ -2256,7 +2095,6 @@ async function run(options) {
|
|
|
2256
2095
|
agentId: options.agent || "",
|
|
2257
2096
|
agentName: null,
|
|
2258
2097
|
port: options.port ?? 4096,
|
|
2259
|
-
verbose: options.verbose ?? false,
|
|
2260
2098
|
conversationFilter: options.conversation ?? null,
|
|
2261
2099
|
idleTimeout: options.idleTimeout ?? null,
|
|
2262
2100
|
json: options.json ?? false,
|
|
@@ -2264,22 +2102,11 @@ async function run(options) {
|
|
|
2264
2102
|
connected: false,
|
|
2265
2103
|
opencodeConnected: false,
|
|
2266
2104
|
opencodeVersion: null,
|
|
2267
|
-
reconnectAttempt: 0,
|
|
2268
2105
|
opencodeProcess: null,
|
|
2269
|
-
|
|
2106
|
+
connection: null,
|
|
2270
2107
|
running: true,
|
|
2271
2108
|
activityLog: [],
|
|
2272
|
-
displayInitialized: false,
|
|
2273
|
-
lastActivity: /* @__PURE__ */ new Date(),
|
|
2274
|
-
pendingRequests: /* @__PURE__ */ new Map(),
|
|
2275
|
-
sessions: /* @__PURE__ */ new Map(),
|
|
2276
2109
|
messageCount: 0,
|
|
2277
|
-
lockCorrelationId: `cli-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
2278
|
-
lockedConversations: /* @__PURE__ */ new Set(),
|
|
2279
|
-
lockHeartbeatTimer: null,
|
|
2280
|
-
consecutiveFetchFailures: 0,
|
|
2281
|
-
reconnecting: false,
|
|
2282
|
-
reconnectPromise: null,
|
|
2283
2110
|
authHeader: ""
|
|
2284
2111
|
};
|
|
2285
2112
|
if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {
|
|
@@ -2296,7 +2123,7 @@ async function run(options) {
|
|
|
2296
2123
|
} else {
|
|
2297
2124
|
log(state, "Shutting down...");
|
|
2298
2125
|
}
|
|
2299
|
-
await cleanup(state
|
|
2126
|
+
await cleanup(state);
|
|
2300
2127
|
await shutdownTelemetry();
|
|
2301
2128
|
process.exit(0);
|
|
2302
2129
|
};
|
|
@@ -2308,13 +2135,13 @@ async function run(options) {
|
|
|
2308
2135
|
if (!interactive) {
|
|
2309
2136
|
printError("Authentication required");
|
|
2310
2137
|
blank();
|
|
2311
|
-
console.log(
|
|
2312
|
-
console.log(
|
|
2138
|
+
console.log(chalk6.dim("Set EVIDENT_AGENT_KEY environment variable for CI"));
|
|
2139
|
+
console.log(chalk6.dim("Or run `evident login` for interactive authentication"));
|
|
2313
2140
|
blank();
|
|
2314
2141
|
process.exit(1);
|
|
2315
2142
|
}
|
|
2316
2143
|
blank();
|
|
2317
|
-
console.log(
|
|
2144
|
+
console.log(chalk6.yellow("You are not logged in to Evident."));
|
|
2318
2145
|
blank();
|
|
2319
2146
|
credentials2 = await promptForLogin(
|
|
2320
2147
|
"Would you like to log in now?",
|
|
@@ -2341,7 +2168,7 @@ async function run(options) {
|
|
|
2341
2168
|
} else {
|
|
2342
2169
|
printError("--agent is required when not using EVIDENT_AGENT_KEY");
|
|
2343
2170
|
blank();
|
|
2344
|
-
console.log(
|
|
2171
|
+
console.log(chalk6.dim("Either provide --agent <id> or set EVIDENT_AGENT_KEY"));
|
|
2345
2172
|
blank();
|
|
2346
2173
|
process.exit(1);
|
|
2347
2174
|
}
|
|
@@ -2360,15 +2187,15 @@ async function run(options) {
|
|
|
2360
2187
|
);
|
|
2361
2188
|
if (interactive && !state.json) {
|
|
2362
2189
|
blank();
|
|
2363
|
-
console.log(
|
|
2364
|
-
console.log(
|
|
2190
|
+
console.log(chalk6.bold("Evident Run"));
|
|
2191
|
+
console.log(chalk6.dim("-".repeat(40)));
|
|
2365
2192
|
}
|
|
2366
|
-
const spinner = interactive && !state.json ?
|
|
2193
|
+
const spinner = interactive && !state.json ? ora3("Validating agent...").start() : null;
|
|
2367
2194
|
let validation = await getAgentInfo(state.agentId, state.authHeader);
|
|
2368
2195
|
if (!validation.valid && validation.authFailed && interactive) {
|
|
2369
2196
|
spinner?.fail("Authentication failed");
|
|
2370
2197
|
blank();
|
|
2371
|
-
console.log(
|
|
2198
|
+
console.log(chalk6.yellow("Your authentication token is invalid or expired."));
|
|
2372
2199
|
blank();
|
|
2373
2200
|
credentials2 = await promptForLogin(
|
|
2374
2201
|
"Would you like to log in again?",
|
|
@@ -2384,172 +2211,90 @@ async function run(options) {
|
|
|
2384
2211
|
}
|
|
2385
2212
|
spinner?.succeed(`Agent: ${validation.agent.name || state.agentId}`);
|
|
2386
2213
|
state.agentName = validation.agent.name;
|
|
2387
|
-
const ocSpinner = interactive && !state.json ?
|
|
2214
|
+
const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
|
|
2388
2215
|
try {
|
|
2389
|
-
await ensureOpenCodeRunning(
|
|
2216
|
+
const oc = await ensureOpenCodeRunning({
|
|
2217
|
+
port: state.port,
|
|
2218
|
+
interactive: state.interactive,
|
|
2219
|
+
agentId: state.agentId,
|
|
2220
|
+
log: (message) => log(state, message)
|
|
2221
|
+
});
|
|
2222
|
+
state.port = oc.port;
|
|
2223
|
+
state.opencodeProcess = oc.process;
|
|
2224
|
+
state.opencodeVersion = oc.version;
|
|
2225
|
+
state.opencodeConnected = oc.process !== null || oc.version !== null;
|
|
2390
2226
|
const version = state.opencodeVersion ? ` (v${state.opencodeVersion})` : "";
|
|
2391
2227
|
ocSpinner?.succeed(`OpenCode running on port ${state.port}${version}`);
|
|
2392
2228
|
} catch (error2) {
|
|
2393
2229
|
ocSpinner?.fail(error2.message);
|
|
2394
2230
|
throw error2;
|
|
2395
2231
|
}
|
|
2396
|
-
const tunnelSpinner = interactive && !state.json ?
|
|
2397
|
-
const
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
state.
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
state.reconnecting = false;
|
|
2419
|
-
state.consecutiveFetchFailures = 0;
|
|
2420
|
-
state.agentId = agentId;
|
|
2421
|
-
logActivity(state, {
|
|
2422
|
-
type: "info",
|
|
2423
|
-
message: isReconnect ? `Tunnel reconnected (agent: ${agentId})` : `Tunnel connected (agent: ${agentId})`
|
|
2424
|
-
});
|
|
2425
|
-
emitAgentConnected(state.agentId, { port: state.port });
|
|
2426
|
-
if (state.interactive) displayStatus(state);
|
|
2427
|
-
},
|
|
2428
|
-
onDisconnected: (code, reason) => {
|
|
2429
|
-
state.connected = false;
|
|
2430
|
-
logActivity(state, {
|
|
2431
|
-
type: "info",
|
|
2432
|
-
message: `Tunnel disconnected (code: ${code}, reason: ${reason})`
|
|
2433
|
-
});
|
|
2434
|
-
emitAgentDisconnected(state.agentId, { code, reason });
|
|
2435
|
-
if (state.interactive) displayStatus(state);
|
|
2436
|
-
if (state.running && code !== 1e3 && !state.reconnecting) {
|
|
2437
|
-
logActivity(state, {
|
|
2438
|
-
type: "info",
|
|
2439
|
-
message: "Attempting automatic reconnection..."
|
|
2440
|
-
});
|
|
2441
|
-
if (state.interactive) displayStatus(state);
|
|
2442
|
-
state.reconnectPromise = connectWithRetry(true).catch((err) => {
|
|
2443
|
-
logActivity(state, {
|
|
2444
|
-
type: "error",
|
|
2445
|
-
error: `Reconnection failed: ${err.message}`
|
|
2446
|
-
});
|
|
2447
|
-
if (state.interactive) displayStatus(state);
|
|
2448
|
-
});
|
|
2449
|
-
}
|
|
2450
|
-
},
|
|
2451
|
-
onError: (error2) => {
|
|
2452
|
-
logActivity(state, { type: "error", error: error2 });
|
|
2453
|
-
if (state.interactive) displayStatus(state);
|
|
2454
|
-
},
|
|
2455
|
-
onRequest: (method, path, requestId) => {
|
|
2456
|
-
state.pendingRequests.set(requestId, {
|
|
2457
|
-
startTime: Date.now(),
|
|
2458
|
-
method,
|
|
2459
|
-
path
|
|
2460
|
-
});
|
|
2461
|
-
logActivity(state, { type: "request", method, path, requestId });
|
|
2462
|
-
if (state.interactive) displayStatus(state);
|
|
2463
|
-
},
|
|
2464
|
-
onResponse: (status, durationMs, requestId) => {
|
|
2465
|
-
const pending = state.pendingRequests.get(requestId);
|
|
2466
|
-
state.pendingRequests.delete(requestId);
|
|
2467
|
-
state.opencodeConnected = true;
|
|
2468
|
-
const lastEntry = state.activityLog[state.activityLog.length - 1];
|
|
2469
|
-
if (lastEntry && lastEntry.requestId === requestId) {
|
|
2470
|
-
lastEntry.type = "response";
|
|
2471
|
-
lastEntry.status = status;
|
|
2472
|
-
lastEntry.durationMs = durationMs;
|
|
2473
|
-
} else if (pending) {
|
|
2474
|
-
logActivity(state, {
|
|
2475
|
-
type: "response",
|
|
2476
|
-
method: pending.method,
|
|
2477
|
-
path: pending.path,
|
|
2478
|
-
status,
|
|
2479
|
-
durationMs,
|
|
2480
|
-
requestId
|
|
2481
|
-
});
|
|
2482
|
-
}
|
|
2483
|
-
if (state.interactive) displayStatus(state);
|
|
2484
|
-
},
|
|
2485
|
-
onInfo: (message) => {
|
|
2486
|
-
logActivity(state, { type: "info", message });
|
|
2487
|
-
if (state.interactive) displayStatus(state);
|
|
2488
|
-
}
|
|
2489
|
-
});
|
|
2490
|
-
if (!isReconnect) {
|
|
2491
|
-
tunnelSpinner?.succeed("Tunnel connected");
|
|
2492
|
-
}
|
|
2493
|
-
return;
|
|
2494
|
-
} catch (error2) {
|
|
2495
|
-
state.reconnectAttempt++;
|
|
2496
|
-
const delay = getReconnectDelay(state.reconnectAttempt);
|
|
2497
|
-
if (error2.message === "Unauthorized") {
|
|
2498
|
-
state.reconnecting = false;
|
|
2499
|
-
if (!isReconnect) {
|
|
2500
|
-
tunnelSpinner?.fail("Unauthorized");
|
|
2501
|
-
}
|
|
2502
|
-
throw error2;
|
|
2503
|
-
}
|
|
2232
|
+
const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
|
|
2233
|
+
const channelDriver = new ChannelDriver({
|
|
2234
|
+
agentId: state.agentId,
|
|
2235
|
+
port: state.port,
|
|
2236
|
+
apiUrl: getApiUrlConfig(),
|
|
2237
|
+
getAuthHeader: () => state.authHeader,
|
|
2238
|
+
conversationFilter: state.conversationFilter,
|
|
2239
|
+
log: (entry) => logActivity(state, {
|
|
2240
|
+
type: entry.level === "error" ? "error" : "info",
|
|
2241
|
+
message: entry.message,
|
|
2242
|
+
error: entry.level === "error" ? entry.message : void 0
|
|
2243
|
+
})
|
|
2244
|
+
});
|
|
2245
|
+
const connection = new RunnerConnection({
|
|
2246
|
+
agentId: state.agentId,
|
|
2247
|
+
getAuthHeader: () => state.authHeader,
|
|
2248
|
+
port: state.port,
|
|
2249
|
+
isRunning: () => state.running,
|
|
2250
|
+
events: {
|
|
2251
|
+
onConnected: (agentId, isReconnect) => {
|
|
2252
|
+
state.connected = true;
|
|
2253
|
+
state.agentId = agentId;
|
|
2504
2254
|
logActivity(state, {
|
|
2505
|
-
type: "
|
|
2506
|
-
|
|
2255
|
+
type: "info",
|
|
2256
|
+
message: `Tunnel ${isReconnect ? "reconnected" : "connected"} (agent: ${agentId})`
|
|
2507
2257
|
});
|
|
2258
|
+
emitAgentConnected(state.agentId, { port: state.port });
|
|
2259
|
+
if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
|
|
2508
2260
|
if (state.interactive) displayStatus(state);
|
|
2509
|
-
|
|
2510
|
-
}
|
|
2511
|
-
}
|
|
2512
|
-
state.reconnecting = false;
|
|
2513
|
-
};
|
|
2514
|
-
const triggerReconnect = async () => {
|
|
2515
|
-
if (!state.reconnecting) {
|
|
2516
|
-
state.reconnectPromise = connectWithRetry(true).catch((err) => {
|
|
2517
|
-
logActivity(state, {
|
|
2518
|
-
type: "error",
|
|
2519
|
-
error: `Reconnection failed: ${err.message}`
|
|
2261
|
+
channelDriver.drainPending().catch(() => {
|
|
2520
2262
|
});
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
if (state.reconnectPromise) {
|
|
2525
|
-
await state.reconnectPromise;
|
|
2526
|
-
}
|
|
2527
|
-
};
|
|
2528
|
-
await connectWithRetry(false);
|
|
2529
|
-
state.lockHeartbeatTimer = setInterval(async () => {
|
|
2530
|
-
for (const convId of state.lockedConversations) {
|
|
2531
|
-
const extended = await extendConversationLock(
|
|
2532
|
-
state.agentId,
|
|
2533
|
-
convId,
|
|
2534
|
-
state.lockCorrelationId,
|
|
2535
|
-
state.authHeader
|
|
2536
|
-
);
|
|
2537
|
-
if (!extended) {
|
|
2263
|
+
},
|
|
2264
|
+
onDisconnected: (code, reason) => {
|
|
2265
|
+
state.connected = false;
|
|
2538
2266
|
logActivity(state, {
|
|
2539
|
-
type: "
|
|
2540
|
-
|
|
2267
|
+
type: "info",
|
|
2268
|
+
message: `Tunnel disconnected (code: ${code}, reason: ${reason})`
|
|
2541
2269
|
});
|
|
2542
|
-
state.
|
|
2543
|
-
|
|
2270
|
+
emitAgentDisconnected(state.agentId, { code, reason });
|
|
2271
|
+
if (state.interactive) displayStatus(state);
|
|
2272
|
+
},
|
|
2273
|
+
onError: (error2) => {
|
|
2274
|
+
logActivity(state, { type: "error", error: error2 });
|
|
2275
|
+
if (state.interactive) displayStatus(state);
|
|
2276
|
+
},
|
|
2277
|
+
// Web traffic is proxied transparently; only note opencode is live.
|
|
2278
|
+
onResponse: () => {
|
|
2279
|
+
state.opencodeConnected = true;
|
|
2280
|
+
},
|
|
2281
|
+
onInfo: (message) => logActivity(state, { type: "info", message })
|
|
2544
2282
|
}
|
|
2545
|
-
}
|
|
2283
|
+
});
|
|
2284
|
+
state.connection = connection;
|
|
2285
|
+
try {
|
|
2286
|
+
await connection.connect();
|
|
2287
|
+
} catch (error2) {
|
|
2288
|
+
if (error2.message === "Unauthorized") tunnelSpinner?.fail("Unauthorized");
|
|
2289
|
+
throw error2;
|
|
2290
|
+
}
|
|
2546
2291
|
if (interactive && !state.json) {
|
|
2547
2292
|
displayStatus(state);
|
|
2548
2293
|
} else {
|
|
2549
|
-
log(state, "
|
|
2294
|
+
log(state, "Driving channel messages...");
|
|
2550
2295
|
}
|
|
2551
|
-
await
|
|
2552
|
-
await cleanup(state
|
|
2296
|
+
await driveChannels(state, channelDriver);
|
|
2297
|
+
await cleanup(state);
|
|
2553
2298
|
if (state.json) {
|
|
2554
2299
|
console.log(
|
|
2555
2300
|
JSON.stringify({
|
|
@@ -2563,7 +2308,7 @@ async function run(options) {
|
|
|
2563
2308
|
await shutdownTelemetry();
|
|
2564
2309
|
process.exit(0);
|
|
2565
2310
|
} catch (error2) {
|
|
2566
|
-
await cleanup(state
|
|
2311
|
+
await cleanup(state);
|
|
2567
2312
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
2568
2313
|
if (state.json) {
|
|
2569
2314
|
console.log(JSON.stringify({ status: "error", error: message }));
|
|
@@ -2581,10 +2326,16 @@ async function run(options) {
|
|
|
2581
2326
|
|
|
2582
2327
|
// src/index.ts
|
|
2583
2328
|
var program = new Command();
|
|
2584
|
-
program.name("evident").description("Run OpenCode locally and connect it to Evident").version("0.1.0").option(
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2329
|
+
program.name("evident").description("Run OpenCode locally and connect it to Evident").version("0.1.0").option(
|
|
2330
|
+
"--endpoint <url>",
|
|
2331
|
+
"Evident API base URL (default: production; e.g. http://localhost:3001)"
|
|
2332
|
+
).option("--tunnel <url>", "Tunnel WebSocket URL (default: production; e.g. ws://localhost:8787)").hook("preAction", (thisCommand) => {
|
|
2333
|
+
const { endpoint, tunnel } = thisCommand.opts();
|
|
2334
|
+
if (endpoint) {
|
|
2335
|
+
setEndpoint(endpoint);
|
|
2336
|
+
}
|
|
2337
|
+
if (tunnel) {
|
|
2338
|
+
setTunnelUrl(tunnel);
|
|
2588
2339
|
}
|
|
2589
2340
|
});
|
|
2590
2341
|
program.command("login").description("Authenticate with Evident").option("--token", "Use token-based authentication (for CI/CD)").option("--no-browser", "Do not open the browser automatically").action(login);
|