agent-relay-codex 0.6.1 → 0.10.1

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/package.json CHANGED
@@ -1,16 +1,15 @@
1
1
  {
2
2
  "name": "agent-relay-codex",
3
- "version": "0.6.1",
3
+ "version": "0.10.1",
4
4
  "description": "Codex integration for Agent Relay — auto-registers sessions as agents and enables inter-agent messaging",
5
5
  "type": "module",
6
6
  "bin": {
7
- "agent-relay-codex": "bin/agent-relay-codex.ts",
8
- "codex-relay": "bin/agent-relay-codex.ts"
7
+ "agent-relay-codex": "bin/agent-relay-codex.ts"
9
8
  },
10
9
  "files": [
11
10
  "bin/**/*.ts",
11
+ "plugin/.codex-plugin/plugin.json",
12
12
  "hooks/**/*.ts",
13
- "plugin/**",
14
13
  "*.ts",
15
14
  "*.sh",
16
15
  "*.ps1",
@@ -44,5 +43,8 @@
44
43
  ],
45
44
  "engines": {
46
45
  "bun": ">=1.0.0"
46
+ },
47
+ "dependencies": {
48
+ "agent-relay-sdk": "0.2.0"
47
49
  }
48
50
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay",
3
- "version": "0.6.1",
3
+ "version": "0.10.1",
4
4
  "description": "Agent Relay integration for Codex sessions",
5
5
  "author": {
6
6
  "name": "Edin Mujkanovic"
package/app-client.ts DELETED
@@ -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
- type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress";
29
-
30
- 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
- interface TurnStartResponse {
52
- turn: Turn;
53
- }
54
-
55
- interface ThreadStartResponse {
56
- thread: Thread;
57
- }
58
-
59
- interface ThreadResumeResponse {
60
- thread: Thread;
61
- }
62
-
63
- interface ThreadReadResponse {
64
- thread: Thread;
65
- }
66
-
67
- interface ThreadListResponse {
68
- data: Thread[];
69
- nextCursor: string | null;
70
- }
71
-
72
- 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
- }
package/approval.ts DELETED
@@ -1,29 +0,0 @@
1
- export type ApprovalMode = "open" | "guarded" | "read-only";
2
-
3
- export type SessionPermissions = {
4
- approvalPolicy?: string;
5
- sandbox?: string;
6
- };
7
-
8
- export function parseApprovalMode(raw: string | undefined): ApprovalMode {
9
- if (raw === "guarded" || raw === "read-only" || raw === "open") return raw;
10
- return "open";
11
- }
12
-
13
- export function codexArgsForApprovalMode(mode: ApprovalMode): string[] {
14
- if (mode === "read-only") return ["--ask-for-approval", "never", "--sandbox", "read-only"];
15
- if (mode === "guarded") return ["--ask-for-approval", "on-request", "--sandbox", "workspace-write"];
16
- return ["--ask-for-approval", "never", "--sandbox", "danger-full-access"];
17
- }
18
-
19
- export function approvalModeFromPermissions(permissions: SessionPermissions): ApprovalMode {
20
- if (permissions.sandbox === "read-only") return "read-only";
21
- if (permissions.sandbox === "workspace-write") return "guarded";
22
- return "open";
23
- }
24
-
25
- export function describeApprovalMode(mode: ApprovalMode): string {
26
- if (mode === "read-only") return "observe, analyze, and report only; no file writes or mutation";
27
- if (mode === "guarded") return "workspace sandboxing with approval prompts for operations Codex considers risky";
28
- return "no Agent Relay restrictions; Codex runs without sandbox restrictions";
29
- }
@@ -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,169 +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 { parseApprovalMode } from "../approval.ts";
6
- import { loadAgentRelayProfile } from "../profile.ts";
7
- import { pickThreadId, type HookInput } from "./session-start-lib.ts";
8
-
9
- type HookHandshake = {
10
- status: "ok" | "error";
11
- code: string;
12
- message: string;
13
- pid?: number;
14
- threadId?: string;
15
- timestamp: string;
16
- };
17
-
18
- function readStdin(): string {
19
- return readFileSync(0, "utf8");
20
- }
21
-
22
- function sanitize(value: string): string {
23
- return value.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 120) || "session";
24
- }
25
-
26
- function isAlive(pid: number): boolean {
27
- try {
28
- process.kill(pid, 0);
29
- return true;
30
- } catch {
31
- return false;
32
- }
33
- }
34
-
35
- function outputContext(context: string): never {
36
- console.log(
37
- JSON.stringify({
38
- continue: true,
39
- hookSpecificOutput: {
40
- hookEventName: "SessionStart",
41
- additionalContext: context,
42
- },
43
- }),
44
- );
45
- process.exit(0);
46
- }
47
-
48
- function outputContinue(): never {
49
- console.log(JSON.stringify({ continue: true }));
50
- process.exit(0);
51
- }
52
-
53
- function handshakePath(runtimeDir: string): string {
54
- return join(runtimeDir, "session-start-handshake.json");
55
- }
56
-
57
- function writeHandshake(runtimeDir: string, payload: HookHandshake): void {
58
- writeFileSync(handshakePath(runtimeDir), `${JSON.stringify(payload, null, 2)}\n`);
59
- }
60
-
61
- function existingAlivePid(pidPath: string): number | null {
62
- if (!existsSync(pidPath)) return null;
63
- const existingPid = Number(readFileSync(pidPath, "utf8").trim());
64
- if (!Number.isFinite(existingPid) || !isAlive(existingPid)) return null;
65
- return existingPid;
66
- }
67
-
68
- const input = JSON.parse(readStdin() || "{}") as HookInput;
69
- const packageRoot =
70
- process.env.AGENT_RELAY_CODEX_PACKAGE_ROOT || resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
71
- const appServerUrl = process.env.CODEX_APP_SERVER_URL;
72
- const runId = process.env.AGENT_RELAY_CODEX_RUN_ID;
73
- const cwd = input.cwd || process.cwd();
74
- const threadId = pickThreadId(input);
75
- const relayUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
76
- const rig = process.env.AGENT_RELAY_CODEX_RIG || "codex-live";
77
- const project = cwd.split("/").filter(Boolean).at(-1) || "unknown";
78
- const profile = loadAgentRelayProfile(process.env, { provider: "codex", rig, project });
79
- const approvalMode = profile.approval ?? parseApprovalMode(process.env.AGENT_RELAY_APPROVAL);
80
-
81
- if (process.env.AGENT_RELAY_DISABLED === "1") {
82
- outputContinue();
83
- }
84
-
85
- if (process.env.AGENT_RELAY_CODEX_MANAGED === "1") {
86
- outputContinue();
87
- }
88
-
89
- if (!appServerUrl || !runId) {
90
- outputContext(
91
- "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.",
92
- );
93
- }
94
-
95
- const runtimeDir = process.env.AGENT_RELAY_CODEX_RUNTIME_DIR || join(process.env.HOME || ".", ".agent-relay", "codex", "runtime", runId);
96
- const sessionKey = sanitize(threadId || "auto");
97
- const sessionDir = join(runtimeDir, sessionKey);
98
- const pidPath = join(sessionDir, "sidecar.pid");
99
- const statePath = join(sessionDir, "live-state.json");
100
- const contextPath = join(sessionDir, "agent-context.json");
101
- const logPath = join(sessionDir, "sidecar.log");
102
- mkdirSync(sessionDir, { recursive: true });
103
- mkdirSync(runtimeDir, { recursive: true });
104
-
105
- const autoPidPath = join(runtimeDir, "auto", "sidecar.pid");
106
- const activePid = existingAlivePid(pidPath) ?? (threadId ? existingAlivePid(autoPidPath) : null);
107
- if (activePid !== null) {
108
- writeHandshake(runtimeDir, {
109
- status: "ok",
110
- code: "HOOK_SIDECAR_REUSED",
111
- message: `using existing sidecar pid ${activePid}`,
112
- pid: activePid,
113
- threadId: threadId || undefined,
114
- timestamp: new Date().toISOString(),
115
- });
116
- outputContinue();
117
- }
118
-
119
- const spawnEnv: Record<string, string | undefined> = {
120
- ...process.env,
121
- AGENT_RELAY_URL: relayUrl,
122
- CODEX_APP_SERVER_URL: appServerUrl,
123
- CODEX_THREAD_MODE: threadId ? "resume" : process.env.CODEX_THREAD_MODE || "start",
124
- AGENT_RELAY_CODEX_CWD: cwd,
125
- AGENT_RELAY_CODEX_STATE_PATH: statePath,
126
- AGENT_RELAY_CONTEXT_PATH: process.env.AGENT_RELAY_CONTEXT_PATH || contextPath,
127
- CODEX_MODEL: input.model || process.env.CODEX_MODEL || "",
128
- AGENT_RELAY_APPROVAL: process.env.AGENT_RELAY_APPROVAL || profile.approval || "",
129
- AGENT_RELAY_CODEX_PARENT_PID: String(process.ppid),
130
- };
131
- if (threadId) {
132
- spawnEnv.CODEX_THREAD_ID = threadId;
133
- } else {
134
- delete spawnEnv.CODEX_THREAD_ID;
135
- }
136
-
137
- const logFile = Bun.file(logPath);
138
- let sidecarPid = 0;
139
- try {
140
- const sidecar = Bun.spawn(["bun", "run", join(packageRoot, "codex", "live-sidecar.ts")], {
141
- env: spawnEnv,
142
- stdout: logFile,
143
- stderr: logFile,
144
- });
145
- sidecar.unref();
146
- sidecarPid = sidecar.pid;
147
-
148
- writeFileSync(pidPath, String(sidecarPid));
149
- appendFileSync(join(runtimeDir, "sidecar-pids.txt"), `${sidecarPid}\n`);
150
- writeHandshake(runtimeDir, {
151
- status: "ok",
152
- code: "HOOK_SIDECAR_STARTED",
153
- message: `spawned sidecar pid ${sidecarPid}`,
154
- pid: sidecarPid,
155
- threadId: threadId || undefined,
156
- timestamp: new Date().toISOString(),
157
- });
158
- } catch (error) {
159
- writeHandshake(runtimeDir, {
160
- status: "error",
161
- code: "HOOK_SIDECAR_SPAWN_FAILED",
162
- message: error instanceof Error ? error.message : String(error),
163
- threadId: threadId || undefined,
164
- timestamp: new Date().toISOString(),
165
- });
166
- throw error;
167
- }
168
-
169
- outputContinue();
package/install-codex.ps1 DELETED
@@ -1,47 +0,0 @@
1
- param(
2
- [switch]$Alias,
3
- [switch]$NoAlias
4
- )
5
-
6
- $ErrorActionPreference = "Stop"
7
-
8
- $Package = if ($env:AGENT_RELAY_CODEX_PACKAGE) { $env:AGENT_RELAY_CODEX_PACKAGE } else { "agent-relay-codex@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
package/install-codex.sh DELETED
@@ -1,75 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- PACKAGE="${AGENT_RELAY_CODEX_PACKAGE:-agent-relay-codex@latest}"
5
- INSTALL_ARGS=(install)
6
-
7
- usage() {
8
- cat <<'EOF'
9
- Install Agent Relay for Codex.
10
-
11
- Usage:
12
- curl -fsSL https://unpkg.com/agent-relay-codex@latest/codex/install-codex.sh | bash
13
- curl -fsSL https://unpkg.com/agent-relay-codex@latest/codex/install-codex.sh | bash -s -- --alias
14
- curl -fsSL https://unpkg.com/agent-relay-codex@latest/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[@]}"