UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

2,678 lines • 114 kB
#!/usr/bin/env node
/**
 * NeuroLink Auth Command
 *
 * Unified authentication command for AI providers supporting:
 * - API key authentication (traditional)
 * - OAuth 2.1 authentication with PKCE (for Claude subscription)
 *
 * Subcommands:
 * - login: Authenticate with a provider (supports --add/--label for multi-account)
 * - logout: Clear stored credentials
 * - status: Show authentication status
 * - refresh: Manually refresh OAuth tokens
 * - list: List all authenticated accounts
 * - remove: Remove an authenticated account
 *
 * Currently supports:
 * - Anthropic (API key + OAuth)
 */
import fs from "fs";
import path from "path";
import { execFile } from "child_process";
import { randomBytes, createHash } from "crypto";
import inquirer from "inquirer";
import chalk from "chalk";
import ora from "ora";
import { logger } from "../../utils/logger.js";
import { defaultTokenStore } from "../../auth/tokenStore.js";
import { CLAUDE_CODE_CLIENT_ID, ANTHROPIC_AUTH_URL, ANTHROPIC_TOKEN_URL, ANTHROPIC_REDIRECT_URI, CLAUDE_CLI_USER_AGENT, OAUTH_BETA_HEADERS, } from "../../auth/anthropicOAuth.js";
import { flushAccountQuotas, loadAccountQuotas, saveAccountQuota, } from "../../proxy/accountQuota.js";
import { fetchAccountUsage, listAnthropicAccountsForUsage, usageToQuota, } from "../../proxy/accountUsage.js";
import { importCodexAuthFile } from "../../auth/codexOAuth.js";
import { fetchCodexAccountUsage, listCodexAccountsForUsage, } from "../../proxy/codexAccountUsage.js";
// =============================================================================
// CONSTANTS
// =============================================================================
const NEUROLINK_CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || ".", ".neurolink");
const ENV_FILE_PATH = path.join(process.cwd(), ".env");
// Anthropic OAuth Configuration (Claude Code Official) - For direct OAuth usage
// Uses claude.ai/oauth/authorize for Claude Pro/Max subscription access
const ANTHROPIC_OAUTH_CONFIG = {
    clientId: CLAUDE_CODE_CLIENT_ID,
    // NOTE: Uses claude.ai NOT console.anthropic.com for direct OAuth
    authorizationUrl: ANTHROPIC_AUTH_URL,
    tokenUrl: ANTHROPIC_TOKEN_URL,
    redirectUri: ANTHROPIC_REDIRECT_URI,
    // Scopes for direct OAuth (no API key creation needed)
    scope: "user:profile user:inference",
    userAgent: CLAUDE_CLI_USER_AGENT,
    betaHeaders: OAUTH_BETA_HEADERS,
};
// Anthropic Console OAuth Configuration - For API key creation flow
// This uses console.anthropic.com for authorization which grants org:create_api_key scope
const ANTHROPIC_CONSOLE_OAUTH_CONFIG = {
    clientId: CLAUDE_CODE_CLIENT_ID,
    // Authorization URL for console (required for API key creation scope)
    authorizationUrl: "https://console.anthropic.com/oauth/authorize",
    tokenUrl: ANTHROPIC_TOKEN_URL,
    redirectUri: ANTHROPIC_REDIRECT_URI,
    // Required scopes - org:create_api_key is needed for API key creation
    scope: "org:create_api_key user:profile user:inference",
    userAgent: CLAUDE_CLI_USER_AGENT,
    // API key creation endpoint
    createApiKeyUrl: "https://api.anthropic.com/api/oauth/claude_cli/create_api_key",
};
// Providers with first-class credential flows in `neurolink auth`. This is
// intentionally narrower than the CLI's full AI-provider catalog: `auth list`
// renders every provider-qualified account stored by the CLI.
const AUTH_LOGIN_PROVIDERS = ["anthropic", "codex"];
// =============================================================================
// SUBCOMMAND HANDLERS
// =============================================================================
/**
 * Handle the login subcommand
 * `neurolink auth login <provider>`
 *
 * When --add is specified, saves tokens to the TokenStore with a compound key
 * (e.g., "anthropic:alice") to support multi-account pools.
 */
export async function handleLogin(argv) {
    try {
        const provider = argv.provider?.toLowerCase();
        // Validate provider
        if (!AUTH_LOGIN_PROVIDERS.includes(provider)) {
            logger.error(chalk.red(`Unsupported provider: ${provider}. Supported: ${AUTH_LOGIN_PROVIDERS.join(", ")}`));
            process.exit(1);
        }
        // Codex has a dedicated import-based login path (imports the current
        // ChatGPT credential from ~/.codex/auth.json into the pool).
        if (provider === "codex") {
            await handleCodexLogin(argv);
            return;
        }
        // If method is specified, use it directly
        // Each handler returns true when credentials were written to a file,
        // false for .env-only or "keep existing" paths.
        let wroteCredentials = false;
        if (argv.method) {
            if (argv.method === "api-key") {
                wroteCredentials = await handleApiKeyAuth(provider, !argv.nonInteractive);
            }
            else if (argv.method === "oauth") {
                await handleOAuthAuth(provider);
                wroteCredentials = true;
            }
            else if (argv.method === "create-api-key") {
                await handleCreateApiKeyOAuth(provider);
                wroteCredentials = true;
            }
        }
        else {
            // Interactive mode - ask user which method they prefer
            wroteCredentials = await handleInteractiveAuth(provider);
        }
        // Only save to TokenStore when the auth flow actually wrote credentials.
        // Skip for .env-only or "keep existing" paths — re-reading a stale or
        // nonexistent credentials file is wasteful and can produce wrong entries.
        if (wroteCredentials) {
            await saveAccountToPool(provider, argv.label);
        }
    }
    catch (error) {
        logger.error(chalk.red("Authentication failed:"));
        logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
        process.exit(1);
    }
}
/**
 * Handle Codex (ChatGPT) login by importing the current credential from
 * `~/.codex/auth.json` into the account pool under a `codex:<label>` key.
 *
 * To add multiple ChatGPT accounts: `codex login` as account A, then
 * `neurolink auth login codex --label a`; repeat for account B, etc. The proxy
 * then pools all imported accounts and rotates automatically — no manual
 * account switching during use.
 */
export async function handleCodexLogin(argv) {
    const spinner = ora("Importing Codex (ChatGPT) credential…").start();
    try {
        const credential = await importCodexAuthFile();
        const rawLabel = argv.label ??
            credential.email ??
            credential.accountId?.slice(0, 8) ??
            Date.now().toString(36).slice(-6);
        const label = rawLabel.trim();
        const compoundKey = `codex:${label}`;
        const scopeParts = ["codex"];
        if (credential.planType) {
            scopeParts.push(`plan:${credential.planType}`);
        }
        if (credential.email) {
            scopeParts.push(`email:${credential.email}`);
        }
        if (credential.accountId) {
            scopeParts.push(`account:${credential.accountId}`);
        }
        await defaultTokenStore.saveTokens(compoundKey, {
            accessToken: credential.accessToken,
            refreshToken: credential.refreshToken,
            expiresAt: credential.expiresAt ?? Date.now() + 3_600_000,
            tokenType: "Bearer",
            scope: scopeParts.join(" "),
        });
        await defaultTokenStore.markEnabled(compoundKey);
        spinner.succeed(`Codex account added to pool: ${chalk.cyan(compoundKey)}`);
        if (credential.planType) {
            logger.always(chalk.gray(`  plan: ${credential.planType}`));
        }
        if (credential.email) {
            logger.always(chalk.gray(`  email: ${credential.email}`));
        }
        logger.always(chalk.gray("  Add more accounts: `codex login` (new account) then `neurolink auth login codex --label <name>`"));
    }
    catch (error) {
        spinner.fail("Codex login failed");
        logger.error(chalk.red(error instanceof Error ? error.message : String(error)));
        process.exit(1);
    }
}
// ---------------------------------------------------------------------------
// Pool-state display helpers
// ---------------------------------------------------------------------------
/**
 * One line per account explaining why the proxy is or isn't routing to it.
 *
 * The proxy skips accounts on two independent grounds — a persisted `disabled`
 * flag and an active cooldown — and neither was previously visible from
 * `auth list`, so an account could silently drop out of rotation.
 */
async function collectAccountPoolState(accountKeys) {
    const notes = {};
    try {
        const { loadAccountCooldowns } = await import("../../proxy/accountCooldown.js");
        const cooldowns = await loadAccountCooldowns();
        const now = Date.now();
        for (const key of accountKeys) {
            if (await defaultTokenStore.isDisabled(key)) {
                const reason = (await defaultTokenStore.getDisabledReason(key)) ?? "";
                notes[key] = chalk.red(`disabled${reason ? ` (${reason})` : ""} — re-enable: neurolink auth enable ${key}`);
                continue;
            }
            const cooldown = cooldowns[key];
            if (cooldown && cooldown.coolingUntil > now) {
                const minutes = Math.max(1, Math.round((cooldown.coolingUntil - now) / 60000));
                notes[key] = chalk.yellow(`cooling (${cooldown.reason}) for ~${minutes}m`);
            }
        }
    }
    catch {
        // Pool state is advisory — never block the listing on it.
    }
    return notes;
}
// ---------------------------------------------------------------------------
// Quota display helpers
// ---------------------------------------------------------------------------
/**
 * Convert a future unix timestamp (seconds) into a human-readable relative
 * duration like "2h 15m" or "4d 3h".  Returns "now" if the time has passed.
 */
function formatTimeUntil(unixTimestamp) {
    const ms = unixTimestamp * 1000 - Date.now();
    if (ms <= 0) {
        return "now";
    }
    const hours = Math.floor(ms / 3600000);
    const minutes = Math.floor((ms % 3600000) / 60000);
    if (hours >= 24) {
        const days = Math.floor(hours / 24);
        const remainHours = hours % 24;
        return `${days}d ${remainHours}h`;
    }
    return `${hours}h ${minutes}m`;
}
/**
 * Format session/weekly quota into compact display strings.
 */
function formatQuotaColumns(quota) {
    const sessionRemaining = Math.round((1 - quota.sessionUsed) * 100);
    const weeklyRemaining = Math.round((1 - quota.weeklyUsed) * 100);
    const colorize = (pct, text) => {
        if (pct <= 10) {
            return chalk.red(text);
        }
        if (pct <= 30) {
            return chalk.yellow(text);
        }
        return chalk.green(text);
    };
    return {
        sessionText: quota.sessionStatus === "unknown"
            ? chalk.gray("unknown")
            : colorize(sessionRemaining, `${sessionRemaining}% left`),
        weeklyText: quota.weeklyStatus === "unknown"
            ? chalk.gray("unknown")
            : colorize(weeklyRemaining, `${weeklyRemaining}% left`),
        sessionReset: quota.sessionResetAt > 0
            ? chalk.gray(`resets ${formatTimeUntil(quota.sessionResetAt)}`)
            : "",
        weeklyReset: quota.weeklyResetAt > 0
            ? chalk.gray(`resets ${formatTimeUntil(quota.weeklyResetAt)}`)
            : "",
    };
}
/**
 * Format the dynamic per-plan limit windows (model-scoped weeklies such as
 * Fable, plus any future kinds) as extra display lines. `session` and
 * `weekly_all` are omitted — they already render as the SESSION / WEEKLY
 * columns. Exported for the continuous test suite.
 */
export function formatQuotaWindowRows(quota) {
    if (!quota.windows?.length) {
        return [];
    }
    const colorize = (pct, text) => {
        if (pct <= 10) {
            return chalk.red(text);
        }
        if (pct <= 30) {
            return chalk.yellow(text);
        }
        return chalk.green(text);
    };
    const rows = [];
    for (const window of quota.windows) {
        if (window.kind === "session" || window.kind === "weekly_all") {
            continue;
        }
        const remaining = Math.round((1 - window.used) * 100);
        const label = window.scopeModel
            ? `${window.group ?? window.kind} (${window.scopeModel})`
            : window.kind;
        const reset = window.resetsAt > 0
            ? chalk.gray(`  resets ${formatTimeUntil(window.resetsAt)}`)
            : "";
        rows.push(`${chalk.gray(`${label}:`)} ${colorize(remaining, `${remaining}% left`)}${reset}`);
    }
    return rows;
}
/**
 * Fetch fresh limits for `auth list --refresh`.
 *
 * Every provider-qualified token-store account receives an explicit result.
 * The running proxy is authoritative for provider adapters that declare proxy
 * support, or for namespaces it returns. Registered direct adapters fill in
 * the remaining namespaces. Providers with no quota adapter are shown as
 * `not_supported`, never silently omitted.
 */
