@evident-ai/cli 0.2.0 → 0.2.1-dev.0c08ffb

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,79 @@ 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 PRODUCTION_API_URL = "https://api.production.evident.run/v1";
16
+ var PRODUCTION_TUNNEL_URL = "wss://tunnel.production.evident.run";
17
+ var defaults = {
18
+ apiUrl: PRODUCTION_API_URL,
19
+ tunnelUrl: PRODUCTION_TUNNEL_URL
20
+ };
21
+ var endpointOverride;
22
+ var tunnelOverride;
23
+ function setEndpoint(url) {
24
+ if (!url) {
25
+ endpointOverride = void 0;
26
+ return;
27
+ }
28
+ const trimmed = url.replace(/\/+$/, "");
29
+ endpointOverride = /\/v1$/.test(trimmed) ? trimmed : `${trimmed}/v1`;
30
+ }
31
+ function setTunnelUrl(url) {
32
+ tunnelOverride = url ? url.replace(/\/+$/, "") : void 0;
33
+ }
34
+ function getApiUrl() {
35
+ return process.env.EVIDENT_API_URL ?? endpointOverride ?? defaults.apiUrl;
36
+ }
37
+ function getTunnelUrl() {
38
+ return process.env.EVIDENT_TUNNEL_URL ?? tunnelOverride ?? defaults.tunnelUrl;
39
+ }
40
+ var config = new Conf({
41
+ projectName: "evident",
42
+ projectSuffix: "",
43
+ defaults
44
+ });
45
+ var credentials = new Conf({
46
+ projectName: "evident",
47
+ projectSuffix: "",
48
+ configName: "credentials",
49
+ defaults: {}
50
+ });
51
+ function getApiUrlConfig() {
52
+ return getApiUrl();
53
+ }
54
+ function getTunnelUrlConfig() {
55
+ return getTunnelUrl();
56
+ }
57
+ function getCredentials() {
58
+ return {
59
+ token: credentials.get("token"),
60
+ user: credentials.get("user"),
61
+ expiresAt: credentials.get("expiresAt")
62
+ };
63
+ }
64
+ function setCredentials(creds) {
65
+ if (creds.token) credentials.set("token", creds.token);
66
+ if (creds.user) credentials.set("user", creds.user);
67
+ if (creds.expiresAt) credentials.set("expiresAt", creds.expiresAt);
68
+ }
69
+ function clearCredentials() {
70
+ credentials.clear();
71
+ }
72
+ function getCliName() {
73
+ const argv1 = process.argv[1] || "";
74
+ const isNpx = process.env.npm_execpath?.includes("npx") || process.env.npm_command === "exec" || argv1.includes("_npx") || argv1.includes(".npm/_cacache");
75
+ if (isNpx) {
76
+ return "npx @evident-ai/cli@latest";
77
+ }
78
+ if (argv1.includes("tsx") || argv1.includes("ts-node")) {
79
+ return "pnpm --filter @evident-ai/cli dev:run";
80
+ }
81
+ return "evident";
82
+ }
83
+
19
84
  // src/lib/api.ts
20
85
  var ApiClient = class {
21
86
  baseUrl;
@@ -123,15 +188,15 @@ async function getKeytar() {
123
188
  return null;
124
189
  }
125
190
  }
126
- async function storeToken(credentials) {
191
+ async function storeToken(credentials2) {
127
192
  const keytar = await getKeytar();
128
193
  if (keytar) {
129
- await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(credentials));
194
+ await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(credentials2));
130
195
  } else {
131
196
  setCredentials({
132
- token: credentials.token,
133
- user: credentials.user,
134
- expiresAt: credentials.expiresAt
197
+ token: credentials2.token,
198
+ user: credentials2.user,
199
+ expiresAt: credentials2.expiresAt
135
200
  });
136
201
  }
137
202
  }
