package browser import ( "context" "fmt" "os" "path/filepath" "runtime" "time" "github.com/chromedp/cdproto/network" "github.com/chromedp/chromedp" "git.davoryn.de/calic/claude-statusline/internal/config" ) // LoginAndGetSessionKey opens a browser window for the user to log in to // claude.ai and extracts the httpOnly sessionKey cookie via DevTools protocol. // The browser uses a persistent profile so the user only needs to log in once. // Returns the session key or an error (e.g. timeout after 2 minutes). func LoginAndGetSessionKey() (string, error) { execPath := findBrowserExec() profileDir := filepath.Join(config.ConfigDir(), "browser-profile") if err := os.MkdirAll(profileDir, 0o755); err != nil { return "", fmt.Errorf("create browser profile dir: %w", err) } opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.Flag("headless", false), chromedp.UserDataDir(profileDir), ) if execPath != "" { opts = append(opts, chromedp.ExecPath(execPath)) } allocCtx, allocCancel := chromedp.NewExecAllocator(context.Background(), opts...) defer allocCancel() ctx, cancel := chromedp.NewContext(allocCtx) defer cancel() // Navigate to login page if err := chromedp.Run(ctx, chromedp.Navigate("https://claude.ai/login")); err != nil { return "", fmt.Errorf("navigate to login: %w", err) } // Poll for the sessionKey cookie (httpOnly, so only accessible via DevTools) deadline := time.Now().Add(2 * time.Minute) ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() for { select { case <-ticker.C: if time.Now().After(deadline) { return "", fmt.Errorf("login timed out after 2 minutes") } var cookies []*network.Cookie if err := chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error { var err error cookies, err = network.GetCookies().Do(ctx) return err })); err != nil { // Browser may have been closed by user return "", fmt.Errorf("get cookies: %w", err) } for _, c := range cookies { if c.Name == "sessionKey" && (c.Domain == ".claude.ai" || c.Domain == "claude.ai") { key := c.Value if err := config.SetSessionKey(key); err != nil { return "", fmt.Errorf("save session key: %w", err) } // Use chromedp.Cancel to close gracefully (flushes cookies to profile) cancel() return key, nil } } } } } // findBrowserExec returns the path to a Chromium-based browser, or "" to let // chromedp use its default detection (Chrome/Chromium on PATH). func findBrowserExec() string { if runtime.GOOS == "windows" { // Prefer Edge (pre-installed on Windows 10+) candidates := []string{ filepath.Join(os.Getenv("ProgramFiles(x86)"), "Microsoft", "Edge", "Application", "msedge.exe"), filepath.Join(os.Getenv("ProgramFiles"), "Microsoft", "Edge", "Application", "msedge.exe"), } for _, p := range candidates { if _, err := os.Stat(p); err == nil { return p } } } // On Linux/macOS, chromedp auto-detects Chrome/Chromium return "" }