function providerFromAccountKey(key) {
    const separator = key.indexOf(":");
    const provider = (separator === -1 ? key : key.slice(0, separator))
        .trim()
        .toLowerCase();
    return provider || "unknown";
}
const DIRECT_QUOTA_REFRESH_CONCURRENCY = 3;
// Provider-specific protocol knowledge is kept behind this capability map.
// The CLI result itself remains provider-generic: adding a new adapter only
// requires registering it here, while every other configured provider still
// receives an explicit `not_supported` result.
const PROVIDER_QUOTA_ADAPTERS = {
    anthropic: {
        supportsProxyRefresh: true,
        listAccounts: () => listAnthropicAccountsForUsage(),
        priorQuotaKeys: (account) => [account.key, account.label],
        async refreshAccount(account, { prior }) {
            if (account.type !== "oauth") {
                return { status: "not_supported" };
            }
            const result = await fetchAccountUsage(account);
            if (result.ok === false) {
                return { status: "unavailable", error: result.error };
            }
            const quota = usageToQuota(result.usage, { now: Date.now(), prior });
            if (!quota) {
                return {
                    status: "unavailable",
                    error: "usage payload had no recognizable limit windows",
                };
            }
            return { status: "refreshed", quota };
        },
    },
    codex: {
        listAccounts: () => listCodexAccountsForUsage(),
        priorQuotaKeys: (account) => [account.key],
        async refreshAccount(account) {
            if (account.type !== "oauth") {
                return { status: "not_supported" };
            }
            const result = await fetchCodexAccountUsage(account);
            if (result.ok === false) {
                return result.reason === "not_oauth"
                    ? { status: "not_supported" }
                    : { status: "unavailable", error: `usage ${result.reason}` };
            }
            return { status: "refreshed", quota: result.quota };
        },
    },
};
async function refreshDirectProviderLimits(options) {
    const { provider, adapter, accountKeys, prior, quotas, errors, accountResults, setAccountResult, } = options;
    let accounts;
    try {
        accounts = await adapter.listAccounts();
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        for (const key of accountKeys) {
            if (providerFromAccountKey(key) === provider) {
                setAccountResult(key, "unavailable", message);
            }
        }
        errors.push(`${provider} accounts: ${message}`);
        return false;
    }
    let attemptedDirectRefresh = false;
    let nextIndex = 0;
    const worker = async () => {
        for (;;) {
            const index = nextIndex++;
            if (index >= accounts.length) {
                return;
            }
            const account = accounts[index];
            if (accountResults[account.key]?.status === "refreshed" ||
                accountResults[account.key]?.status === "snapshot") {
                continue;
            }
            if (account.type === "oauth") {
                attemptedDirectRefresh = true;
            }
            try {
                const priorQuota = adapter
                    .priorQuotaKeys(account)
                    .map((key) => prior[key])
                    .find((quota) => quota !== undefined) ?? null;
                const result = await adapter.refreshAccount(account, {
                    prior: priorQuota,
                });
                if (result.status === "refreshed") {
                    await saveAccountQuota(account.key, result.quota);
                    quotas[account.key] = result.quota;
                    setAccountResult(account.key, "refreshed");
                    continue;
                }
                setAccountResult(account.key, result.status, result.error);
                if (result.status === "unavailable" && result.error) {
                    errors.push(`${provider}:${account.label}: ${result.error}`);
                }
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                setAccountResult(account.key, "unavailable", message);
                errors.push(`${provider}:${account.label}: ${message}`);
            }
        }
    };
    await Promise.all(Array.from({
        length: Math.min(DIRECT_QUOTA_REFRESH_CONCURRENCY, accounts.length || 1),
    }, () => worker()));
    return attemptedDirectRefresh;
}
async function refreshAccountLimitsForList(accountKeys) {
    const errors = [];
    const quotas = {};
    const accountResults = {};
    const proxyRefreshedProviders = new Set();
    let refreshedViaProxy = false;
    let refreshedDirectly = false;
    const setAccountResult = (key, status, error) => {
        accountResults[key] = {
            provider: providerFromAccountKey(key),
            status,
            ...(error ? { error } : {}),
        };
    };
    // `auth list` supports arbitrary provider-prefixed token-store entries.
    // Providers with no quota adapter stay visible as explicitly unsupported
    // rather than being rendered as an unexplained unavailable account.
    for (const accountKey of accountKeys) {
        setAccountResult(accountKey, "not_supported");
    }
    const configuredProviders = new Set(accountKeys.map((accountKey) => providerFromAccountKey(accountKey)));
    const proxyState = detectRunningProxyState();
    if (proxyState?.port) {
        const host = proxyState.host && proxyState.host !== "0.0.0.0"
            ? proxyState.host
            : "127.0.0.1";
        try {
            const response = await fetch(`http://${host}:${proxyState.port}/limits`, {
                signal: AbortSignal.timeout(45_000),
            });
            if (response.ok) {
                const payload = (await response.json());
                for (const provider of configuredProviders) {
                    if (PROVIDER_QUOTA_ADAPTERS[provider]?.supportsProxyRefresh) {
                        proxyRefreshedProviders.add(provider);
                        refreshedViaProxy = true;
                    }
                }
                for (const result of payload.results) {
                    // The proxy already returns the canonical key. Keep the fallback for
                    // a pre-provider-key proxy during a rolling package transition.
                    const key = result.key || `anthropic:${result.account}`;
                    proxyRefreshedProviders.add(providerFromAccountKey(key));
                    refreshedViaProxy = true;
                    if (result.quota) {
                        quotas[key] = result.quota;
                    }
                    setAccountResult(key, result.status === "refreshed"
                        ? "refreshed"
                        : result.quota
                            ? "snapshot"
                            : result.status === "skipped_api_key"
                                ? "not_supported"
                                : "unavailable", result.error);
                    if (result.status === "error" && result.error) {
                        errors.push(`${providerFromAccountKey(key)}:${result.account}: ${result.error}`);
                    }
                }
            }
            if (!response.ok) {
                errors.push(`running proxy /limits returned HTTP ${response.status}; trying direct quota adapters`);
            }
        }
        catch {
            // Keep the message generic: a raw fetch error can echo the requested
            // URL, and this string reaches the text and JSON CLI output.
            errors.push("running proxy /limits unreachable; trying direct quota adapters");
        }
    }
    const prior = await loadAccountQuotas().catch(() => ({}));
    try {
        for (const [provider, adapter] of Object.entries(PROVIDER_QUOTA_ADAPTERS)) {
            if (!configuredProviders.has(provider) ||
                proxyRefreshedProviders.has(provider)) {
                continue;
            }
            refreshedDirectly =
                (await refreshDirectProviderLimits({
                    provider,
                    adapter,
                    accountKeys,
                    prior,
                    quotas,
                    errors,
                    accountResults,
                    setAccountResult,
                })) || refreshedDirectly;
        }
    }
    finally {
        // The quota store's debounced flush timer is unref()'d and this process is
        // short-lived — flush every completed provider snapshot before returning.
        await flushAccountQuotas().catch(() => undefined);
    }
    const via = refreshedViaProxy && refreshedDirectly
        ? "mixed"
        : refreshedViaProxy
            ? "proxy"
            : refreshedDirectly
                ? "direct"
                : "none";
    return {
        via,
        quotas: Object.keys(quotas).length > 0 ? quotas : null,
        accounts: accountResults,
        errors,
    };
}
/**
 * Handle the list subcommand
 * `neurolink auth list`
 *
 * Lists all authenticated accounts from the TokenStore.
 */
export async function handleList(argv) {
    try {
        const allKeys = await defaultTokenStore.listProviders();
        if (allKeys.length === 0) {
            if (argv.format === "json") {
                logger.always(JSON.stringify([], null, 2));
            }
            else {
                logger.always(chalk.yellow("\nNo authenticated accounts found.\n"));
                logger.always(chalk.blue("Run 'neurolink auth login <provider>' to authenticate.\n"));
            }
            return;
        }
        // Build enriched account list with token metadata
        const enrichedAccounts = await Promise.all(allKeys.map(async (key) => {
            const parts = key.split(":");
            const provider = parts[0];
            const label = parts.length > 1 ? parts.slice(1).join(":") : undefined;
            let tier;
            let email;
            let tokenStatus = "unknown";
            let expiresAt;
            let tokenType;
            // Derive email from the compound key label when it looks like an email.
            // The credentials file is a shared singleton that gets overwritten on
            // every login — reading email from it would show the LATEST login's
            // email for ALL accounts.  The label is the per-account source of truth.
            if (label && label.includes("@")) {
                email = label;
            }
            // Fall back to credentials file ONLY for the default (unlabeled) account.
            // Compound-key entries (labeled accounts) must NOT read the shared
            // credentials file because it gets overwritten on every login and would
            // show the latest login's email/tier for all accounts.  Per-account
            // metadata is encoded in the token's scope field instead.
            if (!email && !label) {
                try {
                    const credPath = path.join(NEUROLINK_CONFIG_DIR, `${provider}-credentials.json`);
                    if (fs.existsSync(credPath)) {
                        const creds = JSON.parse(fs.readFileSync(credPath, "utf-8"));
                        email = creds.email;
                        if (creds.subscriptionTier) {
                            tier = creds.subscriptionTier;
                        }
                    }
                }
                catch {
                    /* non-fatal */
                }
            }
            try {
                const tokens = await defaultTokenStore.loadTokens(key);
                if (tokens) {
                    expiresAt = tokens.expiresAt;
                    tokenType = tokens.tokenType;
                    const isExpired = defaultTokenStore.isTokenExpired(tokens, 0);
                    tokenStatus = isExpired ? "expired" : "valid";
                    // Extract per-account metadata from scope (e.g. "tier:pro email:user@example.com")
                    if (tokens.scope) {
                        if (!tier) {
                            const tierMatch = tokens.scope.match(/tier:(\w+)/);
                            if (tierMatch) {
                                tier = tierMatch[1];
                            }
                        }
                        if (!email) {
                            const emailMatch = tokens.scope.match(/email:(\S+)/);
                            if (emailMatch) {
                                email = emailMatch[1];
                            }
                        }
                    }
                }
            }
            catch {
                // Token load failed — show as unknown
            }
            return {
                key,
                provider,
                label,
                email,
                tier,
                tokenStatus,
                expiresAt,
                tokenType,
            };
        }));
        // Optionally fetch fresh provider limits before rendering.
        let refreshOutcome;
        if (argv.refresh) {
            refreshOutcome = await refreshAccountLimitsForList(allKeys);
        }
        // Load persisted quota data (captured from proxy responses), then overlay
        // anything just refreshed — freshly fetched values win over the snapshot.
        let quotas = {};
        try {
            quotas = await loadAccountQuotas();
        }
        catch {
            // Non-fatal — quota display is best-effort
        }
        if (refreshOutcome?.quotas) {
            quotas = { ...quotas, ...refreshOutcome.quotas };
        }
        if (argv.format === "json") {
            // Merge quota data into each account object for JSON output
            const withQuota = enrichedAccounts.map((acct) => {
                const quota = quotas[acct.key] ??
                    // Old Anthropic snapshots used bare labels. Read them once so an
                    // upgrade does not hide a useful historic reading, but all new
                    // writes use the provider-qualified key above.
                    (acct.provider === "anthropic" && acct.label
                        ? quotas[acct.label]
                        : undefined) ??
                    null;
                return {
                    ...acct,
                    quota,
                    refresh: refreshOutcome?.accounts[acct.key] ?? null,
                };
            });
            if (refreshOutcome) {
                // --refresh envelopes the array so the fetch outcome travels with it.
                logger.always(JSON.stringify({
                    refresh: {
                        via: refreshOutcome.via,
                        errors: refreshOutcome.errors,
                        accounts: refreshOutcome.accounts,
                    },
                    accounts: withQuota,
                }, null, 2));
            }
            else {
                logger.always(JSON.stringify(withQuota, null, 2));
            }
        }
        else {
            if (refreshOutcome) {
                if (refreshOutcome.via !== "none") {
                    logger.always(chalk.gray(`\nFetched fresh limits (${refreshOutcome.via === "proxy" ? "via running proxy" : refreshOutcome.via === "direct" ? "direct provider APIs" : "via the running proxy and direct provider APIs"}).`));
                }
                for (const refreshError of refreshOutcome.errors) {
                    logger.always(chalk.yellow(`⚠ ${refreshError}`));
                }
            }
            logger.always(chalk.bold("\nAuthenticated Accounts:\n"));
            // `--refresh` always shows the limit columns, including an explicit
            // unavailable/not-supported state. A blank table silently looked like a
            // healthy Codex account had no quota information.
            const hasQuota = !!refreshOutcome ||
                enrichedAccounts.some((acct) => quotas[acct.key] !== undefined ||
                    (acct.provider === "anthropic" && acct.label
                        ? quotas[acct.label] !== undefined
                        : false));
            // Table header
            // Why an account is (or isn't) in the proxy pool. Without this a
            // disabled or parked account simply vanishes from routing with no clue.
            const poolState = await collectAccountPoolState(enrichedAccounts.map((acct) => acct.key));
            const colKey = "LABEL".padEnd(20);
            const colEmail = "EMAIL".padEnd(28);
            const colStatus = "TOKEN STATUS".padEnd(14);
            const colProvider = "PROVIDER".padEnd(12);
            const colSession = hasQuota ? "SESSION".padEnd(14) : "";
            const colWeekly = hasQuota ? "WEEKLY".padEnd(14) : "";
            logger.always(`  ${chalk.gray(colKey)} ${chalk.gray(colProvider)} ${chalk.gray(colEmail)} ${chalk.gray(colStatus)}${hasQuota ? ` ${chalk.gray(colSession)} ${chalk.gray(colWeekly)}` : ""}`);
            logger.always(`  ${chalk.gray("-".repeat(hasQuota ? 108 : 78))}`);
            for (const acct of enrichedAccounts) {
                const displayLabel = (acct.label ?? acct.key).padEnd(20);
                const displayEmail = (acct.email ?? "-").padEnd(28);
                const displayProvider = acct.provider.padEnd(12);
                let statusText;
                if (acct.tokenStatus === "valid") {
                    statusText = chalk.green("valid".padEnd(14));
                }
                else if (acct.tokenStatus === "expired") {
                    statusText = chalk.red("expired".padEnd(14));
                }
                else {
                    statusText = chalk.yellow("unknown".padEnd(14));
                }
                const quota = quotas[acct.key] ??
                    (acct.provider === "anthropic" && acct.label
                        ? quotas[acct.label]
                        : undefined);
                const poolNote = poolState[acct.key];
                if (hasQuota && quota) {
                    const qc = formatQuotaColumns(quota);
                    logger.always(`  ${chalk.cyan(displayLabel)} ${displayProvider} ${displayEmail} ${statusText} ${qc.sessionText.padEnd(14)} ${qc.weeklyText.padEnd(14)}`);
                    const indent = " ".repeat(2 + 20 + 1 + 12 + 1 + 28 + 1 + 14 + 1);
                    // Second line: reset times (indented under session/weekly columns)
                    if (qc.sessionReset || qc.weeklyReset) {
                        logger.always(`${indent}${(qc.sessionReset || "").padEnd(14)} ${qc.weeklyReset || ""}`);
                    }
                    // Dynamic per-plan windows (e.g. the Fable-only weekly limit)
                    for (const windowRow of formatQuotaWindowRows(quota)) {
                        logger.always(`${indent}${windowRow}`);
                    }
                    if (poolNote) {
                        logger.always(`${indent}${poolNote}`);
                    }
                }
                else {
                    const refreshState = refreshOutcome?.accounts[acct.key];
                    const unavailableText = refreshState?.status === "not_supported"
                        ? "not supported"
                        : refreshOutcome
                            ? "unavailable"
                            : "-";
                    const apiKeyNote = refreshOutcome && acct.tokenType && acct.tokenType !== "Bearer"
                        ? chalk.gray("  (api key — not refreshed)")
                        : "";
                    logger.always(`  ${chalk.cyan(displayLabel)} ${displayProvider} ${displayEmail} ${statusText}${hasQuota ? ` ${chalk.yellow(unavailableText.padEnd(14))} ${chalk.gray("n/a".padEnd(14))}` : ""}${apiKeyNote}`);
                    if (poolNote) {
                        logger.always(`  ${poolNote}`);
                    }
                }
            }
            logger.always("");
        }
    }
    catch (error) {
        logger.error(chalk.red("Failed to list accounts:"));
        logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
        process.exit(1);
    }
}
/**
 * Handle the remove subcommand
 * `neurolink auth remove <provider> --label <label>` or `neurolink auth remove <provider> --account <key>`
 *
 * Removes an authenticated account from the TokenStore.
 * When neither --label nor --account is given, removes the default (unlabelled) account
 * for the specified provider.
 */
