agent-relay-server 0.4.13 → 0.4.15
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 +83 -390
- package/package.json +4 -16
- package/bin/agent-relay-codex.ts +0 -978
- package/codex/README.md +0 -152
- package/codex/app-client.ts +0 -239
- package/codex/hooks/session-start-lib.test.ts +0 -39
- package/codex/hooks/session-start-lib.ts +0 -25
- package/codex/hooks/session-start.ts +0 -176
- package/codex/install-codex.ps1 +0 -47
- package/codex/install-codex.sh +0 -75
- package/codex/live-sidecar.test.ts +0 -20
- package/codex/live-sidecar.ts +0 -626
- package/codex/plugin/.codex-plugin/plugin.json +0 -40
- package/codex/plugin/skills/agent-relay/SKILL.md +0 -29
- package/codex/relay.ts +0 -125
- package/codex/smoke/fallback.ts +0 -128
- package/codex/start-live.sh +0 -64
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.
|
package/codex/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
|
-
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.`);
|
package/codex/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_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
|