feat(bin): add dt, dtach-router, claude-provider CLI scripts

- dt: dtach session manager for claude-in-dtach sessions
- dtach-router: SSH-login dashboard to resume sessions (sourced from bashrc)
- claude-provider: switch Claude Code between Anthropic and OpenRouter;
  the OpenRouter API key is read from $OPENROUTER_API_KEY at runtime and
  is never stored in the repo

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bastien Chanot 2026-05-27 18:51:04 +02:00
parent 27d47ae866
commit 6d341f2c9f
3 changed files with 221 additions and 0 deletions

63
bin/claude-provider Executable file
View File

@ -0,0 +1,63 @@
#!/bin/bash
# Switch Claude Code between Anthropic (default) and OpenRouter.
# Usage: claude-provider [anthropic|openrouter|status]
# After switching, re-source your shell or run: cpsource
#
# OpenRouter mode reads the API key from $OPENROUTER_API_KEY at source time —
# the key is NOT stored in this script or in the repo. Export it from a private,
# untracked location (e.g. ~/.bashrc.local):
# export OPENROUTER_API_KEY="<your-openrouter-key>"
PROVIDER_FILE="$HOME/.claude-provider-env"
write_anthropic() {
cat > "$PROVIDER_FILE" << 'EOF'
# Claude provider: Anthropic (default)
unset ANTHROPIC_BASE_URL
unset ANTHROPIC_API_KEY
unset ANTHROPIC_DEFAULT_SONNET_MODEL
export CLAUDE_PROVIDER="anthropic"
EOF
echo "Switched to Anthropic (default)."
echo "Run: cpsource (or restart your shell)"
}
write_openrouter() {
cat > "$PROVIDER_FILE" << 'EOF'
# Claude provider: OpenRouter
# Key comes from $OPENROUTER_API_KEY in your environment (never hardcoded here).
export ANTHROPIC_BASE_URL="https://openrouter.ai/api"
export ANTHROPIC_API_KEY="${OPENROUTER_API_KEY:?Set OPENROUTER_API_KEY in your environment to use OpenRouter}"
export CLAUDE_PROVIDER="openrouter"
export ANTHROPIC_DEFAULT_SONNET_MODEL="google/gemma-4-31b-it:free"
EOF
echo "Switched to OpenRouter."
echo "Run: cpsource (or restart your shell)"
}
show_status() {
local provider="${CLAUDE_PROVIDER:-anthropic}"
echo "Current provider: $provider"
if [ -f "$PROVIDER_FILE" ]; then
echo "Config file: $PROVIDER_FILE"
fi
}
case "${1:-status}" in
openrouter|or)
write_openrouter
;;
anthropic|an)
write_anthropic
;;
status|st)
show_status
;;
*)
echo "Usage: claude-provider [anthropic|openrouter|status]"
echo " anthropic (an) — Use Anthropic directly (default)"
echo " openrouter (or) — Use OpenRouter proxy"
echo " status (st) — Show current provider"
exit 1
;;
esac