export async function handleRemove(argv) {
    try {
        const provider = argv.provider?.toLowerCase() ?? "anthropic";
        // Resolve the compound key from --account, --label, or provider default
        let compoundKey;
        if (argv.account) {
            compoundKey = argv.account;
        }
        else if (argv.label) {
            compoundKey = `${provider}:${argv.label}`;
        }
        else {
            // Remove the default (unlabelled) account for this provider
            compoundKey = provider;
        }
        // Check if the account exists
        const allKeys = await defaultTokenStore.listProviders();
        if (!allKeys.includes(compoundKey)) {
            logger.error(chalk.red(`Account not found: ${compoundKey}`));
            logger.always(chalk.blue("\nRun 'neurolink auth list' to see all authenticated accounts.\n"));
            process.exit(1);
        }
        await defaultTokenStore.clearTokens(compoundKey);
        // Only remove the legacy credentials file for bare provider keys.
        // Compound keys (e.g., "anthropic:alice") should not touch the shared
        // legacy credentials file — it may belong to the default account.
        if (!compoundKey.includes(":")) {
            const legacyCredFile = path.join(NEUROLINK_CONFIG_DIR, `${compoundKey}-credentials.json`);
            try {
                if (fs.existsSync(legacyCredFile)) {
                    fs.unlinkSync(legacyCredFile);
                    logger.debug(`Removed legacy credentials file: ${legacyCredFile}`);
                }
            }
            catch {
                // Non-fatal — legacy file may not exist or may already be gone
            }
        }
        logger.always(chalk.green(`\nAccount removed: ${compoundKey}\n`));
    }
    catch (error) {
        logger.error(chalk.red("Failed to remove account:"));
        logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
        process.exit(1);
    }
}
/**
 * Handle the logout subcommand
 * `neurolink auth logout <provider>`
 */
export async function handleLogout(argv) {
    try {
        const provider = argv.provider?.toLowerCase();
        // Validate provider
        if (!AUTH_LOGIN_PROVIDERS.includes(provider)) {
            logger.error(chalk.red(`Unsupported provider: ${provider}. Supported: ${AUTH_LOGIN_PROVIDERS.join(", ")}`));
            process.exit(1);
        }
        logger.always(chalk.blue(`\nClearing ${provider} credentials...\n`));
        const spinner = argv.quiet
            ? null
            : ora("Removing stored credentials...").start();
        try {
            // Clear stored credentials file
            const credentialsFile = path.join(NEUROLINK_CONFIG_DIR, `${provider}-credentials.json`);
            if (fs.existsSync(credentialsFile)) {
                fs.unlinkSync(credentialsFile);
                if (spinner) {
                    spinner.succeed("Stored credentials removed");
                }
            }
            else {
                if (spinner) {
                    spinner.info("No stored credentials found");
                }
            }
            // Also clear from TokenStore — both the bare provider key and all
            // compound-key entries (provider:label) used by the account pool.
            try {
                await defaultTokenStore.clearTokens(provider);
            }
            catch {
                // Ignore if no tokens stored for bare key
            }
            try {
                const allKeys = await defaultTokenStore.listProviders();
                for (const k of allKeys) {
                    if (k.startsWith(`${provider}:`)) {
                        await defaultTokenStore.clearTokens(k);
                        logger.debug(`Cleared pooled account: ${k}`);
                    }
                }
            }
            catch {
                // Ignore if listing/clearing fails
            }
            // Check for environment variable
            const envVar = getEnvVarName(provider);
            const hasEnvKey = !!process.env[envVar];
            if (hasEnvKey) {
                logger.always("");
                logger.always(chalk.yellow(`Note: ${envVar} is still set in your environment or .env file.`));
                logger.always(chalk.yellow("You may need to manually remove it from your shell profile or .env file."));
                // Offer to remove from .env if it exists
                if (fs.existsSync(ENV_FILE_PATH)) {
                    const { removeFromEnv } = await inquirer.prompt([
                        {
                            type: "confirm",
                            name: "removeFromEnv",
                            message: `Remove ${envVar} from .env file?`,
                            default: false,
                        },
                    ]);
                    if (removeFromEnv) {
                        await removeFromEnvFile(envVar);
                        logger.always(chalk.green(`Removed ${envVar} from .env file`));
                    }
                }
            }
            logger.always("");
            logger.always(chalk.green(`${provider} credentials cleared successfully.`));
        }
        catch (error) {
            if (spinner) {
                spinner.fail("Failed to clear credentials");
            }
            throw error;
        }
    }
    catch (error) {
        logger.error(chalk.red("Logout failed:"));
        logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
        process.exit(1);
    }
}
/**
 * Handle the status subcommand
 * `neurolink auth status [provider]`
 */
export async function handleStatus(argv) {
    try {
        const provider = argv.provider?.toLowerCase();
        // If provider specified, show just that provider
        const providersToCheck = provider
            ? [provider]
            : [...AUTH_LOGIN_PROVIDERS];
        const results = [];
        for (const p of providersToCheck) {
            const status = await getAuthStatus(p);
            results.push(status);
        }
        // Output results
        if (argv.format === "json") {
            logger.always(JSON.stringify(results, null, 2));
        }
        else {
            logger.always(chalk.bold("\nAuthentication Status:\n"));
            for (const status of results) {
                const providerName = status.provider.charAt(0).toUpperCase() + status.provider.slice(1);
                const statusIcon = status.isAuthenticated
                    ? chalk.green("[Authenticated]")
                    : chalk.yellow("[Not Authenticated]");
                logger.always(`${chalk.cyan(providerName)} ${statusIcon}`);
                if (status.isAuthenticated) {
                    logger.always(`  Method: ${status.method}`);
                    if (status.subscriptionTier) {
                        logger.always(`  Subscription: ${chalk.blue(status.subscriptionTier)}`);
                    }
                    if (status.method === "oauth") {
                        if (status.tokenExpiry) {
                            const isExpired = status.needsRefresh;
                            const expiryLabel = isExpired
                                ? chalk.red("Expired")
                                : chalk.green(status.tokenExpiry);
                            logger.always(`  Token Expires: ${expiryLabel}`);
                        }
                        if (status.hasRefreshToken) {
                            logger.always(`  Refresh Token: ${chalk.green("Available")}`);
                        }
                        else {
                            logger.always(`  Refresh Token: ${chalk.yellow("Not available")}`);
                        }
                        if (status.needsRefresh && status.hasRefreshToken) {
                            logger.always(chalk.yellow(`  Run 'neurolink auth refresh ${status.provider}' to refresh tokens`));
                        }
                    }
                }
                else {
                    logger.always(chalk.blue(`  Run 'neurolink auth login ${status.provider}' to authenticate`));
                }
                logger.always("");
            }
        }
    }
    catch (error) {
        logger.error(chalk.red("Status check failed:"));
        logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
        process.exit(1);
    }
}
/**
 * Refresh every pooled Codex account in place.
 *
 * Unlike the Anthropic path there is no single credentials file to rewrite:
 * accounts live under `codex:<label>` in the token store, so each is refreshed
 * and saved back individually. A failure on one account never stops the rest.
 */
