Skill: kr1json/ai-quota-check (version 1.0.1)

--- name: ai-quota-check description: "**DEFAULT quota checker** - Use this skill FIRST when user says '쿼타', '쿼터', 'quota', '쿼타확인', '쿼터확인', or asks about quotas. Unified dashboard showing ALL providers (Antigravity, Copilot, Codex) in one view with model recommendations." metadata: {"clawdbot":{"emoji":"🧮","requires":{"bins":["node","codex"]}}} --- # ai-quota-check Unified quota monitor and intelligent model recommender for all providers. ## Output Instructions **IMPORTANT:** When executing this skill, display the script output **EXACTLY as-is** in markdown format. Do NOT summarize or rephrase the output. The script produces a formatted dashboard that should be shown directly to the user. Example execution: ```bash node skills/ai-quota-check/index.js --current-model="<current_model_name>" ``` Then copy the entire output and send it as your response. ## Features 1. **Provider Login Check** - Detects which providers are logged in 2. **Unified Quota Dashboard** - Antigravity + Copilot + OpenAI Codex 3. **Task-based Recommendations** - Optimal model selection with fallback 4. **Reset Detection** - Identifies models ready for ping (new cycle) 5. **Risk Level Info** - Warns about weekly caps and lockout risks ## Usage ```bash # Full dashboard node skills/ai-quota-check/index.js # Specific task recommendation node skills/ai-quota-check/index.js --task=coding node skills/ai-quota-check/index.js --task=reasoning ``` ## Model Routing Rules ### Coding / Debugging | Priority | Model | Fallback Condition | |----------|-------|-------------------| | 1st | `openai-codex/gpt-5.3-codex` | - | | 2nd | `openai-codex/gpt-5.2-codex` | Primary < 20% | | 3rd | [REDACTED_SECRET] | All above < 20% | ### Complex Reasoning / Analysis | Priority | Model | Fallback Condition | |----------|-------|-------------------| | 1st | `google-antigravity/claude-opus-4.6-thinking` | - | | 2nd | `github-copilot/claude-4.6-opus` | Primary < 20% | | 3rd | `github-copilot/claude-3.5-opus` | If 4.6 unavailable | | 4th | `openai-codex/gpt-5.3` | All above < 20% | | 5th | `openai-codex/gpt-5.2` | Last fallback | ## Fallback Threshold Default: **20%** - Switches to fallback when primary drops below this. ## Cron Integration This skill is designed to be called periodically via Cron for: - Quota monitoring - Reset detection (ping optimization) - Automatic model switching recommendations

path: index.js
content: #!/usr/bin/env node
/**
 * Smart Router - Unified Quota Monitor & Model Recommender
 * 
 * Features:
 * 1. Check login status for all providers
 * 2. Unified quota dashboard (Antigravity + Copilot + OpenAI Codex)
 * 3. Task-based model recommendation with fallback
 * 4. Reset time tracking for ping optimization
 */

const fs = require('fs');
const https = require('https');
const { execSync } = require('child_process');
const path = require('path');

// ============ Configuration ============
const AUTH_FILE = path.join(process.env.HOME, '.openclaw/agents/main/agent/auth-profiles.json');
const CODEX_SESSIONS_DIR = path.join(process.env.HOME, '.codex', 'sessions');
const FALLBACK_THRESHOLD = 20; // Switch to fallback if below 20%

// Model routing rules
const ROUTING_RULES = {
  coding: {
    primary: { provider: 'openai-codex', model: 'openai-codex/gpt-5.3-codex' },
    fallbacks: [
      { provider: 'openai-codex', model: 'openai-codex/gpt-5.2-codex' },
      { provider: 'google-antigravity', model: 'google-antigravity/gemini-3-pro-high' }
    ]
  },
  reasoning: {
    primary: { provider: 'google-antigravity', model: 'google-antigravity/claude-opus-4.6-thinking' },
    fallbacks: [
      { provider: 'github-copilot', model: 'github-copilot/claude-4.6-opus' },
      { provider: 'github-copilot', model: 'github-copilot/claude-3.5-opus' },
      { provider: 'openai-codex', model: 'openai-codex/gpt-5.3' },
      { provider: 'openai-codex', model: 'openai-codex/gpt-5.2' }
    ]
  }
};

