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