From 633ee4aeebfed8712c7f0d6c6c10cd4f26b43aad Mon Sep 17 00:00:00 2001 From: KeiSei84 <2206745@gmail.com> Date: Tue, 26 May 2026 21:43:39 +0800 Subject: [PATCH] feat(limits): honest kei limits CLI + pet cache integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-CLI subscription limits — research-grounded honest delivery after 5-parallel-agent investigation found that 4 of 5 CLIs have no public programmatic API for quota. ## Reality findings (research) - claude no public API; `anthropic-ratelimit-*` headers per-call only; Admin API exists but needs separate admin token. See dashboard. - grok no public API; `x-ratelimit-*` headers per-call only. No file. - agy interactive /usage slash-cmd shows 100% always (forum bug). No public API. - copilot no public quota API; web dashboard only. The 'gh api /user/ copilot_billing' endpoint does NOT exist. June 2026 billing migration to AI Credits further changes the surface. - kimi Moonshot /v1/users/me/balance returns $ balance only (no session/weekly quota fields). Requires MOONSHOT_API_KEY. ## Delivery (no false promises) - scripts/kei-limits.sh — probe-all honest tool. For Kimi: real curl call to Moonshot balance API if MOONSHOT_API_KEY set. For other 4: status marker + dashboard URL. - Pet integration — reads ~/.claude/pet/limits-cache.json IF present; shows Kimi balance segment ONLY when status=='live'. Pet does NOT poll; cache is populated by user-invoked 'kei limits'. - bin/kei limits arm + --json mode + --quiet mode for cron. Cache is bounded by user's explicit refresh; pet shows '(Xm old)' if older than 1h. No background polling, no rate-limit waste, no fake data. --- bin/kei | 6 ++ scripts/kei-limits.sh | 161 ++++++++++++++++++++++++++++++++++++++++++ scripts/keisei-pet.sh | 32 +++++++++ 3 files changed, 199 insertions(+) create mode 100755 scripts/kei-limits.sh diff --git a/bin/kei b/bin/kei index 880e8f0..d21cef1 100755 --- a/bin/kei +++ b/bin/kei @@ -20,6 +20,8 @@ # kei mcp-wire [] # wire kei-mcp into a CLI's MCP config + hook setup # # (Phase C cross-CLI policy enforcement) # kei mcp-wire --list # show enforcement tier per CLI +# kei limits # probe each CLI's subscription quota (best-effort) +# # (4 of 5 CLIs have no public API — honest report) # kei --on= # one-shot launch of (does not change primary) # kei [args...] # splash → exec primary CLI (default: claude) # @@ -66,6 +68,10 @@ case "${1:-}" in shift exec "$HOME/.claude/scripts/kei-mcp-wire.sh" "$@" ;; + limits|quota|usage) + shift + exec "$HOME/.claude/scripts/kei-limits.sh" "$@" + ;; esac # --- one-shot --on= override (does not write primary.toml) ------- diff --git a/scripts/kei-limits.sh b/scripts/kei-limits.sh new file mode 100755 index 0000000..eda41b7 --- /dev/null +++ b/scripts/kei-limits.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# kei-limits — probe each installed CLI's remaining quota / balance. +# +# Reality (research 2026-05-26): +# • claude — no programmatic API. Headers per-API-call only. Admin API +# exists but needs a separate admin key. See dashboard. +# • grok — same as claude. Headers per-API-call only. No file. +# • agy — interactive /usage slash-cmd is broken (shows 100% always, +# forum-verified bug). No public API. +# • copilot — no public quota API. github.com/settings/billing only. +# Inline output during call shows usage but nothing exposed +# for poll. +# • kimi — Moonshot API /v1/users/me/balance returns $ balance only +# (no session/weekly quota). Requires MOONSHOT_API_KEY. +# +# Output: +# stdout: human summary (default) OR JSON (--json) +# file: ~/.claude/pet/limits-cache.json (always, for pet to read) +# +# Polling: NOT poll-friendly. Run on demand or via launchd at >5 min intervals. +# Pet's job: read the cache; pet does NOT call this script. + +set -u + +CACHE="${KEI_LIMITS_CACHE:-$HOME/.claude/pet/limits-cache.json}" +mkdir -p "$(dirname "$CACHE")" + +JSON_OUT=0 +QUIET=0 +for arg in "$@"; do + case "$arg" in + --json) JSON_OUT=1 ;; + --quiet) QUIET=1 ;; + -h|--help) sed -n '2,22p' "$0" | sed 's|^# \{0,1\}||'; exit 0 ;; + esac +done + +# --- per-CLI probes (each returns one JSON value to stdout) ---------------- +probe_claude() { + # No public API; produce a status marker, no live data. + printf '%s' '{"status":"no-api","note":"see claude.ai/settings/usage","dashboard":"https://claude.ai/settings/usage"}' +} + +probe_grok() { + printf '%s' '{"status":"no-api","note":"headers-only per API call; see x.ai dashboard","dashboard":"https://x.ai"}' +} + +probe_agy() { + printf '%s' '{"status":"broken-api","note":"interactive /usage shows 100% (forum-verified bug); use Google Cloud Console","dashboard":"https://console.cloud.google.com/apis/api/generativelanguage.googleapis.com/quotas"}' +} + +probe_copilot() { + # Try gh CLI graphQL — most variants don't expose Copilot billing publicly. + # If we ever find an endpoint, drop it in here. For now: status marker. + printf '%s' '{"status":"no-api","note":"see github.com/settings/billing → Copilot section","dashboard":"https://github.com/settings/billing"}' +} + +probe_kimi() { + if [ -z "${MOONSHOT_API_KEY:-}" ]; then + printf '%s' '{"status":"need-key","note":"set MOONSHOT_API_KEY in env to fetch live balance","dashboard":"https://platform.kimi.ai"}' + return + fi + # Real probe: Moonshot balance API. Honest about what we get back. + if ! command -v curl >/dev/null 2>&1; then + printf '%s' '{"status":"no-curl","note":"curl required for live probe"}' + return + fi + local resp + resp=$(curl -sS --max-time 5 \ + -H "Authorization: Bearer $MOONSHOT_API_KEY" \ + "https://api.moonshot.ai/v1/users/me/balance" 2>/dev/null || echo '') + if [ -z "$resp" ]; then + printf '%s' '{"status":"probe-failed","note":"no response (network / wrong key)"}' + return + fi + # Validate JSON shape. + local avail cash voucher + avail=$(printf '%s' "$resp" | jq -r '.data.available_balance // empty' 2>/dev/null) + if [ -z "$avail" ]; then + printf '%s' '{"status":"probe-failed","note":"API returned non-balance response"}' + return + fi + cash=$(printf '%s' "$resp" | jq -r '.data.cash_balance // 0' 2>/dev/null) + voucher=$(printf '%s' "$resp" | jq -r '.data.voucher_balance // 0' 2>/dev/null) + jq -n --arg s "live" --arg a "$avail" --arg c "$cash" --arg v "$voucher" \ + '{status:$s, available_balance_usd:($a|tonumber), cash_balance_usd:($c|tonumber), voucher_balance_usd:($v|tonumber), dashboard:"https://platform.kimi.ai"}' +} + +# --- assemble cache JSON --------------------------------------------------- +NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ) +jq -n \ + --arg ts "$NOW" \ + --argjson claude "$(probe_claude)" \ + --argjson grok "$(probe_grok)" \ + --argjson agy "$(probe_agy)" \ + --argjson copilot "$(probe_copilot)" \ + --argjson kimi "$(probe_kimi)" \ + '{ts:$ts, claude:$claude, grok:$grok, agy:$agy, copilot:$copilot, kimi:$kimi}' \ + > "$CACHE" + +# --- output ---------------------------------------------------------------- +if [ "$JSON_OUT" = "1" ]; then + cat "$CACHE" + exit 0 +fi + +if [ "$QUIET" = "1" ]; then + exit 0 +fi + +C0= CB= CG= CY= CR= CD= +if [ -t 1 ]; then + C0=$'\033[0m' + CB=$'\033[1;38;5;39m' + CG=$'\033[32m' + CY=$'\033[33m' + CR=$'\033[31m' + CD=$'\033[2m' +fi + +format_one() { + local label="$1" key="$2" data="$3" + local status note + status=$(printf '%s' "$data" | jq -r '.status') + note=$(printf '%s' "$data" | jq -r '.note // ""') + case "$status" in + live) + local avail + avail=$(printf '%s' "$data" | jq -r '.available_balance_usd // empty') + printf " ${CG}✓${C0} %-8s \$%-8s ${CD}live (Moonshot balance)${C0}\n" "$label" "$avail" + ;; + no-api|need-key) + printf " ${CY}?${C0} %-8s ${CD}%s${C0}\n" "$label" "$note" + ;; + broken-api) + printf " ${CR}✗${C0} %-8s ${CD}%s${C0}\n" "$label" "$note" + ;; + *) + printf " ${CY}?${C0} %-8s ${CD}%s${C0}\n" "$label" "$note" + ;; + esac +} + +cat <' from --json output.${C0}" diff --git a/scripts/keisei-pet.sh b/scripts/keisei-pet.sh index b98aad0..a2a3816 100644 --- a/scripts/keisei-pet.sh +++ b/scripts/keisei-pet.sh @@ -127,6 +127,37 @@ fi [ -n "$spend" ] && global+="${spend} " global="${global% }" +# v0.43: CLI subscription limits (best-effort). +# Pet does NOT poll — reads cache only. Cache populated by `kei limits`. +# Reality: 4 of 5 CLIs have no programmatic limit API (see research). Pet +# shows only what's actually available + how stale the cache is. +limits_cache="${HOME}/.claude/pet/limits-cache.json" +limits="" +if [ -f "$limits_cache" ]; then + # Cache age in seconds. + cache_ts=$(jq -r '.ts // empty' "$limits_cache" 2>/dev/null) + if [ -n "$cache_ts" ]; then + # Convert ISO8601 to epoch (macOS + Linux compatible). + cache_epoch=$( + date -j -u -f "%Y-%m-%dT%H:%M:%SZ" "$cache_ts" "+%s" 2>/dev/null \ + || date -u -d "$cache_ts" "+%s" 2>/dev/null \ + || echo 0 + ) + cache_age=$(( now - cache_epoch )) + # Kimi balance (only CLI with live API). Show $X.XX if available. + kimi_avail=$(jq -r '.kimi | select(.status=="live") | .available_balance_usd' "$limits_cache" 2>/dev/null) + if [ -n "$kimi_avail" ] && [ "$kimi_avail" != "null" ]; then + limits+="K:\$$(printf '%.2f' "$kimi_avail" 2>/dev/null) " + fi + # Stale marker if older than 1h. + if [ "$cache_age" -gt 3600 ] 2>/dev/null && [ -n "$limits" ]; then + stale_min=$((cache_age / 60)) + limits="${limits% }${dim}(${stale_min}m old)${reset} " + fi + fi +fi +limits="${limits% }" + # ── THIS session: tokens + context% (from statusLine stdin) ───────────────── sess="" if [ -n "$SLINE" ]; then @@ -172,6 +203,7 @@ proj="${PWD##*/}"; [ -z "$proj" ] && proj="~" out="" [ -n "$sess" ] && out+="${sess} " [ -n "$global" ] && out+="${dim}${global}${reset} " +[ -n "$limits" ] && out+="${dim}${limits}${reset} " [ -n "$plan" ] && out+="${plan} " out+="${color}${face}${reset}" [ -n "$message" ] && out+=" ${dim}${message}${reset}"