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