Skip to content

Commit d1a9460

Browse files
committed
Add support for additional Claude accounts
1 parent 63b34bc commit d1a9460

File tree

8 files changed

+962
-96
lines changed

8 files changed

+962
-96
lines changed

configs/_common.sh

Lines changed: 298 additions & 34 deletions
Large diffs are not rendered by default.

scripts/account_health.py

Lines changed: 491 additions & 0 deletions
Large diffs are not rendered by default.

scripts/check_infra.py

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import time
1919
from pathlib import Path
2020

21+
from account_health import collect_account_status, discover_account_homes
22+
2123
REAL_HOME = os.environ.get("HOME", os.path.expanduser("~"))
2224

2325

@@ -75,7 +77,7 @@ def try_refresh_token(creds_file: Path) -> dict | None:
7577
return {"expires_in": expires_in, "remaining_min": expires_in // 60}
7678

7779

78-
def check_oauth_token(home_dir: str | None = None) -> dict:
80+
def check_oauth_token(home_dir: str | None = None, allow_refresh: bool = False) -> dict:
7981
"""Check OAuth token validity and time remaining."""
8082
home = home_dir or REAL_HOME
8183
creds_file = Path(home) / ".claude" / ".credentials.json"
@@ -107,8 +109,8 @@ def check_oauth_token(home_dir: str | None = None) -> dict:
107109
has_refresh = bool(oauth.get("refreshToken"))
108110

109111
if remaining_s <= 0:
110-
# Access token expired — try to refresh it
111-
if has_refresh:
112+
if has_refresh and allow_refresh:
113+
# Access token expired — optional refresh path
112114
refresh_result = try_refresh_token(creds_file)
113115
if refresh_result:
114116
return {
@@ -128,12 +130,14 @@ def check_oauth_token(home_dir: str | None = None) -> dict:
128130
"has_refresh_token": has_refresh,
129131
"home": home,
130132
}
133+
status = "WARN" if has_refresh else "FAIL"
134+
action = "refresh/login recommended" if has_refresh else "Run: claude login"
131135
return {
132136
"check": "oauth_token",
133-
"status": "FAIL",
134-
"message": f"Token EXPIRED ({abs(remaining_min)} min ago), no refresh token. Run: claude login",
137+
"status": status,
138+
"message": f"Token EXPIRED ({abs(remaining_min)} min ago), {action}",
135139
"remaining_minutes": remaining_min,
136-
"has_refresh_token": False,
140+
"has_refresh_token": has_refresh,
137141
"home": home,
138142
}
139143
elif remaining_s < 1800: # < 30 min
@@ -156,32 +160,29 @@ def check_oauth_token(home_dir: str | None = None) -> dict:
156160
}
157161

158162

159-
def check_multi_account_tokens() -> list[dict]:
163+
def check_multi_account_tokens(allow_refresh: bool = False) -> list[dict]:
160164
"""Check tokens for all accounts under ~/.claude-homes/."""
161-
results = []
162-
homes_dir = Path(REAL_HOME) / ".claude-homes"
163-
164-
if not homes_dir.is_dir():
165-
# Single account mode
166-
results.append(check_oauth_token())
167-
return results
168-
169-
account_num = 1
170-
found_any = False
171-
while True:
172-
account_home = homes_dir / f"account{account_num}"
173-
creds = account_home / ".claude" / ".credentials.json"
174-
if creds.is_file():
175-
found_any = True
176-
results.append(check_oauth_token(str(account_home)))
177-
account_num += 1
178-
else:
179-
break
165+
real_home_path = Path(REAL_HOME)
166+
return [
167+
check_oauth_token(
168+
None if home == real_home_path else str(home),
169+
allow_refresh=allow_refresh,
170+
)
171+
for home in discover_account_homes(real_home_path)
172+
]
180173

181-
if not found_any:
182-
results.append(check_oauth_token())
183174

