@evident-ai/cli 0.2.1-dev.36013cd → 0.2.1-dev.4829075
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 +1 -1
- package/dist/index.js +94 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
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
|
|
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) {
|
|
@@ -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
|
-
|
|
1867
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2101
|
-
|
|
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
|
-
|
|
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:
|
|
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").
|
|
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,
|