agent-relay-server 0.4.10 → 0.4.12

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 CHANGED
@@ -224,6 +224,9 @@ time, it starts a new thread by default to avoid surprising cwd-based attachment
224
224
  to an unrelated loaded session. Use `codex-relay --thread-mode auto` or
225
225
  `AGENT_RELAY_CODEX_FALLBACK_THREAD_MODE=auto` when you deliberately want the
226
226
  fallback sidecar to attach to the newest loaded thread for the current cwd.
227
+ The lower-level sidecar also defaults to `CODEX_THREAD_MODE=start`; `auto` is an
228
+ explicit opt-in because it may deliver relay messages into an already-open Codex
229
+ session for the same directory.
227
230
 
228
231
  ### Codex approval mode
229
232
 
@@ -311,7 +314,18 @@ agent-relay-codex uninstall
311
314
  ```
312
315
 
313
316
  This removes the Codex SessionStart hook, local plugin marketplace files, and
314
- launcher shims. It leaves shell profile PATH edits visible for manual cleanup.
317
+ launcher shims. It leaves shell profile PATH edits and runtime logs in place.
318
+
319
+ Full managed cleanup:
320
+
321
+ ```bash
322
+ agent-relay-codex uninstall --purge
323
+ ```
324
+
325
+ Purge also removes Agent Relay-managed shell profile PATH snippets, Codex runtime
326
+ logs, the launcher directory, and empty install directories. It only removes
327
+ profile snippets written by the installer marker; manual PATH edits are left
328
+ alone.
315
329
 
316
330
  ## What the Agent Sees
317
331
 
@@ -55,7 +55,7 @@ function usage(exitCode = 0): never {
55
55
  Usage:
56
56
  agent-relay-codex [--relay-url URL] [--listen ws://127.0.0.1:PORT] [-- <codex args...>]
57
57
  agent-relay-codex install [--alias|--no-alias]
58
- agent-relay-codex uninstall
58
+ agent-relay-codex uninstall [--purge]
59
59
  agent-relay-codex alias install
60
60
  agent-relay-codex alias remove
61
61
  agent-relay-codex doctor
@@ -66,6 +66,8 @@ With no subcommand, this launches Codex with live Agent Relay support.`);
66
66
  process.exit(exitCode);
67
67
  }
68
68
 
69
+ const pathMarker = "# Agent Relay Codex alias";
70
+
69
71
  function commandExists(command: string): boolean {
70
72
  return findOnPath(command) !== null;
71
73
  }
@@ -633,26 +635,110 @@ function installPathEntry(): boolean {
633
635
  }
634
636
 
635
637
  const shell = process.env.SHELL || "";
636
- const marker = "# Agent Relay Codex alias";
637
638
  const exportLine = `export PATH=${shellQuote(aliasBinDir)}:$PATH`;
638
639
  let profilePath = join(home, ".profile");
639
- let snippet = `\n${marker}\n${exportLine}\n`;
640
+ let snippet = `\n${pathMarker}\n${exportLine}\n`;
640
641
 
641
642
  if (shell.includes("zsh")) profilePath = join(home, ".zshrc");
642
643
  if (shell.includes("bash")) profilePath = join(home, ".bashrc");
643
644
  if (shell.includes("fish")) {
644
645
  profilePath = join(home, ".config", "fish", "config.fish");
645
- snippet = `\n${marker}\nfish_add_path ${shellQuote(aliasBinDir)}\n`;
646
+ snippet = `\n${pathMarker}\nfish_add_path ${shellQuote(aliasBinDir)}\n`;
646
647
  }
647
648
 
648
649
  mkdirSync(dirname(profilePath), { recursive: true });
649
650
  const current = existsSync(profilePath) ? readFileSync(profilePath, "utf8") : "";
650
- if (!current.includes(marker) && !current.includes(aliasBinDir)) {
651
+ if (!current.includes(pathMarker) && !current.includes(aliasBinDir)) {
651
652
  writeFileSync(profilePath, `${current.replace(/\s*$/, "")}${snippet}`);
652
653
  }
653
654
  return true;
654
655
  }
655
656
 
