Last active
January 31, 2026 12:47
-
-
Save kiyoon/a0e3e7c5139fddf9004e816cf5389ed9 to your computer and use it in GitHub Desktop.
Use when API limit has reached but you want to run something after.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ---------------------------- | |
| # Claude config (configure here) | |
| # ---------------------------- | |
| additional_args=() | |
| # 1) Allowed tools (safer than skipping permissions) | |
| # additional_args+=( | |
| # --allowedTools | |
| # "Read,Edit,Bash(git *),Bash(pytest *),Bash(cargo *),Bash(uv *)," | |
| # ) | |
| # 2) Skip permission prompts (riskier) | |
| # additional_args+=(--dangerously-skip-permissions) | |
| PROMPT="$(cat <<'EOF' | |
| /ralph-loop:ralph-loop "Study the implementations of analyzer in basedpyright and implement as-is. Identify missing features that are partially implemented. | |
| ## Hard Rules (Non-negotiable) | |
| - **Module location rule:** Any code from moduleName.ts must live in module_name/ **or** module_name.rs (for **any** module in TypeScript). Do not place it elsewhere. | |
| - **Porting order rule:** Start by porting modules with minimal internal dependency (no internal imports within the package), then gradually port modules that have more internal imports. | |
| - If anything fails, do NOT ad-hoc patch; analyze basedpyright handling and mirror structure/naming/logic/tests. | |
| - If any existing port doesn't follow these rules, refactor it. | |
| " --completion-promise "___NEVER_PRINT_THIS___" --max-iterations 1000000 | |
| EOF | |
| )" | |
| usage() { | |
| echo "Usage: $0 HH:MM | NOW" >&2 | |
| echo "Example: $0 02:00 or $0 NOW" >&2 | |
| } | |
| # Require time arg | |
| if [[ $# -lt 1 ]]; then | |
| usage | |
| exit 2 | |
| fi | |
| TARGET_TIME="$1" | |
| # Validate HH:MM (hour can be 1–2 digits) | |
| if [[ "$TARGET_TIME" != "NOW" ]]; then | |
| if [[ ! "$TARGET_TIME" =~ ^([0-9]{1,2}):([0-9]{2})$ ]]; then | |
| echo "Error: time must be HH:MM (e.g. 02:00 or 9:22) or NOW. Got: $TARGET_TIME" >&2 | |
| usage | |
| exit 2 | |
| fi | |
| HOUR="${BASH_REMATCH[1]}" | |
| MIN="${BASH_REMATCH[2]}" | |
| # Range checks | |
| if (( 10#$HOUR > 23 || 10#$MIN > 59 )); then | |
| echo "Error: invalid time: $TARGET_TIME" >&2 | |
| usage | |
| exit 2 | |
| fi | |
| # Prefer GNU date if available (macOS: gdate via coreutils) | |
| date_cmd="date" | |
| if command -v gdate >/dev/null 2>&1; then | |
| date_cmd="gdate" | |
| fi | |
| calc_target_epoch() { | |
| local now_epoch today_target_epoch | |
| now_epoch="$("$date_cmd" +%s)" | |
| today_target_epoch="$("$date_cmd" -d "today ${HOUR}:$(printf '%02d' "$MIN"):00" +%s)" | |
| if (( now_epoch < today_target_epoch )); then | |
| echo "$today_target_epoch" | |
| else | |
| "$date_cmd" -d "tomorrow ${HOUR}:$(printf '%02d' "$MIN"):00" +%s | |
| fi | |
| } | |
| target_epoch="$(calc_target_epoch)" | |
| while true; do | |
| now_epoch="$("$date_cmd" +%s)" | |
| remaining=$(( target_epoch - now_epoch )) | |
| if (( remaining <= 0 )); then | |
| break | |
| fi | |
| # Sleep at most 30 min | |
| # When close (<5 min), sleep 10s for better accuracy. | |
| if (( remaining > 3600 )); then | |
| sleep_for=1800 | |
| elif (( remaining > 1800 )); then | |
| sleep_for=600 | |
| elif (( remaining > 300 )); then | |
| sleep_for=60 | |
| else | |
| sleep_for=10 | |
| fi | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Waiting... remaining ${remaining}s (sleeping ${sleep_for}s) until ${TARGET_TIME}" | |
| sleep "$sleep_for" | |
| done | |
| fi | |
| # ---------------------------- | |
| # Run Claude | |
| # ---------------------------- | |
| set +u # allow undefined vars in additional_args | |
| claude "$PROMPT" "${additional_args[@]}" 2>&1 | |
| status=$? | |
| set -u | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Done. Exit code: ${status}" | |
| exit "$status" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ---------------------------- | |
| # Configuration | |
| # ---------------------------- | |
| # Optionally wait until a specific time to start | |
| START_TIME="NOW" | |
| # START_TIME="02:00" | |
| # Wait for the claude to become idle, and execute a command. | |
| # Retry every 5 seconds. | |
| # Execute the claude command N times. | |
| PANE="${1:-}" | |
| # PROMPT="Continue where you left off." | |
| PROMPT="$(cat <<'EOF' | |
| Continue writing the previous response from where you left off. | |
| 1. Do not repeat any part of the previous response. | |
| 2. Continue the response exactly as it would have continued, without any changes in style or content. | |
| 3. Ensure the continuation flows naturally from the previous text. | |
| EOF | |
| )" | |
| N="100" | |
| # ---------------------------- | |
| # pane_full_command <pane> | |
| # ---------------------------- | |
| # Prints the full command executing from that pane (best-effort). | |
| # If pane doesn't exist -> returns exit 3. | |
| # If no command is running -> prints empty line and returns 0. | |
| pane_full_command() { | |
| local pane="${1:-}" | |
| if [[ -z "$pane" ]]; then | |
| echo "Usage: pane_full_command [pane identifier (session:0.0 or %2)]" >&2 | |
| return 1 | |
| fi | |
| if ! command -v tmux &>/dev/null; then | |
| echo "tmux command not found." >&2 | |
| return 2 | |
| fi | |
| # Validate pane exists | |
| if ! tmux list-panes -t "$pane" &>/dev/null; then | |
| echo "No pane detected" >&2 | |
| return 3 | |
| fi | |
| # tmux display is fuzzy if target doesn't exist, so we already validated. | |
| local pane_pid | |
| pane_pid="$(tmux display -pt "$pane" '#{pane_pid}')" | |
| if [[ -z "$pane_pid" ]]; then | |
| echo "No pane detected" >&2 | |
| return 3 | |
| fi | |
| # Find the direct child process of the pane PID and print its full command. | |
| # Using command= for macOS compatibility. | |
| # Note: if the pane is running a shell that execs other processes, this is best-effort. | |
| ps -e -o ppid= -o command= \ | |
| | awk -v ppid="$pane_pid" '$1==ppid { $1=""; sub(/^ /,""); print; exit }' | |
| } | |
| # ---------------------------- | |
| # helpers: capture last lines | |
| # ---------------------------- | |
| strip_ansi() { | |
| # Remove common ANSI escape sequences | |
| sed $'s/\x1b\\[[0-9;]*[[:alpha:]]//g' | |
| } | |
| pane_last_lines() { | |
| local pane="$1" | |
| local n="${2:-200}" | |
| tmux capture-pane -ep -t "$pane" | strip_ansi | tail -n "$n" | |
| } | |
| # ---------------------------- | |
| # is_pane_running_claude <pane> | |
| # ---------------------------- | |
| is_pane_running_claude() { | |
| local pane="$1" | |
| local cmd | |
| # If pane doesn't exist, treat as "not running" | |
| if ! cmd="$(pane_full_command "$pane" 2>/dev/null)"; then | |
| return 1 | |
| fi | |
| # Empty -> no running command | |
| [[ -n "${cmd//[[:space:]]/}" ]] || return 1 | |
| # Match 'claude' token in the command line | |
| echo "$cmd" | grep -Eq '(^|[[:space:]/])claude([[:space:]]|$)' | |
| } | |
| # ---------------------------- | |
| # is_pane_claude_working <pane> | |
| # ---------------------------- | |
| # Detects Claude "working" status line style: | |
| # One of | |
| # ✳ Improving… | |
| # ✳ Thinking… (.. tokens ..) | |
| # ✳ ..ing something… | |
| # 1) Starts with icon and space | |
| # 2) next word ends with "ing" optionally followed by … | |
| # 3) One of the words in the line should end with … | |
| is_pane_claude_working() { | |
| local pane="$1" | |
| local lines | |
| lines="$(pane_last_lines "$pane" 40)" | |
| # 1) starts with icon + space + first word ends with "ing" (optionally followed by …) | |
| echo "$lines" | grep -Eq '^[✽✻✢✳✶·] [[:alpha:]-]+ing…?([[:space:]]|$)' || return 1 | |
| # 2) somewhere in the line, a word ends with … | |
| echo "$lines" | grep -Eq '[^[:space:]]+…([[:space:]]|$)' || return 1 | |
| return 0 | |
| } | |
| # ---------------------------- | |
| # is_pane_running_claude_idle <pane> | |
| # ---------------------------- | |
| # idle = Claude process in pane AND not currently showing "working" line | |
| is_pane_running_claude_idle() { | |
| local pane="$1" | |
| is_pane_running_claude "$pane" || return 1 | |
| is_pane_claude_working "$pane" && return 1 | |
| return 0 | |
| } | |
| tmux_paste_to_pane() { | |
| local pane="$1" | |
| local text="$2" | |
| local buf="${3:-tmux-longprompt}" | |
| # Load text into a named tmux buffer via stdin | |
| printf '%s' "$text" | tmux load-buffer -b "$buf" - | |
| # Paste it ( -p means “as if typed”, so it goes into the input box) | |
| tmux paste-buffer -t "$pane" -b "$buf" -p | |
| # Send Enter to submit | |
| tmux send-keys -t "$pane" Enter | |
| } | |
| # ---------------------------- | |
| # CLI demo (optional) | |
| # ---------------------------- | |
| if [[ "${1:-}" == "--demo" ]]; then | |
| PANE="${2:-}" | |
| if [[ -z "$PANE" ]]; then | |
| echo "Usage: $0 --demo <pane>" >&2 | |
| exit 1 | |
| fi | |
| echo "Full cmd: $(pane_full_command "$PANE" || true)" | |
| if is_pane_running_claude "$PANE"; then | |
| echo "Claude: running" | |
| else | |
| echo "Claude: not running" | |
| fi | |
| if is_pane_running_claude_idle "$PANE"; then | |
| echo "Claude: idle" | |
| else | |
| echo "Claude: busy (or not running)" | |
| fi | |
| fi | |
| # Validate HH:MM (hour can be 1–2 digits) | |
| if [[ "$START_TIME" != "NOW" ]]; then | |
| if [[ ! "$START_TIME" =~ ^([0-9]{1,2}):([0-9]{2})$ ]]; then | |
| echo "Error: time must be HH:MM (e.g. 02:00 or 9:22) or NOW. Got: $START_TIME" >&2 | |
| usage | |
| exit 2 | |
| fi | |
| HOUR="${BASH_REMATCH[1]}" | |
| MIN="${BASH_REMATCH[2]}" | |
| # Range checks | |
| if (( 10#$HOUR > 23 || 10#$MIN > 59 )); then | |
| echo "Error: invalid time: $START_TIME" >&2 | |
| usage | |
| exit 2 | |
| fi | |
| # Prefer GNU date if available (macOS: gdate via coreutils) | |
| date_cmd="date" | |
| if command -v gdate >/dev/null 2>&1; then | |
| date_cmd="gdate" | |
| fi | |
| calc_target_epoch() { | |
| local now_epoch today_target_epoch | |
| now_epoch="$("$date_cmd" +%s)" | |
| today_target_epoch="$("$date_cmd" -d "today ${HOUR}:$(printf '%02d' "$MIN"):00" +%s)" | |
| if (( now_epoch < today_target_epoch )); then | |
| echo "$today_target_epoch" | |
| else | |
| "$date_cmd" -d "tomorrow ${HOUR}:$(printf '%02d' "$MIN"):00" +%s | |
| fi | |
| } | |
| target_epoch="$(calc_target_epoch)" | |
| while true; do | |
| now_epoch="$("$date_cmd" +%s)" | |
| remaining=$(( target_epoch - now_epoch )) | |
| if (( remaining <= 0 )); then | |
| break | |
| fi | |
| # Sleep at most 30 min | |
| # When close (<5 min), sleep 10s for better accuracy. | |
| if (( remaining > 3600 )); then | |
| sleep_for=1800 | |
| elif (( remaining > 1800 )); then | |
| sleep_for=600 | |
| elif (( remaining > 300 )); then | |
| sleep_for=60 | |
| else | |
| sleep_for=10 | |
| fi | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Waiting... remaining ${remaining}s (sleeping ${sleep_for}s) until ${START_TIME}" | |
| sleep "$sleep_for" | |
| done | |
| fi | |
| for i in $(seq 1 "$N"); do | |
| echo "Attempt $i/$N: Waiting for Claude to become idle..." | |
| while ! is_pane_running_claude_idle "$PANE"; do | |
| sleep 5 | |
| done | |
| echo "Claude is idle. Sending command: $PROMPT" | |
| tmux_paste_to_pane "$PANE" "$PROMPT" | |
| sleep 1 | |
| tmux send-keys -t "$PANE" Enter | |
| sleep 10 | |
| done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment