{"slug": "caffeinate-ai-keep-macos-awake-while-one-or-more-monitored-apps-are-running-for", "title": "Caffeinate AI: Keep macOS awake while one or more monitored apps are running (for long agentic sessions with ChatGPT/Codex or Claude)", "summary": "A developer released caffeinate-ai, a zsh script that keeps macOS from idle-sleeping while monitored applications such as ChatGPT and Codex are running, targeting long agentic coding sessions. The tool polls running apps every five seconds and wraps macOS's built-in caffeinate with -i so the system stays awake while the display can still sleep, and it ships with start, stop, restart, toggle, status, doctor, and run subcommands plus environment-variable configuration for interval, match mode, and target app list.", "body_md": "|  | #!/bin/zsh | \n|  | # | \n|  | # caffeinate-ai | \n|  | # | \n|  | # Keep macOS awake while one or more monitored applications are running. | \n|  | # | \n|  | # Setup: | \n|  | # 1. Add to local bin dir: `~/.local/bin/caffeinate-ai` | \n|  | # 2. Make it executable: `chmod +x ~/.local/bin/caffeinate-ai` | \n|  | # 3. View help and usage: `caffeinate-ai --help` | \n|  | # | \n|  | # Defaults: | \n|  | # - Monitor ChatGPT/Codex #TODO: add Claude | \n|  | # - Poll every 5 seconds | \n|  | # - Prevent idle system sleep with `caffeinate -i` | \n|  | # - Allow the display to sleep normally | \n|  | # - Produce no background logs unless --verbose is supplied | \n|  | # | \n|  | # Commands: | \n|  | # caffeinate-ai --help | \n|  | # caffeinate-ai start | \n|  | # caffeinate-ai stop | \n|  | # caffeinate-ai restart | \n|  | # caffeinate-ai toggle | \n|  | # caffeinate-ai status | \n|  | # caffeinate-ai doctor | \n|  | # caffeinate-ai run | \n|  | # caffeinate-ai | \n|  | # When called without a command, defaults to `caffeinate-ai run`, | \n|  | # which runs caffeinate-ai in the foreground of the current shell. | \n|  | set -u | \n|  | set -o pipefail | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Constants | \n|  | # ----------------------------------------------------------------------------- | \n|  | readonly SCRIPT_PATH=\"${0:A}\" | \n|  | readonly SCRIPT_NAME=\"${0:t}\" | \n|  | readonly STATE_DIR=\"${HOME}/Library/Caches/caffeinate-ai\" | \n|  | readonly PID_FILE=\"${STATE_DIR}/caffeinate-ai.pid\" | \n|  | readonly LOCK_DIR=\"${STATE_DIR}/operation.lock\" | \n|  | readonly LOCK_PID_FILE=\"${LOCK_DIR}/pid\" | \n|  | readonly LOG_FILE=\"${STATE_DIR}/caffeinate-ai.log\" | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Defaults / environment configuration | \n|  | # ----------------------------------------------------------------------------- | \n|  | # | \n|  | # These defaults can be overridden by environment variables: | \n|  | # | \n|  | # CAFFEINATE_AI_INTERVAL=10 | \n|  | # CAFFEINATE_AI_MATCH_MODE=exact | \n|  | # CAFFEINATE_AI_APPS='ChatGPT:Codex:Visual Studio Code' | \n|  | # CAFFEINATE_AI_CAFFEINATE_ARGS='-i' | \n|  | # | \n|  | # CLI arguments take precedence over environment configuration. | \n|  | # | \n|  | POLL_INTERVAL=\"${CAFFEINATE_AI_INTERVAL:-5}\" | \n|  | MATCH_MODE=\"${CAFFEINATE_AI_MATCH_MODE:-exact}\" | \n|  | VERBOSE=false | \n|  | DRY_RUN=false | \n|  | # | \n|  | # Add other applications to monitor here | \n|  | # | \n|  | TARGET_APPS=( | \n|  | \"ChatGPT\" | \n|  | \"Codex\" | \n|  | ) | \n|  | CAFFEINATE_ARGS=( | \n|  | \"-i\" | \n|  | ) | \n|  | if [[ -n \"${CAFFEINATE_AI_APPS:-}\" ]]; then | \n|  | TARGET_APPS=( | \n|  | \"${(@s/:/)CAFFEINATE_AI_APPS}\" | \n|  | ) | \n|  | fi | \n|  | if [[ -n \"${CAFFEINATE_AI_CAFFEINATE_ARGS:-}\" ]]; then | \n|  | CAFFEINATE_ARGS=( | \n|  | \"${(@s/:/)CAFFEINATE_AI_CAFFEINATE_ARGS}\" | \n|  | ) | \n|  | fi | \n|  | # | \n|  | # PID of the caffeinate process owned by this watcher. | \n|  | # | \n|  | CAFFEINATE_PID=\"\" | \n|  | # | \n|  | # Set to 1 only for a detached watcher launched by `start`. | \n|  | # | \n|  | IS_DAEMON=\"${CAFFEINATE_AI_DAEMON:-0}\" | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Help | \n|  | # ----------------------------------------------------------------------------- | \n|  | usage() { | \n|  | cat <<EOF | \n|  | Usage: | \n|  | ${SCRIPT_NAME} [command] [options] | \n|  | Commands: | \n|  | start [options] | \n|  | Start caffeinate-ai as a detached background watcher. | \n|  | stop | \n|  | Stop the background watcher and its caffeinate child. | \n|  | restart [options] | \n|  | Stop the current watcher and start a new one. | \n|  | toggle [options] | \n|  | Stop the watcher if running; otherwise start it. | \n|  | Options are used only when starting. | \n|  | status | \n|  | Show the state of the background watcher and caffeinate child. | \n|  | doctor [options] | \n|  | Diagnose process detection, watcher state, and the macOS power | \n|  | assertion. | \n|  | run [options] | \n|  | Run the watcher in the foreground. | \n|  | This is the default when options are supplied without a command. | \n|  | help | \n|  | Show this help message. | \n|  | Options for start, restart, toggle, run, and doctor: | \n|  | -a, --app NAME | \n|  | Add an application/process name to monitor. | \n|  | Supplying --app replaces the default ChatGPT/Codex list. | \n|  | May be specified multiple times: | \n|  | ${SCRIPT_NAME} start \\\\ | \n|  | --app ChatGPT \\\\ | \n|  | --app Codex | \n|  | -i, --interval SECONDS | \n|  | Poll interval. | \n|  | Default: | \n|  | ${POLL_INTERVAL} | \n|  | -m, --match MODE | \n|  | Process matching mode. | \n|  | Supported: | \n|  | exact | \n|  | Exact process-name matching using pgrep -x. | \n|  | contains | \n|  | Match against the full process command using pgrep -f. | \n|  | Default: | \n|  | ${MATCH_MODE} | \n|  | -c, --caffeinate ARG | \n|  | Add an argument passed to caffeinate. | \n|  | Supplying this option replaces the default \"-i\". | \n|  | May be specified multiple times: | \n|  | ${SCRIPT_NAME} start \\\\ | \n|  | --caffeinate=-d \\\\ | \n|  | --caffeinate=-i | \n|  | -v, --verbose | \n|  | Enable diagnostic output. | \n|  | In background mode, verbose output is written to: | \n|  | ${LOG_FILE} | \n|  | The log is truncated each time a new verbose daemon starts. | \n|  | --dry-run | \n|  | Foreground-only mode. | \n|  | Detect applications and report when caffeinate would start or stop, | \n|  | without creating a power assertion. | \n|  | -h, --help | \n|  | Show this help message. | \n|  | Environment variables: | \n|  | CAFFEINATE_AI_INTERVAL | \n|  | Default polling interval. | \n|  | CAFFEINATE_AI_MATCH_MODE | \n|  | Default matching mode: exact or contains. | \n|  | CAFFEINATE_AI_APPS | \n|  | Colon-separated default application names. | \n|  | Example: | \n|  | export CAFFEINATE_AI_APPS='ChatGPT:Codex:Visual Studio Code' | \n|  | CAFFEINATE_AI_CAFFEINATE_ARGS | \n|  | Colon-separated caffeinate arguments. | \n|  | Example: | \n|  | export CAFFEINATE_AI_CAFFEINATE_ARGS='-i' | \n|  | Examples: | \n|  | Start: | \n|  | ${SCRIPT_NAME} start | \n|  | Status: | \n|  | ${SCRIPT_NAME} status | \n|  | Stop: | \n|  | ${SCRIPT_NAME} stop | \n|  | Restart: | \n|  | ${SCRIPT_NAME} restart | \n|  | Toggle: | \n|  | ${SCRIPT_NAME} toggle | \n|  | Diagnose: | \n|  | ${SCRIPT_NAME} doctor | \n|  | Run in the foreground: | \n|  | ${SCRIPT_NAME} run | \n|  | Dry-run process detection: | \n|  | ${SCRIPT_NAME} run --dry-run | \n|  | Run verbosely: | \n|  | ${SCRIPT_NAME} run --verbose | \n|  | Start with verbose background logging: | \n|  | ${SCRIPT_NAME} start --verbose | \n|  | Poll every 10 seconds: | \n|  | ${SCRIPT_NAME} start --interval 10 | \n|  | Watch only Codex: | \n|  | ${SCRIPT_NAME} start --app Codex | \n|  | Watch several applications: | \n|  | ${SCRIPT_NAME} start \\\\ | \n|  | --app ChatGPT \\\\ | \n|  | --app Codex \\\\ | \n|  | --app \"Visual Studio Code\" | \n|  | Prevent both idle system sleep and display sleep: | \n|  | ${SCRIPT_NAME} start \\\\ | \n|  | --caffeinate=-d \\\\ | \n|  | --caffeinate=-i | \n|  | State: | \n|  | PID: | \n|  | ${PID_FILE} | \n|  | Lock: | \n|  | ${LOCK_DIR} | \n|  | Verbose log: | \n|  | ${LOG_FILE} | \n|  | Exit codes: | \n|  | 0 Success | \n|  | 1 Runtime failure or inactive status | \n|  | 2 Invalid arguments | \n|  | 127 Required command not found | \n|  | EOF | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Output | \n|  | # ----------------------------------------------------------------------------- | \n|  | print_status() { | \n|  | print -r -- \"$*\" | \n|  | } | \n|  | log() { | \n|  | if [[ \"${VERBOSE}\" != true ]]; then | \n|  | return 0 | \n|  | fi | \n|  | printf '[%s] %s\\n' \\ | \n|  | \"$(date '+%Y-%m-%d %H:%M:%S')\" \\ | \n|  | \"$*\" | \n|  | } | \n|  | debug() { | \n|  | if [[ \"${VERBOSE}\" == true ]]; then | \n|  | log \"DEBUG: $*\" | \n|  | fi | \n|  | } | \n|  | error() { | \n|  | printf '%s: error: %s\\n' \"${SCRIPT_NAME}\" \"$*\" >&2 | \n|  | } | \n|  | die() { | \n|  | local message=\"$1\" | \n|  | local exit_code=\"${2:-1}\" | \n|  | error \"${message}\" | \n|  | exit \"${exit_code}\" | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Dependency / input validation | \n|  | # ----------------------------------------------------------------------------- | \n|  | require_command() { | \n|  | local command_name=\"$1\" | \n|  | if ! command -v \"${command_name}\" >/dev/null 2>&1; then | \n|  | die \"required command not found: ${command_name}\" 127 | \n|  | fi | \n|  | } | \n|  | validate_positive_integer() { | \n|  | local value=\"$1\" | \n|  | local name=\"$2\" | \n|  | if [[ ! \"${value}\" =~ '^[0-9]+$' ]]; then | \n|  | die \"${name} must be a positive integer; received: ${value}\" 2 | \n|  | fi | \n|  | if (( value < 1 )); then | \n|  | die \"${name} must be greater than zero; received: ${value}\" 2 | \n|  | fi | \n|  | } | \n|  | validate_runtime_dependencies() { | \n|  | require_command caffeinate | \n|  | require_command pgrep | \n|  | require_command ps | \n|  | require_command date | \n|  | require_command kill | \n|  | require_command sleep | \n|  | require_command mkdir | \n|  | require_command mv | \n|  | require_command rm | \n|  | require_command rmdir | \n|  | } | \n|  | validate_lifecycle_dependencies() { | \n|  | require_command pgrep | \n|  | require_command ps | \n|  | require_command kill | \n|  | require_command sleep | \n|  | require_command mkdir | \n|  | require_command rm | \n|  | require_command rmdir | \n|  | } | \n|  | validate_doctor_dependencies() { | \n|  | validate_lifecycle_dependencies | \n|  | require_command pmset | \n|  | require_command grep | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # State directory | \n|  | # ----------------------------------------------------------------------------- | \n|  | ensure_state_dir() { | \n|  | if ! mkdir -p \"${STATE_DIR}\"; then | \n|  | die \"could not create state directory: ${STATE_DIR}\" | \n|  | fi | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Generic process inspection | \n|  | # ----------------------------------------------------------------------------- | \n|  | process_is_alive() { | \n|  | local pid=\"$1\" | \n|  | kill -0 \"${pid}\" 2>/dev/null | \n|  | } | \n|  | process_command() { | \n|  | local pid=\"$1\" | \n|  | ps -p \"${pid}\" -o command= 2>/dev/null | \n|  | } | \n|  | process_matches_script_instance() { | \n|  | local pid=\"$1\" | \n|  | if ! process_is_alive \"${pid}\"; then | \n|  | return 1 | \n|  | fi | \n|  | local command_line | \n|  | if ! command_line=\"$(process_command \"${pid}\")\"; then | \n|  | return 1 | \n|  | fi | \n|  | if [[ \"${command_line}\" == *\"${SCRIPT_PATH}\"* \\|\\| | \n|  | \"${command_line}\" == *\"${SCRIPT_NAME}\"* ]]; then | \n|  | return 0 | \n|  | fi | \n|  | return 1 | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Atomic operation lock | \n|  | # ----------------------------------------------------------------------------- | \n|  | acquire_operation_lock() { | \n|  | validate_lifecycle_dependencies | \n|  | ensure_state_dir | \n|  | local attempts=2 | \n|  | while (( attempts > 0 )); do | \n|  | if mkdir \"${LOCK_DIR}\" 2>/dev/null; then | \n|  | if ! print -r -- \"$$\" >\"${LOCK_PID_FILE}\"; then | \n|  | rmdir \"${LOCK_DIR}\" 2>/dev/null \\|\\| true | \n|  | die \"could not write operation lock PID\" | \n|  | fi | \n|  | return 0 | \n|  | fi | \n|  | # | \n|  | # Another process may have created the directory but not yet written the | \n|  | # PID file. Give it a brief opportunity to finish. | \n|  | # | \n|  | if [[ ! -f \"${LOCK_PID_FILE}\" ]]; then | \n|  | sleep 0.1 | \n|  | fi | \n|  | local owner_pid=\"\" | \n|  | if [[ -f \"${LOCK_PID_FILE}\" ]]; then | \n|  | owner_pid=\"$(<\"${LOCK_PID_FILE}\")\" | \n|  | fi | \n|  | if [[ \"${owner_pid}\" =~ '^[0-9]+$' ]] && | \n|  | process_matches_script_instance \"${owner_pid}\"; then | \n|  | die \\ | \n|  | \"another caffeinate-ai operation is already in progress (PID ${owner_pid})\" | \n|  | fi | \n|  | # | \n|  | # The lock owner no longer exists or the lock metadata is invalid. | \n|  | # | \n|  | rm -f \"${LOCK_PID_FILE}\" 2>/dev/null \\|\\| true | \n|  | if ! rmdir \"${LOCK_DIR}\" 2>/dev/null; then | \n|  | die \"could not recover stale operation lock: ${LOCK_DIR}\" | \n|  | fi | \n|  | (( attempts-- )) | \n|  | done | \n|  | die \"could not acquire operation lock\" | \n|  | } | \n|  | release_operation_lock() { | \n|  | if [[ ! -d \"${LOCK_DIR}\" ]]; then | \n|  | return 0 | \n|  | fi | \n|  | local owner_pid=\"\" | \n|  | if [[ -f \"${LOCK_PID_FILE}\" ]]; then | \n|  | owner_pid=\"$(<\"${LOCK_PID_FILE}\")\" | \n|  | fi | \n|  | # | \n|  | # Only remove a valid lock owned by this process. | \n|  | # | \n|  | if [[ \"${owner_pid}\" != \"$$\" ]]; then | \n|  | return 0 | \n|  | fi | \n|  | rm -f \"${LOCK_PID_FILE}\" 2>/dev/null \\|\\| true | \n|  | rmdir \"${LOCK_DIR}\" 2>/dev/null \\|\\| true | \n|  | } | \n|  | with_operation_lock() { | \n|  | acquire_operation_lock | \n|  | # | \n|  | # Ensure an unexpected exit still releases our lock. | \n|  | # | \n|  | trap 'release_operation_lock' EXIT | \n|  | trap 'release_operation_lock; exit 130' INT | \n|  | trap 'release_operation_lock; exit 143' TERM | \n|  | trap 'release_operation_lock; exit 129' HUP | \n|  | \"$@\" | \n|  | local exit_status=$? | \n|  | release_operation_lock | \n|  | trap - EXIT | \n|  | trap - INT | \n|  | trap - TERM | \n|  | trap - HUP | \n|  | return \"${exit_status}\" | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # PID file | \n|  | # ----------------------------------------------------------------------------- | \n|  | read_daemon_pid() { | \n|  | if [[ ! -f \"${PID_FILE}\" ]]; then | \n|  | return 1 | \n|  | fi | \n|  | local pid | \n|  | pid=\"$(<\"${PID_FILE}\")\" | \n|  | if [[ ! \"${pid}\" =~ '^[0-9]+$' ]]; then | \n|  | debug \"invalid PID file; removing ${PID_FILE}\" | \n|  | rm -f \"${PID_FILE}\" | \n|  | return 1 | \n|  | fi | \n|  | print -r -- \"${pid}\" | \n|  | } | \n|  | write_daemon_pid() { | \n|  | local pid=\"$1\" | \n|  | local temp_file=\"${PID_FILE}.${pid}.tmp\" | \n|  | ensure_state_dir | \n|  | if ! print -r -- \"${pid}\" >\"${temp_file}\"; then | \n|  | die \"could not write temporary PID file: ${temp_file}\" | \n|  | fi | \n|  | if ! mv -f \"${temp_file}\" \"${PID_FILE}\"; then | \n|  | rm -f \"${temp_file}\" | \n|  | die \"could not write PID file: ${PID_FILE}\" | \n|  | fi | \n|  | } | \n|  | remove_pid_file_if_owned() { | \n|  | local expected_pid=\"$1\" | \n|  | if [[ ! -f \"${PID_FILE}\" ]]; then | \n|  | return 0 | \n|  | fi | \n|  | local pid | \n|  | pid=\"$(<\"${PID_FILE}\")\" | \n|  | if [[ \"${pid}\" == \"${expected_pid}\" ]]; then | \n|  | rm -f \"${PID_FILE}\" | \n|  | fi | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Daemon validation | \n|  | # ----------------------------------------------------------------------------- | \n|  | process_matches_daemon() { | \n|  | local pid=\"$1\" | \n|  | if ! process_matches_script_instance \"${pid}\"; then | \n|  | return 1 | \n|  | fi | \n|  | local command_line | \n|  | if ! command_line=\"$(process_command \"${pid}\")\"; then | \n|  | return 1 | \n|  | fi | \n|  | # | \n|  | # A managed daemon is always re-executed using the explicit \"run\" | \n|  | # subcommand. | \n|  | # | \n|  | if [[ \"${command_line}\" != *\" run\"* ]]; then | \n|  | return 1 | \n|  | fi | \n|  | return 0 | \n|  | } | \n|  | daemon_is_running() { | \n|  | local pid | \n|  | if ! pid=\"$(read_daemon_pid)\"; then | \n|  | return 1 | \n|  | fi | \n|  | if process_matches_daemon \"${pid}\"; then | \n|  | return 0 | \n|  | fi | \n|  | debug \"removing stale PID file for PID ${pid}\" | \n|  | rm -f \"${PID_FILE}\" | \n|  | return 1 | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Target process detection | \n|  | # ----------------------------------------------------------------------------- | \n|  | get_matching_pids() { | \n|  | local process_name=\"$1\" | \n|  | local raw_pids=\"\" | \n|  | case \"${MATCH_MODE}\" in | \n|  | exact) | \n|  | raw_pids=\"$( | \n|  | pgrep \\ | \n|  | -U \"${UID}\" \\ | \n|  | -x \\ | \n|  | -- \"${process_name}\" \\ | \n|  | 2>/dev/null \\|\\| | \n|  | true | \n|  | )\" | \n|  | ;; | \n|  | contains) | \n|  | raw_pids=\"$( | \n|  | pgrep \\ | \n|  | -U \"${UID}\" \\ | \n|  | -f \\ | \n|  | -- \"${process_name}\" \\ | \n|  | 2>/dev/null \\|\\| | \n|  | true | \n|  | )\" | \n|  | ;; | \n|  | *) | \n|  | return 2 | \n|  | ;; | \n|  | esac | \n|  | if [[ -z \"${raw_pids}\" ]]; then | \n|  | return 1 | \n|  | fi | \n|  | local pid | \n|  | for pid in ${(f)raw_pids}; do | \n|  | # | \n|  | # Do not allow the watcher or its invoking shell to satisfy its own | \n|  | # contains-mode query. | \n|  | # | \n|  | if [[ \"${pid}\" == \"$$\" \\|\\| \"${pid}\" == \"${PPID}\" ]]; then | \n|  | continue | \n|  | fi | \n|  | print -r -- \"${pid}\" | \n|  | done | \n|  | } | \n|  | process_is_running() { | \n|  | local process_name=\"$1\" | \n|  | local matches | \n|  | matches=\"$(get_matching_pids \"${process_name}\" 2>/dev/null \\|\\| true)\" | \n|  | [[ -n \"${matches}\" ]] | \n|  | } | \n|  | find_running_target() { | \n|  | local app | \n|  | for app in \"${TARGET_APPS[@]}\"; do | \n|  | if process_is_running \"${app}\"; then | \n|  | print -r -- \"${app}\" | \n|  | return 0 | \n|  | fi | \n|  | done | \n|  | return 1 | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # caffeinate lifecycle | \n|  | # ----------------------------------------------------------------------------- | \n|  | caffeinate_is_running() { | \n|  | if [[ -z \"${CAFFEINATE_PID}\" ]]; then | \n|  | return 1 | \n|  | fi | \n|  | process_is_alive \"${CAFFEINATE_PID}\" | \n|  | } | \n|  | start_caffeinate() { | \n|  | if caffeinate_is_running; then | \n|  | debug \"caffeinate already running as PID ${CAFFEINATE_PID}\" | \n|  | return 0 | \n|  | fi | \n|  | CAFFEINATE_PID=\"\" | \n|  | debug \"starting: caffeinate ${(j: :)CAFFEINATE_ARGS}\" | \n|  | caffeinate \"${CAFFEINATE_ARGS[@]}\" & | \n|  | local pid=$! | \n|  | # | \n|  | # Give caffeinate a short opportunity to reject invalid arguments. | \n|  | # | \n|  | sleep 0.1 | \n|  | if ! process_is_alive \"${pid}\"; then | \n|  | wait \"${pid}\" 2>/dev/null | \n|  | local exit_status=$? | \n|  | error \"caffeinate failed to start (exit status ${exit_status})\" | \n|  | return 1 | \n|  | fi | \n|  | CAFFEINATE_PID=\"${pid}\" | \n|  | log \"Started caffeinate (PID ${CAFFEINATE_PID})\" | \n|  | return 0 | \n|  | } | \n|  | stop_caffeinate() { | \n|  | if [[ -z \"${CAFFEINATE_PID}\" ]]; then | \n|  | return 0 | \n|  | fi | \n|  | local pid=\"${CAFFEINATE_PID}\" | \n|  | # | \n|  | # Clear state immediately so cleanup remains idempotent. | \n|  | # | \n|  | CAFFEINATE_PID=\"\" | \n|  | if ! process_is_alive \"${pid}\"; then | \n|  | debug \"caffeinate PID ${pid} is no longer running\" | \n|  | wait \"${pid}\" 2>/dev/null \\|\\| true | \n|  | return 0 | \n|  | fi | \n|  | debug \"sending TERM to caffeinate PID ${pid}\" | \n|  | if ! kill -TERM \"${pid}\" 2>/dev/null; then | \n|  | error \"could not terminate caffeinate PID ${pid}\" | \n|  | return 1 | \n|  | fi | \n|  | wait \"${pid}\" 2>/dev/null \\|\\| true | \n|  | log \"Stopped caffeinate\" | \n|  | return 0 | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Watcher cleanup / signals | \n|  | # ----------------------------------------------------------------------------- | \n|  | cleanup_watcher() { | \n|  | local original_status=$? | \n|  | debug \"cleaning up\" | \n|  | stop_caffeinate \\|\\| true | \n|  | if [[ \"${IS_DAEMON}\" == \"1\" ]]; then | \n|  | remove_pid_file_if_owned \"$$\" | \n|  | fi | \n|  | return \"${original_status}\" | \n|  | } | \n|  | handle_signal() { | \n|  | local signal_name=\"$1\" | \n|  | local exit_code=1 | \n|  | case \"${signal_name}\" in | \n|  | INT) | \n|  | exit_code=130 | \n|  | ;; | \n|  | TERM) | \n|  | exit_code=143 | \n|  | ;; | \n|  | HUP) | \n|  | exit_code=129 | \n|  | ;; | \n|  | esac | \n|  | log \"Received ${signal_name}; shutting down\" | \n|  | exit \"${exit_code}\" | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Argument parsing | \n|  | # ----------------------------------------------------------------------------- | \n|  | parse_run_arguments() { | \n|  | local custom_apps=false | \n|  | local custom_caffeinate_args=false | \n|  | while (( $# > 0 )); do | \n|  | case \"$1\" in | \n|  | -a\\|--app) | \n|  | if (( $# < 2 )); then | \n|  | die \"$1 requires an application/process name\" 2 | \n|  | fi | \n|  | if [[ \"${custom_apps}\" == false ]]; then | \n|  | TARGET_APPS=() | \n|  | custom_apps=true | \n|  | fi | \n|  | TARGET_APPS+=(\"$2\") | \n|  | shift 2 | \n|  | ;; | \n|  | --app=*) | \n|  | if [[ \"${custom_apps}\" == false ]]; then | \n|  | TARGET_APPS=() | \n|  | custom_apps=true | \n|  | fi | \n|  | TARGET_APPS+=(\"${1#*=}\") | \n|  | shift | \n|  | ;; | \n|  | -i\\|--interval) | \n|  | if (( $# < 2 )); then | \n|  | die \"$1 requires a number of seconds\" 2 | \n|  | fi | \n|  | POLL_INTERVAL=\"$2\" | \n|  | shift 2 | \n|  | ;; | \n|  | --interval=*) | \n|  | POLL_INTERVAL=\"${1#*=}\" | \n|  | shift | \n|  | ;; | \n|  | -m\\|--match) | \n|  | if (( $# < 2 )); then | \n|  | die \"$1 requires either 'exact' or 'contains'\" 2 | \n|  | fi | \n|  | MATCH_MODE=\"$2\" | \n|  | shift 2 | \n|  | ;; | \n|  | --match=*) | \n|  | MATCH_MODE=\"${1#*=}\" | \n|  | shift | \n|  | ;; | \n|  | -c\\|--caffeinate) | \n|  | if (( $# < 2 )); then | \n|  | die \"$1 requires a caffeinate argument\" 2 | \n|  | fi | \n|  | if [[ \"${custom_caffeinate_args}\" == false ]]; then | \n|  | CAFFEINATE_ARGS=() | \n|  | custom_caffeinate_args=true | \n|  | fi | \n|  | CAFFEINATE_ARGS+=(\"$2\") | \n|  | shift 2 | \n|  | ;; | \n|  | --caffeinate=*) | \n|  | if [[ \"${custom_caffeinate_args}\" == false ]]; then | \n|  | CAFFEINATE_ARGS=() | \n|  | custom_caffeinate_args=true | \n|  | fi | \n|  | CAFFEINATE_ARGS+=(\"${1#*=}\") | \n|  | shift | \n|  | ;; | \n|  | -v\\|--verbose) | \n|  | VERBOSE=true | \n|  | shift | \n|  | ;; | \n|  | --dry-run) | \n|  | DRY_RUN=true | \n|  | shift | \n|  | ;; | \n|  | -h\\|--help) | \n|  | usage | \n|  | exit 0 | \n|  | ;; | \n|  | --) | \n|  | shift | \n|  | break | \n|  | ;; | \n|  | -*) | \n|  | die \"unknown option: $1\" 2 | \n|  | ;; | \n|  | *) | \n|  | die \"unexpected argument: $1\" 2 | \n|  | ;; | \n|  | esac | \n|  | done | \n|  | validate_positive_integer \"${POLL_INTERVAL}\" \"poll interval\" | \n|  | case \"${MATCH_MODE}\" in | \n|  | exact\\|contains) | \n|  | ;; | \n|  | *) | \n|  | die \\ | \n|  | \"match mode must be 'exact' or 'contains'; received: ${MATCH_MODE}\" \\ | \n|  | 2 | \n|  | ;; | \n|  | esac | \n|  | if (( ${#TARGET_APPS[@]} == 0 )); then | \n|  | die \"at least one application/process must be specified\" 2 | \n|  | fi | \n|  | local app | \n|  | for app in \"${TARGET_APPS[@]}\"; do | \n|  | if [[ -z \"${app}\" ]]; then | \n|  | die \"application/process names may not be empty\" 2 | \n|  | fi | \n|  | done | \n|  | if (( ${#CAFFEINATE_ARGS[@]} == 0 )); then | \n|  | die \"at least one caffeinate argument must be specified\" 2 | \n|  | fi | \n|  | local caffeinate_arg | \n|  | for caffeinate_arg in \"${CAFFEINATE_ARGS[@]}\"; do | \n|  | if [[ -z \"${caffeinate_arg}\" ]]; then | \n|  | die \"caffeinate arguments may not be empty\" 2 | \n|  | fi | \n|  | done | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Foreground watcher | \n|  | # ----------------------------------------------------------------------------- | \n|  | run_monitor() { | \n|  | parse_run_arguments \"$@\" | \n|  | validate_runtime_dependencies | \n|  | trap cleanup_watcher EXIT | \n|  | trap 'handle_signal INT' INT | \n|  | trap 'handle_signal TERM' TERM | \n|  | trap 'handle_signal HUP' HUP | \n|  | if [[ \"${IS_DAEMON}\" == \"1\" ]]; then | \n|  | write_daemon_pid \"$$\" | \n|  | fi | \n|  | log \"Watching for: ${(j:, :)TARGET_APPS}\" | \n|  | log \"Poll interval: ${POLL_INTERVAL}s\" | \n|  | log \"Process matching: ${MATCH_MODE}\" | \n|  | log \"Caffeinate arguments: ${(j: :)CAFFEINATE_ARGS}\" | \n|  | if [[ \"${DRY_RUN}\" == true ]]; then | \n|  | print_status \"Dry-run mode enabled\" | \n|  | fi | \n|  | local previous_target=\"\" | \n|  | while true; do | \n|  | local running_target=\"\" | \n|  | if running_target=\"$(find_running_target)\"; then | \n|  | debug \"detected target process: ${running_target}\" | \n|  | if [[ \"${running_target}\" != \"${previous_target}\" ]]; then | \n|  | if [[ \"${DRY_RUN}\" == true ]]; then | \n|  | print_status \\ | \n|  | \"Dry run: would start caffeinate for ${running_target}\" | \n|  | else | \n|  | log \"Detected ${running_target}\" | \n|  | fi | \n|  | previous_target=\"${running_target}\" | \n|  | fi | \n|  | if [[ \"${DRY_RUN}\" != true ]]; then | \n|  | if ! caffeinate_is_running; then | \n|  | # | \n|  | # If a PID was recorded but is no longer alive, caffeinate died | \n|  | # unexpectedly rather than being intentionally stopped. | \n|  | # | \n|  | if [[ -n \"${CAFFEINATE_PID}\" ]]; then | \n|  | log \\ | \n|  | \"caffeinate exited unexpectedly; restarting\" | \n|  | fi | \n|  | if ! start_caffeinate; then | \n|  | error \\ | \n|  | \"will retry caffeinate on the next polling cycle\" | \n|  | fi | \n|  | fi | \n|  | fi | \n|  | else | \n|  | if [[ -n \"${previous_target}\" ]]; then | \n|  | if [[ \"${DRY_RUN}\" == true ]]; then | \n|  | print_status \"Dry run: would stop caffeinate\" | \n|  | else | \n|  | log \"No monitored applications are running\" | \n|  | fi | \n|  | previous_target=\"\" | \n|  | fi | \n|  | if [[ \"${DRY_RUN}\" != true ]]; then | \n|  | if caffeinate_is_running; then | \n|  | stop_caffeinate \\|\\| true | \n|  | elif [[ -n \"${CAFFEINATE_PID}\" ]]; then | \n|  | debug \\ | \n|  | \"clearing stale caffeinate PID ${CAFFEINATE_PID}\" | \n|  | CAFFEINATE_PID=\"\" | \n|  | fi | \n|  | fi | \n|  | fi | \n|  | sleep \"${POLL_INTERVAL}\" | \n|  | done | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Child argument construction | \n|  | # ----------------------------------------------------------------------------- | \n|  | build_child_arguments() { | \n|  | local app | \n|  | for app in \"${TARGET_APPS[@]}\"; do | \n|  | print -r -- \"--app\" | \n|  | print -r -- \"${app}\" | \n|  | done | \n|  | print -r -- \"--interval\" | \n|  | print -r -- \"${POLL_INTERVAL}\" | \n|  | print -r -- \"--match\" | \n|  | print -r -- \"${MATCH_MODE}\" | \n|  | local caffeinate_arg | \n|  | for caffeinate_arg in \"${CAFFEINATE_ARGS[@]}\"; do | \n|  | print -r -- \"--caffeinate=${caffeinate_arg}\" | \n|  | done | \n|  | if [[ \"${VERBOSE}\" == true ]]; then | \n|  | print -r -- \"--verbose\" | \n|  | fi | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Background daemon start | \n|  | # ----------------------------------------------------------------------------- | \n|  | start_daemon_current_config() { | \n|  | validate_runtime_dependencies | \n|  | require_command nohup | \n|  | if [[ \"${DRY_RUN}\" == true ]]; then | \n|  | die \"--dry-run is only supported with the run command\" 2 | \n|  | fi | \n|  | ensure_state_dir | \n|  | local existing_pid | \n|  | if daemon_is_running; then | \n|  | existing_pid=\"$(read_daemon_pid)\" | \n|  | print_status \\ | \n|  | \"caffeinate-ai is already running (PID ${existing_pid})\" | \n|  | return 0 | \n|  | fi | \n|  | rm -f \"${PID_FILE}\" | \n|  | local child_args=( | \n|  | \"${(@f)$(build_child_arguments)}\" | \n|  | ) | \n|  | debug \"starting detached watcher\" | \n|  | # | \n|  | # Verbose logs are deliberately session-scoped rather than append-only. | \n|  | # | \n|  | if [[ \"${VERBOSE}\" == true ]]; then | \n|  | if ! : >\"${LOG_FILE}\"; then | \n|  | die \"could not initialize verbose log: ${LOG_FILE}\" | \n|  | fi | \n|  | CAFFEINATE_AI_DAEMON=1 \\ | \n|  | nohup \"${SCRIPT_PATH}\" run \"${child_args[@]}\" \\ | \n|  | >>\"${LOG_FILE}\" \\ | \n|  | 2>&1 \\ | \n|  | </dev/null & | \n|  | else | \n|  | CAFFEINATE_AI_DAEMON=1 \\ | \n|  | nohup \"${SCRIPT_PATH}\" run \"${child_args[@]}\" \\ | \n|  | >/dev/null \\ | \n|  | 2>&1 \\ | \n|  | </dev/null & | \n|  | fi | \n|  | local pid=$! | \n|  | # | \n|  | # Wait for the child to finish initialization and write its PID file. | \n|  | # | \n|  | local attempts=30 | \n|  | local daemon_pid=\"\" | \n|  | while (( attempts > 0 )); do | \n|  | if ! process_is_alive \"${pid}\"; then | \n|  | error \"caffeinate-ai failed to start\" | \n|  | if [[ \"${VERBOSE}\" == true ]]; then | \n|  | error \"check log: ${LOG_FILE}\" | \n|  | fi | \n|  | return 1 | \n|  | fi | \n|  | if daemon_pid=\"$(read_daemon_pid 2>/dev/null)\"; then | \n|  | if [[ \"${daemon_pid}\" == \"${pid}\" ]] && | \n|  | process_matches_daemon \"${pid}\"; then | \n|  | print_status \\ | \n|  | \"Started caffeinate-ai (PID ${pid})\" | \n|  | if [[ \"${VERBOSE}\" == true ]]; then | \n|  | print_status \\ | \n|  | \"Verbose log: ${LOG_FILE}\" | \n|  | fi | \n|  | return 0 | \n|  | fi | \n|  | fi | \n|  | sleep 0.1 | \n|  | (( attempts-- )) | \n|  | done | \n|  | # | \n|  | # Never intentionally leave an unmanageable daemon behind. | \n|  | # | \n|  | if process_matches_daemon \"${pid}\"; then | \n|  | kill -TERM \"${pid}\" 2>/dev/null \\|\\| true | \n|  | fi | \n|  | rm -f \"${PID_FILE}\" | \n|  | error \"timed out waiting for caffeinate-ai to initialize\" | \n|  | if [[ \"${VERBOSE}\" == true ]]; then | \n|  | error \"check log: ${LOG_FILE}\" | \n|  | fi | \n|  | return 1 | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Background daemon stop | \n|  | # ----------------------------------------------------------------------------- | \n|  | stop_daemon_current() { | \n|  | validate_lifecycle_dependencies | \n|  | local pid | \n|  | if ! daemon_is_running; then | \n|  | rm -f \"${PID_FILE}\" | \n|  | print_status \\ | \n|  | \"caffeinate-ai background watcher is not running\" | \n|  | return 0 | \n|  | fi | \n|  | pid=\"$(read_daemon_pid)\" | \n|  | print_status \\ | \n|  | \"Stopping caffeinate-ai (PID ${pid})\" | \n|  | if ! kill -TERM \"${pid}\" 2>/dev/null; then | \n|  | error \"could not send TERM to PID ${pid}\" | \n|  | return 1 | \n|  | fi | \n|  | # | \n|  | # Allow roughly five seconds for graceful shutdown. | \n|  | # | \n|  | local attempts=50 | \n|  | while (( attempts > 0 )); do | \n|  | if ! process_matches_daemon \"${pid}\"; then | \n|  | break | \n|  | fi | \n|  | sleep 0.1 | \n|  | (( attempts-- )) | \n|  | done | \n|  | if process_matches_daemon \"${pid}\"; then | \n|  | error \\ | \n|  | \"process did not stop gracefully; sending SIGKILL\" | \n|  | kill -KILL \"${pid}\" 2>/dev/null \\|\\| true | \n|  | attempts=20 | \n|  | while (( attempts > 0 )); do | \n|  | if ! process_matches_daemon \"${pid}\"; then | \n|  | break | \n|  | fi | \n|  | sleep 0.1 | \n|  | (( attempts-- )) | \n|  | done | \n|  | fi | \n|  | if process_matches_daemon \"${pid}\"; then | \n|  | error \"could not stop caffeinate-ai PID ${pid}\" | \n|  | return 1 | \n|  | fi | \n|  | remove_pid_file_if_owned \"${pid}\" | \n|  | print_status \"Stopped caffeinate-ai\" | \n|  | return 0 | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Restart / toggle | \n|  | # ----------------------------------------------------------------------------- | \n|  | restart_daemon_current_config() { | \n|  | stop_daemon_current \\|\\| return $? | \n|  | start_daemon_current_config | \n|  | } | \n|  | toggle_daemon_current_config() { | \n|  | if daemon_is_running; then | \n|  | stop_daemon_current | \n|  | else | \n|  | start_daemon_current_config | \n|  | fi | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # caffeinate child lookup | \n|  | # ----------------------------------------------------------------------------- | \n|  | get_caffeinate_child_pids() { | \n|  | local daemon_pid=\"$1\" | \n|  | pgrep \\ | \n|  | -U \"${UID}\" \\ | \n|  | -P \"${daemon_pid}\" \\ | \n|  | -x \\ | \n|  | caffeinate \\ | \n|  | 2>/dev/null \\|\\| | \n|  | true | \n|  | } | \n|  | get_first_caffeinate_child_pid() { | \n|  | local daemon_pid=\"$1\" | \n|  | local raw_pids | \n|  | raw_pids=\"$(get_caffeinate_child_pids \"${daemon_pid}\")\" | \n|  | if [[ -z \"${raw_pids}\" ]]; then | \n|  | return 1 | \n|  | fi | \n|  | local pids=( | \n|  | \"${(@f)raw_pids}\" | \n|  | ) | \n|  | if (( ${#pids[@]} == 0 )); then | \n|  | return 1 | \n|  | fi | \n|  | print -r -- \"${pids[1]}\" | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Status | \n|  | # ----------------------------------------------------------------------------- | \n|  | status_daemon() { | \n|  | validate_lifecycle_dependencies | \n|  | local pid | \n|  | if ! daemon_is_running; then | \n|  | rm -f \"${PID_FILE}\" | \n|  | print_status \\ | \n|  | \"caffeinate-ai background watcher is not running\" | \n|  | return 1 | \n|  | fi | \n|  | pid=\"$(read_daemon_pid)\" | \n|  | print_status \\ | \n|  | \"caffeinate-ai background watcher is running (PID ${pid})\" | \n|  | local caffeinate_pid=\"\" | \n|  | if caffeinate_pid=\"$( | \n|  | get_first_caffeinate_child_pid \"${pid}\" | \n|  | )\"; then | \n|  | print_status \\ | \n|  | \"caffeinate is active (PID ${caffeinate_pid})\" | \n|  | else | \n|  | print_status \\ | \n|  | \"caffeinate is currently inactive\" | \n|  | fi | \n|  | return 0 | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Doctor | \n|  | # ----------------------------------------------------------------------------- | \n|  | doctor() { | \n|  | parse_run_arguments \"$@\" | \n|  | validate_doctor_dependencies | \n|  | if [[ \"${DRY_RUN}\" == true ]]; then | \n|  | die \"--dry-run is only supported with the run command\" 2 | \n|  | fi | \n|  | print_status \"caffeinate-ai diagnostics\" | \n|  | print_status \"\" | \n|  | # | \n|  | # Watcher | \n|  | # | \n|  | print_status \"Watcher:\" | \n|  | local daemon_pid=\"\" | \n|  | if daemon_is_running; then | \n|  | daemon_pid=\"$(read_daemon_pid)\" | \n|  | print_status \\ | \n|  | \" background: running (PID ${daemon_pid})\" | \n|  | else | \n|  | print_status \\ | \n|  | \" background: not running\" | \n|  | fi | \n|  | print_status \"\" | \n|  | # | \n|  | # Targets | \n|  | # | \n|  | print_status \"Targets:\" | \n|  | local app | \n|  | local raw_pids | \n|  | for app in \"${TARGET_APPS[@]}\"; do | \n|  | raw_pids=\"$(get_matching_pids \"${app}\" 2>/dev/null \\|\\| true)\" | \n|  | if [[ -n \"${raw_pids}\" ]]; then | \n|  | local pids=( | \n|  | \"${(@f)raw_pids}\" | \n|  | ) | \n|  | print_status \\ | \n|  | \" ${app}: running (PIDs ${(j:, :)pids})\" | \n|  | else | \n|  | print_status \\ | \n|  | \" ${app}: not running\" | \n|  | fi | \n|  | done | \n|  | print_status \"\" | \n|  | # | \n|  | # caffeinate | \n|  | # | \n|  | print_status \"Sleep prevention:\" | \n|  | local caffeinate_pid=\"\" | \n|  | if [[ -n \"${daemon_pid}\" ]] && | \n|  | caffeinate_pid=\"$( | \n|  | get_first_caffeinate_child_pid \"${daemon_pid}\" | \n|  | )\"; then | \n|  | print_status \\ | \n|  | \" caffeinate: active (PID ${caffeinate_pid})\" | \n|  | local assertions | \n|  | assertions=\"$(pmset -g assertions 2>/dev/null \\|\\| true)\" | \n|  | if print -r -- \"${assertions}\" \\| | \n|  | grep -E \\ | \n|  | \"pid[[:space:]]+${caffeinate_pid}\\\\(\" \\ | \n|  | >/dev/null 2>&1; then | \n|  | print_status \\ | \n|  | \" power assertion: present\" | \n|  | else | \n|  | print_status \\ | \n|  | \" power assertion: not detected\" | \n|  | fi | \n|  | else | \n|  | print_status \\ | \n|  | \" caffeinate: inactive\" | \n|  | print_status \\ | \n|  | \" power assertion: none expected\" | \n|  | fi | \n|  | print_status \"\" | \n|  | # | \n|  | # Effective diagnostic configuration | \n|  | # | \n|  | print_status \"Configuration:\" | \n|  | print_status \\ | \n|  | \" interval: ${POLL_INTERVAL}s\" | \n|  | print_status \\ | \n|  | \" match: ${MATCH_MODE}\" | \n|  | print_status \\ | \n|  | \" apps: ${(j:, :)TARGET_APPS}\" | \n|  | print_status \\ | \n|  | \" caffeinate arguments: ${(j: :)CAFFEINATE_ARGS}\" | \n|  | return 0 | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Background configuration preparation | \n|  | # ----------------------------------------------------------------------------- | \n|  | prepare_background_configuration() { | \n|  | parse_run_arguments \"$@\" | \n|  | if [[ \"${DRY_RUN}\" == true ]]; then | \n|  | die \"--dry-run is only supported with the run command\" 2 | \n|  | fi | \n|  | validate_runtime_dependencies | \n|  | require_command nohup | \n|  | } | \n|  | # ----------------------------------------------------------------------------- | \n|  | # Main | \n|  | # ----------------------------------------------------------------------------- | \n|  | main() { | \n|  | local subcommand=\"${1:-run}\" | \n|  | case \"${subcommand}\" in | \n|  | start) | \n|  | shift | \n|  | prepare_background_configuration \"$@\" | \n|  | with_operation_lock \\ | \n|  | start_daemon_current_config | \n|  | ;; | \n|  | stop) | \n|  | shift | \n|  | if (( $# > 0 )); then | \n|  | die \"stop does not accept additional arguments\" 2 | \n|  | fi | \n|  | validate_lifecycle_dependencies | \n|  | with_operation_lock \\ | \n|  | stop_daemon_current | \n|  | ;; | \n|  | restart) | \n|  | shift | \n|  | # | \n|  | # Validate everything before stopping an existing daemon. That prevents | \n|  | # an invalid replacement configuration from unnecessarily taking down | \n|  | # a working watcher. | \n|  | # | \n|  | prepare_background_configuration \"$@\" | \n|  | with_operation_lock \\ | \n|  | restart_daemon_current_config | \n|  | ;; | \n|  | toggle) | \n|  | shift | \n|  | prepare_background_configuration \"$@\" | \n|  | with_operation_lock \\ | \n|  | toggle_daemon_current_config | \n|  | ;; | \n|  | status) | \n|  | shift | \n|  | if (( $# > 0 )); then | \n|  | die \"status does not accept additional arguments\" 2 | \n|  | fi | \n|  | status_daemon | \n|  | ;; | \n|  | doctor) | \n|  | shift | \n|  | doctor \"$@\" | \n|  | ;; | \n|  | run) | \n|  | # \"run\" may be implicit when no arguments were supplied. | \n|  | if (( $# > 0 )); then | \n|  | shift | \n|  | fi | \n|  | run_monitor \"$@\" | \n|  | ;; | \n|  | help) | \n|  | usage | \n|  | ;; | \n|  | -h\\|--help) | \n|  | usage | \n|  | ;; | \n|  | -*) | \n|  | # | \n|  | # Shorthand: | \n|  | # | \n|  | # caffeinate-ai --verbose | \n|  | # | \n|  | # is equivalent to: | \n|  | # | \n|  | # caffeinate-ai run --verbose | \n|  | # | \n|  | run_monitor \"$@\" | \n|  | ;; | \n|  | *) | \n|  | error \"unknown command: ${subcommand}\" | \n|  | print >&2 | \n|  | usage >&2 | \n|  | exit 2 | \n|  | ;; | \n|  | esac | \n|  | } | \n|  | main \"$@\" |", "url": "https://wpnews.pro/news/caffeinate-ai-keep-macos-awake-while-one-or-more-monitored-apps-are-running-for", "canonical_source": "https://gist.github.com/sbolel/4b80f5fa5604d710a06054a85756e412", "published_at": "2026-10-02 01:17:36+00:00", "updated_at": "2026-10-02 01:44:59.455761+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents"], "entities": ["ChatGPT", "Codex", "Claude", "macOS", "Visual Studio Code"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/caffeinate-ai-keep-macos-awake-while-one-or-more-monitored-apps-are-running-for", "markdown": "https://wpnews.pro/news/caffeinate-ai-keep-macos-awake-while-one-or-more-monitored-apps-are-running-for.md", "text": "https://wpnews.pro/news/caffeinate-ai-keep-macos-awake-while-one-or-more-monitored-apps-are-running-for.txt", "jsonld": "https://wpnews.pro/news/caffeinate-ai-keep-macos-awake-while-one-or-more-monitored-apps-are-running-for.jsonld"}}