Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions contrib/swiftbar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# SwiftBar Quota Plugin (macOS)

A menu bar widget for [SwiftBar](https://github.com/swiftbar/SwiftBar) that shows
per-account Codex quota from the local codex-multi-auth cache.

```text
⚡88·[90] ← menu bar title: 5h-window remaining % per account,
brackets mark the account currently serving requests
```

Opening the menu renders one card per managed account:

```text
╭──────────────────────────────────╮
│ ● account-a ACTIVE │
│ 5h █████████░ 88% → 4h 42m │
│ 7d ████████░░ 83% → 6d 18h │
╰──────────────────────────────────╯
╭──────────────────────────────────╮
│ ○ account-b IDLE │
│ 5h █████████░ 90% → 4h 33m │
│ 7d █████████░ 86% → 8h 16m │
╰──────────────────────────────────╯
```

- Green card / `●` / `ACTIVE`: the account the runtime rotation router last
served a request with (falls back to the stored active index).
- `5h` / `7d`: the two Codex quota windows — bar, remaining percent, and a
countdown to the window reset (`4h 42m` style).
- Rows turn orange below 30% remaining and red below 10%.

## Install

```bash
brew install --cask swiftbar # if not installed
mkdir -p ~/.swiftbar-plugins
cp contrib/swiftbar/codex-quota.5m.sh ~/.swiftbar-plugins/
chmod +x ~/.swiftbar-plugins/codex-quota.5m.sh
open -a SwiftBar # pick ~/.swiftbar-plugins as the plugin folder
```

## Data source and refresh model

The plugin reads the local quota cache (`quota-cache.json`), account store, and
runtime observability files under `~/.codex/multi-auth/` (or
`CODEX_MULTI_AUTH_DIR`). Reading the cache costs **zero quota**: the cache is
updated passively from the rate-limit headers of real Codex traffic flowing
through the rotation proxy, and by explicit live checks.

- The `5m` in the filename is SwiftBar's re-read interval (cache only).
- The menu re-reads the cache every time it is opened (`refreshOnOpen`).
- **Live refresh** in the menu runs `codex-multi-auth check`, which sends one
minimal probe per account and consumes a small amount of quota.

## Notes

- macOS only (SwiftBar). Card borders require a Menlo-native glyph set; if you
edit the card interior, avoid CJK or emoji — they render in a fallback font
with non-integer widths and break the right border alignment.
- The cache file formats are internal to codex-multi-auth and may change
between versions; the plugin fails soft (shows `⚡?`) when they do.
- Account names shown are the local-part of each account email.
149 changes: 149 additions & 0 deletions contrib/swiftbar/codex-quota.5m.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/bin/bash
# <swiftbar.title>Codex Multi-Auth Quota</swiftbar.title>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# <swiftbar.version>v1.0</swiftbar.version>
# <swiftbar.author>codex-multi-auth contributors</swiftbar.author>
# <swiftbar.about>macOS menu bar cards for codex-multi-auth account quota (5h/7d windows, active-account marker, reset countdowns)</swiftbar.about>
# <swiftbar.dependencies>codex-multi-auth,python3</swiftbar.dependencies>
# <swiftbar.refreshOnOpen>true</swiftbar.refreshOnOpen>
# <swiftbar.runInBash>true</swiftbar.runInBash>
#
# Reads the local codex-multi-auth quota cache (zero quota cost). The cache is
# written by quota-bearing commands (`check`, `forecast --live`, the interactive
# dashboard); the "Live refresh" menu item runs one such check, sending a
# minimal probe per account.

if [ "$(uname -s)" != "Darwin" ]; then
echo "⚡"
echo "---"
echo "Codex quota plugin requires macOS (SwiftBar) | color=gray"
exit 0
fi

if [ "$1" = "livecheck" ]; then
export PATH="/usr/local/bin:/opt/homebrew/bin:$HOME/.npm-global/bin:$PATH"
codex-multi-auth check
exit $?
fi

SELF="$0"

/usr/bin/python3 - "$SELF" <<'PYEOF'
import json, os, sys, time

SELF = sys.argv[1]
HOME = os.path.expanduser("~")
DATA_DIR = os.environ.get("CODEX_MULTI_AUTH_DIR") or os.path.join(HOME, ".codex", "multi-auth")
CACHE = os.path.join(DATA_DIR, "quota-cache.json")
STORE = os.path.join(DATA_DIR, "openai-codex-accounts.json")
OBSERV = os.path.join(DATA_DIR, "runtime-observability.json")

GREEN = "#34C759"
GRAY = "#9A9A9E"
RED = "#FF3B30"
ORANGE = "#FF9F0A"
# Card interior must stay within Menlo-native glyphs (ASCII, box drawing,
# block elements, arrows, geometric circles). CJK or emoji fall back to other
# fonts with non-integer widths and break the right border alignment.
W = 32

def load(path):
try:
with open(path) as f:
return json.load(f)
except Exception:
return None

def pad(s, width):
return s + " " * max(0, width - len(s))

def fmt_reset(ms):
left = int(ms / 1000 - time.time())
if left <= 0:
return "now"
d, r = divmod(left, 86400)
h, r = divmod(r, 3600)
m = r // 60
if d > 0:
return f"{d}d {h}h"
if h > 0:
return f"{h}h {m}m"
return f"{m}m"

def clamp_pct(value):
try:
return max(0, min(100, int(value)))
except (TypeError, ValueError):
return 0

def bar(remaining, slots=10):
filled = round(remaining / 100 * slots)
return "█" * filled + "░" * (slots - filled)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
def quota_color(rem):
if rem < 10: return RED
if rem < 30: return ORANGE
return None

cache = load(CACHE)
store = load(STORE)
if not cache or not store:
print("⚡?")
print("---")
print("Cannot read quota cache or account store | color=red")
print(f"Expected under: {DATA_DIR} | size=11 color=gray")
sys.exit(0)

emails, order = {}, []
for acc in store.get("accounts", []):
aid = acc.get("accountId", "")
emails[aid] = acc.get("email", aid[-6:] if aid else "?")
order.append(aid)

by_id = cache.get("byAccountId", {})

observ = load(OBSERV) or {}
Comment on lines +87 to +104

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Concurrent reads across three separate files

quota-cache.json, openai-codex-accounts.json, and runtime-observability.json are opened sequentially with no advisory lock. the rotation proxy can write any of these between reads, so order (from the accounts store) and by_id (from the quota cache) can be momentarily inconsistent — a freshly-added account appears in order but has no entry in by_id, producing a "?" quota row. the load() except guard handles full parse failures, but not this cross-file ordering gap. for a 5 m plugin this is cosmetic, but flagging explicitly since the codebase has active concurrent writers.

Prompt To Fix With AI
This is a comment left during a code review.
Path: contrib/swiftbar/codex-quota.5m.sh
Line: 73-90

Comment:
**Concurrent reads across three separate files**

`quota-cache.json`, `openai-codex-accounts.json`, and `runtime-observability.json` are opened sequentially with no advisory lock. the rotation proxy can write any of these between reads, so `order` (from the accounts store) and `by_id` (from the quota cache) can be momentarily inconsistent — a freshly-added account appears in `order` but has no entry in `by_id`, producing a `"?"` quota row. the `load()` except guard handles full parse failures, but not this cross-file ordering gap. for a 5 m plugin this is cosmetic, but flagging explicitly since the codebase has active concurrent writers.

How can I resolve this? If you propose a fix, please make it concise.

active_id = observ.get("lastAccountId")
if active_id not in order:
idx = store.get("activeIndex")
active_id = order[idx] if isinstance(idx, int) and 0 <= idx < len(order) else None

titles, blocks = [], []
newest = 0
for aid in order:
email = emails.get(aid, "?")
short = email.split("@")[0]
is_active = (aid == active_id)
frame = GREEN if is_active else GRAY
dot = "●" if is_active else "○"
tag = "ACTIVE" if is_active else "IDLE"
rows = [(pad(f"{dot} {short}", W - len(tag)) + tag, None)]
q = by_id.get(aid)
if not q:
titles.append("?")
rows.append((pad("no quota data", W), None))
else:
newest = max(newest, q.get("updatedAt", 0))
for key, label in (("primary", "5h"), ("secondary", "7d")):
win = q.get(key, {})
rem = clamp_pct(100 - clamp_pct(win.get("usedPercent", 0)))
reset = fmt_reset(win["resetAtMs"]) if win.get("resetAtMs") else "-"
rows.append((pad(f"{label} {bar(rem)} {rem:>3}% → {reset}", W), quota_color(rem)))
if key == "primary":
titles.append(f"[{rem}]" if is_active else str(rem))
blocks.append((frame, rows))

print("⚡" + "·".join(titles))
print("---")
style = "font=Menlo size=12 trim=false emojize=false"
for frame, rows in blocks:
print(f"╭{'─' * (W + 2)}╮ | color={frame} {style}")
for text, override in rows:
print(f"│ {text} │ | color={override or frame} {style}")
print(f"╰{'─' * (W + 2)}╯ | color={frame} {style}")
if newest:
age_min = int((time.time() - newest / 1000) / 60)
age = "just now" if age_min < 1 else (f"{age_min}m ago" if age_min < 60 else f"{age_min//60}h ago")
print(f"Cache updated {age} | size=11 color=gray")
print(f"Live refresh (one probe per account) | bash={SELF} param1=livecheck terminal=false refresh=true")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unquoted SELF path in SwiftBar action

bash={SELF} is written without quotes; if the plugin is installed in a path with spaces (e.g. a home dir like /Users/John Doe/) SwiftBar's parameter parser splits on whitespace and treats everything after the first space as a new param, silently dropping the livecheck dispatch and breaking the live-refresh action. SwiftBar supports bash="{SELF}" (or percent-encoding), which handles spaces cleanly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: contrib/swiftbar/codex-quota.5m.sh
Line: 133

Comment:
**Unquoted `SELF` path in SwiftBar action**

`bash={SELF}` is written without quotes; if the plugin is installed in a path with spaces (e.g. a home dir like `/Users/John Doe/`) SwiftBar's parameter parser splits on whitespace and treats everything after the first space as a new param, silently dropping the `livecheck` dispatch and breaking the live-refresh action. SwiftBar supports `bash="{SELF}"` (or percent-encoding), which handles spaces cleanly.

How can I resolve this? If you propose a fix, please make it concise.

print("Reload from cache | refresh=true")
PYEOF
Comment on lines +30 to +149

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 🏗️ Heavy lift

add regression coverage for quota parsing/render formatting.

this feature ships non-trivial transform/render logic with internal-cache compatibility assumptions, but this pr has no tests for: active-account fallback, color thresholds (<30, <10), title formatting, and bad/missing windows. please add fixture-driven regression tests (golden output or structured row assertions) so future cache/schema changes do not silently break the widget.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/swiftbar/codex-quota.5m.sh` around lines 22 - 135, Add fixture-driven
regression tests that cover active-account fallback, quota_color thresholds (<30
-> ORANGE, <10 -> RED), title formatting (active account wrapped in brackets),
and missing/bad window handling; refactor the rendering logic into a testable
function (e.g., expose load + the loop that builds titles/blocks as a function
that returns (titles, blocks, newest) or export a render_rows helper used by the
main script), then write tests that load sample CACHE/STORE/OBSERV fixtures and
assert either the generated textual lines (golden) or the structured rows/blocks
(preferred) for each fixture case: active-id missing, primary/secondary missing,
usedPercent edge values (9, 29, 30), and ensure fmt_reset, bar, and quota_color
produce the expected strings/colors.