Compare commits

..
Author SHA1 Message Date
superdooper86 ac7ffe81af fix: use WebView JS fetch for auth detection; send all cookies to API
Cookie domain filtering was wrong — the session token domain is unknown
and was never found by claude.ai/anthropic.com filters.

LoginView: replace getAllCookies domain check with callAsyncJavaScript
that fetches /api/bootstrap directly from the WebView. The WebView uses
its own full session (all cookies, any domain) so auth is detected
correctly regardless of where the token lives.

UsageViewModel: claudeCookieHeader now sends all cookies from the app's
WKWebsiteDataStore instead of filtering by domain. checkInitialSignInState
likewise checks for any cookie.
2026-05-11 10:06:33 +02:00
github-actions[bot] f28f7b8a5a Beta release v1.2.1-beta.7 2026-05-11 07:51:25 +00:00
superdooper86 aaed64484c fix: include anthropic.com cookies in all auth checks and API requests
Claude session cookies are on anthropic.com, not claude.ai. The login
window was not detecting auth (Cancel stayed, no Done) and API calls
were sent without the actual session token.

- claudeCookieHeader: include anthropic.com cookies so the token is
  sent to the usage/bootstrap endpoints
- checkInitialSignInState: detect anthropic.com cookies on startup
- didFinish in LoginView: fire auth when anthropic.com cookies found
- signOut: clear anthropic.com data alongside claude.ai
- notAuthenticated catch: set isSignedIn = false so Settings stays
  in sync with the main panel
2026-05-11 09:49:57 +02:00
github-actions[bot] 2e3ee350ca Beta release v1.2.1-beta.6 2026-05-11 07:37:40 +00:00
superdooper86 e939bdb88b fix: add browser headers to all API requests to resolve 403 on usage endpoint
Claude's usage/prepaid/overage endpoints require Origin, Referer, and
User-Agent headers to pass CORS/auth checks. Without them, bootstrap
succeeds (more permissive) but usage returns 403 -> 'Not signed in'.

Added claudeAPIRequest(for:) helper that sets all required browser-like
headers on every request. Bootstrap, usage, prepaid, overage, and the
orgs fallback all go through it.
2026-05-11 09:36:35 +02:00
github-actions[bot] 6749c5f158 Beta release v1.2.1-beta.5 2026-05-11 07:27:17 +00:00
superdooper86 de71ec2794 fix: load /login instead of root so premature auth detection is prevented
Loading https://claude.ai as the start URL caused didFinish to fire on
the landing page while stale/tracking cookies were already in
WKWebsiteDataStore. The 'any claude.ai cookie' check then fired
immediately, closing the login sheet before the user could sign in.

