AI Usage Menubar のビルドエラーを直して、Codex の週間利用枠を表示できるようにした
macOS のメニューバーに Codex と Claude Code の利用率を常駐表示するアプリ「AI Usage Menubar」を試したところ、clone 直後のビルドが失敗しました。原因を追ったついでに、Codex 版に足りていなかった週間利用枠の表示も実装したので、その手順をまとめます。
元記事
このアプリは szk1akhr さんの以下の記事で紹介されているものです。
- CodexとClaude Codeの利用率をmacOSのメニューバーに常駐表示するアプリを作った (@szk1akhr)
- リポジトリ: akh1r0ck/ai-usage-menubar
Objective-C + AppKit で書かれた外部ライブラリ非依存のネイティブアプリです。特筆すべきは利用量の取得に追加の API リクエストを一切行わない設計で、Codex 版はローカルのセッションログ (JSONL) を、Claude Code 版は statusLine のキャッシュを読みます。API キーの管理が不要で、常駐ツールとして理にかなった作りだと思います。
この記事は、そのアプリに対する以下の2点の作業記録です。
- clone 直後に
make buildが失敗するバグの修正 - Codex 版に週間利用枠 (7日枠) の表示を追加
検証環境
| 項目 | 値 |
|---|---|
| macOS | 26.5.1 (Apple Silicon / M2) |
| Xcode Command Line Tools | インストール済み |
| Codex CLI | Homebrew 経由 (team プラン) |
| Claude Code | 2.1.174 |
第1部: ビルドエラーの修正
症状
README の手順どおりに clone してビルドすると、こうなります。
$ git clone https://github.com/akh1r0ck/ai-usage-menubar.git
$ cd ai-usage-menubar
$ make build
./scripts/build-app.sh
/var/folders/.../tmp.stSQWz2ntO/ChatGPT Usage.app: replacing existing signature
mv: rename /var/folders/.../tmp.stSQWz2ntO/ChatGPT Usage.app to
/Users/xxx/src/ai-usage-menubar/dist/ChatGPT Usage.app: No such file or directory
make: *** [build-codex] Error 1
replacing existing signature まで出ているので、コンパイルと署名は成功しています。落ちているのは最後の mv だけです。
原因
scripts/build-app.sh を読むと構造がはっきりします。
#!/bin/sh
set -eu
PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
APP_DIR="$PROJECT_DIR/dist/ChatGPT Usage.app"
STAGING_DIR=$(mktemp -d)
trap 'rm -rf "$STAGING_DIR"' EXIT
STAGING_APP="$STAGING_DIR/ChatGPT Usage.app"
CONTENTS="$STAGING_APP/Contents"
cd "$PROJECT_DIR"
mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources" # ← これは一時ディレクトリ側
clang -fobjc-arc ... -o "$CONTENTS/MacOS/ChatGPTUsage"
cp "Resources/Info.plist" "$CONTENTS/Info.plist"
printf 'APPL????' > "$CONTENTS/PkgInfo"
xattr -cr "$STAGING_APP"
codesign --force --sign - "$STAGING_APP"
rm -rf "$APP_DIR"
mv "$STAGING_APP" "$APP_DIR" # ← ここで落ちる
ビルドは mktemp -d で作った一時ディレクトリで行い、完成品を dist/ へ mv する流れです。ところが、
mkdir -p "$CONTENTS/MacOS"が作るのは一時ディレクトリ側であってdist/ではないrm -rf "$APP_DIR"は対象が存在しなくてもエラーにならず黙って通るmvは移動先の親ディレクトリを自動生成しない
という三段構えで、dist/ が無いまま mv に到達します。
再現条件は「dist/ が一度も作られていない状態」、つまり clone 直後か make clean 直後です。作者の手元では過去のビルドで dist/ が残っていたため、気づかれなかったのだと思われます。
その場の回避策
dist/ を手で作れば通ります。
$ mkdir -p dist
$ make build
恒久的な修正
mv の前に1行足すだけです。scripts/build-app.sh と scripts/build-claude-app.sh の両方に同じ修正を入れます。
codesign --force --sign - "$STAGING_APP"
+ mkdir -p "$PROJECT_DIR/dist"
rm -rf "$APP_DIR"
mv "$STAGING_APP" "$APP_DIR"
これで誰が clone しても初回ビルドが通るようになります。
第2部: Codex 版に週間利用枠を追加する
きっかけ
Codex CLI の画面には「5時間」と「1週間」の2つの枠が表示されます。
残り使用量
5時間 76% 14:49
1週間 87% 9月7日
ところが AI Usage Menubar の Codex 版は5時間枠しか表示しません。Claude Code 版は「セッション」と「週間」の2枠を表示しているのに、Codex 版だけ片方だけなのは惜しい。
Step 1: データが手元に来ているか確認する
改造の可否は、ローカルのデータに週間枠が含まれているかの一点で決まります。含まれていなければ、このアプリの「追加 API 通信をしない」という設計上、どうやっても表示できません。
Codex は ~/.codex/sessions/**/*.jsonl にセッションを記録します。最新ファイルの最後の token_count イベントから rate_limits を抜き出してみます。
$ f=$(ls -t ~/.codex/sessions/**/*.jsonl | head -1)
$ grep '"token_count"' "$f" | tail -1 | python3 -c \
'import json,sys; d=json.load(sys.stdin); print(json.dumps(d.get("payload",{}).get("rate_limits"), indent=2))'
結果:
{
"limit_id": "codex",
"limit_name": null,
"primary": {
"used_percent": 30.0,
"window_minutes": 300,
"resets_at": 1788328159
},
"secondary": {
"used_percent": 14.0,
"window_minutes": 10080,
"resets_at": 1788761372
},
"credits": { "has_credits": false, "unlimited": false, "balance": null },
"plan_type": "team"
}
secondary があり、window_minutes が 10080 = ちょうど7日。週間枠のデータは最初から届いていました。
Step 2: なぜ表示されていなかったのか
Sources/UsageProvider.m のパース部分を見ると、原因は1行に集約されていました。
NSDictionary *limits = payload[@"rate_limits"] ?: @{};
NSDictionary *window = limits[@"primary"] ?: limits[@"secondary"]; // ← ここ
if (![window isKindOfClass:NSDictionary.class]) continue;
snapshot.available = YES;
snapshot.primaryPercent = [window[@"used_percent"] doubleValue];
snapshot.primaryReset = [window[@"resets_at"] doubleValue];
snapshot.windowMinutes = [window[@"window_minutes"] integerValue];
?: はフォールバックであって、両方を読む処理ではありません。primary があれば secondary は見ずに捨てられます。
一方、Sources/UsageProvider.h を見ると:
@property BOOL hasPrimary;
@property double secondaryPercent;
@property BOOL hasSecondary;
@property NSTimeInterval secondaryReset;
@property NSInteger windowMinutes;
受け皿はすでに用意されていました。 Claude 版 (claude_main.m) がこれを使って2枠を描いています。つまり Codex 側のパーサーが埋めていないだけ、という状態でした。
Step 3: パーサーを修正する
primary と secondary を別々に読むように書き換えます。
NSDictionary *limits = payload[@"rate_limits"] ?: @{};
NSDictionary *primaryWindow = limits[@"primary"];
NSDictionary *secondaryWindow = limits[@"secondary"];
BOOL hasPrimaryWindow = [primaryWindow isKindOfClass:NSDictionary.class];
BOOL hasSecondaryWindow = [secondaryWindow isKindOfClass:NSDictionary.class];
if (!hasPrimaryWindow && !hasSecondaryWindow) continue;
snapshot.available = YES;
NSDictionary *window = hasPrimaryWindow ? primaryWindow : secondaryWindow;
snapshot.hasPrimary = YES;
snapshot.primaryPercent = [window[@"used_percent"] doubleValue];
snapshot.primaryReset = [window[@"resets_at"] doubleValue];
snapshot.windowMinutes = [window[@"window_minutes"] integerValue];
snapshot.hasSecondary = hasPrimaryWindow && hasSecondaryWindow;
if (snapshot.hasSecondary) {
snapshot.secondaryPercent = [secondaryWindow[@"used_percent"] doubleValue];
snapshot.secondaryReset = [secondaryWindow[@"resets_at"] doubleValue];
}
意識した点が3つあります。
- 既存の挙動を壊さない。
primaryが無くsecondaryだけある場合は、従来どおり secondary を主枠として扱います - 両方ある時だけ
hasSecondaryを立てる。secondaryを主枠に昇格させたケースで、同じデータを2枚描いてしまうのを防ぎます - ついでに
hasPrimaryも設定。従来は未設定のままでした
Sources/UsageProvider.m は1行が非常に長いので、エディタで手作業だと事故ります。私は置換スクリプトに assert を仕込んで、対象が1件でなければ何も書かずに止まるようにしました。
この時点で make build は通りますが、見た目は変わりません。UI がまだ hasSecondary を見ていないためです。
Step 4: Codex 版の UI に週間枠を追加する
Sources/main.m に手を入れます。Claude 版はカード型 UI ですが、Codex 版はラベルとバーを縦に積む構成なので、その流儀に合わせます。
4-1. プロパティを追加
@property UsageBar *bar;
@property NSTextField *weeklyPercentLabel;
@property NSTextField *weeklyResetLabel;
@property UsageBar *weeklyBar;
4-2. メニューバーの文字列を組み立てるメソッドを追加
既存の UsageStatusTitle() は値を1つしか受け取らない設計なので、NSMutableAttributedString で自前に組みます。Claude 版の menuTitleForSession:weekly:stale: がそのまま手本になります。
- (NSAttributedString *)menuTitleForPrimary:(NSNumber *)primary weekly:(NSNumber *)weekly {
UsageSettingsStore *settings = UsageSettingsStore.sharedStore;
NSFont *font = UsageFont(settings.menuBarFontSize, NSFontWeightBold);
NSString *prefix = settings.showServiceName ? @"Codex " : @"";
NSMutableAttributedString *title = [[NSMutableAttributedString alloc]
initWithString:prefix
attributes:@{NSFontAttributeName:font,
NSForegroundColorAttributeName:[settings accentColorForService:UsageServiceCodex]}];
NSString *primaryText = primary ? UsageMenuValue(primary.doubleValue) : @"--%";
[title appendAttributedString:[[NSAttributedString alloc]
initWithString:primaryText
attributes:@{NSFontAttributeName:font,
NSForegroundColorAttributeName:primary ? UsageColor(primary.doubleValue)
: NSColor.secondaryLabelColor}]];
if (weekly && settings.showWeekly) {
NSString *weeklyText = [NSString stringWithFormat:@" · W%@", UsageMenuValue(weekly.doubleValue)];
[title appendAttributedString:[[NSAttributedString alloc]
initWithString:weeklyText
attributes:@{NSFontAttributeName:font,
NSForegroundColorAttributeName:UsageColor(weekly.doubleValue)}]];
}
return title;
}
Claude 版の Claude 32% · W78% に表記を揃えました。
4-3. ポップオーバーに週間行を追加
buildPopover でラベルとバーを生成し、
self.weeklyPercentLabel = [self label:@"" size:15 color:nil];
self.weeklyPercentLabel.font = ModernFont(15, NSFontWeightBold);
self.weeklyResetLabel = [self label:@"" size:12 color:NSColor.secondaryLabelColor];
self.weeklyBar = [UsageBar new];
self.weeklyBar.translatesAutoresizingMaskIntoConstraints = NO;
[self.weeklyBar.heightAnchor constraintEqualToConstant:16].active = YES;
BOOL weeklyVisible = display.showWeekly;
self.weeklyPercentLabel.hidden = !weeklyVisible;
self.weeklyBar.hidden = !weeklyVisible;
self.weeklyResetLabel.hidden = !weeklyVisible;
スタックの resetLabel の直後に挿入します。
- NSStackView *stack = [NSStackView stackViewWithViews:@[title, self.percentLabel,
- self.periodLabel, self.bar, self.resetLabel,
- self.tokenLabel, self.contextLabel, self.updatedLabel, buttons]];
+ NSStackView *stack = [NSStackView stackViewWithViews:@[title, self.percentLabel,
+ self.periodLabel, self.bar, self.resetLabel,
+ self.weeklyPercentLabel, self.weeklyBar, self.weeklyResetLabel,
+ self.tokenLabel, self.contextLabel, self.updatedLabel, buttons]];
行が増えるのでポップオーバーの高さと、バーの幅制約も追加します。
- controller.view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 320, 255)];
+ controller.view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 320, 340)];
[self.bar.widthAnchor constraintEqualToConstant:288],
+ [self.weeklyBar.widthAnchor constraintEqualToConstant:288],
4-4. refresh で値を流し込む
メニューバーを新メソッド経由に切り替え、
- self.statusItem.button.attributedTitle = CodexTitle(UsageMenuValue(percent), UsageColor(percent));
+ NSNumber *weeklyValue = snapshot.hasSecondary ? @(snapshot.secondaryPercent) : nil;
+ self.statusItem.button.attributedTitle = [self menuTitleForPrimary:@(percent) weekly:weeklyValue];
refresh の末尾に週間表示の更新処理を足します。
BOOL showWeekly = snapshot.hasSecondary && UsageSettingsStore.sharedStore.showWeekly;
self.weeklyPercentLabel.hidden = !showWeekly;
self.weeklyBar.hidden = !showWeekly;
self.weeklyResetLabel.hidden = !showWeekly;
if (showWeekly) {
double weeklyPercent = snapshot.secondaryPercent;
self.weeklyBar.value = weeklyPercent;
self.weeklyPercentLabel.stringValue = UsageSettingsStore.sharedStore.displayRemaining
? [NSString stringWithFormat:@"週間 %.0f%% 残り", 100 - weeklyPercent]
: [NSString stringWithFormat:@"週間 %.0f%% 使用", weeklyPercent];
self.weeklyPercentLabel.textColor = UsageColor(weeklyPercent);
NSTimeInterval weeklyReset = snapshot.secondaryReset;
self.weeklyResetLabel.stringValue = weeklyReset
? [NSString stringWithFormat:@"リセット: %@",
[self formatted:[NSDate dateWithTimeIntervalSince1970:weeklyReset]]]
: @"リセット時刻: --";
self.weeklyResetLabel.hidden = !UsageSettingsStore.sharedStore.showResetTime;
}
データなし時のタイトル生成も新メソッドに一本化しておきます。
- if (!snapshot.available) { self.statusItem.button.attributedTitle = CodexTitle(@"--%", NSColor.secondaryLabelColor);
+ if (!snapshot.available) { self.statusItem.button.attributedTitle = [self menuTitleForPrimary:nil weekly:nil];
Step 5: 設定項目 showWeekly を追加する
ここまでのコードは UsageSettingsStore.showWeekly を参照していますが、これはまだ存在しません。既存の showResetTime を手本に追加します。
このアプリの設定項目は11箇所に分散しているので、漏れなく追加する必要があります。
Sources/UsageUI.h
@property (nonatomic) BOOL showResetTime;
+ @property (nonatomic) BOOL showWeekly;
Sources/UsageUI.m
UserDefaults のキー定義:
static NSString *const ShowResetTimeKey = @"display.showResetTime";
+ static NSString *const ShowWeeklyKey = @"display.showWeekly";
アクセサ (マクロで一括生成される仕組み):
BOOL_SETTING(showResetTime, setShowResetTime, ShowResetTimeKey)
+ BOOL_SETTING(showWeekly, setShowWeekly, ShowWeeklyKey)
デフォルト値 (registerDefaults):
- ShowPercentSignKey: @YES, ShowResetTimeKey: @YES, ShowTokenUsageKey: @YES,
+ ShowPercentSignKey: @YES, ShowResetTimeKey: @YES, ShowWeeklyKey: @YES, ShowTokenUsageKey: @YES,
設定画面クラスのプロパティ宣言:
- @property NSButton *showReset; @property NSButton *showTokens;
+ @property NSButton *showReset; @property NSButton *showWeekly; @property NSButton *showTokens;
チェックボックスの生成:
self.showWeekly = [NSButton checkboxWithTitle:@"週間利用枠" target:self action:@selector(change:)];
self.showWeekly.state = s.showWeekly;
チェックボックスの配置 (detailOptions スタック):
- stackViewWithViews:@[self.showReset,self.showTokens,self.showContext,self.showModel,self.showUpdated]]
+ stackViewWithViews:@[self.showReset,self.showWeekly,self.showTokens,self.showContext,self.showModel,self.showUpdated]]
change: ハンドラ:
else if (sender == self.showWeekly) s.showWeekly = self.showWeekly.state == NSControlStateValueOn;
reloadControls:
- self.showReset.state=s.showResetTime;self.showTokens.state=s.showTokenUsage;
+ self.showReset.state=s.showResetTime;self.showWeekly.state=s.showWeekly;self.showTokens.state=s.showTokenUsage;
設定のエクスポート/インポート対象 (settingKeys と、もう一箇所のキー配列):
- ShowPercentSignKey,ShowResetTimeKey,ShowTokenUsageKey,
+ ShowPercentSignKey,ShowResetTimeKey,ShowWeeklyKey,ShowTokenUsageKey,
デフォルトを @YES にしたのは、週間枠が本来見たかった情報であることと、データが無い環境では hasSecondary が false になって自動的に隠れるためです。
Step 6: ビルドして確認
$ make build && ls dist/
$ pkill -f "ChatGPT Usage"; open "dist/ChatGPT Usage.app"
結果:
- メニューバー:
Codex 30% · W14% - ポップオーバー: 5時間枠の下に「週間 14% 使用」とバー、リセット日時
- 設定 → 表示項目タブに「週間利用枠」のチェックボックス
make check でテストも回せます。Sources/provider_test.m の Codex テストは現状 primary しか検証していないので、PR にするなら secondary のケースを足すのが親切だと思います。
補足: Claude Code 版でモデル別の枠は表示できるか
同じ発想で、Claude Code 版に「Fable の週間枠」を足せないか試しました。結論は現状では不可能です。記録として残しておきます。
Claude Code の /usage 画面には週間制限が2本表示されます。
週間制限
すべてのモデル 5% 使用
Fable 7% 使用
ところが statusLine が渡してくる JSON の rate_limits には、次の2つしかありませんでした。
"rate_limits": {
"five_hour": { "used_percentage": 11, "resets_at": ... },
"seven_day": { "used_percentage": 4, "resets_at": ... }
}
公式ドキュメントを確認しても、rate_limits のフィールドは five_hour / seven_day / spend_limit の3つだけです (spend_limit は Claude apps gateway 経由の支出上限で、モデル別の枠ではありません)。
つまり /usage の「Fable 7%」に相当するデータは statusLine には流れて来ておらず、アプリ側をどう改造しても取得元がありません。モデル別の枠を出すには Anthropic の usage API を直接叩く必要があり、これはこのアプリが掲げる「追加 API 通信をしない」という設計思想と正面から衝突します。
Codex 版との差は、データがローカルに届いているかどうかの一点でした。ここを最初に確認したおかげで、無駄な実装に踏み込まずに済んでいます。
まとめ
make buildが clone 直後に失敗するのは、dist/を作らずにmvしているため。mkdir -p1行で直る- Codex の週間枠 (
rate_limits.secondary) は JSONL に最初から入っていた。パーサーが?:で捨てていただけ UsageSnapshotにはhasSecondaryなどの受け皿が既にあり、Claude 版が使っていた。設計が共通化されていたおかげで改造は素直だった- 一方 Claude 版のモデル別枠は statusLine にデータが無く、実装不可能。着手前にデータソースを確認するのが一番効く
元記事の作者は Issue や Pull Request を歓迎されているので、上記2点は還元したいと思います。軽量で気の利いたアプリなので、Codex や Claude Code を日常的に使う方は試してみてください。
改めて、元記事はこちらです。
- CodexとClaude Codeの利用率をmacOSのメニューバーに常駐表示するアプリを作った (@szk1akhr)

