@evident-ai/cli 0.2.1-dev.d55ec9b → 0.2.1-dev.de4207d

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 CHANGED
@@ -66,7 +66,7 @@ First, install OpenCode and start it in your project directory:
66
66
 
67
67
  ```bash
68
68
  # Install OpenCode
69
- npm install -g @opencode/cli
69
+ npm install -g opencode-ai
70
70
 
71
71
  # Start OpenCode server in your project
72
72
  cd ~/projects/my-app
package/dist/index.js CHANGED
@@ -898,7 +898,7 @@ async function promptOpenCodeInstall(interactive) {
898
898
  error: "OpenCode is not installed",
899
899
  install_url: OPENCODE_INSTALL_URL,
900
900
  install_commands: {
901
- npm: "npm install -g opencode",
901
+ npm: "npm install -g opencode-ai",
902
902
  curl: "curl -fsSL https://opencode.ai/install.sh | sh"
903
903
  }
904
904
  })
@@ -936,7 +936,7 @@ async function promptOpenCodeInstall(interactive) {
936
936
  console.log(chalk4.bold("Install OpenCode using one of these methods:"));
937
937
  blank();
938
938
  console.log(chalk4.dim(" # Option 1: Install via npm (recommended)"));
939
- console.log(` ${chalk4.cyan("npm install -g opencode")}`);
939
+ console.log(` ${chalk4.cyan("npm install -g opencode-ai")}`);
940
940
  blank();
941
941
  console.log(chalk4.dim(" # Option 2: Install via curl"));
942
942
  console.log(` ${chalk4.cyan("curl -fsSL https://opencode.ai/install.sh | sh")}`);
@@ -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) {
@@ -1787,7 +1824,7 @@ async function ensureOpenCodeRunning(state) {
1787
1824
  }
1788
1825
  if (!isOpenCodeInstalled()) {
1789
1826
  if (!state.interactive) {
1790
- throw new Error("OpenCode is not installed. Install it with: npm install -g opencode");
1827
+ throw new Error("OpenCode is not installed. Install it with: npm install -g opencode-ai");
1791
1828
  }
1792
1829
  const result = await promptOpenCodeInstall(true);
1793
1830
  if (result === "exit") {
@@ -1863,9 +1900,20 @@ Port ${state.port} is already in use.`));
1863
1900
  state.opencodeVersion = health.version ?? null;
1864
1901
  }
1865
1902
  } else {
1866
- throw new Error(
1867
- `OpenCode is not running on port ${state.port}. Start it with: opencode serve --port ${state.port}`
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})` : ""}`
1868
1914
  );
1915
+ state.opencodeConnected = true;
1916
+ state.opencodeVersion = health.version ?? null;
1869
1917
  }
1870
1918
  }
1871
1919
  var AUTH_EXPIRED_EXIT_CODE = 77;
@@ -1911,7 +1959,7 @@ function isNetworkError(error2) {
1911
1959
  return false;
1912
1960
  }
1913
1961
  async function processQueue(state, authHeader, triggerReconnect) {
1914
- let idleStart = null;
1962
+ let idlePolls = 0;
1915
1963
  let currentAuthHeader = authHeader;
1916
1964
  while (state.running) {
1917
1965
  if (state.reconnecting && state.reconnectPromise) {
@@ -1930,7 +1978,7 @@ async function processQueue(state, authHeader, triggerReconnect) {
1930
1978
  );
1931
1979
  state.consecutiveFetchFailures = 0;
1932
1980
  if (conversations.length > 0) {
1933
- idleStart = null;
1981
+ idlePolls = 0;
1934
1982
  for (const conv of conversations) {
1935
1983
  if (!state.running) break;
1936
1984
  if (!state.lockedConversations.has(conv.id)) {
@@ -1941,9 +1989,10 @@ async function processQueue(state, authHeader, triggerReconnect) {
1941
1989
  currentAuthHeader
1942
1990
  );
1943
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)` : "";
1944
1993
  logActivity(state, {
1945
1994
  type: "info",
1946
- 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}`
1947
1996
  });
1948
1997
  if (state.interactive) displayStatus(state);
1949
1998
  continue;
@@ -2097,25 +2146,35 @@ async function processQueue(state, authHeader, triggerReconnect) {
2097
2146
  }
2098
2147
  } else {
2099
2148
  if (state.idleTimeout !== null) {
2100
- if (idleStart === null) {
2101
- idleStart = Date.now();
2149
+ idlePolls++;
2150
+ if (idlePolls === 1) {
2102
2151
  logActivity(state, {
2103
2152
  type: "info",
2104
2153
  message: `Queue empty, waiting (timeout: ${state.idleTimeout}s)...`
2105
2154
  });
2106
2155
  if (state.interactive) displayStatus(state);
2107
2156
  }
2108
- 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) {
2109
2169
  logActivity(state, {
2110
2170
  type: "info",
2111
- message: "Idle timeout reached"
2171
+ message: `Releasing ${state.lockedConversations.size} lock(s) before idle exit...`
2112
2172
  });
2113
2173
  if (state.interactive) displayStatus(state);
2114
- break;
2115
2174
  }
2175
+ break;
2116
2176
  }
2117
2177
  }
2118
- await new Promise((resolve) => setTimeout(resolve, MESSAGE_POLL_INTERVAL_MS));
2119
2178
  } catch (error2) {
2120
2179
  if (error2 instanceof AuthenticationError) {
2121
2180
  const result = await handleAuthError(state, error2);
@@ -2161,7 +2220,7 @@ async function cleanup(state, authHeader) {
2161
2220
  clearInterval(state.lockHeartbeatTimer);
2162
2221
  state.lockHeartbeatTimer = null;
2163
2222
  }
2164
- if (authHeader && state.lockedConversations.size > 0) {
2223
+ if (authHeader && authHeader.trim() !== "" && state.lockedConversations.size > 0) {
2165
2224
  for (const convId of state.lockedConversations) {
2166
2225
  await releaseConversationLock(state.agentId, convId, state.lockCorrelationId, authHeader);
2167
2226
  }
@@ -2269,6 +2328,12 @@ async function run(options) {
2269
2328
  if (resolved.agent_id) {
2270
2329
  state.agentId = resolved.agent_id;
2271
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
+ }
2272
2337
  } else {
2273
2338
  printError(resolved.error || "Failed to resolve agent ID from key");
2274
2339
  process.exit(1);
@@ -2525,7 +2590,7 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
2525
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);
2526
2591
  program.command("logout").description("Remove stored credentials").action(logout);
2527
2592
  program.command("whoami").description("Show the currently logged in user").action(whoami);
2528
- 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(
2529
2594
  (options) => {
2530
2595
  run({
2531
2596
  agent: options.agent,