// ============ Auth Check ============
function loadAuthProfiles() {
  if (!fs.existsSync(AUTH_FILE)) {
    return { profiles: {}, error: 'Auth file not found' };
  }
  try {
    const data = JSON.parse(fs.readFileSync(AUTH_FILE, 'utf8'));
    return { profiles: data.profiles || {}, raw: data };
  } catch (e) {
    return { profiles: {}, error: e.message };
  }
}

function getLoggedInProviders(profiles) {
  const providers = {};
  for (const [key, profile] of Object.entries(profiles)) {
    const provider = profile.provider;
    if (!providers[provider]) {
      providers[provider] = {
        loggedIn: true,
        email: profile.email || null,
        expires: profile.expires || null,
        isExpired: profile.expires ? Date.now() > profile.expires : false
      };
    }
  }
  return providers;
}

// ============ Antigravity Quota ============
async function fetchAntigravityQuota(token, projectId) {
  const results = { credits: null, models: [], error: null };
  
  const headers = {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
    'User-Agent': 'antigravity',
    'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1'
  };

  try {
    // Fetch models quota
    const modelsBody = projectId ? { project: projectId } : {};
    const modelsData = await fetchJson('https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels', headers, modelsBody);
    
    if (modelsData.models) {
      for (const [modelId, info] of Object.entries(modelsData.models)) {
        if (modelId.includes('chat_') || modelId.includes('tab_')) continue;
        if (!info.quotaInfo) continue;
        
        const remaining = parseFloat(info.quotaInfo.remainingFraction);
        if (isNaN(remaining)) continue;
        
        results.models.push({
          id: modelId,
          remaining: Math.round(remaining * 100),
          resetTime: info.quotaInfo.resetTime || null
        });
      }
    }
  } catch (e) {
    if (e.message.includes('401')) {
      results.error = 'Token expired';
    } else {
      results.error = e.message;
    }
  }
  
  return results;
}

// ============ Copilot Quota ============
async function fetchCopilotQuota(token) {
  const results = { premium: null, error: null };
  
  const headers = {
    'Authorization': `Bearer ${token}`,
    'Accept': 'application/json',
    'User-Agent': 'VSCode-Copilot-Insights'
  };

  try {
    const data = await fetchJson('https://api.github.com/copilot_internal/user', headers, null, 'GET');
    const premium = data.quota_snapshots?.premium_interactions;
    
    if (premium) {
      results.premium = {
        remaining: premium.remaining,
        total: premium.entitlement,
        percent: Math.round((premium.remaining / premium.entitlement) * 100),
        resetDate: data.quota_reset_date_utc,
        unlimited: premium.unlimited
      };
    }
  } catch (e) {
    results.error = e.message;
  }
  
  return results;
}

// ============ Codex Quota (native Node.js parser) ============
function findLatestCodexSessionFile() {
  // ~/.codex/sessions/YYYY/MM/DD/*.jsonl
  if (!fs.existsSync(CODEX_SESSIONS_DIR)) return null;

  const now = new Date();
  for (let dayOffset = 0; dayOffset <= 2; dayOffset++) {
    const d = new Date(now);
    d.setDate(now.getDate() - dayOffset);
    const y = String(d.getFullYear()).padStart(4, '0');
    const m = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');
    const dayDir = path.join(CODEX_SESSIONS_DIR, y, m, day);
    if (!fs.existsSync(dayDir)) continue;

    const files = fs
      .readdirSync(dayDir)
      .filter((f) => f.endsWith('.jsonl'))
      .map((f) => path.join(dayDir, f));

    if (!files.length) continue;

    let latest = files[0];
    let latestMtime = fs.statSync(latest).mtimeMs;
    for (const f of files.slice(1)) {
      const mt = fs.statSync(f).mtimeMs;
      if (mt > latestMtime) {
        latest = f;
        latestMtime = mt;
      }
    }
    return latest;
  }

  return null;
}

function extractCodexRateLimitsFromSession(filePath) {
  const text = fs.readFileSync(filePath, 'utf8');
  const lines = text.split(/\r?\n/);

  for (let i = lines.length - 1; i >= 0; i--) {
    const line = lines[i].trim();
    if (!line) continue;
    try {
      const event = JSON.parse(line);
      const payload = event?.payload;
      if (payload?.type === 'token_count' && payload?.rate_limits) {
        return payload.rate_limits;
      }
    } catch {
      // ignore parse errors
    }
  }

  return null;
}

