Skip to content

Instantly share code, notes, and snippets.

@kiyoon
Last active January 31, 2026 12:47
Show Gist options
  • Select an option

  • Save kiyoon/a0e3e7c5139fddf9004e816cf5389ed9 to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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"
#!/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