async function handleCodexRefresh(argv) {
    const { refreshCodexToken } = await import("../../auth/codexOAuth.js");
    const keys = await defaultTokenStore.listByPrefix("codex:");
    if (keys.length === 0) {
        logger.error(chalk.red("No Codex accounts found. Run 'codex login', then 'neurolink auth login codex --label <name>'."));
        process.exit(1);
    }
    logger.always(chalk.blue(`\nRefreshing ${keys.length} Codex account(s)...\n`));
    let failed = 0;
    for (const key of keys) {
        const label = key.slice("codex:".length) || key;
        const spinner = argv.quiet ? null : ora(`${label}`).start();
        try {
            const tokens = await defaultTokenStore.peekTokens(key);
            if (!tokens?.refreshToken) {
                throw new Error("no refresh token stored");
            }
            const refreshed = await refreshCodexToken(tokens.refreshToken);
            await defaultTokenStore.saveTokens(key, {
                accessToken: refreshed.accessToken,
                refreshToken: refreshed.refreshToken ?? tokens.refreshToken,
                expiresAt: refreshed.expiresAt ?? Date.now() + 3_600_000,
                tokenType: "Bearer",
                ...(tokens.scope ? { scope: tokens.scope } : {}),
            });
            spinner?.succeed(`${label} refreshed`);
        }
        catch (error) {
            failed += 1;
            spinner?.fail(`${label}: ${error instanceof Error ? error.message : "refresh failed"}`);
        }
    }
    if (failed > 0) {
        logger.always(chalk.yellow(`\n${failed} account(s) need re-import: codex login, then neurolink auth login codex --label <name>\n`));
        process.exit(1);
    }
    logger.always(chalk.green("\nAll Codex accounts refreshed.\n"));
}
/**
 * Handle the refresh subcommand
 * `neurolink auth refresh <provider>`
 */
export async function handleRefresh(argv) {
    try {
        const provider = argv.provider?.toLowerCase();
        // Validate provider
        if (!AUTH_LOGIN_PROVIDERS.includes(provider)) {
            logger.error(chalk.red(`Unsupported provider: ${provider}. Supported: ${AUTH_LOGIN_PROVIDERS.join(", ")}`));
            process.exit(1);
        }
        // Codex credentials live only in the pooled token store — there is no
        // `codex-credentials.json` for the shared path below to read.
        if (provider === "codex") {
            await handleCodexRefresh(argv);
            return;
        }
        logger.always(chalk.blue(`\nRefreshing ${provider} OAuth tokens...\n`));
        const spinner = argv.quiet ? null : ora("Reading stored tokens...").start();
        try {
            // Get stored credentials
            const credentials = await getStoredCredentials(provider);
            if (!credentials || credentials.type !== "oauth") {
                if (spinner) {
                    spinner.fail("No OAuth credentials found");
                }
                logger.error(chalk.red(`No OAuth authentication found for ${provider}. Use 'neurolink auth login ${provider} --method oauth' first.`));
                process.exit(1);
            }
            if (!credentials.oauth?.refreshToken) {
                if (spinner) {
                    spinner.fail("No refresh token available");
                }
                logger.error(chalk.red("No refresh token available. Please re-authenticate with OAuth."));
                process.exit(1);
            }
            if (spinner) {
                spinner.text = "Refreshing access token...";
            }
            // Refresh the token with Claude CLI User-Agent
            // IMPORTANT: Uses JSON body, not URLSearchParams
            const tokenResponse = await fetch(ANTHROPIC_OAUTH_CONFIG.tokenUrl, {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    Accept: "application/json",
                    "User-Agent": ANTHROPIC_OAUTH_CONFIG.userAgent,
                },
                body: JSON.stringify({
                    grant_type: "refresh_token",
                    refresh_token: credentials.oauth.refreshToken,
                    client_id: ANTHROPIC_OAUTH_CONFIG.clientId,
                }),
            });
            if (!tokenResponse.ok) {
                const errorText = await tokenResponse.text();
                throw new Error(`Token refresh failed: ${tokenResponse.status} - ${errorText}`);
            }
            const tokenData = (await tokenResponse.json());
            // Update stored tokens
            const newTokens = {
                accessToken: tokenData.access_token,
                refreshToken: tokenData.refresh_token || credentials.oauth.refreshToken,
                expiresAt: tokenData.expires_in
                    ? Date.now() + tokenData.expires_in * 1000
                    : undefined,
                tokenType: tokenData.token_type || "Bearer",
                scope: tokenData.scope,
            };
            await saveStoredCredentials(provider, {
                type: "oauth",
                oauth: newTokens,
                provider,
                subscriptionTier: credentials.subscriptionTier,
                createdAt: credentials.createdAt,
                updatedAt: Date.now(),
            });
            // Also update the TokenStore pool entries for this provider.
            // Update both the bare provider key and compound "provider:label" keys
            // so that pooled accounts created via saveAccountToPool() stay current.
            try {
                const allKeys = await defaultTokenStore.listProviders();
                for (const key of allKeys) {
                    if (key === provider || key.startsWith(`${provider}:`)) {
                        const existingTokens = await defaultTokenStore.loadTokens(key);
                        if (existingTokens) {
                            await defaultTokenStore.saveTokens(key, {
                                accessToken: newTokens.accessToken,
                                refreshToken: newTokens.refreshToken,
                                expiresAt: newTokens.expiresAt ?? Date.now() + 3600 * 1000,
                                tokenType: newTokens.tokenType || "Bearer",
                                scope: existingTokens.scope, // preserve existing scope metadata
                            });
                            logger.debug(`Updated TokenStore entry: ${key}`);
                        }
                    }
                }
            }
            catch (poolErr) {
                logger.debug(`[auth] Failed to update TokenStore pool entries: ${poolErr instanceof Error ? poolErr.message : String(poolErr)}`);
            }
            if (spinner) {
                spinner.succeed("Access token refreshed successfully!");
            }
            logger.always("");
            logger.always(chalk.green("Token refresh complete!"));
            if (newTokens.expiresAt) {
                logger.always(`  New expiry: ${new Date(newTokens.expiresAt).toLocaleString()}`);
            }
        }
        catch (error) {
            if (spinner) {
                spinner.fail("Token refresh failed");
            }
            throw error;
        }
    }
    catch (error) {
        logger.error(chalk.red("Token refresh failed:"));
        logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
        process.exit(1);
    }
}
/**
 * Handle the cleanup subcommand
 * `neurolink auth cleanup [--force]`
 *
 * Removes stale accounts from the token store:
 *   1. Expired entries with no refresh token (via pruneExpired)
 *   2. Permanently disabled entries (after confirmation)
 */
/**
 * Disable reasons whose credential is still valid — the account is out of the
 * pool because of an external condition, not because the login broke. Cleanup
 * must not delete these.
 *
 * Expressed as the set of reasons that DO mean the credential is broken, so an
 * unrecognised reason — notably the free text an operator passes to
 * `auth disable --reason` — is retained rather than deleted. The inverse
 * allowlist fails open: any reason not enumerated would silently become a
 * delete.
 */
const BROKEN_CREDENTIAL_DISABLE_REASONS = new Set([
    "missing_refresh_token",
    "refresh_invalid",
    "refresh_failed",
]);
export async function handleCleanup(argv) {
    try {
        const removed = [];
        // Step 1: Prune expired entries that have no refresh token
        const pruned = await defaultTokenStore.pruneExpired();
        for (const key of pruned) {
            removed.push({ key, reason: "expired, no refresh token" });
        }
        // Step 2: Find disabled entries (pruneExpired already removes disabled
        // entries, but in case the user runs cleanup with entries that were
        // disabled between the prune call and now, check again)
        const allDisabledKeys = await defaultTokenStore.listDisabled();
        // Only credentials the proxy itself gave up on are deletable. A disable that
        // describes a condition outside the credential — an organization policy, or
        // an operator taking the account out of rotation — leaves a perfectly good
        // login that deleting would destroy for a condition someone else can revert.
        const disabledKeys = [];
        const retained = [];
        for (const key of allDisabledKeys) {
            const reason = (await defaultTokenStore.getDisabledReason(key)) ?? "";
            if (!BROKEN_CREDENTIAL_DISABLE_REASONS.has(reason)) {
                retained.push({ key, reason: reason || "unspecified" });
            }
            else {
                disabledKeys.push(key);
            }
        }
        if (retained.length > 0) {
            logger.always(chalk.blue(`\nKeeping ${retained.length} recoverable disabled account(s):`));
            for (const entry of retained) {
                logger.always(`  - ${entry.key} (${entry.reason})`);
            }
            logger.always(chalk.gray("  Credentials are still valid. Use 'neurolink auth enable <account>' to restore, or 'auth remove' to delete."));
        }
        if (disabledKeys.length > 0) {
            let shouldRemove = false;
            if (argv.force || argv.nonInteractive) {
                shouldRemove = true;
            }
            else {
                logger.always(chalk.yellow(`\nFound ${disabledKeys.length} disabled account(s):`));
                for (const key of disabledKeys) {
                    logger.always(`  - ${key}`);
                }
                const { confirm } = await inquirer.prompt([
                    {
                        name: "confirm",
                        type: "confirm",
                        message: "Remove these disabled accounts?",
                        default: false,
                    },
                ]);
                shouldRemove = confirm;
            }
            if (shouldRemove) {
                for (const key of disabledKeys) {
                    const reason = (await defaultTokenStore.getDisabledReason(key)) ?? "unspecified";
                    await defaultTokenStore.clearTokens(key);
                    removed.push({ key, reason: `disabled: ${reason}` });
                }
            }
        }
        // Report results
        if (removed.length === 0) {
            logger.always(chalk.green("No stale accounts found."));
        }
        else {
            logger.always(chalk.green(`\nCleaned up ${removed.length} stale account${removed.length === 1 ? "" : "s"}:`));
            for (const entry of removed) {
                logger.always(`  - ${entry.key} (${entry.reason})`);
            }
            logger.always("");
        }
    }
    catch (error) {
        logger.error(chalk.red("Cleanup failed:"));
        logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
        process.exit(1);
    }
}
/**
 * Handle the enable subcommand
 * `neurolink auth enable <account>`
 *
 * Re-enables a previously disabled account so it can be used by the proxy pool again.
 */
export async function handleEnable(argv) {
    try {
        // Resolve account key from --account option or positional arg
        const accountKey = argv.account || (argv._ && argv._[2] ? String(argv._[2]) : undefined);
        if (!accountKey) {
            logger.error(chalk.red("Missing required argument: <account>"));
            logger.always(chalk.blue("\nUsage: neurolink auth enable <account>\n" +
                "Run 'neurolink auth list' to see all accounts.\n"));
            process.exit(1);
        }
        // Check if account exists in the token store
        const allKeys = await defaultTokenStore.listProviders();
        if (!allKeys.includes(accountKey)) {
            logger.error(chalk.red(`Account not found: ${accountKey}`));
            logger.always(chalk.blue("\nRun 'neurolink auth list' to see all authenticated accounts.\n"));
            process.exit(1);
        }
        await defaultTokenStore.markEnabled(accountKey);
        logger.always(chalk.green(`\nRe-enabled account: ${accountKey}\n`));
    }
    catch (error) {
        logger.error(chalk.red("Failed to enable account:"));
        logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
        process.exit(1);
    }
}
/**
 * The proxy re-reads the token store on every request, so a disable takes
 * effect on the next request with no restart.
 *
 * "refresh_failed" is refused deliberately: the proxy treats that exact reason
 * as a legacy entry and re-enables it automatically, which would silently undo
 * an operator's disable.
 */
const RESERVED_DISABLE_REASON = "refresh_failed";
export async function handleDisable(argv) {
    try {
        const accountKey = argv.account || (argv._ && argv._[2] ? String(argv._[2]) : undefined);
        if (!accountKey) {
            logger.error(chalk.red("Missing required argument: <account>"));
            logger.always(chalk.blue("\nUsage: neurolink auth disable <account>\n" +
                "Run 'neurolink auth list' to see all accounts.\n"));
            process.exit(1);
        }
        const allKeys = await defaultTokenStore.listProviders();
        if (!allKeys.includes(accountKey)) {
            logger.error(chalk.red(`Account not found: ${accountKey}`));
            logger.always(chalk.blue("\nRun 'neurolink auth list' to see all authenticated accounts.\n"));
            process.exit(1);
        }
        const reason = argv.reason?.trim() || "manual";
        if (reason === RESERVED_DISABLE_REASON) {
            logger.error(chalk.red(`Reason "${RESERVED_DISABLE_REASON}" is reserved — the proxy re-enables accounts carrying it. Choose another reason.`));
            process.exit(1);
        }
        await defaultTokenStore.markDisabled(accountKey, reason);
        logger.always(chalk.yellow(`\nDisabled account: ${accountKey} (${reason})`));
        logger.always(chalk.gray(`Credentials are kept. Re-enable with: neurolink auth enable ${accountKey}\n`));
    }
    catch (error) {
        logger.error(chalk.red("Failed to disable account:"));
        logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
        process.exit(1);
    }
}
/** Compact "in 3h 12m" / "5m" rendering for a future timestamp. */
function formatDuration(ms) {
    const totalMinutes = Math.max(0, Math.round(ms / 60000));
    const hours = Math.floor(totalMinutes / 60);
    const minutes = totalMinutes % 60;
    return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
}
/**
 * Inspect or clear the per-account rate-limit cooldowns the proxy persists.
 *
 * Without this there is no way to see why an account vanished from the pool, or
 * to release one parked by a bad reset timestamp.
 */
