claude-multiacc 1.0.0 → 1.0.2
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 +29 -12
- package/bin/claude-accounts +149 -84
- package/package.json +1 -1
- package/tests/run-tests.sh +57 -14
package/README.md
CHANGED
|
@@ -163,25 +163,37 @@ acct-01 = the Mac's login, acct-02 = the server's login (adopted server-side).
|
|
|
163
163
|
### Add a fresh account (~1 min, one browser sign-in)
|
|
164
164
|
|
|
165
165
|
```bash
|
|
166
|
+
claude-accounts add # no email needed — read back from the sign-in
|
|
167
|
+
# or name it explicitly (fast-fails on a duplicate, and pre-fills the login page):
|
|
166
168
|
claude-accounts add new@example.com
|
|
167
169
|
```
|
|
168
170
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
a
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
171
|
+
This runs the **normal full Claude Code login** — the exact same
|
|
172
|
+
`claude auth login` flow the app itself uses (`claude.com/cai/oauth/authorize`, full
|
|
173
|
+
Claude Code scopes). A browser opens (or a sign-in link is printed); sign in to the
|
|
174
|
+
account you're adding, approve, and if a code is shown, paste it back. It draws on the
|
|
175
|
+
subscription — never API keys.
|
|
176
|
+
|
|
177
|
+
Using the full login on purpose: it does **not** hit the *"Sign in again to continue"*
|
|
178
|
+
step-up wall that the narrow long-lived-token grant triggers, and it stores an
|
|
179
|
+
auto-refreshing `.credentials.json`. That credential is **machine-local** (never synced),
|
|
180
|
+
which is what keeps two machines from invalidating each other's refresh token — so an
|
|
181
|
+
account you `add` on the Mac runs on the Mac, and you `add` it on the server (over SSH) if
|
|
182
|
+
you want it there too. The account is registered **only after the signed-in email is read
|
|
183
|
+
back**; an aborted login leaves nothing behind, and duplicate emails are refused.
|
|
176
184
|
|
|
177
185
|
```bash
|
|
178
|
-
claude-accounts login acct-NN
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
186
|
+
claude-accounts login acct-NN # complete/refresh an existing account (full login)
|
|
187
|
+
claude-accounts add new@example.com --token # portable setup-token instead: works on BOTH
|
|
188
|
+
# machines and syncs to the server, but needs a
|
|
189
|
+
# recent sign-in (may show the step-up screen)
|
|
190
|
+
claude-accounts login acct-NN --token # same, for an existing account
|
|
182
191
|
```
|
|
183
192
|
|
|
184
|
-
`
|
|
193
|
+
Use `--token` when you specifically want one account usable on the server via the Mac's
|
|
194
|
+
sync (the ~1-year inference-only token that mirrors over). For everyday adds, the default
|
|
195
|
+
full login is smoother. `status` warns when a token nears end of life; re-run `login`.
|
|
196
|
+
|
|
185
197
|
Both accounts currently in the pool are live and verified: acct-01 (support@gowalkae.com,
|
|
186
198
|
Mac) and acct-02 (hasan@gowalkqa.com, server) — each adopted from its machine's existing
|
|
187
199
|
login, so each runs on the machine that holds its credential.
|
|
@@ -248,6 +260,11 @@ repointed via `CLAUDE_BIN=/usr/local/bin/claude` and restarted healthy.
|
|
|
248
260
|
- **A service bypasses the shim** — it spawns an absolute path. Point its env
|
|
249
261
|
(`CLAUDE_BIN` etc.) at `/usr/local/bin/claude`. Docker-internal `claude` installs
|
|
250
262
|
(e.g. openclaw live-test containers) are out of scope for a host shim.
|
|
263
|
+
- **`add` sign-in says "Sign in again to continue"** — that's Claude's step-up
|
|
264
|
+
authentication, not a tool error: granting a long-lived token needs a *recent* sign-in.
|
|
265
|
+
Open a fresh incognito window, sign in to the target account at claude.ai **first**,
|
|
266
|
+
then paste the link and approve. The approval scope is `user:inference`
|
|
267
|
+
(*"Contribute to your Claude subscription usage"*) — that's correct and minimal.
|
|
251
268
|
- **Limits look stale** — launchd: `launchctl list | grep claude-multiacc`;
|
|
252
269
|
cron: `crontab -l | grep claude-multiacc`. The shim also kicks a refresh when data
|
|
253
270
|
is >3 min old.
|
package/bin/claude-accounts
CHANGED
|
@@ -20,12 +20,15 @@ claude-accounts — multi-account pool manager for claude-multiacc
|
|
|
20
20
|
USAGE
|
|
21
21
|
claude-accounts list brief account list
|
|
22
22
|
claude-accounts status full health: auth, per-bucket limits, markers
|
|
23
|
-
claude-accounts add
|
|
24
|
-
login-FIRST:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
claude-accounts add [email] [--token] [--force]
|
|
24
|
+
login-FIRST: runs the full Claude Code login (`claude auth login` — the normal
|
|
25
|
+
browser sign-in, no long-lived-token step-up), then registers only after the
|
|
26
|
+
signed-in email is read back. Email is OPTIONAL (derived from the sign-in).
|
|
27
|
+
Stores auto-refreshing .credentials.json valid on THIS machine. Duplicates refused.
|
|
28
|
+
--token instead mints a portable setup-token (works on Mac AND server; needs a
|
|
29
|
+
recent sign-in — use it when you want the account usable on the server too).
|
|
30
|
+
claude-accounts login <acct-NN> [--token] [--force]
|
|
31
|
+
complete/refresh auth for an existing account (full login, or --token)
|
|
29
32
|
claude-accounts import <email> [opts] register an account, optionally with credentials
|
|
30
33
|
--id acct-NN explicit id (default: next free)
|
|
31
34
|
--home mac|server which machine owns the OAuth grant (default: this one)
|
|
@@ -229,27 +232,29 @@ PYEOF
|
|
|
229
232
|
}
|
|
230
233
|
|
|
231
234
|
cmd_add() {
|
|
232
|
-
# Login-FIRST: the account is registered (and synced) only after
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
+
# Login-FIRST: the account is registered (and synced) only after sign-in succeeds
|
|
236
|
+
# and the authenticated email is read back from the account itself. The email
|
|
237
|
+
# argument is OPTIONAL — omit it and it's derived from whoever you sign in as.
|
|
238
|
+
# An aborted or failed sign-in leaves zero traces.
|
|
235
239
|
require_manifest
|
|
236
|
-
local email="
|
|
237
|
-
[ -n "$email" ] || die "usage: claude-accounts add <email> [--force]"
|
|
238
|
-
shift
|
|
239
|
-
local force=0 tui=0
|
|
240
|
+
local email="" force=0 token=0
|
|
240
241
|
while [ $# -gt 0 ]; do
|
|
241
242
|
case "$1" in
|
|
242
243
|
--force) force=1; shift ;;
|
|
243
|
-
--
|
|
244
|
-
|
|
244
|
+
--token) token=1; shift ;; # portable setup-token instead of full login (server use)
|
|
245
|
+
--tui) shift ;; # accepted for compatibility; full login is now the default
|
|
246
|
+
--*) die "unknown option: $1" ;;
|
|
247
|
+
*)
|
|
248
|
+
if [ -z "$email" ]; then email="$1"; shift
|
|
249
|
+
else die "unexpected argument: $1 (usage: claude-accounts add [email] [--token] [--force])"; fi ;;
|
|
245
250
|
esac
|
|
246
251
|
done
|
|
247
|
-
#
|
|
248
|
-
#
|
|
252
|
+
# If an email was named, fast-fail on a duplicate now. If not, we learn (and dedup)
|
|
253
|
+
# the real email from the sign-in below — one account per email either way.
|
|
249
254
|
local owner
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
die "$email is already registered as $owner — nothing created (run 'claude-accounts login $owner' to re-authenticate it)"
|
|
255
|
+
if [ -n "$email" ]; then
|
|
256
|
+
owner="$(email_owner "$email")"
|
|
257
|
+
[ -n "$owner" ] && die "$email is already registered as $owner — nothing created (run 'claude-accounts login $owner' to re-authenticate it)"
|
|
253
258
|
fi
|
|
254
259
|
if [ ! -t 0 ] && [ -z "${CLAUDE_MULTIACC_FORCE_TTY:-}" ]; then
|
|
255
260
|
die "add is interactive (it completes sign-in before registering) — run it from a terminal"
|
|
@@ -269,40 +274,14 @@ cmd_add() {
|
|
|
269
274
|
trap "rm -rf '$lock'" EXIT
|
|
270
275
|
local real
|
|
271
276
|
real="$(find_real_claude "$_self")" || die "real claude binary not found"
|
|
272
|
-
local id d got
|
|
277
|
+
local id d got elabel
|
|
278
|
+
elabel="${email:-the account you sign in as}"
|
|
273
279
|
id="$(next_id)"
|
|
274
280
|
d="$ACC_ROOT/$id"
|
|
275
281
|
seed_account_dir "$d"
|
|
276
|
-
if [ "$
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
Preparing $id for $email — NOTHING is registered until login succeeds.
|
|
280
|
-
An interactive claude session opens now with no account. In ONE incognito
|
|
281
|
-
window signed into $email:
|
|
282
|
-
1. type /login (press c to copy the URL into the incognito window)
|
|
283
|
-
2. /status must show $email
|
|
284
|
-
3. exit the session (/exit or ctrl+d) to finish registration
|
|
285
|
-
EOF
|
|
286
|
-
CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" || true
|
|
287
|
-
if [ ! -f "$d/.credentials.json" ]; then
|
|
288
|
-
rm -rf "${ACC_ROOT:?}/${id:?}"
|
|
289
|
-
die "no login detected — nothing was created"
|
|
290
|
-
fi
|
|
291
|
-
got="$(CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" auth status 2>/dev/null | "$PYBIN" -c '
|
|
292
|
-
import json, sys
|
|
293
|
-
try:
|
|
294
|
-
doc = json.loads(sys.stdin.read())
|
|
295
|
-
print(doc.get("email", "") if doc.get("loggedIn") else "")
|
|
296
|
-
except Exception:
|
|
297
|
-
print("")')"
|
|
298
|
-
if [ -z "$got" ]; then
|
|
299
|
-
rm -rf "${ACC_ROOT:?}/${id:?}"
|
|
300
|
-
die "login could not be verified — nothing was created"
|
|
301
|
-
fi
|
|
302
|
-
else
|
|
303
|
-
# Default: sign-in link -> user pastes the code back here -> token captured.
|
|
304
|
-
# The token works on BOTH machines (no refresh rotation, ~1y lifetime).
|
|
305
|
-
echo "Preparing $id for $email — NOTHING is registered until sign-in completes."
|
|
282
|
+
if [ "$token" = "1" ]; then
|
|
283
|
+
# Portable setup-token variant (works on Mac AND server; needs a recent sign-in).
|
|
284
|
+
echo "Preparing $id for $elabel (portable token) — NOTHING is registered until sign-in completes."
|
|
306
285
|
if ! run_token_ceremony "$d"; then
|
|
307
286
|
rm -rf "${ACC_ROOT:?}/${id:?}"
|
|
308
287
|
die "sign-in failed or aborted — nothing was created"
|
|
@@ -310,28 +289,39 @@ except Exception:
|
|
|
310
289
|
( umask 077; printf '%s' "$CEREMONY_TOKEN" > "$d/server.token" )
|
|
311
290
|
chmod 600 "$d/server.token"
|
|
312
291
|
got="$(token_email "$CEREMONY_TOKEN")"
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
fi
|
|
322
|
-
warn "identity unverified — registering as $email because --force was given"
|
|
323
|
-
got="$email"
|
|
292
|
+
else
|
|
293
|
+
# Default: full Claude Code login (the flow that just works — full scopes, no
|
|
294
|
+
# long-lived-token step-up). Writes auto-refreshing .credentials.json, valid on
|
|
295
|
+
# THIS machine (creds are never synced, so no cross-machine refresh races).
|
|
296
|
+
echo "Preparing $id for $elabel — NOTHING is registered until login completes."
|
|
297
|
+
if ! run_login_ceremony "$d" "$email"; then
|
|
298
|
+
rm -rf "${ACC_ROOT:?}/${id:?}"
|
|
299
|
+
die "login failed or aborted — nothing was created"
|
|
324
300
|
fi
|
|
301
|
+
got="$(config_dir_email "$d")"
|
|
325
302
|
fi
|
|
326
|
-
if [ "$got"
|
|
327
|
-
|
|
328
|
-
|
|
303
|
+
if [ -z "$got" ]; then
|
|
304
|
+
# Signed in, but we could not read back WHICH account. Without a verified email we
|
|
305
|
+
# cannot label or dedup the account, so refuse — unless an email was named AND
|
|
306
|
+
# --force was given (then we trust the named email).
|
|
307
|
+
if [ -n "$email" ] && [ "$force" = "1" ]; then
|
|
308
|
+
warn "identity unverified — registering as $email because --force was given"
|
|
309
|
+
got="$email"
|
|
310
|
+
else
|
|
329
311
|
rm -rf "${ACC_ROOT:?}/${id:?}"
|
|
330
|
-
die "
|
|
312
|
+
die "signed in, but the account identity could not be read back — nothing was created (retry; or 'add <email> --force' to trust a named email)"
|
|
331
313
|
fi
|
|
332
|
-
warn "signed in as $got (expected $email) — registering the account that actually authenticated"
|
|
333
|
-
email="$got"
|
|
334
314
|
fi
|
|
315
|
+
# `got` is now the authoritative signed-in email. Dedup against it, warn on mismatch.
|
|
316
|
+
owner="$(email_owner "$got")"
|
|
317
|
+
if [ -n "$owner" ]; then
|
|
318
|
+
rm -rf "${ACC_ROOT:?}/${id:?}"
|
|
319
|
+
die "you signed in as $got, which is already registered as $owner — nothing was created"
|
|
320
|
+
fi
|
|
321
|
+
if [ -n "$email" ] && [ "$got" != "$email" ]; then
|
|
322
|
+
warn "signed in as $got (you named $email) — registering the account that actually authenticated"
|
|
323
|
+
fi
|
|
324
|
+
email="$got"
|
|
335
325
|
manifest_add_account "$id" "$email" "$(machine_kind)"
|
|
336
326
|
log_to ops.log "add $id $email (auth-verified)"
|
|
337
327
|
auto_sync
|
|
@@ -488,8 +478,17 @@ run_token_ceremony() { # $1 = config dir
|
|
|
488
478
|
umask 077 # the capture file briefly holds the raw token (removed inline below).
|
|
489
479
|
# NB: no EXIT trap here — cmd_add owns the EXIT trap for its mutation lock, and a
|
|
490
480
|
# second EXIT trap would clobber it and leak the lock.
|
|
491
|
-
|
|
492
|
-
|
|
481
|
+
cat <<'TIP'
|
|
482
|
+
A sign-in link will appear below. For a smooth grant (avoids the "Sign in again
|
|
483
|
+
to continue" step-up screen):
|
|
484
|
+
1. Open a fresh incognito/private window.
|
|
485
|
+
2. Sign in at claude.ai to the EXACT account you're adding — do this FIRST.
|
|
486
|
+
3. THEN paste the link below into that same window and approve
|
|
487
|
+
("Contribute to your Claude subscription usage" is the correct permission).
|
|
488
|
+
4. Copy the code it shows and paste it back here.
|
|
489
|
+
If you do see "Sign in again to continue", that is Claude's security step, not an
|
|
490
|
+
error — just sign in to that account and approve; the code still appears.
|
|
491
|
+
TIP
|
|
493
492
|
if [ -t 0 ]; then
|
|
494
493
|
if [ "$(machine_kind)" = "mac" ]; then
|
|
495
494
|
script -q "$cap" env CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" setup-token
|
|
@@ -525,6 +524,56 @@ except Exception:
|
|
|
525
524
|
print("")'
|
|
526
525
|
}
|
|
527
526
|
|
|
527
|
+
config_dir_email() { # $1 = config dir; prints the logged-in email or empty
|
|
528
|
+
local real
|
|
529
|
+
real="$(find_real_claude "$_self")" || { echo ""; return 0; }
|
|
530
|
+
CLAUDE_CONFIG_DIR="$1" CLAUDE_SHIM_ACTIVE=1 "$real" auth status 2>/dev/null | "$PYBIN" -c '
|
|
531
|
+
import json, sys
|
|
532
|
+
try:
|
|
533
|
+
doc = json.loads(sys.stdin.read())
|
|
534
|
+
print(doc.get("email", "") if doc.get("loggedIn") else "")
|
|
535
|
+
except Exception:
|
|
536
|
+
print("")'
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
# Full subscription login: `claude auth login --claudeai`, the SAME flow the
|
|
540
|
+
# interactive app uses (claude.com/cai/oauth/authorize, full Claude Code scopes,
|
|
541
|
+
# normal sign-in — no long-lived-token step-up wall). Writes <dir>/.credentials.json
|
|
542
|
+
# (auto-refreshing). Returns success iff the credential landed. $2 = optional email hint.
|
|
543
|
+
run_login_ceremony() { # $1 = config dir, $2 = email hint
|
|
544
|
+
local d="$1" hint="${2:-}" real
|
|
545
|
+
real="$(find_real_claude "$_self")" || { warn "real claude binary not found"; return 1; }
|
|
546
|
+
cat <<'TIP'
|
|
547
|
+
A browser will open (or a sign-in link will be printed) for the FULL Claude Code
|
|
548
|
+
login. Sign in as the account you want to add and approve; if a code is shown,
|
|
549
|
+
paste it back here. This is the normal login — it draws on the subscription, no
|
|
550
|
+
API keys, and refreshes itself over time.
|
|
551
|
+
TIP
|
|
552
|
+
if [ -t 0 ]; then
|
|
553
|
+
if [ "$(machine_kind)" = "mac" ]; then
|
|
554
|
+
if [ -n "$hint" ]; then
|
|
555
|
+
script -q /dev/null env CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" auth login --claudeai --email "$hint"
|
|
556
|
+
else
|
|
557
|
+
script -q /dev/null env CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" auth login --claudeai
|
|
558
|
+
fi
|
|
559
|
+
else
|
|
560
|
+
if [ -n "$hint" ]; then
|
|
561
|
+
script -q -c "CLAUDE_CONFIG_DIR='$d' CLAUDE_SHIM_ACTIVE=1 '$real' auth login --claudeai --email '$hint'" /dev/null
|
|
562
|
+
else
|
|
563
|
+
script -q -c "CLAUDE_CONFIG_DIR='$d' CLAUDE_SHIM_ACTIVE=1 '$real' auth login --claudeai" /dev/null
|
|
564
|
+
fi
|
|
565
|
+
fi
|
|
566
|
+
else
|
|
567
|
+
# Headless (tests / automation): no PTY.
|
|
568
|
+
if [ -n "$hint" ]; then
|
|
569
|
+
CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" auth login --claudeai --email "$hint" || true
|
|
570
|
+
else
|
|
571
|
+
CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" auth login --claudeai || true
|
|
572
|
+
fi
|
|
573
|
+
fi
|
|
574
|
+
[ -f "$d/.credentials.json" ]
|
|
575
|
+
}
|
|
576
|
+
|
|
528
577
|
cmd_mint() {
|
|
529
578
|
require_manifest
|
|
530
579
|
local id="${1:-}" paste="${2:-}"
|
|
@@ -553,11 +602,20 @@ cmd_mint() {
|
|
|
553
602
|
}
|
|
554
603
|
|
|
555
604
|
cmd_login() {
|
|
556
|
-
# Complete (or refresh) auth for an EXISTING account:
|
|
557
|
-
#
|
|
605
|
+
# Complete (or refresh) auth for an EXISTING account. Default: full Claude Code login
|
|
606
|
+
# (auto-refreshing .credentials.json, this machine). --token: portable setup-token
|
|
607
|
+
# (Mac + server). Verifies the signed-in email matches the manifest.
|
|
558
608
|
require_manifest
|
|
559
|
-
local id="
|
|
560
|
-
[ -
|
|
609
|
+
local id="" force=0 token=0
|
|
610
|
+
while [ $# -gt 0 ]; do
|
|
611
|
+
case "$1" in
|
|
612
|
+
--force) force=1; shift ;;
|
|
613
|
+
--token) token=1; shift ;;
|
|
614
|
+
--*) die "unknown option: $1" ;;
|
|
615
|
+
*) if [ -z "$id" ]; then id="$1"; shift; else die "unexpected argument: $1"; fi ;;
|
|
616
|
+
esac
|
|
617
|
+
done
|
|
618
|
+
[ -n "$id" ] || die "usage: claude-accounts login <acct-NN> [--token] [--force]"
|
|
561
619
|
valid_acct_id "$id" || die "not a valid account id: $id"
|
|
562
620
|
account_ids | grep -qx "$id" || die "unknown account: $id"
|
|
563
621
|
local d="$ACC_ROOT/$id" email got
|
|
@@ -571,17 +629,24 @@ for a in json.load(open(sys.argv[1])).get('accounts', []):
|
|
|
571
629
|
PYEOF
|
|
572
630
|
)"
|
|
573
631
|
echo "Sign in as $email for $id."
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
632
|
+
if [ "$token" = "1" ]; then
|
|
633
|
+
run_token_ceremony "$d" || die "sign-in failed or aborted — nothing changed"
|
|
634
|
+
got="$(token_email "$CEREMONY_TOKEN")"
|
|
635
|
+
if [ -n "$got" ] && [ "$got" != "$email" ] && [ "$force" != "1" ]; then
|
|
636
|
+
die "you signed in as $got but $id is $email — nothing saved (use --force to override)"
|
|
637
|
+
fi
|
|
638
|
+
( umask 077; printf '%s' "$CEREMONY_TOKEN" > "$d/server.token" )
|
|
639
|
+
chmod 600 "$d/server.token"
|
|
640
|
+
echo "$id token saved (portable — works on Mac and server)."
|
|
641
|
+
else
|
|
642
|
+
run_login_ceremony "$d" "$email" || die "login failed or aborted — nothing changed"
|
|
643
|
+
got="$(config_dir_email "$d")"
|
|
644
|
+
if [ -n "$got" ] && [ "$got" != "$email" ] && [ "$force" != "1" ]; then
|
|
645
|
+
die "you signed in as $got but $id is $email — nothing saved (use --force to override)"
|
|
646
|
+
fi
|
|
647
|
+
echo "$id login saved (.credentials.json, this machine, auto-refreshing)."
|
|
578
648
|
fi
|
|
579
|
-
|
|
580
|
-
chmod 600 "$d/server.token"
|
|
581
|
-
log_to ops.log "login $id verified=${got:-unverified}"
|
|
582
|
-
echo "$id auth saved (token, works on Mac and server)."
|
|
583
|
-
echo "Optional, for auto-refreshing OAuth creds on this machine instead:"
|
|
584
|
-
echo " CLAUDE_ACCOUNT=$id claude # then /login"
|
|
649
|
+
log_to ops.log "login $id verified=${got:-unverified} mode=$([ "$token" = 1 ] && echo token || echo login)"
|
|
585
650
|
auto_sync
|
|
586
651
|
}
|
|
587
652
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-multiacc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Multi-account addon for Claude Code: every claude / claude -p runs under a randomly-picked subscription account with the most usage headroom. Mirrors to a deploy server. No API keys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/tests/run-tests.sh
CHANGED
|
@@ -29,9 +29,17 @@ cat > "$FAKEBIN/claude" <<'EOF'
|
|
|
29
29
|
#!/usr/bin/env bash
|
|
30
30
|
# fake real claude for tests (not a multiacc shim)
|
|
31
31
|
if [ "${1:-}" = "auth" ] && [ "${2:-}" = "status" ]; then
|
|
32
|
+
if [ -n "${FAKE_AUTH_FAIL:-}" ]; then echo '{"loggedIn": false}'; exit 0; fi
|
|
32
33
|
printf '{"loggedIn": true, "email": "%s"}\n' "${FAKE_EMAIL:-fake@test}"
|
|
33
34
|
exit 0
|
|
34
35
|
fi
|
|
36
|
+
if [ "${1:-}" = "auth" ] && [ "${2:-}" = "login" ]; then
|
|
37
|
+
[ -n "${FAKE_LOGIN_FAIL:-}" ] && { echo "login aborted" >&2; exit 1; }
|
|
38
|
+
# simulate a completed full-scope login: write auto-refreshing creds to the config dir
|
|
39
|
+
printf '{"claudeAiOauth":{"accessToken":"sk-ant-oat01-login","refreshToken":"r","expiresAt":9999999999999,"refreshTokenExpiresAt":9999999999999,"scopes":["user:inference"],"subscriptionType":"max"}}' > "${CLAUDE_CONFIG_DIR:-/dev/null}/.credentials.json"
|
|
40
|
+
echo "Logged in."
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
35
43
|
if [ "${1:-}" = "setup-token" ]; then
|
|
36
44
|
echo "Open this sign-in link: https://claude.ai/oauth/authorize?fake=1"
|
|
37
45
|
[ -n "${FAKE_TOKEN_FAIL:-}" ] && { echo "sign-in aborted" >&2; exit 1; }
|
|
@@ -357,28 +365,59 @@ check "import refuses duplicate email" "already registered as acct-01" "$out"
|
|
|
357
365
|
out="$(claude-accounts import a@test --id acct-01 --no-sync 2>&1)"
|
|
358
366
|
check "import same-id update allowed" "Imported acct-01" "$out"
|
|
359
367
|
|
|
360
|
-
# ---- 15c. login-first add (
|
|
368
|
+
# ---- 15c. login-first add (DEFAULT full-login flow): registers after verified auth --
|
|
361
369
|
out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=new@test claude-accounts add new@test 2>&1)"
|
|
362
|
-
check "add
|
|
363
|
-
|
|
364
|
-
[ -
|
|
370
|
+
check "add registers after verified login" "Registered acct-04 for new@test" "$out"
|
|
371
|
+
[ -f "$ACC/acct-04/.credentials.json" ] && t_ok "default add writes .credentials.json (full login)" || t_fail "add creds" "missing"
|
|
372
|
+
[ ! -f "$ACC/acct-04/server.token" ] && t_ok "default add does NOT mint a setup-token" || t_fail "add token" "unexpected server.token"
|
|
373
|
+
out="$(CLAUDE_ACCOUNT=acct-04 claude 2>&1)"
|
|
374
|
+
check "new account usable via pin (oauth creds)" "CFG=acct-04" "$out"
|
|
375
|
+
claude-accounts remove acct-04 --yes >/dev/null 2>&1
|
|
376
|
+
|
|
377
|
+
# ---- 15c1. add --token: portable setup-token instead of creds ------------------------
|
|
378
|
+
out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=tok@test claude-accounts add tok@test --token 2>&1)"
|
|
379
|
+
check "add --token registers via portable token" "Registered acct-04 for tok@test" "$out"
|
|
380
|
+
[ -s "$ACC/acct-04/server.token" ] && t_ok "add --token writes server.token" || t_fail "add --token" "missing"
|
|
365
381
|
out="$(CLAUDE_ACCOUNT=acct-04 claude 2>&1)"
|
|
366
|
-
check "
|
|
382
|
+
check "token account exports CLAUDE_CODE_OAUTH_TOKEN" "TOK=sk-ant-oat01-FAKE" "$out"
|
|
367
383
|
claude-accounts remove acct-04 --yes >/dev/null 2>&1
|
|
368
384
|
|
|
369
|
-
# ----
|
|
370
|
-
out="$(CLAUDE_MULTIACC_FORCE_TTY=1
|
|
371
|
-
check "add
|
|
372
|
-
|
|
385
|
+
# ---- 15c0. add with NO email: derives it from the verified sign-in -------------------
|
|
386
|
+
out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=derived@test claude-accounts add 2>&1)"
|
|
387
|
+
check "add without email registers the signed-in address" "Registered acct-04 for derived@test" "$out"
|
|
388
|
+
claude-accounts list 2>&1 | grep -q "derived@test" && t_ok "email-less add lands in the manifest" \
|
|
389
|
+
|| t_fail "email-less add" "derived@test not registered"
|
|
373
390
|
claude-accounts remove acct-04 --yes >/dev/null 2>&1
|
|
391
|
+
# add with no email, but the signed-in email is already registered => refuse + clean up
|
|
392
|
+
out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=a@test claude-accounts add 2>&1)"
|
|
393
|
+
rc=$?
|
|
394
|
+
check "email-less add refuses a duplicate signed-in email" "already registered as acct-01" "$out"
|
|
395
|
+
[ "$rc" != "0" ] && t_ok "email-less duplicate exits nonzero" || t_fail "email-less dup rc" "rc=0"
|
|
396
|
+
[ ! -d "$ACC/acct-04" ] && t_ok "email-less duplicate cleaned up" || t_fail "email-less dup cleanup" "dir left"
|
|
397
|
+
# add with no email but identity can't be read back => refuse (can't label the account)
|
|
398
|
+
out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_AUTH_FAIL=1 claude-accounts add 2>&1)"
|
|
399
|
+
rc=$?
|
|
400
|
+
check "email-less add needs a readable identity" "identity could not be read back" "$out"
|
|
401
|
+
[ ! -d "$ACC/acct-04" ] && t_ok "unverifiable email-less add cleaned up" || t_fail "unverifiable cleanup" "dir left"
|
|
374
402
|
|
|
375
|
-
# ----
|
|
376
|
-
out="$(CLAUDE_MULTIACC_FORCE_TTY=1
|
|
403
|
+
# ---- 15c2. --tui is accepted as a no-op alias (full login is the default now) --------
|
|
404
|
+
out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=tui@test claude-accounts add tui@test --tui 2>&1)"
|
|
405
|
+
check "add --tui still works (alias for default login)" "Registered acct-04 for tui@test" "$out"
|
|
406
|
+
[ -f "$ACC/acct-04/.credentials.json" ] && t_ok "--tui writes creds like the default" || t_fail "tui creds" "missing"
|
|
407
|
+
claude-accounts remove acct-04 --yes >/dev/null 2>&1
|
|
408
|
+
|
|
409
|
+
# ---- 15d. aborted login leaves zero traces ------------------------------------------
|
|
410
|
+
out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_LOGIN_FAIL=1 claude-accounts add ghost@test 2>&1)"
|
|
377
411
|
rc=$?
|
|
378
|
-
check "aborted
|
|
412
|
+
check "aborted login detected" "login failed or aborted" "$out"
|
|
379
413
|
[ "$rc" != "0" ] && t_ok "aborted add exits nonzero" || t_fail "aborted add rc" "rc=0"
|
|
380
414
|
[ ! -d "$ACC/acct-04" ] && t_ok "aborted add cleaned up its dir" || t_fail "aborted add cleanup" "dir left behind"
|
|
381
415
|
claude-accounts list 2>&1 | grep -q ghost@test && t_fail "aborted add not in manifest" "ghost@test registered" || t_ok "aborted add not in manifest"
|
|
416
|
+
# aborted --token path too
|
|
417
|
+
out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_TOKEN_FAIL=1 claude-accounts add ghost2@test --token 2>&1)"
|
|
418
|
+
rc=$?
|
|
419
|
+
check "aborted --token add detected" "sign-in failed or aborted" "$out"
|
|
420
|
+
[ ! -d "$ACC/acct-04" ] && t_ok "aborted --token add cleaned up" || t_fail "aborted token cleanup" "dir left"
|
|
382
421
|
|
|
383
422
|
# ---- 15e. sign-in as an already-registered email is rejected + cleaned ---------------
|
|
384
423
|
out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=a@test claude-accounts add brand@test 2>&1)"
|
|
@@ -389,12 +428,16 @@ check "wrong-account sign-in rejected" "already registered as acct-01" "$out"
|
|
|
389
428
|
# ---- 15f. login command completes auth for an existing auth-less account -------------
|
|
390
429
|
claude-accounts import pending@test --id acct-08 --no-sync >/dev/null 2>&1
|
|
391
430
|
out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=pending@test claude-accounts login acct-08 2>&1)"
|
|
392
|
-
check "login completes existing account" "acct-08
|
|
393
|
-
[ -
|
|
431
|
+
check "login completes existing account (full login)" "acct-08 login saved" "$out"
|
|
432
|
+
[ -f "$ACC/acct-08/.credentials.json" ] && t_ok "login writes .credentials.json" || t_fail "login creds" "missing"
|
|
394
433
|
out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=other@test claude-accounts login acct-08 2>&1)"
|
|
395
434
|
rc=$?
|
|
396
435
|
check "login email mismatch refused" "nothing saved" "$out"
|
|
397
436
|
[ "$rc" != "0" ] && t_ok "mismatched login exits nonzero" || t_fail "mismatched login rc" "rc=0"
|
|
437
|
+
# login --token writes a portable token for the same account
|
|
438
|
+
out="$(CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=pending@test claude-accounts login acct-08 --token 2>&1)"
|
|
439
|
+
check "login --token saves a portable token" "acct-08 token saved" "$out"
|
|
440
|
+
[ -s "$ACC/acct-08/server.token" ] && t_ok "login --token writes server.token" || t_fail "login token" "missing"
|
|
398
441
|
claude-accounts remove acct-08 --yes >/dev/null 2>&1
|
|
399
442
|
|
|
400
443
|
# ---- 16. CLI: limits marking via fixture endpoint ------------------------------
|