#!/usr/bin/env bash # # GreenTokens setup for macOS and Linux: # # curl -fsSL https://www.greentokens.io/setup.sh | bash # # Connects coding agents to GreenTokens: Claude Code, Claude Desktop (macOS), # Codex CLI, OpenCode, Grok CLI, OpenClaw and Hermes Agent. You pick the apps; # it confirms each file before changing it, backs up every file it edits, and # changes only the GreenTokens parts. Run it again and choose "Remove" to undo. # Windows: https://www.greentokens.io/setup.ps1 # # Claude Code and Claude Desktop keep the key in their own settings. The other # agents read it from the GREENTOKENS_API_KEY variable, saved in your shell's # startup file. # # Everything runs from main() on the last line, so a partly downloaded script # does nothing. set -euo pipefail BASE_URL="https://api.greentokens.io" DOCS_URL="https://www.greentokens.io/docs/clients" KEYS_URL="https://www.greentokens.io/dashboard/api-keys" OPUS_MODEL="claude-opus-5-5" SONNET_MODEL="claude-sonnet-5" HAIKU_MODEL="claude-haiku-4-5" CODEX_MODEL="gpt-6-sol" GROK_MODEL="grok-4.7" AGENT_MODEL="claude-sonnet-5" DESKTOP_BUNDLE_ID="com.anthropic.claudefordesktop" PROFILE_ID="io.greentokens.claude-desktop" KEY_VAR="GREENTOKENS_API_KEY" BLOCK_START="# >>> GreenTokens >>>" BLOCK_END="# <<< GreenTokens <<<" OS="" MODE="" KEY="" ENV_FILE="" TMP_DIR="" ANSWER="" DONE=() # One entry per app, in menu order. APP_PATH is what gets edited (a config # file, Claude.app, or the openclaw command); empty means skipped. APP_IDS=() APP_NAMES=() APP_FOUND=() APP_DEFAULT=() APP_SELECTED=() APP_PATH=() if [ -t 1 ] && [ -z "${NO_COLOR-}" ]; then BOLD=$'\033[1m' DIM=$'\033[2m' GREEN=$'\033[32m' YELLOW=$'\033[33m' RED=$'\033[31m' RESET=$'\033[0m' else BOLD="" DIM="" GREEN="" YELLOW="" RED="" RESET="" fi say() { printf '%s\n' "$*"; } note() { printf '%s%s%s\n' "$DIM" "$*" "$RESET"; } heading() { printf '\n%s%s%s\n' "$BOLD" "$*" "$RESET"; } ok() { printf '%s✓%s %s\n' "$GREEN" "$RESET" "$*"; } warn() { printf '%s!%s %s\n' "$YELLOW" "$RESET" "$*"; } fail() { printf '%s✗ %s%s\n' "$RED" "$*" "$RESET" >&2 exit 1 } has() { command -v "$1" >/dev/null 2>&1; } # Shows $HOME as ~. tidy() { case $1 in "$HOME"/*) printf '~%s' "${1#"$HOME"}" ;; *) printf '%s' "$1" ;; esac } # Reads a line into ANSWER. Input comes from the terminal: under `curl | bash`, # stdin is the script itself. ask() { printf '%s' "$1" ANSWER="" IFS= read -r ANSWER /dev/null || true } read_key() { heading "API key" say "Paste your GreenTokens API key. It starts with sk-gt- and stays hidden as you type." note "No key yet? Create one at $KEYS_URL" local attempt code for attempt in 1 2 3; do printf 'API key: ' KEY="" IFS= read -rs KEY /dev/null else python3 -c "$JSON_PY" 2>/dev/null fi } edit_claude_code() { GT_APP=claude-code json_editor; } edit_opencode() { GT_APP=opencode json_editor; } # The line-based editors below share this: blank lines are held back and only # printed before the next real line, so removed sections leave no gaps and # trailing blank lines are dropped. AWK_OUT='function out(line) { if (line ~ /^[ \t]*$/) { held = held line "\n"; return } printf "%s%s\n", held, line; held = "" }' # Codex: ~/.codex/config.toml. Sets the top-level model and model_provider # (which must come before any [table]) and adds [model_providers.greentokens]. # Remove drops the table, and the top-level keys only while they point at it. edit_codex() { awk "$AWK_OUT"' function is_header(line) { return line ~ /^[ \t]*\[/ } function is_ours(line) { return line ~ /^[ \t]*\[[ \t]*model_providers\.greentokens[ \t]*\][ \t]*(#.*)?$/ } NR == FNR { if (is_header($0)) intable = 1 else if (!intable && $0 ~ /^[ \t]*model_provider[ \t]*=[ \t]*["'"'"']greentokens["'"'"']/) provider_is_ours = 1 next } FNR == 1 { intable = 0 } BEGIN { set = ENVIRON["GT_MODE"] == "set" if (set) printf "model = \"%s\"\nmodel_provider = \"greentokens\"\n", ENVIRON["GT_CODEX_MODEL"] } is_header($0) { intable = 1; skip = is_ours($0) } skip { next } !intable && (set || provider_is_ours) && $0 ~ /^[ \t]*model(_provider)?[ \t]*=/ { next } { out($0) } END { if (set) { printf "\n[model_providers.greentokens]\nname = \"GreenTokens\"\nbase_url = \"%s/v1\"\n", ENVIRON["GT_BASE"] printf "env_key = \"GREENTOKENS_API_KEY\"\nwire_api = \"responses\"\n" } } ' "$GT_FILE" "$GT_FILE" } # Grok CLI: ~/.grok/config.toml. Adds a [model."…"] table per model and sets # [models] default. Our tables are the [model.…] ones whose base_url is # GreenTokens; remove drops those, and the default only if it named one. edit_grok() { awk "$AWK_OUT"' function is_header(line) { return line ~ /^[ \t]*\[/ } function quoted(line) { sub(/^[^"]*"/, "", line); sub(/".*$/, "", line); return line } NR == FNR { if (is_header($0)) { section++ model_table[section] = $0 ~ /^[ \t]*\[[ \t]*model\./ name[section] = quoted($0) if ($0 ~ /^[ \t]*\[[ \t]*models[ \t]*\]/) has_models = 1 } else if (index($0, "base_url") && index($0, "\"" ENVIRON["GT_BASE"] "/v1\"")) { ours[section] = 1 } next } # Ours, plus (when setting) any other table with one of our model names, # since TOML refuses the same table twice. FNR == 1 { for (s = 1; s <= section; s++) { if (model_table[s] && (ours[s] || (set && (name[s] == default_model || name[s] == agent)))) { drop[s] = 1 dropped = dropped " " name[s] " " } } section = 0 } BEGIN { set = ENVIRON["GT_MODE"] == "set"; default_model = ENVIRON["GT_GROK_MODEL"]; agent = ENVIRON["GT_AGENT_MODEL"] } is_header($0) { section++ in_models = $0 ~ /^[ \t]*\[[ \t]*models[ \t]*\]/ } drop[section] { next } in_models && $0 ~ /^[ \t]*default[ \t]*=/ && (set || index(dropped, " " quoted($0) " ")) { next } { out($0) } in_models && is_header($0) && set { printf "default = \"%s\"\n", default_model } END { if (!set) exit if (!has_models) printf "\n[models]\ndefault = \"%s\"\n", default_model split(default_model " " agent, models, " ") for (i = 1; i <= 2; i++) { label = models[i] == "grok-4.7" ? "Grok 4.7" : models[i] == "claude-sonnet-5" ? "Claude Sonnet 5" : models[i] printf "\n[model.\"%s\"]\nmodel = \"%s\"\nname = \"%s (GreenTokens)\"\n", models[i], models[i], label printf "base_url = \"%s/v1\"\nenv_key = \"GREENTOKENS_API_KEY\"\napi_backend = \"chat_completions\"\n", ENVIRON["GT_BASE"] } } ' "$GT_FILE" "$GT_FILE" } # Hermes Agent: ~/.hermes/config.yaml. Replaces the top-level model: block (it # runs until the next line that starts at column 0). Remove drops it only when # it points at GreenTokens. edit_hermes() { awk "$AWK_OUT"' function starts_block(line) { return line ~ /^model[ \t]*:/ } function ends_block(line) { return line !~ /^[ \t]*$/ && line ~ /^[^ \t]/ } NR == FNR { if (starts_block($0)) inblock = 1 else if (ends_block($0)) inblock = 0 else if (inblock && index($0, ENVIRON["GT_BASE"] "/v1")) ours = 1 next } FNR == 1 { inblock = 0 } BEGIN { set = ENVIRON["GT_MODE"] == "set" } starts_block($0) { inblock = (set || ours); if (inblock) next } inblock && ends_block($0) { inblock = 0 } inblock { next } { out($0) } END { if (set) { printf "\nmodel:\n provider: custom\n default: %s\n base_url: %s/v1\n key_env: GREENTOKENS_API_KEY\n", ENVIRON["GT_AGENT_MODEL"], ENVIRON["GT_BASE"] } } ' "$GT_FILE" "$GT_FILE" } # Hermes Agent's ~/.hermes/.env: one GREENTOKENS_API_KEY= line. edit_hermes_env() { awk "$AWK_OUT"' $0 ~ /^[ \t]*(export[ \t]+)?GREENTOKENS_API_KEY[ \t]*=/ { next } { out($0) } END { if (ENVIRON["GT_MODE"] == "set") printf "GREENTOKENS_API_KEY=%s\n", ENVIRON["GT_KEY"] } ' "$GT_FILE" } # Shell startup file: the marked GreenTokens block that exports the key. edit_profile() { awk "$AWK_OUT"' BEGIN { start = ENVIRON["GT_BLOCK_START"]; end = ENVIRON["GT_BLOCK_END"] } $0 == start { skip = 1; next } skip { if ($0 == end) skip = 0; next } { out($0); any = 1 } END { if (ENVIRON["GT_MODE"] != "set") exit if (any) printf "\n" printf "%s\n", start if (ENVIRON["GT_FISH"] == "1") printf "set -gx GREENTOKENS_API_KEY \"%s\"\n", ENVIRON["GT_KEY"] else printf "export GREENTOKENS_API_KEY=\"%s\"\n", ENVIRON["GT_KEY"] printf "%s\n", end } ' "$GT_FILE" } # edit_file MODE FILE EDITOR: runs an editor on FILE and writes the result # back, after a backup. Sets EDITED to "changed", "unchanged" or "failed". # Writing through the existing file (not moving a new one over it) keeps its # permissions and any symlink, such as a dotfiles repo's. EDITED="" edit_file() { local mode=$1 file=$2 editor=$3 source=/dev/null tmp backup EDITED=failed if [ -f "$file" ]; then source=$file elif [ "$mode" = remove ]; then EDITED=unchanged return fi mkdir -p "$(dirname "$file")" || return 0 TMP_DIR=${TMP_DIR:-$(mktemp -d)} tmp="$TMP_DIR/edit" if ! GT_FILE=$source GT_MODE=$mode GT_BASE=$BASE_URL GT_KEY=$KEY GT_OPUS=$OPUS_MODEL GT_SONNET=$SONNET_MODEL \ GT_HAIKU=$HAIKU_MODEL GT_CODEX_MODEL=$CODEX_MODEL GT_GROK_MODEL=$GROK_MODEL GT_AGENT_MODEL=$AGENT_MODEL \ GT_BLOCK_START=$BLOCK_START GT_BLOCK_END=$BLOCK_END GT_FISH=$([ "${file%.fish}" != "$file" ] && echo 1 || echo 0) \ "$editor" >"$tmp"; then return 0 fi if [ -f "$file" ] && cmp -s "$file" "$tmp"; then EDITED=unchanged return fi if [ -f "$file" ]; then backup="$file.backup-$(date +%Y%m%d-%H%M%S)" cp -p "$file" "$backup" chmod 600 "$backup" note "Backup: $(tidy "$backup")" cat "$tmp" >"$file" else (umask 077 && cat "$tmp" >"$file") fi EDITED=changed } # Reports an edit_file result for a named file. report_edit() { local mode=$1 file=$2 case $EDITED in changed) if [ "$mode" = set ]; then ok "Updated $(tidy "$file")."; else ok "Removed GreenTokens from $(tidy "$file")."; fi ;; unchanged) if [ "$mode" = set ]; then ok "$(tidy "$file") was already set up."; else say "No GreenTokens settings in $(tidy "$file")."; fi ;; *) warn "Couldn't update $(tidy "$file"): it couldn't be read as $3, or couldn't be written. Nothing was changed." ;; esac [ "$EDITED" != failed ] } # --- Apps --------------------------------------------------------------------- # # detect_ sets DETECTED to the usual path and succeeds if the app looks # installed. apply_ MODE PATH makes the change. DETECTED="" detect_claude_code() { local dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" DETECTED="$dir/settings.json" has claude || [ -d "$dir" ] } detect_codex() { local dir="${CODEX_HOME:-$HOME/.codex}" DETECTED="$dir/config.toml" has codex || [ -d "$dir" ] } detect_opencode() { local dir="${XDG_CONFIG_HOME:-$HOME/.config}/opencode" DETECTED="$dir/opencode.json" if [ ! -f "$DETECTED" ] && [ -f "$dir/opencode.jsonc" ]; then DETECTED="$dir/opencode.jsonc"; fi has opencode || [ -d "$dir" ] } detect_grok() { DETECTED="$HOME/.grok/config.toml" has grok || [ -d "$HOME/.grok" ] } detect_openclaw() { DETECTED=$(command -v openclaw 2>/dev/null || true) [ -n "$DETECTED" ] } detect_hermes() { DETECTED="$HOME/.hermes/config.yaml" has hermes || [ -d "$HOME/.hermes" ] } detect_claude_desktop() { DETECTED=$(find_desktop_app) [ -n "$DETECTED" ] } apply_claude_code() { local mode=$1 file=$2 had_api_key=0 if [ "$mode" = set ] && [ -f "$file" ] && grep -q '"ANTHROPIC_API_KEY"' "$file"; then had_api_key=1; fi edit_file "$mode" "$file" edit_claude_code report_edit "$mode" "$file" JSON || return 0 if [ "$mode" = remove ]; then [ "$EDITED" = changed ] && DONE+=("Claude Code: new sessions use your previous setup again.") return fi if [ "$had_api_key" = 1 ]; then note "Removed ANTHROPIC_API_KEY from that file, since it would clash with the GreenTokens key."; fi if [ -n "${ANTHROPIC_API_KEY-}" ] || [ -n "${ANTHROPIC_BASE_URL-}" ]; then warn "Your shell also sets ANTHROPIC_API_KEY or ANTHROPIC_BASE_URL, which can override these settings." say " If Claude Code doesn't use GreenTokens, remove them from your shell's startup file." fi DONE+=("Claude Code: start a new session with \`claude\` and it uses GreenTokens.") } apply_codex() { edit_file "$1" "$2" edit_codex report_edit "$1" "$2" TOML || return 0 if [ "$1" = set ]; then DONE+=("Codex CLI: run \`codex\`; it uses $CODEX_MODEL on GreenTokens. Pick another with \`codex --model\`.") elif [ "$EDITED" = changed ]; then DONE+=("Codex CLI: back to its default provider.") fi } apply_opencode() { edit_file "$1" "$2" edit_opencode report_edit "$1" "$2" "JSON (files with comments aren't supported)" || return 0 if [ "$1" = set ]; then DONE+=("OpenCode: run \`opencode\`; GreenTokens models are under the GreenTokens provider.") elif [ "$EDITED" = changed ]; then DONE+=("OpenCode: GreenTokens provider removed.") fi } apply_grok() { edit_file "$1" "$2" edit_grok report_edit "$1" "$2" TOML || return 0 if [ "$1" = set ]; then DONE+=("Grok CLI: run \`grok\` (uses $GROK_MODEL) or \`grok -m $AGENT_MODEL\`.") elif [ "$EDITED" = changed ]; then DONE+=("Grok CLI: GreenTokens models removed.") fi } apply_hermes() { local env_file env_file="$(dirname "$2")/.env" edit_file "$1" "$2" edit_hermes report_edit "$1" "$2" YAML || return 0 edit_file "$1" "$env_file" edit_hermes_env report_edit "$1" "$env_file" "a .env file" || return 0 if [ "$1" = set ]; then DONE+=("Hermes Agent: run \`hermes\`; it uses $AGENT_MODEL on GreenTokens.") else DONE+=("Hermes Agent: run \`hermes model\` to choose a provider again.") fi } # OpenClaw is set up through its own `openclaw config` command, as our docs # show, rather than by editing its config file. apply_openclaw() { local mode=$1 cmd=$2 output local provider="{\"baseUrl\":\"$BASE_URL/v1\",\"apiKey\":\"\${GREENTOKENS_API_KEY}\",\"api\":\"openai-completions\",\"models\":[{\"id\":\"claude-sonnet-5\",\"name\":\"Claude Sonnet 5\"},{\"id\":\"claude-opus-5-5\",\"name\":\"Claude Opus 5.5\"},{\"id\":\"gpt-6-sol\",\"name\":\"GPT-6 Sol\"},{\"id\":\"glm-5.3\",\"name\":\"GLM-5.3\"}]}" if [ "$mode" = set ]; then if output=$("$cmd" config set models.providers.greentokens "$provider" --strict-json --merge 2>&1 &1 &1 /dev/null /dev/null 2>&1 /dev/null || true)" = "$DESKTOP_BUNDLE_ID" ] } find_desktop_app() { local path for path in "/Applications/Claude.app" "$HOME/Applications/Claude.app"; do if is_desktop_app "$path"; then printf '%s' "$path" return fi done path=$(mdfind "kMDItemCFBundleIdentifier == '$DESKTOP_BUNDLE_ID'" 2>/dev/null | head -n 1 || true) if [ -n "$path" ] && is_desktop_app "$path"; then printf '%s' "$path"; fi } # True once Claude Desktop's managed settings point at GreenTokens, or our # profile is listed. desktop_configured() { local user file user=$(id -un) for file in "/Library/Managed Preferences/$user/$DESKTOP_BUNDLE_ID" "/Library/Managed Preferences/$DESKTOP_BUNDLE_ID"; do if [ "$(defaults read "$file" inferenceGatewayBaseUrl 2>/dev/null || true)" = "$BASE_URL" ]; then return 0; fi done # grep without -q reads to the end, so `profiles` never dies of SIGPIPE under pipefail. profiles list 2>/dev/null | grep "$PROFILE_ID" >/dev/null } open_profiles_settings() { open "x-apple.systempreferences:com.apple.preferences.configurationprofiles" 2>/dev/null || true } restart_desktop() { local app=$1 running tries running=$(osascript -e "application id \"$DESKTOP_BUNDLE_ID\" is running" 2>/dev/null || true) if [ "$running" != true ]; then if confirm "Open Claude Desktop now?" y; then open "$app"; fi return fi if ! confirm "Claude Desktop is open. Restart it now so it picks up the change?" y; then say "Quit Claude Desktop fully (⌘Q) and open it again when you're ready." return fi osascript -e "tell application id \"$DESKTOP_BUNDLE_ID\" to quit" >/dev/null 2>&1 || true for tries in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do running=$(osascript -e "application id \"$DESKTOP_BUNDLE_ID\" is running" 2>/dev/null || true) [ "$running" = true ] || break sleep 1 done if [ "$running" = true ]; then warn "Claude Desktop didn't quit. Quit it yourself (⌘Q) and open it again." return fi open "$app" ok "Restarted Claude Desktop." } # The settings go in as a configuration profile: the one place, besides the # app's own settings window, where Claude Desktop documents reading them from. write_profile() { cat >"$1" < PayloadContent PayloadType $DESKTOP_BUNDLE_ID PayloadIdentifier $PROFILE_ID.settings PayloadUUID $(uuidgen) PayloadVersion 1 PayloadDisplayName Claude Desktop inferenceProvider gateway inferenceGatewayBaseUrl $BASE_URL inferenceGatewayApiKey $KEY PayloadDisplayName GreenTokens for Claude Desktop PayloadDescription Connects Claude Desktop to GreenTokens ($BASE_URL). PayloadIdentifier $PROFILE_ID PayloadOrganization GreenTokens PayloadScope User PayloadType Configuration PayloadUUID $(uuidgen) PayloadVersion 1 EOF } apply_claude_desktop() { local mode=$1 app=$2 profile if [ "$mode" = remove ]; then remove_claude_desktop "$app" return fi # mktemp -d makes a folder only you can read; it's deleted when the script ends. TMP_DIR=${TMP_DIR:-$(mktemp -d)} profile="$TMP_DIR/GreenTokens for Claude Desktop.mobileconfig" write_profile "$profile" open "$profile" open_profiles_settings say "macOS is adding a settings profile. To install it:" say " 1. In System Settings, open Device Management (called Profiles on older macOS;" say " search for it if you don't see it)." say " 2. Double-click “GreenTokens for Claude Desktop” and click Install." note " macOS marks the profile as unverified because it isn't signed. That's expected." while true; do ask "Press Enter once it's installed, or type s to skip: " if [ "$ANSWER" = s ]; then warn "Skipped. Claude Desktop wasn't changed." return fi if desktop_configured; then break; fi warn "The profile isn't installed yet." done ok "Claude Desktop is set to use GreenTokens." DONE+=("Claude Desktop: on the sign-in screen, choose the third-party option, not the Anthropic sign-in.") restart_desktop "$app" } remove_claude_desktop() { local app=$1 if ! desktop_configured; then say "Claude Desktop isn't set up for GreenTokens. Nothing to remove." return fi open_profiles_settings say "To remove the settings, in System Settings → Device Management (or Profiles)," say "select “GreenTokens for Claude Desktop” and click the minus (−) button." while true; do ask "Press Enter once it's removed, or type s to skip: " if [ "$ANSWER" = s ]; then return; fi if ! desktop_configured; then break; fi warn "The profile is still installed." done ok "Removed GreenTokens from Claude Desktop." DONE+=("Claude Desktop: back to its normal sign-in.") restart_desktop "$app" } # --- Choosing apps and paths -------------------------------------------------- register_app() { APP_IDS+=("$1") APP_NAMES+=("$2") if "detect_$1"; then APP_FOUND+=(1); else APP_FOUND+=(0); fi APP_DEFAULT+=("$DETECTED") APP_SELECTED+=(0) APP_PATH+=("") } # Lists the apps and reads which ones to include into APP_SELECTED. choose_apps() { local i count=${#APP_IDS[@]} found="" token ok_input if [ "$MODE" = set ]; then heading "Which apps should use GreenTokens?"; else heading "Which apps should GreenTokens be removed from?"; fi for ((i = 0; i < count; i++)); do if [ "${APP_FOUND[$i]}" = 1 ]; then printf ' %d) %-16s %sfound%s\n' $((i + 1)) "${APP_NAMES[$i]}" "$GREEN" "$RESET" found="$found $((i + 1))" else printf ' %d) %-16s %snot found%s\n' $((i + 1)) "${APP_NAMES[$i]}" "$DIM" "$RESET" fi done found=${found# } while true; do if [ -n "$found" ]; then ask "Type the numbers (for example: 1 3), \"all\", or press Enter for the found ones [$found]: " [ -n "$ANSWER" ] || ANSWER=$found else ask "Type the numbers (for example: 1 3) or \"all\": " fi for ((i = 0; i < count; i++)); do APP_SELECTED[i]=0; done ok_input=1 for token in ${ANSWER//,/ }; do case $token in all | ALL | All) for ((i = 0; i < count; i++)); do APP_SELECTED[i]=1; done ;; *[!0-9]* | "") ok_input=0 ;; *) if [ "$token" -ge 1 ] && [ "$token" -le "$count" ]; then APP_SELECTED[token - 1]=1; else ok_input=0; fi ;; esac done if [ "$ok_input" = 1 ] && [[ " ${APP_SELECTED[*]} " == *" 1 "* ]]; then return; fi say "Type numbers from the list, separated by spaces." done } # Confirms (or asks for) the path of app I into APP_PATH[I]; empty skips it. choose_path() { local i=$1 id=${APP_IDS[$1]} name=${APP_NAMES[$1]} path=${APP_DEFAULT[$1]} heading "$name" case $id in claude_code | opencode) if [ "$OS" != Darwin ] && ! has python3; then warn "Editing $name's JSON settings needs python3, which isn't installed." say "Install python3 and run this again, or follow $DOCS_URL" return fi ;; esac case $id in claude_desktop) if [ -n "$path" ]; then say "Found Claude Desktop at $(tidy "$path")." if confirm "Is this right?" y; then APP_PATH[i]=$path return fi else say "Claude Desktop wasn't found in Applications." fi while true; do ask_path "Path to Claude.app" [ -n "$ANSWER" ] || break ANSWER=${ANSWER%/} if is_desktop_app "$ANSWER"; then APP_PATH[i]=$ANSWER return fi warn "$(tidy "$ANSWER") isn't Claude Desktop." done ;; openclaw) if [ -n "$path" ]; then say "Found the openclaw command at $(tidy "$path")." if confirm "Is this right?" y; then APP_PATH[i]=$path return fi else say "The openclaw command wasn't found." fi while true; do ask_path "Path to the openclaw command" [ -n "$ANSWER" ] || break if [ -f "$ANSWER" ] && [ -x "$ANSWER" ]; then APP_PATH[i]=$ANSWER return fi warn "$(tidy "$ANSWER") isn't a command that can run." done ;; *) if [ "${APP_FOUND[$i]}" = 1 ]; then say "Found $name. Its config file is:" else say "$name wasn't found on this computer. Its config file would be:" fi say " $(tidy "$path")" if confirm "Is this the right file?" y; then APP_PATH[i]=$path return fi ask_path "Path to $name's config file" if [ -n "$ANSWER" ]; then if [ -d "$ANSWER" ]; then ANSWER="${ANSWER%/}/$(basename "$path")"; fi APP_PATH[i]=$ANSWER return fi ;; esac say "Skipping $name." } # The startup file of the user's login shell. default_env_file() { case ${SHELL##*/} in zsh) printf '%s' "${ZDOTDIR:-$HOME}/.zshrc" ;; bash) if [ "$OS" = Darwin ]; then printf '%s' "$HOME/.bash_profile"; else printf '%s' "$HOME/.bashrc"; fi ;; fish) printf '%s' "${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d/greentokens.fish" ;; *) printf '%s' "$HOME/.profile" ;; esac } # Confirms where GREENTOKENS_API_KEY is saved, into ENV_FILE (empty = don't). choose_env_file() { local names="" i for ((i = 0; i < ${#APP_IDS[@]}; i++)); do if [ -n "${APP_PATH[$i]}" ] && uses_key_var "${APP_IDS[$i]}"; then names="$names, ${APP_NAMES[$i]}"; fi done [ -n "$names" ] || return 0 ENV_FILE=$(default_env_file) heading "API key variable" say "${names#, } read your key from the $KEY_VAR variable. It will be saved in:" say " $(tidy "$ENV_FILE")" if confirm "Is this the right file?" y; then return; fi ask_path "Path to your shell's startup file" ENV_FILE=$ANSWER if [ -z "$ENV_FILE" ]; then warn "Not saving $KEY_VAR. Set it yourself, or those apps won't find your key." fi } apply_env_file() { local mode=$1 edit_file "$mode" "$ENV_FILE" edit_profile report_edit "$mode" "$ENV_FILE" "a shell file" || return 0 if [ "$mode" = set ]; then DONE+=("Open a new terminal window (or run \`source $(tidy "$ENV_FILE")\`) so apps see $KEY_VAR.") fi } # --- Main --------------------------------------------------------------------- main() { (: /dev/null || fail "Run this in a terminal: it asks you questions as it goes." OS=$(uname -s) case $OS in Darwin | Linux) ;; *) fail "This script is for macOS and Linux. On Windows, use https://www.greentokens.io/setup.ps1" ;; esac trap cleanup EXIT printf '\n%sGreenTokens setup%s\n' "$BOLD$GREEN" "$RESET" say "Connects your coding agents to GreenTokens." say "" say " 1) Connect apps to GreenTokens" say " 2) Remove GreenTokens settings" while true; do ask "Choose 1 or 2 [1]: " case ${ANSWER:-1} in 1) MODE=set && break ;; 2) MODE=remove && break ;; esac done register_app claude_code "Claude Code" if [ "$OS" = Darwin ]; then register_app claude_desktop "Claude Desktop"; fi register_app codex "Codex CLI" register_app opencode "OpenCode" register_app grok "Grok CLI" register_app openclaw "OpenClaw" register_app hermes "Hermes Agent" choose_apps local i count=${#APP_IDS[@]} any=0 for ((i = 0; i < count; i++)); do if [ "${APP_SELECTED[$i]}" = 1 ]; then choose_path "$i"; fi if [ -n "${APP_PATH[$i]}" ]; then any=1; fi done if [ "$any" = 0 ]; then say "" say "Nothing selected, so nothing was changed." exit 0 fi if [ "$MODE" = set ]; then choose_env_file read_key if [ -n "$ENV_FILE" ]; then heading "API key variable" apply_env_file set fi fi for ((i = 0; i < count; i++)); do if [ -n "${APP_PATH[$i]}" ]; then heading "${APP_NAMES[$i]}" "apply_${APP_IDS[$i]}" "$MODE" "${APP_PATH[$i]}" fi done if [ "$MODE" = remove ]; then ENV_FILE=$(default_env_file) if [ -f "$ENV_FILE" ] && grep -qxF "$BLOCK_START" "$ENV_FILE"; then heading "API key variable" say "$(tidy "$ENV_FILE") still sets $KEY_VAR. Apps you still use with GreenTokens need it." if confirm "Remove it?" y; then apply_env_file remove; fi fi fi heading "Done" if [ ${#DONE[@]} -eq 0 ]; then say "Nothing was changed." else local line for line in "${DONE[@]}"; do say "• $line"; done fi note "Help: $DOCS_URL" } main "$@"