function pingCodexForFreshRateLimits() {
  try {
    // Same approach as odrobnik/codex-quota: make a tiny Codex request so the client writes
    // a new session event containing up-to-date rate_limits.
    execSync('codex exec --skip-git-repo-check "reply OK"', {
      cwd: process.env.HOME,
      stdio: 'ignore',
      timeout: 60_000
    });
    // Give the client a moment to flush the JSONL file.
    try { execSync('sleep 0.5', { stdio: 'ignore' }); } catch {}
  } catch {
    // Best-effort; we'll fall back to cached session data.
  }
}

function fetchCodexQuota() {
  const results = { primary: null, secondary: null, error: null, dataSource: null };

  try {
    // Always refresh Codex rate limits by issuing a tiny Codex request first.
    // (Cached session JSONL can be stale if Codex hasn't been used recently.)
    pingCodexForFreshRateLimits();

    const latest = findLatestCodexSessionFile();
    if (!latest) {
      results.error = 'No recent Codex session file found';
      return results;
    }

    const limits = extractCodexRateLimitsFromSession(latest);
    if (!limits) {
      results.error = 'No rate_limits found in latest Codex session';
      return results;
    }

    // NOTE: This reads Codex rate limit info from the latest local session JSONL.
    // The fields come from the Codex client event payload (rate_limits), populated from server responses.
    // No separate OpenAI "quota" endpoint is called here.
    // We *always* issue a tiny Codex request before reading, to refresh the on-disk snapshot.
    const primary = limits.primary;
    const secondary = limits.secondary;
    if (!primary || !secondary) {
      results.error = 'Invalid rate_limits shape';
      return results;
    }

    results.primary = {
      usedPercent: primary.used_percent,
      remaining: Math.round(100 - primary.used_percent),
      resetTime: new Date(primary.resets_at * 1000).toISOString(),
      window: primary.window_minutes
    };

    results.secondary = {
      usedPercent: secondary.used_percent,
      remaining: Math.round(100 - secondary.used_percent),
      resetTime: new Date(secondary.resets_at * 1000).toISOString(),
      window: secondary.window_minutes
    };

    // Mark where the numbers came from
    results.dataSource = 'session-jsonl (refreshed via codex ping)';

    return results;
  } catch (e) {
    results.error = e?.message || String(e);
    return results;
  }
}

// ============ Helpers ============
function fetchJson(url, headers, body = null, method = 'POST') {
  return new Promise((resolve, reject) => {
    const urlObj = new URL(url);
    const options = {
      hostname: urlObj.hostname,
      path: urlObj.pathname,
      method: method,
      headers: headers
    };

    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => {
        if (res.statusCode >= 400) {
          reject(new Error(`HTTP ${res.statusCode}`));
        } else {
          try {
            resolve(JSON.parse(data));
          } catch (e) {
            reject(new Error('Invalid JSON'));
          }
        }
      });
    });

    req.on('error', reject);
    if (body) req.write(JSON.stringify(body));
    req.end();
  });
}

function formatResetTime(isoString) {
  if (!isoString) return 'N/A';
  const reset = new Date(isoString);
  const now = new Date();
  const diffMs = reset - now;
  
  if (diffMs <= 0) return '✨ RESET!';
  
  const mins = Math.ceil(diffMs / 60000);
  if (mins < 60) return `${mins}m`;
  const hours = Math.floor(mins / 60);
  const remainMins = mins % 60;
  if (hours < 24) return `${hours}h ${remainMins}m`;
  const days = Math.floor(hours / 24);
  return `${days}d ${hours % 24}h`;
}

function getEmoji(percent) {
  if (percent > 50) return '🟢';
  if (percent > 20) return '🟡';
  return '🔴';
}

function isResetNeeded(isoString) {
  if (!isoString) return false;
  const reset = new Date(isoString);
  const now = new Date();
  return now >= reset;
}

