Skip to content

Commit 89bcd85

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex/sakana-payg
# Conflicts: # CHANGELOG.md
2 parents bf0875d + f6732bd commit 89bcd85

50 files changed

Lines changed: 1619 additions & 119 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ jobs:
8787
needs: changes
8888
if: ${{ needs.changes.outputs.macos-tests == 'true' }}
8989
runs-on: macos-15-intel
90-
timeout-minutes: 45
90+
timeout-minutes: 60
9191
strategy:
9292
fail-fast: false
9393
matrix:
@@ -119,7 +119,8 @@ jobs:
119119
run: ./Scripts/lint.sh lint-macos
120120

121121
- name: Swift Test
122-
timeout-minutes: 40
122+
# Intel cold builds can spend ~26 minutes compiling the full test target before isolated suites start.
123+
timeout-minutes: 50
123124
run: |
124125
CODEXBAR_TEST_GROUP_SIZE=1 \
125126
CODEXBAR_TEST_SUITE_TIMEOUT=120 \

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44

55
### Added
66
- Sakana AI: show best-effort pay-as-you-go credit balance and recent usage without delaying subscription quota refreshes. Thanks @ss251!
7+
- Kimi: show monthly subscription usage alongside weekly and five-hour limits with a short total budget for the optional membership request. Thanks @zhiyue!
78
- Localization: add complete Russian coverage for the app and redesigned website. Thanks @Kirchberg!
89
- Localization: add Galician app translations and language selection. Thanks @B1NAR10!
10+
- ClawRouter: add API-key tracking for monthly budget, spend, requests, tokens, and routed-provider usage.
911
- Claude: show model-scoped weekly quota windows, including promotional Fable limits, from OAuth and web usage responses. Thanks @konon4!
1012
- Usage refresh: add an opt-in Adaptive cadence that polls every 2–30 minutes based on recent menu use, Low Power Mode, and thermal state. Thanks @hhh2210!
1113
- Codex: show a conservative 1.5× pace-headroom hint in menus and CLI output when usage is safely ahead of the reset curve. Thanks @astuteprogrammer!
1214

