diff --git a/bin/claude-provider b/bin/claude-provider new file mode 100755 index 0000000..14ba21a --- /dev/null +++ b/bin/claude-provider @@ -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="" + +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 diff --git a/bin/dt b/bin/dt new file mode 100755 index 0000000..bcc30ee --- /dev/null +++ b/bin/dt @@ -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 rattacher une session +# dt kill tuer une session +# dt --raw listing TSV (pour fzf) +# dt sock 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 diff --git a/bin/dtach-router b/bin/dtach-router new file mode 100755 index 0000000..6654346 --- /dev/null +++ b/bin/dtach-router @@ -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 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 +# ───────────────────────────────────────────────────────────── +