@evident-ai/cli 0.1.6 → 0.2.0

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
@@ -1,4 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ clearCredentials,
4
+ getApiUrlConfig,
5
+ getCredentials,
6
+ getTunnelUrlConfig,
7
+ setCredentials,
8
+ setEnvironment
9
+ } from "./chunk-MWOWXSOP.js";
2
10
 
3
11
  // src/index.ts
4
12
  import { Command } from "commander";
@@ -8,79 +16,6 @@ import open from "open";
8
16
  import ora from "ora";
9
17
  import chalk2 from "chalk";
10
18
 
11
- // src/lib/config.ts
12
- import Conf from "conf";
13
- import { homedir } from "os";
14
- import { join } from "path";
15
- var environmentPresets = {
16
- local: {
17
- apiUrl: "http://localhost:3000/v1",
18
- tunnelUrl: "ws://localhost:8787"
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
- }
29
- };
30
- var defaults = environmentPresets.production;
31
- var currentEnvironment = "production";
32
- function setEnvironment(env) {
33
- currentEnvironment = env;
34
- }
35
- function getEnvironment() {
36
- const envVar = process.env.EVIDENT_ENV;
37
- if (envVar && environmentPresets[envVar]) {
38
- return envVar;
39
- }
40
- return currentEnvironment;
41
- }
42
- function getEnvConfig() {
43
- return environmentPresets[getEnvironment()];
44
- }
45
- function getApiUrl() {
46
- return process.env.EVIDENT_API_URL ?? getEnvConfig().apiUrl;
47
- }
48
- function getTunnelUrl() {
49
- return process.env.EVIDENT_TUNNEL_URL ?? getEnvConfig().tunnelUrl;
50
- }
51
- var config = new Conf({
52
- projectName: "evident",
53
- projectSuffix: "",
54
- defaults
55
- });
56
- var credentials = new Conf({
57
- projectName: "evident",
58
- projectSuffix: "",
59
- configName: "credentials",
60
- defaults: {}
61
- });
62
- function getApiUrlConfig() {
63
- return getApiUrl();
64
- }
65
- function getTunnelUrlConfig() {
66
- return getTunnelUrl();
67
- }
68
- function getCredentials() {
69
- return {
70
- token: credentials.get("token"),
71
- user: credentials.get("user"),
72
- expiresAt: credentials.get("expiresAt")
73
- };
74
- }
75
- function setCredentials(creds) {
76
- if (creds.token) credentials.set("token", creds.token);
77
- if (creds.user) credentials.set("user", creds.user);
78
- if (creds.expiresAt) credentials.set("expiresAt", creds.expiresAt);
79
- }
80
- function clearCredentials() {
81
- credentials.clear();
82
- }
83
-
84
19
  // src/lib/api.ts
85
20
  var ApiClient = class {
86
21
  baseUrl;
@@ -188,15 +123,15 @@ async function getKeytar() {
188
123
  return null;
189
124
  }
190
125
  }
191
- async function storeToken(credentials2) {
126
+ async function storeToken(credentials) {
192
127
  const keytar = await getKeytar();
193
128
  if (keytar) {
194
- await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(credentials2));
129
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(credentials));
195
130
  } else {
196
131
  setCredentials({
197
- token: credentials2.token,
198
- user: credentials2.user,
199
- expiresAt: credentials2.expiresAt
132
+ token: credentials.token,
133
+ user: credentials.user,
134
+ expiresAt: credentials.expiresAt
200
135
  });
201
136
  }
202
137
  }
