set -euo pipefail
readonly API_URL="https://api.github.com/copilot_internal/user"
readonly FEATURES_URL="https://github.com/settings/copilot/features"
readonly SESSION_PREFIX="copilot-powerline-github-usage-"
run_token() {
local started
started="$(ps -o lstart= -p "$1" 2>/dev/null)" || return 0
printf '%s' "$started" | cksum | cut -d' ' -f1
}
SESSION_NAME="${SESSION_PREFIX}$$-$(run_token $$)"
readonly SESSION_NAME
profile="${COPILOT_USAGE_BROWSER_PROFILE:-${HOME}/.copilot/github-usage-profile}"
cache_file="${COPILOT_USAGE_CACHE_FILE:-${HOME}/.copilot/github-usage.json}"
history_file="${COPILOT_USAGE_HISTORY_FILE:-${HOME}/.copilot/github-usage-history.jsonl}"
html_file=""
browser=false
login=false
write_cache=true
write_history=true
usage() {
cat <<'EOF'
Usage: fetch-github-copilot-usage [--browser] [--login] [--html-file FILE] [--no-cache] [--no-history]
Fetch the signed-in user's GitHub Copilot usage counter for the current
billing cycle and print JSON. By default it reads GitHub's Copilot user API
with your GitHub token (requires curl and jq).
--browser Scrape the Copilot settings page with agent-browser instead.
--login Open a persistent browser profile for one-time GitHub login.
--html-file FILE Parse a saved page text fixture instead of opening a browser.
--no-cache Do not write the result to ~/.copilot/github-usage.json.
--no-history Do not append the result to ~/.copilot/github-usage-history.jsonl.
Environment:
COPILOT_GITHUB_TOKEN, GH_TOKEN, GITHUB_TOKEN
GitHub token, checked in this order before
falling back to `gh auth token`.
COPILOT_USAGE_BROWSER_PROFILE Persistent browser profile location.
COPILOT_USAGE_CACHE_FILE Output cache location.
COPILOT_USAGE_HISTORY_FILE Append-only run history location (JSON Lines).
EOF
}
while (($#)); do
case "$1" in
--browser)
browser=true
;;
--login)
login=true
;;
--html-file)
shift
html_file="${1:-}"
[[ -n "$html_file" ]] || {
echo "--html-file requires a path" >&2
exit 2
}
;;
--no-cache)
write_cache=false
;;
--no-history)
write_history=false
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
shift
done
close_stale_browser_sessions() {
local session pid token
for session in $(agent-browser session list 2>/dev/null |
grep -Eo "${SESSION_PREFIX}[0-9]+-[0-9]+" || true); do
token="${session##*-}"
pid="${session#"$SESSION_PREFIX"}"
pid="${pid%-*}"
[[ "$(run_token "$pid")" == "$token" ]] && continue
agent-browser --session "$session" close >/dev/null 2>&1 || true
done
}
if "$login"; then
command -v agent-browser >/dev/null || {
echo "agent-browser is required. Install it before running --login." >&2
exit 1
}
close_stale_browser_sessions
agent-browser --session "$SESSION_NAME" --profile "$profile" --headed open "$FEATURES_URL" || {
status=$?
agent-browser --session "$SESSION_NAME" close >/dev/null 2>&1 || true
exit "$status"
}
cat <<'EOF'
Sign in to GitHub in the browser window, including SSO or two-factor authentication if required.
Close the window when finished, then run this command again without --login.
EOF
exit 0
fi
github_token() {
local name
for name in COPILOT_GITHUB_TOKEN GH_TOKEN GITHUB_TOKEN; do
[[ -z "${!name:-}" ]] || {
printf '%s' "${!name}"
return
}
done
command -v gh >/dev/null && gh auth token 2>/dev/null
}
readonly API_USAGE_FILTER='
def months: ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
def credits:
if . == null then error("quota_snapshots.premium_interactions is missing")
elif .unlimited == true then
if (.credits_used | type) == "number" then .credits_used
else error("credits_used is missing") end
elif .unlimited == false then
if (.entitlement | type) == "number" and .entitlement > 0
and (.quota_remaining | type) == "number"
then [0, .entitlement - .quota_remaining] | max
else error("a per-user budget needs entitlement and quota_remaining") end
else error("unlimited is missing") end;
def cycle:
if type != "string" then error("quota_reset_date is missing") else . end
| (capture("^(?<y>[0-9]{4})-(?<m>[0-9]{2})-01$")
// error("quota_reset_date \(.) is not the 1st of a month"))
| (.y | tonumber) as $reset_year
| (.m | tonumber) as $reset_month
| (if $reset_month == 1 then [$reset_year - 1, 12]
else [$reset_year, $reset_month - 1] end) as [$year, $month]
| (if $month == 2 then
(if ($year % 4 == 0 and $year % 100 != 0) or $year % 400 == 0
then 29 else 28 end)
elif ($month | IN(4, 6, 9, 11)) then 30
else 31 end) as $days
| "\(months[$month - 1]) 1-\($days), \($year)";
try "ok\t\(.quota_snapshots.premium_interactions | credits)\t\(.quota_reset_date | cycle)"
catch "error\t\(.)"
'
read_api_usage() {
local tool token response parsed
for tool in curl jq; do
command -v "$tool" >/dev/null || {
echo "$tool is required. Install it, or pass --browser to scrape the settings page." >&2
exit 1
}
done
token="$(github_token)" || token=""
[[ -n "$token" ]] || {
echo "No GitHub token found. Run gh auth login, or set GH_TOKEN." >&2
exit 1
}
response="$(printf 'Authorization: Bearer %s\n' "$token" |
curl --silent --show-error --fail --max-time 15 \
--header @- --header 'Accept: application/json' "$API_URL")" || {
echo "GitHub API request failed: $API_URL" >&2
exit 1
}
parsed="$(printf '%s' "$response" | jq -r "$API_USAGE_FILTER" 2>/dev/null)" || {
echo "The GitHub API response is not valid JSON." >&2
exit 1
}
case "$parsed" in
ok$'\t'*)
IFS=$'\t' read -r _ credits cycle <<<"$parsed"
;;
*)
echo "Unexpected Copilot user API response: ${parsed#error$'\t'}" >&2
exit 1
;;
esac
}
read_page_usage() {
local page_text
if [[ -n "$html_file" ]]; then
[[ -r "$html_file" ]] || {
echo "Cannot read page text: $html_file" >&2
exit 1
}
page_text="$(<"$html_file")"
else
command -v agent-browser >/dev/null || {
echo "agent-browser is required. Run this script with --login after installing it." >&2
exit 1
}
close_stale_browser_sessions
cleanup_browser_session() {
agent-browser --session "$SESSION_NAME" close >/dev/null 2>&1 || true
}
trap cleanup_browser_session EXIT
agent-browser --session "$SESSION_NAME" --profile "$profile" open "$FEATURES_URL" >/dev/null
page_text="$(agent-browser --session "$SESSION_NAME" read)"
agent-browser --session "$SESSION_NAME" close >/dev/null
trap - EXIT
fi
if [[ "$page_text" == *"Sign in to GitHub"* ]]; then
echo "GitHub login is required. Run: $0 --login" >&2
exit 1
fi
if ! credits="$(printf '%s\n' "$page_text" |
grep -Em1 -o '[0-9][0-9,]* AI credits used' |
tr -cd '0-9')"; then
credits=""
fi
if ! cycle="$(printf '%s\n' "$page_text" |
grep -Em1 -o '[A-Z][a-z]+ [0-9]{1,2}-[0-9]{1,2}, [0-9]{4}')"; then
cycle=""
fi
[[ -n "$credits" && -n "$cycle" ]] || {
echo "Could not find Copilot usage on the features page. GitHub may have changed the page." >&2
exit 1
}
}
if "$browser" || [[ -n "$html_file" ]]; then
read_page_usage
else
read_api_usage
fi
usd="$(awk -v credits="$credits" 'BEGIN { printf "%.2f", credits / 100 }')"
result="$(printf '{"ai_credits_used":%s,"usd":"%s","cycle":"%s"}' "$credits" "$usd" "$cycle")"
if "$write_cache"; then
umask 077
mkdir -p "$(dirname "$cache_file")"
temporary_cache="$(mktemp "${cache_file}.tmp.XXXXXX")"
printf '%s\n' "$result" >"$temporary_cache"
mv "$temporary_cache" "$cache_file"
fi
if "$write_history"; then
umask 077
mkdir -p "$(dirname "$history_file")"
timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf '{"ts":"%s","ai_credits_used":%s,"usd":"%s","cycle":"%s"}\n' \
"$timestamp" "$credits" "$usd" "$cycle" >>"$history_file"
fi
printf '%s\n' "$result"