agent-relay-server 0.4.13 → 0.4.14

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/codex/README.md DELETED
@@ -1,152 +0,0 @@
1
- # Codex Live Sidecar
2
-
3
- Codex integration for Agent Relay.
4
-
5
- ## Purpose
6
-
7
- This sidecar connects to a Codex app-server session and to Agent Relay, then delivers incoming relay messages into the active Codex thread using:
8
-
9
- - `turn/start`
10
- - `turn/steer`
11
- - `turn/interrupt`
12
-
13
- ## Current behavior
14
-
15
- - starts a new thread by default
16
- - resumes the actual launched thread when the SessionStart hook provides a thread id
17
- - only attaches to loaded/latest same-cwd threads when `CODEX_THREAD_MODE=auto`
18
- - registers a relay agent with `client: codex-live`
19
- - marks the relay agent `ready=true` once app-server + thread are attached
20
- - polls relay inbox and delivers messages into the live thread
21
- - coalesces ordinary relay bursts into one delivery turn
22
- - reconnects to the app-server with exponential backoff after disconnects
23
- - writes runtime state to `codex/runtime/live-state.json`
24
-
25
- ## Delivery behavior
26
-
27
- - idle thread: `turn/start`
28
- - active thread: `turn/steer`
29
- - urgent or `meta.delivery = "interrupt"`: `turn/interrupt` then `turn/start`
30
-
31
- ## Run
32
-
33
- ```bash
34
- codex/start-live.sh
35
- ```
36
-
37
- ## Installable workflow
38
-
39
- The packaged Codex path is:
40
-
41
- ```bash
42
- bunx agent-relay-server@latest
43
- curl -fsSL https://unpkg.com/agent-relay-server@latest/codex/install-codex.sh | bash
44
- # after restarting your shell
45
- codex-relay
46
- ```
47
-
48
- The installer always adds a `codex-relay` launcher and asks whether plain
49
- `codex` should also route through Agent Relay. `codex-relay` idempotently
50
- installs or refreshes the Codex hook/plugin, then launches `codex app-server`,
51
- starts Codex with
52
- `--remote`, lets the SessionStart hook attach a sidecar to the actual thread,
53
- and kills sidecars plus the app-server when Codex exits.
54
-
55
- Uninstall the Codex hook, local plugin marketplace files, and launcher shims:
56
-
57
- ```bash
58
- agent-relay-codex uninstall
59
- ```
60
-
61
- Remove all Agent Relay-managed Codex install state, including installer-written
62
- PATH snippets and runtime logs:
63
-
64
- ```bash
65
- agent-relay-codex uninstall --purge
66
- ```
67
-
68
- Purge only removes shell profile snippets written with the Agent Relay installer
69
- marker. Manual PATH edits are left alone.
70
-
71
- ## Approval mode
72
-
73
- Relay replies are usually sent with a shell command (`curl` to
74
- `/api/messages`), so Codex can prompt for approval in stricter modes.
75
-
76
- By default, `codex-relay` starts Codex with `--ask-for-approval never --sandbox
77
- workspace-write` so relay turns do not get stuck on approval prompts while
78
- still keeping Codex inside workspace boundaries. If you pass an explicit Codex
79
- runtime mode, `codex-relay`
80
- leaves it alone and forwards it to the sidecar, including `--ask-for-approval`,
81
- `--sandbox`, `--full-auto`, and `--yolo`.
82
-
83
- Default:
84
-
85
- ```bash
86
- codex-relay
87
- ```
88
-
89
- Example: no prompt loop, still workspace sandboxing:
90
-
91
- ```bash
92
- codex-relay -- --ask-for-approval never --sandbox workspace-write
93
- ```
94
-
95
- Trusted private rig only:
96
-
97
- ```bash
98
- codex-relay -- --dangerously-bypass-approvals-and-sandbox
99
- ```
100
-
101
- Thread attachment defaults are conservative. The SessionStart hook resumes the
102
- actual launched Codex thread when Codex provides a thread id. If the launcher has
103
- to use the fallback sidecar because the hook does not report in time, it starts a
104
- new thread by default instead of attaching to whichever loaded thread happens to
105
- match the cwd. Set `AGENT_RELAY_CODEX_FALLBACK_THREAD_MODE=auto` or run
106
- `codex-relay --thread-mode auto` if you explicitly want cwd-based attachment.
107
-
108
- If you prefer prompts for everything else but want relay sends auto-approved,
109
- add a rule in `~/.codex/rules/default.rules` (adjust URL when using a remote relay):
110
-
111
- ```python
112
- prefix_rule(
113
- pattern = ["curl", "-sS", "-X", "POST", "http://127.0.0.1:4850/api/messages"],
114
- decision = "allow",
115
- justification = "Allow local Agent Relay message posts",
116
- )
117
- ```
118
-
119
- For local development from this repo:
120
-
121
- ```bash
122
- bun run bin/agent-relay-codex.ts
123
- bun run codex:smoke:fallback
124
- ```
125
-
126
- Useful environment variables:
127
-
128
- - `AGENT_RELAY_URL`
129
- - `AGENT_RELAY_TOKEN`
130
- - `AGENT_RELAY_CAPS`
131
- - `AGENT_RELAY_CODEX_HOOK_TIMEOUT_MS` (launcher wait for SessionStart handshake, default `5000`)
132
- - `CODEX_APP_SERVER_URL`
133
- - `CODEX_THREAD_ID`
134
- - `CODEX_THREAD_MODE=auto|resume|start`
135
- - `CODEX_LIVE_STATE_PATH`
136
- - `CODEX_LIVE_COALESCE_WINDOW_MS`
137
- - `CODEX_LIVE_RECONNECT_INITIAL_MS`
138
- - `CODEX_LIVE_RECONNECT_MAX_MS`
139
- - `CODEX_LIVE_RIG`
140
- - `CODEX_MODEL`
141
-
142
- Fallback reason codes are written to `runtime/<run>/launcher.log` when the
143
- launcher has to start a sidecar because the SessionStart hook did not confirm
144
- startup in time.
145
-
146
- ## Notes
147
-
148
- Current sidecar behavior is stable for live delivery. Remaining gaps are advanced policies such as batching by sender, message prioritization queues, and more nuanced retry/backoff behavior.
149
-
150
- - `CODEX_THREAD_MODE=start` is the safe default: the sidecar creates its own thread unless the hook supplied an explicit thread id.
151
- - `CODEX_THREAD_MODE=auto` will attach to an already loaded thread for the same `cwd`. That can be useful for advanced live control, but it also means relay messages can enter your current interactive Codex session if one is already open.
152
- - A brand-new thread is not materialized for `includeTurns` reads until the first turn starts. That is an app-server behavior, not a relay bug.
@@ -1,239 +0,0 @@
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
- }
@@ -1,39 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { pickThreadId } from "./session-start-lib.ts";
3
-
4
- describe("pickThreadId", () => {
5
- it("prefers explicit thread ids over session ids", () => {
6
- const threadId = pickThreadId({
7
- session_id: "session-123",
8
- thread_id: "thread-456",
9
- });
10
-
11
- expect(threadId).toBe("thread-456");
12
- });
13
-
14
- it("uses nested thread id when present", () => {
15
- const threadId = pickThreadId({
16
- session: { id: "session-123" },
17
- thread: { id: "thread-789" },
18
- });
19
-
20
- expect(threadId).toBe("thread-789");
21
- });
22
-
23
- it("falls back to session id when thread id is missing", () => {
24
- const threadId = pickThreadId({
25
- sessionId: "session-abc",
26
- });
27
-
28
- expect(threadId).toBe("session-abc");
29
- });
30
-
31
- it("ignores blank candidates", () => {
32
- const threadId = pickThreadId({
33
- threadId: " ",
34
- session_id: " session-trimmed ",
35
- });
36
-
37
- expect(threadId).toBe("session-trimmed");
38
- });
39
- });
@@ -1,25 +0,0 @@
1
- export type HookInput = {
2
- session_id?: string;
3
- sessionId?: string;
4
- thread_id?: string;
5
- threadId?: string;
6
- session?: { id?: string };
7
- thread?: { id?: string };
8
- cwd?: string;
9
- model?: string;
10
- };
11
-
12
- export function pickThreadId(input: HookInput): string {
13
- const candidates = [
14
- input.thread_id,
15
- input.threadId,
16
- input.thread?.id,
17
- input.session_id,
18
- input.sessionId,
19
- input.session?.id,
20
- ];
21
- for (const value of candidates) {
22
- if (typeof value === "string" && value.trim()) return value.trim();
23
- }
24
- return "";
25
- }
@@ -1,176 +0,0 @@
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
- import { pickThreadId, type HookInput } from "./session-start-lib.ts";
7
-
8
- type HookHandshake = {
9
- status: "ok" | "error";
10
- code: string;
11
- message: string;
12
- pid?: number;
13
- threadId?: string;
14
- timestamp: string;
15
- };
16
-
17
- function readStdin(): string {
18
- return readFileSync(0, "utf8");
19
- }
20
-
21
- function sanitize(value: string): string {
22
- return value.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 120) || "session";
23
- }
24
-
25
- function isAlive(pid: number): boolean {
26
- try {
27
- process.kill(pid, 0);
28
- return true;
29
- } catch {
30
- return false;
31
- }
32
- }
33
-
34
- function outputContext(context: string): never {
35
- console.log(
36
- JSON.stringify({
37
- continue: true,
38
- hookSpecificOutput: {
39
- hookEventName: "SessionStart",
40
- additionalContext: context,
41
- },
42
- }),
43
- );
44
- process.exit(0);
45
- }
46
-
47
- function handshakePath(runtimeDir: string): string {
48
- return join(runtimeDir, "session-start-handshake.json");
49
- }
50
-
51
- function writeHandshake(runtimeDir: string, payload: HookHandshake): void {
52
- writeFileSync(handshakePath(runtimeDir), `${JSON.stringify(payload, null, 2)}\n`);
53
- }
54
-
55
- function existingAlivePid(pidPath: string): number | null {
56
- if (!existsSync(pidPath)) return null;
57
- const existingPid = Number(readFileSync(pidPath, "utf8").trim());
58
- if (!Number.isFinite(existingPid) || !isAlive(existingPid)) return null;
59
- return existingPid;
60
- }
61
-
62
- const input = JSON.parse(readStdin() || "{}") as HookInput;
63
- const packageRoot =
64
- process.env.AGENT_RELAY_CODEX_PACKAGE_ROOT || resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
65
- const appServerUrl = process.env.CODEX_APP_SERVER_URL;
66
- const runId = process.env.AGENT_RELAY_CODEX_RUN_ID;
67
- const cwd = input.cwd || process.cwd();
68
- const threadId = pickThreadId(input);
69
- const relayUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
70
- const rig = process.env.CODEX_LIVE_RIG || "codex-live";
71
-
72
- if (!appServerUrl || !runId) {
73
- outputContext(
74
- "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.",
75
- );
76
- }
77
-
78
- const runtimeDir = process.env.AGENT_RELAY_CODEX_RUNTIME_DIR || join(process.env.HOME || ".", ".agent-relay", "codex", "runtime", runId);
79
- const sessionKey = sanitize(threadId || "auto");
80
- const sessionDir = join(runtimeDir, sessionKey);
81
- const pidPath = join(sessionDir, "sidecar.pid");
82
- const statePath = join(sessionDir, "live-state.json");
83
- const logPath = join(sessionDir, "sidecar.log");
84
- mkdirSync(sessionDir, { recursive: true });
85
- mkdirSync(runtimeDir, { recursive: true });
86
-
87
- const autoPidPath = join(runtimeDir, "auto", "sidecar.pid");
88
- const activePid = existingAlivePid(pidPath) ?? (threadId ? existingAlivePid(autoPidPath) : null);
89
- if (activePid !== null) {
90
- writeHandshake(runtimeDir, {
91
- status: "ok",
92
- code: "HOOK_SIDECAR_REUSED",
93
- message: `using existing sidecar pid ${activePid}`,
94
- pid: activePid,
95
- threadId: threadId || undefined,
96
- timestamp: new Date().toISOString(),
97
- });
98
- if (threadId) {
99
- const identity = buildAgentIdentity({
100
- relayUrl,
101
- cwd,
102
- rig,
103
- capabilities: (process.env.AGENT_RELAY_CAPS || "chat").split(",").map((value) => value.trim()).filter(Boolean),
104
- tags: ["codex", rig, cwd.split("/").filter(Boolean).at(-1) || "unknown"],
105
- threadId,
106
- model: input.model,
107
- appServerUrl,
108
- });
109
- outputContext(`Agent Relay active for this Codex session. Agent ID: ${identity.id}. Relay URL: ${relayUrl}.`);
110
- }
111
- outputContext(`Agent Relay sidecar already running (pid ${activePid}). Relay URL: ${relayUrl}.`);
112
- }
113
-
114
- const spawnEnv: Record<string, string | undefined> = {
115
- ...process.env,
116
- AGENT_RELAY_URL: relayUrl,
117
- CODEX_APP_SERVER_URL: appServerUrl,
118
- CODEX_THREAD_MODE: threadId ? "resume" : process.env.CODEX_THREAD_MODE || "start",
119
- CODEX_LIVE_CWD: cwd,
120
- CODEX_LIVE_STATE_PATH: statePath,
121
- CODEX_MODEL: input.model || process.env.CODEX_MODEL || "",
122
- };
123
- if (threadId) {
124
- spawnEnv.CODEX_THREAD_ID = threadId;
125
- } else {
126
- delete spawnEnv.CODEX_THREAD_ID;
127
- }
128
-
129
- const logFile = Bun.file(logPath);
130
- let sidecarPid = 0;
131
- try {
132
- const sidecar = Bun.spawn(["bun", "run", join(packageRoot, "codex", "live-sidecar.ts")], {
133
- env: spawnEnv,
134
- stdout: logFile,
135
- stderr: logFile,
136
- });
137
- sidecar.unref();
138
- sidecarPid = sidecar.pid;
139
-
140
- writeFileSync(pidPath, String(sidecarPid));
141
- appendFileSync(join(runtimeDir, "sidecar-pids.txt"), `${sidecarPid}\n`);
142
- writeHandshake(runtimeDir, {
143
- status: "ok",
144
- code: "HOOK_SIDECAR_STARTED",
145
- message: `spawned sidecar pid ${sidecarPid}`,
146
- pid: sidecarPid,
147
- threadId: threadId || undefined,
148
- timestamp: new Date().toISOString(),
149
- });
150
- } catch (error) {
151
- writeHandshake(runtimeDir, {
152
- status: "error",
153
- code: "HOOK_SIDECAR_SPAWN_FAILED",
154
- message: error instanceof Error ? error.message : String(error),
155
- threadId: threadId || undefined,
156
- timestamp: new Date().toISOString(),
157
- });
158
- throw error;
159
- }
160
-
161
- if (!threadId) {
162
- outputContext(`Agent Relay sidecar started in auto-thread mode. Relay URL: ${relayUrl}. Incoming messages will arrive as live user turns once the sidecar resolves the active thread.`);
163
- }
164
-
165
- const identity = buildAgentIdentity({
166
- relayUrl,
167
- cwd,
168
- rig,
169
- capabilities: (process.env.AGENT_RELAY_CAPS || "chat").split(",").map((value) => value.trim()).filter(Boolean),
170
- tags: ["codex", rig, cwd.split("/").filter(Boolean).at(-1) || "unknown"],
171
- threadId,
172
- model: input.model,
173
- appServerUrl,
174
- });
175
-
176
- 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. If AGENT_RELAY_TOKEN is set, include it as the X-Agent-Relay-Token header. Message etiquette: acknowledge incoming agent messages briefly unless they are obvious noise. Anti-loop rule: do not auto-reply to pure acknowledgements/thanks/received messages; acknowledge once, then follow up only when there is new work, a decision, or a deliverable.`);
@@ -1,47 +0,0 @@
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@latest" }
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