agent-relay-server 0.3.7 → 0.3.8

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.
@@ -76,6 +76,47 @@ function readJsonFile<T>(path: string, fallback: T): T {
76
76
  return JSON.parse(readFileSync(path, "utf8")) as T;
77
77
  }
78
78
 
79
+ function isAgentRelaySessionStartCommand(command: string): boolean {
80
+ return /agent-relay.*codex\/hooks\/session-start\.ts/.test(command);
81
+ }
82
+
83
+ function removeAgentRelaySessionStartToml(input: string): string {
84
+ const lines = input.split(/\r?\n/);
85
+ const blocks: Array<{ header: string | null; text: string }> = [];
86
+
87
+ for (let index = 0; index < lines.length; ) {
88
+ const line = lines[index] ?? "";
89
+ const header = /^\[\[?/.test(line) ? line : null;
90
+ const block: string[] = [];
91
+ do {
92
+ block.push(lines[index] ?? "");
93
+ index += 1;
94
+ } while (index < lines.length && !/^\[\[?/.test(lines[index] ?? ""));
95
+ blocks.push({ header, text: block.join("\n") });
96
+ }
97
+
98
+ let output = "";
99
+ for (let index = 0; index < blocks.length; ) {
100
+ const block = blocks[index];
101
+ if (block?.header?.startsWith("[[hooks.SessionStart")) {
102
+ const group: typeof blocks = [];
103
+ while (index < blocks.length && blocks[index]?.header?.startsWith("[[hooks.SessionStart")) {
104
+ group.push(blocks[index]!);
105
+ index += 1;
106
+ }
107
+
108
+ const groupText = group.map((block) => block.text).join("\n");
109
+ if (!isAgentRelaySessionStartCommand(groupText)) output += `${groupText}\n`;
110
+ continue;
111
+ }
112
+
113
+ output += `${block?.text ?? ""}\n`;
114
+ index += 1;
115
+ }
116
+
117
+ return output.replace(/\s+$/, "\n");
118
+ }
119
+
79
120
  function compareVersions(left: string, right: string): number {
80
121
  const leftParts = left.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
81
122
  const rightParts = right.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
@@ -215,35 +256,43 @@ function installMarketplace(quiet = false): void {
215
256
 
216
257
  function installHook(): void {
217
258
  mkdirSync(join(home, ".codex"), { recursive: true });
259
+ const command = `bun ${shellQuote(installedHookScript)}`;
260
+
261
+ const configPath = join(home, ".codex", "config.toml");
262
+ const existingConfig = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
263
+ let output = removeAgentRelaySessionStartToml(existingConfig);
264
+ if (!output.endsWith("\n\n")) output += output.endsWith("\n") ? "\n" : "\n\n";
265
+ output += `[[hooks.SessionStart]]
266
+ matcher = "startup|resume"
267
+
268
+ [[hooks.SessionStart.hooks]]
269
+ type = "command"
270
+ command = "${command.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"
271
+ statusMessage = "Starting Agent Relay"
272
+ timeout = 10
273
+ `;
274
+
275
+ writeFileSync(configPath, output);
276
+
218
277
  const hooksPath = join(home, ".codex", "hooks.json");
278
+ if (!existsSync(hooksPath)) return;
279
+
219
280
  const hooksJson = readJsonFile<HooksJson>(hooksPath, { hooks: {} });
220
281
  hooksJson.hooks ??= {};
221
- hooksJson.hooks.SessionStart ??= [];
222
-
223
- const command = `bun ${shellQuote(installedHookScript)}`;
224
- hooksJson.hooks.SessionStart = hooksJson.hooks.SessionStart
282
+ hooksJson.hooks.SessionStart = (hooksJson.hooks.SessionStart ?? [])
225
283
  .map((group) => ({
226
284
  ...group,
227
285
  hooks: (group.hooks ?? []).filter((hook) => {
228
286
  if (hook.type !== "command" || typeof hook.command !== "string") return true;
229
- return !/agent-relay.*codex\/hooks\/session-start\.ts/.test(hook.command);
287
+ return !isAgentRelaySessionStartCommand(hook.command);
230
288
  }),
231
289
  }))
232
290
  .filter((group) => (group.hooks ?? []).length > 0);
233
291
 
234
- hooksJson.hooks.SessionStart.push({
235
- matcher: "startup|resume",
236
- hooks: [
237
- {
238
- type: "command",
239
- command,
240
- statusMessage: "Starting Agent Relay",
241
- timeout: 10,
242
- },
243
- ],
244
- });
292
+ if (hooksJson.hooks.SessionStart.length === 0) delete hooksJson.hooks.SessionStart;
245
293
 
246
- writeFileSync(hooksPath, `${JSON.stringify(hooksJson, null, 2)}\n`);
294
+ if (Object.keys(hooksJson.hooks).length === 0) rmSync(hooksPath, { force: true });
295
+ else writeFileSync(hooksPath, `${JSON.stringify(hooksJson, null, 2)}\n`);
247
296
  }
248
297
 
249
298
  async function pickLoopbackUrl(): Promise<string> {
@@ -407,8 +456,9 @@ function cleanupRun(runDir: string, appServer: ReturnType<typeof Bun.spawn> | nu
407
456
  function installCodexSupport(quiet = false): void {
408
457
  if (!commandExists("bun")) throw new Error("Bun is required: https://bun.sh");
409
458
  findCodexBinary();
410
- installMarketplace(quiet);
459
+ syncInstalledPackage();
411
460
  installHook();
461
+ installMarketplace(quiet);
412
462
  }
413
463
 
414
464
  function writeLauncherShim(name: string): void {
@@ -602,7 +652,9 @@ async function doctor(): Promise<void> {
602
652
  const checks: Array<[string, boolean, string]> = [];
603
653
  checks.push(["bun", commandExists("bun"), "Bun is required to run the sidecar"]);
604
654
  checks.push(["codex", findOnPath("codex", [aliasBinDir]) !== null, "Codex CLI is required"]);
605
- checks.push(["hook", existsSync(join(home, ".codex", "hooks.json")), "~/.codex/hooks.json exists"]);
655
+ const configPath = join(home, ".codex", "config.toml");
656
+ const config = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
657
+ checks.push(["hook", isAgentRelaySessionStartCommand(config), "~/.codex/config.toml has Agent Relay SessionStart hook"]);
606
658
  checks.push(["marketplace", existsSync(marketplaceFile), "Agent Relay marketplace is installed"]);
607
659
  checks.push(["launcher", existsSync(join(aliasBinDir, process.platform === "win32" ? "codex-relay.cmd" : "codex-relay")), "codex-relay launcher shim"]);
608
660
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/src/index.ts CHANGED
File without changes
package/codex/README.md DELETED
@@ -1,107 +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
- - attaches to a loaded thread for the current `cwd` when one exists
16
- - otherwise resumes the newest thread for the current `cwd`
17
- - otherwise creates a new thread
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
- ## Approvals and prompts
56
-
57
- Relay replies are usually sent with a shell command (`curl` to
58
- `/api/messages`), so Codex can prompt for approval in stricter modes.
59
-
60
- `codex-relay` forwards your launch mode to the sidecar, including
61
- `--ask-for-approval`, `--sandbox`, `--full-auto`, and `--yolo`.
62
-
63
- Example: no prompt loop, still workspace sandboxing:
64
-
65
- ```bash
66
- codex-relay -- --ask-for-approval never --sandbox workspace-write
67
- ```
68
-
69
- If you prefer prompts for everything else but want relay sends auto-approved,
70
- add a rule in `~/.codex/rules/default.rules` (adjust URL when using a remote relay):
71
-
72
- ```python
73
- prefix_rule(
74
- pattern = ["curl", "-sS", "-X", "POST", "http://127.0.0.1:4850/api/messages"],
75
- decision = "allow",
76
- justification = "Allow local Agent Relay message posts",
77
- )
78
- ```
79
-
80
- For local development from this repo:
81
-
82
- ```bash
83
- bun run bin/agent-relay-codex.ts
84
- bun run codex:smoke:fallback
85
- ```
86
-
87
- Useful environment variables:
88
-
89
- - `AGENT_RELAY_URL`
90
- - `AGENT_RELAY_CAPS`
91
- - `CODEX_APP_SERVER_URL`
92
- - `CODEX_THREAD_ID`
93
- - `CODEX_THREAD_MODE=auto|resume|start`
94
- - `CODEX_LIVE_STATE_PATH`
95
- - `CODEX_LIVE_COALESCE_WINDOW_MS`
96
- - `CODEX_LIVE_RECONNECT_INITIAL_MS`
97
- - `CODEX_LIVE_RECONNECT_MAX_MS`
98
- - `CODEX_LIVE_RIG`
99
- - `CODEX_MODEL`
100
-
101
- ## Notes
102
-
103
- 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.
104
-
105
- - `CODEX_THREAD_MODE=auto` will attach to an already loaded thread for the same `cwd`. That is what you want for real live control, but it also means the sidecar can attach to your current interactive Codex session if one is already open.
106
- - For isolated testing, set `CODEX_THREAD_MODE=start` so the sidecar always creates its own thread.
107
- - 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.