chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:33 +08:00
commit fc4fcbab58
1848 changed files with 472303 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
codexbar.app
+218
View File
@@ -0,0 +1,218 @@
---
summary: "Development workflow: build/run scripts, logging, and keychain migration notes."
read_when:
- Starting local development
- Running build/test scripts
- Troubleshooting Keychain prompts in dev
---
# CodexBar Development Guide
## Quick Start
### Building and Running
```bash
# Full build, package, and launch (recommended)
./Scripts/compile_and_run.sh
# Also run the sharded test suite before packaging/relaunching
./Scripts/compile_and_run.sh --test
# Just build and package (no tests)
./Scripts/package_app.sh
# Launch existing app (no rebuild)
./Scripts/launch.sh
```
### Development Workflow
1. **Make code changes** in `Sources/CodexBar/`
2. **Run** `./Scripts/compile_and_run.sh --test` to test, rebuild, and launch
3. **Check logs** in Console.app (filter by "codexbar")
4. **Optional file log**: enable Debug → Logging → "Enable file logging" to write
`~/Library/Logs/CodexBar/CodexBar.log` (verbosity defaults to "Verbose")
## Keychain Prompts (Development)
### First Launch After Fresh Clone
You'll see **one keychain prompt per stored credential** on the first launch. This is a **one-time migration** that converts existing keychain items to use `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`.
### Subsequent Rebuilds
The migration flag is stored in UserDefaults, so migrated CodexBar-owned items should not prompt again. Ad-hoc
signing can still prompt for other keychain surfaces; use `./Scripts/compile_and_run.sh --clear-adhoc-keychain`
when you intentionally want to reset ad-hoc keychain state.
### Why This Happens
- Ad-hoc signed development builds change code signature on every rebuild
- macOS keychain normally prompts when signature changes
- We use `ThisDeviceOnly` accessibility to prevent prompts
- Migration runs once to convert any existing items
### Reset Migration (Testing)
```bash
defaults delete com.steipete.codexbar KeychainMigrationV1Completed
```
## Augment Cookie Refresh
### How It Works
CodexBar checks Augment through the provider fetch pipeline. Auto mode tries the Augment CLI first, then the
browser-cookie web path. The web path reuses cached cookies when possible and imports from supported browsers when
the cache is missing or rejected.
### Refresh Frequency
- Default: Every 5 minutes (configurable in Preferences → General)
- Minimum: 1 minute
- Cookie import happens automatically when cached cookies need refresh
### Supported Browsers
- Safari, Chrome variants, Edge variants, Brave, Arc variants, Dia, and Firefox.
### Manual Cookie Override
If automatic import fails:
1. Open Preferences → Providers → Augment
2. Change "Cookie source" to "Manual"
3. Paste cookie header from browser DevTools
## Project Structure
Key source, test, and packaging paths (not exhaustive):
```
CodexBar/
├── Sources/CodexBar/ # Main app (SwiftUI + AppKit)
│ ├── CodexbarApp.swift # App entry point
│ ├── StatusItemController*.swift # Menu bar icon, menu rendering, and actions
│ ├── UsageStore*.swift # Usage refresh, caching, widgets, and history
│ ├── SettingsStore*.swift # User preferences and config persistence
│ ├── Providers/ # App-side provider settings/runtime glue
│ └── Resources/ # Assets and localized strings
├── Sources/CodexBarCore/ # Shared business logic used by app, CLI, and widgets
│ ├── Config/ # Config file model, reader, writer, and validation
│ ├── Providers/ # Provider descriptors, fetchers, parsers, and status probes
│ ├── OpenAIWeb/ # OpenAI dashboard integration helpers
│ ├── WebKit/ # Web session helpers
│ └── Vendored/ # Embedded support code
├── Sources/CodexBarCLI/ # Bundled codexbar command-line tool
├── Sources/CodexBarWidget/ # WidgetKit support
├── WidgetExtension/ # Xcode wrapper for the packaged widget extension
├── Tests/CodexBarTests/ # macOS app/core test suite (XCTest + Swift Testing)
├── TestsLinux/ # Linux-specific CLI/core test coverage
└── Scripts/ # Build and packaging scripts
```
## Common Tasks
### Add a New Provider
See the canonical [provider authoring guide](provider.md#adding-a-new-provider-current-flow) for the complete flow.
1. Add the provider identity to `Sources/CodexBarCore/Providers/Providers.swift`.
2. Add the descriptor and the fetcher, parser, settings-reader, or status-probe pieces the provider needs under
`Sources/CodexBarCore/Providers/YourProvider/`.
3. Register the descriptor from `Sources/CodexBarCore/Providers/ProviderDescriptor.swift`.
4. Add an app-side `ProviderImplementation` under `Sources/CodexBar/Providers/YourProvider/`; implementations can use
protocol defaults when no custom UI or macOS integration is needed.
5. Add the provider's exhaustive switch case to
`Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift`.
6. Add icon assets under `Sources/CodexBar/Resources/`.
7. Add focused tests under `Tests/CodexBarTests/` and, for CLI/core behavior that must run on Linux, `TestsLinux/`.
### Debug Cookie Issues
1. Enable Debug → Logging → "Enable file logging" or raise verbosity in the app settings.
2. Reproduce with `./Scripts/compile_and_run.sh`.
3. Check logs in Console.app:
- Filter: `subsystem:com.steipete.codexbar category:augment`
- Importer messages include the `[augment-cookie]` prefix
### Run Tests Only
```bash
make test
```
### Format Code
```bash
swiftformat Sources Tests
swiftlint --strict
```
## Distribution
### Local Development Build
```bash
./Scripts/package_app.sh
# Creates: CodexBar.app with ad-hoc signing by default
```
### Release Build (Notarized)
```bash
./Scripts/sign-and-notarize.sh
# Creates: CodexBar-<version>.zip and CodexBar-<version>.dSYM.zip
```
See `docs/RELEASING.md` for full release process.
## Troubleshooting
### App Won't Launch
```bash
# Check crash logs
ls -lt ~/Library/Logs/DiagnosticReports/CodexBar* | head -5
# Check Console.app for errors
# Filter: process:CodexBar
```
### Keychain Prompts Keep Appearing
```bash
# Verify migration completed
defaults read com.steipete.codexbar KeychainMigrationV1Completed
# Should output: 1
# Check migration logs
log show --predicate 'category == "keychain-migration"' --last 5m
```
### Cookies Not Refreshing
1. Check the browser is supported by the Augment provider metadata
2. Verify you're logged into Augment in that browser
3. Check Preferences → Providers → Augment → Cookie source is "Automatic"
4. Enable debug logging and check Console.app
### Main-Thread Hangs
Debug builds start the hang watchdog automatically. To diagnose a release build,
enable it explicitly and restart CodexBar:
```bash
defaults write com.steipete.codexbar debugMainThreadHangWatchdog -bool true
```
Hangs are written to the app log. Hangs over two seconds also request a process
sample under `~/Library/Logs/CodexBar/`. Disable the release opt-in with:
```bash
defaults delete com.steipete.codexbar debugMainThreadHangWatchdog
```
## Architecture Notes
### Menu Bar App Pattern
- No dock icon (LSUIElement = true)
- Status item only (NSStatusBar)
- SwiftUI for preferences, AppKit for menu
- Hidden 1×1 window keeps SwiftUI lifecycle alive
### Cookie Management
- Automatic browser import via SweetCookieKit
- Keychain cache for some imported browser cookies and OAuth/device-flow credentials
- `~/.codexbar/config.json` for provider settings, manual cookies, and stored API keys
- Manual override for debugging
- Browser-cookie import when cached sessions need refresh
### Usage Polling
- Background timer (configurable frequency)
- Parallel provider fetches
- First failure can be suppressed when prior data exists
- WidgetKit snapshot for macOS widgets
+179
View File
@@ -0,0 +1,179 @@
---
summary: "Development setup: stable signing and reducing Keychain prompts."
read_when:
- Setting up local development
- Reducing Keychain prompts during rebuilds
- Configuring dev signing
---
# Development Setup Guide
## Reducing Keychain Permission Prompts
When developing CodexBar, you may see frequent keychain permission prompts like:
> **CodexBar wants to access key "Claude Code-credentials" in your keychain.**
This happens because each rebuild creates a new code signature, and macOS treats it as a "different" app.
That can affect both CodexBar-owned entries (`com.steipete.CodexBar`, `com.steipete.codexbar.cache`) and
third-party items such as `Claude Code-credentials`, so an ad-hoc-signed rebuild can keep re-triggering
password/keychain approval dialogs even after you previously chose **Always Allow**.
### Quick Fix (Temporary)
When the prompt appears, click **"Always Allow"** instead of just "Allow". This grants access to the current build.
### Permanent Fix (Recommended)
Use a stable development certificate that doesn't change between rebuilds:
#### 1. Create Development Certificate
```bash
./Scripts/setup_dev_signing.sh
```
This creates a self-signed certificate named "CodexBar Development".
#### 2. Trust the Certificate
1. Open **Keychain Access.app**
2. Find **"CodexBar Development"** in the **login** keychain
3. Double-click it
4. Expand the **"Trust"** section
5. Set **"Code Signing"** to **"Always Trust"**
6. Close the window (enter your password when prompted)
#### 3. Configure Your Shell
Add this to your `~/.zshrc` (or `~/.bashrc` if using bash):
```bash
export APP_IDENTITY='CodexBar Development'
```
Then restart your terminal:
```bash
source ~/.zshrc
```
#### 4. Rebuild
```bash
./Scripts/compile_and_run.sh
```
Now your builds will use the stable certificate, and keychain prompts will be much less frequent!
> Note: `compile_and_run.sh` now auto-detects a valid signing identity (Developer ID or CodexBar Development).
> Set `APP_IDENTITY` to override the auto-detected choice.
---
## Cleaning Up Old App Bundles
If you see multiple `CodexBar *.app` bundles in your project directory, you can clean them up:
```bash
# Remove all numbered builds
rm -rf "CodexBar "*.app
# The .gitignore already excludes these patterns:
# - CodexBar.app
# - CodexBar *.app/
```
The build script creates `CodexBar.app` in the project root. Old numbered builds (like `CodexBar 2.app`) are created when Finder can't overwrite the running app.
---
## Development Workflow
### Standard Build & Run
```bash
./Scripts/compile_and_run.sh
```
This script:
1. Kills existing CodexBar instances
2. Runs `swift build` (release mode)
3. Runs the sharded full test suite when `--test` is passed
4. Packages the app with `./Scripts/package_app.sh`
5. Launches `CodexBar.app`
6. Verifies it stays running
Launching an unbundled `CodexBar` executable, including SwiftPM builds using `.build` or a custom scratch path, disables
Keychain access for that process to avoid repeated password prompts. Use the packaged `CodexBar.app` when local
validation needs browser cookies or stored credentials; packaged app bundles keep their normal Keychain behavior
regardless of signing mode.
When the script falls back to ad-hoc signing, it preserves CodexBar-owned keychain state by default.
That means you may still see keychain prompts for existing CodexBar cache entries, but allowing those prompts keeps the
cached browser/OAuth state available across normal rebuilds.
If you want a clean reset of CodexBar-owned keychain state for an ad-hoc build, run
`./Scripts/compile_and_run.sh --clear-adhoc-keychain` before relaunching.
Third-party keychain items still need stable signing if you want macOS to remember **Always Allow** across rebuilds.
### Quick Build (No Tests)
```bash
swift build -c release
./Scripts/package_app.sh
```
### Run Tests Only
```bash
make test
```
### Debug Build
```bash
swift build # defaults to debug
./Scripts/package_app.sh debug
```
---
## Troubleshooting
### "CodexBar is already running"
The compile_and_run script should kill old instances, but if it doesn't:
```bash
pkill -x CodexBar || pkill -f CodexBar.app || true
```
### "Permission denied" when accessing keychain
Make sure you clicked **"Always Allow"** or set up the development certificate (see above).
### Multiple app bundles keep appearing
This happens when the running app locks the bundle. The compile_and_run script handles this by killing the app first.
If you still see old bundles:
```bash
rm -rf "CodexBar "*.app
```
### App doesn't reflect latest changes
Always rebuild and restart:
```bash
./Scripts/compile_and_run.sh
```
Or manually:
```bash
./Scripts/package_app.sh
pkill -x CodexBar || pkill -f CodexBar.app || true
open -n CodexBar.app
```
+257
View File
@@ -0,0 +1,257 @@
---
summary: "Fork quick start: differences, commands, and planned features."
read_when:
- Onboarding to the fork workflow
- Reviewing fork-specific changes
- Running fork maintenance commands
---
# CodexBar Fork - Quick Start Guide
**Fork Maintainer:** Brandon Charleson ([topoffunnel.com](https://topoffunnel.com))
**Original Author:** Peter Steinberger ([steipete](https://twitter.com/steipete))
**Fork Repository:** https://github.com/topoffunnel/CodexBar
---
## 🎯 What Makes This Fork Different?
### Key Enhancements
1. **Augment Provider Support** - Full integration with Augment Code API
2. **Enhanced Security** - Improved keychain handling, no permission prompts
3. **Better Cookie Management** - Automatic session keepalive, Chrome Beta support
4. **Bug Fixes** - Cursor bonus credits, cookie domain filtering
### Planned Features
- Multi-account management per provider
- Enhanced diagnostics and logging
- Upstream sync automation
- Usage history tracking
---
## 🚀 Quick Commands
### Development
```bash
# Build and run (kills old instances, builds, tests, packages, relaunches)
./Scripts/compile_and_run.sh
# Quick build
swift build
# Run tests
make test
# Format code
swiftformat Sources Tests
swiftlint --strict
# Package app
./Scripts/package_app.sh
# Restart app after rebuild
pkill -x CodexBar || pkill -f CodexBar.app || true
cd /Users/steipete/Projects/codexbar && open -n /Users/steipete/Projects/codexbar/CodexBar.app
```
### Release
```bash
# Edit .mac-release.env first: MAC_RELEASE_REPO, feed URL, download URL,
# bundle id, and Sparkle public/signing key must point at your fork.
./Scripts/release.sh
# See full release process
cat docs/RELEASING.md
```
### Git Workflow
```bash
# Check status
git status
# Create feature branch
git checkout -b feature/my-feature
# Commit changes
git add -A
git commit -m "feat: description"
# Push to fork
git push origin feature/my-feature
# Sync with upstream (TBD - see docs/FORK_ROADMAP.md Phase 4)
```
---
## 📁 Key Files & Directories
### Source Code
- `Sources/CodexBar/` - Swift 6 menu bar app
- `Sources/CodexBarCore/` - Core logic, providers, utilities
- `Sources/CodexBarCore/Providers/Augment/` - Augment provider implementation
- `Tests/CodexBarTests/` - XCTest coverage
### Scripts
- `Scripts/compile_and_run.sh` - Main development script
- `Scripts/package_app.sh` - Package app bundle
- `Scripts/sign-and-notarize.sh` - Release signing
- `Scripts/make_appcast.sh` - Generate appcast XML
### Documentation
- `docs/augment.md` - Augment provider guide
- `docs/FORK_ROADMAP.md` - Development roadmap
- `docs/RELEASING.md` - Release process
- `docs/DEVELOPMENT.md` - Build instructions
- `README.md` - Main documentation
---
## 🔧 Common Tasks
### Adding a New Feature
1. Create feature branch: `git checkout -b feature/my-feature`
2. Make changes in `Sources/`
3. Add tests in `Tests/`
4. Run `./Scripts/compile_and_run.sh` to verify
5. Run `swiftformat Sources Tests && swiftlint --strict`
6. Commit with descriptive message
7. Push and create PR
### Debugging Augment Issues
1. Enable debug logging: `export CODEXBAR_LOG_LEVEL=debug`
2. Check Console.app for "com.steipete.codexbar"
3. Use Settings → Debug → Augment → Show Debug Info
4. Check `docs/augment.md` troubleshooting section
### Testing Changes
```bash
# Run all tests
make test
# Run specific test
swift test --filter AugmentTests
# Build and test together
./Scripts/compile_and_run.sh --test
```
### Updating Documentation
1. Edit relevant `.md` file in `docs/`
2. Update `README.md` if needed
3. Commit with `docs:` prefix
4. No need to rebuild app
---
## 🐛 Troubleshooting
### App Won't Launch
```bash
# Kill all instances
pkill -x CodexBar || pkill -f CodexBar.app || true
# Rebuild and relaunch
./Scripts/compile_and_run.sh
```
### Build Errors
```bash
# Clean build
swift package clean
swift build
# Check for format issues
swiftformat Sources Tests --lint
swiftlint --strict
```
### Cookie Issues (Augment)
1. Check browser is logged into app.augmentcode.com
2. Verify cookie source in Settings → Providers → Augment
3. Try manual cookie import (see `docs/augment.md`)
4. Check debug logs for cookie import details
### Keychain Permission Prompts
- This fork includes fixes to eliminate prompts
- If you still see prompts, check `Sources/CodexBarCore/Keychain/`
- Ensure you're running the latest build
---
## 📚 Learning Resources
### Understanding the Codebase
1. Start with `Sources/CodexBar/CodexbarApp.swift` - App entry point
2. Review `Sources/CodexBarCore/UsageStore.swift` - Main state management
3. Check `Sources/CodexBarCore/Providers/` - Provider implementations
4. Read `docs/provider.md` - Provider authoring guide
### Swift 6 & SwiftUI
- Uses `@Observable` macro (not `ObservableObject`)
- Prefer `@State` ownership over `@StateObject`
- Use `@Bindable` in views for two-way binding
- Strict concurrency checking enabled
### Coding Style
- 4-space indentation
- 120-character line limit
- Explicit `self` is intentional (don't remove)
- Follow existing `MARK` organization
- Use descriptive variable names
---
## 🤝 Contributing
### To This Fork
1. Fork the fork repository
2. Create feature branch
3. Make changes with tests
4. Submit PR to `topoffunnel/CodexBar`
### To Upstream
1. Check if feature benefits all users
2. Create PR to `steipete/CodexBar`
3. Reference this fork if relevant
4. Be patient with review process
See `docs/FORK_ROADMAP.md` for contribution strategy.
---
## 📞 Support
### Fork-Specific Issues
- GitHub Issues: https://github.com/topoffunnel/CodexBar/issues
- Email: [your-email]@topoffunnel.com
### Upstream Issues
- GitHub Issues: https://github.com/steipete/CodexBar/issues
- Twitter: [@steipete](https://twitter.com/steipete)
---
## 📋 Next Steps
1. **Read the Roadmap:** `docs/FORK_ROADMAP.md`
2. **Set Up Development:** `./Scripts/compile_and_run.sh`
3. **Review Augment Docs:** `docs/augment.md`
4. **Check Current Issues:** GitHub Issues tab
5. **Join Development:** Pick a task from Phase 2-5
---
## 🎉 Quick Wins
Want to contribute but not sure where to start? Try these:
- [ ] Add more test coverage for Augment provider
- [ ] Improve error messages in cookie import
- [ ] Add screenshots to `docs/augment.md`
- [ ] Test on different macOS versions
- [ ] Report bugs you find
- [ ] Suggest UI improvements
Happy coding! 🚀
+232
View File
@@ -0,0 +1,232 @@
---
summary: "Fork roadmap: phases, milestones, and planned improvements."
read_when:
- Planning fork work
- Reviewing fork milestones
---
# CodexBar Fork Roadmap
This document outlines the development roadmap for the CodexBar fork maintained by Brandon Charleson.
## ✅ Phase 1: Fork Identity (COMPLETE)
**Status:** Completed Jan 4, 2026
**Achievements:**
- Established dual attribution in About section
- Updated README with fork notice and enhancements
- Created comprehensive Augment provider documentation
- App builds and runs successfully
**Commit:** `da3d13e` - "feat: establish fork identity with dual attribution"
---
## 🔧 Phase 2: Enhanced Augment Diagnostics
**Goal:** Fix persistent cookie disconnection issues with better logging and diagnostics
**Tasks:**
1. **Replace print() with proper logging**
- Use `CodexBarLog.logger("augment")` throughout
- Add structured metadata for debugging
- Follow patterns from Claude/Cursor providers
2. **Enhanced Cookie Diagnostics**
- Log cookie expiration times
- Track cookie refresh attempts
- Add cookie domain filtering diagnostics
- Log browser source priority
3. **Session Keepalive Monitoring**
- Add keepalive status to debug pane
- Log refresh attempts and success/failure
- Track time until next refresh
- Add manual "Force Refresh" button
4. **Debug Pane Improvements**
- Add "Cookie Status" section showing:
- Current cookies and expiration
- Last successful import
- Browser source used
- Keepalive status
- Add "Test Connection" button
- Show detailed error messages
**Files to Modify:**
- `Sources/CodexBarCore/Providers/Augment/AugmentStatusProbe.swift`
- `Sources/CodexBarCore/Providers/Augment/AugmentSessionKeepalive.swift`
- `Sources/CodexBar/UsageStore.swift` (debug pane)
---
## 🎯 Phase 3: Quotio Feature Analysis
**Goal:** Identify and cherry-pick valuable features from Quotio without copying code
**Analysis Areas:**
1. **Multi-Account Management**
- How Quotio handles multiple accounts per provider
- Account switching UI patterns
- Account status indicators
2. **OAuth Flow Improvements**
- Quotio's OAuth implementation patterns
- Token refresh mechanisms
- Error handling strategies
3. **UI/UX Patterns**
- Menu bar organization
- Settings layout
- Status indicators
- Notification patterns
4. **Session Management**
- How Quotio handles session persistence
- Cookie refresh strategies
- Automatic reconnection logic
**Deliverable:** `docs/QUOTIO_ANALYSIS.md` with:
- Feature comparison matrix
- Implementation recommendations
- Priority ranking
- Effort estimates
---
## 🔄 Phase 4: Upstream Sync Workflow
**Goal:** Set up automated workflow to sync with upstream while maintaining fork changes
**Tasks:**
1. **Create Sync Script**
- `Scripts/sync_upstream.sh`
- Fetch upstream changes
- Show diff summary
- Interactive merge/rebase
2. **Conflict Resolution Guide**
- Document common conflict areas
- Resolution strategies
- Testing checklist
3. **Automated Checks**
- CI workflow to detect upstream changes
- Weekly sync reminders
- Compatibility testing
**Files to Create:**
- `Scripts/sync_upstream.sh`
- `docs/UPSTREAM_STRATEGY.md`
- `.github/workflows/upstream-sync-check.yml`
---
## 🚀 Phase 5: Multi-Account Management Foundation
**Goal:** Implement multi-account support for providers (starting with Augment)
**Features:**
1. **Account Management UI**
- Add/remove accounts per provider
- Account nicknames/labels
- Active account indicator
- Quick account switching
2. **Account Storage**
- Keychain-based account storage
- Account metadata (email, plan, last used)
- Secure credential isolation
3. **Account Switching**
- Switch active account from menu
- Preserve per-account usage history
- Automatic account selection based on quota
4. **UI Enhancements**
- Account dropdown in menu bar
- Per-account usage display
- Account health indicators
**Implementation Plan:**
1. Start with Augment provider (already has cookie infrastructure)
2. Create `AccountManager` service
3. Update `UsageStore` to handle multiple accounts
4. Add account switcher to menu bar
5. Extend to other providers (Claude, Cursor, etc.)
**Files to Create:**
- `Sources/CodexBarCore/AccountManager.swift`
- `Sources/CodexBarCore/Providers/Augment/AugmentAccountManager.swift`
- `Sources/CodexBar/AccountSwitcherView.swift`
---
## 📋 Future Enhancements
### Short Term (1-2 weeks)
- [ ] Augment cookie issue resolution (Phase 2)
- [ ] Quotio feature analysis (Phase 3)
- [ ] Upstream sync workflow (Phase 4)
### Medium Term (1-2 months)
- [ ] Multi-account management (Phase 5)
- [ ] Enhanced notification system
- [ ] Usage history tracking
- [ ] Export usage data
### Long Term (3+ months)
- [ ] Custom provider API
- [ ] Usage predictions/alerts
- [ ] Cost optimization suggestions
- [ ] Team usage aggregation
---
## 🤝 Upstream Contribution Strategy
**When to Contribute Upstream:**
- Bug fixes that benefit all users
- Provider improvements (non-fork-specific)
- Documentation improvements
- Performance optimizations
**When to Keep in Fork:**
- Multi-account management (major architectural change)
- Fork-specific branding/attribution
- Experimental features
- Features specific to topoffunnel.com users
**PR Guidelines:**
- Keep PRs focused and small
- Include comprehensive tests
- Follow upstream coding style
- Document breaking changes
- Be patient with review process
---
## 📊 Success Metrics
**Technical:**
- Zero cookie disconnection issues
- < 1 second menu bar response time
- 100% test coverage for new features
- Zero regressions from upstream syncs
**User:**
- Positive feedback from topoffunnel.com users
- Active usage metrics
- Feature requests and engagement
- Community contributions
---
## 🔗 Related Documentation
- [Augment Provider](augment.md) - Augment-specific documentation
- [Development Guide](DEVELOPMENT.md) - Build and test instructions
- [Provider Authoring](provider.md) - How to create new providers
- [Upstream Strategy](UPSTREAM_STRATEGY.md) - Syncing with original repository
- [Quotio Analysis](QUOTIO_ANALYSIS.md) - Feature comparison (TBD)
+334
View File
@@ -0,0 +1,334 @@
---
summary: "Fork setup: remote configuration and multi-upstream workflow."
read_when:
- Setting up fork remotes
- Syncing with upstreams
---
# Fork Setup & Initial Configuration
**One-time setup for managing your CodexBar fork with multiple upstreams**
---
## 🎯 Quick Setup
### Step 1: Configure Git Remotes
```bash
# Verify your fork is origin
git remote -v
# Should show: origin git@github.com:topoffunnel/CodexBar.git
# Add upstream (steipete's original)
git remote add upstream https://github.com/steipete/CodexBar.git
# Add quotio (inspiration source)
git remote add quotio https://github.com/nguyenphutrong/quotio.git
# Fetch all remotes
git fetch --all
# Verify setup
git remote -v
# Should show:
# origin git@github.com:topoffunnel/CodexBar.git (fetch/push)
# upstream https://github.com/steipete/CodexBar.git (fetch/push)
# quotio https://github.com/nguyenphutrong/quotio.git (fetch/push)
```
### Step 2: Test Automation Scripts
```bash
# Make scripts executable (if not already)
chmod +x Scripts/*.sh
# Test upstream monitoring
./Scripts/check_upstreams.sh
# Should show:
# - Number of new commits in upstream
# - Number of new commits in quotio
# - File change summary
```
### Step 3: Initial Upstream Review
```bash
# Check what's new in upstream
./Scripts/check_upstreams.sh upstream
# Review changes in detail
./Scripts/review_upstream.sh upstream
# This creates a review branch: upstream-sync/upstream-YYYYMMDD
```
### Step 4: Initial Quotio Analysis
```bash
# Analyze quotio repository
./Scripts/analyze_quotio.sh
# Creates: quotio-analysis-YYYYMMDD.md
# Review the file for interesting patterns
```
---
## ⚠️ Critical Discovery: Upstream Removed Augment
**IMPORTANT:** Upstream (steipete) has removed the Augment provider in recent commits!
```
Files changed:
.../Providers/Augment/AugmentStatusProbe.swift | 627 deletions
Tests/CodexBarTests/AugmentStatusProbeTests.swift | 88 deletions
```
**This validates our fork strategy:**
- ✅ Your fork preserves Augment support
- ✅ You can continue developing Augment features
- ✅ Upstream changes won't break your Augment work
- ✅ You maintain features important to your users
**Action Required:**
When syncing with upstream, you'll need to:
1. Cherry-pick valuable changes (Vertex AI improvements, bug fixes)
2. **Avoid** merging commits that remove Augment
3. Keep your Augment implementation separate
---
## 🔄 Regular Workflow
### Weekly Upstream Check (Recommended: Monday)
```bash
# Check for new changes
./Scripts/check_upstreams.sh
# If changes found, review them
./Scripts/review_upstream.sh upstream
# Cherry-pick valuable commits (skip Augment removal)
git cherry-pick <commit-hash>
# Test
./Scripts/compile_and_run.sh
# Merge to main
git checkout main
git merge upstream-sync/upstream-$(date +%Y%m%d)
```
### Weekly Quotio Review (Recommended: Thursday)
```bash
# Analyze recent quotio changes
./Scripts/analyze_quotio.sh
# Review specific files of interest
git show quotio/main:path/to/interesting/file.swift
# Document patterns in docs/QUOTIO_ANALYSIS.md
```
---
## 📋 Selective Sync Strategy
### What to Sync from Upstream
**DO sync:**
- Bug fixes (non-Augment)
- Performance improvements
- New provider support (Vertex AI, etc.)
- Documentation improvements
- Test improvements
- Dependency updates
**DON'T sync:**
- Augment provider removal
- Changes that conflict with fork features
- Breaking changes without careful review
### How to Cherry-Pick Selectively
```bash
# Review upstream commits
git log --oneline main..upstream/main
# Example output:
# 001019c style: fix swiftformat violations ✅ SYNC
# e4f1e4c feat(vertex): add token cost tracking ✅ SYNC
# 202efde fix(vertex): disable double-counting ✅ SYNC
# 0c2f888 docs: add Vertex AI documentation ✅ SYNC
# 3c4ca30 feat(vertexai): token cost tracking ✅ SYNC
# abc123d refactor: remove Augment provider ❌ SKIP
# Cherry-pick the good ones
git cherry-pick 001019c
git cherry-pick e4f1e4c
git cherry-pick 202efde
git cherry-pick 0c2f888
git cherry-pick 3c4ca30
# Skip abc123d (Augment removal)
```
---
## 🎨 Quotio Pattern Learning
### Ethical Guidelines
**DO:**
- ✅ Analyze their architecture and patterns
- ✅ Learn from their UX decisions
- ✅ Understand their approach to problems
- ✅ Implement similar concepts independently
- ✅ Credit inspiration in commits
**DON'T:**
- ❌ Copy code verbatim
- ❌ Use their assets or branding
- ❌ Violate their license
- ❌ Claim their work as yours
### Analysis Workflow
```bash
# 1. Fetch latest quotio
git fetch quotio
# 2. Analyze structure
./Scripts/analyze_quotio.sh
# 3. Review specific areas
git show quotio/main:path/to/AccountManager.swift
# 4. Document patterns (not code!)
# Edit docs/QUOTIO_ANALYSIS.md
# 5. Implement independently
# Create feature branch
git checkout -b quotio-inspired/multi-account
# 6. Commit with attribution
git commit -m "feat: multi-account management
Inspired by quotio's account switching pattern:
https://github.com/nguyenphutrong/quotio/...
Implemented independently using CodexBar architecture."
```
---
## 🚀 Contributing to Upstream
### When to Contribute
**Good candidates:**
- Universal bug fixes
- Performance improvements
- Documentation improvements
- Test coverage
- Provider enhancements (non-fork-specific)
**Keep in fork:**
- Augment provider (they removed it)
- Multi-account management (major change)
- Fork branding
- Experimental features
### Contribution Workflow
```bash
# 1. Prepare clean branch from upstream
./Scripts/prepare_upstream_pr.sh fix-cursor-bonus
# 2. Cherry-pick your fix (without fork branding)
git cherry-pick <your-commit-hash>
# 3. Review - ensure no fork-specific code
git diff upstream/main
# 4. Test
make test
# 5. Push to your fork
git push origin upstream-pr/fix-cursor-bonus
# 6. Create PR on GitHub
# Go to: https://github.com/steipete/CodexBar
# Click "New Pull Request"
# Select: base: steipete:main <- compare: topoffunnel:upstream-pr/fix-cursor-bonus
```
---
## 🤖 Automated Monitoring
### GitHub Actions Setup
The workflow `.github/workflows/upstream-monitor.yml` will:
- Run Monday and Thursday at 9 AM UTC
- Check for new commits in both upstreams
- Create/update GitHub issue with summary
- Provide links to review changes
**To enable:**
1. Push the workflow file to your fork
2. Enable GitHub Actions in repository settings
3. Issues will be created automatically
**Manual trigger:**
```bash
# Via GitHub UI: Actions → Monitor Upstream Changes → Run workflow
```
---
## 📊 Verification Checklist
After setup, verify:
- [ ] All three remotes configured (origin, upstream, quotio)
- [ ] Scripts are executable
- [ ] `./Scripts/check_upstreams.sh` runs successfully
- [ ] Can create review branch with `./Scripts/review_upstream.sh`
- [ ] Can analyze quotio with `./Scripts/analyze_quotio.sh`
- [ ] GitHub Actions workflow is present
- [ ] Understand Augment removal in upstream
- [ ] Know how to cherry-pick selectively
- [ ] Know when to contribute upstream vs keep in fork
---
## 🔗 Next Steps
1. **Review Current Upstream Changes**
```bash
./Scripts/review_upstream.sh upstream
```
2. **Decide on Sync Strategy**
- Which commits to cherry-pick?
- How to handle Augment removal?
- See `docs/UPSTREAM_STRATEGY.md`
3. **Start Quotio Analysis**
```bash
./Scripts/analyze_quotio.sh
# Then edit docs/QUOTIO_ANALYSIS.md
```
4. **Update Fork Roadmap**
- Review `docs/FORK_ROADMAP.md`
- Adjust based on upstream changes
- Plan fork-specific features
---
**Setup Complete!** You now have a robust system for managing your fork while learning from multiple sources.
+199
View File
@@ -0,0 +1,199 @@
---
summary: "Issue labeling policy for triage, prioritization, and backlog hygiene."
read_when:
- Triageing GitHub issues
- Adding or updating issue labels
- Organizing the backlog
---
# Issue labeling guide
This repo uses labels to make the issue tracker easier to scan by:
- **type** — what kind of issue is this?
- **priority** — how urgent is it?
- **area** — what subsystem is affected?
- **provider** — which provider/service is involved?
- **workflow state** — what kind of follow-up is needed?
The goal is not to perfectly label everything. The goal is to make open issues easy to sort into:
- what is broken now,
- what needs maintainer attention,
- what is accepted backlog,
- and what belongs to a specific provider or subsystem.
## Labeling rules
For most open issues, aim to apply:
- **1 type label**
- **1 priority label**
- **1 workflow label**
- **1 area label**
- **01 provider labels**
That means most issues should end up with **35 labels max**.
## Type labels
Use the existing GitHub-style labels:
- `bug` — broken behavior, crash, mismatch, false negative, bad parsing, auth failure
- `enhancement` — feature request, UX improvement, support for a new workflow
- `documentation` — docs, onboarding, missing setup guidance
- `question` — only for issues that are primarily asking for clarification or support
Avoid using `question` as a generic fallback when the issue is actually a bug or feature request.
## Priority labels
- `priority:high` — crashes, install failures, auth/account breakage, provider unusable, severe resource issues
- `priority:medium` — real issue or good feature request, but not urgent
- `priority:low` — minor polish, optional UX improvements, long-tail backlog
## Workflow labels
- `needs-triage` — new issue that has not been categorized yet
- `needs-repro` — needs logs, screenshots, exact steps, or a current repro
- `needs-design` — valid request, but needs a product/UX decision before implementation
- `blocked-upstream` — likely caused or limited by upstream provider behavior
- `accepted` — intentionally kept open as part of the backlog/roadmap
## Area labels
- `area:auth-keychain` — keychain prompts, login state, token refresh, account switching
- `area:install-distribution` — Homebrew, packaging, launch/install failures, binary detection
- `area:usage-accuracy` — usage %, reset windows, plan parsing, cost/token math
- `area:performance` — CPU, battery, memory, background sessions/process churn
- `area:ui-ux` — menu bar behavior, settings, copy, visual layout, interaction polish
- `area:widget` — widget registration, app groups, widget gallery visibility
- `area:docs-onboarding` — setup docs, onboarding docs, missing instructions
- `area:notifications` — threshold alerts, prompt waiting, quota notifications
- `area:export-integration` — Prometheus, HTTP server mode, external integrations
- `area:accounts` — multiple accounts, account discovery, account switching UX
## Provider labels
Only apply one when a provider is clearly the main subject:
- `provider:claude`
- `provider:codex`
- `provider:cursor`
- `provider:copilot`
- `provider:gemini`
- `provider:alibaba`
- `provider:factory`
- `provider:antigravity`
- `provider:opencode`
- `provider:zai`
- `provider:openrouter`
Not every issue needs a provider label.
## Close-time labels
These are mostly useful when resolving issues, not as backlog-organizing labels:
- `duplicate`
- `invalid`
- `wontfix`
- `stale`
## Recommended minimum viable label set
If starting from a sparse tracker, add these first:
### Priority
- `priority:high`
- `priority:medium`
- `priority:low`
### Workflow
- `needs-triage`
- `needs-repro`
- `needs-design`
- `accepted`
### Area
- `area:auth-keychain`
- `area:install-distribution`
- `area:usage-accuracy`
- `area:performance`
- `area:ui-ux`
- `area:widget`
- `area:docs-onboarding`
### Provider
- `provider:claude`
- `provider:codex`
- `provider:cursor`
- `provider:copilot`
This smaller set already gives most of the value.
## Examples
### Example 1 — severe Claude keychain issue
Issue: repeated Claude keychain prompts, user cant keep the app running normally.
Suggested labels:
- `bug`
- `priority:high`
- `area:auth-keychain`
- `provider:claude`
### Example 2 — roadmap feature
Issue: multiple account support.
Suggested labels:
- `enhancement`
- `priority:high`
- `area:accounts`
- `needs-design`
### Example 3 — needs better repro
Issue: generic usage mismatch with unclear screenshots and no exact values.
Suggested labels:
- `bug`
- `priority:medium`
- `area:usage-accuracy`
- `needs-repro`
### Example 4 — accepted backlog UI request
Issue: show burn rate / pacing indicators.
Suggested labels:
- `enhancement`
- `priority:medium`
- `area:usage-accuracy`
- `accepted`
## Suggested rollout
1. **Create the new labels**
2. **Backfill the top-priority open issues first**
- all `priority:high` bugs
- major roadmap items
- maintainer-triage issues
3. **Apply labels to new issues at intake**
4. **Backfill older backlog issues gradually**
## Practical guidance
- Prefer **fewer, clearer labels** over many vague labels.
- Do not label everything `question`.
- Do not use both `needs-repro` and `accepted` on the same issue unless there is a strong reason.
- If an issue is provider-specific, add the provider label early.
- If an issue is obviously real and intended to stay open, add `accepted` so it doesnt look abandoned.
## Current workflow-specific labels
These already exist and should stay scoped to their current purpose:
- `upstream-sync`
- `needs-review`
- `changes requested`
They should not replace the general issue triage labels above.
+134
View File
@@ -0,0 +1,134 @@
---
summary: "Current keychain behavior: legacy migration, Claude OAuth keychain bootstrap, and prompt mitigation."
read_when:
- Investigating Keychain prompts
- Auditing Claude OAuth keychain behavior
- Comparing legacy keychain docs vs current architecture
---
# Keychain Fix: Current State
## Scope change from the original doc
The original fix (migrating legacy CodexBar keychain items to `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`) is
still in place, but the architecture has changed:
- Provider settings and manual secrets are now persisted in `~/.codexbar/config.json`.
- Legacy keychain stores are still present mainly to migrate old installs, then clear old items.
- Keychain is still used for runtime cache entries (for example `com.steipete.codexbar.cache`) and Claude OAuth
bootstrap reads from Claude CLI keychain (`Claude Code-credentials`).
## Then vs now
| Previous statement in this doc | Current behavior |
| --- | --- |
| CodexBar stores provider credentials only in keychain | Manual/provider settings are config-file backed (`~/.codexbar/config.json`), while keychain is still used for runtime caches and Claude OAuth bootstrap fallback. |
| `ClaudeOAuthCredentials.swift` migrated CodexBar-owned Claude OAuth keychain items | Claude OAuth primary source is Claude CLI keychain service (`Claude Code-credentials`), with CodexBar cache in `com.steipete.codexbar.cache` (`oauth.claude`). |
| Migration runs in `CodexBarApp.init()` | Migration runs in `HiddenWindowView` `.task` via detached task (`KeychainMigration.migrateIfNeeded()`). |
| Post-migration prompts should be zero in all Claude paths | Legacy-store prompts are reduced; Claude OAuth bootstrap can still prompt when reading Claude CLI keychain, with cooldown + no-UI probes to prevent storms. |
| Log category is `KeychainMigration` | Category is `keychain-migration` (kebab-case). |
## Current keychain surfaces for Claude
### 1. Legacy CodexBar keychain migration (V1)
`Sources/CodexBar/KeychainMigration.swift` migrates legacy `com.steipete.CodexBar` items (for example
`claude-cookie`) to `AfterFirstUnlockThisDeviceOnly`.
- Gate key: `KeychainMigrationV1Completed`
- Runs once unless flag is reset.
- Covers legacy CodexBar-managed accounts only (not Claude CLI's own keychain service).
### 2. Claude OAuth bootstrap path
`Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift`
Load order for credentials:
1. Environment override (`CODEXBAR_CLAUDE_OAUTH_TOKEN`, scopes env key).
2. In-memory cache.
3. CodexBar keychain cache (`com.steipete.codexbar.cache`, account `oauth.claude`).
4. `~/.claude/.credentials.json`.
5. Claude CLI keychain service: `Claude Code-credentials` (promptable fallback).
Prompt mitigation:
- Non-interactive keychain probes use `KeychainNoUIQuery` with `LAContext.interactionNotAllowed`.
- Pre-alert is shown only when preflight suggests interaction may be required.
- Denials are cooled down in the background via `claudeOAuthKeychainDeniedUntil`
(`ClaudeOAuthKeychainAccessGate`). User actions (menu open / manual refresh) clear this cooldown.
- Auto-mode availability checks use non-interactive loads with prompt cooldown respected.
- Background cache-sync-on-change also performs non-interactive Claude keychain probes (`syncWithClaudeKeychainIfChanged`)
and can update cached OAuth data when the token changes.
### Why two Claude keychain prompts can still happen on startup
When CodexBar does not have usable OAuth credentials in its own cache (`com.steipete.codexbar.cache` / `oauth.claude`),
bootstrap falls through to Claude CLI keychain reads.
Current flow can perform up to two interactive reads in one bootstrap call:
1. Interactive read of the newest discovered keychain candidate.
2. If that does not return usable data, interactive legacy service-level fallback read.
On some macOS keychain/ACL states, pressing **Allow** (session-only) for the first read does not grant enough access
for the second read shape, so macOS prompts again. Pressing **Always Allow** usually authorizes both query shapes for
the app identity and avoids the immediate second prompt.
The prompt copy differs because Security.framework is authorizing different operations:
- one path is a direct secret-data read for the key item,
- the fallback path is a key/service access query.
This is OS/keychain ACL behavior, not a `ThisDeviceOnly` migration issue.
### 3. Claude web cookie cache
`Sources/CodexBarCore/CookieHeaderCache.swift` and `Sources/CodexBarCore/KeychainCacheStore.swift`
- Browser-imported Claude session cookies are cached in keychain service `com.steipete.codexbar.cache`.
- Account key is `cookie.claude`.
- Cache writes use `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`.
- Users can clear browser-cookie cache entries from **Preferences → Debug → Caches** or with
`codexbar cache clear --cookies`. `--provider <id>` scopes cookie clearing to one provider and includes scoped
Codex managed-account cookie keys.
## What still uses `ThisDeviceOnly`
- Legacy store implementations (`CookieHeaderStore`, token stores, MiniMax stores) still write using
`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`.
- Keychain cache store (`com.steipete.codexbar.cache`) also writes with `ThisDeviceOnly`.
## Disable keychain access behavior
`Advanced -> Disable Keychain access` sets `debugDisableKeychainAccess` and flips `KeychainAccessGate.isDisabled`.
Effects:
- Blocks keychain reads/writes in legacy stores.
- Disables keychain-backed cookie auto-import paths.
- Forces cookie source resolution to manual/off where applicable.
## Verification
### Check legacy migration flag
```bash
defaults read com.steipete.codexbar KeychainMigrationV1Completed
```
### Check Claude OAuth keychain cooldown
```bash
defaults read com.steipete.codexbar claudeOAuthKeychainDeniedUntil
```
### Inspect keychain-related logs
```bash
log show --predicate 'subsystem == "com.steipete.codexbar" && (category == "keychain-migration" || category == "keychain-preflight" || category == "keychain-prompt" || category == "keychain-cache" || category == "claude-usage" || category == "cookie-cache")' --last 10m
```
### Reset migration for local testing
```bash
defaults delete com.steipete.codexbar KeychainMigrationV1Completed
./Scripts/compile_and_run.sh
```
## Key files (current)
- `Sources/CodexBar/KeychainMigration.swift`
- `Sources/CodexBar/HiddenWindowView.swift`
- `Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift`
- `Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthKeychainAccessGate.swift`
- `Sources/CodexBarCore/KeychainAccessPreflight.swift`
- `Sources/CodexBarCore/KeychainNoUIQuery.swift`
- `Sources/CodexBarCore/KeychainCacheStore.swift`
- `Sources/CodexBarCore/CookieHeaderCache.swift`
+352
View File
@@ -0,0 +1,352 @@
---
summary: "Quotio analysis: UX and architecture patterns for inspiration."
read_when:
- Evaluating external inspiration
- Planning UX or architecture improvements
---
# Quotio Analysis & Pattern Adaptation
**Purpose:** Learn from quotio's implementation patterns without copying code
**Repository:** https://github.com/nguyenphutrong/quotio
**Approach:** Analyze patterns, implement independently
---
## 🎯 Analysis Goals
### What We're Looking For
1. **UI/UX Patterns** - Menu organization, settings layout, status displays
2. **Multi-Account Management** - How they handle multiple accounts per provider
3. **Session Management** - Cookie handling, OAuth flows, session persistence
4. **Provider Architecture** - How providers are structured and managed
5. **Error Handling** - User-friendly error messages and recovery
6. **Performance Optimizations** - Caching, background updates, efficiency
### What We're NOT Doing
- ❌ Copying code verbatim
- ❌ Replicating their exact UI
- ❌ Using their assets or branding
- ❌ Violating their license
### What We ARE Doing
- ✅ Learning from their architectural decisions
- ✅ Understanding their UX patterns
- ✅ Adapting concepts to CodexBar conventions
- ✅ Implementing independently with our own code
- ✅ Crediting inspiration appropriately
---
## 🔍 Analysis Process
### Step 1: Repository Overview
```bash
# Fetch latest quotio
./Scripts/analyze_quotio.sh
# Review file structure
git ls-tree -r --name-only quotio/main | grep -E '\.(swift|md)$'
# Check recent activity
git log --oneline --graph quotio/main --since="30 days ago"
```
### Step 2: Feature Comparison
Create a comparison matrix:
| Feature | CodexBar | Quotio | Notes |
|---------|----------|--------|-------|
| Multi-account | ❌ | ✅ | Priority for fork |
| Provider count | 5 | ? | Check quotio |
| Cookie import | ✅ | ? | Compare approaches |
| OAuth support | ✅ | ? | Compare flows |
| Session keepalive | ✅ | ? | Compare strategies |
| Menu bar UI | Basic | ? | Compare organization |
| Settings UI | Tabs | ? | Compare layout |
### Step 3: Deep Dive Areas
#### Multi-Account Management
```bash
# Find account-related files
git ls-tree -r --name-only quotio/main | grep -i account
# View implementation (read-only)
git show quotio/main:path/to/AccountManager.swift
# Document patterns in this file (see below)
```
**Questions to Answer:**
- How are accounts stored? (Keychain, file, database?)
- How is the active account selected?
- How does UI show multiple accounts?
- How are credentials isolated per account?
- How does account switching work?
#### Session Management
```bash
# Find session-related files
git ls-tree -r --name-only quotio/main | grep -iE '(session|cookie|auth)'
# Review implementation
git show quotio/main:path/to/SessionManager.swift
```
**Questions to Answer:**
- How are cookies refreshed?
- How is session expiration detected?
- How are multiple sessions managed?
- What's the keepalive strategy?
- How are errors handled?
#### UI/UX Patterns
```bash
# Find UI files
git ls-tree -r --name-only quotio/main | grep -iE '(view|menu|ui)'
# Review layouts
git show quotio/main:path/to/MenuBarView.swift
```
**Questions to Answer:**
- How is the menu bar organized?
- How are multiple accounts displayed?
- What status indicators are used?
- How are settings organized?
- What's the navigation pattern?
---
## 📊 Findings Template
### Feature: [Feature Name]
**Quotio Approach:**
- [Describe their implementation pattern]
- [Key architectural decisions]
- [Pros and cons]
**CodexBar Current State:**
- [What we have now]
- [Gaps or limitations]
**Adaptation Plan:**
- [How we'll implement similar functionality]
- [What we'll do differently]
- [Why our approach is better/different]
**Implementation Tasks:**
- [ ] Task 1
- [ ] Task 2
- [ ] Task 3
**Code Attribution:**
```swift
// Inspired by quotio's approach to [feature]:
// https://github.com/nguyenphutrong/quotio/blob/main/path/to/file
// Implemented independently using CodexBar patterns
```
---
## 🎨 Pattern Examples
### Example 1: Multi-Account UI Pattern
**Quotio Pattern (Observed):**
- Dropdown menu in menu bar
- Account nickname/email display
- Active account indicator
- Quick switch action
**CodexBar Adaptation:**
```swift
// Our implementation (example)
struct AccountSwitcherView: View {
@Bindable var store: UsageStore
var body: some View {
Menu {
ForEach(store.accounts) { account in
Button {
store.switchAccount(account)
} label: {
HStack {
Text(account.displayName)
if account.isActive {
Image(systemName: "checkmark")
}
}
}
}
} label: {
// Menu bar icon
}
}
}
// Inspired by quotio's account switching UI pattern
// Implemented using SwiftUI and CodexBar's UsageStore
```
### Example 2: Session Persistence Pattern
**Quotio Pattern (Observed):**
- Automatic session restoration
- Background refresh
- Error recovery
**CodexBar Adaptation:**
```swift
// Our implementation (example)
actor SessionPersistence {
func saveSession(_ session: SessionInfo) async throws {
// Our keychain-based approach
}
func restoreSession() async throws -> SessionInfo? {
// Our restoration logic
}
}
// Inspired by quotio's session persistence approach
// Implemented using Swift concurrency and CodexBar's keychain utilities
```
---
## 📋 Analysis Checklist
### Initial Review
- [ ] Clone/fetch quotio repository
- [ ] Review README and documentation
- [ ] Check license compatibility
- [ ] Identify main features
- [ ] Create feature comparison matrix
### Deep Dive
- [ ] Multi-account management
- [ ] Session/cookie handling
- [ ] UI/UX patterns
- [ ] Provider architecture
- [ ] Error handling
- [ ] Performance optimizations
### Documentation
- [ ] Document patterns (not code)
- [ ] Create adaptation plans
- [ ] Identify implementation tasks
- [ ] Prioritize features
- [ ] Estimate effort
### Implementation
- [ ] Implement independently
- [ ] Follow CodexBar conventions
- [ ] Add proper attribution
- [ ] Write tests
- [ ] Update documentation
---
## 🚀 Priority Features from Quotio
### High Priority
1. **Multi-Account Management**
- Status: Not started
- Effort: Large
- Value: High
- Dependencies: Account storage, UI updates
2. **Enhanced Session Management**
- Status: Partial (have keepalive)
- Effort: Medium
- Value: High
- Dependencies: None
3. **Improved Error Messages**
- Status: Basic
- Effort: Small
- Value: Medium
- Dependencies: None
### Medium Priority
4. **Menu Bar Organization**
- Status: Basic
- Effort: Medium
- Value: Medium
- Dependencies: Multi-account
5. **Settings Layout**
- Status: Functional
- Effort: Small
- Value: Low
- Dependencies: None
### Low Priority
6. **Additional Providers**
- Status: Have 5
- Effort: Varies
- Value: Medium
- Dependencies: Provider framework
---
## 📝 Notes & Observations
### General Observations
- [Add observations as you analyze]
- [Note interesting patterns]
- [Document questions]
### Architectural Differences
- [How quotio differs from CodexBar]
- [Pros and cons of each approach]
- [What we can learn]
### Implementation Ideas
- [Ideas sparked by quotio]
- [How to adapt to CodexBar]
- [Potential improvements]
---
## 🔗 Resources
- **Quotio Repository:** https://github.com/nguyenphutrong/quotio
- **Analysis Script:** `./Scripts/analyze_quotio.sh`
- **Review Command:** `git show quotio/main:path/to/file`
- **Diff Command:** `git diff main quotio/main -- path/to/file`
---
## ⚖️ Legal & Ethical Considerations
### License Compliance
- Quotio's license: [Check their LICENSE file]
- Our approach: Learn patterns, implement independently
- Attribution: Credit inspiration in commits and docs
### Ethical Guidelines
1. Never copy code verbatim
2. Understand the pattern before implementing
3. Implement using our own logic and style
4. Credit inspiration appropriately
5. Respect their intellectual property
### Attribution Format
```
Inspired by quotio's approach to [feature]:
https://github.com/nguyenphutrong/quotio/blob/main/path/to/file
Implemented independently using CodexBar patterns and conventions.
```
---
**Last Updated:** [Date]
**Analyzed By:** [Your Name]
**Status:** [In Progress / Complete]
+119
View File
@@ -0,0 +1,119 @@
---
summary: "CodexBar release checklist: package, sign, notarize, appcast, and asset validation."
read_when:
- Starting a CodexBar release
- Updating signing/notarization or appcast steps
- Validating release assets or Sparkle feed
---
# Release process (CodexBar)
SwiftPM-only; package/sign/notarize manually (no Xcode project). Sparkle feed is served from GitHub Releases. Checklist below merges Trimmys release flow with CodexBar specifics.
**Must read first:** open the master macOS release guide at `~/Projects/agent-scripts/docs/RELEASING-MAC.md` alongside this file and reconcile any differences in favor of CodexBar specifics before starting a release.
## Expectations
- When someone says “release CodexBar”, do the entire end-to-end flow: bump versions/CHANGELOG, build, sign and notarize, upload the zip to the GitHub release, generate/update the appcast with the new signature, publish the tag/release, and verify the enclosure URL responds with 200/OK and installs via Sparkle (no 404s or stale feeds).
### Release automation notes (Scripts/release.sh)
- Always forces a fresh build/notarization (no cached artifacts) before publishing.
- Fails fast if: git tree is dirty, the top changelog section is still “Unreleased” or mismatched, the target version already exists in the appcast, or the build number is not greater than the latest appcast entry.
- Sparkle key probe runs up front; appcast entry + signature verified automatically after generation.
- Release notes are extracted directly from the current changelog section and passed to the GitHub release (no manual notes flag needed).
- Sparkle appcast notes are generated as HTML from the same changelog section and embedded into the appcast entry.
- Requires tools/env on PATH: `swiftformat`, `swiftlint`, `swift`, `sign_update`, `generate_keys`, `generate_appcast`, `gh`, `python3`, `zip`, `curl`, plus `APP_STORE_CONNECT_*`. `SPARKLE_PRIVATE_KEY_FILE` is only needed when overriding the default Keychain Sparkle key.
## Prereqs
- Xcode 26+ installed at `/Applications/Xcode.app` (for ictool/iconutil and SDKs).
- Developer ID Application cert installed: `Developer ID Application: Peter Steinberger (Y5PE65HELJ)`.
- ASC API creds in env: `APP_STORE_CONNECT_API_KEY_P8`, `APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_ISSUER_ID`.
- Sparkle keys: public key expectation is in `.mac-release.env`; CodexBar still uses the older shared AGCY key, so the manifest includes the local Dropbox fallback path. `SPARKLE_PRIVATE_KEY_FILE` overrides it.
- Ensure shell has release env vars loaded (usually `source ~/.profile`) before running `Scripts/release.sh`.
- Shared release helper: `Scripts/mac-release` resolves `MAC_RELEASE_TOOL`, sibling `../agent-scripts`, or `~/Projects/agent-scripts`.
## Icon (glass .icon → .icns)
```
./Scripts/build_icon.sh Icon.icon CodexBar
```
Uses Xcodes `ictool` + transparent padding + iconset → Icon.icns.
## Build, sign, notarize (universal: arm64 + x86_64)
```
./Scripts/sign-and-notarize.sh
```
What it does:
- `swift build -c release --arch arm64` and `swift build -c release --arch x86_64`
- Packages `CodexBar.app` with Info.plist and Icon.icns
- Embeds Sparkle.framework, Updater, Autoupdate, XPCs
- Codesigns **everything** with runtime + timestamp (deep) and adds rpath
- Zips to `CodexBar-macos-universal-<version>.zip`
- Submits to notarytool, waits, staples, validates
Gotchas fixed:
- Sparkle needs signing for framework, Autoupdate, Updater, XPCs (Downloader/Installer) or notarization fails.
- Use `--timestamp` and `--deep` when signing the app to avoid invalid signature errors.
- Avoid `unzip` — it can add AppleDouble `._*` files that break the sealed signature and trigger “app is damaged”. Use Finder or `ditto -x -k CodexBar-<ver>.zip /Applications`. If Gatekeeper complains, delete the app bundle, re-extract with `ditto`, then `spctl -a -t exec` to verify.
- Manual sanity check before uploading: `find CodexBar.app -name '._*'` should return nothing; then `spctl --assess --type execute --verbose CodexBar.app` and `codesign --verify --deep --strict --verbose CodexBar.app` should both pass on the packaged bundle.
## Appcast (Sparkle)
After notarization, or let `Scripts/release.sh` do this:
```
./Scripts/make_appcast.sh CodexBar-macos-universal-0.1.0.zip \
https://raw.githubusercontent.com/steipete/CodexBar/main/appcast.xml
```
Generates HTML release notes from `CHANGELOG.md` (via `Scripts/changelog-to-html.sh`) and embeds them into the appcast entry.
Uploads not handled automatically—commit/publish appcast + zip to the feed location (GitHub Releases/raw URL).
## Tag & release
```
./Scripts/release.sh
```
## Homebrew (Cask)
CodexBar ships a Homebrew **Cask** in `../homebrew-tap`. When installed via Homebrew, CodexBar disables Sparkle and the app
must be updated via `brew`.
After publishing the GitHub release, `.github/workflows/release-cli.yml` builds the macOS, glibc Linux, and static musl Linux CLI tarballs for arm64 and x86_64, uploads them plus checksums, then dispatches the Homebrew tap update for both the CLI formula and app cask. Homebrew continues to use the glibc Linux assets. If the final dispatch is rate-limited, the tarballs and app zip may still be present; rerun or manually update the tap formula/cask from the published assets.
## Checklist (quick)
- [ ] Read both this file and `~/Projects/agent-scripts/docs/RELEASING-MAC.md`; resolve any conflicts toward CodexBars specifics.
- [ ] Update versions (scripts/Info.plist, CHANGELOG, About text) — changelog top section must be finalized; release script pulls notes from it automatically.
- [ ] `swiftformat`, `swiftlint`, `make test` (zero warnings/errors)
- [ ] `./Scripts/build_icon.sh` if icon changed
- [ ] `./Scripts/sign-and-notarize.sh`
- [ ] Generate Sparkle appcast via `Scripts/release.sh` or `Scripts/make_appcast.sh`; use `SPARKLE_PRIVATE_KEY_FILE` only if overriding Keychain signing.
- Upload the dSYM archive alongside the app zip on the GitHub release; the release script now automates this and will fail if its missing.
- After publishing the release and the Release CLI workflow finishes, run `Scripts/check-release-assets.sh <tag>` to confirm the app zip, dSYM zip, CLI tarballs, and CLI checksums are present on GitHub.
- Generate the appcast + HTML release notes: `./Scripts/make_appcast.sh CodexBar-macos-universal-<ver>.zip https://raw.githubusercontent.com/steipete/CodexBar/main/appcast.xml`
- Beta channel: prefix the command with `SPARKLE_CHANNEL=beta` to tag the entry.
- Verify the enclosure signature + size: `./Scripts/verify_appcast.sh <ver>`
- [ ] Upload zip + appcast to feed; publish tag + GitHub release so Sparkle URL is live (avoid 404)
- [ ] Homebrew tap: wait for the Release CLI workflow to update `../homebrew-tap/Casks/codexbar.rb` (app zip url + sha256) and `../homebrew-tap/Formula/codexbar.rb` (CLI tarball urls + sha256), then verify:
- `gh run watch <release-cli-run-id> --exit-status`
- `Scripts/check-release-assets.sh v<version>`
- `brew uninstall --cask codexbar || true`
- `brew untap steipete/tap || true; brew tap steipete/tap`
- `brew install --cask steipete/tap/codexbar && open -a CodexBar`
- [ ] Version continuity: confirm the new version is the immediate next patch/minor (no gaps) and CHANGELOG has no skipped numbers (e.g., after 0.2.0 use 0.2.1, not 0.2.2)
- [ ] Changelog sanity: single top-level title, no duplicate version sections, versions strictly descending with no repeats
- [ ] Release pages: title format `CodexBar <version>`, notes as Markdown list (no stray blank lines)
- [ ] Changelog/release notes are user-facing: avoid internal-only bullets (build numbers, script bumps) and keep entries concise
- [ ] Download uploaded `CodexBar-macos-universal-<ver>.zip`, unzip via `ditto`, run, and verify signature (`spctl -a -t exec -vv CodexBar.app` + `stapler validate`)
- [ ] Confirm `appcast.xml` points to the new zip/version and renders the HTML release notes (not escaped tags)
- [ ] Verify on GitHub Releases: assets present (zip, appcast), release notes match changelog, version/tag correct
- [ ] Open the appcast URL in browser to confirm the new entry is visible and enclosure URL is reachable
- [ ] Manually visit the enclosure URL (curl -I) to ensure 200/OK (no 404) after publishing assets/release
- [ ] Ensure `sparkle:edSignature` is present for the enclosure in appcast (generated by `generate_appcast` with the ed25519 key)
- [ ] When creating the GitHub release, paste the CHANGELOG entry as Markdown list (one `-` per line, blank line between sections); visually confirm bullets render correctly after publishing
- [ ] Keep a previous signed build in `/Applications/CodexBar.app` to test Sparkle delta/full update to the new release
- [ ] Manual Gatekeeper sanity: after packaging, `find CodexBar.app -name '._*'` is empty, `spctl --assess --type execute --verbose CodexBar.app` and `codesign --verify --deep --strict --verbose CodexBar.app` succeed
- [ ] For Sparkle verification: if replacing `/Applications/CodexBar.app`, quit first, replace, relaunch, and test update
- **Definition of “done” for a release:** all of the above are complete, the appcast/enclosure link resolves, Homebrew cask
installs, and a previous public build can update to the new one via Sparkle. Anything short of that is not a finished release.
## Troubleshooting
- **White plate icon**: regenerate icns via `build_icon.sh` (ictool) to ensure transparent padding.
- **Notarization invalid**: verify deep+timestamp signing, especially Sparkles Autoupdate/Updater and XPCs; rerun package + sign-and-notarize.
- **App wont launch**: ensure Sparkle.framework is embedded under `Contents/Frameworks` and rpath added; codesign deep.
- **App “damaged” dialog after unzip**: re-extract with `ditto -x -k`, removing any `._*` files, then re-verify with `spctl`.
- **Update download fails (404)**: ensure the release asset referenced in appcast exists and is published in the corresponding GitHub release; verify with `curl -I <enclosure-url>`.
+10
View File
@@ -0,0 +1,10 @@
---
summary: "Pending cleanup items for CodexBar (e.g., retire Claude Opus fallback)."
read_when:
- Grooming backlog or planning maintenance
- Removing legacy Claude Opus handling
---
## TODO
- December 2025: remove the Claude Opus fallback logic (Claude Code) once users have fully migrated; clean up any UI labels and parsers that reference Opus as a tertiary limit.
+638
View File
@@ -0,0 +1,638 @@
---
summary: "Upstream strategy for forks: remotes, cherry-picks, and contribution policy."
read_when:
- Managing fork/upstream workflow
- Planning contributions or syncs
---
# Multi-Upstream Fork Management Strategy
**Fork:** topoffunnel/CodexBar
**Upstream 1:** steipete/CodexBar (original)
**Upstream 2:** nguyenphutrong/quotio (inspiration source)
---
## 🎯 Core Principles
### Fork Independence
- **Your fork is the primary development target**
- Upstream contributions are optional and selective
- You retain full credit for your innovations
- Fork-specific features stay in the fork
### Selective Contribution
- Only contribute universally beneficial changes upstream
- Keep attribution-sensitive improvements in fork
- Submit small, focused PRs to increase merge likelihood
- Don't contribute fork branding or identity
### Best-of-Both-Worlds
- Monitor both upstreams for valuable changes
- Cherry-pick features that enhance your fork
- Adapt patterns without copying code
- Credit sources appropriately
---
## 🌳 Git Repository Structure
### Remote Configuration
```bash
# Your fork (origin)
git remote add origin git@github.com:topoffunnel/CodexBar.git
# Original upstream (steipete)
git remote add upstream git@github.com:steipete/CodexBar.git
# Quotio inspiration source
git remote add quotio git@github.com:nguyenphutrong/quotio.git
# Verify remotes
git remote -v
```
### Branch Strategy
```
main (your fork's stable branch)
├── feature/* (fork-specific features)
├── upstream-sync/* (tracking upstream changes)
├── quotio-inspired/* (features inspired by quotio)
└── upstream-pr/* (branches for upstream PRs)
```
**Branch Types:**
- `main` - Your fork's stable release branch
- `feature/*` - Fork-specific development
- `upstream-sync/*` - Temporary branches for reviewing upstream changes
- `quotio-inspired/*` - Features adapted from quotio patterns
- `upstream-pr/*` - Clean branches for upstream contributions
---
## 🔄 Workflow 1: Monitoring Upstream Changes
### Daily/Weekly Sync Check
```bash
#!/bin/bash
# Scripts/check_upstreams.sh
echo "==> Fetching upstream changes..."
git fetch upstream
git fetch quotio
echo ""
echo "==> Upstream (steipete) changes:"
git log --oneline main..upstream/main --no-merges | head -20
echo ""
echo "==> Quotio changes:"
git log --oneline --all --remotes=quotio/main --since="1 week ago" | head -20
echo ""
echo "==> Files changed in upstream:"
git diff --stat main..upstream/main
echo ""
echo "==> Files changed in quotio (recent):"
git diff --stat quotio/main~10..quotio/main
```
### Automated Monitoring (GitHub Actions)
Create `.github/workflows/upstream-monitor.yml`:
```yaml
name: Monitor Upstreams
on:
schedule:
- cron: '0 9 * * 1,4' # Monday and Thursday at 9 AM
workflow_dispatch:
jobs:
check-upstream:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Add upstream remotes
run: |
git remote add upstream https://github.com/steipete/CodexBar.git
git remote add quotio https://github.com/nguyenphutrong/quotio.git
git fetch upstream
git fetch quotio
- name: Check for new commits
id: check
run: |
UPSTREAM_NEW=$(git log --oneline main..upstream/main --no-merges | wc -l)
QUOTIO_NEW=$(git log --oneline --all --remotes=quotio/main --since="1 week ago" | wc -l)
echo "upstream_commits=$UPSTREAM_NEW" >> $GITHUB_OUTPUT
echo "quotio_commits=$QUOTIO_NEW" >> $GITHUB_OUTPUT
- name: Create issue if changes detected
if: steps.check.outputs.upstream_commits > 0 || steps.check.outputs.quotio_commits > 0
uses: actions/github-script@v7
with:
script: |
const upstreamCommits = '${{ steps.check.outputs.upstream_commits }}';
const quotioCommits = '${{ steps.check.outputs.quotio_commits }}';
const body = `## Upstream Changes Detected
**steipete/CodexBar:** ${upstreamCommits} new commits
**quotio:** ${quotioCommits} new commits (last week)
Review changes:
- [steipete commits](https://github.com/steipete/CodexBar/compare/main...upstream/main)
- [quotio commits](https://github.com/nguyenphutrong/quotio/commits/main)
Run \`./Scripts/review_upstream.sh\` to analyze changes.`;
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Upstream Changes Available',
body: body,
labels: ['upstream-sync']
});
```
---
## 🔍 Workflow 2: Reviewing & Incorporating Changes
### Step 1: Review Upstream Changes
```bash
#!/bin/bash
# Scripts/review_upstream.sh
UPSTREAM=${1:-upstream} # 'upstream' or 'quotio'
echo "==> Creating review branch for $UPSTREAM..."
git checkout main
git checkout -b upstream-sync/$UPSTREAM-$(date +%Y%m%d)
echo "==> Fetching latest..."
git fetch $UPSTREAM
echo "==> Showing commits to review:"
git log --oneline --graph main..$UPSTREAM/main | head -30
echo ""
echo "==> Detailed diff:"
git diff main..$UPSTREAM/main --stat
echo ""
echo "Next steps:"
echo "1. Review commits: git log -p main..$UPSTREAM/main"
echo "2. Cherry-pick specific commits: git cherry-pick <commit-hash>"
echo "3. Or merge all: git merge $UPSTREAM/main"
echo "4. Test thoroughly"
echo "5. Merge to main: git checkout main && git merge upstream-sync/$UPSTREAM-$(date +%Y%m%d)"
```
### Step 2: Selective Cherry-Picking
```bash
# Review individual commits
git log -p main..upstream/main
# Cherry-pick specific valuable commits
git cherry-pick <commit-hash>
# If conflicts, resolve and continue
git cherry-pick --continue
# Or abort if not suitable
git cherry-pick --abort
```
### Step 3: Quotio Pattern Adaptation
```bash
# Create inspiration branch
git checkout -b quotio-inspired/feature-name
# View quotio implementation (read-only)
git show quotio/main:path/to/file.swift
# Implement similar pattern in your codebase
# (write your own code, don't copy)
# Commit with attribution
git commit -m "feat: implement feature inspired by quotio
Inspired by quotio's approach to [feature]:
https://github.com/nguyenphutrong/quotio/commit/abc123
Implemented independently with CodexBar-specific patterns."
```
---
## 📤 Workflow 3: Contributing to Upstream
### Identifying Upstream-Suitable Changes
**✅ Good for Upstream:**
- Bug fixes that affect all users
- Performance improvements
- Provider enhancements (non-fork-specific)
- Documentation improvements
- Test coverage additions
- Dependency updates
**❌ Keep in Fork:**
- Fork branding/attribution
- Multi-account management (major architectural change)
- Fork-specific UI customizations
- Experimental features
- topoffunnel.com-specific integrations
### Creating Upstream PR Branch
```bash
#!/bin/bash
# Scripts/prepare_upstream_pr.sh
FEATURE_NAME=$1
if [ -z "$FEATURE_NAME" ]; then
echo "Usage: ./Scripts/prepare_upstream_pr.sh <feature-name>"
exit 1
fi
echo "==> Creating upstream PR branch..."
git checkout upstream/main
git checkout -b upstream-pr/$FEATURE_NAME
echo "==> Branch created: upstream-pr/$FEATURE_NAME"
echo ""
echo "Next steps:"
echo "1. Cherry-pick your commits (without fork branding)"
echo "2. Remove any fork-specific code"
echo "3. Ensure tests pass"
echo "4. Push: git push origin upstream-pr/$FEATURE_NAME"
echo "5. Create PR to steipete/CodexBar from GitHub UI"
```
### Cleaning Commits for Upstream
```bash
# Start from upstream's main
git checkout upstream/main
git checkout -b upstream-pr/fix-cursor-bonus
# Cherry-pick your fix (without fork branding)
git cherry-pick <your-commit-hash>
# If commit includes fork branding, amend it
git commit --amend
# Remove fork-specific changes
git reset HEAD~1
# Manually stage only upstream-suitable changes
git add <files>
git commit -m "fix: correct Cursor bonus credits calculation
Fixes issue where bonus credits were incorrectly calculated.
Tested with multiple account types."
# Push to your fork
git push origin upstream-pr/fix-cursor-bonus
# Create PR to steipete/CodexBar via GitHub UI
```
---
## 🏷️ Commit Message Strategy
### Fork Commits (Keep Everything)
```
feat: add multi-account management for Augment
Implements account switching UI and storage.
Fork-specific feature for topoffunnel.com users.
Co-authored-by: Brandon Charleson <brandon@topoffunnel.com>
```
### Upstream-Bound Commits (Generic)
```
fix: correct Cursor bonus credits calculation
The bonus credits were being added instead of subtracted
from the total usage calculation.
Tested with Pro and Team accounts.
```
### Quotio-Inspired Commits (Attribution)
```
feat: implement session persistence inspired by quotio
Adds automatic session restoration on app restart.
Inspired by quotio's approach:
https://github.com/nguyenphutrong/quotio/blob/main/...
Implemented independently using CodexBar patterns.
```
---
## 📋 Decision Matrix: What Goes Where?
| Change Type | Fork | Upstream | Notes |
|------------|------|----------|-------|
| Bug fix (universal) | ✅ | ✅ | Submit to upstream |
| Bug fix (fork-specific) | ✅ | ❌ | Keep in fork |
| Performance improvement | ✅ | ✅ | Submit to upstream |
| New provider support | ✅ | ✅ | Submit to upstream |
| Provider enhancement | ✅ | Maybe | Depends on scope |
| UI improvement (generic) | ✅ | ✅ | Submit to upstream |
| UI improvement (fork brand) | ✅ | ❌ | Keep in fork |
| Multi-account feature | ✅ | ❌ | Too large for upstream |
| Documentation | ✅ | ✅ | Submit to upstream |
| Tests | ✅ | ✅ | Submit to upstream |
| Fork branding | ✅ | ❌ | Never upstream |
| Experimental feature | ✅ | ❌ | Prove it first |
---
## 🔐 Protecting Your Attribution
### Separate Commits Strategy
```bash
# Make changes in feature branch
git checkout -b feature/my-improvement
# Commit 1: Core improvement (upstream-suitable)
git add Sources/CodexBarCore/...
git commit -m "feat: improve cookie handling"
# Commit 2: Fork-specific enhancements
git add Sources/CodexBar/About.swift
git commit -m "feat: add fork attribution for improvement"
# Merge to main (both commits)
git checkout main
git merge feature/my-improvement
# For upstream PR: cherry-pick only commit 1
git checkout upstream/main
git checkout -b upstream-pr/cookie-handling
git cherry-pick <commit-1-hash> # Only the core improvement
```
### Maintaining Fork Identity
Keep these files fork-specific (never upstream):
- `Sources/CodexBar/About.swift` (your attribution)
- `Sources/CodexBar/PreferencesAboutPane.swift` (fork sections)
- `README.md` (fork notice)
- `docs/FORK_*.md` (fork documentation)
- `FORK_STATUS.md`
---
## 🤖 Automation Scripts
All automation scripts are located in `Scripts/`:
- **check_upstreams.sh** - Check for new commits in both upstreams
- **review_upstream.sh** - Create review branch for upstream changes
- **prepare_upstream_pr.sh** - Prepare clean branch for upstream PR
- **analyze_quotio.sh** - Analyze quotio for patterns and features
GitHub Actions workflow: `.github/workflows/upstream-monitor.yml`
---
## 📖 Practical Examples
### Example 1: Weekly Upstream Check
```bash
# Monday morning routine
./Scripts/check_upstreams.sh
# If changes found, review them
./Scripts/review_upstream.sh upstream
# Cherry-pick valuable commits
git cherry-pick abc123
git cherry-pick def456
# Test
./Scripts/compile_and_run.sh
# Merge to main
git checkout main
git merge upstream-sync/upstream-20260104
```
### Example 2: Contributing Bug Fix Upstream
```bash
# You fixed a bug in your fork
git log --oneline -5
# abc123 fix: correct Cursor bonus credits
# def456 feat: add fork attribution
# Prepare upstream PR (only the fix, not attribution)
./Scripts/prepare_upstream_pr.sh fix-cursor-bonus
# Cherry-pick only the fix
git cherry-pick abc123
# Review - ensure no fork branding
git diff upstream/main
# Push and create PR
git push origin upstream-pr/fix-cursor-bonus
# Then create PR on GitHub to steipete/CodexBar
```
### Example 3: Learning from Quotio
```bash
# Analyze quotio
./Scripts/analyze_quotio.sh
# Review their multi-account implementation
git show quotio/main:path/to/AccountManager.swift
# Document patterns in docs/QUOTIO_ANALYSIS.md
# Then implement independently in your fork
# Commit with attribution
git commit -m "feat: implement multi-account management
Inspired by quotio's account switching pattern:
https://github.com/nguyenphutrong/quotio/...
Implemented independently using CodexBar's architecture."
```
---
## 🎓 Best Practices
### For Fork Development
1. **Commit often** - Small, focused commits
2. **Separate concerns** - Fork branding in separate commits
3. **Test thoroughly** - Every change
4. **Document decisions** - Why you chose this approach
5. **Credit sources** - When inspired by others
### For Upstream Contributions
1. **Start small** - Bug fixes before features
2. **One thing per PR** - Focused changes
3. **Follow their style** - Match upstream conventions
4. **Include tests** - Prove it works
5. **Be patient** - Maintainers are busy
### For Multi-Upstream Sync
1. **Check weekly** - Stay current
2. **Review carefully** - Understand before merging
3. **Test everything** - Upstream changes may break your fork
4. **Document conflicts** - How you resolved them
5. **Keep attribution** - Credit all sources
---
## 🔧 Troubleshooting
### Merge Conflicts
```bash
# During upstream merge
git merge upstream/main
# CONFLICT in Sources/CodexBar/About.swift
# Keep your fork version for branding files
git checkout --ours Sources/CodexBar/About.swift
git add Sources/CodexBar/About.swift
# Merge other files manually
# Then continue
git commit
```
### Accidentally Pushed Fork Branding to Upstream PR
```bash
# Oops! Pushed fork branding to upstream PR branch
git checkout upstream-pr/my-feature
# Reset to before the bad commit
git reset --hard HEAD~1
# Re-apply changes without branding
# ... make changes ...
git commit -m "fix: proper commit"
# Force push (only safe on PR branches)
git push origin upstream-pr/my-feature --force
```
### Lost Track of Upstream Changes
```bash
# See what you've merged from upstream
git log --oneline --graph --all --grep="upstream"
# See what's still pending
git log --oneline main..upstream/main
# Create a tracking branch
git checkout -b upstream-tracking upstream/main
git log --oneline upstream-tracking..main
```
---
## 📊 Success Metrics
### Fork Health
- ✅ Builds without errors
- ✅ All tests passing
- ✅ No regressions from upstream merges
- ✅ Fork-specific features working
- ✅ Documentation up to date
### Upstream Relationship
- ✅ PRs are small and focused
- ✅ PRs get merged (or constructive feedback)
- ✅ Maintain good relationship with maintainer
- ✅ Credit given appropriately
- ✅ No fork branding in upstream PRs
### Multi-Source Learning
- ✅ Regular upstream monitoring
- ✅ Quotio patterns documented
- ✅ Independent implementations
- ✅ Proper attribution
- ✅ Best-of-both-worlds achieved
---
## 🗓️ Recommended Schedule
### Weekly
- Monday: Check upstreams (`./Scripts/check_upstreams.sh`)
- Thursday: Review quotio (`./Scripts/analyze_quotio.sh`)
### Monthly
- Review upstream PRs you submitted
- Update QUOTIO_ANALYSIS.md with new findings
- Sync with upstream main
- Update fork documentation
### Quarterly
- Major feature planning
- Upstream contribution strategy review
- Fork roadmap update
- Community engagement
---
## 📞 Getting Help
### Upstream Issues
- Check their issue tracker first
- Ask in discussions if available
- Be respectful and patient
- Provide minimal reproduction
### Fork Issues
- Document in your fork's issues
- Reference upstream if relevant
- Track in FORK_STATUS.md
- Update roadmap as needed
### Quotio Questions
- Review their documentation
- Check their issue tracker
- Don't ask them to help with your fork
- Credit them when you adapt patterns
---
**Remember:** Your fork is independent. Upstream contributions are optional. Learn from others, but implement independently. Credit sources appropriately.
+67
View File
@@ -0,0 +1,67 @@
---
summary: "Abacus AI provider: browser cookie auth for ChatLLM/RouteLLM compute credit tracking."
read_when:
- Adding or modifying the Abacus AI provider
- Debugging Abacus cookie imports or API responses
- Adjusting Abacus usage display or credit formatting
---
# Abacus AI Provider
The Abacus AI provider tracks ChatLLM/RouteLLM compute credit usage via browser cookie authentication.
## Features
- **Monthly credit gauge**: Shows credits used vs. plan total with pace tick indicator.
- **Reserve/deficit estimate**: Projected credit usage through the billing cycle.
- **Reset timing**: Displays the next billing date from the Abacus billing API.
- **Subscription tiers**: Detects Basic and Pro plans.
- **Cookie auth**: Automatic browser cookie import (Safari, Chrome, Firefox) or manual cookie header.
## Setup
1. Open **Settings → Providers**
2. Enable **Abacus AI**
3. Log in to [apps.abacus.ai](https://apps.abacus.ai) in your browser
4. Cookie import happens automatically on the next refresh
### Manual cookie mode
1. In **Settings → Providers → Abacus AI**, set Cookie source to **Manual**
2. Open your browser DevTools on `apps.abacus.ai`, copy the `Cookie:` header from any API request
3. Paste the header into the cookie field in CodexBar
## How it works
Two API endpoints are fetched concurrently using browser session cookies:
- `GET https://apps.abacus.ai/api/_getOrganizationComputePoints` — returns `totalComputePoints` and `computePointsLeft` (values are in credit units, no conversion needed).
- `POST https://apps.abacus.ai/api/_getBillingInfo` — returns `nextBillingDate` (ISO 8601) and `currentTier` (plan name).
Cookie domains: `abacus.ai`, `apps.abacus.ai`. Session cookies are validated before use (anonymous/marketing-only cookie sets are skipped). Valid cookies are cached in Keychain and reused until the session expires.
The billing cycle window is set to 30 days for pace calculation.
## CLI
```bash
codexbar usage --provider abacusai --verbose
```
## Troubleshooting
### "No Abacus AI session found"
Log in to [apps.abacus.ai](https://apps.abacus.ai) in a supported browser (Safari, Chrome, Firefox), then refresh CodexBar.
### "Abacus AI session expired"
Re-login to Abacus AI. The cached cookie will be cleared automatically and a fresh one imported on the next refresh.
### "Unauthorized"
Your session cookies may be invalid. Log out and back in to Abacus AI, or paste a fresh `Cookie:` header in manual mode.
### Credits show 0
Verify that your Abacus AI account has an active subscription with compute credits allocated.
+98
View File
@@ -0,0 +1,98 @@
# Agent Sessions (prototype)
Track live Codex + Claude Code agent sessions — local Mac first, other Macs on the tailnet second — and surface them in the CodexBar menu with click-to-focus of the owning terminal window.
## Why in CodexBar
CodexBar already parses `~/.claude/projects` JSONL (cost scanner) and ships a bundled CLI on macOS + Linux. Sessions reuse both: the local scanner feeds the menu UI, and the same scanner exposed as `codexbar sessions --json` is what remote Macs run over SSH. No daemon, no new app.
## Data model (CodexBarCore)
```swift
public struct AgentSession: Codable, Sendable, Identifiable {
public enum Provider: String, Codable, Sendable { case codex, claude }
public enum Source: String, Codable, Sendable { case cli, desktopApp, ide, unknown }
public enum State: String, Codable, Sendable { case active, idle }
public var id: String // session UUID when resolvable, else "pid:<pid>"
public var provider: Provider
public var source: Source
public var state: State
public var pid: Int32? // nil for file-only (e.g. Codex desktop) sessions
public var cwd: String?
public var projectName: String? // last path component of cwd
public var startedAt: Date?
public var lastActivityAt: Date? // transcript mtime
public var transcriptPath: String?
public var host: String // local hostname, or remote host label
}
```
`active` = last activity ≤ 120 s ago. `idle` = live process (or recent file) with older activity. Constants live in one `SessionScanConfig` struct (activeWindow 120 s, fileOnlyWindow 30 min) so thresholds are tunable/testable.
## Local scanner (CodexBarCore, no new deps)
`LocalAgentSessionScanner` combines two signals:
1. **Process scan** — parse `ps -axo pid=,ppid=,lstart=,command=`.
- Claude: command basename `claude` (skip obvious non-agent helpers). Source: path contains `Application Support/Claude/claude-code``.desktopApp`, else `.cli`. Deduplicate the wrapper/child pair (desktop spawns `disclaimer` parent + `claude` child with same argv; keep the child).
- Codex: basename `codex` with no `app-server` argument → `.cli` (TUI or `exec`). `codex app-server` marks the desktop app as present but is not itself a session.
- cwd per pid via one batched `lsof -a -d cwd -Fn -p <pid,pid,…>` call (parse `p`/`n` records). Failure → cwd nil, session still listed.
2. **Transcript correlation**
- Claude: cwd → `~/.claude/projects/<escaped-cwd>/` (escape: every non-alphanumeric ASCII → `-`), newest `*.jsonl` by mtime → session id (filename UUID), lastActivityAt (mtime). Also reuse `ClaudeDesktopProjectsLocator` roots so desktop local-agent-mode sessions resolve.
- Codex: enumerate `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` for today + yesterday (`$CODEX_HOME` respected). Read only the first line (`session_meta`: `session_id`, `cwd`, `originator`, `source`). File with mtime ≤ fileOnlyWindow and no matching live pid → file-only session, source from `originator` (`codex_exec`/`exec``.cli`; ide-ish originators → `.ide`; desktop → `.desktopApp`). Live `codex` pids match to rollouts by cwd (newest wins); unmatched live pid still listed with nil transcript.
- Never read more than the first line of any JSONL; never load whole transcripts.
Scanner is `Sendable`, pure functions where possible; ps/lsof output parsing lives in dedicated parser types fed by strings so tests use fixtures.
## CLI (CodexBarCLI)
- `codexbar sessions` — table; `--json``[AgentSession]` (stable field names above; ISO-8601 dates).
- `codexbar sessions focus <id>` — macOS only: focus the session's terminal window (see Focus). Exit 1 if id unknown, 2 if focus failed.
- Follows existing `CLI*Command.swift` conventions. Works on Linux for listing (ps/proc paths guarded), focus is Darwin-only.
## Remote hosts (CodexBarCore + app)
`RemoteSessionFetcher`:
- Host list = manual entries (settings, ssh destinations like `steipete@clawmac`) automatic Tailscale discovery (no-op when tailscale is absent): run `tailscale status --json` (PATH, then `/Applications/Tailscale.app/Contents/MacOS/Tailscale`), take online peers with `"OS": "macOS"|"linux"`, use first `DNSName` label as host. Local host excluded.
- Fetch per host (parallel, 5 s budget): `ssh -o BatchMode=yes -o ConnectTimeout=3 <host> sh -lc 'codexbar sessions --json'` with fallback to the bundled app CLI path (resolve the canonical bundled location from `Scripts/package_app.sh` and hardcode it as fallback: `… || <bundled-path> sessions --json`). Host errors are non-fatal: host shown as unreachable, others still render.
- Remote focus: fire-and-forget `ssh <host> sh -lc 'codexbar sessions focus <id>'`.
- Refresh: local scan every 30 s while the status item exists (cheap), remote every 60 s and immediately on menu open; both skipped when the feature is off. Reuse existing refresh loop plumbing rather than new timers if it fits.
## Menu UI (CodexBar app)
- New menu section **Agent Sessions (N)** (N = total, all hosts) above the settings/footer area, built through the existing `MenuDescriptor`-style seam so it's testable headless.
- Local sessions first, then one group per remote host (`clawmac — 2`, unreachable hosts greyed with a tooltip). Row: state dot (● active / ○ idle), provider glyph, `projectName — provider · source · 12m`.
- Click local row → `SessionWindowFocuser`. Click remote row → remote focus ssh call.
- Settings: "Sessions" group — a single enable toggle (default on) plus a manual hosts text field (comma-separated); Tailscale discovery is always on while the feature is enabled. Persist in `SettingsStore` like neighboring prefs.
## Focus (macOS, app + CLI shared in Core or app-adjacent target)
`SessionWindowFocuser`:
1. pid → walk ppid chain to the nearest ancestor whose `NSRunningApplication.bundleIdentifier` is a known terminal/editor host: Ghostty, iTerm2, Apple Terminal, Warp, WezTerm, kitty, Alacritty, VS Code, Cursor, Zed, Claude desktop (`com.anthropic.claudefordesktop`). Fallback: the app owning the pid.
2. Activate the app, then AX (`AXUIElementCreateApplication``AXWindows`): raise the window whose title contains projectName or the cwd tail; fallback to frontmost window of that app. Requires Accessibility permission — call `AXIsProcessTrustedWithOptions` with prompt on first use; degrade gracefully (activate app only) when untrusted.
3. File-only sessions (no pid): Claude desktop → activate Claude.app; Codex desktop → activate Codex.app; otherwise no-op with log.
tmux pane / terminal-tab precision is out of scope for the prototype.
## Tests (Tests/CodexBarTests)
Fixture-driven, no live processes, no Keychain/AX:
- ps output parser: desktop `disclaimer`+`claude` dedupe, codex vs `codex app-server`, weird argv.
- lsof `-Fn` parser.
- Claude cwd escaping → project dir mapping; newest-jsonl selection (temp dirs).
- Codex rollout first-line parse → AgentSession (fixture JSONL), file-only window cutoff.
- Tailscale status JSON → host list (fixture; offline/iOS peers excluded).
- Sessions JSON round-trip (CLI output schema stability).
- Menu section descriptor: counts, grouping, unreachable-host rendering.
## Non-goals (prototype)
Claude.ai chat sessions; Codex cloud tasks; historical session browsing/analytics; "waiting on permission" state; tmux pane/tab focus; Bonjour/mDNS; persistent remote daemon or push transport; widget changes. No new SPM dependencies.
## Proof
`make check` clean; `make test` (or focused `swift test --filter` covering the new tests) green; `swift run CodexBarCLI sessions --json` produces plausible output on this Mac.
+75
View File
@@ -0,0 +1,75 @@
---
summary: "Alibaba Coding Plan provider data sources: browser-session baseline, secondary API mode, and honest quota fallback behavior."
read_when:
- Debugging Alibaba Coding Plan API key handling or quota parsing
- Updating Alibaba Coding Plan endpoints or region behavior
- Adjusting Alibaba Coding Plan provider UI/menu behavior
---
# Alibaba Coding Plan provider
Alibaba Coding Plan supports both browser-session and API-key paths, but the supported baseline is browser-session fetching from the Model Studio/Bailian console. API mode remains secondary and may still be limited by account/region behavior.
## Cookie sources (web mode)
1) Automatic browser import (Model Studio/Bailian cookies).
2) Manual cookie header from Settings.
3) Environment variable `ALIBABA_CODING_PLAN_COOKIE`.
When the RPC endpoint returns `ConsoleNeedLogin`, CodexBar treats that as a console-session requirement. In API mode it is surfaced as an explicit API-path limitation; in `auto` mode fallback remains observable through the fetch-attempt chain.
## Token sources (fallback order)
1) Config token (`~/.codexbar/config.json` -> `providers[].apiKey` for provider `alibaba`).
2) Environment variables, checked in order:
- `ALIBABA_CODING_PLAN_API_KEY`
- `ALIBABA_QWEN_API_KEY`
- `DASHSCOPE_API_KEY`
## Region + endpoint behavior
- International host: `https://modelstudio.console.alibabacloud.com`
- China mainland host: `https://bailian.console.aliyun.com`
- Quota request path:
- `POST /data/api.json?action=zeldaEasy.broadscope-bailian.codingPlan.queryCodingPlanInstanceInfoV2&product=broadscope-bailian&api=queryCodingPlanInstanceInfoV2`
- Region is selected in Preferences -> Providers -> Alibaba Coding Plan -> Gateway region.
- Auto fallback behavior:
- If International fails with credential/host-style API errors, CodexBar retries China mainland once.
### CN API-key limitation (known)
- In some China mainland accounts/environments, the current Alibaba `/data/api.json` coding-plan endpoint can still return console-login-required responses (`ConsoleNeedLogin`) even when an API key is configured.
- In that case, API-key mode may not be functionally available for that account/endpoint, and web session mode is required.
- CodexBar now surfaces this as an API error in API mode (instead of a cookie-login-required message) so the limitation is explicit.
## Overrides
- Override host base: `ALIBABA_CODING_PLAN_HOST`
- Example: `ALIBABA_CODING_PLAN_HOST=modelstudio.console.alibabacloud.com`
- Override full quota URL: `ALIBABA_CODING_PLAN_QUOTA_URL`
- Example: `ALIBABA_CODING_PLAN_QUOTA_URL=https://modelstudio.console.alibabacloud.com/data/api.json?action=...`
- Security policy: endpoint overrides are only accepted when they use `https://`, omit userinfo, and do not contain encoded host delimiters. Custom HTTPS proxy/test domains continue to work for compatibility, but `http://` endpoints are rejected so cookies and API credentials are not sent in cleartext.
- Strict provider-host mode: set `ALIBABA_CODING_PLAN_REQUIRE_PROVIDER_ENDPOINT_OVERRIDES=true` to additionally reject custom proxy/test domains and only accept the known Alibaba Coding Plan console and RPC hosts.
## Request headers
- `Authorization: Bearer <api_key>`
- `x-api-key: <api_key>`
- `X-DashScope-API-Key: <api_key>`
- `Content-Type: application/json`
- `Accept: application/json`
## Parsing + mapping
- Plan name (best effort):
- `codingPlanInstanceInfos[].planName` / `instanceName` / `packageName`
- Quota windows (from `codingPlanQuotaInfo`):
- `per5HourUsedQuota` + `per5HourTotalQuota` + `per5HourQuotaNextRefreshTime` -> primary (5-hour)
- `perWeekUsedQuota` + `perWeekTotalQuota` + `perWeekQuotaNextRefreshTime` -> secondary (weekly)
- `perBillMonthUsedQuota` + `perBillMonthTotalQuota` + `perBillMonthQuotaNextRefreshTime` -> tertiary (monthly)
- Each window maps to `usedPercent = used / total * 100` (bounded to valid range).
- If the payload proves the plan is active but does not expose defensible quota counters, CodexBar preserves the visible plan state without manufacturing a normal quantitative quota window.
- If neither real counters nor a defensible active-plan fallback signal exist, parsing fails explicitly instead of degrading to fake `0%` usage.
## Dashboard links
- International console: `https://modelstudio.console.alibabacloud.com/ap-southeast-1/?tab=globalset#/efm/coding_plan`
- China mainland console: `https://bailian.console.aliyun.com/cn-beijing/?tab=model#/efm/coding_plan`
## Key files
- `Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanProviderDescriptor.swift`
- `Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageFetcher.swift`
- `Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageSnapshot.swift`
- `Sources/CodexBar/Providers/Alibaba/AlibabaCodingPlanProviderImplementation.swift`
+61
View File
@@ -0,0 +1,61 @@
---
summary: "Alibaba Token Plan provider notes: Bailian cookie auth, subscription summary endpoint, and setup."
read_when:
- Adding or modifying the Alibaba Token Plan provider
- Debugging Alibaba Token Plan cookie import or subscription summary fetching
- Explaining Alibaba Token Plan setup and limitations to users
---
# Alibaba Token Plan Provider
The Alibaba Token Plan provider tracks Bailian token-plan credits from the Alibaba Cloud console.
## Features
- **Token-plan usage display**: Shows used, total, and remaining token-plan credits when Bailian returns quota totals.
- **Cookie-based auth**: Uses browser cookies or a pasted `Cookie:` header.
- **Expiry awareness**: Shows the nearest token-plan expiration date as the reset time when the subscription summary includes it.
## Setup
1. Open **Settings -> Providers**
2. Enable **Alibaba Token Plan**
3. Leave **Cookie source** on **Auto** (recommended)
### Manual cookie import (optional)
1. Open `https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/token-plan`
2. Copy a `Cookie:` header from your browser's Network tab
3. Paste it into **Alibaba Token Plan -> Cookie source -> Manual**
## How it works
- Fetches `POST https://bailian.console.aliyun.com/data/api.json?action=GetSubscriptionSummary&product=BssOpenAPI-V3&_tag=`
- Sends form-encoded fields for `product=BssOpenAPI-V3`, `action=GetSubscriptionSummary`, `region=cn-beijing`, and `params={"ProductCode":"sfm_tokenplanteams_dp_cn"}`
- Uses Alibaba/Bailian login cookies, with `sec_token` added when it can be resolved from the dashboard page
- Parses `TotalValue`, `TotalSurplusValue`, `TotalCount`, and `NearestExpireDate` from the subscription summary response
- Supports `ALIBABA_TOKEN_PLAN_HOST` and `ALIBABA_TOKEN_PLAN_QUOTA_URL` for testing endpoint overrides
## Limitations
- Alibaba Token Plan currently supports the Bailian web-cookie path only
- API-key auth, token cost summaries, and automatic status polling are not supported
- The default endpoint is the China mainland Bailian token-plan subscription summary
## Troubleshooting
### "No Alibaba Token Plan session cookies found in browsers"
Log in at `https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/token-plan` in Chrome, then refresh CodexBar.
### "Alibaba Token Plan cookie header is invalid"
The pasted header is empty or not a valid Cookie header. Re-copy the request from the Token Plan page after logging in again.
### "Alibaba Token Plan login required"
Your Bailian session is stale. Sign out and back in on the Bailian console, then refresh CodexBar.
### Empty subscription summary
If Bailian returns `TotalCount: 0`, CodexBar keeps the provider visible but does not show a quota window because the account has no active token-plan subscription summary to graph.
+60
View File
@@ -0,0 +1,60 @@
---
summary: "Amp provider notes: CLI usage, web fallback, cookie auth, and credits."
read_when:
- Adding or modifying the Amp provider
- Debugging Amp cookie import or settings parsing
- Adjusting Amp menu labels or usage math
---
# Amp Provider
The Amp provider tracks Amp Free usage plus individual and workspace credits. It prefers the local Amp CLI, then an Amp
access token, and finally browser cookies.
## Features
- **Amp Free meter**: Shows how much daily free usage remains.
- **Time-to-full reset**: “Resets in …” indicates when free usage replenishes to full.
- **Individual credits**: Shows the remaining paid credit balance when Amp reports one.
- **Workspace credits**: Shows each workspace's remaining paid credit balance separately.
- **CLI-first fetch**: Uses `amp usage` when the Amp CLI is installed and signed in.
- **Access token support**: Uses `AMP_API_KEY` or the access token saved in CodexBar settings.
- **Browser cookie fallback**: Reads the legacy settings-page payload when the CLI and access token are unavailable.
## Setup
1. Open **Settings → Providers**
2. Enable **Amp**
3. Install and sign in to the Amp CLI, add an Amp access token, or leave **Cookie source** on **Auto** for web fallback
### Access token (optional)
Create an access token in Amp settings, then paste it into **Amp → Access token** or set `AMP_API_KEY`.
### Manual cookie import (optional)
1. Open `https://ampcode.com/settings`
2. Copy a `Cookie:` header from your browsers Network tab
3. Paste it into **Amp → Cookie Source → Manual**
## How it works
- Runs `amp usage` first in automatic mode
- Calls `POST https://ampcode.com/api/internal?userDisplayBalanceInfo` with an Amp access token
- Falls back to the settings page with browser cookies
- Parses the same usage display format returned to the CLI
- Computes time-to-full from the hourly replenishment rate
### “Amp access token is invalid or expired”
Create a new access token in Amp settings, update `AMP_API_KEY` or CodexBar settings, then refresh.
## Troubleshooting
### “No Amp session cookie found”
Log in to Amp in a supported browser (Safari or Chromium-based), then refresh in CodexBar.
### “Amp session cookie expired”
Sign out and back in at `https://ampcode.com/settings`, then refresh.
+231
View File
@@ -0,0 +1,231 @@
---
summary: "Antigravity provider notes: OAuth usage, multi-account switching, local LSP probing, and quota parsing."
read_when:
- Adding or modifying the Antigravity provider
- Debugging Antigravity port detection or quota parsing
- Adjusting Antigravity menu labels or model mapping
- Working with Antigravity OAuth or account switching
---
# Antigravity provider
For Google individual, AI Pro, and Ultra accounts blocked by the June 2026 Gemini CLI OAuth
shutdown, Antigravity is the replacement path for Gemini quota tracking in CodexBar. Launch
the Antigravity app or run `agy`, sign in, then refresh. See `docs/gemini.md` for the Gemini
provider migration notes. CodexBar offers the handoff only after an observed Google migration
signal and never enables or falls back to Antigravity automatically.
Antigravity supports four usage data sources:
1. The Antigravity 2.0 app's local `language_server` (preferred when the app is open).
2. The `agy` CLI's embedded HTTPS localhost server (preferred over the IDE because it exposes richer quota data).
3. The Antigravity IDE extension `language_server` (used after `agy` CLI because current IDE local payloads only expose session/model quota data).
4. Google OAuth-backed remote usage (explicit OAuth mode, and the account-scoped fallback used for multi-account switching). The OAuth path can store multiple Google accounts through the shared token-account switcher.
The local and CLI paths both prefer Antigravity's internal `RetrieveUserQuotaSummary` quota payload and may fall back to
`GetUserStatus`, then `GetCommandModelConfigs`; CodexBar never scrapes the desktop UI or the `agy` TUI.
As of Antigravity 2.x, the Antigravity app and `agy` CLI payloads can be richer than Google OAuth and IDE payloads.
`RetrieveUserQuotaSummary` exposes the same two groups shown by Antigravity's Model Quota UI:
- `Gemini Models`: weekly limit and five-hour limit.
- `Claude and GPT models`: weekly limit and five-hour limit.
Older local payloads may only include raw Claude, GPT-OSS, Gemini tiers, account plan, and session reset timestamps.
Current Antigravity IDE local endpoints return `GetUserStatus`, `GetAvailableModels`, and `GetCascadeModelConfigData`
with five-hour/session reset data, but not the app/CLI `RetrieveUserQuotaSummary` weekly/session grouping. OAuth
payloads can be less complete and may only prove model availability. Treat `auto` as the authoritative user-facing mode:
it accepts the first account-matching source in Antigravity app -> `agy` CLI -> Antigravity IDE order, and adds OAuth
when CodexBar has a selected/injected Google account or an existing shared credentials file. An all-100%
`fetchAvailableModels` payload is only accepted after `retrieveUserQuota` echoes bucket fractions; this can be an
availability-style fallback rather than the full Antigravity quota summary.
When OAuth identifies the account but quota endpoints deny access, CodexBar shows `Limits not available` instead of an
empty quota card.
## OAuth account switching
- Login still uses Antigravity's Google OAuth client, discovered from `Antigravity.app` or overridden with `ANTIGRAVITY_OAUTH_CLIENT_ID` and `ANTIGRAVITY_OAUTH_CLIENT_SECRET`.
- A successful login writes the latest shared credentials to `~/.codexbar/antigravity/oauth_creds.json` and upserts a token-account entry for the Google account.
- Each token-account entry stores serialized `AntigravityOAuthCredentials` and is injected into remote fetches through `ANTIGRAVITY_OAUTH_CREDENTIALS_JSON`.
- When a token account is selected, the OAuth fetcher uses that account before falling back to the shared credentials file.
In `auto` mode the ambient Antigravity app, `agy` CLI, and IDE probes still run first, but a snapshot whose account
does not match the selected account is rejected so the pipeline falls through to the account-scoped OAuth fetch (see
`AntigravitySelectedAccountGuard`). If no account is selected/injected, `auto` includes OAuth only when the legacy
shared credentials file already exists. Explicit `cli`/`oauth` source modes stay authoritative and are not re-checked.
- Removing the last saved token account that matches `~/.codexbar/antigravity/oauth_creds.json` deletes that shared file,
so a removed CodexBar account does not silently continue refreshing through the legacy shared cache.
- The menu action is labeled `Add Account...`; switching between saved accounts scopes Google OAuth fetches.
## Remote OAuth data sources
- `POST https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist`
- `POST https://cloudcode-pa.googleapis.com/v1internal:onboardUser`
- `POST https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels`
- `POST https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota`
- `POST https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary` (available, but current observed OAuth
responses are model-bucket shaped rather than Antigravity 2.0's two quota groups)
## Data sources + fallback order
### 1) Antigravity app local probe
When the Antigravity 2.0 app is running:
1. **Process detection**
- Command: `ps -ax -o pid=,command=`.
- The app local strategy scopes detection to the **Antigravity app** language server only
(`AntigravityStatusProbe(processScope: .appOnly)`). It deliberately does **not**
attach to an IDE or `agy` CLI process: a lower-information IDE payload should not mask
`agy`'s richer quota summary, and a stale or still-initializing `agy` can accept the
connection before it is ready. `agy` is owned exclusively by the CLI HTTPS source below,
which waits for real API readiness. The probe still classifies all kinds
(`processInfo(scope: .ideAndCLI)` is used by `isRunning()` for status reporting):
- the **Antigravity app** language server: process names such as `language_server`, `language_server_macos`,
`language_server_macos_arm`, or `language-server` plus
Antigravity markers (`--app_data_dir antigravity`, an Antigravity app bundle path,
or a path containing `/antigravity/`); or
- the **IDE** language server: the Antigravity IDE extension language server, usually under
`Antigravity IDE.app/.../extensions/antigravity/bin/` with `--app_data_dir antigravity-ide`; or
- the **CLI**: an `antigravity-cli` / `antigravity_cli` path segment, or the
`agy` binary (path-anchored so unrelated arguments/binaries do not match).
- CodexBar collects all valid local app language-server candidates and probes each reachable one. If multiple
app processes are open, it prefers the richer quota-summary snapshot over the legacy `GetUserStatus`
two-pool fallback.
- Extract CLI flags:
- `--csrf_token <token>`. Requirement depends on the match kind:
- **App/IDE** matches still require it - a tokenless desktop language-server match is
skipped so a later valid server can be found, otherwise `missingCSRFToken`
is reported (unchanged behavior).
- **CLI** matches accept an empty token, because the CLI's language server
exposes no `--csrf_token` flag and requires none.
- `--extension_server_port <port>` (HTTP fallback; app/IDE only).
- `--extension_server_csrf_token <token>` (preferred HTTP fallback token when present).
2. **Port discovery**
- Command: `lsof -nP -iTCP -sTCP:LISTEN -a -p <pid>`.
- All listening ports are probed.
3. **Connect port probe (HTTPS)**
- `POST https://127.0.0.1:<port>/exa.language_server_pb.LanguageServerService/GetUnleashData`
- Headers:
- `X-Codeium-Csrf-Token: <token>`
- `Connect-Protocol-Version: 1`
- First 200 OK response selects the connect port.
4. **Quota fetch**
- Primary:
- `POST https://127.0.0.1:<connectPort>/exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary`
- Fallback 1:
- `POST https://127.0.0.1:<connectPort>/exa.language_server_pb.LanguageServerService/GetUserStatus`
- Fallback 2:
- `POST https://127.0.0.1:<connectPort>/exa.language_server_pb.LanguageServerService/GetCommandModelConfigs`
- If HTTPS fails, retry over HTTP on `extension_server_port`.
### 2) `agy` CLI HTTPS source
When source mode is `auto` or `cli` and the desktop local probe fails, CodexBar resolves `agy` via:
- `ANTIGRAVITY_CLI_PATH`
- `PATH` / login-shell path lookup
- Well-known paths:
- `~/.local/bin/agy`
- `/opt/homebrew/bin/agy`
- `/usr/local/bin/agy`
CodexBar launches `agy` in a PTY because the CLI exposes its quota server only while the interactive process is alive.
The implementation still does **not** scrape terminal output; it only keeps the process alive, drains discarded PTY
rendering, discovers listening ports with `lsof`, and probes the local HTTPS server:
- First: `POST https://127.0.0.1:<port>/exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary`
- Fallback 1: `POST https://127.0.0.1:<port>/exa.language_server_pb.LanguageServerService/GetUserStatus`
- Fallback 2: `POST https://127.0.0.1:<port>/exa.language_server_pb.LanguageServerService/GetCommandModelConfigs`
The fallback can return quota without the account email or plan fields from `GetUserStatus`.
Differences from the desktop local probe:
- The CLI HTTPS endpoint does **not** require `X-Codeium-Csrf-Token`.
- Before a one-shot CLI invocation launches `agy`, CodexBar spends at most two seconds looking for an already-running,
same-user `agy` at the selected binary path and reuses its tokenless local HTTPS endpoint when it returns parseable
usage for the selected account. Long-lived app/server refreshes keep using CodexBar's managed session, and
CodexBar-owned pids are excluded from external reuse so probe/idle lifecycle accounting stays balanced.
- Readiness is endpoint-based: CodexBar retries until one of the quota endpoints parses, because fresh `agy`
processes can bind a port before the quota service is initialized.
- App runtime uses a bounded warm session: `agy` is kept alive briefly after a refresh, then stopped on idle. CLI runtime
tears it down immediately after the one-shot fetch.
- Repeated endpoint failures force a relaunch instead of reusing a wedged process forever.
- CodexBar records the launched pid + executable identity and conservatively reaps only its own matching stale `agy`
process on the next launch. It never blind-kills a user-launched `agy`.
### 3) Antigravity IDE local probe
When the Antigravity 2.0 app and `agy` CLI are unavailable, CodexBar probes Antigravity IDE language servers with
`AntigravityStatusProbe(processScope: .ideOnly)`. Current observed IDE payloads return model-level/session quota data
through `GetUserStatus`, `GetAvailableModels`, and `GetCascadeModelConfigData`; `RetrieveUserQuotaSummary` returns 404
from the IDE local server. This means the IDE fallback can show session bars, but should not be expected to provide the
weekly limit shown by Antigravity 2.0.
### 4) OAuth remote fallback
When source mode is `auto`, OAuth is used after app, `agy` CLI, and IDE paths fail if CodexBar has a selected/injected
Google account or an existing shared credentials file. The app, `agy` CLI, and IDE probes still run first, but in
`auto` mode their snapshots are accepted only when the reported account matches the selected account; otherwise the
pipeline falls through to this account-scoped OAuth fetch. When source mode is `oauth`, only OAuth is used and the
shared OAuth file can still be used as a fallback credential source.
## Request body (summary)
- Minimal metadata payload:
- `ideName: antigravity`
- `extensionName: antigravity`
- `locale: en`
- `ideVersion: unknown`
## Parsing and model mapping
- Preferred source fields:
- `response.groups[].displayName`
- `response.groups[].buckets[].bucketId`
- `response.groups[].buckets[].displayName`
- `response.groups[].buckets[].remaining.remainingFraction`
- `response.groups[].buckets[].description`
- Legacy source fields:
- `userStatus.cascadeModelConfigData.clientModelConfigs[].quotaInfo.remainingFraction`
- `userStatus.cascadeModelConfigData.clientModelConfigs[].quotaInfo.resetTime`
- Preferred quota summary UI:
- Render `Gemini Session`, `Gemini Weekly`, `Claude + GPT Session`, and `Claude + GPT Weekly` as named windows.
- Keep Antigravity's bucket description as reset prose; infer `windowMinutes` from the bucket ID/display name.
- Use the most constrained known bucket as the compact/menu-bar metric.
- Legacy user-facing quota groups:
- `Gemini` groups Gemini Pro and Gemini Flash text models.
- `Claude + GPT` groups Claude text models and GPT/GPT-OSS text models.
- Representative selection:
- Hidden model rows such as Lite, autocomplete, and image variants do not drive summary bars.
- For each group, CodexBar uses the lowest remaining known quota row and preserves that row's reset metadata.
- Rows with reset metadata but no remaining fraction stay visible as unavailable reset context only when their group
has no known usage row.
- `resetTime` parsing:
- ISO-8601 preferred; numeric epoch seconds as fallback.
- Identity:
- `accountEmail` and `planName` only from `GetUserStatus`.
## UI mapping
- Provider metadata:
- Display: `Antigravity`
- Labels: `Gemini` (primary), `Claude + GPT` (secondary)
- Status badge: Google Workspace incidents for the Gemini product.
- Antigravity exposes many model rows, but current local payloads show them collapsing into two real usage pools:
Gemini and Claude/GPT. Detailed usage should not list every raw Gemini tier unless a future source exposes a genuinely
distinct unknown or consumed quota window.
- Some Antigravity local/CLI model config entries include reset metadata but omit `remainingFraction`. Those windows stay
in `extraRateWindows` for reset context and are marked with `usageKnown: false`; clients should not render their
`usedPercent` as a real exhausted quota.
## Constraints
- Internal protocol; fields may change.
- Requires `lsof` for local/CLI port detection.
- Local HTTPS uses a self-signed cert; the probe allows insecure TLS only for loopback hosts.
## Key files
- `Sources/CodexBarCore/Providers/Antigravity/AntigravityCLISession.swift`
- `Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift`
- `Sources/CodexBarCore/Providers/Antigravity/AntigravityStatusProbe.swift`
- `Sources/CodexBar/Providers/Antigravity/AntigravityProviderImplementation.swift`
+30
View File
@@ -0,0 +1,30 @@
---
summary: "Architecture overview: modules, entry points, and data flow."
read_when:
- Reviewing architecture before feature work
- Refactoring app structure, app lifecycle, or module boundaries
---
# Architecture overview
## Modules
- `Sources/CodexBarCore`: fetch + parse (Codex RPC, PTY runner, Claude probes, OpenAI web scraping, status polling).
- `Sources/CodexBar`: state + UI (UsageStore, SettingsStore, StatusItemController, menus, icon rendering).
- `Sources/CodexBarWidget`: WidgetKit extension wired to the shared snapshot.
- `Sources/CodexBarCLI`: bundled CLI for `codexbar` usage/status output.
- `Sources/CodexBarClaudeWatchdog`: helper process for stable Claude CLI PTY sessions.
- `Sources/CodexBarClaudeWebProbe`: CLI helper to diagnose Claude web fetches.
## Entry points
- `CodexBarApp`: SwiftUI keepalive + Settings scene.
- `AppDelegate`: wires status controller, Sparkle updater, notifications.
## Data flow
- Background refresh → `UsageFetcher`/provider probes → `UsageStore` → menu/icon/widgets.
- Settings toggles feed `SettingsStore``UsageStore` refresh cadence + feature flags.
## Concurrency & platform
- Swift 6 strict concurrency enabled; prefer Sendable state and explicit MainActor hops.
- macOS 14+ targeting; avoid deprecated APIs when refactoring.
See also: `docs/providers.md`, `docs/refresh-loop.md`, `docs/ui.md`.
+183
View File
@@ -0,0 +1,183 @@
---
summary: "Augment provider notes: cookie auth, keepalive, and credits parsing."
read_when:
- Debugging Augment cookie import or keepalive
- Updating Augment usage or credits parsing
- Adjusting Augment menu labels or settings
---
# Augment Provider
The Augment provider tracks your Augment Code usage and credits through browser cookie-based authentication.
## Features
- **Credits Tracking**: Monitor your remaining credits and monthly limits
- **Usage Monitoring**: Track credits consumed in the current billing cycle
- **Plan Information**: Display your current subscription plan
- **CLI Integration**: Uses `auggie account status` when the Auggie CLI is installed (falls back to web)
- **Automatic Session Keepalive**: Prevents cookie expiration with proactive refresh
- **Multi-Browser Support**: Chrome, Chrome Beta, Chrome Canary, Arc, Safari
## Setup
### 1. Enable the Provider
1. Open **Settings → Providers**
2. Enable **Augment**
3. The app will automatically import cookies from your browser
### 2. Cookie Source Options
**Automatic (Recommended)**
- Automatically imports cookies from your browser
- Supports Chrome, Chrome Beta, Chrome Canary, Arc, and Safari
- Browser priority: Chrome Beta → Chrome → Chrome Canary → Arc → Safari
**Manual**
- Paste a cookie header from your browser's developer tools
- Useful for troubleshooting or custom browser configurations
**Off**
- Disables Augment provider entirely
### 3. Verify Connection
1. Check the menu bar for the Augment icon
2. Click the icon to see your current usage
3. If you see "Log in to Augment", visit [app.augmentcode.com](https://app.augmentcode.com) and sign in
## How It Works
### Cookie Import
The provider searches for Augment session cookies in this order:
1. **Chrome Beta** (if installed)
2. **Chrome** (if installed)
3. **Chrome Canary** (if installed)
4. **Arc** (if installed)
5. **Safari** (if installed)
Recognized cookie names:
- `_session` (legacy)
- `auth0`, `auth0.is.authenticated`, `a0.spajs.txs` (Auth0)
- `__Secure-next-auth.session-token`, `next-auth.session-token` (NextAuth)
- `__Host-authjs.csrf-token`, `authjs.session-token` (AuthJS)
- `session`, `web_rpc_proxy_session` (Augment-specific)
Cached cookies:
- Keychain cache `com.steipete.codexbar.cache` (account `cookie.augment`, source + timestamp). Reused before re-importing
from browsers.
### Auggie CLI Integration
If the `auggie` CLI is installed, CodexBar will prefer `auggie account status` for usage data and avoid browser prompts.
When the CLI is unavailable or not authenticated, CodexBar falls back to browser cookies.
### Automatic Session Keepalive
The provider includes an automatic session keepalive system:
- **Check Interval**: Every 1 minute
- **Refresh Buffer**: Refreshes 5 minutes before cookie expiration
- **Rate Limiting**: Minimum 1 minute between refresh attempts
- **Session Cookies**: Refreshed every 30 minutes (no expiration date)
This ensures your session stays active without manual intervention.
### API Endpoints
The provider fetches data from:
- **Credits**: `https://app.augmentcode.com/api/credits`
- **Subscription**: `https://app.augmentcode.com/api/subscription`
## Troubleshooting
### "No session cookie found"
**Cause**: You're not logged into Augment in any supported browser.
**Solution**:
1. Open [app.augmentcode.com](https://app.augmentcode.com) in Chrome, Chrome Beta, or Arc
2. Sign in to your Augment account
3. Return to CodexBar and click "Refresh" in the menu
### "Session has expired"
**Cause**: Your browser session expired and automatic refresh failed.
**Solution**:
1. Visit [app.augmentcode.com](https://app.augmentcode.com)
2. Log out and log back in
3. Return to CodexBar - it will automatically import fresh cookies
4. If needed, use the menu action **Refresh Session** to force a re-import
### Cookies not importing from Chrome Beta
**Cause**: Chrome Beta might not be in the browser search order.
**Solution**: This fork includes Chrome Beta support by default. If issues persist:
1. Check that Chrome Beta is installed at `/Applications/Google Chrome Beta.app`
2. Verify you're logged into Augment in Chrome Beta
3. Try switching to regular Chrome temporarily
### Manual Cookie Import
If automatic import fails, you can manually paste cookies:
1. Open Chrome DevTools (⌘⌥I)
2. Go to **Application → Cookies → https://app.augmentcode.com**
3. Copy all cookie values
4. Format as: `cookie1=value1; cookie2=value2; ...`
5. In CodexBar: **Settings → Providers → Augment → Cookie Source → Manual**
6. Paste the cookie header
## Debug Mode
To see detailed cookie import logs:
1. Open **Settings → Debug**
2. Find **Augment** in the provider list
3. Click **Show Debug Info**
This displays:
- Cookie import attempts and results
- API request/response details
- Session keepalive activity
- Error messages with timestamps
## Privacy & Security
- Cookies are stored securely in macOS Keychain
- Only cookies for `*.augmentcode.com` domains are imported
- Cookies are filtered by domain before sending to API endpoints
- No cookies are sent to third-party services
- Session keepalive only runs when Augment is enabled
## Technical Details
### Cookie Domain Filtering
The provider implements RFC 6265 cookie semantics:
- ✅ Exact match: `app.augmentcode.com``app.augmentcode.com`
- ✅ Parent domain: `augmentcode.com``app.augmentcode.com`
- ✅ Wildcard: `.augmentcode.com``app.augmentcode.com`
- ❌ Different subdomain: `auth.augmentcode.com` ❌→ `app.augmentcode.com`
This prevents cookies from other subdomains being sent to the API.
### Session Refresh Mechanism
1. Keepalive checks cookie expiration every 1 minute
2. If expiration is within 5 minutes, triggers refresh
3. Pings `/api/auth/session` to trigger cookie update
4. Waits 1 second for browser to update cookies
5. Re-imports fresh cookies from browser
6. Logs success/failure for debugging
## Related Documentation
- [Provider Authoring Guide](provider.md) - How to create new providers
- [Development Guide](DEVELOPMENT.md) - Build and test instructions
+106
View File
@@ -0,0 +1,106 @@
---
summary: "Azure OpenAI provider: API key, endpoint, and deployment validation probe."
read_when:
- Debugging Azure OpenAI provider setup
- Updating Azure OpenAI endpoint or deployment validation
- Explaining Azure OpenAI environment variables
---
# Azure OpenAI provider
CodexBar's Azure OpenAI provider validates that a configured deployment is reachable. It does not read Azure spend,
quota history, or token usage history.
## Authentication
Azure OpenAI requires three values:
1. API key
2. Resource endpoint
3. Deployment name
Settings -> Providers -> Azure OpenAI stores those values in the shared CodexBar config. The same values can also be
provided with environment variables:
```bash
export AZURE_OPENAI_API_KEY="..."
export AZURE_OPENAI_ENDPOINT="https://resource.openai.azure.com"
export AZURE_OPENAI_DEPLOYMENT_NAME="chat-prod"
```
You can store the API key through the CLI:
```bash
printf '%s' "$AZURE_OPENAI_API_KEY" | codexbar config set-api-key --provider azure-openai --stdin
```
The endpoint and deployment are stored as `enterpriseHost` and `workspaceID` in the `azureopenai` provider config:
```json
{
"id": "azureopenai",
"apiKey": "<AZURE_OPENAI_API_KEY>",
"enterpriseHost": "https://resource.openai.azure.com",
"workspaceID": "chat-prod"
}
```
## Data source
CodexBar sends a minimal chat-completions request to validate the deployment:
```http
POST https://resource.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=2024-10-21
api-key: <api key>
Accept: application/json
Content-Type: application/json
```
For dated API versions, the request body contains one `ping` message and `max_tokens: 1`. A successful response is
parsed only for the returned `model` field so the menu can show deployment detail.
Set `AZURE_OPENAI_API_VERSION` to override the API version. When it is set to `v1`, CodexBar uses Azure's
OpenAI-compatible v1 path, includes the deployment name as the request `model`, and uses
`max_completion_tokens: 1`:
```http
POST https://resource.openai.azure.com/openai/v1/chat/completions
```
## Endpoint handling
`AZURE_OPENAI_ENDPOINT` and the configured endpoint field must be HTTPS URLs, or bare hosts that can be normalized to
HTTPS. CodexBar rejects explicit `http://` endpoints, user info, and encoded host-delimiter tricks before attaching the
`api-key` header.
Endpoint paths are preserved. CodexBar avoids duplicating a trailing `/openai` for dated API versions or a trailing
`/openai/v1` for the v1 API when building the validation URL.
Each refresh with complete, valid configuration sends this real inference request and can consume billable input and
output tokens for the configured deployment.
## Display
- Settings shows the provider's static `api` label before a fetch. After a successful fetch, Settings' Source row and
the CLI report `deployment`.
- The menu shows the Azure OpenAI resource host as organization context.
- The primary detail line shows `Deployment: <name>` and includes `Model: <model>` when the validation response returns
one.
- The menu bar usage meter does not show spend, quota, or reset history because the provider only performs deployment
validation.
## CLI usage
```bash
codexbar usage --provider azure-openai
codexbar usage --provider azureopenai
codexbar usage --provider aoai
```
## Key files
- `Sources/CodexBarCore/Providers/AzureOpenAI/AzureOpenAIProviderDescriptor.swift`
- `Sources/CodexBarCore/Providers/AzureOpenAI/AzureOpenAISettingsReader.swift`
- `Sources/CodexBarCore/Providers/AzureOpenAI/AzureOpenAIUsageFetcher.swift`
- `Sources/CodexBar/Providers/AzureOpenAI/AzureOpenAIProviderImplementation.swift`
- `Tests/CodexBarTests/AzureOpenAIUsageFetcherTests.swift`
+121
View File
@@ -0,0 +1,121 @@
---
summary: "AWS Bedrock provider: Cost Explorer spend, CloudWatch Claude activity, credentials, and budgets."
read_when:
- Setting up AWS Bedrock usage tracking
- Debugging Bedrock Cost Explorer or CloudWatch fetches
- Updating Bedrock credentials, region, budget, or activity handling
---
# AWS Bedrock provider
CodexBar reads AWS Cost Explorer for Bedrock spend and can compare the current month against an optional budget. When
permitted, it also reads CloudWatch for rolling 14-day Claude token and request totals in the configured region.
## Authentication
CodexBar supports two authentication modes, selected in Preferences → Providers → AWS Bedrock → Authentication.
### Access keys (default)
Provide static AWS credentials through Settings or the environment inherited by CodexBar/the CLI:
```bash
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_REGION="us-east-1"
```
Optional:
```bash
export AWS_SESSION_TOKEN="..."
export CODEXBAR_BEDROCK_BUDGET="250"
```
### AWS profile
Resolve credentials from a named profile in `~/.aws/config` / `~/.aws/credentials` instead of pasting keys. Set the
profile name in Settings (or via `AWS_PROFILE`). CodexBar shells out to the AWS CLI
(`aws configure export-credentials --profile <name>`), so this works with **SSO**, **assume-role**,
`credential_process`, and MFA-cached profiles — not just static credentials.
Requirements:
- AWS CLI v2 on your `PATH` (CodexBar also checks `/opt/homebrew/bin/aws`, `/usr/local/bin/aws`, and `~/.local/bin/aws`).
Override the location with `AWS_CLI_PATH` if it lives elsewhere.
- For SSO profiles, an active session (`aws sso login --profile <name>`). Credentials are resolved fresh on each
refresh; the AWS CLI caches the SSO token, so this does not re-prompt unless the session has expired.
The profile's region is read automatically (`aws configure get region`); leave the Region field blank to use it, or set
`AWS_REGION` / the Region field to override.
Relevant environment variables:
```bash
export CODEXBAR_BEDROCK_AUTH_MODE="profile" # set automatically by Settings; "keys" or "profile"
export AWS_PROFILE="work"
export AWS_CLI_PATH="/opt/homebrew/bin/aws" # optional override
```
The AWS identity (from either mode) must have permission to call Cost Explorer APIs, including `ce:GetCostAndUsage`.
Grant `cloudwatch:GetMetricData` to add the optional Claude activity totals. Without that permission, cost and budget
tracking continue unchanged.
## Data source
- Service: AWS Cost Explorer.
- Region: `AWS_REGION` or `AWS_DEFAULT_REGION`, defaulting to `us-east-1`.
- Usage: current-month Bedrock spend and historical daily cost buckets.
- Claude activity: rolling 14-day input tokens, output tokens, and requests from the configured region's `AWS/Bedrock`
CloudWatch metrics. Other model families are excluded.
- Budget: `CODEXBAR_BEDROCK_BUDGET`, when set to a positive dollar amount.
- Test override: `CODEXBAR_BEDROCK_API_URL` replaces the Cost Explorer endpoint; use HTTPS or loopback HTTP.
- Test override: `CODEXBAR_BEDROCK_CLOUDWATCH_API_URL` replaces the CloudWatch endpoint; use HTTPS or loopback HTTP.
## Display
- Shows month-to-date Bedrock spend.
- Shows budget progress when a budget is configured.
- Shows rolling 14-day Claude tokens and requests when CloudWatch access is available.
- Reuses the shared inline dashboard for daily cost history when enough buckets are available.
## CLI
```bash
codexbar --provider bedrock --source api
codexbar --provider bedrock --format json --pretty
```
## Troubleshooting
### "No AWS Bedrock cost data available"
- Confirm the credentials are visible to CodexBar.
- Confirm the AWS account has Cost Explorer enabled.
- Confirm the IAM principal can call `ce:GetCostAndUsage`.
- To include Claude token/request totals, confirm the principal can call `cloudwatch:GetMetricData` in the configured
region.
- If using temporary credentials, include `AWS_SESSION_TOKEN`.
- In profile mode, confirm the AWS CLI is installed (or set `AWS_CLI_PATH`) and that the profile name is correct.
### "AWS profile session expired"
The profile's SSO/temporary session has expired. Run `aws sso login --profile <name>` (or refresh the underlying
credentials) and retry.
### "AWS CLI not found"
Profile mode requires AWS CLI v2. Install it (e.g. `brew install awscli`) or point CodexBar at the binary with
`AWS_CLI_PATH`.
### Wrong region
Set `AWS_REGION` or `AWS_DEFAULT_REGION`. Bedrock usage is regional, but Cost Explorer itself is account-level; CodexBar still needs a signing region for the request.
## Key files
- `Sources/CodexBarCore/Providers/Bedrock/BedrockProviderDescriptor.swift`
- `Sources/CodexBarCore/Providers/Bedrock/BedrockSettingsReader.swift`
- `Sources/CodexBarCore/Providers/Bedrock/BedrockProfileCredentialProvider.swift`
- `Sources/CodexBarCore/Providers/Bedrock/BedrockUsageStats.swift`
- `Sources/CodexBarCore/Providers/Bedrock/BedrockAWSSigner.swift`
+52
View File
@@ -0,0 +1,52 @@
---
summary: "Chutes provider: API key setup, subscription usage, and quota windows."
read_when:
- Configuring Chutes usage
- Debugging Chutes subscription or quota requests
---
# Chutes Provider
CodexBar reads subscription and quota usage from Chutes' management API with a manually configured API key.
## Authentication
Create a Chutes API key using the [official authentication guide](https://chutes.ai/docs/getting-started/authentication), then add it in CodexBar Settings → Providers → Chutes.
You can also set the environment variable:
```bash
export CHUTES_API_KEY="cpk_..."
```
Or configure it through the CLI:
```bash
printf '%s' "$CHUTES_API_KEY" | codexbar config set-api-key --provider chutes --stdin
```
## Data Source
CodexBar requests:
- `GET https://api.chutes.ai/users/me/subscription_usage`
- `GET https://api.chutes.ai/users/me/quotas` when subscription data does not contain every usage window
- `GET https://api.chutes.ai/users/me/quota_usage/{chute_id}` for quota details when available
All requests use `Authorization: Bearer cpk_...`. Subscription usage is required; quota-detail requests are best-effort.
## Display
The provider prefers the rolling four-hour window as the primary meter and monthly subscription usage as the secondary meter. Accounts without a subscription can still show available pay-as-you-go quota data.
## CLI Usage
```bash
codexbar --provider chutes
```
## Troubleshooting
- Confirm the key can read `https://api.chutes.ai/users/me/subscription_usage`.
- A `401` or `403` means Chutes rejected the key.
- `CHUTES_API_URL` can override the management API base URL, but CodexBar accepts HTTPS endpoints only.
+216
View File
@@ -0,0 +1,216 @@
---
summary: "Claude fetch behavior comparison covering OAuth, web, CLI, and Keychain prompt changes."
read_when:
- Reviewing Claude fetch regressions since 0.18.0 beta 2
- Changing Claude OAuth, web, CLI, or Keychain prompt behavior
- Comparing old and current Claude credential flows
---
# Claude Fetch Comparison (`7b79b2d` vs `HEAD`)
This document compares Claude data fetching behavior between:
- Baseline commit: `7b79b2d080c6b00f6c8f52f89ac115f33a7ca8b0`
- Current `HEAD`: `37841489f849567a598d2a8ba601eb6f1228644e`
Focus areas:
- OAuth
- Web
- CLI
- Keychain permission prompts, cooldowns, and re-prompt behavior
- OAuth token fetch/use/refresh behavior
## High-level Changes
- OAuth moved from "load token and fail if expired" to "auto-refresh when expired".
- Claude keychain reads now use stricter non-interactive probes (`LAContext.interactionNotAllowed`) before any promptable path.
- New Claude keychain prompt cooldown gate: 6-hour suppression after denial.
- New OAuth refresh failure gate:
- `invalid_grant` => terminal block until auth fingerprint changes.
- repeated `400/401` (non-`invalid_grant`) => transient exponential backoff (up to 6h).
- Auto-mode availability checks were hardened to avoid interactive prompts where possible.
## OAuth Flow
### OAuth (Before: `7b79b2d`)
```mermaid
flowchart TD
A["Claude OAuth fetch starts"] --> B["load(environment)"]
B --> C{"token found?"}
C -- "no" --> K["OAuth not available / fail"]
C -- "yes" --> D{"token expired?"}
D -- "yes" --> E["Fail: token expired (run claude)"]
D -- "no" --> F{"has user:profile scope?"}
F -- "no" --> G["Fail: missing scope"]
F -- "yes" --> H["GET /api/oauth/usage with Bearer access token"]
H --> I{"HTTP success?"}
I -- "yes" --> J["Map usage response -> snapshot"]
I -- "no" --> L["Invalidate cache, surface OAuth error"]
```
### OAuth (Now: `HEAD`)
```mermaid
flowchart TD
A["Claude OAuth fetch starts"] --> B["hasCachedCredentials?"]
B --> C{"cached/refreshable creds exist?"}
C -- "yes" --> D["loadWithAutoRefresh(allowKeychainPrompt=false unless bootstrap needed)"]
C -- "no" --> E["Gate: should allow keychain prompt now?"]
E --> F["loadWithAutoRefresh(allowKeychainPrompt=true if gate allows)"]
D --> G{"expired?"}
F --> G
G -- "no" --> H["Use access token directly"]
G -- "yes" --> I["POST /v1/oauth/token refresh_token grant"]
I --> J{"Refresh status"}
J -- "200" --> K["Save refreshed creds to CodexBar keychain cache + memory"]
J -- "400/401 + invalid_grant" --> L["Record terminal auth failure; block refresh until auth fingerprint changes"]
J -- "400/401 other" --> M["Record transient failure; exponential backoff"]
K --> H
H --> N{"has user:profile scope?"}
N -- "no" --> O["Fail: missing scope"]
N -- "yes" --> P["GET /api/oauth/usage with Bearer access token"]
P --> Q["Map usage response -> snapshot"]
```
### OAuth Token Source Resolution (Now)
```mermaid
flowchart TD
A["load(...)"] --> B["Environment token (CODEXBAR_CLAUDE_OAUTH_TOKEN)"]
B -->|miss| C["Memory cache (valid + unexpired)"]
C -->|miss| D["CodexBar keychain cache: com.steipete.codexbar.cache/oauth.claude"]
D -->|miss| E["~/.claude/.credentials.json"]
E -->|miss| F{"allowKeychainPrompt && prompt gate open?"}
F -- "yes" --> G["Claude keychain service: Claude Code-credentials (promptable fallback)"]
F -- "no" --> H["Stop without keychain prompt path"]
```
## Web Flow
### Web (Before and Now: core fetch path largely unchanged)
```mermaid
flowchart TD
A["Claude Web fetch starts"] --> B{"manual cookie header configured?"}
B -- "yes" --> C["Extract sessionKey from manual header"]
B -- "no" --> D["Enumerate cookie import candidates"]
D --> E["Try browser cookie import (claude.ai domain)"]
E --> F{"sessionKey found?"}
C --> F
F -- "no" --> G["Fail: noSessionKeyFound"]
F -- "yes" --> H["GET organizations + usage endpoints"]
H --> I["Build web usage snapshot + identity + optional cost extras"]
```
### Web Candidate Filtering + Prompt Implications (Now)
```mermaid
flowchart TD
A["cookieImportCandidates(...)"] --> B{"browser uses keychain for decryption?"}
B -- "no (Safari/Firefox/Zen)" --> C["Keep candidate even if keychain disabled"]
B -- "yes (Chromium family)" --> D{"Keychain disabled?"}
D -- "yes" --> E["Drop candidate"]
D -- "no" --> F["Check BrowserCookieAccessGate cooldown"]
F --> G{"cooldown active?"}
G -- "yes" --> E
G -- "no" --> H["Attempt import; on access denied record 6h cooldown"]
```
## CLI Flow
### CLI Fetch Path (Provider Runtime = `.cli`)
```mermaid
flowchart TD
A["CLI command (provider=claude)"] --> B{"source mode"}
B -- "auto" --> C["Strategy order: web -> cli"]
B -- "web" --> D["Web strategy only"]
B -- "cli" --> E["CLI PTY strategy only"]
B -- "oauth" --> F["OAuth strategy only"]
E --> G["ClaudeStatusProbe via PTY session"]
G --> H["Parse /usage output -> snapshot"]
```
Notes:
- The PTY parsing path itself is functionally stable in this range.
- Claude CLI session environment still scrubs OAuth env overrides and `ANTHROPIC_*` vars before launching the subprocess.
## App Runtime Auto Pipeline (Provider Fetch Plan)
### App Auto Strategy Order (Descriptor Pipeline)
```mermaid
flowchart TD
A["App runtime source=auto"] --> B["Try OAuth strategy first"]
B -->|"available + success"| Z["Done"]
B -->|"unavailable or fallback on error"| C["Try Web strategy"]
C -->|"success"| Z
C -->|"unavailable or fallback on error"| D["Try CLI strategy"]
D -->|"success"| Z
D -->|"fail"| E["Provider fetch failure"]
```
### Internal `ClaudeUsageFetcher(dataSource:.auto)` Heuristic (Now)
```mermaid
flowchart TD
A["ClaudeUsageFetcher.loadLatestUsage(dataSource=.auto)"] --> B["Probe OAuth creds non-interactively"]
B --> C{"usable OAuth creds?"}
C -- "yes" --> D["Use OAuth path"]
C -- "no" --> E{"has web session?"}
E -- "yes" --> F["Use Web path"]
E -- "no" --> G{"claude binary present?"}
G -- "yes" --> H["Try CLI PTY, if fails then OAuth"]
G -- "no" --> I["Fallback to OAuth"]
```
## Keychain Prompt / Re-prompt Behavior
### Claude OAuth Keychain Prompt Gate (Now)
```mermaid
stateDiagram-v2
[*] --> PromptAllowed
PromptAllowed --> CooldownBlocked: recordDenied()
CooldownBlocked --> CooldownBlocked: shouldAllowPrompt() before deniedUntil
CooldownBlocked --> PromptAllowed: time >= deniedUntil (6h)
```
### OAuth Refresh Failure Gate (Now)
```mermaid
stateDiagram-v2
[*] --> Open
Open --> TransientBackoff: recordTransientFailure (400/401 non-invalid_grant)
TransientBackoff --> Open: cooldown expires
TransientBackoff --> Open: auth fingerprint changes
Open --> TerminalBlocked: recordTerminalAuthFailure (invalid_grant)
TerminalBlocked --> TerminalBlocked: unchanged auth fingerprint
TerminalBlocked --> Open: auth fingerprint changes
TransientBackoff --> TerminalBlocked: terminal auth failure observed
TerminalBlocked --> Open: recordSuccess
```
## Key Differences in Permission Prompt Surfaces
- Before:
- OAuth availability/load paths could reach Claude keychain access with less strict non-interactive protection.
- No dedicated Claude OAuth prompt cooldown gate.
- No terminal-vs-transient refresh failure gate.
- Now:
- Non-interactive probes are stricter and reused broadly.
- Promptable Claude keychain access is gated and usually only bootstrap-oriented.
- Denials cause cooldown suppression to avoid repeated prompts.
- Refresh failures can suppress repeated attempts until either timeout (transient) or credential change (terminal).
## Related Files
- `/Users/ratulsarna/Developer/staipete/CodexBar/Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift`
- `/Users/ratulsarna/Developer/staipete/CodexBar/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift`
- `/Users/ratulsarna/Developer/staipete/CodexBar/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift`
- `/Users/ratulsarna/Developer/staipete/CodexBar/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthKeychainAccessGate.swift`
- `/Users/ratulsarna/Developer/staipete/CodexBar/Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthRefreshFailureGate.swift`
- `/Users/ratulsarna/Developer/staipete/CodexBar/Sources/CodexBarCore/KeychainAccessPreflight.swift`
- `/Users/ratulsarna/Developer/staipete/CodexBar/Sources/CodexBarCore/KeychainNoUIQuery.swift`
- `/Users/ratulsarna/Developer/staipete/CodexBar/Sources/CodexBarCore/BrowserCookieImportOrder.swift`
- `/Users/ratulsarna/Developer/staipete/CodexBar/Sources/CodexBarCore/BrowserCookieAccessGate.swift`
@@ -0,0 +1,183 @@
---
summary: "Accepted design for Claude subscription accounts and per-account menu bar items."
read_when:
- Reviewing Claude multi-account support
- Designing per-account status items
- Evaluating claude-swap integration
---
# Claude multi-account and status item decision
Status: **Phase 1 account display implemented; Phase 2 explicit account activation accepted.**
Related: [#1756](https://github.com/steipete/CodexBar/issues/1756),
[#1268](https://github.com/steipete/CodexBar/issues/1268), and the bounded Claude sign-in repair in
[#1811](https://github.com/steipete/CodexBar/pull/1811).
## Accepted direction
1. Use an opt-in `claude-swap` adapter as the first Claude subscription multi-account source.
2. Normalize its results behind a provider-neutral account snapshot before adding any status item UI.
3. Make per-account status items opt-in, replace the provider item for that provider, cap selection at four, and keep
them mutually exclusive with Merge Icons.
4. Allow an explicit click on an inactive account card to invoke exactly `cswap --switch-to <slot> --json`. Keep
automatic switching and session launching out of scope.
This solves the durable OAuth refresh problem without making CodexBar a second credential vault. It also avoids a
Claude-only status item implementation that would need to be redesigned for Codex and other providers.
![Proposed multi-account settings and status items](screenshots/claude-multi-account-status-items-proposal.svg)
## Current architecture and gap
CodexBar has three account concepts today:
- The ambient Claude OAuth credential is routed from CodexBar's cache, Claude Code's credentials file, or Claude
Code's Keychain item. It represents one active credential. Claude Code-owned expired credentials delegate refresh
back to the CLI; CodexBar-owned cached credentials can refresh directly.
- `ProviderTokenAccount` stores a label and one token plus optional provider metadata. It has no refresh token or
expiry model. Claude entries therefore work for session cookies, Admin API keys, or short-lived OAuth access tokens,
but they are not durable multi-subscription OAuth sessions.
- `TokenAccountUsageSnapshot` and `CodexAccountUsageSnapshot` separately project multi-account usage into menus.
Status items remain provider-scoped: `StatusItemIdentity` has only `merged` and `provider`, and
`statusItems` is keyed by `UsageProvider`.
The recently merged [#1800](https://github.com/steipete/CodexBar/pull/1800) scopes Claude OAuth history to the routed
Keychain identity. [#1776](https://github.com/steipete/CodexBar/pull/1776) prevents CLI-runtime usage refreshes from
delegating credential repair to Claude Code, while app and user-initiated repair remain available. Both changes improve
single-active-account correctness; neither discovers or displays multiple subscriptions.
The closed [#1707](https://github.com/steipete/CodexBar/pull/1707) should not be revived. It coupled account discovery,
credential resolution, provider routing, menu rendering, and animation across a large patch while broadening
Keychain and prompt behavior. The safer seam is a credential-free usage adapter first.
## Source options
| Option | Credential ownership | Durability | Risk | Recommendation |
| --- | --- | --- | --- | --- |
| First-party OAuth account vault | CodexBar | High | New login, refresh, storage, revocation, migration, and security surface | Defer |
| Bounded `claude-swap` adapter | `claude-swap` | High | External executable and schema dependency | **Phase 12** |
| Discover Claude Code Keychain entries | Claude Code / ambiguous | Unknown | Undocumented enumeration; prompt and identity hazards | Reject |
| Existing token accounts | CodexBar config | Low for OAuth | Access token expires without refresh metadata | Keep for current cookie/API-key uses |
As of [`claude-swap` v0.18.0](https://github.com/realiti4/claude-swap/releases/tag/v0.18.0),
`cswap --list --json` still returns a versioned object with `schemaVersion: 1`, an active account number, account slots,
redaction-sensitive email labels, 5-hour and 7-day usage percentages, and reset timestamps. Handled failures return an
error object and non-zero exit. Direct switching returns the same versioned envelope. CodexBar does not need
`--token-status`, credential files, Keychain access, or raw OAuth values for display or explicit activation.
## Phase 1 adapter contract
- Disabled by default. User chooses an executable path and enables “Read accounts from claude-swap.”
- Execute exactly the argument array `cswap --list --json`. Never invoke a shell or accept config-defined passthrough
arguments.
- Require `schemaVersion == 1`; reject unknown versions and partial top-level shapes.
- Bound runtime and stdout, terminate on timeout, and retain the last successful snapshot with a stale marker.
- Parse only slot number, active state, usage status, 5-hour/7-day percentages, and reset timestamps.
- Treat email as display-only sensitive data. Never log or persist it. Respect Hide Personal Info.
- Use the source-issued numeric slot for identity (`claude-swap:<slot>`), not email or credential-derived values.
- CodexBar never reads `claude-swap` storage, Claude Code storage, environment credentials, or Keychain entries. The
subprocess remains solely responsible for its own credential access. The adapter copies only allow-listed
usage/identity fields into its model and never logs or persists raw stdout.
- Never run `auto`, `run`, `--switch`, `--switch-to`, `--add-account`, export, import, purge, or any other command in
Phase 1.
- Isolate adapter failure from ambient Claude usage. Users without `claude-swap` see no behavior change.
The executable is an optional external dependency, not a bundled component. Preferences should show detected version,
last refresh, adapter errors, and a link to the upstream project; CodexBar should not install or update it.
## Phase 2 explicit activation contract
- Only an explicit click on an inactive, actionable account card can start a switch.
- Derive the numeric slot from the already validated account snapshot and execute exactly
`cswap --switch-to <slot> --json`; never accept free-form arguments or invoke a shell.
- Serialize switches, validate `schemaVersion == 1` and the returned target slot, and bound captured output.
- Once launched, let the external credential transaction reach its natural exit without forced timeout or
cancellation. If the adapter setting changes, hide its UI state and discard its result when the original
configuration is no longer current.
- Refresh ambient Claude usage and the adapter account list after completion. Show switch errors independently from
list-refresh errors and preserve the last successful usage snapshots.
- Keep expired, missing, unknown, and Keychain-inaccessible credential slots non-actionable. Never auto-switch, launch
sessions, add/import/export/purge accounts, or mutate credentials directly.
## Provider-neutral account model
Introduce one projection used by menus and status items rather than teaching status item code about Claude OAuth:
```swift
struct ProviderAccountUsageSnapshot: Identifiable {
let id: ProviderAccountIdentity
let provider: UsageProvider
let displayLabel: String
let isActive: Bool
let canActivate: Bool
let snapshot: UsageSnapshot?
let error: String?
let sourceLabel: String?
}
struct ProviderAccountIdentity: Hashable {
let source: String
let opaqueID: String
}
```
Adapters own identity conversion. UI receives a user alias or privacy-safe ordinal when personal information is hidden.
No provider may fill identity, plan, or usage fields using another provider's data.
Existing `TokenAccountUsageSnapshot` and `CodexAccountUsageSnapshot` can migrate behind this projection in small,
separately reviewed steps. Their credential and refresh logic stays source-specific.
## Per-account status item behavior
Proposed setting under each provider's Accounts section:
- `One provider icon` (default; current behavior)
- `Selected account icons`, with up to four account checkboxes
Selecting account icons replaces that provider's aggregate item; it does not add duplicates. Account items use a
stable `StatusItemIdentity.account(provider:source:opaqueID:)`, preserve existing provider autosave names, and open the
provider menu focused on that account. A short user alias or ordinal badge distinguishes otherwise identical provider
icons. Hide Personal Info replaces labels with `Account 1`, `Account 2`, and so on.
Merge Icons continues to mean exactly one status item. Account-icon controls are disabled while it is enabled, with a
button to turn Merge Icons off. Existing users and status item positions remain unchanged until they opt in.
The alternative proposed in the #1268 discussion is a per-account toggle that adds selected account items, leaves
unselected accounts under the provider item, and coexists with Merge Icons. That is more granular, but it creates
duplicate provider/account items, makes “Merge Icons” no longer mean one item, and multiplies autosave and recovery
states. The replacement mode above is the recommendation; if maintainers prefer the additive mode, grouping and Merge
Icons semantics must be decided before implementation.
## UI proof
The mock above shows the recommended mode and its Merge Icons conflict. It is intentionally a decision artifact, not
an implementation screenshot. The following packaged synthetic-account proof verifies the bounded current behavior:
the account action is now named “Sign in with Claude Code…” and no longer claims it will add a durable CodexBar account.
No real credential, browser session, or provider call was used.
![Packaged synthetic Claude sign-in proof](screenshots/claude-sign-in-synthetic-proof.png)
## Accepted decisions
1. The optional external `claude-swap` dependency is accepted for exact `cswap --list --json` execution and explicit
`cswap --switch-to <slot> --json` activation.
2. Automatic switching, account add/import/export/purge, and session launching stay out of scope.
3. Provider-neutral account snapshots land before any per-account status item work.
4. Per-account status items are capped at four and mutually exclusive with Merge Icons.
5. Status item labels use aliases or privacy-safe ordinals, never email identity.
Any further change to these decisions requires a new product/auth review before implementation because it changes
storage, status item migration, process authority, or the credential boundary.
## Implementation and validation sequence
1. Add fixtures for schema v1, error payloads, unknown versions, invalid percentages/timestamps, output limits, and
process timeout. Use a fake executable only.
2. Add the opt-in adapter and provider-neutral projection. Verify no credential reads and no impact on ambient Claude.
3. Add settings-state and menu-model tests. Keep AppKit status item creation out of headless tests.
4. Add status item identity/migration tests, then implement account items behind the opt-in setting.
5. Add exact-argv, strict switch-result, serialization, and refresh tests using a fake executable only.
6. Run focused tests, `make check`, `make test`, packaged synthetic proof, and macOS UI proof with redacted fixtures.
No credential import, automatic switching, session launching, or compatibility shim is part of this proposal.
+203
View File
@@ -0,0 +1,203 @@
---
summary: "Claude provider data sources: OAuth API, web API (cookies), CLI PTY, and local cost usage."
read_when:
- Debugging Claude usage/status parsing
- Updating Claude OAuth/web endpoints or cookie import
- Adjusting Claude CLI PTY automation
- Reviewing local cost usage scanning
---
# Claude provider
Claude supports three usage data paths plus local cost usage. The main provider pipeline uses runtime-specific
automatic selection, but the codebase still has multiple active Claude `.auto` decision sites while the refactor is
pending. For the exact current-state parity contract, see
[docs/refactor/claude-current-baseline.md](refactor/claude-current-baseline.md).
When an Anthropic Admin API key is configured, Claude can also show organization-level spend/messages/tokens in the
same inline dashboard pattern used by the OpenAI API provider.
## Data sources + selection order
### Default selection (debug menu disabled)
- If an Admin API key is configured, the Admin API strategy is used for Claude API spend/usage.
- App runtime main pipeline: OAuth API → CLI PTY → Web API.
- CLI runtime main pipeline: Web API → CLI PTY.
- Explicit picker modes (OAuth/Web/CLI) bypass automatic fallback.
- A lower-level direct Claude fetcher still contains a separate `.auto` order. That inconsistency is tracked in
[docs/refactor/claude-current-baseline.md](refactor/claude-current-baseline.md).
Usage source picker:
- Preferences → Providers → Claude → Usage source (Auto/OAuth/Web/CLI).
Admin API key setup:
- Preferences → Providers → Claude → Admin API key, stored in `~/.codexbar/config.json`.
- CLI/env: `printf '%s' "$ANTHROPIC_ADMIN_KEY" | codexbar config set-api-key --provider claude --stdin`.
- Token accounts can also hold `sk-ant-admin...` keys; they route to the Admin API instead of cookie/OAuth usage.
- Environment fallback: `ANTHROPIC_ADMIN_KEY`.
## Admin API
- Key prefix: `sk-ant-admin...`.
- Endpoints:
- `/v1/organizations/cost_report`
- `/v1/organizations/usage_report/messages`
- Output:
- Today/7d/30d spend and message/token summaries.
- Inline 30-day dashboard chart when daily buckets are present.
- Identity login method: `Admin API`.
## Keychain prompt policy (Claude OAuth)
- Preferences → Providers → Claude → Keychain prompt policy.
- Options:
- `Never prompt`: never attempts interactive Claude OAuth Keychain prompts.
- `Only on user action` (default): interactive prompts are reserved for user-initiated repair flows.
- `Always allow prompts`: allows interactive prompts in both user and background flows.
- This setting only affects Claude OAuth Keychain prompting behavior; it does not switch your Claude usage source.
- If Preferences → Advanced → Disable Keychain access is enabled, this policy remains visible but inactive until
Keychain access is re-enabled.
### Debug selection (debug menu enabled)
- The Debug pane can force OAuth / Web / CLI.
- Web extras are internal-only (not exposed in the Providers pane).
## OAuth API (preferred)
- Credentials:
- CodexBar OAuth cache when available.
- File fallback: `~/.claude/.credentials.json`.
- Claude CLI Keychain bootstrap/repair fallback: `Claude Code-credentials`.
- On Claude Code 2.1.x, `Claude Code-credentials` may contain only MCP server OAuth state (`mcpOAuth`) with no `claudeAiOauth`. CodexBar treats that as an OAuth configuration error, does not run background delegated `claude /status` refresh, and surfaces re-auth guidance. Use Web or CLI usage source, or restore a valid Claude OAuth keychain entry. See #1844.
- Requires `user:profile` scope (CLI tokens with only `user:inference` cannot call usage).
- Endpoint:
- `GET https://api.anthropic.com/api/oauth/usage`
- Headers:
- `Authorization: Bearer <access_token>`
- `anthropic-beta: oauth-2025-04-20`
- Mapping:
- `five_hour` → session window.
- `seven_day` → weekly window; also becomes the primary fallback when `five_hour` is absent or has no utilization.
- `seven_day_sonnet` / `seven_day_opus` → model-specific weekly window.
- `seven_day_routines` / `seven_day_cowork` → Daily Routines extra window.
- Claude Design/Omelette keys are ignored because Claude Design shares the main Claude usage limit.
- `extra_usage` → Extra usage cost (monthly spend/limit).
- Successful OAuth login enables Claude and preserves the selected usage source. With the default Auto source, OAuth
remains preferred when readable, while CLI/Web fallback stays available when OAuth credentials are not usable.
- Plan inference: `subscriptionType` is preferred when present; `rate_limit_tier` falls back to
Max/Pro/Team/Enterprise. When a Max `rate_limit_tier` carries a usage multiplier
(`default_claude_max_5x` / `default_claude_max_20x`), it is surfaced in the label as "Max 5x" / "Max 20x".
## Web API (cookies)
- Preferences → Providers → Claude → Cookie source (Automatic or Manual).
- Manual mode accepts a `Cookie:` header from a claude.ai request.
- Multi-account manual tokens: add entries to `~/.codexbar/config.json` (`tokenAccounts`) and set Claude cookies to
Manual. The menu can show all accounts stacked or a switcher bar (Preferences → Advanced → Display).
- Claude token accounts accept either `sessionKey` cookies or OAuth access tokens (`sk-ant-oat...`). OAuth-token
accounts route to the OAuth path and disable cookie mode; session-key or cookie-header accounts stay in manual
cookie mode. The exact edge-routing rules are documented in
[docs/refactor/claude-current-baseline.md](refactor/claude-current-baseline.md).
- Cookie source order:
1) Safari: `~/Library/Cookies/Cookies.binarycookies`
2) Chrome/Chromium forks: `~/Library/Application Support/Google/Chrome/*/Cookies`
3) Firefox: `~/Library/Application Support/Firefox/Profiles/*/cookies.sqlite`
- Domain: `claude.ai`.
- Cookie name required:
- `sessionKey` (value prefix `sk-ant-...`).
- Cached cookies: Keychain cache `com.steipete.codexbar.cache` (account `cookie.claude`, source + timestamp).
Reused before re-importing from browsers.
- API calls (all include `Cookie: sessionKey=<value>`):
- `GET https://claude.ai/api/organizations` → org UUID.
- `GET https://claude.ai/api/organizations/{orgId}/usage` → session/weekly/opus.
- `GET https://claude.ai/api/organizations/{orgId}/overage_spend_limit` → Extra usage spend/limit.
- `GET https://claude.ai/api/account` → email + plan hints.
- Outputs:
- Session + weekly + model-specific percent used.
- Daily Routines extra window when returned by the usage API.
- Extra usage spend/limit (if enabled).
- Account email + inferred plan.
## claude-swap accounts (opt-in)
The accepted multi-account design in
[claude-multi-account-and-status-items.md](claude-multi-account-and-status-items.md).
- Setup: Preferences → Providers → Claude → "Read accounts from claude-swap", then set the path to the
[`cswap`](https://github.com/realiti4/claude-swap) executable (for example `~/.local/bin/cswap`).
- Behavior: on each Claude refresh, CodexBar runs `cswap --list --json` independently of the ambient Claude fetch (no
shell, fixed arguments, bounded runtime and output), requires `schemaVersion == 1`, and parses only slot number,
active state, usage status, email (display only), and the 5-hour/7-day windows.
- Display: when claude-swap reports more than one account, the Claude menu shows one stacked card per
account (active account first) alongside nothing else changing; with zero or one account the menu is
unchanged. Account identity is `claude-swap:<slot>`.
- Isolation: CodexBar never reads claude-swap or Claude Code credential storage for this feature; the
subprocess handles its own credential access. Adapter failures keep the last successful accounts as
stale data, surface the error in provider settings, and never affect the ambient Claude usage card.
- Sentinel statuses (`token_expired`, `api_key`, `keychain_unavailable`, `no_credentials`,
`unavailable`) render as per-account notes instead of usage bars.
- Switching: an inactive account with usable source credentials shows “Switch Account…”. Clicking it runs exactly
`cswap --switch-to <slot> --json`, validates the versioned result and requested slot, then refreshes both ambient
Claude usage and every claude-swap account card. Switches are serialized; no automatic switching occurs.
- Expired, missing, unknown, or Keychain-inaccessible credentials stay non-actionable. A failed switch remains visible
on that account without discarding its last successful usage. A running Claude Code process can take up to the
claude-swap Keychain cache interval to observe the new account.
- When multiple claude-swap accounts are available, they take explicit precedence over Claude
token-account presentation (stacked cards and the segmented switcher).
Packaged synthetic proof (fake `cswap` executable, no real accounts or credentials):
![Stacked claude-swap account cards](screenshots/claude-swap-accounts-synthetic-proof.png)
## CLI PTY (fallback)
- Runs `claude` in a PTY session (`ClaudeCLISession`).
- Default behavior: exit after each probe; Debug → "Keep CLI sessions alive" keeps it running between probes.
- Probe working directory: `~/Library/Application Support/CodexBar/ClaudeProbe` with local Claude settings that disable
deep-link URL handler registration during headless probes.
- After transient probes exit, CodexBar removes Claude Code `.jsonl` session artifacts for that dedicated
`ClaudeProbe` project directory so background `/usage` polling does not clutter the user's Claude project history.
- Command flow:
1) Start CLI with `--allowed-tools ""` (no tools).
2) Auto-respond to first-run prompts (trust files, workspace, telemetry).
3) Send `/usage`, wait for rendered panel; send Enter retries if needed.
4) Optionally send `/status` to extract identity fields.
- Parsing (`ClaudeStatusProbe`):
- Strips ANSI, locates "Current session" + "Current week" headers.
- Extracts percent left/used and reset text near those headers.
- Parses `Account:` and `Org:` lines when present.
- Surfaces CLI errors (e.g. token expired) directly.
- Some Education and organization-managed subscriptions return only a subscription notice, with no numeric
session or weekly quota fields. CodexBar reports those limits as unavailable, keeps local cost/token history
visible, and never derives quota percentages from spend or token totals.
## Cost usage (local log scan)
- Source roots:
- Native Claude logs:
- `$CLAUDE_CONFIG_DIR` (comma-separated), each root uses `<root>/projects`.
- Fallback roots:
- `~/.config/claude/projects`
- `~/.claude/projects` (Claude Code and current Claude Desktop Code/Cowork CLI sessions)
- Additional embedded Claude Desktop project stores, when present:
- `~/Library/Application Support/Claude/local-agent-mode-sessions/**/.claude/projects`
- `~/Library/Application Support/Claude/claude-code-sessions/**/.claude/projects`
- Current Claude Desktop metadata under `claude-code-sessions` points to shared CLI session JSONL by
`cliSessionId`; metadata-only directories are not treated as usage sources.
- Supported pi sessions:
- `~/.pi/agent/sessions/**/*.jsonl`
- Files: `**/*.jsonl` under the native project roots, discovered Claude Desktop project roots,
plus supported pi session files.
- Parsing:
- Native Claude logs parse lines with `type: "assistant"` and `message.usage`.
- Uses per-model token counts (input, cache read/create, output).
- Deduplicates streaming chunks by `message.id + requestId` (usage is cumulative per chunk).
- pi sessions attribute `anthropic` assistant usage to Claude and bucket it by assistant-turn timestamp, so a single pi
session can contribute to multiple models/days.
- Cache:
- Native + merged provider cache: `~/Library/Caches/CodexBar/cost-usage/claude-v2.json`
- pi session cache: `~/Library/Caches/CodexBar/cost-usage/pi-sessions-v1.json`
## Key files
- OAuth: `Sources/CodexBarCore/Providers/Claude/ClaudeOAuth/*`
- Web API: `Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift`
- CLI PTY: `Sources/CodexBarCore/Providers/Claude/ClaudeStatusProbe.swift`,
`Sources/CodexBarCore/Providers/Claude/ClaudeCLISession.swift`
- Cost usage: `Sources/CodexBarCore/CostUsageFetcher.swift`,
`Sources/CodexBarCore/PiSessionCostScanner.swift`,
`Sources/CodexBarCore/PiSessionCostCache.swift`,
`Sources/CodexBarCore/Vendored/CostUsage/*`
+51
View File
@@ -0,0 +1,51 @@
---
summary: "ClawRouter setup for monthly budget, spend, and routed-provider usage."
read_when:
- Configuring ClawRouter usage tracking
- Debugging ClawRouter budget or provider breakdown display
- Explaining ClawRouter API key and base URL settings
---
# ClawRouter
CodexBar reads the policy attached to a ClawRouter API key. The menu card shows its monthly budget, spend, requests,
tokens, and provider breakdown. Provider rows come directly from ClawRouter, so the integration works with any routed
model provider configured there; CodexBar does not need a separate provider plugin for each route.
## Setup
Create a ClawRouter key with access to the routes you want, then store it in CodexBar:
```bash
printf '%s' "$CLAWROUTER_API_KEY" | codexbar config set-api-key --provider clawrouter --stdin
```
You can also paste the key in CodexBar Settings → Providers → ClawRouter. The hosted service is used by default:
```text
https://clawrouter.openclaw.ai
```
For another deployment, set the Base URL in Settings or use `CLAWROUTER_BASE_URL`. The value may point to the service
root or `/v1`; CodexBar normalizes both to `/v1/usage`. Overrides must be HTTPS URLs or bare hosts normalized to HTTPS.
## Display
- Monthly budget meter and reset date when the policy has a budget.
- This-month spend against the configured limit.
- Request count and total token usage.
- Up to five routed-provider rows, ordered by spend and request count.
- Unmetered policy status and spend when no monthly limit is configured.
ClawRouter usage is policy-wide. If one key can route OpenAI, Anthropic, Google, OpenRouter, or non-model services, the
same CodexBar card aggregates them and lists the provider identifiers returned by `/v1/usage`.
## Environment variables
| Variable | Description |
| --- | --- |
| `CLAWROUTER_API_KEY` | ClawRouter API key. |
| `CLAWROUTER_BASE_URL` | Optional HTTPS service root or `/v1` URL. |
CodexBar sends the key only to the validated ClawRouter endpoint. `/v1/usage` returns accounting metadata; CodexBar
never receives routed prompts or model responses.
+115
View File
@@ -0,0 +1,115 @@
---
summary: "CodexBar CLI configuration commands for provider toggles, API keys, and isolated config files."
read_when:
- Using codexbar config from scripts or CI
- Enabling or disabling providers without opening Settings
- Storing provider API keys from the command line
---
# CLI configuration
`codexbar config` edits the same resolved config file used by the app's Settings → Providers pane.
New installs use `~/.config/codexbar/config.json`; absolute `XDG_CONFIG_HOME` paths and `CODEXBAR_CONFIG` are
supported, and existing `~/.codexbar/config.json` installs keep using the legacy file when no XDG config exists.
The CLI writes the file with `0600` permissions.
## Providers
List persistent provider toggles:
```bash
codexbar config providers
codexbar config providers --json --pretty
```
Enable or disable a provider:
```bash
codexbar config enable --provider grok
codexbar config disable --provider cursor
```
These are persistent app/CLI settings. They are different from `codexbar usage --provider grok`, which is a one-shot
command override and does not edit config.
If every provider is disabled, `codexbar usage` with no `--provider` prints no text output, and
`codexbar usage --json` prints `[]`. Passing `--provider <name>` still fetches that provider for the one command.
## API keys
API keys are stored under the provider entry in config:
```bash
printf '%s' "$ELEVENLABS_API_KEY" | codexbar config set-api-key --provider elevenlabs --stdin
```
`set-api-key` enables the provider by default. Add `--no-enable` when you only want to save the key:
```bash
printf '%s' "$OPENROUTER_API_KEY" | codexbar config set-api-key --provider openrouter --stdin --no-enable
```
Useful examples:
```bash
printf '%s' "$OPENAI_ADMIN_KEY" | codexbar config set-api-key --provider openai --stdin
printf '%s' "$ANTHROPIC_ADMIN_KEY" | codexbar config set-api-key --provider claude --stdin
printf '%s' "$DEEPGRAM_API_KEY" | codexbar config set-api-key --provider deepgram --stdin
printf '%s' "$GROQ_API_KEY" | codexbar config set-api-key --provider groq --stdin
printf '%s' "$LLM_PROXY_API_KEY" | codexbar config set-api-key --provider llmproxy --stdin
printf '%s' "$Z_AI_API_KEY" | codexbar config set-api-key --provider zai --stdin
```
For a z.ai team account:
```bash
printf '%s' "$Z_AI_API_KEY" | codexbar config set-api-key --provider zai --stdin \
--label Team \
--usage-scope team \
--organization-id org_... \
--workspace-id proj_...
```
Use single-line BigModel organization/project IDs; see [z.ai](zai.md).
Only providers that consume config-backed API keys accept this command. Admin API providers may require a key with
organization/usage permissions, not a normal inference key. Browser/OAuth providers such as Grok use their own provider
sessions instead of an xAI API key for CodexBar's billing view, so enable them with
`codexbar config enable --provider grok`.
LLM Proxy also needs a base URL. Use `LLM_PROXY_BASE_URL` for CLI runs, or add `"enterpriseHost"` to the provider entry
in the CodexBar config file.
## Isolated config files
For tests, demos, and CI, point CodexBar at a temporary config file:
```bash
export CODEXBAR_CONFIG=/tmp/codexbar-config.json
codexbar config enable --provider grok
codexbar config providers --json --pretty
```
The override applies to both reads and writes for the current process environment.
## Cost history window
The app setting controls the menu's local cost-history window. For one-off CLI reports, pass `--days`:
```bash
codexbar cost --provider codex --days 90
codexbar cost --provider claude --days 180 --format json --pretty
```
The accepted range is 1...365 days.
## Validation
After hand-editing config:
```bash
codexbar config validate
codexbar config dump --pretty
```
`dump` prints normalized config, including providers omitted from a hand-written file.
+263
View File
@@ -0,0 +1,263 @@
---
summary: "CodexBar CLI for fetching usage from the command line."
read_when:
- "You want to call CodexBar data from scripts or a terminal."
- "Adding or modifying Commander-based CLI commands."
- "Aligning menubar and CLI output/behavior."
---
# CodexBar CLI
A lightweight Commander-based CLI that mirrors the menu bar apps provider fetchers and config file.
Use it when you need usage numbers in scripts, CI, or dashboards without UI.
## Install
- In the app: **Preferences → Advanced → Install CLI**. This symlinks `CodexBarCLI` to `/usr/local/bin/codexbar` and `/opt/homebrew/bin/codexbar`.
- From the repo, after installing `CodexBar.app` in `/Applications`: `./bin/install-codexbar-cli.sh` (same symlink targets).
- Manual: `ln -sf "/Applications/CodexBar.app/Contents/Helpers/CodexBarCLI" /usr/local/bin/codexbar`.
### Release tarball install (macOS/Linux)
- Homebrew formula (Linux today): `brew install steipete/tap/codexbar`.
- Download release tarballs from GitHub Releases:
- macOS: `CodexBarCLI-v<tag>-macos-arm64.tar.gz`, `CodexBarCLI-v<tag>-macos-x86_64.tar.gz`
- Linux (glibc): `CodexBarCLI-v<tag>-linux-aarch64.tar.gz`, `CodexBarCLI-v<tag>-linux-x86_64.tar.gz`
- Linux (static musl): `CodexBarCLI-v<tag>-linux-musl-aarch64.tar.gz`, `CodexBarCLI-v<tag>-linux-musl-x86_64.tar.gz`
- Extract and run `./codexbar` (symlink) or `./CodexBarCLI`.
```
tar -xzf CodexBarCLI-v0.17.0-macos-x86_64.tar.gz
./codexbar --version
./codexbar usage --format json --pretty
```
## Build
- `./Scripts/package_app.sh` (or `./Scripts/compile_and_run.sh`) bundles `CodexBarCLI` into `CodexBar.app/Contents/Helpers/CodexBarCLI`.
- Standalone: `swift build -c release --product CodexBarCLI` (binary at `./.build/release/CodexBarCLI`).
- Dependencies: Swift 6.2+, Commander package (`https://github.com/steipete/Commander`).
## Configuration
CodexBar reads the resolved config file for provider settings, secrets, and ordering. New installs use
`~/.config/codexbar/config.json`; absolute `XDG_CONFIG_HOME` paths and `CODEXBAR_CONFIG` are supported, and existing
`~/.codexbar/config.json` installs keep using the legacy file when no XDG config exists.
See `docs/configuration.md` for the schema.
## Command
- `codexbar` defaults to the `usage` command.
- `--format text|json` (default: text).
- `codexbar cost` prints local token cost usage for Claude + Codex without web/CLI access.
- `--format text|json` (default: text).
- `--refresh` ignores cached scans.
- `codexbar cards` prints a one-shot usage snapshot as a responsive terminal card grid.
- Reuses the same provider, source, account, credits, and status flags as `codexbar usage`.
- Account lines and plan badges are included in the card grid by default.
- `--brief` renders a compact table (Provider / Usage / Reset) instead of the card grid.
- Stdout is always rendered text; `--json-output` only affects stderr logs (no JSON card payload).
- Failed providers are summarized in a footer (not rendered as error cards).
- Honors `$COLUMNS` for layout; falls back to 80 columns. Use `--no-color` for plain output.
- Kitty, Ghostty, WezTerm, and other truecolor terminals auto-enable enhanced gradients/outlines.
- Force enhanced mode elsewhere with `CODEXBAR_CARDS_ENHANCED=1`.
- Exit code is non-zero when any provider fetch fails.
- `codexbar serve` starts a foreground localhost-only HTTP server for usage and cost JSON.
- `--port <port>` defaults to `8080`.
- `--refresh-interval <seconds>` defaults to `60` and controls the in-memory response cache TTL.
- `--request-timeout <seconds>` defaults to `30` and bounds each request before returning `504 Gateway Timeout`; use `0` to keep waiting indefinitely.
- Provider config is reloaded for each usage/cost request; cache entries are keyed by the loaded config so provider toggles and source changes do not require restarting `serve`.
- Transient refresh failures fall back to the last good response for up to ten refresh intervals (minimum five minutes) so polling clients do not flicker between data and errors; disabled when `--refresh-interval 0`.
- v1 binds to `127.0.0.1` only and rejects non-loopback `Host` headers. It does not expose remote bind, auth, CORS, TLS, or daemon mode.
- Endpoints: `GET /health`, `GET /usage`, `GET /usage?provider=<id|both|all>`, `GET /cost`, `GET /cost?provider=<id|both|all>`.
- `GET /health` returns `{"status":"ok"}` plus a `version` field with the running build (e.g. `"0.37.2"`) when resolvable; clients can compare it against `codexbar --version` to detect a `serve` process still running an older binary after an update.
- Codex usage responses include every visible Codex account, matching the menu bar switcher.
- `codexbar cache clear` clears local CodexBar caches.
- `--cookies` removes cached browser-cookie headers from the CodexBar Keychain cache.
- `--cookies --provider <id>` removes browser-cookie cache entries for that provider, including managed Codex account scopes.
- `--cost` removes local cost-usage scan caches.
- `--all` clears both cookies and cost caches. `--provider` is cookie-only and cannot be combined with `--cost` or `--all`.
- `--provider <id|both|all>` (default: enabled providers in config; falls back to defaults when missing).
- Provider IDs live in the config file (see `docs/configuration.md`).
- With three or more providers enabled, the default stays scoped to enabled providers; use `--provider all` to query
every registered provider.
- `--account <label>` / `--account-index <n>` / `--all-accounts` (token accounts from config, or all visible Codex accounts for Codex; requires a single provider).
- `--no-credits` (hide Codex credits in text output).
- `--pretty` (pretty-print JSON).
- `--status` (fetch provider status pages and include them in output).
- `--antigravity-plan-debug` (debug: print Antigravity planInfo fields to stderr).
- `--source <auto|web|cli|oauth|api>` (default: `auto`).
- `auto`: provider-specific fallback order from `docs/providers.md`.
- `web`: web-only where that provider exposes an explicit web source; no CLI/API fallback. Browser import is macOS-only, while supported providers can use configured manual cookies on Linux.
- `cli`: CLI/local-helper source where the provider exposes one (for example Codex RPC/PTy, Claude PTY, Kilo CLI fallback, Kiro CLI, local probes).
- `oauth`: OAuth-backed source where supported (Codex, Claude, Vertex AI).
- `api`: API-key/token flow when the provider supports it (OpenAI, Claude Admin API, z.ai, Gemini, Alibaba, Copilot, Kilo, Kimi, Kimi K2, MiniMax, Ollama, Warp, OpenRouter, ElevenLabs, Deepgram, Synthetic, DeepSeek, Moonshot, Doubao, Codebuff, Crof, Venice, AWS Bedrock).
- Output `source` reflects the strategy actually used (`openai-web`, `web`, `oauth`, `api`, `local`, `cli`, or provider CLI label).
- Codex web: OpenAI web dashboard (usage limits, credits remaining, code review remaining, usage breakdown).
- `--web-timeout <seconds>` (default: 60)
- `--web-debug-dump-html` (writes HTML snapshots to `/tmp` when data is missing)
- Claude web: claude.ai API (session + weekly usage, plus account metadata when available).
- Command Code web: commandcode.ai browser session cookies on macOS, or a configured manual cookie on Linux, for monthly credit usage.
- OpenCode Go auto: local SQLite usage on macOS and Linux, with optional manual-cookie web enrichment.
- Kilo auto: app.kilo.ai API first, then CLI auth fallback (`~/.local/share/kilo/auth.json`) on missing/unauthorized API credentials.
- Linux: browser-backed `auto`/`web` modes are not supported; local sources and configured manual-cookie paths remain available where documented.
- Global flags: `-h/--help`, `-V/--version`, `-v/--verbose`, `--no-color`, `--log-level <trace|verbose|debug|info|warning|error|critical>`, `--json-output`, `--json-only`.
- `--json-output`: JSONL logs on stderr (machine-readable).
- `--json-only`: suppress non-JSON output; errors become JSON payloads.
- `codexbar config validate` checks the resolved config file for invalid fields.
- `--format text|json`, `--pretty`, and `--json-only` are supported.
- Warnings keep exit code 0; errors exit non-zero.
- `codexbar config dump` prints the normalized config JSON.
### Token accounts
The CLI reads multi-account tokens from the same resolved config file as the app.
- Select a specific account: `--account <label>` (matches the label/email in the file).
- Select by index (1-based): `--account-index <n>`.
- Fetch all accounts for the provider: `--all-accounts`.
Account selection flags require a single provider (`--provider claude`, etc.).
For Claude, token accounts accept either `sessionKey` cookies or OAuth access tokens (`sk-ant-oat...`).
OAuth usage requires the `user:profile` scope; inference-only tokens will return an error.
### Codex accounts
For Codex, `--all-accounts` and `codexbar serve` enumerate the same visible accounts as the app switcher:
managed Codex accounts from `managed-codex-accounts.json` plus the live system account when present.
Each fetch is scoped to that account's Codex home before the normal Codex web/OAuth/CLI strategy runs, and JSON
payloads include the visible account label in `account`.
### Cost JSON payload
`codexbar cost --format json` emits an array of payloads (one per provider).
- `provider`, `source`, `updatedAt`
- `sessionTokens`, `sessionCostUSD`
- `last30DaysTokens`, `last30DaysCostUSD`
- `daily[]`: `date`, `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheCreationTokens`, `totalTokens`, `totalCost`, `modelsUsed`, `modelBreakdowns[]` (`modelName`, `cost`)
- Codex only: `projects[]`: `name`, `path`, `totalTokens`, `totalCost`, `daily[]`, `modelBreakdowns[]`, `sources[]`
- `totals`: `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheCreationTokens`, `totalTokens`, `totalCost`
## Example usage
```
codexbar # text, respects app toggles
codexbar --provider claude # force Claude
codexbar --provider all # query all registered providers
codexbar --format json --pretty # machine output
codexbar --format json --provider both
codexbar cost # local cost usage (default 30-day window + today)
codexbar cost --days 90 # choose a 1...365 day cost window
codexbar cost --provider codex --group-by project
codexbar cost --provider claude --format json --pretty
codexbar serve --port 8080 # localhost HTTP JSON server
codexbar serve --request-timeout 0 # disable serve request deadlines
COPILOT_API_TOKEN=... codexbar --provider copilot --format json --pretty
codexbar --status # include status page indicator/description
codexbar --provider codex --source oauth --format json --pretty
codexbar --provider codex --source web --format json --pretty
codexbar --provider codex --all-accounts --format json --pretty
codexbar --provider claude --account steipete@gmail.com
codexbar --provider claude --all-accounts --format json --pretty
codexbar --json-only --format json --pretty
codexbar --provider gemini --source api --format json --pretty
KILO_API_KEY=... codexbar --provider kilo --source api --format json --pretty
MOONSHOT_API_KEY=... codexbar --provider moonshot --source api --format json --pretty
codexbar config validate --format json --pretty
codexbar config dump --pretty
printf '%s' "$OPENAI_ADMIN_KEY" | codexbar config set-api-key --provider openai --stdin
codexbar config enable --provider grok
codexbar cache clear --cookies
codexbar cache clear --cookies --provider claude
codexbar cache clear --all --format json --pretty
```
### Sample output (text)
```
== Codex 0.6.0 (codex-cli) ==
Session: 72% left [========----]
Pace: 12% in deficit | Expected 16% used | Projected empty in 2h 30m
Resets today at 2:15 PM
Weekly: 41% left [====--------]
Pace: 6% in reserve | Expected 47% used | Lasts until reset
Resets Fri at 9:00 AM
Credits: 112.4 left
== Claude Code 2.0.58 (web) ==
Session: 88% left [==========--]
Pace: On pace | Expected 13% used | Lasts until reset
Resets tomorrow at 1:00 AM
Weekly: 63% left [=======-----]
Pace: On pace | Expected 37% used | Runs out in 4d
Resets Sat at 6:00 AM
Sonnet: 95% left [===========-]
Account: user@example.com
Plan: Pro
== Kilo (cli) ==
Credits: 60% left [=======-----]
40/100 credits
Plan: Kilo Pass Pro
Activity: Auto top-up: visa
Note: Using CLI fallback
```
### Sample output (JSON, pretty)
```json
{
"provider": "codex",
"version": "0.6.0",
"source": "openai-web",
"status": { "indicator": "none", "description": "Operational", "updatedAt": "2025-12-04T17:55:00Z", "url": "https://status.openai.com/" },
"usage": {
"primary": { "usedPercent": 28, "windowMinutes": 300, "resetsAt": "2025-12-04T19:15:00Z" },
"secondary": { "usedPercent": 59, "windowMinutes": 10080, "resetsAt": "2025-12-05T17:00:00Z" },
"tertiary": null,
"updatedAt": "2025-12-04T18:10:22Z",
"identity": {
"providerID": "codex",
"accountEmail": "user@example.com",
"accountOrganization": null,
"loginMethod": "plus"
},
"accountEmail": "user@example.com",
"accountOrganization": null,
"loginMethod": "plus"
},
"pace": {
"primary": { "stage": "ahead", "deltaPercent": 12, "expectedUsedPercent": 16, "willLastToReset": false, "etaSeconds": 9000, "summary": "12% in deficit | Expected 16% used | Projected empty in 2h 30m" },
"secondary": { "stage": "slightlyBehind", "deltaPercent": -6, "expectedUsedPercent": 47, "willLastToReset": true, "summary": "6% in reserve | Expected 47% used | Lasts until reset" }
},
"credits": { "remaining": 112.4, "updatedAt": "2025-12-04T18:10:21Z" },
"antigravityPlanInfo": null,
"openaiDashboard": {
"signedInEmail": "user@example.com",
"codeReviewRemainingPercent": 100,
"creditEvents": [
{ "id": "00000000-0000-0000-0000-000000000000", "date": "2025-12-04T00:00:00Z", "service": "CLI", "creditsUsed": 123.45 }
],
"dailyBreakdown": [
{
"day": "2025-12-04",
"services": [{ "service": "CLI", "creditsUsed": 123.45 }],
"totalCreditsUsed": 123.45
}
],
"updatedAt": "2025-12-04T18:10:21Z"
}
}
```
## Exit codes
- 0: success
- 2: provider missing (binary not on PATH)
- 3: parse/format error
- 4: CLI timeout
- 1: unexpected failure
## Notes
- CLI uses the config file for enabled providers, ordering, and secrets.
- CLI binary discovery checks explicit overrides, captured login PATH, inherited PATH, and known install paths before falling back to an interactive shell probe.
- Reset lines follow the in-app reset time display setting when available (default: countdown).
- Text output uses ANSI colors when stdout is a rich TTY; disable with `--no-color` or `NO_COLOR`/`TERM=dumb`.
- Copilot CLI queries require an API token via config `apiKey` or `COPILOT_API_TOKEN`.
- OpenAI API charts require an Admin API key for organization costs/usage. Normal API keys can only use the legacy balance fallback.
- Claude Admin API charts require an Anthropic Admin API key (`sk-ant-admin...` or `ANTHROPIC_ADMIN_KEY`).
- Codex CLI `auto` tries the OpenAI web dashboard, then Codex CLI RPC/PTy; the apps Codex `auto` path prefers OAuth when credentials are present, then CLI.
- Claude CLI `auto` tries web, then CLI PTY; the apps Claude `auto` path prefers OAuth, then CLI, then web.
- Kilo text output splits identity into `Plan:` and `Activity:` lines; in `--source auto`, resolved CLI fetches add
`Note: Using CLI fallback`.
- Kilo auto-mode failures include a fallback-attempt summary line in text mode (API attempt then CLI attempt).
- OpenAI web requires a signed-in `chatgpt.com` session in a supported browser or a manual cookie header. No passwords are stored; CodexBar reuses cookies.
- Safari cookie import may require granting CodexBar Full Disk Access (System Settings → Privacy & Security → Full Disk Access).
- The `openaiDashboard` JSON field is normally sourced from the apps cached dashboard snapshot; `--source auto|web` refreshes it live via WebKit using a per-account cookie store.
- Future: optional `--from-cache` flag to read the menubar apps persisted snapshot (if/when that file lands).
+72
View File
@@ -0,0 +1,72 @@
---
summary: "Codebuff provider data sources: API token, CLI credentials file, credit balance, and weekly rate limits."
read_when:
- Debugging Codebuff credential resolution or usage parsing
- Updating Codebuff credit balance or weekly rate-limit display
- Adjusting Codebuff provider UI/menu behavior
---
# Codebuff
CodexBar surfaces [Codebuff](https://www.codebuff.com) credit balance and
weekly rate limits next to your other AI providers.
## Data sources
- `POST https://www.codebuff.com/api/v1/usage` — current credit usage,
remaining balance, auto top-up state, and the next quota reset date.
- `GET https://www.codebuff.com/api/user/subscription` — subscription tier,
billing period end, and the weekly rate-limit window (`weeklyUsed` /
`weeklyLimit`) when CodexBar is using the CLI credentials-file session token.
Both endpoints use a Bearer token. Codebuff credentials come from the
environment, the normal CodexBar config file, or the official CLI credentials
file; the Codebuff provider does not write a separate Keychain credential.
## Authentication
CodexBar resolves the Codebuff API token in this order:
1. `CODEBUFF_API_KEY` environment variable (takes precedence so CI overrides
work). API-key tokens fetch credit balance only.
2. The per-provider API key stored in Settings → Providers → Codebuff (saved
in `~/.codexbar/config.json` via the normal CodexBar config flow). API-key
tokens fetch credit balance only.
3. `~/.config/manicode/credentials.json` — the file the official `codebuff`
CLI (formerly `manicode`) writes after `codebuff login`. CodexBar reads
`default.authToken`, falling back to top-level `authToken`, and uses that
session token for both credit balance and subscription metadata.
If none of those is available, Codebuff shows the “missing token” error.
## Credit window mapping
- **Primary row** — credit balance (`usage / quota`), with the "next quota
reset" date if provided.
- **Secondary row** — weekly rate-limit window (`weeklyUsed / weeklyLimit`)
shown with a 7-day window.
The account panel shows the Codebuff tier (e.g. "Pro"), remaining balance,
and whether auto top-up is enabled.
## Troubleshooting
- Run `codebuff login` to refresh `~/.config/manicode/credentials.json`.
- Override the API base with `CODEBUFF_API_URL` for staging environments.
- Verify your token works manually:
```sh
curl -s -X POST -H "Authorization: Bearer $CODEBUFF_API_KEY" \
-H 'Content-Type: application/json' -d '{"fingerprintId":"codexbar-usage"}' \
https://www.codebuff.com/api/v1/usage
```
## Related files
- `Sources/CodexBarCore/Providers/Codebuff/` — descriptor, fetcher, snapshot,
settings reader, error types.
- `Sources/CodexBar/Providers/Codebuff/` — settings store bridge + macOS
settings pane implementation.
- `Tests/CodexBarTests/CodebuffSettingsReaderTests.swift`,
`CodebuffUsageFetcherTests.swift`, and the Codebuff extensions in
`ProviderTokenResolverTests.swift`.
+635
View File
@@ -0,0 +1,635 @@
---
summary: "Codex OAuth resolver: tokens, refresh, usage endpoint, and fetch strategy wiring."
read_when:
- Adding or modifying Codex OAuth usage fetching
- Debugging auth.json parsing or token refresh behavior
- Adjusting Codex provider source selection
---
# Codex OAuth Resolver Implementation Plan
> Replicate Codex's direct OAuth token usage in CodexBar instead of calling the CLI.
## Background
Currently, CodexBar fetches Codex usage by:
1. Running `codex` CLI in PTY mode
2. Sending `/status` command
3. Parsing the text output
This is slow and unreliable. The goal is to directly read Codex's OAuth tokens and call the same API endpoints that Codex uses internally.
---
## Codex OAuth Architecture (from source analysis)
### Token Storage
**Location:** `~/.codex/auth.json`
```json
{
"OPENAI_API_KEY": null,
"tokens": {
"id_token": "eyJ...",
"access_token": "eyJ...",
"refresh_token": "...",
"account_id": "account-..."
},
"last_refresh": "2025-12-28T12:34:56Z"
}
```
**Source:** `codex-rs/core/src/auth/storage.rs`
### Token Refresh
**Endpoint:** `POST https://auth.openai.com/oauth/token`
**Request:**
```json
{
"client_id": "app_EMoamEEZ73f0CkXaXp7hrann",
"grant_type": "refresh_token",
"refresh_token": "<refresh_token>",
"scope": "openid profile email"
}
```
**Response:**
```json
{
"id_token": "eyJ...",
"access_token": "eyJ...",
"refresh_token": "..."
}
```
**Refresh Interval:** 8 days (from `TOKEN_REFRESH_INTERVAL` constant)
**Source:** `codex-rs/core/src/auth.rs:504-545`
### Usage API
**Endpoint:** `GET {chatgpt_base_url}/wham/usage` (default: `https://chatgpt.com/backend-api/wham/usage`)
If `chatgpt_base_url` does not include `/backend-api`, Codex falls back to
`{base_url}/api/codex/usage` (see `PathStyle` in `backend-client/src/client.rs`).
**Headers:**
```
Authorization: Bearer <access_token>
ChatGPT-Account-Id: <account_id>
User-Agent: codex-cli
```
**Quick checks**
- Command: `cat ~/.codex/auth.json`
- Command: `curl -H "Authorization: Bearer <access_token>" -H "ChatGPT-Account-Id: <account_id>" -H "User-Agent: codex-cli" https://chatgpt.com/backend-api/wham/usage`
- Command: `CodexBarCLI usage --provider codex --source oauth --json --pretty`
**Response:**
```json
{
"plan_type": "pro",
"rate_limit": {
"primary_window": {
"used_percent": 15,
"reset_at": 1735401600,
"limit_window_seconds": 18000
},
"secondary_window": {
"used_percent": 5,
"reset_at": 1735920000,
"limit_window_seconds": 604800
}
},
"credits": {
"has_credits": true,
"unlimited": false,
"balance": 150.0
}
}
```
**Source:** `codex-rs/backend-client/src/client.rs:161-170`
---
## Implementation
### Files to Create
| File | Location | Purpose |
|------|----------|---------|
| `CodexOAuthCredentials.swift` | `Sources/CodexBarCore/Providers/Codex/CodexOAuth/` | Token storage model + loader |
| `CodexOAuthUsageFetcher.swift` | `Sources/CodexBarCore/Providers/Codex/CodexOAuth/` | API client for usage endpoint |
| `CodexTokenRefresher.swift` | `Sources/CodexBarCore/Providers/Codex/CodexOAuth/` | Token refresh logic |
### Files to Modify
| File | Changes |
|------|---------|
| `CodexProviderDescriptor.swift` | Add `CodexOAuthFetchStrategy`, update `resolveStrategies()` |
---
### Step 1: CodexOAuthCredentials.swift
```swift
import Foundation
public struct CodexOAuthCredentials: Sendable {
public let accessToken: String
public let refreshToken: String
public let idToken: String?
public let accountId: String?
public let lastRefresh: Date?
public var needsRefresh: Bool {
guard let last = lastRefresh else { return true }
let eightDays: TimeInterval = 8 * 24 * 3600
return Date().timeIntervalSince(last) > eightDays
}
}
public enum CodexOAuthCredentialsError: LocalizedError {
case notFound
case decodeFailed(String)
case missingTokens
public var errorDescription: String? {
switch self {
case .notFound:
"Codex auth.json not found. Run `codex` to log in."
case .decodeFailed(let msg):
"Failed to decode Codex credentials: \(msg)"
case .missingTokens:
"Codex auth.json exists but contains no tokens."
}
}
}
public enum CodexOAuthCredentialsStore {
private static var authFilePath: URL {
let home = FileManager.default.homeDirectoryForCurrentUser
// Respect CODEX_HOME if set
if let codexHome = ProcessInfo.processInfo.environment["CODEX_HOME"],
!codexHome.isEmpty {
return URL(fileURLWithPath: codexHome).appendingPathComponent("auth.json")
}
return home.appendingPathComponent(".codex/auth.json")
}
public static func load() throws -> CodexOAuthCredentials {
let url = authFilePath
guard FileManager.default.fileExists(atPath: url.path) else {
throw CodexOAuthCredentialsError.notFound
}
let data = try Data(contentsOf: url)
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw CodexOAuthCredentialsError.decodeFailed("Invalid JSON")
}
// Check for API key auth (no tokens needed for refresh)
if let apiKey = json["OPENAI_API_KEY"] as? String, !apiKey.isEmpty {
return CodexOAuthCredentials(
accessToken: apiKey,
refreshToken: "",
idToken: nil,
accountId: nil,
lastRefresh: nil)
}
guard let tokens = json["tokens"] as? [String: Any],
let accessToken = tokens["access_token"] as? String,
let refreshToken = tokens["refresh_token"] as? String else {
throw CodexOAuthCredentialsError.missingTokens
}
let idToken = tokens["id_token"] as? String
let accountId = tokens["account_id"] as? String
let lastRefresh: Date? = {
guard let str = json["last_refresh"] as? String else { return nil }
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter.date(from: str) ?? ISO8601DateFormatter().date(from: str)
}()
return CodexOAuthCredentials(
accessToken: accessToken,
refreshToken: refreshToken,
idToken: idToken,
accountId: accountId,
lastRefresh: lastRefresh)
}
public static func save(_ credentials: CodexOAuthCredentials) throws {
let url = authFilePath
// Read existing file to preserve structure
var json: [String: Any] = [:]
if let data = try? Data(contentsOf: url),
let existing = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
json = existing
}
var tokens: [String: Any] = [
"access_token": credentials.accessToken,
"refresh_token": credentials.refreshToken
]
if let idToken = credentials.idToken {
tokens["id_token"] = idToken
}
if let accountId = credentials.accountId {
tokens["account_id"] = accountId
}
json["tokens"] = tokens
json["last_refresh"] = ISO8601DateFormatter().string(from: Date())
let data = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys])
try data.write(to: url, options: .atomic)
}
}
```
---
### Step 2: CodexOAuthUsageFetcher.swift
```swift
import Foundation
public struct CodexUsageResponse: Decodable, Sendable {
public let planType: PlanType
public let rateLimit: RateLimitDetails?
public let credits: CreditDetails?
enum CodingKeys: String, CodingKey {
case planType = "plan_type"
case rateLimit = "rate_limit"
case credits
}
public enum PlanType: String, Decodable, Sendable {
case guest, free, go, plus, pro
case freeWorkspace = "free_workspace"
case team, business, education, quorum, k12, enterprise, edu
}
public struct RateLimitDetails: Decodable, Sendable {
public let primaryWindow: WindowSnapshot?
public let secondaryWindow: WindowSnapshot?
enum CodingKeys: String, CodingKey {
case primaryWindow = "primary_window"
case secondaryWindow = "secondary_window"
}
}
public struct WindowSnapshot: Decodable, Sendable {
public let usedPercent: Int
public let resetAt: Int
public let limitWindowSeconds: Int
enum CodingKeys: String, CodingKey {
case usedPercent = "used_percent"
case resetAt = "reset_at"
case limitWindowSeconds = "limit_window_seconds"
}
}
public struct CreditDetails: Decodable, Sendable {
public let hasCredits: Bool
public let unlimited: Bool
public let balance: Double?
enum CodingKeys: String, CodingKey {
case hasCredits = "has_credits"
case unlimited
case balance
}
}
}
public enum CodexOAuthFetchError: LocalizedError, Sendable {
case unauthorized
case invalidResponse
case serverError(Int, String?)
case networkError(Error)
public var errorDescription: String? {
switch self {
case .unauthorized:
"Codex OAuth token expired or invalid. Run `codex` to re-authenticate."
case .invalidResponse:
"Invalid response from Codex usage API."
case .serverError(let code, let msg):
"Codex API error \(code): \(msg ?? "unknown")"
case .networkError(let error):
"Network error: \(error.localizedDescription)"
}
}
}
public enum CodexOAuthUsageFetcher {
private static let defaultChatGPTBaseURL = "https://chatgpt.com/backend-api/"
private static let chatGPTUsagePath = "/wham/usage"
private static let codexUsagePath = "/api/codex/usage"
public static func fetchUsage(
accessToken: String,
accountId: String?
) async throws -> CodexUsageResponse {
var request = URLRequest(url: resolveUsageURL())
request.httpMethod = "GET"
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
request.setValue("CodexBar", forHTTPHeaderField: "User-Agent")
request.setValue("application/json", forHTTPHeaderField: "Accept")
if let accountId {
request.setValue(accountId, forHTTPHeaderField: "ChatGPT-Account-Id")
}
let (data, response): (Data, URLResponse)
do {
(data, response) = try await URLSession.shared.data(for: request)
} catch {
throw CodexOAuthFetchError.networkError(error)
}
guard let http = response as? HTTPURLResponse else {
throw CodexOAuthFetchError.invalidResponse
}
switch http.statusCode {
case 200...299:
do {
return try JSONDecoder().decode(CodexUsageResponse.self, from: data)
} catch {
throw CodexOAuthFetchError.invalidResponse
}
case 401, 403:
throw CodexOAuthFetchError.unauthorized
default:
let body = String(data: data, encoding: .utf8)
throw CodexOAuthFetchError.serverError(http.statusCode, body)
}
}
}
```
---
### Step 3: CodexTokenRefresher.swift
```swift
import Foundation
public enum CodexTokenRefresher {
private static let refreshEndpoint = URL(string: "https://auth.openai.com/oauth/token")!
private static let clientID = "app_EMoamEEZ73f0CkXaXp7hrann"
public enum RefreshError: LocalizedError {
case expired
case revoked
case reused
case networkError(Error)
case invalidResponse(String)
public var errorDescription: String? {
switch self {
case .expired:
"Refresh token expired. Please run `codex` to log in again."
case .revoked:
"Refresh token was revoked. Please run `codex` to log in again."
case .reused:
"Refresh token was already used. Please run `codex` to log in again."
case .networkError(let error):
"Network error during token refresh: \(error.localizedDescription)"
case .invalidResponse(let msg):
"Invalid refresh response: \(msg)"
}
}
}
public static func refresh(_ credentials: CodexOAuthCredentials) async throws -> CodexOAuthCredentials {
guard !credentials.refreshToken.isEmpty else {
// API key auth - no refresh needed
return credentials
}
var request = URLRequest(url: refreshEndpoint)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: String] = [
"client_id": clientID,
"grant_type": "refresh_token",
"refresh_token": credentials.refreshToken,
"scope": "openid profile email"
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response): (Data, URLResponse)
do {
(data, response) = try await URLSession.shared.data(for: request)
} catch {
throw RefreshError.networkError(error)
}
guard let http = response as? HTTPURLResponse else {
throw RefreshError.invalidResponse("No HTTP response")
}
if http.statusCode == 401 {
// Parse error code to classify failure
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let errorCode = (json["error"] as? [String: Any])?["code"] as? String
?? json["error"] as? String
?? json["code"] as? String {
switch errorCode.lowercased() {
case "refresh_token_expired": throw RefreshError.expired
case "refresh_token_reused": throw RefreshError.reused
case "refresh_token_invalidated": throw RefreshError.revoked
default: throw RefreshError.expired
}
}
throw RefreshError.expired
}
guard http.statusCode == 200 else {
throw RefreshError.invalidResponse("Status \(http.statusCode)")
}
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw RefreshError.invalidResponse("Invalid JSON")
}
let newAccessToken = json["access_token"] as? String ?? credentials.accessToken
let newRefreshToken = json["refresh_token"] as? String ?? credentials.refreshToken
let newIdToken = json["id_token"] as? String ?? credentials.idToken
return CodexOAuthCredentials(
accessToken: newAccessToken,
refreshToken: newRefreshToken,
idToken: newIdToken,
accountId: credentials.accountId,
lastRefresh: Date())
}
}
```
---
### Step 4: Update CodexProviderDescriptor.swift
Add OAuth to `sourceModes` and create new strategy:
```swift
// In makeDescriptor(), update fetchPlan:
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .oauth, .web, .cli], // Add .oauth
pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)),
// Update resolveStrategies:
private static func resolveStrategies(context: ProviderFetchContext) async -> [any ProviderFetchStrategy] {
let oauth = CodexOAuthFetchStrategy()
let cli = CodexCLIUsageStrategy()
let web = CodexWebDashboardStrategy()
switch context.sourceMode {
case .oauth:
return [oauth]
case .web:
return [web]
case .cli:
return [cli]
case .auto:
// OAuth first (fast), CLI fallback
if context.runtime == .cli {
return [web, cli]
}
return [oauth, cli]
}
}
// Add new strategy:
struct CodexOAuthFetchStrategy: ProviderFetchStrategy {
let id: String = "codex.oauth"
let kind: ProviderFetchKind = .oauth
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
(try? CodexOAuthCredentialsStore.load()) != nil
}
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
var creds = try CodexOAuthCredentialsStore.load()
// Refresh if needed (8+ days old)
if creds.needsRefresh && !creds.refreshToken.isEmpty {
creds = try await CodexTokenRefresher.refresh(creds)
try CodexOAuthCredentialsStore.save(creds)
}
let usage = try await CodexOAuthUsageFetcher.fetchUsage(
accessToken: creds.accessToken,
accountId: creds.accountId)
return makeResult(
usage: Self.mapUsage(usage),
credits: Self.mapCredits(usage.credits),
sourceLabel: "oauth")
}
func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool {
// Fallback to CLI on auth errors
if let fetchError = error as? CodexOAuthFetchError {
switch fetchError {
case .unauthorized: return true
default: return false
}
}
if error is CodexOAuthCredentialsError { return true }
if error is CodexTokenRefresher.RefreshError { return true }
return false
}
private static func mapUsage(_ response: CodexUsageResponse) -> UsageSnapshot {
let primary: RateWindow? = response.rateLimit?.primaryWindow.map { window in
RateWindow(
usedPercent: Double(window.usedPercent),
windowMinutes: window.limitWindowSeconds / 60,
resetsAt: Date(timeIntervalSince1970: TimeInterval(window.resetAt)),
resetDescription: nil)
}
let secondary: RateWindow? = response.rateLimit?.secondaryWindow.map { window in
RateWindow(
usedPercent: Double(window.usedPercent),
windowMinutes: window.limitWindowSeconds / 60,
resetsAt: Date(timeIntervalSince1970: TimeInterval(window.resetAt)),
resetDescription: nil)
}
let identity = ProviderIdentitySnapshot(
providerID: .codex,
accountEmail: nil,
accountOrganization: nil,
loginMethod: response.planType.rawValue)
return UsageSnapshot(
primary: primary ?? RateWindow(usedPercent: 0, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: secondary,
tertiary: nil,
providerCost: nil,
updatedAt: Date(),
identity: identity)
}
private static func mapCredits(_ credits: CodexUsageResponse.CreditDetails?) -> CreditsSnapshot? {
guard let credits else { return nil }
return CreditsSnapshot(
hasCredits: credits.hasCredits,
unlimited: credits.unlimited,
balance: credits.balance)
}
}
```
---
## Constants Reference
| Constant | Value | Source |
|----------|-------|--------|
| Client ID | `app_EMoamEEZ73f0CkXaXp7hrann` | `auth.rs:618` |
| Refresh URL | `https://auth.openai.com/oauth/token` | `auth.rs:66` |
| Usage URL | `https://chatgpt.com/backend-api/wham/usage` (default) | `client.rs:163` |
| Token refresh interval | 8 days | `auth.rs:59` |
| Auth file | `~/.codex/auth.json` | `storage.rs` |
---
## Testing
1. Ensure `~/.codex/auth.json` exists (run `codex` to log in first)
2. Run CodexBar with debug logging enabled
3. Verify OAuth strategy is selected and API calls succeed
4. Test token refresh by manually setting `last_refresh` to old date
5. Test fallback by temporarily renaming auth.json
---
## Error Handling
| Error | Behavior |
|-------|----------|
| No auth.json | Fall back to CLI strategy |
| Token expired | Attempt refresh, fall back to CLI on failure |
| Refresh failed | Log error, fall back to CLI |
| API error | Fall back to CLI |
| Network error | Retry with backoff, then fall back |
+171
View File
@@ -0,0 +1,171 @@
---
summary: "Codex provider data sources: OpenAI web dashboard, Codex CLI RPC, credits, and local cost usage."
read_when:
- Debugging Codex usage/credits parsing
- Updating OpenAI dashboard scraping or cookie import
- Changing Codex CLI RPC or diagnostic PTY behavior
- Reviewing local cost usage scanning
---
# Codex provider
Codex has three automatic usage data paths (OAuth API, web dashboard, CLI RPC) plus a manual CLI PTY diagnostic parser and a local cost-usage scanner.
The OAuth API is the default app source when credentials are available; web access is optional for dashboard extras.
## Data sources + fallback order
### App default selection (debug menu disabled)
1) OAuth API (auth.json credentials).
2) CLI RPC through `codex app-server`.
3) If OpenAI web extras are enabled and a matching OpenAI web session is available (Automatic or Manual cookies),
dashboard extras load as a separate follow-up refresh and the source label becomes `primary + openai-web`.
Usage source picker:
- Preferences → Providers → Codex → Usage source (Auto/OAuth/CLI).
### CLI default selection (`--source auto`)
1) OpenAI web dashboard (when available).
2) Codex CLI RPC through `codex app-server`.
### OAuth API (preferred for the app)
- Reads OAuth tokens from `~/.codex/auth.json` (or `$CODEX_HOME/auth.json`).
- Refreshes access tokens when `last_refresh` is older than 8 days.
- Calls `GET https://chatgpt.com/backend-api/wham/usage` (default) with `Authorization: Bearer <token>`.
- The app reads reset-credit inventory once per refresh with a best-effort
`GET https://chatgpt.com/backend-api/wham/rate-limit-reset-credits` using the same account-scoped OAuth context;
the CLI requests it only when optional credits are included.
- The menu and provider settings list every still-available expiry, while the optional credits setting controls
nearing-expiry notifications. CodexBar does not redeem or modify reset credits.
- `rate_limit.primary_window` / `secondary_window` map to the session/weekly lanes.
- `additional_rate_limits[]` (model-specific limits such as GPT-5.3-Codex-Spark) map to named
`UsageSnapshot.extraRateWindows` entries. Spark uses stable `codex-spark` / `codex-spark-weekly` ids and
`Codex Spark 5-hour` / `Codex Spark Weekly` titles. When the field is absent, the snapshot is unchanged.
- Preferences → Providers → Codex → Show Codex Spark usage hides only the Spark rows in menus and the provider
preview. It does not change fetching, history, notifications, widgets, credits, or other extra limits.
### Advanced profile-home accounts
- Managed Codex accounts remain the default multi-account path.
- Advanced users can add existing Codex homes to `~/.codexbar/config.json` with
`providers[].codexProfileHomePaths`.
- Each configured path must be absolute or start with `~/`, and point at a Codex home that contains `auth.json`.
- CodexBar reads identity from the configured home, exposes it in the Codex account switcher, and scopes
remote Codex fetches with `CODEX_HOME`.
- Profile homes are not copied, reauthenticated, or removed by CodexBar.
Example:
```json
{
"id": "codex",
"codexProfileHomePaths": [
"~/.codex-work",
"~/.codex-personal"
]
}
```
### OpenAI web dashboard (optional, off by default)
- Enable it in Preferences -> Providers -> Codex -> OpenAI web extras.
- It exists for dashboard-only extras such as code review remaining, usage breakdown, and credits history.
- It is intentionally opt-in because it loads `chatgpt.com` in a hidden WebView and can materially increase battery or network usage.
- OpenAI web battery saver is a separate toggle. When enabled, routine background/settings-driven refreshes are reduced, but explicit manual refreshes still run.
- OpenAI web battery saver currently defaults to off.
- Preferences → Providers → Codex → OpenAI cookies (Automatic or Manual).
- URL: `https://chatgpt.com/codex/settings/usage`.
- Uses an off-screen `WKWebView` with a per-account `WKWebsiteDataStore`.
- Store key: deterministic UUID from the normalized email.
- WebKit store can hold multiple accounts concurrently.
- Cookie import (Automatic mode, when WebKit store has no matching session or login required):
1) Safari: `~/Library/Cookies/Cookies.binarycookies`
2) Chrome/Chromium forks: `~/Library/Application Support/Google/Chrome/*/Cookies`
3) Firefox: `~/Library/Application Support/Firefox/Profiles/*/cookies.sqlite`
- Domains loaded: `chatgpt.com`, `openai.com`.
- No cookie-name filter; we import all matching domain cookies.
- Cached cookies: Keychain cache `com.steipete.codexbar.cache` (account `cookie.codex`, source + timestamp).
Reused before re-importing from browsers.
- Manual cookie header:
- Paste the `Cookie:` header from a `chatgpt.com` request in Preferences → Providers → Codex.
- Used when OpenAI cookies are set to Manual.
- Account match:
- Signed-in email extracted from `client-bootstrap` JSON in HTML (or `__NEXT_DATA__`).
- If Codex email is known and does not match, the web path is rejected.
- Web scrape payload (via `OpenAIDashboardScrapeScript` + `OpenAIDashboardParser`):
- Rate limits (5h + weekly) parsed from body text.
- Credits remaining parsed from body text.
- Code review remaining (%).
- Usage breakdown chart (Recharts bar data + legend colors).
- Credits usage history table rows.
- Credits purchase URL (best-effort).
- Errors surfaced:
- Login required or Cloudflare interstitial.
### Codex CLI RPC (automatic CLI source)
- Launches local RPC server: `codex -s read-only -a untrusted app-server`.
- JSON-RPC over stdin/stdout:
- `initialize` (client name/version)
- `account/read`
- `account/rateLimits/read`
- RPC reads are bounded: initialization has a longer startup budget, and normal requests have a shorter per-method
timeout. On timeout, CodexBar terminates the child `codex app-server` process so the stdout reader unwinds instead
of leaving refresh stuck indefinitely.
- Provides:
- Usage windows (primary + secondary) with reset timestamps.
- Credits snapshot (balance, hasCredits, unlimited).
- Account identity (email + plan type) when available.
- App-server errors are terminal for the CLI strategy, except when Codex includes a recoverable `wham/usage` JSON body in the error text.
- If macOS blocks or quarantines the `codex` executable, CodexBar records the launch failure and skips background CLI
launches for 30 minutes. Use a manual refresh after reinstalling or unblocking `codex` to retry immediately.
- CodexBar also discovers the Codex CLI bundled with current ChatGPT and legacy Codex desktop apps, even when `codex`
is absent from the shell PATH.
- If managed Codex account login still reports a missing executable, turn on **Show debug settings** in
**Settings > Advanced**, then check **Settings > Debug > CLI Paths**. When no Codex binary appears there, confirm
`codex --version` works in Terminal, check `which -a codex` for stale duplicate installs, then run
`npm install -g --include=optional @openai/codex@latest` before retrying Add Account.
### Codex CLI PTY diagnostics (`/status`)
- Manual/debug parser only; automatic background refresh and `CodexBarCLI usage --source cli` do not launch bare Codex TUI.
- Kept for explicit diagnostics/parser coverage because bare `codex` TUI can start interactive auth and open browser tabs.
- Parses rendered `/status` output:
- `Credits:` line
- `5h limit` line → percent + reset text
- `Weekly limit` line → percent + reset text
- Detects update prompts and surfaces a "CLI update needed" error.
## Account identity resolution (for web matching)
1) Latest Codex usage snapshot (from RPC, if available).
2) `~/.codex/auth.json` (JWT claims: email + plan).
3) OpenAI dashboard signed-in email (cached).
4) Last imported browser cookie email (cached).
## Credits
- Web dashboard fills credits only when OAuth/CLI do not provide them.
- CLI RPC: `account/rateLimits/read` → credits balance.
- CLI PTY diagnostics can still parse `Credits:` from saved/manual `/status` output.
## Cost usage (local log scan)
- Source files:
- Native Codex logs:
- `~/.codex/sessions/YYYY/MM/DD/*.jsonl`
- `~/.codex/archived_sessions/*.jsonl` (flat; date inferred from filename when present)
- Or `$CODEX_HOME/sessions/...` + `$CODEX_HOME/archived_sessions/...` if `CODEX_HOME` is set.
- Supported pi sessions:
- `~/.pi/agent/sessions/**/*.jsonl`
- Scanner:
- Native Codex logs parse `event_msg` token_count entries and `turn_context` model markers; when both are present,
`turn_context` is authoritative for the model bucket.
- pi sessions count assistant-message usage rows and attribute `openai-codex` assistant usage to Codex.
- pi assistant usage is bucketed by assistant-turn timestamp, so mixed-model pi sessions can contribute to multiple
days/models correctly.
- Cache:
- Native + merged provider cache: `~/Library/Caches/CodexBar/cost-usage/codex-v2.json`
- pi session cache: `~/Library/Caches/CodexBar/cost-usage/pi-sessions-v1.json`
- Window: configurable 1-365 day rolling history, with a 60s minimum refresh interval.
## Key files
- Web: `Sources/CodexBarCore/OpenAIWeb/*`
- CLI RPC + diagnostic PTY parser: `Sources/CodexBarCore/UsageFetcher.swift`,
`Sources/CodexBarCore/Providers/Codex/CodexStatusProbe.swift`
- Cost usage: `Sources/CodexBarCore/CostUsageFetcher.swift`,
`Sources/CodexBarCore/PiSessionCostScanner.swift`,
`Sources/CodexBarCore/PiSessionCostCache.swift`,
`Sources/CodexBarCore/Vendored/CostUsage/*`
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

+51
View File
@@ -0,0 +1,51 @@
---
summary: "Command Code provider notes: cookie authentication and monthly credit parsing."
read_when:
- Debugging Command Code cookie import or usage parsing
- Updating Command Code billing or credit display
- Adjusting Command Code provider UI/menu behavior
---
# Command Code
CodexBar surfaces [Command Code](https://commandcode.ai) monthly USD credits next
to your other AI coding providers.
## Data source
- `https://api.commandcode.ai` billing endpoints, authenticated with the
signed-in Command Code web session.
- The provider reads monthly credit usage, plan allowance, remaining credits,
and billing-cycle reset timing when the account data is available.
## Authentication
Command Code support uses browser cookies or a manually pasted cookie header.
1. Sign in to `https://commandcode.ai` in a supported browser.
2. Open Settings -> Providers -> Command Code.
3. Enable Command Code and leave Cookie source on Automatic, or switch to Manual
and paste a `Cookie:` header/cURL capture from Command Code.
Automatic import looks for better-auth session cookies from `commandcode.ai`
and `www.commandcode.ai`. If automatic import cannot find a session, use the
manual cookie field.
On Linux, browser import is unavailable. Set `cookieSource` to `manual` and
provide the Command Code `Cookie` header in `cookieHeader`; both `auto` and
`web` CLI source modes then use the billing API.
## Display
- The menu bar item and provider card use the Command Code icon and label.
- The primary row shows monthly credits used/remaining.
- Widgets do not expose Command Code in the provider picker yet.
## Related files
- `Sources/CodexBarCore/Providers/CommandCode/` - descriptor, cookie import,
billing fetcher, snapshot mapping, and plan catalog.
- `Sources/CodexBar/Providers/CommandCode/` - settings store bridge and
provider settings UI.
- `Tests/CodexBarTests/CommandCode*Tests.swift` - parser, cookie, settings,
and icon coverage.
+208
View File
@@ -0,0 +1,208 @@
---
summary: "CodexBar config file layout for CLI + app settings."
read_when:
- "Editing the CodexBar config file or moving settings off Keychain."
- "Adding new provider settings fields or defaults."
- "Explaining CLI/app configuration and security."
---
# Configuration
CodexBar reads a single JSON config file for CLI and app provider settings.
API keys, manual cookie headers, source selection, ordering, and token accounts live here. Keychain is still used for runtime cookie caches, browser Safe Storage access, and provider OAuth/device-flow credentials where those flows require it.
## Location
- `CODEXBAR_CONFIG=/path/to/config.json` when set.
- `$XDG_CONFIG_HOME/codexbar/config.json` when `XDG_CONFIG_HOME` is set to an absolute path. Relative values are
ignored.
- `~/.config/codexbar/config.json` by default for new installs.
- `~/.codexbar/config.json` for existing legacy installs when no XDG config exists.
- The directory is created if missing.
- Permissions are set to `0600` whenever CodexBar writes the file on macOS and Linux.
## Root shape
```json
{
"version": 1,
"providers": [
{
"id": "codex",
"enabled": true,
"source": "auto",
"cookieSource": "auto",
"cookieHeader": null,
"apiKey": null,
"enterpriseHost": null,
"region": null,
"workspaceID": null,
"tokenAccounts": null
}
]
}
```
## Provider fields
All provider fields are optional unless noted.
- `id` (required): provider identifier.
- `enabled`: enable/disable provider (defaults to provider default).
- `source`: preferred source mode.
- `auto|web|cli|oauth|api`
- `auto` uses provider-specific fallback order (see `docs/providers.md`).
- `api` uses the provider's API-backed mode; only some providers consume the `apiKey` field.
- `apiKey`: raw API token for providers that support config-backed direct API usage.
- `enterpriseHost`: provider-specific API host/base URL override. Used by Azure OpenAI, Copilot, LLM Proxy, LiteLLM,
ClawRouter, and Wayfinder.
- `cookieSource`: cookie selection policy.
- `auto` (browser import), `manual` (use `cookieHeader`), `off` (disable cookies)
- `cookieHeader`: raw cookie header value (e.g. `key=value; other=...`).
- `region`: provider-specific region (e.g. `zai`, `minimax`).
- `workspaceID`: provider-specific workspace/deployment/project ID (e.g. Azure OpenAI deployment, OpenAI API project,
`opencode`).
- `tokenAccounts`: multi-account tokens for providers in `TokenAccountSupportCatalog`.
## Manual cookies
Use manual cookies when automatic browser import is unavailable, disabled, or too noisy for your setup.
The app and CLI both read the same resolved config file, so a manual cookie saved in the UI is also used by
`codexbar`, and a cookie written by tooling is shown in the app after reload.
`cookieHeader` expects the HTTP `Cookie:` request header value for the provider origin, not a raw Netscape cookie
export. In browser DevTools, open the Network tab, select a request for the provider site, and copy the request
header named `Cookie`. You can paste either the full `Cookie: name=value; other=value` string or just
`name=value; other=value`.
If you have a Netscape export, convert each non-comment row to `name=value` and join values with `; `. Do not paste
the raw `# Netscape HTTP Cookie File` text into `cookieHeader`.
Example placeholder config:
```json
{
"version": 1,
"providers": [
{
"id": "example-provider",
"enabled": true,
"cookieSource": "manual",
"cookieHeader": "session=<REDACTED>; other=<REDACTED>"
}
]
}
```
Validate after editing:
```bash
codexbar config validate
codexbar usage --provider example-provider --verbose
```
CLI shortcuts:
```bash
codexbar config providers
codexbar config enable --provider grok
codexbar config disable --provider cursor
printf '%s' "$ELEVENLABS_API_KEY" | codexbar config set-api-key --provider elevenlabs --stdin
printf '%s' "$OPENAI_ADMIN_KEY" | codexbar config set-api-key --provider openai --stdin
printf '%s' "$GROQ_API_KEY" | codexbar config set-api-key --provider groq --stdin
printf '%s' "$LLM_PROXY_API_KEY" | codexbar config set-api-key --provider llmproxy --stdin
printf '%s' "$LITELLM_API_KEY" | codexbar config set-api-key --provider litellm --stdin
printf '%s' "$CLAWROUTER_API_KEY" | codexbar config set-api-key --provider clawrouter --stdin
printf '%s' "$SUB2API_API_KEY" | codexbar config set-api-key --provider sub2api --stdin
```
OpenAI API project scoping uses `workspaceID` in config. This maps to `OPENAI_PROJECT_ID` for Admin API usage and is
only applied to the configured OpenAI key, not to selected OpenAI token accounts:
```json
{
"id": "openai",
"enabled": true,
"apiKey": "<OPENAI_ADMIN_KEY>",
"workspaceID": "proj_..."
}
```
LLM Proxy also needs a base URL. Set `enterpriseHost` in config or `LLM_PROXY_BASE_URL` in the process environment:
```json
{
"id": "llmproxy",
"enabled": true,
"apiKey": "<REDACTED>",
"enterpriseHost": "https://proxy.example.com"
}
```
LiteLLM also needs a base URL. Set `enterpriseHost` in config or `LITELLM_BASE_URL` in the process environment:
```json
{
"id": "litellm",
"enabled": true,
"apiKey": "<REDACTED>",
"enterpriseHost": "https://litellm.example.com"
}
```
ClawRouter defaults to the hosted service. To use another deployment, set `enterpriseHost` in config or
`CLAWROUTER_BASE_URL` in the process environment:
```json
{
"id": "clawrouter",
"enabled": true,
"apiKey": "<REDACTED>",
"enterpriseHost": "https://router.example.com"
}
```
sub2api needs its self-hosted base URL. Set `enterpriseHost` in config or `SUB2API_BASE_URL` in the process
environment. Add labeled token accounts in Settings when one deployment has multiple group API keys:
```json
{
"id": "sub2api",
"enabled": true,
"apiKey": "<REDACTED>",
"enterpriseHost": "https://sub2api.example.com"
}
```
See [CLI configuration](cli-configuration.md) for scripting examples and output formats.
Manual cookies are secrets. Keep the CodexBar config file private, leave its permissions at `0600`, never commit it,
and never paste real cookie values or readable DevTools screenshots into public issues.
### tokenAccounts
```json
{
"version": 1,
"activeIndex": 0,
"accounts": [
{
"id": "00000000-0000-0000-0000-000000000000",
"label": "user@example.com",
"token": "sk-...",
"addedAt": 1735123456,
"lastUsed": 1735220000
}
]
}
```
z.ai team accounts also use `usageScope`, `organizationId`, and `workspaceID`; see [z.ai](zai.md).
## Provider IDs
Current IDs (see `Sources/CodexBarCore/Providers/Providers.swift`):
`codex`, `openai`, `azureopenai`, `claude`, `cursor`, `opencode`, `opencodego`, `alibaba`, `alibabatokenplan`, `factory`, `gemini`, `antigravity`, `copilot`, `devin`, `zai`, `minimax`, `manus`, `kimi`, `kilo`, `kiro`, `vertexai`, `augment`, `jetbrains`, `kimik2`, `moonshot`, `amp`, `t3chat`, `ollama`, `synthetic`, `warp`, `openrouter`, `elevenlabs`, `windsurf`, `zed`, `perplexity`, `mimo`, `doubao`, `sakana`, `abacus`, `mistral`, `deepseek`, `codebuff`, `crof`, `venice`, `commandcode`, `qoder`, `stepfun`, `bedrock`, `grok`, `groq`, `llmproxy`, `litellm`, `deepgram`, `poe`, `chutes`, `crossmodel`, `clawrouter`, `sub2api`, `wayfinder`.
## Ordering
The order of `providers` controls display/order in the app and CLI. Reorder the array to change ordering.
## Notes
- Fields not relevant to a provider are ignored.
- Omitted providers are appended with defaults during normalization.
- Keep the file private; it contains secrets.
- Validate the file with `codexbar config validate` (JSON output available with `--format json`).
+69
View File
@@ -0,0 +1,69 @@
---
summary: "Copilot provider data sources: GitHub device flow, Copilot internal usage API, and optional GitHub web budgets."
read_when:
- Debugging Copilot login or usage parsing
- Updating GitHub OAuth device flow behavior
---
# Copilot provider
Copilot uses GitHub OAuth device flow and the Copilot internal usage API for primary usage. Optional budget extras use GitHub web cookies only when enabled.
## Data sources + fallback order
1) **GitHub OAuth device flow** (user initiated)
- Device code request:
- `POST https://github.com/login/device/code`
- Token polling:
- `POST https://github.com/login/oauth/access_token`
- Optional enterprise host:
- set Copilot `enterpriseHost` in `~/.codexbar/config.json` or the provider settings UI
- CodexBar normalizes values such as `https://octocorp.ghe.com/login` to `octocorp.ghe.com`
- device flow uses `https://<enterpriseHost>/login/...`
- Scope: `read:user`.
- Token stored in config:
- `~/.codexbar/config.json``providers[].apiKey` for `copilot`
- token accounts use `providers[].tokenAccounts`
2) **Usage fetch**
- `GET https://api.github.com/copilot_internal/user`
- With an enterprise host, the API host is `api.<enterpriseHost>`.
- Headers:
- `Authorization: token <github_oauth_token>`
- `Accept: application/json`
- `Editor-Version: vscode/1.96.2`
- `Editor-Plugin-Version: copilot-chat/0.26.7`
- `User-Agent: GitHubCopilotChat/0.26.7`
- `X-Github-Api-Version: 2025-04-01`
3) **Budget fetch** (optional GitHub web endpoint, best-effort)
- Disabled by default. The Copilot provider's "Budget extras" setting must be enabled before CodexBar imports
github.com cookies or renders budget bars.
- CodexBar asks the logged-in GitHub web endpoint for customer-scope budgets:
- `GET https://github.com/settings/billing/budgets?page=<page>&page_size=10&scope=customer`
- Headers:
- `Cookie: <github.com browser cookies>`
- `Accept: application/json`
- `X-Requested-With: XMLHttpRequest`
- `GitHub-Verified-Fetch: true`
- `X-Fetch-Nonce: <fresh nonce when available>`
- CodexBar first tries to read a fresh nonce from `https://github.com/settings/billing/budgets`, then calls the JSON
endpoint. If GitHub rejects the web request, CodexBar keeps the normal Copilot quota bars and omits budget bars.
- This is intentionally not the public GitHub REST billing API. The REST API did not expose the personal budget list
for the tested individual account.
## Snapshot mapping
- Primary: `quotaSnapshots.premiumInteractions` percent remaining → used percent.
- Secondary: `quotaSnapshots.chat` percent remaining → used percent.
- Extra: positive Copilot billing budgets from the GitHub web endpoint → `extraRateWindows`, only when "Budget extras"
is enabled.
- Product budget: `copilot`
- SKU budgets: `copilot_premium_request`, `copilot_agent_premium_request`, `spark_premium_request`
- Reset dates are not provided by the API.
- Plan label from `copilotPlan`.
## Key files
- `Sources/CodexBarCore/Providers/Copilot/CopilotUsageFetcher.swift`
- `Sources/CodexBarCore/Providers/Copilot/CopilotDeviceFlow.swift`
- `Sources/CodexBar/Providers/Copilot/CopilotLoginFlow.swift`
- `Sources/CodexBar/CopilotTokenStore.swift` (legacy migration helper)
+35
View File
@@ -0,0 +1,35 @@
# Cost window comparison decision
Issues: [#1500](https://github.com/steipete/CodexBar/issues/1500), [#1708](https://github.com/steipete/CodexBar/issues/1708)
## Proposed product shape
Keep the existing history-window setting as the maximum local scan window. Add an opt-in **Show shorter comparison periods** preference, defaulting off. When enabled, the cost card adds fixed 7, 30, and 90-day totals that are shorter than the selected history window.
Examples:
- 30-day history: Today, Last 30 days, Last 7 days.
- 90-day history: Today, Last 90 days, Last 7 days, Last 30 days.
- 365-day history: Today, Last 365 days, Last 7 days, Last 30 days, Last 90 days.
The implementation derives every comparison from the already-loaded daily report. It does not widen scans, add network requests, retain new data, or change provider source selection. Missing calendar days remain zero-usage days rather than making “last 7 days” mean “last 7 non-empty rows.”
## Why this does not claim lifetime cost
“All available local logs” and “lifetime since install” are different contracts. Local Codex, Claude, and Pi logs may be moved, pruned, excluded, or created before CodexBar was installed. The existing plan-utilization history is also capped and has no token or cost ledger. A 365-day total therefore cannot honestly be labeled a lifetime bill.
A true #1708 implementation needs separate approval for an append-only local ledger with:
- an explicit collection start date and completeness state;
- provider/account ownership and reset behavior;
- migration, retention, export, and deletion controls;
- privacy documentation and bounded storage tests;
- UI wording that separates observed utilization snapshots from estimated local-log cost.
Recommendation: ship the opt-in comparison rows for #1500 independently. Keep #1708 open until the ledger/data-retention contract is approved; label any future scan-only total **Available local logs**, never **Lifetime**.
## Maintainer choice
1. **Recommended:** merge with the preference default off. Existing UI and scan cost stay unchanged until a user opts in.
2. Default the preference on. More useful immediately, but adds vertical menu density for existing users.
3. Keep the model only and revisit the presentation. This preserves tested calendar-window aggregation without shipping a new setting.
+42
View File
@@ -0,0 +1,42 @@
---
summary: "Crof provider data source: API key + usage_api request quota."
read_when:
- Adding or tweaking Crof usage parsing
- Updating Crof API key handling
- Documenting Crof reset behavior
---
# Crof provider
Crof is API-only. CodexBar reads `GET https://crof.ai/usage_api/` with a
Bearer token and displays the returned request quota and dollar credit balance.
## Data sources
1. **API key** supplied via `CROF_API_KEY`, `CROFAI_API_KEY`, or Settings →
Providers → Crof. Settings values are stored in `~/.codexbar/config.json`.
2. **Usage endpoint**
- `GET https://crof.ai/usage_api/`
- Request headers: `Authorization: Bearer <api key>`, `Accept: application/json`
- Response fields: `credits`, `requests_plan`, `usable_requests`
## Usage details
- The primary row shows request quota with the exact usable request count on the right.
The visible remaining percent is floored so partially used quotas like `998/1000`
do not round up to `100% left`.
- Crof support said quota reset is around midnight Central time; CodexBar models this
as the next `America/Chicago` midnight so daylight saving time maps to GMT-5 when
appropriate.
- The secondary row shows the current Crof dollar balance, floored to cents so tiny
microcent-level burns never overstate the remaining balance.
- Reset timing is inferred until Crof exposes reset metadata in the usage API.
- The provider icon is SVG and CodexBar renders it as a template image so it
matches the other monochrome provider icons.
- Dashboard: `https://crof.ai/dashboard`.
## Related files
- `Sources/CodexBarCore/Providers/Crof/`
- `Sources/CodexBar/Providers/Crof/`
- `Tests/CodexBarTests/CrofUsageFetcherTests.swift`
+69
View File
@@ -0,0 +1,69 @@
---
summary: "CrossModel provider: API key wallet balance and daily/weekly/monthly spend."
read_when:
- Debugging CrossModel API key usage or spend parsing
- Updating CrossModel balance or spend display
- Explaining CrossModel setup and environment variables
---
# CrossModel Provider
[CrossModel](https://crossmodel.ai) is a multi-provider, OpenAI- and Anthropic-compatible API aggregation platform. You call one API and CrossModel routes the request to the right upstream provider, billing a prepaid wallet.
## Authentication
CrossModel uses API key authentication. Create a key in the [CrossModel console](https://crossmodel.ai/console/api-keys). Keys start with `cm-`.
### Environment Variable
Set the `CROSSMODEL_API_KEY` environment variable:
```bash
export CROSSMODEL_API_KEY="cm-..."
```
### Settings
You can also configure the API key in CodexBar Settings → Providers → CrossModel.
### CLI config
```bash
printf '%s' "$CROSSMODEL_API_KEY" | codexbar config set-api-key --provider crossmodel --stdin
```
## Data Source
The CrossModel provider fetches data from two read-only API endpoints:
1. **Credits API** (`/v1/credits`): Returns the wallet balance (`balance_micro`), currency, and any in-flight holds (`uncollected_micro`). All amounts are integer micro units (1 major currency unit = 1,000,000 micro).
2. **Usage API** (`/v1/usage`): Returns currency plus spend, token, and request counts for the current UTC day, ISO week, and calendar month. CodexBar only displays usage spend when `/usage` currency matches `/credits` currency.
## Display
The CrossModel menu card shows:
- **Balance**: Displayed in the CrossModel menu section and (optionally) in the menu bar using the API currency
- **Spend notes**: Today / this week / this month spend
- **Spend chart**: Day/week/month spend via the shared inline dashboard
## CLI Usage
```bash
codexbar --provider crossmodel
codexbar -p cm # alias
```
## Environment Variables
| Variable | Description |
|----------|-------------|
| `CROSSMODEL_API_KEY` | Your CrossModel API key (required) |
| `CROSSMODEL_API_URL` | Override the base API URL (optional, defaults to `https://api.crossmodel.ai/v1`; loopback HTTP is allowed for local testing) |
## Notes
- Usage values are cached on CrossModel's side and may be up to 60 seconds stale.
- CrossModel uses a prepaid wallet; there is no per-key spending limit, so no quota meter is shown.
- The balance call is required; the usage call is best-effort, deadline-bounded, and will not block the balance if it is slow, unavailable, or returns a mismatched currency.
+86
View File
@@ -0,0 +1,86 @@
---
summary: "Cursor provider data sources: browser cookies or stored session; usage + billing via cursor.com APIs."
read_when:
- Debugging Cursor usage parsing
- Updating Cursor cookie import or session storage
- Adjusting Cursor provider UI/menu behavior
---
# Cursor provider
Cursor is primarily web-backed. Usage is fetched via browser cookies or a stored WebKit session, with Cursor.app local auth as a final fallback.
## Data sources + fallback order
1) **Cached cookie header** (preferred)
- Stored after successful browser import.
- Keychain cache: `com.steipete.codexbar.cache` (account `cookie.cursor`).
2) **Browser cookie import**
- Cookie order from provider metadata (default: Safari → Chrome → Firefox).
- Domain filters: `cursor.com`, `cursor.sh`.
- Cookie names required (any one counts):
- `WorkosCursorSessionToken`
- `__Secure-next-auth.session-token`
- `next-auth.session-token`
3) **Stored session cookies** (fallback)
- Captured by the "Add Account" WebKit login flow.
- Login teardown uses `WebKitTeardown` to avoid Intel WebKit crashes.
- Stored at: `~/Library/Application Support/CodexBar/cursor-session.json`.
4) **Cursor.app local auth** (last fallback)
- Reads Cursor.app's VS Code-style global state DB for the local app bearer token.
- File:
- macOS: `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb`
- Linux: `$XDG_CONFIG_HOME/Cursor/User/globalStorage/state.vscdb` (default `~/.config/Cursor/...`)
- Used only after cookie/session sources fail so existing account-selection precedence stays stable.
- On Linux, this is the primary automatic source because browser import and the WebKit login flow are macOS-only.
- Derives Cursor's first-party web-session cookie, then uses the same usage and account endpoints as browser sessions.
- Account identity comes from that authenticated session; cached app profile fields are not mixed across accounts.
Manual option:
- Preferences → Providers → Cursor → Cookie source → Manual.
- Paste the `Cookie:` header from a cursor.com request.
## API endpoints
- `GET https://cursor.com/api/usage-summary`
- Plan usage (included), on-demand usage, billing cycle window.
- `GET https://cursor.com/api/auth/me`
- User email + name.
- `GET https://cursor.com/api/usage?user=ID`
- Legacy request-based plan usage (request counts + limits).
## Cookie file paths
- Safari: `~/Library/Cookies/Cookies.binarycookies`
- Chrome/Chromium forks: `~/Library/Application Support/Google/Chrome/*/Cookies`
- Firefox: `~/Library/Application Support/Firefox/Profiles/*/cookies.sqlite`
## Linux CLI
- `codexbar usage --provider cursor` reads the signed-in Cursor app's access token from the Linux global state DB and reuses the same `cursor.com` usage endpoints as macOS.
- Automatic browser cookie import and the in-app WebKit login flow remain macOS-only.
- Manual cookie headers from `~/.config/codexbar/config.json` (or legacy `~/.codexbar/config.json`) work on Linux.
## Local storage footprint
When **Settings → Advanced → Track provider local storage** is enabled on macOS, CodexBar measures:
- `~/Library/Application Support/Cursor`
- `~/Library/Application Support/Caches/cursor-updater`
- `~/.cursor`
- `~/Library/Caches/Cursor`
- `~/Library/Caches/com.todesktop.230313mzl4w4u92`
- `~/Library/Caches/com.todesktop.230313mzl4w4u92.ShipIt`
- `~/Library/Caches/cursor-compile-cache`
- `~/Library/HTTPStorages/com.todesktop.230313mzl4w4u92`
The storage detail lists measured paths and their sizes. CodexBar does not delete Cursor data.
## Snapshot mapping
- Primary: plan usage percent (included plan).
- Secondary: Auto + Composer usage percent.
- Tertiary: API (named model) usage percent.
- Provider cost: Extra usage USD. A capped individual budget wins; team accounts without a user cap use the shared team on-demand budget.
- Reset: billing cycle end date.
## Key files
- `Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift`
- `Sources/CodexBar/CursorLoginRunner.swift` (login flow)
+257
View File
@@ -0,0 +1,257 @@
---
summary: "Accepted security and architecture boundary for declarative HTTP JSON providers."
read_when:
- Evaluating issue 1735
- Designing runtime-defined provider identities
- Reviewing configurable endpoint or response-mapping changes
---
# Declarative custom provider design
Status: accepted design boundary. This document defines a bounded MVP; it does not authorize runtime networking or
implement the feature.
Issue: [#1735](https://github.com/steipete/CodexBar/issues/1735)
## Decision summary
A declarative provider can reduce one-off integrations, but it is not a small extension of LLM Proxy or LiteLLM.
Those providers still have compile-time `UsageProvider` identities, descriptors, implementations, request shapes, and response
decoders. A custom provider adds two new trust boundaries:
1. a config file chooses where CodexBar sends a secret;
2. untrusted response data controls user-visible usage, cost, and identity fields.
Accepted direction: pursue a config-only, GET-only, HTTP JSON MVP after separating runtime provider instance identity
from the closed `UsageProvider` enum. Do not add a single `.custom` enum case: multiple configured providers would then
collide in caches, status items, history, widgets, and settings.
## Current constraints
- `ProviderConfig.id` decodes directly as `UsageProvider`.
- `ProviderDescriptorRegistry` bootstraps exactly one descriptor for every `UsageProvider.allCases` value.
- `ProviderImplementationRegistry` constructs implementations with an exhaustive `UsageProvider` switch.
- Usage, errors, status, history, icons, settings, and menu state are keyed by `UsageProvider` across the app.
- The settings sidebar now persists provider-pane selection as `provider:<UsageProvider.rawValue>` and still assumes one
pane per compile-time provider, reinforcing that dynamic identities need the shared seam rather than a parallel UI path.
- LLM Proxy and LiteLLM accept a configured base URL, but their request paths, auth header, decoding, and snapshot
mapping remain provider-specific Swift code.
- `ProviderEndpointOverrideValidator` already provides hardened HTTPS host parsing and an explicit loopback-HTTP mode,
while `ProviderHTTPClient` limits redirects to the same HTTPS origin. Reuse those primitives, but add a custom-provider
policy for fragments, auth-dependent loopback rules, redirect rejection, response limits, and secret-safe errors.
## Proposed MVP contract
### Configuration
Keep the existing provider array and introduce config version 2 with a tagged provider definition. Existing entries remain
first-party definitions; custom entries have a stable user-chosen instance ID and a fixed implementation kind.
```json
{
"version": 2,
"providers": [
{
"id": "acme-gateway",
"kind": "custom-http-json",
"label": "Acme Gateway",
"enabled": true,
"request": {
"method": "GET",
"url": "https://gateway.example.com/v1/quota",
"authentication": { "type": "bearer" }
},
"mapping": {
"primary": {
"usedPercent": { "path": "quota.used_pct" },
"resetsAt": { "path": "quota.reset_at", "dateFormat": "iso8601" },
"windowMinutes": { "path": "quota.window_minutes" }
},
"cost": {
"used": { "path": "spend.usd" },
"currency": "USD",
"period": "Approx. spend"
},
"identity": {
"organization": { "path": "plan.name" },
"loginMethod": { "literal": "api" }
}
}
}
]
}
```
Rules:
- `id`: lowercase ASCII letters, digits, and hyphens; 164 characters; unique across first-party and custom providers.
- `label`: required, trimmed, 180 characters. MVP uses a built-in generic icon.
- `method`: only `GET`.
- `authentication`: `none`, `bearer`, or `x-api-key`; the secret is never inline. Authenticated instances read only their
derived variable `CODEXBAR_CUSTOM_<INSTANCE_ID>_API_KEY`, with the ID uppercased and hyphens replaced by underscores.
A definition cannot name an arbitrary environment variable or header; bearer uses `Authorization` and x-api-key uses
`X-API-Key`.
- `mapping.primary`: optional. When present, requires exactly one of `usedPercent` or `remainingPercent`. Optional fields
are omitted when their paths are missing or null.
- `mapping.cost`: when present, requires `used`; `currency` is a three-letter uppercase literal and `period` is a bounded
literal. A missing limit maps to zero, matching existing sparse cost snapshots.
- `mapping.identity`: optional bounded strings. The configured provider instance ID, not response data, owns snapshot
identity.
- A definition that produces neither a rate window nor cost data is invalid.
### Mapping language
Use a typed dot-path subset, not JSONPath, jq, JavaScript, predicates, or string interpolation.
Grammar:
```text
path = segment *("." segment / "[" index "]")
segment = ALPHA *(ALPHA / DIGIT / "_" / "-")
index = 1*DIGIT
```
Each target field determines its accepted type. Number coercion accepts JSON numbers and finite numeric strings only.
Percentages are clamped to 0100 after rejecting NaN and infinity. Dates require an explicit format: `iso8601`,
`unix-seconds`, or `unix-milliseconds`. Display strings are trimmed and length-limited. Missing optional paths do not fail
the whole snapshot; a present value with the wrong type does.
No wildcards, recursive descent, filters, arithmetic, template evaluation, or user-supplied code. Multi-window arrays and
aggregation are later design work.
Hard limits: 16 mapped leaves per definition; 256 UTF-8 bytes and 32 components per path; 64 ASCII characters per
segment; array indices 04095; response JSON nesting depth 64; mapped display strings 256 UTF-8 bytes. Validate these
limits before traversal. Preflight structural nesting directly on the bounded response bytes with an iterative,
string-aware scanner before materializing JSON, so a hostile nested payload cannot exhaust the call stack.
### Network and secret boundary
- Require HTTPS for authenticated requests. Allow HTTP only for an unauthenticated loopback URL (`localhost`,
`127.0.0.0/8`, or `::1`). Reject URL user info and fragments.
- Extend `ProviderEndpointOverrideValidator`; do not create a second URL parser. Use a dedicated
`ProviderHTTPClient` configuration that rejects every redirect, even though the shared client safely permits same-origin
HTTPS redirects.
- Bind the secret to the validated origin. Disable redirects for MVP; a 3xx response is an error.
- Before any custom-provider fetch, require a local approval record binding the instance ID, complete normalized request
URL, origin, and auth type. Authenticated approvals also bind the fixed header and derived variable name. Store this
record outside the provider config. First use requires explicit app or interactive CLI confirmation that displays the
exact normalized URL and auth fields; headless use fails closed. No import or bulk-approval path is allowed. Loopback,
IP-literal, `.local`, and visibly private targets require typing the normalized URL instead of accepting a button. Any
bound-field change invalidates approval before the next fetch. This gate applies to unauthenticated loopback HTTP too.
- Never interpolate a secret into a URL, path, query, body, label, mapping, diagnostic, or log.
- Resolve the derived environment variable only after approval, when the provider is enabled and a fetch starts. Do not
enumerate the environment.
- Use a dedicated `URLSessionConfiguration.ephemeral` session with `httpCookieStorage = nil`,
`httpShouldSetCookies = false`, `urlCredentialStorage = nil`, `urlCache = nil`, and a reload-ignoring-local-cache policy.
Do not share a session with first-party providers. A dedicated challenge handler allows normal server-trust evaluation
only and cancels client-certificate or HTTP authentication challenges.
- Send `Accept-Encoding: identity`, reject a non-identity `Content-Encoding`, and enforce the streaming 1 MiB cap on bytes
delivered after URL loading's decoding and before JSON materialization. Cancel the task when the cap is exceeded. Use a
15-second total timeout and a JSON content-type check.
- Accept only 2xx responses. Error text may include status and a bounded generic summary, never request headers or the raw
response body.
- Keep custom-provider response data out of provider status polling, cookie import, OAuth, Keychain, token accounts,
browser automation, and CLI subprocess paths.
- Custom definitions must be local config only. No remote catalogs, downloaded definitions, or config URLs.
### Runtime identity seam
Introduce a `ProviderInstanceID` string value that identifies one configured instance. Keep `UsageProvider` as the
compile-time implementation kind for first-party providers.
```text
ProviderDefinition
firstParty(instanceID, UsageProvider, ProviderConfig)
customHTTPJSON(instanceID, CustomHTTPJSONConfig)
```
Migrate runtime dictionaries, persistence keys, `ProviderIdentitySnapshot.providerID`, and identity accessors that
represent an enabled provider instance to `ProviderInstanceID`. First-party instance IDs retain their current raw values,
preserving existing config and stored history. Provider-specific fetchers continue to receive `UsageProvider`; the custom
fetcher receives only its validated custom definition. Provider-specific identity payloads remain keyed by their
compile-time implementation kind, while shared organization and login-method fields belong to the provider instance.
This seam must land independently with characterization tests before the custom network path. It prevents a custom
provider from being threaded through exhaustive first-party switches or sharing state with another custom instance.
## Threat model
| Threat | Required mitigation |
|---|---|
| Shared or malicious config exfiltrates a secret | Dedicated per-instance variable; separate full-URL/auth approval before secret resolution; config changes invalidate approval; redirects disabled |
| Endpoint redirects auth to another host | Treat every redirect as failure |
| Shared config silently probes or changes a GET target | No network access before separate full-URL approval; any URL change invalidates it; no bulk approval; elevated confirmation for visibly local/private targets |
| Hostile JSON causes CPU or memory pressure | 1 MiB cap; bounded depth and path length; no recursive expressions; request timeout |
| Response injects misleading or huge menu text | Typed targets; numeric bounds; string trimming and length limits; configured identity wins |
| Secret or response leaks through diagnostics | Redacted request description; no headers or raw response body in errors/logs |
| Two custom providers overwrite one another | Stable `ProviderInstanceID` keys throughout runtime and persistence |
| Config silently changes first-party behavior | Tagged definition; versioned decoder; duplicate/reserved ID rejection; migration tests |
Out of scope: defending a user from a request URL they explicitly approved, including a public hostname that later
resolves to a local or private address. Approval grants that origin network authority for the approved URL; the
confirmation must state this clearly. CodexBar still must contain the service response and must never disclose unrelated
credentials.
## Explicit non-goals
- Settings UI for creating or editing custom providers.
- Full JSONPath, jq, scripts, plugins, transforms, arithmetic, or templates.
- POST/PUT/PATCH/DELETE, request bodies, refresh mutations, or multiple endpoints.
- Arbitrary headers, cookies, OAuth, browser sessions, Keychain discovery, file-secret references, or inline secrets.
- Custom SVG/file icons, downloaded assets, or remote provider manifests.
- Status-page discovery, incident notifications, chat/model APIs, cost-log scanning, widgets, or token accounts.
- Arrays of rate windows, cross-response joins, pagination, aggregation, or provider-specific special cases.
- Compatibility shims that reinterpret an unknown first-party provider ID as a custom provider.
## Implementation slices
1. **Identity seam:** add `ProviderInstanceID`; migrate config/runtime/persistence keys without behavior changes; add
decode, history, enablement, menu, CLI, and widget characterization tests.
2. **Pure evaluator:** add config types, validator, dot-path parser, typed coercion, and `UsageSnapshot` mapping using only
fixture data.
3. **Bounded transport:** add URL/auth policy and an injected HTTP transport; prove redirect, timeout, size, content-type,
status, and redaction behavior.
4. **Config and CLI integration:** version-2 migration, `codexbar config validate`, local approval records and interactive
approval command, diagnostics, and custom-provider CLI output. No live credentials in tests.
5. **App integration:** generic metadata/icon, refresh lifecycle, menu rendering, persistence, and disabled/error states
through existing shared provider UI.
Each slice should be a separately reviewable PR. Do not combine the identity migration and arbitrary networking in one
change.
## Required proof before enabling the feature
- Config v1 round-trip and v1-to-v2 migration preserve every existing provider entry.
- Duplicate, reserved, malformed, and colliding instance IDs fail validation.
- Multiple custom instances keep snapshots, errors, histories, menu selection, and persistence isolated.
- URL table covers HTTPS, user info, fragments, loopback HTTP, private/public HTTP, IPv4/IPv6, ports, and redirects.
- Auth tests prove the secret reaches only the intended header and never URL, errors, fixtures, snapshots, or logs.
- Approval tests prove first use and every bound-field change fail closed before network or environment access, and that
one instance cannot reuse another instance's approval or derived secret variable. UI/CLI proof covers exact-URL
display, no bulk approval, and typed confirmation for loopback, IP-literal, `.local`, and visibly private targets.
- Mapping tests cover missing/null paths, arrays, wrong types, date formats, finite-number enforcement, every numeric
path/depth/count bound, iterative depth rejection, and string limits.
- Transport tests cover timeout, cancellation, decoded response cap, compressed-response rejection, content type,
non-2xx, and 3xx without live network access.
- Transport isolation tests prove ambient cookies and URL credentials are neither sent nor persisted and cached responses
are not reused.
- Source-blind CLI proof: a fixture endpoint plus isolated config produces the expected usage, cost, identity, and redacted
failure output.
- `make test`, `make check`, structured autoreview, and exact-head CI are green for every implementation PR.
## Accepted owner decisions
1. Declarative provider support is worth the runtime identity migration and long-term versioned schema support.
2. MVP may use unauthenticated loopback HTTP only under the same separate approval gate, including typed confirmation of
the normalized URL. Every authenticated request requires HTTPS.
3. A derived per-instance environment variable plus local URL/auth approval is the only MVP secret source. Keychain
storage is deferred; the initial design must not imply or preserve a second secret path.
4. MVP supports one primary rate window, with cost and identity optional. Multi-window and aggregation semantics remain
out of scope.
5. The first integrated surface is CLI-only. App settings and menu integration wait until the shared runtime accepts
dynamic identities without provider-specific side paths.
Implementation gate: keep custom-provider networking disabled until the independent identity migration, pure evaluator,
bounded transport, approval flow, and their required proof land as separately reviewable changes. If an implementation
cannot preserve this boundary, stop rather than shipping a single `.custom` slot, a parallel UI path, or a compatibility
fallback.
+111
View File
@@ -0,0 +1,111 @@
---
summary: "Deepgram provider: API key setup, project discovery, and usage-breakdown metrics."
read_when:
- Debugging Deepgram API key or project selection
- Updating Deepgram usage parsing or menu display
- Explaining Deepgram setup to users
---
# Deepgram Provider
[Deepgram](https://deepgram.com) is a speech AI platform that provides APIs for speech-to-text, text-to-speech, audio intelligence, and related voice features.
## Authentication
Deepgram uses API key authentication. Get your API key from the [Deepgram Console](https://console.deepgram.com).
The API key must have access to the project you want to query. For usage data, the key also needs the `usage:read` scope for that project.
### Environment Variables
Set the `DEEPGRAM_API_KEY` environment variable:
```bash
export DEEPGRAM_API_KEY="dg_..."
```
Optionally set a Deepgram project ID:
```bash
export DEEPGRAM_PROJECT_ID="your-project-uuid"
```
If `DEEPGRAM_PROJECT_ID` is omitted, CodexBar calls Deepgram's project list endpoint and aggregates usage across all projects visible to the API key.
### Settings
You can also configure the API key and optional project ID in CodexBar Settings → Providers → Deepgram.
### CLI config
```bash
printf '%s' "$DEEPGRAM_API_KEY" | codexbar config set-api-key --provider deepgram --stdin
```
## Data Source
The Deepgram provider fetches summarized usage data from the Deepgram Management API.
1. **Projects API** (`/v1/projects`): Lists projects visible to the API key when no project ID is configured.
2. **Usage Breakdown API** (`/v1/projects/{PROJECT_ID}/usage/breakdown`): Returns summarized project usage over a date range. The response includes fields such as `start`, `end`, `resolution`, and `results`.
Each usage result may include:
* `start`
* `end`
* `hours`
* `total_hours`
* `agent_hours`
* `tokens_in`
* `tokens_out`
* `tts_characters`
* `requests`
Deepgram's usage breakdown endpoint supports querying with `start` and `end` date parameters. The response includes summarized usage results for the selected period.
## Display
The Deepgram menu card shows:
* **Usage notes**: Request count, total audio hours, total billable hours, agent hours, token totals, and TTS characters when returned by Deepgram
* **Identity**: Project name, project ID, or aggregated project count
Deepgram does not currently provide a credit balance through this provider. The provider displays usage, not remaining credits.
## CLI Usage
```bash
codexbar --provider deepgram
codexbar -p dg # alias
```
## Environment Variables
* **DEEPGRAM_API_KEY**: Your Deepgram API key. Required.
* **DEEPGRAM_PROJECT_ID**: Optional Deepgram project UUID. Leave unset to aggregate all visible projects.
* **DEEPGRAM_API_URL**: Override the base API URL. Optional, defaults to `https://api.deepgram.com/v1`.
Override values must be explicit HTTPS URLs or bare hosts/paths that CodexBar normalizes to HTTPS. Explicit
`http://` URLs fail closed before the Deepgram API key is attached to a request. For local proxy testing, use an
HTTPS listener or omit the scheme and let CodexBar normalize the override to HTTPS.
## Permissions
The API key must have permission to read usage data for the configured project.
If the key is valid but lacks the correct scope, Deepgram may return an error like:
```text
INSUFFICIENT_PERMISSIONS
Your account does not have the required scope to perform that action for this project.
Check that your account has the 'usage:read' scope for this project.
```
In that case, create or update a Deepgram API key with the `usage:read` scope for the target project.
## Notes
* Deepgram usage data is project-scoped.
* The project ID should be the project UUID, not just the project display name.
* The provider uses `Authorization: Token <API_KEY>` for requests.
* This provider currently reports usage metrics, not credit balance or per-request cost.
* Summarized usage data is not limited to 90 days, while console logs are limited to 90 days.
+39
View File
@@ -0,0 +1,39 @@
---
summary: "DeepSeek provider data sources: API key + balance endpoint."
read_when:
- Adding or tweaking DeepSeek balance parsing
- Updating API key handling
- Documenting new provider behavior
---
# DeepSeek provider
DeepSeek is API-only. Balance is reported by `GET https://api.deepseek.com/user/balance`,
so CodexBar only needs a valid API key to show your remaining credit balance.
## Data sources
1. **API key** supplied via `DEEPSEEK_API_KEY` / `DEEPSEEK_KEY`, or selected from DeepSeek token accounts in `~/.codexbar/config.json`.
2. **Balance endpoint**
- `GET https://api.deepseek.com/user/balance`
- Request headers: `Authorization: Bearer <api key>`, `Accept: application/json`
- Response contains `is_available`, and a `balance_infos` array with per-currency entries
(`total_balance`, `granted_balance`, `topped_up_balance`).
## Usage details
- The menu card shows total balance with the paid vs. granted breakdown:
e.g. `$50.00 (Paid: $40.00 / Granted: $10.00)`.
- The API separates granted balance from topped-up balance; CodexBar labels these as granted vs. paid credit.
- When multiple currencies are present, USD is shown preferentially.
- If total balance is zero, CodexBar shows an add-credits message. If balance is nonzero but `is_available` is false, it shows "Balance unavailable for API calls".
- There is no session or weekly window — DeepSeek does not expose per-window quota via API.
- Token-account selection injects the selected key into the fetch environment; otherwise CodexBar reads `DEEPSEEK_API_KEY` / `DEEPSEEK_KEY`.
## Key files
- `Sources/CodexBarCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift` (descriptor + fetch strategy)
- `Sources/CodexBarCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift` (HTTP client + JSON parser)
- `Sources/CodexBarCore/Providers/DeepSeek/DeepSeekSettingsReader.swift` (env var resolution)
- `Sources/CodexBar/Providers/DeepSeek/DeepSeekProviderImplementation.swift` (provider activation and token-account visibility)
- `Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift` (DeepSeek token-account injection)
+43
View File
@@ -0,0 +1,43 @@
---
summary: "Devin provider auth, quota endpoint, and setup."
read_when:
- Adding or modifying the Devin provider
- Debugging Devin localStorage import or quota parsing
- Explaining Devin setup
---
# Devin Provider
The Devin provider tracks included daily and weekly usage quotas from
[app.devin.ai](https://app.devin.ai).
## Setup
1. Sign in to Devin in Google Chrome.
2. Open the organization Usage & Limits page once.
3. Enable **Devin** in **Settings → Providers**.
Automatic mode reads only the Devin session and organization metadata from Chrome localStorage. It does not scan other
browsers. CodexBar sends the session token only to `https://app.devin.ai`.
## Manual Auth
Set **Auth source** to **Manual**, then paste either the bare token or the full `Authorization: Bearer ...` header value
from an app.devin.ai API request. The optional organization field accepts a slug, an internal `org_...` ID, or the full
organization URL.
Environment overrides:
- `DEVIN_BEARER_TOKEN` or `DEVIN_AUTHORIZATION`
- `DEVIN_ORGANIZATION` or `DEVIN_ORG`
## Data Source
CodexBar requests:
```text
GET https://app.devin.ai/api/<internal-org-id>/billing/quota/usage
```
The response supplies daily and weekly usage percentages plus reset timestamps. If Devin changes or expires the browser
session, sign in again and refresh CodexBar.
+22
View File
@@ -0,0 +1,22 @@
---
summary: "Doubao provider notes: API-key auth and Volcengine Ark request-limit tracking."
read_when:
- Adding or modifying the Doubao provider
- Debugging Doubao API-key setup
- Explaining Doubao usage display
---
# Doubao Provider
Doubao tracks Volcengine Ark request-limit headers by probing the chat-completions endpoint with a configured API key.
## Setup
1. Enable **Doubao** in Settings → Providers.
2. Paste an API key in the provider settings, or set `ARK_API_KEY`, `VOLCENGINE_API_KEY`, or `DOUBAO_API_KEY`.
3. Refresh provider usage.
## Behavior
- Endpoint: `POST https://ark.cn-beijing.volces.com/api/coding/v3/chat/completions`
- Probe models: `doubao-seed-2.0-code`, `doubao-1.5-pro-32k`, `doubao-lite-32k`
- Reads `x-ratelimit-remaining-requests`, `x-ratelimit-limit-requests`, and `x-ratelimit-reset-requests` when returned.
- If the key is valid but rate-limit headers are missing, CodexBar shows the key as active and links to the dashboard for details.
+63
View File
@@ -0,0 +1,63 @@
---
summary: "ElevenLabs provider notes: API key setup and subscription usage fields."
read_when:
- Adding or modifying the ElevenLabs provider
- Debugging ElevenLabs API keys or subscription usage parsing
- Adjusting ElevenLabs credit or voice-slot labels
---
# ElevenLabs Provider
The ElevenLabs provider reads subscription usage from the ElevenLabs API using an API key.
## Features
- Character credit usage from the current subscription period.
- Reset timing when the API returns `next_character_count_reset_unix`.
- Voice slot and professional voice slot usage when those fields are present.
- Plan/status text from the subscription response.
## Setup
### CLI
Store the API key without opening Settings:
```bash
printf '%s' "$ELEVENLABS_API_KEY" | codexbar config set-api-key --provider elevenlabs --stdin
```
This trims the piped key, writes it to `~/.codexbar/config.json` with restrictive permissions, and enables ElevenLabs by default. Use `--no-enable` to save the key without enabling the provider.
### Settings
1. Open **Settings -> Providers**
2. Enable **ElevenLabs**
3. Open `https://elevenlabs.io/app/settings/api-keys`
4. Create or copy an API key
5. Paste the key into CodexBar's ElevenLabs provider settings
### Environment Variables
CodexBar also accepts these environment variables:
- `ELEVENLABS_API_KEY`
- `XI_API_KEY`
For tests or self-hosted/proxy setups, override the API base URL with `ELEVENLABS_API_URL`.
## How It Works
- Endpoint: `GET https://api.elevenlabs.io/v1/user/subscription`
- Auth header: `xi-api-key`
- Fields used: `character_count`, `character_limit`, `voice_slots_used`, `voice_limit`, `professional_voice_slots_used`, `professional_voice_limit`, `tier`, `status`, `next_character_count_reset_unix`
## Troubleshooting
### "Missing ElevenLabs API key"
Set the key with `codexbar config set-api-key --provider elevenlabs --stdin`, add it in **Settings -> Providers -> ElevenLabs**, set `ELEVENLABS_API_KEY`, or configure an ElevenLabs token account.
### "ElevenLabs API error"
Confirm the API key is valid and that the current network can reach `api.elevenlabs.io`.
+147
View File
@@ -0,0 +1,147 @@
---
summary: "Factory (Droid) provider data sources: API key, browser cookies, WorkOS tokens, and Factory APIs."
read_when:
- Debugging Factory/Droid usage fetch
- Updating Factory cookie, API key, or WorkOS token handling
- Adjusting Factory provider UI/menu behavior
---
# Factory (Droid) provider
Factory (displayed as "Droid") supports API-key and web-based auth. Source mode can be `auto`, `api`, or `web`.
## Data sources + fallback order
### API (`api`)
1. Resolve a Factory API key from, in order:
- `~/.codexbar/config.json` `providers[].apiKey` for `factory` (also via Settings or
`codexbar config set-api-key --provider factory`)
- `FACTORY_API_KEY`
- optional `~/.factory/.env` (`FACTORY_API_KEY=…` or `export FACTORY_API_KEY=…`)
2. Call Factory APIs with `Authorization: Bearer <apiKey>` (same billing-limits / bearer path as session tokens).
### Web (`web`)
Fetch attempts run in this exact order:
1) **Cached cookie header** (Keychain cache `com.steipete.codexbar.cache`, account `cookie.factory`).
2) **Stored session** (`~/Library/Application Support/CodexBar/factory-session.json`).
3) **Stored bearer token** (same session file).
4) **Stored WorkOS refresh token** (same session file).
5) **Local storage WorkOS tokens** (Safari + Chrome/Chromium/Arc leveldb).
6) **Browser cookies (Safari only)** for Factory domains.
7) **WorkOS cookies (Safari)** to mint tokens.
8) **Browser cookies (Chrome, Firefox)** for Factory domains.
9) **WorkOS cookies (Chrome, Firefox)** to mint tokens.
If a step succeeds, we cache cookies/tokens back into the session store.
Manual option:
- Preferences → Providers → Droid → Cookie source → Manual.
- Paste the `Cookie:` header from app.factory.ai.
### Auto (`auto`)
1. Try API first when a key is resolvable.
2. Fall back to web strategies on missing/unauthorized keys and other recoverable API failures
(timeouts, DNS/network errors, 5xx, parse failures). Cancellation does not fall back.
3. Explicit `api` mode stays strict and does not fall back to web.
4. When both an API key and a browser/WorkOS session are available, Auto may surface the API-key
account rather than the browser session. Use explicit `web` (or `api`) to pin account precedence.
## Settings
- Preferences → Providers → Droid:
- Usage source: `Auto`, `API key`, `Browser cookies`
- API key: optional override for `FACTORY_API_KEY` / `~/.factory/.env`
- Cookie source: Automatic / Manual (web path)
## CLI
```bash
printf '%s' "$FACTORY_API_KEY" | codexbar config set-api-key --provider factory --stdin
codexbar usage --provider factory --source api
codexbar usage --provider factory --source web
```
## Cookie import
- Cookie domains: `factory.ai`, `app.factory.ai`, `auth.factory.ai`.
- Cookie names considered a session:
- `wos-session`
- `__Secure-next-auth.session-token`
- `next-auth.session-token`
- `__Secure-authjs.session-token`
- `__Host-authjs.csrf-token`
- `authjs.session-token`
- `session`
- `access-token`
- Stale-token retry filters:
- `access-token`, `__recent_auth`.
## Base URL selection
- Candidates are tried in order (deduped):
- `https://auth.factory.ai`
- `https://api.factory.ai`
- `https://app.factory.ai`
- `baseURL` (default `https://app.factory.ai`)
- Cookie domains influence candidate ordering (auth domain first if present).
## Factory API endpoints
All requests set:
- `Accept: application/json`
- `Content-Type: application/json`
- `Origin: https://app.factory.ai`
- `Referer: https://app.factory.ai/`
- `x-factory-client: web-app`
- `Authorization: Bearer <token>` when a bearer token / API key is available.
- `Cookie: <session cookies>` when cookies are available.
Endpoints:
- `GET https://api.factory.ai/api/billing/limits`
- Preferred when the account uses token-rate-limits billing (5h / weekly / monthly windows).
- `GET <baseURL>/api/app/auth/me`
- Returns org + subscription metadata + feature flags.
- `GET <baseURL>/api/organization/subscription/usage`
- Query: `useCache=true` and optional `userId`
- Returns Standard + Premium token usage and billing window.
## WorkOS token minting
- Endpoint:
- `POST https://api.workos.com/user_management/authenticate`
- Body:
- `client_id`: one of
- `client_01HXRMBQ9BJ3E7QSTQ9X2PHVB7`
- `client_01HNM792M5G5G1A2THWPXKFMXB`
- `grant_type`: `refresh_token`
- `refresh_token`: from local storage or session store
- Optional: `organization_id`
- When using cookies: `useCookie: true` + `Cookie: <workos.com cookies>`
## Local storage WorkOS token extraction
- Safari:
- Root: `~/Library/Containers/com.apple.Safari/Data/Library/WebKit/WebsiteData/Default`
- Finds `origin` files containing `app.factory.ai` or `auth.factory.ai`, then reads
`LocalStorage/localstorage.sqlite3`.
- Chrome/Chromium/Arc/Helium:
- Roots under `~/Library/Application Support/<Browser>/User Data/<Profile>/Local Storage/leveldb`.
- Helium uses `~/Library/Application Support/net.imput.helium/<Profile>/Local Storage/leveldb` (no `User Data`).
- Scans LevelDB files for `workos:refresh-token` and `workos:access-token`.
- Parsed tokens:
- `workos:refresh-token` (required)
- `workos:access-token` (optional)
- Organization ID parsed from JWT when available.
## Session storage
- File: `~/Library/Application Support/CodexBar/factory-session.json`
- Stores cookies + bearer token + WorkOS refresh token.
## Snapshot mapping
- Token-rate-limits accounts: primary 5h, secondary weekly, tertiary monthly (+ optional Core windows).
- Legacy Standard/Premium accounts: primary Standard, secondary Premium; reset at billing period end.
- Plan/tier + org name from auth response.
## Troubleshooting
- Missing API key: set `FACTORY_API_KEY`, Settings → Droid → API key, or `codexbar config set-api-key --provider factory`.
- Unauthorized API key (401/403): regenerate at app.factory.ai/settings/api-keys.
- Missing session: log in to app.factory.ai in a supported browser, or paste a Cookie header in Manual mode.
## Key files
- `Sources/CodexBarCore/Providers/Factory/FactoryProviderDescriptor.swift`
- `Sources/CodexBarCore/Providers/Factory/FactorySettingsReader.swift`
- `Sources/CodexBarCore/Providers/Factory/FactoryStatusProbe.swift`
- `Sources/CodexBarCore/Providers/Factory/FactoryLocalStorageImporter.swift`
Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

+101
View File
@@ -0,0 +1,101 @@
---
summary: "Gemini provider data sources: OAuth-backed quota APIs, token refresh, and tier detection."
read_when:
- Debugging Gemini quota fetch or auth issues
- Updating Gemini CLI OAuth integration
- Adjusting tier detection or model mapping
---
# Gemini provider
Gemini uses the Gemini CLI OAuth credentials and private quota APIs. No browser cookies.
## Data sources + fallback order
1) **OAuth-backed quota API** (only path used in `fetch()`)
- Reads auth type from `~/.gemini/settings.json`.
- Supported: `oauth-personal` (or unknown → try OAuth creds).
- Unsupported: `api-key`, `vertex-ai` (hard error).
2) **Legacy CLI parsing** (parser exists but not used in current fetch path)
- `GeminiStatusProbe.parse(text:)` can parse `/stats` output.
## OAuth credentials
- File: `~/.gemini/oauth_creds.json`.
- Required fields: `access_token`, `refresh_token` (optional), `id_token`, `expiry_date`.
- If access token is expired, we refresh via Google OAuth using client ID/secret extracted
from the Gemini CLI install (see below).
## OAuth client ID/secret extraction
- Resolution order:
1. `GEMINI_OAUTH_CLIENT_ID` + `GEMINI_OAUTH_CLIENT_SECRET` environment override.
2. `GEMINI_OAUTH2_JS_PATH` pointing at a readable `oauth2.js` file.
3. Installed Gemini CLI package (`oauth2.js` / bundle regex extraction).
4. Known global Gemini CLI install paths (Homebrew npm prefix layouts, then
Homebrew Cellar/`opt` `libexec` package roots when the GUI cannot resolve
the `gemini` binary).
- We locate the installed `gemini` binary, then search for:
- Homebrew nested path:
- `.../libexec/lib/node_modules/@google/gemini-cli/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js`
- Homebrew Cellar/opt package root (no-binary fallback):
- `/opt/homebrew/Cellar/gemini-cli/<version>/libexec/lib/node_modules/@google/gemini-cli`
- `/opt/homebrew/opt/gemini-cli/libexec/lib/node_modules/@google/gemini-cli`
- same under `/usr/local`
- Bun/npm sibling path:
- `.../node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js`
- Regex extraction:
- `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` from `oauth2.js` or Homebrew bundle chunks.
## API endpoints
- Quota:
- `POST https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota`
- Body: `{ "project": "<projectId>" }` (or `{}` if unknown)
- Header: `Authorization: Bearer <access_token>`
- Project discovery (quota project ID):
- Primary: `cloudaicompanionProject` from `loadCodeAssist`.
- Fallback: `GET https://cloudresourcemanager.googleapis.com/v1/projects`
- Picks `gen-lang-client*` or label `generative-language`.
- Tier detection:
- `POST https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist`
- Body: `{ "metadata": { "ideType": "GEMINI_CLI", "pluginType": "GEMINI" } }`
- Token refresh:
- `POST https://oauth2.googleapis.com/token`
- Form body: `client_id`, `client_secret`, `refresh_token`, `grant_type=refresh_token`.
## Parsing + mapping
- Quota buckets:
- `remainingFraction`, `resetTime`, `modelId`.
- For each model, lowest `remainingFraction` wins.
- `percentLeft = remainingFraction * 100`.
- Reset:
- `resetTime` parsed as ISO-8601, formatted as "Resets in Xh Ym".
- UI mapping:
- Primary: Pro models (lowest percent left).
- Secondary: Flash models (lowest percent left).
## Plan detection
- Tier from `loadCodeAssist`:
- `paidTier.name` → paid subscription label from Google, preferred whenever present
- `standard-tier` → "Paid" (fallback when `paidTier.name` is absent)
- `free-tier` + `hd` claim → "Workspace" (fallback when `paidTier.name` is absent)
- `free-tier` → "Free"
- `legacy-tier` → "Legacy"
- Email from `id_token` JWT claims.
## Consumer-tier migration (June 2026)
- [Google stopped serving](https://developers.google.com/gemini-code-assist/docs/deprecations/code-assist-individuals)
Gemini CLI OAuth for individual, AI Pro, and Ultra accounts on 2026-06-18. Standard and Enterprise
subscriptions remain supported; paid API-key access is outside CodexBar's OAuth-backed Gemini provider.
- When quota, `loadCodeAssist`, or token-refresh responses include Google's unsupported-client
migration signal (`UNSUPPORTED_CLIENT`, `IneligibleTierError`, or Antigravity migration copy),
CodexBar surfaces `consumerTierDeprecated` with guidance to use the Antigravity provider.
- Settings shows an **Enable Antigravity provider** action only after CodexBar observes
`consumerTierDeprecated` during a Gemini refresh (typed sentinel state, not user-facing text matching).
- The action is explicit: CodexBar never automatically enables Antigravity or falls back to it.
- Ordinary Gemini login, `notLoggedIn`, and Antigravity setup errors remain unchanged. CodexBar does not
capture Terminal `gemini` OAuth output, so Terminal-only failures cannot activate the migration action.
- Workspace and education Google accounts are outside the June 2026 consumer shutdown; keep using the
Gemini provider. Antigravity remains the consumer replacement path for individual, AI Pro, and Ultra.
## Key files
- `Sources/CodexBarCore/Providers/Gemini/GeminiStatusProbe.swift`
+151
View File
@@ -0,0 +1,151 @@
---
summary: "Grok provider data sources: ACP JSON-RPC, grok.com billing fallback, OAuth credentials, and local session signals."
read_when:
- Debugging Grok billing/usage parsing
- Updating `grok agent stdio` JSON-RPC integration
- Adjusting `~/.grok/auth.json` credential reading
---
# Grok provider
Grok uses xAI's official Grok Build CLI (`grok`, released 2026-05-14). Usage data is
fetched via the ACP JSON-RPC `x.ai/billing` extension method over `grok agent stdio`
when available, then via grok.com's billing gRPC-web endpoint using the signed-in
browser session when the CLI surface does not expose billing.
## Data sources + fallback order
1) **`~/.grok/auth.json` (primary; always works for SuperGrok subscribers)**
- Reads `email`, `team_id`, `first_name`/`last_name`, plan-hint (`auth_mode`)
for the identity row in the menu.
2) **`grok agent stdio` ACP JSON-RPC** (best-effort, currently disabled in grok 0.1.210)
- We spawn `grok agent stdio` and call `initialize` + `x.ai/billing` (no params).
- **Known limitation:** in grok 0.1.210 the `x.ai/billing` extension method
is only wired in the interactive TUI; the agent-stdio surface returns
`-32601 Method not found`. The provider degrades silently to identity-only
when this happens. When xAI exposes billing on the agent protocol, no
code change is required.
- One non-obvious quirk: grok's ACP parser does not unescape `\/` in method
names. `Foundation.JSONSerialization.data` defaults to escaping forward
slashes, so payloads must be re-encoded with `\/``/` before being
written to stdin or grok will silently drop them (12s client-side
timeout instead of the expected error response).
3) **grok.com billing gRPC-web fallback** (best-effort)
- POSTs an empty gRPC-web protobuf request to
`https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig`.
- Uses grok.com browser session cookies. When a non-expired
`~/.grok/auth.json` token is available, CodexBar first sends it with each
browser session, then retries that session with cookies only.
- CodexBar imports Chrome only by default to avoid unrelated browser
Keychain prompts.
- CLI/test runtime does not import browser cookies unless
`CODEXBAR_ALLOW_BROWSER_COOKIE_IMPORT=1` is set.
- `~/.grok/auth.json` is still used for identity and as a last best-effort
bearer-only probe after browser sessions fail. Expired tokens are not sent.
- Parses the returned protobuf enough to recover used percent and
reset timestamp, accepting both gRPC-web frames and the raw protobuf form
returned by some successful requests. A current billing period with an
omitted proto3 `credit_usage_percent` is treated as zero usage. This keeps
billing visible when `grok agent stdio` returns `Method not found`.
4) **Local session signals** (informational fallback)
- Walks `~/.grok/sessions/<encoded-cwd>/<session-id>/signals.json` files (last 30 days).
- Aggregates `totalTokensBeforeCompaction`, `contextTokensUsed`, `modelsUsed`,
and the most recent session timestamp.
## OAuth credentials
- File: `~/.grok/auth.json` (path overridable via `GROK_HOME`).
- Top-level keys are OIDC scope URLs. CodexBar prefers entries under
`https://auth.x.ai::<client-id>` (SuperGrok), falling back to
`https://accounts.x.ai/sign-in` (legacy session).
- Required fields per entry: `key` (bearer token), `refresh_token`, `expires_at`,
`auth_mode`, `email`, `team_id`, `user_id`, `first_name`/`last_name`.
- Tokens are issued by `grok login` and expire after ~7 days; refresh is handled by
the CLI itself (CodexBar does not refresh; it just reads the cached credential).
## JSON-RPC contract
- Transport: stdin/stdout, newline-delimited JSON-RPC 2.0 (no Content-Length framing).
- `initialize` params:
```json
{
"protocolVersion": "1",
"clientCapabilities": {
"fs": { "readTextFile": false, "writeTextFile": false },
"terminal": false
}
}
```
- `x.ai/billing` result shape (all monetary values are `{ val: <cents> }`):
```json
{
"billingCycle": {
"billingPeriodStart": "2026-05-01T00:00:00Z",
"billingPeriodEnd": "2026-06-01T00:00:00Z"
},
"monthlyLimit": { "val": 99900 },
"onDemandCap": { "val": 0 },
"on_demand_enabled": false,
"disabledByConfig": false,
"usage": {
"includedUsed": { "val": 12345 },
"onDemandUsed": { "val": 0 },
"totalUsed": { "val": 12345 }
}
}
```
- Auth errors surface as JSON-RPC errors with the message
`"Authentication required to fetch billing data. Run 'grok login' to authenticate."`.
- Timeouts: 8s for `initialize`, 12s for `x.ai/billing`. CodexBar terminates the
child `grok` process on timeout to avoid leaking subprocesses.
## Mapping to `UsageSnapshot`
- **Primary window** = credit usage (against the subscription/included limit):
- CLI RPC: `usedPercent` = `usage.totalUsed.val / monthlyLimit.val * 100`;
`resetsAt` = `billingCycle.billingPeriodEnd`.
- grok.com fallback: `usedPercent` and `resetsAt` parsed from the gRPC-web
billing protobuf.
- The UI label for the live usage bar is dynamic: "Weekly" or "Monthly"
when `resetsAt` matches a common cycle, falling back to the registered
"Credits" label otherwise. Settings and history views continue to use
"Credits" as the stable metric name.
- **Identity**:
- `accountEmail` from credential `email`.
- `accountOrganization` from credential `team_id`.
- `loginMethod` = "SuperGrok" for OIDC, otherwise the raw `auth_mode`.
## Local fallback (`~/.grok/sessions/`)
Each session directory contains `signals.json` with fields like:
```json
{
"turnCount": 1,
"contextTokensUsed": 2968,
"contextWindowTokens": 512000,
"totalTokensBeforeCompaction": 0,
"modelsUsed": ["grok-build"],
"primaryModelId": "grok-build",
"sessionDurationSeconds": 47
}
```
CodexBar aggregates these into a `GrokLocalSessionSummary` (session count, total
tokens, last session time, primary model) and exposes it for diagnostics even when
the RPC path is unavailable.
## Status
xAI has not exposed a Statuspage-style status feed yet. The "View Status" link
points to `https://status.x.ai`.
## Key files
- `Sources/CodexBarCore/Providers/Grok/GrokProviderDescriptor.swift`
- `Sources/CodexBarCore/Providers/Grok/GrokAuth.swift`
- `Sources/CodexBarCore/Providers/Grok/GrokRPCClient.swift`
- `Sources/CodexBarCore/Providers/Grok/GrokWebBillingFetcher.swift`
- `Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift`
- `Sources/CodexBarCore/Providers/Grok/GrokLocalSessionScanner.swift`
- `Sources/CodexBar/Providers/Grok/GrokProviderImplementation.swift`
+31
View File
@@ -0,0 +1,31 @@
---
summary: "GroqCloud provider setup and Prometheus metrics usage source."
read_when:
- Configuring GroqCloud usage tracking
- Debugging GroqCloud request or token-rate display
---
# GroqCloud
CodexBar's GroqCloud provider is separate from the xAI Grok provider. It uses a GroqCloud API key and the Enterprise
Prometheus metrics API.
## Setup
Store the key in the shared app/CLI config:
```bash
printf '%s' "$GROQ_API_KEY" | codexbar config set-api-key --provider groq --stdin
```
Or set `GROQ_API_KEY` in the process environment. `GROQ_API_URL` can override the default `https://api.groq.com/v1`
base URL for private gateways.
## Menu display
- Primary: requests per minute.
- Secondary: tokens per minute.
- Tertiary: prompt cache hits per minute when the metric exists.
- Dashboard link: GroqCloud metrics dashboard.
If the key lacks Prometheus metrics access, CodexBar shows the API error instead of guessing from unrelated endpoints.
+41
View File
@@ -0,0 +1,41 @@
<svg width="6111" height="3239" viewBox="0 0 6111 3239" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_149_20)">
<path d="M6111.42 217.809C6111.42 97.5163 6013.9 0 5893.61 0H217.772C97.4792 0 -0.0371094 97.5163 -0.0371094 217.809L-0.0371093 4083.91C-0.0371093 4122.35 6.36903 4212.04 48.0089 4212.04H6063.38C6105.02 4212.04 6111.42 4122.35 6111.42 4083.91V217.809Z" fill="#2F3541"/>
<path d="M5893.61 38.4365H217.77C118.706 38.4365 38.3984 118.744 38.3984 217.808V4090.32C38.3984 4146.93 84.2883 4192.82 140.897 4192.82H5970.48C6027.09 4192.82 6072.98 4146.93 6072.98 4090.32V217.808C6072.98 118.744 5992.67 38.4365 5893.61 38.4365Z" fill="black"/>
<g filter="url(#filter0_ii_149_20)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5893.61 19.2188H217.774C108.095 19.2188 19.1836 108.131 19.1836 217.809V4090.32C19.1836 4157.54 73.678 4212.04 140.9 4212.04H698.234V4205.63C698.234 4198.55 692.498 4192.82 685.422 4192.82H140.9C84.2921 4192.82 38.402 4146.93 38.402 4090.32V217.809C38.402 118.745 118.709 38.4372 217.774 38.4372H5893.61C5992.68 38.4372 6072.99 118.745 6072.99 217.809V4090.32C6072.99 4146.93 6027.1 4192.82 5970.49 4192.82H5425.97C5418.89 4192.82 5413.15 4198.55 5413.15 4205.63V4212.04H5970.49C6037.71 4212.04 6092.2 4157.54 6092.2 4090.32V217.809C6092.2 108.131 6003.29 19.2188 5893.61 19.2188Z" fill="#232122"/>
</g>
<g clip-path="url(#clip1_149_20)">
<path d="M140.893 201.794C140.893 159.337 175.31 124.92 217.766 124.92H5893.61C5936.06 124.92 5970.48 159.337 5970.48 201.794V3914.15H140.893V201.794Z" fill="white"/>
<path d="M-128 -66H6110C6138.17 -66 6161 -43.1665 6161 -15V4278H-128V-66Z" fill="url(#paint0_linear_149_20)"/>
</g>
</g>
<defs>
<filter id="filter0_ii_149_20" x="19.1836" y="19.2188" width="6073.02" height="4196.02" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3.20307"/>
<feGaussianBlur stdDeviation="4.80461"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_149_20"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3.20307"/>
<feGaussianBlur stdDeviation="3.20307"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
<feBlend mode="normal" in2="effect1_innerShadow_149_20" result="effect2_innerShadow_149_20"/>
</filter>
<linearGradient id="paint0_linear_149_20" x1="3016.5" y1="-66" x2="3016.5" y2="4278" gradientUnits="userSpaceOnUse">
<stop stop-color="#0F0F11"/>
<stop offset="1"/>
</linearGradient>
<clipPath id="clip0_149_20">
<rect width="6111" height="3239" fill="white"/>
</clipPath>
<clipPath id="clip1_149_20">
<path d="M140.893 201.794C140.893 159.337 175.31 124.92 217.766 124.92H5893.61C5936.06 124.92 5970.48 159.337 5970.48 201.794V3914.15H140.893V201.794Z" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+37
View File
@@ -0,0 +1,37 @@
---
summary: "Convert macOS .icon bundles to CodexBar .icns via Scripts/build_icon.sh and ictool."
read_when:
- Updating the CodexBar app icon or asset pipeline
- Preparing release builds that need a refreshed icns
---
# Icon pipeline (macOS .icon → .icns without Xcodebuild)
We use the new macOS 26 “glass” `.icon` bundle from Icon Composer/IconStudio and convert it to `.icns` via Xcodes hidden CLI (ictool/icontool), without an Xcode project.
## Script
`Scripts/build_icon.sh ICON.icon CodexBar [outdir]`
What it does:
1) Finds `ictool` (or `icontool`) in `/Applications/Xcode.app/Contents/Applications/Icon Composer.app/Contents/Executables/`.
2) Renders the macOS Default appearance of the `.icon` to an 824×824 PNG (inner art, glass applied).
3) Pads to 1024×1024 with transparency (restores Tahoe squircle margin, avoids “white plate”).
4) Downscales to all required sizes into an `.iconset`.
5) Runs `iconutil -c icns``Icon.icns`.
Requirements:
- Xcode 26+ installed (IC tool lives inside the Xcode bundle).
- `sips` and `iconutil` (system tools).
Usage:
```bash
./Scripts/build_icon.sh Icon.icon CodexBar
```
Outputs `Icon.icns` at repo root.
Why this approach:
- Naive `sips`/`iconutil` from raw PNGs often leaves a white/grey plate because the inner art is full-bleed. The ictool render + transparent padding matches Xcodes asset-pipeline output.
Notes:
- If Xcode is in a nonstandard location, set `XCODE_APP=/path/to/Xcode.app` before running.
- Script is CI-friendly; no Xcode project needed.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="#000000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/></svg>

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M60 10 C30 10 15 35 15 55 C15 75 30 95 45 100 L45 110 L55 110 L55 100 C55 100 60 102 65 100 L65 110 L75 110 L75 100 C90 95 105 75 105 55 C105 35 90 10 60 10Z" fill="#E8E8E8"/>
<path d="M20 45 C5 40 0 50 5 60 C10 70 20 65 25 55 C28 48 25 45 20 45Z" fill="#E8E8E8"/>
<path d="M100 45 C115 40 120 50 115 60 C110 70 100 65 95 55 C92 48 95 45 100 45Z" fill="#E8E8E8"/>
<path d="M45 15 Q35 5 30 8" stroke="#E8E8E8" stroke-width="3" stroke-linecap="round"/>
<path d="M75 15 Q85 5 90 8" stroke="#E8E8E8" stroke-width="3" stroke-linecap="round"/>
<circle cx="45" cy="35" r="6" fill="#141414"/>
<circle cx="75" cy="35" r="6" fill="#141414"/>
<circle cx="46" cy="34" r="2.5" fill="#00E5CC"/>
<circle cx="76" cy="34" r="2.5" fill="#00E5CC"/>
</svg>

After

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

+775
View File
@@ -0,0 +1,775 @@
<!doctype html>
<html lang="en" class="bg-black">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CodexBar — every AI coding limit, in your menu bar</title>
<meta
name="description"
content="A tiny macOS menu bar app that tracks AI coding-provider usage windows, credits, costs, and resets across 59 providers — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, and more."
/>
<meta name="theme-color" content="#0a0a0c" />
<link rel="canonical" href="https://codexbar.app/" />
<link rel="alternate" hreflang="x-default" href="https://codexbar.app/" />
<link rel="alternate" hreflang="en" href="https://codexbar.app/?lang=en" />
<link rel="alternate" hreflang="zh-CN" href="https://codexbar.app/?lang=zh-CN" />
<link rel="alternate" hreflang="zh-TW" href="https://codexbar.app/?lang=zh-TW" />
<link rel="alternate" hreflang="ja" href="https://codexbar.app/?lang=ja-JP" />
<link rel="alternate" hreflang="es" href="https://codexbar.app/?lang=es" />
<link rel="alternate" hreflang="pt-BR" href="https://codexbar.app/?lang=pt-BR" />
<link rel="alternate" hreflang="ko" href="https://codexbar.app/?lang=ko" />
<link rel="alternate" hreflang="de" href="https://codexbar.app/?lang=de" />
<link rel="alternate" hreflang="fr" href="https://codexbar.app/?lang=fr" />
<link rel="alternate" hreflang="ar" href="https://codexbar.app/?lang=ar" />
<link rel="alternate" hreflang="it" href="https://codexbar.app/?lang=it" />
<link rel="alternate" hreflang="vi" href="https://codexbar.app/?lang=vi" />
<link rel="alternate" hreflang="nl" href="https://codexbar.app/?lang=nl" />
<link rel="alternate" hreflang="tr" href="https://codexbar.app/?lang=tr" />
<link rel="alternate" hreflang="uk" href="https://codexbar.app/?lang=uk" />
<link rel="alternate" hreflang="ru" href="https://codexbar.app/?lang=ru" />
<link rel="alternate" hreflang="id" href="https://codexbar.app/?lang=id" />
<link rel="alternate" hreflang="pl" href="https://codexbar.app/?lang=pl" />
<link rel="alternate" hreflang="fa" href="https://codexbar.app/?lang=fa" />
<link rel="alternate" hreflang="th" href="https://codexbar.app/?lang=th" />
<link rel="alternate" hreflang="gl" href="https://codexbar.app/?lang=gl" />
<link rel="alternate" hreflang="ca" href="https://codexbar.app/?lang=ca" />
<link rel="alternate" hreflang="sv" href="https://codexbar.app/?lang=sv" />
<meta property="og:title" content="CodexBar" />
<meta
property="og:description"
content="Track usage windows, credits, and resets across 59 AI coding providers from your macOS menu bar."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://codexbar.app/" />
<meta property="og:image" content="https://codexbar.app/social.png?v=4" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:image" content="https://codexbar.app/social.png?v=4" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/png" href="./icon.png?v=2" />
<link rel="apple-touch-icon" href="./icon.png?v=2" />
<script>
(() => {
const queryTheme = new URLSearchParams(location.search).get('theme');
let storedTheme = 'system';
try {
storedTheme = localStorage.getItem('codexbar-theme') || 'system';
} catch (_) {}
const preference = queryTheme === 'light' || queryTheme === 'dark' ? queryTheme : storedTheme;
const resolvedTheme = preference === 'system'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: preference;
document.documentElement.dataset.theme = resolvedTheme;
document.documentElement.dataset.themePreference = preference;
})();
</script>
<link rel="stylesheet" href="./site-utilities.css?v=1" />
<link rel="stylesheet" href="./site.css?v=3" />
</head>
<body class="min-h-screen overflow-x-hidden text-zinc-100">
<header class="absolute inset-x-0 top-0 z-20 flex h-[var(--header-height)] items-center justify-between bg-transparent px-5 sm:px-8 md:grid md:grid-cols-[1fr_auto_1fr] lg:px-12">
<a href="#top" class="button-press flex w-fit items-center gap-2.5 text-sm font-semibold tracking-[-0.03em] text-white sm:text-base" aria-label="CodexBar home" data-i18n-aria-label="nav.home">
<img src="./icon.png?v=2" alt="" class="h-7 w-7 rounded-[7px] sm:h-8 sm:w-8 sm:rounded-lg" />
<span>CodexBar</span>
</a>
<nav class="hidden items-center justify-center gap-3 md:flex" aria-label="Project links" data-i18n-aria-label="nav.projectLinks">
<a
href="https://github.com/steipete/CodexBar/blob/main/docs/providers.md"
class="button-press hidden text-[14px] text-zinc-400 hover:text-white sm:block"
data-i18n="nav.docs">docs</a>
<span class="hidden text-sm text-zinc-700 sm:block" aria-hidden="true" data-project-separator>/</span>
<a
href="https://github.com/steipete/CodexBar"
class="button-press group flex items-center justify-center gap-2.5 text-[14px] text-zinc-400 hover:text-white"
aria-label="CodexBar source on GitHub" data-i18n-aria-label="nav.sourceGitHub"
>
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 .8a11.4 11.4 0 0 0-3.6 22.2c.6.1.8-.3.8-.6v-2.2c-3.3.7-4-1.4-4-1.4-.5-1.4-1.3-1.8-1.3-1.8-1.1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1.1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.6-.3-5.4-1.3-5.4-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.1 1.2a10.8 10.8 0 0 1 5.7 0C16.9 4.7 18 5 18 5c.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.4 5.7.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A11.4 11.4 0 0 0 12 .8Z" />
</svg>
<span class="hidden sm:inline" data-i18n-rich="nav.sourceStars">
source
</span>
</a>
</nav>
<nav class="flex items-center justify-end gap-2 sm:gap-3" aria-label="Display and download" data-i18n-aria-label="nav.displayDownload">
<button class="theme-option grid" id="theme-toggle" type="button" data-theme-value="system" aria-label="Theme: system. Switch to light" title="System theme">
<svg data-theme-icon="system" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect width="20" height="14" x="2" y="3" rx="2" />
<line x1="8" x2="16" y1="21" y2="21" />
<line x1="12" x2="12" y1="17" y2="21" />
</svg>
<svg data-theme-icon="light" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2" /><path d="M12 20v2" /><path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" /><path d="M2 12h2" /><path d="M20 12h2" /><path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" />
</svg>
<svg data-theme-icon="dark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" />
</svg>
</button>
<div class="language-picker" id="language-picker">
<button
class="language-picker-trigger button-press"
id="language-picker-trigger"
type="button"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="language-picker-menu"
aria-label="Language" data-i18n-aria-label="nav.language"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="9" />
<path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 3a15 15 0 0 0 0 18" />
</svg>
<span data-lang-short>EN</span>
</button>
<div class="language-picker-menu" id="language-picker-menu" hidden>
<div class="language-picker-menu-scroll" id="language-picker-list" role="listbox" aria-label="Language" data-i18n-aria-label="nav.language"></div>
</div>
</div>
<span class="header-divider hidden sm:block" aria-hidden="true"></span>
<a
href="https://github.com/steipete/CodexBar/releases/latest"
class="download-button button-press inline-flex h-9 items-center gap-2 rounded-full px-4 text-sm font-medium sm:h-10 sm:px-5"
>
<span class="grid h-4 w-4 place-items-center" aria-hidden="true">
<img src="./images/apple.svg" alt="" class="apple-icon" />
</span>
<span data-i18n="nav.download">download</span>
</a>
</nav>
</header>
<main id="top">
<section class="hero-section relative overflow-hidden">
<div class="mockup-stage" id="mockup-stage" aria-hidden="true">
<div class="mockup-glow" aria-hidden="true"></div>
<div class="mockup-wrap" id="mockup-wrap">
<img
src="./hero-mockup.svg"
alt="MacBook menu bar"
data-i18n-alt="mockup.macbookAlt"
class="block h-full w-full select-none object-contain"
draggable="false"
width="6111"
height="3239"
fetchpriority="high"
/>
</div>
<div class="mockup-hud-layer">
<div class="mockup-artboard">
<div class="mockup-hud-anchor">
<div class="system-menubar" aria-label="macOS menu bar status items" data-i18n-aria-label="mockup.menuBarAria">
<img class="sf-symbol sf-symbol--openclaw" src="./images/menubar/openclaw-menubar.svg" alt="" aria-hidden="true" />
<div class="menu-bar-mark" aria-label="CodexBar menu bar icon" data-i18n-aria-label="mockup.menuIconAria">
<span></span>
<span></span>
</div>
<img class="sf-symbol" src="./images/menubar/wifi.png" alt="" aria-hidden="true" style="filter: brightness(0) invert(86%) drop-shadow(0 0.08em 0.18em rgba(0, 0, 0, 0.58))" />
<img class="sf-symbol" src="./images/menubar/personalhotspot.png" alt="" aria-hidden="true" style="filter: brightness(0) invert(86%) drop-shadow(0 0.08em 0.18em rgba(0, 0, 0, 0.58))" />
<img class="sf-symbol" src="./images/menubar/person-circle.png" alt="" aria-hidden="true" style="filter: brightness(0) invert(86%) drop-shadow(0 0.08em 0.18em rgba(0, 0, 0, 0.58))" />
<span class="time" id="menu-time">Sun Jun 28&nbsp;&nbsp;9:41 AM</span>
</div>
<aside class="codex-popover" aria-label="Open CodexBar usage panel" data-i18n-aria-label="mockup.usagePanelAria">
<header class="popover-top">
<p><strong>Codex</strong><span data-i18n="mockup.updatedJustNow">Updated just now</span></p>
<p class="right"><strong>user@example.com</strong><span data-i18n="mockup.plus">Plus</span></p>
</header>
<section class="popover-section">
<h3 data-i18n="mockup.session">Session</h3>
<div class="usage-meter" style="--fill: 60%; --danger: 58%; --meter-delay: 420ms;"><i style="--tick: 20%;"></i><i style="--tick: 48%;"></i></div>
<p class="usage-row">
<span><strong data-i18n="mockup.left60">60% left</strong><strong data-i18n="mockup.reserve2">2% in reserve</strong></span>
<span class="right"><strong data-i18n="mockup.resets2h53m">Resets in 2h 53m</strong><strong data-i18n="mockup.lastsUntilReset">Lasts until reset</strong></span>
</p>
</section>
<section class="popover-section">
<h3 data-i18n="mockup.weekly">Weekly</h3>
<div class="usage-meter" style="--fill: 73%; --danger: 73%; --meter-delay: 520ms;"><i style="--tick: 20%;"></i><i style="--tick: 48%;"></i></div>
<p class="usage-row">
<span><strong data-i18n="mockup.left73">73% left</strong><strong data-i18n="mockup.onPace">On pace</strong></span>
<span class="right"><strong data-i18n="mockup.resets5d4h">Resets in 5d 4h</strong><strong data-i18n="mockup.runsOut4d21h">Runs out in 4d 21h</strong></span>
</p>
</section>
<section class="reset-credit">
<p><strong data-i18n="mockup.limitResetCredits">Limit Reset Credits</strong><span data-i18n="mockup.nextExpires28d2h">Next expires in 28d 2h</span></p>
<p><strong data-i18n="mockup.manualResetAvailable">1 reset available</strong></p>
</section>
<section class="number-grid" aria-label="Recent Codex usage" data-i18n-aria-label="mockup.recentUsageAria">
<p class="popover-number"><span data-i18n="mockup.today">Today</span><strong>$51.56</strong></p>
<p class="popover-number"><span data-i18n="mockup.last31DaysCost">Last 31 days Cost</span><strong>$5,251.23</strong></p>
<p class="popover-number"><span data-i18n="mockup.last31DaysTokens">Last 31 days tokens</span><strong>5.9B</strong></p>
<p class="popover-number"><span data-i18n="mockup.latestTokens">Latest tokens</span><strong>85M</strong></p>
</section>
<figure class="usage-chart" aria-label="31 day usage chart" data-i18n-aria-label="mockup.usageChartAria">
<i style="--h: 52%;"></i><i style="--h: 55%;"></i><i style="--h: 38%;"></i><i style="--h: 60%;"></i><i style="--h: 34%;"></i><i style="--h: 73%;"></i><i style="--h: 33%;"></i><i style="--h: 10%;"></i><i style="--h: 28%;"></i><i style="--h: 47%;"></i><i style="--h: 18%;"></i><i style="--h: 5%;"></i><i style="--h: 20%;"></i><i style="--h: 18%;"></i><i style="--h: 4%;"></i><i style="--h: 9%;"></i><i style="--h: 32%;"></i><i style="--h: 40%;"></i><i style="--h: 28%;"></i><i style="--h: 38%;"></i><i style="--h: 45%;"></i><i style="--h: 31%;"></i><i style="--h: 39%;"></i><i style="--h: 42%;"></i><i style="--h: 34%;"></i><i style="--h: 8%;"></i><i style="--h: 6%;"></i><i style="--h: 7%;"></i><i style="--h: 17%;"></i><i style="--h: 38%;"></i><i style="--h: 4%;"></i>
</figure>
<p class="popover-note"><strong data-i18n="mockup.topModel">Top model: Example-1</strong><br /><span data-i18n="mockup.estimatedLogs">Estimated from local Codex logs for the selected acco...</span></p>
<section class="popover-foot">
<strong data-i18n="mockup.credits">Credits</strong>
<div class="credit-meter"></div>
<p class="credit-row"><strong data-i18n="mockup.zeroLeft">0 left</strong><span>1K tokens</span></p>
<p class="buy-row">
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="8" cy="8" r="6.2" stroke="currentColor" stroke-width="1.8" /><path d="M8 5v6M5 8h6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /></svg>
<span data-i18n="mockup.buyCredits">Buy credits...</span>
</p>
</section>
</aside>
</div>
</div>
</div>
</div>
<div class="hero-content px-5 sm:px-8 lg:px-12">
<div class="section-inner">
<div class="hero-copy">
<div class="reveal flex items-center justify-center gap-2.5 text-[15px] tracking-[-0.025em] text-zinc-400 sm:justify-start sm:text-lg">
<span data-i18n="hero.badgeStart">AI usage limits</span>
<img src="./icon.png?v=2" alt="" class="h-6 w-6 rounded-md shadow-[0_0_1.5rem_rgba(14,165,233,0.24)]" />
<span data-i18n="hero.badgeEnd">for macOS</span>
</div>
<h1 class="reveal reveal-2 mt-5 text-[clamp(3rem,5.7vw,60px)] font-semibold leading-[0.98] tracking-[-0.06em] text-zinc-100">
<span class="hero-title-line" data-i18n="hero.prefix">Every AI coding limit,</span>
<span class="hero-title-line" data-i18n="hero.accent">in your menu bar.</span>
</h1>
<p class="reveal reveal-3 mt-6 max-w-[38rem] text-balance text-[clamp(1rem,1.25vw,1.28rem)] leading-[1.42] tracking-[-0.035em] text-zinc-400">
<span data-i18n="hero.descriptionShort">Track usage windows, credit balances, and reset countdowns across the AI tools you actually pay for.</span>
</p>
<div class="hero-cta-row reveal reveal-4 mt-8 sm:mt-10">
<a
href="https://github.com/steipete/CodexBar/releases/latest"
class="download-button hero-download button-press inline-flex h-11 items-center gap-2 rounded-full px-5 text-sm font-medium"
>
<span class="grid h-4 w-4 place-items-center" aria-hidden="true">
<img src="./images/apple.svg" alt="" class="apple-icon" />
</span>
<span data-i18n="hero.download">Download for macOS</span>
</a>
<button
id="brew-copy"
type="button"
class="button-press brew-pill inline-flex h-11 max-w-full items-center gap-3 rounded-full px-4 font-mono text-[13px] text-zinc-100 hover:bg-white/10"
aria-label="Copy Homebrew install command" data-i18n-aria-label="clipboard.copyBrew"
>
<span class="text-zinc-400" aria-hidden="true">$</span>
<span data-brew-label class="truncate text-zinc-100">brew install --cask steipete/tap/codexbar</span>
<span data-brew-icon class="grid h-5 w-5 shrink-0 place-items-center text-zinc-400" aria-hidden="true">
<svg data-icon="copy" class="h-3.5 w-3.5" viewBox="0 0 18 18" fill="none">
<rect x="6.25" y="3.25" width="8.5" height="8.5" rx="1.4" stroke="currentColor" stroke-width="1.5" />
<path d="M11.75 14.75H4.65c-.77 0-1.4-.63-1.4-1.4v-7.1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
<svg data-icon="check" class="h-3.5 w-3.5" viewBox="0 0 18 18" fill="none">
<path d="M4.5 9.25 7.5 12.25 13.5 6.25" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
<span class="sr-only" data-brew-status aria-live="polite"></span>
</button>
</div>
<p class="reveal reveal-5 mt-5 text-sm tracking-[-0.02em] text-zinc-600">
<span data-i18n="hero.fineprintShort">Free &amp; open source · macOS 14+</span>
</p>
</div>
</div>
</div>
</section>
<section class="provider-section px-5 py-20 sm:px-8 lg:px-12 lg:py-28" data-provider-section>
<div class="section-inner">
<header class="relative z-10 grid gap-5 tablet:grid-cols-[1fr_0.86fr] tablet:items-center">
<h2 class="provider-reveal max-w-3xl text-balance text-[clamp(2.4rem,4.4vw,5rem)] font-semibold leading-[0.98] tracking-[-0.07em] text-zinc-100">
<span data-i18n-rich="providers.title">59 providers,{mobileBreak}one menu bar</span>
</h2>
<p class="provider-reveal max-w-2xl text-[clamp(1rem,1.35vw,1.28rem)] leading-[1.45] tracking-[-0.035em] text-zinc-400" style="--reveal-delay: 70ms">
<span data-i18n="providers.description">Popular providers become status items with their own usage windows, reset countdowns, charts, and provider menus.</span>
</p>
</header>
<ul class="relative z-10 mt-12 grid list-none gap-3 p-0 tablet:grid-cols-3 xl:grid-cols-5">
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/codex.md"><span class="provider-logo" data-icon="◎" style="--brand:#49A3B0;--icon:url('./logos/codex-dark.svg')"><img src="./logos/codex-dark.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Codex</strong><span data-i18n="auth.oauth">OAuth</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/claude.md"><span class="provider-logo" data-icon="✳" style="--brand:#CC7C5E;--icon:url('./logos/claude.svg')"><img src="./logos/claude.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Claude</strong><span data-i18n="auth.oauth">OAuth</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/cursor.md"><span class="provider-logo" data-icon="◆" style="--brand:#1A1A1A;--icon:url('./logos/cursor.svg')"><img src="./logos/cursor.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Cursor</strong><span data-i18n="auth.cookies">Cookies</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/opencode.md"><span class="provider-logo" data-icon="OC" style="--brand:#3B82F6;--icon:url('./logos/opencode-dark.svg')"><img src="./logos/opencode-dark.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>OpenCode</strong><span data-i18n="auth.cookiesGo">Cookies + Go</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/alibaba-coding-plan.md"><span class="provider-logo" data-icon="a" style="--brand:#FF6A00;--icon:url('./logos/alibaba.svg')"><img src="./logos/alibaba.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Alibaba</strong><span data-i18n="auth.cookies">Cookies</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/alibaba-token-plan.md"><span class="provider-logo" style="--brand:#FF6A00;--icon:url('./logos/alibaba.svg')"><img src="./logos/alibaba.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Alibaba Token</strong><span data-i18n="auth.cookies">Cookies</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/gemini.md"><span class="provider-logo" data-icon="◇" style="--brand:#AB87EA;--icon:url('./logos/gemini.svg')"><img src="./logos/gemini.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Gemini</strong><span data-i18n="auth.oauth">OAuth</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/antigravity.md"><span class="provider-logo" data-icon="A" style="--brand:#60BA7E;--icon:url('./logos/antigravity.svg')"><img src="./logos/antigravity.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Antigravity</strong><span data-i18n="auth.local">Local</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/factory.md"><span class="provider-logo" data-icon="✺" style="--brand:#FF6B35;--icon:url('./logos/droid-dark.svg')"><img src="./logos/droid-dark.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Droid</strong><span data-i18n="auth.cookies">Cookies</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/copilot.md"><span class="provider-logo" data-icon="⌘" style="--brand:#A855F7;--icon:url('./logos/copilot.svg')"><img src="./logos/copilot.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Copilot</strong><span data-i18n="auth.deviceFlow">Device flow</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/devin.md"><span class="provider-logo" style="--brand:#0F172A;--icon:url('./logos/devin.svg')"><img src="./logos/devin.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Devin</strong><span data-i18n="auth.bearerLocal">Bearer / Local</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/zai.md"><span class="provider-logo" data-icon="Z" style="--brand:#E85A6A;--icon:url('./logos/zai-dark.svg')"><img src="./logos/zai-dark.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>z.ai</strong><span data-i18n="auth.apiKey">API key</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/minimax.md"><span class="provider-logo" data-icon="≋" style="--brand:#FE6060;--icon:url('./logos/minimax.svg')"><img src="./logos/minimax.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>MiniMax</strong><span data-i18n="auth.apiKey">API key</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/kimi.md"><span class="provider-logo" data-icon="K" style="--brand:#FE603C;--icon:url('./logos/kimi.svg')"><img src="./logos/kimi.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Kimi</strong><span data-i18n="auth.token">Token</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/kimi-k2.md"><span class="provider-logo" data-icon="K2" style="--brand:#4C00FF;--icon:url('./logos/kimi-k2.svg')"><img src="./logos/kimi-k2.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Kimi K2</strong><span data-i18n="auth.legacyApi">Legacy API</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/kilo.md"><span class="provider-logo" data-icon="kg" style="--brand:#f8f676;--icon:url('./logos/kilo.svg')"><img src="./logos/kilo.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Kilo</strong><span data-i18n="auth.apiKey">API key</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/kiro.md"><span class="provider-logo" data-icon="•" style="--brand:#FF9900;--icon:url('./logos/kiro-dark.svg')"><img src="./logos/kiro-dark.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Kiro</strong><span data-i18n="auth.cli">CLI</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/vertexai.md"><span class="provider-logo" data-icon="VA" style="--brand:#4285F4;--icon:url('./logos/vertex-ai.svg')"><img src="./logos/vertex-ai.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Vertex AI</strong><span data-i18n="auth.gcloud">gcloud</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/augment.md"><span class="provider-logo" data-icon="{ }" style="--brand:#6366F1;--icon:url('./logos/augment.svg')"><img src="./logos/augment.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Augment</strong><span data-i18n="auth.cli">CLI</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/amp.md"><span class="provider-logo" data-icon="amp" style="--brand:#DC2626;--icon:url('./logos/amp.svg')"><img src="./logos/amp.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Amp</strong><span data-i18n="auth.cookies">Cookies</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/ollama.md"><span class="provider-logo" data-icon="ll" style="--brand:#888888;--icon:url('./logos/ollama.svg')"><img src="./logos/ollama.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Ollama</strong><span data-i18n="auth.apiKeyCookies">API key + Cookies</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/synthetic.md"><span class="provider-logo" data-icon="✽" style="--brand:#141414;--icon:url('./logos/synthetic-dark.svg')"><img src="./logos/synthetic-dark.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Synthetic</strong><span data-i18n="auth.apiKey">API key</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/jetbrains.md"><span class="provider-logo" data-icon="JB" style="--brand:#FF3399;--icon:url('./logos/jetbrains-ai.svg')"><img src="./logos/jetbrains-ai.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>JetBrains AI</strong><span data-i18n="auth.local">Local</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/warp.md"><span class="provider-logo" data-icon="W" style="--brand:#7B68EE;--icon:url('./logos/warp.svg')"><img src="./logos/warp.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Warp</strong><span data-i18n="auth.apiKey">API key</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/elevenlabs.md"><span class="provider-logo" style="--brand:#EAEAE6;--icon:url('./logos/elevenlabs.svg')"><img src="./logos/elevenlabs.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>ElevenLabs</strong><span data-i18n="auth.apiKey">API key</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/openrouter.md"><span class="provider-logo" data-icon="OR" style="--brand:#6467F2;--icon:url('./logos/openrouter.svg')"><img src="./logos/openrouter.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>OpenRouter</strong><span data-i18n="auth.apiKey">API key</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/litellm.md"><span class="provider-logo" style="--brand:#4C89F0;--icon:url('./logos/litellm.svg')"><img src="./logos/litellm.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>LiteLLM</strong><span data-i18n="auth.virtualKey">Virtual key</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/providers.md#fetch-strategies-current"><span class="provider-logo" data-icon="∞" style="--brand:#20B2AA;--icon:url('./logos/perplexity.svg')"><img src="./logos/perplexity.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Perplexity</strong><span data-i18n="auth.cookies">Cookies</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/abacus.md"><span class="provider-logo" style="--brand:#38BDF8;--icon:url('./logos/abacus-ai-dark.svg')"><img src="./logos/abacus-ai-dark.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Abacus AI</strong><span data-i18n="auth.cookies">Cookies</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/providers.md#fetch-strategies-current"><span class="provider-logo" style="--brand:#FF500F;--icon:url('./logos/mistral.svg')"><img src="./logos/mistral.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Mistral</strong><span data-i18n="auth.cookies">Cookies</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/deepseek.md"><span class="provider-logo" style="--brand:#527DF0;--icon:url('./logos/deepseek.svg')"><img src="./logos/deepseek.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>DeepSeek</strong><span data-i18n="auth.apiKey">API key</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/t3chat.md"><span class="provider-logo" style="--brand:#2563EB;--icon:url('./logos/t3chat.svg')"><img src="./logos/t3chat.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>T3 Chat</strong><span data-i18n="auth.cookies">Cookies</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/codebuff.md"><span class="provider-logo" style="--brand:#44C800;--icon:url('./logos/codebuff-dark.svg')"><img src="./logos/codebuff-dark.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Codebuff</strong><span data-i18n="auth.apiKey">API key</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/poe.md"><span class="provider-logo" style="--brand:#5D5CDE;--icon:url('./logos/poe.svg')"><img src="./logos/poe.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Poe</strong><span data-i18n="auth.apiKey">API key</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/chutes.md"><span class="provider-logo" style="--brand:#3184FF;--icon:url('./logos/chutes.svg')"><img src="./logos/chutes.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Chutes</strong><span data-i18n="auth.apiKey">API key</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/zed.md"><span class="provider-logo" style="--brand:#084EFF;--icon:url('./logos/zed.svg')"><img src="./logos/zed.svg" alt="" loading="lazy" onerror="this.remove()" /></span><p><strong>Zed</strong><span data-i18n="auth.local">Local</span></p></a></li>
<li class="provider-card"><a class="provider-card-link" href="https://github.com/steipete/CodexBar/blob/main/docs/provider.md"><span class="provider-logo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 .83.18 2 2 0 0 0 .83-.18l8.58-3.9a1 1 0 0 0 0-1.831z"/><path d="M16 17h6"/><path d="M19 14v6"/><path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 .825.178"/><path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l2.116-.962"/></svg></span><p><strong data-i18n="providers.yourProvider">Your provider</strong><span class="provider-authoring"><span data-i18n="providers.authoringGuide">Authoring guide</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 7h10v10"/><path d="M7 17 17 7"/></svg></span></p></a></li>
</ul>
</div>
</section>
<section class="features-section px-5 py-20 sm:px-8 lg:px-12 lg:py-28" data-scroll-section>
<div class="section-inner">
<h2 class="features-heading provider-reveal text-balance" data-i18n="features.title">Limits, spend, and status</h2>
<div class="features-grid relative z-10">
<article class="feature-card provider-reveal" style="--reveal-delay: 80ms">
<div class="feature-card-copy">
<h3 data-i18n="features.resetsTitle">Resets you can plan around</h3>
<p data-i18n="features.resetsBody">Per-provider session, weekly, and monthly windows with countdowns to the next reset — no more guessing whether to start a long task.</p>
</div>
<div class="feature-card-visual" aria-hidden="true">
<img src="./features/feature-resets.webp" alt="" width="1620" height="1020" loading="lazy" />
<div class="feature-card-overlay is-usage-menu">
<div class="feature-usage-panel">
<header class="feature-usage-header">
<p><strong>Codex</strong><span data-i18n="mockup.updatedJustNow">Updated just now</span></p>
<p class="feature-usage-account"><strong>user@example.com</strong><span data-i18n="mockup.plus">Plus</span></p>
</header>
<section class="feature-usage-block">
<h4 class="feature-usage-title" data-i18n="mockup.session">Session</h4>
<div class="feature-usage-track" aria-hidden="true">
<div class="feature-usage-fill" style="--amount: 100%">
<i style="--flex: 89"></i>
<i class="is-reserve" style="--flex: 5"></i>
<i class="is-spent" style="--flex: 6"></i>
</div>
</div>
<div class="feature-usage-meta">
<div>
<p><strong data-i18n="mockup.left89">89% left</strong><span class="is-subtle" data-i18n="mockup.reserve5">5% in reserve</span></p>
</div>
<div>
<p><strong data-i18n="mockup.resets4h12m">Resets in 4h 12m</strong><span class="is-subtle" data-i18n="mockup.lastsUntilReset">Lasts until reset</span></p>
</div>
</div>
</section>
<section class="feature-usage-block">
<h4 class="feature-usage-title" data-i18n="mockup.weekly">Weekly</h4>
<div class="feature-usage-track" aria-hidden="true">
<div class="feature-usage-fill" style="--amount: 100%">
<i style="--flex: 98"></i>
<i class="is-spent" style="--flex: 2"></i>
</div>
</div>
<div class="feature-usage-meta">
<div>
<p><strong data-i18n="mockup.left98">98% left</strong></p>
</div>
<div>
<p><strong data-i18n="mockup.resets6d23h">Resets in 6d 23h</strong></p>
</div>
</div>
</section>
</div>
</div>
</div>
</article>
<article class="feature-card provider-reveal" style="--reveal-delay: 140ms">
<div class="feature-card-copy">
<h3 data-i18n="features.spendTitle">Credits, spend, and cost scans</h3>
<p data-i18n="features.spendBody">Credit balances and monthly spend where the provider exposes them, plus configurable local cost scans for Codex and Claude.</p>
</div>
<div class="feature-card-visual" aria-hidden="true">
<img src="./features/feature-credits.webp" alt="" width="1620" height="1020" loading="lazy" />
<div class="feature-card-overlay">
<div class="feature-cost-panel">
<div class="feature-credit-head">
<p><strong data-i18n="mockup.limitResetCredits">Limit Reset Credits</strong><span data-i18n="mockup.nextExpires27d21h">Next expires in 27d 21h</span></p>
<p><strong data-i18n="mockup.manualResetAvailable">1 reset available</strong></p>
</div>
<dl class="feature-cost-stats">
<div>
<dt data-i18n="mockup.today">Today</dt>
<dd>$86.62</dd>
</div>
<div>
<dt data-i18n="mockup.last31DaysCost">Last 31 days Cost</dt>
<dd>$5,286.29</dd>
</div>
<div>
<dt data-i18n="mockup.last31DaysTokens">Last 31 days tokens</dt>
<dd>5.9B</dd>
</div>
<div>
<dt data-i18n="mockup.latestTokens">Latest tokens</dt>
<dd>119M</dd>
</div>
</dl>
<div class="feature-cost-chart" aria-hidden="true">
<i style="--h: 72%"></i><i style="--h: 77%"></i><i style="--h: 53%"></i><i style="--h: 81%"></i><i style="--h: 46%"></i><i style="--h: 96%"></i><i style="--h: 47%"></i><i style="--h: 10%"></i><i style="--h: 29%"></i><i style="--h: 64%"></i><i style="--h: 21%"></i><i style="--h: 5%"></i><i style="--h: 23%"></i><i style="--h: 22%"></i><i style="--h: 4%"></i><i style="--h: 10%"></i><i style="--h: 37%"></i><i style="--h: 45%"></i><i style="--h: 33%"></i><i style="--h: 50%"></i><i style="--h: 58%"></i><i style="--h: 35%"></i><i style="--h: 48%"></i><i style="--h: 55%"></i><i style="--h: 40%"></i><i style="--h: 8%"></i><i style="--h: 7%"></i><i style="--h: 8%"></i><i style="--h: 20%"></i><i style="--h: 54%"></i><i style="--h: 22%"></i>
</div>
</div>
</div>
</div>
</article>
<article class="feature-card provider-reveal" style="--reveal-delay: 200ms">
<div class="feature-card-copy">
<h3 data-i18n="features.statusTitle">Status &amp; incidents</h3>
<p data-i18n="features.statusBody">Provider status polling surfaces incident badges in the menu and an indicator overlay on the bar icon.</p>
</div>
<div class="feature-card-visual" aria-hidden="true">
<img src="./features/feature-status.webp" alt="" width="1620" height="1020" loading="lazy" />
<div class="feature-card-overlay is-status-menu">
<div class="feature-status-panel">
<p class="feature-status-summary"><span data-i18n="mockup.partialDegradation">Partial degradation</span> <span data-i18n="mockup.twoIncidents">2 incidents</span></p>
<ul class="feature-status-list" aria-hidden="true">
<li class="is-critical">
<span class="feature-status-dot"></span>
<span class="feature-status-name">Codex API</span>
<span class="feature-status-label" data-i18n="mockup.majorOutage">Major outage</span>
</li>
<li class="is-minor">
<span class="feature-status-dot"></span>
<span class="feature-status-name">Chat Completions</span>
<span class="feature-status-label" data-i18n="mockup.degraded">Degraded</span>
</li>
<li>
<span class="feature-status-dot"></span>
<span class="feature-status-name">CLI</span>
<span class="feature-status-label" data-i18n="mockup.operational">Operational</span>
</li>
<li>
<span class="feature-status-dot"></span>
<span class="feature-status-name">Responses</span>
<span class="feature-status-label" data-i18n="mockup.operational">Operational</span>
</li>
</ul>
</div>
</div>
</div>
</article>
</div>
</div>
</section>
<section class="cli-section px-5 py-20 sm:px-8 lg:px-12 lg:py-28" data-scroll-section>
<div class="section-inner relative z-10">
<div class="split-layout">
<div class="split-visual-wrap provider-reveal" role="img" aria-label="CodexBar CLI output preview" data-i18n-aria-label="cli.outputPreviewAria">
<div class="cli-terminal">
<div class="cli-terminal-chrome">
<div class="cli-terminal-dots" aria-hidden="true">
<span></span><span></span><span></span>
</div>
<p class="cli-terminal-title">codexbar · terminal</p>
<div class="cli-terminal-chrome-spacer" aria-hidden="true"></div>
</div>
<div class="cli-terminal-body">
<section class="cli-provider-block">
<p class="cli-provider-label">== Codex (oauth) ==</p>
<div class="cli-row">
<span class="cli-row-label" data-i18n="mockup.session">Session</span>
<span class="cli-row-value" data-i18n="cli.left99">99% left</span>
<div class="cli-meter" aria-hidden="true"><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i></i></div>
<span class="cli-row-note" data-i18n="cli.resets5h">Resets in 5h</span>
</div>
<div class="cli-row">
<span class="cli-row-label" data-i18n="mockup.weekly">Weekly</span>
<span class="cli-row-value" data-i18n="cli.left100">100% left</span>
<div class="cli-meter" aria-hidden="true"><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i></div>
<span class="cli-row-note" data-i18n="cli.resets7d">Resets in 7d</span>
</div>
<div class="cli-row is-meta"><span class="cli-row-label" data-i18n="mockup.credits">Credits</span><span class="cli-row-value" data-i18n="mockup.zeroLeft">0 left</span></div>
<div class="cli-row is-meta"><span class="cli-row-label" data-i18n="mockup.limitResetCredits">Limit Reset Credits</span><span class="cli-row-value" data-i18n="cli.available1">1 available</span></div>
<div class="cli-row is-meta"><span class="cli-row-label" data-i18n="cli.nextResetCredit">Next reset credit</span><span class="cli-row-value" data-i18n="cli.expires27d22h">expires in 27d 22h</span></div>
<div class="cli-row is-meta"><span class="cli-row-label" data-i18n="cli.account">Account</span><span class="cli-row-value">user@example.com</span></div>
<div class="cli-row is-meta"><span class="cli-row-label" data-i18n="cli.plan">Plan</span><span class="cli-row-value">Plus</span></div>
</section>
<section class="cli-provider-block">
<p class="cli-provider-label">== elevenlabs ==</p>
<div class="cli-row">
<span class="cli-row-label" data-i18n="cli.total">Total</span>
<span class="cli-row-value" data-i18n="cli.left79">79% left</span>
<div class="cli-meter" aria-hidden="true"><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i></i><i></i><i></i></div>
<span class="cli-row-note" data-i18n="cli.resets23d18h">Resets in 23d 18h</span>
</div>
<div class="cli-row">
<span class="cli-row-label" data-i18n="cli.auto">Auto</span>
<span class="cli-row-value" data-i18n="cli.left74">74% left</span>
<div class="cli-meter" aria-hidden="true"><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i></i><i></i><i></i><i></i></div>
<span class="cli-row-note" data-i18n="cli.resets23d18h">Resets in 23d 18h</span>
</div>
<div class="cli-row">
<span class="cli-row-label">API</span>
<span class="cli-row-value" data-i18n="cli.left99">99% left</span>
<div class="cli-meter" aria-hidden="true"><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i class="is-filled"></i><i></i></div>
<span class="cli-row-note" data-i18n="cli.resets23d18h">Resets in 23d 18h</span>
</div>
<div class="cli-row is-meta"><span class="cli-row-label" data-i18n="cli.account">Account</span><span class="cli-row-value">user@example.com</span></div>
<div class="cli-row is-meta"><span class="cli-row-label" data-i18n="cli.plan">Plan</span><span class="cli-row-value">Elevenlabs Pro</span></div>
</section>
</div>
<div class="cli-terminal-foot">
<p><strong>&gt;</strong> codexbar<span class="cli-terminal-cursor" aria-hidden="true"></span></p>
</div>
</div>
</div>
<div class="split-copy">
<h2 class="split-copy-heading provider-reveal" style="--reveal-delay: 70ms">
<span class="section-title-line">CodexBar</span>
<span class="section-title-line" data-i18n="cli.titleLine2">on CLI</span>
</h2>
<p class="split-copy-lead provider-reveal" style="--reveal-delay: 110ms">
<span data-i18n-rich="cli.lead">A bundled {codexbar} binary with the same usage output as the menu bar — scripts, CI, and quick terminal checks.</span>
</p>
<p class="split-copy-note provider-reveal" style="--reveal-delay: 150ms">
<span data-i18n="cli.note">Standalone tarballs for macOS and Linux on every release.</span>
</p>
<p class="cli-footnote provider-reveal" style="--reveal-delay: 190ms">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path fill="currentColor" d="M12 2.5 4.5 6.5v5.8c0 4.55 3.08 8.02 7.5 9.2 4.42-1.18 7.5-4.65 7.5-9.2V6.5L12 2.5Z" opacity="0.22" />
<path d="M12 2.5 4.5 6.5v5.8c0 4.55 3.08 8.02 7.5 9.2 4.42-1.18 7.5-4.65 7.5-9.2V6.5L12 2.5Z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" />
<path d="m9.25 11.75 1.9 1.9 3.6-3.6" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span data-i18n="cli.privacy">CodexBar reuses existing provider sessions — no passwords stored on disk.</span>
</p>
</div>
</div>
</div>
</section>
<section class="widgets-section px-5 py-20 sm:px-8 lg:px-12 lg:py-28" data-scroll-section>
<div class="section-inner relative z-10">
<div class="split-layout split-layout--copy-left">
<div class="split-copy">
<h2 class="split-copy-heading provider-reveal">
<span class="section-title-line">CodexBar</span>
<span class="section-title-line" data-i18n="widgets.titleLine2">widgets</span>
</h2>
<p class="split-copy-lead provider-reveal" style="--reveal-delay: 70ms">
<span data-i18n="widgets.lead">WidgetKit widgets for supported providers — usage windows, reset countdowns, and spend where available. Drop them on your Home Screen and desktop.</span>
</p>
<p class="split-copy-note provider-reveal" style="--reveal-delay: 120ms">
<span data-i18n="widgets.note">Small, medium, and large sizes. Switch providers without leaving the widget.</span>
</p>
</div>
<div class="split-visual-wrap provider-reveal" style="--reveal-delay: 80ms">
<div class="mac-widget-sheet" role="img" aria-label="macOS widget gallery preview" data-i18n-aria-label="widgets.galleryAria">
<div class="mac-widget-sheet__body">
<aside class="mac-widget-sidebar" aria-label="Widget categories" data-i18n-aria-label="widgets.categoriesAria">
<div class="mac-widget-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.2-3.2" />
</svg>
<span>codexb</span>
</div>
<ul class="mac-widget-nav">
<li>
<span class="mac-widget-nav-icon" aria-hidden="true">
<svg viewBox="0 0 16 16" fill="currentColor" width="10" height="10">
<rect x="1" y="1" width="6" height="6" rx="1.2" />
<rect x="9" y="1" width="6" height="6" rx="1.2" />
<rect x="1" y="9" width="6" height="6" rx="1.2" />
<rect x="9" y="9" width="6" height="6" rx="1.2" />
</svg>
</span>
<span data-i18n="widgets.allWidgets">All Widgets</span>
</li>
<li class="is-active">
<span class="mac-widget-nav-icon is-app" aria-hidden="true">
<img src="./icon.png?v=2" alt="" loading="lazy" />
</span>
CodexBar
</li>
</ul>
</aside>
<div class="mac-widget-main">
<p class="mac-widget-main__title">CodexBar</p>
<div class="mac-widget-gallery-wrap">
<div class="mac-widget-gallery">
<div class="mac-widget-row">
<article class="widget-tile is-small">
<div class="widget-preview">
<img src="./widgets/03-codexbar-metric-compact.png" alt="" width="204" height="204" loading="lazy" />
</div>
<h4>CodexBar Metric</h4>
<p data-i18n="widgets.metricCaption">Compact widget for credits or cost.</p>
</article>
<article class="widget-tile is-medium">
<div class="widget-preview">
<img src="./widgets/01-codexbar-history-small.png" alt="" width="428" height="204" loading="lazy" />
</div>
<h4>CodexBar History</h4>
<p data-i18n="widgets.historyCaption">Usage history chart with recent totals.</p>
</article>
<article class="widget-tile is-large">
<div class="widget-preview">
<img src="./widgets/02-codexbar-history-large.png" alt="" width="388" height="370" loading="lazy" />
</div>
<h4>CodexBar History</h4>
<p data-i18n="widgets.historyCaption">Usage history chart with recent totals.</p>
</article>
</div>
<div class="mac-widget-row">
<article class="widget-tile is-small">
<div class="widget-preview">
<img src="./widgets/06-codexbar-usage-compact.png" alt="" width="204" height="204" loading="lazy" />
</div>
<h4>CodexBar Usage</h4>
<p data-i18n="widgets.usageCaption">Session and weekly usage with credits and costs.</p>
</article>
<article class="widget-tile is-medium">
<div class="widget-preview">
<img src="./widgets/07-codexbar-usage-wide.png" alt="" width="428" height="204" loading="lazy" />
</div>
<h4>CodexBar Usage</h4>
<p data-i18n="widgets.usageCaption">Session and weekly usage with credits and costs.</p>
</article>
<article class="widget-tile is-large">
<div class="widget-preview">
<img src="./widgets/08-codexbar-usage-large.png" alt="" width="388" height="388" loading="lazy" />
</div>
<h4>CodexBar Usage</h4>
<p data-i18n="widgets.usageCaption">Session and weekly usage with credits and costs.</p>
</article>
</div>
<div class="mac-widget-row">
<article class="widget-tile is-small">
<div class="widget-preview">
<img src="./widgets/09-codexbar-switcher-compact.png" alt="" width="204" height="204" loading="lazy" />
</div>
<h4>CodexBar Switcher</h4>
<p data-i18n="widgets.switcherCaption">Usage widget with a provider switcher.</p>
</article>
<article class="widget-tile is-medium">
<div class="widget-preview">
<img src="./widgets/10-codexbar-switcher-wide.png" alt="" width="428" height="204" loading="lazy" />
</div>
<h4>CodexBar Switcher</h4>
<p data-i18n="widgets.switcherCaption">Usage widget with a provider switcher.</p>
</article>
<article class="widget-tile is-large">
<div class="widget-preview">
<img src="./widgets/11-codexbar-switcher-large.png" alt="" width="388" height="388" loading="lazy" />
</div>
<h4>CodexBar Switcher</h4>
<p data-i18n="widgets.switcherCaption">Usage widget with a provider switcher.</p>
</article>
</div>
<div class="mac-widget-row">
<article class="widget-tile is-medium">
<div class="widget-preview">
<img src="./widgets/04-codexbar-burndown.png" alt="" width="427" height="203" loading="lazy" />
</div>
<h4>CodexBar Burn Down</h4>
<p data-i18n="widgets.burndownCaption">Remaining budget compared with an ideal steady burn rate.</p>
</article>
<article class="widget-tile is-medium">
<div class="widget-preview">
<img src="./widgets/05-codexbar-burndown-combined.png" alt="" width="427" height="203" loading="lazy" />
</div>
<h4>CodexBar Burn Down (Combined)</h4>
<p data-i18n="widgets.burndownCombinedCaption">Session and weekly burn-down charts in one tile.</p>
</article>
</div>
</div>
</div>
</div>
</div>
<div class="mac-widget-sheet__foot">
<p data-i18n="widgets.dragToDesktop">Drag a widget to the desktop…</p>
<span class="mac-widget-ok">OK</span>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="technical-section px-5 py-16 sm:px-8 lg:px-12 lg:py-20" data-scroll-section>
<div class="section-inner">
<p class="section-eyebrow provider-reveal" data-i18n="detail.more">More</p>
<div class="technical-grid relative z-10">
<article class="technical-block provider-reveal" style="--reveal-delay: 60ms">
<h3 data-i18n="detail.installTitle">Install &amp; updates</h3>
<p>
<span data-i18n-rich="detail.installBody">Universal app via {releases} with Sparkle auto-updates, or via {cask} (updates via {upgrade}).</span>
</p>
</article>
<article class="technical-block provider-reveal" style="--reveal-delay: 110ms">
<h3 data-i18n="detail.cliTitle">CLI</h3>
<p>
<span data-i18n-rich="detail.cliBody">A bundled {codexbar} binary plus standalone tarballs for macOS and Linux on each release. On Linux: {linuxCommand}.</span>
</p>
</article>
<article class="technical-block provider-reveal" style="--reveal-delay: 160ms">
<h3 data-i18n="detail.privacyTitle">Privacy</h3>
<p>
<span data-i18n-rich="detail.privacyBody">Reuses existing provider sessions — OAuth, device flow, API keys, browser cookies, local files — so no passwords are stored. Audit notes in {issue}.</span>
</p>
</article>
<article class="technical-block provider-reveal" style="--reveal-delay: 210ms">
<h3 data-i18n="detail.permissionsTitle">macOS permissions</h3>
<p>
<span data-i18n="detail.permissionsBody">Full Disk Access for Safari cookies, Keychain access for cookie decryption and OAuth flows, and Files &amp; Folders prompts when provider helpers read project directories.</span>
</p>
</article>
</div>
</div>
</section>
</main>
<footer class="site-footer px-5 sm:px-8 lg:px-12">
<span data-i18n="footer.builtBy">Built by</span>
<a href="https://github.com/steipete">Peter Steinberger</a>
<span aria-hidden="true"> · </span>
<a href="https://github.com/steipete/CodexBar/blob/main/CHANGELOG.md" data-i18n="footer.changelog">Changelog</a>
<span aria-hidden="true"> · </span>
<a href="https://github.com/steipete/CodexBar/blob/main/LICENSE">MIT</a>
<span aria-hidden="true"> · </span>
<a href="https://trimmy.app">trimmy.app</a>
</footer>
<script type="module" src="./site.js?v=4"></script>
</body>
</html>
@@ -0,0 +1,609 @@
# Spec: Scan-wide fork-family event provenance for Ultra overcounting (issue #2037)
- **Issue:** [steipete/CodexBar#2037](https://github.com/steipete/CodexBar/issues/2037) — Ultra / forked-session token overcounting
- **Status:** Proposed (architecture redesign; fixture-gated) — rev 8a
- **P0 local corpus:** `docs/issue-2037-p0-local-corpus-findings.md` (provisional Codex locks from `~/.codex` forks; Ultra golden still open)
- **Supersedes as the canonical fix:** file-local-only approaches for closing #2037, including claiming [#2066](https://github.com/steipete/CodexBar/pull/2066) as a full fix
- **Related:** [#2066](https://github.com/steipete/CodexBar/pull/2066) (Codex intra-file containment — interim / non-closing at best), [#2043](https://github.com/steipete/CodexBar/pull/2043) / [#2059](https://github.com/steipete/CodexBar/pull/2059) (Pi file-local reconciliation merged then reverted), #968, #1062, #1164 / `45b68c34`
- **Affected code (expected):** `Sources/CodexBarCore/Vendored/CostUsage/` (Codex first); Pi only if the sanitized corpus proves the same shape
## 0. Framing (read first)
After [#2059](https://github.com/steipete/CodexBar/pull/2059), the repository owner retained #2037 as the canonical tracker. A replacement must provide:
1. **Cross-file fork-family / stable event provenance** — copied ancestor events across related fork files must not be charged once per file.
2. **Proof against a sanitized real fork corpus** — exact tokens and nonlinear pricing, not synthetic-only confidence.
[#2043](https://github.com/steipete/CodexBar/pull/2043) showed three failure modes to avoid:
- copied ancestor rows across fork files still duplicated
- distinct events collapsed under a shared lineage key
- cumulative→delta conversion priced without full nonlinear context
This document is the **canonical redesign**. The sanitized corpus is **design input**, not only final QA (§10): the exact provenance key cannot be chosen responsibly until the logs answer which IDs survive copies. If logs contain no usable stable identity, an exact solution may be impossible and the product must ship an explicitly labeled conservative estimate (`accountingQuality = contained`, §6).
**Identity contract (summary):** use an ID as an event key only when the corpus proves **event-level cardinality**; request/message-scoped IDs that cover several snapshots need a copy-stable compound sequence or remain lineage hints. Otherwise use an exact pre-fork copied prefix under ancestry and a corpus-locked fork-boundary rule; never token-only dedupe; preserve post-fork distinct events; ambiguous or invalid graph states → no aggressive merge (§5). Fingerprint inputs must be copy-stable (§7); file-local ordinal is a tie-break only, not a fingerprint field unless the corpus proves it survives copying.
**P1 vs P2 boundary:**
| Phase | In scope | Out of scope |
|-------|----------|--------------|
| **P1** | Same-session union → cross-window family closure → normalize events (incl. priority metadata) → deterministic family/provisional graph → provenance ledger + ownership/baseline continuity → corpus-proven per-event accounting → per-event pricing → two-tier atomic family cache → quality fields/reasons to report/UI | Per-lineage cumulative baselines as primary accounting; closing #2037 when any affected golden needs containment |
| **P2** | Per-lineage baselines on unique events; demote watermark min-cap to ambiguous-only fallback | — |
**Closing #2037:** P1 may close the issue **only if** the sanitized corpus proves the P1 per-event accounting path **and** no affected golden family requires the containment fallback (`accountingQuality = contained`). `provenanceQuality = incomplete` alone does not block closure when a corpus-locked provisional-family rule produces `accountingQuality = primary` and matches the hand token/cost oracle over the available corpus. If any in-scope golden still needs containment (or event/lineage identity remains unproven), **P2 is required** before closing; P1 can still ship as a non-closing step with #2037 left open.
**Relationship to [#2066](https://github.com/steipete/CodexBar/pull/2066):** file-local watermark + min-cap is a **conservative fallback only** when lineage identity is genuinely ambiguous — never the primary method, never sufficient alone to close #2037.
## 1. Problem (two layers)
### 1.1 Inter-file — copied ancestor events (owner-canonical)
Fork children can embed or re-emit ancestor `token_count` history in separate JSONL files. Today each file is scanned largely independently:
- `forked_from_id` + inherited cumulative baseline (#1164) helps when the child only exposes high `total_token_usage`
- it does **not** recognize the same ancestor event copied into another session
- `CodexUsageRow` keeps only `turnID` and a session-local `eventIndex` (plus day/model/tokens). That is insufficient for cross-file provenance
Result: the same completion can be charged once per file that carries a copy.
### 1.2 Intra-file — interleaved cumulative lineages
Ultra-style sessions can interleave multiple monotonic `total_token_usage` sequences in one file. A single global baseline recounts the high/low gap (see `docs/issue-2037-ultra-fork-overcount-spec.md`). Real, but not sufficient alone for §0.
### 1.3 Non-goals (first redesign PR)
- Per-sub-agent breakdown UI
- Changing models.dev / priority rate tables
- Claiming OpenAI invoice parity
- Closing #2037 without corpus-locked keys and golden totals
## 2. Definitions
| Term | Meaning |
|------|---------|
| **Normalized raw event** | Lossless extraction of one log `token_count` (and related ids/metadata) before accounting (§4.2) |
| **Same-session union** | Build a provenance-safe union of active + archived views of the **same** `sessionId` before cross-session family work (§4.1) |
| **Fork family** | All sessions descending from the same root under `forked_from_id` ancestry; one reconciliation unit |
| **Provisional family** | Children sharing the same unavailable `forked_from_id`, grouped under a synthetic missing-parent node (§4.4) |
| **Fork boundary** | The single P0 corpus-locked rule that separates copied ancestor prefix from child-local work (§4.3.1) |
| **Copied prefix** | Ancestor events that appear in a descendant file before that childs fork boundary |
| **Event key** | Stable identity for recognizing the same source event across family files |
| **Lineage** | Real task/agent/turn cumulative sequence *after* family dedupe — only if the fixture shows a reliable id (P2 primary) |
| **Family ledger** | Accepted event keys for a family (count once across the entire family) |
| **Billing disposition** | Whether an observed event emits tokens/cost (`accepted`) or is a copied duplicate (`duplicate`) |
| **State disposition** | Whether an observed cumulative snapshot seeds or advances lineage state even when it is non-billable (§4.6) |
| **Billable owner** | The single family occurrence allowed to emit tokens/cost for one accepted event key (§4.5.1) |
| **Quality** | Independent provenance + accounting trust signals (§6); not a single overloaded enum |
## 3. Pipeline (explicit order)
Conceptual orthogonality of “same-session archive dedupe” vs “cross-session fork dedupe” is true; **the pipeline must still order them**.
```mermaid
flowchart TD
enum[Metadata index / family closure]
same[Union same-session active/archive views]
extract[Extract normalized raw events per canonical session]
graph[Build fork-family graph + validate graph]
prov[Resolve event provenance across the family]
lineage[Account: P1 passthrough / P2 per-lineage]
price[Price each unique event individually]
quality[Assign provenanceQuality + accountingQuality]
cache[Replace family contribution atomically]
window[Filter accepted events to requested report window]
report[Propagate quality fields into CostUsage report / UI]
enum --> same --> extract --> graph --> prov --> lineage --> price --> quality --> cache --> window --> report
```
**Ordering rules:**
1. **Metadata closure first.** Enumerate/index enough metadata to identify relevant family membership, including out-of-window ancestors (§4.3).
2. **Then same-session union.** Build a provenance-safe union of active/archive views so one logical session contributes one normalized event stream before it participates in a fork family. Preserve unique suffixes; do not build families from duplicate extractions of the same `sessionId`.
3. **Then cross-session families.** Provenance ledger operates on canonical logical sessions only.
4. **Provenance before accounting.** Dedupe copies before lineage baselines or containment fallback.
5. **Price after reconciliation.** Nonlinear-safe.
6. **Quality after pricing inputs are known** — set **independent** provenance and accounting quality fields (§6), then persist with the family contribution.
7. **Report window last.** Reconcile the relevant family closure before filtering accepted events to `scanSince` / `scanUntil` (§4.3).
### 3.1 Normative invariants
1. **Once per key:** one accepted family event key has exactly one billable owner.
2. **Owner priority:** real ancestor occurrence before provisional sibling representative; ownership migration never bills both.
3. **State continuity:** copied/non-billable observations may seed or advance raw cumulative state but never increase `countedTotals`.
4. **Remove monotonicity:** removing a family file never increases attributed tokens/cost.
5. **Restore stability:** restoring a parent never increases the contribution of already-known copied keys; total may rise only for newly discovered unique events.
6. **Scan equivalence:** warm incremental = cold cache = force rescan for tokens, rows, costs, graph, ownership, and quality reasons.
7. **Window after reconcile:** out-of-window ancestors may seed/dedupe, but accepted usage is bucketed only by original event time.
8. **Event-local dates:** pricing date and report day derive from the original event timestamp — never scan, cache, restoration, or fork processing time.
## 4. Design steps
### 4.1 Union same-session active/archive views
Before constructing cross-session fork families:
- Identify files that share a `sessionId` (active partial + archived rollout, etc.).
- Merge their normalized events into one **canonical logical-session stream** using a local ordered-view rule; do not wait for fork-family construction (same-session views have no ancestry edge).
- Prefer namespaced event keys when event-level cardinality is corpus-proven. Otherwise align copy-stable normalized events within this `sessionId` only, preserving order and occurrence multiplicity (ordered prefix/overlap matching, not an unordered fingerprint set).
- Preserve unique events from every view (for example an archived suffix missing from an active partial file) while suppressing copied rows.
- Two legitimate identical occurrences remain two events when their ordered positions/multiplicity show both are present; fingerprint equality alone must not collapse them.
- Remember every source path and digest for invalidation, but emit only the merged logical stream into the family graph.
- Conflicting parent metadata across views of the same `sessionId` is an invalid state (§4.4), not a reason to pick whichever file was enumerated first.
This is a **hard prerequisite step**, not an afterthought.
### 4.2 Extract normalized raw usage events
Each token event should retain at least:
| Field | Role |
|-------|------|
| Session ID | Owning canonical session |
| Parent session ID | `forked_from_id` when present |
| Fork timestamp | Family ordering + pre-fork copy window |
| Stable event / request / message ID | Candidate provenance key only after corpus proof of event-level cardinality; namespace by ID kind |
| Turn ID | Attribution / priority join / optional lineage hint — **never** sole cross-completion key |
| Original event timestamp | Ordering + fingerprint |
| Ordinal within file | **Tie-break only** when timestamps collide — **not** a fingerprint input unless the corpus proves ordinals survive copying (insertions shift them) |
| Model and pricing date | Nonlinear / dated pricing |
| Raw `last_token_usage` | Per-event signal (semantics TBD by corpus) |
| Raw `total_token_usage` | Cumulative validation / lineage-local fallback |
| Canonical fingerprint of the source event | Fallback identity when explicit IDs are absent (§7); must use **copy-stable** fields only |
| **Priority / turn metadata** | Enough to reproduce standard vs priority pricing (`logs_2` / turn priority flags, surcharge eligibility, etc.) |
**Priority metadata must survive normalization** so per-event pricing can split standard vs priority totals the way todays priority overlay expects. Dropping it and re-deriving only from aggregates is a #2043-class pricing footgun.
**Why:** todays `CodexUsageRow` is too lossy for cross-file provenance and for faithful per-event pricing.
### 4.3 Build a fork-family graph
```text
root session
├── child A
│ └── grandchild
└── child B
```
All sessions descending from the same root are **one reconciliation unit**. Process parent-before-child when the graph is a DAG.
**Family closure precedes date filtering.** Build or refresh a lightweight metadata index (session ID, parent ID, fork timestamp, source paths, coarse event date range) across all relevant roots/cache entries. For any session that can contribute to the requested report, load the transitive family members and the ancestor events required to establish provenance and cumulative baselines even when those files or events fall outside the report window. Reconcile first, assign accepted usage to each event's original date, then apply `scanSince` / `scanUntil`. An out-of-window parent must not become an artificial “missing parent” merely because the report window starts later.
#### 4.3.1 Corpus-locked fork boundary
The fork boundary is normative provenance input, not a heuristic chosen independently by each parser path. P0 must select and document one corpus-proven rule, for example:
- the child rollout/session metadata fork timestamp
- the final event in a proven shared copied prefix
- the first child-local event after that shared prefix
- another explicit field established by the sanitized corpus
The selected rule becomes part of the parser/reconciliation fingerprint. A parent event is prefix-eligible only when it satisfies that locked rule (for timestamp rules, normally `parentEventTimestamp <= boundary`). If the boundary is missing, contradictory, or cannot distinguish copied prefix from post-fork work, do not guess aggressively: record `ambiguousForkBoundary`, set `provenanceQuality = incomplete`, and use the corpus-defined conservative path (`accountingQuality = contained` when containment is required).
### 4.4 Invalid / ambiguous family states
Treat the following as **invalid or ambiguous** (no aggressive fingerprint merge; `provenanceQuality ≠ complete`, and often undercount-preferring):
| Condition | Handling |
|-----------|----------|
| **Cycle** in `forked_from_id` edges | Apply the normative timestamp-first forest algorithm below; reject cycle-closing edges; set `provenanceQuality = incomplete` |
| **Conflicting parent claims** | Two authoritative views/files for one logical `sessionId` claim different parents, or authoritative session metadata disagrees with another authoritative parent-edge source → accept no guessed parent edge; record `conflictingParents`; set `provenanceQuality = incomplete`. Non-authoritative inference must not create an edge. The cycle/forest algorithm runs only after each session has at most one accepted parent candidate |
| **Stable-ID collisions** | Same namespaced ID maps to multiple token events without a corpus-proven compound sequence, or maps to incompatible payloads → the ID is not event-unique; keep the events separate, set `provenanceQuality = incomplete`, and never silently overwrite or arbitrarily drop one |
| **Missing parent** | Group all children sharing the unresolved parent ID under one provisional family node; apply the deterministic policy below; set `provenanceQuality = incomplete` |
#### 4.4.1 Deterministic DAG / cycle rule
For each connected component, build the accepted forest from the complete currently known edge set — never from file enumeration order:
1. Parse fork timestamps. Valid timestamps sort before missing/invalid timestamps.
2. Sort edges by `(timestampValidity, forkTimestamp ascending, childSessionId, parentSessionId)`.
3. Add edges in that order. Reject an edge if adding it would create a cycle.
4. Mark every component with a rejected edge `provenanceQuality = incomplete` and record `cycleBroken` when quality reasons are persisted.
5. Recompute the whole connected component whenever an edge/member changes; a warm incremental scan and a full rescan must produce the same accepted forest.
This prioritizes the earliest physically reported fork relation while retaining session IDs as stable tie-breakers when timestamps are equal or unusable.
#### 4.4.2 Missing-parent provisional family
A missing parent ID is still relationship evidence. Create a synthetic node keyed by `missing:<parentSessionId>` and attach all children that reference it.
- Reconcile corpus-proven stable event keys shared by those siblings.
- When explicit IDs are unavailable, apply a sibling exact-prefix rule only if the sanitized fixture locks copy-stable equality and the events are pre-fork for every affected child.
- If the pre-fork streams remain ambiguous, choose one deterministic canonical pre-fork stream `(valid forkTimestamp first, earliest timestamp, sessionId)` as the billable representative; other sibling pre-fork streams are state-only. Set `accountingQuality = contained`.
- Post-fork events remain separate unless a proven event key establishes an actual copy.
- If a formerly present parent disappears, recompute the family under this provisional rule and atomically replace its old contribution.
- If the real parent later appears, dissolve `missing:<parentSessionId>`, rebuild the real family from all member views, migrate copied-key ownership to the real ancestor, and atomically replace the provisional and any old sibling-local contributions. Newly discovered unique parent events may add cost; already-known copied keys may not.
**File-removal monotonicity invariant:** removing a file from the available family corpus must never increase the family's attributed tokens or cost. It may stay equal (copied parent history remains represented once) or decrease (unique events disappeared), but must not rise because siblings began double-charging a copied prefix.
Accounting quality for provisional families is determined by the identity path, not merely by the parent's absence:
| Sibling-prefix evidence | Accounting result |
|-------------------------|-------------------|
| Corpus-proven shared event keys or corpus-proven exact-prefix equality | `accountingQuality = primary`; record `missingParent` + `provisionalFamily` |
| Deterministic representative selected because streams disagree or identity/boundary is ambiguous | `accountingQuality = contained`; record `missingParent` + `provisionalFamily` + `containmentFallback` (and the relevant ambiguity reason) |
Thus `provenanceQuality = incomplete` does not imply containment by itself.
### 4.5 Resolve event provenance across the family
For every normalized event:
1. **Prefer a namespaced stable event / request / message ID only when the corpus proves event-level cardinality** as well as copy survival. If one request/message ID covers several token snapshots, compound it with a copy-stable event sequence/type proven by the corpus or treat it only as a lineage hint.
2. **Otherwise**, recognize an **exact copied prefix** only when:
- the files have an ancestor relationship in the family graph, **and**
- the event occurs **before the fork boundary** of the descendant, **and**
- “exact” means corpus-defined equality: prefer byte-identical source lines when that holds; otherwise normalized equality of retained raw fields — pin from the fixture.
3. **Count a copied ancestor event once** across the entire family by assigning its billable owner under §4.5.1. The same proposed key with a different accounting payload is a collision, not a duplicate (§4.4).
4. **Never deduplicate merely because two events have the same token values.**
5. **Preserve identical-but-distinct events after the fork.**
**Tie-break:** `(timestamp, ordinal, session topological order)`.
**Forbidden:** token-only dedupe; file-local `eventIndex` as cross-file identity; shared Ultra/task id as the *only* key for many completions.
#### 4.5.1 Billable ownership (normative)
For each accepted family event key `K`, assign exactly one `billableOwner`:
1. **Real ancestor present and contains `K`:** the earliest real ancestor occurrence in parent-before-child order owns billing. Descendant occurrences are state-only duplicates.
2. **Real parent missing or does not contain `K`:** one deterministic provisional sibling occurrence owns billing (the canonical representative rule in §4.4.2). Other sibling occurrences are state-only duplicates.
3. **Partial parent:** apply the rule per key, not per whole prefix. Keys actually present in the parent are parent-owned; keys only visible in children use the provisional representative.
4. **Parent reappears:** recompute the family and migrate ownership for matching keys to the real ancestor in one cache generation. Never retain both the provisional and real owner.
5. **Conflicting payload for the same proposed key:** this is a collision, so no ownership merge occurs (§4.4 / §7).
Ownership changes where a row is sourced, not its semantic identity, original timestamp, token vector, or price. Therefore a copied key remains billed once across parent disappearance/reappearance transitions.
Billable ownership applies only to the contribution for `K`; it does not shortcut baseline propagation. If grandparent and parent both contain `K`, the grandparent owns billing, but the child still seeds state by walking its **actual parent chain** and applying each fork-boundary snapshot in order. Intervening unique parent events must advance the child baseline even when earlier copied keys are grandparent-owned.
Parent restoration may change `provenanceQuality` from incomplete to complete only after the full real family is recomputed and has no remaining missing edges, collisions, cycle breaks, or ambiguous fork boundaries. Corpus goldens prove that transition; runtime assigns it from recomputed state rather than attempting to “check a golden.”
### 4.6 Account per real lineage (P2 primary; P1 may defer)
Only on **unique** family events:
**P1:** May emit one accounting event per ledger-accepted normalized event using `last` / total policy locked by corpus study, without full multi-run lineage state — still **after** provenance. If intra-file interleaving remains ambiguous, apply containment fallback and set `accountingQuality = contained`.
P1 still requires an explicit reducer per canonical logical session (and therefore one per provisional sibling), never one family-global baseline:
```text
P1SessionAccountingState {
countedTotals
rawTotalsBaseline
inheritedTotalsRemaining
containmentTracker
}
```
Before unique child events are accounted, replay that session's copied-prefix occurrences through the state path with `billingDisposition = duplicate`: update/seed `rawTotalsBaseline`, consume inherited state where applicable, and leave `countedTotals` unchanged. Then process billable unique events. Intra-session interleaving may still latch that session's containment tracker and set `accountingQuality = contained`; it must not create or reuse a family-global watermark.
**P2:**
1. Use a stable task/agent/turn **lineage** id iff the fixture shows it identifies a cumulative sequence without collapsing independent requests.
2. Maintain a **separate cumulative baseline per lineage**.
3. If `last_token_usage` is proven per unique request, use it (optionally cross-checked against lineage-local total delta).
4. Use cumulative totals as validation and/or lineage-local fallback.
5. If lineage identity is genuinely ambiguous → **watermark + min-cap containment** as conservative fallback only (§0); set `accountingQuality = contained`.
6. Optional never-inflate ceiling may wrap the fallback path only.
#### 4.6.1 Billing suppression does not erase state
Provenance decides whether an event is billed; it does **not** decide whether the event is an observed cumulative-state transition:
- Ledger-accepted unique event: `billingDisposition = accepted`; it may seed/advance lineage state and emit its accounted delta.
- Copied ancestor event: `billingDisposition = duplicate`; it emits zero tokens/cost **but still seeds or advances the descendant's raw cumulative baseline**.
- At a resolved fork boundary, initialize each proven child lineage from the corresponding final parent cumulative snapshot at or before the fork.
- If lineage identity is not yet known, seed the P1/fallback reducer from the final filtered copied-prefix total (fork-inheritance adjusted), not zero.
- With a missing parent, use the provisional family's canonical pre-fork stream as state-only seed material for every sibling; ambiguous cases remain `accountingQuality = contained`.
Thus a parent ending at `100M`, followed by a child's first unique absolute total of `101M`, contributes `1M` — not `101M` — even though copied parent events were removed from billing.
Keep #1164 inherited total resolution as the baseline-seeding mechanism where applicable. Authoritative cross-file billing dedupe remains §4.5; state continuity remains mandatory after dedupe.
#### 4.6.2 Placement of #2066 containment
After family provenance lands, containment runs only inside one canonical session's **unique post-ledger stream**:
```text
family copies → provenance ledger → unique stream per canonical session
→ primary per-event/per-lineage accounting
→ session-local containment only if still ambiguous
```
Never apply one #2066 watermark across family members, and never let containment decide whether cross-file copies are duplicates. Provenance owns copy identity; containment is the final ambiguity guard for one already-deduplicated session stream.
### 4.7 Price only after reconciliation
Each unique event retains original model, pricing date, input, cached input, output, **and priority/turn metadata**. Price **individually** after deduplication so standard vs priority and other nonlinear tiers can be reproduced.
Both the pricing-catalog date and report-day bucket derive from the accepted event's **original timestamp**. Do not use scan time, cache-write time, fork time, parent restoration time, or the timestamp of a different copied occurrence.
Never subtract already-priced aggregates or price a collapsed lineage blob (#2043).
### 4.8 Change the cache boundary
Use a **two-tier derived cache** rather than embedding every normalized event in the existing monolithic JSON cache.
**Main manifest / family-result cache** (small, eagerly decoded):
- stable family ID (`rootSessionId` or provisional `missing:<parentSessionId>`)
- canonical logical-session members and all source path/file digests
- parser, normalized-event-schema, pricing, and reconciliation fingerprints
- accepted family contribution maps / cost nanos
- provenance/accounting quality fields and optional quality reasons
- references to member event sidecars
**Per-file or per-canonical-session event sidecars** (lazy, independently invalidatable):
- only normalized fields required for provenance, baseline continuity, and pricing
- no original JSON lines and no redundant final report aggregates
- versioned and atomically written
- loaded only for a dirty family or a forced rescan
“Lossless” here means lossless for the specified provenance/accounting inputs, not byte-for-byte preservation of source JSON. Source logs remain authoritative and every sidecar is disposable/rebuildable.
When one file changes:
1. Rebuild the same-session union if needed.
2. Reparse that files normalized events.
3. Determine its fork family.
4. Reconcile and reprice **that entire family**.
5. Replace the familys previous contribution **atomically**, including its quality fields (§6).
**Initially avoid clever incremental family reconciliation.** Recompute the affected family.
When a source disappears, changes `sessionId`/parent, or moves between families, use the manifest to identify both the old and new affected families. Recompute both and update their contribution records in one in-memory cache transaction before the main cache is atomically saved. Write sidecars via temporary file + rename so an interrupted refresh cannot expose a half-written event set.
#### 4.8.1 Crash-safe generation protocol
1. Read one committed manifest generation `N`; readers ignore unreferenced sidecars.
2. Build all changed family results and sidecars for generation `N+1` in memory / temporary paths.
3. Atomically rename completed sidecars into content-addressed or generation-qualified final paths.
4. Write the complete `N+1` manifest (family membership, contributions, rolled-up day totals, quality, sidecar digests) to a temporary file and atomically rename it last. **The manifest rename is the commit point.**
5. On failure before the manifest commit, generation `N` remains authoritative; partial `N+1` sidecars are orphans and cannot affect totals.
6. Garbage-collect sidecars not referenced by the committed manifest after a successful commit or on a later startup/refresh.
7. Validate referenced sidecar schema + digest before use; a missing/corrupt sidecar invalidates only its affected family and triggers source reparse.
This prevents a crash during reparenting from mixing old family A contributions with new family B contributions. A monotonically increasing generation number plus sidecar content digests is sufficient; a separate complex checksum journal is not required.
**Day-total atomicity:** rolled-up day/model/cost maps are derived exclusively from the family contributions in the newly committed manifest generation. Never patch global day totals in place while ownership moves from family A to B. Generation `N` contains all old owners/totals; generation `N+1` contains all new owners/totals; no reader may observe a mixed generation containing both A's old and B's new contribution for one key.
Do not mandate run-length encoding, pruning, compression, or a particular binary format before measuring the sanitized corpus. First record:
- normalized sidecar bytes versus source-log bytes
- cold main-cache decode time
- dirty-family sidecar load + recompute time
- full rebuild time
- peak resident memory
Lock acceptable budgets before implementation freeze. If needed, optimize with string interning, columnar arrays, timestamp/ordinal deltas, or compression while preserving every normative field and golden result.
Bump `CodexParserHash`, the normalized-event sidecar schema, and clear compatible producer keys when this ships.
## 5. Identity contract (normative)
| Prefer | Avoid |
|--------|--------|
| Namespaced ID proven one-to-one with an accounting event and copied into children | Request/message ID assumed event-unique without cardinality proof |
| Exact pre-fork copied prefix under ancestor relationship | Token-value-only dedupe |
| Separate post-fork events even if counts match | Shared task id as sole key |
| Ambiguous → keep separate + degrade quality | Silent overwrite on ID collision |
## 6. Quality fields → report / UI
### 6.1 Provenance completeness ≠ accounting containment
These are **independent**. A family can be graph-incomplete **and** still use containment fallback — a single enum loses that information.
Persist **two fields** (or an equivalent set of quality reasons):
```text
enum CodexFamilyProvenanceQuality {
case complete // resolvable DAG, no conflicting parents / unhandled cycles
case incomplete // missing parent, cycle break, conflicting parents, etc.
}
enum CodexFamilyAccountingQuality {
case primary // corpus-proven per-event / per-lineage path (no containment)
case contained // watermark min-cap / containment fallback used
}
```
Persist a `Set<CodexFamilyQualityReason>` alongside the two fields. At minimum support and assert in goldens:
```text
missingParent
provisionalFamily
cycleBroken
conflictingParents
stableIdCollision
fingerprintCollision
ambiguousForkBoundary
containmentFallback
```
The UI may collapse reasons into simpler copy, but cache/CLI/test output must preserve them so `incomplete + contained` is diagnosable rather than a black box.
**Roll-up when aggregating days:** worst provenance and worst accounting independently (`complete` < `incomplete`; `primary` < `contained`).
### 6.2 Propagation path (implementation target)
1. **Scanner / family reconcile** assigns both quality fields per family.
2. **Cache** persists both fields and mandatory quality reasons with the familys accepted events / cost nanos.
3. **`CostUsageDailyReport`** (and project breakdowns if present) expose rolled-up fields, e.g. `provenanceQuality` + `accountingQuality` (or derived `isEstimate` = either non-ideal).
4. **UI / menu:** distinct affordances when possible — e.g. “Incomplete fork history” vs “Estimated (conservative accounting)” — not a single vague “Estimated” that conflates the two.
5. **CLI** (`codexbar cost` JSON): include both fields and quality reasons so agents/scripts do not treat estimates as exact.
Exact UX copy TBD; the **data path for both dimensions** is required in P1 so quality is not documentation-only.
## 7. Canonical fingerprint (when IDs are absent)
Used only for the pre-fork exact-copy path or as a secondary assist — not global token dedupe.
**Copy-stable inputs only** (finalize from corpus), e.g.:
- event timestamp (if stable across copies)
- model
- raw `last_token_usage` when present
- raw `total_token_usage` snapshot when present
- any weak ids (`turn_id`, etc.) only as secondary components
**Do not include file-local ordinal** in the fingerprint unless the corpus proves ordinals survive copying. Surrounding insertions in a child log can shift ordinals even when the event is the same ancestor row. Ordinal remains useful as a **within-file or same-stream tie-break** after candidates are matched by copy-stable fields — not as identity.
**Fingerprint collision policy:**
- same high-confidence fingerprint + identical normalized accounting/provenance payload → copied duplicate candidate
- same high-confidence fingerprint + incompatible payload → keep events separate, record `fingerprintCollision`, set `provenanceQuality = incomplete`, and never merge or sum the payloads under one key
- low-confidence fingerprint → do not merge
## 8. Phased delivery
| Phase | Scope | Closes #2037? |
|-------|--------|----------------|
| **P0** | Spec; sanitized corpus; lock eventKey / exact-copy / identity contract | No |
| **P1** | Same-session union → cross-window family closure → normalize (+ priority metadata) → deterministic family/provisional graph → provenance ledger + ownership/baseline continuity → corpus-proven per-event accounting → per-event pricing → two-tier atomic family cache → quality fields/reasons through report/UI | **Only if** corpus proves the P1 per-event path **and** no affected golden family has `accountingQuality = contained`. `incomplete + primary` may close when its provisional-family oracle matches; otherwise ship non-closing and require P2 |
| **P2** | Per-lineage cumulative baselines; containment strictly fallback → clear remaining `contained` goldens | Required to close when P1 still needs containment on in-scope goldens |
| **P3** | Pi parity if logs match | Separate if needed |
[#2066](https://github.com/steipete/CodexBar/pull/2066) may merge earlier only as an **explicit non-closing** interim guard with owner approval and #2037 left open.
## 9. Test plan (after corpus locks keys)
**Corpus golden**
1. Family token total = hand oracle (no multi-file ancestor multi-charge).
2. Dollar total = hand oracle under nonlinear **and** standard vs priority splits.
3. Post-fork identical-but-distinct events preserved.
4. Quality fields and mandatory reasons match expected state (`provenanceQuality`, `accountingQuality` — including combinations such as incomplete + contained).
5. Two children copy the same pre-fork parent event → charged once.
6. **Two siblings reference the same unavailable parent** → provisional-family reason + `provenanceQuality = incomplete`; copied prefix bills once and family does not inflate (quality/ownership focus).
7. Same token vectors post-fork on siblings → not deduped.
8. Cycle in fork edges → deterministic break + `provenanceQuality = incomplete`.
9. Duplicate `sessionId` with conflicting parents → `provenanceQuality = incomplete`.
10. Stable-ID collision with incompatible payloads → no silent merge; `provenanceQuality = incomplete`.
11. Same-session active+archive → provenance-safe union stream before family graph; unique suffixes preserved exactly once.
12. Priority metadata preserved → standard vs priority totals match oracle.
13. Ambiguous lineage → containment fallback + `accountingQuality = contained`; no gap-billion inflation.
14. Touching one family member recomputes whole family; atomic replace ≡ full family rescan.
15. `#968` / `#1062` / `#1164` non-family regressions remain green.
16. Parent total `100M`, copied child prefix `100M`, first unique child total `101M` → copied row bills zero but seeds state; child bills exactly `1M`.
17. Delete a parent shared by two children → family cost never rises; warm-cache, cold-cache, and force-rescan results agree.
18. Missing-parent siblings with corpus-proven shared prefix → prefix represented once; both child baselines seeded; first unique absolute totals count only post-prefix growth (state-continuity focus); post-fork work remains distinct.
19. Enumerate the same cyclic edge set in every order and through incremental arrivals → identical accepted forest and quality reasons.
20. Parent outside the requested day window still seeds/dedupes an in-window child; filtering occurs after reconciliation.
21. Active partial + archived suffix for one `sessionId` → canonical logical stream preserves the union of unique events exactly once.
22. One request/message ID attached to multiple token snapshots → not collapsed unless a corpus-proven compound event sequence distinguishes copies.
23. Real parent present → parent owns copied key; remove parent → one sibling owns; restore parent → ownership migrates back, with the key billed once in every generation.
24. Parent restoration with additional unique parent events → known copied-key contribution does not rise; only newly discovered unique events add cost.
25. Every supported/ambiguous fork-boundary shape → selected corpus rule classifies prefix/post-fork rows exactly; ambiguous boundary records its reason and does not aggressively merge.
26. High-confidence fingerprint collision with incompatible payload → events remain separate and `fingerprintCollision` is asserted.
27. Quality-reason goldens assert at least `missingParent`, `provisionalFamily`, `cycleBroken`, `conflictingParents`, `stableIdCollision`, `fingerprintCollision`, `ambiguousForkBoundary`, and `containmentFallback` where applicable.
28. Crash before manifest commit → old generation remains authoritative; crash after commit → new family/day totals and sidecars are internally consistent; orphan GC does not alter totals.
29. Original event timestamp controls both pricing catalog date and report day across parent restoration, rescans, and copied occurrences.
30. Family ledger emits unique per-session streams before #2066 containment; no family-global watermark is created.
31. Same-session active/archive streams contain two legitimate identical occurrences → ordered union preserves multiplicity while suppressing only the copied overlap.
32. Grandparent and parent both contain key `K`; grandparent owns billing, while child baseline walks grandparent → parent → child and includes intervening unique parent growth.
33. Provisional family with proven shared keys/prefix → `incomplete + primary`; ambiguous representative path → `incomplete + contained`, with exact reasons asserted.
34. Session ownership moves family A → B → main manifest day totals contain exactly one generation's contribution, never A-old + B-new together.
35. Conflicting authoritative parent claims → no parent edge accepted and `conflictingParents`; non-authoritative inference alone never creates an edge.
**Performance / storage acceptance (sanitized corpus):** record the §4.8 metrics for cold cache, dirty-family refresh, and full rebuild; verify that loading one dirty family does not deserialize every event sidecar. Performance budgets must be agreed before implementation freeze, and optimized representations must remain golden-equivalent.
## 10. Fixture required first (design input)
A sanitized Ultra / multi-fork corpus must answer:
1. Which IDs survive when parent events are copied into child files, and what is each ID's cardinality (one event, one request with many snapshots, one lineage, etc.)?
2. Are copied lines byte-identical, or only field-equal after normalization?
3. Does `turn_id` identify a cumulative lineage, or only a turn span unsafe alone?
4. Is `last_token_usage` per-request, cumulative, or sometimes replayed?
5. What happens when a parent file is unavailable (including **two siblings → one missing parent**)?
6. How are priority turns represented so metadata can be normalized?
7. Do active/archive views with one `sessionId` contain unique suffixes that require unioning?
8. Are fork timestamps present, parseable, and physically consistent enough for timestamp-first graph ordering?
9. Which exact fork-boundary rule correctly separates copied prefix from child-local work, including ambiguous/missing metadata cases?
10. Does parent disappearance/reappearance preserve event keys and original timestamps well enough for deterministic ownership migration?
**Deliverables before implementation freeze:** hand token + dollar oracles; documented namespaced event-key cardinality / exact-copy rules; the exact locked fork-boundary rule text; duplicated ancestor chain; missing-parent siblings; baseline-continuity oracle; parent disappear/restore ownership-migration oracle; ambiguous-boundary goldens; redacted before/after output; expected quality fields/reasons per scenario; cache/performance measurements and agreed budgets.
## 11. Risks
| Risk | Mitigation |
|------|------------|
| No stable ID survives copies | Pre-fork exact-copy rule; else `accountingQuality = contained` / labeled estimate only |
| Request/message ID covers multiple token snapshots | Require event-level cardinality proof or a copy-stable compound sequence; otherwise use only as lineage hint |
| Over-dedupe | Identity contract; post-fork preserve; no token-only dedupe |
| Same-session unordered union collapses legitimate repeats | Ordered overlap alignment within one `sessionId`; preserve occurrence multiplicity and unmatched suffixes |
| Removing a parent increases cost | Provisional missing-parent family + file-removal monotonicity invariant + warm/cold regressions |
| Restoring a parent double-bills provisional keys | Per-key owner priority + atomic ownership migration; only new unique parent keys may add cost |
| Deduped prefix resets child cumulative baseline | Separate billing disposition from state disposition; copied observations seed state but bill zero |
| Earliest billable owner skips intervening parent state | Ownership controls billing only; baseline seeding walks the actual parent chain and every fork boundary |
| P1 “state-only” implemented as another global baseline | Explicit `P1SessionAccountingState` per canonical session/provisional sibling; no family-global watermark |
| Cycle result depends on file order | Timestamp-first accepted-forest algorithm over the whole component; stable ID tie-breaks |
| Wrong fork boundary over- or under-dedupes | Corpus-lock one boundary rule; ambiguous boundary records reason and uses conservative path |
| Out-of-window parent treated as missing | Build relevant family closure before report-date filtering |
| Same-session union drops an active/archive unique suffix | Union all same-session views before cross-session graph construction |
| Monolithic normalized-event JSON grows too large | Small manifest/family results + lazy disposable sidecars; benchmark before choosing compact encoding |
| Crash mixes old/new family ownership or day totals | Generation-qualified sidecars; manifest atomic rename as commit point; orphan GC |
| Family move patches day totals twice | Rebuild day maps solely from one committed generation's family contributions; never patch global totals in place |
| Copied occurrence changes price/report day | Preserve original event timestamp as both pricing-date and report-bucket source |
| Quality never reaches UI | §6 mandatory in P1 (both dimensions) |
| Single enum conflates provenance vs accounting | Dual fields / reason set (§6.1) |
| Same-session vs family ordering bugs | §3 hard order + test 11 |
| Conflicting authoritative parent claims delegated to cycle breaker | Accept no guessed edge; mark `conflictingParents`; run forest only after single-parent candidates are resolved |
| Treating #2066 as the fix | Fallback-only; leave #2037 open unless P1 close criteria met |
| Closing #2037 while goldens still use containment | Explicit P1 close gate (§0, §8); otherwise P2 required |
## 12. Open questions for steipete / reporter
1. First milestone corpus: Codex JSONL, Pi, or both?
2. Acceptable dollar tolerance vs hand oracle?
3. Interim non-closing merge of [#2066](https://github.com/steipete/CodexBar/pull/2066) while P0P1 proceed?
4. Preferred UI strings for `provenanceQuality = incomplete` vs `accountingQuality = contained` (including when both apply)?
5. Confirm the close gate: an oracle-matching `incomplete + primary` provisional family is acceptable, but any affected golden with `accountingQuality = contained` keeps #2037 open for P2?
## 13. Summary
Strong basis for implementation once the fixture locks keys:
- **Fixture-first** gate
- **Ordered same-session union** that preserves multiplicity and unique suffixes
- **Cross-window family closure** before date filtering
- **Provenance separated from accounting**
- **One billable owner per event key**, with atomic ownership migration when a parent disappears/reappears
- **Billing ownership separate from parent-chain baseline propagation**
- **Billing suppression preserves baseline state** for copied prefixes
- **Explicit P1 session reducer**, never a family-global baseline
- **Provisional missing-parent families** with file-removal monotonicity
- **Explicit provisional `primary` vs `contained` fork** based on corpus-proven identity
- **Corpus-locked fork-boundary rule** with an explicit ambiguous path
- **Refuse token-only deduplication**
- **Event-level ID cardinality**, not merely ID copy survival
- **Price after reconciliation** (with priority metadata preserved)
- **Containment only as fallback**
- **Correct P1/P2 boundary and close gate** — P1 closes #2037 only with proven per-event path and no contained goldens; otherwise P2 required
- **Identity contract** with copy-stable fingerprints (ordinal tie-break only unless corpus proves otherwise)
- **Explicit pipeline order** (same-session union before families)
- **Timestamp-first deterministic cycle handling**
- **Two-tier derived cache** with lazy sidecars and measured performance budgets
- **Manifest-generation commit protocol** with crash isolation and orphan sidecar GC
- **Day totals derived from one committed family generation**, never patched across moves
- **Dual quality fields** (provenance × accounting) with a real report/UI path
- **Mandatory quality reasons** asserted by goldens and exposed through cache/CLI
- **Event-local pricing/report dates** across rescans and ownership migration
- **Invalid graph / ID collision** states defined
- **Sibling missing-parent** coverage in tests
File-local containment alone cannot close #2037. This scan-wide provenance design can — after the corpus locks the keying rules, and only when the close criteria in §0 / §8 are met.
**Stop condition for prose:** after rev 8, do not begin P1 or add speculative key schemes until P0 locks §10.1 (event-key cardinality), §10.9 (fork-boundary rule), and §10.10 (ownership migration through parent disappearance/reappearance).
**Local P0 progress (2026-07-11, rev 2):** `docs/issue-2037-p0-local-corpus-findings.md` locks §10.1 / §10.9 for ordinary Codex rollout forks and records scanner-integration results:
- No per-`token_count` event ID; ordered normalized `(last,total)` contiguous prefix under ancestry; timestamps rewritten on copy (exclude from fingerprint); leaf `session_meta` is authoritative.
- Sanitized fixture `archived-fork-33ce-3869` + oracle tests are in-repo.
- **Parent present:** `#1164` inherited totals already match the parent-owns-prefix scanner-unit oracle (`Issue2037ScannerIntegrationTests`) — regression-locked, not a new ledger.
- Local Ultra/SolTerra parallel runs did **not** produce interleaved `total_token_usage` drops; do not block cross-file work on drops.
- **Still open for close criteria:** Ultra interleaved corpus, fuller event-key ledger / ownership migration, priority/`logs_2` join.
- **Missing-parent siblings:** sanitized fixture + hand oracle landed. Runtime token-only prefix suppression was removed because equal-counter distinct events are ambiguous; the scanner fails open until the P1 identity/quality path exists.
+212
View File
@@ -0,0 +1,212 @@
# P0 local corpus findings for #2037 (rev 2)
- **Source:** local Codex JSONL under `~/.codex/sessions` + `~/.codex/archived_sessions` (this machine)
- **Date:** 2026-07-11 (rev 2 adds scanner-integration observations)
- **Scope:** structural identity / fork-copy behavior only — no message bodies; session IDs truncated in narrative
- **Status:** Provisional locks for Codex rollout logs on this corpus. Still need an Ultra-shaped golden to close #2037, but ordinary-fork identity + parent-present scanner behavior are locked in-repo.
- **Companion:** `docs/issue-2037-fork-family-provenance-spec.md` (rev 8a+)
## Corpus snapshot
| Metric | Value |
|--------|-------|
| JSONL files | ~81+ (grew during Ultra P0 attempts) |
| Logical sessions (first `session_meta.id`) | dozens |
| Real fork edges (`forked_from_id` ≠ self) | several families |
| `token_count` events surveyed | ~6k+ |
Observed families (prefixes):
- `019f33ce``019f3869``019f38ec` (parent → child → grandchild)
- `019f32c8``019f331e` (mid-session / partial prefix fork)
- `019f3cec``019f3e32` (partial prefix; parent continued after fork)
- Later Ultra-adjacent: `019f4d90``019f52bf` / `019f52e6` / `019f52e7` / `019f52f3` (Sol/Terra, multi-agent v2)
## Locked answers (local Codex)
### §10.1 Event-key cardinality
| Candidate | Survives copy? | Event-level cardinality? | Verdict |
|-----------|----------------|--------------------------|---------|
| Stable event / request / message ID on `token_count` | n/a | **Absent** on `token_count` rows | Do **not** use as event key |
| `turn_id` | Present on other events (`event_msg`, `turn_context`) | **Not** on `token_count` | Lineage / priority join hint only — never sole cross-completion key |
| Envelope `timestamp` on `token_count` | **No** — rewritten at fork into a tight burst | n/a | **Not** copy-stable; exclude from fingerprint match |
| File-local ordinal / line number | No | Tie-break only | Exclude from fingerprint |
| Normalized `last_token_usage` + `total_token_usage` vector | **Yes** (field-equal, ordered) | Unique within observed parent streams, but not proven unique across distinct sibling work | Copy-candidate evidence only; insufficient destructive event identity |
| Byte-identical JSONL line | **No** (0 byte-identical parent↔child lines) | n/a | Prefer normalized equality |
**Lock:** For this Codex shape, there is **no** stable per-`token_count` event ID. Ordered exact-prefix matching of normalized usage vectors under `forked_from_id` ancestry identifies the copied-prefix candidate in the sanitized corpus, but token equality alone must not suppress runtime billing.
### §10.2 Copy equality
Copied ancestor `token_count` rows are **field-equal after normalization**, not byte-identical. Matching inputs that worked locally:
- `last_token_usage` (all int fields)
- `total_token_usage` (all int fields)
Do not require equal timestamps.
### §10.9 Fork-boundary rule (locked for this corpus)
**Corpus oracle rule (not a sufficient runtime event key):** Contiguous ordered usage-fingerprint prefix.
1. Take parent and child canonical `token_count` streams in file order.
2. Let `N` = longest `k` such that `child[0..k)` equals `parent[0..k)` under the normalized usage fingerprint.
3. `child[0..N)` is the copied prefix (state-only on the child; parent owns billing when present).
4. `child[N..)` is child-local work.
**Corroboration (not the classifier):** leaf `session_meta.timestamp` (`fork_ts`) lands within ~01s of the end of the rewritten prefix / start of unique work on observed mid-session forks. Useful for family graph ordering; **do not** classify prefix via `parentEventTimestamp <= fork_ts` because copied child timestamps are rewritten and are **not** the parents original times.
**Evidence:**
| Edge | Parent `token_count`s | Child | Prefix `N` | Parent continued after fork? |
|------|----------------------|-------|------------|------------------------------|
| `33ce→3869` | 135 | 158 | 135 (entire parent) | No |
| `3869→38ec` | 158 | 158 | 158 (entire parent; no unique child tokens in archive) | n/a |
| `32c8→331e` | 270 | 276 | 243 | Yes (+27 parent events) |
| `3cec→3e32` | 387 | 581 | 226 | Yes |
| `4d90→52bf` | 293 (live; continued) | 196 | 180 | Yes (parent continued after copy) |
### §10.4 `last_token_usage` semantics
On ordinary in-session streams, `Δ total_token_usage.total_tokens == last_token_usage.total_tokens` on nearly every step (hundreds of matches, ~0 positive mismatches in sampled files).
**Caveat:** the first post-fork child event can show `last > 0` while `total` is still flat vs the last prefix event. After provenance, billing should follow the corpus-locked per-event policy carefully; total-delta alone undercounts that row, last alone may overcount vs cumulative. Flag for golden oracles — do not invent a hybrid in P0 prose beyond “measure on goldens.”
**Oracle units:** hand oracles that sum `last.total_tokens` are **not** identical to scanner-priced units. Scanner accumulates `input + cached_input + output` from each billed delta. Integration tests must compare **scanner units** (`sum(last.input+cached+output)` with prefix suppressed), not raw `total_tokens` field sums.
### §10.5 / ownership migration
- Authoritative session identity for a file: **first** `session_meta` in the file (the leaf). Later `session_meta` rows often re-embed ancestors.
- Child / grandchild files embed ancestor `session_meta` chains (`leaf`, then parent, then grandparent, …). Graph builder must not treat every embedded meta as a separate root file identity.
- Filename UUID can differ from leaf `session_meta.id` only in the sense that archives are named by leaf id; always prefer leaf meta id.
- Parent may continue after a child forks (partial prefix). Billing of copied keys stays with the earliest real ancestor that contains them; child baseline still walks the actual parent chain (spec §4.5.1).
### §10.7 Same-session multi-file
Common: active `sessions/…` rollout + `archived_sessions/…` for related rollouts. Same-session union must be **ordered overlap**, not unordered fingerprint sets. Multi-file groups often share a leaf id across rollouts; treat carefully (some “same id” groups are true active/archive; some are chained fork archives — use first `session_meta` + `forked_from_id`).
### Inflation demo (one family)
For `019f33ce` + child `019f3869`, summing `last_token_usage.total_tokens` over both files without prefix dedupe vs parent-owns-prefix:
| Method | Sum of `last.total_tokens` |
|--------|----------------------------|
| Naive parent+child | ~28.8M |
| Dedupe (parent owns prefix) | ~15.4M |
| Inflation ratio | ~1.88× |
This is the inter-file overcount shape on ordinary forks (not yet Ultra interleave).
## Ultra / interleaved hunt (observations)
Attempts to produce **intra-file interleaved cumulative streams** (multiple rising `total_token_usage` counters mixed so totals **drop** mid-file):
| Attempt | What appeared | Total drops ≥50k? |
|---------|---------------|-------------------|
| Sol/Terra + multi-agent v2 forks (`4d90` family) | Fork copies, monotonic totals | **No** |
| Parallel-worker P0 prompt (fixture sanitization) | New forks `52e6`/`52e7`/`52f3`, proactive mode sometimes | **No** |
| Corpus-wide scan after those runs | — | **0 files** |
**Conclusion:** Harder tasks and parallel Ultra workers on this Codex build produced useful **fork-copy** logs and Sol/Terra multi-agent metadata, but **not** the reporters interleaved gap-recount shape. Token drops are **not** required to proceed on cross-file provenance. Interleaved Ultra remains reporter-corpus-dependent.
“Ultra” string hits in older logs were mostly chat/tool text / schemas, not a structural Ultra mode flag on `token_count`.
## What local data does *not* yet prove
- Ultra interleaved multi-lineage totals inside one file (the #2037 reporter shape / billions-scale gap)
- Priority / `logs_2` turn surcharge metadata on these `token_count` rows (not visible in the JSONL token payload here)
- Copy-stable event identity for parent-missing siblings; the added fixture proves the arithmetic gap, not that token-vector equality is collision-free
- Cross-session / cross-sibling `usage_fp` collision rate at scale (sample showed collisions across parent/child as expected; token equality is not allowed to become destructive identity)
## P1 design constraints from the provisional corpus
1. **Event key:** none from explicit IDs on `token_count`.
2. **Copy candidate:** ordered contiguous normalized `(last_token_usage, total_token_usage)` under ancestry.
3. **Fork-boundary oracle:** that contiguous prefix length `N`; `session_meta` fork timestamp for graph ordering only.
4. **Low-confidence fingerprint:** usage vectors exclude rewritten timestamps and file ordinals, but cannot drive suppression without stronger copy-stable identity.
5. **Future billable owner:** earliest real ancestor containing a ledger-accepted copied event; descendants state-only only after identity acceptance.
6. **Embedded meta:** first `session_meta` is leaf; ignore ancestor meta rows as competing file identities.
7. **Runtime today:** fail open for missing-parent cross-file equality; `#2066` containment remains file-local and non-closing.
## In-repo fixtures and tests (what we built)
| Path | Role |
|------|------|
| `Tests/.../Fixtures/CostUsage/Issue2037/archived-fork-33ce-3869/` | Sanitized parent+child from local `33ce→3869`; usage+meta only |
| `.../live-fork-4d90-52bf/` | Second parent-present golden from `4d90→52bf`; parent truncated to prefix N=180 |
| `.../missing-parent-siblings/` | Two children, shared prefix, no parent file |
| `.../ORACLE.md` + `manifest.json` | Hand oracle: naive vs parent-owns-prefix / provisional owner |
| `.../harness-smoke/` | Tiny install harness smoke |
| `Issue2037FixtureSupport.swift` | Shared load/install + sanitized event parsing |
| `Issue2037ProvenanceFixtureTests.swift` | Prefix length, oracle arithmetic, field allowlist |
| `Issue2037FixtureHarnessTests.swift` | Install into `CostUsageTestEnvironment` |
| `Issue2037ScannerIntegrationTests.swift` | End-to-end `loadDailyReport` vs scanner-unit oracle |
Fixture timestamps were rewritten to **midday UTC** on distinct calendar days so local timezones do not map parent rows outside a Jan 12 report window (midnight UTC previously made parent appear as the prior local day and dropped it from the report).
## Scanner integration observations (2026-07-11)
### Experiment
1. Install `archived-fork-33ce-3869` into an isolated Codex home (`sessions/` + `archived_sessions/` roots as the scanner expects).
2. `CostUsageScanner.loadDailyReport(provider: .codex, …, forceRescan: true)` over the fixture days.
3. Compare scanned `input + cacheRead + output` to:
- **naive scanner units:** sum of `last.input+cached+output` over parent **and** all child events
- **deduped scanner units:** parent all events + child events after prefix N only
### Result
**With the parent file present in-window, current `#1164` inherited-totals accounting already matches the parent-owns-prefix scanner-unit oracle.**
- Child absolute totals are rebased by the parent snapshot at/before fork.
- Copied prefix therefore contributes ~0 on the child; unique suffix bills the post-fork growth.
- Parent bills its own stream once.
- `Issue2037ScannerIntegrationTests` locks this as a **regression**, not as a new ledger.
### Pitfalls discovered while integrating
1. **Timezone / day-key:** UTC midnight timestamps can fall on the previous **local** calendar day; a narrow `since/until` then silently omits the parent and the report shows only child unique growth (~3.3M in one probe) — looking like undercount, not overcount.
2. **Oracle unit mismatch:** `sum(last.total_tokens)` ≠ scanner priced units when `total_tokens` omits cached input; always compare scanner units in integration tests.
3. **Filename date filter:** flat `archived_sessions` listing allows undated names (`parent.jsonl`); dated filenames are range-filtered — fine for fixtures without dates in the name.
### What `#1164` does *not* cover (next work)
| Gap | Why inheritance is insufficient |
|-----|----------------------------------|
| **Missing parent** + two siblings sharing a copied prefix | No parent snapshot to inherit; each child can bill the full prefix |
| **Provisional family** billable owner | Spec §4.4.2 / §4.5.1 — needs family ledger, not per-file baseline alone |
| **Intra-file interleaved totals** | No drops in local Ultra logs; containment (#2066) is a separate guard |
| **True event-key ledger** across arbitrary ancestry depth | Inheritance is fork-point baseline, not per-event ownership migration |
## Method notes
Repro probes were ephemeral Python over `~/.codex/**/*.jsonl`, hashing normalized usage vectors and comparing ordered prefixes. Raw production JSONL was not copied into the repo; fixtures are sanitized aliases only.
## Next steps
1. ~~Add a **missing-parent sibling** sanitized fixture + hand oracle that exposes todays `#1164`-only gap.~~ **Done** (`missing-parent-siblings`). Runtime cross-file suppression is deferred until copy-stable event identity is proven.
2. ~~Optionally sanitize `4d90→52bf` as a second parent-present golden.~~ **Done** (`live-fork-4d90-52bf`; parent truncated to copied prefix for a clean `#1164` regression). Locks a real corpus quirk: parent ordinal 120 has non-zero `last` with flat `total` — scanner units must follow **total deltas**, not `sum(last)`.
3. Still seek an Ultra interleaved corpus before claiming #2037 fully closed for that shape.
4. Build the event-key ledger / ownership migration path (including parent disappear/reappear) before enabling missing-parent cross-file dedupe.
5. PR packaging: keep #2066 non-closing; ship containment + provenance fixtures/docs without claiming full #2037 close.
### Missing-parent siblings (fixture locked; runtime dedupe deferred 2026-07-12)
Fixture `missing-parent-siblings`: two children, shared 135-event prefix, no parent file.
| Metric | Scanner units |
|--------|---------------|
| Naive (both bill prefix) | ~54.4M |
| Target parent-owns-prefix oracle | ~28.8M |
The provisional token-vector prefix suppression was removed before merge. Distinct sibling events can have identical `last_token_usage` / `total_token_usage` vectors at the same ordinal; treating that equality as event identity can silently suppress legitimate work. Adding envelope timestamps is invalid because copied timestamps are rewritten, and model is not reliably present on `token_count` rows.
**Runtime fail-open rule:** when a parent file is missing, do not perform destructive cross-file dedupe from token counters alone. Each sibling keeps its file-local unresolved-parent accounting. This can retain copied-prefix overcount, but it cannot undercount distinct work merely because counters collide. `Issue2037ScannerIntegrationTests` locks this with two missing-parent siblings whose distinct models/timestamps have equal token vectors.
The sanitized fixture and arithmetic remain useful as the desired family-ledger oracle. They do not authorize suppression until the runtime has a corpus-proven copy-stable event key, collision handling, cross-window family closure, and an exposed ambiguous/contained quality path.
**Required before revisiting runtime suppression:**
- Prove a copy-stable identity stronger than token values; `reasoning_output_tokens` / `total_tokens` only reduce collision probability and are not sufficient identity.
- Build family closure across files outside the reporting window before applying the date filter.
- Add fingerprint-collision, ownership migration, cache-repeat, and affected-setup proof.
@@ -0,0 +1,246 @@
# Spec: Contain Ultra-mode interleaved-lineage token overcounting (issue #2037)
- **Issue:** [steipete/CodexBar#2037](https://github.com/steipete/CodexBar/issues/2037) — "Ultra-mode Terra and Sol sessions can overcount forked context"
- **Status:** Implemented (rev 4 — Phase 1 post-latch containment + min-cap)
- **Affected code:** `Sources/CodexBarCore/Vendored/CostUsage/` (Codex session scanner)
- **Related prior fixes:** #968 (divergent totals), #1062 (repeated total snapshots), commit `45b68c34` (fork replay)
## 0. Framing (read first)
This spec deliberately ships in two phases:
- **Phase 1 (this PR): containment.** Stop the multiplicative blowup and guarantee a *never-inflates* property. In multi-lineage files the result is an explicitly **conservative estimate** — it can undercount genuine totals-only sub-agent usage. We do not claim "true per-lineage usage."
- **Phase 2 (follow-up, fixture-gated): per-lineage accounting.** Candidate-baseline run tracking to recover undercounted totals-only lineages. Blocked on obtaining a sanitized real Ultra fixture (§8), because its correctness depends on empirical `last_token_usage` semantics that cannot be asserted from first principles.
## 1. Problem
CodexBar massively overstates usage for `gpt-5.6-terra` and especially `gpt-5.6-sol` when they run in Ultra mode. In one reported Sol session, the raw log reached roughly **268M** cumulative input tokens while CodexBar attributed **3.29B** input tokens and about $4,000 of standard cost. A single forked turn contributed more than 3B tokens across hundreds of rows. Pricing tables are correct; the inflation comes from usage accounting.
Ultra sessions fork multiple sub-agents that:
1. write **interleaved cumulative token snapshots** (`total_token_usage`) into the *same* session JSONL file, and
2. **replay large portions of the parent context** into sub-agent turns.
## 2. Definitions
**Lineage:** one monotonic cumulative-counter sequence (`total_token_usage`) produced by one agent/sub-agent. Ultra files interleave several lineages with no reliable lineage identifier on token events (`turn_id` cannot be used: cumulative counters span turns in normal sessions).
**Replay vs. genuine context — two different things this spec must not conflate:**
- *Copied cumulative history:* a child counter initialized with (or re-emitting) the parent's accumulated totals. Counting this again is double counting. Must always be excluded.
- *Context actually sent in a new sub-agent request:* the replayed parent context is transmitted as input tokens of a real API call, typically billed at the **cached-input** rate. This may be genuine usage.
Which of these `last_token_usage` represents on a sub-agent's first turn in Ultra logs is an **empirical question** (§8). Phase 1 takes the conservative-for-inflation stance: after latch, `last` is capped by the contained totals delta (so below-watermark `last` is dropped). Phase 2 revisits this against the fixture.
## 3. Root cause
`CostUsageScanner` assumes **one monotonic cumulative counter lineage per session file**. `handleTokenCount` (in `parseCodexFileCancellable`, `CostUsageScanner.swift`) keeps a single `rawTotalsBaseline` and computes totals-derived deltas as `max(0, current baseline)` via `codexTotalDelta`.
With two interleaved lineages A and B in one file the event stream looks like:
```
A: total=100M → delta 100M, baseline=100M
B: total=5M → clamped to 0 (divergent fallback), baseline=5M ← baseline lowered
A: total=101M → delta = 101M 5M = 96M ← gap recounted
B: total=6M → clamped to 0, baseline=6M
A: total=102M → delta = 102M 6M = 96M ← gap recounted again
...
```
Every lineage flip recounts nearly the entire gap between the two counters. Hundreds of interleaved snapshots inflate ~268M real tokens into billions.
Exposure by path:
- **Resolved fork child path** (`handleTokenCount`, the `forkedFromId != nil` branch) runs totals-only — it deliberately ignores `last` to avoid replayed per-turn snapshots (`45b68c34`). Worst offender; matches "a single forked turn contributed 3B+".
- **Total-only events** (no `last_token_usage`) hit the same gap-recount mechanism.
- **Root sessions with `last` present** degrade differently: the divergent flag disables the #1062 guard (`codexShouldPreferTotalDelta`), so re-emitted snapshots can re-count `last`.
Existing mitigations do not cover this failure mode:
| Mitigation | Covers | Gap |
|---|---|---|
| #968 divergent totals (`codexDivergentTotalDelta`) | Counter *decreases* within one lineage → fall back to counted baseline | Lowers the effective baseline, so the *next* event of the larger lineage recounts the gap |
| #1062 (`codexShouldPreferTotalDelta`) | Repeated identical total snapshots re-adding `last` | Only active in the non-divergent path, and only compares adjacent totals; interleaving disables it and defeats adjacency (see §5.2) |
| Fork handling (`forked_from_id`, `CodexInheritedTotalsResolver`) | Separate child *files* replaying parent history | No concept of multiple lineages interleaved *inside one file* |
## 4. Goals / non-goals
**Goals (Phase 1)**
- **Never-inflates invariant:** for any input, tokens attributed from totals-derived deltas never exceed the maximum raw cumulative total observed in the file (fork-inheritance adjusted). No sequence of interleaved or re-emitted snapshots can multiply usage.
- Exact re-emissions of previously seen snapshots contribute zero — including **alternating** re-emissions across lineages (this is where rev 1 of this spec was broken; see §5.2).
- Fork-replayed parent history stays excluded (existing behavior preserved).
- Single-lineage files behave exactly as today; all existing #968/#1062/fork tests pass unchanged.
- Incremental (append-only) scans resume with all correctness-critical reducer state and produce byte-identical results to a full rescan; the optional seen-snapshot FIFO may be absent without affecting containment.
- Multi-lineage results are explicitly documented as a conservative estimate (undercount bias), not exact usage.
**Explicitly accepted Phase 1 limitation**
A smaller lineage growing beneath another lineage's watermark (e.g. 5M → 50M under a 100M watermark) contributes nothing in Phase 1, even when it supplies `last_token_usage`: the contained totals delta is zero, so `min(last, 0) = 0`. This is **normal Ultra behavior, not a rare edge case**, and it is the deliberate trade: undercounting bounded genuine usage beats multiplying it. Phase 2 (§7) exists to recover it.
**Non-goals**
- Per-sub-agent usage attribution/breakdown UI.
- Changing pricing, priority/Ultra cost splitting (`CostUsageScanner+CodexPriority.swift`), or non-Codex providers.
- Phase 2 candidate-run tracking (specified in outline only; separate PR).
## 5. Phase 1 design
All post-latch token-count accounting uses the shared tracker and delta helpers (§5.5), with correctness-critical state persisted for incremental scans (§5.6). Rules below are the shared policy, in precedence order.
### 5.1 High-watermark containment (load-bearing)
Track `rawTotalsWatermark`: the component-wise maximum of every raw cumulative total observed (after fork-inheritance adjustment).
- **Interleaving detection:** an event whose total has **any component strictly below** the corresponding watermark component latches `sawInterleavedTotals` for the file (persisted, permanent). Mixed movement (input ↓ while output ↑) cannot come from one monotonic counter, so "any component" is deliberate. Legitimate single-lineage resets (compaction, restart, corrupt log) also latch the flag; that is accepted — it converts a potential overcount into an undercount.
- **Never lower** the watermark or the baseline on detection.
- Once latched, totals-derived deltas use `codexContainedTotalDelta` (§5.3.1), not a lowerable per-event baseline. Lineage flips cannot re-count the high/low gap.
**Supersedes divergent mode:** once `sawInterleavedTotals` is latched, `codexDivergentTotalDelta` (whose counted-baseline fallback *is* the gap-recount mechanism) and `codexShouldPreferTotalDelta` are not consulted for this file. `sawDivergentTotals` continues to work unchanged for never-interleaved files and for fork-parent snapshot resolution.
### 5.2 Seen-snapshot suppression (optional precision)
Maintain `seenRawTotals`: a **bounded FIFO** (~64) of raw cumulative totals for best-effort exact re-emission suppression.
- Exact matches can short-circuit to zero before delta math.
- After post-latch containment (§5.3), eviction cannot inflate usage: a re-emitted below-watermark total has contained delta 0, so `min(last, 0) = 0`.
- Therefore the seen set is **not** load-bearing for correctness and must not gate incremental resume.
### 5.3 Counting rule in interleaved mode
For each token-count event once `sawInterleavedTotals` is latched and a `total` is present:
1. Optionally, total exactly matches `seenRawTotals` → count **0** (precision optimization; not required for correctness).
2. Compute the **contained totals delta** component-wise (§5.3.1).
3. If `last_token_usage` is present → `delta = min(adjustedLastDelta(last), containedTotalDelta)`.
4. Otherwise → `delta = containedTotalDelta`.
`last` alone must never increase counted usage when the contained totals delta is zero. Smaller-lineage genuine `last` below the watermark is an accepted Phase 1 undercount.
#### 5.3.1 Contained totals delta (not plain watermark delta)
Do **not** use plain `codexTotalDelta(from: watermark, …)` after latch — that breaks #968 “resume from counted baseline” (growth below the old raw watermark that still exceeds counted totals).
Use a dedicated helper, component-wise:
```
if current >= watermark {
delta = max(0, current - max(watermark, counted))
} else {
delta = max(0, current - counted)
}
```
This preserves counted-baseline recovery without allowing high/low lineage gaps to be recounted. Bound after latch:
`counted ≤ max(counted_when_latched, subsequent_watermark)` (and for totals-only streams, `counted ≤ max observed cumulative total`).
### 5.4 Fork children
- **Non-interleaved** resolved forks keep totals-only accounting (`45b68c34` / #1164). Do not apply a global `min(last, total)` cap there — established tests require totals-derived deltas that can exceed `last`.
- **After latch**, resolved forks use the same post-latch rule as root sessions (§5.3), including `min(adjustedLast, containedTotalDelta)`.
- **Unresolved forks** keep skip-first + `min(last, totalDelta)`; `unresolvedForkTotalWatermark` is a presence sentinel while the global tracker supplies the delta baseline.
### 5.5 Shared policy surface
`CodexTotalsTracker` (watermark + optional seen-set + latch) is shared. Post-latch delta policy (`codexContainedTotalDelta` / `codexPostLatchEventDelta`) must be applied by all three consumers:
1. root / non-fork parsing (`handleTokenCount`)
2. resolved-fork parsing (`handleTokenCount`)
3. parent snapshot accumulation (`CodexSnapshotAccumulator`)
Otherwise fork children can inherit baselines computed under a different policy.
### 5.6 Cache: persist correctness-critical state
`CostUsageFileUsage` gains:
- `lastRawTotalsWatermark: CostUsageCodexTotals?` (**required** for interleaved / divergent resume)
- `hasInterleavedTotals: Bool?` (**required** when watermark is present; partial XOR → full rescan)
- `seenRawTotals: [CostUsageCodexTotals]?` (**optional** precision only — missing must not force rescan)
**Invalidation:** regenerate `CodexParserHash`; clear `compatibleCodexProducerKeys`. Legacy divergent entries without a watermark force a per-file full rescan.
## 6. Acceptance criteria
1. **Exact expectations (primary):** fixtures assert exact counted totals / row counts for each rule branch.
2. **Never-inflates property (merge bar):** generated totals-only interleaved sequences satisfy `counted ≤ max observed cumulative total`.
3. **Floor:** fixtures assert a conservative minimum so a degenerate ~0 parser fails.
4. **Manual / real Ultra log:** no multiplied-gap inflation; satisfies the formal containment bound; above the fixture's conservative floor; plausible relative to the raw log — **not** required to land “near” 268M, because Phase 1 intentionally drops smaller-lineage usage.
## 7. Phase 2 outline (separate PR, fixture-gated)
Recover totals-only / below-watermark smaller-lineage usage with candidate-baseline run tracking, gated on a sanitized real Ultra fixture that establishes `last_token_usage` semantics.
## 8. Prerequisite: sanitized real Ultra fixture
Needed before claiming accurate multi-lineage recovery (Phase 2). Phase 1 ships on synthetic fixtures plus the containment property.
## 9. Touched files
| File | Change |
|---|---|
| `CostUsageScanner.swift` | `codexContainedTotalDelta` / `codexPostLatchEventDelta`; tracker + accumulator; post-latch policy in all three consumers |
| `CostUsageScanner+CacheHelpers.swift` | Persist watermark + interleaved flag; seen-set optional; incomplete-state rescan |
| `CostUsageCache.swift` | New fields; clear compatible producer keys |
| `CodexParserHash.generated.swift` | Regenerated |
| `CostUsageScannerBreakdownTests.swift` | Containment / cap / eviction / cache / property tests |
| `docs/issue-2037-ultra-fork-overcount-spec.md` | This spec |
## 10. Test plan (merge bar)
1. `codex interleaved cumulative lineages do not recount the gap`
2. `codex alternating repeated snapshots count zero` — containment (not FIFO) keeps repeats at zero
3. `codex totals only growth below watermark is conservatively dropped`
4. `codex single lineage counter reset undercounts but never inflates` — preserves #968-style recovery past the peak
5. `codex interleaved fork child caps last by contained total delta`
6. `codex root interleaved caps last much larger than watermark delta`
7. `codex fork interleaved caps last much larger than watermark delta`
8. `codex interleaved replay after sixty five unique snapshots stays contained`
9. `codex interleaved totals only sequences stay within containment bound` (property)
10. `codex incremental append preserves interleave containment across boundary` — full state equality vs forceRescan
11. `codex missing watermark or interleaved flag forces full rescan`
12. `codex missing optional seen set keeps incremental resume safe`
13. `codex divergent cache entry without watermark forces full rescan`
14. Existing `#968` / `#1062` / fork replay tests unchanged
Regression: `make check`, focused scanner tests, then `make test`.
---
# PR documentation (draft body — Phase 1)
## Title
Contain Ultra-mode interleaved-lineage token overcounting (#2037)
## Summary
- Ultra-mode Terra/Sol sessions can interleave cumulative `total_token_usage` snapshots from multiple lineages in one JSONL file. A single file-global baseline then recounts the high/low gap on every flip — turning ~268M real input tokens into ~3.29B (~$4,000) in a reported session.
- Phase 1 makes inflation **provably bounded**: a never-lower watermark latches interleaved mode on any component drop; post-latch deltas use a dedicated containment helper (preserving #968 counted-baseline recovery) and `min(adjustedLast, containedTotalDelta)` so `last` cannot grow usage when the contained totals delta is zero.
- **Single-lineage and pre-latch fork behavior is preserved** (including #1164 totals-only fork replay handling). The slow JSON path and fork-parent snapshot builder share the same policy.
- Cache persists watermark + interleaved flag (`seenRawTotals` is optional precision only); incomplete critical state forces a full rescan. Parser hash invalidation rebuilds old caches once.
- **Smaller-lineage undercount is intentional** and deferred to fixture-gated Phase 2. Latched files are a conservative estimate, not claimed true per-lineage usage.
Fixes #2037. Related: #968, #1062, `45b68c34` (fork replay).
## Behavior changes
- Interleaved (Ultra) files: no multiplied-gap inflation; after latch, counted totals stay within the containment bound and `last` cannot exceed the contained totals delta.
- Single-lineage / never-latched files: unchanged counting semantics.
- Pre-latch resolved forks: still totals-only (replay-safe).
- Post-latch: below-watermark smaller-lineage usage may be dropped until Phase 2.
## Test plan
- [x] Focused `CostUsageScannerBreakdownTests` / `CostUsageCacheTests` (containment, caps, eviction, property bound, cache gates)
- [x] Existing #968 / #1062 / fork replay tests
- [x] `make check`
- [ ] `make test`
- [ ] Optional follow-up: rescan a real Sol/Ultra log for plausibility (not a merge blocker)
## Notes for reviewers
- Design is lineage-ID-free; value-based multi-run recovery is Phase 2 behind a sanitized Ultra fixture.
- `seenRawTotals` is **not** load-bearing after the post-latch min-cap; missing it must not force a rescan.
- A real Ultra log is valuable validation, not required to merge Phase 1 containment.
+70
View File
@@ -0,0 +1,70 @@
---
summary: "JetBrains AI provider notes: local XML quota parsing, IDE auto-detection, and UI mapping."
read_when:
- Adding or modifying the JetBrains AI provider
- Debugging JetBrains quota file parsing or IDE detection
- Adjusting JetBrains menu labels or settings
---
# JetBrains AI provider
JetBrains AI is a local-only provider. We read quota information directly from the IDE's configuration files.
## Data sources + fallback order
1) **IDE auto-detection**
- macOS: `~/Library/Application Support/JetBrains/`
- macOS (Android Studio): `~/Library/Application Support/Google/`
- Linux: `~/.config/JetBrains/`
- Linux (Android Studio): `~/.config/Google/`
- Supported IDEs: IntelliJ IDEA, PyCharm, WebStorm, GoLand, CLion, DataGrip, RubyMine, Rider, PhpStorm, RustRover, Android Studio, Fleet, Aqua, DataSpell
- Selection: most recently modified `AIAssistantQuotaManager2.xml`
2) **Quota file parsing**
- Path: `<IDE_BASE>/options/AIAssistantQuotaManager2.xml` (macOS/Linux)
- Format: XML with HTML-encoded JSON attributes
## XML structure
- `quotaInfo` attribute (JSON):
- `type`: quota type (e.g., "Available")
- `current`: tokens used
- `maximum`: total tokens
- `tariffQuota.available`: remaining tokens
- `until`: subscription end date
- `nextRefill` attribute (JSON):
- `type`: refill type (e.g., "Known")
- `next`: next refill date (ISO-8601)
- `tariff.amount`: refill amount
- `tariff.duration`: refill period (e.g., "PT720H")
## Parsing and mapping
- Usage calculation: `tariffQuota.available / maximum * 100` for remaining percent
- Reset date: from `nextRefill.next`, not `quotaInfo.until`
- HTML entity decoding: `&#10;` → newline, `&quot;` → quote
## UI mapping
- Provider metadata:
- Display: `JetBrains AI`
- Label: `Current` (primary only)
- Identity: detected IDE name + version (e.g., "IntelliJ IDEA 2025.3")
- Status badge: none (no status page integration)
## Settings
- IDE Picker: auto-detected IDEs list, or "Auto-detect" (default)
- Custom Path: manual base path override (for advanced users)
## Constraints
- Requires JetBrains IDE with AI Assistant enabled
- XML file only exists after AI Assistant usage
- Internal file format; may change between IDE versions
## Key files
- `Sources/CodexBarCore/Providers/JetBrains/JetBrainsStatusProbe.swift`
- `Sources/CodexBarCore/Providers/JetBrains/JetBrainsIDEDetector.swift`
- `Sources/CodexBar/Providers/JetBrains/JetBrainsProviderImplementation.swift`
Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

+100
View File
@@ -0,0 +1,100 @@
---
summary: "Safe troubleshooting for macOS Keychain and browser Safe Storage prompts."
read_when:
- Investigating Chrome Safe Storage or browser Safe Storage prompts
- Explaining prompts that appear after uninstalling CodexBar
- Collecting safe support details without exposing secrets
---
# Keychain prompts
CodexBar can trigger macOS Keychain prompts when an enabled provider imports browser cookies, reads a provider-owned
OAuth item, or uses a CodexBar-owned cache entry. Chromium browser cookie import commonly asks for the browser's
Safe Storage item, such as "Chrome Safe Storage", "Brave Safe Storage", or "Microsoft Edge Safe Storage".
CodexBar does not need your browser password. macOS owns the prompt, and the prompt should identify the app or binary
that is requesting access. For support reports, include that requesting app/path when possible and do not paste
passwords, cookie headers, OAuth tokens, API keys, or Keychain item values.
Before a Keychain read that may require interaction, CodexBar shows an explanation of the item and its purpose.
**Learn More** opens this page without dismissing that explanation or starting the macOS prompt. Choose **OK** only
when you are ready to continue, or use the opt-out below.
After you acknowledge the Claude OAuth explanation, CodexBar does not repeat that explanation for six hours. This
cooldown only applies to CodexBar's explanatory alert: macOS can still show its own Keychain authorization prompt,
and the Claude **Never prompt** and global **Disable Keychain access** settings remain in effect.
## If the prompt appears after uninstalling CodexBar
Deleting `CodexBar.app` prevents a new process from launching from that bundle, but it does not terminate a process
that is already running from it. That process can continue to request Keychain access until it quits. If macOS still
shows a prompt such as "CodexBar wants to use your confidential information stored in 'Chrome Safe Storage'", the
usual causes are:
- A CodexBar process or bundled helper is still running.
- CodexBar is still enabled in Login Items and relaunched from an existing install.
- Another copy of `CodexBar.app` exists elsewhere on the machine.
- The uninstall path did not remove the same copy that launched the process. Finder, Homebrew cask, Sparkle updates,
and manually copied apps can leave different install paths in play.
- The prompt is naming the requesting binary, not proving that the copy you deleted is the one still running.
Safe checks:
```bash
pgrep -fl 'CodexBar|CodexBarCLI'
ls -ld /Applications/CodexBar.app
brew info --cask codexbar
mdfind 'kMDItemCFBundleIdentifier == "com.steipete.codexbar"'
```
Also check:
- **Activity Monitor**: search for `CodexBar` and `CodexBarCLI`.
- **System Settings -> General -> Login Items**: remove CodexBar if it remains listed.
- **Keychain prompt screenshot**: capture the full prompt, especially any requesting app/path details. Redact user
names or unrelated window contents if needed, but do not include secrets.
If you find a still-running process, quit CodexBar from the menu if possible, or quit it from Activity Monitor. If you
find another installed copy, confirm whether that copy is the one macOS names in the prompt before changing anything
else.
## Stop CodexBar from using Keychain
If CodexBar is still installed and you want it to stop all Keychain access:
1. Open **CodexBar -> Settings -> Advanced**.
2. In **Keychain access**, enable **Disable Keychain access**.
3. Relaunch CodexBar.
This disables Keychain reads and writes from CodexBar. Browser-cookie-based providers will be skipped because
CodexBar can no longer decrypt browser cookies. Manual cookie headers, API keys, and CLI/OAuth flows that do not rely
on Keychain can still work where the provider supports them.
## Browser Safe Storage prompts
If a Chromium-family Safe Storage check requires interaction or is denied, CodexBar pauses automatic cookie imports
for every Chromium-family browser for six hours. This prevents a denial in Arc, Edge, Brave, or another Chromium
browser from immediately moving to the next browser and showing another prompt. **Refresh Now** is an explicit retry
for the browser that was blocked; Safari and Firefox-family cookie imports remain available during the pause.
For normal browser-cookie import prompts, either allow CodexBar in the Keychain item's Access Control list or disable
Keychain access:
1. Open **Keychain Access.app**.
2. Select the `login` keychain.
3. Search for the item named in the prompt, for example `Chrome Safe Storage`.
4. Open the item, choose **Access Control**, and add `CodexBar.app` under "Always allow access by these applications".
5. Relaunch CodexBar.
Avoid "Allow all applications" unless you intentionally want every app to access that item. Do not paste or share the
item's secret value when asking for help.
## What to include in a support issue
- CodexBar version and install source: GitHub release, Homebrew cask, Sparkle update, or another source.
- macOS version.
- The uninstall method if this happened after uninstalling.
- Whether Activity Monitor or `pgrep` still shows CodexBar.
- Whether System Settings -> General -> Login Items still lists CodexBar.
- Whether `/Applications/CodexBar.app`, Homebrew cask metadata, or Spotlight finds another copy.
- A screenshot of the Keychain prompt showing the requested item and requesting app/path, with secrets redacted.
+54
View File
@@ -0,0 +1,54 @@
---
summary: "Kilo provider data sources: app.kilo.ai API token and CLI auth-file fallback."
read_when:
- Adding or modifying the Kilo provider
- Adjusting Kilo source-mode fallback behavior
- Troubleshooting Kilo credentials/auth sessions
---
# Kilo provider
Kilo supports API and CLI-backed auth. Source mode can be `auto`, `api`, or `cli`.
## Data sources + fallback order
1. API (`api`)
- Token from `~/.codexbar/config.json` (`providers[].apiKey` for `kilo`) or `KILO_API_KEY`.
- Calls `https://app.kilo.ai/api/trpc`.
2. CLI session (`cli`)
- Reads `~/.local/share/kilo/auth.json` and uses `kilo.access`.
- Requires a valid CLI login (`kilo login`).
3. Auto (`auto`)
- Tries API first.
- Falls back to CLI only when API credentials are missing or unauthorized (401/403).
## Settings
- Preferences -> Providers -> Kilo:
- Usage source: `Auto`, `API`, `CLI`
- API key: optional override for `KILO_API_KEY`
- In auto mode, resolved CLI fetches can show a fallback note in menu and CLI output.
## CLI output notes
- Kilo text output splits identity into `Plan:` and `Activity:` lines.
- Auto-mode failures include ordered fallback-attempt details in text mode.
## Troubleshooting
- Missing API token: set `KILO_API_KEY` or provider `apiKey`.
- Missing CLI session file: run `kilo login` to create `~/.local/share/kilo/auth.json`.
- Unauthorized API token (401/403): refresh `KILO_API_KEY` or rerun `kilo login`.
## Organizations
CodexBar can show usage for any Kilo organization the API key belongs to.
- Open Preferences → Providers → Kilo, set the API key, then click **Refresh
organizations**.
- Toggle the organizations you want to display alongside Personal. Personal is
always shown.
- When at least one organization is enabled, the menu renders one Kilo card per
enabled scope.
- The CodexBar fetcher sends the standard `X-KILOCODE-ORGANIZATIONID` header on
every usage call to scope the response to that organization.
- CLI source mode (`auth.json`): the header is applied to CLI-resolved tokens
as well. If a CLI token isn't authorized for the chosen organization, that
card surfaces an unauthorized error while Personal and other enabled scopes
continue to render normally.
+45
View File
@@ -0,0 +1,45 @@
---
summary: "Kimi K2 provider data sources: API key + credit endpoint."
read_when:
- Adding or tweaking Kimi K2 usage parsing
- Updating API key handling or config migration
- Documenting new provider behavior
---
# Kimi K2 provider
> This is a legacy, unofficial provider for the `kimi-k2.ai` credit endpoint.
> For the official Kimi API account and billing surface, use the Moonshot / Kimi
> API provider instead.
Kimi K2 is API-only. Usage is reported by the credit counter behind
`GET https://kimi-k2.ai/api/user/credits`, so CodexBar only needs a valid API
key for that legacy endpoint to pull your remaining balance and usage.
## Data sources + fallback order
1) **API key** stored in `~/.codexbar/config.json` or supplied via `KIMI_K2_API_KEY` / `KIMI_API_KEY` / `KIMI_KEY`.
CodexBar stores the key in config after you paste it in Preferences → Providers → Kimi K2 (unofficial).
Surrounding whitespace is ignored before the key is sent.
2) **Credit endpoint**
- `GET https://kimi-k2.ai/api/user/credits`
- Request headers: `Authorization: Bearer <api key>`, `Accept: application/json`
- HTTP 401 is shown as an invalid or expired key; other failures keep the provider's response details.
- Response headers may include `X-Credits-Remaining`.
- JSON payload contains total credits consumed, credits remaining, and optional usage metadata.
CodexBar scans common keys and falls back to the remaining header when JSON omits it.
## Usage details
- Credits are the billing unit; CodexBar computes used percent as `consumed / (consumed + remaining)`.
- There is no explicit reset timestamp in the API, so the snapshot has no reset time.
- The menu's Usage Dashboard action opens the legacy service's human-facing credits page at `https://kimrel.com/my-credits`.
- A key saved in Settings takes precedence over environment variables.
## Key files
- `Sources/CodexBarCore/Providers/KimiK2/KimiK2ProviderDescriptor.swift` (descriptor + fetch strategy)
- `Sources/CodexBarCore/Providers/KimiK2/KimiK2UsageFetcher.swift` (HTTP client + parser)
- `Sources/CodexBarCore/Providers/KimiK2/KimiK2SettingsReader.swift` (env var parsing)
- `Sources/CodexBar/Providers/KimiK2/KimiK2ProviderImplementation.swift` (settings field + activation logic)
- `Sources/CodexBar/KimiK2TokenStore.swift` (legacy migration helper)
+192
View File
@@ -0,0 +1,192 @@
---
summary: "Kimi provider notes: cookie auth, quotas, and rate-limit parsing."
read_when:
- Adding or modifying the Kimi provider
- Debugging Kimi cookie import or usage parsing
- Adjusting Kimi menu labels or settings
---
# Kimi Provider
Tracks usage for [Kimi For Coding](https://www.kimi.com/code) in CodexBar.
## Features
- Displays weekly request quota (from membership tier)
- Shows current 5-hour rate limit usage
- API-key, Kimi Code CLI, automatic cookie, and manual cookie authentication methods
- Automatic refresh countdown
## Setup
Choose one of four authentication methods:
### Method 1: Kimi Code API Key (Recommended)
Create an API key in the [Kimi Code Console](https://www.kimi.com/code/console), then save it in CodexBar:
```bash
codexbar config set-api-key --provider kimi --api-key "kimi-api-key-here"
```
Or provide it through the environment:
```bash
export KIMI_CODE_API_KEY="kimi-code-api-key-here"
```
CodexBar calls `GET https://api.kimi.com/coding/v1/usages` with the API key. Set
`KIMI_CODE_BASE_URL` only when testing a compatible HTTPS proxy or alternate host with an explicit API key.
CodexBar never forwards a Kimi Code CLI credential to an endpoint override.
### Method 2: Kimi Code CLI
If you are signed in with the official Kimi Code CLI, Auto mode can reuse its fresh access token from
`~/.kimi-code/credentials/kimi-code.json`. CodexBar sends the same device identity headers as the CLI,
including the local hostname, OS details, and stable `~/.kimi-code/device_id` value. If that device ID is
missing, CodexBar creates it with private file permissions to match the official client.
CodexBar treats CLI-owned authentication as read-only: it never uses the refresh token and never rewrites
the credential file. When the access token expires, sign in again with Kimi Code CLI or configure an API
key. Set `KIMI_CODE_HOME` only when the official CLI uses a non-default home directory.
Custom `KIMI_CODE_BASE_URL`, `KIMI_CODE_OAUTH_HOST`, and `KIMI_OAUTH_HOST` values disable CLI credential
reuse; use an explicit API key for endpoint-override testing.
### Method 3: Automatic Browser Import
**No setup needed!** If you're already logged in to Kimi in Arc, Chrome, Safari, Edge, Brave, or Chromium:
1. Open CodexBar settings → Providers → Kimi
2. Set "Cookie source" to "Automatic"
3. Enable the Kimi provider toggle
4. CodexBar will automatically find your session
**Note**: Requires Full Disk Access to read browser cookies (System Settings → Privacy & Security → Full Disk Access → CodexBar).
### Method 4: Manual Token Entry
For advanced users or when automatic import fails:
1. Open CodexBar settings → Providers → Kimi
2. Set "Cookie source" to "Manual"
3. Visit `https://www.kimi.com/code/console` in your browser
4. Open Developer Tools (F12 or Cmd+Option+I)
5. Go to **Application****Cookies**
6. Copy the `kimi-auth` cookie value (JWT token)
7. Paste it into the "Auth Token" field in CodexBar
### Cookie Environment Variable
Alternatively, set the `KIMI_AUTH_TOKEN` environment variable:
```bash
export KIMI_AUTH_TOKEN="jwt-token-here"
```
## Authentication Priority
When multiple sources are available, CodexBar uses this order:
1. API key (`providers[].apiKey` or `KIMI_CODE_API_KEY`) in Auto mode
2. Fresh Kimi Code CLI access token (`~/.kimi-code/credentials/kimi-code.json`)
3. Manual cookie/token (from Settings UI) when web fallback is used
4. Cookie environment variable (`KIMI_AUTH_TOKEN`)
5. Browser cookies (Arc → Chrome → Safari → Edge → Brave → Chromium)
**Note**: Browser cookie import requires Full Disk Access permission.
## API Details
### Kimi Code API key
**Endpoint**: `GET https://api.kimi.com/coding/v1/usages`
**Authentication**: Bearer token (from `providers[].apiKey`, `KIMI_CODE_API_KEY`, or a fresh Kimi Code CLI credential)
**Response**:
```json
{
"usage": {
"limit": "2048",
"used": "214",
"remaining": "1834",
"resetTime": "2026-01-09T15:23:13.716839300Z"
},
"limits": [{
"window": {"duration": 300, "timeUnit": "TIME_UNIT_MINUTE"},
"detail": {
"limit": "200",
"used": "139",
"remaining": "61",
"resetTime": "2026-01-06T13:33:02.717479433Z"
}
}]
}
```
### Kimi web cookie fallback
**Endpoint**: `POST https://www.kimi.com/apiv2/kimi.gateway.billing.v1.BillingService/GetUsages`
**Authentication**: Bearer token (from `kimi-auth` cookie)
**Response**:
```json
{
"usages": [{
"scope": "FEATURE_CODING",
"detail": {
"limit": "2048",
"used": "214",
"remaining": "1834",
"resetTime": "2026-01-09T15:23:13.716839300Z"
},
"limits": [{
"window": {"duration": 300, "timeUnit": "TIME_UNIT_MINUTE"},
"detail": {
"limit": "200",
"used": "139",
"remaining": "61",
"resetTime": "2026-01-06T13:33:02.717479433Z"
}
}]
}]
}
```
## Membership Tiers
| Tier | Price | Weekly Quota |
|------|-------|--------------|
| Andante | ¥49/month | 1,024 requests |
| Moderato | ¥99/month | 2,048 requests |
| Allegretto | ¥199/month | 7,168 requests |
All tiers have a rate limit of 200 requests per 5 hours.
## Troubleshooting
### "Kimi auth token is missing"
- Ensure "Cookie source" is set correctly
- If using Automatic mode, verify you're logged in to Kimi in your browser
- Grant Full Disk Access permission if using browser cookies
- Try Manual mode and paste your token directly
### "Kimi auth token is invalid or expired"
- Your token has expired. Paste a new token from your browser
- If using Automatic mode, log in to Kimi again in your browser
### "No Kimi session cookies found"
- You're not logged in to Kimi in any supported browser
- Grant Full Disk Access to CodexBar in System Settings
### "Failed to parse Kimi usage data"
- The API response format may have changed. Please report this issue.
## Implementation
- **Core files**: `Sources/CodexBarCore/Providers/Kimi/`
- **UI files**: `Sources/CodexBar/Providers/Kimi/`
- **Login flow**: `Sources/CodexBar/KimiLoginRunner.swift`
- **Tests**: `Tests/CodexBarTests/KimiProviderTests.swift`
+58
View File
@@ -0,0 +1,58 @@
---
summary: "Kiro provider data sources: CLI-based usage via kiro-cli /usage command."
read_when:
- Debugging Kiro usage parsing
- Updating kiro-cli command behavior
- Reviewing Kiro credit window mapping
---
# Kiro provider
Kiro uses the AWS `kiro-cli` tool to fetch usage data. No browser cookies or OAuth flow—authentication is handled by AWS Builder ID through the CLI.
## Data sources
1) **CLI command** (primary and only strategy)
- Command: `kiro-cli chat --no-interactive "/usage"`
- Timeout: 20 seconds (idle cutoff after 4 seconds of no output once the CLI starts responding).
- CodexBar tries ordinary stdout/stderr pipes first for current Kiro CLI releases. Incomplete or unusable
pipe output falls back to a pseudo-terminal within the same overall command deadline for older releases.
- Requires `kiro-cli` installed and logged in via AWS Builder ID.
- Output is ANSI-decorated; CodexBar strips escape sequences before parsing.
## Output format (example)
```
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ | KIRO FREE ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃ Monthly credits: ┃
┃ ████████████████████████████████████████████████████████ 100% (resets on 01/01) ┃
┃ (0.00 of 50 covered in plan) ┃
┃ Bonus credits: ┃
┃ 0.00/100 credits used, expires in 88 days ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
```
## Snapshot mapping
- **Primary window**: Monthly credits percentage (bar meter).
- `usedPercent`: extracted from `███...█ X%` pattern.
- `resetsAt`: parsed from `resets on MM/DD` (assumes current or next year).
- **Secondary window**: Bonus credits (when present).
- Parsed from `Bonus credits: X.XX/Y credits used`.
- Expiry from `expires in N days`.
- **Identity**:
- `accountOrganization`: plan name (e.g., "KIRO FREE").
- `loginMethod`: plan name (used for menu display).
## Status
Kiro does not have a dedicated status page. The "View Status" link opens the AWS Health Dashboard:
- `https://health.aws.amazon.com/health/status`
## Key files
- `Sources/CodexBarCore/Providers/Kiro/KiroProviderDescriptor.swift`
- `Sources/CodexBarCore/Providers/Kiro/KiroStatusProbe.swift`
- `Sources/CodexBar/Providers/Kiro/KiroProviderImplementation.swift`
+54
View File
@@ -0,0 +1,54 @@
---
summary: "LiteLLM provider setup and usage data shape."
read_when:
- Configuring LiteLLM usage tracking
- Troubleshooting LiteLLM API-key usage in CodexBar
---
# LiteLLM
LiteLLM uses a virtual key plus the proxy base URL. The key reads its own identity and budget data through LiteLLM's
authenticated information endpoints.
Configure it in Settings -> Providers -> LiteLLM, or in `~/.codexbar/config.json`:
```json
{
"id": "litellm",
"enabled": true,
"apiKey": "<LITELLM_API_KEY>",
"enterpriseHost": "https://litellm.example.com"
}
```
Equivalent environment variables:
```bash
export LITELLM_API_KEY=sk-...
export LITELLM_BASE_URL=https://litellm.example.com
```
`LITELLM_BASE_URL` may include `/v1`; CodexBar strips that suffix before calling LiteLLM management endpoints.
## Data Source
The provider calls:
1. `GET /key/info` to discover the authenticated key's `user_id` and `team_id`.
2. `GET /user/info?user_id=<user_id>` to read personal spend, budget, and teams.
3. For team-only keys without a `user_id`, `GET /team/info?team_id=<team_id>` to read team spend and budget.
All requests use `Authorization: Bearer <apiKey>`. CodexBar does not request or store a LiteLLM master key.
For user-bound keys, personal usage is shown as the primary window. If the key has a team, its exact matching team
budget is shown as the secondary window and becomes the automatic menu bar metric because that budget is enforced for
the key. Team-only keys show that team budget as their sole usage window. Spend remains visible as an API-spend row
when LiteLLM does not configure a budget.
The virtual key must be allowed to read its own `/key/info` data and the corresponding user or team information
endpoint. CodexBar validates returned user and team IDs against `/key/info` before displaying usage.
## Security
Treat LiteLLM keys as secrets. CodexBar stores configured keys only in provider config or token-account storage and
sends them only to the configured LiteLLM base URL.
+41
View File
@@ -0,0 +1,41 @@
---
summary: "LLM Proxy provider setup and quota-stats usage source."
read_when:
- Configuring LLM Proxy usage tracking
- Debugging aggregate proxy quota or provider breakdown display
---
# LLM Proxy
CodexBar reads aggregate usage from an LLM-API-Key-Proxy compatible `/v1/quota-stats` endpoint.
## Setup
Store the API key:
```bash
printf '%s' "$LLM_PROXY_API_KEY" | codexbar config set-api-key --provider llmproxy --stdin
```
Set the base URL with `LLM_PROXY_BASE_URL`, or add `enterpriseHost` to the provider config:
```json
{
"id": "llmproxy",
"enabled": true,
"apiKey": "<REDACTED>",
"enterpriseHost": "https://proxy.example.com"
}
```
The base URL may point at either the service root or `/v1`; CodexBar normalizes both to `/v1/quota-stats`.
## Menu display
- Primary: lowest remaining quota group, rendered as percent used.
- Secondary: total requests.
- Tertiary: total tokens.
- Extra rows: top provider summaries by request count.
- Cost: approximate spend when the proxy reports `approx_cost`.
`quota_groups` may be either an array or a keyed object; CodexBar accepts both shapes.
+12
View File
@@ -0,0 +1,12 @@
# CodexBar
A tiny macOS menu bar app that tracks AI coding-provider usage windows, credits, costs, and resets across 59 providers — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, and more.
Canonical documentation:
- CodexBar — every AI coding limit, in your menu bar: https://codexbar.app/ - A tiny macOS menu bar app that tracks AI coding-provider usage windows, credits, costs, and resets across 59 providers — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, and more.
Source: https://github.com/steipete/CodexBar
Guidance for agents:
- Prefer the canonical documentation URLs above over README excerpts or package metadata.
- Fetch only the pages needed for the current task; this is an index, not a full-site corpus.
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus AI logo: stylized vertical lines with dots -->
<g transform="translate(15, 10)">
<!-- Vertical lines -->
<rect x="0" y="0" width="4" height="80" rx="2" fill="white"/>
<rect x="17.5" y="0" width="4" height="80" rx="2" fill="white"/>
<rect x="35" y="0" width="4" height="80" rx="2" fill="white"/>
<rect x="52.5" y="0" width="4" height="80" rx="2" fill="white"/>
<rect x="66" y="0" width="4" height="80" rx="2" fill="white"/>
<!-- Dots on the lines (beads on an abacus) -->
<circle cx="2" cy="20" r="7" fill="white"/>
<circle cx="19.5" cy="45" r="7" fill="white"/>
<circle cx="37" cy="30" r="7" fill="white"/>
<circle cx="54.5" cy="60" r="7" fill="white"/>
<circle cx="68" cy="40" r="7" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Alibaba</title><path d="M24 14.014c-2.8 1.512-5.62 2.896-8.759 3.524-.7.139-1.476.139-2.187.043-.678-.085-1.017-.682-.776-1.31.23-.585.536-1.181.93-1.671.852-1.065 1.814-2.034 2.678-3.088a15.75 15.75 0 001.422-2.054c.306-.511.164-1.129-.372-1.384-.897-.437-1.859-.745-2.81-1.075-.11-.043-.274.074-.492.149.273.244.47.425.743.67-2.821.48-5.49 1.16-8.08 2.098-.012.053-.033.095-.023.117.383.585.208 1.032-.35 1.394a2.365 2.365 0 00-.568.522c1.706.5 3.226.213 4.68-.735-.087-.127-.175-.244-.262-.372.546.096.874.394.918.862.011.107-.054.213-.087.32-.077-.086-.175-.17-.24-.267-.045-.064-.056-.138-.088-.245-1.728 1.15-3.587 1.438-5.632.842 0 .404-.022.745.011 1.075.022.287-.098.415-.36.564-.591.362-1.204.735-1.696 1.214-.59.585-.371 1.299.427 1.597.907.34 1.859.35 2.81.234 1.126-.139 2.23-.32 3.456-.49-1.433.67-2.844 1.14-4.33 1.33-1.04.14-2.078.214-3.106-.084-1.476-.415-2.133-1.501-1.75-2.96.361-1.363 1.236-2.449 2.176-3.45 3.139-3.332 7.108-5.024 11.7-5.365 1.072-.074 2.155.064 3.16.511 1.411.639 2.002 1.99 1.313 3.354-.448.905-1.072 1.735-1.695 2.555-.612.809-1.301 1.554-1.946 2.331-.186.234-.361.48-.503.745-.274.5-.088.83.492.778 1.213-.118 2.45-.213 3.62-.511 1.716-.437 3.389-1.054 5.084-1.597.175-.043.339-.107.492-.17z" fill="#FF6003" fill-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg viewBox="0 0 281 124" fill="#1A1A1A" xmlns="http://www.w3.org/2000/svg"><path d="M236.014 0C260.431 9.71106e-05 280.602 17.4115 280.603 44.7432C280.602 73.5337 260.065 94.1657 233.52 94.166C224.158 94.166 215.639 92.4222 208.63 88.4902C202.886 85.2698 198.203 80.6054 194.919 74.3379L188.115 121.822L187.946 123.016H174.214L174.448 121.423L191.772 2.49414H205.372L203.937 11.3369C212.143 3.86078 223.2 0.000153635 236.014 0ZM47.082 0.154297C56.4435 0.154297 65.0012 1.8991 72.0488 5.84863C77.8222 9.08305 82.5323 13.7713 85.8271 20.085L88.1201 3.69238L88.2861 2.49316H101.863L89.1611 90.6328L88.9873 91.8262H75.4092L76.7227 82.8555C68.5854 90.4564 57.3981 94.3231 44.5889 94.3232C20.1709 94.3232 0.000167223 76.9087 0 49.5771C0.000149745 20.7854 20.54 0.154871 47.082 0.154297ZM116.234 90.6357L116.061 91.8271H102.485L115.351 3.68555L115.521 2.49414H129.083L116.234 90.6357ZM140.673 90.6357L140.499 91.8271H126.924L139.789 3.68555L139.96 2.49414H153.521L140.673 90.6357ZM177.958 2.49414L165.108 90.6357L164.935 91.8271H151.36L164.225 3.68555L164.396 2.49414H177.958ZM48.4854 11.9844C27.8638 11.985 14.0133 28.3799 14.0127 48.9521C14.0127 57.7907 16.8094 66.1771 22.3145 72.334C27.7973 78.4657 36.0631 82.4932 47.2402 82.4932C67.8534 82.4925 81.7122 65.9487 81.7129 45.3682C81.7129 35.4076 78.2493 27.0792 72.4131 21.2441C66.5794 15.4088 58.2871 11.9844 48.4854 11.9844ZM233.362 11.8291C212.749 11.8297 198.89 28.3716 198.89 48.9521C198.89 58.9123 202.356 67.2403 208.189 73.0742C214.023 78.9107 222.315 82.3358 232.116 82.3359C252.738 82.3355 266.589 65.9407 266.59 45.3682C266.59 36.5296 263.795 28.1424 258.29 21.9863C252.807 15.8551 244.542 11.8291 233.362 11.8291Z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.5 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none">
<path fill="#ffffff" d="M78.844 464.762c-8.453 0-15.573-1.451-21.359-4.339-5.77-2.888-10.144-7.289-13.076-13.095-2.932-5.807-4.436-12.912-4.436-21.255v-86.028c0-10.605-2.125-18.321-6.329-23.135-4.234-4.798-11.742-7.334-22.507-7.579-3.35 0-6.034-1.253-8.066-3.804C1.008 303.005 0 300.087 0 296.832c0-3.53 1.008-6.448 3.071-8.725 2.048-2.277 4.762-3.53 8.066-3.774 10.765-.26 18.273-2.781 22.507-7.579 4.235-4.798 6.329-12.392 6.329-22.752v-86.028c0-12.637 3.35-22.249 10.005-28.804 6.654-6.555 16.287-9.856 28.866-9.856H181.5c3.862 0 7.042 1.146 9.617 3.408 2.559 2.277 3.862 5.195 3.862 8.694 0 3.301-1.086 6.128-3.257 8.542-2.172 2.414-5.057 3.622-8.671 3.622H87.732c-5.413 0-9.508 1.39-12.316 4.171-2.823 2.781-4.234 7.075-4.234 12.912v86.425c0 7.579-1.551 14.455-4.623 20.644-3.07 6.204-7.181 11.063-12.316 14.623-5.134 3.53-11.137 5.302-18.07 5.302v-1.528c6.933 0 12.936 1.773 18.07 5.303 5.135 3.529 9.245 8.404 12.316 14.623 3.072 6.188 4.623 13.064 4.623 20.643v86.808c0 5.837 1.411 10.115 4.234 12.911 2.823 2.812 6.934 4.172 12.316 4.172h95.318c3.583 0 6.468 1.207 8.671 3.606 2.202 2.414 3.257 5.257 3.257 8.542s-1.272 6.097-3.862 8.511c-2.575 2.414-5.771 3.606-9.617 3.606H78.844v-.092ZM330.501 464.768c-3.862 0-7.042-1.207-9.617-3.606-2.575-2.414-3.863-5.256-3.863-8.511 0-3.255 1.086-6.128 3.258-8.542 2.171-2.414 5.057-3.606 8.671-3.606h95.317c5.414 0 9.509-1.36 12.316-4.171 2.823-2.781 4.235-7.075 4.235-12.912v-86.808c0-7.579 1.551-14.455 4.622-20.643 3.071-6.204 7.182-11.063 12.316-14.623 5.134-3.53 11.137-5.303 18.071-5.303v1.528c-6.934 0-12.937-1.772-18.071-5.302-5.134-3.53-9.245-8.404-12.316-14.623-3.071-6.189-4.622-13.065-4.622-20.644v-86.425c0-5.807-1.412-10.1-4.235-12.912-2.823-2.781-6.933-4.171-12.316-4.171H328.95c-3.583 0-6.469-1.208-8.671-3.622-2.172-2.384-3.258-5.241-3.258-8.542 0-3.529 1.272-6.417 3.863-8.694 2.559-2.277 5.755-3.407 9.617-3.407h102.654c12.58 0 22.181 3.3 28.867 9.855 6.685 6.556 10.005 16.167 10.005 28.804v86.028c0 10.36 2.125 17.969 6.328 22.752 4.235 4.798 11.742 7.334 22.507 7.579 3.351.244 6.034 1.497 8.066 3.774 2.063 2.277 3.071 5.195 3.071 8.725 0 3.301-1.008 6.189-3.071 8.695-2.032 2.521-4.762 3.804-8.066 3.804-10.765.245-18.257 2.781-22.507 7.579-4.234 4.798-6.328 12.5-6.328 23.135v86.028c0 8.358-1.474 15.418-4.437 21.255-2.962 5.837-7.305 10.176-13.076 13.095-5.785 2.888-12.905 4.339-21.359 4.339H330.501v.092Z"/>
<path fill="#ffffff" d="M356.885 329.738c18.691 0 33.846-14.929 33.846-33.342 0-18.412-15.155-33.341-33.846-33.341-18.691 0-33.846 14.929-33.846 33.341 0 18.413 15.155 33.342 33.846 33.342ZM167.305 329.738c18.691 0 33.846-14.929 33.846-33.342 0-18.412-15.155-33.341-33.846-33.341-18.691 0-33.846 14.929-33.846 33.341 0 18.413 15.155 33.342 33.846 33.342ZM244.477 32.846l-2.59 68.135c0 3.82-3.661 5.73-10.983 5.73-7.321 0-10.982-1.91-10.982-5.73-.651-16.976-1.178-30.148-1.613-39.484-.217-9.55-.434-16.35-.651-20.384-.217-4.034-.326-6.479-.326-7.32v-1.268c0-4.874 4.529-7.319 13.572-7.319 9.044 0 13.573 2.552 13.573 7.64Zm54.941 0-2.59 68.135c0 3.82-3.661 5.73-10.982 5.73-7.322 0-10.982-1.91-10.982-5.73-.652-16.976-1.179-30.148-1.613-39.484-.218-9.55-.435-16.35-.652-20.384-.217-4.034-.326-6.479-.326-7.32v-1.268c0-4.874 4.53-7.319 13.573-7.319s13.572 2.552 13.572 7.64Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg viewBox="0 0 32 32" fill="#3184FF" xmlns="http://www.w3.org/2000/svg">
<title>Chutes</title>
<path d="M16 3c-6.1 0-11 4.9-11 11h5c0-3.3 2.7-6 6-6s6 2.7 6 6h5c0-6.1-4.9-11-11-11Z"/>
<path d="M11 14h10l-5 8-5-8Z"/>
<path d="M15 21h2v5h-2v-5Z"/>
<path d="M11 26h10v3H11v-3Z"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

+1
View File
@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fill="#D97757" fill-rule="nonzero"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M50 8L12 28V72L50 92L88 72V28L50 8ZM50 22.5L74 35.25V64.75L50 77.5L26 64.75V35.25L50 22.5Z" fill="#44FF00"/>
<path d="M50 40L61 46V54L50 60L39 54V46L50 40Z" fill="#44FF00"/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<rect width="24" height="24" rx="4" fill="#44C800"/>
<path d="M7 8l-3 4 3 4M17 8l3 4-3 4M14 6l-4 12" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 265 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M83.7733 42.8087C84.6678 40.1149 84.9771 37.2613 84.6807 34.4385C84.3843 31.6156 83.489 28.8885 82.0544 26.4394C77.6908 18.8436 68.9203 14.9365 60.3548 16.7725C57.9831 14.1344 54.9591 12.1668 51.5864 11.0673C48.2137 9.96772 44.611 9.77498 41.1402 10.5084C37.6694 11.2418 34.4527 12.8755 31.8132 15.2455C29.1736 17.6155 27.204 20.6383 26.1024 24.0103C23.3212 24.5806 20.6938 25.738 18.3958 27.405C16.0977 29.0721 14.1819 31.2104 12.7765 33.6772C8.36538 41.2609 9.3669 50.8267 15.2527 57.3327C14.3549 60.0251 14.0424 62.8782 14.3361 65.7012C14.6298 68.5241 15.523 71.2518 16.9558 73.7017C21.325 81.3002 30.1011 85.207 38.6712 83.3686C40.5554 85.4904 42.8707 87.1858 45.4623 88.3416C48.0539 89.4975 50.8622 90.0871 53.6999 90.0713C62.4793 90.079 70.2575 84.4114 72.9393 76.0515C75.7201 75.4802 78.347 74.3225 80.6449 72.6555C82.9427 70.9886 84.8587 68.8507 86.2649 66.3846C90.6227 58.8145 89.6172 49.3005 83.7733 42.8087ZM53.6999 84.8356C50.1955 84.8411 46.801 83.6129 44.1116 81.3661L44.5848 81.098L60.5123 71.9043C60.9087 71.6718 61.2379 71.3402 61.4674 70.942C61.6969 70.5439 61.8189 70.0929 61.8215 69.6333V47.1769L68.5553 51.072C68.6225 51.1063 68.6694 51.1707 68.6814 51.2456V69.854C68.6641 78.1208 61.9667 84.8183 53.6999 84.8356ZM21.4977 71.0843C19.7402 68.0497 19.1092 64.4925 19.7156 61.0386L20.1885 61.3225L36.1321 70.5165C36.5266 70.748 36.9757 70.87 37.4331 70.87C37.8905 70.87 38.3396 70.748 38.7341 70.5165L58.21 59.2883V67.0628C58.2081 67.1031 58.1973 67.1424 58.1782 67.1779C58.1591 67.2134 58.1322 67.2441 58.0996 67.2678L41.9671 76.5722C34.798 80.7022 25.6388 78.2463 21.4977 71.0843ZM17.3026 36.3898C19.0723 33.3357 21.8655 31.0062 25.1878 29.8138V48.7376C25.1818 49.1949 25.2986 49.6453 25.5261 50.042C25.7535 50.4387 26.0833 50.7671 26.4809 50.9928L45.8622 62.1739L39.1283 66.069C39.0919 66.0883 39.0513 66.0984 39.0101 66.0984C38.9689 66.0984 38.9283 66.0883 38.8919 66.069L22.7908 56.7809C15.6359 52.6337 13.1822 43.4816 17.3026 36.3112V36.3898ZM72.624 49.2426L53.1792 37.9512L59.8976 34.0718C59.9341 34.0524 59.9747 34.0423 60.016 34.0423C60.0573 34.0423 60.0979 34.0524 60.1344 34.0718L76.2355 43.3761C78.6973 44.7966 80.7043 46.8882 82.0221 49.4065C83.3398 51.9249 83.914 54.7661 83.6775 57.5985C83.4411 60.431 82.4038 63.1377 80.6867 65.4027C78.9696 67.6677 76.6436 69.3975 73.9803 70.3901V51.466C73.9663 51.0096 73.834 50.5647 73.5962 50.1749C73.3584 49.7851 73.0234 49.4638 72.624 49.2426ZM79.3261 39.1657L78.8529 38.8815L62.9411 29.6089C62.5442 29.376 62.0924 29.2532 61.6322 29.2532C61.172 29.2532 60.7202 29.376 60.3233 29.6089L40.8629 40.8374V33.0628C40.8587 33.0233 40.8654 32.9834 40.882 32.9473C40.8987 32.9113 40.9248 32.8803 40.9575 32.8579L57.0586 23.5692C59.5263 22.1476 62.3478 21.458 65.193 21.5811C68.0382 21.7042 70.7896 22.6348 73.1253 24.2642C75.461 25.8936 77.2845 28.1543 78.3825 30.782C79.4806 33.4097 79.8077 36.2957 79.3257 39.1025V39.1657H79.3261ZM37.1888 52.9484L30.455 49.069C30.4213 49.0487 30.3925 49.0212 30.3707 48.9884C30.3488 48.9557 30.3345 48.9186 30.3286 48.8797V30.3188C30.3323 27.4714 31.1466 24.6839 32.6761 22.2822C34.2057 19.8805 36.3874 17.9639 38.9661 16.7564C41.5448 15.549 44.4139 15.1005 47.2381 15.4636C50.0622 15.8267 52.7247 16.9862 54.9141 18.8067L54.4409 19.0748L38.5134 28.2686C38.117 28.5011 37.7879 28.8327 37.5584 29.2308C37.329 29.629 37.207 30.0799 37.2045 30.5395L37.1888 52.9487V52.9484ZM40.8472 45.0632L49.5209 40.0643L58.21 45.0635V55.0615L49.5523 60.0608L40.8632 55.0615L40.8472 45.0632Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Codex</title><path d="M19.503 0H4.496A4.496 4.496 0 000 4.496v15.007A4.496 4.496 0 004.496 24h15.007A4.496 4.496 0 0024 19.503V4.496A4.496 4.496 0 0019.503 0z" fill="#fff"></path><path d="M9.064 3.344a4.578 4.578 0 012.285-.312c1 .115 1.891.54 2.673 1.275.01.01.024.017.037.021a.09.09 0 00.043 0 4.55 4.55 0 013.046.275l.047.022.116.057a4.581 4.581 0 012.188 2.399c.209.51.313 1.041.315 1.595a4.24 4.24 0 01-.134 1.223.123.123 0 00.03.115c.594.607.988 1.33 1.183 2.17.289 1.425-.007 2.71-.887 3.854l-.136.166a4.548 4.548 0 01-2.201 1.388.123.123 0 00-.081.076c-.191.551-.383 1.023-.74 1.494-.9 1.187-2.222 1.846-3.711 1.838-1.187-.006-2.239-.44-3.157-1.302a.107.107 0 00-.105-.024c-.388.125-.78.143-1.204.138a4.441 4.441 0 01-1.945-.466 4.544 4.544 0 01-1.61-1.335c-.152-.202-.303-.392-.414-.617a5.81 5.81 0 01-.37-.961 4.582 4.582 0 01-.014-2.298.124.124 0 00.006-.056.085.085 0 00-.027-.048 4.467 4.467 0 01-1.034-1.651 3.896 3.896 0 01-.251-1.192 5.189 5.189 0 01.141-1.6c.337-1.112.982-1.985 1.933-2.618.212-.141.413-.251.601-.33.215-.089.43-.164.646-.227a.098.098 0 00.065-.066 4.51 4.51 0 01.829-1.615 4.535 4.535 0 011.837-1.388zm3.482 10.565a.637.637 0 000 1.272h3.636a.637.637 0 100-1.272h-3.636zM8.462 9.23a.637.637 0 00-1.106.631l1.272 2.224-1.266 2.136a.636.636 0 101.095.649l1.454-2.455a.636.636 0 00.005-.64L8.462 9.23z" fill="url(#lobe-icons-codex-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-codex-_R_0_" x1="12" x2="12" y1="3" y2="21"><stop stop-color="#B1A7FF"></stop><stop offset=".5" stop-color="#7A9DFF"></stop><stop offset="1" stop-color="#3941FF"></stop></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="#1F2328" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>GithubCopilot</title><path d="M19.245 5.364c1.322 1.36 1.877 3.216 2.11 5.817.622 0 1.2.135 1.592.654l.73.964c.21.278.323.61.323.955v2.62c0 .339-.173.669-.453.868C20.239 19.602 16.157 21.5 12 21.5c-4.6 0-9.205-2.583-11.547-4.258-.28-.2-.452-.53-.453-.868v-2.62c0-.345.113-.679.321-.956l.73-.963c.392-.517.974-.654 1.593-.654l.029-.297c.25-2.446.81-4.213 2.082-5.52 2.461-2.54 5.71-2.851 7.146-2.864h.198c1.436.013 4.685.323 7.146 2.864zm-7.244 4.328c-.284 0-.613.016-.962.05-.123.447-.305.85-.57 1.108-1.05 1.023-2.316 1.18-2.994 1.18-.638 0-1.306-.13-1.851-.464-.516.165-1.012.403-1.044.996a65.882 65.882 0 00-.063 2.884l-.002.48c-.002.563-.005 1.126-.013 1.69.002.326.204.63.51.765 2.482 1.102 4.83 1.657 6.99 1.657 2.156 0 4.504-.555 6.985-1.657a.854.854 0 00.51-.766c.03-1.682.006-3.372-.076-5.053-.031-.596-.528-.83-1.046-.996-.546.333-1.212.464-1.85.464-.677 0-1.942-.157-2.993-1.18-.266-.258-.447-.661-.57-1.108-.32-.032-.64-.049-.96-.05zm-2.525 4.013c.539 0 .976.426.976.95v1.753c0 .525-.437.95-.976.95a.964.964 0 01-.976-.95v-1.752c0-.525.437-.951.976-.951zm5 0c.539 0 .976.426.976.95v1.753c0 .525-.437.95-.976.95a.964.964 0 01-.976-.95v-1.752c0-.525.437-.951.976-.951zM7.635 5.087c-1.05.102-1.935.438-2.385.906-.975 1.037-.765 3.668-.21 4.224.405.394 1.17.657 1.995.657h.09c.649-.013 1.785-.176 2.73-1.11.435-.41.705-1.433.675-2.47-.03-.834-.27-1.52-.63-1.813-.39-.336-1.275-.482-2.265-.394zm6.465.394c-.36.292-.6.98-.63 1.813-.03 1.037.24 2.06.675 2.47.968.957 2.136 1.104 2.776 1.11h.044c.825 0 1.59-.263 1.995-.657.555-.556.765-3.187-.21-4.224-.45-.468-1.335-.804-2.385-.906-.99-.088-1.875.058-2.265.394zM12 7.615c-.24 0-.525.015-.84.044.03.16.045.336.06.526l-.001.159a2.94 2.94 0 01-.014.25c.225-.022.425-.027.612-.028h.366c.187 0 .387.006.612.028-.015-.146-.015-.277-.015-.409.015-.19.03-.365.06-.526a9.29 9.29 0 00-.84-.044z"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="#1A1A1A" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cursor</title><path d="M22.106 5.68L12.5.135a.998.998 0 00-.998 0L1.893 5.68a.84.84 0 00-.419.726v11.186c0 .3.16.577.42.727l9.607 5.547a.999.999 0 00.998 0l9.608-5.547a.84.84 0 00.42-.727V6.407a.84.84 0 00-.42-.726zm-.603 1.176L12.228 22.92c-.063.108-.228.064-.228-.061V12.34a.59.59 0 00-.295-.51l-9.11-5.26c-.107-.062-.063-.228.062-.228h18.55c.264 0 .428.286.296.514z"></path></svg>

After

Width:  |  Height:  |  Size: 542 B

+1
View File
@@ -0,0 +1 @@
<svg fill="#5786FE" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>DeepSeek</title><path d="M23.748 4.651c-.254-.124-.364.113-.512.233-.051.04-.094.09-.137.137-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.155-.708-.311-.955-.65-.172-.24-.219-.509-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.094.172.187.129.323-.082.28-.18.553-.266.833-.055.179-.137.218-.328.14a5.5 5.5 0 0 1-1.737-1.179c-.857-.828-1.631-1.743-2.597-2.46a12 12 0 0 0-.689-.47c-.985-.957.13-1.743.387-1.836.27-.098.094-.433-.778-.428-.872.003-1.67.295-2.687.685a3 3 0 0 1-.465.136 9.6 9.6 0 0 0-2.883-.101c-1.885.21-3.39 1.1-4.497 2.622C.082 8.776-.231 10.854.152 13.02c.403 2.284 1.568 4.175 3.36 5.653 1.857 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.132-.284 4.994-1.86.47.234.962.328 1.78.398.629.058 1.235-.031 1.705-.129.735-.155.684-.836.418-.961-2.155-1.004-1.682-.595-2.112-.926 1.095-1.295 2.768-3.598 3.284-6.733.05-.346.115-.834.108-1.114-.004-.171.035-.238.23-.257a4.2 4.2 0 0 0 1.545-.475c1.397-.763 1.96-2.016 2.093-3.517.02-.23-.004-.467-.247-.588M11.58 18.168c-2.088-1.642-3.101-2.183-3.52-2.16-.39.024-.32.472-.234.763.09.288.207.487.371.74.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.168-1.361-.801-2.5-1.86-3.301-3.306-.775-1.393-1.225-2.888-1.299-4.482-.02-.385.094-.522.477-.592a4.7 4.7 0 0 1 1.53-.038c2.131.311 3.946 1.264 5.467 2.774.868.86 1.525 1.887 2.202 2.89.72 1.066 1.494 2.082 2.48 2.915.348.291.626.513.892.677-.802.09-2.14.109-3.055-.615zm1.001-6.44a.306.306 0 0 1 .415-.287.3.3 0 0 1 .113.074.3.3 0 0 1 .086.214c0 .17-.136.307-.308.307a.303.303 0 0 1-.306-.307m3.11 1.596c-.2.081-.4.151-.591.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.551-.758a1.7 1.7 0 0 1 .015-.588c.07-.327-.007-.537-.238-.727-.188-.156-.426-.199-.689-.199a.6.6 0 0 1-.254-.078.253.253 0 0 1-.114-.358 1 1 0 0 1 .192-.21c.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.392.451.462.576.685.915.176.264.336.536.446.848.066.194-.02.353-.25.45"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2.25c1.18 0 2.18.76 2.54 1.82a8.1 8.1 0 0 1 2.04.85 2.68 2.68 0 0 1 3.08.5 2.68 2.68 0 0 1 .5 3.08c.4.64.69 1.33.85 2.04A2.68 2.68 0 0 1 22.75 13c0 1.18-.76 2.18-1.82 2.54a8.1 8.1 0 0 1-.85 2.04 2.68 2.68 0 0 1-.5 3.08 2.68 2.68 0 0 1-3.08.5 8.1 8.1 0 0 1-2.04.85A2.68 2.68 0 0 1 12 23.75a2.68 2.68 0 0 1-2.54-1.82 8.1 8.1 0 0 1-2.04-.85 2.68 2.68 0 0 1-3.08-.5 2.68 2.68 0 0 1-.5-3.08 8.1 8.1 0 0 1-.85-2.04A2.68 2.68 0 0 1 1.25 13c0-1.18.76-2.18 1.82-2.54.16-.71.45-1.4.85-2.04a2.68 2.68 0 0 1 .5-3.08 2.68 2.68 0 0 1 3.08-.5c.64-.4 1.33-.69 2.04-.85A2.68 2.68 0 0 1 12 2.25Zm0 3.5a6.25 6.25 0 1 0 0 12.5 6.25 6.25 0 0 0 0-12.5Zm0 3.25a3 3 0 1 1 0 6 3 3 0 0 1 0-6Z"/>
</svg>

After

Width:  |  Height:  |  Size: 773 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg viewBox="0 0 32 32" fill="#0a0a0c" xmlns="http://www.w3.org/2000/svg">
<path d="M7 6h4v20H7V6Zm7 0h4v20h-4V6Zm7 0h4v20h-4V6Z"/>
<path d="M3.5 15h25v2h-25v-2Z" opacity=".35"/>
</svg>

After

Width:  |  Height:  |  Size: 191 B

+1
View File
@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="#3186FF"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-0-_R_0_)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-1-_R_0_)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-2-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-0-_R_0_" x1="7" x2="11" y1="15.5" y2="12"><stop stop-color="#08B962"></stop><stop offset="1" stop-color="#08B962" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-1-_R_0_" x1="8" x2="11.5" y1="5.5" y2="11"><stop stop-color="#F94543"></stop><stop offset="1" stop-color="#F94543" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-2-_R_0_" x1="3.5" x2="17.5" y1="13.5" y2="12"><stop stop-color="#FABC12"></stop><stop offset=".46" stop-color="#FABC12" stop-opacity="0"></stop></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+1
View File
@@ -0,0 +1 @@
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="g" x1="0.5" y1="0" x2="0.5" y2="1"><stop offset="0%" stop-color="#B97AF7"/><stop offset="100%" stop-color="#6B4FD8"/></linearGradient></defs><path fill="url(#g)" d="M11.64 18c1.48 0 3.12-.3 4.58-.88.54-.22.78-.42.78-.88 0-.38-.4-.84-1.18-.52-1.28.54-2.82.78-4.18.78-4.98 0-8.14-2.32-8.14-5.46 0-2.5 2.18-4.28 5.08-4.28 2.4 0 4.08 1 4.08 2.24 0 1.08-1.1 1.6-2.64 1l.34-1.02C7.96 8 5.98 9.12 5.98 11c0 2.1 2.32 3.5 5.52 3.5 3.76 0 6.5-2.32 6.5-5.54C18 4.94 14.2 2 8.36 2c-1.48 0-3.12.26-4.58.88-.52.22-.78.48-.78.86 0 .46.46.84 1.18.54C5.46 3.74 7 3.5 8.36 3.5c4.98 0 8.14 2.32 8.14 5.46 0 2.5-2.18 4.28-5.08 4.28-2.4 0-4.08-1-4.08-2.24 0-1.08 1.1-1.6 2.64-1l-.34 1.02c2.4.98 4.38-.14 4.38-2.02 0-2.1-2.32-3.5-5.52-3.5C4.74 5.5 2 7.82 2 11.04 2 15.06 5.8 18 11.64 18"/></svg>

After

Width:  |  Height:  |  Size: 859 B

+1
View File
@@ -0,0 +1 @@
<svg fill="#f8f676" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Kilo Code</title><path d="M0 0v24h24V0H0zm22.222 22.222H1.778V1.778h20.444v20.444zm-7.555-4.964h2.222v1.778h-2.794L12.89 17.83v-2.794h1.778v2.222zm4 0h-1.778v-2.222h-2.222v-1.778h2.793l1.207 1.207v2.793zm-7.556-2.591H9.333v-1.778h1.778v1.778zm-5.778-1.778h1.778v4h4v1.778H6.54L5.333 17.46V12.89zm13.334-3.556v1.778h-5.778V9.333h1.987V7.111h-1.987V5.333h2.558l1.206 1.207v2.793h2.014zm-11.556-2h2.222l1.778 1.778v2H9.333v-2H7.111v2H5.333V5.333h1.778v2zm4 0H9.333v-2h1.778v2z"></path></svg>

After

Width:  |  Height:  |  Size: 647 B

Some files were not shown because too many files have changed in this diff Show More