Bring Grok up to par with Claude and Codex in the agents panel (#13992)
* 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>
This commit is contained in:
1 parent
c05d90196f
commit
f45461a38f
23 files changed
+636
-105
No files matched your search
+13
-10
@@ -136,16 +136,19 @@ pi)
|
||||
;;
|
||||
esac
|
||||
|
||||
# Claude and Codex start as the active subscription account, the same as the
|
||||
# shell functions in default/bash/fns/agent-accounts. The home travels in the
|
||||
# command itself, since a terminal launched through uwsm doesn't inherit this
|
||||
# process's environment.
|
||||
if [[ $agent == "claude" && -z ${CLAUDE_CONFIG_DIR:-} ]]; then
|
||||
home=$(omarchy-agent-account-home claude)
|
||||
[[ -n $home ]] && command=(env "CLAUDE_CONFIG_DIR=$home" "${command[@]}")
|
||||
elif [[ $agent == "codex" && -z ${CODEX_HOME:-} ]]; then
|
||||
home=$(omarchy-agent-account-home codex)
|
||||
[[ -n $home ]] && command=(env "CODEX_HOME=$home" "${command[@]}")
|
||||
# Claude, Codex, and Grok start as the active subscription account, the same
|
||||
# as the shell functions in default/bash/fns/agent-accounts. The home travels
|
||||
# in the command itself, since a terminal launched through uwsm doesn't
|
||||
# inherit this process's environment.
|
||||
case "$agent" in
|
||||
claude) home_env="CLAUDE_CONFIG_DIR" ;;
|
||||
codex) home_env="CODEX_HOME" ;;
|
||||
grok) home_env="GROK_HOME" ;;
|
||||
*) home_env="" ;;
|
||||
esac
|
||||
if [[ -n $home_env && -z ${!home_env:-} ]]; then
|
||||
home=$(omarchy-agent-account-home "$agent")
|
||||
[[ -n $home ]] && command=(env "$home_env=$home" "${command[@]}")
|
||||
fi
|
||||
|
||||
if [[ $inline == "true" ]]; then
|
||||
|
||||
@@ -32,21 +32,15 @@ done
|
||||
|
||||
# Whether a provider's own home (~/.claude, ~/.codex, ~/.grok) is signed in.
|
||||
primary_signed_in() {
|
||||
if [[ $1 == "grok" ]]; then
|
||||
[[ -s $HOME/.grok/auth.json ]]
|
||||
else
|
||||
omarchy-agent-account-state list "$1" | jq -e '.[0].accounts | any(.primary and .signedIn)' >/dev/null
|
||||
fi
|
||||
omarchy-agent-account-state list "$1" | jq -e '.[0].accounts | any(.primary and .signedIn)' >/dev/null
|
||||
}
|
||||
|
||||
if [[ $check == "true" ]]; then
|
||||
for p in claude codex grok; do
|
||||
if ! primary_signed_in "$p"; then
|
||||
echo "$p first"
|
||||
elif [[ $p == "grok" ]]; then
|
||||
echo "$p unsupported"
|
||||
else
|
||||
if primary_signed_in "$p"; then
|
||||
echo "$p additional"
|
||||
else
|
||||
echo "$p first"
|
||||
fi
|
||||
done
|
||||
exit 0
|
||||
@@ -60,9 +54,9 @@ if [[ ${1:-} != "claude" && ${1:-} != "codex" && ${1:-} != "grok" ]]; then
|
||||
fi
|
||||
|
||||
case "${1:-}" in
|
||||
claude) provider="claude"; name="Claude"; site="claude.ai"; package="claude" ;;
|
||||
codex) provider="codex"; name="Codex"; site="chatgpt.com"; package="codex" ;;
|
||||
grok) provider="grok"; name="Grok"; site="x.ai"; package="npm:@xai-official/grok" ;;
|
||||
claude) provider="claude"; name="Claude"; site="claude.ai"; package="claude"; home_env="CLAUDE_CONFIG_DIR" ;;
|
||||
codex) provider="codex"; name="Codex"; site="chatgpt.com"; package="codex"; home_env="CODEX_HOME" ;;
|
||||
grok) provider="grok"; name="Grok"; site="x.ai"; package="npm:@xai-official/grok"; home_env="GROK_HOME" ;;
|
||||
*)
|
||||
echo "Usage: omarchy-agent-account-add [--check] [--events] [--reauth <account>] [claude|codex|grok] [label]" >&2
|
||||
exit 1
|
||||
@@ -151,7 +145,7 @@ login() {
|
||||
# primary home through the normal browser, an added one through a private
|
||||
# window. Its home is never recreated or removed.
|
||||
if [[ -n $reauth ]]; then
|
||||
if [[ $provider == "grok" || $reauth == ":primary" ]]; then
|
||||
if [[ $reauth == ":primary" ]]; then
|
||||
account_home="$HOME/.$provider"
|
||||
else
|
||||
account_home=$(omarchy-agent-account-state homes "$provider" | awk -F'\t' -v id="$reauth" '$1 == id { print $2 }')
|
||||
@@ -164,11 +158,7 @@ if [[ -n $reauth ]]; then
|
||||
else
|
||||
say "Sign in again as this account in the private window that opens."
|
||||
private_browser
|
||||
if [[ $provider == "claude" ]]; then
|
||||
interruptible env BROWSER="$browser" CLAUDE_CONFIG_DIR="$account_home" claude auth login || fail "The $name sign-in didn't finish."
|
||||
else
|
||||
interruptible env BROWSER="$browser" CODEX_HOME="$account_home" codex login || fail "The $name sign-in didn't finish."
|
||||
fi
|
||||
interruptible env BROWSER="$browser" "$home_env=$account_home" "${login_command[@]}" || fail "The $name sign-in didn't finish."
|
||||
fi
|
||||
|
||||
omarchy-agent-usage-update --force "$provider" >/dev/null 2>&1 &
|
||||
@@ -193,8 +183,6 @@ if ! primary_signed_in "$provider"; then
|
||||
else
|
||||
fail "The $name sign-in didn't finish."
|
||||
fi
|
||||
elif [[ $provider == "grok" ]]; then
|
||||
fail "Grok is already signed in, and a second Grok account isn't supported yet."
|
||||
else
|
||||
if [[ -z $label && $events == "false" && -t 0 ]]; then
|
||||
label=$(gum input --placeholder "Work" --prompt "Name this $name account: ") || exit 130
|
||||
@@ -216,12 +204,7 @@ EOF
|
||||
home=$(omarchy-agent-account-state begin "$provider") || fail "Couldn't set up a home for the new account."
|
||||
|
||||
private_browser
|
||||
|
||||
if [[ $provider == "claude" ]]; then
|
||||
interruptible env BROWSER="$browser" CLAUDE_CONFIG_DIR="$home" claude auth login
|
||||
else
|
||||
interruptible env BROWSER="$browser" CODEX_HOME="$home" codex login
|
||||
fi
|
||||
interruptible env BROWSER="$browser" "$home_env=$home" "${login_command[@]}"
|
||||
|
||||
if account=$(omarchy-agent-account-state register "$provider" "$label" "$home" 2>&1); then
|
||||
home=""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Print the config home of the active Claude or Codex account
|
||||
# omarchy:args=<claude|codex>
|
||||
# omarchy:summary=Print the config home of the active Claude, Codex, or Grok account
|
||||
# omarchy:args=<claude|codex|grok>
|
||||
# omarchy:hidden=true
|
||||
|
||||
# Prints nothing while the primary account (~/.claude, ~/.codex) is active, so
|
||||
@@ -10,8 +10,8 @@
|
||||
# every `claude` and `codex` typed at a prompt.
|
||||
registry="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/agents/accounts/${1:-}.json"
|
||||
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" ]]; then
|
||||
echo "Usage: omarchy-agent-account-home <claude|codex>" >&2
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" && ${1:-} != "grok" ]]; then
|
||||
echo "Usage: omarchy-agent-account-home <claude|codex|grok>" >&2
|
||||
exit 1
|
||||
elif [[ -f $registry ]]; then
|
||||
exec omarchy-agent-account-state home "$1"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=List Claude and Codex subscription accounts and their limits
|
||||
# omarchy:args=[claude|codex] [--json]
|
||||
# omarchy:summary=List Claude, Codex, and Grok subscription accounts and their limits
|
||||
# omarchy:args=[claude|codex|grok] [--json]
|
||||
# omarchy:examples=omarchy agent account list | omarchy agent account list claude --json
|
||||
|
||||
set -uo pipefail
|
||||
@@ -10,10 +10,10 @@ provider=""
|
||||
json=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
claude | codex) provider="$arg" ;;
|
||||
claude | codex | grok) provider="$arg" ;;
|
||||
--json) json=true ;;
|
||||
*)
|
||||
echo "Usage: omarchy-agent-account-list [claude|codex] [--json]" >&2
|
||||
echo "Usage: omarchy-agent-account-list [claude|codex|grok] [--json]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -26,7 +26,7 @@ accounts=$(omarchy-agent-account-state list $provider) || exit 1
|
||||
# a single account the collectors keep its limits at the record's top level.
|
||||
accounts=$(jq -c --arg dir "$usage_dir" '
|
||||
map(. as $p | .accounts |= map(. + {limits: []}))' <<<"$accounts")
|
||||
for p in claude codex; do
|
||||
for p in claude codex grok; do
|
||||
record="$usage_dir/$p.json"
|
||||
[[ -f $record ]] || continue
|
||||
accounts=$(jq -c --arg p "$p" --slurpfile record "$record" '
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Switch Claude or Codex accounts automatically near a limit, or only notify
|
||||
# omarchy:args=[claude|codex] [manual|auto] [threshold]
|
||||
# omarchy:summary=Switch Claude, Codex, or Grok accounts automatically near a limit, or only notify
|
||||
# omarchy:args=[claude|codex|grok] [manual|auto] [threshold]
|
||||
# omarchy:examples=omarchy agent account mode auto | omarchy agent account mode auto 90 | omarchy agent account mode codex manual
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# The provider can be left out when it's your default agent:
|
||||
# `omarchy agent account use work`.
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" ]]; then
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" && ${1:-} != "grok" ]]; then
|
||||
default=$(omarchy-default-agent)
|
||||
[[ $default == "claude" || $default == "codex" ]] && set -- "$default" "$@"
|
||||
[[ $default == "claude" || $default == "codex" || $default == "grok" ]] && set -- "$default" "$@"
|
||||
fi
|
||||
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" ]]; then
|
||||
echo "Usage: omarchy-agent-account-mode [claude|codex] [manual|auto] [threshold]" >&2
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" && ${1:-} != "grok" ]]; then
|
||||
echo "Usage: omarchy-agent-account-mode [claude|codex|grok] [manual|auto] [threshold]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Forget an added Claude or Codex account
|
||||
# omarchy:args=[claude|codex] <account>
|
||||
# omarchy:summary=Forget an added Claude, Codex, or Grok account
|
||||
# omarchy:args=[claude|codex|grok] <account>
|
||||
# omarchy:examples=omarchy agent account remove work | omarchy agent account remove codex side
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# The provider can be left out when it's your default agent:
|
||||
# `omarchy agent account use work`.
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" ]]; then
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" && ${1:-} != "grok" ]]; then
|
||||
default=$(omarchy-default-agent)
|
||||
[[ $default == "claude" || $default == "codex" ]] && set -- "$default" "$@"
|
||||
[[ $default == "claude" || $default == "codex" || $default == "grok" ]] && set -- "$default" "$@"
|
||||
fi
|
||||
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" ]] || [[ -z ${2:-} ]]; then
|
||||
echo "Usage: omarchy-agent-account-remove [claude|codex] <account>" >&2
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" && ${1:-} != "grok" ]] || [[ -z ${2:-} ]]; then
|
||||
echo "Usage: omarchy-agent-account-remove [claude|codex|grok] <account>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Rename a Claude or Codex subscription account
|
||||
# omarchy:args=[claude|codex] <account> <name>
|
||||
# omarchy:summary=Rename a Claude, Codex, or Grok subscription account
|
||||
# omarchy:args=[claude|codex|grok] <account> <name>
|
||||
# omarchy:examples=omarchy agent account rename primary Hey | omarchy agent account rename codex side Side project
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# The provider can be left out when it's your default agent:
|
||||
# `omarchy agent account rename primary Hey`.
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" ]]; then
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" && ${1:-} != "grok" ]]; then
|
||||
default=$(omarchy-default-agent)
|
||||
[[ $default == "claude" || $default == "codex" ]] && set -- "$default" "$@"
|
||||
[[ $default == "claude" || $default == "codex" || $default == "grok" ]] && set -- "$default" "$@"
|
||||
fi
|
||||
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" ]] || (( $# < 3 )); then
|
||||
echo "Usage: omarchy-agent-account-rename [claude|codex] <account> <name>" >&2
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" && ${1:-} != "grok" ]] || (( $# < 3 )); then
|
||||
echo "Usage: omarchy-agent-account-rename [claude|codex|grok] <account> <name>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/python3
|
||||
# omarchy:summary=Read and change the registry of Claude and Codex subscription accounts
|
||||
# omarchy:summary=Read and change the registry of Claude, Codex, and Grok subscription accounts
|
||||
# omarchy:args=<registry|home|homes|list|refresh|begin|register|use|rename|remove|mode|link|autoswitch> [provider] [args...]
|
||||
# omarchy:hidden=true
|
||||
"""The account registry behind `omarchy agent account`.
|
||||
|
||||
An account is a Claude Code or Codex login Omarchy keeps a home for. The
|
||||
login you already have stays where the CLI put it (~/.claude, ~/.codex) and
|
||||
An account is a Claude Code, Codex, or Grok login Omarchy keeps a home for.
|
||||
The login you already have stays where the CLI put it (~/.claude, ~/.codex,
|
||||
~/.grok) and
|
||||
is the primary account; added accounts get a home of their own under
|
||||
~/.local/state/omarchy/agents/accounts/<provider>/<id>/ that holds only that
|
||||
account's credentials and links everything else back to the primary home, so
|
||||
@@ -51,13 +52,22 @@ PROVIDERS = {
|
||||
"shared_files": ["config.toml", "AGENTS.md", "history.jsonl", "hooks.json"],
|
||||
"carried_keys": [],
|
||||
},
|
||||
"grok": {
|
||||
"name": "Grok",
|
||||
"home_env": "GROK_HOME",
|
||||
# bin holds the unpacked CLI itself: sharing it spares every added account
|
||||
# its own 160MB copy, which the npm launcher unpacks into an empty home.
|
||||
"shared_dirs": ["bin", "sessions", "skills", "agents", "plugins", "installed-plugins", "marketplace-cache", "hooks", "memory", "memory-v2", "rules", "commands", "workflows", "personas", "worktrees"],
|
||||
"shared_files": ["config.toml", "settings.json", "lsp.json", "pager.toml", "sandbox.toml", "trusted_folders.toml", "statusline.sh", "worktrees.db"],
|
||||
"carried_keys": [],
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_THRESHOLD = 95
|
||||
# Ids an account can't take because something else already answers to them:
|
||||
# `use next` cycles, a provider name is read as the provider when it comes
|
||||
# first, and the menu's Add Account row is `.add`.
|
||||
RESERVED_IDS = {"next", "add", "claude", "codex", "primary"}
|
||||
RESERVED_IDS = {"next", "add", "claude", "codex", "grok", "primary"}
|
||||
SWITCH_MODES = ("manual", "auto")
|
||||
|
||||
|
||||
@@ -85,11 +95,12 @@ def usage_record_path(provider: str) -> Path:
|
||||
|
||||
|
||||
# The primary home is where the CLI keeps a login when nothing tells it
|
||||
# otherwise. It is deliberately not read from CLAUDE_CONFIG_DIR / CODEX_HOME:
|
||||
# otherwise. It is deliberately not read from CLAUDE_CONFIG_DIR / CODEX_HOME /
|
||||
# GROK_HOME:
|
||||
# those are how an added account is selected, so honoring them here would make
|
||||
# whichever account launched this command look like the primary.
|
||||
def primary_home(provider: str) -> Path:
|
||||
return Path.home() / (".claude" if provider == "claude" else ".codex")
|
||||
return Path.home() / f".{provider}"
|
||||
|
||||
|
||||
def account_home(provider: str, account: dict[str, Any]) -> Path:
|
||||
@@ -147,6 +158,9 @@ def identity(provider: str, home: Path) -> dict[str, str]:
|
||||
"plan": plan_label(str(login.get("rateLimitTier") or ""), str(login.get("subscriptionType") or "")),
|
||||
}
|
||||
|
||||
if provider == "grok":
|
||||
return grok_identity(home)
|
||||
|
||||
auth = read_json(home / "auth.json") or {}
|
||||
tokens = auth.get("tokens") or {}
|
||||
claims = jwt_claims(str(tokens.get("id_token") or ""))
|
||||
@@ -160,6 +174,23 @@ def identity(provider: str, home: Path) -> dict[str, str]:
|
||||
}
|
||||
|
||||
|
||||
# Grok keeps one login per issuer in auth.json, and the plan only in the
|
||||
# settings it caches from xAI, as a JSON string inside that file.
|
||||
def grok_identity(home: Path) -> dict[str, str]:
|
||||
auth = read_json(home / "auth.json") or {}
|
||||
login = next((v for v in auth.values() if isinstance(v, dict)), {}) if isinstance(auth, dict) else {}
|
||||
try:
|
||||
settings = json.loads((read_json(home / "settings_cache.json") or {}).get("payload") or "{}").get("settings") or {}
|
||||
except Exception:
|
||||
settings = {}
|
||||
return {
|
||||
"accountId": str(login.get("user_id") or ""),
|
||||
"email": str(login.get("email") or ""),
|
||||
"org": "",
|
||||
"plan": str(settings.get("subscription_tier_display") or ""),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- registry
|
||||
|
||||
|
||||
@@ -655,7 +686,7 @@ def autoswitch(provider: str, now: dt.datetime | None = None) -> bool:
|
||||
|
||||
def provider_arg(args: list[str]) -> str:
|
||||
if not args or args[0] not in PROVIDERS:
|
||||
raise AccountError("Name a provider: claude or codex")
|
||||
raise AccountError("Name a provider: claude, codex, or grok")
|
||||
return args[0]
|
||||
|
||||
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Choose which Claude or Codex account new sessions use
|
||||
# omarchy:args=[claude|codex] [account|next]
|
||||
# omarchy:summary=Choose which Claude, Codex, or Grok account new sessions use
|
||||
# omarchy:args=[claude|codex|grok] [account|next]
|
||||
# omarchy:examples=omarchy agent account use work | omarchy agent account use main | omarchy agent account use codex next
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# The provider can be left out when it's your default agent:
|
||||
# `omarchy agent account use work`.
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" ]]; then
|
||||
if [[ ${1:-} != "claude" && ${1:-} != "codex" && ${1:-} != "grok" ]]; then
|
||||
default=$(omarchy-default-agent)
|
||||
[[ $default == "claude" || $default == "codex" ]] && set -- "$default" "$@"
|
||||
[[ $default == "claude" || $default == "codex" || $default == "grok" ]] && set -- "$default" "$@"
|
||||
fi
|
||||
|
||||
case "${1:-}" in
|
||||
claude) name="Claude" ;;
|
||||
codex) name="Codex" ;;
|
||||
grok) name="Grok" ;;
|
||||
*)
|
||||
echo "Usage: omarchy-agent-account-use [claude|codex] [account|next]" >&2
|
||||
echo "Usage: omarchy-agent-account-use [claude|codex|grok] [account|next]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
Executable
+290
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/python3
|
||||
# omarchy:summary=Print the Grok usage record as JSON
|
||||
# omarchy:args=[--force] [--limits-only]
|
||||
# omarchy:hidden=true
|
||||
"""Collect Grok usage into one display-ready JSON record.
|
||||
|
||||
The plan comes from the settings Grok caches from xAI, and the limit from the
|
||||
credits endpoint the Grok CLI itself reads for its /usage view, asked with
|
||||
each account's own sign-in. Sessions and prompts come from the summary Grok
|
||||
keeps beside each session; it records no token counts there, nor when each
|
||||
prompt was sent, so neither tokens nor today's prompts are claimed. The
|
||||
agents panel only ever reads the JSON this prints.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
AGENT_ID = "grok"
|
||||
AGENT_NAME = "Grok"
|
||||
AUTH_HELP = "Start Grok, or run `grok login`, to sign in."
|
||||
CREDITS_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits"
|
||||
# Grok's own CLI watches its subscription once a minute; a panel opened and
|
||||
# shut repeatedly reuses an answer for about that long.
|
||||
PROBE_REUSE_SECONDS = 60
|
||||
# A limits-only refresh needs fresh limits, not a fresh history scan.
|
||||
LIMITS_ONLY_SCAN_REUSE_SECONDS = 900
|
||||
|
||||
|
||||
def read_json(path):
|
||||
try:
|
||||
return json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# A temporary file of its own per write, so overlapping runs can't rename
|
||||
# each other's away.
|
||||
def write_json(path, payload):
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, tmp = tempfile.mkstemp(prefix=path.name + ".", dir=str(path.parent))
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle)
|
||||
os.replace(tmp, path)
|
||||
except Exception:
|
||||
Path(tmp).unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
|
||||
def cache_root():
|
||||
return Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache")) / "omarchy" / "agent-usage"
|
||||
|
||||
|
||||
def parse_time(value):
|
||||
try:
|
||||
parsed = datetime.fromisoformat(str(value or "").replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return None
|
||||
return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
|
||||
|
||||
|
||||
# The home Grok uses when nothing selects an account: GROK_HOME when set,
|
||||
# as the CLI itself reads it, else ~/.grok.
|
||||
def default_home():
|
||||
return Path(os.environ.get("GROK_HOME") or (Path.home() / ".grok"))
|
||||
|
||||
|
||||
# Every session's summary, under <home>/sessions/<folder>/<session>/. Added
|
||||
# accounts link their sessions to the primary's, so this covers all of them.
|
||||
# A resumed session's prompts can't be told apart by day, so today counts
|
||||
# sessions only.
|
||||
def local_stats(home):
|
||||
today = datetime.now().astimezone().date().isoformat()
|
||||
prompts = sessions = today_sessions = 0
|
||||
days = set()
|
||||
for summary_path in (Path(home) / "sessions").glob("*/*/summary.json"):
|
||||
summary = read_json(summary_path)
|
||||
if not isinstance(summary, dict):
|
||||
continue
|
||||
active = parse_time(summary.get("last_active_at"))
|
||||
if not active:
|
||||
continue
|
||||
day = active.astimezone().date().isoformat()
|
||||
sessions += 1
|
||||
prompts += int(summary.get("num_messages") or 0)
|
||||
days.add(day)
|
||||
if day == today:
|
||||
today_sessions += 1
|
||||
return {
|
||||
"hasLocalStats": sessions > 0,
|
||||
"todaySessions": today_sessions,
|
||||
"totalPrompts": prompts,
|
||||
"totalSessions": sessions,
|
||||
"activeDays": len(days),
|
||||
"activeDates": sorted(days),
|
||||
}
|
||||
|
||||
|
||||
def cached_local_stats(home, max_age):
|
||||
cache = cache_root() / "grok-stats.json"
|
||||
cached = read_json(cache) or {}
|
||||
if cached.get("home") == str(home) and time.time() - float(cached.get("at") or 0) < max_age:
|
||||
return cached["stats"]
|
||||
stats = local_stats(home)
|
||||
write_json(cache, {"home": str(home), "at": time.time(), "stats": stats})
|
||||
return stats
|
||||
|
||||
|
||||
# One login per issuer in auth.json; the first is the CLI's own.
|
||||
def login(home):
|
||||
auth = read_json(Path(home) / "auth.json")
|
||||
if not isinstance(auth, dict):
|
||||
return {}
|
||||
return next((v for v in auth.values() if isinstance(v, dict)), {})
|
||||
|
||||
|
||||
def plan(home):
|
||||
cached = read_json(Path(home) / "settings_cache.json") or {}
|
||||
try:
|
||||
settings = json.loads(cached.get("payload") or "{}").get("settings") or {}
|
||||
except Exception:
|
||||
return ""
|
||||
return str(settings.get("subscription_tier_display") or "")
|
||||
|
||||
|
||||
# The credits answer as one limit window: how much of this period's included
|
||||
# usage is gone, and when the period ends. The answer is protobuf JSON, which
|
||||
# leaves out a field holding zero, so a period with nothing used yet has no
|
||||
# percentage at all.
|
||||
def credits_limit(data):
|
||||
config = data["config"]
|
||||
period = config["currentPeriod"]
|
||||
return {
|
||||
"label": "Weekly" if "WEEK" in str(period.get("type")) else "Monthly",
|
||||
"percent": max(0.0, min(1.0, float(config.get("creditUsagePercent") or 0) / 100)),
|
||||
"resetsAt": parse_time(period["end"]).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def collect_limits(home, key, force):
|
||||
result = {"limits": [], "usageStatusText": "", "authHelpText": AUTH_HELP, "live": False, "fetchedAtMs": 0}
|
||||
entry = login(home)
|
||||
token = str(entry.get("key") or "")
|
||||
if not token:
|
||||
result["usageStatusText"] = "Waiting for auth"
|
||||
return result
|
||||
|
||||
cache = cache_root() / f"grok-limits-{key}.json"
|
||||
cached = read_json(cache) or {}
|
||||
fetched_at = float(cached.get("fetchedAtMs") or 0)
|
||||
result["fetchedAtMs"] = fetched_at
|
||||
|
||||
# The CLI refreshes its token while it runs; one left to lapse can't be
|
||||
# used until Grok starts again.
|
||||
expires = parse_time(entry.get("expires_at"))
|
||||
if expires and expires.timestamp() <= time.time():
|
||||
result["limits"] = cached.get("limits") or []
|
||||
result["usageStatusText"] = "Sign-in expired"
|
||||
result["authHelpText"] = "Grok's saved sign-in expired. Start Grok to refresh it."
|
||||
return result
|
||||
|
||||
if cached.get("limits") and not force and time.time() - fetched_at / 1000 < PROBE_REUSE_SECONDS:
|
||||
result.update(limits=cached["limits"], live=True)
|
||||
return result
|
||||
|
||||
request = urllib.request.Request(CREDITS_URL, headers={"Authorization": f"Bearer {token}", "Accept": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=8) as response:
|
||||
limit = credits_limit(json.loads(response.read()))
|
||||
except urllib.error.HTTPError as error:
|
||||
limit = None
|
||||
if error.code in (401, 403):
|
||||
result["usageStatusText"] = "Waiting for auth"
|
||||
except Exception:
|
||||
limit = None
|
||||
|
||||
if limit:
|
||||
result.update(limits=[limit], live=True, fetchedAtMs=round(time.time() * 1000))
|
||||
write_json(cache, {"fetchedAtMs": result["fetchedAtMs"], "limits": result["limits"]})
|
||||
else:
|
||||
result["limits"] = cached.get("limits") or []
|
||||
if not result["usageStatusText"] and not result["limits"]:
|
||||
result["usageStatusText"] = "Grok limits unavailable"
|
||||
return result
|
||||
|
||||
|
||||
# The accounts `omarchy agent account` registered, read straight from its
|
||||
# registry. Only a registry holding a second account matters: with one, the
|
||||
# record describes ~/.grok alone.
|
||||
def registered_accounts():
|
||||
state = Path(os.environ.get("XDG_STATE_HOME") or (Path.home() / ".local" / "state"))
|
||||
registry = read_json(state / "omarchy" / "agents" / "accounts" / "grok.json") or {}
|
||||
accounts = [a for a in registry.get("accounts") or [] if isinstance(a, dict) and a.get("id")]
|
||||
if len(accounts) < 2:
|
||||
return []
|
||||
active_id = registry.get("active") or accounts[0]["id"]
|
||||
if not any(a["id"] == active_id for a in accounts):
|
||||
active_id = accounts[0]["id"]
|
||||
for account in accounts:
|
||||
account["active"] = account["id"] == active_id
|
||||
account["switch"] = {
|
||||
"mode": "auto" if registry.get("switch") == "auto" else "manual",
|
||||
"threshold": registry.get("threshold") or 95,
|
||||
}
|
||||
return accounts
|
||||
|
||||
|
||||
def home_of(account):
|
||||
return Path(account.get("home") or (Path.home() / ".grok"))
|
||||
|
||||
|
||||
# Keyed by who the home is signed in as, so a home signed in to someone else
|
||||
# never shows the last one's numbers.
|
||||
def cache_key(home, fallback):
|
||||
user = str(login(home).get("user_id") or "")
|
||||
return re.sub(r"[^A-Za-z0-9_-]", "", user) or fallback
|
||||
|
||||
|
||||
def account_limits(account, force):
|
||||
home = home_of(account)
|
||||
limits = collect_limits(home, cache_key(home, account["id"]), force)
|
||||
return {
|
||||
"id": account["id"],
|
||||
"label": str(account.get("label") or account["id"]),
|
||||
"email": str(account.get("email") or ""),
|
||||
"plan": plan(home) or str(account.get("plan") or ""),
|
||||
"active": account["active"],
|
||||
"primary": bool(account.get("primary")),
|
||||
"limits": limits["limits"],
|
||||
"stale": not limits["live"],
|
||||
"fetchedAt": limits["fetchedAtMs"],
|
||||
"usageStatusText": limits["usageStatusText"],
|
||||
"authHelpText": limits["authHelpText"],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--force", action="store_true")
|
||||
parser.add_argument("--limits-only", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
registered = registered_accounts()
|
||||
accounts = [account_limits(account, args.force) for account in registered]
|
||||
current = next((a for a in accounts if a["active"]), None)
|
||||
if current:
|
||||
tier = current["plan"]
|
||||
limits = current
|
||||
stale, fetched_at = current["stale"], current["fetchedAt"]
|
||||
else:
|
||||
home = default_home()
|
||||
tier = plan(home)
|
||||
limits = collect_limits(home, cache_key(home, "main"), args.force)
|
||||
stale, fetched_at = not limits["live"], limits["fetchedAtMs"]
|
||||
|
||||
# Nobody signed in to Grok here: an empty record, which the panel skips.
|
||||
# With accounts registered, the primary is ~/.grok by definition.
|
||||
stats_home = Path.home() / ".grok" if accounts else default_home()
|
||||
signed_in = bool(accounts) or bool(login(stats_home))
|
||||
record = {
|
||||
"schemaVersion": 1,
|
||||
"id": AGENT_ID,
|
||||
"name": AGENT_NAME,
|
||||
"updatedAt": datetime.now(timezone.utc).isoformat(),
|
||||
"ready": signed_in,
|
||||
"tierLabel": tier,
|
||||
"usageStatusText": limits["usageStatusText"],
|
||||
"authHelpText": limits["authHelpText"],
|
||||
"limits": limits["limits"],
|
||||
"limitsStale": stale,
|
||||
"limitsFetchedAt": fetched_at,
|
||||
}
|
||||
record.update(cached_local_stats(stats_home, LIMITS_ONLY_SCAN_REUSE_SECONDS if args.limits_only and not args.force else 0))
|
||||
if accounts:
|
||||
record["accountSwitch"] = registered[0]["switch"]
|
||||
record["accounts"] = accounts
|
||||
print(json.dumps(record, separators=(",", ":")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -54,7 +54,7 @@ collect() {
|
||||
# Accounts can be signed in to someone else since they were added; the
|
||||
# records name them from the registry, so bring it up to date first.
|
||||
accounts_dir="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/agents/accounts"
|
||||
if [[ -f $accounts_dir/claude.json || -f $accounts_dir/codex.json ]]; then
|
||||
if [[ -f $accounts_dir/claude.json || -f $accounts_dir/codex.json || -f $accounts_dir/grok.json ]]; then
|
||||
"$OMARCHY_PATH/bin/omarchy-agent-account-state" refresh
|
||||
fi
|
||||
|
||||
@@ -76,7 +76,7 @@ done
|
||||
# Fresh limits are when switching accounts near a limit gets decided. A switch
|
||||
# changes which account the record's own limits describe, so that record is
|
||||
# collected once more; the limits probes it repeats are still cached.
|
||||
for agent in claude codex; do
|
||||
for agent in claude codex grok; do
|
||||
if wanted "$agent" && [[ -f $accounts_dir/$agent.json ]]; then
|
||||
switched=$("$OMARCHY_PATH/bin/omarchy-agent-account-state" autoswitch "$agent")
|
||||
if [[ -n $switched ]]; then
|
||||
|
||||
@@ -9,4 +9,25 @@ if omarchy-cmd-present mise; then
|
||||
# days after they ship. Running omarchy update is the user asking for current
|
||||
# versions now, which is the same call the mup alias makes by hand.
|
||||
MISE_MINIMUM_RELEASE_AGE=0 mise up
|
||||
|
||||
# Grok's npm launcher runs ~/.grok/bin/grok whenever it exists, and the
|
||||
# package's install script that points it at a new release doesn't run
|
||||
# under mise, so updates never took effect. Dropping a link to an older
|
||||
# release lets the launcher unpack the installed one, which --version does
|
||||
# now rather than on the next session. The old release stays until the new
|
||||
# one is in place, and comes back if it never arrives.
|
||||
if grok_dir=$(mise where npm:@xai-official/grok 2>/dev/null) && [[ -n $grok_dir ]]; then
|
||||
grok_link="$HOME/.grok/bin/grok"
|
||||
grok_current="grok-${grok_dir##*/}"
|
||||
grok_previous=$(readlink "$grok_link" 2>/dev/null)
|
||||
if [[ -L $grok_link && $grok_previous != "$grok_current" ]]; then
|
||||
rm -f "$grok_link"
|
||||
mise x npm:@xai-official/grok -- grok --version >/dev/null 2>&1
|
||||
if [[ $(readlink "$grok_link" 2>/dev/null) == "$grok_current" ]]; then
|
||||
find "$HOME/.grok/bin" -maxdepth 1 -name 'grok-*' ! -name "$grok_current" -delete
|
||||
else
|
||||
ln -sfn "$grok_previous" "$grok_link"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -1,6 +1,6 @@
|
||||
# Start Claude Code and Codex as the active subscription account, so `cx`,
|
||||
# `cy`, `tdl` and a plain `claude` all follow `omarchy agent account use`.
|
||||
# An explicitly set CLAUDE_CONFIG_DIR / CODEX_HOME still wins.
|
||||
# Start Claude Code, Codex, and Grok as the active subscription account, so
|
||||
# `cx`, `cy`, `tdl` and a plain `claude` all follow `omarchy agent account use`.
|
||||
# An explicitly set CLAUDE_CONFIG_DIR / CODEX_HOME / GROK_HOME still wins.
|
||||
claude() {
|
||||
local home=""
|
||||
[[ -z ${CLAUDE_CONFIG_DIR:-} ]] && home=$(omarchy-agent-account-home claude)
|
||||
@@ -22,3 +22,14 @@ codex() {
|
||||
command codex "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
grok() {
|
||||
local home=""
|
||||
[[ -z ${GROK_HOME:-} ]] && home=$(omarchy-agent-account-home grok)
|
||||
|
||||
if [[ -n $home ]]; then
|
||||
GROK_HOME="$home" command grok "$@"
|
||||
else
|
||||
command grok "$@"
|
||||
fi
|
||||
}
|
||||
+3
-3
@@ -36,15 +36,15 @@ There are terminal shortcuts too: `a` runs the default agent inline in the curre
|
||||
|
||||
### The agents panel
|
||||
|
||||
The top bar has an agents icon. Before you've set up an agent, its panel walks you through signing in to Claude, Codex, or Grok, and the first one becomes your default agent. After that, the panel puts every subscription on one page: each agent and account with its plan and the percentage used of its 5-hour session and weekly limits (or the remaining prepaid balance), under a line that rotates through what you've used across all of them, like tokens this week and your most used model. Under _Make something_ it offers starter prompts for a new theme, plugin, or app that hand the task to your default agent. Apps are built the way Omarchy's own Hype, Monologue, and Omacut are, in C++ and Qt Quick, following the omarchy-app agent skill, and the compiler and Qt pieces they need come installed. Claude Code, Codex, and Fireworks are covered out of the box.
|
||||
The top bar has an agents icon. Before you've set up an agent, its panel walks you through signing in to Claude, Codex, or Grok, and the first one becomes your default agent. After that, the panel puts every subscription on one page: each agent and account with its plan and the percentage used of its 5-hour session and weekly limits (or the remaining prepaid balance), under a line that rotates through what you've used across all of them, like tokens this week and your most used model. Under _Make something_ it offers starter prompts for a new theme, plugin, or app that hand the task to your default agent. Apps are built the way Omarchy's own Hype, Monologue, and Omacut are, in C++ and Qt Quick, following the omarchy-app agent skill, and the compiler and Qt pieces they need come installed. Claude Code, Codex, Grok, and Fireworks are covered out of the box.
|
||||
|
||||
Left-click the bar icon for the panel, right-click to launch your default agent. The usage records behind it are regenerated every 15 minutes by `omarchy agent usage-update`, and the panel can even merge usage from your other machines via a synced folder. See the README under `$OMARCHY_PATH/shell/plugins/agents/` for the full settings.
|
||||
|
||||
### Several subscriptions
|
||||
|
||||
If you pay for more than one Claude or Codex plan — a personal Max and a work one, say — Omarchy can keep them all signed in and move between them. Add a subscription with the + in the agents panel, which walks you through it right there, or with `omarchy agent account add claude Work` (or `codex`). It installs the CLI if it's missing. The first account for a provider signs in through your normal browser, right where the CLI always looks. Every one after that signs in through a private window, so the account your browser is already signed in to doesn't get picked up by mistake; sign in there as the account you're adding, and close any private window you already had open first. Grok can be signed in the same way, but only one Grok account for now. Never log out of the CLI to do this: logging out revokes the saved sign-in, and that account would have to be added again.
|
||||
If you pay for more than one Claude, Codex, or Grok plan — a personal Max and a work one, say — Omarchy can keep them all signed in and move between them. Add a subscription with the + in the agents panel, which walks you through it right there, or with `omarchy agent account add claude Work` (or `codex`, or `grok`). It installs the CLI if it's missing. The first account for a provider signs in through your normal browser, right where the CLI always looks. Every one after that signs in through a private window, so the account your browser is already signed in to doesn't get picked up by mistake; sign in there as the account you're adding, and close any private window you already had open first. Never log out of the CLI to do this: logging out revokes the saved sign-in, and that account would have to be added again.
|
||||
|
||||
One account per provider is active, and every new session starts as it: `cx`, `cy`, a plain `claude` or `codex`, and anything launched through `omarchy agent`. Switch with `omarchy agent account use work` (and back with `omarchy agent account use main`), or in the panel by pressing an account's number and then `Enter` (or clicking its Use button). Sessions that are already running stay on the account they started with. Conversation history, settings, skills, and plugins are shared across accounts, so `claude --continue` in the same folder picks up where you left off on the new account.
|
||||
One account per provider is active, and every new session starts as it: `cx`, `cy`, a plain `claude`, `codex`, or `grok`, and anything launched through `omarchy agent`. Switch with `omarchy agent account use work` (and back with `omarchy agent account use main`), or in the panel by pressing an account's number and then `Enter` (or clicking its Use button). Sessions that are already running stay on the account they started with. Conversation history, settings, skills, and plugins are shared across accounts, so `claude --continue` in the same folder picks up where you left off on the new account.
|
||||
|
||||
With more than one account, the panel shows each account's limits side by side, with the active one marked. By default Omarchy notifies you when the active account passes 95% of a limit, and clicking the notification switches to the account with the most headroom. Hover an account's line in the panel and click the Autoswitch that appears beside Use (or press `m`), or run `omarchy agent account mode auto`, to have it switch on its own instead; while it's on, Autoswitch takes Use's place, and clicking it again goes back to notifying. Add a number to change the threshold, as in `omarchy agent account mode auto 90`. If every account is over the threshold, you'll hear about it once, along with which account frees up first. While an active account is within 15 points of its threshold (80% at the default), its limits are checked every three minutes rather than every 15.
|
||||
|
||||
|
||||
@@ -235,12 +235,15 @@ Item {
|
||||
}
|
||||
|
||||
// All-time keeps a quiet day from hiding an agent; today's counts admit a
|
||||
// machine whose only source is history.jsonl, which knows nothing older.
|
||||
// machine whose only source is history.jsonl, which knows nothing older. A
|
||||
// signed-in agent with a plan shows before its first numbers arrive, so a
|
||||
// lapsed sign-in still has somewhere to say so.
|
||||
function providerHasData(p) {
|
||||
return numberValue(p.totalPrompts) > 0 || numberValue(p.totalSessions) > 0
|
||||
|| numberValue(p.activeDays) > 0 || numberValue(p.todayPrompts) > 0
|
||||
|| numberValue(p.todaySessions) > 0 || (p.limits && p.limits.length > 0)
|
||||
|| (p.accounts && p.accounts.length > 0) || !!p.balance
|
||||
|| (p.ready === true && p.tierLabel !== "")
|
||||
}
|
||||
|
||||
// A prepaid agent's credit ledger. Like rate limits, the balance is
|
||||
|
||||
@@ -211,7 +211,7 @@ Panel {
|
||||
|
||||
function chooseAddProvider(id) {
|
||||
var state = addChecks[id] || ""
|
||||
if (state === "unsupported" || state === "") return
|
||||
if (state === "") return
|
||||
addProvider = id
|
||||
if (state === "additional") addStage = "name"
|
||||
else startAdd("")
|
||||
@@ -1004,8 +1004,7 @@ Panel {
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
// Pick: each agent by a large mark over its name, three across. One that
|
||||
// can't be added right now is dimmed, and says why on hover.
|
||||
// Pick: each agent by a large mark over its name, three across.
|
||||
Row {
|
||||
id: choiceRow
|
||||
visible: root.picking
|
||||
@@ -1024,7 +1023,6 @@ Panel {
|
||||
readonly property bool hasCursor: root.hasKey("choice", index)
|
||||
width: (choiceRow.width - choiceRow.spacing * (root.addProviders.length - 1)) / root.addProviders.length
|
||||
implicitHeight: choiceBody.implicitHeight + Style.space(16)
|
||||
opacity: state === "unsupported" ? 0.4 : 1.0
|
||||
onHasCursorChanged: if (hasCursor) root.revealItem(choice)
|
||||
|
||||
Column {
|
||||
@@ -1059,11 +1057,6 @@ Panel {
|
||||
onClicked: root.chooseAddProvider(choice.modelData.providerId)
|
||||
}
|
||||
|
||||
PanelToolTip {
|
||||
visible: choice.state === "unsupported" && choice.hasCursor
|
||||
// Kept short: the tooltip can't grow past the panel's edges.
|
||||
text: "Already signed in. " + choice.modelData.providerName + " takes one account."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +72,12 @@ light surfaces — and the bar glyph stands in when there is none.
|
||||
|---|---|---|
|
||||
| `claude` | Anthropic's OAuth usage endpoint (5-hour session + 7-day weekly) | `~/.claude/projects` transcripts, opencode sessions on an Anthropic provider, plus `stats-cache.json` and `history.jsonl` as fallback |
|
||||
| `codex` | The Codex app-server RPC | native Codex CLI session files (plus pi and opencode sessions) |
|
||||
| `grok` | The credits endpoint behind Grok's `/usage` view (the billing period's included usage) | Prompts and sessions from each session's `summary.json` (Grok records no tokens there) |
|
||||
| `fireworks` | Estimated prepaid balance: configured funding minus rated account costs | Fireworks billing API, grouped by day and model for the last 30 days |
|
||||
|
||||
When `~/.local/state/omarchy/agents/accounts/<claude|codex>.json` registers
|
||||
more than one account, the `claude` and `codex` records also carry
|
||||
When `~/.local/state/omarchy/agents/accounts/<claude|codex|grok>.json`
|
||||
registers more than one account, the `claude`, `codex`, and `grok` records
|
||||
also carry
|
||||
`accounts: [{ id, label, email, plan, active, limits, stale, usageStatusText,
|
||||
authHelpText }]`, each account probed with its own sign-in (Claude caches each
|
||||
account's limits separately; Codex runs one app-server per account home), and
|
||||
@@ -89,7 +91,11 @@ account changed.
|
||||
|
||||
Claude limits need a signed-in CLI; without credentials the panel says so and
|
||||
falls back to local stats only. A non-default Claude directory is honored via
|
||||
`CLAUDE_CONFIG_DIR`, Codex via `CODEX_HOME`. Fireworks reads
|
||||
`CLAUDE_CONFIG_DIR`, Codex via `CODEX_HOME`, Grok via `GROK_HOME`. Grok's
|
||||
plan comes from the settings it caches in its home, and its limit from the
|
||||
credits endpoint its own `/usage` view reads, asked with each account's
|
||||
sign-in; a sign-in left to lapse shows the last credits until Grok runs
|
||||
again. Fireworks reads
|
||||
`FIREWORKS_API_KEY` and `FIREWORKS_ACCOUNT_ID` first, then
|
||||
`~/.fireworks/auth.ini` (which `firectl set-api-key` creates), then the key
|
||||
opencode stores in `~/.local/share/opencode/auth.json` when Fireworks is
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="#111" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"></path></svg>
|
||||
|
After Width: | Height: | Size: 748 B |
@@ -0,0 +1 @@
|
||||
<svg fill="#fff" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"></path></svg>
|
||||
|
After Width: | Height: | Size: 748 B |
@@ -5,7 +5,7 @@
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"license": "MIT",
|
||||
"description": "Claude Code, Codex, and Fireworks usage, limits, and pace in a native Omarchy bar panel.",
|
||||
"description": "Claude Code, Codex, Grok, and Fireworks usage, limits, and pace in a native Omarchy bar panel.",
|
||||
"kinds": ["bar-widget"],
|
||||
"activation": "on-demand",
|
||||
"entryPoints": {
|
||||
@@ -21,6 +21,7 @@
|
||||
"providers": {
|
||||
"claude": { "enabled": true },
|
||||
"codex": { "enabled": true },
|
||||
"grok": { "enabled": true },
|
||||
"fireworks": { "enabled": true }
|
||||
},
|
||||
"refreshIntervalSec": 900,
|
||||
|
||||
@@ -52,13 +52,20 @@ fi
|
||||
echo "codex home=${CODEX_HOME:-default} args=$*"
|
||||
SH
|
||||
|
||||
# Grok keys its login by issuer and keeps the plan in the settings it caches,
|
||||
# as a JSON string inside that file.
|
||||
cat >"$mock_bin/grok" <<'SH'
|
||||
#!/bin/bash
|
||||
if [[ ${1:-} == "login" ]]; then
|
||||
"${BROWSER:-omarchy-test-default-browser}" "https://auth.x.ai/oauth/authorize"
|
||||
mkdir -p "${GROK_HOME:-$HOME/.grok}"
|
||||
echo '{"token":"t"}' >"${GROK_HOME:-$HOME/.grok}/auth.json"
|
||||
home=${GROK_HOME:-$HOME/.grok}
|
||||
mkdir -p "$home"
|
||||
printf '{"https://auth.x.ai::client":{"key":"t","user_id":"%s","email":"%s"}}\n' \
|
||||
"${OMARCHY_TEST_LOGIN_UUID:-u-grok}" "${OMARCHY_TEST_LOGIN_EMAIL:-me@example.com}" >"$home/auth.json"
|
||||
printf '{"payload":"{\\"settings\\":{\\"subscription_tier_display\\":\\"SuperGrok\\"}}"}\n' >"$home/settings_cache.json"
|
||||
exit 0
|
||||
fi
|
||||
echo "grok home=${GROK_HOME:-default} args=$*"
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/omarchy-test-default-browser" <<'SH'
|
||||
@@ -183,12 +190,23 @@ OMARCHY_TEST_DEFAULT_AGENT="" omarchy-agent-account-add grok </dev/null >/dev/nu
|
||||
fail "the first agent signed in on a machine with no default becomes the default"
|
||||
[[ -s $HOME/.grok/auth.json ]] && grep -qx "default https://auth.x.ai/oauth/authorize" "$OMARCHY_TEST_BROWSER_LOG" ||
|
||||
fail "a first Grok sign-in lands in ~/.grok through the normal browser"
|
||||
if omarchy-agent-account-add grok Second </dev/null >"$test_tmp/grok-second" 2>&1; then
|
||||
fail "a second Grok account says it isn't supported yet"
|
||||
fi
|
||||
grep -q "isn't supported yet" "$test_tmp/grok-second" || fail "a second Grok account says why it stops" "$(cat "$test_tmp/grok-second")"
|
||||
pass "Grok signs in its first account"
|
||||
|
||||
mkdir -p "$HOME/.grok/bin" && touch "$HOME/.grok/bin/grok"
|
||||
OMARCHY_TEST_LOGIN_UUID=u-grok-2 OMARCHY_TEST_LOGIN_EMAIL=side@example.com \
|
||||
omarchy-agent-account-add grok Side </dev/null >/dev/null
|
||||
[[ $(omarchy-agent-account-list grok --json | jq -c '.[0].accounts[1] | {id, email, plan}') == '{"id":"side","email":"side@example.com","plan":"SuperGrok"}' ]] ||
|
||||
fail "a Grok account reads its identity and plan from its own home" "$(omarchy-agent-account-list grok --json)"
|
||||
[[ $(readlink "$accounts/grok/side/bin") == "$HOME/.grok/bin" && $(readlink "$accounts/grok/side/sessions") == "$HOME/.grok/sessions" ]] ||
|
||||
fail "a Grok account shares the CLI and sessions with the primary"
|
||||
grep -qx -- "--private https://auth.x.ai/oauth/authorize" "$OMARCHY_TEST_BROWSER_LOG" ||
|
||||
fail "a second Grok login opens in a private window" "$(cat "$OMARCHY_TEST_BROWSER_LOG")"
|
||||
omarchy-agent-account-use grok side >/dev/null
|
||||
source "$ROOT/default/bash/fns/agent-accounts"
|
||||
[[ $(grok --version) == "grok home=$accounts/grok/side args=--version" ]] || fail "grok starts as the active Grok account" "$(grok --version)"
|
||||
omarchy-agent-account-use grok main >/dev/null
|
||||
pass "Grok accounts are added and used like the others"
|
||||
|
||||
# ---------------------------------------------------------------------- routing
|
||||
|
||||
omarchy-agent-account-use claude work >/dev/null
|
||||
@@ -307,7 +325,7 @@ pass "a failed registration rolls the new home back to pending"
|
||||
|
||||
# ------------------------------------------------------------ panel add flow
|
||||
|
||||
[[ $(omarchy-agent-account-add --check) == $'claude additional\ncodex additional\ngrok unsupported' ]] ||
|
||||
[[ $(omarchy-agent-account-add --check) == $'claude additional\ncodex additional\ngrok additional' ]] ||
|
||||
fail "--check says what adding would mean for each provider" "$(omarchy-agent-account-add --check)"
|
||||
pass "--check says what adding would mean for each provider"
|
||||
|
||||
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/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"
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
export HOME="$test_tmp/home"
|
||||
mkdir -p "$HOME/.grok/bin" "$test_tmp/bin" "$test_tmp/installs/npm-xai-official-grok/1.0.44"
|
||||
|
||||
# mise reports the installed release by its directory; running grok under it
|
||||
# stands in for the npm launcher unpacking that release.
|
||||
cat >"$test_tmp/bin/mise" <<SH
|
||||
#!/bin/bash
|
||||
case "\$1" in
|
||||
up) ;;
|
||||
where) echo "$test_tmp/installs/npm-xai-official-grok/1.0.44" ;;
|
||||
x) echo ran >"$test_tmp/grok-ran"; touch "\$HOME/.grok/bin/grok-1.0.44"; ln -sfn grok-1.0.44 "\$HOME/.grok/bin/grok" ;;
|
||||
esac
|
||||
SH
|
||||
chmod +x "$test_tmp/bin/mise"
|
||||
|
||||
echo old >"$HOME/.grok/bin/grok-1.0.30"
|
||||
ln -s grok-1.0.30 "$HOME/.grok/bin/grok"
|
||||
|
||||
PATH="$test_tmp/bin:$ROOT/bin:$PATH" "$ROOT/bin/omarchy-update-mise" >/dev/null
|
||||
[[ $(readlink "$HOME/.grok/bin/grok") == "grok-1.0.44" && ! -e $HOME/.grok/bin/grok-1.0.30 ]] ||
|
||||
fail "an update points Grok at the installed release and drops the old one" "$(ls -la "$HOME/.grok/bin")"
|
||||
pass "an update points Grok at the installed release and drops the old one"
|
||||
|
||||
rm -f "$test_tmp/grok-ran"
|
||||
PATH="$test_tmp/bin:$ROOT/bin:$PATH" "$ROOT/bin/omarchy-update-mise" >/dev/null
|
||||
[[ ! -e $test_tmp/grok-ran ]] || fail "a current Grok is left alone"
|
||||
pass "a current Grok is left alone"
|
||||
|
||||
# An unpack that never arrives leaves the old release running.
|
||||
rm -f "$HOME/.grok/bin/grok" "$HOME/.grok/bin/grok-1.0.44"
|
||||
echo old >"$HOME/.grok/bin/grok-1.0.30"
|
||||
ln -s grok-1.0.30 "$HOME/.grok/bin/grok"
|
||||
sed -i 's/^ x) .*/ x) exit 1 ;;/' "$test_tmp/bin/mise"
|
||||
PATH="$test_tmp/bin:$ROOT/bin:$PATH" "$ROOT/bin/omarchy-update-mise" >/dev/null
|
||||
[[ $(readlink "$HOME/.grok/bin/grok") == "grok-1.0.30" && -e $HOME/.grok/bin/grok-1.0.30 ]] ||
|
||||
fail "a failed unpack keeps the old Grok release" "$(ls -la "$HOME/.grok/bin")"
|
||||
pass "a failed unpack keeps the old Grok release"
|
||||
Reference in new issue
Block a user