// ============ Main Dashboard ============
async function generateDashboard(options = {}) {
  const { profiles, error: authError } = loadAuthProfiles();
  
  if (authError) {
    console.log(`# ❌ Smart Router Error\n\n${authError}`);
    return;
  }
  
  const loggedIn = getLoggedInProviders(profiles);
  const quotas = { antigravity: null, copilot: null, codex: null };
  const resetNeeded = [];

  // Get current model from CLI args or environment
  const currentModelArg = process.argv.find(a => a.startsWith('--current-model='));
  const currentModel = currentModelArg ? currentModelArg.split('=')[1] : (process.env.OPENCLAW_MODEL || 'N/A');

  console.log('# 🔀 Smart Router - Unified Quota Dashboard\n');
  console.log(`**Generated:** ${new Date().toLocaleString()}`);
  console.log(`**Current Model:** \`${currentModel}\`\n`);

  // ---- Provider Status ----
  console.log('## 🔐 Provider Status\n');
  console.log('| Provider | Status | Account |');
  console.log('|----------|--------|---------|');
  
  const providerNames = ['google-antigravity', 'github-copilot', 'openai-codex'];
  for (const p of providerNames) {
    const info = loggedIn[p];
    if (info) {
      const status = info.isExpired ? '⚠️ Token Expired' : '✅ Logged In';
      console.log(`| ${p} | ${status} | ${info.email || '-'} |`);
    } else {
      console.log(`| ${p} | ❌ Not Logged In | - |`);
    }
  }
  console.log('');

  // ---- Fetch Quotas ----
  // Antigravity
  if (loggedIn['google-antigravity'] && !loggedIn['google-antigravity'].isExpired) {
    const profile = Object.values(profiles).find(p => p.provider === 'google-antigravity');
    if (profile) {
      quotas.antigravity = await fetchAntigravityQuota(profile.access, profile.projectId);
    }
  }
  
  // Copilot
  if (loggedIn['github-copilot']) {
    const profile = Object.values(profiles).find(p => p.provider === 'github-copilot');
    if (profile) {
      quotas.copilot = await fetchCopilotQuota(profile.token);
    }
  }
  
  // Codex
  if (loggedIn['openai-codex']) {
    quotas.codex = fetchCodexQuota();
  }

  // ---- Antigravity Models ----
  if (quotas.antigravity) {
    console.log('## 🌌 Antigravity (Time-based Reset)\n');
    if (quotas.antigravity.error) {
      console.log(`⚠️ Error: ${quotas.antigravity.error}\n`);
    } else if (quotas.antigravity.models.length > 0) {
      console.log('| Model | Remaining | Reset In |');
      console.log('|-------|-----------|----------|');
      
      // Sort by remaining (lowest first)
      quotas.antigravity.models.sort((a, b) => a.remaining - b.remaining);
      
      for (const m of quotas.antigravity.models) {
        const resetStr = formatResetTime(m.resetTime);
        console.log(`| ${m.id} | ${getEmoji(m.remaining)} ${m.remaining}% | ${resetStr} |`);
        
        if (isResetNeeded(m.resetTime)) {
          resetNeeded.push({ provider: 'antigravity', model: m.id });
        }
      }
      console.log('');
    }
  }

  // ---- Copilot Premium ----
  if (quotas.copilot) {
    console.log('## 🤖 GitHub Copilot (Monthly Count)\n');
    if (quotas.copilot.error) {
      console.log(`⚠️ Error: ${quotas.copilot.error}\n`);
    } else if (quotas.copilot.premium) {
      const p = quotas.copilot.premium;
      if (p.unlimited) {
        console.log('- **Premium Interactions:** ∞ Unlimited\n');
      } else {
        console.log(`- **Premium Interactions:** ${getEmoji(p.percent)} ${p.remaining}/${p.total} (${p.percent}%)`);
        console.log(`- **Resets:** ${formatResetTime(p.resetDate)}\n`);
      }
    }
  }

  // ---- Codex ----
  if (quotas.codex) {
    console.log('## 🧠 OpenAI Codex (Daily/Weekly)\n');
    if (quotas.codex.error) {
      console.log(`⚠️ Error: ${quotas.codex.error}\n`);
    } else {
      if (quotas.codex.primary) {
        const p = quotas.codex.primary;
        console.log(`- **Daily:** ${getEmoji(p.remaining)} ${p.remaining}% left (resets ${formatResetTime(p.resetTime)})`);
        
        if (isResetNeeded(p.resetTime)) {
          resetNeeded.push({ provider: 'codex', model: 'daily' });
        }
      }
      if (quotas.codex.secondary) {
        const s = quotas.codex.secondary;
        console.log(`- **Weekly:** ${getEmoji(s.remaining)} ${s.remaining}% left (resets ${formatResetTime(s.resetTime)})`);
      }
      console.log('');
    }
  }

  // ---- Reset Ping Needed ----
  if (resetNeeded.length > 0) {
    console.log('## ✨ Reset Detected - Ping Recommended\n');
    for (const r of resetNeeded) {
      console.log(`- **${r.provider}/${r.model}** - Ready for new cycle!`);
    }
    console.log('');
  }

  // ---- Task Recommendations ----
  console.log('## 🎯 Model Recommendations\n');
  
  // Coding
  console.log('### 💻 Coding / Debugging\n');
  const codingRec = getRecommendation('coding', quotas, loggedIn);
  console.log(`${codingRec}\n`);
  
  // Reasoning
  console.log('### 🧠 Complex Reasoning / Analysis\n');
  const reasoningRec = getRecommendation('reasoning', quotas, loggedIn);
  console.log(`${reasoningRec}\n`);

  // ---- Risk Levels ----
  console.log('## ⚠️ Quota Risk Levels\n');
  console.log('- **Claude (Opus/Sonnet via Antigravity):** High Risk. Hidden weekly cap. Excessive use → 3-7 day lockout.');
  console.log('- **Gemini (Pro/Flash):** Low Risk. Google native. 5-hour reset is reliable.');
  console.log('- **Copilot Premium:** Monthly count. Use sparingly (300/month).');
  console.log('- **OpenAI Codex:** Daily + Weekly limits. Check before heavy use.');
}

