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

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,12 +1,4 @@
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";
10
2
 
11
3
  // src/index.ts
12
4
  import { Command } from "commander";
@@ -16,6 +8,90 @@ import open from "open";
16
8
  import ora from "ora";
17
9
  import chalk2 from "chalk";
18
10
 
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:3001/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
+ function getCliName() {
84
+ const argv1 = process.argv[1] || "";
85
+ const isNpx = process.env.npm_execpath?.includes("npx") || process.env.npm_command === "exec" || argv1.includes("_npx") || argv1.includes(".npm/_cacache");
86
+ if (isNpx) {
87
+ return "npx @evident-ai/cli@latest";
88
+ }
89
+ if (argv1.includes("tsx") || argv1.includes("ts-node")) {
90
+ return "pnpm --filter @evident-ai/cli dev:run";
91
+ }
92
+ return "evident";
93
+ }
94
+
19
95
  // src/lib/api.ts
20
96
  var ApiClient = class {
21
97
  baseUrl;
@@ -123,15 +199,15 @@ async function getKeytar() {
123
199
  return null;
124
200
  }
125
201
  }
126
- async function storeToken(credentials) {
202
+ async function storeToken(credentials2) {
127
203
  const keytar = await getKeytar();
128
204
  if (keytar) {
129
- await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(credentials));
205
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(credentials2));
130
206
  } else {
131
207
  setCredentials({
132
- token: credentials.token,
133
- user: credentials.user,
134
- expiresAt: credentials.expiresAt
208
+ token: credentials2.token,
209
+ user: credentials2.user,
210
+ expiresAt: credentials2.expiresAt
135
211
  });
136
212
  }
137
213
  }
