@evident-ai/cli 0.2.1-dev.121b962 → 0.2.1-dev.12f8147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +78 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1082,9 +1082,10 @@ async function sendMessageToOpenCode(port, sessionId, content, options, hooks, m
|
|
|
1082
1082
|
}
|
|
1083
1083
|
|
|
1084
1084
|
// src/lib/tunnel/connection.ts
|
|
1085
|
-
import
|
|
1085
|
+
import WebSocket3 from "ws";
|
|
1086
1086
|
|
|
1087
1087
|
// src/lib/tunnel/forwarding.ts
|
|
1088
|
+
import WebSocket from "ws";
|
|
1088
1089
|
var CHUNK_THRESHOLD = 512 * 1024;
|
|
1089
1090
|
var CHUNK_SIZE = 768 * 1024;
|
|
1090
1091
|
async function forwardToOpenCode(port, request) {
|
|
@@ -1096,7 +1097,10 @@ async function forwardToOpenCode(port, request) {
|
|
|
1096
1097
|
"Content-Type": "application/json",
|
|
1097
1098
|
...request.headers
|
|
1098
1099
|
},
|
|
1099
|
-
body: request.body ? JSON.stringify(request.body) : void 0
|
|
1100
|
+
body: request.body ? JSON.stringify(request.body) : void 0,
|
|
1101
|
+
// 90s timeout to match the relay's configured timeout_ms for session creation;
|
|
1102
|
+
// without this, a hanging OpenCode process holds the connection forever.
|
|
1103
|
+
signal: AbortSignal.timeout(9e4)
|
|
1100
1104
|
});
|
|
1101
1105
|
let body;
|
|
1102
1106
|
const contentType = response.headers.get("Content-Type");
|
|
@@ -1125,6 +1129,9 @@ async function forwardToOpenCode(port, request) {
|
|
|
1125
1129
|
}
|
|
1126
1130
|
}
|
|
1127
1131
|
function sendResponse(ws, requestId, response) {
|
|
1132
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1128
1135
|
const bodyStr = JSON.stringify(response.body ?? null);
|
|
1129
1136
|
const bodyBytes = Buffer.from(bodyStr, "utf-8");
|
|
1130
1137
|
if (bodyBytes.length < CHUNK_THRESHOLD) {
|
|
@@ -1140,7 +1147,13 @@ function sendResponse(ws, requestId, response) {
|
|
|
1140
1147
|
sendResponseAsChunks(ws, requestId, response, bodyBytes);
|
|
1141
1148
|
}
|
|
1142
1149
|
function sendResponseAsChunks(ws, requestId, response, bodyBytes) {
|
|
1150
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1143
1153
|
const chunks = splitIntoChunks(bodyBytes, CHUNK_SIZE);
|
|
1154
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1144
1157
|
ws.send(
|
|
1145
1158
|
JSON.stringify({
|
|
1146
1159
|
type: "response_start",
|
|
@@ -1154,6 +1167,9 @@ function sendResponseAsChunks(ws, requestId, response, bodyBytes) {
|
|
|
1154
1167
|
})
|
|
1155
1168
|
);
|
|
1156
1169
|
for (let i = 0; i < chunks.length; i++) {
|
|
1170
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1157
1173
|
ws.send(
|
|
1158
1174
|
JSON.stringify({
|
|
1159
1175
|
type: "response_chunk",
|
|
@@ -1163,6 +1179,9 @@ function sendResponseAsChunks(ws, requestId, response, bodyBytes) {
|
|
|
1163
1179
|
})
|
|
1164
1180
|
);
|
|
1165
1181
|
}
|
|
1182
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1166
1185
|
ws.send(
|
|
1167
1186
|
JSON.stringify({
|
|
1168
1187
|
type: "response_end",
|
|
@@ -1179,6 +1198,7 @@ function splitIntoChunks(data, chunkSize) {
|
|
|
1179
1198
|
}
|
|
1180
1199
|
|
|
1181
1200
|
// src/lib/tunnel/events.ts
|
|
1201
|
+
import WebSocket2 from "ws";
|
|
1182
1202
|
async function subscribeToOpenCodeEvents(port, subscriptionId, ws, abortController) {
|
|
1183
1203
|
const url = `http://localhost:${port}/event`;
|
|
1184
1204
|
try {
|
|
@@ -1192,13 +1212,18 @@ async function subscribeToOpenCodeEvents(port, subscriptionId, ws, abortControll
|
|
|
1192
1212
|
if (!response.body) {
|
|
1193
1213
|
throw new Error("No response body");
|
|
1194
1214
|
}
|
|
1215
|
+
if (ws.readyState === WebSocket2.OPEN) {
|
|
1216
|
+
ws.send(JSON.stringify({ type: "subscription_connected", id: subscriptionId }));
|
|
1217
|
+
}
|
|
1195
1218
|
const reader = response.body.getReader();
|
|
1196
1219
|
const decoder = new TextDecoder();
|
|
1197
1220
|
let buffer = "";
|
|
1198
1221
|
while (true) {
|
|
1199
1222
|
const { done, value } = await reader.read();
|
|
1200
1223
|
if (done) {
|
|
1201
|
-
ws.
|
|
1224
|
+
if (ws.readyState === WebSocket2.OPEN) {
|
|
1225
|
+
ws.send(JSON.stringify({ type: "event_end", id: subscriptionId }));
|
|
1226
|
+
}
|
|
1202
1227
|
break;
|
|
1203
1228
|
}
|
|
1204
1229
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -1208,7 +1233,9 @@ async function subscribeToOpenCodeEvents(port, subscriptionId, ws, abortControll
|
|
|
1208
1233
|
if (line.startsWith("data: ")) {
|
|
1209
1234
|
try {
|
|
1210
1235
|
const event = JSON.parse(line.slice(6));
|
|
1211
|
-
ws.
|
|
1236
|
+
if (ws.readyState === WebSocket2.OPEN) {
|
|
1237
|
+
ws.send(JSON.stringify({ type: "event", id: subscriptionId, event }));
|
|
1238
|
+
}
|
|
1212
1239
|
} catch {
|
|
1213
1240
|
}
|
|
1214
1241
|
}
|
|
@@ -1219,7 +1246,9 @@ async function subscribeToOpenCodeEvents(port, subscriptionId, ws, abortControll
|
|
|
1219
1246
|
return;
|
|
1220
1247
|
}
|
|
1221
1248
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1222
|
-
ws.
|
|
1249
|
+
if (ws.readyState === WebSocket2.OPEN) {
|
|
1250
|
+
ws.send(JSON.stringify({ type: "event_error", id: subscriptionId, error: message }));
|
|
1251
|
+
}
|
|
1223
1252
|
throw error2;
|
|
1224
1253
|
}
|
|
1225
1254
|
}
|
|
@@ -1248,7 +1277,7 @@ function connectTunnel(options) {
|
|
|
1248
1277
|
const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
|
|
1249
1278
|
const activeEventSubscriptions = /* @__PURE__ */ new Map();
|
|
1250
1279
|
return new Promise((resolve, reject) => {
|
|
1251
|
-
const ws = new
|
|
1280
|
+
const ws = new WebSocket3(url, {
|
|
1252
1281
|
headers: {
|
|
1253
1282
|
Authorization: authHeader
|
|
1254
1283
|
}
|
|
@@ -1270,8 +1299,7 @@ function connectTunnel(options) {
|
|
|
1270
1299
|
onConnected?.(connectedAgentId);
|
|
1271
1300
|
resolve({
|
|
1272
1301
|
ws,
|
|
1273
|
-
close: () => ws.close(1e3, "CLI shutdown")
|
|
1274
|
-
activeEventSubscriptions
|
|
1302
|
+
close: () => ws.close(1e3, "CLI shutdown")
|
|
1275
1303
|
});
|
|
1276
1304
|
break;
|
|
1277
1305
|
}
|
|
@@ -1383,10 +1411,10 @@ async function getAgentInfo(agentId, authHeader) {
|
|
|
1383
1411
|
return { valid: false, error: `API error: ${response.status}` };
|
|
1384
1412
|
}
|
|
1385
1413
|
const agent = await response.json();
|
|
1386
|
-
if (agent.
|
|
1414
|
+
if (agent.agent_type !== "local" && agent.agent_type !== "github_actions") {
|
|
1387
1415
|
return {
|
|
1388
1416
|
valid: false,
|
|
1389
|
-
error: `Agent is type '${agent.
|
|
1417
|
+
error: `Agent is type '${agent.agent_type}', must be 'local' or 'github_actions' for CLI connection`
|
|
1390
1418
|
};
|
|
1391
1419
|
}
|
|
1392
1420
|
return { valid: true, agent };
|
|
@@ -1502,7 +1530,12 @@ async function acquireConversationLock(agentId, conversationId, correlationId, a
|
|
|
1502
1530
|
});
|
|
1503
1531
|
checkAuthResponse(response, "acquiring conversation lock");
|
|
1504
1532
|
if (response.status === 409) {
|
|
1505
|
-
|
|
1533
|
+
const body = await response.json();
|
|
1534
|
+
return {
|
|
1535
|
+
acquired: false,
|
|
1536
|
+
error: body.message ?? "Conversation already locked by another runner",
|
|
1537
|
+
expires_at: body.expires_at
|
|
1538
|
+
};
|
|
1506
1539
|
}
|
|
1507
1540
|
if (!response.ok) {
|
|
1508
1541
|
return { acquired: false, error: `Failed to acquire lock: HTTP ${response.status}` };
|
|
@@ -1532,14 +1565,18 @@ async function extendConversationLock(agentId, conversationId, correlationId, au
|
|
|
1532
1565
|
async function releaseConversationLock(agentId, conversationId, correlationId, authHeader) {
|
|
1533
1566
|
const apiUrl = getApiUrlConfig();
|
|
1534
1567
|
try {
|
|
1535
|
-
await fetch(
|
|
1568
|
+
const response = await fetch(
|
|
1536
1569
|
`${apiUrl}/agents/${agentId}/threads/${conversationId}/lock?correlation_id=${encodeURIComponent(correlationId)}`,
|
|
1537
1570
|
{
|
|
1538
1571
|
method: "DELETE",
|
|
1539
1572
|
headers: { Authorization: authHeader }
|
|
1540
1573
|
}
|
|
1541
1574
|
);
|
|
1542
|
-
|
|
1575
|
+
if (!response.ok) {
|
|
1576
|
+
console.error(`[lock] Failed to release lock on ${conversationId}: HTTP ${response.status}`);
|
|
1577
|
+
}
|
|
1578
|
+
} catch (error2) {
|
|
1579
|
+
console.error(`[lock] Failed to release lock on ${conversationId}: ${error2}`);
|
|
1543
1580
|
}
|
|
1544
1581
|
}
|
|
1545
1582
|
async function updateConversationSession(agentId, conversationId, sessionId, authHeader) {
|
|
@@ -1922,7 +1959,7 @@ function isNetworkError(error2) {
|
|
|
1922
1959
|
return false;
|
|
1923
1960
|
}
|
|
1924
1961
|
async function processQueue(state, authHeader, triggerReconnect) {
|
|
1925
|
-
let
|
|
1962
|
+
let idlePolls = 0;
|
|
1926
1963
|
let currentAuthHeader = authHeader;
|
|
1927
1964
|
while (state.running) {
|
|
1928
1965
|
if (state.reconnecting && state.reconnectPromise) {
|
|
@@ -1941,7 +1978,7 @@ async function processQueue(state, authHeader, triggerReconnect) {
|
|
|
1941
1978
|
);
|
|
1942
1979
|
state.consecutiveFetchFailures = 0;
|
|
1943
1980
|
if (conversations.length > 0) {
|
|
1944
|
-
|
|
1981
|
+
idlePolls = 0;
|
|
1945
1982
|
for (const conv of conversations) {
|
|
1946
1983
|
if (!state.running) break;
|
|
1947
1984
|
if (!state.lockedConversations.has(conv.id)) {
|
|
@@ -1952,9 +1989,10 @@ async function processQueue(state, authHeader, triggerReconnect) {
|
|
|
1952
1989
|
currentAuthHeader
|
|
1953
1990
|
);
|
|
1954
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)` : "";
|
|
1955
1993
|
logActivity(state, {
|
|
1956
1994
|
type: "info",
|
|
1957
|
-
message: `Conversation ${conv.id.slice(0, 8)} locked by another runner \u2014 skipping`
|
|
1995
|
+
message: `Conversation ${conv.id.slice(0, 8)} locked by another runner \u2014 skipping${ttlMessage}`
|
|
1958
1996
|
});
|
|
1959
1997
|
if (state.interactive) displayStatus(state);
|
|
1960
1998
|
continue;
|
|
@@ -2108,25 +2146,35 @@ async function processQueue(state, authHeader, triggerReconnect) {
|
|
|
2108
2146
|
}
|
|
2109
2147
|
} else {
|
|
2110
2148
|
if (state.idleTimeout !== null) {
|
|
2111
|
-
|
|
2112
|
-
|
|
2149
|
+
idlePolls++;
|
|
2150
|
+
if (idlePolls === 1) {
|
|
2113
2151
|
logActivity(state, {
|
|
2114
2152
|
type: "info",
|
|
2115
2153
|
message: `Queue empty, waiting (timeout: ${state.idleTimeout}s)...`
|
|
2116
2154
|
});
|
|
2117
2155
|
if (state.interactive) displayStatus(state);
|
|
2118
2156
|
}
|
|
2119
|
-
|
|
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) {
|
|
2120
2169
|
logActivity(state, {
|
|
2121
2170
|
type: "info",
|
|
2122
|
-
message:
|
|
2171
|
+
message: `Releasing ${state.lockedConversations.size} lock(s) before idle exit...`
|
|
2123
2172
|
});
|
|
2124
2173
|
if (state.interactive) displayStatus(state);
|
|
2125
|
-
break;
|
|
2126
2174
|
}
|
|
2175
|
+
break;
|
|
2127
2176
|
}
|
|
2128
2177
|
}
|
|
2129
|
-
await new Promise((resolve) => setTimeout(resolve, MESSAGE_POLL_INTERVAL_MS));
|
|
2130
2178
|
} catch (error2) {
|
|
2131
2179
|
if (error2 instanceof AuthenticationError) {
|
|
2132
2180
|
const result = await handleAuthError(state, error2);
|
|
@@ -2172,7 +2220,7 @@ async function cleanup(state, authHeader) {
|
|
|
2172
2220
|
clearInterval(state.lockHeartbeatTimer);
|
|
2173
2221
|
state.lockHeartbeatTimer = null;
|
|
2174
2222
|
}
|
|
2175
|
-
if (authHeader && state.lockedConversations.size > 0) {
|
|
2223
|
+
if (authHeader && authHeader.trim() !== "" && state.lockedConversations.size > 0) {
|
|
2176
2224
|
for (const convId of state.lockedConversations) {
|
|
2177
2225
|
await releaseConversationLock(state.agentId, convId, state.lockCorrelationId, authHeader);
|
|
2178
2226
|
}
|
|
@@ -2280,6 +2328,12 @@ async function run(options) {
|
|
|
2280
2328
|
if (resolved.agent_id) {
|
|
2281
2329
|
state.agentId = resolved.agent_id;
|
|
2282
2330
|
log(state, `Resolved agent ID from key: ${state.agentId}`);
|
|
2331
|
+
if (state.interactive && !state.json) {
|
|
2332
|
+
logActivity(state, {
|
|
2333
|
+
type: "info",
|
|
2334
|
+
message: `Agent ID resolved from key: ${state.agentId}`
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2283
2337
|
} else {
|
|
2284
2338
|
printError(resolved.error || "Failed to resolve agent ID from key");
|
|
2285
2339
|
process.exit(1);
|
|
@@ -2536,7 +2590,7 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
|
|
|
2536
2590
|
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);
|
|
2537
2591
|
program.command("logout").description("Remove stored credentials").action(logout);
|
|
2538
2592
|
program.command("whoami").description("Show the currently logged in user").action(whoami);
|
|
2539
|
-
program.command("run").description("Connect to Evident and process messages").
|
|
2593
|
+
program.command("run").description("Connect to Evident and process messages").option("-a, --agent [id]", "Agent ID to connect to (optional when EVIDENT_AGENT_KEY is set)").option("-p, --port <port>", "OpenCode port (default: 4096)", "4096").option("-v, --verbose", "Show detailed request/response information").option("-c, --conversation <id>", "Process only this specific conversation").option("--idle-timeout <seconds>", "Exit after N seconds idle").option("--json", "Output in JSON format").action(
|
|
2540
2594
|
(options) => {
|
|
2541
2595
|
run({
|
|
2542
2596
|
agent: options.agent,
|