Remove standalone fetcher, add setup tool with install/uninstall workflow
All checks were successful
Release / build (push) Successful in 1m45s

Drop claude-fetcher binary and cron job — the widget's built-in
BackgroundFetcher is the sole fetcher now. Add cmd/setup with cross-platform
install and uninstall (--uninstall): kills widget, removes binaries + autostart,
cleans Claude Code statusline setting, optionally removes config dir.

Also includes: browser-based login (chromedp), ICO wrapper for Windows tray
icon, and reduced icon size (64px).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-02-26 19:11:08 +01:00
parent 5b0366f16b
commit 47165ce02c
15 changed files with 796 additions and 126 deletions

102
internal/browser/login.go Normal file
View File

@@ -0,0 +1,102 @@
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 ""
}