aboutsummaryrefslogtreecommitdiff
path: root/claude-templates/bin
diff options
context:
space:
mode:
Diffstat (limited to 'claude-templates/bin')
-rwxr-xr-xclaude-templates/bin/agent-page48
-rwxr-xr-xclaude-templates/bin/ai154
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