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

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

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

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 자동 전환 워크플로우 구축 (예정)