@evident-ai/cli 0.1.6 → 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
@@ -14,7 +14,7 @@ import { homedir } from "os";
14
14
  import { join } from "path";
15
15
  var environmentPresets = {
16
16
  local: {
17
- apiUrl: "http://localhost:3000/v1",
17
+ apiUrl: "http://localhost:3001/v1",
18
18
  tunnelUrl: "ws://localhost:8787"
19
19
  },
20
20
  dev: {
@@ -80,6 +80,17 @@ function setCredentials(creds) {
80
80
  function clearCredentials() {
81
81
  credentials.clear();
82
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
+ }
83
94
 
84
95
  // src/lib/api.ts
85
96
  var ApiClient = class {
@@ -432,10 +443,26 @@ async function whoami() {
432
443
  blank();
433
444
  }
434
445
 
435
- // src/commands/tunnel.ts
436
- import WebSocket from "ws";
437
- import chalk4 from "chalk";
446
+ // src/commands/run.ts
447
+ import chalk5 from "chalk";
438
448
  import ora2 from "ora";
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;
439
466
 
440
467
  // src/lib/telemetry.ts
441
468
  var CLI_VERSION = process.env.npm_package_version || "unknown";
@@ -451,7 +478,7 @@ function logEvent(eventType, options = {}) {
451
478
  severity: options.severity || "info",
452
479
  message: options.message,
453
480
  metadata: options.metadata,
454
- sandbox_id: options.sandboxId,
481
+ agent_id: options.agentId,
455
482
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
456
483
  };
457
484
  eventBuffer.push(event);
@@ -465,10 +492,10 @@ function logEvent(eventType, options = {}) {
465
492
  }
466
493
  }
467
494
  var telemetry = {
468
- debug: (eventType, message, metadata, sandboxId) => logEvent(eventType, { severity: "debug", message, metadata, sandboxId }),
469
- info: (eventType, message, metadata, sandboxId) => logEvent(eventType, { severity: "info", message, metadata, sandboxId }),
470
- warn: (eventType, message, metadata, sandboxId) => logEvent(eventType, { severity: "warning", message, metadata, sandboxId }),
471
- 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 })
472
499
  };
473
500
  async function flushEvents() {
474
501
  if (eventBuffer.length === 0) return;
@@ -487,17 +514,18 @@ async function flushEvents() {
487
514
  const controller = new AbortController();
488
515
  const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
489
516
  try {
517
+ const request = {
518
+ events,
519
+ client_type: "cli",
520
+ client_version: CLI_VERSION
521
+ };
490
522
  const response = await fetch(`${apiUrl}/telemetry/events`, {
491
523
  method: "POST",
492
524
  headers: {
493
525
  "Content-Type": "application/json",
494
526
  Authorization: `Bearer ${credentials2.token}`
495
527
  },
496
- body: JSON.stringify({
497
- events,
498
- client_type: "cli",
499
- client_version: CLI_VERSION
500
- }),
528
+ body: JSON.stringify(request),
501
529
  signal: controller.signal
502
530
  });
503
531
  if (!response.ok) {
@@ -520,6 +548,59 @@ async function shutdownTelemetry() {
520
548
  }
521
549
  await flushEvents();
522
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
+ }
523
604
  var EventTypes = {
524
605
  // Tunnel lifecycle
525
606
  TUNNEL_STARTING: "tunnel.starting",
@@ -547,163 +628,467 @@ var EventTypes = {
547
628
  CLI_ERROR: "cli.error"
548
629
  };
549
630
 
550
- // src/commands/tunnel.ts
551
- var MAX_RECONNECT_DELAY = 3e4;
552
- var BASE_RECONNECT_DELAY = 500;
553
- var MAX_ACTIVITY_LOG_ENTRIES = 10;
554
- function logActivity(state, entry) {
555
- const fullEntry = {
556
- ...entry,
557
- timestamp: /* @__PURE__ */ new Date()
558
- };
559
- state.activityLog.push(fullEntry);
560
- if (state.activityLog.length > MAX_ACTIVITY_LOG_ENTRIES) {
561
- state.activityLog.shift();
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" };
562
636
  }
563
- state.lastActivity = fullEntry.timestamp;
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;
564
650
  }
565
- function formatActivityEntry(entry, _verbose) {
566
- const time = entry.timestamp.toLocaleTimeString("en-US", {
567
- hour12: false,
568
- hour: "2-digit",
569
- minute: "2-digit",
570
- second: "2-digit"
571
- });
572
- switch (entry.type) {
573
- case "request": {
574
- const duration = entry.durationMs ? ` (${entry.durationMs}ms)` : "";
575
- const status = entry.status ? ` \u2192 ${colorizeStatus(entry.status)}` : " ...";
576
- return ` ${chalk4.dim(`[${time}]`)} ${chalk4.cyan("\u2190")} ${entry.method} ${entry.path}${status}${duration}`;
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}` };
577
674
  }
578
- case "response": {
579
- const duration = entry.durationMs ? ` (${entry.durationMs}ms)` : "";
580
- return ` ${chalk4.dim(`[${time}]`)} ${chalk4.green("\u2192")} ${entry.method} ${entry.path} ${colorizeStatus(entry.status)}${duration}`;
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;
581
688
  }
582
- case "error": {
583
- const errorMsg = entry.error || "Unknown error";
584
- const path = entry.path ? ` ${entry.method} ${entry.path}` : "";
585
- return ` ${chalk4.dim(`[${time}]`)} ${chalk4.red("\u2717")}${path} - ${chalk4.red(errorMsg)}`;
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";
696
+ var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
697
+ function getProcessCwd(pid) {
698
+ const platform = process.platform;
699
+ try {
700
+ if (platform === "darwin") {
701
+ const output = execSync(`lsof -a -p ${pid} -d cwd -Fn 2>/dev/null`, {
702
+ encoding: "utf-8",
703
+ stdio: ["pipe", "pipe", "pipe"]
704
+ }).trim();
705
+ const lines = output.split("\n");
706
+ for (const line of lines) {
707
+ if (line.startsWith("n") && !line.startsWith("n ")) {
708
+ return line.slice(1);
709
+ }
710
+ }
711
+ } else if (platform === "linux") {
712
+ const output = execSync(`readlink /proc/${pid}/cwd 2>/dev/null`, {
713
+ encoding: "utf-8",
714
+ stdio: ["pipe", "pipe", "pipe"]
715
+ }).trim();
716
+ if (output) return output;
586
717
  }
587
- case "info": {
588
- return ` ${chalk4.dim(`[${time}]`)} ${chalk4.blue("\u25CF")} ${entry.message}`;
718
+ } catch {
719
+ }
720
+ return void 0;
721
+ }
722
+ function isPortInUse(port) {
723
+ const platform = process.platform;
724
+ try {
725
+ if (platform === "darwin" || platform === "linux") {
726
+ execSync(`lsof -i :${port} -sTCP:LISTEN 2>/dev/null`, {
727
+ encoding: "utf-8",
728
+ stdio: ["pipe", "pipe", "pipe"]
729
+ });
730
+ return true;
589
731
  }
590
- default:
591
- return ` ${chalk4.dim(`[${time}]`)} ${entry.message || "Unknown"}`;
732
+ } catch {
592
733
  }
734
+ return false;
593
735
  }
594
- function colorizeStatus(status) {
595
- if (status >= 200 && status < 300) {
596
- return chalk4.green(status.toString());
597
- } else if (status >= 300 && status < 400) {
598
- return chalk4.yellow(status.toString());
599
- } else if (status >= 400 && status < 500) {
600
- return chalk4.red(status.toString());
601
- } else if (status >= 500) {
602
- return chalk4.bgRed.white(` ${status} `);
736
+ function findAvailablePort(startPort, maxAttempts = 10) {
737
+ for (let i = 0; i < maxAttempts; i++) {
738
+ const port = startPort + i;
739
+ if (!isPortInUse(port)) {
740
+ return port;
741
+ }
603
742
  }
604
- return status.toString();
743
+ return null;
605
744
  }
606
- function displayStatus(state) {
607
- console.clear();
608
- console.log(chalk4.bold("Evident Tunnel"));
609
- console.log(chalk4.dim("\u2500".repeat(60)));
610
- blank();
611
- if (state.connected) {
612
- console.log(` ${chalk4.green("\u25CF")} Status: ${chalk4.green("Connected")}`);
613
- console.log(` Sandbox: ${state.sandboxId ?? "Unknown"}`);
614
- if (state.sandboxName) {
615
- console.log(` Name: ${state.sandboxName}`);
745
+ function findOpenCodeProcesses() {
746
+ const instances = [];
747
+ try {
748
+ const platform = process.platform;
749
+ if (platform === "darwin" || platform === "linux") {
750
+ let pids = [];
751
+ try {
752
+ const pgrepOutput = execSync('pgrep -f "opencode serve|opencode-serve"', {
753
+ encoding: "utf-8",
754
+ stdio: ["pipe", "pipe", "pipe"]
755
+ }).trim();
756
+ if (pgrepOutput) {
757
+ pids = pgrepOutput.split("\n").map((p) => parseInt(p.trim(), 10)).filter((p) => !isNaN(p));
758
+ }
759
+ } catch {
760
+ try {
761
+ const psOutput = execSync('ps aux | grep -E "opencode (serve|--port)" | grep -v grep', {
762
+ encoding: "utf-8",
763
+ stdio: ["pipe", "pipe", "pipe"]
764
+ }).trim();
765
+ if (psOutput) {
766
+ for (const line of psOutput.split("\n")) {
767
+ const parts = line.trim().split(/\s+/);
768
+ if (parts.length >= 2) {
769
+ const pid = parseInt(parts[1], 10);
770
+ if (!isNaN(pid)) pids.push(pid);
771
+ }
772
+ }
773
+ }
774
+ } catch {
775
+ }
776
+ }
777
+ for (const pid of pids) {
778
+ try {
779
+ const lsofOutput = execSync(`lsof -Pan -p ${pid} -i TCP -sTCP:LISTEN 2>/dev/null`, {
780
+ encoding: "utf-8",
781
+ stdio: ["pipe", "pipe", "pipe"]
782
+ }).trim();
783
+ for (const line of lsofOutput.split("\n")) {
784
+ const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
785
+ if (portMatch) {
786
+ const port = parseInt(portMatch[1], 10);
787
+ if (!isNaN(port) && !instances.some((i) => i.port === port)) {
788
+ const cwd = getProcessCwd(pid);
789
+ instances.push({ pid, port, cwd });
790
+ }
791
+ }
792
+ }
793
+ } catch {
794
+ }
795
+ }
616
796
  }
617
- } else {
618
- console.log(` ${chalk4.yellow("\u25CB")} Status: ${chalk4.yellow("Reconnecting...")}`);
619
- if (state.reconnectAttempt > 0) {
620
- console.log(` Attempt: ${state.reconnectAttempt}`);
797
+ } catch {
798
+ }
799
+ return instances;
800
+ }
801
+ async function scanPortsForOpenCode() {
802
+ const instances = [];
803
+ const checks = OPENCODE_PORT_RANGE.map(async (port) => {
804
+ const health = await checkOpenCodeHealth(port);
805
+ if (health.healthy) {
806
+ let pid = 0;
807
+ try {
808
+ const lsofOutput = execSync(`lsof -ti :${port} -sTCP:LISTEN 2>/dev/null`, {
809
+ encoding: "utf-8",
810
+ stdio: ["pipe", "pipe", "pipe"]
811
+ }).trim();
812
+ if (lsofOutput) {
813
+ pid = parseInt(lsofOutput.split("\n")[0], 10) || 0;
814
+ }
815
+ } catch {
816
+ }
817
+ const cwd = pid ? getProcessCwd(pid) : void 0;
818
+ return { pid, port, cwd, version: health.version };
819
+ }
820
+ return null;
821
+ });
822
+ const results = await Promise.all(checks);
823
+ for (const result of results) {
824
+ if (result) {
825
+ instances.push(result);
621
826
  }
622
827
  }
623
- blank();
624
- if (state.activityLog.length > 0) {
625
- console.log(chalk4.bold(" Activity:"));
626
- for (const entry of state.activityLog) {
627
- console.log(formatActivityEntry(entry, state.verbose));
828
+ return instances;
829
+ }
830
+ async function findHealthyOpenCodeInstances() {
831
+ const processes = findOpenCodeProcesses();
832
+ const healthy = [];
833
+ for (const proc of processes) {
834
+ const health = await checkOpenCodeHealth(proc.port);
835
+ if (health.healthy) {
836
+ healthy.push({ ...proc, version: health.version });
628
837
  }
629
- } else {
630
- console.log(chalk4.dim(" No activity yet. Waiting for requests..."));
631
838
  }
632
- blank();
633
- console.log(chalk4.dim("\u2500".repeat(60)));
634
- if (state.verbose) {
635
- console.log(chalk4.dim(" Verbose mode: ON (request/response bodies will be logged)"));
839
+ if (healthy.length === 0) {
840
+ const scanned = await scanPortsForOpenCode();
841
+ return scanned;
636
842
  }
637
- console.log(chalk4.dim(" Press Ctrl+C to disconnect"));
843
+ return healthy;
638
844
  }
639
- function displayError(_state, error2, details) {
640
- blank();
641
- console.log(chalk4.bgRed.white.bold(" ERROR "));
642
- console.log(chalk4.red(` ${error2}`));
643
- if (details) {
644
- console.log(chalk4.dim(` ${details}`));
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()];
645
853
  }
646
- blank();
854
+ const child = spawn(command, args, {
855
+ detached: true,
856
+ stdio: "ignore",
857
+ cwd: process.cwd()
858
+ });
859
+ return child;
647
860
  }
648
- async function validateSandbox(token, sandboxId) {
649
- const apiUrl = getApiUrlConfig();
861
+ function stopOpenCode(opencodeProcess) {
862
+ if (!opencodeProcess || !opencodeProcess.pid) {
863
+ return;
864
+ }
650
865
  try {
651
- const response = await fetch(`${apiUrl}/sandboxes/${sandboxId}`, {
652
- headers: {
653
- Authorization: `Bearer ${token}`
654
- }
655
- });
656
- if (response.status === 404) {
657
- return { valid: false, error: "Sandbox not found" };
866
+ if (process.platform === "win32") {
867
+ opencodeProcess.kill("SIGTERM");
868
+ } else {
869
+ process.kill(-opencodeProcess.pid, "SIGTERM");
658
870
  }
659
- if (response.status === 401) {
660
- return { valid: false, error: "Authentication failed. Please run `evident login` again." };
871
+ } catch {
872
+ }
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" });
661
887
  }
662
- if (!response.ok) {
663
- return { valid: false, error: `API error: ${response.status}` };
888
+ return true;
889
+ } catch {
890
+ return false;
891
+ }
892
+ }
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";
907
+ }
908
+ blank();
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"
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
+ ]
960
+ });
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
+ }
664
977
  }
665
- const sandbox = await response.json();
666
- if (sandbox.sandbox_type !== "remote") {
667
- return {
668
- valid: false,
669
- error: `Sandbox is type '${sandbox.sandbox_type}', must be 'remote' for tunnel connection`
978
+ return "exit";
979
+ }
980
+ return action;
981
+ }
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({})
989
+ });
990
+ if (!response.ok) {
991
+ throw new Error(`Failed to create session: HTTP ${response.status}`);
992
+ }
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)
670
1009
  };
671
1010
  }
672
- return { valid: true, name: sandbox.name };
673
- } catch (error2) {
674
- const message = error2 instanceof Error ? error2.message : "Unknown error";
675
- return { valid: false, error: `Failed to validate sandbox: ${message}` };
676
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
+ }
1049
+ }
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
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;
1078
+ }
1079
+ };
1080
+ const [result] = await Promise.all([sendMessage(), pollInteractive()]);
1081
+ return result;
677
1082
  }
678
- async function forwardToOpenCode(port, request, requestId, state) {
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) {
679
1091
  const url = `http://localhost:${port}${request.path}`;
680
- const startTime = Date.now();
681
- state.pendingRequests.set(requestId, {
682
- startTime,
683
- method: request.method,
684
- path: request.path
685
- });
686
- logActivity(state, {
687
- type: "request",
688
- method: request.method,
689
- path: request.path,
690
- requestId
691
- });
692
- displayStatus(state);
693
- if (state.verbose && request.body) {
694
- console.log(chalk4.dim(` Request body: ${JSON.stringify(request.body, null, 2)}`));
695
- }
696
- telemetry.debug(
697
- EventTypes.OPENCODE_REQUEST_FORWARDED,
698
- `Forwarding ${request.method} ${request.path}`,
699
- {
700
- method: request.method,
701
- path: request.path,
702
- port,
703
- requestId
704
- },
705
- state.sandboxId ?? void 0
706
- );
707
1092
  try {
708
1093
  const response = await fetch(url, {
709
1094
  method: request.method,
@@ -715,152 +1100,184 @@ async function forwardToOpenCode(port, request, requestId, state) {
715
1100
  });
716
1101
  let body;
717
1102
  const contentType = response.headers.get("Content-Type");
718
- if (contentType?.includes("application/json")) {
719
- body = await response.json();
720
- } else {
721
- body = await response.text();
722
- }
723
- const durationMs = Date.now() - startTime;
724
- state.pendingRequests.delete(requestId);
725
- const lastEntry = state.activityLog[state.activityLog.length - 1];
726
- if (lastEntry && lastEntry.requestId === requestId) {
727
- lastEntry.type = "response";
728
- lastEntry.status = response.status;
729
- lastEntry.durationMs = durationMs;
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
+ }
730
1112
  } else {
731
- logActivity(state, {
732
- type: "response",
733
- method: request.method,
734
- path: request.path,
735
- status: response.status,
736
- durationMs,
737
- requestId
738
- });
739
- }
740
- displayStatus(state);
741
- if (state.verbose && body) {
742
- const bodyStr = typeof body === "string" ? body : JSON.stringify(body, null, 2);
743
- const truncated = bodyStr.length > 500 ? bodyStr.substring(0, 500) + "..." : bodyStr;
744
- console.log(chalk4.dim(` Response body: ${truncated}`));
1113
+ body = text;
745
1114
  }
746
- telemetry.debug(
747
- EventTypes.OPENCODE_RESPONSE_SENT,
748
- `Response ${response.status}`,
749
- {
750
- status: response.status,
751
- path: request.path,
752
- durationMs,
753
- requestId
754
- },
755
- state.sandboxId ?? void 0
756
- );
757
1115
  return {
758
1116
  status: response.status,
759
1117
  body
760
1118
  };
761
1119
  } catch (error2) {
762
1120
  const message = error2 instanceof Error ? error2.message : "Unknown error";
763
- const durationMs = Date.now() - startTime;
764
- state.pendingRequests.delete(requestId);
765
- logActivity(state, {
766
- type: "error",
767
- method: request.method,
768
- path: request.path,
769
- error: `OpenCode unreachable: ${message}`,
770
- durationMs,
771
- requestId
772
- });
773
- displayStatus(state);
774
- telemetry.error(
775
- EventTypes.OPENCODE_UNREACHABLE,
776
- `Failed to connect to OpenCode: ${message}`,
777
- {
778
- port,
779
- path: request.path,
780
- error: message,
781
- requestId
782
- },
783
- state.sandboxId ?? void 0
784
- );
785
1121
  return {
786
1122
  status: 502,
787
1123
  body: { error: "Failed to connect to OpenCode", message }
788
1124
  };
789
1125
  }
790
1126
  }
791
- function getReconnectDelay(attempt) {
792
- const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
793
- const jitter = Math.random() * 1e3;
794
- return Math.min(exponentialDelay + jitter, MAX_RECONNECT_DELAY);
1127
+ function sendResponse(ws, requestId, response) {
1128
+ const bodyStr = JSON.stringify(response.body ?? null);
1129
+ const bodyBytes = Buffer.from(bodyStr, "utf-8");
1130
+ if (bodyBytes.length < CHUNK_THRESHOLD) {
1131
+ ws.send(
1132
+ JSON.stringify({
1133
+ type: "response",
1134
+ id: requestId,
1135
+ payload: response
1136
+ })
1137
+ );
1138
+ return;
1139
+ }
1140
+ sendResponseAsChunks(ws, requestId, response, bodyBytes);
795
1141
  }
796
- async function connect(token, sandboxId, port, state) {
797
- const tunnelUrl = getTunnelUrlConfig();
798
- const url = `${tunnelUrl}/tunnel/${sandboxId}/connect`;
799
- logActivity(state, {
800
- type: "info",
801
- message: "Connecting to tunnel relay..."
802
- });
803
- displayStatus(state);
804
- telemetry.info(
805
- EventTypes.TUNNEL_STARTING,
806
- `Connecting to ${url}`,
807
- {
808
- sandboxId,
809
- port,
810
- tunnelUrl
811
- },
812
- sandboxId
813
- );
814
- return new Promise((resolve, reject) => {
815
- const ws = new WebSocket(url, {
816
- headers: {
817
- Authorization: `Bearer ${token}`
1142
+ function sendResponseAsChunks(ws, requestId, response, bodyBytes) {
1143
+ const chunks = splitIntoChunks(bodyBytes, CHUNK_SIZE);
1144
+ ws.send(
1145
+ JSON.stringify({
1146
+ type: "response_start",
1147
+ id: requestId,
1148
+ total_chunks: chunks.length,
1149
+ total_size: bodyBytes.length,
1150
+ payload: {
1151
+ status: response.status,
1152
+ headers: response.headers
818
1153
  }
1154
+ })
1155
+ );
1156
+ for (let i = 0; i < chunks.length; i++) {
1157
+ ws.send(
1158
+ JSON.stringify({
1159
+ type: "response_chunk",
1160
+ id: requestId,
1161
+ chunk_index: i,
1162
+ data: chunks[i].toString("base64")
1163
+ })
1164
+ );
1165
+ }
1166
+ ws.send(
1167
+ JSON.stringify({
1168
+ type: "response_end",
1169
+ id: requestId
1170
+ })
1171
+ );
1172
+ }
1173
+ function splitIntoChunks(data, chunkSize) {
1174
+ const chunks = [];
1175
+ for (let i = 0; i < data.length; i += chunkSize) {
1176
+ chunks.push(data.subarray(i, i + chunkSize));
1177
+ }
1178
+ return chunks;
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
819
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;
1230
+ function getReconnectDelay(attempt) {
1231
+ const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
1232
+ const jitter = Math.random() * 1e3;
1233
+ return Math.min(exponentialDelay + jitter, MAX_RECONNECT_DELAY);
1234
+ }
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;
1247
+ const tunnelUrl = getTunnelUrlConfig();
1248
+ const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
1249
+ const activeEventSubscriptions = /* @__PURE__ */ new Map();
1250
+ return new Promise((resolve, reject) => {
1251
+ const ws = new WebSocket(url, {
1252
+ headers: {
1253
+ Authorization: authHeader
1254
+ }
1255
+ });
1256
+ const connectionTimeout = setTimeout(() => {
1257
+ ws.close();
1258
+ reject(new Error("Connection timeout"));
1259
+ }, 3e4);
820
1260
  ws.on("open", () => {
821
- state.connected = true;
822
- state.reconnectAttempt = 0;
823
- logActivity(state, {
824
- type: "info",
825
- message: "WebSocket connection established"
826
- });
827
- displayStatus(state);
1261
+ onInfo?.("WebSocket connection established");
828
1262
  });
829
1263
  ws.on("message", async (data) => {
830
1264
  try {
831
1265
  const message = JSON.parse(data.toString());
832
1266
  switch (message.type) {
833
- case "connected":
834
- state.sandboxId = message.sandbox_id ?? sandboxId;
835
- logActivity(state, {
836
- type: "info",
837
- 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
838
1275
  });
839
- telemetry.info(
840
- EventTypes.TUNNEL_CONNECTED,
841
- `Tunnel connected`,
842
- {
843
- sandboxId: message.sandbox_id
844
- },
845
- message.sandbox_id
846
- );
847
- displayStatus(state);
848
1276
  break;
1277
+ }
849
1278
  case "error":
850
- logActivity(state, {
851
- type: "error",
852
- error: message.message || "Unknown tunnel error"
853
- });
854
- telemetry.error(
855
- EventTypes.TUNNEL_ERROR,
856
- `Tunnel error: ${message.message}`,
857
- {
858
- code: message.code,
859
- message: message.message
860
- },
861
- state.sandboxId ?? void 0
862
- );
863
- displayStatus(state);
1279
+ clearTimeout(connectionTimeout);
1280
+ onError?.(message.message || "Unknown tunnel error");
864
1281
  if (message.code === "unauthorized") {
865
1282
  ws.close();
866
1283
  reject(new Error("Unauthorized"));
@@ -871,287 +1288,1229 @@ async function connect(token, sandboxId, port, state) {
871
1288
  break;
872
1289
  case "request":
873
1290
  if (message.id && message.payload) {
874
- telemetry.debug(
875
- EventTypes.OPENCODE_REQUEST_RECEIVED,
876
- `Request: ${message.payload.method} ${message.payload.path}`,
877
- {
878
- requestId: message.id,
879
- method: message.payload.method,
880
- path: message.payload.path
881
- },
882
- state.sandboxId ?? void 0
883
- );
884
- const response = await forwardToOpenCode(port, message.payload, message.id, state);
885
- ws.send(
886
- JSON.stringify({
887
- type: "response",
888
- id: message.id,
889
- payload: response
890
- })
891
- );
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);
1296
+ sendResponse(ws, message.id, response);
1297
+ }
1298
+ break;
1299
+ case "subscribe_events":
1300
+ if (message.id) {
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
+ });
1311
+ }
1312
+ break;
1313
+ case "unsubscribe_events":
1314
+ if (message.id) {
1315
+ const controller = activeEventSubscriptions.get(message.id);
1316
+ if (controller) {
1317
+ controller.abort();
1318
+ activeEventSubscriptions.delete(message.id);
1319
+ }
892
1320
  }
893
1321
  break;
894
1322
  }
895
1323
  } catch (error2) {
896
1324
  const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
897
- logActivity(state, {
898
- type: "error",
899
- error: `Failed to handle message: ${errorMessage}`
900
- });
901
- telemetry.error(
902
- EventTypes.TUNNEL_ERROR,
903
- `Failed to handle message: ${errorMessage}`,
904
- {
905
- error: errorMessage
906
- },
907
- state.sandboxId ?? void 0
908
- );
909
- displayStatus(state);
1325
+ onError?.(`Failed to handle message: ${errorMessage}`);
910
1326
  }
911
1327
  });
1328
+ ws.on("error", (error2) => {
1329
+ clearTimeout(connectionTimeout);
1330
+ onError?.(`Connection error: ${error2.message}`);
1331
+ reject(error2);
1332
+ });
912
1333
  ws.on("close", (code, reason) => {
913
- state.connected = false;
914
1334
  const reasonStr = reason.toString() || "No reason provided";
915
- logActivity(state, {
916
- type: "info",
917
- message: `Disconnected (code: ${code}, reason: ${reasonStr})`
918
- });
919
- telemetry.info(
920
- EventTypes.TUNNEL_DISCONNECTED,
921
- "Tunnel disconnected",
922
- {
923
- sandboxId: state.sandboxId,
924
- code,
925
- reason: reasonStr
926
- },
927
- state.sandboxId ?? void 0
928
- );
929
- displayStatus(state);
930
- resolve();
1335
+ onDisconnected?.(code, reasonStr);
1336
+ for (const [, controller] of activeEventSubscriptions) {
1337
+ controller.abort();
1338
+ }
1339
+ activeEventSubscriptions.clear();
931
1340
  });
932
- ws.on("error", (error2) => {
933
- state.connected = false;
934
- logActivity(state, {
935
- type: "error",
936
- error: `Connection error: ${error2.message}`
937
- });
938
- telemetry.error(
939
- EventTypes.TUNNEL_ERROR,
940
- `Connection error: ${error2.message}`,
941
- {
942
- error: error2.message
943
- },
944
- state.sandboxId ?? void 0
945
- );
946
- 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 }
947
1354
  });
948
- const cleanup = async () => {
949
- process.removeAllListeners("SIGINT");
950
- process.removeAllListeners("SIGTERM");
951
- logActivity(state, {
952
- type: "info",
953
- message: "Shutting down..."
954
- });
955
- displayStatus(state);
956
- telemetry.info(
957
- EventTypes.TUNNEL_DISCONNECTED,
958
- "Tunnel stopped by user",
959
- {
960
- sandboxId: state.sandboxId
961
- },
962
- state.sandboxId ?? void 0
963
- );
964
- await shutdownTelemetry();
965
- ws.close();
966
- 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."
967
1364
  };
968
- process.removeAllListeners("SIGINT");
969
- process.removeAllListeners("SIGTERM");
970
- process.once("SIGINT", () => void cleanup());
971
- process.once("SIGTERM", () => void cleanup());
1365
+ } catch (error2) {
1366
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
1367
+ return { error: `Failed to resolve agent from key: ${message}` };
1368
+ }
1369
+ }
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}` };
1396
+ }
1397
+ }
1398
+ var AuthenticationError = class extends Error {
1399
+ constructor(message) {
1400
+ super(message);
1401
+ this.name = "AuthenticationError";
1402
+ }
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 }
972
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;
973
1426
  }
974
- async function tunnel(options) {
975
- const verbose = options.verbose ?? false;
976
- const credentials2 = await getToken();
977
- if (!credentials2) {
978
- telemetry.error(EventTypes.CLI_ERROR, "Not logged in", { command: "tunnel" });
979
- printError("Not logged in. Run `evident login` first.");
980
- process.exit(1);
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}`);
981
1436
  }
982
- const port = options.port ?? 4096;
983
- const sandboxId = options.sandbox;
984
- if (!sandboxId) {
985
- printError("--sandbox <id> is required");
986
- blank();
987
- console.log(chalk4.dim("To find your sandbox ID:"));
988
- console.log(chalk4.dim(" 1. Create a remote sandbox in the Evident web UI"));
989
- console.log(chalk4.dim(" 2. Copy the sandbox ID from the URL or settings"));
990
- console.log(chalk4.dim(" 3. Run: evident tunnel --sandbox <id>"));
991
- blank();
992
- telemetry.error(EventTypes.CLI_ERROR, "Missing sandbox ID", { command: "tunnel" });
993
- process.exit(1);
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}`,
1443
+ {
1444
+ method: "PATCH",
1445
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
1446
+ body: JSON.stringify({ status: "processing" })
1447
+ }
1448
+ );
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}`);
994
1465
  }
995
- const state = {
996
- connected: false,
997
- sandboxId,
998
- sandboxName: null,
999
- reconnectAttempt: 0,
1000
- lastActivity: /* @__PURE__ */ new Date(),
1001
- activityLog: [],
1002
- pendingRequests: /* @__PURE__ */ new Map(),
1003
- verbose
1004
- };
1005
- telemetry.info(
1006
- EventTypes.CLI_COMMAND,
1007
- "Starting tunnel command",
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}`,
1008
1475
  {
1009
- command: "tunnel",
1010
- port,
1011
- sandboxId,
1012
- verbose
1013
- },
1014
- sandboxId
1476
+ method: "PATCH",
1477
+ headers: { Authorization: authHeader, "Content-Type": "application/json" },
1478
+ body: JSON.stringify(body)
1479
+ }
1015
1480
  );
1016
- logActivity(state, {
1017
- type: "info",
1018
- message: `Starting tunnel (port: ${port}, verbose: ${verbose})`
1019
- });
1020
- logActivity(state, {
1021
- type: "info",
1022
- message: "Validating sandbox..."
1023
- });
1024
- const validateSpinner = ora2("Validating sandbox...").start();
1025
- const validation = await validateSandbox(credentials2.token, sandboxId);
1026
- if (!validation.valid) {
1027
- validateSpinner.fail(`Sandbox validation failed: ${validation.error}`);
1028
- logActivity(state, {
1029
- type: "error",
1030
- error: `Sandbox validation failed: ${validation.error}`
1031
- });
1032
- telemetry.error(EventTypes.CLI_ERROR, `Sandbox validation failed: ${validation.error}`, {
1033
- command: "tunnel",
1034
- sandboxId
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 })
1035
1502
  });
1036
- displayStatus(state);
1037
- 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) };
1038
1514
  }
1039
- state.sandboxName = validation.name ?? null;
1040
- validateSpinner.succeed(`Sandbox: ${validation.name || sandboxId}`);
1041
- logActivity(state, {
1042
- type: "info",
1043
- 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 })
1044
1551
  });
1045
- logActivity(state, {
1046
- type: "info",
1047
- 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 })
1048
1566
  });
1049
- const opencodeSpinner = ora2("Checking OpenCode connection...").start();
1050
- try {
1051
- telemetry.debug(
1052
- EventTypes.OPENCODE_HEALTH_CHECK,
1053
- `Checking OpenCode on port ${port}`,
1054
- { port },
1055
- sandboxId
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
+ })
1056
1577
  );
1057
- const response = await fetch(`http://localhost:${port}/health`);
1058
- if (!response.ok) {
1059
- throw new Error(`Health check returned ${response.status}`);
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);
1060
1598
  }
1061
- const healthData = await response.json().catch(() => ({}));
1062
- const version = healthData.version ? ` (v${healthData.version})` : "";
1063
- telemetry.info(
1064
- EventTypes.OPENCODE_HEALTH_OK,
1065
- `OpenCode healthy on port ${port}`,
1066
- {
1067
- port,
1068
- healthData
1069
- },
1070
- sandboxId
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")}`);
1662
+ } else {
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}`)}`
1071
1675
  );
1072
- opencodeSpinner.succeed(`OpenCode running on port ${port}${version}`);
1073
- logActivity(state, {
1074
- type: "info",
1075
- message: `OpenCode running on port ${port}${version}`
1076
- });
1077
- } catch (error2) {
1078
- const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
1079
- telemetry.warn(
1080
- EventTypes.OPENCODE_HEALTH_FAILED,
1081
- `Could not connect to OpenCode: ${errorMessage}`,
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: [
1082
1727
  {
1083
- port,
1084
- error: errorMessage
1728
+ name: "Yes, log me in",
1729
+ value: "login",
1730
+ description: "Opens a browser to authenticate with Evident"
1085
1731
  },
1086
- sandboxId
1087
- );
1088
- opencodeSpinner.warn(`Could not connect to OpenCode on port ${port}`);
1089
- logActivity(state, {
1090
- type: "error",
1091
- error: `OpenCode not reachable on port ${port}: ${errorMessage}`
1732
+ {
1733
+ name: "No, exit",
1734
+ value: "exit",
1735
+ description: "Exit without logging in"
1736
+ }
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
+ }
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
+ ]
1092
1842
  });
1093
- printWarning("Make sure OpenCode is running before starting the tunnel:");
1094
- console.log(chalk4.dim(` opencode serve --port ${port}`));
1843
+ if (action === "manual") {
1844
+ blank();
1845
+ console.log(chalk5.bold("Run this command in another terminal:"));
1846
+ blank();
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) {
1095
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 };
1096
1891
  }
1097
- while (true) {
1098
- try {
1099
- await connect(credentials2.token, sandboxId, port, state);
1100
- state.reconnectAttempt++;
1101
- const delay = getReconnectDelay(state.reconnectAttempt);
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) {
1102
1918
  logActivity(state, {
1103
1919
  type: "info",
1104
- message: `Reconnecting in ${Math.round(delay / 1e3)}s (attempt ${state.reconnectAttempt})...`
1920
+ message: "Waiting for tunnel reconnection..."
1105
1921
  });
1106
- telemetry.info(
1107
- EventTypes.TUNNEL_RECONNECTING,
1108
- `Reconnecting (attempt ${state.reconnectAttempt})`,
1109
- {
1110
- attempt: state.reconnectAttempt,
1111
- delayMs: delay
1112
- },
1113
- state.sandboxId ?? void 0
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
1114
1930
  );
1115
- displayStatus(state);
1116
- await sleep(delay);
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
1942
+ );
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
+ });
1956
+ }
1957
+ logActivity(state, {
1958
+ type: "info",
1959
+ message: `Processing conversation ${conv.id.slice(0, 8)}... (${conv.pending_message_count} pending)`
1960
+ });
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
+ });
1973
+ }
1974
+ state.sessions.set(conv.id, sessionId);
1975
+ }
1976
+ const messages = await getPendingMessages(state.agentId, conv.id, currentAuthHeader);
1977
+ for (const message of messages) {
1978
+ if (!state.running) break;
1979
+ logActivity(state, {
1980
+ type: "info",
1981
+ message: `Processing message ${message.id.slice(0, 8)}...`
1982
+ });
1983
+ if (state.interactive) displayStatus(state);
1984
+ const claimed = await markMessageProcessing(
1985
+ state.agentId,
1986
+ conv.id,
1987
+ message.id,
1988
+ currentAuthHeader
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();
2102
+ logActivity(state, {
2103
+ type: "info",
2104
+ message: `Queue empty, waiting (timeout: ${state.idleTimeout}s)...`
2105
+ });
2106
+ if (state.interactive) displayStatus(state);
2107
+ }
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));
1117
2119
  } catch (error2) {
1118
- const message = error2 instanceof Error ? error2.message : "Unknown error";
1119
- if (message === "Unauthorized") {
1120
- telemetry.error(
1121
- EventTypes.CLI_ERROR,
1122
- "Authentication failed",
1123
- {
1124
- command: "tunnel",
1125
- error: message
1126
- },
1127
- state.sandboxId ?? void 0
1128
- );
1129
- await shutdownTelemetry();
1130
- displayError(state, "Authentication failed", "Please run `evident login` again.");
1131
- process.exit(1);
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;
2125
+ logActivity(state, {
2126
+ type: "info",
2127
+ message: "Continuing with new credentials..."
2128
+ });
2129
+ if (state.interactive) displayStatus(state);
2130
+ continue;
2131
+ } else {
2132
+ state.running = false;
2133
+ break;
2134
+ }
1132
2135
  }
