claude-multiacc 2.0.28 → 2.0.29

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.
@@ -2482,6 +2482,21 @@ AUTH_ERR = re.compile(
2482
2482
  ORG_ERR = re.compile(
2483
2483
  r'organization has disabled|subscription access.*disabl|disabled claude subscription'
2484
2484
  r'|ask your admin to enable|not authorized to use claude code', re.I)
2485
+ # EXCULPATORY, tested FIRST — the twin of BROKEN_CLI in bin/codex-accounts, and
2486
+ # it matters MORE here: AUTH_ERR above matches a bare `401` or `403`, so any
2487
+ # machine fault whose output happens to contain those three digits parks a live
2488
+ # login. `mark_expired` writes no `soft_until`, so that verdict is permanent
2489
+ # until a person clears it. These patterns say the CLI never reached the API,
2490
+ # which is a fact about the machine and never about the login.
2491
+ # See health.log 2026-08-24: `env: node: No such file or directory` failed
2492
+ # every account in one pass.
2493
+ BROKEN_CLI = re.compile(
2494
+ r'missing optional dependency|unsupported (platform|target triple):'
2495
+ r'|cannot find module|ERR_MODULE_NOT_FOUND|ERR_DLOPEN_FAILED'
2496
+ r'|env: node: no such file or directory|node: command not found'
2497
+ r'|spawn .{0,80}(ENOENT|EACCES|EPERM)|^dyld\[[0-9]+\]:'
2498
+ r'|cannot execute binary file|bad CPU type in executable|exec format error',
2499
+ re.I | re.M)
2485
2500
 
2486
2501
  def mark_expired(d, slug, detail=''):
2487
2502
  """Park an account the shim must stop selecting. Verify is the strongest signal
@@ -2607,9 +2622,16 @@ for acct in manifest.get('accounts', []):
2607
2622
  pass
2608
2623
  print(f'{aid} {acct["email"]}: PASS ({dt:.1f}s) -> {out[:60]!r}')
2609
2624
  else:
2610
- err = (r.stderr or '').strip()[:200]
2625
+ raw = (r.stderr or '').strip()
2626
+ err = raw[:200]
2611
2627
  hint = ''
2612
- if ORG_ERR.search(out) or ORG_ERR.search(err):
2628
+ # STDERR only, and the WHOLE of it: stdout is the model's own answer,
2629
+ # and a session that merely discusses "Cannot find module" must not be
2630
+ # able to excuse a real dead login.
2631
+ if BROKEN_CLI.search(raw):
2632
+ hint = (' — the Claude CLI cannot run on this machine; not a login '
2633
+ 'problem, nothing was parked')
2634
+ elif ORG_ERR.search(out) or ORG_ERR.search(err):
2613
2635
  # Not an auth problem: the account authenticates fine, its organization
2614
2636
  # has simply turned Claude Code subscription access off. Park it — a
2615
2637
  # re-login changes nothing — and say what actually helps.
@@ -1768,6 +1768,30 @@ ORG_ERR = re.compile(
1768
1768
  r'disabled by (your )?(workspace )?admin|admin (has )?disabled'
1769
1769
  r'|(workspace|organization) has disabled (codex|chatgpt)'
1770
1770
  r'|codex.{0,20}disabled for (your|this) (workspace|organization)', re.I)
1771
+ # EXCULPATORY, and therefore tested FIRST. These say the CLI never reached the
1772
+ # API at all — a missing native package, a missing node, an unrunnable binary —
1773
+ # so they are facts about this MACHINE, not about the login. Parking on one
1774
+ # converts an indeterminate probe into "this login is dead", and that verdict
1775
+ # does not self-heal: `mark_expired` here writes no `soft_until`, so the shim
1776
+ # reads it as proven dead and keeps it, while the panel refuses to re-import
1777
+ # the token until its fingerprint changes.
1778
+ #
1779
+ # Not hypothetical. health.log 2026-08-24T06:37:02Z: all four accounts
1780
+ # `FAIL rc=127 err='env: node: No such file or directory'` — one broken PATH
1781
+ # reddened the entire matrix, and it escaped parking only because that exact
1782
+ # string happens not to match AUTH_ERR. `Missing optional dependency
1783
+ # @openai/codex-<platform>` (my-mini, 2026-09-10) is the same class.
1784
+ #
1785
+ # Keep it NARROW. It must never contain a word a genuine auth failure uses
1786
+ # ("permission denied", "forbidden", a bare "not found"), or a real dead login
1787
+ # would be excused as a broken machine. Widen only with a test.
1788
+ BROKEN_CLI = re.compile(
1789
+ r'missing optional dependency|unsupported (platform|target triple):'
1790
+ r'|cannot find module|ERR_MODULE_NOT_FOUND|ERR_DLOPEN_FAILED'
1791
+ r'|env: node: no such file or directory|node: command not found'
1792
+ r'|spawn .{0,80}(ENOENT|EACCES|EPERM)|^dyld\[[0-9]+\]:'
1793
+ r'|cannot execute binary file|bad CPU type in executable|exec format error',
1794
+ re.I | re.M)
1771
1795
 
1772
1796
  def mark_expired(d, slug, detail=''):
1773
1797
  """Park an account the shim must stop selecting. Verify is the strongest signal
@@ -1845,9 +1869,16 @@ for acct in manifest.get('accounts', []):
1845
1869
  pass
1846
1870
  print(f'{aid} {acct["email"]}: PASS ({dt:.1f}s) -> {(last or out)[:60]!r}')
1847
1871
  else:
1848
- err = (r.stderr or '').strip()[:200]
1872
+ raw = (r.stderr or '').strip()
1873
+ err = raw[:200]
1849
1874
  hint = ''
1850
- if ORG_ERR.search(out) or ORG_ERR.search(err):
1875
+ # STDERR only, and the WHOLE of it: stdout is the model's own answer,
1876
+ # and a session that merely discusses "Cannot find module" must not be
1877
+ # able to excuse a real dead login.
1878
+ if BROKEN_CLI.search(raw):
1879
+ hint = (' — the Codex CLI cannot run on this machine; not a login '
1880
+ 'problem, nothing was parked')
1881
+ elif ORG_ERR.search(out) or ORG_ERR.search(err):
1851
1882
  # Not an auth problem: the account authenticates fine, a workspace admin
1852
1883
  # has simply turned Codex access off. Park it — a re-login changes
1853
1884
  # nothing — and say what actually helps.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.28",
3
+ "version": "2.0.29",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -136,6 +136,15 @@ case "${CLAUDE_CODE_OAUTH_TOKEN:-}" in
136
136
  *) echo "Failed to authenticate. API Error: 401 OAuth access token is invalid." >&2; exit 1 ;;
