#!/usr/bin/env bash # Diazolvera Jumpstart v0.55 — bundled for curl | bash # Source: https://gitlab.com/doprofessional/diazolvera-jumpstart # curl|bash bootstrap: save script to a file (never exec 0 "${_jumpstart_tmp}" export DIAZOLVERA_JUMPSTART_REEXEC=1 if [[ -e /dev/tty ]] 2>/dev/null; then exec bash "${_jumpstart_tmp}" "$@" /dev/null || exec bash "${_jumpstart_tmp}" "$@" else exec bash "${_jumpstart_tmp}" "$@" fi fi set -euo pipefail JUMPSTART_VERSION="0.55" if [[ -n "${BASH_SOURCE[0]:-}" ]]; then JUMPSTART_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" elif [[ -z "${JUMPSTART_DIR:-}" ]]; then JUMPSTART_DIR="${HOME}" fi DEBUG_LOG="${DEBUG_LOG:-${HOME}/.config/diazolvera/jumpstart-debug.log}" # Only write session log when explicitly set (Cursor agent). Skip on LiveOS/x220 curl|bash. SESSION_DEBUG_LOG="${DIAZOLVERA_SESSION_DEBUG_LOG:-}" OPENCODE_JUMPSTART_DIR="${OPENCODE_JUMPSTART_DIR:-${HOME}/opencode-jumpstart}" CURSOR_JUMPSTART_DIR="${CURSOR_JUMPSTART_DIR:-${HOME}/cursor-jumpstart}" # BITBUCKET_WORKSPACE="${BITBUCKET_WORKSPACE:-diazolvera}" # BITBUCKET_TOKEN_FILE="${BITBUCKET_TOKEN_FILE:-${HOME}/.config/diazolvera/atlassian-api-token}" # ATLASSIAN_ROVO_MCP_URL="${ATLASSIAN_ROVO_MCP_URL:-https://mcp.atlassian.com/v1/mcp/authv2}" GITLAB_HOST="${GITLAB_HOST:-gitlab.com}" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } log_error() { echo -e "${RED}[ERROR]${NC} $*"; } command_exists() { command -v "$1" >/dev/null 2>&1; } GLAB_CONFIG="${HOME}/.config/glab-cli/config.yml" CURSOR_AUTH_JSON="${HOME}/.config/cursor/auth.json" # Fast local checks — avoid dnf, network, and slow CLI status on re-runs. glab_configured_local() { [[ -f "${GLAB_CONFIG}" ]] || return 1 grep -A40 " ${GITLAB_HOST}:" "${GLAB_CONFIG}" 2>/dev/null \ | grep -qE 'token:.*[a-f0-9]{8,}|oauth2_refresh_token:' } glab_user_local() { grep -A40 " ${GITLAB_HOST}:" "${GLAB_CONFIG}" 2>/dev/null \ | grep -E '^[[:space:]]+user:' | head -1 \ | sed -E 's/^[[:space:]]+user: (!!null )?//' || true } cursor_configured_local() { [[ -f "${CURSOR_AUTH_JSON}" ]] || return 1 command_exists jq \ && jq -e '(.accessToken // "") | length > 20' "${CURSOR_AUTH_JSON}" >/dev/null 2>&1 } # bitbucket_token_file_present() { # [[ -n "${BITBUCKET_API_TOKEN:-}" ]] && return 0 # [[ -f "${BITBUCKET_TOKEN_FILE}" ]] && [[ -s "${BITBUCKET_TOKEN_FILE}" ]] # } jumpstart_preflight() { local t0 t1 glab_ok cursor_ok t0="$(date +%s%N 2>/dev/null || echo 0)" glab_ok=false cursor_ok=false glab_configured_local && command_exists glab && glab_ok=true cursor_configured_local && command_exists agent && cursor_ok=true t1="$(date +%s%N 2>/dev/null || echo 0)" # "{\"ms\":$(( (t1 - t0) / 1000000 )),\"glab\":${glab_ok},\"cursor\":${cursor_ok}}" "H16" echo "Ready: GitLab=$([[ ${glab_ok} == true ]] && echo yes || echo no) Cursor=$([[ ${cursor_ok} == true ]] && echo yes || echo no)" } ensure_path() { export PATH="${HOME}/.local/bin:${HOME}/bin:${PATH}" } # curl|bash bundle runs from /tmp — JUMPSTART_DIR is often wrong (/). Always resolve a real BROWSER helper. resolve_open_firefox_browser() { local c tmp for c in \ "${DIAZOLVERA_BROWSER:-}" \ "${JUMPSTART_DIR:-}/scripts/open-firefox-tab.sh" \ "${HOME}/diazolvera-jumpstart/scripts/open-firefox-tab.sh"; do [[ -n "${c}" && -x "${c}" ]] && { printf '%s' "${c}" return 0 } done tmp="$(mktemp /tmp/diazolvera-open-firefox.XXXXXX.sh 2>/dev/null || mktemp "${HOME}/.cache/diazolvera-open-firefox.XXXXXX.sh")" cat > "${tmp}" <<'BROWSER_EOF' #!/usr/bin/env bash url="${1:-}" [[ -z "${url}" ]] && exit 1 exec firefox -new-tab "${url}" BROWSER_EOF chmod +x "${tmp}" printf '%s' "${tmp}" } # Read interactive input from /dev/tty when stdin is a pipe (curl | bash). has_interactive_tty() { [[ -t 0 ]] || [[ -e /dev/tty ]] } read_tty_line() { local prompt="$1" local var_name="$2" local value="" if [[ -e /dev/tty ]] && printf '%s' "${prompt}" >/dev/tty 2>/dev/null; then IFS= read -r value /dev/tty 2>/dev/null; then IFS= read -rs value /dev/tty elif [[ -t 0 ]]; then IFS= read -rsp "${prompt}" value echo else log_error "Cannot read input — no TTY." return 1 fi printf -v "${var_name}" '%s' "${value}" } jumpstart_debug() { [[ -n "${DIAZOLVERA_SESSION_DEBUG_LOG:-}" ]] || return 0 local message="$1" data="$2" hypothesis_id="${3:-H0}" ts line ts="$(($(date +%s) * 1000))" line="$(printf '{"sessionId":"jumpstart","timestamp":%s,"location":"jumpstart","message":"%s","data":%s,"runId":"v%s","hypothesisId":"%s"} ' \ "${ts}" "${message}" "${data}" "${JUMPSTART_VERSION}" "${hypothesis_id}")" mkdir -p "$(dirname "${DIAZOLVERA_SESSION_DEBUG_LOG}")" 2>/dev/null || return 0 printf '%s' "${line}" >> "${DIAZOLVERA_SESSION_DEBUG_LOG}" 2>/dev/null || true } require_tty() { if ! has_interactive_tty; then log_error "Interactive TTY required for OAuth/token prompts." jumpstart_debug "tty missing" '{"ok":false}' "H1" return 1 fi jumpstart_debug "tty attached" "{\"ok\":true,\"stdin_tty\":$( [[ -t 0 ]] && echo true || echo false ),\"dev_tty\":$( [[ -e /dev/tty ]] && echo true || echo false )}" "H1" } oauth_with_qr() { local URL_REGEX="$1" shift [[ "${1:-}" == "--" ]] && shift local LOG_FILE PID URL EC LOG_FILE="$(mktemp)" trap '[[ -n "${LOG_FILE:-}" ]] && rm -f "${LOG_FILE}"' RETURN if [[ -e /dev/tty ]]; then "$@" &1 | tee "${LOG_FILE}" & else "$@" 2>&1 | tee "${LOG_FILE}" & fi PID=$! URL="" for _ in $(seq 1 120); do URL="$(grep -oE "${URL_REGEX}" "${LOG_FILE}" 2>/dev/null | head -1 || true)" [[ -n "${URL}" ]] && break kill -0 "${PID}" 2>/dev/null || break sleep 0.5 done if [[ -n "${URL}" ]]; then echo ">>> Scan on phone — complete auth before this session exits:" command -v qrencode >/dev/null 2>&1 && qrencode -t ANSIUTF8 "${URL}" || echo ">>> URL: ${URL}" fi wait "${PID}" && return 0 EC=$? return "${EC}" } # --- deps.sh --- jumpstart_deps() { ensure_path if command_exists glab && command_exists agent && command_exists qrencode; then jumpstart_debug "deps skip" '{"already":true}' "H4" # "deps skip" '{"already":true}' "H16" log_ok "Dependencies ready (glab, agent, qrencode)" return 0 fi log_info "Checking dependencies..." if ! command_exists glab; then log_info "Installing glab (GitLab CLI)..." sudo dnf install -y glab fi glab config set -g git_protocol https 2>/dev/null || true git config --global credential.helper '!glab auth git-credential' 2>/dev/null || true if ! command_exists agent; then log_info "Installing Cursor CLI..." curl -fsSL https://cursor.com/install | bash ensure_path fi if ! command_exists qrencode; then log_info "Installing qrencode (OAuth QR display)..." sudo dnf install -y qrencode fi jumpstart_debug "deps ok" '{"ok":true}' "H4" log_ok "Dependencies ready (glab, agent, qrencode)" } # --- gitlab-auth.sh --- gitlab_already_authed() { if glab_configured_local && command_exists glab; then return 0 fi glab auth status --hostname "${GITLAB_HOST}" >/dev/null 2>&1 } jumpstart_gitlab() { ensure_path if gitlab_already_authed; then jumpstart_debug "gitlab skip" '{"already":true,"method":"local"}' "H5" # "gitlab skip" '{"already":true}' "H16" local user user="$(glab_user_local)" log_ok "GitLab already authenticated${user:+ (as ${user})}" return 0 fi require_tty || return 1 log_info "GitLab auth (${GITLAB_HOST}) — glab device OAuth (scan QR on phone)..." echo ">>> You will see a one-time code in the terminal — enter it on your phone after scanning the QR." if oauth_with_qr 'https://gitlab\.com/oauth/device[^[:space:]]*' -- glab auth login --hostname "${GITLAB_HOST}" --device; then jumpstart_debug "gitlab oauth finished" '{"cmd_exit":0}' "H5" else jumpstart_debug "gitlab oauth finished" '{"cmd_exit":1}' "H5" log_error "GitLab auth failed" return 1 fi if gitlab_already_authed; then jumpstart_debug "gitlab ok" '{"ok":true}' "H5" log_ok "GitLab authenticated" return 0 fi jumpstart_debug "gitlab ok" '{"ok":false}' "H5" log_error "GitLab auth status check failed after login" return 1 } # --- cursor-auth.sh --- cursor_already_authed() { if cursor_configured_local; then return 0 fi ensure_path command_exists agent && agent status >/dev/null 2>&1 } jumpstart_cursor() { ensure_path if ! command_exists agent; then log_error "Cursor CLI (agent) not installed" return 1 fi jumpstart_debug "cursor installed" '{"ok":true}' "H6" if cursor_already_authed; then jumpstart_debug "cursor skip" '{"already":true,"method":"local"}' "H7" # "cursor skip" '{"already":true}' "H16" log_ok "Cursor already authenticated" return 0 fi require_tty || return 1 log_info "Cursor auth — scan QR on phone (browser OAuth)..." if oauth_with_qr 'https://cursor.com/loginDeepControl[^[:space:]]+' -- agent login; then jumpstart_debug "cursor oauth finished" '{"cmd_exit":0}' "H7" else jumpstart_debug "cursor oauth finished" '{"cmd_exit":1}' "H7" log_error "Cursor auth failed" return 1 fi if cursor_already_authed; then jumpstart_debug "cursor ok" '{"ok":true}' "H7" log_ok "Cursor authenticated" return 0 fi jumpstart_debug "cursor ok" '{"ok":false}' "H7" log_warn "agent login finished but agent status check inconclusive" return 0 } # --- mcp-setup.sh --- CURSOR_JUMPSTART_REPO="${CURSOR_JUMPSTART_REPO:-doprofessional/cursor-jumpstart}" CURSOR_JUMPSTART_DIR="${CURSOR_JUMPSTART_DIR:-${HOME}/cursor-jumpstart}" ZOHO_MCP_SCRIPT="${CURSOR_JUMPSTART_DIR}/lib/mcp/zoho.sh" ZOHO_NEED_LOGIN=() ZOHO_MCP_READY_COUNT=0 ZOHO_MCP_TOTAL_COUNT=0 MCP_CONFIG_TXN_ROLLBACK_PATH="" MCP_CONFIG_TXN_HAD_FILE=false MCP_AGENT_LIST="" MCP_XDG_BUNDLED_PATH="" MCP_XDG_ORIG_MODE="" MCP_XDG_WRAPPER_DIR="" MCP_XDG_PATCHED=false MCP_XDG_NEUTERED=false MCP_OAUTH_BATCH=false MCP_HEAL_FAIL_REASON="" MCP_LAST_AGENT_LOG="" _mcp_debug_log() { local location="$1" hypothesis_id="$2" message="$3" data="${4:-{}}" local log_file="${HOME}/.cursor/debug-646f63.log" mkdir -p "$(dirname "${log_file}")" 2>/dev/null || true printf '%s\n' \ "{\"sessionId\":\"646f63\",\"timestamp\":$(($(date +%s)*1000)),\"location\":\"mcp-setup.sh:${location}\",\"hypothesisId\":\"${hypothesis_id}\",\"message\":\"${message}\",\"data\":${data}}" \ >> "${log_file}" 2>/dev/null || true } _mcp_config_snapshot_json() { local config="${HOME}/.cursor/mcp.json" local valid=false size=0 servers=0 [[ -f "${config}" ]] && size="$(stat -c '%s' "${config}" 2>/dev/null || echo 0)" validate_mcp_json_config && valid=true command_exists jq && servers="$(jq '(.mcpServers // {}) | keys | length' "${config}" 2>/dev/null || echo 0)" printf '{"valid":%s,"size":%s,"servers":%s}' "${valid}" "${size}" "${servers}" } validate_mcp_json_config() { local config="${HOME}/.cursor/mcp.json" [[ -f "${config}" ]] || return 0 jq empty "${config}" >/dev/null 2>&1 \ && jq -e '.mcpServers | type == "object"' "${config}" >/dev/null 2>&1 } begin_mcp_config_txn() { local config="${HOME}/.cursor/mcp.json" MCP_CONFIG_TXN_HAD_FILE=false MCP_CONFIG_TXN_ROLLBACK_PATH="" mkdir -p "$(dirname "${config}")" if [[ -f "${config}" ]]; then MCP_CONFIG_TXN_HAD_FILE=true if ! validate_mcp_json_config; then log_warn "Skipping txn snapshot — current mcp.json is invalid (older backups still available)" return 0 fi MCP_CONFIG_TXN_ROLLBACK_PATH="$(mktemp "${config}.rollback.XXXXXX")" || return 1 cp -f "${config}" "${MCP_CONFIG_TXN_ROLLBACK_PATH}" fi } rollback_mcp_config_txn() { local config="${HOME}/.cursor/mcp.json" if [[ "${MCP_CONFIG_TXN_HAD_FILE}" == true && -n "${MCP_CONFIG_TXN_ROLLBACK_PATH}" && -f "${MCP_CONFIG_TXN_ROLLBACK_PATH}" ]]; then cp -f "${MCP_CONFIG_TXN_ROLLBACK_PATH}" "${config}" log_warn "Reverted ~/.cursor/mcp.json to pre-MCP-phase snapshot" elif [[ "${MCP_CONFIG_TXN_HAD_FILE}" == false ]]; then rm -f "${config}" fi rm -f "${MCP_CONFIG_TXN_ROLLBACK_PATH:-}" MCP_CONFIG_TXN_ROLLBACK_PATH="" MCP_CONFIG_TXN_HAD_FILE=false } commit_mcp_config_txn() { # #region agent log _mcp_debug_log "commit_mcp_config_txn:pre" "H1" "before commit" "$(_mcp_config_snapshot_json)" # #endregion validate_mcp_json_config || { log_error "MCP config txn commit failed — invalid JSON" rollback_mcp_config_txn return 1 } rm -f "${MCP_CONFIG_TXN_ROLLBACK_PATH:-}" MCP_CONFIG_TXN_ROLLBACK_PATH="" MCP_CONFIG_TXN_HAD_FILE=false # #region agent log _mcp_debug_log "commit_mcp_config_txn:ok" "H1" "committed" "$(_mcp_config_snapshot_json)" # #endregion } _mcp_config_txn_rollback_on_err() { rollback_mcp_config_txn trap - ERR } _mcp_config_install_file_unlocked() { local tmp="$1" config="$2" pre_backup="" [[ -s "${tmp}" ]] || { rm -f "${tmp}"; log_error "Refusing empty MCP config write"; return 1; } jq empty "${tmp}" >/dev/null 2>&1 || { rm -f "${tmp}"; log_error "Refusing invalid MCP config write (bad JSON)"; return 1; } jq -e '.mcpServers | type == "object"' "${tmp}" >/dev/null 2>&1 || { rm -f "${tmp}" log_error "Refusing invalid MCP config write (missing mcpServers)" return 1 } if [[ -f "${config}" ]] && cmp -s "${tmp}" "${config}" 2>/dev/null; then rm -f "${tmp}" return 0 fi if [[ -f "${config}" ]]; then pre_backup="${config}.pre-write.$(date +%Y%m%d-%H%M%S).json" cp -f "${config}" "${pre_backup}" 2>/dev/null || true fi mv -f "${tmp}" "${config}" if ! validate_mcp_json_config; then if [[ -n "${pre_backup}" && -f "${pre_backup}" ]]; then cp -f "${pre_backup}" "${config}" elif [[ -n "${MCP_CONFIG_TXN_ROLLBACK_PATH}" && -f "${MCP_CONFIG_TXN_ROLLBACK_PATH}" ]]; then cp -f "${MCP_CONFIG_TXN_ROLLBACK_PATH}" "${config}" fi log_error "MCP config write failed validation — restored previous file" return 1 fi } _mcp_config_install_file() { _mcp_config_install_file_unlocked "$@" || return 1 } _mcp_valid_backup_file() { local candidate="$1" [[ -f "${candidate}" ]] || return 1 jq empty "${candidate}" >/dev/null 2>&1 \ && jq -e '.mcpServers | type == "object"' "${candidate}" >/dev/null 2>&1 } _mcp_try_restore_candidate() { local config="$1" candidate="$2" [[ -f "${candidate}" ]] || return 1 if [[ -f "${config}" ]] && cmp -s "${candidate}" "${config}" 2>/dev/null; then return 1 fi if _mcp_valid_backup_file "${candidate}"; then cp -f "${candidate}" "${config}" return 0 fi return 1 } restore_mcp_json_from_known_backups() { local config="${HOME}/.cursor/mcp.json" candidate if _mcp_try_restore_candidate "${config}" "${config}.bak"; then log_info "Restored ~/.cursor/mcp.json from ${config}.bak" return 0 fi for candidate in "${config}.pre-write."*.json; do [[ -e "${candidate}" ]] || continue if _mcp_try_restore_candidate "${config}" "${candidate}"; then log_info "Restored ~/.cursor/mcp.json from ${candidate}" return 0 fi done for candidate in "${config}.rollback."*; do [[ -e "${candidate}" ]] || continue [[ -n "${MCP_CONFIG_TXN_ROLLBACK_PATH}" && "${candidate}" == "${MCP_CONFIG_TXN_ROLLBACK_PATH}" ]] && continue if _mcp_try_restore_candidate "${config}" "${candidate}"; then log_info "Restored ~/.cursor/mcp.json from ${candidate}" return 0 fi done if [[ -n "${MCP_CONFIG_TXN_ROLLBACK_PATH}" && -f "${MCP_CONFIG_TXN_ROLLBACK_PATH}" ]]; then if _mcp_try_restore_candidate "${config}" "${MCP_CONFIG_TXN_ROLLBACK_PATH}"; then log_info "Restored ~/.cursor/mcp.json from txn rollback" return 0 fi fi log_warn "No valid mcp.json backup found (.bak, .pre-write.*, .rollback.*)" return 1 } detect_mcp_json_state() { local config="${HOME}/.cursor/mcp.json" if [[ ! -f "${config}" ]]; then printf 'missing' return 0 fi if validate_mcp_json_config; then printf 'valid' else printf 'corrupt' fi } list_mcp_config_backups() { local config="${HOME}/.cursor/mcp.json" candidate _mcp_valid_backup_file "${config}.bak" && printf '%s\n' "${config}.bak" for candidate in "${config}.pre-write."*.json; do [[ -e "${candidate}" ]] || continue _mcp_valid_backup_file "${candidate}" && printf '%s\n' "${candidate}" done for candidate in "${config}.rollback."*; do [[ -e "${candidate}" ]] || continue _mcp_valid_backup_file "${candidate}" && printf '%s\n' "${candidate}" done } _mcp_write_empty_config_unlocked() { local config="${HOME}/.cursor/mcp.json" mkdir -p "$(dirname "${config}")" printf '{\n "mcpServers": {}\n}\n' > "${config}" validate_mcp_json_config } _mcp_write_empty_config() { _mcp_write_empty_config_unlocked } prompt_mcp_config_recovery() { local state="${1:-$(detect_mcp_json_state)}" local choice backup_count=0 best_backup="" line # #region agent log _mcp_debug_log "prompt_mcp_config_recovery:entry" "H3" "recovery prompt" \ "{\"state\":\"${state}\",\"backupCountPreview\":$(list_mcp_config_backups | wc -l)}" # #endregion [[ "${state}" == "valid" ]] && return 0 case "${state}" in missing) log_warn "MCP config: missing (~/.cursor/mcp.json not found)" ;; corrupt) log_warn "MCP config: corrupt (invalid JSON or bad schema)" ;; *) log_warn "MCP config: ${state}" ;; esac while IFS= read -r line; do [[ -z "${line}" ]] && continue backup_count=$((backup_count + 1)) [[ -z "${best_backup}" ]] && best_backup="${line}" done < <(list_mcp_config_backups) if ((backup_count > 0)); then log_info "Backups found: ${backup_count} (best: ${best_backup})" else log_info "No valid backups found (.bak, .pre-write.*, .rollback.*)" fi echo "Fix ~/.cursor/mcp.json?" echo " Enter = restore best backup" echo " e = reset to empty (re-enter Zoho URLs)" echo " r = retry auto-heal" echo " n = abort jumpstart" read_tty_line "Choice: " choice # #region agent log _mcp_debug_log "prompt_mcp_config_recovery:choice" "H3" "user choice" \ "{\"choice\":\"${choice}\",\"backupCount\":${backup_count}}" # #endregion case "${choice}" in n|N) log_warn "MCP config recovery aborted" return 1 ;; e|E) if _mcp_write_empty_config; then log_ok "Reset ~/.cursor/mcp.json to empty schema — Zoho URL entry will run if needed" # #region agent log _mcp_debug_log "prompt_mcp_config_recovery:empty_ok" "H5" "empty reset ok" "$(_mcp_config_snapshot_json)" # #endregion return 0 fi log_error "Failed to write empty ~/.cursor/mcp.json" return 1 ;; r|R) ensure_cursor_mcp_json_healthy && return 0 return 1 ;; *) if ((backup_count == 0)); then log_error "No backup to restore — use e=empty reset or r=retry" return 1 fi if restore_mcp_json_from_known_backups; then ensure_cursor_mcp_json_healthy && { log_ok "Restored ~/.cursor/mcp.json from backup" return 0 } fi log_error "Backup restore failed" return 1 ;; esac } _mcp_bundled_xdg_open_path() { local p for p in \ "${HOME}/.npm-global/lib/node_modules/mcp-remote/node_modules/open/xdg-open" \ "$(npm root -g 2>/dev/null)/mcp-remote/node_modules/open/xdg-open"; do [[ -f "${p}" ]] && { printf '%s' "${p}" return 0 } done return 1 } # Disable mcp-remote npm "open" → xdg-open (log watcher opens one Firefox tab per MCP). _mcp_oauth_redirect_browser_open() { local xdg [[ -n "${MCP_XDG_WRAPPER_DIR}" && -d "${MCP_XDG_WRAPPER_DIR}" ]] && return 0 xdg="$(_mcp_bundled_xdg_open_path)" || return 1 MCP_XDG_BUNDLED_PATH="${xdg}" MCP_XDG_ORIG_MODE="$(stat -c '%a' "${xdg}" 2>/dev/null || stat -f '%OLp' "${xdg}" 2>/dev/null || echo 755)" MCP_XDG_PATCHED=false MCP_XDG_NEUTERED=false cp -f "${xdg}" "${xdg}.jumpstart.bak" 2>/dev/null || return 1 printf '#!/usr/bin/env bash\n# jumpstart: browser open via log watcher only\nexit 0\n' > "${xdg}" chmod +x "${xdg}" MCP_XDG_PATCHED=true MCP_XDG_NEUTERED=true MCP_XDG_WRAPPER_DIR="$(mktemp -d "${TMPDIR:-/tmp}/jumpstart-xdg-wrap.XXXXXX")" || return 1 printf '#!/usr/bin/env bash\nexit 0\n' > "${MCP_XDG_WRAPPER_DIR}/xdg-open" chmod +x "${MCP_XDG_WRAPPER_DIR}/xdg-open" export PATH="${MCP_XDG_WRAPPER_DIR}:${PATH}" } _mcp_oauth_restore_browser_open() { local xdg="${MCP_XDG_BUNDLED_PATH}" [[ -n "${MCP_XDG_WRAPPER_DIR}" && -d "${MCP_XDG_WRAPPER_DIR}" ]] && rm -rf "${MCP_XDG_WRAPPER_DIR}" MCP_XDG_WRAPPER_DIR="" if [[ -n "${xdg}" && -f "${xdg}" ]]; then if [[ "${MCP_XDG_PATCHED}" == true && -f "${xdg}.jumpstart.bak" ]]; then mv -f "${xdg}.jumpstart.bak" "${xdg}" elif [[ -n "${MCP_XDG_ORIG_MODE}" ]]; then chmod "${MCP_XDG_ORIG_MODE}" "${xdg}" 2>/dev/null || chmod +x "${xdg}" 2>/dev/null || true fi fi MCP_XDG_BUNDLED_PATH="" MCP_XDG_ORIG_MODE="" MCP_XDG_PATCHED=false MCP_XDG_NEUTERED=false } refresh_mcp_agent_list() { MCP_AGENT_LIST="" command_exists agent && MCP_AGENT_LIST="$(agent mcp list 2>/dev/null || true)" } ensure_mcp_deps() { if command_exists jq && command_exists npx && command_exists mcp-remote; then return 0 fi if ! command_exists jq; then log_info "Installing jq..." sudo dnf install -y jq fi if ! command_exists npx; then log_info "Installing nodejs/npm..." sudo dnf install -y nodejs npm fi mkdir -p "${HOME}/.npm-global/lib/node_modules" npm config set prefix "${HOME}/.npm-global" 2>/dev/null || true ensure_path export PATH="${HOME}/.npm-global/bin:${PATH}" if ! command_exists mcp-remote; then log_info "Installing mcp-remote..." npm install -g mcp-remote ensure_path export PATH="${HOME}/.npm-global/bin:${PATH}" fi } ensure_cursor_jumpstart() { if [[ -d "${CURSOR_JUMPSTART_DIR}/.git" && -f "${CURSOR_JUMPSTART_DIR}/lib/mcp/zoho.sh" ]]; then ZOHO_MCP_SCRIPT="${CURSOR_JUMPSTART_DIR}/lib/mcp/zoho.sh" [[ "${JUMPSTART_GIT_PULL:-}" == "1" ]] && git -C "${CURSOR_JUMPSTART_DIR}" pull --ff-only 2>/dev/null || true return 0 fi if ! command_exists glab || ! glab_configured_local; then if ! command_exists glab || ! glab auth status --hostname "${GITLAB_HOST}" >/dev/null 2>&1; then log_error "GitLab auth required to clone ${CURSOR_JUMPSTART_REPO}." return 1 fi fi local clone_dir="${CURSOR_JUMPSTART_DIR}" if ! mkdir -p "$(dirname "${clone_dir}")" 2>/dev/null; then clone_dir="/tmp/cursor-jumpstart" CURSOR_JUMPSTART_DIR="${clone_dir}" ZOHO_MCP_SCRIPT="${clone_dir}/lib/mcp/zoho.sh" fi log_info "Cloning ${CURSOR_JUMPSTART_REPO}..." glab repo clone "${CURSOR_JUMPSTART_REPO}" "${clone_dir}" } list_zoho_mcp_entries_tsv() { local config="${HOME}/.cursor/mcp.json" [[ -f "${config}" ]] || return 0 jq -r ' def is_zoho: (.value.args // []) | map(tostring) | join(" ") | test("zohomcp\\.com"); def zoho_url: (.value.args // []) | map(select(test("zohomcp\\.com"))) | .[0] // ""; (.mcpServers // {}) | to_entries | map(select(is_zoho)) | group_by(zoho_url) | map(sort_by(.key | test("^zoho-")) | .[0]) | .[] | "\(.key)\t\(.value.args | map(select(test("zohomcp\\.com"))) | .[0])" ' "${config}" 2>/dev/null || true } probe_zoho_mcp_http() { local url="$1" curl -sS -o /dev/null -w "%{http_code}" --max-time "${MCP_PROBE_TIMEOUT:-1}" \ -X POST -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"diazolvera-jumpstart","version":"'"${JUMPSTART_VERSION}"'"}}}' \ "${url}" 2>/dev/null || echo "000" } mcp_probe_label() { local code="$1" case "${code}" in 200) echo "connected" ;; 401) echo "needs_oauth" ;; 400) echo "bad_url" ;; 000) echo "timeout" ;; *) echo "http_${code}" ;; esac } agent_mcp_status_for_name() { local name="$1" agent_list="$2" echo "${agent_list}" | grep -E "^${name}:" | head -1 | sed -E 's/^[^:]+:[[:space:]]*//' } mcp_auth_hash_for_url() { echo -n "$1" | md5sum | awk '{print $1}' } mcp_auth_tokens_present_for_url() { local url="$1" hash auth_dir f hash="$(mcp_auth_hash_for_url "${url}")" for auth_dir in "${HOME}"/.mcp-auth/mcp-remote-*; do f="${auth_dir}/${hash}_tokens.json" [[ -f "${f}" ]] || continue command_exists jq && jq -e '(.access_token // "") | length > 10' "${f}" >/dev/null 2>&1 && return 0 grep -q 'access_token' "${f}" 2>/dev/null && return 0 done return 1 } zoho_mcp_url_for_name() { jq -r --arg n "$1" ' (.mcpServers[$n].args // []) | map(select(test("zohomcp\\.com"))) | .[0] // "" ' "${HOME}/.cursor/mcp.json" 2>/dev/null || true } # Jumpstart auth batch: every configured Zoho MCP (do not skip from stale tokens/ready). build_zoho_jumpstart_auth_list() { local line ZOHO_NEED_LOGIN=() refresh_mcp_agent_list while IFS= read -r line; do [[ -z "${line}" ]] && continue ZOHO_NEED_LOGIN+=("${line%%$'\t'*}") done < <(list_zoho_mcp_entries_tsv) } # Classify Zoho MCPs that still need OAuth and/or agent registration. build_zoho_need_action_lists() { local -a lines=() local line name url status ZOHO_NEED_LOGIN=() refresh_mcp_agent_list mapfile -t lines < <(list_zoho_mcp_entries_tsv) for line in "${lines[@]}"; do [[ -z "${line}" ]] && continue name="${line%%$'\t'*}" url="${line#*$'\t'}" status="$(agent_mcp_status_for_name "${name}" "${MCP_AGENT_LIST}")" if [[ "${status}" == "ready" ]] && mcp_auth_tokens_present_for_url "${url}"; then continue fi ZOHO_NEED_LOGIN+=("${name}") done } build_zoho_need_oauth_list() { build_zoho_need_action_lists } report_zoho_mcps_final() { local -a lines=() local line name url status ok=0 need=0 fail=0 [[ -f "${HOME}/.cursor/mcp.json" ]] || return 0 mapfile -t lines < <(list_zoho_mcp_entries_tsv) ((${#lines[@]} == 0)) && return 0 echo echo "=== Zoho MCP status ===" refresh_mcp_agent_list for line in "${lines[@]}"; do [[ -z "${line}" ]] && continue name="${line%%$'\t'*}" url="${line#*$'\t'}" status="$(agent_mcp_status_for_name "${name}" "${MCP_AGENT_LIST}")" if [[ "${status}" == "ready" ]]; then printf " %-28s working ✓\n" "${name}" ok=$((ok + 1)) elif mcp_auth_tokens_present_for_url "${url}"; then printf " %-28s tokens only (agent mcp login %s)\n" "${name}" "${name}" need=$((need + 1)) else printf " %-28s needs OAuth\n" "${name}" need=$((need + 1)) fi done echo ZOHO_MCP_READY_COUNT=${ok} ZOHO_MCP_TOTAL_COUNT=${#lines[@]} if ((${ok} == ${#lines[@]})); then log_ok "All ${ok} Zoho MCPs working" else echo "Summary: ${ok} working, ${need} need attention, ${fail} bad URL" fi } cleanup_stale_mcp_remote() { pkill -f 'mcp-remote.*zohomcp\.com' 2>/dev/null || true pkill -f 'node.*mcp-remote.*zohomcp\.com' 2>/dev/null || true sleep 1 } _zoho_oauth_between_mcp_servers() { local _i cleanup_stale_mcp_remote for _i in $(seq 1 24); do pgrep -f 'mcp-remote.*zohomcp' >/dev/null 2>&1 || break sleep 0.25 done sleep 0.5 } clear_mcp_auth_for_url() { local url="$1" hash auth_dir f hash="$(mcp_auth_hash_for_url "${url}")" for auth_dir in "${HOME}"/.mcp-auth/mcp-remote-*; do [[ -d "${auth_dir}" ]] || continue for f in "${auth_dir}/${hash}_"*; do [[ -e "${f}" ]] && rm -f "${f}" done done } mcp_oauth_succeeded_for_url() { local url="$1" log_file="${2:-}" mcp_auth_tokens_present_for_url "${url}" && return 0 [[ -n "${log_file}" && -f "${log_file}" ]] \ && grep -qE 'Auth code received|Completing authorization|Authentication completed' "${log_file}" 2>/dev/null \ && grep -q 'Press Ctrl+C to exit' "${log_file}" 2>/dev/null } extract_zoho_oauth_authorize_url() { local log_file="$1" grep -oE 'https://mcp\.zoho\.com/mcp-client/[^[:space:]<>"]+' "${log_file}" 2>/dev/null | head -1 } # mcp-remote uses npm open → xdg-open (neutered). Watch log and open Firefox once per MCP. watch_and_open_oauth_url() { local log_file="$1" browser="$2" opened_flag="$3" local _url _i for _i in $(seq 1 240); do [[ -f "${log_file}" ]] || { sleep 0.25; continue; } _url="$(extract_zoho_oauth_authorize_url "${log_file}")" if [[ -n "${_url}" && ! -f "${opened_flag}" ]]; then touch "${opened_flag}" log_info "Opening Firefox tab for Zoho sign-in (mcp.zoho.com)..." "${browser}" "${_url}" 2>/dev/null || firefox -new-tab "${_url}" 2>/dev/null || true return 0 fi sleep 0.5 done } _zoho_oauth_kill_children() { local watcher_pid="${1:-}" tail_pid="${2:-}" mcp_pid="${3:-}" [[ -n "${watcher_pid}" ]] && kill "${watcher_pid}" 2>/dev/null || true [[ -n "${tail_pid}" ]] && kill "${tail_pid}" 2>/dev/null || true _zoho_oauth_graceful_kill "${mcp_pid}" } warn_disk_before_oauth() { local use_pct avail use_pct="$(df / 2>/dev/null | awk 'NR==2{gsub(/%/,"",$5); print $5}' || echo 0)" avail="$(df -h / 2>/dev/null | awk 'NR==2{print $4}' || echo unknown)" if [[ "${use_pct}" -ge 95 ]] 2>/dev/null; then log_warn "/ is ${use_pct}% full (${avail} free) — OAuth tokens may not save to ~/.mcp-auth" fi } mcp_auth_tokens_stable_for_url() { local url="$1" mcp_auth_tokens_present_for_url "${url}" || return 1 sleep 0.25 mcp_auth_tokens_present_for_url "${url}" } _zoho_oauth_graceful_kill() { local pid="$1" _ [[ -n "${pid}" ]] && kill -TERM "${pid}" 2>/dev/null || true for _ in $(seq 1 12); do [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null || break sleep 0.25 done [[ -n "${pid}" ]] && kill -KILL "${pid}" 2>/dev/null || true cleanup_stale_mcp_remote } _zoho_oauth_kill_mcp_remote() { _zoho_oauth_graceful_kill "${1:-}" } _show_agent_register_log_tail() { local log_file="$1" lines=15 [[ -f "${log_file}" && -s "${log_file}" ]] || return 0 if grep -qE '32000|connection closed|Connection closed' "${log_file}" 2>/dev/null; then lines=15 fi tail -"${lines}" "${log_file}" 2>/dev/null || true } _mcp_save_pre_login_snapshot() { local snap="$1" local config="${HOME}/.cursor/mcp.json" validate_mcp_json_config || return 1 cp -f "${config}" "${snap}" } _mcp_guard_config_after_agent_login() { local name="$1" snap="$2" local config="${HOME}/.cursor/mcp.json" validate_mcp_json_config && return 0 log_warn "agent mcp login corrupted ~/.cursor/mcp.json for ${name} — restoring snapshot" # #region agent log _mcp_debug_log "_mcp_guard_config_after_agent_login" "H5" "post-login corrupt" \ "{\"name\":\"${name}\",\"hasSnap\":$([[ -n \"${snap}\" && -f \"${snap}\" ]] && echo true || echo false)}" # #endregion if [[ -n "${snap}" && -f "${snap}" ]] && _mcp_valid_backup_file "${snap}"; then cp -f "${snap}" "${config}" elif restore_mcp_json_from_known_backups; then : else log_error "Could not restore ~/.cursor/mcp.json after agent login for ${name}" return 1 fi validate_mcp_json_config || { log_error "~/.cursor/mcp.json still invalid after restore for ${name}" return 1 } return 0 } _run_zoho_mcp_oauth() { local name="$1" url mcp_remote browser log_file oauth_url opened_flag local watcher_pid tail_pid mcp_pid poll_i auth_done=false ec=0 probe_after url="$(zoho_mcp_url_for_name "${name}")" mcp_remote="$(command -v mcp-remote 2>/dev/null || echo mcp-remote)" browser="$(resolve_open_firefox_browser)" [[ -z "${url}" ]] && { log_error "No URL for ${name}"; return 1; } # Snappy re-run outside batch: skip browser when tokens already work end-to-end. if [[ "${MCP_OAUTH_BATCH}" != true ]] \ && mcp_auth_tokens_present_for_url "${url}" \ && [[ "$(mcp_probe_label "$(probe_zoho_mcp_http "${url}")")" == "connected" ]]; then log_ok "${name} already has working OAuth tokens — skipping browser" return 0 fi cleanup_stale_mcp_remote clear_mcp_auth_for_url "${url}" warn_disk_before_oauth log_info "${name} — opening browser (one tab)..." echo ">>> Sign in, click Allow, then jumpstart continues automatically." log_file="$(mktemp /tmp/zoho-oauth.XXXXXX.log 2>/dev/null || mktemp "${HOME}/.cache/zoho-oauth.XXXXXX.log")" opened_flag="${log_file}.opened" rm -f "${opened_flag}" : > "${log_file}" watch_and_open_oauth_url "${log_file}" "${browser}" "${opened_flag}" & watcher_pid=$! tail -f "${log_file}" 2>/dev/null & tail_pid=$! trap '_zoho_oauth_kill_children "${watcher_pid}" "${tail_pid}" "${mcp_pid}"; trap - INT; echo; log_warn "OAuth interrupted for '"${name}"'"; exit 130' INT "${mcp_remote}" "${url}" --transport http-only --auth-timeout 180 >>"${log_file}" 2>&1 & mcp_pid=$! for poll_i in $(seq 1 720); do if mcp_auth_tokens_stable_for_url "${url}"; then auth_done=true break fi if mcp_oauth_succeeded_for_url "${url}" "${log_file}"; then auth_done=true break fi kill -0 "${mcp_pid}" 2>/dev/null || break sleep 0.25 done # v0.42: stable token wait before killing mcp-remote (-32000 fix). if [[ "${auth_done}" != true ]]; then sleep 0.5 mcp_auth_tokens_stable_for_url "${url}" && auth_done=true fi _zoho_oauth_kill_children "${watcher_pid}" "${tail_pid}" "${mcp_pid}" wait "${mcp_pid}" 2>/dev/null || ec=$? sleep 0.5 if [[ "${auth_done}" != true && ! -f "${opened_flag}" ]]; then oauth_url="$(extract_zoho_oauth_authorize_url "${log_file}")" if [[ -n "${oauth_url}" ]]; then touch "${opened_flag}" log_warn "Opening Firefox for sign-in URL (fallback)..." "${browser}" "${oauth_url}" 2>/dev/null || firefox -new-tab "${oauth_url}" 2>/dev/null || true for poll_i in $(seq 1 240); do mcp_auth_tokens_stable_for_url "${url}" && { auth_done=true; break; } mcp_oauth_succeeded_for_url "${url}" "${log_file}" && { auth_done=true; break; } sleep 0.5 done fi fi trap - INT probe_after="$(mcp_probe_label "$(probe_zoho_mcp_http "${url}")")" if [[ "${auth_done}" == true ]] \ || mcp_auth_tokens_stable_for_url "${url}" \ || [[ "${probe_after}" == "connected" ]]; then log_ok "OAuth complete: ${name}" rm -f "${log_file}" "${opened_flag}" return 0 fi if [[ -f "${log_file}" ]]; then log_warn "Last OAuth output (${name}):" tail -8 "${log_file}" 2>/dev/null || true fi rm -f "${log_file}" "${opened_flag}" return 1 } MCP_LAST_AGENT_LOG="" # Phase A — browser OAuth via mcp-remote + log watcher (v0.34 pattern). phase_a_browser_oauth() { local name="$1" force_reoauth="${2:-false}" url url="$(zoho_mcp_url_for_name "${name}")" [[ -z "${url}" ]] && { log_error "No URL for ${name}"; return 1; } # Non-batch snappy path: skip only when tokens work end-to-end (not stale tokens alone). if [[ "${MCP_OAUTH_BATCH}" != true && "${force_reoauth}" != true ]] \ && mcp_auth_tokens_present_for_url "${url}" \ && [[ "$(mcp_probe_label "$(probe_zoho_mcp_http "${url}")")" == "connected" ]]; then return 0 fi cleanup_stale_mcp_remote clear_mcp_auth_for_url "${url}" log_info "${name} — Phase A: browser OAuth..." _run_zoho_mcp_oauth "${name}" } # Phase B — agent mcp login (post_oauth: 2s settle, skip stale kill attempt 1, 60s timeout). phase_b_agent_register() { local name="$1" post_oauth="${2:-false}" local log_file ec=0 timeout_sec=30 attempt url="" pre_login_snap="" url="$(zoho_mcp_url_for_name "${name}")" [[ "${post_oauth}" == true ]] && timeout_sec=60 MCP_LAST_AGENT_LOG="" pre_login_snap="$(mktemp "${HOME}/.cursor/mcp.json.pre-login.XXXXXX")" || return 1 ensure_cursor_mcp_json_healthy || { log_error "Cannot heal ~/.cursor/mcp.json before agent register for ${name}" rm -f "${pre_login_snap}" return 1 } for attempt in 1 2; do if [[ "${post_oauth}" == true && "${attempt}" -eq 1 ]]; then sleep 2 else cleanup_stale_mcp_remote [[ "${attempt}" -gt 1 ]] && sleep 1 fi log_file="$(mktemp /tmp/zoho-agent-login.XXXXXX.log 2>/dev/null || mktemp "${HOME}/.cache/zoho-agent-login.XXXXXX.log")" MCP_LAST_AGENT_LOG="${log_file}" ec=0 log_info "${name} — Phase B: agent mcp login..." _mcp_save_pre_login_snapshot "${pre_login_snap}" || true if command_exists timeout; then timeout "${timeout_sec}" agent mcp login "${name}" >>"${log_file}" 2>&1 || ec=$? else agent mcp login "${name}" >>"${log_file}" 2>&1 || ec=$? fi _mcp_guard_config_after_agent_login "${name}" "${pre_login_snap}" || { rm -f "${log_file}" "${pre_login_snap}" MCP_LAST_AGENT_LOG="" return 1 } refresh_mcp_agent_list if [[ "$(agent_mcp_status_for_name "${name}" "${MCP_AGENT_LIST}")" == "ready" ]] \ && validate_mcp_json_config; then _mcp_debug_log "phase_b_agent_register:post" "H2" "after agent login ${name}" "$(_mcp_config_snapshot_json)" rm -f "${pre_login_snap}" return 0 fi if grep -qiE 'not valid JSON|Failed to enable MCP|unexpected token' "${log_file}" 2>/dev/null; then log_warn "${name} — agent CLI reported mcp.json JSON error (config restored if needed)" _show_agent_register_log_tail "${log_file}" elif [[ "${ec}" -ne 0 || -s "${log_file}" ]]; then log_warn "Agent register output (${name}):" _show_agent_register_log_tail "${log_file}" fi done rm -f "${pre_login_snap}" return 1 } # Phase C — verify ready (poll agent list; fallback when login log succeeded but list lags). phase_c_verify_ready() { local name="$1" url="$2" agent_log="${3:-}" local poll_i status for poll_i in $(seq 1 15); do refresh_mcp_agent_list status="$(agent_mcp_status_for_name "${name}" "${MCP_AGENT_LIST}")" if [[ "${status}" == "ready" ]] && validate_mcp_json_config; then return 0 fi sleep 2 done if mcp_auth_tokens_present_for_url "${url}" \ && [[ "$(mcp_probe_label "$(probe_zoho_mcp_http "${url}")")" == "connected" ]] \ && [[ -n "${agent_log}" && -f "${agent_log}" ]] \ && grep -qiE 'mcp login successful|login successful' "${agent_log}" 2>/dev/null; then validate_mcp_json_config || return 1 log_ok "${name} — Phase C: registered (agent list lag — will recheck at end)" return 0 fi return 1 } # Per-MCP pipeline. Returns 0=ready, 1=skipped, 2=batch aborted. auth_one_zoho_mcp() { local name="$1" mode="${2:-single}" local choice url phase_a_ok=false force_reoauth=false status [[ -z "${name}" ]] && return 1 if ! command_exists agent; then log_warn "Cursor CLI (agent) not found — cannot register ${name}" return 1 fi url="$(zoho_mcp_url_for_name "${name}")" use_pct="$(df / 2>/dev/null | awk 'NR==2{gsub(/%/,"",$5); print $5}' || echo 0)" [[ "${use_pct}" -ge 95 ]] 2>/dev/null && log_warn "/ is ${use_pct}% full — OAuth tokens may not save" refresh_mcp_agent_list status="$(agent_mcp_status_for_name "${name}" "${MCP_AGENT_LIST}")" if [[ "${status}" == "ready" ]] && validate_mcp_json_config; then log_ok "Already ready: ${name}" return 0 fi while true; do phase_a_ok=false if phase_a_browser_oauth "${name}" "${force_reoauth}"; then phase_a_ok=true log_ok "${name} — Phase A complete (tokens saved)" else log_warn "${name} — Phase A failed" fi if [[ "${phase_a_ok}" == true ]]; then phase_b_agent_register "${name}" true || true if phase_c_verify_ready "${name}" "${url}" "${MCP_LAST_AGENT_LOG}"; then log_ok "${name} — Phase C: ready — advancing to next MCP" rm -f "${MCP_LAST_AGENT_LOG}" MCP_LAST_AGENT_LOG="" validate_mcp_json_config || continue return 0 fi fi refresh_mcp_agent_list status="$(agent_mcp_status_for_name "${name}" "${MCP_AGENT_LIST}")" if [[ "${status}" == "ready" ]] && validate_mcp_json_config; then log_ok "${name} — Phase C: ready — advancing to next MCP" return 0 fi if [[ "${mode}" == batch ]]; then read_tty_line "Login failed: ${name}. Enter=retry register, n=skip this MCP, c=stop batch: " choice [[ "${choice}" == "c" || "${choice}" == "C" ]] && return 2 [[ "${choice}" == "n" || "${choice}" == "N" ]] && { log_warn "Skipped: ${name}" return 1 } force_reoauth=false [[ "${phase_a_ok}" != true ]] && force_reoauth=true log_info "Retrying register for: ${name}..." continue fi read_tty_line "Login failed: ${name}. Enter=retry, n=skip: " choice [[ "${choice}" == "n" || "${choice}" == "N" ]] && return 1 force_reoauth=false [[ "${phase_a_ok}" != true ]] && force_reoauth=true log_info "Retrying: ${name}..." done } _run_zoho_mcp_agent_register() { phase_b_agent_register "${1}" false } run_zoho_mcp_login() { local mode=single [[ "${MCP_OAUTH_BATCH}" == true ]] && mode=batch auth_one_zoho_mcp "${1}" "${mode}" } prompt_zoho_mcp_login() { local choice name ok=0 fail=0 build_zoho_need_action_lists if ((${#ZOHO_NEED_LOGIN[@]} == 0)); then log_ok "All Zoho MCPs already ready — skipping OAuth" report_zoho_mcps_final return 0 fi echo echo "=== Zoho MCP OAuth ===" echo "Each server needs its own sign-in at mcp.zoho.com (Firefox tab per server)." echo "Servers: ${ZOHO_NEED_LOGIN[*]}" echo read_tty_line "Authenticate ${#ZOHO_NEED_LOGIN[@]} Zoho MCP(s)? (Enter=all, n=skip): " choice [[ "${choice}" == "n" || "${choice}" == "N" ]] && return 0 log_info "Starting auth for ${#ZOHO_NEED_LOGIN[@]} MCP(s) (one browser tab each)..." MCP_OAUTH_BATCH=true trap 'cleanup_stale_mcp_remote; MCP_OAUTH_BATCH=false; trap - INT; exit 130' INT local idx=0 total=${#ZOHO_NEED_LOGIN[@]} for name in "${ZOHO_NEED_LOGIN[@]}"; do idx=$((idx + 1)) [[ "${idx}" -gt 1 ]] && _zoho_oauth_between_mcp_servers echo "" log_info "Server ${idx}/${total}: ${name}" ensure_cursor_mcp_json_healthy || { log_error "Cannot heal ~/.cursor/mcp.json before OAuth for ${name} — skipping" fail=$((fail + 1)) continue } run_zoho_mcp_login "${name}" case $? in 0) ok=$((ok + 1)) ;; 2) fail=$((fail + 1)) log_warn "Batch stopped by user at ${name}" break ;; *) fail=$((fail + 1)) ;; esac ensure_cursor_mcp_json_healthy || log_warn "~/.cursor/mcp.json needs heal after ${name}" refresh_mcp_agent_list _zoho_oauth_between_mcp_servers done MCP_OAUTH_BATCH=false trap - INT # #region agent log _mcp_debug_log "prompt_zoho_mcp_login:post" "H2" "auth batch complete" "$(_mcp_config_snapshot_json)" # #endregion log_info "OAuth finished: ${ok} ok, ${fail} failed" } prune_bad_zoho_mcp_urls() { local config="${HOME}/.cursor/mcp.json" local -a lines=() remove=() local line name url code tmp [[ -f "${config}" ]] || return 0 mapfile -t lines < <(list_zoho_mcp_entries_tsv) for line in "${lines[@]}"; do [[ -z "${line}" ]] && continue name="${line%%$'\t'*}" url="${line#*$'\t'}" [[ "$(mcp_probe_label "$(probe_zoho_mcp_http "${url}")")" == "bad_url" ]] && remove+=("${name}") done ((${#remove[@]} == 0)) && return 0 tmp="$(mktemp "${config}.tmp.XXXXXX")" || { log_error "Cannot allocate temp file for ${config}" return 1 } if ! jq --argjson names "$(printf '%s\n' "${remove[@]}" | jq -R . | jq -s .)" \ '.mcpServers |= with_entries(select(.key as $k | $names | index($k) | not))' \ "${config}" > "${tmp}"; then rm -f "${tmp}" log_error "Failed to prune bad Zoho MCP URLs (invalid JSON filter or input)" return 1 fi if ! jq empty "${tmp}" >/dev/null 2>&1; then rm -f "${tmp}" log_error "Refusing to write invalid JSON to ${config}" return 1 fi _mcp_config_install_file "${tmp}" "${config}" } ensure_cursor_mcp_json_healthy() { MCP_HEAL_FAIL_REASON="" # #region agent log _mcp_debug_log "ensure_cursor_mcp_json_healthy:entry" "H2" "heal start" \ "{\"state\":\"$(detect_mcp_json_state)\",\"jq\":$(command_exists jq && echo true || echo false),\"runId\":\"post-fix\"}" # #endregion if _ensure_cursor_mcp_json_healthy_unlocked; then # #region agent log _mcp_debug_log "ensure_cursor_mcp_json_healthy:ok" "H2" "heal ok" "$(_mcp_config_snapshot_json)" # #endregion return 0 fi log_error "Failed to normalize/install ~/.cursor/mcp.json during heal" MCP_HEAL_FAIL_REASON=internal # #region agent log _mcp_debug_log "ensure_cursor_mcp_json_healthy:fail" "H2" "heal failed" \ "{\"reason\":\"${MCP_HEAL_FAIL_REASON}\",\"state\":\"$(detect_mcp_json_state)\"}" # #endregion return 1 } _ensure_cursor_mcp_json_healthy_unlocked() { local config="${HOME}/.cursor/mcp.json" local backup_ts backup_path tmp restored=false mkdir -p "$(dirname "${config}")" if [[ ! -f "${config}" ]]; then # #region agent log _mcp_debug_log "_ensure_cursor_mcp_json_healthy_unlocked:missing" "H2" "creating empty config" "{}" # #endregion printf '{\n "mcpServers": {}\n}\n' > "${config}" return 0 fi if ! jq empty "${config}" >/dev/null 2>&1; then backup_ts="$(date +%Y%m%d-%H%M%S)" backup_path="${config}.corrupt.${backup_ts}.json" cp -f "${config}" "${backup_path}" 2>/dev/null || true log_warn "Detected corrupted ~/.cursor/mcp.json; backup: ${backup_path}" # #region agent log _mcp_debug_log "ensure_cursor_mcp_json_healthy:corrupt" "H4" "corrupt detected" \ "{\"backup\":\"${backup_path}\",\"size\":$(stat -c '%s' "${config}" 2>/dev/null || echo 0)}" # #endregion if restore_mcp_json_from_known_backups; then log_ok "Restored ~/.cursor/mcp.json from backup — re-validating..." restored=true else log_warn "No backup available — resetting ~/.cursor/mcp.json to empty schema" printf '{\n "mcpServers": {}\n}\n' > "${config}" fi fi tmp="$(mktemp "${config}.tmp.XXXXXX")" || { log_error "Cannot allocate temp file for ${config}" return 1 } if ! jq ' if type == "object" then . else {} end | .mcpServers = ( (.mcpServers // {}) | if type == "object" then . else {} end ) ' "${config}" > "${tmp}"; then rm -f "${tmp}" log_error "Failed to normalize ${config} (jq error — is jq installed?)" return 1 fi _mcp_config_install_file_unlocked "${tmp}" "${config}" || { log_error "Failed to install normalized ${config}" return 1 } [[ "${restored}" == true ]] && _mcp_debug_log "ensure_cursor_mcp_json_healthy:restored" "H4" "post-restore normalize ok" "$(_mcp_config_snapshot_json)" return 0 } dedupe_mcp_json_file() { local skip_probe="${1:-false}" local config="${HOME}/.cursor/mcp.json" ensure_cursor_mcp_json_healthy || { log_error "Failed to normalize/heal ~/.cursor/mcp.json before dedupe" return 1 } if [[ ! -f "${config}" ]]; then return 0 fi local tmp tmp="$(mktemp "${config}.tmp.XXXXXX")" || { log_error "Cannot allocate temp file for ${config}" return 1 } if ! jq ' def is_zoho: (.value.args // []) | map(tostring) | join(" ") | test("zohomcp\\.com"); def zoho_url: (.value.args // []) | map(select(test("zohomcp\\.com"))) | .[0] // ""; .mcpServers as $all | ($all | to_entries | map(select(is_zoho)) | group_by(zoho_url) | map(sort_by(.key | test("^zoho-")) | .[0]) | from_entries) as $zoho | ($all | to_entries | map(select(is_zoho | not)) | from_entries) as $other | .mcpServers = ($other + $zoho) ' "${config}" > "${tmp}"; then rm -f "${tmp}" log_error "Failed to dedupe ~/.cursor/mcp.json" return 1 fi if ! jq empty "${tmp}" >/dev/null 2>&1; then rm -f "${tmp}" log_error "Refusing to write invalid deduped JSON to ${config}" return 1 fi _mcp_config_install_file "${tmp}" "${config}" || { log_error "Failed to install deduped ~/.cursor/mcp.json" return 1 } [[ "${skip_probe}" == true ]] || prune_bad_zoho_mcp_urls } finalize_mcp_config() { local attempt log_info "Finalizing ~/.cursor/mcp.json..." # #region agent log _mcp_debug_log "finalize_mcp_config:entry" "H3" "finalize start" "$(_mcp_config_snapshot_json)" # #endregion for attempt in 1 2; do if ! ensure_cursor_mcp_json_healthy; then log_error "Failed to heal ~/.cursor/mcp.json (attempt ${attempt})" [[ "${attempt}" -eq 2 ]] && return 1 continue fi if ! dedupe_mcp_json_file true; then log_error "Failed to dedupe ~/.cursor/mcp.json (attempt ${attempt})" [[ "${attempt}" -eq 2 ]] && return 1 continue fi if validate_mcp_json_config; then # #region agent log _mcp_debug_log "finalize_mcp_config:ok" "H3" "finalize ok" "$(_mcp_config_snapshot_json)" # #endregion log_ok "MCP config validated" return 0 fi log_warn "MCP config invalid after finalize (attempt ${attempt}) — trying restore..." if [[ "${attempt}" -eq 1 ]] && restore_mcp_json_from_known_backups; then log_info "Restored MCP config from backup for re-finalize" continue fi log_error "MCP config could not be repaired — see ~/.cursor/mcp.json.corrupt.* backups" return 1 done return 1 } configure_zoho_mcps_interactive() { local count=0 mapfile -t _zoho_lines < <(list_zoho_mcp_entries_tsv) count=${#_zoho_lines[@]} if ((count > 0)); then log_ok "${count} Zoho MCPs in ~/.cursor/mcp.json — skipping URL entry" return 0 fi dedupe_mcp_json_file false [[ -f "${ZOHO_MCP_SCRIPT}" ]] || { log_error "Missing ${ZOHO_MCP_SCRIPT}"; return 1; } bash "${ZOHO_MCP_SCRIPT}" configure dedupe_mcp_json_file false } jumpstart_mcp() { ensure_path if ! has_interactive_tty; then log_error "MCP setup needs an interactive terminal." return 1 fi log_info "Zoho MCP phase..." ensure_mcp_deps local _mcp_state _mcp_state="$(detect_mcp_json_state)" # #region agent log _mcp_debug_log "jumpstart_mcp:entry" "H3" "mcp phase entry" \ "{\"state\":\"${_mcp_state}\",\"version\":\"${JUMPSTART_VERSION}\"}" # #endregion if [[ "${_mcp_state}" != "valid" ]]; then prompt_mcp_config_recovery "${_mcp_state}" || return 1 fi if ! ensure_cursor_mcp_json_healthy; then prompt_mcp_config_recovery "$(detect_mcp_json_state)" || return 1 ensure_cursor_mcp_json_healthy || { log_error "Failed to heal ~/.cursor/mcp.json before MCP phase" return 1 } fi begin_mcp_config_txn || { log_error "Cannot snapshot ~/.cursor/mcp.json"; return 1; } trap '_mcp_config_txn_rollback_on_err' ERR dedupe_mcp_json_file true || { log_error "Failed to prepare ~/.cursor/mcp.json" return 1 } local skip_mcp="" need_clone=false zoho_count=0 mapfile -t _zoho_lines < <(list_zoho_mcp_entries_tsv) zoho_count=${#_zoho_lines[@]} ((${zoho_count} == 0)) && need_clone=true if ((${zoho_count} > 0)); then log_info "${zoho_count} Zoho MCP(s) already in ~/.cursor/mcp.json — skipping URL entry, continuing to OAuth" else read_tty_line "Paste Zoho MCP URLs now? (Enter=yes, n=skip entire Zoho phase): " skip_mcp if [[ "${skip_mcp}" == "n" || "${skip_mcp}" == "N" ]]; then finalize_mcp_config || return 1 commit_mcp_config_txn || { log_error "MCP config txn commit failed" return 1 } trap - ERR report_zoho_mcps_final return 0 fi fi ensure_mcp_deps if [[ "${need_clone}" == true ]]; then if ! ensure_cursor_jumpstart; then rollback_mcp_config_txn trap - ERR log_warn "MCP setup skipped (clone failed)" return 0 fi fi configure_zoho_mcps_interactive mapfile -t _zoho_lines < <(list_zoho_mcp_entries_tsv) if ((${#_zoho_lines[@]} == 0)); then log_error "No Zoho MCPs configured — URL entry required before OAuth" rollback_mcp_config_txn trap - ERR return 1 fi prompt_zoho_mcp_login finalize_mcp_config || return 1 commit_mcp_config_txn || { log_error "MCP config txn commit failed" return 1 } trap - ERR report_zoho_mcps_final if ((${ZOHO_MCP_TOTAL_COUNT} > 0 && ${ZOHO_MCP_READY_COUNT} == ${ZOHO_MCP_TOTAL_COUNT})); then log_ok "Zoho MCP setup complete — all ${ZOHO_MCP_READY_COUNT} working" return 0 fi if ((${ZOHO_MCP_TOTAL_COUNT} > 0)); then log_warn "Zoho MCP setup incomplete: ${ZOHO_MCP_READY_COUNT}/${ZOHO_MCP_TOTAL_COUNT} ready" log_info "Run: agent mcp login for any tokens-only servers" return 1 fi log_error "Zoho MCP setup finished with no servers configured" return 1 } attach_tty() { if [[ -n "" ]]; then jumpstart_debug "tty attached" '{"ok":true,"via":"reexec"}' "H1" elif [[ -t 0 ]]; then jumpstart_debug "tty attached" '{"ok":true,"via":"stdin"}' "H1" else jumpstart_debug "tty missing" '{"ok":false}' "H1" fi } echo "Diazolvera Jumpstart v0.55" echo "======================================" echo "Phases: GitLab (QR) → Cursor (QR) → Zoho MCPs" echo attach_tty ensure_path jumpstart_deps jumpstart_gitlab # jumpstart_bitbucket jumpstart_cursor jumpstart_debug "phase handoff" '{"from":"cursor","to":"mcp"}' "H8" jumpstart_mcp log_ok "Jumpstart complete"