657
+ function managedProfilePaths(): string[] {
658
+ return [
659
+ join(home, ".profile"),
660
+ join(home, ".bashrc"),
661
+ join(home, ".zshrc"),
662
+ join(home, ".config", "fish", "config.fish"),
663
+ ];
664
+ }
665
+
666
+ function removeManagedPathSnippet(input: string): string {
667
+ const hadTrailingNewline = /\r?\n$/.test(input);
668
+ const lines = input.split(/\r?\n/);
669
+ const output: string[] = [];
670
+
671
+ for (let index = 0; index < lines.length; index += 1) {
672
+ const line = lines[index]!;
673
+ if (line === pathMarker) {
674
+ const next = lines[index + 1] || "";
675
+ if (next.includes(aliasBinDir)) index += 1;
676
+ continue;
677
+ }
678
+ output.push(line);
679
+ }
680
+
681
+ const cleaned = output.join("\n").replace(/\n{3,}/g, "\n\n").replace(/[ \t]+\n/g, "\n");
682
+ return hadTrailingNewline && cleaned ? `${cleaned.replace(/\n*$/, "")}\n` : cleaned.replace(/\n*$/, "");
683
+ }
684
+
685
+ function removeUnixPathEntries(): number {
686
+ let changed = 0;
687
+ for (const profilePath of managedProfilePaths()) {
688
+ if (!existsSync(profilePath)) continue;
689
+ const current = readFileSync(profilePath, "utf8");
690
+ if (!current.includes(pathMarker)) continue;
691
+ const cleaned = removeManagedPathSnippet(current);
692
+ if (cleaned !== current) {
693
+ writeFileSync(profilePath, cleaned);
694
+ changed += 1;
695
+ }
696
+ }
697
+ return changed;
698
+ }
699
+
700
+ function removeWindowsPathEntry(): boolean {
701
+ const script = [
702
+ `$target = ${JSON.stringify(aliasBinDir)}`,
703
+ "$path = [Environment]::GetEnvironmentVariable('Path', 'User')",
704
+ "if ($path) {",
705
+ " $parts = $path -split ';' | Where-Object { $_ -and ($_ -ne $target) }",
706
+ " [Environment]::SetEnvironmentVariable('Path', ($parts -join ';'), 'User')",
707
+ "}",
708
+ "$bin = [Environment]::GetEnvironmentVariable('AGENT_RELAY_CODEX_BIN', 'User')",
709
+ "if ($bin -eq $target) { [Environment]::SetEnvironmentVariable('AGENT_RELAY_CODEX_BIN', $null, 'User') }",
710
+ ].join("; ");
711
+ const result = Bun.spawnSync(["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
712
+ stdout: "pipe",
713
+ stderr: "pipe",
714
+ });
715
+ return result.exitCode === 0;
716
+ }
717
+
718
+ function removeManagedPathEntries(): string {
719
+ if (process.platform === "win32") {
720
+ return removeWindowsPathEntry() ? "Removed managed Windows user PATH entries." : "Could not update Windows user PATH entries.";
721
+ }
722
+ const changed = removeUnixPathEntries();
723
+ return changed > 0 ? `Removed managed PATH snippets from ${changed} shell profile(s).` : "No managed shell profile PATH snippets found.";
724
+ }
725
+
726
+ function removeEmptyDirectory(path: string): boolean {
727
+ if (!existsSync(path)) return false;
728
+ try {
729
+ if (readdirSync(path).length > 0) return false;
730
+ rmSync(path, { recursive: false, force: true });
731
+ return true;
732
+ } catch {
733
+ return false;
734
+ }
735
+ }
736
+
737
+ function removeEmptyInstallParents(): void {
738
+ removeEmptyDirectory(installRoot);
739
+ removeEmptyDirectory(dirname(installRoot));
740
+ }
741
+
656
742
  function installCodexAlias(): void {
657
743
  installLauncherShims(true);
658
744
  const updated = installPathEntry();
@@ -846,7 +932,11 @@ async function install(args: string[]): Promise<void> {
846
932
  }
847
933
  }
848
934
 
