← Files Voice Notify for CodexARCHIVED FILE

scripts/voice_notify_config.sh

14.5 KB · Oct 2, 2026 · 00:29 UTC

↓ Download file

#!/bin/sh
# Configure and test Voice Notify on macOS without Python or developer tools.

umask 077
LC_ALL=C
export LC_ALL

script_directory=$(
    CDPATH= cd "$(/usr/bin/dirname "$0")" 2>/dev/null && pwd -P
) || exit 1
plugin_root=$(
    CDPATH= cd "$script_directory/.." 2>/dev/null && pwd -P
) || exit 1
config_path=${CODEX_VOICE_NOTIFY_CONFIG:-"${HOME:-}/.config/codex-voice-notify/settings.json"}
config_directory=$(/usr/bin/dirname "$config_path")
minimum_hooks_version="0.145.0"

usage_error() {
    printf '%s\n' "$1" >&2
    exit 2
}

valid_voice() {
    case "$1" in female|male) return 0 ;; *) return 1 ;; esac
}

valid_language() {
    case "$1" in ko|ja|en|ru|zh-CN) return 0 ;; *) return 1 ;; esac
}

event_slug() {
    case "$1" in
        SessionStart) printf '%s\n' "session-start" ;;
        UserPromptSubmit) printf '%s\n' "user-prompt-submit" ;;
        PreToolUse) printf '%s\n' "pre-tool-use" ;;
        PostToolUse) printf '%s\n' "post-tool-use" ;;
        PermissionRequest) printf '%s\n' "permission-request" ;;
        PreCompact) printf '%s\n' "pre-compact" ;;
        PostCompact) printf '%s\n' "post-compact" ;;
        SubagentStart) printf '%s\n' "subagent-start" ;;
        SubagentStop) printf '%s\n' "subagent-stop" ;;
        Stop) printf '%s\n' "stop" ;;
        *) return 1 ;;
    esac
}

set_default_settings() {
    enabled=true
    voice=female
    language=ko
    minimum_interval=450
    event_session_start=true
    event_user_prompt_submit=true
    event_pre_tool_use=false
    event_post_tool_use=false
    event_permission_request=true
    event_pre_compact=true
    event_post_compact=true
    event_subagent_start=true
    event_subagent_stop=true
    event_stop=true
}

plist_raw() {
    /usr/bin/plutil -extract "$1" raw -- "$config_path" 2>/dev/null
}

configured_boolean() {
    configured_value=$(plist_raw "$1") || {
        printf '%s\n' "$2"
        return
    }
    case "$configured_value" in
        true|false) printf '%s\n' "$configured_value" ;;
        *) printf '%s\n' "$2" ;;
    esac
}

load_settings() {
    set_default_settings
    [ -f "$config_path" ] || return
    config_bytes=$(
        /usr/bin/wc -c <"$config_path" 2>/dev/null |
            /usr/bin/tr -d '[:space:]'
    )
    case "$config_bytes" in
        ''|*[!0-9]*) return ;;
    esac
    [ "$config_bytes" -le 65536 ] || return

    configured_value=$(plist_raw enabled) || configured_value=""
    case "$configured_value" in true|false) enabled=$configured_value ;; esac
    configured_value=$(plist_raw voice) || configured_value=""
    case "$configured_value" in female|male) voice=$configured_value ;; esac
    configured_value=$(plist_raw language) || configured_value=""
    case "$configured_value" in ko|ja|en|ru|zh-CN) language=$configured_value ;; esac
    configured_value=$(plist_raw min_interval_ms) || configured_value=""
    case "$configured_value" in
        ''|*[!0-9]*) ;;
        *)
            if [ "$configured_value" -gt 10000 ]; then
                minimum_interval=10000
            else
                minimum_interval=$configured_value
            fi
            ;;
    esac

    event_session_start=$(configured_boolean events.SessionStart "$event_session_start")
    event_user_prompt_submit=$(configured_boolean events.UserPromptSubmit "$event_user_prompt_submit")
    event_pre_tool_use=$(configured_boolean events.PreToolUse "$event_pre_tool_use")
    event_post_tool_use=$(configured_boolean events.PostToolUse "$event_post_tool_use")
    event_permission_request=$(configured_boolean events.PermissionRequest "$event_permission_request")
    event_pre_compact=$(configured_boolean events.PreCompact "$event_pre_compact")
    event_post_compact=$(configured_boolean events.PostCompact "$event_post_compact")
    event_subagent_start=$(configured_boolean events.SubagentStart "$event_subagent_start")
    event_subagent_stop=$(configured_boolean events.SubagentStop "$event_subagent_stop")
    event_stop=$(configured_boolean events.Stop "$event_stop")
}