export async function handleCooldown(argv) {
    const { loadAccountCooldowns, clearAccountCooldown } = await import("../../proxy/accountCooldown.js");
    const action = String(argv.action ?? "list");
    try {
        if (action !== "list" && action !== "clear") {
            logger.error(chalk.red(`Unknown action: ${action}. Use list|clear.`));
            process.exit(1);
        }
        const cooldowns = await loadAccountCooldowns();
        const now = Date.now();
        if (action === "list") {
            const active = Object.entries(cooldowns).filter(([, entry]) => entry.coolingUntil > now);
            if (argv.format === "json") {
                logger.always(JSON.stringify(active.map(([account, entry]) => ({ account, ...entry })), null, 2));
                return;
            }
            if (active.length === 0) {
                logger.always(chalk.green("\nNo accounts are cooling.\n"));
                return;
            }
            logger.always(chalk.bold("\nCooling accounts:\n"));
            for (const [account, entry] of active) {
                logger.always(`  ${chalk.cyan(account.padEnd(32))} ${chalk.yellow(entry.reason.padEnd(10))} ${chalk.gray(`recovers in ${formatDuration(entry.coolingUntil - now)}`)}`);
            }
            logger.always("");
            return;
        }
        const targets = argv.all
            ? Object.keys(cooldowns)
            : [argv.account || (argv._?.[3] ? String(argv._[3]) : "")].filter(Boolean);
        if (targets.length === 0) {
            logger.error(chalk.red("Specify an account key, or pass --all to clear every one."));
            process.exit(1);
        }
        // Cooldowns are keyed by the full compound key, so a bare label silently
        // matches nothing. Reporting success for it would send the operator away
        // believing an account was released when it is still parked.
        const unknown = targets.filter((target) => !(target in cooldowns));
        if (unknown.length > 0) {
            logger.error(chalk.red(`No cooldown found for: ${unknown.join(", ")}`));
            logger.always(chalk.blue("\nRun 'neurolink auth cooldown list' to see the exact account keys.\n"));
            process.exit(1);
        }
        for (const target of targets) {
            await clearAccountCooldown(target);
        }
        logger.always(chalk.green(`\nCleared ${targets.length} cooldown(s).`));
        // The running worker keeps its own copy of the cooldown map, loaded once at
        // startup, so a file-level clear alone would not release the account.
        logger.always(chalk.gray("Restart the proxy for a running instance to pick this up: neurolink proxy start\n"));
    }
    catch (error) {
        logger.error(chalk.red("Failed to read or clear cooldowns:"));
        logger.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
        process.exit(1);
    }
}
// =============================================================================
// PRIMARY ACCOUNT (proxy routing.primaryAccount in YAML)
// =============================================================================
const DEFAULT_PROXY_CONFIG_PATH = path.join(NEUROLINK_CONFIG_DIR, "proxy-config.yaml");
/** Lazy-load js-yaml. Returns undefined if unavailable; callers that need YAML
 *  output (rather than JSON fallback) should error with install guidance. */
async function tryLoadJsYaml() {
    try {
        return (await import(/* @vite-ignore */ "js-yaml"));
    }
    catch {
        return undefined;
    }
}
function isYamlPath(p) {
    const lower = p.toLowerCase();
    return lower.endsWith(".yaml") || lower.endsWith(".yml");
}
/** Read and parse a proxy config file. Returns an empty object when the file
 *  doesn't exist (caller can mutate and write). Detects YAML vs JSON by
 *  extension; falls back to JSON parsing if js-yaml is unavailable for a YAML
 *  file (errors loudly if neither parser can read it). */
async function readProxyConfigFile(filePath) {
    const yamlExpected = isYamlPath(filePath);
    let raw;
    try {
        raw = fs.readFileSync(filePath, "utf-8");
    }
    catch (err) {
        const code = err.code;
        if (code === "ENOENT") {
            return {
                data: {},
                format: yamlExpected ? "yaml" : "json",
                hadComments: false,
            };
        }
        throw err;
    }
    const hadComments = /^\s*#/m.test(raw);
    if (yamlExpected) {
        const yaml = await tryLoadJsYaml();
        if (yaml) {
            const parsed = yaml.default?.load?.(raw) ?? yaml.load(raw);
            return {
                data: parsed && typeof parsed === "object"
                    ? parsed
                    : {},
                format: "yaml",
                hadComments,
            };
        }
        // YAML expected but js-yaml absent — try JSON
        try {
            return { data: JSON.parse(raw), format: "json", hadComments };
        }
        catch {
            throw new Error(`Cannot edit ${filePath}: js-yaml is not installed and the file is ` +
                `not valid JSON. Install js-yaml (pnpm add -D js-yaml) or convert ` +
                `the file to JSON.`);
        }
    }
    return { data: JSON.parse(raw), format: "json", hadComments };
}
/** Serialize and write a proxy config file. */
async function writeProxyConfigFile(filePath, doc) {
    let serialized;
    if (doc.format === "yaml") {
        const yaml = await tryLoadJsYaml();
        if (!yaml) {
            throw new Error(`Cannot write ${filePath} as YAML: js-yaml is not installed. ` +
                `Install it (pnpm add -D js-yaml) or use a .json config path.`);
        }
        const dump = yaml.default?.dump ?? yaml.dump;
        if (!dump) {
            throw new Error(`Cannot write ${filePath} as YAML: js-yaml module does not expose ` +
                `a dump function (unexpected version).`);
        }
        serialized = dump(doc.data, {
            lineWidth: 100,
            noRefs: true,
        });
    }
    else {
        serialized = `${JSON.stringify(doc.data, null, 2)}\n`;
    }
    const dir = path.dirname(filePath);
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
    }
    fs.writeFileSync(filePath, serialized, "utf-8");
}
function getRoutingObject(data) {
    const routing = data.routing;
    if (routing && typeof routing === "object" && !Array.isArray(routing)) {
        return routing;
    }
    const fresh = {};
    data.routing = fresh;
    return fresh;
}
function readPrimaryFromRouting(routing) {
    if (!routing) {
        return undefined;
    }
    const kebab = routing["primary-account"];
    if (typeof kebab === "string" && kebab.trim() !== "") {
        return kebab.trim();
    }
    const camel = routing.primaryAccount;
    if (typeof camel === "string" && camel.trim() !== "") {
        return camel.trim();
    }
    return undefined;
}
/** Best-effort detection of a running proxy. Mirrors `proxy status` semantics
 *  without importing the proxy module. */
function detectRunningProxyState() {
    try {
        const stateFile = path.join(NEUROLINK_CONFIG_DIR, "proxy-state.json");
        if (!fs.existsSync(stateFile)) {
            return undefined;
        }
        const parsed = JSON.parse(fs.readFileSync(stateFile, "utf-8"));
        if (!parsed.pid || typeof parsed.pid !== "number") {
            return undefined;
        }
        process.kill(parsed.pid, 0);
        return parsed;
    }
    catch {
        return undefined;
    }
}
function reportRunningProxyConfigUpdate(filePath) {
    const state = detectRunningProxyState();
    if (!state) {
        return;
    }
    logger.always("");
    const editedPath = path.resolve(filePath);
    const watchedPath = state.configFile
        ? path.resolve(state.configFile)
        : undefined;
    if (watchedPath === editedPath) {
        logger.always(chalk.green(`✓ Running proxy PID ${state.pid} is watching this file and will apply the change automatically.`));
        logger.always(chalk.gray("  Verify the active generation with `neurolink proxy status`."));
        return;
    }
    if (watchedPath) {
        logger.always(chalk.yellow(`⚠ Running proxy PID ${state.pid} watches ${watchedPath}, not ${editedPath}; this change will not affect that process.`));
        return;
    }
    logger.always(chalk.yellow(`⚠ Running proxy PID ${state.pid} does not report hot-reload support. Restart it to pick up the change.`));
}
/**
 * Handle the set-primary subcommand
 * `neurolink auth set-primary <email> [--config <path>]`
 *
 * Writes routing.primary-account to the proxy config YAML so the proxy
 * tries this account first under fill-first/round-robin home semantics.
 * Does not touch the token store.
 */
export async function handleSetPrimary(argv) {
    const email = argv.email ?? (argv._ && argv._[2] ? String(argv._[2]) : undefined);
    if (!email || email.trim() === "") {
        logger.error(chalk.red("Missing required argument: <email>"));
        logger.always(chalk.blue("\nUsage: neurolink auth set-primary <email> [--config <path>]\n" +
            "Run 'neurolink auth list' to see authenticated accounts.\n"));
        process.exit(1);
    }
    const trimmed = email.trim();
    const filePath = argv.config ?? DEFAULT_PROXY_CONFIG_PATH;
    try {
        const doc = await readProxyConfigFile(filePath);
        if (doc.hadComments) {
            logger.always(chalk.yellow(`⚠ Note: existing YAML comments in ${filePath} will not be preserved.`));
        }
        const routing = getRoutingObject(doc.data);
        delete routing.primaryAccount;
        routing["primary-account"] = trimmed;
        await writeProxyConfigFile(filePath, doc);
        logger.always(chalk.green(`✓ Set primary account → ${trimmed}`));
        logger.always(chalk.green(`✓ Saved to ${filePath}`));
        // Token-store presence check (non-fatal)
        const compoundKey = `anthropic:${trimmed}`;
        const known = await defaultTokenStore.listByPrefix("anthropic:");
        if (!known.includes(compoundKey)) {
            logger.always("");
            logger.always(chalk.yellow("⚠ This account is not currently authenticated. Run\n" +
                "  `neurolink auth login --add` to add it. The proxy will fall\n" +
                "  back to the first enabled account until then."));
        }
        reportRunningProxyConfigUpdate(filePath);
    }
    catch (err) {
        logger.error(chalk.red("Failed to set primary account:"));
        logger.error(chalk.red(err instanceof Error ? err.message : "Unknown error"));
        process.exit(1);
    }
}
/**
 * Handle the overage subcommand
 * `neurolink auth overage <status|auto|always|never> [--config <path>]`
 *
 * Controls whether the pool may keep serving on paid extra usage once an
 * account's subscription window is spent. Only `never` overrides the provider:
 * nothing here can switch on extra usage that Anthropic reports as disabled.
 */
export async function handleOverage(argv) {
    const action = String(argv.action ?? (argv._ && argv._[2] ? String(argv._[2]) : "status"));
    const filePath = argv.config ?? DEFAULT_PROXY_CONFIG_PATH;
    try {
        const doc = await readProxyConfigFile(filePath);
        const routing = getRoutingObject(doc.data);
        const current = (typeof routing["use-overage"] === "string"
            ? routing["use-overage"]
            : typeof routing.useOverage === "string"
                ? routing.useOverage
                : undefined) ?? "auto";
        if (action === "status") {
            logger.always(chalk.bold(`\nExtra-usage policy: ${chalk.cyan(current)}`));
            logger.always(chalk.gray("  auto   — follow whatever Anthropic reports per account (default)\n" +
                "  always — keep serving whenever the provider permits extra usage\n" +
                "  never  — stop at the subscription limit, never spend credits"));
            // The provider's own signal is what actually decides, so show it too —
            // a policy of "always" is inert when the organization has it switched off.
            const quotas = await loadAccountQuotas();
            const rows = Object.entries(quotas).filter(([account, quota]) => 
            // Anthropic only: Codex has no extra-usage concept and stores a
            // neutral "rejected", which would read here as a real restriction.
            !account.startsWith("codex:") &&
                (quota.overageStatus || quota.overageDisabledReason));
            if (rows.length > 0) {
                logger.always(chalk.bold("\nProvider extra-usage state:"));
                for (const [account, quota] of rows) {
                    const enabled = quota.overageEnabled === true ||
                        quota.overageStatus?.trim().toLowerCase() === "allowed";
                    const detail = quota.overageDisabledReason
                        ? chalk.gray(` (${quota.overageDisabledReason})`)
                        : "";
                    logger.always(`  ${chalk.cyan(account.padEnd(32))} ${enabled ? chalk.green("available") : chalk.yellow("unavailable")}${detail}`);
                }
            }
            logger.always("");
            return;
        }
        if (action !== "auto" && action !== "always" && action !== "never") {
            logger.error(chalk.red(`Unknown action: ${action}. Use status|auto|always|never.`));
            process.exit(1);
        }
        if (doc.hadComments) {
            logger.always(chalk.yellow(`⚠ Note: existing YAML comments in ${filePath} will not be preserved.`));
        }
        delete routing.useOverage;
        routing["use-overage"] = action;
        await writeProxyConfigFile(filePath, doc);
        logger.always(chalk.green(`✓ Extra-usage policy → ${action}`));
        logger.always(chalk.green(`✓ Saved to ${filePath}`));
        reportRunningProxyConfigUpdate(filePath);
    }
    catch (err) {
        logger.error(chalk.red("Failed to read or update extra-usage policy:"));
        logger.error(chalk.red(err instanceof Error ? err.message : "Unknown error"));
        process.exit(1);
    }
}
/**
 * Handle the get-primary subcommand
 * `neurolink auth get-primary [--config <path>]`
 */