@@ -332,8 +408,8 @@ async function login(options) {
332
408
 
333
409
  // src/commands/logout.ts
334
410
  async function logout() {
335
- const credentials = await getToken();
336
- if (!credentials) {
411
+ const credentials2 = await getToken();
412
+ if (!credentials2) {
337
413
  printWarning("You are not logged in.");
338
414
  return;
339
415
  }
@@ -344,16 +420,16 @@ async function logout() {
344
420
  // src/commands/whoami.ts
345
421
  import chalk3 from "chalk";
346
422
  async function whoami() {
347
- const credentials = await getToken();
348
- if (!credentials) {
423
+ const credentials2 = await getToken();
424
+ if (!credentials2) {
349
425
  printError("Not logged in. Run the `login` command to authenticate.");
350
426
  process.exit(1);
351
427
  }
352
428
  blank();
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);
429
+ console.log(keyValue("User", chalk3.bold(credentials2.user.email)));
430
+ console.log(keyValue("User ID", credentials2.user.id));
431
+ if (credentials2.expiresAt) {
432
+ const expiresAt = new Date(credentials2.expiresAt);
357
433
  const now = /* @__PURE__ */ new Date();
358
434
  if (expiresAt < now) {
359
435
  console.log(keyValue("Status", chalk3.red("Token expired")));
@@ -367,12 +443,26 @@ async function whoami() {
367
443
  blank();
368
444
  }
369
445
 
370
- // src/commands/tunnel.ts
371
- import WebSocket from "ws";
372
- import chalk4 from "chalk";
446
+ // src/commands/run.ts
447
+ import chalk5 from "chalk";
373
448
  import ora2 from "ora";
374
- import { execSync, spawn } from "child_process";
375
- import { select } from "@inquirer/prompts";
449
+ import { select as select2 } from "@inquirer/prompts";
450
+
451
+ // ../../packages/types/src/telemetry/index.ts
452
+ var TelemetryEventTypes = {
453
+ // Agent activity events (shown in web UI activity log)
454
+ AGENT_CONNECTED: "agent.connected",
455
+ AGENT_DISCONNECTED: "agent.disconnected",
456
+ AGENT_MESSAGE_PROCESSING: "agent.message_processing",
457
+ AGENT_MESSAGE_DONE: "agent.message_done",
458
+ AGENT_MESSAGE_FAILED: "agent.message_failed"
459
+ };
460
+
461
+ // ../../packages/types/src/tunnel/index.ts
462
+ var TUNNEL_CHUNK_THRESHOLD = 512 * 1024;
463
+ var TUNNEL_CHUNK_SIZE = 768 * 1024;
464
+ var TUNNEL_MAX_RESPONSE_SIZE = 50 * 1024 * 1024;
465
+ var TUNNEL_CHUNK_TIMEOUT_MS = 30 * 1e3;
376
466
 
377
467
  // src/lib/telemetry.ts
378
468
  var CLI_VERSION = process.env.npm_package_version || "unknown";
@@ -388,7 +478,7 @@ function logEvent(eventType, options = {}) {
388
478
  severity: options.severity || "info",
389
479
  message: options.message,
390
480
  metadata: options.metadata,
391
- sandbox_id: options.sandboxId,
481
+ agent_id: options.agentId,
392
482
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
393
483
  };
394
484
  eventBuffer.push(event);
@@ -402,10 +492,10 @@ function logEvent(eventType, options = {}) {
402
492
  }
403
493
  }
404
494
  var telemetry = {
405
- debug: (eventType, message, metadata, sandboxId) => logEvent(eventType, { severity: "debug", message, metadata, sandboxId }),
406
- info: (eventType, message, metadata, sandboxId) => logEvent(eventType, { severity: "info", message, metadata, sandboxId }),
407
- warn: (eventType, message, metadata, sandboxId) => logEvent(eventType, { severity: "warning", message, metadata, sandboxId }),
408
- error: (eventType, message, metadata, sandboxId) => logEvent(eventType, { severity: "error", message, metadata, sandboxId })
495
+ debug: (eventType, message, metadata, agentId) => logEvent(eventType, { severity: "debug", message, metadata, agentId }),
496
+ info: (eventType, message, metadata, agentId) => logEvent(eventType, { severity: "info", message, metadata, agentId }),
497
+ warn: (eventType, message, metadata, agentId) => logEvent(eventType, { severity: "warning", message, metadata, agentId }),
498
+ error: (eventType, message, metadata, agentId) => logEvent(eventType, { severity: "error", message, metadata, agentId })
409
499
  };
410
500
  async function flushEvents() {
411
501
  if (eventBuffer.length === 0) return;
@@ -416,25 +506,26 @@ async function flushEvents() {
416
506
  flushTimeout = null;
417
507
  }
418
508
  try {
419
- const credentials = await getToken();
420
- if (!credentials) {
509
+ const credentials2 = await getToken();
510
+ if (!credentials2) {
421
511
  return;
422
512
  }
423
513
  const apiUrl = getApiUrlConfig();
424
514
  const controller = new AbortController();
425
515
  const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
426
516
  try {
517
+ const request = {
518
+ events,
519
+ client_type: "cli",
520
+ client_version: CLI_VERSION
521
+ };
427
522
  const response = await fetch(`${apiUrl}/telemetry/events`, {
428
523
  method: "POST",
429
524
  headers: {
430
525
  "Content-Type": "application/json",
431
- Authorization: `Bearer ${credentials.token}`
526
+ Authorization: `Bearer ${credentials2.token}`
432
527
  },
433
- body: JSON.stringify({
434
- events,
435
- client_type: "cli",
436
- client_version: CLI_VERSION
437
- }),
528
+ body: JSON.stringify(request),
438
529
  signal: controller.signal
439
530
  });
440
531
  if (!response.ok) {
@@ -457,6 +548,59 @@ async function shutdownTelemetry() {
457
548
  }
458
549
  await flushEvents();
459
550
  }
551
+ function emitEvent(event) {
552
+ logEvent(event.event_type, {
553
+ severity: event.severity,
554
+ message: event.message,
555
+ metadata: event.metadata,
556
+ agentId: event.agent_id
557
+ });
558
+ }
559
+ function emitAgentConnected(agentId, metadata) {
560
+ emitEvent({
561
+ event_type: TelemetryEventTypes.AGENT_CONNECTED,
562
+ severity: "info",
563
+ message: "Agent CLI connected",
564
+ metadata,
565
+ agent_id: agentId
566
+ });
567
+ }
568
+ function emitAgentDisconnected(agentId, metadata) {
569
+ emitEvent({
570
+ event_type: TelemetryEventTypes.AGENT_DISCONNECTED,
571
+ severity: "info",
572
+ message: `Agent CLI disconnected (code: ${metadata.code})`,
573
+ metadata,
574
+ agent_id: agentId
575
+ });
576
+ }
577
+ function emitAgentMessageProcessing(agentId, metadata) {
578
+ emitEvent({
579
+ event_type: TelemetryEventTypes.AGENT_MESSAGE_PROCESSING,
580
+ severity: "info",
581
+ message: `Processing message ${metadata.message_id.slice(0, 8)}...`,
582
+ metadata,
583
+ agent_id: agentId
584
+ });
585
+ }
586
+ function emitAgentMessageDone(agentId, metadata) {
587
+ emitEvent({
588
+ event_type: TelemetryEventTypes.AGENT_MESSAGE_DONE,
589
+ severity: "info",
590
+ message: `Message ${metadata.message_id.slice(0, 8)} processed`,
591
+ metadata,
592
+ agent_id: agentId
593
+ });
594
+ }
595
+ function emitAgentMessageFailed(agentId, metadata) {
596
+ emitEvent({
597
+ event_type: TelemetryEventTypes.AGENT_MESSAGE_FAILED,
598
+ severity: "error",
599
+ message: metadata.error ? `Message ${metadata.message_id.slice(0, 8)} failed: ${metadata.error}` : `Message ${metadata.message_id.slice(0, 8)} ${metadata.reason || "failed"}`,
600
+ metadata,
601
+ agent_id: agentId
602
+ });
603
+ }
460
604
  var EventTypes = {
461
605
  // Tunnel lifecycle
462
606
  TUNNEL_STARTING: "tunnel.starting",
@@ -484,12 +628,71 @@ var EventTypes = {
484
628
  CLI_ERROR: "cli.error"
485
629
  };
486
630
 
487
- // src/commands/tunnel.ts
488
- var MAX_RECONNECT_DELAY = 3e4;
489
- var BASE_RECONNECT_DELAY = 500;
490
- var MAX_ACTIVITY_LOG_ENTRIES = 10;
491
- var CHUNK_THRESHOLD = 512 * 1024;
492
- var CHUNK_SIZE = 768 * 1024;
631
+ // src/lib/auth.ts
632
+ async function getAuthCredentials() {
633
+ const agentKey = process.env.EVIDENT_AGENT_KEY;
634
+ if (agentKey) {
635
+ return { token: agentKey, authType: "agent_key" };
636
+ }
637
+ const userToken = process.env.EVIDENT_TOKEN;
638
+ if (userToken) {
639
+ return { token: userToken, authType: "bearer" };
640
+ }
641
+ const keychainCreds = await getToken();
642
+ if (keychainCreds) {
643
+ return {
644
+ token: keychainCreds.token,
645
+ authType: "bearer",
646
+ user: keychainCreds.user
647
+ };
648
+ }
649
+ return null;
650
+ }
651
+ function getAuthHeader(credentials2) {
652
+ if (credentials2.authType === "agent_key") {
653
+ return `SandboxKey ${credentials2.token}`;
654
+ }
655
+ return `Bearer ${credentials2.token}`;
656
+ }
657
+ function isInteractive(jsonOutput) {
658
+ if (jsonOutput) return false;
659
+ if (process.env.CI) return false;
660
+ if (process.env.GITHUB_ACTIONS) return false;
661
+ if (!process.stdin.isTTY) return false;
662
+ return true;
663
+ }
664
+
665
+ // src/lib/opencode/health.ts
666
+ async function checkOpenCodeHealth(port) {
667
+ try {
668
+ const response = await fetch(`http://localhost:${port}/global/health`, {
669
+ signal: AbortSignal.timeout(2e3)
670
+ // 2 second timeout
671
+ });
672
+ if (!response.ok) {
673
+ return { healthy: false, error: `HTTP ${response.status}` };
674
+ }
675
+ const data = await response.json().catch(() => ({}));
676
+ return { healthy: true, version: data.version };
677
+ } catch (error2) {
678
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
679
+ return { healthy: false, error: message };
680
+ }
681
+ }
682
+ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
683
+ const startTime = Date.now();
684
+ while (Date.now() - startTime < timeoutMs) {
685
+ const health = await checkOpenCodeHealth(port);
686
+ if (health.healthy) {
687
+ return health;
688
+ }
689
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
690
+ }
691
+ return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
692
+ }
693
+
694
+ // src/lib/opencode/process.ts
695
+ import { execSync, spawn } from "child_process";
493
696
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
494
697
  function getProcessCwd(pid) {
495
698
  const platform = process.platform;
@@ -624,22 +827,6 @@ async function scanPortsForOpenCode() {
624
827
  }
625
828
  return instances;
626
829
  }
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
830
  async function findHealthyOpenCodeInstances() {
644
831
  const processes = findOpenCodeProcesses();
645
832
  const healthy = [];
@@ -655,356 +842,288 @@ async function findHealthyOpenCodeInstances() {
655
842
  }
656
843
  return healthy;
657
844
  }
658
- function logActivity(state, entry) {
659
- const fullEntry = {
660
- ...entry,
661
- timestamp: /* @__PURE__ */ new Date()
662
- };
663
- state.activityLog.push(fullEntry);
664
- if (state.activityLog.length > MAX_ACTIVITY_LOG_ENTRIES) {
665
- state.activityLog.shift();
845
+ async function startOpenCode(port) {
846
+ let command = "opencode";
847
+ let args = ["serve", "--port", port.toString()];
848
+ try {
849
+ execSync("which opencode", { stdio: "ignore" });
850
+ } catch {
851
+ command = "npx";
852
+ args = ["opencode", "serve", "--port", port.toString()];
666
853
  }
667
- state.lastActivity = fullEntry.timestamp;
668
- }
669
- function formatActivityEntry(entry, _verbose) {
670
- const time = entry.timestamp.toLocaleTimeString("en-US", {
671
- hour12: false,
672
- hour: "2-digit",
673
- minute: "2-digit",
674
- second: "2-digit"
854
+ const child = spawn(command, args, {
855
+ detached: true,
856
+ stdio: "ignore",
857
+ cwd: process.cwd()
675
858
  });
676
- switch (entry.type) {
677
- case "request": {
678
- const duration = entry.durationMs ? ` (${entry.durationMs}ms)` : "";
679
- const status = entry.status ? ` \u2192 ${colorizeStatus(entry.status)}` : " ...";
680
- return ` ${chalk4.dim(`[${time}]`)} ${chalk4.cyan("\u2190")} ${entry.method} ${entry.path}${status}${duration}`;
681
- }
682
- case "response": {
683
- const duration = entry.durationMs ? ` (${entry.durationMs}ms)` : "";
684
- return ` ${chalk4.dim(`[${time}]`)} ${chalk4.green("\u2192")} ${entry.method} ${entry.path} ${colorizeStatus(entry.status)}${duration}`;
685
- }
686
- case "error": {
687
- const errorMsg = entry.error || "Unknown error";
688
- const path = entry.path ? ` ${entry.method} ${entry.path}` : "";
689
- return ` ${chalk4.dim(`[${time}]`)} ${chalk4.red("\u2717")}${path} - ${chalk4.red(errorMsg)}`;
690
- }
691
- case "info": {
692
- return ` ${chalk4.dim(`[${time}]`)} ${chalk4.blue("\u25CF")} ${entry.message}`;
693
- }
694
- default:
695
- return ` ${chalk4.dim(`[${time}]`)} ${entry.message || "Unknown"}`;
696
- }
859
+ return child;
697
860
  }
698
- function colorizeStatus(status) {
699
- if (status >= 200 && status < 300) {
700
- return chalk4.green(status.toString());
701
- } else if (status >= 300 && status < 400) {
702
- return chalk4.yellow(status.toString());
703
- } else if (status >= 400 && status < 500) {
704
- return chalk4.red(status.toString());
705
- } else if (status >= 500) {
706
- return chalk4.bgRed.white(` ${status} `);
707
- }
708
- return status.toString();
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;
718
- function displayStatus(state) {
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}`);
861
+ function stopOpenCode(opencodeProcess) {
862
+ if (!opencodeProcess || !opencodeProcess.pid) {
863
+ return;
725
864
  }
726
- lines.push(` ID: ${state.sandboxId ?? "Unknown"}`);
727
- lines.push("");
728
- if (state.connected) {
729
- lines.push(` ${chalk4.green("\u25CF")} Tunnel: ${chalk4.green("Connected to Evident")}`);
730
- } else {
731
- if (state.reconnectAttempt > 0) {
732
- lines.push(
733
- ` ${chalk4.yellow("\u25CB")} Tunnel: ${chalk4.yellow(`Reconnecting... (attempt ${state.reconnectAttempt})`)}`
734
- );
865
+ try {
866
+ if (process.platform === "win32") {
867
+ opencodeProcess.kill("SIGTERM");
735
868
  } else {
736
- lines.push(` ${chalk4.yellow("\u25CB")} Tunnel: ${chalk4.yellow("Connecting...")}`);
737
- }
738
- }
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("");
746
- if (state.activityLog.length > 0) {
747
- lines.push(chalk4.bold(" Activity:"));
748
- for (const entry of state.activityLog) {
749
- lines.push(formatActivityEntry(entry, state.verbose));
869
+ process.kill(-opencodeProcess.pid, "SIGTERM");
750
870
  }
751
- } else {
752
- lines.push(chalk4.dim(" No activity yet. Waiting for requests..."));
753
- }
754
- lines.push("");
755
- lines.push(chalk4.dim("\u2500".repeat(60)));
756
- if (state.verbose) {
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("");
871
+ } catch {
762
872
  }
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);
873
+ }
874
+
875
+ // src/lib/opencode/install.ts
876
+ import { execSync as execSync2 } from "child_process";
877
+ import chalk4 from "chalk";
878
+ import { select } from "@inquirer/prompts";
879
+ var OPENCODE_INSTALL_URL = "https://opencode.ai";
880
+ function isOpenCodeInstalled() {
881
+ try {
882
+ const platform = process.platform;
883
+ if (platform === "win32") {
884
+ execSync2("where opencode", { stdio: "ignore" });
885
+ } else {
886
+ execSync2("which opencode", { stdio: "ignore" });
778
887
  }
888
+ return true;
889
+ } catch {
890
+ return false;
779
891
  }
780
892
  }
781
- function displayError(_state, error2, details) {
782
- blank();
783
- console.log(chalk4.bgRed.white.bold(" ERROR "));
784
- console.log(chalk4.red(` ${error2}`));
785
- if (details) {
786
- console.log(chalk4.dim(` ${details}`));
893
+ async function promptOpenCodeInstall(interactive) {
894
+ if (!interactive) {
895
+ console.log(
896
+ JSON.stringify({
897
+ status: "error",
898
+ error: "OpenCode is not installed",
899
+ install_url: OPENCODE_INSTALL_URL,
900
+ install_commands: {
901
+ npm: "npm install -g opencode",
902
+ curl: "curl -fsSL https://opencode.ai/install.sh | sh"
903
+ }
904
+ })
905
+ );
906
+ return "exit";
787
907
  }
788
908
  blank();
789
- }
790
- async function validateSandbox(token, sandboxId) {
791
- const apiUrl = getApiUrlConfig();
792
- try {
793
- const response = await fetch(`${apiUrl}/sandboxes/${sandboxId}`, {
794
- headers: {
795
- Authorization: `Bearer ${token}`
909
+ console.log(chalk4.yellow("OpenCode is not installed on your system."));
910
+ blank();
911
+ console.log(chalk4.dim("OpenCode is an AI coding agent that runs locally on your machine."));
912
+ console.log(chalk4.dim(`Learn more at: ${chalk4.cyan(OPENCODE_INSTALL_URL)}`));
913
+ blank();
914
+ const action = await select({
915
+ message: "How would you like to proceed?",
916
+ choices: [
917
+ {
918
+ name: "Show installation instructions",
919
+ value: "instructions",
920
+ description: "Display commands to install OpenCode"
921
+ },
922
+ {
923
+ name: "Continue without OpenCode",
924
+ value: "continue",
925
+ description: "Connect anyway (requests will fail until OpenCode is installed)"
926
+ },
927
+ {
928
+ name: "Exit",
929
+ value: "exit",
930
+ description: "Exit and install OpenCode manually"
796
931
  }
932
+ ]
933
+ });
934
+ if (action === "instructions") {
935
+ blank();
936
+ console.log(chalk4.bold("Install OpenCode using one of these methods:"));
937
+ blank();
938
+ console.log(chalk4.dim(" # Option 1: Install via npm (recommended)"));
939
+ console.log(` ${chalk4.cyan("npm install -g opencode")}`);
940
+ blank();
941
+ console.log(chalk4.dim(" # Option 2: Install via curl"));
942
+ console.log(` ${chalk4.cyan("curl -fsSL https://opencode.ai/install.sh | sh")}`);
943
+ blank();
944
+ console.log(chalk4.dim(`For more options, visit: ${chalk4.cyan(OPENCODE_INSTALL_URL)}`));
945
+ blank();
946
+ const afterInstall = await select({
947
+ message: "After installing, what would you like to do?",
948
+ choices: [
949
+ {
950
+ name: "I installed it - continue",
951
+ value: "continue",
952
+ description: "Proceed with the run command"
953
+ },
954
+ {
955
+ name: "Exit",
956
+ value: "exit",
957
+ description: "Exit now and run the command again later"
958
+ }
959
+ ]
797
960
  });
798
- if (response.status === 404) {
799
- return { valid: false, error: "Sandbox not found" };
800
- }
801
- if (response.status === 401) {
802
- return { valid: false, error: "Authentication failed. Please run `evident login` again." };
803
- }
804
- if (!response.ok) {
805
- return { valid: false, error: `API error: ${response.status}` };
806
- }
807
- const sandbox = await response.json();
808
- if (sandbox.sandbox_type !== "remote") {
809
- return {
810
- valid: false,
811
- error: `Sandbox is type '${sandbox.sandbox_type}', must be 'remote' for tunnel connection`
812
- };
961
+ if (afterInstall === "continue") {
962
+ if (isOpenCodeInstalled()) {
963
+ console.log(chalk4.green("\n\u2713 OpenCode detected!"));
964
+ return "installed";
965
+ } else {
966
+ console.log(chalk4.yellow("\nOpenCode still not detected in PATH."));
967
+ console.log(chalk4.dim("You may need to restart your terminal or add it to your PATH."));
968
+ const proceed = await select({
969
+ message: "Continue anyway?",
970
+ choices: [
971
+ { name: "Yes, continue", value: "continue" },
972
+ { name: "No, exit", value: "exit" }
973
+ ]
974
+ });
975
+ return proceed === "continue" ? "continue" : "exit";
976
+ }
813
977
  }
814
- return { valid: true, name: sandbox.name };
815
- } catch (error2) {
816
- const message = error2 instanceof Error ? error2.message : "Unknown error";
817
- return { valid: false, error: `Failed to validate sandbox: ${message}` };
978
+ return "exit";
818
979
  }
980
+ return action;
819
981
  }
820
- async function forwardToOpenCode(port, request, requestId, state) {
821
- const url = `http://localhost:${port}${request.path}`;
822
- const startTime = Date.now();
823
- state.pendingRequests.set(requestId, {
824
- startTime,
825
- method: request.method,
826
- path: request.path
827
- });
828
- logActivity(state, {
829
- type: "request",
830
- method: request.method,
831
- path: request.path,
832
- requestId
982
+
983
+ // src/lib/opencode/session.ts
984
+ async function createOpenCodeSession(port) {
985
+ const response = await fetch(`http://localhost:${port}/session`, {
986
+ method: "POST",
987
+ headers: { "Content-Type": "application/json" },
988
+ body: JSON.stringify({})
833
989
  });
834
- displayStatus(state);
835
- if (state.verbose && request.body) {
836
- console.log(chalk4.dim(` Request body: ${JSON.stringify(request.body, null, 2)}`));
990
+ if (!response.ok) {
991
+ throw new Error(`Failed to create session: HTTP ${response.status}`);
837
992
  }
838
- telemetry.debug(
839
- EventTypes.OPENCODE_REQUEST_FORWARDED,
840
- `Forwarding ${request.method} ${request.path}`,
841
- {
842
- method: request.method,
843
- path: request.path,
844
- port,
845
- requestId
846
- },
847
- state.sandboxId ?? void 0
848
- );
849
- try {
850
- const response = await fetch(url, {
851
- method: request.method,
852
- headers: {
853
- "Content-Type": "application/json",
854
- ...request.headers
855
- },
856
- body: request.body ? JSON.stringify(request.body) : void 0
857
- });
858
- let body;
859
- const contentType = response.headers.get("Content-Type");
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
- }
869
- } else {
870
- body = text;
993
+ const data = await response.json();
994
+ return data.id;
995
+ }
996
+ async function sendMessageToOpenCode(port, sessionId, content, options, hooks, maxWaitMs = 10 * 60 * 1e3) {
997
+ const body = {
998
+ parts: [{ type: "text", text: content }]
999
+ };
1000
+ if (options?.agent) {
1001
+ body.agent = options.agent;
1002
+ }
1003
+ if (options?.model) {
1004
+ const slashIndex = options.model.indexOf("/");
1005
+ if (slashIndex !== -1) {
1006
+ body.model = {
1007
+ providerID: options.model.substring(0, slashIndex),
1008
+ modelID: options.model.substring(slashIndex + 1)
1009
+ };
871
1010
  }
872
- const durationMs = Date.now() - startTime;
873
- state.pendingRequests.delete(requestId);
874
- if (!state.opencodeConnected) {
875
- state.opencodeConnected = true;
1011
+ }
1012
+ let pollDone = false;
1013
+ const reportedQuestions = /* @__PURE__ */ new Set();
1014
+ const reportedPermissions = /* @__PURE__ */ new Set();
1015
+ const pollInteractive = async () => {
1016
+ while (!pollDone) {
1017
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1018
+ if (pollDone) break;
1019
+ if (hooks?.onQuestion) {
1020
+ try {
1021
+ const res = await fetch(`http://localhost:${port}/question`);
1022
+ if (res.ok) {
1023
+ const questions = await res.json();
1024
+ for (const q of questions) {
1025
+ if (q.sessionID === sessionId && !reportedQuestions.has(q.id)) {
1026
+ reportedQuestions.add(q.id);
1027
+ await hooks.onQuestion(q);
1028
+ }
1029
+ }
1030
+ }
1031
+ } catch {
1032
+ }
1033
+ }
1034
+ if (hooks?.onPermission) {
1035
+ try {
1036
+ const res = await fetch(`http://localhost:${port}/permission`);
1037
+ if (res.ok) {
1038
+ const permissions = await res.json();
1039
+ for (const p of permissions) {
1040
+ if (p.sessionID === sessionId && !reportedPermissions.has(p.id)) {
1041
+ reportedPermissions.add(p.id);
1042
+ await hooks.onPermission(p);
1043
+ }
1044
+ }
1045
+ }
1046
+ } catch {
1047
+ }
1048
+ }
876
1049
  }
877
- const lastEntry = state.activityLog[state.activityLog.length - 1];
878
- if (lastEntry && lastEntry.requestId === requestId) {
879
- lastEntry.type = "response";
880
- lastEntry.status = response.status;
881
- lastEntry.durationMs = durationMs;
882
- } else {
883
- logActivity(state, {
884
- type: "response",
885
- method: request.method,
886
- path: request.path,
887
- status: response.status,
888
- durationMs,
889
- requestId
1050
+ };
1051
+ const sendMessage = async () => {
1052
+ const controller = new AbortController();
1053
+ const timer = setTimeout(() => controller.abort(), maxWaitMs);
1054
+ try {
1055
+ const res = await fetch(`http://localhost:${port}/session/${sessionId}/message`, {
1056
+ method: "POST",
1057
+ headers: { "Content-Type": "application/json" },
1058
+ body: JSON.stringify(body),
1059
+ signal: controller.signal
890
1060
  });
1061
+ if (!res.ok) {
1062
+ const text = await res.text().catch(() => "");
1063
+ throw new Error(`OpenCode message failed: HTTP ${res.status}${text ? `: ${text}` : ""}`);
1064
+ }
1065
+ const sessionRes = await fetch(`http://localhost:${port}/session/${sessionId}`).catch(
1066
+ () => null
1067
+ );
1068
+ const session = sessionRes?.ok ? await sessionRes.json() : null;
1069
+ return { title: session?.title };
1070
+ } catch (err) {
1071
+ if (err instanceof Error && err.name === "AbortError") {
1072
+ throw new Error("Message processing timed out");
1073
+ }
1074
+ throw err;
1075
+ } finally {
1076
+ clearTimeout(timer);
1077
+ pollDone = true;
891
1078
  }
892
- displayStatus(state);
893
- if (state.verbose && body) {
894
- const bodyStr = typeof body === "string" ? body : JSON.stringify(body, null, 2);
895
- const truncated = bodyStr.length > 500 ? bodyStr.substring(0, 500) + "..." : bodyStr;
896
- console.log(chalk4.dim(` Response body: ${truncated}`));
897
- }
898
- telemetry.debug(
899
- EventTypes.OPENCODE_RESPONSE_SENT,
900
- `Response ${response.status}`,
901
- {
902
- status: response.status,
903
- path: request.path,
904
- durationMs,
905
- requestId
1079
+ };
1080
+ const [result] = await Promise.all([sendMessage(), pollInteractive()]);
1081
+ return result;
1082
+ }
1083
+
1084
+ // src/lib/tunnel/connection.ts
1085
+ import WebSocket from "ws";
1086
+
1087
+ // src/lib/tunnel/forwarding.ts
1088
+ var CHUNK_THRESHOLD = 512 * 1024;
1089
+ var CHUNK_SIZE = 768 * 1024;
1090
+ async function forwardToOpenCode(port, request) {
1091
+ const url = `http://localhost:${port}${request.path}`;
1092
+ try {
1093
+ const response = await fetch(url, {
1094
+ method: request.method,
1095
+ headers: {
1096
+ "Content-Type": "application/json",
1097
+ ...request.headers
906
1098
  },
907
- state.sandboxId ?? void 0
908
- );
1099
+ body: request.body ? JSON.stringify(request.body) : void 0
1100
+ });
1101
+ let body;
1102
+ const contentType = response.headers.get("Content-Type");
1103
+ const text = await response.text();
1104
+ if (!text || text.length === 0) {
1105
+ body = null;
1106
+ } else if (contentType?.includes("application/json")) {
1107
+ try {
1108
+ body = JSON.parse(text);
1109
+ } catch {
1110
+ body = text;
1111
+ }
1112
+ } else {
1113
+ body = text;
1114
+ }
909
1115
  return {
910
1116
  status: response.status,
911
1117
  body
912
1118
  };
913
1119
  } catch (error2) {
914
1120
  const message = error2 instanceof Error ? error2.message : "Unknown error";
915
- const durationMs = Date.now() - startTime;
916
- state.pendingRequests.delete(requestId);
917
- state.opencodeConnected = false;
918
- logActivity(state, {
919
- type: "error",
920
- method: request.method,
921
- path: request.path,
922
- error: `OpenCode unreachable: ${message}`,
923
- durationMs,
924
- requestId
925
- });
926
- displayStatus(state);
927
- telemetry.error(
928
- EventTypes.OPENCODE_UNREACHABLE,
929
- `Failed to connect to OpenCode: ${message}`,
930
- {
931
- port,
932
- path: request.path,
933
- error: message,
934
- requestId
935
- },
936
- state.sandboxId ?? void 0
937
- );
938
1121
  return {
939
1122
  status: 502,
940
1123
  body: { error: "Failed to connect to OpenCode", message }
941
1124
  };
942
1125
  }
943
1126
  }
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
1127
  function sendResponse(ws, requestId, response) {
1009
1128
  const bodyStr = JSON.stringify(response.body ?? null);
1010
1129
  const bodyBytes = Buffer.from(bodyStr, "utf-8");
@@ -1058,79 +1177,107 @@ function splitIntoChunks(data, chunkSize) {
1058
1177
  }
1059
1178
  return chunks;
1060
1179
  }
1180
+
1181
+ // src/lib/tunnel/events.ts
1182
+ async function subscribeToOpenCodeEvents(port, subscriptionId, ws, abortController) {
1183
+ const url = `http://localhost:${port}/event`;
1184
+ try {
1185
+ const response = await fetch(url, {
1186
+ headers: { Accept: "text/event-stream" },
1187
+ signal: abortController.signal
1188
+ });
1189
+ if (!response.ok) {
1190
+ throw new Error(`Failed to connect to OpenCode events: ${response.status}`);
1191
+ }
1192
+ if (!response.body) {
1193
+ throw new Error("No response body");
1194
+ }
1195
+ const reader = response.body.getReader();
1196
+ const decoder = new TextDecoder();
1197
+ let buffer = "";
1198
+ while (true) {
1199
+ const { done, value } = await reader.read();
1200
+ if (done) {
1201
+ ws.send(JSON.stringify({ type: "event_end", id: subscriptionId }));
1202
+ break;
1203
+ }
1204
+ buffer += decoder.decode(value, { stream: true });
1205
+ const lines = buffer.split("\n");
1206
+ buffer = lines.pop() || "";
1207
+ for (const line of lines) {
1208
+ if (line.startsWith("data: ")) {
1209
+ try {
1210
+ const event = JSON.parse(line.slice(6));
1211
+ ws.send(JSON.stringify({ type: "event", id: subscriptionId, event }));
1212
+ } catch {
1213
+ }
1214
+ }
1215
+ }
1216
+ }
1217
+ } catch (error2) {
1218
+ if (abortController.signal.aborted) {
1219
+ return;
1220
+ }
1221
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
1222
+ ws.send(JSON.stringify({ type: "event_error", id: subscriptionId, error: message }));
1223
+ throw error2;
1224
+ }
1225
+ }
1226
+
1227
+ // src/lib/tunnel/connection.ts
1228
+ var MAX_RECONNECT_DELAY = 3e4;
1229
+ var BASE_RECONNECT_DELAY = 500;
1061
1230
  function getReconnectDelay(attempt) {
1062
1231
  const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
1063
1232
  const jitter = Math.random() * 1e3;
1064
1233
  return Math.min(exponentialDelay + jitter, MAX_RECONNECT_DELAY);
1065
1234
  }
1066
- async function connect(token, sandboxId, port, state) {
1235
+ function connectTunnel(options) {
1236
+ const {
1237
+ agentId,
1238
+ authHeader,
1239
+ port,
1240
+ onConnected,
1241
+ onDisconnected,
1242
+ onError,
1243
+ onRequest,
1244
+ onResponse,
1245
+ onInfo
1246
+ } = options;
1067
1247
  const tunnelUrl = getTunnelUrlConfig();
1068
- const url = `${tunnelUrl}/tunnel/${sandboxId}/connect`;
1069
- logActivity(state, {
1070
- type: "info",
1071
- message: "Connecting to tunnel relay..."
1072
- });
1073
- displayStatus(state);
1074
- telemetry.info(
1075
- EventTypes.TUNNEL_STARTING,
1076
- `Connecting to ${url}`,
1077
- {
1078
- sandboxId,
1079
- port,
1080
- tunnelUrl
1081
- },
1082
- sandboxId
1083
- );
1248
+ const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
1249
+ const activeEventSubscriptions = /* @__PURE__ */ new Map();
1084
1250
  return new Promise((resolve, reject) => {
1085
1251
  const ws = new WebSocket(url, {
1086
1252
  headers: {
1087
- Authorization: `Bearer ${token}`
1253
+ Authorization: authHeader
1088
1254
  }
1089
1255
  });
1256
+ const connectionTimeout = setTimeout(() => {
1257
+ ws.close();
1258
+ reject(new Error("Connection timeout"));
1259
+ }, 3e4);
1090
1260
  ws.on("open", () => {
1091
- state.connected = true;
1092
- state.reconnectAttempt = 0;
1093
- logActivity(state, {
1094
- type: "info",
1095
- message: "WebSocket connection established"
1096
- });
1097
- displayStatus(state);
1261
+ onInfo?.("WebSocket connection established");
1098
1262
  });
1099
1263
  ws.on("message", async (data) => {
1100
1264
  try {
1101
1265
  const message = JSON.parse(data.toString());
1102
1266
  switch (message.type) {
1103
- case "connected":
1104
- state.sandboxId = message.sandbox_id ?? sandboxId;
1105
- logActivity(state, {
1106
- type: "info",
1107
- message: `Tunnel connected (sandbox: ${state.sandboxId})`
1267
+ case "connected": {
1268
+ clearTimeout(connectionTimeout);
1269
+ const connectedAgentId = message.agent_id ?? agentId;
1270
+ onConnected?.(connectedAgentId);
1271
+ resolve({
1272
+ ws,
1273
+ close: () => ws.close(1e3, "CLI shutdown"),
1274
+ activeEventSubscriptions
1108
1275
  });
1109
- telemetry.info(
1110
- EventTypes.TUNNEL_CONNECTED,
1111
- `Tunnel connected`,
1112
- {
1113
- sandboxId: message.sandbox_id
1114
- },
1115
- message.sandbox_id
1116
- );
1117
- displayStatus(state);
1118
1276
  break;
1277
+ }
1119
1278
  case "error":
1120
- logActivity(state, {
1121
- type: "error",
1122
- error: message.message || "Unknown tunnel error"
1123
- });
1124
- telemetry.error(
1125
- EventTypes.TUNNEL_ERROR,
1126
- `Tunnel error: ${message.message}`,
1127
- {
1128
- code: message.code,
1129
- message: message.message
1130
- },
1131
- state.sandboxId ?? void 0
1132
- );
1133
- displayStatus(state);
1279
+ clearTimeout(connectionTimeout);
1280
+ onError?.(message.message || "Unknown tunnel error");
1134
1281
  if (message.code === "unauthorized") {
1135
1282
  ws.close();
1136
1283
  reject(new Error("Unauthorized"));
@@ -1141,442 +1288,1229 @@ async function connect(token, sandboxId, port, state) {
1141
1288
  break;
1142
1289
  case "request":
1143
1290
  if (message.id && message.payload) {
1144
- telemetry.debug(
1145
- EventTypes.OPENCODE_REQUEST_RECEIVED,
1146
- `Request: ${message.payload.method} ${message.payload.path}`,
1147
- {
1148
- requestId: message.id,
1149
- method: message.payload.method,
1150
- path: message.payload.path
1151
- },
1152
- state.sandboxId ?? void 0
1153
- );
1154
- const response = await forwardToOpenCode(port, message.payload, message.id, state);
1291
+ const startTime = Date.now();
1292
+ onRequest?.(message.payload.method, message.payload.path, message.id);
1293
+ const response = await forwardToOpenCode(port, message.payload);
1294
+ const durationMs = Date.now() - startTime;
1295
+ onResponse?.(response.status, durationMs, message.id);
1155
1296
  sendResponse(ws, message.id, response);
1156
1297
  }
1157
1298
  break;
1158
1299
  case "subscribe_events":
1159
1300
  if (message.id) {
1160
- void subscribeToOpenCodeEvents(port, message.id, ws, state);
1301
+ const abortController = new AbortController();
1302
+ activeEventSubscriptions.set(message.id, abortController);
1303
+ onInfo?.(`Starting event subscription ${message.id.slice(0, 8)}`);
1304
+ subscribeToOpenCodeEvents(port, message.id, ws, abortController).catch((error2) => {
1305
+ if (!abortController.signal.aborted) {
1306
+ onError?.(`Event subscription failed: ${error2.message}`);
1307
+ }
1308
+ }).finally(() => {
1309
+ activeEventSubscriptions.delete(message.id);
1310
+ });
1161
1311
  }
1162
1312
  break;
1163
1313
  case "unsubscribe_events":
1164
1314
  if (message.id) {
1165
- cancelEventSubscription(message.id, state);
1315
+ const controller = activeEventSubscriptions.get(message.id);
1316
+ if (controller) {
1317
+ controller.abort();
1318
+ activeEventSubscriptions.delete(message.id);
1319
+ }
1166
1320
  }
1167
1321
  break;
1168
1322
  }
1169
1323
  } catch (error2) {
1170
1324
  const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
1171
- logActivity(state, {
1172
- type: "error",
1173
- error: `Failed to handle message: ${errorMessage}`
1174
- });
1175
- telemetry.error(
1176
- EventTypes.TUNNEL_ERROR,
1177
- `Failed to handle message: ${errorMessage}`,
1178
- {
1179
- error: errorMessage
1180
- },
1181
- state.sandboxId ?? void 0
1182
- );
1183
- displayStatus(state);
1325
+ onError?.(`Failed to handle message: ${errorMessage}`);
1184
1326
  }
1185
1327
  });
1328
+ ws.on("error", (error2) => {
1329
+ clearTimeout(connectionTimeout);
1330
+ onError?.(`Connection error: ${error2.message}`);
1331
+ reject(error2);
1332
+ });
1186
1333
  ws.on("close", (code, reason) => {
1187
- state.connected = false;
1188
1334
  const reasonStr = reason.toString() || "No reason provided";
1189
- logActivity(state, {
1190
- type: "info",
1191
- message: `Disconnected (code: ${code}, reason: ${reasonStr})`
1192
- });
1193
- telemetry.info(
1194
- EventTypes.TUNNEL_DISCONNECTED,
1195
- "Tunnel disconnected",
1196
- {
1197
- sandboxId: state.sandboxId,
1198
- code,
1199
- reason: reasonStr
1200
- },
1201
- state.sandboxId ?? void 0
1202
- );
1203
- displayStatus(state);
1204
- resolve();
1335
+ onDisconnected?.(code, reasonStr);
1336
+ for (const [, controller] of activeEventSubscriptions) {
1337
+ controller.abort();
1338
+ }
1339
+ activeEventSubscriptions.clear();
1205
1340
  });
1206
- ws.on("error", (error2) => {
1207
- state.connected = false;
1208
- logActivity(state, {
1209
- type: "error",
1210
- error: `Connection error: ${error2.message}`
1211
- });
1212
- telemetry.error(
1213
- EventTypes.TUNNEL_ERROR,
1214
- `Connection error: ${error2.message}`,
1215
- {
1216
- error: error2.message
1217
- },
1218
- state.sandboxId ?? void 0
1219
- );
1220
- displayStatus(state);
1341
+ });
1342
+ }
1343
+
1344
+ // src/commands/run.ts
1345
+ var MAX_ACTIVITY_LOG_ENTRIES = 10;
1346
+ var MESSAGE_POLL_INTERVAL_MS = 2e3;
1347
+ var MAX_CONSECUTIVE_FETCH_FAILURES = 3;
1348
+ var LOCK_HEARTBEAT_INTERVAL_MS = 5 * 60 * 1e3;
1349
+ async function resolveAgentIdFromKey(authHeader) {
1350
+ const apiUrl = getApiUrlConfig();
1351
+ try {
1352
+ const response = await fetch(`${apiUrl}/me`, {
1353
+ headers: { Authorization: authHeader }
1221
1354
  });
1222
- const cleanup = async () => {
1223
- process.removeAllListeners("SIGINT");
1224
- process.removeAllListeners("SIGTERM");
1225
- logActivity(state, {
1226
- type: "info",
1227
- message: "Shutting down..."
1228
- });
1229
- displayStatus(state);
1230
- telemetry.info(
1231
- EventTypes.TUNNEL_DISCONNECTED,
1232
- "Tunnel stopped by user",
1233
- {
1234
- sandboxId: state.sandboxId
1235
- },
1236
- state.sandboxId ?? void 0
1237
- );
1238
- await shutdownTelemetry();
1239
- ws.close();
1240
- process.exit(0);
1355
+ if (!response.ok) {
1356
+ return { error: `Failed to resolve agent from key: HTTP ${response.status}` };
1357
+ }
1358
+ const data = await response.json();
1359
+ if (data.auth_type === "agent_key" && data.agent_id) {
1360
+ return { agent_id: data.agent_id };
1361
+ }
1362
+ return {
1363
+ error: "Cannot resolve agent ID: auth type is not agent_key. Please provide --agent explicitly."
1241
1364
  };
1242
- process.removeAllListeners("SIGINT");
1243
- process.removeAllListeners("SIGTERM");
1244
- process.once("SIGINT", () => void cleanup());
1245
- process.once("SIGTERM", () => void cleanup());
1246
- });
1365
+ } catch (error2) {
1366
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
1367
+ return { error: `Failed to resolve agent from key: ${message}` };
1368
+ }
1247
1369
  }
1248
- async function tunnel(options) {
1249
- const verbose = options.verbose ?? false;
1250
- const credentials = await getToken();
1251
- if (!credentials) {
1252
- const cliName = (await import("./config-J7LPYFVS.js")).getCliName();
1253
- telemetry.error(EventTypes.CLI_ERROR, "Not logged in", { command: "tunnel" });
1254
- printError(`Not logged in. Run \`${cliName} login\` first.`);
1255
- process.exit(1);
1370
+ async function getAgentInfo(agentId, authHeader) {
1371
+ const apiUrl = getApiUrlConfig();
1372
+ try {
1373
+ const response = await fetch(`${apiUrl}/agents/${agentId}`, {
1374
+ headers: { Authorization: authHeader }
1375
+ });
1376
+ if (response.status === 404) {
1377
+ return { valid: false, error: "Agent not found" };
1378
+ }
1379
+ if (response.status === 401) {
1380
+ return { valid: false, error: "Authentication failed", authFailed: true };
1381
+ }
1382
+ if (!response.ok) {
1383
+ return { valid: false, error: `API error: ${response.status}` };
1384
+ }
1385
+ const agent = await response.json();
1386
+ if (agent.sandbox_type !== "local" && agent.sandbox_type !== "github_actions") {
1387
+ return {
1388
+ valid: false,
1389
+ error: `Agent is type '${agent.sandbox_type}', must be 'local' or 'github_actions' for CLI connection`
1390
+ };
1391
+ }
1392
+ return { valid: true, agent };
1393
+ } catch (error2) {
1394
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
1395
+ return { valid: false, error: `Failed to validate agent: ${message}` };
1256
1396
  }
1257
- const port = options.port ?? 4096;
1258
- const sandboxId = options.sandbox;
1259
- if (!sandboxId) {
1260
- const cliName = (await import("./config-J7LPYFVS.js")).getCliName();
1261
- printError("--sandbox <id> is required");
1262
- blank();
1263
- console.log(chalk4.dim("To find your sandbox ID:"));
1264
- console.log(chalk4.dim(" 1. Create a remote sandbox in the Evident web UI"));
1265
- console.log(chalk4.dim(" 2. Copy the sandbox ID from the URL or settings"));
1266
- console.log(chalk4.dim(` 3. Run: ${cliName} tunnel --sandbox <id>`));
1267
- blank();
1268
- telemetry.error(EventTypes.CLI_ERROR, "Missing sandbox ID", { command: "tunnel" });
1269
- process.exit(1);
1397
+ }
1398
+ var AuthenticationError = class extends Error {
1399
+ constructor(message) {
1400
+ super(message);
1401
+ this.name = "AuthenticationError";
1270
1402
  }
1271
- const state = {
1272
- connected: false,
1273
- opencodeConnected: false,
1274
- opencodeVersion: null,
1275
- sandboxId,
1276
- sandboxName: null,
1277
- reconnectAttempt: 0,
1278
- lastActivity: /* @__PURE__ */ new Date(),
1279
- activityLog: [],
1280
- pendingRequests: /* @__PURE__ */ new Map(),
1281
- verbose,
1282
- displayInitialized: false,
1283
- activeEventSubscriptions: /* @__PURE__ */ new Map()
1284
- };
1285
- telemetry.info(
1286
- EventTypes.CLI_COMMAND,
1287
- "Starting tunnel command",
1403
+ };
1404
+ function checkAuthResponse(response, context) {
1405
+ if (response.status === 401 || response.status === 403) {
1406
+ throw new AuthenticationError(
1407
+ `Authentication failed during ${context}: HTTP ${response.status}. Your session may have expired.`
1408
+ );
1409
+ }
1410
+ }
1411
+ async function getPendingConversations(agentId, authHeader, conversationFilter) {
1412
+ const apiUrl = getApiUrlConfig();
1413
+ const response = await fetch(`${apiUrl}/agents/${agentId}/conversations/pending`, {
1414
+ headers: { Authorization: authHeader }
1415
+ });
1416
+ checkAuthResponse(response, "fetching pending conversations");
1417
+ if (!response.ok) {
1418
+ throw new Error(`Failed to get pending conversations: HTTP ${response.status}`);
1419
+ }
1420
+ const data = await response.json();
1421
+ let conversations = data.conversations;
1422
+ if (conversationFilter) {
1423
+ conversations = conversations.filter((c) => c.id === conversationFilter);
1424
+ }
1425
+ return conversations;
1426
+ }
1427
+ async function getPendingMessages(agentId, conversationId, authHeader) {
1428
+ const apiUrl = getApiUrlConfig();
1429
+ const response = await fetch(
1430
+ `${apiUrl}/agents/${agentId}/threads/${conversationId}/messages?status=pending`,
1431
+ { headers: { Authorization: authHeader } }
1432
+ );
1433
+ checkAuthResponse(response, "fetching pending messages");
1434
+ if (!response.ok) {
1435
+ throw new Error(`Failed to get messages: HTTP ${response.status}`);
1436
+ }
1437
+ return response.json();
1438
+ }
1439
+ async function markMessageProcessing(agentId, conversationId, messageId, authHeader) {
1440
+ const apiUrl = getApiUrlConfig();
1441
+ const response = await fetch(
1442
+ `${apiUrl}/agents/${agentId}/threads/${conversationId}/messages/${messageId}`,
1288
1443
  {
1289
- command: "tunnel",
1290
- port,
1291
- sandboxId,
1292
- verbose
1293
- },
1294
- sandboxId
1444
+ method: "PATCH",
1445
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
1446
+ body: JSON.stringify({ status: "processing" })
1447
+ }
1295
1448
  );
1296
- logActivity(state, {
1297
- type: "info",
1298
- message: `Starting tunnel (port: ${port}, verbose: ${verbose})`
1299
- });
1300
- logActivity(state, {
1301
- type: "info",
1302
- message: "Validating sandbox..."
1303
- });
1304
- const validateSpinner = ora2("Validating sandbox...").start();
1305
- const validation = await validateSandbox(credentials.token, sandboxId);
1306
- if (!validation.valid) {
1307
- validateSpinner.fail(`Sandbox validation failed: ${validation.error}`);
1308
- logActivity(state, {
1309
- type: "error",
1310
- error: `Sandbox validation failed: ${validation.error}`
1311
- });
1312
- telemetry.error(EventTypes.CLI_ERROR, `Sandbox validation failed: ${validation.error}`, {
1313
- command: "tunnel",
1314
- sandboxId
1449
+ checkAuthResponse(response, "marking message as processing");
1450
+ return response.ok;
1451
+ }
1452
+ async function reportInteractiveEvent(agentId, conversationId, type, data, authHeader) {
1453
+ const apiUrl = getApiUrlConfig();
1454
+ const response = await fetch(
1455
+ `${apiUrl}/agents/${agentId}/threads/${conversationId}/interactive-event`,
1456
+ {
1457
+ method: "POST",
1458
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
1459
+ body: JSON.stringify({ type, data })
1460
+ }
1461
+ );
1462
+ checkAuthResponse(response, "reporting interactive event");
1463
+ if (!response.ok) {
1464
+ throw new Error(`Failed to report interactive event: HTTP ${response.status}`);
1465
+ }
1466
+ }
1467
+ async function markMessageDone(agentId, conversationId, messageId, authHeader, sessionId) {
1468
+ const apiUrl = getApiUrlConfig();
1469
+ const body = { status: "done" };
1470
+ if (sessionId) {
1471
+ body.opencode_session_id = sessionId;
1472
+ }
1473
+ const response = await fetch(
1474
+ `${apiUrl}/agents/${agentId}/threads/${conversationId}/messages/${messageId}`,
1475
+ {
1476
+ method: "PATCH",
1477
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
1478
+ body: JSON.stringify(body)
1479
+ }
1480
+ );
1481
+ checkAuthResponse(response, "marking message as done");
1482
+ }
1483
+ async function markMessageFailed(agentId, conversationId, messageId, authHeader) {
1484
+ const apiUrl = getApiUrlConfig();
1485
+ const response = await fetch(
1486
+ `${apiUrl}/agents/${agentId}/threads/${conversationId}/messages/${messageId}`,
1487
+ {
1488
+ method: "PATCH",
1489
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
1490
+ body: JSON.stringify({ status: "failed" })
1491
+ }
1492
+ );
1493
+ checkAuthResponse(response, "marking message as failed");
1494
+ }
1495
+ async function acquireConversationLock(agentId, conversationId, correlationId, authHeader) {
1496
+ const apiUrl = getApiUrlConfig();
1497
+ try {
1498
+ const response = await fetch(`${apiUrl}/agents/${agentId}/threads/${conversationId}/lock`, {
1499
+ method: "POST",
1500
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
1501
+ body: JSON.stringify({ correlation_id: correlationId })
1315
1502
  });
1316
- displayStatus(state);
1317
- process.exit(1);
1503
+ checkAuthResponse(response, "acquiring conversation lock");
1504
+ if (response.status === 409) {
1505
+ return { acquired: false, error: "Conversation already locked by another runner" };
1506
+ }
1507
+ if (!response.ok) {
1508
+ return { acquired: false, error: `Failed to acquire lock: HTTP ${response.status}` };
1509
+ }
1510
+ return { acquired: true };
1511
+ } catch (error2) {
1512
+ if (error2 instanceof AuthenticationError) throw error2;
1513
+ return { acquired: false, error: String(error2) };
1318
1514
  }
1319
- state.sandboxName = validation.name ?? null;
1320
- validateSpinner.succeed(`Sandbox: ${validation.name || sandboxId}`);
1321
- logActivity(state, {
1322
- type: "info",
1323
- message: `Sandbox validated: ${validation.name || sandboxId}`
1515
+ }
1516
+ async function extendConversationLock(agentId, conversationId, correlationId, authHeader) {
1517
+ const apiUrl = getApiUrlConfig();
1518
+ try {
1519
+ const response = await fetch(
1520
+ `${apiUrl}/agents/${agentId}/threads/${conversationId}/lock/extend`,
1521
+ {
1522
+ method: "POST",
1523
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
1524
+ body: JSON.stringify({ correlation_id: correlationId })
1525
+ }
1526
+ );
1527
+ return response.ok;
1528
+ } catch {
1529
+ return false;
1530
+ }
1531
+ }
1532
+ async function releaseConversationLock(agentId, conversationId, correlationId, authHeader) {
1533
+ const apiUrl = getApiUrlConfig();
1534
+ try {
1535
+ await fetch(
1536
+ `${apiUrl}/agents/${agentId}/threads/${conversationId}/lock?correlation_id=${encodeURIComponent(correlationId)}`,
1537
+ {
1538
+ method: "DELETE",
1539
+ headers: { Authorization: authHeader }
1540
+ }
1541
+ );
1542
+ } catch {
1543
+ }
1544
+ }
1545
+ async function updateConversationSession(agentId, conversationId, sessionId, authHeader) {
1546
+ const apiUrl = getApiUrlConfig();
1547
+ const response = await fetch(`${apiUrl}/agents/${agentId}/threads/${conversationId}`, {
1548
+ method: "PATCH",
1549
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
1550
+ body: JSON.stringify({ opencode_session_id: sessionId })
1324
1551
  });
1325
- logActivity(state, {
1326
- type: "info",
1327
- message: `Checking OpenCode on port ${port}...`
1552
+ checkAuthResponse(response, "updating conversation session");
1553
+ if (!response.ok) {
1554
+ const text = await response.text().catch(() => "");
1555
+ throw new Error(
1556
+ `Failed to update conversation session: HTTP ${response.status}${text ? `: ${text}` : ""}`
1557
+ );
1558
+ }
1559
+ }
1560
+ async function updateConversationTitle(agentId, conversationId, title, authHeader) {
1561
+ const apiUrl = getApiUrlConfig();
1562
+ const response = await fetch(`${apiUrl}/agents/${agentId}/threads/${conversationId}`, {
1563
+ method: "PATCH",
1564
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
1565
+ body: JSON.stringify({ title })
1328
1566
  });
1329
- const opencodeSpinner = ora2("Checking OpenCode connection...").start();
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})` : "";
1335
- telemetry.info(
1336
- EventTypes.OPENCODE_HEALTH_OK,
1337
- `OpenCode healthy on port ${port}`,
1338
- {
1339
- port,
1340
- version: healthCheck.version
1341
- },
1342
- sandboxId
1567
+ checkAuthResponse(response, "updating conversation title");
1568
+ }
1569
+ function log(state, message, isError = false) {
1570
+ if (state.json) {
1571
+ console.log(
1572
+ JSON.stringify({
1573
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1574
+ level: isError ? "error" : "info",
1575
+ message
1576
+ })
1343
1577
  );
1344
- opencodeSpinner.succeed(`OpenCode running on port ${port}${version}`);
1345
- logActivity(state, {
1346
- type: "info",
1347
- message: `OpenCode running on port ${port}${version}`
1348
- });
1578
+ } else if (!state.interactive) {
1579
+ const prefix = isError ? chalk5.red("\u2717") : chalk5.green("\u2022");
1580
+ console.log(`${prefix} ${message}`);
1581
+ }
1582
+ }
1583
+ function logActivity(state, entry) {
1584
+ const fullEntry = {
1585
+ ...entry,
1586
+ timestamp: /* @__PURE__ */ new Date()
1587
+ };
1588
+ state.activityLog.push(fullEntry);
1589
+ if (state.activityLog.length > MAX_ACTIVITY_LOG_ENTRIES) {
1590
+ state.activityLog.shift();
1591
+ }
1592
+ state.lastActivity = fullEntry.timestamp;
1593
+ if (!state.interactive) {
1594
+ if (entry.type === "error") {
1595
+ log(state, entry.error ?? "Unknown error", true);
1596
+ } else if (entry.type === "info" && entry.message) {
1597
+ log(state, entry.message);
1598
+ }
1599
+ }
1600
+ }
1601
+ var ANSI = {
1602
+ moveUp: (n) => `\x1B[${n}A`
1603
+ };
1604
+ var STATUS_DISPLAY_HEIGHT = 22;
1605
+ function colorizeStatus(status) {
1606
+ if (status >= 200 && status < 300) {
1607
+ return chalk5.green(status.toString());
1608
+ } else if (status >= 300 && status < 400) {
1609
+ return chalk5.yellow(status.toString());
1610
+ } else if (status >= 400 && status < 500) {
1611
+ return chalk5.red(status.toString());
1612
+ } else if (status >= 500) {
1613
+ return chalk5.bgRed.white(` ${status} `);
1614
+ }
1615
+ return status.toString();
1616
+ }
1617
+ function formatActivityEntry(entry) {
1618
+ const time = entry.timestamp.toLocaleTimeString("en-US", {
1619
+ hour12: false,
1620
+ hour: "2-digit",
1621
+ minute: "2-digit",
1622
+ second: "2-digit"
1623
+ });
1624
+ switch (entry.type) {
1625
+ case "request": {
1626
+ const duration = entry.durationMs ? ` (${entry.durationMs}ms)` : "";
1627
+ const status = entry.status ? ` -> ${colorizeStatus(entry.status)}` : " ...";
1628
+ return ` ${chalk5.dim(`[${time}]`)} ${chalk5.cyan("<-")} ${entry.method} ${entry.path}${status}${duration}`;
1629
+ }
1630
+ case "response": {
1631
+ const duration = entry.durationMs ? ` (${entry.durationMs}ms)` : "";
1632
+ return ` ${chalk5.dim(`[${time}]`)} ${chalk5.green("->")} ${entry.method} ${entry.path} ${colorizeStatus(entry.status)}${duration}`;
1633
+ }
1634
+ case "error": {
1635
+ const errorMsg = entry.error || "Unknown error";
1636
+ const path = entry.path ? ` ${entry.method} ${entry.path}` : "";
1637
+ return ` ${chalk5.dim(`[${time}]`)} ${chalk5.red("x")}${path} - ${chalk5.red(errorMsg)}`;
1638
+ }
1639
+ case "info": {
1640
+ return ` ${chalk5.dim(`[${time}]`)} ${chalk5.blue("*")} ${entry.message}`;
1641
+ }
1642
+ default:
1643
+ return ` ${chalk5.dim(`[${time}]`)} ${entry.message || "Unknown"}`;
1644
+ }
1645
+ }
1646
+ function displayStatus(state) {
1647
+ if (!state.interactive) return;
1648
+ const lines = [];
1649
+ lines.push(chalk5.bold("Evident"));
1650
+ lines.push(chalk5.dim("-".repeat(60)));
1651
+ lines.push("");
1652
+ if (state.agentName) {
1653
+ lines.push(` Agent: ${state.agentName}`);
1654
+ }
1655
+ lines.push(` ID: ${state.agentId}`);
1656
+ if (state.conversationFilter) {
1657
+ lines.push(` Filter: conversation ${state.conversationFilter.slice(0, 8)}...`);
1658
+ }
1659
+ lines.push("");
1660
+ if (state.connected) {
1661
+ lines.push(` ${chalk5.green("*")} Tunnel: ${chalk5.green("Connected to Evident")}`);
1349
1662
  } else {
1350
- telemetry.warn(
1351
- EventTypes.OPENCODE_HEALTH_FAILED,
1352
- `Could not connect to OpenCode: ${healthCheck.error}`,
1663
+ if (state.reconnectAttempt > 0) {
1664
+ lines.push(
1665
+ ` ${chalk5.yellow("o")} Tunnel: ${chalk5.yellow(`Reconnecting... (attempt ${state.reconnectAttempt})`)}`
1666
+ );
1667
+ } else {
1668
+ lines.push(` ${chalk5.yellow("o")} Tunnel: ${chalk5.yellow("Connecting...")}`);
1669
+ }
1670
+ }
1671
+ if (state.opencodeConnected) {
1672
+ const version = state.opencodeVersion ? `, v${state.opencodeVersion}` : "";
1673
+ lines.push(
1674
+ ` ${chalk5.green("*")} OpenCode: ${chalk5.green(`Running on port ${state.port}${version}`)}`
1675
+ );
1676
+ } else {
1677
+ lines.push(` ${chalk5.red("o")} OpenCode: ${chalk5.red(`Not connected (port ${state.port})`)}`);
1678
+ }
1679
+ lines.push("");
1680
+ if (state.messageCount > 0) {
1681
+ lines.push(` Messages: ${state.messageCount} processed`);
1682
+ lines.push("");
1683
+ }
1684
+ if (state.activityLog.length > 0) {
1685
+ lines.push(chalk5.bold(" Activity:"));
1686
+ for (const entry of state.activityLog) {
1687
+ lines.push(formatActivityEntry(entry));
1688
+ }
1689
+ } else {
1690
+ lines.push(chalk5.dim(" No activity yet. Waiting for requests..."));
1691
+ }
1692
+ lines.push("");
1693
+ lines.push(chalk5.dim("-".repeat(60)));
1694
+ if (state.verbose) {
1695
+ lines.push(chalk5.dim(" Verbose mode: ON"));
1696
+ }
1697
+ lines.push("");
1698
+ lines.push(
1699
+ chalk5.dim(` Tip: Run \`opencode attach http://localhost:${state.port}\` to see live activity`)
1700
+ );
1701
+ lines.push(chalk5.dim(" Press Ctrl+C to disconnect"));
1702
+ while (lines.length < STATUS_DISPLAY_HEIGHT) {
1703
+ lines.push("");
1704
+ }
1705
+ if (!state.displayInitialized) {
1706
+ console.log("");
1707
+ console.log(chalk5.dim("=".repeat(60)));
1708
+ console.log("");
1709
+ for (const line of lines) {
1710
+ console.log(line);
1711
+ }
1712
+ state.displayInitialized = true;
1713
+ } else {
1714
+ process.stdout.write(ANSI.moveUp(STATUS_DISPLAY_HEIGHT + 3));
1715
+ console.log(chalk5.dim("=".repeat(60)));
1716
+ console.log("");
1717
+ for (const line of lines) {
1718
+ process.stdout.write("\x1B[2K");
1719
+ console.log(line);
1720
+ }
1721
+ }
1722
+ }
1723
+ async function promptForLogin(promptMessage, successMessage) {
1724
+ const action = await select2({
1725
+ message: promptMessage,
1726
+ choices: [
1353
1727
  {
1354
- port,
1355
- error: healthCheck.error
1728
+ name: "Yes, log me in",
1729
+ value: "login",
1730
+ description: "Opens a browser to authenticate with Evident"
1356
1731
  },
1357
- sandboxId
1358
- );
1359
- opencodeSpinner.warn(`Could not connect to OpenCode on port ${port}`);
1360
- logActivity(state, {
1361
- type: "error",
1362
- error: `OpenCode not reachable on port ${port}: ${healthCheck.error}`
1363
- });
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}`));
1732
+ {
1733
+ name: "No, exit",
1734
+ value: "exit",
1735
+ description: "Exit without logging in"
1372
1736
  }
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
- );
1737
+ ]
1738
+ });
1739
+ if (action === "exit") {
1740
+ console.log(chalk5.dim(`
1741
+ You can log in later by running: ${getCliName()} login`));
1742
+ process.exit(0);
1743
+ }
1744
+ await login({ noBrowser: false });
1745
+ const credentials2 = await getToken();
1746
+ if (!credentials2) {
1747
+ printError("Login failed. Please try again.");
1748
+ process.exit(1);
1749
+ }
1750
+ blank();
1751
+ console.log(chalk5.green(successMessage));
1752
+ blank();
1753
+ return { token: credentials2.token, authType: "bearer", user: credentials2.user };
1754
+ }
1755
+ async function ensureOpenCodeRunning(state) {
1756
+ const healthCheck = await checkOpenCodeHealth(state.port);
1757
+ if (healthCheck.healthy) {
1758
+ state.opencodeConnected = true;
1759
+ state.opencodeVersion = healthCheck.version ?? null;
1760
+ return;
1761
+ }
1762
+ const runningInstances = await findHealthyOpenCodeInstances();
1763
+ if (runningInstances.length > 0) {
1764
+ if (!state.interactive) {
1765
+ throw new Error(
1766
+ `OpenCode not found on port ${state.port}, but running on port ${runningInstances[0].port}. Use --port ${runningInstances[0].port}`
1767
+ );
1768
+ }
1769
+ blank();
1770
+ console.log(chalk5.yellow("Found OpenCode running on different port(s):"));
1771
+ for (const instance of runningInstances) {
1772
+ const ver = instance.version ? ` (v${instance.version})` : "";
1773
+ const cwd = instance.cwd ? ` in ${instance.cwd}` : "";
1774
+ console.log(chalk5.dim(` * Port ${instance.port}${ver}${cwd}`));
1775
+ }
1776
+ blank();
1777
+ if (runningInstances.length === 1) {
1778
+ console.log(chalk5.yellow("Tip: Run with the correct port:"));
1779
+ console.log(
1780
+ chalk5.dim(
1781
+ ` ${getCliName()} run --agent ${state.agentId} --port ${runningInstances[0].port}`
1782
+ )
1783
+ );
1784
+ }
1785
+ blank();
1786
+ throw new Error(`OpenCode not running on port ${state.port}`);
1787
+ }
1788
+ if (!isOpenCodeInstalled()) {
1789
+ if (!state.interactive) {
1790
+ throw new Error("OpenCode is not installed. Install it with: npm install -g opencode");
1791
+ }
1792
+ const result = await promptOpenCodeInstall(true);
1793
+ if (result === "exit") {
1794
+ process.exit(0);
1795
+ }
1796
+ if (result === "installed" || isOpenCodeInstalled()) {
1797
+ } else {
1798
+ throw new Error("OpenCode is not installed");
1799
+ }
1800
+ }
1801
+ if (state.interactive) {
1802
+ let actualPort = state.port;
1803
+ if (isPortInUse(state.port)) {
1804
+ console.log(chalk5.yellow(`
1805
+ Port ${state.port} is already in use.`));
1806
+ const alternativePort = findAvailablePort(state.port + 1);
1807
+ if (alternativePort) {
1808
+ const useAlternative = await select2({
1809
+ message: `Use port ${alternativePort} instead?`,
1810
+ choices: [
1811
+ { name: `Yes, use port ${alternativePort}`, value: "yes" },
1812
+ { name: "No, I will free the port manually", value: "no" }
1813
+ ]
1814
+ });
1815
+ if (useAlternative === "yes") {
1816
+ actualPort = alternativePort;
1817
+ state.port = actualPort;
1818
+ } else {
1819
+ throw new Error(`Port ${state.port} is in use`);
1820
+ }
1386
1821
  }
1822
+ }
1823
+ const action = await select2({
1824
+ message: "OpenCode is not running. What would you like to do?",
1825
+ choices: [
1826
+ {
1827
+ name: "Start OpenCode for me",
1828
+ value: "start",
1829
+ description: `Run 'opencode serve --port ${actualPort}'`
1830
+ },
1831
+ {
1832
+ name: "Show me the command",
1833
+ value: "manual",
1834
+ description: "Display the command to run manually"
1835
+ },
1836
+ {
1837
+ name: "Continue without OpenCode",
1838
+ value: "continue",
1839
+ description: "Requests will fail until OpenCode starts"
1840
+ }
1841
+ ]
1842
+ });
1843
+ if (action === "manual") {
1387
1844
  blank();
1388
- } else {
1845
+ console.log(chalk5.bold("Run this command in another terminal:"));
1389
1846
  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
- ]
1847
+ console.log(` ${chalk5.cyan(`opencode serve --port ${actualPort}`)}`);
1848
+ blank();
1849
+ throw new Error("Please start OpenCode manually");
1850
+ }
1851
+ if (action === "start") {
1852
+ const spinner = ora2("Starting OpenCode...").start();
1853
+ state.opencodeProcess = await startOpenCode(actualPort);
1854
+ const health = await waitForOpenCodeHealth(actualPort, 3e4);
1855
+ if (!health.healthy) {
1856
+ spinner.fail("Failed to start OpenCode");
1857
+ throw new Error("OpenCode failed to start");
1858
+ }
1859
+ spinner.succeed(
1860
+ `OpenCode running on port ${actualPort}${health.version ? ` (v${health.version})` : ""}`
1861
+ );
1862
+ state.opencodeConnected = true;
1863
+ state.opencodeVersion = health.version ?? null;
1864
+ }
1865
+ } else {
1866
+ throw new Error(
1867
+ `OpenCode is not running on port ${state.port}. Start it with: opencode serve --port ${state.port}`
1868
+ );
1869
+ }
1870
+ }
1871
+ var AUTH_EXPIRED_EXIT_CODE = 77;
1872
+ async function handleAuthError(state, error2) {
1873
+ logActivity(state, {
1874
+ type: "error",
1875
+ error: error2.message
1876
+ });
1877
+ if (state.interactive) displayStatus(state);
1878
+ if (!state.interactive) {
1879
+ blank();
1880
+ console.log(chalk5.red("Authentication expired"));
1881
+ console.log(chalk5.dim("Your authentication token is no longer valid."));
1882
+ blank();
1883
+ console.log(chalk5.dim("To fix this:"));
1884
+ console.log(chalk5.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
1885
+ console.log(chalk5.dim(" 2. Restart this command"));
1886
+ blank();
1887
+ await cleanup(state);
1888
+ await shutdownTelemetry();
1889
+ process.exit(AUTH_EXPIRED_EXIT_CODE);
1890
+ return { success: false };
1891
+ }
1892
+ blank();
1893
+ console.log(chalk5.yellow("Your authentication has expired."));
1894
+ blank();
1895
+ try {
1896
+ const credentials2 = await promptForLogin(
1897
+ "Would you like to log in again?",
1898
+ "Re-authenticated successfully! Resuming..."
1899
+ );
1900
+ const newAuthHeader = getAuthHeader(credentials2);
1901
+ return { success: true, newAuthHeader };
1902
+ } catch {
1903
+ return { success: false };
1904
+ }
1905
+ }
1906
+ function isNetworkError(error2) {
1907
+ if (error2 instanceof Error) {
1908
+ const message = error2.message.toLowerCase();
1909
+ return message.includes("fetch failed") || message.includes("network") || message.includes("econnrefused") || message.includes("econnreset") || message.includes("etimedout") || message.includes("socket hang up");
1910
+ }
1911
+ return false;
1912
+ }
1913
+ async function processQueue(state, authHeader, triggerReconnect) {
1914
+ let idleStart = null;
1915
+ let currentAuthHeader = authHeader;
1916
+ while (state.running) {
1917
+ if (state.reconnecting && state.reconnectPromise) {
1918
+ logActivity(state, {
1919
+ type: "info",
1920
+ message: "Waiting for tunnel reconnection..."
1409
1921
  });
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
- )
1922
+ if (state.interactive) displayStatus(state);
1923
+ await state.reconnectPromise;
1924
+ }
1925
+ try {
1926
+ const conversations = await getPendingConversations(
1927
+ state.agentId,
1928
+ currentAuthHeader,
1929
+ state.conversationFilter ?? void 0
1930
+ );
1931
+ state.consecutiveFetchFailures = 0;
1932
+ if (conversations.length > 0) {
1933
+ idleStart = null;
1934
+ for (const conv of conversations) {
1935
+ if (!state.running) break;
1936
+ if (!state.lockedConversations.has(conv.id)) {
1937
+ const lockResult = await acquireConversationLock(
1938
+ state.agentId,
1939
+ conv.id,
1940
+ state.lockCorrelationId,
1941
+ currentAuthHeader
1437
1942
  );
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()];
1943
+ if (!lockResult.acquired) {
1944
+ logActivity(state, {
1945
+ type: "info",
1946
+ message: `Conversation ${conv.id.slice(0, 8)} locked by another runner \u2014 skipping`
1947
+ });
1948
+ if (state.interactive) displayStatus(state);
1949
+ continue;
1950
+ }
1951
+ state.lockedConversations.add(conv.id);
1952
+ logActivity(state, {
1953
+ type: "info",
1954
+ message: `Lock acquired on conversation ${conv.id.slice(0, 8)}`
1955
+ });
1451
1956
  }
1452
- const child = spawn(command, args, {
1453
- detached: true,
1454
- stdio: "ignore",
1455
- cwd: process.cwd()
1456
- // Start in current working directory
1957
+ logActivity(state, {
1958
+ type: "info",
1959
+ message: `Processing conversation ${conv.id.slice(0, 8)}... (${conv.pending_message_count} pending)`
1457
1960
  });
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;
1961
+ if (state.interactive) displayStatus(state);
1962
+ let sessionId = state.sessions.get(conv.id);
1963
+ if (!sessionId) {
1964
+ if (conv.opencode_session_id) {
1965
+ sessionId = conv.opencode_session_id;
1966
+ } else {
1967
+ sessionId = await createOpenCodeSession(state.port);
1968
+ await updateConversationSession(state.agentId, conv.id, sessionId, currentAuthHeader);
1969
+ logActivity(state, {
1970
+ type: "info",
1971
+ message: `Created session ${sessionId.slice(0, 8)}`
1972
+ });
1470
1973
  }
1471
- opencodeStartSpinner.text = `Starting OpenCode on port ${actualPort}... (${i + 1}/${maxRetries})`;
1974
+ state.sessions.set(conv.id, sessionId);
1472
1975
  }
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}`);
1976
+ const messages = await getPendingMessages(state.agentId, conv.id, currentAuthHeader);
1977
+ for (const message of messages) {
1978
+ if (!state.running) break;
1478
1979
  logActivity(state, {
1479
1980
  type: "info",
1480
- message: `OpenCode started on port ${actualPort}${versionStr}`
1981
+ message: `Processing message ${message.id.slice(0, 8)}...`
1481
1982
  });
1482
- } else {
1483
- opencodeStartSpinner.warn(
1484
- "OpenCode process started but not responding. Check if it started correctly."
1983
+ if (state.interactive) displayStatus(state);
1984
+ const claimed = await markMessageProcessing(
1985
+ state.agentId,
1986
+ conv.id,
1987
+ message.id,
1988
+ currentAuthHeader
1485
1989
  );
1990
+ if (!claimed) {
1991
+ logActivity(state, {
1992
+ type: "info",
1993
+ message: `Message ${message.id.slice(0, 8)} already claimed`
1994
+ });
1995
+ continue;
1996
+ }
1997
+ emitAgentMessageProcessing(state.agentId, {
1998
+ message_id: message.id,
1999
+ conversation_id: conv.id
2000
+ });
2001
+ try {
2002
+ const result = await sendMessageToOpenCode(
2003
+ state.port,
2004
+ sessionId,
2005
+ message.content,
2006
+ {
2007
+ agent: message.opencode_agent ?? void 0,
2008
+ model: message.opencode_model ?? void 0
2009
+ },
2010
+ {
2011
+ onQuestion: async (question) => {
2012
+ try {
2013
+ await reportInteractiveEvent(
2014
+ state.agentId,
2015
+ conv.id,
2016
+ "question",
2017
+ question,
2018
+ currentAuthHeader
2019
+ );
2020
+ logActivity(state, {
2021
+ type: "info",
2022
+ message: `Question surfaced to user (id: ${question.id.slice(0, 8)})`
2023
+ });
2024
+ } catch (err) {
2025
+ logActivity(state, {
2026
+ type: "error",
2027
+ error: `Failed to surface question: ${err}`
2028
+ });
2029
+ }
2030
+ },
2031
+ onPermission: async (permission) => {
2032
+ try {
2033
+ await reportInteractiveEvent(
2034
+ state.agentId,
2035
+ conv.id,
2036
+ "permission",
2037
+ permission,
2038
+ currentAuthHeader
2039
+ );
2040
+ logActivity(state, {
2041
+ type: "info",
2042
+ message: `Permission request surfaced to user (id: ${permission.id.slice(0, 8)})`
2043
+ });
2044
+ } catch (err) {
2045
+ logActivity(state, {
2046
+ type: "error",
2047
+ error: `Failed to surface permission: ${err}`
2048
+ });
2049
+ }
2050
+ }
2051
+ }
2052
+ );
2053
+ if (result.title) {
2054
+ try {
2055
+ await updateConversationTitle(
2056
+ state.agentId,
2057
+ conv.id,
2058
+ result.title,
2059
+ currentAuthHeader
2060
+ );
2061
+ } catch {
2062
+ }
2063
+ }
2064
+ await markMessageDone(
2065
+ state.agentId,
2066
+ conv.id,
2067
+ message.id,
2068
+ currentAuthHeader,
2069
+ sessionId
2070
+ );
2071
+ state.messageCount++;
2072
+ logActivity(state, {
2073
+ type: "info",
2074
+ message: `Message ${message.id.slice(0, 8)} processed`
2075
+ });
2076
+ emitAgentMessageDone(state.agentId, {
2077
+ message_id: message.id,
2078
+ conversation_id: conv.id
2079
+ });
2080
+ } catch (error2) {
2081
+ if (error2 instanceof AuthenticationError) {
2082
+ throw error2;
2083
+ }
2084
+ await markMessageFailed(state.agentId, conv.id, message.id, currentAuthHeader);
2085
+ logActivity(state, {
2086
+ type: "error",
2087
+ error: `Message ${message.id.slice(0, 8)} failed: ${error2}`
2088
+ });
2089
+ emitAgentMessageFailed(state.agentId, {
2090
+ message_id: message.id,
2091
+ conversation_id: conv.id,
2092
+ error: String(error2)
2093
+ });
2094
+ }
2095
+ if (state.interactive) displayStatus(state);
2096
+ }
2097
+ }
2098
+ } else {
2099
+ if (state.idleTimeout !== null) {
2100
+ if (idleStart === null) {
2101
+ idleStart = Date.now();
1486
2102
  logActivity(state, {
1487
2103
  type: "info",
1488
- message: "OpenCode may still be starting..."
2104
+ message: `Queue empty, waiting (timeout: ${state.idleTimeout}s)...`
1489
2105
  });
1490
- console.log(chalk4.dim("\nTip: Check for errors by running OpenCode manually:"));
1491
- console.log(chalk4.dim(` opencode serve --port ${actualPort}`));
1492
- blank();
2106
+ if (state.interactive) displayStatus(state);
1493
2107
  }
1494
- } catch (error2) {
1495
- const msg = error2 instanceof Error ? error2.message : "Unknown error";
1496
- opencodeStartSpinner.fail(`Failed to start OpenCode: ${msg}`);
2108
+ if (Date.now() - idleStart > state.idleTimeout * 1e3) {
2109
+ logActivity(state, {
2110
+ type: "info",
2111
+ message: "Idle timeout reached"
2112
+ });
2113
+ if (state.interactive) displayStatus(state);
2114
+ break;
2115
+ }
2116
+ }
2117
+ }
2118
+ await new Promise((resolve) => setTimeout(resolve, MESSAGE_POLL_INTERVAL_MS));
2119
+ } catch (error2) {
2120
+ if (error2 instanceof AuthenticationError) {
2121
+ const result = await handleAuthError(state, error2);
2122
+ if (result.success && result.newAuthHeader) {
2123
+ currentAuthHeader = result.newAuthHeader;
2124
+ state.authHeader = result.newAuthHeader;
1497
2125
  logActivity(state, {
1498
- type: "error",
1499
- error: `Failed to start OpenCode: ${msg}`
2126
+ type: "info",
2127
+ message: "Continuing with new credentials..."
1500
2128
  });
1501
- console.log(chalk4.yellow("\nYou can try starting it manually:"));
1502
- console.log(chalk4.dim(` opencode serve --port ${actualPort}`));
1503
- blank();
2129
+ if (state.interactive) displayStatus(state);
2130
+ continue;
2131
+ } else {
2132
+ state.running = false;
2133
+ break;
2134
+ }
2135
+ }
2136
+ const errorMessage = error2 instanceof Error ? error2.message : String(error2);
2137
+ logActivity(state, {
2138
+ type: "error",
2139
+ error: `Queue processing error: ${errorMessage}`
2140
+ });
2141
+ if (state.interactive) displayStatus(state);
2142
+ if (isNetworkError(error2)) {
2143
+ state.consecutiveFetchFailures++;
2144
+ if (state.consecutiveFetchFailures >= MAX_CONSECUTIVE_FETCH_FAILURES) {
2145
+ logActivity(state, {
2146
+ type: "info",
2147
+ message: `Detected ${state.consecutiveFetchFailures} consecutive fetch failures, triggering reconnection...`
2148
+ });
2149
+ if (state.interactive) displayStatus(state);
2150
+ await triggerReconnect();
2151
+ state.consecutiveFetchFailures = 0;
1504
2152
  }
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
2153
  }
2154
+ await new Promise((resolve) => setTimeout(resolve, MESSAGE_POLL_INTERVAL_MS));
1520
2155
  }
1521
2156
  }
1522
- while (true) {
1523
- try {
1524
- await connect(credentials.token, sandboxId, port, state);
1525
- state.reconnectAttempt++;
1526
- const delay = getReconnectDelay(state.reconnectAttempt);
2157
+ }
2158
+ async function cleanup(state, authHeader) {
2159
+ state.running = false;
2160
+ if (state.lockHeartbeatTimer) {
2161
+ clearInterval(state.lockHeartbeatTimer);
2162
+ state.lockHeartbeatTimer = null;
2163
+ }
2164
+ if (authHeader && state.lockedConversations.size > 0) {
2165
+ for (const convId of state.lockedConversations) {
2166
+ await releaseConversationLock(state.agentId, convId, state.lockCorrelationId, authHeader);
2167
+ }
2168
+ if (state.interactive) {
1527
2169
  logActivity(state, {
1528
2170
  type: "info",
1529
- message: `Reconnecting in ${Math.round(delay / 1e3)}s (attempt ${state.reconnectAttempt})...`
2171
+ message: `Released ${state.lockedConversations.size} lock(s)`
1530
2172
  });
1531
- telemetry.info(
1532
- EventTypes.TUNNEL_RECONNECTING,
1533
- `Reconnecting (attempt ${state.reconnectAttempt})`,
1534
- {
1535
- attempt: state.reconnectAttempt,
1536
- delayMs: delay
1537
- },
1538
- state.sandboxId ?? void 0
1539
- );
1540
2173
  displayStatus(state);
1541
- await sleep(delay);
1542
- } catch (error2) {
1543
- const message = error2 instanceof Error ? error2.message : "Unknown error";
1544
- if (message === "Unauthorized") {
1545
- telemetry.error(
1546
- EventTypes.CLI_ERROR,
1547
- "Authentication failed",
1548
- {
1549
- command: "tunnel",
1550
- error: message
1551
- },
1552
- state.sandboxId ?? void 0
1553
- );
1554
- await shutdownTelemetry();
1555
- displayError(state, "Authentication failed", "Please run `evident login` again.");
2174
+ } else {
2175
+ log(state, `Released ${state.lockedConversations.size} lock(s)`);
2176
+ }
2177
+ state.lockedConversations.clear();
2178
+ }
2179
+ if (state.tunnelConnection) {
2180
+ state.tunnelConnection.close();
2181
+ state.tunnelConnection = null;
2182
+ }
2183
+ if (state.opencodeProcess) {
2184
+ stopOpenCode(state.opencodeProcess);
2185
+ if (state.interactive) {
2186
+ logActivity(state, { type: "info", message: "Stopped OpenCode process" });
2187
+ displayStatus(state);
2188
+ } else {
2189
+ log(state, "Stopped OpenCode process");
2190
+ }
2191
+ state.opencodeProcess = null;
2192
+ }
2193
+ }
2194
+ async function run(options) {
2195
+ const interactive = isInteractive(options.json);
2196
+ const state = {
2197
+ agentId: options.agent || "",
2198
+ agentName: null,
2199
+ port: options.port ?? 4096,
2200
+ verbose: options.verbose ?? false,
2201
+ conversationFilter: options.conversation ?? null,
2202
+ idleTimeout: options.idleTimeout ?? null,
2203
+ json: options.json ?? false,
2204
+ interactive,
2205
+ connected: false,
2206
+ opencodeConnected: false,
2207
+ opencodeVersion: null,
2208
+ reconnectAttempt: 0,
2209
+ opencodeProcess: null,
2210
+ tunnelConnection: null,
2211
+ running: true,
2212
+ activityLog: [],
2213
+ displayInitialized: false,
2214
+ lastActivity: /* @__PURE__ */ new Date(),
2215
+ pendingRequests: /* @__PURE__ */ new Map(),
2216
+ sessions: /* @__PURE__ */ new Map(),
2217
+ messageCount: 0,
2218
+ lockCorrelationId: `cli-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2219
+ lockedConversations: /* @__PURE__ */ new Set(),
2220
+ lockHeartbeatTimer: null,
2221
+ consecutiveFetchFailures: 0,
2222
+ reconnecting: false,
2223
+ reconnectPromise: null,
2224
+ authHeader: ""
2225
+ };
2226
+ if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {
2227
+ log(
2228
+ state,
2229
+ "Warning: No --idle-timeout set in CI environment. The runner will poll indefinitely until the job times out. Consider adding --idle-timeout 30 to avoid wasting runner minutes.",
2230
+ false
2231
+ );
2232
+ }
2233
+ const handleSignal = async () => {
2234
+ if (state.interactive) {
2235
+ logActivity(state, { type: "info", message: "Shutting down..." });
2236
+ displayStatus(state);
2237
+ } else {
2238
+ log(state, "Shutting down...");
2239
+ }
2240
+ await cleanup(state, state.authHeader);
2241
+ await shutdownTelemetry();
2242
+ process.exit(0);
2243
+ };
2244
+ process.on("SIGINT", handleSignal);
2245
+ process.on("SIGTERM", handleSignal);
2246
+ try {
2247
+ let credentials2 = await getAuthCredentials();
2248
+ if (!credentials2) {
2249
+ if (!interactive) {
2250
+ printError("Authentication required");
2251
+ blank();
2252
+ console.log(chalk5.dim("Set EVIDENT_AGENT_KEY environment variable for CI"));
2253
+ console.log(chalk5.dim("Or run `evident login` for interactive authentication"));
2254
+ blank();
1556
2255
  process.exit(1);
1557
2256
  }
1558
- logActivity(state, {
1559
- type: "error",
1560
- error: message
1561
- });
1562
- telemetry.error(
1563
- EventTypes.TUNNEL_ERROR,
1564
- `Tunnel error: ${message}`,
1565
- {
1566
- error: message,
1567
- attempt: state.reconnectAttempt
1568
- },
1569
- state.sandboxId ?? void 0
2257
+ blank();
2258
+ console.log(chalk5.yellow("You are not logged in to Evident."));
2259
+ blank();
2260
+ credentials2 = await promptForLogin(
2261
+ "Would you like to log in now?",
2262
+ "Login successful! Continuing..."
1570
2263
  );
1571
- state.reconnectAttempt++;
1572
- const delay = getReconnectDelay(state.reconnectAttempt);
1573
- logActivity(state, {
1574
- type: "info",
1575
- message: `Reconnecting in ${Math.round(delay / 1e3)}s (attempt ${state.reconnectAttempt})...`
1576
- });
2264
+ }
2265
+ state.authHeader = getAuthHeader(credentials2);
2266
+ if (!state.agentId) {
2267
+ if (credentials2.authType === "agent_key") {
2268
+ const resolved = await resolveAgentIdFromKey(state.authHeader);
2269
+ if (resolved.agent_id) {
2270
+ state.agentId = resolved.agent_id;
2271
+ log(state, `Resolved agent ID from key: ${state.agentId}`);
2272
+ } else {
2273
+ printError(resolved.error || "Failed to resolve agent ID from key");
2274
+ process.exit(1);
2275
+ }
2276
+ } else {
2277
+ printError("--agent is required when not using EVIDENT_AGENT_KEY");
2278
+ blank();
2279
+ console.log(chalk5.dim("Either provide --agent <id> or set EVIDENT_AGENT_KEY"));
2280
+ blank();
2281
+ process.exit(1);
2282
+ }
2283
+ }
2284
+ telemetry.info(
2285
+ EventTypes.CLI_COMMAND,
2286
+ "Starting run command",
2287
+ {
2288
+ command: "run",
2289
+ agentId: state.agentId,
2290
+ port: state.port,
2291
+ conversationFilter: state.conversationFilter,
2292
+ interactive
2293
+ },
2294
+ state.agentId
2295
+ );
2296
+ if (interactive && !state.json) {
2297
+ blank();
2298
+ console.log(chalk5.bold("Evident Run"));
2299
+ console.log(chalk5.dim("-".repeat(40)));
2300
+ }
2301
+ const spinner = interactive && !state.json ? ora2("Validating agent...").start() : null;
2302
+ let validation = await getAgentInfo(state.agentId, state.authHeader);
2303
+ if (!validation.valid && validation.authFailed && interactive) {
2304
+ spinner?.fail("Authentication failed");
2305
+ blank();
2306
+ console.log(chalk5.yellow("Your authentication token is invalid or expired."));
2307
+ blank();
2308
+ credentials2 = await promptForLogin(
2309
+ "Would you like to log in again?",
2310
+ "Login successful! Retrying..."
2311
+ );
2312
+ state.authHeader = getAuthHeader(credentials2);
2313
+ spinner?.start("Validating agent...");
2314
+ validation = await getAgentInfo(state.agentId, state.authHeader);
2315
+ }
2316
+ if (!validation.valid) {
2317
+ spinner?.fail(`Agent validation failed: ${validation.error}`);
2318
+ throw new Error(validation.error);
2319
+ }
2320
+ spinner?.succeed(`Agent: ${validation.agent.name || state.agentId}`);
2321
+ state.agentName = validation.agent.name;
2322
+ const ocSpinner = interactive && !state.json ? ora2("Checking OpenCode...").start() : null;
2323
+ try {
2324
+ await ensureOpenCodeRunning(state);
2325
+ const version = state.opencodeVersion ? ` (v${state.opencodeVersion})` : "";
2326
+ ocSpinner?.succeed(`OpenCode running on port ${state.port}${version}`);
2327
+ } catch (error2) {
2328
+ ocSpinner?.fail(error2.message);
2329
+ throw error2;
2330
+ }
2331
+ const tunnelSpinner = interactive && !state.json ? ora2("Connecting tunnel...").start() : null;
2332
+ const connectWithRetry = async (isReconnect = false) => {
2333
+ if (isReconnect && state.reconnecting) {
2334
+ return;
2335
+ }
2336
+ state.reconnecting = true;
2337
+ if (state.tunnelConnection) {
2338
+ try {
2339
+ state.tunnelConnection.close();
2340
+ } catch {
2341
+ }
2342
+ state.tunnelConnection = null;
2343
+ }
2344
+ while (state.running) {
2345
+ try {
2346
+ state.tunnelConnection = await connectTunnel({
2347
+ agentId: state.agentId,
2348
+ authHeader: state.authHeader,
2349
+ port: state.port,
2350
+ onConnected: (agentId) => {
2351
+ state.connected = true;
2352
+ state.reconnectAttempt = 0;
2353
+ state.reconnecting = false;
2354
+ state.consecutiveFetchFailures = 0;
2355
+ state.agentId = agentId;
2356
+ logActivity(state, {
2357
+ type: "info",
2358
+ message: isReconnect ? `Tunnel reconnected (agent: ${agentId})` : `Tunnel connected (agent: ${agentId})`
2359
+ });
2360
+ emitAgentConnected(state.agentId, { port: state.port });
2361
+ if (state.interactive) displayStatus(state);
2362
+ },
2363
+ onDisconnected: (code, reason) => {
2364
+ state.connected = false;
2365
+ logActivity(state, {
2366
+ type: "info",
2367
+ message: `Tunnel disconnected (code: ${code}, reason: ${reason})`
2368
+ });
2369
+ emitAgentDisconnected(state.agentId, { code, reason });
2370
+ if (state.interactive) displayStatus(state);
2371
+ if (state.running && code !== 1e3 && !state.reconnecting) {
2372
+ logActivity(state, {
2373
+ type: "info",
2374
+ message: "Attempting automatic reconnection..."
2375
+ });
2376
+ if (state.interactive) displayStatus(state);
2377
+ state.reconnectPromise = connectWithRetry(true).catch((err) => {
2378
+ logActivity(state, {
2379
+ type: "error",
2380
+ error: `Reconnection failed: ${err.message}`
2381
+ });
2382
+ if (state.interactive) displayStatus(state);
2383
+ });
2384
+ }
2385
+ },
2386
+ onError: (error2) => {
2387
+ logActivity(state, { type: "error", error: error2 });
2388
+ if (state.interactive) displayStatus(state);
2389
+ },
2390
+ onRequest: (method, path, requestId) => {
2391
+ state.pendingRequests.set(requestId, {
2392
+ startTime: Date.now(),
2393
+ method,
2394
+ path
2395
+ });
2396
+ logActivity(state, { type: "request", method, path, requestId });
2397
+ if (state.interactive) displayStatus(state);
2398
+ },
2399
+ onResponse: (status, durationMs, requestId) => {
2400
+ const pending = state.pendingRequests.get(requestId);
2401
+ state.pendingRequests.delete(requestId);
2402
+ state.opencodeConnected = true;
2403
+ const lastEntry = state.activityLog[state.activityLog.length - 1];
2404
+ if (lastEntry && lastEntry.requestId === requestId) {
2405
+ lastEntry.type = "response";
2406
+ lastEntry.status = status;
2407
+ lastEntry.durationMs = durationMs;
2408
+ } else if (pending) {
2409
+ logActivity(state, {
2410
+ type: "response",
2411
+ method: pending.method,
2412
+ path: pending.path,
2413
+ status,
2414
+ durationMs,
2415
+ requestId
2416
+ });
2417
+ }
2418
+ if (state.interactive) displayStatus(state);
2419
+ },
2420
+ onInfo: (message) => {
2421
+ logActivity(state, { type: "info", message });
2422
+ if (state.interactive) displayStatus(state);
2423
+ }
2424
+ });
2425
+ if (!isReconnect) {
2426
+ tunnelSpinner?.succeed("Tunnel connected");
2427
+ }
2428
+ return;
2429
+ } catch (error2) {
2430
+ state.reconnectAttempt++;
2431
+ const delay = getReconnectDelay(state.reconnectAttempt);
2432
+ if (error2.message === "Unauthorized") {
2433
+ state.reconnecting = false;
2434
+ if (!isReconnect) {
2435
+ tunnelSpinner?.fail("Unauthorized");
2436
+ }
2437
+ throw error2;
2438
+ }
2439
+ logActivity(state, {
2440
+ type: "error",
2441
+ error: `Connection failed, retrying in ${Math.round(delay / 1e3)}s...`
2442
+ });
2443
+ if (state.interactive) displayStatus(state);
2444
+ await new Promise((resolve) => setTimeout(resolve, delay));
2445
+ }
2446
+ }
2447
+ state.reconnecting = false;
2448
+ };
2449
+ const triggerReconnect = async () => {
2450
+ if (!state.reconnecting) {
2451
+ state.reconnectPromise = connectWithRetry(true).catch((err) => {
2452
+ logActivity(state, {
2453
+ type: "error",
2454
+ error: `Reconnection failed: ${err.message}`
2455
+ });
2456
+ if (state.interactive) displayStatus(state);
2457
+ });
2458
+ }
2459
+ if (state.reconnectPromise) {
2460
+ await state.reconnectPromise;
2461
+ }
2462
+ };
2463
+ await connectWithRetry(false);
2464
+ state.lockHeartbeatTimer = setInterval(async () => {
2465
+ for (const convId of state.lockedConversations) {
2466
+ const extended = await extendConversationLock(
2467
+ state.agentId,
2468
+ convId,
2469
+ state.lockCorrelationId,
2470
+ state.authHeader
2471
+ );
2472
+ if (!extended) {
2473
+ logActivity(state, {
2474
+ type: "error",
2475
+ error: `Failed to extend lock on conversation ${convId.slice(0, 8)}`
2476
+ });
2477
+ state.lockedConversations.delete(convId);
2478
+ }
2479
+ }
2480
+ }, LOCK_HEARTBEAT_INTERVAL_MS);
2481
+ if (interactive && !state.json) {
1577
2482
  displayStatus(state);
1578
- await sleep(delay);
2483
+ } else {
2484
+ log(state, "Processing queue...");
2485
+ }
2486
+ await processQueue(state, state.authHeader, triggerReconnect);
2487
+ await cleanup(state, state.authHeader);
2488
+ if (state.json) {
2489
+ console.log(
2490
+ JSON.stringify({
2491
+ status: "success",
2492
+ messages_processed: state.messageCount
2493
+ })
2494
+ );
2495
+ } else if (!interactive) {
2496
+ log(state, `Completed. Processed ${state.messageCount} message(s).`);
2497
+ }
2498
+ await shutdownTelemetry();
2499
+ process.exit(0);
2500
+ } catch (error2) {
2501
+ await cleanup(state, state.authHeader);
2502
+ const message = error2 instanceof Error ? error2.message : String(error2);
2503
+ if (state.json) {
2504
+ console.log(JSON.stringify({ status: "error", error: message }));
2505
+ } else {
2506
+ printError(message);
1579
2507
  }
2508
+ telemetry.error(EventTypes.CLI_ERROR, `Run command failed: ${message}`, {
2509
+ command: "run",
2510
+ agentId: options.agent
2511
+ });
2512
+ await shutdownTelemetry();
2513
+ process.exit(1);
1580
2514
  }
1581
2515
  }
1582
2516
 
@@ -1591,12 +2525,17 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
1591
2525
  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);
1592
2526
  program.command("logout").description("Remove stored credentials").action(logout);
1593
2527
  program.command("whoami").description("Show the currently logged in user").action(whoami);
1594
- program.command("tunnel").description("Establish a tunnel to Evident for Local Mode").requiredOption("-s, --sandbox <id>", "Sandbox ID to connect to (required)").option("-p, --port <port>", "OpenCode port (default: 4096)", "4096").option("-v, --verbose", "Show detailed request/response information").action((options) => {
1595
- tunnel({
1596
- sandbox: options.sandbox,
1597
- port: parseInt(options.port, 10),
1598
- verbose: options.verbose
1599
- });
1600
- });
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(
2529
+ (options) => {
2530
+ run({
2531
+ agent: options.agent,
2532
+ port: parseInt(options.port, 10),
2533
+ verbose: options.verbose,
2534
+ conversation: options.conversation,
2535
+ idleTimeout: options.idleTimeout ? parseInt(options.idleTimeout, 10) : void 0,
2536
+ json: options.json
2537
+ });
2538
+ }
2539
+ );
1601
2540
  program.parse();
1602
2541
  //# sourceMappingURL=index.js.map