Skip to content

Commit 978ce6e

Browse files
Peter Steinbergerumutkeltek
andcommitted
chore: merge main into dashboard snapshot CLI
Co-authored-by: Umut Keltek <umut.keltek@gmail.com>
2 parents 5133b25 + 8393fc8 commit 978ce6e

294 files changed

Lines changed: 16296 additions & 1727 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +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!
10+
- 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!
612
- 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.
713
- 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).
14+
- 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!
815

916
### Fixed
17+
- 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!
18+
- 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!
19+
- Cursor: make on-demand extra usage follow the shared optional-usage setting and remove the unsupported credits placeholder (#2338). Thanks @Zihao-Qi!
20+
- 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).
21+
- Doubao: show Agent Plan windows alongside Coding Plan usage for Volcengine AK/SK accounts that subscribe to both products (#2517). Thanks @Astro-Han!
22+
- Augment: store session cookies owner-only (0600), atomically publish updates, and repair permissions on legacy files (#2567).
23+
- Ollama: direct declined Chrome Keychain access recovery to the provider card's Refresh (⌘R) action instead of the ambiguous manual-cookie path (#2072).
24+
- 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.
25+
- 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!
1026
- Claude: Auto cold boot with Keychain disabled loads without manual refresh (#2494, fixes #2493). Thanks @gmkbenjamin!
1127
- Menu: no more stray floating "Refresh" tooltip beside the menu when switching tabs with the cursor over the actions area.
1228
- Providers: write the Factory and Cursor session files (bearer/refresh tokens, auth cookies) owner-only (0600), matching the codex/kimi/antigravity credential stores.
@@ -26,6 +42,9 @@
2642
### Changed
2743
- About: link the Website entry to codex.bar.
2844

45+
### Fixed
46+
- 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!
47+
2948
## 0.46.0 — 2026-07-29
3049

3150
### 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.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Foundation
2+
3+
enum BackgroundWorkPowerPolicy {
4+
static let lowPowerMinimumInterval: TimeInterval = 30 * 60
5+
6+
static func automaticInterval(
7+
_ requested: TimeInterval?,
8+
lowPowerModeEnabled: Bool) -> TimeInterval?
9+
{
10+
guard let requested else { return nil }
11+
guard lowPowerModeEnabled else { return requested }
12+
return max(requested, self.lowPowerMinimumInterval)
13+
}
14+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import Foundation
2+
import IOKit.ps
3+
4+
enum CodexCostCatchUpMode: String, Sendable {
5+
case automatic
6+
case accelerated
7+
}
8+
9+
enum CodexCostCatchUpPowerSource: String, Sendable {
10+
case ac
11+
case battery
12+
case unknown
13+
14+
static func current() -> Self {
15+
guard let info = IOPSCopyPowerSourcesInfo()?.takeRetainedValue(),
16+
let source = IOPSGetProvidingPowerSourceType(info)?.takeUnretainedValue() as String?
17+
else {
18+
return .unknown
19+
}
20+
if source == kIOPSACPowerValue as String {
21+
return .ac
22+
}
23+
if source == kIOPSBatteryPowerValue as String {
24+
return .battery
25+
}
26+
return .unknown
27+
}
28+
}
29+
30+
enum CodexCostCatchUpPauseReason: Sendable, Equatable {
31+
case lowPower
32+
case thermal
33+
case user
34+
case noProgress
35+
case error(String)
36+
}
37+
38+
struct CodexCostCatchUpActivity: Sendable, Equatable {
39+
enum Phase: Sendable, Equatable {
40+
case indexing
41+
case paused
42+
case complete
43+
}
44+
45+
let phase: Phase
46+
let mode: CodexCostCatchUpMode
47+
let processedBytes: Int64
48+
let totalBytes: Int64
49+
let completedFiles: Int
50+
let totalFiles: Int
51+
let pauseReason: CodexCostCatchUpPauseReason?
52+
let staleSnapshotUpdatedAt: Date?
53+
54+
var fractionCompleted: Double? {
55+
guard self.totalBytes > 0 else {
56+
guard self.totalFiles > 0 else { return nil }
57+
return min(1, max(0, Double(self.completedFiles) / Double(self.totalFiles)))
58+
}
59+
return min(1, max(0, Double(self.processedBytes) / Double(self.totalBytes)))
60+
}
61+
}
62+
63+
struct CodexCostCatchUpPolicy: Sendable {
64+
struct Input: Sendable {
65+
let mode: CodexCostCatchUpMode
66+
let previousActiveDuration: TimeInterval?
67+
let powerSource: CodexCostCatchUpPowerSource
68+
let lowPowerModeEnabled: Bool
69+
let thermalState: ProcessInfo.ThermalState
70+
}
71+
72+
struct Decision: Sendable, Equatable {
73+
enum Action: Sendable, Equatable {
74+
case runAfter(TimeInterval)
75+
case pause(TimeInterval, CodexCostCatchUpPauseReason)
76+
}
77+
78+
let action: Action
79+
let targetDutyCycle: Double?
80+
}
81+
82+
static let automaticBurstDuration: TimeInterval = 2
83+
static let constrainedRetryDelay: TimeInterval = 60
84+
85+
func decision(for input: Input) -> Decision {
86+
if input.thermalState == .critical {
87+
return Decision(
88+
action: .pause(Self.constrainedRetryDelay, .thermal),
89+
targetDutyCycle: nil)
90+
}
91+
if input.mode == .automatic {
92+
if input.lowPowerModeEnabled {
93+
return Decision(
94+
action: .pause(Self.constrainedRetryDelay, .lowPower),
95+
targetDutyCycle: nil)
96+
}
97+
if input.thermalState == .serious {
98+
return Decision(
99+
action: .pause(Self.constrainedRetryDelay, .thermal),
100+
targetDutyCycle: nil)
101+
}
102+
}
103+
if input.mode == .accelerated {
104+
return Decision(action: .runAfter(0), targetDutyCycle: 1)
105+
}
106+
107+
let dutyCycle = switch input.powerSource {
108+
case .ac: 0.20
109+
case .battery: 0.05
110+
case .unknown: 0.15
111+
}
112+
let activeDuration = max(0, input.previousActiveDuration ?? Self.automaticBurstDuration)
113+
let delay = activeDuration * (1 - dutyCycle) / dutyCycle
114+
return Decision(action: .runAfter(delay), targetDutyCycle: dutyCycle)
115+
}
116+
}

0 commit comments

Comments
 (0)