emit_settings() {
    printf '%s\n' \
        '{' \
        "  \"enabled\": $enabled," \
        "  \"voice\": \"$voice\"," \
        "  \"language\": \"$language\"," \
        "  \"min_interval_ms\": $minimum_interval," \
        '  "events": {' \
        "    \"SessionStart\": $event_session_start," \
        "    \"UserPromptSubmit\": $event_user_prompt_submit," \
        "    \"PreToolUse\": $event_pre_tool_use," \
        "    \"PostToolUse\": $event_post_tool_use," \
        "    \"PermissionRequest\": $event_permission_request," \
        "    \"PreCompact\": $event_pre_compact," \
        "    \"PostCompact\": $event_post_compact," \
        "    \"SubagentStart\": $event_subagent_start," \
        "    \"SubagentStop\": $event_subagent_stop," \
        "    \"Stop\": $event_stop" \
        '  }' \
        '}'
}

save_settings() {
    if [ ! -d "$config_directory" ]; then
        /bin/mkdir -p "$config_directory" || return 1
        /bin/chmod 700 "$config_directory" 2>/dev/null
    fi
    temporary_path="$config_directory/.settings.$$"
    emit_settings >"$temporary_path" || {
        /bin/rm -f "$temporary_path"
        return 1
    }
    /bin/chmod 600 "$temporary_path" 2>/dev/null
    /bin/mv -f "$temporary_path" "$config_path" || {
        /bin/rm -f "$temporary_path"
        return 1
    }
    printf 'Saved %s\n' "$config_path"
}

set_event_value() {
    event_name=$1
    event_value=$2
    case "$event_name" in
        SessionStart) event_session_start=$event_value ;;
        UserPromptSubmit) event_user_prompt_submit=$event_value ;;
        PreToolUse) event_pre_tool_use=$event_value ;;
        PostToolUse) event_post_tool_use=$event_value ;;
        PermissionRequest) event_permission_request=$event_value ;;
        PreCompact) event_pre_compact=$event_value ;;
        PostCompact) event_post_compact=$event_value ;;
        SubagentStart) event_subagent_start=$event_value ;;
        SubagentStop) event_subagent_stop=$event_value ;;
        Stop) event_stop=$event_value ;;
        *) return 1 ;;
    esac
}

apply_choices() {
    if [ -n "$voice_choice" ]; then
        valid_voice "$voice_choice" || usage_error "Voice must be female or male."
        voice=$voice_choice
    fi
    if [ -n "$language_choice" ]; then
        valid_language "$language_choice" ||
            usage_error "Language must be ko, ja, en, ru, or zh-CN."
        language=$language_choice
    fi
    if [ -n "$interval_choice" ]; then
        case "$interval_choice" in
            ''|*[!0-9]*) usage_error "Minimum interval must be a non-negative integer." ;;
        esac
        if [ "$interval_choice" -gt 10000 ]; then
            minimum_interval=10000
        else
            minimum_interval=$interval_choice
        fi
    fi
    for event_name in $enable_events; do
        set_event_value "$event_name" true ||
            usage_error "Unsupported event: $event_name"
    done
    for event_name in $disable_events; do
        set_event_value "$event_name" false ||
            usage_error "Unsupported event: $event_name"
    done
}

selected_audio_path() {
    selected_event=$1
    selected_slug=$(event_slug "$selected_event") || return 1
    printf '%s\n' "$plugin_root/assets/audio/$voice/$language/$selected_slug.wav"
}

