claude-multiacc 2.0.37 → 2.0.39
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 +84 -6
- package/bin/claude +122 -6
- package/bin/claude-accounts +146 -9
- package/bin/codex +116 -7
- package/bin/codex-accounts +137 -5
- package/docs/ACCOUNT_OPERATIONS.md +34 -0
- package/docs/CODEX.md +29 -2
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/lib/claude_reset.py +70 -35
- package/lib/codex_reset.py +31 -4
- package/lib/codex_reset_telemetry.py +4 -2
- package/lib/common.sh +19 -0
- package/lib/mcp_registry.py +1372 -0
- package/package.json +2 -2
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/run-tests.sh +588 -0
- package/tests/test_account_seeding.py +40 -0
- package/tests/test_claude_reset.py +61 -11
- package/tests/test_codex_reset.py +43 -2
- package/tests/test_mcp_registry.py +597 -0
package/bin/codex
CHANGED
|
@@ -73,11 +73,112 @@ REAL="$(find_real)" || {
|
|
|
73
73
|
exit 127
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
if [ "$(uname -s)" = "Darwin" ]; then
|
|
77
|
+
file_mtime() { stat -f %m "$1" 2>/dev/null || echo 0; }
|
|
78
|
+
file_sig() { stat -L -f '%m:%z' "$1" 2>/dev/null || echo '?'; }
|
|
79
|
+
else
|
|
80
|
+
file_mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }
|
|
81
|
+
file_sig() { stat -L -c '%Y:%s' "$1" 2>/dev/null || echo '?'; }
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# ---- MCP servers for every account (lib/mcp_registry.py) -------------------------
|
|
85
|
+
# Codex keeps MCP servers in CODEX_HOME (`<dir>/config.toml`), so under the pool there
|
|
86
|
+
# is one MCP configuration per account — and a stock `codex mcp add` lands in ONE random
|
|
87
|
+
# one. The pool's registry (mcp-servers.json, synced) plus a machine-local overlay
|
|
88
|
+
# (mcp-servers.local.json) are reconciled into the picked account right before exec, so
|
|
89
|
+
# a session gets the same servers whichever account it drew. Bash only on the hot path:
|
|
90
|
+
# python runs when the registry, the overlay or the account's own config changed since
|
|
91
|
+
# the last verified reconcile — the stamp in <dir>/.mcp-applied — and never otherwise.
|
|
92
|
+
# Fail-open throughout: a missing python, lib or a broken registry costs nothing but
|
|
93
|
+
# the servers themselves. Same contract as bin/claude's mcp_ensure.
|
|
94
|
+
MCP_PY="$SELF_DIR/../lib/mcp_registry.py"
|
|
95
|
+
MCP_PYBIN="${CLAUDE_MULTIACC_PYTHON:-python3}"
|
|
96
|
+
mcp_sig() { # $1 acct dir -> "m:z,m:z,m:z" (registry, overlay, account config; - = absent)
|
|
97
|
+
local f out="" s
|
|
98
|
+
for f in "$ACC_ROOT/mcp-servers.json" "$ACC_ROOT/mcp-servers.local.json" "$1/config.toml"; do
|
|
99
|
+
if [ -f "$f" ]; then s="$(file_sig "$f")"; else s="-"; fi
|
|
100
|
+
out="$out${out:+,}$s"
|
|
101
|
+
done
|
|
102
|
+
printf '%s\n' "$out"
|
|
103
|
+
}
|
|
104
|
+
mcp_ensure() { # $1 acct dir — silent, never changes the exit status
|
|
105
|
+
[ "${CODEX_MULTIACC_MCP:-1}" = "0" ] && return 0
|
|
106
|
+
[ -f "$ACC_ROOT/mcp-servers.json" ] || [ -f "$ACC_ROOT/mcp-servers.local.json" ] || return 0
|
|
107
|
+
[ -f "$MCP_PY" ] || return 0
|
|
108
|
+
local stamp=""
|
|
109
|
+
[ -f "$1/.mcp-applied" ] && IFS= read -r stamp < "$1/.mcp-applied" 2>/dev/null
|
|
110
|
+
[ "${stamp:-}" = "$(mcp_sig "$1")" ] && return 0
|
|
111
|
+
"$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex apply --account-dir "$1" \
|
|
112
|
+
--fail-open --quiet >/dev/null 2>&1 </dev/null || true
|
|
113
|
+
return 0
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# `codex mcp add|remove` writes ONE account's config.toml — the one it ran under — and
|
|
117
|
+
# nobody else's. So the shim runs it as a child instead of exec-ing it, then mirrors what
|
|
118
|
+
# it wrote into the registry and every other account (`learn` diffs the account against a
|
|
119
|
+
# snapshot taken just before). The real CLI's own parser and writer do the work; the shim
|
|
120
|
+
# only copies the outcome. Exits with the real binary's status; a mirror failure is one
|
|
121
|
+
# hint line. Returns 1 when this run is not such a command.
|
|
122
|
+
mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled the run
|
|
123
|
+
local d="$1" snap="" rc
|
|
124
|
+
shift
|
|
125
|
+
[ "${1:-}" = "mcp" ] || return 1
|
|
126
|
+
case "${2:-}" in add|remove) ;; *) return 1 ;; esac
|
|
127
|
+
[ "${CODEX_MULTIACC_MCP:-1}" != "0" ] && [ -f "$MCP_PY" ] || return 1
|
|
128
|
+
mcp_ensure "$d"
|
|
129
|
+
snap="$(mktemp "${TMPDIR:-/tmp}/codex-mcp-snap.XXXXXX" 2>/dev/null)" || snap=""
|
|
130
|
+
if [ -n "$snap" ] && ! "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex snapshot \
|
|
131
|
+
--account-dir "$d" > "$snap" 2>/dev/null </dev/null; then
|
|
132
|
+
rm -f "$snap" 2>/dev/null; snap=""
|
|
133
|
+
fi
|
|
134
|
+
"$REAL" "$@"
|
|
135
|
+
rc=$?
|
|
136
|
+
if [ "$rc" -eq 0 ] && [ -n "$snap" ]; then
|
|
137
|
+
"$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex learn --account-dir "$d" \
|
|
138
|
+
--before "$snap" </dev/null \
|
|
139
|
+
|| printf 'codex-multiacc: could not mirror the MCP change to the other accounts — run: codex-accounts mcp apply\n' >&2
|
|
140
|
+
elif [ "$rc" -eq 0 ]; then
|
|
141
|
+
printf 'codex-multiacc: could not mirror the MCP change to the other accounts — run: codex-accounts mcp apply\n' >&2
|
|
142
|
+
fi
|
|
143
|
+
[ -n "$snap" ] && rm -f "$snap" 2>/dev/null
|
|
144
|
+
exit "$rc"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# A pooled SESSION exports CODEX_HOME=<acct> and CODEX_SHIM_ACTIVE=1, and every child
|
|
148
|
+
# process inherits both — so a `codex mcp add` run from INSIDE a session (an agent's
|
|
149
|
+
# Bash tool, `npx appinspire-mcp install` run by an agent) arrives at the fast passthrough
|
|
150
|
+
# below with the account already chosen, not at the selection path where the mirror
|
|
151
|
+
# lives. That is the very write the 2026-09-22 outage was made of, in the place MCP
|
|
152
|
+
# servers are most often installed. The account is known, so the same mirror applies:
|
|
153
|
+
# exits inside when it handled the run; anything else stays byte-identical passthrough.
|
|
154
|
+
mcp_nested_mirror() {
|
|
155
|
+
[ -f "$MANIFEST" ] || return 1
|
|
156
|
+
[ "${CODEX_MULTIACC_DISABLE:-0}" = "1" ] && return 1
|
|
157
|
+
[ "${CODEX_MULTIACC_MCP:-1}" = "0" ] && return 1
|
|
158
|
+
[ -n "${CODEX_HOME:-}" ] || return 1
|
|
159
|
+
local verb="${1:-}" cfg="${CODEX_HOME%/}" d=""
|
|
160
|
+
[ "$verb" = "mcp" ] || return 1
|
|
161
|
+
# A DIRECT child of the pool root, by its given spelling or its physical one (an
|
|
162
|
+
# adopted account is a symlink whose target is the operator's ~/.claude).
|
|
163
|
+
case "$cfg" in
|
|
164
|
+
"$ACC_ROOT"/acct-*) case "${cfg#"$ACC_ROOT"/}" in */*) ;; *) d="$cfg" ;; esac ;;
|
|
165
|
+
esac
|
|
166
|
+
if [ -z "$d" ]; then
|
|
167
|
+
local root_c; root_c="$(canon_path "$ACC_ROOT")"
|
|
168
|
+
case "$(canon_path "$cfg")" in
|
|
169
|
+
"$root_c"/acct-*) case "${cfg##*/}" in acct-*) d="$cfg" ;; esac ;;
|
|
170
|
+
esac
|
|
171
|
+
fi
|
|
172
|
+
[ -n "$d" ] && [ -d "$d" ] || return 1
|
|
173
|
+
mcp_mirror_run "$d" "$@"
|
|
174
|
+
}
|
|
175
|
+
|
|
76
176
|
# Fast passthrough: caller pinned a config dir, addon disabled, recursion guard, or
|
|
77
177
|
# no account data yet. Byte-identical behavior to stock codex.
|
|
78
178
|
if [ -n "${CODEX_HOME:-}" ] \
|
|
79
179
|
|| [ "${CODEX_MULTIACC_DISABLE:-0}" = "1" ] || [ -n "${CODEX_SHIM_ACTIVE:-}" ] \
|
|
80
180
|
|| [ ! -f "$MANIFEST" ]; then
|
|
181
|
+
mcp_nested_mirror "$@" || true
|
|
81
182
|
exec "$REAL" "$@"
|
|
82
183
|
fi
|
|
83
184
|
|
|
@@ -94,13 +195,6 @@ fi
|
|
|
94
195
|
# complaining to stderr.
|
|
95
196
|
case "$CODEX_MULTIACC_THRESHOLD" in ''|*[!0-9]*|??????*) CODEX_MULTIACC_THRESHOLD=90 ;; esac
|
|
96
197
|
|
|
97
|
-
if [ "$(uname -s)" = "Darwin" ]; then
|
|
98
|
-
file_mtime() { stat -f %m "$1" 2>/dev/null || echo 0; }
|
|
99
|
-
else
|
|
100
|
-
file_mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }
|
|
101
|
-
fi
|
|
102
|
-
|
|
103
|
-
|
|
104
198
|
# A number this shim will do ARITHMETIC on: digits only, and short enough that bash
|
|
105
199
|
# cannot go out of range. An over-range value makes `[ x -lt y ]` print
|
|
106
200
|
# "integer expression expected" on stderr — which a service-spawned run must never see —
|
|
@@ -645,6 +739,10 @@ if [ -n "${CODEX_ACCOUNT:-}" ]; then
|
|
|
645
739
|
share_state_index "$d"
|
|
646
740
|
export CODEX_HOME="$d"
|
|
647
741
|
export CODEX_SHIM_ACTIVE=1
|
|
742
|
+
# A pinned account carries every registered MCP server too, and a pinned
|
|
743
|
+
# `codex mcp add|remove` is mirrored like any other (exits inside).
|
|
744
|
+
mcp_ensure "$d"
|
|
745
|
+
mcp_mirror_run "$d" "$@" || true
|
|
648
746
|
exec "$REAL" "$@"
|
|
649
747
|
fi
|
|
650
748
|
sel_log "pin-invalid account=$CODEX_ACCOUNT (no such dir; random fallback)"
|
|
@@ -874,6 +972,8 @@ pick="$PICK_DIR"
|
|
|
874
972
|
# CODEX_ACCOUNT pin deliberately does not: a pin is a caller overriding selection,
|
|
875
973
|
# not a turn in the rotation.
|
|
876
974
|
remember_pick "$pick"
|
|
975
|
+
# The picked account carries every registered MCP server before the client starts.
|
|
976
|
+
mcp_ensure "$pick"
|
|
877
977
|
|
|
878
978
|
# Opportunistic limits refresh: non-blocking, throttled, backgrounded. The windows are
|
|
879
979
|
# deliberately wide (10m, matching the 5m scheduled pass): the usage endpoint rate-limits
|
|
@@ -902,6 +1002,14 @@ sel_log "$acct weekly=$(fresh_field "$pick" weekly_percent || echo '?')%" \
|
|
|
902
1002
|
|
|
903
1003
|
export CODEX_SHIM_ACTIVE=1
|
|
904
1004
|
|
|
1005
|
+
# A `codex mcp add|remove` is run as a child under the picked account and mirrored to
|
|
1006
|
+
# every other one (exits inside); anything else falls through to exec.
|
|
1007
|
+
if [ "${1:-}" = "mcp" ]; then
|
|
1008
|
+
share_state_index "$pick"
|
|
1009
|
+
export CODEX_HOME="$pick"
|
|
1010
|
+
mcp_mirror_run "$pick" "$@" || true
|
|
1011
|
+
fi
|
|
1012
|
+
|
|
905
1013
|
# Auto-retry applies only to `codex exec` runs with an alternative account available,
|
|
906
1014
|
# and only when stdin is finite (tty, regular file, or char device like /dev/null).
|
|
907
1015
|
# A service-spawned pipe that never EOFs must take the plain exec path, or the
|
|
@@ -1033,6 +1141,7 @@ while :; do
|
|
|
1033
1141
|
# The account that actually serves the work is the one the next run should rotate
|
|
1034
1142
|
# away from — not the one that bounced.
|
|
1035
1143
|
remember_pick "$cur"
|
|
1144
|
+
mcp_ensure "$cur"
|
|
1036
1145
|
attempt=2
|
|
1037
1146
|
continue
|
|
1038
1147
|
fi
|
package/bin/codex-accounts
CHANGED
|
@@ -89,8 +89,10 @@ USAGE
|
|
|
89
89
|
`codex exec` per account
|
|
90
90
|
codex-accounts limits [--quiet] [--force] [--json]
|
|
91
91
|
refresh usage windows from the ChatGPT usage endpoint, apply >=90% markers.
|
|
92
|
-
When
|
|
93
|
-
|
|
92
|
+
When the pool has parked the account (a window at/over the threshold, a
|
|
93
|
+
finished limit, or a client-reported limit that lifts more than an hour from
|
|
94
|
+
now), automatically redeems an earned usage-limit reset if the account has
|
|
95
|
+
one, using an idempotent request.
|
|
94
96
|
Auto-refreshes long-expired access tokens via the OAuth refresh-token grant
|
|
95
97
|
(rotated credential is persisted), so idle accounts keep fresh telemetry and
|
|
96
98
|
stay selectable. Skips accounts fetched in the last 45s and honors
|
|
@@ -98,6 +100,32 @@ USAGE
|
|
|
98
100
|
codex-accounts health limits + full verify; logs to health.log
|
|
99
101
|
codex-accounts self-update update this npm/git install; logs to update.log
|
|
100
102
|
codex-accounts post-sync (server side) seed dirs, fix perms, quick verify
|
|
103
|
+
codex-accounts mcp add <name> [--provider claude|codex|both] [--scope user]
|
|
104
|
+
[-e KEY=VAL]... [-H 'Header: v']... [--transport stdio|http]
|
|
105
|
+
-- <command> [args...] (or a URL)
|
|
106
|
+
register an MCP server for EVERY account: writes the pool's mcp-servers.json
|
|
107
|
+
registry ([mcp_servers.<name>] in every account's config.toml), syncs the
|
|
108
|
+
registry to the server and peers (their post-sync applies it). Default
|
|
109
|
+
--provider both: the same server is registered in the claude pool too.
|
|
110
|
+
A stock `codex mcp add|remove` run under the shim is mirrored into the
|
|
111
|
+
registry the same way — it no longer lands in one random account.
|
|
112
|
+
Project scope is claude-only: Codex reads no per-project MCP config.
|
|
113
|
+
-e takes ONE variable per flag (KEY=VAL). Exit 3 = registry saved, but an account
|
|
114
|
+
could not be reconciled (named on stderr): the change still syncs; fix the account
|
|
115
|
+
and run `mcp apply`. On a replica pool the change lands in the machine-local
|
|
116
|
+
overlay instead of the synced registry — make registry changes on the source.
|
|
117
|
+
The codex registry syncs only to codex sync targets; a runner Mac's local-only
|
|
118
|
+
codex pool is fed by app-robot's overlay (`mcp import-local --owner app-robot`).
|
|
119
|
+
codex-accounts mcp add-json <name> '<json>' [--provider ...]
|
|
120
|
+
codex-accounts mcp remove <name> [--provider ...] [--user-only]
|
|
121
|
+
unregister AND retire EVERYWHERE: every account (fleet-wide after sync) drops the
|
|
122
|
+
server, even one that was hand-added to a single account (--user-only spares a
|
|
123
|
+
claude project-local copy in the claude pool).
|
|
124
|
+
codex-accounts mcp list [--json] effective registry (+ machine-local overlay)
|
|
125
|
+
codex-accounts mcp apply [--account-dir DIR]... [--all]
|
|
126
|
+
re-apply the registry now (repair drift)
|
|
127
|
+
codex-accounts mcp import-local --owner NAME (stdin JSON) machine-local overlay for
|
|
128
|
+
a runner daemon; applied here, never synced
|
|
101
129
|
|
|
102
130
|
ENV
|
|
103
131
|
CODEX_ACCOUNTS_ROOT pool root, overriding ~/.codex-accounts — one isolated pool
|
|
@@ -110,6 +138,7 @@ ENV
|
|
|
110
138
|
CODEX_SHIM_RETRY=0 disable the `codex exec` auto-retry
|
|
111
139
|
CODEX_MULTIACC_DISABLE=1 bypass the shim entirely
|
|
112
140
|
CODEX_MULTIACC_AUTO_RESET=0 disable automatic usage-reset redemption
|
|
141
|
+
CODEX_MULTIACC_MCP=0 disable the MCP registry (no reconcile at seed/launch, no mirror)
|
|
113
142
|
EOF
|
|
114
143
|
}
|
|
115
144
|
|
|
@@ -1620,7 +1649,8 @@ for acct in manifest.get('accounts', []):
|
|
|
1620
1649
|
if sessionp is None and live:
|
|
1621
1650
|
sessionp = 0
|
|
1622
1651
|
try:
|
|
1623
|
-
reset_result, reset_view = refresh_reset_credits(d, aid, maxp, data, url, headers, say,
|
|
1652
|
+
reset_result, reset_view = refresh_reset_credits(d, aid, maxp, data, url, headers, say,
|
|
1653
|
+
int(now), threshold)
|
|
1624
1654
|
except Exception as e:
|
|
1625
1655
|
say(f'{aid}: usage reset automation failed unexpectedly ({type(e).__name__}); failing open')
|
|
1626
1656
|
reset_result = {'status': 'error'}
|
|
@@ -1998,6 +2028,14 @@ sync_push_target() {
|
|
|
1998
2028
|
|| fail "cannot reach $server"
|
|
1999
2029
|
rsync -az "$MANIFEST" "$server:$sroot/accounts.json" >>"$ACC_ROOT/sync.log" 2>&1 \
|
|
2000
2030
|
|| fail "manifest push to $server failed"
|
|
2031
|
+
# The pool's MCP registry rides with the manifest; the target's post-sync reconciles it
|
|
2032
|
+
# into every account dir there. A missing local registry leaves the target's alone,
|
|
2033
|
+
# and the machine-local overlay (mcp-servers.local.json, a runner daemon's own view
|
|
2034
|
+
# of THIS Mac) never travels.
|
|
2035
|
+
if [ -f "$ACC_ROOT/mcp-servers.json" ]; then
|
|
2036
|
+
rsync -az "$ACC_ROOT/mcp-servers.json" "$server:$sroot/mcp-servers.json" >>"$ACC_ROOT/sync.log" 2>&1 \
|
|
2037
|
+
|| fail "MCP registry push to $server failed"
|
|
2038
|
+
fi
|
|
2001
2039
|
|
|
2002
2040
|
local id d
|
|
2003
2041
|
for id in $(account_ids); do
|
|
@@ -2185,6 +2223,97 @@ EOF
|
|
|
2185
2223
|
fi
|
|
2186
2224
|
}
|
|
2187
2225
|
|
|
2226
|
+
# MCP servers for EVERY account (lib/mcp_registry.py) — the codex twin of
|
|
2227
|
+
# claude-accounts' cmd_mcp. `add`/`add-json`/`remove` edit this pool's registry under
|
|
2228
|
+
# the mutate lock, reconcile every account's config.toml, auto-sync, and by default
|
|
2229
|
+
# (--provider both) make the same change in the claude pool. `list`/`apply`/
|
|
2230
|
+
# `import-local` act on this pool alone; the overlay `import-local` writes is
|
|
2231
|
+
# machine-local by definition, so it is applied here and never synced.
|
|
2232
|
+
mcp_sibling_root() { # the other provider's pool root, by the same env precedence
|
|
2233
|
+
if [ "$MULTIACC_PROVIDER" = "codex" ]; then
|
|
2234
|
+
printf '%s\n' "${CLAUDE_ACCOUNTS_ROOT:-${CLAUDE_ACCOUNTS_DIR:-$HOME/.claude-accounts}}"
|
|
2235
|
+
else
|
|
2236
|
+
printf '%s\n' "${CODEX_ACCOUNTS_ROOT:-${CODEX_ACCOUNTS_DIR:-$HOME/.codex-accounts}}"
|
|
2237
|
+
fi
|
|
2238
|
+
}
|
|
2239
|
+
cmd_mcp() {
|
|
2240
|
+
require_manifest
|
|
2241
|
+
local sub="${1:-}" provider=both before_dd=1 a rc=0 sibling sibling_root
|
|
2242
|
+
local args=()
|
|
2243
|
+
case "$sub" in
|
|
2244
|
+
add|add-json|remove|list|apply|import-local) shift ;;
|
|
2245
|
+
"") usage; return 1 ;;
|
|
2246
|
+
*) die "unknown mcp subcommand: $sub (add, add-json, remove, list, apply, import-local)" ;;
|
|
2247
|
+
esac
|
|
2248
|
+
[ -f "$LIB_DIR/mcp_registry.py" ] || die "lib/mcp_registry.py is missing from this install"
|
|
2249
|
+
case "$sub" in
|
|
2250
|
+
list|apply|import-local)
|
|
2251
|
+
"$PYBIN" "$LIB_DIR/mcp_registry.py" --root "$ACC_ROOT" --provider "$MULTIACC_PROVIDER" "$sub" "$@"
|
|
2252
|
+
return $? ;;
|
|
2253
|
+
esac
|
|
2254
|
+
# --provider is OUR option: strip it, but only before a literal `--` — after it every
|
|
2255
|
+
# word belongs to the MCP server's own command line and is passed through untouched.
|
|
2256
|
+
while [ $# -gt 0 ]; do
|
|
2257
|
+
a="$1"
|
|
2258
|
+
if [ "$before_dd" = 1 ]; then
|
|
2259
|
+
case "$a" in
|
|
2260
|
+
--) before_dd=0 ;;
|
|
2261
|
+
--provider)
|
|
2262
|
+
[ $# -ge 2 ] || die "--provider needs a value: claude, codex or both"
|
|
2263
|
+
provider="$2"; shift 2; continue ;;
|
|
2264
|
+
--provider=*) provider="${a#--provider=}"; shift; continue ;;
|
|
2265
|
+
esac
|
|
2266
|
+
fi
|
|
2267
|
+
args+=("$a"); shift
|
|
2268
|
+
done
|
|
2269
|
+
case "$provider" in
|
|
2270
|
+
claude|codex|both) ;;
|
|
2271
|
+
*) die "--provider must be claude, codex or both (got '$provider')" ;;
|
|
2272
|
+
esac
|
|
2273
|
+
if [ "$provider" = "$MULTIACC_PROVIDER" ] || [ "$provider" = both ]; then
|
|
2274
|
+
mutate_lock || die "could not acquire the account lock — try again"
|
|
2275
|
+
trap mutate_unlock EXIT
|
|
2276
|
+
"$PYBIN" "$LIB_DIR/mcp_registry.py" --root "$ACC_ROOT" --provider "$MULTIACC_PROVIDER" "$sub" \
|
|
2277
|
+
${args[@]+"${args[@]}"}
|
|
2278
|
+
rc=$?
|
|
2279
|
+
mutate_unlock
|
|
2280
|
+
trap - EXIT
|
|
2281
|
+
# 3 = the registry WAS saved and some account could not be reconciled (a corrupt
|
|
2282
|
+
# config, a layout the TOML editor refuses): the change still has to reach the
|
|
2283
|
+
# server, the peers and the sibling pool, and `mcp apply` repairs the account once
|
|
2284
|
+
# it is fixed. 1/2 = nothing was saved: stop here.
|
|
2285
|
+
case "$rc" in
|
|
2286
|
+
0) ;;
|
|
2287
|
+
3) printf '%s: registry saved; some accounts were not reconciled — run: %s mcp apply\n' \
|
|
2288
|
+
"$PROVIDER_CLI" "$PROVIDER_CLI" >&2 ;;
|
|
2289
|
+
*) return "$rc" ;;
|
|
2290
|
+
esac
|
|
2291
|
+
auto_sync
|
|
2292
|
+
fi
|
|
2293
|
+
if [ "$provider" != "$MULTIACC_PROVIDER" ]; then
|
|
2294
|
+
if [ "$MULTIACC_PROVIDER" = "codex" ]; then sibling=claude; else sibling=codex; fi
|
|
2295
|
+
sibling_root="$(mcp_sibling_root)"
|
|
2296
|
+
# With the DEFAULT (both), a machine without the sibling pool or CLI has nothing to
|
|
2297
|
+
# mirror, and silently so — a claude-only Mac must not warn on every codex
|
|
2298
|
+
# change. An EXPLICIT --provider claude asked for that pool by name: refuse loudly.
|
|
2299
|
+
if [ ! -f "$sibling_root/accounts.json" ]; then
|
|
2300
|
+
[ "$provider" = both ] && return "$rc"
|
|
2301
|
+
die "no $sibling pool on this machine ($sibling_root/accounts.json is missing)"
|
|
2302
|
+
fi
|
|
2303
|
+
if [ ! -x "$BIN_DIR/$sibling-accounts" ]; then
|
|
2304
|
+
[ "$provider" = both ] && return "$rc"
|
|
2305
|
+
die "$sibling-accounts is not installed beside $PROVIDER_CLI"
|
|
2306
|
+
fi
|
|
2307
|
+
# The caller's no-sync request travels with the hop, in the sibling's spelling.
|
|
2308
|
+
if [ "${CODEX_MULTIACC_NO_SYNC:-0}" = "1" ]; then export CLAUDE_MULTIACC_NO_SYNC=1; fi
|
|
2309
|
+
local sib_rc=0
|
|
2310
|
+
MULTIACC_PROVIDER="$sibling" "$BIN_DIR/$sibling-accounts" mcp "$sub" --provider "$sibling" \
|
|
2311
|
+
${args[@]+"${args[@]}"} || sib_rc=$?
|
|
2312
|
+
[ "$rc" -eq 0 ] && rc="$sib_rc"
|
|
2313
|
+
fi
|
|
2314
|
+
return "$rc"
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2188
2317
|
cmd_post_sync() {
|
|
2189
2318
|
require_manifest
|
|
2190
2319
|
local_pool_fixup
|
|
@@ -2303,11 +2432,13 @@ PYEOF
|
|
|
2303
2432
|
# distribution for as long as the arg loops rejected the flag. An UNKNOWN verb must still
|
|
2304
2433
|
# FAIL, or the probe stops meaning what it says — so this list must hold exactly the verbs
|
|
2305
2434
|
# the dispatcher below implements, and a test pins that both ways.
|
|
2306
|
-
_KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove login expired relogin re-login sync configure verify limits post-sync health self-update init-pool"
|
|
2435
|
+
_KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove login expired relogin re-login sync configure verify limits post-sync health self-update init-pool mcp"
|
|
2307
2436
|
case " $_KNOWN_VERBS " in
|
|
2308
2437
|
*" ${1:-help} "*)
|
|
2438
|
+
# ...but never past a literal `--`: from there on the words belong to an MCP
|
|
2439
|
+
# server's own command line (`mcp add … -- node s.js -h 127.0.0.1`).
|
|
2309
2440
|
for _arg in "$@"; do
|
|
2310
|
-
case "$_arg" in --help|-h) usage; exit 0 ;; esac
|
|
2441
|
+
case "$_arg" in --) break ;; --help|-h) usage; exit 0 ;; esac
|
|
2311
2442
|
done ;;
|
|
2312
2443
|
esac
|
|
2313
2444
|
|
|
@@ -2332,6 +2463,7 @@ case "${1:-help}" in
|
|
|
2332
2463
|
health) shift; cmd_health "$@" ;;
|
|
2333
2464
|
self-update) shift; cmd_self_update "$@" ;;
|
|
2334
2465
|
init-pool) shift; cmd_init_pool "$@" ;;
|
|
2466
|
+
mcp) shift; cmd_mcp "$@" ;;
|
|
2335
2467
|
help|--help|-h) usage ;;
|
|
2336
2468
|
*) usage; exit 1 ;;
|
|
2337
2469
|
esac
|
|
@@ -165,6 +165,40 @@ set, which is what prevents two machines from overwriting each other's manifests
|
|
|
165
165
|
Make account changes on the source; sign-ins (`login`/`relogin`) still happen on
|
|
166
166
|
whichever machine needs the credential.
|
|
167
167
|
|
|
168
|
+
### MCP servers for every account
|
|
169
|
+
|
|
170
|
+
MCP servers live in the config dir, i.e. per account. Never register one with a stock
|
|
171
|
+
`claude mcp add` on a pool Mac and expect it everywhere — under the shim that command is
|
|
172
|
+
mirrored into the registry (see below), but the registry is the source of truth, and it is
|
|
173
|
+
edited on the **source** machine (a replica's own changes land in its machine-local overlay
|
|
174
|
+
and are not pushed anywhere):
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
claude-accounts mcp add appinspire-mcp -- npx -y appinspire-mcp@latest serve
|
|
178
|
+
# writes ~/.claude-accounts/mcp-servers.json AND ~/.codex-accounts/mcp-servers.json,
|
|
179
|
+
# reconciles every account of both pools, auto-syncs the registry to server + peers
|
|
180
|
+
claude-accounts mcp add --provider claude --scope project --project "$HOME/design-lab/run1" appinspire -e APPINSPIRE_LIBRARY_DIR="$HOME/.appinspire-mcp/library" -- node "$HOME/appinspire-mcp/bin/appinspire-mcp.mjs" serve
|
|
181
|
+
# a checkout, registered for ONE project (claude only: codex reads no project config);
|
|
182
|
+
# one -e per variable, and $HOME rather than ~ inside a value
|
|
183
|
+
claude-accounts mcp remove adspower-local-api # retire EVERYWHERE: user scope + every project entry, hand-added copies too
|
|
184
|
+
claude-accounts mcp remove foo --user-only # user scope only
|
|
185
|
+
claude-accounts mcp list # effective registry (+ machine-local overlay)
|
|
186
|
+
claude-accounts mcp apply # re-apply to every account now (exit 3 from add/remove = do this after fixing the account it named)
|
|
187
|
+
codex-accounts mcp list # the codex pool's view
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
How a fleet Mac receives it: `sync` (and every claude limits pass) pushes the claude
|
|
191
|
+
`mcp-servers.json` next to the manifest to the server and every peer; the target's
|
|
192
|
+
`post-sync` re-seeds its account dirs, which applies the registry; and the shim there
|
|
193
|
+
re-applies it to the picked account right before exec whenever the file changed. The codex
|
|
194
|
+
registry travels only to codex sync targets (the server, gas-mini) — runner Macs' codex pools
|
|
195
|
+
are local-only and are fed by app-robot's runner overlay (`codex-accounts mcp import-local
|
|
196
|
+
--owner app-robot`) or a direct `codex-accounts mcp add --provider codex …` on that Mac. A
|
|
197
|
+
runner daemon's overlay (`<provider>-accounts mcp import-local --owner NAME`, JSON on stdin)
|
|
198
|
+
is applied on the Mac and never synced. `--help` on `mcp` lists every option;
|
|
199
|
+
`CLAUDE_MULTIACC_MCP=0` / `CODEX_MULTIACC_MCP=0` stop the reconcile at seed and launch and the
|
|
200
|
+
shim's mirror (the CLI, sync and fan-out keep working).
|
|
201
|
+
|
|
168
202
|
### Pointing sync somewhere else (or nowhere)
|
|
169
203
|
|
|
170
204
|
The target is no longer baked in. In precedence order:
|
package/docs/CODEX.md
CHANGED
|
@@ -44,8 +44,9 @@ Codex-specific notes:
|
|
|
44
44
|
own public client id) and the rotated credential is persisted 0600. Overrides for
|
|
45
45
|
tests: `CODEX_MULTIACC_TOKEN_URL`, `CODEX_MULTIACC_CLIENT_ID`,
|
|
46
46
|
`CODEX_MULTIACC_USAGE_URL`.
|
|
47
|
-
- **Earned usage resets are redeemed automatically
|
|
48
|
-
|
|
47
|
+
- **Earned usage resets are redeemed automatically for a parked account:** a bucket
|
|
48
|
+
at/over the pool's exclusion threshold (90%), a limit the backend reports finished, or
|
|
49
|
+
an active usage park (a client-reported 429) that lifts more than an hour from now. The limits refresher checks the reset
|
|
49
50
|
credits belonging to that same account, uses the soonest-expiring available credit,
|
|
50
51
|
and writes a per-account/window idempotency key before redemption so a lost response
|
|
51
52
|
or simultaneous fleet poll cannot spend a second reset. Set
|
|
@@ -82,6 +83,32 @@ codex-accounts --help # all account commands and options
|
|
|
82
83
|
claude-multiacc codex status # same status command via the npm wrapper
|
|
83
84
|
```
|
|
84
85
|
|
|
86
|
+
## MCP servers for every account
|
|
87
|
+
|
|
88
|
+
`codex-accounts mcp add|add-json|remove|list|apply|import-local` manages the pool's MCP
|
|
89
|
+
registry (`~/.codex-accounts/mcp-servers.json`); `add`/`remove` default to `--provider both`,
|
|
90
|
+
so the claude pool beside it gets the same change, and `mcp remove` retires the server in
|
|
91
|
+
every account (`--user-only` keeps a claude project-local copy). A stock `codex mcp add|remove`
|
|
92
|
+
run under the shim — or from inside a pooled codex session — is mirrored into the registry and
|
|
93
|
+
every account; on a replica pool the mirror lands in the machine-local overlay instead, so
|
|
94
|
+
make registry changes on the source. The registry holds Claude-style blocks; for codex each
|
|
95
|
+
becomes a `[mcp_servers.<name>]` table in every account's `config.toml`, and unrelated
|
|
96
|
+
tables, their comments and blank lines are left alone: `command`/`args` as is, literal `env`
|
|
97
|
+
values as an inline `env` table, a `KEY=${KEY}` reference as `env_vars = ["KEY"]` (codex
|
|
98
|
+
forwards a variable under its own name only and expands nothing, so `KEY=${OTHER}` and
|
|
99
|
+
`${VAR:-default}` cannot be expressed and are left out with a warning), `headers` on an HTTP
|
|
100
|
+
server as `http_headers` (literal) / `env_http_headers` (references), `Authorization: Bearer
|
|
101
|
+
${VAR}` as `bearer_token_env_var`, and, for a stdio server, `startup_timeout_sec = 60` unless
|
|
102
|
+
the block sets one (a cold `npx` download must not trip codex's 10 s default). Codex-only keys
|
|
103
|
+
codex itself wrote (`oauth`, `enabled_tools`, …) ride along in the registry block and never
|
|
104
|
+
reach `.claude.json`. SSE transports cannot be expressed and are skipped with a warning;
|
|
105
|
+
project scope is claude-only because Codex 0.156 reads no per-project MCP config. Python
|
|
106
|
+
3.11+ is found the same way as for `configure`. `CODEX_MULTIACC_MCP=0` stops the reconcile at
|
|
107
|
+
seed and launch and the mirror; the CLI keeps working. The codex registry syncs only to codex
|
|
108
|
+
sync targets — a runner Mac's local-only codex pool gets its servers from app-robot's runner
|
|
109
|
+
overlay (`mcp import-local --owner app-robot`) or a direct `codex-accounts mcp add
|
|
110
|
+
--provider codex …` there.
|
|
111
|
+
|
|
85
112
|
## Opt-in subagent concurrency policy
|
|
86
113
|
|
|
87
114
|
The addon does not choose delegation policy by default. To manage the concurrent
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|