agent-relay-server 0.1.0 → 0.3.0
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/README.md +177 -91
- package/bin/agent-relay-codex.ts +547 -0
- package/codex/README.md +80 -0
- package/codex/app-client.ts +239 -0
- package/codex/hooks/session-start.ts +114 -0
- package/codex/install-codex.ps1 +47 -0
- package/codex/install-codex.sh +75 -0
- package/codex/live-sidecar.ts +606 -0
- package/codex/plugin/.codex-plugin/plugin.json +25 -0
- package/codex/plugin/skills/agent-relay/SKILL.md +28 -0
- package/codex/relay.ts +116 -0
- package/codex/start-live.sh +64 -0
- package/package.json +14 -3
- package/public/index.html +1078 -446
- package/src/config.ts +8 -0
- package/src/db.ts +49 -20
- package/src/index.ts +5 -1
- package/src/routes.ts +83 -15
- package/src/sse.ts +115 -0
- package/src/types.ts +6 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
|
|
3
|
+
type JsonRpcId = number | string;
|
|
4
|
+
|
|
5
|
+
type JsonRpcRequest = {
|
|
6
|
+
id: JsonRpcId;
|
|
7
|
+
method: string;
|
|
8
|
+
params?: unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type JsonRpcResponse = {
|
|
12
|
+
id: JsonRpcId;
|
|
13
|
+
result?: unknown;
|
|
14
|
+
error?: { code: number; message: string; data?: unknown };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type JsonRpcNotification = {
|
|
18
|
+
method: string;
|
|
19
|
+
params?: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ThreadStatus =
|
|
23
|
+
| { type: "notLoaded" }
|
|
24
|
+
| { type: "idle" }
|
|
25
|
+
| { type: "systemError" }
|
|
26
|
+
| { type: "active"; activeFlags: string[] };
|
|
27
|
+
|
|
28
|
+
export type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress";
|
|
29
|
+
|
|
30
|
+
export interface Turn {
|
|
31
|
+
id: string;
|
|
32
|
+
status: TurnStatus;
|
|
33
|
+
startedAt: number | null;
|
|
34
|
+
completedAt: number | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Thread {
|
|
38
|
+
id: string;
|
|
39
|
+
cwd: string;
|
|
40
|
+
status: ThreadStatus;
|
|
41
|
+
updatedAt: number;
|
|
42
|
+
preview: string;
|
|
43
|
+
turns?: Turn[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ClientEvent =
|
|
47
|
+
| { type: "notification"; message: JsonRpcNotification }
|
|
48
|
+
| { type: "server-request"; message: JsonRpcRequest }
|
|
49
|
+
| { type: "response"; message: JsonRpcResponse };
|
|
50
|
+
|
|
51
|
+
export interface TurnStartResponse {
|
|
52
|
+
turn: Turn;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ThreadStartResponse {
|
|
56
|
+
thread: Thread;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ThreadResumeResponse {
|
|
60
|
+
thread: Thread;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ThreadReadResponse {
|
|
64
|
+
thread: Thread;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ThreadListResponse {
|
|
68
|
+
data: Thread[];
|
|
69
|
+
nextCursor: string | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ThreadLoadedListResponse {
|
|
73
|
+
data: string[];
|
|
74
|
+
nextCursor: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class CodexAppClient {
|
|
78
|
+
private ws!: WebSocket;
|
|
79
|
+
private nextId = 1;
|
|
80
|
+
private pending = new Map<JsonRpcId, { resolve: (value: any) => void; reject: (err: unknown) => void }>();
|
|
81
|
+
private events: ClientEvent[] = [];
|
|
82
|
+
private listeners = new Set<(event: ClientEvent) => void>();
|
|
83
|
+
private connected = false;
|
|
84
|
+
private connectionListeners = new Set<(connected: boolean) => void>();
|
|
85
|
+
|
|
86
|
+
constructor(private readonly url: string, private readonly log: (msg: string) => void = () => {}) {}
|
|
87
|
+
|
|
88
|
+
async connect(): Promise<void> {
|
|
89
|
+
if (this.connected) return;
|
|
90
|
+
await new Promise<void>((resolve, reject) => {
|
|
91
|
+
const ws = new WebSocket(this.url);
|
|
92
|
+
this.ws = ws;
|
|
93
|
+
|
|
94
|
+
ws.onopen = () => {
|
|
95
|
+
this.connected = true;
|
|
96
|
+
this.emitConnection(true);
|
|
97
|
+
resolve();
|
|
98
|
+
};
|
|
99
|
+
ws.onerror = (event) => reject(new Error(`websocket error: ${String((event as ErrorEvent).message || "unknown")}`));
|
|
100
|
+
ws.onclose = (event) => {
|
|
101
|
+
this.connected = false;
|
|
102
|
+
this.emitConnection(false);
|
|
103
|
+
const err = new Error(`websocket closed code=${event.code} reason=${event.reason || "(none)"}`);
|
|
104
|
+
for (const pending of this.pending.values()) pending.reject(err);
|
|
105
|
+
this.pending.clear();
|
|
106
|
+
};
|
|
107
|
+
ws.onmessage = (event) => this.handleMessage(String(event.data));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
close(): void {
|
|
112
|
+
if (!this.ws) return;
|
|
113
|
+
this.ws.close();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
isConnected(): boolean {
|
|
117
|
+
return this.connected;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async initialize(): Promise<unknown> {
|
|
121
|
+
return this.request("initialize", {
|
|
122
|
+
clientInfo: {
|
|
123
|
+
name: "agent-relay-codex-live",
|
|
124
|
+
title: "Agent Relay Codex Live",
|
|
125
|
+
version: "0.1.0",
|
|
126
|
+
},
|
|
127
|
+
capabilities: {
|
|
128
|
+
experimentalApi: true,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
onEvent(listener: (event: ClientEvent) => void): () => void {
|
|
134
|
+
this.listeners.add(listener);
|
|
135
|
+
return () => this.listeners.delete(listener);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
onConnectionChange(listener: (connected: boolean) => void): () => void {
|
|
139
|
+
this.connectionListeners.add(listener);
|
|
140
|
+
return () => this.connectionListeners.delete(listener);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getEvents(): ClientEvent[] {
|
|
144
|
+
return [...this.events];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async settle(ms = 150): Promise<void> {
|
|
148
|
+
await delay(ms);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async threadStart(params: Record<string, unknown>): Promise<ThreadStartResponse> {
|
|
152
|
+
return this.request<ThreadStartResponse>("thread/start", params);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async threadResume(params: Record<string, unknown>): Promise<ThreadResumeResponse> {
|
|
156
|
+
return this.request<ThreadResumeResponse>("thread/resume", params);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async threadRead(threadId: string, includeTurns = false): Promise<ThreadReadResponse> {
|
|
160
|
+
return this.request<ThreadReadResponse>("thread/read", { threadId, includeTurns });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async threadList(params: Record<string, unknown>): Promise<ThreadListResponse> {
|
|
164
|
+
return this.request<ThreadListResponse>("thread/list", params);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async threadLoadedList(limit = 20): Promise<ThreadLoadedListResponse> {
|
|
168
|
+
return this.request<ThreadLoadedListResponse>("thread/loaded/list", { limit });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async turnStart(threadId: string, text: string): Promise<TurnStartResponse> {
|
|
172
|
+
return this.request<TurnStartResponse>("turn/start", {
|
|
173
|
+
threadId,
|
|
174
|
+
input: [{ type: "text", text }],
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async turnSteer(threadId: string, turnId: string, text: string): Promise<{ turnId: string }> {
|
|
179
|
+
return this.request<{ turnId: string }>("turn/steer", {
|
|
180
|
+
threadId,
|
|
181
|
+
expectedTurnId: turnId,
|
|
182
|
+
input: [{ type: "text", text }],
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async turnInterrupt(threadId: string, turnId: string): Promise<Record<string, never>> {
|
|
187
|
+
return this.request<Record<string, never>>("turn/interrupt", { threadId, turnId });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private async request<T = unknown>(method: string, params?: unknown): Promise<T> {
|
|
191
|
+
if (!this.connected) {
|
|
192
|
+
throw new Error("websocket not connected");
|
|
193
|
+
}
|
|
194
|
+
const id = this.nextId++;
|
|
195
|
+
const payload: JsonRpcRequest = { id, method, params };
|
|
196
|
+
const promise = new Promise<T>((resolve, reject) => {
|
|
197
|
+
this.pending.set(id, { resolve, reject });
|
|
198
|
+
});
|
|
199
|
+
this.ws.send(JSON.stringify(payload));
|
|
200
|
+
return promise;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private handleMessage(raw: string): void {
|
|
204
|
+
const parsed = JSON.parse(raw) as JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
|
|
205
|
+
|
|
206
|
+
if ("id" in parsed && ("result" in parsed || "error" in parsed)) {
|
|
207
|
+
const pending = this.pending.get(parsed.id);
|
|
208
|
+
if (pending) {
|
|
209
|
+
this.pending.delete(parsed.id);
|
|
210
|
+
if (parsed.error) {
|
|
211
|
+
pending.reject(new Error(`${parsed.error.message} (${parsed.error.code})`));
|
|
212
|
+
} else {
|
|
213
|
+
pending.resolve(parsed.result);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this.record({ type: "response", message: parsed });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if ("id" in parsed && "method" in parsed) {
|
|
221
|
+
this.log(`server-request ${parsed.method}`);
|
|
222
|
+
this.record({ type: "server-request", message: parsed });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if ("method" in parsed) {
|
|
227
|
+
this.record({ type: "notification", message: parsed });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private record(event: ClientEvent): void {
|
|
232
|
+
this.events.push(event);
|
|
233
|
+
for (const listener of this.listeners) listener(event);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private emitConnection(connected: boolean): void {
|
|
237
|
+
for (const listener of this.connectionListeners) listener(connected);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { buildAgentIdentity } from "../relay.ts";
|
|
6
|
+
|
|
7
|
+
type HookInput = {
|
|
8
|
+
session_id?: string;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function readStdin(): string {
|
|
14
|
+
return readFileSync(0, "utf8");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sanitize(value: string): string {
|
|
18
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 120) || "session";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isAlive(pid: number): boolean {
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function outputContext(context: string): never {
|
|
31
|
+
console.log(
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
continue: true,
|
|
34
|
+
hookSpecificOutput: {
|
|
35
|
+
hookEventName: "SessionStart",
|
|
36
|
+
additionalContext: context,
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const input = JSON.parse(readStdin() || "{}") as HookInput;
|
|
44
|
+
const packageRoot =
|
|
45
|
+
process.env.AGENT_RELAY_CODEX_PACKAGE_ROOT || resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
46
|
+
const appServerUrl = process.env.CODEX_APP_SERVER_URL;
|
|
47
|
+
const runId = process.env.AGENT_RELAY_CODEX_RUN_ID;
|
|
48
|
+
const cwd = input.cwd || process.cwd();
|
|
49
|
+
const threadId = input.session_id || "";
|
|
50
|
+
const relayUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
|
|
51
|
+
const rig = process.env.CODEX_LIVE_RIG || "codex-live";
|
|
52
|
+
|
|
53
|
+
if (!appServerUrl || !runId || !threadId) {
|
|
54
|
+
outputContext(
|
|
55
|
+
"Agent Relay for Codex is installed. For live incoming relay messages, start Codex with `agent-relay-codex start` so a managed app-server and sidecar can attach to this session.",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const runtimeDir = process.env.AGENT_RELAY_CODEX_RUNTIME_DIR || join(process.env.HOME || ".", ".agent-relay", "codex", "runtime", runId);
|
|
60
|
+
const sessionDir = join(runtimeDir, sanitize(threadId));
|
|
61
|
+
const pidPath = join(sessionDir, "sidecar.pid");
|
|
62
|
+
const statePath = join(sessionDir, "live-state.json");
|
|
63
|
+
const logPath = join(sessionDir, "sidecar.log");
|
|
64
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
65
|
+
|
|
66
|
+
if (existsSync(pidPath)) {
|
|
67
|
+
const existingPid = Number(readFileSync(pidPath, "utf8").trim());
|
|
68
|
+
if (Number.isFinite(existingPid) && isAlive(existingPid)) {
|
|
69
|
+
const identity = buildAgentIdentity({
|
|
70
|
+
relayUrl,
|
|
71
|
+
cwd,
|
|
72
|
+
rig,
|
|
73
|
+
capabilities: (process.env.AGENT_RELAY_CAPS || "chat").split(",").map((value) => value.trim()).filter(Boolean),
|
|
74
|
+
tags: ["codex", rig, cwd.split("/").filter(Boolean).at(-1) || "unknown"],
|
|
75
|
+
threadId,
|
|
76
|
+
model: input.model,
|
|
77
|
+
appServerUrl,
|
|
78
|
+
});
|
|
79
|
+
outputContext(`Agent Relay active for this Codex session. Agent ID: ${identity.id}. Relay URL: ${relayUrl}.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const logFile = Bun.file(logPath);
|
|
84
|
+
const sidecar = Bun.spawn(["bun", "run", join(packageRoot, "codex", "live-sidecar.ts")], {
|
|
85
|
+
env: {
|
|
86
|
+
...process.env,
|
|
87
|
+
AGENT_RELAY_URL: relayUrl,
|
|
88
|
+
CODEX_APP_SERVER_URL: appServerUrl,
|
|
89
|
+
CODEX_THREAD_ID: threadId,
|
|
90
|
+
CODEX_THREAD_MODE: "resume",
|
|
91
|
+
CODEX_LIVE_CWD: cwd,
|
|
92
|
+
CODEX_LIVE_STATE_PATH: statePath,
|
|
93
|
+
CODEX_MODEL: input.model || process.env.CODEX_MODEL || "",
|
|
94
|
+
},
|
|
95
|
+
stdout: logFile,
|
|
96
|
+
stderr: logFile,
|
|
97
|
+
});
|
|
98
|
+
sidecar.unref();
|
|
99
|
+
|
|
100
|
+
writeFileSync(pidPath, String(sidecar.pid));
|
|
101
|
+
appendFileSync(join(runtimeDir, "sidecar-pids.txt"), `${sidecar.pid}\n`);
|
|
102
|
+
|
|
103
|
+
const identity = buildAgentIdentity({
|
|
104
|
+
relayUrl,
|
|
105
|
+
cwd,
|
|
106
|
+
rig,
|
|
107
|
+
capabilities: (process.env.AGENT_RELAY_CAPS || "chat").split(",").map((value) => value.trim()).filter(Boolean),
|
|
108
|
+
tags: ["codex", rig, cwd.split("/").filter(Boolean).at(-1) || "unknown"],
|
|
109
|
+
threadId,
|
|
110
|
+
model: input.model,
|
|
111
|
+
appServerUrl,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
outputContext(`Agent Relay active. Agent ID: ${identity.id}. Relay URL: ${relayUrl}. Incoming messages will arrive as live user turns. To reply or send a message, POST JSON to ${relayUrl}/api/messages with from="${identity.id}", to, subject, and body.`);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[switch]$Alias,
|
|
3
|
+
[switch]$NoAlias
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = "Stop"
|
|
7
|
+
|
|
8
|
+
$Package = if ($env:AGENT_RELAY_PACKAGE) { $env:AGENT_RELAY_PACKAGE } else { "agent-relay-server" }
|
|
9
|
+
$InstallArgs = New-Object System.Collections.Generic.List[string]
|
|
10
|
+
$InstallArgs.Add("install")
|
|
11
|
+
|
|
12
|
+
if (-not (Get-Command bun -ErrorAction SilentlyContinue)) {
|
|
13
|
+
Write-Error @"
|
|
14
|
+
Bun is required to install Agent Relay for Codex.
|
|
15
|
+
|
|
16
|
+
Install Bun first:
|
|
17
|
+
powershell -c "irm bun.sh/install.ps1 | iex"
|
|
18
|
+
|
|
19
|
+
Then rerun this installer.
|
|
20
|
+
"@
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (-not (Get-Command codex -ErrorAction SilentlyContinue)) {
|
|
24
|
+
Write-Error "Codex CLI is required before installing Agent Relay for Codex. Install and log in to Codex first, then rerun this installer."
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if ($Alias -and $NoAlias) {
|
|
28
|
+
Write-Error "Use only one of -Alias or -NoAlias."
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if ($Alias) {
|
|
32
|
+
$InstallArgs.Add("--alias")
|
|
33
|
+
} elseif ($NoAlias) {
|
|
34
|
+
$InstallArgs.Add("--no-alias")
|
|
35
|
+
} elseif ($env:AGENT_RELAY_CODEX_ALIAS -eq "1" -or $env:AGENT_RELAY_CODEX_ALIAS -eq "true") {
|
|
36
|
+
$InstallArgs.Add("--alias")
|
|
37
|
+
} else {
|
|
38
|
+
$answer = Read-Host "Make plain 'codex' start with Agent Relay? You can still use 'codex-relay' either way. [y/N]"
|
|
39
|
+
if ($answer -match "^(y|yes)$") {
|
|
40
|
+
$InstallArgs.Add("--alias")
|
|
41
|
+
} else {
|
|
42
|
+
$InstallArgs.Add("--no-alias")
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
& bunx -p $Package agent-relay-codex @InstallArgs
|
|
47
|
+
exit $LASTEXITCODE
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
PACKAGE="${AGENT_RELAY_PACKAGE:-agent-relay-server}"
|
|
5
|
+
INSTALL_ARGS=(install)
|
|
6
|
+
|
|
7
|
+
usage() {
|
|
8
|
+
cat <<'EOF'
|
|
9
|
+
Install Agent Relay for Codex.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
curl -fsSL https://raw.githubusercontent.com/edimuj/agent-relay/main/codex/install-codex.sh | bash
|
|
13
|
+
curl -fsSL https://raw.githubusercontent.com/edimuj/agent-relay/main/codex/install-codex.sh | bash -s -- --alias
|
|
14
|
+
curl -fsSL https://raw.githubusercontent.com/edimuj/agent-relay/main/codex/install-codex.sh | bash -s -- --no-alias
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--alias Install a PATH shim so plain `codex` starts with Agent Relay.
|
|
18
|
+
--no-alias Keep plain `codex` unchanged. You can still use `codex-relay`.
|
|
19
|
+
EOF
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for arg in "$@"; do
|
|
23
|
+
case "$arg" in
|
|
24
|
+
--alias|--no-alias)
|
|
25
|
+
INSTALL_ARGS+=("$arg")
|
|
26
|
+
;;
|
|
27
|
+
-h|--help)
|
|
28
|
+
usage
|
|
29
|
+
exit 0
|
|
30
|
+
;;
|
|
31
|
+
*)
|
|
32
|
+
echo "Unknown option: $arg" >&2
|
|
33
|
+
usage >&2
|
|
34
|
+
exit 2
|
|
35
|
+
;;
|
|
36
|
+
esac
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
40
|
+
cat >&2 <<'EOF'
|
|
41
|
+
Error: Bun is required to install Agent Relay for Codex.
|
|
42
|
+
|
|
43
|
+
Install Bun first:
|
|
44
|
+
curl -fsSL https://bun.sh/install | bash
|
|
45
|
+
|
|
46
|
+
Then rerun this installer.
|
|
47
|
+
EOF
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if ! command -v codex >/dev/null 2>&1; then
|
|
52
|
+
cat >&2 <<'EOF'
|
|
53
|
+
Error: Codex CLI is required before installing Agent Relay for Codex.
|
|
54
|
+
|
|
55
|
+
Install and log in to Codex first, then rerun this installer.
|
|
56
|
+
EOF
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [[ " ${INSTALL_ARGS[*]} " != *" --alias "* && " ${INSTALL_ARGS[*]} " != *" --no-alias "* ]]; then
|
|
61
|
+
if [[ "${AGENT_RELAY_CODEX_ALIAS:-}" == "1" || "${AGENT_RELAY_CODEX_ALIAS:-}" == "true" ]]; then
|
|
62
|
+
INSTALL_ARGS+=(--alias)
|
|
63
|
+
elif [[ -r /dev/tty && -w /dev/tty ]]; then
|
|
64
|
+
printf 'Make plain `codex` start with Agent Relay? You can still use `codex-relay` either way. [y/N] ' >/dev/tty
|
|
65
|
+
read -r answer </dev/tty || answer=""
|
|
66
|
+
case "${answer,,}" in
|
|
67
|
+
y|yes) INSTALL_ARGS+=(--alias) ;;
|
|
68
|
+
*) INSTALL_ARGS+=(--no-alias) ;;
|
|
69
|
+
esac
|
|
70
|
+
else
|
|
71
|
+
INSTALL_ARGS+=(--no-alias)
|
|
72
|
+
fi
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
exec bunx -p "$PACKAGE" agent-relay-codex "${INSTALL_ARGS[@]}"
|