#!/usr/bin/env bash
# claude-acct — seamless Claude CLI account rotation via keychain token-swap.
# Login to each account once (OAuth), `save` it under a name, then `use <name>`
# to switch with zero re-authentication. Tokens auto-refresh; switching back is instant.
#
# Per-session pinning (`token` / `pin` / `unpin` / `sessions`): pins a worktree to
# one account by writing env.CLAUDE_CODE_OAUTH_TOKEN into its
# .claude/settings.local.json. Env beats keychain in Claude's auth precedence, so
# pinned sessions keep their account no matter what `use` does globally. Pins use
# 1-year tokens from `claude setup-token` (stored as <name>.oat), NOT the keychain
# blobs — blob access tokens expire in hours and an expired pin silently falls
# back to the keychain account.
set -euo pipefail

SVC="Claude Code-credentials"
ACCT="${USER}"                       # macOS keychain account attr Claude uses
DIR="${HOME}/.claude/accounts"
ACTIVE="${DIR}/.active"
CLAUDE_JSON="${HOME}/.claude.json"
mkdir -p "$DIR"; chmod 700 "$DIR"

read_keychain() { security find-generic-password -s "$SVC" -a "$ACCT" -w 2>/dev/null; }
write_keychain() { security add-generic-password -U -s "$SVC" -a "$ACCT" -w "$1"; }
active_name() { cat "$ACTIVE" 2>/dev/null || true; }
blob_hash() { shasum -a 256 | awk '{print $1}'; }
file_hash() { [ -f "$1" ] && blob_hash < "$1"; }

auth_email() {
  claude auth status --json 2>/dev/null \
    | node -e 'let s=""; process.stdin.on("data",d=>s+=d); process.stdin.on("end",()=>{try{const j=JSON.parse(s); if (j.email) process.stdout.write(j.email)}catch{}})'
}

cached_oauth_email() {
  node -e 'const fs=require("fs"); const p=process.env.HOME+"/.claude.json"; try { const j=JSON.parse(fs.readFileSync(p,"utf8")); if (j.oauthAccount?.emailAddress) process.stdout.write(j.oauthAccount.emailAddress); } catch {}'
}

save_meta() {
  local name="$1" email="${2:-}"
  [ -n "$email" ] || return 0
  printf 'email=%s\n' "$email" > "${DIR}/${name}.meta"; chmod 600 "${DIR}/${name}.meta"
}

save_oauth_account_meta() {
  local name="$1"
  [ -f "$CLAUDE_JSON" ] || return 0
  node -e '
    const fs = require("fs");
    const src = process.argv[1], dest = process.argv[2];
    try {
      const j = JSON.parse(fs.readFileSync(src, "utf8"));
      if (j.oauthAccount) fs.writeFileSync(dest, JSON.stringify(j.oauthAccount, null, 2) + "\n", { mode: 0o600 });
    } catch {}
  ' "$CLAUDE_JSON" "${DIR}/${name}.oauthAccount.json"
  [ -f "${DIR}/${name}.oauthAccount.json" ] && chmod 600 "${DIR}/${name}.oauthAccount.json"
  return 0
}

apply_oauth_account_meta() {
  local name="$1" meta="${DIR}/${name}.oauthAccount.json"
  [ -f "$CLAUDE_JSON" ] || return 0
  if [ -f "$meta" ]; then
    node -e '
      const fs = require("fs");
      const cfg = process.argv[1], meta = process.argv[2];
      const j = JSON.parse(fs.readFileSync(cfg, "utf8"));
      j.oauthAccount = JSON.parse(fs.readFileSync(meta, "utf8"));
      fs.writeFileSync(cfg, JSON.stringify(j, null, 2) + "\n", { mode: 0o600 });
    ' "$CLAUDE_JSON" "$meta"
  else
    node -e '
      const fs = require("fs");
      const cfg = process.argv[1];
      const j = JSON.parse(fs.readFileSync(cfg, "utf8"));
      delete j.oauthAccount;
      fs.writeFileSync(cfg, JSON.stringify(j, null, 2) + "\n", { mode: 0o600 });
    ' "$CLAUDE_JSON"
    echo "Warning: '${name}' has no saved oauthAccount display cache; Claude /status will show email=null until you re-save or re-add that account." >&2
  fi
}

meta_email() {
  local name="$1" file="${DIR}/${name}.meta"
  [ -f "$file" ] || return 1
  sed -n 's/^email=//p' "$file" | head -n 1
}

