Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e939bdb88b | ||
|
|
6749c5f158 | ||
|
|
de71ec2794 | ||
|
|
ac17ce154f | ||
|
|
d2eac62897 | ||
|
|
8c64fc50ad | ||
|
|
b2c138f665 | ||
|
|
aa6c299e2a | ||
|
|
ecbae7d7ca | ||
|
|
37a039e1d3 | ||
|
|
8f3ead2f65 | ||
|
|
734670bb3f | ||
|
|
94430e00ba | ||
|
|
7b0368b001 | ||
|
|
4629aeffbc | ||
|
|
2cb4ddfe97 | ||
|
|
73b251a1e9 | ||
|
|
7bab6a0df0 | ||
|
|
db6380fa66 | ||
|
|
47a148fa67 | ||
|
|
85ac329d36 | ||
|
|
a0fb6eabcf | ||
|
|
b778d03323 | ||
|
|
13387888b2 | ||
|
|
44be1649a1 | ||
|
|
1160712e1b | ||
|
|
e4f55c80ee | ||
|
|
526752dc36 | ||
|
|
a2d983b555 | ||
|
|
0d807c2285 | ||
|
|
1c42433145 | ||
|
|
c0ac2e5a66 | ||
|
|
405e65f9f7 | ||
|
|
1c7433ebea | ||
|
|
a39ef800e8 | ||
|
|
76dec8b85d | ||
|
|
2f04f8581c | ||
|
|
788c066a1e | ||
|
|
31280d4ab2 | ||
|
|
7b1c5bbf8e | ||
|
|
f2ba44bbaa | ||
|
|
de1fffe0e3 | ||
|
|
e80f07d1b9 | ||
|
|
e9ca8a7efc | ||
|
|
63ef2005a4 | ||
|
|
5ad7bb26f0 | ||
|
|
132a2a8f8d | ||
|
|
debd1c1850 | ||
|
|
9bcbb2ae7d | ||
|
|
c9384f6424 | ||
|
|
e4693fc18f | ||
|
|
84a8592bae | ||
|
|
bdd2038364 | ||
|
|
fd61face77 | ||
|
|
1f58a81a26 | ||
|
|
7f6da51860 | ||
|
|
b49ac23863 | ||
|
|
01d268c3e5 | ||
|
|
871d5245ac | ||
|
|
0524f9b791 | ||
|
|
d68d564351 | ||
|
|
d55bad01db | ||
|
|
7bc72e240b | ||
|
|
d77ed23d09 | ||
|
|
6136b0ded0 | ||
|
|
aba7168448 | ||
|
|
a8c2083397 | ||
|
|
ad23446ed4 | ||
|
|
d8b75d279c | ||
|
|
2658ff7f6a | ||
|
|
e10d79e5a6 | ||
|
|
88daa27630 | ||
|
|
b7c4958a74 | ||
|
|
f46b13d967 | ||
|
|
c449882ec6 | ||
|
|
3c1a2ef1be | ||
|
|
c238db963b | ||
|
|
63c9a8582d | ||
|
|
6f256b3308 | ||
|
|
2761b31a12 | ||
|
|
f9d8abebd5 | ||
|
|
66ee233b1b | ||
|
|
a12c664bde | ||
|
|
c7251d8785 | ||
|
|
7a95b100ef | ||
|
|
fb98c69c34 | ||
|
|
5629c6b61e | ||
|
|
4709f5b0f5 | ||
|
|
5f8aeaa6e9 | ||
|
|
27f1f45109 | ||
|
|
7a5512cc28 | ||
|
|
8c89ce4636 | ||
|
|
3a964dc2f8 | ||
|
|
b553d3922a | ||
|
|
61e8455e5e | ||
|
|
c4b9d7454f | ||
|
|
75aeb03332 | ||
|
|
f5203a0299 | ||
|
|
fe27e0d4c0 | ||
|
|
fb5e1ece25 | ||
|
|
71666224f1 | ||
|
|
bd693cdf94 | ||
|
|
d24a354e1c | ||
|
|
9e47041715 | ||
|
|
22151f0af4 | ||
|
|
3508fa1a94 | ||
|
|
b2fcafd3f0 | ||
|
|
45215de350 | ||
|
|
f969da8006 | ||
|
|
7f769414cc | ||
|
|
db707340bb | ||
|
|
be6bb1495d | ||
|
|
06755de91d | ||
|
|
2855155361 | ||
|
|
ecfb1aa44d | ||
|
|
8b35e5df56 | ||
|
|
794a6cdb65 | ||
|
|
e613ace503 | ||
|
|
38d7683843 | ||
|
|
802cf14a71 | ||
|
|
ae736a7e4a | ||
|
|
a1bb186af2 | ||
|
|
e93a4e6df5 | ||
|
|
a3fbce3f0e | ||
|
|
4b4280630c | ||
|
|
90acbcb968 | ||
|
|
f2f2be1305 | ||
|
|
f17694622c | ||
|
|
aa10f77776 | ||
|
|
cb00ddc806 | ||
|
|
1f357b9123 | ||
|
|
9dee674ff0 | ||
|
|
0025690a74 | ||
|
|
7b03909c55 | ||
|
|
c3ea2be809 | ||
|
|
6f6a7361fe | ||
|
|
d9d65da6b7 | ||
|
|
c18054a810 | ||
|
|
032951882f | ||
|
|
ed186af2c6 | ||
|
|
d3dabc7843 | ||
|
|
987bd36137 | ||
|
|
cfcb74f32b | ||
|
|
273e10566e | ||
|
|
76a8f433a7 | ||
|
|
65a171f296 | ||
|
|
26312290ac | ||
|
|
63cbd70f38 | ||
|
|
235ab81efb | ||
|
|
12d7855ceb | ||
|
|
d1ab5206bd | ||
|
|
997e820be8 | ||
|
|
7fc78d5a83 | ||
|
|
818d4aa6c2 | ||
|
|
6aa74fcabc | ||
|
|
2a01d6f5b8 | ||
|
|
af920eda83 | ||
|
|
6b6802eaea | ||
|
|
e9d29db2ee | ||
|
|
9be5069aac | ||
|
|
cc9e2aaba1 | ||
|
|
19bb6459ed | ||
|
|
abf05b04fc | ||
|
|
849e94c942 | ||
|
|
d76c7496bb | ||
|
|
45a3925f65 | ||
|
|
113e91583d | ||
|
|
c60fb93e1a |
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report something that isn't working correctly
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behaviour:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**Expected behaviour**
|
||||
What you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment**
|
||||
- macOS version: [e.g. Sequoia 15.4]
|
||||
- ClaudeChecker version: [e.g. 1.1.3]
|
||||
- Claude plan: [e.g. Pro, Max]
|
||||
|
||||
**Additional context**
|
||||
Any other context about the problem here.
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea or improvement
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**What problem does this solve?**
|
||||
A clear and concise description of the problem or limitation you're running into.
|
||||
|
||||
**Describe the solution you'd like**
|
||||
What you'd like to happen.
|
||||
|
||||
**Alternatives considered**
|
||||
Any other approaches you've thought about.
|
||||
|
||||
**Screenshots or mockups**
|
||||
If applicable, add visuals to help explain your idea.
|
||||
|
||||
**Additional context**
|
||||
Any other context here.
|
||||
@@ -0,0 +1,79 @@
|
||||
name: Release Beta
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*-beta*'
|
||||
|
||||
jobs:
|
||||
release-beta:
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
xcodebuild \
|
||||
-project ClaudeChecker.xcodeproj \
|
||||
-scheme ClaudeChecker \
|
||||
-configuration Release \
|
||||
-derivedDataPath build \
|
||||
clean build
|
||||
|
||||
- name: Zip app
|
||||
run: |
|
||||
ditto -c -k --keepParent \
|
||||
build/Build/Products/Release/ClaudeChecker.app \
|
||||
ClaudeChecker.zip
|
||||
|
||||
- name: Create GitHub pre-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create "$GITHUB_REF_NAME" ClaudeChecker.zip \
|
||||
--title "$GITHUB_REF_NAME" \
|
||||
--prerelease \
|
||||
--notes "$(cat RELEASE_NOTES.md 2>/dev/null || echo 'Beta release ${{ steps.version.outputs.version }}')"
|
||||
|
||||
- name: Update version-beta.json
|
||||
run: |
|
||||
notes=$(cat RELEASE_NOTES.md 2>/dev/null || echo "Beta release ${{ steps.version.outputs.version }}")
|
||||
jq -n \
|
||||
--arg version "${{ steps.version.outputs.version }}" \
|
||||
--arg url "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/ClaudeChecker.zip" \
|
||||
--arg notes "$notes" \
|
||||
'{version: $version, url: $url, notes: $notes}' > version-beta.json
|
||||
|
||||
- name: Update beta badge in README
|
||||
run: |
|
||||
shields_version=$(echo "${{ steps.version.outputs.version }}" | sed 's/-/--/g')
|
||||
tag="${{ github.ref_name }}"
|
||||
export BADGE="[](https://github.com/${{ github.repository }}/releases/tag/${tag}) <!-- BETA_BADGE -->"
|
||||
python3 -c "
|
||||
import os
|
||||
badge = os.environ['BADGE']
|
||||
lines = open('README.md').readlines()
|
||||
out = []
|
||||
for line in lines:
|
||||
out.append(badge + chr(10) if '<!-- BETA_BADGE -->' in line else line)
|
||||
open('README.md', 'w').writelines(out)
|
||||
"
|
||||
|
||||
- name: Commit version-beta.json and README
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add version-beta.json README.md
|
||||
git commit -m "Beta release ${{ github.ref_name }}"
|
||||
git pull --rebase origin main
|
||||
git push origin HEAD:main
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: ${{ !contains(github.ref_name, '-') }}
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -65,10 +66,24 @@ jobs:
|
||||
../homebrew-tap/Casks/claudechecker.rb 2>/dev/null || true
|
||||
|
||||
- name: Update README badge
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
sed -i '' \
|
||||
's/version-[0-9][^-]*-orange/version-${{ steps.version.outputs.version }}-orange/' \
|
||||
README.md
|
||||
python3 -c "
|
||||
import os, re
|
||||
v = os.environ['NEW_VERSION']
|
||||
content = open('README.md').read()
|
||||
content = re.sub(r'version-[0-9][^-]*-orange', 'version-' + v + '-orange', content)
|
||||
lines = content.splitlines(keepends=True)
|
||||
out = []
|
||||
for line in lines:
|
||||
if '<!-- BETA_BADGE -->' in line:
|
||||
continue
|
||||
out.append(line)
|
||||
if 'license-MIT-blue' in line:
|
||||
out.append('<!-- BETA_BADGE -->' + chr(10))
|
||||
open('README.md', 'w').writelines(out)
|
||||
"
|
||||
|
||||
- name: Commit version.json and README
|
||||
run: |
|
||||
@@ -76,6 +91,7 @@ jobs:
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add version.json README.md
|
||||
git commit -m "Release ${{ github.ref_name }}"
|
||||
git pull --rebase origin main
|
||||
git push origin HEAD:main
|
||||
|
||||
- name: Update Homebrew tap
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for your interest in ClaudeChecker!
|
||||
|
||||
## Reporting bugs & requesting features
|
||||
|
||||
Please use the [issue templates](https://github.com/superdooper86/claudechecker/issues/new/choose) — they keep reports consistent and easy to act on.
|
||||
|
||||
## Pull requests
|
||||
|
||||
- Open an issue first to discuss anything non-trivial. This avoids wasted effort if the direction isn't a fit.
|
||||
- Keep changes focused — one fix or feature per PR.
|
||||
- Follow the existing Swift code style in the repo.
|
||||
- There's no guarantee PRs will be merged; this is a personal project maintained in spare time.
|
||||
|
||||
## Notes
|
||||
|
||||
- This app targets macOS 13+
|
||||
- No external dependencies — keep it that way
|
||||
@@ -27,7 +27,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
let config = WKWebViewConfiguration()
|
||||
config.websiteDataStore = WKWebsiteDataStore.default()
|
||||
cookiePrimerView = WKWebView(frame: .zero, configuration: config)
|
||||
cookiePrimerView?.load(URLRequest(url: URL(string: "https://claude.ai/api/organizations/\(UsageViewModel.orgId)/usage")!))
|
||||
cookiePrimerView?.load(URLRequest(url: URL(string: "https://claude.ai")!))
|
||||
|
||||
// Status item
|
||||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||
@@ -40,14 +40,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
// Popover
|
||||
popover = NSPopover()
|
||||
popover?.contentSize = NSSize(width: 480, height: 640)
|
||||
popover?.behavior = .transient
|
||||
popover?.animates = true
|
||||
popover?.contentViewController = NSHostingController(
|
||||
let hc = NSHostingController(
|
||||
rootView: ContentView()
|
||||
.environmentObject(usageViewModel)
|
||||
.environmentObject(updateManager)
|
||||
)
|
||||
hc.sizingOptions = .preferredContentSize
|
||||
hc.view.wantsLayer = true
|
||||
hc.view.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
|
||||
popover?.contentViewController = hc
|
||||
|
||||
// React to limits or menubar toggle changes
|
||||
usageViewModel.$limits
|
||||
@@ -236,3 +239,5 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -40,20 +40,35 @@ struct ContentView: View {
|
||||
.environmentObject(updater)
|
||||
.transition(.move(edge: .trailing).combined(with: .opacity))
|
||||
} else {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
// Cards grid
|
||||
if vm.limits.isEmpty {
|
||||
EmptyStateView()
|
||||
.padding(.vertical, 40)
|
||||
} else {
|
||||
VStack(spacing: 1) {
|
||||
VStack(spacing: 8) {
|
||||
ForEach(vm.limits) { limit in
|
||||
LimitCard(limit: limit)
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text(limit.window == .fiveHour ? "5 Hour Limit" : "7 Day Limit")
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.foregroundColor(.secondary)
|
||||
.textCase(.uppercase)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 4)
|
||||
|
||||
LimitCard(limit: limit)
|
||||
.background(Color(nsColor: .controlBackgroundColor))
|
||||
.cornerRadius(8)
|
||||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.primary.opacity(0.08), lineWidth: 0.5))
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color.primary.opacity(0.05))
|
||||
.padding(.vertical, 1)
|
||||
.padding(.bottom, 4)
|
||||
|
||||
// Extra credits row
|
||||
if let extra = vm.extraUsage, extra.isEnabled {
|
||||
@@ -80,7 +95,6 @@ struct ContentView: View {
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
|
||||
@@ -196,7 +210,7 @@ struct LimitCard: View {
|
||||
AgentIconView(agent: .claude, size: 14)
|
||||
Text("Claude")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
Text("· \(vm.planLabel) · \(limit.window.label)")
|
||||
Text("· \(vm.planLabel)")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
@@ -382,12 +396,11 @@ struct DiaryRow: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack {
|
||||
AgentIconView(agent: .claude, size: 12)
|
||||
Text("Claude")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(color)
|
||||
Text("\(totalSaved) samples")
|
||||
.font(.system(size: 10.5))
|
||||
.foregroundColor(.secondary)
|
||||
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))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@@ -413,7 +426,9 @@ struct SparklineView: View {
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
if data.count > 1 {
|
||||
let plotData = data.count == 1 ? [data[0], data[0]] : data
|
||||
if plotData.count > 1 {
|
||||
let data = plotData
|
||||
let w = geo.size.width
|
||||
let h = geo.size.height
|
||||
let minV = data.min() ?? 0
|
||||
@@ -773,25 +788,23 @@ struct SettingsView: View {
|
||||
.padding(.vertical, 1)
|
||||
.background(Color.primary.opacity(0.06))
|
||||
.cornerRadius(4)
|
||||
if updater.updateAvailable {
|
||||
Text("v\(updater.latestVersion) available")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.blue)
|
||||
.cornerRadius(4)
|
||||
.fixedSize()
|
||||
} else if updater.betaAvailable {
|
||||
Text("v\(updater.latestBetaVersion) available")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.orange)
|
||||
.cornerRadius(4)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
if updater.updateAvailable {
|
||||
Text("v\(updater.latestVersion) available")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.blue)
|
||||
.cornerRadius(4)
|
||||
} else if updater.betaAvailable {
|
||||
Text("v\(updater.latestBetaVersion) available")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.orange)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
Text("Built by superdooper86 & claude")
|
||||
.font(.system(size: 11))
|
||||
@@ -898,12 +911,12 @@ struct ErrorBanner: View {
|
||||
struct EmptyStateView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 10) {
|
||||
Image(systemName: "key.slash")
|
||||
Image(systemName: "person.crop.circle.badge.questionmark")
|
||||
.font(.system(size: 28))
|
||||
.foregroundColor(.secondary)
|
||||
Text("No API key configured")
|
||||
Text("Not signed in")
|
||||
.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))
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
@@ -1024,9 +1037,9 @@ struct UpdateBanner: View {
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text("Update available — v\(updater.latestVersion)")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
if !updater.releaseNotes.isEmpty {
|
||||
ReleaseNotesText(text: updater.releaseNotes, font: .system(size: 11))
|
||||
}
|
||||
Text("Tap Update to view what's new & install.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Button("Update") { showSheet = true }
|
||||
@@ -1037,6 +1050,7 @@ struct UpdateBanner: View {
|
||||
.padding(.vertical, 10)
|
||||
.background(Color.blue.opacity(0.08))
|
||||
.overlay(Divider(), alignment: .bottom)
|
||||
.onTapGesture { showSheet = true }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1154,3 +1168,5 @@ struct UpdateSheet: View {
|
||||
.frame(width: 380)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.11</string>
|
||||
<string>1.2.1-beta.6</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>31</string>
|
||||
<string>54</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
<key>LSUIElement</key>
|
||||
|
||||
@@ -24,21 +24,25 @@ struct LoginWebView: NSViewRepresentable {
|
||||
|
||||
class Coordinator: NSObject, WKNavigationDelegate {
|
||||
let onAuthenticated: () -> Void
|
||||
var didAuthenticate = false
|
||||
|
||||
init(onAuthenticated: @escaping () -> Void) {
|
||||
self.onAuthenticated = onAuthenticated
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
guard !didAuthenticate else { return }
|
||||
// Don't fire on the login/auth pages themselves
|
||||
if let url = webView.url?.absoluteString,
|
||||
url.contains("/login") || url.contains("/auth") { return }
|
||||
|
||||
// URL is not a login/auth page, so if any claude.ai cookie exists we're signed in
|
||||
WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in
|
||||
let hasSession = cookies.contains {
|
||||
$0.domain.contains("claude.ai") &&
|
||||
($0.name == "sessionKey" || $0.name == "__Secure-next-auth.session-token")
|
||||
}
|
||||
if hasSession {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.onAuthenticated()
|
||||
}
|
||||
let hasAnyCookie = cookies.contains { $0.domain.contains("claude.ai") }
|
||||
guard hasAnyCookie, !self.didAuthenticate else { return }
|
||||
self.didAuthenticate = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.onAuthenticated()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,9 +62,18 @@ struct LoginSheetView: View {
|
||||
Text(authenticated ? "✓ Signed in — loading data…" : "Sign in to Claude")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
Spacer()
|
||||
Button("Cancel") { isPresented = false }
|
||||
.buttonStyle(.bordered)
|
||||
if authenticated {
|
||||
Button("Done") {
|
||||
isPresented = false
|
||||
onDone()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
} else {
|
||||
Button("Cancel") { isPresented = false }
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
|
||||
@@ -318,13 +318,31 @@ class UpdateManager: ObservableObject {
|
||||
}
|
||||
|
||||
private func isNewer(_ version: String, than current: String) -> Bool {
|
||||
let toInts = { (v: String) in v.split(separator: ".").compactMap { Int($0) } }
|
||||
let a = toInts(version), b = toInts(current)
|
||||
for i in 0..<max(a.count, b.count) {
|
||||
let av = i < a.count ? a[i] : 0
|
||||
let bv = i < b.count ? b[i] : 0
|
||||
func parse(_ v: String) -> (base: [Int], pre: [Int]?) {
|
||||
guard !v.isEmpty else { return ([], nil) }
|
||||
let halves = v.split(separator: "-", maxSplits: 1)
|
||||
let base = halves[0].split(separator: ".").compactMap { Int($0) }
|
||||
let pre = halves.count > 1 ? String(halves[1]).split(separator: ".").compactMap { Int($0) } : nil
|
||||
return (base, pre)
|
||||
}
|
||||
let a = parse(version), b = parse(current)
|
||||
// Compare base version numbers
|
||||
for i in 0..<max(a.base.count, b.base.count) {
|
||||
let av = i < a.base.count ? a.base[i] : 0
|
||||
let bv = i < b.base.count ? b.base[i] : 0
|
||||
if av != bv { return av > bv }
|
||||
}
|
||||
// Base equal: stable beats pre-release
|
||||
if a.pre == nil && b.pre != nil { return true }
|
||||
if a.pre != nil && b.pre == nil { return false }
|
||||
// Both pre-release: compare pre-release numbers
|
||||
if let ap = a.pre, let bp = b.pre {
|
||||
for i in 0..<max(ap.count, bp.count) {
|
||||
let av = i < ap.count ? ap[i] : 0
|
||||
let bv = i < bp.count ? bp[i] : 0
|
||||
if av != bv { return av > bv }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import WebKit
|
||||
|
||||
@MainActor
|
||||
class UsageViewModel: ObservableObject {
|
||||
static let orgId = "daf626a9-4924-4ff3-ba98-23b523062f8e"
|
||||
@Published var limits: [AgentLimit] = []
|
||||
@Published var isLoading = false
|
||||
@Published var lastUpdated: Date?
|
||||
@@ -28,16 +27,24 @@ class UsageViewModel: ObservableObject {
|
||||
|
||||
private var burnHistoryStore: [String: [Double]] = [:]
|
||||
private let maxHistorySamples = 24
|
||||
private var cachedOrgId: String?
|
||||
private var previousPercents: [String: Double] = [:]
|
||||
private var firedThresholds: [String: Set<Int>] = [:]
|
||||
|
||||
init() {
|
||||
cachedOrgId = UserDefaults.standard.string(forKey: "claude_org_id") ?? Self.orgId
|
||||
let saved = UserDefaults.standard.double(forKey: "refresh_interval")
|
||||
refreshInterval = saved > 0 ? saved : 60
|
||||
showInMenuBar = UserDefaults.standard.object(forKey: "show_in_menubar") as? Bool ?? true
|
||||
if let saved = UserDefaults.standard.object(forKey: "burn_history") as? [String: [Double]] {
|
||||
burnHistoryStore = saved
|
||||
}
|
||||
loadPlaceholderData()
|
||||
Task { await checkInitialSignInState() }
|
||||
}
|
||||
|
||||
private func checkInitialSignInState() async {
|
||||
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
|
||||
let hasAnyCookie = cookies.contains { $0.domain.contains("claude.ai") }
|
||||
if hasAnyCookie { isSignedIn = true }
|
||||
}
|
||||
|
||||
func signOut() async {
|
||||
@@ -46,6 +53,7 @@ class UsageViewModel: ObservableObject {
|
||||
let records = await store.dataRecords(ofTypes: types)
|
||||
let claudeRecords = records.filter { $0.displayName.contains("claude.ai") }
|
||||
await store.removeData(ofTypes: types, for: claudeRecords)
|
||||
UserDefaults.standard.removeObject(forKey: "claude_org_id")
|
||||
isSignedIn = false
|
||||
isNotAuthenticated = true
|
||||
lastUpdated = nil
|
||||
@@ -63,18 +71,29 @@ class UsageViewModel: ObservableObject {
|
||||
defer { isLoading = false }
|
||||
|
||||
do {
|
||||
let orgId = cachedOrgId!
|
||||
async let usageFetch = fetchUsage(orgId: orgId)
|
||||
async let prepaidFetch = fetchPrepaidCredits(orgId: orgId)
|
||||
async let overageFetch = fetchOverageSpendLimit(orgId: orgId)
|
||||
async let emailFetch = fetchUserEmail()
|
||||
let (usage, prepaid, overage, email) = try await (usageFetch, prepaidFetch, overageFetch, emailFetch)
|
||||
if let email { userEmail = email }
|
||||
let (fetchedOrgId, fetchedEmail, fetchedPlan) = try await fetchBootstrap()
|
||||
|
||||
let orgId: String
|
||||
if let id = fetchedOrgId {
|
||||
UserDefaults.standard.set(id, forKey: "claude_org_id")
|
||||
orgId = id
|
||||
} else if let cached = UserDefaults.standard.string(forKey: "claude_org_id") {
|
||||
orgId = cached
|
||||
} else {
|
||||
throw AppError.notAuthenticated
|
||||
}
|
||||
|
||||
if let email = fetchedEmail { userEmail = email }
|
||||
if let plan = fetchedPlan { planLabel = plan }
|
||||
|
||||
async let usageFetch = fetchUsage(orgId: orgId)
|
||||
async let prepaidFetch = fetchPrepaidCredits(orgId: orgId)
|
||||
async let overageFetch = fetchOverageSpendLimit(orgId: orgId)
|
||||
let (usage, prepaid, overage) = try await (usageFetch, prepaidFetch, overageFetch)
|
||||
limits = buildLimits(from: usage)
|
||||
extraUsage = usage.extraUsage
|
||||
prepaidCredits = prepaid
|
||||
overageSpendLimit = overage
|
||||
planLabel = "Pro"
|
||||
lastUpdated = Date()
|
||||
isSignedIn = true
|
||||
|
||||
@@ -84,8 +103,9 @@ class UsageViewModel: ObservableObject {
|
||||
history.append(limits[i].usedPercent)
|
||||
if history.count > maxHistorySamples { history.removeFirst() }
|
||||
burnHistoryStore[key] = history
|
||||
if history.count > 1 { limits[i].burnHistory = history }
|
||||
limits[i].burnHistory = history
|
||||
}
|
||||
UserDefaults.standard.set(burnHistoryStore, forKey: "burn_history")
|
||||
checkLimitNotifications(for: limits)
|
||||
} catch AppError.notAuthenticated {
|
||||
isNotAuthenticated = true
|
||||
@@ -106,54 +126,89 @@ class UsageViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fetch usage
|
||||
// MARK: - Bootstrap (org ID + email + plan label in one call)
|
||||
|
||||
private func fetchUsage(orgId: String) async throws -> UsageResponse {
|
||||
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/usage")!
|
||||
private func claudeAPIRequest(for url: URL) async -> URLRequest {
|
||||
var req = URLRequest(url: url)
|
||||
req.setValue("application/json", forHTTPHeaderField: "accept")
|
||||
|
||||
// Inject cookies from WKWebsiteDataStore (shared with browser/Claude app)
|
||||
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
|
||||
let claudeCookies = cookies.filter { $0.domain.contains("claude.ai") }
|
||||
if let header = HTTPCookie.requestHeaderFields(with: claudeCookies)["Cookie"] {
|
||||
req.setValue(header, forHTTPHeaderField: "Cookie")
|
||||
req.setValue("application/json, text/plain, */*", forHTTPHeaderField: "accept")
|
||||
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")
|
||||
req.setValue("https://claude.ai", forHTTPHeaderField: "Origin")
|
||||
req.setValue("https://claude.ai/", forHTTPHeaderField: "Referer")
|
||||
req.setValue("same-origin", forHTTPHeaderField: "sec-fetch-site")
|
||||
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)
|
||||
guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
|
||||
if http.statusCode == 401 || http.statusCode == 403 { throw AppError.notAuthenticated }
|
||||
guard http.statusCode == 200 else { throw AppError.networkError }
|
||||
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
return (nil, nil, nil)
|
||||
}
|
||||
|
||||
return try JSONDecoder().decode(UsageResponse.self, from: data)
|
||||
let account = json["account"] as? [String: Any]
|
||||
// memberships may live under account or at root (older API shape)
|
||||
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))
|
||||
planLabel = name.prefix(1).uppercased() + name.dropFirst().lowercased()
|
||||
}
|
||||
|
||||
return (orgId, email, planLabel)
|
||||
}
|
||||
|
||||
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")
|
||||
// MARK: - Fetch usage
|
||||
|
||||
private func claudeCookieHeader() async -> String? {
|
||||
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
|
||||
let claudeCookies = cookies.filter { $0.domain.contains("claude.ai") }
|
||||
if let header = HTTPCookie.requestHeaderFields(with: claudeCookies)["Cookie"] {
|
||||
req.setValue(header, forHTTPHeaderField: "Cookie")
|
||||
}
|
||||
return HTTPCookie.requestHeaderFields(with: claudeCookies)["Cookie"]
|
||||
}
|
||||
|
||||
private func fetchUsage(orgId: String) async throws -> UsageResponse {
|
||||
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/usage")!
|
||||
let req = await claudeAPIRequest(for: url)
|
||||
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
|
||||
guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
|
||||
if http.statusCode == 401 || http.statusCode == 403 { throw AppError.notAuthenticated }
|
||||
guard http.statusCode == 200 else { throw AppError.networkError }
|
||||
return try JSONDecoder().decode(UsageResponse.self, from: data)
|
||||
}
|
||||
|
||||
private func fetchPrepaidCredits(orgId: String) async throws -> PrepaidCredits? {
|
||||
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/prepaid/credits")!
|
||||
var req = URLRequest(url: url)
|
||||
req.setValue("application/json", forHTTPHeaderField: "accept")
|
||||
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
|
||||
let claudeCookies = cookies.filter { $0.domain.contains("claude.ai") }
|
||||
if let header = HTTPCookie.requestHeaderFields(with: claudeCookies)["Cookie"] {
|
||||
req.setValue(header, forHTTPHeaderField: "Cookie")
|
||||
}
|
||||
let req = await claudeAPIRequest(for: url)
|
||||
let (data, response) = try await URLSession.shared.data(for: req)
|
||||
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
|
||||
return try? JSONDecoder().decode(PrepaidCredits.self, from: data)
|
||||
@@ -161,13 +216,7 @@ class UsageViewModel: ObservableObject {
|
||||
|
||||
private func fetchOverageSpendLimit(orgId: String) async throws -> OverageSpendLimit? {
|
||||
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/overage_spend_limit")!
|
||||
var req = URLRequest(url: url)
|
||||
req.setValue("application/json", forHTTPHeaderField: "accept")
|
||||
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
|
||||
let claudeCookies = cookies.filter { $0.domain.contains("claude.ai") }
|
||||
if let header = HTTPCookie.requestHeaderFields(with: claudeCookies)["Cookie"] {
|
||||
req.setValue(header, forHTTPHeaderField: "Cookie")
|
||||
}
|
||||
let req = await claudeAPIRequest(for: url)
|
||||
let (data, response) = try await URLSession.shared.data(for: req)
|
||||
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
|
||||
return try? JSONDecoder().decode(OverageSpendLimit.self, from: data)
|
||||
@@ -241,14 +290,11 @@ class UsageViewModel: ObservableObject {
|
||||
|
||||
private func checkLimitNotifications(for limits: [AgentLimit]) {
|
||||
let thresholds = [80, 95]
|
||||
|
||||
for limit in limits {
|
||||
let key = limit.window.rawValue
|
||||
let curr = limit.usedPercent
|
||||
let prev = previousPercents[key]
|
||||
var fired = firedThresholds[key] ?? []
|
||||
|
||||
// Threshold warnings (80% and 95%)
|
||||
for t in thresholds {
|
||||
let threshold = Double(t)
|
||||
guard curr >= threshold, prev ?? threshold < threshold, !fired.contains(t) else { continue }
|
||||
@@ -259,8 +305,6 @@ class UsageViewModel: ObservableObject {
|
||||
userInfo: ["windowName": limit.window.displayName, "percent": t]
|
||||
)
|
||||
}
|
||||
|
||||
// Reset detection: was high, now low
|
||||
if let prev, prev > 50, curr < 20 {
|
||||
fired.removeAll()
|
||||
NotificationCenter.default.post(
|
||||
@@ -269,7 +313,6 @@ class UsageViewModel: ObservableObject {
|
||||
userInfo: ["windowName": limit.window.displayName]
|
||||
)
|
||||
}
|
||||
|
||||
firedThresholds[key] = fired
|
||||
previousPercents[key] = curr
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
|
||||
[](https://www.apple.com/macos/)
|
||||
[](https://swift.org)
|
||||
[](https://github.com/superdooper86/claudechecker/releases)
|
||||
[](https://github.com/superdooper86/claudechecker/releases)
|
||||
[](LICENSE)
|
||||
[](https://github.com/superdooper86/claudechecker/releases/tag/v1.2.1-beta.5) <!-- BETA_BADGE -->
|
||||
|
||||
</div>
|
||||
|
||||
@@ -29,21 +30,21 @@ No API key required. It reads directly from your `claude.ai` session using a bui
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><img src="screenshots/main-v2.png" width="280" alt="Main window" /><br/><sub>Main window</sub></td>
|
||||
<td align="center"><img src="screenshots/settings-v2.png" width="280" alt="Settings" /><br/><sub>Settings</sub></td>
|
||||
<td align="center"><img src="screenshots/main-v3.png" width="280" alt="Main window" /><br/><sub>Main window</sub></td>
|
||||
<td align="center"><img src="screenshots/settings-v3.png" width="280" alt="Settings" /><br/><sub>Settings</sub></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="screenshots/update-banner.png" width="280" alt="Update banner" /><br/><sub>Update available</sub></td>
|
||||
<td align="center"><img src="screenshots/notification.png" width="280" alt="Update notification" /><br/><sub>System notification</sub></td>
|
||||
<td align="center"><img src="screenshots/update-banner.png" width="280" alt="Update available" /><br/><sub>Update available</sub></td>
|
||||
<td align="center"><img src="screenshots/release-notes.png" width="280" alt="Release notes" /><br/><sub>Release notes</sub></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="screenshots/notification.png" width="280" alt="Update notification" /><br/><sub>Update notification</sub></td>
|
||||
<td align="center"><img src="screenshots/notif-reset.png" width="280" alt="Limit reset" /><br/><sub>Limit reset</sub></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="screenshots/notif-limit-80.png" width="280" alt="80% limit warning" /><br/><sub>80% limit warning</sub></td>
|
||||
<td align="center"><img src="screenshots/notif-limit-95.png" width="280" alt="95% limit warning" /><br/><sub>95% limit warning</sub></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="screenshots/notif-reset.png" width="280" alt="Limit reset" /><br/><sub>Limit reset</sub></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
## What's new in v1.0.11
|
||||
## What's new in v1.2.1
|
||||
|
||||
### Update indicator in menubar
|
||||
When an update is available, the menubar now shows **↑ Update** alongside your usage percentages — so you can see there's an update without opening the app.
|
||||
|
||||
### Update banner can't be dismissed
|
||||
The × close button has been removed from the update banner. The banner stays visible until you install the update.
|
||||
### Bug fixes
|
||||
- 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 after sign-in — API requests now include required browser-like headers (Origin, Referer, User-Agent) that Claude's usage endpoints require
|
||||
|
||||
|
Before Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1.2.1-beta.5",
|
||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.5/ClaudeChecker.zip",
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "0.0.1-beta.49",
|
||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/win-v0.0.1-beta.49/ClaudeChecker-Windows.zip",
|
||||
"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"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.1.1",
|
||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.0.10/ClaudeChecker.zip",
|
||||
"notes": "## What's new in v1.0.10\\n\\n### Main window always opens on click\\nClosing the app while Settings was open would cause the next click to reopen straight to Settings. The app now always returns to the main window when reopened."
|
||||
"version": "1.2.0",
|
||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.0/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"
|
||||
}
|
||||
|
||||