849
- function uninstall(): void {
935
+ function uninstall(args: string[] = []): void {
936
+ const purge = args.includes("--purge");
937
+ const unknown = args.filter((arg) => arg !== "--purge");
938
+ if (unknown.length > 0) throw new Error(`Unknown uninstall option: ${unknown.join(" ")}`);
939
+
850
940
  stopRuntimeSidecars();
851
941
  removeHook();
852
942
  removeLauncherShim("codex");
@@ -854,14 +944,24 @@ function uninstall(): void {
854
944
  rmSync(marketplaceRoot, { recursive: true, force: true });
855
945
  rmSync(installedPackageRoot, { recursive: true, force: true });
856
946
  console.log("Uninstalled Agent Relay Codex hook, plugin marketplace files, and launcher shims.");
857
- console.log(`PATH entries are left untouched; remove ${aliasBinDir} from your shell profile if you no longer want it there.`);
947
+
948
+ if (!purge) {
949
+ console.log(`PATH entries and runtime logs are left untouched. Run agent-relay-codex uninstall --purge for full managed cleanup.`);
950
+ return;
951
+ }
952
+
953
+ rmSync(runtimeRoot, { recursive: true, force: true });
954
+ rmSync(aliasBinDir, { recursive: true, force: true });
955
+ console.log(removeManagedPathEntries());
956
+ removeEmptyInstallParents();
957
+ console.log("Purged Agent Relay Codex runtime files, launcher directory, and empty install directories.");
858
958
  }
859
959
 
860
960
  async function main(): Promise<void> {
861
961
  const [command, ...args] = process.argv.slice(2);
862
962
  if (command === "help" || command === "--help" || command === "-h") usage(0);
863
963
  if (command === "install") return install(args);
864
- if (command === "uninstall") return uninstall();
964
+ if (command === "uninstall") return uninstall(args);
865
965
  if (command === "alias" && args[0] === "install") {
866
966
  installCodexSupport(false);
867
967
  return installCodexAlias();
package/codex/README.md CHANGED
@@ -12,9 +12,9 @@ This sidecar connects to a Codex app-server session and to Agent Relay, then del
12
12
 
13
13
  ## Current behavior
14
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
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
18
  - registers a relay agent with `client: codex-live`
19
19
  - marks the relay agent `ready=true` once app-server + thread are attached
20
20
  - polls relay inbox and delivers messages into the live thread
@@ -52,6 +52,22 @@ starts Codex with
52
52
  `--remote`, lets the SessionStart hook attach a sidecar to the actual thread,
53
53
  and kills sidecars plus the app-server when Codex exits.
54
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
+
55
71
  ## Approval mode
56
72
 
57
73
  Relay replies are usually sent with a shell command (`curl` to
@@ -131,6 +147,6 @@ startup in time.
131
147
 
132
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.
133
149
 
134
- - `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.
135
- - For isolated testing, set `CODEX_THREAD_MODE=start` so the sidecar always creates its own thread.
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.
136
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.
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { loadConfig, parseThreadMode } from "./live-sidecar";
3
+
4
+ describe("codex live sidecar config", () => {
5
+ it("defaults to starting an isolated thread", () => {
6
+ const config = loadConfig({
7
+ CODEX_LIVE_CWD: "/tmp/agent-relay-test",
8
+ });
9
+
10
+ expect(config.threadMode).toBe("start");
11
+ });
12
+
13
+ it("only accepts known thread attachment modes", () => {
14
+ expect(parseThreadMode("auto")).toBe("auto");
15
+ expect(parseThreadMode("resume")).toBe("resume");
16
+ expect(parseThreadMode("start")).toBe("start");
17
+ expect(parseThreadMode("latest")).toBe("start");
18
+ expect(parseThreadMode(undefined)).toBe("start");
19
+ });
20
+ });
@@ -572,38 +572,43 @@ function describeError(error: unknown): string {
572
572
  return error instanceof Error ? error.message : String(error);
573
573
  }
574
574
 
575
- function envNumber(name: string, fallback: number): number {
576
- const raw = process.env[name];
575
+ function envNumber(env: NodeJS.ProcessEnv, name: string, fallback: number): number {
576
+ const raw = env[name];
577
577
  if (!raw) return fallback;
578
578
  const parsed = Number(raw);
579
579
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
580
580
  }
581
581
 
582
- function loadConfig(): Config {
583
- const cwd = process.env.CODEX_LIVE_CWD || process.cwd();
584
- const capabilities = (process.env.AGENT_RELAY_CAPS || "chat")
582
+ export function parseThreadMode(raw: string | undefined): Config["threadMode"] {
583
+ if (raw === "auto" || raw === "resume" || raw === "start") return raw;
584
+ return "start";
585
+ }
586
+
587
+ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
588
+ const cwd = env.CODEX_LIVE_CWD || process.cwd();
589
+ const capabilities = (env.AGENT_RELAY_CAPS || "chat")
585
590
  .split(",")
586
591
  .map((value) => value.trim())
587
592
  .filter(Boolean);
588
593
 
589
594
  return {
590
- relayUrl: process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850",
591
- appServerUrl: process.env.CODEX_APP_SERVER_URL || "ws://127.0.0.1:4501",
595
+ relayUrl: env.AGENT_RELAY_URL || "http://127.0.0.1:4850",
596
+ appServerUrl: env.CODEX_APP_SERVER_URL || "ws://127.0.0.1:4501",
592
597
  cwd,
593
- rig: process.env.CODEX_LIVE_RIG || "codex-live",
598
+ rig: env.CODEX_LIVE_RIG || "codex-live",
594
599
  capabilities,
595
- tags: ["codex", process.env.CODEX_LIVE_RIG || "codex-live", cwd.split("/").filter(Boolean).at(-1) || "unknown"],
596
- statePath: process.env.CODEX_LIVE_STATE_PATH || resolve(cwd, "codex/runtime/live-state.json"),
597
- pollIntervalMs: envNumber("CODEX_LIVE_POLL_INTERVAL_MS", 2000),
598
- heartbeatIntervalMs: envNumber("CODEX_LIVE_HEARTBEAT_INTERVAL_MS", 30000),
599
- coalesceWindowMs: envNumber("CODEX_LIVE_COALESCE_WINDOW_MS", 600),
600
- reconnectInitialDelayMs: envNumber("CODEX_LIVE_RECONNECT_INITIAL_MS", 1000),
601
- reconnectMaxDelayMs: envNumber("CODEX_LIVE_RECONNECT_MAX_MS", 10000),
602
- threadMode: (process.env.CODEX_THREAD_MODE as Config["threadMode"]) || "auto",
603
- threadId: process.env.CODEX_THREAD_ID || undefined,
604
- model: process.env.CODEX_MODEL || undefined,
605
- approvalPolicy: process.env.CODEX_LIVE_APPROVAL_POLICY || undefined,
606
- sandbox: process.env.CODEX_LIVE_SANDBOX || undefined,
600
+ tags: ["codex", env.CODEX_LIVE_RIG || "codex-live", cwd.split("/").filter(Boolean).at(-1) || "unknown"],
601
+ statePath: env.CODEX_LIVE_STATE_PATH || resolve(cwd, "codex/runtime/live-state.json"),
602
+ pollIntervalMs: envNumber(env, "CODEX_LIVE_POLL_INTERVAL_MS", 2000),
603
+ heartbeatIntervalMs: envNumber(env, "CODEX_LIVE_HEARTBEAT_INTERVAL_MS", 30000),
604
+ coalesceWindowMs: envNumber(env, "CODEX_LIVE_COALESCE_WINDOW_MS", 600),
605
+ reconnectInitialDelayMs: envNumber(env, "CODEX_LIVE_RECONNECT_INITIAL_MS", 1000),
606
+ reconnectMaxDelayMs: envNumber(env, "CODEX_LIVE_RECONNECT_MAX_MS", 10000),
607
+ threadMode: parseThreadMode(env.CODEX_THREAD_MODE),
608
+ threadId: env.CODEX_THREAD_ID || undefined,
609
+ model: env.CODEX_MODEL || undefined,
610
+ approvalPolicy: env.CODEX_LIVE_APPROVAL_POLICY || undefined,
611
+ sandbox: env.CODEX_LIVE_SANDBOX || undefined,
607
612
  };
608
613
  }
609
614
 
@@ -613,7 +618,9 @@ async function main(): Promise<void> {
613
618
  await sidecar.run();
614
619
  }
615
620
 
616
- main().catch((error) => {
617
- console.error(error instanceof Error ? error.stack || error.message : String(error));
618
- process.exit(1);
619
- });
621
+ if (import.meta.main) {
622
+ main().catch((error) => {
623
+ console.error(error instanceof Error ? error.stack || error.message : String(error));
624
+ process.exit(1);
625
+ });
626
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "Agent Relay integration for Codex sessions",
5
5
  "author": {
6
6
  "name": "Edin Mujkanovic"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",