184-
return results
175+
def check_account_readiness() -> dict:
176+
"""Summarize launch-safe accounts using deterministic local signals."""
177+
report = collect_account_status()
178+
summary = report["summary"]
179+
action = report["recommended_action"]
180+
status = "OK" if report["ok_to_launch"] else "FAIL"
181+
return {
182+
"check": "account_readiness",
183+
"status": status,
184+
"message": f"{summary}; action={action}",
185+
}
185186

186187

187188
def check_env_local() -> dict:
@@ -380,7 +381,7 @@ def format_table(results: list[dict]) -> str:
380381
elif warns:
381382
lines.append(f"\033[93mREADY with {warns} warning(s). Runs may partially fail.\033[0m")
382383
else:
383-
lines.append(f"\033[92mALL CLEAR: Infrastructure ready for benchmark runs.\033[0m")
384+
lines.append("\033[92mALL CLEAR: Infrastructure ready for benchmark runs.\033[0m")
384385

385386
return "\n".join(lines)
386387

@@ -390,12 +391,18 @@ def main():
390391
description="Check infrastructure readiness before benchmark runs."
391392
)
392393
parser.add_argument("--format", choices=["table", "json"], default="table")
394+
parser.add_argument(
395+
"--refresh-tokens",
396+
action="store_true",
397+
help="Allow OAuth token refresh during infra check.",
398+
)
393399
args = parser.parse_args()
394400

395401
results = []
396402

397403
# Token checks (handles multi-account)
398-
results.extend(check_multi_account_tokens())
404+
results.extend(check_multi_account_tokens(allow_refresh=args.refresh_tokens))
405+
results.append(check_account_readiness())
399406

400407
# Environment
401408
results.append(check_env_local())

scripts/daytona_curator_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1613,7 +1613,7 @@ def main() -> int:
16131613
log.error("DAYTONA_API_KEY required. Set in env or ~/.config/daytona/env.sh")
16141614
return 1
16151615
if not creds.get("oauth_creds"):
1616-
log.error("OAuth credentials required. Check ~/.claude-homes/account1/.claude/.credentials.json")
1616+
log.error("OAuth credentials required. Check ~/.claude-homes/accountN/.claude/.credentials.json")
16171617
return 1
16181618

16191619
# Dispatch to appropriate mode

scripts/daytona_poc_runner.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import argparse
2525
import json
2626
import os
27+
import re
2728
import sys
2829
import time
2930
import urllib.request
@@ -85,6 +86,7 @@ def load_src_access_token():
8586
OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" # Official Claude Code CLI client ID
8687
OAUTH_TOKEN_URL = "https://console.anthropic.com/api/oauth/token"
8788
REFRESH_MARGIN = 1800 # 30 minutes — refresh if token expires within this window
89+
ACCOUNT_NAME_RE = re.compile(r"account(\d+)$")
8890

8991

9092
def _account_creds_path(account_num):
@@ -97,19 +99,39 @@ def _account_home(account_num):
9799
return Path.home() / ".claude-homes" / f"account{account_num}"
98100

99101

102+
def _discover_account_numbers():
103+
"""Return configured account numbers under ~/.claude-homes/accountN."""
104+
homes_dir = Path.home() / ".claude-homes"
105+
if not homes_dir.is_dir():
106+
return []
107+
108+
account_numbers = []
109+
for path in homes_dir.iterdir():
110+
if not path.is_dir():
111+
continue
112+
match = ACCOUNT_NAME_RE.fullmatch(path.name)
113+
if match:
114+
account_numbers.append(int(match.group(1)))
115+
return sorted(account_numbers)
116+
117+
100118
def list_oauth_accounts():
101119
"""List available OAuth accounts and their token status."""
102120
accounts = []
103-
num = 1
104-
while True:
121+
for num in _discover_account_numbers():
105122
creds_path = _account_creds_path(num)
106123
if not creds_path.exists():
107124
# Also try without leading dot
108125
alt_path = creds_path.parent / "credentials.json"
109126
if alt_path.exists():
110127
creds_path = alt_path
111128
else:
112-
break
129+
accounts.append({
130+
"num": num,
131+
"path": str(creds_path),
132+
"error": "missing credentials",
133+
})
134+
continue
113135
try:
114136
creds = json.loads(creds_path.read_text())
115137
oauth = creds.get("claudeAiOauth", {})
@@ -126,7 +148,6 @@ def list_oauth_accounts():
126148
})
127149
except Exception as e:
128150
accounts.append({"num": num, "path": str(creds_path), "error": str(e)})
129-
num += 1
130151
return accounts
131152

