* Let Grok updates through mise take effect Grok's npm launcher runs ~/.grok/bin/grok whenever it exists, and the install script that repoints it at a new release doesn't run under mise, so every machine kept running the release it first unpacked. Updating mise tools now drops a link to an older release and the old binary, and lets the launcher unpack the installed one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep several Grok accounts, the way Claude and Codex do Grok honors GROK_HOME, so an added Grok account gets its own home holding only its login and settings cache, with the CLI binary, sessions, skills, plugins, memory, and config linked back to ~/.grok. The account commands, the add flow (a second account signs in through a private window), the launcher, and a grok shell function all take it like the others, and its identity and plan come from the files Grok writes at login. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Show Grok's plan and credits in the agents panel A grok collector reads each account's plan from the settings Grok caches and its credits from the endpoint behind Grok's own /usage view, so Grok gets a section, per-account limits, and autoswitch like Claude and Codex. A signed-in agent with a plan now shows before its first numbers, so a lapsed sign-in has somewhere to say so. Grok's mark joins the assets, and with every agent able to take another account the add screen no longer needs to dim one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document Grok accounts and limits alongside Claude and Codex Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Read Grok's credits the way xAI actually answers The answer nests under config, names a weekly period with its end, and as protobuf JSON leaves the usage percentage out while it's zero. The collector now reads that shape, so a fresh week shows as 0% until it resets rather than as nothing known. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Count Grok's prompts and sessions from its session summaries Grok keeps a summary beside each session with when it was last active and how many prompts it had, so its section and the hero's summary get today's prompts and sessions and its active days. It records no token counts there, so none are claimed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give each Grok limits cache write its own temporary file Two overlapping collector runs wrote the same cache through one .tmp path, so one rename could leave the other's failing and its record not updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Read a single Grok account from GROK_HOME when it's set The launcher and the CLI honor GROK_HOME, but the collector always read ~/.grok, so a Grok signed in only in a custom home showed nothing. With one account, the collector now reads the home the CLI would; with several registered, the primary stays ~/.grok. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop guesses the real Grok credits answer made unnecessary The collector carried fallbacks for fields and shapes guessed from the binary before xAI's actual answer was seen, a numeric timestamp branch nothing writes, the panel's default for prompt stats, and guards that an empty sign-in already covers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the working Grok release until its update is in place The update dropped the link and older binaries before the new release was unpacked and ignored a failed unpack, which could leave every Grok account without a CLI. The old release now stays until the new one is linked, and its link comes back if it never is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Count only Grok sessions for today, not prompts A session's summary holds all its prompts and only when it was last active, so a resumed session put its whole history on today. Today now counts the sessions active in it; prompts stay an all-time total. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Reuse the Grok session scan on a limits-only refresh Opening the panel and near-limit checks only need fresh limits, so they reuse a scan up to 15 minutes old, as the Codex collector does; --force still rescans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
123 lines
6.8 KiB
Bash
Executable File
123 lines
6.8 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
|
|
|
require_command jq
|
|
require_command python3
|
|
|
|
test_tmp=$(mktemp -d)
|
|
trap 'rm -rf "$test_tmp"' EXIT
|
|
|
|
export HOME="$test_tmp/home"
|
|
export XDG_STATE_HOME="$test_tmp/state"
|
|
export XDG_CACHE_HOME="$test_tmp/cache"
|
|
|
|
future=$(python3 -c 'import datetime as dt; print((dt.datetime.now(dt.timezone.utc) + dt.timedelta(hours=6)).isoformat())')
|
|
past=$(python3 -c 'import datetime as dt; print((dt.datetime.now(dt.timezone.utc) - dt.timedelta(hours=1)).isoformat())')
|
|
period_end=$(python3 -c 'import datetime as dt; print((dt.datetime.now(dt.timezone.utc) + dt.timedelta(days=9)).isoformat())')
|
|
|
|
# A home signed in the way the Grok CLI leaves it.
|
|
signed_in() {
|
|
mkdir -p "$1"
|
|
jq -nc --arg token "$2" --arg user "$3" --arg expires "$4" \
|
|
'{"https://auth.x.ai::client": {key: $token, user_id: $user, email: ($user + "@example.com"), expires_at: $expires}}' >"$1/auth.json"
|
|
printf '{"payload":"{\\"settings\\":{\\"subscription_tier_display\\":\\"%s\\"}}"}\n' "$5" >"$1/settings_cache.json"
|
|
}
|
|
|
|
# xAI answers per token, so each home's probe is told apart by its sign-in.
|
|
collect() {
|
|
COLLECTOR="$ROOT/bin/omarchy-agent-usage-grok" python3 - "$@" <<'PY'
|
|
import importlib.machinery, importlib.util, io, json, os, sys
|
|
|
|
loader = importlib.machinery.SourceFileLoader("collector", os.environ["COLLECTOR"])
|
|
spec = importlib.util.spec_from_loader(loader.name, loader)
|
|
collector = importlib.util.module_from_spec(spec)
|
|
loader.exec_module(collector)
|
|
|
|
used = {"token-main": 42.0, "token-side": 7.0, "token-fresh": 0}
|
|
end = os.environ["PERIOD_END"]
|
|
|
|
# Shaped like xAI's answer: protobuf JSON under config, which drops a
|
|
# percentage of zero.
|
|
def urlopen(request, timeout=None):
|
|
assert request.full_url == collector.CREDITS_URL
|
|
token = request.get_header("Authorization").split(" ", 1)[1]
|
|
config = {"currentPeriod": {"type": "USAGE_PERIOD_TYPE_WEEKLY", "end": end}, "billingPeriodEnd": end}
|
|
if used.get(token):
|
|
config["creditUsagePercent"] = used[token]
|
|
return io.BytesIO(json.dumps({"config": config}).encode())
|
|
|
|
collector.urllib.request.urlopen = urlopen
|
|
sys.argv = ["omarchy-agent-usage-grok"] + (os.environ.get("COLLECT_ARGS") or "--force").split()
|
|
collector.main()
|
|
PY
|
|
}
|
|
export PERIOD_END="$period_end"
|
|
|
|
record=$(collect)
|
|
[[ $(jq -c '{ready, tierLabel, limits}' <<<"$record") == '{"ready":false,"tierLabel":"","limits":[]}' ]] ||
|
|
fail "a machine nobody signed in to Grok on gives an empty record" "$record"
|
|
pass "a machine nobody signed in to Grok on gives an empty record"
|
|
|
|
# A home chosen with GROK_HOME is the one read, as the CLI itself does.
|
|
signed_in "$test_tmp/custom-grok" token-main u-main "$future" "SuperGrok"
|
|
record=$(GROK_HOME="$test_tmp/custom-grok" collect)
|
|
[[ $(jq -c '{ready, tierLabel, percent: .limits[0].percent}' <<<"$record") == '{"ready":true,"tierLabel":"SuperGrok","percent":0.42}' ]] ||
|
|
fail "a single Grok account in GROK_HOME is read from there" "$record"
|
|
pass "a single Grok account in GROK_HOME is read from there"
|
|
rm -f "$XDG_CACHE_HOME"/omarchy/agent-usage/grok-limits-*.json
|
|
|
|
signed_in "$HOME/.grok" token-main u-main "$future" "X Premium+"
|
|
record=$(collect)
|
|
[[ $(jq -c '{ready, tierLabel, stale: .limitsStale, label: .limits[0].label, percent: .limits[0].percent}' <<<"$record") == '{"ready":true,"tierLabel":"X Premium+","stale":false,"label":"Weekly","percent":0.42}' ]] ||
|
|
fail "Grok's plan and credits come from its own home" "$record"
|
|
[[ -n $(jq -r '.limits[0].resetsAt' <<<"$record") ]] || fail "the credits window says when the period ends" "$record"
|
|
pass "Grok's plan and credits come from its own home"
|
|
|
|
signed_in "$HOME/.grok" token-main u-main "$past" "X Premium+"
|
|
record=$(collect)
|
|
[[ $(jq -c '{usageStatusText, first: .limits[0].percent, stale: .limitsStale}' <<<"$record") == '{"usageStatusText":"Sign-in expired","first":0.42,"stale":true}' ]] ||
|
|
fail "a lapsed sign-in keeps the last credits and says so" "$record"
|
|
pass "a lapsed sign-in keeps the last credits and says so"
|
|
|
|
# A period with nothing used yet comes without a percentage.
|
|
signed_in "$HOME/.grok" token-fresh u-main "$future" "X Premium+"
|
|
rm -f "$XDG_CACHE_HOME"/omarchy/agent-usage/grok-limits-*.json
|
|
record=$(collect)
|
|
[[ $(jq -c '.limits[0] | {label, percent}' <<<"$record") == '{"label":"Weekly","percent":0.0}' ]] ||
|
|
fail "an untouched period reads as 0% rather than nothing known" "$record"
|
|
pass "an untouched period reads as 0% rather than nothing known"
|
|
|
|
signed_in "$HOME/.grok" token-main u-main "$future" "X Premium+"
|
|
side="$XDG_STATE_HOME/omarchy/agents/accounts/grok/side"
|
|
signed_in "$side" token-side u-side "$future" "SuperGrok"
|
|
jq -n --arg side "$side" '{active: "side", switch: "auto", threshold: 90, accounts: [
|
|
{id: "main", label: "Main", home: "", primary: true},
|
|
{id: "side", label: "Side", home: $side, primary: false}
|
|
]}' >"$XDG_STATE_HOME/omarchy/agents/accounts/grok.json"
|
|
record=$(collect)
|
|
[[ $(jq -c '{tierLabel, first: .limits[0].percent, accounts: [.accounts[] | {id, active, plan, percent: .limits[0].percent}], switch: .accountSwitch}' <<<"$record") == '{"tierLabel":"SuperGrok","first":0.07,"accounts":[{"id":"main","active":false,"plan":"X Premium+","percent":0.42},{"id":"side","active":true,"plan":"SuperGrok","percent":0.07}],"switch":{"mode":"auto","threshold":90}}' ]] ||
|
|
fail "every Grok account reports its own plan and credits" "$record"
|
|
pass "every Grok account reports its own plan and credits"
|
|
|
|
# Prompts and sessions come from each session's summary; no tokens are claimed.
|
|
sessions="$HOME/.grok/sessions/%2Fhome%2Fme"
|
|
now=$(python3 -c 'import datetime as dt; print(dt.datetime.now(dt.timezone.utc).isoformat())')
|
|
mkdir -p "$sessions/today-1" "$sessions/today-2" "$sessions/old"
|
|
jq -n --arg at "$now" '{last_active_at: $at, num_messages: 3, current_model_id: "grok-4.7"}' >"$sessions/today-1/summary.json"
|
|
jq -n --arg at "$now" '{last_active_at: $at, num_messages: 2}' >"$sessions/today-2/summary.json"
|
|
jq -n '{last_active_at: "2026-01-02T10:00:00Z", num_messages: 5}' >"$sessions/old/summary.json"
|
|
record=$(collect)
|
|
[[ $(jq -c '{hasLocalStats, todaySessions, totalPrompts, totalSessions, activeDays, claimed: (has("todayTotalTokens") or has("todayPrompts"))}' <<<"$record") == '{"hasLocalStats":true,"todaySessions":2,"totalPrompts":10,"totalSessions":3,"activeDays":2,"claimed":false}' ]] ||
|
|
fail "Grok counts prompts and sessions from its session summaries" "$record"
|
|
pass "Grok counts prompts and sessions from its session summaries"
|
|
|
|
# A limits-only refresh reuses the last scan rather than reading every summary.
|
|
mkdir -p "$sessions/later"
|
|
jq -n --arg at "$now" '{last_active_at: $at, num_messages: 1}' >"$sessions/later/summary.json"
|
|
record=$(COLLECT_ARGS="--limits-only" collect)
|
|
[[ $(jq -r '.totalSessions' <<<"$record") == 3 ]] || fail "a limits-only refresh reuses the session scan" "$record"
|
|
pass "a limits-only refresh reuses the session scan"
|