relay-codex 0.0.0-stage → 0.5.0
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/LICENSE +21 -0
- package/README.md +118 -2
- package/assets/relay.svg +15 -0
- package/bin/relay.cjs +148 -0
- package/docs/commands.md +176 -0
- package/docs/publishing.md +72 -0
- package/docs/troubleshooting.md +49 -0
- package/package.json +47 -4
- package/python/Menu.swift +188 -0
- package/python/broker.py +297 -0
- package/python/commands.py +900 -0
- package/python/compatibility.py +227 -0
- package/python/desktop.py +171 -0
- package/python/execution.py +551 -0
- package/python/launch.py +417 -0
- package/python/metadata.py +82 -0
- package/python/notifications.py +36 -0
- package/python/pool.py +586 -0
- package/python/probe_native.py +91 -0
- package/python/processes.py +12 -0
- package/python/requirements.lock +6 -0
- package/python/requirements.txt +2 -0
- package/python/router.py +2288 -0
- package/python/runtime.py +97 -0
- package/python/service.py +737 -0
- package/python/settings.py +110 -0
- package/python/tools.py +233 -0
- package/python/watch.py +74 -0
- package/tests/fake_native.py +240 -0
- package/tests/test_audit.py +320 -0
- package/tests/test_capacity.py +217 -0
- package/tests/test_controller.py +517 -0
- package/tests/test_features.py +350 -0
- package/tests/test_launch.py +150 -0
- package/tests/test_pool.py +446 -0
- package/tests/test_relay.py +173 -0
- package/tests/test_router.py +314 -0
- package/tests/test_services.py +479 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 blackspider-ops
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,119 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center"><img src="https://raw.githubusercontent.com/blackspider-ops/relay/main/assets/relay.svg" alt="Relay — keep Codex moving" width="840"></p>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Relay keeps your Codex sessions moving between your own accounts. When an account
|
|
4
|
+
runs out, it finds another available account. When all accounts are waiting for a
|
|
5
|
+
reset, it waits too, then continues the saved task.
|
|
6
|
+
|
|
7
|
+
**macOS · Native Codex terminal · One account or many**
|
|
8
|
+
|
|
9
|
+
## Get started
|
|
10
|
+
|
|
11
|
+
You'll need [Codex](https://learn.chatgpt.com/docs/cli), Node.js 20 or newer, and
|
|
12
|
+
Python 3.11 or newer. If you use Homebrew, `brew install node python@3.13` installs
|
|
13
|
+
Node and Python.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install -g relay-codex
|
|
17
|
+
relay setup --count 2
|
|
18
|
+
relay run
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Choose however many accounts you have. Each person signs into their own accounts;
|
|
22
|
+
this project does not include anyone's saved logins. Existing logins from the
|
|
23
|
+
earlier `codex-accounts` tool are picked up automatically.
|
|
24
|
+
|
|
25
|
+
The first run downloads Relay's Python libraries. Later runs use the saved copy.
|
|
26
|
+
Your usual `codex` command keeps working.
|
|
27
|
+
|
|
28
|
+
Prefer trying it without a global install?
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npx --yes relay-codex setup --count 2
|
|
32
|
+
npx --yes relay-codex run
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Use it like Codex
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
relay run -- resume --yolo
|
|
39
|
+
relay run -- -C ~/projects/website resume
|
|
40
|
+
relay run -- exec --json "Review this project"
|
|
41
|
+
relay status --watch
|
|
42
|
+
relay menubar
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`--yolo` keeps its usual full-access meaning. Without it, Relay keeps the task's
|
|
46
|
+
permissions. Resume starts with sessions from your current folder, just like
|
|
47
|
+
`codex resume`. Add `--all` to browse every folder, or `-C PATH` to choose a folder.
|
|
48
|
+
Different terminals can work on different conversations at once.
|
|
49
|
+
|
|
50
|
+
Put native Codex commands and options after `relay run --`. Use `relay run --help`
|
|
51
|
+
for the full Codex command and flag list, or `relay help` for Relay management.
|
|
52
|
+
Profiles, extra writable directories, Git worktrees, and noninteractive tasks can
|
|
53
|
+
use the account pool. `relay explain -- <arguments>` shows how a launch will run.
|
|
54
|
+
Native utilities, external/local-provider endpoints, and a few native-only setup
|
|
55
|
+
flags run directly; `--require-recovery` rejects a launch outside Relay recovery.
|
|
56
|
+
|
|
57
|
+
If a conversation is already open in another app, close it there before resuming
|
|
58
|
+
it here, or use Codex's `fork` command for a separate copy. To move a Relay task
|
|
59
|
+
between terminals:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
relay attach --takeover SESSION_ID
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Accounts show their saved email addresses. The watch view updates only when local
|
|
66
|
+
status changes; it uses no model allowance. The menu bar uses an icon and opens
|
|
67
|
+
at Mac login. `relay menubar --no-startup --configure-only` disables login startup.
|
|
68
|
+
|
|
69
|
+
## What happens when something stops?
|
|
70
|
+
|
|
71
|
+
- **Account limit:** use an available account, or wait for the earliest usable reset.
|
|
72
|
+
- **One account:** wait for its reset and continue the same conversation.
|
|
73
|
+
- **Connection or sign-in failure:** retry at 5, 10, and 15 minutes, then try another account.
|
|
74
|
+
- **“Selected model is at capacity”:** wait five minutes and try the same model again.
|
|
75
|
+
Relay keeps waiting rather than cycling logins. A different model is used only
|
|
76
|
+
if you explicitly configure a fallback.
|
|
77
|
+
- **An unclear interruption during a command:** pause for review before continuing.
|
|
78
|
+
Relay cannot guarantee that an external action happened exactly once.
|
|
79
|
+
|
|
80
|
+
## Leave work running
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
relay start --name website --cwd ~/projects/website --prompt "Finish the remaining checks"
|
|
84
|
+
relay attach SESSION_ID
|
|
85
|
+
relay stop SESSION_ID
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Background tasks keep running after their terminal closes. Use `relay status --all`
|
|
89
|
+
to find saved sessions and `relay restart SESSION_ID` to recover a stopped one.
|
|
90
|
+
Opt into task recovery at Mac login with `relay startup enable SESSION_ID`.
|
|
91
|
+
`relay workspace save daily` and `relay workspace restore daily` retain the same
|
|
92
|
+
conversations across several terminals.
|
|
93
|
+
|
|
94
|
+
## A few useful commands
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
relay add personal
|
|
98
|
+
relay account disable personal
|
|
99
|
+
relay pause SESSION_ID
|
|
100
|
+
relay unpause SESSION_ID
|
|
101
|
+
relay retry SESSION_ID
|
|
102
|
+
relay doctor --self-test
|
|
103
|
+
relay menubar
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Accounts stay in your Mac's Keychain. Relay keeps its settings and task records
|
|
107
|
+
in your private Application Support folder. Never share that folder.
|
|
108
|
+
|
|
109
|
+
## Updates and limits
|
|
110
|
+
|
|
111
|
+
Relay checks the installed Codex version and keeps a working copy for running
|
|
112
|
+
sessions. Future Codex changes may still need a Relay update.
|
|
113
|
+
Update Relay with `npm install -g relay-codex@latest`.
|
|
114
|
+
|
|
115
|
+
This is an independent tool, not an OpenAI product. Short local tests cover
|
|
116
|
+
parallel tasks and recovery; they are not an overnight reliability guarantee.
|
|
117
|
+
|
|
118
|
+
[Commands](docs/commands.md) · [Troubleshooting](docs/troubleshooting.md) ·
|
|
119
|
+
[Publishing to npm or Homebrew](docs/publishing.md) · [Contributing](CONTRIBUTING.md)
|
package/assets/relay.svg
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="360" viewBox="0 0 1280 360" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">Relay</title>
|
|
3
|
+
<desc id="desc">Keep Codex moving. Four paths meet in one forward arrow.</desc>
|
|
4
|
+
<rect width="1280" height="360" rx="24" fill="#111916"/>
|
|
5
|
+
<g fill="none" stroke="#a8d9bd" stroke-width="10" stroke-linecap="round">
|
|
6
|
+
<path d="M96 108h62c48 0 44 72 94 72h44" opacity=".5"/>
|
|
7
|
+
<path d="M96 156h82c34 0 30 24 72 24h46" opacity=".7"/>
|
|
8
|
+
<path d="M96 204h82c34 0 30-24 72-24h46"/>
|
|
9
|
+
<path d="M96 252h62c48 0 44-72 94-72h44" opacity=".5"/>
|
|
10
|
+
<path d="m278 158 24 22-24 22" stroke="#f7be74"/>
|
|
11
|
+
</g>
|
|
12
|
+
<text x="366" y="185" fill="#f5f3ec" font-family="Helvetica Neue,Arial,sans-serif" font-size="98" font-weight="650" letter-spacing="-5">Relay</text>
|
|
13
|
+
<text x="371" y="239" fill="#acb9b0" font-family="Helvetica Neue,Arial,sans-serif" font-size="27" letter-spacing=".2">Keep Codex moving.</text>
|
|
14
|
+
<text x="1140" y="73" text-anchor="end" fill="#a8d9bd" font-family="Menlo,monospace" font-size="15">FOR YOUR TERMINAL</text>
|
|
15
|
+
</svg>
|
package/bin/relay.cjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const { spawnSync, spawn } = require('node:child_process');
|
|
9
|
+
const { setTimeout: delay } = require('node:timers/promises');
|
|
10
|
+
|
|
11
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
12
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
13
|
+
|
|
14
|
+
function stateDirectory(env = process.env, home = os.homedir()) {
|
|
15
|
+
if (env.RELAY_STATE || env.CODEX_ACCOUNTS_STATE) {
|
|
16
|
+
return path.resolve(env.RELAY_STATE || env.CODEX_ACCOUNTS_STATE);
|
|
17
|
+
}
|
|
18
|
+
const support = path.join(home, 'Library', 'Application Support');
|
|
19
|
+
const legacy = path.join(support, 'Codex Accounts');
|
|
20
|
+
return fs.existsSync(path.join(legacy, 'accounts')) ? legacy : path.join(support, 'Relay');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pythonCommand() {
|
|
24
|
+
const choices = [process.env.RELAY_PYTHON, 'python3', 'python3.14', 'python3.13', 'python3.12', 'python3.11'].filter(Boolean);
|
|
25
|
+
for (const command of choices) {
|
|
26
|
+
const result = spawnSync(command, ['-c', 'import sys; print(sys.executable); raise SystemExit(sys.version_info < (3, 11))'], { encoding: 'utf8' });
|
|
27
|
+
if (result.status === 0) return result.stdout.trim();
|
|
28
|
+
}
|
|
29
|
+
throw new Error('Relay needs Python 3.11 or newer. Install it with: brew install python@3.13');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function privateDirectory(directory) {
|
|
33
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
34
|
+
const info = fs.lstatSync(directory);
|
|
35
|
+
if (info.isSymbolicLink() || (process.getuid && info.uid !== process.getuid())) {
|
|
36
|
+
throw new Error('Relay needs a private folder owned by your user.');
|
|
37
|
+
}
|
|
38
|
+
fs.chmodSync(directory, 0o700);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fingerprint() {
|
|
42
|
+
const source = path.join(packageRoot, 'python');
|
|
43
|
+
const names = fs.readdirSync(source).filter(name => /\.(py|swift|lock|txt)$/.test(name)).sort();
|
|
44
|
+
for (const name of fs.readdirSync(path.join(packageRoot, 'tests')).filter(name => name.endsWith('.py')).sort()) names.push(`tests/${name}`);
|
|
45
|
+
const hash = crypto.createHash('sha256');
|
|
46
|
+
hash.update(pkg.version);
|
|
47
|
+
for (const name of names) hash.update(name).update(fs.readFileSync(name.startsWith('tests/') ? path.join(packageRoot, name) : path.join(source, name)));
|
|
48
|
+
return { id: `${pkg.version}-${hash.digest('hex').slice(0, 16)}`, names, source };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function processBirth(pid) {
|
|
52
|
+
const result = spawnSync('/bin/ps', ['-p', String(pid), '-o', 'lstart='], { encoding: 'utf8' });
|
|
53
|
+
return result.status === 0 ? result.stdout.trim() : '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ownerAlive(owner) {
|
|
57
|
+
try {
|
|
58
|
+
process.kill(owner.pid, 0);
|
|
59
|
+
return Boolean(owner.birth) && processBirth(owner.pid) === owner.birth;
|
|
60
|
+
} catch { return false; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function withSetupLock(state, operation) {
|
|
64
|
+
const lock = path.join(state, 'relay-setup.lock');
|
|
65
|
+
const deadline = Date.now() + 10 * 60 * 1000;
|
|
66
|
+
while (true) {
|
|
67
|
+
try { fs.mkdirSync(lock, { mode: 0o700 }); break; }
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code !== 'EEXIST') throw error;
|
|
70
|
+
let stale = false;
|
|
71
|
+
try {
|
|
72
|
+
const owner = JSON.parse(fs.readFileSync(path.join(lock, 'owner.json'), 'utf8'));
|
|
73
|
+
stale = !ownerAlive(owner);
|
|
74
|
+
} catch {
|
|
75
|
+
// Another process may be between mkdir and writing its owner record.
|
|
76
|
+
try { stale = Date.now() - fs.statSync(lock).mtimeMs > 30_000; }
|
|
77
|
+
catch (error) { if (error.code === 'ENOENT') continue; throw error; }
|
|
78
|
+
}
|
|
79
|
+
if (stale) {
|
|
80
|
+
const abandoned = `${lock}.stale-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
81
|
+
try { fs.renameSync(lock, abandoned); fs.rmSync(abandoned, { recursive: true, force: true }); }
|
|
82
|
+
catch (error) { if (!['ENOENT', 'EEXIST'].includes(error.code)) throw error; }
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (Date.now() > deadline) throw new Error('Another Relay setup is still running. Let it finish, then try again.');
|
|
86
|
+
await delay(200);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
fs.writeFileSync(path.join(lock, 'owner.json'), JSON.stringify({ pid: process.pid, birth: processBirth(process.pid) }), { mode: 0o600 });
|
|
90
|
+
try { return await operation(); }
|
|
91
|
+
finally { fs.rmSync(lock, { recursive: true, force: true }); }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function run(command, args) {
|
|
95
|
+
const result = spawnSync(command, args, { stdio: 'inherit' });
|
|
96
|
+
if (result.error || result.status !== 0) throw new Error('Setup did not finish. Run Relay again to retry.');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function prepare(state) {
|
|
100
|
+
const bundle = fingerprint();
|
|
101
|
+
const destination = path.join(state, 'relay-runtimes', bundle.id);
|
|
102
|
+
const interpreter = path.join(destination, '.venv', 'bin', 'python');
|
|
103
|
+
const marker = path.join(destination, 'ready.json');
|
|
104
|
+
const ready = () => fs.existsSync(marker) && fs.existsSync(interpreter) && spawnSync(interpreter, ['-c', 'import websockets, jsonschema'], { stdio: 'ignore' }).status === 0;
|
|
105
|
+
if (ready()) return { interpreter, router: path.join(destination, 'router.py') };
|
|
106
|
+
const python = pythonCommand();
|
|
107
|
+
return withSetupLock(state, async () => {
|
|
108
|
+
if (ready()) return { interpreter, router: path.join(destination, 'router.py') };
|
|
109
|
+
privateDirectory(destination);
|
|
110
|
+
for (const name of bundle.names) {
|
|
111
|
+
const source = name.startsWith('tests/') ? path.join(packageRoot, name) : path.join(bundle.source, name);
|
|
112
|
+
fs.mkdirSync(path.dirname(path.join(destination, name)), { recursive: true, mode: 0o700 });
|
|
113
|
+
fs.copyFileSync(source, path.join(destination, name));
|
|
114
|
+
}
|
|
115
|
+
console.error('Preparing Relay for this Mac. This only happens on first use or after an update.');
|
|
116
|
+
// Always retry dependency installation if a previous setup was interrupted.
|
|
117
|
+
run(python, ['-m', 'venv', path.join(destination, '.venv')]);
|
|
118
|
+
run(interpreter, ['-m', 'pip', 'install', '--disable-pip-version-check', '-r', path.join(destination, 'requirements.lock')]);
|
|
119
|
+
fs.writeFileSync(marker, JSON.stringify({ version: pkg.version }), { mode: 0o600 });
|
|
120
|
+
return { interpreter, router: path.join(destination, 'router.py') };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function main() {
|
|
125
|
+
if (process.platform !== 'darwin') throw new Error('Relay currently supports macOS.');
|
|
126
|
+
if (process.argv.length === 3 && ['--version', '-V'].includes(process.argv[2])) {
|
|
127
|
+
console.log(`Relay ${pkg.version}`); return;
|
|
128
|
+
}
|
|
129
|
+
const state = stateDirectory(); privateDirectory(state);
|
|
130
|
+
const runtime = await prepare(state);
|
|
131
|
+
const child = spawn(runtime.interpreter, [runtime.router, ...process.argv.slice(2)], {
|
|
132
|
+
stdio: 'inherit', env: { ...process.env, RELAY_STATE: state, CODEX_ACCOUNTS_STATE: state }
|
|
133
|
+
});
|
|
134
|
+
// Terminal Ctrl-C already reaches both foreground processes.
|
|
135
|
+
const interrupt = () => {};
|
|
136
|
+
const terminate = () => child.kill('SIGTERM');
|
|
137
|
+
process.on('SIGINT', interrupt); process.on('SIGTERM', terminate);
|
|
138
|
+
process.on('SIGHUP', terminate);
|
|
139
|
+
child.on('error', () => { console.error('Relay could not start. Run it again to retry.'); process.exitCode = 1; });
|
|
140
|
+
await new Promise(resolve => child.on('close', (code, signal) => {
|
|
141
|
+
process.exitCode = code ?? (signal === 'SIGINT' ? 130 : 1); resolve();
|
|
142
|
+
}));
|
|
143
|
+
process.off('SIGINT', interrupt); process.off('SIGTERM', terminate);
|
|
144
|
+
process.off('SIGHUP', terminate);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { stateDirectory, fingerprint, ownerAlive, withSetupLock, prepare };
|
|
148
|
+
if (require.main === module) main().catch(error => { console.error(`relay: ${error.message}`); process.exitCode = 1; });
|
package/docs/commands.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Commands
|
|
2
|
+
|
|
3
|
+
## Help
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
relay help
|
|
7
|
+
relay help account
|
|
8
|
+
relay help workspace
|
|
9
|
+
relay run --help
|
|
10
|
+
relay run -- exec --help
|
|
11
|
+
relay help codex resume
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`relay run --help` shows the installed Codex command and flag list, followed by
|
|
15
|
+
Relay's launch options. Put native arguments after `relay run --`.
|
|
16
|
+
|
|
17
|
+
## Accounts
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
relay setup --count 1
|
|
21
|
+
relay setup --count 6
|
|
22
|
+
relay add personal
|
|
23
|
+
relay list
|
|
24
|
+
relay account identify
|
|
25
|
+
relay refresh
|
|
26
|
+
relay account rename personal "My account"
|
|
27
|
+
relay account disable personal
|
|
28
|
+
relay account enable personal
|
|
29
|
+
relay account priority personal 10
|
|
30
|
+
relay account cap personal 2
|
|
31
|
+
relay account models personal YOUR_MODEL
|
|
32
|
+
relay account reauth personal
|
|
33
|
+
relay account remove personal
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Accounts display their saved email addresses. `account identify` fills in emails
|
|
37
|
+
for earlier installations without another sign-in. Commands accept an account's
|
|
38
|
+
label, unique email, or chosen display name. Renaming keeps the login usable.
|
|
39
|
+
A session cap of zero means unlimited. Removing an account retains its login
|
|
40
|
+
unless you request `--revoke`.
|
|
41
|
+
|
|
42
|
+
## Launch and recovery
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
relay run
|
|
46
|
+
relay run -- resume --yolo
|
|
47
|
+
relay run -- -C ~/projects/website resume
|
|
48
|
+
relay run -- -p work --add-dir ~/shared
|
|
49
|
+
relay run -- --worktree "Try the next change"
|
|
50
|
+
relay run -- exec --json "Run the checks"
|
|
51
|
+
relay run -- review --uncommitted
|
|
52
|
+
relay explain -- resume --yolo
|
|
53
|
+
relay run --require-recovery -- resume
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Resume defaults to the current folder. Add `--all` to browse other folders or
|
|
57
|
+
`-C/--cd` to choose another working folder. Native profile files are read from
|
|
58
|
+
`$CODEX_HOME/NAME.config.toml`; explicit config flags override the profile.
|
|
59
|
+
Relay worktrees are detached Git worktrees under its private state folder. Their
|
|
60
|
+
files are retained for review; cleanup does not delete worktrees.
|
|
61
|
+
|
|
62
|
+
Interactive work, resume/fork, profiles, extra directories, worktrees, and supported
|
|
63
|
+
noninteractive exec/review runs use the account pool. Exec supports text/stdin,
|
|
64
|
+
images, JSON events, output schemas, last-message files, and resume/fork. Review
|
|
65
|
+
supports the working tree, base branch, commit, or custom instructions.
|
|
66
|
+
|
|
67
|
+
Native utility commands are forwarded unchanged. Local providers, explicitly
|
|
68
|
+
remote servers, hook-trust bypass, and ignoring native config/rules use native
|
|
69
|
+
Codex directly with its ordinary login. Help/version also run directly. These
|
|
70
|
+
modes do not use Relay recovery. `--require-recovery` rejects such a launch, and
|
|
71
|
+
`explain` shows which mode applies without making an inference request.
|
|
72
|
+
|
|
73
|
+
Noninteractive runs cannot approve a tool request on your behalf. Use explicit
|
|
74
|
+
native permissions such as `--yolo` only when intended, or resume interactively
|
|
75
|
+
when input is required. Automatic review is supported by `--approve-for-me`.
|
|
76
|
+
|
|
77
|
+
## Dashboard and session controls
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
relay status
|
|
81
|
+
relay status --watch
|
|
82
|
+
relay status --all
|
|
83
|
+
relay status --json
|
|
84
|
+
relay menubar
|
|
85
|
+
relay menubar --no-startup
|
|
86
|
+
relay attach SESSION_ID
|
|
87
|
+
relay attach SESSION_ID --takeover
|
|
88
|
+
relay pause SESSION_ID --wait
|
|
89
|
+
relay unpause SESSION_ID --wait
|
|
90
|
+
relay retry SESSION_ID --wait
|
|
91
|
+
relay switch SESSION_ID --account personal --wait
|
|
92
|
+
relay stop SESSION_ID --wait
|
|
93
|
+
relay restart SESSION_ID
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The watch view waits for local file changes and redraws only when useful status
|
|
97
|
+
changes. Heartbeats do not repeatedly print the table. Watching uses no model
|
|
98
|
+
allowance and does not fetch account allowance. `refresh` explicitly checks it.
|
|
99
|
+
The menu uses one local status stream, a template icon, and native session actions.
|
|
100
|
+
It opens at Mac login after `relay menubar`; disable that in its menu or with
|
|
101
|
+
`relay menubar --no-startup --configure-only`.
|
|
102
|
+
|
|
103
|
+
IDs may be shortened to a unique prefix. Without `--wait`, controls are queued;
|
|
104
|
+
with it, Relay waits for the worker's confirmation and reports a rejection.
|
|
105
|
+
Pause stops automatic recovery after current work finishes. Retry clears a review
|
|
106
|
+
hold: inspect an interrupted external action in the conversation first.
|
|
107
|
+
|
|
108
|
+
## Background tasks and saved workspaces
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
relay start --name website --cwd ~/projects/website --prompt "Run the checks"
|
|
112
|
+
relay workspace save daily
|
|
113
|
+
relay workspace list
|
|
114
|
+
relay workspace restore daily
|
|
115
|
+
relay workspace remove daily
|
|
116
|
+
relay startup enable SESSION_ID
|
|
117
|
+
relay startup list
|
|
118
|
+
relay startup disable SESSION_ID
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
A workspace saves the same supervised session IDs, rather than starting fresh
|
|
122
|
+
conversations. Restore restarts stopped jobs and prints reconnect commands; live
|
|
123
|
+
jobs are retained. Save specific sessions by appending their IDs to `workspace save`.
|
|
124
|
+
|
|
125
|
+
The normal command remains available after reboot. Background task recovery at
|
|
126
|
+
login is a separate opt-in setting; enabling the menu does not enable task startup.
|
|
127
|
+
Uncertain work still pauses for review after recovery.
|
|
128
|
+
|
|
129
|
+
## Preferences
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
relay config
|
|
133
|
+
relay config notifications true
|
|
134
|
+
relay config background_on_disconnect true
|
|
135
|
+
relay config keep_awake true
|
|
136
|
+
relay config retry_interval 300
|
|
137
|
+
relay config retry_attempts 3
|
|
138
|
+
relay config routing quota
|
|
139
|
+
relay config preferred_accounts '["personal"]'
|
|
140
|
+
relay config fallback_models '["YOUR_CHOSEN_MODEL"]'
|
|
141
|
+
relay config retry_interval 60 --profile quick
|
|
142
|
+
relay run --profile quick
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Routing choices are `balanced`, `quota`, `priority`, and `round-robin`. Model
|
|
146
|
+
fallback is opt-in. Capacity errors otherwise retain the selected model/account.
|
|
147
|
+
Keep-awake prevents idle system sleep only while a managed task is active; it does
|
|
148
|
+
not keep the display on. Project preferences may live in `.relay.toml` under
|
|
149
|
+
`[settings]` or `[profiles.NAME]`. Legacy `.codex-accounts.toml` files still work.
|
|
150
|
+
Relay recovery profiles go before `--`; native Codex profiles go after it.
|
|
151
|
+
|
|
152
|
+
## Diagnostics and cleanup
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
relay doctor --self-test
|
|
156
|
+
relay doctor --backup
|
|
157
|
+
relay doctor --restore-backup
|
|
158
|
+
relay diagnostics relay-diagnostics.json
|
|
159
|
+
relay clean
|
|
160
|
+
relay clean --older-than 30
|
|
161
|
+
relay clean --apply
|
|
162
|
+
relay events --limit 30
|
|
163
|
+
relay usage
|
|
164
|
+
relay runtime list
|
|
165
|
+
relay runtime preflight /path/to/codex
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Diagnostics export allowlisted versions, event types, and counts without account
|
|
169
|
+
emails, task names, project paths, prompts, or credentials. Review other files
|
|
170
|
+
before sharing them; private state and connection files must stay private.
|
|
171
|
+
|
|
172
|
+
Cleanup is a dry run unless `--apply` is given. It retains live/restartable runtime
|
|
173
|
+
references and saved workspaces. `--jobs` also allows removal of old stopped-job
|
|
174
|
+
records. Stop supervised jobs before applying cleanup. Worktrees and account
|
|
175
|
+
logins are retained. Metadata restore also requires stopped tasks and preserves a
|
|
176
|
+
copy of the replaced database. Usage forecasts are local estimates.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Sharing Relay
|
|
2
|
+
|
|
3
|
+
Relay is published on npm as [relay-codex](https://www.npmjs.com/package/relay-codex).
|
|
4
|
+
Friends can run `npx relay-codex` or install it with `npm install -g relay-codex`.
|
|
5
|
+
|
|
6
|
+
## GitHub and npx
|
|
7
|
+
|
|
8
|
+
The GitHub repository is enough for friends to install or try Relay:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install -g github:blackspider-ops/relay#v0.5.0
|
|
12
|
+
npx --yes github:blackspider-ops/relay#v0.5.0 --version
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`npx` runs packages; it is not a separate publishing service. Publishing
|
|
16
|
+
`relay-codex` on npm also makes `npx relay-codex` available.
|
|
17
|
+
|
|
18
|
+
## Publish on npm
|
|
19
|
+
|
|
20
|
+
The repository includes `.github/workflows/publish.yml`. Configure a trusted
|
|
21
|
+
publisher for `relay-codex` using GitHub user `blackspider-ops`, repository `relay`,
|
|
22
|
+
and workflow filename `publish.yml`. Select direct `npm publish` permission if
|
|
23
|
+
releases should publish automatically. No long-lived publishing token is needed.
|
|
24
|
+
A new trusted publisher must complete its first publish within npm's validation
|
|
25
|
+
window. Run the workflow for an existing reviewed version tag, or publish the
|
|
26
|
+
GitHub release; the workflow runs checks before publishing. After the first
|
|
27
|
+
successful trusted publish, set the repository variable
|
|
28
|
+
`NPM_TRUSTED_PUBLISHING_READY=true` to enable automatic release publishing. Until
|
|
29
|
+
then, releases do not launch a publishing attempt that requires authentication.
|
|
30
|
+
|
|
31
|
+
Initial trust setup requires npm account verification. Interactive local
|
|
32
|
+
`npm publish` also requires verification when package 2FA applies. Installing and
|
|
33
|
+
running the public package does not require npm sign-in. An expired publishing
|
|
34
|
+
prompt should be restarted once, rather than repeatedly running `npm login`.
|
|
35
|
+
|
|
36
|
+
For a manual release, review and publish the exact packed artifact:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm test
|
|
40
|
+
npm run check
|
|
41
|
+
npm pack --ignore-scripts
|
|
42
|
+
npm publish ./relay-codex-VERSION.tgz --access public
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
After publishing, friends can use:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npx relay-codex setup --count 2
|
|
49
|
+
npm install -g relay-codex
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Homebrew
|
|
53
|
+
|
|
54
|
+
The repository includes a formula under `Formula/`. It can be installed from this
|
|
55
|
+
repository as a personal tap; it does not need acceptance into Homebrew's main
|
|
56
|
+
collection. The formula must point to a tagged release archive with its actual
|
|
57
|
+
SHA-256 checksum.
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
brew tap blackspider-ops/relay https://github.com/blackspider-ops/relay
|
|
61
|
+
brew install blackspider-ops/relay/relay-codex
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For a new release, update the package and Python versions together, run the checks,
|
|
65
|
+
tag it, upload the archive, then update the formula URL and checksum. Keep the
|
|
66
|
+
previous release available for users who need to roll back.
|
|
67
|
+
|
|
68
|
+
Official references: [trusted publishing](https://docs.npmjs.com/trusted-publishers/),
|
|
69
|
+
[package publishing verification](https://docs.npmjs.com/requiring-2fa-for-package-publishing-and-settings-modification/),
|
|
70
|
+
[npm publishing](https://docs.npmjs.com/creating-and-publishing-unscoped-public-packages),
|
|
71
|
+
[npx](https://docs.npmjs.com/cli/v11/commands/npx/), and
|
|
72
|
+
[Homebrew taps](https://docs.brew.sh/How-to-Create-and-Maintain-a-Tap).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
**“This conversation is open in another app.”**
|
|
4
|
+
|
|
5
|
+
Close or unload that conversation in its original app before resuming it. Or fork
|
|
6
|
+
it into a separate conversation. For a Relay-owned task, use
|
|
7
|
+
`relay attach --takeover SESSION_ID`.
|
|
8
|
+
|
|
9
|
+
**A session needs attention.**
|
|
10
|
+
|
|
11
|
+
Run `relay status`, then `relay events`. Once the cause is resolved, use
|
|
12
|
+
`relay restart SESSION_ID` if the worker stopped, or `relay retry SESSION_ID` if it
|
|
13
|
+
is still running and you have reviewed the interrupted work.
|
|
14
|
+
|
|
15
|
+
**Setup was interrupted.**
|
|
16
|
+
|
|
17
|
+
Run the command again. Relay retries unfinished dependency installation. If Python
|
|
18
|
+
is missing or too old, install it with `brew install python@3.13`.
|
|
19
|
+
|
|
20
|
+
**An account needs another sign-in.**
|
|
21
|
+
|
|
22
|
+
Move its live tasks to another account, then run `relay account reauth ACCOUNT`.
|
|
23
|
+
Other accounts can keep working.
|
|
24
|
+
|
|
25
|
+
**The terminal disconnected after a worker restart.**
|
|
26
|
+
|
|
27
|
+
Relay reconnects the terminal when a replacement worker publishes a recovered
|
|
28
|
+
connection. If recovery requires review or the project folder is unavailable,
|
|
29
|
+
check `relay status` and `relay events`, then reattach with `relay attach SESSION_ID`.
|
|
30
|
+
|
|
31
|
+
**Something still seems wrong.**
|
|
32
|
+
|
|
33
|
+
Run `relay doctor --self-test`. Include the Relay and Codex versions when reporting
|
|
34
|
+
a bug. Do not post auth files, tokens, connection files, or private task prompts.
|
|
35
|
+
|
|
36
|
+
Long runs, physical sleep/wake cycles, real revoked logins, and future Codex
|
|
37
|
+
versions need broader live testing. The tests do not remove those limits.
|
|
38
|
+
|
|
39
|
+
**The dashboard repeats or an email is missing.**
|
|
40
|
+
|
|
41
|
+
Check `relay --version` and update Relay. Watch now prints only meaningful changes
|
|
42
|
+
from local state, without using model allowance. Earlier enrolled accounts can be
|
|
43
|
+
identified with `relay account identify`; this reads saved login metadata locally.
|
|
44
|
+
|
|
45
|
+
**Why does npm ask for authentication?**
|
|
46
|
+
|
|
47
|
+
That verification protects package publishing. Installing and running the public
|
|
48
|
+
Relay package does not require npm sign-in. Maintainers can use the repository's
|
|
49
|
+
trusted-publishing workflow once npm has authorized that workflow.
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "relay-codex",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Keep your Codex sessions moving between your own accounts.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "blackspider-ops",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/blackspider-ops/relay.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/blackspider-ops/relay#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/blackspider-ops/relay/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"relay": "bin/relay.cjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/*.cjs",
|
|
20
|
+
"python/*.py",
|
|
21
|
+
"python/*.swift",
|
|
22
|
+
"python/*.lock",
|
|
23
|
+
"python/*.txt",
|
|
24
|
+
"tests/*.py",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"docs/*.md",
|
|
28
|
+
"assets/*.svg"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"os": [
|
|
34
|
+
"darwin"
|
|
35
|
+
],
|
|
36
|
+
"keywords": [
|
|
37
|
+
"codex",
|
|
38
|
+
"terminal",
|
|
39
|
+
"accounts",
|
|
40
|
+
"macos"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "node --test tests/bootstrap.test.cjs",
|
|
44
|
+
"check": "node scripts/check-release.cjs"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
}
|
|
49
|
+
}
|