find_codex_command() {
    if [ -n "$codex_choice" ]; then
        printf '%s\n' "$codex_choice"
        return
    fi
    if [ -n "${CODEX_VOICE_NOTIFY_CODEX:-}" ]; then
        printf '%s\n' "$CODEX_VOICE_NOTIFY_CODEX"
        return
    fi
    command -v codex 2>/dev/null
}

inspect_codex() {
    codex_path=$(find_codex_command) || codex_path=""
    if [ -z "$codex_path" ]; then
        printf '%s\n' \
            "Codex CLI was not found. Install or expose Codex on PATH, then rerun setup." >&2
        return 3
    fi
    codex_output=$("$codex_path" --version 2>&1)
    codex_status=$?
    if [ "$codex_status" -ne 0 ]; then
        printf 'Could not determine the Codex CLI version from %s.\n' "$codex_path" >&2
        return 3
    fi
    case "$codex_output" in
        *'
'*)
            printf 'Could not determine the Codex CLI version from %s.\n' "$codex_path" >&2
            return 3
            ;;
    esac
    version_parts=$(
        printf '%s' "$codex_output" |
            /usr/bin/tr '[:upper:]' '[:lower:]' |
            /usr/bin/sed -nE \
                's/^[[:space:]]*(openai[[:space:]]+)?codex(-cli)?[[:space:]]+\(?v?([0-9]+)\.([0-9]+)\.([0-9]+)\)?[[:space:]]*$/\3 \4 \5/p'
    )
    set -- $version_parts
    if [ "$#" -ne 3 ]; then
        printf 'Could not determine the Codex CLI version from %s.\n' "$codex_path" >&2
        return 3
    fi
    codex_major=$1
    codex_minor=$2
    codex_patch=$3
    printf 'Codex CLI: %s.%s.%s (%s)\n' \
        "$codex_major" "$codex_minor" "$codex_patch" "$codex_path"
    if [ "$codex_major" -lt 0 ] ||
        { [ "$codex_major" -eq 0 ] && [ "$codex_minor" -lt 145 ]; }; then
        printf 'Codex CLI %s or newer is required for /hooks. Update Codex, then rerun setup.\n' \
            "$minimum_hooks_version" >&2
        return 3
    fi
    return 0
}

shell_quote() {
    quoted_value=$(
        printf '%s' "$1" | /usr/bin/sed "s/'/'\\\\''/g"
    )
    printf "'%s'" "$quoted_value"
}

open_hook_terminal() {
    working_directory=$(pwd -P)
    terminal_command="printf '\\nVoice Notify opened this new Codex CLI terminal. Type /hooks here and review the bundled hook.\\n\\n'; exec $(shell_quote "$codex_path") --no-alt-screen -C $(shell_quote "$working_directory")"
    /usr/bin/osascript \
        -e 'on run argv' \
        -e 'set terminalCommand to item 1 of argv' \
        -e 'tell application "Terminal"' \
        -e 'activate' \
        -e 'do script terminalCommand' \
        -e 'end tell' \
        -e 'end run' \
        -- "$terminal_command" >/dev/null 2>&1
}

command_name=${1:-show}
if [ "$#" -gt 0 ]; then
    shift
fi
voice_choice=""
language_choice=""
interval_choice=""
enable_events=""
disable_events=""
event_choice="Stop"
dry_run=false
skip_audio_test=false
open_hooks=false
codex_choice=""

