Skip to content

Commit 9ef71bb

Browse files
Peter Steinbergerlucacampanella
andcommitted
chore: merge main into scoped weekly token
Co-authored-by: Luca Campanella <luca.campanella1@gmail.com>
2 parents 66f4b97 + 8393fc8 commit 9ef71bb

228 files changed

Lines changed: 9995 additions & 1331 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.

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,26 @@
33
## 0.46.1 — Unreleased
44

55
### Added
6+
- Notion AI: add Business and Enterprise workspace allowance tracking for rolling and billing-period windows (#2552). Thanks @n0ah37!
7+
- Notion AI: pace estimates on both the rolling and billing-period bars, scored against the real calendar month ending at the reset rather than a flat 30 days (#2552). Thanks @n0ah37!
8+
- Sync: opt-in iCloud sync (Settings → iCloud Sync, default off) syncs provider configuration, a curated preferences subset, and per-device usage snapshots across Macs via CloudKit; API keys/cookies/tokens ride end-to-end-encrypted fields with their own opt-out, hooks and machine-local paths never sync, and menus can show accounts from other Macs with last-known usage ("via <Mac> · 1h ago") when the local fetch is unavailable. The app now also watches `config.json`, so external CLI edits apply live.
9+
- z.ai: add 7-day and 30-day model-usage chart ranges with dataset-consistent legends, colors, and daily tooltips (#2524). Thanks @LeoLin990405!
610
- Refresh: add a default-off global Low Power Mode that limits automatic provider, local usage, and storage work to once every 30 minutes while keeping manual refresh immediate (#2518). Thanks @Carl723000!
11+
- CLI: `codexbar hooks watch` continuously polls providers and fires hooks on real quota/status transitions for headless installs, with in-memory baselines, event rate limits, `--interval` (default 300s, minimum 60s), `--provider`, and JSON output (#2536). Thanks @OfficialAbhinavSingh!
712
- Claude: compact multi-account menu for claude-swap — with four or more accounts the active account keeps its full card while the others become one-line rows sorted by remaining headroom, constrained accounts surface in red/amber, the healthiest switch target gets a star, and the healthy tail folds behind a summary row. Click a row to expand its full card.
813
- Menu: the compact multi-account layout now covers every stacked multi-account list — token accounts on any provider and Codex accounts (flat lists; workspace-grouped Codex lists keep their sections).
914
- Menu bar: Session/Weekly/Auto pace layout tokens that render the signed pace delta (`+11%`, `-8%`, `0%`), restoring the pre-0.45 "Both" display in the layout editor (#2540, fixes #2534). Thanks @kratocz!
1015
- Menu bar: model-scoped weekly percentage layout token, labeled from the active carve-out while keeping the persistent editor vocabulary model-generic (#2440, addresses #2360). Thanks @lucacampanella!
1116

1217
### Fixed
18+
- Notion AI: `codexbar` now honors the provider's Workspace ID, manual cookie header, and `off` source instead of always auto-selecting a workspace (#2552). Thanks @n0ah37!
19+
- Providers with monthly billing windows (Notion AI, Amp, MiMo, StepFun, Doubao, Alibaba, OpenCode Go): the menu bar's pace token, the "runs out" estimate, and predictive pace warnings now measure the real calendar cycle, matching the card and the CLI instead of scoring every period as a flat 30 days (#2552). Thanks @n0ah37!
20+
- Cursor: make on-demand extra usage follow the shared optional-usage setting and remove the unsupported credits placeholder (#2338). Thanks @Zihao-Qi!
21+
- Antigravity/Sessions: inspect processes in-process via libproc instead of spawning full-system ps/lsof, eliminating repeated macOS 26 “access data from other apps” prompts (#2267 hardening).
22+
- Doubao: show Agent Plan windows alongside Coding Plan usage for Volcengine AK/SK accounts that subscribe to both products (#2517). Thanks @Astro-Han!
23+
- Augment: store session cookies owner-only (0600), atomically publish updates, and repair permissions on legacy files (#2567).
24+
- Ollama: direct declined Chrome Keychain access recovery to the provider card's Refresh (⌘R) action instead of the ambiguous manual-cookie path (#2072).
25+
- Menu: merged provider tabs now size to their own content when switching from Overview instead of padding every tab to the tallest provider and leaving large blank regions.
1326
- Codex: persist and budget fork-parent discovery so missing parents quiesce between inventory changes instead of sweeping every rollout on each refresh (#2525, #2538). Thanks @xx205, and @Helmi and @kiranmagic7 for the investigation!
1427
- Claude: Auto cold boot with Keychain disabled loads without manual refresh (#2494, fixes #2493). Thanks @gmkbenjamin!
1528
- Menu: no more stray floating "Refresh" tooltip beside the menu when switching tabs with the cursor over the actions area.
@@ -30,6 +43,9 @@
3043
### Changed
3144
- About: link the Website entry to codex.bar.
3245

46+
### Fixed
47+
- Alibaba: authenticate mainland and international personal Token Plans without a captured Teams workspace, preserve valid sessions on gateway permission errors, and forward optional security tokens (#2533, fixes #2500, #2349, and #2370). Thanks @wait2050!
48+
3349
## 0.46.0 — 2026-07-29
3450

3551
### Added

Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ let useLocalSweetCookieKit =
88
let sweetCookieKitDependency: Package.Dependency =
99
useLocalSweetCookieKit && FileManager.default.fileExists(atPath: sweetCookieKitPath)
1010
? .package(path: sweetCookieKitPath)
11-
: .package(url: "https://github.com/steipete/SweetCookieKit", from: "0.4.1")
11+
: .package(url: "https://github.com/steipete/SweetCookieKit", from: "0.5.1")
1212

1313
let sqlite3LibDir = ProcessInfo.processInfo.environment["CODEXBAR_SQLITE3_LIB_DIR"]?
1414
.trimmingCharacters(in: .whitespacesAndNewlines)

README.md

Lines changed: 1 addition & 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. 66 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. 67 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, ClinePass, 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

Scripts/cloudkit/deploy_schema.sh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
# Deploys the CodexBar CloudKit schema (Scripts/cloudkit/schema.ckdb).
3+
#
4+
# Requires a CloudKit management token (create one at
5+
# https://icloud.developer.apple.com/dashboard → account menu → Tokens,
6+
# or via `xcrun cktool save-token`). Pass it via CLOUDKIT_MANAGEMENT_TOKEN
7+
# or save it first with `xcrun cktool save-token`.
8+
#
9+
# Usage: Scripts/cloudkit/deploy_schema.sh [development|production]
10+
set -euo pipefail
11+
12+
ENVIRONMENT="${1:-development}"
13+
case "$ENVIRONMENT" in
14+
development|production) ;;
15+
*) echo "ERROR: environment must be development or production" >&2; exit 1 ;;
16+
esac
17+
18+
TEAM_ID="Y5PE65HELJ"
19+
CONTAINER_ID="iCloud.com.steipete.codexbar"
20+
SCHEMA_FILE="$(cd "$(dirname "$0")" && pwd)/schema.ckdb"
21+
22+
TOKEN_ARGS=()
23+
if [[ -n "${CLOUDKIT_MANAGEMENT_TOKEN:-}" ]]; then
24+
TOKEN_ARGS=(--token "$CLOUDKIT_MANAGEMENT_TOKEN")
25+
fi
26+
27+
echo "Validating schema against $ENVIRONMENT..."
28+
xcrun cktool validate-schema ${TOKEN_ARGS[@]+"${TOKEN_ARGS[@]}"} \
29+
--team-id "$TEAM_ID" --container-id "$CONTAINER_ID" \
30+
--environment "$ENVIRONMENT" --file "$SCHEMA_FILE"
31+
32+
echo "Importing schema into $ENVIRONMENT..."
33+
xcrun cktool import-schema ${TOKEN_ARGS[@]+"${TOKEN_ARGS[@]}"} \
34+
--team-id "$TEAM_ID" --container-id "$CONTAINER_ID" \
35+
--environment "$ENVIRONMENT" --file "$SCHEMA_FILE"
36+
37+
echo "Done. Current $ENVIRONMENT schema:"
38+
xcrun cktool export-schema ${TOKEN_ARGS[@]+"${TOKEN_ARGS[@]}"} \
39+
--team-id "$TEAM_ID" --container-id "$CONTAINER_ID" \
40+
--environment "$ENVIRONMENT"

Scripts/cloudkit/schema.ckdb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
DEFINE SCHEMA
2+
3+
RECORD TYPE AccountSnapshot (
4+
accountKey STRING,
5+
deviceID STRING,
6+
fetchedAt TIMESTAMP,
7+
provider STRING,
8+
schemaVersion INT64,
9+
displayLabel ENCRYPTED STRING,
10+
usagePayload ENCRYPTED STRING,
11+
GRANT WRITE TO "_creator",
12+
GRANT CREATE TO "_icloud",
13+
GRANT READ TO "_creator"
14+
);
15+
16+
RECORD TYPE Device (
17+
appVersion STRING,
18+
deviceID STRING,
19+
hostName STRING,
20+
lastSeen TIMESTAMP,
21+
model STRING,
22+
schemaVersion INT64,
23+
GRANT WRITE TO "_creator",
24+
GRANT CREATE TO "_icloud",
25+
GRANT READ TO "_creator"
26+
);
27+
28+
RECORD TYPE Preferences (
29+
editCount INT64,
30+
modifiedAt TIMESTAMP,
31+
payload STRING,
32+
schemaVersion INT64,
33+
GRANT WRITE TO "_creator",
34+
GRANT CREATE TO "_icloud",
35+
GRANT READ TO "_creator"
36+
);
37+
38+
RECORD TYPE ProviderIntent (
39+
editCount INT64,
40+
modifiedAt TIMESTAMP,
41+
payload STRING,
42+
provider STRING,
43+
schemaVersion INT64,
44+
apiKey ENCRYPTED STRING,
45+
cookieHeader ENCRYPTED STRING,
46+
secretKey ENCRYPTED STRING,
47+
tokenAccounts ENCRYPTED STRING,
48+
GRANT WRITE TO "_creator",
49+
GRANT CREATE TO "_icloud",
50+
GRANT READ TO "_creator"
51+
);

Scripts/package_app.sh

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,36 @@ if [[ "$ALLOW_LLDB" == "1" && "$LOWER_CONF" != "debug" ]]; then
242242
echo "ERROR: CODEXBAR_ALLOW_LLDB requires debug configuration" >&2
243243
exit 1
244244
fi
245+
# iCloud sync (CloudKit) requires restricted entitlements authorized by an embedded
246+
# Developer ID provisioning profile. Only identity-signed release builds of the primary
247+
# bundle ID carry them; adhoc/debug builds run with sync unavailable.
248+
PROVISIONING_PROFILE_SOURCE="$ROOT/Scripts/profiles/CodexBar-DeveloperID.provisionprofile"
249+
EMBED_PROVISIONING_PROFILE=0
250+
ICLOUD_ENTITLEMENT_KEYS=""
251+
if [[ "$SIGNING_MODE" == "identity" && "$LOWER_CONF" == "release" && "$BUNDLE_ID" == "com.steipete.codexbar" ]]; then
252+
if [[ ! -f "$PROVISIONING_PROFILE_SOURCE" ]]; then
253+
echo "ERROR: Missing $PROVISIONING_PROFILE_SOURCE (required for iCloud entitlements in release builds)" >&2
254+
exit 1
255+
fi
256+
EMBED_PROVISIONING_PROFILE=1
257+
ICLOUD_ENTITLEMENT_KEYS=$(cat <<ICLOUD
258+
<key>com.apple.application-identifier</key>
259+
<string>${APP_TEAM_ID}.${BUNDLE_ID}</string>
260+
<key>com.apple.developer.team-identifier</key>
261+
<string>${APP_TEAM_ID}</string>
262+
<key>com.apple.developer.icloud-services</key>
263+
<array>
264+
<string>CloudKit</string>
265+
</array>
266+
<key>com.apple.developer.icloud-container-identifiers</key>
267+
<array>
268+
<string>iCloud.${BUNDLE_ID}</string>
269+
</array>
270+
<key>com.apple.developer.icloud-container-environment</key>
271+
<string>Production</string>
272+
ICLOUD
273+
)
274+
fi
245275
cat > "$APP_ENTITLEMENTS" <<PLIST
246276
<?xml version="1.0" encoding="UTF-8"?>
247277
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -251,6 +281,7 @@ cat > "$APP_ENTITLEMENTS" <<PLIST
251281
<array>
252282
<string>${APP_GROUP_ID}</string>
253283
</array>
284+
${ICLOUD_ENTITLEMENT_KEYS}
254285
$(if [[ "$ALLOW_LLDB" == "1" ]]; then echo " <key>com.apple.security.get-task-allow</key><true/>"; fi)
255286
</dict>
256287
</plist>
@@ -562,6 +593,12 @@ if [[ -d "${APP}/Contents/PlugIns/CodexBarWidget.appex" ]]; then
562593
"$APP/Contents/PlugIns/CodexBarWidget.appex"
563594
fi
564595

596+
# Embed the Developer ID provisioning profile (authorizes the iCloud entitlements;
597+
# Gatekeeper re-validates it at every launch, so it must be sealed into the signature).
598+
if [[ "$EMBED_PROVISIONING_PROFILE" == "1" ]]; then
599+
cp "$PROVISIONING_PROFILE_SOURCE" "$APP/Contents/embedded.provisionprofile"
600+
fi
601+
565602
# Finally sign the app bundle itself
566603
codesign "${CODESIGN_ARGS[@]}" \
567604
--entitlements "$APP_ENTITLEMENTS" \
19 KB
Binary file not shown.

Sources/CodexBar/CodexbarApp.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ struct CodexBarApp: App {
112112
PreferencesView(
113113
settings: self.settings,
114114
store: self.store,
115+
cloudSyncState: self.appDelegate.cloudSyncState,
115116
updater: self.appDelegate.updaterController,
116117
selection: self.preferencesSelection,
117118
managedCodexAccountCoordinator: self.managedCodexAccountCoordinator,
@@ -377,6 +378,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
377378
}
378379

379380
let updaterController: UpdaterProviding = makeUpdaterController()
381+
let cloudSyncState = CloudSyncState()
380382
private let confettiOverlayController = ScreenConfettiOverlayController()
381383
private let confettiLogger = CodexBarLog.logger(LogCategories.confetti)
382384
private lazy var memoryPressureMonitor = MemoryPressureMonitor(trimAppCaches: { [weak self] in
@@ -390,6 +392,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
390392
private var preferencesSelection: PreferencesSelection?
391393
private var managedCodexAccountCoordinator: ManagedCodexAccountCoordinator?
392394
private var codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator?
395+
private var cloudSyncCoordinator: CloudSyncCoordinator?
393396
private var hasInstalledLimitResetObservers = false
394397
#if DEBUG
395398
private var debugMemoryPressureObserver: NSObjectProtocol?
@@ -405,6 +408,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
405408
self.preferencesSelection = dependencies.selection
406409
self.managedCodexAccountCoordinator = dependencies.managedCodexAccountCoordinator
407410
self.codexAccountPromotionCoordinator = dependencies.codexAccountPromotionCoordinator
411+
self.cloudSyncCoordinator = CloudSyncCoordinator(settings: dependencies.settings, state: self.cloudSyncState)
408412
}
409413

410414
func applicationWillFinishLaunching(_ notification: Notification) {
@@ -417,6 +421,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
417421
self.installDebugMemoryPressureObserverIfNeeded()
418422
#endif
419423
self.ensureStatusController()
424+
self.cloudSyncCoordinator?.start()
420425
Task { @MainActor [weak self] in
421426
await Task.yield()
422427
guard let settings = self?.settings else { return }
@@ -452,6 +457,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
452457
}
453458

454459
func applicationWillTerminate(_ notification: Notification) {
460+
self.cloudSyncCoordinator?.stop()
455461
self.memoryPressureMonitor.stop()
456462
#if DEBUG
457463
self.removeDebugMemoryPressureObserver()
@@ -462,6 +468,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
462468
self.terminateActiveProcessesForAppShutdown()
463469
}
464470

471+
func applicationDidBecomeActive(_ notification: Notification) {
472+
self.cloudSyncCoordinator?.applicationDidBecomeActive()
473+
}
474+
465475
func runProviderLoginFlow(_ provider: UsageProvider) async {
466476
self.ensureStatusController()
467477
guard let statusController else { return }
@@ -557,6 +567,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
557567
managedCodexAccountCoordinator,
558568
codexAccountPromotionCoordinator)
559569
if let statusController = self.statusController as? StatusItemController {
570+
statusController.cloudSyncState = self.cloudSyncState
560571
MenuSwitchFlickerProbe.startIfRequested(controller: statusController)
561572
}
562573
return
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import CodexBarCore
2+
import Foundation
3+
import SwiftUI
4+
5+
struct FleetAccountMenuProjection {
6+
let fallback: AccountSnapshotSyncPayload?
7+
let additionalAccounts: [AccountSnapshotSyncPayload]
8+
}
9+
10+
enum FleetAccountMenuPlanner {
11+
static func projection(
12+
provider: UsageProvider,
13+
snapshots: some Sequence<AccountSnapshotSyncPayload>,
14+
currentDeviceID: String,
15+
localAccountKeys: Set<String>,
16+
hasLocalUsage: Bool) -> FleetAccountMenuProjection
17+
{
18+
let remote = snapshots
19+
.filter { $0.provider == provider && $0.deviceID != currentDeviceID }
20+
let freshestByAccount = Dictionary(grouping: remote, by: \.accountKey)
21+
.compactMap { _, candidates in candidates.max(by: self.isOlder) }
22+
.sorted(by: self.isNewer)
23+
let fallback = hasLocalUsage ? nil : freshestByAccount.first
24+
let additionalAccounts = freshestByAccount.filter { candidate in
25+
!localAccountKeys.contains(candidate.accountKey) && candidate.accountKey != fallback?.accountKey
26+
}
27+
return FleetAccountMenuProjection(fallback: fallback, additionalAccounts: additionalAccounts)
28+
}
29+
30+
static func badge(deviceName: String, fetchedAt: Date, now: Date = .now) -> String {
31+
"via \(deviceName) · \(self.staleness(fetchedAt: fetchedAt, now: now))"
32+
}
33+
34+
static func staleness(fetchedAt: Date, now: Date = .now) -> String {
35+
let seconds = max(0, Int(now.timeIntervalSince(fetchedAt)))
36+
if seconds < 60 {
37+
return L("just now")
38+
}
39+
if seconds < 3600 {
40+
return String(format: L("%dm ago"), max(1, seconds / 60))
41+
}
42+
if seconds < 86400 {
43+
return String(format: L("%dh ago"), max(1, seconds / 3600))
44+
}
45+
return String(format: L("%dd ago"), max(1, seconds / 86400))
46+
}
47+
48+
private static func isOlder(_ lhs: AccountSnapshotSyncPayload, _ rhs: AccountSnapshotSyncPayload) -> Bool {
49+
if lhs.fetchedAt != rhs.fetchedAt {
50+
return lhs.fetchedAt < rhs.fetchedAt
51+
}
52+
return lhs.deviceID < rhs.deviceID
53+
}
54+
55+
private static func isNewer(_ lhs: AccountSnapshotSyncPayload, _ rhs: AccountSnapshotSyncPayload) -> Bool {
56+
if lhs.fetchedAt != rhs.fetchedAt {
57+
return lhs.fetchedAt > rhs.fetchedAt
58+
}
59+
if lhs.accountKey != rhs.accountKey {
60+
return lhs.accountKey < rhs.accountKey
61+
}
62+
return lhs.deviceID < rhs.deviceID
63+
}
64+
}
65+
66+
struct FleetAccountMenuCardView: View {
67+
let model: UsageMenuCardView.Model
68+
let width: CGFloat
69+
70+
var body: some View {
71+
UsageMenuCardView(model: self.model, width: self.width)
72+
.opacity(0.78)
73+
}
74+
}

0 commit comments

Comments
 (0)