name_from_live_email() {
  local email="$1" f n meta local_part
  [ -n "$email" ] || return 1
  for f in "${DIR}"/*.meta; do
    [ -e "$f" ] || continue
    n="$(basename "$f" .meta)"
    meta="$(sed -n 's/^email=//p' "$f" | head -n 1)"
    [ "$meta" = "$email" ] && { printf '%s' "$n"; return 0; }
  done
  local_part="${email%@*}"
  [ -f "${DIR}/${local_part}.json" ] && { printf '%s' "$local_part"; return 0; }
  return 1
}

name_from_exact_blob() {
  local blob="$1" h f n
  [ -n "$blob" ] || return 1
  h="$(printf '%s' "$blob" | blob_hash)"
  for f in "${DIR}"/*.json; do
    [ -e "$f" ] || continue
    [ "$(file_hash "$f")" = "$h" ] || continue
    n="$(basename "$f" .json)"
    case "$n" in *.oauthAccount) continue;; esac
    [ "$n" = "usage-history" ] && continue
    printf '%s' "$n"
    return 0
  done
  return 1
}

write_account_file() {
  local name="$1" blob="$2"
  printf '%s' "$blob" > "${DIR}/${name}.json"; chmod 600 "${DIR}/${name}.json"
}

# Snapshot whatever account is currently live back into its named file, so any
# refreshed/rotated tokens are preserved before we overwrite the keychain.
snapshot_active() {
  local name blob email resolved email_resolved exact_resolved stamp
  blob="$(read_keychain)" || return 0
  [ -z "$blob" ] && return 0
  email="$(auth_email || true)"
  [ -n "$email" ] || email="$(cached_oauth_email || true)"
  exact_resolved="$(name_from_exact_blob "$blob" 2>/dev/null || true)"
  email_resolved="$(name_from_live_email "$email" 2>/dev/null || true)"
  resolved="$exact_resolved"
  [ -n "$resolved" ] || resolved="$email_resolved"
  name="$(active_name)"
  if [ -n "$resolved" ]; then
    write_account_file "$resolved" "$blob"
    if [ -n "$email" ] && [ "$resolved" = "$email_resolved" ]; then
      save_meta "$resolved" "$email"
      save_oauth_account_meta "$resolved"
    fi
    printf '%s' "$resolved" > "$ACTIVE"
    return 0
  fi
  if [ -n "$name" ]; then
    stamp="$(date +%Y%m%d-%H%M%S)"
    write_account_file "unsaved-live-${stamp}" "$blob"
    save_meta "unsaved-live-${stamp}" "$email"
    save_oauth_account_meta "unsaved-live-${stamp}"
    echo "Warning: live Claude credential did not match saved account '${name}'; saved it as 'unsaved-live-${stamp}' instead of overwriting '${name}'." >&2
  fi
}

print_auth_status() {
  claude auth status --json 2>/dev/null \
    | node -e 'let s=""; process.stdin.on("data",d=>s+=d); process.stdin.on("end",()=>{try{const j=JSON.parse(s); const parts=[]; if (j.email) parts.push(j.email); if (j.subscriptionType) parts.push(j.subscriptionType); if (parts.length) console.log("Claude auth: "+parts.join(" · "))}catch{}})' \
    || true
}

usage_report() {
  node - "$@" <<'NODE'
const fs = require("fs");
const https = require("https");
const path = require("path");

const args = process.argv.slice(2);
const asJson = args.includes("--json");
const noHistory = args.includes("--no-history");
const dir = path.join(process.env.HOME, ".claude", "accounts");
const historyPath = path.join(dir, "usage-history.json");
const activePath = path.join(dir, ".active");
const selected = fs.existsSync(activePath) ? fs.readFileSync(activePath, "utf8").trim() : "";

function readJson(file) {
  try {
    return JSON.parse(fs.readFileSync(file, "utf8"));
  } catch {
    return null;
  }
}

function metaEmail(name) {
  const meta = path.join(dir, `${name}.meta`);
  if (fs.existsSync(meta)) {
    const match = fs.readFileSync(meta, "utf8").match(/^email=(.+)$/m);
    if (match) return match[1];
  }
  const oauth = readJson(path.join(dir, `${name}.oauthAccount.json`));
  return oauth?.emailAddress || null;
}

function cachedAuthEmail() {
  const cfg = readJson(path.join(process.env.HOME, ".claude.json"));
  return cfg?.oauthAccount?.emailAddress || null;
}

function request(token, endpoint) {
  return new Promise((resolve) => {
    if (!token) {
      resolve({ ok: false, status: 0, error: "missing access token" });
      return;
    }
    const req = https.request(
      {
        hostname: "claude.ai",
        path: endpoint,
        method: "GET",
        timeout: 15000,
        headers: {
          authorization: `Bearer ${token}`,
          accept: "application/json",
          "anthropic-client-platform": "cli",
          "user-agent": "Claude-Code/2.1.170 claude-acct-usage",
        },
      },
      (res) => {
        let body = "";
        res.on("data", (chunk) => {
          body += chunk;
        });
        res.on("end", () => {
          let data = null;
          try {
            data = JSON.parse(body);
          } catch {
            data = body;
          }
          resolve({
            ok: res.statusCode >= 200 && res.statusCode < 300,
            status: res.statusCode,
            data,
          });
        });
      },
    );
    req.on("error", (error) => resolve({ ok: false, status: 0, error: error.message }));
    req.on("timeout", () => {
      req.destroy();
      resolve({ ok: false, status: 0, error: "timeout" });
    });
    req.end();
  });
}

function pct(value) {
  return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : null;
}

function resetText(value) {
  if (!value) return "reset unknown";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return "reset unknown";
  const now = Date.now();
  const deltaMs = date.getTime() - now;
  const abs = Math.abs(deltaMs);
  const hours = Math.floor(abs / 3_600_000);
  const minutes = Math.round((abs % 3_600_000) / 60_000);
  const rel = deltaMs >= 0 ? `in ${hours}h ${minutes}m` : `${hours}h ${minutes}m ago`;
  return `${date.toLocaleString(undefined, {
    year: "numeric",
    month: "short",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    timeZoneName: "short",
  })} (${rel})`;
}

function resetMinute(value) {
  if (!value) return null;
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return null;
  date.setUTCSeconds(0, 0);
  return date.toISOString();
}

function bar(value, width = 24) {
  const used = pct(value);
  if (used === null) return `[${"?".repeat(width)}]`;
  const filled = Math.round((used / 100) * width);
  return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
}

function trend(entries, currentReset) {
  if (!entries || entries.length < 2) return "";
  const resetKey = resetMinute(currentReset);
  const scoped = resetKey
    ? entries.filter((entry) => entry.resetMinute === resetKey)
    : entries.filter((entry) => !entry.resetMinute && !entry.resetsAt);
  if (scoped.length < 2) return "";
  const chars = " .:-=+*#%@";
  return scoped
    .slice(-18)
    .map((entry) => chars[Math.max(0, Math.min(chars.length - 1, Math.round((pct(entry.usedPercent) ?? 0) / 100 * (chars.length - 1))))])
    .join("");
}

function statusFor(window) {
  const used = pct(window?.utilization);
  if (used === null) return "unknown";
  if (used >= 100) return "limited";
  if (used >= 90) return "hot";
  return "ok";
}

function shouldShowWindow(data, { optional = false } = {}) {
  if (!data) return false;
  if (!optional) return true;
  const used = pct(data.utilization);
  return Boolean(data.resets_at) || (used !== null && used > 0);
}

function accountFiles() {
  return fs
    .readdirSync(dir)
    .filter((file) => file.endsWith(".json"))
    .filter((file) => !file.endsWith(".oauthAccount.json"))
    .filter((file) => file !== "usage-history.json")
    .sort();
}

function appendHistory(results) {
  if (noHistory) return null;
  const now = new Date().toISOString();
  const history = readJson(historyPath) || { version: 1, accounts: {} };
  history.version = 1;
  history.accounts ||= {};
  for (const result of results) {
    if (!result.usage?.ok) continue;
    const key = result.email || result.account;
    history.accounts[key] ||= {};
    for (const [name, source] of [
      ["five_hour", result.usage.data.five_hour],
      ["seven_day", result.usage.data.seven_day],
      ["seven_day_opus", result.usage.data.seven_day_opus],
      ["seven_day_sonnet", result.usage.data.seven_day_sonnet],
    ]) {
      if (!source) continue;
      history.accounts[key][name] ||= [];
      const entries = history.accounts[key][name];
      for (const existing of entries) {
        if (!existing.resetMinute && existing.resetsAt) existing.resetMinute = resetMinute(existing.resetsAt);
      }
      const last = entries[entries.length - 1];
      const entry = {
        capturedAt: now,
        usedPercent: pct(source.utilization),
        resetsAt: source.resets_at || null,
        resetMinute: resetMinute(source.resets_at),
      };
      if (!last || last.usedPercent !== entry.usedPercent || last.resetMinute !== entry.resetMinute) {
        entries.push(entry);
      }
      while (entries.length > 240) entries.shift();
    }
  }
  fs.writeFileSync(historyPath, `${JSON.stringify(history, null, 2)}\n`, { mode: 0o600 });
  return history;
}

async function main() {
  const results = [];
  const activeEmail = cachedAuthEmail();
  for (const file of accountFiles()) {
    const account = file.replace(/\.json$/, "");
    const credential = readJson(path.join(dir, file));
    const token = credential?.claudeAiOauth?.accessToken;
    const [profile, usage] = await Promise.all([
      request(token, "/api/oauth/profile"),
      request(token, "/api/oauth/usage"),
    ]);
    const profileEmail = profile.ok ? profile.data?.account?.email : null;
    const email = profileEmail || metaEmail(account);
    results.push({
      account,
      selected: account === selected,
      active: activeEmail ? email === activeEmail : account === selected,
      email,
      subscriptionType: profile.ok ? credential?.claudeAiOauth?.subscriptionType || null : null,
      profile,
      usage,
    });
  }
  const history = appendHistory(results) || readJson(historyPath) || { accounts: {} };
  if (asJson) {
    const activeResult = results.find((result) => result.active);
    console.log(JSON.stringify({ generatedAt: new Date().toISOString(), active: activeResult?.account || selected, selected, activeEmail, results }, null, 2));
    return;
  }

  console.log(`Claude account usage (${new Date().toLocaleString(undefined, { timeZoneName: "short" })})`);
  console.log("");
  for (const result of results) {
    const recovered = result.account.startsWith("unsaved-live-") ? " (recovered live credential)" : "";
    const staleSelected = result.selected && !result.active ? " (selected marker stale)" : "";
    const marker = result.active ? "*" : (result.selected ? "!" : " ");
    const label = `${marker} ${result.account}${result.email ? ` <${result.email}>` : ""}${recovered}${staleSelected}`;
    console.log(label);
    if (!result.usage.ok) {
      const message = result.usage.data?.error?.message || result.usage.error || `HTTP ${result.usage.status}`;
      const activeHint = result.active ? " ACTIVE ACCOUNT IS NOT USABLE" : "";
      console.log(`  usage: unavailable (${message})${activeHint}`);
      console.log("  fix: re-save this account after a successful login/request if the token is stale");
      console.log("");
      continue;
    }
    const windows = [
      ["5h", "five_hour", result.usage.data.five_hour, false],
      ["weekly", "seven_day", result.usage.data.seven_day, false],
      ["weekly opus", "seven_day_opus", result.usage.data.seven_day_opus, true],
      ["weekly sonnet", "seven_day_sonnet", result.usage.data.seven_day_sonnet, true],
    ].filter(([, , data, optional]) => shouldShowWindow(data, { optional }));
    for (const [labelName, key, data] of windows) {
      const used = pct(data.utilization);
      const keyHistory = history.accounts?.[result.email || result.account]?.[key] || [];
      const trendText = trend(keyHistory, data.resets_at || null);
      const pctText = used === null ? "???" : `${String(Math.round(used)).padStart(3)}%`;
      const suffix = trendText.trim() ? `  trend ${trendText}` : "";
      console.log(`  ${labelName.padEnd(13)} ${bar(used)} ${pctText} ${statusFor(data)}${suffix}`);
      const reset = data.resets_at ? resetText(data.resets_at) : (used === 0 ? "no active window" : "reset unknown");
      console.log(`  ${"".padEnd(13)} resets ${reset}`);
    }
    const extra = result.usage.data.extra_usage;
    if (extra && (Number(extra.used_credits || 0) > 0 || extra.utilization != null)) {
      const used = pct(extra.utilization);
      const credits = extra.used_credits != null ? ` ${extra.used_credits}${extra.currency ? ` ${extra.currency}` : ""}` : "";
      console.log(`  extra usage   ${bar(used)} ${used === null ? "n/a" : `${Math.round(used)}%`}${credits}`);
    }
    console.log("");
  }
}

main().catch((error) => {
  console.error(error instanceof Error ? error.message : String(error));
  process.exit(1);
});
NODE
}

# ---------- per-session pinning ----------

SETTINGS_REL=".claude/settings.local.json"

oat_file() { printf '%s\n' "${DIR}/$1.oat"; }

oat_names() {
  shopt -s nullglob
  local f
  for f in "${DIR}"/*.oat; do basename "$f" .oat; done
}

pin_token_of() { # <dir> -> pinned token or nothing
  node -e '
    const fs = require("fs"), path = require("path");
    try {
      const p = path.join(process.argv[1], ".claude", "settings.local.json");
      const j = JSON.parse(fs.readFileSync(p, "utf8"));
      const t = j?.env?.CLAUDE_CODE_OAUTH_TOKEN;
      if (t) process.stdout.write(t);
    } catch {}
  ' "$1"
}

pin_name_of() { # <dir> -> account name, "(unmanaged)", or nothing
  local tok f
  tok="$(pin_token_of "$1")"
  [ -n "$tok" ] || return 0
  shopt -s nullglob
  for f in "${DIR}"/*.oat; do
    [ "$(cat "$f")" = "$tok" ] && { basename "$f" .oat; return 0; }
  done
  echo "(unmanaged)"
}

write_pin() { # <dir> <token-or-empty-to-remove>
  node -e '
    const fs = require("fs"), path = require("path");
    const dir = process.argv[1], tok = process.argv[2];
    const p = path.join(dir, ".claude", "settings.local.json");
    let j = {};
    if (fs.existsSync(p)) {
      try { j = JSON.parse(fs.readFileSync(p, "utf8")); }
      catch { console.error(`refusing to touch invalid JSON: ${p}`); process.exit(1); }
    } else if (!tok) {
      process.exit(0);
    }
    j.env ||= {};
    if (tok) j.env.CLAUDE_CODE_OAUTH_TOKEN = tok;
    else { delete j.env.CLAUDE_CODE_OAUTH_TOKEN; if (!Object.keys(j.env).length) delete j.env; }
    fs.mkdirSync(path.dirname(p), { recursive: true });
    fs.writeFileSync(p, JSON.stringify(j, null, 2) + "\n", { mode: 0o600 });
    fs.chmodSync(p, 0o600);
  ' "$1" "$2"
}

exclude_pin_from_git() { # <dir> — keep the token file out of git status forever
  git -C "$1" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
  local ex
  ex="$(git -C "$1" rev-parse --path-format=absolute --git-path info/exclude 2>/dev/null)" || return 0
  [ -n "$ex" ] || return 0
  mkdir -p "$(dirname "$ex")" 2>/dev/null || true
  grep -qxF "$SETTINGS_REL" "$ex" 2>/dev/null || echo "$SETTINGS_REL" >> "$ex"
}

running_claude_sessions() { # lines: pid<TAB>cwd
  # ps for discovery, one targeted lsof -p for cwds: `lsof -c claude` misses
  # most claude processes when run sandboxed, the -p form does not.
  local pids
  pids="$(ps -axo pid=,comm= | awk '$2 ~ /\/claude$|^claude$/ { printf "%s%s", s, $1; s="," }')"
  [ -n "$pids" ] || return 0
  lsof -a -p "$pids" -d cwd -Fpn 2>/dev/null | awk '
    /^p/ { pid = substr($0, 2) }
    /^n/ { print pid "\t" substr($0, 2) }
  '
}

candidate_dirs() { # running session cwds first, then recent agent worktrees
  {
    running_claude_sessions | cut -f2
    ls -td "$HOME"/.superset/worktrees/*/* 2>/dev/null | head -10
    ls -td "$HOME"/.paseo/worktrees/*/* 2>/dev/null | head -10
  } | awk '!seen[$0]++'
}

short_dir() { printf '%s\n' "${1/#$HOME/~}"; }

oat_profile_email() { # <token> — best-effort ownership lookup; prints email or nothing
  node -e '
    const https = require("https");
    const tok = process.argv[1];
    const req = https.request(
      {
        hostname: "claude.ai",
        path: "/api/oauth/profile",
        method: "GET",
        timeout: 10000,
        headers: {
          authorization: `Bearer ${tok}`,
          accept: "application/json",
          "anthropic-client-platform": "cli",
        },
      },
      (res) => {
        let b = "";
        res.on("data", (d) => { b += d; });
        res.on("end", () => {
          try {
            const j = JSON.parse(b);
            const e = j?.account?.email;
            if (res.statusCode === 200 && e) process.stdout.write(e);
          } catch {}
        });
      },
    );
    req.on("error", () => {});
    req.on("timeout", () => req.destroy());
    req.end();
  ' "$1"
}

do_pin() { # <dir> <account-name>
  local d="$1" name="$2" tok pids
  d="$(cd "$d" 2>/dev/null && pwd)" || { echo "Not a directory: $1" >&2; exit 1; }
  tok="$(cat "$(oat_file "$name")" 2>/dev/null)" || true
  if [ -z "${tok:-}" ]; then
    echo "Account '${name}' has no pin token yet. Mint one first:" >&2
    echo "  claude-acct token ${name}" >&2
    exit 1
  fi
  write_pin "$d" "$tok"
  exclude_pin_from_git "$d"
  echo "Pinned $(short_dir "$d") -> ${name}"
  pids="$(running_claude_sessions | awk -F'\t' -v d="$d" '$2 == d { printf "%s%s", s, $1; s="," }')"
  if [ -n "$pids" ]; then
    echo "Applies on next session start. Restart the running session there (pid ${pids}) —"
    echo "in Superset/Paseo, restarting the thread resumes the conversation on the new account."
  else
    echo "No session currently running there; the next one picks it up automatically."
  fi
}

case "${1:-}" in
  add)
    # Safe capture of a NEW account: snapshot the current one, then sign into the
    # next via overwrite (claude auth login) — NEVER /logout, which revokes the
    # previous account's session/Remote-Control capability server-side.
    name="${2:?usage: claude-acct add <name>}"
    before="$(read_keychain 2>/dev/null | blob_hash || true)"
    snapshot_active
    echo "Signing in to the new account (browser). Do NOT log out of the current one."
    claude auth login
    blob="$(read_keychain)" || { echo "Login did not produce a credential." >&2; exit 1; }
    [ -z "$blob" ] && { echo "Keychain credential is empty after login." >&2; exit 1; }
    after="$(printf '%s' "$blob" | blob_hash)"
    if [ -n "$before" ] && [ "$before" = "$after" ]; then
      echo "Login did not change the Claude keychain credential; refusing to save '${name}' over the existing account." >&2
      echo "Open a fresh browser/login flow for the new account, then re-run: claude-acct add ${name}" >&2
      exit 1
    fi
    write_account_file "$name" "$blob"
    printf '%s' "$name" > "$ACTIVE"
    save_meta "$name" "$(auth_email || true)"
    save_oauth_account_meta "$name"
    echo "Added and switched to '${name}'."
    print_auth_status
    ;;
  save)
    name="${2:?usage: claude-acct save <name>}"
    blob="$(read_keychain)" || { echo "No active Claude credential in keychain. Run 'claude' and log in first." >&2; exit 1; }
    [ -z "$blob" ] && { echo "Keychain credential is empty — log in first." >&2; exit 1; }
    write_account_file "$name" "$blob"
    printf '%s' "$name" > "$ACTIVE"
    save_meta "$name" "$(auth_email || true)"
    save_oauth_account_meta "$name"
    echo "Saved current account as '${name}'."
    print_auth_status
    ;;
  use)
    name="${2:?usage: claude-acct use <name>}"
    file="${DIR}/${name}.json"
    [ -f "$file" ] || { echo "No saved account '${name}'. Run: claude-acct list" >&2; exit 1; }
    snapshot_active                                   # preserve outgoing account's latest tokens
    write_keychain "$(cat "$file")"
    live="$(read_keychain)" || { echo "Failed to read keychain after switching." >&2; exit 1; }
    if [ "$(printf '%s' "$live" | blob_hash)" != "$(file_hash "$file")" ]; then
      echo "Switch verification failed: keychain does not match '${name}' after write." >&2
      exit 1
    fi
    apply_oauth_account_meta "$name"
    printf '%s' "$name" > "$ACTIVE"
    save_meta "$name" "$(auth_email || true)"
    save_oauth_account_meta "$name"
    echo "Switched to '${name}'. New Claude processes use it immediately; already-running Claude processes may keep their startup account until restarted."
    print_auth_status
    ;;
  list)
    cur="$(active_name)"
    live_blob="$(read_keychain 2>/dev/null || true)"
    live_exact="$(name_from_exact_blob "$live_blob" 2>/dev/null || true)"
    live_email="$(auth_email || true)"
    live_email_name="$(name_from_live_email "$live_email" 2>/dev/null || true)"
    live_active="${live_email_name:-${live_exact:-$cur}}"
    shopt -s nullglob
    found=0
    for f in "${DIR}"/*.json; do
      found=1; n="$(basename "$f" .json)"
      case "$n" in *.oauthAccount) continue;; esac
      [ "$n" = "usage-history" ] && continue
      marker=" "
      [ "$n" = "$live_active" ] && marker="*"
      [ "$n" = "$cur" ] && [ "$n" != "$live_active" ] && marker="!"
      suffix=""
      [ "$n" = "$live_exact" ] && suffix="${suffix} live-exact"
      [ -z "$live_exact" ] && [ "$n" = "$live_email_name" ] && suffix="${suffix} live-email"
      [ "$n" = "$cur" ] && [ "$n" != "$live_active" ] && suffix="${suffix} selected-marker-stale"
      [ ! -f "${DIR}/${n}.oauthAccount.json" ] && suffix="${suffix} no-status-cache"
      [ -n "$suffix" ] && suffix=" (${suffix# })"
      [ "$n" = "$live_active" ] && suffix="${suffix:- (active)}"
      echo "${marker} ${n}${suffix}"
    done
    [ "$found" = 0 ] && echo "No saved accounts yet. Log in, then: claude-acct save <name>"
    [ -n "$live_email" ] && echo "Claude auth: ${live_email}" || true
    ;;
  current)
    live_email="$(auth_email || true)"
    live_name="$(name_from_live_email "$live_email" 2>/dev/null || true)"
    [ -n "$live_name" ] || live_name="$(name_from_exact_blob "$(read_keychain 2>/dev/null || true)" 2>/dev/null || true)"
    if [ -n "$live_name" ]; then
      printf '%s\n' "$live_name"
    else
      active_name && echo "" || echo "(none recorded)"
    fi
    [ -n "$live_email" ] && echo "Claude auth: ${live_email}" >&2 || true
    ;;
  doctor)
    echo "marker: $(active_name || true)"
    live_email="$(auth_email || true)"
    echo "claude_auth_email: ${live_email:-unknown}"
    live_blob="$(read_keychain 2>/dev/null || true)"
    if [ -n "$live_blob" ]; then
      live_hash="$(printf '%s' "$live_blob" | blob_hash)"
      echo "keychain_hash: ${live_hash}"
      exact="$(name_from_exact_blob "$live_blob" 2>/dev/null || true)"
      email_match="$(name_from_live_email "$live_email" 2>/dev/null || true)"
      echo "exact_saved_match: ${exact:-none}"
      echo "email_saved_match: ${email_match:-none}"
    else
      echo "keychain_hash: none"
    fi
    echo "status_cache_files:"
    shopt -s nullglob
    for f in "${DIR}"/*.json; do
      n="$(basename "$f" .json)"
      case "$n" in *.oauthAccount) continue;; esac
      [ "$n" = "usage-history" ] && continue
      if [ -f "${DIR}/${n}.oauthAccount.json" ]; then
        email="$(node -e 'const fs=require("fs"); try { const j=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); if (j.emailAddress) process.stdout.write(j.emailAddress); } catch {}' "${DIR}/${n}.oauthAccount.json")"
        echo "  ${n}: ${email:-present}"
      else
        echo "  ${n}: missing"
      fi
    done
    ;;
  usage)
    shift
    usage_report "$@"
    ;;
  token)
    name="${2:?usage: claude-acct token <name>}"
    [ -f "${DIR}/${name}.json" ] || [ -f "$(oat_file "$name")" ] || {
      echo "Unknown account '${name}'. Save it first (claude-acct add/save), or pick from: $(ls "$DIR" | sed -n 's/\.json$//p' | grep -v -e oauthAccount -e usage-history | tr '\n' ' ')" >&2
      exit 1
    }
    expected_email="$(meta_email "$name" 2>/dev/null || true)"
    echo "Minting a long-lived (~1 year) pin token for '${name}'${expected_email:+ <$expected_email>}."
    echo "A browser OAuth flow will start. The account signed into THAT browser window owns the token."
    [ -n "$expected_email" ] && echo ">>> Make sure the browser is signed into ${expected_email} (use the right profile or a private window)."
    tmp="$(mktemp)"
    claude setup-token 2>&1 | tee "$tmp" || true
    tok="$(grep -oE 'sk-ant-oat01-[A-Za-z0-9_-]+' "$tmp" | tail -1)"
    rm -f "$tmp"
    if [ -z "$tok" ]; then
      printf 'Could not auto-capture the token from setup-token output. Paste it (input hidden): '
      read -rs tok; echo
    fi
    [ -n "$tok" ] || { echo "No token captured." >&2; exit 1; }
    got="$(oat_profile_email "$tok" || true)"
    if [ -n "$got" ] && [ -n "$expected_email" ] && [ "$got" != "$expected_email" ]; then
      echo "REFUSING to save: this token belongs to ${got}, but '${name}' is ${expected_email}." >&2
      echo "Re-run with the browser signed into ${expected_email}, or save it under the matching account name." >&2
      exit 1
    fi
    printf '%s' "$tok" > "$(oat_file "$name")"
    chmod 600 "$(oat_file "$name")"
    if [ -n "$got" ]; then
      echo "Saved pin token for '${name}' (verified owner: ${got})."
    else
      echo "Saved pin token for '${name}' (ownership not verifiable — watch 'claude-acct usage' after first pinned run)."
    fi
    echo "Pin sessions to it with: claude-acct pin"
    ;;
  pin)
    if [ -n "${2:-}" ] && [ -n "${3:-}" ]; then
      do_pin "$2" "$3"
      exit 0
    fi
    # interactive picker
    dirs=(); i=0
    while IFS= read -r d; do [ -d "$d" ] && { dirs[i]="$d"; i=$((i+1)); }; done < <(candidate_dirs)
    [ "$i" -gt 0 ] || { echo "No running Claude sessions or known worktrees found." >&2; exit 1; }
    running="$(running_claude_sessions)"
    echo "Sessions:"
    j=0
    while [ "$j" -lt "$i" ]; do
      d="${dirs[j]}"
      n="$(printf '%s\n' "$running" | awk -F'\t' -v d="$d" '$2 == d' | wc -l | tr -d ' ')"
      state="idle"; [ "$n" -gt 0 ] && state="RUNNING x$n"
      acct="$(pin_name_of "$d")"
      [ -n "$acct" ] || acct="default->$(active_name 2>/dev/null || echo '?')"
      printf '%3d) %-11s %-24s %s\n' "$((j+1))" "$state" "$acct" "$(short_dir "$d")"
      j=$((j+1))
    done
    printf 'Pin which session? [1-%d] ' "$i"
    read -r sel
    case "$sel" in (*[!0-9]*|'') echo "Not a number." >&2; exit 1;; esac
    [ "$sel" -ge 1 ] && [ "$sel" -le "$i" ] || { echo "Out of range." >&2; exit 1; }
    target="${dirs[sel-1]}"
    accts=(); k=0
    while IFS= read -r a; do [ -n "$a" ] && { accts[k]="$a"; k=$((k+1)); }; done < <(oat_names)
    if [ "$k" -eq 0 ]; then
      echo "No accounts have pin tokens yet. Mint one per account first:" >&2
      echo "  claude-acct token <name>   # for each of: $(ls "$DIR" | sed -n 's/\.json$//p' | grep -v -e oauthAccount -e usage-history | tr '\n' ' ')" >&2
      exit 1
    fi
    echo "Accounts:"
    m=0
    while [ "$m" -lt "$k" ]; do
      printf '%3d) %-14s %s\n' "$((m+1))" "${accts[m]}" "$(meta_email "${accts[m]}" 2>/dev/null || true)"
      m=$((m+1))
    done
    echo "  0) remove pin (use the global keychain account)"
    printf 'Pin to which account? [0-%d] ' "$k"
    read -r asel
    case "$asel" in (*[!0-9]*|'') echo "Not a number." >&2; exit 1;; esac
    if [ "$asel" -eq 0 ]; then
      write_pin "$target" ""
      echo "Unpinned $(short_dir "$target"). Restart the session there to apply."
      exit 0
    fi
    [ "$asel" -ge 1 ] && [ "$asel" -le "$k" ] || { echo "Out of range." >&2; exit 1; }
    do_pin "$target" "${accts[asel-1]}"
    ;;
  unpin)
    d="${2:-$PWD}"
    d="$(cd "$d" 2>/dev/null && pwd)" || { echo "Not a directory: ${2:-$PWD}" >&2; exit 1; }
    was="$(pin_name_of "$d")"
    [ -n "$was" ] || { echo "No pin on $(short_dir "$d")."; exit 0; }
    write_pin "$d" ""
    echo "Unpinned $(short_dir "$d") (was: ${was}). Restart the session there to apply."
    ;;
  sessions)
    cur="$(active_name 2>/dev/null || echo '?')"
    running="$(running_claude_sessions)"
    printf '%-11s %-24s %s\n' "STATE" "ACCOUNT" "SESSION"
    candidate_dirs | while IFS= read -r d; do
      [ -d "$d" ] || continue
      n="$(printf '%s\n' "$running" | awk -F'\t' -v d="$d" '$2 == d' | wc -l | tr -d ' ')"
      state="idle"; [ "$n" -gt 0 ] && state="RUNNING x$n"
      acct="$(pin_name_of "$d")"
      [ -n "$acct" ] || acct="default->${cur}"
      printf '%-11s %-24s %s\n' "$state" "$acct" "$(short_dir "$d")"
    done
    echo ""
    echo "default->${cur} sessions follow the keychain account (claude-acct use). Pinned ones don't."
    echo "Pins apply at session start — restart a running session after changing its pin."
    ;;
  *)
    cat >&2 <<'EOF'
claude-acct — rotate Claude CLI accounts without re-logging-in

  claude-acct add  <name>    Sign into a NEW account (overwrite, no logout) and save it
  claude-acct save <name>    Snapshot the currently logged-in account under <name>
  claude-acct use  <name>    Switch the keychain to a previously saved account
  claude-acct usage [--json] Show 5h + weekly usage/reset status for saved accounts
  claude-acct list           List saved accounts (* = active)
  claude-acct current        Print the active account name
  claude-acct doctor         Compare marker, keychain, and Claude auth status

Per-session pinning — run different accounts in PARALLEL sessions:
  claude-acct token <name>      Mint+save a ~1-year pin token for an account (one browser flow)
  claude-acct pin               Pick a running session/worktree, pin it to an account
  claude-acct pin <dir> <name>  Pin a directory non-interactively
  claude-acct unpin [dir]       Remove a pin (defaults to cwd)
  claude-acct sessions          Show running Claude sessions and which account each uses

Pins write env.CLAUDE_CODE_OAUTH_TOKEN into <dir>/.claude/settings.local.json.
Env beats keychain, so pinned sessions ignore `claude-acct use`. A pin applies on
the NEXT start of a session there (Superset/Paseo restart resumes the thread).
CAVEAT: if a pin token expires or is revoked, Claude silently falls back to the
keychain account — re-mint with `claude-acct token <name>` (~yearly).

Add accounts with `claude-acct add <name>` — it uses overwrite-login, never /logout.
NEVER run `/logout` (or `claude auth logout`) to switch: logout REVOKES that
account's Remote Control / session capability server-side and breaks its saved blob.
After accounts are added, `claude-acct use <name>` switches instantly — no re-entry.
EOF
    exit 1;;
esac
