@evident-ai/cli 0.2.1-dev.121b962 → 0.2.1-dev.27d8cfe

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 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 WebSocket from "ws";
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.send(JSON.stringify({ type: "event_end", id: subscriptionId }));
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.send(JSON.stringify({ type: "event", id: subscriptionId, event }));
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.send(JSON.stringify({ type: "event_error", id: subscriptionId, error: message }));
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 WebSocket(url, {
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.sandbox_type !== "local" && agent.sandbox_type !== "github_actions") {
1414
+ if (agent.agent_type !== "local" && agent.agent_type !== "github_actions") {
1387
1415
  return {
1388
1416
  valid: false,
1389
- error: `Agent is type '${agent.sandbox_type}', must be 'local' or 'github_actions' for CLI connection`
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
- return { acquired: false, error: "Conversation already locked by another runner" };
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
- } catch {
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 idleStart = null;
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
- idleStart = null;
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
- if (idleStart === null) {
2112
- idleStart = Date.now();
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
- if (Date.now() - idleStart > state.idleTimeout * 1e3) {
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: "Idle timeout reached"
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").requiredOption("-a, --agent <id>", "Agent ID to connect to").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(
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,