@@ -397,8 +332,8 @@ async function login(options) {
397
332
 
398
333
  // src/commands/logout.ts
399
334
  async function logout() {
400
- const credentials2 = await getToken();
401
- if (!credentials2) {
335
+ const credentials = await getToken();
336
+ if (!credentials) {
402
337
  printWarning("You are not logged in.");
403
338
  return;
404
339
  }
@@ -409,16 +344,16 @@ async function logout() {
409
344
  // src/commands/whoami.ts
410
345
  import chalk3 from "chalk";
411
346
  async function whoami() {
412
- const credentials2 = await getToken();
413
- if (!credentials2) {
347
+ const credentials = await getToken();
348
+ if (!credentials) {
414
349
  printError("Not logged in. Run the `login` command to authenticate.");
415
350
  process.exit(1);
416
351
  }
417
352
  blank();
418
- console.log(keyValue("User", chalk3.bold(credentials2.user.email)));
419
- console.log(keyValue("User ID", credentials2.user.id));
420
- if (credentials2.expiresAt) {
421
- const expiresAt = new Date(credentials2.expiresAt);
353
+ console.log(keyValue("User", chalk3.bold(credentials.user.email)));
354
+ console.log(keyValue("User ID", credentials.user.id));
355
+ if (credentials.expiresAt) {
356
+ const expiresAt = new Date(credentials.expiresAt);
422
357
  const now = /* @__PURE__ */ new Date();
423
358
  if (expiresAt < now) {
424
359
  console.log(keyValue("Status", chalk3.red("Token expired")));
@@ -436,6 +371,8 @@ async function whoami() {
436
371
  import WebSocket from "ws";
437
372
  import chalk4 from "chalk";
438
373
  import ora2 from "ora";
374
+ import { execSync, spawn } from "child_process";
375
+ import { select } from "@inquirer/prompts";
439
376
 
440
377
  // src/lib/telemetry.ts
441
378
  var CLI_VERSION = process.env.npm_package_version || "unknown";
@@ -479,8 +416,8 @@ async function flushEvents() {
479
416
  flushTimeout = null;
480
417
  }
481
418
  try {
482
- const credentials2 = await getToken();
483
- if (!credentials2) {
419
+ const credentials = await getToken();
420
+ if (!credentials) {
484
421
  return;
485
422
  }
486
423
  const apiUrl = getApiUrlConfig();
@@ -491,7 +428,7 @@ async function flushEvents() {
491
428
  method: "POST",
492
429
  headers: {
493
430
  "Content-Type": "application/json",
494
- Authorization: `Bearer ${credentials2.token}`
431
+ Authorization: `Bearer ${credentials.token}`
495
432
  },
496
433
  body: JSON.stringify({
497
434
  events,
@@ -551,6 +488,173 @@ var EventTypes = {
551
488
  var MAX_RECONNECT_DELAY = 3e4;
552
489
  var BASE_RECONNECT_DELAY = 500;
553
490
  var MAX_ACTIVITY_LOG_ENTRIES = 10;
491
+ var CHUNK_THRESHOLD = 512 * 1024;
492
+ var CHUNK_SIZE = 768 * 1024;
493
+ var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
494
+ function getProcessCwd(pid) {
495
+ const platform = process.platform;
496
+ try {
497
+ if (platform === "darwin") {
498
+ const output = execSync(`lsof -a -p ${pid} -d cwd -Fn 2>/dev/null`, {
499
+ encoding: "utf-8",
500
+ stdio: ["pipe", "pipe", "pipe"]
501
+ }).trim();
502
+ const lines = output.split("\n");
503
+ for (const line of lines) {
504
+ if (line.startsWith("n") && !line.startsWith("n ")) {
505
+ return line.slice(1);
506
+ }
507
+ }
508
+ } else if (platform === "linux") {
509
+ const output = execSync(`readlink /proc/${pid}/cwd 2>/dev/null`, {
510
+ encoding: "utf-8",
511
+ stdio: ["pipe", "pipe", "pipe"]
512
+ }).trim();
513
+ if (output) return output;
514
+ }
515
+ } catch {
516
+ }
517
+ return void 0;
518
+ }
519
+ function isPortInUse(port) {
520
+ const platform = process.platform;
521
+ try {
522
+ if (platform === "darwin" || platform === "linux") {
523
+ execSync(`lsof -i :${port} -sTCP:LISTEN 2>/dev/null`, {
524
+ encoding: "utf-8",
525
+ stdio: ["pipe", "pipe", "pipe"]
526
+ });
527
+ return true;
528
+ }
529
+ } catch {
530
+ }
531
+ return false;
532
+ }
533
+ function findAvailablePort(startPort, maxAttempts = 10) {
534
+ for (let i = 0; i < maxAttempts; i++) {
535
+ const port = startPort + i;
536
+ if (!isPortInUse(port)) {
537
+ return port;
538
+ }
539
+ }
540
+ return null;
541
+ }
542
+ function findOpenCodeProcesses() {
543
+ const instances = [];
544
+ try {
545
+ const platform = process.platform;
546
+ if (platform === "darwin" || platform === "linux") {
547
+ let pids = [];
548
+ try {
549
+ const pgrepOutput = execSync('pgrep -f "opencode serve|opencode-serve"', {
550
+ encoding: "utf-8",
551
+ stdio: ["pipe", "pipe", "pipe"]
552
+ }).trim();
553
+ if (pgrepOutput) {
554
+ pids = pgrepOutput.split("\n").map((p) => parseInt(p.trim(), 10)).filter((p) => !isNaN(p));
555
+ }
556
+ } catch {
557
+ try {
558
+ const psOutput = execSync('ps aux | grep -E "opencode (serve|--port)" | grep -v grep', {
559
+ encoding: "utf-8",
560
+ stdio: ["pipe", "pipe", "pipe"]
561
+ }).trim();
562
+ if (psOutput) {
563
+ for (const line of psOutput.split("\n")) {
564
+ const parts = line.trim().split(/\s+/);
565
+ if (parts.length >= 2) {
566
+ const pid = parseInt(parts[1], 10);
567
+ if (!isNaN(pid)) pids.push(pid);
568
+ }
569
+ }
570
+ }
571
+ } catch {
572
+ }
573
+ }
574
+ for (const pid of pids) {
575
+ try {
576
+ const lsofOutput = execSync(`lsof -Pan -p ${pid} -i TCP -sTCP:LISTEN 2>/dev/null`, {
577
+ encoding: "utf-8",
578
+ stdio: ["pipe", "pipe", "pipe"]
579
+ }).trim();
580
+ for (const line of lsofOutput.split("\n")) {
581
+ const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
582
+ if (portMatch) {
583
+ const port = parseInt(portMatch[1], 10);
584
+ if (!isNaN(port) && !instances.some((i) => i.port === port)) {
585
+ const cwd = getProcessCwd(pid);
586
+ instances.push({ pid, port, cwd });
587
+ }
588
+ }
589
+ }
590
+ } catch {
591
+ }
592
+ }
593
+ }
594
+ } catch {
595
+ }
596
+ return instances;
597
+ }
598
+ async function scanPortsForOpenCode() {
599
+ const instances = [];
600
+ const checks = OPENCODE_PORT_RANGE.map(async (port) => {
601
+ const health = await checkOpenCodeHealth(port);
602
+ if (health.healthy) {
603
+ let pid = 0;
604
+ try {
605
+ const lsofOutput = execSync(`lsof -ti :${port} -sTCP:LISTEN 2>/dev/null`, {
606
+ encoding: "utf-8",
607
+ stdio: ["pipe", "pipe", "pipe"]
608
+ }).trim();
609
+ if (lsofOutput) {
610
+ pid = parseInt(lsofOutput.split("\n")[0], 10) || 0;
611
+ }
612
+ } catch {
613
+ }
614
+ const cwd = pid ? getProcessCwd(pid) : void 0;
615
+ return { pid, port, cwd, version: health.version };
616
+ }
617
+ return null;
618
+ });
619
+ const results = await Promise.all(checks);
620
+ for (const result of results) {
621
+ if (result) {
622
+ instances.push(result);
623
+ }
624
+ }
625
+ return instances;
626
+ }
627
+ async function checkOpenCodeHealth(port) {
628
+ try {
629
+ const response = await fetch(`http://localhost:${port}/health`, {
630
+ signal: AbortSignal.timeout(2e3)
631
+ // 2 second timeout
632
+ });
633
+ if (!response.ok) {
634
+ return { healthy: false, error: `HTTP ${response.status}` };
635
+ }
636
+ const data = await response.json().catch(() => ({}));
637
+ return { healthy: true, version: data.version };
638
+ } catch (error2) {
639
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
640
+ return { healthy: false, error: message };
641
+ }
642
+ }
643
+ async function findHealthyOpenCodeInstances() {
644
+ const processes = findOpenCodeProcesses();
645
+ const healthy = [];
646
+ for (const proc of processes) {
647
+ const health = await checkOpenCodeHealth(proc.port);
648
+ if (health.healthy) {
649
+ healthy.push({ ...proc, version: health.version });
650
+ }
651
+ }
652
+ if (healthy.length === 0) {
653
+ const scanned = await scanPortsForOpenCode();
654
+ return scanned;
655
+ }
656
+ return healthy;
657
+ }
554
658
  function logActivity(state, entry) {
555
659
  const fullEntry = {
556
660
  ...entry,
@@ -603,38 +707,76 @@ function colorizeStatus(status) {
603
707
  }
604
708
  return status.toString();
605
709
  }
710
+ var ANSI = {
711
+ saveCursor: "\x1B[s",
712
+ restoreCursor: "\x1B[u",
713
+ clearToEnd: "\x1B[J",
714
+ moveTo: (row) => `\x1B[${row};1H`,
715
+ moveUp: (n) => `\x1B[${n}A`
716
+ };
717
+ var STATUS_DISPLAY_HEIGHT = 20;
606
718
  function displayStatus(state) {
607
- console.clear();
608
- console.log(chalk4.bold("Evident Tunnel"));
609
- console.log(chalk4.dim("\u2500".repeat(60)));
610
- blank();
719
+ const lines = [];
720
+ lines.push(chalk4.bold("Evident Tunnel"));
721
+ lines.push(chalk4.dim("\u2500".repeat(60)));
722
+ lines.push("");
723
+ if (state.sandboxName) {
724
+ lines.push(` Sandbox: ${state.sandboxName}`);
725
+ }
726
+ lines.push(` ID: ${state.sandboxId ?? "Unknown"}`);
727
+ lines.push("");
611
728
  if (state.connected) {
612
- console.log(` ${chalk4.green("\u25CF")} Status: ${chalk4.green("Connected")}`);
613
- console.log(` Sandbox: ${state.sandboxId ?? "Unknown"}`);
614
- if (state.sandboxName) {
615
- console.log(` Name: ${state.sandboxName}`);
616
- }
729
+ lines.push(` ${chalk4.green("\u25CF")} Tunnel: ${chalk4.green("Connected to Evident")}`);
617
730
  } else {
618
- console.log(` ${chalk4.yellow("\u25CB")} Status: ${chalk4.yellow("Reconnecting...")}`);
619
731
  if (state.reconnectAttempt > 0) {
620
- console.log(` Attempt: ${state.reconnectAttempt}`);
732
+ lines.push(
733
+ ` ${chalk4.yellow("\u25CB")} Tunnel: ${chalk4.yellow(`Reconnecting... (attempt ${state.reconnectAttempt})`)}`
734
+ );
735
+ } else {
736
+ lines.push(` ${chalk4.yellow("\u25CB")} Tunnel: ${chalk4.yellow("Connecting...")}`);
621
737
  }
622
738
  }
623
- blank();
739
+ if (state.opencodeConnected) {
740
+ const version = state.opencodeVersion ? ` (v${state.opencodeVersion})` : "";
741
+ lines.push(` ${chalk4.green("\u25CF")} OpenCode: ${chalk4.green(`Running${version}`)}`);
742
+ } else {
743
+ lines.push(` ${chalk4.red("\u25CB")} OpenCode: ${chalk4.red("Not connected")}`);
744
+ }
745
+ lines.push("");
624
746
  if (state.activityLog.length > 0) {
625
- console.log(chalk4.bold(" Activity:"));
747
+ lines.push(chalk4.bold(" Activity:"));
626
748
  for (const entry of state.activityLog) {
627
- console.log(formatActivityEntry(entry, state.verbose));
749
+ lines.push(formatActivityEntry(entry, state.verbose));
628
750
  }
629
751
  } else {
630
- console.log(chalk4.dim(" No activity yet. Waiting for requests..."));
752
+ lines.push(chalk4.dim(" No activity yet. Waiting for requests..."));
631
753
  }
632
- blank();
633
- console.log(chalk4.dim("\u2500".repeat(60)));
754
+ lines.push("");
755
+ lines.push(chalk4.dim("\u2500".repeat(60)));
634
756
  if (state.verbose) {
635
- console.log(chalk4.dim(" Verbose mode: ON (request/response bodies will be logged)"));
757
+ lines.push(chalk4.dim(" Verbose mode: ON (request/response bodies will be logged)"));
758
+ }
759
+ lines.push(chalk4.dim(" Press Ctrl+C to disconnect"));
760
+ while (lines.length < STATUS_DISPLAY_HEIGHT) {
761
+ lines.push("");
762
+ }
763
+ if (!state.displayInitialized) {
764
+ console.log("");
765
+ console.log(chalk4.dim("\u2550".repeat(60)));
766
+ console.log("");
767
+ for (const line of lines) {
768
+ console.log(line);
769
+ }
770
+ state.displayInitialized = true;
771
+ } else {
772
+ process.stdout.write(ANSI.moveUp(STATUS_DISPLAY_HEIGHT + 3));
773
+ console.log(chalk4.dim("\u2550".repeat(60)));
774
+ console.log("");
775
+ for (const line of lines) {
776
+ process.stdout.write("\x1B[2K");
777
+ console.log(line);
778
+ }
636
779
  }
637
- console.log(chalk4.dim(" Press Ctrl+C to disconnect"));
638
780
  }
639
781
  function displayError(_state, error2, details) {
640
782
  blank();
@@ -715,13 +857,23 @@ async function forwardToOpenCode(port, request, requestId, state) {
715
857
  });
716
858
  let body;
717
859
  const contentType = response.headers.get("Content-Type");
718
- if (contentType?.includes("application/json")) {
719
- body = await response.json();
860
+ const text = await response.text();
861
+ if (!text || text.length === 0) {
862
+ body = null;
863
+ } else if (contentType?.includes("application/json")) {
864
+ try {
865
+ body = JSON.parse(text);
866
+ } catch {
867
+ body = text;
868
+ }
720
869
  } else {
721
- body = await response.text();
870
+ body = text;
722
871
  }
723
872
  const durationMs = Date.now() - startTime;
724
873
  state.pendingRequests.delete(requestId);
874
+ if (!state.opencodeConnected) {
875
+ state.opencodeConnected = true;
876
+ }
725
877
  const lastEntry = state.activityLog[state.activityLog.length - 1];
726
878
  if (lastEntry && lastEntry.requestId === requestId) {
727
879
  lastEntry.type = "response";
@@ -762,6 +914,7 @@ async function forwardToOpenCode(port, request, requestId, state) {
762
914
  const message = error2 instanceof Error ? error2.message : "Unknown error";
763
915
  const durationMs = Date.now() - startTime;
764
916
  state.pendingRequests.delete(requestId);
917
+ state.opencodeConnected = false;
765
918
  logActivity(state, {
766
919
  type: "error",
767
920
  method: request.method,
@@ -788,6 +941,123 @@ async function forwardToOpenCode(port, request, requestId, state) {
788
941
  };
789
942
  }
790
943
  }
944
+ async function subscribeToOpenCodeEvents(port, subscriptionId, ws, state) {
945
+ const url = `http://localhost:${port}/event`;
946
+ logActivity(state, {
947
+ type: "info",
948
+ message: `Starting event subscription ${subscriptionId.slice(0, 8)}`
949
+ });
950
+ displayStatus(state);
951
+ const abortController = new AbortController();
952
+ state.activeEventSubscriptions.set(subscriptionId, abortController);
953
+ try {
954
+ const response = await fetch(url, {
955
+ headers: { Accept: "text/event-stream" },
956
+ signal: abortController.signal
957
+ });
958
+ if (!response.ok) {
959
+ throw new Error(`Failed to connect to OpenCode events: ${response.status}`);
960
+ }
961
+ if (!response.body) {
962
+ throw new Error("No response body");
963
+ }
964
+ const reader = response.body.getReader();
965
+ const decoder = new TextDecoder();
966
+ let buffer = "";
967
+ while (true) {
968
+ const { done, value } = await reader.read();
969
+ if (done) {
970
+ ws.send(JSON.stringify({ type: "event_end", id: subscriptionId }));
971
+ break;
972
+ }
973
+ buffer += decoder.decode(value, { stream: true });
974
+ const lines = buffer.split("\n");
975
+ buffer = lines.pop() || "";
976
+ for (const line of lines) {
977
+ if (line.startsWith("data: ")) {
978
+ try {
979
+ const event = JSON.parse(line.slice(6));
980
+ ws.send(JSON.stringify({ type: "event", id: subscriptionId, event }));
981
+ } catch {
982
+ }
983
+ }
984
+ }
985
+ }
986
+ } catch (error2) {
987
+ if (abortController.signal.aborted) {
988
+ return;
989
+ }
990
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
991
+ logActivity(state, {
992
+ type: "error",
993
+ error: `Event subscription failed: ${message}`
994
+ });
995
+ displayStatus(state);
996
+ ws.send(JSON.stringify({ type: "event_error", id: subscriptionId, error: message }));
997
+ } finally {
998
+ state.activeEventSubscriptions.delete(subscriptionId);
999
+ }
1000
+ }
1001
+ function cancelEventSubscription(subscriptionId, state) {
1002
+ const controller = state.activeEventSubscriptions.get(subscriptionId);
1003
+ if (controller) {
1004
+ controller.abort();
1005
+ state.activeEventSubscriptions.delete(subscriptionId);
1006
+ }
1007
+ }
1008
+ function sendResponse(ws, requestId, response) {
1009
+ const bodyStr = JSON.stringify(response.body ?? null);
1010
+ const bodyBytes = Buffer.from(bodyStr, "utf-8");
1011
+ if (bodyBytes.length < CHUNK_THRESHOLD) {
1012
+ ws.send(
1013
+ JSON.stringify({
1014
+ type: "response",
1015
+ id: requestId,
1016
+ payload: response
1017
+ })
1018
+ );
1019
+ return;
1020
+ }
1021
+ sendResponseAsChunks(ws, requestId, response, bodyBytes);
1022
+ }
1023
+ function sendResponseAsChunks(ws, requestId, response, bodyBytes) {
1024
+ const chunks = splitIntoChunks(bodyBytes, CHUNK_SIZE);
1025
+ ws.send(
1026
+ JSON.stringify({
1027
+ type: "response_start",
1028
+ id: requestId,
1029
+ total_chunks: chunks.length,
1030
+ total_size: bodyBytes.length,
1031
+ payload: {
1032
+ status: response.status,
1033
+ headers: response.headers
1034
+ }
1035
+ })
1036
+ );
1037
+ for (let i = 0; i < chunks.length; i++) {
1038
+ ws.send(
1039
+ JSON.stringify({
1040
+ type: "response_chunk",
1041
+ id: requestId,
1042
+ chunk_index: i,
1043
+ data: chunks[i].toString("base64")
1044
+ })
1045
+ );
1046
+ }
1047
+ ws.send(
1048
+ JSON.stringify({
1049
+ type: "response_end",
1050
+ id: requestId
1051
+ })
1052
+ );
1053
+ }
1054
+ function splitIntoChunks(data, chunkSize) {
1055
+ const chunks = [];
1056
+ for (let i = 0; i < data.length; i += chunkSize) {
1057
+ chunks.push(data.subarray(i, i + chunkSize));
1058
+ }
1059
+ return chunks;
1060
+ }
791
1061
  function getReconnectDelay(attempt) {
792
1062
  const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
793
1063
  const jitter = Math.random() * 1e3;
@@ -882,13 +1152,17 @@ async function connect(token, sandboxId, port, state) {
882
1152
  state.sandboxId ?? void 0
883
1153
  );
884
1154
  const response = await forwardToOpenCode(port, message.payload, message.id, state);
885
- ws.send(
886
- JSON.stringify({
887
- type: "response",
888
- id: message.id,
889
- payload: response
890
- })
891
- );
1155
+ sendResponse(ws, message.id, response);
1156
+ }
1157
+ break;
1158
+ case "subscribe_events":
1159
+ if (message.id) {
1160
+ void subscribeToOpenCodeEvents(port, message.id, ws, state);
1161
+ }
1162
+ break;
1163
+ case "unsubscribe_events":
1164
+ if (message.id) {
1165
+ cancelEventSubscription(message.id, state);
892
1166
  }
893
1167
  break;
894
1168
  }
@@ -973,34 +1247,40 @@ async function connect(token, sandboxId, port, state) {
973
1247
  }
974
1248
  async function tunnel(options) {
975
1249
  const verbose = options.verbose ?? false;
976
- const credentials2 = await getToken();
977
- if (!credentials2) {
1250
+ const credentials = await getToken();
1251
+ if (!credentials) {
1252
+ const cliName = (await import("./config-J7LPYFVS.js")).getCliName();
978
1253
  telemetry.error(EventTypes.CLI_ERROR, "Not logged in", { command: "tunnel" });
979
- printError("Not logged in. Run `evident login` first.");
1254
+ printError(`Not logged in. Run \`${cliName} login\` first.`);
980
1255
  process.exit(1);
981
1256
  }
982
1257
  const port = options.port ?? 4096;
983
1258
  const sandboxId = options.sandbox;
984
1259
  if (!sandboxId) {
1260
+ const cliName = (await import("./config-J7LPYFVS.js")).getCliName();
985
1261
  printError("--sandbox <id> is required");
986
1262
  blank();
987
1263
  console.log(chalk4.dim("To find your sandbox ID:"));
988
1264
  console.log(chalk4.dim(" 1. Create a remote sandbox in the Evident web UI"));
989
1265
  console.log(chalk4.dim(" 2. Copy the sandbox ID from the URL or settings"));
990
- console.log(chalk4.dim(" 3. Run: evident tunnel --sandbox <id>"));
1266
+ console.log(chalk4.dim(` 3. Run: ${cliName} tunnel --sandbox <id>`));
991
1267
  blank();
992
1268
  telemetry.error(EventTypes.CLI_ERROR, "Missing sandbox ID", { command: "tunnel" });
993
1269
  process.exit(1);
994
1270
  }
995
1271
  const state = {
996
1272
  connected: false,
1273
+ opencodeConnected: false,
1274
+ opencodeVersion: null,
997
1275
  sandboxId,
998
1276
  sandboxName: null,
999
1277
  reconnectAttempt: 0,
1000
1278
  lastActivity: /* @__PURE__ */ new Date(),
1001
1279
  activityLog: [],
1002
1280
  pendingRequests: /* @__PURE__ */ new Map(),
1003
- verbose
1281
+ verbose,
1282
+ displayInitialized: false,
1283
+ activeEventSubscriptions: /* @__PURE__ */ new Map()
1004
1284
  };
1005
1285
  telemetry.info(
1006
1286
  EventTypes.CLI_COMMAND,
@@ -1022,7 +1302,7 @@ async function tunnel(options) {
1022
1302
  message: "Validating sandbox..."
1023
1303
  });
1024
1304
  const validateSpinner = ora2("Validating sandbox...").start();
1025
- const validation = await validateSandbox(credentials2.token, sandboxId);
1305
+ const validation = await validateSandbox(credentials.token, sandboxId);
1026
1306
  if (!validation.valid) {
1027
1307
  validateSpinner.fail(`Sandbox validation failed: ${validation.error}`);
1028
1308
  logActivity(state, {
@@ -1047,25 +1327,17 @@ async function tunnel(options) {
1047
1327
  message: `Checking OpenCode on port ${port}...`
1048
1328
  });
1049
1329
  const opencodeSpinner = ora2("Checking OpenCode connection...").start();
1050
- try {
1051
- telemetry.debug(
1052
- EventTypes.OPENCODE_HEALTH_CHECK,
1053
- `Checking OpenCode on port ${port}`,
1054
- { port },
1055
- sandboxId
1056
- );
1057
- const response = await fetch(`http://localhost:${port}/health`);
1058
- if (!response.ok) {
1059
- throw new Error(`Health check returned ${response.status}`);
1060
- }
1061
- const healthData = await response.json().catch(() => ({}));
1062
- const version = healthData.version ? ` (v${healthData.version})` : "";
1330
+ const healthCheck = await checkOpenCodeHealth(port);
1331
+ if (healthCheck.healthy) {
1332
+ state.opencodeConnected = true;
1333
+ state.opencodeVersion = healthCheck.version ?? null;
1334
+ const version = healthCheck.version ? ` (v${healthCheck.version})` : "";
1063
1335
  telemetry.info(
1064
1336
  EventTypes.OPENCODE_HEALTH_OK,
1065
1337
  `OpenCode healthy on port ${port}`,
1066
1338
  {
1067
1339
  port,
1068
- healthData
1340
+ version: healthCheck.version
1069
1341
  },
1070
1342
  sandboxId
1071
1343
  );
@@ -1074,29 +1346,182 @@ async function tunnel(options) {
1074
1346
  type: "info",
1075
1347
  message: `OpenCode running on port ${port}${version}`
1076
1348
  });
1077
- } catch (error2) {
1078
- const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
1349
+ } else {
1079
1350
  telemetry.warn(
1080
1351
  EventTypes.OPENCODE_HEALTH_FAILED,
1081
- `Could not connect to OpenCode: ${errorMessage}`,
1352
+ `Could not connect to OpenCode: ${healthCheck.error}`,
1082
1353
  {
1083
1354
  port,
1084
- error: errorMessage
1355
+ error: healthCheck.error
1085
1356
  },
1086
1357
  sandboxId
1087
1358
  );
1088
1359
  opencodeSpinner.warn(`Could not connect to OpenCode on port ${port}`);
1089
1360
  logActivity(state, {
1090
1361
  type: "error",
1091
- error: `OpenCode not reachable on port ${port}: ${errorMessage}`
1362
+ error: `OpenCode not reachable on port ${port}: ${healthCheck.error}`
1092
1363
  });
1093
- printWarning("Make sure OpenCode is running before starting the tunnel:");
1094
- console.log(chalk4.dim(` opencode serve --port ${port}`));
1095
- blank();
1364
+ const runningInstances = await findHealthyOpenCodeInstances();
1365
+ if (runningInstances.length > 0) {
1366
+ blank();
1367
+ console.log(chalk4.yellow("Found OpenCode running on different port(s):"));
1368
+ for (const instance of runningInstances) {
1369
+ const ver = instance.version ? ` (v${instance.version})` : "";
1370
+ const cwd = instance.cwd ? ` in ${instance.cwd}` : "";
1371
+ console.log(chalk4.dim(` \u2022 Port ${instance.port}${ver}${cwd}`));
1372
+ }
1373
+ blank();
1374
+ if (runningInstances.length === 1) {
1375
+ console.log(chalk4.yellow("Tip: Run with the correct port:"));
1376
+ console.log(
1377
+ chalk4.dim(
1378
+ ` npx @evident-ai/cli@latest tunnel --sandbox ${sandboxId} --port ${runningInstances[0].port}`
1379
+ )
1380
+ );
1381
+ } else {
1382
+ console.log(chalk4.yellow("Tip: Specify which port to use:"));
1383
+ console.log(
1384
+ chalk4.dim(` npx @evident-ai/cli@latest tunnel --sandbox ${sandboxId} --port <PORT>`)
1385
+ );
1386
+ }
1387
+ blank();
1388
+ } else {
1389
+ blank();
1390
+ const action = await select({
1391
+ message: "OpenCode is not running. What would you like to do?",
1392
+ choices: [
1393
+ {
1394
+ name: "Start OpenCode for me",
1395
+ value: "start",
1396
+ description: `Run 'opencode serve --port ${port}' in a new process`
1397
+ },
1398
+ {
1399
+ name: "Show me the command to run",
1400
+ value: "manual",
1401
+ description: "Display instructions for starting OpenCode manually"
1402
+ },
1403
+ {
1404
+ name: "Continue without OpenCode",
1405
+ value: "continue",
1406
+ description: "Connect the tunnel anyway (requests will fail until OpenCode starts)"
1407
+ }
1408
+ ]
1409
+ });
1410
+ if (action === "start") {
1411
+ let actualPort = port;
1412
+ if (isPortInUse(port)) {
1413
+ console.log(chalk4.yellow(`
1414
+ Port ${port} is already in use by another process.`));
1415
+ const alternativePort = findAvailablePort(port + 1);
1416
+ if (alternativePort) {
1417
+ const useAlternative = await select({
1418
+ message: `Would you like to use port ${alternativePort} instead?`,
1419
+ choices: [
1420
+ { name: `Yes, use port ${alternativePort}`, value: "yes" },
1421
+ { name: "No, I will free up the port manually", value: "no" }
1422
+ ]
1423
+ });
1424
+ if (useAlternative === "yes") {
1425
+ actualPort = alternativePort;
1426
+ } else {
1427
+ console.log(chalk4.dim(`
1428
+ Free up port ${port} and run the tunnel command again.`));
1429
+ blank();
1430
+ process.exit(1);
1431
+ }
1432
+ } else {
1433
+ console.log(
1434
+ chalk4.red(
1435
+ `Could not find an available port. Please free up port ${port} and try again.`
1436
+ )
1437
+ );
1438
+ blank();
1439
+ process.exit(1);
1440
+ }
1441
+ }
1442
+ const opencodeStartSpinner = ora2(`Starting OpenCode on port ${actualPort}...`).start();
1443
+ try {
1444
+ let command = "opencode";
1445
+ let args = ["serve", "--port", actualPort.toString()];
1446
+ try {
1447
+ execSync("which opencode", { stdio: "ignore" });
1448
+ } catch {
1449
+ command = "npx";
1450
+ args = ["opencode", "serve", "--port", actualPort.toString()];
1451
+ }
1452
+ const child = spawn(command, args, {
1453
+ detached: true,
1454
+ stdio: "ignore",
1455
+ cwd: process.cwd()
1456
+ // Start in current working directory
1457
+ });
1458
+ child.unref();
1459
+ const maxRetries = 10;
1460
+ const retryDelayMs = 1e3;
1461
+ let healthy = false;
1462
+ let version;
1463
+ for (let i = 0; i < maxRetries; i++) {
1464
+ await sleep(retryDelayMs);
1465
+ const retryHealth = await checkOpenCodeHealth(actualPort);
1466
+ if (retryHealth.healthy) {
1467
+ healthy = true;
1468
+ version = retryHealth.version;
1469
+ break;
1470
+ }
1471
+ opencodeStartSpinner.text = `Starting OpenCode on port ${actualPort}... (${i + 1}/${maxRetries})`;
1472
+ }
1473
+ if (healthy) {
1474
+ state.opencodeConnected = true;
1475
+ state.opencodeVersion = version ?? null;
1476
+ const versionStr = version ? ` (v${version})` : "";
1477
+ opencodeStartSpinner.succeed(`OpenCode started on port ${actualPort}${versionStr}`);
1478
+ logActivity(state, {
1479
+ type: "info",
1480
+ message: `OpenCode started on port ${actualPort}${versionStr}`
1481
+ });
1482
+ } else {
1483
+ opencodeStartSpinner.warn(
1484
+ "OpenCode process started but not responding. Check if it started correctly."
1485
+ );
1486
+ logActivity(state, {
1487
+ type: "info",
1488
+ message: "OpenCode may still be starting..."
1489
+ });
1490
+ console.log(chalk4.dim("\nTip: Check for errors by running OpenCode manually:"));
1491
+ console.log(chalk4.dim(` opencode serve --port ${actualPort}`));
1492
+ blank();
1493
+ }
1494
+ } catch (error2) {
1495
+ const msg = error2 instanceof Error ? error2.message : "Unknown error";
1496
+ opencodeStartSpinner.fail(`Failed to start OpenCode: ${msg}`);
1497
+ logActivity(state, {
1498
+ type: "error",
1499
+ error: `Failed to start OpenCode: ${msg}`
1500
+ });
1501
+ console.log(chalk4.yellow("\nYou can try starting it manually:"));
1502
+ console.log(chalk4.dim(` opencode serve --port ${actualPort}`));
1503
+ blank();
1504
+ }
1505
+ } else if (action === "manual") {
1506
+ blank();
1507
+ console.log(chalk4.yellow("To start OpenCode, run one of these commands:"));
1508
+ blank();
1509
+ console.log(chalk4.dim(" # Start OpenCode in your project directory:"));
1510
+ console.log(chalk4.dim(` opencode serve --port ${port}`));
1511
+ blank();
1512
+ console.log(chalk4.dim(" # Or if you have OpenCode installed globally:"));
1513
+ console.log(chalk4.dim(` npx opencode serve --port ${port}`));
1514
+ blank();
1515
+ console.log(
1516
+ chalk4.dim("The tunnel will automatically forward requests once OpenCode is running.")
1517
+ );
1518
+ blank();
1519
+ }
1520
+ }
1096
1521
  }
1097
1522
  while (true) {
1098
1523
  try {
1099
- await connect(credentials2.token, sandboxId, port, state);
1524
+ await connect(credentials.token, sandboxId, port, state);
1100
1525
  state.reconnectAttempt++;
1101
1526
  const delay = getReconnectDelay(state.reconnectAttempt);
1102
1527
  logActivity(state, {