first commit
This commit is contained in:
commit
9ad073d4fc
3 files changed
+1051
No files matched your search
@@ -0,0 +1,99 @@
|
|||||||
|
# mcode-status
|
||||||
|
|
||||||
|
Aygea-branded **z.ai GLM usage** segment for the [MiniMax Code](https://codex.minimaxi.com) statusline — shows your current 5-hour token-window burn right in the TUI:
|
||||||
|
|
||||||
|
```
|
||||||
|
🔥 23% · 4:12p(2h14m)
|
||||||
|
```
|
||||||
|
|
||||||
|
Styled with the Aygea palette (blue → lavender → pink as the window fills), rendered via mcode's `colorMode: ansi` statusline option, with plain-text fallback.
|
||||||
|
|
||||||
|
## Install (one line)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.itsaygea.com/admin/mcode-sl/raw/branch/main/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer is self-contained (the statusline script is embedded in it) and:
|
||||||
|
|
||||||
|
1. installs `~/.local/bin/mcode-zai-usage`
|
||||||
|
2. checks SecretVault for your `zai_api_key` secret (never prints it)
|
||||||
|
3. merges the `tui:` section into `~/.minimax/config.yaml` — atomic write, timestamped backup, refuses ambiguous structures, everything outside the `tui:` block stays byte-identical
|
||||||
|
4. primes the quota cache so the segment appears immediately
|
||||||
|
|
||||||
|
Then **restart the MiniMax Code TUI**.
|
||||||
|
|
||||||
|
## Secret setup (once per machine)
|
||||||
|
|
||||||
|
This looks like an example entry, but it's the actual setup: create a SecretVault secret named `zai_api_key` holding your z.ai API key (for `https://api.z.ai`):
|
||||||
|
|
||||||
|
- via the SecretVault MCP `create_secret` tool in any agent session, or your usual SecretVault flow
|
||||||
|
|
||||||
|
No key in the repo, no key in argv, no key in the config file — the statusline reads it at runtime with `secretvault run`. A legacy `~/.secretvault/credential.json` (`url` + `clientKey`, the Claude-statusline layout) also works as a fallback.
|
||||||
|
|
||||||
|
## No cron, no systemd
|
||||||
|
|
||||||
|
MiniMax Code re-runs the command every `intervalSeconds` — mcode *is* the scheduler. The script never blocks the TUI on the network: display comes from a local cache, and a detached, flock-guarded background job refreshes it when the cache is >150s old (sharing `~/.cache/aygea-sl/quota.json` with the Aygea Claude statusline if both are installed).
|
||||||
|
|
||||||
|
## Config (managed in `~/.minimax/config.yaml`)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tui:
|
||||||
|
statusLine:
|
||||||
|
- current-dir
|
||||||
|
- git-branch
|
||||||
|
- approval-mode
|
||||||
|
- model-with-reasoning
|
||||||
|
- context-remaining
|
||||||
|
- custom-command
|
||||||
|
customStatusLine:
|
||||||
|
command: /home/you/.local/bin/mcode-zai-usage
|
||||||
|
intervalSeconds: 60
|
||||||
|
timeoutMs: 10000
|
||||||
|
colorMode: ansi
|
||||||
|
```
|
||||||
|
|
||||||
|
Installer env overrides: `MCODE_SL_ITEMS` (item list), `MCODE_SL_INTERVAL`, `MCODE_SL_TIMEOUT`, `MCODE_SL_SECRET`.
|
||||||
|
|
||||||
|
## Script env tunables
|
||||||
|
|
||||||
|
| Variable | Default | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `AYGEA_SL_SECRET_NAME` | `zai_api_key` | SecretVault secret name |
|
||||||
|
| `AYGEA_SL_URL` | `https://api.z.ai` | quota API base URL |
|
||||||
|
| `AYGEA_SL_NO_VAULT=1` | off | skip SecretVault, use credential.json |
|
||||||
|
| `AYGEA_SL_QUOTA_CACHE` | `~/.cache/aygea-sl/quota.json` | cache path |
|
||||||
|
| `AYGEA_SL_REFRESH_AFTER` | `150` | refresh when cache older (sec) |
|
||||||
|
| `AYGEA_SL_MAX_AGE` | `360` | hide when cache older (sec) |
|
||||||
|
| `AYGEA_SL_CURL_TIMEOUT` | `6` | per-fetch curl `--max-time` |
|
||||||
|
| `AYGEA_SL_ONLY_GLM=1` | off | show only for glm models |
|
||||||
|
| `AYGEA_SL_MEM=1` | off | prepend an 🧠 memory segment (Linux/macOS) |
|
||||||
|
| `NO_COLOR` | — | plain text output |
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.itsaygea.com/admin/mcode-sl/raw/branch/main/install.sh | bash -s -- --uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Requires `jq` + `curl`; `secretvault` optional (fallback: credential.json); `python3` for the config merge. Test locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf '{}' | ./zai-usage.sh # statusline preview
|
||||||
|
./zai-usage.sh --refresh # one foreground refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
`install.sh` embeds a copy of `zai-usage.sh` for `curl | bash` installs; checkout installs prefer the sibling file. After editing `zai-usage.sh`, regenerate the embedded copy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 - <<'EOF'
|
||||||
|
import re
|
||||||
|
src = open('zai-usage.sh').read(); assert src.endswith("\n")
|
||||||
|
ins = open('install.sh').read()
|
||||||
|
new = re.sub(r"(cat <<'ZAI_EMBED_EOF'\n).*?ZAI_EMBED_EOF\n",
|
||||||
|
lambda m: m.group(1) + src + "ZAI_EMBED_EOF\n", ins, flags=re.S)
|
||||||
|
open('install.sh', 'w').write(new)
|
||||||
|
EOF
|
||||||
|
```
|
||||||
Executable
+653
@@ -0,0 +1,653 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# mcode-status installer — Aygea z.ai GLM usage segment for MiniMax Code.
|
||||||
|
#
|
||||||
|
# bash install.sh install / update from a repo checkout
|
||||||
|
# curl -fsSL <raw-url>/install.sh | bash install from anywhere (piped)
|
||||||
|
# bash install.sh --uninstall remove script + config keys
|
||||||
|
#
|
||||||
|
# What it does:
|
||||||
|
# 1. installs zai-usage.sh -> ~/.local/bin/mcode-zai-usage
|
||||||
|
# (piped mode uses the embedded copy; checkout mode prefers the
|
||||||
|
# sibling zai-usage.sh so edits apply without re-embedding)
|
||||||
|
# 2. checks SecretVault for the $MCODE_SL_SECRET secret (default
|
||||||
|
# zai_api_key) — the key itself is never printed or stored here
|
||||||
|
# 3. surgically merges the tui section into ~/.minimax/config.yaml:
|
||||||
|
# atomic write (temp + fsync + rename), timestamped backup taken
|
||||||
|
# BEFORE any change, duplicate-structure refusal, everything outside
|
||||||
|
# the tui block preserved byte-for-byte
|
||||||
|
# 4. primes the quota cache with one bounded foreground fetch
|
||||||
|
#
|
||||||
|
# No cron/systemd needed: MiniMax Code re-runs the command every
|
||||||
|
# intervalSeconds itself, and the script refreshes its cache in a detached
|
||||||
|
# background job when it goes stale.
|
||||||
|
#
|
||||||
|
# Secret setup (once per machine) — looks like an example entry, isn't:
|
||||||
|
# secret name: zai_api_key (z.ai API key for https://api.z.ai)
|
||||||
|
# via the SecretVault MCP `create_secret` tool, or your usual flow.
|
||||||
|
#
|
||||||
|
# Env overrides:
|
||||||
|
# MCODE_SL_SECRET vault secret name (default zai_api_key)
|
||||||
|
# MCODE_SL_ITEMS comma list of statusline items (see DEFAULT_ITEMS)
|
||||||
|
# MCODE_SL_INTERVAL intervalSeconds (default 60; mcode clamps to >=10)
|
||||||
|
# MCODE_SL_TIMEOUT timeoutMs (default 10000; clamped 500..30000)
|
||||||
|
|
||||||
|
set -u
|
||||||
|
export LC_ALL=C
|
||||||
|
|
||||||
|
DEFAULT_ITEMS="current-dir,git-branch,approval-mode,model-with-reasoning,context-remaining,custom-command"
|
||||||
|
ITEMS="${MCODE_SL_ITEMS:-$DEFAULT_ITEMS}"
|
||||||
|
INTERVAL="${MCODE_SL_INTERVAL:-60}"
|
||||||
|
TIMEOUT="${MCODE_SL_TIMEOUT:-10000}"
|
||||||
|
SECRET_NAME="${MCODE_SL_SECRET:-zai_api_key}"
|
||||||
|
|
||||||
|
BIN_DIR="${HOME}/.local/bin"
|
||||||
|
DEST="${BIN_DIR}/mcode-zai-usage"
|
||||||
|
CONFIG="${HOME}/.minimax/config.yaml"
|
||||||
|
|
||||||
|
mode=install
|
||||||
|
[ "${1:-}" = "--uninstall" ] && mode=uninstall
|
||||||
|
|
||||||
|
info() { printf ' %s\n' "$1"; }
|
||||||
|
die() { printf 'install: %s\n' "$1" >&2; exit 1; }
|
||||||
|
have() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
# Prefer a sibling zai-usage.sh (checkout mode); otherwise extract embedded.
|
||||||
|
SCRIPT_SOURCE=""
|
||||||
|
_self="${BASH_SOURCE[0]:-}"
|
||||||
|
if [ -n "$_self" ] && [ -f "$_self" ]; then
|
||||||
|
_dir="$(cd "$(dirname "$_self")" && pwd)"
|
||||||
|
[ -f "$_dir/zai-usage.sh" ] && SCRIPT_SOURCE="$_dir/zai-usage.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
materialize_script() {
|
||||||
|
local target="$1"
|
||||||
|
if [ -n "$SCRIPT_SOURCE" ]; then
|
||||||
|
install -m 755 "$SCRIPT_SOURCE" "$target"
|
||||||
|
else
|
||||||
|
embed_zai_usage > "$target.tmp.$$"
|
||||||
|
chmod 755 "$target.tmp.$$"
|
||||||
|
mv -f "$target.tmp.$$" "$target"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Embedded copy of zai-usage.sh — fallback for `curl | bash` installs.
|
||||||
|
# Checkout installs prefer the sibling file. After editing zai-usage.sh,
|
||||||
|
# regenerate this block (command in README).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
embed_zai_usage() {
|
||||||
|
cat <<'ZAI_EMBED_EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# mcode-status — z.ai GLM 5-hour usage segment for the MiniMax Code statusline.
|
||||||
|
#
|
||||||
|
# Output (one line, Aygea palette):
|
||||||
|
# 🔥 23% · 4:12p(2h14m) (+ optional 🧠 mem segment, see AYGEA_SL_MEM)
|
||||||
|
#
|
||||||
|
# Registered in ~/.minimax/config.yaml as tui.customStatusLine.command with
|
||||||
|
# colorMode: ansi. MiniMax Code re-runs it every intervalSeconds and renders
|
||||||
|
# stdout as the "custom-command" item — mcode is the scheduler, so there is
|
||||||
|
# NO cron/systemd dependency. Network refreshes happen in a detached,
|
||||||
|
# flock-guarded background job; the statusline call itself never blocks.
|
||||||
|
#
|
||||||
|
# Credentials (checked in order):
|
||||||
|
# 1. SecretVault secret $AYGEA_SL_SECRET_NAME (default zai_api_key) via
|
||||||
|
# `secretvault run` — the public-repo-safe path. Endpoint defaults to
|
||||||
|
# https://api.z.ai (override with AYGEA_SL_URL).
|
||||||
|
# 2. Legacy ~/.secretvault/credential.json (url + clientKey, the Aygea
|
||||||
|
# Claude-statusline layout) → endpoint ${url}/proxy/zai/api/...
|
||||||
|
#
|
||||||
|
# Cache (shared with the Aygea Claude statusline):
|
||||||
|
# ~/.cache/aygea-sl/quota.json { "pct": 23, "reset_iso": "...", "ts": ... }
|
||||||
|
#
|
||||||
|
# Env tunables:
|
||||||
|
# AYGEA_SL_SECRET_NAME vault secret name (default zai_api_key)
|
||||||
|
# AYGEA_SL_URL quota base URL (default https://api.z.ai)
|
||||||
|
# AYGEA_SL_NO_VAULT=1 skip SecretVault, use credential.json only
|
||||||
|
# AYGEA_SL_CRED_FILE credential.json path (default ~/.secretvault/credential.json)
|
||||||
|
# AYGEA_SL_QUOTA_CACHE cache path (default ~/.cache/aygea-sl/quota.json)
|
||||||
|
# AYGEA_SL_REFRESH_AFTER refresh when cache older (default 150 sec)
|
||||||
|
# AYGEA_SL_MAX_AGE hide when cache older (default 360 sec)
|
||||||
|
# AYGEA_SL_CURL_TIMEOUT curl --max-time (default 6)
|
||||||
|
# AYGEA_SL_ONLY_GLM=1 show only when session model name contains "glm"
|
||||||
|
# AYGEA_SL_MEM=1 prepend an 🧠 system-memory segment (Linux/macOS)
|
||||||
|
# NO_COLOR plain text output
|
||||||
|
#
|
||||||
|
# Modes: (none) statusline mode — always exit 0, print segment or nothing
|
||||||
|
# --refresh one foreground refresh, exit non-zero on failure
|
||||||
|
#
|
||||||
|
# Requires: jq, curl. Optional: secretvault, flock, setsid.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
export LC_ALL=C
|
||||||
|
|
||||||
|
SECRET_NAME="${AYGEA_SL_SECRET_NAME:-zai_api_key}"
|
||||||
|
NO_VAULT="${AYGEA_SL_NO_VAULT:-0}"
|
||||||
|
CRED_FILE="${AYGEA_SL_CRED_FILE:-$HOME/.secretvault/credential.json}"
|
||||||
|
CACHE_FILE="${AYGEA_SL_QUOTA_CACHE:-$HOME/.cache/aygea-sl/quota.json}"
|
||||||
|
CACHE_DIR="$(dirname "$CACHE_FILE")"
|
||||||
|
REFRESH_AFTER="${AYGEA_SL_REFRESH_AFTER:-150}"
|
||||||
|
MAX_AGE="${AYGEA_SL_MAX_AGE:-360}"
|
||||||
|
CURL_TIMEOUT="${AYGEA_SL_CURL_TIMEOUT:-6}"
|
||||||
|
ONLY_GLM="${AYGEA_SL_ONLY_GLM:-0}"
|
||||||
|
MEM="${AYGEA_SL_MEM:-0}"
|
||||||
|
URL_BASE="${AYGEA_SL_URL:-https://api.z.ai}"
|
||||||
|
|
||||||
|
is_int() { case "$1" in ''|*[!0-9]*) return 1;; *) return 0;; esac; }
|
||||||
|
|
||||||
|
# Scratch files used by do_refresh; cleaned by the EXIT trap. Global (not
|
||||||
|
# function-local) so the trap can never hit unbound vars under set -u.
|
||||||
|
HDR_TMP=""; RESP_TMP=""
|
||||||
|
trap 'rm -f "${HDR_TMP:-}" "${RESP_TMP:-}" 2>/dev/null' EXIT
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Aygea dark palette (truecolor -> 256 -> plain)
|
||||||
|
############################################
|
||||||
|
if [ -n "${NO_COLOR:-}" ]; then MODE=none
|
||||||
|
elif [ "${COLORTERM:-}" = "truecolor" ] || [ "${COLORTERM:-}" = "24bit" ]; then MODE=truecolor
|
||||||
|
elif [ -z "${TERM##*256*}" ]; then MODE=256
|
||||||
|
else MODE=none; fi
|
||||||
|
|
||||||
|
if [ "$MODE" = truecolor ]; then
|
||||||
|
C_BLUE=$'\033[38;2;106;143;211m'; C_LAV=$'\033[38;2;201;184;232m'
|
||||||
|
C_PINK=$'\033[38;2;216;140;168m'; C_PCT=$'\033[38;2;192;209;230m'
|
||||||
|
C_MEM=$'\033[38;2;184;232;216m'; C_FRAME=$'\033[38;2;42;58;94m'
|
||||||
|
R=$'\033[0m'
|
||||||
|
elif [ "$MODE" = 256 ]; then
|
||||||
|
C_BLUE=$'\033[38;5;67m'; C_LAV=$'\033[38;5;182m'; C_PINK=$'\033[38;5;175m'
|
||||||
|
C_PCT=$'\033[38;5;152m'; C_MEM=$'\033[38;5;158m'; C_FRAME=$'\033[38;5;24m'
|
||||||
|
R=$'\033[0m'
|
||||||
|
else
|
||||||
|
C_BLUE=""; C_LAV=""; C_PINK=""; C_PCT=""; C_MEM=""; C_FRAME=""; R=""
|
||||||
|
fi
|
||||||
|
SEP="${C_FRAME} · ${R}"
|
||||||
|
|
||||||
|
pct_color() {
|
||||||
|
if [ "$1" -gt 80 ]; then printf %s "$C_PINK"
|
||||||
|
elif [ "$1" -ge 50 ]; then printf %s "$C_LAV"
|
||||||
|
else printf %s "$C_BLUE"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Credentials — SecretVault first, credential.json fallback.
|
||||||
|
# The key never appears in argv, logs, or this file.
|
||||||
|
############################################
|
||||||
|
CRED_SRC="" # "vault" | "cred"
|
||||||
|
|
||||||
|
resolve_creds() {
|
||||||
|
if [ "$NO_VAULT" != "1" ] && command -v secretvault >/dev/null 2>&1; then
|
||||||
|
case "$SECRET_NAME" in
|
||||||
|
*[!A-Za-z0-9_]*|'') ;; # unsafe name -> skip vault
|
||||||
|
*)
|
||||||
|
VK=$(secretvault run --secret "$SECRET_NAME" -- \
|
||||||
|
bash -c 'printf %s "${'"$SECRET_NAME"'}"' 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$VK" ]; then CRED_SRC="vault"; return 0; fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
if [ -r "$CRED_FILE" ] && command -v jq >/dev/null 2>&1; then
|
||||||
|
VK=$(jq -r '.clientKey // empty' "$CRED_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$VK" ]; then CRED_SRC="cred"; return 0; fi
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint_for_src() {
|
||||||
|
if [ "$CRED_SRC" = "vault" ]; then
|
||||||
|
printf '%s/api/monitor/usage/quota/limit' "${URL_BASE%/}"
|
||||||
|
else
|
||||||
|
local u
|
||||||
|
u=$(jq -r '.url // empty' "$CRED_FILE" 2>/dev/null)
|
||||||
|
[ -n "$u" ] && printf '%s/proxy/zai/api/monitor/usage/quota/limit' "${u%/}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Refresh: fetch quota, rewrite cache atomically.
|
||||||
|
# Same schema as the Aygea Claude statusline: { pct, reset_iso, ts }.
|
||||||
|
# The Authorization header reaches curl via a 0600 temp file so the key
|
||||||
|
# never lands in any process argv.
|
||||||
|
############################################
|
||||||
|
do_refresh() {
|
||||||
|
command -v jq >/dev/null 2>&1 || return 1
|
||||||
|
resolve_creds || return 1
|
||||||
|
local ep resp reset_ms reset_iso epoch
|
||||||
|
ep=$(endpoint_for_src); [ -n "$ep" ] || return 1
|
||||||
|
|
||||||
|
mkdir -p "$CACHE_DIR" 2>/dev/null || return 1
|
||||||
|
HDR_TMP="$CACHE_DIR/.hdr.$$"
|
||||||
|
RESP_TMP="$CACHE_FILE.tmp.$$"
|
||||||
|
printf 'Authorization: Bearer %s\n' "$VK" > "$HDR_TMP" || return 1
|
||||||
|
chmod 600 "$HDR_TMP" 2>/dev/null
|
||||||
|
|
||||||
|
resp=$(curl -sS --max-time "$CURL_TIMEOUT" \
|
||||||
|
--header @"$HDR_TMP" -H 'Accept: application/json' "$ep" 2>/dev/null) || return 1
|
||||||
|
|
||||||
|
reset_ms=$(printf '%s' "$resp" | jq -r '
|
||||||
|
([.data.limits[]? | select(.unit == 3)][0]) as $t
|
||||||
|
| if $t == null or $t == {} then empty else ($t.nextResetTime // 0) end' 2>/dev/null) || return 1
|
||||||
|
is_int "$reset_ms" || return 1
|
||||||
|
|
||||||
|
if [ "$reset_ms" -gt 0 ]; then
|
||||||
|
epoch=$((reset_ms / 1000))
|
||||||
|
reset_iso=$(date -d "@$epoch" +%Y-%m-%dT%H:%M:%S 2>/dev/null \
|
||||||
|
|| date -r "$epoch" +%Y-%m-%dT%H:%M:%S 2>/dev/null)
|
||||||
|
else
|
||||||
|
reset_iso=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$resp" | jq -r --arg iso "$reset_iso" '
|
||||||
|
([.data.limits[]? | select(.unit == 3)][0]) as $t
|
||||||
|
| if $t == null or $t == {} then empty else
|
||||||
|
{ pct: (($t.percentage // 0) | round),
|
||||||
|
reset_iso: (if $iso == "" then null else $iso end),
|
||||||
|
ts: (now | floor) }
|
||||||
|
end
|
||||||
|
' > "$RESP_TMP" 2>/dev/null || return 1
|
||||||
|
[ -s "$RESP_TMP" ] || { rm -f "$RESP_TMP"; return 1; }
|
||||||
|
mv -f "$RESP_TMP" "$CACHE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn_bg_refresh() {
|
||||||
|
if command -v setsid >/dev/null 2>&1; then
|
||||||
|
setsid "$0" --refresh-bg >/dev/null 2>&1 </dev/null &
|
||||||
|
else
|
||||||
|
( "$0" --refresh-bg >/dev/null 2>&1 </dev/null ) &
|
||||||
|
disown 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_bg_locked() {
|
||||||
|
mkdir -p "$CACHE_DIR" 2>/dev/null || exit 0
|
||||||
|
if command -v flock >/dev/null 2>&1; then
|
||||||
|
exec 9>"$CACHE_DIR/.refresh.lock"
|
||||||
|
flock -n 9 || exit 0 # another refresh is already running
|
||||||
|
do_refresh || exit 0
|
||||||
|
else
|
||||||
|
do_refresh || exit 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Cache reading + segment rendering
|
||||||
|
############################################
|
||||||
|
cache_age() {
|
||||||
|
[ -r "$CACHE_FILE" ] || return 1
|
||||||
|
local ts now
|
||||||
|
ts=$(jq -r '.ts // 0' "$CACHE_FILE" 2>/dev/null) || return 1
|
||||||
|
is_int "$ts" || return 1
|
||||||
|
now=$(date +%s)
|
||||||
|
if [ "$ts" -gt "$now" ]; then printf 0; else printf '%s' "$((now - ts))"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# "4:12p(2h14m)" from reset_iso (local time). Portable GNU/BSD date.
|
||||||
|
reset_info() {
|
||||||
|
[ -n "$1" ] || return 0
|
||||||
|
local now rsec epoch clk rh rm
|
||||||
|
epoch=$(date -d "$1" +%s 2>/dev/null || date -j -f '%Y-%m-%dT%H:%M:%S' "$1" +%s 2>/dev/null) || return 0
|
||||||
|
now=$(date +%s); rsec=$((epoch - now))
|
||||||
|
clk=$(date -d "@$epoch" +%_I:%M%p 2>/dev/null || date -r "$epoch" +%_I:%M%p 2>/dev/null)
|
||||||
|
[ -n "$clk" ] || return 0
|
||||||
|
clk=$(printf '%s' "$clk" | sed 's/^ *//; s/^0//; s/PM$/p/; s/AM$/a/')
|
||||||
|
if [ "$rsec" -ge 0 ]; then
|
||||||
|
rh=$((rsec / 3600)); rm=$(((rsec % 3600) / 60))
|
||||||
|
printf '%s(%sh%sm)' "$clk" "$rh" "$rm"
|
||||||
|
else
|
||||||
|
printf '%s' "$clk"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_segment() {
|
||||||
|
[ -r "$CACHE_FILE" ] || return 1
|
||||||
|
local pct rinfo seg
|
||||||
|
pct=$(jq -r '.pct // empty' "$CACHE_FILE" 2>/dev/null) || return 1
|
||||||
|
is_int "$pct" || return 1
|
||||||
|
[ "$pct" -lt 0 ] && pct=0; [ "$pct" -gt 100 ] && pct=100
|
||||||
|
rinfo=$(reset_info "$(jq -r '.reset_iso // ""' "$CACHE_FILE" 2>/dev/null)")
|
||||||
|
seg="$(pct_color "$pct")🔥 ${pct}%${R}"
|
||||||
|
[ -n "$rinfo" ] && seg="${seg}${C_PCT} · ${rinfo}${R}"
|
||||||
|
if [ "$MEM" = "1" ]; then
|
||||||
|
local m; m=$(mem_segment)
|
||||||
|
[ -n "$m" ] && seg="${m}${SEP}${seg}"
|
||||||
|
fi
|
||||||
|
printf '%s' "$seg"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 🧠 used/totalG — Linux /proc/meminfo or macOS vm_stat. Empty on failure.
|
||||||
|
mem_segment() {
|
||||||
|
local used_kb tot_kb
|
||||||
|
if [ -r /proc/meminfo ]; then
|
||||||
|
read -r tot_kb av_kb < <(awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} END{print t" "a}' /proc/meminfo 2>/dev/null)
|
||||||
|
is_int "$tot_kb" && [ "$tot_kb" -gt 0 ] && is_int "$av_kb" || return 0
|
||||||
|
used_kb=$((tot_kb - av_kb)); [ "$used_kb" -lt 0 ] && used_kb=0
|
||||||
|
elif command -v vm_stat >/dev/null 2>&1; then
|
||||||
|
local ps free_b
|
||||||
|
ps=$(pagesize 2>/dev/null || echo 16384)
|
||||||
|
free_b=$(vm_stat 2>/dev/null | awk -v ps="$ps" '
|
||||||
|
/Pages free/ {gsub(/[^0-9]/,""); f=$1}
|
||||||
|
/Pages inactive/ {gsub(/[^0-9]/,""); n=$1}
|
||||||
|
/Pages speculative/ {gsub(/[^0-9]/,""); s=$1}
|
||||||
|
END{if (f!="") print (f+n+s)*ps}')
|
||||||
|
is_int "$free_b" || return 0
|
||||||
|
tot_kb=$(sysctl -n hw.memsize 2>/dev/null)
|
||||||
|
is_int "$tot_kb" || return 0
|
||||||
|
tot_kb=$((tot_kb / 1024))
|
||||||
|
used_kb=$((tot_kb - free_b / 1024)); [ "$used_kb" -lt 0 ] && used_kb=0
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local mpct ug tg mc
|
||||||
|
mpct=$((used_kb * 100 / tot_kb))
|
||||||
|
ug=$(awk -v k="$used_kb" 'BEGIN{printf "%.1f", k/1048576}')
|
||||||
|
tg=$(awk -v k="$tot_kb" 'BEGIN{printf "%.0f", k/1048576}')
|
||||||
|
if [ "$mpct" -gt 80 ]; then mc="$C_PINK"
|
||||||
|
elif [ "$mpct" -ge 50 ]; then mc="$C_LAV"
|
||||||
|
else mc="$C_MEM"; fi
|
||||||
|
printf '%s🧠 %s/%sG%s' "$mc" "$ug" "$tg" "$R"
|
||||||
|
}
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Main
|
||||||
|
############################################
|
||||||
|
case "${1:-}" in
|
||||||
|
--refresh) do_refresh; exit $? ;;
|
||||||
|
--refresh-bg) refresh_bg_locked; exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Statusline mode: drain mcode's stdin JSON, always exit 0.
|
||||||
|
payload=$(cat 2>/dev/null || printf '')
|
||||||
|
|
||||||
|
if [ "$ONLY_GLM" = "1" ]; then
|
||||||
|
model=$(printf '%s' "$payload" | jq -r '.model // empty' 2>/dev/null)
|
||||||
|
printf '%s' "$model" | grep -qi 'glm' || exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
age=$(cache_age || true)
|
||||||
|
|
||||||
|
# Fresh cache: print it — sub-millisecond, no network, no subprocess storm.
|
||||||
|
if [ -n "${age:-}" ] && is_int "$age" && [ "$age" -le "$REFRESH_AFTER" ]; then
|
||||||
|
print_segment || true
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stale/missing: detached refresh now; show still-valid stale data meanwhile.
|
||||||
|
spawn_bg_refresh
|
||||||
|
if [ -n "${age:-}" ] && is_int "$age" && [ "$age" -le "$MAX_AGE" ]; then
|
||||||
|
print_segment || true
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
ZAI_EMBED_EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Config merge — python3 stdlib line surgery with hard safety rails:
|
||||||
|
# * refuses ambiguous structures (multiple tui blocks / duplicate keys)
|
||||||
|
# * validates the result (structural checks + PyYAML when available)
|
||||||
|
# * atomic commit: temp file + fsync + os.replace, mode preserved (0600 new)
|
||||||
|
# * timestamped backup taken BEFORE the first modification
|
||||||
|
# * everything outside the tui block byte-identical (incl. trailing NL)
|
||||||
|
############################################
|
||||||
|
merge_config() {
|
||||||
|
have python3 || die "python3 required. Add this to $CONFIG manually:
|
||||||
|
tui:
|
||||||
|
statusLine: [$ITEMS]
|
||||||
|
customStatusLine: {command: $DEST, intervalSeconds: $INTERVAL, timeoutMs: $TIMEOUT, colorMode: ansi}"
|
||||||
|
python3 - "$CONFIG" "$DEST" "$ITEMS" "$INTERVAL" "$TIMEOUT" "$mode" <<'PY'
|
||||||
|
import sys, re, os, time, shutil
|
||||||
|
|
||||||
|
cfg_path, cmd, items_csv, interval, timeout, mode = sys.argv[1:7]
|
||||||
|
items = [i.strip() for i in items_csv.split(",") if i.strip()]
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(cfg_path, encoding="utf-8") as f:
|
||||||
|
text = f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
text = ""
|
||||||
|
lines = text.splitlines()
|
||||||
|
had_nl = text.endswith("\n") if text else True
|
||||||
|
|
||||||
|
TOP = re.compile(r"^([A-Za-z_][A-Za-z0-9_-]*):\s*(#.*)?$")
|
||||||
|
TUI = re.compile(r"^tui:\s*(#.*)?$")
|
||||||
|
|
||||||
|
def die(msg):
|
||||||
|
print("merge-refused: " + msg, file=sys.stderr)
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
# --- guard: refuse ambiguous structures rather than guess ---
|
||||||
|
tui_idxs = [i for i, l in enumerate(lines) if TUI.match(l)]
|
||||||
|
if len(tui_idxs) > 1:
|
||||||
|
die("multiple top-level 'tui:' blocks — merge them manually first")
|
||||||
|
|
||||||
|
def block_bounds(start):
|
||||||
|
end = next((j for j in range(start + 1, len(lines)) if TOP.match(lines[j])), len(lines))
|
||||||
|
return start + 1, end
|
||||||
|
|
||||||
|
if tui_idxs:
|
||||||
|
a, b = block_bounds(tui_idxs[0])
|
||||||
|
for key in ("statusLine", "customStatusLine"):
|
||||||
|
n = sum(1 for l in lines[a:b] if re.match(r"^ %s:\s*(#.*)?$" % key, l))
|
||||||
|
if n > 1:
|
||||||
|
die("multiple '%s:' keys inside tui block — clean up manually first" % key)
|
||||||
|
|
||||||
|
def custom_block():
|
||||||
|
return [" customStatusLine:",
|
||||||
|
f" command: {cmd}",
|
||||||
|
f" intervalSeconds: {interval}",
|
||||||
|
f" timeoutMs: {timeout}",
|
||||||
|
" colorMode: ansi"]
|
||||||
|
|
||||||
|
def status_block():
|
||||||
|
return [" statusLine:"] + [f" - {i}" for i in items]
|
||||||
|
|
||||||
|
def validate(new_lines):
|
||||||
|
ti = next((i for i, l in enumerate(new_lines) if TUI.match(l)), None)
|
||||||
|
if ti is not None:
|
||||||
|
a = ti + 1
|
||||||
|
b = next((j for j in range(a, len(new_lines)) if TOP.match(new_lines[j])), len(new_lines))
|
||||||
|
block = new_lines[a:b]
|
||||||
|
if any(l.startswith("\t") for l in block):
|
||||||
|
die("tab indentation inside tui block")
|
||||||
|
keys2 = [m.group(1) for l in block
|
||||||
|
if (m := re.match(r"^ ([A-Za-z_][A-Za-z0-9_-]*):", l))]
|
||||||
|
if len(keys2) != len(set(keys2)):
|
||||||
|
die("duplicate keys inside tui block: %s" % keys2)
|
||||||
|
tops = [m.group(1) for l in new_lines if (m := TOP.match(l))]
|
||||||
|
if len(tops) != len(set(tops)):
|
||||||
|
die("duplicate top-level keys after merge: %s" % sorted(set(tops)))
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
yaml.safe_load("\n".join(new_lines))
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
die("merged YAML failed PyYAML validation: %s" % e)
|
||||||
|
|
||||||
|
def commit(new_lines, label):
|
||||||
|
validate(new_lines)
|
||||||
|
data = "\n".join(new_lines) + ("\n" if had_nl or not new_lines else "")
|
||||||
|
existed = os.path.exists(cfg_path)
|
||||||
|
st = os.stat(cfg_path) if existed else None
|
||||||
|
tmp = "%s.tmp.%d" % (cfg_path, os.getpid())
|
||||||
|
try:
|
||||||
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
|
f.write(data)
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
os.chmod(tmp, (st.st_mode & 0o7777) if st else 0o600)
|
||||||
|
msg = label
|
||||||
|
if existed:
|
||||||
|
bak = "%s.bak.%s" % (cfg_path, time.strftime("%Y%m%d-%H%M%S"))
|
||||||
|
shutil.copy2(cfg_path, bak)
|
||||||
|
msg += " (backup: %s)" % os.path.basename(bak)
|
||||||
|
os.replace(tmp, cfg_path)
|
||||||
|
except BaseException:
|
||||||
|
try:
|
||||||
|
os.unlink(tmp)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
if mode in ("remove", "uninstall"):
|
||||||
|
if not tui_idxs:
|
||||||
|
print("noop-no-tui"); sys.exit(0)
|
||||||
|
a, b = block_bounds(tui_idxs[0])
|
||||||
|
block, dropped = [], False
|
||||||
|
i, in_custom = a, False
|
||||||
|
while i < b:
|
||||||
|
l = lines[i]
|
||||||
|
if re.match(r"^ customStatusLine:", l):
|
||||||
|
in_custom, dropped = True, True
|
||||||
|
i += 1; continue
|
||||||
|
if in_custom:
|
||||||
|
if re.match(r"^ \S", l):
|
||||||
|
in_custom = False
|
||||||
|
else:
|
||||||
|
i += 1; continue
|
||||||
|
block.append(l); i += 1
|
||||||
|
res, kept_any, dropped_item = [], False, False
|
||||||
|
i, in_items = 0, False
|
||||||
|
while i < len(block):
|
||||||
|
l = block[i]
|
||||||
|
if re.match(r"^ statusLine:", l):
|
||||||
|
in_items = True; res.append(l); i += 1; continue
|
||||||
|
if in_items:
|
||||||
|
m = re.match(r"^ - (.+)$", l)
|
||||||
|
if m:
|
||||||
|
if m.group(1).strip() == "custom-command":
|
||||||
|
dropped_item = True; i += 1; continue
|
||||||
|
kept_any = True; res.append(l); i += 1; continue
|
||||||
|
if re.match(r"^ \S", l) or TOP.match(l):
|
||||||
|
in_items = False
|
||||||
|
if not kept_any:
|
||||||
|
res.pop()
|
||||||
|
res.append(l); i += 1
|
||||||
|
if not dropped and not dropped_item:
|
||||||
|
print("noop-clean"); sys.exit(0)
|
||||||
|
lines[tui_idxs[0] + 1:b] = res
|
||||||
|
commit(lines, "removed")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# ---- install / update ----
|
||||||
|
if not tui_idxs:
|
||||||
|
if lines:
|
||||||
|
lines.append("")
|
||||||
|
lines += ["tui:"] + status_block() + custom_block()
|
||||||
|
commit(lines, "created-tui")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
ti = tui_idxs[0]
|
||||||
|
a, b = block_bounds(ti)
|
||||||
|
block = lines[a:b]
|
||||||
|
dirty = False
|
||||||
|
|
||||||
|
sl_idx = next((k for k, l in enumerate(block) if re.match(r"^ statusLine:", l)), None)
|
||||||
|
if sl_idx is None:
|
||||||
|
block = status_block() + block
|
||||||
|
dirty = True
|
||||||
|
else:
|
||||||
|
end = sl_idx + 1
|
||||||
|
while end < len(block) and re.match(r"^ - ", block[end]):
|
||||||
|
end += 1
|
||||||
|
have_items = [re.sub(r"^ - ", "", block[k]).strip() for k in range(sl_idx + 1, end)]
|
||||||
|
if "custom-command" not in have_items:
|
||||||
|
block.insert(end, " - custom-command")
|
||||||
|
dirty = True
|
||||||
|
for l in block:
|
||||||
|
if re.match(r"^ - build-mode\s*$", l):
|
||||||
|
print("WARN: statusLine contains build-mode — mcode disables the custom command then.", file=sys.stderr)
|
||||||
|
break
|
||||||
|
|
||||||
|
cs_idx = next((k for k, l in enumerate(block) if re.match(r"^ customStatusLine:", l)), None)
|
||||||
|
if cs_idx is None:
|
||||||
|
block += custom_block()
|
||||||
|
dirty = True
|
||||||
|
else:
|
||||||
|
cend = cs_idx + 1
|
||||||
|
while cend < len(block) and re.match(r"^ ", block[cend]):
|
||||||
|
cend += 1
|
||||||
|
def set_val(key, val):
|
||||||
|
for k in range(cs_idx + 1, cend):
|
||||||
|
m = re.match(r"^(\s+%s:\s*).*$" % key, block[k])
|
||||||
|
if m:
|
||||||
|
block[k] = f"{m.group(1)}{val}"
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
for key, val in (("command", cmd), ("intervalSeconds", interval),
|
||||||
|
("timeoutMs", timeout), ("colorMode", "ansi")):
|
||||||
|
if not set_val(key, val):
|
||||||
|
block.insert(cend, f" {key}: {val}")
|
||||||
|
cend += 1
|
||||||
|
dirty = True
|
||||||
|
|
||||||
|
if not dirty:
|
||||||
|
print("already-configured")
|
||||||
|
else:
|
||||||
|
lines[a:b] = block
|
||||||
|
commit(lines, "updated")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Uninstall
|
||||||
|
############################################
|
||||||
|
if [ "$mode" = "uninstall" ]; then
|
||||||
|
rm -f "$DEST" && info "removed $DEST"
|
||||||
|
if [ -f "$CONFIG" ]; then
|
||||||
|
merge_config || die "config merge refused — file left untouched"
|
||||||
|
fi
|
||||||
|
printf 'Done.\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Install
|
||||||
|
############################################
|
||||||
|
for dep in jq curl; do
|
||||||
|
have "$dep" || info "WARN: '$dep' not found — segment stays hidden until it is installed."
|
||||||
|
done
|
||||||
|
|
||||||
|
# SecretVault presence check — never prints the secret itself.
|
||||||
|
if have secretvault; then
|
||||||
|
if secretvault run --secret "$SECRET_NAME" -- true >/dev/null 2>&1; then
|
||||||
|
info "SecretVault: secret '$SECRET_NAME' found"
|
||||||
|
else
|
||||||
|
info "WARN: SecretVault has no readable secret '$SECRET_NAME' yet."
|
||||||
|
info " Create it (z.ai API key) via the SecretVault create_secret tool;"
|
||||||
|
info " until then the segment stays hidden (credential.json also works)."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
info "WARN: secretvault CLI not found — falling back to ~/.secretvault/credential.json."
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$BIN_DIR"
|
||||||
|
materialize_script "$DEST"
|
||||||
|
info "installed $DEST"
|
||||||
|
|
||||||
|
if [ ! -d "$(dirname "$CONFIG")" ]; then
|
||||||
|
mkdir -p "$(dirname "$CONFIG")"
|
||||||
|
fi
|
||||||
|
action=$(merge_config) || die "config merge refused — file left untouched (reason above)"
|
||||||
|
info "config: $action"
|
||||||
|
|
||||||
|
if "$DEST" --refresh 2>/dev/null; then
|
||||||
|
info "quota cache primed: $(printf '{}' | "$DEST" 2>/dev/null || echo '(hidden)')"
|
||||||
|
else
|
||||||
|
info "cache prime skipped (offline or key not set) — self-heals on the next interval"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Installed. The tui section of ~/.minimax/config.yaml now has:
|
||||||
|
statusLine: $ITEMS
|
||||||
|
customStatusLine:
|
||||||
|
command: $DEST
|
||||||
|
intervalSeconds: $INTERVAL
|
||||||
|
timeoutMs: $TIMEOUT
|
||||||
|
colorMode: ansi
|
||||||
|
|
||||||
|
Restart the MiniMax Code TUI (or start a new session) to pick it up.
|
||||||
|
Preview: printf '{}' | $DEST
|
||||||
|
EOF
|
||||||
|
|
||||||
Executable
+299
@@ -0,0 +1,299 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# mcode-status — z.ai GLM 5-hour usage segment for the MiniMax Code statusline.
|
||||||
|
#
|
||||||
|
# Output (one line, Aygea palette):
|
||||||
|
# 🔥 23% · 4:12p(2h14m) (+ optional 🧠 mem segment, see AYGEA_SL_MEM)
|
||||||
|
#
|
||||||
|
# Registered in ~/.minimax/config.yaml as tui.customStatusLine.command with
|
||||||
|
# colorMode: ansi. MiniMax Code re-runs it every intervalSeconds and renders
|
||||||
|
# stdout as the "custom-command" item — mcode is the scheduler, so there is
|
||||||
|
# NO cron/systemd dependency. Network refreshes happen in a detached,
|
||||||
|
# flock-guarded background job; the statusline call itself never blocks.
|
||||||
|
#
|
||||||
|
# Credentials (checked in order):
|
||||||
|
# 1. SecretVault secret $AYGEA_SL_SECRET_NAME (default zai_api_key) via
|
||||||
|
# `secretvault run` — the public-repo-safe path. Endpoint defaults to
|
||||||
|
# https://api.z.ai (override with AYGEA_SL_URL).
|
||||||
|
# 2. Legacy ~/.secretvault/credential.json (url + clientKey, the Aygea
|
||||||
|
# Claude-statusline layout) → endpoint ${url}/proxy/zai/api/...
|
||||||
|
#
|
||||||
|
# Cache (shared with the Aygea Claude statusline):
|
||||||
|
# ~/.cache/aygea-sl/quota.json { "pct": 23, "reset_iso": "...", "ts": ... }
|
||||||
|
#
|
||||||
|
# Env tunables:
|
||||||
|
# AYGEA_SL_SECRET_NAME vault secret name (default zai_api_key)
|
||||||
|
# AYGEA_SL_URL quota base URL (default https://api.z.ai)
|
||||||
|
# AYGEA_SL_NO_VAULT=1 skip SecretVault, use credential.json only
|
||||||
|
# AYGEA_SL_CRED_FILE credential.json path (default ~/.secretvault/credential.json)
|
||||||
|
# AYGEA_SL_QUOTA_CACHE cache path (default ~/.cache/aygea-sl/quota.json)
|
||||||
|
# AYGEA_SL_REFRESH_AFTER refresh when cache older (default 150 sec)
|
||||||
|
# AYGEA_SL_MAX_AGE hide when cache older (default 360 sec)
|
||||||
|
# AYGEA_SL_CURL_TIMEOUT curl --max-time (default 6)
|
||||||
|
# AYGEA_SL_ONLY_GLM=1 show only when session model name contains "glm"
|
||||||
|
# AYGEA_SL_MEM=1 prepend an 🧠 system-memory segment (Linux/macOS)
|
||||||
|
# NO_COLOR plain text output
|
||||||
|
#
|
||||||
|
# Modes: (none) statusline mode — always exit 0, print segment or nothing
|
||||||
|
# --refresh one foreground refresh, exit non-zero on failure
|
||||||
|
#
|
||||||
|
# Requires: jq, curl. Optional: secretvault, flock, setsid.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
export LC_ALL=C
|
||||||
|
|
||||||
|
SECRET_NAME="${AYGEA_SL_SECRET_NAME:-zai_api_key}"
|
||||||
|
NO_VAULT="${AYGEA_SL_NO_VAULT:-0}"
|
||||||
|
CRED_FILE="${AYGEA_SL_CRED_FILE:-$HOME/.secretvault/credential.json}"
|
||||||
|
CACHE_FILE="${AYGEA_SL_QUOTA_CACHE:-$HOME/.cache/aygea-sl/quota.json}"
|
||||||
|
CACHE_DIR="$(dirname "$CACHE_FILE")"
|
||||||
|
REFRESH_AFTER="${AYGEA_SL_REFRESH_AFTER:-150}"
|
||||||
|
MAX_AGE="${AYGEA_SL_MAX_AGE:-360}"
|
||||||
|
CURL_TIMEOUT="${AYGEA_SL_CURL_TIMEOUT:-6}"
|
||||||
|
ONLY_GLM="${AYGEA_SL_ONLY_GLM:-0}"
|
||||||
|
MEM="${AYGEA_SL_MEM:-0}"
|
||||||
|
URL_BASE="${AYGEA_SL_URL:-https://api.z.ai}"
|
||||||
|
|
||||||
|
is_int() { case "$1" in ''|*[!0-9]*) return 1;; *) return 0;; esac; }
|
||||||
|
|
||||||
|
# Scratch files used by do_refresh; cleaned by the EXIT trap. Global (not
|
||||||
|
# function-local) so the trap can never hit unbound vars under set -u.
|
||||||
|
HDR_TMP=""; RESP_TMP=""
|
||||||
|
trap 'rm -f "${HDR_TMP:-}" "${RESP_TMP:-}" 2>/dev/null' EXIT
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Aygea dark palette (truecolor -> 256 -> plain)
|
||||||
|
############################################
|
||||||
|
if [ -n "${NO_COLOR:-}" ]; then MODE=none
|
||||||
|
elif [ "${COLORTERM:-}" = "truecolor" ] || [ "${COLORTERM:-}" = "24bit" ]; then MODE=truecolor
|
||||||
|
elif [ -z "${TERM##*256*}" ]; then MODE=256
|
||||||
|
else MODE=none; fi
|
||||||
|
|
||||||
|
if [ "$MODE" = truecolor ]; then
|
||||||
|
C_BLUE=$'\033[38;2;106;143;211m'; C_LAV=$'\033[38;2;201;184;232m'
|
||||||
|
C_PINK=$'\033[38;2;216;140;168m'; C_PCT=$'\033[38;2;192;209;230m'
|
||||||
|
C_MEM=$'\033[38;2;184;232;216m'; C_FRAME=$'\033[38;2;42;58;94m'
|
||||||
|
R=$'\033[0m'
|
||||||
|
elif [ "$MODE" = 256 ]; then
|
||||||
|
C_BLUE=$'\033[38;5;67m'; C_LAV=$'\033[38;5;182m'; C_PINK=$'\033[38;5;175m'
|
||||||
|
C_PCT=$'\033[38;5;152m'; C_MEM=$'\033[38;5;158m'; C_FRAME=$'\033[38;5;24m'
|
||||||
|
R=$'\033[0m'
|
||||||
|
else
|
||||||
|
C_BLUE=""; C_LAV=""; C_PINK=""; C_PCT=""; C_MEM=""; C_FRAME=""; R=""
|
||||||
|
fi
|
||||||
|
SEP="${C_FRAME} · ${R}"
|
||||||
|
|
||||||
|
pct_color() {
|
||||||
|
if [ "$1" -gt 80 ]; then printf %s "$C_PINK"
|
||||||
|
elif [ "$1" -ge 50 ]; then printf %s "$C_LAV"
|
||||||
|
else printf %s "$C_BLUE"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Credentials — SecretVault first, credential.json fallback.
|
||||||
|
# The key never appears in argv, logs, or this file.
|
||||||
|
############################################
|
||||||
|
CRED_SRC="" # "vault" | "cred"
|
||||||
|
|
||||||
|
resolve_creds() {
|
||||||
|
if [ "$NO_VAULT" != "1" ] && command -v secretvault >/dev/null 2>&1; then
|
||||||
|
case "$SECRET_NAME" in
|
||||||
|
*[!A-Za-z0-9_]*|'') ;; # unsafe name -> skip vault
|
||||||
|
*)
|
||||||
|
VK=$(secretvault run --secret "$SECRET_NAME" -- \
|
||||||
|
bash -c 'printf %s "${'"$SECRET_NAME"'}"' 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$VK" ]; then CRED_SRC="vault"; return 0; fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
if [ -r "$CRED_FILE" ] && command -v jq >/dev/null 2>&1; then
|
||||||
|
VK=$(jq -r '.clientKey // empty' "$CRED_FILE" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
if [ -n "$VK" ]; then CRED_SRC="cred"; return 0; fi
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint_for_src() {
|
||||||
|
if [ "$CRED_SRC" = "vault" ]; then
|
||||||
|
printf '%s/api/monitor/usage/quota/limit' "${URL_BASE%/}"
|
||||||
|
else
|
||||||
|
local u
|
||||||
|
u=$(jq -r '.url // empty' "$CRED_FILE" 2>/dev/null)
|
||||||
|
[ -n "$u" ] && printf '%s/proxy/zai/api/monitor/usage/quota/limit' "${u%/}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Refresh: fetch quota, rewrite cache atomically.
|
||||||
|
# Same schema as the Aygea Claude statusline: { pct, reset_iso, ts }.
|
||||||
|
# The Authorization header reaches curl via a 0600 temp file so the key
|
||||||
|
# never lands in any process argv.
|
||||||
|
############################################
|
||||||
|
do_refresh() {
|
||||||
|
command -v jq >/dev/null 2>&1 || return 1
|
||||||
|
resolve_creds || return 1
|
||||||
|
local ep resp reset_ms reset_iso epoch
|
||||||
|
ep=$(endpoint_for_src); [ -n "$ep" ] || return 1
|
||||||
|
|
||||||
|
mkdir -p "$CACHE_DIR" 2>/dev/null || return 1
|
||||||
|
HDR_TMP="$CACHE_DIR/.hdr.$$"
|
||||||
|
RESP_TMP="$CACHE_FILE.tmp.$$"
|
||||||
|
printf 'Authorization: Bearer %s\n' "$VK" > "$HDR_TMP" || return 1
|
||||||
|
chmod 600 "$HDR_TMP" 2>/dev/null
|
||||||
|
|
||||||
|
resp=$(curl -sS --max-time "$CURL_TIMEOUT" \
|
||||||
|
--header @"$HDR_TMP" -H 'Accept: application/json' "$ep" 2>/dev/null) || return 1
|
||||||
|
|
||||||
|
reset_ms=$(printf '%s' "$resp" | jq -r '
|
||||||
|
([.data.limits[]? | select(.unit == 3)][0]) as $t
|
||||||
|
| if $t == null or $t == {} then empty else ($t.nextResetTime // 0) end' 2>/dev/null) || return 1
|
||||||
|
is_int "$reset_ms" || return 1
|
||||||
|
|
||||||
|
if [ "$reset_ms" -gt 0 ]; then
|
||||||
|
epoch=$((reset_ms / 1000))
|
||||||
|
reset_iso=$(date -d "@$epoch" +%Y-%m-%dT%H:%M:%S 2>/dev/null \
|
||||||
|
|| date -r "$epoch" +%Y-%m-%dT%H:%M:%S 2>/dev/null)
|
||||||
|
else
|
||||||
|
reset_iso=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$resp" | jq -r --arg iso "$reset_iso" '
|
||||||
|
([.data.limits[]? | select(.unit == 3)][0]) as $t
|
||||||
|
| if $t == null or $t == {} then empty else
|
||||||
|
{ pct: (($t.percentage // 0) | round),
|
||||||
|
reset_iso: (if $iso == "" then null else $iso end),
|
||||||
|
ts: (now | floor) }
|
||||||
|
end
|
||||||
|
' > "$RESP_TMP" 2>/dev/null || return 1
|
||||||
|
[ -s "$RESP_TMP" ] || { rm -f "$RESP_TMP"; return 1; }
|
||||||
|
mv -f "$RESP_TMP" "$CACHE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn_bg_refresh() {
|
||||||
|
if command -v setsid >/dev/null 2>&1; then
|
||||||
|
setsid "$0" --refresh-bg >/dev/null 2>&1 </dev/null &
|
||||||
|
else
|
||||||
|
( "$0" --refresh-bg >/dev/null 2>&1 </dev/null ) &
|
||||||
|
disown 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_bg_locked() {
|
||||||
|
mkdir -p "$CACHE_DIR" 2>/dev/null || exit 0
|
||||||
|
if command -v flock >/dev/null 2>&1; then
|
||||||
|
exec 9>"$CACHE_DIR/.refresh.lock"
|
||||||
|
flock -n 9 || exit 0 # another refresh is already running
|
||||||
|
do_refresh || exit 0
|
||||||
|
else
|
||||||
|
do_refresh || exit 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Cache reading + segment rendering
|
||||||
|
############################################
|
||||||
|
cache_age() {
|
||||||
|
[ -r "$CACHE_FILE" ] || return 1
|
||||||
|
local ts now
|
||||||
|
ts=$(jq -r '.ts // 0' "$CACHE_FILE" 2>/dev/null) || return 1
|
||||||
|
is_int "$ts" || return 1
|
||||||
|
now=$(date +%s)
|
||||||
|
if [ "$ts" -gt "$now" ]; then printf 0; else printf '%s' "$((now - ts))"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# "4:12p(2h14m)" from reset_iso (local time). Portable GNU/BSD date.
|
||||||
|
reset_info() {
|
||||||
|
[ -n "$1" ] || return 0
|
||||||
|
local now rsec epoch clk rh rm
|
||||||
|
epoch=$(date -d "$1" +%s 2>/dev/null || date -j -f '%Y-%m-%dT%H:%M:%S' "$1" +%s 2>/dev/null) || return 0
|
||||||
|
now=$(date +%s); rsec=$((epoch - now))
|
||||||
|
clk=$(date -d "@$epoch" +%_I:%M%p 2>/dev/null || date -r "$epoch" +%_I:%M%p 2>/dev/null)
|
||||||
|
[ -n "$clk" ] || return 0
|
||||||
|
clk=$(printf '%s' "$clk" | sed 's/^ *//; s/^0//; s/PM$/p/; s/AM$/a/')
|
||||||
|
if [ "$rsec" -ge 0 ]; then
|
||||||
|
rh=$((rsec / 3600)); rm=$(((rsec % 3600) / 60))
|
||||||
|
printf '%s(%sh%sm)' "$clk" "$rh" "$rm"
|
||||||
|
else
|
||||||
|
printf '%s' "$clk"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_segment() {
|
||||||
|
[ -r "$CACHE_FILE" ] || return 1
|
||||||
|
local pct rinfo seg
|
||||||
|
pct=$(jq -r '.pct // empty' "$CACHE_FILE" 2>/dev/null) || return 1
|
||||||
|
is_int "$pct" || return 1
|
||||||
|
[ "$pct" -lt 0 ] && pct=0; [ "$pct" -gt 100 ] && pct=100
|
||||||
|
rinfo=$(reset_info "$(jq -r '.reset_iso // ""' "$CACHE_FILE" 2>/dev/null)")
|
||||||
|
seg="$(pct_color "$pct")🔥 ${pct}%${R}"
|
||||||
|
[ -n "$rinfo" ] && seg="${seg}${C_PCT} · ${rinfo}${R}"
|
||||||
|
if [ "$MEM" = "1" ]; then
|
||||||
|
local m; m=$(mem_segment)
|
||||||
|
[ -n "$m" ] && seg="${m}${SEP}${seg}"
|
||||||
|
fi
|
||||||
|
printf '%s' "$seg"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 🧠 used/totalG — Linux /proc/meminfo or macOS vm_stat. Empty on failure.
|
||||||
|
mem_segment() {
|
||||||
|
local used_kb tot_kb
|
||||||
|
if [ -r /proc/meminfo ]; then
|
||||||
|
read -r tot_kb av_kb < <(awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} END{print t" "a}' /proc/meminfo 2>/dev/null)
|
||||||
|
is_int "$tot_kb" && [ "$tot_kb" -gt 0 ] && is_int "$av_kb" || return 0
|
||||||
|
used_kb=$((tot_kb - av_kb)); [ "$used_kb" -lt 0 ] && used_kb=0
|
||||||
|
elif command -v vm_stat >/dev/null 2>&1; then
|
||||||
|
local ps free_b
|
||||||
|
ps=$(pagesize 2>/dev/null || echo 16384)
|
||||||
|
free_b=$(vm_stat 2>/dev/null | awk -v ps="$ps" '
|
||||||
|
/Pages free/ {gsub(/[^0-9]/,""); f=$1}
|
||||||
|
/Pages inactive/ {gsub(/[^0-9]/,""); n=$1}
|
||||||
|
/Pages speculative/ {gsub(/[^0-9]/,""); s=$1}
|
||||||
|
END{if (f!="") print (f+n+s)*ps}')
|
||||||
|
is_int "$free_b" || return 0
|
||||||
|
tot_kb=$(sysctl -n hw.memsize 2>/dev/null)
|
||||||
|
is_int "$tot_kb" || return 0
|
||||||
|
tot_kb=$((tot_kb / 1024))
|
||||||
|
used_kb=$((tot_kb - free_b / 1024)); [ "$used_kb" -lt 0 ] && used_kb=0
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local mpct ug tg mc
|
||||||
|
mpct=$((used_kb * 100 / tot_kb))
|
||||||
|
ug=$(awk -v k="$used_kb" 'BEGIN{printf "%.1f", k/1048576}')
|
||||||
|
tg=$(awk -v k="$tot_kb" 'BEGIN{printf "%.0f", k/1048576}')
|
||||||
|
if [ "$mpct" -gt 80 ]; then mc="$C_PINK"
|
||||||
|
elif [ "$mpct" -ge 50 ]; then mc="$C_LAV"
|
||||||
|
else mc="$C_MEM"; fi
|
||||||
|
printf '%s🧠 %s/%sG%s' "$mc" "$ug" "$tg" "$R"
|
||||||
|
}
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# Main
|
||||||
|
############################################
|
||||||
|
case "${1:-}" in
|
||||||
|
--refresh) do_refresh; exit $? ;;
|
||||||
|
--refresh-bg) refresh_bg_locked; exit 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Statusline mode: drain mcode's stdin JSON, always exit 0.
|
||||||
|
payload=$(cat 2>/dev/null || printf '')
|
||||||
|
|
||||||
|
if [ "$ONLY_GLM" = "1" ]; then
|
||||||
|
model=$(printf '%s' "$payload" | jq -r '.model // empty' 2>/dev/null)
|
||||||
|
printf '%s' "$model" | grep -qi 'glm' || exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
age=$(cache_age || true)
|
||||||
|
|
||||||
|
# Fresh cache: print it — sub-millisecond, no network, no subprocess storm.
|
||||||
|
if [ -n "${age:-}" ] && is_int "$age" && [ "$age" -le "$REFRESH_AFTER" ]; then
|
||||||
|
print_segment || true
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stale/missing: detached refresh now; show still-valid stale data meanwhile.
|
||||||
|
spawn_bg_refresh
|
||||||
|
if [ -n "${age:-}" ] && is_int "$age" && [ "$age" -le "$MAX_AGE" ]; then
|
||||||
|
print_segment || true
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
Reference in new issue
Block a user