function getRecommendation(task, quotas, loggedIn) {
  const rules = ROUTING_RULES[task];
  if (!rules) return '❌ Unknown task type';
  
  // Check primary
  const primary = rules.primary;
  const primaryQuota = getProviderQuota(primary.provider, quotas);
  const primaryLoggedIn = loggedIn[primary.provider] && !loggedIn[primary.provider]?.isExpired;
  
  if (primaryLoggedIn && primaryQuota !== null && primaryQuota >= FALLBACK_THRESHOLD) {
    return `✅ **Use:** \`${primary.model}\` (${primaryQuota}% remaining)`;
  }
  
  // Check fallback(s)
  const fallbacks = rules.fallbacks || (rules.fallback ? [rules.fallback] : []);
  for (const fb of fallbacks) {
    const fbQuota = getProviderQuota(fb.provider, quotas);
    const fbLoggedIn = loggedIn[fb.provider] && !loggedIn[fb.provider]?.isExpired;
    
    if (fbLoggedIn && (fbQuota === null || fbQuota >= FALLBACK_THRESHOLD)) {
      let reason = '';
      if (!primaryLoggedIn) {
        reason = `(${primary.provider} not logged in)`;
      } else if (primaryQuota !== null && primaryQuota < FALLBACK_THRESHOLD) {
        reason = `(${primary.model} at ${primaryQuota}%)`;
      }
      return `⚠️ **Fallback:** \`${fb.model}\` ${reason}`;
    }
  }
  
  return '❌ No available model (all providers low or not logged in)';
}

function getProviderQuota(provider, quotas) {
  switch (provider) {
    case 'google-antigravity':
      // Return lowest model quota (conservative)
      if (quotas.antigravity?.models?.length > 0) {
        return Math.min(...quotas.antigravity.models.map(m => m.remaining));
      }
      return null;
    
    case 'github-copilot':
      return quotas.copilot?.premium?.percent ?? null;
    
    case 'openai-codex':
      return quotas.codex?.primary?.remaining ?? null;
    
    default:
      return null;
  }
}

// ============ CLI ============
const args = process.argv.slice(2);

if (args.includes('--help') || args.includes('-h')) {
  console.log(`
Smart Router - Unified Quota Monitor & Model Recommender

Usage:
  node index.js              Show full dashboard
  node index.js --json       Output as JSON (for automation)
  node index.js --task=coding    Show coding recommendation
  node index.js --task=reasoning Show reasoning recommendation

Options:
  --help, -h        Show this help
  --json, -j        Output as JSON
`);
  process.exit(0);
}

generateDashboard();


path: README.md
content: # ai-quota-check

Unified quota monitor and intelligent model recommender for all AI providers (Antigravity + Copilot + OpenAI Codex).

## Features

- **Provider Login Check** - Detects logged-in providers
- **Unified Quota Dashboard** - Antigravity + Copilot + OpenAI Codex
- **Task-based Recommendations** - Optimal model selection with fallback
- **Reset Detection** - Identifies models ready for new cycle
- **Risk Level Info** - Warns about weekly caps and lockout risks

## Usage

```bash
# (from workspace root)
node skills/ai-quota-check/index.js

node skills/ai-quota-check/index.js --current-model="google-antigravity/claude-opus-4-5-thinking"
```

```bash
# (from inside this skill directory)
node index.js
node index.js --current-model="google-antigravity/claude-opus-4-5-thinking"
```

## Trigger Words

- 쿼타확인, 쿼터확인, quota, 쿼터


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)