Compare commits
10
Commits
v1.2.1-beta.23
...
v1.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a338f4e5fe | ||
|
|
88e7d684c0 | ||
|
|
9af0d6401c | ||
|
|
7cb2159b4c | ||
|
|
206e97780d | ||
|
|
2b79ba4ae0 | ||
|
|
60b6c60c0a | ||
|
|
8140831236 | ||
|
|
9ed8916075 | ||
|
|
2ab2d913ce |
@@ -18,16 +18,43 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
var updateManager = UpdateManager()
|
var updateManager = UpdateManager()
|
||||||
var refreshTimer: Timer?
|
var refreshTimer: Timer?
|
||||||
var cookiePrimerView: WKWebView?
|
var cookiePrimerView: WKWebView?
|
||||||
|
var cookiePrimerWindow: NSWindow?
|
||||||
|
var primerNavDelegate: PrimerNavDelegate?
|
||||||
var cancellables = Set<AnyCancellable>()
|
var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
NSApp.setActivationPolicy(.accessory)
|
NSApp.setActivationPolicy(.accessory)
|
||||||
|
|
||||||
// Hidden WKWebView to prime the shared cookie store
|
// Background WebView that loads claude.ai on every launch.
|
||||||
|
// Anchored in a real (off-screen, invisible) NSWindow so WebKit doesn't throttle
|
||||||
|
// JS execution. When the page finishes loading the nav delegate calls
|
||||||
|
// adoptPrimerWebView, which sets it as the API WebView and runs the first fetch.
|
||||||
let config = WKWebViewConfiguration()
|
let config = WKWebViewConfiguration()
|
||||||
config.websiteDataStore = WKWebsiteDataStore.default()
|
config.websiteDataStore = WKWebsiteDataStore.default()
|
||||||
cookiePrimerView = WKWebView(frame: .zero, configuration: config)
|
let wv = WKWebView(frame: CGRect(x: 0, y: 0, width: 1, height: 1), configuration: config)
|
||||||
cookiePrimerView?.load(URLRequest(url: URL(string: "https://claude.ai")!))
|
let win = NSWindow(
|
||||||
|
contentRect: NSRect(x: -9999, y: -9999, width: 1, height: 1),
|
||||||
|
styleMask: [],
|
||||||
|
backing: .buffered,
|
||||||
|
defer: false
|
||||||
|
)
|
||||||
|
win.contentView = wv
|
||||||
|
win.alphaValue = 0
|
||||||
|
win.orderFront(nil)
|
||||||
|
cookiePrimerWindow = win
|
||||||
|
cookiePrimerView = wv
|
||||||
|
|
||||||
|
let delegate = PrimerNavDelegate { [weak self] in
|
||||||
|
guard let self, let wv = self.cookiePrimerView else { return }
|
||||||
|
Task { @MainActor [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
await self.usageViewModel.adoptPrimerWebView(wv)
|
||||||
|
await self.updateManager.checkForUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
primerNavDelegate = delegate
|
||||||
|
wv.navigationDelegate = delegate
|
||||||
|
wv.load(URLRequest(url: URL(string: "https://claude.ai")!))
|
||||||
|
|
||||||
// Status item
|
// Status item
|
||||||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||||
@@ -134,12 +161,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial fetch after cookie primer + update check
|
|
||||||
Task {
|
|
||||||
try? await Task.sleep(nanoseconds: 1_500_000_000)
|
|
||||||
await usageViewModel.refresh()
|
|
||||||
await updateManager.checkForUpdates()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setMenubarIcon(button: NSStatusBarButton) {
|
private func setMenubarIcon(button: NSStatusBarButton) {
|
||||||
@@ -240,4 +261,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One-shot WKNavigationDelegate: fires onDone on first finish or error, then goes silent.
|
||||||
|
final class PrimerNavDelegate: NSObject, WKNavigationDelegate {
|
||||||
|
private var done = false
|
||||||
|
private let onDone: () -> Void
|
||||||
|
init(_ onDone: @escaping () -> Void) { self.onDone = onDone }
|
||||||
|
private func finish() { guard !done else { return }; done = true; onDone() }
|
||||||
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { finish() }
|
||||||
|
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { finish() }
|
||||||
|
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { finish() }
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ struct DiagnosticsView: View {
|
|||||||
DiagRow("Signed in", vm.isSignedIn ? "Yes" : "No")
|
DiagRow("Signed in", vm.isSignedIn ? "Yes" : "No")
|
||||||
DiagRow("Email", vm.userEmail.isEmpty ? "(none)" : vm.userEmail)
|
DiagRow("Email", vm.userEmail.isEmpty ? "(none)" : vm.userEmail)
|
||||||
DiagRow("Org ID", UserDefaults.standard.string(forKey: "claude_org_id") ?? "(none)")
|
DiagRow("Org ID", UserDefaults.standard.string(forKey: "claude_org_id") ?? "(none)")
|
||||||
|
DiagRow("lastActiveOrg", vm.diagLastActiveOrg.isEmpty ? "(not read)" : vm.diagLastActiveOrg)
|
||||||
DiagRow("Error", vm.errorMessage ?? "(none)")
|
DiagRow("Error", vm.errorMessage ?? "(none)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,21 +66,6 @@ struct DiagnosticsView: View {
|
|||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
// Response body
|
|
||||||
if !vm.diagLastBody.isEmpty {
|
|
||||||
DiagSection(title: "Response Body (first 500 chars)") {
|
|
||||||
Text(vm.diagLastBody)
|
|
||||||
.font(.system(size: 10, design: .monospaced))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.textSelection(.enabled)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(8)
|
|
||||||
.background(Color.primary.opacity(0.04))
|
|
||||||
.cornerRadius(5)
|
|
||||||
}
|
|
||||||
Divider()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cookie summary
|
// Cookie summary
|
||||||
DiagSection(title: "Cookie Store") {
|
DiagSection(title: "Cookie Store") {
|
||||||
DiagRow("Total cookies", "\(vm.diagCookieCount)")
|
DiagRow("Total cookies", "\(vm.diagCookieCount)")
|
||||||
@@ -164,6 +150,7 @@ struct DiagnosticsView: View {
|
|||||||
lines.append("Signed in: \(vm.isSignedIn ? "Yes" : "No")")
|
lines.append("Signed in: \(vm.isSignedIn ? "Yes" : "No")")
|
||||||
lines.append("Email: \(vm.userEmail.isEmpty ? "(none)" : vm.userEmail)")
|
lines.append("Email: \(vm.userEmail.isEmpty ? "(none)" : vm.userEmail)")
|
||||||
lines.append("Org ID: \(UserDefaults.standard.string(forKey: "claude_org_id") ?? "(none)")")
|
lines.append("Org ID: \(UserDefaults.standard.string(forKey: "claude_org_id") ?? "(none)")")
|
||||||
|
lines.append("lastActiveOrg: \(vm.diagLastActiveOrg.isEmpty ? "(not read)" : vm.diagLastActiveOrg)")
|
||||||
lines.append("Error: \(vm.errorMessage ?? "(none)")")
|
lines.append("Error: \(vm.errorMessage ?? "(none)")")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Last path: \(vm.diagLastPath)")
|
lines.append("Last path: \(vm.diagLastPath)")
|
||||||
@@ -173,8 +160,6 @@ struct DiagnosticsView: View {
|
|||||||
lines.append("Claude cookies: \(vm.diagClaudeCookieCount)")
|
lines.append("Claude cookies: \(vm.diagClaudeCookieCount)")
|
||||||
lines.append("Domains: \(vm.diagCookieDomains.joined(separator: ", "))")
|
lines.append("Domains: \(vm.diagCookieDomains.joined(separator: ", "))")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Response body:")
|
|
||||||
lines.append(vm.diagLastBody)
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Live cookies:")
|
lines.append("Live cookies:")
|
||||||
for c in liveAllCookies {
|
for c in liveAllCookies {
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.2.1-beta.23</string>
|
<string>1.3.2</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>71</string>
|
<string>76</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>13.0</string>
|
<string>13.0</string>
|
||||||
<key>LSUIElement</key>
|
<key>LSUIElement</key>
|
||||||
|
|||||||
@@ -32,16 +32,15 @@ class UsageViewModel: ObservableObject {
|
|||||||
|
|
||||||
// WebView used for JS-based API calls (avoids URLSession 403 fingerprinting issues)
|
// WebView used for JS-based API calls (avoids URLSession 403 fingerprinting issues)
|
||||||
private var apiWebView: WKWebView?
|
private var apiWebView: WKWebView?
|
||||||
private var navWaiter: WKNavWaiter?
|
|
||||||
|
|
||||||
// Diagnostics — populated on every urlFetch call
|
// Diagnostics
|
||||||
@Published var diagCookieCount: Int = 0
|
@Published var diagCookieCount: Int = 0
|
||||||
@Published var diagClaudeCookieCount: Int = 0
|
@Published var diagClaudeCookieCount: Int = 0
|
||||||
@Published var diagCookieDomains: [String] = []
|
@Published var diagCookieDomains: [String] = []
|
||||||
@Published var diagLastPath: String = ""
|
@Published var diagLastPath: String = ""
|
||||||
@Published var diagLastStatus: Int = 0
|
@Published var diagLastStatus: Int = 0
|
||||||
@Published var diagLastBody: String = ""
|
|
||||||
@Published var diagLastError: String = ""
|
@Published var diagLastError: String = ""
|
||||||
|
@Published var diagLastActiveOrg: String = "" // lastActiveOrg cookie value from JS
|
||||||
@Published var diagLastFetch: Date? = nil
|
@Published var diagLastFetch: Date? = nil
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@@ -55,38 +54,24 @@ class UsageViewModel: ObservableObject {
|
|||||||
Task { await checkInitialSignInState() }
|
Task { await checkInitialSignInState() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called after login: store the webview (already on claude.ai) so jsFetch can use it.
|
// Called by AppDelegate once the primer WebView has loaded claude.ai.
|
||||||
|
// Sets it as the API WebView and triggers the first data fetch.
|
||||||
|
func adoptPrimerWebView(_ wv: WKWebView) async {
|
||||||
|
apiWebView = wv
|
||||||
|
await refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called after login: store the login WebView (already on claude.ai) so jsFetch can use it.
|
||||||
func adoptAndRefresh(_ loginWebView: WKWebView) async {
|
func adoptAndRefresh(_ loginWebView: WKWebView) async {
|
||||||
for _ in 0..<30 {
|
for _ in 0..<30 {
|
||||||
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
|
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
|
||||||
if cookies.contains(where: { $0.domain.contains("claude.ai") }) { break }
|
if cookies.contains(where: { $0.domain.contains("claude.ai") }) { break }
|
||||||
try? await Task.sleep(nanoseconds: 300_000_000)
|
try? await Task.sleep(nanoseconds: 300_000_000)
|
||||||
}
|
}
|
||||||
if loginWebView.url?.host?.hasSuffix("claude.ai") == true {
|
apiWebView = loginWebView
|
||||||
apiWebView = loginWebView
|
|
||||||
} else {
|
|
||||||
await prepareApiWebView()
|
|
||||||
}
|
|
||||||
await refresh()
|
await refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates (or reuses) a background WKWebView navigated to claude.ai for JS fetch.
|
|
||||||
private func prepareApiWebView() async {
|
|
||||||
if let wv = apiWebView, wv.url?.host?.hasSuffix("claude.ai") == true { return }
|
|
||||||
let config = WKWebViewConfiguration()
|
|
||||||
config.websiteDataStore = WKWebsiteDataStore.default()
|
|
||||||
let wv = WKWebView(frame: CGRect(x: 0, y: 0, width: 1, height: 1), configuration: config)
|
|
||||||
apiWebView = wv
|
|
||||||
await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
|
|
||||||
let waiter = WKNavWaiter { cont.resume() }
|
|
||||||
self.navWaiter = waiter
|
|
||||||
wv.navigationDelegate = waiter
|
|
||||||
wv.load(URLRequest(url: URL(string: "https://claude.ai/")!))
|
|
||||||
}
|
|
||||||
wv.navigationDelegate = nil
|
|
||||||
navWaiter = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs a fetch() inside the live claude.ai WebView — same browser context as the frontend,
|
// Runs a fetch() inside the live claude.ai WebView — same browser context as the frontend,
|
||||||
// so no CORS/fingerprinting issues that URLSession hits.
|
// so no CORS/fingerprinting issues that URLSession hits.
|
||||||
private func jsFetch(_ path: String) async throws -> (Int, String) {
|
private func jsFetch(_ path: String) async throws -> (Int, String) {
|
||||||
@@ -115,7 +100,6 @@ class UsageViewModel: ObservableObject {
|
|||||||
diagLastPath = path
|
diagLastPath = path
|
||||||
diagLastFetch = Date()
|
diagLastFetch = Date()
|
||||||
diagLastStatus = status
|
diagLastStatus = status
|
||||||
diagLastBody = String(body.prefix(500))
|
|
||||||
diagLastError = status != 200 ? "JS HTTP \(status)" : ""
|
diagLastError = status != 200 ? "JS HTTP \(status)" : ""
|
||||||
return (status, body)
|
return (status, body)
|
||||||
}
|
}
|
||||||
@@ -179,7 +163,6 @@ class UsageViewModel: ObservableObject {
|
|||||||
guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
|
guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
|
||||||
let body = String(data: data, encoding: .utf8) ?? ""
|
let body = String(data: data, encoding: .utf8) ?? ""
|
||||||
diagLastStatus = http.statusCode
|
diagLastStatus = http.statusCode
|
||||||
diagLastBody = String(body.prefix(500))
|
|
||||||
if http.statusCode != 200 {
|
if http.statusCode != 200 {
|
||||||
diagLastError = "HTTP \(http.statusCode)"
|
diagLastError = "HTTP \(http.statusCode)"
|
||||||
}
|
}
|
||||||
@@ -196,8 +179,8 @@ class UsageViewModel: ObservableObject {
|
|||||||
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
|
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
|
||||||
guard cookies.contains(where: { $0.domain.contains("claude.ai") }) else { return }
|
guard cookies.contains(where: { $0.domain.contains("claude.ai") }) else { return }
|
||||||
isSignedIn = true
|
isSignedIn = true
|
||||||
await prepareApiWebView()
|
// AppDelegate's primer WebView will call adoptPrimerWebView once it loads,
|
||||||
await refresh()
|
// which triggers the first real data fetch.
|
||||||
}
|
}
|
||||||
|
|
||||||
func signOut() async {
|
func signOut() async {
|
||||||
@@ -225,10 +208,31 @@ class UsageViewModel: ObservableObject {
|
|||||||
defer { isLoading = false }
|
defer { isLoading = false }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let (fetchedOrgId, fetchedEmail, fetchedPlan) = try await fetchBootstrap()
|
// Read lastActiveOrg cookie from JS — most reliable org source since it's
|
||||||
|
// set by the claude.ai frontend to whichever org is currently active.
|
||||||
|
var cookieOrgId: String? = nil
|
||||||
|
if let wv = apiWebView, wv.url?.host?.hasSuffix("claude.ai") == true {
|
||||||
|
let js = """
|
||||||
|
return document.cookie.split(';')
|
||||||
|
.map(c => c.trim().split('='))
|
||||||
|
.filter(p => p[0] === 'lastActiveOrg')
|
||||||
|
.map(p => p.slice(1).join('='))[0] || null;
|
||||||
|
"""
|
||||||
|
let raw: Any? = try? await withCheckedThrowingContinuation { cont in
|
||||||
|
wv.callAsyncJavaScript(js, arguments: [:], in: nil, in: .defaultClient) { r in
|
||||||
|
cont.resume(returning: (try? r.get()) ?? nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let v = raw as? String, !v.isEmpty {
|
||||||
|
cookieOrgId = v
|
||||||
|
diagLastActiveOrg = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (fetchedOrgId, fetchedEmail, fetchedPlan) = try await fetchBootstrap(preferredOrgId: cookieOrgId)
|
||||||
|
|
||||||
let orgId: String
|
let orgId: String
|
||||||
if let id = fetchedOrgId {
|
if let id = cookieOrgId ?? fetchedOrgId {
|
||||||
UserDefaults.standard.set(id, forKey: "claude_org_id")
|
UserDefaults.standard.set(id, forKey: "claude_org_id")
|
||||||
orgId = id
|
orgId = id
|
||||||
} else if let cached = UserDefaults.standard.string(forKey: "claude_org_id") {
|
} else if let cached = UserDefaults.standard.string(forKey: "claude_org_id") {
|
||||||
@@ -283,7 +287,7 @@ class UsageViewModel: ObservableObject {
|
|||||||
|
|
||||||
// MARK: - Bootstrap
|
// MARK: - Bootstrap
|
||||||
|
|
||||||
private func fetchBootstrap() async throws -> (orgId: String?, email: String?, planLabel: String?) {
|
private func fetchBootstrap(preferredOrgId: String? = nil) async throws -> (orgId: String?, email: String?, planLabel: String?) {
|
||||||
let (status, body) = try await apiFetch("/api/bootstrap")
|
let (status, body) = try await apiFetch("/api/bootstrap")
|
||||||
if status == 401 || status == 403 {
|
if status == 401 || status == 403 {
|
||||||
throw AppError.detail("HTTP \(status) — \(body.prefix(120))")
|
throw AppError.detail("HTTP \(status) — \(body.prefix(120))")
|
||||||
@@ -299,7 +303,8 @@ class UsageViewModel: ObservableObject {
|
|||||||
|
|
||||||
let account = json["account"] as? [String: Any]
|
let account = json["account"] as? [String: Any]
|
||||||
let memberships = (account?["memberships"] ?? json["memberships"]) as? [[String: Any]]
|
let memberships = (account?["memberships"] ?? json["memberships"]) as? [[String: Any]]
|
||||||
let firstOrg = memberships?.first?["organization"] as? [String: Any]
|
let allOrgs = memberships?.compactMap { $0["organization"] as? [String: Any] } ?? []
|
||||||
|
let firstOrg = allOrgs.first
|
||||||
|
|
||||||
var orgId: String? = firstOrg?["uuid"] as? String
|
var orgId: String? = firstOrg?["uuid"] as? String
|
||||||
if orgId == nil {
|
if orgId == nil {
|
||||||
@@ -316,8 +321,10 @@ class UsageViewModel: ObservableObject {
|
|||||||
|
|
||||||
let email = account?["email_address"] as? String
|
let email = account?["email_address"] as? String
|
||||||
|
|
||||||
|
// Read capabilities from the preferred (active) org, falling back to first org.
|
||||||
|
let capOrg = preferredOrgId.flatMap { id in allOrgs.first(where: { $0["uuid"] as? String == id }) } ?? firstOrg
|
||||||
var planLabel: String? = nil
|
var planLabel: String? = nil
|
||||||
if let caps = firstOrg?["capabilities"] as? [String],
|
if let caps = capOrg?["capabilities"] as? [String],
|
||||||
let cap = caps.first(where: { $0.hasPrefix("claude_") }) {
|
let cap = caps.first(where: { $0.hasPrefix("claude_") }) {
|
||||||
let name = String(cap.dropFirst("claude_".count))
|
let name = String(cap.dropFirst("claude_".count))
|
||||||
planLabel = name.prefix(1).uppercased() + name.dropFirst().lowercased()
|
planLabel = name.prefix(1).uppercased() + name.dropFirst().lowercased()
|
||||||
@@ -464,16 +471,6 @@ class UsageViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Navigation delegate that resolves a continuation on first finish/error (idempotent).
|
|
||||||
private class WKNavWaiter: NSObject, WKNavigationDelegate {
|
|
||||||
private var done = false
|
|
||||||
private let onDone: () -> Void
|
|
||||||
init(_ onDone: @escaping () -> Void) { self.onDone = onDone }
|
|
||||||
private func finish() { guard !done else { return }; done = true; onDone() }
|
|
||||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { finish() }
|
|
||||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { finish() }
|
|
||||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { finish() }
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AppError: LocalizedError {
|
enum AppError: LocalizedError {
|
||||||
case notAuthenticated
|
case notAuthenticated
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
[](https://www.apple.com/macos/)
|
[](https://www.apple.com/macos/)
|
||||||
[](https://swift.org)
|
[](https://swift.org)
|
||||||
[](https://github.com/superdooper86/claudechecker/releases)
|
[](https://github.com/superdooper86/claudechecker/releases)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/superdooper86/claudechecker/releases/tag/v1.2.1-beta.22) <!-- BETA_BADGE -->
|
<!-- BETA_BADGE -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+3
-19
@@ -1,20 +1,4 @@
|
|||||||
## What's new in v1.2.1-beta.18
|
## What's new in v1.3.2
|
||||||
|
|
||||||
### Bug fixes
|
### Reliability
|
||||||
- Replaced callAsyncJavaScript fetch approach with WebKit navigation: instead of running `fetch()` in the page's JS context (which was returning 401 because it bypasses the SPA's auth interceptors), each API call now navigates the WebView to the API URL directly. WebKit sends full browser headers and cookies automatically at the HTTP layer, the same way a real browser navigation works. This is more reliable regardless of what server-side auth mechanism claude.ai uses.
|
- Data now loads automatically on every app launch without requiring manual re-authentication. The background WebView is anchored in a hidden window so macOS no longer throttles its JavaScript execution.
|
||||||
- API calls are now sequential to share a single WebView for all navigations
|
|
||||||
- Added redirect detection: if WebKit follows a 302 to /login, the response is treated as an auth failure rather than returning HTML to the JSON parser
|
|
||||||
|
|
||||||
## What's new in v1.2.1-beta.17
|
|
||||||
|
|
||||||
### Bug fixes
|
|
||||||
- Fixed "Not signed in" after login by adopting the login WebView directly for API calls
|
|
||||||
- Added diagnostic error messages for JS errors and unexpected responses
|
|
||||||
|
|
||||||
## What's new in v1.2.1
|
|
||||||
|
|
||||||
### Bug fixes
|
|
||||||
- Fixed WebKit suspending the background WKWebView: anchored in a transparent 1×1 NSWindow
|
|
||||||
- Fixed login window auto-closing before sign-in completes
|
|
||||||
- Added /api/organizations as a final fallback for org ID resolution
|
|
||||||
- Fixed Settings incorrectly showing "Signed in" after a failed refresh
|
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.2.1-beta.22",
|
"version": "1.2.1-beta.25",
|
||||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.22/ClaudeChecker.zip",
|
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.25/ClaudeChecker.zip",
|
||||||
"notes": "## What's new in v1.2.1-beta.18\n\n### Bug fixes\n- Replaced callAsyncJavaScript fetch approach with WebKit navigation: instead of running `fetch()` in the page's JS context (which was returning 401 because it bypasses the SPA's auth interceptors), each API call now navigates the WebView to the API URL directly. WebKit sends full browser headers and cookies automatically at the HTTP layer, the same way a real browser navigation works. This is more reliable regardless of what server-side auth mechanism claude.ai uses.\n- API calls are now sequential to share a single WebView for all navigations\n- Added redirect detection: if WebKit follows a 302 to /login, the response is treated as an auth failure rather than returning HTML to the JSON parser\n\n## What's new in v1.2.1-beta.17\n\n### Bug fixes\n- Fixed \"Not signed in\" after login by adopting the login WebView directly for API calls\n- Added diagnostic error messages for JS errors and unexpected responses\n\n## What's new in v1.2.1\n\n### Bug fixes\n- Fixed WebKit suspending the background WKWebView: anchored in a transparent 1×1 NSWindow\n- Fixed login window auto-closing before sign-in completes\n- Added /api/organizations as a final fallback for org ID resolution\n- Fixed Settings incorrectly showing \"Signed in\" after a failed refresh"
|
"notes": "## What's new in v1.2.1-beta.18\n\n### Bug fixes\n- Replaced callAsyncJavaScript fetch approach with WebKit navigation: instead of running `fetch()` in the page's JS context (which was returning 401 because it bypasses the SPA's auth interceptors), each API call now navigates the WebView to the API URL directly. WebKit sends full browser headers and cookies automatically at the HTTP layer, the same way a real browser navigation works. This is more reliable regardless of what server-side auth mechanism claude.ai uses.\n- API calls are now sequential to share a single WebView for all navigations\n- Added redirect detection: if WebKit follows a 302 to /login, the response is treated as an auth failure rather than returning HTML to the JSON parser\n\n## What's new in v1.2.1-beta.17\n\n### Bug fixes\n- Fixed \"Not signed in\" after login by adopting the login WebView directly for API calls\n- Added diagnostic error messages for JS errors and unexpected responses\n\n## What's new in v1.2.1\n\n### Bug fixes\n- Fixed WebKit suspending the background WKWebView: anchored in a transparent 1×1 NSWindow\n- Fixed login window auto-closing before sign-in completes\n- Added /api/organizations as a final fallback for org ID resolution\n- Fixed Settings incorrectly showing \"Signed in\" after a failed refresh"
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.2.0",
|
"version": "1.3.1",
|
||||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.0/ClaudeChecker.zip",
|
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.3.1/ClaudeChecker.zip",
|
||||||
"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"
|
"notes": "## What's new in v1.3.1\n\n### Privacy\n- Diagnostics panel no longer displays API response bodies, which could contain account and financial data. Status codes, error messages, cookie names, and request paths are still shown."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user