feat(@scripts): add send command for tmux session prompts

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-17 23:00:04 -07:00
parent 80b421a21f
commit 4d1175687b

View file

@ -320,6 +320,41 @@ claude_slug() {
printf %s "$1" | sed 's|[^A-Za-z0-9]|-|g'
}
# POSIX single-quote escape — safe for embedding arbitrary user-provided
# strings inside an outer single-quoted context (e.g. ssh '<remote-cmd>').
# sh_quote "" → ''
# sh_quote "hello" → 'hello'
# sh_quote "it's" → 'it'\''s'
# The classic '\'' trick: close the open quote, escape a literal ', reopen.
sh_quote() {
printf "'%s'" "$(printf %s "$1" | sed "s/'/'\\\\''/g")"
}
# Row filter for the send subcommand. Reads TSV rows on stdin
# (host \t kind \t name/slug \t detail \t [cwd]) and emits the subset
# matching <selector> with <pattern>. Disk rows are always dropped — you
# can't `tmux send-keys` to a session that isn't running.
#
# selector = all → every tmux row (pattern ignored)
# selector = host → tmux rows where col 1 == pattern
# selector = match → tmux rows where pattern (case-insensitive
# substring) appears in col 3 (session
# name slug) OR col 5 (cwd, if present)
filter_targets() {
_selector=$1
_pattern=$2
awk -F'\t' -v sel="$_selector" -v pat="$_pattern" '
BEGIN { IGNORECASE = 1 }
$2 != "tmux" { next }
sel == "all" { print; next }
sel == "host" { if ($1 == pat) print; next }
sel == "match" {
if (index(tolower($3), tolower(pat)) > 0) { print; next }
if (NF >= 5 && index(tolower($5), tolower(pat)) > 0) { print; next }
}
'
}
# Dedupe session rows (col 2 == "session") across hosts by UUID (col 3),
# keeping the row with the highest mtime (col 6). Output sorted desc by mtime.
dedupe_sessions() {
@ -1288,6 +1323,74 @@ cmd_version() {
fi
}
cmd_send() {
# Broadcast a prompt to live rclaude tmux sessions matching a selector.
# Useful for batch-prompting agents you have running across hosts.
#
# rclaude send "fix the build" → all live rclaude tmuxes
# rclaude send --on apricot "context update" → only apricot
# rclaude send --match lilith "rebase main" → tmux name or cwd matches
# rclaude send --no-enter "draft only..." → don't press Enter after
# rclaude send --dry-run "..." → show targets, don't send
_selector=all
_pattern=""
_enter=1
_dry=0
_text=""
while [ $# -gt 0 ]; do
case $1 in
--on) shift; _selector=host; _pattern=${1:-}; shift ;;
--on=*) _selector=host; _pattern=${1#--on=}; shift ;;
--match) shift; _selector=match; _pattern=${1:-}; shift ;;
--match=*) _selector=match; _pattern=${1#--match=}; shift ;;
--no-enter) _enter=0; shift ;;
--dry-run|-n) _dry=1; shift ;;
--) shift; _text=$*; break ;;
-h|--help)
cat <<EOF
usage: rclaude send [--on <host>|--match <pat>] [--no-enter] [--dry-run] <text...>
Broadcasts <text> to every live rclaude tmux session matching the selector
via 'tmux send-keys'. Disk-only sessions are skipped (can't send-keys to
something not running).
EOF
return 2 ;;
*)
if [ -z "$_text" ]; then _text=$1
else _text="$_text $1"
fi
shift ;;
esac
done
if [ -z "$_text" ]; then
echo "rclaude send: missing text. See 'rclaude send --help'" >&2
exit 2
fi
_rows=$(scan_hosts | while IFS= read -r _h; do list_tmux_on "$_h"; done)
_targets=$(printf '%s\n' "$_rows" | filter_targets "$_selector" "$_pattern")
if [ -z "$_targets" ]; then
echo "rclaude send: no matching live tmux sessions" >&2
exit 1
fi
_count=$(printf '%s\n' "$_targets" | wc -l | tr -d ' ')
printf 'rclaude send: %s target(s)\n' "$_count" >&2
printf '%s\n' "$_targets" | while IFS=$(printf '\t') read -r _h _kind _name _rest; do
printf ' → %s : %s\n' "$_h" "$_name" >&2
[ "$_dry" = "1" ] && continue
_qtext=$(sh_quote "$_text")
_qname=$(sh_quote "$_name")
if is_local "$_h"; then
tmux send-keys -t "$_name" -l "$_text"
[ "$_enter" = "1" ] && tmux send-keys -t "$_name" Enter
else
ssh -o BatchMode=yes -o ConnectTimeout=5 "$_h" \
"tmux send-keys -t $_qname -l $_qtext"
[ "$_enter" = "1" ] && \
ssh -o BatchMode=yes "$_h" "tmux send-keys -t $_qname Enter"
fi
done
}
cmd_voice() {
# `rclaude voice` — toggle / inspect the rvoice push-to-talk binding.
# rvoice itself is a separate script (bin/rvoice) driven by Hammerspoon.
@ -1362,6 +1465,7 @@ case ${1:-} in
send) shift; cmd_send "$@"; exit ;;
setup|install) shift; cmd_setup "$@"; exit ;;
voice) shift; cmd_voice "$@"; exit ;;
send) shift; cmd_send "$@"; exit ;;
-v|--version) cmd_version; exit ;;
-h|--help|help) cmd_help; exit ;;
esac