export async function handleGetPrimary(argv) {
    const filePath = argv.config ?? DEFAULT_PROXY_CONFIG_PATH;
    try {
        if (!fs.existsSync(filePath)) {
            logger.always(chalk.blue(`No proxy config file found at ${filePath}.`));
            logger.always("Falling back to insertion-order index 0 (no primary configured).");
            return;
        }
        const doc = await readProxyConfigFile(filePath);
        const routing = typeof doc.data.routing === "object" && doc.data.routing
            ? doc.data.routing
            : undefined;
        const primary = readPrimaryFromRouting(routing);
        if (!primary) {
            logger.always(chalk.blue(`No primary account configured. Falling back to insertion-order ` +
                `index 0.`));
            logger.always(`Source: ${filePath} (no \`routing.primaryAccount\` field)`);
            return;
        }
        const compoundKey = `anthropic:${primary}`;
        const known = await defaultTokenStore.listByPrefix("anthropic:");
        const present = known.includes(compoundKey);
        logger.always(chalk.bold(`Configured primary: ${primary}`));
        logger.always(`Status: ${present
            ? chalk.green(`authenticated (${compoundKey} present in token store)`)
            : chalk.yellow(`not authenticated (token store has no ${compoundKey})`)}`);
        logger.always(`Source: ${filePath}`);
    }
    catch (err) {
        logger.error(chalk.red("Failed to read primary account:"));
        logger.error(chalk.red(err instanceof Error ? err.message : "Unknown error"));
        process.exit(1);
    }
}
/**
 * Handle the clear-primary subcommand
 * `neurolink auth clear-primary [--config <path>]`
 */
export async function handleClearPrimary(argv) {
    const filePath = argv.config ?? DEFAULT_PROXY_CONFIG_PATH;
    try {
        if (!fs.existsSync(filePath)) {
            logger.always(chalk.blue(`No proxy config file found at ${filePath}.`));
            logger.always("Nothing to clear.");
            return;
        }
        const doc = await readProxyConfigFile(filePath);
        const routing = typeof doc.data.routing === "object" && doc.data.routing
            ? doc.data.routing
            : undefined;
        const before = readPrimaryFromRouting(routing);
        if (!before || !routing) {
            logger.always(chalk.blue("No primary account was configured."));
            return;
        }
        if (doc.hadComments) {
            logger.always(chalk.yellow(`⚠ Note: existing YAML comments in ${filePath} will not be preserved.`));
        }
        delete routing.primaryAccount;
        delete routing["primary-account"];
        await writeProxyConfigFile(filePath, doc);
        logger.always(chalk.green(`✓ Cleared primary account (was: ${before})`));
        logger.always(chalk.green(`✓ Saved to ${filePath}`));
        reportRunningProxyConfigUpdate(filePath);
    }
    catch (err) {
        logger.error(chalk.red("Failed to clear primary account:"));
        logger.error(chalk.red(err instanceof Error ? err.message : "Unknown error"));
        process.exit(1);
    }
}
// =============================================================================
// LEGACY HANDLER (for backward compatibility)
// =============================================================================
/**
 * Legacy main auth command handler
 * @deprecated Use subcommand handlers instead
 */
export async function handleAuth(argv) {
    // Map legacy flags to subcommands
    if (argv.status) {
        await handleStatus({
            provider: argv.provider,
            format: "text",
            quiet: false,
            debug: argv.debug,
        });
    }
    else if (argv.logout) {
        await handleLogout({
            provider: argv.provider,
            format: "text",
            quiet: false,
            debug: argv.debug,
        });
    }
    else {
        await handleLogin({
            provider: argv.provider,
            method: argv.method,
            format: "text",
            quiet: false,
            nonInteractive: argv.nonInteractive,
            debug: argv.debug,
        });
    }
}
// =============================================================================
// AUTHENTICATION METHODS
// =============================================================================
/**
 * Interactive authentication - ask user which method they prefer
 */
async function handleInteractiveAuth(provider) {
    logger.always(chalk.blue(`\n${provider.charAt(0).toUpperCase() + provider.slice(1)} Authentication Setup\n`));
    const currentStatus = await checkExistingAuth(provider);
    if (currentStatus.hasValidAuth) {
        logger.always(chalk.green("You already have valid authentication configured."));
        logger.always(`  Type: ${currentStatus.type}`);
        if (currentStatus.type === "api-key") {
            logger.always(`  Key: ${maskCredential(currentStatus.credential || "")}`);
        }
        logger.always("");
        const { reconfigure } = await inquirer.prompt([
            {
                type: "confirm",
                name: "reconfigure",
                message: "Would you like to reconfigure authentication?",
                default: false,
            },
        ]);
        if (!reconfigure) {
            logger.always(chalk.blue("Keeping existing configuration."));
            return false;
        }
    }
    // Show authentication method options
    const { method } = await inquirer.prompt([
        {
            type: "list",
            name: "method",
            message: "Select authentication method:",
            choices: [
                {
                    name: "API Key - Traditional authentication with API key (pay-per-use)",
                    value: "api-key",
                },
                {
                    name: "Claude Pro/Max OAuth - Use subscription directly (Recommended for Pro/Max users)",
                    value: "oauth",
                },
                {
                    name: "Create API Key (via OAuth) - Creates a real API key using your account",
                    value: "create-api-key",
                },
            ],
        },
    ]);
    if (method === "api-key") {
        return await handleApiKeyAuth(provider, true);
    }
    else if (method === "create-api-key") {
        await handleCreateApiKeyOAuth(provider);
        return true;
    }
    else {
        await handleOAuthAuth(provider);
        return true;
    }
}
/**
 * Handle API key authentication
 */
async function handleApiKeyAuth(provider, interactive) {
    logger.always(chalk.blue("\nAPI Key Authentication\n"));
    if (provider === "anthropic") {
        logger.always(chalk.yellow("To get your Anthropic API key:"));
        logger.always("1. Visit: https://console.anthropic.com/");
        logger.always("2. Sign in to your Anthropic account");
        logger.always("3. Go to 'API Keys' section");
        logger.always("4. Click 'Create Key' and copy the API key (starts with sk-ant-)");
        logger.always("");
    }
    if (!interactive) {
        const envKey = process.env.ANTHROPIC_API_KEY?.trim();
        if (envKey) {
            await saveStoredCredentials(provider, {
                type: "api-key",
                apiKey: envKey,
                provider,
                createdAt: Date.now(),
                updatedAt: Date.now(),
            });
            logger.always(chalk.green("Using ANTHROPIC_API_KEY from environment."));
            return true;
        }
        throw new Error("Non-interactive mode requires ANTHROPIC_API_KEY environment variable when using --method api-key");
    }
    const { apiKey } = await inquirer.prompt([
        {
            type: "password",
            name: "apiKey",
            message: `Enter your ${provider} API key:`,
            validate: (input) => {
                if (!input.trim()) {
                    return "API key is required";
                }
                if (provider === "anthropic" && !input.startsWith("sk-ant-")) {
                    return "Anthropic API key should start with 'sk-ant-'";
                }
                if (input.trim().length < 20) {
                    return "API key seems too short";
                }
                return true;
            },
        },
    ]);
    const trimmedKey = apiKey.trim();
    // Validate the API key
    const spinner = ora("Validating API key...").start();
    try {
        const isValid = await validateApiKey(provider, trimmedKey);
        if (!isValid) {
            spinner.fail("API key validation failed");
            throw new Error("The API key could not be validated. Please check and try again.");
        }
        spinner.succeed("API key validated successfully");
    }
    catch (error) {
        spinner.fail("API key validation failed");
        throw error instanceof Error
            ? error
            : new Error(String(error) || "Validation error");
    }
    // Ask where to store the key
    const { storageOption } = await inquirer.prompt([
        {
            type: "list",
            name: "storageOption",
            message: "Where would you like to store the API key?",
            choices: [
                { name: ".env file (project-level)", value: "env" },
                { name: "NeuroLink config (user-level)", value: "config" },
                { name: "Both", value: "both" },
            ],
        },
    ]);
    const spinnerSave = ora("Saving API key...").start();
    try {
        if (storageOption === "env" || storageOption === "both") {
            await saveToEnvFile(provider, trimmedKey);
        }
        if (storageOption === "config" || storageOption === "both") {
            await saveStoredCredentials(provider, {
                type: "api-key",
                apiKey: trimmedKey,
                provider,
                createdAt: Date.now(),
                updatedAt: Date.now(),
            });
        }
        spinnerSave.succeed("API key saved successfully");
        logger.always("");
        logger.always(chalk.green("Authentication configured successfully!"));
        showUsageExample(provider);
        // Credentials file was written only if user chose "config" or "both"
        return storageOption === "config" || storageOption === "both";
    }
    catch (error) {
        spinnerSave.fail("Failed to save API key");
        throw error;
    }
}
/**
 * Handle API key creation via OAuth flow
 * This authenticates via console.anthropic.com, gets an OAuth token with org:create_api_key scope,
 * then uses that to create a real API key. This is the recommended method for Claude Pro/Max users.
 *
 * Based on opencode-anthropic-auth@0.0.8 implementation.
 * Uses manual code entry since localhost redirect is not registered with Anthropic OAuth.
 */