137
137
  esac ;;
138
138
  esac
139
+ if [ -f "$ctl" ] && grep -qx "brokencli:$acct" "$ctl" 2>/dev/null; then
140
+ # An infrastructure fault, not a login verdict: the CLI never reached the API.
141
+ # Verbatim from my-mini, 2026-09-10. Note it carries no auth vocabulary at all
142
+ # — the danger is that AUTH_ERR matches a bare `401`, and a node stack frame
143
+ # or a version string can contain one.
144
+ echo "Error: Missing optional dependency @openai/codex-darwin-arm64. Reinstall Codex: npm install -g @openai/codex@latest" >&2
145
+ echo " at findCodexExecutable (file:///opt/homebrew/lib/node_modules/@openai/codex/bin/codex.js:107:9)" >&2
146
+ exit 1
147
+ fi
139
148
  case " $* " in
140
149
  *" -p --output-format text --max-turns 1 "*) echo "OK"; exit 0 ;;
141
150
  esac
@@ -3898,6 +3907,27 @@ check "import rejects an API key as token" "not a subscription setup-token" "$ou
3898
3907
  out="$(printf 'sk-ant-api03-ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ' | claude-accounts mint acct-01 --paste 2>&1)"
3899
3908
  check "mint --paste rejects an API key" "not a subscription setup-token" "$out"
3900
3909
 
3910
+ # ---- 17a2. a CLI that cannot run is not a dead login ----------------------------------
3911
+ # health.log 2026-08-24T06:37:02Z: all four accounts FAIL rc=127
3912
+ # err='env: node: No such file or directory'. One broken PATH reddened the whole
3913
+ # verify matrix, and it escaped parking only because that string happens not to
3914
+ # match AUTH_ERR — which matches a bare `401`, so a node stack frame carrying one
3915
+ # is enough. `mark_expired` writes no soft stamp, so such a park is permanent,
3916
+ # and the panel then refuses to re-import the token until its fingerprint changes.
3917
+ rm -f "$ACC/acct-01/.expired"
3918
+ printf 'brokencli:acct-01\n' > "$WORK/ctl-brokencli"
3919
+ out="$(FAKE_CTL="$WORK/ctl-brokencli" claude-accounts verify 2>&1)"
3920
+ check "a broken CLI is reported as a machine fault" "cannot run on this machine" "$out"
3921
+ check "a broken CLI still fails the account" "acct-01" "$out"
3922
+ [ ! -f "$ACC/acct-01/.expired" ] \
3923
+ && t_ok "a broken CLI never parks a live login" \
3924
+ || t_fail "broken CLI parking" "verify parked an account over an infrastructure fault"
3925
+ case "$out" in
3926
+ *"login is dead"*) t_fail "broken CLI wording" "verify blamed the login for a machine fault" ;;
3927
+ *) t_ok "a broken CLI is not called a dead login" ;;
3928
+ esac
3929
+ rm -f "$WORK/ctl-brokencli"
3930
+
3901
3931
  # ---- 17b. verify authenticates the way the SHIM would ---------------------------------
3902
3932
  # A dead credential next to a live portable token: the shim runs that account with the
3903
3933
  # TOKEN, so verify must too — testing it with the dead credential would fail a healthy