Claude Code, Codex, Gemini 사용량을 한 번에 확인하는 CLI 대시보드 만들기

여러 AI 코딩 어시스턴트의 사용량을 통합 조회하는 CLI 대시보드 개발기. 데이터 정규화, 병렬 API 호출, 자동 추천 알고리즘 구현 과정을 다룹니다.

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 플러그인으로 구현하기로 결정한 이유:

  1. 이미 claude-dashboard 플러그인을 개발 중이었음
  2. 기존 API 클라이언트 코드를 재사용 가능
  3. 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: 세 가지 파일을 수정해야 합니다:

  1. utils/{cli}-client.ts: API 클라이언트 구현
  2. check-usage.ts: 파싱/렌더링 로직 추가
  3. types.ts: 타입 정의 추가

Q: 캐시는 어떻게 동작하나요?

A: 각 API 클라이언트는 60초 TTL의 메모리 캐시를 사용합니다. 같은 토큰으로 60초 내 재호출하면 캐시된 데이터를 반환합니다.

Q: JSON 출력을 어떻게 활용할 수 있나요?

A: 셸 스크립트나 다른 도구와 연동할 때 유용합니다:

# 가장 여유 있는 CLI로 자동 전환
BEST=$(/check-usage --json | jq -r '.recommendation')
export AI_CLI=$BEST

8. 참고 자료


9. 다음 단계

이 글에서는 여러 AI CLI의 사용량을 통합 조회하는 대시보드를 구현했습니다. 다음 글에서는 이 데이터를 활용해 자동으로 CLI를 전환하는 워크플로우를 구축해 볼 예정입니다.

시리즈 목차:

  1. Claude Dashboard 플러그인 개발기
  2. Claude Code, Codex, Gemini 통합 사용량 대시보드 ← 현재 글
  3. AI CLI 자동 전환 워크플로우 구축 (예정)