104
bin/dt Executable file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env bash
# dt — gestionnaire de sessions claude-dans-dtach (1 session = 1 claude = 1 socket)
# Sockets dans ~/.dtach/. La creation se fait via l'alias `cc` (voir plus bas),
# pas ici : dt ne gere que listing / rattachement / kill.
#
# dt ls lister (nom, age, dossier, commande)
# dt at <nom> rattacher une session
# dt kill <nom> tuer une session
# dt --raw listing TSV (pour fzf)
# dt sock <nom> imprime le chemin du socket (utilitaire interne)
#
# Detacher une session attachee : Ctrl-\
#
# CREATION (a mettre dans ~/.bashrc) :
# dtach_claude() { dtach -c "$HOME/.dtach/${1:-claude-$(date +%H%M%S)}" -e '^\' claude; }
# alias cc='dtach_claude'
# # usage : cd ~/projets/seo && cc seo
set -uo pipefail
DTDIR="${DTACH_DIR:-$HOME/.dtach}"
mkdir -p "$DTDIR"
now=$(date +%s)
# PID(s) du process dtach pour ce socket, via la cmdline (ss ne liste pas les
# sockets dtach de maniere fiable selon la version). Matche -c/-n/-A/-N.
_pids_for_socket() {
local sock="$1"
pgrep -f -- "dtach -[cnAN] $sock" 2>/dev/null | sort -u
}
_cwd_of() { readlink -f "/proc/$1/cwd" 2>/dev/null; }
# Heure de demarrage REELLE du process (fiable, immuable), en epoch.
_starttime_of() {
local ls; ls=$(ps -o lstart= -p "$1" 2>/dev/null) || return
[ -n "$ls" ] && date -d "$ls" +%s 2>/dev/null
}
_age() {
local epoch="${1:-$now}"
# garde-fou : si epoch n'est pas numerique, age=0
[[ "$epoch" =~ ^[0-9]+$ ]] || epoch="$now"
local age="$(( now - epoch ))"
(( age < 0 )) && age=0
if (( age<60 )); then echo "${age}s"
elif (( age<3600 )); then echo "$(( age/60 ))m"
elif (( age<86400));then echo "$(( age/3600 ))h$(( (age%3600)/60 ))m"
else echo "$(( age/86400 ))j$(( (age%86400)/3600 ))h"; fi
}
# Une ligne TSV par session vivante. Nettoie les sockets morts au passage.
emit_raw() {
shopt -s nullglob
local sock name pids master cwd start
for sock in "$DTDIR"/*; do
[ -S "$sock" ] || continue
name=$(basename "$sock")
pids=$(_pids_for_socket "$sock")
if [ -z "$pids" ]; then rm -f "$sock"; continue; fi # orphelin -> menage
master=$(echo "$pids" | head -1)
cwd=$(_cwd_of "$master"); cwd="${cwd:-?}"
start=$(_starttime_of "$master"); start="${start:-$now}"
# colonnes : NOM(cache pour fzf) TAB DOSSIER TAB AGE
printf '%s\t%s\t%s\n' "$name" "${cwd/#$HOME/\~}" "$(_age "$start")"
done
}
cmd_at() {
local name="${1:?nom requis}"; local sock="$DTDIR/$name"
[ -S "$sock" ] || { echo "Session '$name' introuvable."; return 1; }
exec dtach -a "$sock" -e '^\'
}
cmd_kill() {
local name="${1:?nom requis}"; local sock="$DTDIR/$name"
[ -S "$sock" ] || { echo "Session '$name' introuvable."; return 1; }
local master; master=$(_pids_for_socket "$sock" | head -1)
[ -n "$master" ] && kill "$master" 2>/dev/null
rm -f "$sock"
echo "Session '$name' tuee."
}
cmd_sock() { echo "$DTDIR/${1:?nom requis}"; }
cmd_ls() {
local rows; rows=$(emit_raw)
if [ -z "$rows" ]; then echo "Aucune session dtach."; return 0; fi
printf '\n \033[1;36mSessions claude\033[0m\n\n'
printf ' \033[2m%-40s %s\033[0m\n' DOSSIER AGE
printf '%s\n' "$rows" | while IFS=$'\t' read -r name cwd age; do
printf ' \033[34m%-40s\033[0m \033[33m%s\033[0m\n' "$cwd" "$age"
done
printf '\n'
}
case "${1:-}" in
ls) shift; cmd_ls "$@" ;;
at) shift; cmd_at "$@" ;;
kill) shift; cmd_kill "$@" ;;
sock) shift; cmd_sock "$@" ;;
--raw) emit_raw ;;
""|-h|--help) sed -n '2,18p' "$0" | sed 's/^# \?//' ;;
*) echo "Commande inconnue: $1"; sed -n '2,18p' "$0" | sed 's/^# \?//'; exit 1 ;;
esac

54
bin/dtach-router Executable file
View File

@ -0,0 +1,54 @@
#!/usr/bin/env bash
# dtach-router — au login SSH : s'il existe des sessions claude/dtach, affiche
# le dashboard et propose d'en rejoindre une (fzf). Sinon, shell normal direct.
# NE CREE PLUS de session (la creation se fait via l'alias `cc`).
# À SOURCER depuis ~/.bashrc. Pas de set -e/-u (fuiraient dans le shell hote).
set -o pipefail
# SSH interactif uniquement ; sinon on ne fait rien.
case $- in *i*) ;; *) return 0 2>/dev/null ;; esac
DT="${DT_BIN:-dt}"
raw=$("$DT" --raw 2>/dev/null)
# Aucune session : shell normal, zero friction.
[ -z "$raw" ] && return 0 2>/dev/null
# Des sessions existent : dashboard + menu de selection.
"$DT" ls || true
out=$(printf '%s\n' "$raw" | \
fzf --with-nth=2,3 --delimiter='\t' \
--header=$'ENTER=rejoindre une session ESC=shell normal' \
--preview 'echo "Dossier : {2}"; echo "Age : {3}"' \
--preview-window=down,15% )
# ESC / selection vide : shell normal.
[ -z "$out" ] && return 0 2>/dev/null
name=$(printf '%s' "$out" | cut -f1)
dt at "$name" </dev/tty >/dev/tty 2>&1
return 0 2>/dev/null
# ─────────────────────────────────────────────────────────────
# INSTALLATION (~/.bashrc serveur), a la fin :
#
# # creation de session claude-dans-dtach
# dtach_claude() { dtach -c "$HOME/.dtach/${1:-claude-$(date +%H%M%S)}" -e '^\' claude; }
# alias cc='dtach_claude'
#
# # dashboard + reprise au login
# [ -x "$HOME/.local/bin/dtach-router" ] && source "$HOME/.local/bin/dtach-router"
#
# # rappeler le menu depuis un shell :
# alias d='source ~/.local/bin/dtach-router'
#
# chmod +x ~/.local/bin/dt ~/.local/bin/dtach-router
#
# USAGE :
# cd ~/projets/seo && cc seo -> claude dans dtach, session "seo"
# Ctrl-\ -> detache, retour au shell
# (au prochain login) -> dashboard propose "seo", ENTER pour reprendre
# ─────────────────────────────────────────────────────────────