agent-control-plane 0.1.3 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-control-plane",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Help a repo keep GitHub-driven coding agents running reliably without constant human babysitting",
5
5
  "homepage": "https://github.com/ducminhnguyen0319/agent-control-plane",
6
6
  "bugs": {
@@ -1,110 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5
- SOURCE_HTML="${ROOT_DIR}/tools/architecture/architecture-infographics.html"
6
- OUTPUT_DIR="${ROOT_DIR}/assets/architecture"
7
- PDF_OUT="${OUTPUT_DIR}/agent-control-plane-architecture.pdf"
8
- OVERVIEW_OUT="${OUTPUT_DIR}/overview-infographic.png"
9
- RUNTIME_OUT="${OUTPUT_DIR}/runtime-loop-infographic.png"
10
- LIFECYCLE_OUT="${OUTPUT_DIR}/worker-lifecycle-infographic.png"
11
- STATE_OUT="${OUTPUT_DIR}/state-dashboard-infographic.png"
12
-
13
- require_bin() {
14
- local name="$1"
15
- if ! command -v "$name" >/dev/null 2>&1; then
16
- echo "missing required dependency: $name" >&2
17
- exit 1
18
- fi
19
- }
20
-
21
- require_bin python3
22
- require_bin playwright
23
-
24
- PLAYWRIGHT_CLI="$(command -v playwright)"
25
- PLAYWRIGHT_PACKAGE_ROOT="$(
26
- python3 - "$PLAYWRIGHT_CLI" <<'PY'
27
- import os
28
- import sys
29
-
30
- print(os.path.dirname(os.path.realpath(sys.argv[1])))
31
- PY
32
- )"
33
-
34
- mkdir -p "${OUTPUT_DIR}"
35
- tmpdir="$(mktemp -d)"
36
-
37
- cleanup() {
38
- rm -rf "${tmpdir}"
39
- }
40
- trap cleanup EXIT
41
-
42
- cat >"${tmpdir}/render-architecture.js" <<'EOF'
43
- const fs = require("fs");
44
- const path = require("path");
45
- const { chromium } = require(process.env.PLAYWRIGHT_PACKAGE_ROOT);
46
-
47
- async function screenshotSection(page, selector, filename) {
48
- const element = await page.$(selector);
49
- if (!element) {
50
- throw new Error(`missing section for selector: ${selector}`);
51
- }
52
- await element.screenshot({ path: filename });
53
- }
54
-
55
- (async () => {
56
- const sourceHtml = process.env.ACP_ARCH_SOURCE_HTML;
57
- const pdfOut = process.env.ACP_ARCH_PDF_OUT;
58
- const overviewOut = process.env.ACP_ARCH_OVERVIEW_OUT;
59
- const runtimeOut = process.env.ACP_ARCH_RUNTIME_OUT;
60
- const lifecycleOut = process.env.ACP_ARCH_LIFECYCLE_OUT;
61
- const stateOut = process.env.ACP_ARCH_STATE_OUT;
62
-
63
- const browser = await chromium.launch({ headless: true });
64
- const page = await browser.newPage({
65
- viewport: { width: 1664, height: 964 },
66
- deviceScaleFactor: 1,
67
- colorScheme: "light",
68
- });
69
-
70
- await page.goto(`file://${sourceHtml}`, { waitUntil: "load" });
71
- await page.waitForTimeout(400);
72
-
73
- await screenshotSection(page, "#overview-page", overviewOut);
74
- await screenshotSection(page, "#runtime-loop-page", runtimeOut);
75
- await screenshotSection(page, "#worker-lifecycle-page", lifecycleOut);
76
- await screenshotSection(page, "#state-dashboard-page", stateOut);
77
-
78
- await page.pdf({
79
- path: pdfOut,
80
- printBackground: true,
81
- preferCSSPageSize: true,
82
- margin: {
83
- top: "0in",
84
- right: "0in",
85
- bottom: "0in",
86
- left: "0in",
87
- },
88
- });
89
-
90
- await browser.close();
91
- })().catch((error) => {
92
- console.error(error);
93
- process.exit(1);
94
- });
95
- EOF
96
-
97
- PLAYWRIGHT_PACKAGE_ROOT="${PLAYWRIGHT_PACKAGE_ROOT}" \
98
- ACP_ARCH_SOURCE_HTML="${SOURCE_HTML}" \
99
- ACP_ARCH_PDF_OUT="${PDF_OUT}" \
100
- ACP_ARCH_OVERVIEW_OUT="${OVERVIEW_OUT}" \
101
- ACP_ARCH_RUNTIME_OUT="${RUNTIME_OUT}" \
102
- ACP_ARCH_LIFECYCLE_OUT="${LIFECYCLE_OUT}" \
103
- ACP_ARCH_STATE_OUT="${STATE_OUT}" \
104
- node "${tmpdir}/render-architecture.js"
105
-
106
- echo "ARCHITECTURE_PDF=${PDF_OUT}"
107
- echo "ARCHITECTURE_OVERVIEW_PNG=${OVERVIEW_OUT}"
108
- echo "ARCHITECTURE_RUNTIME_PNG=${RUNTIME_OUT}"
109
- echo "ARCHITECTURE_LIFECYCLE_PNG=${LIFECYCLE_OUT}"
110
- echo "ARCHITECTURE_STATE_PNG=${STATE_OUT}"
@@ -1,333 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5
- OUTPUT_DIR="${ROOT_DIR}/assets/readme"
6
- PNG_OUT="${OUTPUT_DIR}/dashboard-demo.png"
7
- GIF_OUT="${OUTPUT_DIR}/dashboard-demo.gif"
8
-
9
- require_bin() {
10
- local name="$1"
11
- if ! command -v "$name" >/dev/null 2>&1; then
12
- echo "missing required dependency: $name" >&2
13
- exit 1
14
- fi
15
- }
16
-
17
- require_bin python3
18
- require_bin ffmpeg
19
- require_bin playwright
20
- require_bin curl
21
-
22
- PLAYWRIGHT_CLI="$(command -v playwright)"
23
- PLAYWRIGHT_PACKAGE_ROOT="$(
24
- python3 - "$PLAYWRIGHT_CLI" <<'PY'
25
- import os
26
- import sys
27
-
28
- print(os.path.dirname(os.path.realpath(sys.argv[1])))
29
- PY
30
- )"
31
-
32
- tmpdir="$(mktemp -d)"
33
- server_pid=""
34
- controller_pid=""
35
- port="$(
36
- python3 - <<'PY'
37
- import socket
38
-
39
- sock = socket.socket()
40
- sock.bind(("127.0.0.1", 0))
41
- print(sock.getsockname()[1])
42
- sock.close()
43
- PY
44
- )"
45
-
46
- cleanup() {
47
- if [[ -n "${server_pid}" ]]; then
48
- kill "${server_pid}" >/dev/null 2>&1 || true
49
- wait "${server_pid}" 2>/dev/null || true
50
- fi
51
- if [[ -n "${controller_pid}" ]]; then
52
- kill "${controller_pid}" >/dev/null 2>&1 || true
53
- wait "${controller_pid}" 2>/dev/null || true
54
- fi
55
- rm -rf "${tmpdir}"
56
- }
57
- trap cleanup EXIT
58
-
59
- profile_registry_root="${tmpdir}/profiles"
60
- profile_dir="${profile_registry_root}/demo-retail"
61
- runs_root="${tmpdir}/runtime/demo-retail/runs"
62
- state_root="${tmpdir}/runtime/demo-retail/state"
63
- frames_dir="${tmpdir}/frames"
64
- mkdir -p \
65
- "${profile_dir}" \
66
- "${runs_root}/demo-issue-14" \
67
- "${runs_root}/demo-issue-17" \
68
- "${runs_root}/demo-issue-21" \
69
- "${state_root}/resident-workers/issues/14" \
70
- "${state_root}/resident-workers/issues/issue-lane-recurring-general-openclaw-safe" \
71
- "${state_root}/retries/providers" \
72
- "${state_root}/scheduled-issues" \
73
- "${state_root}/resident-workers/issue-queue/pending" \
74
- "${frames_dir}" \
75
- "${OUTPUT_DIR}"
76
-
77
- sleep 600 &
78
- controller_pid="$!"
79
-
80
- cat >"${profile_dir}/control-plane.yaml" <<EOF
81
- schema_version: "1"
82
- id: "demo-retail"
83
- repo:
84
- slug: "example/retail-agent-demo"
85
- root: "${tmpdir}/repo"
86
- default_branch: "main"
87
- runtime:
88
- orchestrator_agent_root: "${tmpdir}/runtime/demo-retail"
89
- worktree_root: "${tmpdir}/worktrees"
90
- agent_repo_root: "${tmpdir}/repo"
91
- runs_root: "${runs_root}"
92
- state_root: "${state_root}"
93
- history_root: "${tmpdir}/runtime/demo-retail/history"
94
- retained_repo_root: "${tmpdir}/repo"
95
- vscode_workspace_file: "${tmpdir}/demo-retail.code-workspace"
96
- session_naming:
97
- issue_prefix: "demo-issue-"
98
- pr_prefix: "demo-pr-"
99
- execution:
100
- coding_worker: "openclaw"
101
- openclaw:
102
- model: "openrouter/sonic"
103
- thinking: "adaptive"
104
- timeout_seconds: 900
105
- EOF
106
-
107
- cat >"${runs_root}/demo-issue-14/run.env" <<'EOF'
108
- TASK_KIND=issue
109
- TASK_ID=14
110
- SESSION=demo-issue-14
111
- MODE=safe
112
- STARTED_AT=2026-03-27T15:00:00Z
113
- CODING_WORKER=openclaw
114
- WORKTREE=/tmp/demo-worktree-14
115
- BRANCH=agent/demo/issue-14
116
- RESIDENT_WORKER_KEY=issue-lane-recurring-general-openclaw-safe
117
- OPENCLAW_MODEL=openrouter/sonic
118
- EOF
119
-
120
- cat >"${runs_root}/demo-issue-14/runner.env" <<'EOF'
121
- RUNNER_STATE=succeeded
122
- THREAD_ID=thread-demo-14
123
- LAST_EXIT_CODE=0
124
- UPDATED_AT=2026-03-27T15:04:00Z
125
- EOF
126
-
127
- cat >"${runs_root}/demo-issue-14/result.env" <<'EOF'
128
- OUTCOME=implemented
129
- ACTION=host-publish-issue-pr
130
- EOF
131
-
132
- cat >"${runs_root}/demo-issue-17/run.env" <<'EOF'
133
- TASK_KIND=issue
134
- TASK_ID=17
135
- SESSION=demo-issue-17
136
- MODE=safe
137
- STARTED_AT=2026-03-27T15:05:00Z
138
- CODING_WORKER=openclaw
139
- WORKTREE=/tmp/demo-worktree-17
140
- BRANCH=agent/demo/issue-17
141
- OPENCLAW_MODEL=openrouter/sonic
142
- EOF
143
-
144
- cat >"${runs_root}/demo-issue-17/runner.env" <<'EOF'
145
- RUNNER_STATE=succeeded
146
- THREAD_ID=thread-demo-17
147
- LAST_EXIT_CODE=0
148
- UPDATED_AT=2026-03-27T15:08:00Z
149
- EOF
150
-
151
- cat >"${runs_root}/demo-issue-17/result.env" <<'EOF'
152
- OUTCOME=reported
153
- ACTION=host-comment-scheduled-report
154
- EOF
155
-
156
- cat >"${runs_root}/demo-issue-21/run.env" <<'EOF'
157
- TASK_KIND=issue
158
- TASK_ID=21
159
- SESSION=demo-issue-21
160
- MODE=safe
161
- STARTED_AT=2026-03-27T15:10:00Z
162
- CODING_WORKER=openclaw
163
- WORKTREE=/tmp/demo-worktree-21
164
- BRANCH=agent/demo/issue-21
165
- OPENCLAW_MODEL=openrouter/sonic
166
- EOF
167
-
168
- cat >"${runs_root}/demo-issue-21/runner.env" <<'EOF'
169
- RUNNER_STATE=succeeded
170
- THREAD_ID=thread-demo-21
171
- LAST_EXIT_CODE=0
172
- UPDATED_AT=2026-03-27T15:12:00Z
173
- EOF
174
-
175
- cat >"${runs_root}/demo-issue-21/result.env" <<'EOF'
176
- OUTCOME=blocked
177
- ACTION=host-comment-blocked
178
- FAILURE_REASON=provider-quota-limit
179
- EOF
180
-
181
- cat >"${state_root}/resident-workers/issues/14/controller.env" <<EOF
182
- ISSUE_ID=14
183
- SESSION=demo-issue-14
184
- CONTROLLER_PID=${controller_pid}
185
- CONTROLLER_MODE=safe
186
- CONTROLLER_LOOP_COUNT=4
187
- CONTROLLER_STATE=waiting-provider
188
- CONTROLLER_REASON=provider-cooldown
189
- ACTIVE_RESIDENT_WORKER_KEY=issue-lane-recurring-general-openclaw-safe
190
- ACTIVE_RESIDENT_LANE_KIND=recurring
191
- ACTIVE_RESIDENT_LANE_VALUE=general
192
- ACTIVE_PROVIDER_BACKEND=openclaw
193
- ACTIVE_PROVIDER_MODEL=openrouter/sonic
194
- PROVIDER_SWITCH_COUNT=1
195
- PROVIDER_FAILOVER_COUNT=1
196
- PROVIDER_WAIT_COUNT=2
197
- PROVIDER_WAIT_TOTAL_SECONDS=45
198
- PROVIDER_LAST_WAIT_SECONDS=21
199
- UPDATED_AT=2026-03-27T15:13:00Z
200
- EOF
201
-
202
- cat >"${state_root}/resident-workers/issues/issue-lane-recurring-general-openclaw-safe/metadata.env" <<'EOF'
203
- RESIDENT_WORKER_KIND=issue
204
- RESIDENT_WORKER_SCOPE=lane
205
- RESIDENT_WORKER_KEY=issue-lane-recurring-general-openclaw-safe
206
- ISSUE_ID=14
207
- CODING_WORKER=openclaw
208
- TASK_COUNT=9
209
- LAST_STATUS=running
210
- LAST_STARTED_AT=2026-03-27T15:00:00Z
211
- LAST_RUN_SESSION=demo-issue-14
212
- LAST_OUTCOME=implemented
213
- LAST_ACTION=host-publish-issue-pr
214
- EOF
215
-
216
- cat >"${state_root}/retries/providers/openclaw-openrouter-sonic.env" <<'EOF'
217
- ATTEMPTS=2
218
- NEXT_ATTEMPT_EPOCH=4102444800
219
- NEXT_ATTEMPT_AT=2100-01-01T00:00:00Z
220
- LAST_REASON=provider-quota-limit
221
- UPDATED_AT=2026-03-27T15:14:00Z
222
- EOF
223
-
224
- cat >"${state_root}/scheduled-issues/17.env" <<'EOF'
225
- INTERVAL_SECONDS=1800
226
- LAST_STARTED_AT=2026-03-27T15:05:00Z
227
- NEXT_DUE_AT=2026-03-27T15:35:00Z
228
- UPDATED_AT=2026-03-27T15:05:00Z
229
- EOF
230
-
231
- cat >"${state_root}/scheduled-issues/44.env" <<'EOF'
232
- INTERVAL_SECONDS=3600
233
- LAST_STARTED_AT=2026-03-27T14:30:00Z
234
- NEXT_DUE_AT=2026-03-27T15:30:00Z
235
- UPDATED_AT=2026-03-27T14:30:00Z
236
- EOF
237
-
238
- cat >"${state_root}/resident-workers/issue-queue/pending/issue-27.env" <<'EOF'
239
- ISSUE_ID=27
240
- SESSION=demo-issue-27
241
- UPDATED_AT=2026-03-27T15:14:00Z
242
- EOF
243
-
244
- cat >"${state_root}/resident-workers/issue-queue/pending/issue-28.env" <<'EOF'
245
- ISSUE_ID=28
246
- SESSION=demo-issue-28
247
- UPDATED_AT=2026-03-27T15:15:00Z
248
- EOF
249
-
250
- ACP_PROFILE_REGISTRY_ROOT="${profile_registry_root}" \
251
- python3 "${ROOT_DIR}/tools/dashboard/server.py" \
252
- --host 127.0.0.1 \
253
- --port "${port}" \
254
- >"${tmpdir}/server.log" 2>&1 &
255
- server_pid="$!"
256
-
257
- dashboard_url="http://127.0.0.1:${port}"
258
-
259
- for _ in $(seq 1 40); do
260
- if curl -sf "${dashboard_url}/api/snapshot.json" >/dev/null 2>&1; then
261
- break
262
- fi
263
- sleep 0.25
264
- done
265
-
266
- if ! curl -sf "${dashboard_url}/api/snapshot.json" >/dev/null 2>&1; then
267
- cat "${tmpdir}/server.log" >&2
268
- echo "failed to start dashboard demo server" >&2
269
- exit 1
270
- fi
271
-
272
- cat >"${tmpdir}/capture-demo.js" <<'EOF'
273
- const fs = require("fs");
274
- const path = require("path");
275
- const { chromium } = require(process.env.PLAYWRIGHT_PACKAGE_ROOT);
276
-
277
- async function captureFrame(page, target, filename) {
278
- await page.evaluate((top) => window.scrollTo({ top, behavior: "instant" }), target);
279
- await page.waitForTimeout(350);
280
- await page.screenshot({ path: filename });
281
- }
282
-
283
- (async () => {
284
- const pngOut = process.env.ACP_PNG_OUT;
285
- const framesDir = process.env.ACP_FRAMES_DIR;
286
- const url = process.env.ACP_DEMO_URL;
287
-
288
- fs.mkdirSync(path.dirname(pngOut), { recursive: true });
289
- fs.mkdirSync(framesDir, { recursive: true });
290
-
291
- const browser = await chromium.launch({ headless: true });
292
- const page = await browser.newPage({
293
- viewport: { width: 1440, height: 1080 },
294
- deviceScaleFactor: 1,
295
- colorScheme: "light",
296
- });
297
-
298
- await page.goto(url, { waitUntil: "networkidle" });
299
- await page.waitForTimeout(800);
300
- await page.screenshot({ path: pngOut, fullPage: true });
301
-
302
- await captureFrame(page, 0, path.join(framesDir, "frame-00.png"));
303
- await page.click("#refresh-button");
304
- await page.waitForTimeout(500);
305
- await captureFrame(page, 0, path.join(framesDir, "frame-01.png"));
306
- await captureFrame(page, 900, path.join(framesDir, "frame-02.png"));
307
- await captureFrame(page, 1700, path.join(framesDir, "frame-03.png"));
308
- await captureFrame(page, 0, path.join(framesDir, "frame-04.png"));
309
-
310
- await browser.close();
311
- })().catch((error) => {
312
- console.error(error);
313
- process.exit(1);
314
- });
315
- EOF
316
-
317
- PLAYWRIGHT_PACKAGE_ROOT="${PLAYWRIGHT_PACKAGE_ROOT}" \
318
- ACP_DEMO_URL="${dashboard_url}" \
319
- ACP_PNG_OUT="${PNG_OUT}" \
320
- ACP_FRAMES_DIR="${frames_dir}" \
321
- node "${tmpdir}/capture-demo.js"
322
-
323
- ffmpeg \
324
- -y \
325
- -framerate 1.25 \
326
- -i "${frames_dir}/frame-%02d.png" \
327
- -vf "fps=10,scale=1200:-1:flags=lanczos,split[s0][s1];[s0]palettegen=stats_mode=diff[p];[s1][p]paletteuse=dither=bayer" \
328
- "${GIF_OUT}" \
329
- >/dev/null 2>&1
330
-
331
- echo "DASHBOARD_DEMO_URL=${dashboard_url}"
332
- echo "PNG_OUT=${PNG_OUT}"
333
- echo "GIF_OUT=${GIF_OUT}"
@@ -1,451 +0,0 @@
1
- # codex-quota
2
-
3
- Multi-account manager for OpenAI Codex CLI and OpenCode. Add, switch, list, and remove accounts with OAuth browser authentication. Seamlessly switch between both tools with shared credentials.
4
-
5
- Zero dependencies - uses Node.js built-ins only.
6
-
7
- ## Installation
8
-
9
- ```bash
10
- npm install -g codex-quota
11
- ```
12
-
13
- Or with bun:
14
-
15
- ```bash
16
- bun add -g codex-quota
17
- ```
18
-
19
- After installation, both `codex-quota` and `cq` commands are available.
20
-
21
- ## Quick Start
22
-
23
- ```bash
24
- # Add a new account (opens browser for OAuth)
25
- codex-quota codex add personal
26
-
27
- # Add a Claude credential (interactive)
28
- codex-quota claude add work
29
-
30
- # Check quota for all accounts
31
- codex-quota
32
-
33
- # Switch active Codex account
34
- codex-quota codex switch personal
35
-
36
- # Switch Claude credentials
37
- codex-quota claude switch work
38
-
39
- # Sync activeLabel to CLI auth files
40
- codex-quota codex sync
41
- codex-quota claude sync
42
-
43
- # Preview sync without writing files
44
- codex-quota codex sync --dry-run
45
- codex-quota claude sync --dry-run
46
-
47
- # List accounts
48
- codex-quota codex list
49
- codex-quota claude list
50
-
51
- # Remove an account
52
- codex-quota codex remove old-account
53
- codex-quota claude remove old-account
54
- ```
55
-
56
- ## Commands
57
-
58
- Run `codex-quota` with no namespace to check combined Codex + Claude usage.
59
-
60
- ### codex quota
61
-
62
- Check usage quota for Codex accounts.
63
-
64
- ```bash
65
- codex-quota codex quota # All Codex accounts
66
- codex-quota codex quota personal # Specific account
67
- codex-quota codex quota --json # JSON output
68
- ```
69
-
70
- ### claude quota
71
-
72
- Check usage quota for Claude accounts.
73
-
74
- ```bash
75
- codex-quota claude quota # All Claude accounts
76
- codex-quota claude quota work # Specific credential
77
- codex-quota claude quota --json # JSON output
78
- ```
79
-
80
- ### codex add
81
-
82
- Add a new Codex account via OAuth browser authentication.
83
-
84
- ```bash
85
- codex-quota codex add # Label derived from email
86
- codex-quota codex add work # With explicit label
87
- codex-quota codex add --no-browser # Print URL (for SSH/headless)
88
- ```
89
-
90
- ### claude add
91
-
92
- Add a Claude credential interactively.
93
-
94
- ```bash
95
- codex-quota claude add # Prompt for label + credentials
96
- codex-quota claude add work # With explicit label
97
- codex-quota claude add work --json # JSON output
98
- ```
99
-
100
- ### codex switch
101
-
102
- Switch the active account for Codex CLI, OpenCode, and pi.
103
-
104
- ```bash
105
- codex-quota codex switch personal
106
- ```
107
-
108
- When you run `codex switch`:
109
-
110
- 1. **Codex CLI** - Updates `~/.codex/auth.json` with the selected account tokens
111
- 2. **OpenCode** - If `~/.local/share/opencode/auth.json` exists, updates the `openai` provider entry
112
- 3. **pi** - If `~/.pi/agent/auth.json` exists, updates the `openai-codex` provider entry
113
-
114
- It also updates `activeLabel` in `~/.codex-accounts.json` when available.
115
-
116
- ### claude switch
117
-
118
- Switch Claude Code, OpenCode, and pi to a stored Claude credential.
119
-
120
- ```bash
121
- codex-quota claude switch work
122
- ```
123
-
124
- This updates `activeLabel` in `~/.claude-accounts.json` when available. OAuth-based
125
- credentials are required to update CLI auth files.
126
-
127
- ### codex list
128
-
129
- List all Codex accounts from all sources with status indicators.
130
-
131
- ```bash
132
- codex-quota codex list
133
- codex-quota codex list --json
134
- ```
135
-
136
- Output shows:
137
- - `*` = active account (from `activeLabel`)
138
- - `~` = CLI auth account when it diverges from `activeLabel`
139
- - Email, plan type, token expiry
140
- - Source file for each account
141
-
142
- If CLI auth diverges from the tracked `activeLabel`, `list` and `quota` print a warning and
143
- suggest `codex-quota codex sync` to realign.
144
-
145
- ### claude list
146
-
147
- List Claude credentials from `CLAUDE_ACCOUNTS` or `~/.claude-accounts.json`.
148
-
149
- ```bash
150
- codex-quota claude list
151
- codex-quota claude list --json
152
- ```
153
-
154
- Output shows:
155
- - `*` = active account (from `activeLabel`)
156
- - Source file for each credential
157
-
158
- For OAuth-based accounts, `list` and `quota` warn when stored tokens diverge from the
159
- `activeLabel` account. Session-key-only accounts are skipped.
160
-
161
- ### codex remove
162
-
163
- Remove a Codex account from storage.
164
-
165
- ```bash
166
- codex-quota codex remove old-account
167
- ```
168
-
169
- Note: Accounts from `CODEX_ACCOUNTS` env var cannot be removed via CLI.
170
-
171
- ### claude remove
172
-
173
- Remove a Claude credential from storage.
174
-
175
- ```bash
176
- codex-quota claude remove old-account
177
- ```
178
-
179
- Note: Accounts from `CLAUDE_ACCOUNTS` env var cannot be removed via CLI.
180
-
181
- ### codex sync
182
-
183
- Sync the `activeLabel` Codex account to CLI auth files.
184
-
185
- ```bash
186
- codex-quota codex sync
187
- codex-quota codex sync --dry-run
188
- codex-quota codex sync --json
189
- ```
190
-
191
- This updates:
192
- 1. `~/.codex/auth.json`
193
- 2. `~/.local/share/opencode/auth.json` (if it exists)
194
- 3. `~/.pi/agent/auth.json` (if it exists)
195
-
196
- ### claude sync
197
-
198
- Sync the `activeLabel` Claude account to CLI auth files.
199
-
200
- ```bash
201
- codex-quota claude sync
202
- codex-quota claude sync --dry-run
203
- codex-quota claude sync --json
204
- ```
205
-
206
- Only OAuth-based Claude accounts can be synced. Session-key-only accounts are skipped with
207
- a warning.
208
-
209
- ## Options
210
-
211
- | Option | Description |
212
- |--------|-------------|
213
- | `--json` | Output in JSON format |
214
- | `--dry-run` | Preview sync without writing files |
215
- | `--no-browser` | Print auth URL instead of opening browser |
216
- | `--no-color` | Disable colored output |
217
- | `--version, -v` | Show version number |
218
- | `--help, -h` | Show help |
219
-
220
- ## Account Sources
221
-
222
- Accounts are loaded from these locations (in order). Read/write indicates whether the CLI
223
- reads from or writes to each path.
224
-
225
- | Source | Purpose | Read | Write |
226
- |--------|---------|------|-------|
227
- | `CODEX_ACCOUNTS` env var | JSON array of accounts | Yes | No |
228
- | `~/.codex-accounts.json` | Primary multi-account file (shared with OpenCode) | Yes | Yes (`add`, `remove`) |
229
- | `~/.opencode/openai-codex-auth-accounts.json` | OpenCode accounts | Yes | No |
230
- | `~/.codex/auth.json` | Codex CLI single-account (label `codex-cli`) | Yes | Yes (`switch`) |
231
- | `~/.local/share/opencode/auth.json` | OpenCode auth file (`openai` provider) | No | Yes (`switch` if it exists) |
232
- | `~/.pi/agent/auth.json` | pi auth file (`openai-codex` provider) | No | Yes (`switch` if it exists) |
233
-
234
- New accounts added via `codex-quota codex add` are saved to `~/.codex-accounts.json`, which is
235
- shared with OpenCode.
236
-
237
- Claude sources (in order):
238
-
239
- | Source | Purpose | Read | Write |
240
- |--------|---------|------|-------|
241
- | `CLAUDE_ACCOUNTS` env var | JSON array of credentials | Yes | No |
242
- | `~/.claude-accounts.json` | Claude multi-account file | Yes | Yes (`add`, `remove`) |
243
- | `~/.claude/.credentials.json` | Claude Code credentials | Yes | Yes (`switch`, `sync`) |
244
- | `~/.local/share/opencode/auth.json` | OpenCode auth file (`anthropic` provider) | No | Yes (`switch`, `sync` if it exists) |
245
- | `~/.pi/agent/auth.json` | pi auth file (`anthropic` provider) | No | Yes (`switch`, `sync` if it exists) |
246
-
247
- ## Multi-Account JSON Schema
248
-
249
- File: `~/.codex-accounts.json`
250
-
251
- ```json
252
- {
253
- "schemaVersion": 1,
254
- "activeLabel": "personal",
255
- "accounts": [
256
- {
257
- "label": "personal",
258
- "accountId": "chatgpt-account-uuid",
259
- "access": "access-token",
260
- "refresh": "refresh-token",
261
- "idToken": "id-token-or-null",
262
- "expires": 1234567890000
263
- }
264
- ]
265
- }
266
- ```
267
-
268
- | Field | Type | Description |
269
- |-------|------|-------------|
270
- | `schemaVersion` | number | Schema version marker (root field) |
271
- | `activeLabel` | string\|null | Active account label (root field) |
272
- | `label` | string | Unique identifier for the account |
273
- | `accountId` | string | ChatGPT account UUID |
274
- | `access` | string | OAuth access token |
275
- | `refresh` | string | OAuth refresh token |
276
- | `idToken` | string\|null | OAuth ID token (optional, for email extraction) |
277
- | `expires` | number | Token expiry timestamp in milliseconds |
278
-
279
- Root-level fields are preserved on write; unknown root fields are kept intact.
280
-
281
- Claude multi-account files (`~/.claude-accounts.json`) use the same root fields
282
- (`schemaVersion`, `activeLabel`) and store account entries that include a
283
- Claude OAuth token and refresh metadata.
284
-
285
- ## OAuth Flow
286
-
287
- The `codex add` command uses OAuth 2.0 with PKCE for secure browser authentication:
288
-
289
- 1. Generates PKCE code verifier and challenge
290
- 2. Starts local callback server on `http://127.0.0.1:1455`
291
- 3. Opens browser to OpenAI authorization page
292
- 4. User authenticates in browser
293
- 5. Callback server receives authorization code
294
- 6. Exchanges code for tokens using PKCE verifier
295
- 7. Saves tokens to `~/.codex-accounts.json`
296
-
297
- ### Headless/SSH Mode
298
-
299
- In SSH sessions or headless environments (detected via `SSH_CLIENT`, `SSH_TTY`, or missing `DISPLAY`), the auth URL is printed instead of opening a browser:
300
-
301
- ```bash
302
- codex-quota codex add --no-browser
303
- # Prints: Open this URL in your browser: https://auth.openai.com/authorize?...
304
- ```
305
-
306
- Copy the URL to a browser on another machine, complete authentication, and the callback will be received by the local server.
307
-
308
- ## Troubleshooting
309
-
310
- ### Port 1455 in use
311
-
312
- ```
313
- Error: Port 1455 is in use. Close other codex-quota instances and retry.
314
- ```
315
-
316
- Another process is using port 1455. Check for:
317
- - Other `codex-quota codex add` commands running
318
- - OpenCode or Codex CLI auth processes
319
-
320
- Find and kill the process:
321
- ```bash
322
- lsof -i :1455
323
- kill <pid>
324
- ```
325
-
326
- ### SSH/Headless authentication
327
-
328
- If browser doesn't open in SSH session:
329
-
330
- 1. Use `--no-browser` flag: `codex-quota codex add --no-browser`
331
- 2. Copy the printed URL to a browser on another machine
332
- 3. Complete authentication in browser
333
- 4. The callback is received by the server running over SSH
334
-
335
- ### Token refresh failures
336
-
337
- If token refresh fails:
338
- ```
339
- Error: Failed to refresh token. Re-authenticate with 'codex-quota codex add'.
340
- ```
341
-
342
- The refresh token may have expired. Add the account again:
343
- ```bash
344
- codex-quota codex remove expired-account
345
- codex-quota codex add new-label
346
- ```
347
-
348
- ### Environment variable accounts
349
-
350
- Accounts from `CODEX_ACCOUNTS` env var cannot be removed via CLI:
351
- ```
352
- Error: Cannot remove account from CODEX_ACCOUNTS env var. Modify the env var directly.
353
- ```
354
-
355
- Edit your shell configuration to remove the account from the env var.
356
-
357
- ## JSON Output
358
-
359
- All commands support `--json` for scripting:
360
-
361
- ```bash
362
- # Quota (combined)
363
- codex-quota --json
364
- # {"codex":[{"label":"personal","email":"user@example.com","usage":{...}}],"claude":[...]}
365
-
366
- # List (Codex)
367
- codex-quota codex list --json
368
- # {"accounts":[{"label":"personal","isActive":true,"email":"...","source":"..."}]}
369
-
370
- # Add (Codex, success)
371
- codex-quota codex add work --json
372
- # {"success":true,"label":"work","email":"user@example.com","accountId":"...","source":"~/.codex-accounts.json"}
373
-
374
- # Switch (Codex)
375
- codex-quota codex switch personal --json
376
- # {"success":true,"label":"personal","email":"...","authPath":"~/.codex/auth.json"}
377
-
378
- # Sync (Codex)
379
- codex-quota codex sync --json
380
- # {"success":true,"activeLabel":"work","updated":["~/.codex/auth.json",...],"skipped":[...]}
381
-
382
- # Errors include structured data
383
- codex-quota codex switch nonexistent --json
384
- # {"success":false,"error":"Account not found","availableLabels":["personal","work"]}
385
- ```
386
-
387
- ## Claude Code Usage (Optional)
388
-
389
- Use the `claude` namespace to check Claude usage alongside OpenAI quotas:
390
-
391
- ```bash
392
- codex-quota claude quota
393
- ```
394
-
395
- If multiple Claude accounts are configured, each account is fetched and displayed separately.
396
-
397
- To add a Claude credential interactively:
398
-
399
- ```bash
400
- codex-quota claude add
401
- ```
402
-
403
- This uses OAuth credentials to call:
404
- - `https://api.anthropic.com/api/oauth/usage`
405
-
406
- Authentication sources (in order):
407
- 1. `CLAUDE_ACCOUNTS` env var (JSON array or `{ accounts: [...] }`)
408
- 2. `~/.claude-accounts.json` (multi-account format with `oauthToken`)
409
- 3. `~/.claude/.credentials.json` OAuth `accessToken`
410
-
411
- Multi-account format (Claude):
412
- ```json
413
- {
414
- "accounts": [
415
- {
416
- "label": "personal",
417
- "oauthToken": "claude-ai-access-token",
418
- "orgId": "org_uuid_optional"
419
- }
420
- ]
421
- }
422
- ```
423
-
424
- Notes:
425
- - `label` plus `oauthToken` is required.
426
- - `orgId` is optional.
427
-
428
- Environment overrides:
429
- - `CLAUDE_ACCOUNTS` to supply multi-account JSON directly
430
- - `CLAUDE_CREDENTIALS_PATH` to point to a different credentials file
431
-
432
- Codex overrides:
433
- - `CODEX_ACCOUNTS` to supply multi-account JSON directly (read-only)
434
- - `CODEX_AUTH_PATH` to point to a different Codex CLI auth file
435
- - `XDG_DATA_HOME` to relocate OpenCode auth paths
436
- - `PI_AUTH_PATH` to point to a different pi auth file
437
-
438
- Notes:
439
- - Claude usage in the bundled public package is OAuth-only.
440
-
441
- ## Releasing
442
-
443
- - Run `bun test` and `bun run preflight` before publishing.
444
- - Bump version with `bun pm version patch|minor|major`.
445
- - Dry-run the package with `bun run release:pack`.
446
- - Publish with `bun run release:publish` (local publish, no provenance).
447
- - Ensure the git working tree is clean.
448
-
449
- ## License
450
-
451
- MIT