while [ "$#" -gt 0 ]; do
    case "$1" in
        --voice)
            [ "$#" -ge 2 ] || usage_error "--voice requires a value."
            voice_choice=$2
            shift 2
            ;;
        --language)
            [ "$#" -ge 2 ] || usage_error "--language requires a value."
            language_choice=$2
            shift 2
            ;;
        --min-interval-ms)
            [ "$#" -ge 2 ] || usage_error "--min-interval-ms requires a value."
            interval_choice=$2
            shift 2
            ;;
        --enable-event)
            [ "$#" -ge 2 ] || usage_error "--enable-event requires a value."
            event_slug "$2" >/dev/null ||
                usage_error "Unsupported event: $2"
            enable_events="$enable_events $2"
            shift 2
            ;;
        --disable-event)
            [ "$#" -ge 2 ] || usage_error "--disable-event requires a value."
            event_slug "$2" >/dev/null ||
                usage_error "Unsupported event: $2"
            disable_events="$disable_events $2"
            shift 2
            ;;
        --event)
            [ "$#" -ge 2 ] || usage_error "--event requires a value."
            event_slug "$2" >/dev/null ||
                usage_error "Unsupported event: $2"
            event_choice=$2
            shift 2
            ;;
        --dry-run)
            dry_run=true
            shift
            ;;
        --skip-audio-test)
            skip_audio_test=true
            shift
            ;;
        --open-hooks)
            open_hooks=true
            shift
            ;;
        --codex-command)
            [ "$#" -ge 2 ] || usage_error "--codex-command requires a value."
            codex_choice=$2
            shift 2
            ;;
        *)
            usage_error "Unknown option: $1"
            ;;
    esac
done

load_settings

case "$command_name" in
    show)
        emit_settings
        ;;
    reset)
        /bin/rm -f "$config_path"
        printf '%s\n' "Defaults restored."
        ;;
    mute)
        enabled=false
        save_settings || exit 1
        ;;
    unmute)
        enabled=true
        save_settings || exit 1
        ;;
    set)
        apply_choices
        save_settings || exit 1
        ;;
    test)
        apply_choices
        enabled=true
        set_event_value "$event_choice" true || exit 2
        audio_path=$(selected_audio_path "$event_choice") || exit 2
        if [ ! -f "$audio_path" ]; then
            printf 'Audio file is unavailable: %s\n' "$audio_path" >&2
            exit 1
        fi
        printf '%s\n' "$audio_path"
        if [ "$dry_run" = false ]; then
            [ -x /usr/bin/afplay ] || {
                printf '%s\n' "The macOS afplay command is unavailable." >&2
                exit 1
            }
            /usr/bin/afplay "$audio_path" || exit $?
        fi
        ;;
    setup)
        apply_choices
        enabled=true
        inspect_codex || exit $?
        audio_path=$(selected_audio_path Stop) || exit 1
        if [ ! -f "$audio_path" ]; then
            printf 'Stop audio file is unavailable: %s\n' "$audio_path" >&2
            exit 1
        fi
        printf 'Stop audio: %s\n' "$audio_path"
        if [ "$dry_run" = false ] && [ "$skip_audio_test" = false ]; then
            [ -x /usr/bin/afplay ] || {
                printf '%s\n' "The macOS afplay command is unavailable." >&2
                exit 1
            }
            /usr/bin/afplay "$audio_path" || exit $?
        fi
        if [ "$dry_run" = true ]; then
            printf 'Dry run: would save voice=%s language=%s.\n' "$voice" "$language"
        else
            save_settings || exit 1
        fi
        if [ "$open_hooks" = true ]; then
            if [ "$dry_run" = true ]; then
                printf '%s\n' \
                    "Dry run: would open a new terminal and start Codex CLI for manual /hooks review."
            elif open_hook_terminal; then
                printf '%s\n' \
                    "Started Codex CLI in a new Terminal window. Type /hooks in that window, review the Voice Notify hook, and trust it."
            else
                printf '%s\n' \
                    "Could not open a new Codex CLI Terminal automatically. Start Codex CLI yourself, enter /hooks, and review the Voice Notify hook." >&2
                exit 4
            fi
        else
            printf '%s\n' "Next: run Codex, enter /hooks, and review the Voice Notify hook."
        fi
        printf '%s\n' \
            "After trust is granted, fully restart Codex before testing lifecycle events."
        ;;
    *)
        usage_error "Command must be show, set, mute, unmute, reset, test, or setup."
        ;;
esac
exit 0

SHA-256: 833f767d7703f98b933cf4501bd66515dc3b7a5f9b17f7363aeb2d5e21ffc9b9