| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684 |
- using System;
- using System.Drawing;
- using System.IO;
- using System.Net.Http;
- using System.Text.Json;
- using System.Threading.Tasks;
- using System.Windows.Forms;
- using System.Text.RegularExpressions;
- using System.Diagnostics;
- using System.Collections.Generic;
- using System.Text;
- namespace qdr.app.tools.claudecodebalancewidget
- {
- public partial class MainForm : Form
- {
- private NotifyIcon notifyIcon;
- private System.Windows.Forms.Timer refreshTimer;
- private HttpClient httpClient;
- private string sessionToken = "";
- private string apiKey = "";
- private decimal currentBalance = 0;
- private bool isVisible = true;
- private bool isDragging = false;
- private Point dragStartPoint;
- private DateTime lastUpdateTime = DateTime.MinValue;
- private bool isRefreshing = false;
- private string lastError = "";
- private AuthenticationMethod authMethod = AuthenticationMethod.SessionCookie;
- public enum AuthenticationMethod
- {
- SessionCookie,
- ApiKey,
- ConsoleOnly // New method for API key users
- }
- public MainForm()
- {
- InitializeComponent();
- InitializeSystemTray();
- InitializeHttpClient();
- InitializeTimer();
- SetupTransparentWindow();
-
- LoadConfiguration();
- _ = RefreshBalance();
- }
- private void SetupTransparentWindow()
- {
- this.BackColor = Color.Lime;
- this.TransparencyKey = Color.Lime;
- this.Opacity = 0.95;
-
- this.SetStyle(ControlStyles.AllPaintingInWmPaint |
- ControlStyles.UserPaint |
- ControlStyles.DoubleBuffer |
- ControlStyles.ResizeRedraw |
- ControlStyles.SupportsTransparentBackColor, true);
- this.Size = new Size(300, 100);
- }
- private void InitializeSystemTray()
- {
- notifyIcon = new NotifyIcon();
- // Use the application icon instead of creating a custom one
- notifyIcon.Icon = this.Icon ?? Icon.ExtractAssociatedIcon(Application.ExecutablePath);
- notifyIcon.Text = "Claude Balance Monitor";
- notifyIcon.Visible = true;
-
- var contextMenu = new ContextMenuStrip();
-
- var showHideItem = new ToolStripMenuItem("Show/Hide", null, ShowHide_Click);
- var refreshItem = new ToolStripMenuItem("Refresh Now", null, RefreshNow_Click);
- var openConsoleItem = new ToolStripMenuItem("Open Console Billing", null, OpenConsole_Click);
- var settingsItem = new ToolStripMenuItem("Settings...", null, Settings_Click);
- var aboutItem = new ToolStripMenuItem("About", null, About_Click);
- var exitItem = new ToolStripMenuItem("Exit", null, Exit_Click);
-
- contextMenu.Items.Add(showHideItem);
- contextMenu.Items.Add(refreshItem);
- contextMenu.Items.Add(openConsoleItem);
- contextMenu.Items.Add(new ToolStripSeparator());
- contextMenu.Items.Add(settingsItem);
- contextMenu.Items.Add(aboutItem);
- contextMenu.Items.Add(new ToolStripSeparator());
- contextMenu.Items.Add(exitItem);
-
- notifyIcon.ContextMenuStrip = contextMenu;
- notifyIcon.DoubleClick += ShowHide_Click;
- }
- // Removed CreateIcon method since we're using the application icon now
- private void InitializeHttpClient()
- {
- httpClient = new HttpClient();
- httpClient.Timeout = TimeSpan.FromSeconds(15);
-
- httpClient.DefaultRequestHeaders.Add("User-Agent",
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
- httpClient.DefaultRequestHeaders.Add("Accept",
- "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
- httpClient.DefaultRequestHeaders.Add("Accept-Language", "en-US,en;q=0.5");
- httpClient.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
- httpClient.DefaultRequestHeaders.Add("DNT", "1");
- httpClient.DefaultRequestHeaders.Add("Connection", "keep-alive");
- httpClient.DefaultRequestHeaders.Add("Upgrade-Insecure-Requests", "1");
- }
- private void InitializeTimer()
- {
- refreshTimer = new System.Windows.Forms.Timer();
- refreshTimer.Interval = 300000; // 5 minutes - slower since we're using Console scraping
- refreshTimer.Tick += async (s, e) => await RefreshBalance();
- refreshTimer.Start();
- }
- private void LoadConfiguration()
- {
- try
- {
- string configPath = GetConfigPath();
- string configDir = Path.GetDirectoryName(configPath);
-
- if (!Directory.Exists(configDir))
- {
- Directory.CreateDirectory(configDir);
- }
-
- if (File.Exists(configPath))
- {
- var lines = File.ReadAllLines(configPath);
- foreach (var line in lines)
- {
- if (line.StartsWith("SessionToken="))
- {
- sessionToken = line.Substring("SessionToken=".Length).Trim();
- }
- else if (line.StartsWith("ApiKey="))
- {
- apiKey = line.Substring("ApiKey=".Length).Trim();
- }
- else if (line.StartsWith("AuthMethod="))
- {
- if (Enum.TryParse<AuthenticationMethod>(line.Substring("AuthMethod=".Length).Trim(), out var method))
- {
- authMethod = method;
- }
- }
- }
- }
- else
- {
- var settingsForm = new SettingsForm("");
- if (settingsForm.ShowDialog() == DialogResult.OK)
- {
- sessionToken = settingsForm.SessionToken;
- SaveConfiguration();
- }
- }
- }
- catch (Exception ex)
- {
- lastError = $"Config error: {ex.Message}";
- MessageBox.Show($"Error loading configuration: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
- }
- }
- private string GetConfigPath()
- {
- return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
- "ClaudeBalanceMonitor", "config.txt");
- }
- private void SaveConfiguration()
- {
- try
- {
- string configPath = GetConfigPath();
- Directory.CreateDirectory(Path.GetDirectoryName(configPath));
-
- var lines = new List<string>
- {
- $"SessionToken={sessionToken}",
- $"ApiKey={apiKey}",
- $"AuthMethod={authMethod}"
- };
-
- File.WriteAllLines(configPath, lines);
- }
- catch (Exception ex)
- {
- MessageBox.Show($"Error saving configuration: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
- }
- }
- private async Task RefreshBalance()
- {
- if (isRefreshing) return;
-
- try
- {
- isRefreshing = true;
- lastError = "";
-
- decimal balance = -1;
-
- switch (authMethod)
- {
- case AuthenticationMethod.ApiKey:
- case AuthenticationMethod.ConsoleOnly:
- balance = await GetBalanceViaConsoleWithApiKey();
- break;
- case AuthenticationMethod.SessionCookie:
- balance = await GetBalanceViaSessionCookie();
- break;
- }
-
- UpdateBalance(balance, balance >= 0 ? "" : lastError);
- if (balance >= 0)
- {
- lastUpdateTime = DateTime.Now;
- }
- }
- catch (Exception ex)
- {
- UpdateBalance(-1, ex.Message);
- Console.WriteLine($"Error refreshing balance: {ex.Message}");
- }
- finally
- {
- isRefreshing = false;
- }
- }
- private async Task<decimal> GetBalanceViaConsoleWithApiKey()
- {
- try
- {
- if (string.IsNullOrEmpty(apiKey))
- {
- lastError = "No API key configured";
- return -1;
- }
- // Since there's no direct API endpoint for balance, we need to:
- // 1. First verify the API key works
- // 2. Then try to access the Console billing page
-
- // Step 1: Verify API key with a minimal request
- var isValidApiKey = await VerifyApiKey();
- if (!isValidApiKey)
- {
- return -1;
- }
- // Step 2: Try to access Console billing page
- // This is tricky because we need to be logged in to Console
- // For now, we'll show a message that the user needs to check manually
- lastError = "API key valid - Check Console manually for balance";
- return -2; // Special code to indicate "check manually"
- }
- catch (Exception ex)
- {
- lastError = $"API Error: {ex.Message}";
- return -1;
- }
- }
- private async Task<bool> VerifyApiKey()
- {
- try
- {
- // Make a small test request to verify the API key works
- var request = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages");
- request.Headers.Add("x-api-key", apiKey);
- request.Headers.Add("anthropic-version", "2023-06-01");
-
- var testContent = @"{
- ""model"": ""claude-3-5-haiku-20241022"",
- ""max_tokens"": 1,
- ""messages"": [{
- ""role"": ""user"",
- ""content"": ""Hi""
- }]
- }";
-
- request.Content = new StringContent(testContent, Encoding.UTF8, "application/json");
-
- var response = await httpClient.SendAsync(request);
-
- if (response.IsSuccessStatusCode)
- {
- return true;
- }
- else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
- {
- lastError = "Invalid API key";
- return false;
- }
- else if (response.StatusCode == System.Net.HttpStatusCode.PaymentRequired)
- {
- lastError = "No credits available";
- return false;
- }
- else
- {
- lastError = $"API verification failed: {response.StatusCode}";
- return false;
- }
- }
- catch (Exception ex)
- {
- lastError = $"API verification error: {ex.Message}";
- return false;
- }
- }
- private async Task<decimal> GetBalanceViaSessionCookie()
- {
- try
- {
- if (string.IsNullOrEmpty(sessionToken))
- {
- lastError = "No session token configured";
- return -1;
- }
- // Clear existing headers
- httpClient.DefaultRequestHeaders.Remove("Cookie");
- httpClient.DefaultRequestHeaders.Remove("X-Requested-With");
- httpClient.DefaultRequestHeaders.Remove("Referer");
-
- // Add session authentication with enhanced headers
- httpClient.DefaultRequestHeaders.Add("Cookie", $"sessionKey={sessionToken}");
- httpClient.DefaultRequestHeaders.Add("X-Requested-With", "XMLHttpRequest");
- httpClient.DefaultRequestHeaders.Add("Referer", "https://console.anthropic.com/");
- httpClient.DefaultRequestHeaders.Add("Sec-Fetch-Dest", "document");
- httpClient.DefaultRequestHeaders.Add("Sec-Fetch-Mode", "navigate");
- httpClient.DefaultRequestHeaders.Add("Sec-Fetch-Site", "same-origin");
- var response = await httpClient.GetAsync("https://console.anthropic.com/settings/billing");
-
- if (response.IsSuccessStatusCode)
- {
- var html = await response.Content.ReadAsStringAsync();
- return ExtractBalanceFromHtml(html);
- }
- else if (response.StatusCode == System.Net.HttpStatusCode.Forbidden)
- {
- lastError = "Forbidden - Session token invalid or requires additional auth";
- return -1;
- }
- else
- {
- lastError = $"HTTP {response.StatusCode}";
- return -1;
- }
- }
- catch (Exception ex)
- {
- lastError = $"Session Error: {ex.Message}";
- return -1;
- }
- }
- private decimal ExtractBalanceFromHtml(string html)
- {
- // Enhanced patterns for finding credit balance
- var patterns = new[]
- {
- @"Credit balance[\s\S]*?\$?([\d,]+\.?\d*)",
- @"Available credit[\s\S]*?\$?([\d,]+\.?\d*)",
- @"remaining[^>]*>[\s]*\$?([\d,]+\.?\d*)",
- @"balance[^>]*>[\s]*\$?([\d,]+\.?\d*)",
- @"credit[^>]*>[\s]*\$?([\d,]+\.?\d*)",
- @"\$?([\d,]+\.?\d*)[^<]*remaining",
- @"""credit_balance"":\s*""?\$?([\d,]+\.?\d*)""?",
- @"""balance"":\s*""?\$?([\d,]+\.?\d*)""?",
- @"data-balance=""?\$?([\d,]+\.?\d*)""?",
- @"balance.*?(\d+\.?\d*)",
- @"Credits?[\s\S]*?\$?([\d,]+\.?\d*)",
- @"\$?([\d,]+\.?\d*)[\s]*credit"
- };
- foreach (var pattern in patterns)
- {
- var matches = Regex.Matches(html, pattern, RegexOptions.IgnoreCase);
- foreach (Match match in matches)
- {
- var balanceStr = match.Groups[1].Value.Replace(",", "");
- if (decimal.TryParse(balanceStr, out decimal balance) && balance >= 0)
- {
- return balance;
- }
- }
- }
- lastError = "Could not find balance in Console page";
- return -1;
- }
- private void UpdateBalance(decimal balance, string error = "")
- {
- if (InvokeRequired)
- {
- Invoke(new Action<decimal, string>(UpdateBalance), balance, error);
- return;
- }
- currentBalance = balance;
- lastError = error;
-
- if (balance >= 0)
- {
- notifyIcon.Text = $"Claude Balance: ${balance:F2}\nLast updated: {DateTime.Now:HH:mm:ss}\nMethod: {authMethod}";
- }
- else if (balance == -2) // Special case for "check manually"
- {
- notifyIcon.Text = $"Claude API Key Valid\nCheck Console for balance\nRight-click → Open Console";
- }
- else
- {
- notifyIcon.Text = $"Claude Balance: Error\n{(string.IsNullOrEmpty(error) ? "Unknown error" : error)}\nMethod: {authMethod}";
- }
-
- if (isVisible)
- {
- Invalidate();
- }
- }
- private void MainForm_Paint(object sender, PaintEventArgs e)
- {
- var g = e.Graphics;
- g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
- g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
-
- // Draw background with rounded corners
- var rect = new Rectangle(5, 5, Width - 10, Height - 10);
- using (var path = CreateRoundedRectangle(rect, 12))
- {
- using (var brush = new System.Drawing.Drawing2D.LinearGradientBrush(rect,
- Color.FromArgb(200, 20, 25, 35),
- Color.FromArgb(180, 40, 45, 60),
- System.Drawing.Drawing2D.LinearGradientMode.Vertical))
- {
- g.FillPath(brush, path);
- }
-
- using (var pen = new Pen(Color.FromArgb(100, 255, 255, 255), 1))
- {
- g.DrawPath(pen, path);
- }
- }
-
- // Draw balance text
- string balanceText;
- Color textColor;
-
- if (currentBalance >= 0)
- {
- balanceText = $"${currentBalance:F2}";
- textColor = currentBalance > 5 ? Color.FromArgb(255, 144, 238, 144) :
- currentBalance > 1 ? Color.FromArgb(255, 255, 215, 0) :
- Color.FromArgb(255, 255, 140, 140);
- }
- else if (currentBalance == -2) // Special case for "check manually"
- {
- balanceText = "Check Console";
- textColor = Color.FromArgb(255, 100, 149, 237);
- }
- else
- {
- balanceText = "Error";
- textColor = Color.FromArgb(255, 255, 100, 100);
- }
-
- // Main balance text
- using (var font = new Font("Segoe UI", currentBalance == -2 ? 10 : 13, FontStyle.Bold))
- using (var brush = new SolidBrush(textColor))
- {
- var textSize = g.MeasureString(balanceText, font);
- var x = (Width - textSize.Width) / 2;
- var y = currentBalance == -2 ? 18 : 12;
- g.DrawString(balanceText, font, brush, x, y);
- }
-
- // Status text
- string statusText;
- if (currentBalance == -2)
- {
- statusText = "API Key Valid - Click to open Console";
- }
- else if (!string.IsNullOrEmpty(lastError))
- {
- statusText = lastError.Length > 30 ? lastError.Substring(0, 30) + "..." : lastError;
- }
- else if (lastUpdateTime != DateTime.MinValue)
- {
- statusText = $"Updated: {lastUpdateTime:HH:mm:ss} ({authMethod})";
- }
- else
- {
- statusText = "Loading...";
- }
-
- using (var font = new Font("Segoe UI", 7.5f))
- using (var brush = new SolidBrush(Color.FromArgb(180, 200, 200, 200)))
- {
- var textSize = g.MeasureString(statusText, font);
- var x = (Width - textSize.Width) / 2;
- var y = Height - 22;
- g.DrawString(statusText, font, brush, x, y);
- }
- }
- private System.Drawing.Drawing2D.GraphicsPath CreateRoundedRectangle(Rectangle rect, int radius)
- {
- var path = new System.Drawing.Drawing2D.GraphicsPath();
- var diameter = radius * 2;
-
- path.AddArc(rect.X, rect.Y, diameter, diameter, 180, 90);
- path.AddArc(rect.Right - diameter, rect.Y, diameter, diameter, 270, 90);
- path.AddArc(rect.Right - diameter, rect.Bottom - diameter, diameter, diameter, 0, 90);
- path.AddArc(rect.X, rect.Bottom - diameter, diameter, diameter, 90, 90);
- path.CloseFigure();
-
- return path;
- }
- private async void RefreshNow_Click(object sender, EventArgs e)
- {
- await RefreshBalance();
- }
- private void OpenConsole_Click(object sender, EventArgs e)
- {
- try
- {
- Process.Start(new ProcessStartInfo
- {
- FileName = "https://console.anthropic.com/settings/billing",
- UseShellExecute = true
- });
- }
- catch (Exception ex)
- {
- MessageBox.Show($"Could not open browser: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
- }
- }
- private void About_Click(object sender, EventArgs e)
- {
- MessageBox.Show(
- $"Claude Code Balance Widget v{System.Reflection.Assembly.GetExecutingAssembly().GetName().Version}\n\n" +
- "⚠️ IMPORTANT: Anthropic doesn't provide a balance API endpoint.\n" +
- "This widget can verify your API key but cannot automatically\n" +
- "retrieve your balance. You'll need to check the Console manually.\n\n" +
- "Created by Dalibor Votruba (Quadarax)\n\n" +
- "Supported Methods:\n" +
- "• API Key Verification + Manual Console Check\n" +
- "• Session Cookie (may not work due to security)",
- "About Claude Balance Monitor",
- MessageBoxButtons.OK,
- MessageBoxIcon.Information
- );
- }
- private void ShowHide_Click(object sender, EventArgs e)
- {
- if (isVisible)
- {
- Hide();
- isVisible = false;
- }
- else
- {
- Show();
- WindowState = FormWindowState.Normal;
- isVisible = true;
- }
- }
- private void Settings_Click(object sender, EventArgs e)
- {
- var settingsForm = new SettingsForm(sessionToken);
- if (settingsForm.ShowDialog() == DialogResult.OK)
- {
- sessionToken = settingsForm.SessionToken;
- SaveConfiguration();
- _ = RefreshBalance();
- }
- }
- private void Exit_Click(object sender, EventArgs e)
- {
- refreshTimer?.Stop();
- notifyIcon?.Dispose();
- httpClient?.Dispose();
- Application.Exit();
- }
- // Mouse event handlers for dragging and clicking
- private void MainForm_MouseDown(object sender, MouseEventArgs e)
- {
- if (e.Button == MouseButtons.Left)
- {
- isDragging = true;
- dragStartPoint = e.Location;
- this.Cursor = Cursors.SizeAll;
- }
- else if (e.Button == MouseButtons.Right)
- {
- notifyIcon.ContextMenuStrip.Show(Cursor.Position);
- }
- }
- private void MainForm_MouseMove(object sender, MouseEventArgs e)
- {
- if (isDragging)
- {
- Point screenPoint = PointToScreen(e.Location);
- Location = new Point(screenPoint.X - dragStartPoint.X, screenPoint.Y - dragStartPoint.Y);
- }
- else
- {
- this.Cursor = currentBalance == -2 ? Cursors.Hand : Cursors.SizeAll;
- }
- }
- private void MainForm_MouseUp(object sender, MouseEventArgs e)
- {
- if (e.Button == MouseButtons.Left)
- {
- if (isDragging)
- {
- // Check if this was actually a drag or just a click
- var dragDistance = Math.Sqrt(Math.Pow(e.X - dragStartPoint.X, 2) + Math.Pow(e.Y - dragStartPoint.Y, 2));
-
- if (dragDistance < 5) // Less than 5 pixels = click, not drag
- {
- // This was a click, not a drag
- if (currentBalance == -2) // If showing "Check Console", open console on click
- {
- OpenConsole_Click(sender, e);
- }
- }
- }
-
- isDragging = false;
- this.Cursor = currentBalance == -2 ? Cursors.Hand : Cursors.Default;
- }
- }
- protected override void OnFormClosing(FormClosingEventArgs e)
- {
- if (e.CloseReason == CloseReason.UserClosing)
- {
- e.Cancel = true;
- Hide();
- isVisible = false;
- }
- else
- {
- base.OnFormClosing(e);
- }
- }
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- refreshTimer?.Dispose();
- notifyIcon?.Dispose();
- httpClient?.Dispose();
- }
- base.Dispose(disposing);
- }
- }
- }
|