2136
+ const errorMessage = error2 instanceof Error ? error2.message : String(error2);
1133
2137
  logActivity(state, {
1134
2138
  type: "error",
1135
- error: message
2139
+ error: `Queue processing error: ${errorMessage}`
1136
2140
  });
1137
- telemetry.error(
1138
- EventTypes.TUNNEL_ERROR,
1139
- `Tunnel error: ${message}`,
1140
- {
1141
- error: message,
1142
- attempt: state.reconnectAttempt
1143
- },
1144
- state.sandboxId ?? void 0
1145
- );
1146
- state.reconnectAttempt++;
1147
- const delay = getReconnectDelay(state.reconnectAttempt);
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;
2152
+ }
2153
+ }
2154
+ await new Promise((resolve) => setTimeout(resolve, MESSAGE_POLL_INTERVAL_MS));
2155
+ }
2156
+ }
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) {
1148
2169
  logActivity(state, {
1149
2170
  type: "info",
1150
- message: `Reconnecting in ${Math.round(delay / 1e3)}s (attempt ${state.reconnectAttempt})...`
2171
+ message: `Released ${state.lockedConversations.size} lock(s)`
1151
2172
  });
1152
2173
  displayStatus(state);
1153
- await sleep(delay);
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");
1154
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();
2255
+ process.exit(1);
2256
+ }
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..."
2263
+ );
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) {
2482
+ displayStatus(state);
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);
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);
1155
2514
  }
1156
2515
  }
1157
2516
 
@@ -1166,12 +2525,17 @@ program.name("evident").description("Run OpenCode locally and connect it to Evid
1166
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);
1167
2526
  program.command("logout").description("Remove stored credentials").action(logout);
1168
2527
  program.command("whoami").description("Show the currently logged in user").action(whoami);
1169
- 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) => {
1170
- tunnel({
1171
- sandbox: options.sandbox,
1172
- port: parseInt(options.port, 10),
1173
- verbose: options.verbose
1174
- });
1175
- });
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
+ );
1176
2540
  program.parse();
1177
2541
  //# sourceMappingURL=index.js.map