132153

@@ -551,7 +572,7 @@ def main():
551572
)
552573
parser.add_argument(
553574
"--account", type=int, default=1,
554-
help="OAuth account number (1-3). Used with --auth oauth. Default: 1",
575+
help="OAuth account number under ~/.claude-homes/accountN. Default: 1",
555576
)
556577
parser.add_argument(
557578
"--list-accounts", action="store_true",
@@ -566,7 +587,7 @@ def main():
566587
print("No OAuth accounts found at ~/.claude-homes/accountN/.claude/.credentials.json")
567588
print("\nTo set up accounts, create the directory structure:")
568589
print(" mkdir -p ~/.claude-homes/account1/.claude")
569-
print(" # Then run: HOME=~/.claude-homes/account1 claude (to login via browser)")
590+
print(" # Or run: python3 scripts/headless_login.py --account 1")
570591
else:
571592
print(f"Found {len(accounts)} OAuth account(s):\n")
572593
for a in accounts:
@@ -598,8 +619,7 @@ def main():
598619
print(f"ERROR: Failed to load OAuth credentials for account {args.account}: {e}")
599620
print("\nTo set up OAuth accounts:")
600621
print(f" mkdir -p ~/.claude-homes/account{args.account}/.claude")
601-
print(f" HOME=~/.claude-homes/account{args.account} claude")
602-
print(" # Complete browser login, then credentials are saved automatically")
622+
print(f" python3 scripts/headless_login.py --account {args.account}")
603623
sys.exit(1)
604624
else:
605625
anthropic_key = load_anthropic_api_key()

scripts/daytona_runner.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import json
2828
import logging
2929
import os
30+
import re
3031
import sys
3132
import time
3233
import urllib.error
@@ -66,6 +67,7 @@
6667
}
6768

6869
MCP_CONFIGS = {"mcp-remote-direct", "mcp-remote-artifact"}
70+
ACCOUNT_NAME_RE = re.compile(r"account(\d+)$")
6971

7072

7173
def resolve_dockerfile_name(task: "TaskSpec", config_name: str) -> str:
@@ -134,17 +136,36 @@ def _account_creds_path(account_num: int) -> Path:
134136
return Path.home() / ".claude-homes" / f"account{account_num}" / ".claude" / ".credentials.json"
135137

136138

139+
def _discover_account_numbers() -> List[int]:
140+
homes_dir = Path.home() / ".claude-homes"
141+
if not homes_dir.is_dir():
142+
return []
143+
144+
account_numbers: List[int] = []
145+
for path in homes_dir.iterdir():
146+
if not path.is_dir():
147+
continue
148+
match = ACCOUNT_NAME_RE.fullmatch(path.name)
149+
if match:
150+
account_numbers.append(int(match.group(1)))
151+
return sorted(account_numbers)
152+
153+
137154
def list_oauth_accounts() -> List[dict]:
138155
accounts = []
139-
num = 1
140-
while True:
156+
for num in _discover_account_numbers():
141157
creds_path = _account_creds_path(num)
142158
if not creds_path.exists():
143159
alt_path = creds_path.parent / "credentials.json"
144160
if alt_path.exists():
145161
creds_path = alt_path
146162
else:
147-
break
163+
accounts.append({
164+
"num": num,
165+
"path": str(creds_path),
166+
"error": "missing credentials",
167+
})
168+
continue
148169
try:
149170
creds = json.loads(creds_path.read_text())
150171
oauth = creds.get("claudeAiOauth", {})
@@ -159,7 +180,6 @@ def list_oauth_accounts() -> List[dict]:
159180
})
160181
except Exception as e:
161182
accounts.append({"num": num, "path": str(creds_path), "error": str(e)})
162-
num += 1
163183
return accounts
164184

165185

0 commit comments

Comments
 (0)