claude-multiacc 1.0.18 → 1.0.20
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/bin/claude +28 -0
- package/bin/claude-accounts +28 -5
- package/package.json +1 -1
- package/tests/run-tests.sh +36 -0
package/bin/claude
CHANGED
|
@@ -1046,13 +1046,27 @@ argv_set_model() { # $1 = model id
|
|
|
1046
1046
|
PARK_SOFT_AUTH=3600 # 1h: a mis-parked healthy account is back within the hour
|
|
1047
1047
|
PARK_SOFT_ORG=21600 # 6h: an org policy will not change in minutes
|
|
1048
1048
|
|
|
1049
|
+
# True when the tail of stdout carries a stream-json result marked is_error. Bounded
|
|
1050
|
+
# to the tail because that is where the final object lands, and because a task log
|
|
1051
|
+
# runs to hundreds of KB of transcript that must not be rescanned per attempt.
|
|
1052
|
+
stream_reported_error() {
|
|
1053
|
+
tail -c 8192 "$tmpd/out" 2>/dev/null \
|
|
1054
|
+
| LC_ALL=C grep -qE '"type"[[:space:]]*:[[:space:]]*"result"' 2>/dev/null || return 1
|
|
1055
|
+
tail -c 8192 "$tmpd/out" 2>/dev/null \
|
|
1056
|
+
| LC_ALL=C grep -qE '"is_error"[[:space:]]*:[[:space:]]*true' 2>/dev/null
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1049
1059
|
attempt=1
|
|
1050
1060
|
cur="$pick"
|
|
1051
1061
|
rc=0
|
|
1062
|
+
stream_err=0 # the run exited 0 but the stream said otherwise
|
|
1052
1063
|
rotated=0 # at most one account rotation, exactly as before
|
|
1053
1064
|
model_fb_used=0 # ...and at most one model fallback after it
|
|
1054
1065
|
ARGV=("$@")
|
|
1055
1066
|
while :; do
|
|
1067
|
+
# Per ATTEMPT: a stream error seen on an earlier account must never decide the
|
|
1068
|
+
# exit status of a later one that failed for its own, real reason.
|
|
1069
|
+
stream_err=0
|
|
1056
1070
|
tok="$(acct_token "$cur")"
|
|
1057
1071
|
if [ -n "$stdin_file" ]; then exec 3< "$stdin_file"; else exec 3< /dev/null; fi
|
|
1058
1072
|
if [ -n "$tok" ]; then
|
|
@@ -1062,6 +1076,17 @@ while :; do
|
|
|
1062
1076
|
fi
|
|
1063
1077
|
rc=$?
|
|
1064
1078
|
exec 3<&-
|
|
1079
|
+
# A `--output-format stream-json` run reports an API error INSIDE the stream and
|
|
1080
|
+
# still exits 0: the failure arrives as the final result object carrying
|
|
1081
|
+
# "is_error":true, not as a non-zero status. EVERY app-robot task takes that path,
|
|
1082
|
+
# so gating the retry branch on $rc alone meant the pool never rotated and never
|
|
1083
|
+
# fell back for the exact failures it exists for — the shim saw a clean success
|
|
1084
|
+
# every time while the task died in a second having done no work.
|
|
1085
|
+
#
|
|
1086
|
+
# Only the FINAL result counts. A 429 that appears mid-stream and is retried by
|
|
1087
|
+
# the client itself ends in a healthy result, and re-running that would throw away
|
|
1088
|
+
# a completed run.
|
|
1089
|
+
if [ "$rc" -eq 0 ] && stream_reported_error; then rc=1; stream_err=1; fi
|
|
1065
1090
|
if [ "$rc" -ne 0 ] && { [ "$rotated" = 0 ] || [ "$model_fb_used" = 0 ]; } \
|
|
1066
1091
|
&& grep -qiE "$ERRPAT" "$tmpd/out" "$tmpd/err" 2>/dev/null; then
|
|
1067
1092
|
# Atomic marker writes: a reader must never observe a half-written marker
|
|
@@ -1143,4 +1168,7 @@ done
|
|
|
1143
1168
|
|
|
1144
1169
|
cat "$tmpd/out"
|
|
1145
1170
|
cat "$tmpd/err" >&2
|
|
1171
|
+
# The exit status belongs to the CLI, not to us: a stream-json caller parses the
|
|
1172
|
+
# stream and expects 0 here. We only borrowed rc to decide whether to retry.
|
|
1173
|
+
[ "$stream_err" = 1 ] && [ "$rc" = 1 ] && rc=0
|
|
1146
1174
|
exit "$rc"
|
package/bin/claude-accounts
CHANGED
|
@@ -2206,6 +2206,13 @@ cmd_post_sync() {
|
|
|
2206
2206
|
cmd_verify --quick
|
|
2207
2207
|
}
|
|
2208
2208
|
|
|
2209
|
+
# Version of the copy at $1, read from its package.json on disk — the only answer
|
|
2210
|
+
# that describes what will actually execute.
|
|
2211
|
+
pkg_version_at() {
|
|
2212
|
+
[ -f "$1/package.json" ] || return 0
|
|
2213
|
+
sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$1/package.json" | head -1
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2209
2216
|
cmd_self_update() {
|
|
2210
2217
|
# Update the addon in place. npm global install => npm i -g @latest (its postinstall
|
|
2211
2218
|
# re-runs install.sh). git checkout => git pull + ./install.sh. Anything else is a
|
|
@@ -2217,16 +2224,32 @@ cmd_self_update() {
|
|
|
2217
2224
|
case "$REPO_DIR" in
|
|
2218
2225
|
*/node_modules/claude-multiacc|*/node_modules/claude-multiacc/*)
|
|
2219
2226
|
command -v npm >/dev/null 2>&1 || { ulog "self-update: npm not found; skipping"; return 0; }
|
|
2220
|
-
local cur lat
|
|
2221
|
-
|
|
2227
|
+
local cur lat prefix after
|
|
2228
|
+
# Update THE COPY THAT IS RUNNING, not whichever one the ambient npm prefix
|
|
2229
|
+
# happens to point at. my-mini had two global installs — homebrew's on PATH and
|
|
2230
|
+
# nvm's under `npm root -g` — and self-update kept upgrading the nvm one and
|
|
2231
|
+
# reporting success while every `claude` invocation ran the stale homebrew copy.
|
|
2232
|
+
# It sat eleven versions behind for weeks and said "already latest" throughout.
|
|
2233
|
+
# A deploy nobody runs is not a deploy, and one that announces success is worse
|
|
2234
|
+
# than one that fails.
|
|
2235
|
+
prefix="${REPO_DIR%/lib/node_modules/claude-multiacc*}"
|
|
2236
|
+
cur="$(pkg_version_at "$REPO_DIR")"
|
|
2222
2237
|
lat="$(npm view claude-multiacc version 2>/dev/null)"
|
|
2223
2238
|
if [ -n "$lat" ] && [ "$cur" = "$lat" ]; then
|
|
2224
2239
|
ulog "self-update: already latest ($cur)"
|
|
2225
2240
|
return 0
|
|
2226
2241
|
fi
|
|
2227
|
-
ulog "self-update: npm $cur -> ${lat:-latest}"
|
|
2228
|
-
if npm install -g claude-multiacc@latest
|
|
2229
|
-
|
|
2242
|
+
ulog "self-update: npm $cur -> ${lat:-latest} (prefix $prefix)"
|
|
2243
|
+
if npm install -g --prefix "$prefix" claude-multiacc@latest \
|
|
2244
|
+
>>"$ACC_ROOT/update.log" 2>&1; then
|
|
2245
|
+
# Verify by re-reading the file on disk. npm reporting success says nothing
|
|
2246
|
+
# about which tree it wrote to.
|
|
2247
|
+
after="$(pkg_version_at "$REPO_DIR")"
|
|
2248
|
+
if [ -n "$lat" ] && [ "$after" != "$lat" ]; then
|
|
2249
|
+
ulog "self-update: npm reported success but $REPO_DIR is still $after, not $lat — this install is NOT being updated"
|
|
2250
|
+
return 1
|
|
2251
|
+
fi
|
|
2252
|
+
ulog "self-update: npm update ok ($after)"
|
|
2230
2253
|
else
|
|
2231
2254
|
ulog "self-update: npm update FAILED (see update.log)"
|
|
2232
2255
|
return 1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-multiacc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"description": "Multi-account addon for Claude Code and OpenAI Codex CLI: every claude / claude -p and every codex / codex exec 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
|
@@ -90,6 +90,17 @@ if [ -f "$ctl" ] && grep -qx "orgfail:$acct" "$ctl" 2>/dev/null; then
|
|
|
90
90
|
fi
|
|
91
91
|
# A limit scoped to ONE MODEL, on EVERY account: rotating cannot help, only
|
|
92
92
|
# switching model can. Satisfied the moment the run carries the fallback model.
|
|
93
|
+
# The shape every app-robot task actually produces: --output-format stream-json
|
|
94
|
+
# reports the API error INSIDE the stream and exits 0.
|
|
95
|
+
if [ -f "$ctl" ] && grep -qx "streamlimit" "$ctl" 2>/dev/null; then
|
|
96
|
+
case " $* " in
|
|
97
|
+
*" claude-opus-5 "*|*"--model=claude-opus-5"*)
|
|
98
|
+
echo '{"type":"result","subtype":"success","is_error":false,"result":"done"}' ;;
|
|
99
|
+
*)
|
|
100
|
+
echo '{"type":"result","subtype":"success","is_error":true,"api_error_status":429,"result":"You'"'"'ve reached your Fable 5 limit. Switch to another model, or manage usage credits to continue."}'
|
|
101
|
+
exit 0 ;;
|
|
102
|
+
esac
|
|
103
|
+
fi
|
|
93
104
|
if [ -f "$ctl" ] && grep -qx "modellimit" "$ctl" 2>/dev/null; then
|
|
94
105
|
case " $* " in
|
|
95
106
|
*" claude-opus-5 "*|*"--model=claude-opus-5"*) : ;;
|
|
@@ -601,6 +612,31 @@ case "$out" in
|
|
|
601
612
|
esac
|
|
602
613
|
rm -f "$FAKE_CTL" "$ACC"/acct-*/.limited
|
|
603
614
|
|
|
615
|
+
# ---- 12a2. an in-stream API error still triggers recovery --------------------
|
|
616
|
+
# With --output-format stream-json the CLI exits 0 and reports the 429 as the final
|
|
617
|
+
# result object. Every app-robot task takes that path, so gating retry on the exit
|
|
618
|
+
# status alone meant the pool never rotated and never fell back for the exact
|
|
619
|
+
# failures it exists for — while the shim recorded a clean success every time.
|
|
620
|
+
rm -f "$ACC"/acct-*/.limited; : > "$ACC/selection.log"
|
|
621
|
+
echo "streamlimit" > "$FAKE_CTL"
|
|
622
|
+
out="$(claude -p --model claude-fable-5 --output-format stream-json hello < /dev/null 2>/dev/null)"
|
|
623
|
+
rc=$?
|
|
624
|
+
case "$out" in
|
|
625
|
+
*'"is_error":false'*) t_ok "an in-stream API error is recovered, not reported as success" ;;
|
|
626
|
+
*) t_fail "stream error recovery" "rc=$rc out=$out" ;;
|
|
627
|
+
esac
|
|
628
|
+
[ "$rc" = "0" ] && t_ok "...and the caller still gets exit 0, as the CLI would give" \
|
|
629
|
+
|| t_fail "stream error exit status" "rc=$rc"
|
|
630
|
+
|
|
631
|
+
# A stream that merely MENTIONS a limit mid-run and then completes must be left alone:
|
|
632
|
+
# re-running it would throw away a finished task.
|
|
633
|
+
rm -f "$FAKE_CTL" "$ACC"/acct-*/.limited; : > "$ACC/selection.log"
|
|
634
|
+
out="$(claude -p --model claude-fable-5 hello < /dev/null 2>/dev/null)"
|
|
635
|
+
grep -q "retry from=" "$ACC/selection.log" 2>/dev/null \
|
|
636
|
+
&& t_fail "spurious retry" "a healthy run was retried" \
|
|
637
|
+
|| t_ok "a healthy run is never retried"
|
|
638
|
+
rm -f "$FAKE_CTL" "$ACC"/acct-*/.limited
|
|
639
|
+
|
|
604
640
|
# ---- 12b. pipe stdin skips retry buffering but passes bytes through -----------
|
|
605
641
|
out="$(printf 'pipe-data' | claude -p --echo-stdin 2>/dev/null)"
|
|
606
642
|
[ "$out" = "pipe-data" ] && t_ok "pipe stdin passes through (no retry buffering)" || t_fail "pipe stdin passthrough" "got: $out"
|