#!/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 /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 2>&1 /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 <