claude-multiacc 2.0.26 → 2.0.28
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 +50 -138
- package/bin/claude-accounts +2 -3
- package/bin/claude-multiacc +3 -0
- package/bin/cli.mjs +16 -18
- package/bin/codex +124 -0
- package/bin/codex-accounts +2 -3
- package/bin/multiacc-select +4 -0
- package/docs/ACCOUNT_OPERATIONS.md +3 -61
- package/docs/CODEX.md +83 -0
- package/docs/INSTALLATION.md +91 -0
- package/docs/UNIFIED_SELECTOR.md +2 -0
- package/docs/VERIFICATION.md +68 -0
- package/install.sh +9 -284
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.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/common.sh +92 -2
- package/lib/install-help.txt +20 -0
- package/lib/install_actions.sh +238 -0
- package/lib/install_calendar.sh +59 -0
- package/package.json +5 -2
- package/tests/__pycache__/packaged_command_support.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/packaged_command_support.py +81 -0
- package/tests/run-tests.sh +295 -2
- package/tests/test_packaged_commands.py +170 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Install / update / uninstall
|
|
2
|
+
|
|
3
|
+
### npm (recommended)
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g claude-multiacc # postinstall wires up the shim, CLI, and scheduled jobs
|
|
7
|
+
# or run it once without installing globally:
|
|
8
|
+
npx claude-multiacc
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`claude-multiacc` then wraps everything:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
claude-multiacc # install or update the addon (idempotent)
|
|
15
|
+
claude-multiacc self-update # update the running installation + re-install
|
|
16
|
+
claude-multiacc list # Claude pool, via claude-accounts
|
|
17
|
+
claude-multiacc codex list # Codex pool, via codex-accounts
|
|
18
|
+
claude-multiacc doctor # status for both pools
|
|
19
|
+
claude-multiacc install --help # installer options
|
|
20
|
+
claude-multiacc uninstall # keep both account pools
|
|
21
|
+
# To delete both pools as well: claude-multiacc uninstall --purge-data
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
After `claude-multiacc uninstall`, `npm uninstall -g claude-multiacc` also removes
|
|
25
|
+
the npm package and its command links.
|
|
26
|
+
|
|
27
|
+
npm links `claude-multiacc`, `claude-accounts`, `codex-accounts`, and `multiacc-select`
|
|
28
|
+
into its global bin directory. If `claude-multiacc` works, both account commands work
|
|
29
|
+
in that same shell, even when postinstall was skipped. The `claude` and `codex` shims
|
|
30
|
+
still need the installer's PATH setup: open a new shell after installation.
|
|
31
|
+
|
|
32
|
+
If an older installation says `codex-accounts: command not found`, update it:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g claude-multiacc@latest
|
|
36
|
+
codex-accounts verify
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Before updating, `claude-multiacc codex verify` runs the same verification through
|
|
40
|
+
the existing npm wrapper. For git/npx installs, open a new shell or run the exact
|
|
41
|
+
`export PATH=...` command printed by the installer. Run `claude-multiacc install`
|
|
42
|
+
if auto-setup was skipped and the pool has no manifest yet.
|
|
43
|
+
|
|
44
|
+
**Automatic updates:** a globally-installed package re-runs `install.sh` from its own
|
|
45
|
+
postinstall, so `npm i -g claude-multiacc@latest` fully updates the shim, CLI, and jobs.
|
|
46
|
+
A daily job (launchd on macOS, cron on Linux, 04:07) runs `claude-accounts self-update`
|
|
47
|
+
to pull the latest release on its own — `npm i -g claude-multiacc@latest` for npm installs,
|
|
48
|
+
`git pull && ./install.sh` for git checkouts; set `CLAUDE_MULTIACC_AUTOUPDATE=0` before install to
|
|
49
|
+
opt out. `update-notifier` also prints a one-line nudge when a newer version is on npm.
|
|
50
|
+
(Note: newer npm versions gate install scripts; if postinstall is blocked, just run
|
|
51
|
+
`claude-multiacc install` once.)
|
|
52
|
+
|
|
53
|
+
New versions publish to npm automatically from `main` via GitHub Actions
|
|
54
|
+
(`.github/workflows/publish.yml`, OIDC Trusted Publishing — no long-lived token after the
|
|
55
|
+
first bootstrap publish; `scripts/auto-version.mjs` bumps the patch above the last release).
|
|
56
|
+
|
|
57
|
+
### From a git checkout
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/gowalk-public/claude-multiacc.git
|
|
61
|
+
cd claude-multiacc
|
|
62
|
+
./install.sh # macOS or Linux, idempotent
|
|
63
|
+
./install.sh --server root@1.2.3.4 # override sync target
|
|
64
|
+
git pull && ./install.sh # update (data untouched)
|
|
65
|
+
./install.sh --uninstall # restores stock behavior, keeps account data
|
|
66
|
+
./install.sh --uninstall --purge-data # also deletes both account pools
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`self-update` auto-detects which of these you used (npm global vs git checkout).
|
|
70
|
+
|
|
71
|
+
**Servers / machines without repo access:** install via **npm** — the registry is
|
|
72
|
+
public, so the daily self-update needs no credentials at all (no rsync, no keys).
|
|
73
|
+
A git checkout self-updates too, but only if `git pull` can authenticate; for a
|
|
74
|
+
private repo on a server that means a read-only deploy key (the 138 server runs
|
|
75
|
+
this way: `core.sshCommand` pinned to its deploy key). A plain copied tree is the
|
|
76
|
+
one layout that can NOT self-update — don't ship the addon that way.
|
|
77
|
+
|
|
78
|
+
What install does (all reversible, nothing else):
|
|
79
|
+
|
|
80
|
+
- **macOS:** marked PATH block at the END of `~/.zshenv`, `~/.zprofile`, `~/.zshrc`
|
|
81
|
+
(+ bash rc files if present) — end-of-file placement matters because those files
|
|
82
|
+
re-prepend `~/.local/bin`; launchd agents `com.claude-multiacc.limits` +
|
|
83
|
+
`.codex-limits` (15m Claude / 5m Codex), `.health` + `.codex-health` (weekly Mon morning), and
|
|
84
|
+
`.update` (daily 04:07). Notes when this Mac keeps Claude Code logins in the
|
|
85
|
+
Keychain (the pool reads them; ssh sessions cannot — mint portable tokens for
|
|
86
|
+
accounts that must work from everywhere).
|
|
87
|
+
- **Linux (root):** PATH block in `~/.bashrc` + `/etc/profile.d/claude-multiacc.sh`,
|
|
88
|
+
shim symlinks at `/usr/local/bin/claude` and `/usr/local/bin/codex` (shadow via
|
|
89
|
+
PATH order — on the systemd default PATH too; the original binaries are untouched),
|
|
90
|
+
cron entries for limits/health (both providers) + the daily auto-update.
|
|
91
|
+
- Both: `~/.claude-accounts/` and `~/.codex-accounts/`, each with an `accounts.json` manifest.
|
package/docs/UNIFIED_SELECTOR.md
CHANGED
|
@@ -13,6 +13,8 @@ multiacc-select --request-json - --response-json -
|
|
|
13
13
|
|
|
14
14
|
The command reads one UTF-8 JSON object from stdin and writes one JSON object to
|
|
15
15
|
stdout. `multiacc-select --version` reports the selector contract version.
|
|
16
|
+
`multiacc-select --help` (or `-h`) prints usage and exits successfully without
|
|
17
|
+
reading stdin. All three forms also work through `claude-multiacc select`.
|
|
16
18
|
|
|
17
19
|
The request schema is `claude-multiacc/pool-selection.v2` and contains:
|
|
18
20
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Verification and troubleshooting
|
|
2
|
+
|
|
3
|
+
## Verification
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
tests/run-tests.sh # sandboxed compatibility tests, no quota
|
|
7
|
+
python3 tests/test_selector.py # unified selector contract
|
|
8
|
+
npm run test:commands # packed npm commands; run npm install --ignore-scripts first
|
|
9
|
+
claude-accounts verify # real matrix: `claude -p "reply OK"` per authed account
|
|
10
|
+
claude-accounts verify --quick # auth presence/expiry only, no inference
|
|
11
|
+
codex-accounts verify # real codex exec call per account
|
|
12
|
+
codex-accounts verify --quick # local Codex auth check, no inference
|
|
13
|
+
claude-accounts limits # live per-bucket usage incl. the Fable bucket
|
|
14
|
+
claude-accounts health # limits + full verify, logs to health.log, notifies on failure (Mac)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Verified end-to-end on both machines (2026-07-13): full matrix PASS, 20-invocation shim
|
|
18
|
+
spread clean, real project `-p` through the shim OK on Mac (`~/pipeline`) and server
|
|
19
|
+
(`/opt/inapp-onboarding`), uninstall→stock→reinstall roundtrip byte-identical, three
|
|
20
|
+
server systemd services (`vpn-server-management`, `inapp-onboarding`, `vpn-seo-panel`)
|
|
21
|
+
repointed via `CLAUDE_BIN=/usr/local/bin/claude` and restarted healthy.
|
|
22
|
+
|
|
23
|
+
## Troubleshooting
|
|
24
|
+
|
|
25
|
+
- **`claude` resolves to the real binary, not the shim** — open a new shell, or check
|
|
26
|
+
that the marked block is the LAST PATH manipulation in your rc file
|
|
27
|
+
(`grep -A2 'claude-multiacc >>>' ~/.zshrc`).
|
|
28
|
+
- **An account never gets picked** — `claude-accounts status`: no auth on this machine,
|
|
29
|
+
a dead login (`selectable: NO`, see `claude-accounts expired`), or an active `.limited`
|
|
30
|
+
marker (shows bucket + minutes to reset).
|
|
31
|
+
- **"Failed to authenticate: OAuth session expired and could not be refreshed"** — the
|
|
32
|
+
login behind that run is dead. Run `claude-accounts expired` to see which, then
|
|
33
|
+
`claude-accounts relogin`. Accounts in that state are excluded from selection
|
|
34
|
+
automatically, so this only surfaces when *every* account needs a re-login (the shim
|
|
35
|
+
then falls back to the machine's own `~/.claude` login) or when it was pinned with
|
|
36
|
+
`CLAUDE_ACCOUNT`.
|
|
37
|
+
- **"Please run /login · API Error: 401 OAuth access token is invalid"** — the portable
|
|
38
|
+
setup-token is rejected even though its presence and `claude auth status` look healthy.
|
|
39
|
+
The shim now runs a private first-use inference, parks a rejected token, and reselects
|
|
40
|
+
before direct, TUI, or `--resume` work sees the 401. Run `claude-accounts verify` to
|
|
41
|
+
check every token immediately; `claude-accounts expired` reports token-only accounts
|
|
42
|
+
as `UNVERIFIED` until that proof exists.
|
|
43
|
+
- **Everything marked limited** — the shim still runs: the still-serving limited accounts
|
|
44
|
+
go through the same two cuts (session gate, then strict best-weekly) and one is handed
|
|
45
|
+
out anyway; check `selection.log` for `all-limited fallback=` lines.
|
|
46
|
+
- **Sync fails** — `tail ~/.claude-accounts/sync.log`; it's ssh/rsync to the manifest's
|
|
47
|
+
`server` (BatchMode — needs key auth).
|
|
48
|
+
- **A service bypasses the shim** — it spawns an absolute path. Point its env
|
|
49
|
+
(`CLAUDE_BIN` etc.) at `/usr/local/bin/claude`. Docker-internal `claude` installs
|
|
50
|
+
(e.g. openclaw live-test containers) are out of scope for a host shim.
|
|
51
|
+
- **`add` sign-in says "Sign in again to continue"** — that's Claude's step-up
|
|
52
|
+
authentication, not a tool error: granting a long-lived token needs a *recent* sign-in.
|
|
53
|
+
Open a fresh incognito window, sign in to the target account at claude.ai **first**,
|
|
54
|
+
then paste the link and approve. The approval scope is `user:inference`
|
|
55
|
+
(*"Contribute to your Claude subscription usage"*) — that's correct and minimal.
|
|
56
|
+
- **Limits look stale** — launchd: `launchctl list | grep claude-multiacc`;
|
|
57
|
+
cron: `crontab -l | grep claude-multiacc`. The shim also kicks a refresh when data
|
|
58
|
+
is >3 min old.
|
|
59
|
+
|
|
60
|
+
## Security notes & known risks
|
|
61
|
+
|
|
62
|
+
- Tokens and credentials live as 0600 files under `~/.claude-accounts` (and
|
|
63
|
+
`/root/.claude-accounts` on the server). Server compromise = account access.
|
|
64
|
+
- `selection.log` records timestamps/account/cwd only — never prompt text.
|
|
65
|
+
- Rotating multiple subscriptions to spread usage may be flagged by anti-abuse systems;
|
|
66
|
+
accounts can be banned for limit circumvention. Known and accepted by the operator.
|
|
67
|
+
- The usage endpoint is the internal one `/usage` consumes; if it changes shape, limit
|
|
68
|
+
tracking fails open (accounts stay available, error-retry is the backstop).
|
package/install.sh
CHANGED
|
@@ -20,6 +20,7 @@ set -u
|
|
|
20
20
|
REPO_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
21
21
|
# shellcheck source=lib/common.sh
|
|
22
22
|
. "$REPO_DIR/lib/common.sh"
|
|
23
|
+
. "$REPO_DIR/lib/install_actions.sh"
|
|
23
24
|
# common.sh is sourced in claude mode (ACC_ROOT = the claude pool); the codex pool
|
|
24
25
|
# root is needed here too for the codex agents and uninstall/purge messaging.
|
|
25
26
|
CODEX_ACC_ROOT="${CODEX_ACCOUNTS_ROOT:-${CODEX_ACCOUNTS_DIR:-$HOME/.codex-accounts}}"
|
|
@@ -31,6 +32,7 @@ NO_SCHEDULE=0
|
|
|
31
32
|
INSTANCE_OVERRIDE=""
|
|
32
33
|
while [ $# -gt 0 ]; do
|
|
33
34
|
case "$1" in
|
|
35
|
+
--help|-h) cat "$REPO_DIR/lib/install-help.txt"; exit 0 ;;
|
|
34
36
|
--uninstall) UNINSTALL=1; shift ;;
|
|
35
37
|
--purge-data) PURGE=1; shift ;;
|
|
36
38
|
--server) SERVER_OVERRIDE="${2:?--server requires a value}"; shift 2 ;;
|
|
@@ -127,50 +129,9 @@ cron_env_prefix() {
|
|
|
127
129
|
printf "CLAUDE_ACCOUNTS_ROOT='%s' CODEX_ACCOUNTS_ROOT='%s' " "$ACC_ROOT" "$CODEX_ACC_ROOT"
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
is_pool_root() { # true when $1 is safe to delete as an account pool
|
|
131
|
-
case "$1" in
|
|
132
|
-
''|/|"$HOME") return 1 ;;
|
|
133
|
-
*/.claude-accounts|*/.codex-accounts) return 0 ;;
|
|
134
|
-
/*) [ -f "$1/accounts.json" ] ;;
|
|
135
|
-
*) return 1 ;;
|
|
136
|
-
esac
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
strip_block() { # remove our marked block from a file (portable, no sed -i)
|
|
140
|
-
local f="$1"
|
|
141
|
-
[ -f "$f" ] || return 0
|
|
142
|
-
# Unmatched begin marker (end line hand-deleted): stripping would eat the rest
|
|
143
|
-
# of the rc file. Leave it alone and say so.
|
|
144
|
-
if grep -qF "$MARK_BEGIN" "$f" && ! grep -qF "$MARK_END" "$f"; then
|
|
145
|
-
echo " WARNING: $f has an unterminated claude-multiacc block — fix it by hand; not touching this file" >&2
|
|
146
|
-
return 1
|
|
147
|
-
fi
|
|
148
|
-
awk -v b="$MARK_BEGIN" -v e="$MARK_END" '
|
|
149
|
-
$0 == b { skip = 1; next }
|
|
150
|
-
$0 == e { skip = 0; next }
|
|
151
|
-
!skip { print }
|
|
152
|
-
' "$f" > "$f.claude-multiacc.tmp" && mv "$f.claude-multiacc.tmp" "$f"
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
path_block_body() {
|
|
156
|
-
# Move (not just add) the shim dir to the front: later rc lines prepend
|
|
157
|
-
# ~/.local/bin, so a plain add-once guard would leave the real binary first.
|
|
158
|
-
printf 'PATH="$(printf %%s ":$PATH:" | sed '\''s|:%s/bin:|:|g; s|^:||; s|:$||'\'')"\n' "$REPO_DIR"
|
|
159
|
-
printf 'export PATH="%s/bin:$PATH"\n' "$REPO_DIR"
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
append_block() { # strip then append our PATH block to a file
|
|
163
|
-
local f="$1"
|
|
164
|
-
strip_block "$f" || return 1 # never create a second block next to a broken one
|
|
165
|
-
{
|
|
166
|
-
printf '%s\n' "$MARK_BEGIN"
|
|
167
|
-
path_block_body
|
|
168
|
-
printf '%s\n' "$MARK_END"
|
|
169
|
-
} >> "$f"
|
|
170
|
-
}
|
|
171
|
-
|
|
172
132
|
mac_schedule_install() {
|
|
173
133
|
mkdir -p "$HOME/Library/LaunchAgents"
|
|
134
|
+
. "$REPO_DIR/lib/install_calendar.sh"
|
|
174
135
|
cat > "$PLIST_LIMITS" <<EOF
|
|
175
136
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
176
137
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -183,52 +144,12 @@ mac_schedule_install() {
|
|
|
183
144
|
</array>
|
|
184
145
|
$(plist_env_block) <key>StartInterval</key><integer>900</integer>
|
|
185
146
|
<key>RunAtLoad</key><true/>
|
|
186
|
-
<!--
|
|
187
|
-
must not kill that push with the job's process group. -->
|
|
147
|
+
<!-- Preserve the detached telemetry push after this job exits. -->
|
|
188
148
|
<key>AbandonProcessGroup</key><true/>
|
|
189
149
|
<key>StandardOutPath</key><string>/dev/null</string>
|
|
190
150
|
<key>StandardErrorPath</key><string>/dev/null</string>
|
|
191
151
|
</dict></plist>
|
|
192
152
|
EOF
|
|
193
|
-
cat > "$PLIST_HEALTH" <<EOF
|
|
194
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
195
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
196
|
-
<plist version="1.0"><dict>
|
|
197
|
-
<key>Label</key><string>$LABEL.health</string>
|
|
198
|
-
<key>ProgramArguments</key><array>
|
|
199
|
-
<string>$REPO_DIR/bin/claude-accounts</string>
|
|
200
|
-
<string>health</string>
|
|
201
|
-
</array>
|
|
202
|
-
$(plist_env_block) <key>StartCalendarInterval</key><dict>
|
|
203
|
-
<key>Weekday</key><integer>1</integer>
|
|
204
|
-
<key>Hour</key><integer>9</integer>
|
|
205
|
-
<key>Minute</key><integer>17</integer>
|
|
206
|
-
</dict>
|
|
207
|
-
<key>StandardOutPath</key><string>/dev/null</string>
|
|
208
|
-
<key>StandardErrorPath</key><string>/dev/null</string>
|
|
209
|
-
</dict></plist>
|
|
210
|
-
EOF
|
|
211
|
-
# Daily auto-update (04:07). No-op unless this is an npm or git install.
|
|
212
|
-
cat > "$PLIST_UPDATE" <<EOF
|
|
213
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
214
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
215
|
-
<plist version="1.0"><dict>
|
|
216
|
-
<key>Label</key><string>$LABEL.update</string>
|
|
217
|
-
<key>ProgramArguments</key><array>
|
|
218
|
-
<string>$REPO_DIR/bin/claude-accounts</string>
|
|
219
|
-
<string>self-update</string>
|
|
220
|
-
<string>--quiet</string>
|
|
221
|
-
</array>
|
|
222
|
-
$(plist_env_block) <key>StartCalendarInterval</key><dict>
|
|
223
|
-
<key>Hour</key><integer>4</integer>
|
|
224
|
-
<key>Minute</key><integer>7</integer>
|
|
225
|
-
</dict>
|
|
226
|
-
<key>StandardOutPath</key><string>/dev/null</string>
|
|
227
|
-
<key>StandardErrorPath</key><string>/dev/null</string>
|
|
228
|
-
</dict></plist>
|
|
229
|
-
EOF
|
|
230
|
-
# Codex pool: its own limits/health agents, so either provider's telemetry can
|
|
231
|
-
# fail or be removed without touching the other's.
|
|
232
153
|
cat > "$PLIST_CODEX_LIMITS" <<EOF
|
|
233
154
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
234
155
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -246,30 +167,11 @@ $(plist_env_block) <key>StartInterval</key><integer>300</integer>
|
|
|
246
167
|
<key>StandardErrorPath</key><string>/dev/null</string>
|
|
247
168
|
</dict></plist>
|
|
248
169
|
EOF
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
<key>ProgramArguments</key><array>
|
|
255
|
-
<string>$REPO_DIR/bin/codex-accounts</string>
|
|
256
|
-
<string>health</string>
|
|
257
|
-
</array>
|
|
258
|
-
$(plist_env_block) <key>StartCalendarInterval</key><dict>
|
|
259
|
-
<key>Weekday</key><integer>1</integer>
|
|
260
|
-
<key>Hour</key><integer>9</integer>
|
|
261
|
-
<key>Minute</key><integer>37</integer>
|
|
262
|
-
</dict>
|
|
263
|
-
<key>StandardOutPath</key><string>/dev/null</string>
|
|
264
|
-
<key>StandardErrorPath</key><string>/dev/null</string>
|
|
265
|
-
</dict></plist>
|
|
266
|
-
EOF
|
|
267
|
-
launchctl unload "$PLIST_LIMITS" 2>/dev/null || true
|
|
268
|
-
launchctl unload "$PLIST_HEALTH" 2>/dev/null || true
|
|
269
|
-
launchctl unload "$PLIST_UPDATE" 2>/dev/null || true
|
|
270
|
-
launchctl unload "$PLIST_CODEX_LIMITS" 2>/dev/null || true
|
|
271
|
-
launchctl unload "$PLIST_CODEX_HEALTH" 2>/dev/null || true
|
|
272
|
-
launchctl load -w "$PLIST_LIMITS" 2>/dev/null || echo " (launchctl load limits agent failed — limits refresh will rely on the shim's opportunistic kick)"
|
|
170
|
+
local plist
|
|
171
|
+
for plist in "$PLIST_LIMITS" "$PLIST_HEALTH" "$PLIST_UPDATE" "$PLIST_CODEX_LIMITS" "$PLIST_CODEX_HEALTH"; do
|
|
172
|
+
launchctl unload "$plist" 2>/dev/null || true
|
|
173
|
+
done
|
|
174
|
+
launchctl load -w "$PLIST_LIMITS" 2>/dev/null || echo " (limits agent failed — the shim will refresh on demand)"
|
|
273
175
|
launchctl load -w "$PLIST_HEALTH" 2>/dev/null || true
|
|
274
176
|
launchctl load -w "$PLIST_CODEX_LIMITS" 2>/dev/null || true
|
|
275
177
|
launchctl load -w "$PLIST_CODEX_HEALTH" 2>/dev/null || true
|
|
@@ -317,183 +219,6 @@ linux_schedule_remove() {
|
|
|
317
219
|
rm -f "$tmp"
|
|
318
220
|
}
|
|
319
221
|
|
|
320
|
-
do_uninstall() {
|
|
321
|
-
echo "claude-multiacc: uninstalling (restoring stock behavior)"
|
|
322
|
-
# An instance install never wrote a shell rc block, a /etc/profile.d file or the
|
|
323
|
-
# /usr/local/bin shims — those belong to the DEFAULT install and are shared with it.
|
|
324
|
-
# Removing them here would disable the operator's shim while only one instance was
|
|
325
|
-
# being uninstalled, so an instance removes exactly its own agents.
|
|
326
|
-
if [ -n "$INSTANCE" ]; then
|
|
327
|
-
echo " instance $INSTANCE: removing its agents only (PATH block and shims belong to the default install)"
|
|
328
|
-
else
|
|
329
|
-
strip_block "$HOME/.zshenv"
|
|
330
|
-
strip_block "$HOME/.zprofile"
|
|
331
|
-
strip_block "$HOME/.zshrc"
|
|
332
|
-
strip_block "$HOME/.bashrc"
|
|
333
|
-
strip_block "$HOME/.bash_profile"
|
|
334
|
-
strip_block "$HOME/.profile"
|
|
335
|
-
fi
|
|
336
|
-
if [ "$(machine_kind)" = "mac" ]; then
|
|
337
|
-
mac_schedule_remove
|
|
338
|
-
else
|
|
339
|
-
linux_schedule_remove
|
|
340
|
-
if [ -z "$INSTANCE" ]; then
|
|
341
|
-
[ -f "$PROFILED" ] && rm -f "$PROFILED"
|
|
342
|
-
if [ -L /usr/local/bin/claude ] && [ "$(canon_path /usr/local/bin/claude)" = "$(canon_path "$REPO_DIR/bin/claude")" ]; then
|
|
343
|
-
rm -f /usr/local/bin/claude
|
|
344
|
-
echo " removed /usr/local/bin/claude shim symlink"
|
|
345
|
-
fi
|
|
346
|
-
if [ -L /usr/local/bin/codex ] && [ "$(canon_path /usr/local/bin/codex)" = "$(canon_path "$REPO_DIR/bin/codex")" ]; then
|
|
347
|
-
rm -f /usr/local/bin/codex
|
|
348
|
-
echo " removed /usr/local/bin/codex shim symlink"
|
|
349
|
-
fi
|
|
350
|
-
fi
|
|
351
|
-
fi
|
|
352
|
-
local root
|
|
353
|
-
for root in "$ACC_ROOT" "$CODEX_ACC_ROOT"; do
|
|
354
|
-
if [ "$PURGE" = "1" ]; then
|
|
355
|
-
# Either the conventional location, or a directory that is provably a pool (it
|
|
356
|
-
# holds a manifest) — an instance root lives anywhere, so the manifest is what
|
|
357
|
-
# makes `rm -rf` safe. Anything else is left alone.
|
|
358
|
-
if is_pool_root "$root"; then
|
|
359
|
-
rm -rf "$root"; echo " purged $root"
|
|
360
|
-
else
|
|
361
|
-
echo " refusing to purge unusual accounts root: $root" >&2
|
|
362
|
-
fi
|
|
363
|
-
fi
|
|
364
|
-
done
|
|
365
|
-
if [ "$PURGE" != "1" ]; then
|
|
366
|
-
echo " account data kept at $ACC_ROOT and $CODEX_ACC_ROOT (use --purge-data to remove)"
|
|
367
|
-
fi
|
|
368
|
-
echo "uninstall complete — stock claude behavior restored"
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
do_install() {
|
|
372
|
-
local kind
|
|
373
|
-
kind="$(machine_kind)"
|
|
374
|
-
echo "claude-multiacc: installing (mode: $kind, repo: $REPO_DIR)"
|
|
375
|
-
if [ -n "$INSTANCE" ]; then
|
|
376
|
-
echo " instance: $INSTANCE (agents labelled $LABEL.*, pools $ACC_ROOT + $CODEX_ACC_ROOT)"
|
|
377
|
-
fi
|
|
378
|
-
|
|
379
|
-
[ -x "$REPO_DIR/bin/claude" ] || chmod +x "$REPO_DIR/bin/claude" 2>/dev/null || true
|
|
380
|
-
[ -x "$REPO_DIR/bin/claude-accounts" ] || chmod +x "$REPO_DIR/bin/claude-accounts" 2>/dev/null || true
|
|
381
|
-
[ -x "$REPO_DIR/bin/codex" ] || chmod +x "$REPO_DIR/bin/codex" 2>/dev/null || true
|
|
382
|
-
[ -x "$REPO_DIR/bin/codex-accounts" ] || chmod +x "$REPO_DIR/bin/codex-accounts" 2>/dev/null || true
|
|
383
|
-
|
|
384
|
-
local real
|
|
385
|
-
if real="$(find_real_claude "$REPO_DIR/bin/claude")"; then
|
|
386
|
-
echo " real claude binary: $real ($("$real" --version 2>/dev/null | head -1 || echo 'version unknown'))"
|
|
387
|
-
else
|
|
388
|
-
echo " WARNING: no real claude binary found yet — install Claude Code first (https://claude.com/claude-code)" >&2
|
|
389
|
-
fi
|
|
390
|
-
local real_codex
|
|
391
|
-
if real_codex="$(find_real_codex "$REPO_DIR/bin/codex")"; then
|
|
392
|
-
echo " real codex binary: $real_codex ($("$real_codex" --version 2>/dev/null | head -1 || echo 'version unknown'))"
|
|
393
|
-
else
|
|
394
|
-
echo " note: no codex binary found — the codex pool stays idle until Codex CLI is installed (npm i -g @openai/codex)"
|
|
395
|
-
fi
|
|
396
|
-
|
|
397
|
-
# Keychain-mode note: Claude Code keeps per-config-dir logins in the login Keychain
|
|
398
|
-
# (one item per dir) whenever the session can open it, and only sessions without
|
|
399
|
-
# keychain access (ssh, launchd background jobs) fall back to .credentials.json. The
|
|
400
|
-
# pool reads both (lib/keychain.py); what an operator has to know is that a login
|
|
401
|
-
# made from a GUI session is invisible to their ssh sessions — mint a portable
|
|
402
|
-
# token for anything that must work from everywhere.
|
|
403
|
-
if [ "$kind" = "mac" ] && [ ! -f "$HOME/.claude/.credentials.json" ] \
|
|
404
|
-
&& security find-generic-password -s "Claude Code-credentials" >/dev/null 2>&1; then
|
|
405
|
-
echo " note: this Mac keeps Claude Code logins in the Keychain. Per-dir logins made here"
|
|
406
|
-
echo " are read from it; ssh/background sessions cannot open it (they see 'locked') —"
|
|
407
|
-
echo " use 'claude-accounts mint <acct-NN>' for accounts that must work from everywhere."
|
|
408
|
-
fi
|
|
409
|
-
|
|
410
|
-
manifest_init "${SERVER_OVERRIDE:-$DEFAULT_SERVER}"
|
|
411
|
-
if [ -n "$SERVER_OVERRIDE" ]; then
|
|
412
|
-
"$PYBIN" - "$MANIFEST" "$SERVER_OVERRIDE" <<'PYEOF'
|
|
413
|
-
import json, os, sys
|
|
414
|
-
doc = json.load(open(sys.argv[1]))
|
|
415
|
-
doc['server'] = sys.argv[2]
|
|
416
|
-
with open(sys.argv[1] + '.tmp', 'w') as f:
|
|
417
|
-
json.dump(doc, f, indent=2)
|
|
418
|
-
f.write('\n')
|
|
419
|
-
os.replace(sys.argv[1] + '.tmp', sys.argv[1])
|
|
420
|
-
PYEOF
|
|
421
|
-
fi
|
|
422
|
-
if sync_target_is_local "$(sync_target)"; then
|
|
423
|
-
echo " account pool: $ACC_ROOT (manifest ready; sync target: none — local-only)"
|
|
424
|
-
else
|
|
425
|
-
echo " account pool: $ACC_ROOT (manifest ready; sync target: $(sync_target))"
|
|
426
|
-
fi
|
|
427
|
-
# The codex pool gets its own skeleton + manifest (same schema, separate root).
|
|
428
|
-
if ! "$REPO_DIR/bin/codex-accounts" init-pool "${SERVER_OVERRIDE:-}" >/dev/null 2>&1; then
|
|
429
|
-
echo " WARNING: codex pool init failed (codex-accounts init-pool)" >&2
|
|
430
|
-
else
|
|
431
|
-
echo " codex account pool: $CODEX_ACC_ROOT (manifest ready)"
|
|
432
|
-
fi
|
|
433
|
-
|
|
434
|
-
# An INSTANCE install never rewrites the shell rc blocks: one interactive PATH
|
|
435
|
-
# cannot serve two pools, and clobbering the default install's block would point
|
|
436
|
-
# the operator's shell at an instance pool. Its agents (below) carry the roots
|
|
437
|
-
# instead, and the runner daemon invokes the shims by absolute path.
|
|
438
|
-
if [ -n "$INSTANCE" ]; then
|
|
439
|
-
echo " PATH block: skipped (instance install) — for a shell against this pool:"
|
|
440
|
-
echo " export CLAUDE_ACCOUNTS_ROOT='$ACC_ROOT' CODEX_ACCOUNTS_ROOT='$CODEX_ACC_ROOT'"
|
|
441
|
-
echo " export PATH=\"$REPO_DIR/bin:\$PATH\""
|
|
442
|
-
fi
|
|
443
|
-
if [ "$kind" = "mac" ]; then
|
|
444
|
-
# .zshenv covers non-interactive zsh; the .zshrc block must be LAST so it wins
|
|
445
|
-
# over ~/.local/bin re-prepends done earlier in .zshrc/.zprofile.
|
|
446
|
-
# zsh reads: .zshenv always; .zprofile for login; .zshrc for interactive.
|
|
447
|
-
# The block must end each file that later re-prepends ~/.local/bin.
|
|
448
|
-
if [ -z "$INSTANCE" ]; then
|
|
449
|
-
touch "$HOME/.zshenv"
|
|
450
|
-
append_block "$HOME/.zshenv"
|
|
451
|
-
touch "$HOME/.zprofile"
|
|
452
|
-
append_block "$HOME/.zprofile"
|
|
453
|
-
touch "$HOME/.zshrc"
|
|
454
|
-
append_block "$HOME/.zshrc"
|
|
455
|
-
[ -f "$HOME/.bash_profile" ] && append_block "$HOME/.bash_profile"
|
|
456
|
-
[ -f "$HOME/.bashrc" ] && append_block "$HOME/.bashrc"
|
|
457
|
-
echo " PATH block: end of ~/.zshenv, ~/.zprofile, ~/.zshrc (+ bash rc files if present)"
|
|
458
|
-
fi
|
|
459
|
-
[ "$NO_SCHEDULE" = "1" ] || mac_schedule_install
|
|
460
|
-
else
|
|
461
|
-
if [ -z "$INSTANCE" ]; then
|
|
462
|
-
touch "$HOME/.bashrc"
|
|
463
|
-
append_block "$HOME/.bashrc"
|
|
464
|
-
if [ -w /etc/profile.d ] 2>/dev/null || [ "$(id -u)" = "0" ]; then
|
|
465
|
-
{
|
|
466
|
-
printf '%s\n' "$MARK_BEGIN"
|
|
467
|
-
path_block_body
|
|
468
|
-
printf '%s\n' "$MARK_END"
|
|
469
|
-
} > "$PROFILED"
|
|
470
|
-
echo " PATH block: ~/.bashrc + $PROFILED"
|
|
471
|
-
fi
|
|
472
|
-
fi
|
|
473
|
-
if [ "$(id -u)" = "0" ]; then
|
|
474
|
-
if [ -e /usr/local/bin/claude ] && [ ! -L /usr/local/bin/claude ]; then
|
|
475
|
-
echo " WARNING: /usr/local/bin/claude exists and is a real file — NOT overwriting." >&2
|
|
476
|
-
else
|
|
477
|
-
ln -sfn "$REPO_DIR/bin/claude" /usr/local/bin/claude
|
|
478
|
-
echo " shim: /usr/local/bin/claude -> $REPO_DIR/bin/claude (systemd-PATH compatible)"
|
|
479
|
-
fi
|
|
480
|
-
if [ -e /usr/local/bin/codex ] && [ ! -L /usr/local/bin/codex ]; then
|
|
481
|
-
echo " WARNING: /usr/local/bin/codex exists and is a real file — NOT overwriting." >&2
|
|
482
|
-
else
|
|
483
|
-
ln -sfn "$REPO_DIR/bin/codex" /usr/local/bin/codex
|
|
484
|
-
echo " shim: /usr/local/bin/codex -> $REPO_DIR/bin/codex (systemd-PATH compatible)"
|
|
485
|
-
fi
|
|
486
|
-
fi
|
|
487
|
-
[ "$NO_SCHEDULE" = "1" ] || linux_schedule_install
|
|
488
|
-
fi
|
|
489
|
-
|
|
490
|
-
echo
|
|
491
|
-
echo "install complete. Open a new shell (or 'export PATH=\"$REPO_DIR/bin:\$PATH\"'), then:"
|
|
492
|
-
echo " claude-accounts list # the Claude Code pool"
|
|
493
|
-
echo " codex-accounts list # the Codex pool"
|
|
494
|
-
echo " claude-accounts status | codex-accounts status # auth + limits detail"
|
|
495
|
-
}
|
|
496
|
-
|
|
497
222
|
if [ "$UNINSTALL" = "1" ]; then
|
|
498
223
|
do_uninstall
|
|
499
224
|
else
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/common.sh
CHANGED
|
@@ -588,12 +588,101 @@ os.replace(sys.argv[1] + '.tmp', sys.argv[1])
|
|
|
588
588
|
PYEOF
|
|
589
589
|
}
|
|
590
590
|
|
|
591
|
+
# ---- one index for the shared session tree -----------------------------------------
|
|
592
|
+
# codex refuses to start until its rollout index (state_<schema>.sqlite) has been
|
|
593
|
+
# backfilled from every rollout under $CODEX_HOME/sessions. The codex layout shares that
|
|
594
|
+
# tree across accounts (<acct>/sessions -> ~/.codex/sessions), so the index is shared
|
|
595
|
+
# the same way: <acct>/state_N.sqlite -> ~/.codex/state_N.sqlite. Otherwise every new
|
|
596
|
+
# account re-reads the whole tree (10 GB on a busy Mac) before its first launch, and a
|
|
597
|
+
# launch that meets another process's unfinished scan dies after 30 s with the CLI's
|
|
598
|
+
# "local database appears to be damaged" message. bin/codex applies the same at every
|
|
599
|
+
# launch (and pre-links the name the installed binary will create); seeding covers a
|
|
600
|
+
# CODEX_HOME handed straight to the real binary. The two CORE functions below are
|
|
601
|
+
# byte-identical with bin/codex — tests/run-tests.sh diffs them.
|
|
602
|
+
# CORE — byte-identical in bin/codex; tests diff them.
|
|
603
|
+
state_index_names() { # $1 acct dir, $2 home dir -> the index file names either side holds
|
|
604
|
+
local f name seen=" "
|
|
605
|
+
for f in "$2"/state_[0-9]*.sqlite "$1"/state_[0-9]*.sqlite; do
|
|
606
|
+
[ -e "$f" ] || [ -L "$f" ] || continue # an unmatched glob is the pattern itself
|
|
607
|
+
name="${f##*/}"
|
|
608
|
+
case "$seen" in *" $name "*) continue ;; esac
|
|
609
|
+
seen="$seen$name "
|
|
610
|
+
printf '%s\n' "$name"
|
|
611
|
+
done
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
# CORE — byte-identical in bin/codex; tests diff them.
|
|
615
|
+
shared_index_rejected() { # $1 acct dir, $2 index name, $3 shared target
|
|
616
|
+
# True when codex's own corruption recovery moved THIS account's link out of the way:
|
|
617
|
+
# it renames the database it judged damaged (and its -wal/-shm) into
|
|
618
|
+
# <CODEX_HOME>/db-backups/sqlite-<ts>-<n>/ and rebuilds, and under a link that renames
|
|
619
|
+
# the LINK. A link sitting in there pointing at the shared file is codex's verdict on
|
|
620
|
+
# that file, so this account keeps the index codex rebuilt for it and this name is
|
|
621
|
+
# left alone — handing the link back would hand the damage back.
|
|
622
|
+
local marker
|
|
623
|
+
for marker in "$1"/db-backups/*/"$2"; do
|
|
624
|
+
[ -L "$marker" ] || continue
|
|
625
|
+
[ "$(readlink "$marker" 2>/dev/null)" = "$3" ] && return 0
|
|
626
|
+
done
|
|
627
|
+
return 1
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
# CORE — byte-identical in bin/codex; tests diff them.
|
|
631
|
+
share_state_index_links() { # $1 acct dir, $2 home dir, $3 extra index name ('' for none)
|
|
632
|
+
local d="$1" home="$2" name names target link retired n
|
|
633
|
+
[ -L "$d" ] && return 0 # adopted: the dir IS the home
|
|
634
|
+
[ -L "$d/sessions" ] || return 0 # a private tree keeps its private index
|
|
635
|
+
[ "$(readlink "$d/sessions" 2>/dev/null)" = "$home/sessions" ] || return 0
|
|
636
|
+
[ -d "$home" ] || return 0
|
|
637
|
+
names="$(state_index_names "$d" "$home")"
|
|
638
|
+
case "${3:-}" in ''|*[!A-Za-z0-9_.]*) ;; *) names="$names $3" ;; esac
|
|
639
|
+
for name in $names; do
|
|
640
|
+
target="$home/$name"; link="$d/$name"
|
|
641
|
+
[ -L "$link" ] && continue # already shared (or pointed elsewhere on purpose)
|
|
642
|
+
shared_index_rejected "$d" "$name" "$target" && continue
|
|
643
|
+
if [ ! -e "$link" ]; then
|
|
644
|
+
ln -s "$target" "$link" 2>/dev/null || true # a new account: the whole point
|
|
645
|
+
continue
|
|
646
|
+
fi
|
|
647
|
+
# This account has an index of its own, and MOVING one that a process can still open
|
|
648
|
+
# is unsafe. codex holds the state database through an sqlx pool that opens its
|
|
649
|
+
# connections lazily and BY PATH (max_connections(5), create_if_missing(true) —
|
|
650
|
+
# codex-rs/state/src/sqlite.rs open_read_write_pool), so a rename under a live holder
|
|
651
|
+
# leaves connection 1 on the old inode while every connection the pool opens
|
|
652
|
+
# afterwards follows the new link: one process, two databases. A -wal or a -shm beside
|
|
653
|
+
# the file is that proof. A codex killed mid-write leaves them behind too, and the
|
|
654
|
+
# next clean session on the account removes them, so this heals itself in time.
|
|
655
|
+
{ [ -e "$link-wal" ] || [ -e "$link-shm" ]; } && continue
|
|
656
|
+
if [ -e "$target" ]; then
|
|
657
|
+
retired="$link.private"; n=0 # never overwrite an earlier copy
|
|
658
|
+
while [ -e "$retired" ] && [ "$n" -lt 100 ]; do n=$((n+1)); retired="$link.private.$n"; done
|
|
659
|
+
[ -e "$retired" ] && continue
|
|
660
|
+
mv "$link" "$retired" 2>/dev/null || continue
|
|
661
|
+
else
|
|
662
|
+
# Atomic or nothing: link(2) refuses an existing target, so two shims racing to be
|
|
663
|
+
# the first to promote cannot rename one's fresh symlink onto the file the other
|
|
664
|
+
# just promoted — which is how a self-referential shared index (ELOOP, and every
|
|
665
|
+
# account on the Mac unable to start) could appear. A cross-device link simply
|
|
666
|
+
# fails and this account keeps its own index.
|
|
667
|
+
ln "$link" "$target" 2>/dev/null || continue
|
|
668
|
+
[ -L "$link" ] || rm -f "$link"
|
|
669
|
+
fi
|
|
670
|
+
ln -s "$target" "$link" 2>/dev/null || true
|
|
671
|
+
done
|
|
672
|
+
return 0
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
codex_share_state_index() { # $1 acct dir — seed time: whatever index names exist already
|
|
676
|
+
share_state_index_links "$1" "${HOME:-/nonexistent}/.codex" ""
|
|
677
|
+
}
|
|
678
|
+
|
|
591
679
|
# Seed an account config dir so headless runs never prompt.
|
|
592
680
|
# claude: stripped .claude.json, copied settings.json, shared projects/ symlink
|
|
593
681
|
# (shared history => --continue/--resume work regardless of picked account).
|
|
594
682
|
# codex: copied config.toml (carries project trust + settings, no identity),
|
|
595
|
-
# shared sessions/ symlink (=> `codex resume` finds every session)
|
|
596
|
-
# shared
|
|
683
|
+
# shared sessions/ symlink (=> `codex resume` finds every session) with the
|
|
684
|
+
# shared rollout index that tree needs, and a shared AGENTS.md symlink so
|
|
685
|
+
# global instructions apply under any account.
|
|
597
686
|
seed_account_dir() { # $1 = acct dir
|
|
598
687
|
local d="$1"
|
|
599
688
|
mkdir -p "$d"
|
|
@@ -608,6 +697,7 @@ seed_account_dir() { # $1 = acct dir
|
|
|
608
697
|
if [ ! -e "$d/AGENTS.md" ] && [ -f "$HOME/.codex/AGENTS.md" ]; then
|
|
609
698
|
ln -s "$HOME/.codex/AGENTS.md" "$d/AGENTS.md" 2>/dev/null || true
|
|
610
699
|
fi
|
|
700
|
+
codex_share_state_index "$d"
|
|
611
701
|
return 0
|
|
612
702
|
fi
|
|
613
703
|
if [ ! -f "$d/.claude.json" ] && [ -f "$HOME/.claude.json" ]; then
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
USAGE
|
|
2
|
+
claude-multiacc install [options]
|
|
3
|
+
./install.sh [options]
|
|
4
|
+
claude-multiacc uninstall [--purge-data]
|
|
5
|
+
./install.sh --uninstall [--purge-data]
|
|
6
|
+
|
|
7
|
+
OPTIONS
|
|
8
|
+
--server user@host set the sync target
|
|
9
|
+
--no-server use local-only pools
|
|
10
|
+
--instance NAME label this instance's scheduled jobs
|
|
11
|
+
--no-schedule install without scheduled jobs
|
|
12
|
+
--uninstall remove shell setup and jobs; keep account data
|
|
13
|
+
--purge-data also delete both account pools when uninstalling
|
|
14
|
+
-h, --help show this help without installing or uninstalling
|
|
15
|
+
|
|
16
|
+
Use CLAUDE_ACCOUNTS_ROOT and CODEX_ACCOUNTS_ROOT to select isolated pools.
|
|
17
|
+
After npm install -g claude-multiacc, both claude-accounts and codex-accounts
|
|
18
|
+
are available alongside claude-multiacc in npm's bin directory immediately.
|
|
19
|
+
Git/npx installs add the package's bin directory through shell startup files;
|
|
20
|
+
open a new shell or use the export PATH command printed by the installer.
|