"""Multi-account credential store. A user hands the CLI a session cookie once (paste it, or pass ``TRIPO_COOKIE``); from then on the CLI mints short-lived JWTs on demand over plain HTTP — no browser, no repeated login. ~/.tripo/accounts.json { "active": "main", "accounts": { "main": {"cookie": "...", "token": "...", "session_expires_at": "...", "user_id": "...", "email": "...", "added_at": 1791800000} } } """ import json import time from pathlib import Path DEFAULT_ACCOUNTS_PATH = Path.home() / ".tripo" / "accounts.json" class Accounts: def __init__(self, path=None): self.path = Path(path) if path else DEFAULT_ACCOUNTS_PATH self.data = {"active": None, "accounts": {}} self.load() # --- persistence --- def load(self): if self.path.exists(): try: loaded = json.loads(self.path.read_text(encoding="utf-8")) self.data = { "active": loaded.get("active"), "accounts": loaded.get("accounts", {}), } except Exception: self.data = {"active": None, "accounts": {}} def save(self): self.path.parent.mkdir(parents=True, exist_ok=True) self.path.write_text(json.dumps(self.data, indent=2, ensure_ascii=False), encoding="utf-8") try: self.path.chmod(0o600) # holds a session cookie: keep it private except OSError: pass # --- queries --- def names(self): return sorted(self.data["accounts"]) def rotation(self, start=None): """Account names starting at ``start`` (default: active), wrapping around. Used by the optional auto-fallback so a command can try the next account when the current one is out of credits. """ names = self.names() if not names: return [] start = start or self.data.get("active") if start not in names: return names index = names.index(start) return names[index:] + names[:index] def exists(self, name): return name in self.data["accounts"] def active_name(self): return self.data.get("active") def get(self, name=None): """Return ``(name, record)`` for ``name`` or the active account.""" name = name or self.data.get("active") if not name or name not in self.data["accounts"]: return None, None return name, self.data["accounts"][name] # --- mutations --- def add(self, name, cookie, token=None, session_expires_at=None, user_id=None, email=None): record = self.data["accounts"].get(name, {}) record.update({ "cookie": cookie, "token": token, "session_expires_at": session_expires_at, "user_id": user_id, "email": email, "added_at": record.get("added_at") or int(time.time()), "updated_at": int(time.time()), }) self.data["accounts"][name] = record if not self.data.get("active"): self.data["active"] = name self.save() return name def use(self, name): if name not in self.data["accounts"]: raise KeyError(name) self.data["active"] = name self.save() def remove(self, name): if name not in self.data["accounts"]: raise KeyError(name) del self.data["accounts"][name] if self.data.get("active") == name: self.data["active"] = self.names()[0] if self.data["accounts"] else None self.save() def update_token(self, name, token, session_expires_at=None): record = self.data["accounts"].get(name) if record is None: return record["token"] = token record["updated_at"] = int(time.time()) if session_expires_at: record["session_expires_at"] = session_expires_at self.save() def update_cookie(self, name, cookie): record = self.data["accounts"].get(name) if record is None: return record["cookie"] = cookie self.save()