1. 문제 상황
Rate Limit 관리의 어려움
2025년 이후 AI 코딩 어시스턴트 시장이 폭발적으로 성장하면서, 많은 개발자들이 여러 AI CLI 도구를 동시에 사용하게 되었다:
- Claude Code (Anthropic)
- Codex CLI (OpenAI)
- Gemini CLI (Google)
- z.ai (ZHIPU)
각 서비스는 자체적인 rate limit을 가지고 있어서, 한 서비스가 제한에 걸리면 다른 서비스로 전환해야 한다. 문제는:
# 현재 상황: 각 CLI마다 따로 확인해야 함
claude --usage # Claude 사용량 확인
codex --status # Codex 사용량 확인
gemini quota # Gemini 사용량 확인
# z.ai는 확인 방법이 없음...
Pain Points:
- 각 CLI마다 다른 명령어로 사용량을 확인해야 함
- 출력 형식이 제각각이라 비교가 어려움
- 어떤 CLI가 가장 여유 있는지 직관적으로 알 수 없음
- 일부 CLI는 사용량 확인 명령어 자체가 없음
원하는 결과
════════════════════════════════════════
CLI Usage Dashboard
════════════════════════════════════════
[Claude]
5h: 25% (4h10m) | 7d: 18% (4d20h)
[Codex]
5h: 0% (4h59m) | 7d: 1% (3d8h) | Plan: plus
[Gemini]
Used: 0% (15h7m) | Model: gemini-2.0-flash
════════════════════════════════════════
Recommendation: codex (Lowest usage (0% used))
════════════════════════════════════════
하나의 명령어로 모든 AI CLI의 사용량을 확인하고, 가장 여유 있는 CLI를 자동으로 추천받고 싶었다.
2. 설계 과정
2.1 아키텍처 결정
Option 1: 독립 CLI 도구
npm install -g ai-usage-checker
ai-usage
- 장점: 범용성
- 단점: 별도 설치 필요, 업데이트 관리
Option 2: Claude Code Plugin Command ← 선택
/check-usage
- 장점: 이미 사용 중인 환경에 통합, 자동 업데이트
- 단점: Claude Code 사용자 한정
Claude Code 플러그인으로 구현하기로 결정한 이유:
- 이미
claude-dashboard플러그인을 개발 중이었음 - 기존 API 클라이언트 코드를 재사용 가능
- Claude Code 사용자가 주요 타겟
2.2 데이터 정규화 전략
각 CLI마다 API 응답 형식이 완전히 다르다:
// Claude: OAuth API
interface ClaudeUsage {
five_hour: { utilization: number; resets_at: string };
seven_day: { utilization: number; resets_at: string };
}
// Codex: ChatGPT Backend API
interface CodexUsage {
rate_limit: {
primary_window: { used_percent: number; reset_at: number }; // Unix timestamp (seconds)
secondary_window: { used_percent: number; reset_at: number };
};
plan_type: string;
}
// Gemini: Google Code Assist API
interface GeminiUsage {
buckets: Array<{ remainingFraction: number; resetTime: string }>; // 남은 비율 (1 - 사용량)
}
// z.ai: ZHIPU API
interface ZaiUsage {
limits: Array<{
type: 'TOKENS_LIMIT' | 'TIME_LIMIT';
currentValue: number; // 0-1 범위
nextResetTime: number; // Unix timestamp (milliseconds)
}>;
}
정규화된 공통 인터페이스:
interface CLIUsage {
name: string;
available: boolean; // CLI 설치 여부
error: boolean; // API 호출 실패 여부
fiveHourPercent: number | null; // 5시간 사용량 (0-100)
sevenDayPercent: number | null; // 7일 사용량 (0-100)
fiveHourReset: string | null; // 리셋 시간 (ISO string)
sevenDayReset: string | null;
model?: string; // 현재 모델 (선택)
plan?: string; // 플랜 정보 (선택)
}
2.3 추천 알고리즘
단순하지만 효과적인 알고리즘을 선택:
function calculateRecommendation(
claudeUsage: CLIUsage,
codexUsage: CLIUsage | null,
geminiUsage: CLIUsage | null,
zaiUsage: CLIUsage | null,
lang: 'en' | 'ko'
): { name: string | null; reason: string } {
const candidates: { name: string; score: number }[] = [];
// 5시간 사용량을 primary metric으로 사용
// ← 핵심: 단기 제한이 더 자주 걸리므로 5h 기준으로 추천
if (!claudeUsage.error && claudeUsage.fiveHourPercent !== null) {
candidates.push({ name: 'claude', score: claudeUsage.fiveHourPercent });
}
if (codexUsage?.available && !codexUsage.error && codexUsage.fiveHourPercent !== null) {
candidates.push({ name: 'codex', score: codexUsage.fiveHourPercent });
}
// ... 다른 CLI들도 동일하게 처리
// 낮은 사용량 순으로 정렬 (오름차순)
candidates.sort((a, b) => a.score - b.score);
const best = candidates[0];
return {
name: best.name,
reason: `Lowest usage (${best.score}% used)`
};
}
3. 구현 상세
3.1 프로젝트 구조
scripts/
├── check-usage.ts # 메인 엔트리포인트
├── utils/
│ ├── api-client.ts # Claude OAuth API
│ ├── codex-client.ts # Codex (ChatGPT) API
│ ├── gemini-client.ts # Gemini (Google) API
│ ├── zai-api-client.ts # z.ai (ZHIPU) API
│ ├── colors.ts # ANSI 색상 유틸
│ ├── formatters.ts # 시간/숫자 포맷팅
│ └── i18n.ts # 다국어 지원
└── types.ts # TypeScript 타입 정의
3.2 CLI 설치 감지
각 CLI가 설치되어 있는지 확인하는 방법이 모두 다르다:
// Codex: auth.json 파일 존재 확인
export async function isCodexInstalled(): Promise<boolean> {
try {
await stat(path.join(os.homedir(), '.codex', 'auth.json'));
return true;
} catch {
return false;
}
}
// Gemini: Keychain 또는 oauth_creds.json 확인
export async function isGeminiInstalled(): Promise<boolean> {
// macOS Keychain 먼저 확인
const keychainToken = await getTokenFromKeychain();
if (keychainToken) return true;
// 파일 fallback
try {
await stat(path.join(os.homedir(), '.gemini', 'oauth_creds.json'));
return true;
} catch {
return false;
}
}
// z.ai: 환경변수 기반 (ANTHROPIC_BASE_URL이 z.ai 도메인인지)
export function isZaiInstalled(): boolean {
const baseUrl = process.env.ANTHROPIC_BASE_URL || '';
return baseUrl.includes('z.ai') || baseUrl.includes('zhipu');
}
3.3 병렬 API 호출
성능 최적화를 위해 모든 API를 병렬로 호출:
async function main(): Promise<void> {
// 1단계: 설치 여부 확인 (병렬)
const [claudeLimits, codexInstalled, geminiInstalled] = await Promise.all([
fetchUsageLimits(60), // Claude는 항상 호출
isCodexInstalled(), // 파일 존재 확인
isGeminiInstalled(), // Keychain/파일 확인
]);
const zaiInstalled = isZaiInstalled(); // 동기 함수
// 2단계: 설치된 CLI만 API 호출 (병렬)
// ← 핵심: 불필요한 API 호출 방지
const [codexLimits, geminiLimits, zaiLimits] = await Promise.all([
codexInstalled ? fetchCodexUsage(60) : Promise.resolve(null),
geminiInstalled ? fetchGeminiUsage(60) : Promise.resolve(null),
zaiInstalled ? fetchZaiUsage(60) : Promise.resolve(null),
]);
}
3.4 시간 포맷 통일
각 API가 반환하는 시간 형식이 모두 다르다:
// Claude: ISO 8601 string
"2026-02-01T03:00:00.063824+00:00"
// Codex: Unix timestamp (seconds)
1738393200
// z.ai: Unix timestamp (milliseconds)
1738393200000
// Gemini: ISO 8601 string (다른 형식)
"2026-02-01T13:56:52Z"
통일된 포맷터:
// ISO string 처리
function formatTimeRemaining(resetAt: string | Date, t: Translations): string {
const reset = typeof resetAt === 'string' ? new Date(resetAt) : resetAt;
const diffMs = reset.getTime() - Date.now();
const totalMinutes = Math.floor(diffMs / (1000 * 60));
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
if (hours > 0) {
return `${hours}${t.time.hours}${minutes}${t.time.minutes}`; // "4h30m"
}
return `${minutes}${t.time.minutes}`; // "30m"
}
// Unix timestamp (seconds) 처리 - Codex용
function formatTimeFromTimestamp(resetAt: number, t: Translations): string {
const resetDate = new Date(resetAt * 1000); // ← 초 → 밀리초 변환
return formatTimeRemaining(resetDate, t);
}
// Unix timestamp (milliseconds) 처리 - z.ai용
function formatTimeFromTimestampMs(resetAtMs: number, t: Translations): string {
const resetDate = new Date(resetAtMs); // 이미 밀리초
return formatTimeRemaining(resetDate, t);
}
3.5 ANSI 컬러 출력
터미널에서 사용량에 따라 색상을 다르게 표시:
export const COLORS = {
pastelGreen: '\x1b[38;5;151m', // 0-50%: 안전
pastelYellow: '\x1b[38;5;222m', // 51-80%: 경고
pastelRed: '\x1b[38;5;210m', // 81-100%: 위험
pastelCyan: '\x1b[38;5;117m', // 라벨용
gray: '\x1b[90m', // 비활성
reset: '\x1b[0m',
} as const;
function getColorForPercent(percent: number): string {
if (percent <= 50) return COLORS.pastelGreen;
if (percent <= 80) return COLORS.pastelYellow;
return COLORS.pastelRed;
}
function colorize(text: string, color: string): string {
return `${color}${text}${COLORS.reset}`;
}
3.6 JSON 출력 모드
스크립팅을 위한 JSON 출력 지원:
const args = process.argv.slice(2);
const isJsonMode = args.includes('--json');
if (isJsonMode) {
const output: CheckUsageOutput = {
claude: claudeUsage,
codex: codexInstalled ? codexUsage : null,
gemini: geminiInstalled ? geminiUsage : null,
zai: zaiInstalled ? zaiUsage : null,
recommendation: recommendation.name,
recommendationReason: recommendation.reason,
};
console.log(JSON.stringify(output, null, 2));
return;
}
사용 예시:
# JSON 출력으로 jq와 조합
/check-usage --json | jq '.recommendation'
# "codex"
# 셸 스크립트에서 활용
BEST_CLI=$(/check-usage --json | jq -r '.recommendation')
echo "Switching to $BEST_CLI"
3.7 다국어 지원 (i18n)
시스템 언어 자동 감지:
function detectSystemLanguage(): 'en' | 'ko' {
const lang = process.env.LANG || process.env.LC_ALL || '';
if (lang.toLowerCase().startsWith('ko')) {
return 'ko';
}
return 'en';
}
// 사용
const lang = detectSystemLanguage();
const t = getTranslationsByLang(lang);
// 출력 예시
const recLabel = lang === 'ko' ? '추천' : 'Recommendation';
// Korean: "추천: codex (가장 여유 (0% 사용))"
// English: "Recommendation: codex (Lowest usage (0% used))"
4. 빌드 설정
esbuild를 이용한 번들링
// scripts/build.js
const commonOptions = {
bundle: true,
platform: 'node',
format: 'esm',
define: {
__VERSION__: JSON.stringify(version),
},
};
// 메인 status line
await build({
...commonOptions,
entryPoints: ['scripts/statusline.ts'],
outfile: 'dist/index.js',
});
// check-usage 명령어 (새로 추가)
await build({
...commonOptions,
entryPoints: ['scripts/check-usage.ts'],
outfile: 'dist/check-usage.js',
});
독립 실행 가능한 스크립트
#!/usr/bin/env node // ← shebang 추가
/**
* CLI Usage Dashboard
*/
// stdin 없이 독립 실행
// status line과 달리 stdin에서 데이터를 받지 않음
async function main(): Promise<void> {
// 직접 API 호출
}
main().catch((err) => {
console.error('Error:', err.message);
process.exit(1);
});
5. 핵심 개념 정리
| 개념 | 설명 | 적용 |
|---|---|---|
| 데이터 정규화 | 서로 다른 API 응답을 공통 인터페이스로 통일 | CLIUsage 인터페이스 |
| 조건부 병렬 호출 | 설치된 CLI만 API 호출 | Promise.all + 조건부 |
| Graceful Degradation | 일부 API 실패해도 나머지 표시 | error 플래그 |
| 시간 형식 통일 | ISO, Unix(s), Unix(ms) → 공통 포맷 | 포맷터 함수들 |
| 다중 출력 형식 | 터미널 + JSON | --json 플래그 |
6. 베스트 프랙티스
CLI 도구 개발 체크리스트
- [ ] 설치 감지: 각 의존성이 설치되어 있는지 먼저 확인
- [ ] 병렬 처리: 독립적인 API 호출은
Promise.all로 병렬화 - [ ] 에러 처리: 일부 실패해도 나머지 기능은 정상 동작
- [ ] 타임아웃: 외부 API 호출에 항상 타임아웃 설정 (5초 권장)
- [ ] 캐싱: 반복 호출 시 캐시 활용 (60초 TTL)
- [ ] 다중 출력: 사람용(컬러) + 스크립트용(JSON) 지원
- [ ] i18n: 시스템 언어 자동 감지
타입 안전성
// Bad: any 사용
const data: any = await response.json();
const usage = data.five_hour.utilization; // 런타임 에러 가능
// Good: 타입 가드 사용
const data: unknown = await response.json();
if (!data || typeof data !== 'object') {
return null; // 조기 반환
}
if (!('rate_limit' in data)) {
return null; // 필수 필드 확인
}
const typedData = data as CodexApiResponse; // 이제 안전
7. FAQ
Q: 왜 5시간 사용량을 기준으로 추천하나요?
A: 대부분의 AI CLI 서비스에서 5시간 제한이 더 자주 걸립니다. 7일 제한은 일반적인 사용 패턴에서는 거의 도달하지 않습니다. 따라서 단기 제한인 5시간 사용량을 primary metric으로 사용합니다.
Q: API 호출이 실패하면 어떻게 되나요?
A: 해당 CLI는 ⚠️ Error fetching data로 표시되고, 나머지 CLI는 정상적으로 표시됩니다. 추천 알고리즘에서도 해당 CLI는 제외됩니다.
Q: 새로운 AI CLI를 추가하려면 어떻게 해야 하나요?
A: 세 가지 파일을 수정해야 합니다:
utils/{cli}-client.ts: API 클라이언트 구현check-usage.ts: 파싱/렌더링 로직 추가types.ts: 타입 정의 추가
Q: 캐시는 어떻게 동작하나요?
A: 각 API 클라이언트는 60초 TTL의 메모리 캐시를 사용합니다. 같은 토큰으로 60초 내 재호출하면 캐시된 데이터를 반환합니다.
Q: JSON 출력을 어떻게 활용할 수 있나요?
A: 셸 스크립트나 다른 도구와 연동할 때 유용합니다:
# 가장 여유 있는 CLI로 자동 전환
BEST=$(/check-usage --json | jq -r '.recommendation')
export AI_CLI=$BEST
8. 참고 자료
- Claude Code Plugin 개발 가이드
- esbuild 번들러 문서
- ANSI Escape Codes Reference
- Node.js Child Process Best Practices
9. 다음 단계
이 글에서는 여러 AI CLI의 사용량을 통합 조회하는 대시보드를 구현했습니다. 다음 글에서는 이 데이터를 활용해 자동으로 CLI를 전환하는 워크플로우를 구축해 볼 예정입니다.
시리즈 목차:
- Claude Dashboard 플러그인 개발기
- Claude Code, Codex, Gemini 통합 사용량 대시보드 ← 현재 글