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 +15 -1
- package/bin/agent-relay-codex.ts +108 -8
- package/codex/README.md +21 -5
- package/codex/live-sidecar.test.ts +20 -0
- package/codex/live-sidecar.ts +31 -24
- package/codex/plugin/.codex-plugin/plugin.json +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
package/bin/agent-relay-codex.ts
CHANGED
|
@@ -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${
|
|
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${
|
|
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(
|
|
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
|
-
|
|
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
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
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=
|
|
135
|
-
-
|
|
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
|
+
});
|
package/codex/live-sidecar.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
583
|
-
|
|
584
|
-
|
|
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:
|
|
591
|
-
appServerUrl:
|
|
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:
|
|
598
|
+
rig: env.CODEX_LIVE_RIG || "codex-live",
|
|
594
599
|
capabilities,
|
|
595
|
-
tags: ["codex",
|
|
596
|
-
statePath:
|
|
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: (
|
|
603
|
-
threadId:
|
|
604
|
-
model:
|
|
605
|
-
approvalPolicy:
|
|
606
|
-
sandbox:
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
+
}
|