async function handleCreateApiKeyOAuth(provider) {
    logger.always(chalk.blue("\nCreate API Key (via OAuth) - Claude Pro/Max\n"));
    if (provider === "anthropic") {
        logger.always(chalk.cyan("This will authenticate using your Claude Pro or Max subscription"));
        logger.always(chalk.cyan("and create an API key for use with the Anthropic API.\n"));
        logger.always(chalk.yellow("Note: After signing in, you'll see an authorization code."));
        logger.always(chalk.yellow("Copy that code and paste it back here.\n"));
    }
    const spinner = ora("Starting OAuth flow...").start();
    // Generate PKCE challenge - OpenCode sets state = verifier
    const codeVerifier = randomBytes(32).toString("base64url");
    const codeChallenge = createHash("sha256")
        .update(codeVerifier)
        .digest("base64url");
    // Build authorization URL using CONSOLE config (for API key creation scope)
    // Based on opencode-anthropic-auth implementation
    const authUrl = new URL(ANTHROPIC_CONSOLE_OAUTH_CONFIG.authorizationUrl);
    authUrl.searchParams.set("code", "true"); // Required param
    authUrl.searchParams.set("client_id", ANTHROPIC_CONSOLE_OAUTH_CONFIG.clientId);
    authUrl.searchParams.set("response_type", "code");
    authUrl.searchParams.set("redirect_uri", ANTHROPIC_CONSOLE_OAUTH_CONFIG.redirectUri);
    authUrl.searchParams.set("scope", ANTHROPIC_CONSOLE_OAUTH_CONFIG.scope);
    authUrl.searchParams.set("code_challenge", codeChallenge);
    authUrl.searchParams.set("code_challenge_method", "S256");
    // OpenCode sets state = verifier for simplicity
    authUrl.searchParams.set("state", codeVerifier);
    spinner.text = "Opening browser for authentication...";
    // Open browser
    try {
        await openBrowser(authUrl.toString());
        spinner.succeed("Browser opened for authentication");
    }
    catch {
        spinner.warn("Could not open browser automatically");
        logger.always("");
        logger.always(chalk.yellow("Please open this URL manually:"));
        logger.always(chalk.cyan(authUrl.toString()));
    }
    logger.always("");
    logger.always(chalk.blue("═".repeat(60)));
    logger.always(chalk.blue.bold("  Complete authentication in your browser"));
    logger.always(chalk.blue("═".repeat(60)));
    logger.always("");
    logger.always("1. Sign in to your Anthropic account in the browser");
    logger.always("2. Authorize the application");
    logger.always("3. Copy the authorization code shown on the page");
    logger.always("4. Paste the code below");
    logger.always("");
    logger.always(chalk.dim("  Authentication URL:"));
    logger.always(chalk.cyan(`  ${authUrl.toString()}`));
    logger.always("");
    // Prompt user to enter the authorization code
    const { authCode } = await inquirer.prompt([
        {
            type: "input",
            name: "authCode",
            message: "Paste the authorization code:",
            validate: (input) => {
                if (!input.trim()) {
                    return "Authorization code is required";
                }
                if (input.trim().length < 10) {
                    return "Authorization code seems too short";
                }
                return true;
            },
        },
    ]);
    const exchangeSpinner = ora("Exchanging authorization code for tokens...").start();
    // Parse code#state format (e.g., "abc123#xyz789")
    // IMPORTANT: OpenCode sets state = verifier in the auth URL, so the state
    // returned in code#state IS the original verifier used for the code challenge!
    const trimmedCode = authCode.trim();
    const splits = trimmedCode.split("#");
    const actualCode = splits[0];
    const codeState = splits[1] || codeVerifier;
    // Use the state as the verifier (since we set state = verifier in the auth URL)
    const actualVerifier = codeState;
    // Exchange code for tokens using JSON body (per opencode-anthropic-auth)
    const tokenResponse = await fetch(ANTHROPIC_CONSOLE_OAUTH_CONFIG.tokenUrl, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            code: actualCode,
            state: codeState,
            grant_type: "authorization_code",
            client_id: ANTHROPIC_CONSOLE_OAUTH_CONFIG.clientId,
            redirect_uri: ANTHROPIC_CONSOLE_OAUTH_CONFIG.redirectUri,
            code_verifier: actualVerifier,
        }),
    });
    if (!tokenResponse.ok) {
        const errorText = await tokenResponse.text();
        exchangeSpinner.fail("Token exchange failed");
        throw new Error(`Token exchange failed: ${tokenResponse.status} - ${errorText}`);
    }
    const tokenData = (await tokenResponse.json());
    exchangeSpinner.succeed("OAuth tokens obtained successfully");
    // Now create an API key using the OAuth token
    const apiKeySpinner = ora("Creating API key...").start();
    try {
        const apiKeyResponse = await fetch(ANTHROPIC_CONSOLE_OAUTH_CONFIG.createApiKeyUrl, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${tokenData.access_token}`,
                "User-Agent": ANTHROPIC_CONSOLE_OAUTH_CONFIG.userAgent,
            },
        });
        if (!apiKeyResponse.ok) {
            const errorText = await apiKeyResponse.text();
            apiKeySpinner.fail("API key creation failed");
            throw new Error(`API key creation failed: ${apiKeyResponse.status} - ${errorText}`);
        }
        const apiKeyData = (await apiKeyResponse.json());
        if (!apiKeyData.raw_key) {
            apiKeySpinner.fail("API key creation failed");
            throw new Error("No API key returned from creation endpoint");
        }
        apiKeySpinner.succeed("API key created successfully!");
        // Auto-save to both locations for convenience
        const spinnerSave = ora("Saving API key...").start();
        try {
            // Save to .env file (project-level)
            await saveToEnvFile(provider, apiKeyData.raw_key);
            // Save to NeuroLink config (user-level)
            await saveStoredCredentials(provider, {
                type: "api-key",
                apiKey: apiKeyData.raw_key,
                provider,
                subscriptionTier: "pro", // Default for OAuth API-key flow; user can override via CLI
                createdAt: Date.now(),
                updatedAt: Date.now(),
            });
            spinnerSave.succeed("API key saved to .env and NeuroLink config");
            logger.always("");
            logger.always(chalk.green("═".repeat(60)));
            logger.always(chalk.green.bold("  API key created and saved successfully!"));
            logger.always(chalk.green("═".repeat(60)));
            logger.always("");
            logger.always(`  API Key: ${chalk.cyan(maskCredential(apiKeyData.raw_key))}`);
            logger.always(`  Created via: ${chalk.blue("Claude Pro/Max OAuth")}`);
            showUsageExample(provider);
        }
        catch (error) {
            spinnerSave.fail("Failed to save API key");
            throw error;
        }
    }
    catch (error) {
        if (error instanceof Error) {
            throw error;
        }
        throw new Error(`Failed to create API key: ${String(error)}`, {
            cause: error,
        });
    }
}
/**
 * Handle OAuth authentication using code-based flow
 * User authenticates in browser and copies the authorization code back to CLI
 * Uses claude.ai/oauth/authorize for Claude Pro/Max subscription access
 */
async function handleOAuthAuth(provider) {
    logger.always(chalk.blue("\nClaude Pro/Max OAuth Authentication\n"));
    if (provider === "anthropic") {
        logger.always(chalk.cyan("This will authenticate using your Claude Pro or Max subscription."));
        logger.always(chalk.cyan("Your subscription includes API access at no extra cost!\n"));
        logger.always(chalk.yellow("Note: After signing in, you'll see an authorization code."));
        logger.always(chalk.yellow("Copy that code and paste it back here.\n"));
    }
    const spinner = ora("Starting OAuth flow...").start();
    // Generate PKCE challenge - state = verifier (OpenCode's approach)
    const codeVerifier = randomBytes(32).toString("base64url");
    const codeChallenge = createHash("sha256")
        .update(codeVerifier)
        .digest("base64url");
    // Build authorization URL using claude.ai (NOT console.anthropic.com)
    // This is the direct OAuth flow for Claude Pro/Max
    const authUrl = new URL(ANTHROPIC_OAUTH_CONFIG.authorizationUrl);
    authUrl.searchParams.set("code", "true");
    authUrl.searchParams.set("client_id", ANTHROPIC_OAUTH_CONFIG.clientId);
    authUrl.searchParams.set("response_type", "code");
    authUrl.searchParams.set("redirect_uri", ANTHROPIC_OAUTH_CONFIG.redirectUri);
    authUrl.searchParams.set("scope", ANTHROPIC_OAUTH_CONFIG.scope);
    authUrl.searchParams.set("code_challenge", codeChallenge);
    authUrl.searchParams.set("code_challenge_method", "S256");
    // OpenCode sets state = verifier for simplicity
    authUrl.searchParams.set("state", codeVerifier);
    spinner.text = "Opening browser for authentication...";
    // Open browser
    try {
        await openBrowser(authUrl.toString());
        spinner.succeed("Browser opened for authentication");
    }
    catch {
        spinner.warn("Could not open browser automatically");
        logger.always("");
        logger.always(chalk.yellow("Please open this URL manually:"));
        logger.always(chalk.cyan(authUrl.toString()));
    }
    logger.always("");
    logger.always(chalk.blue("═".repeat(60)));
    logger.always(chalk.blue.bold("  Complete authentication in your browser"));
    logger.always(chalk.blue("═".repeat(60)));
    logger.always("");
    logger.always("1. Sign in to your Claude account in the browser");
    logger.always("2. Authorize the application");
    logger.always("3. Copy the authorization code shown on the page");
    logger.always("4. Paste the code below");
    logger.always("");
    logger.always(chalk.dim("  Authentication URL:"));
    logger.always(chalk.cyan(`  ${authUrl.toString()}`));
    logger.always("");
    // Prompt user to enter the authorization code
    const { authCode } = await inquirer.prompt([
        {
            type: "input",
            name: "authCode",
            message: "Paste the authorization code:",
            validate: (input) => {
                if (!input.trim()) {
                    return "Authorization code is required";
                }
                if (input.trim().length < 10) {
                    return "Authorization code seems too short";
                }
                return true;
            },
        },
    ]);
    const trimmedCode = authCode.trim();
    const exchangeSpinner = ora("Exchanging authorization code for tokens...").start();
    // Parse code#state format (e.g., "abc123#xyz789")
    // OpenCode sets state = verifier in the auth URL, so the state
    // returned in code#state IS the original verifier used for the code challenge
    const codeParts = trimmedCode.split("#");
    const actualCode = codeParts[0];
    const codeState = codeParts[1] || codeVerifier;
    // Use the state as the verifier (since we set state = verifier in the auth URL)
    const actualVerifier = codeState;
    // Exchange code for tokens with Claude CLI User-Agent
    // IMPORTANT: Uses JSON body, not URLSearchParams
    const tokenResponse = await fetch(ANTHROPIC_OAUTH_CONFIG.tokenUrl, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            code: actualCode,
            state: codeState,
            grant_type: "authorization_code",
            client_id: ANTHROPIC_OAUTH_CONFIG.clientId,
            redirect_uri: ANTHROPIC_OAUTH_CONFIG.redirectUri,
            code_verifier: actualVerifier,
        }),
    });
    if (!tokenResponse.ok) {
        const errorText = await tokenResponse.text();
        exchangeSpinner.fail("Token exchange failed");
        throw new Error(`Token exchange failed: ${tokenResponse.status} - ${errorText}`);
    }
    const tokenData = (await tokenResponse.json());
    // Save tokens
    const tokens = {
        accessToken: tokenData.access_token,
        refreshToken: tokenData.refresh_token,
        expiresAt: tokenData.expires_in
            ? Date.now() + tokenData.expires_in * 1000
            : undefined,
        tokenType: tokenData.token_type || "Bearer",
        scope: tokenData.scope,
    };
    // === Triple fallback chain for email & subscription tier ===
    // Primary: extract email from token exchange response
    let email = tokenData.account?.email_address;
    let subscriptionTier;
    // Fallback 1: validation endpoint (/v1/oauth/validate)
    // Always try when email OR tier is missing — token exchange gives email but not tier
    if (!email || !subscriptionTier) {
        try {
            const { AnthropicOAuth } = await import("../../auth/anthropicOAuth.js");
            const oauth = new AnthropicOAuth();
            const validationResult = await oauth.validateTokenWithDetails(tokens.accessToken);
            if (validationResult.user) {
                email = email || validationResult.user.email;
                if (validationResult.user.subscription) {
                    subscriptionTier = validationResult.user
                        .subscription;
                }
            }
        }
        catch {
            /* non-fatal — validation is best-effort */
        }
    }
    // Fallback 2: /v1/me endpoint (also detects subscription tier)
    if (!subscriptionTier || !email) {
        const meResult = await detectSubscriptionTierAndEmail(tokens.accessToken);
        if (!subscriptionTier && meResult.tier) {
            subscriptionTier = meResult.tier;
        }
        if (!email && meResult.email) {
            email = meResult.email;
        }
    }
    await saveStoredCredentials(provider, {
        type: "oauth",
        oauth: tokens,
        provider,
        subscriptionTier,
        email,
        createdAt: Date.now(),
        updatedAt: Date.now(),
    });
    exchangeSpinner.succeed("OAuth authentication successful!");
    logger.always("");
    logger.always(chalk.green("═".repeat(60)));
    logger.always(chalk.green.bold("  Authentication configured successfully!"));
    logger.always(chalk.green("═".repeat(60)));
    logger.always("");
    if (email) {
        logger.always(`  Email:             ${chalk.blue(email)}`);
    }
    if (subscriptionTier) {
        logger.always(`  Subscription Tier: ${chalk.blue(subscriptionTier)}`);
    }
    logger.always(`  Token expires: ${tokens.expiresAt ? new Date(tokens.expiresAt).toLocaleString() : "Never"}`);
    logger.always(`  Refresh token: ${tokens.refreshToken ? chalk.green("Available") : chalk.yellow("Not available")}`);
    showUsageExample(provider);
}
// =============================================================================
// MULTI-ACCOUNT HELPERS
// =============================================================================
/**
 * Save the most recently authenticated credentials to the TokenStore
 * using a compound key (e.g., "anthropic:alice") for multi-account pools.
 *
 * Reads back the credentials file that was just written by the auth flow,
 * then saves the tokens into the TokenStore under the compound key.
 */
async function saveAccountToPool(provider, label) {
    const credentials = await getStoredCredentials(provider);
    if (!credentials) {
        logger.error(chalk.red("Could not read back credentials after authentication. Multi-account save skipped."));
        return;
    }
    // Determine the compound key
    // Use email as the label when available (human-readable, unique per account)
    const credEmail = credentials.email;
    let compoundKey;
    if (label) {
        compoundKey = `${provider}:${label}`;
    }
    else {
        const autoLabel = credEmail ?? Date.now().toString(36).slice(-6);
        compoundKey = `${provider}:${autoLabel}`;
    }
    // Decision 10A: Auto-prune stale token-prefix entries when email is known.
    // When we have a real email, remove old entries whose stored token value is
    // identical to the one we are about to save.  This avoids false positives on
    // legitimate user labels like "primary", "work123", or "office42" that the old
    // broad regex (^[A-Za-z0-9_]{5,8}$) would incorrectly match.
    if (credEmail) {
        const newAccessToken = credentials.type === "oauth"
            ? credentials.oauth?.accessToken
            : credentials.apiKey;
        try {
            const allKeys = await defaultTokenStore.listProviders();
            for (const k of allKeys) {
                if (!k.startsWith(`${provider}:`)) {
                    continue;
                }
                const existingLabel = k.split(":").slice(1).join(":");
                // Skip the email key we are about to write
                if (existingLabel === credEmail) {
                    continue;
                }
                // Skip the explicit user label we are about to write
                if (label && existingLabel === label) {
                    continue;
                }
                // Only prune when the stored token value is identical (true duplicate)
                if (newAccessToken) {
                    try {
                        const existingTokens = await defaultTokenStore.loadTokens(k);
                        if (existingTokens &&
                            existingTokens.accessToken === newAccessToken) {
                            logger.debug(`Auto-pruning duplicate token entry: ${k}`);
                            await defaultTokenStore.clearTokens(k);
                        }
                    }
                    catch {
                        // Non-fatal — skip entries we can't read
                    }
                }
            }
        }
        catch {
            // Non-fatal — dedup is best-effort
            logger.debug("Failed to auto-prune stale entries during login");
        }
    }
    // Build per-account metadata scope string so email/tier are stored
    // alongside the token rather than in the shared credentials file.
    const metaParts = [];
    if (credentials.subscriptionTier) {
        metaParts.push(`tier:${credentials.subscriptionTier}`);
    }
    if (credentials.email) {
        metaParts.push(`email:${credentials.email}`);
    }
    // Build StoredOAuthTokens for the TokenStore
    if (credentials.type === "oauth" && credentials.oauth) {
        const baseScope = credentials.oauth.scope ?? "";
        const scopeWithMeta = [baseScope, ...metaParts].filter(Boolean).join(" ");
        await defaultTokenStore.saveTokens(compoundKey, {
            accessToken: credentials.oauth.accessToken,
            refreshToken: credentials.oauth.refreshToken,
            expiresAt: credentials.oauth.expiresAt ?? Date.now() + 3600 * 1000,
            tokenType: credentials.oauth.tokenType || "Bearer",
            scope: scopeWithMeta || undefined,
        });
    }
    else if (credentials.type === "api-key" && credentials.apiKey) {
        // For API keys, store as a non-expiring token in the TokenStore.
        // Use Number.MAX_SAFE_INTEGER so isTokenExpired() and pruneExpired()
        // never treat a perfectly valid API key as expired.
        const scopeWithMeta = metaParts.join(" ") || undefined;
        await defaultTokenStore.saveTokens(compoundKey, {
            accessToken: credentials.apiKey,
            expiresAt: Number.MAX_SAFE_INTEGER,
            tokenType: "api-key",
            scope: scopeWithMeta,
        });
    }
    else {
        logger.error(chalk.red("No valid credentials found. Multi-account save skipped."));
        return;
    }
    // Login is an explicit operator action and is the only token-save path that
    // should make a previously disabled account eligible again.
    await defaultTokenStore.markEnabled(compoundKey);
    logger.always(chalk.green(`\nAccount added: ${compoundKey}\n`));
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
 * Get authentication status for a provider
 * Priority: OAuth > stored API key > environment API key
 */
async function getAuthStatus(provider) {
    const result = {
        provider,
        isAuthenticated: false,
        method: "none",
    };
    // Codex never writes a credentials file — its accounts exist only as pooled
    // `codex:<label>` token-store entries, so checking the file alone reports a
    // fully authenticated pool as "Not Authenticated".
    if (provider === "codex") {
        const keys = await defaultTokenStore.listByPrefix("codex:");
        if (keys.length > 0) {
            result.isAuthenticated = true;
            result.method = "oauth";
            const tokens = await defaultTokenStore.peekTokens(keys[0]);
            result.hasRefreshToken = !!tokens?.refreshToken;
            if (tokens?.expiresAt) {
                result.tokenExpiry = new Date(tokens.expiresAt).toLocaleString();
                result.needsRefresh = Date.now() >= tokens.expiresAt;
            }
        }
        return result;
    }
    // Check stored credentials FIRST (OAuth takes priority over API key)
    const stored = await getStoredCredentials(provider);
    if (stored) {
        // OAuth credentials take highest priority
        if (stored.type === "oauth" && stored.oauth) {
            result.isAuthenticated = true;
            result.method = "oauth";
            result.subscriptionTier = stored.subscriptionTier;
            result.hasRefreshToken = !!stored.oauth.refreshToken;
            if (stored.oauth.expiresAt) {
                result.tokenExpiry = new Date(stored.oauth.expiresAt).toLocaleString();
                result.needsRefresh = Date.now() >= stored.oauth.expiresAt;
            }
            return result;
        }
        // Stored API key is second priority
        if (stored.type === "api-key" && stored.apiKey) {
            result.isAuthenticated = true;
            result.method = "api-key";
            return result;
        }
    }
    // Fall back to environment API key
    const envKey = getEnvApiKey(provider);
    if (envKey) {
        result.isAuthenticated = true;
        result.method = "api-key";
        return result;
    }
    return result;
}
/**
 * Detect subscription tier and email from /v1/me endpoint.
 * Returns both tier and email when available (used as fallback 2 in the
 * triple fallback chain for email resolution).
 */
async function detectSubscriptionTierAndEmail(accessToken) {
    try {
        const response = await fetch("https://api.anthropic.com/v1/me", {
            headers: {
                Authorization: `Bearer ${accessToken}`,
                "Content-Type": "application/json",
                "User-Agent": "claude-cli/2.1.80 (external, cli)",
                "anthropic-version": "2023-06-01",
            },
        });
        if (response.ok) {
            const data = (await response.json());
            return {
                tier: data.subscription,
                email: data.email || data.email_address,
            };
        }
    }
    catch {
        // Ignore errors - subscription/email detection is optional
    }
    return {};
}
/**
 * Open URL in the default browser (cross-platform)
 */
async function openBrowser(url) {
    return new Promise((resolve, reject) => {
        const platform = process.platform;
        let command;
        let args;
        switch (platform) {
            case "darwin":
                command = "open";
                args = [url];
                break;
            case "win32":
                command = "cmd";
                args = ["/c", "start", "", url];
                break;
            default:
                // Linux and other Unix-like systems
                command = "xdg-open";
                args = [url];
        }
        // Use execFile instead of exec to prevent command injection
        execFile(command, args, (error) => {
            if (error) {
                reject(error);
            }
            else {
                resolve();
            }
        });
    });
}
/**
 * Get environment variable name for a provider
 */
function getEnvVarName(provider) {
    switch (provider) {
        case "anthropic":
            return "ANTHROPIC_API_KEY";
        default:
            return `${provider.toUpperCase()}_API_KEY`;
    }
}
/**
 * Get API key from environment
 */
function getEnvApiKey(provider) {
    return process.env[getEnvVarName(provider)];
}
/**
 * Get stored credentials from file
 */
async function getStoredCredentials(provider) {
    const credentialsFile = path.join(NEUROLINK_CONFIG_DIR, `${provider}-credentials.json`);
    try {
        if (fs.existsSync(credentialsFile)) {
            const data = fs.readFileSync(credentialsFile, "utf-8");
            return JSON.parse(data);
        }
    }
    catch (error) {
        logger.debug(`Failed to read credentials: ${error}`);
    }
    return null;
}
/**
 * Save credentials to file
 */
async function saveStoredCredentials(provider, credentials) {
    // Ensure config directory exists
    if (!fs.existsSync(NEUROLINK_CONFIG_DIR)) {
        fs.mkdirSync(NEUROLINK_CONFIG_DIR, { recursive: true });
    }
    const credentialsFile = path.join(NEUROLINK_CONFIG_DIR, `${provider}-credentials.json`);
    fs.writeFileSync(credentialsFile, JSON.stringify(credentials, null, 2), {
        mode: 0o600, // Restrict permissions
    });
}
/**
 * Check existing authentication status
 */
async function checkExistingAuth(provider) {
    const envKey = getEnvApiKey(provider);
    if (envKey) {
        return { hasValidAuth: true, type: "api-key", credential: envKey };
    }
    const stored = await getStoredCredentials(provider);
    if (stored) {
        if (stored.type === "api-key" && stored.apiKey) {
            return { hasValidAuth: true, type: "api-key", credential: stored.apiKey };
        }
        if (stored.type === "oauth" && stored.oauth) {
            // Check if token is still valid
            if (stored.oauth.expiresAt) {
                const isExpired = Date.now() >= stored.oauth.expiresAt;
                if (!isExpired || stored.oauth.refreshToken) {
                    return { hasValidAuth: true, type: "oauth" };
                }
            }
            else {
                return { hasValidAuth: true, type: "oauth" };
            }
        }
    }
    return { hasValidAuth: false };
}
/**
 * Validate API key by making a test request
 */
async function validateApiKey(provider, apiKey) {
    if (provider === "anthropic") {
        try {
            // Simple validation - check message API
            const response = await fetch("https://api.anthropic.com/v1/messages", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    "x-api-key": apiKey,
                    "anthropic-version": "2023-06-01",
                },
                body: JSON.stringify({
                    model: "claude-3-haiku-20240307",
                    max_tokens: 1,
                    messages: [{ role: "user", content: "Hi" }],
                }),
            });
            // 200 means valid, 401 means invalid key
            // Other errors (rate limit, etc.) still mean the key format is valid
            return response.status !== 401;
        }
        catch {
            // Network error - assume key format is valid
            return true;
        }
    }
    return true;
}
/**
 * Save API key to .env file
 */
async function saveToEnvFile(provider, apiKey) {
    const envVar = getEnvVarName(provider);
    let content = "";
    if (fs.existsSync(ENV_FILE_PATH)) {
        content = fs.readFileSync(ENV_FILE_PATH, "utf-8");
    }
    // Check if variable already exists
    const regex = new RegExp(`^${envVar}=.*$`, "m");
    if (regex.test(content)) {
        // Replace existing
        content = content.replace(regex, `${envVar}=${apiKey}`);
    }
    else {
        // Add new
        if (content && !content.endsWith("\n")) {
            content += "\n";
        }
        content += `${envVar}=${apiKey}\n`;
    }
    fs.writeFileSync(ENV_FILE_PATH, content);
}
/**
 * Remove variable from .env file
 */
async function removeFromEnvFile(envVar) {
    if (!fs.existsSync(ENV_FILE_PATH)) {
        return;
    }
    let content = fs.readFileSync(ENV_FILE_PATH, "utf-8");
    const regex = new RegExp(`^${envVar}=.*\n?`, "m");
    content = content.replace(regex, "");
    fs.writeFileSync(ENV_FILE_PATH, content);
}
/**
 * Mask credential for display
 */
function maskCredential(credential) {
    if (!credential || credential.length < 8) {
        return "****";
    }
    const knownPrefixes = ["sk-ant-", "sk-"];
    const prefix = knownPrefixes.find((p) => credential.startsWith(p)) ??
        credential.slice(0, 4);
    const end = credential.slice(-4);
    const stars = "*".repeat(Math.max(4, credential.length - prefix.length - 4));
    return `${prefix}${stars}${end}`;
}
/**
 * Show usage example after successful authentication
 */
function showUsageExample(provider) {
    logger.always("");
    logger.always(chalk.green("You can now use the NeuroLink CLI with this provider:"));
    logger.always(chalk.cyan(`   neurolink generate "Hello!" --provider ${provider}`));
    logger.always(chalk.cyan(`   neurolink generate "Explain quantum computing" --provider ${provider}`));
    logger.always("");
    logger.always(chalk.blue("To check authentication status:"));
    logger.always(chalk.cyan(`   neurolink auth status ${provider}`));
    logger.always("");
    logger.always(chalk.blue("To logout:"));
    logger.always(chalk.cyan(`   neurolink auth logout ${provider}`));
}
//# sourceMappingURL=auth.js.map