@@ -332,8 +397,8 @@ async function login(options) {
332
397
 
333
398
  // src/commands/logout.ts
334
399
  async function logout() {
335
- const credentials = await getToken();
336
- if (!credentials) {
400
+ const credentials2 = await getToken();
401
+ if (!credentials2) {
337
402
  printWarning("You are not logged in.");
338
403
  return;
339
404
  }
@@ -344,16 +409,16 @@ async function logout() {
344
409
  // src/commands/whoami.ts
345
410
  import chalk3 from "chalk";
346
411
  async function whoami() {
347
- const credentials = await getToken();
348
- if (!credentials) {
412
+ const credentials2 = await getToken();
413
+ if (!credentials2) {
349
414
  printError("Not logged in. Run the `login` command to authenticate.");
350
415
  process.exit(1);
351
416
  }
352
417
  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);
418
+ console.log(keyValue("User", chalk3.bold(credentials2.user.email)));
419
+ console.log(keyValue("User ID", credentials2.user.id));
420
+ if (credentials2.expiresAt) {
421
+ const expiresAt = new Date(credentials2.expiresAt);
357
422
  const now = /* @__PURE__ */ new Date();
358
423
  if (expiresAt < now) {
359
424
  console.log(keyValue("Status", chalk3.red("Token expired")));
@@ -367,12 +432,23 @@ async function whoami() {
367
432
  blank();
368
433
  }
369
434
 
370
- // src/commands/tunnel.ts
371
- import WebSocket from "ws";
372
- import chalk4 from "chalk";
373
- import ora2 from "ora";
374
- import { execSync, spawn } from "child_process";
375
- import { select } from "@inquirer/prompts";
435
+ // src/commands/run.ts
436
+ import chalk6 from "chalk";
437
+ import ora3 from "ora";
438
+ import { select as select3 } from "@inquirer/prompts";
439
+
440
+ // ../../packages/types/src/telemetry/index.ts
441
+ var TelemetryEventTypes = {
442
+ // Agent activity events (shown in web UI activity log)
443
+ AGENT_CONNECTED: "agent.connected",
444
+ AGENT_DISCONNECTED: "agent.disconnected",
445
+ AGENT_MESSAGE_PROCESSING: "agent.message_processing",
446
+ AGENT_MESSAGE_DONE: "agent.message_done",
447
+ AGENT_MESSAGE_FAILED: "agent.message_failed"
448
+ };
449
+
450
+ // ../../packages/types/src/tunnel/index.ts
451
+ var MAX_FRAME_BYTES = 256 * 1024;
376
452
 
377
453
  // src/lib/telemetry.ts
378
454
  var CLI_VERSION = process.env.npm_package_version || "unknown";
@@ -388,7 +464,7 @@ function logEvent(eventType, options = {}) {
388
464
  severity: options.severity || "info",
389
465
  message: options.message,
390
466
  metadata: options.metadata,
391
- sandbox_id: options.sandboxId,
467
+ agent_id: options.agentId,
392
468
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
393
469
  };
394
470
  eventBuffer.push(event);
@@ -402,10 +478,10 @@ function logEvent(eventType, options = {}) {
402
478
  }
403
479
  }
404
480
  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 })
481
+ debug: (eventType, message, metadata, agentId) => logEvent(eventType, { severity: "debug", message, metadata, agentId }),
482
+ info: (eventType, message, metadata, agentId) => logEvent(eventType, { severity: "info", message, metadata, agentId }),
483
+ warn: (eventType, message, metadata, agentId) => logEvent(eventType, { severity: "warning", message, metadata, agentId }),
484
+ error: (eventType, message, metadata, agentId) => logEvent(eventType, { severity: "error", message, metadata, agentId })
409
485
  };
410
486
  async function flushEvents() {
411
487
  if (eventBuffer.length === 0) return;
@@ -416,25 +492,26 @@ async function flushEvents() {
416
492
  flushTimeout = null;
417
493
  }
418
494
  try {
419
- const credentials = await getToken();
420
- if (!credentials) {
495
+ const credentials2 = await getToken();
496
+ if (!credentials2) {
421
497
  return;
422
498
  }
423
499
  const apiUrl = getApiUrlConfig();
424
500
  const controller = new AbortController();
425
501
  const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
426
502
  try {
503
+ const request = {
504
+ events,
505
+ client_type: "cli",
506
+ client_version: CLI_VERSION
507
+ };
427
508
  const response = await fetch(`${apiUrl}/telemetry/events`, {
428
509
  method: "POST",
429
510
  headers: {
430
511
  "Content-Type": "application/json",
431
- Authorization: `Bearer ${credentials.token}`
512
+ Authorization: `Bearer ${credentials2.token}`
432
513
  },
433
- body: JSON.stringify({
434
- events,
435
- client_type: "cli",
436
- client_version: CLI_VERSION
437
- }),
514
+ body: JSON.stringify(request),
438
515
  signal: controller.signal
439
516
  });
440
517
  if (!response.ok) {
@@ -457,6 +534,32 @@ async function shutdownTelemetry() {
457
534
  }
458
535
  await flushEvents();
459
536
  }
537
+ function emitEvent(event) {
538
+ logEvent(event.event_type, {
539
+ severity: event.severity,
540
+ message: event.message,
541
+ metadata: event.metadata,
542
+ agentId: event.agent_id
543
+ });
544
+ }
545
+ function emitAgentConnected(agentId, metadata) {
546
+ emitEvent({
547
+ event_type: TelemetryEventTypes.AGENT_CONNECTED,
548
+ severity: "info",
549
+ message: "Agent CLI connected",
550
+ metadata,
551
+ agent_id: agentId
552
+ });
553
+ }
554
+ function emitAgentDisconnected(agentId, metadata) {
555
+ emitEvent({
556
+ event_type: TelemetryEventTypes.AGENT_DISCONNECTED,
557
+ severity: "info",
558
+ message: `Agent CLI disconnected (code: ${metadata.code})`,
559
+ metadata,
560
+ agent_id: agentId
561
+ });
562
+ }
460
563
  var EventTypes = {
461
564
  // Tunnel lifecycle
462
565
  TUNNEL_STARTING: "tunnel.starting",
@@ -484,12 +587,71 @@ var EventTypes = {
484
587
  CLI_ERROR: "cli.error"
485
588
  };
486
589
 
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;
590
+ // src/lib/auth.ts
591
+ async function getAuthCredentials() {
592
+ const agentKey = process.env.EVIDENT_AGENT_KEY;
593
+ if (agentKey) {
594
+ return { token: agentKey, authType: "agent_key" };
595
+ }
596
+ const userToken = process.env.EVIDENT_TOKEN;
597
+ if (userToken) {
598
+ return { token: userToken, authType: "bearer" };
599
+ }
600
+ const keychainCreds = await getToken();
601
+ if (keychainCreds) {
602
+ return {
603
+ token: keychainCreds.token,
604
+ authType: "bearer",
605
+ user: keychainCreds.user
606
+ };
607
+ }
608
+ return null;
609
+ }
610
+ function getAuthHeader(credentials2) {
611
+ if (credentials2.authType === "agent_key") {
612
+ return `SandboxKey ${credentials2.token}`;
613
+ }
614
+ return `Bearer ${credentials2.token}`;
615
+ }
616
+ function isInteractive(jsonOutput) {
617
+ if (jsonOutput) return false;
618
+ if (process.env.CI) return false;
619
+ if (process.env.GITHUB_ACTIONS) return false;
620
+ if (!process.stdin.isTTY) return false;
621
+ return true;
622
+ }
623
+
624
+ // src/lib/opencode/health.ts
625
+ async function checkOpenCodeHealth(port) {
626
+ try {
627
+ const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
628
+ signal: AbortSignal.timeout(2e3)
629
+ // 2 second timeout
630
+ });
631
+ if (!response.ok) {
632
+ return { healthy: false, error: `HTTP ${response.status}` };
633
+ }
634
+ const data = await response.json().catch(() => ({}));
635
+ return { healthy: true, version: data.version };
636
+ } catch (error2) {
637
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
638
+ return { healthy: false, error: message };
639
+ }
640
+ }
641
+ async function waitForOpenCodeHealth(port, timeoutMs = 3e4) {
642
+ const startTime = Date.now();
643
+ while (Date.now() - startTime < timeoutMs) {
644
+ const health = await checkOpenCodeHealth(port);
645
+ if (health.healthy) {
646
+ return health;
647
+ }
648
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
649
+ }
650
+ return { healthy: false, error: "Timeout waiting for OpenCode to be healthy" };
651
+ }
652
+
653
+ // src/lib/opencode/process.ts
654
+ import { execSync, spawn } from "child_process";
493
655
  var OPENCODE_PORT_RANGE = [4096, 4097, 4098, 4099, 4100];
494
656
  function getProcessCwd(pid) {
495
657
  const platform = process.platform;
@@ -624,22 +786,6 @@ async function scanPortsForOpenCode() {
624
786
  }
625
787
  return instances;
626
788
  }
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
789
  async function findHealthyOpenCodeInstances() {
644
790
  const processes = findOpenCodeProcesses();
645
791
  const healthy = [];
@@ -655,948 +801,1557 @@ async function findHealthyOpenCodeInstances() {
655
801
  }
656
802
  return healthy;
657
803
  }
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();
804
+ async function startOpenCode(port) {
805
+ let command = "opencode";
806
+ let args = ["serve", "--port", port.toString(), "--hostname", "127.0.0.1"];
807
+ try {
808
+ execSync("which opencode", { stdio: "ignore" });
809
+ } catch {
810
+ command = "npx";
811
+ args = ["opencode", "serve", "--port", port.toString(), "--hostname", "127.0.0.1"];
666
812
  }
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"
813
+ const child = spawn(command, args, {
814
+ detached: true,
815
+ stdio: "ignore",
816
+ cwd: process.cwd()
675
817
  });
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
- }
697
- }
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}`);
725
- }
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
- );
735
- } 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));
750
- }
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("");
762
- }
763
- if (!state.displayInitialized) {
764
- console.log("");
765
- console.log(chalk4.dim("\u2550".repeat(60)));
766
- console.log("");
767
- for (const line of lines) {
768
- console.log(line);
769
- }
770
- state.displayInitialized = true;
771
- } else {
772
- process.stdout.write(ANSI.moveUp(STATUS_DISPLAY_HEIGHT + 3));
773
- console.log(chalk4.dim("\u2550".repeat(60)));
774
- console.log("");
775
- for (const line of lines) {
776
- process.stdout.write("\x1B[2K");
777
- console.log(line);
778
- }
779
- }
818
+ return child;
780
819
  }
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}`));
820
+ function stopOpenCode(opencodeProcess) {
821
+ if (!opencodeProcess || !opencodeProcess.pid) {
822
+ return;
787
823
  }
788
- blank();
789
- }
790
- async function validateSandbox(token, sandboxId) {
791
- const apiUrl = getApiUrlConfig();
792
824
  try {
793
- const response = await fetch(`${apiUrl}/sandboxes/${sandboxId}`, {
794
- headers: {
795
- Authorization: `Bearer ${token}`
796
- }
797
- });
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
- };
825
+ if (process.platform === "win32") {
826
+ opencodeProcess.kill("SIGTERM");
827
+ } else {
828
+ process.kill(-opencodeProcess.pid, "SIGTERM");
813
829
  }
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}` };
830
+ } catch {
818
831
  }
819
832
  }
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
833
- });
834
- displayStatus(state);
835
- if (state.verbose && request.body) {
836
- console.log(chalk4.dim(` Request body: ${JSON.stringify(request.body, null, 2)}`));
837
- }
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
- );
833
+
834
+ // src/lib/opencode/install.ts
835
+ import { execSync as execSync2 } from "child_process";
836
+ import chalk4 from "chalk";
837
+ import { select } from "@inquirer/prompts";
838
+ var OPENCODE_INSTALL_URL = "https://opencode.ai";
839
+ function isOpenCodeInstalled() {
849
840
  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;
871
- }
872
- const durationMs = Date.now() - startTime;
873
- state.pendingRequests.delete(requestId);
874
- if (!state.opencodeConnected) {
875
- state.opencodeConnected = true;
876
- }
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;
841
+ const platform = process.platform;
842
+ if (platform === "win32") {
843
+ execSync2("where opencode", { stdio: "ignore" });
882
844
  } else {
883
- logActivity(state, {
884
- type: "response",
885
- method: request.method,
886
- path: request.path,
887
- status: response.status,
888
- durationMs,
889
- requestId
890
- });
845
+ execSync2("which opencode", { stdio: "ignore" });
891
846
  }
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}`,
847
+ return true;
848
+ } catch {
849
+ return false;
850
+ }
851
+ }
852
+ async function promptOpenCodeInstall(interactive) {
853
+ if (!interactive) {
854
+ console.log(
855
+ JSON.stringify({
856
+ status: "error",
857
+ error: "OpenCode is not installed",
858
+ install_url: OPENCODE_INSTALL_URL,
859
+ install_commands: {
860
+ npm: "npm install -g opencode-ai",
861
+ curl: "curl -fsSL https://opencode.ai/install.sh | sh"
862
+ }
863
+ })
864
+ );
865
+ return "exit";
866
+ }
867
+ blank();
868
+ console.log(chalk4.yellow("OpenCode is not installed on your system."));
869
+ blank();
870
+ console.log(chalk4.dim("OpenCode is an AI coding agent that runs locally on your machine."));
871
+ console.log(chalk4.dim(`Learn more at: ${chalk4.cyan(OPENCODE_INSTALL_URL)}`));
872
+ blank();
873
+ const action = await select({
874
+ message: "How would you like to proceed?",
875
+ choices: [
901
876
  {
902
- status: response.status,
903
- path: request.path,
904
- durationMs,
905
- requestId
877
+ name: "Show installation instructions",
878
+ value: "instructions",
879
+ description: "Display commands to install OpenCode"
906
880
  },
907
- state.sandboxId ?? void 0
908
- );
909
- return {
910
- status: response.status,
911
- body
912
- };
913
- } catch (error2) {
914
- 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
881
  {
931
- port,
932
- path: request.path,
933
- error: message,
934
- requestId
882
+ name: "Continue without OpenCode",
883
+ value: "continue",
884
+ description: "Connect anyway (requests will fail until OpenCode is installed)"
935
885
  },
936
- state.sandboxId ?? void 0
937
- );
938
- return {
939
- status: 502,
940
- body: { error: "Failed to connect to OpenCode", message }
941
- };
886
+ {
887
+ name: "Exit",
888
+ value: "exit",
889
+ description: "Exit and install OpenCode manually"
890
+ }
891
+ ]
892
+ });
893
+ if (action === "instructions") {
894
+ blank();
895
+ console.log(chalk4.bold("Install OpenCode using one of these methods:"));
896
+ blank();
897
+ console.log(chalk4.dim(" # Option 1: Install via npm (recommended)"));
898
+ console.log(` ${chalk4.cyan("npm install -g opencode-ai")}`);
899
+ blank();
900
+ console.log(chalk4.dim(" # Option 2: Install via curl"));
901
+ console.log(` ${chalk4.cyan("curl -fsSL https://opencode.ai/install.sh | sh")}`);
902
+ blank();
903
+ console.log(chalk4.dim(`For more options, visit: ${chalk4.cyan(OPENCODE_INSTALL_URL)}`));
904
+ blank();
905
+ const afterInstall = await select({
906
+ message: "After installing, what would you like to do?",
907
+ choices: [
908
+ {
909
+ name: "I installed it - continue",
910
+ value: "continue",
911
+ description: "Proceed with the run command"
912
+ },
913
+ {
914
+ name: "Exit",
915
+ value: "exit",
916
+ description: "Exit now and run the command again later"
917
+ }
918
+ ]
919
+ });
920
+ if (afterInstall === "continue") {
921
+ if (isOpenCodeInstalled()) {
922
+ console.log(chalk4.green("\n\u2713 OpenCode detected!"));
923
+ return "installed";
924
+ } else {
925
+ console.log(chalk4.yellow("\nOpenCode still not detected in PATH."));
926
+ console.log(chalk4.dim("You may need to restart your terminal or add it to your PATH."));
927
+ const proceed = await select({
928
+ message: "Continue anyway?",
929
+ choices: [
930
+ { name: "Yes, continue", value: "continue" },
931
+ { name: "No, exit", value: "exit" }
932
+ ]
933
+ });
934
+ return proceed === "continue" ? "continue" : "exit";
935
+ }
936
+ }
937
+ return "exit";
942
938
  }