Loading /login ensures the URL-guard catches the initial page load and
only checks cookies after the real post-login redirect.
2026-05-11 09:25:45 +02:00
github-actions[bot] ac17ce154f Beta release v1.2.1-beta.4 2026-05-11 06:59:20 +00:00
SuperDooper d2eac62897 chore: bump to v1.2.1-beta.4 2026-05-11 08:58:05 +02:00
SuperDooper 8c64fc50ad fix: checkInitialSignInState uses any claude.ai cookie, not specific names 2026-05-11 08:58:04 +02:00
SuperDooper b2c138f665 fix: detect auth by any claude.ai cookie, not specific cookie names 2026-05-11 08:58:02 +02:00
github-actions[bot] aa6c299e2a Beta release v1.2.1-beta.3 2026-05-10 21:41:28 +00:00
SuperDooper ecbae7d7ca chore: release notes for v1.2.1-beta.3 2026-05-10 23:40:31 +02:00
SuperDooper 37a039e1d3 chore: bump to v1.2.1-beta.3 2026-05-10 23:40:30 +02:00
SuperDooper 8f3ead2f65 fix: replace stale 'No API key configured' with correct signed-out message 2026-05-10 23:40:29 +02:00
github-actions[bot] 734670bb3f Beta release v1.2.1-beta.2 2026-05-10 21:29:37 +00:00
SuperDooper 94430e00ba chore: release notes for v1.2.1-beta.2 2026-05-10 23:28:40 +02:00
SuperDooper 7b0368b001 chore: bump to v1.2.1-beta.2 2026-05-10 23:28:39 +02:00
SuperDooper 4629aeffbc fix: load claude.ai instead of /login so already-signed-in users are detected 2026-05-10 23:28:38 +02:00
github-actions[bot] 2cb4ddfe97 Beta release v1.2.1-beta.1 2026-05-10 21:16:08 +00:00
SuperDooper 73b251a1e9 chore: release notes for v1.2.1-beta.1 2026-05-10 23:14:48 +02:00
SuperDooper 7bab6a0df0 chore: bump to v1.2.1-beta.1 2026-05-10 23:14:47 +02:00
SuperDooper db6380fa66 fix: detect sign-in state from cookies on startup, add orgs API fallback for org ID 2026-05-10 23:14:45 +02:00
github-actions[bot] 47a148fa67 Release v1.2.0 2026-05-08 21:20:18 +00:00
SuperDooper 85ac329d36 chore: release notes for v1.2.0 2026-05-08 23:19:24 +02:00
SuperDooper a0fb6eabcf chore: bump to v1.2.0 2026-05-08 23:19:23 +02:00
github-actions[bot] b778d03323 Beta release v1.1.4-beta.7 2026-05-08 21:11:36 +00:00
SuperDooper 13387888b2 chore: release notes for 1.1.4-beta.7 2026-05-08 23:10:35 +02:00
SuperDooper 44be1649a1 chore: bump to 1.1.4-beta.7 2026-05-08 23:10:34 +02:00
SuperDooper 1160712e1b fix: remove ScrollView from main panel so popover auto-sizes to content 2026-05-08 23:10:22 +02:00
github-actions[bot] e4f55c80ee Beta release v1.1.4-beta.6 2026-05-08 21:05:48 +00:00
SuperDooper 526752dc36 chore: release notes for 1.1.4-beta.6 2026-05-08 23:04:54 +02:00
SuperDooper a2d983b555 chore: bump to 1.1.4-beta.6 2026-05-08 23:04:53 +02:00
SuperDooper 0d807c2285 fix: Session Diary — remove Claude icon/header, split stats left/right 2026-05-08 23:04:52 +02:00
SuperDooper 1c42433145 Windows beta release win-v0.0.1-beta.49 2026-05-08 20:46:42 +00:00
SuperDooper c0ac2e5a66 Windows beta release win-v0.0.1-beta.48 2026-05-08 20:39:17 +00:00
SuperDooper 405e65f9f7 Windows beta release win-v0.0.1-beta.47 2026-05-08 20:28:41 +00:00
SuperDooper 1c7433ebea Windows beta release win-v0.0.1-beta.46 2026-05-08 20:19:52 +00:00
SuperDooper a39ef800e8 Windows beta release win-v0.0.1-beta.45 2026-05-08 20:05:58 +00:00
SuperDooper 76dec8b85d Windows beta release win-v0.0.1-beta.44 2026-05-08 20:00:55 +00:00
SuperDooper 2f04f8581c Windows beta release win-v0.0.1-beta.42 2026-05-08 19:41:45 +00:00
SuperDooper 788c066a1e Windows beta release win-v0.0.1-beta.41 2026-05-08 19:39:07 +00:00
SuperDooper 31280d4ab2 Windows beta release win-v0.0.1-beta.40 2026-05-08 19:33:28 +00:00
SuperDooper 7b1c5bbf8e Windows beta release win-v0.0.1-beta.39 2026-05-08 19:19:11 +00:00
SuperDooper f2ba44bbaa Windows beta release win-v0.0.1-beta.38 2026-05-08 19:07:16 +00:00
SuperDooper de1fffe0e3 Windows beta release win-v0.0.1-beta.37 2026-05-08 19:00:02 +00:00
SuperDooper e80f07d1b9 Windows beta release win-v0.0.1-beta.36 2026-05-08 17:40:07 +00:00
github-actions[bot] e9ca8a7efc Beta release v1.1.4-beta.5 2026-05-08 17:32:10 +00:00
SuperDooper 63ef2005a4 Update release notes for 1.1.4-beta.5 2026-05-08 19:31:20 +02:00
SuperDooper 5ad7bb26f0 Bump version to 1.1.4-beta.5 2026-05-08 19:31:18 +02:00
SuperDooper 132a2a8f8d Consolidate bootstrap into single fetchBootstrap() call 2026-05-08 19:31:06 +02:00
github-actions[bot] debd1c1850 Beta release v1.1.4-beta.4 2026-05-08 17:27:59 +00:00
SuperDooper 9bcbb2ae7d Update release notes for 1.1.4-beta.4 2026-05-08 19:26:58 +02:00
SuperDooper c9384f6424 Bump version to 1.1.4-beta.4 2026-05-08 19:26:57 +02:00
SuperDooper e4693fc18f Fix fetchUserEmail return type and capabilities subscript 2026-05-08 19:26:48 +02:00
SuperDooper 84a8592bae Windows beta release win-v0.0.1-beta.35 2026-05-08 17:24:24 +00:00
SuperDooper bdd2038364 Update release notes for 1.1.4-beta.3 2026-05-08 19:22:51 +02:00
SuperDooper fd61face77 Bump version to 1.1.4-beta.3 2026-05-08 19:22:50 +02:00
SuperDooper 1f58a81a26 Parse plan label from bootstrap on every refresh via fetchUserEmail 2026-05-08 19:22:41 +02:00
github-actions[bot] 7f6da51860 Beta release v1.1.4-beta.2 2026-05-08 17:20:51 +00:00
9 changed files with 122 additions and 110 deletions
+8 -11
View File
@@ -40,8 +40,7 @@ struct ContentView: View {
.environmentObject(updater) .environmentObject(updater)
.transition(.move(edge: .trailing).combined(with: .opacity)) .transition(.move(edge: .trailing).combined(with: .opacity))
} else { } else {
ScrollView(.vertical, showsIndicators: false) { VStack(spacing: 0) {
VStack(spacing: 0) {
// Cards grid // Cards grid
if vm.limits.isEmpty { if vm.limits.isEmpty {
EmptyStateView() EmptyStateView()
@@ -96,7 +95,6 @@ struct ContentView: View {
.padding(.bottom, 8) .padding(.bottom, 8)
} }
} }
}
} }
.transition(.opacity) .transition(.opacity)
@@ -398,12 +396,11 @@ struct DiaryRow: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
HStack { HStack {
AgentIconView(agent: .claude, size: 12) Text("\(totalSaved) samples")
Text("Claude") .font(.system(size: 10.5))
.font(.system(size: 12, weight: .medium)) .foregroundColor(.secondary)
.foregroundColor(color)
Spacer() Spacer()
Text("\(totalSaved) samples · Avg Burn Rate: +\(String(format: "%.1f", burnRate))%/h") Text("Avg burn rate: \(String(format: "%.1f", burnRate))%/h")
.font(.system(size: 10.5)) .font(.system(size: 10.5))
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
@@ -914,12 +911,12 @@ struct ErrorBanner: View {
struct EmptyStateView: View { struct EmptyStateView: View {
var body: some View { var body: some View {
VStack(spacing: 10) { VStack(spacing: 10) {
Image(systemName: "key.slash") Image(systemName: "person.crop.circle.badge.questionmark")
.font(.system(size: 28)) .font(.system(size: 28))
.foregroundColor(.secondary) .foregroundColor(.secondary)
Text("No API key configured") Text("Not signed in")
.font(.system(size: 13, weight: .medium)) .font(.system(size: 13, weight: .medium))
Text("Open Settings to add your Anthropic API key.") Text("Sign in to claude.ai to see your usage.")
.font(.system(size: 11.5)) .font(.system(size: 11.5))
.foregroundColor(.secondary) .foregroundColor(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
+2 -2
View File
@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.1.4-beta.2</string> <string>1.2.1-beta.8</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>42</string> <string>56</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>13.0</string> <string>13.0</string>
<key>LSUIElement</key> <key>LSUIElement</key>
+13 -9
View File
@@ -36,15 +36,19 @@ struct LoginWebView: NSViewRepresentable {
if let url = webView.url?.absoluteString, if let url = webView.url?.absoluteString,
url.contains("/login") || url.contains("/auth") { return } url.contains("/login") || url.contains("/auth") { return }
WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in // Ask the WebView itself whether we're authenticated it uses its own
let hasSession = cookies.contains { // session (cookies, localStorage, etc.) so we don't need to know the
$0.domain.contains("claude.ai") && // cookie domain or name.
($0.name == "sessionKey" || $0.name == "__Secure-next-auth.session-token") webView.callAsyncJavaScript(
} "const r = await fetch('/api/bootstrap', {credentials: 'include'}); return r.status;",
guard hasSession, !self.didAuthenticate else { return } arguments: [:], in: nil, in: .defaultClient
self.didAuthenticate = true ) { [weak self] result in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { guard let self, !self.didAuthenticate else { return }
self.onAuthenticated() if case .success(let val) = result, let status = val as? Int, status == 200 {
self.didAuthenticate = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.onAuthenticated()
}
} }
} }
} }
+77 -74
View File
@@ -25,14 +25,12 @@ class UsageViewModel: ObservableObject {
} }
} }
private var cachedOrgId: String?
private var burnHistoryStore: [String: [Double]] = [:] private var burnHistoryStore: [String: [Double]] = [:]
private let maxHistorySamples = 24 private let maxHistorySamples = 24
private var previousPercents: [String: Double] = [:] private var previousPercents: [String: Double] = [:]
private var firedThresholds: [String: Set<Int>] = [:] private var firedThresholds: [String: Set<Int>] = [:]
init() { init() {
cachedOrgId = UserDefaults.standard.string(forKey: "claude_org_id")
let saved = UserDefaults.standard.double(forKey: "refresh_interval") let saved = UserDefaults.standard.double(forKey: "refresh_interval")
refreshInterval = saved > 0 ? saved : 60 refreshInterval = saved > 0 ? saved : 60
showInMenuBar = UserDefaults.standard.object(forKey: "show_in_menubar") as? Bool ?? true showInMenuBar = UserDefaults.standard.object(forKey: "show_in_menubar") as? Bool ?? true
@@ -40,15 +38,20 @@ class UsageViewModel: ObservableObject {
burnHistoryStore = saved burnHistoryStore = saved
} }
loadPlaceholderData() loadPlaceholderData()
Task { await checkInitialSignInState() }
}
private func checkInitialSignInState() async {
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
if !cookies.isEmpty { isSignedIn = true }
} }
func signOut() async { func signOut() async {
let store = WKWebsiteDataStore.default() let store = WKWebsiteDataStore.default()
let types = WKWebsiteDataStore.allWebsiteDataTypes() let types = WKWebsiteDataStore.allWebsiteDataTypes()
let records = await store.dataRecords(ofTypes: types) let records = await store.dataRecords(ofTypes: types)
let claudeRecords = records.filter { $0.displayName.contains("claude.ai") } let claudeRecords = records.filter { $0.displayName.contains("claude.ai") || $0.displayName.contains("anthropic.com") }
await store.removeData(ofTypes: types, for: claudeRecords) await store.removeData(ofTypes: types, for: claudeRecords)
cachedOrgId = nil
UserDefaults.standard.removeObject(forKey: "claude_org_id") UserDefaults.standard.removeObject(forKey: "claude_org_id")
isSignedIn = false isSignedIn = false
isNotAuthenticated = true isNotAuthenticated = true
@@ -67,25 +70,25 @@ class UsageViewModel: ObservableObject {
defer { isLoading = false } defer { isLoading = false }
do { do {
// Fetch org ID dynamically if not cached let (fetchedOrgId, fetchedEmail, fetchedPlan) = try await fetchBootstrap()
if cachedOrgId == nil {
let (fetchedOrgId, fetchedPlan) = try await fetchOrgId() let orgId: String
cachedOrgId = fetchedOrgId if let id = fetchedOrgId {
if let id = cachedOrgId { UserDefaults.standard.set(id, forKey: "claude_org_id")
UserDefaults.standard.set(id, forKey: "claude_org_id") orgId = id
} } else if let cached = UserDefaults.standard.string(forKey: "claude_org_id") {
if let plan = fetchedPlan { planLabel = plan } orgId = cached
} } else {
guard let orgId = cachedOrgId else {
throw AppError.notAuthenticated throw AppError.notAuthenticated
} }
if let email = fetchedEmail { userEmail = email }
if let plan = fetchedPlan { planLabel = plan }
async let usageFetch = fetchUsage(orgId: orgId) async let usageFetch = fetchUsage(orgId: orgId)
async let prepaidFetch = fetchPrepaidCredits(orgId: orgId) async let prepaidFetch = fetchPrepaidCredits(orgId: orgId)
async let overageFetch = fetchOverageSpendLimit(orgId: orgId) async let overageFetch = fetchOverageSpendLimit(orgId: orgId)
async let emailFetch = fetchUserEmail() let (usage, prepaid, overage) = try await (usageFetch, prepaidFetch, overageFetch)
let (usage, prepaid, overage, email) = try await (usageFetch, prepaidFetch, overageFetch, emailFetch)
if let email { userEmail = email }
limits = buildLimits(from: usage) limits = buildLimits(from: usage)
extraUsage = usage.extraUsage extraUsage = usage.extraUsage
prepaidCredits = prepaid prepaidCredits = prepaid
@@ -105,6 +108,7 @@ class UsageViewModel: ObservableObject {
checkLimitNotifications(for: limits) checkLimitNotifications(for: limits)
} catch AppError.notAuthenticated { } catch AppError.notAuthenticated {
isNotAuthenticated = true isNotAuthenticated = true
isSignedIn = false
errorMessage = "Not signed in" errorMessage = "Not signed in"
} catch let error as DecodingError { } catch let error as DecodingError {
switch error { switch error {
@@ -122,65 +126,81 @@ class UsageViewModel: ObservableObject {
} }
} }
// MARK: - Fetch org ID from bootstrap // MARK: - Bootstrap (org ID + email + plan label in one call)
private func fetchOrgId() async throws -> (orgId: String?, planLabel: String?) { private func claudeAPIRequest(for url: URL) async -> URLRequest {
let url = URL(string: "https://claude.ai/api/bootstrap")!
var req = URLRequest(url: url) var req = URLRequest(url: url)
req.setValue("application/json", forHTTPHeaderField: "accept") req.setValue("application/json, text/plain, */*", forHTTPHeaderField: "accept")
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies() req.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent")
let claudeCookies = cookies.filter { $0.domain.contains("claude.ai") } req.setValue("https://claude.ai", forHTTPHeaderField: "Origin")
if claudeCookies.isEmpty { throw AppError.notAuthenticated } req.setValue("https://claude.ai/", forHTTPHeaderField: "Referer")
if let header = HTTPCookie.requestHeaderFields(with: claudeCookies)["Cookie"] { req.setValue("same-origin", forHTTPHeaderField: "sec-fetch-site")
req.setValue(header, forHTTPHeaderField: "Cookie") req.setValue("cors", forHTTPHeaderField: "sec-fetch-mode")
req.setValue("empty", forHTTPHeaderField: "sec-fetch-dest")
if let cookie = await claudeCookieHeader() {
req.setValue(cookie, forHTTPHeaderField: "Cookie")
} }
return req
}
private func fetchBootstrap() async throws -> (orgId: String?, email: String?, planLabel: String?) {
let url = URL(string: "https://claude.ai/api/bootstrap")!
var req = await claudeAPIRequest(for: url)
guard req.value(forHTTPHeaderField: "Cookie") != nil else { throw AppError.notAuthenticated }
let (data, response) = try await URLSession.shared.data(for: req) let (data, response) = try await URLSession.shared.data(for: req)
guard let http = response as? HTTPURLResponse else { throw AppError.networkError } guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
if http.statusCode == 401 || http.statusCode == 403 { throw AppError.notAuthenticated } if http.statusCode == 401 || http.statusCode == 403 { throw AppError.notAuthenticated }
guard http.statusCode == 200 else { throw AppError.networkError } guard http.statusCode == 200 else { throw AppError.networkError }
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return (nil, nil) } guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return (nil, nil, nil)
}
func planFromOrg(_ org: [String: Any]?) -> String? { let account = json["account"] as? [String: Any]
guard let caps = org?["capabilities"] as? [String] else { return nil } // memberships may live under account or at root (older API shape)
guard let cap = caps.first(where: { $0.hasPrefix("claude_") }) else { return nil } let memberships = (account?["memberships"] ?? json["memberships"]) as? [[String: Any]]
let firstOrg = memberships?.first?["organization"] as? [String: Any]
// org ID primary path then flat-list fallback then dedicated endpoint
var orgId: String? = firstOrg?["uuid"] as? String
if orgId == nil {
orgId = (json["organizations"] as? [[String: Any]])?.first?["uuid"] as? String
}
if orgId == nil {
// Final fallback: fetch /api/organizations directly
let orgsReq = await claudeAPIRequest(for: URL(string: "https://claude.ai/api/organizations")!)
if let (orgsData, orgsResp) = try? await URLSession.shared.data(for: orgsReq),
let orgsHttp = orgsResp as? HTTPURLResponse, orgsHttp.statusCode == 200,
let orgs = try? JSONSerialization.jsonObject(with: orgsData) as? [[String: Any]] {
orgId = orgs.first?["uuid"] as? String
}
}
let email = account?["email_address"] as? String
// plan label from capabilities e.g. "claude_pro" -> "Pro"
var planLabel: String? = nil
if let caps = firstOrg?["capabilities"] as? [String],
let cap = caps.first(where: { $0.hasPrefix("claude_") }) {
let name = String(cap.dropFirst("claude_".count)) let name = String(cap.dropFirst("claude_".count))
return name.prefix(1).uppercased() + name.dropFirst().lowercased() planLabel = name.prefix(1).uppercased() + name.dropFirst().lowercased()
} }
// account.memberships[0].organization.uuid return (orgId, email, planLabel)
if let account = json["account"] as? [String: Any],
let memberships = account["memberships"] as? [[String: Any]],
let org = memberships.first?["organization"] as? [String: Any],
let uuid = org["uuid"] as? String {
return (uuid, planFromOrg(org))
}
// root memberships (older API shape)
if let memberships = json["memberships"] as? [[String: Any]],
let org = memberships.first?["organization"] as? [String: Any],
let uuid = org["uuid"] as? String {
return (uuid, planFromOrg(org))
}
// root organizations array
if let orgs = json["organizations"] as? [[String: Any]],
let uuid = orgs.first?["uuid"] as? String {
return (uuid, planFromOrg(orgs.first))
}
return (nil, nil)
} }
// MARK: - Fetch usage // MARK: - Fetch usage
private func claudeCookieHeader() async -> String? { private func claudeCookieHeader() async -> String? {
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies() let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
let claudeCookies = cookies.filter { $0.domain.contains("claude.ai") } guard !cookies.isEmpty else { return nil }
return HTTPCookie.requestHeaderFields(with: claudeCookies)["Cookie"] // Send all cookies from the app's WebView store the session token may be
// on any domain (claude.ai, anthropic.com, or an auth sub-service).
return HTTPCookie.requestHeaderFields(with: cookies)["Cookie"]
} }
private func fetchUsage(orgId: String) async throws -> UsageResponse { private func fetchUsage(orgId: String) async throws -> UsageResponse {
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/usage")! let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/usage")!
var req = URLRequest(url: url) let req = await claudeAPIRequest(for: url)
req.setValue("application/json", forHTTPHeaderField: "accept")
if let cookie = await claudeCookieHeader() { req.setValue(cookie, forHTTPHeaderField: "Cookie") }
let (data, response) = try await URLSession.shared.data(for: req) let (data, response) = try await URLSession.shared.data(for: req)
guard let http = response as? HTTPURLResponse else { throw AppError.networkError } guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
if http.statusCode == 401 || http.statusCode == 403 { throw AppError.notAuthenticated } if http.statusCode == 401 || http.statusCode == 403 { throw AppError.notAuthenticated }
@@ -188,24 +208,9 @@ class UsageViewModel: ObservableObject {
return try JSONDecoder().decode(UsageResponse.self, from: data) return try JSONDecoder().decode(UsageResponse.self, from: data)
} }
private func fetchUserEmail() async throws -> String? {
let url = URL(string: "https://claude.ai/api/bootstrap")!
var req = URLRequest(url: url)
req.setValue("application/json", forHTTPHeaderField: "accept")
if let cookie = await claudeCookieHeader() { req.setValue(cookie, forHTTPHeaderField: "Cookie") }
let (data, response) = try await URLSession.shared.data(for: req)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let account = json["account"] as? [String: Any],
let email = account["email_address"] as? String else { return nil }
return email
}
private func fetchPrepaidCredits(orgId: String) async throws -> PrepaidCredits? { private func fetchPrepaidCredits(orgId: String) async throws -> PrepaidCredits? {
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/prepaid/credits")! let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/prepaid/credits")!
var req = URLRequest(url: url) let req = await claudeAPIRequest(for: url)
req.setValue("application/json", forHTTPHeaderField: "accept")
if let cookie = await claudeCookieHeader() { req.setValue(cookie, forHTTPHeaderField: "Cookie") }
let (data, response) = try await URLSession.shared.data(for: req) let (data, response) = try await URLSession.shared.data(for: req)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil } guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
return try? JSONDecoder().decode(PrepaidCredits.self, from: data) return try? JSONDecoder().decode(PrepaidCredits.self, from: data)
@@ -213,9 +218,7 @@ class UsageViewModel: ObservableObject {
private func fetchOverageSpendLimit(orgId: String) async throws -> OverageSpendLimit? { private func fetchOverageSpendLimit(orgId: String) async throws -> OverageSpendLimit? {
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/overage_spend_limit")! let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/overage_spend_limit")!
var req = URLRequest(url: url) let req = await claudeAPIRequest(for: url)
req.setValue("application/json", forHTTPHeaderField: "accept")
if let cookie = await claudeCookieHeader() { req.setValue(cookie, forHTTPHeaderField: "Cookie") }
let (data, response) = try await URLSession.shared.data(for: req) let (data, response) = try await URLSession.shared.data(for: req)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil } guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
return try? JSONDecoder().decode(OverageSpendLimit.self, from: data) return try? JSONDecoder().decode(OverageSpendLimit.self, from: data)
+2 -2
View File
@@ -8,9 +8,9 @@
[![macOS](https://img.shields.io/badge/macOS-13.0+-000000?style=flat&logo=apple&logoColor=white)](https://www.apple.com/macos/) [![macOS](https://img.shields.io/badge/macOS-13.0+-000000?style=flat&logo=apple&logoColor=white)](https://www.apple.com/macos/)
[![Swift](https://img.shields.io/badge/Swift-5.9-F05138?style=flat&logo=swift&logoColor=white)](https://swift.org) [![Swift](https://img.shields.io/badge/Swift-5.9-F05138?style=flat&logo=swift&logoColor=white)](https://swift.org)
[![Version](https://img.shields.io/badge/version-1.1.3-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases) [![Version](https://img.shields.io/badge/version-1.2.0-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases)
[![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE) [![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE)
[![Beta](https://img.shields.io/badge/beta-1.1.4--beta.1-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases/tag/v1.1.4-beta.1) <!-- BETA_BADGE --> [![Beta](https://img.shields.io/badge/beta-1.2.1--beta.7-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases/tag/v1.2.1-beta.7) <!-- BETA_BADGE -->
</div> </div>
+11 -3
View File
@@ -1,4 +1,12 @@
## What's new in v1.1.4-beta.2 ## What's new in v1.2.1
### Improvements ### Bug fixes
- Plan name (e.g. Pro, Max) is now read from the API instead of being hardcoded - Fixed "Not signed in" showing incorrectly on launch when the session was already active
- Sign-in state is now detected immediately from stored cookies on startup, before the first data refresh completes
- Fixed login window auto-closing before the user could sign in — the login window now correctly loads the `/login` page so it only detects auth after the actual sign-in redirect
- Fixed "No API key configured" showing after signing out — now correctly shows "Not signed in" with a prompt to sign in
- Added `/api/organizations` as a final fallback for org ID resolution when the bootstrap API response doesn't include it
- Fixed usage data not loading — API requests now include required browser-like headers (Origin, Referer, User-Agent)
- Fixed Settings incorrectly showing "Signed in" after a failed refresh — sign-in state now resets when authentication fails
- Rewrote login detection to use the WebView's own fetch call instead of inspecting cookie domains — correctly detects auth regardless of which domain the session token is stored on
- Fixed API requests not including session cookies — now sends all cookies from the app's WebView store rather than filtering by domain
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"version": "1.1.4-beta.1", "version": "1.2.1-beta.7",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.1.4-beta.1/ClaudeChecker.zip", "url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.7/ClaudeChecker.zip",
"notes": "## What's new in v1.1.4-beta.1\n\n### Bug fix\n- Org ID is now fetched dynamically from the bootstrap API instead of being hardcoded — the app now works for all users, not just the developer's account" "notes": "## What's new in v1.2.1\n\n### Bug fixes\n- Fixed \"Not signed in\" showing incorrectly on launch when the session was already active\n- Sign-in state is now detected immediately from stored cookies on startup, before the first data refresh completes\n- Fixed login window auto-closing before the user could sign in — the login window now correctly loads the `/login` page so it only detects auth after the actual sign-in redirect\n- Fixed \"No API key configured\" showing after signing out — now correctly shows \"Not signed in\" with a prompt to sign in\n- Added `/api/organizations` as a final fallback for org ID resolution when the bootstrap API response doesn't include it\n- Fixed usage data not loading — API requests now include required browser-like headers (Origin, Referer, User-Agent)\n- Fixed sign-in detection and cookie handling for accounts whose session cookies are on the `anthropic.com` domain rather than `claude.ai`\n- Fixed Settings incorrectly showing \"Signed in\" after a failed refresh — sign-in state now resets when authentication fails"
} }
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"version": "0.0.1-beta.33", "version": "0.0.1-beta.49",
"url": "https://github.com/superdooper86/claudechecker/releases/download/win-v0.0.1-beta.33/ClaudeChecker-Windows.zip", "url": "https://github.com/superdooper86/claudechecker/releases/download/win-v0.0.1-beta.49/ClaudeChecker-Windows.zip",
"notes": "## What's new in beta.33\r\n\r\n- Fix: gauge percentage text is now vertically centered within the dial\r\n- Remove: debug banner no longer appears on the main panel" "notes": "## What's new in beta.36\r\n\r\n### Bug fixes\r\n- Settings no longer incorrectly shows \"Not signed in\" when limits are working\r\n- Session Diary now shows correctly after the first successful refresh\r\n- Extra Usage Credits section now restored from cache on startup\r\n- Plan name, overage, and prepaid credits are now cached and shown immediately on launch\r\n- App now loads cached state instantly on startup before the background refresh completes"
} }
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"version": "1.1.3", "version": "1.2.0",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.1.3/ClaudeChecker.zip", "url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.0/ClaudeChecker.zip",
"notes": "## What's new in v1.1.3\n\n### UI polish\n- Limit headers now read \"5 Hour Limit\" and \"7 Day Limit\"\n- Tapping anywhere on the blue update banner opens the update window\n\n### Session Diary fix\n- Burn history is persisted across app launches — the sparkline populates immediately on first open instead of requiring a manual refresh\n\n### Window sizing fixes\n- The popover now resizes correctly when switching between the main view and settings\n- Background colour no longer shows through during the resize animation" "notes": "## What's new in v1.2.0\n\n### Bug fixes\n- App now works for all users — org ID is fetched dynamically from the API instead of being hardcoded\n- Plan name (e.g. Pro, Max) now updates correctly on every refresh\n\n### Improvements\n- Plan name is read from the API rather than hardcoded\n- Bootstrap API call consolidated — org ID, email, and plan name fetched in a single request per refresh\n- Main panel no longer scrolls — popover auto-sizes to fit content\n- Session Diary card redesigned — sample count and avg burn rate shown left/right above the sparkline, Claude header removed"
} }