diff options
Diffstat (limited to 'claude-templates/bin')
| -rwxr-xr-x | claude-templates/bin/agent-page | 48 | ||||
| -rwxr-xr-x | claude-templates/bin/ai | 154 |
2 files changed, 184 insertions, 18 deletions
diff --git a/claude-templates/bin/agent-page b/claude-templates/bin/agent-page new file mode 100755 index 0000000..11a5264 --- /dev/null +++ b/claude-templates/bin/agent-page @@ -0,0 +1,48 @@ +#!/bin/bash +# agent-page — page Craig's phone over Signal, from any machine or agent runtime. +# +# Usage: agent-page <message...> +# +# The pager identity (+15045173983) is registered in velox's signal-cli, so +# velox sends directly and every other machine ssh-relays the send over the +# tailnet. The recipient is Craig's Signal account UUID — his phone number +# reads as unregistered in Signal's directory, so never page the number. +# Verified end to end 2026-07-13 (phone push confirmed). +# +# This is the AWAY channel. At his desk, use the desktop channel instead: +# notify info "Title" "Message" --persist +# See protocols.org "Paging Craig" for choosing between them. +# +# Known caveats (tracked on the rulesets Signal-pager task): velox must be +# up and reachable on the tailnet, and the signal-cli account wants a +# periodic `receive` (staleness warnings appear otherwise). +# +# Source: ~/code/rulesets/claude-templates/bin/agent-page +# Install: make -C ~/code/rulesets install + +PAGER_ACCOUNT="+15045173983" +CRAIG_UUID="b1b5601e-6126-47f8-afaa-0a59f5188fde" +VELOX_HOST="velox.tailf3bb8c.ts.net" + +if [ $# -eq 0 ]; then + echo "usage: agent-page <message...>" >&2 + exit 2 +fi + +msg="$*" + +if [ "$(uname -n)" = "velox" ]; then + signal-cli -a "$PAGER_ACCOUNT" send -m "$msg" "$CRAIG_UUID" + rc=$? +else + # printf %q hardens the message for the remote shell. + ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \ + "$VELOX_HOST" \ + "signal-cli -a $PAGER_ACCOUNT send -m $(printf '%q' "$msg") $CRAIG_UUID" + rc=$? +fi + +if [ "$rc" -ne 0 ]; then + echo "agent-page: phone page failed (velox down or unreachable?) — fall back to the desktop channel: notify info 'Page' '<message>' --persist" >&2 +fi +exit "$rc" diff --git a/claude-templates/bin/ai b/claude-templates/bin/ai index 5a806ec..cf17875 100755 --- a/claude-templates/bin/ai +++ b/claude-templates/bin/ai @@ -1,16 +1,23 @@ #!/bin/bash -# ai — Claude Code session launcher (unified aix + hey) +# ai — agent session launcher (unified aix + hey) # # Usage: -# ai Select one or more projects via fzf and open each in -# an 'ai' tmux session window (creates session if needed). +# ai Pick the agent first (claude, codex/ChatGPT, or any +# local ollama model), then select one or more projects +# via fzf; each opens in an 'ai' tmux session window. # Git-aware: fetches, annotates with ↑/↓/dirty, auto-pulls -# clean-and-behind repos before opening. +# clean-and-behind repos before opening. --runtime or +# AI_RUNTIME skips the agent pick. # # ai <dir>... Single-project mode. Opens each given directory directly # in the 'ai' session (new window or switch to existing). # Use '.' for current directory. Git prep per dir. # +# ai --runtime <rt> Launch with a different agent CLI: claude (default), +# codex, or local (codex --oss against this machine's +# ollama; model per AI_LOCAL_MODEL, default gpt-oss:120b). +# Also settable via AI_RUNTIME. +# # ai --attach Attach to the existing 'ai' session without changes. # # ai -h | --help Show this help. @@ -25,7 +32,56 @@ # would kill the script. SESSION="ai" -CLAUDE_CMD="claude" +RUNTIME="${AI_RUNTIME:-claude}" +LOCAL_MODEL="${AI_LOCAL_MODEL:-gpt-oss:120b}" + +# Map the runtime name to the agent CLI a pane launches. All three take the +# opening instructions as a positional prompt. AGENT_BIN is the binary the +# dependency check probes; AGENT_CMD is the full launch command (the local +# runtime rides codex's open-source provider against the machine's ollama — +# model per AI_LOCAL_MODEL, default gpt-oss:120b, verified on ratio's +# Strix Halo 2026-07-13). +resolve_agent_cmd() { + case "$RUNTIME" in + claude) AGENT_BIN="claude"; AGENT_CMD="claude" ;; + codex) AGENT_BIN="codex"; AGENT_CMD="codex" ;; + local) AGENT_BIN="codex"; AGENT_CMD="codex --oss --local-provider=ollama -m $LOCAL_MODEL" ;; + *) + echo "ai: unknown runtime '$RUNTIME' — valid runtimes: claude, codex, local" >&2 + exit 2 + ;; + esac +} + +# One line per launchable agent, claude first (Enter-Enter keeps the old +# muscle memory). Local models appear only when both codex (the CLI that +# drives them) and a live ollama answer; a dead server just drops the lines. +build_runtime_choices() { + command -v claude >/dev/null 2>&1 && echo "claude — Claude Code" + command -v codex >/dev/null 2>&1 && echo "codex — ChatGPT (Codex CLI)" + if command -v codex >/dev/null 2>&1 && command -v ollama >/dev/null 2>&1; then + timeout 3 ollama list 2>/dev/null | tail -n +2 | awk 'NF {print "local:" $1 " — ollama"}' + fi +} + +# Interactive runtime pick for the bare-`ai` flow. Sets RUNTIME (and +# LOCAL_MODEL for a local pick) and re-resolves the agent command. +# Returns 1 when the pick is cancelled. +pick_runtime() { + local choice + choice=$(build_runtime_choices | fzf --height=30% --reverse --prompt='agent> ') || return 1 + [ -z "$choice" ] && return 1 + case "$choice" in + claude*) RUNTIME="claude" ;; + codex*) RUNTIME="codex" ;; + local:*) + RUNTIME="local" + LOCAL_MODEL="${choice#local:}" + LOCAL_MODEL="${LOCAL_MODEL%% *}" + ;; + esac + resolve_agent_cmd +} # Run in the pane's shell just before Claude launches. `stty susp undef` clears # the tty's SIGTSTP (C-z) character for this pane only, so an accidental C-z is @@ -46,16 +102,18 @@ build_instructions() { } usage() { - sed -n '2,20p' "$0" | sed 's|^# \?||' + sed -n '2,23p' "$0" | sed 's|^# \?||' exit 0 } -for cmd in fzf tmux claude; do - if ! command -v "$cmd" &>/dev/null; then - echo "ai: $cmd is not installed" >&2 - exit 1 - fi -done +check_deps() { + for cmd in fzf tmux "$AGENT_BIN"; do + if ! command -v "$cmd" &>/dev/null; then + echo "ai: $cmd is not installed" >&2 + exit 1 + fi + done +} # ---------- shared helpers ---------- @@ -73,7 +131,7 @@ create_window() { wid=$(tmux new-window -a -t "$SESSION:{end}" -n "$name" -c "$dir" -P -F '#{window_id}') sleep 0.1 instructions=$(build_instructions "$name") - tmux send-keys -t "$wid" "${LAUNCH_PREFIX}$CLAUDE_CMD \"$instructions\"" Enter + tmux send-keys -t "$wid" "${LAUNCH_PREFIX}$AGENT_CMD \"$instructions\"" Enter echo "$wid" } @@ -87,6 +145,7 @@ maybe_add_candidate() { build_candidates() { candidates=() maybe_add_candidate "$HOME/.emacs.d" + maybe_add_candidate "$HOME/.dotfiles" if [ -d "$HOME/code" ]; then while IFS= read -r d; do maybe_add_candidate "$d" @@ -318,7 +377,7 @@ single_mode() { local instructions wid=$(tmux new-session -d -s "$SESSION" -n "$name" -c "$dir" -P -F '#{window_id}') instructions=$(build_instructions "$name") - tmux send-keys -t "$wid" "${LAUNCH_PREFIX}$CLAUDE_CMD \"$instructions\"" Enter + tmux send-keys -t "$wid" "${LAUNCH_PREFIX}$AGENT_CMD \"$instructions\"" Enter fi sort_windows @@ -378,7 +437,7 @@ multi_mode() { local instructions first_wid=$(tmux new-session -d -s "$SESSION" -n "$name" -c "$dir" -P -F '#{window_id}') instructions=$(build_instructions "$name") - tmux send-keys -t "$first_wid" "${LAUNCH_PREFIX}$CLAUDE_CMD \"$instructions\"" Enter + tmux send-keys -t "$first_wid" "${LAUNCH_PREFIX}$AGENT_CMD \"$instructions\"" Enter for entry in "${selected[@]:1}"; do dir="${entry/#\~/$HOME}" name="$(basename "$dir")" @@ -402,19 +461,78 @@ multi_mode() { attach_session } +# Print the launch command a real run would send to the pane, then exit. +# Exists for the launcher's bats tests: exercises runtime resolution and the +# opening line with no tmux or fzf involved. +print_launch_mode() { + local arg="$1" dir name + dir="$(cd "$arg" 2>/dev/null && pwd)" || { echo "ai: cannot access '$arg'" >&2; exit 1; } + if [ ! -f "$dir/.ai/protocols.org" ]; then + echo "ai: $dir has no .ai/protocols.org — not an agent-template project" >&2 + exit 1 + fi + name="$(basename "$dir")" + printf '%s "%s"\n' "$AGENT_CMD" "$(build_instructions "$name")" + exit 0 +} + # ---------- dispatch ---------- +print_launch="" +runtime_explicit="${AI_RUNTIME:+1}" +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + usage + ;; + --runtime) + [ -z "${2:-}" ] && { echo "ai: --runtime needs a value — valid runtimes: claude, codex, local" >&2; exit 2; } + RUNTIME="$2" + runtime_explicit=1 + shift 2 + ;; + --runtime=*) + RUNTIME="${1#--runtime=}" + runtime_explicit=1 + shift + ;; + --print-launch) + print_launch=1 + shift + ;; + --print-runtimes) + build_runtime_choices + exit 0 + ;; + *) + break + ;; + esac +done + +resolve_agent_cmd + +if [ -n "$print_launch" ]; then + [ $# -eq 0 ] && { echo "ai: --print-launch needs a project directory" >&2; exit 2; } + print_launch_mode "$1" +fi + case "${1:-}" in - -h|--help) - usage - ;; --attach) + check_deps attach_mode ;; "") + # Bare `ai`: pick the agent first (skipped when --runtime or AI_RUNTIME + # already chose), then the familiar project multi-select. + if [ -z "$runtime_explicit" ]; then + pick_runtime || exit 0 + fi + check_deps multi_mode ;; *) + check_deps for arg in "$@"; do single_mode "$arg" done |