1315
### Changed
16+
- Repository: reject oversized tracked blobs and generated release/build artifacts during checks. Thanks @joeVenner!
1417
- Branding: replace the app and website icon with a usage-meter prompt mark that matches CodexBar's core UI.
1518
- Website: redesign codexbar.app around faster download, provider discovery, feature, CLI, and widget paths with responsive dark/light and localized layouts. Thanks @vyctorbrzezowski!
1619
- Architecture: accept a bounded opt-in adaptive refresh design with a deterministic 2–30-minute cadence and no behavioral telemetry. Thanks @hhh2210!
@@ -23,6 +26,7 @@
2326
### Fixed
2427
- Gemini: resolve fnm from the active PATH, stop package-discovery helpers on deadline, and return after the first output line even when descendants keep stdout open.
2528
- Branding: replace the malformed Poe icon and use Poe's official purple consistently across the app, widget, and website. Thanks @garethpaul!
29+
- Claude: make the "Avoid Keychain prompts" setting use the no-prompt policy instead of the experimental `security` CLI reader. Thanks @gmkbenjamin!
2630
- Monthly quota pace: show reserve, deficit, and run-out estimates for OpenCode Go, Doubao, and Alibaba monthly reset windows using their calendar-cycle length. Thanks @Zihao-Qi and @joeVenner!
2731
- Localization: translate the Default Terminal setting across every supported app language. Thanks @Zihao-Qi!
2832
- Settings: recover collapsed sidebars and undersized saved window frames when reopening Settings. Thanks @ProspectOre!

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
[![License: MIT](https://img.shields.io/badge/license-MIT-6e5aff?style=flat-square)](LICENSE)
1010
[![Site](https://img.shields.io/badge/site-codexbar.app-16d3b4?style=flat-square)](https://codexbar.app)
1111

12-
<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 56 providers." width="100%" /></a>
12+
<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 57 providers." width="100%" /></a>
1313

1414
Tiny macOS 14+ menu bar app that keeps **AI coding-provider limits visible** and shows when each window resets. Codex, OpenAI, Claude, Cursor, Gemini, Copilot, Grok, GroqCloud, ElevenLabs, Deepgram, z.ai, MiniMax, Kiro, Zed, Vertex AI, Augment, OpenRouter, LiteLLM, LLM Proxy, Codebuff, Command Code, AWS Bedrock, and many newer coding providers. One status item per provider, or Merge Icons mode with a provider switcher. No Dock icon, minimal UI, dynamic bar icons.
1515

@@ -124,6 +124,7 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow.
124124
- [Grok](docs/grok.md) — Grok CLI billing RPC plus grok.com browser-session fallback.
125125
- [GroqCloud](docs/groqcloud.md) — API key for Enterprise Prometheus request/token/cache-hit metrics.
126126
- [LLM Proxy](docs/llm-proxy.md) — API key + base URL for aggregate proxy quota stats and provider breakdowns.
127+
- [ClawRouter](docs/clawrouter.md) — API key for monthly budget, spend, requests, tokens, and routed-provider usage.
127128
- [LiteLLM](docs/litellm.md) — Virtual key + proxy URL for personal and team budget/spend tracking.
128129
- [Deepgram](docs/deepgram.md) — API key usage summaries across speech, agent, token, and TTS metrics.
129130
- [Poe](docs/poe.md) — API key for current point balance and recent points history.

Scripts/check_repository_size.sh

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
55
MAX_BYTES=$((2 * 1024 * 1024))
66
failures=0
77
tracked_files=0
8+
declare -a blob_paths=()
9+
declare -a blob_ids=()
810

911
cd "$ROOT_DIR"
1012

11-
while IFS= read -r -d '' path; do
13+
while IFS= read -r -d '' entry; do
14+
metadata=${entry%%$'\t'*}
15+
path=${entry#*$'\t'}
16+
read -r mode object stage <<<"$metadata"
17+
[[ "$stage" == "0" ]] || continue
1218
tracked_files=$((tracked_files + 1))
1319

1420
case "$path" in
@@ -19,14 +25,29 @@ while IFS= read -r -d '' path; do
1925
;;
2026
esac
2127

22-
[[ -f "$path" && ! -L "$path" ]] || continue
23-
24-
size=$(wc -c < "$path")
25-
if ((size > MAX_BYTES)); then
26-
printf 'ERROR: tracked file exceeds %d bytes: %s (%d bytes)\n' "$MAX_BYTES" "$path" "$size" >&2
27-
failures=$((failures + 1))
28-
fi
29-
done < <(git ls-files -z)
28+
# Submodule entries name commits rather than file blobs.
29+
[[ "$mode" == "160000" ]] && continue
30+
blob_paths+=("$path")
31+
blob_ids+=("$object")
32+
done < <(git ls-files --stage -z)
33+
34+
if ((${#blob_ids[@]} > 0)); then
35+
index=0
36+
while read -r object type size; do
37+
path=${blob_paths[$index]}
38+
if [[ "$type" != "blob" ]]; then
39+
printf 'ERROR: tracked index entry is not a readable blob: %q (%s)\n' "$path" "$object" >&2
40+
failures=$((failures + 1))
41+
index=$((index + 1))
42+
continue
43+
fi
44+
if ((size > MAX_BYTES)); then
45+
printf 'ERROR: tracked file exceeds %d bytes: %q (%d bytes)\n' "$MAX_BYTES" "$path" "$size" >&2
46+
failures=$((failures + 1))
47+
fi
48+
index=$((index + 1))
49+
done < <(printf '%s\n' "${blob_ids[@]}" | git cat-file --batch-check='%(objectname) %(objecttype) %(objectsize)')
50+
fi
3051

3152
if ((failures > 0)); then
3253
printf 'Repository size check failed with %d violation(s).\n' "$failures" >&2

Scripts/test_repository_size.sh

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ trap 'rm -rf "$TEMP_DIR"' EXIT
88
mkdir -p "$TEMP_DIR/Scripts"
99
cp "$ROOT_DIR/Scripts/check_repository_size.sh" "$TEMP_DIR/Scripts/"
1010
git -C "$TEMP_DIR" init --quiet
11+
empty_output=$("$TEMP_DIR/Scripts/check_repository_size.sh")
12+
grep -Fq 'repository size OK: 0 tracked files' <<<"$empty_output"
13+
1114
printf 'small source file\n' > "$TEMP_DIR/source.txt"
1215
git -C "$TEMP_DIR" add source.txt Scripts/check_repository_size.sh
1316

@@ -20,13 +23,33 @@ dd if=/dev/zero of="$TEMP_DIR/boundary.bin" bs=1024 count=2048 2>/dev/null
2023
git -C "$TEMP_DIR" add boundary.bin
2124
"$TEMP_DIR/Scripts/check_repository_size.sh" >/dev/null
2225
printf 'x' >> "$TEMP_DIR/boundary.bin"
26+
git -C "$TEMP_DIR" add boundary.bin
2327
if "$TEMP_DIR/Scripts/check_repository_size.sh" >"$TEMP_DIR/large.log" 2>&1; then
24-
printf 'ERROR: oversized tracked file was accepted.\n' >&2
28+
printf 'ERROR: staged blob one byte above the limit was accepted.\n' >&2
2529
exit 1
2630
fi
2731
grep -Fq 'tracked file exceeds 2097152 bytes: boundary.bin (2097153 bytes)' "$TEMP_DIR/large.log"
2832

2933
git -C "$TEMP_DIR" rm --cached --force --quiet boundary.bin
34+
git -C "$TEMP_DIR" add untracked.bin
35+
printf 'working tree is now small\n' > "$TEMP_DIR/untracked.bin"
36+
if "$TEMP_DIR/Scripts/check_repository_size.sh" >"$TEMP_DIR/staged-large.log" 2>&1; then
37+
printf 'ERROR: oversized staged blob was accepted after its working-tree file changed.\n' >&2
38+
exit 1
39+
fi
40+
grep -Fq 'tracked file exceeds 2097152 bytes: untracked.bin (2098176 bytes)' "$TEMP_DIR/staged-large.log"
41+
42+
git -C "$TEMP_DIR" rm --cached --force --quiet untracked.bin
43+
printf 'small staged blob\n' > "$TEMP_DIR/index-is-authoritative.bin"
44+
git -C "$TEMP_DIR" add index-is-authoritative.bin
45+
dd if=/dev/zero of="$TEMP_DIR/index-is-authoritative.bin" bs=1024 count=2049 2>/dev/null
46+
"$TEMP_DIR/Scripts/check_repository_size.sh" >/dev/null
47+
48+
odd_path=$'odd\nname.txt'
49+
printf 'small source file\n' > "$TEMP_DIR/$odd_path"
50+
git -C "$TEMP_DIR" add "$odd_path"
51+
"$TEMP_DIR/Scripts/check_repository_size.sh" >/dev/null
52+
3053
artifacts=(
3154
"CodexBar 2.app/Contents/MacOS/CodexBar"
3255
"CodexBar.dSYM/Contents/Info.plist"
@@ -47,7 +70,7 @@ for artifact in "${artifacts[@]}"; do
4770
done
4871
ln -s source.txt "$TEMP_DIR/CodexBar-latest.dmg"
4972
git -C "$TEMP_DIR" add -f CodexBar-latest.dmg
50-
73+
rm "$TEMP_DIR/CodexBar.zip"
5174
if "$TEMP_DIR/Scripts/check_repository_size.sh" >"$TEMP_DIR/artifact.log" 2>&1; then
5275
printf 'ERROR: tracked release artifacts were accepted.\n' >&2
5376
exit 1

Sources/CodexBar/InlineUsageDashboardContent.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,25 @@ extension UsageMenuCardView.Model {
4646
return usage.displayLines
4747
}
4848

49+
if input.provider == .clawrouter,
50+
let usage = input.snapshot?.clawRouterUsage
51+
{
52+
var notes = [
53+
"\(UsageFormatter.tokenCountString(usage.requestCount)) \(L("requests")) · " +
54+
"\(UsageFormatter.tokenCountString(usage.totalTokens)) \(L("tokens"))",
55+
]
56+
if usage.errorCount > 0 {
57+
notes.append("\(usage.successCount) succeeded · \(usage.errorCount) failed")
58+
}
59+
if !usage.providers.isEmpty {
60+
let mix = usage.providers.prefix(5)
61+
.map { "\($0.provider): \(UsageFormatter.tokenCountString($0.requestCount))" }
62+
.joined(separator: " · ")
63+
notes.append("Routed providers: \(mix)")
64+
}
65+
return notes
66+
}
67+
4968
if input.provider == .minimax,
5069
input.showOptionalCreditsAndExtraUsage,
5170
let billing = input.snapshot?.minimaxUsage?.billingSummary

Sources/CodexBar/MenuCardView+Costs.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,26 @@ extension UsageMenuCardView.Model {
305305
return nil
306306
}
307307

308+
if provider == .clawrouter, cost.limit <= 0 {
309+
let spend = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
310+
return ProviderCostSection(
311+
title: "ClawRouter spend",
312+
percentUsed: nil,
313+
spendLine: "\(L("This month")): \(spend)",
314+
percentLine: nil)
315+
}
316+
308317
guard cost.limit > 0 else { return nil }
309318

310319
let used: String
311320
let limit: String
312321
let title: String
313322

314-
if cost.currencyCode == "Quota" {
323+
if provider == .clawrouter {
324+
title = "Monthly budget"
325+
used = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
326+
limit = UsageFormatter.currencyString(cost.limit, currencyCode: cost.currencyCode)
327+
} else if cost.currencyCode == "Quota" {
315328
title = L("Quota usage")
316329
used = String(format: "%.0f", cost.used)
317330
limit = String(format: "%.0f", cost.limit)

Sources/CodexBar/MenuDescriptor.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,18 @@ struct MenuDescriptor {
286286
if let openRouterUsage = snapshot.openRouterUsage {
287287
Self.appendOpenRouterUsageSummary(entries: &entries, usage: openRouterUsage)
288288
}
289+
if let clawRouterUsage = snapshot.clawRouterUsage {
290+
entries.append(.text(
291+
"\(UsageFormatter.tokenCountString(clawRouterUsage.requestCount)) \(L("requests")) · " +
292+
"\(UsageFormatter.tokenCountString(clawRouterUsage.totalTokens)) \(L("tokens"))",
293+
.secondary))
294+
if !clawRouterUsage.providers.isEmpty {
295+
let mix = clawRouterUsage.providers.prefix(5)
296+
.map { "\($0.provider): \(UsageFormatter.tokenCountString($0.requestCount))" }
297+
.joined(separator: " · ")
298+
entries.append(.text("Routed providers: \(mix)", .secondary))
299+
}
300+
}
289301
if let poeUsage = snapshot.poeUsage, !poeUsage.daily.isEmpty {
290302
Self.appendPoeUsageSummary(entries: &entries, usage: poeUsage)
291303
}

Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ struct ClaudeProviderImplementation: ProviderImplementation {
6767
let subtitle = if context.settings.debugDisableKeychainAccess {
6868
"Inactive while \"Disable Keychain access\" is enabled in Advanced."
6969
} else {
70-
"Use /usr/bin/security to read Claude credentials and avoid CodexBar keychain prompts."
70+
"Never allow Claude OAuth credential reads to show macOS Keychain prompts."
7171
}
7272

7373
let promptFreeBinding = Binding(
@@ -141,8 +141,7 @@ struct ClaudeProviderImplementation: ProviderImplementation {
141141
if context.settings.debugDisableKeychainAccess {
142142
return "Global Keychain access is disabled in Advanced, so this setting is currently inactive."
143143
}
144-
return "Controls Claude OAuth Keychain prompts when the standard reader is active. Choosing " +
145-
"\"Never prompt\" can make OAuth unavailable; use Web/CLI when needed."
144+
return "Choosing \"Never prompt\" can make OAuth unavailable; use Web/CLI when needed."
146145
}
147146

148147
return [
@@ -162,11 +161,11 @@ struct ClaudeProviderImplementation: ProviderImplementation {
162161
ProviderSettingsPickerDescriptor(
163162
id: "claude-keychain-prompt-policy",
164163
title: "Keychain prompt policy",
165-
subtitle: "Applies only to the Security.framework OAuth keychain reader.",
164+
subtitle: "Controls when Claude OAuth may ask macOS for Keychain access.",
166165
dynamicSubtitle: keychainPromptPolicySubtitle,
167166
binding: keychainPromptPolicyBinding,
168167
options: keychainPromptPolicyOptions,
169-
isVisible: { context.settings.claudeOAuthKeychainReadStrategy == .securityFramework },
168+
isVisible: nil,
170169
isEnabled: { !context.settings.debugDisableKeychainAccess },
171170
onChange: nil),
172171
ProviderSettingsPickerDescriptor(
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import CodexBarCore
2+
import Foundation
3+
4+
struct ClawRouterProviderImplementation: ProviderImplementation {
5+
let id: UsageProvider = .clawrouter
6+
7+
@MainActor
8+
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
9+
ProviderPresentation { _ in "api" }
10+
}
11+
12+
@MainActor
13+
func observeSettings(_ settings: SettingsStore) {
14+
_ = settings.clawRouterAPIKey
15+
_ = settings.clawRouterBaseURL
16+
}
17+
18+
@MainActor
19+
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
20+
ProviderTokenResolver.clawRouterToken(environment: context.environment) != nil
21+
}
22+
23+
@MainActor
24+
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
25+
[
26+
ProviderSettingsFieldDescriptor(
27+
id: "clawrouter-api-key",
28+
title: "API key",
29+
subtitle: "Stored in the CodexBar config file. Reads monthly budget and routed usage from /v1/usage.",
30+
kind: .secure,
31+
placeholder: "ClawRouter key…",
32+
binding: context.stringBinding(\.clawRouterAPIKey),
33+
actions: [],
34+
isVisible: nil,
35+
onActivate: nil),
36+
ProviderSettingsFieldDescriptor(
37+
id: "clawrouter-base-url",
38+
title: "Base URL",
39+
subtitle: "Optional. Defaults to the hosted ClawRouter service.",
40+
kind: .plain,
41+
placeholder: ClawRouterSettingsReader.defaultBaseURL.absoluteString,
42+
binding: context.stringBinding(\.clawRouterBaseURL),
43+
actions: [],
44+
isVisible: nil,
45+
onActivate: nil),
46+
]
47+
}
48+
}

0 commit comments

Comments
 (0)