939
+ return action;
943
940
  }
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)}`
941
+
942
+ // src/lib/opencode/session.ts
943
+ async function createOpenCodeSession(port) {
944
+ const response = await fetch(`http://localhost:${port}/session`, {
945
+ method: "POST",
946
+ headers: { "Content-Type": "application/json" },
947
+ body: JSON.stringify({})
949
948
  });
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;
949
+ if (!response.ok) {
950
+ throw new Error(`Failed to create session: HTTP ${response.status}`);
951
+ }
952
+ const data = await response.json();
953
+ return data.id;
954
+ }
955
+ async function sendMessageToOpenCode(port, sessionId, content, options, hooks, maxWaitMs = 10 * 60 * 1e3) {
956
+ const body = {
957
+ parts: [{ type: "text", text: content }]
958
+ };
959
+ if (options?.agent) {
960
+ body.agent = options.agent;
961
+ }
962
+ if (options?.model) {
963
+ const slashIndex = options.model.indexOf("/");
964
+ if (slashIndex !== -1) {
965
+ body.model = {
966
+ providerID: options.model.substring(0, slashIndex),
967
+ modelID: options.model.substring(slashIndex + 1)
968
+ };
969
+ }
970
+ }
971
+ let pollDone = false;
972
+ const reportedQuestions = /* @__PURE__ */ new Set();
973
+ const reportedPermissions = /* @__PURE__ */ new Set();
974
+ const pollInteractive = async () => {
975
+ while (!pollDone) {
976
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
977
+ if (pollDone) break;
978
+ if (hooks?.onQuestion) {
979
+ try {
980
+ const res = await fetch(`http://localhost:${port}/question`);
981
+ if (res.ok) {
982
+ const questions = await res.json();
983
+ for (const q of questions) {
984
+ if (q.sessionID === sessionId && !reportedQuestions.has(q.id)) {
985
+ reportedQuestions.add(q.id);
986
+ await hooks.onQuestion(q);
987
+ }
988
+ }
989
+ }
990
+ } catch {
991
+ }
972
992
  }
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 {
993
+ if (hooks?.onPermission) {
994
+ try {
995
+ const res = await fetch(`http://localhost:${port}/permission`);
996
+ if (res.ok) {
997
+ const permissions = await res.json();
998
+ for (const p of permissions) {
999
+ if (p.sessionID === sessionId && !reportedPermissions.has(p.id)) {
1000
+ reportedPermissions.add(p.id);
1001
+ await hooks.onPermission(p);
1002
+ }
1003
+ }
982
1004
  }
1005
+ } catch {
983
1006
  }
984
1007
  }
985
1008
  }
986
- } catch (error2) {
987
- if (abortController.signal.aborted) {
988
- return;
1009
+ };
1010
+ const sendMessage = async () => {
1011
+ const controller = new AbortController();
1012
+ const timer = setTimeout(() => controller.abort(), maxWaitMs);
1013
+ try {
1014
+ const res = await fetch(`http://localhost:${port}/session/${sessionId}/message`, {
1015
+ method: "POST",
1016
+ headers: { "Content-Type": "application/json" },
1017
+ body: JSON.stringify(body),
1018
+ signal: controller.signal
1019
+ });
1020
+ if (!res.ok) {
1021
+ const text = await res.text().catch(() => "");
1022
+ throw new Error(`OpenCode message failed: HTTP ${res.status}${text ? `: ${text}` : ""}`);
1023
+ }
1024
+ const sessionRes = await fetch(`http://localhost:${port}/session/${sessionId}`).catch(
1025
+ () => null
1026
+ );
1027
+ const session = sessionRes?.ok ? await sessionRes.json() : null;
1028
+ return { title: session?.title };
1029
+ } catch (err) {
1030
+ if (err instanceof Error && err.name === "AbortError") {
1031
+ throw new Error("Message processing timed out");
1032
+ }
1033
+ throw err;
1034
+ } finally {
1035
+ clearTimeout(timer);
1036
+ pollDone = true;
989
1037
  }
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
- }
1038
+ };
1039
+ const [result] = await Promise.all([sendMessage(), pollInteractive()]);
1040
+ return result;
1000
1041
  }
1001
- function cancelEventSubscription(subscriptionId, state) {
1002
- const controller = state.activeEventSubscriptions.get(subscriptionId);
1003
- if (controller) {
1004
- controller.abort();
1005
- state.activeEventSubscriptions.delete(subscriptionId);
1042
+
1043
+ // src/lib/tunnel/connection.ts
1044
+ import WebSocket2 from "ws";
1045
+
1046
+ // src/lib/tunnel/forwarding.ts
1047
+ import WebSocket from "ws";
1048
+ var LOOPBACK_HOST = "127.0.0.1";
1049
+ var STRIP_REQ = /* @__PURE__ */ new Set([
1050
+ "host",
1051
+ "connection",
1052
+ "keep-alive",
1053
+ "proxy-authorization",
1054
+ "transfer-encoding",
1055
+ "upgrade",
1056
+ "content-length"
1057
+ ]);
1058
+ var STRIP_RES = /* @__PURE__ */ new Set([
1059
+ "connection",
1060
+ "keep-alive",
1061
+ "transfer-encoding",
1062
+ "content-encoding",
1063
+ "content-length"
1064
+ ]);
1065
+ var StreamForwarder = class {
1066
+ constructor(ws, port, callbacks = {}) {
1067
+ this.ws = ws;
1068
+ this.port = port;
1069
+ this.callbacks = callbacks;
1006
1070
  }
1007
- }
1008
- function sendResponse(ws, requestId, response) {
1009
- const bodyStr = JSON.stringify(response.body ?? null);
1010
- const bodyBytes = Buffer.from(bodyStr, "utf-8");
1011
- if (bodyBytes.length < CHUNK_THRESHOLD) {
1012
- ws.send(
1013
- JSON.stringify({
1014
- type: "response",
1015
- id: requestId,
1016
- payload: response
1017
- })
1018
- );
1019
- return;
1071
+ inflight = /* @__PURE__ */ new Map();
1072
+ /**
1073
+ * Handle an edge→agent frame. Unknown frame types are ignored.
1074
+ */
1075
+ handleFrame(frame) {
1076
+ switch (frame.type) {
1077
+ case "open":
1078
+ this.callbacks.onOpen?.(frame.sid, frame.method, frame.path);
1079
+ void this.handleOpen(frame);
1080
+ break;
1081
+ case "req_data":
1082
+ this.inflight.get(frame.sid)?.pushBody?.(Buffer.from(frame.b64, "base64"));
1083
+ break;
1084
+ case "req_end":
1085
+ this.inflight.get(frame.sid)?.endBody?.();
1086
+ break;
1087
+ case "abort":
1088
+ this.inflight.get(frame.sid)?.abort?.();
1089
+ break;
1090
+ }
1020
1091
  }
1021
- sendResponseAsChunks(ws, requestId, response, bodyBytes);
1022
- }
1023
- function sendResponseAsChunks(ws, requestId, response, bodyBytes) {
1024
- const chunks = splitIntoChunks(bodyBytes, CHUNK_SIZE);
1025
- ws.send(
1026
- JSON.stringify({
1027
- type: "response_start",
1028
- id: requestId,
1029
- total_chunks: chunks.length,
1030
- total_size: bodyBytes.length,
1031
- payload: {
1032
- status: response.status,
1033
- headers: response.headers
1092
+ /**
1093
+ * Abort every in-flight stream (e.g. on WebSocket close).
1094
+ */
1095
+ abortAll() {
1096
+ for (const stream of this.inflight.values()) {
1097
+ try {
1098
+ stream.abort();
1099
+ } catch {
1034
1100
  }
1035
- })
1036
- );
1037
- for (let i = 0; i < chunks.length; i++) {
1038
- ws.send(
1039
- JSON.stringify({
1040
- type: "response_chunk",
1041
- id: requestId,
1042
- chunk_index: i,
1043
- data: chunks[i].toString("base64")
1044
- })
1045
- );
1101
+ }
1102
+ this.inflight.clear();
1046
1103
  }
1047
- ws.send(
1048
- JSON.stringify({
1049
- type: "response_end",
1050
- id: requestId
1051
- })
1052
- );
1053
- }
1054
- function splitIntoChunks(data, chunkSize) {
1055
- const chunks = [];
1056
- for (let i = 0; i < data.length; i += chunkSize) {
1057
- chunks.push(data.subarray(i, i + chunkSize));
1104
+ send(frame) {
1105
+ if (this.ws.readyState === WebSocket.OPEN) {
1106
+ this.ws.send(JSON.stringify(frame));
1107
+ }
1058
1108
  }
1059
- return chunks;
1060
- }
1109
+ async handleOpen(frame) {
1110
+ const { sid, method, path, headers, has_body } = frame;
1111
+ const ac = new AbortController();
1112
+ let bodyPromise;
1113
+ let pushBody;
1114
+ let endBody;
1115
+ if (has_body) {
1116
+ const chunks = [];
1117
+ bodyPromise = new Promise((resolve) => {
1118
+ pushBody = (buf) => {
1119
+ chunks.push(buf);
1120
+ };
1121
+ endBody = () => {
1122
+ resolve(Buffer.concat(chunks));
1123
+ };
1124
+ });
1125
+ }
1126
+ const fwdHeaders = {};
1127
+ for (const [k, v] of Object.entries(headers ?? {})) {
1128
+ if (!STRIP_REQ.has(k.toLowerCase())) fwdHeaders[k] = v;
1129
+ }
1130
+ this.inflight.set(sid, { pushBody, endBody, abort: () => ac.abort() });
1131
+ const body = bodyPromise ? await bodyPromise : void 0;
1132
+ if (ac.signal.aborted) {
1133
+ this.inflight.delete(sid);
1134
+ return;
1135
+ }
1136
+ let upstream;
1137
+ try {
1138
+ upstream = await fetch(`http://${LOOPBACK_HOST}:${this.port}${path}`, {
1139
+ method,
1140
+ headers: fwdHeaders,
1141
+ body,
1142
+ redirect: "manual",
1143
+ signal: ac.signal
1144
+ });
1145
+ } catch (err) {
1146
+ this.inflight.delete(sid);
1147
+ if (!ac.signal.aborted) {
1148
+ this.send({ type: "res_err", sid, message: `upstream fetch failed: ${String(err)}` });
1149
+ }
1150
+ return;
1151
+ }
1152
+ const resHeaders = {};
1153
+ upstream.headers.forEach((value, key) => {
1154
+ if (!STRIP_RES.has(key.toLowerCase())) resHeaders[key] = value;
1155
+ });
1156
+ this.send({ type: "head", sid, status: upstream.status, headers: resHeaders });
1157
+ this.callbacks.onHead?.(sid, upstream.status);
1158
+ try {
1159
+ if (upstream.body) {
1160
+ const reader = upstream.body.getReader();
1161
+ while (true) {
1162
+ const { done, value } = await reader.read();
1163
+ if (done) break;
1164
+ const chunk = Buffer.from(value);
1165
+ for (let i = 0; i < chunk.length; i += MAX_FRAME_BYTES) {
1166
+ const slice = chunk.subarray(i, i + MAX_FRAME_BYTES);
1167
+ this.send({ type: "res_data", sid, b64: slice.toString("base64") });
1168
+ }
1169
+ }
1170
+ }
1171
+ this.send({ type: "res_end", sid });
1172
+ } catch (err) {
1173
+ if (!ac.signal.aborted) {
1174
+ this.send({ type: "res_err", sid, message: String(err) });
1175
+ }
1176
+ } finally {
1177
+ this.inflight.delete(sid);
1178
+ }
1179
+ }
1180
+ };
1181
+
1182
+ // src/lib/tunnel/connection.ts
1183
+ var MAX_RECONNECT_DELAY = 3e4;
1184
+ var BASE_RECONNECT_DELAY = 500;
1061
1185
  function getReconnectDelay(attempt) {
1062
1186
  const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, attempt);
1063
1187
  const jitter = Math.random() * 1e3;
1064
1188
  return Math.min(exponentialDelay + jitter, MAX_RECONNECT_DELAY);
1065
1189
  }
1066
- async function connect(token, sandboxId, port, state) {
1190
+ function describeSocketError(error2, url) {
1191
+ const code = error2.code;
1192
+ switch (code) {
1193
+ case "ECONNREFUSED":
1194
+ return `connection refused at ${url} \u2014 is the tunnel relay running? (ECONNREFUSED)`;
1195
+ case "ENOTFOUND":
1196
+ return `host not found for ${url} \u2014 check the tunnel URL (ENOTFOUND)`;
1197
+ case "ETIMEDOUT":
1198
+ return `connection timed out to ${url} (ETIMEDOUT)`;
1199
+ case "ECONNRESET":
1200
+ return `connection reset by ${url} (ECONNRESET)`;
1201
+ default: {
1202
+ const base = error2.message?.trim();
1203
+ const suffix = code ? ` (${code})` : "";
1204
+ return `${base && base.length > 0 ? base : "socket error"}${suffix} connecting to ${url}`;
1205
+ }
1206
+ }
1207
+ }
1208
+ var STREAM_FRAME_TYPES = /* @__PURE__ */ new Set([
1209
+ "open",
1210
+ "req_data",
1211
+ "req_end",
1212
+ "abort"
1213
+ ]);
1214
+ function isStreamFrame(message) {
1215
+ return STREAM_FRAME_TYPES.has(message.type);
1216
+ }
1217
+ function connectTunnel(options) {
1218
+ const {
1219
+ agentId,
1220
+ authHeader,
1221
+ port,
1222
+ onConnected,
1223
+ onDisconnected,
1224
+ onError,
1225
+ onRequest,
1226
+ onResponse,
1227
+ onInfo
1228
+ } = options;
1067
1229
  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
- );
1230
+ const url = `${tunnelUrl}/tunnel/${agentId}/connect`;
1084
1231
  return new Promise((resolve, reject) => {
1085
- const ws = new WebSocket(url, {
1232
+ const ws = new WebSocket2(url, {
1086
1233
  headers: {
1087
- Authorization: `Bearer ${token}`
1234
+ Authorization: authHeader
1088
1235
  }
1089
1236
  });
1090
- ws.on("open", () => {
1091
- state.connected = true;
1092
- state.reconnectAttempt = 0;
1093
- logActivity(state, {
1094
- type: "info",
1095
- message: "WebSocket connection established"
1237
+ const streamStartTimes = /* @__PURE__ */ new Map();
1238
+ const forwarder = new StreamForwarder(ws, port, {
1239
+ onOpen: (sid, method, path) => {
1240
+ streamStartTimes.set(sid, Date.now());
1241
+ onRequest?.(method, path, sid);
1242
+ },
1243
+ onHead: (sid, status) => {
1244
+ const startedAt = streamStartTimes.get(sid);
1245
+ streamStartTimes.delete(sid);
1246
+ onResponse?.(status, startedAt ? Date.now() - startedAt : 0, sid);
1247
+ }
1248
+ });
1249
+ const connectionTimeout = setTimeout(() => {
1250
+ ws.close();
1251
+ reject(new Error("Connection timeout"));
1252
+ }, 3e4);
1253
+ let upgradeRejection = null;
1254
+ ws.on("unexpected-response", (_req, res) => {
1255
+ clearTimeout(connectionTimeout);
1256
+ const chunks = [];
1257
+ res.on("data", (chunk) => chunks.push(chunk));
1258
+ res.on("end", () => {
1259
+ const bodyRaw = Buffer.concat(chunks).toString("utf8").trim();
1260
+ let detail = bodyRaw;
1261
+ try {
1262
+ const parsed = JSON.parse(bodyRaw);
1263
+ detail = parsed.error ?? parsed.message ?? bodyRaw;
1264
+ if (parsed.details) detail += ` (${parsed.details})`;
1265
+ } catch {
1266
+ }
1267
+ const statusLine = `HTTP ${res.statusCode}${res.statusMessage ? ` ${res.statusMessage}` : ""}`;
1268
+ upgradeRejection = detail ? `${statusLine}: ${detail}` : statusLine;
1269
+ onError?.(`Tunnel refused by relay (${upgradeRejection})`);
1270
+ reject(new Error(`Tunnel handshake rejected: ${upgradeRejection}`));
1096
1271
  });
1097
- displayStatus(state);
1098
1272
  });
1099
- ws.on("message", async (data) => {
1273
+ ws.on("open", () => {
1274
+ onInfo?.("WebSocket connection established");
1275
+ });
1276
+ ws.on("message", (data) => {
1277
+ let message;
1100
1278
  try {
1101
- const message = JSON.parse(data.toString());
1102
- 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})`
1108
- });
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
- break;
1119
- 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);
1134
- if (message.code === "unauthorized") {
1135
- ws.close();
1136
- reject(new Error("Unauthorized"));
1137
- }
1138
- break;
1139
- case "ping":
1140
- ws.send(JSON.stringify({ type: "pong" }));
1141
- break;
1142
- case "request":
1143
- 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);
1155
- sendResponse(ws, message.id, response);
1156
- }
1157
- break;
1158
- case "subscribe_events":
1159
- if (message.id) {
1160
- void subscribeToOpenCodeEvents(port, message.id, ws, state);
1161
- }
1162
- break;
1163
- case "unsubscribe_events":
1164
- if (message.id) {
1165
- cancelEventSubscription(message.id, state);
1166
- }
1167
- break;
1168
- }
1279
+ message = JSON.parse(data.toString());
1169
1280
  } catch (error2) {
1170
1281
  const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
1171
- logActivity(state, {
1172
- type: "error",
1173
- error: `Failed to handle message: ${errorMessage}`
1282
+ onError?.(`Failed to handle message: ${errorMessage}`);
1283
+ return;
1284
+ }
1285
+ if (isStreamFrame(message)) {
1286
+ forwarder.handleFrame(message);
1287
+ return;
1288
+ }
1289
+ switch (message.type) {
1290
+ case "connected": {
1291
+ clearTimeout(connectionTimeout);
1292
+ const connectedAgentId = message.agent_id ?? agentId;
1293
+ onConnected?.(connectedAgentId);
1294
+ resolve({
1295
+ ws,
1296
+ close: () => ws.close(1e3, "CLI shutdown")
1297
+ });
1298
+ break;
1299
+ }
1300
+ case "error":
1301
+ clearTimeout(connectionTimeout);
1302
+ onError?.(message.message || "Unknown tunnel error");
1303
+ if (message.code === "unauthorized") {
1304
+ ws.close();
1305
+ reject(new Error("Unauthorized"));
1306
+ }
1307
+ break;
1308
+ case "ping":
1309
+ ws.send(JSON.stringify({ type: "pong" }));
1310
+ break;
1311
+ }
1312
+ });
1313
+ ws.on("error", (error2) => {
1314
+ clearTimeout(connectionTimeout);
1315
+ const detail = upgradeRejection ?? describeSocketError(error2, url);
1316
+ onError?.(`Connection error: ${detail}`);
1317
+ reject(upgradeRejection ? new Error(upgradeRejection) : new Error(detail));
1318
+ });
1319
+ ws.on("close", (code, reason) => {
1320
+ const reasonStr = reason.toString() || upgradeRejection || (code === 1006 ? "abnormal closure" : "No reason provided");
1321
+ forwarder.abortAll();
1322
+ streamStartTimes.clear();
1323
+ onDisconnected?.(code, reasonStr);
1324
+ });
1325
+ });
1326
+ }
1327
+
1328
+ // src/lib/tunnel/runner-connection.ts
1329
+ var RunnerConnection = class {
1330
+ opts;
1331
+ sleep;
1332
+ connection = null;
1333
+ resolvedAgentId;
1334
+ /** True while a (re)connect loop is in flight. */
1335
+ reconnecting = false;
1336
+ /** The in-flight reconnect promise, awaitable by the caller. */
1337
+ reconnectPromise = null;
1338
+ /** 1-based count of the current reconnect attempt streak. */
1339
+ reconnectAttempt = 0;
1340
+ constructor(opts) {
1341
+ this.opts = opts;
1342
+ this.resolvedAgentId = opts.agentId;
1343
+ this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1344
+ }
1345
+ get agentId() {
1346
+ return this.resolvedAgentId;
1347
+ }
1348
+ /** Establish the initial tunnel connection (with retry/backoff). */
1349
+ async connect() {
1350
+ await this.connectWithRetry(false);
1351
+ }
1352
+ /** Close the active connection (idempotent). */
1353
+ close() {
1354
+ if (this.connection) {
1355
+ try {
1356
+ this.connection.close();
1357
+ } catch {
1358
+ }
1359
+ this.connection = null;
1360
+ }
1361
+ }
1362
+ async connectWithRetry(isReconnect) {
1363
+ if (isReconnect && this.reconnecting) return;
1364
+ this.reconnecting = true;
1365
+ this.close();
1366
+ const { events } = this.opts;
1367
+ while (this.opts.isRunning()) {
1368
+ try {
1369
+ this.connection = await connectTunnel({
1370
+ agentId: this.resolvedAgentId,
1371
+ authHeader: this.opts.getAuthHeader(),
1372
+ port: this.opts.port,
1373
+ onConnected: (agentId) => {
1374
+ this.reconnectAttempt = 0;
1375
+ this.reconnecting = false;
1376
+ this.resolvedAgentId = agentId;
1377
+ events.onConnected(agentId, isReconnect);
1378
+ },
1379
+ onDisconnected: (code, reason) => {
1380
+ events.onDisconnected(code, reason);
1381
+ if (this.opts.isRunning() && code !== 1e3 && !this.reconnecting) {
1382
+ this.reconnectPromise = this.connectWithRetry(true).catch((err) => {
1383
+ events.onError?.(`Reconnection failed: ${err.message}`);
1384
+ });
1385
+ }
1386
+ },
1387
+ onError: (error2) => events.onError?.(error2),
1388
+ onResponse: () => events.onResponse?.(),
1389
+ onInfo: (message) => events.onInfo?.(message)
1390
+ });
1391
+ return;
1392
+ } catch (error2) {
1393
+ this.reconnectAttempt++;
1394
+ if (error2.message === "Unauthorized") {
1395
+ this.reconnecting = false;
1396
+ throw error2;
1397
+ }
1398
+ const delay = getReconnectDelay(this.reconnectAttempt);
1399
+ events.onReconnecting?.(this.reconnectAttempt);
1400
+ events.onError?.(`Connection failed, retrying in ${Math.round(delay / 1e3)}s...`);
1401
+ await this.sleep(delay);
1402
+ }
1403
+ }
1404
+ this.reconnecting = false;
1405
+ }
1406
+ };
1407
+
1408
+ // src/lib/channels/driver.ts
1409
+ var DEFAULT_RETRY_POLICY = {
1410
+ maxAttempts: 6,
1411
+ baseDelayMs: 500,
1412
+ maxDelayMs: 3e4
1413
+ };
1414
+ var ChannelAuthError = class extends Error {
1415
+ constructor(message) {
1416
+ super(message);
1417
+ this.name = "ChannelAuthError";
1418
+ }
1419
+ };
1420
+ function backoffDelay(attempt, policy) {
1421
+ const exp = policy.baseDelayMs * Math.pow(2, attempt);
1422
+ const capped = Math.min(policy.maxDelayMs, exp);
1423
+ return Math.floor(Math.random() * capped);
1424
+ }
1425
+ function isRetryableStatus(status) {
1426
+ return status === 429 || status >= 500 && status <= 599;
1427
+ }
1428
+ var ChannelDriver = class {
1429
+ agentId;
1430
+ port;
1431
+ apiUrl;
1432
+ getAuthHeader;
1433
+ conversationFilter;
1434
+ retry;
1435
+ log;
1436
+ fetchImpl;
1437
+ sleep;
1438
+ /** Cache of conversationId → opencode sessionId. */
1439
+ sessions = /* @__PURE__ */ new Map();
1440
+ /** Serialises drains so a reconnect during a drain doesn't double-process. */
1441
+ draining = false;
1442
+ constructor(config2) {
1443
+ this.agentId = config2.agentId;
1444
+ this.port = config2.port;
1445
+ this.apiUrl = config2.apiUrl.replace(/\/$/, "");
1446
+ this.getAuthHeader = config2.getAuthHeader;
1447
+ this.conversationFilter = config2.conversationFilter ?? null;
1448
+ this.retry = { ...DEFAULT_RETRY_POLICY, ...config2.retry };
1449
+ this.log = config2.log ?? (() => {
1450
+ });
1451
+ this.fetchImpl = config2.fetchImpl ?? fetch;
1452
+ this.sleep = config2.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1453
+ }
1454
+ /** The IPv4-loopback base URL for the local `opencode serve`. */
1455
+ get opencodeBase() {
1456
+ return `http://127.0.0.1:${this.port}`;
1457
+ }
1458
+ // -------------------------------------------------------------------------
1459
+ // Public API
1460
+ // -------------------------------------------------------------------------
1461
+ /**
1462
+ * Drain all pending channel conversations once: poll → process → callback.
1463
+ * Called on tunnel `connected` (WI-CHAN-4) and on each poll tick by `run.ts`.
1464
+ * Re-entrant calls while a drain is in flight are skipped (return 0).
1465
+ *
1466
+ * @returns the number of messages processed.
1467
+ */
1468
+ async drainPending() {
1469
+ if (this.draining) return 0;
1470
+ this.draining = true;
1471
+ let processed = 0;
1472
+ try {
1473
+ const conversations = await this.getPendingConversations();
1474
+ for (const conv of conversations) {
1475
+ processed += await this.processConversation(conv);
1476
+ }
1477
+ } finally {
1478
+ this.draining = false;
1479
+ }
1480
+ return processed;
1481
+ }
1482
+ // -------------------------------------------------------------------------
1483
+ // Conversation processing
1484
+ // -------------------------------------------------------------------------
1485
+ async processConversation(conv) {
1486
+ const sessionId = await this.ensureSession(conv);
1487
+ const messages = await this.getPendingMessages(conv.id);
1488
+ let processed = 0;
1489
+ for (const message of messages) {
1490
+ const claimed = await this.markProcessing(conv.id, message.id);
1491
+ if (!claimed) {
1492
+ this.log({
1493
+ level: "info",
1494
+ message: `Message ${message.id.slice(0, 8)} already claimed \u2014 skipping`,
1495
+ conversation_id: conv.id,
1496
+ message_id: message.id
1174
1497
  });
1175
- telemetry.error(
1176
- EventTypes.TUNNEL_ERROR,
1177
- `Failed to handle message: ${errorMessage}`,
1498
+ continue;
1499
+ }
1500
+ try {
1501
+ await sendMessageToOpenCode(
1502
+ this.port,
1503
+ sessionId,
1504
+ message.content,
1178
1505
  {
1179
- error: errorMessage
1506
+ agent: message.opencode_agent ?? void 0,
1507
+ model: message.opencode_model ?? void 0
1180
1508
  },
1181
- state.sandboxId ?? void 0
1509
+ {
1510
+ onQuestion: (question) => this.reportInteraction(conv.id, "question", question),
1511
+ onPermission: (permission) => this.reportInteraction(conv.id, "permission", permission)
1512
+ }
1182
1513
  );
1183
- displayStatus(state);
1514
+ await this.confirmCompletion(sessionId);
1515
+ await this.markDone(conv.id, message.id, sessionId);
1516
+ processed += 1;
1517
+ this.log({
1518
+ level: "info",
1519
+ message: `Message ${message.id.slice(0, 8)} processed`,
1520
+ conversation_id: conv.id,
1521
+ message_id: message.id
1522
+ });
1523
+ } catch (err) {
1524
+ if (err instanceof ChannelAuthError) throw err;
1525
+ await this.markFailed(conv.id, message.id).catch(() => {
1526
+ });
1527
+ this.log({
1528
+ level: "error",
1529
+ message: `Message ${message.id.slice(0, 8)} failed: ${err instanceof Error ? err.message : String(err)}`,
1530
+ conversation_id: conv.id,
1531
+ message_id: message.id
1532
+ });
1184
1533
  }
1534
+ }
1535
+ return processed;
1536
+ }
1537
+ async ensureSession(conv) {
1538
+ const cached = this.sessions.get(conv.id);
1539
+ if (cached) return cached;
1540
+ if (conv.opencode_session_id) {
1541
+ this.sessions.set(conv.id, conv.opencode_session_id);
1542
+ return conv.opencode_session_id;
1543
+ }
1544
+ const sessionId = await createOpenCodeSession(this.port);
1545
+ this.sessions.set(conv.id, sessionId);
1546
+ await this.persistSession(conv.id, sessionId).catch(() => {
1185
1547
  });
1186
- ws.on("close", (code, reason) => {
1187
- state.connected = false;
1188
- 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",
1548
+ return sessionId;
1549
+ }
1550
+ /**
1551
+ * Local reconcile: re-query `GET /session/:id` and check `time.completed`.
1552
+ * Best-effort — if opencode is unreachable or the field is absent we proceed
1553
+ * to mark done anyway (the blocking call already returned).
1554
+ */
1555
+ async confirmCompletion(sessionId) {
1556
+ try {
1557
+ const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}`);
1558
+ if (!res.ok) return;
1559
+ const session = await res.json();
1560
+ if (session.time && session.time.completed == null) {
1561
+ this.log({
1562
+ level: "info",
1563
+ message: `Session ${sessionId.slice(0, 8)} not marked completed on reconcile \u2014 delivering anyway`
1564
+ });
1565
+ }
1566
+ } catch {
1567
+ }
1568
+ }
1569
+ // -------------------------------------------------------------------------
1570
+ // Evident API calls (combinedAuth thread routes)
1571
+ // -------------------------------------------------------------------------
1572
+ async getPendingConversations() {
1573
+ const res = await this.fetchImpl(
1574
+ `${this.apiUrl}/agents/${this.agentId}/conversations/pending`,
1575
+ {
1576
+ headers: { Authorization: this.getAuthHeader() }
1577
+ }
1578
+ );
1579
+ this.assertAuth(res, "fetching pending conversations");
1580
+ if (!res.ok) {
1581
+ throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);
1582
+ }
1583
+ const data = await res.json();
1584
+ let conversations = data.conversations;
1585
+ if (this.conversationFilter) {
1586
+ conversations = conversations.filter((c) => c.id === this.conversationFilter);
1587
+ }
1588
+ return conversations;
1589
+ }
1590
+ async getPendingMessages(conversationId) {
1591
+ const res = await this.fetchImpl(
1592
+ `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages?status=pending`,
1593
+ { headers: { Authorization: this.getAuthHeader() } }
1594
+ );
1595
+ this.assertAuth(res, "fetching pending messages");
1596
+ if (!res.ok) {
1597
+ throw new Error(`Failed to get messages: HTTP ${res.status}`);
1598
+ }
1599
+ return await res.json();
1600
+ }
1601
+ async markProcessing(conversationId, messageId) {
1602
+ const res = await this.fetchImpl(
1603
+ `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
1604
+ {
1605
+ method: "PATCH",
1606
+ headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
1607
+ body: JSON.stringify({ status: "processing" })
1608
+ }
1609
+ );
1610
+ this.assertAuth(res, "marking message as processing");
1611
+ return res.ok;
1612
+ }
1613
+ /**
1614
+ * EXISTING combinedAuth completion route — idempotent + retried (WI-CHAN-2).
1615
+ * `PATCH .../messages/:id {status:'done', opencode_session_id}`. The server's
1616
+ * `queued_conversation_messages.status`/`processed_at` gate makes a re-call
1617
+ * for an already-`done` message a no-op (no double Slack post).
1618
+ */
1619
+ async markDone(conversationId, messageId, sessionId) {
1620
+ await this.callWithRetry(
1621
+ "marking message as done",
1622
+ () => this.fetchImpl(
1623
+ `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
1196
1624
  {
1197
- sandboxId: state.sandboxId,
1198
- code,
1199
- reason: reasonStr
1200
- },
1201
- state.sandboxId ?? void 0
1202
- );
1203
- displayStatus(state);
1204
- resolve();
1205
- });
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}`,
1625
+ method: "PATCH",
1626
+ headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
1627
+ body: JSON.stringify({ status: "done", opencode_session_id: sessionId })
1628
+ }
1629
+ )
1630
+ );
1631
+ }
1632
+ async markFailed(conversationId, messageId) {
1633
+ await this.callWithRetry(
1634
+ "marking message as failed",
1635
+ () => this.fetchImpl(
1636
+ `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
1215
1637
  {
1216
- error: error2.message
1217
- },
1218
- state.sandboxId ?? void 0
1638
+ method: "PATCH",
1639
+ headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
1640
+ body: JSON.stringify({ status: "failed" })
1641
+ }
1642
+ )
1643
+ );
1644
+ }
1645
+ async persistSession(conversationId, sessionId) {
1646
+ const res = await this.fetchImpl(
1647
+ `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}`,
1648
+ {
1649
+ method: "PATCH",
1650
+ headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
1651
+ body: JSON.stringify({ opencode_session_id: sessionId })
1652
+ }
1653
+ );
1654
+ this.assertAuth(res, "persisting session id");
1655
+ }
1656
+ /**
1657
+ * EXISTING combinedAuth interaction route (WI-CHAN-3) — idempotent + retried.
1658
+ * `POST .../interactive-event {type, data}`. The server persists the
1659
+ * interaction and posts a link to the proxied opencode-web conversation.
1660
+ */
1661
+ async reportInteraction(conversationId, type, data) {
1662
+ try {
1663
+ await this.callWithRetry(
1664
+ "reporting interactive event",
1665
+ () => this.fetchImpl(
1666
+ `${this.apiUrl}/agents/${this.agentId}/threads/${conversationId}/interactive-event`,
1667
+ {
1668
+ method: "POST",
1669
+ headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
1670
+ body: JSON.stringify({ type, data })
1671
+ }
1672
+ )
1219
1673
  );
1220
- displayStatus(state);
1221
- });
1222
- const cleanup = async () => {
1223
- process.removeAllListeners("SIGINT");
1224
- process.removeAllListeners("SIGTERM");
1225
- logActivity(state, {
1226
- type: "info",
1227
- message: "Shutting down..."
1674
+ this.log({
1675
+ level: "info",
1676
+ message: `${type} surfaced to channel (id: ${data.id.slice(0, 8)})`,
1677
+ conversation_id: conversationId
1228
1678
  });
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
1679
+ } catch (err) {
1680
+ if (err instanceof ChannelAuthError) throw err;
1681
+ this.log({
1682
+ level: "error",
1683
+ message: `Failed to surface ${type}: ${err instanceof Error ? err.message : String(err)}`,
1684
+ conversation_id: conversationId
1685
+ });
1686
+ }
1687
+ }
1688
+ // -------------------------------------------------------------------------
1689
+ // Retry wrapper
1690
+ // -------------------------------------------------------------------------
1691
+ /**
1692
+ * Invoke an Evident API call, retrying on transient failures (5xx / 429 /
1693
+ * network errors) with exponential backoff + jitter (capped). Auth failures
1694
+ * (401/403) are terminal and surface as `ChannelAuthError`; other 4xx are
1695
+ * terminal too. No on-disk persistence — a crash mid-retry drops the callback
1696
+ * (accepted by ADR-0039).
1697
+ */
1698
+ async callWithRetry(context, call) {
1699
+ let lastError;
1700
+ for (let attempt = 0; attempt < this.retry.maxAttempts; attempt += 1) {
1701
+ let res;
1702
+ try {
1703
+ res = await call();
1704
+ } catch (err) {
1705
+ lastError = err;
1706
+ if (attempt < this.retry.maxAttempts - 1) {
1707
+ await this.sleep(backoffDelay(attempt, this.retry));
1708
+ continue;
1709
+ }
1710
+ throw err;
1711
+ }
1712
+ if (res.status === 401 || res.status === 403) {
1713
+ throw new ChannelAuthError(
1714
+ `Authentication failed during ${context}: HTTP ${res.status}. Your session may have expired.`
1715
+ );
1716
+ }
1717
+ if (res.ok) return;
1718
+ if (isRetryableStatus(res.status)) {
1719
+ lastError = new Error(`${context}: HTTP ${res.status}`);
1720
+ if (attempt < this.retry.maxAttempts - 1) {
1721
+ await this.sleep(backoffDelay(attempt, this.retry));
1722
+ continue;
1723
+ }
1724
+ }
1725
+ throw new Error(`${context}: HTTP ${res.status}`);
1726
+ }
1727
+ throw lastError instanceof Error ? lastError : new Error(`${context}: exhausted retries`);
1728
+ }
1729
+ assertAuth(res, context) {
1730
+ if (res.status === 401 || res.status === 403) {
1731
+ throw new ChannelAuthError(
1732
+ `Authentication failed during ${context}: HTTP ${res.status}. Your session may have expired.`
1237
1733
  );
1238
- await shutdownTelemetry();
1239
- ws.close();
1240
- process.exit(0);
1241
- };
1242
- process.removeAllListeners("SIGINT");
1243
- process.removeAllListeners("SIGTERM");
1244
- process.once("SIGINT", () => void cleanup());
1245
- process.once("SIGTERM", () => void cleanup());
1734
+ }
1735
+ }
1736
+ };
1737
+
1738
+ // src/commands/ensure-opencode.ts
1739
+ import chalk5 from "chalk";
1740
+ import ora2 from "ora";
1741
+ import { select as select2 } from "@inquirer/prompts";
1742
+ async function ensureOpenCodeRunning(ctx) {
1743
+ const healthCheck = await checkOpenCodeHealth(ctx.port);
1744
+ if (healthCheck.healthy) {
1745
+ return { port: ctx.port, process: null, version: healthCheck.version ?? null };
1746
+ }
1747
+ const runningInstances = await findHealthyOpenCodeInstances();
1748
+ if (runningInstances.length > 0) {
1749
+ if (!ctx.interactive) {
1750
+ throw new Error(
1751
+ `OpenCode not found on port ${ctx.port}, but running on port ${runningInstances[0].port}. Use --port ${runningInstances[0].port}`
1752
+ );
1753
+ }
1754
+ blank();
1755
+ console.log(chalk5.yellow("Found OpenCode running on different port(s):"));
1756
+ for (const instance of runningInstances) {
1757
+ const ver = instance.version ? ` (v${instance.version})` : "";
1758
+ const cwd = instance.cwd ? ` in ${instance.cwd}` : "";
1759
+ console.log(chalk5.dim(` * Port ${instance.port}${ver}${cwd}`));
1760
+ }
1761
+ blank();
1762
+ if (runningInstances.length === 1) {
1763
+ console.log(chalk5.yellow("Tip: Run with the correct port:"));
1764
+ console.log(
1765
+ chalk5.dim(
1766
+ ` ${getCliName()} run --agent ${ctx.agentId} --port ${runningInstances[0].port}`
1767
+ )
1768
+ );
1769
+ }
1770
+ blank();
1771
+ throw new Error(`OpenCode not running on port ${ctx.port}`);
1772
+ }
1773
+ if (!isOpenCodeInstalled()) {
1774
+ if (!ctx.interactive) {
1775
+ throw new Error("OpenCode is not installed. Install it with: npm install -g opencode-ai");
1776
+ }
1777
+ const result = await promptOpenCodeInstall(true);
1778
+ if (result === "exit") process.exit(0);
1779
+ if (result !== "installed" && !isOpenCodeInstalled()) {
1780
+ throw new Error("OpenCode is not installed");
1781
+ }
1782
+ }
1783
+ if (!ctx.interactive) {
1784
+ ctx.log(`OpenCode is not running on port ${ctx.port}. Starting it automatically...`);
1785
+ const proc = await startOpenCode(ctx.port);
1786
+ const health = await waitForOpenCodeHealth(ctx.port, 3e4);
1787
+ if (!health.healthy) {
1788
+ throw new Error(
1789
+ `OpenCode failed to start on port ${ctx.port}. Install with: npm install -g opencode-ai`
1790
+ );
1791
+ }
1792
+ ctx.log(`OpenCode started on port ${ctx.port}${health.version ? ` (v${health.version})` : ""}`);
1793
+ return { port: ctx.port, process: proc, version: health.version ?? null };
1794
+ }
1795
+ let port = ctx.port;
1796
+ if (isPortInUse(port)) {
1797
+ console.log(chalk5.yellow(`
1798
+ Port ${port} is already in use.`));
1799
+ const alternativePort = findAvailablePort(port + 1);
1800
+ if (alternativePort) {
1801
+ const useAlternative = await select2({
1802
+ message: `Use port ${alternativePort} instead?`,
1803
+ choices: [
1804
+ { name: `Yes, use port ${alternativePort}`, value: "yes" },
1805
+ { name: "No, I will free the port manually", value: "no" }
1806
+ ]
1807
+ });
1808
+ if (useAlternative === "yes") {
1809
+ port = alternativePort;
1810
+ } else {
1811
+ throw new Error(`Port ${ctx.port} is in use`);
1812
+ }
1813
+ }
1814
+ }
1815
+ const action = await select2({
1816
+ message: "OpenCode is not running. What would you like to do?",
1817
+ choices: [
1818
+ {
1819
+ name: "Start OpenCode for me",
1820
+ value: "start",
1821
+ description: `Run 'opencode serve --port ${port}'`
1822
+ },
1823
+ {
1824
+ name: "Show me the command",
1825
+ value: "manual",
1826
+ description: "Display the command to run manually"
1827
+ },
1828
+ {
1829
+ name: "Continue without OpenCode",
1830
+ value: "continue",
1831
+ description: "Requests will fail until OpenCode starts"
1832
+ }
1833
+ ]
1246
1834
  });
1835
+ if (action === "manual") {
1836
+ blank();
1837
+ console.log(chalk5.bold("Run this command in another terminal:"));
1838
+ blank();
1839
+ console.log(` ${chalk5.cyan(`opencode serve --port ${port}`)}`);
1840
+ blank();
1841
+ throw new Error("Please start OpenCode manually");
1842
+ }
1843
+ if (action === "start") {
1844
+ const spinner = ora2("Starting OpenCode...").start();
1845
+ const proc = await startOpenCode(port);
1846
+ const health = await waitForOpenCodeHealth(port, 3e4);
1847
+ if (!health.healthy) {
1848
+ spinner.fail("Failed to start OpenCode");
1849
+ throw new Error("OpenCode failed to start");
1850
+ }
1851
+ spinner.succeed(
1852
+ `OpenCode running on port ${port}${health.version ? ` (v${health.version})` : ""}`
1853
+ );
1854
+ return { port, process: proc, version: health.version ?? null };
1855
+ }
1856
+ return { port, process: null, version: null };
1857
+ }
1858
+
1859
+ // src/commands/agent-lookup.ts
1860
+ async function resolveAgentIdFromKey(authHeader) {
1861
+ const apiUrl = getApiUrlConfig();
1862
+ try {
1863
+ const response = await fetch(`${apiUrl}/me`, {
1864
+ headers: { Authorization: authHeader }
1865
+ });
1866
+ if (!response.ok) {
1867
+ return { error: `Failed to resolve agent from key: HTTP ${response.status}` };
1868
+ }
1869
+ const data = await response.json();
1870
+ if (data.auth_type === "agent_key" && data.agent_id) {
1871
+ return { agent_id: data.agent_id };
1872
+ }
1873
+ return {
1874
+ error: "Cannot resolve agent ID: auth type is not agent_key. Please provide --agent explicitly."
1875
+ };
1876
+ } catch (error2) {
1877
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
1878
+ return { error: `Failed to resolve agent from key: ${message}` };
1879
+ }
1880
+ }
1881
+ async function getAgentInfo(agentId, authHeader) {
1882
+ const apiUrl = getApiUrlConfig();
1883
+ try {
1884
+ const response = await fetch(`${apiUrl}/agents/${agentId}`, {
1885
+ headers: { Authorization: authHeader }
1886
+ });
1887
+ if (response.status === 404) {
1888
+ return { valid: false, error: "Agent not found" };
1889
+ }
1890
+ if (response.status === 401) {
1891
+ return { valid: false, error: "Authentication failed", authFailed: true };
1892
+ }
1893
+ if (!response.ok) {
1894
+ return { valid: false, error: `API error: ${response.status}` };
1895
+ }
1896
+ const agent = await response.json();
1897
+ if (agent.agent_type !== "local") {
1898
+ return {
1899
+ valid: false,
1900
+ error: `Agent is type '${agent.agent_type}', must be 'local' for CLI connection`
1901
+ };
1902
+ }
1903
+ return { valid: true, agent };
1904
+ } catch (error2) {
1905
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
1906
+ return { valid: false, error: `Failed to validate agent: ${message}` };
1907
+ }
1908
+ }
1909
+
1910
+ // src/commands/run.ts
1911
+ var MAX_ACTIVITY_LOG_ENTRIES = 10;
1912
+ var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
1913
+ function log(state, message, isError = false) {
1914
+ if (state.json) {
1915
+ console.log(
1916
+ JSON.stringify({
1917
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1918
+ level: isError ? "error" : "info",
1919
+ message
1920
+ })
1921
+ );
1922
+ } else if (!state.interactive) {
1923
+ const prefix = isError ? chalk6.red("\u2717") : chalk6.green("\u2022");
1924
+ console.log(`${prefix} ${message}`);
1925
+ }
1926
+ }
1927
+ function logActivity(state, entry) {
1928
+ const fullEntry = {
1929
+ ...entry,
1930
+ timestamp: /* @__PURE__ */ new Date()
1931
+ };
1932
+ state.activityLog.push(fullEntry);
1933
+ if (state.activityLog.length > MAX_ACTIVITY_LOG_ENTRIES) {
1934
+ state.activityLog.shift();
1935
+ }
1936
+ if (!state.interactive) {
1937
+ if (entry.type === "error") {
1938
+ log(state, entry.error ?? "Unknown error", true);
1939
+ } else if (entry.type === "info" && entry.message) {
1940
+ log(state, entry.message);
1941
+ }
1942
+ }
1943
+ }
1944
+ function displayStatus(state) {
1945
+ if (!state.interactive) return;
1946
+ const attempt = state.connection?.reconnectAttempt ?? 0;
1947
+ const tunnel = state.connected ? chalk6.green("tunnel: connected") : attempt > 0 ? chalk6.yellow(`tunnel: reconnecting (#${attempt})`) : chalk6.yellow("tunnel: connecting");
1948
+ const opencode = state.opencodeConnected ? chalk6.green(`opencode: :${state.port}`) : chalk6.red(`opencode: :${state.port} (down)`);
1949
+ const messages = state.messageCount > 0 ? chalk6.dim(` \xB7 ${state.messageCount} processed`) : "";
1950
+ const last = state.activityLog[state.activityLog.length - 1];
1951
+ const detail = last ? chalk6.dim(` \xB7 ${last.type === "error" ? last.error ?? "" : last.message ?? ""}`) : "";
1952
+ const agent = state.agentName ?? state.agentId;
1953
+ console.log(
1954
+ `${chalk6.bold("Evident")} ${chalk6.dim(agent)} ${tunnel} ${opencode}${messages}${detail}`
1955
+ );
1247
1956
  }
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.`);
1957
+ async function promptForLogin(promptMessage, successMessage) {
1958
+ const action = await select3({
1959
+ message: promptMessage,
1960
+ choices: [
1961
+ {
1962
+ name: "Yes, log me in",
1963
+ value: "login",
1964
+ description: "Opens a browser to authenticate with Evident"
1965
+ },
1966
+ {
1967
+ name: "No, exit",
1968
+ value: "exit",
1969
+ description: "Exit without logging in"
1970
+ }
1971
+ ]
1972
+ });
1973
+ if (action === "exit") {
1974
+ console.log(chalk6.dim(`
1975
+ You can log in later by running: ${getCliName()} login`));
1976
+ process.exit(0);
1977
+ }
1978
+ await login({ noBrowser: false });
1979
+ const credentials2 = await getToken();
1980
+ if (!credentials2) {
1981
+ printError("Login failed. Please try again.");
1255
1982
  process.exit(1);
1256
1983
  }
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");
1984
+ blank();
1985
+ console.log(chalk6.green(successMessage));
1986
+ blank();
1987
+ return { token: credentials2.token, authType: "bearer", user: credentials2.user };
1988
+ }
1989
+ var AUTH_EXPIRED_EXIT_CODE = 77;
1990
+ async function handleAuthError(state, error2) {
1991
+ logActivity(state, {
1992
+ type: "error",
1993
+ error: error2.message
1994
+ });
1995
+ if (state.interactive) displayStatus(state);
1996
+ if (!state.interactive) {
1262
1997
  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>`));
1998
+ console.log(chalk6.red("Authentication expired"));
1999
+ console.log(chalk6.dim("Your authentication token is no longer valid."));
1267
2000
  blank();
1268
- telemetry.error(EventTypes.CLI_ERROR, "Missing sandbox ID", { command: "tunnel" });
1269
- process.exit(1);
2001
+ console.log(chalk6.dim("To fix this:"));
2002
+ console.log(chalk6.dim(` 1. Run '${getCliName()} login' to re-authenticate`));
2003
+ console.log(chalk6.dim(" 2. Restart this command"));
2004
+ blank();
2005
+ await cleanup(state);
2006
+ await shutdownTelemetry();
2007
+ process.exit(AUTH_EXPIRED_EXIT_CODE);
2008
+ return { success: false };
1270
2009
  }
2010
+ blank();
2011
+ console.log(chalk6.yellow("Your authentication has expired."));
2012
+ blank();
2013
+ try {
2014
+ const credentials2 = await promptForLogin(
2015
+ "Would you like to log in again?",
2016
+ "Re-authenticated successfully! Resuming..."
2017
+ );
2018
+ const newAuthHeader = getAuthHeader(credentials2);
2019
+ return { success: true, newAuthHeader };
2020
+ } catch {
2021
+ return { success: false };
2022
+ }
2023
+ }
2024
+ async function driveChannels(state, driver) {
2025
+ let idlePolls = 0;
2026
+ while (state.running) {
2027
+ if (state.connection?.reconnecting && state.connection.reconnectPromise) {
2028
+ logActivity(state, { type: "info", message: "Waiting for tunnel reconnection..." });
2029
+ if (state.interactive) displayStatus(state);
2030
+ await state.connection.reconnectPromise;
2031
+ }
2032
+ try {
2033
+ const processed = await driver.drainPending();
2034
+ state.messageCount += processed;
2035
+ if (processed > 0) {
2036
+ idlePolls = 0;
2037
+ if (state.interactive) displayStatus(state);
2038
+ } else if (state.idleTimeout !== null) {
2039
+ idlePolls++;
2040
+ if (idlePolls === 1) {
2041
+ logActivity(state, {
2042
+ type: "info",
2043
+ message: `Queue empty, waiting (timeout: ${state.idleTimeout}s)...`
2044
+ });
2045
+ if (state.interactive) displayStatus(state);
2046
+ }
2047
+ }
2048
+ } catch (error2) {
2049
+ if (error2 instanceof ChannelAuthError) {
2050
+ const result = await handleAuthError(state, error2);
2051
+ if (result.success && result.newAuthHeader) {
2052
+ state.authHeader = result.newAuthHeader;
2053
+ logActivity(state, { type: "info", message: "Continuing with new credentials..." });
2054
+ if (state.interactive) displayStatus(state);
2055
+ continue;
2056
+ }
2057
+ state.running = false;
2058
+ break;
2059
+ }
2060
+ const errorMessage = error2 instanceof Error ? error2.message : String(error2);
2061
+ logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage}` });
2062
+ if (state.interactive) displayStatus(state);
2063
+ }
2064
+ await new Promise((resolve) => setTimeout(resolve, CHANNEL_POLL_INTERVAL_MS));
2065
+ if (state.idleTimeout !== null && idlePolls >= 2) {
2066
+ const idleMs = idlePolls * CHANNEL_POLL_INTERVAL_MS;
2067
+ if (idleMs > state.idleTimeout * 1e3) {
2068
+ logActivity(state, { type: "info", message: "Idle timeout reached" });
2069
+ if (state.interactive) displayStatus(state);
2070
+ break;
2071
+ }
2072
+ }
2073
+ }
2074
+ }
2075
+ async function cleanup(state) {
2076
+ state.running = false;
2077
+ if (state.connection) {
2078
+ state.connection.close();
2079
+ state.connection = null;
2080
+ }
2081
+ if (state.opencodeProcess) {
2082
+ stopOpenCode(state.opencodeProcess);
2083
+ if (state.interactive) {
2084
+ logActivity(state, { type: "info", message: "Stopped OpenCode process" });
2085
+ displayStatus(state);
2086
+ } else {
2087
+ log(state, "Stopped OpenCode process");
2088
+ }
2089
+ state.opencodeProcess = null;
2090
+ }
2091
+ }
2092
+ async function run(options) {
2093
+ const interactive = isInteractive(options.json);
1271
2094
  const state = {
2095
+ agentId: options.agent || "",
2096
+ agentName: null,
2097
+ port: options.port ?? 4096,
2098
+ conversationFilter: options.conversation ?? null,
2099
+ idleTimeout: options.idleTimeout ?? null,
2100
+ json: options.json ?? false,
2101
+ interactive,
1272
2102
  connected: false,
1273
2103
  opencodeConnected: false,
1274
2104
  opencodeVersion: null,
1275
- sandboxId,
1276
- sandboxName: null,
1277
- reconnectAttempt: 0,
1278
- lastActivity: /* @__PURE__ */ new Date(),
2105
+ opencodeProcess: null,
2106
+ connection: null,
2107
+ running: true,
1279
2108
  activityLog: [],
1280
- pendingRequests: /* @__PURE__ */ new Map(),
1281
- verbose,
1282
- displayInitialized: false,
1283
- activeEventSubscriptions: /* @__PURE__ */ new Map()
2109
+ messageCount: 0,
2110
+ authHeader: ""
1284
2111
  };
1285
- telemetry.info(
1286
- EventTypes.CLI_COMMAND,
1287
- "Starting tunnel command",
1288
- {
1289
- command: "tunnel",
1290
- port,
1291
- sandboxId,
1292
- verbose
1293
- },
1294
- sandboxId
1295
- );
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
1315
- });
1316
- displayStatus(state);
1317
- process.exit(1);
1318
- }
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}`
1324
- });
1325
- logActivity(state, {
1326
- type: "info",
1327
- message: `Checking OpenCode on port ${port}...`
1328
- });
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
2112
+ if (state.idleTimeout === null && (process.env.GITHUB_ACTIONS || process.env.CI)) {
2113
+ log(
2114
+ state,
2115
+ "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.",
2116
+ false
1343
2117
  );
1344
- opencodeSpinner.succeed(`OpenCode running on port ${port}${version}`);
1345
- logActivity(state, {
1346
- type: "info",
1347
- message: `OpenCode running on port ${port}${version}`
1348
- });
1349
- } else {
1350
- telemetry.warn(
1351
- EventTypes.OPENCODE_HEALTH_FAILED,
1352
- `Could not connect to OpenCode: ${healthCheck.error}`,
1353
- {
1354
- port,
1355
- error: healthCheck.error
1356
- },
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}`));
1372
- }
1373
- blank();
1374
- if (runningInstances.length === 1) {
1375
- console.log(chalk4.yellow("Tip: Run with the correct port:"));
1376
- console.log(
1377
- chalk4.dim(
1378
- ` npx @evident-ai/cli@latest tunnel --sandbox ${sandboxId} --port ${runningInstances[0].port}`
1379
- )
1380
- );
1381
- } else {
1382
- console.log(chalk4.yellow("Tip: Specify which port to use:"));
1383
- console.log(
1384
- chalk4.dim(` npx @evident-ai/cli@latest tunnel --sandbox ${sandboxId} --port <PORT>`)
1385
- );
2118
+ }
2119
+ const handleSignal = async () => {
2120
+ if (state.interactive) {
2121
+ logActivity(state, { type: "info", message: "Shutting down..." });
2122
+ displayStatus(state);
2123
+ } else {
2124
+ log(state, "Shutting down...");
2125
+ }
2126
+ await cleanup(state);
2127
+ await shutdownTelemetry();
2128
+ process.exit(0);
2129
+ };
2130
+ process.on("SIGINT", handleSignal);
2131
+ process.on("SIGTERM", handleSignal);
2132
+ try {
2133
+ let credentials2 = await getAuthCredentials();
2134
+ if (!credentials2) {
2135
+ if (!interactive) {
2136
+ printError("Authentication required");
2137
+ blank();
2138
+ console.log(chalk6.dim("Set EVIDENT_AGENT_KEY environment variable for CI"));
2139
+ console.log(chalk6.dim("Or run `evident login` for interactive authentication"));
2140
+ blank();
2141
+ process.exit(1);
1386
2142
  }
1387
2143
  blank();
1388
- } else {
2144
+ console.log(chalk6.yellow("You are not logged in to Evident."));
1389
2145
  blank();
1390
- const action = await select({
1391
- message: "OpenCode is not running. What would you like to do?",
1392
- choices: [
1393
- {
1394
- name: "Start OpenCode for me",
1395
- value: "start",
1396
- description: `Run 'opencode serve --port ${port}' in a new process`
1397
- },
1398
- {
1399
- name: "Show me the command to run",
1400
- value: "manual",
1401
- description: "Display instructions for starting OpenCode manually"
1402
- },
1403
- {
1404
- name: "Continue without OpenCode",
1405
- value: "continue",
1406
- description: "Connect the tunnel anyway (requests will fail until OpenCode starts)"
1407
- }
1408
- ]
1409
- });
1410
- if (action === "start") {
1411
- let actualPort = port;
1412
- if (isPortInUse(port)) {
1413
- console.log(chalk4.yellow(`
1414
- Port ${port} is already in use by another process.`));
1415
- const alternativePort = findAvailablePort(port + 1);
1416
- if (alternativePort) {
1417
- const useAlternative = await select({
1418
- message: `Would you like to use port ${alternativePort} instead?`,
1419
- choices: [
1420
- { name: `Yes, use port ${alternativePort}`, value: "yes" },
1421
- { name: "No, I will free up the port manually", value: "no" }
1422
- ]
1423
- });
1424
- if (useAlternative === "yes") {
1425
- actualPort = alternativePort;
1426
- } else {
1427
- console.log(chalk4.dim(`
1428
- Free up port ${port} and run the tunnel command again.`));
1429
- blank();
1430
- process.exit(1);
1431
- }
1432
- } else {
1433
- console.log(
1434
- chalk4.red(
1435
- `Could not find an available port. Please free up port ${port} and try again.`
1436
- )
1437
- );
1438
- blank();
1439
- process.exit(1);
1440
- }
1441
- }
1442
- const opencodeStartSpinner = ora2(`Starting OpenCode on port ${actualPort}...`).start();
1443
- try {
1444
- let command = "opencode";
1445
- let args = ["serve", "--port", actualPort.toString()];
1446
- try {
1447
- execSync("which opencode", { stdio: "ignore" });
1448
- } catch {
1449
- command = "npx";
1450
- args = ["opencode", "serve", "--port", actualPort.toString()];
1451
- }
1452
- const child = spawn(command, args, {
1453
- detached: true,
1454
- stdio: "ignore",
1455
- cwd: process.cwd()
1456
- // Start in current working directory
1457
- });
1458
- child.unref();
1459
- const maxRetries = 10;
1460
- const retryDelayMs = 1e3;
1461
- let healthy = false;
1462
- let version;
1463
- for (let i = 0; i < maxRetries; i++) {
1464
- await sleep(retryDelayMs);
1465
- const retryHealth = await checkOpenCodeHealth(actualPort);
1466
- if (retryHealth.healthy) {
1467
- healthy = true;
1468
- version = retryHealth.version;
1469
- break;
1470
- }
1471
- opencodeStartSpinner.text = `Starting OpenCode on port ${actualPort}... (${i + 1}/${maxRetries})`;
1472
- }
1473
- if (healthy) {
1474
- state.opencodeConnected = true;
1475
- state.opencodeVersion = version ?? null;
1476
- const versionStr = version ? ` (v${version})` : "";
1477
- opencodeStartSpinner.succeed(`OpenCode started on port ${actualPort}${versionStr}`);
1478
- logActivity(state, {
1479
- type: "info",
1480
- message: `OpenCode started on port ${actualPort}${versionStr}`
1481
- });
1482
- } else {
1483
- opencodeStartSpinner.warn(
1484
- "OpenCode process started but not responding. Check if it started correctly."
1485
- );
2146
+ credentials2 = await promptForLogin(
2147
+ "Would you like to log in now?",
2148
+ "Login successful! Continuing..."
2149
+ );
2150
+ }
2151
+ state.authHeader = getAuthHeader(credentials2);
2152
+ if (!state.agentId) {
2153
+ if (credentials2.authType === "agent_key") {
2154
+ const resolved = await resolveAgentIdFromKey(state.authHeader);
2155
+ if (resolved.agent_id) {
2156
+ state.agentId = resolved.agent_id;
2157
+ log(state, `Resolved agent ID from key: ${state.agentId}`);
2158
+ if (state.interactive && !state.json) {
1486
2159
  logActivity(state, {
1487
2160
  type: "info",
1488
- message: "OpenCode may still be starting..."
2161
+ message: `Agent ID resolved from key: ${state.agentId}`
1489
2162
  });
1490
- console.log(chalk4.dim("\nTip: Check for errors by running OpenCode manually:"));
1491
- console.log(chalk4.dim(` opencode serve --port ${actualPort}`));
1492
- blank();
1493
2163
  }
1494
- } catch (error2) {
1495
- const msg = error2 instanceof Error ? error2.message : "Unknown error";
1496
- opencodeStartSpinner.fail(`Failed to start OpenCode: ${msg}`);
1497
- logActivity(state, {
1498
- type: "error",
1499
- error: `Failed to start OpenCode: ${msg}`
1500
- });
1501
- console.log(chalk4.yellow("\nYou can try starting it manually:"));
1502
- console.log(chalk4.dim(` opencode serve --port ${actualPort}`));
1503
- blank();
2164
+ } else {
2165
+ printError(resolved.error || "Failed to resolve agent ID from key");
2166
+ process.exit(1);
1504
2167
  }
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}`));
2168
+ } else {
2169
+ printError("--agent is required when not using EVIDENT_AGENT_KEY");
1514
2170
  blank();
1515
- console.log(
1516
- chalk4.dim("The tunnel will automatically forward requests once OpenCode is running.")
1517
- );
2171
+ console.log(chalk6.dim("Either provide --agent <id> or set EVIDENT_AGENT_KEY"));
1518
2172
  blank();
2173
+ process.exit(1);
1519
2174
  }
1520
2175
  }
1521
- }
1522
- while (true) {
2176
+ telemetry.info(
2177
+ EventTypes.CLI_COMMAND,
2178
+ "Starting run command",
2179
+ {
2180
+ command: "run",
2181
+ agentId: state.agentId,
2182
+ port: state.port,
2183
+ conversationFilter: state.conversationFilter,
2184
+ interactive
2185
+ },
2186
+ state.agentId
2187
+ );
2188
+ if (interactive && !state.json) {
2189
+ blank();
2190
+ console.log(chalk6.bold("Evident Run"));
2191
+ console.log(chalk6.dim("-".repeat(40)));
2192
+ }
2193
+ const spinner = interactive && !state.json ? ora3("Validating agent...").start() : null;
2194
+ let validation = await getAgentInfo(state.agentId, state.authHeader);
2195
+ if (!validation.valid && validation.authFailed && interactive) {
2196
+ spinner?.fail("Authentication failed");
2197
+ blank();
2198
+ console.log(chalk6.yellow("Your authentication token is invalid or expired."));
2199
+ blank();
2200
+ credentials2 = await promptForLogin(
2201
+ "Would you like to log in again?",
2202
+ "Login successful! Retrying..."
2203
+ );
2204
+ state.authHeader = getAuthHeader(credentials2);
2205
+ spinner?.start("Validating agent...");
2206
+ validation = await getAgentInfo(state.agentId, state.authHeader);
2207
+ }
2208
+ if (!validation.valid) {
2209
+ spinner?.fail(`Agent validation failed: ${validation.error}`);
2210
+ throw new Error(validation.error);
2211
+ }
2212
+ spinner?.succeed(`Agent: ${validation.agent.name || state.agentId}`);
2213
+ state.agentName = validation.agent.name;
2214
+ const ocSpinner = interactive && !state.json ? ora3("Checking OpenCode...").start() : null;
1523
2215
  try {
1524
- await connect(credentials.token, sandboxId, port, state);
1525
- state.reconnectAttempt++;
1526
- const delay = getReconnectDelay(state.reconnectAttempt);
1527
- logActivity(state, {
1528
- type: "info",
1529
- message: `Reconnecting in ${Math.round(delay / 1e3)}s (attempt ${state.reconnectAttempt})...`
2216
+ const oc = await ensureOpenCodeRunning({
2217
+ port: state.port,
2218
+ interactive: state.interactive,
2219
+ agentId: state.agentId,
2220
+ log: (message) => log(state, message)
1530
2221
  });
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
- displayStatus(state);
1541
- await sleep(delay);
2222
+ state.port = oc.port;
2223
+ state.opencodeProcess = oc.process;
2224
+ state.opencodeVersion = oc.version;
2225
+ state.opencodeConnected = oc.process !== null || oc.version !== null;
2226
+ const version = state.opencodeVersion ? ` (v${state.opencodeVersion})` : "";
2227
+ ocSpinner?.succeed(`OpenCode running on port ${state.port}${version}`);
1542
2228
  } 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.");
1556
- process.exit(1);
1557
- }
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
2229
+ ocSpinner?.fail(error2.message);
2230
+ throw error2;
2231
+ }
2232
+ const tunnelSpinner = interactive && !state.json ? ora3("Connecting tunnel...").start() : null;
2233
+ const channelDriver = new ChannelDriver({
2234
+ agentId: state.agentId,
2235
+ port: state.port,
2236
+ apiUrl: getApiUrlConfig(),
2237
+ getAuthHeader: () => state.authHeader,
2238
+ conversationFilter: state.conversationFilter,
2239
+ log: (entry) => logActivity(state, {
2240
+ type: entry.level === "error" ? "error" : "info",
2241
+ message: entry.message,
2242
+ error: entry.level === "error" ? entry.message : void 0
2243
+ })
2244
+ });
2245
+ const connection = new RunnerConnection({
2246
+ agentId: state.agentId,
2247
+ getAuthHeader: () => state.authHeader,
2248
+ port: state.port,
2249
+ isRunning: () => state.running,
2250
+ events: {
2251
+ onConnected: (agentId, isReconnect) => {
2252
+ state.connected = true;
2253
+ state.agentId = agentId;
2254
+ logActivity(state, {
2255
+ type: "info",
2256
+ message: `Tunnel ${isReconnect ? "reconnected" : "connected"} (agent: ${agentId})`
2257
+ });
2258
+ emitAgentConnected(state.agentId, { port: state.port });
2259
+ if (!isReconnect) tunnelSpinner?.succeed("Tunnel connected");
2260
+ if (state.interactive) displayStatus(state);
2261
+ channelDriver.drainPending().catch(() => {
2262
+ });
1568
2263
  },
1569
- state.sandboxId ?? void 0
1570
- );
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
+ onDisconnected: (code, reason) => {
2265
+ state.connected = false;
2266
+ logActivity(state, {
2267
+ type: "info",
2268
+ message: `Tunnel disconnected (code: ${code}, reason: ${reason})`
2269
+ });
2270
+ emitAgentDisconnected(state.agentId, { code, reason });
2271
+ if (state.interactive) displayStatus(state);
2272
+ },
2273
+ onError: (error2) => {
2274
+ logActivity(state, { type: "error", error: error2 });
2275
+ if (state.interactive) displayStatus(state);
2276
+ },
2277
+ // Web traffic is proxied transparently; only note opencode is live.
2278
+ onResponse: () => {
2279
+ state.opencodeConnected = true;
2280
+ },
2281
+ onInfo: (message) => logActivity(state, { type: "info", message })
2282
+ }
2283
+ });
2284
+ state.connection = connection;
2285
+ try {
2286
+ await connection.connect();
2287
+ } catch (error2) {
2288
+ if (error2.message === "Unauthorized") tunnelSpinner?.fail("Unauthorized");
2289
+ throw error2;
2290
+ }
2291
+ if (interactive && !state.json) {
1577
2292
  displayStatus(state);
1578
- await sleep(delay);
2293
+ } else {
2294
+ log(state, "Driving channel messages...");
2295
+ }
2296
+ await driveChannels(state, channelDriver);
2297
+ await cleanup(state);
2298
+ if (state.json) {
2299
+ console.log(
2300
+ JSON.stringify({
2301
+ status: "success",
2302
+ messages_processed: state.messageCount
2303
+ })
2304
+ );
2305
+ } else if (!interactive) {
2306
+ log(state, `Completed. Processed ${state.messageCount} message(s).`);
2307
+ }
2308
+ await shutdownTelemetry();
2309
+ process.exit(0);
2310
+ } catch (error2) {
2311
+ await cleanup(state);
2312
+ const message = error2 instanceof Error ? error2.message : String(error2);
2313
+ if (state.json) {
2314
+ console.log(JSON.stringify({ status: "error", error: message }));
2315
+ } else {
2316
+ printError(message);
1579
2317
  }
2318
+ telemetry.error(EventTypes.CLI_ERROR, `Run command failed: ${message}`, {
2319
+ command: "run",
2320
+ agentId: options.agent
2321
+ });
2322
+ await shutdownTelemetry();
2323
+ process.exit(1);
1580
2324
  }
1581
2325
  }
1582
2326
 
1583
2327
  // src/index.ts
1584
2328
  var program = new Command();
1585
- program.name("evident").description("Run OpenCode locally and connect it to Evident").version("0.1.0").option("-e, --env <environment>", "Environment to use (local, dev, production)", "production").hook("preAction", (thisCommand) => {
1586
- const env = thisCommand.opts().env;
1587
- if (env) {
1588
- setEnvironment(env);
2329
+ program.name("evident").description("Run OpenCode locally and connect it to Evident").version("0.1.0").option(
2330
+ "--endpoint <url>",
2331
+ "Evident API base URL (default: production; e.g. http://localhost:3001)"
2332
+ ).option("--tunnel <url>", "Tunnel WebSocket URL (default: production; e.g. ws://localhost:8787)").hook("preAction", (thisCommand) => {
2333
+ const { endpoint, tunnel } = thisCommand.opts();
2334
+ if (endpoint) {
2335
+ setEndpoint(endpoint);
2336
+ }
2337
+ if (tunnel) {
2338
+ setTunnelUrl(tunnel);
1589
2339
  }
1590
2340
  });
1591
2341
  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
2342
  program.command("logout").description("Remove stored credentials").action(logout);
1593
2343
  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
- });
2344
+ program.command("run").description("Connect to Evident and process messages").option("-a, --agent [id]", "Agent ID to connect to (optional when EVIDENT_AGENT_KEY is set)").option("-p, --port <port>", "OpenCode port (default: 4096)", "4096").option("-v, --verbose", "Show detailed request/response information").option("-c, --conversation <id>", "Process only this specific conversation").option("--idle-timeout <seconds>", "Exit after N seconds idle").option("--json", "Output in JSON format").action(
2345
+ (options) => {
2346
+ run({
2347
+ agent: options.agent,
2348
+ port: parseInt(options.port, 10),
2349
+ verbose: options.verbose,
2350
+ conversation: options.conversation,
2351
+ idleTimeout: options.idleTimeout ? parseInt(options.idleTimeout, 10) : void 0,
2352
+ json: options.json
2353
+ });
2354
+ }
2355
+ );
1601
2356
  program.parse();
1602
2357
  //# sourceMappingURL=index.js.map